gpucanvas 0.4.3 → 0.4.4

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.
@@ -7,6 +7,11 @@ var worldToScreen = (camera, point) => ({
7
7
  x: (point.x - camera.x) * camera.zoom,
8
8
  y: (point.y - camera.y) * camera.zoom
9
9
  });
10
+ var rectToScreen = (camera, rect) => ({
11
+ ...worldToScreen(camera, rect),
12
+ width: rect.width * camera.zoom,
13
+ height: rect.height * camera.zoom
14
+ });
10
15
  var MIN_ZOOM = 0.02;
11
16
  var MAX_ZOOM = 16;
12
17
  var clampZoom = (zoom) => Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, zoom));
@@ -24,8 +29,7 @@ function visibleWorldRect(camera, viewportWidth, viewportHeight) {
24
29
  height: viewportHeight / camera.zoom
25
30
  };
26
31
  }
27
- var containsPoint = (rect, point) => point.x >= rect.x && point.x < rect.x + rect.width && point.y >= rect.y && point.y < rect.y + rect.height;
28
- var intersects = (a, b) => a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;
32
+ var viewOf = (canvas) => visibleWorldRect(canvas.getCamera(), canvas.surface.clientWidth, canvas.surface.clientHeight);
29
33
  function fitCamera(bounds, viewportWidth, viewportHeight, padding = 40) {
30
34
  const zoom = clampZoom(
31
35
  Math.min(
@@ -39,14 +43,61 @@ function fitCamera(bounds, viewportWidth, viewportHeight, padding = 40) {
39
43
  zoom
40
44
  };
41
45
  }
46
+
47
+ // src/elements.ts
48
+ function rectInNode(node, nodeRect, element) {
49
+ if (element === node) return { ...nodeRect };
50
+ const outer = node.getBoundingClientRect();
51
+ const inner = element.getBoundingClientRect();
52
+ const scale = outer.width > 0 ? nodeRect.width / outer.width : 1;
53
+ return {
54
+ x: nodeRect.x + (inner.left - outer.left) * scale,
55
+ y: nodeRect.y + (inner.top - outer.top) * scale,
56
+ width: inner.width * scale,
57
+ height: inner.height * scale
58
+ };
59
+ }
60
+ function elementInNode(node, nodeRect, point) {
61
+ const outer = node.getBoundingClientRect();
62
+ const scale = nodeRect.width > 0 ? outer.width / nodeRect.width : 1;
63
+ const x = outer.left + (point.x - nodeRect.x) * scale;
64
+ const y = outer.top + (point.y - nodeRect.y) * scale;
65
+ let hit = node;
66
+ descend: for (; ; ) {
67
+ for (let child = hit.lastElementChild; child; child = child.previousElementSibling) {
68
+ const box = child.getBoundingClientRect();
69
+ if (box.width > 0 && box.height > 0 && x >= box.left && x < box.right && y >= box.top && y < box.bottom) {
70
+ hit = child;
71
+ continue descend;
72
+ }
73
+ }
74
+ return hit;
75
+ }
76
+ }
77
+ function nodeOfElement(nodes, element) {
78
+ for (let at = element; at; at = at.parentElement) {
79
+ if (nodes.has(at)) return at;
80
+ }
81
+ return null;
82
+ }
83
+
84
+ // src/input.ts
85
+ var markSurface = (element) => {
86
+ element.dataset.gpucanvasSurface = "";
87
+ };
88
+ var isSurface = (target) => target instanceof Element && target.hasAttribute("data-gpucanvas-surface");
89
+ var pointIn = (element, event) => {
90
+ const bounds = element.getBoundingClientRect();
91
+ return { x: event.clientX - bounds.left, y: event.clientY - bounds.top };
92
+ };
42
93
  function bindCameraInput(element, getCamera, onChange) {
43
94
  const onWheel = (event) => {
44
95
  event.preventDefault();
45
- const bounds = element.getBoundingClientRect();
46
96
  const camera = getCamera();
47
97
  if (event.ctrlKey || event.metaKey) {
48
98
  const factor = Math.exp(-event.deltaY * (event.deltaMode === 1 ? 0.05 : 0.01));
49
- onChange(zoomAt(camera, event.clientX - bounds.left, event.clientY - bounds.top, factor));
99
+ const at = pointIn(element, event);
100
+ onChange(zoomAt(camera, at.x, at.y, factor));
50
101
  } else {
51
102
  onChange({
52
103
  ...camera,
@@ -76,8 +127,7 @@ function bindCameraInput(element, getCamera, onChange) {
76
127
  }
77
128
  }
78
129
  if (event.button !== 0 && event.button !== 1 || event.shiftKey) return;
79
- if (event.target !== element && !event.target.hasAttribute?.("data-gpucanvas-surface"))
80
- return;
130
+ if (!isSurface(event.target)) return;
81
131
  drag = { pointerId: event.pointerId, x: event.clientX, y: event.clientY };
82
132
  element.setPointerCapture(event.pointerId);
83
133
  };
@@ -89,13 +139,8 @@ function bindCameraInput(element, getCamera, onChange) {
89
139
  touch.y = event.clientY;
90
140
  const after = pinch();
91
141
  if (!before.distance) return;
92
- const bounds = element.getBoundingClientRect();
93
- const zoomed = zoomAt(
94
- getCamera(),
95
- before.x - bounds.left,
96
- before.y - bounds.top,
97
- after.distance / before.distance
98
- );
142
+ const at = pointIn(element, { clientX: before.x, clientY: before.y });
143
+ const zoomed = zoomAt(getCamera(), at.x, at.y, after.distance / before.distance);
99
144
  onChange({
100
145
  ...zoomed,
101
146
  x: zoomed.x - (after.x - before.x) / zoomed.zoom,
@@ -135,6 +180,113 @@ function bindCameraInput(element, getCamera, onChange) {
135
180
  };
136
181
  }
137
182
 
183
+ // src/rect.ts
184
+ var center = (rect) => ({
185
+ x: rect.x + rect.width / 2,
186
+ y: rect.y + rect.height / 2
187
+ });
188
+ var containsPoint = (rect, point) => point.x >= rect.x && point.x < rect.x + rect.width && point.y >= rect.y && point.y < rect.y + rect.height;
189
+ var intersects = (a, b) => a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;
190
+ var intersection = (a, b) => {
191
+ const x = Math.max(a.x, b.x);
192
+ const y = Math.max(a.y, b.y);
193
+ const right = Math.min(a.x + a.width, b.x + b.width);
194
+ const bottom = Math.min(a.y + a.height, b.y + b.height);
195
+ return right > x && bottom > y ? { x, y, width: right - x, height: bottom - y } : null;
196
+ };
197
+ var contains = (outer, inner) => inner.x >= outer.x && inner.y >= outer.y && inner.x + inner.width <= outer.x + outer.width && inner.y + inner.height <= outer.y + outer.height;
198
+ function unionOf(rects) {
199
+ let only = null;
200
+ let count = 0;
201
+ let minX = Infinity;
202
+ let minY = Infinity;
203
+ let maxX = -Infinity;
204
+ let maxY = -Infinity;
205
+ for (const rect of rects) {
206
+ if (!rect) continue;
207
+ only = rect;
208
+ count++;
209
+ minX = Math.min(minX, rect.x);
210
+ minY = Math.min(minY, rect.y);
211
+ maxX = Math.max(maxX, rect.x + rect.width);
212
+ maxY = Math.max(maxY, rect.y + rect.height);
213
+ }
214
+ if (count < 2) return only;
215
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
216
+ }
217
+ var sameRect = (a, b) => !!a && !!b && Math.abs(a.x - b.x) < 0.5 && Math.abs(a.y - b.y) < 0.5 && Math.abs(a.width - b.width) < 0.5 && Math.abs(a.height - b.height) < 0.5;
218
+
219
+ // src/types.ts
220
+ var DEFAULT_CAMERA = { x: 0, y: 0, zoom: 1 };
221
+ var DEFAULT_SELECTION_COLOR = "#4f8cff";
222
+ var SELECTION_WIDTH = 2;
223
+ var HOVER_WIDTH = 1;
224
+
225
+ // src/base.ts
226
+ var styleNode = (element) => Object.assign(element.style, { boxSizing: "border-box", contain: "strict", overflow: "hidden" });
227
+ function createBase(host, options, onCamera) {
228
+ const surface = document.createElement("div");
229
+ markSurface(surface);
230
+ Object.assign(surface.style, {
231
+ position: "relative",
232
+ width: "100%",
233
+ height: "100%",
234
+ overflow: "hidden",
235
+ touchAction: "none"
236
+ });
237
+ host.append(surface);
238
+ const nodes = /* @__PURE__ */ new Map();
239
+ let camera = options.camera ?? DEFAULT_CAMERA;
240
+ const setCamera = (next) => {
241
+ camera = next;
242
+ onCamera(camera);
243
+ options.onCameraChange?.(camera);
244
+ };
245
+ const unbindInput = bindCameraInput(surface, () => camera, setCamera);
246
+ const nodeAt = (point) => {
247
+ let top = null;
248
+ for (const node of nodes.values()) if (containsPoint(node.rect, point)) top = node;
249
+ return top;
250
+ };
251
+ const api = {
252
+ surface,
253
+ getNodeRect: (element) => {
254
+ const node = nodes.get(element);
255
+ return node ? { ...node.rect } : null;
256
+ },
257
+ getNodesIn(rect) {
258
+ const hits = [];
259
+ for (const node of nodes.values()) if (intersects(node.rect, rect)) hits.push(node.element);
260
+ return hits;
261
+ },
262
+ getNodeOf: (element) => nodeOfElement(nodes, element),
263
+ getElementAt(point) {
264
+ const top = nodeAt(point);
265
+ return top ? elementInNode(top.element, top.rect, point) : null;
266
+ },
267
+ getCamera: () => camera,
268
+ setCamera,
269
+ screenToWorld: (point) => screenToWorld(camera, point),
270
+ worldToScreen: (point) => worldToScreen(camera, point),
271
+ fit(padding) {
272
+ const bounds = unionOf([...nodes.values()].map((node) => node.rect));
273
+ if (bounds) setCamera(fitCamera(bounds, surface.clientWidth, surface.clientHeight, padding));
274
+ }
275
+ };
276
+ return {
277
+ surface,
278
+ nodes,
279
+ camera: () => camera,
280
+ view: () => viewOf(api),
281
+ nodeAt,
282
+ api,
283
+ destroy() {
284
+ unbindInput();
285
+ surface.remove();
286
+ }
287
+ };
288
+ }
289
+
138
290
  // src/effects.ts
139
291
  var defineEffect = (definition) => Object.freeze({
140
292
  animated: false,
@@ -518,84 +670,20 @@ var EFFECTS = {
518
670
  grayscale
519
671
  };
520
672
 
521
- // src/elements.ts
522
- function rectInNode(node, nodeRect, element) {
523
- if (element === node) return { ...nodeRect };
524
- const outer = node.getBoundingClientRect();
525
- const inner = element.getBoundingClientRect();
526
- const scale = outer.width > 0 ? nodeRect.width / outer.width : 1;
527
- return {
528
- x: nodeRect.x + (inner.left - outer.left) * scale,
529
- y: nodeRect.y + (inner.top - outer.top) * scale,
530
- width: inner.width * scale,
531
- height: inner.height * scale
532
- };
533
- }
534
- function elementInNode(node, nodeRect, point) {
535
- const outer = node.getBoundingClientRect();
536
- const scale = nodeRect.width > 0 ? outer.width / nodeRect.width : 1;
537
- const x = outer.left + (point.x - nodeRect.x) * scale;
538
- const y = outer.top + (point.y - nodeRect.y) * scale;
539
- let hit = node;
540
- descend: for (; ; ) {
541
- for (let child = hit.lastElementChild; child; child = child.previousElementSibling) {
542
- const box = child.getBoundingClientRect();
543
- if (box.width > 0 && box.height > 0 && x >= box.left && x < box.right && y >= box.top && y < box.bottom) {
544
- hit = child;
545
- continue descend;
546
- }
547
- }
548
- return hit;
549
- }
550
- }
551
- function nodeOfElement(nodes, element) {
552
- for (let at = element; at; at = at.parentElement) {
553
- if (nodes.has(at)) return at;
554
- }
555
- return null;
556
- }
557
-
558
- // src/node-style.ts
559
- function applyNodeSize(element, rect) {
560
- element.style.width = `${rect.width}px`;
561
- element.style.height = `${rect.height}px`;
562
- }
563
- function applyNodeBase(element) {
564
- element.style.boxSizing = "border-box";
565
- element.style.contain = "strict";
566
- element.style.overflow = "hidden";
567
- }
568
- function boundsOf(rects) {
569
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
570
- for (const rect of rects) {
571
- minX = Math.min(minX, rect.x);
572
- minY = Math.min(minY, rect.y);
573
- maxX = Math.max(maxX, rect.x + rect.width);
574
- maxY = Math.max(maxY, rect.y + rect.height);
575
- }
576
- return minX === Infinity ? null : { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
577
- }
578
-
579
- // src/types.ts
580
- var DEFAULT_CAMERA = { x: 0, y: 0, zoom: 1 };
581
- var DEFAULT_SELECTION_COLOR = "#4f8cff";
582
- var SELECTION_WIDTH = 2;
583
- var HOVER_WIDTH = 1;
584
-
585
673
  // src/dom-backend.ts
586
674
  function createDomCanvas(host, options) {
587
- const surface = document.createElement("div");
588
- surface.dataset.gpucanvasSurface = "";
589
- Object.assign(surface.style, {
590
- position: "relative",
591
- width: "100%",
592
- height: "100%",
593
- overflow: "hidden",
594
- touchAction: "none",
595
- background: options.background ?? "transparent"
596
- });
675
+ let frameMs = 0;
676
+ const apply = (camera) => {
677
+ const start = performance.now();
678
+ world.style.transform = `scale(${camera.zoom}) translate(${-camera.x}px, ${-camera.y}px)`;
679
+ world.style.setProperty("--gpucanvas-zoom", String(camera.zoom));
680
+ frameMs = performance.now() - start;
681
+ };
682
+ const base = createBase(host, options, apply);
683
+ const { surface, nodes } = base;
684
+ surface.style.background = options.background ?? "transparent";
597
685
  const world = document.createElement("div");
598
- world.dataset.gpucanvasSurface = "";
686
+ markSurface(world);
599
687
  Object.assign(world.style, {
600
688
  position: "absolute",
601
689
  left: "0",
@@ -607,8 +695,6 @@ function createDomCanvas(host, options) {
607
695
  Object.assign(overlay.style, { position: "absolute", left: "0", top: "0", zIndex: "1" });
608
696
  world.append(overlay);
609
697
  surface.append(world);
610
- host.append(surface);
611
- const nodes = /* @__PURE__ */ new Map();
612
698
  const ring = (color, width) => `calc(${width}px / var(--gpucanvas-zoom)) solid ${color}`;
613
699
  const selectionColor = options.selectionColor ?? DEFAULT_SELECTION_COLOR;
614
700
  const selectionRing = ring(selectionColor, SELECTION_WIDTH);
@@ -619,7 +705,7 @@ function createDomCanvas(host, options) {
619
705
  const boxes = /* @__PURE__ */ new Map();
620
706
  const elementRect = (element) => {
621
707
  const node = element.isConnected ? nodeOfElement(nodes, element) : null;
622
- return node ? rectInNode(node, nodes.get(node), element) : null;
708
+ return node ? rectInNode(node, nodes.get(node).rect, element) : null;
623
709
  };
624
710
  let boxFrame = 0;
625
711
  const placeBoxes = () => {
@@ -693,55 +779,33 @@ function createDomCanvas(host, options) {
693
779
  element.animate(keyframes, { duration: period * 1e3, iterations: Infinity })
694
780
  );
695
781
  };
696
- let camera = options.camera ?? DEFAULT_CAMERA;
697
- let frameMs = 0;
698
- const apply = () => {
699
- const start = performance.now();
700
- world.style.transform = `scale(${camera.zoom}) translate(${-camera.x}px, ${-camera.y}px)`;
701
- world.style.setProperty("--gpucanvas-zoom", String(camera.zoom));
702
- frameMs = performance.now() - start;
782
+ const updateNode = (element, rect) => {
783
+ nodes.set(element, { element, rect });
784
+ element.style.left = `${rect.x}px`;
785
+ element.style.top = `${rect.y}px`;
786
+ element.style.width = `${rect.width}px`;
787
+ element.style.height = `${rect.height}px`;
703
788
  };
704
- const setCamera = (next) => {
705
- camera = next;
706
- apply();
707
- options.onCameraChange?.(camera);
708
- };
709
- const unbindInput = bindCameraInput(surface, () => camera, setCamera);
710
- apply();
789
+ apply(base.camera());
711
790
  return {
791
+ ...base.api,
712
792
  mode: "dom",
713
- surface,
714
793
  addNode(element, rect) {
715
- applyNodeBase(element);
794
+ styleNode(element);
716
795
  element.style.position = "absolute";
717
- nodes.set(element, rect);
718
- this.updateNode(element, rect);
796
+ updateNode(element, rect);
719
797
  world.append(element);
720
798
  },
721
- updateNode(element, rect) {
722
- nodes.set(element, rect);
723
- element.style.left = `${rect.x}px`;
724
- element.style.top = `${rect.y}px`;
725
- applyNodeSize(element, rect);
726
- },
799
+ updateNode,
727
800
  removeNode(element) {
728
801
  clearEffect(element);
729
802
  nodes.delete(element);
803
+ selection.delete(element);
730
804
  outlinedNodes.delete(element);
731
805
  element.style.outline = "";
732
806
  element.remove();
733
807
  },
734
- getNodeRect: (element) => nodes.get(element) ?? null,
735
- getNodesIn(rect) {
736
- return [...nodes].filter(([, node]) => intersects(node, rect)).map(([element]) => element);
737
- },
738
- getNodeOf: (element) => nodeOfElement(nodes, element),
739
808
  getElementRect: elementRect,
740
- getElementAt(point) {
741
- let top = null;
742
- for (const [element, rect] of nodes) if (containsPoint(rect, point)) top = element;
743
- return top ? elementInNode(top, nodes.get(top), point) : null;
744
- },
745
809
  setSelection(elements) {
746
810
  selection = new Set(elements);
747
811
  drawOutlines();
@@ -754,10 +818,6 @@ function createDomCanvas(host, options) {
754
818
  setEffect(element, effect, params) {
755
819
  if (nodes.has(element)) applyEffect(element, effect, params);
756
820
  },
757
- getCamera: () => camera,
758
- setCamera,
759
- screenToWorld: (point) => screenToWorld(camera, point),
760
- worldToScreen: (point) => worldToScreen(camera, point),
761
821
  // The browser repaints DOM nodes itself.
762
822
  invalidate() {
763
823
  },
@@ -765,14 +825,10 @@ function createDomCanvas(host, options) {
765
825
  setBackground(color) {
766
826
  surface.style.background = color;
767
827
  },
768
- fit(padding) {
769
- const bounds = boundsOf(nodes.values());
770
- if (bounds) setCamera(fitCamera(bounds, surface.clientWidth, surface.clientHeight, padding));
771
- },
772
828
  getStats() {
773
- const view = visibleWorldRect(camera, surface.clientWidth, surface.clientHeight);
829
+ const view = base.view();
774
830
  let visible = 0;
775
- for (const rect of nodes.values()) if (intersects(rect, view)) visible++;
831
+ for (const node of nodes.values()) if (intersects(node.rect, view)) visible++;
776
832
  return {
777
833
  mode: "dom",
778
834
  nodes: nodes.size,
@@ -786,17 +842,52 @@ function createDomCanvas(host, options) {
786
842
  },
787
843
  destroy() {
788
844
  cancelAnimationFrame(boxFrame);
789
- unbindInput();
790
- surface.remove();
845
+ base.destroy();
791
846
  }
792
847
  };
793
848
  }
794
849
 
850
+ // src/shelves.ts
851
+ var Shelves = class {
852
+ constructor(width, height, tolerance) {
853
+ this.width = width;
854
+ this.height = height;
855
+ this.tolerance = tolerance;
856
+ }
857
+ width;
858
+ height;
859
+ tolerance;
860
+ shelves = [];
861
+ /** Height taken by rows so far. */
862
+ filled = 0;
863
+ /** Top left corner of a free `w` x `h` box, or null when it does not fit. */
864
+ place(w, h) {
865
+ if (w > this.width || h > this.height) return null;
866
+ let shelf = this.shelves.find(
867
+ (candidate) => candidate.height >= h && candidate.height <= h * this.tolerance && candidate.x + w <= this.width
868
+ );
869
+ if (!shelf) {
870
+ if (this.filled + h > this.height) return null;
871
+ shelf = { x: 0, y: this.filled, height: h };
872
+ this.shelves.push(shelf);
873
+ this.filled += h;
874
+ }
875
+ const at = { x: shelf.x, y: shelf.y };
876
+ shelf.x += w;
877
+ return at;
878
+ }
879
+ clear() {
880
+ this.shelves = [];
881
+ this.filled = 0;
882
+ }
883
+ };
884
+
795
885
  // src/atlas.ts
796
886
  var PADDING = 1;
797
887
  var Atlas = class {
798
- constructor(device, size, layerCount) {
888
+ constructor(device, size, layerCount, keep) {
799
889
  this.device = device;
890
+ this.keep = keep;
800
891
  this.size = size;
801
892
  this.texture = device.createTexture({
802
893
  label: "gpucanvas atlas",
@@ -806,8 +897,7 @@ var Atlas = class {
806
897
  });
807
898
  this.view = this.texture.createView({ dimension: "2d-array" });
808
899
  this.layers = Array.from({ length: layerCount }, () => ({
809
- shelves: [],
810
- nextY: 0,
900
+ shelves: new Shelves(size, size, 1.5),
811
901
  generation: 0,
812
902
  lastUsed: -1,
813
903
  lastAllocated: -1
@@ -815,25 +905,21 @@ var Atlas = class {
815
905
  this.spare = layerCount - 1;
816
906
  }
817
907
  device;
908
+ keep;
818
909
  texture;
819
910
  view;
820
911
  size;
821
- /** Slots to rescue when their layer is recycled, most important first. */
822
- keep = () => [];
823
912
  layers;
824
913
  spare;
825
914
  frame = 0;
826
915
  /** The layer `allocatePinned` fills; it is never recycled. */
827
916
  pinned = 0;
828
- get layerCount() {
829
- return this.layers.length;
830
- }
831
917
  get usedLayers() {
832
- return this.layers.filter((layer, index) => index !== this.spare && layer.nextY > 0).length;
918
+ return this.layers.filter((layer, index) => index !== this.spare && layer.shelves.filled > 0).length;
833
919
  }
834
920
  /** How much of the pinned layer is taken, 0 to 1. */
835
921
  get pinnedFill() {
836
- return this.layers[this.pinned].nextY / this.size;
922
+ return this.layers[this.pinned].shelves.filled / this.size;
837
923
  }
838
924
  nextFrame() {
839
925
  this.frame++;
@@ -909,32 +995,23 @@ var Atlas = class {
909
995
  }
910
996
  allocateIn(index, w, h) {
911
997
  const layer = this.layers[index];
912
- let shelf = layer.shelves.find(
913
- (candidate) => candidate.height >= h && candidate.height <= h * 1.5 && candidate.x + w <= this.size
914
- );
915
- if (!shelf) {
916
- if (layer.nextY + h > this.size) return null;
917
- shelf = { y: layer.nextY, height: h, x: 0 };
918
- layer.shelves.push(shelf);
919
- layer.nextY += h;
920
- }
998
+ const at = layer.shelves.place(w, h);
999
+ if (!at) return null;
921
1000
  const slot = {
922
1001
  layer: index,
923
- x: shelf.x,
924
- y: shelf.y,
1002
+ x: at.x,
1003
+ y: at.y,
925
1004
  width: w - PADDING,
926
1005
  height: h - PADDING,
927
1006
  generation: layer.generation
928
1007
  };
929
- shelf.x += w;
930
1008
  layer.lastUsed = this.frame;
931
1009
  layer.lastAllocated = this.frame;
932
1010
  return slot;
933
1011
  }
934
1012
  evict(index) {
935
1013
  const layer = this.layers[index];
936
- layer.shelves = [];
937
- layer.nextY = 0;
1014
+ layer.shelves.clear();
938
1015
  layer.generation++;
939
1016
  }
940
1017
  destroy() {
@@ -942,7 +1019,55 @@ var Atlas = class {
942
1019
  }
943
1020
  };
944
1021
 
1022
+ // src/capture.ts
1023
+ function captureRegion(device, source, region, width, height) {
1024
+ const texture = device.createTexture({
1025
+ size: { width, height },
1026
+ format: "rgba8unorm",
1027
+ usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
1028
+ });
1029
+ try {
1030
+ device.queue.drawElementImageToTexture(
1031
+ {
1032
+ source,
1033
+ sourceX: region.x,
1034
+ sourceY: region.y,
1035
+ sourceWidth: region.width,
1036
+ sourceHeight: region.height
1037
+ },
1038
+ { texture, size: { width, height }, premultipliedAlpha: false }
1039
+ );
1040
+ } catch (error) {
1041
+ texture.destroy();
1042
+ throw error;
1043
+ }
1044
+ return readTexture(device, texture, width, height);
1045
+ }
1046
+ async function readTexture(device, texture, width, height) {
1047
+ const bytesPerRow = Math.ceil(width * 4 / 256) * 256;
1048
+ const buffer = device.createBuffer({
1049
+ size: bytesPerRow * height,
1050
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
1051
+ });
1052
+ const encoder = device.createCommandEncoder();
1053
+ encoder.copyTextureToBuffer({ texture }, { buffer, bytesPerRow }, { width, height });
1054
+ device.queue.submit([encoder.finish()]);
1055
+ try {
1056
+ await buffer.mapAsync(GPUMapMode.READ);
1057
+ const rows = new Uint8Array(buffer.getMappedRange());
1058
+ const pixels = new Uint8ClampedArray(width * height * 4);
1059
+ for (let y = 0; y < height; y++) {
1060
+ pixels.set(rows.subarray(y * bytesPerRow, y * bytesPerRow + width * 4), y * width * 4);
1061
+ }
1062
+ return new ImageData(pixels, width, height);
1063
+ } finally {
1064
+ buffer.destroy();
1065
+ texture.destroy();
1066
+ }
1067
+ }
1068
+
945
1069
  // src/composite-shader.ts
1070
+ var KIND = { snapshot: 0, placeholder: 1, ring: 2 };
946
1071
  var prelude = (
947
1072
  /* wgsl */
948
1073
  `
@@ -985,7 +1110,7 @@ struct Varyings {
985
1110
  fn vs(@builtin(vertex_index) vertex: u32, instance: Instance) -> Varyings {
986
1111
  let corner = vec2f(f32(vertex & 1u), f32(vertex >> 1u));
987
1112
  // A ring's quad reaches past its element by the ring's width, an effect's by its padding.
988
- let ring = instance.info.y > 1.5;
1113
+ let ring = instance.info.y > ${KIND.ring - 0.5};
989
1114
  let grow = select(instance.info.z, instance.info.z / view.zoom, ring);
990
1115
  let start = instance.rect.xy - grow;
991
1116
  let end = instance.rect.xy + instance.rect.zw + grow;
@@ -1026,12 +1151,12 @@ fn fs(in: Varyings) -> @location(0) vec4f {
1026
1151
  // Derivatives first: WGSL only allows them in uniform control flow.
1027
1152
  let dx = dpdx(in.texel);
1028
1153
  let dy = dpdy(in.texel);
1029
- if (in.info.y > 1.5) {
1154
+ if (in.info.y > ${KIND.ring - 0.5}) {
1030
1155
  // The ring covers what lies outside the element; the element itself shows through.
1031
1156
  let outside = max(in.bounds.xy - in.texel, in.texel - in.bounds.zw);
1032
1157
  return select(vec4f(0.0), in.color, max(outside.x, outside.y) > 0.0);
1033
1158
  }
1034
- if (in.info.y > 0.5) {
1159
+ if (in.info.y > ${KIND.placeholder - 0.5}) {
1035
1160
  return view.placeholder;
1036
1161
  }
1037
1162
  let layer = i32(in.info.x);
@@ -1108,12 +1233,14 @@ fn fs(in: Varyings) -> @location(0) vec4f {
1108
1233
  // src/gpu-renderer.ts
1109
1234
  var FLOATS_PER_INSTANCE = 16;
1110
1235
  var CompositeRenderer = class {
1111
- constructor(device, context, format, atlas, background, placeholder) {
1236
+ constructor(device, context, format, atlas, background, placeholder, onEffectReady) {
1112
1237
  this.device = device;
1113
1238
  this.context = context;
1239
+ this.format = format;
1114
1240
  this.atlas = atlas;
1115
1241
  this.background = background;
1116
1242
  this.placeholder = placeholder;
1243
+ this.onEffectReady = onEffectReady;
1117
1244
  this.bindGroupLayout = device.createBindGroupLayout({
1118
1245
  entries: [
1119
1246
  { binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: {} },
@@ -1127,9 +1254,8 @@ var CompositeRenderer = class {
1127
1254
  });
1128
1255
  this.sampler = device.createSampler({ magFilter: "linear", minFilter: "linear" });
1129
1256
  this.pipeline = device.createRenderPipeline(
1130
- this.pipelineDescriptor("gpucanvas composite", compositeShader, format)
1257
+ this.pipelineDescriptor("gpucanvas composite", compositeShader)
1131
1258
  );
1132
- this.format = format;
1133
1259
  this.uniforms = device.createBuffer({
1134
1260
  size: this.uniformData.byteLength,
1135
1261
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
@@ -1146,10 +1272,14 @@ var CompositeRenderer = class {
1146
1272
  }
1147
1273
  device;
1148
1274
  context;
1275
+ format;
1149
1276
  atlas;
1150
1277
  background;
1151
1278
  placeholder;
1279
+ onEffectReady;
1152
1280
  instances = new Float32Array(FLOATS_PER_INSTANCE * 4096);
1281
+ count = 0;
1282
+ runs = [];
1153
1283
  instanceBuffer;
1154
1284
  uniforms;
1155
1285
  uniformData = new Float32Array(20);
@@ -1159,11 +1289,8 @@ var CompositeRenderer = class {
1159
1289
  sampler;
1160
1290
  /** Effect pipelines, null while one compiles. Compiling never blocks a frame. */
1161
1291
  effects = /* @__PURE__ */ new Map();
1162
- /** Called when an effect pipeline is ready, so the caller can draw it. */
1163
- onEffectReady = () => {
1164
- };
1165
- format;
1166
- pipelineDescriptor(label, code, format) {
1292
+ failed = /* @__PURE__ */ new Set();
1293
+ pipelineDescriptor(label, code) {
1167
1294
  const module = this.device.createShaderModule({ label, code });
1168
1295
  return {
1169
1296
  label,
@@ -1189,7 +1316,7 @@ var CompositeRenderer = class {
1189
1316
  entryPoint: "fs",
1190
1317
  targets: [
1191
1318
  {
1192
- format,
1319
+ format: this.format,
1193
1320
  // Snapshots are requested premultiplied.
1194
1321
  blend: {
1195
1322
  color: { srcFactor: "one", dstFactor: "one-minus-src-alpha" },
@@ -1206,27 +1333,82 @@ var CompositeRenderer = class {
1206
1333
  if (this.effects.has(effect)) return this.effects.get(effect) !== null;
1207
1334
  this.effects.set(effect, null);
1208
1335
  this.device.createRenderPipelineAsync(
1209
- this.pipelineDescriptor(
1210
- `gpucanvas effect ${effect.name}`,
1211
- effectShader(effect.wgsl),
1212
- this.format
1213
- )
1336
+ this.pipelineDescriptor(`gpucanvas effect ${effect.name}`, effectShader(effect.wgsl))
1214
1337
  ).then((pipeline) => {
1215
1338
  this.effects.set(effect, pipeline);
1216
1339
  this.onEffectReady();
1217
- }).catch((error) => console.error(`[gpucanvas] effect "${effect.name}" failed`, error));
1340
+ }).catch((error) => {
1341
+ this.failed.add(effect);
1342
+ console.error(`[gpucanvas] effect "${effect.name}" failed`, error);
1343
+ });
1218
1344
  return false;
1219
1345
  }
1220
- /** Grows the CPU and GPU instance storage to hold at least `count` instances. */
1221
- reserve(count) {
1222
- const floats = count * FLOATS_PER_INSTANCE;
1223
- if (this.instances.length >= floats) return;
1224
- this.instances = new Float32Array(Math.max(floats, this.instances.length * 2));
1225
- this.instanceBuffer.destroy();
1226
- this.instanceBuffer = this.createInstanceBuffer();
1346
+ /** Whether `effect`'s pipeline failed to compile; it never draws then. */
1347
+ effectFailed(effect) {
1348
+ return this.failed.has(effect);
1349
+ }
1350
+ /** Starts a frame with room for at least `capacity` quads. */
1351
+ begin(capacity) {
1352
+ const floats = capacity * FLOATS_PER_INSTANCE;
1353
+ if (this.instances.length < floats) {
1354
+ this.instances = new Float32Array(Math.max(floats, this.instances.length * 2));
1355
+ this.instanceBuffer.destroy();
1356
+ this.instanceBuffer = this.createInstanceBuffer();
1357
+ }
1358
+ this.count = 0;
1359
+ this.runs.length = 0;
1360
+ }
1361
+ /** Room for the next quad, in the run of `effect`; returns its first float. */
1362
+ next(effect) {
1363
+ const last = this.runs.at(-1);
1364
+ if (last && last.effect === effect) last.count++;
1365
+ else this.runs.push({ effect, start: this.count, count: 1 });
1366
+ return this.count++ * FLOATS_PER_INSTANCE;
1367
+ }
1368
+ writeRect(offset, rect) {
1369
+ const data = this.instances;
1370
+ data[offset] = rect.x;
1371
+ data[offset + 1] = rect.y;
1372
+ data[offset + 2] = rect.width;
1373
+ data[offset + 3] = rect.height;
1374
+ }
1375
+ writeSlot(offset, slot) {
1376
+ const data = this.instances;
1377
+ this.atlas.touch(slot.layer);
1378
+ data[offset + 4] = slot.x;
1379
+ data[offset + 5] = slot.y;
1380
+ data[offset + 6] = slot.x + slot.width;
1381
+ data[offset + 7] = slot.y + slot.height;
1382
+ data[offset + 8] = slot.layer;
1383
+ data[offset + 9] = KIND.snapshot;
1384
+ }
1385
+ /** `rect` showing the snapshot in `slot`, or the placeholder color while there is none. */
1386
+ quad(rect, slot) {
1387
+ const offset = this.next(null);
1388
+ this.writeRect(offset, rect);
1389
+ if (slot) this.writeSlot(offset, slot);
1390
+ else this.instances[offset + 9] = KIND.placeholder;
1391
+ this.instances[offset + 10] = 0;
1227
1392
  }
1228
- draw(camera, cssWidth, cssHeight, dpr, count, runs, effects) {
1229
- const { device } = this;
1393
+ /** `rect` drawn through `effect` from the snapshot in `slot`, reaching `pad` past its edges. */
1394
+ effectQuad(rect, slot, effect, pad, params) {
1395
+ const offset = this.next(effect);
1396
+ this.writeRect(offset, rect);
1397
+ this.writeSlot(offset, slot);
1398
+ this.instances[offset + 10] = pad;
1399
+ this.instances.set(params, offset + 12);
1400
+ }
1401
+ /** An outline `width` CSS pixels wide just outside `rect`, in a premultiplied color. */
1402
+ ring(rect, color, width) {
1403
+ const offset = this.next(null);
1404
+ this.writeRect(offset, rect);
1405
+ this.instances.set(color, offset + 4);
1406
+ this.instances[offset + 9] = KIND.ring;
1407
+ this.instances[offset + 10] = width;
1408
+ }
1409
+ /** Draws the quads listed since `begin`. */
1410
+ draw(camera, cssWidth, cssHeight, dpr, effects) {
1411
+ const { device, count, runs } = this;
1230
1412
  this.uniformData.set([
1231
1413
  cssWidth,
1232
1414
  cssHeight,
@@ -1269,7 +1451,7 @@ var CompositeRenderer = class {
1269
1451
  pass.setBindGroup(0, this.bindGroup);
1270
1452
  pass.setVertexBuffer(0, this.instanceBuffer);
1271
1453
  let current = null;
1272
- for (const run of runs.length ? runs : [{ effect: null, start: 0, count }]) {
1454
+ for (const run of runs) {
1273
1455
  const pipeline = run.effect && this.effects.get(run.effect) || this.pipeline;
1274
1456
  if (pipeline !== current) pass.setPipeline(current = pipeline);
1275
1457
  pass.draw(4, run.count, 0, run.start);
@@ -1297,21 +1479,17 @@ var Page = class {
1297
1479
  element;
1298
1480
  width;
1299
1481
  height;
1300
- /** Nodes currently placed on this page. */
1301
- count = 0;
1302
1482
  /** Has the `drawable` attribute and is rendered; parked pages use `content-visibility: hidden`. */
1303
1483
  drawable = false;
1304
1484
  /** Frame in which the page became drawable. Its snapshot exists from that frame's paint on. */
1305
1485
  promotedFrame = -1;
1306
- /** Kept drawable for the live (hit-testable) or focused node. */
1307
- pinned = false;
1308
1486
  /** Registered for hit testing with `updateElementGeometry`. */
1309
1487
  registered = false;
1310
- shelves = [];
1311
- nextY = 0;
1488
+ shelves;
1312
1489
  constructor(width, height) {
1313
1490
  this.width = width;
1314
1491
  this.height = height;
1492
+ this.shelves = new Shelves(width + GAP, height + GAP, 1.25);
1315
1493
  this.element = document.createElement("div");
1316
1494
  this.element.dataset.gpucanvasPage = "";
1317
1495
  Object.assign(this.element.style, {
@@ -1330,25 +1508,8 @@ var Page = class {
1330
1508
  }
1331
1509
  /** Reserves a spot for a `width` x `height` node, or returns null when the page is full. */
1332
1510
  place(width, height) {
1333
- const w = Math.ceil(width) + GAP;
1334
- const h = Math.ceil(height) + GAP;
1335
- if (w > this.width + GAP || h > this.height + GAP) return null;
1336
- let shelf = this.shelves.find(
1337
- (candidate) => candidate.height >= h && candidate.height <= h * 1.25 && candidate.x + w <= this.width + GAP
1338
- );
1339
- if (!shelf) {
1340
- if (this.nextY + h > this.height + GAP) return null;
1341
- shelf = { y: this.nextY, height: h, x: 0 };
1342
- this.shelves.push(shelf);
1343
- this.nextY += h;
1344
- }
1345
- const slot = { page: this, x: shelf.x, y: shelf.y };
1346
- shelf.x += w;
1347
- this.count++;
1348
- return slot;
1349
- }
1350
- release() {
1351
- this.count--;
1511
+ const at = this.shelves.place(Math.ceil(width) + GAP, Math.ceil(height) + GAP);
1512
+ return at && { page: this, ...at };
1352
1513
  }
1353
1514
  setDrawable(drawable, frame) {
1354
1515
  if (this.drawable === drawable) return;
@@ -1374,8 +1535,6 @@ var TILE_PIXELS = 512;
1374
1535
  var CONTENT_SETTLE_MS = 150;
1375
1536
  var SMALL_NODE_PX = 200;
1376
1537
  var SMALL_REFRESH_MS = 250;
1377
- var right = (rect) => rect.x + rect.width;
1378
- var bottom = (rect) => rect.y + rect.height;
1379
1538
  var SETTLE_MS = 120;
1380
1539
  var USE_EVENTS = ["click", "input", "change", "keydown"];
1381
1540
  var USED_MS = 1e3;
@@ -1387,6 +1546,7 @@ var THUMBNAIL_REFRESH_MS = 1e3;
1387
1546
  var MAX_PROMOTIONS_PER_FRAME = 8;
1388
1547
  var SLOW_FRAME_MS = 22;
1389
1548
  var FAST_FRAME_MS = 12;
1549
+ var MAX_CAPTURE_TRIES = 20;
1390
1550
  var MISSING = 0;
1391
1551
  var DIRTY = 1;
1392
1552
  var BLURRY = 2;
@@ -1399,14 +1559,6 @@ function rasterScaleFor(deviceScale) {
1399
1559
  const scale = 2 ** Math.ceil(Math.log2(deviceScale) - 0.14);
1400
1560
  return Math.min(MAX_DETAIL_SCALE, Math.max(MIN_RASTER_SCALE, scale));
1401
1561
  }
1402
- var intersection = (a, b) => {
1403
- const x = Math.max(a.x, b.x);
1404
- const y = Math.max(a.y, b.y);
1405
- const right2 = Math.min(a.x + a.width, b.x + b.width);
1406
- const bottom2 = Math.min(a.y + a.height, b.y + b.height);
1407
- return right2 > x && bottom2 > y ? { x, y, width: right2 - x, height: bottom2 - y } : null;
1408
- };
1409
- var contains = (outer, inner) => inner.x >= outer.x && inner.y >= outer.y && inner.x + inner.width <= outer.x + outer.width && inner.y + inner.height <= outer.y + outer.height;
1410
1562
  function cssColorToRgba(color) {
1411
1563
  const context = document.createElement("canvas").getContext("2d");
1412
1564
  context.fillStyle = color;
@@ -1415,37 +1567,58 @@ function cssColorToRgba(color) {
1415
1567
  return [r / 255, g / 255, b / 255, a / 255];
1416
1568
  }
1417
1569
  var premultiplied = ([r, g, b, a]) => [r * a, g * a, b * a, a];
1418
- var distanceToCenter = (rect, view) => Math.hypot(
1419
- rect.x + rect.width / 2 - (view.x + view.width / 2),
1420
- rect.y + rect.height / 2 - (view.y + view.height / 2)
1421
- );
1570
+ var distanceToCenter = (rect, view) => {
1571
+ const a = center(rect);
1572
+ const b = center(view);
1573
+ return Math.hypot(a.x - b.x, a.y - b.y);
1574
+ };
1422
1575
  async function createGpuCanvas(host, options) {
1423
1576
  const adapter = await navigator.gpu.requestAdapter({ powerPreference: "high-performance" });
1424
1577
  if (!adapter) throw new Error("gpucanvas: no WebGPU adapter");
1425
1578
  const device = await adapter.requestDevice();
1426
- const surface = document.createElement("div");
1427
- surface.dataset.gpucanvasSurface = "";
1428
- Object.assign(surface.style, {
1429
- position: "relative",
1430
- width: "100%",
1431
- height: "100%",
1432
- overflow: "hidden",
1433
- touchAction: "none"
1434
- });
1435
1579
  const canvas = document.createElement("canvas");
1436
1580
  canvas.setAttribute("content", "drawable");
1437
- canvas.dataset.gpucanvasSurface = "";
1581
+ markSurface(canvas);
1438
1582
  Object.assign(canvas.style, { display: "block", width: "100%", height: "100%" });
1439
- surface.append(canvas);
1440
- host.append(surface);
1441
1583
  const context = canvas.getContext("webgpu");
1442
1584
  const format = navigator.gpu.getPreferredCanvasFormat();
1443
- context.configure({ device, format, alphaMode: "opaque" });
1585
+ try {
1586
+ if (!context) throw new Error("gpucanvas: no WebGPU canvas context");
1587
+ context.configure({ device, format, alphaMode: "opaque" });
1588
+ } catch (error) {
1589
+ device.destroy();
1590
+ throw error;
1591
+ }
1592
+ const base = createBase(host, options, () => {
1593
+ lastCameraChange = performance.now();
1594
+ setLive(null);
1595
+ clearTimeout(settleTimer);
1596
+ settleTimer = window.setTimeout(() => {
1597
+ pickLive();
1598
+ schedule();
1599
+ }, SETTLE_MS);
1600
+ schedule();
1601
+ });
1602
+ const { surface, nodes } = base;
1603
+ surface.append(canvas);
1444
1604
  const atlasSize = Math.min(options.atlasSize ?? 4096, device.limits.maxTextureDimension2D);
1445
1605
  const atlas = new Atlas(
1446
1606
  device,
1447
1607
  atlasSize,
1448
- Math.min(options.atlasLayers ?? 5, device.limits.maxTextureArrayLayers)
1608
+ Math.min(options.atlasLayers ?? 5, device.limits.maxTextureArrayLayers),
1609
+ function* () {
1610
+ const onScreen = (tile) => intersects(tile.rect, frameView);
1611
+ for (const node of visible) yield node.slot;
1612
+ for (const node of visible) {
1613
+ for (const tile of node.tiles) {
1614
+ if (tile.scale === detailScale && !tile.stale && onScreen(tile)) yield tile.slot;
1615
+ }
1616
+ }
1617
+ for (const node of visible) {
1618
+ yield node.preview;
1619
+ for (const tile of node.tiles) if (onScreen(tile)) yield tile.slot;
1620
+ }
1621
+ }
1449
1622
  );
1450
1623
  const pageSize = Math.min(options.pageSize ?? DEFAULT_PAGE_SIZE, atlasSize);
1451
1624
  const maxRasterCalls = options.maxRastersPerFrame ?? 12;
@@ -1465,18 +1638,16 @@ async function createGpuCanvas(host, options) {
1465
1638
  context,
1466
1639
  format,
1467
1640
  atlas,
1468
- ...colorsFor(options.background ?? "#f4f4f5")
1641
+ ...colorsFor(options.background ?? "#f4f4f5"),
1642
+ () => schedule()
1469
1643
  );
1470
1644
  const selectionColor = premultiplied(
1471
1645
  cssColorToRgba(options.selectionColor ?? DEFAULT_SELECTION_COLOR)
1472
1646
  );
1473
1647
  const hoverColor = options.hoverColor ? premultiplied(cssColorToRgba(options.hoverColor)) : selectionColor;
1474
- const nodes = /* @__PURE__ */ new Map();
1475
1648
  const effects = /* @__PURE__ */ new Map();
1476
1649
  const startedAt = performance.now();
1477
1650
  const FAR_AWAY = { x: -1e7, y: -1e7 };
1478
- renderer.onEffectReady = () => schedule();
1479
- const runs = [];
1480
1651
  let effectsMoving = false;
1481
1652
  let selection = /* @__PURE__ */ new Set();
1482
1653
  let hovered = null;
@@ -1492,22 +1663,8 @@ async function createGpuCanvas(host, options) {
1492
1663
  return rect;
1493
1664
  };
1494
1665
  const pages = /* @__PURE__ */ new Map();
1495
- atlas.keep = function* () {
1496
- const onScreen = (tile) => intersects(tile.rect, frameView);
1497
- for (const node of visible) yield node.slot;
1498
- for (const node of visible) {
1499
- for (const tile of node.tiles) {
1500
- if (tile.scale === detailScale && !tile.stale && onScreen(tile)) yield tile.slot;
1501
- }
1502
- }
1503
- for (const node of visible) {
1504
- yield node.preview;
1505
- for (const tile of node.tiles) if (onScreen(tile)) yield tile.slot;
1506
- }
1507
- };
1508
1666
  const pageByElement = /* @__PURE__ */ new Map();
1509
1667
  let openPage = null;
1510
- let camera = options.camera ?? DEFAULT_CAMERA;
1511
1668
  let lastCameraChange = -Infinity;
1512
1669
  let frameNumber = 0;
1513
1670
  let promotionsPerFrame = 2;
@@ -1545,6 +1702,7 @@ async function createGpuCanvas(host, options) {
1545
1702
  frame(false);
1546
1703
  });
1547
1704
  };
1705
+ const canvasDpr = () => canvas.width / Math.max(1, canvas.clientWidth);
1548
1706
  const resizeObserver = new ResizeObserver(([entry]) => {
1549
1707
  const size = entry.devicePixelContentBoxSize?.[0];
1550
1708
  canvas.width = Math.max(
@@ -1560,7 +1718,8 @@ async function createGpuCanvas(host, options) {
1560
1718
  resizeObserver.observe(canvas, { box: "device-pixel-content-box" });
1561
1719
  const transitioning = /* @__PURE__ */ new Map();
1562
1720
  const captures = [];
1563
- const isPinned = (page) => captures.some((capture) => capture.node.page === page) || live?.page === page || lingering?.page === page || focused?.page === page || [...transitioning.keys()].some((node) => node.page === page);
1721
+ const heldNodes = () => [live, lingering, focused, ...transitioning.keys()];
1722
+ const isPinned = (page) => captures.some((capture) => capture.node.page === page) || heldNodes().some((node) => node?.page === page);
1564
1723
  let detailScale = 1;
1565
1724
  let frameView = { x: 0, y: 0, width: 0, height: 0 };
1566
1725
  let contentTimer = 0;
@@ -1570,7 +1729,8 @@ async function createGpuCanvas(host, options) {
1570
1729
  };
1571
1730
  let refreshTimer = 0;
1572
1731
  let refreshAt = Infinity;
1573
- const refreshWait = (node, now) => Math.max(node.rect.width, node.rect.height) * camera.zoom < SMALL_NODE_PX ? node.refreshedAt + SMALL_REFRESH_MS - now : 0;
1732
+ const smallOnScreen = (node) => Math.max(node.rect.width, node.rect.height) * base.camera().zoom < SMALL_NODE_PX;
1733
+ const refreshWait = (node, now) => smallOnScreen(node) ? node.refreshedAt + SMALL_REFRESH_MS - now : 0;
1574
1734
  const refreshLater = (wait) => {
1575
1735
  const at = performance.now() + wait;
1576
1736
  if (at >= refreshAt) return;
@@ -1658,8 +1818,8 @@ async function createGpuCanvas(host, options) {
1658
1818
  const size = Math.max(1, Math.floor(TILE_PIXELS / detailScale));
1659
1819
  const jobs = [];
1660
1820
  const first = (edge, origin) => Math.floor((edge - origin) / size);
1661
- for (let row = first(shown.y, node.rect.y); row <= first(bottom(shown) - 1e-6, node.rect.y); row++) {
1662
- for (let column = first(shown.x, node.rect.x); column <= first(right(shown) - 1e-6, node.rect.x); column++) {
1821
+ for (let row = first(shown.y, node.rect.y); row <= first(shown.y + shown.height - 1e-6, node.rect.y); row++) {
1822
+ for (let column = first(shown.x, node.rect.x); column <= first(shown.x + shown.width - 1e-6, node.rect.x); column++) {
1663
1823
  if (current.some((tile) => tile.column === column && tile.row === row)) continue;
1664
1824
  const rect = intersection(node.rect, {
1665
1825
  x: node.rect.x + column * size,
@@ -1713,15 +1873,24 @@ async function createGpuCanvas(host, options) {
1713
1873
  }
1714
1874
  };
1715
1875
  const slotFor = (previous, width, height, thumbnail = false) => atlas.isValid(previous) && atlas.isPinned(previous) === thumbnail && previous.width === width && previous.height === height && Number.isInteger(previous.x) && Number.isInteger(previous.y) ? previous : (thumbnail ? atlas.allocatePinned(width, height) : null) ?? atlas.allocate(width, height);
1876
+ const texels = (length, scale) => Math.max(1, Math.round(length * scale));
1877
+ const nodeRegion = (node) => ({
1878
+ x: node.pageX,
1879
+ y: node.pageY,
1880
+ width: node.rect.width,
1881
+ height: node.rect.height
1882
+ });
1716
1883
  const freshThumb = (node) => atlas.isValid(node.thumb) && !node.thumbStale;
1884
+ const setThumb = (node, slot, now) => {
1885
+ node.thumb = slot;
1886
+ node.thumbStale = false;
1887
+ node.thumbAt = now;
1888
+ };
1717
1889
  const drawn = (node, slot, scale, now) => {
1718
1890
  node.slot = slot;
1719
1891
  node.slotScale = scale;
1720
- if (scale === THUMBNAIL_SCALE) {
1721
- node.thumb = slot;
1722
- node.thumbStale = false;
1723
- node.thumbAt = now;
1724
- } else if (node.dirty) node.thumbStale = true;
1892
+ if (scale === THUMBNAIL_SCALE) setThumb(node, slot, now);
1893
+ else if (node.dirty) node.thumbStale = true;
1725
1894
  if (node.dirty) {
1726
1895
  node.preview = null;
1727
1896
  node.refreshedAt = now;
@@ -1784,17 +1953,11 @@ async function createGpuCanvas(host, options) {
1784
1953
  const thumbnail = node.targetScale === THUMBNAIL_SCALE;
1785
1954
  const slot = slotFor(
1786
1955
  thumbnail ? node.thumb : node.slot,
1787
- Math.max(1, Math.round(node.rect.width * node.targetScale)),
1788
- Math.max(1, Math.round(node.rect.height * node.targetScale)),
1956
+ texels(node.rect.width, node.targetScale),
1957
+ texels(node.rect.height, node.targetScale),
1789
1958
  thumbnail
1790
1959
  );
1791
- const region = {
1792
- x: node.pageX,
1793
- y: node.pageY,
1794
- width: node.rect.width,
1795
- height: node.rect.height
1796
- };
1797
- if (!slot || !drawSnapshot(page, region, slot)) {
1960
+ if (!slot || !drawSnapshot(page, nodeRegion(node), slot)) {
1798
1961
  pending++;
1799
1962
  continue;
1800
1963
  }
@@ -1807,11 +1970,7 @@ async function createGpuCanvas(host, options) {
1807
1970
  const warm = /* @__PURE__ */ new Set();
1808
1971
  const drawRegion = (node, rect, scale2, previous) => {
1809
1972
  if (!warm.has(scale2) && overBudget()) return null;
1810
- const slot = slotFor(
1811
- previous,
1812
- Math.max(1, Math.round(rect.width * scale2)),
1813
- Math.max(1, Math.round(rect.height * scale2))
1814
- );
1973
+ const slot = slotFor(previous, texels(rect.width, scale2), texels(rect.height, scale2));
1815
1974
  const region = {
1816
1975
  x: node.pageX + rect.x - node.rect.x,
1817
1976
  y: node.pageY + rect.y - node.rect.y,
@@ -1862,21 +2021,13 @@ async function createGpuCanvas(host, options) {
1862
2021
  if (atlas.isValid(node.thumb) && start - node.thumbAt < THUMBNAIL_REFRESH_MS) continue;
1863
2022
  const slot = slotFor(
1864
2023
  node.thumb,
1865
- Math.max(1, Math.round(node.rect.width * THUMBNAIL_SCALE)),
1866
- Math.max(1, Math.round(node.rect.height * THUMBNAIL_SCALE)),
2024
+ texels(node.rect.width, THUMBNAIL_SCALE),
2025
+ texels(node.rect.height, THUMBNAIL_SCALE),
1867
2026
  true
1868
2027
  );
1869
- const region = {
1870
- x: node.pageX,
1871
- y: node.pageY,
1872
- width: node.rect.width,
1873
- height: node.rect.height
1874
- };
1875
- if (!slot || !drawSnapshot(page, region, slot)) continue;
2028
+ if (!slot || !drawSnapshot(page, nodeRegion(node), slot)) continue;
1876
2029
  calls++;
1877
- node.thumb = slot;
1878
- node.thumbStale = false;
1879
- node.thumbAt = start;
2030
+ setThumb(node, slot, start);
1880
2031
  }
1881
2032
  const done = plan.nodes.every((node) => !node.dirty && atlas.isValid(node.slot)) && plan.previews.every((node) => node.previewTarget === null) && tilesLeft === 0;
1882
2033
  if (done && !isPinned(page)) page.setDrawable(false, frameNumber);
@@ -1889,76 +2040,17 @@ async function createGpuCanvas(host, options) {
1889
2040
  let instances2 = visible.length;
1890
2041
  for (const node of visible) instances2 += node.tiles.length + (node.preview ? 1 : 0);
1891
2042
  instances2 += selection.size + 1;
1892
- renderer.reserve(instances2);
1893
- const data = renderer.instances;
1894
- let count = 0;
1895
- runs.length = 0;
2043
+ renderer.begin(instances2);
1896
2044
  effectsMoving = false;
1897
- let runEffect = null;
1898
- const startRun = (effect) => {
1899
- if (runs.length && runEffect === effect) return;
1900
- runEffect = effect;
1901
- runs.push({ effect, start: count, count: 0 });
1902
- };
1903
- const push = (rect, slot) => {
1904
- startRun(null);
1905
- runs[runs.length - 1].count++;
1906
- const offset = count++ * FLOATS_PER_INSTANCE;
1907
- data[offset] = rect.x;
1908
- data[offset + 1] = rect.y;
1909
- data[offset + 2] = rect.width;
1910
- data[offset + 3] = rect.height;
1911
- if (slot) {
1912
- atlas.touch(slot.layer);
1913
- data[offset + 4] = slot.x;
1914
- data[offset + 5] = slot.y;
1915
- data[offset + 6] = slot.x + slot.width;
1916
- data[offset + 7] = slot.y + slot.height;
1917
- data[offset + 8] = slot.layer;
1918
- data[offset + 9] = 0;
1919
- } else {
1920
- data[offset + 9] = 1;
1921
- }
1922
- data[offset + 10] = 0;
1923
- };
1924
- const pushEffect = (node, slot, worn) => {
1925
- startRun(worn.effect);
1926
- runs[runs.length - 1].count++;
1927
- const offset = count++ * FLOATS_PER_INSTANCE;
1928
- const { rect } = node;
1929
- data[offset] = rect.x;
1930
- data[offset + 1] = rect.y;
1931
- data[offset + 2] = rect.width;
1932
- data[offset + 3] = rect.height;
1933
- atlas.touch(slot.layer);
1934
- data[offset + 4] = slot.x;
1935
- data[offset + 5] = slot.y;
1936
- data[offset + 6] = slot.x + slot.width;
1937
- data[offset + 7] = slot.y + slot.height;
1938
- data[offset + 8] = slot.layer;
1939
- data[offset + 9] = 0;
1940
- data[offset + 10] = worn.pad;
1941
- data.set(worn.params, offset + 12);
1942
- };
1943
2045
  const pushRing = (element, color, width) => {
1944
2046
  const rect = ringRect(element);
1945
- if (!rect || !intersects(rect, frameView)) return;
1946
- startRun(null);
1947
- runs[runs.length - 1].count++;
1948
- const offset = count++ * FLOATS_PER_INSTANCE;
1949
- data[offset] = rect.x;
1950
- data[offset + 1] = rect.y;
1951
- data[offset + 2] = rect.width;
1952
- data[offset + 3] = rect.height;
1953
- data.set(color, offset + 4);
1954
- data[offset + 9] = 2;
1955
- data[offset + 10] = width;
2047
+ if (rect && intersects(rect, frameView)) renderer.ring(rect, color, width);
1956
2048
  };
1957
2049
  let onThumbnail = 0;
1958
2050
  for (const node of visible) {
1959
- const base = atlas.isValid(node.slot) ? node.slot : atlas.isValid(node.thumb) ? node.thumb : null;
1960
- if (base && base !== node.slot) onThumbnail++;
1961
- const hasSlot = base !== null;
2051
+ const image = atlas.isValid(node.slot) ? node.slot : atlas.isValid(node.thumb) ? node.thumb : null;
2052
+ if (image && image !== node.slot) onThumbnail++;
2053
+ const hasSlot = image !== null;
1962
2054
  const sharp = node.tiles.length > 0 || node.preview !== null && atlas.isValid(node.preview);
1963
2055
  const shown = sharp ? 3 : hasSlot ? 2 : 1;
1964
2056
  const before = node.shownFrame === stats.frames - 1 ? node.shown : 0;
@@ -1967,29 +2059,31 @@ async function createGpuCanvas(host, options) {
1967
2059
  node.shown = shown;
1968
2060
  node.shownFrame = stats.frames;
1969
2061
  const worn = effects.size ? effects.get(node.element) : void 0;
1970
- if (worn) {
2062
+ if (worn && !renderer.effectFailed(worn.effect)) {
1971
2063
  if (worn.effect.animated) effectsMoving = true;
1972
- if (base && renderer.hasEffect(worn.effect)) {
1973
- pushEffect(node, base, worn);
2064
+ if (image && renderer.hasEffect(worn.effect)) {
2065
+ renderer.effectQuad(node.rect, image, worn.effect, worn.pad, worn.params);
1974
2066
  continue;
1975
2067
  }
1976
2068
  effectsMoving = true;
1977
2069
  }
1978
- push(node.rect, base);
2070
+ renderer.quad(node.rect, image);
1979
2071
  const current = (tile) => tile.scale === detailScale && !tile.stale;
1980
2072
  for (const tile of node.tiles)
1981
- if (!current(tile) && atlas.isValid(tile.slot)) push(tile.rect, tile.slot);
1982
- if (node.preview && atlas.isValid(node.preview)) push(node.previewRect, node.preview);
2073
+ if (!current(tile) && atlas.isValid(tile.slot)) renderer.quad(tile.rect, tile.slot);
2074
+ if (node.preview && atlas.isValid(node.preview))
2075
+ renderer.quad(node.previewRect, node.preview);
1983
2076
  for (const tile of node.tiles)
1984
- if (current(tile) && atlas.isValid(tile.slot)) push(tile.rect, tile.slot);
2077
+ if (current(tile) && atlas.isValid(tile.slot)) renderer.quad(tile.rect, tile.slot);
1985
2078
  }
1986
2079
  stats.onThumbnail = onThumbnail;
1987
2080
  for (const element of selection) pushRing(element, selectionColor, SELECTION_WIDTH);
1988
2081
  if (hovered && !selection.has(hovered)) pushRing(hovered, hoverColor, HOVER_WIDTH);
1989
2082
  const cssWidth = canvas.clientWidth;
1990
- const dpr = canvas.width / Math.max(1, cssWidth);
2083
+ const dpr = canvasDpr();
1991
2084
  const now = performance.now();
1992
- renderer.draw(camera, cssWidth, canvas.clientHeight, dpr, count, runs, {
2085
+ const camera = base.camera();
2086
+ renderer.draw(camera, cssWidth, canvas.clientHeight, dpr, {
1993
2087
  time: (now - startedAt) / 1e3,
1994
2088
  pointer: pointer ? screenToWorld(camera, pointer) : FAR_AWAY,
1995
2089
  press,
@@ -2000,9 +2094,9 @@ async function createGpuCanvas(host, options) {
2000
2094
  if (destroyed) return;
2001
2095
  const start = performance.now();
2002
2096
  atlas.nextFrame();
2003
- const cssWidth = canvas.clientWidth;
2004
- const dpr = canvas.width / Math.max(1, cssWidth);
2005
- const view = visibleWorldRect(camera, cssWidth, canvas.clientHeight);
2097
+ const dpr = canvasDpr();
2098
+ const camera = base.camera();
2099
+ const view = base.view();
2006
2100
  const moving = start - lastCameraChange < SETTLE_MS;
2007
2101
  planFrame(view, rasterScaleFor(camera.zoom * dpr), camera.zoom * dpr, moving);
2008
2102
  if (!inPaint && plans.size > 0) {
@@ -2044,69 +2138,37 @@ async function createGpuCanvas(host, options) {
2044
2138
  const drawCaptures = () => {
2045
2139
  for (const capture of [...captures]) {
2046
2140
  const { node, scale } = capture;
2047
- const width = Math.max(1, Math.round(node.rect.width * scale));
2048
- const height = Math.max(1, Math.round(node.rect.height * scale));
2049
- const texture = device.createTexture({
2050
- size: { width, height },
2051
- format: "rgba8unorm",
2052
- usage: GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
2053
- });
2141
+ let image;
2054
2142
  try {
2055
- device.queue.drawElementImageToTexture(
2056
- {
2057
- source: node.page.element,
2058
- sourceX: node.pageX,
2059
- sourceY: node.pageY,
2060
- sourceWidth: node.rect.width,
2061
- sourceHeight: node.rect.height
2062
- },
2063
- { texture, size: { width, height }, premultipliedAlpha: false }
2143
+ image = captureRegion(
2144
+ device,
2145
+ node.page.element,
2146
+ nodeRegion(node),
2147
+ texels(node.rect.width, scale),
2148
+ texels(node.rect.height, scale)
2064
2149
  );
2065
2150
  } catch (error) {
2066
- texture.destroy();
2067
- if (++capture.tries > 20) {
2151
+ if (++capture.tries > MAX_CAPTURE_TRIES) {
2068
2152
  captures.splice(captures.indexOf(capture), 1);
2069
2153
  capture.reject(error instanceof Error ? error : new Error(String(error)));
2070
2154
  }
2071
2155
  continue;
2072
2156
  }
2073
2157
  captures.splice(captures.indexOf(capture), 1);
2074
- readTexture(texture, width, height).then(capture.resolve, capture.reject);
2158
+ image.then(capture.resolve, capture.reject);
2075
2159
  }
2076
2160
  if (captures.length) {
2077
2161
  awaitingPaint = true;
2078
2162
  canvas.requestPaint();
2079
2163
  } else schedule();
2080
2164
  };
2081
- const readTexture = async (texture, width, height) => {
2082
- const bytesPerRow = Math.ceil(width * 4 / 256) * 256;
2083
- const buffer = device.createBuffer({
2084
- size: bytesPerRow * height,
2085
- usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
2086
- });
2087
- const encoder = device.createCommandEncoder();
2088
- encoder.copyTextureToBuffer({ texture }, { buffer, bytesPerRow }, { width, height });
2089
- device.queue.submit([encoder.finish()]);
2090
- try {
2091
- await buffer.mapAsync(GPUMapMode.READ);
2092
- const rows = new Uint8Array(buffer.getMappedRange());
2093
- const pixels = new Uint8ClampedArray(width * height * 4);
2094
- for (let y = 0; y < height; y++) {
2095
- pixels.set(rows.subarray(y * bytesPerRow, y * bytesPerRow + width * 4), y * width * 4);
2096
- }
2097
- return new ImageData(pixels, width, height);
2098
- } finally {
2099
- buffer.destroy();
2100
- texture.destroy();
2101
- }
2102
- };
2103
2165
  canvas.onpaint = (event) => {
2104
2166
  if (captures.length) drawCaptures();
2105
2167
  let changed = false;
2106
2168
  for (const element of event.changedElements) {
2107
2169
  const page = pageByElement.get(element);
2108
2170
  if (!page) continue;
2109
- for (const node of [live, lingering, focused, ...transitioning.keys()]) {
2171
+ for (const node of heldNodes()) {
2110
2172
  if (node?.page === page) {
2111
2173
  node.dirty = true;
2112
2174
  changed = true;
@@ -2118,13 +2180,10 @@ async function createGpuCanvas(host, options) {
2118
2180
  awaitingPaint = false;
2119
2181
  frame(true);
2120
2182
  };
2121
- const viewNow = () => visibleWorldRect(camera, canvas.clientWidth, canvas.clientHeight);
2122
2183
  const nodeOf = (target) => {
2123
- let element = target instanceof HTMLElement ? target : target.parentElement;
2124
- while (element?.parentElement && !pageByElement.has(element.parentElement)) {
2125
- element = element.parentElement;
2126
- }
2127
- return element ? nodes.get(element) : void 0;
2184
+ const element = target instanceof Element ? target : target.parentElement;
2185
+ const node = element && nodeOfElement(nodes, element);
2186
+ return node ? nodes.get(node) : void 0;
2128
2187
  };
2129
2188
  const mutations = new MutationObserver((records) => {
2130
2189
  ringRects.clear();
@@ -2137,7 +2196,7 @@ async function createGpuCanvas(host, options) {
2137
2196
  const node = nodeOf(record.target);
2138
2197
  if (node && !node.dirty) {
2139
2198
  node.dirty = true;
2140
- if (!changed) changed = intersects(node.rect, viewNow());
2199
+ if (!changed) changed = intersects(node.rect, base.view());
2141
2200
  }
2142
2201
  }
2143
2202
  if (changed) schedule();
@@ -2155,8 +2214,13 @@ async function createGpuCanvas(host, options) {
2155
2214
  page.element.style.transform = "";
2156
2215
  schedule();
2157
2216
  };
2217
+ const letGo = (node) => {
2218
+ node.dirty = true;
2219
+ unpin(node.page);
2220
+ };
2158
2221
  const positionLivePage = () => {
2159
2222
  if (!live || livePositioned) return;
2223
+ const camera = base.camera();
2160
2224
  const { zoom } = camera;
2161
2225
  const x = (live.rect.x - camera.x - live.pageX) * zoom;
2162
2226
  const y = (live.rect.y - camera.y - live.pageY) * zoom;
@@ -2166,7 +2230,7 @@ async function createGpuCanvas(host, options) {
2166
2230
  canvasTransform: new DOMMatrix([zoom, 0, 0, zoom, x, y])
2167
2231
  });
2168
2232
  } else {
2169
- const dpr = canvas.width / Math.max(1, canvas.clientWidth);
2233
+ const dpr = canvasDpr();
2170
2234
  const drawn2 = new DOMMatrix([zoom, 0, 0, zoom, x * dpr, y * dpr]);
2171
2235
  live.page.element.style.transform = canvas.getElementTransform(live.page.element, drawn2).toString();
2172
2236
  }
@@ -2185,16 +2249,12 @@ async function createGpuCanvas(host, options) {
2185
2249
  clearTimeout(lingerTimer);
2186
2250
  const earlier = lingering;
2187
2251
  lingering = previous;
2188
- if (earlier && earlier !== previous) {
2189
- earlier.dirty = true;
2190
- unpin(earlier.page);
2191
- }
2252
+ if (earlier && earlier !== previous) letGo(earlier);
2192
2253
  lingerTimer = window.setTimeout(() => {
2193
2254
  const last = lingering;
2194
2255
  if (!last) return;
2195
2256
  lingering = null;
2196
- last.dirty = true;
2197
- unpin(last.page);
2257
+ letGo(last);
2198
2258
  }, LINGER_MS);
2199
2259
  unpin(previous.page);
2200
2260
  }
@@ -2211,14 +2271,10 @@ async function createGpuCanvas(host, options) {
2211
2271
  };
2212
2272
  const pickLive = () => {
2213
2273
  if (!pointer) return setLive(null);
2214
- const point = screenToWorld(camera, pointer);
2215
- let hit = null;
2216
- for (const node of nodes.values()) if (containsPoint(node.rect, point)) hit = node;
2217
- setLive(hit);
2274
+ setLive(base.nodeAt(screenToWorld(base.camera(), pointer)));
2218
2275
  };
2219
2276
  const onPointerMove = (event) => {
2220
- const bounds = canvas.getBoundingClientRect();
2221
- pointer = { x: event.clientX - bounds.left, y: event.clientY - bounds.top };
2277
+ pointer = pointIn(canvas, event);
2222
2278
  if (event.buttons === 0) pickLive();
2223
2279
  };
2224
2280
  const onPointerLeave = () => {
@@ -2229,9 +2285,8 @@ async function createGpuCanvas(host, options) {
2229
2285
  let pressedAt = -Infinity;
2230
2286
  let tapped = false;
2231
2287
  const onPointerDown = (event) => {
2232
- const bounds = canvas.getBoundingClientRect();
2233
- const at = { x: event.clientX - bounds.left, y: event.clientY - bounds.top };
2234
- press = screenToWorld(camera, at);
2288
+ const at = pointIn(canvas, event);
2289
+ press = screenToWorld(base.camera(), at);
2235
2290
  pressedAt = performance.now();
2236
2291
  if (event.pointerType !== "mouse") {
2237
2292
  pointer = at;
@@ -2243,13 +2298,8 @@ async function createGpuCanvas(host, options) {
2243
2298
  const onClick = (event) => {
2244
2299
  if (!tapped) return;
2245
2300
  tapped = false;
2246
- const bounds = canvas.getBoundingClientRect();
2247
- const point = screenToWorld(camera, {
2248
- x: event.clientX - bounds.left,
2249
- y: event.clientY - bounds.top
2250
- });
2251
- let top = null;
2252
- for (const node of nodes.values()) if (containsPoint(node.rect, point)) top = node;
2301
+ const point = screenToWorld(base.camera(), pointIn(canvas, event));
2302
+ const top = base.nodeAt(point);
2253
2303
  if (!top || top.element.contains(event.target)) return;
2254
2304
  let target = elementInNode(top.element, top.rect, point);
2255
2305
  while (target && !(target instanceof HTMLElement)) target = target.parentElement;
@@ -2271,10 +2321,7 @@ async function createGpuCanvas(host, options) {
2271
2321
  if (node === focused) return;
2272
2322
  const previous = focused;
2273
2323
  focused = node;
2274
- if (previous) {
2275
- previous.dirty = true;
2276
- unpin(previous.page);
2277
- }
2324
+ if (previous) letGo(previous);
2278
2325
  if (node) {
2279
2326
  node.page.setDrawable(true, frameNumber);
2280
2327
  node.dirty = true;
@@ -2285,10 +2332,7 @@ async function createGpuCanvas(host, options) {
2285
2332
  if (event.relatedTarget && canvas.contains(event.relatedTarget)) return;
2286
2333
  const previous = focused;
2287
2334
  focused = null;
2288
- if (previous) {
2289
- previous.dirty = true;
2290
- unpin(previous.page);
2291
- }
2335
+ if (previous) letGo(previous);
2292
2336
  };
2293
2337
  const onResourceLoad = (event) => {
2294
2338
  const node = nodeOf(event.target);
@@ -2325,8 +2369,8 @@ async function createGpuCanvas(host, options) {
2325
2369
  const node = nodeOf(event.target);
2326
2370
  const now = performance.now();
2327
2371
  if (!node || now - (usedAt.get(node) ?? -Infinity) > USED_MS) return;
2328
- if (!intersects(node.rect, viewNow())) return;
2329
- if (Math.max(node.rect.width, node.rect.height) * camera.zoom < SMALL_NODE_PX) return;
2372
+ if (!intersects(node.rect, base.view())) return;
2373
+ if (smallOnScreen(node)) return;
2330
2374
  const ms = transitionMs(event.target) + 32;
2331
2375
  const end = now + ms;
2332
2376
  if ((transitioning.get(node) ?? 0) >= end) return;
@@ -2334,38 +2378,34 @@ async function createGpuCanvas(host, options) {
2334
2378
  node.dirty = true;
2335
2379
  schedule();
2336
2380
  setTimeout(() => {
2337
- if (transitioning.get(node) !== end) return;
2381
+ if (destroyed || transitioning.get(node) !== end) return;
2338
2382
  transitioning.delete(node);
2339
- node.dirty = true;
2340
- unpin(node.page);
2383
+ letGo(node);
2341
2384
  schedule();
2342
2385
  }, ms);
2343
2386
  };
2344
2387
  for (const type of USE_EVENTS) canvas.addEventListener(type, onUse, true);
2345
2388
  canvas.addEventListener("transitionrun", onTransitionRun);
2346
- const setCamera = (next) => {
2347
- camera = next;
2348
- lastCameraChange = performance.now();
2349
- setLive(null);
2350
- clearTimeout(settleTimer);
2351
- settleTimer = window.setTimeout(() => {
2352
- pickLive();
2353
- schedule();
2354
- }, SETTLE_MS);
2355
- schedule();
2356
- options.onCameraChange?.(camera);
2389
+ const newPage = (width, height) => {
2390
+ const page = new Page(width, height);
2391
+ pages.set(page, /* @__PURE__ */ new Set());
2392
+ pageByElement.set(page.element, page);
2393
+ canvas.append(page.element);
2394
+ return page;
2395
+ };
2396
+ const dropPage = (page) => {
2397
+ pages.delete(page);
2398
+ pageByElement.delete(page.element);
2399
+ pageSlots.delete(page);
2400
+ page.element.remove();
2357
2401
  };
2358
- const unbindInput = bindCameraInput(surface, () => camera, setCamera);
2359
2402
  const placeNode = (element, rect) => {
2360
2403
  let slot = openPage?.place(rect.width, rect.height) ?? null;
2361
2404
  if (!slot) {
2362
- const page = new Page(
2405
+ const page = newPage(
2363
2406
  Math.max(pageSize, Math.ceil(rect.width)),
2364
2407
  Math.max(pageSize, Math.ceil(rect.height))
2365
2408
  );
2366
- pages.set(page, /* @__PURE__ */ new Set());
2367
- pageByElement.set(page.element, page);
2368
- canvas.append(page.element);
2369
2409
  if (page.width === pageSize && page.height === pageSize) openPage = page;
2370
2410
  slot = page.place(rect.width, rect.height);
2371
2411
  }
@@ -2382,21 +2422,13 @@ async function createGpuCanvas(host, options) {
2382
2422
  const releaseNode = (node) => {
2383
2423
  const members = pages.get(node.page);
2384
2424
  members.delete(node);
2385
- node.page.release();
2386
- if (members.size === 0 && node.page !== openPage) {
2387
- pages.delete(node.page);
2388
- pageByElement.delete(node.page.element);
2389
- node.page.element.remove();
2390
- }
2425
+ if (members.size === 0 && node.page !== openPage) dropPage(node.page);
2391
2426
  };
2392
2427
  const lift = (node) => {
2393
2428
  const previous = node.page;
2394
2429
  const wasLive = live === node;
2395
2430
  if (wasLive) setLive(null);
2396
- const page = new Page(Math.ceil(node.rect.width), Math.ceil(node.rect.height));
2397
- pages.set(page, /* @__PURE__ */ new Set());
2398
- pageByElement.set(page.element, page);
2399
- canvas.append(page.element);
2431
+ const page = newPage(Math.ceil(node.rect.width), Math.ceil(node.rect.height));
2400
2432
  page.place(node.rect.width, node.rect.height);
2401
2433
  const target = page.element;
2402
2434
  if (target.moveBefore) target.moveBefore(node.element, null);
@@ -2427,15 +2459,11 @@ async function createGpuCanvas(host, options) {
2427
2459
  pages.get(slot.page).add(node);
2428
2460
  };
2429
2461
  return {
2462
+ ...base.api,
2430
2463
  mode: "gpu",
2431
- surface,
2432
2464
  addNode(element, rect) {
2433
- Object.assign(element.style, {
2434
- boxSizing: "border-box",
2435
- contain: "strict",
2436
- overflow: "hidden",
2437
- pointerEvents: "none"
2438
- });
2465
+ styleNode(element);
2466
+ element.style.pointerEvents = "none";
2439
2467
  const node = {
2440
2468
  element,
2441
2469
  rect,
@@ -2489,25 +2517,10 @@ async function createGpuCanvas(host, options) {
2489
2517
  element.remove();
2490
2518
  schedule();
2491
2519
  },
2492
- getNodeRect: (element) => {
2493
- const node = nodes.get(element);
2494
- return node ? { ...node.rect } : null;
2495
- },
2496
- getNodesIn(rect) {
2497
- const hits = [];
2498
- for (const node of nodes.values()) if (intersects(node.rect, rect)) hits.push(node.element);
2499
- return hits;
2500
- },
2501
- getNodeOf: (element) => nodeOfElement(nodes, element),
2502
2520
  getElementRect(element) {
2503
2521
  const rect = ringRect(element);
2504
2522
  return rect ? { ...rect } : null;
2505
2523
  },
2506
- getElementAt(point) {
2507
- let top = null;
2508
- for (const node of nodes.values()) if (containsPoint(node.rect, point)) top = node;
2509
- return top ? elementInNode(top.element, top.rect, point) : null;
2510
- },
2511
2524
  setSelection(elements) {
2512
2525
  selection = new Set(elements);
2513
2526
  ringRects.clear();
@@ -2527,10 +2540,6 @@ async function createGpuCanvas(host, options) {
2527
2540
  } else effects.delete(element);
2528
2541
  schedule();
2529
2542
  },
2530
- screenToWorld: (point) => screenToWorld(camera, point),
2531
- worldToScreen: (point) => worldToScreen(camera, point),
2532
- getCamera: () => camera,
2533
- setCamera,
2534
2543
  invalidate: onFontsLoaded,
2535
2544
  snapshot(element, options2 = {}) {
2536
2545
  const node = nodes.get(element);
@@ -2549,10 +2558,6 @@ async function createGpuCanvas(host, options) {
2549
2558
  [renderer.background, renderer.placeholder] = colorsFor(color);
2550
2559
  schedule();
2551
2560
  },
2552
- fit(padding) {
2553
- const bounds = boundsOf([...nodes.values()].map((node) => node.rect));
2554
- if (bounds) setCamera(fitCamera(bounds, canvas.clientWidth, canvas.clientHeight, padding));
2555
- },
2556
2561
  getStats: () => ({
2557
2562
  mode: "gpu",
2558
2563
  nodes: nodes.size,
@@ -2578,6 +2583,7 @@ async function createGpuCanvas(host, options) {
2578
2583
  clearTimeout(settleTimer);
2579
2584
  clearTimeout(lingerTimer);
2580
2585
  clearTimeout(contentTimer);
2586
+ clearTimeout(refreshTimer);
2581
2587
  mutations.disconnect();
2582
2588
  document.fonts.removeEventListener("loadingdone", onFontsLoaded);
2583
2589
  canvas.removeEventListener("load", onResourceLoad, true);
@@ -2589,10 +2595,9 @@ async function createGpuCanvas(host, options) {
2589
2595
  canvas.removeEventListener("focusout", onFocusOut);
2590
2596
  canvas.removeEventListener("transitionrun", onTransitionRun);
2591
2597
  for (const type of USE_EVENTS) canvas.removeEventListener(type, onUse, true);
2592
- unbindInput();
2593
2598
  resizeObserver.disconnect();
2594
2599
  canvas.onpaint = null;
2595
- surface.remove();
2600
+ base.destroy();
2596
2601
  renderer.destroy();
2597
2602
  atlas.destroy();
2598
2603
  device.destroy();
@@ -2651,7 +2656,6 @@ var labelText = (element) => {
2651
2656
  }
2652
2657
  return text.replace(/\s+/g, " ").trim();
2653
2658
  };
2654
- var sameRect = (a, b) => Math.abs(a.x - b.x) < 0.5 && Math.abs(a.y - b.y) < 0.5 && Math.abs(a.width - b.width) < 0.5 && Math.abs(a.height - b.height) < 0.5;
2655
2659
  var round = (rect) => ({
2656
2660
  x: Math.round(rect.x),
2657
2661
  y: Math.round(rect.y),
@@ -2703,11 +2707,7 @@ function createAgentView(canvas) {
2703
2707
  return {
2704
2708
  /** What is in `rect` (default: the view), node by node, as plain JSON. */
2705
2709
  describe(options = {}) {
2706
- const area = options.rect ?? visibleWorldRect(
2707
- canvas.getCamera(),
2708
- canvas.surface.clientWidth,
2709
- canvas.surface.clientHeight
2710
- );
2710
+ const area = options.rect ?? viewOf(canvas);
2711
2711
  return canvas.getNodesIn(area).flatMap((node) => read(node, options.depth ?? 6, options));
2712
2712
  },
2713
2713
  /** The element behind an id from `describe`, or null once it is gone. */
@@ -2752,25 +2752,7 @@ function createAgentView(canvas) {
2752
2752
  // src/layers.ts
2753
2753
  var sameLayer = (a, b) => !!a && !!b && a[0] === b[0];
2754
2754
  var layerIn = (layer, list) => list.some((other) => sameLayer(layer, other));
2755
- function layerRect(canvas, layer) {
2756
- let union = null;
2757
- for (const element of layer) {
2758
- const rect = canvas.getElementRect(element);
2759
- if (rect) union = union ? unionRect(union, rect) : rect;
2760
- }
2761
- return union;
2762
- }
2763
- function unionRect(a, b) {
2764
- const x = Math.min(a.x, b.x);
2765
- const y = Math.min(a.y, b.y);
2766
- return {
2767
- x,
2768
- y,
2769
- width: Math.max(a.x + a.width, b.x + b.width) - x,
2770
- height: Math.max(a.y + a.height, b.y + b.height) - y
2771
- };
2772
- }
2773
- var sameRect2 = (a, b) => !!a && !!b && Math.abs(a.x - b.x) < 0.5 && Math.abs(a.y - b.y) < 0.5 && Math.abs(a.width - b.width) < 0.5 && Math.abs(a.height - b.height) < 0.5;
2755
+ var layerRect = (canvas, layer) => unionOf(layer.map((element) => canvas.getElementRect(element)));
2774
2756
  function createLayers(canvas, pick) {
2775
2757
  const chainOf = (element) => {
2776
2758
  const node = canvas.getNodeOf(element);
@@ -2789,7 +2771,7 @@ function createLayers(canvas, pick) {
2789
2771
  let previous = canvas.getNodeRect(node);
2790
2772
  for (const layer of inner) {
2791
2773
  const rect = layerRect(canvas, layer);
2792
- if (sameRect2(rect, previous)) continue;
2774
+ if (sameRect(rect, previous)) continue;
2793
2775
  chain.push(layer);
2794
2776
  previous = rect;
2795
2777
  }
@@ -2807,7 +2789,7 @@ function createLayers(canvas, pick) {
2807
2789
  for (const child of element.children) {
2808
2790
  const rect = canvas.getElementRect(child);
2809
2791
  if (!rect || rect.width <= 0 || rect.height <= 0) continue;
2810
- if (sameRect2(rect, outer)) visit(child, outer);
2792
+ if (sameRect(rect, outer)) visit(child, outer);
2811
2793
  else children.push([child]);
2812
2794
  }
2813
2795
  };
@@ -2855,7 +2837,7 @@ var pill = (color) => ({
2855
2837
  whiteSpace: "nowrap",
2856
2838
  pointerEvents: "none"
2857
2839
  });
2858
- function createOverlay(surface, color, hoverColor) {
2840
+ function createOverlay(surface, color) {
2859
2841
  const root = document.createElement("div");
2860
2842
  Object.assign(root.style, {
2861
2843
  position: "absolute",
@@ -2870,7 +2852,7 @@ function createOverlay(surface, color, hoverColor) {
2870
2852
  root.append(element);
2871
2853
  return element;
2872
2854
  };
2873
- const hoverName = make(pill(hoverColor));
2855
+ const hoverName = make(pill(color));
2874
2856
  const name = make(pill(color));
2875
2857
  const size = make(pill(color));
2876
2858
  const bounds = make({ position: "absolute", border: `1px solid ${color}` });
@@ -2955,9 +2937,10 @@ function createOverlay(surface, color, hoverColor) {
2955
2937
  }
2956
2938
  };
2957
2939
  }
2958
- var guideRect = (guide, toScreen) => {
2959
- const a = toScreen(guide.x1, guide.y1);
2960
- const b = toScreen(guide.x2, guide.y2);
2940
+ var handleAt = (target) => target instanceof Element ? target.closest("[data-gpucanvas-handle]")?.dataset.gpucanvasHandle ?? null : null;
2941
+ var guideRect = (guide, camera) => {
2942
+ const a = worldToScreen(camera, { x: guide.x1, y: guide.y1 });
2943
+ const b = worldToScreen(camera, { x: guide.x2, y: guide.y2 });
2961
2944
  return {
2962
2945
  x: Math.min(a.x, b.x),
2963
2946
  y: Math.min(a.y, b.y),
@@ -3004,16 +2987,16 @@ function guidesFor(rect, targets, edges) {
3004
2987
  const hits = targets.filter((target) => anchorsX(target).some((at) => close(at, x)));
3005
2988
  if (!hits.length) continue;
3006
2989
  const top = Math.min(rect.y, ...hits.map((t) => t.y));
3007
- const bottom2 = Math.max(rect.y + rect.height, ...hits.map((t) => t.y + t.height));
3008
- guides.push({ x1: x, y1: top, x2: x, y2: bottom2 });
2990
+ const bottom = Math.max(rect.y + rect.height, ...hits.map((t) => t.y + t.height));
2991
+ guides.push({ x1: x, y1: top, x2: x, y2: bottom });
3009
2992
  }
3010
2993
  for (const f of edges.y) {
3011
2994
  const y = rect.y + rect.height * f;
3012
2995
  const hits = targets.filter((target) => anchorsY(target).some((at) => close(at, y)));
3013
2996
  if (!hits.length) continue;
3014
2997
  const left = Math.min(rect.x, ...hits.map((t) => t.x));
3015
- const right2 = Math.max(rect.x + rect.width, ...hits.map((t) => t.x + t.width));
3016
- guides.push({ x1: left, y1: y, x2: right2, y2: y });
2998
+ const right = Math.max(rect.x + rect.width, ...hits.map((t) => t.x + t.width));
2999
+ guides.push({ x1: left, y1: y, x2: right, y2: y });
3017
3000
  }
3018
3001
  return guides;
3019
3002
  }
@@ -3061,9 +3044,11 @@ function createMarquee(canvas, color = DEFAULT_SELECTION_COLOR, select = (world)
3061
3044
  }
3062
3045
  };
3063
3046
  }
3047
+ var defaultLabel = (canvas) => (layer) => canvas.getNodeOf(layer[0]) === layer[0] ? null : layer[0].localName;
3064
3048
  var CLICK_SLOP = 4;
3065
3049
  var SNAP_PX = 6;
3066
3050
  var MAX_NAMED = 1;
3051
+ var EVERYWHERE = { x: -1e9, y: -1e9, width: 2e9, height: 2e9 };
3067
3052
  var isEditable = (element) => !!element && (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement || element.isContentEditable);
3068
3053
  function bindEditor(canvas, options = {}) {
3069
3054
  const {
@@ -3077,11 +3062,11 @@ function bindEditor(canvas, options = {}) {
3077
3062
  onChange,
3078
3063
  onTransform
3079
3064
  } = options;
3080
- const label = options.label ?? ((layer) => canvas.getNodeOf(layer[0]) === layer[0] ? null : layer[0].localName);
3065
+ const label = options.label ?? defaultLabel(canvas);
3081
3066
  const { surface } = canvas;
3082
3067
  const layers = createLayers(canvas, pick);
3083
3068
  const transforms = !!onTransform && trigger === "click";
3084
- const overlay = createOverlay(surface, color, color);
3069
+ const overlay = createOverlay(surface, color);
3085
3070
  let marqueeLayers = [];
3086
3071
  const box = marquee ? createMarquee(canvas, color, (world) => {
3087
3072
  marqueeLayers = layers.layersIn(scopes.at(-1) ?? null, world);
@@ -3108,10 +3093,7 @@ function bindEditor(canvas, options = {}) {
3108
3093
  canvas.setHover(layer?.[0] ?? null);
3109
3094
  draw();
3110
3095
  };
3111
- const at = (event) => {
3112
- const bounds = surface.getBoundingClientRect();
3113
- return { x: event.clientX - bounds.left, y: event.clientY - bounds.top };
3114
- };
3096
+ const at = (event) => pointIn(surface, event);
3115
3097
  const chainAt = (point) => layers.chainAt(canvas.screenToWorld(point));
3116
3098
  const fieldAt = (point) => {
3117
3099
  const element = canvas.getElementAt(canvas.screenToWorld(point));
@@ -3140,15 +3122,7 @@ function bindEditor(canvas, options = {}) {
3140
3122
  const draw = () => {
3141
3123
  if (!frame) frame = requestAnimationFrame(render);
3142
3124
  };
3143
- const toScreen = (rect) => {
3144
- const { x, y } = canvas.worldToScreen(rect);
3145
- const zoom = canvas.getCamera().zoom;
3146
- return { x, y, width: rect.width * zoom, height: rect.height * zoom };
3147
- };
3148
- const unionOf = (rects) => rects.reduce(
3149
- (union, rect) => !rect ? union : union ? unionRect(union, rect) : rect,
3150
- null
3151
- );
3125
+ const toScreen = (rect) => rectToScreen(canvas.getCamera(), rect);
3152
3126
  const render = () => {
3153
3127
  frame = 0;
3154
3128
  const transforming = gesture?.kind === "move" || gesture?.kind === "resize" ? gesture : null;
@@ -3164,9 +3138,7 @@ function bindEditor(canvas, options = {}) {
3164
3138
  } : null,
3165
3139
  bounds: selection.length > 1 || !!transforming,
3166
3140
  handles: transforms && selection.length > 0 && gesture?.kind !== "marquee",
3167
- guides: transforming ? transforming.guides.map(
3168
- (guide) => guideRect(guide, (x, y) => canvas.worldToScreen({ x, y }))
3169
- ) : []
3141
+ guides: transforming ? transforming.guides.map((guide) => guideRect(guide, canvas.getCamera())) : []
3170
3142
  });
3171
3143
  if (selection.length || hovered || gesture) draw();
3172
3144
  };
@@ -3184,8 +3156,7 @@ function bindEditor(canvas, options = {}) {
3184
3156
  if (!snap) return [];
3185
3157
  const scope = scopes.at(-1);
3186
3158
  if (!scope) {
3187
- const view = visibleWorldRect(canvas.getCamera(), surface.clientWidth, surface.clientHeight);
3188
- return canvas.getNodesIn(view).filter((node) => !moving.some((layer) => layer[0] === node)).map((node) => canvas.getNodeRect(node));
3159
+ return canvas.getNodesIn(viewOf(canvas)).filter((node) => !moving.some((layer) => layer[0] === node)).map((node) => canvas.getNodeRect(node));
3189
3160
  }
3190
3161
  const rects = layers.childrenOf(scope).filter((layer) => !layerIn(layer, moving)).map((layer) => layerRect(canvas, layer));
3191
3162
  return [layerRect(canvas, scope), ...rects].filter((rect) => !!rect);
@@ -3225,21 +3196,21 @@ function bindEditor(canvas, options = {}) {
3225
3196
  } else {
3226
3197
  let left = bounds.x;
3227
3198
  let top = bounds.y;
3228
- let right2 = bounds.x + bounds.width;
3229
- let bottom2 = bounds.y + bounds.height;
3230
- if (handle.includes("w")) left = Math.min(left + dx, right2 - 1);
3231
- if (handle.includes("e")) right2 = Math.max(right2 + dx, left + 1);
3232
- if (handle.includes("n")) top = Math.min(top + dy, bottom2 - 1);
3233
- if (handle.includes("s")) bottom2 = Math.max(bottom2 + dy, top + 1);
3234
- next = { x: left, y: top, width: right2 - left, height: bottom2 - top };
3199
+ let right = bounds.x + bounds.width;
3200
+ let bottom = bounds.y + bounds.height;
3201
+ if (handle.includes("w")) left = Math.min(left + dx, right - 1);
3202
+ if (handle.includes("e")) right = Math.max(right + dx, left + 1);
3203
+ if (handle.includes("n")) top = Math.min(top + dy, bottom - 1);
3204
+ if (handle.includes("s")) bottom = Math.max(bottom + dy, top + 1);
3205
+ next = { x: left, y: top, width: right - left, height: bottom - top };
3235
3206
  const corner = handle.length === 2;
3236
3207
  if (keepRatio && corner) {
3237
3208
  const scale = Math.max(next.width / bounds.width, next.height / bounds.height);
3238
3209
  const width = bounds.width * scale;
3239
3210
  const height = bounds.height * scale;
3240
3211
  next = {
3241
- x: handle.includes("w") ? right2 - width : left,
3242
- y: handle.includes("n") ? bottom2 - height : top,
3212
+ x: handle.includes("w") ? right - width : left,
3213
+ y: handle.includes("n") ? bottom - height : top,
3243
3214
  width,
3244
3215
  height
3245
3216
  };
@@ -3287,10 +3258,10 @@ function bindEditor(canvas, options = {}) {
3287
3258
  swallowClick = true;
3288
3259
  surface.setPointerCapture(event.pointerId);
3289
3260
  };
3290
- const handle = event.target.closest?.("[data-gpucanvas-handle]");
3261
+ const handle = handleAt(event.target);
3291
3262
  if (handle && transforms) {
3292
3263
  take();
3293
- return beginTransform(point, handle.dataset.gpucanvasHandle);
3264
+ return beginTransform(point, handle);
3294
3265
  }
3295
3266
  const target = targetIn(chainAt(point));
3296
3267
  if (!target) {
@@ -3427,7 +3398,7 @@ function bindEditor(canvas, options = {}) {
3427
3398
  } else if (event.key === "Tab") {
3428
3399
  event.preventDefault();
3429
3400
  const scope = scopes.at(-1);
3430
- const siblings = scope ? layers.childrenOf(scope) : canvas.getNodesIn({ x: -1e9, y: -1e9, width: 2e9, height: 2e9 }).map((node) => [node]);
3401
+ const siblings = scope ? layers.childrenOf(scope) : canvas.getNodesIn(EVERYWHERE).map((node) => [node]);
3431
3402
  const index = siblings.findIndex((layer) => sameLayer(layer, selection[0]));
3432
3403
  const step = event.shiftKey ? -1 : 1;
3433
3404
  const next = siblings[(index + step + siblings.length) % siblings.length];
@@ -3487,7 +3458,7 @@ var pillStyle = (color) => ({
3487
3458
  whiteSpace: "nowrap",
3488
3459
  pointerEvents: "none"
3489
3460
  });
3490
- function createMarks(canvas, { color = "#4f8cff" } = {}) {
3461
+ function createMarks(canvas, { color = DEFAULT_SELECTION_COLOR } = {}) {
3491
3462
  const id = `gpucanvas-marks-${++instances}`;
3492
3463
  const root = document.createElement("div");
3493
3464
  Object.assign(root.style, {
@@ -3530,11 +3501,7 @@ function createMarks(canvas, { color = "#4f8cff" } = {}) {
3530
3501
  return ref;
3531
3502
  };
3532
3503
  const toScreen = (point) => canvas.worldToScreen(point);
3533
- const screenRect = (rect) => {
3534
- const a = toScreen(rect);
3535
- const b = toScreen({ x: rect.x + rect.width, y: rect.y + rect.height });
3536
- return { x: a.x, y: a.y, width: b.x - a.x, height: b.y - a.y };
3537
- };
3504
+ const screenRect = (rect) => rectToScreen(canvas.getCamera(), rect);
3538
3505
  const worldRect = (target) => target instanceof Element ? canvas.getElementRect(target) : { x: target.x, y: target.y, width: 0, height: 0 };
3539
3506
  const place = (element, x, y) => {
3540
3507
  const transform = `translate(${Math.round(x)}px, ${Math.round(y)}px)`;
@@ -3880,6 +3847,7 @@ export {
3880
3847
  EFFECTS,
3881
3848
  supportsGpuCanvas,
3882
3849
  createAgentView,
3850
+ defaultLabel,
3883
3851
  bindEditor,
3884
3852
  createMarks,
3885
3853
  toBlob,
@@ -3890,4 +3858,4 @@ export {
3890
3858
  swapNodes,
3891
3859
  createGpuCanvas2 as createGpuCanvas
3892
3860
  };
3893
- //# sourceMappingURL=chunk-2EDYGRFR.js.map
3861
+ //# sourceMappingURL=chunk-IJ3R6X75.js.map