Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
caf4bce
feat: add GeometryRenderer.getRenderShapes for centralized GeometryCo…
1egoman Jul 18, 2026
f514879
feat: add filters parameter to GeometryComponent.getRenderShapes
1egoman Jul 18, 2026
db09a55
docs: update docs to take into account new GeometryComponent
1egoman Jul 18, 2026
b0985ea
feat: add initial getRenderShapes Mirror filter implementation
1egoman Jul 19, 2026
24af43b
fix: run npm run format
1egoman Jul 19, 2026
4a95ec2
feat: add fillet / chamfer utilities
1egoman Jul 19, 2026
909c518
feat: port BaseCornerGeometryReplacerTool to use new CornerReplacemen…
1egoman Jul 19, 2026
848c4d1
refactor: move out mod into math
1egoman Jul 19, 2026
126f68e
feat: move BaseCornerGeometryReplacerTool fillet / chamfer logic into…
1egoman Jul 19, 2026
930d2ce
feat: update tests to exercise fillet tool and ApplyFilterToGeometryA…
1egoman Jul 19, 2026
2d69f27
feat: get ApplyFilterToGeometryAction to work for multiple filters
1egoman Jul 19, 2026
8fe72f2
feat: get initial version of chamfer / fillet filter working within g…
1egoman Jul 19, 2026
cfae9d7
refactor: break up fillet tool / ApplyFilterToGeometryAction tests
1egoman Jul 20, 2026
0c218e3
feat: block adding chamfer / fillet to rectangle corners which alread…
1egoman Jul 20, 2026
ba33100
feat: get GeometryComponent.getRenderShapes working for fillet and ch…
1egoman Jul 20, 2026
6cb7ed2
feat: add initial geometry component get render shapes test
1egoman Jul 20, 2026
b7915d2
fix: make FilletFilterIndicator use fillet generation utilities rathe…
1egoman Jul 20, 2026
b3124b1
fix: run npm run format
1egoman Jul 20, 2026
0e9cf35
feat: add more geometry component getRenderShapes tests
1egoman Jul 20, 2026
7cdbc0c
fix: address type error in ApplyFilterToGeometryAction
1egoman Jul 21, 2026
85ea017
feat: add sort to filter rendering
1egoman Jul 21, 2026
3c1c817
feat: add mirror filter test
1egoman Jul 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/constraint-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ The constraint system has two layers: user-facing constraints (persisted, visibl

## User-Facing Constraints

Currently there are two user-facing types: `LinearConstraint` (target distance between two endpoints) and `PerpendicularConstraint` (right angle between three points — center, A, and B, where center→A ⟂ center→B).
Currently there are two user-facing types: `LinearConstraint` (target distance between two endpoints) and `PerpendicularConstraint` (right angle between three points -- center, A, and B, where center->A is perpendicular to center->B).

**Endpoints** (`ConstraintEndpoint`) can be:

Expand Down Expand Up @@ -52,7 +52,7 @@ Triggered by `ReconstrainAction` (key R) or programmatically:
- Fixed point pins for vertices that should not move
2. `gradientDescent()` runs the solver
3. `DCELShapeIndex.computeShapesForVertexId()` maps solved vertex positions back to shape-level updates (polygon point indices, rectangle corners, ellipse key points)
4. Each shape is updated via `updatePolygon()`/`updateRectangle()`/`updateEllipse()`
4. Each shape is updated via `GeometryComponent.update` with the new geometry data
5. DCEL is immediatly resynced for all touched shapes
6. Entire solve is wrapped in a single history transaction

Expand Down
50 changes: 35 additions & 15 deletions docs/geometry-store.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,30 @@

## What It Owns

- **All geometries**: stored in `geometryById: Map<Id, Geometry>`. Use `listWithComponent`/`listWithComponents` to query by component type.
- **All geometries**: stored in `geometryById: Map<Id, Geometry>`. Use `listWithComponent`/`listWithComponents` to query by component.
- **Working (transient) shapes**: `workingPolygon`, `workingRectangle`, `workingEllipse`, `workingConstraints` -- these represent shapes being actively drawn, NOT persisted
- **DCEL index**: `dcelIndex: DCELShapeIndex` for planar subdivision queries

All shapes use ID prefixes: `ply_` (polygon), `rct_` (rectangle), `elp_` (ellipse), `cns_` (constraint). IDs are generated by `HistoryManager.generateStableId()` which uses UUIDs plus a monotonic counter to prevent collisions across load/save cycles.

## Unified Component System

All geometry data is stored via a single `GeometryComponent<D>` (`src/lib/entity/components/GeometryComponent.ts`). Each geometry entity has exactly one geometry component whose data is a discriminated union:

```ts
type GeometryData = PolygonData | RectangleData | EllipseData;
```

Each `*Data` type has a `type` discriminator:

- `PolygonData` — `{ type: 'polygon', points, closed, openAtIndex, lastFillColor? }`
- `RectangleData` — `{ type: 'rectangle', upperLeft, lowerRight }`
- `EllipseData` — `{ type: 'ellipse', center, radiusX, radiusY }`

Read geometry data with `GeometryComponent.get(entity)`, which returns a `GeometryData`. Use `switch (data.type)` to discriminate. Update with `GeometryComponent.update(entity, partial)`.

The per-shape component types (`PolygonComponent`, `RectangleComponent`, `EllipseComponent`) are deprecated and used only internally by the old component definition files.

## CRUD Methods

All operations are unified by component detection rather than shape-specific methods:
Expand All @@ -18,7 +36,7 @@ All operations are unified by component detection rather than shape-specific met
| --------------------------- | ------------------------- | -------------------------------------------------- |
| Insert | `add(idPrefix, template)` | `addDirect(geometry)` |
| Update | `updateById(id, fn)` | `updateByIdDirect(id, fn)` |
| Update (narrowed component) | — | `updateByIdWithComponentDirect(id, component, fn)` |
| Update (narrowed component) | -- | `updateByIdWithComponentDirect(id, component, fn)` |
| Delete | `delete(id)` | `deleteDirect(id)` |

The history-recording methods call Direct internally, then record the operation to HistoryManager. HistoryManager uses only Direct methods during undo/redo replay to avoid infinite recording loops.
Expand All @@ -27,13 +45,15 @@ Direct methods emit `geometryAdded`/`geometryUpdated`/`geometryDeleted` events s

## Query Methods

| Method | Returns |
| ----------------------------------------- | ----------------------------------------------------------------------------- |
| `listWithComponent(component)` | `Array<Geometry<C>>` — all geometries with that component |
| `listWithComponents(a, b, c?, d?)` | `Array<Geometry<A & B & (C & D)?>>` — all geometries with multiple components |
| `getById(id)` | `Geometry \| Polygon \| null` |
| `getByIdWithComponent(id, component)` | `Geometry<C> \| null` — narrowed by component |
| `getByIdWithComponents(id, a, b, c?, d?)` | `Geometry<A & B & ...> \| null` — narrowed by multiple components |
| Method | Returns |
| ----------------------------------------- | --------------------------------------------------------------------------- |
| `listWithComponent(component)` | `Array<Entity<C>>` — all geometries with that component |
| `listWithComponents(a, b, c?, d?)` | `Array<Entity<A & B & (C & D)?>>` — all geometries with multiple components |
| `getById(id)` | `Entity \| null` |
| `getByIdWithComponent(id, component)` | `Entity<C> \| null` — narrowed by component |
| `getByIdWithComponents(id, a, b, c?, d?)` | `Entity<A & B & ...> \| null` — narrowed by multiple components |

To get all geometry entities: `store.listWithComponent(GeometryComponent)` returns `Array<Geometry>`.

## DCEL Sync

Expand All @@ -47,18 +67,18 @@ Emitted on `EventEmitter`:

| Event | Payload |
| ------------------- | --------------------- |
| `geometryAdded` | `geometry: Geometry` |
| `geometryUpdated` | `geometry: Geometry` |
| `geometryAdded` | `geometry: Entity` |
| `geometryUpdated` | `geometry: Entity` |
| `geometryDeleted` | `geometryId: Id` |
| `workingXxxChanged` | working shape or null |

Legacy shape-specific events (`polygonsChanged`, `rectanglesChanged`, `ellipsesChanged`, etc.) have been removed. Subscribe to `geometryAdded`/`geometryUpdated`/`geometryDeleted` instead and re-query with `listWithComponent`.
Subscribe to `geometryAdded`/`geometryUpdated`/`geometryDeleted` and re-query with `listWithComponent` as needed.

## Constraint Endpoint Resolution

`resolveConstraintEndpoint(endpoint)` handles four endpoint types:

- `"point"` -- returns the literal `SheetPosition`
- `"locked-rectangle"` -- looks up rectangle, computes corner via `rectCorners()`
- `"locked-ellipse"` -- looks up ellipse, computes key point via `ellipsePoints()`
- `"locked-polygon"` -- looks up polygon, returns the indexed segment's point
- `"locked-rectangle"` -- looks up geometry, computes corner via `GeometryComponent.keyPoints`
- `"locked-ellipse"` -- looks up geometry, computes key point via `GeometryComponent.keyPoints`
- `"locked-polygon"` -- looks up geometry, returns the indexed segment's point
10 changes: 6 additions & 4 deletions docs/history-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@

## Undo/Redo Stacks

Two LIFO stacks: `undoStack` and `redoStack` of `UndoEntry` discriminated union types. There are ~25 entry types covering every undoable operation (generic geometry insert/delete/fillColor/renderOrder/linkDimensions, polygon-specific move/vertex ops, rectangle/ellipse move, constraint CRUD, etc.).
Two LIFO stacks: `undoStack` and `redoStack` of `UndoEntry` discriminated union types. There are ~25 entry types covering every undoable operation (generic geometry insert/delete/fillColor/renderOrder/linkDimensions, polygon-specific move/vertex ops, shape move, constraint CRUD, etc.).

- `undo()`: pop from undoStack, apply reverse, push to redoStack
- `redo()`: pop from redoStack, apply forward, push to undoStack
- Both emit `stacksChange` event (used by UndoAction/RedoAction to enable/disable buttons)

## Unified Entry Types
## Entry Types

Historically there were separate entry types per shape (`PolygonInsertEntry`, `RectangleInsertEntry`, `EllipseInsertEntry`). These have been unified into a single set of entry types that operate on `Geometry` directly:

Expand All @@ -22,9 +22,9 @@ Historically there were separate entry types per shape (`PolygonInsertEntry`, `R
| RenderOrder | `polygon-render-order` / `rectangle-render-order` / `ellipse-render-order` | `render-order` |
| LinkDimensions | `rectangle-link-dimensions` / `ellipse-link-dimensions` | `link-dimensions` |

Move entries (`polygon-move`, `rectangle-move`, `ellipse-move`) remain separate since they store different data shapes (segment arrays vs `{upperLeft, lowerRight}` vs `{center, radiusX, radiusY}`).
Move entries (`polygon-move`, `rectangle-move`, `ellipse-move`) remain separate since they store different data shapes (segment arrays vs `RectangleData` vs `EllipseData`).

Move entries store **raw component data** rather than geometry wrappers — e.g., `rectangle-move` stores `before: RectangleComponent[keyof RectangleComponent]` (which is `{ upperLeft, lowerRight }`), not a full `Geometry<RectangleComponent>`.
Move entries store **raw geometry state data** rather than full entities -- e.g., `rectangle-move` stores `before: RectangleData` and `after: RectangleData`, not full `Entity<GeometryComponent<RectangleData>>`.

Factory functions: `UndoEntry.insert(geometry)`, `UndoEntry.deleteGeometry(geometry)`, `UndoEntry.fillColor(id, before, after)`, etc.

Expand Down Expand Up @@ -54,6 +54,8 @@ The `applyForward`/`applyReverse` switch statements use only **Direct** methods

This prevents infinite loops (HistoryManager -> GeometryStore -> HistoryManager -> ...).

When updating polygon geometry in handlers, always use `GeometryComponent.isPolygon(old)` guards inside the `updateByIdWithComponentDirect` callback rather than casting with `as Entity<GeometryComponent<PolygonData>>`.

## State Serialization

Full undo/redo stacks are serialized into the SVG file's magic state comment (`<!-- cad2d-state:... -->`). The `stableIdCounter` is also preserved to prevent ID collisions after load.
Expand Down
9 changes: 2 additions & 7 deletions docs/layers.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,9 @@ Within each layer, shapes are sorted ascending by `renderOrder: number`. New sha
| Raise to Top | `renderOrder = 0` |
| Lower to Bottom | `renderOrder = getMaxRenderOrder()` |

## Per-Shape-Type Rendering
## Rendering

Each renderer file exports `ListLayers` or `SingleLayers` maps from `RendererLayers` to rendering components. For example, `PolygonLayers[Solids]` renders `<PolygonSolid>`, `PolygonLayers[Overlays]` renders `<PolygonOverlay>`.

The `ViewportRenderer2D` composes all layers by iterating the layer order and for each layer, rendering:

1. `ListLayersRenderer` -- iterates completed shapes (polygons, rectangles, ellipses) sorted by renderOrder
2. `SingleLayerRenderer`s -- global overlays (constraints, working shapes, DCEL debug)
All geometries are rendered by `GeometryRenderer.tsx` which iterates entities from the store, reads their `GeometryComponent` data, and dispatches to per-type draw routines via `switch (data.type)`.

## State Not Serialized

Expand Down
Loading
Loading