Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
83bc3cd
refactor(render-scene): rename entities to artifacts
D-Queck Oct 2, 2026
d16b5e5
refactor(dom): use shape roles internally
D-Queck Oct 2, 2026
e92d80f
test(architecture): lock the src top-level structure
D-Queck Oct 2, 2026
b2b0332
refactor(runtime): adopt canvas object terminology
D-Queck Oct 2, 2026
0c6e92b
refactor(core): remove generic entity terminology
D-Queck Oct 2, 2026
0d0dbb6
docs(architecture): align terminology and pipeline
D-Queck Oct 2, 2026
8c7f8cd
define isometric object types
D-Queck Oct 2, 2026
58ca1f9
Integrate isometric in sfl-language layer
D-Queck Oct 2, 2026
f8bad2c
refactor(sfl-semantic): split canvas object artifact types
D-Queck Oct 2, 2026
b1bc6c7
refactor(sfl-semantic): delegate canvas object resolution
D-Queck Oct 2, 2026
62836b3
feat(api): export isometric semantic types
D-Queck Oct 2, 2026
0e37c81
test(sfl-semantic): cover isometric object resolution
D-Queck Oct 2, 2026
8a42a62
feat(render-input): adapt isometric objects
D-Queck Oct 2, 2026
8b0140c
feat(model): define isometric face geometry
D-Queck Oct 2, 2026
c0e1b90
feat(render-scene): build isometric face artifacts
D-Queck Oct 2, 2026
6e189f3
refactor(isometric): read depth from layout size
D-Queck Oct 2, 2026
1d08951
feat(render-scene): add rounded polygon paths
D-Queck Oct 2, 2026
d48afe5
feat(render-scene): round isometric object corners
D-Queck Oct 2, 2026
395ba78
feat(render-scene): label isometric objects
D-Queck Oct 2, 2026
c9fc49c
feat(render-scene): add isometric guides and ground shadows
D-Queck Oct 2, 2026
05fbb78
test(isometric): cover rendering pipeline
D-Queck Oct 2, 2026
05f76d2
chore(isometric): satisfy package formatting
D-Queck Oct 3, 2026
a7af75b
fix(isometric): render single silhouette outline instead of per-face …
D-Queck Oct 3, 2026
21d4edf
feat(scene): support blurred shadows via a generic SceneStyle.blur fi…
D-Queck Oct 3, 2026
98fa9ae
fix(isometric): wire interactivity into the runtime, fix corner rounding
D-Queck Oct 3, 2026
093d229
feat(isometric): axis-handle resize instead of free corner resize
D-Queck Oct 3, 2026
f7ee69d
fix(isometric): replace resize-box overlay with per-axis vector handles
D-Queck Oct 3, 2026
b1bd9d3
fix(isometric): rewrite geometry as two independent true-axis vectors
D-Queck Oct 3, 2026
1e2645c
fix(isometric): thicker/longer axis arrows, angle-matched cursors, na…
D-Queck Oct 3, 2026
93126a6
feat(isometric): label follows the true diagonal, exact-angle cursor,…
D-Queck Oct 3, 2026
e7669aa
feat(isometric): draggable labels with background, ground-plane shado…
D-Queck Oct 3, 2026
1605d64
feat(isometric): shadow(distance:px) controls whether the shadow trac…
D-Queck Oct 3, 2026
a04fd42
fix(isometric): shadow(distance:px) now always pushes the shadow away
D-Queck Oct 3, 2026
3aad1ac
feat(isometric): quick-duplicate bubbles on all six cube faces
D-Queck Oct 3, 2026
aec0a54
fix(isometric): quick-duplicate bubbles - 3 visible faces, no overlap…
D-Queck Oct 3, 2026
107d9dd
feat(isometric): make topFill/frontFill/sideFill color-editable
D-Queck Oct 3, 2026
66f624b
feat(isometric): single fill color, two-step color picker
D-Queck Oct 3, 2026
d14c55d
fix(isometric): soften the derived face shading
D-Queck Oct 3, 2026
e2ef910
fix(isometric): label guide line no longer jumps during drag
D-Queck Oct 3, 2026
05213f8
fix(charts): field isolines are now one smooth curve, not dozens of d…
D-Queck Oct 4, 2026
2dbe78c
fix(charts): interactive label editing
D-Queck Oct 4, 2026
b427d16
fix(charts): sankey interactive positioning
D-Queck Oct 4, 2026
a387be2
feat(charts): sankey snapping lines
D-Queck Oct 4, 2026
4a2eb63
fix(charts): boxplot label change
D-Queck Oct 4, 2026
14462de
feat(objects): integrate object connections
D-Queck Oct 4, 2026
877d8b8
fix(objects): connection background layering
D-Queck Oct 4, 2026
11add62
fix(objects): connections 3D - routing
D-Queck Oct 4, 2026
e7eabd0
fix(objects): delete connection artefacts
D-Queck Oct 4, 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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ Two things count as breaking here that are easy to overlook:

