@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.
Files changed (240) hide show
  1. package/README.md +108 -102
  2. package/dist/board/editor/align.d.ts +11 -0
  3. package/dist/board/editor/align.js +72 -0
  4. package/dist/board/editor/animation.d.ts +17 -0
  5. package/dist/board/editor/animation.js +187 -0
  6. package/dist/board/editor/camera.d.ts +32 -0
  7. package/dist/board/editor/camera.js +137 -0
  8. package/dist/board/editor/clipboard.d.ts +41 -0
  9. package/dist/board/editor/clipboard.js +129 -0
  10. package/dist/board/editor/connection-ports.d.ts +19 -0
  11. package/dist/board/editor/connection-ports.js +85 -0
  12. package/dist/board/editor/context.d.ts +111 -0
  13. package/dist/board/editor/context.js +30 -0
  14. package/dist/board/editor/copy.d.ts +17 -0
  15. package/dist/board/editor/copy.js +121 -0
  16. package/dist/board/editor/creation.d.ts +51 -0
  17. package/dist/board/editor/creation.js +225 -0
  18. package/dist/board/editor/deletion.d.ts +9 -0
  19. package/dist/board/editor/deletion.js +90 -0
  20. package/dist/board/editor/document-edits.d.ts +22 -0
  21. package/dist/board/editor/document-edits.js +193 -0
  22. package/dist/board/editor/document.d.ts +41 -0
  23. package/dist/board/editor/document.js +302 -0
  24. package/dist/board/editor/editor.d.ts +225 -0
  25. package/dist/board/editor/editor.js +439 -0
  26. package/dist/board/editor/index.d.ts +18 -0
  27. package/dist/board/editor/index.js +12 -0
  28. package/dist/board/editor/interaction.d.ts +115 -0
  29. package/dist/board/editor/interaction.js +584 -0
  30. package/dist/board/editor/query.d.ts +27 -0
  31. package/dist/board/editor/query.js +203 -0
  32. package/dist/board/editor/remote.d.ts +11 -0
  33. package/dist/board/editor/remote.js +55 -0
  34. package/dist/board/editor/selection-transform.d.ts +24 -0
  35. package/dist/board/editor/selection-transform.js +81 -0
  36. package/dist/board/editor/selection.d.ts +33 -0
  37. package/dist/board/editor/selection.js +197 -0
  38. package/dist/board/editor/snapping.d.ts +33 -0
  39. package/dist/board/editor/snapping.js +75 -0
  40. package/dist/board/editor/snapshots.d.ts +27 -0
  41. package/dist/board/editor/snapshots.js +132 -0
  42. package/dist/board/editor/spatial.d.ts +41 -0
  43. package/dist/board/editor/spatial.js +191 -0
  44. package/dist/board/editor/state.d.ts +12 -0
  45. package/dist/board/editor/state.js +49 -0
  46. package/dist/board/editor/tool.d.ts +10 -0
  47. package/dist/board/editor/tool.js +19 -0
  48. package/dist/board/editor/wheel.d.ts +5 -0
  49. package/dist/board/editor/wheel.js +17 -0
  50. package/dist/board/export/index.d.ts +28 -6
  51. package/dist/board/export/index.js +47 -13
  52. package/dist/board/export/node/index.d.ts +56 -0
  53. package/dist/board/{headless → export/node}/index.js +26 -21
  54. package/dist/board/export/node/scripts.d.ts +16 -0
  55. package/dist/board/export/node/scripts.js +258 -0
  56. package/dist/board/export/scene.d.ts +8 -5
  57. package/dist/board/export/scene.js +24 -9
  58. package/dist/board/{animation.d.ts → model/animation.d.ts} +8 -5
  59. package/dist/board/{animation.js → model/animation.js} +37 -10
  60. package/dist/board/{core → model}/arrow-geometry.d.ts +2 -2
  61. package/dist/board/{core → model}/arrow-geometry.js +2 -2
  62. package/dist/board/model/authoring.d.ts +20 -0
  63. package/dist/board/model/authoring.js +185 -0
  64. package/dist/board/{core → model}/draw-geometry.d.ts +2 -2
  65. package/dist/board/{core → model}/draw-geometry.js +1 -1
  66. package/dist/board/model/draw-input.d.ts +14 -0
  67. package/dist/board/model/draw-input.js +28 -0
  68. package/dist/board/{core → model}/export-assets.d.ts +1 -1
  69. package/dist/board/{core → model}/export-assets.js +1 -1
  70. package/dist/board/{core → model}/export-plan.d.ts +2 -2
  71. package/dist/board/{core → model}/export-plan.js +3 -2
  72. package/dist/board/{core → model}/file-preview.d.ts +1 -1
  73. package/dist/board/{core → model}/file-preview.js +2 -2
  74. package/dist/board/{core → model}/file-snapshot.d.ts +1 -1
  75. package/dist/board/{core → model}/file-snapshot.js +4 -3
  76. package/dist/board/{geometry.d.ts → model/geometry.d.ts} +3 -3
  77. package/dist/board/{geometry.js → model/geometry.js} +1 -1
  78. package/dist/board/{image-key.d.ts → model/image-key.d.ts} +2 -2
  79. package/dist/board/{image-key.js → model/image-key.js} +7 -3
  80. package/dist/board/model/index.d.ts +42 -0
  81. package/dist/board/model/index.js +39 -0
  82. package/dist/board/model/items/builtin.d.ts +5 -0
  83. package/dist/board/model/items/builtin.js +119 -0
  84. package/dist/board/model/items/create.d.ts +61 -0
  85. package/dist/board/model/items/create.js +272 -0
  86. package/dist/board/model/items/definition.d.ts +35 -0
  87. package/dist/board/model/items/definition.js +18 -0
  88. package/dist/board/model/items/id.d.ts +5 -0
  89. package/dist/board/model/items/id.js +11 -0
  90. package/dist/board/model/items/index.d.ts +6 -0
  91. package/dist/board/model/items/index.js +6 -0
  92. package/dist/board/model/items/registry.d.ts +35 -0
  93. package/dist/board/model/items/registry.js +84 -0
  94. package/dist/board/{media-playback.d.ts → model/media-playback.d.ts} +4 -3
  95. package/dist/board/{media-playback.js → model/media-playback.js} +15 -5
  96. package/dist/board/{media.d.ts → model/media.d.ts} +1 -1
  97. package/dist/board/{media.js → model/media.js} +8 -2
  98. package/dist/board/{core → model}/palette.d.ts +1 -1
  99. package/dist/board/{core → model}/palette.js +1 -1
  100. package/dist/board/{presets.d.ts → model/presets.d.ts} +3 -3
  101. package/dist/board/{presets.js → model/presets.js} +1 -1
  102. package/dist/board/{replay.d.ts → model/replay.d.ts} +5 -5
  103. package/dist/board/{replay.js → model/replay.js} +2 -2
  104. package/dist/board/model/sampling.d.ts +19 -0
  105. package/dist/board/model/sampling.js +96 -0
  106. package/dist/board/{core → model}/scene.d.ts +1 -1
  107. package/dist/board/{core → model}/scene.js +1 -1
  108. package/dist/board/model/script-example.d.ts +4 -0
  109. package/dist/board/model/script-example.js +21 -0
  110. package/dist/board/model/script-hit.d.ts +10 -0
  111. package/dist/board/model/script-hit.js +52 -0
  112. package/dist/board/model/script-index.d.ts +7 -0
  113. package/dist/board/model/script-index.js +15 -0
  114. package/dist/board/model/script-scene.d.ts +24 -0
  115. package/dist/board/model/script-scene.js +267 -0
  116. package/dist/board/model/script.d.ts +105 -0
  117. package/dist/board/model/script.js +1 -0
  118. package/dist/board/model/shape-types.d.ts +8 -0
  119. package/dist/board/model/shape-types.js +8 -0
  120. package/dist/board/{task.d.ts → model/task.d.ts} +4 -4
  121. package/dist/board/{task.js → model/task.js} +3 -3
  122. package/dist/board/{core → model}/text-metrics.d.ts +1 -1
  123. package/dist/board/{core → model}/text-metrics.js +1 -1
  124. package/dist/board/{core → model}/tool-styles.d.ts +1 -1
  125. package/dist/board/{core → model}/tool-styles.js +1 -1
  126. package/dist/board/{runtime/board-player.d.ts → player/index.d.ts} +2 -2
  127. package/dist/board/{runtime/board-player.js → player/index.js} +5 -4
  128. package/dist/board/render/board-background.d.ts +1 -1
  129. package/dist/board/render/clip.d.ts +1 -1
  130. package/dist/board/render/css-color.d.ts +6 -1
  131. package/dist/board/render/css-color.js +34 -20
  132. package/dist/board/render/diagnostics.d.ts +20 -0
  133. package/dist/board/render/diagnostics.js +117 -0
  134. package/dist/board/render/index.d.ts +9 -6
  135. package/dist/board/render/index.js +10 -7
  136. package/dist/board/render/media-interaction.d.ts +2 -2
  137. package/dist/board/render/media-interaction.js +1 -1
  138. package/dist/board/render/palette.d.ts +3 -2
  139. package/dist/board/render/palette.js +7 -3
  140. package/dist/board/render/renderers/arrow-card-renderer.js +4 -3
  141. package/dist/board/render/renderers/audio-card-renderer.js +2 -1
  142. package/dist/board/render/renderers/board-renderer-registry.d.ts +20 -10
  143. package/dist/board/render/renderers/board-renderer-registry.js +17 -17
  144. package/dist/board/render/renderers/custom-card-renderer.d.ts +5 -0
  145. package/dist/board/render/renderers/{sketch-card-renderer.js → custom-card-renderer.js} +16 -15
  146. package/dist/board/render/renderers/draw-card-renderer.js +4 -3
  147. package/dist/board/render/renderers/effect-card-renderer.js +22 -5
  148. package/dist/board/render/renderers/far-plate.js +1 -1
  149. package/dist/board/render/renderers/file-card-renderer.d.ts +1 -1
  150. package/dist/board/render/renderers/file-card-renderer.js +2 -2
  151. package/dist/board/render/renderers/frame-card-renderer.js +30 -14
  152. package/dist/board/render/renderers/image-card-renderer.d.ts +1 -1
  153. package/dist/board/render/renderers/image-card-renderer.js +43 -4
  154. package/dist/board/render/renderers/shape-card-renderer.js +11 -28
  155. package/dist/board/render/renderers/task-card-renderer.js +1 -1
  156. package/dist/board/render/renderers/text-card-renderer.js +6 -4
  157. package/dist/board/render/renderers/unknown-card-renderer.js +1 -0
  158. package/dist/board/render/renderers/video-card-renderer.d.ts +1 -1
  159. package/dist/board/render/renderers/video-card-renderer.js +2 -1
  160. package/dist/board/render/sampling.d.ts +22 -0
  161. package/dist/board/render/sampling.js +21 -0
  162. package/dist/board/render/script-export.d.ts +11 -0
  163. package/dist/board/render/script-export.js +86 -0
  164. package/dist/board/render/script-host.d.ts +31 -0
  165. package/dist/board/render/script-host.js +421 -0
  166. package/dist/board/render/script-modules.d.ts +2 -0
  167. package/dist/board/render/script-modules.js +2 -0
  168. package/dist/board/render/script-runtime.d.ts +29 -0
  169. package/dist/board/render/script-runtime.js +213 -0
  170. package/dist/board/render/script-sandbox.d.ts +4 -0
  171. package/dist/board/render/script-sandbox.js +173 -0
  172. package/dist/board/render/stroke.d.ts +3 -2
  173. package/dist/board/render/stroke.js +23 -2
  174. package/dist/board/render/text-measurement.js +2 -2
  175. package/dist/board/replica/index.d.ts +70 -0
  176. package/dist/board/replica/index.js +276 -0
  177. package/dist/board/stage/assets.d.ts +41 -0
  178. package/dist/board/stage/assets.js +355 -0
  179. package/dist/board/stage/index.d.ts +8 -0
  180. package/dist/board/stage/index.js +8 -0
  181. package/dist/board/stage/lru.d.ts +32 -0
  182. package/dist/board/stage/lru.js +29 -0
  183. package/dist/board/stage/maintenance.d.ts +5 -0
  184. package/dist/board/stage/maintenance.js +11 -0
  185. package/dist/board/stage/pointer.d.ts +8 -0
  186. package/dist/board/stage/pointer.js +33 -0
  187. package/dist/board/stage/scene.d.ts +85 -0
  188. package/dist/board/stage/scene.js +515 -0
  189. package/dist/board/stage/script-pointer.d.ts +19 -0
  190. package/dist/board/stage/script-pointer.js +35 -0
  191. package/dist/board/stage/stage.d.ts +76 -0
  192. package/dist/board/stage/stage.js +774 -0
  193. package/dist/board/stage/texture-pool.d.ts +24 -0
  194. package/dist/board/stage/texture-pool.js +153 -0
  195. package/dist/board/stage/views.d.ts +38 -0
  196. package/dist/board/stage/views.js +93 -0
  197. package/dist/chunks/environment.d.ts +289 -23
  198. package/dist/chunks/host-bridge.d.ts +2 -2
  199. package/dist/chunks/http.d.ts +185 -71
  200. package/dist/chunks/http.js +368 -269
  201. package/dist/chunks/transport.js +3 -1
  202. package/dist/chunks/websocket.d.ts +9 -1
  203. package/dist/chunks/websocket.js +16 -2
  204. package/dist/host-bridge.js +2 -1
  205. package/dist/http.d.ts +2 -2
  206. package/dist/index.d.ts +182 -4
  207. package/dist/index.js +1393 -129
  208. package/dist/media.d.ts +1 -10
  209. package/dist/media.js +16 -27
  210. package/dist/protocol/dist/board-assets.d.ts +1 -0
  211. package/dist/protocol/dist/board-constants.js +2 -2
  212. package/dist/protocol/dist/board-model.d.ts +201 -11
  213. package/dist/protocol/dist/board-model.js +33 -14
  214. package/dist/protocol/dist/board-patch.js +3 -2
  215. package/dist/protocol/dist/board-schema.d.ts +4 -2
  216. package/dist/protocol/dist/board-schema.js +18 -4
  217. package/dist/protocol/dist/board-script-modules.d.ts +25 -0
  218. package/dist/protocol/dist/board-script-modules.js +127 -0
  219. package/dist/protocol/dist/board.d.ts +28 -2
  220. package/dist/protocol/dist/board.js +3 -1
  221. package/dist/protocol/dist/core/media.d.ts +5 -0
  222. package/dist/protocol/dist/core/media.js +39 -0
  223. package/dist/protocol/dist/display/index.d.ts +1 -0
  224. package/dist/protocol/dist/index.d.ts +7 -5
  225. package/dist/protocol/dist/realtime/types.d.ts +1 -0
  226. package/dist/protocol/dist/rtc/index.d.ts +1 -0
  227. package/dist/types.d.ts +1 -1
  228. package/package.json +21 -8
  229. package/dist/board/core/shape-definition.d.ts +0 -23
  230. package/dist/board/core/shape-definition.js +0 -45
  231. package/dist/board/core/shape-types.d.ts +0 -38
  232. package/dist/board/core/shape-types.js +0 -22
  233. package/dist/board/headless/index.d.ts +0 -55
  234. package/dist/board/headless/sketch.d.ts +0 -19
  235. package/dist/board/headless/sketch.js +0 -146
  236. package/dist/board/index.d.ts +0 -30
  237. package/dist/board/index.js +0 -29
  238. package/dist/board/render/renderers/sketch-card-renderer.d.ts +0 -5
  239. package/dist/board/render/sketch-host.d.ts +0 -14
  240. 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
