Appearance
Delete
1. Identity
A delete is a destructive removal of one or more selected entities from scene.geometry, with mandatory cascade to dependent features (fillets, crossings, splines), constraint pruning, and orphan-node sweep — committed as a single undoable history entry.
2. When to use it
- Remove a mis-drawn segment, arc, circle, spline, or block label.
- Tear down a fillet or crossing feature without re-running the script.
- Drop a free-standing node that no longer participates in any geometry.
- Discard a whole rect / polygon atomic feature by selecting any of its segments (cascade deletes the parent feature record + all 4 segments + auto-emitted constraints).
3. Inputs
- Tool button: not yet wired — delete has no dedicated toolbar slot; it is keyboard- and context-menu-driven.
- Keyboard shortcut:
DeleteorBackspacewhile at least one entity is selected (Canvas/index.tsx:2883-2886); both keys are wired identically and the handler is gated onselection.length > 0. - MotorScript builtin: not yet wired — delete is a canvas-only operation; reverse-codegen does not emit a
delete(...)statement. - Command Palette: "Edit: Delete selection".
- Context menu: right-click on an entity → "Delete" (per-entity context menu items dispatch the matching primitive-specific store action —
deleteSegment,deleteArc,deleteFillet, etc. — rather than going throughdeleteSelected).
4. State machine
Delete is a single-step operation. Two entry flows converge on the same cascade.
Selection-based flow (Del / Backspace, deleteSelected in store/geometry.ts:601-815):
Action / Press Del or Backspace: with
selectionnon-empty. The action bumpsctx.latestSolveIdso any in-flight solver callback against the pre-delete scene is dropped, cancels any armedvariableTimer, then iteratesselectionand calls the right primitive-specific delete peritem.kind.- Preview: none — delete is instantaneous on key release.
- Cancel options: Undo (
Cmd/Ctrl+Z) after the fact; there is no mid-action abort because there is no intermediate step.
Finalize (auto on dispatch): all per-item mutations run inside one
withHistoryenvelope, so the entire cascade — including constraint pruning and orphan-node sweep — is a single undoable entry. Selection is cleared by the store as part of the history commit.
Single-entity flow (right-click → Delete): the context-menu item calls the matching primitive deleter directly (e.g. deleteSegment(scene, id) in commands.ts:1897); same cascade rules, same single history entry.
5. Committed state
After finalize, the scene mutation per item.kind (see store/geometry.ts:633-812):
- node:
deleteNode(scene, id)(commands.ts:1795-1895). Cascades:- Any
FilletwheresourceCornerId,tangentNodeIds[0/1], orparents[*].farEndIdreferenced the node → routed throughdeleteFilletfor the cleanup (commands.ts:1882-1884). - Any
CrossingwhosevirtualNodeIdis this node → routed throughdeleteCrossing(commands.ts:1885-1887). - If the node is an interior spline CP (
splineCpRef.splineIdset), the entire parentSplineand all its CP nodes are dropped . - Every
Segment/Arc/Splineusing the node asn0/n1is removed. - Every
Constraintwith anyentityA-Dequal to the deleted id (or any cascaded CP id) is pruned. freezeOrphanedVirtualsruns to drop stalevirtualRefflags on surviving tangent nodes whose owning fillet/crossing was just cascaded.
- Any
- segment: if the segment belongs to a
RectorPolygonfeature, the whole feature is deleted (deleteRect/deletePolygon); otherwisedeleteSegmentruns and the action prunes constraints whose entity set is entirely contained in the deleted segment's endpoints, then sweeps orphan endpoints (store/geometry.ts:683-743). - arc:
deleteArcplus constraint cascade for any constraint referencing the arc id (entityA-D) plus orphan-endpoint sweep (store/geometry.ts:744-787). - circle: filtered out of
geometry.circles. No cascade — circles carry no node references. - spline:
deleteSplineremoves the spline and its owned CP nodes; the loop records the dropped CP ids so any CP node that was also independently selected is not double-deleted . - fillet / crossing: routed through
deleteFillet/deleteCrossing, which restore the source-corner geometry where applicable and freeze any orphaned virtuals. - rect / polygon: feature-level delete via
deleteRect/deletePolygon; cascade-dedup tracked bydeletedRectIds/deletedPolygonIdsso multi-selecting several segments of one feature only deletes it once . - label:
deleteLabelremoves fromgeometry.blockLabels.
No MotorScript statement is emitted; delete is canvas-only and the next runScript will re-materialise anything the script still declares (so deleting script-generated geometry without editing the script is effectively a one-frame removal — the user must edit the script for a permanent delete of declared entities).
6. Constraints / interactions
| Selected entity | Cascade target |
|---|---|
| node | dependent fillets, crossings, parent spline (if CP), all incident segments/arcs/splines, all referencing constraints |
| segment in a rect/polygon | the entire parent feature (all 4 segments + auto constraints + feature record) |
| segment (plain) | constraints whose entity set ⊆ {n0, n1}; orphan endpoint nodes |
| arc | constraints referencing the arc id; orphan endpoint nodes |
| spline | owned CP nodes (deduped against selection) |
| fillet / crossing | feature-level cleanup via the matching deleter |
| virtualRef node | the parent feature (see below) |
virtualRef nodes: when the user selects a node whose virtualRef points at a fillet (kind fillet-tangent) or crossing (kind crossing-point), deleteNode's cascade walks geometry.fillets / geometry.crossings and removes the parent feature through deleteFillet / deleteCrossing. The invariant "you can't have a fillet without its tangent node" is preserved by routing through these deleters so the cleanup runs once per parent. See Crossing and Fillet for the symmetric "parent-side" perspective.
Construction layer: the isConstruction flag does NOT gate delete. Construction segments / nodes delete with the same cascade as real geometry (it is the solver and trim tool that skip construction, not the deleter). solver-skip is unrelated — locked construction targets are still user-deletable; delete is an explicit user action and bypasses solver lock semantics.
7. Failure modes
- Empty selection (Del / Backspace pressed with
selection.length === 0): the keydown handler early-returns (Canvas/index.tsx:2884);deleteSelected's own guard atstore/geometry.ts:603is a belt-and-suspenders check. No-op, no history entry, no toast. - Selection references a stale id (e.g. solver re-id race): the per-kind primitive deleters tolerate missing ids —
deleteSegmentetc. apply afilter((s) => s.id !== id)which is a no-op when the id is absent.deleteSelected's inner segment branch also re-fetchesnext.geometry.segments.find(...)andcontinues when already gone (handles the multi-select-with-cascade case atstore/geometry.ts:683-687). - Locked construction primitive: deletable (delete is user-explicit; the solver-skip flag does not propagate into the deleter).
- Cascade leaves an orphan node that is still referenced by a constraint: the orphan-sweep guard checks all four constraint entity slots before deleting the node (
store/geometry.ts:735-742), so the node survives and the constraint keeps a valid endpoint reference. - Selecting a node that is the last anchor of a constraint with N=2 entities but only one in the selection: the OTHER entity is preserved; the constraint is pruned because at least one of its referenced ids was the deleted node. Solver re-runs without the dropped constraint on the next tick.
Contract: delete is all-or-nothing per withHistory envelope. A throw in any deleter rolls back to the pre-delete scene (the history helper's transactional semantics); solverStatus is invalidated by the latestSolveId bump regardless.
8. Figures
before after Del on segment AB
╲ ╲
╲ fillet F (radius r) ╲ (fillet F is gone —
╲ ╲ cascade through
A•────X────•B A sourceCornerId)
╱ ╱ constraints on AB
╱ ◜ corner C is fillet ╱ are pruned;
╱ sourceCornerId ╱ orphan node B is
swept if no other
segment used itASCII: Cascade-through-fillet. Selecting segment AB and pressing Del removes AB; the fillet F whose parents listed AB-or-CD as a construction parent is cascade-deleted via deleteFillet, freeing its tangent nodes (or removing them if they become orphans).
Figure 2 (before): scene with a fillet F replacing a sharp corner (the ghosted dashed corner shows the original parent intersection), plus a selected segment slated for deletion.
Figure 3 (after): cascade through dependent features — deleting the segment removes the fillet F too; the original parent corner is restored at the parents' true intersection.
9. Known bugs
None known.
10. Class API
In the current model, the cascade-delete walker becomes a single polymorphic call instead of a scatter of kind-conditional forEach blocks.
Scene.removeNode / removeSegment / removeArc / removeCircle / ...(app/src/lib/fea2d/model/Scene.ts) — primary delete entry points; each removes the indexed instance and bumps the revision counter.SceneFeature.referencesEntity(id: string): boolean— implemented by every concrete feature class (FilletFeature,CrossingFeature,RectFeature,PolygonFeature). The cascade walker is one line:scene.features.filter(f => f.referencesEntity(deletedId))replaces the previous scatter atcommands.ts:1857 / :1928 / :1980.Constraint.referencesEntity(id: string): boolean— default implementation on the abstract base walksentityIdsand matches; subclasses don't need to override (none currently do). The constraint-prune step becomesscene.constraints.filter(c => c.referencesEntity(deletedId)).
The a future refactor pass dissolution of commands.ts will route every delete path through these polymorphic predicates; today (post-phase-7) the cascade walker still has scaffolding in commands.ts but the per-feature / per-constraint discrimination is already class-driven.