ParaShape

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:

kindPayloadShows
pointpositiona marker at one position (hosts may add axis guides / dimension labels)
pointspositions, color?many point markers at once (e.g. all candidate vertices in a vertex filter)
segmentssegments, color?many line segments at once (e.g. all candidate edges in an edge filter)
vectordirection, anchorKey?a directed arrow; anchored at anchorKey's rendered bounding-box min corner if found, else the host's focus point
axisorigin, directiona long directed half-line
planeorigin, normala finite patch oriented to normal (a plane is conceptually infinite — only a legible patch is drawn)
boundingBoxkeythe bounding box of the scene object named key (a node's rendered key)
entitiesentities: 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() then preview({ items: build() }) (skipped when build returns no items).
  • Leave: clearPreview() then the same with restore if 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/restore errors 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) — the Viewport class implements preview/clearPreview directly; 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). Its entities handling calls render(entity) from @parashape/parametric/compute to turn each EntityJSON into 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-rendering entities items with the SAME render() the web host uses, so Ruby reuses the render.rb item pipeline — identical tessellation on both hosts. preview.rb interprets the items as transient construction geometry (construction points, edges, construction lines, bounding-box wireframes) inside a temp group and a transparent operation; preview-clear erases 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.