- Use `space.boards` for collection operations and bind an ID with
80
- `space.board(boardId)` for entity operations:
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
- items: [{
87
- id: "goal",
88
- type: "geo",
89
- frame: { x: 80, y: 80, width: 240, height: 120, rotation: 0 },
90
- props: { shape: "rounded", text: "Ship" },
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.board.id);
96
- // Equivalent: space.boards.byId(created.board.id)
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
- // Machine-readable types, enums and coordinate spaces for dynamic clients.
99
- const capabilities = await board.capabilities();
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
- const snapshot = await board.authoring({ include: ["items"] });
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
- await board.mutateSemantic({
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
- await board.play({
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
- A bound `BoardClient` injects its `boardId` into semantic mutation requests.
120
- Realtime subscriptions use the same semantic resource projection:
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
- ```ts
123
- const stop = board.subscribe({
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
- stop();
134
- ```
127
+ ### Embedding an editor
135
128
 
136
- Task Items keep a small, replaceable display snapshot beside their stable `taskRunId`. The SDK can build that projection from an authoritative TaskRun without copying the full payload, result or inline media into a Board:
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 { taskRunToBoardTaskSnapshot } from "@neta-art/cohub/board";
140
-
141
- const snapshot = taskRunToBoardTaskSnapshot(taskRun);
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
- Use `client.tasks.getMany(ids, { spaceId })` to restore the TaskRuns for a Board in one request. The server applies the same Space permissions and result sanitization as the regular task list endpoint.
149
+ const replica = createBoardReplica({ remote: space.board(boardId) });
150
+ await replica.start();
145
151
 
146
- Board is split by dependency: the model runs anywhere, drawing needs PixiJS.
147
- `@neta-art/cohub/board` carries the document schema, geometry, the shape layer,
148
- timeline compilation and export planning, with no renderer and no PixiJS — so
149
- agents, servers and edge workers can read, write and measure boards without a
150
- graphics stack:
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
- ```ts
153
- import {
154
- BoardDocumentSchema,
155
- compileComposition,
156
- createBoardExtensionRegistry,
157
- itemBounds,
158
- planBoardExport,
159
- } from "@neta-art/cohub/board";
160
-
161
- const composition = compileComposition({
162
- id: "ambient",
163
- name: "Ambient",
164
- duration: 1_000,
165
- tracks: [{
166
- id: "image-translation",
167
- target: { type: "item", itemId: "image" },
168
- channel: "transform.translation",
169
- fill: "both",
170
- keyframes: [
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
- await space.boards.create({
180
- path: "boards/ambient.board",
181
- metadata: {
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
- Drawing pixels is where PixiJS enters. The card renderers and themes the editor
189
- uses live behind `@neta-art/cohub/board/render`, and turning a plan into an
190
- image has dedicated browser and Node.js entries:
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 { getBoardCardRenderer } from "@neta-art/cohub/board/render";
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
- Install `pixi.js` to use `board/render` or `board/export`, and add
202
- `@napi-rs/canvas` as well for `board/headless`. Both are optional peers, and
203
- `@neta-art/cohub/board` never reaches for either, so HTTP-only installations
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, so nothing extra is needed to draw or export. Called
208
- straight off `@neta-art/cohub/board` with no renderer in play,
209
- `measureBoardText` returns a per-character estimate instead — fine for laying
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 };