Skip to content

Add isometric objects - #13

Merged
D-Queck merged 48 commits into
mainfrom
add-isometric-objects
Oct 4, 2026
Merged

D-Queck merged 48 commits into
mainfrom
add-isometric-objects

Conversation

@D-Queck

@D-Queck D-Queck commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Isometric Objects — Connections

New Connection SFL construct, distinct from Link: connects two
Isometric objects face-to-face rather than side-to-side.

  • Full 6-face model (top/bottom/front/back/left/right) —
    previously only the 3 visible faces existed, which made a connection to
    the far side of an object route incorrectly across its visible faces.
  • Straight-line routing by default (isometric projection is linear, so a
    straight line in 3D space stays straight in 2D — no elbow/orthogonal
    routing needed).
  • A connection attached to a hidden face now renders behind the object
    that owns that face, instead of floating on top of it.
  • Live-follow during both move and axis-resize of either connected object
    (new face-aware parallel to the existing rect+side live-link-preview
    mechanism, since face-center geometry doesn't fit that model).
  • Deleting a canvas object now cascades to any Connection referencing it
    (previously only Link blocks were cleaned up).
  • New insertConnection patch function, exported from
    stickyflow/interaction.

Sankey

  • Nodes can now be dragged freely in both directions instead of only
    reordering within their column. The dropped position is persisted as an
    offsetX/offsetY on the node's zones entry, added on top of the automatic
    column/stacking layout rather than replacing it — repeated drags
    accumulate correctly.
  • Connected ribbons and the dragged node's label follow live during the
    drag (previously only the node itself moved).
  • Alignment guides (snap to another node's edge/center) appear while
    dragging, reusing the same snap mechanism shape drags already have.
  • A node's label can now be renamed via double-click; the rename updates
    the zones entry and every source/target reference to that node in
    one patch (applySankeyNodeRenameChange).
  • New sankeyNodeMove / sankeyNodeRename interaction events and
    applySankeyNodeMoveChange / applySankeyNodeRenameChange patch
    functions, exported from stickyflow/interaction.

Boxplot

  • Category/series axis labels (both the vertical default orientation and
    the horizontal Y-axis variant) are now wired into the existing
    property-edit system (data-sf-prop → Series[id].label), so they can
    be renamed via double-click like every other label in the library. This
    was previously dead — the tick labels carried no editable property path.

D-Queck and others added 30 commits October 2, 2026 16:03
…strokes

Each face previously stroked its own four edges, producing visible seams
at the internal boundaries. Faces now fill only; a separate hull path
traces the six-point silhouette with the bold stroke, and a faint path
draws the three interior seam lines converging on the near corner.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…lter

Generalizes SvgFilterDefinitions.define() to take any filter-primitive
value, adds a feGaussianBlur branch, and wires the isometric object's
ground shadow to shadowStyle.blur (default 8) so it can be softened
instead of rendering a hard-edged ellipse.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Interactivity: isometric objects were tagged with their own SceneRole
but never added to the interaction layer's per-role allowlists, so
clicking one never resolved to an owner at all - selection, move,
resize and rename were all silent no-ops. Also fixes the hull being
picked as the primary node (not the ground shadow or a single face,
which gave the selection ring the wrong bounds) and the SFL block
scanner's keyword list (it never knew `Isometric` was a valid owner
header, so even a resolved move/resize/rename never found the block to
patch). Resize now also preserves the object's depth (previously the
generic 2-value size writer silently dropped the third value), and
rides the composite (chart/table) scale+translate preview instead of
the per-kind preview path, since isometric has no registered shape-kind
geometry function there - this is what gives it live resize feedback
and keeps the label translating correctly instead of jumping to the
drag handle.

