@canvas-harness/core 0.0.1 → 0.0.3

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