Files

302 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Action matrix
The action matrix tries every ordinary action on every face, edge, vertex and body of the final
bodies of a corpus of real designs, through the same commands and parameters the desktop sends,
and records what works. [`tests/action_matrix/baseline.json`](../tests/action_matrix/baseline.json)
is a ratchet: an action that passed there and fails now is a regression and fails the test.
The test is [`apps/solidcraft-cli/tests/action_matrix.rs`](../apps/solidcraft-cli/tests/action_matrix.rs).
## Running it
- **Every CI run** (pull requests too) checks a fixed sample, one action in 40 by a stable hash of
its name, on every design except the slowest to build (the `SLOW` list, and any design that
takes more than 8 s to load in costs.json; `action_matrix_sample`, a few minutes).
- **Nightly** (and by hand, Run workflow), the "Action matrix" jobs run the whole matrix in 16
shards (`action_matrix_shard` with `ACTION_MATRIX_SHARD=k/16`) of about equal cost, cut by
[`tests/action_matrix/costs.json`](../tests/action_matrix/costs.json): what each design took on a
runner. A design that takes more than ten minutes is split into slices (actions by a hash of their name, each
slice loading the design again), and the pieces go largest first to the shard with the least
work. A design not in costs.json yet counts as two minutes. Each shard uploads its outcomes and
timings (artifact `action-matrix-k`); with all sixteen in one folder,
`cargo xtask action-matrix --from DIR --bless` writes the baseline and costs.json from them
without running anything. `ACTION_MATRIX_SHARD=k/n ACTION_MATRIX_SHARD_LIST=1` prints the plan.
- **Determinism** (nightly, "Action matrix determinism"): `action_matrix_determinism` runs one
action in 8 of every design that takes at most five minutes twice on one runner and lists every action whose outcome changed; it fails when a
family of pieces passes a different number of times.
- **Locally**: `cargo xtask action-matrix` runs it whole (a few hours on one machine now; the nightly shards' artifacts and `--from` are quicker). Add `--bless` to
rewrite the baseline after a fix (newly passing actions are listed when you don't), and
`--results FILE` to keep every outcome with its message and the command that was sent.
`ACTION_MATRIX_FILTER=oracle/23` runs only the designs whose name contains it;
`ACTION_MATRIX_JOBS` sets how many designs run at once.
For a quick local look, `ACTION_MATRIX_ACTIONS=edge.fillet,edge.chamfer` tries only those actions (never
bless such a run) and `ACTION_MATRIX_VALIDITY=1` adds each passing action's body check (`Body::validity`)
to the results.
`ACTION_MATRIX_UNFOLD=1` also unfolds and refolds every sheet metal body after each action that
passed, recorded as `<action>+unfold`. To recheck one design on its own:
`ACTION_MATRIX_DESIGN=oracle/69-sm-tray ACTION_MATRIX_OUT=out.jsonl ACTION_MATRIX_ACTIONS=face.hole
cargo test --profile ci -p solidcraft-cli --test action_matrix action_matrix_worker -- --exact --ignored`
(from `apps/solidcraft-cli`).
Each design runs in its own process, so an action that hangs (90 s without a result) or aborts is
recorded as `timeout` or `crash` and the rest of the design still runs. An action that passes in the baseline and
times out is listed but not judged (and stays passing when blessed): the slowest actions take 40 to
80 s on a busy runner and land either side of the limit from night to night. Loading a design, before
its first action, may take up to 300 s (the largest take several times longer on a loaded CI
runner than here). None did in this run.
Pieces that share a name (`#2`, `~3`) are numbered by where they lie, and the numbers of pieces
lying almost level can swap between runs (#569). The ratchet therefore counts passes per family of
pieces (the name without its numbers) and samples whole families, so a swap is not a regression.
Each open cause below has a small reproduction among the ignored tests at the end of
`action_matrix.rs` (`cargo test --profile ci -p solidcraft-cli --test action_matrix -- --ignored`
lists which still fail; skip `action_matrix_` to leave out the matrix itself).
## The corpus
- `corpus/*`: the old design files of [`apps/solidcraft-cli/tests/corpus`](../apps/solidcraft-cli/tests/corpus/).
- `examples/*`, `regressions/*`: the scripts in [`examples/`](../examples/).
- `oracle/*`: the 76 reference recipes, rebuilt (`solidcraft-cli recipe`).
- `demo/enclosure_steps`: the electronics enclosure built over MCP;
`demo/pi-case`: the Raspberry Pi 4 case of the 0.6 README demo, built through the desktop UI
and saved by `examples/demo/enclosure_ui_mcp.py --story pi-0.6 --save`;
`demo/pi-case-0.8`: the 0.8 README demo's case (`--story pi`), with a rounded rim, engraved
text and a PETG material;
`demo/logo-up-down-join`: the SolidCraft logo as it was reported in #954, one sketch extruded
up and down and joined.
- `typical/*`: [`tests/action_matrix/designs`](../tests/action_matrix/designs/), parts made the
ordinary way: a stacked extrude, a filleted and shelled box, a revolved knob, a plate with a
counterbored hole patterned 3 × 2 and a chamfered outline, a sheet metal L bracket with holes, a
join and a cut of three bodies, a loft on a plate, a sweep, and one sketch of squares drawn edge
to edge extruded up and down and joined (`up-down-join`).
## The actions
Each one runs on a scratch copy of the design. It passes when the command works, the timeline
has no new errors, and any sketch it makes is attached to its face (healthy, not a legacy face
sketch found again by a point). A face pick is what the desktop sends: the world body, the face
index, the occurrence path and a point inside the face; never `fixed`.
| Entity | Action | Command and parameters |
|---|---|---|
| planar face | `face.sketch` | `sketch.create` on the face |
| | `face.offset_plane` | `construct.plane.offset`, 10% of the body's size |
| | `face.angle_plane` | `construct.plane.perpendicular` through the face's longest straight edge |
| | `face.midplane` | `construct.plane.midplane` with the nearest opposite parallel face |
| | `face.press_pull`, `face.extrude` | `solid.press_pull` / `solid.extrude` (`face`, join), 5% of the size (0.5 to 5 mm) |
| | `face.hole` | `solid.hole` well inside the face (0.8 × the way to its nearest edge, at most 6 mm), through all |
| | `face.draft_neutral` | `solid.draft` of the walls square to the face, 2°, the face's plane neutral |
| | `face.shell` | `solid.shell` removing the face, 1% of the size (0.1 to 0.5 mm) |
| cylinder, cone, torus, sphere | `curved.axis` | `construct.axis.through_cylinder` (not spheres) |
| | `curved.tangent_plane` | `construct.plane.tangent` |
| edge | `edge.fillet`, `edge.chamfer` | the edge alone, 10% of the shortest edge at its ends (not tangent edges, where there is nothing to blend) |
| | `edge.axis` | `construct.axis.edge` (straight edges) |
| | `edge.normal_plane` | `construct.plane.along_path`, halfway |
| | `edge.project` | a sketch on the origin plane it shows longest in, `sketch.project` |
| vertex | `vertex.point` | `construct.point.vertex` |
| | `vertex.plane_three_points` | `construct.plane.three_points` with two neighbours along its edges |
| body | `body.move`, `body.copy`, `body.mirror`, `body.pattern` | `solid.move` 10 mm, and with `copy` beside it; `solid.mirror` about YZ; `solid.pattern.rectangular` 2 × |
Sketching on a cylinder's cap is covered by `face.sketch` on the planar cap. Bodies are capped at
120 faces, 240 edges and 120 vertices each. Entities are named by their persistent names
(`F2:top`, edges `face|face`, vertices by the faces around them).
**The edit round**: one pass in six also changes a driving parameter (the first user parameter,
else a feature's, by 10%, one that the design itself survives) and checks the new feature still
resolves; it is recorded as `<action>+edit`.
## Results
**30567 of 35576 actions pass (85.9%)** on 104 designs; the edit round passes 1370 of 1386.
### By category
| Category | Actions | Pass | Rate |
|---|---:|---:|---:|
| body | 472 | 471 | 99.8% |
| curved | 1848 | 1814 | 98.2% |
| edge | 18763 | 17469 | 93.1% |
| face | 9323 | 5679 | 60.9% |
| vertex | 5170 | 5134 | 99.3% |
### By action
| Action | Actions | Pass | Rate | Edit round |
|---|---:|---:|---:|---:|
| `body.copy` | 118 | 118 | 100.0% | 6 / 6 |
| `body.mirror` | 118 | 118 | 100.0% | 1 / 1 |
| `body.move` | 118 | 118 | 100.0% | 3 / 3 |
| `body.pattern` | 118 | 117 | 99.2% | 8 / 8 |
| `curved.axis` | 906 | 884 | 97.6% | 39 / 39 |
| `curved.tangent_plane` | 942 | 930 | 98.7% | 44 / 44 |
| `edge.axis` | 2897 | 2846 | 98.2% | 140 / 140 |
| `edge.chamfer` | 3465 | 2863 | 82.6% | 122 / 123 |
| `edge.fillet` | 3465 | 2896 | 83.6% | 126 / 126 |
| `edge.normal_plane` | 4468 | 4413 | 98.8% | 190 / 192 |
| `edge.project` | 4468 | 4451 | 99.6% | 223 / 223 |
| `face.angle_plane` | 1004 | 980 | 97.6% | 51 / 51 |
| `face.draft_neutral` | 895 | 246 | 27.5% | 8 / 8 |
| `face.extrude` | 1123 | 465 | 41.4% | 25 / 26 |
| `face.hole` | 1123 | 726 | 64.6% | 40 / 43 |
| `face.midplane` | 686 | 674 | 98.3% | 25 / 26 |
| `face.offset_plane` | 1123 | 586 | 52.2% | 26 / 26 |
| `face.press_pull` | 1123 | 465 | 41.4% | 25 / 26 |
| `face.shell` | 1123 | 953 | 84.9% | 43 / 45 |
| `face.sketch` | 1123 | 584 | 52.0% | 27 / 27 |
| `vertex.plane_three_points` | 2048 | 2012 | 98.2% | 80 / 85 |
| `vertex.point` | 3122 | 3122 | 100.0% | 118 / 118 |
### By design
| Design | Actions | Pass | Rate |
|---|---:|---:|---:|
| corpus/components-arm | 593 | 354 | 59.7% |
| corpus/issue-489-stacked-join | 243 | 227 | 93.4% |
| corpus/joints-hinge | 344 | 258 | 75.0% |
| corpus/m0-box-fillet-hole | 170 | 158 | 92.9% |
| corpus/m0-revolve-primitives | 344 | 300 | 87.2% |
| corpus/modern-path-pattern | 299 | 294 | 98.3% |
| corpus/modern-plastic | 830 | 634 | 76.4% |
| corpus/modern-sheet-metal | 394 | 273 | 69.3% |
| corpus/move-bodies | 262 | 262 | 100.0% |
| corpus/naming-split-pieces | 285 | 224 | 78.6% |
| corpus/split-body-pieces | 304 | 280 | 92.1% |
| demo/enclosure_steps | 1070 | 991 | 92.6% |
| demo/pi-case | 1780 | 1500 | 84.3% |
| examples/bracket | 202 | 191 | 94.6% |
| examples/sensor_enclosure | 2020 | 1852 | 91.7% |
| oracle/01-box | 131 | 131 | 100.0% |
| oracle/02-cylinder | 41 | 41 | 100.0% |
| oracle/03-box-through-hole | 155 | 151 | 97.4% |
| oracle/04-revolve-vase | 101 | 79 | 78.2% |
| oracle/05-taper-extrude | 95 | 87 | 91.6% |
| oracle/06-symmetric-extrude | 131 | 131 | 100.0% |
| oracle/07-fillet-all-edges | 249 | 249 | 100.0% |
| oracle/08-chamfer | 206 | 188 | 91.3% |
| oracle/09-shell | 274 | 265 | 96.7% |
| oracle/10-rect-pattern-holes | 419 | 415 | 99.0% |
| oracle/11-circular-pattern-flange | 209 | 201 | 96.2% |
| oracle/12-mirror-feature | 193 | 181 | 93.8% |
| oracle/13-combine-join | 231 | 202 | 87.4% |
| oracle/14-combine-cut | 160 | 146 | 91.2% |
| oracle/15-combine-intersect | 108 | 78 | 72.2% |
| oracle/16-sweep | 64 | 56 | 87.5% |
| oracle/17-loft | 90 | 64 | 71.1% |
| oracle/18-hole-features | 286 | 262 | 91.6% |
| oracle/19-l-bracket-constrained | 187 | 187 | 100.0% |
| oracle/20-slot-constrained | 106 | 100 | 94.3% |
| oracle/21-draft | 123 | 99 | 80.5% |
| oracle/22-split-body | 80 | 56 | 70.0% |
| oracle/23-bracket-multifeature | 450 | 381 | 84.7% |
| oracle/24-user-parameters | 155 | 151 | 97.4% |
| oracle/25-sketch-triangle | 102 | 100 | 98.0% |
| oracle/26-sketch-belt | 129 | 125 | 96.9% |
| oracle/27-sketch-trapezoid | 178 | 173 | 97.2% |
| oracle/28-blind-pocket-fillets | 361 | 330 | 91.4% |
| oracle/29-two-sided-and-partial-revolve | 314 | 256 | 81.5% |
| oracle/30-fillet-after-shell | 421 | 406 | 96.4% |
| oracle/31-fillet-l-all-edges | 375 | 371 | 98.9% |
| oracle/32-boss-plate-fillets | 174 | 167 | 96.0% |
| oracle/33-fillet-tangent-chain | 184 | 166 | 90.2% |
| oracle/34-chamfer-then-fillet | 312 | 255 | 81.7% |
| oracle/35-fillet-curved-curved | 73 | 70 | 95.9% |
| oracle/36-corner-blend-two-radii | 242 | 236 | 97.5% |
| oracle/37-pyramid-apex-blend | 169 | 144 | 85.2% |
| oracle/38-coincident-cylinders | 41 | 41 | 100.0% |
| oracle/39-tee-pipe | 119 | 118 | 99.2% |
| oracle/40-box-sphere-torus | 226 | 192 | 85.0% |
| oracle/41-tube-radial-holes | 497 | 495 | 99.6% |
| oracle/42-overlapping-patterns | 1094 | 926 | 84.6% |
| oracle/43-cut-splits-body | 262 | 254 | 96.9% |
| oracle/44-shell-l-bracket | 396 | 390 | 98.5% |
| oracle/45-shell-boss-plate | 335 | 322 | 96.1% |
| oracle/46-shell-two-open-faces | 278 | 268 | 96.4% |
| oracle/47-draft-curved-pocket | 221 | 179 | 81.0% |
| oracle/48-pulley | 298 | 274 | 91.9% |
| oracle/49-helical-sweep | 36 | 18 | 50.0% |
| oracle/50-sweep-tight-bend | 77 | 61 | 79.2% |
| oracle/51-loft-rect-circle-offset | 90 | 64 | 71.1% |
| oracle/52-circular-pattern-fillet | 267 | 226 | 84.6% |
| oracle/53-path-pattern | 299 | 294 | 98.3% |
| oracle/54-mirror-body-fillets | 239 | 209 | 87.4% |
| oracle/55-tapped-hole-cosmetic | 179 | 167 | 93.3% |
| oracle/56-holes-on-curved-face | 143 | 123 | 86.0% |
| oracle/57-hole-pattern-sketch-points | 485 | 456 | 94.0% |
| oracle/58-phone-stand | 635 | 552 | 86.9% |
| oracle/59-enclosure-base | 1029 | 942 | 91.5% |
| oracle/60-pipe-flange | 378 | 356 | 94.2% |
| oracle/61-shaft-keyway | 310 | 231 | 74.5% |
| oracle/62-heat-sink | 1781 | 1524 | 85.6% |
| oracle/63-hinge-knuckle | 432 | 353 | 81.7% |
| oracle/64-sketch-tangent-arc-chain | 173 | 142 | 82.1% |
| oracle/65-sketch-gear-tooth | 170 | 144 | 84.7% |
| oracle/66-sketch-linkage | 201 | 195 | 97.0% |
| oracle/67-sm-base-flange | 131 | 105 | 80.2% |
| oracle/68-sm-edge-flange | 263 | 187 | 71.1% |
| oracle/69-sm-tray | 919 | 599 | 65.2% |
| oracle/70-sm-flange-45 | 261 | 185 | 70.9% |
| oracle/71-sm-custom-rule | 394 | 278 | 70.6% |
| oracle/72-sm-contour-flange | 394 | 273 | 69.3% |
| oracle/73-sm-hem | 395 | 272 | 68.9% |
| oracle/74-sm-unfold-refold | 619 | 439 | 70.9% |
| oracle/75-sm-bracket-holes | 479 | 323 | 67.4% |
| oracle/76-sm-thickness-variant | 263 | 187 | 71.1% |
| regressions/join_struts_on_bore_wall | 544 | 424 | 77.9% |
| regressions/loft_cut_beside_round_hole | 386 | 305 | 79.0% |
| regressions/oblique_cut_of_cylinder | 306 | 269 | 87.9% |
| regressions/oblique_cut_of_cylinder_hang | 306 | 270 | 88.2% |
| regressions/shallow_loft_cut_from_top_face | 307 | 264 | 86.0% |
| typical/filleted-shelled-box | 402 | 386 | 96.0% |
| typical/holes-pattern-chamfer | 536 | 480 | 89.6% |
| typical/loft | 218 | 180 | 82.6% |
| typical/multi-body-join-cut | 309 | 284 | 91.9% |
| typical/revolved-part | 161 | 153 | 95.0% |
| typical/sheet-metal-bracket | 311 | 230 | 74.0% |
| typical/stacked-extrude | 283 | 275 | 97.2% |
| typical/sweep | 280 | 185 | 66.1% |
## Causes
Every failure, grouped by cause (5013 in all). Faces that can't take an attached sketch fail four
actions each (`face.sketch`, `face.offset_plane`, and `face.press_pull` and `face.extrude`, which
make a legacy face sketch there).
| Failures | Issue | Cause | Actions | Designs | Example |
|---:|---|---|---|---:|---|
| 986 | #556 | Fillet or chamfer of one edge whose ends meet curved, slanted or several faces | `edge.chamfer` 496, `edge.fillet` 490 | 46 | corpus/components-arm `ArmBody (Arm:2)/F4:+x\|F4:+y` |
| 684 | #495 | Sheet metal thickness (rim) faces have no attachment | `face.extrude` 202, `face.press_pull` 202, `face.offset_plane` 140, `face.sketch` 140 | 12 | corpus/modern-sheet-metal `Body1/F2:face:0` |
| 649 | #390 | Draft refuses walls whose neighbours aren't square to the pull (rounds, other walls), or corners that change after the draft | `face.draft_neutral` 649 | 65 | corpus/components-arm `ArmBody (Arm:2)/F4:+x` |
| 384 | #494 | Revolve, Offset Face, Boss and the other plastic features drop the body's face sources | `face.offset_plane` 100, `face.sketch` 100, `face.extrude` 92, `face.press_pull` 92 | 11 | corpus/m0-revolve-primitives `Body1/F2:end` |
| 380 | #493 | Cut walls, hole faces and chamfer faces have no source | `face.extrude` 95, `face.offset_plane` 95, `face.press_pull` 95, `face.sketch` 95 | 17 | corpus/split-body-pieces `Body1 (1)/F4:side:l1` |
| 336 | #557 | Pattern and mirror copies share or lose their faces' sources | `face.extrude` 84, `face.offset_plane` 84, `face.press_pull` 84, `face.sketch` 84 | 6 | demo/pi-case `Body1/F6:top` |
| 285 | #558 | Picks on a moved or pasted component miss (construction geometry, Hole, Press Pull, Project) | `edge.normal_plane` 55, `edge.axis` 51, `vertex.plane_three_points` 36, `face.extrude` 34 | 2 | corpus/components-arm `ArmBody (Arm:2)/F4:+x` |
| 226 | #559 | A through-all Hole along a wall or beside curved faces: the cut fails | `face.hole` 226 | 46 | corpus/m0-revolve-primitives `Body1/F2:end` |
| 155 | #560 | Hole on a sheet metal face is refused | `face.hole` 155 | 12 | corpus/modern-sheet-metal `Body1/F2:face:0` |
| 146 | #561 | Loft, Sweep and Coil faces have no source | `face.offset_plane` 38, `face.sketch` 38, `face.extrude` 35, `face.press_pull` 35 | 10 | oracle/16-sweep `Tube/F3:end` |
| 116 | #431 | Fillet or chamfer along a curved edge: the ball doesn't fit along the edge | `edge.chamfer` 68, `edge.fillet` 48 | 16 | corpus/modern-sheet-metal `Body1/F2:face:14\|F2:face:16` |
| 100 | #492 | A face split into pieces by a later feature | `face.extrude` 25, `face.offset_plane` 25, `face.press_pull` 25, `face.sketch` 25 | 5 | corpus/issue-489-stacked-join `Body1/F2:top#[F2:side:l1,F4:side:l5]` |
| 98 | #491 | A face that a Join merged from two features' faces | `face.sketch` 27, `face.offset_plane` 25, `face.extrude` 23, `face.press_pull` 23 | 12 | corpus/issue-489-stacked-join `Body1/F2:side:l2` |
| 86 | | Shell's other refusals: a removed face meeting a round as large as the wall, freeform (loft) faces, walls thinner than twice the shell (expected, not filed) | `face.shell` 86 | 22 | corpus/modern-plastic `Body1/F3:face:15` |
| 84 | #566 | Shell of swept and sheet metal bodies: a corner whose faces don't meet the same way | `face.shell` 84 | 12 | oracle/30-fillet-after-shell `Box/F3:inner:F2:start` |
| 80 | #562 | Press Pull beside rounds and curved neighbours: the join fails | `face.extrude` 40, `face.press_pull` 40 | 13 | demo/enclosure_steps `Enclosure/F2:start` |
| 69 | | Blend larger than the faces around it (the 10% radius doesn't fit; expected, not filed) | `edge.chamfer` 38, `edge.fillet` 31 | 11 | corpus/modern-plastic `Body1/F2:inner:F1:+x\|F3:face:19` |
| 56 | #563 | Draft and tapered Extrude drop the faces' sources | `face.extrude` 14, `face.offset_plane` 14, `face.press_pull` 14, `face.sketch` 14 | 3 | oracle/05-taper-extrude `Frustum/F2:start` |
| 48 | #564 | The rim a Shell leaves has no source | `face.extrude` 12, `face.offset_plane` 12, `face.press_pull` 12, `face.sketch` 12 | 8 | demo/pi-case `Body1/F2:top` |
| 18 | #567 | Axis or tangent plane on a drill tip: no face at NaN | `curved.axis` 9, `curved.tangent_plane` 9 | 3 | oracle/18-hole-features `Plate/F5:face:15` |
| 9 | #568 | Construction geometry on model edges, faces and vertices is lost when a parameter moves it | `vertex.plane_three_points+edit` 5, `edge.normal_plane+edit` 2, `face.midplane+edit` 1, `face.shell+edit` 1 | 4 | demo/enclosure_steps `Enclosure/v[F2:top & F4:inner:F2:side:l4 & F4:inner:F3:blend:F2:side:l` |
| 9 | | Axis through a face the kernel sees as a cylinder that the command doesn't (sensor enclosure slot ends; not filed yet) | `curved.axis` 9 | 1 | examples/sensor_enclosure `Lid/F15:blend:F14:side:l2\|F14:side:l3` |
| 7 | | Other failures in the edit round: the hole cut and blend sizes above, and an extrude from the M0 box's top face whose profile isn't found again (not filed yet) | `face.hole+edit` 3, `face.extrude+edit` 1, `face.press_pull+edit` 1, `edge.chamfer+edit` 1 | 4 | corpus/m0-box-fillet-hole `Body1/F2:top` |
| 2 | | A helix edge projected along its axis is a point (expected) | `edge.project` 2 | 1 | oracle/50-sweep-tight-bend `Bend/F3:side:c1#1\|F3:side:c1#2~2` |
Where the face history stands (`crates/kernel/src/history.rs`, [face attachments](face-attachments.md)):
a flat face takes an attached sketch when the body carries an exact source certificate for it
(extrude caps and sides, boxes, shells' inner walls, STEP faces, sheet metal panels), or else
when its persistent name follows exact history (a `lineage` certificate): cut walls and floors,
chamfers, pattern and mirror copies, sides a Join merged, revolves, lofts, sweeps, drafts, tapers,
shell rims, the plastic features' faces and the faces around them, and pieces of a split face (by
the piece's own name where Offset Face moved it off the others' plane). Sheet metal panels, their
rims and the flat ends of bends attach by name. What still refuses a sketch is what makes faces
without recording their history (some cuts, the face a Split Body cuts with another body's
face): press-pull and extrude-from-face then fall back to a legacy face sketch found again by a
point.