Repository navigation
Releases: hishamk/spatialboard
Releases · hishamk/spatialboard
Release list
SpatialBoard 0.3.5
Fixed
- A node whose port resolver throws no longer aborts the rest of a recompute pass. Resolving a node's ports runs host code and can throw, but it sat outside the guard that already isolates a throwing compute, so the exception escaped the flush and every node later in the topological order silently lost its recompute. The resolver now parks the error on that node and the pass carries on, exactly like a throwing compute.
- A node that stops emitting an output port while staying on the board no longer leaves the dropped port's last value behind. The value map only refreshed the ports a node still has, so a resolver that narrows its outputs after a data or type change left the gone port readable at its stale value. On recompute a node now drops any stored value for a port it no longer backs, and the drop counts as a change so a downstream edge still naming that port re-reads null.
SpatialBoard 0.3.4
Fixed
- Selection chrome drawn by the image, youtube, and blocknote-placeholder
blocks now stays screen-constant at any zoom. These blocks render their own
ring and handles in HTML inside the zoomed viewport layer and used fixed
pixel values, so the chrome scaled with the viewport — a thick ring and fat
rounded handle dots at high zoom. Ring outlines, handle borders, and handle
corner radii now divide by zoom like the SVG selection chrome; the youtube
block also drops the 6-unit handle floor that inflated its handles as zoom
rose. Ring corner rounding stays in canvas units, tracking the node's ink. - Selecting a blocknote placeholder no longer shifts its layout: the ring was
a border-width swap (1px → 2px) and is now an outline, which paints outside
the box.
SpatialBoard 0.3.3
Fixed
- The group fan-out FAB now mounts in floating chrome too. It was rendered
only in the console-chrome branch, so boards on the default
chrome="floating"had no fan affordance on grouped selections. The FAB is
selection chrome — it anchors to the selection envelope both modes share —
and keeps its own gates (grouped multi-selection, not read-only). - Undoing a fan-out pick no longer scatters the group to the transient ring
layout. The pick's z-raise pushed its undo snapshot while the members were
still fanned out, so undo restored the ring positions instead of the pile;
the snapshot now captures the members at their home positions.
SpatialBoard 0.3.2
Added
DataFlowEngine.getComputeError(nodeId)— the error the node's last
computethrew (sync) or rejected with (async), orundefinedafter a
successful run. Errors park there instead of aborting the flush, clear on
the node's next successful run, and both transitions notify change
listeners so hosts can render the failure.- A
graph:replacedengine event, emitted when the whole nodes map is
swapped at once (undo / redo / deserialize) rather than mutated through the
granular node lifecycle events. Consumers that mirror per-node state — the
data-flow engine does — rebuild against the new graph on it.
Fixed
- Deleting a node now recomputes its dependents immediately. The delete
handler marked them dirty but nothing scheduled the flush (node deletion
cascades the connected edges away without events), so stale outputs
survived until an unrelated event touched the graph. - A node recreated with a deleted node's id no longer accepts the dead
instance's late async result: generation counters bump on delete instead
of resetting, so an in-flight promise from the old instance can never
match the new one. Matters for serialized boards, where ids are
deterministic. - A fresh run supersedes an async run still in flight for the same node:
the superseded landing can no longer overwrite a newer result — or erase
the error a newer failing run parked. - One throwing
computeno longer costs the rest of the flush its
recompute: sync throws are caught and parked per node instead of escaping
the flush microtask after the dirty set was already cleared, and async
rejections are handled the same way instead of surfacing as unhandled
rejections. Failed nodes keep their last good outputs. dispose()now severs the SpatialEngine subscription itself — a host
that never called theconnect()cleanup no longer keeps a recomputing
zombie — marks the engine inert (a flush already in progress stops, and
a latemarkDirtyschedules nothing), and bumps the generation counters
so async computes still in flight resolve into nothing, even across a
dispose-and-reconnect revival.setContainerSizeignores any measurement that is not at least one whole
pixel on both axes. A NaN/Infinity size from an unvalidated host measurement
used to flow into the maintain-center shift and poison the viewport (and the
stored size that later fit math reads); a hidden container's 0×0 measurement
used to shift the viewport by half the old size on re-show; and a sub-pixel
measurement while a panel animates open (e.g. 0.5×0.4) used to consume the
one-shot parked fit against a degenerate size. Such measurements now change
nothing, parked fits keep waiting, and the next real (≥1px) measurement
recovers everything. Measurements are coerced to numbers first, so untyped
hosts passing numeric strings keep working.- Undo, redo, and deserialize now replay the data flow. They replace the
whole nodes map at once, which arrives with no granular node events, so the
data-flow engine used to leave restored nodes stale and removed nodes'
values lingering until an unrelated edit; it now reconciles against the new
graph — dropping gone nodes' state and recomputing the rest — with no
manual touch. Ordinary edits are unaffected (no whole-graph recompute). - Deleting a node drops its whole stored state by id, not just the ports its
type resolves at delete time. A node whose port set is data-driven and was
narrowed before deletion used to leak the dropped ports' values onto a
later node reusing the same id. - A throwing
onChangelistener is isolated: it is reported and the
remaining listeners still run, on the synchronous and asynchronous notify
paths alike, instead of starving later listeners or surfacing as an
unhandled rejection.
SpatialBoard 0.3.0
Six fixes and additions driven by real host-app usage of the data-flow board.
Added
fitToContent/fitToNodes(and their animated variants) accept options:padding(canvas-space margin, defaults unchanged),insets(screen-pixel bands reserved for host chrome floating over the canvas — content centers in the remaining region), andmaxZoom(framing-zoom cap, so fitting a single small node stops zooming all the way in).FitOptions/FitInsetsare exported from both the board andspatialboard/engineentries. Option values are sanitized: a NaN/InfinitypaddingormaxZoomand negative/non-finite insets can never poison the viewport transform.- Fits requested before the board's first size measurement are parked and applied when the container measures, instead of framing the 2000×1500 stand-in defaults. A
fitToContent()inside a seeding effect now just works — no more double-rAF / setTimeout dances in hosts.fitToFrameparks the same way, and attaching a different container re-arms parking (a reused engine remounted into another panel waits for the new measurement instead of framing the previous container's size). Headless engines (no container attached) keep fitting immediately. PortValueadmits arrays (unknown[]): matrices, vectors, and token lists flow through ports directly instead of being wrapped in an object to satisfy the type. (Note for consumers narrowingPortValue: aftertypeof v === "object"the type is nowRecord<string, unknown> | unknown[]— code assigning that to aRecordor indexing by string needs anArray.isArraybranch.)
Changed
- Output change detection is deep (SameValueZero primitives, arrays, plain objects, depth-capped), and a flush now skips downstream nodes whose upstream outputs did not actually change — a freshly built but structurally equal payload no longer recomputes the downstream graph. A
computethat reads external state and relied on those accidental recomputes should callmarkDirtyexplicitly. Structural comparison applies to plain data only: Date, Map, Set, class instances, and subclassed arrays compare by reference, so a fresh exotic instance always counts as a change.
Fixed
- Duplicate port wires between the same pair of nodes no longer make the target read as a cycle (its
computesilently never ran;cycleNodeIdsflagged it). The topological sort now counts in-degree per unique adjacency, so twin wires — easy to create from a StrictMode double-run seeding effect — cannot wedge a node.createEdgeadditionally returns the existing edge id for an exact duplicate port wire instead of stacking a twin. animatePanTo(x, y, 0)— and any camera animation given a zero, negative, or NaN duration — now snaps to the target instead of tweening elapsed/0 into NaN viewport transforms that blanked the canvas.- Port wires no longer intercept pointer events over the solid cards they cross: buttons, inputs, and draggable cells inside a card stay clickable under a crossing wire. The wire remains selectable in the gaps, and the right-click "Objects here" list reaches it even where a card covers it. Freeform edges keep their full hit priority, so annotation arrows over images behave as before.
- An inline
nodeTypesprop (nodeTypes={[...coreBoardNodes, myType]}, the README pattern) no longer rebuilds the registry — and recomputes the whole data-flow graph — on every host render; when acomputepublished into host state, that rebuild loop could hang the page.
Full details in CHANGELOG.md.
SpatialBoard 0.2.0
The first public release of SpatialBoard — an infinite-canvas library for React: whiteboards, node graphs, and slide decks in one MIT-licensed package.
Try it live: https://spatialboard.hishamkhalifa.com
Highlights since 0.1.0:
- Three package entries —
spatialboard(the slim board),spatialboard/blocknote(opt-in rich text), andspatialboard/engine(headless: no React, no CSS, runs in tests and agent tooling). - Typed data flow — nodes declare ports and a pure
compute; the reactive engine propagates values through the wired graph, detects cycles, and badges live values on edges. - Presentations — frames double as slides with animated transitions (pan, fade, zoom, cube), one call:
engine.enterPresentation(). - Compact mobile chrome — bottom toolbar, on-demand inspector sheet, and touch-sized hit targets under 640px.
- Interop — Excalidraw
.excalidrawlibimport and a Mermaid importer that turns flowcharts and sequence diagrams into editable nodes. - Round-trip exports — PNG/SVG exports embed the board's SBD source, so an exported image drops back onto a canvas as editable nodes.
- Agent-native surface — budgeted state snapshots for LLM context, a one-call authoring API, and SBD: a markdown board format models read and write.
Full details in CHANGELOG.md.