@neta-art/cohub 8.6.0 → 8.7.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/dist/board/core/arrow-geometry.js +8 -45
- package/dist/board/core/draw-geometry.d.ts +8 -6
- package/dist/board/core/draw-geometry.js +6 -32
- package/dist/board/core/shape-types.d.ts +1 -1
- package/dist/board/export/scene.js +1 -0
- package/dist/board/geometry.d.ts +1 -1
- package/dist/board/index.d.ts +4 -3
- package/dist/board/index.js +2 -1
- package/dist/board/render/renderers/board-renderer-registry.d.ts +2 -0
- package/dist/board/render/renderers/draw-card-renderer.js +28 -18
- package/dist/board/semantic-document.d.ts +1 -1
- package/dist/board/semantic-document.js +23 -4
- package/dist/board/semantic-mutation.d.ts +1 -1
- package/dist/board/semantic-mutation.js +7 -1
- package/dist/chunks/http.js +32 -21
- package/dist/chunks/websocket.d.ts +410 -368
- package/dist/protocol/dist/board-authoring.d.ts +201 -181
- package/dist/protocol/dist/board-authoring.js +32 -21
- package/dist/protocol/dist/board-codec.js +200 -1
- package/dist/protocol/dist/board-geometry.d.ts +15 -0
- package/dist/protocol/dist/board-geometry.js +137 -0
- package/dist/protocol/dist/board-node.js +94 -1
- package/dist/protocol/dist/board.js +2 -2
- package/dist/protocol/dist/index.d.ts +2 -1
- package/package.json +1 -1
|
@@ -26,12 +26,17 @@ const idSchema = z.string().min(1).max(160);
|
|
|
26
26
|
const jsonObjectSchema = z.record(z.string(), z.unknown());
|
|
27
27
|
const finiteSchema = z.number().finite();
|
|
28
28
|
const extensionTypeSchema = z.string().regex(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/).max(160);
|
|
29
|
-
const
|
|
29
|
+
const BoardAuthoringPositionSchema = z.object({
|
|
30
30
|
x: finiteSchema,
|
|
31
|
-
y: finiteSchema
|
|
31
|
+
y: finiteSchema
|
|
32
|
+
}).strict();
|
|
33
|
+
const BoardAuthoringSizeSchema = z.object({
|
|
32
34
|
width: finiteSchema.positive(),
|
|
33
|
-
height: finiteSchema.positive()
|
|
34
|
-
|
|
35
|
+
height: finiteSchema.positive()
|
|
36
|
+
}).strict();
|
|
37
|
+
const BoardAuthoringPositionPatchSchema = z.object({
|
|
38
|
+
x: finiteSchema.optional(),
|
|
39
|
+
y: finiteSchema.optional()
|
|
35
40
|
}).strict();
|
|
36
41
|
const pointSchema = z.object({
|
|
37
42
|
x: finiteSchema,
|
|
@@ -62,15 +67,26 @@ const BoardItemSourceSchema = z.object({
|
|
|
62
67
|
path: z.string().refine(isSafeBoardSourcePath, "source path must be a safe relative Board file path"),
|
|
63
68
|
snapshot: sourceSnapshotSchema.optional()
|
|
64
69
|
}).strict();
|
|
65
|
-
const
|
|
70
|
+
const sizedBaseFields = {
|
|
66
71
|
id: idSchema,
|
|
67
|
-
|
|
72
|
+
position: BoardAuthoringPositionSchema.default({
|
|
73
|
+
x: 0,
|
|
74
|
+
y: 0
|
|
75
|
+
}),
|
|
76
|
+
rotation: finiteSchema.default(0),
|
|
68
77
|
parentId: idSchema.nullable().optional(),
|
|
69
78
|
locked: z.boolean().optional(),
|
|
70
|
-
metadata: jsonObjectSchema.optional()
|
|
79
|
+
metadata: jsonObjectSchema.optional(),
|
|
80
|
+
size: BoardAuthoringSizeSchema.optional()
|
|
71
81
|
};
|
|
72
82
|
const styledBaseFields = {
|
|
73
|
-
...
|
|
83
|
+
...sizedBaseFields,
|
|
84
|
+
style: BoardItemStyleSchema.optional()
|
|
85
|
+
};
|
|
86
|
+
const { position: _positionField, size: _sizeField, ...strokeBaseFields } = sizedBaseFields;
|
|
87
|
+
const strokeStyledBaseFields = {
|
|
88
|
+
...strokeBaseFields,
|
|
89
|
+
rotation: z.literal(0).default(0),
|
|
74
90
|
style: BoardItemStyleSchema.optional()
|
|
75
91
|
};
|
|
76
92
|
const BoardTextAuthoringItemSchema = z.object({
|
|
@@ -90,12 +106,12 @@ const BoardGeoAuthoringItemSchema = z.object({
|
|
|
90
106
|
}).strict()
|
|
91
107
|
}).strict();
|
|
92
108
|
const BoardDrawAuthoringItemSchema = z.object({
|
|
93
|
-
...
|
|
109
|
+
...strokeStyledBaseFields,
|
|
94
110
|
type: z.literal("draw"),
|
|
95
111
|
props: z.object({ points: z.array(pointSchema).min(1) }).strict()
|
|
96
112
|
}).strict();
|
|
97
113
|
const BoardArrowAuthoringItemSchema = z.object({
|
|
98
|
-
...
|
|
114
|
+
...strokeStyledBaseFields,
|
|
99
115
|
type: z.literal("arrow"),
|
|
100
116
|
props: z.object({
|
|
101
117
|
start: worldPointSchema,
|
|
@@ -112,7 +128,7 @@ const BoardFrameAuthoringItemSchema = z.object({
|
|
|
112
128
|
props: z.object({ label: z.string().default("Frame") }).strict()
|
|
113
129
|
}).strict();
|
|
114
130
|
const fileBackedFields = {
|
|
115
|
-
...
|
|
131
|
+
...sizedBaseFields,
|
|
116
132
|
style: z.object({}).strict().optional(),
|
|
117
133
|
source: BoardItemSourceSchema
|
|
118
134
|
};
|
|
@@ -137,7 +153,7 @@ const BoardFileAuthoringItemSchema = z.object({
|
|
|
137
153
|
props: z.object({}).strict()
|
|
138
154
|
}).strict();
|
|
139
155
|
const BoardTaskAuthoringItemSchema = z.object({
|
|
140
|
-
...
|
|
156
|
+
...sizedBaseFields,
|
|
141
157
|
type: z.literal("task"),
|
|
142
158
|
props: z.object({
|
|
143
159
|
taskRunId: z.string().min(1),
|
|
@@ -159,7 +175,7 @@ const builtinItemSchemas = [
|
|
|
159
175
|
];
|
|
160
176
|
const BoardBuiltinAuthoringItemSchema = z.discriminatedUnion("type", builtinItemSchemas);
|
|
161
177
|
const BoardExtensionAuthoringItemSchema = z.object({
|
|
162
|
-
...
|
|
178
|
+
...sizedBaseFields,
|
|
163
179
|
type: extensionTypeSchema,
|
|
164
180
|
kindVersion: z.number().int().positive(),
|
|
165
181
|
props: jsonObjectSchema,
|
|
@@ -171,16 +187,11 @@ const BoardExtensionAuthoringItemSchema = z.object({
|
|
|
171
187
|
}).strict().optional()
|
|
172
188
|
}).strict();
|
|
173
189
|
const BoardAuthoringItemSchema = z.union([BoardBuiltinAuthoringItemSchema, BoardExtensionAuthoringItemSchema]);
|
|
174
|
-
const framePatchSchema = z.object({
|
|
175
|
-
x: finiteSchema.optional(),
|
|
176
|
-
y: finiteSchema.optional(),
|
|
177
|
-
width: finiteSchema.positive().optional(),
|
|
178
|
-
height: finiteSchema.positive().optional(),
|
|
179
|
-
rotation: finiteSchema.optional()
|
|
180
|
-
}).strict();
|
|
181
190
|
/** JSON Merge Patch semantics, constrained to the stable Item envelope. */
|
|
182
191
|
const BoardItemPatchSchema = z.object({
|
|
183
|
-
|
|
192
|
+
position: BoardAuthoringPositionPatchSchema.optional(),
|
|
193
|
+
size: BoardAuthoringSizeSchema.nullable().optional(),
|
|
194
|
+
rotation: finiteSchema.optional(),
|
|
184
195
|
parentId: idSchema.nullable().optional(),
|
|
185
196
|
locked: z.boolean().nullable().optional(),
|
|
186
197
|
props: jsonObjectSchema.optional(),
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { BoardAuthoringItemSchema, BoardItemPatchSchema } from "./board-authoring.js";
|
|
2
|
+
import { BoardNodeInputSchema } from "./board.js";
|
|
3
|
+
import { boardArrowFrame, boardDrawBounds, boardDrawPointsToLocal } from "./board-geometry.js";
|
|
4
|
+
import { BOARD_NATIVE_NODE_TYPES, validateBoardNodeInput } from "./board-node.js";
|
|
2
5
|
//#region ../protocol/dist/board-codec.js
|
|
3
6
|
var BoardItemValidationError = class extends Error {
|
|
4
7
|
diagnostics;
|
|
@@ -16,7 +19,203 @@ function authoringSchemaDiagnostics(error, path) {
|
|
|
16
19
|
path: `${path}.${issue.path.map(String).join(".") || "item"}`
|
|
17
20
|
}));
|
|
18
21
|
}
|
|
22
|
+
function authoringDiagnostic(diagnostic, path) {
|
|
23
|
+
const internalPath = diagnostic.path;
|
|
24
|
+
const authoringSuffix = internalPath.replace(/^node(?:\.|$)/, "").replace(/^data\.points/, "props.points").replace(/^data\./, "props.").replace(/^style\./, "style.").replace(/^view\./, "source.snapshot.").replace(/^nodeId$/, "id").replace(/^(x|y)$/, "position.$1").replace(/^(width|height)$/, "size.$1");
|
|
25
|
+
const mappedPath = `${path}${authoringSuffix ? `.${authoringSuffix}` : ""}`;
|
|
26
|
+
return {
|
|
27
|
+
...diagnostic,
|
|
28
|
+
path: mappedPath,
|
|
29
|
+
message: diagnostic.message.replace(internalPath, mappedPath)
|
|
30
|
+
};
|
|
31
|
+
}
|
|
19
32
|
const isRecord = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
33
|
+
function commonData(item) {
|
|
34
|
+
return {
|
|
35
|
+
...item.locked ? { locked: true } : {},
|
|
36
|
+
...item.metadata ? { metadata: item.metadata } : {}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function defaultSize(type) {
|
|
40
|
+
if (type === "frame") return {
|
|
41
|
+
width: 480,
|
|
42
|
+
height: 320
|
|
43
|
+
};
|
|
44
|
+
if (type === "text") return {
|
|
45
|
+
width: 320,
|
|
46
|
+
height: 48
|
|
47
|
+
};
|
|
48
|
+
if (type === "task") return {
|
|
49
|
+
width: 420,
|
|
50
|
+
height: 240
|
|
51
|
+
};
|
|
52
|
+
if (type === "image" || type === "video") return {
|
|
53
|
+
width: 640,
|
|
54
|
+
height: 360
|
|
55
|
+
};
|
|
56
|
+
if (type === "audio") return {
|
|
57
|
+
width: 480,
|
|
58
|
+
height: 96
|
|
59
|
+
};
|
|
60
|
+
if (type === "file") return {
|
|
61
|
+
width: 360,
|
|
62
|
+
height: 220
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
width: 240,
|
|
66
|
+
height: 160
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function authoringFrame(item) {
|
|
70
|
+
if (item.type === "draw") {
|
|
71
|
+
const points = item.props;
|
|
72
|
+
const size = Number(item.style?.strokeWidth ?? 4);
|
|
73
|
+
const bounds = boardDrawBounds(points.points, size);
|
|
74
|
+
return {
|
|
75
|
+
x: bounds.x,
|
|
76
|
+
y: bounds.y,
|
|
77
|
+
width: bounds.width,
|
|
78
|
+
height: bounds.height,
|
|
79
|
+
rotation: item.rotation
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (item.type === "arrow") {
|
|
83
|
+
const props = item.props;
|
|
84
|
+
return {
|
|
85
|
+
...boardArrowFrame({
|
|
86
|
+
...props,
|
|
87
|
+
size: Number(item.style?.strokeWidth ?? 2.5)
|
|
88
|
+
}),
|
|
89
|
+
rotation: item.rotation
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const position = item.position;
|
|
93
|
+
const size = item.size ?? defaultSize(item.type);
|
|
94
|
+
return {
|
|
95
|
+
x: position.x,
|
|
96
|
+
y: position.y,
|
|
97
|
+
width: size.width,
|
|
98
|
+
height: size.height,
|
|
99
|
+
rotation: item.rotation
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function boardAuthoringItemToNode(value, options = {}) {
|
|
103
|
+
const item = BoardAuthoringItemSchema.parse(value);
|
|
104
|
+
const node = {
|
|
105
|
+
nodeId: item.id,
|
|
106
|
+
type: item.type,
|
|
107
|
+
parentId: item.parentId ?? null,
|
|
108
|
+
orderKey: options.orderKey ?? null,
|
|
109
|
+
...authoringFrame(item),
|
|
110
|
+
refKind: null,
|
|
111
|
+
refPath: null,
|
|
112
|
+
refUrl: null,
|
|
113
|
+
view: {},
|
|
114
|
+
style: {},
|
|
115
|
+
data: commonData(item)
|
|
116
|
+
};
|
|
117
|
+
const itemStyle = "style" in item ? item.style : void 0;
|
|
118
|
+
const color = itemStyle && "color" in itemStyle ? itemStyle.color : void 0;
|
|
119
|
+
const strokeWidth = itemStyle && "strokeWidth" in itemStyle ? itemStyle.strokeWidth : void 0;
|
|
120
|
+
const fillOpacity = itemStyle && "fillOpacity" in itemStyle ? itemStyle.fillOpacity : void 0;
|
|
121
|
+
switch (item.type) {
|
|
122
|
+
case "text":
|
|
123
|
+
node.data = {
|
|
124
|
+
...node.data,
|
|
125
|
+
...item.props,
|
|
126
|
+
color: color ?? "neutral"
|
|
127
|
+
};
|
|
128
|
+
break;
|
|
129
|
+
case "geo":
|
|
130
|
+
node.data = {
|
|
131
|
+
...node.data,
|
|
132
|
+
geo: item.props.shape,
|
|
133
|
+
text: item.props.text,
|
|
134
|
+
color: color ?? "brand",
|
|
135
|
+
fillOpacity: fillOpacity ?? 0
|
|
136
|
+
};
|
|
137
|
+
break;
|
|
138
|
+
case "draw": {
|
|
139
|
+
const points = item.props;
|
|
140
|
+
node.data = {
|
|
141
|
+
...node.data,
|
|
142
|
+
points: boardDrawPointsToLocal(points.points, node.x, node.y),
|
|
143
|
+
color: color ?? "brand",
|
|
144
|
+
size: strokeWidth ?? 4
|
|
145
|
+
};
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
case "arrow":
|
|
149
|
+
node.data = {
|
|
150
|
+
...node.data,
|
|
151
|
+
...item.props,
|
|
152
|
+
color: color ?? "brand",
|
|
153
|
+
size: strokeWidth ?? 2.5
|
|
154
|
+
};
|
|
155
|
+
break;
|
|
156
|
+
case "frame":
|
|
157
|
+
node.data = {
|
|
158
|
+
...node.data,
|
|
159
|
+
...item.props,
|
|
160
|
+
color: color ?? "neutral"
|
|
161
|
+
};
|
|
162
|
+
break;
|
|
163
|
+
case "image": {
|
|
164
|
+
const source = item.source;
|
|
165
|
+
node.data = {
|
|
166
|
+
...node.data,
|
|
167
|
+
...item.props.crop ? { crop: item.props.crop } : {}
|
|
168
|
+
};
|
|
169
|
+
node.refKind = "space_file";
|
|
170
|
+
node.refPath = source.path;
|
|
171
|
+
node.view = source.snapshot ?? {};
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
case "video":
|
|
175
|
+
case "audio":
|
|
176
|
+
case "file": {
|
|
177
|
+
const source = item.source;
|
|
178
|
+
node.refKind = "space_file";
|
|
179
|
+
node.refPath = source.path;
|
|
180
|
+
node.view = source.snapshot ?? {};
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
case "task":
|
|
184
|
+
node.data = {
|
|
185
|
+
...node.data,
|
|
186
|
+
taskRunId: item.props.taskRunId
|
|
187
|
+
};
|
|
188
|
+
node.view = item.props.snapshot;
|
|
189
|
+
break;
|
|
190
|
+
default: {
|
|
191
|
+
const extension = item;
|
|
192
|
+
node.data = {
|
|
193
|
+
...node.data,
|
|
194
|
+
...extension.props,
|
|
195
|
+
kindVersion: extension.kindVersion
|
|
196
|
+
};
|
|
197
|
+
node.style = extension.style ?? {};
|
|
198
|
+
if (extension.source) {
|
|
199
|
+
node.refKind = extension.source.kind;
|
|
200
|
+
node.refPath = extension.source.ref;
|
|
201
|
+
node.view = extension.source.snapshot ?? {};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (BOARD_NATIVE_NODE_TYPES.includes(node.type)) {
|
|
206
|
+
const diagnostics = validateBoardNodeInput(node);
|
|
207
|
+
if (diagnostics.length) throw new BoardItemValidationError(diagnostics.map((diagnostic) => authoringDiagnostic(diagnostic, options.path ?? "item")));
|
|
208
|
+
} else {
|
|
209
|
+
const parsed = BoardNodeInputSchema.safeParse(node);
|
|
210
|
+
if (!parsed.success) throw new BoardItemValidationError(parsed.error.issues.map((issue) => ({
|
|
211
|
+
severity: "error",
|
|
212
|
+
code: "INVALID_BOARD_NODE",
|
|
213
|
+
message: issue.message,
|
|
214
|
+
path: `${options.path ?? "item"}.${issue.path.join(".") || "item"}`
|
|
215
|
+
})));
|
|
216
|
+
}
|
|
217
|
+
return node;
|
|
218
|
+
}
|
|
20
219
|
function applyBoardItemPatch(current, value, path = "item") {
|
|
21
220
|
const merged = mergePatch(current, BoardItemPatchSchema.parse(value));
|
|
22
221
|
merged.id = current.id;
|
|
@@ -34,4 +233,4 @@ function mergePatch(current, patch) {
|
|
|
34
233
|
return result;
|
|
35
234
|
}
|
|
36
235
|
//#endregion
|
|
37
|
-
export { BoardItemValidationError, applyBoardItemPatch, authoringSchemaDiagnostics };
|
|
236
|
+
export { BoardItemValidationError, applyBoardItemPatch, authoringSchemaDiagnostics, boardAuthoringItemToNode };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BoardFrame, BoardPoint, DrawPoint } from "./board-document.js";
|
|
2
|
+
//#region ../protocol/dist/board-geometry.d.ts
|
|
3
|
+
type BoardArrowGeometryInput = {
|
|
4
|
+
start: BoardPoint;
|
|
5
|
+
end: BoardPoint;
|
|
6
|
+
bend: number;
|
|
7
|
+
size: number;
|
|
8
|
+
};
|
|
9
|
+
/** Radius of a draw sample in world units given stroke size and pressure. */
|
|
10
|
+
declare function boardDrawSampleRadius(size: number, pressure: number): number;
|
|
11
|
+
/** Exact local bounds required by the draw node contract. */
|
|
12
|
+
declare function boardDrawBounds(points: readonly DrawPoint[], size: number): Pick<BoardFrame, "x" | "y" | "width" | "height">;
|
|
13
|
+
declare function boardArrowFrame(input: BoardArrowGeometryInput): BoardFrame;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { BoardArrowGeometryInput, boardArrowFrame, boardDrawBounds, boardDrawSampleRadius };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
//#region ../protocol/dist/board-geometry.js
|
|
2
|
+
/**
|
|
3
|
+
* Renderer-independent Board geometry contracts.
|
|
4
|
+
*
|
|
5
|
+
* Renderers build their own primitives from these results, but validation and
|
|
6
|
+
* item factories use the same coordinate and bounds rules.
|
|
7
|
+
*/
|
|
8
|
+
/** Radius of a draw sample in world units given stroke size and pressure. */
|
|
9
|
+
function boardDrawSampleRadius(size, pressure) {
|
|
10
|
+
const clamped = Math.min(1, Math.max(0, pressure));
|
|
11
|
+
return Math.max(.5, size / 2 * (.5 + clamped));
|
|
12
|
+
}
|
|
13
|
+
/** Convert persisted frame-local draw points to world-space authoring points. */
|
|
14
|
+
function boardDrawPointsToWorld(points, x, y) {
|
|
15
|
+
return points.map((point) => ({
|
|
16
|
+
x: point.x + x,
|
|
17
|
+
y: point.y + y,
|
|
18
|
+
p: point.p
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
/** Convert world-space authoring points to persisted frame-local points. */
|
|
22
|
+
function boardDrawPointsToLocal(points, x, y) {
|
|
23
|
+
return points.map((point) => ({
|
|
24
|
+
x: point.x - x,
|
|
25
|
+
y: point.y - y,
|
|
26
|
+
p: point.p
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
/** Exact local bounds required by the draw node contract. */
|
|
30
|
+
function boardDrawBounds(points, size) {
|
|
31
|
+
if (points.length === 0) return {
|
|
32
|
+
x: 0,
|
|
33
|
+
y: 0,
|
|
34
|
+
width: 1,
|
|
35
|
+
height: 1
|
|
36
|
+
};
|
|
37
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
38
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
39
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
40
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
41
|
+
for (const point of points) {
|
|
42
|
+
const radius = boardDrawSampleRadius(size, point.p);
|
|
43
|
+
minX = Math.min(minX, point.x - radius);
|
|
44
|
+
minY = Math.min(minY, point.y - radius);
|
|
45
|
+
maxX = Math.max(maxX, point.x + radius);
|
|
46
|
+
maxY = Math.max(maxY, point.y + radius);
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
x: minX,
|
|
50
|
+
y: minY,
|
|
51
|
+
width: Math.max(1, maxX - minX),
|
|
52
|
+
height: Math.max(1, maxY - minY)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** Resolve the world-space quadratic curve represented by a free arrow. */
|
|
56
|
+
function boardResolveArrow(input) {
|
|
57
|
+
const start = input.start;
|
|
58
|
+
const end = input.end;
|
|
59
|
+
const mid = {
|
|
60
|
+
x: (start.x + end.x) / 2,
|
|
61
|
+
y: (start.y + end.y) / 2
|
|
62
|
+
};
|
|
63
|
+
if (!input.bend) return {
|
|
64
|
+
start,
|
|
65
|
+
end,
|
|
66
|
+
control: mid
|
|
67
|
+
};
|
|
68
|
+
const dx = end.x - start.x;
|
|
69
|
+
const dy = end.y - start.y;
|
|
70
|
+
const length = Math.hypot(dx, dy) || 1;
|
|
71
|
+
const offset = input.bend * length;
|
|
72
|
+
return {
|
|
73
|
+
start,
|
|
74
|
+
end,
|
|
75
|
+
control: {
|
|
76
|
+
x: mid.x + -dy / length * offset,
|
|
77
|
+
y: mid.y + dx / length * offset
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** Sample an already resolved arrow curve. */
|
|
82
|
+
function boardSampleResolvedArrow(resolved, segments) {
|
|
83
|
+
const { start, control, end } = resolved;
|
|
84
|
+
const count = Math.max(1, Math.floor(segments));
|
|
85
|
+
const points = [];
|
|
86
|
+
for (let index = 0; index <= count; index += 1) {
|
|
87
|
+
const t = index / count;
|
|
88
|
+
const mt = 1 - t;
|
|
89
|
+
points.push({
|
|
90
|
+
x: mt * mt * start.x + 2 * mt * t * control.x + t * t * end.x,
|
|
91
|
+
y: mt * mt * start.y + 2 * mt * t * control.y + t * t * end.y
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return points;
|
|
95
|
+
}
|
|
96
|
+
/** World bounds of the curve, padded for the stroke. */
|
|
97
|
+
function boardArrowBounds(input) {
|
|
98
|
+
const { start, control, end } = boardResolveArrow(input);
|
|
99
|
+
const points = [start, end];
|
|
100
|
+
for (const axis of ["x", "y"]) {
|
|
101
|
+
const denominator = start[axis] - 2 * control[axis] + end[axis];
|
|
102
|
+
if (Math.abs(denominator) < 1e-12) continue;
|
|
103
|
+
const t = (start[axis] - control[axis]) / denominator;
|
|
104
|
+
if (t > 0 && t < 1) {
|
|
105
|
+
const mt = 1 - t;
|
|
106
|
+
points.push({
|
|
107
|
+
x: mt * mt * start.x + 2 * mt * t * control.x + t * t * end.x,
|
|
108
|
+
y: mt * mt * start.y + 2 * mt * t * control.y + t * t * end.y
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
113
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
114
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
115
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
116
|
+
for (const point of points) {
|
|
117
|
+
minX = Math.min(minX, point.x);
|
|
118
|
+
minY = Math.min(minY, point.y);
|
|
119
|
+
maxX = Math.max(maxX, point.x);
|
|
120
|
+
maxY = Math.max(maxY, point.y);
|
|
121
|
+
}
|
|
122
|
+
const pad = Math.max(8, input.size * 2);
|
|
123
|
+
return {
|
|
124
|
+
x: minX - pad,
|
|
125
|
+
y: minY - pad,
|
|
126
|
+
width: Math.max(1, maxX - minX + pad * 2),
|
|
127
|
+
height: Math.max(1, maxY - minY + pad * 2)
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function boardArrowFrame(input) {
|
|
131
|
+
return {
|
|
132
|
+
...boardArrowBounds(input),
|
|
133
|
+
rotation: 0
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
export { boardArrowBounds, boardArrowFrame, boardDrawBounds, boardDrawPointsToLocal, boardDrawPointsToWorld, boardDrawSampleRadius, boardResolveArrow, boardSampleResolvedArrow };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BOARD_ARROW_STROKE_SIZE } from "./board-constants.js";
|
|
2
2
|
import { BoardTaskSnapshotSchema } from "./board-document.js";
|
|
3
|
+
import { boardArrowFrame, boardDrawBounds } from "./board-geometry.js";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
//#region ../protocol/dist/board-node.js
|
|
5
6
|
const BOARD_COLOR_IDS = [
|
|
@@ -155,5 +156,97 @@ function jsonSchema(schema) {
|
|
|
155
156
|
return z.toJSONSchema(schema);
|
|
156
157
|
}
|
|
157
158
|
jsonSchema(nodeEnvelopeSchema), Object.fromEntries(BOARD_NATIVE_NODE_TYPES.map((type) => [type, jsonSchema(dataSchemas[type])])), Object.fromEntries(BOARD_NATIVE_NODE_TYPES.map((type) => [type, jsonSchema(viewSchemas[type])]));
|
|
159
|
+
function issueDiagnostic(issue, path) {
|
|
160
|
+
const fullPath = [path, ...issue.path].join(".");
|
|
161
|
+
const values = "values" in issue && Array.isArray(issue.values) ? issue.values.filter((value) => typeof value === "string") : void 0;
|
|
162
|
+
return {
|
|
163
|
+
severity: "error",
|
|
164
|
+
code: "INVALID_BOARD_NODE",
|
|
165
|
+
message: `${fullPath}: ${issue.message}`,
|
|
166
|
+
path: fullPath,
|
|
167
|
+
...values?.length ? { allowedValues: values } : {}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function drawGeometryDiagnostic(node, data, path) {
|
|
171
|
+
const { x: minX, y: minY, width, height } = boardDrawBounds(data.points, data.size);
|
|
172
|
+
const tolerance = Math.max(.01, node.width * 1e-6, node.height * 1e-6);
|
|
173
|
+
if (Math.abs(minX) <= tolerance && Math.abs(minY) <= tolerance && Math.abs(width - node.width) <= tolerance && Math.abs(height - node.height) <= tolerance) return null;
|
|
174
|
+
return {
|
|
175
|
+
severity: "error",
|
|
176
|
+
code: "INVALID_BOARD_GEOMETRY",
|
|
177
|
+
message: `${path}.data.points must use frame-local coordinates and match the node frame`,
|
|
178
|
+
path: `${path}.data.points`,
|
|
179
|
+
expected: `frame-local bounds x=0, y=0, width=${width}, height=${height}`,
|
|
180
|
+
received: {
|
|
181
|
+
frame: {
|
|
182
|
+
x: node.x,
|
|
183
|
+
y: node.y,
|
|
184
|
+
width: node.width,
|
|
185
|
+
height: node.height
|
|
186
|
+
},
|
|
187
|
+
pointsBounds: {
|
|
188
|
+
x: minX,
|
|
189
|
+
y: minY,
|
|
190
|
+
width,
|
|
191
|
+
height
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
coordinateSpace: "frame-local"
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function validateBoardNodeInput(node, path = "node") {
|
|
198
|
+
const envelopeResult = nodeEnvelopeSchema.safeParse(node);
|
|
199
|
+
if (!envelopeResult.success) return envelopeResult.error.issues.map((issue) => issueDiagnostic(issue, path));
|
|
200
|
+
if (typeof node.type !== "string" || !BOARD_NATIVE_NODE_TYPES.includes(node.type)) return [{
|
|
201
|
+
severity: "error",
|
|
202
|
+
code: "INVALID_BOARD_NODE",
|
|
203
|
+
message: `${path}.type is not supported`,
|
|
204
|
+
path: `${path}.type`,
|
|
205
|
+
expected: "BoardNativeNodeType",
|
|
206
|
+
received: node.type,
|
|
207
|
+
allowedValues: BOARD_NATIVE_NODE_TYPES
|
|
208
|
+
}];
|
|
209
|
+
const type = node.type;
|
|
210
|
+
const dataResult = dataSchemas[type].safeParse(node.data ?? {});
|
|
211
|
+
if (!dataResult.success) return dataResult.error.issues.map((issue) => issueDiagnostic(issue, `${path}.data`));
|
|
212
|
+
const viewResult = viewSchemas[type].safeParse(node.view ?? {});
|
|
213
|
+
if (!viewResult.success) return viewResult.error.issues.map((issue) => issueDiagnostic(issue, `${path}.view`));
|
|
214
|
+
if (type === "image" || type === "video" || type === "audio" || type === "file") {
|
|
215
|
+
if (node.refKind !== "space_file" || typeof node.refPath !== "string" || !node.refPath) return [{
|
|
216
|
+
severity: "error",
|
|
217
|
+
code: "INVALID_BOARD_NODE",
|
|
218
|
+
message: `${path} requires a space file reference`,
|
|
219
|
+
path: `${path}.refPath`,
|
|
220
|
+
expected: "non-empty refPath with refKind space_file"
|
|
221
|
+
}];
|
|
222
|
+
}
|
|
223
|
+
if (type === "draw") {
|
|
224
|
+
const diagnostic = drawGeometryDiagnostic(node, dataResult.data, path);
|
|
225
|
+
return diagnostic ? [diagnostic] : [];
|
|
226
|
+
}
|
|
227
|
+
if (type === "arrow") {
|
|
228
|
+
const data = dataResult.data;
|
|
229
|
+
const expected = boardArrowFrame(data);
|
|
230
|
+
const tolerance = Math.max(.01, node.width * 1e-6, node.height * 1e-6);
|
|
231
|
+
if (Math.abs(node.x - expected.x) > tolerance || Math.abs(node.y - expected.y) > tolerance || Math.abs(node.width - expected.width) > tolerance || Math.abs(node.height - expected.height) > tolerance) return [{
|
|
232
|
+
severity: "error",
|
|
233
|
+
code: "INVALID_BOARD_GEOMETRY",
|
|
234
|
+
message: `${path}.data endpoints must use world-space coordinates and match the arrow frame`,
|
|
235
|
+
path: `${path}.data`,
|
|
236
|
+
expected: `world-space curve bounds x=${expected.x}, y=${expected.y}, width=${expected.width}, height=${expected.height}`,
|
|
237
|
+
received: {
|
|
238
|
+
frame: {
|
|
239
|
+
x: node.x,
|
|
240
|
+
y: node.y,
|
|
241
|
+
width: node.width,
|
|
242
|
+
height: node.height
|
|
243
|
+
},
|
|
244
|
+
curveBounds: expected
|
|
245
|
+
},
|
|
246
|
+
coordinateSpace: "world"
|
|
247
|
+
}];
|
|
248
|
+
}
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
158
251
|
//#endregion
|
|
159
|
-
export { BOARD_COLOR_IDS, BOARD_GEO_KINDS, BOARD_NATIVE_NODE_TYPES, BoardColorIdSchema, BoardGeoKindSchema };
|
|
252
|
+
export { BOARD_COLOR_IDS, BOARD_GEO_KINDS, BOARD_NATIVE_NODE_TYPES, BoardColorIdSchema, BoardGeoKindSchema, validateBoardNodeInput };
|
|
@@ -78,7 +78,7 @@ const BoardCameraFocusParamsSchema = z.object({
|
|
|
78
78
|
path: ["minZoom"]
|
|
79
79
|
});
|
|
80
80
|
});
|
|
81
|
-
z.object({
|
|
81
|
+
const BoardNodeInputSchema = z.object({
|
|
82
82
|
nodeId: idSchema,
|
|
83
83
|
type: z.string().min(1).max(40),
|
|
84
84
|
parentId: idSchema.nullable(),
|
|
@@ -195,4 +195,4 @@ function isBoardPath(path) {
|
|
|
195
195
|
return path.toLowerCase().endsWith(BOARD_EXTENSION);
|
|
196
196
|
}
|
|
197
197
|
//#endregion
|
|
198
|
-
export { BOARD_AUTHORING_SCHEMAS, BOARD_DOCUMENT_KIND, BOARD_EXTENSION, BOARD_MANIFEST_KIND, BoardCameraFocusParamsSchema, BoardCameraFocusSchema, BoardCameraStateSchema, BoardCreateInputSchema, BoardManifestSchema, InvalidBoardFileError, isBoardPath, parseBoardManifest, serializeBoardManifest };
|
|
198
|
+
export { BOARD_AUTHORING_SCHEMAS, BOARD_DOCUMENT_KIND, BOARD_EXTENSION, BOARD_MANIFEST_KIND, BoardCameraFocusParamsSchema, BoardCameraFocusSchema, BoardCameraStateSchema, BoardCreateInputSchema, BoardManifestSchema, BoardNodeInputSchema, InvalidBoardFileError, isBoardPath, parseBoardManifest, serializeBoardManifest };
|
|
@@ -8,8 +8,9 @@ import "./board-capability-registry.js";
|
|
|
8
8
|
import { BOARD_COLOR_IDS, BOARD_GEO_KINDS, BoardColorId, BoardGeoKind } from "./board-node.js";
|
|
9
9
|
import "./board-codec.js";
|
|
10
10
|
import "./board-content.js";
|
|
11
|
+
import { BoardArrowGeometryInput, boardArrowFrame, boardDrawBounds, boardDrawSampleRadius } from "./board-geometry.js";
|
|
11
12
|
import "./realtime/board-awareness.js";
|
|
12
13
|
import "./app.js";
|
|
13
14
|
import "./realtime/types.js";
|
|
14
15
|
import "./app-catalog.js";
|
|
15
|
-
export { AUTO_BOARD_CONNECTION_ANCHOR, BOARD_COLOR_IDS, BOARD_CONNECTION_DIRECTIONS, BOARD_CONNECTION_LINES, BOARD_CONNECTION_ROUTINGS, BOARD_CONNECTION_SIDES, BOARD_DOCUMENT_KIND, BOARD_EXTENSION, BOARD_GEO_KINDS, BOARD_STROKE_MAX_SIZE, BOARD_STROKE_MIN_SIZE, BoardAnimationTarget, BoardAnimationTargetSchema, type BoardAssetRef, BoardAuthoringItem, BoardAuthoringItemSchema, BoardAuthoringSnapshot, BoardCameraFocus, BoardCameraFocusParams, BoardCameraFocusParamsSchema, BoardCameraFocusSchema, BoardCameraState, BoardCameraStateSchema, BoardCapability, BoardColorId, BoardComposition, BoardCompositionInput, BoardCompositionInputSchema, BoardCompositionSchema, BoardConnection, BoardConnectionAnchor, BoardConnectionAnchorSchema, BoardConnectionDirection, BoardConnectionEndpoint, BoardConnectionEndpointSchema, BoardConnectionInput, BoardConnectionLine, BoardConnectionPatch, BoardConnectionPatchSchema, BoardConnectionRecord, BoardConnectionRouting, BoardConnectionRoutingConfig, BoardConnectionRoutingSchema, BoardConnectionSchema, BoardConnectionSide, BoardConnectionStyle, BoardConnectionStyleSchema, BoardConnectionWaypointSchema, BoardDiagnostic, BoardEasing, BoardEasingSchema, type BoardEffect, BoardGeoKind, BoardManifest, BoardManifestSchema, BoardMutationReceipt, BoardPlaybackSnapshot, BoardPlaybackStatus, BoardProceduralClip, BoardProceduralClipSchema, BoardRelationSchema, BoardRenderCost, BoardSemanticCommand, BoardSemanticCommandSchema, BoardTimelineMarker, BoardTimelineMarkerSchema, BoardTrack, BoardTrackInterpolation, BoardTrackSchema, BoardValidationResult, DEFAULT_BOARD_CONNECTION_ROUTING, DEFAULT_BOARD_CONNECTION_STYLE, DEFAULT_BOARD_RELATION, clampBoardStrokeSize, connectionItemIds, connectionOtherItemId, connectionTouchesItem, createBoardConnection, flipBoardConnection, isBoardPath, normalizeBoardConnectionStyle, parseBoardManifest, serializeBoardManifest };
|
|
16
|
+
export { AUTO_BOARD_CONNECTION_ANCHOR, BOARD_COLOR_IDS, BOARD_CONNECTION_DIRECTIONS, BOARD_CONNECTION_LINES, BOARD_CONNECTION_ROUTINGS, BOARD_CONNECTION_SIDES, BOARD_DOCUMENT_KIND, BOARD_EXTENSION, BOARD_GEO_KINDS, BOARD_STROKE_MAX_SIZE, BOARD_STROKE_MIN_SIZE, BoardAnimationTarget, BoardAnimationTargetSchema, BoardArrowGeometryInput, type BoardAssetRef, BoardAuthoringItem, BoardAuthoringItemSchema, BoardAuthoringSnapshot, BoardCameraFocus, BoardCameraFocusParams, BoardCameraFocusParamsSchema, BoardCameraFocusSchema, BoardCameraState, BoardCameraStateSchema, BoardCapability, BoardColorId, BoardComposition, BoardCompositionInput, BoardCompositionInputSchema, BoardCompositionSchema, BoardConnection, BoardConnectionAnchor, BoardConnectionAnchorSchema, BoardConnectionDirection, BoardConnectionEndpoint, BoardConnectionEndpointSchema, BoardConnectionInput, BoardConnectionLine, BoardConnectionPatch, BoardConnectionPatchSchema, BoardConnectionRecord, BoardConnectionRouting, BoardConnectionRoutingConfig, BoardConnectionRoutingSchema, BoardConnectionSchema, BoardConnectionSide, BoardConnectionStyle, BoardConnectionStyleSchema, BoardConnectionWaypointSchema, BoardDiagnostic, BoardEasing, BoardEasingSchema, type BoardEffect, BoardGeoKind, BoardManifest, BoardManifestSchema, BoardMutationReceipt, BoardPlaybackSnapshot, BoardPlaybackStatus, BoardProceduralClip, BoardProceduralClipSchema, BoardRelationSchema, BoardRenderCost, BoardSemanticCommand, BoardSemanticCommandSchema, BoardTimelineMarker, BoardTimelineMarkerSchema, BoardTrack, BoardTrackInterpolation, BoardTrackSchema, BoardValidationResult, DEFAULT_BOARD_CONNECTION_ROUTING, DEFAULT_BOARD_CONNECTION_STYLE, DEFAULT_BOARD_RELATION, boardArrowFrame, boardDrawBounds, boardDrawSampleRadius, clampBoardStrokeSize, connectionItemIds, connectionOtherItemId, connectionTouchesItem, createBoardConnection, flipBoardConnection, isBoardPath, normalizeBoardConnectionStyle, parseBoardManifest, serializeBoardManifest };
|