diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index c43b2a14..2ad0dbe8 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -283,6 +283,7 @@ mod loft_spacing; mod mirror_planes; mod path_scope; mod primitive_points; +mod rim_cut; mod robustness; mod robustness_goldens; mod spiral_join; diff --git a/crates/engine/src/tests/fixtures/battery-tpu-case.solidcraft b/crates/engine/src/tests/fixtures/battery-tpu-case.solidcraft new file mode 100644 index 00000000..1d5902c0 --- /dev/null +++ b/crates/engine/src/tests/fixtures/battery-tpu-case.solidcraft @@ -0,0 +1,1484 @@ +{ + "format": "solidcraft/18", + "name": "Battery TPU Case", + "units": "mm", + "params": [ + { + "name": "batt_w", + "expr": "290 mm", + "unit": "mm", + "comment": "Battery flat top/bottom width", + "model": false + }, + { + "name": "batt_bulge", + "expr": "10 mm", + "unit": "mm", + "comment": "Side bulge past the top/bottom corners", + "model": false + }, + { + "name": "batt_h", + "expr": "92 mm", + "unit": "mm", + "comment": "Battery height", + "model": false + }, + { + "name": "peak_left", + "expr": "27 mm", + "unit": "mm", + "comment": "Height of the left side's widest point", + "model": false + }, + { + "name": "peak_right", + "expr": "batt_h - peak_left", + "unit": "mm", + "comment": "Height of the right side's widest point", + "model": false + }, + { + "name": "batt_len", + "expr": "198 mm", + "unit": "mm", + "comment": "Battery length", + "model": false + }, + { + "name": "tab_w", + "expr": "15 mm", + "unit": "mm", + "comment": "Connector tab width (from the left top corner)", + "model": false + }, + { + "name": "tab_depth", + "expr": "5.5 mm", + "unit": "mm", + "comment": "Connector tab protrusion", + "model": false + }, + { + "name": "ramp_len", + "expr": "35 mm", + "unit": "mm", + "comment": "Ramp from tab back to the wall", + "model": false + }, + { + "name": "ramp_blend_wall", + "expr": "80 mm", + "unit": "mm", + "comment": "Blend radius ramp to wall", + "model": false + }, + { + "name": "ramp_blend_tab", + "expr": "40 mm", + "unit": "mm", + "comment": "Blend radius tab to ramp", + "model": false + }, + { + "name": "wall", + "expr": "3 mm", + "unit": "mm", + "comment": "TPU wall thickness", + "model": false + }, + { + "name": "r_big", + "expr": "5 mm", + "unit": "mm", + "comment": "Fillet on the 4 long profile edges (inside and outside)", + "model": false + }, + { + "name": "r_small", + "expr": "1 mm", + "unit": "mm", + "comment": "Fillet on all other edges (inside and outside)", + "model": false + }, + { + "name": "pleat_depth", + "expr": "10 mm", + "unit": "mm", + "comment": "Accordion pleat depth at the open edge", + "model": false + }, + { + "name": "pleat_pitch", + "expr": "16 mm", + "unit": "mm", + "comment": "Accordion fold pitch", + "model": false + }, + { + "name": "clear", + "expr": "1 mm", + "unit": "mm", + "comment": "Overshoot so trimming tools never sit on a face", + "model": false + }, + { + "name": "d1", + "expr": "batt_w", + "unit": "mm", + "model": true + }, + { + "name": "d2", + "expr": "batt_w", + "unit": "mm", + "model": true + }, + { + "name": "d3", + "expr": "batt_h", + "unit": "mm", + "model": true + }, + { + "name": "d4", + "expr": "batt_bulge", + "unit": "mm", + "model": true + }, + { + "name": "d5", + "expr": "peak_right", + "unit": "mm", + "model": true + }, + { + "name": "d6", + "expr": "batt_bulge", + "unit": "mm", + "model": true + }, + { + "name": "d7", + "expr": "peak_left", + "unit": "mm", + "model": true + }, + { + "name": "d10", + "expr": "batt_w + 2 * batt_bulge + 10 * clear", + "unit": "mm", + "model": true + }, + { + "name": "d11", + "expr": "clear", + "unit": "mm", + "model": true + }, + { + "name": "d12", + "expr": "batt_len", + "unit": "mm", + "model": true + }, + { + "name": "d13", + "expr": "batt_len + tab_depth", + "unit": "mm", + "model": true + }, + { + "name": "d14", + "expr": "batt_w / 2 - tab_w", + "unit": "mm", + "model": true + }, + { + "name": "d15", + "expr": "ramp_len", + "unit": "mm", + "model": true + }, + { + "name": "d16", + "expr": "ramp_blend_wall", + "unit": "mm", + "model": true + }, + { + "name": "d17", + "expr": "ramp_blend_tab", + "unit": "mm", + "model": true + } + ], + "features": [ + { + "id": 1, + "name": "BatteryProfile", + "suppressed": false, + "type": "sketch", + "plane": { + "type": "custom", + "plane": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x_dir": [ + 1.0, + 0.0, + 0.0 + ], + "y_dir": [ + -0.0, + 0.0, + 1.0 + ] + } + }, + "sketch": { + "points": [ + { + "id": "origin", + "pos": [ + 0.0, + 0.0 + ], + "fixed": true + }, + { + "id": "l1.start", + "pos": [ + -145.0, + 92.0 + ], + "fixed": false + }, + { + "id": "l1.end", + "pos": [ + 145.0, + 92.0 + ], + "fixed": false + }, + { + "id": "l2.end", + "pos": [ + 155.0, + 65.0 + ], + "fixed": false + }, + { + "id": "l3.end", + "pos": [ + 145.0, + 0.0 + ], + "fixed": false + }, + { + "id": "l4.end", + "pos": [ + -145.0, + 0.0 + ], + "fixed": false + }, + { + "id": "l5.end", + "pos": [ + -155.0, + 27.0 + ], + "fixed": false + }, + { + "id": "p1", + "pos": [ + -100.0, + 0.0 + ], + "fixed": true, + "link": "j1" + }, + { + "id": "p2", + "pos": [ + 100.0, + 0.0 + ], + "fixed": true, + "link": "j1" + }, + { + "id": "p3", + "pos": [ + 0.0, + -100.0 + ], + "fixed": true, + "link": "j2" + }, + { + "id": "p4", + "pos": [ + 0.0, + 100.0 + ], + "fixed": true, + "link": "j2" + } + ], + "curves": [ + { + "id": "l1", + "type": "line", + "a": 1, + "b": 2, + "construction": false + }, + { + "id": "l2", + "type": "line", + "a": 2, + "b": 3, + "construction": false + }, + { + "id": "l3", + "type": "line", + "a": 3, + "b": 4, + "construction": false + }, + { + "id": "l4", + "type": "line", + "a": 4, + "b": 5, + "construction": false + }, + { + "id": "l5", + "type": "line", + "a": 5, + "b": 6, + "construction": false + }, + { + "id": "l6", + "type": "line", + "a": 6, + "b": 1, + "construction": false + }, + { + "id": "l7", + "type": "line", + "a": 7, + "b": 8, + "construction": true, + "link": "j1" + }, + { + "id": "l8", + "type": "line", + "a": 9, + "b": 10, + "construction": true, + "link": "j2" + } + ], + "constraints": [ + { + "id": "k1", + "type": "horizontal", + "l": 0 + }, + { + "id": "k2", + "type": "horizontal", + "l": 3 + }, + { + "id": "k3", + "type": "collinear", + "a": 3, + "b": 6 + }, + { + "id": "k4", + "type": "symmetric", + "p": 1, + "q": 2, + "l": 7 + }, + { + "id": "k5", + "type": "symmetric", + "p": 4, + "q": 5, + "l": 7 + }, + { + "id": "k6", + "type": "length", + "l": 0, + "value": 290.0, + "param": "d1" + }, + { + "id": "k7", + "type": "length", + "l": 3, + "value": 290.0, + "param": "d2" + }, + { + "id": "k8", + "type": "distance_y", + "p": 1, + "q": 5, + "value": 92.0, + "param": "d3" + }, + { + "id": "k9", + "type": "distance_x", + "p": 2, + "q": 3, + "value": 10.0, + "param": "d4" + }, + { + "id": "k10", + "type": "distance_y", + "p": 4, + "q": 3, + "value": 65.0, + "param": "d5" + }, + { + "id": "k11", + "type": "distance_x", + "p": 5, + "q": 6, + "value": 10.0, + "param": "d6" + }, + { + "id": "k12", + "type": "distance_y", + "p": 5, + "q": 6, + "value": 27.0, + "param": "d7" + } + ], + "links": [ + { + "id": "j1", + "kind": "project", + "source": { + "type": "sketch_axis", + "name": "x" + } + }, + { + "id": "j2", + "kind": "project", + "source": { + "type": "sketch_axis", + "name": "y" + } + } + ], + "counters": { + "j": 2, + "k": 12, + "l": 8, + "p": 4 + } + } + }, + { + "id": 2, + "name": "BatteryStock", + "suppressed": false, + "body_names": [ + "Battery" + ], + "param_names": [ + "d8" + ], + "type": "extrude", + "sketch": 1, + "profiles": { + "type": "all" + }, + "extent": { + "distance": "batt_len + tab_depth + clear", + "direction": "positive" + }, + "operation": "new_body" + }, + { + "id": 3, + "name": "PlanOutline", + "suppressed": false, + "param_names": [ + "d9" + ], + "type": "sketch", + "plane": { + "type": "offset", + "base": { + "type": "origin", + "name": "XY" + }, + "distance": "-clear" + }, + "sketch": { + "points": [ + { + "id": "origin", + "pos": [ + 0.0, + 0.0 + ], + "fixed": true + }, + { + "id": "l1.start", + "pos": [ + -160.0, + 1.0 + ], + "fixed": false + }, + { + "id": "l1.end", + "pos": [ + 160.0, + 1.0 + ], + "fixed": false + }, + { + "id": "l2.end", + "pos": [ + 160.0, + -198.0 + ], + "fixed": false + }, + { + "id": "corner1", + "pos": [ + -95.0, + -198.0 + ], + "fixed": false + }, + { + "id": "corner2", + "pos": [ + -130.0, + -203.5 + ], + "fixed": false + }, + { + "id": "l5.end", + "pos": [ + -160.0, + -203.5 + ], + "fixed": false + }, + { + "id": "p1", + "pos": [ + 0.0, + -100.0 + ], + "fixed": true, + "link": "j1" + }, + { + "id": "p2", + "pos": [ + 0.0, + 100.0 + ], + "fixed": true, + "link": "j1" + }, + { + "id": "p3", + "pos": [ + -88.75261852925456, + -198.0 + ], + "fixed": false + }, + { + "id": "p4", + "pos": [ + -101.17164524862525, + -198.96982996764112 + ], + "fixed": false + }, + { + "id": "a1.center", + "pos": [ + -88.75261852925458, + -278.0 + ], + "fixed": false + }, + { + "id": "p5", + "pos": [ + -126.91417737568737, + -203.01508501617946 + ], + "fixed": false + }, + { + "id": "p6", + "pos": [ + -133.12369073537272, + -203.5 + ], + "fixed": false + }, + { + "id": "a2.center", + "pos": [ + -133.12369073537272, + -163.5 + ], + "fixed": false + } + ], + "curves": [ + { + "id": "l1", + "type": "line", + "a": 1, + "b": 2, + "construction": false + }, + { + "id": "l2", + "type": "line", + "a": 2, + "b": 3, + "construction": false + }, + { + "id": "l3", + "type": "line", + "a": 3, + "b": 9, + "construction": false + }, + { + "id": "l4", + "type": "line", + "a": 10, + "b": 12, + "construction": false + }, + { + "id": "l5", + "type": "line", + "a": 13, + "b": 6, + "construction": false + }, + { + "id": "l6", + "type": "line", + "a": 6, + "b": 1, + "construction": false + }, + { + "id": "l7", + "type": "line", + "a": 7, + "b": 8, + "construction": true, + "link": "j1" + }, + { + "id": "a1", + "type": "arc", + "c": 11, + "a": 9, + "b": 10, + "construction": false + }, + { + "id": "a2", + "type": "arc", + "c": 14, + "a": 13, + "b": 12, + "construction": false + } + ], + "constraints": [ + { + "id": "k1", + "type": "horizontal", + "l": 0 + }, + { + "id": "k2", + "type": "vertical", + "l": 1 + }, + { + "id": "k3", + "type": "horizontal", + "l": 2 + }, + { + "id": "k4", + "type": "horizontal", + "l": 4 + }, + { + "id": "k5", + "type": "vertical", + "l": 5 + }, + { + "id": "k6", + "type": "symmetric", + "p": 1, + "q": 2, + "l": 6 + }, + { + "id": "k7", + "type": "length", + "l": 0, + "value": 320.0, + "param": "d10" + }, + { + "id": "k8", + "type": "distance_y", + "p": 0, + "q": 1, + "value": 1.0, + "param": "d11" + }, + { + "id": "k9", + "type": "distance_y", + "p": 0, + "q": 3, + "value": 198.0, + "param": "d12" + }, + { + "id": "k10", + "type": "distance_y", + "p": 0, + "q": 5, + "value": 203.5, + "param": "d13" + }, + { + "id": "k11", + "type": "distance_x", + "p": 0, + "q": 5, + "value": 130.0, + "param": "d14" + }, + { + "id": "k12", + "type": "distance_x", + "p": 5, + "q": 4, + "value": 35.0, + "param": "d15" + }, + { + "id": "k13", + "type": "point_on_curve", + "p": 4, + "c": 2 + }, + { + "id": "k14", + "type": "point_on_curve", + "p": 4, + "c": 3 + }, + { + "id": "k15", + "type": "tangent", + "a": 2, + "b": 7 + }, + { + "id": "k16", + "type": "tangent", + "a": 3, + "b": 7 + }, + { + "id": "k17", + "type": "radius", + "c": 7, + "value": 80.0, + "param": "d16" + }, + { + "id": "k18", + "type": "point_on_curve", + "p": 5, + "c": 3 + }, + { + "id": "k19", + "type": "point_on_curve", + "p": 5, + "c": 4 + }, + { + "id": "k20", + "type": "tangent", + "a": 3, + "b": 8 + }, + { + "id": "k21", + "type": "tangent", + "a": 4, + "b": 8 + }, + { + "id": "k22", + "type": "radius", + "c": 8, + "value": 40.0, + "param": "d17" + } + ], + "links": [ + { + "id": "j1", + "kind": "project", + "source": { + "type": "sketch_axis", + "name": "y" + } + } + ], + "counters": { + "a": 2, + "corner": 2, + "j": 1, + "k": 22, + "l": 7, + "p": 6 + } + } + }, + { + "id": 4, + "name": "TabAndRamp", + "suppressed": false, + "param_names": [ + "d18" + ], + "type": "extrude", + "sketch": 3, + "profiles": { + "type": "all" + }, + "extent": { + "distance": "batt_h + 2 * clear", + "direction": "positive" + }, + "operation": "intersect", + "targets": [ + "Battery" + ] + }, + { + "id": 5, + "name": "CopyForCase", + "suppressed": false, + "body_names": [ + "Case" + ], + "param_names": [ + "d19", + "d20", + "d21" + ], + "type": "move", + "bodies": [ + "Battery" + ], + "translate": [ + "0", + "0", + "0" + ], + "copy": true + }, + { + "id": 6, + "name": "CaseShell", + "suppressed": false, + "param_names": [ + "d22" + ], + "face_names": [ + "F2:side:l4" + ], + "type": "shell", + "faces": [ + [ + 0.0, + -100.0, + 0.0 + ] + ], + "thickness": "wall", + "body": "Case", + "direction": "outside" + }, + { + "id": 7, + "name": "Fillet1", + "suppressed": false, + "param_names": [ + "d23" + ], + "edge_refs": [ + { + "dir": [ + 0.0, + -1.0, + 0.0 + ], + "min": [ + -158.08338344416106, + -206.5, + -2.220446049250313e-16 + ], + "max": [ + 158.08338344416106, + 3.0000000000000004, + 95.0 + ] + }, + { + "dir": [ + 0.0, + -1.0, + 0.0 + ], + "min": [ + -158.08338344416106, + -206.5, + -2.220446049250313e-16 + ], + "max": [ + 158.08338344416106, + 3.0000000000000004, + 95.0 + ] + }, + { + "dir": [ + 0.0, + 1.0, + 0.0 + ], + "min": [ + -158.08338344416106, + -206.5, + -2.220446049250313e-16 + ], + "max": [ + 158.08338344416106, + 3.0000000000000004, + 95.0 + ] + }, + { + "dir": [ + 0.0, + -1.0, + 0.0 + ], + "min": [ + -158.08338344416106, + -206.5, + -2.220446049250313e-16 + ], + "max": [ + 158.08338344416106, + 3.0000000000000004, + 95.0 + ] + }, + { + "dir": [ + 0.0, + 1.0, + 0.0 + ], + "min": [ + -158.08338344416106, + -206.5, + -2.220446049250313e-16 + ], + "max": [ + 158.08338344416106, + 3.0000000000000004, + 95.0 + ] + }, + { + "dir": [ + 0.0, + 1.0, + 0.0 + ], + "min": [ + -158.08338344416106, + -206.5, + -2.220446049250313e-16 + ], + "max": [ + 158.08338344416106, + 3.0000000000000004, + 95.0 + ] + }, + { + "dir": [ + 0.0, + -1.0, + 0.0 + ], + "min": [ + -158.08338344416106, + -206.5, + -2.220446049250313e-16 + ], + "max": [ + 158.08338344416106, + 3.0000000000000004, + 95.0 + ] + }, + { + "dir": [ + 0.0, + 1.0, + 0.0 + ], + "min": [ + -158.08338344416106, + -206.5, + -2.220446049250313e-16 + ], + "max": [ + 158.08338344416106, + 3.0000000000000004, + 95.0 + ] + } + ], + "edge_names": [ + "F2:side:l5@F5.1|F2:side:l6@F5.1", + "F2:side:l1@F5.1|F2:side:l6@F5.1", + "F2:side:l1@F5.1|F2:side:l2@F5.1", + "F2:side:l2@F5.1|F2:side:l3@F5.1", + "F6:inner:F2:side:l5@F5.1|F6:inner:F2:side:l6@F5.1", + "F6:inner:F2:side:l1@F5.1|F6:inner:F2:side:l6@F5.1", + "F6:inner:F2:side:l1@F5.1|F6:inner:F2:side:l2@F5.1", + "F6:inner:F2:side:l2@F5.1|F6:inner:F2:side:l3@F5.1" + ], + "type": "fillet", + "edges": [ + [ + -158.0834, + -101.75, + 26.6874 + ], + [ + -147.5738, + -101.75, + 95.0 + ], + [ + 147.088, + -99.0, + 95.0 + ], + [ + 158.0834, + -99.0, + 65.3126 + ], + [ + -155.0, + -101.75, + 27.0 + ], + [ + -145.0, + -101.75, + 92.0 + ], + [ + 145.0, + -99.0, + 92.0 + ], + [ + 155.0, + -99.0, + 65.0 + ] + ], + "radius": "r_big", + "body": "Case" + }, + { + "id": 8, + "name": "Fillet2", + "suppressed": false, + "param_names": [ + "d24" + ], + "edge_refs": [ + { + "dir": [ + 0.34731435582359377, + 0.0, + -0.9377487607237037 + ], + "min": [ + -157.9189160095873, + -206.5, + -2.220446049250313e-16 + ], + "max": [ + 157.9189160095873, + 3.0000000000000004, + 95.0 + ] + }, + { + "dir": [ + -0.1520571842539413, + 6.854850442197856e-18, + -0.9883716976506173 + ], + "min": [ + -157.9189160095873, + -206.5, + -2.220446049250313e-16 + ], + "max": [ + 157.9189160095873, + 3.0000000000000004, + 95.0 + ] + }, + { + "dir": [ + -0.3473143558235947, + 0.0, + 0.9377487607237034 + ], + "min": [ + -157.9189160095873, + -206.5, + -2.220446049250313e-16 + ], + "max": [ + 157.9189160095873, + 3.0000000000000004, + 95.0 + ] + }, + { + "dir": [ + 0.15205718425394107, + 6.990187846251234e-18, + 0.9883716976506172 + ], + "min": [ + -157.9189160095873, + -206.5, + -2.220446049250313e-16 + ], + "max": [ + 157.9189160095873, + 3.0000000000000004, + 95.0 + ] + }, + { + "dir": [ + 1.0, + -1.5479309295511088e-18, + 0.0 + ], + "min": [ + -157.9189160095873, + -206.5, + -2.220446049250313e-16 + ], + "max": [ + 157.9189160095873, + 3.0000000000000004, + 95.0 + ] + } + ], + "edge_names": [ + "F2:side:l2@F5.1|F2:start@F5.1", + "F2:side:l3@F5.1|F2:start@F5.1", + "F2:side:l5@F5.1|F2:start@F5.1", + "F2:side:l6@F5.1|F2:start@F5.1", + "F2:side:l1@F5.1|F2:start@F5.1" + ], + "type": "fillet", + "edges": [ + [ + 152.5857, + 3.0, + 80.1563 + ], + [ + 153.0593, + 3.0, + 32.6563 + ], + [ + -153.1413, + 3.0, + 13.3437 + ], + [ + -152.8286, + 3.0, + 60.8437 + ], + [ + -0.2429, + 3.0, + 95.0 + ] + ], + "radius": "r_small", + "body": "Case" + }, + { + "id": 9, + "name": "Fillet3", + "suppressed": false, + "param_names": [ + "d25" + ], + "edge_refs": [ + { + "dir": [ + -1.0, + -2.4685277687458984e-16, + 0.0 + ], + "min": [ + -157.91891600958726, + -206.5, + -2.220446049250313e-16 + ], + "max": [ + 157.91891600958732, + 3.000000000000001, + 95.0 + ] + } + ], + "edge_names": [ + "F6:inner:F2:side:l1@F5.1|F6:inner:F4:side:l3@F5.1" + ], + "type": "fillet", + "edges": [ + [ + 26.3837, + -198.0, + 92.0 + ] + ], + "radius": "r_small", + "body": "Case" + }, + { + "id": 10, + "name": "Fillet4", + "suppressed": false, + "param_names": [ + "d26" + ], + "edge_refs": [ + { + "dir": [ + 0.1520571842539411, + 0.0, + 0.988371697650617 + ], + "min": [ + -157.91891600958726, + -206.5, + -1.6689699017913788e-14 + ], + "max": [ + 157.91891600958732, + 3.000000000000001, + 95.0 + ] + }, + { + "dir": [ + -0.34731435582359377, + 0.0, + 0.9377487607237037 + ], + "min": [ + -157.91891600958726, + -206.5, + -1.6689699017913788e-14 + ], + "max": [ + 157.91891600958732, + 3.000000000000001, + 95.0 + ] + }, + { + "dir": [ + -1.0, + 0.0, + 0.0 + ], + "min": [ + -157.91891600958726, + -206.5, + -1.6689699017913788e-14 + ], + "max": [ + 157.91891600958732, + 3.000000000000001, + 95.0 + ] + }, + { + "dir": [ + -0.15205718425394102, + 0.0, + -0.9883716976506172 + ], + "min": [ + -157.91891600958726, + -206.5, + -1.6689699017913788e-14 + ], + "max": [ + 157.91891600958732, + 3.000000000000001, + 95.0 + ] + }, + { + "dir": [ + 0.3473143558235949, + 0.0, + -0.9377487607237033 + ], + "min": [ + -157.91891600958726, + -206.5, + -1.6689699017913788e-14 + ], + "max": [ + 157.91891600958732, + 3.000000000000001, + 95.0 + ] + } + ], + "edge_names": [ + "F6:inner:F2:side:l3@F5.1|F6:inner:F2:start@F5.1", + "F6:inner:F2:side:l2@F5.1|F6:inner:F2:start@F5.1", + "F6:inner:F2:side:l1@F5.1|F6:inner:F2:start@F5.1", + "F6:inner:F2:side:l6@F5.1|F6:inner:F2:start@F5.1", + "F6:inner:F2:side:l5@F5.1|F6:inner:F2:start@F5.1" + ], + "type": "fillet", + "edges": [ + [ + 149.9014, + 0.0, + 31.8594 + ], + [ + 150.3792, + 0.0, + 77.4761 + ], + [ + 0.4048, + 0.0, + 92.0 + ], + [ + -150.2276, + 0.0, + 58.0208 + ], + [ + -149.7749, + 0.0, + 12.8922 + ] + ], + "radius": "r_small", + "body": "Case" + } + ], + "next_id": 30, + "visibility": { + "hidden_bodies": [ + "Repro" + ], + "hidden_occurrences": [], + "hidden_origin": [], + "hidden_sketches": [], + "shown_sketches": [] + } +} \ No newline at end of file diff --git a/crates/engine/src/tests/rim_cut.rs b/crates/engine/src/tests/rim_cut.rs new file mode 100644 index 00000000..9de40f3a --- /dev/null +++ b/crates/engine/src/tests/rim_cut.rs @@ -0,0 +1,57 @@ +//! #882: a cut through the open rim of a shelled case failed once an R1 round went on the outer +//! loop of the other end face. One strip of that round (along a sloped side edge, far from the +//! cut) was never meshed: its boundary was projected onto its surface starting from a corner +//! that a search without a hint misses, so the face was dropped. Every boolean with the body +//! then failed (truck's meshes both operands to classify faces), and the exact fallback refused +//! it for the same face. The owner's design, `battery-tpu-case.solidcraft` (format 18). + +use std::path::Path; + +use serde_json::json; + +use super::run; +use crate::Session; + +fn open_at(feature: &str) -> Session { + let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/tests/fixtures/battery-tpu-case.solidcraft"); + let mut s = Session::default(); + run(&mut s, "doc.open", json!({"path": path.to_string_lossy()})); + run(&mut s, "timeline.roll_to", json!({"feature": feature})); + s +} + +/// The window cut through the front wall and its rim, 48 × 4 × 50 from below the rim. +fn cut_window(s: &mut Session) -> serde_json::Value { + run(s, "solid.box", json!({"corner": [-24, -0.5, -1], "length": 48, "width": 4, "height": 50, "operation": "cut", "targets": ["Case"]})) +} + +/// Every face of the case after the round meshes (the strip had no triangles at any tolerance). +#[test] +fn every_face_of_the_rounded_case_meshes() { + let s = open_at("Fillet2"); + let case = s.model.state().body("Case").map(|b| b.body.clone()).unwrap(); + let faces = case.faces(0.05).unwrap(); + assert_eq!(faces.len(), case.face_count()); + let unmeshed: Vec = faces.iter().filter(|f| f.area <= 0.0).map(|f| f.index).collect(); + assert!(unmeshed.is_empty(), "faces with no mesh: {unmeshed:?}"); +} + +/// The cut takes the same window after the round as before it (the round is 120 mm away), in one +/// boolean with no retry. +#[test] +fn rim_cut_after_the_round_on_the_other_end() { + let mut before = open_at("Fillet1"); + let mut after = open_at("Fillet2"); + let faces = |s: &Session| s.model.state().body("Case").map(|b| b.body.face_count()).unwrap(); + let (f1, f2) = (faces(&before), faces(&after)); + cut_window(&mut before); + let id = cut_window(&mut after)["feature"].as_u64().unwrap(); + let result = after.model.result(id).unwrap(); + assert!(result.error.is_none() && result.warning.is_none(), "{result:?}"); + assert!(result.boolean_diagnostics.events.is_empty(), "{:?}", result.boolean_diagnostics); + // The window's three faces (two sides and the top), at both points of the timeline. + assert_eq!(faces(&before), f1 + 3); + assert_eq!(faces(&after), f2 + 3); + let case = after.model.state().body("Case").map(|b| b.body.clone()).unwrap(); + assert_eq!(case.shell_count(), 1); +} diff --git a/vendor/README.md b/vendor/README.md index 0d30476d..e5e9c1a6 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -59,6 +59,13 @@ Changes: This prevents huge arbitrary pole parameters from corrupting trimmed meshes (#796), while retaining genuine extrapolation for unbounded planes. Work is bounded to four endpoint evaluations per projected point; no search or tessellation tolerance is relaxed. +- `tessellation/triangulation.rs`: a boundary loop is projected onto its surface point by point, + each searched from the one before; the first has no hint. When that walk fails, it is tried + once more from the first point an unhinted search finds. An unhinted search can miss a point + that lies on the surface, such as the corner of a fillet strip on a surface extended past it, + and the face was then left unmeshed, which also failed every boolean with the body (#882). A + walk that succeeds is kept as it was: where a loop starts decides how it is laid out across a + periodic surface's seam. - `tessellation/triangulation.rs`: the inside test of a face's boundary skips loops whose parameter box is clear of the point (it tested every segment of every loop, so a face with hundreds of holes meshed in quadratic time). diff --git a/vendor/truck-meshalgo/src/tessellation/triangulation.rs b/vendor/truck-meshalgo/src/tessellation/triangulation.rs index c5cb86de..558d202a 100644 --- a/vendor/truck-meshalgo/src/tessellation/triangulation.rs +++ b/vendor/truck-meshalgo/src/tessellation/triangulation.rs @@ -411,43 +411,60 @@ impl PolyBoundaryPiece { ) -> Option { let (up, vp) = (surface.u_period(), surface.v_period()); let (urange, vrange) = surface.try_range_tuple(); - let mut bdry3d: Vec = wire + let bdry3d: Vec = wire .flat_map(|poly_edge| { let n = poly_edge.len() - 1; poly_edge.into_iter().take(n) }) .collect(); - bdry3d.push(bdry3d[0]); - let mut previous = None; - let mut vec = bdry3d - .into_iter() - .flat_map(|pt| { - let (mut u, mut v) = match sp(surface, pt, previous) { - // SolidCraft: validate exact and nearest results alike, once per - // boundary point, including the bounded provisional preview. - Some(hint) => sane(surface, pt, hint), - None => return vec![None], - }; - if let (Some(up), Some((u0, _))) = (up, previous) { - u = get_mindiff(u, u0, up); - } - if let (Some(vp), Some((_, v0))) = (vp, previous) { - v = get_mindiff(v, v0, vp); - } - let res = (|| { - if let Some((u0, v0)) = previous { - if !u0.near(&u) && surface.uder(u0, v0).so_small() { - return vec![Some(Point2::new(u, v0)), Some(Point2::new(u, v))]; - } else if !v0.near(&v) && surface.vder(u0, v0).so_small() { - return vec![Some(Point2::new(u0, v)), Some(Point2::new(u, v))]; - } + let walk = |mut bdry3d: Vec| -> Option> { + bdry3d.push(bdry3d[0]); + let mut previous = None; + bdry3d + .into_iter() + .flat_map(|pt| { + let (mut u, mut v) = match sp(surface, pt, previous) { + // SolidCraft: validate exact and nearest results alike, once per + // boundary point, including the bounded provisional preview. + Some(hint) => sane(surface, pt, hint), + None => return vec![None], + }; + if let (Some(up), Some((u0, _))) = (up, previous) { + u = get_mindiff(u, u0, up); } - vec![Some(Point2::new(u, v))] - })(); - previous = Some((u, v)); - res - }) - .collect::>>()?; + if let (Some(vp), Some((_, v0))) = (vp, previous) { + v = get_mindiff(v, v0, vp); + } + let res = (|| { + if let Some((u0, v0)) = previous { + if !u0.near(&u) && surface.uder(u0, v0).so_small() { + return vec![Some(Point2::new(u, v0)), Some(Point2::new(u, v))]; + } else if !v0.near(&v) && surface.vder(u0, v0).so_small() { + return vec![Some(Point2::new(u0, v)), Some(Point2::new(u, v))]; + } + } + vec![Some(Point2::new(u, v))] + })(); + previous = Some((u, v)); + res + }) + .collect::>>() + }; + // SolidCraft: each point is searched from the one before it, but the first has no + // hint, and a search without one can miss a point that lies on the surface (a corner of + // a fillet strip on a surface extended past it, #882): the whole face was then left + // unmeshed. When the walk fails, it is tried once more from the first point an unhinted + // search finds. A walk that succeeds is kept as it is (where a loop starts decides how + // it is laid out across a periodic surface's seam). + let mut vec = match walk(bdry3d.clone()) { + Some(vec) => vec, + None => { + let start = bdry3d.iter().skip(1).position(|pt| sp(surface, *pt, None).is_some())? + 1; + let mut rotated = bdry3d; + rotated.rotate_left(start); + walk(rotated)? + } + }; let grav = vec.iter().fold(Point2::origin(), |g, p| g + p.to_vec()) / vec.len() as f64; if let (Some(up), Some((u0, _))) = (up, urange) { let quot = f64::floor((grav.x - u0) / up);