Skip to content

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, codegen codegen.ts:codegenRect).
  • Command Palette: not yet wired.
  • Context menu: not yet wired.

4. State machine

  1. 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 A to 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 (clearDraft on tool-change branch around Canvas/index.tsx:2451-2455).
  2. 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: Esc between click 1 and click 2 reverts to empty draft.
  3. 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•────────────•B

5. Committed state

After finalize, scene.geometry gains (all atomic, one undo step):

  • 4 × Node at corners — TL = (minX, maxY), TR = (maxX, maxY), BR = (maxX, minY), BL = (minX, minY). Existing nodes at those positions are deduplicated by addSegment (tools/rectangle-tool.ts:113-127).
  • 4 × Segment in order [top, right, bottom, left] sharing the corner nodes.
  • 1 × RectFeature whose segmentIds, nodeIds, and constraintIds index 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:

ConstraintApplied toBehavior
horizontal / verticala side segmentlocks that side's axis (often redundant with the auto-emitted parallel/perpendicular triad)
distancetwo corner nodes, or a segmentdimensions a side or diagonal
coincidenta corner nodesnaps the whole rectangle by one corner
fixa corner nodegrounds 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 a RectFeature on 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: applyCloneMirror calls the same replicateRects after 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 RectFeature becomes 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 == 0 xor dy == 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): createRectangle early-returns with empty ids and no mutation (tools/rectangle-tool.ts:82-93).
  • MotorScript rect(p1,p2) with degenerate corners: throws rect: 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

A (click 1)cursoraxis-aligned preview

Figure 1: After click 1 (first corner A), an axis-aligned dashed rectangle rubber-bands to the live cursor.

TLTRBRBLRectFeature: 4 segments + 4 nodes + parallel(top,bot) + parallel(L,R) + ⊥(TL)

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: RectFeatureSnapshot
    • static fromJSON(snap, _scene?): RectFeature
    • clone: RectFeature
    • equals(other: SceneEntity): boolean
  • Polymorphic surface (SceneFeature):
    • referencesEntity(id: string): boolean — true when id matches the feature id itself or any of the owned segment / node / constraint ids. Drives single-step cascade-delete.
    • isStale(_scene: unknown): boolean — returns false (RectFeature has no parametric re-evaluation; mutations are delete + regenerate).

Public fields are readonly: RectFeature is a thin owner — every "mutation" is delete + regenerate .

motordevs studio — geometry editor specification