### Changed

- **Breaking (terminology).** Runtime and interaction APIs now use the current
canvas-object vocabulary: `entityClick` is `canvasObjectClick`, and the pure
patch helpers are `insertCanvasObject` / `removeCanvasObject` with matching
option types. Diagnostic ownership is now exposed as `ownerId` instead of
`entityId`. The legacy SFL `Entity` block and the version-1 serialized
`data-sf-role="entity"` value remain supported.
- **Breaking (internal module layout).** `rendering/renderScene` is split into
`rendering/scene` (scene vocabulary), `rendering/renderInput` (pipeline input)
and `rendering/domContractVersion`. The public entry re-exports the same
Expand Down
43 changes: 24 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,17 @@ const { svg, width, height, diagnostics } = render(sflSourceText);
// A complete, deterministic SVG string. render() never touches the DOM.
```

Pipeline: `SFL text → parse → sflToCanvasState → sflToImportJson → sflToRenderInput
→ buildRenderScene → renderSceneToSvg`.
Render pipeline: `SFL text → parse → sflToCanvasState → sflToRenderInput
→ buildRenderScene → renderSceneToSvg`. `sflToImportJson` is a separate
adapter and is not part of rendering.

## Structure

```
src/
model/ L0 framework-agnostic diagram model and geometry
sflLanguage/ L1 SFL Language: AST, lexer, parser and canonical emitter
sflSemantic/ L2 SFL Semantic: SFL AST → resolved canvas state
sflSemantic/ L2 SFL Semantic: SFL AST → semantic canvas state
renderInput/ L3 renderer contract, SFL adapter and presentation-value decoding
renderScene/ L4 scene contract and deterministic scene construction
svg/ L5 SVG serialization and versioned DOM contract
Expand All @@ -49,6 +50,9 @@ src/
The three tiers and their one-way dependency are described under *Two tiers*
below and enforced by `test/boundary.test.ts`.

When adding a new placeable artifact, follow the layer-by-layer
[Canvas Object integration checklist](docs/ADDING_CANVAS_OBJECT.md).

## Known gaps (documented, not hidden)

- **Absolute link waypoints have no SFL representation.** A manually-adjusted
Expand Down Expand Up @@ -76,7 +80,7 @@ below and enforced by `test/boundary.test.ts`.
DOM write is incremental (`patchSceneNodes`, only what differs), but the
geometry it diffs against is recomputed for the whole scene graph every
time — text measurement, link routing, everything — not just the changed
entity. Fine at the hundreds-of-shapes scale this package targets; a
canvas object. Fine at the hundreds-of-shapes scale this package targets; a
many-thousand-node board would need incremental geometry, which doesn't
exist yet.
- **No conflict handling for concurrent writers**: `applyMoveChange` and
Expand All @@ -89,7 +93,7 @@ below and enforced by `test/boundary.test.ts`.
node a role, an id, and a focusable/labelled primary (`tabindex`,
`aria-label`) — enough for a screen reader to enumerate what's on the board
and read each label. It does not yet describe *relationships* (which link
connects which entities, table semantics beyond a labelled grid, a
connects which canvas objects, table semantics beyond a labelled grid, a
meaningful reading order for a non-trivial layout) — a sighted user reads
those from position; a screen reader user currently can't.

