Plot-Board v2 is WorldScript's free-form scene canvas, replacing the swimlane-only view with three co-existing modes: Swimlane (Kanban), Canvas (free-form), and Timeline (Gantt-like). All modes share the same underlying data (StorySection[]) β no duplication.
State is split across two slices by mutability tier:
- NOT wrapped by
redux-undoβ pan/zoom actions should not enter the undo history. - Persisted to
localStorage(keyworldscript-plot-board) viaplotBoardPersistenceMiddleware. - Reset on page load for drawing state (
isDrawingConnection,drawFromSectionId,selectedConnectionId). - Holds:
activeMode,zoom,panX,panY,snapToGrid,selectedConnectionId,isDrawingConnection,drawFromSectionId,activeSubplotFilter.
- Wrapped by
redux-undo(100-step history) β Ctrl+Z restores deleted connections. - Persisted via IndexedDB (part of the project save).
- Holds:
plotConnections[],plotSubplots[],plotTensionOverrides(viaProjectDatafields). - Selectors from
features/project/projectSelectors.ts:selectPlotConnections,selectPlotSubplots,selectPlotTensionOverrides. - Actions:
addPlotConnection,updatePlotConnection,removePlotConnection,removePlotConnectionsForSection,finishPlotDrawConnection,addPlotSubplot,updatePlotSubplot,deletePlotSubplot,assignSectionToPlotSubplot,removeSectionFromPlotSubplot,setPlotTensionOverride,clearPlotTensionOverride,clearAllPlotTensionOverrides.
Scene content (title, summary, status, color, position) also stays in projectSlice and is undo-able.
| File | Responsibility |
|---|---|
components/scene-board/PlotCanvas.tsx |
Canvas wrapper β CSS transform pan/zoom, pointer events, mini-map |
components/scene-board/ConnectionLayer.tsx |
SVG bezier paths for plot connections (inside transform div) |
components/scene-board/ConnectionToolbar.tsx |
Floating toolbar for the selected connection |
components/scene-board/SubplotPanel.tsx |
Collapsible subplot sidebar |
components/scene-board/TensionCurvePanel.tsx |
Collapsible SVG tension chart |
components/scene-board/SceneCard.tsx |
Sortable scene card (swimlane mode) |
components/scene-board/ActSwimlane.tsx |
Act column in swimlane mode |
components/SceneBoardView.tsx |
Orchestrator β mode tabs, context provider |
| Type | Icon | Color | Curve style |
|---|---|---|---|
cause-effect |
β | #ef4444 |
Vertical bow bezier |
parallel |
β₯ | #3b82f6 |
Horizontal S-curve, dashed stroke |
subplot |
β | #8b5cf6 |
Vertical bow bezier |
temporal |
β | #f59e0b |
Vertical bow bezier |
character-arc |
β‘ | #10b981 |
Vertical bow bezier |
When a connection has a subplotId, it inherits the subplot's color (overrides type default).
- Click the draw-connection button on a card (or trigger via
plotBoardActions.startDrawConnection). isDrawingConnectionbecomestrue; the canvas cursor turns crosshair.- A dashed preview line tracks the cursor (canvas-space cursor position passed from
PlotCanvasβConnectionLayer). - Click a destination card β
projectActions.finishPlotDrawConnectionis dispatched (self-loops rejected), thenplotBoardActions.finishDrawConnectionclears the draw UI state. - Press Escape or change mode to cancel.
Subplots are stored as a plain Subplot[] array in projectSlice.data.plotSubplots (undo-able). Each subplot has a color and an array of sectionIds. The SubplotPanel dispatches projectActions:
addPlotSubplot/deletePlotSubplot/updatePlotSubplotassignSectionToPlotSubplot/removeSectionFromPlotSubplot
The active filter (viewport-only) stays in plotBoardSlice:
plotBoardActions.setActiveSubplotFilterβ dims canvas cards not in the active subplot
computeTensionCurve(sections, tensionOverrides) (in services/plotBoardService.ts) maps each section to a 0β10 score:
| Status | Auto-score |
|---|---|
draft |
2 |
outline |
3 |
first-draft |
5 |
revised |
7 |
final |
9 |
User can drag a dot to override. Overrides dispatch projectActions.setPlotTensionOverride (undo-able). Reset dispatches projectActions.clearAllPlotTensionOverrides.
Beat sheets are static SVG overlay markers β no AI calls, no new dependencies. Vertical dashed lines at proportional X positions. Available presets:
three-actβ 3 beats (Act 1 End, Midpoint, Act 2 End)save-the-catβ 15 beats at standard Blake Snyder positionshero's-journeyβ 12 stages (Campbell)
| Gesture | Action |
|---|---|
| One finger drag on background | Pan canvas |
| Two finger pinch | Zoom (0.25Γβ4Γ) |
| Two finger drag | Pan (via pinch midpoint) |
| Wheel | Zoom |
| β button | Toggle snap-to-grid (8px) |
Canvas pointer events use setPointerCapture for reliable drag even when the pointer leaves the element.
The canvas uses CSS transforms: translate(panX, panY) scale(zoom). To convert screen coordinates to canvas coordinates:
const canvasX = (screenX - panX) / zoom;
const canvasY = (screenY - panY) / zoom;ConnectionLayer is rendered inside the transformed div, so its SVG coordinates match card positions directly (no coordinate conversion needed).
- Max zoom: 4Γ / Min zoom: 0.25Γ (clamped in
setZoomreducer) - Max connections rendered comfortably: ~50 (no adapter, plain array)
- Canvas virtual size: 2400Γ1600px (configurable via
canvasBoundsstate inPlotCanvas) - Snap grid: 8px