From bbfb779c19953b58854d96ee9a6b8a8a748f6d93 Mon Sep 17 00:00:00 2001 From: snowbldr Date: Wed, 15 Apr 2026 17:27:21 -0600 Subject: [PATCH 1/3] fix Screw3D SDF accuracy: octree holes, taper slope, asymmetric profiles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three bugs in Screw3D.Evaluate caused the octree marching cubes renderer to produce meshes with holes (non-manifold boundary edges). The uniform renderer was unaffected because it evaluates every cube unconditionally. One additional floating-point bug in Line2.IntersectLine was found along the way and fixed. ## Bug 1: Helical stretch overestimates distance (all thread types) **Root cause.** Screw3D maps 3D coordinates to a 2D thread profile via a helical unwrap: the angular position θ and axial position z are combined into a single linear coordinate along the pitch. This mapping is non-isometric — a unit step in 3D maps to a step of size √(1 + (lead/(2πr))²) in the unwrapped 2D space. The 2D thread SDF therefore returns a value larger than the true 3D distance. The octree renderer's isEmpty check evaluates the SDF at cube centers and skips any cube where |d| ≥ half_diagonal. The overestimated distance causes it to skip cubes that actually contain the surface, producing holes. Steeper helices (high pitch-to-radius ratio, multi-start threads) have a larger stretch factor and are affected more. **Fix.** Divide the final SDF value by the maximum singular value of the full Jacobian of the (r, θ, z) → (profileX, profileY) mapping: J = | 0 k 1 | where k = lead/(2πr) | 1 0 tan(t) | and t = taper angle JJᵀ = | 1+k² tan(t) | | tan(t) 1+tan²(t) | σ_max = √((a + c + √((a-c)² + 4·tan²(t))) / 2) where a = 1+k², c = 1+tan²(t) When taper=0 this simplifies to √(1+k²). Dividing by σ_max restores the Lipschitz-1 property: |SDF(p) - SDF(q)| ≤ |p - q|. The correction is applied to max(d0, d1) (the intersection of thread profile and length constraint) rather than to d0 alone, so that end cap surfaces (where d=0) remain at d=0 after division. The stretch factor varies with r (larger near the axis). The octree isEmpty check evaluates at cube centers, but the nearest surface may be at a smaller r where the stretch is larger. Using rEff = max(r - threadDepth, r/2) gives a conservative bound: the nearest thread surface can be at most threadDepth closer to the axis than the evaluation point. ## Bug 2: Taper slope uses atan instead of tan **Root cause.** The Evaluate method computed the taper slope as math.Atan(s.taper) instead of math.Tan(s.taper). For a taper angle in radians, tan(angle) gives the slope (rise/run), while atan(angle) gives the angle whose tangent is the input — a different operation entirely. For small angles like NPT (~1.8°), atan(x) ≈ tan(x) ≈ x, so the error was invisible (0.06%). But at larger taper angles the error is significant: taper=30° tan=0.5774 atan=0.4823 (16.5% error) This caused the thread crest to be at the wrong radius for tapered screws, and the incorrect slope fed into the Jacobian stretch correction, leaving residual holes in tapered screw meshes. **Fix.** Replace math.Atan(s.taper) with math.Tan(s.taper). The value is precomputed in the constructor as tanTaper and tan2Taper fields. ## Bug 3: SawTooth wrap discontinuity for asymmetric profiles (buttress) **Root cause.** The helical coordinate is folded into one pitch period via SawTooth(z, pitch), mapping to [-pitch/2, +pitch/2]. For symmetric profiles (ISO, ACME), the SDF is continuous across the wrap boundary because the profile is mirror-symmetric: SDF(+pitch/2) = SDF(-pitch/2). For asymmetric profiles (ANSI buttress, plastic buttress), the profile shape differs at the left vs right edge. The SDF has a discontinuity at the wrap boundary — for buttress threads, up to 0.76mm jump at the boundary vs 0.00mm for ISO. This discontinuity causes the octree to skip cubes straddling the boundary, producing holes. **Fix.** Evaluate the 2D thread profile at the current period and both adjacent periods, taking the minimum (union semantics): d0 = min(thread.Evaluate(p0), thread.Evaluate(p0 - pitch), thread.Evaluate(p0 + pitch)) Each thread tooth is geometrically identical, so the correct SDF for a periodic thread pattern is the union of all period copies. The minimum of signed distances gives union semantics: inside any copy → negative, outside all copies → positive with distance to the nearest surface. For symmetric profiles this is a no-op: the adjacent-period values are always >= the current-period value at the boundary. For asymmetric profiles it eliminates the discontinuity by ensuring the SDF always reflects the nearest thread tooth regardless of which period SawTooth maps to. ## Bug 4: Line2.IntersectLine floating point comparison **Root cause.** The collinear intersection path in IntersectLine compared interval endpoints with exact equality (x[0] == x[1]) to decide whether the overlap is a single point or a range. Floating point arithmetic can produce x[0]=0, x[1]=1e-16 for what should be a single-point intersection, causing IntersectLine to return 2 nearly-identical points instead of 1. **Fix.** Replace exact equality with tolerance comparison: math.Abs(x[0]-x[1]) <= tolerance (1e-9). ## New files **render/mesh.go** — Mesh analysis utilities: - CollectTriangles: renders an SDF3 and returns all triangles - CountBoundaryEdges: counts edges shared by only one triangle - IsWatertight: convenience check for zero boundary edges - MaxZ: finds maximum Z vertex coordinate **sdf/screw_test.go** — Unit test for taper slope (tan vs atan validation) **render/screw_test.go** — 82 subtests across 3 test functions: - Test_Screw_Watertight_Straight (33 configs) - Test_Screw_Watertight_Tapered (16 configs) - Test_Screw_EndCap_Position (33 configs) **examples/screw_assortment/** — Example rendering 44 screw configs at 300 cells (the dominant cell count across sdfx examples) covering all thread profiles, start counts, handedness, taper angles, and extreme dimensions. Outputs screws.stl with SHA1SUM verification. Layout spaces screws by max-neighbor radius so oversized configs don't crowd small ones. Low-resolution stress configs (25/50 cells) live in render/screw_test.go rather than here. **tools/stldiff/** — Cross-branch STL regression checker. main.go parses binary STLs and reports IDENTICAL (canonical hash match), MINOR (tiny float drift), or MATERIAL (real geometry change). run.sh creates detached worktrees at BASE and HEAD refs, renders every example on both, compares every produced STL, and prints a summary. Invocable via `make stldiff` from the repo root; override refs with `make stldiff BASE=x HEAD=y`. Validation for this PR (./tools/stldiff/run.sh master HEAD, 197 STLs across 76 examples): 183 IDENTICAL, 6 MINOR (same triangle counts, sub-micron bbox drift), 8 MATERIAL — all 8 MATERIAL entries are in screw-using examples (3dp_nutbolt, bolt_container, gas_cap, nutsandbolts, tapers, test), as expected from the screw SDF fix. No unexpected geometry changes in any non-screw example. Regenerated all example SHA1SUMs. Most were stale pre-existing drift (byte-level noise from sha1tool.py file ordering and renderer nondeterminism); only the 8 MATERIAL entries above reflect genuine screw SDF output changes. --- Makefile | 12 ++ examples/3dp_nutbolt/SHA1SUM | 8 +- examples/angle/SHA1SUM | 2 +- examples/arrow/SHA1SUM | 8 +- examples/axochord/SHA1SUM | 6 +- examples/axoloti/SHA1SUM | 6 +- examples/beehive/SHA1SUM | 8 +- examples/bezier/SHA1SUM | 10 +- examples/birdhouse/SHA1SUM | 2 +- examples/bjj/SHA1SUM | 6 +- examples/bolt_container/SHA1SUM | 2 +- examples/bucky/SHA1SUM | 2 +- examples/camshaft/SHA1SUM | 2 +- examples/cap/SHA1SUM | 2 +- examples/carburetor/SHA1SUM | 4 +- examples/challenge/SHA1SUM | 10 +- examples/cylinder_head/SHA1SUM | 2 +- examples/dc2test/SHA1SUM | 2 +- examples/delta/SHA1SUM | 10 +- examples/devo/SHA1SUM | 2 +- examples/draincover/SHA1SUM | 8 +- examples/drone/SHA1SUM | 4 +- examples/dust_collection/SHA1SUM | 6 +- examples/eurorack/SHA1SUM | 12 +- examples/extrusion/SHA1SUM | 4 +- examples/fidget/SHA1SUM | 12 +- examples/finial/SHA1SUM | 4 +- examples/flask/SHA1SUM | 12 +- examples/gas_cap/SHA1SUM | 2 +- examples/gears/SHA1SUM | 2 +- examples/geneva/SHA1SUM | 6 +- examples/gridfinity/SHA1SUM | 6 +- examples/gyroid/SHA1SUM | 8 +- examples/hole_patterns/SHA1SUM | 2 +- examples/holes/SHA1SUM | 2 +- examples/hollowing_stl/SHA1SUM | 2 +- examples/hpe/SHA1SUM | 7 +- examples/inlet_hood/SHA1SUM | 2 +- examples/joko/SHA1SUM | 2 +- examples/keycap/SHA1SUM | 2 +- examples/loadcell/SHA1SUM | 2 +- examples/maestro/SHA1SUM | 2 +- examples/maixgo/SHA1SUM | 2 +- examples/mcg/SHA1SUM | 2 +- examples/mesh_test/SHA1SUM | 2 +- examples/midget/SHA1SUM | 4 +- examples/monkey_hat/SHA1SUM | 2 +- examples/msquare/SHA1SUM | 6 +- examples/nordic/SHA1SUM | 4 +- examples/nutcover/SHA1SUM | 2 +- examples/nutsandbolts/SHA1SUM | 2 +- examples/offset_box/SHA1SUM | 4 +- examples/opengate/SHA1SUM | 2 +- examples/panel_box/SHA1SUM | 6 +- examples/phone/SHA1SUM | 4 +- examples/pico_cnc/SHA1SUM | 8 +- examples/picorx/SHA1SUM | 10 +- examples/pillar_holder/SHA1SUM | 2 +- examples/pipe_connectors/SHA1SUM | 14 +-- examples/pool/SHA1SUM | 2 +- examples/pottery_wheel/SHA1SUM | 6 +- examples/radio/SHA1SUM | 6 +- examples/ringnut_tool/SHA1SUM | 2 +- examples/rpi/SHA1SUM | 2 +- examples/screw_assortment/Makefile | 2 + examples/screw_assortment/SHA1SUM | 1 + examples/screw_assortment/main.go | 183 +++++++++++++++++++++++++++++ examples/servo/SHA1SUM | 2 +- examples/spiral/SHA1SUM | 2 +- examples/sprue/SHA1SUM | 2 +- examples/square_flange/SHA1SUM | 2 +- examples/tabbox/SHA1SUM | 8 +- examples/tacho_bracket/SHA1SUM | 2 +- examples/tapers/SHA1SUM | 4 +- examples/test/SHA1SUM | 82 ++++++------- examples/text/SHA1SUM | 4 +- render/mesh.go | 114 ++++++++++++++++++ render/screw_test.go | 178 ++++++++++++++++++++++++++++ sdf/line.go | 2 +- sdf/screw.go | 96 ++++++++++++--- sdf/screw_test.go | 52 ++++++++ tools/stldiff/main.go | 179 ++++++++++++++++++++++++++++ tools/stldiff/run.sh | 86 ++++++++++++++ 83 files changed, 1089 insertions(+), 221 deletions(-) create mode 100644 examples/screw_assortment/Makefile create mode 100644 examples/screw_assortment/SHA1SUM create mode 100644 examples/screw_assortment/main.go create mode 100644 render/mesh.go create mode 100644 render/screw_test.go create mode 100644 sdf/screw_test.go create mode 100644 tools/stldiff/main.go create mode 100755 tools/stldiff/run.sh diff --git a/Makefile b/Makefile index 967faec4b..85764f6d3 100644 --- a/Makefile +++ b/Makefile @@ -11,3 +11,15 @@ test: for dir in $(DIRS); do \ $(MAKE) -C $$dir $@ || exit 1; \ done + +# Cross-branch STL regression check. Renders every example at `master` +# and at HEAD, then compares STL outputs. Use before landing a change +# that touches core rendering or SDF code to confirm no unrelated +# example's geometry was disturbed. Override the base/head refs with +# e.g. `make stldiff BASE=v1.0 HEAD=feature-branch`. +BASE ?= master +HEAD ?= HEAD +stldiff: + ./tools/stldiff/run.sh $(BASE) $(HEAD) + +.PHONY: all clean hash test stldiff diff --git a/examples/3dp_nutbolt/SHA1SUM b/examples/3dp_nutbolt/SHA1SUM index de75d34a2..956be0711 100644 --- a/examples/3dp_nutbolt/SHA1SUM +++ b/examples/3dp_nutbolt/SHA1SUM @@ -1,4 +1,4 @@ -62730f847b96546e44f172cf6ff10418d2e7c768 metric_bolt.stl -b6b872684fb42428e41a7968ca44ce766ddcd431 inch_nut.stl -7692568d9d23c14b2ccd87671c32a90ed240520b metric_nut.stl -fb6b9a563e035acf30595bf26123c3aaff7ed8db inch_bolt.stl +2cfc14602640e6f7218815a411c21a68c93053c3 inch_nut.stl +72511cb417650cba23f292e02bdaca03b46272dc metric_nut.stl +9d4b5e573aae83416936799ca25166836d79312a inch_bolt.stl +cbdf62eb9d0080d6adceaee83b5a6448916bf25c metric_bolt.stl diff --git a/examples/angle/SHA1SUM b/examples/angle/SHA1SUM index 67a3e412f..46dea0903 100644 --- a/examples/angle/SHA1SUM +++ b/examples/angle/SHA1SUM @@ -1 +1 @@ -6bb50c749489c6acad9d340abd616e3c3477fd2b angle.stl +6e15dba6d73714cadec736cadb9563f7bc6a3beb angle.stl diff --git a/examples/arrow/SHA1SUM b/examples/arrow/SHA1SUM index c30c526ce..cd0596f3a 100644 --- a/examples/arrow/SHA1SUM +++ b/examples/arrow/SHA1SUM @@ -1,4 +1,4 @@ -ae5d00f9a29b0277a92650a8f8ed27e55e11c425 axes3.stl -c68ef109b8e87c2f6d070b24434b2f32fc7d163c axes1.stl -921c2e7a1856002d66d9427801b4d060b92cb0bc axes2.stl -e27c2b82a6ad881a341ab53fae2f0e513ec1c539 arrow1.stl +6aa2c8429e2698f523aad615c89a3b47b69df1a8 axes2.stl +b4ba50b3e24b6f2d994f1a261b4a88a8ade6192b axes3.stl +cf7db3eee35aff1460600223b19baa74c89e3c19 axes1.stl +2d6bc6ba3482b72868fa67825c190075ad8f5635 arrow1.stl diff --git a/examples/axochord/SHA1SUM b/examples/axochord/SHA1SUM index 2bbce5cdc..db0fd7658 100644 --- a/examples/axochord/SHA1SUM +++ b/examples/axochord/SHA1SUM @@ -1,3 +1,3 @@ -b8949b06966e37c461fe3049fa2048c339cf7deb lower.stl -e2f27841e47f74a1be3deecf551713ccbe77a92b upper.stl -e8e7ffd3272cf1971354199052aa43967e65f506 plate.dxf +69962775e80b423e3a94f6449ba254477d7f22a0 upper.stl +3865a8683916bb46852be8a39cc8fe72fd5a0115 lower.stl +03272781c811f263aaa989c7081eb7e0650b3745 plate.dxf diff --git a/examples/axoloti/SHA1SUM b/examples/axoloti/SHA1SUM index d34c593f0..2fb307bc0 100644 --- a/examples/axoloti/SHA1SUM +++ b/examples/axoloti/SHA1SUM @@ -1,3 +1,3 @@ -fdd337d5bef744b283e18539ed8e1307bd4e3fc6 panel_and_base.stl -ef1bfc7f9ff7929afe5e11cffbe14249728cb35f base.stl -8787e69b1f60d12fc3747ab7f7e9032dee8781ad panel.stl +ba8f79eb17dc6426f0542fd3a216b891821f624d panel_and_base.stl +8ec9aa4e318c1d49429e5394fccac779ead38e00 panel.stl +0e091f490bc9658d2afb7f9495e09197a29d0607 base.stl diff --git a/examples/beehive/SHA1SUM b/examples/beehive/SHA1SUM index c9fe362d2..d130e1bfa 100644 --- a/examples/beehive/SHA1SUM +++ b/examples/beehive/SHA1SUM @@ -1,4 +1,4 @@ -84e8331b9c7ea2bb7b4e3431318718a133b55605 wheel.stl -feb2edecb01188777216d15a00af662231a0ec34 antcap.stl -f395c2d4885321096c16963979bb963f7bf7add2 retainer.stl -6db55018eb7f1f7d72656d06e14972530e9b84bd reducer.stl +fd6dd8039a9528e1d55236d625f221f657cf53b6 antcap.stl +398cc07c271ce64a8632135ea127b42f59b91ee0 wheel.stl +df8a98c56e4138c9d820ea6867b75448f9358c5e retainer.stl +1c5329d9d07ebca7728cecb8418ecc3e7b5280cf reducer.stl diff --git a/examples/bezier/SHA1SUM b/examples/bezier/SHA1SUM index 01ddb934f..9fd396825 100644 --- a/examples/bezier/SHA1SUM +++ b/examples/bezier/SHA1SUM @@ -1,6 +1,6 @@ -c9ba248df3904f9946a0c310d36bed0a87b3e809 bowlingpin.stl -9f82530ef50026fcfaa6941bc21e15d1b06d66b2 shape.stl +bc60b19af39eb17b1009b491dafa4d56964aadad bowl.stl +624f8655b11249544e82fb9ca6d93a411b6bd2f2 egg1.stl adddacde85dabbb082ed4aabc756cbeeebf1e3a2 egg2.stl -1d89995aa46c3495e08993df787eb2ad803a860c vase.stl -b51816e709434b4457e038806b34880f5fc3e11e bowl.stl -3ed59663d04b5d2d86e56c3c394962a4b2b2be57 egg1.stl +3ac488440597769a4bd2ec5efad68e23dca2104f shape.stl +e06726376a6fbb77c4a81c873b3b2fbc80adb342 vase.stl +ac44df145f92f9b1d85971c4d4118e6fb2e89ab8 bowlingpin.stl diff --git a/examples/birdhouse/SHA1SUM b/examples/birdhouse/SHA1SUM index aaa339894..c9d53ed7e 100644 --- a/examples/birdhouse/SHA1SUM +++ b/examples/birdhouse/SHA1SUM @@ -1 +1 @@ -322d5fab83e578ccf5eb8b7fc74e3dc99365a393 birdhouse.stl +c0e7bc46ce0388fe950a049c4feed7134747fac0 birdhouse.stl diff --git a/examples/bjj/SHA1SUM b/examples/bjj/SHA1SUM index e81130935..04b8a34ba 100644 --- a/examples/bjj/SHA1SUM +++ b/examples/bjj/SHA1SUM @@ -1,3 +1,3 @@ -69030ed0289a09f47470f04b29264d20e40f7ea0 bushing.stl -0e64b5ac2910539fd8315cb1bf33526be6029239 plate.stl -0ddedf9cb322fb860b144c36b1cc0e5bad2f34f6 gear.stl +760353c8059792b55f4dbb0b4ada953528331354 gear.stl +339d730965fc6d1fb89bf0dabb19923fde6d08ba bushing.stl +e11fd387aa686d5c71b6a570dfb090ec2c93f1e8 plate.stl diff --git a/examples/bolt_container/SHA1SUM b/examples/bolt_container/SHA1SUM index 0a5c80fe0..82ffec59a 100644 --- a/examples/bolt_container/SHA1SUM +++ b/examples/bolt_container/SHA1SUM @@ -1 +1 @@ -cf46d9d726cc5ac8605fb80bc7a786e2630f8901 container.stl +4e35e8918b1b2fa805c096cdd78b5efa5df40a7e container.stl diff --git a/examples/bucky/SHA1SUM b/examples/bucky/SHA1SUM index f582d1e15..2ccab46a5 100644 --- a/examples/bucky/SHA1SUM +++ b/examples/bucky/SHA1SUM @@ -1 +1 @@ -7794b3356063098862ae85487169769df56cbd15 icosahedron.stl +02fa09a9707bfc966f95a64a066dbbc4980dcf89 icosahedron.stl diff --git a/examples/camshaft/SHA1SUM b/examples/camshaft/SHA1SUM index f8892329c..a4c5b87c6 100644 --- a/examples/camshaft/SHA1SUM +++ b/examples/camshaft/SHA1SUM @@ -1 +1 @@ -50c80de94a736b0f68d51479424a5228a411db8a camshaft.stl +c260bd58b718dd7be6cde73293f00ebdfe997907 camshaft.stl diff --git a/examples/cap/SHA1SUM b/examples/cap/SHA1SUM index 1cf55ece9..8ea5152b3 100644 --- a/examples/cap/SHA1SUM +++ b/examples/cap/SHA1SUM @@ -1 +1 @@ -cf4553902f2d381224be06c99f7d5689bd8d17f6 cap.stl +2d7d3e4e392d397b7ba5affd713e61eeeb647fac cap.stl diff --git a/examples/carburetor/SHA1SUM b/examples/carburetor/SHA1SUM index 05c707ecf..2670ec657 100644 --- a/examples/carburetor/SHA1SUM +++ b/examples/carburetor/SHA1SUM @@ -1,2 +1,2 @@ -f022c40c4661c7a2b0c3ace90eaf8aaba3e15858 air.stl -70e3044b059d284c335f9fa4d612ce8c032aa793 plate.stl +65981160ade147447a7ad54d18b463a20fa00c5a air.stl +8a8b66558de59c87bc94f514096534962889f6d9 plate.stl diff --git a/examples/challenge/SHA1SUM b/examples/challenge/SHA1SUM index 22a51a7c3..6a0d91a68 100644 --- a/examples/challenge/SHA1SUM +++ b/examples/challenge/SHA1SUM @@ -1,5 +1,5 @@ -19eefd74e170f626b5cfabf704ca48116d19ff24 cc16a.stl -c46acf64c98c70940845c1b93839ce00fe95167f cc18c.stl -6af88b0f55e0a7be1c0954f4e2acad4ee319f4a5 cc18b.stl -ec17b150d1c94799f3c7ec405faba8c30c89c2bf cc16b.stl -4162385649041bde9b6871869f2f6e661986be36 cc18a.dxf +f16fe753e52814fc449e7242e546964cbb331b1a cc16a.stl +b0e894f5e4daf9ed264a6b558f375b9e1837d70c cc16b.stl +c52766743c8809a84a57dba8622f43edc509b1af cc18b.stl +bb4772d8ee1aa5904a3e5dea4d7439503afdcea3 cc18c.stl +09f690d3d08e97bac663c17156a1c1541124b736 cc18a.dxf diff --git a/examples/cylinder_head/SHA1SUM b/examples/cylinder_head/SHA1SUM index 82351d16b..91f708b3c 100644 --- a/examples/cylinder_head/SHA1SUM +++ b/examples/cylinder_head/SHA1SUM @@ -1 +1 @@ -0c9dfaf4cea0e8bea14770e86fe73d6a97f33a51 head.stl +698a6cd42312597265772785d68fcd2bb7aceb11 head.stl diff --git a/examples/dc2test/SHA1SUM b/examples/dc2test/SHA1SUM index 055600c3a..48cb8c33b 100644 --- a/examples/dc2test/SHA1SUM +++ b/examples/dc2test/SHA1SUM @@ -1 +1 @@ -dee150f4f56fce0a473c82b704ca22ea2b2d62eb output.dxf +1e435a651ce855fd0d0b0387aa865548ad0416e3 output.dxf diff --git a/examples/delta/SHA1SUM b/examples/delta/SHA1SUM index c7ccc6082..99d294d25 100644 --- a/examples/delta/SHA1SUM +++ b/examples/delta/SHA1SUM @@ -1,5 +1,5 @@ -97ef663a5758b7445a22144ad5c334c92e3083b4 servomount.stl -5d9f0d05b27ff33eaa94a64d19ee4eed09420c68 base.stl -f3507391bc64d4526e2e482c26ee6c7ac97ee45d platform.stl -f4cea8dc2e93a4159834718c241537c0b7670cfc rodend.stl -0543b2f185d241c96dffc733f10bb2bc74387ea5 arm.stl +9f34d7cc96e9df6e45637eaa48e3c9c63168e308 servomount.stl +9f2c059e603ff0dd17fdca573d09a9a0c697e157 arm.stl +9760406667b09dcb6e555a39cbe25f3ea3f7dc46 platform.stl +167279f5c471a9a2a603796485f0f2472b791023 base.stl +fa1853678241ab065da0e57ac4148a91ff6961fc rodend.stl diff --git a/examples/devo/SHA1SUM b/examples/devo/SHA1SUM index ccb042a33..7c612cdb5 100644 --- a/examples/devo/SHA1SUM +++ b/examples/devo/SHA1SUM @@ -1 +1 @@ -5baf90c8014d247e1c39cdea33b088a8df0d5c47 energy_dome.stl +83aa46ae352b1cbb22d890f15369cb5656517c78 energy_dome.stl diff --git a/examples/draincover/SHA1SUM b/examples/draincover/SHA1SUM index 8d49de6ea..6028de9a7 100644 --- a/examples/draincover/SHA1SUM +++ b/examples/draincover/SHA1SUM @@ -1,4 +1,4 @@ -61b8572f71a7a0023e797ae2e1a85520f203f2fa drain6.stl -43c77edc723d281660a542d09b75b33911c9be4b vent2.stl -39404c947113643c8ebd27c84dc3a8a34513798c drain12.stl -393f550896847c97efc2756b635f8b9ae18a0209 drain4.stl +892b04f7b9447846e2b967bd560f7d35e8b4274c vent2.stl +4f5592ab08d454ab6805a7734251a1032c1dbaff drain6.stl +7c14b4d50e55658b00d57a36c15ab2985098b287 drain4.stl +3ead10c9a1e22693a42d1182eeb988403d575810 drain12.stl diff --git a/examples/drone/SHA1SUM b/examples/drone/SHA1SUM index 3a36758ed..8b02391c3 100644 --- a/examples/drone/SHA1SUM +++ b/examples/drone/SHA1SUM @@ -1,2 +1,2 @@ -8cb1b6d115b4a98c3f23d8bf92b1a5cf93db2076 socket.stl -cce17e34e80a8324403df0b20728ad2cb967e7e6 arm.stl +3c845c0c5ca0d635de38a5d091bc263924b5d54c arm.stl +7461960f28509c6dc4f5716164c31f0fceb0a46c socket.stl diff --git a/examples/dust_collection/SHA1SUM b/examples/dust_collection/SHA1SUM index 2d8f2795e..7ec1886a7 100644 --- a/examples/dust_collection/SHA1SUM +++ b/examples/dust_collection/SHA1SUM @@ -1,3 +1,3 @@ -5a26014d78ea3aaae3fbd954d4beeea1977b3cd8 mvh25_mpvc.stl -bea8fa61ef57965207486603da1b9e494e359317 fdd_mpvc.stl -f460f403225b08cde92414e0f9628ef03effcd5c fdd_fvh25.stl +984cff4aba61a54e0f08ffb14b3560406fed60db fdd_mpvc.stl +4444122bf47971bf11a4245434c446826bef82a4 mvh25_mpvc.stl +9cfa522157f1351eaf142c44cd2ac9360a823349 fdd_fvh25.stl diff --git a/examples/eurorack/SHA1SUM b/examples/eurorack/SHA1SUM index 227fe5c62..9def216e5 100644 --- a/examples/eurorack/SHA1SUM +++ b/examples/eurorack/SHA1SUM @@ -1,6 +1,6 @@ -7882f266bf3fcc23a2ed182ea5bc714a3807e150 psu_mount.stl -d564aa123ace80c138337110f8e13f9463ee21a5 pwr_mount.stl -78ecb6999434e8a2f5461746dc60b3cc1881be9e ar_panel.stl -68167fd3dd643898752fa9db919a54f7a145da14 bb_panel.stl -3fb5e0e24fbee00b087ed0cf03d16a9cd224ea31 pwr_panel.stl -4d787958166ca82c714c0ab268daf2fc4199c12a pwr_panel_routing.stl +f44467d40791e51bf136dce632292962f4806d24 psu_mount.stl +70db01e1c4b61eaf847a53cb828ab70402a175e2 pwr_panel_routing.stl +2f0b66a758fecbd138d329849a6fcd76e0e30d33 pwr_panel.stl +97486eb990d0d0627d2acfc921e6f18edfde7a40 pwr_mount.stl +7991f9c83e5a323b5ab8a8c885ddf4db19ecd38c bb_panel.stl +0dda4526b038c0e4b5c055e5c5c2d4a3eaf1bfde ar_panel.stl diff --git a/examples/extrusion/SHA1SUM b/examples/extrusion/SHA1SUM index 5ec2a1551..e5becdca7 100644 --- a/examples/extrusion/SHA1SUM +++ b/examples/extrusion/SHA1SUM @@ -1,2 +1,2 @@ -6a8cc1a21d2b2d43bed7ee1ed38081d868f65be6 extrude2.stl -251f7fa0df5a1f16857c4ffb8fde55348c2b3dff extrude1.stl +eff84e7a47647642364e05281f9f965db4cfe9cd extrude2.stl +c1135247694e6f853dab11ab1eb7c62554a7291e extrude1.stl diff --git a/examples/fidget/SHA1SUM b/examples/fidget/SHA1SUM index 557ca771c..f3234bf68 100644 --- a/examples/fidget/SHA1SUM +++ b/examples/fidget/SHA1SUM @@ -1,6 +1,6 @@ -9a5bd09c2d908028445a838beea1e10d1b57b170 cap_double_female.stl -75160f66179c8669ebad5938e981f7ba4c7db417 cap_single.stl -9e9f5b74d30f704eff968b7a861804cee2ebb219 body2.stl -08391b73c438609c5e9e8e7ffbc4279383f5320a cap_double_male.stl -3b655d9d212ff0643cd37a30f713739efe2eb133 body1.stl -535b7b8a297996ae16da4d76ed2adc9cfb353e8e washer.stl +3f89f6be239637ba5729677960f99a8ea43167c5 cap_double_male.stl +188db987067420ace393933c7fdc4e01af61bf35 cap_double_female.stl +8a97acd27677c5bfc1f50d4e49e43032fe221e6c body2.stl +5722fad6fe86022421f5ac7e4367446724e8602a cap_single.stl +6b5aac156b1162cd4a0210189e0848389cc4f137 body1.stl +cd9e1b40bcfd7c0e7ab87aef25e98e78d3509150 washer.stl diff --git a/examples/finial/SHA1SUM b/examples/finial/SHA1SUM index 94c71126a..c1d1c8e45 100644 --- a/examples/finial/SHA1SUM +++ b/examples/finial/SHA1SUM @@ -1,2 +1,2 @@ -a7084874984ecddd96c14f47d1a82c94cd30779d f1.stl -7c6ded3a75f97d46be86e449fd8e62f6b99a4051 f2.stl +5078fe034c2a1ac515491efb451e8d52a9920f82 f1.stl +4eb43d9ed32883b97a610b15b88154e4b8478249 f2.stl diff --git a/examples/flask/SHA1SUM b/examples/flask/SHA1SUM index 31aed9b51..410bc498c 100644 --- a/examples/flask/SHA1SUM +++ b/examples/flask/SHA1SUM @@ -1,6 +1,6 @@ -2ee697f28873349477261d23b0e80d6d6b10d543 flask_250.stl -76f19c5797eaf11cbb31e9a51e0f996b7dc68d8e flask_150.stl -3d5b0437f8f66101d2af4a545e42f903063b70d7 odd_side.stl -8607cb0bcaeff18dae6f0a248fd8b28a48030b7f flask_200.stl -81b4bcfca85f4a7ae0e7f5417fdc6ca83e118e50 flask_300.stl -db1261c6c479f6b0a61e1bcca1ebfffbf9300329 pins.stl +0d144d415bd1c304c6a7a930b86e717bc6149538 flask_250.stl +138662ec3599cae5a9f20076a8c95abb945169bd flask_150.stl +4260ef35a1cbc9ed4a393aab1ca4ea20a07a1579 pins.stl +28262e0051ec5237ae26259a998657223eff75f7 flask_200.stl +3fe89e95af1993dc4574b5f916f65111897fac96 flask_300.stl +f57a9bac30cd05d46e709c053e6af5d806597ac1 odd_side.stl diff --git a/examples/gas_cap/SHA1SUM b/examples/gas_cap/SHA1SUM index 1eb8adda8..c4ad22176 100644 --- a/examples/gas_cap/SHA1SUM +++ b/examples/gas_cap/SHA1SUM @@ -1 +1 @@ -ed1b97a8196c354418b1a286358d9b0673f9ea74 cap.stl +ea12a2ed62e0404c5e90367a794347e9be59c896 cap.stl diff --git a/examples/gears/SHA1SUM b/examples/gears/SHA1SUM index 626a30b4c..14ef52da5 100644 --- a/examples/gears/SHA1SUM +++ b/examples/gears/SHA1SUM @@ -1 +1 @@ -170f81e55b74b24995ac4f38e4eebba385b12020 gear.stl +9e2d4908749580432703c07646dd4c28c21a4dfd gear.stl diff --git a/examples/geneva/SHA1SUM b/examples/geneva/SHA1SUM index ac1740c4f..2e91318cd 100644 --- a/examples/geneva/SHA1SUM +++ b/examples/geneva/SHA1SUM @@ -1,3 +1,3 @@ -296e89913ae93e4a7bc016cbfe98fb3ee475b1b0 driver.stl -a631a9bb063e8128ec3c16118d69816d9cae9ce3 geneva.stl -095429735fe9f68a0e84ac95938c26c2fa72ef60 driven.stl +2649cadaac9b697f98ae0bbd119bb16c9a76c439 driver.stl +fdf0d70d32fa4056856efdaf669226c0387cd6cc driven.stl +b18706de7733a247885ed2995ab6991705f7fbe2 geneva.stl diff --git a/examples/gridfinity/SHA1SUM b/examples/gridfinity/SHA1SUM index 5680ff8a3..ef63e5175 100644 --- a/examples/gridfinity/SHA1SUM +++ b/examples/gridfinity/SHA1SUM @@ -1,3 +1,3 @@ -55d9c91181b8f0fbd9a87c5b00b470ab57187e6b base_4x4.stl -ba2f2d8c9950c2a5dca960ab476c24d05db2afa5 body_1x1x3.stl -26b282a6f550b7a552b75c735fb285b44a577ffc body_1x2x1.stl +30d49a3379941e634f7e96d4e2fdd1b879681138 body_1x2x1.stl +fe21ad97b1846a22fddc37ace76d98c56cc7fedf base_4x4.stl +8f13ba46ac7e05f53c2048464316b93ecb9fc83e body_1x1x3.stl diff --git a/examples/gyroid/SHA1SUM b/examples/gyroid/SHA1SUM index b6b029aa5..c8c2a538e 100644 --- a/examples/gyroid/SHA1SUM +++ b/examples/gyroid/SHA1SUM @@ -1,4 +1,4 @@ -f8ee93ac572b2f5cb6f9809c3af0d6fb7036b350 gyroid_teapot.stl -2df88d6594aec5f8258ba4157d09185234356322 gyroid_cube.stl -f8ee93ac572b2f5cb6f9809c3af0d6fb7036b350 gyroid_teapot_mesh.stl -6b7f3136512def0513e84e1561e4c180b1c6accd gyroid_surface.stl +d0b97eecc4afe7b1656ce89deed94f2128930b60 gyroid_teapot.stl +62092daf78fdadc26497632ba1a1994b88a19c01 gyroid_surface.stl +a0f0df27e19562db3390158734aeda8abff29ce1 gyroid_cube.stl +d0b97eecc4afe7b1656ce89deed94f2128930b60 gyroid_teapot_mesh.stl diff --git a/examples/hole_patterns/SHA1SUM b/examples/hole_patterns/SHA1SUM index ee0b8cad3..0df953e40 100644 --- a/examples/hole_patterns/SHA1SUM +++ b/examples/hole_patterns/SHA1SUM @@ -1 +1 @@ -32ec3b01f08847b08586caef7be43091212760b9 holes.dxf +2f6b9a865e01286d0348fbb51462fa792a38613e holes.dxf diff --git a/examples/holes/SHA1SUM b/examples/holes/SHA1SUM index 0b461953a..7a6612efe 100644 --- a/examples/holes/SHA1SUM +++ b/examples/holes/SHA1SUM @@ -1 +1 @@ -d2784c9846d50e7efeed831fa099659b13668a41 test_holes.stl +7117424bff60639d1411aaeec5d1525b8318d4e4 test_holes.stl diff --git a/examples/hollowing_stl/SHA1SUM b/examples/hollowing_stl/SHA1SUM index a0ce6dcbb..b91110e9e 100644 --- a/examples/hollowing_stl/SHA1SUM +++ b/examples/hollowing_stl/SHA1SUM @@ -1 +1 @@ -875dd9994d5a2dc95dfb3ac85228b5caf27516c1 inside-carved-out.stl +85cc33f5171d73955a4a5d6326e65025865a4df7 inside-carved-out.stl diff --git a/examples/hpe/SHA1SUM b/examples/hpe/SHA1SUM index e0e4fb2c9..af063c362 100644 --- a/examples/hpe/SHA1SUM +++ b/examples/hpe/SHA1SUM @@ -1,3 +1,4 @@ -3684e54340bd7d1d223fe2ddf5c8b76cc4e158ed ap725.stl -c05d996c6bd7eec45b73ff314af535973cdd58e4 ap723h.stl -cfb33fac10848c216685087ba042ec3d0bdaaa52 ap745.stl +cc4fae3a0fedc3ec280c8f2d19c6363cf57ce544 ap723h.stl +e2816ba152aa300635c59dcd402b10f3bbe05b68 ap745.stl +ac4a5164135f2379d29dadf8edddf8f9b2047d4c ap723h_support.stl +f34aedf6334b712e2c9005df6e3c60176157a083 ap725.stl diff --git a/examples/inlet_hood/SHA1SUM b/examples/inlet_hood/SHA1SUM index 4c0652013..5b8b595c7 100644 --- a/examples/inlet_hood/SHA1SUM +++ b/examples/inlet_hood/SHA1SUM @@ -1 +1 @@ -f23a71c6858b8a2532786d579a14cb7c43a73b24 hood.stl +d88c0bae4c26b6c15da229490c5de564bafd7b34 hood.stl diff --git a/examples/joko/SHA1SUM b/examples/joko/SHA1SUM index 5dc3fd269..b4ca25754 100644 --- a/examples/joko/SHA1SUM +++ b/examples/joko/SHA1SUM @@ -1 +1 @@ -65a4b51616cc641f60a4d10ea8da28dff9173c2d part.stl +21e0817c8e33d32f43ae88cff813ef17215ef7ec part.stl diff --git a/examples/keycap/SHA1SUM b/examples/keycap/SHA1SUM index 5baca1ed1..be28b4ee5 100644 --- a/examples/keycap/SHA1SUM +++ b/examples/keycap/SHA1SUM @@ -1 +1 @@ -ebb8225b51eb566d2521c235eaae4faf9e6f82bc round_cap.stl +728f39761ba3670e5baab5432882849bda925dcf round_cap.stl diff --git a/examples/loadcell/SHA1SUM b/examples/loadcell/SHA1SUM index 14170d909..deb14b2e0 100644 --- a/examples/loadcell/SHA1SUM +++ b/examples/loadcell/SHA1SUM @@ -1 +1 @@ -916dd7c097a641db5f550ccb7b4a34b52fe15e0a holder.stl +d93ddfbcbcb89806218964137660ef90741d38c7 holder.stl diff --git a/examples/maestro/SHA1SUM b/examples/maestro/SHA1SUM index a269219b3..8540c726f 100644 --- a/examples/maestro/SHA1SUM +++ b/examples/maestro/SHA1SUM @@ -1 +1 @@ -3612ad2a8f95bd3877c51f989626165299794189 mm18.stl +41f1579f616ffe262d6f7ab8235fad2e9f6be0f6 mm18.stl diff --git a/examples/maixgo/SHA1SUM b/examples/maixgo/SHA1SUM index 8d5ce7819..c8b7b191d 100644 --- a/examples/maixgo/SHA1SUM +++ b/examples/maixgo/SHA1SUM @@ -1 +1 @@ -7d9cbed1270d525c6b6d2b0ca90e4e24f39d9c97 bezel.stl +63ff6a95256a7fad5c05033f05cdea8c34090e75 bezel.stl diff --git a/examples/mcg/SHA1SUM b/examples/mcg/SHA1SUM index 08a03988c..65c8c7f89 100644 --- a/examples/mcg/SHA1SUM +++ b/examples/mcg/SHA1SUM @@ -1 +1 @@ -3a827b3342c857a7f216a5a68996b47b18a1c02f mcg.stl +b2f71caa62ce75151b24a9322493cd7b51de26a8 mcg.stl diff --git a/examples/mesh_test/SHA1SUM b/examples/mesh_test/SHA1SUM index 49c77a6db..64218d889 100644 --- a/examples/mesh_test/SHA1SUM +++ b/examples/mesh_test/SHA1SUM @@ -1 +1 @@ -ff39b42ac5767309e2ab9d81beb6b98665c81293 test.dxf +5c9cafc45232bf948a66d499c6110ec5be9e6157 test.dxf diff --git a/examples/midget/SHA1SUM b/examples/midget/SHA1SUM index 799a207e8..10c5bf0d3 100644 --- a/examples/midget/SHA1SUM +++ b/examples/midget/SHA1SUM @@ -1,2 +1,2 @@ -c5dd4356bc02b9022a27e0038234d6fc8252cd0b crankcase_front.stl -7d8d39a9ead1a1e30acf3e3917cce9e1a7d9f87e cylinder_pattern.stl +c72a64733d1ca328eaa97ca2bdba0f879ccbaa56 crankcase_front.stl +3c6c2ea02ecf35937937fc623be840bb33a89ad3 cylinder_pattern.stl diff --git a/examples/monkey_hat/SHA1SUM b/examples/monkey_hat/SHA1SUM index 46ff55122..d183c5e3f 100644 --- a/examples/monkey_hat/SHA1SUM +++ b/examples/monkey_hat/SHA1SUM @@ -1 +1 @@ -f7649befff327283a12099b2f71fcb68df6c1a83 monkey-out.stl +6d2a4913933a8abd14181e6d0f9c663454bb7a1b monkey-out.stl diff --git a/examples/msquare/SHA1SUM b/examples/msquare/SHA1SUM index 9e5090fa7..7046017d0 100644 --- a/examples/msquare/SHA1SUM +++ b/examples/msquare/SHA1SUM @@ -1,3 +1,3 @@ -48fd5b9b918af4e361f98ac81c9b718a2951756a ms6_upper.stl -482b7e2692f703cc7d84cb88edd183302b49459e ms6_lower.stl -ed30d6ad716af8199dd1b0b4d20151d17e383a53 ms6.stl +aed79c8c77bcac7eba63fc9bb020dff9846489bd ms6.stl +975e6f17a5aeaa4d4dc256c677a5e0dc2b3270a3 ms6_upper.stl +17bca2041941d36dbb8baa444dd695639a36c7cd ms6_lower.stl diff --git a/examples/nordic/SHA1SUM b/examples/nordic/SHA1SUM index 778962bed..b87d4b4e9 100644 --- a/examples/nordic/SHA1SUM +++ b/examples/nordic/SHA1SUM @@ -1,2 +1,2 @@ -9d554e73113456b5678aa7f1ce374f16e4ef1c64 nrf52dk.stl -2773c2b035c4dd8e490a7373d08fa0597f921b31 nrf52833dk.stl +228866d28e7cf769a0421ab2caa3719b39e43991 nrf52dk.stl +4f33a790a06b9729e6b0ba80cd0d23ed19bc584c nrf52833dk.stl diff --git a/examples/nutcover/SHA1SUM b/examples/nutcover/SHA1SUM index 8ab948f1c..e4b1cf94d 100644 --- a/examples/nutcover/SHA1SUM +++ b/examples/nutcover/SHA1SUM @@ -1 +1 @@ -ec616ec540309101ad01d025e107e937f349bbe6 cover.stl +a7f8e8ed3f74cccae1b5b29d10644160cd259c08 cover.stl diff --git a/examples/nutsandbolts/SHA1SUM b/examples/nutsandbolts/SHA1SUM index acc1748cc..d5e991730 100644 --- a/examples/nutsandbolts/SHA1SUM +++ b/examples/nutsandbolts/SHA1SUM @@ -1 +1 @@ -89aa1acd441b79ca7a773a96ca7d28f4deacf135 nutandbolt.stl +43e4ce2a96d1de88e474ed127d43fabd61f45425 nutandbolt.stl diff --git a/examples/offset_box/SHA1SUM b/examples/offset_box/SHA1SUM index 9393801c6..f1d81b261 100644 --- a/examples/offset_box/SHA1SUM +++ b/examples/offset_box/SHA1SUM @@ -1,2 +1,2 @@ -e3c7f02fb285edba70fb5a013983fddbf7500828 top.stl -8200f6f80feee18dae64c1ea0e74f5e730801a85 base.stl +311e3ab9dcc32ee1f4912c8c138aba64706859ea top.stl +ca57914e658cab5937255f20e31b1a5f688967cc base.stl diff --git a/examples/opengate/SHA1SUM b/examples/opengate/SHA1SUM index 9ffc97d18..b8e6956ad 100644 --- a/examples/opengate/SHA1SUM +++ b/examples/opengate/SHA1SUM @@ -1 +1 @@ -cc8076715f0e645b2ee127b7414d7cdefaaba92e main_board.stl +562c374b4fe5f30e1feef63671908507f7e9e910 main_board.stl diff --git a/examples/panel_box/SHA1SUM b/examples/panel_box/SHA1SUM index 4be1a5008..82737e319 100644 --- a/examples/panel_box/SHA1SUM +++ b/examples/panel_box/SHA1SUM @@ -1,3 +1,3 @@ -d98110d092bb1e3a564d7e74e40b65bd2fe1af29 top.stl -535d69110164ae7b75de5d9e354cc2c1293e07fc bottom.stl -a8db3ca28d49f0b8012c20aaa24b421dfb6049df panel.stl +ea9c0c5a86ab1c4d93ef11deb0e58548b81602ba bottom.stl +9ee3a6425b0926ed9c79d53481743391827d8a03 top.stl +f450ac3944c306eadb5d4b415aeb6d729e51eb0e panel.stl diff --git a/examples/phone/SHA1SUM b/examples/phone/SHA1SUM index 07bd2a1b7..efc02eca3 100644 --- a/examples/phone/SHA1SUM +++ b/examples/phone/SHA1SUM @@ -1,2 +1,2 @@ -9033aa45e4462fc013f6cc0a6dceda60e050bf4c clip.stl -3ab48d7cdc00764ca65c90fe80746449de7b96e7 holder.stl +da7461fe57b8c71626ef553edfc9c688754101d7 holder.stl +257974fd64f10b1de2e072e9428a897163ed588b clip.stl diff --git a/examples/pico_cnc/SHA1SUM b/examples/pico_cnc/SHA1SUM index 7bec0a0ce..4785207f3 100644 --- a/examples/pico_cnc/SHA1SUM +++ b/examples/pico_cnc/SHA1SUM @@ -1,4 +1,4 @@ -033255dd88475e5820b81998863930c0f4d538e2 pen_holder.stl -4d9d7f989da039576d40b14554ee6cad5372168a serial.stl -12a5d3b6ab2644f42ae5b2b6c8f279abd601935b pico_cnc.stl -b557553f1a0940c3a76178605501eaa306b52640 keypad_panel.stl +f2db7ac03d9274449f2d43357721db9483d38ccc keypad_panel.stl +679670d48e3a3f36106cab2fbbcda917372aaac7 serial.stl +74eba348d81e96ade37687513b875c369bb5ca96 pico_cnc.stl +ca3bac984a710630f031373d8a602cf14aeddd6a pen_holder.stl diff --git a/examples/picorx/SHA1SUM b/examples/picorx/SHA1SUM index 35d9e5bbe..ec8f6e2e6 100644 --- a/examples/picorx/SHA1SUM +++ b/examples/picorx/SHA1SUM @@ -1,5 +1,5 @@ -ebc5de4e0b35ee9c29a9671020b422f888155b0b rhs.stl -3bc39f6053b1a73573ff7dba1486727e1a029031 bezel.stl -c86c28b126c302438484f9f7b25bd6f40b33a60d lhs.stl -67a18cda7fb2c50a162c45f89c7bb0072c41755b pcb_mount0.stl -7d66353a36a46c6a465dd0ec53b08543ca0a8a65 pcb_mount1.stl +f478553062801be01a466f14c6e5ace19fe15b43 pcb_mount1.stl +a66250edc73fb7ffa78f7849aac8305238bc422a pcb_mount0.stl +aeb1e0a6d8a0d12128ea652dbf267abbc257174c lhs.stl +9520c819612262cc76e827d21d3c67e0f5366498 rhs.stl +7575d01481335cdc503f4d042ee33a5198401e57 bezel.stl diff --git a/examples/pillar_holder/SHA1SUM b/examples/pillar_holder/SHA1SUM index eee5cc108..638ee8f7d 100644 --- a/examples/pillar_holder/SHA1SUM +++ b/examples/pillar_holder/SHA1SUM @@ -1 +1 @@ -52e30172cc63e86759bc67b1a23ee9daf8d3b4be holder.stl +e0b3d2776edd0fe929f286c5c8283b189a437123 holder.stl diff --git a/examples/pipe_connectors/SHA1SUM b/examples/pipe_connectors/SHA1SUM index fa84f8373..b39bff087 100644 --- a/examples/pipe_connectors/SHA1SUM +++ b/examples/pipe_connectors/SHA1SUM @@ -1,7 +1,7 @@ -95d6a38a0d672c6552cc77f40681348455b5dac6 pipe_connector_3a.stl -4ad6edcfaab468408021848841ece53669e421c9 pipe_connector_5a.stl -c108916ffeb0e2e00d285a5a3f206c2ecc782392 pipe_connector_4b.stl -cd1f5fa4443691a6bd7c03267f0f4475f065b6fd pipe_connector_4a.stl -33bfd9b569ca66648a191c3f711d6ff6541403f2 pipe_connector_2a.stl -18f46c177d4759d51e98070ff08bfc92faf5d052 pipe_connector_3b.stl -09db51ea4ecf3a9c2a3c3e4101068694994c5749 pipe_connector_2b.stl +daa6a28348413f2c4922b59f63e52c0924dcc7d3 pipe_connector_3a.stl +0b1e252a5cf7c1f6585e8e0b9553b48fb8fecc9c pipe_connector_3b.stl +2daa7e6ff219ea5f82132ff2b3b76d070aaaf434 pipe_connector_2b.stl +37d397a64c85a77192353f5c38290b057db7776d pipe_connector_2a.stl +0bad8e7be66d59c24a45b3e4cc5a829e98bb23ee pipe_connector_5a.stl +cbc0047ea2750f099a3b8f1419becc4127c837f7 pipe_connector_4a.stl +b7a2de6eb370f7dab2d214aa6d4c95223bb6e490 pipe_connector_4b.stl diff --git a/examples/pool/SHA1SUM b/examples/pool/SHA1SUM index b5e889227..f26283022 100644 --- a/examples/pool/SHA1SUM +++ b/examples/pool/SHA1SUM @@ -1 +1 @@ -7dff183b3fafdb8540d8369b20915f9349e01a94 pool1.stl +9bdfd1d51deb6d539b4d5e6975ab96541e7fdf55 pool1.stl diff --git a/examples/pottery_wheel/SHA1SUM b/examples/pottery_wheel/SHA1SUM index 8d5fdbe7e..286ed38c7 100644 --- a/examples/pottery_wheel/SHA1SUM +++ b/examples/pottery_wheel/SHA1SUM @@ -1,3 +1,3 @@ -4980cc758dc2569b7d5bb74314e0e95a944f8cd5 wheel.stl -0568472581e3833567bda218a768e4f00f51f31c core_box.stl -d78b394cdced7576787ea604a0e24a46eab8e1e2 wheel.dxf +7eb765113540c672839e06bc381917a7bc06cfb5 wheel.stl +b680f974d424b1dca0425da72542c31402632645 core_box.stl +c35b30ccabb7f0f311d5a2aaa60f49d47531f0ab wheel.dxf diff --git a/examples/radio/SHA1SUM b/examples/radio/SHA1SUM index 692888bfb..b73416faa 100644 --- a/examples/radio/SHA1SUM +++ b/examples/radio/SHA1SUM @@ -1,3 +1,3 @@ -27aab2d05eb39290fedce423bbef0294eca13ca3 vc_mount.stl -d4358341055f3514cbf1aada26da8e4dad7f0693 vc_knob.stl -f45c689c7c34333a7a5d5f1b6eae8011cb23f8a8 fr_mount.stl +eaf460aaee16a3776925893905b6f49323941612 fr_mount.stl +1e3162621df8091667c643d1b71d7a7d3d3d21a4 vc_mount.stl +9c51b103110335d909204374c37102de3b791ee0 vc_knob.stl diff --git a/examples/ringnut_tool/SHA1SUM b/examples/ringnut_tool/SHA1SUM index 7fb8056b0..15ee03ea2 100644 --- a/examples/ringnut_tool/SHA1SUM +++ b/examples/ringnut_tool/SHA1SUM @@ -1 +1 @@ -077cfbc13946409efecb92385474631926fb3582 tool.stl +c97a6e640a05e44aba6808cc692254e10f82b3bb tool.stl diff --git a/examples/rpi/SHA1SUM b/examples/rpi/SHA1SUM index 35718860f..12e6caaa3 100644 --- a/examples/rpi/SHA1SUM +++ b/examples/rpi/SHA1SUM @@ -1 +1 @@ -bf655073bd79cd23dfbf4b15bdf3f155125b0825 display_stand.stl +f1060ddaa3ec90b6094f99610fb2b6a7d0fdd116 display_stand.stl diff --git a/examples/screw_assortment/Makefile b/examples/screw_assortment/Makefile new file mode 100644 index 000000000..d6053c1f9 --- /dev/null +++ b/examples/screw_assortment/Makefile @@ -0,0 +1,2 @@ +TOP = ../.. +include $(TOP)/mk/example.mk diff --git a/examples/screw_assortment/SHA1SUM b/examples/screw_assortment/SHA1SUM new file mode 100644 index 000000000..5b55f3f03 --- /dev/null +++ b/examples/screw_assortment/SHA1SUM @@ -0,0 +1 @@ +d40749406d577ed45cb2048ade68a068ebf9ebf7 screws.stl diff --git a/examples/screw_assortment/main.go b/examples/screw_assortment/main.go new file mode 100644 index 000000000..90310b612 --- /dev/null +++ b/examples/screw_assortment/main.go @@ -0,0 +1,183 @@ +// Screw assortment: renders an array of screw configurations covering all +// thread profiles (ISO, ACME, buttress, plastic-buttress), multi-start, +// left-hand, tapered, and extreme dimensions. Verifies all octree-rendered +// meshes are watertight (zero boundary edges). +// +// Watertightness at extreme/low-resolution configurations is also covered +// by the unit tests in render/screw_test.go; this example focuses on a +// clean visual showcase of the full Screw3D configuration space. +// +// Usage: +// +// go run main.go +package main + +import ( + "fmt" + "log" + + "github.com/deadsy/sdfx/render" + "github.com/deadsy/sdfx/sdf" +) + +const meshCells = 300 + +type config struct { + name string + radius, pitch, length float64 + taperDeg float64 + starts int + profile string // "iso", "iso-int", "acme", "buttress", "plastic-buttress" +} + +func makeThread(c config) sdf.SDF2 { + var thread sdf.SDF2 + var err error + switch c.profile { + case "iso-int": + thread, err = sdf.ISOThread(c.radius, c.pitch, false) + case "acme": + thread, err = sdf.AcmeThread(c.radius, c.pitch) + case "buttress": + thread, err = sdf.ANSIButtressThread(c.radius, c.pitch) + case "plastic-buttress": + thread, err = sdf.PlasticButtressThread(c.radius, c.pitch) + default: + thread, err = sdf.ISOThread(c.radius, c.pitch, true) + } + if err != nil { + log.Fatal(err) + } + return thread +} + +func buildScrew(c config) sdf.SDF3 { + thread := makeThread(c) + taperRad := sdf.DtoR(c.taperDeg) + screw, err := sdf.Screw3D(thread, c.length, taperRad, c.pitch, c.starts) + if err != nil { + log.Fatal(err) + } + return screw +} + +func allConfigs() []config { + return []config{ + // --- Straight screws --- + // ISO external + {"M10x2", 5, 2, 20, 0, 1, "iso"}, + {"M5x3", 2.5, 3, 10, 0, 1, "iso"}, + {"coarse_M10x5", 5, 5, 20, 0, 1, "iso"}, + {"steep_M3x3", 1.5, 3, 10, 0, 1, "iso"}, + {"extreme_M2x3", 1.0, 3, 6, 0, 1, "iso"}, + {"short_1pitch", 5, 2, 2, 0, 1, "iso"}, + // Multi-start + {"dual_start", 5, 2, 20, 0, 2, "iso"}, + {"triple_start", 5, 2, 20, 0, 3, "iso"}, + {"multi8", 5, 2, 20, 0, 8, "iso"}, + {"multi16", 5, 2, 20, 0, 16, "iso"}, + {"multi8_coarse", 5, 5, 20, 0, 8, "iso"}, + // Left-hand + {"left_M10x2", 5, 2, 20, 0, -1, "iso"}, + {"left_8start", 5, 2, 20, 0, -8, "iso"}, + {"left_steep_M3x3", 1.5, 3, 10, 0, -1, "iso"}, + // ISO internal + {"internal_M10x2", 5, 2, 20, 0, 1, "iso-int"}, + {"internal_M5x3", 2.5, 3, 10, 0, 1, "iso-int"}, + // ACME + {"acme_M10x2", 5, 2, 20, 0, 1, "acme"}, + {"acme_steep_M5x3", 2.5, 3, 10, 0, 1, "acme"}, + // Buttress + {"buttress_M10x2", 5, 2, 20, 0, 1, "buttress"}, + {"buttress_steep", 2.5, 3, 10, 0, 1, "buttress"}, + {"buttress_left", 5, 2, 20, 0, -1, "buttress"}, + {"buttress_multi4", 5, 2, 20, 0, 4, "buttress"}, + // Plastic buttress + {"plastic_butt_M10", 5, 2, 20, 0, 1, "plastic-buttress"}, + {"plastic_butt_left", 5, 2, 20, 0, -1, "plastic-buttress"}, + {"plastic_butt_multi4", 5, 2, 20, 0, 4, "plastic-buttress"}, + // Fine thread + {"fine_M20x0.5", 10, 0.5, 20, 0, 1, "iso"}, + // Sub-pitch length + {"sub_pitch_len", 5, 3, 2, 0, 1, "iso"}, + + // --- Tapered screws --- + {"taper_1.8_NPT", 5, 2, 20, 1.79, 1, "iso"}, + {"taper_5", 5, 2, 20, 5, 1, "iso"}, + {"taper_15", 5, 2, 20, 15, 1, "iso"}, + {"taper_30", 5, 2, 20, 30, 1, "iso"}, + {"taper_45", 5, 2, 20, 45, 1, "iso"}, + {"taper_30_4start", 5, 2, 20, 30, 4, "iso"}, + {"taper_30_coarse", 5, 5, 20, 30, 1, "iso"}, + {"taper_15_steep", 2.5, 3, 10, 15, 1, "iso"}, + {"taper_30_left", 5, 2, 20, 30, -1, "iso"}, + {"taper_15_internal", 5, 2, 20, 15, 1, "iso-int"}, + {"taper_15_acme", 5, 2, 20, 15, 1, "acme"}, + {"taper_30_buttress", 5, 2, 20, 30, 1, "buttress"}, + {"taper_15_plastic_butt", 5, 2, 20, 15, 1, "plastic-buttress"}, + {"taper_30_left_buttress", 5, 2, 20, 30, -1, "buttress"}, + {"taper_15_multi4_buttress", 5, 2, 20, 15, 4, "buttress"}, + + // --- Oversized (placed last so it doesn't dominate the layout) --- + {"large_M64x6", 32, 6, 60, 0, 1, "iso"}, + } +} + +func main() { + configs := allConfigs() + allPassed := true + + // Lay out screws in a row along X. Gap scales with the larger of the + // neighboring radii so oversized screws don't crowd small ones. + var allTris []*sdf.Triangle3 + xOffset := 0.0 + prevR := 0.0 + + for i, c := range configs { + screw := buildScrew(c) + bb := screw.BoundingBox() + r := bb.Max.X + + tris := render.CollectTriangles(screw, render.NewMarchingCubesOctree(meshCells)) + be := render.CountBoundaryEdges(tris) + + status := "PASS" + if be > 0 { + status = "FAIL" + allPassed = false + } + + fmt.Printf("%-28s r=%5.1f p=%3.1f taper=%5.1f° starts=%3d → %6d tris, %4d boundary edges [%s]\n", + c.name, c.radius, c.pitch, c.taperDeg, c.starts, len(tris), be, status) + + if i > 0 { + gap := 3.0 + if s := max(prevR, r) * 0.5; s > gap { + gap = s + } + xOffset += gap + } + + for j := range tris { + t := tris[j] + t[0].X += xOffset + r + t[1].X += xOffset + r + t[2].X += xOffset + r + allTris = append(allTris, &t) + } + xOffset += r * 2 + prevR = r + } + + fmt.Println() + if allPassed { + fmt.Println("ALL PASSED: all octree meshes are watertight") + } else { + fmt.Println("SOME FAILED: octree meshes have holes (boundary edges)") + } + + fmt.Printf("\nrendering screws.stl (%d triangles)\n", len(allTris)) + if err := render.SaveSTL("screws.stl", allTris); err != nil { + log.Fatal(err) + } +} diff --git a/examples/servo/SHA1SUM b/examples/servo/SHA1SUM index 964297d11..a299be79a 100644 --- a/examples/servo/SHA1SUM +++ b/examples/servo/SHA1SUM @@ -1 +1 @@ -ca5c01c247dd5a480714318930ef37ba0043f194 servos.stl +96ee8a76288eeb418c11f1dfb3b521340daa018b servos.stl diff --git a/examples/spiral/SHA1SUM b/examples/spiral/SHA1SUM index 592ed60e5..abad45cb8 100644 --- a/examples/spiral/SHA1SUM +++ b/examples/spiral/SHA1SUM @@ -1 +1 @@ -e49bc93d1dbcd097e4b2b3270600a52f1c0baec5 spiral.dxf +2790f439b1e5a5faf8bdd54dad505fb9c280f529 spiral.dxf diff --git a/examples/sprue/SHA1SUM b/examples/sprue/SHA1SUM index 65e936616..d10534bf8 100644 --- a/examples/sprue/SHA1SUM +++ b/examples/sprue/SHA1SUM @@ -1 +1 @@ -55b3439fabeac744238d2052fef624d0fca9cd08 sprue.stl +0bad329ab778797007e9fdcbe28162a0dd5f347d sprue.stl diff --git a/examples/square_flange/SHA1SUM b/examples/square_flange/SHA1SUM index d85f3642d..b892a6e2f 100644 --- a/examples/square_flange/SHA1SUM +++ b/examples/square_flange/SHA1SUM @@ -1 +1 @@ -ddf54e3477b00ac380b961ee08de526cfe1c8333 flange.stl +5b2378ee028bb1c57c33b6f99ede7e7713cf0fff flange.stl diff --git a/examples/tabbox/SHA1SUM b/examples/tabbox/SHA1SUM index f24594af1..40ce7dd84 100644 --- a/examples/tabbox/SHA1SUM +++ b/examples/tabbox/SHA1SUM @@ -1,4 +1,4 @@ -f874b064006446cc8eb7530dc2da078d8d1fa92c box0_upper.stl -6c16b9628d3747b3e9092aef6e3ea1a27024809b box1_lower.stl -d868f85aae9ffa662f651cbf6181cd8e4cffc19d box0_lower.stl -015656ba1ed2562e2c50d1afae7db0c8c6da4679 box1_upper.stl +3cd3906cb72a401f42fc9773414990abb12ac664 box1_lower.stl +ee66a5dace683e110d1055cfabb31a1c6ef77055 box0_upper.stl +da12ae7aad27657e21e8445de8b2d5b22e88230f box0_lower.stl +7d532010873308f772380f69ec5e09810aa306f2 box1_upper.stl diff --git a/examples/tacho_bracket/SHA1SUM b/examples/tacho_bracket/SHA1SUM index f6a866bc6..6a5ca0fbb 100644 --- a/examples/tacho_bracket/SHA1SUM +++ b/examples/tacho_bracket/SHA1SUM @@ -1 +1 @@ -c0887daaa070e07bce4f2bc9ad82c828bd81ca79 tacho.stl +d5b8c7121609d780d3cc4204af269d13c8caa7d0 tacho.stl diff --git a/examples/tapers/SHA1SUM b/examples/tapers/SHA1SUM index 8fe55e841..6842349fb 100644 --- a/examples/tapers/SHA1SUM +++ b/examples/tapers/SHA1SUM @@ -1,2 +1,2 @@ -86e337c963e5878e7568c7c8dace8e5bf31e477e taper2.stl -f36cbf39c34dd2488c214fdf572bce8e69d77fb4 taper1.stl +e2cdd67b254aba5b6af0f8ca92d8b1b2bf8c4382 taper1.stl +df63a71f37ac87a906fa3ad7d5b7c4f16d340081 taper2.stl diff --git a/examples/test/SHA1SUM b/examples/test/SHA1SUM index 08d3dcc9d..d7e9c03b8 100644 --- a/examples/test/SHA1SUM +++ b/examples/test/SHA1SUM @@ -1,42 +1,42 @@ -11162d0b9af736ee2e8fd106a37a86709b13363a ellipsoid_egg.stl -397ef43b3d047a16623c9f8def0ed5d98689b2d6 test6.stl -e7657822e5acc11956d1bde0e7d131696c006742 test19.stl -690d13892bdf3953ee3a2163a898ba4494bad08a test26.stl -4f02179179e7a6db6a6e8e4d1d7533008b7d9d3a cut2d.stl -cec10c8de98312f30da55028632f00435a233107 test30.stl -d8c9d09a64da8b3cae851d21c90e153e64c0d537 cam1.stl -ec884aec363798a0dc964196bc82d7ff490d4545 test13.stl -5c9f8a124aa62eb17b877af4999d27b0155c3053 test3.stl -ebfa594bcf5e63ff85b4effbd31d14ccc69da0e6 test16.stl -b288710d07d20d4f61839b4253c6bdc5f1e13a8f cam2.stl -9db2812997ec4009fa791c7284c478c40bc99f2b test2.stl -1bcf50afa2d9495946f31ae441cef6582672ffe3 driver.stl -639d8c3005ff4511b3337282ed9902a87b59134f test14.stl -87ad9a20e61afbf13c094479596a1734537c8b16 test1.stl -9c4a86bafa823d60ea7d19a107e77ac795bcd5d9 test5.stl -be5fdbfd6eb216e3b664e0174216ad7d15e852a4 test9.stl +3d269fcbbb16b8b832c78e30dc2629dcec438557 test29.stl +eaae66e9d5621f4652949cc80225b8e2ea5028ee test15.stl +3d1d2cf7a31bde27457e6c5d5ce4fb68839cdd61 test14.stl +327448f6750bbd45cfb5cbd39559896c83cf2c82 test28.stl +16a2f0f19a98c634ca58458b46eb600740eeca43 test16.stl +ddd997b1949cb02a6700e97ce995e0203ab724eb rounded_box.stl +018b78652ee757c621f92cdfd22d19184c4a89a6 test17.stl +bfd55946311480153f4903b4135cdf4fc38e3148 test13.stl +c56d242041eb50d7f54bbc04189730432e81e4c7 test12.stl +2815a07bd35f29ebaa3e6fd55579f70ccdd3f3e7 test10.stl +85a99e342f1fcf7addbae440f948f1adc02c5616 test11.stl +f071a72f5debe3244eac48037efe3a054a8415b3 test6.stl +e0cb87f6461780931ca691482dc60606a13ad1e4 screw.stl +5deb8078c54e7190e145628d16a7cc3a9153619b test7.stl +c5bacb1fc9f66132a85c649ccdcb3998062807eb cam2.stl +91126234f9252d093fa3ed0b52eb032019667bff cam0.stl +e9e4da00282745212852491cabe83b1304f39ce2 flange.stl +b0b3c0f1a8a464ee2f4700a018754073b956dcf2 loft.stl +dac3cb434be0695a2043509d95194221065c94ff test5.stl +0cf650a1f4b62d35247072db41da764ce09f812f test4.stl +4cf5e2af7f5b6b696227b20bbd17558f4a9b6f9c cam1.stl +00fcba0bc543b7a7f5246ec0ff9291a7691586e2 test1.stl +b25848bdd68c36d661870983e3e2b29c41400143 test3.stl +8d384f6974f5396990f42125b3a4471c5c2212c0 driver.stl +0f5f7064422e6522a59741c2745f3e868bcf1fda test2.stl +26b0d1fcefe39def718ea33a725c417a15d25983 ellipsoid_egg.stl +db72c05e8493ecf220da307b8390130328788139 cut2d.stl +4e0857d95996fbcf94cf5fe23484116e1307a751 rotate_copy.stl +27a507a2f56be9a1ac9cdb2016fd2e5fb5e9a623 standard_pipe.stl +c0d5620aa345916016c4d3430ff3e58249a65996 test9.stl +f70e902b5a064bcec42a0f6f38f67023d80ff329 driven.stl +560c81c73f57cae84003acdf71f70e99efc5f7f6 test20.stl +83cfe52152f482eb9e3ee64970705dc13630389e washer.stl +c6b9a9b397c1bde286e10c21b91334bc5544aaaf test21.stl 730b024c8df2519a1bff56f58e14e977dd662463 test22.stl -c8730de7c2fb10c77a39ac5bb286bdb264f12311 test12.stl -fa9a5be2c035d98be4678e836fc1e6a75b7eed24 test10.stl -cd0f60e64f827b228c34caa70f4ddf422e874193 test29.stl -8c051b63eabb8b6f92cdec7d61a0d1a6a1873292 test4.stl -175303fe7975a092691501d805c07f3809cfbc3c rotate_copy.stl -6971de9e278943f7584abc645cb84804c04fce49 test20.stl -1196fc3a56e4f9f26c451ed21d216d6c29b1a8e8 test31.stl -47a29b2948d43486c8d0ce78cad4d490a8542ac3 rounded_box.stl -03cd9a89a0f77b30aef8af20310793c3dede7b97 test17.stl -a9e78227521256d037889ca759424947d0232c87 flange.stl -9a4de589d71433afb4cb14dcd975c97a2a334fe5 washer.stl -64a8189b1fd324b23dd9252d669fe2e4ee5e5eca test11.stl -c62e1ed7bca22326719549ecc7317f9c37693870 test7.stl -3fe68164f0fe09dc54208e0cc6bb9694ff6e7465 test21.stl -4ff18a7cd49ea9e6f0408676e2f580bf9c57e460 test18.stl -e84b6cd56d21272eec42a8f175305391eddd9135 standard_pipe.stl -48fb28668c31ddf581bf5098635a4ff610d7be87 test15.stl -11a9d8c84a86858a181ed220db2f873f0e55eb36 screw.stl -1465a0a7b47a38de51830b3501cabd19143846a9 test28.stl -d0c2d209a8b24ec3cbe18796a402de42fe1da797 loft.stl -cc4aeae854f0f70d12542d3567075b3cc38810cc cam0.stl -a5a82faeaf72a02285533701cf0c4584ef1aa97f test27.stl -a27adffab3c8ca6136ef80fffb275530c977e046 driven.stl -d1b3f78f93d7dcb90140928dd22b52c928cdd347 circle_2d.dxf +6b7ee756f98b62c6c93ad988e8e9eab6c0c73349 test26.stl +008bf06fdbbbc4d916035363f05b14c64cecb3fc test27.stl +ba9d31578d91a1f49d75ac038a03b2b26a745868 test31.stl +0b8a2e537dbe03f7965379a5122ec20816e76b10 test19.stl +6af10b39495a7c8eb39b37417d741133320d1dfb test18.stl +8a3fb773d6f267da2f34423e3111e83cd25b7051 test30.stl +a8061c7c9f9bf4ac34a6b9da8a23206bc0106d92 circle_2d.dxf diff --git a/examples/text/SHA1SUM b/examples/text/SHA1SUM index 18ca66a48..b358fc07a 100644 --- a/examples/text/SHA1SUM +++ b/examples/text/SHA1SUM @@ -1,3 +1,3 @@ -ca4e4bfa168559104edb837f9e4727cb9eee42a2 shape.stl +db3b02e36e8624a00476c82543f8cebb0ccd53c1 shape.stl 5321bf047989f75d1de44db3751641c9b052403f shape.svg -bd950896148d9b2a8efba9a67b61ba868eb7171c shape.dxf +828e99c7d8200f6ed155ad71498d1ec735014de1 shape.dxf diff --git a/render/mesh.go b/render/mesh.go new file mode 100644 index 000000000..a5b7d1d9b --- /dev/null +++ b/render/mesh.go @@ -0,0 +1,114 @@ +//----------------------------------------------------------------------------- +/* + +Mesh Analysis Utilities + +Functions for collecting rendered triangles and checking mesh quality +(watertightness, boundary edges). + +*/ +//----------------------------------------------------------------------------- + +package render + +import ( + "math" + "sync" + + "github.com/deadsy/sdfx/sdf" +) + +//----------------------------------------------------------------------------- + +// CollectTriangles renders an SDF3 with the given renderer and returns all +// triangles as a flat slice. +func CollectTriangles(s sdf.SDF3, r Render3) []sdf.Triangle3 { + ch := make(chan []*sdf.Triangle3) + var tris []sdf.Triangle3 + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + for batch := range ch { + for _, t := range batch { + tris = append(tris, *t) + } + } + }() + r.Render(s, sdf.NewTriangle3Buffer(ch)) + close(ch) + wg.Wait() + return tris +} + +//----------------------------------------------------------------------------- + +// meshVertex is a quantized 3D position for vertex deduplication. +// Quantized to 1e-4 resolution — well below meaningful geometry scale +// but avoids overflow in int32 for typical part sizes (up to ~200mm). +type meshVertex struct { + x, y, z int32 +} + +func quantizeVertex(x, y, z float64) meshVertex { + return meshVertex{int32(x * 1e4), int32(y * 1e4), int32(z * 1e4)} +} + +// meshEdge is an unordered pair of vertices. +type meshEdge struct { + a, b meshVertex +} + +// makeMeshEdge returns a canonical (sorted) edge so (a,b) == (b,a). +func makeMeshEdge(a, b meshVertex) meshEdge { + if a.x < b.x || (a.x == b.x && a.y < b.y) || (a.x == b.x && a.y == b.y && a.z < b.z) { + return meshEdge{a, b} + } + return meshEdge{b, a} +} + +//----------------------------------------------------------------------------- + +// CountBoundaryEdges returns the number of edges shared by only one triangle. +// A watertight mesh has zero boundary edges — every edge borders exactly +// two triangles. +func CountBoundaryEdges(tris []sdf.Triangle3) int { + edgeCount := make(map[meshEdge]int, len(tris)*3) + for _, t := range tris { + v0 := quantizeVertex(t[0].X, t[0].Y, t[0].Z) + v1 := quantizeVertex(t[1].X, t[1].Y, t[1].Z) + v2 := quantizeVertex(t[2].X, t[2].Y, t[2].Z) + edgeCount[makeMeshEdge(v0, v1)]++ + edgeCount[makeMeshEdge(v1, v2)]++ + edgeCount[makeMeshEdge(v2, v0)]++ + } + boundary := 0 + for _, count := range edgeCount { + if count == 1 { + boundary++ + } + } + return boundary +} + +// IsWatertight returns true if the rendered mesh has no boundary edges. +func IsWatertight(tris []sdf.Triangle3) bool { + return CountBoundaryEdges(tris) == 0 +} + +//----------------------------------------------------------------------------- + +// MaxZ returns the maximum Z coordinate across all triangle vertices. +func MaxZ(tris []sdf.Triangle3) float64 { + z := -math.MaxFloat64 + for _, t := range tris { + for _, v := range t { + if v.Z > z { + z = v.Z + } + } + } + return z +} + +//----------------------------------------------------------------------------- diff --git a/render/screw_test.go b/render/screw_test.go new file mode 100644 index 000000000..726ddb738 --- /dev/null +++ b/render/screw_test.go @@ -0,0 +1,178 @@ +package render + +import ( + "testing" + + "github.com/deadsy/sdfx/sdf" +) + +// screwTestConfig defines a screw configuration for watertightness testing. +type screwTestConfig struct { + name string + radius, pitch, length float64 + taperDeg float64 + starts, cells int + profile string // "iso", "iso-int", "acme", "buttress", "plastic-buttress" +} + +func makeTestThread(t *testing.T, profile string, radius, pitch float64) sdf.SDF2 { + t.Helper() + var thread sdf.SDF2 + var err error + switch profile { + case "iso-int": + thread, err = sdf.ISOThread(radius, pitch, false) + case "acme": + thread, err = sdf.AcmeThread(radius, pitch) + case "buttress": + thread, err = sdf.ANSIButtressThread(radius, pitch) + case "plastic-buttress": + thread, err = sdf.PlasticButtressThread(radius, pitch) + default: + thread, err = sdf.ISOThread(radius, pitch, true) + } + if err != nil { + t.Fatal(err) + } + return thread +} + +func buildScrew(t *testing.T, c screwTestConfig) sdf.SDF3 { + t.Helper() + thread := makeTestThread(t, c.profile, c.radius, c.pitch) + taperRad := sdf.DtoR(c.taperDeg) + screw, err := sdf.Screw3D(thread, c.length, taperRad, c.pitch, c.starts) + if err != nil { + t.Fatal(err) + } + return screw +} + +func straightScrewConfigs() []screwTestConfig { + return []screwTestConfig{ + // ISO external — various pitch/radius ratios and resolutions + {"M10x2_200", 5, 2, 20, 0, 1, 200, "iso"}, + {"M10x2_100", 5, 2, 20, 0, 1, 100, "iso"}, + {"M5x3_100", 2.5, 3, 10, 0, 1, 100, "iso"}, + {"coarse_M10x5", 5, 5, 20, 0, 1, 100, "iso"}, + {"steep_M3x3", 1.5, 3, 10, 0, 1, 100, "iso"}, + {"steep_M3x3_50", 1.5, 3, 10, 0, 1, 50, "iso"}, + {"extreme_M2x3", 1.0, 3, 6, 0, 1, 100, "iso"}, + {"extreme_M2x3_50", 1.0, 3, 6, 0, 1, 50, "iso"}, + {"M3x3_25cells", 1.5, 3, 10, 0, 1, 25, "iso"}, + {"short_1pitch", 5, 2, 2, 0, 1, 100, "iso"}, + {"large_M64x6", 32, 6, 60, 0, 1, 200, "iso"}, + // Multi-start + {"multi8", 5, 2, 20, 0, 8, 100, "iso"}, + {"multi16", 5, 2, 20, 0, 16, 100, "iso"}, + {"multi8_coarse", 5, 5, 20, 0, 8, 100, "iso"}, + // Left-hand + {"left_M10x2", 5, 2, 20, 0, -1, 100, "iso"}, + {"left_8start", 5, 2, 20, 0, -8, 100, "iso"}, + {"left_steep_M3x3", 1.5, 3, 10, 0, -1, 100, "iso"}, + // ISO internal + {"internal_M10x2", 5, 2, 20, 0, 1, 100, "iso-int"}, + {"internal_M5x3", 2.5, 3, 10, 0, 1, 100, "iso-int"}, + // ACME + {"acme_M10x2", 5, 2, 20, 0, 1, 100, "acme"}, + {"acme_steep_M5x3", 2.5, 3, 10, 0, 1, 100, "acme"}, + // Buttress + {"buttress_M10x2", 5, 2, 20, 0, 1, 100, "buttress"}, + {"buttress_steep", 2.5, 3, 10, 0, 1, 100, "buttress"}, + {"buttress_left", 5, 2, 20, 0, -1, 100, "buttress"}, + {"buttress_multi4", 5, 2, 20, 0, 4, 100, "buttress"}, + {"buttress_25cells", 2.5, 3, 10, 0, 1, 25, "buttress"}, + // Plastic buttress + {"plastic_butt_M10", 5, 2, 20, 0, 1, 100, "plastic-buttress"}, + {"plastic_butt_left", 5, 2, 20, 0, -1, 100, "plastic-buttress"}, + {"plastic_butt_multi4", 5, 2, 20, 0, 4, 100, "plastic-buttress"}, + // Dual/triple start (common real-world) + {"dual_start", 5, 2, 20, 0, 2, 100, "iso"}, + {"triple_start", 5, 2, 20, 0, 3, 100, "iso"}, + // Very fine thread (low stretch factor, opposite extreme) + {"fine_M20x0.5", 10, 0.5, 20, 0, 1, 100, "iso"}, + // Sub-pitch length + {"sub_pitch_len", 5, 3, 2, 0, 1, 100, "iso"}, + } +} + +func taperedScrewConfigs() []screwTestConfig { + return []screwTestConfig{ + {"taper_1.8_NPT", 5, 2, 20, 1.79, 1, 100, "iso"}, + {"taper_5", 5, 2, 20, 5, 1, 100, "iso"}, + {"taper_15", 5, 2, 20, 15, 1, 100, "iso"}, + {"taper_30", 5, 2, 20, 30, 1, 100, "iso"}, + {"taper_45", 5, 2, 20, 45, 1, 100, "iso"}, + {"taper_30_4start", 5, 2, 20, 30, 4, 100, "iso"}, + {"taper_30_coarse", 5, 5, 20, 30, 1, 100, "iso"}, + {"taper_15_steep", 2.5, 3, 10, 15, 1, 100, "iso"}, + {"taper_30_50cells", 5, 2, 20, 30, 1, 50, "iso"}, + {"taper_30_left", 5, 2, 20, 30, -1, 100, "iso"}, + {"taper_15_internal", 5, 2, 20, 15, 1, 100, "iso-int"}, + {"taper_15_acme", 5, 2, 20, 15, 1, 100, "acme"}, + {"taper_30_buttress", 5, 2, 20, 30, 1, 100, "buttress"}, + {"taper_15_plastic_butt", 5, 2, 20, 15, 1, 100, "plastic-buttress"}, + {"taper_30_left_buttress", 5, 2, 20, 30, -1, 100, "buttress"}, + {"taper_15_multi4_buttress", 5, 2, 20, 15, 4, 100, "buttress"}, + } +} + +func Test_Screw_Watertight_Straight(t *testing.T) { + for _, c := range straightScrewConfigs() { + t.Run(c.name, func(t *testing.T) { + screw := buildScrew(t, c) + tris := CollectTriangles(screw, NewMarchingCubesOctree(c.cells)) + be := CountBoundaryEdges(tris) + if be != 0 { + t.Errorf("octree mesh has %d boundary edges (want 0 for watertight)", be) + } + t.Logf("%d tris, %d boundary edges", len(tris), be) + }) + } +} + +func Test_Screw_Watertight_Tapered(t *testing.T) { + for _, c := range taperedScrewConfigs() { + t.Run(c.name, func(t *testing.T) { + screw := buildScrew(t, c) + tris := CollectTriangles(screw, NewMarchingCubesOctree(c.cells)) + be := CountBoundaryEdges(tris) + if be != 0 { + t.Errorf("octree mesh has %d boundary edges (want 0 for watertight)", be) + } + t.Logf("%d tris, %d boundary edges", len(tris), be) + }) + } +} + +func Test_Screw_EndCap_Position(t *testing.T) { + // Verify octree and uniform renderers agree on end-cap Z positions. + configs := straightScrewConfigs() + for _, c := range configs { + t.Run(c.name, func(t *testing.T) { + screw := buildScrew(t, c) + halfLen := c.length / 2.0 + cubeSize := c.length / float64(c.cells) + + octTris := CollectTriangles(screw, NewMarchingCubesOctree(c.cells)) + uniTris := CollectTriangles(screw, NewMarchingCubesUniform(c.cells)) + + octMaxZ := MaxZ(octTris) + uniMaxZ := MaxZ(uniTris) + + if halfLen-octMaxZ > cubeSize { + t.Errorf("octree maxZ=%.4f too far from halfLen=%.2f (err=%.4f, cubeSize=%.4f)", + octMaxZ, halfLen, halfLen-octMaxZ, cubeSize) + } + if halfLen-uniMaxZ > cubeSize { + t.Errorf("uniform maxZ=%.4f too far from halfLen=%.2f (err=%.4f, cubeSize=%.4f)", + uniMaxZ, halfLen, halfLen-uniMaxZ, cubeSize) + } + delta := octMaxZ - uniMaxZ + if delta > cubeSize || delta < -cubeSize { + t.Errorf("octree/uniform disagree: octMaxZ=%.4f uniMaxZ=%.4f delta=%.4f (cubeSize=%.4f)", + octMaxZ, uniMaxZ, delta, cubeSize) + } + }) + } +} diff --git a/sdf/line.go b/sdf/line.go index b72785d81..f18411230 100644 --- a/sdf/line.go +++ b/sdf/line.go @@ -101,7 +101,7 @@ func (a *Line2) IntersectLine(b *Line2) []v2.Vec { if x != nil { // collinear, intersecting p0 := p.Add(r.MulScalar(x[0])) - if x[0] == x[1] { + if math.Abs(x[0]-x[1]) <= tolerance { return []v2.Vec{p0} } p1 := p.Add(r.MulScalar(x[1])) diff --git a/sdf/screw.go b/sdf/screw.go index 488c2458d..622504c78 100644 --- a/sdf/screw.go +++ b/sdf/screw.go @@ -360,13 +360,17 @@ func PlasticButtressThread( // ScrewSDF3 is a 3d screw form. type ScrewSDF3 struct { - thread SDF2 // 2D thread profile - pitch float64 // thread to thread distance - lead float64 // distance per turn (starts * pitch) - length float64 // total length of screw - taper float64 // thread taper angle - starts int // number of thread starts - bb Box3 // bounding box + thread SDF2 // 2D thread profile + pitch float64 // thread to thread distance + lead float64 // distance per turn (starts * pitch) + leadOver2π float64 // lead / (2π), precomputed for the helical correction + length float64 // total length of screw + taper float64 // thread taper angle + tanTaper float64 // tan(taper), precomputed + tan2Taper float64 // tan²(taper), precomputed + threadDepth float64 // radial extent of thread profile (max.Y - min.Y) + starts int // number of thread starts + bb Box3 // bounding box } // Screw3D returns a screw SDF3. @@ -398,36 +402,92 @@ func Screw3D( s.length = length / 2 s.taper = taper s.lead = -pitch * float64(starts) + s.leadOver2π = s.lead / Tau + s.tanTaper = math.Tan(taper) + s.tan2Taper = s.tanTaper * s.tanTaper // Work out the bounding box. // The max-y axis of the sdf2 bounding box is the radius of the thread. bb := s.thread.BoundingBox() + s.threadDepth = bb.Max.Y - bb.Min.Y r := bb.Max.Y // add the taper increment - r += s.length * math.Tan(taper) + r += s.length * s.tanTaper s.bb = Box3{v3.Vec{-r, -r, -s.length}, v3.Vec{r, r, s.length}} return &s, nil } // Evaluate returns the minimum distance to a 3d screw form. func (s *ScrewSDF3) Evaluate(p v3.Vec) float64 { - // map the 3d point back to the xy space of the profile + // Map the 3d point back to the 2d thread profile space. p0 := v2.Vec{} - // the distance from the 3d z-axis maps to the 2d y-axis - p0.Y = math.Sqrt(p.X*p.X + p.Y*p.Y) + // The distance from the z-axis maps to the 2d y-axis (radial coordinate). + r := math.Sqrt(p.X*p.X + p.Y*p.Y) + p0.Y = r if s.taper != 0 { - p0.Y += p.Z * math.Atan(s.taper) + // Adjust radial position for taper: the thread radius changes linearly + // with z. tanTaper converts the taper angle to a slope (rise/run). + p0.Y += p.Z * s.tanTaper } - // the x/y angle and the z-height map to the 2d x-axis - // ie: the position along thread pitch + // The x/y angle and z-height map to the 2d x-axis: this unwraps the + // helical path into a linear coordinate along the thread pitch. theta := math.Atan2(p.Y, p.X) z := p.Z + s.lead*theta/Tau p0.X = SawTooth(z, s.pitch) - // get the thread profile distance + // Get the thread profile distance in the 2d mapped space. + // The thread profile covers one pitch period, and SawTooth wraps the + // helical coordinate into [-pitch/2, +pitch/2]. For asymmetric profiles + // (e.g. buttress), the SDF has a discontinuity at the wrap boundary + // because the profile shape differs at x = +pitch/2 vs x = -pitch/2. + // The correct SDF for a periodic thread is the union (min) of adjacent + // periods — each thread tooth is a copy, and min gives union semantics. d0 := s.thread.Evaluate(p0) - // create a region for the screw length + dL := s.thread.Evaluate(v2.Vec{X: p0.X - s.pitch, Y: p0.Y}) + dR := s.thread.Evaluate(v2.Vec{X: p0.X + s.pitch, Y: p0.Y}) + d0 = math.Min(d0, math.Min(dL, dR)) + // Create a region for the screw length. d1 := math.Abs(p.Z) - s.length - // return the intersection - return math.Max(d0, d1) + // Intersect the thread with the length constraint. + d := math.Max(d0, d1) + // The 3D→2D mapping stretches distances: a unit step in 3D maps to + // a larger step in the unwrapped 2D space. Without correction, the + // SDF overestimates distances, causing the octree renderer's isEmpty + // check to skip cubes that contain the surface (producing holes). + // + // The Jacobian of the mapping (r,θ,z) → (p0.X, p0.Y) in physical + // cylindrical coordinates is: + // J = | 0 k 1 | where k = lead/(2πr) + // | 1 0 tan(t) | and t = taper angle + // + // The maximum stretch factor is the largest singular value of J, + // i.e. sqrt(λ_max) of J·Jᵀ: + // | 1+k² tan(t) | + // | tan(t) 1+tan²(t) | + // + // When taper=0 this reduces to sqrt(1+k²). + // + // Dividing by this factor gives a correct distance bound (Lipschitz-1). + // Applied to the final intersection result rather than d0 alone to + // preserve end cap surface positions (d=0 stays at 0). + // + // The stretch factor varies with r (larger near the axis). The octree + // isEmpty check evaluates at cube centers, but the nearest surface may + // be at a smaller r where the stretch is larger. Using r reduced by the + // thread depth gives a conservative bound: the nearest surface point + // on the thread can be at most threadDepth closer to the axis. + if r > 0 { + rEff := r - s.threadDepth + if rEff < r*0.5 { + rEff = r * 0.5 + } + k2 := s.leadOver2π / rEff + k2 *= k2 + a := 1 + k2 + c := 1 + s.tan2Taper + disc := (a-c)*(a-c) + 4*s.tan2Taper + stretch2 := (a + c + math.Sqrt(disc)) * 0.5 + d /= math.Sqrt(stretch2) + } + return d } // BoundingBox returns the bounding box for a 3d screw form. diff --git a/sdf/screw_test.go b/sdf/screw_test.go new file mode 100644 index 000000000..e66b7a12b --- /dev/null +++ b/sdf/screw_test.go @@ -0,0 +1,52 @@ +package sdf + +import ( + "math" + "testing" + + v3 "github.com/deadsy/sdfx/vec/v3" +) + +// Test_Screw_TaperSlope verifies that Evaluate uses tan(taper) not atan(taper) +// for the taper slope computation. At 30° the error is 16.5%. +func Test_Screw_TaperSlope(t *testing.T) { + taperAngle := DtoR(30) + slope := math.Tan(taperAngle) + slopeAtan := math.Atan(taperAngle) + + thread, err := ISOThread(5, 2, true) + if err != nil { + t.Fatal(err) + } + screw, err := Screw3D(thread, 20, taperAngle, 2, 1) + if err != nil { + t.Fatal(err) + } + + // At z=-8 (pitch multiple), SawTooth maps to 0 (thread crest center). + // The tan-predicted crest radius is larger than the atan-predicted one. + testZ := -8.0 + rCrestTan := 5 + math.Abs(testZ)*slope + rCrestAtan := 5 + math.Abs(testZ)*slopeAtan + + // A point between the two predicted radii should be inside if tan is used. + testR := (rCrestTan + rCrestAtan) / 2 + d := screw.Evaluate(v3.Vec{X: testR, Y: 0, Z: testZ}) + if d > 0 { + t.Errorf("point at r=%.3f (between tan=%.3f and atan=%.3f) is outside (d=%+.6f); expected inside with tan slope", + testR, rCrestTan, rCrestAtan, d) + } + + // Verify surface is near the tan-predicted radius, not the atan-predicted one. + dInside := screw.Evaluate(v3.Vec{X: rCrestTan - 0.5, Y: 0, Z: testZ}) + dOutside := screw.Evaluate(v3.Vec{X: rCrestTan + 0.5, Y: 0, Z: testZ}) + if dInside >= 0 { + t.Errorf("r_crest-0.5: expected inside (d<0), got d=%+.4f", dInside) + } + if dOutside <= 0 { + t.Errorf("r_crest+0.5: expected outside (d>0), got d=%+.4f", dOutside) + } + + t.Logf("taper=30° slope_tan=%.4f slope_atan=%.4f (%.1f%% error)", + slope, slopeAtan, 100*(slope-slopeAtan)/slope) +} diff --git a/tools/stldiff/main.go b/tools/stldiff/main.go new file mode 100644 index 000000000..4f2c9be13 --- /dev/null +++ b/tools/stldiff/main.go @@ -0,0 +1,179 @@ +// stldiff: compare two binary STL files. +// Prints IDENTICAL / MINOR / MATERIAL plus metrics. +package main + +import ( + "crypto/sha1" + "encoding/binary" + "fmt" + "io" + "math" + "os" + "sort" +) + +type vec3 struct{ x, y, z float32 } + +type tri struct{ a, b, c vec3 } + +func loadSTL(path string) ([]tri, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + var header [80]byte + if _, err := io.ReadFull(f, header[:]); err != nil { + return nil, err + } + var count uint32 + if err := binary.Read(f, binary.LittleEndian, &count); err != nil { + return nil, err + } + tris := make([]tri, count) + for i := uint32(0); i < count; i++ { + var buf [12]float32 + if err := binary.Read(f, binary.LittleEndian, &buf); err != nil { + return nil, err + } + var attr uint16 + if err := binary.Read(f, binary.LittleEndian, &attr); err != nil { + return nil, err + } + tris[i] = tri{ + a: vec3{buf[3], buf[4], buf[5]}, + b: vec3{buf[6], buf[7], buf[8]}, + c: vec3{buf[9], buf[10], buf[11]}, + } + } + return tris, nil +} + +func lessVec(a, b vec3) bool { + if a.x != b.x { + return a.x < b.x + } + if a.y != b.y { + return a.y < b.y + } + return a.z < b.z +} + +func sortTriVerts(t tri) tri { + vs := [3]vec3{t.a, t.b, t.c} + sort.Slice(vs[:], func(i, j int) bool { return lessVec(vs[i], vs[j]) }) + return tri{vs[0], vs[1], vs[2]} +} + +func canonicalHash(tris []tri) string { + sorted := make([]tri, len(tris)) + for i, t := range tris { + sorted[i] = sortTriVerts(t) + } + sort.Slice(sorted, func(i, j int) bool { + a, b := sorted[i], sorted[j] + if a.a != b.a { + return lessVec(a.a, b.a) + } + if a.b != b.b { + return lessVec(a.b, b.b) + } + return lessVec(a.c, b.c) + }) + h := sha1.New() + buf := make([]byte, 4) + for _, t := range sorted { + for _, v := range [3]vec3{t.a, t.b, t.c} { + for _, f := range [3]float32{v.x, v.y, v.z} { + binary.LittleEndian.PutUint32(buf, math.Float32bits(f)) + h.Write(buf) + } + } + } + return fmt.Sprintf("%x", h.Sum(nil))[:12] +} + +type metrics struct { + count int + min, max vec3 + hash string +} + +func computeMetrics(tris []tri) metrics { + m := metrics{count: len(tris)} + if len(tris) == 0 { + return m + } + m.min = tris[0].a + m.max = tris[0].a + for _, t := range tris { + for _, v := range [3]vec3{t.a, t.b, t.c} { + if v.x < m.min.x { + m.min.x = v.x + } + if v.y < m.min.y { + m.min.y = v.y + } + if v.z < m.min.z { + m.min.z = v.z + } + if v.x > m.max.x { + m.max.x = v.x + } + if v.y > m.max.y { + m.max.y = v.y + } + if v.z > m.max.z { + m.max.z = v.z + } + } + } + m.hash = canonicalHash(tris) + return m +} + +func bboxDelta(a, b metrics) float64 { + ax, ay, az := float64(a.max.x-a.min.x), float64(a.max.y-a.min.y), float64(a.max.z-a.min.z) + bx, by, bz := float64(b.max.x-b.min.x), float64(b.max.y-b.min.y), float64(b.max.z-b.min.z) + return math.Abs(ax-bx) + math.Abs(ay-by) + math.Abs(az-bz) +} + +func abs(x int) int { + if x < 0 { + return -x + } + return x +} + +func main() { + if len(os.Args) != 3 { + fmt.Fprintln(os.Stderr, "usage: stldiff ") + os.Exit(2) + } + a, err := loadSTL(os.Args[1]) + if err != nil { + fmt.Fprintln(os.Stderr, "A:", err) + os.Exit(2) + } + b, err := loadSTL(os.Args[2]) + if err != nil { + fmt.Fprintln(os.Stderr, "B:", err) + os.Exit(2) + } + ma, mb := computeMetrics(a), computeMetrics(b) + if ma.hash == mb.hash { + fmt.Printf("IDENTICAL tris=%d hash=%s\n", ma.count, ma.hash) + return + } + bb := bboxDelta(ma, mb) + triDelta := mb.count - ma.count + bboxSize := float64(ma.max.x-ma.min.x) + float64(ma.max.y-ma.min.y) + float64(ma.max.z-ma.min.z) + relBbox := bb / bboxSize + triRel := float64(abs(triDelta)) / float64(ma.count+1) + status := "MATERIAL" + if relBbox < 1e-4 && triRel < 0.01 { + status = "MINOR " + } + fmt.Printf("%s tris=%d→%d (Δ%+d, %.2f%%) bbox-Δ=%.3e (%.2f%%) hashA=%s hashB=%s\n", + status, ma.count, mb.count, triDelta, triRel*100, bb, relBbox*100, ma.hash, mb.hash) +} diff --git a/tools/stldiff/run.sh b/tools/stldiff/run.sh new file mode 100755 index 000000000..75240cb69 --- /dev/null +++ b/tools/stldiff/run.sh @@ -0,0 +1,86 @@ +#!/bin/bash +# Cross-branch STL regression check. +# +# Renders every example/*/ at BASE and HEAD revs, then compares the STL +# outputs. Reports per-file: IDENTICAL (canonical hash match), MINOR (tiny +# float drift), or MATERIAL (geometry genuinely changed). Intended as a +# sanity check that a core-library change didn't inadvertently alter +# unrelated examples. +# +# Usage: run.sh [BASE_REF] [HEAD_REF] (defaults: master, HEAD) + +set -u + +BASE_REF="${1:-master}" +HEAD_REF="${2:-HEAD}" + +REPO_ROOT=$(cd "$(dirname "$0")/../.." && pwd) +TOOL_DIR="$REPO_ROOT/tools/stldiff" +BASE_TREE=$(mktemp -d -t sdfx-stldiff-base-XXXXXX) +HEAD_TREE=$(mktemp -d -t sdfx-stldiff-head-XXXXXX) +LOG=$(mktemp -t sdfx-stldiff-results-XXXXXX.txt) +TIMEOUT=120 + +cleanup() { + git -C "$REPO_ROOT" worktree remove --force "$BASE_TREE" 2>/dev/null + git -C "$REPO_ROOT" worktree remove --force "$HEAD_TREE" 2>/dev/null +} +trap cleanup EXIT + +git -C "$REPO_ROOT" worktree add --detach "$BASE_TREE" "$BASE_REF" >/dev/null +git -C "$REPO_ROOT" worktree add --detach "$HEAD_TREE" "$HEAD_REF" >/dev/null + +( cd "$TOOL_DIR" && go build -o "$TOOL_DIR/stldiff" . ) + +run_with_timeout() { + local d=$1 n=$2 + ( cd "$d" && rm -f *.stl && make all >/dev/null 2>&1 && ./"$n" >/dev/null 2>&1 ) & + local pid=$! + ( sleep $TIMEOUT && kill -9 $pid 2>/dev/null ) & + local watcher=$! + wait $pid 2>/dev/null + local rc=$? + kill -9 $watcher 2>/dev/null + wait $watcher 2>/dev/null + return $rc +} + +: > "$LOG" + +for dir in "$HEAD_TREE"/examples/*/; do + name=$(basename "$dir") + basedir="$BASE_TREE/examples/$name" + headdir="$HEAD_TREE/examples/$name" + + [ -d "$basedir" ] || { echo "[$name] SKIP (not in base)" | tee -a "$LOG"; continue; } + [ -f "$basedir/main.go" ] || { echo "[$name] SKIP (no main.go)" | tee -a "$LOG"; continue; } + + run_with_timeout "$basedir" "$name"; bstatus=$? + run_with_timeout "$headdir" "$name"; hstatus=$? + if [ $bstatus -ne 0 ] || [ $hstatus -ne 0 ]; then + echo "[$name] BUILD/RUN FAILED (base=$bstatus head=$hstatus)" | tee -a "$LOG" + continue + fi + + any=0 + for bstl in "$basedir"/*.stl; do + [ -f "$bstl" ] || continue + any=1 + fname=$(basename "$bstl") + hstl="$headdir/$fname" + if [ ! -f "$hstl" ]; then + echo "[$name/$fname] MISSING on head" | tee -a "$LOG" + continue + fi + result=$("$TOOL_DIR/stldiff" "$bstl" "$hstl" 2>&1) + echo "[$name/$fname] $result" | tee -a "$LOG" + done + [ $any -eq 0 ] && echo "[$name] no STL produced" | tee -a "$LOG" +done + +echo "---" | tee -a "$LOG" +echo "Summary (base=$BASE_REF head=$HEAD_REF):" | tee -a "$LOG" +echo " IDENTICAL: $(grep -c IDENTICAL "$LOG")" | tee -a "$LOG" +echo " MINOR: $(grep -c 'MINOR ' "$LOG")" | tee -a "$LOG" +echo " MATERIAL: $(grep -c MATERIAL "$LOG")" | tee -a "$LOG" +echo " Results log: $LOG" From f6418660744cd814ad2a554119f1a70bb73e2153 Mon Sep 17 00:00:00 2001 From: snowbldr Date: Wed, 29 Apr 2026 22:42:59 -0600 Subject: [PATCH 2/3] fix buttress screws rendering incorrectly and test failures --- examples/3dp_nutbolt/SHA1SUM | 4 +-- examples/bolt_container/SHA1SUM | 2 +- examples/gas_cap/SHA1SUM | 2 +- examples/nutsandbolts/SHA1SUM | 2 +- examples/screw_assortment/SHA1SUM | 2 +- examples/tapers/SHA1SUM | 4 +-- examples/test/SHA1SUM | 2 +- sdf/screw.go | 56 ++++++++++++++++++++----------- sdf/screw_test.go | 43 ++++++++++++++++++++++++ 9 files changed, 88 insertions(+), 29 deletions(-) diff --git a/examples/3dp_nutbolt/SHA1SUM b/examples/3dp_nutbolt/SHA1SUM index 956be0711..d818aa1d6 100644 --- a/examples/3dp_nutbolt/SHA1SUM +++ b/examples/3dp_nutbolt/SHA1SUM @@ -1,4 +1,4 @@ 2cfc14602640e6f7218815a411c21a68c93053c3 inch_nut.stl 72511cb417650cba23f292e02bdaca03b46272dc metric_nut.stl -9d4b5e573aae83416936799ca25166836d79312a inch_bolt.stl -cbdf62eb9d0080d6adceaee83b5a6448916bf25c metric_bolt.stl +c088d7cb69cc55cfcd4ab34d3767ac6a4ed7f87e inch_bolt.stl +5fe74df283a275721c340c0bb17d8261600f0111 metric_bolt.stl diff --git a/examples/bolt_container/SHA1SUM b/examples/bolt_container/SHA1SUM index 82ffec59a..c49a33eca 100644 --- a/examples/bolt_container/SHA1SUM +++ b/examples/bolt_container/SHA1SUM @@ -1 +1 @@ -4e35e8918b1b2fa805c096cdd78b5efa5df40a7e container.stl +e27eee531a9aa5f76bcf702b7978df6f246567ac container.stl diff --git a/examples/gas_cap/SHA1SUM b/examples/gas_cap/SHA1SUM index c4ad22176..bcd4208cb 100644 --- a/examples/gas_cap/SHA1SUM +++ b/examples/gas_cap/SHA1SUM @@ -1 +1 @@ -ea12a2ed62e0404c5e90367a794347e9be59c896 cap.stl +e2fd73686ac8a64c340e5dc9706452e48c901aeb cap.stl diff --git a/examples/nutsandbolts/SHA1SUM b/examples/nutsandbolts/SHA1SUM index d5e991730..8e8d792d9 100644 --- a/examples/nutsandbolts/SHA1SUM +++ b/examples/nutsandbolts/SHA1SUM @@ -1 +1 @@ -43e4ce2a96d1de88e474ed127d43fabd61f45425 nutandbolt.stl +5c6fa9fdc196190b7cfbdac1b711afcf512154cc nutandbolt.stl diff --git a/examples/screw_assortment/SHA1SUM b/examples/screw_assortment/SHA1SUM index 5b55f3f03..883ae4b60 100644 --- a/examples/screw_assortment/SHA1SUM +++ b/examples/screw_assortment/SHA1SUM @@ -1 +1 @@ -d40749406d577ed45cb2048ade68a068ebf9ebf7 screws.stl +e9687c98362308d5580688d9569bef7776dfda8e screws.stl diff --git a/examples/tapers/SHA1SUM b/examples/tapers/SHA1SUM index 6842349fb..8daedc5c2 100644 --- a/examples/tapers/SHA1SUM +++ b/examples/tapers/SHA1SUM @@ -1,2 +1,2 @@ -e2cdd67b254aba5b6af0f8ca92d8b1b2bf8c4382 taper1.stl -df63a71f37ac87a906fa3ad7d5b7c4f16d340081 taper2.stl +41696199e4158060f63bf478a8a6ff948864aba4 taper1.stl +b1cf744fb06cab08750d87f888ee2623b90266c5 taper2.stl diff --git a/examples/test/SHA1SUM b/examples/test/SHA1SUM index d7e9c03b8..344dee61e 100644 --- a/examples/test/SHA1SUM +++ b/examples/test/SHA1SUM @@ -10,7 +10,7 @@ c56d242041eb50d7f54bbc04189730432e81e4c7 test12.stl 2815a07bd35f29ebaa3e6fd55579f70ccdd3f3e7 test10.stl 85a99e342f1fcf7addbae440f948f1adc02c5616 test11.stl f071a72f5debe3244eac48037efe3a054a8415b3 test6.stl -e0cb87f6461780931ca691482dc60606a13ad1e4 screw.stl +92e0c416725f0d7621e88422cf5b72dbb921c76b screw.stl 5deb8078c54e7190e145628d16a7cc3a9153619b test7.stl c5bacb1fc9f66132a85c649ccdcb3998062807eb cam2.stl 91126234f9252d093fa3ed0b52eb032019667bff cam0.stl diff --git a/sdf/screw.go b/sdf/screw.go index 622504c78..dca0e6f0b 100644 --- a/sdf/screw.go +++ b/sdf/screw.go @@ -306,6 +306,14 @@ func ISOThread( // ANSIButtressThread returns the 2d profile for an ANSI 45/7 buttress thread. // https://en.wikipedia.org/wiki/Buttress_thread // AMSE B1.9-1973 +// +// The polygon spans x ∈ [-pitch, +pitch] and is evaluated by Screw3D within +// the central pitch period x ∈ [-pitch/2, +pitch/2]. The extensions on both +// sides reproduce the periodic thread structure (45° flank descending toward +// next/previous valley) so the SDF is continuous across the SawTooth wrap +// boundary at x = ±pitch/2 — required because buttress profiles are +// asymmetric and a flat crest extension would create a wrap discontinuity +// that breaks octree marching cubes. func ANSIButtressThread( radius float64, // radius of thread pitch float64, // thread to thread distance @@ -318,20 +326,29 @@ func ANSIButtressThread( h1 := ((b / 2.0) * pitch) + (0.5 * h0) hp := pitch / 2.0 + xV := t0*h0 - hp // valley root x + x7 := hp - (h0-h1)*t1 // 7° flank top x (start of right-side crest) + x45 := (h0-h1)*t0 - hp // 45° flank top x (end of left-side crest) + yEdge := radius + x45 // y on 45° flank at x=±pitch (slope -1, x45<0) + tp := NewPolygon() tp.Add(pitch, 0) - tp.Add(pitch, radius) - tp.Add(hp-((h0-h1)*t1), radius) - tp.Add(t0*h0-hp, radius-h1).Smooth(0.0714*pitch, 5) - tp.Add((h0-h1)*t0-hp, radius) - tp.Add(-pitch, radius) + tp.Add(pitch, yEdge) + tp.Add(x45+pitch, radius) + tp.Add(x7, radius) + tp.Add(xV, radius-h1).Smooth(0.0714*pitch, 5) + tp.Add(x45, radius) + tp.Add(x7-pitch, radius) + tp.Add(xV-pitch, radius-h1).Smooth(0.0714*pitch, 5) + tp.Add(-pitch, yEdge) tp.Add(-pitch, 0) return Polygon2D(tp.Vertices()) } // PlasticButtressThread returns the 2d profile for a screw top style plastic buttress thread. -// Similar to ANSI 45/7 - but with more corner rounding +// Similar to ANSI 45/7 - but with more corner rounding. +// See ANSIButtressThread for an explanation of the 2-period polygon shape. func PlasticButtressThread( radius float64, // radius of thread pitch float64, // thread to thread distance @@ -344,13 +361,21 @@ func PlasticButtressThread( h1 := ((b / 2.0) * pitch) + (0.5 * h0) hp := pitch / 2.0 + xV := t0*h0 - hp + x7 := hp - (h0-h1)*t1 + x45 := (h0-h1)*t0 - hp + yEdge := radius + x45 + tp := NewPolygon() tp.Add(pitch, 0) - tp.Add(pitch, radius) - tp.Add(hp-((h0-h1)*t1), radius).Smooth(0.05*pitch, 5) - tp.Add(t0*h0-hp, radius-h1).Smooth(0.15*pitch, 5) - tp.Add((h0-h1)*t0-hp, radius).Smooth(0.15*pitch, 5) - tp.Add(-pitch, radius) + tp.Add(pitch, yEdge) + tp.Add(x45+pitch, radius).Smooth(0.15*pitch, 5) + tp.Add(x7, radius).Smooth(0.05*pitch, 5) + tp.Add(xV, radius-h1).Smooth(0.15*pitch, 5) + tp.Add(x45, radius).Smooth(0.15*pitch, 5) + tp.Add(x7-pitch, radius).Smooth(0.05*pitch, 5) + tp.Add(xV-pitch, radius-h1).Smooth(0.15*pitch, 5) + tp.Add(-pitch, yEdge) tp.Add(-pitch, 0) return Polygon2D(tp.Vertices()) @@ -434,16 +459,7 @@ func (s *ScrewSDF3) Evaluate(p v3.Vec) float64 { z := p.Z + s.lead*theta/Tau p0.X = SawTooth(z, s.pitch) // Get the thread profile distance in the 2d mapped space. - // The thread profile covers one pitch period, and SawTooth wraps the - // helical coordinate into [-pitch/2, +pitch/2]. For asymmetric profiles - // (e.g. buttress), the SDF has a discontinuity at the wrap boundary - // because the profile shape differs at x = +pitch/2 vs x = -pitch/2. - // The correct SDF for a periodic thread is the union (min) of adjacent - // periods — each thread tooth is a copy, and min gives union semantics. d0 := s.thread.Evaluate(p0) - dL := s.thread.Evaluate(v2.Vec{X: p0.X - s.pitch, Y: p0.Y}) - dR := s.thread.Evaluate(v2.Vec{X: p0.X + s.pitch, Y: p0.Y}) - d0 = math.Min(d0, math.Min(dL, dR)) // Create a region for the screw length. d1 := math.Abs(p.Z) - s.length // Intersect the thread with the length constraint. diff --git a/sdf/screw_test.go b/sdf/screw_test.go index e66b7a12b..583f4a9ca 100644 --- a/sdf/screw_test.go +++ b/sdf/screw_test.go @@ -4,9 +4,52 @@ import ( "math" "testing" + v2 "github.com/deadsy/sdfx/vec/v2" v3 "github.com/deadsy/sdfx/vec/v3" ) +// Test_Buttress_WrapContinuity verifies that asymmetric buttress thread +// profiles produce the same SDF at x = +pitch/2 and x = -pitch/2 — the +// SawTooth wrap boundary. A discontinuity here would cause octree marching +// cubes to skip cubes straddling the boundary and produce mesh holes; the +// 2-period polygon design is what makes the SDF wrap-continuous. +func Test_Buttress_WrapContinuity(t *testing.T) { + cases := []struct { + name string + make func(r, p float64) (SDF2, error) + radius float64 + pitch float64 + }{ + {"ANSI", ANSIButtressThread, 5, 2}, + {"Plastic", PlasticButtressThread, 5, 2}, + {"ANSI_steep", ANSIButtressThread, 2.5, 3}, + {"Plastic_steep", PlasticButtressThread, 2.5, 3}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + s, err := c.make(c.radius, c.pitch) + if err != nil { + t.Fatal(err) + } + hp := c.pitch / 2 + // Sample a vertical line of points at the wrap boundary, comparing + // SDF(+pitch/2, y) vs SDF(-pitch/2, y) at radii covering the + // thread region. Tolerance is generous enough to absorb numerical + // noise from polygon edge intersections, but tight enough to + // catch the ~0.4mm gap that the periodic union previously masked. + tol := 1e-6 + for _, y := range []float64{c.radius - 0.1, c.radius - 0.3, c.radius - 0.5, c.radius} { + dR := s.Evaluate(v2.Vec{X: hp, Y: y}) + dL := s.Evaluate(v2.Vec{X: -hp, Y: y}) + if math.Abs(dR-dL) > tol { + t.Errorf("y=%.3f: SDF discontinuous at wrap: dR(%+.4f)=%+.6f dL(%+.4f)=%+.6f delta=%.6f", + y, hp, dR, -hp, dL, dR-dL) + } + } + }) + } +} + // Test_Screw_TaperSlope verifies that Evaluate uses tan(taper) not atan(taper) // for the taper slope computation. At 30° the error is 16.5%. func Test_Screw_TaperSlope(t *testing.T) { From 7c2ee327c354622aff60d6e4140befb6defbc6a7 Mon Sep 17 00:00:00 2001 From: snowbldr Date: Fri, 1 May 2026 22:38:18 -0600 Subject: [PATCH 3/3] fix buttress screws rendering incorrectly and test failures --- sdf/screw.go | 62 +++++++++++++++++++++++++++++++++++++++++++++++ sdf/screw_test.go | 35 ++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/sdf/screw.go b/sdf/screw.go index dca0e6f0b..eb2cc9768 100644 --- a/sdf/screw.go +++ b/sdf/screw.go @@ -399,6 +399,34 @@ type ScrewSDF3 struct { } // Screw3D returns a screw SDF3. +// +// The thread SDF2 represents the 2D thread profile, which is helically swept +// to form the screw. Screw3D's evaluation maps any 3D point back to a 2D +// coordinate via SawTooth-wrapping the helical position into [-pitch/2, +// +pitch/2] and queries thread.Evaluate at that point. +// +// For the resulting SDF to be continuous (and therefore usable with the +// octree marching cubes renderer, which prunes "empty" cubes by SDF +// magnitude), the thread profile must satisfy: +// +// thread.Evaluate(+pitch/2, y) == thread.Evaluate(-pitch/2, y) for all y +// +// at least up to floating-point tolerance. In other words, the profile must +// describe the periodic shape such that what's just to the left of x=+pitch/2 +// matches what's just to the right of x=-pitch/2 — including any flanks, +// crests, or other features near the boundary. +// +// This is automatic for symmetric profiles like ISO and ACME, where the +// polygon is mirror-symmetric in x and the wrap point sits in a region of +// constant curvature. For asymmetric profiles like buttress threads, the +// polygon must be authored to extend past ±pitch/2 with the actual periodic +// continuation (next/previous flank descending into next/previous valley) +// rather than flat scaffolding. See ANSIButtressThread for an example. +// +// Screw3D verifies wrap continuity at construction time and returns an error +// if the profile is discontinuous. The uniform renderer is forgiving of +// discontinuities (it samples on a regular grid), but the octree renderer +// will silently produce holes — fail-fast at construction is preferable. func Screw3D( thread SDF2, // 2D thread profile length float64, // length of screw @@ -421,6 +449,9 @@ func Screw3D( if pitch <= 0 { return nil, ErrMsg("pitch <= 0") } + if err := checkThreadWrapContinuous(thread, pitch); err != nil { + return nil, err + } s := ScrewSDF3{} s.thread = thread s.pitch = pitch @@ -441,6 +472,37 @@ func Screw3D( return &s, nil } +// checkThreadWrapContinuous asserts that the thread profile's SDF agrees at +// x=+pitch/2 and x=-pitch/2 — the SawTooth wrap boundary — at sample radii +// across the profile. A mismatch indicates the profile doesn't represent the +// actual periodic shape near the wrap and will produce holes when rendered +// with the octree marching cubes renderer. +func checkThreadWrapContinuous(thread SDF2, pitch float64) error { + bb := thread.BoundingBox() + hp := pitch / 2 + yMin, yMax := bb.Min.Y, bb.Max.Y + // Sample at several radii spanning the profile. The crest level is the + // region that matters most (the surface is there); valleys are inside the + // material so a small wrap mismatch deep below the crest is harmless, + // but anywhere near or above the surface a discontinuity is fatal for + // the octree's empty-cube check. + const tol = 1e-6 + for _, frac := range []float64{0.5, 0.7, 0.9, 1.0} { + y := yMin + frac*(yMax-yMin) + dPlus := thread.Evaluate(v2.Vec{X: hp, Y: y}) + dMinus := thread.Evaluate(v2.Vec{X: -hp, Y: y}) + if math.Abs(dPlus-dMinus) > tol { + return ErrMsg(fmt.Sprintf( + "thread profile is discontinuous at the SawTooth wrap boundary "+ + "x=±pitch/2 (pitch=%g): SDF(+pitch/2,%g)=%g vs SDF(-pitch/2,%g)=%g, "+ + "|Δ|=%g. Asymmetric profiles must extend past ±pitch/2 with the "+ + "actual periodic continuation; see ANSIButtressThread for an example.", + pitch, y, dPlus, y, dMinus, math.Abs(dPlus-dMinus))) + } + } + return nil +} + // Evaluate returns the minimum distance to a 3d screw form. func (s *ScrewSDF3) Evaluate(p v3.Vec) float64 { // Map the 3d point back to the 2d thread profile space. diff --git a/sdf/screw_test.go b/sdf/screw_test.go index 583f4a9ca..7f5aa3c18 100644 --- a/sdf/screw_test.go +++ b/sdf/screw_test.go @@ -50,6 +50,41 @@ func Test_Buttress_WrapContinuity(t *testing.T) { } } +// Test_Screw3D_RejectsDiscontinuousProfile verifies that Screw3D refuses to +// build a screw from a thread profile whose SDF is discontinuous at the +// SawTooth wrap boundary (x=±pitch/2). Such profiles silently produce holes +// in octree-rendered meshes, so we fail fast at construction. A user-defined +// asymmetric profile with flat scaffolding extensions (which is exactly the +// shape the original buttress profiles had) is the canonical example. +func Test_Screw3D_RejectsDiscontinuousProfile(t *testing.T) { + // Asymmetric profile shaped like a buttress: notch at x=+pitch/4 with a + // near-vertical right flank and a 45° left flank, with flat-crest + // scaffolding extending from the period boundaries to ±pitch. SDF at + // x=+pitch/2 sees the near-vertical flank just to the left; SDF at + // x=-pitch/2 sees nothing nearby — so the wrap is discontinuous. + pitch := 2.0 + radius := 5.0 + depth := 0.6 + tp := NewPolygon() + tp.Add(pitch, 0) + tp.Add(pitch, radius) + tp.Add(pitch/2+0.05, radius) // gentle (right) flank top + tp.Add(pitch/4, radius-depth) // valley root, asymmetric position + tp.Add(pitch/4-depth, radius) // 45° (left) flank top + tp.Add(-pitch, radius) + tp.Add(-pitch, 0) + bad, err := Polygon2D(tp.Vertices()) + if err != nil { + t.Fatal(err) + } + + _, err = Screw3D(bad, 10, 0, pitch, 1) + if err == nil { + t.Fatalf("Screw3D accepted a discontinuous thread profile; expected an error") + } + t.Logf("got expected rejection: %s", err) +} + // Test_Screw_TaperSlope verifies that Evaluate uses tan(taper) not atan(taper) // for the taper slope computation. At 30° the error is 16.5%. func Test_Screw_TaperSlope(t *testing.T) {