Expand Down Expand Up @@ -165,7 +169,7 @@ break at different points under each.

`render()`'s output is meant to be inserted via `innerHTML` (that's how the
docs examples and `stickyflow/viewer` use it), so everything user-supplied —
entity/link text, ids — is escaped before it reaches the SVG string
canvas-object/link text, ids — is escaped before it reaches the SVG string
(`renderSceneToSvg.ts`'s `xml()`: `& < > " '`), and ids are additionally
restricted to a safe character set (`safeId`). There is no `<script>`,
`<foreignObject>`, or event-handler attribute anywhere in the renderer's own
Expand All @@ -179,7 +183,7 @@ actively hostile payload (an attribute break-out plus an inline

The SVG is the binding surface for a viewer. Every node carries its owner's
identity so a click on any sub-node (a cylinder cap, a table cell, link text)
resolves to the entity via `closest('[data-sf-id]')`:
resolves to the owning canvas object via `closest('[data-sf-id]')`:

```html
<svg … role="img" data-sf-contract="1">
Expand All @@ -191,8 +195,9 @@ resolves to the entity via `closest('[data-sf-id]')`:

- `data-sf-contract` on the root is the versioned handshake (`DOM_CONTRACT_VERSION`):
a viewer checks it on mount and refuses a mismatch instead of binding blindly.
- `data-sf-role` ∈ `entity | link | frame | swimlane | table`; `data-sf-id` is the
stable owner id; `data-sf-kind` is the shape type for entities.
- `data-sf-role` ∈ `entity | link | frame | swimlane | table`; `entity` is the
version-1 serialized compatibility value for an internal `shape` role.
`data-sf-id` is the stable owner id; `data-sf-kind` is the concrete kind.
- The **primary** node of each owner is focusable (`tabindex="0"`) and labelled
(`aria-label`, whitespace-collapsed to one line). Ids and labels are XML-escaped.

Expand Down Expand Up @@ -229,10 +234,10 @@ pays nothing for viewer JS. The web component sits on top and composes both.
A third, opt-in tier layered on top of `stickyflow/viewer` (never the other way
round): double-click to edit text, drag to move (a multi-selection moves
together), drag a corner handle to resize, drag either endpoint of a selected
link onto a new anchor — a different spot on the same entity, or a different
entity entirely — to reconnect it. The dragged endpoint follows the cursor as
link onto a new anchor — a different spot on the same canvas object, or a different
canvas object entirely — to reconnect it. The dragged endpoint follows the cursor as
a free "ghost" over empty canvas and snaps to the nearest point on whichever
entity's under the pointer, so the gesture reads as continuous rather than
canvas object's under the pointer, so the gesture reads as continuous rather than
freezing until you land exactly on a target. All with a live preview (real
router, real text re-wrap) while the gesture is in progress. The package never mutates its
own model — it's a **controlled component**: each gesture only emits an event
Expand Down Expand Up @@ -261,30 +266,30 @@ They patch only the touched property's own line(s) — via brace-depth block
location, not a full parse-mutate-emit round trip (there's no reverse
serializer in this package yet, see *Known gaps*) — so everything else in the
document (comments, formatting, unrelated blocks) survives untouched, and
`applyMoveChange` resolves the entity's *current* position through the real
`applyMoveChange` resolves the canvas object's *current* position through the real
parser first, so a delta lands correctly even when the position is inherited
from a composed `Layout` reference rather than set inline. A host is free to
ignore these and patch its own structured state instead — the events carry
everything needed either way.

**Structural changes** — `insertEntity`/`removeEntity`/`insertLink`/`removeLink`
**Structural changes** — `insertCanvasObject`/`removeCanvasObject`/`insertLink`/`removeLink`
(also from `stickyflow/interaction`, pure/no DOM) cover create/delete, which no
drag gesture produces on its own. Call them directly — from a host reflecting
an LLM-authored change, an "add shape" toolbar button, etc. — and pass the
result to `viewer.update()` the same way:

```ts
import { insertEntity, insertLink, removeEntity } from 'stickyflow/interaction'
import { insertCanvasObject, insertLink, removeCanvasObject } from 'stickyflow/interaction'

currentSource = insertEntity(currentSource, { id: 'n2', kind: 'shape', text: 'New', x: 320, y: 80, width: 160, height: 60 })
currentSource = insertCanvasObject(currentSource, { id: 'n2', kind: 'shape', text: 'New', x: 320, y: 80, width: 160, height: 60 })
currentSource = insertLink(currentSource, { id: 'l1', fromId: 'n1', toId: 'n2' })
currentSource = removeEntity(currentSource, 'n1') // also removes links attached to it by default
currentSource = removeCanvasObject(currentSource, 'n1') // also removes links attached to it by default
```

Same surgical-patch philosophy as the apply\*Change functions: `insertEntity`/
Same surgical-patch philosophy as the apply\*Change functions: `insertCanvasObject`/
`insertLink` append a new block (indented to match, id-uniqueness- and
parent-checked first — a no-op if the id already exists or a referenced
entity/parent doesn't), `removeEntity`/`removeLink` splice out a block's exact
canvas object/parent doesn't), `removeCanvasObject`/`removeLink` splice out a block's exact
line range. Neither attempts a generic value formatter for arbitrary style
properties — SFL's quoting rules depend on each property's own semantics, not
just the JS type — so extra properties go through an optional raw `body`
Expand Down
2 changes: 1 addition & 1 deletion demo/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ const write = (line: string): void => {
const viewer = createViewer({ container, source: SFL, interactive: true, diagramId: 'checkout' });

viewer.on('ready', () => write('ready'));
viewer.on('entityClick', e => write(`entityClick → ${e.owner.kind} "${e.owner.label}" (${e.owner.id})`));
viewer.on('canvasObjectClick', e => write(`canvasObjectClick → ${e.owner.kind} "${e.owner.label}" (${e.owner.id})`));
viewer.on('linkClick', e => write(`linkClick → ${e.owner.id}`));
viewer.on('selectionChange', e => write(`selection: [${e.selection.map(o => o.id).join(', ')}]`));
viewer.on('sceneDiff', e => write(`sceneDiff → added:[${e.added}] removed:[${e.removed}] changed:[${e.changed}]`));
Expand Down
115 changes: 115 additions & 0 deletions docs/ADDING_CANVAS_OBJECT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Adding a Canvas Object

Use this checklist when adding a new kind of placeable diagram artifact. In current code, use **canvas object** as
the umbrella term. Use **shape** only for the concrete `Shape` artifact. Do not introduce new `Entity` names; that
word is reserved for legacy SFL compatibility.

The required dependency direction is:

`L0 Model -> L1 SFL Language -> L2 SFL Semantic -> L3 RenderInput -> L4 Render Scene -> L5 SVG`

Complete one layer before moving to the next. A new artifact is finished only when its public pipeline test and
documentation are complete.

## Before implementation

- [ ] Choose one precise artifact name and use it consistently in SFL, TypeScript, scene IDs, tests, and docs.
- [ ] Decide whether this is genuinely a new canvas-object kind or only a new `Shape.shapeType`.
- [ ] Write down its authored SFL contract: block kind, properties, child blocks, and defaults.
- [ ] Decide which values are semantic data and which values only control rendering.
- [ ] Confirm that links and interaction are part of this change; otherwise leave them for separate features.

## L0 - Model (`src/model`)

- [ ] Add the framework-neutral renderer input type for the artifact.
- [ ] Put reusable geometry calculations in the model when they do not depend on Scene or SVG.
- [ ] Reuse existing `Point`, `Bounds`, paint, shadow, and geometry contracts where they fit.
- [ ] Export the model type from its domain index.
- [ ] Add unit tests for non-trivial geometry, clamping, and boundary cases.

Do not import SFL, Render Scene, SVG, DOM, or runtime code into Model.

## L1 - SFL Language (`src/sflLanguage`)

- [ ] Register the authored block kind in the AST/parser vocabulary.
- [ ] Confirm that parsing produces syntax only; do not resolve defaults or diagram meaning here.
- [ ] Confirm that canonical emission preserves the block and is idempotent.
- [ ] Add a parse-and-emit test using the smallest valid example.

## L2 - SFL Semantic (`src/sflSemantic`)

- [ ] Add the artifact-specific semantic payload and specialized canvas-object type.
- [ ] Add a type guard named `is<Artifact>Object` when consumers need narrowing.
- [ ] Add the block kind to canvas-object classification.
- [ ] Add a focused reader for artifact properties.
- [ ] Add a focused resolver that combines the shared base with the artifact payload.
- [ ] Delegate to that resolver from the canvas-object processing switch.
- [ ] Export the semantic type and guard through the semantic public surface.
- [ ] Add a semantic test for resolved values, defaults, and the type guard.

Keep shared canvas-object fields in the base resolver. Keep artifact-only fields in the artifact reader and resolver.

## L3 - RenderInput (`src/renderInput`)

- [ ] Add the renderer-ready artifact collection to `RenderInput` if it needs its own rendering workflow.
- [ ] Add one semantic-to-model adapter for the artifact.
- [ ] Decode authored presentation values here; do not make Render Scene understand SFL values.
- [ ] Add the artifact to canvas-object collection.
- [ ] Add an adapter test proving that semantic values and defaults reach RenderInput.

Choose one owner for every default. Prefer the adapter for presentation defaults and Render Scene only for
defensive fallbacks required by programmatically constructed `RenderInput`.

## L4 - Render Scene (`src/renderScene`)

- [ ] Add a `SceneRole` only when the artifact needs its own DOM-contract identity.
- [ ] Implement an artifact builder that returns existing abstract scene nodes.
- [ ] Keep geometry calculations out of the scene builder when they can remain pure Model operations.
- [ ] Add a collection builder that applies semantic tagging for the artifact owner.
- [ ] Register that collection builder in the small scene orchestration function.
- [ ] Give every generated node a deterministic ID and order.
- [ ] Include all visual extents in bounds: strokes, labels, shadows, and decorations.
- [ ] Add focused scene tests for node types, geometry, paint, order, identity, and optional visuals.

Do not put artifact-specific construction back into the central scene orchestrator.

## L5 - SVG (`src/svg`)

- [ ] Verify that the artifact serializes through existing Scene nodes.
- [ ] Add SVG code only if the artifact requires a genuinely new generic Scene primitive.
- [ ] Preserve escaping, deterministic IDs, and the versioned DOM contract.

Most new artifacts should require **no L5 implementation change**. If SVG needs to know the artifact kind, first
check whether the abstraction belongs in Render Scene instead.

## Public API and optional runtime support

- [ ] Export intentionally public model and semantic types from `src/index.ts` without disturbing its L0-L5 order.
- [ ] Rebuild `dist` and inspect the actual ESM and declaration exports.
- [ ] Update `stickyflow/viewer` or `stickyflow/interaction` only when the feature explicitly includes interaction.
- [ ] If insertion/removal is supported, extend the Canvas Object patch contracts without restoring `Entity` names.
- [ ] Update downstream consumers after a public rename; do not hide current naming behind new legacy aliases.

## Completion tests

- [ ] Add one public pipeline test: SFL text -> semantic state -> RenderInput -> Render Scene -> SVG.
- [ ] Assert diagnostics are empty for the valid example.
- [ ] Assert stable scene IDs and at least one meaningful SVG result.
- [ ] Run focused tests while implementing.
- [ ] Run `npm run lint`, `npm run typecheck`, and `git diff --check`.
- [ ] Run `npm run ci`; report unrelated existing failures explicitly instead of silently ignoring them.

## Documentation and review

- [ ] Add or update the artifact page in the StickyFlow-Web stickyflow.js library docs.
- [ ] Document only behavior that is implemented now; list future interaction, icon, or link behavior separately.
- [ ] Run the Web docs export check and production build.
- [ ] Review the final diff for duplicate defaults, oversized orchestrators, optional payload fields, and forgotten
registrations.
- [ ] Keep structural refactors in separate commits from new artifact behavior where practical.

## Definition of done

The artifact is done when a public `render(source)` call produces its expected SVG, every layer can be tested in
isolation, no lower layer imports a higher layer, the public API and docs use current Canvas Object terminology, and
no unrelated subsystem needed artifact-specific knowledge.
Loading
Loading