@weasel-js/core 0.8.0 → 1.0.1

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +748 -0
  2. package/dist/{DrawCommand-BKJ5JPkC.d.ts → DrawCommand-DCmiNkBj.d.ts} +14 -169
  3. package/dist/{chunk-GLZLSPGJ.js → chunk-6SHZHXXR.js} +46 -9
  4. package/dist/chunk-6SHZHXXR.js.map +1 -0
  5. package/dist/chunk-DCIU3SRK.js +505 -0
  6. package/dist/chunk-DCIU3SRK.js.map +1 -0
  7. package/dist/{chunk-DLV5F4NY.js → chunk-FVNWK7U3.js} +1525 -763
  8. package/dist/chunk-FVNWK7U3.js.map +1 -0
  9. package/dist/chunk-IO5Z75X4.js +167 -0
  10. package/dist/chunk-IO5Z75X4.js.map +1 -0
  11. package/dist/{chunk-BPRHGTLQ.js → chunk-UB6A6L77.js} +3 -3
  12. package/dist/{chunk-BPRHGTLQ.js.map → chunk-UB6A6L77.js.map} +1 -1
  13. package/dist/{chunk-CMBE45MT.js → chunk-Z3KNFTTS.js} +6 -6
  14. package/dist/chunk-Z3KNFTTS.js.map +1 -0
  15. package/dist/clipboard.d.ts +1 -1
  16. package/dist/{index-Clf_koyR.d.ts → index-DPI8DV0A.d.ts} +104 -91
  17. package/dist/index.d.ts +352 -43
  18. package/dist/index.js +6 -6
  19. package/dist/insert.js +5 -5
  20. package/dist/insert.js.map +1 -1
  21. package/dist/move.js +2 -2
  22. package/dist/paint-types-CnLIzqq1.d.ts +261 -0
  23. package/dist/patterns-builtin.d.ts +57 -2
  24. package/dist/patterns-builtin.js +1 -97
  25. package/dist/patterns-builtin.js.map +1 -1
  26. package/dist/renderer.d.ts +76 -15
  27. package/dist/renderer.js +6 -6
  28. package/dist/resize.js +2 -2
  29. package/dist/routing.d.ts +5 -5
  30. package/dist/routing.js +1 -1
  31. package/dist/{types-DUpI7Apc.d.ts → types-Kh_osAq9.d.ts} +8 -1
  32. package/dist/{viewToMat3-DFMdZvVl.d.ts → viewToMat3-BvaWCN99.d.ts} +1 -1
  33. package/package.json +6 -6
  34. package/dist/chunk-6DAUIMTO.js +0 -233
  35. package/dist/chunk-6DAUIMTO.js.map +0 -1
  36. package/dist/chunk-CMBE45MT.js.map +0 -1
  37. package/dist/chunk-DLV5F4NY.js.map +0 -1
  38. package/dist/chunk-GLZLSPGJ.js.map +0 -1
  39. package/dist/chunk-Y52N27PF.js +0 -39
  40. package/dist/chunk-Y52N27PF.js.map +0 -1
  41. package/dist/registerTexture-BzHTLhD9.d.ts +0 -25
@@ -1,25 +1,25 @@
1
- import { defineViewportTool, reportRouteConflicts, defineTool, RESERVED_ID_PREFIXES, RESERVED_ID_NAMES } from './chunk-6DAUIMTO.js';
2
- import { scratchKey, createTransformOp, snap, getScratch, createReparentOp, setScratch } from './chunk-BPRHGTLQ.js';
3
- import { fixedCornerOf, DEFAULT_RESIZE_BEHAVIORS, CORNER_ANCHORS, cornerPoint } from './chunk-CMBE45MT.js';
4
- import { AUTO_POSE_DESCRIPTOR, RECT_POSE_DESCRIPTOR, resolveUnit, meanScale, pathPoseDescriptor, polygonFromPoints, starPath, regularPolygonPath, linePath, ellipsePath, rectPath, strokeHitTest, boundsOfPath, PATH_M, PATH_L, PATH_Z, PATH_Q, PATH_C, DEFAULT_FLATTEN_TOLERANCE, flattenQuadratic, flattenCubic, PathBuilder, pointInPath, transformPath, flattenCubicWithArcLen, flattenQuadraticWithArcLen, translatePath, PATH_CMD_LENGTHS, pointToGridCell } from './chunk-GLZLSPGJ.js';
1
+ import { ActionDisabledReason, defineViewportTool, useOptionalDepRegistry, DepRegistryProvider, useActionsRegistry, ActionsProvider, useDepRegistry, defineTool, reportRouteConflicts, buildDepsFromRequires, useDepSource, actionBindings } from './chunk-DCIU3SRK.js';
2
+ import { scratchKey, createTransformOp, snap, getScratch, createReparentOp, setScratch } from './chunk-UB6A6L77.js';
3
+ import { fixedCornerOf, DEFAULT_RESIZE_BEHAVIORS, CORNER_ANCHORS, cornerPoint } from './chunk-Z3KNFTTS.js';
4
+ import { AUTO_POSE_DESCRIPTOR, RECT_POSE_DESCRIPTOR, resolveUnit, pathPoseDescriptor, polygonFromPoints, starPath, regularPolygonPath, linePath, ellipsePath, rectPath, strokeHitTest, withinPxRadius, boundsOfPath, PATH_M, PATH_L, PATH_Z, PATH_Q, PATH_C, DEFAULT_FLATTEN_TOLERANCE, flattenQuadratic, flattenCubic, PathBuilder, pointInPath, transformPath, translatePath, pxExtent, remapRotatedLeaf, withinPxBox, flattenCubicWithArcLen, flattenQuadraticWithArcLen, PATH_CMD_LENGTHS, pointToGridCell } from './chunk-6SHZHXXR.js';
5
5
  import { dispatchApplyBatch, dlog, dwarn, usePointerContext, PointerContextProvider, applyOpsTo, extractWeaselClipboardFromSvg, parseWeaselClipboardText, materializePaste, WEASEL_CLIPBOARD_MIME, WEASEL_CLIPBOARD_MIME_WEB, sniffWeaselClipboardText } from './chunk-775XXAHR.js';
6
6
  import { createSetSelectionOp } from './chunk-BHVYVFGV.js';
7
7
  import { registerOpFactory, createDeleteOp, createInsertOp, rebuildOp } from './chunk-GVCNT7UH.js';
8
- import { getTexture } from './chunk-Y52N27PF.js';
8
+ import { getTexture, chunks, dots, crosshatch, hatch } from './chunk-IO5Z75X4.js';
9
9
  import { createContext, createElement, forwardRef, useMemo, useRef, useState, useCallback, useImperativeHandle, useEffect, useSyncExternalStore, useContext, useReducer, useLayoutEffect } from 'react';
10
10
  import { subscribeGlyphReady, DEFAULT_BAKE_BUDGET, TEXT_VERT_SRC, TEXT_FRAG_SRC, TEXT_SDF_UNIFORMS, TEXT_SDF_ATTRIBUTES, TEXT_FRAG_R8_SRC, markAllFontsNotUploaded, resetBakeBudget, glyphGeneration, syncDynamicPageTexture, ensureFontTexture, dynamicPageTextureId, textureCacheKey, resolveFontVariant, resolveGlyphFallback, glyphOutline } from '@weasel-js/font';
11
11
  export { canQueryLocalFonts, canQueryLocalFonts as canQueryLocalFonts2, enableLocalFontOutlines, enableLocalFontOutlines as enableLocalFontOutlines2, hasFontOutlines, hasFontOutlines as hasFontOutlines2, isCanvasFont, isCanvasFont as isCanvasFont2, listFontOutlines, listFontOutlines as listFontOutlines2, outlineStatus, outlineStatus as outlineStatus2, registerCanvasFont, registerCanvasFont as registerCanvasFont2, registerFont, registerFont as registerFont2, registerFontOutlines, registerFontOutlines as registerFontOutlines2, subscribeGlyphReady, subscribeGlyphReady as subscribeGlyphReady2, unregisterCanvasFont, unregisterCanvasFont as unregisterCanvasFont2, unregisterFontOutlines, unregisterFontOutlines as unregisterFontOutlines2 } from '@weasel-js/font';
12
12
  import earcut from 'earcut';
13
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
14
13
  import { applyToPoint, rotateAboutPoint, boxToBox, pointInPolygon, segmentsCross } from '@weasel-js/geom';
14
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
15
15
  import polygonClipping from 'polygon-clipping';
16
- import { matchSpec, mimeMatchesGlob } from '@weasel-js/gestures';
16
+ import { matchSpec, parseTargetSpec, mimeMatchesGlob } from '@weasel-js/gestures';
17
17
  import { NORMAL, IMPLICIT_TAGS } from '@weasel-js/modes';
18
18
  import { createHistory } from '@weasel-js/history';
19
19
  export * from '@weasel-js/history';
20
20
 
21
21
  // src/version.ts
22
- var VERSION = "0.8.0" ;
22
+ var VERSION = "1.0.1" ;
23
23
 
24
24
  // src/features/grid/roundToCell.ts
