Viewport bridge
packages/ui's panel blocks need to show transient feedback on the host's canvas — a point marker while hovering a point arg, a bounding box while hovering a node's key, and so on — without owning a renderer themselves. ViewportBridge (packages/ui/src/types.ts) is the injected contract that makes this possible: the UI describes what to show as data (PreviewJSON), and each host interprets that data with its own renderer. Same data in, same preview out — that is what keeps the web and SketchUp previews consistent.
export interface BuilderHost {
// ...
viewport?: ViewportBridge
}
export interface ViewportBridge {
// Replace the current preview with `preview` (an implicit clear first).
preview: (preview: PreviewJSON) => void
clearPreview: () => void
}viewport is optional. A host with no canvas overlay of its own can leave it undefined — every call site goes through hoverPreview, which no-ops without a viewport.
The PreviewJSON vocabulary
PreviewJSON is { items: PreviewItem[] }. Each item is a tagged union member:
kind | Payload | Shows |
|---|---|---|
point | position | a marker at one position (hosts may add axis guides / dimension labels) |
points | positions, color? | many point markers at once (e.g. all candidate vertices in a vertex filter) |
segments | segments, color? | many line segments at once (e.g. all candidate edges in an edge filter) |
vector | direction, anchorKey? | a directed arrow; anchored at anchorKey's rendered bounding-box min corner if found, else the host's focus point |
axis | origin, direction | a long directed half-line |
plane | origin, normal | a finite patch oriented to normal (a plane is conceptually infinite — only a legible patch is drawn) |
boundingBox | key | the bounding box of the scene object named key (a node's rendered key) |
entities | entities: EntityJSON[], color? | ANY engine entities as a transient ghost |
Colors are "#rrggbb" strings — JSON-portable to any host (the Ruby side parses the same string the Three.js side does).
entities is the escape hatch. The seven kinds above are a fixed, named vocabulary — good for the previews the panel needs today, but any NEW use case (preview a custom modifier's output shape, say) would otherwise mean growing the union. entities sidesteps that: a hover handler builds ordinary EntityJSON[] (the engine's own value currency) and puts it in the payload; the host renders it through the same render() primitives the real scene uses (curves as lines, faces as translucent meshes, points as dots).
hoverPreview
hoverPreview (packages/ui/src/blocks/NodeShared.ts) is the one lifecycle every hover handler in the panel goes through:
export function hoverPreview(
viewport: ViewportBridge | undefined,
build: () => PreviewItem[],
restore?: () => PreviewItem[],
): { onMouseEnter: () => void; onMouseLeave: () => void }- Enter:
clearPreview()thenpreview({ items: build() })(skipped whenbuildreturns no items). - Leave:
clearPreview()then the same withrestoreif given — used to put back an ANCESTOR's preview (e.g. a nested arg row clearing on leave must restore its parent generator's bounding box, not leave the canvas blank). - Handlers are always returned, viewport or not, so a component's DOM shape never depends on whether the host has a canvas.
build/restoreerrors are swallowed — a broken preview must never break the panel.
const hover = hoverPreview(ctx.viewport, () => [{ kind: "boundingBox", key: entry.key }])
// ...
nodeHeader({ onMouseEnter: hover.onMouseEnter, onMouseLeave: hover.onMouseLeave, /* ... */ })Implementing the bridge
- Web (
apps/web) — theViewportclass implementspreview/clearPreviewdirectly;SceneHighlight.apply(preview)(apps/web/src/SceneHighlight.ts) dispatches each item to a Three.js drawing method (spheres, arrows, translucent planes,LineSegments2, screen-space dimension labels). Itsentitieshandling callsrender(entity)from@parashape/parametric/computeto turn eachEntityJSONinto a drawable primitive (point / curve / mesh). - SketchUp (
apps/sketchup+sketchup/parashape/preview.rb) —sketchupViewport(apps/sketchup/src/SketchUpHost.ts) sends the whole payload as ONE bridge message (preview-show), pre-renderingentitiesitems with the SAMErender()the web host uses, so Ruby reuses therender.rbitem pipeline — identical tessellation on both hosts.preview.rbinterprets the items as transient construction geometry (construction points, edges, construction lines, bounding-box wireframes) inside a temp group and a transparent operation;preview-clearerases the group.
A third host only needs to implement preview/clearPreview and interpret the item kinds with whatever primitives its own canvas/scene supports — unknown kinds can simply be skipped.