Appearance
Rectangle
1. Identity
A rectangle is a feature record (RectFeature, scene-model.ts:496-509) that owns 4 axis-aligned segments, 4 corner nodes, and 3 auto-emitted constraints — the four sides are not loose primitives but are bound by a single feature id with cascade-delete semantics. The feature-record pattern is shared with Polygon.
2. When to use it
- Sketching a stator/rotor bounding box or a rectangular slot opening in one gesture.
- Anchoring downstream geometry (block labels, frames) to a known closed loop without manually constraining four segments.
- Replicating identical rectangular features via linear/mirror clone and keeping them recognisable as rectangles in the feature tree, not loose 4-segment fans.
3. Inputs
- Tool button: rectangle slot in the primitives toolbar.
- Keyboard shortcut:
R(Canvas/index.tsx:2760). - MotorScript builtin:
rect(p1, p2)— opposite-corner points (executor/builtins/primitives.ts:319, codegencodegen.ts:codegenRect). - Command Palette: not yet wired.
- Context menu: not yet wired.
4. State machine
- Click 1 — first corner: a snap-resolved world point
A. Modifier keys: none (no square-lock yet — not yet wired).- Preview after step 1: axis-aligned dashed rectangle from
Ato the live cursor; the four edges rubber-band as the cursor moves (Canvas/index.tsx:2268,2839). - Cancel:
Esc, right-click, or switching tool clears the draft (clearDrafton tool-change branch aroundCanvas/index.tsx:2451-2455).
- Preview after step 1: axis-aligned dashed rectangle from
- Click 2 — opposite corner: a snap-resolved world point
B. Tab-typed coords also accepted (Canvas/index.tsx:2268-2292).- Preview after step 2: none — the click commits.
- Cancel:
Escbetween click 1 and click 2 reverts to empty draft.
- Finalize: implicit on click 2 (or Tab-confirmed coordinate). Invokes
createRectangle(scene, A.x, A.y, B.x, B.y)(tools/rectangle-tool.ts:73).
A• (click 1) A•─────────────•
│ │
│ dashed │
│ preview │
╳ cursor │ ╳ cursor (rubber-bands)
•───────────────•
click 2 ↓
A•─────────────•TR
│ │
│ committed │
│ │
BL•────────────•B5. Committed state
After finalize, scene.geometry gains (all atomic, one undo step):
- 4 ×
Nodeat corners —TL = (minX, maxY),TR = (maxX, maxY),BR = (maxX, minY),BL = (minX, minY). Existing nodes at those positions are deduplicated byaddSegment(tools/rectangle-tool.ts:113-127). - 4 ×
Segmentin order [top, right, bottom, left] sharing the corner nodes. - 1 ×
RectFeaturewhosesegmentIds,nodeIds, andconstraintIdsindex the above (scene-model.ts:496-509). - 3 ×
Constraint:parallel(top, bottom)parallel(left, right)perpendicular(top, right)
This is the canonical Solvespace rectangle encoding: parallel pairs fix the shape, one perpendicular at a corner removes skew while leaving rigid rotation free. (The legacy JSDoc on RectFeature at scene-model.ts:492-494 mentioning "2 × horizontal + 2 × vertical" is stale — the implementation in both tools/rectangle-tool.ts:190-213 and executor/builtins/primitives.ts:340-347 emits 2 parallel + 1 perpendicular.)
MotorScript statement generated: rect(p1, p2) via codegenRect (codegen.ts:182-191); the executor's rect reconstructs the same RectFeature so script-roundtrip preserves feature identity.
6. Constraints / interactions
The RectFeature itself accepts no direct constraints. The constraint palette will not show rect-specific kinds; users apply constraints to the underlying segments or corner nodes:
| Constraint | Applied to | Behavior |
|---|---|---|
| horizontal / vertical | a side segment | locks that side's axis (often redundant with the auto-emitted parallel/perpendicular triad) |
| distance | two corner nodes, or a segment | dimensions a side or diagonal |
| coincident | a corner node | snaps the whole rectangle by one corner |
| fix | a corner node | grounds the rectangle |
Interactions:
- Delete:
deleteRect(scene, rectId)(tools/rectangle-tool.ts:247) removes the feature, its 4 segments, and its 3 constraints in one step. Corner nodes are left in place (they may be shared with other geometry). - Clone / array :
replicateRects(transforms.ts:871) re-emits aRectFeatureon the cloned side iff all 4 corner nodes are inside the expanded selection. Partial selection degrades cleanly to loose cloned segments — no feature record is created. - Mirror:
applyCloneMirrorcalls the samereplicateRectsafter node mirroring (transforms.ts:1236), so mirrored copies preserve the feature record under the same all-nodes-selected gate. - Fillet / trim / crossing: operate on the underlying segments. Filleting a corner breaks one segment into segment + arc — the
RectFeaturebecomes structurally invalid (the perpendicular at that corner may conflict); current behaviour leaves the feature record dangling. Treat as a known sharp edge until covered by an explicit lane.
7. Failure modes
- Zero-area (clicks coincide,
dx == 0 && dy == 0): Canvas pre-guard rejects the click;surfaceError('rectangle zero-extent click')is dispatched and no scene mutation runs (Canvas/index.tsx:1539-1545,2288-2294). - Single-axis-zero (
dx == 0xordy == 0, degenerate to a line): Canvas pre-guard rejects;surfaceError('rectangle zero-extent axis')(Canvas/index.tsx:1532-1536,2281-2287). The rectangle tool does not silently fall through to a line primitive. - NaN / Infinity coords (e.g. Tab-typed expression returns non-finite):
createRectangleearly-returns with empty ids and no mutation (tools/rectangle-tool.ts:82-93). - MotorScript
rect(p1,p2)with degenerate corners: throwsrect: degenerate rectangle (size=…)(executor/builtins/primitives.ts:329-331) — script run halts with a surfaced error. - Node lookup miss after addSegment dedup (defensive — should not occur in practice): early-return empty result (
tools/rectangle-tool.ts:161-170).
User-visible contract: degenerate input is a toast-level error and a no-op; the partial draft is cleared and the tool stays armed for another attempt.
8. Figures
Figure 1: After click 1 (first corner A), an axis-aligned dashed rectangle rubber-bands to the live cursor.
Figure 2: Committed RectFeature — 4 corner nodes, 4 segments, with constraint glyphs marking the two parallel pairs and one perpendicular auto-emitted at the corner.
9. Known bugs
None known. ## 10. Class API
In the current model, rectangles are class instances rather than plain objects. RectFeature is a thin owner — it holds id tuples only, while the actual Node / Segment / Constraint records live in the Scene-level Map indices.
- Class file:
app/src/lib/fea2d/model/RectFeature.ts - Constructor:
new RectFeature(id, segmentIds: [SegmentId, SegmentId, SegmentId, SegmentId], nodeIds: [NodeId, NodeId, NodeId, NodeId], constraintIds: [ConstraintId, ConstraintId, ConstraintId])— validates the four-segment / four-node / three-constraint tuple counts. - Snapshot type:
RectFeatureSnapshot(app/src/lib/fea2d/model/snapshots/RectFeature.ts). - Lifecycle methods:
toJSON: RectFeatureSnapshotstatic fromJSON(snap, _scene?): RectFeatureclone: RectFeatureequals(other: SceneEntity): boolean
- Polymorphic surface (
SceneFeature):referencesEntity(id: string): boolean— true whenidmatches the feature id itself or any of the owned segment / node / constraint ids. Drives single-step cascade-delete.isStale(_scene: unknown): boolean— returnsfalse(RectFeature has no parametric re-evaluation; mutations are delete + regenerate).
Public fields are readonly: RectFeature is a thin owner — every "mutation" is delete + regenerate .