@weasel-js/core 0.8.0 → 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 (39) hide show
  1. package/CHANGELOG.md +413 -0
  2. package/dist/{DrawCommand-BKJ5JPkC.d.ts → DrawCommand-DCmiNkBj.d.ts} +14 -169
  3. package/dist/{chunk-GLZLSPGJ.js → chunk-6SHZHXXR.js} +46 -9
  4. package/dist/chunk-6SHZHXXR.js.map +1 -0
  5. package/dist/{chunk-DLV5F4NY.js → chunk-D2C6K6FO.js} +915 -547
  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-BPRHGTLQ.js → chunk-UB6A6L77.js} +3 -3
  12. package/dist/{chunk-BPRHGTLQ.js.map → chunk-UB6A6L77.js.map} +1 -1
  13. package/dist/{chunk-CMBE45MT.js → chunk-Z3KNFTTS.js} +6 -6
  14. package/dist/chunk-Z3KNFTTS.js.map +1 -0
  15. package/dist/{index-Clf_koyR.d.ts → index-ChM3ZJ2v.d.ts} +103 -90
  16. package/dist/index.d.ts +313 -32
  17. package/dist/index.js +6 -6
  18. package/dist/insert.js +5 -5
  19. package/dist/insert.js.map +1 -1
  20. package/dist/move.js +2 -2
  21. package/dist/paint-types-CnLIzqq1.d.ts +261 -0
  22. package/dist/patterns-builtin.d.ts +57 -2
  23. package/dist/patterns-builtin.js +1 -97
  24. package/dist/patterns-builtin.js.map +1 -1
  25. package/dist/renderer.d.ts +24 -7
  26. package/dist/renderer.js +6 -6
  27. package/dist/resize.js +2 -2
  28. package/dist/routing.d.ts +4 -4
  29. package/dist/routing.js +1 -1
  30. package/dist/{viewToMat3-DFMdZvVl.d.ts → viewToMat3-BvaWCN99.d.ts} +1 -1
  31. package/package.json +6 -6
  32. package/dist/chunk-6DAUIMTO.js +0 -233
  33. package/dist/chunk-6DAUIMTO.js.map +0 -1
  34. package/dist/chunk-CMBE45MT.js.map +0 -1
  35. package/dist/chunk-DLV5F4NY.js.map +0 -1
  36. package/dist/chunk-GLZLSPGJ.js.map +0 -1
  37. package/dist/chunk-Y52N27PF.js +0 -39
  38. package/dist/chunk-Y52N27PF.js.map +0 -1
  39. package/dist/registerTexture-BzHTLhD9.d.ts +0 -25
@@ -1,17 +1,17 @@
1
- import { defineViewportTool, reportRouteConflicts, defineTool, RESERVED_ID_PREFIXES, RESERVED_ID_NAMES } from './chunk-6DAUIMTO.js';
2
- import { scratchKey, createTransformOp, snap, getScratch, createReparentOp, setScratch } from './chunk-BPRHGTLQ.js';
3
- import { fixedCornerOf, DEFAULT_RESIZE_BEHAVIORS, CORNER_ANCHORS, cornerPoint } from './chunk-CMBE45MT.js';
4
- import { AUTO_POSE_DESCRIPTOR, RECT_POSE_DESCRIPTOR, resolveUnit, meanScale, pathPoseDescriptor, polygonFromPoints, starPath, regularPolygonPath, linePath, ellipsePath, rectPath, strokeHitTest, boundsOfPath, PATH_M, PATH_L, PATH_Z, PATH_Q, PATH_C, DEFAULT_FLATTEN_TOLERANCE, flattenQuadratic, flattenCubic, PathBuilder, pointInPath, transformPath, flattenCubicWithArcLen, flattenQuadraticWithArcLen, translatePath, PATH_CMD_LENGTHS, pointToGridCell } from './chunk-GLZLSPGJ.js';
1
+ import { ActionDisabledReason, defineViewportTool, useOptionalDepRegistry, DepRegistryProvider, useActionsRegistry, ActionsProvider, useDepRegistry, defineTool, reportRouteConflicts, buildDepsFromRequires, useDepSource, actionBindings } from './chunk-DCIU3SRK.js';
2
+ import { scratchKey, createTransformOp, snap, getScratch, createReparentOp, setScratch } from './chunk-UB6A6L77.js';
3
+ import { fixedCornerOf, DEFAULT_RESIZE_BEHAVIORS, CORNER_ANCHORS, cornerPoint } from './chunk-Z3KNFTTS.js';
4
+ import { AUTO_POSE_DESCRIPTOR, RECT_POSE_DESCRIPTOR, resolveUnit, pathPoseDescriptor, polygonFromPoints, starPath, regularPolygonPath, linePath, ellipsePath, rectPath, strokeHitTest, withinPxRadius, boundsOfPath, PATH_M, PATH_L, PATH_Z, PATH_Q, PATH_C, DEFAULT_FLATTEN_TOLERANCE, flattenQuadratic, flattenCubic, PathBuilder, pointInPath, transformPath, translatePath, pxExtent, remapRotatedLeaf, withinPxBox, flattenCubicWithArcLen, flattenQuadraticWithArcLen, PATH_CMD_LENGTHS, pointToGridCell } from './chunk-6SHZHXXR.js';
5
5
  import { dispatchApplyBatch, dlog, dwarn, usePointerContext, PointerContextProvider, applyOpsTo, extractWeaselClipboardFromSvg, parseWeaselClipboardText, materializePaste, WEASEL_CLIPBOARD_MIME, WEASEL_CLIPBOARD_MIME_WEB, sniffWeaselClipboardText } from './chunk-775XXAHR.js';
6
6
  import { createSetSelectionOp } from './chunk-BHVYVFGV.js';
7
7
  import { registerOpFactory, createDeleteOp, createInsertOp, rebuildOp } from './chunk-GVCNT7UH.js';
8
- import { getTexture } from './chunk-Y52N27PF.js';
8
+ import { getTexture, chunks, dots, crosshatch, hatch } from './chunk-IO5Z75X4.js';
9
9
  import { createContext, createElement, forwardRef, useMemo, useRef, useState, useCallback, useImperativeHandle, useEffect, useSyncExternalStore, useContext, useReducer, useLayoutEffect } from 'react';
10
10
  import { subscribeGlyphReady, DEFAULT_BAKE_BUDGET, TEXT_VERT_SRC, TEXT_FRAG_SRC, TEXT_SDF_UNIFORMS, TEXT_SDF_ATTRIBUTES, TEXT_FRAG_R8_SRC, markAllFontsNotUploaded, resetBakeBudget, glyphGeneration, syncDynamicPageTexture, ensureFontTexture, dynamicPageTextureId, textureCacheKey, resolveFontVariant, resolveGlyphFallback, glyphOutline } from '@weasel-js/font';
11
11
  export { canQueryLocalFonts, canQueryLocalFonts as canQueryLocalFonts2, enableLocalFontOutlines, enableLocalFontOutlines as enableLocalFontOutlines2, hasFontOutlines, hasFontOutlines as hasFontOutlines2, isCanvasFont, isCanvasFont as isCanvasFont2, listFontOutlines, listFontOutlines as listFontOutlines2, outlineStatus, outlineStatus as outlineStatus2, registerCanvasFont, registerCanvasFont as registerCanvasFont2, registerFont, registerFont as registerFont2, registerFontOutlines, registerFontOutlines as registerFontOutlines2, subscribeGlyphReady, subscribeGlyphReady as subscribeGlyphReady2, unregisterCanvasFont, unregisterCanvasFont as unregisterCanvasFont2, unregisterFontOutlines, unregisterFontOutlines as unregisterFontOutlines2 } from '@weasel-js/font';
12
12
  import earcut from 'earcut';
13
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
14
13
  import { applyToPoint, rotateAboutPoint, boxToBox, pointInPolygon, segmentsCross } from '@weasel-js/geom';
14
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
15
15
  import polygonClipping from 'polygon-clipping';
16
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.8.0" ;
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
  };
@@ -3048,28 +3118,42 @@ function drawPathFillPattern(ctx, fill, handle) {
3048
3118
  if (isDev3) console.warn(`weasel: pattern TextureHandle "${tex.id}" not registered`);
3049
3119
  return;
3050
3120
  }
3051
- ctx.textureCache.upload(tex.id, entry.source);
3121
+ ctx.textureCache.upload(tex.id, entry.source, "repeat");
3052
3122
  const gl = ctx.gl;
3053
- gl.useProgram(ctx.imageFill.handle);
3123
+ gl.useProgram(ctx.patternFill.handle);
3054
3124
  gl.bindVertexArray(handle.vao);
3055
- setProjAndModel(ctx, ctx.imageFill);
3056
- 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);
3057
3132
  ctx.textureCache.bind(tex.id, 0);
3058
- gl.uniform1i(ctx.imageFill.uniform("u_sampler"), 0);
3059
- gl.uniform1f(ctx.imageFill.uniform("u_opacity"), fill.opacity ?? 1);
3060
- gl.uniform1f(ctx.imageFill.uniform("u_alpha"), ctx.state.alpha);
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);
3061
3136
  applyClipTest(ctx);
3062
3137
  gl.drawElements(gl.TRIANGLES, handle.indexCount, gl.UNSIGNED_INT, 0);
3063
3138
  gl.bindVertexArray(null);
3064
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
+ }
3065
3150
  function drawPathFillGradient(ctx, fill, handle) {
3066
3151
  const gl = ctx.gl;
3067
3152
  const key = ctx.gradRampCache.upload(fill.stops);
3068
3153
  gl.useProgram(ctx.gradFill.handle);
3069
3154
  gl.bindVertexArray(handle.vao);
3070
3155
  setProjAndModel(ctx, ctx.gradFill);
3071
- const identity3 = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
3072
- gl.uniformMatrix3fv(ctx.gradFill.uniform("u_worldInv"), false, identity3);
3156
+ gl.uniformMatrix3fv(ctx.gradFill.uniform("u_worldInv"), false, gradientSpaceInverse(ctx, fill.units));
3073
3157
  ctx.gradRampCache.bind(key, 0);
3074
3158
  gl.uniform1i(ctx.gradFill.uniform("u_ramp"), 0);
3075
3159
  gl.uniform1f(ctx.gradFill.uniform("u_alpha"), ctx.state.alpha);
@@ -3589,6 +3673,11 @@ function drawImage(ctx, cmd) {
3589
3673
  setProjAndModel(ctx, ctx.imageFill);
3590
3674
  setColorMatrixUniforms(ctx, ctx.imageFill);
3591
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
+ );
3592
3681
  gl.uniform1i(ctx.imageFill.uniform("u_sampler"), 0);
3593
3682
  gl.uniform1f(ctx.imageFill.uniform("u_opacity"), cmd.opacity ?? 1);
3594
3683
  gl.uniform1f(ctx.imageFill.uniform("u_alpha"), ctx.state.alpha);
@@ -3689,6 +3778,7 @@ var WeaselRenderer = class {
3689
3778
  textSdfR8;
3690
3779
  imageFill;
3691
3780
  gradFill;
3781
+ patternFill;
3692
3782
  meshCache;
3693
3783
  textureCache;
3694
3784
  imageCache;
@@ -3764,6 +3854,9 @@ var WeaselRenderer = class {
3764
3854
  this.gradFill = new ShaderProgram(this.gl, GRAD_VERT_SRC, GRAD_FRAG_SRC);
3765
3855
  this.gradFill.lookupUniforms(GRAD_FILL_UNIFORMS);
3766
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);
3767
3860
  const aPos = this.pathFill.attribute("a_position");
3768
3861
  if (aPos === void 0) throw new Error("WeaselRenderer: a_position not found in path-fill shader");
3769
3862
  this.meshCache = new GLMeshCache(this.gl, aPos);
@@ -3872,6 +3965,9 @@ var WeaselRenderer = class {
3872
3965
  this.gradFill = new ShaderProgram(this.gl, GRAD_VERT_SRC, GRAD_FRAG_SRC);
3873
3966
  this.gradFill.lookupUniforms(GRAD_FILL_UNIFORMS);
3874
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);
3875
3971
  const aPos = this.pathFill.attribute("a_position");
3876
3972
  if (aPos === void 0) throw new Error("a_position missing after restore");
3877
3973
  this.meshCache = new GLMeshCache(this.gl, aPos);
@@ -3944,7 +4040,15 @@ var WeaselRenderer = class {
3944
4040
  if (this.rectIbo) gl.deleteBuffer(this.rectIbo);
3945
4041
  if (this.rectVao) gl.deleteVertexArray(this.rectVao);
3946
4042
  }
3947
- 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) {
3948
4052
  if (this.contextLost || this.disposed) return;
3949
4053
  const gl = this.gl;
3950
4054
  this.meshCache.drainPendingDeletes();
@@ -3959,6 +4063,7 @@ var WeaselRenderer = class {
3959
4063
  textSdfR8: this.textSdfR8,
3960
4064
  imageFill: this.imageFill,
3961
4065
  gradFill: this.gradFill,
4066
+ patternFill: this.patternFill,
3962
4067
  meshCache: this.meshCache,
3963
4068
  textureCache: this.textureCache,
3964
4069
  imageCache: this.imageCache,
@@ -3973,7 +4078,8 @@ var WeaselRenderer = class {
3973
4078
  heightCss: this.heightCss,
3974
4079
  clipDepth: 0,
3975
4080
  flattenTolerance: this.flattenTolerance,
3976
- textOutlineMinScreenSize: this.textOutlineMinScreenSize
4081
+ textOutlineMinScreenSize: this.textOutlineMinScreenSize,
4082
+ viewMatrix
3977
4083
  };
3978
4084
  for (const cmd of commands) dispatch(ctx, cmd);
3979
4085
  this.meshCache.freeTransient();
@@ -4021,6 +4127,10 @@ var WeaselRenderer = class {
4021
4127
  return this.gradFill;
4022
4128
  }
4023
4129
  /** @internal */
4130
+ _patternFill() {
4131
+ return this.patternFill;
4132
+ }
4133
+ /** @internal */
4024
4134
  _meshCache() {
4025
4135
  return this.meshCache;
4026
4136
  }
@@ -4086,6 +4196,11 @@ function alignedStrokeRect(rect, align, width = 1) {
4086
4196
  };
4087
4197
  }
4088
4198
 
4199
+ // src/core/viewport/meanScale.ts
4200
+ function meanScale(s) {
4201
+ return Math.sqrt(s.x * s.y);
4202
+ }
4203
+
4089
4204
  // src/features/grid/layer.ts
4090
4205
  var DEFAULT_LINE = {
4091
4206
  paint: { fill: "solid", color: "rgba(255, 255, 255, 0.08)" },
@@ -5224,254 +5339,6 @@ function matchesKeyBinding(e, b) {
5224
5339
  }
5225
5340
  return true;
5226
5341
  }
5227
- var DepRegistryContext = createContext(null);
5228
- function DepRegistryProvider({ children }) {
5229
- const sourcesRef = useRef(/* @__PURE__ */ new Map());
5230
- const registry2 = useMemo(() => ({
5231
- register: (name, source) => {
5232
- sourcesRef.current.set(name, source);
5233
- return () => {
5234
- sourcesRef.current.delete(name);
5235
- };
5236
- },
5237
- get: (name) => sourcesRef.current.get(name)?.()
5238
- }), []);
5239
- return /* @__PURE__ */ jsx(DepRegistryContext.Provider, { value: registry2, children });
5240
- }
5241
- function useDepRegistry() {
5242
- const r = useContext(DepRegistryContext);
5243
- if (r === null) {
5244
- throw new Error("useDepRegistry: no DepRegistryProvider in scope. Wrap your tree with <DepRegistryProvider> (typically inside <SceneCanvas>).");
5245
- }
5246
- return r;
5247
- }
5248
- function useOptionalDepRegistry() {
5249
- return useContext(DepRegistryContext);
5250
- }
5251
- function useDepSource(name, source) {
5252
- const registry2 = useDepRegistry();
5253
- const sourceRef = useRef(source);
5254
- sourceRef.current = source;
5255
- useEffect(() => {
5256
- return registry2.register(name, () => sourceRef.current());
5257
- }, [name, registry2]);
5258
- }
5259
-
5260
- // src/interactions/actions/buildDeps.ts
5261
- var DEV = (() => {
5262
- try {
5263
- return Boolean(import.meta.env?.DEV);
5264
- } catch {
5265
- return false;
5266
- }
5267
- })();
5268
- function buildDepsFromRequires(action, depRegistry) {
5269
- const requires = action.requires ?? [];
5270
- const entries = requires.map((name) => [name, depRegistry.get(name)]);
5271
- const deps = Object.fromEntries(entries);
5272
- if (DEV) {
5273
- const declared = new Set(requires);
5274
- return new Proxy(deps, {
5275
- get(target, prop, receiver) {
5276
- if (typeof prop === "string" && !declared.has(prop) && prop !== "then") {
5277
- const message = `[weasel:dispatcher] action "${action.id}" read deps.${prop} but did not declare it in \`requires\`. Add \`requires: [...'${prop}']\` to the descriptor or the dep will be undefined at runtime.`;
5278
- console.warn(message);
5279
- throw new Error(message);
5280
- }
5281
- return Reflect.get(target, prop, receiver);
5282
- }
5283
- });
5284
- }
5285
- return deps;
5286
- }
5287
- var ActionDisabledReason = {
5288
- SelectionRequired: "selection-required",
5289
- SceneEmpty: "scene-empty",
5290
- NotApplicable: "not-applicable",
5291
- /** Sentinel: the predicate threw. Surfaced by `evaluateEnabled`'s catch. */
5292
- PredicateThrew: "predicate-threw"
5293
- };
5294
- var enabledSlowWarned = /* @__PURE__ */ new Set();
5295
- var enabledThrewWarned = /* @__PURE__ */ new Set();
5296
- var ENABLED_BUDGET_MS = 4;
5297
- function evaluateEnabled(action, deps) {
5298
- if (!action.enabled) return { enabled: true };
5299
- const isDev3 = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
5300
- const now = typeof performance !== "undefined" && typeof performance.now === "function" ? () => performance.now() : () => Date.now();
5301
- const start = isDev3 ? now() : 0;
5302
- try {
5303
- const result = action.enabled(deps);
5304
- if (isDev3) {
5305
- const elapsed = now() - start;
5306
- if (elapsed > ENABLED_BUDGET_MS && !enabledSlowWarned.has(action.id)) {
5307
- enabledSlowWarned.add(action.id);
5308
- console.warn(
5309
- `weasel actions: enabled() for action "${action.id}" took ${elapsed.toFixed(2)}ms (budget ${ENABLED_BUDGET_MS}ms). The predicate must be pure and fast \u2014 see Action.enabled JSDoc.`
5310
- );
5311
- }
5312
- }
5313
- if (result === true) return { enabled: true };
5314
- return { enabled: false, reason: result };
5315
- } catch (e) {
5316
- if (isDev3 && !enabledThrewWarned.has(action.id)) {
5317
- enabledThrewWarned.add(action.id);
5318
- console.warn(
5319
- `weasel actions: enabled() for action "${action.id}" threw; treating as disabled. The predicate must not throw \u2014 see Action.enabled JSDoc.`,
5320
- e
5321
- );
5322
- }
5323
- return { enabled: false, reason: ActionDisabledReason.PredicateThrew };
5324
- }
5325
- }
5326
- function validateActionId(id) {
5327
- if (id.length === 0) {
5328
- throw new Error(`weasel: action id may not be empty`);
5329
- }
5330
- if (RESERVED_ID_PREFIXES.has(id[0])) {
5331
- throw new Error(
5332
- `weasel: action id "${id}" starts with reserved sigil "${id[0]}" (reserved set: ${[...RESERVED_ID_PREFIXES].join(" ")})`
5333
- );
5334
- }
5335
- if (RESERVED_ID_NAMES.has(id)) {
5336
- throw new Error(
5337
- `weasel: action id "${id}" collides with a reserved phase keyword (reserved: ${[...RESERVED_ID_NAMES].join(", ")})`
5338
- );
5339
- }
5340
- }
5341
- function validateActionDefaultBinding(action) {
5342
- const gs = action.defaultBinding;
5343
- if (!gs) return;
5344
- const entries = Array.isArray(gs) ? gs : [gs];
5345
- for (const entry of entries) {
5346
- const spec = "kind" in entry ? entry : entry.spec;
5347
- const phase = spec.phase;
5348
- if (phase === void 0) continue;
5349
- const atoms = Array.isArray(phase) ? phase : [{ channel: "&", phase }];
5350
- for (const atom of atoms) {
5351
- if (atom.channel === "&") {
5352
- throw new Error(
5353
- `weasel: action "${action.id}" defaultBinding uses '&' channel which has no owning tool to resolve to. Use a named tool channel (e.g. '[rect:engaged]') or '*' instead, or move this binding to the tool's own Tool.bindings.`
5354
- );
5355
- }
5356
- }
5357
- }
5358
- }
5359
- var ActionsContext = createContext(null);
5360
- function ActionsProvider({ children }) {
5361
- const actionsRef = useRef(/* @__PURE__ */ new Map());
5362
- const versionRef = useRef(0);
5363
- const cachedRef = useRef([]);
5364
- const cachedVerRef = useRef(-1);
5365
- const listenersRef = useRef(/* @__PURE__ */ new Set());
5366
- const depReg = useOptionalDepRegistry();
5367
- const depRegRef = useRef(depReg);
5368
- depRegRef.current = depReg;
5369
- const wiredDepRegRef = useRef(null);
5370
- const dispatcherRef = useRef(null);
5371
- const registry2 = useMemo(() => {
5372
- const snapshot = () => {
5373
- const v = versionRef.current;
5374
- if (cachedVerRef.current === v) return cachedRef.current;
5375
- const out = Object.freeze(Array.from(actionsRef.current.values()));
5376
- cachedRef.current = out;
5377
- cachedVerRef.current = v;
5378
- return out;
5379
- };
5380
- const notify2 = () => {
5381
- for (const l of listenersRef.current) {
5382
- try {
5383
- l();
5384
- } catch (err) {
5385
- console.error("weasel ActionsRegistry: subscriber threw", err);
5386
- }
5387
- }
5388
- };
5389
- return {
5390
- register: (action) => {
5391
- validateActionId(action.id);
5392
- validateActionDefaultBinding(action);
5393
- actionsRef.current.set(action.id, action);
5394
- versionRef.current++;
5395
- notify2();
5396
- return () => {
5397
- const cur = actionsRef.current.get(action.id);
5398
- if (cur === action) {
5399
- actionsRef.current.delete(action.id);
5400
- versionRef.current++;
5401
- notify2();
5402
- }
5403
- };
5404
- },
5405
- unregister: (id) => {
5406
- if (actionsRef.current.delete(id)) {
5407
- versionRef.current++;
5408
- notify2();
5409
- }
5410
- },
5411
- list: () => snapshot(),
5412
- trigger: (id, params) => {
5413
- const a = actionsRef.current.get(id);
5414
- if (!a) return false;
5415
- try {
5416
- if (a.invoker && a.invoker.timing === "immediate") {
5417
- const r = wiredDepRegRef.current ?? depRegRef.current;
5418
- const deps = !r ? {} : a.requires ? buildDepsFromRequires(a, r) : {
5419
- selection: r.get("selection"),
5420
- scene: r.get("scene"),
5421
- history: r.get("history"),
5422
- view: r.get("view"),
5423
- pointer: r.get("pointer"),
5424
- activeTool: r.get("activeTool"),
5425
- booleansAdapter: r.get("booleansAdapter")
5426
- };
5427
- a.invoker.run(deps, params);
5428
- }
5429
- } catch (err) {
5430
- console.error(`weasel ActionsRegistry: action "${id}" threw`, err);
5431
- }
5432
- return true;
5433
- },
5434
- subscribe: (listener) => {
5435
- listenersRef.current.add(listener);
5436
- return () => {
5437
- listenersRef.current.delete(listener);
5438
- };
5439
- },
5440
- setDispatcher: (d) => {
5441
- dispatcherRef.current = d;
5442
- },
5443
- setDepRegistry: (r) => {
5444
- wiredDepRegRef.current = r;
5445
- },
5446
- begin: (id, params) => {
5447
- const disp = dispatcherRef.current;
5448
- if (!disp) return null;
5449
- const r = wiredDepRegRef.current ?? depRegRef.current;
5450
- const deps = r ? {
5451
- selection: r.get("selection"),
5452
- scene: r.get("scene"),
5453
- history: r.get("history"),
5454
- view: r.get("view"),
5455
- pointer: r.get("pointer"),
5456
- activeTool: r.get("activeTool"),
5457
- booleansAdapter: r.get("booleansAdapter")
5458
- } : {};
5459
- return disp.beginUiOngoing(id, deps, params);
5460
- }
5461
- };
5462
- }, []);
5463
- return /* @__PURE__ */ jsx(ActionsContext.Provider, { value: registry2, children });
5464
- }
5465
- function useActionsRegistry() {
5466
- return useContext(ActionsContext);
5467
- }
5468
- function useAction(action) {
5469
- const reg = useActionsRegistry();
5470
- useEffect(() => {
5471
- if (!reg) return;
5472
- return reg.register(action);
5473
- }, [reg, action]);
5474
- }
5475
5342
 
5476
5343
  // src/interactions/actions/invoker.ts
5477
5344
  function resolveParams(params) {
@@ -6044,6 +5911,62 @@ var moveAction = {
6044
5911
  enabled: () => true
6045
5912
  };
6046
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
+
6047
5970
  // src/interactions/actions/rotate/geometry.ts
6048
5971
  function aabbCenter(bounds) {
6049
5972
  return { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
@@ -6360,6 +6283,13 @@ var resizeAction = {
6360
6283
  const computePose = (id) => {
6361
6284
  const sp = scratch.startPoses.get(id);
6362
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
+ }
6363
6293
  return scratch.geometry.remapBounds(sp, scratch.originBounds, proposedBounds);
6364
6294
  };
6365
6295
  scratch.previews.clear();
@@ -7217,7 +7147,13 @@ function makeViewportZoomAction(opts = {}) {
7217
7147
  var viewportZoomAction = makeViewportZoomAction();
7218
7148
 
7219
7149
  // src/interactions/dispatcher/predicates.ts
7220
- 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");
7221
7157
  function kindOf(target) {
7222
7158
  if (typeof target !== "object" || target === null) return void 0;
7223
7159
  const k = target.kind;
@@ -7258,17 +7194,17 @@ function splitCubicAtT(p0, p1, p2, p3, t) {
7258
7194
  function fitCubicThroughDeletion(prev, next) {
7259
7195
  const dx = next.x - prev.x;
7260
7196
  const dy = next.y - prev.y;
7261
- const dist4 = Math.sqrt(dx * dx + dy * dy);
7197
+ const dist3 = Math.sqrt(dx * dx + dy * dy);
7262
7198
  const c1DirX = prev.outHandle ? prev.outHandle.x - prev.x : dx;
7263
7199
  const c1DirY = prev.outHandle ? prev.outHandle.y - prev.y : dy;
7264
- const c1 = controlOnSide(prev, c1DirX, c1DirY, dist4, 1 / 3);
7200
+ const c1 = controlOnSide(prev, c1DirX, c1DirY, dist3, 1 / 3);
7265
7201
  const c2DirX = next.inHandle ? next.inHandle.x - next.x : -dx;
7266
7202
  const c2DirY = next.inHandle ? next.inHandle.y - next.y : -dy;
7267
- const c2 = controlOnSide(next, c2DirX, c2DirY, dist4, 1 / 3);
7203
+ const c2 = controlOnSide(next, c2DirX, c2DirY, dist3, 1 / 3);
7268
7204
  return { c1, c2 };
7269
7205
  }
7270
- function controlOnSide(anchor, dirX, dirY, dist4, fraction) {
7271
- const targetDist = dist4 * fraction;
7206
+ function controlOnSide(anchor, dirX, dirY, dist3, fraction) {
7207
+ const targetDist = dist3 * fraction;
7272
7208
  const len = Math.hypot(dirX, dirY) || 1;
7273
7209
  const ux = dirX / len, uy = dirY / len;
7274
7210
  return { x: anchor.x + ux * targetDist, y: anchor.y + uy * targetDist };
@@ -7539,64 +7475,6 @@ function anchorsInRect(set, rect) {
7539
7475
  }
7540
7476
  return hits;
7541
7477
  }
7542
-
7543
- // src/features/paths/poseRotation.ts
7544
- function poseRotationOf(pose) {
7545
- const p = pose;
7546
- if (p.rotation && p.x != null && p.y != null && p.width != null && p.height != null) {
7547
- return { cx: p.x + p.width / 2, cy: p.y + p.height / 2, rotation: p.rotation };
7548
- }
7549
- return null;
7550
- }
7551
- function rotatePathAround(path, cx, cy, rotation) {
7552
- if (path.kind === "rect") {
7553
- return rotateRectCorners(path.x, path.y, path.width, path.height, cx, cy, rotation);
7554
- }
7555
- return rotatePolygon(path, cx, cy, rotation);
7556
- }
7557
- function rotatePolygon(path, cx, cy, rotation) {
7558
- const cos = Math.cos(rotation);
7559
- const sin = Math.sin(rotation);
7560
- const { commands, coords } = path;
7561
- const next = new Float32Array(coords.length);
7562
- let ci = 0;
7563
- for (let i = 0; i < commands.length; i++) {
7564
- const len = COORD_COUNT[commands[i]];
7565
- for (let k = 0; k < len; k += 2) {
7566
- const dx = coords[ci + k] - cx;
7567
- const dy = coords[ci + k + 1] - cy;
7568
- next[ci + k] = cx + dx * cos - dy * sin;
7569
- next[ci + k + 1] = cy + dx * sin + dy * cos;
7570
- }
7571
- ci += len;
7572
- }
7573
- return { kind: "polygon", commands: path.commands, coords: next, fillRule: path.fillRule };
7574
- }
7575
- function rotateRectCorners(x, y, w, h, cx, cy, rotation) {
7576
- const cos = Math.cos(rotation);
7577
- const sin = Math.sin(rotation);
7578
- const rotate = (px, py) => {
7579
- const dx = px - cx;
7580
- const dy = py - cy;
7581
- return [cx + dx * cos - dy * sin, cy + dx * sin + dy * cos];
7582
- };
7583
- const [x0, y0] = rotate(x, y);
7584
- const [x1, y1] = rotate(x + w, y);
7585
- const [x2, y2] = rotate(x + w, y + h);
7586
- const [x3, y3] = rotate(x, y + h);
7587
- const commands = new Uint8Array([PATH_M, PATH_L, PATH_L, PATH_L, PATH_Z]);
7588
- const coords = new Float32Array([x0, y0, x1, y1, x2, y2, x3, y3]);
7589
- return { kind: "polygon", commands, coords, fillRule: "nonzero" };
7590
- }
7591
- var COORD_COUNT = {
7592
- [PATH_M]: 2,
7593
- [PATH_L]: 2,
7594
- [PATH_C]: 6,
7595
- [PATH_Q]: 4,
7596
- [PATH_Z]: 0
7597
- };
7598
-
7599
- // src/features/paths/pathInWorld.ts
7600
7478
  function pathInPoseFrame(path, pose) {
7601
7479
  if (path.kind === "rect") {
7602
7480
  if (path.x === pose.x && path.y === pose.y && path.width === pose.width && path.height === pose.height) return path;
@@ -9281,7 +9159,7 @@ var LABEL = {
9281
9159
  divide: "Divide",
9282
9160
  crop: "Crop"
9283
9161
  };
9284
- function isEmpty(p) {
9162
+ function isEmpty2(p) {
9285
9163
  return p.commands.length === 0;
9286
9164
  }
9287
9165
  function applyBooleanOp(adapter, op) {
@@ -9319,7 +9197,7 @@ function applyBooleanOp(adapter, op) {
9319
9197
  break;
9320
9198
  }
9321
9199
  }
9322
- results = results.filter((p) => !isEmpty(p));
9200
+ results = results.filter((p) => !isEmpty2(p));
9323
9201
  if (results.length === 0) return { kind: "noop", reason: "empty-result" };
9324
9202
  const topmostId = entries[entries.length - 1].id;
9325
9203
  const topAnchor = adapter.getZOrder?.(topmostId);
@@ -9870,14 +9748,24 @@ var DEFAULT_PALETTE = [
9870
9748
  var GHOST_STROKE = DEFAULT_PALETTE[0];
9871
9749
 
9872
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
+ }
9873
9756
  function refreshPreviews(scratch) {
9874
9757
  scratch.previews.clear();
9875
9758
  for (const id of scratch.ids) {
9876
9759
  const prev = scratch.startData.get(id);
9877
- const next = mergeAlphaFromPrev(scratch.currentColor, prev?.fill ?? DEFAULT_FILL_COLOR);
9878
- scratch.previews.set(id, { ...prev ?? {}, fill: next });
9760
+ scratch.previews.set(id, { ...prev ?? {}, fill: resolveFill(scratch, prev) });
9879
9761
  }
9880
9762
  }
9763
+ function readParams(params) {
9764
+ return {
9765
+ color: params?.color,
9766
+ paint: params?.paint
9767
+ };
9768
+ }
9881
9769
  var setFillAction = {
9882
9770
  id: "setFill",
9883
9771
  label: "Set fill",
@@ -9892,9 +9780,10 @@ var setFillAction = {
9892
9780
  if (!selection || !scene) return {};
9893
9781
  const ids = selection.get();
9894
9782
  if (ids.length === 0) return {};
9895
- const ctxColor = ctx.params?.color;
9896
- const optsColor = opts?.params?.color;
9897
- const initialColor = ctxColor ?? optsColor ?? DEFAULT_STROKE_COLOR;
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;
9898
9787
  const startData = /* @__PURE__ */ new Map();
9899
9788
  for (const id of ids) {
9900
9789
  const node = scene.get(id);
@@ -9905,15 +9794,20 @@ var setFillAction = {
9905
9794
  scene,
9906
9795
  startData,
9907
9796
  currentColor: initialColor,
9797
+ currentPaint: initialPaint,
9908
9798
  previews: /* @__PURE__ */ new Map(),
9909
9799
  applyOps
9910
9800
  };
9911
9801
  refreshPreviews(scratch);
9912
9802
  return {
9913
9803
  onMove(moveCtx) {
9914
- const next = moveCtx.params?.color;
9915
- if (next === void 0) return;
9916
- 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
+ }
9917
9811
  refreshPreviews(scratch);
9918
9812
  },
9919
9813
  onEnd(_endCtx, reason) {
@@ -9923,8 +9817,7 @@ var setFillAction = {
9923
9817
  }
9924
9818
  const ops = [];
9925
9819
  for (const id of scratch.ids) {
9926
- const prev = scratch.startData.get(id);
9927
- const merged = mergeAlphaFromPrev(scratch.currentColor, prev?.fill ?? DEFAULT_FILL_COLOR);
9820
+ const merged = resolveFill(scratch, scratch.startData.get(id));
9928
9821
  const nodeNow = scratch.scene.get(id);
9929
9822
  if (!nodeNow) continue;
9930
9823
  const from = { ...nodeNow.data };
@@ -10818,6 +10711,54 @@ function targetRank(target) {
10818
10711
  if (target.startsWith("affordance:")) return 2;
10819
10712
  return 1;
10820
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
+ }
10821
10762
  function specificity(spec) {
10822
10763
  const t = targetRank("target" in spec ? spec.target : void 0);
10823
10764
  const mods = "mods" in spec ? spec.mods : void 0;
@@ -10834,12 +10775,14 @@ function compareSpecificity(a, b) {
10834
10775
  }
10835
10776
  return 0;
10836
10777
  }
10837
- function matchSorted(e, bindings, isMac, engagedChannels) {
10778
+ function matchSorted(e, bindings, isMac, engagedChannels, warn = (m) => console.warn(m)) {
10838
10779
  const out = [];
10839
10780
  const engaged = engagedChannels ?? EMPTY_ENGAGED;
10781
+ const exclusive = isExclusiveClaim(e);
10782
+ const pool = exclusive ? bindings.filter((sb) => targetConsultsAffordance(specTargetOf(sb.binding.spec))) : bindings;
10840
10783
  for (const scope of SCOPE_PRIORITY) {
10841
10784
  const scopeMatches = [];
10842
- for (const sb of bindings) {
10785
+ for (const sb of pool) {
10843
10786
  if (sb.scope !== scope) continue;
10844
10787
  const phaseCtx = { selfChannel: sb.ownerToolId, engagedChannels: engaged };
10845
10788
  if (matchSpec(e, sb.binding.spec, isMac, phaseCtx)) {
@@ -10849,10 +10792,37 @@ function matchSorted(e, bindings, isMac, engagedChannels) {
10849
10792
  scopeMatches.sort((a, b) => compareSpecificity(a.binding.spec, b.binding.spec));
10850
10793
  out.push(...scopeMatches);
10851
10794
  }
10795
+ if (exclusive && out.length === 0 && bindings.length > 0) {
10796
+ reportDeadClaim(claimOf(e)?.owner, warn);
10797
+ }
10852
10798
  return out;
10853
10799
  }
10854
10800
  var EMPTY_ENGAGED = /* @__PURE__ */ new Set();
10855
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
+ }
10822
+ }
10823
+ return out;
10824
+ }
10825
+
10856
10826
  // src/features/chrome-caps/rule.ts
10857
10827
  var ALWAYS = { all: [] };
10858
10828
  var NEVER = { any: [] };
@@ -11157,7 +11127,7 @@ function useHoverTracking(args) {
11157
11127
  function pointerGestureId(pointerId) {
11158
11128
  return `pointer-${pointerId ?? "mouse"}`;
11159
11129
  }
11160
- var DEV2 = (() => {
11130
+ var DEV = (() => {
11161
11131
  try {
11162
11132
  return Boolean(import.meta.env?.DEV);
11163
11133
  } catch {
@@ -11165,9 +11135,9 @@ var DEV2 = (() => {
11165
11135
  }
11166
11136
  })();
11167
11137
  var TRACE_LIMIT = 200;
11168
- var traceLog = DEV2 && typeof window !== "undefined" ? window.__weaselDispatchLog__ ??= [] : [];
11138
+ var traceLog = DEV && typeof window !== "undefined" ? window.__weaselDispatchLog__ ??= [] : [];
11169
11139
  function recordTrace(entry) {
11170
- if (!DEV2) return;
11140
+ if (!DEV) return;
11171
11141
  traceLog.push(entry);
11172
11142
  if (traceLog.length > TRACE_LIMIT) traceLog.shift();
11173
11143
  }
@@ -11198,6 +11168,7 @@ function filterEligible(matches, actionLookup, ruleCtx) {
11198
11168
  return isEligible(action, ruleCtx);
11199
11169
  });
11200
11170
  }
11171
+ var EMPTY_TRIGGERS = /* @__PURE__ */ new Set();
11201
11172
  var EMPTY_ENGAGED2 = /* @__PURE__ */ new Set();
11202
11173
  function modifiersOf(e) {
11203
11174
  return { alt: e.altKey, ctrl: e.ctrlKey, meta: e.metaKey, shift: e.shiftKey };
@@ -11305,34 +11276,23 @@ function createDispatcher(opts) {
11305
11276
  return `ongoing-${event.kind}`;
11306
11277
  }
11307
11278
  function assembleScopedBindings(ctx) {
11308
- const result = [];
11309
- for (const toolId of ctx.hotkeyStack) {
11310
- const tool = ctx.toolsById.get(toolId);
11311
- for (const binding of tool?.bindings ?? []) {
11312
- result.push({ binding, scope: "hotkey", ownerToolId: toolId });
11313
- }
11314
- }
11315
- const activeTool = ctx.toolsById.get(ctx.activeToolId);
11316
- for (const binding of activeTool?.bindings ?? []) {
11317
- result.push({ binding, scope: "active", ownerToolId: ctx.activeToolId });
11318
- }
11319
- for (const toolId of ctx.ambientToolIds ?? []) {
11320
- const tool = ctx.toolsById.get(toolId);
11321
- for (const binding of tool?.bindings ?? []) {
11322
- result.push({ binding, scope: "ambient", ownerToolId: toolId });
11323
- }
11324
- }
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
+ });
11325
11292
  for (const action of ctx.actions.list()) {
11326
- const gs = action.defaultBinding;
11327
- if (!gs) continue;
11328
- const raw = Array.isArray(gs) ? gs : [gs];
11329
- for (const entry of raw) {
11330
- const isBoundObj = !("kind" in entry);
11331
- const spec = isBoundObj ? entry.spec : entry;
11332
- const opts2 = isBoundObj ? entry.opts : void 0;
11333
- const defaultBinding = { spec, actionId: action.id, ...opts2 !== void 0 ? { opts: opts2 } : {} };
11334
- const targetScope = action.scope === "hotkey" ? "hotkey" : "ambient";
11335
- result.push({ binding: defaultBinding, scope: targetScope, ownerToolId: null });
11293
+ const targetScope = action.scope === "hotkey" ? "hotkey" : "ambient";
11294
+ for (const binding of actionBindings(action)) {
11295
+ result.push({ binding, scope: targetScope, ownerToolId: null });
11336
11296
  }
11337
11297
  }
11338
11298
  return result;
@@ -11487,15 +11447,26 @@ function createDispatcher(opts) {
11487
11447
  deltaY: event.deltaY,
11488
11448
  clientX: event.clientX,
11489
11449
  clientY: event.clientY,
11450
+ affordance: event.affordance,
11490
11451
  ...resolved
11491
11452
  };
11492
11453
  } else if (event.kind === "click" || event.kind === "doubleclick") {
11493
11454
  params = {
11494
11455
  worldX: event.worldX,
11495
11456
  worldY: event.worldY,
11457
+ affordance: event.affordance,
11496
11458
  // Press point as well as release point — an action that places
11497
11459
  // geometry at the click wants the former. See `ClickEvent`.
11498
- ...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,
11499
11470
  mods: modifiersOf(event),
11500
11471
  ...resolved
11501
11472
  };
@@ -11867,7 +11838,7 @@ function computeMultiTouchGeometry(positions) {
11867
11838
  return { centroid, spread };
11868
11839
  }
11869
11840
  function useGestureDispatcher(opts) {
11870
- 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;
11871
11842
  const onDoubleClickRef = useRef(onDoubleClick);
11872
11843
  onDoubleClickRef.current = onDoubleClick;
11873
11844
  const activeTool = useActiveToolContext();
@@ -11882,7 +11853,6 @@ function useGestureDispatcher(opts) {
11882
11853
  activeToolId: activeTool.active,
11883
11854
  hotkeyStack: activeTool.hotkeyStack,
11884
11855
  toolsById,
11885
- ambientToolIds,
11886
11856
  isMac: IS_MAC,
11887
11857
  getRuleCtx
11888
11858
  });
@@ -11892,7 +11862,6 @@ function useGestureDispatcher(opts) {
11892
11862
  activeToolId: activeTool.active,
11893
11863
  hotkeyStack: activeTool.hotkeyStack,
11894
11864
  toolsById,
11895
- ambientToolIds,
11896
11865
  isMac: IS_MAC,
11897
11866
  getRuleCtx
11898
11867
  };
@@ -11970,6 +11939,7 @@ function useGestureDispatcher(opts) {
11970
11939
  ctrlKey: down.ctrlKey,
11971
11940
  metaKey: down.metaKey,
11972
11941
  shiftKey: down.shiftKey,
11942
+ ...down.affordance !== void 0 ? { affordance: down.affordance } : {},
11973
11943
  ...down.bodyTarget !== void 0 ? { bodyTarget: down.bodyTarget } : {},
11974
11944
  ...down.bodyKind !== void 0 ? { bodyKind: down.bodyKind } : {}
11975
11945
  };
@@ -11979,11 +11949,15 @@ function useGestureDispatcher(opts) {
11979
11949
  y: down.worldY,
11980
11950
  clientX: down.clientX,
11981
11951
  clientY: down.clientY,
11982
- ...down.affordance !== void 0 ? { affordance: down.affordance } : {},
11983
11952
  ...shared
11984
11953
  });
11985
11954
  if (result === "unhandled") {
11986
- dispatch2({ kind: "contextmenu", ...shared });
11955
+ dispatch2({
11956
+ kind: "contextmenu",
11957
+ worldX: down.worldX,
11958
+ worldY: down.worldY,
11959
+ ...shared
11960
+ });
11987
11961
  }
11988
11962
  };
11989
11963
  const DOUBLE_CLICK_MAX_MS = 600;
@@ -12038,6 +12012,9 @@ function useGestureDispatcher(opts) {
12038
12012
  const onWheel = (e) => {
12039
12013
  const rect = canvas?.getBoundingClientRect();
12040
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);
12041
12018
  const ev = {
12042
12019
  kind: "wheel",
12043
12020
  altKey: e.altKey,
@@ -12047,7 +12024,10 @@ function useGestureDispatcher(opts) {
12047
12024
  deltaX: e.deltaX,
12048
12025
  deltaY: e.deltaY,
12049
12026
  clientX: localX,
12050
- 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 } : {}
12051
12031
  };
12052
12032
  const result = dispatch2(ev);
12053
12033
  if (result === "handled") {
@@ -12341,6 +12321,7 @@ function useGestureDispatcher(opts) {
12341
12321
  shiftKey: e.shiftKey,
12342
12322
  worldX: wClick.x,
12343
12323
  worldY: wClick.y,
12324
+ ...down.affordance !== void 0 ? { affordance: down.affordance } : {},
12344
12325
  ...down.bodyTarget !== void 0 ? { bodyTarget: down.bodyTarget } : {},
12345
12326
  ...down.bodyKind !== void 0 ? { bodyKind: down.bodyKind } : {}
12346
12327
  };
@@ -12382,8 +12363,10 @@ function useGestureDispatcher(opts) {
12382
12363
  };
12383
12364
  const onContextMenu = (e) => {
12384
12365
  e.preventDefault();
12385
- const worldPoint = { x: e.clientX, y: e.clientY };
12386
- 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);
12387
12370
  const ev = {
12388
12371
  kind: "contextmenu",
12389
12372
  target: e.target,
@@ -12391,6 +12374,9 @@ function useGestureDispatcher(opts) {
12391
12374
  ctrlKey: e.ctrlKey,
12392
12375
  metaKey: e.metaKey,
12393
12376
  shiftKey: e.shiftKey,
12377
+ worldX: w.x,
12378
+ worldY: w.y,
12379
+ ...affordance !== void 0 ? { affordance } : {},
12394
12380
  ...menuBody?.body !== void 0 ? { bodyTarget: menuBody.body } : {},
12395
12381
  ...menuBody?.kind !== void 0 ? { bodyKind: menuBody.kind } : {}
12396
12382
  };
@@ -12790,47 +12776,209 @@ function useViewAnimation(setView) {
12790
12776
  );
12791
12777
  return { animateTo, animateToBounds, cancel };
12792
12778
  }
12793
- function useTools(opts) {
12794
- if (!(opts.active in opts.registry)) {
12795
- 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";
12796
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) {
12797
12829
  const ctx = useActiveToolContext();
12830
+ const actionsRegistry = useActionsRegistry();
12798
12831
  const hasInitializedRef = useRef(false);
12799
12832
  const isFirstRender = !hasInitializedRef.current;
12800
12833
  const needsFirstMountSyncRef = useRef(false);
12801
12834
  if (isFirstRender) {
12802
12835
  hasInitializedRef.current = true;
12803
- needsFirstMountSyncRef.current = ctx.active === "select" && opts.active !== "select";
12836
+ needsFirstMountSyncRef.current = ctx.active === "select" && opts.focused !== "select";
12804
12837
  }
12805
12838
  useEffect(() => {
12806
12839
  if (!needsFirstMountSyncRef.current) return;
12807
12840
  needsFirstMountSyncRef.current = false;
12808
- 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
+ };
12809
12868
  }, []);
12810
- const active = isFirstRender && ctx.active === "select" && opts.active !== "select" ? opts.active : ctx.active;
12811
- const hotkeyStack = ctx.hotkeyStack;
12812
- const hotkeyEngaged = hotkeyStack.at(-1) ?? null;
12813
- const registryRef = useRef(opts.registry);
12814
- registryRef.current = opts.registry;
12815
- const ambientRef = useRef(opts.ambient ?? []);
12816
- ambientRef.current = opts.ambient ?? [];
12817
- const activeRef = useRef(active);
12818
- activeRef.current = active;
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;
12819
12966
  const hotkeyRef = useRef(hotkeyEngaged);
12820
12967
  hotkeyRef.current = hotkeyEngaged;
12968
+ const setFocused = contributions.setFocused;
12821
12969
  const setActive = useCallback(
12822
12970
  (id) => {
12823
- if (!(id in registryRef.current)) {
12971
+ if (!(id in slottedRef.current.registry)) {
12824
12972
  throw new Error(`setActive: "${id}" not in registry`);
12825
12973
  }
12826
12974
  dlog("tools", "active:", activeRef.current, "\u2192", id);
12827
- ctx.setActive(id);
12975
+ setFocused(id);
12828
12976
  },
12829
- [ctx]
12977
+ [setFocused]
12830
12978
  );
12831
12979
  const engageHotkey = useCallback(
12832
12980
  (id) => {
12833
- if (!(id in registryRef.current)) {
12981
+ if (!(id in slottedRef.current.registry)) {
12834
12982
  throw new Error(`engageHotkey: "${id}" not in registry`);
12835
12983
  }
12836
12984
  dlog("tools", "hotkey engaged:", id);
@@ -12842,32 +12990,12 @@ function useTools(opts) {
12842
12990
  if (hotkeyRef.current) dlog("tools", "hotkey disengaged:", hotkeyRef.current);
12843
12991
  ctx.popHotkey();
12844
12992
  }, [ctx]);
12845
- const lastConflictSigRef = useRef(null);
12846
- const toolSetSig = Object.keys(opts.registry).join(",") + "|" + (opts.ambient ?? []).map((t) => t.id).join(",");
12847
- useEffect(() => {
12848
- if (process.env.NODE_ENV === "production") return;
12849
- if (lastConflictSigRef.current === toolSetSig) return;
12850
- lastConflictSigRef.current = toolSetSig;
12851
- reportRouteConflicts({
12852
- registry: registryRef.current,
12853
- ambient: ambientRef.current
12854
- });
12855
- }, [toolSetSig]);
12856
12993
  const has = useCallback(
12857
- (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),
12858
12995
  []
12859
12996
  );
12860
- const getActiveOverlays = useCallback(() => {
12861
- const out = [];
12862
- const activeTool = registryRef.current[activeRef.current];
12863
- if (activeTool?.overlay) out.push(activeTool.overlay);
12864
- const mod = hotkeyRef.current ? registryRef.current[hotkeyRef.current] : null;
12865
- if (mod?.overlay) out.push(mod.overlay);
12866
- for (const t of ambientRef.current) {
12867
- if (t.overlay) out.push(t.overlay);
12868
- }
12869
- return out;
12870
- }, []);
12997
+ const active = contributions.focused;
12998
+ const getActiveOverlays = contributions.overlays;
12871
12999
  return useMemo(
12872
13000
  () => ({
12873
13001
  active,
@@ -12875,8 +13003,8 @@ function useTools(opts) {
12875
13003
  hotkeyEngaged,
12876
13004
  engageHotkey,
12877
13005
  disengageHotkey,
12878
- ambient: ambientRef.current,
12879
- registry: registryRef.current,
13006
+ ambient: slottedRef.current.ambient,
13007
+ registry: slottedRef.current.registry,
12880
13008
  has,
12881
13009
  getActiveOverlays
12882
13010
  }),
@@ -12884,38 +13012,6 @@ function useTools(opts) {
12884
13012
  );
12885
13013
  }
12886
13014
 
12887
- // src/interactions/actions/defaults/toolOffhand.ts
12888
- var TOOL_OFFHAND_ID = "tool.offhand";
12889
- function buildToolOffhandBindings(specs) {
12890
- return specs.map(({ toolId, key }) => ({
12891
- spec: { kind: "key-held", key },
12892
- opts: { params: { toolId } }
12893
- }));
12894
- }
12895
- function makeToolOffhandAction(bindings) {
12896
- return {
12897
- id: TOOL_OFFHAND_ID,
12898
- label: "Engage tool (offhand)",
12899
- defaultBinding: bindings,
12900
- scope: "hotkey",
12901
- invoker: {
12902
- timing: "ongoing",
12903
- start: (ctx, opts) => {
12904
- const activeTool = ctx.deps.activeTool;
12905
- const toolId = resolveParams(opts?.params)?.toolId;
12906
- if (!activeTool || !toolId) return {};
12907
- activeTool.pushHotkey(toolId);
12908
- return {
12909
- onEnd: () => {
12910
- activeTool.popHotkey();
12911
- }
12912
- };
12913
- }
12914
- },
12915
- requires: ["activeTool"]
12916
- };
12917
- }
12918
-
12919
13015
  // src/interactions/actions/defaults/toolActivate.ts
12920
13016
  var TOOL_ACTIVATE_ID = "tool.activate";
12921
13017
  function buildToolActivateBindings(specs) {
@@ -12992,9 +13088,6 @@ var BUILTIN_SELECT_KEYS = {
12992
13088
  hand: { key: "H" },
12993
13089
  pen: { key: "P" }
12994
13090
  };
12995
- var BUILTIN_OFFHAND_ACTIONS = {
12996
- hand: " "
12997
- };
12998
13091
  function useKeybindings(tools, options = {}) {
12999
13092
  const toolsRef = useRef(tools);
13000
13093
  toolsRef.current = tools;
@@ -13002,19 +13095,6 @@ function useKeybindings(tools, options = {}) {
13002
13095
  optionsRef.current = options;
13003
13096
  const initialActiveRef = useRef(tools.active);
13004
13097
  const registry2 = useActionsRegistry();
13005
- useEffect(() => {
13006
- if (optionsRef.current.disable) return;
13007
- if (!registry2) return;
13008
- const specs = [];
13009
- for (const [toolId, key] of Object.entries(BUILTIN_OFFHAND_ACTIONS)) {
13010
- if (toolsRef.current.has(toolId)) {
13011
- specs.push({ toolId, key });
13012
- }
13013
- }
13014
- if (specs.length === 0) return;
13015
- const bindings = buildToolOffhandBindings(specs);
13016
- return registry2.register(makeToolOffhandAction(bindings));
13017
- }, [registry2, tools]);
13018
13098
  useEffect(() => {
13019
13099
  if (optionsRef.current.disable) return;
13020
13100
  if (!registry2) return;
@@ -13576,8 +13656,108 @@ function textLineBoxes(pose, opts = {}) {
13576
13656
  return out;
13577
13657
  }
13578
13658
 
13579
- // src/features/images/imageCache.ts
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
13580
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
+
13759
+ // src/features/images/imageCache.ts
13760
+ var cache6 = /* @__PURE__ */ new Map();
13581
13761
  var subscribers = /* @__PURE__ */ new Set();
13582
13762
  var defaultLoader = async (src) => {
13583
13763
  if (typeof Image === "undefined" || typeof createImageBitmap === "undefined") {
@@ -13595,28 +13775,28 @@ function notify() {
13595
13775
  }
13596
13776
  function startLoad(src) {
13597
13777
  const entry = { status: "loading" };
13598
- cache5.set(src, entry);
13778
+ cache6.set(src, entry);
13599
13779
  loader(src).then(
13600
13780
  (bitmap) => {
13601
- if (cache5.get(src) !== entry) return;
13602
- cache5.set(src, { status: "ready", bitmap });
13781
+ if (cache6.get(src) !== entry) return;
13782
+ cache6.set(src, { status: "ready", bitmap });
13603
13783
  notify();
13604
13784
  },
13605
13785
  () => {
13606
- if (cache5.get(src) !== entry) return;
13607
- cache5.set(src, { status: "error" });
13786
+ if (cache6.get(src) !== entry) return;
13787
+ cache6.set(src, { status: "error" });
13608
13788
  notify();
13609
13789
  }
13610
13790
  );
13611
13791
  }
13612
13792
  function getImageBitmap(src) {
13613
- const entry = cache5.get(src);
13793
+ const entry = cache6.get(src);
13614
13794
  if (entry) return entry.bitmap;
13615
13795
  startLoad(src);
13616
13796
  return void 0;
13617
13797
  }
13618
13798
  function imageStatus(src) {
13619
- return cache5.get(src)?.status ?? "idle";
13799
+ return cache6.get(src)?.status ?? "idle";
13620
13800
  }
13621
13801
  function subscribeImageReady(cb) {
13622
13802
  subscribers.add(cb);
@@ -13801,6 +13981,17 @@ function rectsToPath(rects) {
13801
13981
  }
13802
13982
  return { kind: "polygon", commands, coords, fillRule: "nonzero" };
13803
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
+ }
13804
13995
  var PATH_PAINTER = {
13805
13996
  id: "kit:path",
13806
13997
  matches: (node) => {
@@ -13814,12 +14005,12 @@ var PATH_PAINTER = {
13814
14005
  const d = node.data;
13815
14006
  const projected = pathInPoseFrame(d.path, pose);
13816
14007
  const hasStroke = !!d.stroke && d.stroke !== "none" && (d.strokeWidth ?? 0) > 0;
13817
- const fillColor = d.fill ?? d.color;
13818
- const hasFill = fillColor !== "none" && (fillColor !== void 0 || !hasStroke);
14008
+ const declared = resolveNodeFill(d.fill, d.color, hasStroke, hasStroke ? null : "#888");
14009
+ const fill = declared && resolveFillPattern(fillInPoseFrame(declared, pose));
13819
14010
  const cmd = {
13820
14011
  kind: "path",
13821
14012
  path: projected,
13822
- ...hasFill ? { fill: { color: fillColor ?? "#888" } } : {},
14013
+ ...fill ? { fill } : {},
13823
14014
  ...hasStroke ? { stroke: { paint: { color: d.stroke }, width: d.strokeWidth ?? 1 } } : {}
13824
14015
  };
13825
14016
  return [cmd];
@@ -13835,9 +14026,8 @@ var PATH_PAINTER = {
13835
14026
  ink: (node) => {
13836
14027
  const d = node.data;
13837
14028
  const hasStroke = !!d.stroke && d.stroke !== "none" && (d.strokeWidth ?? 0) > 0;
13838
- const fillColor = d.fill ?? d.color;
13839
14029
  return {
13840
- filled: fillColor !== "none" && (fillColor !== void 0 || !hasStroke),
14030
+ filled: resolveNodeFill(d.fill, d.color, hasStroke, hasStroke ? null : "#888") !== null,
13841
14031
  strokeWidth: hasStroke ? d.strokeWidth ?? 1 : 0
13842
14032
  };
13843
14033
  }
@@ -13854,10 +14044,12 @@ var SHAPE_PAINTER = {
13854
14044
  paint: (node, pose) => nodeMemo(node, PAINT_SLOT, pose, () => {
13855
14045
  const d = node.data;
13856
14046
  const path = pathForShape(d, pose);
14047
+ const declaredFill = resolveNodeFill(d.fill, d.color, false, "#888");
14048
+ const shapeFill = declaredFill && resolveFillPattern(fillInPoseFrame(declaredFill, pose));
13857
14049
  return [{
13858
14050
  kind: "path",
13859
14051
  path,
13860
- fill: { color: d.fill ?? d.color ?? "#888" },
14052
+ ...shapeFill ? { fill: shapeFill } : {},
13861
14053
  ...d.stroke && (d.strokeWidth ?? 0) > 0 ? { stroke: { paint: { color: d.stroke }, width: d.strokeWidth ?? 1 } } : {}
13862
14054
  }];
13863
14055
  }),
@@ -14258,11 +14450,8 @@ function useSelectTool(adapter, options) {
14258
14450
  ...moveOpts
14259
14451
  },
14260
14452
  { spec: { kind: "drag", target: "empty" }, actionId: "areaSelect" },
14261
- // Click on empty canvas clear. Same no-affordance requirement as
14262
- // the press binding above, and for the same two reasons: clicking a
14263
- // widget that happens to float over empty canvas isn't a click on
14264
- // empty canvas, and declining in the SPEC lets the chrome's owner
14265
- // have the click instead of it being consumed here.
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.
14266
14455
  {
14267
14456
  spec: {
14268
14457
  kind: "click",
@@ -14348,17 +14537,17 @@ function worldToLocal(xf, wx, wy) {
14348
14537
  function annulusSemiAxes(shape, view) {
14349
14538
  const bandPx = shape.minBandPx ?? 0;
14350
14539
  if (bandPx === 0) return { rx: shape.rx, ry: shape.ry };
14351
- const band = bandPx / meanScale(view.scale);
14540
+ const band = pxExtent(bandPx, view.scale);
14352
14541
  return {
14353
- rx: Math.max(shape.rx, shape.innerWidth / 2 + band),
14354
- 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)
14355
14544
  };
14356
14545
  }
14357
14546
  function hitRegion(region, wx, wy, xf, view) {
14358
14547
  const local = worldToLocal(xf, wx, wy);
14359
14548
  if (region.shape.kind === "point") {
14360
- const radiusWorld = region.shape.hitRadiusPx / meanScale(view.scale);
14361
- return Math.abs(local.x - region.shape.x) <= radiusWorld && Math.abs(local.y - region.shape.y) <= radiusWorld;
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);
14362
14551
  }
14363
14552
  if (region.shape.kind === "annulus") {
14364
14553
  const s = region.shape;
@@ -14424,14 +14613,13 @@ function asIsVisible(data) {
14424
14613
  function recordRegionHitbox(debug, affordanceId, region, xf, view) {
14425
14614
  if (region.shape.kind === "point") {
14426
14615
  const w2 = localToWorld(xf, region.shape.x, region.shape.y);
14427
- const r2 = region.shape.hitRadiusPx / meanScale(view.scale);
14616
+ const e = pxExtent(region.shape.hitRadiusPx, view.scale);
14428
14617
  debug.recordHitbox(affordanceId, "handle", {
14429
14618
  kind: "rect",
14430
- x: w2.x - r2,
14431
- y: w2.y - r2,
14432
- width: r2 * 2,
14433
- height: r2 * 2,
14434
- ...xf.identity ? {} : { rotation: Math.atan2(xf.sin, xf.cos) }
14619
+ x: w2.x - e.x,
14620
+ y: w2.y - e.y,
14621
+ width: e.x * 2,
14622
+ height: e.y * 2
14435
14623
  });
14436
14624
  return;
14437
14625
  }
@@ -14490,7 +14678,7 @@ function paintRegion(region, xf, view, state, viewT, out) {
14490
14678
  if (paint.kind === "annulus") {
14491
14679
  if (region.shape.kind !== "annulus") return;
14492
14680
  const s = region.shape;
14493
- const insetLocal = (paint.insetPx ?? 0) / meanScale(view.scale);
14681
+ const insetLocal = pxExtent(paint.insetPx ?? 0, view.scale);
14494
14682
  const cmd = annulusCommand(s, annulusSemiAxes(s, view), insetLocal, xf, viewT, paint.fill, paint.stroke);
14495
14683
  if (cmd) out.push(cmd);
14496
14684
  return;
@@ -14507,13 +14695,13 @@ function paintRegion(region, xf, view, state, viewT, out) {
14507
14695
  }
14508
14696
  }
14509
14697
  function annulusCommand(shape, semiAxes, insetLocal, xf, viewT, fill, stroke) {
14510
- const rx = semiAxes.rx - insetLocal;
14511
- const ry = semiAxes.ry - insetLocal;
14698
+ const rx = semiAxes.rx - insetLocal.x;
14699
+ const ry = semiAxes.ry - insetLocal.y;
14512
14700
  if (rx <= 0 || ry <= 0) return null;
14513
14701
  const cx = shape.cx;
14514
14702
  const cy = shape.cy;
14515
- const rectHW = shape.innerWidth / 2 + insetLocal;
14516
- const rectHH = shape.innerHeight / 2 + insetLocal;
14703
+ const rectHW = shape.innerWidth / 2 + insetLocal.x;
14704
+ const rectHH = shape.innerHeight / 2 + insetLocal.y;
14517
14705
  const haveTopBottom = rectHH < ry;
14518
14706
  const haveLeftRight = rectHW < rx;
14519
14707
  if (!haveTopBottom && !haveLeftRight) return null;
@@ -14774,6 +14962,7 @@ function useHandTool(opts = {}) {
14774
14962
  () => Object.assign(defineViewportTool({
14775
14963
  id: "hand",
14776
14964
  capabilities: ["navigation"],
14965
+ hotkey: "space",
14777
14966
  hookName: "useHandTool",
14778
14967
  presentation: {
14779
14968
  label: "Hand",
@@ -14898,10 +15087,6 @@ function mirrorHandle(anchor, out) {
14898
15087
  if (!out || anchor.altBroken) return void 0;
14899
15088
  return { x: 2 * anchor.x - out.x, y: 2 * anchor.y - out.y };
14900
15089
  }
14901
- function dist2(ax, ay, bx, by) {
14902
- const dx = ax - bx, dy = ay - by;
14903
- return Math.hypot(dx, dy);
14904
- }
14905
15090
  function usePenTool(options) {
14906
15091
  const { wrapPath, adapter, autoSelect = true, autoCommitOnClose = true, closeHitRadius = 8, snapPoint: snapPoint2 } = options;
14907
15092
  const scratchRef = useRef(null);
@@ -14925,11 +15110,14 @@ function usePenTool(options) {
14925
15110
  const fn = optsRef.current.snapPoint;
14926
15111
  return fn ? fn({ x, y }) : { x, y };
14927
15112
  }, []);
14928
- const closeRadius = useCallback((deps) => {
14929
- const view = deps.view?.get();
14930
- const scale2 = view?.scale ?? { x: 1, y: 1 };
14931
- return optsRef.current.closeHitRadius / meanScale(scale2);
14932
- }, []);
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
+ );
14933
15121
  const actions = useMemo(() => {
14934
15122
  const s = () => scratchRef.current;
14935
15123
  return [
@@ -14945,10 +15133,9 @@ function usePenTool(options) {
14945
15133
  if (p?.pressX === void 0 || p.pressY === void 0) return;
14946
15134
  const { x: wx, y: wy } = snap2(p.pressX, p.pressY);
14947
15135
  const scratch = s();
14948
- const radius = closeRadius(deps);
14949
15136
  if (scratch.current && scratch.current.anchors.length >= 3) {
14950
15137
  const first = scratch.current.anchors[0];
14951
- if (dist2(first.x, first.y, wx, wy) <= radius) {
15138
+ if (withinCloseRadius(deps, first.x, first.y, wx, wy)) {
14952
15139
  scratch.current.closed = true;
14953
15140
  scratch.finishedSubpaths.push(scratch.current);
14954
15141
  scratch.current = null;
@@ -15067,7 +15254,7 @@ function usePenTool(options) {
15067
15254
  }
15068
15255
  }
15069
15256
  ];
15070
- }, [commit, snap2, closeRadius]);
15257
+ }, [commit, snap2, withinCloseRadius]);
15071
15258
  return useMemo(() => {
15072
15259
  return defineTool({
15073
15260
  id: "pen",
@@ -16844,8 +17031,8 @@ function CanvasInner(props, ref) {
16844
17031
  const isVisible = getIsVisibleRef.current?.() ?? alwaysVisible;
16845
17032
  for (const layer of layers2) {
16846
17033
  if (!layer.hitTest) continue;
16847
- const binding = layer.hitTest(worldX, worldY, void 0, view, dims, isVisible);
16848
- 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 };
16849
17036
  }
16850
17037
  return null;
16851
17038
  }, []);
@@ -17281,7 +17468,7 @@ function CanvasInner(props, ref) {
17281
17468
  effectiveView,
17282
17469
  { width, height }
17283
17470
  );
17284
- renderer.render(commands);
17471
+ renderer.render(commands, viewToMat3(effectiveView));
17285
17472
  }, [layersWithDebug, width, height, effectiveView, debugSink, redrawNonce, dprProp]);
17286
17473
  const shaderIdKey = shaders?.map((h) => h.id).join("|") ?? "";
17287
17474
  useEffect(() => {
@@ -19721,6 +19908,8 @@ function toAffordanceHit(hit) {
19721
19908
  const scratch = hit.binding.initialScratch ?? {};
19722
19909
  return {
19723
19910
  kind: hit.region.hitKind ?? `${hit.affordanceId}:${hit.regionId}`,
19911
+ owner: hit.affordanceId,
19912
+ strength: "shared",
19724
19913
  ...scratch.targetId !== void 0 ? { targetIds: [scratch.targetId] } : {},
19725
19914
  ...scratch.anchor !== void 0 ? { anchor: scratch.anchor } : {},
19726
19915
  ...scratch.fixedPoint !== void 0 ? { fixedPoint: scratch.fixedPoint } : {},
@@ -20249,7 +20438,7 @@ function chordLengthParam(samples) {
20249
20438
  ts[0] = 0;
20250
20439
  let total = 0;
20251
20440
  for (let i = 1; i < n; i++) {
20252
- total += dist3(samples[i - 1], samples[i]);
20441
+ total += dist2(samples[i - 1], samples[i]);
20253
20442
  ts[i] = total;
20254
20443
  }
20255
20444
  if (total === 0) return null;
@@ -20282,13 +20471,13 @@ function solveLS(samples, ts, a, e, tStart, tEnd) {
20282
20471
  let alpha1;
20283
20472
  let alpha2;
20284
20473
  if (Math.abs(det) < 1e-12) {
20285
- const chord = dist3(a, e);
20474
+ const chord = dist2(a, e);
20286
20475
  alpha1 = alpha2 = chord / 3;
20287
20476
  } else {
20288
20477
  alpha1 = (c22 * x1 - c12 * x2) / det;
20289
20478
  alpha2 = (c11 * x2 - c12 * x1) / det;
20290
20479
  if (alpha1 < 1e-6 || alpha2 < 1e-6) {
20291
- const chord = dist3(a, e);
20480
+ const chord = dist2(a, e);
20292
20481
  alpha1 = alpha2 = chord / 3;
20293
20482
  }
20294
20483
  }
@@ -20324,7 +20513,7 @@ function newtonRefineT(p, t, p0, p1, p2, p3) {
20324
20513
  function sub(a, b) {
20325
20514
  return { x: a.x - b.x, y: a.y - b.y };
20326
20515
  }
20327
- function dist3(a, b) {
20516
+ function dist2(a, b) {
20328
20517
  return Math.hypot(a.x - b.x, a.y - b.y);
20329
20518
  }
20330
20519
  function unit(v) {
@@ -21334,7 +21523,7 @@ function SceneCanvasInner(props, ref) {
21334
21523
  const getMode = getActiveModeRef.current;
21335
21524
  if (!getMode) return true;
21336
21525
  const registry2 = toolsForEligibilityRef.current?.registry;
21337
- const caps = registry2?.[toolId]?.capabilities ?? [];
21526
+ const caps = registry2?.[toolId]?.eligibility.capabilities ?? [];
21338
21527
  if (caps.length === 0) return false;
21339
21528
  const allowed = getMode().allowedCapabilities;
21340
21529
  for (const c of caps) if (allowed.has(c)) return true;
@@ -21808,10 +21997,6 @@ function GestureDispatcherMounter({
21808
21997
  for (const tool of tools.ambient) m.set(tool.id, tool);
21809
21998
  return m;
21810
21999
  }, [tools.registry, tools.ambient]);
21811
- const ambientToolIds = useMemo(
21812
- () => tools.ambient.map((t) => t.id),
21813
- [tools.ambient]
21814
- );
21815
22000
  const boundsOfRef = useRef(boundsOf2);
21816
22001
  boundsOfRef.current = boundsOf2;
21817
22002
  const pickEveryRef = useRef(pickEvery);
@@ -21923,9 +22108,14 @@ function GestureDispatcherMounter({
21923
22108
  const worldPoint = clientToWorld2(screenPoint.x, screenPoint.y);
21924
22109
  const extra = canvasApiRef?.current?.hitTestExtras?.(worldPoint.x, worldPoint.y);
21925
22110
  if (extra) {
22111
+ const claim = extra.hit;
21926
22112
  return {
21927
22113
  kind: `layer:${extra.layerId}`,
21928
- ...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 } : {}
21929
22119
  };
21930
22120
  }
21931
22121
  return affordanceAt ? affordanceAt(worldPoint) : null;
@@ -21945,7 +22135,6 @@ function GestureDispatcherMounter({
21945
22135
  canvasRef,
21946
22136
  actions: registry2,
21947
22137
  toolsById,
21948
- ambientToolIds,
21949
22138
  enabled,
21950
22139
  keyboard,
21951
22140
  affordanceAt: wrappedAffordanceAt,
@@ -22203,7 +22392,7 @@ function renderSceneToCanvas(args) {
22203
22392
  entry.dpr = dpr;
22204
22393
  }
22205
22394
  const commands = buildSceneViewCommands(scene, view, drawOne, extraCommands);
22206
- entry.renderer.render(commands);
22395
+ entry.renderer.render(commands, viewToMat3(view));
22207
22396
  }
22208
22397
  function SceneViewCanvasInner(props) {
22209
22398
  const { scene, view, width, height, drawOne, extraCommands, className, canvasRef } = props;
@@ -22543,7 +22732,7 @@ function renderSceneToPixels(args) {
22543
22732
  bakeBudget: Infinity
22544
22733
  });
22545
22734
  try {
22546
- renderer.render(plan.commands);
22735
+ renderer.render(plan.commands, viewToMat3(plan.view));
22547
22736
  if (typeof gl.isContextLost === "function" && gl.isContextLost()) {
22548
22737
  throw new Error("renderSceneToPixels: WebGL2 context was lost during render");
22549
22738
  }
@@ -23267,6 +23456,64 @@ function applyStyleToRange(runs, start, end, patch) {
23267
23456
  return coalesceRuns(out);
23268
23457
  }
23269
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
+
23270
23517
  // src/features/text/useTextEdit.ts
23271
23518
  var FLAG_SHORTCUTS = [
23272
23519
  { key: "b", shift: false, flag: "bold" },
@@ -23428,7 +23675,21 @@ function useTextEdit(opts) {
23428
23675
  togglePending(flag);
23429
23676
  return;
23430
23677
  }
23431
- 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);
23432
23693
  writeRunsPreservingSelection(overlay, next, range2.start, range2.end);
23433
23694
  syncSelection();
23434
23695
  }
@@ -23706,6 +23967,95 @@ function useSceneTextEdit(scene, container, options = {}) {
23706
23967
  return { ...edit, onDoubleClick };
23707
23968
  }
23708
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
+
23709
24059
  // src/core/ops/setText.ts
23710
24060
  function createSetTextOp(args) {
23711
24061
  const { id, from, to, label, coalesceKey = `setText:${id}` } = args;
@@ -25040,8 +25390,8 @@ function bestAxis(b, axis, anchors, candidates, worldTolerance) {
25040
25390
  return { delta: bestGuide ? bestDelta : 0, guide: bestGuide };
25041
25391
  }
25042
25392
  function matchAlignment(bounds, candidates, worldTolerance, anchors) {
25043
- const x = bestAxis(bounds, "x", anchors.x, candidates, worldTolerance);
25044
- const y = bestAxis(bounds, "y", anchors.y, candidates, worldTolerance);
25393
+ const x = bestAxis(bounds, "x", anchors.x, candidates, worldTolerance.x);
25394
+ const y = bestAxis(bounds, "y", anchors.y, candidates, worldTolerance.y);
25045
25395
  return { dx: x.delta, dy: y.delta, activeX: x.guide, activeY: y.guide };
25046
25396
  }
25047
25397
 
@@ -25049,7 +25399,7 @@ function matchAlignment(bounds, candidates, worldTolerance, anchors) {
25049
25399
  var activeList = (m) => [m.activeX, m.activeY].filter((g) => g !== null);
25050
25400
  function worldTol(base) {
25051
25401
  const t = base.tolerance ?? 6;
25052
- 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 };
25053
25403
  }
25054
25404
  function alignMoveBehavior(args) {
25055
25405
  const proj = args.projection ?? RECT_ALIGN_PROJECTION;
@@ -27270,6 +27620,24 @@ function snapPoint(opts) {
27270
27620
  };
27271
27621
  }
27272
27622
 
27273
- export { ALWAYS, ANCHOR_HIT_BASE_PX, ActionDisabledReason, ActionsProvider, ActionsProviderIfRoot, ActiveToolContextProvider, ActiveToolContextProviderIfRoot, BUNDLE_TOOLS, COARSE_TARGET_SCALE, CURVE_REPS, ColorOverrideRegistry, CropIcon, CursorCoordsHud, DEFAULT_ALPHA_DECAY, DEFAULT_ALPHA_MIN, DEFAULT_DEBUG_THEME, DEFAULT_DEVICE_PROFILE, DEFAULT_FILL_COLOR, DEFAULT_HANDLE_SIZE2 as DEFAULT_HANDLE_SIZE, DEFAULT_PALETTE, DEFAULT_ROTATION_HANDLE_DISTANCE, DEFAULT_STROKE_COLOR, DEFAULT_TEXT_STYLE, DEFAULT_VELOCITY_DECAY, DepRegistryProvider, DepRegistryProviderIfRoot, DeviceProfileProvider, DivideIcon, EASINGS, EllipseIcon, ExcludeIcon, EyedropperIcon, FALLBACK_FIT_VIEW, GHOST_STROKE, HANDLE_BASE_PX, HandIcon, IDENTITY_COLOR_MATRIX, IDENTITY_POSE_COMPOSITION, IntersectIcon, KIT_SHAPE_KINDS, LassoIcon, LineIcon, MIXED, MOVE_ANCHORS, MinimapCanvas, NEVER, OUTLINE_MIN_SCREEN_PX, PATH_ANCHOR_CHROME_ID, PenIcon, PencilIcon, PickHud, PointerProviderIfRoot, PolygonIcon, RECT_ALIGN_PROJECTION, ROTATION_HANDLE_BASE_PX, RectIcon, SPRING_PRESETS, SceneCanvas, SceneViewCanvas, SelectIcon, SelectionContextProvider, SelectionContextProviderIfRoot, ShaderCompileError, StarIcon, SubtractIcon, TextIcon, UnionIcon, UnknownIcon, VERSION, WeaselProvider, WeaselRenderer, aabbCenter, actionIs, alignDeltaFor, alignInsertBehavior, alignMoveBehavior, alignResizeBehavior, alignedStrokeRect, always, and, animateLifecycle, animateOnSetPose, annulusSemiAxes, applyBooleanOp, applyHitExistingGate, applyStyleToRange, areaSelectAction, arrayAdapter, asNodeId, bezierCubic, bezierQuadratic, buildChromeCtx, buildGradientRamp, buildRuleCtx, buildSceneViewCommands, canBringForward, canHover, canSendBackward, capabilityAll, capabilityIn, capabilityIs, capabilityNot, caretIndexAt, cellAt, charOffsetToDomPosition, clampView, clearSelectionAction, clientToCanvas, clipboardCopyAction, clipboardCutAction, cloneAction, coarsePointer, composeAffordanceLayer, composePath, composeRectPose, composeSelectionPose, composeWorldPose, computeFitView2 as computeFitView, computeIndicatorCommand, computeWheelAction, cond, constrainTo45, containedThenNearest, countPathAnchors, createCellHighlightLayer, createChildrenLayer, createCornerResizeAffordance, createDebugOverlayLayer, createDebugSink, createDispatcher, createGridLayer, createGuidesLayer, createHistory2 as createHistory, createMarkdownRenderer, createMoveToIndexOp, createNodeProperties, createNodeRouting, createParallaxLayer, createPathAnchorAffordances, createPathEditingOverlayLayer, createPathLayer, createPenPreviewLayer, createReorderOp, createRotationAffordance, createScene, createSelectionHandlesLayer, createSelectionOutlineLayer, createSelectionOverlayLayer, createSetDataOp, createSetLayerOp, createSetPathOp, createSetTextOp, createTextLayer, createViewportLayer, cycleVertexColors, decomposePath, decomposeRectPose, defaultCommitAdapter, defaultDrawOne, defaultLabelTextRenderer, defaultNodeProperties, defaultNodeRouting, defaultVisibilityRules, deriveAlignmentGuides, deriveParallaxView, describeRule, domPositionToCharOffset, domToRuns, drawLayers, easeIn, easeInBack, easeInBounce, easeInCirc, easeInCubic, easeInElastic, easeInExpo, easeInOut, easeInOutBack, easeInOutBounce, easeInOutCirc, easeInOutCubic, easeInOutElastic, easeInOutExpo, easeInOutQuad, easeInOutQuart, easeInOutQuint, easeInOutSine, easeInQuad, easeInQuart, easeInQuint, easeInSine, easeOut, easeOutBack, easeOutBounce, easeOutCirc, easeOutCubic2 as easeOutCubic, easeOutElastic, easeOutExpo, easeOutQuad, easeOutQuart, easeOutQuint, easeOutSine, editAnchorsAction, enterTextEditAction, enumerateAnchors, evaluate, evaluateEnabled, findNodeShape, findShapeInk, findShapeSilhouette, fitTextPose, fitToBounds, fitViewToBounds, fitZoom, focused, fontString, forEachCoalesced, freeform, gateLayer, gesturing, getAlpha01, getImageBitmap, getNodeShapes, getStylusData, hexToRgba, hitAffordanceRegions, hitAnchor, hitRotationHandle, hovering, hoveringSelected, imageStatus, inferredNodeProperties, inferredNodeRouting, insertAction, isEditableTarget2 as isEditableTarget, lassoSelectAction, layoutMarkdown, lerpColorArray, lerpOklab, lerpOklch, linear, makeViewportZoomAction, markdownToRuns, mat3, matchAlignment, matchesKeyBinding, measureText, measureTextBounds, measuredWidth, mergeAlphaFromPrev, modeIn, modeIs, modeNot, modifierHeld, momentum, moveAction, multiActive, nearest, nearestWithin, nestedHitTester, never, none, normalizeHex, not, nurbs, oklabToOklch, oklabToSrgbU8, oklchToOklab, openFilePicker, or, parseColor, parseColorToRgba255, parseDebugFlags, pathContainsPoint, pathContainsPolygon, pathContainsRect, pathDistanceToPoint, pathDivide, pathExclude, pathFromD, pathInPoseFrame, pathInWorld, pathIntersect, pathIntersectsPolygon, pathIntersectsRect, pathOriginProjection, pathSubtract, pathToAnchors, pathUnion, pickTopMostHit, pinchZoomAction, planPixelRender, pointInRotatedRect, pointInTextPose, poseRotationOf, pressureToWidth, rainbowVertexColors, rebaseLocalPose, rectCorners, registerContentHandler, registerNodeShape, registerProgram, renderLabel, renderSceneToCanvas, renderSceneToPixels, resizeAction, resolveColor, resolveDeviceProfile, resolveRuns, resolveTextStyle, resolveVisibility, rgbaToHex, rotateAction, rotateAroundAABBCenter, rotatePathAround, rotatePoint, rotatedRectCorners, rotationDegreesUnit, rotationHandle, roundToCell, runsToDom, runsToMarkdown, runsToPlainText, sceneFromJSON, sceneNodeClientRect, sceneToAdapter, screenToWorld, selectFromLasso, selectionAtLeast, selectionEmpty, selectionIs, shapeCoversPoint, sliceAction, snapPoint, solidVertexColors, specificity, spiro, splitPathByLine, splitSubpaths, springPose, springVertexColors, srgbU8ToOklab, staggerVertexColors, startThresholdDrag, styleAtRange, subscribeImageReady, tessellate, tessellateStroke, textCommand, textLineBoxes, tileGrid, toHex8, toRuns, translatePoseViaDescriptor, translateRectPose, tweenPose, tweenVertexColors, unionBounds, unionBoundsPath, useAction, useActionsRegistry, useActiveToolContext, useAlign, useAnimator, useArrayAdapter, useAutoCenter, useBooleans, useBooleansAdapter, useCanvasFocus, useCanvasSize, useDecayLoop, useDepRegistry, useDepSource, useDeviceProfile, useDistribute, useDragGesture, useDragHandle, useDragRadial, useDragRect, useDropZone, useEllipseTool, useEyedropperTool, useGestureDispatcher, useGridCellHover, useGuides, useHandTool, useHandleDrag, useHoverTracking, useImageTool, useKeybindings, useLassoTool, useLineTool, useOptionalActiveToolContext, useOptionalDepRegistry, usePenTool, usePencilTool, usePinchGesture, usePinchZoomTool, usePointerStylus, usePolygonTool, usePublishSelection, useRectTool, useResizePolicy, useRotateTool, useScene, useSceneAdapter, useSceneTextEdit, useSelectTool, useSelection, useSelectionContext, useSimulation, useSliceDep, useStandardActions, useStarTool, useTextEdit, useTextTool, useTools, useVelocityTracker, useViewAnimation, useViewTween, useZoom, verticalAlignOffset, viewToMat3, viewToTransform, viewportDragPanAction, viewportZoomAction, when, withAlpha01, withCoord, worldEditToStorage, worldPoseLookup, worldToScreen, zoomAt, zoomAtLeast };
27274
- //# sourceMappingURL=chunk-DLV5F4NY.js.map
27275
- //# sourceMappingURL=chunk-DLV5F4NY.js.map
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