@weasel-js/core 1.5.1 → 1.6.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 (41) hide show
  1. package/CHANGELOG.md +273 -0
  2. package/dist/{autoPoseDescriptor-Dr6CwNZK.d.ts → autoPoseDescriptor-_XOyF7t4.d.ts} +2 -1
  3. package/dist/{chunk-P6MGECVO.js → chunk-44EWMJV2.js} +3 -3
  4. package/dist/{chunk-P6MGECVO.js.map → chunk-44EWMJV2.js.map} +1 -1
  5. package/dist/{chunk-XXQ6FLCJ.js → chunk-H6N2JSZ3.js} +3 -3
  6. package/dist/{chunk-XXQ6FLCJ.js.map → chunk-H6N2JSZ3.js.map} +1 -1
  7. package/dist/{chunk-HAGOFNP5.js → chunk-HPFJUUTF.js} +25 -20
  8. package/dist/chunk-HPFJUUTF.js.map +1 -0
  9. package/dist/{chunk-3LPQ2XZG.js → chunk-L5YNZYEG.js} +8 -13
  10. package/dist/chunk-L5YNZYEG.js.map +1 -0
  11. package/dist/{chunk-LXJDWBEL.js → chunk-PNNZSYRD.js} +2 -2
  12. package/dist/{chunk-LXJDWBEL.js.map → chunk-PNNZSYRD.js.map} +1 -1
  13. package/dist/{chunk-GREL4MVO.js → chunk-THCSDHII.js} +2084 -1344
  14. package/dist/chunk-THCSDHII.js.map +1 -0
  15. package/dist/clipboard.d.ts +2 -1
  16. package/dist/clone.d.ts +3 -2
  17. package/dist/{depSchema-BubKMv-2.d.ts → depSchema-BhgZc0-Q.d.ts} +79 -55
  18. package/dist/{grid-Z_Af3vTl.d.ts → grid-Dx2gQbNv.d.ts} +22 -6
  19. package/dist/index.d.ts +374 -92
  20. package/dist/index.js +6 -6
  21. package/dist/insert.d.ts +4 -3
  22. package/dist/insert.js +2 -2
  23. package/dist/{math-E3rZn4bR.d.ts → math-DRa6HBBs.d.ts} +7 -21
  24. package/dist/math.d.ts +5 -4
  25. package/dist/math.js +2 -2
  26. package/dist/move.d.ts +5 -4
  27. package/dist/move.js +3 -3
  28. package/dist/{options-BPcmwYk7.d.ts → options-CzjxFoYZ.d.ts} +1 -1
  29. package/dist/path-JEV2c5If.d.ts +48 -0
  30. package/dist/{pointSnapToGrid-CgcK2R_I.d.ts → pointSnapToGrid-7mCcm6qG.d.ts} +1 -1
  31. package/dist/{poseDescriptor-PgfVKfa0.d.ts → poseDescriptor-rZa9W5hJ.d.ts} +1 -48
  32. package/dist/renderer.d.ts +8 -7
  33. package/dist/renderer.js +6 -6
  34. package/dist/resize.d.ts +6 -5
  35. package/dist/resize.js +3 -3
  36. package/dist/{types-BdaK9PcP.d.ts → types-CxC8P6W0.d.ts} +27 -1
  37. package/dist/{types-BHGdrOcu.d.ts → types-D_74YlWs.d.ts} +1 -1
  38. package/package.json +10 -10
  39. package/dist/chunk-3LPQ2XZG.js.map +0 -1
  40. package/dist/chunk-GREL4MVO.js.map +0 -1
  41. package/dist/chunk-HAGOFNP5.js.map +0 -1
@@ -1,8 +1,8 @@
1
- import { rotatePoint, aabbCenter, extractPolylines, createSimulation } from './chunk-LXJDWBEL.js';
2
- import { createReparentOp, createTransformOp, snap } from './chunk-P6MGECVO.js';
3
- import { fixedCornerOf, DEFAULT_RESIZE_BEHAVIORS, CORNER_ANCHORS, cornerPoint } from './chunk-XXQ6FLCJ.js';
4
- import { resolveUnit, ANGLE_RADIANS, withinPxRadius, scaleDelta, pxExtent, withinPxBox, pointToGridCell } from './chunk-HAGOFNP5.js';
5
- import { unionBounds, visualBoundsViaDescriptor, translatePoseViaDescriptor, poseDescriptorForNode, unionAABB, AUTO_POSE_DESCRIPTOR, remapRotatedLeaf, PATH_Z, PATH_Q, PATH_C, PATH_L, PATH_M, polylineFromPoints, starPath, regularPolygonPath, linePath, ellipsePath, rectPath, polygonFromPoints, PathBuilder, boundsOfPath, pointInPath, isPathLike, strokeHitTest, isRectPose, transformPath, translatePath, DEFAULT_FLATTEN_TOLERANCE, flattenQuadratic, flattenCubic, flattenCubicWithArcLen, flattenQuadraticWithArcLen, RECT_POSE_DESCRIPTOR, aabbIntersectsRect } from './chunk-3LPQ2XZG.js';
1
+ import { rotatePoint, aabbCenter, extractPolylines, createSimulation } from './chunk-PNNZSYRD.js';
2
+ import { createReparentOp, createTransformOp, snap } from './chunk-44EWMJV2.js';
3
+ import { fixedCornerOf, DEFAULT_RESIZE_BEHAVIORS, CORNER_ANCHORS, cornerPoint } from './chunk-H6N2JSZ3.js';
4
+ import { unitScale, formatUnit, resolveUnit, ANGLE_RADIANS, withinPxRadius, scaleDelta, pxExtent, withinPxBox, pointToGridCell } from './chunk-HPFJUUTF.js';
5
+ import { unionBounds, visualBoundsViaDescriptor, translatePoseViaDescriptor, poseDescriptorForNode, unionAABB, AUTO_POSE_DESCRIPTOR, remapRotatedLeaf, PATH_Z, PATH_Q, PATH_C, PATH_L, PATH_M, polylineFromPoints, starPath, regularPolygonPath, linePath, ellipsePath, rectPath, polygonFromPoints, PathBuilder, boundsOfPath, pointInPath, isPathLike, strokeHitTest, isRectPose, transformPath, translatePath, DEFAULT_FLATTEN_TOLERANCE, flattenQuadratic, flattenCubic, flattenCubicWithArcLen, flattenQuadraticWithArcLen, RECT_POSE_DESCRIPTOR, aabbIntersectsRect } from './chunk-L5YNZYEG.js';
6
6
  import { dispatchApplyBatch, dlog, dwarn, usePointerContext, PointerContextProvider, extractWeaselClipboardFromSvg, parseWeaselClipboardText, materializePaste, WEASEL_CLIPBOARD_MIME, WEASEL_CLIPBOARD_MIME_WEB, sniffWeaselClipboardText, applyOpsTo } from './chunk-T3UQ3F6R.js';
7
7
  import { createSetSelectionOp } from './chunk-DOBSZOPR.js';
8
8
  import { registerOpFactory, createInsertOp, createDeleteOp, rebuildOp } from './chunk-4RJP2N2L.js';
@@ -10,17 +10,17 @@ import { getTexture, chunks, dots, crosshatch, hatch } from './chunk-BJN5ZRIJ.js
10
10
  import { createContext, createElement, forwardRef, useMemo, useRef, useCallback, useState, useLayoutEffect, useEffect, useImperativeHandle, useSyncExternalStore, useContext, useId, useReducer } from 'react';
11
11
  import { GLYPH_COVERAGE_GLSL, subscribeGlyphReady, DEFAULT_BAKE_BUDGET, markAllFontsNotUploaded, resetBakeBudget, syncDynamicPageTexture, ensureFontTexture, dynamicPageTextureId, textureCacheKey, GLYPH_MODE_R8, GLYPH_MODE_MSDF } from '@weasel-js/font';
12
12
  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';
13
- import { dashForStrokeStyle, strokeDashStyleOf, resolveScreenLength, alignedStrokeRect, lerpColorArray, srgbU8ToOklab, oklabToOklch, oklchToOklab, oklabToSrgbU8 } from '@weasel-js/paint';
14
- export { STROKE_DASH_RATIOS, alignedStrokeRect, dashForStrokeStyle, lerpColorArray, lerpOklab, lerpOklch, oklabToOklch, oklabToSrgbU8, oklchToOklab, resolveScreenLength, srgbU8ToOklab, strokeDashStyleOf } from '@weasel-js/paint';
13
+ import { hexToColorLiteral, resolvePaletteColor, colorLiteralToHex, dashForStrokeStyle, strokeDashStyleOf, resolveScreenLength, alignedStrokeRect, lerpColorArray, hslToSrgb, srgbFloatToOklab, oklabToOklch, oklchToOklab, oklabToSrgbU8 } from '@weasel-js/paint';
14
+ export { STROKE_DASH_RATIOS, alignedStrokeRect, contrastLineColor, dashForStrokeStyle, hexToOklchDeg, hslToSrgb, interpolateSrgb, lerpColorArray, lerpOklab, lerpOklch, linearSrgbToOklab, linearToSrgb, oklabToLinearSrgb, oklabToOklch, oklabToSrgbU8, oklchDegToHex, oklchToOklab, resolveScreenLength, srgbFloatToOklab, srgbToHsl, srgbToLinear, srgbU8ToOklab, strokeDashStyleOf } from '@weasel-js/paint';
15
15
  import { boxToBox, forEachSegment, multiply as multiply$1, rotate, translate as translate$1, rectToContour, invert as invert$1, clampRectWithin, applyToPoint, rotateAboutPoint, cubicEvalAt, pointInPolygon, segmentsCross, pathCommandCoordCount, pointSegmentDist2, elevateQuadraticToCubic } from '@weasel-js/geom';
16
16
  import { resolveTextStyle, resolveAlign, resolveRuns, cachedLayoutRuns, verticalAlignOffset, textPoseLayoutInput, runsToPlainText, layoutTextPose, fontString, textLineBoxes } from '@weasel-js/text';
17
- export { DEFAULT_TEXT_STYLE, MARKDOWN_RUN_GRAMMAR, SCRIPT_METRICS, cachedLayoutRuns, createMarkdownRenderer, fontString, layoutMarkdown, layoutRuns, layoutTextPose, markdownToRuns, measureText, measureTextBounds, measuredWidth, resolveAlign, resolveRuns, resolveTextStyle, runsToMarkdown, runsToPlainText, textLineBoxes, textPoseLayoutInput, toRuns, verticalAlignOffset } from '@weasel-js/text';
17
+ export { DEFAULT_TEXT_STYLE, MARKDOWN_RUN_GRAMMAR, SCRIPT_METRICS, cachedLayoutRuns, createMarkdownRenderer, fontString, layoutMarkdown, layoutRuns, layoutTextPose, markdownToRuns, measureText, measureTextBounds, measuredWidth, resolveAlign, resolveRuns, resolveTextStyle, runsToMarkdown, runsToPlainText, textLineBoxes, textPoseLayoutInput, toRuns, transformRunTexts, verticalAlignOffset } from '@weasel-js/text';
18
18
  import earcut from 'earcut';
19
19
  import { isAnchorOrControl, isBody, ActionDisabledReason, scratchKey, resolveParams, createDispatcher, DEFAULT_ALLOWED_CAPABILITIES, itemsFromFiles, evaluate, resolveDeviceProfile, isResizeHandle, isRotateHandle, defineViewportTool as defineViewportTool$1, NEVER, recordModeSwitch, DEFAULT_DEVICE_PROFILE, defineTool as defineTool$1, getScratch, openPointerSession, actionBindings, ALWAYS, clientToCanvas, setScratch, clientToCanvasRect, isEditableTarget } from '@weasel-js/routing';
20
- export { ALWAYS, ActionDisabledReason, COARSE_TARGET_SCALE, DEFAULT_ALLOWED_CAPABILITIES, DEFAULT_DEVICE_PROFILE, NEVER, actionBindings, buildRuleCtx, clientToCanvas, createDispatcher, deleteScratch, describeRule, evaluate, evaluateEnabled, getScratch, isEditableTarget, liveScope, matchesKeyBinding, mergeContributions, openPointerSession, resolveDeviceProfile, resolveParams, scopeBindings, scratchKey, setScratch, specificity } from '@weasel-js/routing';
20
+ export { ALWAYS, ActionDisabledReason, COARSE_TARGET_SCALE, DEFAULT_ALLOWED_CAPABILITIES, DEFAULT_DEVICE_PROFILE, NEVER, actionBindings, actionItems, buildDepsFromRequires, buildRuleCtx, clientToCanvas, createDispatcher, deleteScratch, describeRule, evaluate, evaluateEnabled, getScratch, isEditableTarget, liveScope, matchesKeyBinding, mergeContributions, openPointerSession, resolveDeviceProfile, resolveParams, scopeBindings, scratchKey, setScratch, specificity } from '@weasel-js/routing';
21
21
  import { ActiveToolContextProviderIfRoot, useOptionalActiveToolContext, useTools as useTools$1, useOptionalDepRegistry, DepRegistryProvider, useActionsRegistry, ActionsScope, ActionsProvider, useDepRegistry, useGestureDispatcher, useDepSource, ActiveToolContextProvider } from '@weasel-js/routing/react';
22
- export { ActionsProvider, ActionsScope, ActiveToolContextProvider, ActiveToolContextProviderIfRoot, DepRegistryProvider, useAction, useActionsRegistry, useActiveToolContext, useDepRegistry, useDepSource, useGestureDispatcher, useOptionalActiveToolContext, useOptionalDepRegistry } from '@weasel-js/routing/react';
23
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
22
+ export { ActionsProvider, ActionsScope, ActiveToolContextProvider, ActiveToolContextProviderIfRoot, DepRegistryProvider, useAction, useActionsRegistry, useActiveToolContext, useDepRegistry, useDepSource, useGestureDispatcher, useOngoingAction, useOptionalActiveToolContext, useOptionalDepRegistry } from '@weasel-js/routing/react';
23
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
24
24
  import { pathCrop as pathCrop$1, pathDivide as pathDivide$1, pathUnion as pathUnion$1, pathSubtract as pathSubtract$1, pathExclude as pathExclude$1, pathIntersect as pathIntersect$1 } from '@weasel-js/geom/booleans';
25
25
  import { mimeMatchesGlob } from '@weasel-js/gestures';
26
26
  import { createPaintedCursorState, GLYPHS, cursorWorldSize, cursorPaintOps, chromeLineWidthScale, cursorPaintMatrix, resolveCursorTier, cursorFor } from '@weasel-js/cursor';
@@ -29,7 +29,7 @@ import { createHistory } from '@weasel-js/history';
29
29
  export * from '@weasel-js/history';
30
30
 
31
31
  // src/version.ts
32
- var VERSION = "1.5.1" ;
32
+ var VERSION = "1.6.0" ;
33
33
 
34
34
  // src/features/grid/roundToCell.ts
