@canvas-harness/core 0.0.0 → 0.0.2
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/LICENSE +21 -0
- package/dist/index.cjs +446 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +194 -2
- package/dist/index.d.ts +194 -2
- package/dist/index.js +437 -26
- package/dist/index.js.map +1 -1
- package/package.json +13 -10
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 = [];
|
|
@@ -3326,6 +3475,55 @@ var createCanvasStore = (opts = {}) => {
|
|
|
3326
3475
|
if (batch) emitChange(batch);
|
|
3327
3476
|
});
|
|
3328
3477
|
},
|
|
3478
|
+
async addImage(opts2) {
|
|
3479
|
+
validateImageInput(opts2.src);
|
|
3480
|
+
const rawBlob = await toImageBlob(opts2.src);
|
|
3481
|
+
const maxDim = opts2.maxDimension ?? 2048;
|
|
3482
|
+
const { blob, naturalW, naturalH } = await downscaleImageBlob(rawBlob, maxDim);
|
|
3483
|
+
const src = await blobToDataUri(blob);
|
|
3484
|
+
const DEFAULT_MAX_NODE_SIDE = 400;
|
|
3485
|
+
const aspectScale = Math.min(1, DEFAULT_MAX_NODE_SIDE / Math.max(naturalW, naturalH));
|
|
3486
|
+
const w = opts2.w ?? Math.max(1, Math.round(naturalW * aspectScale));
|
|
3487
|
+
const h = opts2.h ?? Math.max(1, Math.round(naturalH * aspectScale));
|
|
3488
|
+
const id = asNodeId(idGenerator());
|
|
3489
|
+
this.addNode({
|
|
3490
|
+
id,
|
|
3491
|
+
type: "image",
|
|
3492
|
+
x: opts2.x,
|
|
3493
|
+
y: opts2.y,
|
|
3494
|
+
w,
|
|
3495
|
+
h,
|
|
3496
|
+
angle: 0,
|
|
3497
|
+
z: 0,
|
|
3498
|
+
groups: [],
|
|
3499
|
+
style: opts2.style,
|
|
3500
|
+
data: { src, naturalW, naturalH, alt: opts2.alt }
|
|
3501
|
+
});
|
|
3502
|
+
return id;
|
|
3503
|
+
},
|
|
3504
|
+
async addSvg(opts2) {
|
|
3505
|
+
validateSvgMarkup(opts2.src);
|
|
3506
|
+
const sanitized = sanitizeSvg(opts2.src);
|
|
3507
|
+
const intrinsic = extractSvgDimensions(sanitized);
|
|
3508
|
+
const w = opts2.w ?? intrinsic.w;
|
|
3509
|
+
const h = opts2.h ?? intrinsic.h;
|
|
3510
|
+
const mergedStyle = opts2.color || opts2.style ? { ...opts2.color ? { iconColor: opts2.color } : {}, ...opts2.style } : void 0;
|
|
3511
|
+
const id = asNodeId(idGenerator());
|
|
3512
|
+
this.addNode({
|
|
3513
|
+
id,
|
|
3514
|
+
type: "icon",
|
|
3515
|
+
x: opts2.x,
|
|
3516
|
+
y: opts2.y,
|
|
3517
|
+
w,
|
|
3518
|
+
h,
|
|
3519
|
+
angle: 0,
|
|
3520
|
+
z: 0,
|
|
3521
|
+
groups: [],
|
|
3522
|
+
...mergedStyle ? { style: mergedStyle } : {},
|
|
3523
|
+
data: { src: sanitized, alt: opts2.alt }
|
|
3524
|
+
});
|
|
3525
|
+
return id;
|
|
3526
|
+
},
|
|
3329
3527
|
addEdge(edge) {
|
|
3330
3528
|
const withZ = edge.z === 0 ? { ...edge, z: ++topZ } : edge;
|
|
3331
3529
|
if (withZ.z > topZ) topZ = withZ.z;
|
|
@@ -3798,6 +3996,182 @@ var createFrameLoop = ({ draw, historySize = 60 }) => {
|
|
|
3798
3996
|
};
|
|
3799
3997
|
};
|
|
3800
3998
|
|
|
3999
|
+
// src/render/assets/cache.ts
|
|
4000
|
+
var MAX_ENTRIES2 = 256;
|
|
4001
|
+
var bucketSize = (px) => {
|
|
4002
|
+
if (px <= 32) return 32;
|
|
4003
|
+
if (px <= 64) return 64;
|
|
4004
|
+
if (px <= 128) return 128;
|
|
4005
|
+
if (px <= 256) return 256;
|
|
4006
|
+
if (px <= 512) return 512;
|
|
4007
|
+
return Math.ceil(px / 256) * 256;
|
|
4008
|
+
};
|
|
4009
|
+
var createAssetCache = (opts = {}) => {
|
|
4010
|
+
const entries = /* @__PURE__ */ new Map();
|
|
4011
|
+
let disposed = false;
|
|
4012
|
+
const notify = () => {
|
|
4013
|
+
if (disposed) return;
|
|
4014
|
+
opts.onReady?.();
|
|
4015
|
+
};
|
|
4016
|
+
const touch = (key, entry) => {
|
|
4017
|
+
entries.delete(key);
|
|
4018
|
+
entries.set(key, entry);
|
|
4019
|
+
if (entries.size > MAX_ENTRIES2) {
|
|
4020
|
+
const oldestKey = entries.keys().next().value;
|
|
4021
|
+
if (oldestKey !== void 0) {
|
|
4022
|
+
const evicted = entries.get(oldestKey);
|
|
4023
|
+
if (evicted?.kind === "icon" && evicted.bitmap) evicted.bitmap.close?.();
|
|
4024
|
+
entries.delete(oldestKey);
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
};
|
|
4028
|
+
const startImageDecode = (key, src) => {
|
|
4029
|
+
const entry = { kind: "image", state: "pending", bitmap: null };
|
|
4030
|
+
touch(key, entry);
|
|
4031
|
+
const img = new Image();
|
|
4032
|
+
img.onload = () => {
|
|
4033
|
+
if (disposed) return;
|
|
4034
|
+
entry.state = "ready";
|
|
4035
|
+
entry.bitmap = img;
|
|
4036
|
+
notify();
|
|
4037
|
+
};
|
|
4038
|
+
img.onerror = (e) => {
|
|
4039
|
+
if (disposed) return;
|
|
4040
|
+
entry.state = "error";
|
|
4041
|
+
entry.err = e;
|
|
4042
|
+
notify();
|
|
4043
|
+
};
|
|
4044
|
+
img.src = src;
|
|
4045
|
+
};
|
|
4046
|
+
const startIconRaster = (key, markup, color, sizePx) => {
|
|
4047
|
+
const entry = { kind: "icon", state: "pending", bitmap: null };
|
|
4048
|
+
touch(key, entry);
|
|
4049
|
+
const colored = color ? applySvgColor(markup, color) : markup;
|
|
4050
|
+
const blob = new Blob([colored], { type: "image/svg+xml" });
|
|
4051
|
+
const url = URL.createObjectURL(blob);
|
|
4052
|
+
const img = new Image();
|
|
4053
|
+
img.onload = async () => {
|
|
4054
|
+
URL.revokeObjectURL(url);
|
|
4055
|
+
if (disposed) return;
|
|
4056
|
+
try {
|
|
4057
|
+
const bitmap = await createImageBitmap(img, {
|
|
4058
|
+
resizeWidth: sizePx,
|
|
4059
|
+
resizeHeight: sizePx,
|
|
4060
|
+
resizeQuality: "high"
|
|
4061
|
+
});
|
|
4062
|
+
if (disposed) {
|
|
4063
|
+
bitmap.close?.();
|
|
4064
|
+
return;
|
|
4065
|
+
}
|
|
4066
|
+
entry.state = "ready";
|
|
4067
|
+
entry.bitmap = bitmap;
|
|
4068
|
+
notify();
|
|
4069
|
+
} catch (e) {
|
|
4070
|
+
entry.state = "error";
|
|
4071
|
+
entry.err = e;
|
|
4072
|
+
notify();
|
|
4073
|
+
}
|
|
4074
|
+
};
|
|
4075
|
+
img.onerror = (e) => {
|
|
4076
|
+
URL.revokeObjectURL(url);
|
|
4077
|
+
if (disposed) return;
|
|
4078
|
+
entry.state = "error";
|
|
4079
|
+
entry.err = e;
|
|
4080
|
+
notify();
|
|
4081
|
+
};
|
|
4082
|
+
img.src = url;
|
|
4083
|
+
};
|
|
4084
|
+
return {
|
|
4085
|
+
getImage(src) {
|
|
4086
|
+
const key = `img:${src}`;
|
|
4087
|
+
const existing = entries.get(key);
|
|
4088
|
+
if (existing && existing.kind === "image") {
|
|
4089
|
+
if (existing.state === "ready") {
|
|
4090
|
+
touch(key, existing);
|
|
4091
|
+
return existing.bitmap;
|
|
4092
|
+
}
|
|
4093
|
+
return null;
|
|
4094
|
+
}
|
|
4095
|
+
startImageDecode(key, src);
|
|
4096
|
+
return null;
|
|
4097
|
+
},
|
|
4098
|
+
getIcon(markup, color, devicePixelSize) {
|
|
4099
|
+
const size = bucketSize(Math.max(1, Math.ceil(devicePixelSize)));
|
|
4100
|
+
const key = `icon:${size}:${color ?? ""}:${markup}`;
|
|
4101
|
+
const existing = entries.get(key);
|
|
4102
|
+
if (existing && existing.kind === "icon") {
|
|
4103
|
+
if (existing.state === "ready") {
|
|
4104
|
+
touch(key, existing);
|
|
4105
|
+
return existing.bitmap;
|
|
4106
|
+
}
|
|
4107
|
+
return null;
|
|
4108
|
+
}
|
|
4109
|
+
startIconRaster(key, markup, color, size);
|
|
4110
|
+
return null;
|
|
4111
|
+
},
|
|
4112
|
+
dispose() {
|
|
4113
|
+
disposed = true;
|
|
4114
|
+
for (const entry of entries.values()) {
|
|
4115
|
+
if (entry.kind === "icon" && entry.bitmap) entry.bitmap.close?.();
|
|
4116
|
+
}
|
|
4117
|
+
entries.clear();
|
|
4118
|
+
}
|
|
4119
|
+
};
|
|
4120
|
+
};
|
|
4121
|
+
|
|
4122
|
+
// src/render/assets/paint.ts
|
|
4123
|
+
var PLACEHOLDER_FILL = "#e5e7eb";
|
|
4124
|
+
var PLACEHOLDER_TEXT_FILL = "#94a3b8";
|
|
4125
|
+
var paintPlaceholder = (ctx, w, h, label) => {
|
|
4126
|
+
ctx.fillStyle = PLACEHOLDER_FILL;
|
|
4127
|
+
ctx.fillRect(0, 0, w, h);
|
|
4128
|
+
if (w >= 32 && h >= 16) {
|
|
4129
|
+
ctx.fillStyle = PLACEHOLDER_TEXT_FILL;
|
|
4130
|
+
ctx.font = "11px system-ui, sans-serif";
|
|
4131
|
+
ctx.textAlign = "center";
|
|
4132
|
+
ctx.textBaseline = "middle";
|
|
4133
|
+
ctx.fillText(label, w / 2, h / 2);
|
|
4134
|
+
}
|
|
4135
|
+
};
|
|
4136
|
+
var paintImageNode = (ctx, node, cache4, theme) => {
|
|
4137
|
+
if (node.w <= 0 || node.h <= 0) return;
|
|
4138
|
+
const data = node.data;
|
|
4139
|
+
if (!data?.src) return;
|
|
4140
|
+
const bitmap = cache4.getImage(data.src);
|
|
4141
|
+
const opacity = resolveOpacity(node.style, theme);
|
|
4142
|
+
const needsScope = opacity !== 1;
|
|
4143
|
+
if (needsScope) {
|
|
4144
|
+
ctx.save();
|
|
4145
|
+
ctx.globalAlpha = opacity;
|
|
4146
|
+
}
|
|
4147
|
+
if (bitmap?.complete) {
|
|
4148
|
+
ctx.drawImage(bitmap, 0, 0, node.w, node.h);
|
|
4149
|
+
} else {
|
|
4150
|
+
paintPlaceholder(ctx, node.w, node.h, "loading\u2026");
|
|
4151
|
+
}
|
|
4152
|
+
if (needsScope) ctx.restore();
|
|
4153
|
+
};
|
|
4154
|
+
var paintIconNode = (ctx, node, cache4, scale, theme) => {
|
|
4155
|
+
if (node.w <= 0 || node.h <= 0) return;
|
|
4156
|
+
const data = node.data;
|
|
4157
|
+
if (!data?.src) return;
|
|
4158
|
+
const sizePx = Math.max(node.w, node.h) * scale;
|
|
4159
|
+
const color = node.style?.iconColor;
|
|
4160
|
+
const bitmap = cache4.getIcon(data.src, color, sizePx);
|
|
4161
|
+
const opacity = resolveOpacity(node.style, theme);
|
|
4162
|
+
const needsScope = opacity !== 1;
|
|
4163
|
+
if (needsScope) {
|
|
4164
|
+
ctx.save();
|
|
4165
|
+
ctx.globalAlpha = opacity;
|
|
4166
|
+
}
|
|
4167
|
+
if (bitmap) {
|
|
4168
|
+
ctx.drawImage(bitmap, 0, 0, node.w, node.h);
|
|
4169
|
+
} else {
|
|
4170
|
+
paintPlaceholder(ctx, node.w, node.h, "svg\u2026");
|
|
4171
|
+
}
|
|
4172
|
+
if (needsScope) ctx.restore();
|
|
4173
|
+
};
|
|
4174
|
+
|
|
3801
4175
|
// src/render/background.ts
|
|
3802
4176
|
var MIN_PATTERN_SCREEN_PX = 8;
|
|
3803
4177
|
var MIN_VISIBLE_PATTERN_PX = 2;
|
|
@@ -3939,14 +4313,14 @@ var hitTestRotateHandle = (node, worldPoint, cameraZ) => {
|
|
|
3939
4313
|
};
|
|
3940
4314
|
|
|
3941
4315
|
// src/render/overlay.ts
|
|
3942
|
-
var
|
|
4316
|
+
var DEFAULT_SELECTION_COLOR = "#3b82f6";
|
|
3943
4317
|
var SELECTION_OUTLINE_PX = 1.5;
|
|
3944
|
-
var
|
|
4318
|
+
var MARQUEE_FILL_ALPHA = 0.08;
|
|
3945
4319
|
var MARQUEE_STROKE_PX = 1;
|
|
3946
|
-
var drawSelectionOutline = (ctx, node, scale) => {
|
|
4320
|
+
var drawSelectionOutline = (ctx, node, scale, color) => {
|
|
3947
4321
|
if (node.angle === 0) {
|
|
3948
4322
|
ctx.save();
|
|
3949
|
-
ctx.strokeStyle =
|
|
4323
|
+
ctx.strokeStyle = color;
|
|
3950
4324
|
ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
|
|
3951
4325
|
ctx.beginPath();
|
|
3952
4326
|
ctx.rect(node.x, node.y, node.w, node.h);
|
|
@@ -3965,7 +4339,7 @@ var drawSelectionOutline = (ctx, node, scale) => {
|
|
|
3965
4339
|
{ x: -node.w / 2, y: node.h / 2 }
|
|
3966
4340
|
].map((p) => ({ x: cx + p.x * cos - p.y * sin, y: cy + p.x * sin + p.y * cos }));
|
|
3967
4341
|
ctx.save();
|
|
3968
|
-
ctx.strokeStyle =
|
|
4342
|
+
ctx.strokeStyle = color;
|
|
3969
4343
|
ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
|
|
3970
4344
|
ctx.beginPath();
|
|
3971
4345
|
const first = corners[0];
|
|
@@ -3978,13 +4352,13 @@ var drawSelectionOutline = (ctx, node, scale) => {
|
|
|
3978
4352
|
ctx.stroke();
|
|
3979
4353
|
ctx.restore();
|
|
3980
4354
|
};
|
|
3981
|
-
var drawResizeHandles = (ctx, node, scale) => {
|
|
4355
|
+
var drawResizeHandles = (ctx, node, scale, color) => {
|
|
3982
4356
|
const halfPx = RESIZE_HANDLE_SIZE_PX / 2;
|
|
3983
4357
|
const halfWorld = halfPx / scale;
|
|
3984
4358
|
const positions = handleWorldPositions(node);
|
|
3985
4359
|
ctx.save();
|
|
3986
4360
|
ctx.fillStyle = "#fff";
|
|
3987
|
-
ctx.strokeStyle =
|
|
4361
|
+
ctx.strokeStyle = color;
|
|
3988
4362
|
ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
|
|
3989
4363
|
for (const key of Object.keys(positions)) {
|
|
3990
4364
|
const p = positions[key];
|
|
@@ -3995,7 +4369,7 @@ var drawResizeHandles = (ctx, node, scale) => {
|
|
|
3995
4369
|
}
|
|
3996
4370
|
ctx.restore();
|
|
3997
4371
|
};
|
|
3998
|
-
var drawRotateHandle = (ctx, node, scale, cameraZ) => {
|
|
4372
|
+
var drawRotateHandle = (ctx, node, scale, cameraZ, color) => {
|
|
3999
4373
|
const center = rotateHandleWorldPosition(node, cameraZ);
|
|
4000
4374
|
const radiusWorld = ROTATE_HANDLE_RADIUS_PX / scale;
|
|
4001
4375
|
const cx = node.x + node.w / 2;
|
|
@@ -4008,7 +4382,7 @@ var drawRotateHandle = (ctx, node, scale, cameraZ) => {
|
|
|
4008
4382
|
y: cy + 0 * sin + topMidLocalY * cos
|
|
4009
4383
|
};
|
|
4010
4384
|
ctx.save();
|
|
4011
|
-
ctx.strokeStyle =
|
|
4385
|
+
ctx.strokeStyle = color;
|
|
4012
4386
|
ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
|
|
4013
4387
|
ctx.beginPath();
|
|
4014
4388
|
ctx.moveTo(topMidWorld.x, topMidWorld.y);
|
|
@@ -4021,12 +4395,12 @@ var drawRotateHandle = (ctx, node, scale, cameraZ) => {
|
|
|
4021
4395
|
ctx.stroke();
|
|
4022
4396
|
ctx.restore();
|
|
4023
4397
|
};
|
|
4024
|
-
var drawEdgeMidpointHandle = (ctx, midpoint, scale) => {
|
|
4398
|
+
var drawEdgeMidpointHandle = (ctx, midpoint, scale, color) => {
|
|
4025
4399
|
const radiusPx = 5;
|
|
4026
4400
|
const radiusWorld = radiusPx / scale;
|
|
4027
4401
|
ctx.save();
|
|
4028
4402
|
ctx.fillStyle = "#fff";
|
|
4029
|
-
ctx.strokeStyle =
|
|
4403
|
+
ctx.strokeStyle = color;
|
|
4030
4404
|
ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
|
|
4031
4405
|
ctx.beginPath();
|
|
4032
4406
|
ctx.arc(midpoint.x, midpoint.y, radiusWorld, 0, Math.PI * 2);
|
|
@@ -4034,12 +4408,12 @@ var drawEdgeMidpointHandle = (ctx, midpoint, scale) => {
|
|
|
4034
4408
|
ctx.stroke();
|
|
4035
4409
|
ctx.restore();
|
|
4036
4410
|
};
|
|
4037
|
-
var drawEdgeEndpointHandles = (ctx, source, target, scale) => {
|
|
4411
|
+
var drawEdgeEndpointHandles = (ctx, source, target, scale, color) => {
|
|
4038
4412
|
const radiusPx = 5;
|
|
4039
4413
|
const radiusWorld = radiusPx / scale;
|
|
4040
4414
|
ctx.save();
|
|
4041
4415
|
ctx.fillStyle = "#fff";
|
|
4042
|
-
ctx.strokeStyle =
|
|
4416
|
+
ctx.strokeStyle = color;
|
|
4043
4417
|
ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
|
|
4044
4418
|
for (const p of [source, target]) {
|
|
4045
4419
|
ctx.beginPath();
|
|
@@ -4049,11 +4423,13 @@ var drawEdgeEndpointHandles = (ctx, source, target, scale) => {
|
|
|
4049
4423
|
}
|
|
4050
4424
|
ctx.restore();
|
|
4051
4425
|
};
|
|
4052
|
-
var drawMarquee = (ctx, rect, scale) => {
|
|
4426
|
+
var drawMarquee = (ctx, rect, scale, color) => {
|
|
4053
4427
|
ctx.save();
|
|
4054
|
-
ctx.
|
|
4428
|
+
ctx.globalAlpha = MARQUEE_FILL_ALPHA;
|
|
4429
|
+
ctx.fillStyle = color;
|
|
4055
4430
|
ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
|
|
4056
|
-
ctx.
|
|
4431
|
+
ctx.globalAlpha = 1;
|
|
4432
|
+
ctx.strokeStyle = color;
|
|
4057
4433
|
ctx.lineWidth = MARQUEE_STROKE_PX / scale;
|
|
4058
4434
|
ctx.setLineDash([4 / scale, 3 / scale]);
|
|
4059
4435
|
ctx.strokeRect(rect.x, rect.y, rect.w, rect.h);
|
|
@@ -4132,12 +4508,18 @@ var createRenderer = (opts) => {
|
|
|
4132
4508
|
const staticSurface = setupSurface(opts.staticCanvas);
|
|
4133
4509
|
const interactiveSurface = setupSurface(opts.interactiveCanvas);
|
|
4134
4510
|
let background = opts.background;
|
|
4511
|
+
let selectionColor = opts.selectionColor ?? DEFAULT_SELECTION_COLOR;
|
|
4135
4512
|
sizeSurface(staticSurface, opts.width, opts.height);
|
|
4136
4513
|
sizeSurface(interactiveSurface, opts.width, opts.height);
|
|
4137
4514
|
let staticDirty = true;
|
|
4138
4515
|
let interactiveDirty = false;
|
|
4139
4516
|
let overlaySet = /* @__PURE__ */ new Set();
|
|
4140
4517
|
let lastDrawn = 0;
|
|
4518
|
+
const requestRepaint = () => {
|
|
4519
|
+
staticDirty = true;
|
|
4520
|
+
loop.requestFrame();
|
|
4521
|
+
};
|
|
4522
|
+
const assetCache = createAssetCache({ onReady: requestRepaint });
|
|
4141
4523
|
const isInteractive = (state) => state.mode !== "idle" || store.getSelection().length > 0;
|
|
4142
4524
|
const drawFrame = () => {
|
|
4143
4525
|
if (staticDirty) {
|
|
@@ -4207,6 +4589,20 @@ var createRenderer = (opts) => {
|
|
|
4207
4589
|
drawn++;
|
|
4208
4590
|
continue;
|
|
4209
4591
|
}
|
|
4592
|
+
if (node.type === "image") {
|
|
4593
|
+
drawWithNodeTransform(staticSurface.ctx, node, () => {
|
|
4594
|
+
paintImageNode(staticSurface.ctx, node, assetCache, theme);
|
|
4595
|
+
});
|
|
4596
|
+
drawn++;
|
|
4597
|
+
continue;
|
|
4598
|
+
}
|
|
4599
|
+
if (node.type === "icon") {
|
|
4600
|
+
drawWithNodeTransform(staticSurface.ctx, node, () => {
|
|
4601
|
+
paintIconNode(staticSurface.ctx, node, assetCache, scale, theme);
|
|
4602
|
+
});
|
|
4603
|
+
drawn++;
|
|
4604
|
+
continue;
|
|
4605
|
+
}
|
|
4210
4606
|
if (node.type === "text") {
|
|
4211
4607
|
drawWithNodeTransform(staticSurface.ctx, node, () => {
|
|
4212
4608
|
if (isEditingThis) return;
|
|
@@ -4365,8 +4761,17 @@ var createRenderer = (opts) => {
|
|
|
4365
4761
|
isMoving: true};
|
|
4366
4762
|
const dragRoughEnabled = inDragMap.size <= ROUGH_MAX_MOVING_NODES && camera.z >= ROUGH_MIN_ZOOM;
|
|
4367
4763
|
for (const node of inDragMap.values()) {
|
|
4368
|
-
if (!isDrawablePrimitive(node.type) && node.type !== "text")
|
|
4764
|
+
if (!isDrawablePrimitive(node.type) && node.type !== "text" && node.type !== "image" && node.type !== "icon")
|
|
4765
|
+
continue;
|
|
4369
4766
|
drawWithNodeTransform(ctx, node, () => {
|
|
4767
|
+
if (node.type === "image") {
|
|
4768
|
+
paintImageNode(ctx, node, assetCache, theme);
|
|
4769
|
+
return;
|
|
4770
|
+
}
|
|
4771
|
+
if (node.type === "icon") {
|
|
4772
|
+
paintIconNode(ctx, node, assetCache, scale, theme);
|
|
4773
|
+
return;
|
|
4774
|
+
}
|
|
4370
4775
|
if (isDrawablePrimitive(node.type)) {
|
|
4371
4776
|
const useRough = dragRoughEnabled && (node.style?.roughness ?? 0) > 0;
|
|
4372
4777
|
const roughReady = useRough ? getRoughCanvasCtor() !== null : false;
|
|
@@ -4418,32 +4823,32 @@ var createRenderer = (opts) => {
|
|
|
4418
4823
|
for (const id of selectedNodeIds) {
|
|
4419
4824
|
const node = inDragMap.get(id) ?? store.getNode(id);
|
|
4420
4825
|
if (!node) continue;
|
|
4421
|
-
drawSelectionOutline(ctx, node, scale);
|
|
4826
|
+
drawSelectionOutline(ctx, node, scale, selectionColor);
|
|
4422
4827
|
}
|
|
4423
4828
|
if (interaction.mode !== "dragging" && selectedNodeIds.length === 1) {
|
|
4424
4829
|
const node = inDragMap.get(selectedNodeIds[0]) ?? store.getNode(selectedNodeIds[0]);
|
|
4425
4830
|
if (node) {
|
|
4426
|
-
drawResizeHandles(ctx, node, scale);
|
|
4427
|
-
drawRotateHandle(ctx, node, scale, camera.z);
|
|
4831
|
+
drawResizeHandles(ctx, node, scale, selectionColor);
|
|
4832
|
+
drawRotateHandle(ctx, node, scale, camera.z, selectionColor);
|
|
4428
4833
|
}
|
|
4429
4834
|
}
|
|
4430
4835
|
}
|
|
4431
4836
|
for (const id of selectedEdgeIds) {
|
|
4432
4837
|
const geom = store.getEdgeGeometry(id);
|
|
4433
4838
|
if (geom) {
|
|
4434
|
-
drawEdgeEndpointHandles(ctx, geom.source, geom.target, scale);
|
|
4839
|
+
drawEdgeEndpointHandles(ctx, geom.source, geom.target, scale, selectionColor);
|
|
4435
4840
|
const edge = store.getEdge(id);
|
|
4436
4841
|
if (edge && edge.pathStyle === "bezier") {
|
|
4437
4842
|
const mid = getPointAndTangentAtArcLength(geom.samples, 0.5).point;
|
|
4438
|
-
drawEdgeMidpointHandle(ctx, mid, scale);
|
|
4843
|
+
drawEdgeMidpointHandle(ctx, mid, scale, selectionColor);
|
|
4439
4844
|
}
|
|
4440
4845
|
}
|
|
4441
4846
|
}
|
|
4442
4847
|
if (interaction.mode === "marqueeing" && interaction.marqueeRect) {
|
|
4443
|
-
drawMarquee(ctx, interaction.marqueeRect, scale);
|
|
4848
|
+
drawMarquee(ctx, interaction.marqueeRect, scale, selectionColor);
|
|
4444
4849
|
}
|
|
4445
4850
|
if (interaction.mode === "creating-shape" && interaction.createDraftRect) {
|
|
4446
|
-
drawMarquee(ctx, interaction.createDraftRect, scale);
|
|
4851
|
+
drawMarquee(ctx, interaction.createDraftRect, scale, selectionColor);
|
|
4447
4852
|
}
|
|
4448
4853
|
if ((interaction.mode === "creating-edge" || interaction.mode === "reconnecting-edge") && interaction.draftEdge) {
|
|
4449
4854
|
const draft = {
|
|
@@ -4451,7 +4856,7 @@ var createRenderer = (opts) => {
|
|
|
4451
4856
|
source: interaction.draftEdge.source,
|
|
4452
4857
|
target: interaction.draftEdge.target,
|
|
4453
4858
|
pathStyle: "bezier",
|
|
4454
|
-
style: { strokeColor:
|
|
4859
|
+
style: { strokeColor: selectionColor }
|
|
4455
4860
|
};
|
|
4456
4861
|
const geom = computeEdgeGeometry(draft, (id) => store.getNode(id));
|
|
4457
4862
|
if (geom) {
|
|
@@ -4555,6 +4960,11 @@ var createRenderer = (opts) => {
|
|
|
4555
4960
|
staticDirty = true;
|
|
4556
4961
|
loop.requestFrame();
|
|
4557
4962
|
},
|
|
4963
|
+
setSelectionColor(color) {
|
|
4964
|
+
selectionColor = color;
|
|
4965
|
+
interactiveDirty = true;
|
|
4966
|
+
loop.requestFrame();
|
|
4967
|
+
},
|
|
4558
4968
|
stats: () => loop.stats(),
|
|
4559
4969
|
lastDrawCount: () => lastDrawn,
|
|
4560
4970
|
getOverlaySet: () => [...overlaySet],
|
|
@@ -4565,6 +4975,7 @@ var createRenderer = (opts) => {
|
|
|
4565
4975
|
unsubSelection();
|
|
4566
4976
|
unsubInteraction();
|
|
4567
4977
|
unsubFontEpoch();
|
|
4978
|
+
assetCache.dispose();
|
|
4568
4979
|
}
|
|
4569
4980
|
};
|
|
4570
4981
|
};
|
|
@@ -5594,6 +6005,6 @@ var installedExtensions = (store) => {
|
|
|
5594
6005
|
// src/index.ts
|
|
5595
6006
|
var VERSION = "0.0.0";
|
|
5596
6007
|
|
|
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 };
|
|
6008
|
+
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
6009
|
//# sourceMappingURL=index.js.map
|
|
5599
6010
|
//# sourceMappingURL=index.js.map
|