25
25
  function roundToCell(value, cellSize) {
@@ -333,6 +333,60 @@ var GRAD_FILL_UNIFORMS = [
333
333
  ];
334
334
  var GRAD_FILL_ATTRIBUTES = ["a_position"];
335
335
 
336
+ // src/renderer/shaders/patternFill.ts
337
+ var PATTERN_VERT_SRC = (
338
+ /* glsl */
339
+ `#version 300 es
340
+ in vec2 a_position;
341
+ uniform mat3 u_proj;
342
+ uniform mat3 u_model;
343
+ uniform mat3 u_worldInv;
344
+ out vec2 v_world;
345
+ void main() {
346
+ vec3 screen = u_model * vec3(a_position, 1.0);
347
+ vec3 clip = u_proj * vec3(screen.xy, 1.0);
348
+ gl_Position = vec4(clip.xy, 0.0, 1.0);
349
+ vec3 world = u_worldInv * vec3(screen.xy, 1.0);
350
+ v_world = world.xy;
351
+ }
352
+ `
353
+ );
354
+ var PATTERN_FRAG_SRC = (
355
+ /* glsl */
356
+ `#version 300 es
357
+ precision highp float;
358
+ in vec2 v_world;
359
+ uniform sampler2D u_sampler;
360
+ uniform vec2 u_tileOrigin;
361
+ uniform vec2 u_tileSize;
362
+ uniform float u_opacity;
363
+ uniform float u_alpha;
364
+ uniform mat4 u_colorMatrix;
365
+ uniform vec4 u_colorBias;
366
+ out vec4 outColor;
367
+ void main() {
368
+ vec2 uv = (v_world - u_tileOrigin) / max(u_tileSize, vec2(0.0001));
369
+ vec4 texel = texture(u_sampler, uv);
370
+ vec4 mapped = clamp(u_colorMatrix * texel + u_colorBias, 0.0, 1.0);
371
+ float a = mapped.a * u_opacity * u_alpha;
372
+ outColor = vec4(mapped.rgb * a, a);
373
+ }
374
+ `
375
+ );
376
+ var PATTERN_FILL_UNIFORMS = [
377
+ "u_proj",
378
+ "u_model",
379
+ "u_worldInv",
380
+ "u_sampler",
381
+ "u_tileOrigin",
382
+ "u_tileSize",
383
+ "u_opacity",
384
+ "u_alpha",
385
+ "u_colorMatrix",
386
+ "u_colorBias"
387
+ ];
388
+ var PATTERN_FILL_ATTRIBUTES = ["a_position"];
389
+
336
390
  // src/renderer/cache/GLMeshCache.ts
337
391
  var GLMeshCache = class {
338
392
  constructor(gl, aPositionLoc) {
@@ -458,17 +512,20 @@ var GLTextureCache = class {
458
512
  has(id) {
459
513
  return this.map.has(id);
460
514
  }
461
- upload(id, source) {
515
+ /** Uploads once per id; `wrap` therefore applies on first upload only.
516
+ * An id is either a pattern tile or a shader texture, never both. */
517
+ upload(id, source, wrap = "clamp") {
462
518
  if (this.map.has(id)) return id;
463
519
  const gl = this.gl;
464
520
  const tex = gl.createTexture();
465
521
  if (!tex) throw new Error(`GLTextureCache: createTexture failed for id="${id}"`);
522
+ const wrapMode = wrap === "repeat" ? gl.REPEAT : gl.CLAMP_TO_EDGE;
466
523
  gl.bindTexture(gl.TEXTURE_2D, tex);
467
524
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
468
525
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
469
526
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
470
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
471
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
527
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapMode);
528
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrapMode);
472
529
  gl.bindTexture(gl.TEXTURE_2D, null);
473
530
  this.map.set(id, tex);
474
531
  return id;
@@ -1100,6 +1157,18 @@ function scale(m, sx, sy) {
1100
1157
  const s = create(sx, 0, 0, sy, 0, 0);
1101
1158
  return multiply(m, s);
1102
1159
  }
1160
+ function invert(m) {
1161
+ const a = m[0], b = m[1];
1162
+ const c = m[3], d = m[4];
1163
+ const tx = m[6], ty = m[7];
1164
+ const det = a * d - b * c;
1165
+ if (det === 0 || !Number.isFinite(det)) return identity();
1166
+ const ia = d / det;
1167
+ const ib = -b / det;
1168
+ const ic = -c / det;
1169
+ const id = a / det;
1170
+ return create(ia, ib, ic, id, -(ia * tx + ic * ty), -(ib * tx + id * ty));
1171
+ }
1103
1172
  function apply(m, x, y) {
1104
1173
  const a = m[0], b = m[1];
1105
1174
  const c = m[3], d = m[4];
@@ -1127,6 +1196,7 @@ var mat3 = {
1127
1196
  multiply,
1128
1197
  translate,
1129
1198
  scale,
1199
+ invert,
1130
1200
  apply,
1131
1201
  screenToClip
1132
1202
  };
@@ -2154,7 +2224,7 @@ function warnMissingGlyphOnce(family, cp) {
2154
2224
  `weasel layoutRuns: no glyph for U+${cp.toString(16).toUpperCase().padStart(4, "0")} (${JSON.stringify(ch)}) in "${family}", and the dynamic tier could not rasterize it \u2014 skipping the character. Bake it into the atlas, or call registerCanvasFont("${family}") to serve missing codepoints from installed fonts.`
2155
2225
  );
2156
2226
  }
2157
- function layoutRuns(runs, opts, origin) {
2227
+ function layoutRuns(runs, opts) {
2158
2228
  const ctx = { groups: /* @__PURE__ */ new Map() };
2159
2229
  const entries = [];
2160
2230
  let prevCp;
@@ -2324,7 +2394,7 @@ function layoutRuns(runs, opts, origin) {
2324
2394
  }
2325
2395
  }
2326
2396
  const lineBoxes = [];
2327
- let penY = origin.y;
2397
+ let penY = 0;
2328
2398
  let maxLineWidth = 0;
2329
2399
  const finiteWidth = Number.isFinite(opts.maxWidth) ? opts.maxWidth : 0;
2330
2400
  for (const line of lines) {
@@ -2336,7 +2406,7 @@ function layoutRuns(runs, opts, origin) {
2336
2406
  const slack = finiteWidth - line.width;
2337
2407
  return opts.align === "center" ? slack / 2 : slack;
2338
2408
  })();
2339
- const lineX0 = origin.x + alignShift;
2409
+ const lineX0 = alignShift;
2340
2410
  const baselineSource = line.entries[0] ?? line.blank;
2341
2411
  const lineBaselineY = baselineSource ? penY + baselineSource.font.common.base * (baselineSource.fontSize / baselineSource.font.info.size) : penY;
2342
2412
  let penX = lineX0;
@@ -2410,7 +2480,7 @@ function layoutRuns(runs, opts, origin) {
2410
2480
  groups: [...ctx.groups.values()],
2411
2481
  decorations,
2412
2482
  lines: lineBoxes,
2413
- bounds: { width: maxLineWidth, height: penY - origin.y }
2483
+ bounds: { width: maxLineWidth, height: penY }
2414
2484
  };
2415
2485
  }
2416
2486
  var LAYOUT_CACHE_VARIANT_LIMIT = 8;
@@ -2423,10 +2493,10 @@ function outlineBucket(runs, min) {
2423
2493
  for (const size of sizes) if (size >= min) n++;
2424
2494
  return n;
2425
2495
  }
2426
- function variantKey(runs, opts, origin) {
2427
- return `${opts.maxWidth}|${opts.lineHeight}|${opts.align}|${outlineBucket(runs, opts.outlineMinSize)}|${origin.x}|${origin.y}`;
2496
+ function variantKey(runs, opts) {
2497
+ return `${opts.maxWidth}|${opts.lineHeight}|${opts.align}|${outlineBucket(runs, opts.outlineMinSize)}`;
2428
2498
  }
2429
- function cachedLayoutRuns(runs, opts, origin) {
2499
+ function cachedLayoutRuns(runs, opts) {
2430
2500
  const fonts = glyphGeneration();
2431
2501
  let entry = cache2.get(runs);
2432
2502
  if (entry === void 0) {
@@ -2436,10 +2506,10 @@ function cachedLayoutRuns(runs, opts, origin) {
2436
2506
  entry.generation = fonts;
2437
2507
  entry.byVariant.clear();
2438
2508
  }
2439
- const key = variantKey(runs, opts, origin);
2509
+ const key = variantKey(runs, opts);
2440
2510
  const hit = entry.byVariant.get(key);
2441
2511
  if (hit !== void 0) return hit;
2442
- const laid = layoutRuns(runs, opts, origin);
2512
+ const laid = layoutRuns(runs, opts);
2443
2513
  if (entry.byVariant.size >= LAYOUT_CACHE_VARIANT_LIMIT) entry.byVariant.clear();
2444
2514
  entry.byVariant.set(key, laid);
2445
2515
  return laid;
@@ -2757,26 +2827,267 @@ function outlineStrokeMesh(glyphKey, d, emWidth, stroke) {
2757
2827
  return mesh.indices.length === 0 ? null : mesh;
2758
2828
  }
2759
2829
 
2830
+ // src/renderer/solidBatch.ts
2831
+ var MAX_VERTICES_PER_BATCH = 32768;
2832
+ var FLOATS_PER_VERTEX = 6;
2833
+ var INITIAL_VERTICES = 256;
2834
+ var INITIAL_INDICES = 384;
2835
+ var SolidBatch = class {
2836
+ gl;
2837
+ vao;
2838
+ vbo;
2839
+ ibo;
2840
+ verts = new Float32Array(INITIAL_VERTICES * FLOATS_PER_VERTEX);
2841
+ idx = new Uint32Array(INITIAL_INDICES);
2842
+ nVerts = 0;
2843
+ nIdx = 0;
2844
+ /** What the GPU buffers are currently sized for. */
2845
+ vboVerts = 0;
2846
+ iboIdx = 0;
2847
+ constructor(gl, prog) {
2848
+ const aPos = prog.attribute("a_position");
2849
+ const aColor = prog.attribute("a_vertexColor");
2850
+ if (aPos === void 0 || aColor === void 0) {
2851
+ throw new Error("SolidBatch: vertex-color program is missing a_position / a_vertexColor");
2852
+ }
2853
+ const vao = gl.createVertexArray();
2854
+ const vbo = gl.createBuffer();
2855
+ const ibo = gl.createBuffer();
2856
+ if (!vao || !vbo || !ibo) throw new Error("SolidBatch: failed to create GL objects");
2857
+ this.gl = gl;
2858
+ this.vao = vao;
2859
+ this.vbo = vbo;
2860
+ this.ibo = ibo;
2861
+ const stride = FLOATS_PER_VERTEX * 4;
2862
+ gl.bindVertexArray(vao);
2863
+ gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
2864
+ gl.enableVertexAttribArray(aPos);
2865
+ gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, stride, 0);
2866
+ gl.enableVertexAttribArray(aColor);
2867
+ gl.vertexAttribPointer(aColor, 4, gl.FLOAT, false, stride, 8);
2868
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
2869
+ gl.bindVertexArray(null);
2870
+ this.growGpu(INITIAL_VERTICES, INITIAL_INDICES);
2871
+ }
2872
+ get length() {
2873
+ return this.nIdx;
2874
+ }
2875
+ /** Whether staging `vertices` more would put the run past the per-flush cap. */
2876
+ wouldOverflow(vertices) {
2877
+ return this.nVerts + vertices > MAX_VERTICES_PER_BATCH;
2878
+ }
2879
+ /**
2880
+ * Append one rect's four corners through `m`, all carrying `rgba` (straight
2881
+ * alpha). An affine maps a rect to a parallelogram, so two triangles still
2882
+ * cover it and the batch draws at `u_model` identity.
2883
+ */
2884
+ pushRect(x, y, w, h, m, r, g, b, a) {
2885
+ this.reserve(4, 6);
2886
+ const v = this.verts;
2887
+ let i = this.nVerts * FLOATS_PER_VERTEX;
2888
+ const ma = m[0], mb = m[1], mc = m[3], md = m[4], mtx = m[6], mty = m[7];
2889
+ const x1 = x + w;
2890
+ const y1 = y + h;
2891
+ const ax = ma * x + mc * y + mtx, ay = mb * x + md * y + mty;
2892
+ const bx = ma * x1 + mc * y + mtx, by = mb * x1 + md * y + mty;
2893
+ const cx = ma * x1 + mc * y1 + mtx, cy = mb * x1 + md * y1 + mty;
2894
+ const dx = ma * x + mc * y1 + mtx, dy = mb * x + md * y1 + mty;
2895
+ v[i++] = ax;
2896
+ v[i++] = ay;
2897
+ v[i++] = r;
2898
+ v[i++] = g;
2899
+ v[i++] = b;
2900
+ v[i++] = a;
2901
+ v[i++] = bx;
2902
+ v[i++] = by;
2903
+ v[i++] = r;
2904
+ v[i++] = g;
2905
+ v[i++] = b;
2906
+ v[i++] = a;
2907
+ v[i++] = cx;
2908
+ v[i++] = cy;
2909
+ v[i++] = r;
2910
+ v[i++] = g;
2911
+ v[i++] = b;
2912
+ v[i++] = a;
2913
+ v[i++] = dx;
2914
+ v[i++] = dy;
2915
+ v[i++] = r;
2916
+ v[i++] = g;
2917
+ v[i++] = b;
2918
+ v[i++] = a;
2919
+ const base = this.nVerts;
2920
+ let j = this.nIdx;
2921
+ this.idx[j++] = base;
2922
+ this.idx[j++] = base + 1;
2923
+ this.idx[j++] = base + 2;
2924
+ this.idx[j++] = base;
2925
+ this.idx[j++] = base + 2;
2926
+ this.idx[j++] = base + 3;
2927
+ this.nVerts += 4;
2928
+ this.nIdx += 6;
2929
+ }
2930
+ /**
2931
+ * Append a tessellated mesh through `m`, all vertices carrying `rgba`. The
2932
+ * mesh's own indices are rebased onto the staged vertices, which is why the
2933
+ * index buffer is uploaded per flush rather than written once.
2934
+ */
2935
+ pushMesh(mesh, m, r, g, b, a) {
2936
+ const src = mesh.vertices;
2937
+ const srcIdx = mesh.indices;
2938
+ const n = src.length >> 1;
2939
+ this.reserve(n, srcIdx.length);
2940
+ const v = this.verts;
2941
+ let i = this.nVerts * FLOATS_PER_VERTEX;
2942
+ const ma = m[0], mb = m[1], mc = m[3], md = m[4], mtx = m[6], mty = m[7];
2943
+ for (let k = 0; k < n; k++) {
2944
+ const x = src[k * 2];
2945
+ const y = src[k * 2 + 1];
2946
+ v[i++] = ma * x + mc * y + mtx;
2947
+ v[i++] = mb * x + md * y + mty;
2948
+ v[i++] = r;
2949
+ v[i++] = g;
2950
+ v[i++] = b;
2951
+ v[i++] = a;
2952
+ }
2953
+ const base = this.nVerts;
2954
+ const out = this.idx;
2955
+ let j = this.nIdx;
2956
+ for (let k = 0; k < srcIdx.length; k++) out[j++] = base + srcIdx[k];
2957
+ this.nVerts += n;
2958
+ this.nIdx += srcIdx.length;
2959
+ }
2960
+ /** Upload the staged geometry and bind the batch VAO. Returns the index
2961
+ * count for the caller's `drawElements`. */
2962
+ uploadAndBind() {
2963
+ const gl = this.gl;
2964
+ if (this.nVerts > this.vboVerts || this.nIdx > this.iboIdx) {
2965
+ this.growGpu(this.nVerts, this.nIdx);
2966
+ }
2967
+ gl.bindVertexArray(this.vao);
2968
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo);
2969
+ gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.verts, 0, this.nVerts * FLOATS_PER_VERTEX);
2970
+ gl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, 0, this.idx, 0, this.nIdx);
2971
+ return this.nIdx;
2972
+ }
2973
+ reset() {
2974
+ this.nVerts = 0;
2975
+ this.nIdx = 0;
2976
+ }
2977
+ dispose() {
2978
+ const gl = this.gl;
2979
+ gl.deleteBuffer(this.vbo);
2980
+ gl.deleteBuffer(this.ibo);
2981
+ gl.deleteVertexArray(this.vao);
2982
+ }
2983
+ /** Grow the CPU arrays so `vertices` / `indices` more fit. */
2984
+ reserve(vertices, indices) {
2985
+ const needVerts = (this.nVerts + vertices) * FLOATS_PER_VERTEX;
2986
+ if (needVerts > this.verts.length) {
2987
+ let len = this.verts.length;
2988
+ while (len < needVerts) len *= 2;
2989
+ const grown = new Float32Array(len);
2990
+ grown.set(this.verts);
2991
+ this.verts = grown;
2992
+ }
2993
+ const needIdx = this.nIdx + indices;
2994
+ if (needIdx > this.idx.length) {
2995
+ let len = this.idx.length;
2996
+ while (len < needIdx) len *= 2;
2997
+ const grown = new Uint32Array(len);
2998
+ grown.set(this.idx);
2999
+ this.idx = grown;
3000
+ }
3001
+ }
3002
+ /** Size both GPU buffers for at least what is staged, doubling to amortize. */
3003
+ growGpu(vertices, indices) {
3004
+ const gl = this.gl;
3005
+ if (vertices > this.vboVerts) {
3006
+ let capacity = Math.max(this.vboVerts, INITIAL_VERTICES);
3007
+ while (capacity < vertices) capacity *= 2;
3008
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo);
3009
+ gl.bufferData(gl.ARRAY_BUFFER, capacity * FLOATS_PER_VERTEX * 4, gl.DYNAMIC_DRAW);
3010
+ this.vboVerts = capacity;
3011
+ }
3012
+ if (indices > this.iboIdx) {
3013
+ let capacity = Math.max(this.iboIdx, INITIAL_INDICES);
3014
+ while (capacity < indices) capacity *= 2;
3015
+ gl.bindVertexArray(this.vao);
3016
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.ibo);
3017
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, capacity * 4, gl.DYNAMIC_DRAW);
3018
+ gl.bindVertexArray(null);
3019
+ this.iboIdx = capacity;
3020
+ }
3021
+ }
3022
+ };
3023
+
2760
3024
  // src/renderer/draw.ts
2761
- function setColorMatrixUniforms(ctx, prog) {
3025
+ var FRAME_UPLOADS = /* @__PURE__ */ new WeakMap();
3026
+ var FRAME_PROJ = /* @__PURE__ */ new WeakMap();
3027
+ function uploadedFor(ctx, prog) {
3028
+ let byProgram = FRAME_UPLOADS.get(ctx);
3029
+ if (!byProgram) {
3030
+ byProgram = /* @__PURE__ */ new WeakMap();
3031
+ FRAME_UPLOADS.set(ctx, byProgram);
3032
+ }
3033
+ let uploaded = byProgram.get(prog);
3034
+ if (!uploaded) {
3035
+ uploaded = {};
3036
+ byProgram.set(prog, uploaded);
3037
+ }
3038
+ return uploaded;
3039
+ }
3040
+ function sameValues(prev, next) {
3041
+ if (prev === void 0 || prev.length !== next.length) return false;
3042
+ for (let i = 0; i < prev.length; i++) if (prev[i] !== next[i]) return false;
3043
+ return true;
3044
+ }
3045
+ var BATCH_MODEL = mat3.identity();
3046
+ function projFor(ctx) {
3047
+ let proj = FRAME_PROJ.get(ctx);
3048
+ if (!proj) {
3049
+ proj = mat3.screenToClip(ctx.widthCss, ctx.heightCss);
3050
+ FRAME_PROJ.set(ctx, proj);
3051
+ }
3052
+ return proj;
3053
+ }
3054
+ var COLOR_MATRIX_SCRATCH = new Float32Array(16);
3055
+ function setColorMatrixUniforms(ctx, prog, cm = ctx.state.colorMatrix) {
2762
3056
  const gl = ctx.gl;
2763
- const cm = ctx.state.colorMatrix;
2764
- const m4 = new Float32Array(16);
2765
- for (let row = 0; row < 4; row++) {
2766
- for (let col = 0; col < 4; col++) {
2767
- m4[col * 4 + row] = cm[row * 5 + col];
3057
+ const uploaded = uploadedFor(ctx, prog);
3058
+ const mLoc = prog.uniform("u_colorMatrix");
3059
+ if (mLoc !== void 0) {
3060
+ for (let row = 0; row < 4; row++) {
3061
+ for (let col = 0; col < 4; col++) {
3062
+ COLOR_MATRIX_SCRATCH[col * 4 + row] = cm[row * 5 + col];
3063
+ }
3064
+ }
3065
+ if (!sameValues(uploaded.colorMatrix, COLOR_MATRIX_SCRATCH)) {
3066
+ gl.uniformMatrix4fv(mLoc, false, COLOR_MATRIX_SCRATCH);
3067
+ uploaded.colorMatrix = Float32Array.from(COLOR_MATRIX_SCRATCH);
2768
3068
  }
2769
3069
  }
2770
- const mLoc = prog.uniform("u_colorMatrix");
2771
3070
  const bLoc = prog.uniform("u_colorBias");
2772
- if (mLoc !== void 0) gl.uniformMatrix4fv(mLoc, false, m4);
2773
- if (bLoc !== void 0) gl.uniform4f(bLoc, cm[4], cm[9], cm[14], cm[19]);
3071
+ if (bLoc !== void 0) {
3072
+ const bias = [cm[4], cm[9], cm[14], cm[19]];
3073
+ if (!sameValues(uploaded.colorBias, bias)) {
3074
+ gl.uniform4f(bLoc, bias[0], bias[1], bias[2], bias[3]);
3075
+ uploaded.colorBias = Float32Array.from(bias);
3076
+ }
3077
+ }
2774
3078
  }
2775
- function fillMeshHandle(ctx, path) {
3079
+ function fillMesh(ctx, path) {
2776
3080
  if (ctx.flattenTolerance !== void 0) {
2777
- return ctx.meshCache.uploadTransient(tessellate(path, { flattenTolerance: ctx.flattenTolerance }));
3081
+ return tessellate(path, { flattenTolerance: ctx.flattenTolerance });
2778
3082
  }
2779
- return ctx.meshCache.handleFor(getMesh(path));
3083
+ return getMesh(path);
3084
+ }
3085
+ function meshHandle(ctx, mesh) {
3086
+ if (ctx.flattenTolerance !== void 0) return ctx.meshCache.uploadTransient(mesh);
3087
+ return ctx.meshCache.handleFor(mesh);
3088
+ }
3089
+ function fillMeshHandle(ctx, path) {
3090
+ return meshHandle(ctx, fillMesh(ctx, path));
2780
3091
  }
2781
3092
  function dispatch(ctx, cmd) {
2782
3093
  switch (cmd.kind) {
@@ -2785,10 +3096,13 @@ function dispatch(ctx, cmd) {
2785
3096
  case "path":
2786
3097
  return drawPath(ctx, cmd);
2787
3098
  case "text":
3099
+ flushSolids(ctx);
2788
3100
  return drawText(ctx, cmd);
2789
3101
  case "image":
3102
+ flushSolids(ctx);
2790
3103
  return drawImage(ctx, cmd);
2791
3104
  case "shader":
3105
+ flushSolids(ctx);
2792
3106
  return drawShader(ctx, cmd);
2793
3107
  }
2794
3108
  }
@@ -2917,11 +3231,13 @@ function drawGroup(ctx, cmd) {
2917
3231
  "weasel: clip nesting depth exceeded (max 7). You can't nest more than 7 levels of clipped containers in a single draw tree. Flatten the hierarchy or compose poses outside the scene graph."
2918
3232
  );
2919
3233
  }
3234
+ flushSolids(ctx);
2920
3235
  pushClip(ctx, cmd.clip, newDepth);
2921
3236
  ctx.clipDepth = newDepth;
2922
3237
  }
2923
3238
  for (const child of cmd.children) dispatch(ctx, child);
2924
3239
  if (cmd.clip) {
3240
+ flushSolids(ctx);
2925
3241
  popClip(ctx, cmd.clip, ctx.clipDepth - 1);
2926
3242
  ctx.clipDepth -= 1;
2927
3243
  }
@@ -2930,38 +3246,98 @@ function drawGroup(ctx, cmd) {
2930
3246
  function drawPath(ctx, cmd) {
2931
3247
  if (!cmd.fill && !cmd.stroke) return;
2932
3248
  if (cmd.fill) {
2933
- const isSolidRectFast = cmd.path.kind === "rect" && (cmd.fill.fill === void 0 || cmd.fill.fill === "solid") && (!cmd.vertexColors || cmd.vertexColors.length === 0) && ctx.rectVao !== null && ctx.rectVbo !== null;
2934
- if (isSolidRectFast && cmd.path.kind === "rect") {
2935
- drawRectFast(ctx, cmd.path, cmd.fill);
3249
+ const batchablePaint = (cmd.fill.fill === void 0 || cmd.fill.fill === "solid") && (!cmd.vertexColors || cmd.vertexColors.length === 0);
3250
+ const solid = cmd.fill;
3251
+ if (batchablePaint && cmd.path.kind === "rect") {
3252
+ pushRect(ctx, cmd.path, solid);
2936
3253
  } else {
2937
- const handle = fillMeshHandle(ctx, cmd.path);
2938
- if (cmd.vertexColors && cmd.vertexColors.length > 0 && (cmd.fill.fill === void 0 || cmd.fill.fill === "solid")) {
2939
- drawPathFillVColor(ctx, cmd, cmd.fill, handle);
2940
- } else if (handle.requiresStencil) {
2941
- drawPathFillStencil(ctx, cmd.fill, handle);
3254
+ const mesh = fillMesh(ctx, cmd.path);
3255
+ if (batchablePaint && canBatchMesh(mesh)) {
3256
+ pushMesh(ctx, mesh, solid);
2942
3257
  } else {
2943
- drawPathFillByKind(ctx, cmd.fill, handle);
3258
+ flushSolids(ctx);
3259
+ const handle = meshHandle(ctx, mesh);
3260
+ if (cmd.vertexColors && cmd.vertexColors.length > 0 && (cmd.fill.fill === void 0 || cmd.fill.fill === "solid")) {
3261
+ drawPathFillVColor(ctx, cmd, solid, handle);
3262
+ } else if (handle.requiresStencil) {
3263
+ drawPathFillStencil(ctx, cmd.fill, handle);
3264
+ } else {
3265
+ drawPathFillByKind(ctx, cmd.fill, handle);
3266
+ }
2944
3267
  }
2945
3268
  }
2946
3269
  }
2947
- if (cmd.stroke) {
2948
- drawPathStroke(ctx, cmd);
3270
+ if (cmd.stroke) drawPathStroke(ctx, cmd);
3271
+ }
3272
+ function isIdentityColorMatrix(cm) {
3273
+ return cm === IDENTITY_COLOR_MATRIX || sameValues(IDENTITY_COLOR_MATRIX, cm);
3274
+ }
3275
+ function stagedStateIsLive(ctx, staged) {
3276
+ if (staged.clipDepth !== ctx.clipDepth) return false;
3277
+ if (!staged.foldsAlpha && staged.alpha !== ctx.state.alpha) return false;
3278
+ const colorMatrix = ctx.state.colorMatrix;
3279
+ return staged.colorMatrix === colorMatrix || sameValues(staged.colorMatrix, colorMatrix);
3280
+ }
3281
+ var MAX_BATCHED_MESH_VERTICES = 256;
3282
+ function canBatchMesh(mesh) {
3283
+ return !mesh.requiresStencil && mesh.vertices.length >> 1 <= MAX_BATCHED_MESH_VERTICES;
3284
+ }
3285
+ function stageSolid(ctx, vertices) {
3286
+ if (ctx.solidState !== void 0 && !stagedStateIsLive(ctx, ctx.solidState)) flushSolids(ctx);
3287
+ if (ctx.solidBatch.wouldOverflow(vertices)) flushSolids(ctx);
3288
+ if (ctx.solidState === void 0) {
3289
+ const colorMatrix = ctx.state.colorMatrix;
3290
+ const foldsAlpha = isIdentityColorMatrix(colorMatrix);
3291
+ ctx.solidState = {
3292
+ alpha: foldsAlpha ? 1 : ctx.state.alpha,
3293
+ colorMatrix,
3294
+ clipDepth: ctx.clipDepth,
3295
+ foldsAlpha
3296
+ };
2949
3297
  }
3298
+ return ctx.solidState;
3299
+ }
3300
+ function stagedColor(ctx, staged, paint) {
3301
+ const [r, g, b, a] = resolveColor(paint.color);
3302
+ return [r, g, b, a * (paint.opacity ?? 1) * (staged.foldsAlpha ? ctx.state.alpha : 1)];
3303
+ }
3304
+ function pushRect(ctx, rect, fill) {
3305
+ const staged = stageSolid(ctx, 4);
3306
+ const [r, g, b, a] = stagedColor(ctx, staged, fill);
3307
+ ctx.solidBatch.pushRect(
3308
+ rect.x,
3309
+ rect.y,
3310
+ rect.width,
3311
+ rect.height,
3312
+ ctx.state.transform,
3313
+ r,
3314
+ g,
3315
+ b,
3316
+ a
3317
+ );
2950
3318
  }
2951
- function drawRectFast(ctx, rect, fill) {
3319
+ function pushMesh(ctx, mesh, paint) {
3320
+ const staged = stageSolid(ctx, mesh.vertices.length >> 1);
3321
+ const [r, g, b, a] = stagedColor(ctx, staged, paint);
3322
+ ctx.solidBatch.pushMesh(mesh, ctx.state.transform, r, g, b, a);
3323
+ }
3324
+ function flushSolids(ctx) {
3325
+ const batch = ctx.solidBatch;
3326
+ const staged = ctx.solidState;
3327
+ if (batch.length === 0 || staged === void 0) return;
2952
3328
  const gl = ctx.gl;
2953
- const { x, y, width: w, height: h } = rect;
2954
- const corners = new Float32Array([x, y, x + w, y, x + w, y + h, x, y + h]);
2955
- gl.useProgram(ctx.pathFill.handle);
2956
- gl.bindVertexArray(ctx.rectVao);
2957
- gl.bindBuffer(gl.ARRAY_BUFFER, ctx.rectVbo);
2958
- gl.bufferSubData(gl.ARRAY_BUFFER, 0, corners);
2959
- setProjAndModel(ctx, ctx.pathFill);
2960
- setSolidPaintUniforms(ctx, ctx.pathFill, fill.color, fill.opacity);
2961
- setColorMatrixUniforms(ctx, ctx.pathFill);
2962
- applyClipTest(ctx);
2963
- gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, 0);
3329
+ const prog = ctx.pathFillVColor;
3330
+ gl.useProgram(prog.handle);
3331
+ const indexCount = batch.uploadAndBind();
3332
+ setProjAndModel(ctx, prog, BATCH_MODEL);
3333
+ gl.uniform4f(prog.uniform("u_color"), 1, 1, 1, 1);
3334
+ gl.uniform1f(prog.uniform("u_alpha"), staged.alpha);
3335
+ setColorMatrixUniforms(ctx, prog, staged.colorMatrix);
3336
+ applyClipTest(ctx, staged.clipDepth);
3337
+ gl.drawElements(gl.TRIANGLES, indexCount, gl.UNSIGNED_INT, 0);
2964
3338
  gl.bindVertexArray(null);
3339
+ batch.reset();
3340
+ ctx.solidState = void 0;
2965
3341
  }
2966
3342
  function expandAnchorColors(perAnchor, handle) {
2967
3343
  const aA = handle.anchorA;
@@ -3006,11 +3382,18 @@ function drawPathFillVColor(ctx, cmd, fill, handle) {
3006
3382
  gl.bindVertexArray(null);
3007
3383
  gl.deleteBuffer(colorVbo);
3008
3384
  }
3009
- function setProjAndModel(ctx, prog) {
3385
+ function setProjAndModel(ctx, prog, model = ctx.state.transform) {
3010
3386
  const gl = ctx.gl;
3011
- const proj = mat3.screenToClip(ctx.widthCss, ctx.heightCss);
3012
- gl.uniformMatrix3fv(prog.uniform("u_proj"), false, proj);
3013
- gl.uniformMatrix3fv(prog.uniform("u_model"), false, ctx.state.transform);
3387
+ const uploaded = uploadedFor(ctx, prog);
3388
+ const proj = projFor(ctx);
3389
+ if (!sameValues(uploaded.proj, proj)) {
3390
+ gl.uniformMatrix3fv(prog.uniform("u_proj"), false, proj);
3391
+ uploaded.proj = Float32Array.from(proj);
3392
+ }
3393
+ if (!sameValues(uploaded.model, model)) {
3394
+ gl.uniformMatrix3fv(prog.uniform("u_model"), false, model);
3395
+ uploaded.model = Float32Array.from(model);
3396
+ }
3014
3397
  }
3015
3398
  function setSolidPaintUniforms(ctx, prog, color, opacity) {
3016
3399
  const gl = ctx.gl;
@@ -3048,28 +3431,42 @@ function drawPathFillPattern(ctx, fill, handle) {
3048
3431
  if (isDev3) console.warn(`weasel: pattern TextureHandle "${tex.id}" not registered`);
3049
3432
  return;
3050
3433
  }
3051
- ctx.textureCache.upload(tex.id, entry.source);
3434
+ ctx.textureCache.upload(tex.id, entry.source, "repeat");
3052
3435
  const gl = ctx.gl;
3053
- gl.useProgram(ctx.imageFill.handle);
3436
+ gl.useProgram(ctx.patternFill.handle);
3054
3437
  gl.bindVertexArray(handle.vao);
3055
- setProjAndModel(ctx, ctx.imageFill);
3056
- setColorMatrixUniforms(ctx, ctx.imageFill);
3438
+ setProjAndModel(ctx, ctx.patternFill);
3439
+ setColorMatrixUniforms(ctx, ctx.patternFill);
3440
+ gl.uniformMatrix3fv(ctx.patternFill.uniform("u_worldInv"), false, gradientSpaceInverse(ctx, fill.units));
3441
+ const [tw, th] = textureSize(entry.source);
3442
+ const origin = fill.origin ?? { x: 0, y: 0 };
3443
+ gl.uniform2f(ctx.patternFill.uniform("u_tileOrigin"), origin.x, origin.y);
3444
+ gl.uniform2f(ctx.patternFill.uniform("u_tileSize"), tw, th);
3057
3445
  ctx.textureCache.bind(tex.id, 0);
3058
- gl.uniform1i(ctx.imageFill.uniform("u_sampler"), 0);
3059
- gl.uniform1f(ctx.imageFill.uniform("u_opacity"), fill.opacity ?? 1);
3060
- gl.uniform1f(ctx.imageFill.uniform("u_alpha"), ctx.state.alpha);
3446
+ gl.uniform1i(ctx.patternFill.uniform("u_sampler"), 0);
3447
+ gl.uniform1f(ctx.patternFill.uniform("u_opacity"), fill.opacity ?? 1);
3448
+ gl.uniform1f(ctx.patternFill.uniform("u_alpha"), ctx.state.alpha);
3061
3449
  applyClipTest(ctx);
3062
3450
  gl.drawElements(gl.TRIANGLES, handle.indexCount, gl.UNSIGNED_INT, 0);
3063
3451
  gl.bindVertexArray(null);
3064
3452
  }
3453
+ function textureSize(source) {
3454
+ const w = "naturalWidth" in source ? source.naturalWidth : source.width;
3455
+ const h = "naturalHeight" in source ? source.naturalHeight : source.height;
3456
+ return [w || 1, h || 1];
3457
+ }
3458
+ function gradientSpaceInverse(ctx, units) {
3459
+ if (units === "local") return mat3.invert(ctx.state.transform);
3460
+ if (units === "world" && ctx.viewMatrix) return mat3.invert(ctx.viewMatrix);
3461
+ return mat3.identity();
3462
+ }
3065
3463
  function drawPathFillGradient(ctx, fill, handle) {
3066
3464
  const gl = ctx.gl;
3067
3465
  const key = ctx.gradRampCache.upload(fill.stops);
3068
3466
  gl.useProgram(ctx.gradFill.handle);
3069
3467
  gl.bindVertexArray(handle.vao);
3070
3468
  setProjAndModel(ctx, ctx.gradFill);
3071
- const identity3 = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
3072
- gl.uniformMatrix3fv(ctx.gradFill.uniform("u_worldInv"), false, identity3);
3469
+ gl.uniformMatrix3fv(ctx.gradFill.uniform("u_worldInv"), false, gradientSpaceInverse(ctx, fill.units));
3073
3470
  ctx.gradRampCache.bind(key, 0);
3074
3471
  gl.uniform1i(ctx.gradFill.uniform("u_ramp"), 0);
3075
3472
  gl.uniform1f(ctx.gradFill.uniform("u_alpha"), ctx.state.alpha);
@@ -3103,9 +3500,8 @@ function drawPathFillGradient(ctx, fill, handle) {
3103
3500
  gl.drawElements(gl.TRIANGLES, handle.indexCount, gl.UNSIGNED_INT, 0);
3104
3501
  gl.bindVertexArray(null);
3105
3502
  }
3106
- function applyClipTest(ctx) {
3503
+ function applyClipTest(ctx, depth = ctx.clipDepth) {
3107
3504
  const gl = ctx.gl;
3108
- const depth = ctx.clipDepth;
3109
3505
  if (depth === 0) {
3110
3506
  gl.disable(gl.STENCIL_TEST);
3111
3507
  return;
@@ -3189,6 +3585,7 @@ function drawPathStroke(ctx, cmd) {
3189
3585
  }
3190
3586
  const align = stroke.align ?? "center";
3191
3587
  if (cmd.path.kind === "polygon" && align !== "center") {
3588
+ flushSolids(ctx);
3192
3589
  drawPathStrokeStenciled(ctx, cmd, align);
3193
3590
  return;
3194
3591
  }
@@ -3199,6 +3596,12 @@ function drawPathStrokeUnclipped(ctx, cmd) {
3199
3596
  const solid = stroke.paint;
3200
3597
  const mesh = tessellateStroke(cmd.path, stroke, { flattenTolerance: ctx.flattenTolerance });
3201
3598
  if (mesh.indices.length === 0) return;
3599
+ const hasVColors = !!(stroke.vertexColors && stroke.vertexColors.length > 0);
3600
+ if (!hasVColors && canBatchMesh(mesh)) {
3601
+ pushMesh(ctx, mesh, solid);
3602
+ return;
3603
+ }
3604
+ flushSolids(ctx);
3202
3605
  const handle = ctx.meshCache.uploadTransient(mesh);
3203
3606
  const gl = ctx.gl;
3204
3607
  if (stroke.vertexColors && stroke.vertexColors.length > 0) {
@@ -3296,20 +3699,17 @@ function drawText(ctx, cmd) {
3296
3699
  const maxWidth = cmd.maxWidth ?? Infinity;
3297
3700
  const minScreen = ctx.textOutlineMinScreenSize ?? OUTLINE_MIN_SCREEN_PX;
3298
3701
  const outlineMinSize = Number.isFinite(minScreen) ? minScreen / modelScale(ctx.state.transform) : void 0;
3299
- const laid = cachedLayoutRuns(
3300
- cmd.runs,
3301
- { maxWidth, lineHeight, align, outlineMinSize },
3302
- { x: cmd.x, y: cmd.y }
3303
- );
3702
+ const laid = cachedLayoutRuns(cmd.runs, { maxWidth, lineHeight, align, outlineMinSize });
3304
3703
  if (laid.groups.length === 0 && laid.decorations.length === 0) return;
3305
- const dy = verticalAlignOffset(cmd.verticalAlign, cmd.height, laid.bounds.height);
3704
+ const dx = cmd.x;
3705
+ const dy = cmd.y + verticalAlignOffset(cmd.verticalAlign, cmd.height, laid.bounds.height);
3306
3706
  const gl = ctx.gl;
3307
3707
  applyClipTest(ctx);
3308
3708
  const preparedPrograms = /* @__PURE__ */ new Set();
3309
3709
  let currentProg = null;
3310
3710
  for (const group of laid.groups) {
3311
3711
  if (group.source === "outline") {
3312
- drawTextOutlineGroup(ctx, group, dy);
3712
+ drawTextOutlineGroup(ctx, group, dx, dy);
3313
3713
  currentProg = null;
3314
3714
  continue;
3315
3715
  }
@@ -3324,31 +3724,31 @@ function drawText(ctx, cmd) {
3324
3724
  setColorMatrixUniforms(ctx, prog);
3325
3725
  gl.uniform1f(prog.uniform("u_alpha"), ctx.state.alpha);
3326
3726
  }
3327
- drawTextGroup(ctx, group, prog, dy);
3727
+ drawTextGroup(ctx, group, prog, dx, dy);
3328
3728
  }
3329
- drawTextDecorations(ctx, laid.decorations, dy);
3729
+ drawTextDecorations(ctx, laid.decorations, dx, dy);
3330
3730
  }
3331
3731
  var SYNTHETIC_ITALIC_RADIANS = 0.2094;
3332
- function drawTextOutlineGroup(ctx, group, dy) {
3333
- const mesh = outlineGroupMesh(group, dy);
3732
+ function drawTextOutlineGroup(ctx, group, dx, dy) {
3733
+ const mesh = outlineGroupMesh(group, dx, dy);
3334
3734
  if (mesh) drawPathFillByKind(ctx, group.fill, ctx.meshCache.uploadTransient(mesh));
3335
3735
  if (!group.stroke) return;
3336
- const strokeMesh = outlineGroupStrokeMesh(group, dy);
3736
+ const strokeMesh = outlineGroupStrokeMesh(group, dx, dy);
3337
3737
  if (strokeMesh) {
3338
3738
  drawPathFillByKind(ctx, group.stroke.paint, ctx.meshCache.uploadTransient(strokeMesh));
3339
3739
  }
3340
3740
  }
3341
- function outlineGroupMesh(group, dy) {
3342
- return mergeGlyphMeshes(group, dy, (glyph) => outlineMesh(glyph.key, glyph.d));
3741
+ function outlineGroupMesh(group, dx, dy) {
3742
+ return mergeGlyphMeshes(group, dx, dy, (glyph) => outlineMesh(glyph.key, glyph.d));
3343
3743
  }
3344
- function outlineGroupStrokeMesh(group, dy) {
3744
+ function outlineGroupStrokeMesh(group, dx, dy) {
3345
3745
  const stroke = group.stroke;
3346
3746
  if (!stroke) return null;
3347
3747
  const width = stroke.width ?? 1;
3348
3748
  if (!(width > 0)) return null;
3349
- return mergeGlyphMeshes(group, dy, (glyph) => glyph.scale > 0 ? outlineStrokeMesh(glyph.key, glyph.d, quantizeEmWidth(width / glyph.scale), stroke) : null);
3749
+ return mergeGlyphMeshes(group, dx, dy, (glyph) => glyph.scale > 0 ? outlineStrokeMesh(glyph.key, glyph.d, quantizeEmWidth(width / glyph.scale), stroke) : null);
3350
3750
  }
3351
- function mergeGlyphMeshes(group, dy, meshFor) {
3751
+ function mergeGlyphMeshes(group, dx, dy, meshFor) {
3352
3752
  const parts = [];
3353
3753
  let vertexFloats = 0;
3354
3754
  let indexCount = 0;
@@ -3371,7 +3771,7 @@ function mergeGlyphMeshes(group, dy, meshFor) {
3371
3771
  for (let k = 0; k < mesh.vertices.length; k += 2) {
3372
3772
  const ex = mesh.vertices[k];
3373
3773
  const ey = mesh.vertices[k + 1];
3374
- vertices[vi++] = x + (ex - ey * shear) * scale2;
3774
+ vertices[vi++] = x + dx + (ex - ey * shear) * scale2;
3375
3775
  vertices[vi++] = baselineY + dy + ey * scale2;
3376
3776
  }
3377
3777
  for (let k = 0; k < mesh.indices.length; k++) indices[ii++] = base + mesh.indices[k];
@@ -3379,7 +3779,7 @@ function mergeGlyphMeshes(group, dy, meshFor) {
3379
3779
  }
3380
3780
  return { vertices, indices };
3381
3781
  }
3382
- function drawTextDecorations(ctx, decorations, dy) {
3782
+ function drawTextDecorations(ctx, decorations, dx, dy) {
3383
3783
  if (decorations.length === 0) return;
3384
3784
  const batches = /* @__PURE__ */ new Map();
3385
3785
  for (const d of decorations) {
@@ -3402,14 +3802,15 @@ function drawTextDecorations(ctx, decorations, dy) {
3402
3802
  const vertices = new Float32Array(rects.length * 4 * 2);
3403
3803
  let vi = 0;
3404
3804
  for (const d of rects) {
3805
+ const dx0 = d.x0 + dx, dx1 = d.x1 + dx;
3405
3806
  const dy0 = d.y0 + dy, dy1 = d.y1 + dy;
3406
- vertices[vi++] = d.x0;
3807
+ vertices[vi++] = dx0;
3407
3808
  vertices[vi++] = dy0;
3408
- vertices[vi++] = d.x1;
3809
+ vertices[vi++] = dx1;
3409
3810
  vertices[vi++] = dy0;
3410
- vertices[vi++] = d.x0;
3811
+ vertices[vi++] = dx0;
3411
3812
  vertices[vi++] = dy1;
3412
- vertices[vi++] = d.x1;
3813
+ vertices[vi++] = dx1;
3413
3814
  vertices[vi++] = dy1;
3414
3815
  }
3415
3816
  const indices = new Uint32Array(rects.length * 6);
@@ -3448,7 +3849,7 @@ function drawTextDecorations(ctx, decorations, dy) {
3448
3849
  gl.deleteBuffer(ibo);
3449
3850
  }
3450
3851
  }
3451
- function drawTextGroup(ctx, group, prog, dy) {
3852
+ function drawTextGroup(ctx, group, prog, dx, dy) {
3452
3853
  if (group.source === "canvas") {
3453
3854
  if (!syncDynamicPageTexture(ctx.textureCache, group.page)) return;
3454
3855
  } else {
@@ -3459,23 +3860,24 @@ function drawTextGroup(ctx, group, prog, dy) {
3459
3860
  const vertices = new Float32Array(group.quads.length * 4 * 5);
3460
3861
  let vi = 0;
3461
3862
  for (const q of group.quads) {
3863
+ const x0 = q.x0 + dx, x1 = q.x1 + dx;
3462
3864
  const y0 = q.y0 + dy, y1 = q.y1 + dy, by = q.baselineY + dy;
3463
- vertices[vi++] = q.x0;
3865
+ vertices[vi++] = x0;
3464
3866
  vertices[vi++] = y0;
3465
3867
  vertices[vi++] = q.u0;
3466
3868
  vertices[vi++] = q.v0;
3467
3869
  vertices[vi++] = by;
3468
- vertices[vi++] = q.x1;
3870
+ vertices[vi++] = x1;
3469
3871
  vertices[vi++] = y0;
3470
3872
  vertices[vi++] = q.u1;
3471
3873
  vertices[vi++] = q.v0;
3472
3874
  vertices[vi++] = by;
3473
- vertices[vi++] = q.x0;
3875
+ vertices[vi++] = x0;
3474
3876
  vertices[vi++] = y1;
3475
3877
  vertices[vi++] = q.u0;
3476
3878
  vertices[vi++] = q.v1;
3477
3879
  vertices[vi++] = by;
3478
- vertices[vi++] = q.x1;
3880
+ vertices[vi++] = x1;
3479
3881
  vertices[vi++] = y1;
3480
3882
  vertices[vi++] = q.u1;
3481
3883
  vertices[vi++] = q.v1;
@@ -3589,6 +3991,11 @@ function drawImage(ctx, cmd) {
3589
3991
  setProjAndModel(ctx, ctx.imageFill);
3590
3992
  setColorMatrixUniforms(ctx, ctx.imageFill);
3591
3993
  ctx.imageCache.bind(cmd.image, 0);
3994
+ gl.texParameteri(
3995
+ gl.TEXTURE_2D,
3996
+ gl.TEXTURE_MAG_FILTER,
3997
+ cmd.sampling === "nearest" ? gl.NEAREST : gl.LINEAR
3998
+ );
3592
3999
  gl.uniform1i(ctx.imageFill.uniform("u_sampler"), 0);
3593
4000
  gl.uniform1f(ctx.imageFill.uniform("u_opacity"), cmd.opacity ?? 1);
3594
4001
  gl.uniform1f(ctx.imageFill.uniform("u_alpha"), ctx.state.alpha);
@@ -3689,6 +4096,7 @@ var WeaselRenderer = class {
3689
4096
  textSdfR8;
3690
4097
  imageFill;
3691
4098
  gradFill;
4099
+ patternFill;
3692
4100
  meshCache;
3693
4101
  textureCache;
3694
4102
  imageCache;
@@ -3696,10 +4104,7 @@ var WeaselRenderer = class {
3696
4104
  programRegistry = /* @__PURE__ */ new Map();
3697
4105
  quadVbo = null;
3698
4106
  quadIbo = null;
3699
- /** Shared rect-fill geometry: 4 verts × 2 floats (dynamic), 6-index static IBO. */
3700
- rectVao = null;
3701
- rectVbo = null;
3702
- rectIbo = null;
4107
+ solidBatch;
3703
4108
  groupState = new GroupState();
3704
4109
  widthCss;
3705
4110
  heightCss;
@@ -3764,6 +4169,9 @@ var WeaselRenderer = class {
3764
4169
  this.gradFill = new ShaderProgram(this.gl, GRAD_VERT_SRC, GRAD_FRAG_SRC);
3765
4170
  this.gradFill.lookupUniforms(GRAD_FILL_UNIFORMS);
3766
4171
  this.gradFill.lookupAttributes(GRAD_FILL_ATTRIBUTES);
4172
+ this.patternFill = new ShaderProgram(this.gl, PATTERN_VERT_SRC, PATTERN_FRAG_SRC);
4173
+ this.patternFill.lookupUniforms(PATTERN_FILL_UNIFORMS);
4174
+ this.patternFill.lookupAttributes(PATTERN_FILL_ATTRIBUTES);
3767
4175
  const aPos = this.pathFill.attribute("a_position");
3768
4176
  if (aPos === void 0) throw new Error("WeaselRenderer: a_position not found in path-fill shader");
3769
4177
  this.meshCache = new GLMeshCache(this.gl, aPos);
@@ -3771,31 +4179,7 @@ var WeaselRenderer = class {
3771
4179
  this.imageCache = new GLImageCache(this.gl, this.imageMinification);
3772
4180
  this.gradRampCache = new GradientRampCache(this.gl);
3773
4181
  this.uploadQuadGeometry();
3774
- this.uploadRectGeometry(aPos);
3775
- }
3776
- /** Allocate the shared rect VAO + dynamic VBO (4 verts × 2 floats) + static
3777
- * IBO (6 indices). drawRectFast bufferSubData's the 4 corner coords each
3778
- * draw, avoiding per-rect buffer allocation in animated demos. */
3779
- uploadRectGeometry(aPositionLoc) {
3780
- const gl = this.gl;
3781
- this.rectVao = gl.createVertexArray();
3782
- this.rectVbo = gl.createBuffer();
3783
- this.rectIbo = gl.createBuffer();
3784
- if (!this.rectVao || !this.rectVbo || !this.rectIbo) {
3785
- throw new Error("WeaselRenderer: failed to create rect-fast geometry");
3786
- }
3787
- gl.bindVertexArray(this.rectVao);
3788
- gl.bindBuffer(gl.ARRAY_BUFFER, this.rectVbo);
3789
- gl.bufferData(gl.ARRAY_BUFFER, 8 * 4, gl.DYNAMIC_DRAW);
3790
- gl.enableVertexAttribArray(aPositionLoc);
3791
- gl.vertexAttribPointer(aPositionLoc, 2, gl.FLOAT, false, 0, 0);
3792
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.rectIbo);
3793
- gl.bufferData(
3794
- gl.ELEMENT_ARRAY_BUFFER,
3795
- new Uint32Array([0, 1, 2, 0, 2, 3]),
3796
- gl.STATIC_DRAW
3797
- );
3798
- gl.bindVertexArray(null);
4182
+ this.solidBatch = new SolidBatch(this.gl, this.pathFillVColor);
3799
4183
  }
3800
4184
  uploadQuadGeometry() {
3801
4185
  const gl = this.gl;
@@ -3872,6 +4256,9 @@ var WeaselRenderer = class {
3872
4256
  this.gradFill = new ShaderProgram(this.gl, GRAD_VERT_SRC, GRAD_FRAG_SRC);
3873
4257
  this.gradFill.lookupUniforms(GRAD_FILL_UNIFORMS);
3874
4258
  this.gradFill.lookupAttributes(GRAD_FILL_ATTRIBUTES);
4259
+ this.patternFill = new ShaderProgram(this.gl, PATTERN_VERT_SRC, PATTERN_FRAG_SRC);
4260
+ this.patternFill.lookupUniforms(PATTERN_FILL_UNIFORMS);
4261
+ this.patternFill.lookupAttributes(PATTERN_FILL_ATTRIBUTES);
3875
4262
  const aPos = this.pathFill.attribute("a_position");
3876
4263
  if (aPos === void 0) throw new Error("a_position missing after restore");
3877
4264
  this.meshCache = new GLMeshCache(this.gl, aPos);
@@ -3880,7 +4267,7 @@ var WeaselRenderer = class {
3880
4267
  this.gradRampCache = new GradientRampCache(this.gl);
3881
4268
  markAllFontsNotUploaded();
3882
4269
  this.uploadQuadGeometry();
3883
- this.uploadRectGeometry(aPos);
4270
+ this.solidBatch = new SolidBatch(this.gl, this.pathFillVColor);
3884
4271
  for (const id of this.programRegistry.keys()) {
3885
4272
  const src = getProgramSource(id);
3886
4273
  if (!src) continue;
@@ -3940,11 +4327,17 @@ var WeaselRenderer = class {
3940
4327
  this.gradRampCache.free();
3941
4328
  if (this.quadVbo) gl.deleteBuffer(this.quadVbo);
3942
4329
  if (this.quadIbo) gl.deleteBuffer(this.quadIbo);
3943
- if (this.rectVbo) gl.deleteBuffer(this.rectVbo);
3944
- if (this.rectIbo) gl.deleteBuffer(this.rectIbo);
3945
- if (this.rectVao) gl.deleteVertexArray(this.rectVao);
4330
+ this.solidBatch.dispose();
3946
4331
  }
3947
- render(commands) {
4332
+ /**
4333
+ * Draw one frame.
4334
+ *
4335
+ * `viewMatrix` is the frame's world→screen transform. It is only consulted
4336
+ * by `units: 'world'` gradients, which fall back to screen space without
4337
+ * it — every other command carries its own transform in the stream, so
4338
+ * callers with no view concept can keep calling `render(commands)`.
4339
+ */
4340
+ render(commands, viewMatrix) {
3948
4341
  if (this.contextLost || this.disposed) return;
3949
4342
  const gl = this.gl;
3950
4343
  this.meshCache.drainPendingDeletes();
@@ -3959,6 +4352,7 @@ var WeaselRenderer = class {
3959
4352
  textSdfR8: this.textSdfR8,
3960
4353
  imageFill: this.imageFill,
3961
4354
  gradFill: this.gradFill,
4355
+ patternFill: this.patternFill,
3962
4356
  meshCache: this.meshCache,
3963
4357
  textureCache: this.textureCache,
3964
4358
  imageCache: this.imageCache,
@@ -3966,16 +4360,17 @@ var WeaselRenderer = class {
3966
4360
  programRegistry: this.programRegistry,
3967
4361
  quadVbo: this.quadVbo,
3968
4362
  quadIbo: this.quadIbo,
3969
- rectVao: this.rectVao,
3970
- rectVbo: this.rectVbo,
4363
+ solidBatch: this.solidBatch,
3971
4364
  state: this.groupState,
3972
4365
  widthCss: this.widthCss,
3973
4366
  heightCss: this.heightCss,
3974
4367
  clipDepth: 0,
3975
4368
  flattenTolerance: this.flattenTolerance,
3976
- textOutlineMinScreenSize: this.textOutlineMinScreenSize
4369
+ textOutlineMinScreenSize: this.textOutlineMinScreenSize,
4370
+ viewMatrix
3977
4371
  };
3978
4372
  for (const cmd of commands) dispatch(ctx, cmd);
4373
+ flushSolids(ctx);
3979
4374
  this.meshCache.freeTransient();
3980
4375
  }
3981
4376
  resize(dims) {
@@ -4005,6 +4400,10 @@ var WeaselRenderer = class {
4005
4400
  return this.pathFillVColor;
4006
4401
  }
4007
4402
  /** @internal */
4403
+ _solidBatch() {
4404
+ return this.solidBatch;
4405
+ }
4406
+ /** @internal */
4008
4407
  _textSdf() {
4009
4408
  return this.textSdf;
4010
4409
  }
@@ -4021,6 +4420,10 @@ var WeaselRenderer = class {
4021
4420
  return this.gradFill;
4022
4421
  }
4023
4422
  /** @internal */
4423
+ _patternFill() {
4424
+ return this.patternFill;
4425
+ }
4426
+ /** @internal */
4024
4427
  _meshCache() {
4025
4428
  return this.meshCache;
4026
4429
  }
@@ -4086,6 +4489,11 @@ function alignedStrokeRect(rect, align, width = 1) {
4086
4489
  };
4087
4490
  }
4088
4491
 
4492
+ // src/core/viewport/meanScale.ts
4493
+ function meanScale(s) {
4494
+ return Math.sqrt(s.x * s.y);
4495
+ }
4496
+
4089
4497
  // src/features/grid/layer.ts
4090
4498
  var DEFAULT_LINE = {
4091
4499
  paint: { fill: "solid", color: "rgba(255, 255, 255, 0.08)" },
@@ -5224,254 +5632,6 @@ function matchesKeyBinding(e, b) {
5224
5632
  }
5225
5633
  return true;
5226
5634
  }
5227
- var DepRegistryContext = createContext(null);
5228
- function DepRegistryProvider({ children }) {
5229
- const sourcesRef = useRef(/* @__PURE__ */ new Map());
5230
- const registry2 = useMemo(() => ({
5231
- register: (name, source) => {
5232
- sourcesRef.current.set(name, source);
5233
- return () => {
5234
- sourcesRef.current.delete(name);
5235
- };
5236
- },
5237
- get: (name) => sourcesRef.current.get(name)?.()
5238
- }), []);
5239
- return /* @__PURE__ */ jsx(DepRegistryContext.Provider, { value: registry2, children });
5240
- }
5241
- function useDepRegistry() {
5242
- const r = useContext(DepRegistryContext);
5243
- if (r === null) {
5244
- throw new Error("useDepRegistry: no DepRegistryProvider in scope. Wrap your tree with <DepRegistryProvider> (typically inside <SceneCanvas>).");
5245
- }
5246
- return r;
5247
- }
5248
- function useOptionalDepRegistry() {
5249
- return useContext(DepRegistryContext);
5250
- }
5251
- function useDepSource(name, source) {
5252
- const registry2 = useDepRegistry();
5253
- const sourceRef = useRef(source);
5254
- sourceRef.current = source;
5255
- useEffect(() => {
5256
- return registry2.register(name, () => sourceRef.current());
5257
- }, [name, registry2]);
5258
- }
5259
-
5260
- // src/interactions/actions/buildDeps.ts
5261
- var DEV = (() => {
5262
- try {
5263
- return Boolean(import.meta.env?.DEV);
5264
- } catch {
5265
- return false;
5266
- }
5267
- })();
5268
- function buildDepsFromRequires(action, depRegistry) {
5269
- const requires = action.requires ?? [];
5270
- const entries = requires.map((name) => [name, depRegistry.get(name)]);
5271
- const deps = Object.fromEntries(entries);
5272
- if (DEV) {
5273
- const declared = new Set(requires);
5274
- return new Proxy(deps, {
5275
- get(target, prop, receiver) {
5276
- if (typeof prop === "string" && !declared.has(prop) && prop !== "then") {
5277
- const message = `[weasel:dispatcher] action "${action.id}" read deps.${prop} but did not declare it in \`requires\`. Add \`requires: [...'${prop}']\` to the descriptor or the dep will be undefined at runtime.`;
5278
- console.warn(message);
5279
- throw new Error(message);
5280
- }
5281
- return Reflect.get(target, prop, receiver);
5282
- }
5283
- });
5284
- }
5285
- return deps;
5286
- }
5287
- var ActionDisabledReason = {
5288
- SelectionRequired: "selection-required",
5289
- SceneEmpty: "scene-empty",
5290
- NotApplicable: "not-applicable",
5291
- /** Sentinel: the predicate threw. Surfaced by `evaluateEnabled`'s catch. */
5292
- PredicateThrew: "predicate-threw"
5293
- };
5294
- var enabledSlowWarned = /* @__PURE__ */ new Set();
5295
- var enabledThrewWarned = /* @__PURE__ */ new Set();
5296
- var ENABLED_BUDGET_MS = 4;
5297
- function evaluateEnabled(action, deps) {
5298
- if (!action.enabled) return { enabled: true };
5299
- const isDev3 = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
5300
- const now = typeof performance !== "undefined" && typeof performance.now === "function" ? () => performance.now() : () => Date.now();
5301
- const start = isDev3 ? now() : 0;
5302
- try {
5303
- const result = action.enabled(deps);
5304
- if (isDev3) {
5305
- const elapsed = now() - start;
5306
- if (elapsed > ENABLED_BUDGET_MS && !enabledSlowWarned.has(action.id)) {
5307
- enabledSlowWarned.add(action.id);
5308
- console.warn(
5309
- `weasel actions: enabled() for action "${action.id}" took ${elapsed.toFixed(2)}ms (budget ${ENABLED_BUDGET_MS}ms). The predicate must be pure and fast \u2014 see Action.enabled JSDoc.`
5310
- );
5311
- }
5312
- }
5313
- if (result === true) return { enabled: true };
5314
- return { enabled: false, reason: result };
5315
- } catch (e) {
5316
- if (isDev3 && !enabledThrewWarned.has(action.id)) {
5317
- enabledThrewWarned.add(action.id);
5318
- console.warn(
5319
- `weasel actions: enabled() for action "${action.id}" threw; treating as disabled. The predicate must not throw \u2014 see Action.enabled JSDoc.`,
5320
- e
5321
- );
5322
- }
5323
- return { enabled: false, reason: ActionDisabledReason.PredicateThrew };
5324
- }
5325
- }
5326
- function validateActionId(id) {
5327
- if (id.length === 0) {
5328
- throw new Error(`weasel: action id may not be empty`);
5329
- }
5330
- if (RESERVED_ID_PREFIXES.has(id[0])) {
5331
- throw new Error(
5332
- `weasel: action id "${id}" starts with reserved sigil "${id[0]}" (reserved set: ${[...RESERVED_ID_PREFIXES].join(" ")})`
5333
- );
5334
- }
5335
- if (RESERVED_ID_NAMES.has(id)) {
5336
- throw new Error(
5337
- `weasel: action id "${id}" collides with a reserved phase keyword (reserved: ${[...RESERVED_ID_NAMES].join(", ")})`
5338
- );
5339
- }
5340
- }
5341
- function validateActionDefaultBinding(action) {
5342
- const gs = action.defaultBinding;
5343
- if (!gs) return;
5344
- const entries = Array.isArray(gs) ? gs : [gs];
5345
- for (const entry of entries) {
5346
- const spec = "kind" in entry ? entry : entry.spec;
5347
- const phase = spec.phase;
5348
- if (phase === void 0) continue;
5349
- const atoms = Array.isArray(phase) ? phase : [{ channel: "&", phase }];
5350
- for (const atom of atoms) {
5351
- if (atom.channel === "&") {
5352
- throw new Error(
5353
- `weasel: action "${action.id}" defaultBinding uses '&' channel which has no owning tool to resolve to. Use a named tool channel (e.g. '[rect:engaged]') or '*' instead, or move this binding to the tool's own Tool.bindings.`
5354
- );
5355
- }
5356
- }
5357
- }
5358
- }
5359
- var ActionsContext = createContext(null);
5360
- function ActionsProvider({ children }) {
5361
- const actionsRef = useRef(/* @__PURE__ */ new Map());
5362
- const versionRef = useRef(0);
5363
- const cachedRef = useRef([]);
5364
- const cachedVerRef = useRef(-1);
5365
- const listenersRef = useRef(/* @__PURE__ */ new Set());
5366
- const depReg = useOptionalDepRegistry();
5367
- const depRegRef = useRef(depReg);
5368
- depRegRef.current = depReg;
5369
- const wiredDepRegRef = useRef(null);
5370
- const dispatcherRef = useRef(null);
5371
- const registry2 = useMemo(() => {
5372
- const snapshot = () => {
5373
- const v = versionRef.current;
5374
- if (cachedVerRef.current === v) return cachedRef.current;
5375
- const out = Object.freeze(Array.from(actionsRef.current.values()));
5376
- cachedRef.current = out;
5377
- cachedVerRef.current = v;
5378
- return out;
5379
- };
5380
- const notify2 = () => {
5381
- for (const l of listenersRef.current) {
5382
- try {
5383
- l();
5384
- } catch (err) {
5385
- console.error("weasel ActionsRegistry: subscriber threw", err);
5386
- }
5387
- }
5388
- };
5389
- return {
5390
- register: (action) => {
5391
- validateActionId(action.id);
5392
- validateActionDefaultBinding(action);
5393
- actionsRef.current.set(action.id, action);
5394
- versionRef.current++;
5395
- notify2();
5396
- return () => {
5397
- const cur = actionsRef.current.get(action.id);
5398
- if (cur === action) {
5399
- actionsRef.current.delete(action.id);
5400
- versionRef.current++;
5401
- notify2();
5402
- }
5403
- };
5404
- },
5405
- unregister: (id) => {
5406
- if (actionsRef.current.delete(id)) {
5407
- versionRef.current++;
5408
- notify2();
5409
- }
5410
- },
5411
- list: () => snapshot(),
5412
- trigger: (id, params) => {
5413
- const a = actionsRef.current.get(id);
5414
- if (!a) return false;
5415
- try {
5416
- if (a.invoker && a.invoker.timing === "immediate") {
5417
- const r = wiredDepRegRef.current ?? depRegRef.current;
5418
- const deps = !r ? {} : a.requires ? buildDepsFromRequires(a, r) : {
5419
- selection: r.get("selection"),
5420
- scene: r.get("scene"),
5421
- history: r.get("history"),
5422
- view: r.get("view"),
5423
- pointer: r.get("pointer"),
5424
- activeTool: r.get("activeTool"),
5425
- booleansAdapter: r.get("booleansAdapter")
5426
- };
5427
- a.invoker.run(deps, params);
5428
- }
5429
- } catch (err) {
5430
- console.error(`weasel ActionsRegistry: action "${id}" threw`, err);
5431
- }
5432
- return true;
5433
- },
5434
- subscribe: (listener) => {
5435
- listenersRef.current.add(listener);
5436
- return () => {
5437
- listenersRef.current.delete(listener);
5438
- };
5439
- },
5440
- setDispatcher: (d) => {
5441
- dispatcherRef.current = d;
5442
- },
5443
- setDepRegistry: (r) => {
5444
- wiredDepRegRef.current = r;
5445
- },
5446
- begin: (id, params) => {
5447
- const disp = dispatcherRef.current;
5448
- if (!disp) return null;
5449
- const r = wiredDepRegRef.current ?? depRegRef.current;
5450
- const deps = r ? {
5451
- selection: r.get("selection"),
5452
- scene: r.get("scene"),
5453
- history: r.get("history"),
5454
- view: r.get("view"),
5455
- pointer: r.get("pointer"),
5456
- activeTool: r.get("activeTool"),
5457
- booleansAdapter: r.get("booleansAdapter")
5458
- } : {};
5459
- return disp.beginUiOngoing(id, deps, params);
5460
- }
5461
- };
5462
- }, []);
5463
- return /* @__PURE__ */ jsx(ActionsContext.Provider, { value: registry2, children });
5464
- }
5465
- function useActionsRegistry() {
5466
- return useContext(ActionsContext);
5467
- }
5468
- function useAction(action) {
5469
- const reg = useActionsRegistry();
5470
- useEffect(() => {
5471
- if (!reg) return;
5472
- return reg.register(action);
5473
- }, [reg, action]);
5474
- }
5475
5635
 
5476
5636
  // src/interactions/actions/invoker.ts
5477
5637
  function resolveParams(params) {
@@ -5513,12 +5673,7 @@ function moveGestureAdapter(scene) {
5513
5673
  return {
5514
5674
  getNode: (id) => scene.get(asNodeId(id)),
5515
5675
  getNodes: () => {
5516
- const out = [];
5517
- for (const id of scene.renderOrder()) {
5518
- const n = scene.get(id);
5519
- if (n) out.push(n);
5520
- }
5521
- return out;
5676
+ return [...scene.renderOrderNodes()];
5522
5677
  },
5523
5678
  getPose: (id) => scene.get(asNodeId(id)).pose,
5524
5679
  getParent: (id) => scene.get(asNodeId(id))?.parent ?? null,
@@ -6044,6 +6199,62 @@ var moveAction = {
6044
6199
  enabled: () => true
6045
6200
  };
6046
6201
 
6202
+ // src/features/paths/poseRotation.ts
6203
+ function poseRotationOf(pose) {
6204
+ const p = pose;
6205
+ if (p.rotation && p.x != null && p.y != null && p.width != null && p.height != null) {
6206
+ return { cx: p.x + p.width / 2, cy: p.y + p.height / 2, rotation: p.rotation };
6207
+ }
6208
+ return null;
6209
+ }
6210
+ function rotatePathAround(path, cx, cy, rotation) {
6211
+ if (path.kind === "rect") {
6212
+ return rotateRectCorners(path.x, path.y, path.width, path.height, cx, cy, rotation);
6213
+ }
6214
+ return rotatePolygon(path, cx, cy, rotation);
6215
+ }
6216
+ function rotatePolygon(path, cx, cy, rotation) {
6217
+ const cos = Math.cos(rotation);
6218
+ const sin = Math.sin(rotation);
6219
+ const { commands, coords } = path;
6220
+ const next = new Float32Array(coords.length);
6221
+ let ci = 0;
6222
+ for (let i = 0; i < commands.length; i++) {
6223
+ const len = COORD_COUNT[commands[i]];
6224
+ for (let k = 0; k < len; k += 2) {
6225
+ const dx = coords[ci + k] - cx;
6226
+ const dy = coords[ci + k + 1] - cy;
6227
+ next[ci + k] = cx + dx * cos - dy * sin;
6228
+ next[ci + k + 1] = cy + dx * sin + dy * cos;
6229
+ }
6230
+ ci += len;
6231
+ }
6232
+ return { kind: "polygon", commands: path.commands, coords: next, fillRule: path.fillRule };
6233
+ }
6234
+ function rotateRectCorners(x, y, w, h, cx, cy, rotation) {
6235
+ const cos = Math.cos(rotation);
6236
+ const sin = Math.sin(rotation);
6237
+ const rotate = (px, py) => {
6238
+ const dx = px - cx;
6239
+ const dy = py - cy;
6240
+ return [cx + dx * cos - dy * sin, cy + dx * sin + dy * cos];
6241
+ };
6242
+ const [x0, y0] = rotate(x, y);
6243
+ const [x1, y1] = rotate(x + w, y);
6244
+ const [x2, y2] = rotate(x + w, y + h);
6245
+ const [x3, y3] = rotate(x, y + h);
6246
+ const commands = new Uint8Array([PATH_M, PATH_L, PATH_L, PATH_L, PATH_Z]);
6247
+ const coords = new Float32Array([x0, y0, x1, y1, x2, y2, x3, y3]);
6248
+ return { kind: "polygon", commands, coords, fillRule: "nonzero" };
6249
+ }
6250
+ var COORD_COUNT = {
6251
+ [PATH_M]: 2,
6252
+ [PATH_L]: 2,
6253
+ [PATH_C]: 6,
6254
+ [PATH_Q]: 4,
6255
+ [PATH_Z]: 0
6256
+ };
6257
+
6047
6258
  // src/interactions/actions/rotate/geometry.ts
6048
6259
  function aabbCenter(bounds) {
6049
6260
  return { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
@@ -6078,12 +6289,7 @@ function defaultCommitAdapter(scene) {
6078
6289
  return {
6079
6290
  getNode: (id) => scene.get(asNodeId(id)),
6080
6291
  getNodes: () => {
6081
- const out = [];
6082
- for (const id of scene.renderOrder()) {
6083
- const n = scene.get(id);
6084
- if (n) out.push(n);
6085
- }
6086
- return out;
6292
+ return [...scene.renderOrderNodes()];
6087
6293
  },
6088
6294
  getPose: (id) => scene.get(asNodeId(id)).pose,
6089
6295
  getParent: (id) => scene.get(asNodeId(id))?.parent ?? null,
@@ -6360,6 +6566,13 @@ var resizeAction = {
6360
6566
  const computePose = (id) => {
6361
6567
  const sp = scratch.startPoses.get(id);
6362
6568
  if (sp === void 0) return void 0;
6569
+ if (isGroupPath && poseRotationOf(sp) !== null) {
6570
+ return remapRotatedLeaf(
6571
+ sp,
6572
+ scratch.originBounds,
6573
+ proposedBounds
6574
+ );
6575
+ }
6363
6576
  return scratch.geometry.remapBounds(sp, scratch.originBounds, proposedBounds);
6364
6577
  };
6365
6578
  scratch.previews.clear();
@@ -7217,7 +7430,13 @@ function makeViewportZoomAction(opts = {}) {
7217
7430
  var viewportZoomAction = makeViewportZoomAction();
7218
7431
 
7219
7432
  // src/interactions/dispatcher/predicates.ts
7220
- var isBody = (_target, bodyTarget) => bodyTarget === "selected-body" || bodyTarget === "unselected-body";
7433
+ function bodyPredicate(fn) {
7434
+ return Object.assign(fn, { readsAffordance: false });
7435
+ }
7436
+ var isBody = bodyPredicate((_target, bodyTarget) => bodyTarget === "selected-body" || bodyTarget === "unselected-body");
7437
+ bodyPredicate((_target, bodyTarget) => bodyTarget === "selected-body");
7438
+ bodyPredicate((_target, bodyTarget) => bodyTarget === "unselected-body");
7439
+ bodyPredicate((_target, bodyTarget) => bodyTarget === "empty");
7221
7440
  function kindOf(target) {
7222
7441
  if (typeof target !== "object" || target === null) return void 0;
7223
7442
  const k = target.kind;
@@ -7258,17 +7477,17 @@ function splitCubicAtT(p0, p1, p2, p3, t) {
7258
7477
  function fitCubicThroughDeletion(prev, next) {
7259
7478
  const dx = next.x - prev.x;
7260
7479
  const dy = next.y - prev.y;
7261
- const dist4 = Math.sqrt(dx * dx + dy * dy);
7480
+ const dist3 = Math.sqrt(dx * dx + dy * dy);
7262
7481
  const c1DirX = prev.outHandle ? prev.outHandle.x - prev.x : dx;
7263
7482
  const c1DirY = prev.outHandle ? prev.outHandle.y - prev.y : dy;
7264
- const c1 = controlOnSide(prev, c1DirX, c1DirY, dist4, 1 / 3);
7483
+ const c1 = controlOnSide(prev, c1DirX, c1DirY, dist3, 1 / 3);
7265
7484
  const c2DirX = next.inHandle ? next.inHandle.x - next.x : -dx;
7266
7485
  const c2DirY = next.inHandle ? next.inHandle.y - next.y : -dy;
7267
- const c2 = controlOnSide(next, c2DirX, c2DirY, dist4, 1 / 3);
7486
+ const c2 = controlOnSide(next, c2DirX, c2DirY, dist3, 1 / 3);
7268
7487
  return { c1, c2 };
7269
7488
  }
7270
- function controlOnSide(anchor, dirX, dirY, dist4, fraction) {
7271
- const targetDist = dist4 * fraction;
7489
+ function controlOnSide(anchor, dirX, dirY, dist3, fraction) {
7490
+ const targetDist = dist3 * fraction;
7272
7491
  const len = Math.hypot(dirX, dirY) || 1;
7273
7492
  const ux = dirX / len, uy = dirY / len;
7274
7493
  return { x: anchor.x + ux * targetDist, y: anchor.y + uy * targetDist };
@@ -7539,64 +7758,6 @@ function anchorsInRect(set, rect) {
7539
7758
  }
7540
7759
  return hits;
7541
7760
  }
7542
-
7543
- // src/features/paths/poseRotation.ts
7544
- function poseRotationOf(pose) {
7545
- const p = pose;
7546
- if (p.rotation && p.x != null && p.y != null && p.width != null && p.height != null) {
7547
- return { cx: p.x + p.width / 2, cy: p.y + p.height / 2, rotation: p.rotation };
7548
- }
7549
- return null;
7550
- }
7551
- function rotatePathAround(path, cx, cy, rotation) {
7552
- if (path.kind === "rect") {
7553
- return rotateRectCorners(path.x, path.y, path.width, path.height, cx, cy, rotation);
7554
- }
7555
- return rotatePolygon(path, cx, cy, rotation);
7556
- }
7557
- function rotatePolygon(path, cx, cy, rotation) {
7558
- const cos = Math.cos(rotation);
7559
- const sin = Math.sin(rotation);
7560
- const { commands, coords } = path;
7561
- const next = new Float32Array(coords.length);
7562
- let ci = 0;
7563
- for (let i = 0; i < commands.length; i++) {
7564
- const len = COORD_COUNT[commands[i]];
7565
- for (let k = 0; k < len; k += 2) {
7566
- const dx = coords[ci + k] - cx;
7567
- const dy = coords[ci + k + 1] - cy;
7568
- next[ci + k] = cx + dx * cos - dy * sin;
7569
- next[ci + k + 1] = cy + dx * sin + dy * cos;
7570
- }
7571
- ci += len;
7572
- }
7573
- return { kind: "polygon", commands: path.commands, coords: next, fillRule: path.fillRule };
7574
- }
7575
- function rotateRectCorners(x, y, w, h, cx, cy, rotation) {
7576
- const cos = Math.cos(rotation);
7577
- const sin = Math.sin(rotation);
7578
- const rotate = (px, py) => {
7579
- const dx = px - cx;
7580
- const dy = py - cy;
7581
- return [cx + dx * cos - dy * sin, cy + dx * sin + dy * cos];
7582
- };
7583
- const [x0, y0] = rotate(x, y);
7584
- const [x1, y1] = rotate(x + w, y);
7585
- const [x2, y2] = rotate(x + w, y + h);
7586
- const [x3, y3] = rotate(x, y + h);
7587
- const commands = new Uint8Array([PATH_M, PATH_L, PATH_L, PATH_L, PATH_Z]);
7588
- const coords = new Float32Array([x0, y0, x1, y1, x2, y2, x3, y3]);
7589
- return { kind: "polygon", commands, coords, fillRule: "nonzero" };
7590
- }
7591
- var COORD_COUNT = {
7592
- [PATH_M]: 2,
7593
- [PATH_L]: 2,
7594
- [PATH_C]: 6,
7595
- [PATH_Q]: 4,
7596
- [PATH_Z]: 0
7597
- };
7598
-
7599
- // src/features/paths/pathInWorld.ts
7600
7761
  function pathInPoseFrame(path, pose) {
7601
7762
  if (path.kind === "rect") {
7602
7763
  if (path.x === pose.x && path.y === pose.y && path.width === pose.width && path.height === pose.height) return path;
@@ -9281,7 +9442,7 @@ var LABEL = {
9281
9442
  divide: "Divide",
9282
9443
  crop: "Crop"
9283
9444
  };
9284
- function isEmpty(p) {
9445
+ function isEmpty2(p) {
9285
9446
  return p.commands.length === 0;
9286
9447
  }
9287
9448
  function applyBooleanOp(adapter, op) {
@@ -9319,7 +9480,7 @@ function applyBooleanOp(adapter, op) {
9319
9480
  break;
9320
9481
  }
9321
9482
  }
9322
- results = results.filter((p) => !isEmpty(p));
9483
+ results = results.filter((p) => !isEmpty2(p));
9323
9484
  if (results.length === 0) return { kind: "noop", reason: "empty-result" };
9324
9485
  const topmostId = entries[entries.length - 1].id;
9325
9486
  const topAnchor = adapter.getZOrder?.(topmostId);
@@ -9870,14 +10031,24 @@ var DEFAULT_PALETTE = [
9870
10031
  var GHOST_STROKE = DEFAULT_PALETTE[0];
9871
10032
 
9872
10033
  // src/interactions/actions/defaults/setFill.ts
10034
+ function resolveFill(scratch, prev) {
10035
+ if (scratch.currentPaint) return scratch.currentPaint;
10036
+ const prevColor = typeof prev?.fill === "string" ? prev.fill : DEFAULT_FILL_COLOR;
10037
+ return mergeAlphaFromPrev(scratch.currentColor, prevColor);
10038
+ }
9873
10039
  function refreshPreviews(scratch) {
9874
10040
  scratch.previews.clear();
9875
10041
  for (const id of scratch.ids) {
9876
10042
  const prev = scratch.startData.get(id);
9877
- const next = mergeAlphaFromPrev(scratch.currentColor, prev?.fill ?? DEFAULT_FILL_COLOR);
9878
- scratch.previews.set(id, { ...prev ?? {}, fill: next });
10043
+ scratch.previews.set(id, { ...prev ?? {}, fill: resolveFill(scratch, prev) });
9879
10044
  }
9880
10045
  }
10046
+ function readParams(params) {
10047
+ return {
10048
+ color: params?.color,
10049
+ paint: params?.paint
10050
+ };
10051
+ }
9881
10052
  var setFillAction = {
9882
10053
  id: "setFill",
9883
10054
  label: "Set fill",
@@ -9892,9 +10063,10 @@ var setFillAction = {
9892
10063
  if (!selection || !scene) return {};
9893
10064
  const ids = selection.get();
9894
10065
  if (ids.length === 0) return {};
9895
- const ctxColor = ctx.params?.color;
9896
- const optsColor = opts?.params?.color;
9897
- const initialColor = ctxColor ?? optsColor ?? DEFAULT_STROKE_COLOR;
10066
+ const fromCtx = readParams(ctx.params);
10067
+ const fromOpts = readParams(opts?.params);
10068
+ const initialPaint = fromCtx.paint ?? fromOpts.paint;
10069
+ const initialColor = fromCtx.color ?? fromOpts.color ?? DEFAULT_STROKE_COLOR;
9898
10070
  const startData = /* @__PURE__ */ new Map();
9899
10071
  for (const id of ids) {
9900
10072
  const node = scene.get(id);
@@ -9905,15 +10077,20 @@ var setFillAction = {
9905
10077
  scene,
9906
10078
  startData,
9907
10079
  currentColor: initialColor,
10080
+ currentPaint: initialPaint,
9908
10081
  previews: /* @__PURE__ */ new Map(),
9909
10082
  applyOps
9910
10083
  };
9911
10084
  refreshPreviews(scratch);
9912
10085
  return {
9913
10086
  onMove(moveCtx) {
9914
- const next = moveCtx.params?.color;
9915
- if (next === void 0) return;
9916
- scratch.currentColor = next;
10087
+ const { color, paint } = readParams(moveCtx.params);
10088
+ if (color === void 0 && paint === void 0) return;
10089
+ if (paint !== void 0) scratch.currentPaint = paint;
10090
+ if (color !== void 0) {
10091
+ scratch.currentColor = color;
10092
+ if (paint === void 0) scratch.currentPaint = void 0;
10093
+ }
9917
10094
  refreshPreviews(scratch);
9918
10095
  },
9919
10096
  onEnd(_endCtx, reason) {
@@ -9923,8 +10100,7 @@ var setFillAction = {
9923
10100
  }
9924
10101
  const ops = [];
9925
10102
  for (const id of scratch.ids) {
9926
- const prev = scratch.startData.get(id);
9927
- const merged = mergeAlphaFromPrev(scratch.currentColor, prev?.fill ?? DEFAULT_FILL_COLOR);
10103
+ const merged = resolveFill(scratch, scratch.startData.get(id));
9928
10104
  const nodeNow = scratch.scene.get(id);
9929
10105
  if (!nodeNow) continue;
9930
10106
  const from = { ...nodeNow.data };
@@ -10813,13 +10989,82 @@ function modsCount(mods) {
10813
10989
  }
10814
10990
  function targetRank(target) {
10815
10991
  if (target === void 0) return 0;
10816
- if (typeof target !== "string") return 1;
10817
- if (target.startsWith("kind:")) return target.endsWith(":selected") ? 3 : 2;
10818
- if (target.startsWith("affordance:")) return 2;
10819
- return 1;
10992
+ const form = parseTargetSpec(target);
10993
+ if (form === null) return 1;
10994
+ switch (form.form) {
10995
+ case "kind":
10996
+ return form.requireSelected ? 3 : 2;
10997
+ case "affordance":
10998
+ return 2;
10999
+ case "body":
11000
+ return 1;
11001
+ case "predicate":
11002
+ return 1;
11003
+ default: {
11004
+ return 1;
11005
+ }
11006
+ }
11007
+ }
11008
+ function targetConsultsAffordance(specTarget) {
11009
+ if (specTarget === void 0) return false;
11010
+ const form = parseTargetSpec(specTarget);
11011
+ if (form === null) return false;
11012
+ switch (form.form) {
11013
+ case "predicate":
11014
+ return form.kindOf.readsAffordance !== false;
11015
+ case "affordance":
11016
+ return true;
11017
+ case "body":
11018
+ return false;
11019
+ case "kind":
11020
+ return false;
11021
+ default: {
11022
+ return false;
11023
+ }
11024
+ }
11025
+ }
11026
+ function specTargetOf(spec) {
11027
+ return "target" in spec ? spec.target : void 0;
11028
+ }
11029
+ function claimOf(e) {
11030
+ return "affordance" in e ? e.affordance : void 0;
11031
+ }
11032
+ function claimGestureOf(e) {
11033
+ switch (e.kind) {
11034
+ case "pointerdown":
11035
+ case "click":
11036
+ return "pointer";
11037
+ case "doubleclick":
11038
+ return "doubleClick";
11039
+ case "contextmenu":
11040
+ return "contextMenu";
11041
+ case "longpress":
11042
+ return "longPress";
11043
+ case "wheel":
11044
+ return "wheel";
11045
+ default:
11046
+ return null;
11047
+ }
11048
+ }
11049
+ function isExclusiveClaim(e) {
11050
+ const claim = claimOf(e);
11051
+ if (claim?.strength !== "exclusive") return false;
11052
+ const gesture = claimGestureOf(e);
11053
+ if (gesture === null) return false;
11054
+ return claim.claimedKinds === void 0 || claim.claimedKinds.includes(gesture);
11055
+ }
11056
+ var warnedDeadClaims = /* @__PURE__ */ new Set();
11057
+ function reportDeadClaim(owner, warn) {
11058
+ if (process.env.NODE_ENV === "production") return;
11059
+ const key = String(owner);
11060
+ if (warnedDeadClaims.has(key)) return;
11061
+ warnedDeadClaims.add(key);
11062
+ warn(
11063
+ `[weasel] exclusive claim by "${key}" matched no binding: no \`affordance:\` or \`kindOf\` target resolved against it, so the press was dropped.`
11064
+ );
10820
11065
  }
10821
11066
  function specificity(spec) {
10822
- const t = targetRank("target" in spec ? spec.target : void 0);
11067
+ const t = targetRank(specTargetOf(spec));
10823
11068
  const mods = "mods" in spec ? spec.mods : void 0;
10824
11069
  const m = modsCount(mods);
10825
11070
  const p = "phase" in spec && spec.phase !== void 0 ? 1 : 0;
@@ -10834,12 +11079,14 @@ function compareSpecificity(a, b) {
10834
11079
  }
10835
11080
  return 0;
10836
11081
  }
10837
- function matchSorted(e, bindings, isMac, engagedChannels) {
11082
+ function matchSorted(e, bindings, isMac, engagedChannels, warn = (m) => console.warn(m)) {
10838
11083
  const out = [];
10839
11084
  const engaged = engagedChannels ?? EMPTY_ENGAGED;
11085
+ const exclusive = isExclusiveClaim(e);
11086
+ const pool = exclusive ? bindings.filter((sb) => targetConsultsAffordance(specTargetOf(sb.binding.spec))) : bindings;
10840
11087
  for (const scope of SCOPE_PRIORITY) {
10841
11088
  const scopeMatches = [];
10842
- for (const sb of bindings) {
11089
+ for (const sb of pool) {
10843
11090
  if (sb.scope !== scope) continue;
10844
11091
  const phaseCtx = { selfChannel: sb.ownerToolId, engagedChannels: engaged };
10845
11092
  if (matchSpec(e, sb.binding.spec, isMac, phaseCtx)) {
@@ -10849,10 +11096,37 @@ function matchSorted(e, bindings, isMac, engagedChannels) {
10849
11096
  scopeMatches.sort((a, b) => compareSpecificity(a.binding.spec, b.binding.spec));
10850
11097
  out.push(...scopeMatches);
10851
11098
  }
11099
+ if (exclusive && out.length === 0 && bindings.length > 0) {
11100
+ reportDeadClaim(claimOf(e)?.owner, warn);
11101
+ }
10852
11102
  return out;
10853
11103
  }
10854
11104
  var EMPTY_ENGAGED = /* @__PURE__ */ new Set();
10855
11105
 
11106
+ // src/contributions/eligibility.ts
11107
+ function liveScope(id, eligibility, state) {
11108
+ const tags = eligibility.capabilities;
11109
+ if (tags && tags.length > 0 && state.allows && !state.allows(tags)) return null;
11110
+ if (state.engagedIds?.has(id)) return "hotkey";
11111
+ if (eligibility.offhand && state.heldTriggers.has(eligibility.offhand)) return "hotkey";
11112
+ if (eligibility.focus && state.focusedId === id) return "active";
11113
+ if (eligibility.always || eligibility.claimed) return "ambient";
11114
+ return null;
11115
+ }
11116
+
11117
+ // src/contributions/assemble.ts
11118
+ function scopeBindings(entries, state) {
11119
+ const out = [];
11120
+ for (const entry of entries) {
11121
+ const scope = liveScope(entry.id, entry.eligibility ?? {}, state);
11122
+ if (scope === null) continue;
11123
+ for (const binding of entry.bindings ?? []) {
11124
+ out.push({ binding, scope, ownerToolId: entry.id });
11125
+ }
11126
+ }
11127
+ return out;
11128
+ }
11129
+
10856
11130
  // src/features/chrome-caps/rule.ts
10857
11131
  var ALWAYS = { all: [] };
10858
11132
  var NEVER = { any: [] };
@@ -11157,7 +11431,7 @@ function useHoverTracking(args) {
11157
11431
  function pointerGestureId(pointerId) {
11158
11432
  return `pointer-${pointerId ?? "mouse"}`;
11159
11433
  }
11160
- var DEV2 = (() => {
11434
+ var DEV = (() => {
11161
11435
  try {
11162
11436
  return Boolean(import.meta.env?.DEV);
11163
11437
  } catch {
@@ -11165,9 +11439,9 @@ var DEV2 = (() => {
11165
11439
  }
11166
11440
  })();
11167
11441
  var TRACE_LIMIT = 200;
11168
- var traceLog = DEV2 && typeof window !== "undefined" ? window.__weaselDispatchLog__ ??= [] : [];
11442
+ var traceLog = DEV && typeof window !== "undefined" ? window.__weaselDispatchLog__ ??= [] : [];
11169
11443
  function recordTrace(entry) {
11170
- if (!DEV2) return;
11444
+ if (!DEV) return;
11171
11445
  traceLog.push(entry);
11172
11446
  if (traceLog.length > TRACE_LIMIT) traceLog.shift();
11173
11447
  }
@@ -11198,6 +11472,7 @@ function filterEligible(matches, actionLookup, ruleCtx) {
11198
11472
  return isEligible(action, ruleCtx);
11199
11473
  });
11200
11474
  }
11475
+ var EMPTY_TRIGGERS = /* @__PURE__ */ new Set();
11201
11476
  var EMPTY_ENGAGED2 = /* @__PURE__ */ new Set();
11202
11477
  function modifiersOf(e) {
11203
11478
  return { alt: e.altKey, ctrl: e.ctrlKey, meta: e.metaKey, shift: e.shiftKey };
@@ -11305,34 +11580,23 @@ function createDispatcher(opts) {
11305
11580
  return `ongoing-${event.kind}`;
11306
11581
  }
11307
11582
  function assembleScopedBindings(ctx) {
11308
- const result = [];
11309
- for (const toolId of ctx.hotkeyStack) {
11310
- const tool = ctx.toolsById.get(toolId);
11311
- for (const binding of tool?.bindings ?? []) {
11312
- result.push({ binding, scope: "hotkey", ownerToolId: toolId });
11313
- }
11314
- }
11315
- const activeTool = ctx.toolsById.get(ctx.activeToolId);
11316
- for (const binding of activeTool?.bindings ?? []) {
11317
- result.push({ binding, scope: "active", ownerToolId: ctx.activeToolId });
11318
- }
11319
- for (const toolId of ctx.ambientToolIds ?? []) {
11320
- const tool = ctx.toolsById.get(toolId);
11321
- for (const binding of tool?.bindings ?? []) {
11322
- result.push({ binding, scope: "ambient", ownerToolId: toolId });
11323
- }
11324
- }
11583
+ const ordered = [];
11584
+ for (const id of ctx.hotkeyStack) {
11585
+ const tool = ctx.toolsById.get(id);
11586
+ if (tool && !ordered.includes(tool)) ordered.push(tool);
11587
+ }
11588
+ for (const tool of ctx.toolsById.values()) {
11589
+ if (!ordered.includes(tool)) ordered.push(tool);
11590
+ }
11591
+ const result = scopeBindings(ordered, {
11592
+ focusedId: ctx.activeToolId,
11593
+ heldTriggers: EMPTY_TRIGGERS,
11594
+ engagedIds: new Set(ctx.hotkeyStack)
11595
+ });
11325
11596
  for (const action of ctx.actions.list()) {
11326
- const gs = action.defaultBinding;
11327
- if (!gs) continue;
11328
- const raw = Array.isArray(gs) ? gs : [gs];
11329
- for (const entry of raw) {
11330
- const isBoundObj = !("kind" in entry);
11331
- const spec = isBoundObj ? entry.spec : entry;
11332
- const opts2 = isBoundObj ? entry.opts : void 0;
11333
- const defaultBinding = { spec, actionId: action.id, ...opts2 !== void 0 ? { opts: opts2 } : {} };
11334
- const targetScope = action.scope === "hotkey" ? "hotkey" : "ambient";
11335
- result.push({ binding: defaultBinding, scope: targetScope, ownerToolId: null });
11597
+ const targetScope = action.scope === "hotkey" ? "hotkey" : "ambient";
11598
+ for (const binding of actionBindings(action)) {
11599
+ result.push({ binding, scope: targetScope, ownerToolId: null });
11336
11600
  }
11337
11601
  }
11338
11602
  return result;
@@ -11487,15 +11751,26 @@ function createDispatcher(opts) {
11487
11751
  deltaY: event.deltaY,
11488
11752
  clientX: event.clientX,
11489
11753
  clientY: event.clientY,
11754
+ affordance: event.affordance,
11490
11755
  ...resolved
11491
11756
  };
11492
11757
  } else if (event.kind === "click" || event.kind === "doubleclick") {
11493
11758
  params = {
11494
11759
  worldX: event.worldX,
11495
11760
  worldY: event.worldY,
11761
+ affordance: event.affordance,
11496
11762
  // Press point as well as release point — an action that places
11497
11763
  // geometry at the click wants the former. See `ClickEvent`.
11498
- ...event.kind === "click" ? { pressX: event.pressX, pressY: event.pressY, affordance: event.affordance } : {},
11764
+ ...event.kind === "click" ? { pressX: event.pressX, pressY: event.pressY } : {},
11765
+ mods: modifiersOf(event),
11766
+ ...resolved
11767
+ };
11768
+ } else if (event.kind === "contextmenu" || event.kind === "longpress") {
11769
+ params = {
11770
+ worldX: event.kind === "contextmenu" ? event.worldX : event.x,
11771
+ worldY: event.kind === "contextmenu" ? event.worldY : event.y,
11772
+ affordance: event.affordance,
11773
+ bodyTarget: event.bodyTarget,
11499
11774
  mods: modifiersOf(event),
11500
11775
  ...resolved
11501
11776
  };
@@ -11867,7 +12142,7 @@ function computeMultiTouchGeometry(positions) {
11867
12142
  return { centroid, spread };
11868
12143
  }
11869
12144
  function useGestureDispatcher(opts) {
11870
- const { canvasRef, actions, toolsById, ambientToolIds, enabled = true, keyboard = true, affordanceAt, classifyTarget, dispatcher: dispatcherOpt, clientToWorld: clientToWorld2, requestRedraw, getRuleCtx, onDoubleClick } = opts;
12145
+ const { canvasRef, actions, toolsById, enabled = true, keyboard = true, affordanceAt, classifyTarget, dispatcher: dispatcherOpt, clientToWorld: clientToWorld2, requestRedraw, getRuleCtx, onDoubleClick } = opts;
11871
12146
  const onDoubleClickRef = useRef(onDoubleClick);
11872
12147
  onDoubleClickRef.current = onDoubleClick;
11873
12148
  const activeTool = useActiveToolContext();
@@ -11882,7 +12157,6 @@ function useGestureDispatcher(opts) {
11882
12157
  activeToolId: activeTool.active,
11883
12158
  hotkeyStack: activeTool.hotkeyStack,
11884
12159
  toolsById,
11885
- ambientToolIds,
11886
12160
  isMac: IS_MAC,
11887
12161
  getRuleCtx
11888
12162
  });
@@ -11892,7 +12166,6 @@ function useGestureDispatcher(opts) {
11892
12166
  activeToolId: activeTool.active,
11893
12167
  hotkeyStack: activeTool.hotkeyStack,
11894
12168
  toolsById,
11895
- ambientToolIds,
11896
12169
  isMac: IS_MAC,
11897
12170
  getRuleCtx
11898
12171
  };
@@ -11970,6 +12243,7 @@ function useGestureDispatcher(opts) {
11970
12243
  ctrlKey: down.ctrlKey,
11971
12244
  metaKey: down.metaKey,
11972
12245
  shiftKey: down.shiftKey,
12246
+ ...down.affordance !== void 0 ? { affordance: down.affordance } : {},
11973
12247
  ...down.bodyTarget !== void 0 ? { bodyTarget: down.bodyTarget } : {},
11974
12248
  ...down.bodyKind !== void 0 ? { bodyKind: down.bodyKind } : {}
11975
12249
  };
@@ -11979,11 +12253,15 @@ function useGestureDispatcher(opts) {
11979
12253
  y: down.worldY,
11980
12254
  clientX: down.clientX,
11981
12255
  clientY: down.clientY,
11982
- ...down.affordance !== void 0 ? { affordance: down.affordance } : {},
11983
12256
  ...shared
11984
12257
  });
11985
12258
  if (result === "unhandled") {
11986
- dispatch2({ kind: "contextmenu", ...shared });
12259
+ dispatch2({
12260
+ kind: "contextmenu",
12261
+ worldX: down.worldX,
12262
+ worldY: down.worldY,
12263
+ ...shared
12264
+ });
11987
12265
  }
11988
12266
  };
11989
12267
  const DOUBLE_CLICK_MAX_MS = 600;
@@ -12038,6 +12316,9 @@ function useGestureDispatcher(opts) {
12038
12316
  const onWheel = (e) => {
12039
12317
  const rect = canvas?.getBoundingClientRect();
12040
12318
  const [localX, localY] = rect ? clientToCanvasRect(rect, e.clientX, e.clientY) : [e.clientX, e.clientY];
12319
+ const screenPoint = { x: e.clientX, y: e.clientY };
12320
+ const affordance = affordanceAtRef.current?.(screenPoint) ?? void 0;
12321
+ const body = classifyTargetRef.current?.(screenPoint);
12041
12322
  const ev = {
12042
12323
  kind: "wheel",
12043
12324
  altKey: e.altKey,
@@ -12047,7 +12328,10 @@ function useGestureDispatcher(opts) {
12047
12328
  deltaX: e.deltaX,
12048
12329
  deltaY: e.deltaY,
12049
12330
  clientX: localX,
12050
- clientY: localY
12331
+ clientY: localY,
12332
+ ...affordance !== void 0 ? { affordance } : {},
12333
+ ...body?.body !== void 0 ? { bodyTarget: body.body } : {},
12334
+ ...body?.kind !== void 0 ? { bodyKind: body.kind } : {}
12051
12335
  };
12052
12336
  const result = dispatch2(ev);
12053
12337
  if (result === "handled") {
@@ -12341,6 +12625,7 @@ function useGestureDispatcher(opts) {
12341
12625
  shiftKey: e.shiftKey,
12342
12626
  worldX: wClick.x,
12343
12627
  worldY: wClick.y,
12628
+ ...down.affordance !== void 0 ? { affordance: down.affordance } : {},
12344
12629
  ...down.bodyTarget !== void 0 ? { bodyTarget: down.bodyTarget } : {},
12345
12630
  ...down.bodyKind !== void 0 ? { bodyKind: down.bodyKind } : {}
12346
12631
  };
@@ -12382,8 +12667,10 @@ function useGestureDispatcher(opts) {
12382
12667
  };
12383
12668
  const onContextMenu = (e) => {
12384
12669
  e.preventDefault();
12385
- const worldPoint = { x: e.clientX, y: e.clientY };
12386
- const menuBody = classifyTargetRef.current?.(worldPoint);
12670
+ const screenPoint = { x: e.clientX, y: e.clientY };
12671
+ const affordance = affordanceAtRef.current?.(screenPoint) ?? void 0;
12672
+ const menuBody = classifyTargetRef.current?.(screenPoint);
12673
+ const w = toWorld(e.clientX, e.clientY);
12387
12674
  const ev = {
12388
12675
  kind: "contextmenu",
12389
12676
  target: e.target,
@@ -12391,6 +12678,9 @@ function useGestureDispatcher(opts) {
12391
12678
  ctrlKey: e.ctrlKey,
12392
12679
  metaKey: e.metaKey,
12393
12680
  shiftKey: e.shiftKey,
12681
+ worldX: w.x,
12682
+ worldY: w.y,
12683
+ ...affordance !== void 0 ? { affordance } : {},
12394
12684
  ...menuBody?.body !== void 0 ? { bodyTarget: menuBody.body } : {},
12395
12685
  ...menuBody?.kind !== void 0 ? { bodyKind: menuBody.kind } : {}
12396
12686
  };
@@ -12786,51 +13076,213 @@ function useViewAnimation(setView) {
12786
13076
  if (target === currentView) return;
12787
13077
  animateTo(currentView, target, { duration: opts?.duration, easing: opts?.easing });
12788
13078
  },
12789
- [animateTo]
12790
- );
12791
- return { animateTo, animateToBounds, cancel };
13079
+ [animateTo]
13080
+ );
13081
+ return { animateTo, animateToBounds, cancel };
13082
+ }
13083
+
13084
+ // src/interactions/actions/defaults/toolOffhand.ts
13085
+ var TOOL_OFFHAND_ID = "tool.offhand";
13086
+ function offhandKeyFor(trigger) {
13087
+ switch (trigger) {
13088
+ case "space":
13089
+ return " ";
13090
+ case "alt":
13091
+ return "Alt";
13092
+ case "ctrl":
13093
+ return "Control";
13094
+ case "meta":
13095
+ return "Meta";
13096
+ case "shift":
13097
+ return "Shift";
13098
+ }
13099
+ }
13100
+ function buildToolOffhandBindings(specs) {
13101
+ return specs.map(({ toolId, key }) => ({
13102
+ spec: { kind: "key-held", key },
13103
+ opts: { params: { toolId } }
13104
+ }));
13105
+ }
13106
+ function makeToolOffhandAction(bindings) {
13107
+ return {
13108
+ id: TOOL_OFFHAND_ID,
13109
+ label: "Engage tool (offhand)",
13110
+ defaultBinding: bindings,
13111
+ scope: "hotkey",
13112
+ invoker: {
13113
+ timing: "ongoing",
13114
+ start: (ctx, opts) => {
13115
+ const activeTool = ctx.deps.activeTool;
13116
+ const toolId = resolveParams(opts?.params)?.toolId;
13117
+ if (!activeTool || !toolId) return {};
13118
+ activeTool.pushHotkey(toolId);
13119
+ return {
13120
+ onEnd: () => {
13121
+ activeTool.popHotkey();
13122
+ }
13123
+ };
13124
+ }
13125
+ },
13126
+ requires: ["activeTool"]
13127
+ };
12792
13128
  }
12793
- function useTools(opts) {
12794
- if (!(opts.active in opts.registry)) {
12795
- throw new Error(`useTools: active "${opts.active}" not in registry`);
12796
- }
13129
+
13130
+ // src/contributions/useContributions.ts
13131
+ var NO_TRIGGERS = /* @__PURE__ */ new Set();
13132
+ function useContributions(opts) {
12797
13133
  const ctx = useActiveToolContext();
13134
+ const actionsRegistry = useActionsRegistry();
12798
13135
  const hasInitializedRef = useRef(false);
12799
13136
  const isFirstRender = !hasInitializedRef.current;
12800
13137
  const needsFirstMountSyncRef = useRef(false);
12801
13138
  if (isFirstRender) {
12802
13139
  hasInitializedRef.current = true;
12803
- needsFirstMountSyncRef.current = ctx.active === "select" && opts.active !== "select";
13140
+ needsFirstMountSyncRef.current = ctx.active === "select" && opts.focused !== "select";
12804
13141
  }
12805
13142
  useEffect(() => {
12806
13143
  if (!needsFirstMountSyncRef.current) return;
12807
13144
  needsFirstMountSyncRef.current = false;
12808
- ctx.setActive(opts.active);
13145
+ ctx.setActive(opts.focused);
13146
+ }, []);
13147
+ const focused2 = isFirstRender && ctx.active === "select" && opts.focused !== "select" ? opts.focused : ctx.active;
13148
+ const entriesRef = useRef(opts.entries);
13149
+ entriesRef.current = opts.entries;
13150
+ const focusedRef = useRef(focused2);
13151
+ focusedRef.current = focused2;
13152
+ const engagedRef = useRef(ctx.hotkeyStack);
13153
+ engagedRef.current = ctx.hotkeyStack;
13154
+ const setFocused = useCallback((id) => {
13155
+ ctx.setActive(id);
13156
+ }, [ctx]);
13157
+ const snapshot = useCallback(() => {
13158
+ const engaged = engagedRef.current;
13159
+ const engagedIds = new Set(engaged);
13160
+ const ordered = [];
13161
+ for (const id of engaged) {
13162
+ const entry = entriesRef.current.find((e) => e.id === id);
13163
+ if (entry && !ordered.includes(entry)) ordered.push(entry);
13164
+ }
13165
+ for (const entry of entriesRef.current) {
13166
+ if (!ordered.includes(entry)) ordered.push(entry);
13167
+ }
13168
+ return {
13169
+ ordered,
13170
+ state: { focusedId: focusedRef.current, heldTriggers: NO_TRIGGERS, engagedIds }
13171
+ };
12809
13172
  }, []);
12810
- const active = isFirstRender && ctx.active === "select" && opts.active !== "select" ? opts.active : ctx.active;
12811
- const hotkeyStack = ctx.hotkeyStack;
12812
- const hotkeyEngaged = hotkeyStack.at(-1) ?? null;
12813
- const registryRef = useRef(opts.registry);
12814
- registryRef.current = opts.registry;
12815
- const ambientRef = useRef(opts.ambient ?? []);
12816
- ambientRef.current = opts.ambient ?? [];
12817
- const activeRef = useRef(active);
12818
- activeRef.current = active;
13173
+ const scopedBindings = useCallback(() => {
13174
+ const { ordered, state } = snapshot();
13175
+ return scopeBindings(ordered, state);
13176
+ }, [snapshot]);
13177
+ const overlays = useCallback(() => {
13178
+ const { ordered, state } = snapshot();
13179
+ const active = [];
13180
+ const hotkey = [];
13181
+ const ambient = [];
13182
+ for (const entry of ordered) {
13183
+ if (!entry.overlay) continue;
13184
+ const scope = liveScope(entry.id, entry.eligibility ?? {}, state);
13185
+ if (scope === "active") active.push(entry.overlay);
13186
+ else if (scope === "hotkey") hotkey.push(entry.overlay);
13187
+ else if (scope === "ambient") ambient.push(entry.overlay);
13188
+ }
13189
+ return [...active, ...hotkey, ...ambient];
13190
+ }, [snapshot]);
13191
+ const offhandSig = opts.entries.map((e) => e.eligibility?.offhand ? `${e.id}:${e.eligibility.offhand}` : "").filter(Boolean).join(",");
13192
+ useEffect(() => {
13193
+ if (!actionsRegistry || offhandSig === "") return;
13194
+ const specs = offhandSig.split(",").map((entry) => {
13195
+ const [toolId, trigger] = entry.split(":");
13196
+ return { toolId, key: offhandKeyFor(trigger) };
13197
+ });
13198
+ return actionsRegistry.register(makeToolOffhandAction(buildToolOffhandBindings(specs)));
13199
+ }, [actionsRegistry, offhandSig]);
13200
+ const lastConflictSigRef = useRef(null);
13201
+ const entrySig = opts.entries.map((e) => e.id).join(",");
13202
+ useEffect(() => {
13203
+ if (process.env.NODE_ENV === "production") return;
13204
+ if (lastConflictSigRef.current === entrySig) return;
13205
+ lastConflictSigRef.current = entrySig;
13206
+ const registry2 = [];
13207
+ const ambient = [];
13208
+ for (const entry of entriesRef.current) {
13209
+ (entry.eligibility?.focus ? registry2 : ambient).push(entry);
13210
+ }
13211
+ reportRouteConflicts({
13212
+ registry: registry2,
13213
+ ambient,
13214
+ actions: actionsRegistry?.list() ?? []
13215
+ });
13216
+ }, [entrySig, actionsRegistry]);
13217
+ return useMemo(
13218
+ () => ({
13219
+ entries: entriesRef.current,
13220
+ focused: focused2,
13221
+ setFocused,
13222
+ scopedBindings,
13223
+ overlays
13224
+ }),
13225
+ [focused2, setFocused, scopedBindings, overlays]
13226
+ );
13227
+ }
13228
+
13229
+ // src/tools/useTools.ts
13230
+ function declareSlot(tool, slot) {
13231
+ const hotkey = tool.def?.hotkey;
13232
+ const eligibility = {
13233
+ ...tool.eligibility,
13234
+ [slot]: true,
13235
+ ...hotkey ? { offhand: hotkey } : {}
13236
+ };
13237
+ return sameEligibility(tool.eligibility, eligibility) ? tool : { ...tool, eligibility };
13238
+ }
13239
+ function sameEligibility(a, b) {
13240
+ if (!a) return false;
13241
+ const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
13242
+ for (const key of keys) if (a[key] !== b[key]) return false;
13243
+ return true;
13244
+ }
13245
+ function useTools(opts) {
13246
+ if (!(opts.active in opts.registry)) {
13247
+ throw new Error(`useTools: active "${opts.active}" not in registry`);
13248
+ }
13249
+ const ctx = useActiveToolContext();
13250
+ const slotted = useMemo(() => {
13251
+ const registry2 = {};
13252
+ for (const [id, tool] of Object.entries(opts.registry)) {
13253
+ registry2[id] = declareSlot(tool, "focus");
13254
+ }
13255
+ const ambient = (opts.ambient ?? []).map((t) => declareSlot(t, "always"));
13256
+ const byId = /* @__PURE__ */ new Map();
13257
+ for (const tool of [...Object.values(registry2), ...ambient]) {
13258
+ const prior = byId.get(tool.id);
13259
+ if (prior) byId.set(tool.id, { ...tool, eligibility: { ...prior.eligibility, ...tool.eligibility } });
13260
+ else byId.set(tool.id, tool);
13261
+ }
13262
+ return { registry: registry2, ambient, entries: [...byId.values()] };
13263
+ }, [opts.registry, opts.ambient]);
13264
+ const contributions = useContributions({ entries: slotted.entries, focused: opts.active });
13265
+ const hotkeyEngaged = ctx.hotkeyStack.at(-1) ?? null;
13266
+ const slottedRef = useRef(slotted);
13267
+ slottedRef.current = slotted;
13268
+ const activeRef = useRef(contributions.focused);
13269
+ activeRef.current = contributions.focused;
12819
13270
  const hotkeyRef = useRef(hotkeyEngaged);
12820
13271
  hotkeyRef.current = hotkeyEngaged;
13272
+ const setFocused = contributions.setFocused;
12821
13273
  const setActive = useCallback(
12822
13274
  (id) => {
12823
- if (!(id in registryRef.current)) {
13275
+ if (!(id in slottedRef.current.registry)) {
12824
13276
  throw new Error(`setActive: "${id}" not in registry`);
12825
13277
  }
12826
13278
  dlog("tools", "active:", activeRef.current, "\u2192", id);
12827
- ctx.setActive(id);
13279
+ setFocused(id);
12828
13280
  },
12829
- [ctx]
13281
+ [setFocused]
12830
13282
  );
12831
13283
  const engageHotkey = useCallback(
12832
13284
  (id) => {
12833
- if (!(id in registryRef.current)) {
13285
+ if (!(id in slottedRef.current.registry)) {
12834
13286
  throw new Error(`engageHotkey: "${id}" not in registry`);
12835
13287
  }
12836
13288
  dlog("tools", "hotkey engaged:", id);
@@ -12842,32 +13294,12 @@ function useTools(opts) {
12842
13294
  if (hotkeyRef.current) dlog("tools", "hotkey disengaged:", hotkeyRef.current);
12843
13295
  ctx.popHotkey();
12844
13296
  }, [ctx]);
12845
- const lastConflictSigRef = useRef(null);
12846
- const toolSetSig = Object.keys(opts.registry).join(",") + "|" + (opts.ambient ?? []).map((t) => t.id).join(",");
12847
- useEffect(() => {
12848
- if (process.env.NODE_ENV === "production") return;
12849
- if (lastConflictSigRef.current === toolSetSig) return;
12850
- lastConflictSigRef.current = toolSetSig;
12851
- reportRouteConflicts({
12852
- registry: registryRef.current,
12853
- ambient: ambientRef.current
12854
- });
12855
- }, [toolSetSig]);
12856
13297
  const has = useCallback(
12857
- (id) => id in registryRef.current || ambientRef.current.some((t) => t.id === id),
13298
+ (id) => id in slottedRef.current.registry || slottedRef.current.ambient.some((t) => t.id === id),
12858
13299
  []
12859
13300
  );
12860
- const getActiveOverlays = useCallback(() => {
12861
- const out = [];
12862
- const activeTool = registryRef.current[activeRef.current];
12863
- if (activeTool?.overlay) out.push(activeTool.overlay);
12864
- const mod = hotkeyRef.current ? registryRef.current[hotkeyRef.current] : null;
12865
- if (mod?.overlay) out.push(mod.overlay);
12866
- for (const t of ambientRef.current) {
12867
- if (t.overlay) out.push(t.overlay);
12868
- }
12869
- return out;
12870
- }, []);
13301
+ const active = contributions.focused;
13302
+ const getActiveOverlays = contributions.overlays;
12871
13303
  return useMemo(
12872
13304
  () => ({
12873
13305
  active,
@@ -12875,8 +13307,8 @@ function useTools(opts) {
12875
13307
  hotkeyEngaged,
12876
13308
  engageHotkey,
12877
13309
  disengageHotkey,
12878
- ambient: ambientRef.current,
12879
- registry: registryRef.current,
13310
+ ambient: slottedRef.current.ambient,
13311
+ registry: slottedRef.current.registry,
12880
13312
  has,
12881
13313
  getActiveOverlays
12882
13314
  }),
@@ -12884,38 +13316,6 @@ function useTools(opts) {
12884
13316
  );
12885
13317
  }
12886
13318
 
12887
- // src/interactions/actions/defaults/toolOffhand.ts
12888
- var TOOL_OFFHAND_ID = "tool.offhand";
12889
- function buildToolOffhandBindings(specs) {
12890
- return specs.map(({ toolId, key }) => ({
12891
- spec: { kind: "key-held", key },
12892
- opts: { params: { toolId } }
12893
- }));
12894
- }
12895
- function makeToolOffhandAction(bindings) {
12896
- return {
12897
- id: TOOL_OFFHAND_ID,
12898
- label: "Engage tool (offhand)",
12899
- defaultBinding: bindings,
12900
- scope: "hotkey",
12901
- invoker: {
12902
- timing: "ongoing",
12903
- start: (ctx, opts) => {
12904
- const activeTool = ctx.deps.activeTool;
12905
- const toolId = resolveParams(opts?.params)?.toolId;
12906
- if (!activeTool || !toolId) return {};
12907
- activeTool.pushHotkey(toolId);
12908
- return {
12909
- onEnd: () => {
12910
- activeTool.popHotkey();
12911
- }
12912
- };
12913
- }
12914
- },
12915
- requires: ["activeTool"]
12916
- };
12917
- }
12918
-
12919
13319
  // src/interactions/actions/defaults/toolActivate.ts
12920
13320
  var TOOL_ACTIVATE_ID = "tool.activate";
12921
13321
  function buildToolActivateBindings(specs) {
@@ -12992,9 +13392,6 @@ var BUILTIN_SELECT_KEYS = {
12992
13392
  hand: { key: "H" },
12993
13393
  pen: { key: "P" }
12994
13394
  };
12995
- var BUILTIN_OFFHAND_ACTIONS = {
12996
- hand: " "
12997
- };
12998
13395
  function useKeybindings(tools, options = {}) {
12999
13396
  const toolsRef = useRef(tools);
13000
13397
  toolsRef.current = tools;
@@ -13002,19 +13399,6 @@ function useKeybindings(tools, options = {}) {
13002
13399
  optionsRef.current = options;
13003
13400
  const initialActiveRef = useRef(tools.active);
13004
13401
  const registry2 = useActionsRegistry();
13005
- useEffect(() => {
13006
- if (optionsRef.current.disable) return;
13007
- if (!registry2) return;
13008
- const specs = [];
13009
- for (const [toolId, key] of Object.entries(BUILTIN_OFFHAND_ACTIONS)) {
13010
- if (toolsRef.current.has(toolId)) {
13011
- specs.push({ toolId, key });
13012
- }
13013
- }
13014
- if (specs.length === 0) return;
13015
- const bindings = buildToolOffhandBindings(specs);
13016
- return registry2.register(makeToolOffhandAction(bindings));
13017
- }, [registry2, tools]);
13018
13402
  useEffect(() => {
13019
13403
  if (optionsRef.current.disable) return;
13020
13404
  if (!registry2) return;
@@ -13553,21 +13937,18 @@ function textLineBoxes(pose, opts = {}) {
13553
13937
  const style = resolveTextStyle(pose.style);
13554
13938
  const source = pose.runs && pose.runs.length > 0 ? pose.runs : pose.text;
13555
13939
  const runs = resolveRuns(toRuns(source), style);
13556
- const laid = layoutRuns(
13557
- runs,
13558
- {
13559
- maxWidth: opts.maxWidth ?? pose.width,
13560
- lineHeight: style.lineHeight,
13561
- align: style.align
13562
- },
13563
- { x: pose.x, y: pose.y }
13564
- );
13565
- const dy = verticalAlignOffset(pose.verticalAlign, pose.height, laid.bounds.height);
13940
+ const laid = layoutRuns(runs, {
13941
+ maxWidth: opts.maxWidth ?? pose.width,
13942
+ lineHeight: style.lineHeight,
13943
+ align: style.align
13944
+ });
13945
+ const dx = pose.x;
13946
+ const dy = pose.y + verticalAlignOffset(pose.verticalAlign, pose.height, laid.bounds.height);
13566
13947
  const out = [];
13567
13948
  for (const line of laid.lines) {
13568
13949
  if (!opts.includeEmpty && line.x1 <= line.x0) continue;
13569
13950
  out.push({
13570
- x: line.x0 - padding,
13951
+ x: line.x0 + dx - padding,
13571
13952
  y: line.y0 + dy - padding,
13572
13953
  width: line.x1 - line.x0 + padding * 2,
13573
13954
  height: line.y1 - line.y0 + padding * 2
@@ -13576,8 +13957,108 @@ function textLineBoxes(pose, opts = {}) {
13576
13957
  return out;
13577
13958
  }
13578
13959
 
13579
- // src/features/images/imageCache.ts
13960
+ // src/core/fillInPoseFrame.ts
13961
+ function fillInPoseFrame(fill, box) {
13962
+ if (isBoundsPattern(fill)) {
13963
+ const o = fill.origin ?? { x: 0, y: 0 };
13964
+ return { ...fill, origin: { x: box.x + o.x, y: box.y + o.y }, units: "local" };
13965
+ }
13966
+ if (!isBoundsGradient(fill)) return fill;
13967
+ const toBox = (p) => ({
13968
+ x: box.x + p.x * box.width,
13969
+ y: box.y + p.y * box.height
13970
+ });
13971
+ if (fill.fill === "linear-gradient") {
13972
+ return { ...fill, from: toBox(fill.from), to: toBox(fill.to), units: "local" };
13973
+ }
13974
+ if (fill.fill === "radial-gradient") {
13975
+ const scale2 = Math.hypot(box.width, box.height) / Math.SQRT2;
13976
+ return { ...fill, center: toBox(fill.center), radius: fill.radius * scale2, units: "local" };
13977
+ }
13978
+ return { ...fill, center: toBox(fill.center), units: "local" };
13979
+ }
13980
+ function fillToBoundsFrame(fill, box) {
13981
+ if (fill.fill === "pattern") {
13982
+ const o = fill.origin ?? { x: 0, y: 0 };
13983
+ return { ...fill, origin: { x: o.x - box.x, y: o.y - box.y }, units: "bounds" };
13984
+ }
13985
+ const isGradient = fill.fill === "linear-gradient" || fill.fill === "radial-gradient" || fill.fill === "conic-gradient";
13986
+ if (!isGradient) return fill;
13987
+ if (box.width === 0 || box.height === 0) return fill;
13988
+ const fromBox = (p) => ({
13989
+ x: (p.x - box.x) / box.width,
13990
+ y: (p.y - box.y) / box.height
13991
+ });
13992
+ if (fill.fill === "linear-gradient") {
13993
+ return { ...fill, from: fromBox(fill.from), to: fromBox(fill.to), units: "bounds" };
13994
+ }
13995
+ if (fill.fill === "radial-gradient") {
13996
+ const scale2 = Math.hypot(box.width, box.height) / Math.SQRT2;
13997
+ return { ...fill, center: fromBox(fill.center), radius: fill.radius / scale2, units: "bounds" };
13998
+ }
13999
+ return { ...fill, center: fromBox(fill.center), units: "bounds" };
14000
+ }
14001
+ function isBoundsPattern(fill) {
14002
+ return fill.fill === "pattern" && fill.units === "bounds";
14003
+ }
14004
+ function isBoundsGradient(fill) {
14005
+ if (fill === void 0) return false;
14006
+ const kind = fill.fill;
14007
+ if (kind !== "linear-gradient" && kind !== "radial-gradient" && kind !== "conic-gradient") {
14008
+ return false;
14009
+ }
14010
+ return fill.units === "bounds";
14011
+ }
14012
+
14013
+ // src/features/patterns/resolveSpec.ts
13580
14014
  var cache5 = /* @__PURE__ */ new Map();
14015
+ function specKey(spec) {
14016
+ const keys = Object.keys(spec).sort();
14017
+ const bag = spec;
14018
+ return keys.map((k) => `${k}=${String(bag[k])}`).join("&");
14019
+ }
14020
+ function resolvePatternSpec(spec) {
14021
+ const key = specKey(spec);
14022
+ const hit = cache5.get(key);
14023
+ if (hit !== void 0) return hit;
14024
+ const built = buildTile(spec);
14025
+ cache5.set(key, built);
14026
+ return built;
14027
+ }
14028
+ function buildTile(spec) {
14029
+ const { color, size } = spec;
14030
+ switch (spec.tile) {
14031
+ case "hatch":
14032
+ return hatch({ color, size, lineWidth: spec.lineWidth });
14033
+ case "crosshatch":
14034
+ return crosshatch({ color, size, lineWidth: spec.lineWidth });
14035
+ case "dots":
14036
+ return dots({ color, size, radius: spec.radius });
14037
+ case "chunks":
14038
+ return chunks({
14039
+ color,
14040
+ size,
14041
+ bg: spec.bg,
14042
+ density: spec.density,
14043
+ chunkSize: spec.chunkSize,
14044
+ seed: spec.seed
14045
+ });
14046
+ default:
14047
+ return null;
14048
+ }
14049
+ }
14050
+ function isPatternSpec(payload) {
14051
+ return "tile" in payload;
14052
+ }
14053
+ function resolveFillPattern(fill) {
14054
+ if (fill.fill !== "pattern") return fill;
14055
+ if (!isPatternSpec(fill.pattern)) return fill;
14056
+ const handle = resolvePatternSpec(fill.pattern);
14057
+ return handle ? { ...fill, pattern: handle } : null;
14058
+ }
14059
+
14060
+ // src/features/images/imageCache.ts
14061
+ var cache6 = /* @__PURE__ */ new Map();
13581
14062
  var subscribers = /* @__PURE__ */ new Set();
13582
14063
  var defaultLoader = async (src) => {
13583
14064
  if (typeof Image === "undefined" || typeof createImageBitmap === "undefined") {
@@ -13595,28 +14076,28 @@ function notify() {
13595
14076
  }
13596
14077
  function startLoad(src) {
13597
14078
  const entry = { status: "loading" };
13598
- cache5.set(src, entry);
14079
+ cache6.set(src, entry);
13599
14080
  loader(src).then(
13600
14081
  (bitmap) => {
13601
- if (cache5.get(src) !== entry) return;
13602
- cache5.set(src, { status: "ready", bitmap });
14082
+ if (cache6.get(src) !== entry) return;
14083
+ cache6.set(src, { status: "ready", bitmap });
13603
14084
  notify();
13604
14085
  },
13605
14086
  () => {
13606
- if (cache5.get(src) !== entry) return;
13607
- cache5.set(src, { status: "error" });
14087
+ if (cache6.get(src) !== entry) return;
14088
+ cache6.set(src, { status: "error" });
13608
14089
  notify();
13609
14090
  }
13610
14091
  );
13611
14092
  }
13612
14093
  function getImageBitmap(src) {
13613
- const entry = cache5.get(src);
14094
+ const entry = cache6.get(src);
13614
14095
  if (entry) return entry.bitmap;
13615
14096
  startLoad(src);
13616
14097
  return void 0;
13617
14098
  }
13618
14099
  function imageStatus(src) {
13619
- return cache5.get(src)?.status ?? "idle";
14100
+ return cache6.get(src)?.status ?? "idle";
13620
14101
  }
13621
14102
  function subscribeImageReady(cb) {
13622
14103
  subscribers.add(cb);
@@ -13801,6 +14282,17 @@ function rectsToPath(rects) {
13801
14282
  }
13802
14283
  return { kind: "polygon", commands, coords, fillRule: "nonzero" };
13803
14284
  }
14285
+ function isFillStyle(v) {
14286
+ return typeof v === "object" && v !== null;
14287
+ }
14288
+ function resolveNodeFill(fill, color, hasStroke, fallback) {
14289
+ if (isFillStyle(fill)) return fill;
14290
+ const c = fill ?? color;
14291
+ if (c === "none") return null;
14292
+ if (c !== void 0) return { color: c };
14293
+ if (hasStroke && fallback === null) return null;
14294
+ return fallback === null ? null : { color: fallback };
14295
+ }
13804
14296
  var PATH_PAINTER = {
13805
14297
  id: "kit:path",
13806
14298
  matches: (node) => {
@@ -13814,12 +14306,12 @@ var PATH_PAINTER = {
13814
14306
  const d = node.data;
13815
14307
  const projected = pathInPoseFrame(d.path, pose);
13816
14308
  const hasStroke = !!d.stroke && d.stroke !== "none" && (d.strokeWidth ?? 0) > 0;
13817
- const fillColor = d.fill ?? d.color;
13818
- const hasFill = fillColor !== "none" && (fillColor !== void 0 || !hasStroke);
14309
+ const declared = resolveNodeFill(d.fill, d.color, hasStroke, hasStroke ? null : "#888");
14310
+ const fill = declared && resolveFillPattern(fillInPoseFrame(declared, pose));
13819
14311
  const cmd = {
13820
14312
  kind: "path",
13821
14313
  path: projected,
13822
- ...hasFill ? { fill: { color: fillColor ?? "#888" } } : {},
14314
+ ...fill ? { fill } : {},
13823
14315
  ...hasStroke ? { stroke: { paint: { color: d.stroke }, width: d.strokeWidth ?? 1 } } : {}
13824
14316
  };
13825
14317
  return [cmd];
@@ -13835,9 +14327,8 @@ var PATH_PAINTER = {
13835
14327
  ink: (node) => {
13836
14328
  const d = node.data;
13837
14329
  const hasStroke = !!d.stroke && d.stroke !== "none" && (d.strokeWidth ?? 0) > 0;
13838
- const fillColor = d.fill ?? d.color;
13839
14330
  return {
13840
- filled: fillColor !== "none" && (fillColor !== void 0 || !hasStroke),
14331
+ filled: resolveNodeFill(d.fill, d.color, hasStroke, hasStroke ? null : "#888") !== null,
13841
14332
  strokeWidth: hasStroke ? d.strokeWidth ?? 1 : 0
13842
14333
  };
13843
14334
  }
@@ -13854,10 +14345,12 @@ var SHAPE_PAINTER = {
13854
14345
  paint: (node, pose) => nodeMemo(node, PAINT_SLOT, pose, () => {
13855
14346
  const d = node.data;
13856
14347
  const path = pathForShape(d, pose);
14348
+ const declaredFill = resolveNodeFill(d.fill, d.color, false, "#888");
14349
+ const shapeFill = declaredFill && resolveFillPattern(fillInPoseFrame(declaredFill, pose));
13857
14350
  return [{
13858
14351
  kind: "path",
13859
14352
  path,
13860
- fill: { color: d.fill ?? d.color ?? "#888" },
14353
+ ...shapeFill ? { fill: shapeFill } : {},
13861
14354
  ...d.stroke && (d.strokeWidth ?? 0) > 0 ? { stroke: { paint: { color: d.stroke }, width: d.strokeWidth ?? 1 } } : {}
13862
14355
  }];
13863
14356
  }),
@@ -14258,11 +14751,8 @@ function useSelectTool(adapter, options) {
14258
14751
  ...moveOpts
14259
14752
  },
14260
14753
  { spec: { kind: "drag", target: "empty" }, actionId: "areaSelect" },
14261
- // Click on empty canvas clear. Same no-affordance requirement as
14262
- // the press binding above, and for the same two reasons: clicking a
14263
- // widget that happens to float over empty canvas isn't a click on
14264
- // empty canvas, and declining in the SPEC lets the chrome's owner
14265
- // have the click instead of it being consumed here.
14754
+ // resize/rotate bind only drag (no click), so a click on either handle
14755
+ // (body genuinely empty) would hit target:'empty' and wrongly clear selection.
14266
14756
  {
14267
14757
  spec: {
14268
14758
  kind: "click",
@@ -14348,17 +14838,17 @@ function worldToLocal(xf, wx, wy) {
14348
14838
  function annulusSemiAxes(shape, view) {
14349
14839
  const bandPx = shape.minBandPx ?? 0;
14350
14840
  if (bandPx === 0) return { rx: shape.rx, ry: shape.ry };
14351
- const band = bandPx / meanScale(view.scale);
14841
+ const band = pxExtent(bandPx, view.scale);
14352
14842
  return {
14353
- rx: Math.max(shape.rx, shape.innerWidth / 2 + band),
14354
- ry: Math.max(shape.ry, shape.innerHeight / 2 + band)
14843
+ rx: Math.max(shape.rx, shape.innerWidth / 2 + band.x),
14844
+ ry: Math.max(shape.ry, shape.innerHeight / 2 + band.y)
14355
14845
  };
14356
14846
  }
14357
14847
  function hitRegion(region, wx, wy, xf, view) {
14358
14848
  const local = worldToLocal(xf, wx, wy);
14359
14849
  if (region.shape.kind === "point") {
14360
- const radiusWorld = region.shape.hitRadiusPx / meanScale(view.scale);
14361
- return Math.abs(local.x - region.shape.x) <= radiusWorld && Math.abs(local.y - region.shape.y) <= radiusWorld;
14850
+ const anchor = localToWorld(xf, region.shape.x, region.shape.y);
14851
+ return withinPxBox(anchor.x - wx, anchor.y - wy, region.shape.hitRadiusPx, view.scale);
14362
14852
  }
14363
14853
  if (region.shape.kind === "annulus") {
14364
14854
  const s = region.shape;
@@ -14424,14 +14914,13 @@ function asIsVisible(data) {
14424
14914
  function recordRegionHitbox(debug, affordanceId, region, xf, view) {
14425
14915
  if (region.shape.kind === "point") {
14426
14916
  const w2 = localToWorld(xf, region.shape.x, region.shape.y);
14427
- const r2 = region.shape.hitRadiusPx / meanScale(view.scale);
14917
+ const e = pxExtent(region.shape.hitRadiusPx, view.scale);
14428
14918
  debug.recordHitbox(affordanceId, "handle", {
14429
14919
  kind: "rect",
14430
- x: w2.x - r2,
14431
- y: w2.y - r2,
14432
- width: r2 * 2,
14433
- height: r2 * 2,
14434
- ...xf.identity ? {} : { rotation: Math.atan2(xf.sin, xf.cos) }
14920
+ x: w2.x - e.x,
14921
+ y: w2.y - e.y,
14922
+ width: e.x * 2,
14923
+ height: e.y * 2
14435
14924
  });
14436
14925
  return;
14437
14926
  }
@@ -14490,7 +14979,7 @@ function paintRegion(region, xf, view, state, viewT, out) {
14490
14979
  if (paint.kind === "annulus") {
14491
14980
  if (region.shape.kind !== "annulus") return;
14492
14981
  const s = region.shape;
14493
- const insetLocal = (paint.insetPx ?? 0) / meanScale(view.scale);
14982
+ const insetLocal = pxExtent(paint.insetPx ?? 0, view.scale);
14494
14983
  const cmd = annulusCommand(s, annulusSemiAxes(s, view), insetLocal, xf, viewT, paint.fill, paint.stroke);
14495
14984
  if (cmd) out.push(cmd);
14496
14985
  return;
@@ -14507,13 +14996,13 @@ function paintRegion(region, xf, view, state, viewT, out) {
14507
14996
  }
14508
14997
  }
14509
14998
  function annulusCommand(shape, semiAxes, insetLocal, xf, viewT, fill, stroke) {
14510
- const rx = semiAxes.rx - insetLocal;
14511
- const ry = semiAxes.ry - insetLocal;
14999
+ const rx = semiAxes.rx - insetLocal.x;
15000
+ const ry = semiAxes.ry - insetLocal.y;
14512
15001
  if (rx <= 0 || ry <= 0) return null;
14513
15002
  const cx = shape.cx;
14514
15003
  const cy = shape.cy;
14515
- const rectHW = shape.innerWidth / 2 + insetLocal;
14516
- const rectHH = shape.innerHeight / 2 + insetLocal;
15004
+ const rectHW = shape.innerWidth / 2 + insetLocal.x;
15005
+ const rectHH = shape.innerHeight / 2 + insetLocal.y;
14517
15006
  const haveTopBottom = rectHH < ry;
14518
15007
  const haveLeftRight = rectHW < rx;
14519
15008
  if (!haveTopBottom && !haveLeftRight) return null;
@@ -14774,6 +15263,7 @@ function useHandTool(opts = {}) {
14774
15263
  () => Object.assign(defineViewportTool({
14775
15264
  id: "hand",
14776
15265
  capabilities: ["navigation"],
15266
+ hotkey: "space",
14777
15267
  hookName: "useHandTool",
14778
15268
  presentation: {
14779
15269
  label: "Hand",
@@ -14898,10 +15388,6 @@ function mirrorHandle(anchor, out) {
14898
15388
  if (!out || anchor.altBroken) return void 0;
14899
15389
  return { x: 2 * anchor.x - out.x, y: 2 * anchor.y - out.y };
14900
15390
  }
14901
- function dist2(ax, ay, bx, by) {
14902
- const dx = ax - bx, dy = ay - by;
14903
- return Math.hypot(dx, dy);
14904
- }
14905
15391
  function usePenTool(options) {
14906
15392
  const { wrapPath, adapter, autoSelect = true, autoCommitOnClose = true, closeHitRadius = 8, snapPoint: snapPoint2 } = options;
14907
15393
  const scratchRef = useRef(null);
@@ -14925,11 +15411,14 @@ function usePenTool(options) {
14925
15411
  const fn = optsRef.current.snapPoint;
14926
15412
  return fn ? fn({ x, y }) : { x, y };
14927
15413
  }, []);
14928
- const closeRadius = useCallback((deps) => {
14929
- const view = deps.view?.get();
14930
- const scale2 = view?.scale ?? { x: 1, y: 1 };
14931
- return optsRef.current.closeHitRadius / meanScale(scale2);
14932
- }, []);
15414
+ const withinCloseRadius = useCallback(
15415
+ (deps, ax, ay, wx, wy) => {
15416
+ const view = deps.view?.get();
15417
+ const scale2 = view?.scale ?? { x: 1, y: 1 };
15418
+ return withinPxRadius(ax - wx, ay - wy, optsRef.current.closeHitRadius, scale2);
15419
+ },
15420
+ []
15421
+ );
14933
15422
  const actions = useMemo(() => {
14934
15423
  const s = () => scratchRef.current;
14935
15424
  return [
@@ -14945,10 +15434,9 @@ function usePenTool(options) {
14945
15434
  if (p?.pressX === void 0 || p.pressY === void 0) return;
14946
15435
  const { x: wx, y: wy } = snap2(p.pressX, p.pressY);
14947
15436
  const scratch = s();
14948
- const radius = closeRadius(deps);
14949
15437
  if (scratch.current && scratch.current.anchors.length >= 3) {
14950
15438
  const first = scratch.current.anchors[0];
14951
- if (dist2(first.x, first.y, wx, wy) <= radius) {
15439
+ if (withinCloseRadius(deps, first.x, first.y, wx, wy)) {
14952
15440
  scratch.current.closed = true;
14953
15441
  scratch.finishedSubpaths.push(scratch.current);
14954
15442
  scratch.current = null;
@@ -15067,7 +15555,7 @@ function usePenTool(options) {
15067
15555
  }
15068
15556
  }
15069
15557
  ];
15070
- }, [commit, snap2, closeRadius]);
15558
+ }, [commit, snap2, withinCloseRadius]);
15071
15559
  return useMemo(() => {
15072
15560
  return defineTool({
15073
15561
  id: "pen",
@@ -16443,6 +16931,15 @@ function wrapWithPoseRotation(cmds, pose) {
16443
16931
  children: cmds
16444
16932
  }];
16445
16933
  }
16934
+
16935
+ // src/canvas/wrapNodeOutput.ts
16936
+ function wrapNodeOutput(cmds, pose, alpha) {
16937
+ const rotated = wrapWithPoseRotation(cmds, pose);
16938
+ if (alpha !== 1 && rotated.length > 0) {
16939
+ return [{ kind: "group", alpha, children: rotated }];
16940
+ }
16941
+ return rotated;
16942
+ }
16446
16943
  function CursorCoordsHud({ canvasRef, viewRef, offset }) {
16447
16944
  const [state, setState] = useState({
16448
16945
  client: { x: 0, y: 0 },
@@ -16694,12 +17191,7 @@ function buildSceneLayer(cfg, adapter, debugSink, boundsOfFn, hideIds, slot) {
16694
17191
  const oy = pose.y ?? (b ? b.y : 0);
16695
17192
  debugSink.recordOrigin(obj.id, { x: ox, y: oy });
16696
17193
  }
16697
- const rotated = wrapWithPoseRotation(cmds, pose);
16698
- const alpha = cfg.alphaFor ? cfg.alphaFor(obj.id) : 1;
16699
- if (alpha !== 1 && rotated.length > 0) {
16700
- return [{ kind: "group", alpha, children: rotated }];
16701
- }
16702
- return rotated;
17194
+ return wrapNodeOutput(cmds, pose, cfg.alphaFor ? cfg.alphaFor(obj.id) : 1);
16703
17195
  };
16704
17196
  const hierarchicalAdapter = cfg.toPose ? {
16705
17197
  ...a,
@@ -16723,13 +17215,8 @@ function buildSceneLayer(cfg, adapter, debugSink, boundsOfFn, hideIds, slot) {
16723
17215
  const pose = toPose(obj);
16724
17216
  if (drawOne) {
16725
17217
  const cmds = drawOne(obj, pose, view);
16726
- const rotated = wrapWithPoseRotation(cmds, pose);
16727
- const alpha = cfg.alphaFor ? cfg.alphaFor(obj.id) : 1;
16728
- if (alpha !== 1 && rotated.length > 0) {
16729
- children.push({ kind: "group", alpha, children: rotated });
16730
- } else {
16731
- for (const cmd of rotated) children.push(cmd);
16732
- }
17218
+ const wrapped = wrapNodeOutput(cmds, pose, cfg.alphaFor ? cfg.alphaFor(obj.id) : 1);
17219
+ for (const cmd of wrapped) children.push(cmd);
16733
17220
  }
16734
17221
  if (debugSink) {
16735
17222
  const b = boundsOfFn ? boundsOfFn(obj.id) : null;
@@ -16844,8 +17331,8 @@ function CanvasInner(props, ref) {
16844
17331
  const isVisible = getIsVisibleRef.current?.() ?? alwaysVisible;
16845
17332
  for (const layer of layers2) {
16846
17333
  if (!layer.hitTest) continue;
16847
- const binding = layer.hitTest(worldX, worldY, void 0, view, dims, isVisible);
16848
- if (binding) return { layerId: layer.id, binding };
17334
+ const hit = layer.hitTest(worldX, worldY, void 0, view, dims, isVisible);
17335
+ if (hit) return { layerId: layer.id, hit };
16849
17336
  }
16850
17337
  return null;
16851
17338
  }, []);
@@ -17281,7 +17768,7 @@ function CanvasInner(props, ref) {
17281
17768
  effectiveView,
17282
17769
  { width, height }
17283
17770
  );
17284
- renderer.render(commands);
17771
+ renderer.render(commands, viewToMat3(effectiveView));
17285
17772
  }, [layersWithDebug, width, height, effectiveView, debugSink, redrawNonce, dprProp]);
17286
17773
  const shaderIdKey = shaders?.map((h) => h.id).join("|") ?? "";
17287
17774
  useEffect(() => {
@@ -17443,6 +17930,7 @@ function createScene(options) {
17443
17930
  return i;
17444
17931
  }
17445
17932
  function rebuildLayerIndex() {
17933
+ invalidateOrder();
17446
17934
  state.layerIndex.clear();
17447
17935
  for (let i = 0; i < state.layers.length; i++) {
17448
17936
  state.layerIndex.set(state.layers[i].id, i);
@@ -17468,6 +17956,7 @@ function createScene(options) {
17468
17956
  const idx = sibs.indexOf(id);
17469
17957
  if (idx < 0) throw new Error(`Scene: node "${id}" not found in its parent's children`);
17470
17958
  sibs.splice(idx, 1);
17959
+ invalidateOrder();
17471
17960
  return { parent: node.parent, index: idx };
17472
17961
  }
17473
17962
  function attach(id, parent, index) {
@@ -17476,6 +17965,7 @@ function createScene(options) {
17476
17965
  sibs.splice(i, 0, id);
17477
17966
  const node = requireNode(id);
17478
17967
  node.parent = parent;
17968
+ invalidateOrder();
17479
17969
  }
17480
17970
  function descendants(id, out) {
17481
17971
  const n = state.nodes.get(id);
@@ -17555,9 +18045,11 @@ function createScene(options) {
17555
18045
  apply: (p) => {
17556
18046
  requireLayerIndex(p.to);
17557
18047
  requireNode(p.id).layer = p.to;
18048
+ invalidateOrder();
17558
18049
  },
17559
18050
  revert: (p) => {
17560
18051
  requireNode(p.id).layer = p.from;
18052
+ invalidateOrder();
17561
18053
  }
17562
18054
  });
17563
18055
  registerKitOp("kit:move", {
@@ -17696,21 +18188,98 @@ function createScene(options) {
17696
18188
  registered.set(k, h);
17697
18189
  }
17698
18190
  }
17699
- function* renderOrderInternal() {
17700
- for (const layer of state.layers) {
17701
- const stack = [...state.roots].reverse();
17702
- while (stack.length > 0) {
17703
- const id = stack.pop();
17704
- const node = state.nodes.get(id);
17705
- if (!node) continue;
17706
- if (node.layer === layer.id) yield id;
17707
- if (node.kind === "container") {
17708
- for (let i = node.children.length - 1; i >= 0; i--) {
17709
- stack.push(node.children[i]);
17710
- }
18191
+ function renderOrderInternal() {
18192
+ return state.layers.length === 1 ? renderOrderFlat() : renderOrderBucketed();
18193
+ }
18194
+ function renderOrderNodesInternal() {
18195
+ return state.layers.length === 1 ? renderOrderNodesFlat() : renderOrderNodesBucketed();
18196
+ }
18197
+ let structureGeneration = 0;
18198
+ let orderCache = { gen: 0 };
18199
+ function invalidateOrder() {
18200
+ structureGeneration++;
18201
+ }
18202
+ function currentOrderCache() {
18203
+ if (orderCache.gen !== structureGeneration) {
18204
+ orderCache = { gen: structureGeneration };
18205
+ }
18206
+ return orderCache;
18207
+ }
18208
+ function renderOrderFlat() {
18209
+ const only = state.layers[0].id;
18210
+ const out = [];
18211
+ const stack = [...state.roots].reverse();
18212
+ while (stack.length > 0) {
18213
+ const id = stack.pop();
18214
+ const node = state.nodes.get(id);
18215
+ if (!node) continue;
18216
+ if (node.layer === only) out.push(id);
18217
+ if (node.kind === "container") {
18218
+ for (let i = node.children.length - 1; i >= 0; i--) {
18219
+ stack.push(node.children[i]);
18220
+ }
18221
+ }
18222
+ }
18223
+ return out;
18224
+ }
18225
+ function renderOrderNodesFlat() {
18226
+ const only = state.layers[0].id;
18227
+ const out = [];
18228
+ const stack = [...state.roots].reverse();
18229
+ while (stack.length > 0) {
18230
+ const id = stack.pop();
18231
+ const node = state.nodes.get(id);
18232
+ if (!node) continue;
18233
+ if (node.layer === only) out.push(node);
18234
+ if (node.kind === "container") {
18235
+ for (let i = node.children.length - 1; i >= 0; i--) {
18236
+ stack.push(node.children[i]);
18237
+ }
18238
+ }
18239
+ }
18240
+ return out;
18241
+ }
18242
+ function renderOrderBucketed() {
18243
+ const buckets = state.layers.map(() => []);
18244
+ const stack = [...state.roots].reverse();
18245
+ while (stack.length > 0) {
18246
+ const id = stack.pop();
18247
+ const node = state.nodes.get(id);
18248
+ if (!node) continue;
18249
+ const bucket = buckets[state.layerIndex.get(node.layer) ?? -1];
18250
+ if (bucket) bucket.push(id);
18251
+ if (node.kind === "container") {
18252
+ for (let i = node.children.length - 1; i >= 0; i--) {
18253
+ stack.push(node.children[i]);
18254
+ }
18255
+ }
18256
+ }
18257
+ const out = [];
18258
+ for (const bucket of buckets) {
18259
+ for (const id of bucket) out.push(id);
18260
+ }
18261
+ return out;
18262
+ }
18263
+ function renderOrderNodesBucketed() {
18264
+ const buckets = state.layers.map(() => []);
18265
+ const stack = [...state.roots].reverse();
18266
+ while (stack.length > 0) {
18267
+ const id = stack.pop();
18268
+ const node = state.nodes.get(id);
18269
+ if (!node) continue;
18270
+ const bucket = buckets[state.layerIndex.get(node.layer) ?? -1];
18271
+ if (bucket) bucket.push(node);
18272
+ if (node.kind === "container") {
18273
+ for (let i = node.children.length - 1; i >= 0; i--) {
18274
+ stack.push(node.children[i]);
17711
18275
  }
17712
18276
  }
17713
18277
  }
18278
+ const out = [];
18279
+ for (const bucket of buckets) {
18280
+ for (const node of bucket) out.push(node);
18281
+ }
18282
+ return out;
17714
18283
  }
17715
18284
  const scene = {
17716
18285
  get nodes() {
@@ -17739,7 +18308,12 @@ function createScene(options) {
17739
18308
  return out;
17740
18309
  },
17741
18310
  renderOrder() {
17742
- return renderOrderInternal();
18311
+ const c = currentOrderCache();
18312
+ return c.ids ??= renderOrderInternal();
18313
+ },
18314
+ renderOrderNodes() {
18315
+ const c = currentOrderCache();
18316
+ return c.nodes ??= renderOrderNodesInternal();
17743
18317
  },
17744
18318
  add(spec) {
17745
18319
  const id = spec.id ?? generateId();
@@ -18047,9 +18621,8 @@ function createScene(options) {
18047
18621
  },
18048
18622
  toJSON() {
18049
18623
  const nodes = [];
18050
- for (const id of renderOrderInternal()) {
18051
- const n = state.nodes.get(id);
18052
- if (!n) continue;
18624
+ for (const n of renderOrderNodesInternal()) {
18625
+ const id = n.id;
18053
18626
  const out = {
18054
18627
  id,
18055
18628
  kind: n.kind,
@@ -18083,6 +18656,7 @@ function createScene(options) {
18083
18656
  state.roots.length = 0;
18084
18657
  state.layers.length = 0;
18085
18658
  state.layerIndex.clear();
18659
+ invalidateOrder();
18086
18660
  for (let i = 0; i < json.systemLayers.length; i++) {
18087
18661
  const spec = json.systemLayers[i];
18088
18662
  if (state.layerIndex.has(spec.id)) {
@@ -18380,9 +18954,8 @@ function sceneToAdapter(scene, options = {}) {
18380
18954
  getNodes() {
18381
18955
  const visible = visibleLayers();
18382
18956
  const out = [];
18383
- for (const id of scene.renderOrder()) {
18384
- const n = scene.get(id);
18385
- if (n && visible.has(n.layer)) out.push(n);
18957
+ for (const n of scene.renderOrderNodes()) {
18958
+ if (visible.has(n.layer)) out.push(n);
18386
18959
  }
18387
18960
  return out;
18388
18961
  },
@@ -18722,9 +19295,7 @@ function useSceneSelectTool(args) {
18722
19295
  // path descriptor for a tighter test.
18723
19296
  hitTestArea: (rect) => {
18724
19297
  const hits = [];
18725
- for (const nid of scene.renderOrder()) {
18726
- const n = scene.get(nid);
18727
- if (!n) continue;
19298
+ for (const n of scene.renderOrderNodes()) {
18728
19299
  if (isPathLike(n.pose) && pathPoseDescriptor.intersectsRect) {
18729
19300
  if (pathPoseDescriptor.intersectsRect(n.pose, rect)) hits.push(n.id);
18730
19301
  continue;
@@ -18762,9 +19333,8 @@ function useSceneSelectTool(args) {
18762
19333
  }
18763
19334
  const tolerance = pickTolerancePx / meanScale(getView?.()?.scale ?? { x: 1, y: 1 });
18764
19335
  const out = [];
18765
- for (const id of scene.renderOrder()) {
18766
- const n = scene.get(id);
18767
- if (!n || !poseContainsRotated(n.pose, wx, wy, tolerance)) continue;
19336
+ for (const n of scene.renderOrderNodes()) {
19337
+ if (!poseContainsRotated(n.pose, wx, wy, tolerance)) continue;
18768
19338
  if (shapePicking && !shapeCoversPoint(n, n.pose, wx, wy, { tolerance })) continue;
18769
19339
  out.push(n.id);
18770
19340
  }
@@ -19721,6 +20291,8 @@ function toAffordanceHit(hit) {
19721
20291
  const scratch = hit.binding.initialScratch ?? {};
19722
20292
  return {
19723
20293
  kind: hit.region.hitKind ?? `${hit.affordanceId}:${hit.regionId}`,
20294
+ owner: hit.affordanceId,
20295
+ strength: "shared",
19724
20296
  ...scratch.targetId !== void 0 ? { targetIds: [scratch.targetId] } : {},
19725
20297
  ...scratch.anchor !== void 0 ? { anchor: scratch.anchor } : {},
19726
20298
  ...scratch.fixedPoint !== void 0 ? { fixedPoint: scratch.fixedPoint } : {},
@@ -19906,23 +20478,25 @@ function hitTestAreaPolygon(scene, area, areaBounds) {
19906
20478
  const ab = areaBounds ?? boundsOf(area);
19907
20479
  if (!ab) return [];
19908
20480
  const hits = [];
19909
- for (const id of scene.renderOrder()) {
19910
- const node = scene.get(id);
19911
- if (!node || node.kind === "container") continue;
20481
+ const order = scene.renderOrderNodes();
20482
+ for (let i = 0; i < order.length; i++) {
20483
+ const node = order[i];
20484
+ if (node.kind === "container") continue;
19912
20485
  const pose = node.pose;
19913
- const b = aabbOfPose(pose);
20486
+ const silhouette = pose !== null && typeof pose === "object" && pose.kind === "polygon";
20487
+ const b = silhouette ? nodeMemo(node, "aabb", pose, () => aabbOfPose(pose)) : aabbOfPose(pose);
19914
20488
  if (!Number.isFinite(b.x) || !Number.isFinite(b.y) || !Number.isFinite(b.width) || !Number.isFinite(b.height)) {
19915
20489
  continue;
19916
20490
  }
19917
20491
  if (b.x >= ab.x + ab.width || b.x + b.width <= ab.x || b.y >= ab.y + ab.height || b.y + b.height <= ab.y) {
19918
20492
  continue;
19919
20493
  }
19920
- if (!isPathLike(pose) || pose.kind === "rect") {
19921
- hits.push(id);
20494
+ if (!silhouette) {
20495
+ hits.push(node.id);
19922
20496
  continue;
19923
20497
  }
19924
20498
  if (silhouetteOverlapsArea(pose.coords, area)) {
19925
- hits.push(id);
20499
+ hits.push(node.id);
19926
20500
  }
19927
20501
  }
19928
20502
  return hits;
@@ -20249,7 +20823,7 @@ function chordLengthParam(samples) {
20249
20823
  ts[0] = 0;
20250
20824
  let total = 0;
20251
20825
  for (let i = 1; i < n; i++) {
20252
- total += dist3(samples[i - 1], samples[i]);
20826
+ total += dist2(samples[i - 1], samples[i]);
20253
20827
  ts[i] = total;
20254
20828
  }
20255
20829
  if (total === 0) return null;
@@ -20282,13 +20856,13 @@ function solveLS(samples, ts, a, e, tStart, tEnd) {
20282
20856
  let alpha1;
20283
20857
  let alpha2;
20284
20858
  if (Math.abs(det) < 1e-12) {
20285
- const chord = dist3(a, e);
20859
+ const chord = dist2(a, e);
20286
20860
  alpha1 = alpha2 = chord / 3;
20287
20861
  } else {
20288
20862
  alpha1 = (c22 * x1 - c12 * x2) / det;
20289
20863
  alpha2 = (c11 * x2 - c12 * x1) / det;
20290
20864
  if (alpha1 < 1e-6 || alpha2 < 1e-6) {
20291
- const chord = dist3(a, e);
20865
+ const chord = dist2(a, e);
20292
20866
  alpha1 = alpha2 = chord / 3;
20293
20867
  }
20294
20868
  }
@@ -20324,7 +20898,7 @@ function newtonRefineT(p, t, p0, p1, p2, p3) {
20324
20898
  function sub(a, b) {
20325
20899
  return { x: a.x - b.x, y: a.y - b.y };
20326
20900
  }
20327
- function dist3(a, b) {
20901
+ function dist2(a, b) {
20328
20902
  return Math.hypot(a.x - b.x, a.y - b.y);
20329
20903
  }
20330
20904
  function unit(v) {
@@ -21334,7 +21908,7 @@ function SceneCanvasInner(props, ref) {
21334
21908
  const getMode = getActiveModeRef.current;
21335
21909
  if (!getMode) return true;
21336
21910
  const registry2 = toolsForEligibilityRef.current?.registry;
21337
- const caps = registry2?.[toolId]?.capabilities ?? [];
21911
+ const caps = registry2?.[toolId]?.eligibility.capabilities ?? [];
21338
21912
  if (caps.length === 0) return false;
21339
21913
  const allowed = getMode().allowedCapabilities;
21340
21914
  for (const c of caps) if (allowed.has(c)) return true;
@@ -21808,10 +22382,6 @@ function GestureDispatcherMounter({
21808
22382
  for (const tool of tools.ambient) m.set(tool.id, tool);
21809
22383
  return m;
21810
22384
  }, [tools.registry, tools.ambient]);
21811
- const ambientToolIds = useMemo(
21812
- () => tools.ambient.map((t) => t.id),
21813
- [tools.ambient]
21814
- );
21815
22385
  const boundsOfRef = useRef(boundsOf2);
21816
22386
  boundsOfRef.current = boundsOf2;
21817
22387
  const pickEveryRef = useRef(pickEvery);
@@ -21923,9 +22493,14 @@ function GestureDispatcherMounter({
21923
22493
  const worldPoint = clientToWorld2(screenPoint.x, screenPoint.y);
21924
22494
  const extra = canvasApiRef?.current?.hitTestExtras?.(worldPoint.x, worldPoint.y);
21925
22495
  if (extra) {
22496
+ const claim = extra.hit;
21926
22497
  return {
21927
22498
  kind: `layer:${extra.layerId}`,
21928
- ...extra.binding.initialScratch !== void 0 ? { payload: extra.binding.initialScratch } : {}
22499
+ owner: extra.layerId,
22500
+ strength: claim.strength ?? "shared",
22501
+ ...claim.claimedKinds !== void 0 ? { claimedKinds: claim.claimedKinds } : {},
22502
+ ...claim.cursor !== void 0 ? { cursor: claim.cursor } : {},
22503
+ ...claim.initialScratch !== void 0 ? { payload: claim.initialScratch } : {}
21929
22504
  };
21930
22505
  }
21931
22506
  return affordanceAt ? affordanceAt(worldPoint) : null;
@@ -21945,7 +22520,6 @@ function GestureDispatcherMounter({
21945
22520
  canvasRef,
21946
22521
  actions: registry2,
21947
22522
  toolsById,
21948
- ambientToolIds,
21949
22523
  enabled,
21950
22524
  keyboard,
21951
22525
  affordanceAt: wrappedAffordanceAt,
@@ -22156,14 +22730,25 @@ var inferredNodeProperties = inferredNodeRouting.map((e) => ({
22156
22730
 
22157
22731
  // src/canvas/sceneViewRender.ts
22158
22732
  var RENDERER_CACHE = /* @__PURE__ */ new WeakMap();
22159
- function buildSceneViewCommands(scene, view, drawOne, extraCommands) {
22160
- const children = [];
22161
- for (const id of scene.renderOrder()) {
22162
- const node = scene.get(id);
22163
- if (!node) continue;
22164
- const cmds = drawOne(node, node.pose, view);
22165
- for (const cmd of cmds) children.push(cmd);
22166
- }
22733
+ function sceneAsHierarchy(scene) {
22734
+ return {
22735
+ getLayers: () => scene.layers.map((l) => ({ id: l.id, visible: l.visible })),
22736
+ getNode: (id) => scene.get(id),
22737
+ getChildren: (parentId) => {
22738
+ if (parentId === null) return scene.roots;
22739
+ const node = scene.get(parentId);
22740
+ return node && node.kind === "container" ? node.children : [];
22741
+ },
22742
+ getPose: (id) => scene.get(id).pose
22743
+ };
22744
+ }
22745
+ function buildSceneViewCommands(scene, view, drawOne, extraCommands, alphaFor) {
22746
+ const wrappedDrawOne = (node, pose, v) => wrapNodeOutput(drawOne(node, pose, v), pose, alphaFor ? alphaFor(node.id) : 1);
22747
+ const children = buildSceneTree(
22748
+ sceneAsHierarchy(scene),
22749
+ wrappedDrawOne,
22750
+ view
22751
+ );
22167
22752
  if (extraCommands && extraCommands.length > 0) {
22168
22753
  for (const cmd of extraCommands) children.push(cmd);
22169
22754
  }
@@ -22174,7 +22759,7 @@ function buildSceneViewCommands(scene, view, drawOne, extraCommands) {
22174
22759
  }];
22175
22760
  }
22176
22761
  function renderSceneToCanvas(args) {
22177
- const { canvas, scene, view, width, height, drawOne, extraCommands } = args;
22762
+ const { canvas, scene, view, width, height, drawOne, extraCommands, alphaFor } = args;
22178
22763
  const dpr = args.dpr ?? (typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1);
22179
22764
  let entry = RENDERER_CACHE.get(canvas);
22180
22765
  if (!entry) {
@@ -22202,11 +22787,11 @@ function renderSceneToCanvas(args) {
22202
22787
  entry.height = height;
22203
22788
  entry.dpr = dpr;
22204
22789
  }
22205
- const commands = buildSceneViewCommands(scene, view, drawOne, extraCommands);
22206
- entry.renderer.render(commands);
22790
+ const commands = buildSceneViewCommands(scene, view, drawOne, extraCommands, alphaFor);
22791
+ entry.renderer.render(commands, viewToMat3(view));
22207
22792
  }
22208
22793
  function SceneViewCanvasInner(props) {
22209
- const { scene, view, width, height, drawOne, extraCommands, className, canvasRef } = props;
22794
+ const { scene, view, width, height, drawOne, extraCommands, alphaFor, className, canvasRef } = props;
22210
22795
  useSyncExternalStore(scene.subscribe, scene.getVersion, scene.getVersion);
22211
22796
  const localRef = useRef(null);
22212
22797
  const setCanvasRef = (el) => {
@@ -22227,7 +22812,8 @@ function SceneViewCanvasInner(props) {
22227
22812
  width,
22228
22813
  height,
22229
22814
  drawOne,
22230
- extraCommands
22815
+ extraCommands,
22816
+ alphaFor
22231
22817
  });
22232
22818
  });
22233
22819
  return /* @__PURE__ */ jsx(
@@ -22326,6 +22912,7 @@ function MinimapCanvasInner(props) {
22326
22912
  width,
22327
22913
  height,
22328
22914
  drawOne,
22915
+ alphaFor,
22329
22916
  fit = "scene",
22330
22917
  poseBounds,
22331
22918
  indicatorStyle,
@@ -22437,6 +23024,7 @@ function MinimapCanvasInner(props) {
22437
23024
  height,
22438
23025
  drawOne,
22439
23026
  extraCommands,
23027
+ alphaFor,
22440
23028
  className,
22441
23029
  canvasRef: setCanvasRef
22442
23030
  }
@@ -22478,7 +23066,7 @@ function planPixelRender(args) {
22478
23066
  fill: { fill: "solid", color: args.background }
22479
23067
  });
22480
23068
  }
22481
- commands.push(...buildSceneViewCommands(args.scene, view, drawOne));
23069
+ commands.push(...buildSceneViewCommands(args.scene, view, drawOne, void 0, args.alphaFor));
22482
23070
  return { width, height, view, commands };
22483
23071
  }
22484
23072
  function defaultCreateCanvas(width, height) {
@@ -22543,7 +23131,7 @@ function renderSceneToPixels(args) {
22543
23131
  bakeBudget: Infinity
22544
23132
  });
22545
23133
  try {
22546
- renderer.render(plan.commands);
23134
+ renderer.render(plan.commands, viewToMat3(plan.view));
22547
23135
  if (typeof gl.isContextLost === "function" && gl.isContextLost()) {
22548
23136
  throw new Error("renderSceneToPixels: WebGL2 context was lost during render");
22549
23137
  }
@@ -22830,15 +23418,11 @@ function measureText(ctx, text, maxWidth, style) {
22830
23418
  function measureTextBounds(text, style, opts) {
22831
23419
  const resolved = resolveTextStyle(style);
22832
23420
  const runs = resolveRuns([{ text }], resolved);
22833
- const { bounds } = layoutRuns(
22834
- runs,
22835
- {
22836
- maxWidth: opts?.maxWidth ?? Infinity,
22837
- lineHeight: opts?.lineHeight ?? resolved.lineHeight,
22838
- align: resolved.align
22839
- },
22840
- { x: 0, y: 0 }
22841
- );
23421
+ const { bounds } = layoutRuns(runs, {
23422
+ maxWidth: opts?.maxWidth ?? Infinity,
23423
+ lineHeight: opts?.lineHeight ?? resolved.lineHeight,
23424
+ align: resolved.align
23425
+ });
22842
23426
  return bounds;
22843
23427
  }
22844
23428
 
@@ -23267,6 +23851,64 @@ function applyStyleToRange(runs, start, end, patch) {
23267
23851
  return coalesceRuns(out);
23268
23852
  }
23269
23853
 
23854
+ // src/features/text/runs/flagRange.ts
23855
+ function nodeHasFlag(style, key) {
23856
+ switch (key) {
23857
+ case "bold":
23858
+ return isBoldWeight(style.fontWeight);
23859
+ case "italic":
23860
+ return style.fontStyle === "italic";
23861
+ case "underline":
23862
+ return style.underline === true;
23863
+ case "strikethrough":
23864
+ return style.strikethrough === true;
23865
+ }
23866
+ }
23867
+ function isBoldWeight(w) {
23868
+ if (w === void 0) return false;
23869
+ if (typeof w === "number") return w >= 600;
23870
+ return w === "bold" || w === "bolder";
23871
+ }
23872
+ function weightIsExpressibleAsRunBold(w) {
23873
+ return w === 700 || w === "bold";
23874
+ }
23875
+ function clearNodeFlag(style, key) {
23876
+ const next = { ...style };
23877
+ switch (key) {
23878
+ case "bold":
23879
+ next.fontWeight = 400;
23880
+ break;
23881
+ case "italic":
23882
+ next.fontStyle = "normal";
23883
+ break;
23884
+ case "underline":
23885
+ delete next.underline;
23886
+ break;
23887
+ case "strikethrough":
23888
+ delete next.strikethrough;
23889
+ break;
23890
+ }
23891
+ return next;
23892
+ }
23893
+ function setFlagOverRange(runs, style, start, end, key, value) {
23894
+ if (value || !nodeHasFlag(style, key)) {
23895
+ return {
23896
+ runs: applyStyleToRange(runs, start, end, { [key]: value }),
23897
+ style,
23898
+ applied: true
23899
+ };
23900
+ }
23901
+ if (key === "bold" && !weightIsExpressibleAsRunBold(style.fontWeight)) {
23902
+ return { runs: [...runs], style, applied: false };
23903
+ }
23904
+ const total = runs.reduce((n, r) => n + r.text.length, 0);
23905
+ const lo = Math.max(0, start);
23906
+ const hi = Math.min(total, end);
23907
+ let next = applyStyleToRange(runs, 0, total, { [key]: true });
23908
+ if (lo < hi) next = applyStyleToRange(next, lo, hi, { [key]: false });
23909
+ return { runs: next, style: clearNodeFlag(style, key), applied: true };
23910
+ }
23911
+
23270
23912
  // src/features/text/useTextEdit.ts
23271
23913
  var FLAG_SHORTCUTS = [
23272
23914
  { key: "b", shift: false, flag: "bold" },
@@ -23428,7 +24070,21 @@ function useTextEdit(opts) {
23428
24070
  togglePending(flag);
23429
24071
  return;
23430
24072
  }
23431
- const next = toggleFlagInRange(domToRuns(overlay), range2.start, range2.end, flag);
24073
+ if (editingId === null) return;
24074
+ const runs = domToRuns(overlay);
24075
+ const nodeStyle = optsRef.current.getStyle(editingId) ?? {};
24076
+ const setStyle = optsRef.current.setStyle;
24077
+ const current = styleAtRange(runs, range2.start, range2.end)[flag];
24078
+ const unsetting = current === true;
24079
+ if (unsetting && setStyle && nodeHasFlag(nodeStyle, flag)) {
24080
+ const r = setFlagOverRange(runs, nodeStyle, range2.start, range2.end, flag, false);
24081
+ if (!r.applied) return;
24082
+ setStyle(editingId, r.style);
24083
+ writeRunsPreservingSelection(overlay, r.runs, range2.start, range2.end);
24084
+ syncSelection();
24085
+ return;
24086
+ }
24087
+ const next = toggleFlagInRange(runs, range2.start, range2.end, flag);
23432
24088
  writeRunsPreservingSelection(overlay, next, range2.start, range2.end);
23433
24089
  syncSelection();
23434
24090
  }
@@ -23679,10 +24335,9 @@ function useSceneTextEdit(scene, container, options = {}) {
23679
24335
  const cy = view ? canvasY / view.scale.y + view.y : canvasY;
23680
24336
  const readText = (data) => optsRef.current.getText ? optsRef.current.getText(data) : data.text ?? "";
23681
24337
  const readStyle = (data) => optsRef.current.getStyle ? optsRef.current.getStyle(data) : data.style;
23682
- const order = [...sceneRef.current.renderOrder()];
24338
+ const order = sceneRef.current.renderOrderNodes();
23683
24339
  for (let i = order.length - 1; i >= 0; i--) {
23684
- const node = sceneRef.current.get(order[i]);
23685
- if (!node) continue;
24340
+ const node = order[i];
23686
24341
  const text = readText(node.data);
23687
24342
  const pose = {
23688
24343
  x: node.pose.x,
@@ -23706,6 +24361,95 @@ function useSceneTextEdit(scene, container, options = {}) {
23706
24361
  return { ...edit, onDoubleClick };
23707
24362
  }
23708
24363
 
24364
+ // src/core/gradient.ts
24365
+ function sampleGradientStops(stops, t) {
24366
+ if (stops.length === 0) return "rgba(0,0,0,0)";
24367
+ const sorted = [...stops].sort((a, b) => a.offset - b.offset);
24368
+ if (sorted.length === 1) return sorted[0].color;
24369
+ const first = sorted[0];
24370
+ const last = sorted[sorted.length - 1];
24371
+ if (t <= first.offset) return first.color;
24372
+ if (t >= last.offset) return last.color;
24373
+ for (let i = 0; i < sorted.length - 1; i++) {
24374
+ const lo = sorted[i];
24375
+ const hi = sorted[i + 1];
24376
+ if (t < lo.offset || t > hi.offset) continue;
24377
+ const span = hi.offset - lo.offset;
24378
+ if (span <= 0) return hi.color;
24379
+ return lerpColor(lo.color, hi.color, (t - lo.offset) / span);
24380
+ }
24381
+ return last.color;
24382
+ }
24383
+ function lerpColor(a, b, t) {
24384
+ const [ar, ag, ab, aa] = hexToRgba(a);
24385
+ const [br, bg, bb, ba] = hexToRgba(b);
24386
+ return rgbaToHex([
24387
+ ar + (br - ar) * t,
24388
+ ag + (bg - ag) * t,
24389
+ ab + (bb - ab) * t,
24390
+ aa + (ba - aa) * t
24391
+ ]);
24392
+ }
24393
+ function withGradientKind(fill, kind) {
24394
+ if (fill.fill === kind) return fill;
24395
+ const common = { stops: fill.stops, units: fill.units, opacity: fill.opacity };
24396
+ const { center, radius, angle } = gradientGeometry(fill);
24397
+ if (kind === "linear-gradient") {
24398
+ const dx = Math.cos(angle) * radius;
24399
+ const dy = Math.sin(angle) * radius;
24400
+ return {
24401
+ fill: "linear-gradient",
24402
+ from: { x: center.x - dx, y: center.y - dy },
24403
+ to: { x: center.x + dx, y: center.y + dy },
24404
+ ...common
24405
+ };
24406
+ }
24407
+ if (kind === "radial-gradient") {
24408
+ return { fill: "radial-gradient", center, radius, ...common };
24409
+ }
24410
+ return { fill: "conic-gradient", center, angle, ...common };
24411
+ }
24412
+ function gradientGeometry(fill) {
24413
+ if (fill.fill === "linear-gradient") {
24414
+ const dx = fill.to.x - fill.from.x;
24415
+ const dy = fill.to.y - fill.from.y;
24416
+ return {
24417
+ center: { x: (fill.from.x + fill.to.x) / 2, y: (fill.from.y + fill.to.y) / 2 },
24418
+ radius: Math.hypot(dx, dy) / 2,
24419
+ angle: Math.atan2(dy, dx)
24420
+ };
24421
+ }
24422
+ if (fill.fill === "radial-gradient") {
24423
+ return { center: fill.center, radius: fill.radius, angle: 0 };
24424
+ }
24425
+ return { center: fill.center, radius: conicArmRadius(fill.units), angle: fill.angle };
24426
+ }
24427
+ function conicArmRadius(units) {
24428
+ return units === "bounds" ? 0.5 : 50;
24429
+ }
24430
+ function gradientForBounds(kind, bounds, stops, units) {
24431
+ const center = { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
24432
+ if (kind === "linear-gradient") {
24433
+ return {
24434
+ fill: "linear-gradient",
24435
+ from: { x: bounds.x, y: center.y },
24436
+ to: { x: bounds.x + bounds.width, y: center.y },
24437
+ stops,
24438
+ units
24439
+ };
24440
+ }
24441
+ if (kind === "radial-gradient") {
24442
+ return {
24443
+ fill: "radial-gradient",
24444
+ center,
24445
+ radius: Math.max(bounds.width, bounds.height) / 2,
24446
+ stops,
24447
+ units
24448
+ };
24449
+ }
24450
+ return { fill: "conic-gradient", center, angle: 0, stops, units };
24451
+ }
24452
+
23709
24453
  // src/core/ops/setText.ts
23710
24454
  function createSetTextOp(args) {
23711
24455
  const { id, from, to, label, coalesceKey = `setText:${id}` } = args;
@@ -25040,8 +25784,8 @@ function bestAxis(b, axis, anchors, candidates, worldTolerance) {
25040
25784
  return { delta: bestGuide ? bestDelta : 0, guide: bestGuide };
25041
25785
  }
25042
25786
  function matchAlignment(bounds, candidates, worldTolerance, anchors) {
25043
- const x = bestAxis(bounds, "x", anchors.x, candidates, worldTolerance);
25044
- const y = bestAxis(bounds, "y", anchors.y, candidates, worldTolerance);
25787
+ const x = bestAxis(bounds, "x", anchors.x, candidates, worldTolerance.x);
25788
+ const y = bestAxis(bounds, "y", anchors.y, candidates, worldTolerance.y);
25045
25789
  return { dx: x.delta, dy: y.delta, activeX: x.guide, activeY: y.guide };
25046
25790
  }
25047
25791
 
@@ -25049,7 +25793,7 @@ function matchAlignment(bounds, candidates, worldTolerance, anchors) {
25049
25793
  var activeList = (m) => [m.activeX, m.activeY].filter((g) => g !== null);
25050
25794
  function worldTol(base) {
25051
25795
  const t = base.tolerance ?? 6;
25052
- return base.getView ? t / Math.max(1e-9, meanScale(base.getView().scale)) : t;
25796
+ return base.getView ? pxExtent(t, base.getView().scale) : { x: t, y: t };
25053
25797
  }
25054
25798
  function alignMoveBehavior(args) {
25055
25799
  const proj = args.projection ?? RECT_ALIGN_PROJECTION;
@@ -27270,6 +28014,24 @@ function snapPoint(opts) {
27270
28014
  };
27271
28015
  }
27272
28016
 
27273
- export { ALWAYS, ANCHOR_HIT_BASE_PX, ActionDisabledReason, ActionsProvider, ActionsProviderIfRoot, ActiveToolContextProvider, ActiveToolContextProviderIfRoot, BUNDLE_TOOLS, COARSE_TARGET_SCALE, CURVE_REPS, ColorOverrideRegistry, CropIcon, CursorCoordsHud, DEFAULT_ALPHA_DECAY, DEFAULT_ALPHA_MIN, DEFAULT_DEBUG_THEME, DEFAULT_DEVICE_PROFILE, DEFAULT_FILL_COLOR, DEFAULT_HANDLE_SIZE2 as DEFAULT_HANDLE_SIZE, DEFAULT_PALETTE, DEFAULT_ROTATION_HANDLE_DISTANCE, DEFAULT_STROKE_COLOR, DEFAULT_TEXT_STYLE, DEFAULT_VELOCITY_DECAY, DepRegistryProvider, DepRegistryProviderIfRoot, DeviceProfileProvider, DivideIcon, EASINGS, EllipseIcon, ExcludeIcon, EyedropperIcon, FALLBACK_FIT_VIEW, GHOST_STROKE, HANDLE_BASE_PX, HandIcon, IDENTITY_COLOR_MATRIX, IDENTITY_POSE_COMPOSITION, IntersectIcon, KIT_SHAPE_KINDS, LassoIcon, LineIcon, MIXED, MOVE_ANCHORS, MinimapCanvas, NEVER, OUTLINE_MIN_SCREEN_PX, PATH_ANCHOR_CHROME_ID, PenIcon, PencilIcon, PickHud, PointerProviderIfRoot, PolygonIcon, RECT_ALIGN_PROJECTION, ROTATION_HANDLE_BASE_PX, RectIcon, SPRING_PRESETS, SceneCanvas, SceneViewCanvas, SelectIcon, SelectionContextProvider, SelectionContextProviderIfRoot, ShaderCompileError, StarIcon, SubtractIcon, TextIcon, UnionIcon, UnknownIcon, VERSION, WeaselProvider, WeaselRenderer, aabbCenter, actionIs, alignDeltaFor, alignInsertBehavior, alignMoveBehavior, alignResizeBehavior, alignedStrokeRect, always, and, animateLifecycle, animateOnSetPose, annulusSemiAxes, applyBooleanOp, applyHitExistingGate, applyStyleToRange, areaSelectAction, arrayAdapter, asNodeId, bezierCubic, bezierQuadratic, buildChromeCtx, buildGradientRamp, buildRuleCtx, buildSceneViewCommands, canBringForward, canHover, canSendBackward, capabilityAll, capabilityIn, capabilityIs, capabilityNot, caretIndexAt, cellAt, charOffsetToDomPosition, clampView, clearSelectionAction, clientToCanvas, clipboardCopyAction, clipboardCutAction, cloneAction, coarsePointer, composeAffordanceLayer, composePath, composeRectPose, composeSelectionPose, composeWorldPose, computeFitView2 as computeFitView, computeIndicatorCommand, computeWheelAction, cond, constrainTo45, containedThenNearest, countPathAnchors, createCellHighlightLayer, createChildrenLayer, createCornerResizeAffordance, createDebugOverlayLayer, createDebugSink, createDispatcher, createGridLayer, createGuidesLayer, createHistory2 as createHistory, createMarkdownRenderer, createMoveToIndexOp, createNodeProperties, createNodeRouting, createParallaxLayer, createPathAnchorAffordances, createPathEditingOverlayLayer, createPathLayer, createPenPreviewLayer, createReorderOp, createRotationAffordance, createScene, createSelectionHandlesLayer, createSelectionOutlineLayer, createSelectionOverlayLayer, createSetDataOp, createSetLayerOp, createSetPathOp, createSetTextOp, createTextLayer, createViewportLayer, cycleVertexColors, decomposePath, decomposeRectPose, defaultCommitAdapter, defaultDrawOne, defaultLabelTextRenderer, defaultNodeProperties, defaultNodeRouting, defaultVisibilityRules, deriveAlignmentGuides, deriveParallaxView, describeRule, domPositionToCharOffset, domToRuns, drawLayers, easeIn, easeInBack, easeInBounce, easeInCirc, easeInCubic, easeInElastic, easeInExpo, easeInOut, easeInOutBack, easeInOutBounce, easeInOutCirc, easeInOutCubic, easeInOutElastic, easeInOutExpo, easeInOutQuad, easeInOutQuart, easeInOutQuint, easeInOutSine, easeInQuad, easeInQuart, easeInQuint, easeInSine, easeOut, easeOutBack, easeOutBounce, easeOutCirc, easeOutCubic2 as easeOutCubic, easeOutElastic, easeOutExpo, easeOutQuad, easeOutQuart, easeOutQuint, easeOutSine, editAnchorsAction, enterTextEditAction, enumerateAnchors, evaluate, evaluateEnabled, findNodeShape, findShapeInk, findShapeSilhouette, fitTextPose, fitToBounds, fitViewToBounds, fitZoom, focused, fontString, forEachCoalesced, freeform, gateLayer, gesturing, getAlpha01, getImageBitmap, getNodeShapes, getStylusData, hexToRgba, hitAffordanceRegions, hitAnchor, hitRotationHandle, hovering, hoveringSelected, imageStatus, inferredNodeProperties, inferredNodeRouting, insertAction, isEditableTarget2 as isEditableTarget, lassoSelectAction, layoutMarkdown, lerpColorArray, lerpOklab, lerpOklch, linear, makeViewportZoomAction, markdownToRuns, mat3, matchAlignment, matchesKeyBinding, measureText, measureTextBounds, measuredWidth, mergeAlphaFromPrev, modeIn, modeIs, modeNot, modifierHeld, momentum, moveAction, multiActive, nearest, nearestWithin, nestedHitTester, never, none, normalizeHex, not, nurbs, oklabToOklch, oklabToSrgbU8, oklchToOklab, openFilePicker, or, parseColor, parseColorToRgba255, parseDebugFlags, pathContainsPoint, pathContainsPolygon, pathContainsRect, pathDistanceToPoint, pathDivide, pathExclude, pathFromD, pathInPoseFrame, pathInWorld, pathIntersect, pathIntersectsPolygon, pathIntersectsRect, pathOriginProjection, pathSubtract, pathToAnchors, pathUnion, pickTopMostHit, pinchZoomAction, planPixelRender, pointInRotatedRect, pointInTextPose, poseRotationOf, pressureToWidth, rainbowVertexColors, rebaseLocalPose, rectCorners, registerContentHandler, registerNodeShape, registerProgram, renderLabel, renderSceneToCanvas, renderSceneToPixels, resizeAction, resolveColor, resolveDeviceProfile, resolveRuns, resolveTextStyle, resolveVisibility, rgbaToHex, rotateAction, rotateAroundAABBCenter, rotatePathAround, rotatePoint, rotatedRectCorners, rotationDegreesUnit, rotationHandle, roundToCell, runsToDom, runsToMarkdown, runsToPlainText, sceneFromJSON, sceneNodeClientRect, sceneToAdapter, screenToWorld, selectFromLasso, selectionAtLeast, selectionEmpty, selectionIs, shapeCoversPoint, sliceAction, snapPoint, solidVertexColors, specificity, spiro, splitPathByLine, splitSubpaths, springPose, springVertexColors, srgbU8ToOklab, staggerVertexColors, startThresholdDrag, styleAtRange, subscribeImageReady, tessellate, tessellateStroke, textCommand, textLineBoxes, tileGrid, toHex8, toRuns, translatePoseViaDescriptor, translateRectPose, tweenPose, tweenVertexColors, unionBounds, unionBoundsPath, useAction, useActionsRegistry, useActiveToolContext, useAlign, useAnimator, useArrayAdapter, useAutoCenter, useBooleans, useBooleansAdapter, useCanvasFocus, useCanvasSize, useDecayLoop, useDepRegistry, useDepSource, useDeviceProfile, useDistribute, useDragGesture, useDragHandle, useDragRadial, useDragRect, useDropZone, useEllipseTool, useEyedropperTool, useGestureDispatcher, useGridCellHover, useGuides, useHandTool, useHandleDrag, useHoverTracking, useImageTool, useKeybindings, useLassoTool, useLineTool, useOptionalActiveToolContext, useOptionalDepRegistry, usePenTool, usePencilTool, usePinchGesture, usePinchZoomTool, usePointerStylus, usePolygonTool, usePublishSelection, useRectTool, useResizePolicy, useRotateTool, useScene, useSceneAdapter, useSceneTextEdit, useSelectTool, useSelection, useSelectionContext, useSimulation, useSliceDep, useStandardActions, useStarTool, useTextEdit, useTextTool, useTools, useVelocityTracker, useViewAnimation, useViewTween, useZoom, verticalAlignOffset, viewToMat3, viewToTransform, viewportDragPanAction, viewportZoomAction, when, withAlpha01, withCoord, worldEditToStorage, worldPoseLookup, worldToScreen, zoomAt, zoomAtLeast };
27274
- //# sourceMappingURL=chunk-DLV5F4NY.js.map
27275
- //# sourceMappingURL=chunk-DLV5F4NY.js.map
28017
+ // src/contributions/merge.ts
28018
+ function mergeContributions(...bundles) {
28019
+ const out = [];
28020
+ const seen = /* @__PURE__ */ new Set();
28021
+ for (const bundle of bundles) {
28022
+ for (const entry of bundle) {
28023
+ if (seen.has(entry.id)) {
28024
+ throw new Error(
28025
+ `mergeContributions: duplicate contribution id "${entry.id}". Two bundles registered the same id; rename one or merge them by hand.`
28026
+ );
28027
+ }
28028
+ seen.add(entry.id);
28029
+ out.push(entry);
28030
+ }
28031
+ }
28032
+ return out;
28033
+ }
28034
+
28035
+ export { ALWAYS, ANCHOR_HIT_BASE_PX, ActionsProviderIfRoot, ActiveToolContextProvider, ActiveToolContextProviderIfRoot, BUNDLE_TOOLS, COARSE_TARGET_SCALE, CURVE_REPS, ColorOverrideRegistry, CropIcon, CursorCoordsHud, DEFAULT_ALPHA_DECAY, DEFAULT_ALPHA_MIN, DEFAULT_DEBUG_THEME, DEFAULT_DEVICE_PROFILE, DEFAULT_FILL_COLOR, DEFAULT_HANDLE_SIZE2 as DEFAULT_HANDLE_SIZE, DEFAULT_PALETTE, DEFAULT_ROTATION_HANDLE_DISTANCE, DEFAULT_STROKE_COLOR, DEFAULT_TEXT_STYLE, DEFAULT_VELOCITY_DECAY, DepRegistryProviderIfRoot, DeviceProfileProvider, DivideIcon, EASINGS, EllipseIcon, ExcludeIcon, EyedropperIcon, FALLBACK_FIT_VIEW, GHOST_STROKE, HANDLE_BASE_PX, HandIcon, IDENTITY_COLOR_MATRIX, IDENTITY_POSE_COMPOSITION, IntersectIcon, KIT_SHAPE_KINDS, LassoIcon, LineIcon, MIXED, MOVE_ANCHORS, MinimapCanvas, NEVER, OUTLINE_MIN_SCREEN_PX, PATH_ANCHOR_CHROME_ID, PenIcon, PencilIcon, PickHud, PointerProviderIfRoot, PolygonIcon, RECT_ALIGN_PROJECTION, ROTATION_HANDLE_BASE_PX, RectIcon, SPRING_PRESETS, SceneCanvas, SceneViewCanvas, SelectIcon, SelectionContextProvider, SelectionContextProviderIfRoot, ShaderCompileError, StarIcon, SubtractIcon, TextIcon, UnionIcon, UnknownIcon, VERSION, WeaselProvider, WeaselRenderer, aabbCenter, actionIs, alignDeltaFor, alignInsertBehavior, alignMoveBehavior, alignResizeBehavior, alignedStrokeRect, always, and, animateLifecycle, animateOnSetPose, annulusSemiAxes, applyBooleanOp, applyHitExistingGate, applyStyleToRange, areaSelectAction, arrayAdapter, asNodeId, bezierCubic, bezierQuadratic, buildChromeCtx, buildGradientRamp, buildRuleCtx, buildSceneViewCommands, canBringForward, canHover, canSendBackward, capabilityAll, capabilityIn, capabilityIs, capabilityNot, caretIndexAt, cellAt, charOffsetToDomPosition, clampView, clearSelectionAction, clientToCanvas, clipboardCopyAction, clipboardCutAction, cloneAction, coarsePointer, composeAffordanceLayer, composePath, composeRectPose, composeSelectionPose, composeWorldPose, computeFitView2 as computeFitView, computeIndicatorCommand, computeWheelAction, cond, constrainTo45, containedThenNearest, countPathAnchors, createCellHighlightLayer, createChildrenLayer, createCornerResizeAffordance, createDebugOverlayLayer, createDebugSink, createDispatcher, createGridLayer, createGuidesLayer, createHistory2 as createHistory, createMarkdownRenderer, createMoveToIndexOp, createNodeProperties, createNodeRouting, createParallaxLayer, createPathAnchorAffordances, createPathEditingOverlayLayer, createPathLayer, createPenPreviewLayer, createReorderOp, createRotationAffordance, createScene, createSelectionHandlesLayer, createSelectionOutlineLayer, createSelectionOverlayLayer, createSetDataOp, createSetLayerOp, createSetPathOp, createSetTextOp, createTextLayer, createViewportLayer, cycleVertexColors, decomposePath, decomposeRectPose, defaultCommitAdapter, defaultDrawOne, defaultLabelTextRenderer, defaultNodeProperties, defaultNodeRouting, defaultVisibilityRules, deriveAlignmentGuides, deriveParallaxView, describeRule, domPositionToCharOffset, domToRuns, drawLayers, easeIn, easeInBack, easeInBounce, easeInCirc, easeInCubic, easeInElastic, easeInExpo, easeInOut, easeInOutBack, easeInOutBounce, easeInOutCirc, easeInOutCubic, easeInOutElastic, easeInOutExpo, easeInOutQuad, easeInOutQuart, easeInOutQuint, easeInOutSine, easeInQuad, easeInQuart, easeInQuint, easeInSine, easeOut, easeOutBack, easeOutBounce, easeOutCirc, easeOutCubic2 as easeOutCubic, easeOutElastic, easeOutExpo, easeOutQuad, easeOutQuart, easeOutQuint, easeOutSine, editAnchorsAction, enterTextEditAction, enumerateAnchors, evaluate, fillInPoseFrame, fillToBoundsFrame, findNodeShape, findShapeInk, findShapeSilhouette, fitTextPose, fitToBounds, fitViewToBounds, fitZoom, focused, fontString, forEachCoalesced, freeform, gateLayer, gesturing, getAlpha01, getImageBitmap, getNodeShapes, getStylusData, gradientForBounds, gradientGeometry, hexToRgba, hitAffordanceRegions, hitAnchor, hitRotationHandle, hovering, hoveringSelected, imageStatus, inferredNodeProperties, inferredNodeRouting, insertAction, isEditableTarget2 as isEditableTarget, isPatternSpec, lassoSelectAction, layoutMarkdown, lerpColorArray, lerpOklab, lerpOklch, linear, liveScope, makeViewportZoomAction, markdownToRuns, mat3, matchAlignment, matchesKeyBinding, meanScale, measureText, measureTextBounds, measuredWidth, mergeAlphaFromPrev, mergeContributions, modeIn, modeIs, modeNot, modifierHeld, momentum, moveAction, multiActive, nearest, nearestWithin, nestedHitTester, never, nodeHasFlag, none, normalizeHex, not, nurbs, oklabToOklch, oklabToSrgbU8, oklchToOklab, openFilePicker, or, parseColor, parseColorToRgba255, parseDebugFlags, pathContainsPoint, pathContainsPolygon, pathContainsRect, pathDistanceToPoint, pathDivide, pathExclude, pathFromD, pathInPoseFrame, pathInWorld, pathIntersect, pathIntersectsPolygon, pathIntersectsRect, pathOriginProjection, pathSubtract, pathToAnchors, pathUnion, pickTopMostHit, pinchZoomAction, planPixelRender, pointInRotatedRect, pointInTextPose, poseRotationOf, pressureToWidth, rainbowVertexColors, rebaseLocalPose, rectCorners, registerContentHandler, registerNodeShape, registerProgram, renderLabel, renderSceneToCanvas, renderSceneToPixels, resizeAction, resolveColor, resolveDeviceProfile, resolveFillPattern, resolvePatternSpec, resolveRuns, resolveTextStyle, resolveVisibility, rgbaToHex, rotateAction, rotateAroundAABBCenter, rotatePathAround, rotatePoint, rotatedRectCorners, rotationDegreesUnit, rotationHandle, roundToCell, runsToDom, runsToMarkdown, runsToPlainText, sampleGradientStops, sceneFromJSON, sceneNodeClientRect, sceneToAdapter, scopeBindings, screenToWorld, selectFromLasso, selectionAtLeast, selectionEmpty, selectionIs, setFlagOverRange, shapeCoversPoint, sliceAction, snapPoint, solidVertexColors, specificity, spiro, splitPathByLine, splitSubpaths, springPose, springVertexColors, srgbU8ToOklab, staggerVertexColors, startThresholdDrag, styleAtRange, subscribeImageReady, tessellate, tessellateStroke, textCommand, textLineBoxes, tileGrid, toHex8, toRuns, translatePoseViaDescriptor, translateRectPose, tweenPose, tweenVertexColors, unionBounds, unionBoundsPath, useActiveToolContext, useAlign, useAnimator, useArrayAdapter, useAutoCenter, useBooleans, useBooleansAdapter, useCanvasFocus, useCanvasSize, useDecayLoop, useDeviceProfile, useDistribute, useDragGesture, useDragHandle, useDragRadial, useDragRect, useDropZone, useEllipseTool, useEyedropperTool, useGestureDispatcher, useGridCellHover, useGuides, useHandTool, useHandleDrag, useHoverTracking, useImageTool, useKeybindings, useLassoTool, useLineTool, useOptionalActiveToolContext, usePenTool, usePencilTool, usePinchGesture, usePinchZoomTool, usePointerStylus, usePolygonTool, usePublishSelection, useRectTool, useResizePolicy, useRotateTool, useScene, useSceneAdapter, useSceneTextEdit, useSelectTool, useSelection, useSelectionContext, useSimulation, useSliceDep, useStandardActions, useStarTool, useTextEdit, useTextTool, useTools, useVelocityTracker, useViewAnimation, useViewTween, useZoom, verticalAlignOffset, viewToMat3, viewToTransform, viewportDragPanAction, viewportZoomAction, when, withAlpha01, withCoord, withGradientKind, worldEditToStorage, worldPoseLookup, worldToScreen, zoomAt, zoomAtLeast };
28036
+ //# sourceMappingURL=chunk-FVNWK7U3.js.map
28037
+ //# sourceMappingURL=chunk-FVNWK7U3.js.map