# frozen_string_literal: true require "test_helper" class StandSurfaceTest < Minitest::Test def test_fuller_blends_remove_port_creases_without_thinning_or_moving_feet [[6.0,6.0,6.0], [6.0,4.56,4.56]].each do |widths| mesh, surface, ports, ends = junction_fixture(widths) before = Marshal.load(Marshal.dump(mesh)) before_angles = port_angles(before, ports) before_volume = surface.signed_volume surface.fair_junctions! surface.validate! after_angles = port_angles(mesh, ports) assert_operator after_angles.max, :<, before_angles.max * 0.40 # A 48-sided profile has 7.5-degree facets; compare actual flat-shaded # triangle normals, allowing that discretization but not the old crease. assert_operator after_angles.max, :<, 10.0, "avoid sharp creases across the arm openings" collars = ports.map { |port| { ring: port[:collar_rings].last } } assert_operator port_angles(mesh, collars).max, :<, 1.0, "the blend must taper smoothly into the unchanged rail" assert_operator surface.signed_volume, :>, before_volume assert mesh.vertices.each_with_index.all? { |p,i| p.first(2) == before.vertices[i].first(2) }, "preserve the footprint and routes" assert before.vertices.each_with_index.all? { |p,i| p[2] > 1e-7 || mesh.vertices[i] == p }, "hold every table-contact vertex fixed" assert ends.flatten.all? { |id| mesh.vertices[id] == before.vertices[id] }, "outer collar boundaries must stay fixed" sides = surface.instance_variable_get(:@sides) assert mesh.vertices.each_with_index.all? { |p,i| (p[2]-before.vertices[i][2])*sides.fetch(i,0) >= -1e-9 }, "only add thickness" assert_equal before.triangles, mesh.triangles, "retain the connected topology" end end private def junction_fixture(widths) mesh = MoonModel::Mesh.new(name: "junction_fixture") surface = MoonModel::StandSurface.new(mesh, 48, 48) section = ->(_x,_y,q,z) { [z + (q < 0 ? 3.75 : 3.0)*q, 0.0].max } junction = { center: [0.0,0.0,3.0], section: section, ports: [] } ends = [] widths.each_with_index do |width,i| angle = i*2*Math::PI/3 port = surface.port(junction, angle, 6.0, width) junction[:ports] << port finish = { center: [18*Math.cos(angle),18*Math.sin(angle),3.0], section: section } terminal = surface.port(finish, angle+Math::PI, 0, width) surface.rail(port, terminal, width, 128, section) ends << terminal[:ring] center = mesh.vertex(finish[:center]) 48.times { |j| mesh.triangle(center, terminal[:ring][j], terminal[:ring][(j+1)%48]) } end surface.junction(junction) surface.orient! [mesh, surface, junction[:ports], ends] end def port_angles(mesh, ports) geometry = MoonModel::StandSurface.new(mesh, 48, 24) edges = geometry.edge_faces ports.flat_map do |port| # Exclude the silhouette and intentional flat-bottom edges. (2...22).map do |index| ids = [port[:ring][index],port[:ring][index+1]].minmax normals = edges.fetch(ids).map do |face,_| n = geometry.normal(mesh.triangles[face]) length = Math.sqrt(n.sum { |v| v*v }) n.map { |v| v/length } end cosine = normals[0].zip(normals[1]).sum { |a,b| a*b } Math.acos([[cosine,-1.0].max,1.0].min)*180/Math::PI end end end end