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..d818aa1d6 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 +c088d7cb69cc55cfcd4ab34d3767ac6a4ed7f87e inch_bolt.stl +5fe74df283a275721c340c0bb17d8261600f0111 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..c49a33eca 100644 --- a/examples/bolt_container/SHA1SUM +++ b/examples/bolt_container/SHA1SUM @@ -1 +1 @@ -cf46d9d726cc5ac8605fb80bc7a786e2630f8901 container.stl +e27eee531a9aa5f76bcf702b7978df6f246567ac 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..bcd4208cb 100644 --- a/examples/gas_cap/SHA1SUM +++ b/examples/gas_cap/SHA1SUM @@ -1 +1 @@ -ed1b97a8196c354418b1a286358d9b0673f9ea74 cap.stl +e2fd73686ac8a64c340e5dc9706452e48c901aeb 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..8e8d792d9 100644 --- a/examples/nutsandbolts/SHA1SUM +++ b/examples/nutsandbolts/SHA1SUM @@ -1 +1 @@ -89aa1acd441b79ca7a773a96ca7d28f4deacf135 nutandbolt.stl +5c6fa9fdc196190b7cfbdac1b711afcf512154cc 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 ed8e48cf2..ec8f6e2e6 100644 --- a/examples/picorx/SHA1SUM +++ b/examples/picorx/SHA1SUM @@ -1,5 +1,5 @@ -7d66353a36a46c6a465dd0ec53b08543ca0a8a65 pcb_mount1.stl -c86c28b126c302438484f9f7b25bd6f40b33a60d lhs.stl -ebc5de4e0b35ee9c29a9671020b422f888155b0b rhs.stl -67a18cda7fb2c50a162c45f89c7bb0072c41755b pcb_mount0.stl -3bc39f6053b1a73573ff7dba1486727e1a029031 bezel.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..883ae4b60 --- /dev/null +++ b/examples/screw_assortment/SHA1SUM @@ -0,0 +1 @@ +e9687c98362308d5580688d9569bef7776dfda8e 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..8daedc5c2 100644 --- a/examples/tapers/SHA1SUM +++ b/examples/tapers/SHA1SUM @@ -1,2 +1,2 @@ -86e337c963e5878e7568c7c8dace8e5bf31e477e taper2.stl -f36cbf39c34dd2488c214fdf572bce8e69d77fb4 taper1.stl +41696199e4158060f63bf478a8a6ff948864aba4 taper1.stl +b1cf744fb06cab08750d87f888ee2623b90266c5 taper2.stl diff --git a/examples/test/SHA1SUM b/examples/test/SHA1SUM index 08d3dcc9d..344dee61e 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 +92e0c416725f0d7621e88422cf5b72dbb921c76b 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..eb2cc9768 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()) @@ -360,16 +385,48 @@ 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. +// +// 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 @@ -392,42 +449,123 @@ 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 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 } +// 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 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. d0 := s.thread.Evaluate(p0) - // create a region for the screw length + // 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..7f5aa3c18 --- /dev/null +++ b/sdf/screw_test.go @@ -0,0 +1,130 @@ +package sdf + +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_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) { + 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"