@canvas-harness/core 0.0.1 → 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/dist/index.cjs +446 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +187 -1
- package/dist/index.d.ts +187 -1
- package/dist/index.js +437 -26
- 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 = [];
|
|
@@ -3328,6 +3477,55 @@ var createCanvasStore = (opts = {}) => {
|
|
|
3328
3477
|
if (batch) emitChange(batch);
|
|
3329
3478
|
});
|
|
3330
3479
|
},
|
|
3480
|
+
async addImage(opts2) {
|
|
3481
|
+
validateImageInput(opts2.src);
|
|
3482
|
+
const rawBlob = await toImageBlob(opts2.src);
|
|
3483
|
+
const maxDim = opts2.maxDimension ?? 2048;
|
|
3484
|
+
const { blob, naturalW, naturalH } = await downscaleImageBlob(rawBlob, maxDim);
|
|
3485
|
+
const src = await blobToDataUri(blob);
|
|
3486
|
+
const DEFAULT_MAX_NODE_SIDE = 400;
|
|
3487
|
+
const aspectScale = Math.min(1, DEFAULT_MAX_NODE_SIDE / Math.max(naturalW, naturalH));
|
|
3488
|
+
const w = opts2.w ?? Math.max(1, Math.round(naturalW * aspectScale));
|
|
3489
|
+
const h = opts2.h ?? Math.max(1, Math.round(naturalH * aspectScale));
|
|
3490
|
+
const id = asNodeId(idGenerator());
|
|
3491
|
+
this.addNode({
|
|
3492
|
+
id,
|
|
3493
|
+
type: "image",
|
|
3494
|
+
x: opts2.x,
|
|
3495
|
+
y: opts2.y,
|
|
3496
|
+
w,
|
|
3497
|
+
h,
|
|
3498
|
+
angle: 0,
|
|
3499
|
+
z: 0,
|
|
3500
|
+
groups: [],
|
|
3501
|
+
style: opts2.style,
|
|
3502
|
+
data: { src, naturalW, naturalH, alt: opts2.alt }
|
|
3503
|
+
});
|
|
3504
|
+
return id;
|
|
3505
|
+
},
|
|
3506
|
+
async addSvg(opts2) {
|
|
3507
|
+
validateSvgMarkup(opts2.src);
|
|
3508
|
+
const sanitized = sanitizeSvg(opts2.src);
|
|
3509
|
+
const intrinsic = extractSvgDimensions(sanitized);
|
|
3510
|
+
const w = opts2.w ?? intrinsic.w;
|
|
3511
|
+
const h = opts2.h ?? intrinsic.h;
|
|
3512
|
+
const mergedStyle = opts2.color || opts2.style ? { ...opts2.color ? { iconColor: opts2.color } : {}, ...opts2.style } : void 0;
|
|
3513
|
+
const id = asNodeId(idGenerator());
|
|
3514
|
+
this.addNode({
|
|
3515
|
+
id,
|
|
3516
|
+
type: "icon",
|
|
3517
|
+
x: opts2.x,
|
|
3518
|
+
y: opts2.y,
|
|
3519
|
+
w,
|
|
3520
|
+
h,
|
|
3521
|
+
angle: 0,
|
|
3522
|
+
z: 0,
|
|
3523
|
+
groups: [],
|
|
3524
|
+
...mergedStyle ? { style: mergedStyle } : {},
|
|
3525
|
+
data: { src: sanitized, alt: opts2.alt }
|
|
3526
|
+
});
|
|
3527
|
+
return id;
|
|
3528
|
+
},
|
|
3331
3529
|
addEdge(edge) {
|
|
3332
3530
|
const withZ = edge.z === 0 ? { ...edge, z: ++topZ } : edge;
|
|
3333
3531
|
if (withZ.z > topZ) topZ = withZ.z;
|
|
@@ -3800,6 +3998,182 @@ var createFrameLoop = ({ draw, historySize = 60 }) => {
|
|
|
3800
3998
|
};
|
|
3801
3999
|
};
|
|
3802
4000
|
|
|
4001
|
+
// src/render/assets/cache.ts
|
|
4002
|
+
var MAX_ENTRIES2 = 256;
|
|
4003
|
+
var bucketSize = (px) => {
|
|
4004
|
+
if (px <= 32) return 32;
|
|
4005
|
+
if (px <= 64) return 64;
|
|
4006
|
+
if (px <= 128) return 128;
|
|
4007
|
+
if (px <= 256) return 256;
|
|
4008
|
+
if (px <= 512) return 512;
|
|
4009
|
+
return Math.ceil(px / 256) * 256;
|
|
4010
|
+
};
|
|
4011
|
+
var createAssetCache = (opts = {}) => {
|
|
4012
|
+
const entries = /* @__PURE__ */ new Map();
|
|
4013
|
+
let disposed = false;
|
|
4014
|
+
const notify = () => {
|
|
4015
|
+
if (disposed) return;
|
|
4016
|
+
opts.onReady?.();
|
|
4017
|
+
};
|
|
4018
|
+
const touch = (key, entry) => {
|
|
4019
|
+
entries.delete(key);
|
|
4020
|
+
entries.set(key, entry);
|
|
4021
|
+
if (entries.size > MAX_ENTRIES2) {
|
|
4022
|
+
const oldestKey = entries.keys().next().value;
|
|
4023
|
+
if (oldestKey !== void 0) {
|
|
4024
|
+
const evicted = entries.get(oldestKey);
|
|
4025
|
+
if (evicted?.kind === "icon" && evicted.bitmap) evicted.bitmap.close?.();
|
|
4026
|
+
entries.delete(oldestKey);
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
};
|
|
4030
|
+
const startImageDecode = (key, src) => {
|
|
4031
|
+
const entry = { kind: "image", state: "pending", bitmap: null };
|
|
4032
|
+
touch(key, entry);
|
|
4033
|
+
const img = new Image();
|
|
4034
|
+
img.onload = () => {
|
|
4035
|
+
if (disposed) return;
|
|
4036
|
+
entry.state = "ready";
|
|
4037
|
+
entry.bitmap = img;
|
|
4038
|
+
notify();
|
|
4039
|
+
};
|
|
4040
|
+
img.onerror = (e) => {
|
|
4041
|
+
if (disposed) return;
|
|
4042
|
+
entry.state = "error";
|
|
4043
|
+
entry.err = e;
|
|
4044
|
+
notify();
|
|
4045
|
+
};
|
|
4046
|
+
img.src = src;
|
|
4047
|
+
};
|
|
4048
|
+
const startIconRaster = (key, markup, color, sizePx) => {
|
|
4049
|
+
const entry = { kind: "icon", state: "pending", bitmap: null };
|
|
4050
|
+
touch(key, entry);
|
|
4051
|
+
const colored = color ? applySvgColor(markup, color) : markup;
|
|
4052
|
+
const blob = new Blob([colored], { type: "image/svg+xml" });
|
|
4053
|
+
const url = URL.createObjectURL(blob);
|
|
4054
|
+
const img = new Image();
|
|
4055
|
+
img.onload = async () => {
|
|
4056
|
+
URL.revokeObjectURL(url);
|
|
4057
|
+
if (disposed) return;
|
|
4058
|
+
try {
|
|
4059
|
+
const bitmap = await createImageBitmap(img, {
|
|
4060
|
+
resizeWidth: sizePx,
|
|
4061
|
+
resizeHeight: sizePx,
|
|
4062
|
+
resizeQuality: "high"
|
|
4063
|
+
});
|
|
4064
|
+
if (disposed) {
|
|
4065
|
+
bitmap.close?.();
|
|
4066
|
+
return;
|
|
4067
|
+
}
|
|
4068
|
+
entry.state = "ready";
|
|
4069
|
+
entry.bitmap = bitmap;
|
|
4070
|
+
notify();
|
|
4071
|
+
} catch (e) {
|
|
4072
|
+
entry.state = "error";
|
|
4073
|
+
entry.err = e;
|
|
4074
|
+
notify();
|
|
4075
|
+
}
|
|
4076
|
+
};
|
|
4077
|
+
img.onerror = (e) => {
|
|
4078
|
+
URL.revokeObjectURL(url);
|
|
4079
|
+
if (disposed) return;
|
|
4080
|
+
entry.state = "error";
|
|
4081
|
+
entry.err = e;
|
|
4082
|
+
notify();
|
|
4083
|
+
};
|
|
4084
|
+
img.src = url;
|
|
4085
|
+
};
|
|
4086
|
+
return {
|
|
4087
|
+
getImage(src) {
|
|
4088
|
+
const key = `img:${src}`;
|
|
4089
|
+
const existing = entries.get(key);
|
|
4090
|
+
if (existing && existing.kind === "image") {
|
|
4091
|
+
if (existing.state === "ready") {
|
|
4092
|
+
touch(key, existing);
|
|
4093
|
+
return existing.bitmap;
|
|
4094
|
+
}
|
|
4095
|
+
return null;
|
|
4096
|
+
}
|
|
4097
|
+
startImageDecode(key, src);
|
|
4098
|
+
return null;
|
|
4099
|
+
},
|
|
4100
|
+
getIcon(markup, color, devicePixelSize) {
|
|
4101
|
+
const size = bucketSize(Math.max(1, Math.ceil(devicePixelSize)));
|
|
4102
|
+
const key = `icon:${size}:${color ?? ""}:${markup}`;
|
|
4103
|
+
const existing = entries.get(key);
|
|
4104
|
+
if (existing && existing.kind === "icon") {
|
|
4105
|
+
if (existing.state === "ready") {
|
|
4106
|
+
touch(key, existing);
|
|
4107
|
+
return existing.bitmap;
|
|
4108
|
+
}
|
|
4109
|
+
return null;
|
|
4110
|
+
}
|
|
4111
|
+
startIconRaster(key, markup, color, size);
|
|
4112
|
+
return null;
|
|
4113
|
+
},
|
|
4114
|
+
dispose() {
|
|
4115
|
+
disposed = true;
|
|
4116
|
+
for (const entry of entries.values()) {
|
|
4117
|
+
if (entry.kind === "icon" && entry.bitmap) entry.bitmap.close?.();
|
|
4118
|
+
}
|
|
4119
|
+
entries.clear();
|
|
4120
|
+
}
|
|
4121
|
+
};
|
|
4122
|
+
};
|
|
4123
|
+
|
|
4124
|
+
// src/render/assets/paint.ts
|
|
4125
|
+
var PLACEHOLDER_FILL = "#e5e7eb";
|
|
4126
|
+
var PLACEHOLDER_TEXT_FILL = "#94a3b8";
|
|
4127
|
+
var paintPlaceholder = (ctx, w, h, label) => {
|
|
4128
|
+
ctx.fillStyle = PLACEHOLDER_FILL;
|
|
4129
|
+
ctx.fillRect(0, 0, w, h);
|
|
4130
|
+
if (w >= 32 && h >= 16) {
|
|
4131
|
+
ctx.fillStyle = PLACEHOLDER_TEXT_FILL;
|
|
4132
|
+
ctx.font = "11px system-ui, sans-serif";
|
|
4133
|
+
ctx.textAlign = "center";
|
|
4134
|
+
ctx.textBaseline = "middle";
|
|
4135
|
+
ctx.fillText(label, w / 2, h / 2);
|
|
4136
|
+
}
|
|
4137
|
+
};
|
|
4138
|
+
var paintImageNode = (ctx, node, cache4, theme) => {
|
|
4139
|
+
if (node.w <= 0 || node.h <= 0) return;
|
|
4140
|
+
const data = node.data;
|
|
4141
|
+
if (!data?.src) return;
|
|
4142
|
+
const bitmap = cache4.getImage(data.src);
|
|
4143
|
+
const opacity = resolveOpacity(node.style, theme);
|
|
4144
|
+
const needsScope = opacity !== 1;
|
|
4145
|
+
if (needsScope) {
|
|
4146
|
+
ctx.save();
|
|
4147
|
+
ctx.globalAlpha = opacity;
|
|
4148
|
+
}
|
|
4149
|
+
if (bitmap?.complete) {
|
|
4150
|
+
ctx.drawImage(bitmap, 0, 0, node.w, node.h);
|
|
4151
|
+
} else {
|
|
4152
|
+
paintPlaceholder(ctx, node.w, node.h, "loading\u2026");
|
|
4153
|
+
}
|
|
4154
|
+
if (needsScope) ctx.restore();
|
|
4155
|
+
};
|
|
4156
|
+
var paintIconNode = (ctx, node, cache4, scale, theme) => {
|
|
4157
|
+
if (node.w <= 0 || node.h <= 0) return;
|
|
4158
|
+
const data = node.data;
|
|
4159
|
+
if (!data?.src) return;
|
|
4160
|
+
const sizePx = Math.max(node.w, node.h) * scale;
|
|
4161
|
+
const color = node.style?.iconColor;
|
|
4162
|
+
const bitmap = cache4.getIcon(data.src, color, sizePx);
|
|
4163
|
+
const opacity = resolveOpacity(node.style, theme);
|
|
4164
|
+
const needsScope = opacity !== 1;
|
|
4165
|
+
if (needsScope) {
|
|
4166
|
+
ctx.save();
|
|
4167
|
+
ctx.globalAlpha = opacity;
|
|
4168
|
+
}
|
|
4169
|
+
if (bitmap) {
|
|
4170
|
+
ctx.drawImage(bitmap, 0, 0, node.w, node.h);
|
|
4171
|
+
} else {
|
|
4172
|
+
paintPlaceholder(ctx, node.w, node.h, "svg\u2026");
|
|
4173
|
+
}
|
|
4174
|
+
if (needsScope) ctx.restore();
|
|
4175
|
+
};
|
|
4176
|
+
|
|
3803
4177
|
// src/render/background.ts
|
|
3804
4178
|
var MIN_PATTERN_SCREEN_PX = 8;
|
|
3805
4179
|
var MIN_VISIBLE_PATTERN_PX = 2;
|
|
@@ -3941,14 +4315,14 @@ var hitTestRotateHandle = (node, worldPoint, cameraZ) => {
|
|
|
3941
4315
|
};
|
|
3942
4316
|
|
|
3943
4317
|
// src/render/overlay.ts
|
|
3944
|
-
var
|
|
4318
|
+
var DEFAULT_SELECTION_COLOR = "#3b82f6";
|
|
3945
4319
|
var SELECTION_OUTLINE_PX = 1.5;
|
|
3946
|
-
var
|
|
4320
|
+
var MARQUEE_FILL_ALPHA = 0.08;
|
|
3947
4321
|
var MARQUEE_STROKE_PX = 1;
|
|
3948
|
-
var drawSelectionOutline = (ctx, node, scale) => {
|
|
4322
|
+
var drawSelectionOutline = (ctx, node, scale, color) => {
|
|
3949
4323
|
if (node.angle === 0) {
|
|
3950
4324
|
ctx.save();
|
|
3951
|
-
ctx.strokeStyle =
|
|
4325
|
+
ctx.strokeStyle = color;
|
|
3952
4326
|
ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
|
|
3953
4327
|
ctx.beginPath();
|
|
3954
4328
|
ctx.rect(node.x, node.y, node.w, node.h);
|
|
@@ -3967,7 +4341,7 @@ var drawSelectionOutline = (ctx, node, scale) => {
|
|
|
3967
4341
|
{ x: -node.w / 2, y: node.h / 2 }
|
|
3968
4342
|
].map((p) => ({ x: cx + p.x * cos - p.y * sin, y: cy + p.x * sin + p.y * cos }));
|
|
3969
4343
|
ctx.save();
|
|
3970
|
-
ctx.strokeStyle =
|
|
4344
|
+
ctx.strokeStyle = color;
|
|
3971
4345
|
ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
|
|
3972
4346
|
ctx.beginPath();
|
|
3973
4347
|
const first = corners[0];
|
|
@@ -3980,13 +4354,13 @@ var drawSelectionOutline = (ctx, node, scale) => {
|
|
|
3980
4354
|
ctx.stroke();
|
|
3981
4355
|
ctx.restore();
|
|
3982
4356
|
};
|
|
3983
|
-
var drawResizeHandles = (ctx, node, scale) => {
|
|
4357
|
+
var drawResizeHandles = (ctx, node, scale, color) => {
|
|
3984
4358
|
const halfPx = RESIZE_HANDLE_SIZE_PX / 2;
|
|
3985
4359
|
const halfWorld = halfPx / scale;
|
|
3986
4360
|
const positions = handleWorldPositions(node);
|
|
3987
4361
|
ctx.save();
|
|
3988
4362
|
ctx.fillStyle = "#fff";
|
|
3989
|
-
ctx.strokeStyle =
|
|
4363
|
+
ctx.strokeStyle = color;
|
|
3990
4364
|
ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
|
|
3991
4365
|
for (const key of Object.keys(positions)) {
|
|
3992
4366
|
const p = positions[key];
|
|
@@ -3997,7 +4371,7 @@ var drawResizeHandles = (ctx, node, scale) => {
|
|
|
3997
4371
|
}
|
|
3998
4372
|
ctx.restore();
|
|
3999
4373
|
};
|
|
4000
|
-
var drawRotateHandle = (ctx, node, scale, cameraZ) => {
|
|
4374
|
+
var drawRotateHandle = (ctx, node, scale, cameraZ, color) => {
|
|
4001
4375
|
const center = rotateHandleWorldPosition(node, cameraZ);
|
|
4002
4376
|
const radiusWorld = ROTATE_HANDLE_RADIUS_PX / scale;
|
|
4003
4377
|
const cx = node.x + node.w / 2;
|
|
@@ -4010,7 +4384,7 @@ var drawRotateHandle = (ctx, node, scale, cameraZ) => {
|
|
|
4010
4384
|
y: cy + 0 * sin + topMidLocalY * cos
|
|
4011
4385
|
};
|
|
4012
4386
|
ctx.save();
|
|
4013
|
-
ctx.strokeStyle =
|
|
4387
|
+
ctx.strokeStyle = color;
|
|
4014
4388
|
ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
|
|
4015
4389
|
ctx.beginPath();
|
|
4016
4390
|
ctx.moveTo(topMidWorld.x, topMidWorld.y);
|
|
@@ -4023,12 +4397,12 @@ var drawRotateHandle = (ctx, node, scale, cameraZ) => {
|
|
|
4023
4397
|
ctx.stroke();
|
|
4024
4398
|
ctx.restore();
|
|
4025
4399
|
};
|
|
4026
|
-
var drawEdgeMidpointHandle = (ctx, midpoint, scale) => {
|
|
4400
|
+
var drawEdgeMidpointHandle = (ctx, midpoint, scale, color) => {
|
|
4027
4401
|
const radiusPx = 5;
|
|
4028
4402
|
const radiusWorld = radiusPx / scale;
|
|
4029
4403
|
ctx.save();
|
|
4030
4404
|
ctx.fillStyle = "#fff";
|
|
4031
|
-
ctx.strokeStyle =
|
|
4405
|
+
ctx.strokeStyle = color;
|
|
4032
4406
|
ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
|
|
4033
4407
|
ctx.beginPath();
|
|
4034
4408
|
ctx.arc(midpoint.x, midpoint.y, radiusWorld, 0, Math.PI * 2);
|
|
@@ -4036,12 +4410,12 @@ var drawEdgeMidpointHandle = (ctx, midpoint, scale) => {
|
|
|
4036
4410
|
ctx.stroke();
|
|
4037
4411
|
ctx.restore();
|
|
4038
4412
|
};
|
|
4039
|
-
var drawEdgeEndpointHandles = (ctx, source, target, scale) => {
|
|
4413
|
+
var drawEdgeEndpointHandles = (ctx, source, target, scale, color) => {
|
|
4040
4414
|
const radiusPx = 5;
|
|
4041
4415
|
const radiusWorld = radiusPx / scale;
|
|
4042
4416
|
ctx.save();
|
|
4043
4417
|
ctx.fillStyle = "#fff";
|
|
4044
|
-
ctx.strokeStyle =
|
|
4418
|
+
ctx.strokeStyle = color;
|
|
4045
4419
|
ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
|
|
4046
4420
|
for (const p of [source, target]) {
|
|
4047
4421
|
ctx.beginPath();
|
|
@@ -4051,11 +4425,13 @@ var drawEdgeEndpointHandles = (ctx, source, target, scale) => {
|
|
|
4051
4425
|
}
|
|
4052
4426
|
ctx.restore();
|
|
4053
4427
|
};
|
|
4054
|
-
var drawMarquee = (ctx, rect, scale) => {
|
|
4428
|
+
var drawMarquee = (ctx, rect, scale, color) => {
|
|
4055
4429
|
ctx.save();
|
|
4056
|
-
ctx.
|
|
4430
|
+
ctx.globalAlpha = MARQUEE_FILL_ALPHA;
|
|
4431
|
+
ctx.fillStyle = color;
|
|
4057
4432
|
ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
|
|
4058
|
-
ctx.
|
|
4433
|
+
ctx.globalAlpha = 1;
|
|
4434
|
+
ctx.strokeStyle = color;
|
|
4059
4435
|
ctx.lineWidth = MARQUEE_STROKE_PX / scale;
|
|
4060
4436
|
ctx.setLineDash([4 / scale, 3 / scale]);
|
|
4061
4437
|
ctx.strokeRect(rect.x, rect.y, rect.w, rect.h);
|
|
@@ -4134,12 +4510,18 @@ var createRenderer = (opts) => {
|
|
|
4134
4510
|
const staticSurface = setupSurface(opts.staticCanvas);
|
|
4135
4511
|
const interactiveSurface = setupSurface(opts.interactiveCanvas);
|
|
4136
4512
|
let background = opts.background;
|
|
4513
|
+
let selectionColor = opts.selectionColor ?? DEFAULT_SELECTION_COLOR;
|
|
4137
4514
|
sizeSurface(staticSurface, opts.width, opts.height);
|
|
4138
4515
|
sizeSurface(interactiveSurface, opts.width, opts.height);
|
|
4139
4516
|
let staticDirty = true;
|
|
4140
4517
|
let interactiveDirty = false;
|
|
4141
4518
|
let overlaySet = /* @__PURE__ */ new Set();
|
|
4142
4519
|
let lastDrawn = 0;
|
|
4520
|
+
const requestRepaint = () => {
|
|
4521
|
+
staticDirty = true;
|
|
4522
|
+
loop.requestFrame();
|
|
4523
|
+
};
|
|
4524
|
+
const assetCache = createAssetCache({ onReady: requestRepaint });
|
|
4143
4525
|
const isInteractive = (state) => state.mode !== "idle" || store.getSelection().length > 0;
|
|
4144
4526
|
const drawFrame = () => {
|
|
4145
4527
|
if (staticDirty) {
|
|
@@ -4209,6 +4591,20 @@ var createRenderer = (opts) => {
|
|
|
4209
4591
|
drawn++;
|
|
4210
4592
|
continue;
|
|
4211
4593
|
}
|
|
4594
|
+
if (node.type === "image") {
|
|
4595
|
+
drawWithNodeTransform(staticSurface.ctx, node, () => {
|
|
4596
|
+
paintImageNode(staticSurface.ctx, node, assetCache, theme);
|
|
4597
|
+
});
|
|
4598
|
+
drawn++;
|
|
4599
|
+
continue;
|
|
4600
|
+
}
|
|
4601
|
+
if (node.type === "icon") {
|
|
4602
|
+
drawWithNodeTransform(staticSurface.ctx, node, () => {
|
|
4603
|
+
paintIconNode(staticSurface.ctx, node, assetCache, scale, theme);
|
|
4604
|
+
});
|
|
4605
|
+
drawn++;
|
|
4606
|
+
continue;
|
|
4607
|
+
}
|
|
4212
4608
|
if (node.type === "text") {
|
|
4213
4609
|
drawWithNodeTransform(staticSurface.ctx, node, () => {
|
|
4214
4610
|
if (isEditingThis) return;
|
|
@@ -4367,8 +4763,17 @@ var createRenderer = (opts) => {
|
|
|
4367
4763
|
isMoving: true};
|
|
4368
4764
|
const dragRoughEnabled = inDragMap.size <= ROUGH_MAX_MOVING_NODES && camera.z >= ROUGH_MIN_ZOOM;
|
|
4369
4765
|
for (const node of inDragMap.values()) {
|
|
4370
|
-
if (!isDrawablePrimitive(node.type) && node.type !== "text")
|
|
4766
|
+
if (!isDrawablePrimitive(node.type) && node.type !== "text" && node.type !== "image" && node.type !== "icon")
|
|
4767
|
+
continue;
|
|
4371
4768
|
drawWithNodeTransform(ctx, node, () => {
|
|
4769
|
+
if (node.type === "image") {
|
|
4770
|
+
paintImageNode(ctx, node, assetCache, theme);
|
|
4771
|
+
return;
|
|
4772
|
+
}
|
|
4773
|
+
if (node.type === "icon") {
|
|
4774
|
+
paintIconNode(ctx, node, assetCache, scale, theme);
|
|
4775
|
+
return;
|
|
4776
|
+
}
|
|
4372
4777
|
if (isDrawablePrimitive(node.type)) {
|
|
4373
4778
|
const useRough = dragRoughEnabled && (node.style?.roughness ?? 0) > 0;
|
|
4374
4779
|
const roughReady = useRough ? getRoughCanvasCtor() !== null : false;
|
|
@@ -4420,32 +4825,32 @@ var createRenderer = (opts) => {
|
|
|
4420
4825
|
for (const id of selectedNodeIds) {
|
|
4421
4826
|
const node = inDragMap.get(id) ?? store.getNode(id);
|
|
4422
4827
|
if (!node) continue;
|
|
4423
|
-
drawSelectionOutline(ctx, node, scale);
|
|
4828
|
+
drawSelectionOutline(ctx, node, scale, selectionColor);
|
|
4424
4829
|
}
|
|
4425
4830
|
if (interaction.mode !== "dragging" && selectedNodeIds.length === 1) {
|
|
4426
4831
|
const node = inDragMap.get(selectedNodeIds[0]) ?? store.getNode(selectedNodeIds[0]);
|
|
4427
4832
|
if (node) {
|
|
4428
|
-
drawResizeHandles(ctx, node, scale);
|
|
4429
|
-
drawRotateHandle(ctx, node, scale, camera.z);
|
|
4833
|
+
drawResizeHandles(ctx, node, scale, selectionColor);
|
|
4834
|
+
drawRotateHandle(ctx, node, scale, camera.z, selectionColor);
|
|
4430
4835
|
}
|
|
4431
4836
|
}
|
|
4432
4837
|
}
|
|
4433
4838
|
for (const id of selectedEdgeIds) {
|
|
4434
4839
|
const geom = store.getEdgeGeometry(id);
|
|
4435
4840
|
if (geom) {
|
|
4436
|
-
drawEdgeEndpointHandles(ctx, geom.source, geom.target, scale);
|
|
4841
|
+
drawEdgeEndpointHandles(ctx, geom.source, geom.target, scale, selectionColor);
|
|
4437
4842
|
const edge = store.getEdge(id);
|
|
4438
4843
|
if (edge && edge.pathStyle === "bezier") {
|
|
4439
4844
|
const mid = getPointAndTangentAtArcLength(geom.samples, 0.5).point;
|
|
4440
|
-
drawEdgeMidpointHandle(ctx, mid, scale);
|
|
4845
|
+
drawEdgeMidpointHandle(ctx, mid, scale, selectionColor);
|
|
4441
4846
|
}
|
|
4442
4847
|
}
|
|
4443
4848
|
}
|
|
4444
4849
|
if (interaction.mode === "marqueeing" && interaction.marqueeRect) {
|
|
4445
|
-
drawMarquee(ctx, interaction.marqueeRect, scale);
|
|
4850
|
+
drawMarquee(ctx, interaction.marqueeRect, scale, selectionColor);
|
|
4446
4851
|
}
|
|
4447
4852
|
if (interaction.mode === "creating-shape" && interaction.createDraftRect) {
|
|
4448
|
-
drawMarquee(ctx, interaction.createDraftRect, scale);
|
|
4853
|
+
drawMarquee(ctx, interaction.createDraftRect, scale, selectionColor);
|
|
4449
4854
|
}
|
|
4450
4855
|
if ((interaction.mode === "creating-edge" || interaction.mode === "reconnecting-edge") && interaction.draftEdge) {
|
|
4451
4856
|
const draft = {
|
|
@@ -4453,7 +4858,7 @@ var createRenderer = (opts) => {
|
|
|
4453
4858
|
source: interaction.draftEdge.source,
|
|
4454
4859
|
target: interaction.draftEdge.target,
|
|
4455
4860
|
pathStyle: "bezier",
|
|
4456
|
-
style: { strokeColor:
|
|
4861
|
+
style: { strokeColor: selectionColor }
|
|
4457
4862
|
};
|
|
4458
4863
|
const geom = computeEdgeGeometry(draft, (id) => store.getNode(id));
|
|
4459
4864
|
if (geom) {
|
|
@@ -4557,6 +4962,11 @@ var createRenderer = (opts) => {
|
|
|
4557
4962
|
staticDirty = true;
|
|
4558
4963
|
loop.requestFrame();
|
|
4559
4964
|
},
|
|
4965
|
+
setSelectionColor(color) {
|
|
4966
|
+
selectionColor = color;
|
|
4967
|
+
interactiveDirty = true;
|
|
4968
|
+
loop.requestFrame();
|
|
4969
|
+
},
|
|
4560
4970
|
stats: () => loop.stats(),
|
|
4561
4971
|
lastDrawCount: () => lastDrawn,
|
|
4562
4972
|
getOverlaySet: () => [...overlaySet],
|
|
@@ -4567,6 +4977,7 @@ var createRenderer = (opts) => {
|
|
|
4567
4977
|
unsubSelection();
|
|
4568
4978
|
unsubInteraction();
|
|
4569
4979
|
unsubFontEpoch();
|
|
4980
|
+
assetCache.dispose();
|
|
4570
4981
|
}
|
|
4571
4982
|
};
|
|
4572
4983
|
};
|
|
@@ -5616,6 +6027,8 @@ exports.FONT_FAMILY_MAP = FONT_FAMILY_MAP;
|
|
|
5616
6027
|
exports.FONT_SIZE_MAP = FONT_SIZE_MAP;
|
|
5617
6028
|
exports.LINE_HEIGHT_MAP = LINE_HEIGHT_MAP;
|
|
5618
6029
|
exports.LINK_COLOR = LINK_COLOR;
|
|
6030
|
+
exports.MAX_IMAGE_BYTES = MAX_IMAGE_BYTES;
|
|
6031
|
+
exports.MAX_SVG_BYTES = MAX_SVG_BYTES;
|
|
5619
6032
|
exports.MAX_ZOOM = MAX_ZOOM;
|
|
5620
6033
|
exports.MIN_ZOOM = MIN_ZOOM;
|
|
5621
6034
|
exports.PALM_REJECTION_GRACE_MS = PALM_REJECTION_GRACE_MS;
|
|
@@ -5627,6 +6040,7 @@ exports.SCHEMA_VERSION = SCHEMA_VERSION;
|
|
|
5627
6040
|
exports.UniformGrid = UniformGrid;
|
|
5628
6041
|
exports.VERSION = VERSION;
|
|
5629
6042
|
exports.applyCameraTransform = applyCameraTransform;
|
|
6043
|
+
exports.applySvgColor = applySvgColor;
|
|
5630
6044
|
exports.arrowheadLength = arrowheadLength;
|
|
5631
6045
|
exports.asBatchId = asBatchId;
|
|
5632
6046
|
exports.asClientId = asClientId;
|
|
@@ -5635,6 +6049,7 @@ exports.asGroupId = asGroupId;
|
|
|
5635
6049
|
exports.asNodeId = asNodeId;
|
|
5636
6050
|
exports.attachSync = attachSync;
|
|
5637
6051
|
exports.autoRouteControls = autoRouteControls;
|
|
6052
|
+
exports.blobToDataUri = blobToDataUri;
|
|
5638
6053
|
exports.clampEffectiveScale = clampEffectiveScale;
|
|
5639
6054
|
exports.clampZoom = clampZoom;
|
|
5640
6055
|
exports.clearMeasureCache = clearMeasureCache;
|
|
@@ -5656,6 +6071,7 @@ exports.defineExtension = defineExtension;
|
|
|
5656
6071
|
exports.defineNode = defineNode;
|
|
5657
6072
|
exports.deserializeClipboard = deserializeClipboard;
|
|
5658
6073
|
exports.detectConflicts = detectConflicts;
|
|
6074
|
+
exports.downscaleImageBlob = downscaleImageBlob;
|
|
5659
6075
|
exports.drawArrowhead = drawArrowhead;
|
|
5660
6076
|
exports.drawEdge = drawEdge;
|
|
5661
6077
|
exports.drawMinimapViewport = drawMinimapViewport;
|
|
@@ -5669,6 +6085,7 @@ exports.estimateMarkdownContentHeight = estimateMarkdownContentHeight;
|
|
|
5669
6085
|
exports.exportSelection = exportSelection;
|
|
5670
6086
|
exports.exportSelectionSvg = exportSelectionSvg;
|
|
5671
6087
|
exports.exportViewport = exportViewport;
|
|
6088
|
+
exports.extractSvgDimensions = extractSvgDimensions;
|
|
5672
6089
|
exports.fromSerialized = fromSerialized;
|
|
5673
6090
|
exports.fullVisibleClipResult = fullVisibleClipResult;
|
|
5674
6091
|
exports.getCanvasFont = getCanvasFont;
|
|
@@ -5735,6 +6152,7 @@ exports.rotateVecByAngle = rotateVecByAngle;
|
|
|
5735
6152
|
exports.sampleBezier = sampleBezier;
|
|
5736
6153
|
exports.sampleSelfLoop = sampleSelfLoop;
|
|
5737
6154
|
exports.samplesFor = samplesFor;
|
|
6155
|
+
exports.sanitizeSvg = sanitizeSvg;
|
|
5738
6156
|
exports.sceneBounds = sceneBounds;
|
|
5739
6157
|
exports.screenToWorld = screenToWorld;
|
|
5740
6158
|
exports.selfLoopGeometry = selfLoopGeometry;
|
|
@@ -5748,6 +6166,7 @@ exports.sizeSurface = sizeSurface;
|
|
|
5748
6166
|
exports.storeToJSON = storeToJSON;
|
|
5749
6167
|
exports.subscribeFontEpoch = subscribeFontEpoch;
|
|
5750
6168
|
exports.tangentAtArcLength = tangentAtArcLength;
|
|
6169
|
+
exports.toImageBlob = toImageBlob;
|
|
5751
6170
|
exports.toSerialized = toSerialized;
|
|
5752
6171
|
exports.toggleBold = toggleBold;
|
|
5753
6172
|
exports.toggleCode = toggleCode;
|
|
@@ -5756,6 +6175,8 @@ exports.toggleStrike = toggleStrike;
|
|
|
5756
6175
|
exports.toggleUnderline = toggleUnderline;
|
|
5757
6176
|
exports.tokenize = tokenize;
|
|
5758
6177
|
exports.unionRects = unionRects;
|
|
6178
|
+
exports.validateImageInput = validateImageInput;
|
|
6179
|
+
exports.validateSvgMarkup = validateSvgMarkup;
|
|
5759
6180
|
exports.viewportWorldRect = viewportWorldRect;
|
|
5760
6181
|
exports.withAutoFitHeight = withAutoFitHeight;
|
|
5761
6182
|
exports.worldToNodeLocal = worldToNodeLocal;
|