@weasel-js/core 0.7.2 → 1.0.0

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 (51) hide show
  1. package/CHANGELOG.md +541 -0
  2. package/dist/{DrawCommand-CJtqqt8H.d.ts → DrawCommand-DCmiNkBj.d.ts} +38 -170
  3. package/dist/{chunk-CQNKCG34.js → chunk-6SHZHXXR.js} +47 -10
  4. package/dist/chunk-6SHZHXXR.js.map +1 -0
  5. package/dist/{chunk-5F3PIERN.js → chunk-D2C6K6FO.js} +1420 -652
  6. package/dist/chunk-D2C6K6FO.js.map +1 -0
  7. package/dist/chunk-DCIU3SRK.js +505 -0
  8. package/dist/chunk-DCIU3SRK.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-VOVKONXA.js → chunk-UB6A6L77.js} +3 -3
  12. package/dist/{chunk-VOVKONXA.js.map → chunk-UB6A6L77.js.map} +1 -1
  13. package/dist/{chunk-BGGZ4CVF.js → chunk-Z3KNFTTS.js} +6 -6
  14. package/dist/chunk-Z3KNFTTS.js.map +1 -0
  15. package/dist/clipboard.d.ts +2 -2
  16. package/dist/clone.d.ts +1 -1
  17. package/dist/{geometry-D9BDMiQi.d.ts → geometry-DVeZ2i-c.d.ts} +1 -1
  18. package/dist/{grid-CaSK9bHV.d.ts → grid-XcFQVS0f.d.ts} +1 -1
  19. package/dist/{index-DOYRTfP0.d.ts → index-ChM3ZJ2v.d.ts} +278 -93
  20. package/dist/index.d.ts +442 -57
  21. package/dist/index.js +6 -6
  22. package/dist/insert.d.ts +2 -2
  23. package/dist/insert.js +5 -5
  24. package/dist/insert.js.map +1 -1
  25. package/dist/move.d.ts +3 -3
  26. package/dist/move.js +2 -2
  27. package/dist/{options-DMWeTELe.d.ts → options-wAvCnOZd.d.ts} +1 -1
  28. package/dist/paint-types-CnLIzqq1.d.ts +261 -0
  29. package/dist/patterns-builtin.d.ts +57 -2
  30. package/dist/patterns-builtin.js +1 -97
  31. package/dist/patterns-builtin.js.map +1 -1
  32. package/dist/{pointSnapToGrid-C3EruUwt.d.ts → pointSnapToGrid-BuDZWBrL.d.ts} +2 -2
  33. package/dist/renderer.d.ts +25 -8
  34. package/dist/renderer.js +6 -6
  35. package/dist/resize.d.ts +3 -3
  36. package/dist/resize.js +2 -2
  37. package/dist/routing.d.ts +7 -7
  38. package/dist/routing.js +1 -1
  39. package/dist/{types-Dcaa0tPq.d.ts → types-BhJJ2OCM.d.ts} +9 -1
  40. package/dist/{types-Cpb4hii1.d.ts → types-DUpI7Apc.d.ts} +3 -95
  41. package/dist/{viewToMat3-D4lrBigW.d.ts → viewToMat3-BvaWCN99.d.ts} +1 -1
  42. package/package.json +6 -6
  43. package/dist/chunk-5F3PIERN.js.map +0 -1
  44. package/dist/chunk-BGGZ4CVF.js.map +0 -1
  45. package/dist/chunk-CQNKCG34.js.map +0 -1
  46. package/dist/chunk-SYM6RAM4.js +0 -232
  47. package/dist/chunk-SYM6RAM4.js.map +0 -1
  48. package/dist/chunk-Y52N27PF.js +0 -39
  49. package/dist/chunk-Y52N27PF.js.map +0 -1
  50. package/dist/registerTexture-BzHTLhD9.d.ts +0 -25
  51. /package/dist/{types-B6MMiodD.d.ts → path-B6MMiodD.d.ts} +0 -0
@@ -1,17 +1,17 @@
1
- import { defineViewportTool, reportRouteConflicts, defineTool, RESERVED_ID_PREFIXES, RESERVED_ID_NAMES } from './chunk-SYM6RAM4.js';
2
- import { scratchKey, createTransformOp, snap, getScratch, createReparentOp, setScratch } from './chunk-VOVKONXA.js';
3
- import { fixedCornerOf, DEFAULT_RESIZE_BEHAVIORS, CORNER_ANCHORS, cornerPoint } from './chunk-BGGZ4CVF.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-CQNKCG34.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
16
  import { matchSpec, mimeMatchesGlob } from '@weasel-js/gestures';
17
17
  import { NORMAL, IMPLICIT_TAGS } from '@weasel-js/modes';
@@ -19,7 +19,7 @@ import { createHistory } from '@weasel-js/history';
19
19
  export * from '@weasel-js/history';
20
20
 
21
21
  // src/version.ts
22
- var VERSION = "0.7.2" ;
22
+ var VERSION = "1.0.0" ;
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
  };
@@ -1501,6 +1571,16 @@ function extractPolygon(p, opts) {
1501
1571
  out.push(next);
1502
1572
  return next;
1503
1573
  };
1574
+ const dropDuplicateClosingPoint = () => {
1575
+ if (!pts || !aA || !aB || !aT) return;
1576
+ const n = pts.length / 2;
1577
+ if (n < 2) return;
1578
+ if (pts[0] !== pts[(n - 1) * 2] || pts[1] !== pts[(n - 1) * 2 + 1]) return;
1579
+ pts.length -= 2;
1580
+ aA.length -= 1;
1581
+ aB.length -= 1;
1582
+ aT.length -= 1;
1583
+ };
1504
1584
  const commit = (target) => {
1505
1585
  if (!pts || !aA || !aB || !aT) return;
1506
1586
  target.anchorA = new Uint32Array(aA);
@@ -1584,7 +1664,10 @@ function extractPolygon(p, opts) {
1584
1664
  break;
1585
1665
  }
1586
1666
  case PATH_Z: {
1587
- if (current) current.closed = true;
1667
+ if (current) {
1668
+ current.closed = true;
1669
+ dropDuplicateClosingPoint();
1670
+ }
1588
1671
  break;
1589
1672
  }
1590
1673
  default:
@@ -2067,8 +2150,20 @@ function sameFill(a, b) {
2067
2150
  }
2068
2151
  return false;
2069
2152
  }
