@neta-art/cohub 9.0.1 → 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -102
- package/dist/board/editor/align.d.ts +11 -0
- package/dist/board/editor/align.js +72 -0
- package/dist/board/editor/animation.d.ts +17 -0
- package/dist/board/editor/animation.js +187 -0
- package/dist/board/editor/camera.d.ts +32 -0
- package/dist/board/editor/camera.js +137 -0
- package/dist/board/editor/clipboard.d.ts +41 -0
- package/dist/board/editor/clipboard.js +129 -0
- package/dist/board/editor/connection-ports.d.ts +19 -0
- package/dist/board/editor/connection-ports.js +85 -0
- package/dist/board/editor/context.d.ts +111 -0
- package/dist/board/editor/context.js +30 -0
- package/dist/board/editor/copy.d.ts +17 -0
- package/dist/board/editor/copy.js +121 -0
- package/dist/board/editor/creation.d.ts +51 -0
- package/dist/board/editor/creation.js +225 -0
- package/dist/board/editor/deletion.d.ts +9 -0
- package/dist/board/editor/deletion.js +90 -0
- package/dist/board/editor/document-edits.d.ts +22 -0
- package/dist/board/editor/document-edits.js +193 -0
- package/dist/board/editor/document.d.ts +41 -0
- package/dist/board/editor/document.js +302 -0
- package/dist/board/editor/editor.d.ts +225 -0
- package/dist/board/editor/editor.js +439 -0
- package/dist/board/editor/index.d.ts +18 -0
- package/dist/board/editor/index.js +12 -0
- package/dist/board/editor/interaction.d.ts +115 -0
- package/dist/board/editor/interaction.js +584 -0
- package/dist/board/editor/query.d.ts +27 -0
- package/dist/board/editor/query.js +203 -0
- package/dist/board/editor/remote.d.ts +11 -0
- package/dist/board/editor/remote.js +55 -0
- package/dist/board/editor/selection-transform.d.ts +24 -0
- package/dist/board/editor/selection-transform.js +81 -0
- package/dist/board/editor/selection.d.ts +33 -0
- package/dist/board/editor/selection.js +197 -0
- package/dist/board/editor/snapping.d.ts +33 -0
- package/dist/board/editor/snapping.js +75 -0
- package/dist/board/editor/snapshots.d.ts +27 -0
- package/dist/board/editor/snapshots.js +132 -0
- package/dist/board/editor/spatial.d.ts +41 -0
- package/dist/board/editor/spatial.js +191 -0
- package/dist/board/editor/state.d.ts +12 -0
- package/dist/board/editor/state.js +49 -0
- package/dist/board/editor/tool.d.ts +10 -0
- package/dist/board/editor/tool.js +19 -0
- package/dist/board/editor/wheel.d.ts +5 -0
- package/dist/board/editor/wheel.js +17 -0
- package/dist/board/export/index.d.ts +28 -6
- package/dist/board/export/index.js +47 -13
- package/dist/board/export/node/index.d.ts +56 -0
- package/dist/board/{headless → export/node}/index.js +26 -21
- package/dist/board/export/node/scripts.d.ts +16 -0
- package/dist/board/export/node/scripts.js +258 -0
- package/dist/board/export/scene.d.ts +8 -5
- package/dist/board/export/scene.js +24 -9
- package/dist/board/{animation.d.ts → model/animation.d.ts} +8 -5
- package/dist/board/{animation.js → model/animation.js} +37 -10
- package/dist/board/{core → model}/arrow-geometry.d.ts +2 -2
- package/dist/board/{core → model}/arrow-geometry.js +2 -2
- package/dist/board/model/authoring.d.ts +20 -0
- package/dist/board/model/authoring.js +185 -0
- package/dist/board/{core → model}/draw-geometry.d.ts +2 -2
- package/dist/board/{core → model}/draw-geometry.js +1 -1
- package/dist/board/model/draw-input.d.ts +14 -0
- package/dist/board/model/draw-input.js +28 -0
- package/dist/board/{core → model}/export-assets.d.ts +1 -1
- package/dist/board/{core → model}/export-assets.js +1 -1
- package/dist/board/{core → model}/export-plan.d.ts +2 -2
- package/dist/board/{core → model}/export-plan.js +3 -2
- package/dist/board/{core → model}/file-preview.d.ts +1 -1
- package/dist/board/{core → model}/file-preview.js +2 -2
- package/dist/board/{core → model}/file-snapshot.d.ts +1 -1
- package/dist/board/{core → model}/file-snapshot.js +4 -3
- package/dist/board/{geometry.d.ts → model/geometry.d.ts} +3 -3
- package/dist/board/{geometry.js → model/geometry.js} +1 -1
- package/dist/board/{image-key.d.ts → model/image-key.d.ts} +2 -2
- package/dist/board/{image-key.js → model/image-key.js} +7 -3
- package/dist/board/model/index.d.ts +42 -0
- package/dist/board/model/index.js +39 -0
- package/dist/board/model/items/builtin.d.ts +5 -0
- package/dist/board/model/items/builtin.js +119 -0
- package/dist/board/model/items/create.d.ts +61 -0
- package/dist/board/model/items/create.js +272 -0
- package/dist/board/model/items/definition.d.ts +35 -0
- package/dist/board/model/items/definition.js +18 -0
- package/dist/board/model/items/id.d.ts +5 -0
- package/dist/board/model/items/id.js +11 -0
- package/dist/board/model/items/index.d.ts +6 -0
- package/dist/board/model/items/index.js +6 -0
- package/dist/board/model/items/registry.d.ts +35 -0
- package/dist/board/model/items/registry.js +84 -0
- package/dist/board/{media-playback.d.ts → model/media-playback.d.ts} +4 -3
- package/dist/board/{media-playback.js → model/media-playback.js} +15 -5
- package/dist/board/{media.d.ts → model/media.d.ts} +1 -1
- package/dist/board/{media.js → model/media.js} +8 -2
- package/dist/board/{core → model}/palette.d.ts +1 -1
- package/dist/board/{core → model}/palette.js +1 -1
- package/dist/board/{presets.d.ts → model/presets.d.ts} +3 -3
- package/dist/board/{presets.js → model/presets.js} +1 -1
- package/dist/board/{replay.d.ts → model/replay.d.ts} +5 -5
- package/dist/board/{replay.js → model/replay.js} +2 -2
- package/dist/board/model/sampling.d.ts +19 -0
- package/dist/board/model/sampling.js +96 -0
- package/dist/board/{core → model}/scene.d.ts +1 -1
- package/dist/board/{core → model}/scene.js +1 -1
- package/dist/board/model/script-example.d.ts +4 -0
- package/dist/board/model/script-example.js +21 -0
- package/dist/board/model/script-hit.d.ts +10 -0
- package/dist/board/model/script-hit.js +52 -0
- package/dist/board/model/script-index.d.ts +7 -0
- package/dist/board/model/script-index.js +15 -0
- package/dist/board/model/script-scene.d.ts +24 -0
- package/dist/board/model/script-scene.js +267 -0
- package/dist/board/model/script.d.ts +105 -0
- package/dist/board/model/script.js +1 -0
- package/dist/board/model/shape-types.d.ts +8 -0
- package/dist/board/model/shape-types.js +8 -0
- package/dist/board/{task.d.ts → model/task.d.ts} +4 -4
- package/dist/board/{task.js → model/task.js} +3 -3
- package/dist/board/{core → model}/text-metrics.d.ts +1 -1
- package/dist/board/{core → model}/text-metrics.js +1 -1
- package/dist/board/{core → model}/tool-styles.d.ts +1 -1
- package/dist/board/{core → model}/tool-styles.js +1 -1
- package/dist/board/{runtime/board-player.d.ts → player/index.d.ts} +2 -2
- package/dist/board/{runtime/board-player.js → player/index.js} +5 -4
- package/dist/board/render/board-background.d.ts +1 -1
- package/dist/board/render/clip.d.ts +1 -1
- package/dist/board/render/css-color.d.ts +6 -1
- package/dist/board/render/css-color.js +34 -20
- package/dist/board/render/diagnostics.d.ts +20 -0
- package/dist/board/render/diagnostics.js +117 -0
- package/dist/board/render/index.d.ts +9 -6
- package/dist/board/render/index.js +10 -7
- package/dist/board/render/media-interaction.d.ts +2 -2
- package/dist/board/render/media-interaction.js +1 -1
- package/dist/board/render/palette.d.ts +3 -2
- package/dist/board/render/palette.js +7 -3
- package/dist/board/render/renderers/arrow-card-renderer.js +4 -3
- package/dist/board/render/renderers/audio-card-renderer.js +2 -1
- package/dist/board/render/renderers/board-renderer-registry.d.ts +20 -10
- package/dist/board/render/renderers/board-renderer-registry.js +17 -17
- package/dist/board/render/renderers/custom-card-renderer.d.ts +5 -0
- package/dist/board/render/renderers/{sketch-card-renderer.js → custom-card-renderer.js} +16 -15
- package/dist/board/render/renderers/draw-card-renderer.js +4 -3
- package/dist/board/render/renderers/effect-card-renderer.js +22 -5
- package/dist/board/render/renderers/far-plate.js +1 -1
- package/dist/board/render/renderers/file-card-renderer.d.ts +1 -1
- package/dist/board/render/renderers/file-card-renderer.js +2 -2
- package/dist/board/render/renderers/frame-card-renderer.js +30 -14
- package/dist/board/render/renderers/image-card-renderer.d.ts +1 -1
- package/dist/board/render/renderers/image-card-renderer.js +43 -4
- package/dist/board/render/renderers/shape-card-renderer.js +11 -28
- package/dist/board/render/renderers/task-card-renderer.js +1 -1
- package/dist/board/render/renderers/text-card-renderer.js +6 -4
- package/dist/board/render/renderers/unknown-card-renderer.js +1 -0
- package/dist/board/render/renderers/video-card-renderer.d.ts +1 -1
- package/dist/board/render/renderers/video-card-renderer.js +2 -1
- package/dist/board/render/sampling.d.ts +22 -0
- package/dist/board/render/sampling.js +21 -0
- package/dist/board/render/script-export.d.ts +11 -0
- package/dist/board/render/script-export.js +86 -0
- package/dist/board/render/script-host.d.ts +31 -0
- package/dist/board/render/script-host.js +421 -0
- package/dist/board/render/script-modules.d.ts +2 -0
- package/dist/board/render/script-modules.js +2 -0
- package/dist/board/render/script-runtime.d.ts +29 -0
- package/dist/board/render/script-runtime.js +213 -0
- package/dist/board/render/script-sandbox.d.ts +4 -0
- package/dist/board/render/script-sandbox.js +173 -0
- package/dist/board/render/stroke.d.ts +3 -2
- package/dist/board/render/stroke.js +23 -2
- package/dist/board/render/text-measurement.js +2 -2
- package/dist/board/replica/index.d.ts +70 -0
- package/dist/board/replica/index.js +276 -0
- package/dist/board/stage/assets.d.ts +41 -0
- package/dist/board/stage/assets.js +355 -0
- package/dist/board/stage/index.d.ts +8 -0
- package/dist/board/stage/index.js +8 -0
- package/dist/board/stage/lru.d.ts +32 -0
- package/dist/board/stage/lru.js +29 -0
- package/dist/board/stage/maintenance.d.ts +5 -0
- package/dist/board/stage/maintenance.js +11 -0
- package/dist/board/stage/pointer.d.ts +8 -0
- package/dist/board/stage/pointer.js +33 -0
- package/dist/board/stage/scene.d.ts +85 -0
- package/dist/board/stage/scene.js +515 -0
- package/dist/board/stage/script-pointer.d.ts +19 -0
- package/dist/board/stage/script-pointer.js +35 -0
- package/dist/board/stage/stage.d.ts +76 -0
- package/dist/board/stage/stage.js +774 -0
- package/dist/board/stage/texture-pool.d.ts +24 -0
- package/dist/board/stage/texture-pool.js +153 -0
- package/dist/board/stage/views.d.ts +38 -0
- package/dist/board/stage/views.js +93 -0
- package/dist/chunks/environment.d.ts +289 -23
- package/dist/chunks/host-bridge.d.ts +2 -2
- package/dist/chunks/http.d.ts +185 -71
- package/dist/chunks/http.js +368 -269
- package/dist/chunks/transport.js +3 -1
- package/dist/chunks/websocket.d.ts +9 -1
- package/dist/chunks/websocket.js +16 -2
- package/dist/host-bridge.js +2 -1
- package/dist/http.d.ts +2 -2
- package/dist/index.d.ts +182 -4
- package/dist/index.js +1393 -129
- package/dist/media.d.ts +1 -10
- package/dist/media.js +16 -27
- package/dist/protocol/dist/board-assets.d.ts +1 -0
- package/dist/protocol/dist/board-constants.js +2 -2
- package/dist/protocol/dist/board-model.d.ts +201 -11
- package/dist/protocol/dist/board-model.js +33 -14
- package/dist/protocol/dist/board-patch.js +3 -2
- package/dist/protocol/dist/board-schema.d.ts +4 -2
- package/dist/protocol/dist/board-schema.js +18 -4
- package/dist/protocol/dist/board-script-modules.d.ts +25 -0
- package/dist/protocol/dist/board-script-modules.js +127 -0
- package/dist/protocol/dist/board.d.ts +28 -2
- package/dist/protocol/dist/board.js +3 -1
- package/dist/protocol/dist/core/media.d.ts +5 -0
- package/dist/protocol/dist/core/media.js +39 -0
- package/dist/protocol/dist/display/index.d.ts +1 -0
- package/dist/protocol/dist/index.d.ts +7 -5
- package/dist/protocol/dist/realtime/types.d.ts +1 -0
- package/dist/protocol/dist/rtc/index.d.ts +1 -0
- package/dist/types.d.ts +1 -1
- package/package.json +21 -8
- package/dist/board/core/shape-definition.d.ts +0 -23
- package/dist/board/core/shape-definition.js +0 -45
- package/dist/board/core/shape-types.d.ts +0 -38
- package/dist/board/core/shape-types.js +0 -22
- package/dist/board/headless/index.d.ts +0 -55
- package/dist/board/headless/sketch.d.ts +0 -19
- package/dist/board/headless/sketch.js +0 -146
- package/dist/board/index.d.ts +0 -30
- package/dist/board/index.js +0 -29
- package/dist/board/render/renderers/sketch-card-renderer.d.ts +0 -5
- package/dist/board/render/sketch-host.d.ts +0 -14
- package/dist/board/render/sketch-host.js +0 -216
package/README.md
CHANGED
|
@@ -76,138 +76,144 @@ await session.messages.send({
|
|
|
76
76
|
|
|
77
77
|
## Boards
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
`space.board(boardId)
|
|
79
|
+
A Board is one document of `board` settings, `items` and `animations`. Bind
|
|
80
|
+
one with `space.board(boardId)`; every write is a JSON Merge Patch (`null`
|
|
81
|
+
deletes, unmentioned fields stay):
|
|
81
82
|
|
|
82
83
|
```ts
|
|
83
84
|
const created = await space.boards.create({
|
|
84
85
|
path: "boards/plan.board",
|
|
85
86
|
title: "Plan",
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
style: { color: "green" },
|
|
92
|
-
}],
|
|
87
|
+
document: {
|
|
88
|
+
items: {
|
|
89
|
+
goal: { type: "shape", position: { x: 80, y: 80 }, size: { width: 240, height: 120 }, props: { geometry: "rounded", text: "Ship" } },
|
|
90
|
+
},
|
|
91
|
+
},
|
|
93
92
|
});
|
|
94
93
|
|
|
95
|
-
const board = space.board(created.
|
|
96
|
-
|
|
94
|
+
const board = space.board(created.id);
|
|
95
|
+
const { items, version } = await board.get({ only: ["items"] });
|
|
96
|
+
await board.apply({ items: { goal: { props: { text: "Shipped" } } } }, { baseVersion: version });
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
const stop = board.subscribe({
|
|
99
|
+
changed: (event) => console.log("version", event.payload.version),
|
|
100
|
+
playback: (event) => console.log("playback", event.payload.playback?.status),
|
|
101
|
+
});
|
|
102
|
+
```
|
|
100
103
|
|
|
101
|
-
|
|
104
|
+
Task Items keep a small, replaceable display snapshot beside their stable
|
|
105
|
+
`taskRunId`; build it from an authoritative TaskRun with
|
|
106
|
+
`taskRunToBoardTaskSnapshot(taskRun)` from `@neta-art/cohub/board`, and restore
|
|
107
|
+
a Board's TaskRuns in one request with `client.tasks.getMany(ids, { spaceId })`.
|
|
102
108
|
|
|
103
|
-
|
|
104
|
-
baseVersion: snapshot.board.version,
|
|
105
|
-
commands: [{
|
|
106
|
-
type: "item.patch",
|
|
107
|
-
itemId: "goal",
|
|
108
|
-
patch: { props: { text: "Updated plan" } },
|
|
109
|
-
}],
|
|
110
|
-
});
|
|
109
|
+
### Layers
|
|
111
110
|
|
|
112
|
-
|
|
113
|
-
commandId: crypto.randomUUID(),
|
|
114
|
-
type: "play",
|
|
115
|
-
compositionId: "ambient",
|
|
116
|
-
});
|
|
117
|
-
```
|
|
111
|
+
Board is layered by dependency, so each use pulls in only what it needs:
|
|
118
112
|
|
|
119
|
-
|
|
120
|
-
|
|
113
|
+
| Import | What it is | Needs |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| `@neta-art/cohub/board` | Document model, geometry, item types (`defineBoardItem`, `createBoardRegistry`), export planning | — |
|
|
116
|
+
| `@neta-art/cohub/board/replica` | A local copy kept in step with the server, with offline writes | — |
|
|
117
|
+
| `@neta-art/cohub/board/editor` | The editor engine: selection, tools, gestures, undo, camera | — |
|
|
118
|
+
| `@neta-art/cohub/board/player` | Animation playback | — |
|
|
119
|
+
| `@neta-art/cohub/board/render` | Canvas renderers for each item type | `pixi.js` |
|
|
120
|
+
| `@neta-art/cohub/board/stage` | An editor drawn into a DOM element, with input and DOM item views | `pixi.js` |
|
|
121
|
+
| `@neta-art/cohub/board/export` | Render a document to an image | `pixi.js` |
|
|
122
|
+
| `@neta-art/cohub/board/export/node` | The same in Node.js | `pixi.js`, `@napi-rs/canvas` |
|
|
121
123
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
changed(event) {
|
|
125
|
-
console.log("version", event.payload.version);
|
|
126
|
-
console.log("items", event.payload.changed.items);
|
|
127
|
-
},
|
|
128
|
-
playback(event) {
|
|
129
|
-
console.log("playback", event.payload.status);
|
|
130
|
-
},
|
|
131
|
-
});
|
|
124
|
+
`pixi.js` and `@napi-rs/canvas` are optional peers: HTTP-only installs, agents
|
|
125
|
+
and servers never load them.
|
|
132
126
|
|
|
133
|
-
|
|
134
|
-
```
|
|
127
|
+
### Embedding an editor
|
|
135
128
|
|
|
136
|
-
|
|
129
|
+
The editor has no framework. Read state through its getters and listen with
|
|
130
|
+
`subscribe(channel, listener)`; snapshots keep their identity until they
|
|
131
|
+
change, so they work directly with `useSyncExternalStore`:
|
|
137
132
|
|
|
138
133
|
```ts
|
|
139
|
-
import {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
134
|
+
import { createBoardRegistry, defineBoardItem, worldPoint } from "@neta-art/cohub/board";
|
|
135
|
+
import { createBoardEditor } from "@neta-art/cohub/board/editor";
|
|
136
|
+
import { createBoardReplica } from "@neta-art/cohub/board/replica";
|
|
137
|
+
import { createBoardAssetManager, mountBoardStage } from "@neta-art/cohub/board/stage";
|
|
138
|
+
import { z } from "zod";
|
|
139
|
+
|
|
140
|
+
// A business item type. The server stores extension props as they are; the
|
|
141
|
+
// schema is checked when the editor creates or edits one.
|
|
142
|
+
const order = defineBoardItem({
|
|
143
|
+
type: "acme.order",
|
|
144
|
+
props: z.object({ orderId: z.string(), status: z.enum(["open", "paid"]) }),
|
|
145
|
+
size: { width: 320, height: 200 },
|
|
146
|
+
capabilities: { canRotate: false },
|
|
147
|
+
});
|
|
143
148
|
|
|
144
|
-
|
|
149
|
+
const replica = createBoardReplica({ remote: space.board(boardId) });
|
|
150
|
+
await replica.start();
|
|
145
151
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
152
|
+
const editor = createBoardEditor({
|
|
153
|
+
document: replica.state.document!,
|
|
154
|
+
registry: createBoardRegistry([order]),
|
|
155
|
+
key: boardId,
|
|
156
|
+
onCommit: (patch) => replica.apply(patch),
|
|
157
|
+
});
|
|
158
|
+
replica.subscribe((state) => state.document && editor.loadDocument(state.document, boardId));
|
|
159
|
+
|
|
160
|
+
// Prefer stable media URLs. Existing Space paths remain supported by addFile.
|
|
161
|
+
editor.addMedia("https://cdn.example.com/cover.webp", worldPoint(0, 0));
|
|
162
|
+
// Supply a kind for sources without a file extension.
|
|
163
|
+
editor.addMedia("https://cdn.example.com/media?id=42", worldPoint(400, 0), {
|
|
164
|
+
kind: "video",
|
|
165
|
+
snapshot: { title: "Launch", naturalWidth: 1920, naturalHeight: 1080 },
|
|
166
|
+
});
|
|
151
167
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
{ time: 0, value: { x: 0, y: 0 } },
|
|
172
|
-
{ time: 500, value: { x: 0, y: -8 } },
|
|
173
|
-
{ time: 1_000, value: { x: 0, y: 0 } },
|
|
174
|
-
],
|
|
168
|
+
// Media URLs load directly; these callbacks only receive Space paths.
|
|
169
|
+
// A published copy can resolve paths against captured artifacts instead.
|
|
170
|
+
const source = {
|
|
171
|
+
resolveFileUrl: (path: string) => space.files.resolveUrl(path, { purpose: "preview" }),
|
|
172
|
+
resolvePlaybackUrl: (path: string) => space.files.resolveUrl(path, { purpose: "playback" }),
|
|
173
|
+
};
|
|
174
|
+
const stage = mountBoardStage(element, {
|
|
175
|
+
editor,
|
|
176
|
+
source,
|
|
177
|
+
assets: createBoardAssetManager({ resolveFileUrl: source.resolveFileUrl }),
|
|
178
|
+
views: [{
|
|
179
|
+
type: "acme.order",
|
|
180
|
+
// Mount anything: a React root, a Vue app, plain DOM.
|
|
181
|
+
mount: (target, item, context) => {
|
|
182
|
+
const root = createRoot(target);
|
|
183
|
+
const render = (next = item, ctx = context) => root.render(<OrderCard item={next} selected={ctx.selected} />);
|
|
184
|
+
render();
|
|
185
|
+
return { update: render, destroy: () => root.unmount() };
|
|
186
|
+
},
|
|
175
187
|
}],
|
|
176
|
-
playback: { loop: true, endBehavior: "hold", reducedMotion: { mode: "base" } },
|
|
177
188
|
});
|
|
178
189
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
playback: { compositionId: composition.id, delayMs: 500 },
|
|
183
|
-
},
|
|
184
|
-
compositions: [composition],
|
|
185
|
-
});
|
|
190
|
+
editor.addItem("acme.order", { at: editor.viewCenter(), props: { orderId: "o-1", status: "open" } });
|
|
191
|
+
|
|
192
|
+
// React: const selection = useSyncExternalStore((fn) => editor.subscribe("selection", fn), () => editor.selection);
|
|
186
193
|
```
|
|
187
194
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
195
|
+
Items of a type the client does not know still render as a placeholder, move
|
|
196
|
+
and keep every field, so documents written by newer clients are never
|
|
197
|
+
rewritten by older ones. For a canvas look instead of DOM, pass renderers to
|
|
198
|
+
`createBoardCardRendererResolver` from `board/render` and hand the result to
|
|
199
|
+
the stage or to `renderBoardExport` as `renderers`.
|
|
200
|
+
|
|
201
|
+
### Images
|
|
202
|
+
|
|
203
|
+
In the browser, `renderBoardExport(renderer, document, options)` from
|
|
204
|
+
`board/export` draws with the page's renderer (`stage.renderer`). In Node.js:
|
|
191
205
|
|
|
192
206
|
```ts
|
|
193
|
-
import {
|
|
194
|
-
import { renderBoardExport } from "@neta-art/cohub/board/export";
|
|
195
|
-
import {
|
|
196
|
-
createBoardHeadlessRenderer,
|
|
197
|
-
exportBoardImageBytes,
|
|
198
|
-
} from "@neta-art/cohub/board/headless";
|
|
199
|
-
```
|
|
207
|
+
import { createNodeBoardRenderer, exportBoardImageBytes } from "@neta-art/cohub/board/export/node";
|
|
200
208
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
stay lightweight.
|
|
209
|
+
const renderer = await createNodeBoardRenderer();
|
|
210
|
+
const result = exportBoardImageBytes(renderer, document, { format: "png", scale: 2 });
|
|
211
|
+
```
|
|
205
212
|
|
|
206
213
|
Text metrics follow the same split. The renderers measure through a real canvas
|
|
207
|
-
and set that up themselves
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
out a board on a server, but call `installBoardTextMeasurement` from
|
|
214
|
+
and set that up themselves. Called straight off `@neta-art/cohub/board` with no
|
|
215
|
+
renderer in play, `measureBoardText` returns a per-character estimate — fine
|
|
216
|
+
for laying out a board on a server; call `installBoardTextMeasurement` from
|
|
211
217
|
`board/render` first if the numbers have to match what the editor draws.
|
|
212
218
|
|
|
213
219
|
## Session subscriptions
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { BoardFrame } from "../../protocol/dist/board-layout.js";
|
|
2
|
+
import { Rect } from "../model/geometry.js";
|
|
3
|
+
import "../model/index.js";
|
|
4
|
+
//#region src/board/editor/align.d.ts
|
|
5
|
+
type AlignMode = "left" | "center-x" | "right" | "top" | "center-y" | "bottom";
|
|
6
|
+
type DistributeAxis = "horizontal" | "vertical";
|
|
7
|
+
declare function alignFrames(frames: Map<string, BoardFrame>, mode: AlignMode): Map<string, BoardFrame>;
|
|
8
|
+
declare function distributeFrames(frames: Map<string, BoardFrame>, axis: DistributeAxis): Map<string, BoardFrame>;
|
|
9
|
+
declare function framesBounds(frames: Iterable<BoardFrame>): Rect | null;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { AlignMode, DistributeAxis, alignFrames, distributeFrames, framesBounds };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { itemBounds, selectionBounds } from "../model/geometry.js";
|
|
2
|
+
//#region src/board/editor/align.ts
|
|
3
|
+
function alignFrames(frames, mode) {
|
|
4
|
+
if (frames.size < 2) return /* @__PURE__ */ new Map();
|
|
5
|
+
const bounds = selectionBounds([...frames.values()]);
|
|
6
|
+
if (!bounds) return /* @__PURE__ */ new Map();
|
|
7
|
+
const next = /* @__PURE__ */ new Map();
|
|
8
|
+
for (const [id, frame] of frames) {
|
|
9
|
+
const box = itemBounds(frame);
|
|
10
|
+
let x = frame.x;
|
|
11
|
+
let y = frame.y;
|
|
12
|
+
switch (mode) {
|
|
13
|
+
case "left":
|
|
14
|
+
x = frame.x + (bounds.x - box.x);
|
|
15
|
+
break;
|
|
16
|
+
case "center-x":
|
|
17
|
+
x = frame.x + (bounds.x + bounds.width / 2 - (box.x + box.width / 2));
|
|
18
|
+
break;
|
|
19
|
+
case "right":
|
|
20
|
+
x = frame.x + (bounds.x + bounds.width - (box.x + box.width));
|
|
21
|
+
break;
|
|
22
|
+
case "top":
|
|
23
|
+
y = frame.y + (bounds.y - box.y);
|
|
24
|
+
break;
|
|
25
|
+
case "center-y":
|
|
26
|
+
y = frame.y + (bounds.y + bounds.height / 2 - (box.y + box.height / 2));
|
|
27
|
+
break;
|
|
28
|
+
case "bottom": y = frame.y + (bounds.y + bounds.height - (box.y + box.height));
|
|
29
|
+
}
|
|
30
|
+
if (x !== frame.x || y !== frame.y) next.set(id, {
|
|
31
|
+
...frame,
|
|
32
|
+
x,
|
|
33
|
+
y
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return next;
|
|
37
|
+
}
|
|
38
|
+
function distributeFrames(frames, axis) {
|
|
39
|
+
if (frames.size < 3) return /* @__PURE__ */ new Map();
|
|
40
|
+
const entries = [...frames.entries()].map(([id, frame]) => ({
|
|
41
|
+
id,
|
|
42
|
+
frame,
|
|
43
|
+
box: itemBounds(frame)
|
|
44
|
+
}));
|
|
45
|
+
const horizontal = axis === "horizontal";
|
|
46
|
+
entries.sort((a, b) => horizontal ? a.box.x - b.box.x : a.box.y - b.box.y);
|
|
47
|
+
const first = entries[0];
|
|
48
|
+
const last = entries[entries.length - 1];
|
|
49
|
+
if (!first || !last) return /* @__PURE__ */ new Map();
|
|
50
|
+
const start = horizontal ? first.box.x : first.box.y;
|
|
51
|
+
const end = horizontal ? last.box.x + last.box.width : last.box.y + last.box.height;
|
|
52
|
+
const totalSize = entries.reduce((sum, entry) => sum + (horizontal ? entry.box.width : entry.box.height), 0);
|
|
53
|
+
const gap = (end - start - totalSize) / (entries.length - 1);
|
|
54
|
+
const next = /* @__PURE__ */ new Map();
|
|
55
|
+
let cursor = start;
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const size = horizontal ? entry.box.width : entry.box.height;
|
|
58
|
+
const delta = cursor - (horizontal ? entry.box.x : entry.box.y);
|
|
59
|
+
if (delta !== 0) next.set(entry.id, {
|
|
60
|
+
...entry.frame,
|
|
61
|
+
x: horizontal ? entry.frame.x + delta : entry.frame.x,
|
|
62
|
+
y: horizontal ? entry.frame.y : entry.frame.y + delta
|
|
63
|
+
});
|
|
64
|
+
cursor += size + gap;
|
|
65
|
+
}
|
|
66
|
+
return next;
|
|
67
|
+
}
|
|
68
|
+
function framesBounds(frames) {
|
|
69
|
+
return selectionBounds([...frames]);
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
export { alignFrames, distributeFrames, framesBounds };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { BoardChapter } from "../model/authoring.js";
|
|
2
|
+
import "../model/index.js";
|
|
3
|
+
import { BoardPlayhead, EditorContext } from "./context.js";
|
|
4
|
+
//#region src/board/editor/animation.d.ts
|
|
5
|
+
declare function createAnimationModule(ctx: EditorContext): {
|
|
6
|
+
setPlayhead: (next: BoardPlayhead | null) => void;
|
|
7
|
+
setRecording: (value: boolean) => void;
|
|
8
|
+
keyframeState: (property: string) => "none" | "animated" | "keyframe";
|
|
9
|
+
toggleKeyframe: (property: string) => void;
|
|
10
|
+
removeKeyframe: (trackId: string, at: number) => void;
|
|
11
|
+
setKeyframeEase: (trackId: string, at: number, ease: string | null) => void;
|
|
12
|
+
createAnimation: (name: string, duration?: number) => string;
|
|
13
|
+
createSequence: (name: string, chapters: BoardChapter[]) => string | null;
|
|
14
|
+
};
|
|
15
|
+
type AnimationModule = ReturnType<typeof createAnimationModule>;
|
|
16
|
+
//#endregion
|
|
17
|
+
export { AnimationModule, createAnimationModule };
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { boardSequence } from "../model/authoring.js";
|
|
2
|
+
import { createBoardItemId } from "../model/items/id.js";
|
|
3
|
+
import { readPath, sameJson } from "./document-edits.js";
|
|
4
|
+
//#region src/board/editor/animation.ts
|
|
5
|
+
function createAnimationModule(ctx) {
|
|
6
|
+
const { state, options } = ctx;
|
|
7
|
+
function setPlayhead(next) {
|
|
8
|
+
if (next && !state.base.animations[next.animationId]) next = null;
|
|
9
|
+
if (sameJson(next, state.playhead)) return;
|
|
10
|
+
state.playhead = next;
|
|
11
|
+
if (!next) state.recording = false;
|
|
12
|
+
ctx.document.discardDraft();
|
|
13
|
+
}
|
|
14
|
+
function setRecording(value) {
|
|
15
|
+
state.recording = value && state.playhead !== null && !options.readonly;
|
|
16
|
+
}
|
|
17
|
+
function current() {
|
|
18
|
+
const { playhead, base } = state;
|
|
19
|
+
const animation = playhead ? base.animations[playhead.animationId] : void 0;
|
|
20
|
+
return playhead && animation ? {
|
|
21
|
+
playhead,
|
|
22
|
+
animation
|
|
23
|
+
} : null;
|
|
24
|
+
}
|
|
25
|
+
function tracksFor(itemId) {
|
|
26
|
+
const result = /* @__PURE__ */ new Map();
|
|
27
|
+
const animation = current()?.animation;
|
|
28
|
+
if (!animation) return result;
|
|
29
|
+
for (const [trackId, track] of Object.entries(animation.tracks)) if (track.target === itemId) result.set(track.property, [trackId, track]);
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
function keyframeState(property) {
|
|
33
|
+
const selected = ctx.selection.items();
|
|
34
|
+
if (!state.playhead || selected.length === 0) return "none";
|
|
35
|
+
const time = Math.round(state.playhead.time);
|
|
36
|
+
let animated = true;
|
|
37
|
+
let keyed = true;
|
|
38
|
+
for (const item of selected) {
|
|
39
|
+
const track = tracksFor(item.id).get(property)?.[1];
|
|
40
|
+
if (!track) animated = false;
|
|
41
|
+
if (!track?.keyframes.some((keyframe) => keyframe.at === time)) keyed = false;
|
|
42
|
+
}
|
|
43
|
+
return keyed ? "keyframe" : animated ? "animated" : "none";
|
|
44
|
+
}
|
|
45
|
+
function commitAnimation(animationId, animation) {
|
|
46
|
+
ctx.document.commit({ animations: {
|
|
47
|
+
...state.base.animations,
|
|
48
|
+
[animationId]: animation
|
|
49
|
+
} });
|
|
50
|
+
}
|
|
51
|
+
function toggleKeyframe(property) {
|
|
52
|
+
const found = options.readonly ? null : current();
|
|
53
|
+
if (!found) return;
|
|
54
|
+
const { playhead, animation } = found;
|
|
55
|
+
const time = Math.round(playhead.time);
|
|
56
|
+
const tracks = { ...animation.tracks };
|
|
57
|
+
const remove = keyframeState(property) === "keyframe";
|
|
58
|
+
for (const item of ctx.selection.items()) {
|
|
59
|
+
const existing = tracksFor(item.id).get(property);
|
|
60
|
+
const shown = readPath(ctx.document.displayItem(item.id), property.split("."));
|
|
61
|
+
if (remove && existing) {
|
|
62
|
+
const keyframes = existing[1].keyframes.filter((keyframe) => keyframe.at !== time);
|
|
63
|
+
if (keyframes.length) tracks[existing[0]] = {
|
|
64
|
+
...existing[1],
|
|
65
|
+
keyframes
|
|
66
|
+
};
|
|
67
|
+
else delete tracks[existing[0]];
|
|
68
|
+
} else if (!remove) {
|
|
69
|
+
const [trackId, track] = existing ?? [`${item.id}-${property.replaceAll(".", "-")}`, {
|
|
70
|
+
target: item.id,
|
|
71
|
+
property,
|
|
72
|
+
keyframes: [],
|
|
73
|
+
composite: "replace",
|
|
74
|
+
interpolation: "auto"
|
|
75
|
+
}];
|
|
76
|
+
const keyframes = [...track.keyframes.filter((keyframe) => keyframe.at !== time), {
|
|
77
|
+
at: time,
|
|
78
|
+
value: shown
|
|
79
|
+
}].sort((a, b) => a.at - b.at);
|
|
80
|
+
tracks[trackId] = {
|
|
81
|
+
...track,
|
|
82
|
+
keyframes
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
commitAnimation(playhead.animationId, {
|
|
87
|
+
...animation,
|
|
88
|
+
duration: Math.max(animation.duration, time),
|
|
89
|
+
tracks
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function trackAt(trackId) {
|
|
93
|
+
const found = options.readonly ? null : current();
|
|
94
|
+
const track = found?.animation.tracks[trackId];
|
|
95
|
+
return found && track ? {
|
|
96
|
+
...found,
|
|
97
|
+
track
|
|
98
|
+
} : null;
|
|
99
|
+
}
|
|
100
|
+
function removeKeyframe(trackId, at) {
|
|
101
|
+
const found = trackAt(trackId);
|
|
102
|
+
if (!found) return;
|
|
103
|
+
const { playhead, animation, track } = found;
|
|
104
|
+
const time = Math.round(at);
|
|
105
|
+
const keyframes = track.keyframes.filter((keyframe) => keyframe.at !== time);
|
|
106
|
+
const tracks = { ...animation.tracks };
|
|
107
|
+
if (keyframes.length) tracks[trackId] = {
|
|
108
|
+
...track,
|
|
109
|
+
keyframes
|
|
110
|
+
};
|
|
111
|
+
else delete tracks[trackId];
|
|
112
|
+
commitAnimation(playhead.animationId, {
|
|
113
|
+
...animation,
|
|
114
|
+
tracks
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function setKeyframeEase(trackId, at, ease) {
|
|
118
|
+
const found = trackAt(trackId);
|
|
119
|
+
if (!found) return;
|
|
120
|
+
const { playhead, animation, track } = found;
|
|
121
|
+
const time = Math.round(at);
|
|
122
|
+
const keyframes = track.keyframes.map((keyframe) => {
|
|
123
|
+
if (keyframe.at !== time) return keyframe;
|
|
124
|
+
if (!ease || ease === "linear") {
|
|
125
|
+
const { ease: _removed, ...rest } = keyframe;
|
|
126
|
+
return rest;
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
...keyframe,
|
|
130
|
+
ease
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
commitAnimation(playhead.animationId, {
|
|
134
|
+
...animation,
|
|
135
|
+
tracks: {
|
|
136
|
+
...animation.tracks,
|
|
137
|
+
[trackId]: {
|
|
138
|
+
...track,
|
|
139
|
+
keyframes
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function createAnimation(name, duration = 5e3) {
|
|
145
|
+
const id = createBoardItemId();
|
|
146
|
+
commitAnimation(id, {
|
|
147
|
+
name,
|
|
148
|
+
duration,
|
|
149
|
+
play: "manual",
|
|
150
|
+
delay: 0,
|
|
151
|
+
loop: false,
|
|
152
|
+
end: "hold",
|
|
153
|
+
markers: [],
|
|
154
|
+
tracks: {}
|
|
155
|
+
});
|
|
156
|
+
setPlayhead({
|
|
157
|
+
animationId: id,
|
|
158
|
+
time: 0
|
|
159
|
+
});
|
|
160
|
+
return id;
|
|
161
|
+
}
|
|
162
|
+
function createSequence(name, chapters) {
|
|
163
|
+
if (options.readonly) return null;
|
|
164
|
+
const id = createBoardItemId();
|
|
165
|
+
commitAnimation(id, {
|
|
166
|
+
...boardSequence(state.base, chapters),
|
|
167
|
+
name
|
|
168
|
+
});
|
|
169
|
+
setPlayhead({
|
|
170
|
+
animationId: id,
|
|
171
|
+
time: 0
|
|
172
|
+
});
|
|
173
|
+
return id;
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
setPlayhead,
|
|
177
|
+
setRecording,
|
|
178
|
+
keyframeState,
|
|
179
|
+
toggleKeyframe,
|
|
180
|
+
removeKeyframe,
|
|
181
|
+
setKeyframeEase,
|
|
182
|
+
createAnimation,
|
|
183
|
+
createSequence
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
//#endregion
|
|
187
|
+
export { createAnimationModule };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { BoardViewport, Rect, ScreenPoint, WorldPoint } from "../model/geometry.js";
|
|
2
|
+
import "../model/index.js";
|
|
3
|
+
import { BoardCameraPolicy, EditorContext } from "./context.js";
|
|
4
|
+
//#region src/board/editor/camera.d.ts
|
|
5
|
+
type BoardFocusOptions = {
|
|
6
|
+
fit?: "contain" | "cover";
|
|
7
|
+
padding?: number;
|
|
8
|
+
minZoom?: number;
|
|
9
|
+
maxZoom?: number;
|
|
10
|
+
animate?: boolean;
|
|
11
|
+
};
|
|
12
|
+
declare function createCameraModule(ctx: EditorContext): {
|
|
13
|
+
setCamera: (viewport: BoardViewport) => void;
|
|
14
|
+
setCameraPolicy: (policy: BoardCameraPolicy) => void;
|
|
15
|
+
takeCameraControl: () => void;
|
|
16
|
+
cancelAnimation: () => void;
|
|
17
|
+
viewCenter: () => WorldPoint;
|
|
18
|
+
visibleRect: () => Rect | null;
|
|
19
|
+
zoomAt: (point: ScreenPoint, factor: number, animate?: boolean) => void;
|
|
20
|
+
zoomIn: () => void;
|
|
21
|
+
zoomOut: () => void;
|
|
22
|
+
resetZoom: () => void;
|
|
23
|
+
focusRect: (rect: Rect, focus?: BoardFocusOptions) => void;
|
|
24
|
+
fitView: (focus?: BoardFocusOptions) => void;
|
|
25
|
+
focusItems: (ids: Iterable<string>, focus?: BoardFocusOptions) => void;
|
|
26
|
+
focusNode: (id: string, focus?: BoardFocusOptions) => void;
|
|
27
|
+
focusSelection: (focus?: BoardFocusOptions) => void;
|
|
28
|
+
wheel: (point: ScreenPoint, deltaX: number, deltaY: number, zoomKey: boolean, deltaMode?: number) => void;
|
|
29
|
+
};
|
|
30
|
+
type CameraModule = ReturnType<typeof createCameraModule>;
|
|
31
|
+
//#endregion
|
|
32
|
+
export { BoardFocusOptions, CameraModule, createCameraModule };
|