@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/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 SELECTION_COLOR = "#3b82f6";
4318
+ var DEFAULT_SELECTION_COLOR = "#3b82f6";
3945
4319
  var SELECTION_OUTLINE_PX = 1.5;
3946
- var MARQUEE_FILL = "rgba(59, 130, 246, 0.08)";
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 = SELECTION_COLOR;
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 = SELECTION_COLOR;
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 = SELECTION_COLOR;
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 = SELECTION_COLOR;
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 = SELECTION_COLOR;
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 = SELECTION_COLOR;
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.fillStyle = MARQUEE_FILL;
4430
+ ctx.globalAlpha = MARQUEE_FILL_ALPHA;
4431
+ ctx.fillStyle = color;
4057
4432
  ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
4058
- ctx.strokeStyle = SELECTION_COLOR;
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") continue;
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: "#3b82f6" }
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;