2070
- function groupKey(family, weight, style, synthetic, fill, source, page) {
2071
- return `${family}|${weight}|${style}|${synthetic.bold ? 1 : 0}${synthetic.italic ? 1 : 0}|${fillKey(fill)}|${source}|${page}`;
2153
+ function strokeKey(s) {
2154
+ if (!s) return "-";
2155
+ return [
2156
+ fillKey(s.paint),
2157
+ s.width ?? 1,
2158
+ s.join ?? "miter",
2159
+ s.cap ?? "butt",
2160
+ s.miterLimit ?? "",
2161
+ s.align ?? "center",
2162
+ (s.dash ?? []).join(",")
2163
+ ].join(":");
2164
+ }
2165
+ function groupKey(family, weight, style, synthetic, fill, stroke, source, page) {
2166
+ return `${family}|${weight}|${style}|${synthetic.bold ? 1 : 0}${synthetic.italic ? 1 : 0}|${fillKey(fill)}|${strokeKey(stroke)}|${source}|${page}`;
2072
2167
  }
2073
2168
  function getOrCreateGroup(ctx, run, resolved, page, sourceOverride) {
2074
2169
  const atlasFamily = resolved.resolved.family;
@@ -2081,6 +2176,7 @@ function getOrCreateGroup(ctx, run, resolved, page, sourceOverride) {
2081
2176
  resolvedStyle,
2082
2177
  resolved.synthetic,
2083
2178
  run.fill,
2179
+ source === "outline" ? run.stroke : void 0,
2084
2180
  source,
2085
2181
  page
2086
2182
  );
@@ -2094,6 +2190,9 @@ function getOrCreateGroup(ctx, run, resolved, page, sourceOverride) {
2094
2190
  source,
2095
2191
  page,
2096
2192
  fill: run.fill,
2193
+ // Only the outline tier can paint it, and carrying it on an SDF group
2194
+ // would be a promise the renderer cannot keep.
2195
+ ...source === "outline" && run.stroke !== void 0 ? { stroke: run.stroke } : {},
2097
2196
  quads: [],
2098
2197
  glyphs: []
2099
2198
  };
@@ -2698,6 +2797,36 @@ function outlineMesh(key, d) {
2698
2797
  return mesh;
2699
2798
  }
2700
2799
 
2800
+ // src/renderer/cache/outlineStrokeMeshCache.ts
2801
+ var OUTLINE_STROKE_MESH_CACHE_LIMIT = 2048;
2802
+ var WIDTH_QUANTUM = 1 / 1024;
2803
+ var cache4 = /* @__PURE__ */ new Map();
2804
+ function quantizeEmWidth(emWidth) {
2805
+ return Math.round(emWidth / WIDTH_QUANTUM) * WIDTH_QUANTUM;
2806
+ }
2807
+ function outlineStrokeMesh(glyphKey, d, emWidth, stroke) {
2808
+ if (!(emWidth > 0)) return null;
2809
+ const key = [
2810
+ glyphKey,
2811
+ emWidth,
2812
+ stroke.join ?? "miter",
2813
+ stroke.cap ?? "butt",
2814
+ stroke.miterLimit ?? "",
2815
+ stroke.align ?? "center",
2816
+ (stroke.dash ?? []).join(",")
2817
+ ].join("|");
2818
+ const cached = cache4.get(key);
2819
+ if (cached !== void 0) return cached.indices.length === 0 ? null : cached;
2820
+ if (cache4.size >= OUTLINE_STROKE_MESH_CACHE_LIMIT) cache4 = /* @__PURE__ */ new Map();
2821
+ const mesh = tessellateStroke(
2822
+ pathFromD(d),
2823
+ { ...stroke, width: emWidth },
2824
+ { flattenTolerance: OUTLINE_FLATTEN_TOLERANCE }
2825
+ );
2826
+ cache4.set(key, mesh);
2827
+ return mesh.indices.length === 0 ? null : mesh;
2828
+ }
2829
+
2701
2830
  // src/renderer/draw.ts
2702
2831
  function setColorMatrixUniforms(ctx, prog) {
2703
2832
  const gl = ctx.gl;
@@ -2989,28 +3118,42 @@ function drawPathFillPattern(ctx, fill, handle) {
2989
3118
  if (isDev3) console.warn(`weasel: pattern TextureHandle "${tex.id}" not registered`);
2990
3119
  return;
2991
3120
  }
2992
- ctx.textureCache.upload(tex.id, entry.source);
3121
+ ctx.textureCache.upload(tex.id, entry.source, "repeat");
2993
3122
  const gl = ctx.gl;
2994
- gl.useProgram(ctx.imageFill.handle);
3123
+ gl.useProgram(ctx.patternFill.handle);
2995
3124
  gl.bindVertexArray(handle.vao);
2996
- setProjAndModel(ctx, ctx.imageFill);
2997
- setColorMatrixUniforms(ctx, ctx.imageFill);
3125
+ setProjAndModel(ctx, ctx.patternFill);
3126
+ setColorMatrixUniforms(ctx, ctx.patternFill);
3127
+ gl.uniformMatrix3fv(ctx.patternFill.uniform("u_worldInv"), false, gradientSpaceInverse(ctx, fill.units));
3128
+ const [tw, th] = textureSize(entry.source);
3129
+ const origin = fill.origin ?? { x: 0, y: 0 };
3130
+ gl.uniform2f(ctx.patternFill.uniform("u_tileOrigin"), origin.x, origin.y);
3131
+ gl.uniform2f(ctx.patternFill.uniform("u_tileSize"), tw, th);
2998
3132
  ctx.textureCache.bind(tex.id, 0);
2999
- gl.uniform1i(ctx.imageFill.uniform("u_sampler"), 0);
3000
- gl.uniform1f(ctx.imageFill.uniform("u_opacity"), fill.opacity ?? 1);
3001
- gl.uniform1f(ctx.imageFill.uniform("u_alpha"), ctx.state.alpha);
3133
+ gl.uniform1i(ctx.patternFill.uniform("u_sampler"), 0);
3134
+ gl.uniform1f(ctx.patternFill.uniform("u_opacity"), fill.opacity ?? 1);
3135
+ gl.uniform1f(ctx.patternFill.uniform("u_alpha"), ctx.state.alpha);
3002
3136
  applyClipTest(ctx);
3003
3137
  gl.drawElements(gl.TRIANGLES, handle.indexCount, gl.UNSIGNED_INT, 0);
3004
3138
  gl.bindVertexArray(null);
3005
3139
  }
3140
+ function textureSize(source) {
3141
+ const w = "naturalWidth" in source ? source.naturalWidth : source.width;
3142
+ const h = "naturalHeight" in source ? source.naturalHeight : source.height;
3143
+ return [w || 1, h || 1];
3144
+ }
3145
+ function gradientSpaceInverse(ctx, units) {
3146
+ if (units === "local") return mat3.invert(ctx.state.transform);
3147
+ if (units === "world" && ctx.viewMatrix) return mat3.invert(ctx.viewMatrix);
3148
+ return mat3.identity();
3149
+ }
3006
3150
  function drawPathFillGradient(ctx, fill, handle) {
3007
3151
  const gl = ctx.gl;
3008
3152
  const key = ctx.gradRampCache.upload(fill.stops);
3009
3153
  gl.useProgram(ctx.gradFill.handle);
3010
3154
  gl.bindVertexArray(handle.vao);
3011
3155
  setProjAndModel(ctx, ctx.gradFill);
3012
- const identity3 = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
3013
- gl.uniformMatrix3fv(ctx.gradFill.uniform("u_worldInv"), false, identity3);
3156
+ gl.uniformMatrix3fv(ctx.gradFill.uniform("u_worldInv"), false, gradientSpaceInverse(ctx, fill.units));
3014
3157
  ctx.gradRampCache.bind(key, 0);
3015
3158
  gl.uniform1i(ctx.gradFill.uniform("u_ramp"), 0);
3016
3159
  gl.uniform1f(ctx.gradFill.uniform("u_alpha"), ctx.state.alpha);
@@ -3272,16 +3415,30 @@ function drawText(ctx, cmd) {
3272
3415
  var SYNTHETIC_ITALIC_RADIANS = 0.2094;
3273
3416
  function drawTextOutlineGroup(ctx, group, dy) {
3274
3417
  const mesh = outlineGroupMesh(group, dy);
3275
- if (!mesh) return;
3276
- drawPathFillByKind(ctx, group.fill, ctx.meshCache.uploadTransient(mesh));
3418
+ if (mesh) drawPathFillByKind(ctx, group.fill, ctx.meshCache.uploadTransient(mesh));
3419
+ if (!group.stroke) return;
3420
+ const strokeMesh = outlineGroupStrokeMesh(group, dy);
3421
+ if (strokeMesh) {
3422
+ drawPathFillByKind(ctx, group.stroke.paint, ctx.meshCache.uploadTransient(strokeMesh));
3423
+ }
3277
3424
  }
3278
3425
  function outlineGroupMesh(group, dy) {
3426
+ return mergeGlyphMeshes(group, dy, (glyph) => outlineMesh(glyph.key, glyph.d));
3427
+ }
3428
+ function outlineGroupStrokeMesh(group, dy) {
3429
+ const stroke = group.stroke;
3430
+ if (!stroke) return null;
3431
+ const width = stroke.width ?? 1;
3432
+ if (!(width > 0)) return null;
3433
+ return mergeGlyphMeshes(group, dy, (glyph) => glyph.scale > 0 ? outlineStrokeMesh(glyph.key, glyph.d, quantizeEmWidth(width / glyph.scale), stroke) : null);
3434
+ }
3435
+ function mergeGlyphMeshes(group, dy, meshFor) {
3279
3436
  const parts = [];
3280
3437
  let vertexFloats = 0;
3281
3438
  let indexCount = 0;
3282
3439
  for (const glyph of group.glyphs) {
3283
- const mesh = outlineMesh(glyph.key, glyph.d);
3284
- if (mesh.indices.length === 0) continue;
3440
+ const mesh = meshFor(glyph);
3441
+ if (!mesh || mesh.indices.length === 0) continue;
3285
3442
  parts.push({ mesh, glyph });
3286
3443
  vertexFloats += mesh.vertices.length;
3287
3444
  indexCount += mesh.indices.length;
@@ -3516,6 +3673,11 @@ function drawImage(ctx, cmd) {
3516
3673
  setProjAndModel(ctx, ctx.imageFill);
3517
3674
  setColorMatrixUniforms(ctx, ctx.imageFill);
3518
3675
  ctx.imageCache.bind(cmd.image, 0);
3676
+ gl.texParameteri(
3677
+ gl.TEXTURE_2D,
3678
+ gl.TEXTURE_MAG_FILTER,
3679
+ cmd.sampling === "nearest" ? gl.NEAREST : gl.LINEAR
3680
+ );
3519
3681
  gl.uniform1i(ctx.imageFill.uniform("u_sampler"), 0);
3520
3682
  gl.uniform1f(ctx.imageFill.uniform("u_opacity"), cmd.opacity ?? 1);
3521
3683
  gl.uniform1f(ctx.imageFill.uniform("u_alpha"), ctx.state.alpha);
@@ -3616,6 +3778,7 @@ var WeaselRenderer = class {
3616
3778
  textSdfR8;
3617
3779
  imageFill;
3618
3780
  gradFill;
3781
+ patternFill;
3619
3782
  meshCache;
3620
3783
  textureCache;
3621
3784
  imageCache;
@@ -3691,6 +3854,9 @@ var WeaselRenderer = class {
3691
3854
  this.gradFill = new ShaderProgram(this.gl, GRAD_VERT_SRC, GRAD_FRAG_SRC);
3692
3855
  this.gradFill.lookupUniforms(GRAD_FILL_UNIFORMS);
3693
3856
  this.gradFill.lookupAttributes(GRAD_FILL_ATTRIBUTES);
3857
+ this.patternFill = new ShaderProgram(this.gl, PATTERN_VERT_SRC, PATTERN_FRAG_SRC);
3858
+ this.patternFill.lookupUniforms(PATTERN_FILL_UNIFORMS);
3859
+ this.patternFill.lookupAttributes(PATTERN_FILL_ATTRIBUTES);
3694
3860
  const aPos = this.pathFill.attribute("a_position");
3695
3861
  if (aPos === void 0) throw new Error("WeaselRenderer: a_position not found in path-fill shader");
3696
3862
  this.meshCache = new GLMeshCache(this.gl, aPos);
@@ -3799,6 +3965,9 @@ var WeaselRenderer = class {
3799
3965
  this.gradFill = new ShaderProgram(this.gl, GRAD_VERT_SRC, GRAD_FRAG_SRC);
3800
3966
  this.gradFill.lookupUniforms(GRAD_FILL_UNIFORMS);
3801
3967
  this.gradFill.lookupAttributes(GRAD_FILL_ATTRIBUTES);
3968
+ this.patternFill = new ShaderProgram(this.gl, PATTERN_VERT_SRC, PATTERN_FRAG_SRC);
3969
+ this.patternFill.lookupUniforms(PATTERN_FILL_UNIFORMS);
3970
+ this.patternFill.lookupAttributes(PATTERN_FILL_ATTRIBUTES);
3802
3971
  const aPos = this.pathFill.attribute("a_position");
3803
3972
  if (aPos === void 0) throw new Error("a_position missing after restore");
3804
3973
  this.meshCache = new GLMeshCache(this.gl, aPos);
@@ -3871,7 +4040,15 @@ var WeaselRenderer = class {
3871
4040
  if (this.rectIbo) gl.deleteBuffer(this.rectIbo);
3872
4041
  if (this.rectVao) gl.deleteVertexArray(this.rectVao);
3873
4042
  }
3874
- render(commands) {
4043
+ /**
4044
+ * Draw one frame.
4045
+ *
4046
+ * `viewMatrix` is the frame's world→screen transform. It is only consulted
4047
+ * by `units: 'world'` gradients, which fall back to screen space without
4048
+ * it — every other command carries its own transform in the stream, so
4049
+ * callers with no view concept can keep calling `render(commands)`.
4050
+ */
4051
+ render(commands, viewMatrix) {
3875
4052
  if (this.contextLost || this.disposed) return;
3876
4053
  const gl = this.gl;
3877
4054
  this.meshCache.drainPendingDeletes();
@@ -3886,6 +4063,7 @@ var WeaselRenderer = class {
3886
4063
  textSdfR8: this.textSdfR8,
3887
4064
  imageFill: this.imageFill,
3888
4065
  gradFill: this.gradFill,
4066
+ patternFill: this.patternFill,
3889
4067
  meshCache: this.meshCache,
3890
4068
  textureCache: this.textureCache,
3891
4069
  imageCache: this.imageCache,
@@ -3900,7 +4078,8 @@ var WeaselRenderer = class {
3900
4078
  heightCss: this.heightCss,
3901
4079
  clipDepth: 0,
3902
4080
  flattenTolerance: this.flattenTolerance,
3903
- textOutlineMinScreenSize: this.textOutlineMinScreenSize
4081
+ textOutlineMinScreenSize: this.textOutlineMinScreenSize,
4082
+ viewMatrix
3904
4083
  };
3905
4084
  for (const cmd of commands) dispatch(ctx, cmd);
3906
4085
  this.meshCache.freeTransient();
@@ -3948,6 +4127,10 @@ var WeaselRenderer = class {
3948
4127
  return this.gradFill;
3949
4128
  }
3950
4129
  /** @internal */
4130
+ _patternFill() {
4131
+ return this.patternFill;
4132
+ }
4133
+ /** @internal */
3951
4134
  _meshCache() {
3952
4135
  return this.meshCache;
3953
4136
  }
@@ -4013,6 +4196,11 @@ function alignedStrokeRect(rect, align, width = 1) {
4013
4196
  };
4014
4197
  }
4015
4198
 
4199
+ // src/core/viewport/meanScale.ts
4200
+ function meanScale(s) {
4201
+ return Math.sqrt(s.x * s.y);
4202
+ }
4203
+
4016
4204
  // src/features/grid/layer.ts
4017
4205
  var DEFAULT_LINE = {
4018
4206
  paint: { fill: "solid", color: "rgba(255, 255, 255, 0.08)" },
@@ -4731,16 +4919,115 @@ function startThresholdDrag(e, opts) {
4731
4919
  document.addEventListener("pointercancel", onCancel);
4732
4920
  return { isDragging: () => activated };
4733
4921
  }
4922
+
4923
+ // src/core/device/profile.ts
4924
+ var COARSE_TARGET_SCALE = 1.75;
4925
+ function resolveDeviceProfile(detected, overrides) {
4926
+ const coarsePointer2 = overrides?.coarsePointer ?? detected.coarsePointer;
4927
+ const canHover2 = overrides?.canHover ?? detected.canHover;
4928
+ const dpr = overrides?.dpr ?? detected.dpr;
4929
+ const targetScale = overrides?.targetScale ?? (coarsePointer2 ? COARSE_TARGET_SCALE : 1);
4930
+ return { coarsePointer: coarsePointer2, canHover: canHover2, dpr, targetScale };
4931
+ }
4932
+ var DEFAULT_DEVICE_PROFILE = resolveDeviceProfile({
4933
+ coarsePointer: false,
4934
+ canHover: true,
4935
+ dpr: 1
4936
+ });
4937
+
4938
+ // src/core/device/useDeviceProfile.ts
4939
+ var COARSE_QUERY = "(pointer: coarse)";
4940
+ var HOVER_QUERY = "(hover: hover)";
4941
+ function hasMatchMedia() {
4942
+ return typeof window !== "undefined" && typeof window.matchMedia === "function";
4943
+ }
4944
+ function detectOnce() {
4945
+ const dpr = typeof window !== "undefined" && window.devicePixelRatio || 1;
4946
+ if (!hasMatchMedia()) {
4947
+ return {
4948
+ coarsePointer: DEFAULT_DEVICE_PROFILE.coarsePointer,
4949
+ canHover: DEFAULT_DEVICE_PROFILE.canHover,
4950
+ dpr
4951
+ };
4952
+ }
4953
+ return {
4954
+ coarsePointer: window.matchMedia(COARSE_QUERY).matches,
4955
+ canHover: window.matchMedia(HOVER_QUERY).matches,
4956
+ dpr
4957
+ };
4958
+ }
4959
+ function useDetectedFacts() {
4960
+ const [facts, setFacts] = useState(detectOnce);
4961
+ useEffect(() => {
4962
+ if (!hasMatchMedia()) return;
4963
+ const coarseMq = window.matchMedia(COARSE_QUERY);
4964
+ const hoverMq = window.matchMedia(HOVER_QUERY);
4965
+ const onCoarse = (e) => setFacts((f) => ({ ...f, coarsePointer: e.matches }));
4966
+ const onHover = (e) => setFacts((f) => ({ ...f, canHover: e.matches }));
4967
+ coarseMq.addEventListener("change", onCoarse);
4968
+ hoverMq.addEventListener("change", onHover);
4969
+ let disposed = false;
4970
+ let densityMq = null;
4971
+ const onDensity = () => {
4972
+ if (disposed) return;
4973
+ const next = window.devicePixelRatio || 1;
4974
+ setFacts((f) => f.dpr === next ? f : { ...f, dpr: next });
4975
+ arm();
4976
+ };
4977
+ function arm() {
4978
+ if (disposed) return;
4979
+ densityMq?.removeEventListener("change", onDensity);
4980
+ densityMq = window.matchMedia(
4981
+ `(resolution: ${window.devicePixelRatio || 1}dppx)`
4982
+ );
4983
+ densityMq.addEventListener("change", onDensity);
4984
+ }
4985
+ arm();
4986
+ setFacts({
4987
+ coarsePointer: coarseMq.matches,
4988
+ canHover: hoverMq.matches,
4989
+ dpr: window.devicePixelRatio || 1
4990
+ });
4991
+ return () => {
4992
+ disposed = true;
4993
+ coarseMq.removeEventListener("change", onCoarse);
4994
+ hoverMq.removeEventListener("change", onHover);
4995
+ densityMq?.removeEventListener("change", onDensity);
4996
+ };
4997
+ }, []);
4998
+ return facts;
4999
+ }
5000
+ var DeviceProfileContext = createContext(null);
5001
+ function useDeviceProfile(overrides) {
5002
+ const provided = useContext(DeviceProfileContext);
5003
+ const detected = useDetectedFacts();
5004
+ const base = provided ?? detected;
5005
+ const { coarsePointer: coarsePointer2, canHover: canHover2, dpr, targetScale } = overrides ?? {};
5006
+ return useMemo(
5007
+ () => resolveDeviceProfile(base, {
5008
+ ...coarsePointer2 !== void 0 ? { coarsePointer: coarsePointer2 } : {},
5009
+ ...canHover2 !== void 0 ? { canHover: canHover2 } : {},
5010
+ ...dpr !== void 0 ? { dpr } : {},
5011
+ ...targetScale !== void 0 ? { targetScale } : {}
5012
+ }),
5013
+ [base.coarsePointer, base.canHover, base.dpr, coarsePointer2, canHover2, dpr, targetScale]
5014
+ );
5015
+ }
5016
+ function DeviceProfileProvider({ value, children }) {
5017
+ const profile = useDeviceProfile(value);
5018
+ return createElement(DeviceProfileContext.Provider, { value: profile }, children);
5019
+ }
5020
+
5021
+ // src/core/viewport/useCanvasSize.ts
4734
5022
  function useCanvasSize(containerRef) {
4735
- const [size, setSize] = useState({ width: 0, height: 0, dpr: 1 });
5023
+ const { dpr } = useDeviceProfile();
5024
+ const [size, setSize] = useState({ width: 0, height: 0 });
4736
5025
  const measure3 = useCallback(() => {
4737
5026
  if (!containerRef.current) return;
4738
5027
  const rect = containerRef.current.getBoundingClientRect();
4739
- setSize({
4740
- width: rect.width,
4741
- height: rect.height,
4742
- dpr: window.devicePixelRatio || 1
4743
- });
5028
+ setSize(
5029
+ (prev) => prev.width === rect.width && prev.height === rect.height ? prev : { width: rect.width, height: rect.height }
5030
+ );
4744
5031
  }, [containerRef]);
4745
5032
  useEffect(() => {
4746
5033
  measure3();
@@ -4750,9 +5037,14 @@ function useCanvasSize(containerRef) {
4750
5037
  }
4751
5038
  return () => observer.disconnect();
4752
5039
  }, [measure3, containerRef]);
4753
- return size;
5040
+ return { width: size.width, height: size.height, dpr };
4754
5041
  }
4755
5042
 
5043
+ // src/core/device/targets.ts
5044
+ var HANDLE_BASE_PX = 8;
5045
+ var ANCHOR_HIT_BASE_PX = 8;
5046
+ var ROTATION_HANDLE_BASE_PX = 24;
5047
+
4756
5048
  // src/core/viewport/fitToBounds.ts
4757
5049
  function fitZoom(availW, availH, contentW, contentH, clamp2) {
4758
5050
  const raw = Math.min(availW / contentW, availH / contentH);
@@ -5047,254 +5339,6 @@ function matchesKeyBinding(e, b) {
5047
5339
  }
5048
5340
  return true;
5049
5341
  }
5050
- var DepRegistryContext = createContext(null);
5051
- function DepRegistryProvider({ children }) {
5052
- const sourcesRef = useRef(/* @__PURE__ */ new Map());
5053
- const registry2 = useMemo(() => ({
5054
- register: (name, source) => {
5055
- sourcesRef.current.set(name, source);
5056
- return () => {
5057
- sourcesRef.current.delete(name);
5058
- };
5059
- },
5060
- get: (name) => sourcesRef.current.get(name)?.()
5061
- }), []);
5062
- return /* @__PURE__ */ jsx(DepRegistryContext.Provider, { value: registry2, children });
5063
- }
5064
- function useDepRegistry() {
5065
- const r = useContext(DepRegistryContext);
5066
- if (r === null) {
5067
- throw new Error("useDepRegistry: no DepRegistryProvider in scope. Wrap your tree with <DepRegistryProvider> (typically inside <SceneCanvas>).");
5068
- }
5069
- return r;
5070
- }
5071
- function useOptionalDepRegistry() {
5072
- return useContext(DepRegistryContext);
5073
- }
5074
- function useDepSource(name, source) {
5075
- const registry2 = useDepRegistry();
5076
- const sourceRef = useRef(source);
5077
- sourceRef.current = source;
5078
- useEffect(() => {
5079
- return registry2.register(name, () => sourceRef.current());
5080
- }, [name, registry2]);
5081
- }
5082
-
5083
- // src/interactions/actions/buildDeps.ts
5084
- var DEV = (() => {
5085
- try {
5086
- return Boolean(import.meta.env?.DEV);
5087
- } catch {
5088
- return false;
5089
- }
5090
- })();
5091
- function buildDepsFromRequires(action, depRegistry) {
5092
- const requires = action.requires ?? [];
5093
- const entries = requires.map((name) => [name, depRegistry.get(name)]);
5094
- const deps = Object.fromEntries(entries);
5095
- if (DEV) {
5096
- const declared = new Set(requires);
5097
- return new Proxy(deps, {
5098
- get(target, prop, receiver) {
5099
- if (typeof prop === "string" && !declared.has(prop) && prop !== "then") {
5100
- 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.`;
5101
- console.warn(message);
5102
- throw new Error(message);
5103
- }
5104
- return Reflect.get(target, prop, receiver);
5105
- }
5106
- });
5107
- }
5108
- return deps;
5109
- }
5110
- var ActionDisabledReason = {
5111
- SelectionRequired: "selection-required",
5112
- SceneEmpty: "scene-empty",
5113
- NotApplicable: "not-applicable",
5114
- /** Sentinel: the predicate threw. Surfaced by `evaluateEnabled`'s catch. */
5115
- PredicateThrew: "predicate-threw"
5116
- };
5117
- var enabledSlowWarned = /* @__PURE__ */ new Set();
5118
- var enabledThrewWarned = /* @__PURE__ */ new Set();
5119
- var ENABLED_BUDGET_MS = 4;
5120
- function evaluateEnabled(action, deps) {
5121
- if (!action.enabled) return { enabled: true };
5122
- const isDev3 = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
5123
- const now = typeof performance !== "undefined" && typeof performance.now === "function" ? () => performance.now() : () => Date.now();
5124
- const start = isDev3 ? now() : 0;
5125
- try {
5126
- const result = action.enabled(deps);
5127
- if (isDev3) {
5128
- const elapsed = now() - start;
5129
- if (elapsed > ENABLED_BUDGET_MS && !enabledSlowWarned.has(action.id)) {
5130
- enabledSlowWarned.add(action.id);
5131
- console.warn(
5132
- `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.`
5133
- );
5134
- }
5135
- }
5136
- if (result === true) return { enabled: true };
5137
- return { enabled: false, reason: result };
5138
- } catch (e) {
5139
- if (isDev3 && !enabledThrewWarned.has(action.id)) {
5140
- enabledThrewWarned.add(action.id);
5141
- console.warn(
5142
- `weasel actions: enabled() for action "${action.id}" threw; treating as disabled. The predicate must not throw \u2014 see Action.enabled JSDoc.`,
5143
- e
5144
- );
5145
- }
5146
- return { enabled: false, reason: ActionDisabledReason.PredicateThrew };
5147
- }
5148
- }
5149
- function validateActionId(id) {
5150
- if (id.length === 0) {
5151
- throw new Error(`weasel: action id may not be empty`);
5152
- }
5153
- if (RESERVED_ID_PREFIXES.has(id[0])) {
5154
- throw new Error(
5155
- `weasel: action id "${id}" starts with reserved sigil "${id[0]}" (reserved set: ${[...RESERVED_ID_PREFIXES].join(" ")})`
5156
- );
5157
- }
5158
- if (RESERVED_ID_NAMES.has(id)) {
5159
- throw new Error(
5160
- `weasel: action id "${id}" collides with a reserved phase keyword (reserved: ${[...RESERVED_ID_NAMES].join(", ")})`
5161
- );
5162
- }
5163
- }
5164
- function validateActionDefaultBinding(action) {
5165
- const gs = action.defaultBinding;
5166
- if (!gs) return;
5167
- const entries = Array.isArray(gs) ? gs : [gs];
5168
- for (const entry of entries) {
5169
- const spec = "kind" in entry ? entry : entry.spec;
5170
- const phase = spec.phase;
5171
- if (phase === void 0) continue;
5172
- const atoms = Array.isArray(phase) ? phase : [{ channel: "&", phase }];
5173
- for (const atom of atoms) {
5174
- if (atom.channel === "&") {
5175
- throw new Error(
5176
- `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.`
5177
- );
5178
- }
5179
- }
5180
- }
5181
- }
5182
- var ActionsContext = createContext(null);
5183
- function ActionsProvider({ children }) {
5184
- const actionsRef = useRef(/* @__PURE__ */ new Map());
5185
- const versionRef = useRef(0);
5186
- const cachedRef = useRef([]);
5187
- const cachedVerRef = useRef(-1);
5188
- const listenersRef = useRef(/* @__PURE__ */ new Set());
5189
- const depReg = useOptionalDepRegistry();
5190
- const depRegRef = useRef(depReg);
5191
- depRegRef.current = depReg;
5192
- const wiredDepRegRef = useRef(null);
5193
- const dispatcherRef = useRef(null);
5194
- const registry2 = useMemo(() => {
5195
- const snapshot = () => {
5196
- const v = versionRef.current;
5197
- if (cachedVerRef.current === v) return cachedRef.current;
5198
- const out = Object.freeze(Array.from(actionsRef.current.values()));
5199
- cachedRef.current = out;
5200
- cachedVerRef.current = v;
5201
- return out;
5202
- };
5203
- const notify2 = () => {
5204
- for (const l of listenersRef.current) {
5205
- try {
5206
- l();
5207
- } catch (err) {
5208
- console.error("weasel ActionsRegistry: subscriber threw", err);
5209
- }
5210
- }
5211
- };
5212
- return {
5213
- register: (action) => {
5214
- validateActionId(action.id);
5215
- validateActionDefaultBinding(action);
5216
- actionsRef.current.set(action.id, action);
5217
- versionRef.current++;
5218
- notify2();
5219
- return () => {
5220
- const cur = actionsRef.current.get(action.id);
5221
- if (cur === action) {
5222
- actionsRef.current.delete(action.id);
5223
- versionRef.current++;
5224
- notify2();
5225
- }
5226
- };
5227
- },
5228
- unregister: (id) => {
5229
- if (actionsRef.current.delete(id)) {
5230
- versionRef.current++;
5231
- notify2();
5232
- }
5233
- },
5234
- list: () => snapshot(),
5235
- trigger: (id, params) => {
5236
- const a = actionsRef.current.get(id);
5237
- if (!a) return false;
5238
- try {
5239
- if (a.invoker && a.invoker.timing === "immediate") {
5240
- const r = wiredDepRegRef.current ?? depRegRef.current;
5241
- const deps = !r ? {} : a.requires ? buildDepsFromRequires(a, r) : {
5242
- selection: r.get("selection"),
5243
- scene: r.get("scene"),
5244
- history: r.get("history"),
5245
- view: r.get("view"),
5246
- pointer: r.get("pointer"),
5247
- activeTool: r.get("activeTool"),
5248
- booleansAdapter: r.get("booleansAdapter")
5249
- };
5250
- a.invoker.run(deps, params);
5251
- }
5252
- } catch (err) {
5253
- console.error(`weasel ActionsRegistry: action "${id}" threw`, err);
5254
- }
5255
- return true;
5256
- },
5257
- subscribe: (listener) => {
5258
- listenersRef.current.add(listener);
5259
- return () => {
5260
- listenersRef.current.delete(listener);
5261
- };
5262
- },
5263
- setDispatcher: (d) => {
5264
- dispatcherRef.current = d;
5265
- },
5266
- setDepRegistry: (r) => {
5267
- wiredDepRegRef.current = r;
5268
- },
5269
- begin: (id, params) => {
5270
- const disp = dispatcherRef.current;
5271
- if (!disp) return null;
5272
- const r = wiredDepRegRef.current ?? depRegRef.current;
5273
- const deps = r ? {
5274
- selection: r.get("selection"),
5275
- scene: r.get("scene"),
5276
- history: r.get("history"),
5277
- view: r.get("view"),
5278
- pointer: r.get("pointer"),
5279
- activeTool: r.get("activeTool"),
5280
- booleansAdapter: r.get("booleansAdapter")
5281
- } : {};
5282
- return disp.beginUiOngoing(id, deps, params);
5283
- }
5284
- };
5285
- }, []);
5286
- return /* @__PURE__ */ jsx(ActionsContext.Provider, { value: registry2, children });
5287
- }
5288
- function useActionsRegistry() {
5289
- return useContext(ActionsContext);
5290
- }
5291
- function useAction(action) {
5292
- const reg = useActionsRegistry();
5293
- useEffect(() => {
5294
- if (!reg) return;
5295
- return reg.register(action);
5296
- }, [reg, action]);
5297
- }
5298
5342
 
5299
5343
  // src/interactions/actions/invoker.ts
5300
5344
  function resolveParams(params) {
@@ -5867,6 +5911,62 @@ var moveAction = {
5867
5911
  enabled: () => true
5868
5912
  };
5869
5913
 
5914
+ // src/features/paths/poseRotation.ts
5915
+ function poseRotationOf(pose) {
5916
+ const p = pose;
5917
+ if (p.rotation && p.x != null && p.y != null && p.width != null && p.height != null) {
5918
+ return { cx: p.x + p.width / 2, cy: p.y + p.height / 2, rotation: p.rotation };
5919
+ }
5920
+ return null;
5921
+ }
5922
+ function rotatePathAround(path, cx, cy, rotation) {
5923
+ if (path.kind === "rect") {
5924
+ return rotateRectCorners(path.x, path.y, path.width, path.height, cx, cy, rotation);
5925
+ }
5926
+ return rotatePolygon(path, cx, cy, rotation);
5927
+ }
5928
+ function rotatePolygon(path, cx, cy, rotation) {
5929
+ const cos = Math.cos(rotation);
5930
+ const sin = Math.sin(rotation);
5931
+ const { commands, coords } = path;
5932
+ const next = new Float32Array(coords.length);
5933
+ let ci = 0;
5934
+ for (let i = 0; i < commands.length; i++) {
5935
+ const len = COORD_COUNT[commands[i]];
5936
+ for (let k = 0; k < len; k += 2) {
5937
+ const dx = coords[ci + k] - cx;
5938
+ const dy = coords[ci + k + 1] - cy;
5939
+ next[ci + k] = cx + dx * cos - dy * sin;
5940
+ next[ci + k + 1] = cy + dx * sin + dy * cos;
5941
+ }
5942
+ ci += len;
5943
+ }
5944
+ return { kind: "polygon", commands: path.commands, coords: next, fillRule: path.fillRule };
5945
+ }
5946
+ function rotateRectCorners(x, y, w, h, cx, cy, rotation) {
5947
+ const cos = Math.cos(rotation);
5948
+ const sin = Math.sin(rotation);
5949
+ const rotate = (px, py) => {
5950
+ const dx = px - cx;
5951
+ const dy = py - cy;
5952
+ return [cx + dx * cos - dy * sin, cy + dx * sin + dy * cos];
5953
+ };
5954
+ const [x0, y0] = rotate(x, y);
5955
+ const [x1, y1] = rotate(x + w, y);
5956
+ const [x2, y2] = rotate(x + w, y + h);
5957
+ const [x3, y3] = rotate(x, y + h);
5958
+ const commands = new Uint8Array([PATH_M, PATH_L, PATH_L, PATH_L, PATH_Z]);
5959
+ const coords = new Float32Array([x0, y0, x1, y1, x2, y2, x3, y3]);
5960
+ return { kind: "polygon", commands, coords, fillRule: "nonzero" };
5961
+ }
5962
+ var COORD_COUNT = {
5963
+ [PATH_M]: 2,
5964
+ [PATH_L]: 2,
5965
+ [PATH_C]: 6,
5966
+ [PATH_Q]: 4,
5967
+ [PATH_Z]: 0
5968
+ };
5969
+
5870
5970
  // src/interactions/actions/rotate/geometry.ts
5871
5971
  function aabbCenter(bounds) {
5872
5972
  return { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
@@ -6183,6 +6283,13 @@ var resizeAction = {
6183
6283
  const computePose = (id) => {
6184
6284
  const sp = scratch.startPoses.get(id);
6185
6285
  if (sp === void 0) return void 0;
6286
+ if (isGroupPath && poseRotationOf(sp) !== null) {
6287
+ return remapRotatedLeaf(
6288
+ sp,
6289
+ scratch.originBounds,
6290
+ proposedBounds
6291
+ );
6292
+ }
6186
6293
  return scratch.geometry.remapBounds(sp, scratch.originBounds, proposedBounds);
6187
6294
  };
6188
6295
  scratch.previews.clear();
@@ -6812,6 +6919,12 @@ var clearSelectionAction = {
6812
6919
  }
6813
6920
  };
6814
6921
 
6922
+ // src/interactions/actions/defaults/freshNodeId.ts
6923
+ var counter = 0;
6924
+ function freshNodeId() {
6925
+ return asNodeId(`n${(counter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`);
6926
+ }
6927
+
6815
6928
  // src/interactions/actions/defaults/clone.ts
6816
6929
  function translatePose(pose, dx, dy) {
6817
6930
  const p = pose;
@@ -6821,10 +6934,6 @@ function translatePose(pose, dx, dy) {
6821
6934
  y: (p["y"] ?? 0) + dy
6822
6935
  };
6823
6936
  }
6824
- var cloneIdCounter = 0;
6825
- function freshCloneId() {
6826
- return asNodeId(`n${(cloneIdCounter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`);
6827
- }
6828
6937
  var cloneAction = {
6829
6938
  id: "clone",
6830
6939
  label: "Clone",
@@ -6891,7 +7000,7 @@ var cloneAction = {
6891
7000
  if (origin === void 0 || !originNode) continue;
6892
7001
  const newPose = translatePose(origin, dx, dy);
6893
7002
  const node = {
6894
- id: freshCloneId(),
7003
+ id: freshNodeId(),
6895
7004
  kind: originNode.kind,
6896
7005
  layer: originNode.layer,
6897
7006
  pose: newPose,
@@ -7038,7 +7147,13 @@ function makeViewportZoomAction(opts = {}) {
7038
7147
  var viewportZoomAction = makeViewportZoomAction();
7039
7148
 
7040
7149
  // src/interactions/dispatcher/predicates.ts
7041
- var isBody = (_target, bodyTarget) => bodyTarget === "selected-body" || bodyTarget === "unselected-body";
7150
+ function bodyPredicate(fn) {
7151
+ return Object.assign(fn, { readsAffordance: false });
7152
+ }
7153
+ var isBody = bodyPredicate((_target, bodyTarget) => bodyTarget === "selected-body" || bodyTarget === "unselected-body");
7154
+ bodyPredicate((_target, bodyTarget) => bodyTarget === "selected-body");
7155
+ bodyPredicate((_target, bodyTarget) => bodyTarget === "unselected-body");
7156
+ bodyPredicate((_target, bodyTarget) => bodyTarget === "empty");
7042
7157
  function kindOf(target) {
7043
7158
  if (typeof target !== "object" || target === null) return void 0;
7044
7159
  const k = target.kind;
@@ -7079,17 +7194,17 @@ function splitCubicAtT(p0, p1, p2, p3, t) {
7079
7194
  function fitCubicThroughDeletion(prev, next) {
7080
7195
  const dx = next.x - prev.x;
7081
7196
  const dy = next.y - prev.y;
7082
- const dist4 = Math.sqrt(dx * dx + dy * dy);
7197
+ const dist3 = Math.sqrt(dx * dx + dy * dy);
7083
7198
  const c1DirX = prev.outHandle ? prev.outHandle.x - prev.x : dx;
7084
7199
  const c1DirY = prev.outHandle ? prev.outHandle.y - prev.y : dy;
7085
- const c1 = controlOnSide(prev, c1DirX, c1DirY, dist4, 1 / 3);
7200
+ const c1 = controlOnSide(prev, c1DirX, c1DirY, dist3, 1 / 3);
7086
7201
  const c2DirX = next.inHandle ? next.inHandle.x - next.x : -dx;
7087
7202
  const c2DirY = next.inHandle ? next.inHandle.y - next.y : -dy;
7088
- const c2 = controlOnSide(next, c2DirX, c2DirY, dist4, 1 / 3);
7203
+ const c2 = controlOnSide(next, c2DirX, c2DirY, dist3, 1 / 3);
7089
7204
  return { c1, c2 };
7090
7205
  }
7091
- function controlOnSide(anchor, dirX, dirY, dist4, fraction) {
7092
- const targetDist = dist4 * fraction;
7206
+ function controlOnSide(anchor, dirX, dirY, dist3, fraction) {
7207
+ const targetDist = dist3 * fraction;
7093
7208
  const len = Math.hypot(dirX, dirY) || 1;
7094
7209
  const ux = dirX / len, uy = dirY / len;
7095
7210
  return { x: anchor.x + ux * targetDist, y: anchor.y + uy * targetDist };
@@ -7360,64 +7475,6 @@ function anchorsInRect(set, rect) {
7360
7475
  }
7361
7476
  return hits;
7362
7477
  }
7363
-
7364
- // src/features/paths/poseRotation.ts
7365
- function poseRotationOf(pose) {
7366
- const p = pose;
7367
- if (p.rotation && p.x != null && p.y != null && p.width != null && p.height != null) {
7368
- return { cx: p.x + p.width / 2, cy: p.y + p.height / 2, rotation: p.rotation };
7369
- }
7370
- return null;
7371
- }
7372
- function rotatePathAround(path, cx, cy, rotation) {
7373
- if (path.kind === "rect") {
7374
- return rotateRectCorners(path.x, path.y, path.width, path.height, cx, cy, rotation);
7375
- }
7376
- return rotatePolygon(path, cx, cy, rotation);
7377
- }
7378
- function rotatePolygon(path, cx, cy, rotation) {
7379
- const cos = Math.cos(rotation);
7380
- const sin = Math.sin(rotation);
7381
- const { commands, coords } = path;
7382
- const next = new Float32Array(coords.length);
7383
- let ci = 0;
7384
- for (let i = 0; i < commands.length; i++) {
7385
- const len = COORD_COUNT[commands[i]];
7386
- for (let k = 0; k < len; k += 2) {
7387
- const dx = coords[ci + k] - cx;
7388
- const dy = coords[ci + k + 1] - cy;
7389
- next[ci + k] = cx + dx * cos - dy * sin;
7390
- next[ci + k + 1] = cy + dx * sin + dy * cos;
7391
- }
7392
- ci += len;
7393
- }
7394
- return { kind: "polygon", commands: path.commands, coords: next, fillRule: path.fillRule };
7395
- }
7396
- function rotateRectCorners(x, y, w, h, cx, cy, rotation) {
7397
- const cos = Math.cos(rotation);
7398
- const sin = Math.sin(rotation);
7399
- const rotate = (px, py) => {
7400
- const dx = px - cx;
7401
- const dy = py - cy;
7402
- return [cx + dx * cos - dy * sin, cy + dx * sin + dy * cos];
7403
- };
7404
- const [x0, y0] = rotate(x, y);
7405
- const [x1, y1] = rotate(x + w, y);
7406
- const [x2, y2] = rotate(x + w, y + h);
7407
- const [x3, y3] = rotate(x, y + h);
7408
- const commands = new Uint8Array([PATH_M, PATH_L, PATH_L, PATH_L, PATH_Z]);
7409
- const coords = new Float32Array([x0, y0, x1, y1, x2, y2, x3, y3]);
7410
- return { kind: "polygon", commands, coords, fillRule: "nonzero" };
7411
- }
7412
- var COORD_COUNT = {
7413
- [PATH_M]: 2,
7414
- [PATH_L]: 2,
7415
- [PATH_C]: 6,
7416
- [PATH_Q]: 4,
7417
- [PATH_Z]: 0
7418
- };
7419
-
7420
- // src/features/paths/pathInWorld.ts
7421
7478
  function pathInPoseFrame(path, pose) {
7422
7479
  if (path.kind === "rect") {
7423
7480
  if (path.x === pose.x && path.y === pose.y && path.width === pose.width && path.height === pose.height) return path;
@@ -7754,6 +7811,125 @@ var pinchZoomAction = {
7754
7811
  enabled: () => true
7755
7812
  };
7756
7813
 
7814
+ // src/interactions/actions/defaults/delete.ts
7815
+ function hostIndex(scene, id) {
7816
+ const node = scene.get(id);
7817
+ const siblings = node?.parent == null ? scene.roots : scene.childrenOf(node.parent);
7818
+ return siblings.indexOf(id);
7819
+ }
7820
+ function hasSelectedAncestor(scene, id, set) {
7821
+ let parent = scene.get(id)?.parent ?? null;
7822
+ while (parent != null) {
7823
+ if (set.has(parent)) return true;
7824
+ parent = scene.get(parent)?.parent ?? null;
7825
+ }
7826
+ return false;
7827
+ }
7828
+ function buildDeleteOps(scene, ids, label) {
7829
+ const set = new Set(ids);
7830
+ const ops = [];
7831
+ for (const id of ids) {
7832
+ const node = scene.get(id);
7833
+ if (!node) continue;
7834
+ if (hasSelectedAncestor(scene, id, set)) continue;
7835
+ ops.push(createDeleteOp({
7836
+ node,
7837
+ index: hostIndex(scene, id),
7838
+ label
7839
+ }));
7840
+ }
7841
+ return ops;
7842
+ }
7843
+ var deleteAction = {
7844
+ id: "delete",
7845
+ label: "Delete",
7846
+ // Suppressed while any tool is mid-gesture — accidentally hitting
7847
+ // Delete during a drag shouldn't wipe the selection out from under
7848
+ // the in-flight handle.
7849
+ defaultBinding: {
7850
+ kind: "key",
7851
+ key: ["Delete", "Backspace"],
7852
+ phase: [{ channel: "*", phase: "initial" }]
7853
+ },
7854
+ eligible: { capability: "edits-page" },
7855
+ requires: ["scene", "selection", "applyOps"],
7856
+ invoker: {
7857
+ timing: "immediate",
7858
+ run: (deps) => {
7859
+ const selection = deps.selection;
7860
+ const scene = deps.scene;
7861
+ const applyOps = deps.applyOps;
7862
+ if (!selection || !scene) return;
7863
+ const ids = selection.get();
7864
+ if (ids.length === 0) return;
7865
+ const ops = buildDeleteOps(scene, ids, "Delete");
7866
+ if (ops.length > 0) {
7867
+ if (applyOps) applyOps(ops, "Delete");
7868
+ else scene.applyBatch(ops, "Delete", defaultCommitAdapter(scene));
7869
+ }
7870
+ selection.set([]);
7871
+ }
7872
+ },
7873
+ enabled: () => true
7874
+ };
7875
+
7876
+ // src/interactions/actions/defaults/requiresSelection.ts
7877
+ function requiresSelection(deps) {
7878
+ const selection = deps?.selection;
7879
+ if (!selection || selection.get().length === 0) {
7880
+ return ActionDisabledReason.SelectionRequired;
7881
+ }
7882
+ return true;
7883
+ }
7884
+
7885
+ // src/interactions/actions/defaults/clipboard.ts
7886
+ var clipboardCopyAction = {
7887
+ id: "clipboard.copy",
7888
+ label: "Copy",
7889
+ defaultBinding: { kind: "key", key: "c", mods: { mod: true } },
7890
+ // Same gate as `duplicate`: a mode that can neither edit the page nor own a
7891
+ // selection has nothing to copy. Notably excludes `text-edit`, where Cmd+C
7892
+ // must stay the browser's own copy of the caret range.
7893
+ eligible: { capability: ["edits-page", "creates-selection"] },
7894
+ // `selection` is read by the `enabled` gate, not the invoker — the
7895
+ // dispatcher builds deps for both, and an undeclared read is undefined.
7896
+ requires: ["clipboard", "selection"],
7897
+ invoker: {
7898
+ timing: "immediate",
7899
+ run: (deps) => {
7900
+ deps.clipboard?.copy();
7901
+ }
7902
+ },
7903
+ enabled: requiresSelection
7904
+ };
7905
+ var clipboardCutAction = {
7906
+ id: "clipboard.cut",
7907
+ label: "Cut",
7908
+ defaultBinding: { kind: "key", key: "x", mods: { mod: true } },
7909
+ eligible: { capability: "edits-page" },
7910
+ requires: ["clipboard", "scene", "selection", "applyOps"],
7911
+ invoker: {
7912
+ timing: "immediate",
7913
+ run: (deps) => {
7914
+ const clipboard = deps.clipboard;
7915
+ const selection = deps.selection;
7916
+ const scene = deps.scene;
7917
+ const applyOps = deps.applyOps;
7918
+ if (!clipboard || !selection || !scene) return;
7919
+ const ids = selection.get();
7920
+ if (ids.length === 0) return;
7921
+ clipboard.copy();
7922
+ const ops = buildDeleteOps(scene, ids, "Cut");
7923
+ if (ops.length > 0) {
7924
+ if (applyOps) applyOps(ops, "Cut");
7925
+ else scene.applyBatch(ops, "Cut", defaultCommitAdapter(scene));
7926
+ }
7927
+ selection.set([]);
7928
+ }
7929
+ },
7930
+ enabled: requiresSelection
7931
+ };
7932
+
7757
7933
  // src/interactions/actions/defaults/enterTextEdit.ts
7758
7934
  var enterTextEditAction = {
7759
7935
  id: "enterTextEdit",
@@ -7886,25 +8062,46 @@ var selectAllAction = {
7886
8062
  }
7887
8063
  };
7888
8064
 
7889
- // src/interactions/actions/defaults/delete.ts
7890
- function hostIndex(scene, id) {
8065
+ // src/interactions/actions/defaults/duplicate.ts
8066
+ var DUPLICATE_OFFSET = 12;
8067
+ function translatePose2(pose, dx, dy) {
8068
+ const p = pose;
8069
+ return { ...p, x: (p["x"] ?? 0) + dx, y: (p["y"] ?? 0) + dy };
8070
+ }
8071
+ function hasSelectedAncestor2(scene, id, set) {
8072
+ let parent = scene.get(id)?.parent ?? null;
8073
+ while (parent != null) {
8074
+ if (set.has(parent)) return true;
8075
+ parent = scene.get(parent)?.parent ?? null;
8076
+ }
8077
+ return false;
8078
+ }
8079
+ function copySubtree(scene, id, parent, offset, offsetDescendants, ops) {
7891
8080
  const node = scene.get(id);
7892
- const siblings = node?.parent == null ? scene.roots : scene.childrenOf(node.parent);
7893
- return siblings.indexOf(id);
8081
+ if (!node) return null;
8082
+ const newId = freshNodeId();
8083
+ const copy = {
8084
+ ...node,
8085
+ id: newId,
8086
+ pose: translatePose2(node.pose, offset.dx, offset.dy),
8087
+ parent
8088
+ };
8089
+ ops.push(createInsertOp({ node: copy, label: "Duplicate" }));
8090
+ const childOffset = offsetDescendants ? offset : { dx: 0, dy: 0 };
8091
+ for (const child of scene.childrenOf(id)) {
8092
+ copySubtree(scene, child, newId, childOffset, offsetDescendants, ops);
8093
+ }
8094
+ return newId;
7894
8095
  }
7895
- var deleteAction = {
7896
- id: "delete",
7897
- label: "Delete",
7898
- // Suppressed while any tool is mid-gesture accidentally hitting
7899
- // Delete during a drag shouldn't wipe the selection out from under
7900
- // the in-flight handle.
7901
- defaultBinding: {
7902
- kind: "key",
7903
- key: ["Delete", "Backspace"],
7904
- phase: [{ channel: "*", phase: "initial" }]
7905
- },
7906
- eligible: { capability: "edits-page" },
7907
- requires: ["scene", "selection", "applyOps"],
8096
+ var duplicateAction = {
8097
+ id: "duplicate",
8098
+ label: "Duplicate",
8099
+ defaultBinding: { kind: "key", key: "d", mods: { mod: true } },
8100
+ eligible: { capability: ["edits-page", "creates-selection"] },
8101
+ // `selection` is read by the `enabled` gate as well as the invoker. An
8102
+ // undeclared read throws inside the dev-build deps Proxy before the gate can
8103
+ // answer, which is how this action spent a while doing nothing at all.
8104
+ requires: ["scene", "selection", "applyOps", "poseComposition"],
7908
8105
  invoker: {
7909
8106
  timing: "immediate",
7910
8107
  run: (deps) => {
@@ -7914,44 +8111,22 @@ var deleteAction = {
7914
8111
  if (!selection || !scene) return;
7915
8112
  const ids = selection.get();
7916
8113
  if (ids.length === 0) return;
8114
+ const set = new Set(ids);
8115
+ const offset = { dx: DUPLICATE_OFFSET, dy: DUPLICATE_OFFSET };
7917
8116
  const ops = [];
8117
+ const newIds = [];
7918
8118
  for (const id of ids) {
7919
- const node = scene.get(id);
8119
+ const nid = asNodeId(id);
8120
+ const node = scene.get(nid);
7920
8121
  if (!node) continue;
7921
- ops.push(createDeleteOp({
7922
- node,
7923
- index: hostIndex(scene, id),
7924
- label: "Delete"
7925
- }));
8122
+ if (hasSelectedAncestor2(scene, nid, set)) continue;
8123
+ const newId = copySubtree(scene, nid, node.parent ?? null, offset, deps.poseComposition === void 0, ops);
8124
+ if (newId) newIds.push(newId);
7926
8125
  }
7927
- if (ops.length > 0) {
7928
- if (applyOps) applyOps(ops, "Delete");
7929
- else scene.applyBatch(ops, "Delete", defaultCommitAdapter(scene));
7930
- }
7931
- selection.set([]);
7932
- }
7933
- },
7934
- enabled: () => true
7935
- };
7936
-
7937
- // src/interactions/actions/defaults/requiresSelection.ts
7938
- function requiresSelection(deps) {
7939
- const selection = deps?.selection;
7940
- if (!selection || selection.get().length === 0) {
7941
- return ActionDisabledReason.SelectionRequired;
7942
- }
7943
- return true;
7944
- }
7945
-
7946
- // src/interactions/actions/defaults/duplicate.ts
7947
- var duplicateAction = {
7948
- id: "duplicate",
7949
- label: "Duplicate",
7950
- defaultBinding: { kind: "key", key: "d", mods: { mod: true } },
7951
- eligible: { capability: ["edits-page", "creates-selection"] },
7952
- invoker: {
7953
- timing: "immediate",
7954
- run: (_deps, params) => {
8126
+ if (ops.length === 0) return;
8127
+ if (applyOps) applyOps(ops, "Duplicate");
8128
+ else scene.applyBatch(ops, "Duplicate", defaultCommitAdapter(scene));
8129
+ selection.set(newIds);
7955
8130
  }
7956
8131
  },
7957
8132
  enabled: requiresSelection
@@ -8984,7 +9159,7 @@ var LABEL = {
8984
9159
  divide: "Divide",
8985
9160
  crop: "Crop"
8986
9161
  };
8987
- function isEmpty(p) {
9162
+ function isEmpty2(p) {
8988
9163
  return p.commands.length === 0;
8989
9164
  }
8990
9165
  function applyBooleanOp(adapter, op) {
@@ -9022,7 +9197,7 @@ function applyBooleanOp(adapter, op) {
9022
9197
  break;
9023
9198
  }
9024
9199
  }
9025
- results = results.filter((p) => !isEmpty(p));
9200
+ results = results.filter((p) => !isEmpty2(p));
9026
9201
  if (results.length === 0) return { kind: "noop", reason: "empty-result" };
9027
9202
  const topmostId = entries[entries.length - 1].id;
9028
9203
  const topAnchor = adapter.getZOrder?.(topmostId);
@@ -9573,14 +9748,24 @@ var DEFAULT_PALETTE = [
9573
9748
  var GHOST_STROKE = DEFAULT_PALETTE[0];
9574
9749
 
9575
9750
  // src/interactions/actions/defaults/setFill.ts
9751
+ function resolveFill(scratch, prev) {
9752
+ if (scratch.currentPaint) return scratch.currentPaint;
9753
+ const prevColor = typeof prev?.fill === "string" ? prev.fill : DEFAULT_FILL_COLOR;
9754
+ return mergeAlphaFromPrev(scratch.currentColor, prevColor);
9755
+ }
9576
9756
  function refreshPreviews(scratch) {
9577
9757
  scratch.previews.clear();
9578
9758
  for (const id of scratch.ids) {
9579
9759
  const prev = scratch.startData.get(id);
9580
- const next = mergeAlphaFromPrev(scratch.currentColor, prev?.fill ?? DEFAULT_FILL_COLOR);
9581
- scratch.previews.set(id, { ...prev ?? {}, fill: next });
9760
+ scratch.previews.set(id, { ...prev ?? {}, fill: resolveFill(scratch, prev) });
9582
9761
  }
9583
9762
  }
9763
+ function readParams(params) {
9764
+ return {
9765
+ color: params?.color,
9766
+ paint: params?.paint
9767
+ };
9768
+ }
9584
9769
  var setFillAction = {
9585
9770
  id: "setFill",
9586
9771
  label: "Set fill",
@@ -9595,9 +9780,10 @@ var setFillAction = {
9595
9780
  if (!selection || !scene) return {};
9596
9781
  const ids = selection.get();
9597
9782
  if (ids.length === 0) return {};
9598
- const ctxColor = ctx.params?.color;
9599
- const optsColor = opts?.params?.color;
9600
- const initialColor = ctxColor ?? optsColor ?? DEFAULT_STROKE_COLOR;
9783
+ const fromCtx = readParams(ctx.params);
9784
+ const fromOpts = readParams(opts?.params);
9785
+ const initialPaint = fromCtx.paint ?? fromOpts.paint;
9786
+ const initialColor = fromCtx.color ?? fromOpts.color ?? DEFAULT_STROKE_COLOR;
9601
9787
  const startData = /* @__PURE__ */ new Map();
9602
9788
  for (const id of ids) {
9603
9789
  const node = scene.get(id);
@@ -9608,15 +9794,20 @@ var setFillAction = {
9608
9794
  scene,
9609
9795
  startData,
9610
9796
  currentColor: initialColor,
9797
+ currentPaint: initialPaint,
9611
9798
  previews: /* @__PURE__ */ new Map(),
9612
9799
  applyOps
9613
9800
  };
9614
9801
  refreshPreviews(scratch);
9615
9802
  return {
9616
9803
  onMove(moveCtx) {
9617
- const next = moveCtx.params?.color;
9618
- if (next === void 0) return;
9619
- scratch.currentColor = next;
9804
+ const { color, paint } = readParams(moveCtx.params);
9805
+ if (color === void 0 && paint === void 0) return;
9806
+ if (paint !== void 0) scratch.currentPaint = paint;
9807
+ if (color !== void 0) {
9808
+ scratch.currentColor = color;
9809
+ if (paint === void 0) scratch.currentPaint = void 0;
9810
+ }
9620
9811
  refreshPreviews(scratch);
9621
9812
  },
9622
9813
  onEnd(_endCtx, reason) {
@@ -9626,8 +9817,7 @@ var setFillAction = {
9626
9817
  }
9627
9818
  const ops = [];
9628
9819
  for (const id of scratch.ids) {
9629
- const prev = scratch.startData.get(id);
9630
- const merged = mergeAlphaFromPrev(scratch.currentColor, prev?.fill ?? DEFAULT_FILL_COLOR);
9820
+ const merged = resolveFill(scratch, scratch.startData.get(id));
9631
9821
  const nodeNow = scratch.scene.get(id);
9632
9822
  if (!nodeNow) continue;
9633
9823
  const from = { ...nodeNow.data };
@@ -10021,7 +10211,8 @@ function resolveTextStyle(style) {
10021
10211
  selectionColor: style.selectionColor ?? null,
10022
10212
  letterSpacing: style.letterSpacing ?? DEFAULT_TEXT_STYLE.letterSpacing,
10023
10213
  underline: style.underline ?? DEFAULT_TEXT_STYLE.underline,
10024
- strikethrough: style.strikethrough ?? DEFAULT_TEXT_STYLE.strikethrough
10214
+ strikethrough: style.strikethrough ?? DEFAULT_TEXT_STYLE.strikethrough,
10215
+ ...style.stroke !== void 0 ? { stroke: style.stroke } : {}
10025
10216
  };
10026
10217
  }
10027
10218
  function fontString(s) {
@@ -10047,6 +10238,9 @@ function resolveRuns(runs, style) {
10047
10238
  fontWeight: run.bold ? 700 : baseWeight,
10048
10239
  fontStyle: run.italic ? "italic" : style.fontStyle,
10049
10240
  fill: run.fill ?? style.fill,
10241
+ // Unlike the decorations below, a run's stroke *replaces* the node's
10242
+ // rather than adding to it — there is only one outline to paint.
10243
+ ...(run.stroke ?? style.stroke) !== void 0 ? { stroke: run.stroke ?? style.stroke } : {},
10050
10244
  letterSpacing: run.letterSpacing ?? style.letterSpacing,
10051
10245
  // `||`, not `??`: run-level decorations are additive over the node style.
10052
10246
  underline: run.underline || style.underline,
@@ -10435,7 +10629,9 @@ var KIT_STANDARD_DESCRIPTORS = [
10435
10629
  setStrokeAction,
10436
10630
  setFillOpacityAction,
10437
10631
  setStrokeOpacityAction,
10438
- ingestAction
10632
+ ingestAction,
10633
+ clipboardCopyAction,
10634
+ clipboardCutAction
10439
10635
  ];
10440
10636
  function useStandardActions(opts) {
10441
10637
  const optsRef = useRef(opts);
@@ -10515,6 +10711,54 @@ function targetRank(target) {
10515
10711
  if (target.startsWith("affordance:")) return 2;
10516
10712
  return 1;
10517
10713
  }
10714
+ function targetConsultsAffordance(specTarget) {
10715
+ if (specTarget === void 0) return false;
10716
+ if (typeof specTarget === "object" && specTarget !== null && "kindOf" in specTarget) {
10717
+ const kindOf2 = specTarget.kindOf;
10718
+ return kindOf2?.readsAffordance !== false;
10719
+ }
10720
+ return typeof specTarget === "string" && specTarget.startsWith("affordance:");
10721
+ }
10722
+ function specTargetOf(spec) {
10723
+ return "target" in spec ? spec.target : void 0;
10724
+ }
10725
+ function claimOf(e) {
10726
+ return "affordance" in e ? e.affordance : void 0;
10727
+ }
10728
+ function claimGestureOf(e) {
10729
+ switch (e.kind) {
10730
+ case "pointerdown":
10731
+ case "click":
10732
+ return "pointer";
10733
+ case "doubleclick":
10734
+ return "doubleClick";
10735
+ case "contextmenu":
10736
+ return "contextMenu";
10737
+ case "longpress":
10738
+ return "longPress";
10739
+ case "wheel":
10740
+ return "wheel";
10741
+ default:
10742
+ return null;
10743
+ }
10744
+ }
10745
+ function isExclusiveClaim(e) {
10746
+ const claim = claimOf(e);
10747
+ if (claim?.strength !== "exclusive") return false;
10748
+ const gesture = claimGestureOf(e);
10749
+ if (gesture === null) return false;
10750
+ return claim.claimedKinds === void 0 || claim.claimedKinds.includes(gesture);
10751
+ }
10752
+ var warnedDeadClaims = /* @__PURE__ */ new Set();
10753
+ function reportDeadClaim(owner, warn) {
10754
+ if (process.env.NODE_ENV === "production") return;
10755
+ const key = String(owner);
10756
+ if (warnedDeadClaims.has(key)) return;
10757
+ warnedDeadClaims.add(key);
10758
+ warn(
10759
+ `[weasel] exclusive claim by "${key}" matched no binding: no \`affordance:\` or \`kindOf\` target resolved against it, so the press was dropped.`
10760
+ );
10761
+ }
10518
10762
  function specificity(spec) {
10519
10763
  const t = targetRank("target" in spec ? spec.target : void 0);
10520
10764
  const mods = "mods" in spec ? spec.mods : void 0;
@@ -10531,24 +10775,53 @@ function compareSpecificity(a, b) {
10531
10775
  }
10532
10776
  return 0;
10533
10777
  }
10534
- function matchSorted(e, bindings, isMac, engagedChannels) {
10778
+ function matchSorted(e, bindings, isMac, engagedChannels, warn = (m) => console.warn(m)) {
10535
10779
  const out = [];
10536
10780
  const engaged = engagedChannels ?? EMPTY_ENGAGED;
10781
+ const exclusive = isExclusiveClaim(e);
10782
+ const pool = exclusive ? bindings.filter((sb) => targetConsultsAffordance(specTargetOf(sb.binding.spec))) : bindings;
10537
10783
  for (const scope of SCOPE_PRIORITY) {
10538
10784
  const scopeMatches = [];
10539
- for (const sb of bindings) {
10785
+ for (const sb of pool) {
10540
10786
  if (sb.scope !== scope) continue;
10541
10787
  const phaseCtx = { selfChannel: sb.ownerToolId, engagedChannels: engaged };
10542
10788
  if (matchSpec(e, sb.binding.spec, isMac, phaseCtx)) {
10543
10789
  scopeMatches.push({ binding: sb.binding, scope, ownerToolId: sb.ownerToolId });
10544
10790
  }
10545
10791
  }
10546
- scopeMatches.sort((a, b) => compareSpecificity(a.binding.spec, b.binding.spec));
10547
- out.push(...scopeMatches);
10792
+ scopeMatches.sort((a, b) => compareSpecificity(a.binding.spec, b.binding.spec));
10793
+ out.push(...scopeMatches);
10794
+ }
10795
+ if (exclusive && out.length === 0 && bindings.length > 0) {
10796
+ reportDeadClaim(claimOf(e)?.owner, warn);
10797
+ }
10798
+ return out;
10799
+ }
10800
+ var EMPTY_ENGAGED = /* @__PURE__ */ new Set();
10801
+
10802
+ // src/contributions/eligibility.ts
10803
+ function liveScope(id, eligibility, state) {
10804
+ const tags = eligibility.capabilities;
10805
+ if (tags && tags.length > 0 && state.allows && !state.allows(tags)) return null;
10806
+ if (state.engagedIds?.has(id)) return "hotkey";
10807
+ if (eligibility.offhand && state.heldTriggers.has(eligibility.offhand)) return "hotkey";
10808
+ if (eligibility.focus && state.focusedId === id) return "active";
10809
+ if (eligibility.always || eligibility.claimed) return "ambient";
10810
+ return null;
10811
+ }
10812
+
10813
+ // src/contributions/assemble.ts
10814
+ function scopeBindings(entries, state) {
10815
+ const out = [];
10816
+ for (const entry of entries) {
10817
+ const scope = liveScope(entry.id, entry.eligibility ?? {}, state);
10818
+ if (scope === null) continue;
10819
+ for (const binding of entry.bindings ?? []) {
10820
+ out.push({ binding, scope, ownerToolId: entry.id });
10821
+ }
10548
10822
  }
10549
10823
  return out;
10550
10824
  }
10551
- var EMPTY_ENGAGED = /* @__PURE__ */ new Set();
10552
10825
 
10553
10826
  // src/features/chrome-caps/rule.ts
10554
10827
  var ALWAYS = { all: [] };
@@ -10607,6 +10880,14 @@ function evaluateSelector(s, ctx) {
10607
10880
  const resizable2 = ctx.selectionResizable ?? true;
10608
10881
  if (resizable2 !== s.resizable) return false;
10609
10882
  }
10883
+ if (s.coarsePointer !== void 0) {
10884
+ const device = ctx.device ?? DEFAULT_DEVICE_PROFILE;
10885
+ if (device.coarsePointer !== s.coarsePointer) return false;
10886
+ }
10887
+ if (s.canHover !== void 0) {
10888
+ const device = ctx.device ?? DEFAULT_DEVICE_PROFILE;
10889
+ if (device.canHover !== s.canHover) return false;
10890
+ }
10610
10891
  return true;
10611
10892
  }
10612
10893
  var MAX_RULE_DEPTH = 10;
@@ -10702,6 +10983,8 @@ var capabilityIs = (cap) => cond({ capability: cap });
10702
10983
  var capabilityIn = (caps) => cond({ capability: { in: caps } });
10703
10984
  var capabilityAll = (caps) => cond({ capability: [...caps] });
10704
10985
  var capabilityNot = (cap) => cond({ capability: { not: cap } });
10986
+ var coarsePointer = cond({ coarsePointer: true });
10987
+ var canHover = cond({ canHover: true });
10705
10988
  var DEFAULT_ALLOWED_CAPABILITIES = /* @__PURE__ */ new Set([
10706
10989
  ...NORMAL.allows,
10707
10990
  ...IMPLICIT_TAGS
@@ -10718,7 +11001,8 @@ function buildRuleCtx(args) {
10718
11001
  mode: args.mode,
10719
11002
  allowedCapabilities: args.allowedCapabilities,
10720
11003
  selectionResizable: args.selectionResizable,
10721
- editingAnchors: args.editingAnchors
11004
+ editingAnchors: args.editingAnchors,
11005
+ device: args.device
10722
11006
  };
10723
11007
  }
10724
11008
 
@@ -10843,7 +11127,7 @@ function useHoverTracking(args) {
10843
11127
  function pointerGestureId(pointerId) {
10844
11128
  return `pointer-${pointerId ?? "mouse"}`;
10845
11129
  }
10846
- var DEV2 = (() => {
11130
+ var DEV = (() => {
10847
11131
  try {
10848
11132
  return Boolean(import.meta.env?.DEV);
10849
11133
  } catch {
@@ -10851,9 +11135,9 @@ var DEV2 = (() => {
10851
11135
  }
10852
11136
  })();
10853
11137
  var TRACE_LIMIT = 200;
10854
- var traceLog = DEV2 && typeof window !== "undefined" ? window.__weaselDispatchLog__ ??= [] : [];
11138
+ var traceLog = DEV && typeof window !== "undefined" ? window.__weaselDispatchLog__ ??= [] : [];
10855
11139
  function recordTrace(entry) {
10856
- if (!DEV2) return;
11140
+ if (!DEV) return;
10857
11141
  traceLog.push(entry);
10858
11142
  if (traceLog.length > TRACE_LIMIT) traceLog.shift();
10859
11143
  }
@@ -10884,6 +11168,7 @@ function filterEligible(matches, actionLookup, ruleCtx) {
10884
11168
  return isEligible(action, ruleCtx);
10885
11169
  });
10886
11170
  }
11171
+ var EMPTY_TRIGGERS = /* @__PURE__ */ new Set();
10887
11172
  var EMPTY_ENGAGED2 = /* @__PURE__ */ new Set();
10888
11173
  function modifiersOf(e) {
10889
11174
  return { alt: e.altKey, ctrl: e.ctrlKey, meta: e.metaKey, shift: e.shiftKey };
@@ -10991,34 +11276,23 @@ function createDispatcher(opts) {
10991
11276
  return `ongoing-${event.kind}`;
10992
11277
  }
10993
11278
  function assembleScopedBindings(ctx) {
10994
- const result = [];
10995
- for (const toolId of ctx.hotkeyStack) {
10996
- const tool = ctx.toolsById.get(toolId);
10997
- for (const binding of tool?.bindings ?? []) {
10998
- result.push({ binding, scope: "hotkey", ownerToolId: toolId });
10999
- }
11000
- }
11001
- const activeTool = ctx.toolsById.get(ctx.activeToolId);
11002
- for (const binding of activeTool?.bindings ?? []) {
11003
- result.push({ binding, scope: "active", ownerToolId: ctx.activeToolId });
11004
- }
11005
- for (const toolId of ctx.ambientToolIds ?? []) {
11006
- const tool = ctx.toolsById.get(toolId);
11007
- for (const binding of tool?.bindings ?? []) {
11008
- result.push({ binding, scope: "ambient", ownerToolId: toolId });
11009
- }
11010
- }
11279
+ const ordered = [];
11280
+ for (const id of ctx.hotkeyStack) {
11281
+ const tool = ctx.toolsById.get(id);
11282
+ if (tool && !ordered.includes(tool)) ordered.push(tool);
11283
+ }
11284
+ for (const tool of ctx.toolsById.values()) {
11285
+ if (!ordered.includes(tool)) ordered.push(tool);
11286
+ }
11287
+ const result = scopeBindings(ordered, {
11288
+ focusedId: ctx.activeToolId,
11289
+ heldTriggers: EMPTY_TRIGGERS,
11290
+ engagedIds: new Set(ctx.hotkeyStack)
11291
+ });
11011
11292
  for (const action of ctx.actions.list()) {
11012
- const gs = action.defaultBinding;
11013
- if (!gs) continue;
11014
- const raw = Array.isArray(gs) ? gs : [gs];
11015
- for (const entry of raw) {
11016
- const isBoundObj = !("kind" in entry);
11017
- const spec = isBoundObj ? entry.spec : entry;
11018
- const opts2 = isBoundObj ? entry.opts : void 0;
11019
- const defaultBinding = { spec, actionId: action.id, ...opts2 !== void 0 ? { opts: opts2 } : {} };
11020
- const targetScope = action.scope === "hotkey" ? "hotkey" : "ambient";
11021
- result.push({ binding: defaultBinding, scope: targetScope, ownerToolId: null });
11293
+ const targetScope = action.scope === "hotkey" ? "hotkey" : "ambient";
11294
+ for (const binding of actionBindings(action)) {
11295
+ result.push({ binding, scope: targetScope, ownerToolId: null });
11022
11296
  }
11023
11297
  }
11024
11298
  return result;
@@ -11173,15 +11447,26 @@ function createDispatcher(opts) {
11173
11447
  deltaY: event.deltaY,
11174
11448
  clientX: event.clientX,
11175
11449
  clientY: event.clientY,
11450
+ affordance: event.affordance,
11176
11451
  ...resolved
11177
11452
  };
11178
11453
  } else if (event.kind === "click" || event.kind === "doubleclick") {
11179
11454
  params = {
11180
11455
  worldX: event.worldX,
11181
11456
  worldY: event.worldY,
11457
+ affordance: event.affordance,
11182
11458
  // Press point as well as release point — an action that places
11183
11459
  // geometry at the click wants the former. See `ClickEvent`.
11184
- ...event.kind === "click" ? { pressX: event.pressX, pressY: event.pressY, affordance: event.affordance } : {},
11460
+ ...event.kind === "click" ? { pressX: event.pressX, pressY: event.pressY } : {},
11461
+ mods: modifiersOf(event),
11462
+ ...resolved
11463
+ };
11464
+ } else if (event.kind === "contextmenu" || event.kind === "longpress") {
11465
+ params = {
11466
+ worldX: event.kind === "contextmenu" ? event.worldX : event.x,
11467
+ worldY: event.kind === "contextmenu" ? event.worldY : event.y,
11468
+ affordance: event.affordance,
11469
+ bodyTarget: event.bodyTarget,
11185
11470
  mods: modifiersOf(event),
11186
11471
  ...resolved
11187
11472
  };
@@ -11553,7 +11838,7 @@ function computeMultiTouchGeometry(positions) {
11553
11838
  return { centroid, spread };
11554
11839
  }
11555
11840
  function useGestureDispatcher(opts) {
11556
- const { canvasRef, actions, toolsById, ambientToolIds, enabled = true, keyboard = true, affordanceAt, classifyTarget, dispatcher: dispatcherOpt, clientToWorld: clientToWorld2, requestRedraw, getRuleCtx, onDoubleClick } = opts;
11841
+ const { canvasRef, actions, toolsById, enabled = true, keyboard = true, affordanceAt, classifyTarget, dispatcher: dispatcherOpt, clientToWorld: clientToWorld2, requestRedraw, getRuleCtx, onDoubleClick } = opts;
11557
11842
  const onDoubleClickRef = useRef(onDoubleClick);
11558
11843
  onDoubleClickRef.current = onDoubleClick;
11559
11844
  const activeTool = useActiveToolContext();
@@ -11568,7 +11853,6 @@ function useGestureDispatcher(opts) {
11568
11853
  activeToolId: activeTool.active,
11569
11854
  hotkeyStack: activeTool.hotkeyStack,
11570
11855
  toolsById,
11571
- ambientToolIds,
11572
11856
  isMac: IS_MAC,
11573
11857
  getRuleCtx
11574
11858
  });
@@ -11578,7 +11862,6 @@ function useGestureDispatcher(opts) {
11578
11862
  activeToolId: activeTool.active,
11579
11863
  hotkeyStack: activeTool.hotkeyStack,
11580
11864
  toolsById,
11581
- ambientToolIds,
11582
11865
  isMac: IS_MAC,
11583
11866
  getRuleCtx
11584
11867
  };
@@ -11634,6 +11917,49 @@ function useGestureDispatcher(opts) {
11634
11917
  const DRAG_THRESHOLD_PX = 4;
11635
11918
  const bufferedDown = /* @__PURE__ */ new Map();
11636
11919
  const lastPointerDown = /* @__PURE__ */ new Map();
11920
+ const LONG_PRESS_MS = 500;
11921
+ const longPressTimers = /* @__PURE__ */ new Map();
11922
+ const cancelLongPress = (pointerId) => {
11923
+ const t = longPressTimers.get(pointerId);
11924
+ if (t !== void 0) {
11925
+ clearTimeout(t);
11926
+ longPressTimers.delete(pointerId);
11927
+ }
11928
+ };
11929
+ const cancelAllLongPress = () => {
11930
+ for (const t of longPressTimers.values()) clearTimeout(t);
11931
+ longPressTimers.clear();
11932
+ };
11933
+ const fireLongPress = (pointerId) => {
11934
+ const down = lastPointerDown.get(pointerId);
11935
+ if (!down) return;
11936
+ const shared = {
11937
+ target: down.target,
11938
+ altKey: down.altKey,
11939
+ ctrlKey: down.ctrlKey,
11940
+ metaKey: down.metaKey,
11941
+ shiftKey: down.shiftKey,
11942
+ ...down.affordance !== void 0 ? { affordance: down.affordance } : {},
11943
+ ...down.bodyTarget !== void 0 ? { bodyTarget: down.bodyTarget } : {},
11944
+ ...down.bodyKind !== void 0 ? { bodyKind: down.bodyKind } : {}
11945
+ };
11946
+ const result = dispatch2({
11947
+ kind: "longpress",
11948
+ x: down.worldX,
11949
+ y: down.worldY,
11950
+ clientX: down.clientX,
11951
+ clientY: down.clientY,
11952
+ ...shared
11953
+ });
11954
+ if (result === "unhandled") {
11955
+ dispatch2({
11956
+ kind: "contextmenu",
11957
+ worldX: down.worldX,
11958
+ worldY: down.worldY,
11959
+ ...shared
11960
+ });
11961
+ }
11962
+ };
11637
11963
  const DOUBLE_CLICK_MAX_MS = 600;
11638
11964
  const DOUBLE_CLICK_MAX_PX = 8;
11639
11965
  const onKeyDown = (e) => {
@@ -11686,6 +12012,9 @@ function useGestureDispatcher(opts) {
11686
12012
  const onWheel = (e) => {
11687
12013
  const rect = canvas?.getBoundingClientRect();
11688
12014
  const [localX, localY] = rect ? clientToCanvasRect(rect, e.clientX, e.clientY) : [e.clientX, e.clientY];
12015
+ const screenPoint = { x: e.clientX, y: e.clientY };
12016
+ const affordance = affordanceAtRef.current?.(screenPoint) ?? void 0;
12017
+ const body = classifyTargetRef.current?.(screenPoint);
11689
12018
  const ev = {
11690
12019
  kind: "wheel",
11691
12020
  altKey: e.altKey,
@@ -11695,7 +12024,10 @@ function useGestureDispatcher(opts) {
11695
12024
  deltaX: e.deltaX,
11696
12025
  deltaY: e.deltaY,
11697
12026
  clientX: localX,
11698
- clientY: localY
12027
+ clientY: localY,
12028
+ ...affordance !== void 0 ? { affordance } : {},
12029
+ ...body?.body !== void 0 ? { bodyTarget: body.body } : {},
12030
+ ...body?.kind !== void 0 ? { bodyKind: body.kind } : {}
11699
12031
  };
11700
12032
  const result = dispatch2(ev);
11701
12033
  if (result === "handled") {
@@ -11737,6 +12069,7 @@ function useGestureDispatcher(opts) {
11737
12069
  bufferedDown.set(e.pointerId, { ev, clientX: e.clientX, clientY: e.clientY });
11738
12070
  dispatch2({ ...ev, stage: "press" });
11739
12071
  lastPointerDown.set(e.pointerId, {
12072
+ target: e.target,
11740
12073
  clientX: e.clientX,
11741
12074
  clientY: e.clientY,
11742
12075
  worldX: w.x,
@@ -11749,7 +12082,19 @@ function useGestureDispatcher(opts) {
11749
12082
  metaKey: e.metaKey,
11750
12083
  shiftKey: e.shiftKey
11751
12084
  });
12085
+ if ((e.pointerType === "touch" || e.pointerType === "pen") && activePointers.size === 1) {
12086
+ cancelLongPress(e.pointerId);
12087
+ longPressTimers.set(
12088
+ e.pointerId,
12089
+ setTimeout(() => {
12090
+ longPressTimers.delete(e.pointerId);
12091
+ if (disposed) return;
12092
+ fireLongPress(e.pointerId);
12093
+ }, LONG_PRESS_MS)
12094
+ );
12095
+ }
11752
12096
  if (activePointers.size >= 2) {
12097
+ cancelAllLongPress();
11753
12098
  for (const id of activePointers) bufferedDown.delete(id);
11754
12099
  const { centroid, spread } = computeMultiTouchGeometry(pointerPositions);
11755
12100
  if (!multiTouchStart) {
@@ -11780,6 +12125,7 @@ function useGestureDispatcher(opts) {
11780
12125
  const dy = e.clientY - buffered.clientY;
11781
12126
  if (Math.hypot(dx, dy) >= DRAG_THRESHOLD_PX) {
11782
12127
  bufferedDown.delete(e.pointerId);
12128
+ cancelLongPress(e.pointerId);
11783
12129
  dispatch2(buffered.ev);
11784
12130
  }
11785
12131
  }
@@ -11893,6 +12239,7 @@ function useGestureDispatcher(opts) {
11893
12239
  clearHoverCursor();
11894
12240
  };
11895
12241
  const onPointerUp = (e) => {
12242
+ cancelLongPress(e.pointerId);
11896
12243
  const prevSize = activePointers.size;
11897
12244
  activePointers.delete(e.pointerId);
11898
12245
  pointerPositions.delete(e.pointerId);
@@ -11974,6 +12321,7 @@ function useGestureDispatcher(opts) {
11974
12321
  shiftKey: e.shiftKey,
11975
12322
  worldX: wClick.x,
11976
12323
  worldY: wClick.y,
12324
+ ...down.affordance !== void 0 ? { affordance: down.affordance } : {},
11977
12325
  ...down.bodyTarget !== void 0 ? { bodyTarget: down.bodyTarget } : {},
11978
12326
  ...down.bodyKind !== void 0 ? { bodyKind: down.bodyKind } : {}
11979
12327
  };
@@ -11997,6 +12345,7 @@ function useGestureDispatcher(opts) {
11997
12345
  }, 0);
11998
12346
  };
11999
12347
  const onPointerCancel = (e) => {
12348
+ cancelLongPress(e.pointerId);
12000
12349
  activePointers.delete(e.pointerId);
12001
12350
  pointerPositions.delete(e.pointerId);
12002
12351
  lastPointerDown.delete(e.pointerId);
@@ -12014,8 +12363,10 @@ function useGestureDispatcher(opts) {
12014
12363
  };
12015
12364
  const onContextMenu = (e) => {
12016
12365
  e.preventDefault();
12017
- const worldPoint = { x: e.clientX, y: e.clientY };
12018
- const menuBody = classifyTargetRef.current?.(worldPoint);
12366
+ const screenPoint = { x: e.clientX, y: e.clientY };
12367
+ const affordance = affordanceAtRef.current?.(screenPoint) ?? void 0;
12368
+ const menuBody = classifyTargetRef.current?.(screenPoint);
12369
+ const w = toWorld(e.clientX, e.clientY);
12019
12370
  const ev = {
12020
12371
  kind: "contextmenu",
12021
12372
  target: e.target,
@@ -12023,6 +12374,9 @@ function useGestureDispatcher(opts) {
12023
12374
  ctrlKey: e.ctrlKey,
12024
12375
  metaKey: e.metaKey,
12025
12376
  shiftKey: e.shiftKey,
12377
+ worldX: w.x,
12378
+ worldY: w.y,
12379
+ ...affordance !== void 0 ? { affordance } : {},
12026
12380
  ...menuBody?.body !== void 0 ? { bodyTarget: menuBody.body } : {},
12027
12381
  ...menuBody?.kind !== void 0 ? { bodyKind: menuBody.kind } : {}
12028
12382
  };
@@ -12119,6 +12473,7 @@ function useGestureDispatcher(opts) {
12119
12473
  window.removeEventListener("paste", onPaste);
12120
12474
  canvas?.classList.remove(DROPOVER_CLASS);
12121
12475
  clearHoverCursor();
12476
+ cancelAllLongPress();
12122
12477
  disposed = true;
12123
12478
  dispatcher.cancelAll("cancel");
12124
12479
  };
@@ -12421,47 +12776,209 @@ function useViewAnimation(setView) {
12421
12776
  );
12422
12777
  return { animateTo, animateToBounds, cancel };
12423
12778
  }
12424
- function useTools(opts) {
12425
- if (!(opts.active in opts.registry)) {
12426
- throw new Error(`useTools: active "${opts.active}" not in registry`);
12779
+
12780
+ // src/interactions/actions/defaults/toolOffhand.ts
12781
+ var TOOL_OFFHAND_ID = "tool.offhand";
12782
+ function offhandKeyFor(trigger) {
12783
+ switch (trigger) {
12784
+ case "space":
12785
+ return " ";
12786
+ case "alt":
12787
+ return "Alt";
12788
+ case "ctrl":
12789
+ return "Control";
12790
+ case "meta":
12791
+ return "Meta";
12792
+ case "shift":
12793
+ return "Shift";
12427
12794
  }
12795
+ }
12796
+ function buildToolOffhandBindings(specs) {
12797
+ return specs.map(({ toolId, key }) => ({
12798
+ spec: { kind: "key-held", key },
12799
+ opts: { params: { toolId } }
12800
+ }));
12801
+ }
12802
+ function makeToolOffhandAction(bindings) {
12803
+ return {
12804
+ id: TOOL_OFFHAND_ID,
12805
+ label: "Engage tool (offhand)",
12806
+ defaultBinding: bindings,
12807
+ scope: "hotkey",
12808
+ invoker: {
12809
+ timing: "ongoing",
12810
+ start: (ctx, opts) => {
12811
+ const activeTool = ctx.deps.activeTool;
12812
+ const toolId = resolveParams(opts?.params)?.toolId;
12813
+ if (!activeTool || !toolId) return {};
12814
+ activeTool.pushHotkey(toolId);
12815
+ return {
12816
+ onEnd: () => {
12817
+ activeTool.popHotkey();
12818
+ }
12819
+ };
12820
+ }
12821
+ },
12822
+ requires: ["activeTool"]
12823
+ };
12824
+ }
12825
+
12826
+ // src/contributions/useContributions.ts
12827
+ var NO_TRIGGERS = /* @__PURE__ */ new Set();
12828
+ function useContributions(opts) {
12428
12829
  const ctx = useActiveToolContext();
12830
+ const actionsRegistry = useActionsRegistry();
12429
12831
  const hasInitializedRef = useRef(false);
12430
12832
  const isFirstRender = !hasInitializedRef.current;
12431
12833
  const needsFirstMountSyncRef = useRef(false);
12432
12834
  if (isFirstRender) {
12433
12835
  hasInitializedRef.current = true;
12434
- needsFirstMountSyncRef.current = ctx.active === "select" && opts.active !== "select";
12836
+ needsFirstMountSyncRef.current = ctx.active === "select" && opts.focused !== "select";
12435
12837
  }
12436
12838
  useEffect(() => {
12437
12839
  if (!needsFirstMountSyncRef.current) return;
12438
12840
  needsFirstMountSyncRef.current = false;
12439
- ctx.setActive(opts.active);
12841
+ ctx.setActive(opts.focused);
12842
+ }, []);
12843
+ const focused2 = isFirstRender && ctx.active === "select" && opts.focused !== "select" ? opts.focused : ctx.active;
12844
+ const entriesRef = useRef(opts.entries);
12845
+ entriesRef.current = opts.entries;
12846
+ const focusedRef = useRef(focused2);
12847
+ focusedRef.current = focused2;
12848
+ const engagedRef = useRef(ctx.hotkeyStack);
12849
+ engagedRef.current = ctx.hotkeyStack;
12850
+ const setFocused = useCallback((id) => {
12851
+ ctx.setActive(id);
12852
+ }, [ctx]);
12853
+ const snapshot = useCallback(() => {
12854
+ const engaged = engagedRef.current;
12855
+ const engagedIds = new Set(engaged);
12856
+ const ordered = [];
12857
+ for (const id of engaged) {
12858
+ const entry = entriesRef.current.find((e) => e.id === id);
12859
+ if (entry && !ordered.includes(entry)) ordered.push(entry);
12860
+ }
12861
+ for (const entry of entriesRef.current) {
12862
+ if (!ordered.includes(entry)) ordered.push(entry);
12863
+ }
12864
+ return {
12865
+ ordered,
12866
+ state: { focusedId: focusedRef.current, heldTriggers: NO_TRIGGERS, engagedIds }
12867
+ };
12440
12868
  }, []);
12441
- const active = isFirstRender && ctx.active === "select" && opts.active !== "select" ? opts.active : ctx.active;
12442
- const hotkeyStack = ctx.hotkeyStack;
12443
- const hotkeyEngaged = hotkeyStack.at(-1) ?? null;
12444
- const registryRef = useRef(opts.registry);
12445
- registryRef.current = opts.registry;
12446
- const ambientRef = useRef(opts.ambient ?? []);
12447
- ambientRef.current = opts.ambient ?? [];
12448
- const activeRef = useRef(active);
12449
- activeRef.current = active;
12869
+ const scopedBindings = useCallback(() => {
12870
+ const { ordered, state } = snapshot();
12871
+ return scopeBindings(ordered, state);
12872
+ }, [snapshot]);
12873
+ const overlays = useCallback(() => {
12874
+ const { ordered, state } = snapshot();
12875
+ const active = [];
12876
+ const hotkey = [];
12877
+ const ambient = [];
12878
+ for (const entry of ordered) {
12879
+ if (!entry.overlay) continue;
12880
+ const scope = liveScope(entry.id, entry.eligibility ?? {}, state);
12881
+ if (scope === "active") active.push(entry.overlay);
12882
+ else if (scope === "hotkey") hotkey.push(entry.overlay);
12883
+ else if (scope === "ambient") ambient.push(entry.overlay);
12884
+ }
12885
+ return [...active, ...hotkey, ...ambient];
12886
+ }, [snapshot]);
12887
+ const offhandSig = opts.entries.map((e) => e.eligibility?.offhand ? `${e.id}:${e.eligibility.offhand}` : "").filter(Boolean).join(",");
12888
+ useEffect(() => {
12889
+ if (!actionsRegistry || offhandSig === "") return;
12890
+ const specs = offhandSig.split(",").map((entry) => {
12891
+ const [toolId, trigger] = entry.split(":");
12892
+ return { toolId, key: offhandKeyFor(trigger) };
12893
+ });
12894
+ return actionsRegistry.register(makeToolOffhandAction(buildToolOffhandBindings(specs)));
12895
+ }, [actionsRegistry, offhandSig]);
12896
+ const lastConflictSigRef = useRef(null);
12897
+ const entrySig = opts.entries.map((e) => e.id).join(",");
12898
+ useEffect(() => {
12899
+ if (process.env.NODE_ENV === "production") return;
12900
+ if (lastConflictSigRef.current === entrySig) return;
12901
+ lastConflictSigRef.current = entrySig;
12902
+ const registry2 = [];
12903
+ const ambient = [];
12904
+ for (const entry of entriesRef.current) {
12905
+ (entry.eligibility?.focus ? registry2 : ambient).push(entry);
12906
+ }
12907
+ reportRouteConflicts({
12908
+ registry: registry2,
12909
+ ambient,
12910
+ actions: actionsRegistry?.list() ?? []
12911
+ });
12912
+ }, [entrySig, actionsRegistry]);
12913
+ return useMemo(
12914
+ () => ({
12915
+ entries: entriesRef.current,
12916
+ focused: focused2,
12917
+ setFocused,
12918
+ scopedBindings,
12919
+ overlays
12920
+ }),
12921
+ [focused2, setFocused, scopedBindings, overlays]
12922
+ );
12923
+ }
12924
+
12925
+ // src/tools/useTools.ts
12926
+ function declareSlot(tool, slot) {
12927
+ const hotkey = tool.def?.hotkey;
12928
+ const eligibility = {
12929
+ ...tool.eligibility,
12930
+ [slot]: true,
12931
+ ...hotkey ? { offhand: hotkey } : {}
12932
+ };
12933
+ return sameEligibility(tool.eligibility, eligibility) ? tool : { ...tool, eligibility };
12934
+ }
12935
+ function sameEligibility(a, b) {
12936
+ if (!a) return false;
12937
+ const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
12938
+ for (const key of keys) if (a[key] !== b[key]) return false;
12939
+ return true;
12940
+ }
12941
+ function useTools(opts) {
12942
+ if (!(opts.active in opts.registry)) {
12943
+ throw new Error(`useTools: active "${opts.active}" not in registry`);
12944
+ }
12945
+ const ctx = useActiveToolContext();
12946
+ const slotted = useMemo(() => {
12947
+ const registry2 = {};
12948
+ for (const [id, tool] of Object.entries(opts.registry)) {
12949
+ registry2[id] = declareSlot(tool, "focus");
12950
+ }
12951
+ const ambient = (opts.ambient ?? []).map((t) => declareSlot(t, "always"));
12952
+ const byId = /* @__PURE__ */ new Map();
12953
+ for (const tool of [...Object.values(registry2), ...ambient]) {
12954
+ const prior = byId.get(tool.id);
12955
+ if (prior) byId.set(tool.id, { ...tool, eligibility: { ...prior.eligibility, ...tool.eligibility } });
12956
+ else byId.set(tool.id, tool);
12957
+ }
12958
+ return { registry: registry2, ambient, entries: [...byId.values()] };
12959
+ }, [opts.registry, opts.ambient]);
12960
+ const contributions = useContributions({ entries: slotted.entries, focused: opts.active });
12961
+ const hotkeyEngaged = ctx.hotkeyStack.at(-1) ?? null;
12962
+ const slottedRef = useRef(slotted);
12963
+ slottedRef.current = slotted;
12964
+ const activeRef = useRef(contributions.focused);
12965
+ activeRef.current = contributions.focused;
12450
12966
  const hotkeyRef = useRef(hotkeyEngaged);
12451
12967
  hotkeyRef.current = hotkeyEngaged;
12968
+ const setFocused = contributions.setFocused;
12452
12969
  const setActive = useCallback(
12453
12970
  (id) => {
12454
- if (!(id in registryRef.current)) {
12971
+ if (!(id in slottedRef.current.registry)) {
12455
12972
  throw new Error(`setActive: "${id}" not in registry`);
12456
12973
  }
12457
12974
  dlog("tools", "active:", activeRef.current, "\u2192", id);
12458
- ctx.setActive(id);
12975
+ setFocused(id);
12459
12976
  },
12460
- [ctx]
12977
+ [setFocused]
12461
12978
  );
12462
12979
  const engageHotkey = useCallback(
12463
12980
  (id) => {
12464
- if (!(id in registryRef.current)) {
12981
+ if (!(id in slottedRef.current.registry)) {
12465
12982
  throw new Error(`engageHotkey: "${id}" not in registry`);
12466
12983
  }
12467
12984
  dlog("tools", "hotkey engaged:", id);
@@ -12473,32 +12990,12 @@ function useTools(opts) {
12473
12990
  if (hotkeyRef.current) dlog("tools", "hotkey disengaged:", hotkeyRef.current);
12474
12991
  ctx.popHotkey();
12475
12992
  }, [ctx]);
12476
- const lastConflictSigRef = useRef(null);
12477
- const toolSetSig = Object.keys(opts.registry).join(",") + "|" + (opts.ambient ?? []).map((t) => t.id).join(",");
12478
- useEffect(() => {
12479
- if (process.env.NODE_ENV === "production") return;
12480
- if (lastConflictSigRef.current === toolSetSig) return;
12481
- lastConflictSigRef.current = toolSetSig;
12482
- reportRouteConflicts({
12483
- registry: registryRef.current,
12484
- ambient: ambientRef.current
12485
- });
12486
- }, [toolSetSig]);
12487
12993
  const has = useCallback(
12488
- (id) => id in registryRef.current || ambientRef.current.some((t) => t.id === id),
12994
+ (id) => id in slottedRef.current.registry || slottedRef.current.ambient.some((t) => t.id === id),
12489
12995
  []
12490
12996
  );
12491
- const getActiveOverlays = useCallback(() => {
12492
- const out = [];
12493
- const activeTool = registryRef.current[activeRef.current];
12494
- if (activeTool?.overlay) out.push(activeTool.overlay);
12495
- const mod = hotkeyRef.current ? registryRef.current[hotkeyRef.current] : null;
12496
- if (mod?.overlay) out.push(mod.overlay);
12497
- for (const t of ambientRef.current) {
12498
- if (t.overlay) out.push(t.overlay);
12499
- }
12500
- return out;
12501
- }, []);
12997
+ const active = contributions.focused;
12998
+ const getActiveOverlays = contributions.overlays;
12502
12999
  return useMemo(
12503
13000
  () => ({
12504
13001
  active,
@@ -12506,8 +13003,8 @@ function useTools(opts) {
12506
13003
  hotkeyEngaged,
12507
13004
  engageHotkey,
12508
13005
  disengageHotkey,
12509
- ambient: ambientRef.current,
12510
- registry: registryRef.current,
13006
+ ambient: slottedRef.current.ambient,
13007
+ registry: slottedRef.current.registry,
12511
13008
  has,
12512
13009
  getActiveOverlays
12513
13010
  }),
@@ -12515,38 +13012,6 @@ function useTools(opts) {
12515
13012
  );
12516
13013
  }
12517
13014
 
12518
- // src/interactions/actions/defaults/toolOffhand.ts
12519
- var TOOL_OFFHAND_ID = "tool.offhand";
12520
- function buildToolOffhandBindings(specs) {
12521
- return specs.map(({ toolId, key }) => ({
12522
- spec: { kind: "key-held", key },
12523
- opts: { params: { toolId } }
12524
- }));
12525
- }
12526
- function makeToolOffhandAction(bindings) {
12527
- return {
12528
- id: TOOL_OFFHAND_ID,
12529
- label: "Engage tool (offhand)",
12530
- defaultBinding: bindings,
12531
- scope: "hotkey",
12532
- invoker: {
12533
- timing: "ongoing",
12534
- start: (ctx, opts) => {
12535
- const activeTool = ctx.deps.activeTool;
12536
- const toolId = resolveParams(opts?.params)?.toolId;
12537
- if (!activeTool || !toolId) return {};
12538
- activeTool.pushHotkey(toolId);
12539
- return {
12540
- onEnd: () => {
12541
- activeTool.popHotkey();
12542
- }
12543
- };
12544
- }
12545
- },
12546
- requires: ["activeTool"]
12547
- };
12548
- }
12549
-
12550
13015
  // src/interactions/actions/defaults/toolActivate.ts
12551
13016
  var TOOL_ACTIVATE_ID = "tool.activate";
12552
13017
  function buildToolActivateBindings(specs) {
@@ -12623,9 +13088,6 @@ var BUILTIN_SELECT_KEYS = {
12623
13088
  hand: { key: "H" },
12624
13089
  pen: { key: "P" }
12625
13090
  };
12626
- var BUILTIN_OFFHAND_ACTIONS = {
12627
- hand: " "
12628
- };
12629
13091
  function useKeybindings(tools, options = {}) {
12630
13092
  const toolsRef = useRef(tools);
12631
13093
  toolsRef.current = tools;
@@ -12633,19 +13095,6 @@ function useKeybindings(tools, options = {}) {
12633
13095
  optionsRef.current = options;
12634
13096
  const initialActiveRef = useRef(tools.active);
12635
13097
  const registry2 = useActionsRegistry();
12636
- useEffect(() => {
12637
- if (optionsRef.current.disable) return;
12638
- if (!registry2) return;
12639
- const specs = [];
12640
- for (const [toolId, key] of Object.entries(BUILTIN_OFFHAND_ACTIONS)) {
12641
- if (toolsRef.current.has(toolId)) {
12642
- specs.push({ toolId, key });
12643
- }
12644
- }
12645
- if (specs.length === 0) return;
12646
- const bindings = buildToolOffhandBindings(specs);
12647
- return registry2.register(makeToolOffhandAction(bindings));
12648
- }, [registry2, tools]);
12649
13098
  useEffect(() => {
12650
13099
  if (optionsRef.current.disable) return;
12651
13100
  if (!registry2) return;
@@ -13207,8 +13656,108 @@ function textLineBoxes(pose, opts = {}) {
13207
13656
  return out;
13208
13657
  }
13209
13658
 
13659
+ // src/core/fillInPoseFrame.ts
13660
+ function fillInPoseFrame(fill, box) {
13661
+ if (isBoundsPattern(fill)) {
13662
+ const o = fill.origin ?? { x: 0, y: 0 };
13663
+ return { ...fill, origin: { x: box.x + o.x, y: box.y + o.y }, units: "local" };
13664
+ }
13665
+ if (!isBoundsGradient(fill)) return fill;
13666
+ const toBox = (p) => ({
13667
+ x: box.x + p.x * box.width,
13668
+ y: box.y + p.y * box.height
13669
+ });
13670
+ if (fill.fill === "linear-gradient") {
13671
+ return { ...fill, from: toBox(fill.from), to: toBox(fill.to), units: "local" };
13672
+ }
13673
+ if (fill.fill === "radial-gradient") {
13674
+ const scale2 = Math.hypot(box.width, box.height) / Math.SQRT2;
13675
+ return { ...fill, center: toBox(fill.center), radius: fill.radius * scale2, units: "local" };
13676
+ }
13677
+ return { ...fill, center: toBox(fill.center), units: "local" };
13678
+ }
13679
+ function fillToBoundsFrame(fill, box) {
13680
+ if (fill.fill === "pattern") {
13681
+ const o = fill.origin ?? { x: 0, y: 0 };
13682
+ return { ...fill, origin: { x: o.x - box.x, y: o.y - box.y }, units: "bounds" };
13683
+ }
13684
+ const isGradient = fill.fill === "linear-gradient" || fill.fill === "radial-gradient" || fill.fill === "conic-gradient";
13685
+ if (!isGradient) return fill;
13686
+ if (box.width === 0 || box.height === 0) return fill;
13687
+ const fromBox = (p) => ({
13688
+ x: (p.x - box.x) / box.width,
13689
+ y: (p.y - box.y) / box.height
13690
+ });
13691
+ if (fill.fill === "linear-gradient") {
13692
+ return { ...fill, from: fromBox(fill.from), to: fromBox(fill.to), units: "bounds" };
13693
+ }
13694
+ if (fill.fill === "radial-gradient") {
13695
+ const scale2 = Math.hypot(box.width, box.height) / Math.SQRT2;
13696
+ return { ...fill, center: fromBox(fill.center), radius: fill.radius / scale2, units: "bounds" };
13697
+ }
13698
+ return { ...fill, center: fromBox(fill.center), units: "bounds" };
13699
+ }
13700
+ function isBoundsPattern(fill) {
13701
+ return fill.fill === "pattern" && fill.units === "bounds";
13702
+ }
13703
+ function isBoundsGradient(fill) {
13704
+ if (fill === void 0) return false;
13705
+ const kind = fill.fill;
13706
+ if (kind !== "linear-gradient" && kind !== "radial-gradient" && kind !== "conic-gradient") {
13707
+ return false;
13708
+ }
13709
+ return fill.units === "bounds";
13710
+ }
13711
+
13712
+ // src/features/patterns/resolveSpec.ts
13713
+ var cache5 = /* @__PURE__ */ new Map();
13714
+ function specKey(spec) {
13715
+ const keys = Object.keys(spec).sort();
13716
+ const bag = spec;
13717
+ return keys.map((k) => `${k}=${String(bag[k])}`).join("&");
13718
+ }
13719
+ function resolvePatternSpec(spec) {
13720
+ const key = specKey(spec);
13721
+ const hit = cache5.get(key);
13722
+ if (hit !== void 0) return hit;
13723
+ const built = buildTile(spec);
13724
+ cache5.set(key, built);
13725
+ return built;
13726
+ }
13727
+ function buildTile(spec) {
13728
+ const { color, size } = spec;
13729
+ switch (spec.tile) {
13730
+ case "hatch":
13731
+ return hatch({ color, size, lineWidth: spec.lineWidth });
13732
+ case "crosshatch":
13733
+ return crosshatch({ color, size, lineWidth: spec.lineWidth });
13734
+ case "dots":
13735
+ return dots({ color, size, radius: spec.radius });
13736
+ case "chunks":
13737
+ return chunks({
13738
+ color,
13739
+ size,
13740
+ bg: spec.bg,
13741
+ density: spec.density,
13742
+ chunkSize: spec.chunkSize,
13743
+ seed: spec.seed
13744
+ });
13745
+ default:
13746
+ return null;
13747
+ }
13748
+ }
13749
+ function isPatternSpec(payload) {
13750
+ return "tile" in payload;
13751
+ }
13752
+ function resolveFillPattern(fill) {
13753
+ if (fill.fill !== "pattern") return fill;
13754
+ if (!isPatternSpec(fill.pattern)) return fill;
13755
+ const handle = resolvePatternSpec(fill.pattern);
13756
+ return handle ? { ...fill, pattern: handle } : null;
13757
+ }
13758
+
13210
13759
  // src/features/images/imageCache.ts
13211
- var cache4 = /* @__PURE__ */ new Map();
13760
+ var cache6 = /* @__PURE__ */ new Map();
13212
13761
  var subscribers = /* @__PURE__ */ new Set();
13213
13762
  var defaultLoader = async (src) => {
13214
13763
  if (typeof Image === "undefined" || typeof createImageBitmap === "undefined") {
@@ -13226,28 +13775,28 @@ function notify() {
13226
13775
  }
13227
13776
  function startLoad(src) {
13228
13777
  const entry = { status: "loading" };
13229
- cache4.set(src, entry);
13778
+ cache6.set(src, entry);
13230
13779
  loader(src).then(
13231
13780
  (bitmap) => {
13232
- if (cache4.get(src) !== entry) return;
13233
- cache4.set(src, { status: "ready", bitmap });
13781
+ if (cache6.get(src) !== entry) return;
13782
+ cache6.set(src, { status: "ready", bitmap });
13234
13783
  notify();
13235
13784
  },
13236
13785
  () => {
13237
- if (cache4.get(src) !== entry) return;
13238
- cache4.set(src, { status: "error" });
13786
+ if (cache6.get(src) !== entry) return;
13787
+ cache6.set(src, { status: "error" });
13239
13788
  notify();
13240
13789
  }
13241
13790
  );
13242
13791
  }
13243
13792
  function getImageBitmap(src) {
13244
- const entry = cache4.get(src);
13793
+ const entry = cache6.get(src);
13245
13794
  if (entry) return entry.bitmap;
13246
13795
  startLoad(src);
13247
13796
  return void 0;
13248
13797
  }
13249
13798
  function imageStatus(src) {
13250
- return cache4.get(src)?.status ?? "idle";
13799
+ return cache6.get(src)?.status ?? "idle";
13251
13800
  }
13252
13801
  function subscribeImageReady(cb) {
13253
13802
  subscribers.add(cb);
@@ -13348,6 +13897,13 @@ function shapeCoversPoint(node, pose, x, y, opts = {}) {
13348
13897
  function getNodeShapes() {
13349
13898
  return [...PAINTERS.high, ...PAINTERS.normal];
13350
13899
  }
13900
+ function withLeafStroke(style, stroke, strokeWidth) {
13901
+ if (style?.stroke !== void 0) return style;
13902
+ if (!stroke || stroke === "none") return style;
13903
+ const width = strokeWidth ?? 1;
13904
+ if (width <= 0) return style;
13905
+ return { ...style, stroke: { paint: { color: stroke }, width } };
13906
+ }
13351
13907
  var TEXT_PAINTER = {
13352
13908
  id: "kit:text",
13353
13909
  matches: (node) => {
@@ -13366,7 +13922,8 @@ var TEXT_PAINTER = {
13366
13922
  const d = node.data;
13367
13923
  const p = pose;
13368
13924
  const y = p.y;
13369
- return d.runs && d.runs.length > 0 ? [textCommandFromRuns(p.x, y, d.runs, d.style, void 0, p.height)] : [textCommand(p.x, y, d.text, d.style, void 0, p.height)];
13925
+ const style = withLeafStroke(d.style, d.stroke, d.strokeWidth);
13926
+ return d.runs && d.runs.length > 0 ? [textCommandFromRuns(p.x, y, d.runs, style, void 0, p.height)] : [textCommand(p.x, y, d.text, style, void 0, p.height)];
13370
13927
  }),
13371
13928
  // The pose is a *wrap box*, not a bounding box — "Away" in a 300-unit box
13372
13929
  // leaves most of it empty, and a pose-rect silhouette claims all of it. The
@@ -13424,6 +13981,17 @@ function rectsToPath(rects) {
13424
13981
  }
13425
13982
  return { kind: "polygon", commands, coords, fillRule: "nonzero" };
13426
13983
  }
13984
+ function isFillStyle(v) {
13985
+ return typeof v === "object" && v !== null;
13986
+ }
13987
+ function resolveNodeFill(fill, color, hasStroke, fallback) {
13988
+ if (isFillStyle(fill)) return fill;
13989
+ const c = fill ?? color;
13990
+ if (c === "none") return null;
13991
+ if (c !== void 0) return { color: c };
13992
+ if (hasStroke && fallback === null) return null;
13993
+ return fallback === null ? null : { color: fallback };
13994
+ }
13427
13995
  var PATH_PAINTER = {
13428
13996
  id: "kit:path",
13429
13997
  matches: (node) => {
@@ -13437,12 +14005,12 @@ var PATH_PAINTER = {
13437
14005
  const d = node.data;
13438
14006
  const projected = pathInPoseFrame(d.path, pose);
13439
14007
  const hasStroke = !!d.stroke && d.stroke !== "none" && (d.strokeWidth ?? 0) > 0;
13440
- const fillColor = d.fill ?? d.color;
13441
- const hasFill = fillColor !== "none" && (fillColor !== void 0 || !hasStroke);
14008
+ const declared = resolveNodeFill(d.fill, d.color, hasStroke, hasStroke ? null : "#888");
14009
+ const fill = declared && resolveFillPattern(fillInPoseFrame(declared, pose));
13442
14010
  const cmd = {
13443
14011
  kind: "path",
13444
14012
  path: projected,
13445
- ...hasFill ? { fill: { color: fillColor ?? "#888" } } : {},
14013
+ ...fill ? { fill } : {},
13446
14014
  ...hasStroke ? { stroke: { paint: { color: d.stroke }, width: d.strokeWidth ?? 1 } } : {}
13447
14015
  };
13448
14016
  return [cmd];
@@ -13458,9 +14026,8 @@ var PATH_PAINTER = {
13458
14026
  ink: (node) => {
13459
14027
  const d = node.data;
13460
14028
  const hasStroke = !!d.stroke && d.stroke !== "none" && (d.strokeWidth ?? 0) > 0;
13461
- const fillColor = d.fill ?? d.color;
13462
14029
  return {
13463
- filled: fillColor !== "none" && (fillColor !== void 0 || !hasStroke),
14030
+ filled: resolveNodeFill(d.fill, d.color, hasStroke, hasStroke ? null : "#888") !== null,
13464
14031
  strokeWidth: hasStroke ? d.strokeWidth ?? 1 : 0
13465
14032
  };
13466
14033
  }
@@ -13477,10 +14044,12 @@ var SHAPE_PAINTER = {
13477
14044
  paint: (node, pose) => nodeMemo(node, PAINT_SLOT, pose, () => {
13478
14045
  const d = node.data;
13479
14046
  const path = pathForShape(d, pose);
14047
+ const declaredFill = resolveNodeFill(d.fill, d.color, false, "#888");
14048
+ const shapeFill = declaredFill && resolveFillPattern(fillInPoseFrame(declaredFill, pose));
13480
14049
  return [{
13481
14050
  kind: "path",
13482
14051
  path,
13483
- fill: { color: d.fill ?? d.color ?? "#888" },
14052
+ ...shapeFill ? { fill: shapeFill } : {},
13484
14053
  ...d.stroke && (d.strokeWidth ?? 0) > 0 ? { stroke: { paint: { color: d.stroke }, width: d.strokeWidth ?? 1 } } : {}
13485
14054
  }];
13486
14055
  }),
@@ -13881,11 +14450,8 @@ function useSelectTool(adapter, options) {
13881
14450
  ...moveOpts
13882
14451
  },
13883
14452
  { spec: { kind: "drag", target: "empty" }, actionId: "areaSelect" },
13884
- // Click on empty canvas clear. Same no-affordance requirement as
13885
- // the press binding above, and for the same two reasons: clicking a
13886
- // widget that happens to float over empty canvas isn't a click on
13887
- // empty canvas, and declining in the SPEC lets the chrome's owner
13888
- // have the click instead of it being consumed here.
14453
+ // resize/rotate bind only drag (no click), so a click on either handle
14454
+ // (body genuinely empty) would hit target:'empty' and wrongly clear selection.
13889
14455
  {
13890
14456
  spec: {
13891
14457
  kind: "click",
@@ -13971,17 +14537,17 @@ function worldToLocal(xf, wx, wy) {
13971
14537
  function annulusSemiAxes(shape, view) {
13972
14538
  const bandPx = shape.minBandPx ?? 0;
13973
14539
  if (bandPx === 0) return { rx: shape.rx, ry: shape.ry };
13974
- const band = bandPx / meanScale(view.scale);
14540
+ const band = pxExtent(bandPx, view.scale);
13975
14541
  return {
13976
- rx: Math.max(shape.rx, shape.innerWidth / 2 + band),
13977
- ry: Math.max(shape.ry, shape.innerHeight / 2 + band)
14542
+ rx: Math.max(shape.rx, shape.innerWidth / 2 + band.x),
14543
+ ry: Math.max(shape.ry, shape.innerHeight / 2 + band.y)
13978
14544
  };
13979
14545
  }
13980
14546
  function hitRegion(region, wx, wy, xf, view) {
13981
14547
  const local = worldToLocal(xf, wx, wy);
13982
14548
  if (region.shape.kind === "point") {
13983
- const radiusWorld = region.shape.hitRadiusPx / meanScale(view.scale);
13984
- return Math.abs(local.x - region.shape.x) <= radiusWorld && Math.abs(local.y - region.shape.y) <= radiusWorld;
14549
+ const anchor = localToWorld(xf, region.shape.x, region.shape.y);
14550
+ return withinPxBox(anchor.x - wx, anchor.y - wy, region.shape.hitRadiusPx, view.scale);
13985
14551
  }
13986
14552
  if (region.shape.kind === "annulus") {
13987
14553
  const s = region.shape;
@@ -14047,14 +14613,13 @@ function asIsVisible(data) {
14047
14613
  function recordRegionHitbox(debug, affordanceId, region, xf, view) {
14048
14614
  if (region.shape.kind === "point") {
14049
14615
  const w2 = localToWorld(xf, region.shape.x, region.shape.y);
14050
- const r2 = region.shape.hitRadiusPx / meanScale(view.scale);
14616
+ const e = pxExtent(region.shape.hitRadiusPx, view.scale);
14051
14617
  debug.recordHitbox(affordanceId, "handle", {
14052
14618
  kind: "rect",
14053
- x: w2.x - r2,
14054
- y: w2.y - r2,
14055
- width: r2 * 2,
14056
- height: r2 * 2,
14057
- ...xf.identity ? {} : { rotation: Math.atan2(xf.sin, xf.cos) }
14619
+ x: w2.x - e.x,
14620
+ y: w2.y - e.y,
14621
+ width: e.x * 2,
14622
+ height: e.y * 2
14058
14623
  });
14059
14624
  return;
14060
14625
  }
@@ -14113,7 +14678,7 @@ function paintRegion(region, xf, view, state, viewT, out) {
14113
14678
  if (paint.kind === "annulus") {
14114
14679
  if (region.shape.kind !== "annulus") return;
14115
14680
  const s = region.shape;
14116
- const insetLocal = (paint.insetPx ?? 0) / meanScale(view.scale);
14681
+ const insetLocal = pxExtent(paint.insetPx ?? 0, view.scale);
14117
14682
  const cmd = annulusCommand(s, annulusSemiAxes(s, view), insetLocal, xf, viewT, paint.fill, paint.stroke);
14118
14683
  if (cmd) out.push(cmd);
14119
14684
  return;
@@ -14130,13 +14695,13 @@ function paintRegion(region, xf, view, state, viewT, out) {
14130
14695
  }
14131
14696
  }
14132
14697
  function annulusCommand(shape, semiAxes, insetLocal, xf, viewT, fill, stroke) {
14133
- const rx = semiAxes.rx - insetLocal;
14134
- const ry = semiAxes.ry - insetLocal;
14698
+ const rx = semiAxes.rx - insetLocal.x;
14699
+ const ry = semiAxes.ry - insetLocal.y;
14135
14700
  if (rx <= 0 || ry <= 0) return null;
14136
14701
  const cx = shape.cx;
14137
14702
  const cy = shape.cy;
14138
- const rectHW = shape.innerWidth / 2 + insetLocal;
14139
- const rectHH = shape.innerHeight / 2 + insetLocal;
14703
+ const rectHW = shape.innerWidth / 2 + insetLocal.x;
14704
+ const rectHH = shape.innerHeight / 2 + insetLocal.y;
14140
14705
  const haveTopBottom = rectHH < ry;
14141
14706
  const haveLeftRight = rectHW < rx;
14142
14707
  if (!haveTopBottom && !haveLeftRight) return null;
@@ -14397,6 +14962,7 @@ function useHandTool(opts = {}) {
14397
14962
  () => Object.assign(defineViewportTool({
14398
14963
  id: "hand",
14399
14964
  capabilities: ["navigation"],
14965
+ hotkey: "space",
14400
14966
  hookName: "useHandTool",
14401
14967
  presentation: {
14402
14968
  label: "Hand",
@@ -14521,10 +15087,6 @@ function mirrorHandle(anchor, out) {
14521
15087
  if (!out || anchor.altBroken) return void 0;
14522
15088
  return { x: 2 * anchor.x - out.x, y: 2 * anchor.y - out.y };
14523
15089
  }
14524
- function dist2(ax, ay, bx, by) {
14525
- const dx = ax - bx, dy = ay - by;
14526
- return Math.hypot(dx, dy);
14527
- }
14528
15090
  function usePenTool(options) {
14529
15091
  const { wrapPath, adapter, autoSelect = true, autoCommitOnClose = true, closeHitRadius = 8, snapPoint: snapPoint2 } = options;
14530
15092
  const scratchRef = useRef(null);
@@ -14548,11 +15110,14 @@ function usePenTool(options) {
14548
15110
  const fn = optsRef.current.snapPoint;
14549
15111
  return fn ? fn({ x, y }) : { x, y };
14550
15112
  }, []);
14551
- const closeRadius = useCallback((deps) => {
14552
- const view = deps.view?.get();
14553
- const scale2 = view?.scale ?? { x: 1, y: 1 };
14554
- return optsRef.current.closeHitRadius / meanScale(scale2);
14555
- }, []);
15113
+ const withinCloseRadius = useCallback(
15114
+ (deps, ax, ay, wx, wy) => {
15115
+ const view = deps.view?.get();
15116
+ const scale2 = view?.scale ?? { x: 1, y: 1 };
15117
+ return withinPxRadius(ax - wx, ay - wy, optsRef.current.closeHitRadius, scale2);
15118
+ },
15119
+ []
15120
+ );
14556
15121
  const actions = useMemo(() => {
14557
15122
  const s = () => scratchRef.current;
14558
15123
  return [
@@ -14568,10 +15133,9 @@ function usePenTool(options) {
14568
15133
  if (p?.pressX === void 0 || p.pressY === void 0) return;
14569
15134
  const { x: wx, y: wy } = snap2(p.pressX, p.pressY);
14570
15135
  const scratch = s();
14571
- const radius = closeRadius(deps);
14572
15136
  if (scratch.current && scratch.current.anchors.length >= 3) {
14573
15137
  const first = scratch.current.anchors[0];
14574
- if (dist2(first.x, first.y, wx, wy) <= radius) {
15138
+ if (withinCloseRadius(deps, first.x, first.y, wx, wy)) {
14575
15139
  scratch.current.closed = true;
14576
15140
  scratch.finishedSubpaths.push(scratch.current);
14577
15141
  scratch.current = null;
@@ -14690,7 +15254,7 @@ function usePenTool(options) {
14690
15254
  }
14691
15255
  }
14692
15256
  ];
14693
- }, [commit, snap2, closeRadius]);
15257
+ }, [commit, snap2, withinCloseRadius]);
14694
15258
  return useMemo(() => {
14695
15259
  return defineTool({
14696
15260
  id: "pen",
@@ -15548,7 +16112,7 @@ function buildChromeState(args) {
15548
16112
  }
15549
16113
 
15550
16114
  // src/interactions/actions/rotate/handle.ts
15551
- var DEFAULT_ROTATION_HANDLE_DISTANCE = 24;
16115
+ var DEFAULT_ROTATION_HANDLE_DISTANCE = ROTATION_HANDLE_BASE_PX;
15552
16116
  function rotationHandle(pose, distance = DEFAULT_ROTATION_HANDLE_DISTANCE) {
15553
16117
  const rotation = pose.rotation ?? 0;
15554
16118
  const c = aabbCenter(pose);
@@ -15663,7 +16227,7 @@ var DEFAULT_HANDLE_OUTLINE = {
15663
16227
  paint: { fill: "solid", color: "#1a130d" },
15664
16228
  width: 1
15665
16229
  };
15666
- var DEFAULT_HANDLE_SIZE = 8;
16230
+ var DEFAULT_HANDLE_SIZE = HANDLE_BASE_PX;
15667
16231
  function defaultHandlesOf(b) {
15668
16232
  return [
15669
16233
  { x: b.x, y: b.y },
@@ -16467,8 +17031,8 @@ function CanvasInner(props, ref) {
16467
17031
  const isVisible = getIsVisibleRef.current?.() ?? alwaysVisible;
16468
17032
  for (const layer of layers2) {
16469
17033
  if (!layer.hitTest) continue;
16470
- const binding = layer.hitTest(worldX, worldY, void 0, view, dims, isVisible);
16471
- if (binding) return { layerId: layer.id, binding };
17034
+ const hit = layer.hitTest(worldX, worldY, void 0, view, dims, isVisible);
17035
+ if (hit) return { layerId: layer.id, hit };
16472
17036
  }
16473
17037
  return null;
16474
17038
  }, []);
@@ -16904,7 +17468,7 @@ function CanvasInner(props, ref) {
16904
17468
  effectiveView,
16905
17469
  { width, height }
16906
17470
  );
16907
- renderer.render(commands);
17471
+ renderer.render(commands, viewToMat3(effectiveView));
16908
17472
  }, [layersWithDebug, width, height, effectiveView, debugSink, redrawNonce, dprProp]);
16909
17473
  const shaderIdKey = shaders?.map((h) => h.id).join("|") ?? "";
16910
17474
  useEffect(() => {
@@ -19168,8 +19732,8 @@ var DEFAULT_FILL3 = "#d4c4a8";
19168
19732
  var DEFAULT_STROKE = "#1a130d";
19169
19733
  function createCornerResizeAffordance(opts = {}) {
19170
19734
  const {
19171
- handleHitRadius = 8,
19172
- handleSize = 8,
19735
+ handleHitRadius = HANDLE_BASE_PX,
19736
+ handleSize = HANDLE_BASE_PX,
19173
19737
  fill = DEFAULT_FILL3,
19174
19738
  stroke = DEFAULT_STROKE
19175
19739
  } = opts;
@@ -19311,8 +19875,8 @@ function createPathAnchorAffordances(getAnchorState, opts = {}) {
19311
19875
  }
19312
19876
 
19313
19877
  // src/canvas/affordanceAt.ts
19314
- var HANDLE_HIT_RADIUS = 8;
19315
- var ANCHOR_HIT_RADIUS = 8;
19878
+ var HANDLE_HIT_RADIUS = HANDLE_BASE_PX;
19879
+ var ANCHOR_HIT_RADIUS = ANCHOR_HIT_BASE_PX;
19316
19880
  function buildAffordanceAt(opts) {
19317
19881
  const {
19318
19882
  getChromeState,
@@ -19344,6 +19908,8 @@ function toAffordanceHit(hit) {
19344
19908
  const scratch = hit.binding.initialScratch ?? {};
19345
19909
  return {
19346
19910
  kind: hit.region.hitKind ?? `${hit.affordanceId}:${hit.regionId}`,
19911
+ owner: hit.affordanceId,
19912
+ strength: "shared",
19347
19913
  ...scratch.targetId !== void 0 ? { targetIds: [scratch.targetId] } : {},
19348
19914
  ...scratch.anchor !== void 0 ? { anchor: scratch.anchor } : {},
19349
19915
  ...scratch.fixedPoint !== void 0 ? { fixedPoint: scratch.fixedPoint } : {},
@@ -19872,7 +20438,7 @@ function chordLengthParam(samples) {
19872
20438
  ts[0] = 0;
19873
20439
  let total = 0;
19874
20440
  for (let i = 1; i < n; i++) {
19875
- total += dist3(samples[i - 1], samples[i]);
20441
+ total += dist2(samples[i - 1], samples[i]);
19876
20442
  ts[i] = total;
19877
20443
  }
19878
20444
  if (total === 0) return null;
@@ -19905,13 +20471,13 @@ function solveLS(samples, ts, a, e, tStart, tEnd) {
19905
20471
  let alpha1;
19906
20472
  let alpha2;
19907
20473
  if (Math.abs(det) < 1e-12) {
19908
- const chord = dist3(a, e);
20474
+ const chord = dist2(a, e);
19909
20475
  alpha1 = alpha2 = chord / 3;
19910
20476
  } else {
19911
20477
  alpha1 = (c22 * x1 - c12 * x2) / det;
19912
20478
  alpha2 = (c11 * x2 - c12 * x1) / det;
19913
20479
  if (alpha1 < 1e-6 || alpha2 < 1e-6) {
19914
- const chord = dist3(a, e);
20480
+ const chord = dist2(a, e);
19915
20481
  alpha1 = alpha2 = chord / 3;
19916
20482
  }
19917
20483
  }
@@ -19947,7 +20513,7 @@ function newtonRefineT(p, t, p0, p1, p2, p3) {
19947
20513
  function sub(a, b) {
19948
20514
  return { x: a.x - b.x, y: a.y - b.y };
19949
20515
  }
19950
- function dist3(a, b) {
20516
+ function dist2(a, b) {
19951
20517
  return Math.hypot(a.x - b.x, a.y - b.y);
19952
20518
  }
19953
20519
  function unit(v) {
@@ -20584,7 +21150,7 @@ function makeGetNodeAtPoint(pickEvery, nodeResolver) {
20584
21150
  return { id, kind, pose, data };
20585
21151
  };
20586
21152
  }
20587
- var DEFAULT_HANDLE_SIZE2 = 8;
21153
+ var DEFAULT_HANDLE_SIZE2 = HANDLE_BASE_PX;
20588
21154
  var IS_DEV = (() => {
20589
21155
  try {
20590
21156
  return Boolean(import.meta.env?.DEV);
@@ -20603,10 +21169,10 @@ function recordCoordTrace(entry) {
20603
21169
  coordTrace.push(entry);
20604
21170
  if (coordTrace.length > COORD_TRACE_LIMIT) coordTrace.shift();
20605
21171
  }
20606
- function mergeLayersWithDefaults(user) {
21172
+ function mergeLayersWithDefaults(user, targetScale = 1) {
20607
21173
  const defaults = {
20608
21174
  scene: { drawOne: defaultDrawOne },
20609
- selectionOverlay: { handles: { size: DEFAULT_HANDLE_SIZE2 } }
21175
+ selectionOverlay: { handles: { size: HANDLE_BASE_PX * targetScale } }
20610
21176
  };
20611
21177
  if (!user) return defaults;
20612
21178
  const result = { ...user };
@@ -20694,8 +21260,10 @@ function SceneCanvasInner(props, ref) {
20694
21260
  chromeVisibility,
20695
21261
  getActiveMode,
20696
21262
  getFocused: getFocusedProp,
21263
+ device,
20697
21264
  ...rest
20698
21265
  } = props;
21266
+ const deviceProfile = useDeviceProfile(device);
20699
21267
  const isSerialized = (s) => typeof s === "object" && s != null && s.version === 1 && Array.isArray(s.nodes);
20700
21268
  const [bakedScene] = useState(
20701
21269
  () => isSerialized(sceneInput) ? sceneFromJSON(sceneInput, {}) : null
@@ -20813,7 +21381,7 @@ function SceneCanvasInner(props, ref) {
20813
21381
  }
20814
21382
  }), []);
20815
21383
  const selectToolWithDefaults = useMemo(() => ({
20816
- handleHitRadius: DEFAULT_HANDLE_SIZE2,
21384
+ handleHitRadius: HANDLE_BASE_PX * deviceProfile.targetScale,
20817
21385
  // Shift-click belongs to the anchor selection while a path is being
20818
21386
  // anchor-edited; see `UseSelectToolOptions.extendClickLocked`.
20819
21387
  extendClickLocked: () => effectivePathEditingId() !== "",
@@ -20823,7 +21391,7 @@ function SceneCanvasInner(props, ref) {
20823
21391
  // pipeline (audit 3.4). The classifier is now `select.pick`, which
20824
21392
  // declares that capability itself — one rule, evaluated in one place.
20825
21393
  ...selectToolOpts
20826
- }), [selectToolOpts]);
21394
+ }), [selectToolOpts, deviceProfile.targetScale]);
20827
21395
  const selectionRef = useRef(selection);
20828
21396
  selectionRef.current = selection;
20829
21397
  const kindClassifier = useMemo(() => {
@@ -20955,7 +21523,7 @@ function SceneCanvasInner(props, ref) {
20955
21523
  const getMode = getActiveModeRef.current;
20956
21524
  if (!getMode) return true;
20957
21525
  const registry2 = toolsForEligibilityRef.current?.registry;
20958
- const caps = registry2?.[toolId]?.capabilities ?? [];
21526
+ const caps = registry2?.[toolId]?.eligibility.capabilities ?? [];
20959
21527
  if (caps.length === 0) return false;
20960
21528
  const allowed = getMode().allowedCapabilities;
20961
21529
  for (const c of caps) if (allowed.has(c)) return true;
@@ -20981,8 +21549,8 @@ function SceneCanvasInner(props, ref) {
20981
21549
  };
20982
21550
  }, [Boolean(onDoubleClick)]);
20983
21551
  const mergedLayers = useMemo(
20984
- () => mergeLayersWithDefaults(layers),
20985
- [layers]
21552
+ () => mergeLayersWithDefaults(layers, deviceProfile.targetScale),
21553
+ [layers, deviceProfile.targetScale]
20986
21554
  );
20987
21555
  const resolvedActions = useMemo(() => {
20988
21556
  if (selectToolOpts?.rotate !== false) return actions;
@@ -21041,6 +21609,8 @@ function SceneCanvasInner(props, ref) {
21041
21609
  chromeVisibilityRef.current = effectiveChromeVisibility;
21042
21610
  const getActiveModeRef = useRef(getActiveMode);
21043
21611
  getActiveModeRef.current = getActiveMode;
21612
+ const deviceProfileRef = useRef(deviceProfile);
21613
+ deviceProfileRef.current = deviceProfile;
21044
21614
  const resizablePredRef = useRef(selectToolOpts?.resize?.resizable);
21045
21615
  resizablePredRef.current = selectToolOpts?.resize?.resizable;
21046
21616
  const buildCurrentRuleCtx = useCallback(() => {
@@ -21062,7 +21632,8 @@ function SceneCanvasInner(props, ref) {
21062
21632
  mode: modeInfo.id,
21063
21633
  allowedCapabilities: modeInfo.allowedCapabilities,
21064
21634
  selectionResizable,
21065
- editingAnchors: effectivePathEditingId() !== ""
21635
+ editingAnchors: effectivePathEditingId() !== "",
21636
+ device: deviceProfileRef.current
21066
21637
  };
21067
21638
  }, [dispatcher, getHover]);
21068
21639
  const anchorEditingAllowed = useMemo(
@@ -21317,7 +21888,7 @@ function SceneCanvasInner(props, ref) {
21317
21888
  testHookRef.current?._markReady();
21318
21889
  }
21319
21890
  }, []);
21320
- return /* @__PURE__ */ jsx(DepRegistryProviderIfRoot, { children: /* @__PURE__ */ jsx(PointerProviderIfRoot, { children: /* @__PURE__ */ jsxs(ActionsProviderIfRoot, { children: [
21891
+ return /* @__PURE__ */ jsx(DeviceProfileProvider, { value: device, children: /* @__PURE__ */ jsx(DepRegistryProviderIfRoot, { children: /* @__PURE__ */ jsx(PointerProviderIfRoot, { children: /* @__PURE__ */ jsxs(ActionsProviderIfRoot, { children: [
21321
21892
  canvas,
21322
21893
  /* @__PURE__ */ jsx(PointerPublisher, { canvasRef: internalCanvasRef, viewRef: currentViewRef }),
21323
21894
  /* @__PURE__ */ jsx(
@@ -21380,7 +21951,7 @@ function SceneCanvasInner(props, ref) {
21380
21951
  }
21381
21952
  ),
21382
21953
  children
21383
- ] }) }) });
21954
+ ] }) }) }) });
21384
21955
  }
21385
21956
  function ToolKeybindingsMounter({
21386
21957
  internalTools,
@@ -21426,10 +21997,6 @@ function GestureDispatcherMounter({
21426
21997
  for (const tool of tools.ambient) m.set(tool.id, tool);
21427
21998
  return m;
21428
21999
  }, [tools.registry, tools.ambient]);
21429
- const ambientToolIds = useMemo(
21430
- () => tools.ambient.map((t) => t.id),
21431
- [tools.ambient]
21432
- );
21433
22000
  const boundsOfRef = useRef(boundsOf2);
21434
22001
  boundsOfRef.current = boundsOf2;
21435
22002
  const pickEveryRef = useRef(pickEvery);
@@ -21541,9 +22108,14 @@ function GestureDispatcherMounter({
21541
22108
  const worldPoint = clientToWorld2(screenPoint.x, screenPoint.y);
21542
22109
  const extra = canvasApiRef?.current?.hitTestExtras?.(worldPoint.x, worldPoint.y);
21543
22110
  if (extra) {
22111
+ const claim = extra.hit;
21544
22112
  return {
21545
22113
  kind: `layer:${extra.layerId}`,
21546
- ...extra.binding.initialScratch !== void 0 ? { payload: extra.binding.initialScratch } : {}
22114
+ owner: extra.layerId,
22115
+ strength: claim.strength ?? "shared",
22116
+ ...claim.claimedKinds !== void 0 ? { claimedKinds: claim.claimedKinds } : {},
22117
+ ...claim.cursor !== void 0 ? { cursor: claim.cursor } : {},
22118
+ ...claim.initialScratch !== void 0 ? { payload: claim.initialScratch } : {}
21547
22119
  };
21548
22120
  }
21549
22121
  return affordanceAt ? affordanceAt(worldPoint) : null;
@@ -21563,7 +22135,6 @@ function GestureDispatcherMounter({
21563
22135
  canvasRef,
21564
22136
  actions: registry2,
21565
22137
  toolsById,
21566
- ambientToolIds,
21567
22138
  enabled,
21568
22139
  keyboard,
21569
22140
  affordanceAt: wrappedAffordanceAt,
@@ -21821,7 +22392,7 @@ function renderSceneToCanvas(args) {
21821
22392
  entry.dpr = dpr;
21822
22393
  }
21823
22394
  const commands = buildSceneViewCommands(scene, view, drawOne, extraCommands);
21824
- entry.renderer.render(commands);
22395
+ entry.renderer.render(commands, viewToMat3(view));
21825
22396
  }
21826
22397
  function SceneViewCanvasInner(props) {
21827
22398
  const { scene, view, width, height, drawOne, extraCommands, className, canvasRef } = props;
@@ -22161,7 +22732,7 @@ function renderSceneToPixels(args) {
22161
22732
  bakeBudget: Infinity
22162
22733
  });
22163
22734
  try {
22164
- renderer.render(plan.commands);
22735
+ renderer.render(plan.commands, viewToMat3(plan.view));
22165
22736
  if (typeof gl.isContextLost === "function" && gl.isContextLost()) {
22166
22737
  throw new Error("renderSceneToPixels: WebGL2 context was lost during render");
22167
22738
  }
@@ -22760,7 +23331,8 @@ var STYLE_KEYS = [
22760
23331
  "fontFamily",
22761
23332
  "fontSize",
22762
23333
  "letterSpacing",
22763
- "fill"
23334
+ "fill",
23335
+ "stroke"
22764
23336
  ];
22765
23337
  var FLAG_KEYS = /* @__PURE__ */ new Set([
22766
23338
  "bold",
@@ -22788,8 +23360,9 @@ function valueAt(run, key) {
22788
23360
  if (FLAG_KEYS.has(key)) return value ?? false;
22789
23361
  return value;
22790
23362
  }
23363
+ var STRUCTURAL_KEYS = /* @__PURE__ */ new Set(["fill", "stroke"]);
22791
23364
  function sameValue(key, a, b) {
22792
- return key === "fill" ? deepEqual(a, b) : Object.is(a, b);
23365
+ return STRUCTURAL_KEYS.has(key) ? deepEqual(a, b) : Object.is(a, b);
22793
23366
  }
22794
23367
  function sameStyle(a, b) {
22795
23368
  return STYLE_KEYS.every((key) => sameValue(key, valueAt(a, key), valueAt(b, key)));
@@ -22883,7 +23456,79 @@ function applyStyleToRange(runs, start, end, patch) {
22883
23456
  return coalesceRuns(out);
22884
23457
  }
22885
23458
 
23459
+ // src/features/text/runs/flagRange.ts
23460
+ function nodeHasFlag(style, key) {
23461
+ switch (key) {
23462
+ case "bold":
23463
+ return isBoldWeight(style.fontWeight);
23464
+ case "italic":
23465
+ return style.fontStyle === "italic";
23466
+ case "underline":
23467
+ return style.underline === true;
23468
+ case "strikethrough":
23469
+ return style.strikethrough === true;
23470
+ }
23471
+ }
23472
+ function isBoldWeight(w) {
23473
+ if (w === void 0) return false;
23474
+ if (typeof w === "number") return w >= 600;
23475
+ return w === "bold" || w === "bolder";
23476
+ }
23477
+ function weightIsExpressibleAsRunBold(w) {
23478
+ return w === 700 || w === "bold";
23479
+ }
23480
+ function clearNodeFlag(style, key) {
23481
+ const next = { ...style };
23482
+ switch (key) {
23483
+ case "bold":
23484
+ next.fontWeight = 400;
23485
+ break;
23486
+ case "italic":
23487
+ next.fontStyle = "normal";
23488
+ break;
23489
+ case "underline":
23490
+ delete next.underline;
23491
+ break;
23492
+ case "strikethrough":
23493
+ delete next.strikethrough;
23494
+ break;
23495
+ }
23496
+ return next;
23497
+ }
23498
+ function setFlagOverRange(runs, style, start, end, key, value) {
23499
+ if (value || !nodeHasFlag(style, key)) {
23500
+ return {
23501
+ runs: applyStyleToRange(runs, start, end, { [key]: value }),
23502
+ style,
23503
+ applied: true
23504
+ };
23505
+ }
23506
+ if (key === "bold" && !weightIsExpressibleAsRunBold(style.fontWeight)) {
23507
+ return { runs: [...runs], style, applied: false };
23508
+ }
23509
+ const total = runs.reduce((n, r) => n + r.text.length, 0);
23510
+ const lo = Math.max(0, start);
23511
+ const hi = Math.min(total, end);
23512
+ let next = applyStyleToRange(runs, 0, total, { [key]: true });
23513
+ if (lo < hi) next = applyStyleToRange(next, lo, hi, { [key]: false });
23514
+ return { runs: next, style: clearNodeFlag(style, key), applied: true };
23515
+ }
23516
+
22886
23517
  // src/features/text/useTextEdit.ts
23518
+ var FLAG_SHORTCUTS = [
23519
+ { key: "b", shift: false, flag: "bold" },
23520
+ { key: "i", shift: false, flag: "italic" },
23521
+ { key: "u", shift: false, flag: "underline" },
23522
+ { key: "x", shift: true, flag: "strikethrough" }
23523
+ ];
23524
+ function flagForKey(e) {
23525
+ if (!e.metaKey && !e.ctrlKey) return null;
23526
+ const key = e.key.toLowerCase();
23527
+ return FLAG_SHORTCUTS.find((s) => s.key === key && s.shift === e.shiftKey)?.flag ?? null;
23528
+ }
23529
+ function pendingKey(flag) {
23530
+ return `pending${flag[0].toUpperCase()}${flag.slice(1)}`;
23531
+ }
22887
23532
  function toggleFlagInRange(runs, start, end, flag) {
22888
23533
  const current = styleAtRange(runs, start, end)[flag];
22889
23534
  return applyStyleToRange(runs, start, end, { [flag]: current !== true });
@@ -23030,12 +23675,26 @@ function useTextEdit(opts) {
23030
23675
  togglePending(flag);
23031
23676
  return;
23032
23677
  }
23033
- const next = toggleFlagInRange(domToRuns(overlay), range2.start, range2.end, flag);
23678
+ if (editingId === null) return;
23679
+ const runs = domToRuns(overlay);
23680
+ const nodeStyle = optsRef.current.getStyle(editingId) ?? {};
23681
+ const setStyle = optsRef.current.setStyle;
23682
+ const current = styleAtRange(runs, range2.start, range2.end)[flag];
23683
+ const unsetting = current === true;
23684
+ if (unsetting && setStyle && nodeHasFlag(nodeStyle, flag)) {
23685
+ const r = setFlagOverRange(runs, nodeStyle, range2.start, range2.end, flag, false);
23686
+ if (!r.applied) return;
23687
+ setStyle(editingId, r.style);
23688
+ writeRunsPreservingSelection(overlay, r.runs, range2.start, range2.end);
23689
+ syncSelection();
23690
+ return;
23691
+ }
23692
+ const next = toggleFlagInRange(runs, range2.start, range2.end, flag);
23034
23693
  writeRunsPreservingSelection(overlay, next, range2.start, range2.end);
23035
23694
  syncSelection();
23036
23695
  }
23037
23696
  function togglePending(flag) {
23038
- const key = flag === "bold" ? "pendingBold" : "pendingItalic";
23697
+ const key = pendingKey(flag);
23039
23698
  if (overlay.dataset[key] === "1") {
23040
23699
  delete overlay.dataset[key];
23041
23700
  } else {
@@ -23053,9 +23712,9 @@ function useTextEdit(opts) {
23053
23712
  cancelEdit();
23054
23713
  return;
23055
23714
  }
23056
- if ((e.metaKey || e.ctrlKey) && (e.key === "b" || e.key === "i" || e.key === "B" || e.key === "I")) {
23715
+ const flag = flagForKey(e);
23716
+ if (flag) {
23057
23717
  e.preventDefault();
23058
- const flag = e.key.toLowerCase() === "b" ? "bold" : "italic";
23059
23718
  handleStyleToggle(flag);
23060
23719
  }
23061
23720
  };
@@ -23067,18 +23726,21 @@ function useTextEdit(opts) {
23067
23726
  };
23068
23727
  const onBeforeInput = (ie) => {
23069
23728
  if (ie.inputType !== "insertText" || !ie.data) return;
23070
- const pendingBold = overlay.dataset.pendingBold === "1";
23071
- const pendingItalic = overlay.dataset.pendingItalic === "1";
23729
+ const pending = FLAG_SHORTCUTS.map((s) => s.flag).filter((flag) => overlay.dataset[pendingKey(flag)] === "1");
23072
23730
  ie.preventDefault();
23073
23731
  const sel2 = window.getSelection();
23074
23732
  if (!sel2 || sel2.rangeCount === 0) return;
23075
23733
  const range2 = sel2.getRangeAt(0);
23076
23734
  range2.deleteContents();
23077
- if (pendingBold || pendingItalic) {
23735
+ if (pending.length > 0) {
23078
23736
  const span = document.createElement("span");
23079
23737
  span.setAttribute("data-run", "");
23080
- if (pendingBold) span.style.fontWeight = "700";
23081
- if (pendingItalic) span.style.fontStyle = "italic";
23738
+ if (pending.includes("bold")) span.style.fontWeight = "700";
23739
+ if (pending.includes("italic")) span.style.fontStyle = "italic";
23740
+ const decorations = [];
23741
+ if (pending.includes("underline")) decorations.push("underline");
23742
+ if (pending.includes("strikethrough")) decorations.push("line-through");
23743
+ if (decorations.length > 0) span.style.textDecoration = decorations.join(" ");
23082
23744
  span.textContent = ie.data;
23083
23745
  range2.insertNode(span);
23084
23746
  const after = document.createRange();
@@ -23086,8 +23748,7 @@ function useTextEdit(opts) {
23086
23748
  after.collapse(true);
23087
23749
  sel2.removeAllRanges();
23088
23750
  sel2.addRange(after);
23089
- delete overlay.dataset.pendingBold;
23090
- delete overlay.dataset.pendingItalic;
23751
+ for (const flag of pending) delete overlay.dataset[pendingKey(flag)];
23091
23752
  } else {
23092
23753
  const textNode = document.createTextNode(ie.data);
23093
23754
  range2.insertNode(textNode);
@@ -23306,6 +23967,95 @@ function useSceneTextEdit(scene, container, options = {}) {
23306
23967
  return { ...edit, onDoubleClick };
23307
23968
  }
23308
23969
 
23970
+ // src/core/gradient.ts
23971
+ function sampleGradientStops(stops, t) {
23972
+ if (stops.length === 0) return "rgba(0,0,0,0)";
23973
+ const sorted = [...stops].sort((a, b) => a.offset - b.offset);
23974
+ if (sorted.length === 1) return sorted[0].color;
23975
+ const first = sorted[0];
23976
+ const last = sorted[sorted.length - 1];
23977
+ if (t <= first.offset) return first.color;
23978
+ if (t >= last.offset) return last.color;
23979
+ for (let i = 0; i < sorted.length - 1; i++) {
23980
+ const lo = sorted[i];
23981
+ const hi = sorted[i + 1];
23982
+ if (t < lo.offset || t > hi.offset) continue;
23983
+ const span = hi.offset - lo.offset;
23984
+ if (span <= 0) return hi.color;
23985
+ return lerpColor(lo.color, hi.color, (t - lo.offset) / span);
23986
+ }
23987
+ return last.color;
23988
+ }
23989
+ function lerpColor(a, b, t) {
23990
+ const [ar, ag, ab, aa] = hexToRgba(a);
23991
+ const [br, bg, bb, ba] = hexToRgba(b);
23992
+ return rgbaToHex([
23993
+ ar + (br - ar) * t,
23994
+ ag + (bg - ag) * t,
23995
+ ab + (bb - ab) * t,
23996
+ aa + (ba - aa) * t
23997
+ ]);
23998
+ }
23999
+ function withGradientKind(fill, kind) {
24000
+ if (fill.fill === kind) return fill;
24001
+ const common = { stops: fill.stops, units: fill.units, opacity: fill.opacity };
24002
+ const { center, radius, angle } = gradientGeometry(fill);
24003
+ if (kind === "linear-gradient") {
24004
+ const dx = Math.cos(angle) * radius;
24005
+ const dy = Math.sin(angle) * radius;
24006
+ return {
24007
+ fill: "linear-gradient",
24008
+ from: { x: center.x - dx, y: center.y - dy },
24009
+ to: { x: center.x + dx, y: center.y + dy },
24010
+ ...common
24011
+ };
24012
+ }
24013
+ if (kind === "radial-gradient") {
24014
+ return { fill: "radial-gradient", center, radius, ...common };
24015
+ }
24016
+ return { fill: "conic-gradient", center, angle, ...common };
24017
+ }
24018
+ function gradientGeometry(fill) {
24019
+ if (fill.fill === "linear-gradient") {
24020
+ const dx = fill.to.x - fill.from.x;
24021
+ const dy = fill.to.y - fill.from.y;
24022
+ return {
24023
+ center: { x: (fill.from.x + fill.to.x) / 2, y: (fill.from.y + fill.to.y) / 2 },
24024
+ radius: Math.hypot(dx, dy) / 2,
24025
+ angle: Math.atan2(dy, dx)
24026
+ };
24027
+ }
24028
+ if (fill.fill === "radial-gradient") {
24029
+ return { center: fill.center, radius: fill.radius, angle: 0 };
24030
+ }
24031
+ return { center: fill.center, radius: conicArmRadius(fill.units), angle: fill.angle };
24032
+ }
24033
+ function conicArmRadius(units) {
24034
+ return units === "bounds" ? 0.5 : 50;
24035
+ }
24036
+ function gradientForBounds(kind, bounds, stops, units) {
24037
+ const center = { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
24038
+ if (kind === "linear-gradient") {
24039
+ return {
24040
+ fill: "linear-gradient",
24041
+ from: { x: bounds.x, y: center.y },
24042
+ to: { x: bounds.x + bounds.width, y: center.y },
24043
+ stops,
24044
+ units
24045
+ };
24046
+ }
24047
+ if (kind === "radial-gradient") {
24048
+ return {
24049
+ fill: "radial-gradient",
24050
+ center,
24051
+ radius: Math.max(bounds.width, bounds.height) / 2,
24052
+ stops,
24053
+ units
24054
+ };
24055
+ }
24056
+ return { fill: "conic-gradient", center, angle: 0, stops, units };
24057
+ }
24058
+
23309
24059
  // src/core/ops/setText.ts
23310
24060
  function createSetTextOp(args) {
23311
24061
  const { id, from, to, label, coalesceKey = `setText:${id}` } = args;
@@ -24370,7 +25120,7 @@ function arrayAdapter(config) {
24370
25120
  poseBounds = defaultPoseBounds2,
24371
25121
  intersectsRect,
24372
25122
  nextId = defaultNextId,
24373
- translatePose: translatePose2 = defaultTranslatePose
25123
+ translatePose: translatePose3 = defaultTranslatePose
24374
25124
  } = config;
24375
25125
  const getSelection = selectionRef ? () => selectionRef.current : () => [];
24376
25126
  const adapter = {
@@ -24446,7 +25196,7 @@ function arrayAdapter(config) {
24446
25196
  for (const n of src) {
24447
25197
  const cloned = JSON.parse(JSON.stringify(n));
24448
25198
  cloned.id = nextId();
24449
- const translated = translatePose2(toPose(cloned), dx, dy);
25199
+ const translated = translatePose3(toPose(cloned), dx, dy);
24450
25200
  out.push(fromPose(cloned, translated));
24451
25201
  }
24452
25202
  return out;
@@ -24640,8 +25390,8 @@ function bestAxis(b, axis, anchors, candidates, worldTolerance) {
24640
25390
  return { delta: bestGuide ? bestDelta : 0, guide: bestGuide };
24641
25391
  }
24642
25392
  function matchAlignment(bounds, candidates, worldTolerance, anchors) {
24643
- const x = bestAxis(bounds, "x", anchors.x, candidates, worldTolerance);
24644
- const y = bestAxis(bounds, "y", anchors.y, candidates, worldTolerance);
25393
+ const x = bestAxis(bounds, "x", anchors.x, candidates, worldTolerance.x);
25394
+ const y = bestAxis(bounds, "y", anchors.y, candidates, worldTolerance.y);
24645
25395
  return { dx: x.delta, dy: y.delta, activeX: x.guide, activeY: y.guide };
24646
25396
  }
24647
25397
 
@@ -24649,7 +25399,7 @@ function matchAlignment(bounds, candidates, worldTolerance, anchors) {
24649
25399
  var activeList = (m) => [m.activeX, m.activeY].filter((g) => g !== null);
24650
25400
  function worldTol(base) {
24651
25401
  const t = base.tolerance ?? 6;
24652
- return base.getView ? t / Math.max(1e-9, meanScale(base.getView().scale)) : t;
25402
+ return base.getView ? pxExtent(t, base.getView().scale) : { x: t, y: t };
24653
25403
  }
24654
25404
  function alignMoveBehavior(args) {
24655
25405
  const proj = args.projection ?? RECT_ALIGN_PROJECTION;
@@ -26870,6 +27620,24 @@ function snapPoint(opts) {
26870
27620
  };
26871
27621
  }
26872
27622
 
26873
- export { ALWAYS, ActionDisabledReason, ActionsProvider, ActionsProviderIfRoot, ActiveToolContextProvider, ActiveToolContextProviderIfRoot, BUNDLE_TOOLS, CURVE_REPS, ColorOverrideRegistry, CropIcon, CursorCoordsHud, DEFAULT_ALPHA_DECAY, DEFAULT_ALPHA_MIN, DEFAULT_DEBUG_THEME, 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, DivideIcon, EASINGS, EllipseIcon, ExcludeIcon, EyedropperIcon, FALLBACK_FIT_VIEW, GHOST_STROKE, 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, 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, canSendBackward, capabilityAll, capabilityIn, capabilityIs, capabilityNot, caretIndexAt, cellAt, charOffsetToDomPosition, clampView, clearSelectionAction, clientToCanvas, cloneAction, 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, 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, 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 };
26874
- //# sourceMappingURL=chunk-5F3PIERN.js.map
26875
- //# sourceMappingURL=chunk-5F3PIERN.js.map
27623
+ // src/contributions/merge.ts
27624
+ function mergeContributions(...bundles) {
27625
+ const out = [];
27626
+ const seen = /* @__PURE__ */ new Set();
27627
+ for (const bundle of bundles) {
27628
+ for (const entry of bundle) {
27629
+ if (seen.has(entry.id)) {
27630
+ throw new Error(
27631
+ `mergeContributions: duplicate contribution id "${entry.id}". Two bundles registered the same id; rename one or merge them by hand.`
27632
+ );
27633
+ }
27634
+ seen.add(entry.id);
27635
+ out.push(entry);
27636
+ }
27637
+ }
27638
+ return out;
27639
+ }
27640
+
27641
+ 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 };
27642
+ //# sourceMappingURL=chunk-D2C6K6FO.js.map
27643
+ //# sourceMappingURL=chunk-D2C6K6FO.js.map