@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.js
CHANGED
|
@@ -2906,6 +2906,155 @@ var createDefaultTextareaEditor = ({
|
|
|
2906
2906
|
};
|
|
2907
2907
|
};
|
|
2908
2908
|
|
|
2909
|
+
// src/assets/image.ts
|
|
2910
|
+
var MAX_IMAGE_BYTES = 2 * 1024 * 1024;
|
|
2911
|
+
var ACCEPTED_MIME = /* @__PURE__ */ new Set(["image/png", "image/jpeg"]);
|
|
2912
|
+
var validateImageInput = (input) => {
|
|
2913
|
+
if (typeof input === "string") {
|
|
2914
|
+
if (!input.startsWith("data:")) {
|
|
2915
|
+
throw new Error(
|
|
2916
|
+
"addImage: external URL strings are not supported. Pass a File, Blob, or `data:image/(png|jpeg)` URI."
|
|
2917
|
+
);
|
|
2918
|
+
}
|
|
2919
|
+
const mimeMatch = /^data:([^;,]+)/.exec(input);
|
|
2920
|
+
const mime = mimeMatch?.[1] ?? "";
|
|
2921
|
+
if (!ACCEPTED_MIME.has(mime)) {
|
|
2922
|
+
throw new Error(
|
|
2923
|
+
`addImage: unsupported MIME "${mime || "(unknown)"}". Only image/png and image/jpeg are supported.`
|
|
2924
|
+
);
|
|
2925
|
+
}
|
|
2926
|
+
const comma = input.indexOf(",");
|
|
2927
|
+
if (comma < 0) throw new Error("addImage: malformed data URI (missing payload separator)");
|
|
2928
|
+
const decodedBytes = Math.floor((input.length - comma - 1) * 3 / 4);
|
|
2929
|
+
if (decodedBytes > MAX_IMAGE_BYTES) {
|
|
2930
|
+
throw new Error(
|
|
2931
|
+
`addImage: image exceeds the 2 MB limit (${Math.round(decodedBytes / 1024)} KB).`
|
|
2932
|
+
);
|
|
2933
|
+
}
|
|
2934
|
+
return;
|
|
2935
|
+
}
|
|
2936
|
+
if (!ACCEPTED_MIME.has(input.type)) {
|
|
2937
|
+
throw new Error(
|
|
2938
|
+
`addImage: unsupported file type "${input.type || "(unknown)"}". Only image/png and image/jpeg are supported.`
|
|
2939
|
+
);
|
|
2940
|
+
}
|
|
2941
|
+
if (input.size > MAX_IMAGE_BYTES) {
|
|
2942
|
+
throw new Error(`addImage: file exceeds the 2 MB limit (${Math.round(input.size / 1024)} KB).`);
|
|
2943
|
+
}
|
|
2944
|
+
};
|
|
2945
|
+
var toImageBlob = async (input) => {
|
|
2946
|
+
if (typeof input === "string") {
|
|
2947
|
+
const res = await fetch(input);
|
|
2948
|
+
return res.blob();
|
|
2949
|
+
}
|
|
2950
|
+
return input;
|
|
2951
|
+
};
|
|
2952
|
+
var downscaleImageBlob = async (blob, maxDim) => {
|
|
2953
|
+
const bitmap = await createImageBitmap(blob);
|
|
2954
|
+
const naturalW = bitmap.width;
|
|
2955
|
+
const naturalH = bitmap.height;
|
|
2956
|
+
const maxSide = Math.max(naturalW, naturalH);
|
|
2957
|
+
if (maxDim <= 0 || maxSide <= maxDim) {
|
|
2958
|
+
bitmap.close?.();
|
|
2959
|
+
return { blob, naturalW, naturalH };
|
|
2960
|
+
}
|
|
2961
|
+
const scale = maxDim / maxSide;
|
|
2962
|
+
const w = Math.max(1, Math.round(naturalW * scale));
|
|
2963
|
+
const h = Math.max(1, Math.round(naturalH * scale));
|
|
2964
|
+
const canvas = new OffscreenCanvas(w, h);
|
|
2965
|
+
const ctx = canvas.getContext("2d");
|
|
2966
|
+
if (!ctx) throw new Error("addImage: failed to acquire OffscreenCanvas 2d context");
|
|
2967
|
+
ctx.drawImage(bitmap, 0, 0, w, h);
|
|
2968
|
+
bitmap.close?.();
|
|
2969
|
+
const outType = blob.type === "image/png" ? "image/png" : "image/jpeg";
|
|
2970
|
+
const outBlob = await canvas.convertToBlob({ type: outType, quality: 0.9 });
|
|
2971
|
+
return { blob: outBlob, naturalW: w, naturalH: h };
|
|
2972
|
+
};
|
|
2973
|
+
var blobToDataUri = (blob) => {
|
|
2974
|
+
return new Promise((resolve, reject) => {
|
|
2975
|
+
const reader = new FileReader();
|
|
2976
|
+
reader.onload = () => {
|
|
2977
|
+
if (typeof reader.result === "string") resolve(reader.result);
|
|
2978
|
+
else reject(new Error("FileReader returned non-string result"));
|
|
2979
|
+
};
|
|
2980
|
+
reader.onerror = () => reject(reader.error ?? new Error("FileReader failed"));
|
|
2981
|
+
reader.readAsDataURL(blob);
|
|
2982
|
+
});
|
|
2983
|
+
};
|
|
2984
|
+
|
|
2985
|
+
// src/assets/svg.ts
|
|
2986
|
+
var MAX_SVG_BYTES = 2 * 1024 * 1024;
|
|
2987
|
+
var DEFAULT_SVG_SIZE = 24;
|
|
2988
|
+
var validateSvgMarkup = (markup) => {
|
|
2989
|
+
if (typeof markup !== "string") {
|
|
2990
|
+
throw new Error("addSvg: src must be a string of SVG markup");
|
|
2991
|
+
}
|
|
2992
|
+
const byteLen = new Blob([markup]).size;
|
|
2993
|
+
if (byteLen > MAX_SVG_BYTES) {
|
|
2994
|
+
throw new Error(`addSvg: SVG markup exceeds the 2 MB limit (${Math.round(byteLen / 1024)} KB).`);
|
|
2995
|
+
}
|
|
2996
|
+
if (!/<svg[\s>]/i.test(markup)) {
|
|
2997
|
+
throw new Error("addSvg: src does not look like SVG markup (no <svg> tag found)");
|
|
2998
|
+
}
|
|
2999
|
+
};
|
|
3000
|
+
var sanitizeSvg = (markup) => {
|
|
3001
|
+
const parser = new DOMParser();
|
|
3002
|
+
const doc = parser.parseFromString(markup, "image/svg+xml");
|
|
3003
|
+
const root = doc.documentElement;
|
|
3004
|
+
if (root.nodeName === "parsererror" || root.querySelector("parsererror")) {
|
|
3005
|
+
throw new Error("addSvg: malformed SVG (parser error)");
|
|
3006
|
+
}
|
|
3007
|
+
const removable = [];
|
|
3008
|
+
const walker = doc.createTreeWalker(doc, NodeFilter.SHOW_ELEMENT);
|
|
3009
|
+
let n = walker.nextNode();
|
|
3010
|
+
while (n) {
|
|
3011
|
+
const el = n;
|
|
3012
|
+
const tag = el.tagName.toLowerCase();
|
|
3013
|
+
if (tag === "script" || tag === "foreignobject") {
|
|
3014
|
+
removable.push(el);
|
|
3015
|
+
} else {
|
|
3016
|
+
for (const attr of [...el.attributes]) {
|
|
3017
|
+
const name = attr.name.toLowerCase();
|
|
3018
|
+
const value = attr.value.trim().toLowerCase();
|
|
3019
|
+
if (name.startsWith("on")) {
|
|
3020
|
+
el.removeAttribute(attr.name);
|
|
3021
|
+
} else if ((name === "href" || name === "xlink:href" || name === "src") && value.startsWith("javascript:")) {
|
|
3022
|
+
el.removeAttribute(attr.name);
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
n = walker.nextNode();
|
|
3027
|
+
}
|
|
3028
|
+
for (const el of removable) el.remove();
|
|
3029
|
+
return new XMLSerializer().serializeToString(doc);
|
|
3030
|
+
};
|
|
3031
|
+
var extractSvgDimensions = (markup) => {
|
|
3032
|
+
const parser = new DOMParser();
|
|
3033
|
+
const doc = parser.parseFromString(markup, "image/svg+xml");
|
|
3034
|
+
const svg = doc.documentElement;
|
|
3035
|
+
if (svg.nodeName.toLowerCase() !== "svg") {
|
|
3036
|
+
return { w: DEFAULT_SVG_SIZE, h: DEFAULT_SVG_SIZE };
|
|
3037
|
+
}
|
|
3038
|
+
const widthAttr = svg.getAttribute("width");
|
|
3039
|
+
const heightAttr = svg.getAttribute("height");
|
|
3040
|
+
if (widthAttr && heightAttr) {
|
|
3041
|
+
const w = Number.parseFloat(widthAttr);
|
|
3042
|
+
const h = Number.parseFloat(heightAttr);
|
|
3043
|
+
if (Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0) return { w, h };
|
|
3044
|
+
}
|
|
3045
|
+
const viewBox = svg.getAttribute("viewBox");
|
|
3046
|
+
if (viewBox) {
|
|
3047
|
+
const parts = viewBox.split(/[\s,]+/).map(Number.parseFloat);
|
|
3048
|
+
if (parts.length === 4 && parts.every(Number.isFinite) && parts[2] > 0 && parts[3] > 0) {
|
|
3049
|
+
return { w: parts[2], h: parts[3] };
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
return { w: DEFAULT_SVG_SIZE, h: DEFAULT_SVG_SIZE };
|
|
3053
|
+
};
|
|
3054
|
+
var applySvgColor = (markup, color) => {
|
|
3055
|
+
return markup.replace(/currentColor/gi, color);
|
|
3056
|
+
};
|
|
3057
|
+
|
|
2909
3058
|
// src/store/conflict.ts
|
|
2910
3059
|
var detectConflicts = (batch, getNode, getEdge) => {
|
|
2911
3060
|
const out = [];
|
|
@@ -2982,6 +3131,8 @@ var inverseOp = (op) => {
|
|
|
2982
3131
|
return { type: "group.remove", group: op.group };
|
|
2983
3132
|
case "group.remove":
|
|
2984
3133
|
return { type: "group.upsert", group: op.group };
|
|
3134
|
+
case "frame.reorder":
|
|
3135
|
+
return { type: "frame.reorder", ids: op.prev, prev: op.ids };
|
|
2985
3136
|
}
|
|
2986
3137
|
};
|
|
2987
3138
|
var inverseBatch = (batch) => {
|
|
@@ -3049,6 +3200,7 @@ var createCanvasStore = (opts = {}) => {
|
|
|
3049
3200
|
const groupIdsAtom = atom("groupIds", []);
|
|
3050
3201
|
const cameraAtom = atom("camera", initial.camera);
|
|
3051
3202
|
const selectionAtom = atom("selection", initial.selection);
|
|
3203
|
+
const frameOrderAtom = atom("frameOrder", initial.frameOrder ?? []);
|
|
3052
3204
|
const interactionAtom = atom("interaction", idleInteractionState());
|
|
3053
3205
|
const localPresenceAtom = atom("presence", emptyPresenceState(clientId));
|
|
3054
3206
|
const remotePresence = /* @__PURE__ */ new Map();
|
|
@@ -3155,6 +3307,9 @@ var createCanvasStore = (opts = {}) => {
|
|
|
3155
3307
|
nodeAtoms.set(op.node.id, a);
|
|
3156
3308
|
nodeIdsAtom.update((ids) => [...ids, op.node.id]);
|
|
3157
3309
|
reindexNode(op.node);
|
|
3310
|
+
if (op.node.type === "frame") {
|
|
3311
|
+
frameOrderAtom.update((ids) => ids.includes(op.node.id) ? ids : [...ids, op.node.id]);
|
|
3312
|
+
}
|
|
3158
3313
|
break;
|
|
3159
3314
|
}
|
|
3160
3315
|
case "node.update": {
|
|
@@ -3179,6 +3334,9 @@ var createCanvasStore = (opts = {}) => {
|
|
|
3179
3334
|
nodeIdsAtom.update((ids) => ids.filter((x) => x !== id));
|
|
3180
3335
|
unindexNode(id);
|
|
3181
3336
|
incidentEdges.delete(id);
|
|
3337
|
+
if (op.node.type === "frame") {
|
|
3338
|
+
frameOrderAtom.update((ids) => ids.filter((x) => x !== id));
|
|
3339
|
+
}
|
|
3182
3340
|
break;
|
|
3183
3341
|
}
|
|
3184
3342
|
case "edge.add": {
|
|
@@ -3228,6 +3386,10 @@ var createCanvasStore = (opts = {}) => {
|
|
|
3228
3386
|
groupIdsAtom.update((ids) => ids.filter((x) => x !== id));
|
|
3229
3387
|
break;
|
|
3230
3388
|
}
|
|
3389
|
+
case "frame.reorder": {
|
|
3390
|
+
frameOrderAtom.set([...op.ids]);
|
|
3391
|
+
break;
|
|
3392
|
+
}
|
|
3231
3393
|
}
|
|
3232
3394
|
};
|
|
3233
3395
|
const enqueueOp = (op) => {
|
|
@@ -3250,6 +3412,7 @@ var createCanvasStore = (opts = {}) => {
|
|
|
3250
3412
|
return prev;
|
|
3251
3413
|
};
|
|
3252
3414
|
const populateInitial = (scene) => {
|
|
3415
|
+
const seededFrameOrder = [];
|
|
3253
3416
|
for (const id of Object.keys(scene.nodes)) {
|
|
3254
3417
|
const node = scene.nodes[id];
|
|
3255
3418
|
if (!node) continue;
|
|
@@ -3257,7 +3420,9 @@ var createCanvasStore = (opts = {}) => {
|
|
|
3257
3420
|
nodeAtoms.set(node.id, a);
|
|
3258
3421
|
nodeIdsAtom.update((ids) => [...ids, node.id]);
|
|
3259
3422
|
reindexNode(node);
|
|
3423
|
+
if (node.type === "frame") seededFrameOrder.push(node.id);
|
|
3260
3424
|
}
|
|
3425
|
+
if (!scene.frameOrder) frameOrderAtom.set(seededFrameOrder);
|
|
3261
3426
|
for (const id of Object.keys(scene.edges)) {
|
|
3262
3427
|
const edge = scene.edges[id];
|
|
3263
3428
|
if (!edge) continue;
|
|
@@ -3326,6 +3491,55 @@ var createCanvasStore = (opts = {}) => {
|
|
|
3326
3491
|
if (batch) emitChange(batch);
|
|
3327
3492
|
});
|
|
3328
3493
|
},
|
|
3494
|
+
async addImage(opts2) {
|
|
3495
|
+
validateImageInput(opts2.src);
|
|
3496
|
+
const rawBlob = await toImageBlob(opts2.src);
|
|
3497
|
+
const maxDim = opts2.maxDimension ?? 2048;
|
|
3498
|
+
const { blob, naturalW, naturalH } = await downscaleImageBlob(rawBlob, maxDim);
|
|
3499
|
+
const src = await blobToDataUri(blob);
|
|
3500
|
+
const DEFAULT_MAX_NODE_SIDE = 400;
|
|
3501
|
+
const aspectScale = Math.min(1, DEFAULT_MAX_NODE_SIDE / Math.max(naturalW, naturalH));
|
|
3502
|
+
const w = opts2.w ?? Math.max(1, Math.round(naturalW * aspectScale));
|
|
3503
|
+
const h = opts2.h ?? Math.max(1, Math.round(naturalH * aspectScale));
|
|
3504
|
+
const id = asNodeId(idGenerator());
|
|
3505
|
+
this.addNode({
|
|
3506
|
+
id,
|
|
3507
|
+
type: "image",
|
|
3508
|
+
x: opts2.x,
|
|
3509
|
+
y: opts2.y,
|
|
3510
|
+
w,
|
|
3511
|
+
h,
|
|
3512
|
+
angle: 0,
|
|
3513
|
+
z: 0,
|
|
3514
|
+
groups: [],
|
|
3515
|
+
style: opts2.style,
|
|
3516
|
+
data: { src, naturalW, naturalH, alt: opts2.alt }
|
|
3517
|
+
});
|
|
3518
|
+
return id;
|
|
3519
|
+
},
|
|
3520
|
+
async addSvg(opts2) {
|
|
3521
|
+
validateSvgMarkup(opts2.src);
|
|
3522
|
+
const sanitized = sanitizeSvg(opts2.src);
|
|
3523
|
+
const intrinsic = extractSvgDimensions(sanitized);
|
|
3524
|
+
const w = opts2.w ?? intrinsic.w;
|
|
3525
|
+
const h = opts2.h ?? intrinsic.h;
|
|
3526
|
+
const mergedStyle = opts2.color || opts2.style ? { ...opts2.color ? { iconColor: opts2.color } : {}, ...opts2.style } : void 0;
|
|
3527
|
+
const id = asNodeId(idGenerator());
|
|
3528
|
+
this.addNode({
|
|
3529
|
+
id,
|
|
3530
|
+
type: "icon",
|
|
3531
|
+
x: opts2.x,
|
|
3532
|
+
y: opts2.y,
|
|
3533
|
+
w,
|
|
3534
|
+
h,
|
|
3535
|
+
angle: 0,
|
|
3536
|
+
z: 0,
|
|
3537
|
+
groups: [],
|
|
3538
|
+
...mergedStyle ? { style: mergedStyle } : {},
|
|
3539
|
+
data: { src: sanitized, alt: opts2.alt }
|
|
3540
|
+
});
|
|
3541
|
+
return id;
|
|
3542
|
+
},
|
|
3329
3543
|
addEdge(edge) {
|
|
3330
3544
|
const withZ = edge.z === 0 ? { ...edge, z: ++topZ } : edge;
|
|
3331
3545
|
if (withZ.z > topZ) topZ = withZ.z;
|
|
@@ -3510,6 +3724,53 @@ var createCanvasStore = (opts = {}) => {
|
|
|
3510
3724
|
getNodeCount: () => nodeIdsAtom.value.length,
|
|
3511
3725
|
getEdgeCount: () => edgeIdsAtom.value.length,
|
|
3512
3726
|
getGroupCount: () => groupIdsAtom.value.length,
|
|
3727
|
+
getFrames: () => {
|
|
3728
|
+
const out = [];
|
|
3729
|
+
for (const id of frameOrderAtom.value) {
|
|
3730
|
+
const n = nodeAtoms.get(id)?.value;
|
|
3731
|
+
if (n && n.type === "frame") out.push(n);
|
|
3732
|
+
}
|
|
3733
|
+
return out;
|
|
3734
|
+
},
|
|
3735
|
+
setFrameOrder(ids) {
|
|
3736
|
+
const valid = /* @__PURE__ */ new Set();
|
|
3737
|
+
for (const a of nodeAtoms.values()) {
|
|
3738
|
+
if (a.value.type === "frame") valid.add(a.value.id);
|
|
3739
|
+
}
|
|
3740
|
+
const filtered = [];
|
|
3741
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3742
|
+
for (const id of ids) {
|
|
3743
|
+
if (valid.has(id) && !seen.has(id)) {
|
|
3744
|
+
filtered.push(id);
|
|
3745
|
+
seen.add(id);
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
for (const id of valid) {
|
|
3749
|
+
if (!seen.has(id)) filtered.push(id);
|
|
3750
|
+
}
|
|
3751
|
+
const prev = [...frameOrderAtom.value];
|
|
3752
|
+
if (filtered.length === prev.length && filtered.every((id, i) => id === prev[i])) {
|
|
3753
|
+
return;
|
|
3754
|
+
}
|
|
3755
|
+
enqueueOp({ type: "frame.reorder", ids: filtered, prev });
|
|
3756
|
+
},
|
|
3757
|
+
getNodesInFrame(id) {
|
|
3758
|
+
const frame = nodeAtoms.get(id)?.value;
|
|
3759
|
+
if (!frame || frame.type !== "frame") return [];
|
|
3760
|
+
const frameAabb = nodeAABB(frame);
|
|
3761
|
+
const candidates = nodeIndex.queryRect(frameAabb);
|
|
3762
|
+
const out = [];
|
|
3763
|
+
for (const cid of candidates) {
|
|
3764
|
+
if (cid === id) continue;
|
|
3765
|
+
const node = nodeAtoms.get(cid)?.value;
|
|
3766
|
+
if (!node || node.type === "frame") continue;
|
|
3767
|
+
const a = nodeAABB(node);
|
|
3768
|
+
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) {
|
|
3769
|
+
out.push(node);
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
return out;
|
|
3773
|
+
},
|
|
3513
3774
|
getEdgeGeometry(id) {
|
|
3514
3775
|
const edge = edgeAtoms.get(id)?.value;
|
|
3515
3776
|
if (!edge) return void 0;
|
|
@@ -3671,7 +3932,8 @@ var toSerialized = (scene) => ({
|
|
|
3671
3932
|
edges: Object.values(scene.edges),
|
|
3672
3933
|
groups: Object.values(scene.groups),
|
|
3673
3934
|
camera: scene.camera,
|
|
3674
|
-
selection: scene.selection
|
|
3935
|
+
selection: scene.selection,
|
|
3936
|
+
...scene.frameOrder && scene.frameOrder.length > 0 ? { frameOrder: scene.frameOrder } : {}
|
|
3675
3937
|
});
|
|
3676
3938
|
var fromSerialized = (raw) => {
|
|
3677
3939
|
let working = raw;
|
|
@@ -3693,7 +3955,8 @@ var fromSerialized = (raw) => {
|
|
|
3693
3955
|
edges: Object.fromEntries(ser.edges.map((e) => [asEdgeId(e.id), e])),
|
|
3694
3956
|
groups: Object.fromEntries(ser.groups.map((g) => [asGroupId(g.id), g])),
|
|
3695
3957
|
camera: ser.camera,
|
|
3696
|
-
selection: ser.selection
|
|
3958
|
+
selection: ser.selection,
|
|
3959
|
+
...ser.frameOrder ? { frameOrder: ser.frameOrder } : {}
|
|
3697
3960
|
};
|
|
3698
3961
|
};
|
|
3699
3962
|
var storeToJSON = (store) => ({
|
|
@@ -3798,6 +4061,182 @@ var createFrameLoop = ({ draw, historySize = 60 }) => {
|
|
|
3798
4061
|
};
|
|
3799
4062
|
};
|
|
3800
4063
|
|
|
4064
|
+
// src/render/assets/cache.ts
|
|
4065
|
+
var MAX_ENTRIES2 = 256;
|
|
4066
|
+
var bucketSize = (px) => {
|
|
4067
|
+
if (px <= 32) return 32;
|
|
4068
|
+
if (px <= 64) return 64;
|
|
4069
|
+
if (px <= 128) return 128;
|
|
4070
|
+
if (px <= 256) return 256;
|
|
4071
|
+
if (px <= 512) return 512;
|
|
4072
|
+
return Math.ceil(px / 256) * 256;
|
|
4073
|
+
};
|
|
4074
|
+
var createAssetCache = (opts = {}) => {
|
|
4075
|
+
const entries = /* @__PURE__ */ new Map();
|
|
4076
|
+
let disposed = false;
|
|
4077
|
+
const notify = () => {
|
|
4078
|
+
if (disposed) return;
|
|
4079
|
+
opts.onReady?.();
|
|
4080
|
+
};
|
|
4081
|
+
const touch = (key, entry) => {
|
|
4082
|
+
entries.delete(key);
|
|
4083
|
+
entries.set(key, entry);
|
|
4084
|
+
if (entries.size > MAX_ENTRIES2) {
|
|
4085
|
+
const oldestKey = entries.keys().next().value;
|
|
4086
|
+
if (oldestKey !== void 0) {
|
|
4087
|
+
const evicted = entries.get(oldestKey);
|
|
4088
|
+
if (evicted?.kind === "icon" && evicted.bitmap) evicted.bitmap.close?.();
|
|
4089
|
+
entries.delete(oldestKey);
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
};
|
|
4093
|
+
const startImageDecode = (key, src) => {
|
|
4094
|
+
const entry = { kind: "image", state: "pending", bitmap: null };
|
|
4095
|
+
touch(key, entry);
|
|
4096
|
+
const img = new Image();
|
|
4097
|
+
img.onload = () => {
|
|
4098
|
+
if (disposed) return;
|
|
4099
|
+
entry.state = "ready";
|
|
4100
|
+
entry.bitmap = img;
|
|
4101
|
+
notify();
|
|
4102
|
+
};
|
|
4103
|
+
img.onerror = (e) => {
|
|
4104
|
+
if (disposed) return;
|
|
4105
|
+
entry.state = "error";
|
|
4106
|
+
entry.err = e;
|
|
4107
|
+
notify();
|
|
4108
|
+
};
|
|
4109
|
+
img.src = src;
|
|
4110
|
+
};
|
|
4111
|
+
const startIconRaster = (key, markup, color, sizePx) => {
|
|
4112
|
+
const entry = { kind: "icon", state: "pending", bitmap: null };
|
|
4113
|
+
touch(key, entry);
|
|
4114
|
+
const colored = color ? applySvgColor(markup, color) : markup;
|
|
4115
|
+
const blob = new Blob([colored], { type: "image/svg+xml" });
|
|
4116
|
+
const url = URL.createObjectURL(blob);
|
|
4117
|
+
const img = new Image();
|
|
4118
|
+
img.onload = async () => {
|
|
4119
|
+
URL.revokeObjectURL(url);
|
|
4120
|
+
if (disposed) return;
|
|
4121
|
+
try {
|
|
4122
|
+
const bitmap = await createImageBitmap(img, {
|
|
4123
|
+
resizeWidth: sizePx,
|
|
4124
|
+
resizeHeight: sizePx,
|
|
4125
|
+
resizeQuality: "high"
|
|
4126
|
+
});
|
|
4127
|
+
if (disposed) {
|
|
4128
|
+
bitmap.close?.();
|
|
4129
|
+
return;
|
|
4130
|
+
}
|
|
4131
|
+
entry.state = "ready";
|
|
4132
|
+
entry.bitmap = bitmap;
|
|
4133
|
+
notify();
|
|
4134
|
+
} catch (e) {
|
|
4135
|
+
entry.state = "error";
|
|
4136
|
+
entry.err = e;
|
|
4137
|
+
notify();
|
|
4138
|
+
}
|
|
4139
|
+
};
|
|
4140
|
+
img.onerror = (e) => {
|
|
4141
|
+
URL.revokeObjectURL(url);
|
|
4142
|
+
if (disposed) return;
|
|
4143
|
+
entry.state = "error";
|
|
4144
|
+
entry.err = e;
|
|
4145
|
+
notify();
|
|
4146
|
+
};
|
|
4147
|
+
img.src = url;
|
|
4148
|
+
};
|
|
4149
|
+
return {
|
|
4150
|
+
getImage(src) {
|
|
4151
|
+
const key = `img:${src}`;
|
|
4152
|
+
const existing = entries.get(key);
|
|
4153
|
+
if (existing && existing.kind === "image") {
|
|
4154
|
+
if (existing.state === "ready") {
|
|
4155
|
+
touch(key, existing);
|
|
4156
|
+
return existing.bitmap;
|
|
4157
|
+
}
|
|
4158
|
+
return null;
|
|
4159
|
+
}
|
|
4160
|
+
startImageDecode(key, src);
|
|
4161
|
+
return null;
|
|
4162
|
+
},
|
|
4163
|
+
getIcon(markup, color, devicePixelSize) {
|
|
4164
|
+
const size = bucketSize(Math.max(1, Math.ceil(devicePixelSize)));
|
|
4165
|
+
const key = `icon:${size}:${color ?? ""}:${markup}`;
|
|
4166
|
+
const existing = entries.get(key);
|
|
4167
|
+
if (existing && existing.kind === "icon") {
|
|
4168
|
+
if (existing.state === "ready") {
|
|
4169
|
+
touch(key, existing);
|
|
4170
|
+
return existing.bitmap;
|
|
4171
|
+
}
|
|
4172
|
+
return null;
|
|
4173
|
+
}
|
|
4174
|
+
startIconRaster(key, markup, color, size);
|
|
4175
|
+
return null;
|
|
4176
|
+
},
|
|
4177
|
+
dispose() {
|
|
4178
|
+
disposed = true;
|
|
4179
|
+
for (const entry of entries.values()) {
|
|
4180
|
+
if (entry.kind === "icon" && entry.bitmap) entry.bitmap.close?.();
|
|
4181
|
+
}
|
|
4182
|
+
entries.clear();
|
|
4183
|
+
}
|
|
4184
|
+
};
|
|
4185
|
+
};
|
|
4186
|
+
|
|
4187
|
+
// src/render/assets/paint.ts
|
|
4188
|
+
var PLACEHOLDER_FILL = "#e5e7eb";
|
|
4189
|
+
var PLACEHOLDER_TEXT_FILL = "#94a3b8";
|
|
4190
|
+
var paintPlaceholder = (ctx, w, h, label) => {
|
|
4191
|
+
ctx.fillStyle = PLACEHOLDER_FILL;
|
|
4192
|
+
ctx.fillRect(0, 0, w, h);
|
|
4193
|
+
if (w >= 32 && h >= 16) {
|
|
4194
|
+
ctx.fillStyle = PLACEHOLDER_TEXT_FILL;
|
|
4195
|
+
ctx.font = "11px system-ui, sans-serif";
|
|
4196
|
+
ctx.textAlign = "center";
|
|
4197
|
+
ctx.textBaseline = "middle";
|
|
4198
|
+
ctx.fillText(label, w / 2, h / 2);
|
|
4199
|
+
}
|
|
4200
|
+
};
|
|
4201
|
+
var paintImageNode = (ctx, node, cache4, theme) => {
|
|
4202
|
+
if (node.w <= 0 || node.h <= 0) return;
|
|
4203
|
+
const data = node.data;
|
|
4204
|
+
if (!data?.src) return;
|
|
4205
|
+
const bitmap = cache4.getImage(data.src);
|
|
4206
|
+
const opacity = resolveOpacity(node.style, theme);
|
|
4207
|
+
const needsScope = opacity !== 1;
|
|
4208
|
+
if (needsScope) {
|
|
4209
|
+
ctx.save();
|
|
4210
|
+
ctx.globalAlpha = opacity;
|
|
4211
|
+
}
|
|
4212
|
+
if (bitmap?.complete) {
|
|
4213
|
+
ctx.drawImage(bitmap, 0, 0, node.w, node.h);
|
|
4214
|
+
} else {
|
|
4215
|
+
paintPlaceholder(ctx, node.w, node.h, "loading\u2026");
|
|
4216
|
+
}
|
|
4217
|
+
if (needsScope) ctx.restore();
|
|
4218
|
+
};
|
|
4219
|
+
var paintIconNode = (ctx, node, cache4, scale, theme) => {
|
|
4220
|
+
if (node.w <= 0 || node.h <= 0) return;
|
|
4221
|
+
const data = node.data;
|
|
4222
|
+
if (!data?.src) return;
|
|
4223
|
+
const sizePx = Math.max(node.w, node.h) * scale;
|
|
4224
|
+
const color = node.style?.iconColor;
|
|
4225
|
+
const bitmap = cache4.getIcon(data.src, color, sizePx);
|
|
4226
|
+
const opacity = resolveOpacity(node.style, theme);
|
|
4227
|
+
const needsScope = opacity !== 1;
|
|
4228
|
+
if (needsScope) {
|
|
4229
|
+
ctx.save();
|
|
4230
|
+
ctx.globalAlpha = opacity;
|
|
4231
|
+
}
|
|
4232
|
+
if (bitmap) {
|
|
4233
|
+
ctx.drawImage(bitmap, 0, 0, node.w, node.h);
|
|
4234
|
+
} else {
|
|
4235
|
+
paintPlaceholder(ctx, node.w, node.h, "svg\u2026");
|
|
4236
|
+
}
|
|
4237
|
+
if (needsScope) ctx.restore();
|
|
4238
|
+
};
|
|
4239
|
+
|
|
3801
4240
|
// src/render/background.ts
|
|
3802
4241
|
var MIN_PATTERN_SCREEN_PX = 8;
|
|
3803
4242
|
var MIN_VISIBLE_PATTERN_PX = 2;
|
|
@@ -3939,14 +4378,14 @@ var hitTestRotateHandle = (node, worldPoint, cameraZ) => {
|
|
|
3939
4378
|
};
|
|
3940
4379
|
|
|
3941
4380
|
// src/render/overlay.ts
|
|
3942
|
-
var
|
|
4381
|
+
var DEFAULT_SELECTION_COLOR = "#3b82f6";
|
|
3943
4382
|
var SELECTION_OUTLINE_PX = 1.5;
|
|
3944
|
-
var
|
|
4383
|
+
var MARQUEE_FILL_ALPHA = 0.08;
|
|
3945
4384
|
var MARQUEE_STROKE_PX = 1;
|
|
3946
|
-
var drawSelectionOutline = (ctx, node, scale) => {
|
|
4385
|
+
var drawSelectionOutline = (ctx, node, scale, color) => {
|
|
3947
4386
|
if (node.angle === 0) {
|
|
3948
4387
|
ctx.save();
|
|
3949
|
-
ctx.strokeStyle =
|
|
4388
|
+
ctx.strokeStyle = color;
|
|
3950
4389
|
ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
|
|
3951
4390
|
ctx.beginPath();
|
|
3952
4391
|
ctx.rect(node.x, node.y, node.w, node.h);
|
|
@@ -3965,7 +4404,7 @@ var drawSelectionOutline = (ctx, node, scale) => {
|
|
|
3965
4404
|
{ x: -node.w / 2, y: node.h / 2 }
|
|
3966
4405
|
].map((p) => ({ x: cx + p.x * cos - p.y * sin, y: cy + p.x * sin + p.y * cos }));
|
|
3967
4406
|
ctx.save();
|
|
3968
|
-
ctx.strokeStyle =
|
|
4407
|
+
ctx.strokeStyle = color;
|
|
3969
4408
|
ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
|
|
3970
4409
|
ctx.beginPath();
|
|
3971
4410
|
const first = corners[0];
|
|
@@ -3978,13 +4417,13 @@ var drawSelectionOutline = (ctx, node, scale) => {
|
|
|
3978
4417
|
ctx.stroke();
|
|
3979
4418
|
ctx.restore();
|
|
3980
4419
|
};
|
|
3981
|
-
var drawResizeHandles = (ctx, node, scale) => {
|
|
4420
|
+
var drawResizeHandles = (ctx, node, scale, color) => {
|
|
3982
4421
|
const halfPx = RESIZE_HANDLE_SIZE_PX / 2;
|
|
3983
4422
|
const halfWorld = halfPx / scale;
|
|
3984
4423
|
const positions = handleWorldPositions(node);
|
|
3985
4424
|
ctx.save();
|
|
3986
4425
|
ctx.fillStyle = "#fff";
|
|
3987
|
-
ctx.strokeStyle =
|
|
4426
|
+
ctx.strokeStyle = color;
|
|
3988
4427
|
ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
|
|
3989
4428
|
for (const key of Object.keys(positions)) {
|
|
3990
4429
|
const p = positions[key];
|
|
@@ -3995,7 +4434,7 @@ var drawResizeHandles = (ctx, node, scale) => {
|
|
|
3995
4434
|
}
|
|
3996
4435
|
ctx.restore();
|
|
3997
4436
|
};
|
|
3998
|
-
var drawRotateHandle = (ctx, node, scale, cameraZ) => {
|
|
4437
|
+
var drawRotateHandle = (ctx, node, scale, cameraZ, color) => {
|
|
3999
4438
|
const center = rotateHandleWorldPosition(node, cameraZ);
|
|
4000
4439
|
const radiusWorld = ROTATE_HANDLE_RADIUS_PX / scale;
|
|
4001
4440
|
const cx = node.x + node.w / 2;
|
|
@@ -4008,7 +4447,7 @@ var drawRotateHandle = (ctx, node, scale, cameraZ) => {
|
|
|
4008
4447
|
y: cy + 0 * sin + topMidLocalY * cos
|
|
4009
4448
|
};
|
|
4010
4449
|
ctx.save();
|
|
4011
|
-
ctx.strokeStyle =
|
|
4450
|
+
ctx.strokeStyle = color;
|
|
4012
4451
|
ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
|
|
4013
4452
|
ctx.beginPath();
|
|
4014
4453
|
ctx.moveTo(topMidWorld.x, topMidWorld.y);
|
|
@@ -4021,12 +4460,12 @@ var drawRotateHandle = (ctx, node, scale, cameraZ) => {
|
|
|
4021
4460
|
ctx.stroke();
|
|
4022
4461
|
ctx.restore();
|
|
4023
4462
|
};
|
|
4024
|
-
var drawEdgeMidpointHandle = (ctx, midpoint, scale) => {
|
|
4463
|
+
var drawEdgeMidpointHandle = (ctx, midpoint, scale, color) => {
|
|
4025
4464
|
const radiusPx = 5;
|
|
4026
4465
|
const radiusWorld = radiusPx / scale;
|
|
4027
4466
|
ctx.save();
|
|
4028
4467
|
ctx.fillStyle = "#fff";
|
|
4029
|
-
ctx.strokeStyle =
|
|
4468
|
+
ctx.strokeStyle = color;
|
|
4030
4469
|
ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
|
|
4031
4470
|
ctx.beginPath();
|
|
4032
4471
|
ctx.arc(midpoint.x, midpoint.y, radiusWorld, 0, Math.PI * 2);
|
|
@@ -4034,12 +4473,12 @@ var drawEdgeMidpointHandle = (ctx, midpoint, scale) => {
|
|
|
4034
4473
|
ctx.stroke();
|
|
4035
4474
|
ctx.restore();
|
|
4036
4475
|
};
|
|
4037
|
-
var drawEdgeEndpointHandles = (ctx, source, target, scale) => {
|
|
4476
|
+
var drawEdgeEndpointHandles = (ctx, source, target, scale, color) => {
|
|
4038
4477
|
const radiusPx = 5;
|
|
4039
4478
|
const radiusWorld = radiusPx / scale;
|
|
4040
4479
|
ctx.save();
|
|
4041
4480
|
ctx.fillStyle = "#fff";
|
|
4042
|
-
ctx.strokeStyle =
|
|
4481
|
+
ctx.strokeStyle = color;
|
|
4043
4482
|
ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
|
|
4044
4483
|
for (const p of [source, target]) {
|
|
4045
4484
|
ctx.beginPath();
|
|
@@ -4049,17 +4488,53 @@ var drawEdgeEndpointHandles = (ctx, source, target, scale) => {
|
|
|
4049
4488
|
}
|
|
4050
4489
|
ctx.restore();
|
|
4051
4490
|
};
|
|
4052
|
-
var drawMarquee = (ctx, rect, scale) => {
|
|
4491
|
+
var drawMarquee = (ctx, rect, scale, color) => {
|
|
4053
4492
|
ctx.save();
|
|
4054
|
-
ctx.
|
|
4493
|
+
ctx.globalAlpha = MARQUEE_FILL_ALPHA;
|
|
4494
|
+
ctx.fillStyle = color;
|
|
4055
4495
|
ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
|
|
4056
|
-
ctx.
|
|
4496
|
+
ctx.globalAlpha = 1;
|
|
4497
|
+
ctx.strokeStyle = color;
|
|
4057
4498
|
ctx.lineWidth = MARQUEE_STROKE_PX / scale;
|
|
4058
4499
|
ctx.setLineDash([4 / scale, 3 / scale]);
|
|
4059
4500
|
ctx.strokeRect(rect.x, rect.y, rect.w, rect.h);
|
|
4060
4501
|
ctx.restore();
|
|
4061
4502
|
};
|
|
4062
4503
|
|
|
4504
|
+
// src/render/paint-frame.ts
|
|
4505
|
+
var FRAME_BORDER_PX = 1.5;
|
|
4506
|
+
var FRAME_BORDER_COLOR_DEFAULT = "#94a3b8";
|
|
4507
|
+
var FRAME_FILL_DEFAULT = "rgba(148, 163, 184, 0.06)";
|
|
4508
|
+
var FRAME_LABEL_FONT_PX = 12;
|
|
4509
|
+
var FRAME_LABEL_GAP_PX = 6;
|
|
4510
|
+
var FRAME_LABEL_COLOR = "#64748b";
|
|
4511
|
+
var paintFrameNode = (ctx, node, scale, theme) => {
|
|
4512
|
+
if (node.w <= 0 || node.h <= 0) return;
|
|
4513
|
+
const opacity = resolveOpacity(node.style, theme);
|
|
4514
|
+
const needsScope = opacity !== 1;
|
|
4515
|
+
if (needsScope) {
|
|
4516
|
+
ctx.save();
|
|
4517
|
+
ctx.globalAlpha = opacity;
|
|
4518
|
+
}
|
|
4519
|
+
const fill = node.style?.backgroundColor ?? (theme ? theme("frame.background") : void 0) ?? FRAME_FILL_DEFAULT;
|
|
4520
|
+
ctx.fillStyle = fill;
|
|
4521
|
+
ctx.fillRect(0, 0, node.w, node.h);
|
|
4522
|
+
const stroke = resolveColor(node.style, "strokeColor", FRAME_BORDER_COLOR_DEFAULT, theme);
|
|
4523
|
+
ctx.strokeStyle = stroke;
|
|
4524
|
+
ctx.lineWidth = FRAME_BORDER_PX / scale;
|
|
4525
|
+
ctx.setLineDash([]);
|
|
4526
|
+
ctx.strokeRect(0, 0, node.w, node.h);
|
|
4527
|
+
const labelPx = FRAME_LABEL_FONT_PX / scale;
|
|
4528
|
+
const gapPx = FRAME_LABEL_GAP_PX / scale;
|
|
4529
|
+
const label = node.content?.trim() || "Frame";
|
|
4530
|
+
ctx.fillStyle = FRAME_LABEL_COLOR;
|
|
4531
|
+
ctx.textBaseline = "bottom";
|
|
4532
|
+
ctx.textAlign = "left";
|
|
4533
|
+
ctx.font = `500 ${labelPx}px system-ui, -apple-system, sans-serif`;
|
|
4534
|
+
ctx.fillText(label, 0, -gapPx);
|
|
4535
|
+
if (needsScope) ctx.restore();
|
|
4536
|
+
};
|
|
4537
|
+
|
|
4063
4538
|
// src/render/shapes/content-bounds.ts
|
|
4064
4539
|
var SQRT2_INV = 1 / Math.SQRT2;
|
|
4065
4540
|
var contentBounds = (node) => {
|
|
@@ -4132,12 +4607,19 @@ var createRenderer = (opts) => {
|
|
|
4132
4607
|
const staticSurface = setupSurface(opts.staticCanvas);
|
|
4133
4608
|
const interactiveSurface = setupSurface(opts.interactiveCanvas);
|
|
4134
4609
|
let background = opts.background;
|
|
4610
|
+
let selectionColor = opts.selectionColor ?? DEFAULT_SELECTION_COLOR;
|
|
4611
|
+
let hideFrames = false;
|
|
4135
4612
|
sizeSurface(staticSurface, opts.width, opts.height);
|
|
4136
4613
|
sizeSurface(interactiveSurface, opts.width, opts.height);
|
|
4137
4614
|
let staticDirty = true;
|
|
4138
4615
|
let interactiveDirty = false;
|
|
4139
4616
|
let overlaySet = /* @__PURE__ */ new Set();
|
|
4140
4617
|
let lastDrawn = 0;
|
|
4618
|
+
const requestRepaint = () => {
|
|
4619
|
+
staticDirty = true;
|
|
4620
|
+
loop.requestFrame();
|
|
4621
|
+
};
|
|
4622
|
+
const assetCache = createAssetCache({ onReady: requestRepaint });
|
|
4141
4623
|
const isInteractive = (state) => state.mode !== "idle" || store.getSelection().length > 0;
|
|
4142
4624
|
const drawFrame = () => {
|
|
4143
4625
|
if (staticDirty) {
|
|
@@ -4176,7 +4658,18 @@ var createRenderer = (opts) => {
|
|
|
4176
4658
|
const cameraIsMoving = interaction.mode === "panning" || interaction.mode === "zooming";
|
|
4177
4659
|
const movingNodeCount = excludedNodes?.size ?? 0;
|
|
4178
4660
|
const roughEnabled = !cameraIsMoving && movingNodeCount <= ROUGH_MAX_MOVING_NODES && camera.z >= ROUGH_MIN_ZOOM && visible.length <= ROUGH_MAX_NODES;
|
|
4661
|
+
if (!hideFrames) {
|
|
4662
|
+
for (const node of visible) {
|
|
4663
|
+
if (node.type !== "frame") continue;
|
|
4664
|
+
if (excludedNodes?.has(node.id)) continue;
|
|
4665
|
+
drawWithNodeTransform(staticSurface.ctx, node, () => {
|
|
4666
|
+
paintFrameNode(staticSurface.ctx, node, scale, theme);
|
|
4667
|
+
});
|
|
4668
|
+
drawn++;
|
|
4669
|
+
}
|
|
4670
|
+
}
|
|
4179
4671
|
for (const node of visible) {
|
|
4672
|
+
if (node.type === "frame") continue;
|
|
4180
4673
|
if (excludedNodes?.has(node.id)) continue;
|
|
4181
4674
|
const isEditingThis = editingNodeId === node.id;
|
|
4182
4675
|
if (isDrawablePrimitive(node.type)) {
|
|
@@ -4207,6 +4700,20 @@ var createRenderer = (opts) => {
|
|
|
4207
4700
|
drawn++;
|
|
4208
4701
|
continue;
|
|
4209
4702
|
}
|
|
4703
|
+
if (node.type === "image") {
|
|
4704
|
+
drawWithNodeTransform(staticSurface.ctx, node, () => {
|
|
4705
|
+
paintImageNode(staticSurface.ctx, node, assetCache, theme);
|
|
4706
|
+
});
|
|
4707
|
+
drawn++;
|
|
4708
|
+
continue;
|
|
4709
|
+
}
|
|
4710
|
+
if (node.type === "icon") {
|
|
4711
|
+
drawWithNodeTransform(staticSurface.ctx, node, () => {
|
|
4712
|
+
paintIconNode(staticSurface.ctx, node, assetCache, scale, theme);
|
|
4713
|
+
});
|
|
4714
|
+
drawn++;
|
|
4715
|
+
continue;
|
|
4716
|
+
}
|
|
4210
4717
|
if (node.type === "text") {
|
|
4211
4718
|
drawWithNodeTransform(staticSurface.ctx, node, () => {
|
|
4212
4719
|
if (isEditingThis) return;
|
|
@@ -4365,8 +4872,21 @@ var createRenderer = (opts) => {
|
|
|
4365
4872
|
isMoving: true};
|
|
4366
4873
|
const dragRoughEnabled = inDragMap.size <= ROUGH_MAX_MOVING_NODES && camera.z >= ROUGH_MIN_ZOOM;
|
|
4367
4874
|
for (const node of inDragMap.values()) {
|
|
4368
|
-
if (!isDrawablePrimitive(node.type) && node.type !== "text")
|
|
4875
|
+
if (!isDrawablePrimitive(node.type) && node.type !== "text" && node.type !== "image" && node.type !== "icon" && node.type !== "frame")
|
|
4876
|
+
continue;
|
|
4369
4877
|
drawWithNodeTransform(ctx, node, () => {
|
|
4878
|
+
if (node.type === "frame") {
|
|
4879
|
+
paintFrameNode(ctx, node, scale, theme);
|
|
4880
|
+
return;
|
|
4881
|
+
}
|
|
4882
|
+
if (node.type === "image") {
|
|
4883
|
+
paintImageNode(ctx, node, assetCache, theme);
|
|
4884
|
+
return;
|
|
4885
|
+
}
|
|
4886
|
+
if (node.type === "icon") {
|
|
4887
|
+
paintIconNode(ctx, node, assetCache, scale, theme);
|
|
4888
|
+
return;
|
|
4889
|
+
}
|
|
4370
4890
|
if (isDrawablePrimitive(node.type)) {
|
|
4371
4891
|
const useRough = dragRoughEnabled && (node.style?.roughness ?? 0) > 0;
|
|
4372
4892
|
const roughReady = useRough ? getRoughCanvasCtor() !== null : false;
|
|
@@ -4418,32 +4938,32 @@ var createRenderer = (opts) => {
|
|
|
4418
4938
|
for (const id of selectedNodeIds) {
|
|
4419
4939
|
const node = inDragMap.get(id) ?? store.getNode(id);
|
|
4420
4940
|
if (!node) continue;
|
|
4421
|
-
drawSelectionOutline(ctx, node, scale);
|
|
4941
|
+
drawSelectionOutline(ctx, node, scale, selectionColor);
|
|
4422
4942
|
}
|
|
4423
4943
|
if (interaction.mode !== "dragging" && selectedNodeIds.length === 1) {
|
|
4424
4944
|
const node = inDragMap.get(selectedNodeIds[0]) ?? store.getNode(selectedNodeIds[0]);
|
|
4425
4945
|
if (node) {
|
|
4426
|
-
drawResizeHandles(ctx, node, scale);
|
|
4427
|
-
drawRotateHandle(ctx, node, scale, camera.z);
|
|
4946
|
+
drawResizeHandles(ctx, node, scale, selectionColor);
|
|
4947
|
+
drawRotateHandle(ctx, node, scale, camera.z, selectionColor);
|
|
4428
4948
|
}
|
|
4429
4949
|
}
|
|
4430
4950
|
}
|
|
4431
4951
|
for (const id of selectedEdgeIds) {
|
|
4432
4952
|
const geom = store.getEdgeGeometry(id);
|
|
4433
4953
|
if (geom) {
|
|
4434
|
-
drawEdgeEndpointHandles(ctx, geom.source, geom.target, scale);
|
|
4954
|
+
drawEdgeEndpointHandles(ctx, geom.source, geom.target, scale, selectionColor);
|
|
4435
4955
|
const edge = store.getEdge(id);
|
|
4436
4956
|
if (edge && edge.pathStyle === "bezier") {
|
|
4437
4957
|
const mid = getPointAndTangentAtArcLength(geom.samples, 0.5).point;
|
|
4438
|
-
drawEdgeMidpointHandle(ctx, mid, scale);
|
|
4958
|
+
drawEdgeMidpointHandle(ctx, mid, scale, selectionColor);
|
|
4439
4959
|
}
|
|
4440
4960
|
}
|
|
4441
4961
|
}
|
|
4442
4962
|
if (interaction.mode === "marqueeing" && interaction.marqueeRect) {
|
|
4443
|
-
drawMarquee(ctx, interaction.marqueeRect, scale);
|
|
4963
|
+
drawMarquee(ctx, interaction.marqueeRect, scale, selectionColor);
|
|
4444
4964
|
}
|
|
4445
4965
|
if (interaction.mode === "creating-shape" && interaction.createDraftRect) {
|
|
4446
|
-
drawMarquee(ctx, interaction.createDraftRect, scale);
|
|
4966
|
+
drawMarquee(ctx, interaction.createDraftRect, scale, selectionColor);
|
|
4447
4967
|
}
|
|
4448
4968
|
if ((interaction.mode === "creating-edge" || interaction.mode === "reconnecting-edge") && interaction.draftEdge) {
|
|
4449
4969
|
const draft = {
|
|
@@ -4451,7 +4971,7 @@ var createRenderer = (opts) => {
|
|
|
4451
4971
|
source: interaction.draftEdge.source,
|
|
4452
4972
|
target: interaction.draftEdge.target,
|
|
4453
4973
|
pathStyle: "bezier",
|
|
4454
|
-
style: { strokeColor:
|
|
4974
|
+
style: { strokeColor: selectionColor }
|
|
4455
4975
|
};
|
|
4456
4976
|
const geom = computeEdgeGeometry(draft, (id) => store.getNode(id));
|
|
4457
4977
|
if (geom) {
|
|
@@ -4555,6 +5075,16 @@ var createRenderer = (opts) => {
|
|
|
4555
5075
|
staticDirty = true;
|
|
4556
5076
|
loop.requestFrame();
|
|
4557
5077
|
},
|
|
5078
|
+
setSelectionColor(color) {
|
|
5079
|
+
selectionColor = color;
|
|
5080
|
+
interactiveDirty = true;
|
|
5081
|
+
loop.requestFrame();
|
|
5082
|
+
},
|
|
5083
|
+
setHideFrames(hidden) {
|
|
5084
|
+
hideFrames = hidden;
|
|
5085
|
+
staticDirty = true;
|
|
5086
|
+
loop.requestFrame();
|
|
5087
|
+
},
|
|
4558
5088
|
stats: () => loop.stats(),
|
|
4559
5089
|
lastDrawCount: () => lastDrawn,
|
|
4560
5090
|
getOverlaySet: () => [...overlaySet],
|
|
@@ -4565,6 +5095,7 @@ var createRenderer = (opts) => {
|
|
|
4565
5095
|
unsubSelection();
|
|
4566
5096
|
unsubInteraction();
|
|
4567
5097
|
unsubFontEpoch();
|
|
5098
|
+
assetCache.dispose();
|
|
4568
5099
|
}
|
|
4569
5100
|
};
|
|
4570
5101
|
};
|
|
@@ -4584,6 +5115,7 @@ var sceneBounds = (store) => {
|
|
|
4584
5115
|
let maxY = Number.NEGATIVE_INFINITY;
|
|
4585
5116
|
for (const n of nodes) {
|
|
4586
5117
|
if (n.hidden) continue;
|
|
5118
|
+
if (n.type === "frame") continue;
|
|
4587
5119
|
const r = nodeAABB(n);
|
|
4588
5120
|
if (r.x < minX) minX = r.x;
|
|
4589
5121
|
if (r.y < minY) minY = r.y;
|
|
@@ -4614,6 +5146,7 @@ var renderMinimapContent = (ctx, store, mapWidth, mapHeight, opts = {}) => {
|
|
|
4614
5146
|
const defaultColor = opts.defaultNodeColor ?? "#94a3b8";
|
|
4615
5147
|
for (const node of store.getAllNodes()) {
|
|
4616
5148
|
if (node.hidden) continue;
|
|
5149
|
+
if (node.type === "frame") continue;
|
|
4617
5150
|
const r = nodeAABB(node);
|
|
4618
5151
|
const x = offX + (r.x - bx) * scale;
|
|
4619
5152
|
const y = offY + (r.y - by) * scale;
|
|
@@ -5594,6 +6127,6 @@ var installedExtensions = (store) => {
|
|
|
5594
6127
|
// src/index.ts
|
|
5595
6128
|
var VERSION = "0.0.0";
|
|
5596
6129
|
|
|
5597
|
-
export { BEZIER_SEGMENTS, CODE_BG_COLOR, CODE_BLOCK_MARGIN_Y, CODE_BLOCK_PADDING_X, CONTENT_HEIGHT_BUFFER, CONTENT_PADDING, DEFAULT_BACKGROUND, DEFAULT_CAMERA, DEFAULT_HIGHLIGHT_COLOR, DEFAULT_HIGHLIGHT_COLOR_DARK, DEFAULT_MINIMAP_MAX_NODES, DEFAULT_STYLE, DEFAULT_TEXT_COLOR, EDGE_HANDLE_SLOP_PX, EDGE_HIT_SLOP_PX, EdgeGeometryCache, FONT_FAMILY_MAP, FONT_SIZE_MAP, LINE_HEIGHT_MAP, LINK_COLOR, MAX_ZOOM, MIN_ZOOM, PALM_REJECTION_GRACE_MS, RESIZE_HANDLES, RESIZE_HANDLE_SIZE_PX, ROTATE_HANDLE_OFFSET_PX, ROTATE_HANDLE_RADIUS_PX, SCHEMA_VERSION, UniformGrid, VERSION, applyCameraTransform, arrowheadLength, asBatchId, asClientId, asEdgeId, asGroupId, asNodeId, attachSync, autoRouteControls, clampEffectiveScale, clampZoom, clearMeasureCache, clearSurface, clearTextBitmapCache, clipSamples, computeAutoFitHeight, computeEdgeGeometry, copy, createCanvasStore, createDefaultTextareaEditor, createFrameLoop, createPalmRejectionState, createRenderer, cubicBezier, cubicBezierTangent, cut, defineExtension, defineNode, deserializeClipboard, detectConflicts, drawArrowhead, drawEdge, drawMinimapViewport, drawShape, drawTextToCanvas, drawWithNodeTransform, edgeAABBFromSamples, edgeLabelBoundsWorld, emptyPresenceState, estimateMarkdownContentHeight, exportSelection, exportSelectionSvg, exportViewport, fromSerialized, fullVisibleClipResult, getCanvasFont, getContentHeight, getContext, getDpr, getFontEpoch, getMarkdownLineHeightPx, getOrRenderTextBitmap, getPointAndTangentAtArcLength, getTextBitmapCacheSize, handleEnter, handleWorldPositions, hitTestAny, hitTestEdge, hitTestHandles, hitTestPoint, hitTestRotateHandle, idleInteractionState, inflateRect, insertLink, installExtension, installedExtensions, inverseBatch, inverseOp, isAttached, isCanvasHarnessClipboard, isDrawablePrimitive, isMoving, isNodeRemoteEditing, layoutTokens, makeIdGenerator, marqueeNodes, measureText, midpointToCubicControls, minimapScreenToWorld, nodeAABB, nodeIntersectsRect, nodeLocalToWorld, notePenActive, notePenInactive, opSchemas, opSchemasAsAnthropicTools, paintBackground, panByScreen, paste, pointInNode, projectEndToWorld, projectToNodeBoundary, quantizeDpr, quantizeZoom, randomClientId, rectContainsPoint, rectFromPoints, rectsIntersect, registerMigrator, renderMinimapContent, resolveColor, resolveOpacity, resolveRenderScale, resolveStrokeWidth, rotateHandleWorldPosition, rotateVecByAngle, sampleBezier, sampleSelfLoop, samplesFor, sceneBounds, screenToWorld, selfLoopGeometry, serializeSelection, setupSurface, shouldAutoFit, shouldRejectTouch, sideNormalLocal, sideOf, sizeSurface, storeToJSON, subscribeFontEpoch, tangentAtArcLength, toSerialized, toggleBold, toggleCode, toggleItalic, toggleStrike, toggleUnderline, tokenize, unionRects, viewportWorldRect, withAutoFitHeight, worldToNodeLocal, worldToScreen, worldViewport, worldViewportFromCamera, zoomAtScreenPoint };
|
|
6130
|
+
export { BEZIER_SEGMENTS, CODE_BG_COLOR, CODE_BLOCK_MARGIN_Y, CODE_BLOCK_PADDING_X, CONTENT_HEIGHT_BUFFER, CONTENT_PADDING, DEFAULT_BACKGROUND, DEFAULT_CAMERA, DEFAULT_HIGHLIGHT_COLOR, DEFAULT_HIGHLIGHT_COLOR_DARK, DEFAULT_MINIMAP_MAX_NODES, DEFAULT_STYLE, DEFAULT_TEXT_COLOR, EDGE_HANDLE_SLOP_PX, EDGE_HIT_SLOP_PX, EdgeGeometryCache, FONT_FAMILY_MAP, FONT_SIZE_MAP, LINE_HEIGHT_MAP, LINK_COLOR, MAX_IMAGE_BYTES, MAX_SVG_BYTES, MAX_ZOOM, MIN_ZOOM, PALM_REJECTION_GRACE_MS, RESIZE_HANDLES, RESIZE_HANDLE_SIZE_PX, ROTATE_HANDLE_OFFSET_PX, ROTATE_HANDLE_RADIUS_PX, SCHEMA_VERSION, UniformGrid, VERSION, applyCameraTransform, applySvgColor, arrowheadLength, asBatchId, asClientId, asEdgeId, asGroupId, asNodeId, attachSync, autoRouteControls, blobToDataUri, clampEffectiveScale, clampZoom, clearMeasureCache, clearSurface, clearTextBitmapCache, clipSamples, computeAutoFitHeight, computeEdgeGeometry, copy, createCanvasStore, createDefaultTextareaEditor, createFrameLoop, createPalmRejectionState, createRenderer, cubicBezier, cubicBezierTangent, cut, defineExtension, defineNode, deserializeClipboard, detectConflicts, downscaleImageBlob, drawArrowhead, drawEdge, drawMinimapViewport, drawShape, drawTextToCanvas, drawWithNodeTransform, edgeAABBFromSamples, edgeLabelBoundsWorld, emptyPresenceState, estimateMarkdownContentHeight, exportSelection, exportSelectionSvg, exportViewport, extractSvgDimensions, fromSerialized, fullVisibleClipResult, getCanvasFont, getContentHeight, getContext, getDpr, getFontEpoch, getMarkdownLineHeightPx, getOrRenderTextBitmap, getPointAndTangentAtArcLength, getTextBitmapCacheSize, handleEnter, handleWorldPositions, hitTestAny, hitTestEdge, hitTestHandles, hitTestPoint, hitTestRotateHandle, idleInteractionState, inflateRect, insertLink, installExtension, installedExtensions, inverseBatch, inverseOp, isAttached, isCanvasHarnessClipboard, isDrawablePrimitive, isMoving, isNodeRemoteEditing, layoutTokens, makeIdGenerator, marqueeNodes, measureText, midpointToCubicControls, minimapScreenToWorld, nodeAABB, nodeIntersectsRect, nodeLocalToWorld, notePenActive, notePenInactive, opSchemas, opSchemasAsAnthropicTools, paintBackground, panByScreen, paste, pointInNode, projectEndToWorld, projectToNodeBoundary, quantizeDpr, quantizeZoom, randomClientId, rectContainsPoint, rectFromPoints, rectsIntersect, registerMigrator, renderMinimapContent, resolveColor, resolveOpacity, resolveRenderScale, resolveStrokeWidth, rotateHandleWorldPosition, rotateVecByAngle, sampleBezier, sampleSelfLoop, samplesFor, sanitizeSvg, sceneBounds, screenToWorld, selfLoopGeometry, serializeSelection, setupSurface, shouldAutoFit, shouldRejectTouch, sideNormalLocal, sideOf, sizeSurface, storeToJSON, subscribeFontEpoch, tangentAtArcLength, toImageBlob, toSerialized, toggleBold, toggleCode, toggleItalic, toggleStrike, toggleUnderline, tokenize, unionRects, validateImageInput, validateSvgMarkup, viewportWorldRect, withAutoFitHeight, worldToNodeLocal, worldToScreen, worldViewport, worldViewportFromCamera, zoomAtScreenPoint };
|
|
5598
6131
|
//# sourceMappingURL=index.js.map
|
|
5599
6132
|
//# sourceMappingURL=index.js.map
|