Files
3d_globe/test/stand_surface_test.rb
T
2026-09-08 11:50:32 -07:00

74 lines
3.3 KiB
Ruby

# 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