@canvas-harness/core 0.0.1 → 0.0.3
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/index.cjs +570 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +226 -1
- package/dist/index.d.ts +226 -1
- package/dist/index.js +561 -28
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -2908,6 +2908,155 @@ var createDefaultTextareaEditor = ({
|
|
|
2908
2908
|
};
|
|
2909
2909
|
};
|
|
2910
2910
|
|
|
2911
|
+
// src/assets/image.ts
|
|
2912
|
+
var MAX_IMAGE_BYTES = 2 * 1024 * 1024;
|
|
2913
|
+
var ACCEPTED_MIME = /* @__PURE__ */ new Set(["image/png", "image/jpeg"]);
|
|
2914
|
+
var validateImageInput = (input) => {
|
|
2915
|
+
if (typeof input === "string") {
|
|
2916
|
+
if (!input.startsWith("data:")) {
|
|
2917
|
+
throw new Error(
|
|
2918
|
+
"addImage: external URL strings are not supported. Pass a File, Blob, or `data:image/(png|jpeg)` URI."
|
|
2919
|
+
);
|
|
2920
|
+
}
|
|
2921
|
+
const mimeMatch = /^data:([^;,]+)/.exec(input);
|
|
2922
|
+
const mime = mimeMatch?.[1] ?? "";
|
|
2923
|
+
if (!ACCEPTED_MIME.has(mime)) {
|
|
2924
|
+
throw new Error(
|
|
2925
|
+
`addImage: unsupported MIME "${mime || "(unknown)"}". Only image/png and image/jpeg are supported.`
|
|
2926
|
+
);
|
|
2927
|
+
}
|
|
2928
|
+
const comma = input.indexOf(",");
|
|
2929
|
+
if (comma < 0) throw new Error("addImage: malformed data URI (missing payload separator)");
|
|
2930
|
+
const decodedBytes = Math.floor((input.length - comma - 1) * 3 / 4);
|
|
2931
|
+
if (decodedBytes > MAX_IMAGE_BYTES) {
|
|
2932
|
+
throw new Error(
|
|
2933
|
+
`addImage: image exceeds the 2 MB limit (${Math.round(decodedBytes / 1024)} KB).`
|
|
2934
|
+
);
|
|
2935
|
+
}
|
|
2936
|
+
return;
|
|
2937
|
+
}
|
|
2938
|
+
if (!ACCEPTED_MIME.has(input.type)) {
|
|
2939
|
+
throw new Error(
|
|
2940
|
+
`addImage: unsupported file type "${input.type || "(unknown)"}". Only image/png and image/jpeg are supported.`
|
|
2941
|
+
);
|
|
2942
|
+
}
|
|
2943
|
+
if (input.size > MAX_IMAGE_BYTES) {
|
|
2944
|
+
throw new Error(`addImage: file exceeds the 2 MB limit (${Math.round(input.size / 1024)} KB).`);
|
|
2945
|
+
}
|
|
2946
|
+
};
|
|
2947
|
+
var toImageBlob = async (input) => {
|
|
2948
|
+
if (typeof input === "string") {
|
|
2949
|
+
const res = await fetch(input);
|
|
2950
|
+
return res.blob();
|
|
2951
|
+
}
|
|
2952
|
+
return input;
|
|
2953
|
+
};
|
|
2954
|
+
var downscaleImageBlob = async (blob, maxDim) => {
|
|
2955
|
+
const bitmap = await createImageBitmap(blob);
|
|
2956
|
+
const naturalW = bitmap.width;
|
|
2957
|
+
const naturalH = bitmap.height;
|
|
2958
|
+
const maxSide = Math.max(naturalW, naturalH);
|
|
2959
|
+
if (maxDim <= 0 || maxSide <= maxDim) {
|
|
2960
|
+
bitmap.close?.();
|
|
2961
|
+
return { blob, naturalW, naturalH };
|
|
2962
|
+
}
|
|
2963
|
+
const scale = maxDim / maxSide;
|
|
2964
|
+
const w = Math.max(1, Math.round(naturalW * scale));
|
|
2965
|
+
const h = Math.max(1, Math.round(naturalH * scale));
|
|
2966
|
+
const canvas = new OffscreenCanvas(w, h);
|
|
2967
|
+
const ctx = canvas.getContext("2d");
|
|
2968
|
+
if (!ctx) throw new Error("addImage: failed to acquire OffscreenCanvas 2d context");
|
|
2969
|
+
ctx.drawImage(bitmap, 0, 0, w, h);
|
|
2970
|
+
bitmap.close?.();
|
|
2971
|
+
const outType = blob.type === "image/png" ? "image/png" : "image/jpeg";
|
|
2972
|
+
const outBlob = await canvas.convertToBlob({ type: outType, quality: 0.9 });
|
|
2973
|
+
return { blob: outBlob, naturalW: w, naturalH: h };
|
|
2974
|
+
};
|
|
2975
|
+
var blobToDataUri = (blob) => {
|
|
2976
|
+
return new Promise((resolve, reject) => {
|
|
2977
|
+
const reader = new FileReader();
|
|
2978
|
+
reader.onload = () => {
|
|
2979
|
+
if (typeof reader.result === "string") resolve(reader.result);
|
|
2980
|
+
else reject(new Error("FileReader returned non-string result"));
|
|
2981
|
+
};
|
|
2982
|
+
reader.onerror = () => reject(reader.error ?? new Error("FileReader failed"));
|
|
2983
|
+
reader.readAsDataURL(blob);
|
|
2984
|
+
});
|
|
2985
|
+
};
|
|
2986
|
+
|
|
2987
|
+
// src/assets/svg.ts
|
|
2988
|
+
var MAX_SVG_BYTES = 2 * 1024 * 1024;
|
|
2989
|
+
var DEFAULT_SVG_SIZE = 24;
|
|
2990
|
+
var validateSvgMarkup = (markup) => {
|
|
2991
|
+
if (typeof markup !== "string") {
|
|
2992
|
+
throw new Error("addSvg: src must be a string of SVG markup");
|
|
2993
|
+
}
|
|
2994
|
+
const byteLen = new Blob([markup]).size;
|
|
2995
|
+
if (byteLen > MAX_SVG_BYTES) {
|
|
2996
|
+
throw new Error(`addSvg: SVG markup exceeds the 2 MB limit (${Math.round(byteLen / 1024)} KB).`);
|
|
2997
|
+
}
|
|
2998
|
+
if (!/<svg[\s>]/i.test(markup)) {
|
|
2999
|
+
throw new Error("addSvg: src does not look like SVG markup (no <svg> tag found)");
|
|
3000
|
+
}
|
|
3001
|
+
};
|
|
3002
|
+
var sanitizeSvg = (markup) => {
|
|
3003
|
+
const parser = new DOMParser();
|
|
3004
|
+
const doc = parser.parseFromString(markup, "image/svg+xml");
|
|
3005
|
+
const root = doc.documentElement;
|
|
3006
|
+
if (root.nodeName === "parsererror" || root.querySelector("parsererror")) {
|
|
3007
|
+
throw new Error("addSvg: malformed SVG (parser error)");
|
|
3008
|
+
}
|
|
3009
|
+
const removable = [];
|
|
3010
|
+
const walker = doc.createTreeWalker(doc, NodeFilter.SHOW_ELEMENT);
|
|
3011
|
+
let n = walker.nextNode();
|
|
3012
|
+
while (n) {
|
|
3013
|
+
const el = n;
|
|
3014
|
+
const tag = el.tagName.toLowerCase();
|
|
3015
|
+
if (tag === "script" || tag === "foreignobject") {
|
|
3016
|
+
removable.push(el);
|
|
3017
|
+
} else {
|
|
3018
|
+
for (const attr of [...el.attributes]) {
|
|
3019
|
+
const name = attr.name.toLowerCase();
|
|
3020
|
+
const value = attr.value.trim().toLowerCase();
|
|
3021
|
+
if (name.startsWith("on")) {
|
|
3022
|
+
el.removeAttribute(attr.name);
|
|
3023
|
+
} else if ((name === "href" || name === "xlink:href" || name === "src") && value.startsWith("javascript:")) {
|
|
3024
|
+
el.removeAttribute(attr.name);
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
n = walker.nextNode();
|
|
3029
|
+
}
|
|
3030
|
+
for (const el of removable) el.remove();
|
|
3031
|
+
return new XMLSerializer().serializeToString(doc);
|
|
3032
|
+
};
|
|
3033
|
+
var extractSvgDimensions = (markup) => {
|
|
3034
|
+
const parser = new DOMParser();
|
|
3035
|
+
const doc = parser.parseFromString(markup, "image/svg+xml");
|
|
3036
|
+
const svg = doc.documentElement;
|
|
3037
|
+
if (svg.nodeName.toLowerCase() !== "svg") {
|
|
3038
|
+
return { w: DEFAULT_SVG_SIZE, h: DEFAULT_SVG_SIZE };
|
|
3039
|
+
}
|
|
3040
|
+
const widthAttr = svg.getAttribute("width");
|
|
3041
|
+
const heightAttr = svg.getAttribute("height");
|
|
3042
|
+
if (widthAttr && heightAttr) {
|
|
3043
|
+
const w = Number.parseFloat(widthAttr);
|
|
3044
|
+
const h = Number.parseFloat(heightAttr);
|
|
3045
|
+
if (Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0) return { w, h };
|
|
3046
|
+
}
|
|
3047
|
+
const viewBox = svg.getAttribute("viewBox");
|
|
3048
|
+
if (viewBox) {
|
|
3049
|
+
const parts = viewBox.split(/[\s,]+/).map(Number.parseFloat);
|
|
3050
|
+
if (parts.length === 4 && parts.every(Number.isFinite) && parts[2] > 0 && parts[3] > 0) {
|
|
3051
|
+
return { w: parts[2], h: parts[3] };
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
return { w: DEFAULT_SVG_SIZE, h: DEFAULT_SVG_SIZE };
|
|
3055
|
+
};
|
|
3056
|
+
var applySvgColor = (markup, color) => {
|
|
3057
|
+
return markup.replace(/currentColor/gi, color);
|
|
3058
|
+
};
|
|
3059
|
+
|
|
2911
3060
|
// src/store/conflict.ts
|
|
2912
3061
|
var detectConflicts = (batch, getNode, getEdge) => {
|
|
2913
3062
|
const out = [];
|
|
@@ -2984,6 +3133,8 @@ var inverseOp = (op) => {
|
|
|
2984
3133
|
return { type: "group.remove", group: op.group };
|
|
2985
3134
|
case "group.remove":
|
|
2986
3135
|
return { type: "group.upsert", group: op.group };
|
|
3136
|
+
case "frame.reorder":
|
|
3137
|
+
return { type: "frame.reorder", ids: op.prev, prev: op.ids };
|
|
2987
3138
|
}
|
|
2988
3139
|
};
|
|
2989
3140
|
var inverseBatch = (batch) => {
|
|
@@ -3051,6 +3202,7 @@ var createCanvasStore = (opts = {}) => {
|
|
|
3051
3202
|
const groupIdsAtom = signia.atom("groupIds", []);
|
|
3052
3203
|
const cameraAtom = signia.atom("camera", initial.camera);
|
|
3053
3204
|
const selectionAtom = signia.atom("selection", initial.selection);
|
|
3205
|
+
const frameOrderAtom = signia.atom("frameOrder", initial.frameOrder ?? []);
|
|
3054
3206
|
const interactionAtom = signia.atom("interaction", idleInteractionState());
|
|
3055
3207
|
const localPresenceAtom = signia.atom("presence", emptyPresenceState(clientId));
|
|
3056
3208
|
const remotePresence = /* @__PURE__ */ new Map();
|
|
@@ -3157,6 +3309,9 @@ var createCanvasStore = (opts = {}) => {
|
|
|
3157
3309
|
nodeAtoms.set(op.node.id, a);
|
|
3158
3310
|
nodeIdsAtom.update((ids) => [...ids, op.node.id]);
|
|
3159
3311
|
reindexNode(op.node);
|
|
3312
|
+
if (op.node.type === "frame") {
|
|
3313
|
+
frameOrderAtom.update((ids) => ids.includes(op.node.id) ? ids : [...ids, op.node.id]);
|
|
3314
|
+
}
|
|
3160
3315
|
break;
|
|
3161
3316
|
}
|
|
3162
3317
|
case "node.update": {
|
|
@@ -3181,6 +3336,9 @@ var createCanvasStore = (opts = {}) => {
|
|
|
3181
3336
|
nodeIdsAtom.update((ids) => ids.filter((x) => x !== id));
|
|
3182
3337
|
unindexNode(id);
|
|
3183
3338
|
incidentEdges.delete(id);
|
|
3339
|
+
if (op.node.type === "frame") {
|
|
3340
|
+
frameOrderAtom.update((ids) => ids.filter((x) => x !== id));
|
|
3341
|
+
}
|
|
3184
3342
|
break;
|
|
3185
3343
|
}
|
|
3186
3344
|
case "edge.add": {
|
|
@@ -3230,6 +3388,10 @@ var createCanvasStore = (opts = {}) => {
|
|
|
3230
3388
|
groupIdsAtom.update((ids) => ids.filter((x) => x !== id));
|
|
3231
3389
|
break;
|
|
3232
3390
|
}
|
|
3391
|
+
case "frame.reorder": {
|
|
3392
|
+
frameOrderAtom.set([...op.ids]);
|
|
3393
|
+
break;
|
|
3394
|
+
}
|
|
3233
3395
|
}
|
|
3234
3396
|
};
|
|
3235
3397
|
const enqueueOp = (op) => {
|
|
@@ -3252,6 +3414,7 @@ var createCanvasStore = (opts = {}) => {
|
|
|
3252
3414
|
return prev;
|
|
3253
3415
|
};
|
|
3254
3416
|
const populateInitial = (scene) => {
|
|
3417
|
+
const seededFrameOrder = [];
|
|
3255
3418
|
for (const id of Object.keys(scene.nodes)) {
|
|
3256
3419
|
const node = scene.nodes[id];
|
|
3257
3420
|
if (!node) continue;
|
|
@@ -3259,7 +3422,9 @@ var createCanvasStore = (opts = {}) => {
|
|
|
3259
3422
|
nodeAtoms.set(node.id, a);
|
|
3260
3423
|
nodeIdsAtom.update((ids) => [...ids, node.id]);
|
|
3261
3424
|
reindexNode(node);
|
|
3425
|
+
if (node.type === "frame") seededFrameOrder.push(node.id);
|
|
3262
3426
|
}
|
|
3427
|
+
if (!scene.frameOrder) frameOrderAtom.set(seededFrameOrder);
|
|
3263
3428
|
for (const id of Object.keys(scene.edges)) {
|
|
3264
3429
|
const edge = scene.edges[id];
|
|
3265
3430
|
if (!edge) continue;
|
|
@@ -3328,6 +3493,55 @@ var createCanvasStore = (opts = {}) => {
|
|
|
3328
3493
|
if (batch) emitChange(batch);
|
|
3329
3494
|
});
|
|
3330
3495
|
},
|
|
3496
|
+
async addImage(opts2) {
|
|
3497
|
+
validateImageInput(opts2.src);
|
|
3498
|
+
const rawBlob = await toImageBlob(opts2.src);
|
|
3499
|
+
const maxDim = opts2.maxDimension ?? 2048;
|
|
3500
|
+
const { blob, naturalW, naturalH } = await downscaleImageBlob(rawBlob, maxDim);
|
|
3501
|
+
const src = await blobToDataUri(blob);
|
|
3502
|
+
const DEFAULT_MAX_NODE_SIDE = 400;
|
|
3503
|
+
const aspectScale = Math.min(1, DEFAULT_MAX_NODE_SIDE / Math.max(naturalW, naturalH));
|
|
3504
|
+
const w = opts2.w ?? Math.max(1, Math.round(naturalW * aspectScale));
|
|
3505
|
+
const h = opts2.h ?? Math.max(1, Math.round(naturalH * aspectScale));
|
|
3506
|
+
const id = asNodeId(idGenerator());
|
|
3507
|
+
this.addNode({
|
|
3508
|
+
id,
|
|
3509
|
+
type: "image",
|
|
3510
|
+
x: opts2.x,
|
|
3511
|
+
y: opts2.y,
|
|
3512
|
+
w,
|
|
3513
|
+
h,
|
|
3514
|
+
angle: 0,
|
|
3515
|
+
z: 0,
|
|
3516
|
+
groups: [],
|
|
3517
|
+
style: opts2.style,
|
|
3518
|
+
data: { src, naturalW, naturalH, alt: opts2.alt }
|
|
3519
|
+
});
|
|
3520
|
+
return id;
|
|
3521
|
+
},
|
|
3522
|
+
async addSvg(opts2) {
|
|
3523
|
+
validateSvgMarkup(opts2.src);
|
|
3524
|
+
const sanitized = sanitizeSvg(opts2.src);
|
|
3525
|
+
const intrinsic = extractSvgDimensions(sanitized);
|
|
3526
|
+
const w = opts2.w ?? intrinsic.w;
|
|
3527
|
+
const h = opts2.h ?? intrinsic.h;
|
|
3528
|
+
const mergedStyle = opts2.color || opts2.style ? { ...opts2.color ? { iconColor: opts2.color } : {}, ...opts2.style } : void 0;
|
|
3529
|
+
const id = asNodeId(idGenerator());
|
|
3530
|
+
this.addNode({
|
|
3531
|
+
id,
|
|
3532
|
+
type: "icon",
|
|
3533
|
+
x: opts2.x,
|
|
3534
|
+
y: opts2.y,
|
|
3535
|
+
w,
|
|
3536
|
+
h,
|
|
3537
|
+
angle: 0,
|
|
3538
|
+
z: 0,
|
|
3539
|
+
groups: [],
|
|
3540
|
+
...mergedStyle ? { style: mergedStyle } : {},
|
|
3541
|
+
data: { src: sanitized, alt: opts2.alt }
|
|
3542
|
+
});
|
|
3543
|
+
return id;
|
|
3544
|
+
},
|
|
3331
3545
|
addEdge(edge) {
|
|
3332
3546
|
const withZ = edge.z === 0 ? { ...edge, z: ++topZ } : edge;
|
|
3333
3547
|
if (withZ.z > topZ) topZ = withZ.z;
|
|
@@ -3512,6 +3726,53 @@ var createCanvasStore = (opts = {}) => {
|
|
|
3512
3726
|
getNodeCount: () => nodeIdsAtom.value.length,
|
|
3513
3727
|
getEdgeCount: () => edgeIdsAtom.value.length,
|
|
3514
3728
|
getGroupCount: () => groupIdsAtom.value.length,
|
|
3729
|
+
getFrames: () => {
|
|
3730
|
+
const out = [];
|
|
3731
|
+
for (const id of frameOrderAtom.value) {
|
|
3732
|
+
const n = nodeAtoms.get(id)?.value;
|
|
3733
|
+
if (n && n.type === "frame") out.push(n);
|
|
3734
|
+
}
|
|
3735
|
+
return out;
|
|
3736
|
+
},
|
|
3737
|
+
setFrameOrder(ids) {
|
|
3738
|
+
const valid = /* @__PURE__ */ new Set();
|
|
3739
|
+
for (const a of nodeAtoms.values()) {
|
|
3740
|
+
if (a.value.type === "frame") valid.add(a.value.id);
|
|
3741
|
+
}
|
|
3742
|
+
const filtered = [];
|
|
3743
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3744
|
+
for (const id of ids) {
|
|
3745
|
+
if (valid.has(id) && !seen.has(id)) {
|
|
3746
|
+
filtered.push(id);
|
|
3747
|
+
seen.add(id);
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
for (const id of valid) {
|
|
3751
|
+
if (!seen.has(id)) filtered.push(id);
|
|
3752
|
+
}
|
|
3753
|
+
const prev = [...frameOrderAtom.value];
|
|
3754
|
+
if (filtered.length === prev.length && filtered.every((id, i) => id === prev[i])) {
|
|
3755
|
+
return;
|
|
3756
|
+
}
|
|
3757
|
+
enqueueOp({ type: "frame.reorder", ids: filtered, prev });
|
|
3758
|
+
},
|
|
3759
|
+
getNodesInFrame(id) {
|
|
3760
|
+
const frame = nodeAtoms.get(id)?.value;
|
|
3761
|
+
if (!frame || frame.type !== "frame") return [];
|
|
3762
|
+
const frameAabb = nodeAABB(frame);
|
|
3763
|
+
const candidates = nodeIndex.queryRect(frameAabb);
|
|
3764
|
+
const out = [];
|
|
3765
|
+
for (const cid of candidates) {
|
|
3766
|
+
if (cid === id) continue;
|
|
3767
|
+
const node = nodeAtoms.get(cid)?.value;
|
|
3768
|
+
if (!node || node.type === "frame") continue;
|
|
3769
|
+
const a = nodeAABB(node);
|
|
3770
|
+
if (a.x >= frameAabb.x && a.y >= frameAabb.y && a.x + a.w <= frameAabb.x + frameAabb.w && a.y + a.h <= frameAabb.y + frameAabb.h) {
|
|
3771
|
+
out.push(node);
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
return out;
|
|
3775
|
+
},
|
|
3515
3776
|
getEdgeGeometry(id) {
|
|
3516
3777
|
const edge = edgeAtoms.get(id)?.value;
|
|
3517
3778
|
if (!edge) return void 0;
|
|
@@ -3673,7 +3934,8 @@ var toSerialized = (scene) => ({
|
|
|
3673
3934
|
edges: Object.values(scene.edges),
|
|
3674
3935
|
groups: Object.values(scene.groups),
|
|
3675
3936
|
camera: scene.camera,
|
|
3676
|
-
selection: scene.selection
|
|
3937
|
+
selection: scene.selection,
|
|
3938
|
+
...scene.frameOrder && scene.frameOrder.length > 0 ? { frameOrder: scene.frameOrder } : {}
|
|
3677
3939
|
});
|
|
3678
3940
|
var fromSerialized = (raw) => {
|
|
3679
3941
|
let working = raw;
|
|
@@ -3695,7 +3957,8 @@ var fromSerialized = (raw) => {
|
|
|
3695
3957
|
edges: Object.fromEntries(ser.edges.map((e) => [asEdgeId(e.id), e])),
|
|
3696
3958
|
groups: Object.fromEntries(ser.groups.map((g) => [asGroupId(g.id), g])),
|
|
3697
3959
|
camera: ser.camera,
|
|
3698
|
-
selection: ser.selection
|
|
3960
|
+
selection: ser.selection,
|
|
3961
|
+
...ser.frameOrder ? { frameOrder: ser.frameOrder } : {}
|
|
3699
3962
|
};
|
|
3700
3963
|
};
|
|
3701
3964
|
var storeToJSON = (store) => ({
|
|
@@ -3800,6 +4063,182 @@ var createFrameLoop = ({ draw, historySize = 60 }) => {
|
|
|
3800
4063
|
};
|
|
3801
4064
|
};
|
|
3802
4065
|
|
|
4066
|
+
// src/render/assets/cache.ts
|
|
4067
|
+
var MAX_ENTRIES2 = 256;
|
|
4068
|
+
var bucketSize = (px) => {
|
|
4069
|
+
if (px <= 32) return 32;
|
|
4070
|
+
if (px <= 64) return 64;
|
|
4071
|
+
if (px <= 128) return 128;
|
|
4072
|
+
if (px <= 256) return 256;
|
|
4073
|
+
if (px <= 512) return 512;
|
|
4074
|
+
return Math.ceil(px / 256) * 256;
|
|
4075
|
+
};
|
|
4076
|
+
var createAssetCache = (opts = {}) => {
|
|
4077
|
+
const entries = /* @__PURE__ */ new Map();
|
|
4078
|
+
let disposed = false;
|
|
4079
|
+
const notify = () => {
|
|
4080
|
+
if (disposed) return;
|
|
4081
|
+
opts.onReady?.();
|
|
4082
|
+
};
|
|
4083
|
+
const touch = (key, entry) => {
|
|
4084
|
+
entries.delete(key);
|
|
4085
|
+
entries.set(key, entry);
|
|
4086
|
+
if (entries.size > MAX_ENTRIES2) {
|
|
4087
|
+
const oldestKey = entries.keys().next().value;
|
|
4088
|
+
if (oldestKey !== void 0) {
|
|
4089
|
+
const evicted = entries.get(oldestKey);
|
|
4090
|
+
if (evicted?.kind === "icon" && evicted.bitmap) evicted.bitmap.close?.();
|
|
4091
|
+
entries.delete(oldestKey);
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
};
|
|
4095
|
+
const startImageDecode = (key, src) => {
|
|
4096
|
+
const entry = { kind: "image", state: "pending", bitmap: null };
|
|
4097
|
+
touch(key, entry);
|
|
4098
|
+
const img = new Image();
|
|
4099
|
+
img.onload = () => {
|
|
4100
|
+
if (disposed) return;
|
|
4101
|
+
entry.state = "ready";
|
|
4102
|
+
entry.bitmap = img;
|
|
4103
|
+
notify();
|
|
4104
|
+
};
|
|
4105
|
+
img.onerror = (e) => {
|
|
4106
|
+
if (disposed) return;
|
|
4107
|
+
entry.state = "error";
|
|
4108
|
+
entry.err = e;
|
|
4109
|
+
notify();
|
|
4110
|
+
};
|
|
4111
|
+
img.src = src;
|
|
4112
|
+
};
|
|
4113
|
+
const startIconRaster = (key, markup, color, sizePx) => {
|
|
4114
|
+
const entry = { kind: "icon", state: "pending", bitmap: null };
|
|
4115
|
+
touch(key, entry);
|
|
4116
|
+
const colored = color ? applySvgColor(markup, color) : markup;
|
|
4117
|
+
const blob = new Blob([colored], { type: "image/svg+xml" });
|
|
4118
|
+
const url = URL.createObjectURL(blob);
|
|
4119
|
+
const img = new Image();
|
|
4120
|
+
img.onload = async () => {
|
|
4121
|
+
URL.revokeObjectURL(url);
|
|
4122
|
+
if (disposed) return;
|
|
4123
|
+
try {
|
|
4124
|
+
const bitmap = await createImageBitmap(img, {
|
|
4125
|
+
resizeWidth: sizePx,
|
|
4126
|
+
resizeHeight: sizePx,
|
|
4127
|
+
resizeQuality: "high"
|
|
4128
|
+
});
|
|
4129
|
+
if (disposed) {
|
|
4130
|
+
bitmap.close?.();
|
|
4131
|
+
return;
|
|
4132
|
+
}
|
|
4133
|
+
entry.state = "ready";
|
|
4134
|
+
entry.bitmap = bitmap;
|
|
4135
|
+
notify();
|
|
4136
|
+
} catch (e) {
|
|
4137
|
+
entry.state = "error";
|
|
4138
|
+
entry.err = e;
|
|
4139
|
+
notify();
|
|
4140
|
+
}
|
|
4141
|
+
};
|
|
4142
|
+
img.onerror = (e) => {
|
|
4143
|
+
URL.revokeObjectURL(url);
|
|
4144
|
+
if (disposed) return;
|
|
4145
|
+
entry.state = "error";
|
|
4146
|
+
entry.err = e;
|
|
4147
|
+
notify();
|
|
4148
|
+
};
|
|
4149
|
+
img.src = url;
|
|
4150
|
+
};
|
|
4151
|
+
return {
|
|
4152
|
+
getImage(src) {
|
|
4153
|
+
const key = `img:${src}`;
|
|
4154
|
+
const existing = entries.get(key);
|
|
4155
|
+
if (existing && existing.kind === "image") {
|
|
4156
|
+
if (existing.state === "ready") {
|
|
4157
|
+
touch(key, existing);
|
|
4158
|
+
return existing.bitmap;
|
|
4159
|
+
}
|
|
4160
|
+
return null;
|
|
4161
|
+
}
|
|
4162
|
+
startImageDecode(key, src);
|
|
4163
|
+
return null;
|
|
4164
|
+
},
|
|
4165
|
+
getIcon(markup, color, devicePixelSize) {
|
|
4166
|
+
const size = bucketSize(Math.max(1, Math.ceil(devicePixelSize)));
|
|
4167
|
+
const key = `icon:${size}:${color ?? ""}:${markup}`;
|
|
4168
|
+
const existing = entries.get(key);
|
|
4169
|
+
if (existing && existing.kind === "icon") {
|
|
4170
|
+
if (existing.state === "ready") {
|
|
4171
|
+
touch(key, existing);
|
|
4172
|
+
return existing.bitmap;
|
|
4173
|
+
}
|
|
4174
|
+
return null;
|
|
4175
|
+
}
|
|
4176
|
+
startIconRaster(key, markup, color, size);
|
|
4177
|
+
return null;
|
|
4178
|
+
},
|
|
4179
|
+
dispose() {
|
|
4180
|
+
disposed = true;
|
|
4181
|
+
for (const entry of entries.values()) {
|
|
4182
|
+
if (entry.kind === "icon" && entry.bitmap) entry.bitmap.close?.();
|
|
4183
|
+
}
|
|
4184
|
+
entries.clear();
|
|
4185
|
+
}
|
|
4186
|
+
};
|
|
4187
|
+
};
|
|
4188
|
+
|
|
4189
|
+
// src/render/assets/paint.ts
|
|
4190
|
+
var PLACEHOLDER_FILL = "#e5e7eb";
|
|
4191
|
+
var PLACEHOLDER_TEXT_FILL = "#94a3b8";
|
|
4192
|
+
var paintPlaceholder = (ctx, w, h, label) => {
|
|
4193
|
+
ctx.fillStyle = PLACEHOLDER_FILL;
|
|
4194
|
+
ctx.fillRect(0, 0, w, h);
|
|
4195
|
+
if (w >= 32 && h >= 16) {
|
|
4196
|
+
ctx.fillStyle = PLACEHOLDER_TEXT_FILL;
|
|
4197
|
+
ctx.font = "11px system-ui, sans-serif";
|
|
4198
|
+
ctx.textAlign = "center";
|
|
4199
|
+
ctx.textBaseline = "middle";
|
|
4200
|
+
ctx.fillText(label, w / 2, h / 2);
|
|
4201
|
+
}
|
|
4202
|
+
};
|
|
4203
|
+
var paintImageNode = (ctx, node, cache4, theme) => {
|
|
4204
|
+
if (node.w <= 0 || node.h <= 0) return;
|
|
4205
|
+
const data = node.data;
|
|
4206
|
+
if (!data?.src) return;
|
|
4207
|
+
const bitmap = cache4.getImage(data.src);
|
|
4208
|
+
const opacity = resolveOpacity(node.style, theme);
|
|
4209
|
+
const needsScope = opacity !== 1;
|
|
4210
|
+
if (needsScope) {
|
|
4211
|
+
ctx.save();
|
|
4212
|
+
ctx.globalAlpha = opacity;
|
|
4213
|
+
}
|
|
4214
|
+
if (bitmap?.complete) {
|
|
4215
|
+
ctx.drawImage(bitmap, 0, 0, node.w, node.h);
|
|
4216
|
+
} else {
|
|
4217
|
+
paintPlaceholder(ctx, node.w, node.h, "loading\u2026");
|
|
4218
|
+
}
|
|
4219
|
+
if (needsScope) ctx.restore();
|
|
4220
|
+
};
|
|
4221
|
+
var paintIconNode = (ctx, node, cache4, scale, theme) => {
|
|
4222
|
+
if (node.w <= 0 || node.h <= 0) return;
|
|
4223
|
+
const data = node.data;
|
|
4224
|
+
if (!data?.src) return;
|
|
4225
|
+
const sizePx = Math.max(node.w, node.h) * scale;
|
|
4226
|
+
const color = node.style?.iconColor;
|
|
4227
|
+
const bitmap = cache4.getIcon(data.src, color, sizePx);
|
|
4228
|
+
const opacity = resolveOpacity(node.style, theme);
|
|
4229
|
+
const needsScope = opacity !== 1;
|
|
4230
|
+
if (needsScope) {
|
|
4231
|
+
ctx.save();
|
|
4232
|
+
ctx.globalAlpha = opacity;
|
|
4233
|
+
}
|
|
4234
|
+
if (bitmap) {
|
|
4235
|
+
ctx.drawImage(bitmap, 0, 0, node.w, node.h);
|
|
4236
|
+
} else {
|
|
4237
|
+
paintPlaceholder(ctx, node.w, node.h, "svg\u2026");
|
|
4238
|
+
}
|
|
4239
|
+
if (needsScope) ctx.restore();
|
|
4240
|
+
};
|
|
4241
|
+
|
|
3803
4242
|
// src/render/background.ts
|
|
3804
4243
|
var MIN_PATTERN_SCREEN_PX = 8;
|
|
3805
4244
|
var MIN_VISIBLE_PATTERN_PX = 2;
|
|
@@ -3941,14 +4380,14 @@ var hitTestRotateHandle = (node, worldPoint, cameraZ) => {
|
|
|
3941
4380
|
};
|
|
3942
4381
|
|
|
3943
4382
|
// src/render/overlay.ts
|
|
3944
|
-
var
|
|
4383
|
+
var DEFAULT_SELECTION_COLOR = "#3b82f6";
|
|
3945
4384
|
var SELECTION_OUTLINE_PX = 1.5;
|
|
3946
|
-
var
|
|
4385
|
+
var MARQUEE_FILL_ALPHA = 0.08;
|
|
3947
4386
|
var MARQUEE_STROKE_PX = 1;
|
|
3948
|
-
var drawSelectionOutline = (ctx, node, scale) => {
|
|
4387
|
+
var drawSelectionOutline = (ctx, node, scale, color) => {
|
|
3949
4388
|
if (node.angle === 0) {
|
|
3950
4389
|
ctx.save();
|
|
3951
|
-
ctx.strokeStyle =
|
|
4390
|
+
ctx.strokeStyle = color;
|
|
3952
4391
|
ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
|
|
3953
4392
|
ctx.beginPath();
|
|
3954
4393
|
ctx.rect(node.x, node.y, node.w, node.h);
|
|
@@ -3967,7 +4406,7 @@ var drawSelectionOutline = (ctx, node, scale) => {
|
|
|
3967
4406
|
{ x: -node.w / 2, y: node.h / 2 }
|
|
3968
4407
|
].map((p) => ({ x: cx + p.x * cos - p.y * sin, y: cy + p.x * sin + p.y * cos }));
|
|
3969
4408
|
ctx.save();
|
|
3970
|
-
ctx.strokeStyle =
|
|
4409
|
+
ctx.strokeStyle = color;
|
|
3971
4410
|
ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
|
|
3972
4411
|
ctx.beginPath();
|
|
3973
4412
|
const first = corners[0];
|
|
@@ -3980,13 +4419,13 @@ var drawSelectionOutline = (ctx, node, scale) => {
|
|
|
3980
4419
|
ctx.stroke();
|
|
3981
4420
|
ctx.restore();
|
|
3982
4421
|
};
|
|
3983
|
-
var drawResizeHandles = (ctx, node, scale) => {
|
|
4422
|
+
var drawResizeHandles = (ctx, node, scale, color) => {
|
|
3984
4423
|
const halfPx = RESIZE_HANDLE_SIZE_PX / 2;
|
|
3985
4424
|
const halfWorld = halfPx / scale;
|
|
3986
4425
|
const positions = handleWorldPositions(node);
|
|
3987
4426
|
ctx.save();
|
|
3988
4427
|
ctx.fillStyle = "#fff";
|
|
3989
|
-
ctx.strokeStyle =
|
|
4428
|
+
ctx.strokeStyle = color;
|
|
3990
4429
|
ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
|
|
3991
4430
|
for (const key of Object.keys(positions)) {
|
|
3992
4431
|
const p = positions[key];
|
|
@@ -3997,7 +4436,7 @@ var drawResizeHandles = (ctx, node, scale) => {
|
|
|
3997
4436
|
}
|
|
3998
4437
|
ctx.restore();
|
|
3999
4438
|
};
|
|
4000
|
-
var drawRotateHandle = (ctx, node, scale, cameraZ) => {
|
|
4439
|
+
var drawRotateHandle = (ctx, node, scale, cameraZ, color) => {
|
|
4001
4440
|
const center = rotateHandleWorldPosition(node, cameraZ);
|
|
4002
4441
|
const radiusWorld = ROTATE_HANDLE_RADIUS_PX / scale;
|
|
4003
4442
|
const cx = node.x + node.w / 2;
|
|
@@ -4010,7 +4449,7 @@ var drawRotateHandle = (ctx, node, scale, cameraZ) => {
|
|
|
4010
4449
|
y: cy + 0 * sin + topMidLocalY * cos
|
|
4011
4450
|
};
|
|
4012
4451
|
ctx.save();
|
|
4013
|
-
ctx.strokeStyle =
|
|
4452
|
+
ctx.strokeStyle = color;
|
|
4014
4453
|
ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
|
|
4015
4454
|
ctx.beginPath();
|
|
4016
4455
|
ctx.moveTo(topMidWorld.x, topMidWorld.y);
|
|
@@ -4023,12 +4462,12 @@ var drawRotateHandle = (ctx, node, scale, cameraZ) => {
|
|
|
4023
4462
|
ctx.stroke();
|
|
4024
4463
|
ctx.restore();
|
|
4025
4464
|
};
|
|
4026
|
-
var drawEdgeMidpointHandle = (ctx, midpoint, scale) => {
|
|
4465
|
+
var drawEdgeMidpointHandle = (ctx, midpoint, scale, color) => {
|
|
4027
4466
|
const radiusPx = 5;
|
|
4028
4467
|
const radiusWorld = radiusPx / scale;
|
|
4029
4468
|
ctx.save();
|
|
4030
4469
|
ctx.fillStyle = "#fff";
|
|
4031
|
-
ctx.strokeStyle =
|
|
4470
|
+
ctx.strokeStyle = color;
|
|
4032
4471
|
ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
|
|
4033
4472
|
ctx.beginPath();
|
|
4034
4473
|
ctx.arc(midpoint.x, midpoint.y, radiusWorld, 0, Math.PI * 2);
|
|
@@ -4036,12 +4475,12 @@ var drawEdgeMidpointHandle = (ctx, midpoint, scale) => {
|
|
|
4036
4475
|
ctx.stroke();
|
|
4037
4476
|
ctx.restore();
|
|
4038
4477
|
};
|
|
4039
|
-
var drawEdgeEndpointHandles = (ctx, source, target, scale) => {
|
|
4478
|
+
var drawEdgeEndpointHandles = (ctx, source, target, scale, color) => {
|
|
4040
4479
|
const radiusPx = 5;
|
|
4041
4480
|
const radiusWorld = radiusPx / scale;
|
|
4042
4481
|
ctx.save();
|
|
4043
4482
|
ctx.fillStyle = "#fff";
|
|
4044
|
-
ctx.strokeStyle =
|
|
4483
|
+
ctx.strokeStyle = color;
|
|
4045
4484
|
ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
|
|
4046
4485
|
for (const p of [source, target]) {
|
|
4047
4486
|
ctx.beginPath();
|
|
@@ -4051,17 +4490,53 @@ var drawEdgeEndpointHandles = (ctx, source, target, scale) => {
|
|
|
4051
4490
|
}
|
|
4052
4491
|
ctx.restore();
|
|
4053
4492
|
};
|
|
4054
|
-
var drawMarquee = (ctx, rect, scale) => {
|
|
4493
|
+
var drawMarquee = (ctx, rect, scale, color) => {
|
|
4055
4494
|
ctx.save();
|
|
4056
|
-
ctx.
|
|
4495
|
+
ctx.globalAlpha = MARQUEE_FILL_ALPHA;
|
|
4496
|
+
ctx.fillStyle = color;
|
|
4057
4497
|
ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
|
|
4058
|
-
ctx.
|
|
4498
|
+
ctx.globalAlpha = 1;
|
|
4499
|
+
ctx.strokeStyle = color;
|
|
4059
4500
|
ctx.lineWidth = MARQUEE_STROKE_PX / scale;
|
|
4060
4501
|
ctx.setLineDash([4 / scale, 3 / scale]);
|
|
4061
4502
|
ctx.strokeRect(rect.x, rect.y, rect.w, rect.h);
|
|
4062
4503
|
ctx.restore();
|
|
4063
4504
|
};
|
|
4064
4505
|
|
|
4506
|
+
// src/render/paint-frame.ts
|
|
4507
|
+
var FRAME_BORDER_PX = 1.5;
|
|
4508
|
+
var FRAME_BORDER_COLOR_DEFAULT = "#94a3b8";
|
|
4509
|
+
var FRAME_FILL_DEFAULT = "rgba(148, 163, 184, 0.06)";
|
|
4510
|
+
var FRAME_LABEL_FONT_PX = 12;
|
|
4511
|
+
var FRAME_LABEL_GAP_PX = 6;
|
|
4512
|
+
var FRAME_LABEL_COLOR = "#64748b";
|
|
4513
|
+
var paintFrameNode = (ctx, node, scale, theme) => {
|
|
4514
|
+
if (node.w <= 0 || node.h <= 0) return;
|
|
4515
|
+
const opacity = resolveOpacity(node.style, theme);
|
|
4516
|
+
const needsScope = opacity !== 1;
|
|
4517
|
+
if (needsScope) {
|
|
4518
|
+
ctx.save();
|
|
4519
|
+
ctx.globalAlpha = opacity;
|
|
4520
|
+
}
|
|
4521
|
+
const fill = node.style?.backgroundColor ?? (theme ? theme("frame.background") : void 0) ?? FRAME_FILL_DEFAULT;
|
|
4522
|
+
ctx.fillStyle = fill;
|
|
4523
|
+
ctx.fillRect(0, 0, node.w, node.h);
|
|
4524
|
+
const stroke = resolveColor(node.style, "strokeColor", FRAME_BORDER_COLOR_DEFAULT, theme);
|
|
4525
|
+
ctx.strokeStyle = stroke;
|
|
4526
|
+
ctx.lineWidth = FRAME_BORDER_PX / scale;
|
|
4527
|
+
ctx.setLineDash([]);
|
|
4528
|
+
ctx.strokeRect(0, 0, node.w, node.h);
|
|
4529
|
+
const labelPx = FRAME_LABEL_FONT_PX / scale;
|
|
4530
|
+
const gapPx = FRAME_LABEL_GAP_PX / scale;
|
|
4531
|
+
const label = node.content?.trim() || "Frame";
|
|
4532
|
+
ctx.fillStyle = FRAME_LABEL_COLOR;
|
|
4533
|
+
ctx.textBaseline = "bottom";
|
|
4534
|
+
ctx.textAlign = "left";
|
|
4535
|
+
ctx.font = `500 ${labelPx}px system-ui, -apple-system, sans-serif`;
|
|
4536
|
+
ctx.fillText(label, 0, -gapPx);
|
|
4537
|
+
if (needsScope) ctx.restore();
|
|
4538
|
+
};
|
|
4539
|
+
|
|
4065
4540
|
// src/render/shapes/content-bounds.ts
|
|
4066
4541
|
var SQRT2_INV = 1 / Math.SQRT2;
|
|
4067
4542
|
var contentBounds = (node) => {
|
|
@@ -4134,12 +4609,19 @@ var createRenderer = (opts) => {
|
|
|
4134
4609
|
const staticSurface = setupSurface(opts.staticCanvas);
|
|
4135
4610
|
const interactiveSurface = setupSurface(opts.interactiveCanvas);
|
|
4136
4611
|
let background = opts.background;
|
|
4612
|
+
let selectionColor = opts.selectionColor ?? DEFAULT_SELECTION_COLOR;
|
|
4613
|
+
let hideFrames = false;
|
|
4137
4614
|
sizeSurface(staticSurface, opts.width, opts.height);
|
|
4138
4615
|
sizeSurface(interactiveSurface, opts.width, opts.height);
|
|
4139
4616
|
let staticDirty = true;
|
|
4140
4617
|
let interactiveDirty = false;
|
|
4141
4618
|
let overlaySet = /* @__PURE__ */ new Set();
|
|
4142
4619
|
let lastDrawn = 0;
|
|
4620
|
+
const requestRepaint = () => {
|
|
4621
|
+
staticDirty = true;
|
|
4622
|
+
loop.requestFrame();
|
|
4623
|
+
};
|
|
4624
|
+
const assetCache = createAssetCache({ onReady: requestRepaint });
|
|
4143
4625
|
const isInteractive = (state) => state.mode !== "idle" || store.getSelection().length > 0;
|
|
4144
4626
|
const drawFrame = () => {
|
|
4145
4627
|
if (staticDirty) {
|
|
@@ -4178,7 +4660,18 @@ var createRenderer = (opts) => {
|
|
|
4178
4660
|
const cameraIsMoving = interaction.mode === "panning" || interaction.mode === "zooming";
|
|
4179
4661
|
const movingNodeCount = excludedNodes?.size ?? 0;
|
|
4180
4662
|
const roughEnabled = !cameraIsMoving && movingNodeCount <= ROUGH_MAX_MOVING_NODES && camera.z >= ROUGH_MIN_ZOOM && visible.length <= ROUGH_MAX_NODES;
|
|
4663
|
+
if (!hideFrames) {
|
|
4664
|
+
for (const node of visible) {
|
|
4665
|
+
if (node.type !== "frame") continue;
|
|
4666
|
+
if (excludedNodes?.has(node.id)) continue;
|
|
4667
|
+
drawWithNodeTransform(staticSurface.ctx, node, () => {
|
|
4668
|
+
paintFrameNode(staticSurface.ctx, node, scale, theme);
|
|
4669
|
+
});
|
|
4670
|
+
drawn++;
|
|
4671
|
+
}
|
|
4672
|
+
}
|
|
4181
4673
|
for (const node of visible) {
|
|
4674
|
+
if (node.type === "frame") continue;
|
|
4182
4675
|
if (excludedNodes?.has(node.id)) continue;
|
|
4183
4676
|
const isEditingThis = editingNodeId === node.id;
|
|
4184
4677
|
if (isDrawablePrimitive(node.type)) {
|
|
@@ -4209,6 +4702,20 @@ var createRenderer = (opts) => {
|
|
|
4209
4702
|
drawn++;
|
|
4210
4703
|
continue;
|
|
4211
4704
|
}
|
|
4705
|
+
if (node.type === "image") {
|
|
4706
|
+
drawWithNodeTransform(staticSurface.ctx, node, () => {
|
|
4707
|
+
paintImageNode(staticSurface.ctx, node, assetCache, theme);
|
|
4708
|
+
});
|
|
4709
|
+
drawn++;
|
|
4710
|
+
continue;
|
|
4711
|
+
}
|
|
4712
|
+
if (node.type === "icon") {
|
|
4713
|
+
drawWithNodeTransform(staticSurface.ctx, node, () => {
|
|
4714
|
+
paintIconNode(staticSurface.ctx, node, assetCache, scale, theme);
|
|
4715
|
+
});
|
|
4716
|
+
drawn++;
|
|
4717
|
+
continue;
|
|
4718
|
+
}
|
|
4212
4719
|
if (node.type === "text") {
|
|
4213
4720
|
drawWithNodeTransform(staticSurface.ctx, node, () => {
|
|
4214
4721
|
if (isEditingThis) return;
|
|
@@ -4367,8 +4874,21 @@ var createRenderer = (opts) => {
|
|
|
4367
4874
|
isMoving: true};
|
|
4368
4875
|
const dragRoughEnabled = inDragMap.size <= ROUGH_MAX_MOVING_NODES && camera.z >= ROUGH_MIN_ZOOM;
|
|
4369
4876
|
for (const node of inDragMap.values()) {
|
|
4370
|
-
if (!isDrawablePrimitive(node.type) && node.type !== "text")
|
|
4877
|
+
if (!isDrawablePrimitive(node.type) && node.type !== "text" && node.type !== "image" && node.type !== "icon" && node.type !== "frame")
|
|
4878
|
+
continue;
|
|
4371
4879
|
drawWithNodeTransform(ctx, node, () => {
|
|
4880
|
+
if (node.type === "frame") {
|
|
4881
|
+
paintFrameNode(ctx, node, scale, theme);
|
|
4882
|
+
return;
|
|
4883
|
+
}
|
|
4884
|
+
if (node.type === "image") {
|
|
4885
|
+
paintImageNode(ctx, node, assetCache, theme);
|
|
4886
|
+
return;
|
|
4887
|
+
}
|
|
4888
|
+
if (node.type === "icon") {
|
|
4889
|
+
paintIconNode(ctx, node, assetCache, scale, theme);
|
|
4890
|
+
return;
|
|
4891
|
+
}
|
|
4372
4892
|
if (isDrawablePrimitive(node.type)) {
|
|
4373
4893
|
const useRough = dragRoughEnabled && (node.style?.roughness ?? 0) > 0;
|
|
4374
4894
|
const roughReady = useRough ? getRoughCanvasCtor() !== null : false;
|
|
@@ -4420,32 +4940,32 @@ var createRenderer = (opts) => {
|
|
|
4420
4940
|
for (const id of selectedNodeIds) {
|
|
4421
4941
|
const node = inDragMap.get(id) ?? store.getNode(id);
|
|
4422
4942
|
if (!node) continue;
|
|
4423
|
-
drawSelectionOutline(ctx, node, scale);
|
|
4943
|
+
drawSelectionOutline(ctx, node, scale, selectionColor);
|
|
4424
4944
|
}
|
|
4425
4945
|
if (interaction.mode !== "dragging" && selectedNodeIds.length === 1) {
|
|
4426
4946
|
const node = inDragMap.get(selectedNodeIds[0]) ?? store.getNode(selectedNodeIds[0]);
|
|
4427
4947
|
if (node) {
|
|
4428
|
-
drawResizeHandles(ctx, node, scale);
|
|
4429
|
-
drawRotateHandle(ctx, node, scale, camera.z);
|
|
4948
|
+
drawResizeHandles(ctx, node, scale, selectionColor);
|
|
4949
|
+
drawRotateHandle(ctx, node, scale, camera.z, selectionColor);
|
|
4430
4950
|
}
|
|
4431
4951
|
}
|
|
4432
4952
|
}
|
|
4433
4953
|
for (const id of selectedEdgeIds) {
|
|
4434
4954
|
const geom = store.getEdgeGeometry(id);
|
|
4435
4955
|
if (geom) {
|
|
4436
|
-
drawEdgeEndpointHandles(ctx, geom.source, geom.target, scale);
|
|
4956
|
+
drawEdgeEndpointHandles(ctx, geom.source, geom.target, scale, selectionColor);
|
|
4437
4957
|
const edge = store.getEdge(id);
|
|
4438
4958
|
if (edge && edge.pathStyle === "bezier") {
|
|
4439
4959
|
const mid = getPointAndTangentAtArcLength(geom.samples, 0.5).point;
|
|
4440
|
-
drawEdgeMidpointHandle(ctx, mid, scale);
|
|
4960
|
+
drawEdgeMidpointHandle(ctx, mid, scale, selectionColor);
|
|
4441
4961
|
}
|
|
4442
4962
|
}
|
|
4443
4963
|
}
|
|
4444
4964
|
if (interaction.mode === "marqueeing" && interaction.marqueeRect) {
|
|
4445
|
-
drawMarquee(ctx, interaction.marqueeRect, scale);
|
|
4965
|
+
drawMarquee(ctx, interaction.marqueeRect, scale, selectionColor);
|
|
4446
4966
|
}
|
|
4447
4967
|
if (interaction.mode === "creating-shape" && interaction.createDraftRect) {
|
|
4448
|
-
drawMarquee(ctx, interaction.createDraftRect, scale);
|
|
4968
|
+
drawMarquee(ctx, interaction.createDraftRect, scale, selectionColor);
|
|
4449
4969
|
}
|
|
4450
4970
|
if ((interaction.mode === "creating-edge" || interaction.mode === "reconnecting-edge") && interaction.draftEdge) {
|
|
4451
4971
|
const draft = {
|
|
@@ -4453,7 +4973,7 @@ var createRenderer = (opts) => {
|
|
|
4453
4973
|
source: interaction.draftEdge.source,
|
|
4454
4974
|
target: interaction.draftEdge.target,
|
|
4455
4975
|
pathStyle: "bezier",
|
|
4456
|
-
style: { strokeColor:
|
|
4976
|
+
style: { strokeColor: selectionColor }
|
|
4457
4977
|
};
|
|
4458
4978
|
const geom = computeEdgeGeometry(draft, (id) => store.getNode(id));
|
|
4459
4979
|
if (geom) {
|
|
@@ -4557,6 +5077,16 @@ var createRenderer = (opts) => {
|
|
|
4557
5077
|
staticDirty = true;
|
|
4558
5078
|
loop.requestFrame();
|
|
4559
5079
|
},
|
|
5080
|
+
setSelectionColor(color) {
|
|
5081
|
+
selectionColor = color;
|
|
5082
|
+
interactiveDirty = true;
|
|
5083
|
+
loop.requestFrame();
|
|
5084
|
+
},
|
|
5085
|
+
setHideFrames(hidden) {
|
|
5086
|
+
hideFrames = hidden;
|
|
5087
|
+
staticDirty = true;
|
|
5088
|
+
loop.requestFrame();
|
|
5089
|
+
},
|
|
4560
5090
|
stats: () => loop.stats(),
|
|
4561
5091
|
lastDrawCount: () => lastDrawn,
|
|
4562
5092
|
getOverlaySet: () => [...overlaySet],
|
|
@@ -4567,6 +5097,7 @@ var createRenderer = (opts) => {
|
|
|
4567
5097
|
unsubSelection();
|
|
4568
5098
|
unsubInteraction();
|
|
4569
5099
|
unsubFontEpoch();
|
|
5100
|
+
assetCache.dispose();
|
|
4570
5101
|
}
|
|
4571
5102
|
};
|
|
4572
5103
|
};
|
|
@@ -4586,6 +5117,7 @@ var sceneBounds = (store) => {
|
|
|
4586
5117
|
let maxY = Number.NEGATIVE_INFINITY;
|
|
4587
5118
|
for (const n of nodes) {
|
|
4588
5119
|
if (n.hidden) continue;
|
|
5120
|
+
if (n.type === "frame") continue;
|
|
4589
5121
|
const r = nodeAABB(n);
|
|
4590
5122
|
if (r.x < minX) minX = r.x;
|
|
4591
5123
|
if (r.y < minY) minY = r.y;
|
|
@@ -4616,6 +5148,7 @@ var renderMinimapContent = (ctx, store, mapWidth, mapHeight, opts = {}) => {
|
|
|
4616
5148
|
const defaultColor = opts.defaultNodeColor ?? "#94a3b8";
|
|
4617
5149
|
for (const node of store.getAllNodes()) {
|
|
4618
5150
|
if (node.hidden) continue;
|
|
5151
|
+
if (node.type === "frame") continue;
|
|
4619
5152
|
const r = nodeAABB(node);
|
|
4620
5153
|
const x = offX + (r.x - bx) * scale;
|
|
4621
5154
|
const y = offY + (r.y - by) * scale;
|
|
@@ -5616,6 +6149,8 @@ exports.FONT_FAMILY_MAP = FONT_FAMILY_MAP;
|
|
|
5616
6149
|
exports.FONT_SIZE_MAP = FONT_SIZE_MAP;
|
|
5617
6150
|
exports.LINE_HEIGHT_MAP = LINE_HEIGHT_MAP;
|
|
5618
6151
|
exports.LINK_COLOR = LINK_COLOR;
|
|
6152
|
+
exports.MAX_IMAGE_BYTES = MAX_IMAGE_BYTES;
|
|
6153
|
+
exports.MAX_SVG_BYTES = MAX_SVG_BYTES;
|
|
5619
6154
|
exports.MAX_ZOOM = MAX_ZOOM;
|
|
5620
6155
|
exports.MIN_ZOOM = MIN_ZOOM;
|
|
5621
6156
|
exports.PALM_REJECTION_GRACE_MS = PALM_REJECTION_GRACE_MS;
|
|
@@ -5627,6 +6162,7 @@ exports.SCHEMA_VERSION = SCHEMA_VERSION;
|
|
|
5627
6162
|
exports.UniformGrid = UniformGrid;
|
|
5628
6163
|
exports.VERSION = VERSION;
|
|
5629
6164
|
exports.applyCameraTransform = applyCameraTransform;
|
|
6165
|
+
exports.applySvgColor = applySvgColor;
|
|
5630
6166
|
exports.arrowheadLength = arrowheadLength;
|
|
5631
6167
|
exports.asBatchId = asBatchId;
|
|
5632
6168
|
exports.asClientId = asClientId;
|
|
@@ -5635,6 +6171,7 @@ exports.asGroupId = asGroupId;
|
|
|
5635
6171
|
exports.asNodeId = asNodeId;
|
|
5636
6172
|
exports.attachSync = attachSync;
|
|
5637
6173
|
exports.autoRouteControls = autoRouteControls;
|
|
6174
|
+
exports.blobToDataUri = blobToDataUri;
|
|
5638
6175
|
exports.clampEffectiveScale = clampEffectiveScale;
|
|
5639
6176
|
exports.clampZoom = clampZoom;
|
|
5640
6177
|
exports.clearMeasureCache = clearMeasureCache;
|
|
@@ -5656,6 +6193,7 @@ exports.defineExtension = defineExtension;
|
|
|
5656
6193
|
exports.defineNode = defineNode;
|
|
5657
6194
|
exports.deserializeClipboard = deserializeClipboard;
|
|
5658
6195
|
exports.detectConflicts = detectConflicts;
|
|
6196
|
+
exports.downscaleImageBlob = downscaleImageBlob;
|
|
5659
6197
|
exports.drawArrowhead = drawArrowhead;
|
|
5660
6198
|
exports.drawEdge = drawEdge;
|
|
5661
6199
|
exports.drawMinimapViewport = drawMinimapViewport;
|
|
@@ -5669,6 +6207,7 @@ exports.estimateMarkdownContentHeight = estimateMarkdownContentHeight;
|
|
|
5669
6207
|
exports.exportSelection = exportSelection;
|
|
5670
6208
|
exports.exportSelectionSvg = exportSelectionSvg;
|
|
5671
6209
|
exports.exportViewport = exportViewport;
|
|
6210
|
+
exports.extractSvgDimensions = extractSvgDimensions;
|
|
5672
6211
|
exports.fromSerialized = fromSerialized;
|
|
5673
6212
|
exports.fullVisibleClipResult = fullVisibleClipResult;
|
|
5674
6213
|
exports.getCanvasFont = getCanvasFont;
|
|
@@ -5735,6 +6274,7 @@ exports.rotateVecByAngle = rotateVecByAngle;
|
|
|
5735
6274
|
exports.sampleBezier = sampleBezier;
|
|
5736
6275
|
exports.sampleSelfLoop = sampleSelfLoop;
|
|
5737
6276
|
exports.samplesFor = samplesFor;
|
|
6277
|
+
exports.sanitizeSvg = sanitizeSvg;
|
|
5738
6278
|
exports.sceneBounds = sceneBounds;
|
|
5739
6279
|
exports.screenToWorld = screenToWorld;
|
|
5740
6280
|
exports.selfLoopGeometry = selfLoopGeometry;
|
|
@@ -5748,6 +6288,7 @@ exports.sizeSurface = sizeSurface;
|
|
|
5748
6288
|
exports.storeToJSON = storeToJSON;
|
|
5749
6289
|
exports.subscribeFontEpoch = subscribeFontEpoch;
|
|
5750
6290
|
exports.tangentAtArcLength = tangentAtArcLength;
|
|
6291
|
+
exports.toImageBlob = toImageBlob;
|
|
5751
6292
|
exports.toSerialized = toSerialized;
|
|
5752
6293
|
exports.toggleBold = toggleBold;
|
|
5753
6294
|
exports.toggleCode = toggleCode;
|
|
@@ -5756,6 +6297,8 @@ exports.toggleStrike = toggleStrike;
|
|
|
5756
6297
|
exports.toggleUnderline = toggleUnderline;
|
|
5757
6298
|
exports.tokenize = tokenize;
|
|
5758
6299
|
exports.unionRects = unionRects;
|
|
6300
|
+
exports.validateImageInput = validateImageInput;
|
|
6301
|
+
exports.validateSvgMarkup = validateSvgMarkup;
|
|
5759
6302
|
exports.viewportWorldRect = viewportWorldRect;
|
|
5760
6303
|
exports.withAutoFitHeight = withAutoFitHeight;
|
|
5761
6304
|
exports.worldToNodeLocal = worldToNodeLocal;
|