@neta-art/cohub 12.6.0 → 13.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 +19 -4
- package/dist/board/editor/editor.d.ts +7 -5
- package/dist/board/editor/index.js +1 -1
- package/dist/board/editor/interaction.js +1 -1
- package/dist/board/export/index.d.ts +1 -6
- package/dist/board/export/index.js +16 -10
- package/dist/board/export/node/index.d.ts +2 -7
- package/dist/board/export/node/index.js +2 -5
- package/dist/board/export/scene.d.ts +3 -7
- package/dist/board/export/scene.js +22 -26
- package/dist/board/model/asset-key.d.ts +5 -2
- package/dist/board/model/asset-key.js +13 -5
- package/dist/board/model/index.d.ts +5 -4
- package/dist/board/model/index.js +4 -3
- package/dist/board/model/media-playback.d.ts +3 -6
- package/dist/board/model/media-playback.js +4 -4
- package/dist/board/model/read.d.ts +7 -0
- package/dist/board/model/read.js +18 -0
- package/dist/board/model/themes.d.ts +50 -0
- package/dist/board/model/themes.js +44 -6
- package/dist/board/render/backdrop-layer.d.ts +21 -0
- package/dist/board/render/backdrop-layer.js +227 -0
- package/dist/board/render/backdrop.d.ts +45 -0
- package/dist/board/render/backdrop.js +62 -0
- package/dist/board/render/index.d.ts +3 -2
- package/dist/board/render/index.js +7 -6
- package/dist/board/render/renderers/arrow-card-renderer.js +1 -1
- package/dist/board/render/renderers/audio-card-renderer.js +2 -2
- package/dist/board/render/renderers/file-card-renderer.js +1 -1
- package/dist/board/render/renderers/frame-card-renderer.js +1 -1
- package/dist/board/render/renderers/model-card-renderer.js +1 -1
- package/dist/board/render/renderers/shape-card-renderer.js +1 -1
- package/dist/board/render/renderers/task-card-renderer.js +2 -2
- package/dist/board/render/renderers/text-card-renderer.js +2 -2
- package/dist/board/render/renderers/video-card-renderer.js +1 -1
- package/dist/board/render/theme.d.ts +2 -0
- package/dist/board/render/theme.js +4 -2
- package/dist/board/replica/index.d.ts +2 -2
- package/dist/board/replica/index.js +38 -39
- package/dist/board/stage/assets.d.ts +2 -2
- package/dist/board/stage/assets.js +136 -65
- package/dist/board/stage/backdrop.d.ts +25 -0
- package/dist/board/stage/backdrop.js +104 -0
- package/dist/board/stage/index.d.ts +2 -1
- package/dist/board/stage/scene.js +1 -1
- package/dist/board/stage/stage.d.ts +7 -2
- package/dist/board/stage/stage.js +45 -25
- package/dist/board/stage/texture-pool.d.ts +1 -1
- package/dist/board/stage/texture-pool.js +4 -5
- package/dist/chunks/environment.d.ts +26 -15
- package/dist/chunks/http.d.ts +31 -23
- package/dist/chunks/http.js +1205 -46
- package/dist/chunks/websocket.d.ts +1 -1
- package/dist/http.d.ts +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1077
- package/dist/media.d.ts +18 -32
- package/dist/media.js +81 -67
- package/dist/protocol/dist/board-model.d.ts +33 -26
- package/dist/protocol/dist/board-model.js +21 -18
- package/dist/protocol/dist/board.d.ts +2 -0
- package/dist/protocol/dist/board.js +5 -1
- package/dist/protocol/dist/core/media.d.ts +32 -9
- package/dist/protocol/dist/core/media.js +203 -48
- package/dist/protocol/dist/index.d.ts +2 -2
- package/package.json +1 -1
- package/dist/board/render/board-background.d.ts +0 -18
- package/dist/board/render/board-background.js +0 -104
package/dist/chunks/http.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { S as getRealtimeSpaceRoom, c as HttpError, l as HttpTransport, s as isHttpErrorCode, x as getRealtimeBoardRoom, z as isUuid } from "./http-error.js";
|
|
2
2
|
import { s as resolveApiBaseUrl, u as resolveExecutionToken } from "./environment.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
//#region ../protocol/dist/board-constants.js
|
|
5
|
-
const BOARD_ARROW_STROKE_SIZE = 2.5;
|
|
6
4
|
const BOARD_DEFAULT_ITEM_SIZE = {
|
|
7
5
|
width: 240,
|
|
8
6
|
height: 160
|
|
@@ -628,27 +626,28 @@ const BOARD_ENTER_PRESETS = [
|
|
|
628
626
|
"deal"
|
|
629
627
|
];
|
|
630
628
|
const BoardBackgroundSchema = z.object({
|
|
631
|
-
kind: z.enum([
|
|
632
|
-
"solid",
|
|
633
|
-
"dots",
|
|
634
|
-
"grid",
|
|
635
|
-
"image"
|
|
636
|
-
]).default("dots"),
|
|
637
629
|
color: BoardColorSchema.optional(),
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
"
|
|
641
|
-
"
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
630
|
+
pattern: z.enum([
|
|
631
|
+
"none",
|
|
632
|
+
"dots",
|
|
633
|
+
"grid"
|
|
634
|
+
]).optional().describe("Spaced by grid.size; omit to follow the theme."),
|
|
635
|
+
image: z.object({
|
|
636
|
+
src: BoardMediaSourceSchema,
|
|
637
|
+
fit: z.enum([
|
|
638
|
+
"cover",
|
|
639
|
+
"contain",
|
|
640
|
+
"repeat"
|
|
641
|
+
]).default("cover").describe("cover and contain fill the view; repeat tiles the Board at one board unit per image pixel."),
|
|
642
|
+
opacity: ratio.optional()
|
|
643
|
+
}).strict().optional()
|
|
644
|
+
}).strict().describe("Layers drawn under every item: color, then image, then pattern. Without a color or image the theme's backdrop shows.");
|
|
646
645
|
const BoardSettingsSchema = z.object({
|
|
647
646
|
title: z.string().min(1).max(255).optional(),
|
|
648
|
-
background: BoardBackgroundSchema.default({
|
|
647
|
+
background: BoardBackgroundSchema.default({}),
|
|
649
648
|
grid: z.object({
|
|
650
|
-
|
|
651
|
-
|
|
649
|
+
size: finite.min(4).max(512).default(24).describe("Pattern spacing and snap step, in board units."),
|
|
650
|
+
snap: z.boolean().default(false).describe("Snap moved and resized items to the grid.")
|
|
652
651
|
}).strict().optional(),
|
|
653
652
|
enter: z.object({
|
|
654
653
|
preset: z.enum(BOARD_ENTER_PRESETS),
|
|
@@ -706,10 +705,14 @@ z.object({
|
|
|
706
705
|
items: z.array(z.string().min(1)).max(5e4).optional(),
|
|
707
706
|
animations: z.array(z.string().min(1)).max(1e4).optional(),
|
|
708
707
|
limit: z.number().int().min(1).max(5e4).optional(),
|
|
709
|
-
cursor: z.string().min(1).optional()
|
|
708
|
+
cursor: z.string().min(1).optional(),
|
|
709
|
+
since: z.number().int().nonnegative().optional()
|
|
710
710
|
}).strict().refine((read) => !(read.cursor && (read.rect || read.within)), {
|
|
711
711
|
path: ["cursor"],
|
|
712
712
|
message: "cursor pages by id, so it cannot be combined with rect or within"
|
|
713
|
+
}).refine((read) => read.since === void 0 || Object.entries(read).every(([key, value]) => key === "since" || value === void 0), {
|
|
714
|
+
path: ["since"],
|
|
715
|
+
message: "since syncs the whole Board, so it cannot be combined with other read options"
|
|
713
716
|
});
|
|
714
717
|
z.object({
|
|
715
718
|
patch: BoardPatchSchema,
|
|
@@ -763,6 +766,1082 @@ z.discriminatedUnion("type", [
|
|
|
763
766
|
}).strict()
|
|
764
767
|
]);
|
|
765
768
|
//#endregion
|
|
769
|
+
//#region ../protocol/dist/board-json.js
|
|
770
|
+
/** Structural equality for JSON-shaped semantic values. */
|
|
771
|
+
function boardJsonEquals(a, b) {
|
|
772
|
+
if (a === b) return true;
|
|
773
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return Number.isNaN(a) && Number.isNaN(b);
|
|
774
|
+
const aArray = Array.isArray(a);
|
|
775
|
+
const bArray = Array.isArray(b);
|
|
776
|
+
if (aArray !== bArray) return false;
|
|
777
|
+
if (aArray && bArray) return a.length === b.length && a.every((value, index) => boardJsonEquals(value, b[index]));
|
|
778
|
+
const aRecord = a;
|
|
779
|
+
const bRecord = b;
|
|
780
|
+
const keys = Object.keys(aRecord);
|
|
781
|
+
return keys.length === Object.keys(bRecord).length && keys.every((key) => Object.hasOwn(bRecord, key) && boardJsonEquals(aRecord[key], bRecord[key]));
|
|
782
|
+
}
|
|
783
|
+
//#endregion
|
|
784
|
+
//#region ../protocol/dist/board-layout.js
|
|
785
|
+
const IDENTITY_MATRIX = [
|
|
786
|
+
1,
|
|
787
|
+
0,
|
|
788
|
+
0,
|
|
789
|
+
1,
|
|
790
|
+
0,
|
|
791
|
+
0
|
|
792
|
+
];
|
|
793
|
+
function multiplyMatrix(parent, child) {
|
|
794
|
+
const [a1, b1, c1, d1, tx1, ty1] = parent;
|
|
795
|
+
const [a2, b2, c2, d2, tx2, ty2] = child;
|
|
796
|
+
return [
|
|
797
|
+
a1 * a2 + c1 * b2,
|
|
798
|
+
b1 * a2 + d1 * b2,
|
|
799
|
+
a1 * c2 + c1 * d2,
|
|
800
|
+
b1 * c2 + d1 * d2,
|
|
801
|
+
a1 * tx2 + c1 * ty2 + tx1,
|
|
802
|
+
b1 * tx2 + d1 * ty2 + ty1
|
|
803
|
+
];
|
|
804
|
+
}
|
|
805
|
+
function invertMatrix(matrix) {
|
|
806
|
+
const [a, b, c, d, tx, ty] = matrix;
|
|
807
|
+
const det = a * d - b * c || 1e-12;
|
|
808
|
+
return [
|
|
809
|
+
d / det,
|
|
810
|
+
-b / det,
|
|
811
|
+
-c / det,
|
|
812
|
+
a / det,
|
|
813
|
+
(c * ty - d * tx) / det,
|
|
814
|
+
(b * tx - a * ty) / det
|
|
815
|
+
];
|
|
816
|
+
}
|
|
817
|
+
function applyMatrix(matrix, point) {
|
|
818
|
+
const [a, b, c, d, tx, ty] = matrix;
|
|
819
|
+
return {
|
|
820
|
+
x: a * point.x + c * point.y + tx,
|
|
821
|
+
y: b * point.x + d * point.y + ty
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
function scaleVector(scale) {
|
|
825
|
+
return typeof scale === "number" ? {
|
|
826
|
+
x: scale,
|
|
827
|
+
y: scale
|
|
828
|
+
} : scale;
|
|
829
|
+
}
|
|
830
|
+
function isWideCharacter(code) {
|
|
831
|
+
return code >= 4352 && code <= 4447 || code >= 11904 && code <= 42191 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65072 && code <= 65103 || code >= 65280 && code <= 65376 || code >= 65504 && code <= 65510 || code >= 127744 && code <= 129791 || code >= 131072 && code <= 262141;
|
|
832
|
+
}
|
|
833
|
+
function estimateTextWidth(text, fontSize) {
|
|
834
|
+
let ems = 0;
|
|
835
|
+
for (const character of text) {
|
|
836
|
+
const code = character.codePointAt(0) ?? 0;
|
|
837
|
+
if (character === " ") ems += .28;
|
|
838
|
+
else if (isWideCharacter(code)) ems += 1;
|
|
839
|
+
else if (/[il.,:;'|!]/.test(character)) ems += .3;
|
|
840
|
+
else if (/[mwMW@]/.test(character)) ems += .85;
|
|
841
|
+
else if (/[A-Z0-9]/.test(character)) ems += .64;
|
|
842
|
+
else ems += .55;
|
|
843
|
+
}
|
|
844
|
+
return ems * fontSize;
|
|
845
|
+
}
|
|
846
|
+
function estimateLines(text, { fontSize }, maxWidth) {
|
|
847
|
+
const measure = (line) => estimateTextWidth(line, fontSize);
|
|
848
|
+
const paragraphs = text.split("\n");
|
|
849
|
+
const lines = maxWidth ? paragraphs.flatMap((paragraph) => wrapParagraph(paragraph, maxWidth, measure)) : paragraphs;
|
|
850
|
+
return {
|
|
851
|
+
lines,
|
|
852
|
+
width: lines.reduce((max, line) => Math.max(max, measure(line)), 0)
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
function wrapParagraph(paragraph, width, measure) {
|
|
856
|
+
if (!paragraph) return [""];
|
|
857
|
+
const tokens = paragraph.match(/[\u1100-\u115f\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\uff00-\uff60]|[^\s\u1100-\u115f\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\uff00-\uff60]+|\s+/g) ?? [paragraph];
|
|
858
|
+
const lines = [];
|
|
859
|
+
let line = "";
|
|
860
|
+
for (const token of tokens) {
|
|
861
|
+
const candidate = line + token;
|
|
862
|
+
if (!line || measure(candidate.trimEnd()) <= width) {
|
|
863
|
+
line = candidate;
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
lines.push(line.trimEnd());
|
|
867
|
+
line = /^\s+$/.test(token) ? "" : token;
|
|
868
|
+
}
|
|
869
|
+
lines.push(line.trimEnd());
|
|
870
|
+
return lines;
|
|
871
|
+
}
|
|
872
|
+
const textSizeCache = /* @__PURE__ */ new Map();
|
|
873
|
+
const TEXT_CACHE_LIMIT = 4096;
|
|
874
|
+
function clampBoardFontSize(fontSize) {
|
|
875
|
+
return Math.min(512, Math.max(2, fontSize));
|
|
876
|
+
}
|
|
877
|
+
function layoutBoardText(props) {
|
|
878
|
+
const fontSize = clampBoardFontSize(props.fontSize ?? 24);
|
|
879
|
+
const key = `${fontSize}|${props.fontWeight}|${props.font}|${props.lineHeight}|${props.width ?? ""}|${props.text}`;
|
|
880
|
+
const cached = textSizeCache.get(key);
|
|
881
|
+
if (cached) return cached;
|
|
882
|
+
const font = {
|
|
883
|
+
fontSize,
|
|
884
|
+
fontWeight: props.fontWeight ?? 500,
|
|
885
|
+
font: props.font ?? "sans"
|
|
886
|
+
};
|
|
887
|
+
const { lines, width } = estimateLines(props.text || "", font, props.width || void 0);
|
|
888
|
+
const lineHeight = fontSize * (props.lineHeight ?? 4 / 3);
|
|
889
|
+
const layout = {
|
|
890
|
+
width: Math.max(props.width ?? 0, width, fontSize * .5),
|
|
891
|
+
height: Math.max(1, lines.length) * lineHeight,
|
|
892
|
+
lines,
|
|
893
|
+
lineHeight
|
|
894
|
+
};
|
|
895
|
+
if (textSizeCache.size >= TEXT_CACHE_LIMIT) textSizeCache.clear();
|
|
896
|
+
textSizeCache.set(key, layout);
|
|
897
|
+
return layout;
|
|
898
|
+
}
|
|
899
|
+
function drawSampleRadius(strokeWidth, pressure) {
|
|
900
|
+
const clamped = Math.min(1, Math.max(0, pressure));
|
|
901
|
+
return Math.max(.5, strokeWidth / 2 * (.5 + clamped));
|
|
902
|
+
}
|
|
903
|
+
function drawPointsBounds(points, strokeWidth) {
|
|
904
|
+
if (points.length === 0) return {
|
|
905
|
+
x: 0,
|
|
906
|
+
y: 0,
|
|
907
|
+
width: 1,
|
|
908
|
+
height: 1
|
|
909
|
+
};
|
|
910
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
911
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
912
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
913
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
914
|
+
for (const point of points) {
|
|
915
|
+
const radius = drawSampleRadius(strokeWidth, point.p);
|
|
916
|
+
minX = Math.min(minX, point.x - radius);
|
|
917
|
+
minY = Math.min(minY, point.y - radius);
|
|
918
|
+
maxX = Math.max(maxX, point.x + radius);
|
|
919
|
+
maxY = Math.max(maxY, point.y + radius);
|
|
920
|
+
}
|
|
921
|
+
return {
|
|
922
|
+
x: minX,
|
|
923
|
+
y: minY,
|
|
924
|
+
width: Math.max(1, maxX - minX),
|
|
925
|
+
height: Math.max(1, maxY - minY)
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
function pointsRect(points) {
|
|
929
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
930
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
931
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
932
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
933
|
+
for (const point of points) {
|
|
934
|
+
minX = Math.min(minX, point.x);
|
|
935
|
+
minY = Math.min(minY, point.y);
|
|
936
|
+
maxX = Math.max(maxX, point.x);
|
|
937
|
+
maxY = Math.max(maxY, point.y);
|
|
938
|
+
}
|
|
939
|
+
if (!Number.isFinite(minX)) return {
|
|
940
|
+
x: 0,
|
|
941
|
+
y: 0,
|
|
942
|
+
width: 1,
|
|
943
|
+
height: 1
|
|
944
|
+
};
|
|
945
|
+
return {
|
|
946
|
+
x: minX,
|
|
947
|
+
y: minY,
|
|
948
|
+
width: Math.max(1, maxX - minX),
|
|
949
|
+
height: Math.max(1, maxY - minY)
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
function isArrowBinding(end) {
|
|
953
|
+
return typeof end.item === "string";
|
|
954
|
+
}
|
|
955
|
+
function arrowBindings(item) {
|
|
956
|
+
if (item.type !== "arrow") return [];
|
|
957
|
+
const props = item.props;
|
|
958
|
+
const ids = [props.start, props.end].filter(isArrowBinding).map((end) => end.item);
|
|
959
|
+
return [...new Set(ids)];
|
|
960
|
+
}
|
|
961
|
+
function bendControl(start, end, bend) {
|
|
962
|
+
const mid = {
|
|
963
|
+
x: (start.x + end.x) / 2,
|
|
964
|
+
y: (start.y + end.y) / 2
|
|
965
|
+
};
|
|
966
|
+
if (!bend) return mid;
|
|
967
|
+
const dx = end.x - start.x;
|
|
968
|
+
const dy = end.y - start.y;
|
|
969
|
+
const length = Math.hypot(dx, dy) || 1;
|
|
970
|
+
const offset = bend * length;
|
|
971
|
+
return {
|
|
972
|
+
x: mid.x + -dy / length * offset,
|
|
973
|
+
y: mid.y + dx / length * offset
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
const MAX_DEPTH = 64;
|
|
977
|
+
function createBoardLayout(initialLookup, dependents) {
|
|
978
|
+
let lookup = initialLookup;
|
|
979
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
980
|
+
const matrices = /* @__PURE__ */ new Map();
|
|
981
|
+
const bounds = /* @__PURE__ */ new Map();
|
|
982
|
+
function matrixOf(id, depth = 0) {
|
|
983
|
+
const cached = matrices.get(id);
|
|
984
|
+
if (cached) return cached;
|
|
985
|
+
const item = lookup(id);
|
|
986
|
+
if (!item) return IDENTITY_MATRIX;
|
|
987
|
+
const matrix = multiplyMatrix(item.parent && depth < MAX_DEPTH && lookup(item.parent) ? matrixOf(item.parent, depth + 1) : IDENTITY_MATRIX, localMatrixOf(id));
|
|
988
|
+
matrices.set(id, matrix);
|
|
989
|
+
return matrix;
|
|
990
|
+
}
|
|
991
|
+
function localMatrixOf(id) {
|
|
992
|
+
const item = lookup(id);
|
|
993
|
+
if (!item) return IDENTITY_MATRIX;
|
|
994
|
+
if (item.type === "arrow") return translate(item.position);
|
|
995
|
+
const box = boxOf(id);
|
|
996
|
+
const scale = scaleVector(item.scale);
|
|
997
|
+
const radians = item.rotation * Math.PI / 180;
|
|
998
|
+
const cos = Math.cos(radians);
|
|
999
|
+
const sin = Math.sin(radians);
|
|
1000
|
+
const ox = box.x + item.origin.x * box.width;
|
|
1001
|
+
const oy = box.y + item.origin.y * box.height;
|
|
1002
|
+
const a = cos * scale.x;
|
|
1003
|
+
const b = sin * scale.x;
|
|
1004
|
+
const c = -sin * scale.y;
|
|
1005
|
+
const d = cos * scale.y;
|
|
1006
|
+
return [
|
|
1007
|
+
a,
|
|
1008
|
+
b,
|
|
1009
|
+
c,
|
|
1010
|
+
d,
|
|
1011
|
+
item.position.x + ox - (a * ox + c * oy),
|
|
1012
|
+
item.position.y + oy - (b * ox + d * oy)
|
|
1013
|
+
];
|
|
1014
|
+
}
|
|
1015
|
+
function boxOf(id, depth = 0) {
|
|
1016
|
+
const cached = boxes.get(id);
|
|
1017
|
+
if (cached) return cached;
|
|
1018
|
+
const item = lookup(id);
|
|
1019
|
+
let box;
|
|
1020
|
+
if (!item) box = {
|
|
1021
|
+
x: 0,
|
|
1022
|
+
y: 0,
|
|
1023
|
+
width: 1,
|
|
1024
|
+
height: 1
|
|
1025
|
+
};
|
|
1026
|
+
else if ("size" in item && item.size) box = {
|
|
1027
|
+
x: 0,
|
|
1028
|
+
y: 0,
|
|
1029
|
+
width: item.size.width,
|
|
1030
|
+
height: item.size.height
|
|
1031
|
+
};
|
|
1032
|
+
else if (item.type === "text") {
|
|
1033
|
+
const layout = layoutBoardText(item.props);
|
|
1034
|
+
box = {
|
|
1035
|
+
x: 0,
|
|
1036
|
+
y: 0,
|
|
1037
|
+
width: layout.width,
|
|
1038
|
+
height: layout.height
|
|
1039
|
+
};
|
|
1040
|
+
} else if (item.type === "draw") box = drawPointsBounds(item.props.points, item.style.strokeWidth ?? 4);
|
|
1041
|
+
else if (item.type === "arrow") {
|
|
1042
|
+
const props = item.props;
|
|
1043
|
+
const toLocal = invertMatrix(multiplyMatrix(parentOf(item, depth), translate(item.position)));
|
|
1044
|
+
const start = isArrowBinding(props.start) ? applyMatrix(toLocal, resolveBoundEnd(item, props.start, props.end, depth)) : props.start;
|
|
1045
|
+
const end = isArrowBinding(props.end) ? applyMatrix(toLocal, resolveBoundEnd(item, props.end, props.start, depth)) : props.end;
|
|
1046
|
+
const points = [
|
|
1047
|
+
start,
|
|
1048
|
+
end,
|
|
1049
|
+
...props.waypoints
|
|
1050
|
+
];
|
|
1051
|
+
if (props.bend) points.push(bendControl(start, end, props.bend));
|
|
1052
|
+
const rect = pointsRect(points);
|
|
1053
|
+
const pad = Math.max(8, (item.style.strokeWidth ?? 2.5) * 2);
|
|
1054
|
+
box = {
|
|
1055
|
+
x: rect.x - pad,
|
|
1056
|
+
y: rect.y - pad,
|
|
1057
|
+
width: rect.width + pad * 2,
|
|
1058
|
+
height: rect.height + pad * 2
|
|
1059
|
+
};
|
|
1060
|
+
} else box = {
|
|
1061
|
+
x: 0,
|
|
1062
|
+
y: 0,
|
|
1063
|
+
...boardDefaultSize(item.type)
|
|
1064
|
+
};
|
|
1065
|
+
boxes.set(id, box);
|
|
1066
|
+
return box;
|
|
1067
|
+
}
|
|
1068
|
+
function parentOf(item, depth) {
|
|
1069
|
+
return item.parent && depth < MAX_DEPTH && lookup(item.parent) ? matrixOf(item.parent, depth + 1) : IDENTITY_MATRIX;
|
|
1070
|
+
}
|
|
1071
|
+
function resolveBoundEnd(arrow, end, other, depth) {
|
|
1072
|
+
const target = lookup(end.item);
|
|
1073
|
+
const arrowSpace = multiplyMatrix(parentOf(arrow, depth), translate(arrow.position));
|
|
1074
|
+
if (!target || target.type === "arrow" || depth >= MAX_DEPTH) return applyMatrix(arrowSpace, {
|
|
1075
|
+
x: 0,
|
|
1076
|
+
y: 0
|
|
1077
|
+
});
|
|
1078
|
+
const frame = frameOf(end.item, depth + 1);
|
|
1079
|
+
const toward = isArrowBinding(other) ? lookup(other.item) && lookup(other.item)?.type !== "arrow" ? center(frameOf(other.item, depth + 1)) : center(frame) : applyMatrix(arrowSpace, other);
|
|
1080
|
+
return anchorPoint(frame, end.anchor, toward);
|
|
1081
|
+
}
|
|
1082
|
+
function frameOf(id, depth = 0) {
|
|
1083
|
+
boxOf(id, depth);
|
|
1084
|
+
const box = boxes.get(id);
|
|
1085
|
+
const matrix = matrixOf(id, depth);
|
|
1086
|
+
const [a, b, c, d] = matrix;
|
|
1087
|
+
const centerPoint = applyMatrix(matrix, {
|
|
1088
|
+
x: box.x + box.width / 2,
|
|
1089
|
+
y: box.y + box.height / 2
|
|
1090
|
+
});
|
|
1091
|
+
const width = box.width * Math.hypot(a, b);
|
|
1092
|
+
const height = box.height * Math.hypot(c, d);
|
|
1093
|
+
return {
|
|
1094
|
+
x: centerPoint.x - width / 2,
|
|
1095
|
+
y: centerPoint.y - height / 2,
|
|
1096
|
+
width,
|
|
1097
|
+
height,
|
|
1098
|
+
rotation: Math.atan2(b, a) * 180 / Math.PI
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
function boundsOf(id) {
|
|
1102
|
+
const cached = bounds.get(id);
|
|
1103
|
+
if (cached) return cached;
|
|
1104
|
+
const box = boxOf(id);
|
|
1105
|
+
const matrix = matrixOf(id);
|
|
1106
|
+
const rect = pointsRect([
|
|
1107
|
+
applyMatrix(matrix, {
|
|
1108
|
+
x: box.x,
|
|
1109
|
+
y: box.y
|
|
1110
|
+
}),
|
|
1111
|
+
applyMatrix(matrix, {
|
|
1112
|
+
x: box.x + box.width,
|
|
1113
|
+
y: box.y
|
|
1114
|
+
}),
|
|
1115
|
+
applyMatrix(matrix, {
|
|
1116
|
+
x: box.x + box.width,
|
|
1117
|
+
y: box.y + box.height
|
|
1118
|
+
}),
|
|
1119
|
+
applyMatrix(matrix, {
|
|
1120
|
+
x: box.x,
|
|
1121
|
+
y: box.y + box.height
|
|
1122
|
+
})
|
|
1123
|
+
]);
|
|
1124
|
+
bounds.set(id, rect);
|
|
1125
|
+
return rect;
|
|
1126
|
+
}
|
|
1127
|
+
function setLookup(nextLookup) {
|
|
1128
|
+
lookup = nextLookup;
|
|
1129
|
+
}
|
|
1130
|
+
function invalidate(ids) {
|
|
1131
|
+
if (!ids) {
|
|
1132
|
+
boxes.clear();
|
|
1133
|
+
matrices.clear();
|
|
1134
|
+
bounds.clear();
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
const queue = [...ids];
|
|
1138
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1139
|
+
while (queue.length) {
|
|
1140
|
+
const id = queue.pop();
|
|
1141
|
+
if (seen.has(id)) continue;
|
|
1142
|
+
seen.add(id);
|
|
1143
|
+
boxes.delete(id);
|
|
1144
|
+
matrices.delete(id);
|
|
1145
|
+
bounds.delete(id);
|
|
1146
|
+
if (dependents) for (const next of dependents(id)) queue.push(next);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
return {
|
|
1150
|
+
has: (id) => lookup(id) !== void 0,
|
|
1151
|
+
box: (id) => boxOf(id),
|
|
1152
|
+
matrix: (id) => matrixOf(id),
|
|
1153
|
+
localMatrix: (id) => localMatrixOf(id),
|
|
1154
|
+
parentMatrix: (id) => {
|
|
1155
|
+
const item = lookup(id);
|
|
1156
|
+
return item ? parentOf(item, 0) : IDENTITY_MATRIX;
|
|
1157
|
+
},
|
|
1158
|
+
bounds: boundsOf,
|
|
1159
|
+
frame: (id) => frameOf(id),
|
|
1160
|
+
arrowEnd: (id, which) => {
|
|
1161
|
+
const item = lookup(id);
|
|
1162
|
+
if (item?.type !== "arrow") return {
|
|
1163
|
+
x: 0,
|
|
1164
|
+
y: 0
|
|
1165
|
+
};
|
|
1166
|
+
const arrow = item;
|
|
1167
|
+
const [end, other] = which === "start" ? [arrow.props.start, arrow.props.end] : [arrow.props.end, arrow.props.start];
|
|
1168
|
+
return isArrowBinding(end) ? resolveBoundEnd(arrow, end, other, 0) : applyMatrix(matrixOf(id), end);
|
|
1169
|
+
},
|
|
1170
|
+
invalidate,
|
|
1171
|
+
setLookup
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
function translate(point) {
|
|
1175
|
+
return [
|
|
1176
|
+
1,
|
|
1177
|
+
0,
|
|
1178
|
+
0,
|
|
1179
|
+
1,
|
|
1180
|
+
point.x,
|
|
1181
|
+
point.y
|
|
1182
|
+
];
|
|
1183
|
+
}
|
|
1184
|
+
function center(frame) {
|
|
1185
|
+
return {
|
|
1186
|
+
x: frame.x + frame.width / 2,
|
|
1187
|
+
y: frame.y + frame.height / 2
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
function rotateAround(point, pivot, degrees) {
|
|
1191
|
+
if (!degrees) return point;
|
|
1192
|
+
const radians = degrees * Math.PI / 180;
|
|
1193
|
+
const cos = Math.cos(radians);
|
|
1194
|
+
const sin = Math.sin(radians);
|
|
1195
|
+
const dx = point.x - pivot.x;
|
|
1196
|
+
const dy = point.y - pivot.y;
|
|
1197
|
+
return {
|
|
1198
|
+
x: pivot.x + dx * cos - dy * sin,
|
|
1199
|
+
y: pivot.y + dx * sin + dy * cos
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
function framePoint(frame, nx, ny) {
|
|
1203
|
+
return rotateAround({
|
|
1204
|
+
x: frame.x + nx * frame.width,
|
|
1205
|
+
y: frame.y + ny * frame.height
|
|
1206
|
+
}, center(frame), frame.rotation);
|
|
1207
|
+
}
|
|
1208
|
+
function autoAnchorSide(frame, toward) {
|
|
1209
|
+
const local = rotateAround(toward, center(frame), -frame.rotation);
|
|
1210
|
+
const c = center(frame);
|
|
1211
|
+
const dx = (local.x - c.x) / Math.max(frame.width / 2, 1e-4);
|
|
1212
|
+
const dy = (local.y - c.y) / Math.max(frame.height / 2, 1e-4);
|
|
1213
|
+
if (Math.abs(dx) >= Math.abs(dy)) return dx >= 0 ? "right" : "left";
|
|
1214
|
+
return dy >= 0 ? "bottom" : "top";
|
|
1215
|
+
}
|
|
1216
|
+
function sideAnchor(side, offset) {
|
|
1217
|
+
switch (side) {
|
|
1218
|
+
case "top": return {
|
|
1219
|
+
x: offset,
|
|
1220
|
+
y: 0
|
|
1221
|
+
};
|
|
1222
|
+
case "bottom": return {
|
|
1223
|
+
x: offset,
|
|
1224
|
+
y: 1
|
|
1225
|
+
};
|
|
1226
|
+
case "left": return {
|
|
1227
|
+
x: 0,
|
|
1228
|
+
y: offset
|
|
1229
|
+
};
|
|
1230
|
+
default: return {
|
|
1231
|
+
x: 1,
|
|
1232
|
+
y: offset
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
function anchorPoint(frame, anchor, toward) {
|
|
1237
|
+
if (anchor === "auto") {
|
|
1238
|
+
const normalized = sideAnchor(autoAnchorSide(frame, toward), .5);
|
|
1239
|
+
return framePoint(frame, normalized.x, normalized.y);
|
|
1240
|
+
}
|
|
1241
|
+
if ("side" in anchor) {
|
|
1242
|
+
const normalized = sideAnchor(anchor.side, anchor.offset);
|
|
1243
|
+
return framePoint(frame, normalized.x, normalized.y);
|
|
1244
|
+
}
|
|
1245
|
+
return framePoint(frame, anchor.x, anchor.y);
|
|
1246
|
+
}
|
|
1247
|
+
//#endregion
|
|
1248
|
+
//#region ../protocol/dist/board-patch.js
|
|
1249
|
+
function slot(map, key) {
|
|
1250
|
+
let value = map[key];
|
|
1251
|
+
if (!value) {
|
|
1252
|
+
value = {};
|
|
1253
|
+
map[key] = value;
|
|
1254
|
+
}
|
|
1255
|
+
return value;
|
|
1256
|
+
}
|
|
1257
|
+
function emptyDelta() {
|
|
1258
|
+
return {
|
|
1259
|
+
items: {},
|
|
1260
|
+
animations: {},
|
|
1261
|
+
tracks: {}
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
function withoutEmpty(value) {
|
|
1265
|
+
for (const [key, entry] of Object.entries(value)) if (entry === void 0 || isRecord$2(entry) && Object.keys(entry).length === 0) delete value[key];
|
|
1266
|
+
return value;
|
|
1267
|
+
}
|
|
1268
|
+
function isRecord$2(value) {
|
|
1269
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1270
|
+
}
|
|
1271
|
+
function mergePatch(target, patch) {
|
|
1272
|
+
if (!isRecord$2(patch)) return patch;
|
|
1273
|
+
const result = isRecord$2(target) ? { ...target } : {};
|
|
1274
|
+
for (const [key, value] of Object.entries(patch)) if (value === null) delete result[key];
|
|
1275
|
+
else result[key] = mergePatch(result[key], value);
|
|
1276
|
+
return result;
|
|
1277
|
+
}
|
|
1278
|
+
function def(schema) {
|
|
1279
|
+
return schema._zod.def;
|
|
1280
|
+
}
|
|
1281
|
+
function unwrap(schema) {
|
|
1282
|
+
let current = schema;
|
|
1283
|
+
for (let guard = 0; guard < 16; guard += 1) {
|
|
1284
|
+
const d = def(current);
|
|
1285
|
+
if (d.type === "optional" || d.type === "default" || d.type === "nullable" || d.type === "readonly" || d.type === "prefault") current = d.innerType;
|
|
1286
|
+
else if (d.type === "pipe") current = d.in;
|
|
1287
|
+
else return current;
|
|
1288
|
+
}
|
|
1289
|
+
return current;
|
|
1290
|
+
}
|
|
1291
|
+
function unwrapObjectShape(schema) {
|
|
1292
|
+
const d = def(unwrap(schema));
|
|
1293
|
+
return d.type === "object" ? d.shape : null;
|
|
1294
|
+
}
|
|
1295
|
+
function schemaAtPath(schema, path) {
|
|
1296
|
+
let current = schema;
|
|
1297
|
+
for (const segment of path) {
|
|
1298
|
+
const d = def(unwrap(current));
|
|
1299
|
+
if (d.type === "object") {
|
|
1300
|
+
const shape = d.shape;
|
|
1301
|
+
if (!Object.hasOwn(shape, segment)) return null;
|
|
1302
|
+
current = shape[segment];
|
|
1303
|
+
} else if (d.type === "record") current = d.valueType;
|
|
1304
|
+
else if (d.type === "union") {
|
|
1305
|
+
const branch = d.options.find((option) => unwrapObjectShape(option)?.[segment]);
|
|
1306
|
+
if (!branch) return null;
|
|
1307
|
+
current = unwrapObjectShape(branch)[segment];
|
|
1308
|
+
} else return null;
|
|
1309
|
+
}
|
|
1310
|
+
return current;
|
|
1311
|
+
}
|
|
1312
|
+
const NOT_ANIMATABLE = /* @__PURE__ */ new Set([
|
|
1313
|
+
"type",
|
|
1314
|
+
"parent",
|
|
1315
|
+
"z",
|
|
1316
|
+
"locked",
|
|
1317
|
+
"metadata",
|
|
1318
|
+
"script"
|
|
1319
|
+
]);
|
|
1320
|
+
const CAMERA_PROPERTIES = {
|
|
1321
|
+
focus: {
|
|
1322
|
+
kind: "discrete",
|
|
1323
|
+
schema: z.union([z.string().min(1), z.object({
|
|
1324
|
+
x: z.number().finite(),
|
|
1325
|
+
y: z.number().finite(),
|
|
1326
|
+
width: z.number().finite().positive(),
|
|
1327
|
+
height: z.number().finite().positive()
|
|
1328
|
+
}).strict()]).describe("Item id or world-space rect to fit with 48 px padding; holds until the next focus keyframe.")
|
|
1329
|
+
},
|
|
1330
|
+
zoom: {
|
|
1331
|
+
kind: "number",
|
|
1332
|
+
schema: z.number().finite().positive().describe("Pixels per world unit; overrides the zoom computed by focus."),
|
|
1333
|
+
multiplicative: true
|
|
1334
|
+
},
|
|
1335
|
+
center: {
|
|
1336
|
+
kind: "vec2",
|
|
1337
|
+
schema: BoardVec2Schema.describe("World-space viewport center; overrides focus positioning using the resolved zoom.")
|
|
1338
|
+
},
|
|
1339
|
+
shake: {
|
|
1340
|
+
kind: "number",
|
|
1341
|
+
schema: z.number().finite().nonnegative().describe("Shake amplitude in screen pixels, applied after focus, zoom and center.")
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
const ANIMATION_PROPERTIES = { time: {
|
|
1345
|
+
kind: "number",
|
|
1346
|
+
schema: z.number().finite().nonnegative()
|
|
1347
|
+
} };
|
|
1348
|
+
function kindOf(schema, path) {
|
|
1349
|
+
const leaf = path[path.length - 1];
|
|
1350
|
+
if (path.length === 1 && leaf === "scale") return "scale";
|
|
1351
|
+
if (schema === BoardColorSchema || unwrap(schema) === BoardColorSchema) return "color";
|
|
1352
|
+
if (def(unwrap(schema)).type === "number") return "number";
|
|
1353
|
+
const shape = unwrapObjectShape(schema);
|
|
1354
|
+
if (shape && Object.keys(shape).length === 2 && "x" in shape && "y" in shape) return "vec2";
|
|
1355
|
+
return "discrete";
|
|
1356
|
+
}
|
|
1357
|
+
function resolveBoardProperty(targetType, property) {
|
|
1358
|
+
if (targetType === "camera") {
|
|
1359
|
+
const spec = CAMERA_PROPERTIES[property];
|
|
1360
|
+
return spec ? {
|
|
1361
|
+
ok: true,
|
|
1362
|
+
spec
|
|
1363
|
+
} : {
|
|
1364
|
+
ok: false,
|
|
1365
|
+
message: `camera has no property ${property}; expected ${Object.keys(CAMERA_PROPERTIES).join(", ")}`
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
if (targetType === "animation") {
|
|
1369
|
+
const spec = ANIMATION_PROPERTIES[property];
|
|
1370
|
+
return spec ? {
|
|
1371
|
+
ok: true,
|
|
1372
|
+
spec
|
|
1373
|
+
} : {
|
|
1374
|
+
ok: false,
|
|
1375
|
+
message: `animations are driven through time`
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
const path = property.split(".");
|
|
1379
|
+
if (path.some((segment) => !segment) || NOT_ANIMATABLE.has(path[0])) return {
|
|
1380
|
+
ok: false,
|
|
1381
|
+
message: `${property} cannot be animated`
|
|
1382
|
+
};
|
|
1383
|
+
const schema = boardItemSchema(targetType);
|
|
1384
|
+
if (!schema) return {
|
|
1385
|
+
ok: false,
|
|
1386
|
+
message: `unknown item type ${targetType}`
|
|
1387
|
+
};
|
|
1388
|
+
if (!isBuiltinItemType(targetType) && path[0] === "props") return {
|
|
1389
|
+
ok: true,
|
|
1390
|
+
spec: {
|
|
1391
|
+
kind: "discrete",
|
|
1392
|
+
schema: z.unknown()
|
|
1393
|
+
}
|
|
1394
|
+
};
|
|
1395
|
+
const leaf = schemaAtPath(schema, path);
|
|
1396
|
+
if (!leaf) return {
|
|
1397
|
+
ok: false,
|
|
1398
|
+
message: `${targetType} has no property ${property}`
|
|
1399
|
+
};
|
|
1400
|
+
const kind = kindOf(leaf, path);
|
|
1401
|
+
return {
|
|
1402
|
+
ok: true,
|
|
1403
|
+
spec: {
|
|
1404
|
+
kind,
|
|
1405
|
+
schema: unwrap(leaf),
|
|
1406
|
+
...property === "opacity" || kind === "scale" ? { multiplicative: true } : {}
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
function lastKeyframeAt(trackPatches) {
|
|
1411
|
+
let end = 0;
|
|
1412
|
+
for (const track of trackPatches) {
|
|
1413
|
+
const keyframes = track?.keyframes;
|
|
1414
|
+
if (!Array.isArray(keyframes)) continue;
|
|
1415
|
+
for (const keyframe of keyframes) {
|
|
1416
|
+
const at = keyframe?.at;
|
|
1417
|
+
if (typeof at === "number" && Number.isFinite(at)) end = Math.max(end, at);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
return end > 0 ? end : void 0;
|
|
1421
|
+
}
|
|
1422
|
+
function trackReferences(track) {
|
|
1423
|
+
const refs = /* @__PURE__ */ new Set([track.target]);
|
|
1424
|
+
if (track.target === "camera" && track.property === "focus") {
|
|
1425
|
+
for (const keyframe of track.keyframes) if (typeof keyframe.value === "string") refs.add(keyframe.value);
|
|
1426
|
+
}
|
|
1427
|
+
refs.delete(BOARD_CAMERA_TARGET);
|
|
1428
|
+
return [...refs];
|
|
1429
|
+
}
|
|
1430
|
+
function pruneBoardTrack(track, deleted) {
|
|
1431
|
+
if (deleted.has(track.target)) return null;
|
|
1432
|
+
if (track.target !== "camera" || track.property !== "focus") return track;
|
|
1433
|
+
const keyframes = track.keyframes.filter((entry) => typeof entry.value !== "string" || !deleted.has(entry.value));
|
|
1434
|
+
if (keyframes.length === track.keyframes.length) return track;
|
|
1435
|
+
return keyframes.length ? {
|
|
1436
|
+
...track,
|
|
1437
|
+
keyframes
|
|
1438
|
+
} : null;
|
|
1439
|
+
}
|
|
1440
|
+
function error(path, code, message) {
|
|
1441
|
+
return {
|
|
1442
|
+
severity: "error",
|
|
1443
|
+
code,
|
|
1444
|
+
path,
|
|
1445
|
+
message
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
function stripId(value) {
|
|
1449
|
+
if (!("id" in value)) return value;
|
|
1450
|
+
const { id: _id, ...rest } = value;
|
|
1451
|
+
return rest;
|
|
1452
|
+
}
|
|
1453
|
+
function applyBoardPatch(state, input, options = {}) {
|
|
1454
|
+
const parsedPatch = BoardPatchSchema.safeParse(input);
|
|
1455
|
+
if (!parsedPatch.success) return {
|
|
1456
|
+
ok: false,
|
|
1457
|
+
diagnostics: parsedPatch.error.issues.map((issue) => error(issue.path.map(String).join("."), "INVALID_PATCH", issue.message))
|
|
1458
|
+
};
|
|
1459
|
+
const patch = parsedPatch.data;
|
|
1460
|
+
const diagnostics = [];
|
|
1461
|
+
const nextItems = /* @__PURE__ */ new Map();
|
|
1462
|
+
const nextAnimations = /* @__PURE__ */ new Map();
|
|
1463
|
+
const nextTracks = /* @__PURE__ */ new Map();
|
|
1464
|
+
let nextSettings;
|
|
1465
|
+
const item = (id) => nextItems.has(id) ? nextItems.get(id) ?? void 0 : state.item(id);
|
|
1466
|
+
const animation = (id) => nextAnimations.has(id) ? nextAnimations.get(id) ?? void 0 : state.animation(id);
|
|
1467
|
+
const setTrack = (animationId, trackId, value) => {
|
|
1468
|
+
const tracks = nextTracks.get(animationId) ?? /* @__PURE__ */ new Map();
|
|
1469
|
+
tracks.set(trackId, value);
|
|
1470
|
+
nextTracks.set(animationId, tracks);
|
|
1471
|
+
};
|
|
1472
|
+
const track = (animationId, trackId) => {
|
|
1473
|
+
const pending = nextTracks.get(animationId);
|
|
1474
|
+
if (pending?.has(trackId)) return pending.get(trackId) ?? void 0;
|
|
1475
|
+
return state.track(animationId, trackId);
|
|
1476
|
+
};
|
|
1477
|
+
if (patch.board !== void 0) {
|
|
1478
|
+
const merged = patch.board === null ? {} : mergePatch(state.settings(), patch.board);
|
|
1479
|
+
const parsed = BoardSettingsSchema.safeParse(merged);
|
|
1480
|
+
if (parsed.success) nextSettings = parsed.data;
|
|
1481
|
+
else for (const issue of parsed.error.issues) diagnostics.push(error(["board", ...issue.path.map(String)].join("."), "INVALID_BOARD", issue.message));
|
|
1482
|
+
}
|
|
1483
|
+
const zByParent = /* @__PURE__ */ new Map();
|
|
1484
|
+
for (const [id, value] of Object.entries(patch.items ?? {})) {
|
|
1485
|
+
if (value === null) {
|
|
1486
|
+
if (state.item(id)) nextItems.set(id, null);
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
const current = state.item(id);
|
|
1490
|
+
const body = stripId(value);
|
|
1491
|
+
const candidate = !current || typeof body.type === "string" && body.type !== current.type ? { ...body } : mergePatch(current, body);
|
|
1492
|
+
if (candidate.z === void 0) {
|
|
1493
|
+
const parent = typeof candidate.parent === "string" ? candidate.parent : void 0;
|
|
1494
|
+
const key = parent ?? "";
|
|
1495
|
+
const top = zByParent.get(key) ?? state.maxZ(parent);
|
|
1496
|
+
candidate.z = current?.z ?? top + 1;
|
|
1497
|
+
if (!current) zByParent.set(key, top + 1);
|
|
1498
|
+
}
|
|
1499
|
+
const parsed = parseBoardItem(candidate, `items.${id}`);
|
|
1500
|
+
if (!parsed.ok) {
|
|
1501
|
+
diagnostics.push(...parsed.diagnostics);
|
|
1502
|
+
continue;
|
|
1503
|
+
}
|
|
1504
|
+
if (current && boardJsonEquals(current, parsed.item)) continue;
|
|
1505
|
+
nextItems.set(id, parsed.item);
|
|
1506
|
+
}
|
|
1507
|
+
for (const [id, value] of Object.entries(patch.animations ?? {})) {
|
|
1508
|
+
if (value === null) {
|
|
1509
|
+
if (!state.animation(id)) continue;
|
|
1510
|
+
nextAnimations.set(id, null);
|
|
1511
|
+
for (const trackId of state.trackIds(id)) setTrack(id, trackId, null);
|
|
1512
|
+
continue;
|
|
1513
|
+
}
|
|
1514
|
+
const { tracks, ...headerPatch } = stripId(value);
|
|
1515
|
+
if (state.item(id) || nextItems.get(id)) {
|
|
1516
|
+
diagnostics.push(error(`animations.${id}`, "ID_CONFLICT", `${id} is already an item id`));
|
|
1517
|
+
continue;
|
|
1518
|
+
}
|
|
1519
|
+
const current = state.animation(id);
|
|
1520
|
+
if (!current || Object.keys(headerPatch).length > 0) {
|
|
1521
|
+
const header = current ? mergePatch(current, headerPatch) : {
|
|
1522
|
+
...headerPatch,
|
|
1523
|
+
...headerPatch.duration === void 0 ? { duration: lastKeyframeAt([...Object.values(tracks ?? {}), ...state.trackIds(id).map((trackId) => state.track(id, trackId))]) } : {}
|
|
1524
|
+
};
|
|
1525
|
+
const parsed = BoardAnimationHeaderSchema.safeParse(header);
|
|
1526
|
+
if (!parsed.success) {
|
|
1527
|
+
for (const issue of parsed.error.issues) diagnostics.push(error([
|
|
1528
|
+
"animations",
|
|
1529
|
+
id,
|
|
1530
|
+
...issue.path.map(String)
|
|
1531
|
+
].join("."), "INVALID_ANIMATION", issue.message));
|
|
1532
|
+
continue;
|
|
1533
|
+
}
|
|
1534
|
+
if (!current || !boardJsonEquals(current, parsed.data)) nextAnimations.set(id, parsed.data);
|
|
1535
|
+
}
|
|
1536
|
+
for (const [trackId, trackPatch] of Object.entries(tracks ?? {})) {
|
|
1537
|
+
const existing = state.track(id, trackId);
|
|
1538
|
+
if (trackPatch === null) {
|
|
1539
|
+
if (existing) setTrack(id, trackId, null);
|
|
1540
|
+
continue;
|
|
1541
|
+
}
|
|
1542
|
+
const parsed = BoardTrackSchema.safeParse(existing ? mergePatch(existing, trackPatch) : trackPatch);
|
|
1543
|
+
if (!parsed.success) {
|
|
1544
|
+
for (const issue of parsed.error.issues) diagnostics.push(error([
|
|
1545
|
+
"animations",
|
|
1546
|
+
id,
|
|
1547
|
+
"tracks",
|
|
1548
|
+
trackId,
|
|
1549
|
+
...issue.path.map(String)
|
|
1550
|
+
].join("."), "INVALID_TRACK", issue.message));
|
|
1551
|
+
continue;
|
|
1552
|
+
}
|
|
1553
|
+
if (!existing || !boardJsonEquals(existing, parsed.data)) setTrack(id, trackId, parsed.data);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
for (const id of nextItems.keys()) if (nextItems.get(id) && animation(id)) diagnostics.push(error(`items.${id}`, "ID_CONFLICT", `${id} is already an animation id`));
|
|
1557
|
+
if (diagnostics.length) return {
|
|
1558
|
+
ok: false,
|
|
1559
|
+
diagnostics
|
|
1560
|
+
};
|
|
1561
|
+
const deletedItems = [...nextItems].filter(([, value]) => value === null).map(([id]) => id);
|
|
1562
|
+
const deletionLayout = createBoardLayout((id) => item(id) ?? state.item(id));
|
|
1563
|
+
const pendingDeletes = [...deletedItems];
|
|
1564
|
+
while (pendingDeletes.length) {
|
|
1565
|
+
const id = pendingDeletes.pop();
|
|
1566
|
+
for (const child of state.children(id)) {
|
|
1567
|
+
if (nextItems.has(child) && nextItems.get(child)?.parent !== id) continue;
|
|
1568
|
+
if (options.cascade) {
|
|
1569
|
+
if (nextItems.get(child) !== null) {
|
|
1570
|
+
nextItems.set(child, null);
|
|
1571
|
+
pendingDeletes.push(child);
|
|
1572
|
+
}
|
|
1573
|
+
} else diagnostics.push(error(`items.${id}`, "ITEM_REFERENCED", `${id} still has child ${child}; delete it too or use cascade`));
|
|
1574
|
+
}
|
|
1575
|
+
for (const arrowId of state.binders(id)) {
|
|
1576
|
+
const arrow = item(arrowId);
|
|
1577
|
+
if (arrow?.type !== "arrow") continue;
|
|
1578
|
+
const props = { ...arrow.props };
|
|
1579
|
+
for (const which of ["start", "end"]) {
|
|
1580
|
+
const end = props[which];
|
|
1581
|
+
if (!isArrowBinding(end) || end.item !== id) continue;
|
|
1582
|
+
const world = options.resolveArrowEnd?.(arrowId, which) ?? deletionLayout.arrowEnd(arrowId, which);
|
|
1583
|
+
props[which] = applyMatrix(invertMatrix(deletionLayout.matrix(arrowId)), world);
|
|
1584
|
+
}
|
|
1585
|
+
nextItems.set(arrowId, {
|
|
1586
|
+
...arrow,
|
|
1587
|
+
props
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
for (const ref of state.trackRefs(id)) {
|
|
1591
|
+
const current = track(ref.animation, ref.track);
|
|
1592
|
+
if (!current || !trackReferences(current).includes(id)) continue;
|
|
1593
|
+
if (options.cascade) setTrack(ref.animation, ref.track, pruneBoardTrack(current, /* @__PURE__ */ new Set([id])));
|
|
1594
|
+
else diagnostics.push(error(`items.${id}`, "ITEM_REFERENCED", `${id} is animated by ${ref.animation}.${ref.track}; delete the track or use cascade`));
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
for (const [id, value] of nextAnimations) {
|
|
1598
|
+
if (value !== null) continue;
|
|
1599
|
+
for (const ref of state.trackRefs(id)) {
|
|
1600
|
+
if (ref.animation === id || track(ref.animation, ref.track) === void 0) continue;
|
|
1601
|
+
if (options.cascade) setTrack(ref.animation, ref.track, null);
|
|
1602
|
+
else diagnostics.push(error(`animations.${id}`, "ANIMATION_REFERENCED", `${id} is nested in ${ref.animation}.${ref.track}; delete the track or use cascade`));
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
const validationState = {
|
|
1606
|
+
...state,
|
|
1607
|
+
trackIds: (id) => [.../* @__PURE__ */ new Set([...state.trackIds(id), ...nextTracks.get(id)?.keys() ?? []])]
|
|
1608
|
+
};
|
|
1609
|
+
const validationItems = new Map(nextItems);
|
|
1610
|
+
const descendants = [];
|
|
1611
|
+
for (const [id, value] of nextItems) if (value && value.parent !== state.item(id)?.parent) descendants.push(...state.children(id));
|
|
1612
|
+
const seenDescendants = /* @__PURE__ */ new Set();
|
|
1613
|
+
while (descendants.length) {
|
|
1614
|
+
const id = descendants.pop();
|
|
1615
|
+
if (seenDescendants.has(id)) continue;
|
|
1616
|
+
seenDescendants.add(id);
|
|
1617
|
+
validationItems.set(id, item(id) ?? null);
|
|
1618
|
+
descendants.push(...state.children(id));
|
|
1619
|
+
}
|
|
1620
|
+
for (const [id, value] of validationItems) {
|
|
1621
|
+
if (!value) continue;
|
|
1622
|
+
if (value.parent !== void 0) {
|
|
1623
|
+
const parent = item(value.parent);
|
|
1624
|
+
if (!parent) diagnostics.push(error(`items.${id}.parent`, "PARENT_NOT_FOUND", `parent ${value.parent} does not exist`));
|
|
1625
|
+
else if (parent.type !== "frame") diagnostics.push(error(`items.${id}.parent`, "INVALID_PARENT", `parent ${value.parent} is a ${parent.type}; only frames contain items`));
|
|
1626
|
+
else {
|
|
1627
|
+
let cursor = value.parent;
|
|
1628
|
+
for (let depth = 0; cursor; depth += 1) {
|
|
1629
|
+
if (depth >= 64) {
|
|
1630
|
+
diagnostics.push(error(`items.${id}.parent`, "PARENT_DEPTH", "parent hierarchy exceeds 64 levels"));
|
|
1631
|
+
break;
|
|
1632
|
+
}
|
|
1633
|
+
if (cursor === id) {
|
|
1634
|
+
diagnostics.push(error(`items.${id}.parent`, "PARENT_CYCLE", `${id} cannot be inside itself`));
|
|
1635
|
+
break;
|
|
1636
|
+
}
|
|
1637
|
+
cursor = item(cursor)?.parent;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
for (const bound of arrowBindings(value)) {
|
|
1642
|
+
const target = item(bound);
|
|
1643
|
+
if (!target) diagnostics.push(error(`items.${id}.props`, "BINDING_NOT_FOUND", `bound item ${bound} does not exist`));
|
|
1644
|
+
else if (bound === id || target.type === "arrow") diagnostics.push(error(`items.${id}.props`, "INVALID_BINDING", "arrows bind to items other than arrows"));
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
for (const [id, value] of nextItems) {
|
|
1648
|
+
if (!value || !state.item(id) || state.item(id)?.type === value.type) continue;
|
|
1649
|
+
if (value.type !== "frame") {
|
|
1650
|
+
for (const child of state.children(id)) if (item(child)?.parent === id) diagnostics.push(error(`items.${child}.parent`, "INVALID_PARENT", `${id} is not a frame`));
|
|
1651
|
+
}
|
|
1652
|
+
if (value.type === "arrow") for (const binder of state.binders(id)) {
|
|
1653
|
+
const arrow = item(binder);
|
|
1654
|
+
if (arrow && arrowBindings(arrow).includes(id)) diagnostics.push(error(`items.${binder}.props`, "INVALID_BINDING", "arrows cannot bind to arrows"));
|
|
1655
|
+
}
|
|
1656
|
+
for (const ref of state.trackRefs(id)) {
|
|
1657
|
+
const current = track(ref.animation, ref.track);
|
|
1658
|
+
if (current) diagnostics.push(...validateTrack(`animations.${ref.animation}.tracks.${ref.track}`, ref.animation, current, item, animation, track, validationState));
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
for (const [animationId, tracks] of nextTracks) {
|
|
1662
|
+
if (!animation(animationId)) {
|
|
1663
|
+
for (const [trackId, value] of tracks) if (value) diagnostics.push(error(`animations.${animationId}.tracks.${trackId}`, "ANIMATION_NOT_FOUND", `animation ${animationId} does not exist`));
|
|
1664
|
+
continue;
|
|
1665
|
+
}
|
|
1666
|
+
for (const [trackId, value] of tracks) {
|
|
1667
|
+
if (!value) continue;
|
|
1668
|
+
diagnostics.push(...validateTrack(`animations.${animationId}.tracks.${trackId}`, animationId, value, item, animation, track, validationState));
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
if (diagnostics.length) return {
|
|
1672
|
+
ok: false,
|
|
1673
|
+
diagnostics
|
|
1674
|
+
};
|
|
1675
|
+
const before = emptyDelta();
|
|
1676
|
+
const after = emptyDelta();
|
|
1677
|
+
if (nextSettings && !boardJsonEquals(nextSettings, state.settings())) {
|
|
1678
|
+
before.board = state.settings();
|
|
1679
|
+
after.board = nextSettings;
|
|
1680
|
+
}
|
|
1681
|
+
for (const [id, value] of nextItems) {
|
|
1682
|
+
const previous = state.item(id) ?? null;
|
|
1683
|
+
if (boardJsonEquals(previous, value)) continue;
|
|
1684
|
+
before.items[id] = previous;
|
|
1685
|
+
after.items[id] = value;
|
|
1686
|
+
}
|
|
1687
|
+
for (const [id, value] of nextAnimations) {
|
|
1688
|
+
before.animations[id] = state.animation(id) ?? null;
|
|
1689
|
+
after.animations[id] = value;
|
|
1690
|
+
}
|
|
1691
|
+
for (const [animationId, tracks] of nextTracks) for (const [trackId, value] of tracks) {
|
|
1692
|
+
const previous = state.track(animationId, trackId) ?? null;
|
|
1693
|
+
if (boardJsonEquals(previous, value)) continue;
|
|
1694
|
+
slot(before.tracks, animationId)[trackId] = previous;
|
|
1695
|
+
slot(after.tracks, animationId)[trackId] = value;
|
|
1696
|
+
}
|
|
1697
|
+
return {
|
|
1698
|
+
ok: true,
|
|
1699
|
+
before: withoutEmpty(before),
|
|
1700
|
+
after: withoutEmpty(after),
|
|
1701
|
+
diagnostics: []
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
function validateTrack(path, animationId, value, item, animation, track, state) {
|
|
1705
|
+
const diagnostics = [];
|
|
1706
|
+
let targetType;
|
|
1707
|
+
if (value.target === "camera") targetType = BOARD_CAMERA_TARGET;
|
|
1708
|
+
else if (item(value.target)) targetType = item(value.target).type;
|
|
1709
|
+
else if (animation(value.target)) {
|
|
1710
|
+
targetType = "animation";
|
|
1711
|
+
if (value.target === animationId || nestsInto(value.target, animationId, animation, track, state)) return [error(`${path}.target`, "ANIMATION_CYCLE", `${animationId} cannot contain itself through ${value.target}`)];
|
|
1712
|
+
} else return [error(`${path}.target`, "TARGET_NOT_FOUND", `target ${value.target} is not an item, animation or camera`)];
|
|
1713
|
+
const resolved = resolveBoardProperty(targetType, value.property);
|
|
1714
|
+
if (!resolved.ok) return [error(`${path}.property`, "INVALID_PROPERTY", resolved.message)];
|
|
1715
|
+
for (const [index, keyframe] of value.keyframes.entries()) {
|
|
1716
|
+
const parsed = resolved.spec.schema.safeParse(keyframe.value);
|
|
1717
|
+
if (!parsed.success) {
|
|
1718
|
+
diagnostics.push(error(`${path}.keyframes.${index}.value`, "INVALID_KEYFRAME", parsed.error.issues[0]?.message ?? "invalid value"));
|
|
1719
|
+
continue;
|
|
1720
|
+
}
|
|
1721
|
+
if (value.property === "focus" && typeof keyframe.value === "string" && !item(keyframe.value)) diagnostics.push(error(`${path}.keyframes.${index}.value`, "TARGET_NOT_FOUND", `focus item ${keyframe.value} does not exist`));
|
|
1722
|
+
}
|
|
1723
|
+
if (value.interpolation === "spline" && resolved.spec.kind !== "vec2") diagnostics.push(error(`${path}.interpolation`, "INVALID_TRACK", "spline interpolation needs a position-like property"));
|
|
1724
|
+
return diagnostics;
|
|
1725
|
+
}
|
|
1726
|
+
function nestsInto(from, into, animation, track, state) {
|
|
1727
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1728
|
+
const stack = [from];
|
|
1729
|
+
while (stack.length) {
|
|
1730
|
+
const current = stack.pop();
|
|
1731
|
+
if (current === into) return true;
|
|
1732
|
+
if (seen.has(current) || !animation(current)) continue;
|
|
1733
|
+
seen.add(current);
|
|
1734
|
+
for (const trackId of state.trackIds(current)) {
|
|
1735
|
+
const value = track(current, trackId);
|
|
1736
|
+
if (value && value.property === "time" && animation(value.target)) stack.push(value.target);
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
return false;
|
|
1740
|
+
}
|
|
1741
|
+
function createDocumentState(document) {
|
|
1742
|
+
let children = null;
|
|
1743
|
+
let binders = null;
|
|
1744
|
+
let refs = null;
|
|
1745
|
+
const index = () => {
|
|
1746
|
+
if (children) return;
|
|
1747
|
+
children = /* @__PURE__ */ new Map();
|
|
1748
|
+
binders = /* @__PURE__ */ new Map();
|
|
1749
|
+
refs = /* @__PURE__ */ new Map();
|
|
1750
|
+
for (const [id, value] of Object.entries(document.items)) {
|
|
1751
|
+
if (value.parent) children.set(value.parent, [...children.get(value.parent) ?? [], id]);
|
|
1752
|
+
for (const bound of arrowBindings(value)) binders.set(bound, [...binders.get(bound) ?? [], id]);
|
|
1753
|
+
}
|
|
1754
|
+
for (const [animationId, value] of Object.entries(document.animations)) for (const [trackId, track] of Object.entries(value.tracks)) for (const ref of trackReferences(track)) refs.set(ref, [...refs.get(ref) ?? [], {
|
|
1755
|
+
animation: animationId,
|
|
1756
|
+
track: trackId
|
|
1757
|
+
}]);
|
|
1758
|
+
};
|
|
1759
|
+
return {
|
|
1760
|
+
settings: () => document.board,
|
|
1761
|
+
item: (id) => document.items[id],
|
|
1762
|
+
animation: (id) => {
|
|
1763
|
+
const value = document.animations[id];
|
|
1764
|
+
if (!value) return void 0;
|
|
1765
|
+
const { tracks: _tracks, ...header } = value;
|
|
1766
|
+
return header;
|
|
1767
|
+
},
|
|
1768
|
+
track: (animationId, trackId) => document.animations[animationId]?.tracks[trackId],
|
|
1769
|
+
trackIds: (animationId) => Object.keys(document.animations[animationId]?.tracks ?? {}),
|
|
1770
|
+
children: (id) => {
|
|
1771
|
+
index();
|
|
1772
|
+
return children?.get(id) ?? [];
|
|
1773
|
+
},
|
|
1774
|
+
binders: (id) => {
|
|
1775
|
+
index();
|
|
1776
|
+
return binders?.get(id) ?? [];
|
|
1777
|
+
},
|
|
1778
|
+
trackRefs: (id) => {
|
|
1779
|
+
index();
|
|
1780
|
+
return refs?.get(id) ?? [];
|
|
1781
|
+
},
|
|
1782
|
+
maxZ: (parent) => {
|
|
1783
|
+
let max = 0;
|
|
1784
|
+
for (const value of Object.values(document.items)) if (value.parent === parent && (value.z ?? 0) > max) max = value.z ?? 0;
|
|
1785
|
+
return max;
|
|
1786
|
+
}
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
function applyBoardDelta(document, delta) {
|
|
1790
|
+
const items = delta.items ? { ...document.items } : document.items;
|
|
1791
|
+
for (const [id, value] of Object.entries(delta.items ?? {})) if (value) items[id] = value;
|
|
1792
|
+
else delete items[id];
|
|
1793
|
+
const animations = delta.animations || delta.tracks ? { ...document.animations } : document.animations;
|
|
1794
|
+
for (const [id, value] of Object.entries(delta.animations ?? {})) if (value) animations[id] = {
|
|
1795
|
+
...value,
|
|
1796
|
+
tracks: animations[id]?.tracks ?? {}
|
|
1797
|
+
};
|
|
1798
|
+
else delete animations[id];
|
|
1799
|
+
for (const [animationId, tracks] of Object.entries(delta.tracks ?? {})) {
|
|
1800
|
+
const current = animations[animationId];
|
|
1801
|
+
if (!current) continue;
|
|
1802
|
+
const nextTracks = { ...current.tracks };
|
|
1803
|
+
for (const [trackId, value] of Object.entries(tracks)) if (value) nextTracks[trackId] = value;
|
|
1804
|
+
else delete nextTracks[trackId];
|
|
1805
|
+
animations[animationId] = {
|
|
1806
|
+
...current,
|
|
1807
|
+
tracks: nextTracks
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
return {
|
|
1811
|
+
board: delta.board ?? document.board,
|
|
1812
|
+
items,
|
|
1813
|
+
animations
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
function applyBoardPatchToDocument(document, patch, options = {}) {
|
|
1817
|
+
const result = applyBoardPatch(createDocumentState(document), patch, options);
|
|
1818
|
+
return result.ok ? {
|
|
1819
|
+
ok: true,
|
|
1820
|
+
document: applyBoardDelta(document, result.after),
|
|
1821
|
+
before: result.before,
|
|
1822
|
+
after: result.after
|
|
1823
|
+
} : result;
|
|
1824
|
+
}
|
|
1825
|
+
function parseBoardDocument(input) {
|
|
1826
|
+
if (!isRecord$2(input)) return {
|
|
1827
|
+
ok: false,
|
|
1828
|
+
diagnostics: [error("", "INVALID_DOCUMENT", "expected an object")]
|
|
1829
|
+
};
|
|
1830
|
+
const result = applyBoardPatchToDocument({
|
|
1831
|
+
board: BoardSettingsSchema.parse({}),
|
|
1832
|
+
items: {},
|
|
1833
|
+
animations: {}
|
|
1834
|
+
}, {
|
|
1835
|
+
...isRecord$2(input.board) ? { board: input.board } : {},
|
|
1836
|
+
...isRecord$2(input.items) ? { items: input.items } : {},
|
|
1837
|
+
...isRecord$2(input.animations) ? { animations: input.animations } : {}
|
|
1838
|
+
});
|
|
1839
|
+
return result.ok ? {
|
|
1840
|
+
ok: true,
|
|
1841
|
+
document: result.document
|
|
1842
|
+
} : result;
|
|
1843
|
+
}
|
|
1844
|
+
//#endregion
|
|
766
1845
|
//#region ../protocol/dist/model/metrics.js
|
|
767
1846
|
const count = z.number().finite().nonnegative();
|
|
768
1847
|
const usageSchema = z.object({
|
|
@@ -1881,6 +2960,67 @@ function createBoardEntityId() {
|
|
|
1881
2960
|
return `c-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1882
2961
|
}
|
|
1883
2962
|
//#endregion
|
|
2963
|
+
//#region src/board/model/json.ts
|
|
2964
|
+
function sameJson(a, b) {
|
|
2965
|
+
if (a === b) return true;
|
|
2966
|
+
if (!a || !b || typeof a !== "object" || typeof b !== "object") return false;
|
|
2967
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
2968
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
2969
|
+
for (let index = 0; index < a.length; index += 1) if (!sameJson(a[index], b[index])) return false;
|
|
2970
|
+
return true;
|
|
2971
|
+
}
|
|
2972
|
+
const left = a;
|
|
2973
|
+
const right = b;
|
|
2974
|
+
let members = 0;
|
|
2975
|
+
for (const key of Object.keys(left)) {
|
|
2976
|
+
if (left[key] === void 0) continue;
|
|
2977
|
+
members += 1;
|
|
2978
|
+
if (!sameJson(left[key], right[key])) return false;
|
|
2979
|
+
}
|
|
2980
|
+
for (const key of Object.keys(right)) if (right[key] !== void 0) members -= 1;
|
|
2981
|
+
return members === 0;
|
|
2982
|
+
}
|
|
2983
|
+
//#endregion
|
|
2984
|
+
//#region src/board/model/document-share.ts
|
|
2985
|
+
function shareRecord(previous, next) {
|
|
2986
|
+
let same = Object.keys(previous).length === Object.keys(next).length;
|
|
2987
|
+
const shared = {};
|
|
2988
|
+
for (const [id, value] of Object.entries(next)) {
|
|
2989
|
+
const current = previous[id];
|
|
2990
|
+
const kept = current !== void 0 && sameJson(current, value) ? current : value;
|
|
2991
|
+
if (kept !== current) same = false;
|
|
2992
|
+
shared[id] = kept;
|
|
2993
|
+
}
|
|
2994
|
+
return same ? previous : shared;
|
|
2995
|
+
}
|
|
2996
|
+
function shareBoardDocument(previous, next) {
|
|
2997
|
+
if (!previous) return next;
|
|
2998
|
+
const board = sameJson(previous.board, next.board) ? previous.board : next.board;
|
|
2999
|
+
const items = shareRecord(previous.items, next.items);
|
|
3000
|
+
const animations = shareRecord(previous.animations, next.animations);
|
|
3001
|
+
if (board === previous.board && items === previous.items && animations === previous.animations) return previous;
|
|
3002
|
+
return {
|
|
3003
|
+
board,
|
|
3004
|
+
items,
|
|
3005
|
+
animations
|
|
3006
|
+
};
|
|
3007
|
+
}
|
|
3008
|
+
//#endregion
|
|
3009
|
+
//#region src/board/model/read.ts
|
|
3010
|
+
function readBoardDocument(result, base = null) {
|
|
3011
|
+
if (result.changes) {
|
|
3012
|
+
if (!base) throw new Error(`Board ${result.id} returned changes without a document to apply them to.`);
|
|
3013
|
+
return result.changes.reduce(applyBoardDelta, base);
|
|
3014
|
+
}
|
|
3015
|
+
const parsed = parseBoardDocument({
|
|
3016
|
+
board: result.board,
|
|
3017
|
+
items: result.items ?? {},
|
|
3018
|
+
animations: result.animations ?? {}
|
|
3019
|
+
});
|
|
3020
|
+
if (!parsed.ok) throw new Error(`Board ${result.id} could not be read.`);
|
|
3021
|
+
return shareBoardDocument(base, parsed.document);
|
|
3022
|
+
}
|
|
3023
|
+
//#endregion
|
|
1884
3024
|
//#region src/board/client/index.ts
|
|
1885
3025
|
var BoardRealtimeClient = class {
|
|
1886
3026
|
websocketClient;
|
|
@@ -1965,9 +3105,25 @@ var BoardClient = class {
|
|
|
1965
3105
|
if (input.animations?.length) params.set("animations", input.animations.join(","));
|
|
1966
3106
|
if (input.limit !== void 0) params.set("limit", String(input.limit));
|
|
1967
3107
|
if (input.cursor) params.set("cursor", input.cursor);
|
|
3108
|
+
if (input.since !== void 0) params.set("since", String(input.since));
|
|
1968
3109
|
const query = params.toString();
|
|
1969
3110
|
return this.http.request(`${this.path}${query ? `?${query}` : ""}`, { fetch: customFetch });
|
|
1970
3111
|
}
|
|
3112
|
+
/** Bring a cached document up to date in one request that carries only what changed since its version. */
|
|
3113
|
+
async sync(cached, customFetch) {
|
|
3114
|
+
const result = await this.get(cached ? { since: cached.version } : {}, customFetch);
|
|
3115
|
+
const document = readBoardDocument(result, cached?.document);
|
|
3116
|
+
const { id, title, version, updatedAt, playback } = result;
|
|
3117
|
+
return {
|
|
3118
|
+
id,
|
|
3119
|
+
title,
|
|
3120
|
+
version,
|
|
3121
|
+
updatedAt,
|
|
3122
|
+
playback,
|
|
3123
|
+
document,
|
|
3124
|
+
changed: document !== cached?.document || version !== cached.version
|
|
3125
|
+
};
|
|
3126
|
+
}
|
|
1971
3127
|
/** Merge a patch into the Board: fields merge, `null` deletes. */
|
|
1972
3128
|
apply(patch, options = {}) {
|
|
1973
3129
|
return this.http.request(`${this.path}/apply`, {
|
|
@@ -2092,14 +3248,7 @@ var SpaceBoardsApi = class {
|
|
|
2092
3248
|
}
|
|
2093
3249
|
assetSource() {
|
|
2094
3250
|
return {
|
|
2095
|
-
|
|
2096
|
-
purpose: "preview",
|
|
2097
|
-
fresh: options?.fresh
|
|
2098
|
-
}),
|
|
2099
|
-
resolvePlaybackUrl: (path, options) => this.files.resolveUrl(path, {
|
|
2100
|
-
purpose: "playback",
|
|
2101
|
-
fresh: options?.fresh
|
|
2102
|
-
}),
|
|
3251
|
+
resolveFile: (path, options) => this.files.resolve(path, options),
|
|
2103
3252
|
subscribe: (listener) => this.files.onUrlChange(listener)
|
|
2104
3253
|
};
|
|
2105
3254
|
}
|
|
@@ -3838,7 +4987,8 @@ const fileUrlStore = {
|
|
|
3838
4987
|
const record = request.result;
|
|
3839
4988
|
if (record) found.set(path, {
|
|
3840
4989
|
url: record.url,
|
|
3841
|
-
verifiedAt: record.verifiedAt
|
|
4990
|
+
verifiedAt: record.verifiedAt,
|
|
4991
|
+
media: record.media
|
|
3842
4992
|
});
|
|
3843
4993
|
}
|
|
3844
4994
|
return found;
|
|
@@ -3848,11 +4998,10 @@ const fileUrlStore = {
|
|
|
3848
4998
|
async write(scope, spaceId, changes) {
|
|
3849
4999
|
await database.transact([STORE], "readwrite", (tx) => {
|
|
3850
5000
|
const store = tx.objectStore(STORE);
|
|
3851
|
-
for (const [path,
|
|
5001
|
+
for (const [path, stored] of changes.put) store.put({
|
|
5002
|
+
...stored,
|
|
3852
5003
|
key: recordKey(scope, spaceId, path),
|
|
3853
|
-
scope
|
|
3854
|
-
url,
|
|
3855
|
-
verifiedAt
|
|
5004
|
+
scope
|
|
3856
5005
|
});
|
|
3857
5006
|
for (const path of changes.remove) store.delete(recordKey(scope, spaceId, path));
|
|
3858
5007
|
return () => void 0;
|
|
@@ -3906,7 +5055,8 @@ function inlineFileDataUrl(file) {
|
|
|
3906
5055
|
function resolvedOf(file) {
|
|
3907
5056
|
if (file.delivery === "url" && file.url) return {
|
|
3908
5057
|
url: file.url,
|
|
3909
|
-
inline: false
|
|
5058
|
+
inline: false,
|
|
5059
|
+
...file.media ? { media: file.media } : {}
|
|
3910
5060
|
};
|
|
3911
5061
|
return {
|
|
3912
5062
|
url: inlineFileDataUrl(file),
|
|
@@ -3936,7 +5086,7 @@ function createSpaceFileUrls(options) {
|
|
|
3936
5086
|
const keyOf = (spaceId, path) => `${spaceId}\u0000${path}`;
|
|
3937
5087
|
const epochOf = (spaceId) => epochs.get(spaceId) ?? 0;
|
|
3938
5088
|
const persists = (scope) => cache && scope !== null && Boolean(globalThis.indexedDB);
|
|
3939
|
-
function persist(scope, spaceId, path,
|
|
5089
|
+
function persist(scope, spaceId, path, entry) {
|
|
3940
5090
|
if (!writes) {
|
|
3941
5091
|
const batch = /* @__PURE__ */ new Map();
|
|
3942
5092
|
writes = batch;
|
|
@@ -3957,11 +5107,12 @@ function createSpaceFileUrls(options) {
|
|
|
3957
5107
|
remove: /* @__PURE__ */ new Set()
|
|
3958
5108
|
};
|
|
3959
5109
|
writes.set(id, pending);
|
|
3960
|
-
if (
|
|
5110
|
+
if (entry) {
|
|
3961
5111
|
pending.remove.delete(path);
|
|
3962
5112
|
pending.put.set(path, {
|
|
3963
|
-
url,
|
|
3964
|
-
verifiedAt: now()
|
|
5113
|
+
url: entry.url,
|
|
5114
|
+
verifiedAt: now(),
|
|
5115
|
+
...entry.media ? { media: entry.media } : {}
|
|
3965
5116
|
});
|
|
3966
5117
|
} else {
|
|
3967
5118
|
pending.put.delete(path);
|
|
@@ -4022,7 +5173,7 @@ function createSpaceFileUrls(options) {
|
|
|
4022
5173
|
function adopt(key, spaceId, path, entry) {
|
|
4023
5174
|
const previous = memory.get(key);
|
|
4024
5175
|
if (!entry.inline || entry.url.length <= INLINE_MEMORY_MAX_CHARS) remember(memory, key, entry);
|
|
4025
|
-
if (!entry.inline && persists(entry.scope)) persist(entry.scope, spaceId, path, entry
|
|
5176
|
+
if (!entry.inline && persists(entry.scope)) persist(entry.scope, spaceId, path, entry);
|
|
4026
5177
|
if (previous && previous.url !== entry.url) emit(spaceId, path);
|
|
4027
5178
|
}
|
|
4028
5179
|
function revalidate(token, scope, spaceId, path) {
|
|
@@ -4078,6 +5229,7 @@ function createSpaceFileUrls(options) {
|
|
|
4078
5229
|
const entry = {
|
|
4079
5230
|
url: record.url,
|
|
4080
5231
|
inline: false,
|
|
5232
|
+
...record.media ? { media: record.media } : {},
|
|
4081
5233
|
scope,
|
|
4082
5234
|
epoch: -1,
|
|
4083
5235
|
verifiedAt: record.verifiedAt
|
|
@@ -4131,7 +5283,11 @@ function createSpaceFileUrls(options) {
|
|
|
4131
5283
|
request.signal?.throwIfAborted();
|
|
4132
5284
|
return server(await options.credential(), scope, spaceId, path, false);
|
|
4133
5285
|
}), request);
|
|
4134
|
-
|
|
5286
|
+
if (!entry || purpose === "playback" && entry.inline) return null;
|
|
5287
|
+
return entry.media ? {
|
|
5288
|
+
url: entry.url,
|
|
5289
|
+
media: entry.media
|
|
5290
|
+
} : { url: entry.url };
|
|
4135
5291
|
},
|
|
4136
5292
|
applyFsChange(spaceId, payload) {
|
|
4137
5293
|
if (payload.resync) epochs.set(spaceId, epochOf(spaceId) + 1);
|
|
@@ -4401,9 +5557,9 @@ var SpaceFilesApi = class {
|
|
|
4401
5557
|
return this.services.reads.read(this.spaceId, path, { signal });
|
|
4402
5558
|
}
|
|
4403
5559
|
/** Resolve a browser-ready file URL, waiting for CDN delivery when necessary. */
|
|
4404
|
-
|
|
5560
|
+
resolve(path, options = {}) {
|
|
4405
5561
|
const { fetch: customFetch, ...request } = options;
|
|
4406
|
-
if (customFetch) return this.
|
|
5562
|
+
if (customFetch) return this.resolveThrough(path, customFetch, options);
|
|
4407
5563
|
return this.services.urls.resolve(this.spaceId, path, request);
|
|
4408
5564
|
}
|
|
4409
5565
|
onUrlChange(listener) {
|
|
@@ -4411,7 +5567,7 @@ var SpaceFilesApi = class {
|
|
|
4411
5567
|
if (spaceId === this.spaceId) listener(path);
|
|
4412
5568
|
});
|
|
4413
5569
|
}
|
|
4414
|
-
async
|
|
5570
|
+
async resolveThrough(path, customFetch, options) {
|
|
4415
5571
|
const purpose = options.purpose ?? "preview";
|
|
4416
5572
|
const requestedTimeoutMs = options.timeoutMs ?? 15e3;
|
|
4417
5573
|
const timeoutMs = Number.isFinite(requestedTimeoutMs) ? Math.max(0, requestedTimeoutMs) : 15e3;
|
|
@@ -4422,8 +5578,11 @@ var SpaceFilesApi = class {
|
|
|
4422
5578
|
const file = await this.read(path, customFetch, deadline.signal);
|
|
4423
5579
|
if (deadline.timedOut()) return null;
|
|
4424
5580
|
if ("content" in file) {
|
|
4425
|
-
if (file.delivery === "url" && file.url) return file.
|
|
4426
|
-
|
|
5581
|
+
if (file.delivery === "url" && file.url) return file.media ? {
|
|
5582
|
+
url: file.url,
|
|
5583
|
+
media: file.media
|
|
5584
|
+
} : { url: file.url };
|
|
5585
|
+
return purpose === "preview" ? { url: inlineFileDataUrl(file) } : null;
|
|
4427
5586
|
}
|
|
4428
5587
|
const remainingMs = deadlineAt - Date.now();
|
|
4429
5588
|
if (remainingMs <= 0) return null;
|
|
@@ -6484,4 +7643,4 @@ var CohubHttpClient = class {
|
|
|
6484
7643
|
};
|
|
6485
7644
|
const createHttpClient = (options) => new CohubHttpClient(options);
|
|
6486
7645
|
//#endregion
|
|
6487
|
-
export { defaultDesktopCommandTimeoutMs as $, readRtcTransportStats as A, ModelsApi as B, createSessionGenerationStreamClient as C,
|
|
7646
|
+
export { defaultDesktopCommandTimeoutMs as $, readRtcTransportStats as A, ModelsApi as B, createSessionGenerationStreamClient as C, SpaceDisplaysApi as D, createSessionPatchReducer as E, ReferencesApi as F, DESKTOP_COMMAND_DEFAULT_TIMEOUT_MS as G, CronJobsApi as H, SearchApi as I, DESKTOP_COMMAND_PAYLOAD_MAX_BYTES as J, DESKTOP_COMMAND_MAX_TIMEOUT_MS as K, PublicAssetsApi as L, BoardClient as M, ensureRealtimeConnected as N, DisplayConnection as O, SessionAccessApi as P, DESKTOP_COMMAND_VERSION as Q, SkillsApi as R, SessionGenerationStreamClient as S, SessionPatchReducer as T, ChannelsApi as U, GenerationsApi as V, DESKTOP_COMMAND_ACCEPT_TIMEOUT_MS as W, DESKTOP_COMMAND_SETTLEMENT_GRACE_SECONDS as X, DESKTOP_COMMAND_PENDING_TTL_SECONDS as Y, DESKTOP_COMMAND_TERMINAL_TTL_SECONDS as Z, createSpaceFileServices as _, BoardAnimationSchema as _t, WorkCommerceApi as a, NAVIGATION_ERROR_MESSAGE_MAX_LENGTH as at, buildSpaceInvitePath as b, parseBoardItem as bt, AppsApi as c, DISPLAY_INPUT_MAX_TEXT as ct, UserApi as d, splitDisplayText as dt, isDesktopCallMethod as et, TasksApi as f, parseBoardDocument as ft, SpacesApi as g, BOARD_ITEM_TYPES as gt, SpaceWebhooksApi as h, BOARD_COLOR_TOKENS as ht, AppCommerceApi as i, resolveOpenSurface as it, waitForIceGathering as j, connectDisplay as k, ReferralsApi as l, DISPLAY_SYSTEM_ACTIONS as lt, SpacePublicFilesApi as m, BOARD_MIME_TYPE as mt, createHttpClient as n, parseDesktopCommand as nt, DesktopCommandsApi as o, DISPLAY_ELEMENT_REF_PATTERN as ot, SpaceClient as p, BOARD_EXTENSION as pt, DESKTOP_COMMAND_OPEN_TIMEOUT_MS as q, RoomsApi as r, parseDesktopCommandId as rt, UiCommandsApi as s, DISPLAY_INPUT_MAX_SCHEDULE_MS as st, CohubHttpClient as t, isTerminalDesktopCommandStatus as tt, UsersApi as u, normalizeDisplayPoint as ut, clearFileUrlCache as v, BoardPatchSchema as vt, parseAssistantMessageCommit as w, buildSpacePath as x, normalizeBoardRemoteUrl as xt, PublicInviteApi as y, BoardTrackSchema as yt, PromptsApi as z };
|