Rendering: each face rounded its own corners independently of the
hull's silhouette rounding, so the two never quite lined up - leaving
slivers of background visible at the corners and the interior seam
lines overshooting past the rounded edge. Faces are now drawn sharp
and clipped to the hull's own rounded path, so the fill and the stroke
are always trimmed by the exact same curve.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Dragging a corner handle stretched width and height independently,
breaking the isometric angle - the box no longer looked like a
projected cube once released. Isometric objects now get their own
selection overlay (three circular axis handles at the right/bottom/left
vertices instead of the generic rectangle's eight square ones): one
drags width, one height, one depth, and nothing else moves - x/y and
the other two dimensions stay exactly fixed, so the angle is preserved
by construction instead of needing to be re-derived after the fact.

Live preview recomputes the actual isometric geometry (faces, hull,
seams, ground shadow, label) on every pointermove via the same
isometricFaces/roundedPolygonPath used at render time, rather than a
generic bounding-box transform - this also fixes the corner-radius
clip being clipped to the stale pre-drag silhouette mid-drag (dropped
for the duration of the drag; the post-commit re-render restores it).

Also fixes a precision bug this surfaced: reading the hull's rendered
getBBox() as the resize start size drifted by a few units because
rounded corners pull the Bezier curve in from the sharp-corner bounds.
The hull now also carries a declaredBounds attribute (the same
mechanism Symbol shapes use) with the exact pre-rounding size.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The axis handles from the previous commit still sat inside a generic
rectangular selection ring, which read as the same box-resize UI as
every other shape despite only offering 3 points instead of 8. Swaps
the ring for a halo that traces the hull's own (rounded) outline, and
each handle for a shaft-and-arrowhead vector pointing outward along
that dimension's true isometric edge angle - closer to a 3D tool's
per-axis scale gizmo than a resize box.

Also makes the live-drag preview reposition the halo and the two
stationary arrows (not the one being dragged) to match the live
geometry; previously only the object itself updated, leaving the
overlay visibly pinned to the pre-drag size until drop.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The previous formula tied width and depth to the same term (both set
the apex's horizontal/vertical reach together), so changing either one
retargeted the apex angle of the other - exactly the "significant
geometry changes" the axis handles were producing. isometricFaces now
anchors the apex and reaches right/left along two fixed-angle (30 deg)
vectors scaled independently by width and depth - each is a pure
translation of its own side, the other side's geometry is completely
unaffected. The on-screen bounding box is derived from these (see the
new isometricBounds), not stored directly - equal width/height/depth
now renders a true cube.

Depth still has one residual coupling: the apex sits at x + depth*cos
(so `left` lands exactly on the box's declared x), so holding x fixed
while depth changes still drags the apex sideways. The axis-resize
session now compensates x by the same amount depth moves, so only the
left side visibly slides and the apex/right side stay exactly put.

Width/depth arrows are repositioned to the center of the face each one
alone resizes (side, front) rather than a corner vertex.

Updates the existing isometric-geometry unit tests to the new formula
and demo values across the stickyflow.js build - not run here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…med label anchor

- Shaft/arrowhead sized up slightly (still a vector, not a box handle).
- width/depth point at the true 30deg axis angle, not horizontal/
  vertical - swapped their cursors from ew/ns-resize to the closer
  nwse/nesw-resize diagonals (no native CSS cursor exists for an exact
  30deg angle).
- Added isometricTopFaceCenterX and used it for the label/label-guide
  x position in both the renderer and the live-drag preview, instead
  of the equivalent but unexplained bounds.x + bounds.width / 2 - same
  number, but now reads as "centered on the top face" like the
  seams/hull it sits above.

Verified live (scripted pointer drags + DOM attribute snapshots mid-
drag) that hull/seams/label/label-guide were already updating on every
pointermove before this commit, not just on drop - most likely a
stale dev-server build (file: deps don't hot-reload) rather than a
real regression, flagged to the user rather than silently assumed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… Flucht snapping

Label fix: the label/label-guide were anchored to the top face's x only,
with y pinned to the apex (which never moves). That made the label slide
horizontally while the top face's actual centroid - built from the same
apex+toRight+toLeft vectors as every edge - moves diagonally when width
or depth changes. isometricTopFaceCenter now returns the full point and
both the renderer and the live-drag preview anchor to it, so the label
rides the same fixed-angle line the edges do instead of visibly lagging
on one axis.

Cursor fix: nwse/nesw-resize are 45deg, not this object's fixed 30deg
axis angle, so they never quite matched the arrow they sat on. Replaced
with a tiny inline double-headed-arrow SVG cursor rotated to the exact
angle of the direction vector the arrow itself is drawn with (same
source value, so they can't drift apart).

New: isometric "Flucht" snapping. Dragging a single isometric object now
tries snapping its apex onto the 30/150deg line through another
isometric object's apex before falling back to the regular horizontal/
vertical bounds snap - lets objects be dragged into the same true-
isometric diagonal instead of hand-tuning position numbers. Required
widening GuideLine to a union (a plain horizontal/vertical pair can't
express an arbitrary-angle line) - computeSnap's own return type stays
narrowed to the h/v-only variant via the new RectGuideLine so its
existing callers didn't need any guard added.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
D-Queck added 18 commits October 3, 2026 14:26
…w fixes

- Label can be freely dragged (labelOffsetX/Y) with an optional
  labelBackground plate for legibility over colored faces
- Ground shadow is now the true isometric footprint (top face carried
  down by height, spread outward), following both axes instead of a
  fixed-orientation ellipse
- Shadow sits in its own layer strictly behind every isometric object's
  body, so one object's shadow can never paint over another's faces
- Shadow stays pinned to the object's own base instead of drifting with
  height, and intensifies the taller the object grows
…ks height

Without distance, the ground shadow keeps dropping by the object's own
height as before (moves with it when scaling up/down). Setting
distance explicitly anchors it a fixed drop below the top face instead,
independent of height, so users can choose per object whether the
shadow should follow the resize or stay pinned to one plane.
It previously replaced the height-based drop outright, so a distance
smaller than the object's own height moved the shadow closer to it
instead of further away - visibly "upward" and counter-intuitive.
distance is now added on top of the height-based drop, so positive
values only ever increase the gap.
Adds a duplicate bubble at each of the six faces (the three visible
ones plus their hidden opposites, mirrored through the object's
visual center), each offset exactly along that face's own true
isometric vector - width/depth move diagonally at the fixed 30deg
axis angle, height moves straight down, so a chain of duplicates
stays aligned on the same axis instead of drifting.

Click-only (no drag-to-link, unlike the generic 2D quickDuplicate
bubbles) via a new, separate binding - a cube face is a 3D axis
direction, not a 2D box edge, so dragging it onto another shape to
link up doesn't carry the same meaning. Reuses the existing
`quickDuplicate` event, widened to accept the six new iso-prefixed
directions alongside top/right/bottom/left.

Also extends insertCanvasObject with an isometric kind (depth +
3-value layout.size), and fixes the axis-resize live-drag preview,
which was still writing cx/cy/rx/ry to the ground shadow - now a
<path>, not an <ellipse>, since the shadow rewrite - so it silently
no-opped and the shadow sat frozen until the drag ended.
…, hover

- Only the three visible-face directions (top, front, side) get a
  bubble now; the three hidden/reflected ones are gone
- Bubble margin now clears the axis-resize arrow tips instead of
  sitting mid-shaft and overlapping them
- Bubbles grow and switch to the brand color on hover, with a matching
  transition
Tags each face with the same prop/propKind:'color' contract chart
legend swatches use, so clicking a face in an editable canvas opens
the color picker directly - cubes aren't limited to whatever fixed
colors the SFL happened to declare.
- Replaces topFill/frontFill/sideFill with one `fill` property; the
  front/side faces derive their shade from it automatically (uniform
  RGB darkening), instead of three independently chosen colors that
  can drift out of sync with each other
- Clicking a face now requires the cube to already be selected -
  the first click always just selects it, a second click opens the
  color picker. Gated on a selection snapshot taken at pointerdown
  (before the click's own selection can happen), since checking live
  DOM state at click time would always see the object as "already
  selected", even on the very click that just selected it
0.88/0.76 made a white cube's side face a visibly harsh gray next to
it. 0.95/0.88 reads as a soft, elegant light/shadow step instead of a
hard color jump, while still giving darker colors enough contrast to
read as three faces.
anchorY was missing a /2 - it landed on the "center" vertex (where
top/front/side meet) instead of the top face's centroid, snapping the
guide line's start point there for the live drag preview and only
correcting back once the renderer re-ran on release. Matches
isometricTopFaceCenter's formula exactly now.
…ashes

computeIsolineSegments (marching squares) hands back one independent
segment per grid cell - correct in isolation, but each was drawn as
its own disconnected "M ... L ..." command, so the isoline was never
actually one continuous path, just many short straight dashes abutting
at every cell boundary. That's what read as crooked, not the data.

Adds stitchIsolineSegments: walks the segment graph (adjacent cells
compute their shared edge intersection identically, so endpoints match
exactly) into ordered point chains, one per contiguous strand or
closed loop. Each chain is then run through the same Catmull-Rom
smoothing (linePathData) line/path charts already use for their own
points, instead of straight linear segments.
@D-Queck
D-Queck merged commit f1f7f4e into main Oct 4, 2026
0 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant