Skip to content

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: Delete or Backspace while at least one entity is selected (Canvas/index.tsx:2883-2886); both keys are wired identically and the handler is gated on selection.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 through deleteSelected).

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):

  1. Action / Press Del or Backspace: with selection non-empty. The action bumps ctx.latestSolveId so any in-flight solver callback against the pre-delete scene is dropped, cancels any armed variableTimer, then iterates selection and calls the right primitive-specific delete per item.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.
  2. Finalize (auto on dispatch): all per-item mutations run inside one withHistory envelope, 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 Fillet where sourceCornerId, tangentNodeIds[0/1], or parents[*].farEndId referenced the node → routed through deleteFillet for the cleanup (commands.ts:1882-1884).
    • Any Crossing whose virtualNodeId is this node → routed through deleteCrossing (commands.ts:1885-1887).
    • If the node is an interior spline CP (splineCpRef.splineId set), the entire parent Spline and all its CP nodes are dropped .
    • Every Segment / Arc / Spline using the node as n0/n1 is removed.
    • Every Constraint with any entityA-D equal to the deleted id (or any cascaded CP id) is pruned.
    • freezeOrphanedVirtuals runs to drop stale virtualRef flags on surviving tangent nodes whose owning fillet/crossing was just cascaded.
  • segment: if the segment belongs to a Rect or Polygon feature, the whole feature is deleted (deleteRect / deletePolygon); otherwise deleteSegment runs 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: deleteArc plus 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: deleteSpline removes 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 by deletedRectIds / deletedPolygonIds so multi-selecting several segments of one feature only deletes it once .
  • label: deleteLabel removes from geometry.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 entityCascade target
nodedependent fillets, crossings, parent spline (if CP), all incident segments/arcs/splines, all referencing constraints
segment in a rect/polygonthe entire parent feature (all 4 segments + auto constraints + feature record)
segment (plain)constraints whose entity set ⊆ {n0, n1}; orphan endpoint nodes
arcconstraints referencing the arc id; orphan endpoint nodes
splineowned CP nodes (deduped against selection)
fillet / crossingfeature-level cleanup via the matching deleter
virtualRef nodethe 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 at store/geometry.ts:603 is 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 — deleteSegment etc. apply a filter((s) => s.id !== id) which is a no-op when the id is absent. deleteSelected's inner segment branch also re-fetches next.geometry.segments.find(...) and continues when already gone (handles the multi-select-with-cascade case at store/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 it

ASCII: 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).

orig cornerselected (Del)fillet F

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.

corner restored(segment + fillet F deleted)

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 at commands.ts:1857 / :1928 / :1980.
  • Constraint.referencesEntity(id: string): boolean — default implementation on the abstract base walks entityIds and matches; subclasses don't need to override (none currently do). The constraint-prune step becomes scene.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.

motordevs studio — geometry editor specification