35
35
  function roundToCell(value, cellSize) {
@@ -757,6 +757,16 @@ var GLTextureCache = class {
757
757
  };
758
758
 
759
759
  // src/renderer/cache/GLImageCache.ts
760
+ var DEFAULT_MAX_TEXTURE_SIDE = 4096;
761
+ var live = /* @__PURE__ */ new Set();
762
+ function releaseImageSource(key) {
763
+ for (const cache7 of live) cache7.release(key);
764
+ }
765
+ function maxImageTextureSide() {
766
+ let side = Infinity;
767
+ for (const cache7 of live) side = Math.min(side, cache7.maxTextureSide);
768
+ return Number.isFinite(side) ? side : DEFAULT_MAX_TEXTURE_SIDE;
769
+ }
760
770
  var GLImageCache = class {
761
771
  /** `minification` selects the MIN_FILTER strategy for uploaded textures.
762
772
  * `'linear'` (default) is the screen path's existing behavior. `'mipmap'`
@@ -767,9 +777,14 @@ var GLImageCache = class {
767
777
  constructor(gl, minification = "linear") {
768
778
  this.gl = gl;
769
779
  this.minification = minification;
780
+ const reported = gl.getParameter(gl.MAX_TEXTURE_SIZE);
781
+ this.maxTextureSide = typeof reported === "number" && reported > 0 ? reported : DEFAULT_MAX_TEXTURE_SIDE;
782
+ live.add(this);
770
783
  }
771
784
  gl;
772
785
  minification;
786
+ /** This context's `MAX_TEXTURE_SIZE`. */
787
+ maxTextureSide;
773
788
  map = /* @__PURE__ */ new WeakMap();
774
789
  /**
775
790
  * MAG_FILTER each texture currently carries, so a redundant write can be
@@ -788,6 +803,19 @@ var GLImageCache = class {
788
803
  * was redundant either way.
789
804
  */
790
805
  magFilters = /* @__PURE__ */ new WeakMap();
806
+ /** Delete `key`'s texture, if it has one. */
807
+ release(key) {
808
+ const tex = this.map.get(key);
809
+ if (!tex) return;
810
+ this.gl.deleteTexture(tex);
811
+ this.map.delete(key);
812
+ this.magFilters.delete(key);
813
+ }
814
+ /** Stop answering {@link releaseImageSource} and {@link maxImageTextureSide}.
815
+ * Call when the context is disposed or lost. */
816
+ dispose() {
817
+ live.delete(this);
818
+ }
791
819
  upload(key, source, repetition) {
792
820
  const existing = this.map.get(key);
793
821
  if (existing) return existing;
@@ -839,8 +867,6 @@ function wrapModes(gl, rep) {
839
867
  return [gl.CLAMP_TO_EDGE, gl.CLAMP_TO_EDGE];
840
868
  }
841
869
  }
842
-
843
- // src/renderer/math/color.ts
844
870
  function parseColor(input) {
845
871
  const cached = CACHE.get(input);
846
872
  if (cached !== void 0) return cached;
@@ -1033,21 +1059,7 @@ function parsePercentOrNumber(tok) {
1033
1059
  const n = Number(tok);
1034
1060
  return Number.isFinite(n) ? clamp01(n) : null;
1035
1061
  }
1036
- function hslToRgb(h, s, l) {
1037
- if (s === 0) return [l, l, l];
1038
- const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
1039
- const p = 2 * l - q;
1040
- const hk = h / 360;
1041
- return [hueToChannel(p, q, hk + 1 / 3), hueToChannel(p, q, hk), hueToChannel(p, q, hk - 1 / 3)];
1042
- }
1043
- function hueToChannel(p, q, t) {
1044
- if (t < 0) t += 1;
1045
- if (t > 1) t -= 1;
1046
- if (t < 1 / 6) return p + (q - p) * 6 * t;
1047
- if (t < 1 / 2) return q;
1048
- if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
1049
- return p;
1050
- }
1062
+ var hslToRgb = hslToSrgb;
1051
1063
  function clamp01(n) {
1052
1064
  return n < 0 ? 0 : n > 1 ? 1 : n;
1053
1065
  }
@@ -1517,9 +1529,15 @@ function scaled(m, sx, sy) {
1517
1529
  const s = create(sx, 0, 0, sy, 0, 0);
1518
1530
  return multiply(m, s);
1519
1531
  }
1532
+ function toAffine(m) {
1533
+ return [m[0], m[1], m[3], m[4], m[6], m[7]];
1534
+ }
1535
+ function fromAffine(a) {
1536
+ return create(a[0], a[1], a[2], a[3], a[4], a[5]);
1537
+ }
1520
1538
  function invert(m) {
1521
- const inv = invert$1([m[0], m[1], m[3], m[4], m[6], m[7]]);
1522
- return inv && create(inv[0], inv[1], inv[2], inv[3], inv[4], inv[5]);
1539
+ const inv = invert$1(toAffine(m));
1540
+ return inv && fromAffine(inv);
1523
1541
  }
1524
1542
  function apply(m, x, y) {
1525
1543
  const a = m[0], b = m[1];
@@ -1548,6 +1566,8 @@ function meanScaleOf(m) {
1548
1566
  }
1549
1567
  var mat3 = {
1550
1568
  identity,
1569
+ toAffine,
1570
+ fromAffine,
1551
1571
  multiply,
1552
1572
  translated,
1553
1573
  scaled,
@@ -5009,31 +5029,217 @@ var EffectTargets = class {
5009
5029
  }
5010
5030
  };
5011
5031
 
5012
- // src/renderer/WeaselRenderer.ts
5013
- var UNIFORM_QUALIFIER = /^(?:lowp|mediump|highp|precise|invariant|centroid|flat|smooth|noperspective)$/;
5032
+ // src/renderer/shaders/extractUniformNames.ts
5033
+ var QUALIFIERS = /* @__PURE__ */ new Set([
5034
+ "lowp",
5035
+ "mediump",
5036
+ "highp",
5037
+ "precise",
5038
+ "invariant",
5039
+ "centroid",
5040
+ "flat",
5041
+ "smooth",
5042
+ "noperspective"
5043
+ ]);
5044
+ var IDENT = /^[A-Za-z_]\w*$/;
5045
+ var MAX_DEPTH = 16;
5014
5046
  function extractUniformNames(glsl) {
5015
- const re = /\buniform\s+([^;{]+);/g;
5016
- const names = [];
5017
- let m;
5018
- while ((m = re.exec(glsl)) !== null) {
5019
- const words = m[1].trim().split(/\s+/);
5020
- let i = 0;
5021
- while (i < words.length && UNIFORM_QUALIFIER.test(words[i])) i++;
5022
- i++;
5023
- const declarators = words.slice(i).join(" ");
5024
- if (!declarators) continue;
5025
- for (const raw of declarators.split(",")) {
5026
- const d = raw.trim();
5027
- const arr = /^(\w+)\s*\[\s*(\d+)\s*\]$/.exec(d);
5028
- if (arr) {
5029
- const size = parseInt(arr[2], 10);
5030
- for (let k = 0; k < size; k++) names.push(`${arr[1]}[${k}]`);
5031
- } else if (/^\w+$/.test(d)) {
5032
- names.push(d);
5033
- }
5034
- }
5035
- }
5036
- return names;
5047
+ const { code, defines } = preprocess(glsl);
5048
+ const toks = tokenize2(code);
5049
+ const consts = /* @__PURE__ */ new Map();
5050
+ const structs = /* @__PURE__ */ new Map();
5051
+ const out = /* @__PURE__ */ new Set();
5052
+ const resolveName = (name, depth2) => {
5053
+ if (depth2 > MAX_DEPTH) return null;
5054
+ let best = null;
5055
+ for (const body of defines.get(name) ?? []) {
5056
+ const v = evalInt(tokenize2(body), (n) => resolveName(n, depth2 + 1));
5057
+ if (v !== null && (best === null || v > best)) best = v;
5058
+ }
5059
+ return best ?? consts.get(name) ?? null;
5060
+ };
5061
+ const evalSize = (from, to) => evalInt(toks.slice(from, to), (n) => resolveName(n, 0));
5062
+ const closer = (open) => {
5063
+ let depth2 = 0;
5064
+ for (let k = open; k < toks.length; k++) {
5065
+ if (toks[k] === "(" || toks[k] === "[" || toks[k] === "{") depth2++;
5066
+ else if (toks[k] === ")" || toks[k] === "]" || toks[k] === "}") {
5067
+ if (--depth2 === 0) return k;
5068
+ }
5069
+ }
5070
+ return toks.length;
5071
+ };
5072
+ const arraySuffix = (j) => {
5073
+ if (toks[j] !== "[") return [void 0, j];
5074
+ const end = closer(j);
5075
+ return [evalSize(j + 1, end), end + 1];
5076
+ };
5077
+ const declarators = (j, typeSize) => {
5078
+ const found = [];
5079
+ while (j < toks.length && toks[j] !== ";" && toks[j] !== "}") {
5080
+ const name = toks[j];
5081
+ const [size, next] = arraySuffix(j + 1);
5082
+ j = next;
5083
+ if (IDENT.test(name)) found.push({ name, size: size === void 0 ? typeSize : size });
5084
+ while (j < toks.length && toks[j] !== "," && toks[j] !== ";" && toks[j] !== "}") {
5085
+ j = toks[j] === "(" || toks[j] === "[" || toks[j] === "{" ? closer(j) + 1 : j + 1;
5086
+ }
5087
+ if (toks[j] === ",") j++;
5088
+ }
5089
+ return [found, j];
5090
+ };
5091
+ const structSpecifier = (i) => {
5092
+ let j = i + 1;
5093
+ const name = toks[j] !== "{" ? toks[j++] : void 0;
5094
+ const members = [];
5095
+ if (toks[j] !== "{") return [members, j];
5096
+ const end = closer(j);
5097
+ j++;
5098
+ while (j < end) {
5099
+ while (QUALIFIERS.has(toks[j])) j++;
5100
+ let type;
5101
+ if (toks[j] === "struct") [type, j] = structSpecifier(j);
5102
+ else type = toks[j++];
5103
+ const [typeSize, afterType] = arraySuffix(j);
5104
+ const [decls, term] = declarators(afterType, typeSize);
5105
+ for (const d of decls) if (d.size !== null) members.push({ type, name: d.name, size: d.size ?? null });
5106
+ j = term + 1;
5107
+ }
5108
+ if (name) {
5109
+ const prior = structs.get(name) ?? [];
5110
+ for (const m of members) if (!prior.some((p) => p.name === m.name)) prior.push(m);
5111
+ structs.set(name, prior);
5112
+ }
5113
+ return [members, end + 1];
5114
+ };
5115
+ const expand = (name, type, size, depth2) => {
5116
+ if (depth2 > MAX_DEPTH) return;
5117
+ if (size !== null) {
5118
+ for (let k = 0; k < size; k++) expand(`${name}[${k}]`, type, null, depth2 + 1);
5119
+ return;
5120
+ }
5121
+ const members = typeof type === "string" ? structs.get(type) : type;
5122
+ if (!members) {
5123
+ out.add(name);
5124
+ return;
5125
+ }
5126
+ for (const m of members) expand(`${name}.${m.name}`, m.type, m.size, depth2 + 1);
5127
+ };
5128
+ let depth = 0;
5129
+ for (let i = 0; i < toks.length; i++) {
5130
+ const t = toks[i];
5131
+ if (t === "{") depth++;
5132
+ else if (t === "}") depth--;
5133
+ else if (t === "struct") i = structSpecifier(i)[1] - 1;
5134
+ else if (depth === 0 && t === "const") {
5135
+ let j = i + 1;
5136
+ while (QUALIFIERS.has(toks[j])) j++;
5137
+ if ((toks[j] === "int" || toks[j] === "uint") && IDENT.test(toks[j + 1] ?? "") && toks[j + 2] === "=") {
5138
+ let end = j + 3;
5139
+ while (end < toks.length && toks[end] !== ";") end++;
5140
+ const v = evalSize(j + 3, end);
5141
+ const prior = consts.get(toks[j + 1]);
5142
+ if (v !== null && (prior === void 0 || v > prior)) consts.set(toks[j + 1], v);
5143
+ }
5144
+ } else if (depth === 0 && t === "uniform") {
5145
+ let j = i + 1;
5146
+ while (QUALIFIERS.has(toks[j])) j++;
5147
+ let type;
5148
+ if (toks[j] === "struct") {
5149
+ [type, j] = structSpecifier(j);
5150
+ } else if (toks[j + 1] === "{") {
5151
+ j = closer(j + 1) + 1;
5152
+ while (j < toks.length && toks[j] !== ";") j++;
5153
+ i = j;
5154
+ continue;
5155
+ } else {
5156
+ type = toks[j++];
5157
+ }
5158
+ const [typeSize, afterType] = arraySuffix(j);
5159
+ const [decls, term] = declarators(afterType, typeSize);
5160
+ for (const d of decls) if (d.size !== null) expand(d.name, type, d.size ?? null, 0);
5161
+ i = term;
5162
+ }
5163
+ }
5164
+ return [...out];
5165
+ }
5166
+ function preprocess(glsl) {
5167
+ const spliced = glsl.replace(/\\\r?\n/g, "");
5168
+ let stripped = "";
5169
+ for (let k = 0; k < spliced.length; k++) {
5170
+ if (spliced[k] === "/" && spliced[k + 1] === "/") {
5171
+ while (k < spliced.length && spliced[k] !== "\n") k++;
5172
+ stripped += "\n";
5173
+ } else if (spliced[k] === "/" && spliced[k + 1] === "*") {
5174
+ const end = spliced.indexOf("*/", k + 2);
5175
+ k = end < 0 ? spliced.length : end + 1;
5176
+ stripped += " ";
5177
+ } else {
5178
+ stripped += spliced[k];
5179
+ }
5180
+ }
5181
+ const defines = /* @__PURE__ */ new Map();
5182
+ const lines = stripped.split("\n").map((line) => {
5183
+ if (!/^\s*#/.test(line)) return line;
5184
+ const def = /^\s*#\s*define\s+([A-Za-z_]\w*)(\(?)(.*)$/.exec(line);
5185
+ if (def && !def[2]) defines.set(def[1], [...defines.get(def[1]) ?? [], def[3].trim()]);
5186
+ return "";
5187
+ });
5188
+ return { code: lines.join("\n"), defines };
5189
+ }
5190
+ function tokenize2(src) {
5191
+ return src.match(/[A-Za-z_]\w*|\d[\w.]*|\S/g) ?? [];
5192
+ }
5193
+ function intLiteral(tok) {
5194
+ const m = /^(?:0[xX]([0-9a-fA-F]+)|(0[0-7]*)|([1-9]\d*))[uU]?$/.exec(tok);
5195
+ if (!m) return null;
5196
+ if (m[1] !== void 0) return parseInt(m[1], 16);
5197
+ if (m[2] !== void 0) return parseInt(m[2], 8);
5198
+ return parseInt(m[3], 10);
5199
+ }
5200
+ function evalInt(toks, lookup) {
5201
+ let p = 0;
5202
+ const primary = () => {
5203
+ const t = toks[p++];
5204
+ if (t === "(") {
5205
+ const v2 = sum();
5206
+ return toks[p++] === ")" ? v2 : null;
5207
+ }
5208
+ if (t === "-") {
5209
+ const v2 = primary();
5210
+ return v2 === null ? null : -v2;
5211
+ }
5212
+ if (t === "+") return primary();
5213
+ if (t === void 0) return null;
5214
+ return intLiteral(t) ?? (IDENT.test(t) ? lookup(t) : null);
5215
+ };
5216
+ const product = () => {
5217
+ let v2 = primary();
5218
+ while (v2 !== null && (toks[p] === "*" || toks[p] === "/" || toks[p] === "%")) {
5219
+ const op = toks[p++];
5220
+ const r = primary();
5221
+ if (r === null || op !== "*" && r === 0) return null;
5222
+ v2 = op === "*" ? v2 * r : op === "/" ? Math.trunc(v2 / r) : v2 % r;
5223
+ }
5224
+ return v2;
5225
+ };
5226
+ const sum = () => {
5227
+ let v2 = product();
5228
+ while (v2 !== null && (toks[p] === "+" || toks[p] === "-")) {
5229
+ const op = toks[p++];
5230
+ const r = product();
5231
+ if (r === null) return null;
5232
+ v2 = op === "+" ? v2 + r : v2 - r;
5233
+ }
5234
+ return v2;
5235
+ };
5236
+ const v = sum();
5237
+ return p === toks.length && v !== null && v >= 0 ? v : null;
5238
+ }
5239
+
5240
+ // src/renderer/WeaselRenderer.ts
5241
+ function customUniformNames(vertSrc, fragSrc) {
5242
+ return [...CUSTOM_KIT_UNIFORMS, ...extractUniformNames(vertSrc), ...extractUniformNames(fragSrc)];
5037
5243
  }
5038
5244
  var WeaselRenderer = class {
5039
5245
  gl;
@@ -5170,11 +5376,7 @@ var WeaselRenderer = class {
5170
5376
  const vertSrc = src.vert === "" ? CUSTOM_VERT_SRC : src.vert;
5171
5377
  const fragSrc = src.frag;
5172
5378
  const program = new ShaderProgram(this.gl, vertSrc, fragSrc);
5173
- program.lookupUniforms([
5174
- ...CUSTOM_KIT_UNIFORMS,
5175
- ...extractUniformNames(vertSrc),
5176
- ...extractUniformNames(fragSrc)
5177
- ]);
5379
+ program.lookupUniforms(customUniformNames(vertSrc, fragSrc));
5178
5380
  program.lookupAttributes(CUSTOM_ATTRIBUTES);
5179
5381
  const previous = this.programRegistry.get(handle.id);
5180
5382
  if (previous) this.gl.deleteProgram(previous.handle);
@@ -5269,6 +5471,7 @@ var WeaselRenderer = class {
5269
5471
  if (aPos === void 0) throw new Error("a_position missing after restore");
5270
5472
  this.meshCache = new GLMeshCache(this.gl, aPos);
5271
5473
  this.textureCache = new GLTextureCache(this.gl);
5474
+ this.imageCache.dispose();
5272
5475
  this.imageCache = new GLImageCache(this.gl, this.imageMinification);
5273
5476
  this.gradRamps = new GradientRampAtlas(this.gl);
5274
5477
  markAllFontsNotUploaded();
@@ -5281,7 +5484,7 @@ var WeaselRenderer = class {
5281
5484
  const vertSrc = src.vert === "" ? CUSTOM_VERT_SRC : src.vert;
5282
5485
  try {
5283
5486
  const program = new ShaderProgram(this.gl, vertSrc, src.frag);
5284
- program.lookupUniforms([...CUSTOM_KIT_UNIFORMS, ...extractUniformNames(src.frag)]);
5487
+ program.lookupUniforms(customUniformNames(vertSrc, src.frag));
5285
5488
  program.lookupAttributes(CUSTOM_ATTRIBUTES);
5286
5489
  this.programRegistry.set(id, program);
5287
5490
  } catch (e) {
@@ -5331,6 +5534,7 @@ var WeaselRenderer = class {
5331
5534
  }
5332
5535
  this.programRegistry.clear();
5333
5536
  this.textureCache.free();
5537
+ this.imageCache.dispose();
5334
5538
  this.gradRamps.free();
5335
5539
  if (this.quadVbo) gl.deleteBuffer(this.quadVbo);
5336
5540
  if (this.quadIbo) gl.deleteBuffer(this.quadIbo);
@@ -5835,32 +6039,32 @@ function drawLayers(layers, data, visibility, order, view, dims, cache7, onLayer
5835
6039
  }
5836
6040
  const groupOf = groupMembership(groups, layerById.keys());
5837
6041
  const bracketed = /* @__PURE__ */ new Set();
5838
- let run = null;
6042
+ let run2 = null;
5839
6043
  const closeRun = () => {
5840
- if (!run) return;
5841
- for (const c of wrapGroup(run.group, run.children, v, dims)) out.push(c);
5842
- run = null;
6044
+ if (!run2) return;
6045
+ for (const c of wrapGroup(run2.group, run2.children, v, dims)) out.push(c);
6046
+ run2 = null;
5843
6047
  };
5844
6048
  for (const layer of sequence) {
5845
6049
  if (!isLayerVisible(layer, visibility)) continue;
5846
6050
  const cmds = drawOneLayer(layer, data, v, dims, cache7, onLayerError);
5847
6051
  if (cmds.length === 0) continue;
5848
6052
  const group = groupOf?.get(layer.id);
5849
- if (run && run.group !== group) closeRun();
6053
+ if (run2 && run2.group !== group) closeRun();
5850
6054
  if (!group) {
5851
6055
  for (const c of cmds) out.push(c);
5852
6056
  continue;
5853
6057
  }
5854
- if (!run) {
6058
+ if (!run2) {
5855
6059
  if (bracketed.has(group)) {
5856
6060
  console.warn(
5857
6061
  `[weasel] layer group "${group.id}" is not one run \u2014 another layer is drawn between its members, so it composites as more than one pass over more than one buffer. Move its members together in the layer order.`
5858
6062
  );
5859
6063
  }
5860
6064
  bracketed.add(group);
5861
- run = { group, children: [] };
6065
+ run2 = { group, children: [] };
5862
6066
  }
5863
- for (const c of cmds) run.children.push(c);
6067
+ for (const c of cmds) run2.children.push(c);
5864
6068
  }
5865
6069
  closeRun();
5866
6070
  return out;
@@ -6656,7 +6860,7 @@ function composeWorldPose(adapter, id, compose2) {
6656
6860
  while (cursor !== null) {
6657
6861
  if (seen.has(cursor)) break;
6658
6862
  seen.add(cursor);
6659
- chain.push(cursor);
6863
+ if (adapter.definesFrame?.(cursor) ?? true) chain.push(cursor);
6660
6864
  cursor = adapter.getParent(cursor);
6661
6865
  }
6662
6866
  let world = adapter.getPose(chain[chain.length - 1]);
@@ -6676,9 +6880,19 @@ function translateRectPose(pose, dx, dy) {
6676
6880
  return { ...pose, x: pose.x + dx, y: pose.y + dy };
6677
6881
  }
6678
6882
  function rebaseLocalPose(adapter, worldPose, newParentId, compose2, decompose) {
6679
- if (newParentId === null) return worldPose;
6680
- const parentWorld = composeWorldPose(adapter, newParentId, compose2);
6681
- return decompose(parentWorld, worldPose);
6883
+ const frame = frameAtOrAbove(adapter, newParentId);
6884
+ if (frame === null) return worldPose;
6885
+ return decompose(composeWorldPose(adapter, frame, compose2), worldPose);
6886
+ }
6887
+ function frameAtOrAbove(adapter, id) {
6888
+ const seen = /* @__PURE__ */ new Set();
6889
+ let cursor = id;
6890
+ while (cursor !== null && !seen.has(cursor)) {
6891
+ if (adapter.definesFrame?.(cursor) ?? true) return cursor;
6892
+ seen.add(cursor);
6893
+ cursor = adapter.getParent(cursor);
6894
+ }
6895
+ return null;
6682
6896
  }
6683
6897
  function decomposeRectPose(parent, world) {
6684
6898
  return {
@@ -6744,6 +6958,88 @@ function worldPoseLookup(adapter, compose2) {
6744
6958
  };
6745
6959
  }
6746
6960
 
6961
+ // src/core/scene/poseSnapshot.ts
6962
+ var DEPTH = 4;
6963
+ function isPlainObject(v) {
6964
+ if (typeof v !== "object" || v === null) return false;
6965
+ const proto = Object.getPrototypeOf(v);
6966
+ return proto === Object.prototype || proto === null;
6967
+ }
6968
+ function isTypedArray(v) {
6969
+ return ArrayBuffer.isView(v) && !(v instanceof DataView);
6970
+ }
6971
+ function snapshotPose(value, depth = DEPTH) {
6972
+ if (depth <= 0) return value;
6973
+ if (Array.isArray(value)) {
6974
+ return value.map((v) => snapshotPose(v, depth - 1));
6975
+ }
6976
+ if (isTypedArray(value)) return value.slice();
6977
+ if (isPlainObject(value)) {
6978
+ const out = { ...value };
6979
+ for (const key in out) {
6980
+ const field = out[key];
6981
+ if (typeof field === "object" && field !== null) out[key] = snapshotPose(field, depth - 1);
6982
+ }
6983
+ return out;
6984
+ }
6985
+ return value;
6986
+ }
6987
+ function samePoseValue(snapshot, value, depth = DEPTH) {
6988
+ if (snapshot === value) return true;
6989
+ if (typeof snapshot !== "object" || snapshot === null || typeof value !== "object" || value === null) return Object.is(snapshot, value);
6990
+ if (depth <= 0) return false;
6991
+ const proto = Object.getPrototypeOf(snapshot);
6992
+ if (proto === Object.prototype || proto === null) {
6993
+ const other = Object.getPrototypeOf(value);
6994
+ if (other !== Object.prototype && other !== null) return false;
6995
+ const a = snapshot;
6996
+ const b = value;
6997
+ let count = 0;
6998
+ for (const key in a) {
6999
+ count++;
7000
+ const left = a[key];
7001
+ const right = b[key];
7002
+ if (left === right) {
7003
+ if (right === void 0 && !(key in b)) return false;
7004
+ continue;
7005
+ }
7006
+ if (!samePoseValue(left, right, depth - 1)) return false;
7007
+ }
7008
+ for (const _key in b) if (--count < 0) return false;
7009
+ return true;
7010
+ }
7011
+ if (Array.isArray(snapshot) || isTypedArray(snapshot)) {
7012
+ const a = snapshot;
7013
+ if (!(Array.isArray(value) || isTypedArray(value))) return false;
7014
+ const b = value;
7015
+ if (a.length !== b.length) return false;
7016
+ for (let i = 0; i < a.length; i++) {
7017
+ if (!samePoseValue(a[i], b[i], depth - 1)) return false;
7018
+ }
7019
+ return true;
7020
+ }
7021
+ return false;
7022
+ }
7023
+
7024
+ // src/core/scene/depMemo.ts
7025
+ function recordDeps(into, deps) {
7026
+ into.nodes.length = 0;
7027
+ into.poses.length = 0;
7028
+ for (const dep of deps) {
7029
+ into.nodes.push(dep?.node);
7030
+ into.poses.push(dep === void 0 ? void 0 : snapshotPose(dep.pose));
7031
+ }
7032
+ }
7033
+ function sameDeps2(record, ids, depOf2) {
7034
+ if (record.nodes.length !== ids.length) return false;
7035
+ for (let i = 0; i < ids.length; i++) {
7036
+ const dep = depOf2(ids[i]);
7037
+ if (record.nodes[i] !== (dep === void 0 ? void 0 : dep.node)) return false;
7038
+ if (dep !== void 0 && !samePoseValue(record.poses[i], dep.pose)) return false;
7039
+ }
7040
+ return true;
7041
+ }
7042
+
6747
7043
  // src/core/scene/dependents.ts
6748
7044
  var NONE = Object.freeze([]);
6749
7045
  function dependencyIdsOf(node, childrenOf) {
@@ -6817,69 +7113,6 @@ function sameDependsOn(a, b) {
6817
7113
  return true;
6818
7114
  }
6819
7115
 
6820
- // src/core/scene/poseSnapshot.ts
6821
- var DEPTH = 4;
6822
- function isPlainObject(v) {
6823
- if (typeof v !== "object" || v === null) return false;
6824
- const proto = Object.getPrototypeOf(v);
6825
- return proto === Object.prototype || proto === null;
6826
- }
6827
- function isTypedArray(v) {
6828
- return ArrayBuffer.isView(v) && !(v instanceof DataView);
6829
- }
6830
- function snapshotPose(value, depth = DEPTH) {
6831
- if (depth <= 0) return value;
6832
- if (Array.isArray(value)) {
6833
- return value.map((v) => snapshotPose(v, depth - 1));
6834
- }
6835
- if (isTypedArray(value)) return value.slice();
6836
- if (isPlainObject(value)) {
6837
- const out = { ...value };
6838
- for (const key in out) {
6839
- const field = out[key];
6840
- if (typeof field === "object" && field !== null) out[key] = snapshotPose(field, depth - 1);
6841
- }
6842
- return out;
6843
- }
6844
- return value;
6845
- }
6846
- function samePoseValue(snapshot, value, depth = DEPTH) {
6847
- if (snapshot === value) return true;
6848
- if (typeof snapshot !== "object" || snapshot === null || typeof value !== "object" || value === null) return Object.is(snapshot, value);
6849
- if (depth <= 0) return false;
6850
- const proto = Object.getPrototypeOf(snapshot);
6851
- if (proto === Object.prototype || proto === null) {
6852
- const other = Object.getPrototypeOf(value);
6853
- if (other !== Object.prototype && other !== null) return false;
6854
- const a = snapshot;
6855
- const b = value;
6856
- let count = 0;
6857
- for (const key in a) {
6858
- count++;
6859
- const left = a[key];
6860
- const right = b[key];
6861
- if (left === right) {
6862
- if (right === void 0 && !(key in b)) return false;
6863
- continue;
6864
- }
6865
- if (!samePoseValue(left, right, depth - 1)) return false;
6866
- }
6867
- for (const _key in b) if (--count < 0) return false;
6868
- return true;
6869
- }
6870
- if (Array.isArray(snapshot) || isTypedArray(snapshot)) {
6871
- const a = snapshot;
6872
- if (!(Array.isArray(value) || isTypedArray(value))) return false;
6873
- const b = value;
6874
- if (a.length !== b.length) return false;
6875
- for (let i = 0; i < a.length; i++) {
6876
- if (!samePoseValue(a[i], b[i], depth - 1)) return false;
6877
- }
6878
- return true;
6879
- }
6880
- return false;
6881
- }
6882
-
6883
7116
  // src/core/scene/derivedPath.ts
6884
7117
  var SLOT = "kit:derivedPath";
6885
7118
  var resolving = /* @__PURE__ */ new Set();
@@ -6901,7 +7134,7 @@ function resolveDerivedPath(node, depOf2, childrenOf, memoSlot = SLOT) {
6901
7134
  computed = true;
6902
7135
  return draw(node, derivePath, ids.map(depOf2), { nodes: [], poses: [], path: null });
6903
7136
  });
6904
- if (!computed && !matches(record, ids, depOf2)) {
7137
+ if (!computed && !sameDeps2(record, ids, depOf2)) {
6905
7138
  draw(node, derivePath, ids.map(depOf2), record);
6906
7139
  }
6907
7140
  if (cycleHits !== hitsBefore) dropPoseKeyedMemoSlots(node);
@@ -6912,23 +7145,9 @@ function resolveDerivedPath(node, depOf2, childrenOf, memoSlot = SLOT) {
6912
7145
  }
6913
7146
  function draw(node, derivePath, deps, into) {
6914
7147
  into.path = derivePath(node, deps);
6915
- into.nodes.length = 0;
6916
- into.poses.length = 0;
6917
- for (const dep of deps) {
6918
- into.nodes.push(dep?.node);
6919
- into.poses.push(dep === void 0 ? void 0 : snapshotPose(dep.pose));
6920
- }
7148
+ recordDeps(into, deps);
6921
7149
  return into;
6922
7150
  }
6923
- function matches(record, ids, depOf2) {
6924
- if (record.nodes.length !== ids.length) return false;
6925
- for (let i = 0; i < ids.length; i++) {
6926
- const dep = depOf2(ids[i]);
6927
- if (record.nodes[i] !== (dep === void 0 ? void 0 : dep.node)) return false;
6928
- if (dep !== void 0 && !samePoseValue(record.poses[i], dep.pose)) return false;
6929
- }
6930
- return true;
6931
- }
6932
7151
 
6933
7152
  // src/core/scene/effectivePose.ts
6934
7153
  var POSE_SLOT = {
@@ -6956,18 +7175,26 @@ function derivedPoseIn(source, node, reading) {
6956
7175
  resolving2.add(node.id);
6957
7176
  const hitsBefore = cycleHits2;
6958
7177
  try {
6959
- const value = nodeMemo(
6960
- node,
6961
- POSE_SLOT[reading],
6962
- node.pose,
6963
- () => derive(node, ids.map((id) => depIn(source, id, reading)))
6964
- );
7178
+ const depOf2 = (id) => depIn(source, id, reading);
7179
+ let computed = false;
7180
+ const record = nodeMemo(node, POSE_SLOT[reading], node.pose, () => {
7181
+ computed = true;
7182
+ return run(node, derive, ids.map(depOf2), { nodes: [], poses: [], value: null });
7183
+ });
7184
+ if (!computed && !sameDeps2(record, ids, depOf2)) {
7185
+ run(node, derive, ids.map(depOf2), record);
7186
+ }
6965
7187
  if (cycleHits2 !== hitsBefore) dropPoseKeyedMemoSlots(node);
6966
- return value;
7188
+ return record.value;
6967
7189
  } finally {
6968
7190
  resolving2.delete(node.id);
6969
7191
  }
6970
7192
  }
7193
+ function run(node, derive, deps, into) {
7194
+ into.value = derive(node, deps);
7195
+ recordDeps(into, deps);
7196
+ return into;
7197
+ }
6971
7198
  function derivedDepOf(source, id) {
6972
7199
  return depIn(source, id, "effective");
6973
7200
  }
@@ -7012,6 +7239,9 @@ function documentPose(source, node) {
7012
7239
  function effectivePose(source, node) {
7013
7240
  return poseIn(source, node, "effective");
7014
7241
  }
7242
+ function definesFrame(node) {
7243
+ return !(node.dependsOn === "children" && node.derivePose !== void 0);
7244
+ }
7015
7245
  function poseRotationOf(pose) {
7016
7246
  const p = pose;
7017
7247
  if (p.rotation && p.x != null && p.y != null && p.width != null && p.height != null) {
@@ -7222,14 +7452,14 @@ function defaultRectEl(target) {
7222
7452
  }
7223
7453
  function useHandleDrag(opts) {
7224
7454
  const { onMove, onStart, onEnd, onCancel, getRect } = opts;
7225
- const live = useRef(null);
7455
+ const live2 = useRef(null);
7226
7456
  useEffect(() => () => {
7227
- live.current?.cancel();
7457
+ live2.current?.cancel();
7228
7458
  }, []);
7229
7459
  const onPointerDown = useCallback(
7230
7460
  (e) => {
7231
7461
  e.preventDefault();
7232
- live.current?.cancel();
7462
+ live2.current?.cancel();
7233
7463
  const target = e.currentTarget;
7234
7464
  const rectEl = getRect ? getRect(target) : defaultRectEl(target);
7235
7465
  const rect = rectEl.getBoundingClientRect();
@@ -7238,17 +7468,17 @@ function useHandleDrag(opts) {
7238
7468
  return { x, y };
7239
7469
  };
7240
7470
  let moved = false;
7241
- live.current = openPointerSession(target, e, {
7471
+ live2.current = openPointerSession(target, e, {
7242
7472
  onMove: (ev) => {
7243
7473
  moved = true;
7244
7474
  onMove(localOf(ev.clientX, ev.clientY), ev);
7245
7475
  },
7246
7476
  onEnd: (ev) => {
7247
- live.current = null;
7477
+ live2.current = null;
7248
7478
  onEnd?.({ point: localOf(ev.clientX, ev.clientY), moved, event: ev });
7249
7479
  },
7250
7480
  onCancel: (reason) => {
7251
- live.current = null;
7481
+ live2.current = null;
7252
7482
  onCancel?.(reason);
7253
7483
  }
7254
7484
  });
@@ -7354,9 +7584,9 @@ var DRAG_THRESHOLD_PX_SQ = 25;
7354
7584
  function useDragHandle(getPayload, options) {
7355
7585
  const optsRef = useRef(options);
7356
7586
  optsRef.current = options;
7357
- const live = useRef(null);
7587
+ const live2 = useRef(null);
7358
7588
  useEffect(() => () => {
7359
- live.current?.cancel();
7589
+ live2.current?.cancel();
7360
7590
  }, []);
7361
7591
  const onPointerDown = useCallback((e) => {
7362
7592
  if (e.pointerType === "mouse" && e.button !== 0) return;
@@ -7366,7 +7596,7 @@ function useDragHandle(getPayload, options) {
7366
7596
  const startY = e.clientY;
7367
7597
  let ghost = null;
7368
7598
  let past = false;
7369
- live.current = openPointerSession(target, e, {
7599
+ live2.current = openPointerSession(target, e, {
7370
7600
  onMove: (ev) => {
7371
7601
  if (ghost) {
7372
7602
  ghost.move(ev);
@@ -7396,11 +7626,11 @@ function useDragHandle(getPayload, options) {
7396
7626
  );
7397
7627
  },
7398
7628
  onEnd: (ev) => {
7399
- live.current = null;
7629
+ live2.current = null;
7400
7630
  ghost?.drop(ev);
7401
7631
  },
7402
7632
  onCancel: () => {
7403
- live.current = null;
7633
+ live2.current = null;
7404
7634
  ghost?.cancel();
7405
7635
  }
7406
7636
  }, { capture: false });
@@ -7763,10 +7993,11 @@ function keySpecShortcut(spec) {
7763
7993
  shift: mods?.shift === true
7764
7994
  };
7765
7995
  }
7766
- function actionShortcuts(action) {
7996
+ function actionShortcuts(action, params) {
7767
7997
  const out = [];
7768
7998
  const seen = /* @__PURE__ */ new Set();
7769
- for (const { spec } of actionBindings(action)) {
7999
+ for (const { spec, opts } of actionBindings(action)) {
8000
+ if (params && !paramsMatch(opts?.params, params)) continue;
7770
8001
  const shortcut = keySpecShortcut(spec);
7771
8002
  if (!shortcut) continue;
7772
8003
  const token = `${shortcut.key}|${shortcut.mod}|${shortcut.alt}|${shortcut.shift}`;
@@ -7776,6 +8007,10 @@ function actionShortcuts(action) {
7776
8007
  }
7777
8008
  return out;
7778
8009
  }
8010
+ function paramsMatch(bound, wanted) {
8011
+ if (typeof bound !== "object" || bound === null) return false;
8012
+ return Object.entries(wanted).every(([k, v]) => bound[k] === v);
8013
+ }
7779
8014
 
7780
8015
  // src/core/scene/types.ts
7781
8016
  var asNodeId = (s) => s;
@@ -7975,7 +8210,11 @@ function runLayoutPass(scratch, moveCtx) {
7975
8210
  y: moveCtx.drag.current.y - selectionCenter.y
7976
8211
  };
7977
8212
  const candidates = [];
7978
- const testInside = (cPose, aabb, layout2) => {
8213
+ const mode = layoutDep.dropTarget ?? "innermost";
8214
+ const testInside = (id, cPose, aabb, layout2) => {
8215
+ const region = layout2.dropRegion?.({ id, bounds: aabb }) ?? null;
8216
+ if (region) return regionContains(region, aabb, selectionCenter);
8217
+ if (mode === "region") return false;
7979
8218
  if (layout2.contains) return layout2.contains(cPose, selectionCenter);
7980
8219
  return selectionCenter.x >= aabb.x && selectionCenter.x < aabb.x + aabb.width && selectionCenter.y >= aabb.y && selectionCenter.y < aabb.y + aabb.height;
7981
8220
  };
@@ -7987,7 +8226,7 @@ function runLayoutPass(scratch, moveCtx) {
7987
8226
  if (!node) return;
7988
8227
  const worldPose = composeWorldPose(poseAdapter, id, pc.compose);
7989
8228
  const worldAABB = poseDescriptorForNode(d, node).getBounds(worldPose);
7990
- if (!testInside(worldPose, worldAABB, layout2)) return;
8229
+ if (!testInside(id, worldPose, worldAABB, layout2)) return;
7991
8230
  if (layout2.acceptsDrop) {
7992
8231
  const arg = { id, bounds: worldAABB };
7993
8232
  for (const c of dragged) if (!layout2.acceptsDrop(arg, c.arg)) return;
@@ -8010,28 +8249,33 @@ function runLayoutPass(scratch, moveCtx) {
8010
8249
  };
8011
8250
  walk(null, []);
8012
8251
  let dest = null;
8013
- for (const c of candidates) {
8014
- if (dest === null) {
8015
- dest = c;
8016
- continue;
8017
- }
8018
- if (c.depth > dest.depth) {
8019
- dest = c;
8020
- continue;
8021
- }
8022
- if (c.depth < dest.depth) continue;
8023
- let cAfter = false;
8024
- for (let i = 0; i < c.zPath.length; i++) {
8025
- if (c.zPath[i] > dest.zPath[i]) {
8026
- cAfter = true;
8027
- break;
8252
+ if (mode === "topmost") {
8253
+ const byId = new Map(candidates.map((c) => [c.id, c]));
8254
+ for (const id of scene.renderOrder()) dest = byId.get(id) ?? dest;
8255
+ } else {
8256
+ for (const c of candidates) {
8257
+ if (dest === null) {
8258
+ dest = c;
8259
+ continue;
8028
8260
  }
8029
- if (c.zPath[i] < dest.zPath[i]) {
8030
- cAfter = false;
8031
- break;
8261
+ if (c.depth > dest.depth) {
8262
+ dest = c;
8263
+ continue;
8264
+ }
8265
+ if (c.depth < dest.depth) continue;
8266
+ let cAfter = false;
8267
+ for (let i = 0; i < c.zPath.length; i++) {
8268
+ if (c.zPath[i] > dest.zPath[i]) {
8269
+ cAfter = true;
8270
+ break;
8271
+ }
8272
+ if (c.zPath[i] < dest.zPath[i]) {
8273
+ cAfter = false;
8274
+ break;
8275
+ }
8032
8276
  }
8277
+ if (cAfter) dest = c;
8033
8278
  }
8034
- if (cAfter) dest = c;
8035
8279
  }
8036
8280
  if (!dest) return;
8037
8281
  const layout = dest.layout;
@@ -8098,6 +8342,10 @@ function runLayoutPass(scratch, moveCtx) {
8098
8342
  }
8099
8343
  scratch.layoutPass = { layout, container, placements, sourceReflow, reflowIds };
8100
8344
  }
8345
+ function regionContains(region, bounds, point) {
8346
+ const local = { x: point.x - bounds.x, y: point.y - bounds.y };
8347
+ return typeof region === "function" ? region(local) : pointInPath(region, local.x, local.y);
8348
+ }
8101
8349
  function groupBySourceContainer(dragged, exclude) {
8102
8350
  const out = /* @__PURE__ */ new Map();
8103
8351
  for (const d of dragged) {
@@ -8113,17 +8361,18 @@ function groupBySourceContainer(dragged, exclude) {
8113
8361
  return out;
8114
8362
  }
8115
8363
  function translateCommitOps(scratch, ids, dx, dy) {
8116
- const m = [1, 0, 0, 1, dx, dy];
8117
8364
  const ops = [];
8118
8365
  for (const id of ids) {
8119
- if (!scratch.startPoses.has(id)) continue;
8366
+ if (!scratch.startPoses.has(id) || scratch.carried.has(id)) continue;
8120
8367
  const node = scratch.scene.get(id);
8121
8368
  if (!node) continue;
8369
+ const l = localDelta(scratch, id, dx, dy);
8370
+ const m = [1, 0, 0, 1, l.dx, l.dy];
8122
8371
  const authored = node.pose;
8123
8372
  ops.push(createTransformOp({
8124
8373
  id,
8125
8374
  from: authored,
8126
- to: translatePoseViaDescriptor(authored, dx, dy, scratch.projection),
8375
+ to: translatePoseViaDescriptor(authored, l.dx, l.dy, scratch.projection),
8127
8376
  label: "Move"
8128
8377
  }));
8129
8378
  if (scratch.geometryProjection) {
@@ -8138,6 +8387,16 @@ function translateCommitOps(scratch, ids, dx, dy) {
8138
8387
  }
8139
8388
  return ops;
8140
8389
  }
8390
+ function localDelta(scratch, id, dx, dy) {
8391
+ const f = scratch.frames.get(id);
8392
+ if (f === void 0) return { dx, dy };
8393
+ const { pc, projection: d } = scratch;
8394
+ const from = d.getBounds(pc.decompose(f.frameWorld, f.world));
8395
+ const to = d.getBounds(
8396
+ pc.decompose(f.frameWorld, translatePoseViaDescriptor(f.world, dx, dy, d))
8397
+ );
8398
+ return { dx: to.x - from.x, dy: to.y - from.y };
8399
+ }
8141
8400
  function unreleasedIds(scratch, released) {
8142
8401
  const selected = new Set(scratch.ids);
8143
8402
  const out = scratch.ids.filter((id) => !released.has(id));
@@ -8159,7 +8418,11 @@ function unreleasedIds(scratch, released) {
8159
8418
  function scenePoseAdapter(scene) {
8160
8419
  return {
8161
8420
  getPose: (id) => documentPose(scene, scene.get(id)),
8162
- getParent: (id) => scene.get(id)?.parent ?? null
8421
+ getParent: (id) => scene.get(id)?.parent ?? null,
8422
+ definesFrame: (id) => {
8423
+ const node = scene.get(id);
8424
+ return node === void 0 || definesFrame(node);
8425
+ }
8163
8426
  };
8164
8427
  }
8165
8428
  function resolveDropTarget(endCtx, scratch) {
@@ -8264,6 +8527,23 @@ var moveAction = {
8264
8527
  queue.push(childId);
8265
8528
  }
8266
8529
  }
8530
+ const carried = /* @__PURE__ */ new Set();
8531
+ const frames = /* @__PURE__ */ new Map();
8532
+ if (pc.closure !== "identity") {
8533
+ const poseAdapter = scenePoseAdapter(scene);
8534
+ for (const id of startPoses.keys()) {
8535
+ const frame = frameAtOrAbove(poseAdapter, scene.get(id)?.parent ?? null);
8536
+ if (frame === null) continue;
8537
+ if (startPoses.has(asNodeId(frame))) {
8538
+ carried.add(id);
8539
+ continue;
8540
+ }
8541
+ frames.set(id, {
8542
+ world: composeWorldPose(poseAdapter, id, pc.compose),
8543
+ frameWorld: composeWorldPose(poseAdapter, frame, pc.compose)
8544
+ });
8545
+ }
8546
+ }
8267
8547
  const behaviors = opts?.behaviors ?? [];
8268
8548
  const adapter = moveGestureAdapter(scene);
8269
8549
  const origin = /* @__PURE__ */ new Map();
@@ -8283,6 +8563,8 @@ var moveAction = {
8283
8563
  startPoses,
8284
8564
  ids,
8285
8565
  cascadeIds,
8566
+ carried,
8567
+ frames,
8286
8568
  scene,
8287
8569
  currentDelta: { dx: 0, dy: 0 },
8288
8570
  previews: /* @__PURE__ */ new Map(),
@@ -8335,7 +8617,12 @@ var moveAction = {
8335
8617
  scratch.currentDelta = { dx, dy };
8336
8618
  scratch.previews.clear();
8337
8619
  for (const [id, ori] of scratch.startPoses) {
8338
- scratch.previews.set(id, translatePoseViaDescriptor(ori, dx, dy, scratch.projection));
8620
+ if (scratch.carried.has(id)) {
8621
+ scratch.previews.set(id, ori);
8622
+ continue;
8623
+ }
8624
+ const l = localDelta(scratch, id, dx, dy);
8625
+ scratch.previews.set(id, translatePoseViaDescriptor(ori, l.dx, l.dy, scratch.projection));
8339
8626
  }
8340
8627
  scratch.layoutPass = null;
8341
8628
  runLayoutPass(scratch, moveCtx);
@@ -8534,15 +8821,18 @@ var moveAction = {
8534
8821
  };
8535
8822
 
8536
8823
  // src/interactions/actions/defaultCommitAdapter.ts
8537
- function defaultCommitAdapter(scene) {
8824
+ function defaultCommitAdapter(scene, selection) {
8538
8825
  return {
8539
8826
  getNode: (id) => scene.get(asNodeId(id)),
8540
8827
  getNodes: () => {
8541
8828
  return [...scene.renderOrderNodes()];
8542
8829
  },
8543
8830
  getPose: (id) => documentPose(scene, scene.get(asNodeId(id))),
8544
- getSelection: () => [...scene.getSelection()],
8545
- setSelection: (ids) => scene.setSelection(ids.map(asNodeId)),
8831
+ getSelection: () => [...selection ? selection.getSelection() : scene.getSelection()],
8832
+ setSelection: (ids) => {
8833
+ if (selection) selection.setSelection(ids.map(asNodeId));
8834
+ else scene.setSelection(ids.map(asNodeId));
8835
+ },
8546
8836
  getParent: (id) => scene.get(asNodeId(id))?.parent ?? null,
8547
8837
  getRemovalClosure: (ids) => [...scene.removalClosure(ids.map(asNodeId))],
8548
8838
  getChildren: (parentId) => parentId === null ? [...scene.roots] : [...scene.childrenOf(asNodeId(parentId))],
@@ -8586,8 +8876,8 @@ function poseFrame(adapter, pc) {
8586
8876
  pc,
8587
8877
  world: (id) => composeWorldPose(adapter, id, pc.compose),
8588
8878
  parentWorld: (id) => {
8589
- const parent = adapter.getParent(id);
8590
- return parent === null ? null : composeWorldPose(adapter, parent, pc.compose);
8879
+ const frame = frameAtOrAbove(adapter, adapter.getParent(id));
8880
+ return frame === null ? null : composeWorldPose(adapter, frame, pc.compose);
8591
8881
  },
8592
8882
  local: (id, worldPose) => rebaseLocalPose(adapter, worldPose, adapter.getParent(id), pc.compose, pc.decompose),
8593
8883
  localUnder: (parentId, worldPose) => rebaseLocalPose(adapter, worldPose, parentId, pc.compose, pc.decompose)
@@ -8601,7 +8891,11 @@ function scenePoseFrame(scene, poseComposition) {
8601
8891
  const node = scene.get(id);
8602
8892
  return node === void 0 ? { x: 0, y: 0, width: 0, height: 0 } : effectivePose(source, node);
8603
8893
  },
8604
- getParent: (id) => scene.get(id)?.parent ?? null
8894
+ getParent: (id) => scene.get(id)?.parent ?? null,
8895
+ definesFrame: (id) => {
8896
+ const node = scene.get(id);
8897
+ return node === void 0 || definesFrame(node);
8898
+ }
8605
8899
  };
8606
8900
  return poseFrame(adapter, pc);
8607
8901
  }
@@ -8958,7 +9252,7 @@ var resizeAction = {
8958
9252
  }
8959
9253
  if (ops.length > 0) {
8960
9254
  if (scratch.applyOps) scratch.applyOps(ops, "Resize");
8961
- else scratch.scene.applyBatch(ops, "Resize", defaultCommitAdapter(scratch.scene));
9255
+ else scratch.scene.applyBatch(ops, "Resize", defaultCommitAdapter(scratch.scene, selection.adapterMethods));
8962
9256
  }
8963
9257
  dropPreviewOverrides(scratch);
8964
9258
  scratch.previews.clear();
@@ -9114,7 +9408,7 @@ var rotateAction = {
9114
9408
  }
9115
9409
  if (ops.length > 0) {
9116
9410
  if (scratch.applyOps) scratch.applyOps(ops, "Rotate");
9117
- else scratch.scene.applyBatch(ops, "Rotate", defaultCommitAdapter(scratch.scene));
9411
+ else scratch.scene.applyBatch(ops, "Rotate", defaultCommitAdapter(scratch.scene, selection.adapterMethods));
9118
9412
  }
9119
9413
  dropPreviewOverrides(scratch);
9120
9414
  scratch.previews.clear();
@@ -9581,7 +9875,7 @@ var cloneAction = {
9581
9875
  }
9582
9876
  if (ops.length > 0) {
9583
9877
  if (scratch.applyOps) scratch.applyOps(ops, "Clone");
9584
- else scratch.scene.applyBatch(ops, "Clone", defaultCommitAdapter(scratch.scene));
9878
+ else scratch.scene.applyBatch(ops, "Clone", defaultCommitAdapter(scratch.scene, selection.adapterMethods));
9585
9879
  }
9586
9880
  scratch.previews.clear();
9587
9881
  },
@@ -9618,6 +9912,14 @@ function createVelocityTracker() {
9618
9912
  }
9619
9913
 
9620
9914
  // src/interactions/actions/defaults/viewportDragPan.ts
9915
+ function screenDeltaFromWorld(drag, scratch) {
9916
+ const v0 = scratch.startView;
9917
+ const now = scratch.view.get();
9918
+ return {
9919
+ x: (drag.current.x - now.x - (drag.start.x - v0.x)) * v0.scale.x,
9920
+ y: (drag.current.y - now.y - (drag.start.y - v0.y)) * v0.scale.y
9921
+ };
9922
+ }
9621
9923
  var viewportDragPanAction = {
9622
9924
  id: "viewport.dragPan",
9623
9925
  label: "Drag to pan viewport",
@@ -9655,7 +9957,7 @@ var viewportDragPanAction = {
9655
9957
  kind: "pan",
9656
9958
  onMove(moveCtx) {
9657
9959
  if (!moveCtx.drag) return;
9658
- const screen = moveCtx.drag.screenDelta ?? moveCtx.drag.delta;
9960
+ const screen = moveCtx.drag.screenDelta ?? screenDeltaFromWorld(moveCtx.drag, scratch);
9659
9961
  const screenDx = scratch.axis === "y" ? 0 : screen.x;
9660
9962
  const screenDy = scratch.axis === "x" ? 0 : screen.y;
9661
9963
  if (scratch.tracker) {
@@ -9753,6 +10055,11 @@ function makeViewportZoomAction(opts = {}) {
9753
10055
  spec: { kind: "wheel", mods: { ctrl: true } },
9754
10056
  opts: { params: { kind: "wheel" } }
9755
10057
  },
10058
+ // Safari's trackpad pinch, reported as a scale rather than a wheel.
10059
+ {
10060
+ spec: { kind: "pinch" },
10061
+ opts: { params: { kind: "pinch" } }
10062
+ },
9756
10063
  // Cmd+= → zoom in (also accepts Cmd+Shift+= which is Cmd++ on many keyboards)
9757
10064
  {
9758
10065
  spec: { kind: "key", key: "=", mods: { mod: true, shift: "optional" } },
@@ -9786,14 +10093,21 @@ function makeViewportZoomAction(opts = {}) {
9786
10093
  view.animate(target, { ...tweenOpts, ms: ms ?? tweenOpts.ms });
9787
10094
  };
9788
10095
  const stepFrom = () => canAnimate && view.animationTarget?.() || current;
10096
+ const focal = () => ({
10097
+ x: params?.clientX ?? 0,
10098
+ y: params?.clientY ?? 0
10099
+ });
10100
+ const scaleBy = (factor) => {
10101
+ if (!(factor > 0) || !Number.isFinite(factor)) return;
10102
+ view.set(zoomAt(current, focal(), factor, clamp));
10103
+ };
9789
10104
  switch (kind) {
9790
- case "wheel": {
9791
- const deltaY = params?.deltaY ?? 0;
9792
- const clientX = params?.clientX ?? 0;
9793
- const clientY = params?.clientY ?? 0;
9794
- view.set(wheelZoom(current, { x: clientX, y: clientY }, deltaY, clamp));
10105
+ case "wheel":
10106
+ scaleBy(wheelZoomFactor(params?.deltaY ?? 0));
10107
+ break;
10108
+ case "pinch":
10109
+ scaleBy(params?.scale ?? Number.NaN);
9795
10110
  break;
9796
- }
9797
10111
  case "in":
9798
10112
  stepTo(zoomAt(stepFrom(), keyAnchor(view), KEY_STEP, clamp));
9799
10113
  break;
@@ -10594,6 +10908,150 @@ function makePinchZoomAction(opts = {}) {
10594
10908
  };
10595
10909
  }
10596
10910
  var pinchZoomAction = makePinchZoomAction();
10911
+ var SVG_BASE = {
10912
+ viewBox: "0 0 20 20",
10913
+ width: 20,
10914
+ height: 20,
10915
+ fill: "none",
10916
+ stroke: "currentColor",
10917
+ strokeWidth: 1.5,
10918
+ "aria-hidden": true
10919
+ };
10920
+ function CutIcon() {
10921
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE, children: [
10922
+ /* @__PURE__ */ jsx("circle", { cx: "6", cy: "14", r: "2.5" }),
10923
+ /* @__PURE__ */ jsx("circle", { cx: "14", cy: "14", r: "2.5" }),
10924
+ /* @__PURE__ */ jsx("path", { d: "M8 12 L16 4", strokeLinecap: "round" }),
10925
+ /* @__PURE__ */ jsx("path", { d: "M12 12 L4 4", strokeLinecap: "round" })
10926
+ ] });
10927
+ }
10928
+ function CopyIcon() {
10929
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE, children: [
10930
+ /* @__PURE__ */ jsx("path", { d: "M6 14 H3 V3 H14 V6", strokeLinejoin: "round" }),
10931
+ /* @__PURE__ */ jsx("rect", { x: "6", y: "6", width: "11", height: "11" })
10932
+ ] });
10933
+ }
10934
+ function PasteIcon() {
10935
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE, children: [
10936
+ /* @__PURE__ */ jsx("rect", { x: "4", y: "5", width: "12", height: "13", rx: "1" }),
10937
+ /* @__PURE__ */ jsx("rect", { x: "7.5", y: "2.5", width: "5", height: "4", rx: "0.5", fill: "currentColor", stroke: "none" })
10938
+ ] });
10939
+ }
10940
+ function DuplicateIcon() {
10941
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE, children: [
10942
+ /* @__PURE__ */ jsx("rect", { x: "3", y: "3", width: "10", height: "10" }),
10943
+ /* @__PURE__ */ jsx("rect", { x: "7", y: "7", width: "10", height: "10", fill: "currentColor", stroke: "none" })
10944
+ ] });
10945
+ }
10946
+ function GroupIcon() {
10947
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE, children: [
10948
+ /* @__PURE__ */ jsx("path", { d: "M 2 5 L 2 2 L 5 2" }),
10949
+ /* @__PURE__ */ jsx("path", { d: "M 18 5 L 18 2 L 15 2" }),
10950
+ /* @__PURE__ */ jsx("path", { d: "M 2 15 L 2 18 L 5 18" }),
10951
+ /* @__PURE__ */ jsx("path", { d: "M 18 15 L 18 18 L 15 18" }),
10952
+ /* @__PURE__ */ jsx("rect", { x: "5", y: "6", width: "4.5", height: "8", fill: "currentColor", stroke: "none" }),
10953
+ /* @__PURE__ */ jsx("rect", { x: "11", y: "6", width: "4", height: "8", fill: "currentColor", stroke: "none" })
10954
+ ] });
10955
+ }
10956
+ function UngroupIcon() {
10957
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE, children: [
10958
+ /* @__PURE__ */ jsx("path", { d: "M 2 5 L 2 2 L 5 2", strokeDasharray: "1.5 1.5" }),
10959
+ /* @__PURE__ */ jsx("path", { d: "M 18 15 L 18 18 L 15 18", strokeDasharray: "1.5 1.5" }),
10960
+ /* @__PURE__ */ jsx("rect", { x: "3", y: "5", width: "4.5", height: "8", fill: "currentColor", stroke: "none" }),
10961
+ /* @__PURE__ */ jsx("rect", { x: "13", y: "7", width: "4", height: "8", fill: "currentColor", stroke: "none" })
10962
+ ] });
10963
+ }
10964
+ function BringForwardIcon() {
10965
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE, children: [
10966
+ /* @__PURE__ */ jsx("rect", { x: "3", y: "3", width: "11", height: "11" }),
10967
+ /* @__PURE__ */ jsx("rect", { x: "6", y: "6", width: "11", height: "11", fill: "currentColor", stroke: "none" })
10968
+ ] });
10969
+ }
10970
+ function BringToFrontIcon() {
10971
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE, children: [
10972
+ /* @__PURE__ */ jsx("rect", { x: "2", y: "2", width: "9", height: "9" }),
10973
+ /* @__PURE__ */ jsx("rect", { x: "6", y: "6", width: "9", height: "9" }),
10974
+ /* @__PURE__ */ jsx("rect", { x: "9", y: "9", width: "9", height: "9", fill: "currentColor", stroke: "none" })
10975
+ ] });
10976
+ }
10977
+ function SendBackwardIcon() {
10978
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE, children: [
10979
+ /* @__PURE__ */ jsx("rect", { x: "3", y: "3", width: "11", height: "11", fill: "currentColor", stroke: "none" }),
10980
+ /* @__PURE__ */ jsx("rect", { x: "6", y: "6", width: "11", height: "11" })
10981
+ ] });
10982
+ }
10983
+ function SendToBackIcon() {
10984
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE, children: [
10985
+ /* @__PURE__ */ jsx("rect", { x: "2", y: "2", width: "9", height: "9", fill: "currentColor", stroke: "none" }),
10986
+ /* @__PURE__ */ jsx("rect", { x: "6", y: "6", width: "9", height: "9" }),
10987
+ /* @__PURE__ */ jsx("rect", { x: "9", y: "9", width: "9", height: "9" })
10988
+ ] });
10989
+ }
10990
+ function FlipXIcon() {
10991
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE, children: [
10992
+ /* @__PURE__ */ jsx("line", { x1: "10", y1: "2", x2: "10", y2: "18", strokeDasharray: "2 1.5" }),
10993
+ /* @__PURE__ */ jsx("path", { d: "M 8 4 L 3 10 L 8 16 Z" }),
10994
+ /* @__PURE__ */ jsx("path", { d: "M 12 4 L 17 10 L 12 16 Z", fill: "currentColor", stroke: "none" })
10995
+ ] });
10996
+ }
10997
+ function FlipYIcon() {
10998
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE, children: [
10999
+ /* @__PURE__ */ jsx("line", { x1: "2", y1: "10", x2: "18", y2: "10", strokeDasharray: "2 1.5" }),
11000
+ /* @__PURE__ */ jsx("path", { d: "M 4 8 L 10 3 L 16 8 Z" }),
11001
+ /* @__PURE__ */ jsx("path", { d: "M 4 12 L 10 17 L 16 12 Z", fill: "currentColor", stroke: "none" })
11002
+ ] });
11003
+ }
11004
+
11005
+ // src/interactions/actions/defaults/icons/actionGlyphs.ts
11006
+ var ACTION_GLYPHS = {
11007
+ delete: `<path d="M3.6 5.8h12.8M8 5.8V4.5A1.1 1.1 0 0 1 9.1 3.4h1.8A1.1 1.1 0 0 1 12 4.5v1.3"/><path d="M5.2 5.8v9.6A1.6 1.6 0 0 0 6.8 17h6.4a1.6 1.6 0 0 0 1.6-1.6V5.8"/><path d="M8.4 8.8v5.2M11.6 8.8v5.2" stroke-width="1"/>`,
11008
+ undo: `<path d="M4.4 8.4h6.8a3.6 3.6 0 0 1 0 7.2H8.6"/><path d="M6.47 10.14 4.4 8.4 6.47 6.66"/>`,
11009
+ redo: `<path d="M15.6 8.4H8.8a3.6 3.6 0 0 0 0 7.2H11.4"/><path d="M13.53 6.66 15.6 8.4 13.53 10.14"/>`
11010
+ };
11011
+ var SVG_BASE2 = {
11012
+ viewBox: "0 0 20 20",
11013
+ fill: "none",
11014
+ stroke: "currentColor",
11015
+ strokeWidth: 1.5,
11016
+ strokeLinecap: "round",
11017
+ strokeLinejoin: "round"
11018
+ };
11019
+ var MARKUP = {
11020
+ delete: { __html: ACTION_GLYPHS.delete },
11021
+ undo: { __html: ACTION_GLYPHS.undo },
11022
+ redo: { __html: ACTION_GLYPHS.redo }
11023
+ };
11024
+ function renderGlyph(name, { className, size = 20, label }) {
11025
+ return /* @__PURE__ */ jsx(
11026
+ "svg",
11027
+ {
11028
+ ...SVG_BASE2,
11029
+ className,
11030
+ width: size,
11031
+ height: size,
11032
+ role: label ? "img" : void 0,
11033
+ "aria-label": label,
11034
+ "aria-hidden": label ? void 0 : true,
11035
+ dangerouslySetInnerHTML: MARKUP[name]
11036
+ }
11037
+ );
11038
+ }
11039
+ function UndoIcon(p) {
11040
+ return renderGlyph("undo", p);
11041
+ }
11042
+ function RedoIcon(p) {
11043
+ return renderGlyph("redo", p);
11044
+ }
11045
+ function DeleteIcon(p) {
11046
+ return renderGlyph("delete", p);
11047
+ }
11048
+ function requiresSelection(deps) {
11049
+ const selection = deps?.selection;
11050
+ if (!selection || selection.get().length === 0) {
11051
+ return ActionDisabledReason.SelectionRequired;
11052
+ }
11053
+ return true;
11054
+ }
10597
11055
 
10598
11056
  // src/interactions/actions/defaults/delete.ts
10599
11057
  function hostIndex(scene, id) {
@@ -10620,6 +11078,8 @@ function buildDeleteOps(scene, ids, label) {
10620
11078
  var deleteAction = {
10621
11079
  id: "delete",
10622
11080
  label: "Delete",
11081
+ icon: createElement(DeleteIcon),
11082
+ group: "edit",
10623
11083
  // Suppressed while any tool is mid-gesture — accidentally hitting
10624
11084
  // Delete during a drag shouldn't wipe the selection out from under
10625
11085
  // the in-flight handle.
@@ -10642,25 +11102,20 @@ var deleteAction = {
10642
11102
  const ops = buildDeleteOps(scene, ids, "Delete");
10643
11103
  if (ops.length > 0) {
10644
11104
  if (applyOps) applyOps(ops, "Delete");
10645
- else scene.applyBatch(ops, "Delete", defaultCommitAdapter(scene));
11105
+ else scene.applyBatch(ops, "Delete", defaultCommitAdapter(scene, selection.adapterMethods));
10646
11106
  }
10647
11107
  selection.set([]);
10648
11108
  }
10649
11109
  },
10650
- enabled: () => true
11110
+ enabled: requiresSelection
10651
11111
  };
10652
- function requiresSelection(deps) {
10653
- const selection = deps?.selection;
10654
- if (!selection || selection.get().length === 0) {
10655
- return ActionDisabledReason.SelectionRequired;
10656
- }
10657
- return true;
10658
- }
10659
11112
 
10660
11113
  // src/interactions/actions/defaults/clipboard.ts
10661
11114
  var clipboardCopyAction = {
10662
11115
  id: "clipboard.copy",
10663
11116
  label: "Copy",
11117
+ icon: createElement(CopyIcon),
11118
+ group: "clipboard",
10664
11119
  defaultBinding: { kind: "key", key: "c", mods: { mod: true } },
10665
11120
  // Same gate as `duplicate`: a mode that can neither edit the page nor own a
10666
11121
  // selection has nothing to copy. Notably excludes `text-edit`, where Cmd+C
@@ -10680,6 +11135,8 @@ var clipboardCopyAction = {
10680
11135
  var clipboardCutAction = {
10681
11136
  id: "clipboard.cut",
10682
11137
  label: "Cut",
11138
+ icon: createElement(CutIcon),
11139
+ group: "clipboard",
10683
11140
  defaultBinding: { kind: "key", key: "x", mods: { mod: true } },
10684
11141
  eligible: { capability: "edits-page" },
10685
11142
  requires: ["clipboard", "scene", "selection", "applyOps"],
@@ -10697,13 +11154,31 @@ var clipboardCutAction = {
10697
11154
  const ops = buildDeleteOps(scene, ids, "Cut");
10698
11155
  if (ops.length > 0) {
10699
11156
  if (applyOps) applyOps(ops, "Cut");
10700
- else scene.applyBatch(ops, "Cut", defaultCommitAdapter(scene));
11157
+ else scene.applyBatch(ops, "Cut", defaultCommitAdapter(scene, selection.adapterMethods));
10701
11158
  }
10702
11159
  selection.set([]);
10703
11160
  }
10704
11161
  },
10705
11162
  enabled: requiresSelection
10706
11163
  };
11164
+ var clipboardPasteAction = {
11165
+ id: "clipboard.paste",
11166
+ label: "Paste",
11167
+ icon: createElement(PasteIcon),
11168
+ group: "clipboard",
11169
+ eligible: { capability: "edits-page" },
11170
+ requires: ["clipboard"],
11171
+ invoker: {
11172
+ timing: "immediate",
11173
+ run: (deps) => {
11174
+ deps.clipboard?.paste();
11175
+ }
11176
+ },
11177
+ enabled: (deps) => {
11178
+ const clipboard = deps?.clipboard;
11179
+ return clipboard && !clipboard.isEmpty() ? true : ActionDisabledReason.NotApplicable;
11180
+ }
11181
+ };
10707
11182
 
10708
11183
  // src/interactions/actions/defaults/enterTextEdit.ts
10709
11184
  var enterTextEditAction = {
@@ -10798,8 +11273,6 @@ var selectAllAction = {
10798
11273
  }
10799
11274
  }
10800
11275
  };
10801
-
10802
- // src/interactions/actions/defaults/duplicate.ts
10803
11276
  var DUPLICATE_OFFSET = 12;
10804
11277
  function hasSelectedAncestor(scene, id, set) {
10805
11278
  let parent = scene.get(id)?.parent ?? null;
@@ -10829,6 +11302,8 @@ function copySubtree(scene, id, parent, offset, offsetDescendants, ops, d) {
10829
11302
  var duplicateAction = {
10830
11303
  id: "duplicate",
10831
11304
  label: "Duplicate",
11305
+ icon: createElement(DuplicateIcon),
11306
+ group: "edit",
10832
11307
  defaultBinding: { kind: "key", key: "d", mods: { mod: true } },
10833
11308
  eligible: { capability: ["edits-page", "creates-selection"] },
10834
11309
  // `selection` is read by the `enabled` gate as well as the invoker. An
@@ -10859,7 +11334,7 @@ var duplicateAction = {
10859
11334
  }
10860
11335
  if (ops.length === 0) return;
10861
11336
  if (applyOps) applyOps(ops, "Duplicate");
10862
- else scene.applyBatch(ops, "Duplicate", defaultCommitAdapter(scene));
11337
+ else scene.applyBatch(ops, "Duplicate", defaultCommitAdapter(scene, selection.adapterMethods));
10863
11338
  selection.set(newIds);
10864
11339
  }
10865
11340
  },
@@ -10895,8 +11370,6 @@ function withKitRegistry(registry2) {
10895
11370
  }
10896
11371
  };
10897
11372
  }
10898
-
10899
- // src/interactions/actions/defaults/group.ts
10900
11373
  var containerIdCounter = 0;
10901
11374
  function freshContainerId() {
10902
11375
  return asNodeId(`n${(containerIdCounter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`);
@@ -10904,6 +11377,8 @@ function freshContainerId() {
10904
11377
  var groupAction = {
10905
11378
  id: "group",
10906
11379
  label: "Group",
11380
+ icon: createElement(GroupIcon),
11381
+ group: "structure",
10907
11382
  defaultBinding: { kind: "key", key: "g", mods: { mod: true } },
10908
11383
  eligible: { capability: "edits-page" },
10909
11384
  requires: ["scene", "selection", "applyOps", "poseDescriptor", "poseComposition"],
@@ -10963,15 +11438,17 @@ var groupAction = {
10963
11438
  }));
10964
11439
  }
10965
11440
  if (applyOps) applyOps(ops, "Group");
10966
- else scene.applyBatch(ops, "Group", defaultCommitAdapter(scene));
11441
+ else scene.applyBatch(ops, "Group", defaultCommitAdapter(scene, selection.adapterMethods));
10967
11442
  selection.set([containerId]);
10968
11443
  }
10969
11444
  },
10970
- enabled: () => true
11445
+ enabled: requiresSelection
10971
11446
  };
10972
11447
  var ungroupAction = {
10973
11448
  id: "ungroup",
10974
11449
  label: "Ungroup",
11450
+ icon: createElement(UngroupIcon),
11451
+ group: "structure",
10975
11452
  defaultBinding: { kind: "key", key: "g", mods: { mod: true, shift: true } },
10976
11453
  eligible: { capability: "edits-page" },
10977
11454
  requires: ["scene", "selection", "poseComposition"],
@@ -11012,13 +11489,22 @@ var ungroupAction = {
11012
11489
  });
11013
11490
  }
11014
11491
  },
11015
- enabled: () => true
11492
+ enabled: (deps) => {
11493
+ const selection = deps?.selection;
11494
+ const scene = deps?.scene;
11495
+ const any = selection?.get().some((id) => scene?.get(id)?.kind === "container");
11496
+ return any ? true : ActionDisabledReason.NotApplicable;
11497
+ }
11016
11498
  };
11017
-
11018
- // src/interactions/actions/defaults/undoRedo.ts
11499
+ function historyCan(deps, which) {
11500
+ const history = deps?.history;
11501
+ return history?.[which]?.() ? true : ActionDisabledReason.NotApplicable;
11502
+ }
11019
11503
  var undoAction = {
11020
11504
  id: "undo",
11021
11505
  label: "Undo",
11506
+ icon: createElement(UndoIcon),
11507
+ group: "history",
11022
11508
  defaultBinding: { kind: "key", key: "z", mods: { mod: true } },
11023
11509
  requires: ["history"],
11024
11510
  invoker: {
@@ -11026,11 +11512,14 @@ var undoAction = {
11026
11512
  run: (deps) => {
11027
11513
  deps.history?.undo?.();
11028
11514
  }
11029
- }
11515
+ },
11516
+ enabled: (deps) => historyCan(deps, "canUndo")
11030
11517
  };
11031
11518
  var redoAction = {
11032
11519
  id: "redo",
11033
11520
  label: "Redo",
11521
+ icon: createElement(RedoIcon),
11522
+ group: "history",
11034
11523
  defaultBinding: { kind: "key", key: "z", mods: { mod: true, shift: true } },
11035
11524
  requires: ["history"],
11036
11525
  invoker: {
@@ -11038,7 +11527,8 @@ var redoAction = {
11038
11527
  run: (deps) => {
11039
11528
  deps.history?.redo?.();
11040
11529
  }
11041
- }
11530
+ },
11531
+ enabled: (deps) => historyCan(deps, "canRedo")
11042
11532
  };
11043
11533
 
11044
11534
  // src/interactions/actions/flip/helpers.ts
@@ -11142,8 +11632,8 @@ function localMirror(axis, frame, id, pivotWorld) {
11142
11632
  rotation: 0
11143
11633
  });
11144
11634
  const theta = at.rotation ?? 0;
11145
- const mirror2 = axis === "x" ? [-1, 0, 0, 1, 0, 0] : [1, 0, 0, -1, 0, 0];
11146
- const turned = multiply$1(multiply$1(rotate(theta), mirror2), rotate(-theta));
11635
+ const mirror = axis === "x" ? [-1, 0, 0, 1, 0, 0] : [1, 0, 0, -1, 0, 0];
11636
+ const turned = multiply$1(multiply$1(rotate(theta), mirror), rotate(-theta));
11147
11637
  return multiply$1(translate$1(at.x, at.y), multiply$1(turned, translate$1(-at.x, -at.y)));
11148
11638
  }
11149
11639
  function flipSelection(selection, scene, axis, pivot, applyOps, geometryProjection, geom, poseComposition) {
@@ -11178,11 +11668,16 @@ function flipSelection(selection, scene, axis, pivot, applyOps, geometryProjecti
11178
11668
  }
11179
11669
  if (ops.length === 0) return;
11180
11670
  if (applyOps) applyOps(ops, "Flip");
11181
- else scene.applyBatch(ops, "Flip", defaultCommitAdapter(scene));
11671
+ else scene.applyBatch(ops, "Flip", defaultCommitAdapter(scene, selection.adapterMethods));
11182
11672
  }
11183
11673
  var flipAction = {
11184
11674
  id: "flip",
11185
11675
  label: "Flip",
11676
+ group: "flip",
11677
+ variants: [
11678
+ { key: "x", label: "Flip Horizontal", icon: createElement(FlipXIcon), params: { axis: "x" } },
11679
+ { key: "y", label: "Flip Vertical", icon: createElement(FlipYIcon), params: { axis: "y" } }
11680
+ ],
11186
11681
  defaultBinding: [
11187
11682
  { spec: { kind: "key", key: ["h", "H"], mods: { shift: true } }, opts: { params: { axis: "x" } } },
11188
11683
  { spec: { kind: "key", key: ["v", "V"], mods: { shift: true } }, opts: { params: { axis: "y" } } }
@@ -11265,7 +11760,7 @@ function nudgeSelection(selection, scene, dx, dy, applyOps, geometryProjection,
11265
11760
  }
11266
11761
  if (ops.length === 0) return;
11267
11762
  if (applyOps) applyOps(ops, "Nudge");
11268
- else scene.applyBatch(ops, "Nudge", defaultCommitAdapter(scene));
11763
+ else scene.applyBatch(ops, "Nudge", defaultCommitAdapter(scene, selection.adapterMethods));
11269
11764
  }
11270
11765
  function makeNudgeAction(dir2) {
11271
11766
  return {
@@ -11495,20 +11990,22 @@ function createMoveToIndexOp(args) {
11495
11990
  };
11496
11991
  }
11497
11992
  registerOpFactory("moveToIndex", (args) => createMoveToIndexOp(args));
11498
-
11499
- // src/interactions/actions/defaults/reorder.ts
11500
11993
  function reorderSelection(selection, scene, direction, distance, applyOps) {
11501
11994
  const ids = selection.get();
11502
11995
  if (ids.length === 0) return;
11503
11996
  const reorderDir = direction === "forward" ? distance === "extreme" ? "front" : "forward" : distance === "extreme" ? "back" : "backward";
11504
11997
  const op = createReorderOp({ ids, direction: reorderDir, label: "Reorder" });
11505
11998
  if (applyOps) applyOps([op], "Reorder");
11506
- else scene.applyBatch([op], "Reorder", defaultCommitAdapter(scene));
11999
+ else scene.applyBatch([op], "Reorder", defaultCommitAdapter(scene, selection.adapterMethods));
11507
12000
  }
11508
12001
  var reorderForwardAction = {
11509
12002
  id: "reorder.forward",
11510
12003
  label: "Bring Forward",
11511
12004
  group: "reorder",
12005
+ variants: [
12006
+ { key: "adjacent", label: "Bring Forward", icon: createElement(BringForwardIcon), params: { distance: "adjacent" } },
12007
+ { key: "extreme", label: "Bring to Front", icon: createElement(BringToFrontIcon), params: { distance: "extreme" } }
12008
+ ],
11512
12009
  defaultBinding: [
11513
12010
  {
11514
12011
  // Modifier matching is strict, so Shift must be absent here — listing
@@ -11554,6 +12051,10 @@ var reorderBackwardAction = {
11554
12051
  id: "reorder.backward",
11555
12052
  label: "Send Backward",
11556
12053
  group: "reorder",
12054
+ variants: [
12055
+ { key: "adjacent", label: "Send Backward", icon: createElement(SendBackwardIcon), params: { distance: "adjacent" } },
12056
+ { key: "extreme", label: "Send to Back", icon: createElement(SendToBackIcon), params: { distance: "extreme" } }
12057
+ ],
11557
12058
  defaultBinding: [
11558
12059
  {
11559
12060
  spec: { kind: "key", key: ["["], mods: { mod: true } },
@@ -11586,7 +12087,7 @@ var reorderBackwardAction = {
11586
12087
  },
11587
12088
  enabled: requiresSelection
11588
12089
  };
11589
- var SVG_BASE = {
12090
+ var SVG_BASE3 = {
11590
12091
  viewBox: "0 0 16 16",
11591
12092
  width: 16,
11592
12093
  height: 16,
@@ -11599,42 +12100,42 @@ var AXIS = {
11599
12100
  fill: "none"
11600
12101
  };
11601
12102
  function AlignLeftIcon() {
11602
- return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE, children: [
12103
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE3, children: [
11603
12104
  /* @__PURE__ */ jsx("line", { x1: "2", y1: "2", x2: "2", y2: "14", ...AXIS }),
11604
12105
  /* @__PURE__ */ jsx("rect", { x: "2.5", y: "3", width: "9", height: "3" }),
11605
12106
  /* @__PURE__ */ jsx("rect", { x: "2.5", y: "9", width: "6", height: "3" })
11606
12107
  ] });
11607
12108
  }
11608
12109
  function AlignRightIcon() {
11609
- return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE, children: [
12110
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE3, children: [
11610
12111
  /* @__PURE__ */ jsx("line", { x1: "14", y1: "2", x2: "14", y2: "14", ...AXIS }),
11611
12112
  /* @__PURE__ */ jsx("rect", { x: "4.5", y: "3", width: "9", height: "3" }),
11612
12113
  /* @__PURE__ */ jsx("rect", { x: "7.5", y: "9", width: "6", height: "3" })
11613
12114
  ] });
11614
12115
  }
11615
12116
  function AlignTopIcon() {
11616
- return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE, children: [
12117
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE3, children: [
11617
12118
  /* @__PURE__ */ jsx("line", { x1: "2", y1: "2", x2: "14", y2: "2", ...AXIS }),
11618
12119
  /* @__PURE__ */ jsx("rect", { x: "3", y: "2.5", width: "3", height: "9" }),
11619
12120
  /* @__PURE__ */ jsx("rect", { x: "9", y: "2.5", width: "3", height: "6" })
11620
12121
  ] });
11621
12122
  }
11622
12123
  function AlignBottomIcon() {
11623
- return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE, children: [
12124
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE3, children: [
11624
12125
  /* @__PURE__ */ jsx("line", { x1: "2", y1: "14", x2: "14", y2: "14", ...AXIS }),
11625
12126
  /* @__PURE__ */ jsx("rect", { x: "3", y: "4.5", width: "3", height: "9" }),
11626
12127
  /* @__PURE__ */ jsx("rect", { x: "9", y: "7.5", width: "3", height: "6" })
11627
12128
  ] });
11628
12129
  }
11629
12130
  function AlignCenterXIcon() {
11630
- return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE, children: [
12131
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE3, children: [
11631
12132
  /* @__PURE__ */ jsx("line", { x1: "8", y1: "2", x2: "8", y2: "14", ...AXIS }),
11632
12133
  /* @__PURE__ */ jsx("rect", { x: "3.5", y: "3", width: "9", height: "3" }),
11633
12134
  /* @__PURE__ */ jsx("rect", { x: "5", y: "9", width: "6", height: "3" })
11634
12135
  ] });
11635
12136
  }
11636
12137
  function AlignCenterYIcon() {
11637
- return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE, children: [
12138
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE3, children: [
11638
12139
  /* @__PURE__ */ jsx("line", { x1: "2", y1: "8", x2: "14", y2: "8", ...AXIS }),
11639
12140
  /* @__PURE__ */ jsx("rect", { x: "3", y: "3.5", width: "3", height: "9" }),
11640
12141
  /* @__PURE__ */ jsx("rect", { x: "9", y: "5", width: "3", height: "6" })
@@ -11748,7 +12249,7 @@ function planDistribute(bounds, axis, mode) {
11748
12249
  }
11749
12250
  return targets;
11750
12251
  }
11751
- var SVG_BASE2 = {
12252
+ var SVG_BASE4 = {
11752
12253
  viewBox: "0 0 16 16",
11753
12254
  width: 16,
11754
12255
  height: 16,
@@ -11756,14 +12257,14 @@ var SVG_BASE2 = {
11756
12257
  "aria-hidden": true
11757
12258
  };
11758
12259
  function DistributeHorizontalIcon() {
11759
- return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE2, children: [
12260
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE4, children: [
11760
12261
  /* @__PURE__ */ jsx("rect", { x: "1", y: "4", width: "3", height: "8" }),
11761
12262
  /* @__PURE__ */ jsx("rect", { x: "6.5", y: "4", width: "3", height: "8" }),
11762
12263
  /* @__PURE__ */ jsx("rect", { x: "12", y: "4", width: "3", height: "8" })
11763
12264
  ] });
11764
12265
  }
11765
12266
  function DistributeVerticalIcon() {
11766
- return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE2, children: [
12267
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE4, children: [
11767
12268
  /* @__PURE__ */ jsx("rect", { x: "4", y: "1", width: "8", height: "3" }),
11768
12269
  /* @__PURE__ */ jsx("rect", { x: "4", y: "6.5", width: "8", height: "3" }),
11769
12270
  /* @__PURE__ */ jsx("rect", { x: "4", y: "12", width: "8", height: "3" })
@@ -11933,7 +12434,7 @@ function applyBooleanOp(adapter, op) {
11933
12434
  dispatchApplyBatch(adapter, ops, LABEL[op]);
11934
12435
  return { kind: "applied", resultIds: newNodes.map((n) => n.id) };
11935
12436
  }
11936
- var SVG_BASE3 = {
12437
+ var SVG_BASE5 = {
11937
12438
  viewBox: "0 0 20 20",
11938
12439
  width: 20,
11939
12440
  height: 20,
@@ -11943,7 +12444,7 @@ var SVG_BASE3 = {
11943
12444
  "aria-hidden": true
11944
12445
  };
11945
12446
  function UnionIcon() {
11946
- return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE3, children: [
12447
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE5, children: [
11947
12448
  /* @__PURE__ */ jsx("circle", { cx: "7", cy: "10", r: "5", fill: "currentColor", stroke: "none" }),
11948
12449
  /* @__PURE__ */ jsx("circle", { cx: "13", cy: "10", r: "5", fill: "currentColor", stroke: "none" }),
11949
12450
  /* @__PURE__ */ jsx("circle", { cx: "7", cy: "10", r: "5" }),
@@ -11951,7 +12452,7 @@ function UnionIcon() {
11951
12452
  ] });
11952
12453
  }
11953
12454
  function IntersectIcon() {
11954
- return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE3, children: [
12455
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE5, children: [
11955
12456
  /* @__PURE__ */ jsx("circle", { cx: "7", cy: "10", r: "5" }),
11956
12457
  /* @__PURE__ */ jsx("circle", { cx: "13", cy: "10", r: "5" }),
11957
12458
  /* @__PURE__ */ jsx(
@@ -11965,7 +12466,7 @@ function IntersectIcon() {
11965
12466
  ] });
11966
12467
  }
11967
12468
  function SubtractIcon() {
11968
- return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE3, children: [
12469
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE5, children: [
11969
12470
  /* @__PURE__ */ jsx("circle", { cx: "7", cy: "10", r: "5" }),
11970
12471
  /* @__PURE__ */ jsx("circle", { cx: "13", cy: "10", r: "5" }),
11971
12472
  /* @__PURE__ */ jsx(
@@ -11979,7 +12480,7 @@ function SubtractIcon() {
11979
12480
  ] });
11980
12481
  }
11981
12482
  function ExcludeIcon() {
11982
- return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE3, children: [
12483
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE5, children: [
11983
12484
  /* @__PURE__ */ jsx("circle", { cx: "7", cy: "10", r: "5" }),
11984
12485
  /* @__PURE__ */ jsx("circle", { cx: "13", cy: "10", r: "5" }),
11985
12486
  /* @__PURE__ */ jsx(
@@ -11994,14 +12495,14 @@ function ExcludeIcon() {
11994
12495
  ] });
11995
12496
  }
11996
12497
  function DivideIcon() {
11997
- return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE3, children: [
12498
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE5, children: [
11998
12499
  /* @__PURE__ */ jsx("circle", { cx: "7", cy: "10", r: "5" }),
11999
12500
  /* @__PURE__ */ jsx("circle", { cx: "13", cy: "10", r: "5" }),
12000
12501
  /* @__PURE__ */ jsx("line", { x1: "10", y1: "6", x2: "10", y2: "14" })
12001
12502
  ] });
12002
12503
  }
12003
12504
  function CropIcon() {
12004
- return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE3, children: [
12505
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE5, children: [
12005
12506
  /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx("clipPath", { id: "weasel-default-crop-mask", children: /* @__PURE__ */ jsx("rect", { x: "9", y: "5", width: "8", height: "10" }) }) }),
12006
12507
  /* @__PURE__ */ jsx("circle", { cx: "9", cy: "10", r: "5" }),
12007
12508
  /* @__PURE__ */ jsx(
@@ -12457,7 +12958,7 @@ function createPaintAction(spec) {
12457
12958
  }
12458
12959
  if (ops.length > 0) {
12459
12960
  if (scratch.applyOps) scratch.applyOps(ops, spec.label);
12460
- else scratch.scene.applyBatch(ops, spec.label, defaultCommitAdapter(scratch.scene));
12961
+ else scratch.scene.applyBatch(ops, spec.label, defaultCommitAdapter(scratch.scene, selection.adapterMethods));
12461
12962
  }
12462
12963
  scratch.previews.clear();
12463
12964
  },
@@ -12508,14 +13009,23 @@ function mergeAlphaFromPrev(picked, prev) {
12508
13009
  // src/util/paint.ts
12509
13010
  var DEFAULT_FILL_COLOR = "#ffffffff";
12510
13011
  var DEFAULT_STROKE_COLOR = "#000000ff";
12511
- var DEFAULT_PALETTE = [
12512
- "#7fb069",
12513
- "#d4a574",
12514
- "#a48bd4",
12515
- "#7ab8d4",
12516
- "#d47a7a"
12517
- ];
12518
- var GHOST_STROKE = DEFAULT_PALETTE[0];
13012
+ var srgb = (hex) => hexToColorLiteral(hex);
13013
+ var DEFAULT_PALETTE = {
13014
+ entries: [
13015
+ { name: "sage", color: srgb("#7fb069") },
13016
+ { name: "tan", color: srgb("#d4a574") },
13017
+ { name: "lavender", color: srgb("#a48bd4") },
13018
+ { name: "sky", color: srgb("#7ab8d4") },
13019
+ { name: "rose", color: srgb("#d47a7a") }
13020
+ ]
13021
+ };
13022
+ function cycleFill(seq, palette = DEFAULT_PALETTE, fallback = DEFAULT_FILL_COLOR) {
13023
+ const { entries } = palette;
13024
+ const entry = entries[(seq % entries.length + entries.length) % entries.length];
13025
+ const color = entry && resolvePaletteColor(palette, { ref: entry.name });
13026
+ return (color && colorLiteralToHex(color)) ?? fallback;
13027
+ }
13028
+ var GHOST_STROKE = cycleFill(0);
12519
13029
  function solid(color) {
12520
13030
  const a = getAlpha01(color);
12521
13031
  const rgb = color.length === 9 && color.startsWith("#") ? color.slice(0, 7) : color;
@@ -13018,7 +13528,7 @@ var ingestAction = {
13018
13528
  insert,
13019
13529
  applyOps: (ops, label) => {
13020
13530
  if (applyOps) applyOps(ops, label ?? "Ingest");
13021
- else scene.applyBatch(ops, label ?? "Ingest", { ...defaultCommitAdapter(scene), ...selection.adapterMethods });
13531
+ else scene.applyBatch(ops, label ?? "Ingest", defaultCommitAdapter(scene, selection.adapterMethods));
13022
13532
  },
13023
13533
  scene,
13024
13534
  selection,
@@ -13045,8 +13555,8 @@ var KIT_STANDARD_DESCRIPTORS = [
13045
13555
  escapeAction,
13046
13556
  cancelGestureAction,
13047
13557
  selectAllAction,
13048
- deleteAction,
13049
13558
  duplicateAction,
13559
+ deleteAction,
13050
13560
  groupAction,
13051
13561
  ungroupAction,
13052
13562
  undoAction,
@@ -13059,11 +13569,11 @@ var KIT_STANDARD_DESCRIPTORS = [
13059
13569
  reorderForwardAction,
13060
13570
  reorderBackwardAction,
13061
13571
  alignLeftAction,
13572
+ alignCenterXAction,
13062
13573
  alignRightAction,
13063
13574
  alignTopAction,
13064
- alignBottomAction,
13065
- alignCenterXAction,
13066
13575
  alignCenterYAction,
13576
+ alignBottomAction,
13067
13577
  distributeHorizontalAction,
13068
13578
  distributeVerticalAction,
13069
13579
  pathfinderUnionAction,
@@ -13101,8 +13611,9 @@ var KIT_STANDARD_DESCRIPTORS = [
13101
13611
  setFillOpacityAction,
13102
13612
  setStrokeOpacityAction,
13103
13613
  ingestAction,
13614
+ clipboardCutAction,
13104
13615
  clipboardCopyAction,
13105
- clipboardCutAction
13616
+ clipboardPasteAction
13106
13617
  ];
13107
13618
  var KIT_STANDARD_ACTION_IDS = KIT_STANDARD_DESCRIPTORS.map((a) => a.id);
13108
13619
  function useStandardActions(opts) {
@@ -14075,7 +14586,7 @@ function createTimeline(register, id, opts) {
14075
14586
  let playhead = 0;
14076
14587
  let prevPlayhead = -Infinity;
14077
14588
  let done = false;
14078
- let live = true;
14589
+ let live2 = true;
14079
14590
  let cancelled = false;
14080
14591
  let wantPaused = opts.autoplay === false;
14081
14592
  let wantScale = 1;
@@ -14163,12 +14674,12 @@ function createTimeline(register, id, opts) {
14163
14674
  opts.onDone?.();
14164
14675
  }
14165
14676
  if (playhead < duration) return false;
14166
- live = false;
14677
+ live2 = false;
14167
14678
  return true;
14168
14679
  };
14169
14680
  const onCancel = () => {
14170
14681
  cancelled = true;
14171
- live = false;
14682
+ live2 = false;
14172
14683
  booker?.retract();
14173
14684
  };
14174
14685
  const base = register({ id, cancelKey: opts.cancelKey, tick, onCancel });
@@ -14176,11 +14687,11 @@ function createTimeline(register, id, opts) {
14176
14687
  const rearm = () => {
14177
14688
  if (cancelled || playhead >= duration) return;
14178
14689
  done = false;
14179
- if (live) return;
14690
+ if (live2) return;
14180
14691
  booker?.rearm();
14181
14692
  lastVirtual = 0;
14182
14693
  offset = playhead;
14183
- live = true;
14694
+ live2 = true;
14184
14695
  register({ id, cancelKey: opts.cancelKey, keepExisting: true, tick, onCancel });
14185
14696
  if (wantPaused) base.pause();
14186
14697
  base.setTimeScale(wantScale);
@@ -14208,8 +14719,8 @@ function createTimeline(register, id, opts) {
14208
14719
  wantScale = s;
14209
14720
  base.setTimeScale(s);
14210
14721
  },
14211
- timeScale: () => live ? base.timeScale() : wantScale,
14212
- isPaused: () => live ? base.isPaused() : wantPaused,
14722
+ timeScale: () => live2 ? base.timeScale() : wantScale,
14723
+ isPaused: () => live2 ? base.isPaused() : wantPaused,
14213
14724
  seek(t) {
14214
14725
  booker?.retract();
14215
14726
  offset = t - lastVirtual;
@@ -14890,22 +15401,34 @@ function useKeybindings(tools, options = {}) {
14890
15401
 
14891
15402
  // src/tools/prefs.ts
14892
15403
  function prefUnit(system, display, opts) {
14893
- const factor = system.units[display];
14894
- if (factor === void 0) {
14895
- const known = Object.keys(system.units).join(", ") || "(none)";
14896
- throw new Error(
14897
- `prefUnit: unknown unit '${display}' (system base: '${system.base}', known units: ${known})`
14898
- );
15404
+ let self;
15405
+ try {
15406
+ self = unitScale(system, display);
15407
+ } catch (e) {
15408
+ throw new Error(`prefUnit: ${e instanceof Error ? e.message : String(e)}`);
14899
15409
  }
14900
15410
  const scale = opts?.precision === void 0 ? void 0 : 10 ** opts.precision;
14901
15411
  const suffix = opts?.suffix ?? display;
14902
15412
  const accepts = { [suffix]: 1 };
14903
- for (const [name, f] of Object.entries(system.units)) accepts[name] = f / factor;
15413
+ for (const name of Object.keys(system.units)) {
15414
+ const other = unitScale(system, name);
15415
+ const factor = other.factor / self.factor;
15416
+ const offset = (other.offset - self.offset) / self.factor;
15417
+ accepts[name] = offset === 0 ? factor : { factor, offset };
15418
+ }
15419
+ const toDisplay = (stored) => {
15420
+ const shown = (stored - self.offset) / self.factor;
15421
+ return scale === void 0 ? shown : Math.round(shown * scale) / scale;
15422
+ };
14904
15423
  return {
14905
- toDisplay: (stored) => scale === void 0 ? stored / factor : Math.round(stored / factor * scale) / scale,
14906
- fromDisplay: (shown) => shown * factor,
15424
+ toDisplay,
15425
+ fromDisplay: (shown) => shown * self.factor + self.offset,
14907
15426
  suffix,
14908
- accepts
15427
+ accepts,
15428
+ format: (stored) => `${formatUnit(stored, display, system, {
15429
+ precision: opts?.precision ?? 2,
15430
+ suffix: false
15431
+ })}${suffix}`
14909
15432
  };
14910
15433
  }
14911
15434
  var TOOL_PREF_KINDS = {
@@ -15609,17 +16132,53 @@ function resolveFillPattern(fill) {
15609
16132
  }
15610
16133
 
15611
16134
  // src/features/images/imageCache.ts
16135
+ var MAX_VECTOR_RASTER_PIXELS = 4096 * 4096;
16136
+ var SHRINK_SLACK = 4;
16137
+ var FALLBACK_WIDTH = 300;
16138
+ var FALLBACK_HEIGHT = 150;
15612
16139
  var cache6 = /* @__PURE__ */ new Map();
15613
16140
  var subscribers = /* @__PURE__ */ new Set();
15614
- var defaultLoader = async (src) => {
16141
+ var decodedVectors = /* @__PURE__ */ new Map();
16142
+ function isVectorImageSrc(src) {
16143
+ if (/^data:image\/svg\+xml[;,]/i.test(src)) return true;
16144
+ if (/^(data|blob):/i.test(src)) return false;
16145
+ return /\.svgz?$/i.test(src.replace(/[?#].*$/, ""));
16146
+ }
16147
+ function requireDom() {
15615
16148
  if (typeof Image === "undefined" || typeof createImageBitmap === "undefined") {
15616
16149
  throw new Error("weasel imageCache: no DOM image loader in this environment");
15617
16150
  }
16151
+ }
16152
+ async function decodeImage(src) {
15618
16153
  const img = new Image();
15619
16154
  img.crossOrigin = "anonymous";
15620
16155
  img.src = src;
15621
16156
  await img.decode();
15622
- return createImageBitmap(img);
16157
+ return img;
16158
+ }
16159
+ function decodeVector(src) {
16160
+ let decoded = decodedVectors.get(src);
16161
+ if (!decoded) {
16162
+ decoded = decodeImage(src);
16163
+ decodedVectors.set(src, decoded);
16164
+ decoded.catch(() => decodedVectors.delete(src));
16165
+ }
16166
+ return decoded;
16167
+ }
16168
+ async function rasterizeVector(src, size) {
16169
+ const img = await decodeVector(src);
16170
+ const width = size?.width ?? (img.naturalWidth || FALLBACK_WIDTH);
16171
+ const height = size?.height ?? (img.naturalHeight || FALLBACK_HEIGHT);
16172
+ const canvas = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(width, height) : Object.assign(document.createElement("canvas"), { width, height });
16173
+ const c2d = canvas.getContext("2d");
16174
+ if (!c2d) throw new Error("weasel imageCache: no 2D context to rasterize an SVG");
16175
+ c2d.drawImage(img, 0, 0, width, height);
16176
+ return createImageBitmap(canvas);
16177
+ }
16178
+ var defaultLoader = async (src, size) => {
16179
+ requireDom();
16180
+ if (isVectorImageSrc(src)) return rasterizeVector(src, size);
16181
+ return createImageBitmap(await decodeImage(src));
15623
16182
  };
15624
16183
  var loader = defaultLoader;
15625
16184
  function notify() {
@@ -15631,7 +16190,11 @@ function startLoad(src) {
15631
16190
  loader(src).then(
15632
16191
  (bitmap) => {
15633
16192
  if (cache6.get(src) !== entry) return;
15634
- cache6.set(src, { status: "ready", bitmap });
16193
+ cache6.set(src, {
16194
+ status: "ready",
16195
+ bitmap,
16196
+ ...isVectorImageSrc(src) ? { vector: { natural: { width: bitmap.width, height: bitmap.height }, scale: 1, failed: /* @__PURE__ */ new Set() } } : {}
16197
+ });
15635
16198
  notify();
15636
16199
  },
15637
16200
  () => {
@@ -15641,11 +16204,54 @@ function startLoad(src) {
15641
16204
  }
15642
16205
  );
15643
16206
  }
15644
- function getImageBitmap(src) {
16207
+ function rasterScaleFor(natural, drawnAt) {
16208
+ const need = Math.max(drawnAt.width / natural.width, drawnAt.height / natural.height);
16209
+ if (!(need > 0) || !Number.isFinite(need)) return void 0;
16210
+ const side = maxImageTextureSide();
16211
+ const cap = Math.min(
16212
+ side / natural.width,
16213
+ side / natural.height,
16214
+ Math.sqrt(MAX_VECTOR_RASTER_PIXELS / (natural.width * natural.height))
16215
+ );
16216
+ return Math.min(2 ** Math.ceil(Math.log2(need)), cap);
16217
+ }
16218
+ function maybeReraster(src, entry, v, drawnAt) {
16219
+ if (v.pending !== void 0) return;
16220
+ const target = rasterScaleFor(v.natural, drawnAt);
16221
+ if (target === void 0) return;
16222
+ const grow = target > v.scale;
16223
+ if (!grow && target * SHRINK_SLACK > v.scale) return;
16224
+ for (const f of v.failed) if (target === f || grow && f > v.scale && target >= f) return;
16225
+ v.pending = target;
16226
+ const size = {
16227
+ width: Math.max(1, Math.round(v.natural.width * target)),
16228
+ height: Math.max(1, Math.round(v.natural.height * target))
16229
+ };
16230
+ loader(src, size).then(
16231
+ (bitmap) => {
16232
+ if (cache6.get(src) !== entry) return;
16233
+ v.pending = void 0;
16234
+ const old = entry.bitmap;
16235
+ entry.bitmap = bitmap;
16236
+ v.scale = target;
16237
+ if (old && old !== bitmap) releaseImageSource(old);
16238
+ notify();
16239
+ },
16240
+ () => {
16241
+ if (cache6.get(src) !== entry) return;
16242
+ v.pending = void 0;
16243
+ v.failed.add(target);
16244
+ }
16245
+ );
16246
+ }
16247
+ function getImageBitmap(src, drawnAt) {
15645
16248
  const entry = cache6.get(src);
15646
- if (entry) return entry.bitmap;
15647
- startLoad(src);
15648
- return void 0;
16249
+ if (!entry) {
16250
+ startLoad(src);
16251
+ return void 0;
16252
+ }
16253
+ if (drawnAt && entry.vector) maybeReraster(src, entry, entry.vector, drawnAt);
16254
+ return entry.bitmap;
15649
16255
  }
15650
16256
  function imageStatus(src) {
15651
16257
  return cache6.get(src)?.status ?? "idle";
@@ -15967,6 +16573,21 @@ function pathForShape(d, p) {
15967
16573
  return { kind: "rect", x: p.x, y: p.y, width: p.width, height: p.height };
15968
16574
  }
15969
16575
  }
16576
+ function sourceInPixels(s, bmp) {
16577
+ return {
16578
+ x: s.x * bmp.width,
16579
+ y: s.y * bmp.height,
16580
+ w: s.width * bmp.width,
16581
+ h: s.height * bmp.height
16582
+ };
16583
+ }
16584
+ function drawnSize(pose, source, pixelScale) {
16585
+ if (!(pixelScale && pixelScale > 0)) return void 0;
16586
+ return {
16587
+ width: Math.abs(pose.width) * pixelScale / (source?.width || 1),
16588
+ height: Math.abs(pose.height) * pixelScale / (source?.height || 1)
16589
+ };
16590
+ }
15970
16591
  var IMAGE_PAINTER = {
15971
16592
  id: "kit:image",
15972
16593
  matches: (node) => {
@@ -15977,7 +16598,7 @@ var IMAGE_PAINTER = {
15977
16598
  paint: (node, pose, ctx) => {
15978
16599
  const d = node.data;
15979
16600
  const p = pose;
15980
- const bmp = ctx?.resolveImage ? ctx.resolveImage(node) : getImageBitmap(d.image.src);
16601
+ const bmp = ctx?.resolveImage ? ctx.resolveImage(node) : getImageBitmap(d.image.src, drawnSize(p, d.image.source, ctx?.pixelScale));
15981
16602
  if (bmp) {
15982
16603
  return [{
15983
16604
  kind: "image",
@@ -15986,7 +16607,10 @@ var IMAGE_PAINTER = {
15986
16607
  y: p.y,
15987
16608
  w: p.width,
15988
16609
  h: p.height,
15989
- ...d.image.opacity !== void 0 ? { opacity: d.image.opacity } : {}
16610
+ ...d.image.opacity !== void 0 ? { opacity: d.image.opacity } : {},
16611
+ ...d.image.source ? { source: sourceInPixels(d.image.source, bmp) } : {},
16612
+ ...d.image.flipX ? { flipX: true } : {},
16613
+ ...d.image.flipY ? { flipY: true } : {}
15990
16614
  }];
15991
16615
  }
15992
16616
  const error = ctx?.resolveImage ? false : imageStatus(d.image.src) === "error";
@@ -16232,7 +16856,11 @@ function scenePickSource(scene, opts = {}) {
16232
16856
  const n = scene.get(asNodeId(id));
16233
16857
  return n ? effectivePose(scene, n) : void 0;
16234
16858
  },
16235
- getParent: (id) => scene.get(asNodeId(id))?.parent ?? null
16859
+ getParent: (id) => scene.get(asNodeId(id))?.parent ?? null,
16860
+ definesFrame: (id) => {
16861
+ const n = scene.get(asNodeId(id));
16862
+ return n === void 0 || definesFrame(n);
16863
+ }
16236
16864
  } : null;
16237
16865
  const poseOf = getPose ? (node) => getPose(node.id) : chain ? (node) => composeWorldPose(chain, node.id, composition.compose) : (node) => effectivePose(scene, node);
16238
16866
  const childrenOf = (id) => scene.childrenOf(id);
@@ -16683,6 +17311,92 @@ function useTextTool() {
16683
17311
  []
16684
17312
  );
16685
17313
  }
17314
+ var PERCENT = {
17315
+ toDisplay: (v) => Math.round(v * 1e3) / 10,
17316
+ // Rounded rather than divided: 33.3 / 100 is 0.33299999999999996, and that
17317
+ // dust would be written into the document and shown back on the next read.
17318
+ fromDisplay: (v) => Math.round(v * 1e3) / 1e5,
17319
+ suffix: "%"
17320
+ };
17321
+ var flag = (name, icon) => ({
17322
+ kind: "boolean",
17323
+ name,
17324
+ description: `${name} for the characters in the range.`,
17325
+ icon,
17326
+ control: "toggle",
17327
+ pair: "Character style",
17328
+ default: false
17329
+ });
17330
+ useTextTool.options = {
17331
+ name: "Text",
17332
+ description: "Character styling for the selected range.",
17333
+ children: {
17334
+ bold: flag("Bold", "bold"),
17335
+ italic: flag("Italic", "italic"),
17336
+ underline: flag("Underline", "underline"),
17337
+ strikethrough: flag("Strikethrough", "strikethrough"),
17338
+ overline: flag("Overline", "overline"),
17339
+ // Its own kind rather than an enum: the two values are exclusive *and*
17340
+ // absent is a third state an enum cannot name, so clicking the lit
17341
+ // segment has to clear it. The host draws it and owns that rule.
17342
+ script: {
17343
+ kind: "script",
17344
+ name: "Script",
17345
+ description: "Raise or lower the range off the baseline, at a smaller size.",
17346
+ default: void 0
17347
+ },
17348
+ textTransform: {
17349
+ kind: "enum",
17350
+ name: "Case",
17351
+ description: "Draw the range in capitals, lowercase or title case. The text itself is unchanged.",
17352
+ control: "toggle",
17353
+ default: "none",
17354
+ options: [{ value: "none", label: "None", short: "\u2013" }, { value: "uppercase", label: "Uppercase", short: "AA" }, { value: "lowercase", label: "Lowercase", short: "aa" }, { value: "capitalize", label: "Capitalize", short: "Aa" }]
17355
+ },
17356
+ fontFamily: {
17357
+ kind: "font-family",
17358
+ name: "Font",
17359
+ description: "Typeface for the range.",
17360
+ default: void 0
17361
+ },
17362
+ fontSize: { kind: "number", name: "Size", description: "Type size.", min: 1, step: 1, default: 16 },
17363
+ letterSpacing: {
17364
+ kind: "number",
17365
+ name: "Tracking",
17366
+ // The typographic term, short enough for a strip; `name` stays the
17367
+ // accessible one.
17368
+ short: "VA",
17369
+ description: "Space added between characters.",
17370
+ step: 0.1,
17371
+ default: 0
17372
+ },
17373
+ baselineShift: {
17374
+ kind: "number",
17375
+ name: "Baseline shift",
17376
+ short: "Shift",
17377
+ description: "Raise or lower the range, in ems of the inherited size.",
17378
+ step: 1e-3,
17379
+ unit: PERCENT,
17380
+ default: 0
17381
+ },
17382
+ fontScale: {
17383
+ kind: "number",
17384
+ name: "Scale",
17385
+ description: "Size of the range against the inherited size.",
17386
+ min: 0.01,
17387
+ step: 1e-3,
17388
+ unit: PERCENT,
17389
+ default: 1
17390
+ },
17391
+ fill: {
17392
+ kind: "color",
17393
+ name: "Color",
17394
+ description: "Ink for the range.",
17395
+ alpha: true,
17396
+ default: "#000000"
17397
+ }
17398
+ }
17399
+ };
16686
17400
 
16687
17401
  // src/features/paths/nearestAnchor.ts
16688
17402
  function nearestPathAnchor(paths, point, px, scale, accept) {
@@ -16748,44 +17462,9 @@ function anchorCount(s) {
16748
17462
  return (s.current ? s.current.anchors.length : 0) + s.finishedSubpaths.reduce((n, sp) => n + sp.anchors.length, 0);
16749
17463
  }
16750
17464
  function buildPath(subpaths, trailing) {
16751
- const b = new PathBuilder();
16752
17465
  const all = [...subpaths];
16753
17466
  if (trailing && trailing.anchors.length > 0) all.push(trailing);
16754
- for (const sp of all) {
16755
- if (sp.anchors.length === 0) continue;
16756
- const first = sp.anchors[0];
16757
- b.moveTo(first.x, first.y);
16758
- for (let i = 1; i < sp.anchors.length; i++) {
16759
- const prev = sp.anchors[i - 1];
16760
- const curr = sp.anchors[i];
16761
- const out = prev.outHandle;
16762
- const inH = curr.inHandle ?? mirrorHandle(prev, out);
16763
- if (out || curr.inHandle) {
16764
- const c1 = out ?? { x: prev.x, y: prev.y };
16765
- const c2 = inH ?? { x: curr.x, y: curr.y };
16766
- b.curveTo(c1.x, c1.y, c2.x, c2.y, curr.x, curr.y);
16767
- } else {
16768
- b.lineTo(curr.x, curr.y);
16769
- }
16770
- }
16771
- if (sp.closed) {
16772
- const last = sp.anchors[sp.anchors.length - 1];
16773
- const first0 = sp.anchors[0];
16774
- const out = last.outHandle;
16775
- const inH = first0.inHandle ?? mirrorHandle(last, out);
16776
- if (out || first0.inHandle) {
16777
- const c1 = out ?? { x: last.x, y: last.y };
16778
- const c2 = inH ?? { x: first0.x, y: first0.y };
16779
- b.curveTo(c1.x, c1.y, c2.x, c2.y, first0.x, first0.y);
16780
- }
16781
- b.close();
16782
- }
16783
- }
16784
- return b.build();
16785
- }
16786
- function mirrorHandle(anchor, out) {
16787
- if (!out || anchor.altBroken) return void 0;
16788
- return { x: 2 * anchor.x - out.x, y: 2 * anchor.y - out.y };
17467
+ return anchorsToPath(all.map((sp) => sp.anchors), all.map((sp) => sp.closed));
16789
17468
  }
16790
17469
  function usePenTool(options) {
16791
17470
  const { wrapPath, adapter, autoSelect = true, autoCommitOnClose = true, closeHitRadius = 8, anchorSnapRadius = 8, snapPoint: snapPoint2 } = options;
@@ -16830,6 +17509,64 @@ function usePenTool(options) {
16830
17509
  if (optsRef.current.autoSelect) optsRef.current.adapter.setSelection([id]);
16831
17510
  resetScratch(s);
16832
17511
  }, [commitContinuation]);
17512
+ const joinable = useCallback((s, deps, hit) => {
17513
+ if (!hit || hit.end === null || !s.current || s.current.anchors.length === 0) return false;
17514
+ const cont = s.continuing;
17515
+ if (cont && hit.id === cont.id && hit.sub === cont.sub) return false;
17516
+ const scene = deps.scene;
17517
+ if (!scene && !deps.applyOps) return false;
17518
+ if (!(cont ? deps.editAnchors.editOps : optsRef.current.adapter.makeNode)) return false;
17519
+ return cont?.id === hit.id || scene?.get(hit.id) !== void 0;
17520
+ }, []);
17521
+ const commitJoin = useCallback((s, deps, hit) => {
17522
+ const edit = deps.editAnchors;
17523
+ const scene = deps.scene;
17524
+ const current = s.current;
17525
+ const { anchors: added, closed: addedClosed } = pathToAnchors(buildPath([...s.finishedSubpaths, current], null));
17526
+ const other = edit.getEditablePath(hit.id);
17527
+ const otherSet = pathToAnchors(other);
17528
+ const tail = hit.end === "last" ? reverseAnchors(otherSet.anchors[hit.sub]) : otherSet.anchors[hit.sub];
17529
+ const joined = added[added.length - 1];
17530
+ const joint = joined[joined.length - 1];
17531
+ const out = current.anchors[current.anchors.length - 1].outHandle ?? tail[0].outHandle;
17532
+ if (out) joint.outHandle = { ...out };
17533
+ joined.push(...tail.slice(1));
17534
+ const cont = s.continuing;
17535
+ const sameNode = cont?.id === hit.id;
17536
+ const rest = sameNode ? { anchors: [], closed: [] } : {
17537
+ anchors: otherSet.anchors.filter((_, i) => i !== hit.sub),
17538
+ closed: otherSet.closed.filter((_, i) => i !== hit.sub)
17539
+ };
17540
+ const label = "Join paths";
17541
+ const ops = [];
17542
+ let keptId;
17543
+ if (cont) {
17544
+ if (cont.reversed) added[0] = reverseAnchors(added[0]);
17545
+ const set = pathToAnchors(cont.original);
17546
+ set.anchors.splice(cont.sub, 1, added[0]);
17547
+ set.closed.splice(cont.sub, 1, addedClosed[0]);
17548
+ set.anchors.push(...added.slice(1), ...rest.anchors);
17549
+ set.closed.push(...addedClosed.slice(1), ...rest.closed);
17550
+ if (sameNode) {
17551
+ set.anchors.splice(hit.sub, 1);
17552
+ set.closed.splice(hit.sub, 1);
17553
+ }
17554
+ const next = { ...anchorsToPath(set.anchors, set.closed), fillRule: cont.original.fillRule };
17555
+ ops.push(...edit.editOps(cont.id, next, label));
17556
+ keptId = cont.id;
17557
+ } else {
17558
+ const path = anchorsToPath([...added, ...rest.anchors], [...addedClosed, ...rest.closed]);
17559
+ const node = optsRef.current.adapter.makeNode(optsRef.current.wrapPath(path, { closed: false }));
17560
+ ops.push(createInsertOp({ node }));
17561
+ keptId = node.id;
17562
+ }
17563
+ if (!sameNode) ops.push(createDeleteOp({ node: scene.get(hit.id) }));
17564
+ const applyOps = deps.applyOps;
17565
+ if (applyOps) applyOps(ops, label);
17566
+ else scene.applyBatch(ops, label, defaultCommitAdapter(scene));
17567
+ if (optsRef.current.autoSelect) optsRef.current.adapter.setSelection([keptId]);
17568
+ resetScratch(s);
17569
+ }, []);
16833
17570
  const pickUpEndpoint = useCallback((deps, wx, wy) => {
16834
17571
  const scratch = scratchRef.current;
16835
17572
  if (scratch.current || scratch.finishedSubpaths.length > 0) return false;
@@ -16852,9 +17589,9 @@ function usePenTool(options) {
16852
17589
  const radius = optsRef.current.anchorSnapRadius;
16853
17590
  if (radius > 0) {
16854
17591
  const hit = nearestPathAnchor(pathsNear(deps, x, y, radius), { x, y }, radius, viewScale(deps));
16855
- if (hit) return { x: hit.x, y: hit.y };
17592
+ if (hit) return { x: hit.x, y: hit.y, hit };
16856
17593
  }
16857
- return snap2(x, y);
17594
+ return { ...snap2(x, y), hit: null };
16858
17595
  }, [snap2]);
16859
17596
  const withinCloseRadius = useCallback(
16860
17597
  (deps, ax, ay, wx, wy) => {
@@ -16869,7 +17606,7 @@ function usePenTool(options) {
16869
17606
  id: "pen.placeAnchor",
16870
17607
  label: "Pen \u2014 place anchor",
16871
17608
  eligible: { capability: "creates-paths" },
16872
- requires: ["view", "areaSelect", "editAnchors"],
17609
+ requires: ["view", "areaSelect", "editAnchors", "scene", "applyOps"],
16873
17610
  invoker: {
16874
17611
  timing: "immediate",
16875
17612
  run: (deps, params) => {
@@ -16879,7 +17616,7 @@ function usePenTool(options) {
16879
17616
  forceRenderRef.current();
16880
17617
  return;
16881
17618
  }
16882
- const { x: wx, y: wy } = placeAt(deps, p.pressX, p.pressY);
17619
+ const { x: wx, y: wy, hit } = placeAt(deps, p.pressX, p.pressY);
16883
17620
  const scratch = s();
16884
17621
  if (scratch.current && scratch.current.anchors.length >= 3) {
16885
17622
  const first = scratch.current.anchors[0];
@@ -16893,8 +17630,10 @@ function usePenTool(options) {
16893
17630
  return;
16894
17631
  }
16895
17632
  }
17633
+ const join = joinable(scratch, deps, hit);
16896
17634
  if (!scratch.current) scratch.current = { anchors: [], closed: false };
16897
17635
  scratch.current.anchors.push({ x: wx, y: wy });
17636
+ if (join) commitJoin(scratch, deps, hit);
16898
17637
  forceRenderRef.current();
16899
17638
  }
16900
17639
  }
@@ -16926,32 +17665,35 @@ function usePenTool(options) {
16926
17665
  id: "pen.dragHandle",
16927
17666
  label: "Pen \u2014 drag out a bezier handle",
16928
17667
  eligible: { capability: "creates-paths" },
16929
- requires: ["view", "areaSelect", "editAnchors"],
17668
+ requires: ["view", "areaSelect", "editAnchors", "scene", "applyOps"],
16930
17669
  invoker: {
16931
17670
  timing: "ongoing",
16932
17671
  start: (ctx) => {
16933
17672
  const scratch = s();
16934
17673
  const origin = ctx.drag?.start ?? ctx.world;
16935
17674
  const pickedUp = pickUpEndpoint(ctx.deps, origin.x, origin.y);
17675
+ let joinHit = null;
16936
17676
  if (!pickedUp) {
16937
- const { x: ax, y: ay } = placeAt(ctx.deps, origin.x, origin.y);
17677
+ const { x: ax, y: ay, hit } = placeAt(ctx.deps, origin.x, origin.y);
17678
+ if (joinable(scratch, ctx.deps, hit)) joinHit = hit;
16938
17679
  if (!scratch.current) scratch.current = { anchors: [], closed: false };
16939
17680
  scratch.current.anchors.push({ x: ax, y: ay });
16940
17681
  }
16941
17682
  scratch.draggingHandleAt = scratch.current.anchors.length - 1;
16942
- applyOutHandle(scratch, ctx.world, ctx.modifiers.shift, optsRef.current.snapPoint);
16943
17683
  if (ctx.modifiers.alt) {
16944
17684
  scratch.current.anchors[scratch.draggingHandleAt].altBroken = true;
16945
17685
  }
17686
+ const linked = !pickedUp;
17687
+ applyOutHandle(scratch, ctx.world, ctx.modifiers.shift, linked, optsRef.current.snapPoint);
16946
17688
  forceRenderRef.current();
16947
17689
  return {
16948
17690
  onMove: (moveCtx) => {
16949
17691
  const sm = s();
16950
17692
  if (sm.draggingHandleAt === null) return;
16951
- applyOutHandle(sm, moveCtx.world, moveCtx.modifiers.shift, optsRef.current.snapPoint);
16952
17693
  if (moveCtx.modifiers.alt && sm.current) {
16953
17694
  sm.current.anchors[sm.draggingHandleAt].altBroken = true;
16954
17695
  }
17696
+ applyOutHandle(sm, moveCtx.world, moveCtx.modifiers.shift, linked, optsRef.current.snapPoint);
16955
17697
  forceRenderRef.current();
16956
17698
  },
16957
17699
  onEnd: (endCtx, reason) => {
@@ -16968,12 +17710,13 @@ function usePenTool(options) {
16968
17710
  return;
16969
17711
  }
16970
17712
  if (se.draggingHandleAt !== null) {
16971
- applyOutHandle(se, endCtx.world, endCtx.modifiers.shift, optsRef.current.snapPoint);
16972
17713
  if (endCtx.modifiers.alt && se.current) {
16973
17714
  se.current.anchors[se.draggingHandleAt].altBroken = true;
16974
17715
  }
17716
+ applyOutHandle(se, endCtx.world, endCtx.modifiers.shift, linked, optsRef.current.snapPoint);
16975
17717
  se.draggingHandleAt = null;
16976
17718
  }
17719
+ if (joinHit && se.current) commitJoin(se, ctx.deps, joinHit);
16977
17720
  forceRenderRef.current();
16978
17721
  }
16979
17722
  };
@@ -17014,7 +17757,7 @@ function usePenTool(options) {
17014
17757
  }
17015
17758
  }
17016
17759
  ];
17017
- }, [commit, placeAt, withinCloseRadius, pickUpEndpoint]);
17760
+ }, [commit, placeAt, withinCloseRadius, pickUpEndpoint, joinable, commitJoin]);
17018
17761
  return useMemo(() => {
17019
17762
  return defineTool({
17020
17763
  id: "pen",
@@ -17072,7 +17815,7 @@ usePenTool.prefs = {
17072
17815
  }
17073
17816
  }
17074
17817
  };
17075
- function applyOutHandle(s, target, shift, snap2) {
17818
+ function applyOutHandle(s, target, shift, linked, snap2) {
17076
17819
  if (s.current === null || s.draggingHandleAt === null) return;
17077
17820
  const anchor = s.current.anchors[s.draggingHandleAt];
17078
17821
  const to = snap2 ? snap2(target) : target;
@@ -17084,6 +17827,7 @@ function applyOutHandle(s, target, shift, snap2) {
17084
17827
  dy = c.dy;
17085
17828
  }
17086
17829
  anchor.outHandle = { x: anchor.x + dx, y: anchor.y + dy };
17830
+ if (linked && !anchor.altBroken) anchor.inHandle = { x: anchor.x - dx, y: anchor.y - dy };
17087
17831
  }
17088
17832
  function viewScale(deps) {
17089
17833
  return deps.view?.get().scale ?? { x: 1, y: 1 };
@@ -17536,9 +18280,24 @@ function usePencilTool() {
17536
18280
  }
17537
18281
 
17538
18282
  // src/canvas/defaultDrawOne.ts
17539
- function defaultDrawOne(node, pose, _view, ctx) {
18283
+ function pixelScaleOf(view) {
18284
+ const dpr = typeof window !== "undefined" && window.devicePixelRatio || 1;
18285
+ return Math.max(Math.abs(view.scale.x), Math.abs(view.scale.y)) * dpr;
18286
+ }
18287
+ var lastView;
18288
+ var lastViewCtx = {};
18289
+ function viewOnlyCtx(view) {
18290
+ const pixelScale = pixelScaleOf(view);
18291
+ if (view !== lastView || lastViewCtx.pixelScale !== pixelScale) {
18292
+ lastView = view;
18293
+ lastViewCtx = { pixelScale };
18294
+ }
18295
+ return lastViewCtx;
18296
+ }
18297
+ function defaultDrawOne(node, pose, view, ctx) {
17540
18298
  const painter = findNodeShape(node);
17541
- const primary = painter ? painter.paint(node, pose, ctx) : [];
18299
+ const paintCtx = !view || ctx?.pixelScale !== void 0 ? ctx : ctx ? { ...ctx, pixelScale: pixelScaleOf(view) } : viewOnlyCtx(view);
18300
+ const primary = painter ? painter.paint(node, pose, paintCtx) : [];
17542
18301
  const data = node.data;
17543
18302
  if (data?.label && data.text == null) {
17544
18303
  const p = pose;
@@ -18604,11 +19363,11 @@ function buildSceneTree(adapter, drawOne, view, forLayer, derivedPathOf) {
18604
19363
  const buckets = /* @__PURE__ */ new Map();
18605
19364
  for (const l of layers) buckets.set(l.id, []);
18606
19365
  const compose2 = adapter.composePose?.bind(adapter);
18607
- function visit(id, ancestorClips, parentWorld) {
19366
+ function visit(id, ancestorClips, parentFrame) {
18608
19367
  const node = adapter.getNode(id);
18609
19368
  if (!node) return;
18610
19369
  const local = adapter.getPose(id);
18611
- const pose = compose2 && parentWorld !== null ? compose2(parentWorld, local) : local;
19370
+ const pose = compose2 && parentFrame !== null ? compose2(parentFrame, local) : local;
18612
19371
  const paints = forLayer === void 0 || node.layer === forLayer;
18613
19372
  const self = paints ? drawOne(node, pose, view) : [];
18614
19373
  let ownClips = ancestorClips;
@@ -18632,7 +19391,8 @@ function buildSceneTree(adapter, drawOne, view, forLayer, derivedPathOf) {
18632
19391
  bucket.push(ownClips.length > 0 ? wrapInClips(self, ownClips) : { kind: "group", children: self });
18633
19392
  }
18634
19393
  }
18635
- for (const cid of adapter.getChildren(id)) visit(cid, ownClips, pose);
19394
+ const childFrame = definesFrame(node) ? pose : parentFrame;
19395
+ for (const cid of adapter.getChildren(id)) visit(cid, ownClips, childFrame);
18636
19396
  }
18637
19397
  for (const rootId of adapter.getChildren(null)) visit(rootId, [], null);
18638
19398
  const out = [];
@@ -19842,219 +20602,641 @@ function wireSceneSlotToScene(slot, scene, alphaFor, colorOverrides) {
19842
20602
  alphaFor: composeAlphaFor(scene, alphaFor)
19843
20603
  };
19844
20604
  }
19845
-
19846
- // src/core/scene/poseOverrides.ts
19847
- function createPoseOverrides(getNode, onInvalidate) {
19848
- const entries = /* @__PURE__ */ new Map();
19849
- const listeners = /* @__PURE__ */ new Set();
19850
- let generation2 = 0;
19851
- function invalidate(id) {
19852
- const node = getNode(id);
19853
- if (node) dropPoseKeyedMemoSlots(node);
19854
- onInvalidate?.(id);
20605
+ function flattenVerts2(poly2) {
20606
+ const flat = new Array(poly2.length * 2);
20607
+ for (let i = 0; i < poly2.length; i++) {
20608
+ flat[i * 2] = poly2[i].x;
20609
+ flat[i * 2 + 1] = poly2[i].y;
19855
20610
  }
19856
- function published() {
19857
- generation2++;
19858
- for (const listener of listeners) listener();
20611
+ return flat;
20612
+ }
20613
+ function polygonContainsRectCenter(poly2, rect) {
20614
+ return pointInPolygon(flattenVerts2(poly2), rect.x + rect.width / 2, rect.y + rect.height / 2);
20615
+ }
20616
+ function polygonContainsRect(poly2, rect) {
20617
+ if (poly2.length < 3) return false;
20618
+ const flat = flattenVerts2(poly2);
20619
+ if (!pointInPolygon(flat, rect.x, rect.y)) return false;
20620
+ if (!pointInPolygon(flat, rect.x + rect.width, rect.y)) return false;
20621
+ if (!pointInPolygon(flat, rect.x + rect.width, rect.y + rect.height)) return false;
20622
+ if (!pointInPolygon(flat, rect.x, rect.y + rect.height)) return false;
20623
+ return !anyPolyEdgeCrossesRect(poly2, rect);
20624
+ }
20625
+ function polygonIntersectsRect(poly2, rect) {
20626
+ if (poly2.length < 3) return false;
20627
+ const ab = polygonAabb(poly2);
20628
+ if (ab.x + ab.width < rect.x || ab.x > rect.x + rect.width) return false;
20629
+ if (ab.y + ab.height < rect.y || ab.y > rect.y + rect.height) return false;
20630
+ for (const v of poly2) {
20631
+ if (v.x >= rect.x && v.x <= rect.x + rect.width && v.y >= rect.y && v.y <= rect.y + rect.height) return true;
19859
20632
  }
19860
- return {
19861
- set(id, entry) {
19862
- entries.set(id, entry);
19863
- invalidate(id);
19864
- published();
20633
+ const flat = flattenVerts2(poly2);
20634
+ if (pointInPolygon(flat, rect.x, rect.y)) return true;
20635
+ if (pointInPolygon(flat, rect.x + rect.width, rect.y)) return true;
20636
+ if (pointInPolygon(flat, rect.x + rect.width, rect.y + rect.height)) return true;
20637
+ if (pointInPolygon(flat, rect.x, rect.y + rect.height)) return true;
20638
+ return anyPolyEdgeCrossesRect(poly2, rect);
20639
+ }
20640
+ function polygonAabb(poly2) {
20641
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
20642
+ for (const v of poly2) {
20643
+ if (v.x < minX) minX = v.x;
20644
+ if (v.y < minY) minY = v.y;
20645
+ if (v.x > maxX) maxX = v.x;
20646
+ if (v.y > maxY) maxY = v.y;
20647
+ }
20648
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
20649
+ }
20650
+ function anyPolyEdgeCrossesRect(poly2, rect) {
20651
+ const r0x = rect.x, r0y = rect.y;
20652
+ const r1x = rect.x + rect.width, r1y = rect.y;
20653
+ const r2x = rect.x + rect.width, r2y = rect.y + rect.height;
20654
+ const r3x = rect.x, r3y = rect.y + rect.height;
20655
+ for (let i = 0, j = poly2.length - 1; i < poly2.length; j = i++) {
20656
+ const a = poly2[j], b = poly2[i];
20657
+ if (segmentsCross(a.x, a.y, b.x, b.y, r0x, r0y, r1x, r1y)) return true;
20658
+ if (segmentsCross(a.x, a.y, b.x, b.y, r1x, r1y, r2x, r2y)) return true;
20659
+ if (segmentsCross(a.x, a.y, b.x, b.y, r2x, r2y, r3x, r3y)) return true;
20660
+ if (segmentsCross(a.x, a.y, b.x, b.y, r3x, r3y, r0x, r0y)) return true;
20661
+ }
20662
+ return false;
20663
+ }
20664
+
20665
+ // src/canvas/sceneAdapter.ts
20666
+ function walkClipAware(scene, poseBounds, nodeTest, poseComposition) {
20667
+ return pickWalk(scenePickSource(scene, poseComposition ? { poseComposition } : {}), {
20668
+ hits: (node, pose) => nodeTest(node, pose),
20669
+ clipAdmits: (clip, _node, pose) => pathIntersectsRect(clip, poseBounds(pose))
20670
+ });
20671
+ }
20672
+ var adapterFallbackIdCounter = 0;
20673
+ function adapterFallbackId() {
20674
+ return `n${(adapterFallbackIdCounter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
20675
+ }
20676
+ var pasteIdCounter = 0;
20677
+ function pasteNodeId() {
20678
+ return `paste-${(pasteIdCounter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
20679
+ }
20680
+ function copyField(value) {
20681
+ if (Array.isArray(value)) return [...value];
20682
+ if (value !== null && typeof value === "object") return { ...value };
20683
+ return value;
20684
+ }
20685
+ function sceneToAdapter(scene, options = {}) {
20686
+ const visibleLayers = () => {
20687
+ const out = /* @__PURE__ */ new Set();
20688
+ for (const layer of scene.layers) {
20689
+ if (layer.visible) out.add(layer.id);
20690
+ }
20691
+ return out;
20692
+ };
20693
+ const sel = options.selection;
20694
+ const getSelection = sel?.getSelection ?? sel?.get ?? (() => []);
20695
+ const setSelection = sel?.setSelection ?? sel?.set ?? (() => {
20696
+ });
20697
+ const d = options.poseDescriptor ?? AUTO_POSE_DESCRIPTOR;
20698
+ const poseBounds = (p) => visualBoundsViaDescriptor(p, d);
20699
+ const translate3 = (p, dx, dy, node) => translatePoseViaDescriptor(
20700
+ p,
20701
+ dx,
20702
+ dy,
20703
+ node === void 0 ? d : poseDescriptorForNode(d, node)
20704
+ );
20705
+ const composition = options.poseComposition;
20706
+ const composes = composition !== void 0 && composition.closure !== "identity";
20707
+ if (composes && options.cascadeContainerPose) {
20708
+ throw new Error(
20709
+ "sceneToAdapter: poseComposition and cascadeContainerPose contradict each other. Under a composing strategy a child's pose is already relative to its parent, so the cascade would move every descendant twice. Drop cascadeContainerPose."
20710
+ );
20711
+ }
20712
+ const adapter = {
20713
+ getNode(id) {
20714
+ return scene.get(asNodeId(id));
19865
20715
  },
19866
- get(id) {
19867
- return entries.get(id);
20716
+ getNodes() {
20717
+ const visible = visibleLayers();
20718
+ const out = [];
20719
+ for (const n of scene.renderOrderNodes()) {
20720
+ if (visible.has(n.layer)) out.push(n);
20721
+ }
20722
+ return out;
19868
20723
  },
19869
- has(id) {
19870
- return entries.has(id);
20724
+ getPose(id) {
20725
+ const nid = asNodeId(id);
20726
+ const n = scene.get(nid);
20727
+ if (!n) throw new Error(`sceneToAdapter: unknown node "${id}"`);
20728
+ return effectivePose(scene, n);
19871
20729
  },
19872
- ids() {
19873
- return [...entries.keys()];
20730
+ getParent(id) {
20731
+ const n = scene.get(asNodeId(id));
20732
+ return n?.parent ?? null;
19874
20733
  },
19875
- clear(id) {
19876
- if (!entries.delete(id)) return;
19877
- invalidate(id);
19878
- published();
20734
+ definesFrame(id) {
20735
+ const n = scene.get(asNodeId(id));
20736
+ return n === void 0 || definesFrame(n);
19879
20737
  },
19880
- clearAll() {
19881
- if (entries.size === 0) return;
19882
- for (const id of entries.keys()) invalidate(id);
19883
- entries.clear();
19884
- published();
20738
+ getWorldPose(id) {
20739
+ if (!composes) return adapter.getPose(id);
20740
+ return composeWorldPose(adapter, id, composition.compose);
19885
20741
  },
19886
- commit() {
19887
- for (const id of entries.keys()) invalidate(id);
19888
- published();
20742
+ // Only present when it would do something: the render walk skips the fold
20743
+ // entirely when this is absent, which is the absolute-pose path.
20744
+ ...composes ? { composePose: composition.compose } : {},
20745
+ setPose(id, pose) {
20746
+ if (options.cascadeContainerPose) {
20747
+ const node = scene.get(asNodeId(id));
20748
+ if (node && node.kind === "container") {
20749
+ const g = poseDescriptorForNode(d, node);
20750
+ const before = g.getBounds(node.pose);
20751
+ const after = g.getBounds(pose);
20752
+ const dx = after.x - before.x;
20753
+ const dy = after.y - before.y;
20754
+ if (dx !== 0 || dy !== 0) {
20755
+ const descIds = [];
20756
+ const collect = (rootId) => {
20757
+ for (const cid of scene.childrenOf(asNodeId(rootId))) {
20758
+ descIds.push(cid);
20759
+ collect(cid);
20760
+ }
20761
+ };
20762
+ collect(id);
20763
+ scene.batch("move container", () => {
20764
+ scene.setPose(asNodeId(id), pose);
20765
+ for (const cid of descIds) {
20766
+ const cn = scene.get(asNodeId(cid));
20767
+ if (!cn) continue;
20768
+ scene.setPose(asNodeId(cid), translate3(cn.pose, dx, dy, cn));
20769
+ }
20770
+ });
20771
+ return;
20772
+ }
20773
+ }
20774
+ }
20775
+ scene.setPose(asNodeId(id), pose);
19889
20776
  },
19890
- subscribe(fn) {
19891
- listeners.add(fn);
19892
- return () => {
19893
- listeners.delete(fn);
19894
- };
20777
+ setParent(id, parentId) {
20778
+ scene.move(asNodeId(id), parentId === null ? null : asNodeId(parentId));
19895
20779
  },
19896
- getGeneration() {
19897
- return generation2;
19898
- }
19899
- };
19900
- }
19901
-
19902
- // src/core/scene/scene.ts
19903
- var defaultIdCounter = 0;
19904
- var defaultGenerateId = () => asNodeId(`n${(defaultIdCounter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`);
19905
- function layerFromSerialized(spec) {
19906
- const base = {
19907
- id: spec.id,
19908
- visible: spec.visible ?? true,
19909
- locked: spec.locked ?? false
19910
- };
19911
- return spec.kind === "user" ? { ...base, kind: "user", name: spec.name ?? spec.id } : { ...base, kind: "system" };
19912
- }
19913
- function createScene(options) {
19914
- if (!options.systemLayers || options.systemLayers.length === 0) {
19915
- throw new Error("useScene: systemLayers must declare at least one layer");
19916
- }
19917
- const state = {
19918
- nodes: /* @__PURE__ */ new Map(),
19919
- roots: [],
19920
- layers: [],
19921
- layerIndex: /* @__PURE__ */ new Map()
19922
- };
19923
- const registry2 = withKitRegistry(options.registry ?? {});
19924
- const fnFields = createNodeFnFields(registry2);
19925
- const fnField = (name) => fnFields.find((f) => f.spec.field === name);
19926
- const dependents = createDependentsIndex();
19927
- const childDerived = /* @__PURE__ */ new Set();
19928
- function invalidateDependents(id) {
19929
- invalidateChildDerivedFrom(state.nodes.get(id)?.parent ?? null);
19930
- if (dependents.isEmpty()) return;
19931
- const subtree = [id];
19932
- descendants(id, subtree);
19933
- for (const src of subtree) {
19934
- for (const dep of dependents.transitiveDependentsOf(src)) {
19935
- const node = state.nodes.get(dep);
19936
- if (node !== void 0) dropPoseKeyedMemoSlots(node);
19937
- }
19938
- }
19939
- }
19940
- function invalidateChildDerivedFrom(start) {
19941
- if (childDerived.size === 0) return;
19942
- let cursor = start;
19943
- while (cursor !== null) {
19944
- const node = state.nodes.get(cursor);
19945
- if (node === void 0) return;
19946
- if (childDerived.has(cursor)) {
19947
- dropPoseKeyedMemoSlots(node);
19948
- for (const dep of dependents.transitiveDependentsOf(cursor)) {
19949
- const d = state.nodes.get(dep);
19950
- if (d !== void 0) dropPoseKeyedMemoSlots(d);
20780
+ getChildren(id) {
20781
+ if (id === null) return [...scene.roots];
20782
+ return [...scene.childrenOf(asNodeId(id))];
20783
+ },
20784
+ setChildOrder(parentId, ids) {
20785
+ scene.batch("Reorder", () => {
20786
+ for (let i = 0; i < ids.length; i++) {
20787
+ const current = parentId === null ? scene.roots : scene.childrenOf(asNodeId(parentId));
20788
+ if (current[i] === ids[i]) continue;
20789
+ scene.reorder(asNodeId(ids[i]), i);
19951
20790
  }
20791
+ });
20792
+ },
20793
+ getLayers() {
20794
+ return scene.layers.map((l) => ({ id: l.id, visible: l.visible }));
20795
+ },
20796
+ ...options.layouts ? {
20797
+ getLayout: typeof options.layouts === "function" ? options.layouts : (id) => options.layouts[id] ?? null
20798
+ } : {},
20799
+ // Unified applyOps: when a label is supplied the ops are wrapped in a
20800
+ // batch checkpoint (history-aware path used by Move/Resize/Rotate/Insert/
20801
+ // Delete tools). When a journal is active (via scene's getActiveJournal
20802
+ // option), scene.applyBatch routes to the journal instead of recording a
20803
+ // new parent-history entry. Without a label the ops are applied transiently
20804
+ // via applyOpsTo (AreaSelectAdapter contract — no checkpoint).
20805
+ applyOps(ops, label) {
20806
+ if (label !== void 0) {
20807
+ scene.applyBatch(ops, label, this);
20808
+ } else {
20809
+ applyOpsTo(this, ops);
19952
20810
  }
19953
- cursor = node.parent;
19954
- }
19955
- }
19956
- function trackChildDerived(id, dependsOn) {
19957
- if (dependsOn === "children") childDerived.add(id);
19958
- else childDerived.delete(id);
19959
- }
19960
- function retarget(id, dependsOn) {
19961
- const node = requireNode(id);
19962
- if (dependsOn === void 0) delete node.dependsOn;
19963
- else node.dependsOn = dependsOn;
19964
- dependents.add(id, dependsOn === void 0 || dependsOn === "children" ? [] : dependsOn);
19965
- trackChildDerived(id, dependsOn);
19966
- dropPoseKeyedMemoSlots(requireNode(id));
19967
- invalidateDependents(id);
19968
- }
19969
- for (let i = 0; i < options.systemLayers.length; i++) {
19970
- const spec = options.systemLayers[i];
19971
- if (state.layerIndex.has(spec.id)) {
19972
- throw new Error(`useScene: duplicate system layer id "${spec.id}"`);
19973
- }
19974
- state.layers.push({
19975
- kind: "system",
19976
- id: spec.id,
19977
- visible: spec.visible ?? true,
19978
- locked: spec.locked ?? false
19979
- });
19980
- state.layerIndex.set(spec.id, i);
19981
- }
19982
- const generateId = options.generateId ?? defaultGenerateId;
19983
- const listeners = /* @__PURE__ */ new Set();
19984
- const registered = /* @__PURE__ */ new Map();
19985
- let historyAdapterAccessor = null;
19986
- function bindOpToHistoryAdapter(op) {
19987
- return {
19988
- name: op.name,
19989
- args: op.args,
19990
- label: op.label,
19991
- coalesceKey: op.coalesceKey,
19992
- apply: () => {
19993
- const get = historyAdapterAccessor;
19994
- if (!get) {
19995
- dwarn("scene", `restored op "${op.name ?? "?"}" has no history adapter \u2014 skipping (setHistoryAdapter was not wired)`);
19996
- return "noop";
19997
- }
19998
- try {
19999
- return op.apply(get());
20000
- } catch (err) {
20001
- dwarn("scene", `restored op "${op.name ?? "?"}" failed against the history adapter \u2014 skipping: ${String(err)}`);
20002
- return "noop";
20811
+ },
20812
+ // Node-mutation surface used by kit InsertOps / DeleteOps (e.g.
20813
+ // useNest wrapping the selection in a new container node, or its
20814
+ // inverse). Re-adds via the full structural spec so the round-trip
20815
+ // survives undo/redo; removes by id.
20816
+ insertNode(node, index) {
20817
+ scene.add({
20818
+ kind: node.kind,
20819
+ layer: node.layer,
20820
+ pose: node.pose,
20821
+ data: node.data,
20822
+ id: node.id,
20823
+ ...index !== void 0 ? { index } : {},
20824
+ ...node.parent !== null ? { parent: node.parent } : {},
20825
+ // Derivation travels with the node, or undo of a delete brings an
20826
+ // edge back as a static path that never follows its endpoints again.
20827
+ ...node.dependsOn !== void 0 ? { dependsOn: node.dependsOn } : {},
20828
+ ...fnFieldsOfNode(node)
20829
+ });
20830
+ },
20831
+ removeNode(id) {
20832
+ scene.remove(asNodeId(id));
20833
+ },
20834
+ setData(id, data) {
20835
+ scene.update(asNodeId(id), { data });
20836
+ },
20837
+ getRemovalClosure(ids) {
20838
+ return [...scene.removalClosure(ids.map(asNodeId))];
20839
+ },
20840
+ // AreaSelectAdapter surface — included unconditionally so plain
20841
+ // `useSelectTool(sceneToAdapter(scene, { selection }))` Just Works for the
20842
+ // marquee gesture. `applyOps` (above) dispatches transiently when called
20843
+ // without a label, matching the AreaSelectAdapter contract; `hitTestArea`
20844
+ // does an AABB-vs-AABB scan over `scene.renderOrder()`, reading bounds
20845
+ // through the pose descriptor.
20846
+ getSelection,
20847
+ setSelection,
20848
+ hitTestArea(rect) {
20849
+ return walkClipAware(scene, poseBounds, (_n, pose) => {
20850
+ const b = poseBounds(pose);
20851
+ return b.x < rect.x + rect.width && b.x + b.width > rect.x && b.y < rect.y + rect.height && b.y + b.height > rect.y;
20852
+ }, composition);
20853
+ },
20854
+ hitTestLasso(polygon, mode) {
20855
+ if (polygon.length < 3) return [];
20856
+ return walkClipAware(scene, poseBounds, (_n, pose) => {
20857
+ const b = poseBounds(pose);
20858
+ return mode === "centers" ? polygonContainsRectCenter(polygon, b) : mode === "enclosed" ? polygonContainsRect(polygon, b) : polygonIntersectsRect(polygon, b);
20859
+ }, composition);
20860
+ },
20861
+ // commitInsert (gesture-time leaf insert) is opt-in: present only when
20862
+ // `options.commitInsert` is. The full insertNode/removeNode mutators
20863
+ // above are always present so kit-side InsertOp / DeleteOp round-trip
20864
+ // through scene.add / scene.remove regardless.
20865
+ //
20866
+ // Contract: this is a pure factory — it constructs a Node spec but does
20867
+ // NOT touch the scene. `useInsert`'s onEnd dispatches an `InsertOp`
20868
+ // whose apply() calls `adapter.insertNode` → `scene.add`, which is the
20869
+ // sole insertion path. Calling `scene.add` here too would double-insert,
20870
+ // and the second add throws on id collision (silently dropping the
20871
+ // gesture's pointer-up cleanup and breaking the next drag).
20872
+ ...options.commitInsert ? {
20873
+ commitInsert: (bounds) => {
20874
+ const created = options.commitInsert(bounds);
20875
+ if (!created) return null;
20876
+ const layer = options.insertLayer ?? "default";
20877
+ const id = asNodeId(created.id ?? adapterFallbackId());
20878
+ return {
20879
+ kind: "leaf",
20880
+ id,
20881
+ layer,
20882
+ pose: created.pose,
20883
+ data: created.data,
20884
+ parent: null
20885
+ };
20886
+ }
20887
+ } : {},
20888
+ // Clipboard seam (`useClipboardOps` / the paste ingestion path).
20889
+ //
20890
+ // `snapshotSelection` captures adapter-shaped node copies; `commitPaste`
20891
+ // is a pure factory like `commitInsert` above — it mints fresh nodes but
20892
+ // NEVER touches the scene. Insertion happens via the hook's InsertOps
20893
+ // (one per created node, parents before children), which is what makes a
20894
+ // whole paste a single undo entry.
20895
+ snapshotSelection(ids) {
20896
+ const idSet = new Set(ids);
20897
+ const taken = /* @__PURE__ */ new Set();
20898
+ const items = [];
20899
+ const poseFor = (n, isRoot) => {
20900
+ if (!composes) return n.pose;
20901
+ if (isRoot) return adapter.getWorldPose(n.id);
20902
+ const frame = frameAtOrAbove(adapter, n.parent);
20903
+ return frame !== null && taken.has(frame) ? n.pose : adapter.getWorldPose(n.id);
20904
+ };
20905
+ const derivation = (n) => n.dependsOn === "children" && n.derivePose ? { dependsOn: n.dependsOn, derivePose: n.derivePose } : {};
20906
+ const capture = (id, isRoot = false) => {
20907
+ if (taken.has(id)) return;
20908
+ const n = scene.get(asNodeId(id));
20909
+ if (!n) return;
20910
+ taken.add(id);
20911
+ if (n.kind === "container") {
20912
+ items.push({
20913
+ kind: "container",
20914
+ id: n.id,
20915
+ layer: n.layer,
20916
+ parent: n.parent,
20917
+ pose: copyField(poseFor(n, isRoot)),
20918
+ data: copyField(n.data),
20919
+ children: [...n.children],
20920
+ ...n.clipFromPose ? { clipFromPose: n.clipFromPose } : {},
20921
+ ...derivation(n)
20922
+ });
20923
+ for (const cid of scene.childrenOf(asNodeId(id))) capture(cid);
20924
+ } else {
20925
+ items.push({
20926
+ kind: "leaf",
20927
+ id: n.id,
20928
+ layer: n.layer,
20929
+ parent: n.parent,
20930
+ pose: copyField(poseFor(n, isRoot)),
20931
+ data: copyField(n.data)
20932
+ });
20003
20933
  }
20004
- },
20005
- invert: () => bindOpToHistoryAdapter(op.invert())
20006
- };
20007
- }
20008
- let selection = [];
20009
- const overrides = createPoseOverrides(
20010
- (id) => state.nodes.get(id),
20011
- invalidateDependents
20012
- );
20013
- const history = createHistory(void 0, {
20014
- selection: {
20015
- get: () => selection,
20016
- set: (ids) => {
20017
- selection = reachable(ids);
20018
- notify2();
20934
+ };
20935
+ for (const id of ids) {
20936
+ if (scene.ancestorsOf(asNodeId(id)).some((a) => idSet.has(a))) continue;
20937
+ capture(id, true);
20019
20938
  }
20939
+ return { items };
20020
20940
  },
20021
- ...options.coalesceWindowMs !== void 0 ? { coalesceWindowMs: options.coalesceWindowMs } : {},
20022
- ...options.historyLimit !== void 0 ? { historyLimit: options.historyLimit } : {},
20023
- onEvict: (entry) => {
20024
- pruneCacheForDroppedOps(entry.forwardOps);
20025
- pruneCacheForDroppedOps(entry.baseOps);
20941
+ commitPaste(clipboard, offset, ctx) {
20942
+ const src = clipboard.items;
20943
+ if (src.length === 0) return [];
20944
+ const idMap = /* @__PURE__ */ new Map();
20945
+ for (const item of src) idMap.set(item.id, asNodeId(pasteNodeId()));
20946
+ const isRoot = (item) => item.parent === null || !idMap.has(item.parent);
20947
+ const byId = new Map(src.map((item) => [item.id, item]));
20948
+ const ridesPastedFrame = (item) => {
20949
+ if (!composes) return false;
20950
+ let p = item.parent === null ? void 0 : byId.get(item.parent);
20951
+ while (p !== void 0) {
20952
+ if (definesFrame(p)) return true;
20953
+ p = p.parent === null ? void 0 : byId.get(p.parent);
20954
+ }
20955
+ return false;
20956
+ };
20957
+ let dx = offset.dx;
20958
+ let dy = offset.dy;
20959
+ if (ctx?.dropPoint) {
20960
+ let minX = Infinity;
20961
+ let minY = Infinity;
20962
+ for (const item of src) {
20963
+ if (!isRoot(item)) continue;
20964
+ const b = poseBounds(item.pose);
20965
+ if (b.x < minX) minX = b.x;
20966
+ if (b.y < minY) minY = b.y;
20967
+ }
20968
+ if (Number.isFinite(minX) && Number.isFinite(minY)) {
20969
+ dx = ctx.dropPoint.worldX - minX;
20970
+ dy = ctx.dropPoint.worldY - minY;
20971
+ }
20972
+ }
20973
+ const out = [];
20974
+ for (const item of src) {
20975
+ const base = {
20976
+ id: idMap.get(item.id),
20977
+ layer: item.layer,
20978
+ parent: isRoot(item) ? null : idMap.get(item.parent),
20979
+ pose: ridesPastedFrame(item) ? copyField(item.pose) : translate3(copyField(item.pose), dx, dy),
20980
+ data: copyField(item.data)
20981
+ };
20982
+ if (item.kind === "container") {
20983
+ out.push({
20984
+ ...base,
20985
+ kind: "container",
20986
+ children: item.children.filter((c) => idMap.has(c)).map((c) => idMap.get(c)),
20987
+ ...item.clipFromPose ? { clipFromPose: item.clipFromPose } : {},
20988
+ ...item.dependsOn === "children" && item.derivePose ? { dependsOn: item.dependsOn, derivePose: item.derivePose } : {}
20989
+ });
20990
+ } else {
20991
+ out.push({ ...base, kind: "leaf" });
20992
+ }
20993
+ }
20994
+ return out;
20026
20995
  },
20027
- // Restore-time rebuild: scene-registered kinds (kit:* + consumer
20028
- // registerOp) rebuild via makeOp; external ops rebuild via the global
20029
- // op-factory registry, wrapped in the lazy history-adapter binding.
20030
- // Return null only when the global registry also misses — the engine
20031
- // then places its no-op placeholder. (The engine's own global-registry
20032
- // fallback never fires for scene histories: this hook already
20033
- // consulted it.)
20034
- rebuildOp: (name, args) => {
20035
- if (registered.has(name)) return makeOp(name, args);
20036
- const rebuilt = rebuildOp(name, args);
20037
- return rebuilt === null ? null : bindOpToHistoryAdapter(rebuilt);
20038
- }
20039
- });
20040
- let version = 0;
20041
- let batchDepth = 0;
20042
- let currentBatch = null;
20043
- let suppressRecording = false;
20044
- let untrackedDepth = 0;
20045
- let lockBypass = 0;
20046
- let appliedLog = null;
20047
- let batchDirty = false;
20048
- let activeJournalAccessor = options.getActiveJournal ?? null;
20049
- function isLockedInternal(id) {
20050
- if (!state.layers.some((l) => l.locked)) return false;
20051
- let cur = state.nodes.get(id);
20052
- while (cur !== void 0) {
20053
- if (state.layers[state.layerIndex.get(cur.layer) ?? -1]?.locked) return true;
20054
- cur = cur.parent === null ? void 0 : state.nodes.get(cur.parent);
20996
+ // Constant cascade: the hook re-snapshots the just-created items, so
20997
+ // repeated pastes stack +12/+12 each time.
20998
+ getPasteOffset() {
20999
+ return { dx: 12, dy: 12 };
20055
21000
  }
20056
- return false;
20057
- }
21001
+ };
21002
+ return adapter;
21003
+ }
21004
+ function useSceneAdapter(scene, options = {}) {
21005
+ const {
21006
+ selection,
21007
+ commitInsert,
21008
+ insertLayer,
21009
+ layouts,
21010
+ poseDescriptor,
21011
+ cascadeContainerPose,
21012
+ poseComposition
21013
+ } = options;
21014
+ return useMemo(
21015
+ () => sceneToAdapter(scene, {
21016
+ selection,
21017
+ commitInsert,
21018
+ insertLayer,
21019
+ layouts,
21020
+ poseDescriptor,
21021
+ cascadeContainerPose,
21022
+ poseComposition
21023
+ }),
21024
+ [scene, selection, commitInsert, insertLayer, layouts, poseDescriptor, cascadeContainerPose, poseComposition]
21025
+ );
21026
+ }
21027
+
21028
+ // src/core/scene/poseOverrides.ts
21029
+ function createPoseOverrides(getNode, onInvalidate) {
21030
+ const entries = /* @__PURE__ */ new Map();
21031
+ const listeners = /* @__PURE__ */ new Set();
21032
+ let generation2 = 0;
21033
+ function invalidate(id) {
21034
+ const node = getNode(id);
21035
+ if (node) dropPoseKeyedMemoSlots(node);
21036
+ onInvalidate?.(id);
21037
+ }
21038
+ function published() {
21039
+ generation2++;
21040
+ for (const listener of listeners) listener();
21041
+ }
21042
+ return {
21043
+ set(id, entry) {
21044
+ entries.set(id, entry);
21045
+ invalidate(id);
21046
+ published();
21047
+ },
21048
+ get(id) {
21049
+ return entries.get(id);
21050
+ },
21051
+ has(id) {
21052
+ return entries.has(id);
21053
+ },
21054
+ ids() {
21055
+ return [...entries.keys()];
21056
+ },
21057
+ clear(id) {
21058
+ if (!entries.delete(id)) return;
21059
+ invalidate(id);
21060
+ published();
21061
+ },
21062
+ clearAll() {
21063
+ if (entries.size === 0) return;
21064
+ for (const id of entries.keys()) invalidate(id);
21065
+ entries.clear();
21066
+ published();
21067
+ },
21068
+ commit() {
21069
+ for (const id of entries.keys()) invalidate(id);
21070
+ published();
21071
+ },
21072
+ subscribe(fn) {
21073
+ listeners.add(fn);
21074
+ return () => {
21075
+ listeners.delete(fn);
21076
+ };
21077
+ },
21078
+ getGeneration() {
21079
+ return generation2;
21080
+ }
21081
+ };
21082
+ }
21083
+
21084
+ // src/core/scene/scene.ts
21085
+ var defaultIdCounter = 0;
21086
+ var defaultGenerateId = () => asNodeId(`n${(defaultIdCounter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`);
21087
+ function layerFromSerialized(spec) {
21088
+ const base = {
21089
+ id: spec.id,
21090
+ visible: spec.visible ?? true,
21091
+ locked: spec.locked ?? false
21092
+ };
21093
+ return spec.kind === "user" ? { ...base, kind: "user", name: spec.name ?? spec.id } : { ...base, kind: "system" };
21094
+ }
21095
+ function createScene(options) {
21096
+ if (!options.systemLayers || options.systemLayers.length === 0) {
21097
+ throw new Error("useScene: systemLayers must declare at least one layer");
21098
+ }
21099
+ const state = {
21100
+ nodes: /* @__PURE__ */ new Map(),
21101
+ roots: [],
21102
+ layers: [],
21103
+ layerIndex: /* @__PURE__ */ new Map()
21104
+ };
21105
+ const registry2 = withKitRegistry(options.registry ?? {});
21106
+ const fnFields = createNodeFnFields(registry2);
21107
+ const fnField = (name) => fnFields.find((f) => f.spec.field === name);
21108
+ const dependents = createDependentsIndex();
21109
+ const childDerived = /* @__PURE__ */ new Set();
21110
+ function invalidateDependents(id) {
21111
+ invalidateChildDerivedFrom(state.nodes.get(id)?.parent ?? null);
21112
+ if (dependents.isEmpty()) return;
21113
+ const subtree = [id];
21114
+ descendants(id, subtree);
21115
+ for (const src of subtree) {
21116
+ for (const dep of dependents.transitiveDependentsOf(src)) {
21117
+ const node = state.nodes.get(dep);
21118
+ if (node !== void 0) dropPoseKeyedMemoSlots(node);
21119
+ }
21120
+ }
21121
+ }
21122
+ function invalidateChildDerivedFrom(start) {
21123
+ if (childDerived.size === 0) return;
21124
+ let cursor = start;
21125
+ while (cursor !== null) {
21126
+ const node = state.nodes.get(cursor);
21127
+ if (node === void 0) return;
21128
+ if (childDerived.has(cursor)) {
21129
+ dropPoseKeyedMemoSlots(node);
21130
+ for (const dep of dependents.transitiveDependentsOf(cursor)) {
21131
+ const d = state.nodes.get(dep);
21132
+ if (d !== void 0) dropPoseKeyedMemoSlots(d);
21133
+ }
21134
+ }
21135
+ cursor = node.parent;
21136
+ }
21137
+ }
21138
+ function trackChildDerived(id, dependsOn) {
21139
+ if (dependsOn === "children") childDerived.add(id);
21140
+ else childDerived.delete(id);
21141
+ }
21142
+ function retarget(id, dependsOn) {
21143
+ const node = requireNode(id);
21144
+ if (dependsOn === void 0) delete node.dependsOn;
21145
+ else node.dependsOn = dependsOn;
21146
+ dependents.add(id, dependsOn === void 0 || dependsOn === "children" ? [] : dependsOn);
21147
+ trackChildDerived(id, dependsOn);
21148
+ dropPoseKeyedMemoSlots(requireNode(id));
21149
+ invalidateDependents(id);
21150
+ }
21151
+ for (let i = 0; i < options.systemLayers.length; i++) {
21152
+ const spec = options.systemLayers[i];
21153
+ if (state.layerIndex.has(spec.id)) {
21154
+ throw new Error(`useScene: duplicate system layer id "${spec.id}"`);
21155
+ }
21156
+ state.layers.push({
21157
+ kind: "system",
21158
+ id: spec.id,
21159
+ visible: spec.visible ?? true,
21160
+ locked: spec.locked ?? false
21161
+ });
21162
+ state.layerIndex.set(spec.id, i);
21163
+ }
21164
+ const generateId = options.generateId ?? defaultGenerateId;
21165
+ const listeners = /* @__PURE__ */ new Set();
21166
+ const registered = /* @__PURE__ */ new Map();
21167
+ let historyAdapterAccessor = null;
21168
+ function bindOpToHistoryAdapter(op) {
21169
+ return {
21170
+ name: op.name,
21171
+ args: op.args,
21172
+ label: op.label,
21173
+ coalesceKey: op.coalesceKey,
21174
+ apply: () => {
21175
+ const get = historyAdapterAccessor;
21176
+ if (!get) {
21177
+ dwarn("scene", `restored op "${op.name ?? "?"}" has no history adapter \u2014 skipping (setHistoryAdapter was not wired)`);
21178
+ return "noop";
21179
+ }
21180
+ try {
21181
+ return op.apply(get());
21182
+ } catch (err) {
21183
+ dwarn("scene", `restored op "${op.name ?? "?"}" failed against the history adapter \u2014 skipping: ${String(err)}`);
21184
+ return "noop";
21185
+ }
21186
+ },
21187
+ invert: () => bindOpToHistoryAdapter(op.invert())
21188
+ };
21189
+ }
21190
+ let selection = [];
21191
+ const overrides = createPoseOverrides(
21192
+ (id) => state.nodes.get(id),
21193
+ invalidateDependents
21194
+ );
21195
+ const history = createHistory(void 0, {
21196
+ selection: {
21197
+ get: () => selection,
21198
+ set: (ids) => {
21199
+ selection = reachable(ids);
21200
+ notify2();
21201
+ }
21202
+ },
21203
+ ...options.coalesceWindowMs !== void 0 ? { coalesceWindowMs: options.coalesceWindowMs } : {},
21204
+ ...options.historyLimit !== void 0 ? { historyLimit: options.historyLimit } : {},
21205
+ onEvict: (entry) => {
21206
+ pruneCacheForDroppedOps(entry.forwardOps);
21207
+ pruneCacheForDroppedOps(entry.baseOps);
21208
+ },
21209
+ // Restore-time rebuild: scene-registered kinds (kit:* + consumer
21210
+ // registerOp) rebuild via makeOp; external ops rebuild via the global
21211
+ // op-factory registry, wrapped in the lazy history-adapter binding.
21212
+ // Return null only when the global registry also misses — the engine
21213
+ // then places its no-op placeholder. (The engine's own global-registry
21214
+ // fallback never fires for scene histories: this hook already
21215
+ // consulted it.)
21216
+ rebuildOp: (name, args) => {
21217
+ if (registered.has(name)) return makeOp(name, args);
21218
+ const rebuilt = rebuildOp(name, args);
21219
+ return rebuilt === null ? null : bindOpToHistoryAdapter(rebuilt);
21220
+ }
21221
+ });
21222
+ let version = 0;
21223
+ let batchDepth = 0;
21224
+ let currentBatch = null;
21225
+ let suppressRecording = false;
21226
+ let untrackedDepth = 0;
21227
+ let lockBypass = 0;
21228
+ let appliedLog = null;
21229
+ let batchDirty = false;
21230
+ let activeJournalAccessor = options.getActiveJournal ?? null;
21231
+ function isLockedInternal(id) {
21232
+ if (!state.layers.some((l) => l.locked)) return false;
21233
+ let cur = state.nodes.get(id);
21234
+ while (cur !== void 0) {
21235
+ if (state.layers[state.layerIndex.get(cur.layer) ?? -1]?.locked) return true;
21236
+ cur = cur.parent === null ? void 0 : state.nodes.get(cur.parent);
21237
+ }
21238
+ return false;
21239
+ }
20058
21240
  function refuseUntracked(verb) {
20059
21241
  if (untrackedDepth > 0) throw new Error(`Scene: cannot ${verb} inside untracked() \u2014 it records`);
20060
21242
  }
@@ -21899,7 +23081,7 @@ function CanvasView(props) {
21899
23081
  const viewSelection = selectionProp ?? (selectionOptions ? ownSelection : void 0);
21900
23082
  const [internalView, setInternalView] = useState(() => normalizeView(defaultView ?? IDENTITY_VIEW2));
21901
23083
  const effectiveView = viewProp ?? internalView;
21902
- const live = useRef({
23084
+ const live2 = useRef({
21903
23085
  view: effectiveView,
21904
23086
  bounds,
21905
23087
  layers,
@@ -21913,8 +23095,8 @@ function CanvasView(props) {
21913
23095
  // surface's otherwise. Filled in below, once the surface's is in hand.
21914
23096
  selection: viewSelection ?? ownSelection
21915
23097
  });
21916
- live.current = {
21917
- ...live.current,
23098
+ live2.current = {
23099
+ ...live2.current,
21918
23100
  view: effectiveView,
21919
23101
  bounds,
21920
23102
  layers,
@@ -21926,7 +23108,7 @@ function CanvasView(props) {
21926
23108
  viewSelection
21927
23109
  };
21928
23110
  const rectAt = useCallback((outer, dims) => {
21929
- const b = live.current.bounds;
23111
+ const b = live2.current.bounds;
21930
23112
  return typeof b === "function" ? b(outer, dims) : b;
21931
23113
  }, []);
21932
23114
  const rectNow = useCallback(() => {
@@ -21934,18 +23116,18 @@ function CanvasView(props) {
21934
23116
  return rectAt(surface?.view() ?? IDENTITY_VIEW2, surface?.dims() ?? UNMEASURED_DIMS);
21935
23117
  }, [registry2, rectAt]);
21936
23118
  const cameraAt = useCallback((outer, dims) => {
21937
- const v = live.current.view;
23119
+ const v = live2.current.view;
21938
23120
  return normalizeView(typeof v === "function" ? v(outer, dims) : v);
21939
23121
  }, []);
21940
23122
  const camera = useCallback(() => {
21941
23123
  const surface = registry2?.surface();
21942
23124
  return cameraAt(surface?.view() ?? IDENTITY_VIEW2, surface?.dims() ?? UNMEASURED_DIMS);
21943
23125
  }, [registry2, cameraAt]);
21944
- const drawnLayers = useCallback(() => paintedLayers(registry2?.surface()?.layers() ?? [], live.current), [registry2]);
23126
+ const drawnLayers = useCallback(() => paintedLayers(registry2?.surface()?.layers() ?? [], live2.current), [registry2]);
21945
23127
  const [sceneLayerGate] = useState(createSceneLayerGate);
21946
- const layerIsPainted = useCallback((layerId) => sceneLayerGate(registry2?.surface()?.layers() ?? [], live.current)(layerId), [registry2, sceneLayerGate]);
23128
+ const layerIsPainted = useCallback((layerId) => sceneLayerGate(registry2?.surface()?.layers() ?? [], live2.current)(layerId), [registry2, sceneLayerGate]);
21947
23129
  const setView = useCallback((next) => {
21948
- const { viewBounds: vb, onViewChange: cb, viewProp: controlled } = live.current;
23130
+ const { viewBounds: vb, onViewChange: cb, viewProp: controlled } = live2.current;
21949
23131
  const rect = rectNow();
21950
23132
  const valid = normalizeView(next);
21951
23133
  const clamped = vb ? clampView(valid, vb, { width: rect.w, height: rect.h }) : valid;
@@ -21988,11 +23170,11 @@ function CanvasView(props) {
21988
23170
  ...createDispatcherPreviewSources(() => dispatcherRef.current)
21989
23171
  }), []);
21990
23172
  const selection = viewSelection ?? inputs?.selectionApi ?? ownSelection;
21991
- live.current.selection = selection;
23173
+ live2.current.selection = selection;
21992
23174
  const inputsRef = useRef(inputs);
21993
23175
  inputsRef.current = inputs;
21994
23176
  const ruleInputs = useCallback(() => ({
21995
- selection: live.current.selection.get(),
23177
+ selection: live2.current.selection.get(),
21996
23178
  view: camera(),
21997
23179
  action: dispatcherRef.current.getActiveAction()
21998
23180
  }), [camera]);
@@ -22036,550 +23218,166 @@ function CanvasView(props) {
22036
23218
  helpersRef.current,
22037
23219
  drawnLayers()
22038
23220
  );
22039
- if (extra) {
22040
- const claim = extra.hit;
22041
- return {
22042
- kind: `layer:${extra.layerId}`,
22043
- owner: extra.layerId,
22044
- strength: claim.strength ?? "shared",
22045
- ...claim.claimedKinds !== void 0 ? { claimedKinds: claim.claimedKinds } : {},
22046
- ...claim.cursor !== void 0 ? { cursor: claim.cursor } : {},
22047
- ...claim.initialScratch !== void 0 ? { payload: claim.initialScratch } : {}
22048
- };
22049
- }
22050
- return inner(world);
22051
- };
22052
- }, [getAnchorState, rectNow, registry2, targetScale, camera, drawnLayers]);
22053
- const classifyTarget = useMemo(() => {
22054
- const inner = buildClassifyTarget(
22055
- () => live.current.selection.get(),
22056
- (wx, wy) => {
22057
- const i = inputsRef.current;
22058
- const frame = { ...camera(), layerIsPainted };
22059
- if (i?.pickBest) return i.pickBest(wx, wy, frame);
22060
- const ids = i?.pickEvery?.(wx, wy, frame) ?? [];
22061
- return ids.length > 0 ? ids[ids.length - 1] : null;
22062
- },
22063
- (id2) => inputsRef.current?.kindOfNode?.(id2)
22064
- );
22065
- return (world) => inner(world);
22066
- }, [camera, layerIsPainted]);
22067
- const registration = useMemo(() => ({
22068
- id,
22069
- order,
22070
- interactive,
22071
- paint,
22072
- layer: createViewportLayer({
22073
- id,
22074
- label: label ?? id,
22075
- source: drawnLayers,
22076
- view: cameraAt,
22077
- bounds: (outer, dims) => rectAt(outer, dims),
22078
- // The surface half of the envelope passes through; the view half is
22079
- // this view's, so its layers paint its chrome rather than the
22080
- // surface's.
22081
- data: (outer) => ({ ...outer, ...helpersRef.current }),
22082
- ...background !== void 0 ? { background } : {}
22083
- }),
22084
- layerIsPainted,
22085
- target: {
22086
- dispatcher: dispatcherRef.current,
22087
- affordanceAt,
22088
- classifyTarget,
22089
- clientToWorld: clientToWorldHere,
22090
- deps: () => ({
22091
- view: viewApi,
22092
- ingestion: ingestionApi,
22093
- // An Escape here cancels what is in flight here.
22094
- dispatcher: { cancelAll: (reason) => dispatcherRef.current.cancelAll(reason) },
22095
- // Only a view with its own selection overlays that one; otherwise the
22096
- // surface's answer stands, wrappers (`selectionMode`) included.
22097
- ...live.current.viewSelection ? { selection: live.current.viewSelection } : {}
22098
- }),
22099
- // Action eligibility is chrome-caps evaluated against this view, so a
22100
- // rule that hides an action in one panel cannot decline a gesture in
22101
- // another.
22102
- getRuleCtx: () => inputsRef.current?.chromeCaps?.ruleCtx(ruleInputs())
22103
- }
22104
- }), [
22105
- id,
22106
- order,
22107
- interactive,
22108
- paint,
22109
- label,
22110
- background,
22111
- rectAt,
22112
- cameraAt,
22113
- viewApi,
22114
- affordanceAt,
22115
- classifyTarget,
22116
- clientToWorldHere,
22117
- ruleInputs,
22118
- ingestionApi,
22119
- drawnLayers,
22120
- layerIsPainted
22121
- ]);
22122
- useEffect(() => {
22123
- if (!registry2) return;
22124
- return registry2.register(registration);
22125
- }, [registry2, registration]);
22126
- useEffect(() => {
22127
- registry2?.surface()?.requestRedraw();
22128
- }, [registry2, effectiveView]);
22129
- return null;
22130
- }
22131
- function ActionsProviderIfRoot({ children }) {
22132
- const parent = useActionsRegistry();
22133
- if (parent) return /* @__PURE__ */ jsx(ActionsScope, { children });
22134
- return /* @__PURE__ */ jsx(ActionsProvider, { children });
22135
- }
22136
- function useToolActions(tools) {
22137
- const registry2 = useActionsRegistry();
22138
- useEffect(() => {
22139
- if (!registry2) return;
22140
- const unregisters = [];
22141
- for (const tool of [...Object.values(tools.registry), ...tools.ambient]) {
22142
- for (const action of tool.actions ?? []) {
22143
- unregisters.push(registry2.register(action));
22144
- }
22145
- }
22146
- return () => {
22147
- for (const u of unregisters) u();
22148
- };
22149
- }, [registry2, tools.registry, tools.ambient]);
22150
- }
22151
- function PointerProviderIfRoot({ children }) {
22152
- const parent = usePointerContext();
22153
- if (parent) return /* @__PURE__ */ jsx(Fragment, { children });
22154
- return /* @__PURE__ */ jsx(PointerContextProvider, { children });
22155
- }
22156
- function PointerPublisher({
22157
- canvasRef
22158
- }) {
22159
- const ctx = usePointerContext();
22160
- const registry2 = useOptionalViewRegistry();
22161
- useEffect(() => {
22162
- if (!ctx) return;
22163
- const el = canvasRef.current;
22164
- if (!el) return;
22165
- const publish = (pointerId, clientX, clientY) => {
22166
- const target = registry2?.resolver.at(pointerId, clientX, clientY);
22167
- const rect = el.getBoundingClientRect();
22168
- const [worldX, worldY] = clientToWorld(
22169
- clientX,
22170
- clientY,
22171
- target?.origin ?? rect,
22172
- target?.view ?? { x: 0, y: 0, scale: { x: 1, y: 1 } }
22173
- );
22174
- ctx.pointerRef.current = { worldX, worldY };
22175
- };
22176
- const onMove = (e) => publish(e.pointerId, e.clientX, e.clientY);
22177
- const onLeave = (e) => {
22178
- if (e.buttons !== 0) {
22179
- publish(e.pointerId, e.clientX, e.clientY);
22180
- return;
22181
- }
22182
- ctx.pointerRef.current = null;
22183
- };
22184
- const onDocMove = (e) => {
22185
- if (e.target === el) return;
22186
- if (e.buttons === 0) return;
22187
- publish(e.pointerId, e.clientX, e.clientY);
22188
- };
22189
- el.addEventListener("pointermove", onMove);
22190
- el.addEventListener("pointerleave", onLeave);
22191
- document.addEventListener("pointermove", onDocMove);
22192
- return () => {
22193
- el.removeEventListener("pointermove", onMove);
22194
- el.removeEventListener("pointerleave", onLeave);
22195
- document.removeEventListener("pointermove", onDocMove);
22196
- };
22197
- }, [ctx, canvasRef, registry2]);
22198
- return null;
22199
- }
22200
- function flattenVerts2(poly2) {
22201
- const flat = new Array(poly2.length * 2);
22202
- for (let i = 0; i < poly2.length; i++) {
22203
- flat[i * 2] = poly2[i].x;
22204
- flat[i * 2 + 1] = poly2[i].y;
22205
- }
22206
- return flat;
22207
- }
22208
- function polygonContainsRectCenter(poly2, rect) {
22209
- return pointInPolygon(flattenVerts2(poly2), rect.x + rect.width / 2, rect.y + rect.height / 2);
22210
- }
22211
- function polygonContainsRect(poly2, rect) {
22212
- if (poly2.length < 3) return false;
22213
- const flat = flattenVerts2(poly2);
22214
- if (!pointInPolygon(flat, rect.x, rect.y)) return false;
22215
- if (!pointInPolygon(flat, rect.x + rect.width, rect.y)) return false;
22216
- if (!pointInPolygon(flat, rect.x + rect.width, rect.y + rect.height)) return false;
22217
- if (!pointInPolygon(flat, rect.x, rect.y + rect.height)) return false;
22218
- return !anyPolyEdgeCrossesRect(poly2, rect);
22219
- }
22220
- function polygonIntersectsRect(poly2, rect) {
22221
- if (poly2.length < 3) return false;
22222
- const ab = polygonAabb(poly2);
22223
- if (ab.x + ab.width < rect.x || ab.x > rect.x + rect.width) return false;
22224
- if (ab.y + ab.height < rect.y || ab.y > rect.y + rect.height) return false;
22225
- for (const v of poly2) {
22226
- if (v.x >= rect.x && v.x <= rect.x + rect.width && v.y >= rect.y && v.y <= rect.y + rect.height) return true;
22227
- }
22228
- const flat = flattenVerts2(poly2);
22229
- if (pointInPolygon(flat, rect.x, rect.y)) return true;
22230
- if (pointInPolygon(flat, rect.x + rect.width, rect.y)) return true;
22231
- if (pointInPolygon(flat, rect.x + rect.width, rect.y + rect.height)) return true;
22232
- if (pointInPolygon(flat, rect.x, rect.y + rect.height)) return true;
22233
- return anyPolyEdgeCrossesRect(poly2, rect);
22234
- }
22235
- function polygonAabb(poly2) {
22236
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
22237
- for (const v of poly2) {
22238
- if (v.x < minX) minX = v.x;
22239
- if (v.y < minY) minY = v.y;
22240
- if (v.x > maxX) maxX = v.x;
22241
- if (v.y > maxY) maxY = v.y;
22242
- }
22243
- return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
22244
- }
22245
- function anyPolyEdgeCrossesRect(poly2, rect) {
22246
- const r0x = rect.x, r0y = rect.y;
22247
- const r1x = rect.x + rect.width, r1y = rect.y;
22248
- const r2x = rect.x + rect.width, r2y = rect.y + rect.height;
22249
- const r3x = rect.x, r3y = rect.y + rect.height;
22250
- for (let i = 0, j = poly2.length - 1; i < poly2.length; j = i++) {
22251
- const a = poly2[j], b = poly2[i];
22252
- if (segmentsCross(a.x, a.y, b.x, b.y, r0x, r0y, r1x, r1y)) return true;
22253
- if (segmentsCross(a.x, a.y, b.x, b.y, r1x, r1y, r2x, r2y)) return true;
22254
- if (segmentsCross(a.x, a.y, b.x, b.y, r2x, r2y, r3x, r3y)) return true;
22255
- if (segmentsCross(a.x, a.y, b.x, b.y, r3x, r3y, r0x, r0y)) return true;
22256
- }
22257
- return false;
22258
- }
22259
-
22260
- // src/canvas/sceneAdapter.ts
22261
- function walkClipAware(scene, poseBounds, nodeTest, poseComposition) {
22262
- return pickWalk(scenePickSource(scene, poseComposition ? { poseComposition } : {}), {
22263
- hits: (node, pose) => nodeTest(node, pose),
22264
- clipAdmits: (clip, _node, pose) => pathIntersectsRect(clip, poseBounds(pose))
22265
- });
22266
- }
22267
- var adapterFallbackIdCounter = 0;
22268
- function adapterFallbackId() {
22269
- return `n${(adapterFallbackIdCounter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
22270
- }
22271
- var pasteIdCounter = 0;
22272
- function pasteNodeId() {
22273
- return `paste-${(pasteIdCounter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
22274
- }
22275
- function copyField(value) {
22276
- if (Array.isArray(value)) return [...value];
22277
- if (value !== null && typeof value === "object") return { ...value };
22278
- return value;
22279
- }
22280
- function sceneToAdapter(scene, options = {}) {
22281
- const visibleLayers = () => {
22282
- const out = /* @__PURE__ */ new Set();
22283
- for (const layer of scene.layers) {
22284
- if (layer.visible) out.add(layer.id);
22285
- }
22286
- return out;
22287
- };
22288
- const sel = options.selection;
22289
- const getSelection = sel?.getSelection ?? sel?.get ?? (() => []);
22290
- const setSelection = sel?.setSelection ?? sel?.set ?? (() => {
22291
- });
22292
- const d = options.poseDescriptor ?? AUTO_POSE_DESCRIPTOR;
22293
- const poseBounds = (p) => visualBoundsViaDescriptor(p, d);
22294
- const translate3 = (p, dx, dy, node) => translatePoseViaDescriptor(
22295
- p,
22296
- dx,
22297
- dy,
22298
- node === void 0 ? d : poseDescriptorForNode(d, node)
22299
- );
22300
- const composition = options.poseComposition;
22301
- const composes = composition !== void 0 && composition.closure !== "identity";
22302
- if (composes && options.cascadeContainerPose) {
22303
- throw new Error(
22304
- "sceneToAdapter: poseComposition and cascadeContainerPose contradict each other. Under a composing strategy a child's pose is already relative to its parent, so the cascade would move every descendant twice. Drop cascadeContainerPose."
22305
- );
22306
- }
22307
- const adapter = {
22308
- getNode(id) {
22309
- return scene.get(asNodeId(id));
22310
- },
22311
- getNodes() {
22312
- const visible = visibleLayers();
22313
- const out = [];
22314
- for (const n of scene.renderOrderNodes()) {
22315
- if (visible.has(n.layer)) out.push(n);
22316
- }
22317
- return out;
22318
- },
22319
- getPose(id) {
22320
- const nid = asNodeId(id);
22321
- const n = scene.get(nid);
22322
- if (!n) throw new Error(`sceneToAdapter: unknown node "${id}"`);
22323
- return effectivePose(scene, n);
22324
- },
22325
- getParent(id) {
22326
- const n = scene.get(asNodeId(id));
22327
- return n?.parent ?? null;
22328
- },
22329
- getWorldPose(id) {
22330
- if (!composes) return adapter.getPose(id);
22331
- return composeWorldPose(adapter, id, composition.compose);
22332
- },
22333
- // Only present when it would do something: the render walk skips the fold
22334
- // entirely when this is absent, which is the absolute-pose path.
22335
- ...composes ? { composePose: composition.compose } : {},
22336
- setPose(id, pose) {
22337
- if (options.cascadeContainerPose) {
22338
- const node = scene.get(asNodeId(id));
22339
- if (node && node.kind === "container") {
22340
- const g = poseDescriptorForNode(d, node);
22341
- const before = g.getBounds(node.pose);
22342
- const after = g.getBounds(pose);
22343
- const dx = after.x - before.x;
22344
- const dy = after.y - before.y;
22345
- if (dx !== 0 || dy !== 0) {
22346
- const descIds = [];
22347
- const collect = (rootId) => {
22348
- for (const cid of scene.childrenOf(asNodeId(rootId))) {
22349
- descIds.push(cid);
22350
- collect(cid);
22351
- }
22352
- };
22353
- collect(id);
22354
- scene.batch("setPose", () => {
22355
- scene.setPose(asNodeId(id), pose);
22356
- for (const cid of descIds) {
22357
- const cn = scene.get(asNodeId(cid));
22358
- if (!cn) continue;
22359
- scene.setPose(asNodeId(cid), translate3(cn.pose, dx, dy, cn));
22360
- }
22361
- });
22362
- return;
22363
- }
22364
- }
22365
- }
22366
- scene.setPose(asNodeId(id), pose);
22367
- },
22368
- setParent(id, parentId) {
22369
- scene.move(asNodeId(id), parentId === null ? null : asNodeId(parentId));
22370
- },
22371
- getChildren(id) {
22372
- if (id === null) return [...scene.roots];
22373
- return [...scene.childrenOf(asNodeId(id))];
22374
- },
22375
- setChildOrder(parentId, ids) {
22376
- scene.batch("Reorder", () => {
22377
- for (let i = 0; i < ids.length; i++) {
22378
- const current = parentId === null ? scene.roots : scene.childrenOf(asNodeId(parentId));
22379
- if (current[i] === ids[i]) continue;
22380
- scene.reorder(asNodeId(ids[i]), i);
22381
- }
22382
- });
22383
- },
22384
- getLayers() {
22385
- return scene.layers.map((l) => ({ id: l.id, visible: l.visible }));
22386
- },
22387
- ...options.layouts ? {
22388
- getLayout: typeof options.layouts === "function" ? options.layouts : (id) => options.layouts[id] ?? null
22389
- } : {},
22390
- // Unified applyOps: when a label is supplied the ops are wrapped in a
22391
- // batch checkpoint (history-aware path used by Move/Resize/Rotate/Insert/
22392
- // Delete tools). When a journal is active (via scene's getActiveJournal
22393
- // option), scene.applyBatch routes to the journal instead of recording a
22394
- // new parent-history entry. Without a label the ops are applied transiently
22395
- // via applyOpsTo (AreaSelectAdapter contract — no checkpoint).
22396
- applyOps(ops, label) {
22397
- if (label !== void 0) {
22398
- scene.applyBatch(ops, label, this);
22399
- } else {
22400
- applyOpsTo(this, ops);
22401
- }
22402
- },
22403
- // Node-mutation surface used by kit InsertOps / DeleteOps (e.g.
22404
- // useNest wrapping the selection in a new container node, or its
22405
- // inverse). Re-adds via the full structural spec so the round-trip
22406
- // survives undo/redo; removes by id.
22407
- insertNode(node, index) {
22408
- scene.add({
22409
- kind: node.kind,
22410
- layer: node.layer,
22411
- pose: node.pose,
22412
- data: node.data,
22413
- id: node.id,
22414
- ...index !== void 0 ? { index } : {},
22415
- ...node.parent !== null ? { parent: node.parent } : {},
22416
- // Derivation travels with the node, or undo of a delete brings an
22417
- // edge back as a static path that never follows its endpoints again.
22418
- ...node.dependsOn !== void 0 ? { dependsOn: node.dependsOn } : {},
22419
- ...fnFieldsOfNode(node)
22420
- });
22421
- },
22422
- removeNode(id) {
22423
- scene.remove(asNodeId(id));
22424
- },
22425
- setData(id, data) {
22426
- scene.update(asNodeId(id), { data });
22427
- },
22428
- getRemovalClosure(ids) {
22429
- return [...scene.removalClosure(ids.map(asNodeId))];
22430
- },
22431
- // AreaSelectAdapter surface — included unconditionally so plain
22432
- // `useSelectTool(sceneToAdapter(scene, { selection }))` Just Works for the
22433
- // marquee gesture. `applyOps` (above) dispatches transiently when called
22434
- // without a label, matching the AreaSelectAdapter contract; `hitTestArea`
22435
- // does an AABB-vs-AABB scan over `scene.renderOrder()`, reading bounds
22436
- // through the pose descriptor.
22437
- getSelection,
22438
- setSelection,
22439
- hitTestArea(rect) {
22440
- return walkClipAware(scene, poseBounds, (_n, pose) => {
22441
- const b = poseBounds(pose);
22442
- return b.x < rect.x + rect.width && b.x + b.width > rect.x && b.y < rect.y + rect.height && b.y + b.height > rect.y;
22443
- }, composition);
22444
- },
22445
- hitTestLasso(polygon, mode) {
22446
- if (polygon.length < 3) return [];
22447
- return walkClipAware(scene, poseBounds, (_n, pose) => {
22448
- const b = poseBounds(pose);
22449
- return mode === "centers" ? polygonContainsRectCenter(polygon, b) : mode === "enclosed" ? polygonContainsRect(polygon, b) : polygonIntersectsRect(polygon, b);
22450
- }, composition);
22451
- },
22452
- // commitInsert (gesture-time leaf insert) is opt-in: present only when
22453
- // `options.commitInsert` is. The full insertNode/removeNode mutators
22454
- // above are always present so kit-side InsertOp / DeleteOp round-trip
22455
- // through scene.add / scene.remove regardless.
22456
- //
22457
- // Contract: this is a pure factory — it constructs a Node spec but does
22458
- // NOT touch the scene. `useInsert`'s onEnd dispatches an `InsertOp`
22459
- // whose apply() calls `adapter.insertNode` → `scene.add`, which is the
22460
- // sole insertion path. Calling `scene.add` here too would double-insert,
22461
- // and the second add throws on id collision (silently dropping the
22462
- // gesture's pointer-up cleanup and breaking the next drag).
22463
- ...options.commitInsert ? {
22464
- commitInsert: (bounds) => {
22465
- const created = options.commitInsert(bounds);
22466
- if (!created) return null;
22467
- const layer = options.insertLayer ?? "default";
22468
- const id = asNodeId(created.id ?? adapterFallbackId());
22469
- return {
22470
- kind: "leaf",
22471
- id,
22472
- layer,
22473
- pose: created.pose,
22474
- data: created.data,
22475
- parent: null
22476
- };
22477
- }
22478
- } : {},
22479
- // Clipboard seam (`useClipboardOps` / the paste ingestion path).
22480
- //
22481
- // `snapshotSelection` captures adapter-shaped node copies; `commitPaste`
22482
- // is a pure factory like `commitInsert` above — it mints fresh nodes but
22483
- // NEVER touches the scene. Insertion happens via the hook's InsertOps
22484
- // (one per created node, parents before children), which is what makes a
22485
- // whole paste a single undo entry.
22486
- snapshotSelection(ids) {
22487
- const idSet = new Set(ids);
22488
- const taken = /* @__PURE__ */ new Set();
22489
- const items = [];
22490
- const poseFor = (n, isRoot) => isRoot && composes ? adapter.getWorldPose(n.id) : n.pose;
22491
- const capture = (id, isRoot = false) => {
22492
- if (taken.has(id)) return;
22493
- const n = scene.get(asNodeId(id));
22494
- if (!n) return;
22495
- taken.add(id);
22496
- if (n.kind === "container") {
22497
- items.push({
22498
- kind: "container",
22499
- id: n.id,
22500
- layer: n.layer,
22501
- parent: n.parent,
22502
- pose: copyField(poseFor(n, isRoot)),
22503
- data: copyField(n.data),
22504
- children: [...n.children],
22505
- ...n.clipFromPose ? { clipFromPose: n.clipFromPose } : {}
22506
- });
22507
- for (const cid of scene.childrenOf(asNodeId(id))) capture(cid);
22508
- } else {
22509
- items.push({
22510
- kind: "leaf",
22511
- id: n.id,
22512
- layer: n.layer,
22513
- parent: n.parent,
22514
- pose: copyField(poseFor(n, isRoot)),
22515
- data: copyField(n.data)
22516
- });
22517
- }
22518
- };
22519
- for (const id of ids) {
22520
- if (scene.ancestorsOf(asNodeId(id)).some((a) => idSet.has(a))) continue;
22521
- capture(id, true);
22522
- }
22523
- return { items };
22524
- },
22525
- commitPaste(clipboard, offset, ctx) {
22526
- const src = clipboard.items;
22527
- if (src.length === 0) return [];
22528
- const idMap = /* @__PURE__ */ new Map();
22529
- for (const item of src) idMap.set(item.id, asNodeId(pasteNodeId()));
22530
- const isRoot = (item) => item.parent === null || !idMap.has(item.parent);
22531
- let dx = offset.dx;
22532
- let dy = offset.dy;
22533
- if (ctx?.dropPoint) {
22534
- let minX = Infinity;
22535
- let minY = Infinity;
22536
- for (const item of src) {
22537
- if (!isRoot(item)) continue;
22538
- const b = poseBounds(item.pose);
22539
- if (b.x < minX) minX = b.x;
22540
- if (b.y < minY) minY = b.y;
22541
- }
22542
- if (Number.isFinite(minX) && Number.isFinite(minY)) {
22543
- dx = ctx.dropPoint.worldX - minX;
22544
- dy = ctx.dropPoint.worldY - minY;
22545
- }
22546
- }
22547
- const out = [];
22548
- for (const item of src) {
22549
- const base = {
22550
- id: idMap.get(item.id),
22551
- layer: item.layer,
22552
- parent: isRoot(item) ? null : idMap.get(item.parent),
22553
- pose: translate3(copyField(item.pose), dx, dy),
22554
- data: copyField(item.data)
23221
+ if (extra) {
23222
+ const claim = extra.hit;
23223
+ return {
23224
+ kind: `layer:${extra.layerId}`,
23225
+ owner: extra.layerId,
23226
+ strength: claim.strength ?? "shared",
23227
+ ...claim.claimedKinds !== void 0 ? { claimedKinds: claim.claimedKinds } : {},
23228
+ ...claim.cursor !== void 0 ? { cursor: claim.cursor } : {},
23229
+ ...claim.initialScratch !== void 0 ? { payload: claim.initialScratch } : {}
22555
23230
  };
22556
- if (item.kind === "container") {
22557
- out.push({
22558
- ...base,
22559
- kind: "container",
22560
- children: item.children.filter((c) => idMap.has(c)).map((c) => idMap.get(c)),
22561
- ...item.clipFromPose ? { clipFromPose: item.clipFromPose } : {}
22562
- });
22563
- } else {
22564
- out.push({ ...base, kind: "leaf" });
22565
- }
22566
23231
  }
22567
- return out;
22568
- },
22569
- // Constant cascade: the hook re-snapshots the just-created items, so
22570
- // repeated pastes stack +12/+12 each time.
22571
- getPasteOffset() {
22572
- return { dx: 12, dy: 12 };
23232
+ return inner(world);
23233
+ };
23234
+ }, [getAnchorState, rectNow, registry2, targetScale, camera, drawnLayers]);
23235
+ const classifyTarget = useMemo(() => {
23236
+ const inner = buildClassifyTarget(
23237
+ () => live2.current.selection.get(),
23238
+ (wx, wy) => {
23239
+ const i = inputsRef.current;
23240
+ const frame = { ...camera(), layerIsPainted };
23241
+ if (i?.pickBest) return i.pickBest(wx, wy, frame);
23242
+ const ids = i?.pickEvery?.(wx, wy, frame) ?? [];
23243
+ return ids.length > 0 ? ids[ids.length - 1] : null;
23244
+ },
23245
+ (id2) => inputsRef.current?.kindOfNode?.(id2)
23246
+ );
23247
+ return (world) => inner(world);
23248
+ }, [camera, layerIsPainted]);
23249
+ const registration = useMemo(() => ({
23250
+ id,
23251
+ order,
23252
+ interactive,
23253
+ paint,
23254
+ layer: createViewportLayer({
23255
+ id,
23256
+ label: label ?? id,
23257
+ source: drawnLayers,
23258
+ view: cameraAt,
23259
+ bounds: (outer, dims) => rectAt(outer, dims),
23260
+ // The surface half of the envelope passes through; the view half is
23261
+ // this view's, so its layers paint its chrome rather than the
23262
+ // surface's.
23263
+ data: (outer) => ({ ...outer, ...helpersRef.current }),
23264
+ ...background !== void 0 ? { background } : {}
23265
+ }),
23266
+ layerIsPainted,
23267
+ target: {
23268
+ dispatcher: dispatcherRef.current,
23269
+ affordanceAt,
23270
+ classifyTarget,
23271
+ clientToWorld: clientToWorldHere,
23272
+ deps: () => ({
23273
+ view: viewApi,
23274
+ ingestion: ingestionApi,
23275
+ // An Escape here cancels what is in flight here.
23276
+ dispatcher: { cancelAll: (reason) => dispatcherRef.current.cancelAll(reason) },
23277
+ // Only a view with its own selection overlays that one; otherwise the
23278
+ // surface's answer stands, wrappers (`selectionMode`) included.
23279
+ ...live2.current.viewSelection ? { selection: live2.current.viewSelection } : {}
23280
+ }),
23281
+ // Action eligibility is chrome-caps evaluated against this view, so a
23282
+ // rule that hides an action in one panel cannot decline a gesture in
23283
+ // another.
23284
+ getRuleCtx: () => inputsRef.current?.chromeCaps?.ruleCtx(ruleInputs())
22573
23285
  }
22574
- };
22575
- return adapter;
23286
+ }), [
23287
+ id,
23288
+ order,
23289
+ interactive,
23290
+ paint,
23291
+ label,
23292
+ background,
23293
+ rectAt,
23294
+ cameraAt,
23295
+ viewApi,
23296
+ affordanceAt,
23297
+ classifyTarget,
23298
+ clientToWorldHere,
23299
+ ruleInputs,
23300
+ ingestionApi,
23301
+ drawnLayers,
23302
+ layerIsPainted
23303
+ ]);
23304
+ useEffect(() => {
23305
+ if (!registry2) return;
23306
+ return registry2.register(registration);
23307
+ }, [registry2, registration]);
23308
+ useEffect(() => {
23309
+ registry2?.surface()?.requestRedraw();
23310
+ }, [registry2, effectiveView]);
23311
+ return null;
22576
23312
  }
22577
- function useSceneAdapter(scene, options = {}) {
22578
- const { selection, commitInsert, insertLayer, layouts, poseDescriptor, cascadeContainerPose } = options;
22579
- return useMemo(
22580
- () => sceneToAdapter(scene, { selection, commitInsert, insertLayer, layouts, poseDescriptor, cascadeContainerPose }),
22581
- [scene, selection, commitInsert, insertLayer, layouts, poseDescriptor, cascadeContainerPose]
22582
- );
23313
+ function ActionsProviderIfRoot({ children }) {
23314
+ const parent = useActionsRegistry();
23315
+ if (parent) return /* @__PURE__ */ jsx(ActionsScope, { children });
23316
+ return /* @__PURE__ */ jsx(ActionsProvider, { children });
23317
+ }
23318
+ function useToolActions(tools) {
23319
+ const registry2 = useActionsRegistry();
23320
+ useEffect(() => {
23321
+ if (!registry2) return;
23322
+ const unregisters = [];
23323
+ for (const tool of [...Object.values(tools.registry), ...tools.ambient]) {
23324
+ for (const action of tool.actions ?? []) {
23325
+ unregisters.push(registry2.register(action));
23326
+ }
23327
+ }
23328
+ return () => {
23329
+ for (const u of unregisters) u();
23330
+ };
23331
+ }, [registry2, tools.registry, tools.ambient]);
23332
+ }
23333
+ function PointerProviderIfRoot({ children }) {
23334
+ const parent = usePointerContext();
23335
+ if (parent) return /* @__PURE__ */ jsx(Fragment, { children });
23336
+ return /* @__PURE__ */ jsx(PointerContextProvider, { children });
23337
+ }
23338
+ function PointerPublisher({
23339
+ canvasRef
23340
+ }) {
23341
+ const ctx = usePointerContext();
23342
+ const registry2 = useOptionalViewRegistry();
23343
+ useEffect(() => {
23344
+ if (!ctx) return;
23345
+ const el = canvasRef.current;
23346
+ if (!el) return;
23347
+ const publish = (pointerId, clientX, clientY) => {
23348
+ const target = registry2?.resolver.at(pointerId, clientX, clientY);
23349
+ const rect = el.getBoundingClientRect();
23350
+ const [worldX, worldY] = clientToWorld(
23351
+ clientX,
23352
+ clientY,
23353
+ target?.origin ?? rect,
23354
+ target?.view ?? { x: 0, y: 0, scale: { x: 1, y: 1 } }
23355
+ );
23356
+ ctx.pointerRef.current = { worldX, worldY };
23357
+ };
23358
+ const onMove = (e) => publish(e.pointerId, e.clientX, e.clientY);
23359
+ const onLeave = (e) => {
23360
+ if (e.buttons !== 0) {
23361
+ publish(e.pointerId, e.clientX, e.clientY);
23362
+ return;
23363
+ }
23364
+ ctx.pointerRef.current = null;
23365
+ };
23366
+ const onDocMove = (e) => {
23367
+ if (e.target === el) return;
23368
+ if (e.buttons === 0) return;
23369
+ publish(e.pointerId, e.clientX, e.clientY);
23370
+ };
23371
+ el.addEventListener("pointermove", onMove);
23372
+ el.addEventListener("pointerleave", onLeave);
23373
+ document.addEventListener("pointermove", onDocMove);
23374
+ return () => {
23375
+ el.removeEventListener("pointermove", onMove);
23376
+ el.removeEventListener("pointerleave", onLeave);
23377
+ document.removeEventListener("pointermove", onDocMove);
23378
+ };
23379
+ }, [ctx, canvasRef, registry2]);
23380
+ return null;
22583
23381
  }
22584
23382
  function aabbOfPose(pose, descriptor = AUTO_POSE_DESCRIPTOR) {
22585
23383
  return descriptor.getBounds(pose);
@@ -22612,18 +23410,7 @@ function rotationAboutCenter(pose, descriptor) {
22612
23410
  // src/canvas/SceneCanvas/useSceneSelectTool.ts
22613
23411
  var DEFAULT_PICK_TOLERANCE_PX = 4;
22614
23412
  function useSceneSelectTool(args) {
22615
- const {
22616
- scene,
22617
- selection,
22618
- geometry,
22619
- selectTool: opts,
22620
- insertTool,
22621
- layouts,
22622
- poseComposition,
22623
- getView,
22624
- alphaOf,
22625
- layerIsPainted
22626
- } = args;
23413
+ const { scene, adapter, geometry, selectTool: opts, getView, alphaOf, layerIsPainted } = args;
22627
23414
  const d = args.poseDescriptor ?? AUTO_POSE_DESCRIPTOR;
22628
23415
  const pickEveryProp = geometry?.pickEvery;
22629
23416
  const boundsOfProp = geometry?.boundsOf;
@@ -22631,62 +23418,6 @@ function useSceneSelectTool(args) {
22631
23418
  const pickTolerancePx = geometry?.pickTolerancePx ?? DEFAULT_PICK_TOLERANCE_PX;
22632
23419
  const moveOptions = opts?.move;
22633
23420
  const snap2 = opts?.snap;
22634
- const commitInsert = insertTool?.create;
22635
- const insertLayer = insertTool?.layer;
22636
- const adapter = useMemo(() => {
22637
- const base = sceneToAdapter(scene, {
22638
- commitInsert,
22639
- insertLayer,
22640
- layouts,
22641
- poseDescriptor: d,
22642
- ...poseComposition ? { poseComposition } : {}
22643
- });
22644
- const framed = poseComposition !== void 0 && poseComposition.closure !== "identity";
22645
- const collectDescendants = (id, out) => {
22646
- for (const cid of scene.childrenOf(asNodeId(id))) {
22647
- out.push(cid);
22648
- collectDescendants(cid, out);
22649
- }
22650
- };
22651
- return {
22652
- ...base,
22653
- setPose(id, pose) {
22654
- const n = scene.get(asNodeId(id));
22655
- if (framed || !n || n.kind !== "container") {
22656
- base.setPose(id, pose);
22657
- return;
22658
- }
22659
- const g = poseDescriptorForNode(d, n);
22660
- const prev = g.getBounds(n.pose);
22661
- const next = g.getBounds(pose);
22662
- const dx = next.x - prev.x;
22663
- const dy = next.y - prev.y;
22664
- if (dx === 0 && dy === 0) {
22665
- base.setPose(id, pose);
22666
- return;
22667
- }
22668
- const desc = [];
22669
- collectDescendants(id, desc);
22670
- scene.batch("move container", () => {
22671
- base.setPose(id, pose);
22672
- for (const cid of desc) {
22673
- const cn = scene.get(asNodeId(cid));
22674
- if (!cn) continue;
22675
- base.setPose(
22676
- cid,
22677
- translatePoseViaDescriptor(cn.pose, dx, dy, poseDescriptorForNode(d, cn))
22678
- );
22679
- }
22680
- });
22681
- },
22682
- // Selection methods, widened to `string[]` so the synthesized adapter
22683
- // satisfies `AreaSelectAdapter` (which keeps the kit-internal adapter
22684
- // contract on plain `string[]`; the brand lives on the public selection
22685
- // surface, not on adapter shapes).
22686
- getSelection: () => [...selection.adapterMethods.getSelection()],
22687
- setSelection: (ids) => selection.adapterMethods.setSelection(ids)
22688
- };
22689
- }, [scene, commitInsert, insertLayer, layouts, selection, d, poseComposition]);
22690
23421
  const wiredMoveOptions = useMemo(() => {
22691
23422
  const merged = { ...moveOptions ?? {} };
22692
23423
  if (snap2) {
@@ -22761,7 +23492,6 @@ function useSceneSelectTool(args) {
22761
23492
  };
22762
23493
  }, [wiredHitBody, adapter]);
22763
23494
  return {
22764
- adapter,
22765
23495
  selectTool,
22766
23496
  pickEvery: wiredHitBody,
22767
23497
  pickBest: wiredPickBest,
@@ -23218,60 +23948,21 @@ var CLOSE_HINT_RADIUS_PX = 8;
23218
23948
  function w2s(wx, wy, view) {
23219
23949
  return [(wx - view.x) * view.scale.x, (wy - view.y) * view.scale.y];
23220
23950
  }
23221
- function mirror(anchor, out) {
23222
- if (!out || anchor.altBroken) return void 0;
23223
- return { x: 2 * anchor.x - out.x, y: 2 * anchor.y - out.y };
23224
- }
23225
23951
  function approximateCircle(cx, cy, r) {
23226
23952
  return circlePath(cx, cy, r);
23227
23953
  }
23228
23954
  function subpathToPath(sp, view) {
23229
23955
  if (sp.anchors.length === 0) return null;
23230
- const cmds = [];
23231
- const xs = [];
23232
- const [sx, sy] = w2s(sp.anchors[0].x, sp.anchors[0].y, view);
23233
- cmds.push(PATH_M);
23234
- xs.push(sx, sy);
23235
- for (let i = 1; i < sp.anchors.length; i++) {
23236
- const prev = sp.anchors[i - 1];
23237
- const curr = sp.anchors[i];
23238
- const out = prev.outHandle;
23239
- const inH = curr.inHandle ?? mirror(prev, out);
23240
- const [tx, ty] = w2s(curr.x, curr.y, view);
23241
- if (out || curr.inHandle) {
23242
- const c1 = out ?? prev;
23243
- const c2 = inH ?? curr;
23244
- const [c1x, c1y] = w2s(c1.x, c1.y, view);
23245
- const [c2x, c2y] = w2s(c2.x, c2.y, view);
23246
- cmds.push(PATH_C);
23247
- xs.push(c1x, c1y, c2x, c2y, tx, ty);
23248
- } else {
23249
- cmds.push(PATH_L);
23250
- xs.push(tx, ty);
23251
- }
23252
- }
23253
- if (sp.closed) {
23254
- const last = sp.anchors[sp.anchors.length - 1];
23255
- const first = sp.anchors[0];
23256
- const out = last.outHandle;
23257
- const inH = first.inHandle ?? mirror(last, out);
23258
- if (out || first.inHandle) {
23259
- const c1 = out ?? last;
23260
- const c2 = inH ?? first;
23261
- const [c1x, c1y] = w2s(c1.x, c1.y, view);
23262
- const [c2x, c2y] = w2s(c2.x, c2.y, view);
23263
- const [tx, ty] = w2s(first.x, first.y, view);
23264
- cmds.push(PATH_C);
23265
- xs.push(c1x, c1y, c2x, c2y, tx, ty);
23266
- }
23267
- cmds.push(PATH_Z);
23268
- }
23269
- return {
23270
- kind: "polygon",
23271
- commands: new Uint8Array(cmds),
23272
- coords: new Float32Array(xs),
23273
- fillRule: "nonzero"
23956
+ const pt = (p) => {
23957
+ const [x, y] = w2s(p.x, p.y, view);
23958
+ return { x, y };
23274
23959
  };
23960
+ const anchors = sp.anchors.map((a) => ({
23961
+ ...pt(a),
23962
+ ...a.inHandle ? { inHandle: pt(a.inHandle) } : {},
23963
+ ...a.outHandle ? { outHandle: pt(a.outHandle) } : {}
23964
+ }));
23965
+ return anchorsToPath([anchors], [sp.closed]);
23275
23966
  }
23276
23967
  function createPenPreviewLayer(opts) {
23277
23968
  const style = { ...DEFAULT_STYLE2, ...opts.style ?? {} };
@@ -23360,10 +24051,10 @@ function createPenPreviewLayer(opts) {
23360
24051
  path: approximateCircle(hx, hy, HANDLE_DOT_RADIUS_PX),
23361
24052
  fill: { fill: "solid", color: style.handleStroke }
23362
24053
  });
23363
- const inSide = mirror(a, a.outHandle);
24054
+ const inSide = a.inHandle;
23364
24055
  if (inSide) {
23365
24056
  const [mx, my] = w2s(inSide.x, inSide.y, view);
23366
- const mirrorLine = {
24057
+ const inLine = {
23367
24058
  kind: "polygon",
23368
24059
  commands: new Uint8Array([PATH_M, PATH_L]),
23369
24060
  coords: new Float32Array([ax, ay, mx, my]),
@@ -23371,7 +24062,7 @@ function createPenPreviewLayer(opts) {
23371
24062
  };
23372
24063
  out.push({
23373
24064
  kind: "path",
23374
- path: mirrorLine,
24065
+ path: inLine,
23375
24066
  stroke: { paint: { fill: "solid", color: style.handleStroke }, width: 1 }
23376
24067
  });
23377
24068
  out.push({
@@ -23587,7 +24278,7 @@ function useBuiltinShapeTools(args) {
23587
24278
  const { scene, adapter, options } = args;
23588
24279
  const seqRef = useRef(0);
23589
24280
  const freshId = (prefix) => asNodeId(`${prefix}-${++seqRef.current}`);
23590
- const nextFill = () => DEFAULT_PALETTE[seqRef.current % DEFAULT_PALETTE.length];
24281
+ const nextFill = () => cycleFill(seqRef.current);
23591
24282
  const layerOf = () => scene.layers[0]?.id ?? "default";
23592
24283
  const makeLeaf = (id, pose, data) => ({
23593
24284
  id,
@@ -23604,6 +24295,15 @@ function useBuiltinShapeTools(args) {
23604
24295
  const polygon = usePolygonTool();
23605
24296
  const star = useStarTool();
23606
24297
  const pencil = usePencilTool();
24298
+ const makePenLeaf = (carrier) => {
24299
+ const id = freshId("pn");
24300
+ const color = nextFill();
24301
+ return makeLeaf(id, carrier.bounds, {
24302
+ path: carrier.path,
24303
+ fill: carrier.closed ? solid(nextFill()) : null,
24304
+ stroke: strokeOf(color, 2)
24305
+ });
24306
+ };
23607
24307
  const pen = usePenTool({
23608
24308
  snapPoint: snapPoint2,
23609
24309
  wrapPath: (path, { closed }) => {
@@ -23612,16 +24312,11 @@ function useBuiltinShapeTools(args) {
23612
24312
  },
23613
24313
  adapter: {
23614
24314
  addNode: (carrier) => {
23615
- const id = freshId("pn");
23616
- const color = nextFill();
23617
- const node = makeLeaf(id, carrier.bounds, {
23618
- path: carrier.path,
23619
- fill: carrier.closed ? solid(nextFill()) : null,
23620
- stroke: strokeOf(color, 2)
23621
- });
24315
+ const node = makePenLeaf(carrier);
23622
24316
  adapter.insertNode(node);
23623
- return String(id);
24317
+ return String(node.id);
23624
24318
  },
24319
+ makeNode: (carrier) => makePenLeaf(carrier),
23625
24320
  setSelection: (ids) => adapter.setSelection(ids)
23626
24321
  }
23627
24322
  });
@@ -23891,6 +24586,19 @@ function useEditAnchorsDepSource(scene, selection, adapter, externalState, optio
23891
24586
  }
23892
24587
  const selectedAnchors = effectiveId ? ext?.getSelectedAnchors() ?? EMPTY_ANCHOR_SELECTION : EMPTY_ANCHOR_SELECTION;
23893
24588
  const marquee = effectiveId ? ext?.getMarquee() ?? null : null;
24589
+ const editOps = (id, worldPath, label) => {
24590
+ const wp = worldPath;
24591
+ const node = sc.get(id);
24592
+ if (!node) return [];
24593
+ const storage = classifyStorage(node);
24594
+ if (!storage) return [];
24595
+ if (storage.kind === "pose") return [createTransformOp({ id, from: node.pose, to: wp, label })];
24596
+ const { pose: newPose, path: aligned } = worldEditToStorage(storage.pose, wp);
24597
+ return [
24598
+ createTransformOp({ id, from: node.pose, to: newPose, label }),
24599
+ createSetDataOp({ id, from: node.data, to: { ...node.data, path: aligned }, label })
24600
+ ];
24601
+ };
23894
24602
  return {
23895
24603
  editingId: effectiveId,
23896
24604
  setEditingId(id) {
@@ -23933,24 +24641,10 @@ function useEditAnchorsDepSource(scene, selection, adapter, externalState, optio
23933
24641
  return { pose: node.pose, data: node.data };
23934
24642
  },
23935
24643
  applyEdit(id, worldPath, label) {
23936
- const wp = worldPath;
23937
- const node = sc.get(id);
23938
- if (!node) return;
23939
- const storage = classifyStorage(node);
23940
- if (!storage) return;
23941
- if (storage.kind === "pose") {
23942
- ad.applyOps([createTransformOp({ id, from: node.pose, to: wp, label })], label);
23943
- return;
23944
- }
23945
- const { pose: newPose, path: aligned } = worldEditToStorage(storage.pose, wp);
23946
- ad.applyOps(
23947
- [
23948
- createTransformOp({ id, from: node.pose, to: newPose, label }),
23949
- createSetDataOp({ id, from: node.data, to: { ...node.data, path: aligned }, label })
23950
- ],
23951
- label
23952
- );
23953
- }
24644
+ const ops = editOps(id, worldPath, label);
24645
+ if (ops.length > 0) ad.applyOps(ops, label);
24646
+ },
24647
+ editOps
23954
24648
  };
23955
24649
  });
23956
24650
  }
@@ -24143,7 +24837,7 @@ function useInsertDepSource(scene, adapter, factories) {
24143
24837
  seq = ++insertSeqRef.current;
24144
24838
  id = asNodeId(`kit-${kind}-${seq}`);
24145
24839
  }
24146
- const color = DEFAULT_PALETTE[seq % DEFAULT_PALETTE.length];
24840
+ const color = cycleFill(seq);
24147
24841
  const fill = solid(color);
24148
24842
  const layer = sc.layers[0]?.id ?? "default";
24149
24843
  const extent = insertPreviewExtent({ shape: kind, bounds, extras });
@@ -24239,15 +24933,16 @@ function useDispatcherDepSource(dispatcher) {
24239
24933
  cancelAll: (reason) => ref.current.cancelAll(reason)
24240
24934
  }));
24241
24935
  }
24242
- function useLayoutDepSource(layouts) {
24243
- const ref = useRef(layouts);
24244
- ref.current = layouts;
24936
+ function useLayoutDepSource(layouts, dropTarget) {
24937
+ const ref = useRef({ layouts, dropTarget });
24938
+ ref.current = { layouts, dropTarget };
24245
24939
  useDepSource("layout", () => ({
24246
24940
  getLayout: (containerId) => {
24247
- const l = ref.current;
24941
+ const l = ref.current.layouts;
24248
24942
  if (!l) return null;
24249
24943
  return typeof l === "function" ? l(containerId) : l[containerId] ?? null;
24250
- }
24944
+ },
24945
+ dropTarget: ref.current.dropTarget
24251
24946
  }));
24252
24947
  }
24253
24948
  function useGeometryProjection(projection) {
@@ -24348,8 +25043,8 @@ var kitImageHandler = {
24348
25043
 
24349
25044
  // src/features/ingestion/svgHandler.ts
24350
25045
  var SVG_MIME = "image/svg+xml";
24351
- var FALLBACK_WIDTH = 300;
24352
- var FALLBACK_HEIGHT = 150;
25046
+ var FALLBACK_WIDTH2 = 300;
25047
+ var FALLBACK_HEIGHT2 = 150;
24353
25048
  function isSvgFileItem(item) {
24354
25049
  if (item.kind !== "file") return false;
24355
25050
  if (item.mime === SVG_MIME) return true;
@@ -24387,8 +25082,8 @@ var defaultMeasure2 = async (file) => {
24387
25082
  img.src = url;
24388
25083
  await img.decode();
24389
25084
  return {
24390
- width: img.naturalWidth || FALLBACK_WIDTH,
24391
- height: img.naturalHeight || FALLBACK_HEIGHT
25085
+ width: img.naturalWidth || FALLBACK_WIDTH2,
25086
+ height: img.naturalHeight || FALLBACK_HEIGHT2
24392
25087
  };
24393
25088
  } finally {
24394
25089
  URL.revokeObjectURL(url);
@@ -24932,6 +25627,7 @@ function SceneCanvasInner(props, ref) {
24932
25627
  insertNodeFactories,
24933
25628
  ingestion,
24934
25629
  layouts,
25630
+ layoutDropTarget,
24935
25631
  poseComposition,
24936
25632
  geometryProjection,
24937
25633
  routing,
@@ -25063,6 +25759,12 @@ function SceneCanvasInner(props, ref) {
25063
25759
  }, [scene, selectionOptions, selectionMode]);
25064
25760
  const internalSelection = useSelection(derivedSelectionOptions);
25065
25761
  const baseSelection = selectionProp ?? internalSelection;
25762
+ const baseAdapterMethods = baseSelection.adapterMethods;
25763
+ const readOnlyAdapterMethods = useMemo(() => ({
25764
+ getSelection: () => baseAdapterMethods.getSelection(),
25765
+ setSelection: () => {
25766
+ }
25767
+ }), [baseAdapterMethods]);
25066
25768
  const selection = useMemo(() => {
25067
25769
  if (selectionMode !== "none") return baseSelection;
25068
25770
  const noopSet = () => {
@@ -25074,9 +25776,10 @@ function SceneCanvasInner(props, ref) {
25074
25776
  remove: noopSet,
25075
25777
  toggle: noopSet,
25076
25778
  clear: noopSet,
25077
- applyClick: noopSet
25779
+ applyClick: noopSet,
25780
+ adapterMethods: readOnlyAdapterMethods
25078
25781
  };
25079
- }, [baseSelection, selectionMode]);
25782
+ }, [baseSelection, selectionMode, readOnlyAdapterMethods]);
25080
25783
  const currentSelection = selection.current;
25081
25784
  const selectionKinds = useMemo(() => {
25082
25785
  if (currentSelection.length === 0) return void 0;
@@ -25168,9 +25871,20 @@ function SceneCanvasInner(props, ref) {
25168
25871
  const [sceneLayerGate] = useState(createSceneLayerGate);
25169
25872
  const layerIsPainted = useCallback((layerId) => sceneLayerGate(surfaceViewRegistry?.surface()?.layers() ?? [], layerPaintRef.current)(layerId), [surfaceViewRegistry, sceneLayerGate]);
25170
25873
  const viewLayerGate = layerVisibility !== void 0 || layerOrder !== void 0 ? layerIsPainted : void 0;
25171
- const { adapter, selectTool: internalSelect, pickEvery: internalPickEvery, pickBest: internalPickBest, boundsOf: internalBoundsOf } = useSceneSelectTool({
25874
+ const framed = poseComposition !== void 0 && poseComposition.closure !== "identity";
25875
+ const adapter = useSceneAdapter(scene, {
25876
+ // `adapterMethods` is memoized; `selection` itself is a fresh object each render.
25877
+ selection: selection.adapterMethods,
25878
+ poseDescriptor: descriptor,
25879
+ commitInsert: insertTool?.create,
25880
+ insertLayer: insertTool?.layer,
25881
+ layouts,
25882
+ poseComposition,
25883
+ cascadeContainerPose: !framed
25884
+ });
25885
+ const { selectTool: internalSelect, pickEvery: internalPickEvery, pickBest: internalPickBest, boundsOf: internalBoundsOf } = useSceneSelectTool({
25172
25886
  scene,
25173
- selection,
25887
+ adapter,
25174
25888
  poseDescriptor: descriptor,
25175
25889
  geometry,
25176
25890
  // The pick tolerance is declared in screen pixels; this is what converts it.
@@ -25180,10 +25894,7 @@ function SceneCanvasInner(props, ref) {
25180
25894
  // that once per walk instead of once per candidate.
25181
25895
  ...alphaFor ? { alphaOf: composedAlphaFor } : {},
25182
25896
  ...viewLayerGate ? { layerIsPainted: viewLayerGate } : {},
25183
- selectTool: selectToolWithDefaults,
25184
- ...insertTool ? { insertTool } : {},
25185
- ...layouts ? { layouts } : {},
25186
- ...poseComposition ? { poseComposition } : {}
25897
+ selectTool: selectToolWithDefaults
25187
25898
  });
25188
25899
  const getNodeAtPoint = useMemo(() => {
25189
25900
  if (!internalPickEvery) return void 0;
@@ -25695,6 +26406,7 @@ function SceneCanvasInner(props, ref) {
25695
26406
  editAnchorsExternalState,
25696
26407
  anchorEditingAllowed,
25697
26408
  layouts,
26409
+ layoutDropTarget,
25698
26410
  poseComposition,
25699
26411
  insertNodeFactories,
25700
26412
  snapPoint: toolOptions?.snapPoint,
@@ -25945,6 +26657,7 @@ function StandardActionsRegistrar({
25945
26657
  anchorEditingAllowed,
25946
26658
  poseComposition,
25947
26659
  layouts,
26660
+ layoutDropTarget,
25948
26661
  insertNodeFactories,
25949
26662
  snapPoint: snapPoint2,
25950
26663
  canvasRef,
@@ -25993,7 +26706,7 @@ function StandardActionsRegistrar({
25993
26706
  useViewportActions({ pan: viewportPanEnabled, zoom: viewportZoom, pinchZoom: viewportPinchZoom });
25994
26707
  useAreaSelectDepSource(scene, selection, poseDescriptor, poseComposition, alphaOf);
25995
26708
  useNodeAtPointDepSource(pickEvery);
25996
- useLayoutDepSource(layouts);
26709
+ useLayoutDepSource(layouts, layoutDropTarget);
25997
26710
  useInsertDepSource(scene, adapter, insertNodeFactories);
25998
26711
  useSnapDepSource(snapPoint2);
25999
26712
  useIngestionDepSource(canvasRef, () => currentViewRef.current, ingestionResolveSrc, ingestionSvg, ingestionClipboard);
@@ -26186,7 +26899,8 @@ function shapeSchema(opts = {}) {
26186
26899
  letterSpacing: { kind: "number", name: "Tracking", description: "Extra advance per glyph, world units.", default: 0, step: 0.1 },
26187
26900
  underline: { kind: "boolean", name: "Underline", description: "Underline the text.", default: false, control: "toggle", short: "U", pair: "Decoration" },
26188
26901
  strikethrough: { kind: "boolean", name: "Strikethrough", description: "Strike through the text.", default: false, control: "toggle", short: "S", pair: "Decoration" },
26189
- overline: { kind: "boolean", name: "Overline", description: "Rule the text above its ascent.", default: false, control: "toggle", short: "O", pair: "Decoration" }
26902
+ overline: { kind: "boolean", name: "Overline", description: "Rule the text above its ascent.", default: false, control: "toggle", short: "O", pair: "Decoration" },
26903
+ textTransform: { kind: "enum", name: "Case", description: "Draw the text in capitals, lowercase or title case. The text itself is unchanged.", default: "none", control: "toggle", options: [{ value: "none", label: "None", short: "\u2013" }, { value: "uppercase", label: "Uppercase", short: "AA" }, { value: "lowercase", label: "Lowercase", short: "aa" }, { value: "capitalize", label: "Capitalize", short: "Aa" }] }
26190
26904
  }
26191
26905
  },
26192
26906
  paragraph: {
@@ -27213,6 +27927,20 @@ function createTextLayer(opts) {
27213
27927
  }
27214
27928
  };
27215
27929
  }
27930
+ function sourceClusters(cells) {
27931
+ const out = [];
27932
+ for (const c of cells) {
27933
+ const prev = out[out.length - 1];
27934
+ if (prev && prev.srcIndex === c.srcIndex && prev.srcEnd === c.srcEnd) {
27935
+ const x0 = Math.min(prev.x, c.x);
27936
+ const x1 = Math.max(prev.x + prev.advance, c.x + c.advance);
27937
+ out[out.length - 1] = { ...prev, x: x0, advance: x1 - x0 };
27938
+ } else {
27939
+ out.push(c);
27940
+ }
27941
+ }
27942
+ return out;
27943
+ }
27216
27944
  function pointInTextPose(x, y, pose, opts = {}) {
27217
27945
  const p = opts.padding ?? 0;
27218
27946
  return x >= pose.x - p && x <= pose.x + pose.width + p && y >= pose.y - p && y <= pose.y + pose.height + p;
@@ -27231,8 +27959,8 @@ function caretIndexAt(x, y, pose) {
27231
27959
  break;
27232
27960
  }
27233
27961
  }
27234
- const { cells } = line;
27235
- if (cells.length === 0) return line.srcEnd;
27962
+ if (line.cells.length === 0) return line.srcEnd;
27963
+ const cells = sourceClusters(line.cells);
27236
27964
  const visual = cells.map((_, i) => i).sort((a, b) => cells[a].x - cells[b].x);
27237
27965
  for (const i of visual) {
27238
27966
  const c = cells[i];
@@ -27265,6 +27993,7 @@ function solidColor(p) {
27265
27993
  if ("color" in p) return p.color;
27266
27994
  return null;
27267
27995
  }
27996
+ var TRANSFORMS = /* @__PURE__ */ new Set(["none", "uppercase", "lowercase", "capitalize"]);
27268
27997
  var EMPTY_STYLE = {
27269
27998
  bold: false,
27270
27999
  italic: false,
@@ -27320,6 +28049,8 @@ function styleStateFromElement(el, parent) {
27320
28049
  const shift = parseFloat(rawShift);
27321
28050
  if (Number.isFinite(shift)) next.baselineShift = shift;
27322
28051
  }
28052
+ const tt = el.style.textTransform;
28053
+ if (TRANSFORMS.has(tt)) next.textTransform = tt;
27323
28054
  if (el.style.fontFamily) next.fontFamily = el.style.fontFamily;
27324
28055
  if (el.style.color) next.color = el.style.color;
27325
28056
  const ls = el.style.letterSpacing;
@@ -27333,23 +28064,24 @@ function styleStateFromElement(el, parent) {
27333
28064
  return next;
27334
28065
  }
27335
28066
  function styleEquals(a, b) {
27336
- return a.bold === b.bold && a.italic === b.italic && a.underline === b.underline && a.strikethrough === b.strikethrough && a.overline === b.overline && a.fontSize === b.fontSize && a.fontFamily === b.fontFamily && a.color === b.color && a.letterSpacing === b.letterSpacing && a.script === b.script && a.baselineShift === b.baselineShift && a.fontScale === b.fontScale;
28067
+ return a.bold === b.bold && a.italic === b.italic && a.underline === b.underline && a.strikethrough === b.strikethrough && a.overline === b.overline && a.fontSize === b.fontSize && a.fontFamily === b.fontFamily && a.color === b.color && a.letterSpacing === b.letterSpacing && a.script === b.script && a.baselineShift === b.baselineShift && a.fontScale === b.fontScale && a.textTransform === b.textTransform;
27337
28068
  }
27338
28069
  function toRun(text, style) {
27339
- const run = { text };
27340
- if (style.bold) run.bold = true;
27341
- if (style.italic) run.italic = true;
27342
- if (style.fontSize != null) run.fontSize = style.fontSize;
27343
- if (style.fontFamily != null) run.fontFamily = style.fontFamily;
27344
- if (style.color != null) run.fill = { fill: "solid", color: style.color };
27345
- if (style.letterSpacing != null) run.letterSpacing = style.letterSpacing;
27346
- if (style.underline) run.underline = true;
27347
- if (style.strikethrough) run.strikethrough = true;
27348
- if (style.overline) run.overline = true;
27349
- if (style.script != null) run.script = style.script;
27350
- if (style.baselineShift != null) run.baselineShift = style.baselineShift;
27351
- if (style.fontScale != null) run.fontScale = style.fontScale;
27352
- return run;
28070
+ const run2 = { text };
28071
+ if (style.bold) run2.bold = true;
28072
+ if (style.italic) run2.italic = true;
28073
+ if (style.fontSize != null) run2.fontSize = style.fontSize;
28074
+ if (style.fontFamily != null) run2.fontFamily = style.fontFamily;
28075
+ if (style.color != null) run2.fill = { fill: "solid", color: style.color };
28076
+ if (style.letterSpacing != null) run2.letterSpacing = style.letterSpacing;
28077
+ if (style.underline) run2.underline = true;
28078
+ if (style.strikethrough) run2.strikethrough = true;
28079
+ if (style.overline) run2.overline = true;
28080
+ if (style.script != null) run2.script = style.script;
28081
+ if (style.baselineShift != null) run2.baselineShift = style.baselineShift;
28082
+ if (style.fontScale != null) run2.fontScale = style.fontScale;
28083
+ if (style.textTransform != null) run2.textTransform = style.textTransform;
28084
+ return run2;
27353
28085
  }
27354
28086
  function domToRuns(parent) {
27355
28087
  const fragments = [];
@@ -27391,27 +28123,28 @@ function domToRuns(parent) {
27391
28123
  }
27392
28124
  function runsToDom(runs, parent) {
27393
28125
  parent.replaceChildren();
27394
- for (const run of runs) {
28126
+ for (const run2 of runs) {
27395
28127
  const span = document.createElement("span");
27396
28128
  span.setAttribute("data-run", "");
27397
- span.textContent = run.text;
27398
- if (run.bold) span.style.fontWeight = "700";
27399
- if (run.italic) span.style.fontStyle = "italic";
27400
- if (run.fontSize != null) span.style.fontSize = `${run.fontSize}px`;
27401
- else if (run.fontScale != null) span.style.fontSize = `${run.fontScale * 100}%`;
27402
- if (run.fontFamily != null) span.style.fontFamily = run.fontFamily;
27403
- const color = solidColor(run.fill);
28129
+ span.textContent = run2.text;
28130
+ if (run2.bold) span.style.fontWeight = "700";
28131
+ if (run2.italic) span.style.fontStyle = "italic";
28132
+ if (run2.fontSize != null) span.style.fontSize = `${run2.fontSize}px`;
28133
+ else if (run2.fontScale != null) span.style.fontSize = `${run2.fontScale * 100}%`;
28134
+ if (run2.fontFamily != null) span.style.fontFamily = run2.fontFamily;
28135
+ const color = solidColor(run2.fill);
27404
28136
  if (color != null) span.style.color = color;
27405
28137
  const decorations = [];
27406
- if (run.underline) decorations.push("underline");
27407
- if (run.strikethrough) decorations.push("line-through");
27408
- if (run.overline) decorations.push("overline");
28138
+ if (run2.underline) decorations.push("underline");
28139
+ if (run2.strikethrough) decorations.push("line-through");
28140
+ if (run2.overline) decorations.push("overline");
27409
28141
  if (decorations.length > 0) span.style.textDecoration = decorations.join(" ");
27410
- if (run.script != null) span.style.verticalAlign = run.script;
27411
- if (run.baselineShift != null) {
27412
- span.setAttribute("data-baseline-shift", String(run.baselineShift));
28142
+ if (run2.script != null) span.style.verticalAlign = run2.script;
28143
+ if (run2.baselineShift != null) {
28144
+ span.setAttribute("data-baseline-shift", String(run2.baselineShift));
27413
28145
  }
27414
- if (run.letterSpacing != null) span.style.letterSpacing = `${run.letterSpacing}px`;
28146
+ if (run2.letterSpacing != null) span.style.letterSpacing = `${run2.letterSpacing}px`;
28147
+ if (run2.textTransform != null) span.style.textTransform = run2.textTransform;
27415
28148
  parent.appendChild(span);
27416
28149
  }
27417
28150
  }
@@ -27471,7 +28204,8 @@ var STYLE_KEYS = [
27471
28204
  "stroke",
27472
28205
  "script",
27473
28206
  "baselineShift",
27474
- "fontScale"
28207
+ "fontScale",
28208
+ "textTransform"
27475
28209
  ];
27476
28210
  var FLAG_KEYS = /* @__PURE__ */ new Set([
27477
28211
  "bold",
@@ -27495,8 +28229,8 @@ function deepEqual(a, b) {
27495
28229
  }
27496
28230
  return false;
27497
28231
  }
27498
- function valueAt(run, key) {
27499
- const value = run[key];
28232
+ function valueAt(run2, key) {
28233
+ const value = run2[key];
27500
28234
  if (FLAG_KEYS.has(key)) return value ?? false;
27501
28235
  return value;
27502
28236
  }
@@ -27509,7 +28243,7 @@ function sameStyle(a, b) {
27509
28243
  }
27510
28244
  function totalLength(runs) {
27511
28245
  let n = 0;
27512
- for (const run of runs) n += run.text.length;
28246
+ for (const run2 of runs) n += run2.text.length;
27513
28247
  return n;
27514
28248
  }
27515
28249
  function styleAtRange(runs, start, end) {
@@ -27520,14 +28254,14 @@ function styleAtRange(runs, start, end) {
27520
28254
  const mixed = /* @__PURE__ */ new Set();
27521
28255
  let seen = false;
27522
28256
  let pos = 0;
27523
- for (const run of runs) {
27524
- const runEnd = pos + run.text.length;
28257
+ for (const run2 of runs) {
28258
+ const runEnd = pos + run2.text.length;
27525
28259
  const a = Math.max(pos, lo);
27526
28260
  const b = Math.min(runEnd, hi);
27527
28261
  pos = runEnd;
27528
28262
  if (!(a < b)) continue;
27529
28263
  for (const key of STYLE_KEYS) {
27530
- const value = valueAt(run, key);
28264
+ const value = valueAt(run2, key);
27531
28265
  if (!seen) first.set(key, value);
27532
28266
  else if (!mixed.has(key) && !sameValue(key, first.get(key), value)) {
27533
28267
  mixed.add(key);
@@ -27548,14 +28282,14 @@ function styleAtRange(runs, start, end) {
27548
28282
  }
27549
28283
  function runsCarryStyling(runs) {
27550
28284
  return runs.some(
27551
- (run) => STYLE_KEYS.some((key) => {
27552
- const value = run[key];
28285
+ (run2) => STYLE_KEYS.some((key) => {
28286
+ const value = run2[key];
27553
28287
  return value !== void 0 && value !== false;
27554
28288
  })
27555
28289
  );
27556
28290
  }
27557
- function patchRun(run, patch) {
27558
- const next = { ...run };
28291
+ function patchRun(run2, patch) {
28292
+ const next = { ...run2 };
27559
28293
  for (const key of STYLE_KEYS) {
27560
28294
  if (!(key in patch)) continue;
27561
28295
  const value = patch[key];
@@ -27566,11 +28300,11 @@ function patchRun(run, patch) {
27566
28300
  }
27567
28301
  function coalesceRuns(runs) {
27568
28302
  const out = [];
27569
- for (const run of runs) {
27570
- if (run.text.length === 0) continue;
28303
+ for (const run2 of runs) {
28304
+ if (run2.text.length === 0) continue;
27571
28305
  const prev = out[out.length - 1];
27572
- if (prev && sameStyle(prev, run)) prev.text += run.text;
27573
- else out.push({ ...run });
28306
+ if (prev && sameStyle(prev, run2)) prev.text += run2.text;
28307
+ else out.push({ ...run2 });
27574
28308
  }
27575
28309
  return out;
27576
28310
  }
@@ -27580,16 +28314,16 @@ function applyStyleToRange(runs, start, end, patch) {
27580
28314
  if (!(lo < hi)) return coalesceRuns(runs);
27581
28315
  const out = [];
27582
28316
  let pos = 0;
27583
- for (const run of runs) {
27584
- const runEnd = pos + run.text.length;
28317
+ for (const run2 of runs) {
28318
+ const runEnd = pos + run2.text.length;
27585
28319
  const a = Math.max(pos, lo);
27586
28320
  const b = Math.min(runEnd, hi);
27587
28321
  if (a < b) {
27588
- if (pos < a) out.push({ ...run, text: run.text.slice(0, a - pos) });
27589
- out.push(patchRun({ ...run, text: run.text.slice(a - pos, b - pos) }, patch));
27590
- if (b < runEnd) out.push({ ...run, text: run.text.slice(b - pos) });
28322
+ if (pos < a) out.push({ ...run2, text: run2.text.slice(0, a - pos) });
28323
+ out.push(patchRun({ ...run2, text: run2.text.slice(a - pos, b - pos) }, patch));
28324
+ if (b < runEnd) out.push({ ...run2, text: run2.text.slice(b - pos) });
27591
28325
  } else {
27592
- out.push({ ...run });
28326
+ out.push({ ...run2 });
27593
28327
  }
27594
28328
  pos = runEnd;
27595
28329
  }
@@ -27685,7 +28419,7 @@ function patchForToggle(current, toggle, nodeStyle) {
27685
28419
  function styleAtCaret(runs, at) {
27686
28420
  return at > 0 ? styleAtRange(runs, at - 1, at) : styleAtRange(runs, 0, 1);
27687
28421
  }
27688
- function spliceRunAt(runs, at, run) {
28422
+ function spliceRunAt(runs, at, run2) {
27689
28423
  const out = [];
27690
28424
  let pos = 0;
27691
28425
  let placed2 = false;
@@ -27694,7 +28428,7 @@ function spliceRunAt(runs, at, run) {
27694
28428
  if (!placed2 && at <= end) {
27695
28429
  const k = at - pos;
27696
28430
  if (k > 0) out.push({ ...r, text: r.text.slice(0, k) });
27697
- out.push(run);
28431
+ out.push(run2);
27698
28432
  if (k < r.text.length) out.push({ ...r, text: r.text.slice(k) });
27699
28433
  placed2 = true;
27700
28434
  } else {
@@ -27702,7 +28436,7 @@ function spliceRunAt(runs, at, run) {
27702
28436
  }
27703
28437
  pos = end;
27704
28438
  }
27705
- if (!placed2) out.push(run);
28439
+ if (!placed2) out.push(run2);
27706
28440
  return applyStyleToRange(out, 0, 0, {});
27707
28441
  }
27708
28442
  function restoreOverlayFocus(overlay, range) {
@@ -28112,6 +28846,7 @@ function applyOverlayStyle(el, style) {
28112
28846
  if (style.underline) decorations.push("underline");
28113
28847
  if (style.strikethrough) decorations.push("line-through");
28114
28848
  el.style.textDecoration = decorations.length > 0 ? decorations.join(" ") : "none";
28849
+ el.style.textTransform = style.textTransform;
28115
28850
  el.style.whiteSpace = style.wrap ? "pre-wrap" : "pre";
28116
28851
  el.style.overflowWrap = "normal";
28117
28852
  el.style.wordBreak = "normal";
@@ -28313,7 +29048,8 @@ var OVERRIDES = [
28313
29048
  "fill",
28314
29049
  "script",
28315
29050
  "baselineShift",
28316
- "fontScale"
29051
+ "fontScale",
29052
+ "textTransform"
28317
29053
  ];
28318
29054
  function effectiveRangeStyle(range, style, paint) {
28319
29055
  const resolved = resolveTextStyle(style, paint);
@@ -28321,13 +29057,14 @@ function effectiveRangeStyle(range, style, paint) {
28321
29057
  const out = {
28322
29058
  fontFamily: resolved.fontFamily,
28323
29059
  fontSize: resolved.fontSize,
28324
- letterSpacing: resolved.letterSpacing
29060
+ letterSpacing: resolved.letterSpacing,
29061
+ textTransform: resolved.textTransform
28325
29062
  };
28326
29063
  if (resolved.fill !== null) out.fill = resolved.fill;
28327
29064
  for (const key of FLAGS) {
28328
29065
  const on = nodeHasFlag(node, key);
28329
- const run = range?.[key];
28330
- out[key] = on || run === void 0 ? on : run;
29066
+ const run2 = range?.[key];
29067
+ out[key] = on || run2 === void 0 ? on : run2;
28331
29068
  }
28332
29069
  if (range === null) return out;
28333
29070
  for (const key of OVERRIDES) {
@@ -28567,7 +29304,7 @@ function asMesh(fill) {
28567
29304
  }
28568
29305
  function shifted(color, dL, cScale, dHueDeg) {
28569
29306
  const [r, g, b, a] = resolveColor(color);
28570
- const [L, A, B] = srgbU8ToOklab(r * 255, g * 255, b * 255);
29307
+ const [L, A, B] = srgbFloatToOklab(r, g, b);
28571
29308
  const [, C, h] = oklabToOklch(L, A, B);
28572
29309
  const [nL, nA, nB] = oklchToOklab(
28573
29310
  Math.min(1, Math.max(0, L + dL)),
@@ -30541,19 +31278,19 @@ var isDev2 = typeof import.meta !== "undefined" && import.meta.env?.DEV;
30541
31278
  function useBooleans(adapter, _options = {}) {
30542
31279
  const adapterRef = useRef(adapter);
30543
31280
  adapterRef.current = adapter;
30544
- const run = useCallback((op) => {
31281
+ const run2 = useCallback((op) => {
30545
31282
  const result = applyBooleanOp(adapterRef.current, op);
30546
31283
  if (isDev2 && result.kind === "noop" && result.reason === "too-few-for-subtract") {
30547
31284
  console.warn("[useBooleans] subtract requires at least 2 selected paths");
30548
31285
  }
30549
31286
  }, []);
30550
31287
  return {
30551
- union: useCallback(() => run("union"), [run]),
30552
- intersect: useCallback(() => run("intersect"), [run]),
30553
- subtract: useCallback(() => run("subtract"), [run]),
30554
- exclude: useCallback(() => run("exclude"), [run]),
30555
- divide: useCallback(() => run("divide"), [run]),
30556
- crop: useCallback(() => run("crop"), [run])
31288
+ union: useCallback(() => run2("union"), [run2]),
31289
+ intersect: useCallback(() => run2("intersect"), [run2]),
31290
+ subtract: useCallback(() => run2("subtract"), [run2]),
31291
+ exclude: useCallback(() => run2("exclude"), [run2]),
31292
+ divide: useCallback(() => run2("divide"), [run2]),
31293
+ crop: useCallback(() => run2("crop"), [run2])
30557
31294
  };
30558
31295
  }
30559
31296
  function useBooleansAdapter(adapter) {
@@ -31244,9 +31981,6 @@ function cellRectAt(bounds, cols, rows, gap, col, row) {
31244
31981
  height: ch
31245
31982
  };
31246
31983
  }
31247
- function sortedChildIds(children) {
31248
- return children.map((c) => c.id).sort();
31249
- }
31250
31984
  function tileGrid(opts) {
31251
31985
  const { cols, rows } = opts;
31252
31986
  const gap = opts.gap ?? 0;
@@ -31297,12 +32031,18 @@ function tileGrid(opts) {
31297
32031
  snap: snap2,
31298
32032
  childPoses(container, children) {
31299
32033
  const out = /* @__PURE__ */ new Map();
31300
- const ids = sortedChildIds(children);
31301
- const byId = new Map(children.map((c) => [c.id, c.pose]));
31302
- for (let i = 0; i < ids.length && i < capacity; i++) {
32034
+ const ordered = children.map((c) => ({ c, cell: cellIndexOf(container.bounds, c.pose) })).sort((a, b) => {
32035
+ if (a.cell !== b.cell) {
32036
+ if (a.cell === null) return 1;
32037
+ if (b.cell === null) return -1;
32038
+ return a.cell - b.cell;
32039
+ }
32040
+ return a.c.id < b.c.id ? -1 : a.c.id > b.c.id ? 1 : 0;
32041
+ });
32042
+ for (let i = 0; i < ordered.length && i < capacity; i++) {
31303
32043
  const col = i % cols;
31304
32044
  const row = Math.floor(i / cols);
31305
- out.set(ids[i], cellPose(container.bounds, col, row, byId.get(ids[i])));
32045
+ out.set(ordered[i].c.id, cellPose(container.bounds, col, row, ordered[i].c.pose));
31306
32046
  }
31307
32047
  return out;
31308
32048
  },
@@ -31438,6 +32178,6 @@ function snapPoint(opts) {
31438
32178
  };
31439
32179
  }
31440
32180
 
31441
- export { ANCHOR_HIT_BASE_PX, ActionsProviderIfRoot, ArrowIcon, BUILTIN_MARKERS, BUNDLE_TOOLS, CURVE_REPS, CanvasView, ColorOverrideRegistry, CropIcon, CursorCoordsHud, DEFAULT_DEBUG_STROKES, DEFAULT_DEBUG_THEME, DEFAULT_FILL_COLOR, DEFAULT_HANDLE_SIZE2 as DEFAULT_HANDLE_SIZE, DEFAULT_MAX_ZOOM, DEFAULT_MIN_ZOOM, DEFAULT_PALETTE, DEFAULT_ROTATION_HANDLE_DISTANCE, DEFAULT_SHAPE_FILL, DEFAULT_STROKE_COLOR, DepRegistryProviderIfRoot, DeviceProfileProvider, DivideIcon, EASINGS, EFFECT_KIT_UNIFORMS, EFFECT_VERT_SRC, EllipseIcon, ExcludeIcon, EyedropperIcon, FALLBACK_FIT_VIEW, GHOST_STROKE, HANDLE_BASE_PX, HandIcon, IDENTITY_COLOR_MATRIX, IDENTITY_JOINT, IDENTITY_POSE_COMPOSITION, ImageIcon, IntersectIcon, KIT_SHAPE_KINDS, KIT_STANDARD_ACTION_IDS, LassoIcon, LineIcon, MESH_BAKE_SIZE, MESH_GRADIENT_KIND, MIXED, MOVE_ANCHORS, MinimapCanvas, OUTLINE_MIN_SCREEN_PX, PAINTED_CURSOR_LAYER_ID, PATH_ANCHOR_CHROME_ID, PenIcon, PencilIcon, PickHud, PointerProviderIfRoot, PolygonIcon, RECT_POSE_COMPOSITION, RIGID_POSE_COMPOSITION, ROTATION_HANDLE_BASE_PX, RectIcon, SPRING_PRESETS, SPRITE_STRIDE, SceneCanvas, SceneViewCanvas, SelectIcon, SelectionContextProvider, SelectionContextProviderIfRoot, ShaderCompileError, StarIcon, SubtractIcon, TOOL_PREF_KINDS, TextIcon, UNION_OF_CHILDREN, UnionIcon, UnknownIcon, VERSION, VIEW_ANIMATION_KEY, WHEEL_ZOOM_STEP, WeaselProvider, WeaselRenderer, ZOOM_FLOOR, _resetMarkersForTests, _resetPaintKindsForTests, actionIs, actionShortcuts, alignDeltaFor, alignInsertBehavior, alignMoveBehavior, alignResizeBehavior, always, and, animateLifecycle, animateOnSetPose, annulusSemiAxes, applyBooleanOp, applyStyleToRange, areaSelectAction, arrayAdapter, asNodeId, asPaint, bakeMesh, bezierCubic, bezierQuadratic, bindRig, blendPoses, blur, buildChromeCtx, buildGradientRamp, buildSceneViewCommands, canBringForward, canHover, canSendBackward, capabilityAll, capabilityIn, capabilityIs, capabilityNot, caretIndexAt, cellAt, charOffsetToDomPosition, circlePath, clampView, clearSelectionAction, clipboardCopyAction, clipboardCutAction, cloneAction, coarsePointer, composeAffordanceLayer, composePath, composeRectPose, composeRigidPose, composeSelectionPose, composeWorldPose, computeFitView, computeFitViewport, computeIndicatorCommand, computeWheelAction, cond, constrainTo45, containedThenNearest, cornerWeights, countPathAnchors, createCellHighlightLayer, createChildrenLayer, createCornerResizeAffordance, createDebugOverlayLayer, createDebugSink, createGridLayer, createGuidesLayer, createHistory2 as createHistory, createKeyState, createMoveToIndexOp, createNodeProperties, createNodeRouting, createPaintedCursorLayer, createParallaxLayer, createPathAnchorAffordances, createPathEditingOverlayLayer, createPathLayer, createPenPreviewLayer, createPoseFeed, createPoseOverrides, createReorderOp, createRotationAffordance, createScene, createSelectionHandlesLayer, createSelectionOutlineLayer, createSelectionOverlayLayer, createSetDataOp, createSetLayerOp, createSetPathOp, createSetTextOp, createSlew, createTextLayer, createTiledLayer, createVelocityTracker, createViewResolver, createViewportLayer, cubicBezierEasing, cullDrawCommands, cycleVertexColors, decomposePath, decomposeRectPose, decomposeRigidPose, defaultCommitAdapter, defaultDrawOne, defaultLabelTextRenderer, defaultNodeProperties, defaultNodeRouting, defaultVisibilityRules, defineTool, defineViewportTool, deriveAlignmentGuides, deriveParallaxView, derivedDepOf, derivedPose, documentPose, domPositionToCharOffset, domToRuns, drawLayers, drawOneLayer, 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, easeOutCubic, easeOutElastic, easeOutExpo, easeOutQuad, easeOutQuart, easeOutQuint, easeOutSine, editAnchorsAction, effectivePose, effectiveRangeStyle, enterTextEditAction, enumerateAnchors, evalPatch, fillInPoseFrame, fillToBoundsFrame, findNodeShape, findShapeInk, findShapeSilhouette, fitTextPose, fitToBounds, fitViewToBounds, fitZoom, flattenPreviews, focused, forEachCoalesced, frameRect, freeform, gateLayer, gesturing, getAlpha01, getImageBitmap, getMarker, getNodeShapes, getPaintKind, getStylusData, gradientForBounds, gradientGeometry, hexToRgba, hitAffordanceRegions, hitAnchor, hitRotationHandle, hostAnchorCss, hostAnchorRect, hovering, hoveringSelected, imageStatus, inferredNodeProperties, inferredNodeRouting, insertAction, insertPreviewExtent, interpolateView, isBuiltinToolPref, isGradientFill, isLayerPainted, isLayerVisible, isMeshGradientFill, isPatternSpec, isTensorPatch, isValidPatch, keySpecShortcut, lassoSelectAction, linear, listMarkers, listPaintKinds, makePinchZoomAction, makeViewportWheelPanAction, makeViewportZoomAction, markerDrawCommands, markerInset, markerKeyOf, markerSites, mat3, matchAlignment, meanScale, mergeAlphaFromPrev, meshBounds, meshGradientXml, modeIn, modeIs, modeNot, modifierHeld, momentum, moveAction, multiActive, nearest, nearestWithin, nestedHitTester, never, nodeHasFlag, none, normalizeHex, normalizeView, normalizeZoom, not, nurbs, openFilePicker, or, paintAlpha, paintKindOf, paintWithAlpha, paintWithColor, parseColor, parseColorToRgba255, parseDebugFlags, patchBounds, patchCorner, pathContainsPoint, pathContainsPolygon, pathContainsRect, pathDistanceToPoint, pathDivide, pathExclude, pathFromD, pathInPoseFrame, pathInWorld, pathIntersect, pathIntersectsPolygon, pathIntersectsRect, pathOriginProjection, pathSubtract, pathToAnchors, pathUnion, pickTopMostHit, pinchZoomAction, planPixelRender, pointInTextPose, poseRotationOf, prefUnit, pressureToWidth, rainbowVertexColors, rebaseLocalPose, rectMarkerPath, registerContentHandler, registerEffect, registerMarker, registerNodeShape, registerPaintKind, registerProgram, renderLabel, renderSceneToCanvas, renderSceneToPixels, resizeAction, resolveColor, resolveEasing, resolveFillPattern, resolveMarkerSize, resolveOverlays, resolvePatternSpec, resolvePreviews, resolveSkeleton, resolveStrokeWidth, resolveVisibility, rgbaToHex, rigidRigApply, rotateAction, rotateAroundAABBCenter, rotatePathAround, rotationDegreesUnit, rotationHandle, roundRectPath, roundToCell, runsToDom, sampleGradientStops, sampleTrack, sceneFromJSON, sceneNodeClientRect, sceneSelectionStore, sceneToAdapter, screenToWorld, seedMeshPatch, selectFromLasso, selectionAtLeast, selectionEmpty, selectionIs, setFlagOverRange, shapeCoversPoint, slewToward, sliceAction, snapPoint, solid, solidVertexColors, spiro, splitPathByLine, splitSubpaths, springPose, springVertexColors, squarePath, staggerVertexColors, startThresholdDrag, strokeInsets, strokeOf, strokeWith, styleAtRange, subscribeImageReady, tessellate, tessellateStroke, textCommand, textCommandFromPose, textCommandFromRuns, tileGrid, tiledProject, toHex8, translateRectPose, trimPolyline, tweenPose, tweenVertexColors, unionBoundsPath, unionOfChildren, unionOfChildrenVia, useAlign, useAnimator, useArrayAdapter, useAutoCenter, useBooleans, useBooleansAdapter, useCanvasFocus, useCanvasSize, useDecayLoop, useDeviceProfile, useDistribute, useDragGesture, useDragHandle, useDragRadial, useDragRect, useDropZone, useEllipseTool, useEyedropperTool, useGridCellHover, useGuides, useHandTool, useHandleDrag, useHostAnchor, useHoverTracking, useImageTool, useKeyState, useKeybindings, useLassoTool, useLineTool, useNodeOverlayFrame, usePenTool, usePencilTool, usePinchGesture, usePointerStylus, usePolygonTool, usePoseCompositionDepSource, usePoseDescriptorDepSource, usePoseRun, usePublishSelection, useRectTool, useResizePolicy, useRig, useScene, useSceneAdapter, useSceneTextEdit, useSelectTool, useSelection, useSelectionContext, useSimulation, useSliceDep, useStandardActions, useStarTool, useTextEdit, useTextTool, useTools, useVelocityTracker, useViewAnimation, useViewAnimationOn, useVisibleRaf, viewToMat3, viewToTransform, viewZoom, viewportDragPanAction, viewportWheelPanAction, viewportZoomAction, viewportsAt, vignette, wheelPan, wheelZoom, wheelZoomFactor, when, withAlpha01, withCoord, withGradientKind, worldEditToStorage, worldPoseLookup, worldToScreen, zoomAt, zoomAtLeast };
31442
- //# sourceMappingURL=chunk-GREL4MVO.js.map
31443
- //# sourceMappingURL=chunk-GREL4MVO.js.map
32181
+ export { ACTION_GLYPHS, ANCHOR_HIT_BASE_PX, ActionsProviderIfRoot, AlignBottomIcon, AlignCenterXIcon, AlignCenterYIcon, AlignLeftIcon, AlignRightIcon, AlignTopIcon, ArrowIcon, BUILTIN_MARKERS, BUNDLE_TOOLS, BringForwardIcon, BringToFrontIcon, CURVE_REPS, CanvasView, ColorOverrideRegistry, CopyIcon, CropIcon, CursorCoordsHud, CutIcon, DEFAULT_DEBUG_STROKES, DEFAULT_DEBUG_THEME, DEFAULT_FILL_COLOR, DEFAULT_HANDLE_SIZE2 as DEFAULT_HANDLE_SIZE, DEFAULT_MAX_ZOOM, DEFAULT_MIN_ZOOM, DEFAULT_PALETTE, DEFAULT_ROTATION_HANDLE_DISTANCE, DEFAULT_SHAPE_FILL, DEFAULT_STROKE_COLOR, DeleteIcon, DepRegistryProviderIfRoot, DeviceProfileProvider, DistributeHorizontalIcon, DistributeVerticalIcon, DivideIcon, DuplicateIcon, EASINGS, EFFECT_KIT_UNIFORMS, EFFECT_VERT_SRC, EllipseIcon, ExcludeIcon, EyedropperIcon, FALLBACK_FIT_VIEW, FlipXIcon, FlipYIcon, GHOST_STROKE, GroupIcon, HANDLE_BASE_PX, HandIcon, IDENTITY_COLOR_MATRIX, IDENTITY_JOINT, IDENTITY_POSE_COMPOSITION, ImageIcon, IntersectIcon, KIT_SHAPE_KINDS, KIT_STANDARD_ACTION_IDS, LassoIcon, LineIcon, MESH_BAKE_SIZE, MESH_GRADIENT_KIND, MIXED, MOVE_ANCHORS, MinimapCanvas, OUTLINE_MIN_SCREEN_PX, PAINTED_CURSOR_LAYER_ID, PATH_ANCHOR_CHROME_ID, PasteIcon, PenIcon, PencilIcon, PickHud, PointerProviderIfRoot, PolygonIcon, RECT_POSE_COMPOSITION, RIGID_POSE_COMPOSITION, ROTATION_HANDLE_BASE_PX, RectIcon, RedoIcon, SPRING_PRESETS, SPRITE_STRIDE, SceneCanvas, SceneViewCanvas, SelectIcon, SelectionContextProvider, SelectionContextProviderIfRoot, SendBackwardIcon, SendToBackIcon, ShaderCompileError, StarIcon, SubtractIcon, TOOL_PREF_KINDS, TextIcon, UNION_OF_CHILDREN, UndoIcon, UngroupIcon, UnionIcon, UnknownIcon, VERSION, VIEW_ANIMATION_KEY, WHEEL_ZOOM_STEP, WeaselProvider, WeaselRenderer, ZOOM_FLOOR, _resetMarkersForTests, _resetPaintKindsForTests, actionIs, actionShortcuts, alignDeltaFor, alignInsertBehavior, alignMoveBehavior, alignResizeBehavior, always, and, animateLifecycle, animateOnSetPose, annulusSemiAxes, applyBooleanOp, applyStyleToRange, areaSelectAction, arrayAdapter, asNodeId, asPaint, bakeMesh, bezierCubic, bezierQuadratic, bindRig, blendPoses, blur, buildChromeCtx, buildGradientRamp, buildSceneViewCommands, canBringForward, canHover, canSendBackward, capabilityAll, capabilityIn, capabilityIs, capabilityNot, caretIndexAt, cellAt, charOffsetToDomPosition, circlePath, clampView, clearSelectionAction, clipboardCopyAction, clipboardCutAction, clipboardPasteAction, cloneAction, coarsePointer, composeAffordanceLayer, composePath, composeRectPose, composeRigidPose, composeSelectionPose, composeWorldPose, computeFitView, computeFitViewport, computeIndicatorCommand, computeWheelAction, cond, constrainTo45, containedThenNearest, cornerWeights, countPathAnchors, createCellHighlightLayer, createChildrenLayer, createCornerResizeAffordance, createDebugOverlayLayer, createDebugSink, createGridLayer, createGuidesLayer, createHistory2 as createHistory, createKeyState, createMoveToIndexOp, createNodeProperties, createNodeRouting, createPaintedCursorLayer, createParallaxLayer, createPathAnchorAffordances, createPathEditingOverlayLayer, createPathLayer, createPenPreviewLayer, createPoseFeed, createPoseOverrides, createReorderOp, createRotationAffordance, createScene, createSelectionHandlesLayer, createSelectionOutlineLayer, createSelectionOverlayLayer, createSetDataOp, createSetLayerOp, createSetPathOp, createSetTextOp, createSlew, createTextLayer, createTiledLayer, createVelocityTracker, createViewResolver, createViewportLayer, cubicBezierEasing, cullDrawCommands, cycleFill, cycleVertexColors, decomposePath, decomposeRectPose, decomposeRigidPose, defaultCommitAdapter, defaultDrawOne, defaultLabelTextRenderer, defaultNodeProperties, defaultNodeRouting, defaultVisibilityRules, defineTool, defineViewportTool, definesFrame, deriveAlignmentGuides, deriveParallaxView, derivedDepOf, derivedPose, documentPose, domPositionToCharOffset, domToRuns, drawLayers, drawOneLayer, 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, easeOutCubic, easeOutElastic, easeOutExpo, easeOutQuad, easeOutQuart, easeOutQuint, easeOutSine, editAnchorsAction, effectivePose, effectiveRangeStyle, enterTextEditAction, enumerateAnchors, evalPatch, fillInPoseFrame, fillToBoundsFrame, findNodeShape, findShapeInk, findShapeSilhouette, fitTextPose, fitToBounds, fitViewToBounds, fitZoom, flattenPreviews, focused, forEachCoalesced, frameRect, freeform, gateLayer, gesturing, getAlpha01, getImageBitmap, getMarker, getNodeShapes, getPaintKind, getStylusData, gradientForBounds, gradientGeometry, hexToRgba, hitAffordanceRegions, hitAnchor, hitRotationHandle, hostAnchorCss, hostAnchorRect, hovering, hoveringSelected, imageStatus, inferredNodeProperties, inferredNodeRouting, insertAction, insertPreviewExtent, interpolateView, isBuiltinToolPref, isGradientFill, isLayerPainted, isLayerVisible, isMeshGradientFill, isPatternSpec, isTensorPatch, isValidPatch, isVectorImageSrc, keySpecShortcut, lassoSelectAction, linear, listMarkers, listPaintKinds, makePinchZoomAction, makeViewportWheelPanAction, makeViewportZoomAction, markerDrawCommands, markerInset, markerKeyOf, markerSites, mat3, matchAlignment, meanScale, mergeAlphaFromPrev, meshBounds, meshGradientXml, modeIn, modeIs, modeNot, modifierHeld, momentum, moveAction, multiActive, nearest, nearestWithin, nestedHitTester, never, nodeHasFlag, none, normalizeHex, normalizeView, normalizeZoom, not, nurbs, openFilePicker, or, paintAlpha, paintKindOf, paintWithAlpha, paintWithColor, parseColor, parseColorToRgba255, parseDebugFlags, patchBounds, patchCorner, pathContainsPoint, pathContainsPolygon, pathContainsRect, pathDistanceToPoint, pathDivide, pathExclude, pathFromD, pathInPoseFrame, pathInWorld, pathIntersect, pathIntersectsPolygon, pathIntersectsRect, pathOriginProjection, pathSubtract, pathToAnchors, pathUnion, pickTopMostHit, pinchZoomAction, planPixelRender, pointInTextPose, poseRotationOf, prefUnit, pressureToWidth, rainbowVertexColors, rebaseLocalPose, rectMarkerPath, registerContentHandler, registerEffect, registerMarker, registerNodeShape, registerPaintKind, registerProgram, renderLabel, renderSceneToCanvas, renderSceneToPixels, resizeAction, resolveColor, resolveEasing, resolveFillPattern, resolveMarkerSize, resolveOverlays, resolvePatternSpec, resolvePreviews, resolveSkeleton, resolveStrokeWidth, resolveVisibility, rgbaToHex, rigidRigApply, rotateAction, rotateAroundAABBCenter, rotatePathAround, rotationDegreesUnit, rotationHandle, roundRectPath, roundToCell, runsToDom, sampleGradientStops, sampleTrack, sceneFromJSON, sceneNodeClientRect, sceneSelectionStore, sceneToAdapter, screenToWorld, seedMeshPatch, selectFromLasso, selectionAtLeast, selectionEmpty, selectionIs, setFlagOverRange, shapeCoversPoint, slewToward, sliceAction, snapPoint, solid, solidVertexColors, spiro, splitPathByLine, splitSubpaths, springPose, springVertexColors, squarePath, staggerVertexColors, startThresholdDrag, strokeInsets, strokeOf, strokeWith, styleAtRange, subscribeImageReady, tessellate, tessellateStroke, textCommand, textCommandFromPose, textCommandFromRuns, tileGrid, tiledProject, toHex8, translateRectPose, trimPolyline, tweenPose, tweenVertexColors, unionBoundsPath, unionOfChildren, unionOfChildrenVia, useAlign, useAnimator, useArrayAdapter, useAutoCenter, useBooleans, useBooleansAdapter, useCanvasFocus, useCanvasSize, useDecayLoop, useDeviceProfile, useDistribute, useDragGesture, useDragHandle, useDragRadial, useDragRect, useDropZone, useEllipseTool, useEyedropperTool, useGridCellHover, useGuides, useHandTool, useHandleDrag, useHostAnchor, useHoverTracking, useImageTool, useKeyState, useKeybindings, useLassoTool, useLineTool, useNodeOverlayFrame, usePenTool, usePencilTool, usePinchGesture, usePointerStylus, usePolygonTool, usePoseCompositionDepSource, usePoseDescriptorDepSource, usePoseRun, usePublishSelection, useRectTool, useResizePolicy, useRig, useScene, useSceneAdapter, useSceneTextEdit, useSelectTool, useSelection, useSelectionContext, useSimulation, useSliceDep, useStandardActions, useStarTool, useTextEdit, useTextTool, useTools, useVelocityTracker, useViewAnimation, useViewAnimationOn, useVisibleRaf, viewToMat3, viewToTransform, viewZoom, viewportDragPanAction, viewportWheelPanAction, viewportZoomAction, viewportsAt, vignette, wheelPan, wheelZoom, wheelZoomFactor, when, withAlpha01, withCoord, withGradientKind, worldEditToStorage, worldPoseLookup, worldToScreen, zoomAt, zoomAtLeast };
32182
+ //# sourceMappingURL=chunk-THCSDHII.js.map
32183
+ //# sourceMappingURL=chunk-THCSDHII.js.map