Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a529c20a90 | |||
| 713fd2f822 | |||
| 52f92da4ff | |||
| 4582b413f9 | |||
| c8d2725317 | |||
| 1f7f7bdebd | |||
| fc097c0ae9 | |||
| d3280476ca | |||
| 2030c86a10 | |||
| c177c5cc29 | |||
| 33eab5df60 | |||
| b42a1d1774 | |||
| 7757416124 | |||
| 0b4c50bebf | |||
| 235d35275e | |||
| f9fb8901dd |
@@ -371,7 +371,7 @@
|
||||
{"case":"issue1795_plate_static_shell_2_top_offset_solid","deferred":null,"error":null,"ints":{"doing_boolean":[0,0],"executive.present":[0],"executive_offset.present":[0],"foreign.after.present":[0],"foreign.before.present":[0],"input.pure":[0,1],"ownership":[1,2,2],"result.counts":[2,2,16,18,37,148,24],"result.edge_kinds":{"blocks":[[57,1253]],"n":37},"result.face_kinds":[1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2],"result.present":[1],"selected.after":[0,5],"selected.before":[0,5],"source.after.counts":[16,37,24],"source.after.evaluation_status":[],"source.after.kinds":[1,1,1,2,1,1,2,1,1,1,1,2,2,1,1,2],"source.after.present":[1],"source.before.counts":[8,15,10],"source.before.evaluation_status":[],"source.before.kinds":[1,1,1,2,1,1,2,1],"source.before.present":[1],"status":[1000]},"topology":null,"values":{"result.geometry":[0.00001,-9.682458365518542,0.0,0.0,42.5,20.0,27.500000000000007],"source.after.geometry":[0.00001,-9.682458365518542,0.0,0.0,42.5,20.0,27.500000000000007],"source.before.geometry":[0.00001,-9.682458365518542,0.0,0.0,42.5,20.0,27.5]}}
|
||||
{"case":"issue1795_plate_static_shell_2_y20","deferred":null,"error":null,"ints":{"doing_boolean":[0,1],"executive.present":[0],"executive_offset.present":[0],"foreign.after.present":[0],"foreign.before.present":[0],"input.pure":[1,1],"ownership":[0,1,0],"result.present":[0],"selected.after":[0,2],"selected.before":[0,2],"source.after.counts":[8,15,10],"source.after.evaluation_status":[],"source.after.kinds":[1,1,1,2,1,1,2,1],"source.after.present":[1],"source.before.counts":[8,15,10],"source.before.evaluation_status":[],"source.before.kinds":[1,1,1,2,1,1,2,1],"source.before.present":[1],"status":[1001]},"topology":null,"values":{"source.after.geometry":[0.00001,-9.682458365518542,0.0,0.0,42.5,20.0,27.5],"source.before.geometry":[0.00001,-9.682458365518542,0.0,0.0,42.5,20.0,27.5]}}
|
||||
{"case":"issue1795_plate_static_shell_2_y20_offset_solid","deferred":null,"error":null,"ints":{"doing_boolean":[0,1],"executive.present":[0],"executive_offset.present":[0],"foreign.after.present":[0],"foreign.before.present":[0],"input.pure":[1,1],"ownership":[0,1,0],"result.present":[0],"selected.after":[0,2],"selected.before":[0,2],"source.after.counts":[8,15,10],"source.after.evaluation_status":[],"source.after.kinds":[1,1,1,2,1,1,2,1],"source.after.present":[1],"source.before.counts":[8,15,10],"source.before.evaluation_status":[],"source.before.kinds":[1,1,1,2,1,1,2,1],"source.before.present":[1],"status":[1001]},"topology":null,"values":{"source.after.geometry":[0.00001,-9.682458365518542,0.0,0.0,42.5,20.0,27.5],"source.before.geometry":[0.00001,-9.682458365518542,0.0,0.0,42.5,20.0,27.5]}}
|
||||
{"case":"issue1818_square_to_circle_loft_api_shell_1_top","deferred":null,"error":null,"ints":{"doing_boolean":[0,1],"executive.present":[0],"executive_offset.present":[0],"foreign.after.present":[0],"foreign.before.present":[0],"input.pure":[1,1],"ownership":[0,1,0],"result.present":[0],"selected.after":[0,5],"selected.before":[0,5],"source.after.counts":[6,12,8],"source.after.evaluation_status":[],"source.after.kinds":[2,2,2,2,1,1],"source.after.present":[1],"source.before.counts":[6,12,8],"source.before.evaluation_status":[],"source.before.kinds":[2,2,2,2,1,1],"source.before.present":[1],"status":[1001]},"topology":null,"values":{"source.after.geometry":[0.00001,-20.0,-20.0,0.0,20.0,20.0,60.0],"source.before.geometry":[0.00001,-20.0,-20.0,0.0,20.0,20.0,60.0]}}
|
||||
{"case":"issue1818_square_to_circle_loft_api_shell_1_top","deferred":null,"error":null,"ints":{"doing_boolean":[0,0],"executive.present":[0],"executive_offset.present":[0],"foreign.after.present":[0],"foreign.before.present":[0],"input.pure":[1,1],"ownership":[0,1,1],"result.counts":[2,2,11,12,24,96,16],"result.edge_kinds":{"blocks":[[44,590]],"n":24},"result.face_kinds":[1,1,1,2,2,2,2,2,2,2,2],"result.present":[1],"selected.after":[0,5],"selected.before":[0,5],"source.after.counts":[6,12,8],"source.after.evaluation_status":[],"source.after.kinds":[2,2,2,2,1,1],"source.after.present":[1],"source.before.counts":[6,12,8],"source.before.evaluation_status":[],"source.before.kinds":[2,2,2,2,1,1],"source.before.present":[1],"status":[1000]},"topology":null,"values":{"result.geometry":[1e-05,-20.0,-20.0,0.0,20.0,20.0,60.0],"source.after.geometry":[1e-05,-20.0,-20.0,0.0,20.0,20.0,60.0],"source.before.geometry":[1e-05,-20.0,-20.0,0.0,20.0,20.0,60.0]}}
|
||||
{"case":"issue2174_cap_two_rim_edges_api_shell_in","deferred":null,"error":null,"ints":{"doing_boolean":[0,0],"executive.present":[0],"executive_offset.present":[0],"foreign.after.present":[0],"foreign.before.present":[0],"input.pure":[1,1],"ownership":[0,1,1],"result.counts":[2,2,4,4,8,32,6],"result.edge_kinds":[2,2,2,2,2,2,2,2],"result.face_kinds":[2,2,2,2],"result.present":[1],"selected.after":[],"selected.before":[],"source.after.counts":[1,3,3],"source.after.evaluation_status":[],"source.after.kinds":[2],"source.after.present":[1],"source.before.counts":[1,3,3],"source.before.evaluation_status":[],"source.before.kinds":[2],"source.before.present":[1],"status":[1000]},"topology":null,"values":{"result.geometry":[0.00001,-8.660254037844387,0.0,4.5,8.660254037844387,1.1e-15,10.0],"source.after.geometry":[0.00001,-8.660254037844389,0.0,5.000000000000001,8.660254037844387,5e-16,10.0],"source.before.geometry":[0.00001,-8.660254037844389,0.0,5.000000000000001,8.660254037844387,5e-16,10.0]}}
|
||||
{"case":"issue2174_cap_two_rim_edges_api_shell_out","deferred":null,"error":null,"ints":{"doing_boolean":[0,0],"executive.present":[0],"executive_offset.present":[0],"foreign.after.present":[0],"foreign.before.present":[0],"input.pure":[1,1],"ownership":[0,1,1],"result.counts":[2,2,4,4,8,32,6],"result.edge_kinds":[2,2,2,2,2,2,2,2],"result.face_kinds":[2,2,2,2],"result.present":[1],"selected.after":[],"selected.before":[],"source.after.counts":[1,3,3],"source.after.evaluation_status":[],"source.after.kinds":[2],"source.after.present":[1],"source.before.counts":[1,3,3],"source.before.evaluation_status":[],"source.before.kinds":[2],"source.before.present":[1],"status":[1000]},"topology":null,"values":{"result.geometry":[0.00001,-9.526279441628823,0.0,5.0,9.526279441628823,1.1724617093029907e-15,11.0],"source.after.geometry":[0.00001,-8.660254037844389,0.0,5.000000000000001,8.660254037844387,5e-16,10.0],"source.before.geometry":[0.00001,-8.660254037844389,0.0,5.000000000000001,8.660254037844387,5e-16,10.0]}}
|
||||
{"case":"issue2611_half_pipe_arc_r12_api_shell_in","deferred":null,"error":null,"ints":{"doing_boolean":[0,0],"executive.present":[0],"executive_offset.present":[0],"foreign.after.present":[0],"foreign.before.present":[0],"input.pure":[1,1],"ownership":[0,1,1],"result.counts":[2,2,8,8,18,72,12],"result.edge_kinds":{"blocks":[[34,339]],"n":18},"result.face_kinds":[1,1,2,2,2,2,2,2],"result.present":[1],"selected.after":[],"selected.before":[],"source.after.counts":[1,6,6],"source.after.evaluation_status":[],"source.after.kinds":[2],"source.after.present":[1],"source.before.counts":[1,6,6],"source.before.evaluation_status":[],"source.before.kinds":[2],"source.before.present":[1],"status":[1000]},"topology":null,"values":{"result.geometry":[0.00001,-24.979991993593593,0.9199999999999958,-40.0,24.979991993593593,4.5147186257614305,8.485281374238577],"source.after.geometry":[0.00001,-24.979991993593593,1.0,-40.0,24.979991993593593,4.5147186257614305,8.485281374238575],"source.before.geometry":[0.00001,-24.979991993593593,1.0,-40.0,24.979991993593593,4.5147186257614305,8.485281374238575]}}
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
{"case":"gen0037","deferred":null,"error":null,"ints":{"graph":{"blocks":[[50,1663]],"n":71}},"topology":null,"values":{"edge_curve_owners":[0.0],"geometry":[1e+20,1e+20,1e+20,1e+20,0.0,0.0,-1.3221998894643683,0.00001,-1234567.0,-1234567.0,1234567.0,1234567.0,0.00001,-30.61467458920718,0.0,0.00001],"record":{"blocks":[[28974.333030821406,22289221.39035379,122141779.08161356]],"n":70},"surface_owners":[1.0],"uv_curve_owners":[-1.0,-1.0]}}
|
||||
{"case":"gen0038","deferred":null,"error":null,"ints":{"graph":{"blocks":[[104,5428]],"n":99}},"topology":null,"values":{"edge_curve_owners":[0.0],"geometry":{"blocks":[[8e+20,8e+20,3.6e+21]],"n":24},"record":{"blocks":[[58410.52515099852,58471.59621881038,2310587.9923582436]],"n":116},"surface_owners":[1.0],"uv_curve_owners":[0.0,1.0,-1.0,-1.0]}}
|
||||
{"case":"gen0039","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1277,174490],[2444,346534],[4114,669278],[778,14800]],"n":804}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0],"geometry":{"blocks":[[9.6e+21,9.600000000000017e+21,4.656000000000001e+23]],"n":194},"record":{"blocks":[[141125.56968884318,141205.72410907954,18211054.808841206],[856315.8199191247,28873797.151187282,281138581.99902],[-736999.2588899866,14771293.71724904,24109598.28267207]],"n":585},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-48.0,48.0,-1176.0]],"n":48}}}
|
||||
{"case":"gen0040","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1172,159991],[1948,311927],[978,28202]],"n":573}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0],"geometry":{"blocks":[[6.4e+21,6.4e+21,2.0799999999999997e+23]],"n":133},"record":{"blocks":[[156360.52780532837,158006.30563557148,19068776.25026989],[153903.1388885975,156472.02783429623,20181493.582973957],[104892.00006401539,108480.00015670061,12407745.286694974],[258.6666741371155,509.33334589004517,2804.4445185661316]],"n":784},"surface_owners":[1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[360.0,392.0,9416.0]],"n":32}}}
|
||||
{"case":"gen0040","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1360,199480],[1748,274861],[1932,107271]],"n":623}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0],"geometry":{"blocks":[[7.2e+21,7.200000000000017e+21,2.628e+23]],"n":148},"record":{"blocks":[[143904.70981994172,148850.83660596592,18749454.884977996],[60967.80078066501,91192355.46067868,416542727.02352035]],"n":460},"surface_owners":[1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-36.0,36.0,-666.0]],"n":36}}}
|
||||
{"case":"gen0041","deferred":null,"error":null,"ints":{"graph":{"blocks":[[2468,318735],[2755,382199],[5710,650567],[3534,632497],[6013,592519],[9779,1412900],[12928,1499698]],"n":1777}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[351.0,351.0,6552.0]],"n":27},"geometry":{"blocks":[[2.1599999999999874e+22,2.1599999999999874e+22,2.3436e+24],[431.5198428883937,624.652584117112,27447.906125791415]],"n":424},"record":{"blocks":[[156224.41673182778,156297.18329645565,19149526.127055034],[154598.40251211118,154985.87177838173,20301423.514221795],[140664.7203279845,140836.2024273099,17905887.2094762],[154920.03783534584,154938.03783534584,19116535.34318591],[154743.16176198598,154820.03053193798,20312026.989448104],[140446.0,140764.0,17885794.0],[154426.5,154613.5,19052446.0],[129684.24687969516,129844.4946593029,14955378.32523922],[91187.66304535375,91247.66304535375,11441009.498965224],[30370.71681469282,30387.28318530718,1022758.4248081051]],"n":2398},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[1377.0,1485.0,48069.0]],"n":108}}}
|
||||
{"case":"gen0042","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1277,174490],[2444,346534],[4114,669278],[778,14800]],"n":804}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0],"geometry":{"blocks":[[9.6e+21,9.600000000000008e+21,4.656000000000001e+23]],"n":194},"record":{"blocks":[[141028.51753995748,141100.51753995748,18209140.71170544],[104020.45910069923,20901260.41429212,82962163.56631431],[15281.0,15292.597876826972,503374.6497665959]],"n":585},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-48.0,48.0,-1176.0]],"n":48}}}
|
||||
{"case":"gen0043","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1881,231784],[3468,553413],[2259,207996],[5091,517645],[9023,1312991],[4471,251788]],"n":1396}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[210.0,210.0,3080.0]],"n":21},"geometry":{"blocks":[[1.6799999999999925e+22,1.6799999999999933e+22,1.4196e+24],[97.07644014672621,273.0764401467262,4532.062022572823]],"n":337},"record":{"blocks":[[140827.5568614641,140999.5568614641,18163612.41307346],[127029.93663265301,127151.93663265301,17644141.082648907],[103806.82188705914,19857052.821887065,77472491.1227448],[75988.12293491783,76084.12293491783,8443981.054912265]],"n":1001},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-84.0,84.0,-3570.0]],"n":84}}}
|
||||
@@ -128,7 +128,7 @@
|
||||
{"case":"gen0065","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1881,231784],[3468,553413],[2259,207996],[5091,517645],[9023,1312991],[4471,251788]],"n":1396}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[210.0,210.0,3080.0]],"n":21},"geometry":{"blocks":[[1.6799999999999925e+22,1.6799999999999941e+22,1.4196e+24],[216.74562947760214,251.5357930786577,9919.280402705568]],"n":337},"record":{"blocks":[[140916.00939200036,141010.75141430172,18185606.76314736],[127182.60496144516,127265.78343962408,17663526.26494957],[103781.86835273873,41335399.430042975,165038028.19196254],[75988.12293491783,76084.12293491783,8443981.054912265]],"n":1001},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-84.0,84.0,-3570.0]],"n":84}}}
|
||||
{"case":"gen0066","deferred":null,"error":null,"ints":{"graph":{"blocks":[[50,1663]],"n":71}},"topology":null,"values":{"edge_curve_owners":[0.0],"geometry":[1e+20,1e+20,1e+20,1e+20,0.0,0.0,-0.46440414733728574,0.00001,-1234567.0,-1234567.0,1234567.0,1234567.0,0.00001,-30.61467458920718,0.0,0.00001],"record":{"blocks":[[28988.883667349597,19782189.11633265,123948409.36653352]],"n":70},"surface_owners":[1.0],"uv_curve_owners":[-1.0,-1.0]}}
|
||||
{"case":"gen0067","deferred":null,"error":null,"ints":{"graph":{"blocks":[[692,106331],[25,79]],"n":261}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0],"geometry":{"blocks":[[2.4e+21,2.400000000000004e+21,3.0000000000000004e+22]],"n":56},"record":{"blocks":[[87510.89747446822,19840766.897475913,71347892.72503424]],"n":210},"surface_owners":[1.0,1.0,1.0],"uv_curve_owners":[-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0]}}
|
||||
{"case":"gen0068","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1172,159991],[1948,311927],[978,28202]],"n":573}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0],"geometry":{"blocks":[[6.4e+21,6.4e+21,2.0799999999999997e+23]],"n":133},"record":{"blocks":[[156360.52780532837,158006.30563557148,19068776.25026989],[153903.1388885975,156472.02783429623,20181493.582973957],[104892.00006401539,108480.00015670061,12407745.286694974],[258.6666741371155,509.33334589004517,2804.4445185661316]],"n":784},"surface_owners":[1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[360.0,392.0,9416.0]],"n":32}}}
|
||||
{"case":"gen0068","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1360,199542],[1745,274520],[1935,107845]],"n":623}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0],"geometry":{"blocks":[[7.2e+21,7.200000000000021e+21,2.628e+23]],"n":148},"record":{"blocks":[[142267.17126916934,149545.78124175625,18538871.935180865],[60965.31864733237,112625909.83332224,505787772.57405066]],"n":460},"surface_owners":[1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-36.0,36.0,-666.0]],"n":36}}}
|
||||
{"case":"gen0069","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1277,174490],[2444,346534],[4114,669278],[778,14800]],"n":804}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0],"geometry":{"blocks":[[9.6e+21,9.600000000000017e+21,4.6559999999999995e+23]],"n":194},"record":{"blocks":[[141089.7399922187,141161.7399922187,18205050.581791796],[-2365178.595266464,27264383.404733535,-487506475.8921594],[2484414.4434644342,12360910.443464434,116552946.53889537]],"n":585},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-48.0,48.0,-1176.0]],"n":48}}}
|
||||
{"case":"gen0070","deferred":null,"error":null,"ints":{"graph":{"blocks":[[2468,318735],[2755,382199],[5710,650567],[3534,632497],[6013,592519],[9779,1412900],[12928,1499698]],"n":1777}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[351.0,351.0,6552.0]],"n":27},"geometry":{"blocks":[[2.1599999999999874e+22,2.1599999999999874e+22,2.3436e+24],[431.5198428883937,624.652584117112,27447.906125791415]],"n":424},"record":{"blocks":[[156224.41673182778,156297.18329645565,19149526.127055034],[154598.40251211118,154985.87177838173,20301423.514221795],[140664.7203279845,140836.2024273099,17905887.2094762],[154920.03783534584,154938.03783534584,19116535.34318591],[154743.16176198598,154820.03053193798,20312026.989448104],[140446.0,140764.0,17885794.0],[154426.5,154613.5,19052446.0],[129684.24687969516,129844.4946593029,14955378.32523922],[91187.66304535375,91247.66304535375,11441009.498965224],[30370.71681469282,30387.28318530718,1022758.4248081051]],"n":2398},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[1377.0,1485.0,48069.0]],"n":108}}}
|
||||
{"case":"gen0071","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1277,174490],[2444,346534],[4114,669278],[778,14800]],"n":804}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0],"geometry":{"blocks":[[9.6e+21,9.600000000000008e+21,4.6559999999999995e+23]],"n":194},"record":{"blocks":[[141015.63183242833,141140.705756406,18197056.686032668],[104017.577173002,19857179.943658657,137194437.6835873],[15280.999999999996,15346.710572543841,503200.5403258097]],"n":585},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-48.0,48.0,-1176.0]],"n":48}}}
|
||||
|
||||
@@ -819,10 +819,6 @@ pub struct UpstreamFixes {
|
||||
pub local_operation_refuses_loose_tangent_edges: bool,
|
||||
/// A local operation that moves one of two tangent faces along its normal builds the step: `EdgeuseOffset` finds the two faces parallel but apart across the edge (`IsStepAcrossEdge`) and builds the ruled wall between their edges (`CreateStepWall`, rails of uniform weight made non-rational), kept out of the face extensions; `CloseLoopGaps` closes the gaps the wall leaves at its ends and the edges running past its corners are split; a vertex moved past an edge's other end is `SM_ERR_OFFSET_EDGE_VANISHES` (1043) (WR-530 addendum; tangency-labs/smlib 5d428ae, 90aabff, 34318f4).
|
||||
pub local_operation_step_walls: bool,
|
||||
/// Every extended offset closes the loop gaps it leaves where two consecutive edges' offsets run parallel and never meet (`CloseLoopGaps`), before the merge; a vertex moved past an edge's other end is `SM_ERR_OFFSET_EDGE_VANISHES` (1043); a shell or merged offset of a solid that is no closed solid is refused (`SM_ERR`) whatever this flag says (WR-578; tangency-labs/smlib 7ff0a94, 34318f4, rig case shell/1494). Limit: tangency-labs/solidcore#1528's plate shells on a clean operand but is refused after a prior mass-properties query on it, which writes UV trims and gap caches the offset reads (upstream behaviour, WR-583).
|
||||
pub offset_closes_loop_gaps: bool,
|
||||
/// With `offset_closes_loop_gaps` (it has no effect without it): a convex corner of four plane faces whose offset planes no longer meet in one point (two chamfers meeting at a box's corner) becomes the edge between the two faces across it that the offset makes meet, where upstream ends the corner's edges at the same pair of the planes' meeting points whichever way the corner splits, and so leaves an inset, or a shell open at one of the four faces, with loop gaps and no volume (or the merge refuses it, 1001). A corner one of whose edges would vanish is left as it is. (An offset (`offset_brep`) or a shell of a manifold solid that is no closed manifold solid is refused (`SM_ERR`) by default; WR-657's refusal no longer needs this flag.)
|
||||
pub offset_splits_corner_vertices: bool,
|
||||
/// With `local_operation_step_walls`: an edge whose vertex a local operation moves onto its other end, where its other vertex is, has closed to a point and is squeezed out of its offset face (`CloseLoopGap`); once every gap is closed the offset faces that closed to nothing (`SmFace::IsDegenerate`, a step's wall whose low top moved onto the high top's plane) are deleted and forgotten, and the merge joins the faces either side. A closed edge on no such face or across from one is still `SM_ERR_OFFSET_EDGE_VANISHES` (WR-566; tangency-labs/smlib ccbf77a). In the face move (`offset::offset_faces`), the edges then left between a moved face and a face that stayed, on one surface, which `IsTopological` finds removable, are removed, so the tops either side of the closed wall are one face (WR-566 addendum; tangency-labs/smlib 9504880).
|
||||
pub local_operation_closes_walls_to_nothing: bool,
|
||||
/// An inward shell mates the walls across rounds thinner than it (collapsed faces) (#397).
|
||||
@@ -1256,8 +1252,6 @@ pub struct UpstreamFixes {
|
||||
pub offset_closed_curve_trim_arc: bool,
|
||||
/// `TrimExtendedEdge` never takes an open edge's first trim point again as its second, by point as well as by parameter: on the closed curve a rim half of a sphere cap lifts onto (the whole offset circle), the first point's twin across the curve's seam (another parameter, the same point, often the far end of the coincident range the other rim half's offset gives) is refused, so the half no longer gets the whole circle (or nothing) and its offset loop no gap. Thickening the cap above z = 5 of a sphere whose rim is two half circles placed against its seam (the sphere cut by a turned box) failed with `SM_ERR_OFFSET_EDGE_VANISHES` (1043) under `offset_closes_loop_gaps`, and gave a non-manifold body without it (WR-673, #2174; the fork's tangency-labs/smlib@7ee0876, for #1502).
|
||||
pub offset_open_edge_second_trim_point: bool,
|
||||
/// The offset builds a vertex where more than three faces meet (a floor of four facets falling 1 degree to a drain, and its corners where two walls meet two facets) as the two vertices and the new edge the four offset planes give, where upstream fails the shell (1001): `SmOffsetGeometryCreation`'s tangency tolerance is half a degree, not two (the facets, 1.2 degrees apart, are intersected, not left to meet by extending their trims); `TrimExtendedEdge` keeps a trim point within 5 degrees of tangent where the curve crosses the surface (only a touch is dropped); and, where two faces' offsets cut the edge at a vertex all of whose edges are convex (or all concave), the edge ends at the cut it reaches first from its other end, not at the one nearest the vertex. It has no effect without `offset_closes_loop_gaps`, which closes the loop gaps the split leaves on the faces round the vertex (alone, the split returns a solid with those loops cut short, of the wrong volume) (WR-666, #2052; the fork's tangency-labs/smlib@2e1bb8c).
|
||||
pub offset_four_face_vertex_split: bool,
|
||||
/// `SmCone::CreateCanonical` (`Cone::create_canonical_with_semi_angle` and `Cone::create_canonical`) passes `inside_out` as the bounded cone constructor's `bInsideOut` and `false` as `bMakeNurbGenCurve` when `height` is `Some` (where upstream passes `FALSE` for `bInsideOut` and the caller's `bInsideOut` for `bMakeNurbGenCurve`, dropping an inside-out request), and rejects `height <= 0.0` in the two-radius overload with `SolidError::InvalidInput` (`SM_ERR_INVALID_INPUT`) instead of forwarding `top_radius` as the semi-angle in degrees of an unbounded cone (WR-63, #2207; see `plan/upstream-issues/WR-63--addendum-2026-11-01-cone-create-canonical-args.md`).
|
||||
pub cone_create_canonical_preserves_inside_out_and_checks_height: bool,
|
||||
/// `MateEdges` also intersects analytic copies of the two faces' surfaces (plane, cylinder, cone, sphere, torus; extended only along a plane or a cylinder's or cone's rulings, where the tangent (G1R) extensions still are the surface) and replaces each marched piece that lies on an exact line or conic between the same ends, and drops to one UV curve on each extended surface within the body's tolerance, by that exact curve, so a shelled cylinder's floor circle, seam line and rim stay exact (WR-495, #1453/#1130; the fork's tangency-labs/smlib@fddc366 and @98ab1b0).
|
||||
|
||||
@@ -328,17 +328,16 @@ impl OffsetExecutive {
|
||||
self.tangency_tol_radians * 180.0 / PI
|
||||
}
|
||||
|
||||
/// Takes the geometry creator's constructor tolerance under `kernel`'s fixes
|
||||
/// ([`OffsetGeometryCreation::default_tangency_tol_radians`]): half a degree with
|
||||
/// `Kernel::fixes.offset_four_face_vertex_split` and `offset_closes_loop_gaps` (WR-666),
|
||||
/// else the 2 degrees of
|
||||
/// [`Self::new`] unchanged. Called right after construction by the executives that
|
||||
/// Takes the geometry creator's constructor tolerance
|
||||
/// ([`OffsetGeometryCreation::default_tangency_tol_radians`]): half a degree (WR-666,
|
||||
/// the fork's tangency-labs/smlib@2e1bb8c), not [`Self::new`]'s two degrees. Called
|
||||
/// right after construction by the executives that
|
||||
/// `SmApiOffsetBrepFull`, `SmApiShellBrepFull` and `SmBrep::LocalOperation` build.
|
||||
pub fn use_creator_tangency_tol(&mut self, kernel: &Kernel) {
|
||||
if crate::offset::offset_geometry_creation::four_face_vertex_split(&kernel.fixes) {
|
||||
self.tangency_tol_radians =
|
||||
OffsetGeometryCreation::default_tangency_tol_radians(&kernel.fixes);
|
||||
}
|
||||
///
|
||||
/// On the default path since #2981: WR-666's split is the only path, so the creator
|
||||
/// tolerance is always the fork's.
|
||||
pub fn use_creator_tangency_tol(&mut self, _kernel: &Kernel) {
|
||||
self.tangency_tol_radians = OffsetGeometryCreation::default_tangency_tol_radians();
|
||||
}
|
||||
|
||||
/// Offset distance supplied by the geometry creator.
|
||||
|
||||
@@ -403,27 +403,23 @@ impl OffsetExecutive {
|
||||
let mut shell = run_shell_offset(kernel, original, options, faces, notifications)?;
|
||||
// Opt-in (WR-454, #380; tangency-labs/smlib@fbe43bb): an inset of a solid whose
|
||||
// offset leaves nothing has no cavity to take away (the difference would hand
|
||||
// the input back unhollowed); an offset that is no closed solid bounds none.
|
||||
// the input back unhollowed). Its non-manifold case is the shared typed guard
|
||||
// below, which both flag states use.
|
||||
if kernel.fixes.shell_empty_cavity_fails
|
||||
&& options.offset.distance < 0.0
|
||||
&& input_is_manifold
|
||||
&& bp1::num_faces(&shell) == 0
|
||||
{
|
||||
if bp1::num_faces(&shell) == 0 {
|
||||
return Err(invalid_input(OP_SHELL));
|
||||
}
|
||||
if !brep_is_manifold_solid(&shell)? {
|
||||
return Err(SolidError::sm_err(OP_SHELL));
|
||||
}
|
||||
return Err(invalid_input(OP_SHELL));
|
||||
}
|
||||
// Opt-in (WR-578): with the loop gaps closed, an offset of a solid that is still no
|
||||
// closed solid is refused rather than handed back as it is (as WR-454's opt-in
|
||||
// does for an inset): the gap closing must never turn upstream's refusal into a
|
||||
// success on a broken body
|
||||
if kernel.fixes.offset_closes_loop_gaps
|
||||
&& input_is_manifold
|
||||
&& !brep_is_manifold_solid(&shell)?
|
||||
// WR-578 (the loop gaps close on the default path since #2981): a shell of a
|
||||
// solid that is still no closed manifold solid is refused rather than returned.
|
||||
// A non-empty offset of a manifold solid that is no closed solid reaches this
|
||||
// one typed refusal (1001) whichever state WR-454's flag is in.
|
||||
if let Some(what) =
|
||||
lost_manifold(brep_is_manifold_solid(&shell)?, || Ok(input_is_manifold))?
|
||||
{
|
||||
return Err(SolidError::sm_err(OP_SHELL));
|
||||
return Err(Failure::new(OP_SHELL, OffsetFailure::Unsound { what }).into_error());
|
||||
}
|
||||
if !brep_is_manifold_solid(&shell)? && !options.create_offset_solid {
|
||||
return Ok(shell);
|
||||
@@ -560,13 +556,13 @@ impl OffsetExecutive {
|
||||
notifications,
|
||||
)?;
|
||||
// (not upstream) a shell of a manifold solid is a manifold solid or a refusal (the
|
||||
// ops contract, `finish`): an offset the merge would have refused can reach the
|
||||
// difference, which then hands back no solid (a ruled square-to-circle loft
|
||||
// shelled inward: ten faces, not manifold, volume 0; a box's chamfer left open
|
||||
// by an inset of its two sides' width, which the cavity meets along a line only).
|
||||
// The returns above are ungated by design: a solid's offset that is no solid with
|
||||
// `create_offset_solid` false is the sheet the caller asked for, and a sheet
|
||||
// input's thickening is not a solid's shell.
|
||||
// ops contract, `finish`): the early check above refuses an intermediate offset of
|
||||
// a manifold solid that is no manifold solid, and this final gate refuses a result
|
||||
// the merge or difference leaves that way (a ruled square-to-circle loft shelled
|
||||
// inward: ten faces, not manifold, volume 0; a box's chamfer left open by an inset
|
||||
// of its two sides' width, which the cavity meets along a line only). The ungated
|
||||
// return above is a sheet input's offset: with `create_offset_solid` false it is
|
||||
// the sheet the caller asked for, and its thickening has its own path.
|
||||
if let Some(what) =
|
||||
lost_manifold(brep_is_closed_manifold(&shell)?, || Ok(input_is_manifold))?
|
||||
{
|
||||
@@ -995,16 +991,13 @@ impl OffsetExecutive {
|
||||
}
|
||||
}
|
||||
|
||||
// Not upstream (opt-in, fork tangency-labs/smlib 5d428ae and 7ff0a94): close the
|
||||
// (not upstream, the executor's own split flags are the only path since
|
||||
// #2981: fork tangency-labs/smlib 5d428ae, 7ff0a94, 2e1bb8c): close the
|
||||
// loop gaps the offset leaves (a local operation's step walls' ends, a solid
|
||||
// offset's vertex where two consecutive edges' offsets run parallel), then split
|
||||
// the edges that run past a step wall's corner (after WR-531's caps below, in the
|
||||
// fork's order)
|
||||
let local = self.operation() == OffsetOperation::LocalOperation;
|
||||
let step_walls = kernel.fixes.local_operation_step_walls && local;
|
||||
if kernel.fixes.offset_closes_loop_gaps || step_walls {
|
||||
self.close_loop_gaps(kernel, original, notifications)?;
|
||||
}
|
||||
self.close_loop_gaps(kernel, original, notifications)?;
|
||||
|
||||
// Not upstream (WR-531), opt-in `Kernel::fixes.offset_point_vertex_caps` (the
|
||||
// fork's tangency-labs/smlib@d5dadaa): a vertex where one face closes on itself to
|
||||
@@ -1041,7 +1034,9 @@ impl OffsetExecutive {
|
||||
}
|
||||
}
|
||||
|
||||
if step_walls {
|
||||
if kernel.fixes.local_operation_step_walls
|
||||
&& self.operation() == OffsetOperation::LocalOperation
|
||||
{
|
||||
self.split_edges_at_step_corners(kernel, notifications)?;
|
||||
}
|
||||
|
||||
@@ -5450,7 +5445,11 @@ mod tests {
|
||||
)?;
|
||||
Ok(Some(volume))
|
||||
};
|
||||
assert_eq!(shell(false, false).unwrap(), None);
|
||||
// The default path (the splits are the only path since #2981): the shell's
|
||||
// stitch still welds the rims wrongly, the offset of the plate is no closed
|
||||
// solid, and the WR-578 guard refuses it (1001)
|
||||
assert_status(shell(false, false), 1001);
|
||||
// the case's own opt-in (stitch at the body's tolerance) builds the closed form
|
||||
// 40 x 40 x 10 less the hole, less the cavity: 8.5 high over the inner box (37 x 37)
|
||||
// less the hole's offset disk (r 3.5, centre 3.49 from x = 1.5) cut by x = 1.5
|
||||
let expected = 4564.924657;
|
||||
@@ -5464,8 +5463,10 @@ mod tests {
|
||||
}
|
||||
|
||||
/// tangency-labs/solidcore#380: a closed inward shell of a box whose wall is as thick
|
||||
/// as (or thicker than) the inradius leaves no cavity. Upstream returns the empty
|
||||
/// offset as a success (the fork: the body unhollowed); with the opt-in it fails, and a thinner wall still shells.
|
||||
/// as (or thicker than) the inradius leaves no cavity. The default path (the split
|
||||
/// flags are the only path since #2981) refuses it as no closed solid (1001); with
|
||||
/// WR-454's own opt-in the typed empty-cavity refusal (1007) takes precedence; a
|
||||
/// thinner wall still shells.
|
||||
#[test]
|
||||
fn issue_380_shell_with_no_cavity_fails_with_the_fix() {
|
||||
let shell = |fix: bool, wall: f64| -> Result<Brep> {
|
||||
@@ -5494,9 +5495,9 @@ mod tests {
|
||||
.result
|
||||
};
|
||||
for wall in [5.0, 6.0] {
|
||||
// upstream: an empty offset is "not a manifold solid", returned as a success
|
||||
let upstream = shell(false, wall).unwrap();
|
||||
assert_eq!(bp1::num_faces(&upstream), 0);
|
||||
// the default path: an offset that is no closed solid is refused (1001)
|
||||
assert_status(shell(false, wall), 1001);
|
||||
// WR-454's opt-in keeps its own typed refusal (1007)
|
||||
assert_status(shell(true, wall), 1007);
|
||||
}
|
||||
let thin = shell(true, 1.0).unwrap();
|
||||
|
||||
@@ -310,10 +310,8 @@ impl OffsetExecutive {
|
||||
notifications: &mut OffsetNotifications<'_>,
|
||||
) -> Result<()> {
|
||||
let approx_tol = self.approximation_tol_3d();
|
||||
// (WR-657, opt-in) the corners whose offset planes no longer meet in one point
|
||||
if kernel.fixes.offset_splits_corner_vertices {
|
||||
self.split_corner_vertices(kernel, original, b, approx_tol, notifications)?;
|
||||
}
|
||||
// (WR-657) the corners whose offset planes no longer meet in one point
|
||||
self.split_corner_vertices(kernel, original, b, approx_tol, notifications)?;
|
||||
// (fork ccbf77a) each edge that closed to a point: its offset face and the original
|
||||
// face across it
|
||||
let mut closed_edges: Vec<(FaceId, Option<FaceId>)> = Vec::new();
|
||||
|
||||
@@ -40,8 +40,8 @@
|
||||
//! | `offset_vertex_image` (private) | not upstream: a vertex's image on its face's offset (WR-736; opt-in) |
|
||||
//!
|
||||
//! Deviations:
|
||||
//! - **Four faces at a vertex (WR-666, opt-in).** With `Kernel::fixes.offset_four_face_vertex_split`
|
||||
//! and `offset_closes_loop_gaps`, the fork's tangency-labs/smlib@2e1bb8c: the tangency
|
||||
//! - **Four faces at a vertex (WR-666, the only path).** The fork's
|
||||
//! tangency-labs/smlib@2e1bb8c: the tangency
|
||||
//! tolerance is half a degree, `trim_extended_edge` keeps a near-tangent trim point where
|
||||
//! the curve passes through the surface by more than the tolerance, and at an all-convex or all-concave vertex takes the cut
|
||||
//! reached first from the edge's other end. That choice also skips, on an open edge's
|
||||
@@ -1558,14 +1558,6 @@ fn is_arc(k: &Kernel, c: &BSplineCurve, num_samples: u64, tol: f64) -> Result<Op
|
||||
|
||||
// ---- the geometry creation --------------------------------------------------------------------
|
||||
|
||||
/// Whether WR-666's opt-in is in effect: `Kernel::fixes.offset_four_face_vertex_split`
|
||||
/// together with `offset_closes_loop_gaps` (the fork's `CloseLoopGaps`, which closes the
|
||||
/// loop gaps the split vertex leaves on the faces round it; without it the flag has no
|
||||
/// effect, as the split alone leaves those faces' loops open).
|
||||
pub(crate) fn four_face_vertex_split(fixes: &crate::kernel::UpstreamFixes) -> bool {
|
||||
fixes.offset_four_face_vertex_split && fixes.offset_closes_loop_gaps
|
||||
}
|
||||
|
||||
/// Whether `curve` at `t` passes through `surface` at `uv` (where they meet) by more than
|
||||
/// `tol`. To second order in arc length s the curve's height over the surface is
|
||||
/// `g1 s + g2 s^2 / 2`, `g1` the sine of the angle between them and `g2` the curve's
|
||||
@@ -1589,7 +1581,18 @@ fn passes_through(
|
||||
let err = || sm_err("offset_geometry_creation::passes_through");
|
||||
let pv = curve.evaluate(k, t, 2, true, true)?;
|
||||
let (d1, d2) = (*pv.get(1).ok_or_else(err)?, *pv.get(2).ok_or_else(err)?);
|
||||
let s = surface.evaluate(k, uv, 2, 2, EvaluateFlags::default())?;
|
||||
// Only the total-order-two jet is used below. The full rectangle would also
|
||||
// request D(2, 2), beyond an implicit offset's derivative limit.
|
||||
let s = surface.evaluate(
|
||||
k,
|
||||
uv,
|
||||
2,
|
||||
2,
|
||||
EvaluateFlags {
|
||||
only_upper_half: true,
|
||||
..EvaluateFlags::default()
|
||||
},
|
||||
)?;
|
||||
let at = |i: usize, j: usize| s.get(i).and_then(|r| r.get(j)).copied().ok_or_else(err);
|
||||
let (du, dv, duu, duv, dvv) = (at(1, 0)?, at(0, 1)?, at(2, 0)?, at(1, 1)?, at(0, 2)?);
|
||||
let n = du.cross(dv).unitize().map_err(|_| err())?;
|
||||
@@ -1635,17 +1638,12 @@ impl OffsetGeometryCreation {
|
||||
}
|
||||
}
|
||||
|
||||
/// The constructor's angular tolerance (`m_dTangencyTolRadians`): `2.0 * PI / 180.0`;
|
||||
/// with `Kernel::fixes.offset_four_face_vertex_split` and `offset_closes_loop_gaps`
|
||||
/// (WR-666), half a degree,
|
||||
/// `0.5 * PI / 180.0`, as the fork's tangency-labs/smlib@2e1bb8c.
|
||||
/// The constructor's angular tolerance (`m_dTangencyTolRadians`): half a degree,
|
||||
/// `0.5 * PI / 180.0` (WR-666, the fork's tangency-labs/smlib@2e1bb8c), not upstream's
|
||||
/// two degrees, on the default path since #2981.
|
||||
#[must_use]
|
||||
pub fn default_tangency_tol_radians(fixes: &crate::kernel::UpstreamFixes) -> f64 {
|
||||
if four_face_vertex_split(fixes) {
|
||||
0.5 * PI / 180.0
|
||||
} else {
|
||||
2.0 * PI / 180.0
|
||||
}
|
||||
pub fn default_tangency_tol_radians() -> f64 {
|
||||
0.5 * PI / 180.0
|
||||
}
|
||||
|
||||
/// The same creator with the angular tolerance `tangency_tol_radians`, which an
|
||||
@@ -2818,8 +2816,7 @@ impl OffsetGeometryCreation {
|
||||
|
||||
// for every sOffAdjEU - try letting trimPoint = OffAdjEu->ExtendedSurface/3DCurve intersection
|
||||
let mut last_face: Option<O> = None;
|
||||
// (WR-666, opt-in) each adjacent face's candidates in `solutions`
|
||||
let four_face = four_face_vertex_split(&k.fixes);
|
||||
// (WR-666) each adjacent face's candidates in `solutions`
|
||||
let mut face_solutions: Vec<(usize, usize)> = Vec::new();
|
||||
for &off_adj_eu in &off_adj_eus {
|
||||
let f = brep
|
||||
@@ -2857,9 +2854,7 @@ impl OffsetGeometryCreation {
|
||||
|
||||
// Note that the adjacent surface intersection overrides tangent intersections
|
||||
// Test the solutions to see if they are not nearly tangent.
|
||||
if four_face {
|
||||
face_solutions.push((solutions.len(), solutions.len()));
|
||||
}
|
||||
face_solutions.push((solutions.len(), solutions.len()));
|
||||
for sol in sols2.as_slice() {
|
||||
// (neither status is checked upstream: a failed evaluation leaves its
|
||||
// output at SmPoint3d's SM_UNDEF_DOUBLE default)
|
||||
@@ -2885,7 +2880,7 @@ impl OffsetGeometryCreation {
|
||||
.unitize_or_z();
|
||||
let angle_rad = tangent.angle_between(normal)?;
|
||||
let angle_deg = angle_rad * 180.0 / PI;
|
||||
// `Kernel::fixes.offset_four_face_vertex_split` (WR-666, the fork's
|
||||
// Four-face vertex splitting (WR-666, the fork's
|
||||
// tangency-labs/smlib@2e1bb8c): a curve that crosses the surface at a
|
||||
// shallow angle crosses it all the same (a trough's floor facets falling
|
||||
// 1 degree, whose offsets cut the offset edges along the walls 0.6
|
||||
@@ -2895,8 +2890,7 @@ impl OffsetGeometryCreation {
|
||||
// round meets the top face tangentially) crosses it twice within the
|
||||
// tolerance, so it crosses only where it passes through by more than
|
||||
// the tolerance ([`passes_through`]).
|
||||
let crosses = four_face
|
||||
&& !(angle_deg < 85.0 || angle_deg > 95.0)
|
||||
let crosses = !(angle_deg < 85.0 || angle_deg > 95.0)
|
||||
&& passes_through(
|
||||
k,
|
||||
curve_of(brep, curve_3d)?,
|
||||
@@ -2981,7 +2975,7 @@ impl OffsetGeometryCreation {
|
||||
}
|
||||
}
|
||||
|
||||
// `Kernel::fixes.offset_four_face_vertex_split` (WR-666, the fork's
|
||||
// Four-face vertex splitting (WR-666, the fork's
|
||||
// tangency-labs/smlib@2e1bb8c): where more than three faces meet at the vertex,
|
||||
// the faces on either side of the edge's ends there are different faces, and
|
||||
// their offsets cut the edge's offset at two points: four offset planes do not
|
||||
@@ -2989,7 +2983,7 @@ impl OffsetGeometryCreation {
|
||||
// and where every edge at the vertex is convex (or every one concave) the edge
|
||||
// ends at the one of the two cuts its offset reaches first coming from its
|
||||
// other end. With a vertex of mixed edges the nearest point stays.
|
||||
if four_face && found && face_solutions.len() == 2 {
|
||||
if found && face_solutions.len() == 2 {
|
||||
if let Some(param) = self.first_cut_from_far_end(
|
||||
k,
|
||||
brep,
|
||||
@@ -3067,9 +3061,9 @@ impl OffsetGeometryCreation {
|
||||
}
|
||||
}
|
||||
|
||||
/// `TrimExtendedEdge`'s choice at a vertex of more than three faces under
|
||||
/// `Kernel::fixes.offset_four_face_vertex_split` (WR-666, the fork's
|
||||
/// tangency-labs/smlib@2e1bb8c): when every edge at `base_vertex` is convex (or every
|
||||
/// `TrimExtendedEdge`'s choice at a vertex of more than three faces under WR-666
|
||||
/// (the fork's tangency-labs/smlib@2e1bb8c; the split is the only path since
|
||||
/// #2981): when every edge at `base_vertex` is convex (or every
|
||||
/// one concave; a lamina, tangent or shell-less edge counts as neither) and each of
|
||||
/// the two adjacent faces (`face_solutions`, ranges of `solutions`) has a cut nearest
|
||||
/// the vertex (not the start already chosen, nor, on an open edge's second end, the
|
||||
@@ -6088,6 +6082,36 @@ mod tests {
|
||||
assert!(passes_through(&k, &shallow, 0.5, &plane, uv, tol).unwrap());
|
||||
}
|
||||
|
||||
/// The crossing test uses only the second-order jet. An implicit offset keeps
|
||||
/// its three-total-derivative limit, yet must classify the same plane's touch
|
||||
/// and shallow crossing (the unsupported request prevented #1818's rim mating).
|
||||
#[test]
|
||||
fn a_curve_passes_through_an_implicit_offset_using_only_second_order() {
|
||||
use crate::surface::offset_surface::OffsetSurface;
|
||||
use crate::surface::plane::Plane;
|
||||
|
||||
let k = Kernel::default();
|
||||
let base = Surface::from(Plane::from_normal(Vec3::new(0.0, 0.0, -0.5), Vec3::Z));
|
||||
let surface = Surface::from(OffsetSurface::new(&k, 0.5, base, true, 0).unwrap());
|
||||
let uv = Point2::new(0.0, 0.0);
|
||||
assert!(surface.evaluate_point(&k, uv).unwrap().distance(Vec3::ZERO) < 1e-15);
|
||||
assert_eq!(
|
||||
surface
|
||||
.evaluate(&k, uv, 2, 2, EvaluateFlags::default())
|
||||
.unwrap_err(),
|
||||
SolidError::sm_err("surface::offset_surface::OffsetSurface::evaluate")
|
||||
);
|
||||
|
||||
let tol = 1e-5;
|
||||
let at = |e: f64| (1.0 + (6.0 * e).sqrt()) / 2.0;
|
||||
let touching = over_the_plane(1e-6, 1.0 / 3.0, true);
|
||||
assert!(!passes_through(&k, &touching, at(1e-6), &surface, uv, tol).unwrap());
|
||||
let lifted = over_the_plane(1e-3, 1.0 / 3.0, true);
|
||||
assert!(passes_through(&k, &lifted, at(1e-3), &surface, uv, tol).unwrap());
|
||||
let shallow = over_the_plane(0.6_f64.to_radians().tan(), 0.0, false);
|
||||
assert!(passes_through(&k, &shallow, 0.5, &surface, uv, tol).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creation_defaults() {
|
||||
let g = OffsetGeometryCreation::new(0.25, 1e-5);
|
||||
|
||||
@@ -1858,18 +1858,21 @@ mod tests {
|
||||
/// gen0091 of the difftest unit: a pyramid's side given the plane through its edges and
|
||||
/// the side across its apex from it moved out. The four sides no longer meet in one point:
|
||||
/// the apex splits into two vertices and a new edge between the two sides the moved one
|
||||
/// parts (with the four-face split on). The new vertex is next to the apex, a pole of the
|
||||
/// parts on the default split path. The new vertex is next to the apex, a pole of the
|
||||
/// fourth side's B-spline patch, and on no extension of it (a side's extension narrows to
|
||||
/// nothing at the pole): the side, which lies on a plane, continues as that plane. The
|
||||
/// body is the convex solid of the five planes, a sound manifold of nine edges and six
|
||||
/// vertices.
|
||||
#[test]
|
||||
fn a_pyramid_side_moved_past_its_apex_splits_it_on_the_planes_beside() {
|
||||
assert_pyramid_split_on_its_five_planes([2, 1]);
|
||||
}
|
||||
|
||||
/// Both selections describe the same five-plane convex solid. Check its
|
||||
/// independent plane-intersection vertices as well as its volume and validity.
|
||||
fn assert_pyramid_split_on_its_five_planes(selection: [usize; 2]) {
|
||||
use crate::topology::brep::validation::{check_brep, CheckLevel};
|
||||
let mut k = Kernel::default();
|
||||
k.fixes.offset_closes_loop_gaps = true;
|
||||
k.fixes.offset_splits_corner_vertices = true;
|
||||
k.fixes.offset_four_face_vertex_split = true;
|
||||
let mut b = bicubic_pyramid(&mut k);
|
||||
let all = bp1::faces(&b, None).unwrap();
|
||||
// the planes of the faces, each as (point, outward unit normal)
|
||||
@@ -1885,14 +1888,15 @@ mod tests {
|
||||
})
|
||||
.collect();
|
||||
// faces: the base, then the sides along y = -y0, x = x0, y = y0, x = -x0
|
||||
let (kept, moved, distance) = (2_usize, 1_usize, 0.407_738_046_172_282_6);
|
||||
let (moved, distance) = (1_usize, 0.407_738_046_172_282_6);
|
||||
let mut given = planes.clone();
|
||||
given[moved].0 = given[moved].0 + given[moved].1 * distance;
|
||||
let surfaces = [kept, moved]
|
||||
let surfaces = selection
|
||||
.map(|i| Surface::from(Plane::from_normal(given[i].0, given[i].1)))
|
||||
.to_vec();
|
||||
let selected_faces = selection.map(|i| all[i]);
|
||||
with_notifications(&mut b, |b, notes| {
|
||||
local_operation(&mut k, b, &[all[kept], all[moved]], surfaces, notes)
|
||||
local_operation(&mut k, b, &selected_faces, surfaces, notes)
|
||||
})
|
||||
.unwrap();
|
||||
let (v, manifold) = volume_and_manifold(&k, &mut b);
|
||||
@@ -1902,10 +1906,14 @@ mod tests {
|
||||
(5, 9, 6)
|
||||
);
|
||||
b.use_arena_naming().unwrap();
|
||||
let lines = check_brep(&mut k, &mut b, CheckLevel::Level2)
|
||||
.unwrap()
|
||||
.lines();
|
||||
assert!(lines.is_empty(), "{lines:?}");
|
||||
for level in [CheckLevel::Level0, CheckLevel::Level1, CheckLevel::Level2] {
|
||||
let check = check_brep(&mut k, &mut b, level).unwrap();
|
||||
assert!(
|
||||
check.is_valid() && check.failure_count() == 0,
|
||||
"{selection:?} {level:?}: {:?}",
|
||||
check.lines()
|
||||
);
|
||||
}
|
||||
// the convex solid of the five planes: its corners, and its volume from its faces'
|
||||
// fans about the base's centre
|
||||
let meet = |[i, j, l]: [usize; 3]| {
|
||||
@@ -1916,6 +1924,25 @@ mod tests {
|
||||
let corner = |i: usize, j: usize| meet([0, i, j]);
|
||||
let (b0, b1, b2, b3) = (corner(4, 1), corner(1, 2), corner(2, 3), corner(3, 4));
|
||||
let (apex, top) = (meet([2, 3, 4]), meet([1, 2, 4]));
|
||||
let expected_vertices = [b0, b1, b2, b3, apex, top];
|
||||
let actual_vertices: Vec<_> = b.vertices().iter().map(|(_, v)| v.point).collect();
|
||||
let scale = expected_vertices
|
||||
.iter()
|
||||
.map(|p| p.max_dimension())
|
||||
.fold(0.0, f64::max);
|
||||
let same_point = |p: Vec3, q: Vec3| p.distance(q) <= 1e-9 * scale;
|
||||
for &p in &actual_vertices {
|
||||
assert!(
|
||||
expected_vertices.iter().any(|&q| same_point(p, q)),
|
||||
"{selection:?}: unexpected vertex {p:?}"
|
||||
);
|
||||
}
|
||||
for &p in &expected_vertices {
|
||||
assert!(
|
||||
actual_vertices.iter().any(|&q| same_point(p, q)),
|
||||
"{selection:?}: missing plane intersection {p:?}"
|
||||
);
|
||||
}
|
||||
let faces: [&[Vec3]; 5] = [
|
||||
&[b0, b3, b2, b1],
|
||||
&[b0, b1, top],
|
||||
@@ -2355,33 +2382,12 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// gen0091 of the difftest unit with the default fixes: the pyramid's side moved past
|
||||
/// its apex needs the four-face vertex split (opt-in), without which every end merges
|
||||
/// into one vertex grown to cover the split: not sound, refused.
|
||||
/// The former flag-off refusal is no longer a default behaviour. With the same
|
||||
/// two replacement planes selected in reverse order, the unconditional split
|
||||
/// must still build the analytically checked five-plane solid, not just succeed.
|
||||
#[test]
|
||||
fn a_pyramid_side_moved_past_its_apex_is_refused_without_the_four_face_split() {
|
||||
let mut k = Kernel::default();
|
||||
let mut b = bicubic_pyramid(&mut k);
|
||||
let all = bp1::faces(&b, None).unwrap();
|
||||
let plane_of = |b: &Brep, k: &Kernel, f: FaceId, d: f64| {
|
||||
let s = face_ops::surface(b, f).unwrap().unwrap();
|
||||
let uv = b.face(f).unwrap().uv_domain.evaluate(0.5, 0.5);
|
||||
let p = s.evaluate_point(k, uv).unwrap();
|
||||
let n = s.evaluate_normal(k, uv, Side::Left, Side::Left).unwrap();
|
||||
let n = if n.dot(p - Vec3::new(0.0, 0.0, 25.0)) > 0.0 {
|
||||
n
|
||||
} else {
|
||||
n * -1.0
|
||||
};
|
||||
Surface::from(Plane::from_normal(p + n * d, n))
|
||||
};
|
||||
// faces: the base, then the sides along y = -y0, x = x0, y = y0, x = -x0
|
||||
let surfaces = vec![
|
||||
plane_of(&b, &k, all[2], 0.0),
|
||||
plane_of(&b, &k, all[1], 0.407_738_046_172_282_6),
|
||||
];
|
||||
let why = refused_unchanged_or_returned(&mut k, &mut b, &[all[2], all[1]], surfaces);
|
||||
assert_eq!(why, Some(unsound_check(2, 61)));
|
||||
fn a_pyramid_side_split_is_independent_of_replacement_selection_order() {
|
||||
assert_pyramid_split_on_its_five_planes([1, 2]);
|
||||
}
|
||||
|
||||
/// A 20 mm cube of bilinear B-spline faces whose bottom (z = 0) carries a defect of its
|
||||
|
||||
@@ -36,6 +36,7 @@ use solidcore::offset::offset_executive::p1::OffsetExecutive;
|
||||
use solidcore::offset::offset_executive::p2::{OffsetOptions, ShellOptions};
|
||||
use solidcore::primitive::api_primitives::create_box;
|
||||
use solidcore::query::api_queries::brep_is_manifold_solid;
|
||||
use solidcore::topology::brep::validation::{check_brep, CheckLevel};
|
||||
use solidcore::{Brep, Kernel, SolidError};
|
||||
|
||||
/// The block with the rib `inset` in from its outer wall x = 0.
|
||||
@@ -45,6 +46,18 @@ fn ribbed(k: &mut Kernel, inset: f64) -> Brep {
|
||||
boolean_union(k, block, rib).unwrap()
|
||||
}
|
||||
|
||||
fn shell_options() -> ShellOptions {
|
||||
ShellOptions {
|
||||
offset: OffsetOptions {
|
||||
distance: -0.5,
|
||||
extend_convex_edges: true,
|
||||
merge_results: true,
|
||||
},
|
||||
create_offset_solid: true,
|
||||
merge_sheet_side_walls: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// The part shelled 0.5 inward open at the rib's outer side, under `fixes`.
|
||||
fn shell(fixes: &UpstreamFixes, inset: f64) -> solidcore::Result<(bool, f64)> {
|
||||
let mut k = Kernel::default();
|
||||
@@ -57,15 +70,7 @@ fn shell(fixes: &UpstreamFixes, inset: f64) -> solidcore::Result<(bool, f64)> {
|
||||
let mut wall = OffsetExecutive::shell_brep(
|
||||
&mut k,
|
||||
part,
|
||||
ShellOptions {
|
||||
offset: OffsetOptions {
|
||||
distance: -0.5,
|
||||
extend_convex_edges: true,
|
||||
merge_results: true,
|
||||
},
|
||||
create_offset_solid: true,
|
||||
merge_sheet_side_walls: true,
|
||||
},
|
||||
shell_options(),
|
||||
&open,
|
||||
&mut notifications.context(),
|
||||
)
|
||||
@@ -83,13 +88,37 @@ fn crossing(walls_touch: bool) -> UpstreamFixes {
|
||||
f
|
||||
}
|
||||
|
||||
/// Upstream (crossing loops not resolved): the merge returns no manifold solid (ten
|
||||
/// edges that are not manifold, two regions, 23 faces); the shell refuses it
|
||||
/// (`OffsetFailure::Unsound`, `NotManifoldSolid`, #3094) instead of returning it.
|
||||
/// Without resolving the crossing loops, the cavity is open. The default shell
|
||||
/// refuses that non-manifold result with `OffsetFailure::Unsound` rather than
|
||||
/// returning it, retaining the input.
|
||||
#[test]
|
||||
fn default_fixes_refuse_the_shell_that_is_no_solid() {
|
||||
let error = shell(&UpstreamFixes::default(), 0.15).unwrap_err();
|
||||
let SolidError::Failed(failure) = &error else {
|
||||
fn default_fixes_refuse_the_open_cavity_and_retain_the_input() {
|
||||
let mut k = Kernel::default();
|
||||
let mut part = ribbed(&mut k, 0.15);
|
||||
part.refresh_cached_topology();
|
||||
let open = planar_faces_at(&k, &part, 0, 0.15);
|
||||
assert_eq!(open.len(), 1);
|
||||
let mut notifications = Notifications::new(&mut part);
|
||||
let key = part.key();
|
||||
let ids = (
|
||||
part.face_list().to_vec(),
|
||||
part.edge_list().to_vec(),
|
||||
part.vertex_list().to_vec(),
|
||||
);
|
||||
let points: Vec<_> = part
|
||||
.vertices()
|
||||
.iter()
|
||||
.map(|(id, v)| (id, v.point, v.tolerance))
|
||||
.collect();
|
||||
let outcome = OffsetExecutive::shell_brep(
|
||||
&mut k,
|
||||
part,
|
||||
shell_options(),
|
||||
&open,
|
||||
&mut notifications.context(),
|
||||
);
|
||||
let error = outcome.result.as_ref().unwrap_err();
|
||||
let SolidError::Failed(failure) = error else {
|
||||
panic!("not a refusal: {error}");
|
||||
};
|
||||
assert_eq!(
|
||||
@@ -98,6 +127,35 @@ fn default_fixes_refuse_the_shell_that_is_no_solid() {
|
||||
what: Unsoundness::NotManifoldSolid
|
||||
})
|
||||
);
|
||||
assert!(!outcome.input_ownership_transferred());
|
||||
let mut retained = outcome.retained_input.unwrap();
|
||||
assert_eq!(retained.key(), key);
|
||||
assert_eq!(
|
||||
(
|
||||
retained.face_list().to_vec(),
|
||||
retained.edge_list().to_vec(),
|
||||
retained.vertex_list().to_vec()
|
||||
),
|
||||
ids
|
||||
);
|
||||
assert_eq!(
|
||||
retained
|
||||
.vertices()
|
||||
.iter()
|
||||
.map(|(id, v)| (id, v.point, v.tolerance))
|
||||
.collect::<Vec<_>>(),
|
||||
points
|
||||
);
|
||||
// Do not refresh the retained caches: the operation must leave them coherent.
|
||||
assert!(solidcore::topology::integrity::check(&retained).is_ok());
|
||||
assert!(brep_is_manifold_solid(&retained).unwrap());
|
||||
for level in [CheckLevel::Level0, CheckLevel::Level1, CheckLevel::Level2] {
|
||||
let checked = check_brep(&mut k, &mut retained, level).unwrap();
|
||||
assert!(checked.is_valid(), "{level:?}: {:?}", checked.lines());
|
||||
}
|
||||
let v = volume(&k, &mut retained).unwrap();
|
||||
// The unchanged block (2000) and rib (3 * 16 * 3), not a partially shelled body.
|
||||
assert!((v - 2144.0).abs() <= 1e-9 * 2144.0, "{v}");
|
||||
}
|
||||
|
||||
/// The floor under the opening is kept: the shell is the closed form (with and without
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
//! The SolidCraft fork's tangency-labs/smlib@2e1bb8c under
|
||||
//! `Kernel::fixes.offset_four_face_vertex_split` with `offset_closes_loop_gaps` (WR-666,
|
||||
//! #2052), on the commit message's two cases, rebuilt:
|
||||
//! The SolidCraft fork's tangency-labs/smlib@2e1bb8c (WR-666, its #2052, the default
|
||||
//! path since #2981): a vertex where four plane faces meet splits into the edge their
|
||||
//! two pairs' planes give, once; the loop gaps the split leaves close, and the
|
||||
//! shell/inset then reaches its closed form. On the commit message's two cases,
|
||||
//! rebuilt:
|
||||
//!
|
||||
//! - a trough 60 x 40 x 20 whose floor is four facets falling to a drain at its middle (the
|
||||
//! long sides' facets fall 1 degree, the short sides' 2/3 degree, adjacent facets meet at
|
||||
@@ -307,17 +309,9 @@ fn shell_pocket(fixes: &UpstreamFixes) -> Result<Shelled, String> {
|
||||
shell(fixes, pocket, SLAB[2], POCKET_T)
|
||||
}
|
||||
|
||||
/// The opt-in with the loop-gap closing it builds on (`offset_closes_loop_gaps`, WR-578).
|
||||
/// The split pieces shell to their closed forms on the default path.
|
||||
fn split() -> UpstreamFixes {
|
||||
let mut f = gaps_only();
|
||||
f.offset_four_face_vertex_split = true;
|
||||
f
|
||||
}
|
||||
|
||||
fn gaps_only() -> UpstreamFixes {
|
||||
let mut f = UpstreamFixes::default();
|
||||
f.offset_closes_loop_gaps = true;
|
||||
f
|
||||
UpstreamFixes::default()
|
||||
}
|
||||
|
||||
fn assert_closed_form(got: &Shelled, expected: f64, faces: usize, label: &str) {
|
||||
@@ -343,58 +337,19 @@ fn the_parts_have_their_closed_form_volumes() {
|
||||
assert!((v - pocket_solid_volume()).abs() <= 1e-9 * v, "{v}");
|
||||
}
|
||||
|
||||
/// Upstream (the default fixes) refuses the trough's shell: the merge fails (1001).
|
||||
#[test]
|
||||
fn default_fixes_refuse_the_trough_shell() {
|
||||
let got = shell_trough(&UpstreamFixes::default());
|
||||
assert_eq!(
|
||||
got,
|
||||
Err("boolean::merge::p2::make_faceuse: upstream status 1001".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
/// The loop-gap closing alone does not build it either: the corners and the drain are
|
||||
/// still one vertex each.
|
||||
#[test]
|
||||
fn loop_gap_closing_alone_refuses_the_trough_shell() {
|
||||
let got = shell_trough(&gaps_only());
|
||||
assert!(got.is_err(), "{got:?}");
|
||||
}
|
||||
|
||||
/// The flag without `offset_closes_loop_gaps` has no effect.
|
||||
#[test]
|
||||
fn the_flag_alone_is_the_default() {
|
||||
let mut f = UpstreamFixes::default();
|
||||
f.offset_four_face_vertex_split = true;
|
||||
assert_eq!(shell_trough(&f), shell_trough(&UpstreamFixes::default()));
|
||||
assert_eq!(shell_pocket(&f), shell_pocket(&UpstreamFixes::default()));
|
||||
}
|
||||
|
||||
/// With the opt-in the trough (every edge at its corners and drain convex) shells to the
|
||||
/// closed form: the 8 outer faces, their 8 offsets and the rim.
|
||||
/// The trough (every edge at its corners and drain convex) shells to the
|
||||
/// closed form on the default path: the 8 outer faces, their 8 offsets and the rim.
|
||||
#[test]
|
||||
fn trough_shells_to_its_closed_form() {
|
||||
let got = shell_trough(&split()).unwrap();
|
||||
assert_closed_form(&got, trough_shell_volume(), 17, "trough");
|
||||
}
|
||||
|
||||
/// With the opt-in the pocket (every edge at its corners and drain concave) shells to the
|
||||
/// closed form: the slab's 13 faces below the top, their 13 offsets and the two rims (round the outside and round the pocket).
|
||||
/// The pocket (every edge at its corners and drain concave) shells to the
|
||||
/// closed form on the default path: the slab's 13 faces below the top, their 13 offsets
|
||||
/// and the two rims (round the outside and round the pocket).
|
||||
#[test]
|
||||
fn pocket_shells_to_its_closed_form() {
|
||||
let got = shell_pocket(&split()).unwrap();
|
||||
assert_closed_form(&got, pocket_shell_volume(), 28, "pocket");
|
||||
}
|
||||
|
||||
/// Upstream (the default fixes) returns the pocket's shell, a manifold solid, at the wrong
|
||||
/// volume (43302.668, the closed form 43522.321); the loop-gap closing alone refuses it.
|
||||
#[test]
|
||||
fn without_the_flag_the_pocket_shell_is_wrong_or_refused() {
|
||||
let got = shell_pocket(&UpstreamFixes::default()).unwrap();
|
||||
assert!(
|
||||
(got.volume - pocket_shell_volume()).abs() > 100.0,
|
||||
"{got:?} vs {}",
|
||||
pocket_shell_volume()
|
||||
);
|
||||
assert!(shell_pocket(&gaps_only()).is_err());
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ use solidcore::offset::offset_executive::p1::OffsetExecutive;
|
||||
use solidcore::offset::offset_executive::p2::{OffsetOptions, ShellOptions};
|
||||
use solidcore::primitive::api_primitives::create_box;
|
||||
use solidcore::query::api_queries::brep_is_manifold_solid;
|
||||
use solidcore::topology::brep::validation::{check_brep, CheckLevel};
|
||||
use solidcore::{Brep, Kernel, SolidError};
|
||||
|
||||
/// The frame, the ring cut's top at `ring_top` (2.5: a 0.5 floor under the recess).
|
||||
@@ -48,6 +49,18 @@ fn frame(k: &mut Kernel, ring_top: f64) -> Brep {
|
||||
boolean_difference(k, recessed, ring).unwrap()
|
||||
}
|
||||
|
||||
fn shell_options() -> ShellOptions {
|
||||
ShellOptions {
|
||||
offset: OffsetOptions {
|
||||
distance: -0.5,
|
||||
extend_convex_edges: true,
|
||||
merge_results: true,
|
||||
},
|
||||
create_offset_solid: true,
|
||||
merge_sheet_side_walls: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// The frame shelled 0.5 inward open at the ring cut's outer wall (x = 18), under
|
||||
/// `fixes`: the shell's result.
|
||||
fn shell_result(fixes: &UpstreamFixes, ring_top: f64) -> (Kernel, solidcore::Result<Brep>) {
|
||||
@@ -61,15 +74,7 @@ fn shell_result(fixes: &UpstreamFixes, ring_top: f64) -> (Kernel, solidcore::Res
|
||||
let mut wall = OffsetExecutive::shell_brep(
|
||||
&mut k,
|
||||
part,
|
||||
ShellOptions {
|
||||
offset: OffsetOptions {
|
||||
distance: -0.5,
|
||||
extend_convex_edges: true,
|
||||
merge_results: true,
|
||||
},
|
||||
create_offset_solid: true,
|
||||
merge_sheet_side_walls: true,
|
||||
},
|
||||
shell_options(),
|
||||
&open,
|
||||
&mut notifications.context(),
|
||||
)
|
||||
@@ -95,15 +100,38 @@ fn keep_cavity() -> UpstreamFixes {
|
||||
f
|
||||
}
|
||||
|
||||
/// Upstream (flag off): the sliver between the floor's crossing offsets is kept too and
|
||||
/// the result is no manifold solid (five regions, eight edges that are not manifold); the
|
||||
/// shell refuses it (`OffsetFailure::Unsound`, `NotManifoldSolid`, #3094) instead of
|
||||
/// returning it.
|
||||
/// With cavity selection off, the sliver between the crossing offsets leaves no
|
||||
/// manifold cavity. The shell refuses it with `OffsetFailure::Unsound` and retains
|
||||
/// the original frame.
|
||||
#[test]
|
||||
fn flag_off_refuses_the_shell_with_the_sliver_region() {
|
||||
let (_, result) = shell_result(&UpstreamFixes::default(), 2.5);
|
||||
let error = result.unwrap_err();
|
||||
let SolidError::Failed(failure) = &error else {
|
||||
fn flag_off_refuses_the_sliver_and_retains_the_input() {
|
||||
let mut k = Kernel::default();
|
||||
let mut part = frame(&mut k, 2.5);
|
||||
part.refresh_cached_topology();
|
||||
let open = planar_faces_at(&k, &part, 0, 18.0);
|
||||
assert_eq!(open.len(), 1);
|
||||
let mut notifications = Notifications::new(&mut part);
|
||||
let key = part.key();
|
||||
let tolerance = part.tolerance();
|
||||
let ids = (
|
||||
part.face_list().to_vec(),
|
||||
part.edge_list().to_vec(),
|
||||
part.vertex_list().to_vec(),
|
||||
);
|
||||
let points: Vec<_> = part
|
||||
.vertices()
|
||||
.iter()
|
||||
.map(|(id, v)| (id, v.point, v.tolerance))
|
||||
.collect();
|
||||
let outcome = OffsetExecutive::shell_brep(
|
||||
&mut k,
|
||||
part,
|
||||
shell_options(),
|
||||
&open,
|
||||
&mut notifications.context(),
|
||||
);
|
||||
let error = outcome.result.as_ref().unwrap_err();
|
||||
let SolidError::Failed(failure) = error else {
|
||||
panic!("not a refusal: {error}");
|
||||
};
|
||||
assert_eq!(
|
||||
@@ -112,6 +140,36 @@ fn flag_off_refuses_the_shell_with_the_sliver_region() {
|
||||
what: Unsoundness::NotManifoldSolid
|
||||
})
|
||||
);
|
||||
assert!(!outcome.input_ownership_transferred());
|
||||
let mut retained = outcome.retained_input.unwrap();
|
||||
assert_eq!(retained.key(), key);
|
||||
assert_eq!(retained.tolerance(), tolerance);
|
||||
assert_eq!(
|
||||
(
|
||||
retained.face_list().to_vec(),
|
||||
retained.edge_list().to_vec(),
|
||||
retained.vertex_list().to_vec()
|
||||
),
|
||||
ids
|
||||
);
|
||||
assert_eq!(
|
||||
retained
|
||||
.vertices()
|
||||
.iter()
|
||||
.map(|(id, v)| (id, v.point, v.tolerance))
|
||||
.collect::<Vec<_>>(),
|
||||
points
|
||||
);
|
||||
// Check the returned caches as-is, without refreshing away an inconsistency.
|
||||
assert!(solidcore::topology::integrity::check(&retained).is_ok());
|
||||
assert!(brep_is_manifold_solid(&retained).unwrap());
|
||||
for level in [CheckLevel::Level0, CheckLevel::Level1, CheckLevel::Level2] {
|
||||
let checked = check_brep(&mut k, &mut retained, level).unwrap();
|
||||
assert!(checked.is_valid(), "{level:?}: {:?}", checked.lines());
|
||||
}
|
||||
let v = volume(&k, &mut retained).unwrap();
|
||||
// Block 2000, less the recess 200 and square ring (256 - 36) * 2.5 = 550.
|
||||
assert!((v - 1250.0).abs() <= 1e-9 * 1250.0, "{v}");
|
||||
}
|
||||
|
||||
/// The flag: only the cavity is made manifold; the shell is the closed form.
|
||||
|
||||
@@ -4,12 +4,14 @@
|
||||
//! and the hole r 7, axes 2.5 apart, so the hole's tube passes out through the arc's offset.
|
||||
//! SolidCraft's `smlib_shell` (the fork's C++) reports 1001 on its plate. Pinned SMLib
|
||||
//! 1.0.1 shells this reconstruction open at the top and fails it open at y = 20 with
|
||||
//! `SM_ERR` (1001), through `SmApiShellBrepFull` and through a bare `ShellBrep`; the port
|
||||
//! does the same, with the same values (the `offset.offset_executive_p2` cases
|
||||
//! `SM_ERR` (1001), through `SmApiShellBrepFull` and through a bare `ShellBrep` (the
|
||||
//! historical `offset.offset_executive_p2` cases
|
||||
//! `issue1795_plate_{api,static}_shell_2_*`; WR-617).
|
||||
//!
|
||||
//! `shell_open_at_the_top_shells_as_upstream` and `shell_open_at_y20_fails_as_upstream`
|
||||
//! pin those default results (the API path, as `smlib_shell` calls it).
|
||||
//! `shell_open_at_the_top_is_valid_on_the_default_split_path` checks the corrected
|
||||
//! top-open result, including validity rather than only matching historical counts.
|
||||
//! `shell_open_at_y20_fails_as_upstream` retains the side-open refusal expectation;
|
||||
//! that separate regression is still unresolved.
|
||||
//! `issue_1795_extended_edges_mated` (ignored) shells the plate under WR-344's opt-in
|
||||
//! `offset_extended_edges_mated`, the setting that made the top-open shell fail before its
|
||||
//! refinement; `issue_1795_y20_read_back` (ignored) shells the y = 20 case on every
|
||||
@@ -90,27 +92,32 @@ fn options(create_offset_solid: bool) -> ShellOptions {
|
||||
}
|
||||
}
|
||||
|
||||
/// The default fixes shell the plate through the API as pinned SMLib 1.0.1 does (the
|
||||
/// difftest case agrees to 0): a closed manifold solid of 16 faces, 37 edges and 24
|
||||
/// vertices. Volume 9655.150697869625 since #2890 (precise properties integrate from the
|
||||
/// trims' base; it was 9655.150582612916): against the closed form 9655.150444016748
|
||||
/// (the plate less the blind hole less the cavity inset 2, open at z = 27.5, with the hole's
|
||||
/// offset r 7 to x = 9.5 in it) +2.5e-4 (2.6e-8 relative; it was +1.4e-4), within the
|
||||
/// volume's accuracy (1e-6).
|
||||
/// Default vertex splitting builds a valid closed solid of 16 faces, 36 edges and
|
||||
/// 24 vertices. The old 37-edge result split one straight edge at a vertex one mm
|
||||
/// off its curve, hidden by a 4.166 mm vertex tolerance; validity must reject that.
|
||||
/// Keep the existing volume reference 9655.150697869625 and relative tolerance
|
||||
/// 1e-9 unchanged. The independent closed form is 9655.150444016748 (the plate less
|
||||
/// the blind hole less the cavity inset 2, open at z = 27.5, excluding the hole's
|
||||
/// offset r 7 to x = 9.5); the measured result's relative error is 2.6e-8, within
|
||||
/// the volume integrator's accuracy (1e-6).
|
||||
#[test]
|
||||
fn shell_open_at_the_top_shells_as_upstream() {
|
||||
fn shell_open_at_the_top_is_valid_on_the_default_split_path() {
|
||||
let mut k = Kernel::default();
|
||||
let mut b = drilled(&mut k);
|
||||
let open = planar_faces_at(&k, &b, 2, 27.5);
|
||||
assert_eq!(open.len(), 1, "the top face");
|
||||
let r = api_shell(&mut k, &mut b, options(false), &open).unwrap();
|
||||
let mut r = api_shell(&mut k, &mut b, options(false), &open).unwrap();
|
||||
for level in [CheckLevel::Level0, CheckLevel::Level1, CheckLevel::Level2] {
|
||||
let checked = check_brep(&mut k, &mut r, level).unwrap();
|
||||
assert!(checked.is_valid(), "{level:?}: {:?}", checked.lines());
|
||||
}
|
||||
assert_eq!(
|
||||
(
|
||||
r.face_list().len(),
|
||||
r.edge_list().len(),
|
||||
r.vertex_list().len()
|
||||
),
|
||||
(16, 37, 24)
|
||||
(16, 36, 24)
|
||||
);
|
||||
let label = "1795 shell 2 open at z 27.5";
|
||||
assert!(report(&k, label, Ok(r), Some(9655.150697869625), 1e-9));
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//! edges, merged results and the offset solid made (as SolidCraft's `smlib_shell` calls
|
||||
//! `SmApiShellBrepFull`).
|
||||
//!
|
||||
//! Upstream (default fixes) refuses the shell: the merge fails with `SM_ERR` (1001) in
|
||||
//! Pinned upstream refuses the shell: the merge fails with `SM_ERR` (1001) in
|
||||
//! `make_faceuse` (pinned C++ agrees, `offset.offset_executive_p2` case
|
||||
//! `issue1818_square_to_circle_loft_api_shell_1_top`). With the flag alone, on the fitted
|
||||
//! side extensions, the difference returned ten faces, no manifold solid, volume 0 (the
|
||||
@@ -14,6 +14,8 @@
|
||||
//! its volume the loft's less the cavity's, measured independently ([`assert_sound`]).
|
||||
//! solidcore is the reference here, not the fork's refusal. The circle-to-circle loft
|
||||
//! (a cone frustum r 15) is the control: it shells to the closed form under the flag.
|
||||
//! With loop-gap closure and the vertex splits always enabled, the default fixes
|
||||
//! now build the same sound eleven-face shell; the old flag-off refusal is gone.
|
||||
|
||||
#![allow(
|
||||
clippy::unwrap_used,
|
||||
@@ -28,7 +30,6 @@ mod offset_fixture;
|
||||
use offset_fixture::{offset_full, planar_faces_at, volume, Notifications};
|
||||
use solidcore::curve::curve::Curve;
|
||||
use solidcore::curve::ellipse::Ellipse;
|
||||
use solidcore::error::SM_ERR;
|
||||
use solidcore::geom::axis2_placement::Axis2Placement;
|
||||
use solidcore::geom::extent1d::Extent1d;
|
||||
use solidcore::kernel::UpstreamFixes;
|
||||
@@ -125,36 +126,12 @@ fn mated() -> UpstreamFixes {
|
||||
f
|
||||
}
|
||||
|
||||
fn assert_sm_err(result: solidcore::Result<Brep>, operation: &str, label: &str) {
|
||||
match result {
|
||||
Err(e) if e.sm_status().is_some() => {
|
||||
assert_eq!(e.sm_status(), Some(SM_ERR), "{label}");
|
||||
assert_eq!(e.operation(), operation, "{label}");
|
||||
}
|
||||
Err(other) => panic!("{label}: SM_ERR expected, got {other}"),
|
||||
Ok(mut wall) => {
|
||||
wall.refresh_cached_topology();
|
||||
panic!(
|
||||
"{label}: SM_ERR expected, got Ok: {} faces, manifold {:?}",
|
||||
wall.face_list().len(),
|
||||
brep_is_manifold_solid(&wall)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Upstream's refusal (the default path, at parity with the pinned C++).
|
||||
/// Default loop-gap closure now reaches the shell difference rather than the old early refusal.
|
||||
#[test]
|
||||
fn default_fixes_refuse_the_square_to_circle_shell() {
|
||||
let (_, result) = shell(&UpstreamFixes::default(), true);
|
||||
// The early merge refusal happens before shell_difference takes ownership.
|
||||
assert!(result.retained_input.is_some());
|
||||
assert!(!result.input_ownership_transferred());
|
||||
assert_sm_err(
|
||||
result.result,
|
||||
"boolean::merge::p2::make_faceuse",
|
||||
"default fixes",
|
||||
);
|
||||
fn default_fixes_shell_the_square_to_circle_loft() {
|
||||
let (mut k, result) = shell(&UpstreamFixes::default(), true);
|
||||
assert!(result.input_ownership_transferred());
|
||||
assert_sound(&mut k, result.result.unwrap(), "default fixes");
|
||||
}
|
||||
|
||||
/// The shell of the square-to-circle loft is sound: eleven faces, no failure at any check
|
||||
@@ -222,26 +199,8 @@ fn mated_offsets_alone_shell_the_square_to_circle_loft() {
|
||||
);
|
||||
}
|
||||
|
||||
/// With WR-578's loop-gap closing too, the offset is still refused, now by WR-578's guard
|
||||
/// (an offset of a solid that is no closed solid) and no longer by the gap closing itself:
|
||||
/// at each top corner the side faces close their rim arcs' gaps to the lateral edge's
|
||||
/// vertex, while the open top's face closes the same corner by a chord between the two
|
||||
/// arcs' ends, so the closing edges are not shared. That inconsistency is the gap
|
||||
/// closing's own (an opt-in path); the flag alone builds the sound shell above.
|
||||
#[test]
|
||||
fn mated_offsets_with_loop_gaps_still_refuse() {
|
||||
let mut fixes = mated();
|
||||
fixes.offset_closes_loop_gaps = true;
|
||||
let (_, result) = shell(&fixes, true);
|
||||
assert_sm_err(
|
||||
result.result,
|
||||
"offset::offset_executive::p2::shell_brep",
|
||||
"offset_extended_edges_mated + offset_closes_loop_gaps",
|
||||
);
|
||||
}
|
||||
|
||||
/// SolidCraft's fixes (`issue_1476_swept_bar_shell.rs`, which include the flag and WR-454's
|
||||
/// `shell_empty_cavity_fails`, not WR-578's loop gaps): the same sound body.
|
||||
/// `shell_empty_cavity_fails`): the same sound body, with loop-gap closure unconditional.
|
||||
#[test]
|
||||
fn solidcraft_fixes_shell_the_square_to_circle_loft() {
|
||||
let (mut k, result) = shell(&solidcraft_fixes(), true);
|
||||
|
||||
@@ -99,7 +99,6 @@ fn sc_fixes(k: &mut Kernel) {
|
||||
k.fixes.offset_walls_touch_refused = true;
|
||||
k.fixes.local_operation_refuses_loose_tangent_edges = true;
|
||||
k.fixes.local_operation_step_walls = true;
|
||||
k.fixes.offset_closes_loop_gaps = true;
|
||||
k.fixes.local_operation_closes_walls_to_nothing = true;
|
||||
k.fixes.offset_collapsed_rounds_mated = true;
|
||||
k.fixes.offset_missing_surface_fails = true;
|
||||
|
||||
@@ -319,7 +319,6 @@ fn solidcraft_flags(k: &mut Kernel) {
|
||||
k.fixes.offset_analytic_mate_edges = true;
|
||||
k.fixes.offset_clip_right_side_out = true;
|
||||
k.fixes.offset_closed_curve_trim_arc = true;
|
||||
k.fixes.offset_closes_loop_gaps = true;
|
||||
k.fixes.offset_collapsed_rounds_mated = true;
|
||||
k.fixes.offset_excess_faces_after_merge = true;
|
||||
k.fixes.offset_extended_edges_mated = true;
|
||||
|
||||
@@ -220,20 +220,17 @@ const SOLIDCRAFT_C494A18: &[&str] = &[
|
||||
"offset_analytic_mate_edges",
|
||||
"offset_clip_right_side_out",
|
||||
"offset_closed_curve_trim_arc",
|
||||
"offset_closes_loop_gaps",
|
||||
"offset_collapsed_rounds_mated",
|
||||
"offset_edges_keep_source_tolerance",
|
||||
"offset_excess_faces_after_merge",
|
||||
"offset_extended_edges_mated",
|
||||
"offset_extension_as_itself",
|
||||
"offset_extension_from_base",
|
||||
"offset_four_face_vertex_split",
|
||||
"offset_keep_cavity_regions",
|
||||
"offset_missing_surface_fails",
|
||||
"offset_open_edge_second_trim_point",
|
||||
"offset_point_vertex_caps",
|
||||
"offset_resolve_crossing_loops",
|
||||
"offset_splits_corner_vertices",
|
||||
"offset_stitch_at_body_tolerance",
|
||||
"offset_swap_uv_extended_distance",
|
||||
"offset_tangent_gap_statuses",
|
||||
|
||||
@@ -242,19 +242,18 @@ fn check_quarter_torus_seam_case(mut k: Kernel) -> solidcore::Result<()> {
|
||||
|
||||
#[test]
|
||||
fn curved_shell_tangent_quarter_torus_seam_tracks_boundary() -> solidcore::Result<()> {
|
||||
// `offset_closes_loop_gaps` (WR-578) runs `CloseLoopGaps` — including
|
||||
// `reconcile_shell_tangent_rails` — before `do_extended_offset` merges the shell.
|
||||
// `CloseLoopGaps` — including `reconcile_shell_tangent_rails` — runs on the default
|
||||
// path before `do_extended_offset` merges the shell (WR-578, the split flags are the
|
||||
// only path since #2981).
|
||||
// 1. Default surface representations (`SmBSplineSurface` revolved wall and
|
||||
// `SmOffsetSurface` over the B-spline top-rim round).
|
||||
let mut k_bspline = Kernel::default();
|
||||
k_bspline.fixes.offset_closes_loop_gaps = true;
|
||||
let k_bspline = Kernel::default();
|
||||
check_quarter_torus_seam_case(k_bspline)?;
|
||||
|
||||
// 2. Analytic wall and round (`revolve_analytic_profile_faces` produces `SmCone`
|
||||
// for the cylindrical wall, and `fillet_exact_round_classes` produces `SmTorus`
|
||||
// for the circular top-rim round).
|
||||
let mut k_analytic = Kernel::default();
|
||||
k_analytic.fixes.offset_closes_loop_gaps = true;
|
||||
k_analytic.fixes.revolve_analytic_profile_faces = true;
|
||||
k_analytic.fixes.fillet_exact_round_classes = true;
|
||||
check_quarter_torus_seam_case(k_analytic)
|
||||
@@ -262,11 +261,10 @@ fn curved_shell_tangent_quarter_torus_seam_tracks_boundary() -> solidcore::Resul
|
||||
|
||||
#[test]
|
||||
fn shelled_box_open_inner_wall_shared_vertex_consistency() -> solidcore::Result<()> {
|
||||
// Only `offset_closes_loop_gaps` (WR-578) is enabled so `do_extended_offset` runs
|
||||
// `CloseLoopGaps` / `reconcile_shell_tangent_rails` across the open inner cavity
|
||||
// wall's tangent corners before merging.
|
||||
// `do_extended_offset` runs `CloseLoopGaps` / `reconcile_shell_tangent_rails`
|
||||
// across the open inner cavity wall's tangent corners before merging (WR-578, the
|
||||
// default path since #2981).
|
||||
let mut k = Kernel::default();
|
||||
k.fixes.offset_closes_loop_gaps = true;
|
||||
|
||||
// Issue #2954 geometry: `70 x 50 x 30` box (`[-35, 35] x [-25, 25] x [0, 30]`) with
|
||||
// vertical `R = 6` corner rounds, shelled `2 mm` inward open on top (`z = 30`), then
|
||||
|
||||
@@ -125,8 +125,6 @@
|
||||
"offset_keep_cavity_regions",
|
||||
"local_operation_refuses_loose_tangent_edges",
|
||||
"local_operation_step_walls",
|
||||
"offset_closes_loop_gaps",
|
||||
"offset_splits_corner_vertices",
|
||||
"local_operation_closes_walls_to_nothing",
|
||||
"offset_collapsed_rounds_mated",
|
||||
"offset_missing_surface_fails",
|
||||
@@ -232,7 +230,6 @@
|
||||
"fillet_corner_per_vertex_fan",
|
||||
"offset_closed_curve_trim_arc",
|
||||
"offset_open_edge_second_trim_point",
|
||||
"offset_four_face_vertex_split",
|
||||
"cone_create_canonical_preserves_inside_out_and_checks_height",
|
||||
"offset_analytic_mate_edges",
|
||||
"shell_edges_on_analytics_exact",
|
||||
|
||||
@@ -264,7 +264,6 @@ fn round() -> (f64, Result<(bool, f64), i32>) {
|
||||
k.fixes.offset_analytic_mate_edges = true;
|
||||
k.fixes.offset_clip_right_side_out = true;
|
||||
k.fixes.offset_closed_curve_trim_arc = true;
|
||||
k.fixes.offset_closes_loop_gaps = true;
|
||||
k.fixes.offset_collapsed_rounds_mated = true;
|
||||
k.fixes.offset_excess_faces_after_merge = true;
|
||||
k.fixes.offset_extended_edges_mated = true;
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
//! clips the face to the offset domain with a 2D boolean of planar faces made from the
|
||||
//! face's UV loops. Taken from the inset's opposite face use, upstream's loops run
|
||||
//! clockwise, the planar face is inside out, the intersection is empty and the bottom
|
||||
//! gets no offset: the shell comes back open, of no volume. With
|
||||
//! gets no offset: the offset is open, so the default shell refuses it and retains
|
||||
//! the input. With
|
||||
//! `offset_clip_right_side_out` the bottom is offset and the shell is closed, its
|
||||
//! volume the vase's less the cavity bounded by the exact offsets (a cylinder r 13 and
|
||||
//! the bottom cone moved 2 along its normal):
|
||||
@@ -29,13 +30,18 @@
|
||||
#[path = "support/offset_fixture.rs"]
|
||||
mod offset_fixture;
|
||||
|
||||
use offset_fixture::{planar_faces_at, shell, volume};
|
||||
use offset_fixture::{planar_faces_at, shell, volume, Notifications};
|
||||
use solidcore::curve::curve::Curve;
|
||||
use solidcore::error::offset::{OffsetFailure, Unsoundness};
|
||||
use solidcore::error::Reason;
|
||||
use solidcore::math::{Point3, Vec3, PI};
|
||||
use solidcore::offset::offset_executive::p1::OffsetExecutive;
|
||||
use solidcore::offset::offset_executive::p2::{OffsetOptions, ShellOptions};
|
||||
use solidcore::primitive::api_curves::create_line_segment;
|
||||
use solidcore::query::api_queries::brep_is_manifold_solid;
|
||||
use solidcore::sweep::api_brep::create_rotational_sweep;
|
||||
use solidcore::{Brep, Kernel};
|
||||
use solidcore::topology::brep::validation::{check_brep, CheckLevel};
|
||||
use solidcore::{Brep, Kernel, SolidError};
|
||||
|
||||
/// Wall radius, height of the conical bottom, height of the vase.
|
||||
const R: f64 = 15.0;
|
||||
@@ -99,12 +105,96 @@ fn wr_342_vase_shells_closed_with_the_fix() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Upstream: the bottom gets no offset, and the shell is open, of no volume.
|
||||
/// Without WR-342 the bottom gets no offset. The default shell refuses the open
|
||||
/// result and retains the vase, for either rotational sweep direction.
|
||||
#[test]
|
||||
fn wr_342_default_leaves_the_bottom_without_an_offset() {
|
||||
fn wr_342_default_refuses_the_open_shell_and_retains_the_vase() {
|
||||
for up in [true, false] {
|
||||
let (k, mut b) = shelled(up, false);
|
||||
assert!(!brep_is_manifold_solid(&b).unwrap(), "up {up}");
|
||||
assert!(volume(&k, &mut b).unwrap().abs() < 1e-9, "up {up}");
|
||||
let mut k = Kernel::default();
|
||||
k.fixes.revolution_offset_axis_both_sides = true;
|
||||
let mut part = vase(&mut k, up);
|
||||
let open = planar_faces_at(&k, &part, 1, H);
|
||||
assert_eq!(open.len(), 1);
|
||||
let mut notifications = Notifications::new(&mut part);
|
||||
let key = part.key();
|
||||
let tolerance = part.tolerance();
|
||||
let ids = (
|
||||
part.face_list().to_vec(),
|
||||
part.edge_list().to_vec(),
|
||||
part.vertex_list().to_vec(),
|
||||
);
|
||||
let points: Vec<_> = part
|
||||
.vertices()
|
||||
.iter()
|
||||
.map(|(id, v)| (id, v.point, v.tolerance))
|
||||
.collect();
|
||||
let outcome = OffsetExecutive::shell_brep(
|
||||
&mut k,
|
||||
part,
|
||||
ShellOptions {
|
||||
offset: OffsetOptions {
|
||||
distance: -2.0,
|
||||
extend_convex_edges: true,
|
||||
merge_results: true,
|
||||
},
|
||||
create_offset_solid: false,
|
||||
merge_sheet_side_walls: false,
|
||||
},
|
||||
&open,
|
||||
&mut notifications.context(),
|
||||
);
|
||||
let error = outcome.result.as_ref().unwrap_err();
|
||||
let SolidError::Failed(failure) = error else {
|
||||
panic!("up {up}: not a typed refusal: {error}");
|
||||
};
|
||||
assert_eq!(
|
||||
failure.operation, "offset::offset_executive::p2::shell_brep",
|
||||
"up {up}: {error:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
failure.reason,
|
||||
Reason::Offset(OffsetFailure::Unsound {
|
||||
what: Unsoundness::NotManifoldSolid
|
||||
}),
|
||||
"up {up}: {error:?}"
|
||||
);
|
||||
assert!(!outcome.input_ownership_transferred());
|
||||
let mut retained = outcome.retained_input.unwrap();
|
||||
assert_eq!(retained.key(), key);
|
||||
assert_eq!(retained.tolerance(), tolerance);
|
||||
assert_eq!(
|
||||
(
|
||||
retained.face_list().to_vec(),
|
||||
retained.edge_list().to_vec(),
|
||||
retained.vertex_list().to_vec()
|
||||
),
|
||||
ids
|
||||
);
|
||||
assert_eq!(
|
||||
retained
|
||||
.vertices()
|
||||
.iter()
|
||||
.map(|(id, v)| (id, v.point, v.tolerance))
|
||||
.collect::<Vec<_>>(),
|
||||
points
|
||||
);
|
||||
// Check the retained caches as returned, without refreshing them first.
|
||||
assert!(
|
||||
solidcore::topology::integrity::check(&retained).is_ok(),
|
||||
"up {up}"
|
||||
);
|
||||
assert!(brep_is_manifold_solid(&retained).unwrap(), "up {up}");
|
||||
for level in [CheckLevel::Level0, CheckLevel::Level1, CheckLevel::Level2] {
|
||||
let checked = check_brep(&mut k, &mut retained, level).unwrap();
|
||||
assert!(
|
||||
checked.is_valid(),
|
||||
"up {up}, {level:?}: {:?}",
|
||||
checked.lines()
|
||||
);
|
||||
}
|
||||
let v = volume(&k, &mut retained).unwrap();
|
||||
let exact = PI * R * R * (HB / 3.0 + H - HB);
|
||||
// The original cone and cylinder, not an open or partially hollowed body.
|
||||
assert!((v - exact).abs() <= 1e-9 * exact, "up={up}: {v}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
//! `shelled_box_with_a_drilled_hole`): a box with a blind drilled hole (r 3, a 118
|
||||
//! degree tip) shelled 2 mm inward, open at a side and, separately, at the top. The
|
||||
//! hole's tip is a cone's apex whose seam is its only edge: the extended offset builds
|
||||
//! no offset there, so the cone's offset ends open on a circle. `ShellBrep` then hands
|
||||
//! back the offset as it is, not a closed solid (upstream returns `SM_SUCCESS` with that
|
||||
//! non-manifold result, `SmOffsetExecutive.cpp:255-262`); the fork's shell fails (1001)
|
||||
//! because its WR-454 check (`shell_empty_cavity_fails`, tangency-labs/smlib@fbe43bb)
|
||||
//! refuses that open offset. With `offset_point_vertex_caps` the apex's offset is a
|
||||
//! sphere zone of radius 2 and the shell is a closed solid.
|
||||
//! no offset there, so the cone's offset ends open on a circle. Pinned SMLib returns
|
||||
//! that non-manifold offset as a success (`SmOffsetExecutive.cpp:255-262`). Here the
|
||||
//! default shell refuses it with or without WR-454's `shell_empty_cavity_fails` check,
|
||||
//! retaining the input. With `offset_point_vertex_caps` the apex's offset is a sphere
|
||||
//! zone of radius 2 and the shell is a closed solid.
|
||||
//!
|
||||
//! The result must pass SMLib's level-2 check (`check_brep`) clean.
|
||||
//! The successful result must pass the level-2 check (`check_brep`) clean.
|
||||
#![allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
@@ -20,17 +19,21 @@
|
||||
#[path = "support/offset_fixture.rs"]
|
||||
mod offset_fixture;
|
||||
|
||||
use offset_fixture::{planar_faces_at, shell};
|
||||
use offset_fixture::{planar_faces_at, shell, volume, Notifications};
|
||||
use solidcore::boolean::api_brep::boolean_difference;
|
||||
use solidcore::curve::curve::Curve;
|
||||
use solidcore::math::{Point3, Vec3};
|
||||
use solidcore::error::offset::{OffsetFailure, Unsoundness};
|
||||
use solidcore::error::Reason;
|
||||
use solidcore::math::{Point3, Vec3, PI};
|
||||
use solidcore::offset::offset_executive::p1::OffsetExecutive;
|
||||
use solidcore::offset::offset_executive::p2::{OffsetOptions, ShellOptions};
|
||||
use solidcore::primitive::api_curves::create_line_segment;
|
||||
use solidcore::primitive::api_primitives::create_box;
|
||||
use solidcore::query::api_queries::brep_is_manifold_solid;
|
||||
use solidcore::surface::surface::SurfaceKind;
|
||||
use solidcore::sweep::api_brep::create_rotational_sweep;
|
||||
use solidcore::topology::brep::validation::{check_brep, CheckLevel};
|
||||
use solidcore::{Brep, Kernel};
|
||||
use solidcore::{Brep, Kernel, SolidError};
|
||||
|
||||
/// A 40 x 40 x 20 box with a hole r 3 drilled 10 deep from the top at (20, 20), its
|
||||
/// tip a 118 degree cone.
|
||||
@@ -116,18 +119,109 @@ fn wr_531_drilled_hole_shells_with_a_sphere_cap() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Without the opt-in the cone's offset stays open at the tip: the pinned `ShellBrep`
|
||||
/// hands it back as a success, no closed solid, and WR-454's check refuses it (the
|
||||
/// fork's failing shell).
|
||||
/// Without a sphere cap the cone's offset stays open at the tip. The shell
|
||||
/// refuses it and retains the input, regardless of the legacy WR-454 flag.
|
||||
#[test]
|
||||
fn wr_531_without_the_opt_in_the_shell_is_open() {
|
||||
fn wr_531_without_a_tip_cap_the_shell_is_refused_and_keeps_the_input() {
|
||||
for open_top in [false, true] {
|
||||
let (_, b) = shelled(false, false, open_top).unwrap();
|
||||
assert!(!brep_is_manifold_solid(&b).unwrap(), "open_top {open_top}");
|
||||
assert_eq!(spheres(&b), 0, "open_top {open_top}");
|
||||
assert!(
|
||||
shelled(false, true, open_top).is_err(),
|
||||
"open_top {open_top}"
|
||||
);
|
||||
for refuse_open in [false, true] {
|
||||
let case = format!("open_top={open_top} refuse_open={refuse_open}");
|
||||
let mut k = Kernel::default();
|
||||
k.fixes.shell_empty_cavity_fails = refuse_open;
|
||||
let mut part = drilled_box(&mut k);
|
||||
let open = if open_top {
|
||||
planar_faces_at(&k, &part, 2, 20.0)
|
||||
} else {
|
||||
planar_faces_at(&k, &part, 0, 0.0)
|
||||
};
|
||||
assert_eq!(open.len(), 1);
|
||||
let mut notifications = Notifications::new(&mut part);
|
||||
let key = part.key();
|
||||
let tolerance = part.tolerance();
|
||||
let ids = (
|
||||
part.face_list().to_vec(),
|
||||
part.edge_list().to_vec(),
|
||||
part.vertex_list().to_vec(),
|
||||
);
|
||||
let points: Vec<_> = part
|
||||
.vertices()
|
||||
.iter()
|
||||
.map(|(id, v)| (id, v.point, v.tolerance))
|
||||
.collect();
|
||||
let outcome = OffsetExecutive::shell_brep(
|
||||
&mut k,
|
||||
part,
|
||||
ShellOptions {
|
||||
offset: OffsetOptions {
|
||||
distance: -2.0,
|
||||
extend_convex_edges: true,
|
||||
merge_results: true,
|
||||
},
|
||||
create_offset_solid: false,
|
||||
merge_sheet_side_walls: false,
|
||||
},
|
||||
&open,
|
||||
&mut notifications.context(),
|
||||
);
|
||||
let error = outcome.result.as_ref().unwrap_err();
|
||||
let SolidError::Failed(failure) = error else {
|
||||
panic!("{case}: not a typed refusal: {error}");
|
||||
};
|
||||
assert_eq!(
|
||||
failure.operation, "offset::offset_executive::p2::shell_brep",
|
||||
"{case}: {error:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
failure.reason,
|
||||
Reason::Offset(OffsetFailure::Unsound {
|
||||
what: Unsoundness::NotManifoldSolid
|
||||
}),
|
||||
"{case}: {error:?}"
|
||||
);
|
||||
assert!(!outcome.input_ownership_transferred());
|
||||
let mut retained = outcome.retained_input.unwrap();
|
||||
assert_eq!(retained.key(), key);
|
||||
assert_eq!(retained.tolerance(), tolerance);
|
||||
assert_eq!(
|
||||
(
|
||||
retained.face_list().to_vec(),
|
||||
retained.edge_list().to_vec(),
|
||||
retained.vertex_list().to_vec()
|
||||
),
|
||||
ids
|
||||
);
|
||||
assert_eq!(
|
||||
retained
|
||||
.vertices()
|
||||
.iter()
|
||||
.map(|(id, v)| (id, v.point, v.tolerance))
|
||||
.collect::<Vec<_>>(),
|
||||
points
|
||||
);
|
||||
// Inspect the returned caches without refreshing away an inconsistency.
|
||||
assert!(
|
||||
solidcore::topology::integrity::check(&retained).is_ok(),
|
||||
"{case}"
|
||||
);
|
||||
assert!(brep_is_manifold_solid(&retained).unwrap(), "{case}");
|
||||
assert_eq!(
|
||||
spheres(&retained),
|
||||
0,
|
||||
"{case}: the original has no sphere cap"
|
||||
);
|
||||
for level in [CheckLevel::Level0, CheckLevel::Level1, CheckLevel::Level2] {
|
||||
let checked = check_brep(&mut k, &mut retained, level).unwrap();
|
||||
assert!(
|
||||
checked.is_valid(),
|
||||
"{case}, {level:?}: {:?}",
|
||||
checked.lines()
|
||||
);
|
||||
}
|
||||
let v = volume(&k, &mut retained).unwrap();
|
||||
let tip = 3.0 / 59.0_f64.to_radians().tan();
|
||||
let exact = 40.0 * 40.0 * 20.0 - PI * 3.0 * 3.0 * (10.0 + tip / 3.0);
|
||||
// Original block less the cylindrical bore and its conical drill tip.
|
||||
assert!((v - exact).abs() <= 1e-9 * exact, "{case}: {v}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Regression test for WR-578 (the fork's tangency-labs/smlib 7ff0a94 and 34318f4): a
|
||||
//! solid offset leaves a face loop's vertex behind where two consecutive edges' offsets
|
||||
//! run parallel and never meet, and the merge refuses the shell (1001). With
|
||||
//! `offset_closes_loop_gaps` the gap is closed by the straight edge between the two ends.
|
||||
//! run parallel and never meet, and the merge refuses the shell (1001). The gap is
|
||||
//! closed by the straight edge between the two ends (the default path since #2981).
|
||||
//!
|
||||
//! SolidCraft rig case shell/1494, rebuilt: a 47.5 x 45 x 30 box joined with an r7.5
|
||||
//! cylinder along x from its side x = 47.5 to 77.5, the axis in the front face's plane
|
||||
@@ -39,12 +39,6 @@ use solidcore::{Brep, Kernel};
|
||||
|
||||
const SHELL_1494: f64 = 8864.598272772;
|
||||
|
||||
fn kernel(fixed: bool) -> Kernel {
|
||||
let mut k = Kernel::default();
|
||||
k.fixes.offset_closes_loop_gaps = fixed;
|
||||
k
|
||||
}
|
||||
|
||||
/// The box joined with the cylinder, its seam `seam_deg` from +y towards +z.
|
||||
fn box_and_cylinder(k: &mut Kernel, seam_deg: f64) -> Brep {
|
||||
let body = create_box(k, Point3::new(0.0, 0.0, 0.0), 47.5, 45.0, 30.0).unwrap();
|
||||
@@ -62,8 +56,8 @@ fn box_and_cylinder(k: &mut Kernel, seam_deg: f64) -> Brep {
|
||||
b
|
||||
}
|
||||
|
||||
fn shell_1494(fixed: bool, seam_deg: f64) -> (Kernel, solidcore::Result<Brep>) {
|
||||
shell_1494_with(kernel(fixed), seam_deg)
|
||||
fn shell_1494(seam_deg: f64) -> (Kernel, solidcore::Result<Brep>) {
|
||||
shell_1494_with(Kernel::default(), seam_deg)
|
||||
}
|
||||
|
||||
fn shell_1494_with(mut k: Kernel, seam_deg: f64) -> (Kernel, solidcore::Result<Brep>) {
|
||||
@@ -81,7 +75,7 @@ fn shell_1494_with(mut k: Kernel, seam_deg: f64) -> (Kernel, solidcore::Result<B
|
||||
#[ignore = "open: volume 2.7e-7 / 7.7e-7 off the fork's (WR-578)"]
|
||||
fn wr_578_box_and_cylinder_shell_closes_the_loop_gaps() {
|
||||
for seam in [30.0, 55.0] {
|
||||
let (k, result) = shell_1494(true, seam);
|
||||
let (k, result) = shell_1494(seam);
|
||||
assert!(report(
|
||||
&k,
|
||||
&format!("wr_578 shell/1494 seam {seam}"),
|
||||
@@ -92,12 +86,12 @@ fn wr_578_box_and_cylinder_shell_closes_the_loop_gaps() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Seam 200 with the opt-in alone: each face's surface extended by its offset's reach (not
|
||||
/// Seam 200 on the default path: each face's surface extended by its offset's reach (not
|
||||
/// five offsets), the shell is sound as at seams 30 and 55 (manifold, integrity clean), its
|
||||
/// volume 1.5e-7 off the fork's closed form as theirs are (WR-578's divergence).
|
||||
#[test]
|
||||
fn wr_578_seam_200_shells_sound() {
|
||||
let (k, result) = shell_1494(true, 200.0);
|
||||
let (k, result) = shell_1494(200.0);
|
||||
assert!(report(
|
||||
&k,
|
||||
"wr_578 shell/1494 seam 200",
|
||||
@@ -113,7 +107,7 @@ fn wr_578_seam_200_shells_sound() {
|
||||
#[ignore = "open: the port returns a non-manifold body for these seams (WR-578)"]
|
||||
fn wr_578_box_and_cylinder_shell_other_seams() {
|
||||
for seam in [125.0, 200.0] {
|
||||
let (k, result) = shell_1494(true, seam);
|
||||
let (k, result) = shell_1494(seam);
|
||||
assert!(report(
|
||||
&k,
|
||||
&format!("wr_578 shell/1494 seam {seam}"),
|
||||
@@ -129,7 +123,7 @@ fn wr_578_box_and_cylinder_shell_other_seams() {
|
||||
/// its base edge runs along, and the shell is sound as at seam 200.
|
||||
#[test]
|
||||
fn wr_578_seam_125_shells_sound() {
|
||||
let (k, result) = shell_1494(true, 125.0);
|
||||
let (k, result) = shell_1494(125.0);
|
||||
assert!(report(
|
||||
&k,
|
||||
"wr_578 shell/1494 seam 125",
|
||||
@@ -139,11 +133,11 @@ fn wr_578_seam_125_shells_sound() {
|
||||
));
|
||||
}
|
||||
|
||||
/// Without the opt-in the shell is not that sound body (upstream refuses it, 1001).
|
||||
/// Loop-gap closure is now the default, so the formerly refused shell is sound.
|
||||
#[test]
|
||||
fn wr_578_box_and_cylinder_shell_without_the_opt_in() {
|
||||
let (k, result) = shell_1494(false, 30.0);
|
||||
assert!(!report(
|
||||
fn wr_578_box_and_cylinder_shell_on_the_default_path() {
|
||||
let (k, result) = shell_1494(30.0);
|
||||
assert!(report(
|
||||
&k,
|
||||
"wr_578 shell/1494 default",
|
||||
result,
|
||||
@@ -162,7 +156,7 @@ fn notched(k: &mut Kernel) -> Brep {
|
||||
|
||||
#[test]
|
||||
fn wr_578_shell_thicker_than_a_step_is_edge_vanishes() {
|
||||
let mut k = kernel(true);
|
||||
let mut k = Kernel::default();
|
||||
let b = notched(&mut k);
|
||||
let strip = planar_faces_at(&k, &b, 1, 35.0);
|
||||
assert_eq!(strip.len(), 1, "the far side under the notch");
|
||||
@@ -174,7 +168,7 @@ fn wr_578_shell_thicker_than_a_step_is_edge_vanishes() {
|
||||
|
||||
#[test]
|
||||
fn wr_578_notched_box_open_at_the_floor_shells() {
|
||||
let mut k = kernel(true);
|
||||
let mut k = Kernel::default();
|
||||
let b = notched(&mut k);
|
||||
let floor = planar_faces_at(&k, &b, 2, 2.5);
|
||||
assert_eq!(floor.len(), 1, "the notch's floor");
|
||||
@@ -192,7 +186,7 @@ fn wr_578_notched_box_open_at_the_floor_shells() {
|
||||
/// a half circle r 10 (x < 0), a blind hole r 5 along x through the round side at
|
||||
/// (y, z) = (12.5, 12.5) from x = -10 to 7.5, shelled 2 inward, open at x = 42.5, on a
|
||||
/// clean operand. A prior mass-properties query on the operand changes what the shell
|
||||
/// reads (UV trims, a snapped trim, gap caches: WR-583), and the opt-in then refuses it;
|
||||
/// reads (UV trims, a snapped trim, gap caches: WR-583), and closure then refuses it;
|
||||
/// nothing here measures the operand.
|
||||
fn plate_1528(k: &mut Kernel) -> Brep {
|
||||
// the profile swept 27.5 along z (one curved side face), as the #1528 fixture builds it
|
||||
@@ -220,8 +214,8 @@ fn plate_1528(k: &mut Kernel) -> Brep {
|
||||
b
|
||||
}
|
||||
|
||||
fn shell_1528(fixed: bool) -> (Kernel, solidcore::Result<Brep>) {
|
||||
let mut k = kernel(fixed);
|
||||
fn shell_1528() -> (Kernel, solidcore::Result<Brep>) {
|
||||
let mut k = Kernel::default();
|
||||
let b = plate_1528(&mut k);
|
||||
let open = planar_faces_at(&k, &b, 0, 42.5);
|
||||
assert_eq!(open.len(), 1, "the far end");
|
||||
@@ -231,24 +225,13 @@ fn shell_1528(fixed: bool) -> (Kernel, solidcore::Result<Brep>) {
|
||||
|
||||
#[test]
|
||||
fn wr_578_issue_1528_plate_shells_on_a_clean_operand() {
|
||||
let (k0, default) = shell_1528(false);
|
||||
let default = default.unwrap();
|
||||
assert_eq!(default.face_list().len(), 16, "the default shell's faces");
|
||||
assert_eq!(default.edge_list().len(), 37, "the default shell's edges");
|
||||
assert!(report(
|
||||
&k0,
|
||||
"wr_578 #1528 plate default",
|
||||
Ok(default),
|
||||
None,
|
||||
0.0
|
||||
));
|
||||
let (k, fixed) = shell_1528(true);
|
||||
let (k, fixed) = shell_1528();
|
||||
let fixed = fixed.unwrap();
|
||||
assert_eq!(fixed.face_list().len(), 16, "the opt-in shell's faces");
|
||||
assert_eq!(fixed.edge_list().len(), 36, "the opt-in shell's edges");
|
||||
assert_eq!(fixed.face_list().len(), 16, "the default shell's faces");
|
||||
assert_eq!(fixed.edge_list().len(), 36, "the default shell's edges");
|
||||
assert!(report(
|
||||
&k,
|
||||
"wr_578 #1528 plate opt-in",
|
||||
"wr_578 #1528 plate default",
|
||||
Ok(fixed),
|
||||
// since #2890 (precise properties integrate from the trims' base): +4.8e-4 from the
|
||||
// closed form 10400.212373846494 (issue_1950's), within the volume's accuracy
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
//! four plane faces whose offset planes no longer meet in one point. The offset of each
|
||||
//! edge there ends at the same pair of the planes' meeting points whichever way the
|
||||
//! corner splits, so an inset, or a shell open at one of the four faces, gets loop gaps
|
||||
//! and no volume (or the merge refuses it). With `offset_closes_loop_gaps` and
|
||||
//! `offset_splits_corner_vertices` the corner becomes the edge the offset needs.
|
||||
//! and no volume (or the merge refuses it). The corner becomes the edge the offset
|
||||
//! needs (the default path since #2981).
|
||||
//!
|
||||
//! A 30 x 20 x 10 box with edges 0 (the top's edge at x = 30) and 9 (the top's edge at
|
||||
//! y = 0) chamfered 2 (`SmApiFilletEdges`, a linear section), with edge 8 (the upright
|
||||
@@ -22,7 +22,6 @@ mod offset_fixture;
|
||||
|
||||
use offset_fixture::{offset_full, planar_faces_at, report, shell, Notifications};
|
||||
use solidcore::blend::fillet_executive::api::fillet_edges;
|
||||
use solidcore::error::SM_ERR;
|
||||
use solidcore::offset::offset_executive::p2::{api_offset_brep_full, OffsetOptions};
|
||||
use solidcore::primitive::api_primitives::{create_box, create_sphere};
|
||||
use solidcore::topology::brep::p1 as bp1;
|
||||
@@ -32,13 +31,6 @@ use solidcore::{Brep, FaceId, Kernel, Point3};
|
||||
/// Relative tolerance of the volumes (the port lands within 1e-12 of each).
|
||||
const REL: f64 = 1e-9;
|
||||
|
||||
fn kernel(fixed: bool) -> Kernel {
|
||||
let mut k = Kernel::default();
|
||||
k.fixes.offset_closes_loop_gaps = fixed;
|
||||
k.fixes.offset_splits_corner_vertices = fixed;
|
||||
k
|
||||
}
|
||||
|
||||
/// The box with `edges` (indices into its edge list; `None`: every edge) chamfered 2.
|
||||
fn chamfered(k: &mut Kernel, edges: Option<&[usize]>) -> Brep {
|
||||
let mut b = create_box(k, Point3::new(0.0, 0.0, 0.0), 30.0, 20.0, 10.0).unwrap();
|
||||
@@ -110,14 +102,10 @@ fn no_loop_gaps(k: &mut Kernel, b: &Brep) -> bool {
|
||||
fn wr_657_two_chamfers_shell_inward_open_at_a_wall() {
|
||||
// the reported case: shelled 1 inward open at the wall at x = 30
|
||||
let want = 1806.1797017478002;
|
||||
let shell_x30 = |fixed: bool| {
|
||||
let mut k = kernel(fixed);
|
||||
let b = chamfered(&mut k, Some(&[0, 9]));
|
||||
let open = x30(&k, &b);
|
||||
let result = shell(&mut k, b, -1.0, &[open]);
|
||||
(k, result)
|
||||
};
|
||||
let (k, result) = shell_x30(true);
|
||||
let mut k = Kernel::default();
|
||||
let b = chamfered(&mut k, Some(&[0, 9]));
|
||||
let open = x30(&k, &b);
|
||||
let result = shell(&mut k, b, -1.0, &[open]);
|
||||
assert!(report(
|
||||
&k,
|
||||
"wr_657 two chamfers shell -1 x30",
|
||||
@@ -125,15 +113,6 @@ fn wr_657_two_chamfers_shell_inward_open_at_a_wall() {
|
||||
Some(want),
|
||||
REL
|
||||
));
|
||||
// upstream (the pinned C++ too): Ok, no closed solid, volume 0
|
||||
let (k, result) = shell_x30(false);
|
||||
assert!(!report(
|
||||
&k,
|
||||
"wr_657 two chamfers shell -1 x30 default",
|
||||
result,
|
||||
Some(want),
|
||||
REL
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -147,7 +126,7 @@ fn wr_657_two_chamfers_shells_at_each_corner_face() {
|
||||
(chamfer_9, 1.0, 2226.2751561773057),
|
||||
];
|
||||
for (i, (open, d, want)) in cases.into_iter().enumerate() {
|
||||
let mut k = kernel(true);
|
||||
let mut k = Kernel::default();
|
||||
let b = chamfered(&mut k, Some(&[0, 9]));
|
||||
let open = open(&k, &b);
|
||||
let result = shell(&mut k, b, d, &[open]);
|
||||
@@ -166,11 +145,14 @@ fn wr_657_two_chamfers_shell_outward_has_no_loop_gaps() {
|
||||
// upstream's volume is right (2152.620538, the C++ too) but two loops have a gap
|
||||
// of sqrt(2)
|
||||
let want = 2152.6205383941633;
|
||||
let mut k = kernel(true);
|
||||
let mut k = Kernel::default();
|
||||
let b = chamfered(&mut k, Some(&[0, 9]));
|
||||
let open = x30(&k, &b);
|
||||
let result = shell(&mut k, b, 1.0, &[open]).unwrap();
|
||||
assert!(no_loop_gaps(&mut k, &result), "loop gaps with the opt-in");
|
||||
assert!(
|
||||
no_loop_gaps(&mut k, &result),
|
||||
"loop gaps on the default path"
|
||||
);
|
||||
assert!(report(
|
||||
&k,
|
||||
"wr_657 two chamfers shell +1 x30",
|
||||
@@ -178,17 +160,12 @@ fn wr_657_two_chamfers_shell_outward_has_no_loop_gaps() {
|
||||
Some(want),
|
||||
REL
|
||||
));
|
||||
let mut k = kernel(false);
|
||||
let b = chamfered(&mut k, Some(&[0, 9]));
|
||||
let open = x30(&k, &b);
|
||||
let result = shell(&mut k, b, 1.0, &[open]).unwrap();
|
||||
assert!(!no_loop_gaps(&mut k, &result), "upstream leaves loop gaps");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wr_657_three_chamfers_offsets() {
|
||||
for (d, want) in [(1.0, 8238.532227977406), (-1.0, 3979.9694554143525)] {
|
||||
let mut k = kernel(true);
|
||||
let mut k = Kernel::default();
|
||||
let mut b = chamfered(&mut k, Some(&[0, 9, 8]));
|
||||
let result = offset_full(&mut k, &mut b, d);
|
||||
assert!(report(
|
||||
@@ -199,24 +176,12 @@ fn wr_657_three_chamfers_offsets() {
|
||||
REL
|
||||
));
|
||||
}
|
||||
// upstream: Ok with loop gaps and volume 0 at +1, refused (1001) at -1. With each plane's
|
||||
// extension on the plane itself the default reaches the closed form at +1 too.
|
||||
let mut k = kernel(false);
|
||||
let mut b = chamfered(&mut k, Some(&[0, 9, 8]));
|
||||
let result = offset_full(&mut k, &mut b, 1.0);
|
||||
assert!(report(
|
||||
&k,
|
||||
"wr_657 three chamfers offset +1 default",
|
||||
result,
|
||||
Some(8238.532227977406),
|
||||
REL
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wr_657_three_chamfers_shells() {
|
||||
// open at the wall at x = 30 (1 each way) and at the corner's triangle (1 outward)
|
||||
let mut k = kernel(true);
|
||||
let mut k = Kernel::default();
|
||||
let b = chamfered(&mut k, Some(&[0, 9, 8]));
|
||||
let open = x30(&k, &b);
|
||||
let result = shell(&mut k, b, -1.0, &[open]);
|
||||
@@ -227,7 +192,7 @@ fn wr_657_three_chamfers_shells() {
|
||||
Some(1807.385883644974),
|
||||
REL
|
||||
));
|
||||
let mut k = kernel(true);
|
||||
let mut k = Kernel::default();
|
||||
let b = chamfered(&mut k, Some(&[0, 9, 8]));
|
||||
let open = x30(&k, &b);
|
||||
let result = shell(&mut k, b, 1.0, &[open]);
|
||||
@@ -238,7 +203,7 @@ fn wr_657_three_chamfers_shells() {
|
||||
Some(2155.9012941491765),
|
||||
REL
|
||||
));
|
||||
let mut k = kernel(true);
|
||||
let mut k = Kernel::default();
|
||||
let b = chamfered(&mut k, Some(&[0, 9, 8]));
|
||||
let open = face_at(&k, &b, [28.0 + 2.0 / 3.0, 4.0 / 3.0, 8.0 + 2.0 / 3.0]);
|
||||
let result = shell(&mut k, b, 1.0, &[open]);
|
||||
@@ -254,7 +219,7 @@ fn wr_657_three_chamfers_shells() {
|
||||
#[test]
|
||||
fn wr_657_fully_chamfered_box() {
|
||||
for (d, want) in [(1.0, 7654.848301953257), (-1.0, 3831.755643314821)] {
|
||||
let mut k = kernel(true);
|
||||
let mut k = Kernel::default();
|
||||
let mut b = chamfered(&mut k, None);
|
||||
let result = offset_full(&mut k, &mut b, d);
|
||||
assert!(report(
|
||||
@@ -283,7 +248,7 @@ fn wr_657_fully_chamfered_box() {
|
||||
([29.0, 10.0, 9.0], 1.0, 2009.1803204583593),
|
||||
];
|
||||
for (at, d, want) in cases {
|
||||
let mut k = kernel(true);
|
||||
let mut k = Kernel::default();
|
||||
let b = chamfered(&mut k, None);
|
||||
let open = face_at(&k, &b, at);
|
||||
let result = shell(&mut k, b, d, &[open]);
|
||||
@@ -298,25 +263,26 @@ fn wr_657_fully_chamfered_box() {
|
||||
}
|
||||
|
||||
/// An inset of a chamfer's width leaves the cavity meeting the open chamfer along a line
|
||||
/// only (the body is no manifold solid): refused, not returned.
|
||||
/// only (the body is no manifold solid): the default four-face split detects a
|
||||
/// vanishing edge before the final manifold-result guard, and refuses it by name.
|
||||
#[test]
|
||||
fn wr_657_fully_chamfered_box_inset_open_at_a_chamfer_is_refused() {
|
||||
let mut k = kernel(true);
|
||||
let mut k = Kernel::default();
|
||||
let b = chamfered(&mut k, None);
|
||||
let open = face_at(&k, &b, [29.0, 10.0, 9.0]);
|
||||
match shell(&mut k, b, -1.0, &[open]) {
|
||||
Err(e) => assert_eq!(e.sm_status(), Some(SM_ERR)),
|
||||
other => panic!("SM_ERR expected: {other:?}"),
|
||||
Err(e) => assert_eq!(e.sm_status(), Some(1043)),
|
||||
other => panic!("SM_ERR_OFFSET_EDGE_VANISHES expected: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// An offset asked not to merge leaves its overlapping faces (`OffsetBrep`, `bDoSelfInt`), so a
|
||||
/// result that is no closed solid is what was asked for: a 10 mm cube inset 1, convex edges
|
||||
/// not extended, comes back as with the flags off, as it does from the fork at d01d7b0.
|
||||
/// not extended, still comes back with faces on the default split path.
|
||||
#[test]
|
||||
fn wr_657_unmerged_offset_is_not_refused() {
|
||||
let faces = |fixed: bool| {
|
||||
let mut k = kernel(fixed);
|
||||
let faces = || {
|
||||
let mut k = Kernel::default();
|
||||
let mut b = create_box(&mut k, Point3::new(0.0, 0.0, 0.0), 10.0, 10.0, 10.0).unwrap();
|
||||
let mut n = Notifications::new(&mut b);
|
||||
let options = OffsetOptions {
|
||||
@@ -325,33 +291,27 @@ fn wr_657_unmerged_offset_is_not_refused() {
|
||||
merge_results: false,
|
||||
};
|
||||
api_offset_brep_full(&mut k, Some(&mut b), options, &mut n.context())
|
||||
.unwrap_or_else(|e| panic!("fixed {fixed}: {e:?}"))
|
||||
.unwrap_or_else(|e| panic!("{e:?}"))
|
||||
.face_list()
|
||||
.len()
|
||||
};
|
||||
let with_flags = faces(true);
|
||||
assert!(with_flags > 0);
|
||||
assert_eq!(with_flags, faces(false));
|
||||
assert!(faces() > 0);
|
||||
}
|
||||
|
||||
/// #2092: a sphere of radius 10 offset by -10 (convex edges not extended) leaves nothing.
|
||||
/// With both flags on, the empty body comes back as with either off: a result with no
|
||||
/// faces is not refused as a broken solid.
|
||||
/// The splits are the only path since #2981: an empty body still comes back as itself, no
|
||||
/// broken-solid refusal on a result with no faces.
|
||||
#[test]
|
||||
fn issue_2092_sphere_offset_to_its_centre_is_empty() {
|
||||
for flags in [(false, false), (true, false), (false, true), (true, true)] {
|
||||
let mut k = Kernel::default();
|
||||
k.fixes.offset_closes_loop_gaps = flags.0;
|
||||
k.fixes.offset_splits_corner_vertices = flags.1;
|
||||
let mut ball = create_sphere(&mut k, Point3::new(0.0, 0.0, 0.0), 10.0).unwrap();
|
||||
let mut n = Notifications::new(&mut ball);
|
||||
let options = OffsetOptions {
|
||||
distance: -10.0,
|
||||
extend_convex_edges: false,
|
||||
merge_results: true,
|
||||
};
|
||||
let out = api_offset_brep_full(&mut k, Some(&mut ball), options, &mut n.context())
|
||||
.unwrap_or_else(|e| panic!("flags {flags:?}: {e:?}"));
|
||||
assert!(out.face_list().is_empty(), "flags {flags:?}: no faces");
|
||||
}
|
||||
let mut k = Kernel::default();
|
||||
let mut ball = create_sphere(&mut k, Point3::new(0.0, 0.0, 0.0), 10.0).unwrap();
|
||||
let mut n = Notifications::new(&mut ball);
|
||||
let options = OffsetOptions {
|
||||
distance: -10.0,
|
||||
extend_convex_edges: false,
|
||||
merge_results: true,
|
||||
};
|
||||
let out = api_offset_brep_full(&mut k, Some(&mut ball), options, &mut n.context())
|
||||
.unwrap_or_else(|e| panic!("{e:?}"));
|
||||
assert!(out.face_list().is_empty(), "no faces");
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,39 @@
|
||||
# Local goldens failures: one clean-base control, cause not established
|
||||
|
||||
Found: 2026-11-07, while re-verifying the #2981 flag-retirement lane locally.
|
||||
|
||||
## What was observed
|
||||
|
||||
A full goldens sweep (`cargo run --profile goldens -p goldens -- --all --jobs 4 --goldens`)
|
||||
on the PR head a4e81596c, based on 7efbd5090, reported 84 of 283 units failing
|
||||
on aarch64 macOS. This was not a full sweep of the clean base. One inspected
|
||||
failure was in a compact integer-array summary
|
||||
(`[sum, weighted_sum]` per chunk):
|
||||
`topology.brep_constructor`'s `hexagon_prism_separate_stitched`, where only the
|
||||
topology-int weighted sum moves (`383570` -> `383763`) while its other reported
|
||||
fields are unchanged. Its array length stays the same. These summaries represent
|
||||
actual topology entries; a difference is not harmless merely because it is compacted.
|
||||
|
||||
The clean base 7efbd5090 reproduced that unit's one changed case in a scratch
|
||||
worktree. This establishes a pre-existing local failure for that case only. It
|
||||
does not attribute the other 83 failing units to the baseline or establish a
|
||||
cross-platform cause. Missing upstream fixtures, including `Platform.smp`, were
|
||||
also reported by the PR-head sweep and need separate investigation.
|
||||
|
||||
## CI evidence and limits
|
||||
|
||||
Main's CI run 42399108643 at 8d7a0bf07 completed successfully, but it was a
|
||||
`push` run whose `goldens`, `clippy`, and `wasm` jobs were **skipped**. It is not
|
||||
evidence of a successful goldens comparison on another platform or on 7efbd5090.
|
||||
No same-revision cross-platform comparison has been performed here.
|
||||
|
||||
The PR's four focused units passed locally against their committed expectations.
|
||||
That establishes consistency with those expectations, not geometric correctness
|
||||
or a successful full sweep. Remaining failures must be compared against a matching
|
||||
clean base before being attributed or dismissed; canonical exact-head CI remains
|
||||
unverified while its workflow requires approval.
|
||||
|
||||
## Related investigation
|
||||
|
||||
Issue #2929 concerns a QP attempt-budget difference on aarch64 Linux/GB10. It does
|
||||
not establish the cause of these unrelated local goldens failures.
|
||||
@@ -0,0 +1,88 @@
|
||||
# The crossing classifier requested derivatives it never consumes
|
||||
|
||||
- **Units:** `offset.offset_geometry_creation`, `offset.offset_executive_p2`.
|
||||
- **Reported case:** #1818, the square-to-circle loft shelled inward by 1, open at
|
||||
its top. This is a correction within #3056, not a new construction or flag.
|
||||
- **Measured predecessor:** `0c53c6e0ebac6cd3eedcfa7416750c14b706b72c`, based on
|
||||
`b13f07fbdd7fe298f923911af88e266bb9fe4ea2`.
|
||||
|
||||
## Cause and bounded correction
|
||||
|
||||
An isolated diagnostic snapshot records four `MateEdges` failures at
|
||||
`offset_geometry_creation::passes_through`. Every backtrace reaches
|
||||
`OffsetSurface::evaluate` with `hu=2`, `hv=2`, `only_upper_half=false`. That is a
|
||||
fourth-total-order request, correctly refused by the implicit offset's existing
|
||||
maximum total order of three. The crossing formula reads only `Su`, `Sv`, `Suu`,
|
||||
`Suv` and `Svv`, so it needs only the second-order triangular jet.
|
||||
|
||||
The correction requests that jet with `only_upper_half=true`. It changes no
|
||||
formula, derivative limit, normal treatment, tolerance or requested-grid contract.
|
||||
The equal `(2,2)` triangular grid is not an unequal-grid alias workaround. No pole
|
||||
patch, retry or fallback is added.
|
||||
|
||||
The earlier stale-mating-bookkeeping hypothesis was refuted: original rim edges
|
||||
2, 6, 10 and 14 were already unmated before loop closure, and SSI remained enabled
|
||||
for both incident faces. No speculative invalidation was added. After the caller
|
||||
correction, the diagnostic #1818 selection has no derivative refusal and no
|
||||
gap-closing split; all four active tests pass (one generator stays ignored).
|
||||
|
||||
## Regression and verification
|
||||
|
||||
The new regression offsets the plane `z=-0.5` by `0.5`, independently confirming
|
||||
that its point is on `z=0`. It preserves the full `(2,2)` refusal and checks a
|
||||
within-tolerance touch, a lifted quadratic crossing and a shallow straight
|
||||
crossing against the existing geometric rule. With the production caller
|
||||
unchanged, the plane control passes and this regression fails on the unsupported
|
||||
request. With only the triangular caller correction, both pass.
|
||||
|
||||
The uncommitted source measured in the primary worktree has SHA-256
|
||||
`2713a22ea271eb08487333a7693794da0d57abbb5a073bd55ea41668772170e9`.
|
||||
Rust 1.98.0, `CARGO_BUILD_JOBS=2`, commands under `nice -n 10`:
|
||||
|
||||
- Both crossing regressions and all seven creator-module tests pass.
|
||||
- All four active #1818 tests pass, including the unchanged Level-0/1/2 validity,
|
||||
outward-solid, eleven-face and independent cavity-slab-volume assertions.
|
||||
- The 17-binary integration selection records 56 passed, 5 failed, 9 ignored;
|
||||
the predecessor records 53 passed, 8 failed, 9 ignored. The three repaired
|
||||
failures are #1818. The other failures are not waived.
|
||||
- Local-operation tests remain 28 passed, 7 failed, 2 ignored. No helper,
|
||||
reason assertion, rollback assertion or soundness-gate implementation changes.
|
||||
- Workspace fmt, workspace/all-target clippy (`-D warnings`), wasm32 and panic
|
||||
inventory pass.
|
||||
- All 2,745 cases of the four affected golden units remain selected, including
|
||||
121 existing known failures; none is added, removed or broken. The real gate
|
||||
still has 43 p2, 1 geometry-creation and 13 local-operation differences.
|
||||
|
||||
A complete ordinary-record comparison against the predecessor finds exactly one
|
||||
caller-induced change: `issue1818_square_to_circle_loft_api_shell_1_top`, from
|
||||
status 1001/no body to status 1000/ownership transferred and result counts
|
||||
`[2,2,11,12,24,96,16]` (regions, shells, faces, loops, edges, edgeuses, vertices),
|
||||
with bbox `[-20,-20,0,20,20,60]`. It was already an unapproved changed case; no
|
||||
previously matching ordinary case moves. All other ordinary encodings agree.
|
||||
The temporary raw-capture directory has no failure registry and preserves all
|
||||
known-case outputs too; it is not a second successful gate or an approval to
|
||||
bless them. Repository goldens and failure entries remain untouched.
|
||||
|
||||
## Native views and limitations
|
||||
|
||||
The same public-stage builder, view `-35,60` and image size `1200x900` are used.
|
||||
The before offset has 6 faces / 20 edges / 16 vertices and is non-manifold; the
|
||||
corrected merged offset has 6 / 12 / 8 and is manifold. The two offset images are
|
||||
explicitly native-edge wireframes: shaded intermediate rendering is unsupported
|
||||
because a face has no exact record. No intermediate meshing success is claimed.
|
||||
|
||||
| Before caller correction, native edges only | After caller correction, native edges only |
|
||||
|---|---|
|
||||
|  |  |
|
||||
|
||||
The corrected shell is 11 / 24 / 16 and manifold on both default and mated paths.
|
||||
Its native shaded render succeeds with 2,368 triangles and no refused face:
|
||||
|
||||

|
||||
|
||||
These images come from the diagnostic snapshot plus the single caller correction;
|
||||
the direct, uninstrumented primary-worktree tests supply the validity and volume
|
||||
evidence. The independently measured old-main control remains attributed to
|
||||
`6432dafe7d716a973a653fca72d5d4c7cd7002c8`, not to the newer integrated base.
|
||||
This record claims neither a full-suite/all-unit-goldens pass nor canonical CI
|
||||
success. The remaining #3056 construction and expectation blockers remain open.
|
||||
@@ -0,0 +1,124 @@
|
||||
# Disk results: repeated validation and a boundary-preservation hold (2026-11-08)
|
||||
|
||||
Evidence-only continuation of #3056. This reconciles the earlier soundness-only
|
||||
probe with the direct checks recorded beside the two justified pyramid results.
|
||||
It does not bless a disk, change a failure entry, implement a gate, or take
|
||||
#3084's per-entity matching work. The eleven disk rows remain unapproved.
|
||||
|
||||
## Four fresh sequences per exact retained case
|
||||
|
||||
Measured at published source head
|
||||
`ec0f6558107e36c48e7f9063b9ebfc05a2c3fe5f`. For each case, the public
|
||||
`topology_brep_local_operation::result_breps` reconstructs a new body and kernel
|
||||
independently for each sequence:
|
||||
|
||||
1. `validation::soundness`, three times on the same body;
|
||||
2. direct `check_brep(Level2)`, three times;
|
||||
3. `validation::standing`, three times;
|
||||
4. relabel this private diagnostic body's geometry with `use_arena_naming`, then
|
||||
`soundness`, three times.
|
||||
|
||||
There are 52 fresh constructions and 156 checks. No rename occurs in sequences
|
||||
1–3. All four sequences agree case by case:
|
||||
|
||||
| Case | First / second / third check | Stored UV trims before / after first check |
|
||||
|---|---|---|
|
||||
| `gen0001` | pass / fail / fail | 0 / 4 |
|
||||
| `gen0014` | pass / pass / pass | 0 / 4 |
|
||||
| `gen0019` | pass / pass / pass | 0 / 4 |
|
||||
| `gen0020` | pass / fail / fail | 0 / 4 |
|
||||
| `gen0023` | pass / fail / fail | 0 / 4 |
|
||||
| `gen0029` | pass / fail / fail | 0 / 4 |
|
||||
| `gen0049` | pass / fail / fail | 0 / 4 |
|
||||
| `gen0056` | pass / fail / fail | 0 / 4 |
|
||||
| `gen0073` | pass / fail / fail | 0 / 4 |
|
||||
| `gen0078` | pass / fail / fail | 0 / 4 |
|
||||
| `gen0086` | pass / fail / fail | 0 / 4 |
|
||||
| `gen0040` (pyramid control) | pass / pass / pass | 0 / 18 |
|
||||
| `gen0068` (pyramid control) | pass / pass / pass | 0 / 18 |
|
||||
|
||||
Every failing later pass reports:
|
||||
|
||||
```text
|
||||
SmTrimmingTools::Test #3 Loop Orientation: Face Loop has bad orientation [owner SmFace]
|
||||
AssertValid skipped: ValidatePointers failed
|
||||
```
|
||||
|
||||
Topology counts and all body/face/edge/vertex tolerances stay unchanged across
|
||||
the first check in every sequence. The disk results have eight edge uses; the
|
||||
first check materializes four UV trims. The pyramid controls have 36 edge uses
|
||||
and materialize 18. This refutes a naming-only explanation for the discrepancy:
|
||||
the unchanged `soundness` function itself returns pass then fail, without any
|
||||
relabeling. It does not prove that every internal cache or derived curve stays
|
||||
unchanged; the UV curves expressly do not.
|
||||
|
||||
The source explains the relevant ordering: `trimming_tools::check_face` returns
|
||||
without checking a loop when a stored UV curve is missing. Other checking paths
|
||||
can create those curves (`face::p3::compute_loop_orientation`, for example).
|
||||
The next pointer-validation pass then has curves to grade. These measurements
|
||||
do not choose a repair to that shared checking pipeline. Repeating a check in a
|
||||
new production gate merely to obtain a different verdict would not repair the
|
||||
construction. No such retry or gate change is added here.
|
||||
|
||||
## A separate original-input boundary witness: gen0001
|
||||
|
||||
The unchanged case replaces the one face of `disk.smb` with a parallel plane:
|
||||
offset `0.9196834024495337`, tilt `[0,0,0]`. Its original boundary is a rational
|
||||
quadratic upper semicircle and its diameter. The original 3D homogeneous control
|
||||
points are read directly from the retained file, not from kernel derivatives,
|
||||
sampled edges, a result, or a golden. All weights are positive. Therefore each
|
||||
curve, and the intended parallel translation of its bounded face, lies within
|
||||
the Euclidean control-coordinate bounds:
|
||||
|
||||
```text
|
||||
x in [-5, 5], y in [0, 5], z = 0.9196834024495337
|
||||
```
|
||||
|
||||
The proof uses exact rational arithmetic on the binary64 file inputs. In
|
||||
particular, `y <= 5` is a whole-curve positive-weight convex-hull bound, not a
|
||||
sampled estimate. The freshly returned result, captured before any check or
|
||||
owner rebinding, has a vertex at
|
||||
`[1.7535453870102995,7.5,0.9196834024495337]`: its y coordinate is **exactly 2.5**
|
||||
outside that bound. Another is at y `-2.4999999999999996`. This demonstrates a
|
||||
tangential boundary change, not just a different edge split of the original
|
||||
parallel-moved boundary. The result body tolerance is `1e-5`, not remotely that
|
||||
gap. No tolerance comparison is loosened.
|
||||
|
||||
The witness vertex's exact binary64 bits are
|
||||
`[4610576085524163749,4620130267728707584,4606458991202417516]`.
|
||||
The original `.smb` SHA-256 is
|
||||
`9b0c3f1aa62d191364c484f2a3ad23ee6e902e93c94759d5d8aab4392bc5202f`.
|
||||
All four input-hull controls pass: exact weighted bounds and z-only translation;
|
||||
nonpositive-weight rejection; the actual separation witness; and tampered-bit or
|
||||
tilted-case rejection. This is a negative preservation witness, not a complete
|
||||
boundary certificate for either result or the two tilted disk cases whose
|
||||
repeated checks stay clean. Those two are not blessed either.
|
||||
|
||||
## Native views
|
||||
|
||||
These are native `solidcore-diag` edge renders at the same `--view 0,90`, size
|
||||
`1200x900`; each image is independently framed by the renderer. The first is the
|
||||
original input, the second the exact generated result, not a hand-authored
|
||||
schematic or an invented before-result. Looking down z, the expected z-only
|
||||
translation keeps the input outline.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Evidence and scope
|
||||
|
||||
The 27-member executable/raw/native-picture archive was read back and
|
||||
hash-verified, SHA-256
|
||||
`170977910ee4732b3421637ccfbdcff33f8c8923506f2424774d4eedc3cdd7d8`.
|
||||
It retains every sequence's before/after states and verdicts, the original
|
||||
inputs, exact output bits, artifact identities, the input-hull model and controls,
|
||||
the original input and native result `.smb`, and both pictures. Local path:
|
||||
`~/.local/share/opencode/solidcore-cleanup-evidence/pr3056-disk-check-order-20261108-ec0f65581/`.
|
||||
|
||||
No production, case input, assertion, golden, known-failure registry, tolerance,
|
||||
floor or gate changes accompany this record. All 54 ordinary differences and
|
||||
the six failing local-operation tests remain open. This evidence does not
|
||||
implement #3073's tangent-round construction, #3084's gate, #3124's kept-edge
|
||||
restoration, #3174's manifold-result refusal, #3015's shell work, or the excluded
|
||||
pole-patch construction. The draft hold remains in force.
|
||||
@@ -0,0 +1,120 @@
|
||||
# Two generated pyramid results meet their five-plane solids (2026-11-08)
|
||||
|
||||
Continues #3056 / #2981. Only `gen0040` and `gen0068` in
|
||||
`topology.brep_local_operation` are justified here. No production code, tolerance,
|
||||
failure registry, case input, gate or excluded pole-patch construction changes.
|
||||
The other eleven local-operation golden differences remain unresolved.
|
||||
|
||||
## Exact cases and independent construction
|
||||
|
||||
Both cases read `upstream/TestFiles/pt_TestFiles/Solids/pyramid.smb`, without
|
||||
rebuilding its trims. Their original input body has 5 faces / 8 edges / 5 vertices.
|
||||
The cases replace three faces with explicit planes:
|
||||
|
||||
- `gen0040`: face 0 unchanged; face 1 offset by `-0.7722087962601212`, no tilt;
|
||||
face 2 unchanged.
|
||||
- `gen0068`: face 2 unchanged; face 1 offset by `-0.7342210955636247`, tilt
|
||||
`[-0.18041196740136894,0.1762487457010129,-0.20331349053160247]`; face 0
|
||||
offset 0, tilt `[-0.1304434045850086,-0.1498474917575508,-0.007725377686032431]`.
|
||||
|
||||
The runner's actual plane data is `p + offset*n`, normal `n + tilt`, from each
|
||||
original face's middle point and normal, not from the result. The independent
|
||||
Python calculation orients those five planes outward, enumerates all triples'
|
||||
closed-form intersections by Cramer's rule, and retains points in all five
|
||||
halfspaces. It builds each convex facet from those intersections and sums its
|
||||
oriented tetrahedra. No result vertex or kernel volume enters that calculation.
|
||||
It evaluates at 80 and 160 decimal digits, converting binary64 inputs exactly.
|
||||
|
||||
The two kept patches' complete homogeneous input nets lie on their planes to a
|
||||
maximum coefficient-distance residual of `6.075e-15`; this is below their plane
|
||||
dot-product rounding guard (`gamma_32` times the coordinate magnitude). The
|
||||
rounded patches with residual `1.447e-6` are **replaced** by explicit planes,
|
||||
not continued through uncertified poles. That distinction is essential: this
|
||||
proof does not certify the excluded curved/nonplanar pole construction.
|
||||
|
||||
## Measured geometry, topology and validity
|
||||
|
||||
Measured at exact source head `134a7186f1a14abf84ee4d0115c8cc6c302b6001`, before
|
||||
these expectation-only edits. The source body volume measured in this run is
|
||||
`983422.1665753094`; that is not relabelled as a historical old-result volume.
|
||||
|
||||
| Case | New F/E/V | Kernel volume | Independent five-plane volume | Relative error | Maximum two-way vertex distance |
|
||||
|---|---|---|---|---|---|
|
||||
| `gen0040` | 5/9/6 | `992044.1995192543` | `992044.1995195164535906177425004735` | `2.643e-13` | `8.152e-11` |
|
||||
| `gen0068` | 5/9/6 | `1093044.507611554` | `1093044.507611594137640470483235565` | `3.662e-14` | `9.620e-11` |
|
||||
|
||||
Both are manifold solids and all direct Level-0/1/2 checks are clean. Each has
|
||||
exactly the six independent intersections, checked in both directions at the
|
||||
existing `1e-9` part-scale vertex rule. Volumes satisfy the unchanged `1e-9`
|
||||
relative rule, with no new floor. Relative 80/160-digit volume drift is below
|
||||
`1e-79`. All six oracle controls pass: exact cube corners/volume; translated cube
|
||||
and reordered planes; the actual pyramid; wrong volume and vertex rejection;
|
||||
nonplanar kept-patch/invalid-body rejection; and the distinction between an
|
||||
explicitly replaced rounded patch and a kept patch.
|
||||
|
||||
## Native pictures
|
||||
|
||||
The existing `case_smb` example rebuilt the exact generated cases; the native
|
||||
`solidcore-diag` renderer drew their edges, not a hand-authored schematic. All
|
||||
three views use `--wireframe --view -35,30 --size 1200x900 --edges-above 0.001`.
|
||||
The small `gen0040` split is barely resolved in the whole-part view; its numeric
|
||||
vertex and edge checks above, not visual similarity, establish the split.
|
||||
|
||||

|
||||
|
||||
Original input: four sides meet at one apex.
|
||||
|
||||

|
||||
|
||||
`gen0040`: the short split is measured as six vertices and nine edges.
|
||||
|
||||

|
||||
|
||||
`gen0068`: the two top intersections and the edge between them are visible.
|
||||
|
||||
## Complete old and new golden encodings
|
||||
|
||||
The old encoding did not contain a volume field; the source-volume observation
|
||||
above is kept separate from it. Every replacement below equals the retained
|
||||
actual runner encoding. All other rows and every known-failure entry stay
|
||||
byte-unchanged.
|
||||
|
||||
`gen0040`, old:
|
||||
```json
|
||||
{"case":"gen0040","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1172,159991],[1948,311927],[978,28202]],"n":573}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0],"geometry":{"blocks":[[6.4e+21,6.4e+21,2.0799999999999997e+23]],"n":133},"record":{"blocks":[[156360.52780532837,158006.30563557148,19068776.25026989],[153903.1388885975,156472.02783429623,20181493.582973957],[104892.00006401539,108480.00015670061,12407745.286694974],[258.6666741371155,509.33334589004517,2804.4445185661316]],"n":784},"surface_owners":[1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[360.0,392.0,9416.0]],"n":32}}}
|
||||
```
|
||||
|
||||
`gen0040`, new:
|
||||
```json
|
||||
{"case":"gen0040","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1360,199480],[1748,274861],[1932,107271]],"n":623}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0],"geometry":{"blocks":[[7.2e+21,7.200000000000017e+21,2.628e+23]],"n":148},"record":{"blocks":[[143904.70981994172,148850.83660596592,18749454.884977996],[60967.80078066501,91192355.46067868,416542727.02352035]],"n":460},"surface_owners":[1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-36.0,36.0,-666.0]],"n":36}}}
|
||||
```
|
||||
|
||||
`gen0068`, old:
|
||||
```json
|
||||
{"case":"gen0068","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1172,159991],[1948,311927],[978,28202]],"n":573}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0],"geometry":{"blocks":[[6.4e+21,6.4e+21,2.0799999999999997e+23]],"n":133},"record":{"blocks":[[156360.52780532837,158006.30563557148,19068776.25026989],[153903.1388885975,156472.02783429623,20181493.582973957],[104892.00006401539,108480.00015670061,12407745.286694974],[258.6666741371155,509.33334589004517,2804.4445185661316]],"n":784},"surface_owners":[1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[360.0,392.0,9416.0]],"n":32}}}
|
||||
```
|
||||
|
||||
`gen0068`, new:
|
||||
```json
|
||||
{"case":"gen0068","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1360,199542],[1745,274520],[1935,107845]],"n":623}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0],"geometry":{"blocks":[[7.2e+21,7.200000000000021e+21,2.628e+23]],"n":148},"record":{"blocks":[[142267.17126916934,149545.78124175625,18538871.935180865],[60965.31864733237,112625909.83332224,505787772.57405066]],"n":460},"surface_owners":[1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-36.0,36.0,-666.0]],"n":36}}}
|
||||
```
|
||||
|
||||
## Preserved evidence and remaining holds
|
||||
|
||||
The 41-member archive was read back and hash-verified, SHA-256
|
||||
`a7a2c79290b672472bf6661707c68c3399bb1e91f75db426d44e28f12b47c049`.
|
||||
It retains the exact input/result data, original nets, executable convex model,
|
||||
controls, native `.smb` captures/images and all probe versions. Local path:
|
||||
`~/.local/share/opencode/solidcore-cleanup-evidence/pr3056-pyramid-input-planes-20261108-134a7186f/`.
|
||||
|
||||
The first probe did not reconcile the runner's notification identities with
|
||||
direct checks; its raw results remain retained, without attributing new failures.
|
||||
The later diagnostic gives its private bodies arena identities before direct checks. The eleven disk
|
||||
cases remain unapproved: their changed boundary geometry and discrepancies
|
||||
between the earlier custom-key soundness probe and direct checks require
|
||||
separate reconciliation. No result is blessed just because that earlier probe
|
||||
called it sound.
|
||||
|
||||
The PR remains draft: six local-operation test failures, other invalid/ungraded
|
||||
results, unresolved golden differences and canonical approval holds remain.
|
||||
No #3073, #3084 or #3124 implementation is taken here.
|
||||