@weasel-js/core 1.5.2 → 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.
@@ -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, srgbFloatToOklab, oklabToOklch, oklchToOklab, oklabToSrgbU8 } from '@weasel-js/paint';
14
- export { STROKE_DASH_RATIOS, alignedStrokeRect, dashForStrokeStyle, hexToOklchDeg, lerpColorArray, lerpOklab, lerpOklch, oklabToOklch, oklabToSrgbU8, oklchDegToHex, oklchToOklab, resolveScreenLength, srgbFloatToOklab, 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.2" ;
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
  }
@@ -5017,31 +5029,217 @@ var EffectTargets = class {
5017
5029
  }
5018
5030
  };
5019
5031
 
5020
- // src/renderer/WeaselRenderer.ts
5021
- 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;
5022
5046
  function extractUniformNames(glsl) {
5023
- const re = /\buniform\s+([^;{]+);/g;
5024
- const names = [];
5025
- let m;
5026
- while ((m = re.exec(glsl)) !== null) {
5027
- const words = m[1].trim().split(/\s+/);
5028
- let i = 0;
5029
- while (i < words.length && UNIFORM_QUALIFIER.test(words[i])) i++;
5030
- i++;
5031
- const declarators = words.slice(i).join(" ");
5032
- if (!declarators) continue;
5033
- for (const raw of declarators.split(",")) {
5034
- const d = raw.trim();
5035
- const arr = /^(\w+)\s*\[\s*(\d+)\s*\]$/.exec(d);
5036
- if (arr) {
5037
- const size = parseInt(arr[2], 10);
5038
- for (let k = 0; k < size; k++) names.push(`${arr[1]}[${k}]`);
5039
- } else if (/^\w+$/.test(d)) {
5040
- names.push(d);
5041
- }
5042
- }
5043
- }
5044
- 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)];
5045
5243
  }
5046
5244
  var WeaselRenderer = class {
5047
5245
  gl;
@@ -5178,11 +5376,7 @@ var WeaselRenderer = class {
5178
5376
  const vertSrc = src.vert === "" ? CUSTOM_VERT_SRC : src.vert;
5179
5377
  const fragSrc = src.frag;
5180
5378
  const program = new ShaderProgram(this.gl, vertSrc, fragSrc);
5181
- program.lookupUniforms([
5182
- ...CUSTOM_KIT_UNIFORMS,
5183
- ...extractUniformNames(vertSrc),
5184
- ...extractUniformNames(fragSrc)
5185
- ]);
5379
+ program.lookupUniforms(customUniformNames(vertSrc, fragSrc));
5186
5380
  program.lookupAttributes(CUSTOM_ATTRIBUTES);
5187
5381
  const previous = this.programRegistry.get(handle.id);
5188
5382
  if (previous) this.gl.deleteProgram(previous.handle);
@@ -5277,6 +5471,7 @@ var WeaselRenderer = class {
5277
5471
  if (aPos === void 0) throw new Error("a_position missing after restore");
5278
5472
  this.meshCache = new GLMeshCache(this.gl, aPos);
5279
5473
  this.textureCache = new GLTextureCache(this.gl);
5474
+ this.imageCache.dispose();
5280
5475
  this.imageCache = new GLImageCache(this.gl, this.imageMinification);
5281
5476
  this.gradRamps = new GradientRampAtlas(this.gl);
5282
5477
  markAllFontsNotUploaded();
@@ -5289,7 +5484,7 @@ var WeaselRenderer = class {
5289
5484
  const vertSrc = src.vert === "" ? CUSTOM_VERT_SRC : src.vert;
5290
5485
  try {
5291
5486
  const program = new ShaderProgram(this.gl, vertSrc, src.frag);
5292
- program.lookupUniforms([...CUSTOM_KIT_UNIFORMS, ...extractUniformNames(src.frag)]);
5487
+ program.lookupUniforms(customUniformNames(vertSrc, src.frag));
5293
5488
  program.lookupAttributes(CUSTOM_ATTRIBUTES);
5294
5489
  this.programRegistry.set(id, program);
5295
5490
  } catch (e) {
@@ -5339,6 +5534,7 @@ var WeaselRenderer = class {
5339
5534
  }
5340
5535
  this.programRegistry.clear();
5341
5536
  this.textureCache.free();
5537
+ this.imageCache.dispose();
5342
5538
  this.gradRamps.free();
5343
5539
  if (this.quadVbo) gl.deleteBuffer(this.quadVbo);
5344
5540
  if (this.quadIbo) gl.deleteBuffer(this.quadIbo);
@@ -6664,7 +6860,7 @@ function composeWorldPose(adapter, id, compose2) {
6664
6860
  while (cursor !== null) {
6665
6861
  if (seen.has(cursor)) break;
6666
6862
  seen.add(cursor);
6667
- chain.push(cursor);
6863
+ if (adapter.definesFrame?.(cursor) ?? true) chain.push(cursor);
6668
6864
  cursor = adapter.getParent(cursor);
6669
6865
  }
6670
6866
  let world = adapter.getPose(chain[chain.length - 1]);
@@ -6684,9 +6880,19 @@ function translateRectPose(pose, dx, dy) {
6684
6880
  return { ...pose, x: pose.x + dx, y: pose.y + dy };
6685
6881
  }
6686
6882
  function rebaseLocalPose(adapter, worldPose, newParentId, compose2, decompose) {
6687
- if (newParentId === null) return worldPose;
6688
- const parentWorld = composeWorldPose(adapter, newParentId, compose2);
6689
- 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;
6690
6896
  }
6691
6897
  function decomposeRectPose(parent, world) {
6692
6898
  return {
@@ -7033,6 +7239,9 @@ function documentPose(source, node) {
7033
7239
  function effectivePose(source, node) {
7034
7240
  return poseIn(source, node, "effective");
7035
7241
  }
7242
+ function definesFrame(node) {
7243
+ return !(node.dependsOn === "children" && node.derivePose !== void 0);
7244
+ }
7036
7245
  function poseRotationOf(pose) {
7037
7246
  const p = pose;
7038
7247
  if (p.rotation && p.x != null && p.y != null && p.width != null && p.height != null) {
@@ -7243,14 +7452,14 @@ function defaultRectEl(target) {
7243
7452
  }
7244
7453
  function useHandleDrag(opts) {
7245
7454
  const { onMove, onStart, onEnd, onCancel, getRect } = opts;
7246
- const live = useRef(null);
7455
+ const live2 = useRef(null);
7247
7456
  useEffect(() => () => {
7248
- live.current?.cancel();
7457
+ live2.current?.cancel();
7249
7458
  }, []);
7250
7459
  const onPointerDown = useCallback(
7251
7460
  (e) => {
7252
7461
  e.preventDefault();
7253
- live.current?.cancel();
7462
+ live2.current?.cancel();
7254
7463
  const target = e.currentTarget;
7255
7464
  const rectEl = getRect ? getRect(target) : defaultRectEl(target);
7256
7465
  const rect = rectEl.getBoundingClientRect();
@@ -7259,17 +7468,17 @@ function useHandleDrag(opts) {
7259
7468
  return { x, y };
7260
7469
  };
7261
7470
  let moved = false;
7262
- live.current = openPointerSession(target, e, {
7471
+ live2.current = openPointerSession(target, e, {
7263
7472
  onMove: (ev) => {
7264
7473
  moved = true;
7265
7474
  onMove(localOf(ev.clientX, ev.clientY), ev);
7266
7475
  },
7267
7476
  onEnd: (ev) => {
7268
- live.current = null;
7477
+ live2.current = null;
7269
7478
  onEnd?.({ point: localOf(ev.clientX, ev.clientY), moved, event: ev });
7270
7479
  },
7271
7480
  onCancel: (reason) => {
7272
- live.current = null;
7481
+ live2.current = null;
7273
7482
  onCancel?.(reason);
7274
7483
  }
7275
7484
  });
@@ -7375,9 +7584,9 @@ var DRAG_THRESHOLD_PX_SQ = 25;
7375
7584
  function useDragHandle(getPayload, options) {
7376
7585
  const optsRef = useRef(options);
7377
7586
  optsRef.current = options;
7378
- const live = useRef(null);
7587
+ const live2 = useRef(null);
7379
7588
  useEffect(() => () => {
7380
- live.current?.cancel();
7589
+ live2.current?.cancel();
7381
7590
  }, []);
7382
7591
  const onPointerDown = useCallback((e) => {
7383
7592
  if (e.pointerType === "mouse" && e.button !== 0) return;
@@ -7387,7 +7596,7 @@ function useDragHandle(getPayload, options) {
7387
7596
  const startY = e.clientY;
7388
7597
  let ghost = null;
7389
7598
  let past = false;
7390
- live.current = openPointerSession(target, e, {
7599
+ live2.current = openPointerSession(target, e, {
7391
7600
  onMove: (ev) => {
7392
7601
  if (ghost) {
7393
7602
  ghost.move(ev);
@@ -7417,11 +7626,11 @@ function useDragHandle(getPayload, options) {
7417
7626
  );
7418
7627
  },
7419
7628
  onEnd: (ev) => {
7420
- live.current = null;
7629
+ live2.current = null;
7421
7630
  ghost?.drop(ev);
7422
7631
  },
7423
7632
  onCancel: () => {
7424
- live.current = null;
7633
+ live2.current = null;
7425
7634
  ghost?.cancel();
7426
7635
  }
7427
7636
  }, { capture: false });
@@ -7784,10 +7993,11 @@ function keySpecShortcut(spec) {
7784
7993
  shift: mods?.shift === true
7785
7994
  };
7786
7995
  }
7787
- function actionShortcuts(action) {
7996
+ function actionShortcuts(action, params) {
7788
7997
  const out = [];
7789
7998
  const seen = /* @__PURE__ */ new Set();
7790
- for (const { spec } of actionBindings(action)) {
7999
+ for (const { spec, opts } of actionBindings(action)) {
8000
+ if (params && !paramsMatch(opts?.params, params)) continue;
7791
8001
  const shortcut = keySpecShortcut(spec);
7792
8002
  if (!shortcut) continue;
7793
8003
  const token = `${shortcut.key}|${shortcut.mod}|${shortcut.alt}|${shortcut.shift}`;
@@ -7797,6 +8007,10 @@ function actionShortcuts(action) {
7797
8007
  }
7798
8008
  return out;
7799
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
+ }
7800
8014
 
7801
8015
  // src/core/scene/types.ts
7802
8016
  var asNodeId = (s) => s;
@@ -7996,7 +8210,11 @@ function runLayoutPass(scratch, moveCtx) {
7996
8210
  y: moveCtx.drag.current.y - selectionCenter.y
7997
8211
  };
7998
8212
  const candidates = [];
7999
- 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;
8000
8218
  if (layout2.contains) return layout2.contains(cPose, selectionCenter);
8001
8219
  return selectionCenter.x >= aabb.x && selectionCenter.x < aabb.x + aabb.width && selectionCenter.y >= aabb.y && selectionCenter.y < aabb.y + aabb.height;
8002
8220
  };
@@ -8008,7 +8226,7 @@ function runLayoutPass(scratch, moveCtx) {
8008
8226
  if (!node) return;
8009
8227
  const worldPose = composeWorldPose(poseAdapter, id, pc.compose);
8010
8228
  const worldAABB = poseDescriptorForNode(d, node).getBounds(worldPose);
8011
- if (!testInside(worldPose, worldAABB, layout2)) return;
8229
+ if (!testInside(id, worldPose, worldAABB, layout2)) return;
8012
8230
  if (layout2.acceptsDrop) {
8013
8231
  const arg = { id, bounds: worldAABB };
8014
8232
  for (const c of dragged) if (!layout2.acceptsDrop(arg, c.arg)) return;
@@ -8031,28 +8249,33 @@ function runLayoutPass(scratch, moveCtx) {
8031
8249
  };
8032
8250
  walk(null, []);
8033
8251
  let dest = null;
8034
- for (const c of candidates) {
8035
- if (dest === null) {
8036
- dest = c;
8037
- continue;
8038
- }
8039
- if (c.depth > dest.depth) {
8040
- dest = c;
8041
- continue;
8042
- }
8043
- if (c.depth < dest.depth) continue;
8044
- let cAfter = false;
8045
- for (let i = 0; i < c.zPath.length; i++) {
8046
- if (c.zPath[i] > dest.zPath[i]) {
8047
- cAfter = true;
8048
- 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;
8049
8260
  }
8050
- if (c.zPath[i] < dest.zPath[i]) {
8051
- cAfter = false;
8052
- 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
+ }
8053
8276
  }
8277
+ if (cAfter) dest = c;
8054
8278
  }
8055
- if (cAfter) dest = c;
8056
8279
  }
8057
8280
  if (!dest) return;
8058
8281
  const layout = dest.layout;
@@ -8119,6 +8342,10 @@ function runLayoutPass(scratch, moveCtx) {
8119
8342
  }
8120
8343
  scratch.layoutPass = { layout, container, placements, sourceReflow, reflowIds };
8121
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
+ }
8122
8349
  function groupBySourceContainer(dragged, exclude) {
8123
8350
  const out = /* @__PURE__ */ new Map();
8124
8351
  for (const d of dragged) {
@@ -8134,17 +8361,18 @@ function groupBySourceContainer(dragged, exclude) {
8134
8361
  return out;
8135
8362
  }
8136
8363
  function translateCommitOps(scratch, ids, dx, dy) {
8137
- const m = [1, 0, 0, 1, dx, dy];
8138
8364
  const ops = [];
8139
8365
  for (const id of ids) {
8140
- if (!scratch.startPoses.has(id)) continue;
8366
+ if (!scratch.startPoses.has(id) || scratch.carried.has(id)) continue;
8141
8367
  const node = scratch.scene.get(id);
8142
8368
  if (!node) continue;
8369
+ const l = localDelta(scratch, id, dx, dy);
8370
+ const m = [1, 0, 0, 1, l.dx, l.dy];
8143
8371
  const authored = node.pose;
8144
8372
  ops.push(createTransformOp({
8145
8373
  id,
8146
8374
  from: authored,
8147
- to: translatePoseViaDescriptor(authored, dx, dy, scratch.projection),
8375
+ to: translatePoseViaDescriptor(authored, l.dx, l.dy, scratch.projection),
8148
8376
  label: "Move"
8149
8377
  }));
8150
8378
  if (scratch.geometryProjection) {
@@ -8159,6 +8387,16 @@ function translateCommitOps(scratch, ids, dx, dy) {
8159
8387
  }
8160
8388
  return ops;
8161
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
+ }
8162
8400
  function unreleasedIds(scratch, released) {
8163
8401
  const selected = new Set(scratch.ids);
8164
8402
  const out = scratch.ids.filter((id) => !released.has(id));
@@ -8180,7 +8418,11 @@ function unreleasedIds(scratch, released) {
8180
8418
  function scenePoseAdapter(scene) {
8181
8419
  return {
8182
8420
  getPose: (id) => documentPose(scene, scene.get(id)),
8183
- 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
+ }
8184
8426
  };
8185
8427
  }
8186
8428
  function resolveDropTarget(endCtx, scratch) {
@@ -8285,6 +8527,23 @@ var moveAction = {
8285
8527
  queue.push(childId);
8286
8528
  }
8287
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
+ }
8288
8547
  const behaviors = opts?.behaviors ?? [];
8289
8548
  const adapter = moveGestureAdapter(scene);
8290
8549
  const origin = /* @__PURE__ */ new Map();
@@ -8304,6 +8563,8 @@ var moveAction = {
8304
8563
  startPoses,
8305
8564
  ids,
8306
8565
  cascadeIds,
8566
+ carried,
8567
+ frames,
8307
8568
  scene,
8308
8569
  currentDelta: { dx: 0, dy: 0 },
8309
8570
  previews: /* @__PURE__ */ new Map(),
@@ -8356,7 +8617,12 @@ var moveAction = {
8356
8617
  scratch.currentDelta = { dx, dy };
8357
8618
  scratch.previews.clear();
8358
8619
  for (const [id, ori] of scratch.startPoses) {
8359
- 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));
8360
8626
  }
8361
8627
  scratch.layoutPass = null;
8362
8628
  runLayoutPass(scratch, moveCtx);
@@ -8555,15 +8821,18 @@ var moveAction = {
8555
8821
  };
8556
8822
 
8557
8823
  // src/interactions/actions/defaultCommitAdapter.ts
8558
- function defaultCommitAdapter(scene) {
8824
+ function defaultCommitAdapter(scene, selection) {
8559
8825
  return {
8560
8826
  getNode: (id) => scene.get(asNodeId(id)),
8561
8827
  getNodes: () => {
8562
8828
  return [...scene.renderOrderNodes()];
8563
8829
  },
8564
8830
  getPose: (id) => documentPose(scene, scene.get(asNodeId(id))),
8565
- getSelection: () => [...scene.getSelection()],
8566
- 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
+ },
8567
8836
  getParent: (id) => scene.get(asNodeId(id))?.parent ?? null,
8568
8837
  getRemovalClosure: (ids) => [...scene.removalClosure(ids.map(asNodeId))],
8569
8838
  getChildren: (parentId) => parentId === null ? [...scene.roots] : [...scene.childrenOf(asNodeId(parentId))],
@@ -8607,8 +8876,8 @@ function poseFrame(adapter, pc) {
8607
8876
  pc,
8608
8877
  world: (id) => composeWorldPose(adapter, id, pc.compose),
8609
8878
  parentWorld: (id) => {
8610
- const parent = adapter.getParent(id);
8611
- 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);
8612
8881
  },
8613
8882
  local: (id, worldPose) => rebaseLocalPose(adapter, worldPose, adapter.getParent(id), pc.compose, pc.decompose),
8614
8883
  localUnder: (parentId, worldPose) => rebaseLocalPose(adapter, worldPose, parentId, pc.compose, pc.decompose)
@@ -8622,7 +8891,11 @@ function scenePoseFrame(scene, poseComposition) {
8622
8891
  const node = scene.get(id);
8623
8892
  return node === void 0 ? { x: 0, y: 0, width: 0, height: 0 } : effectivePose(source, node);
8624
8893
  },
8625
- 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
+ }
8626
8899
  };
8627
8900
  return poseFrame(adapter, pc);
8628
8901
  }
@@ -8979,7 +9252,7 @@ var resizeAction = {
8979
9252
  }
8980
9253
  if (ops.length > 0) {
8981
9254
  if (scratch.applyOps) scratch.applyOps(ops, "Resize");
8982
- else scratch.scene.applyBatch(ops, "Resize", defaultCommitAdapter(scratch.scene));
9255
+ else scratch.scene.applyBatch(ops, "Resize", defaultCommitAdapter(scratch.scene, selection.adapterMethods));
8983
9256
  }
8984
9257
  dropPreviewOverrides(scratch);
8985
9258
  scratch.previews.clear();
@@ -9135,7 +9408,7 @@ var rotateAction = {
9135
9408
  }
9136
9409
  if (ops.length > 0) {
9137
9410
  if (scratch.applyOps) scratch.applyOps(ops, "Rotate");
9138
- else scratch.scene.applyBatch(ops, "Rotate", defaultCommitAdapter(scratch.scene));
9411
+ else scratch.scene.applyBatch(ops, "Rotate", defaultCommitAdapter(scratch.scene, selection.adapterMethods));
9139
9412
  }
9140
9413
  dropPreviewOverrides(scratch);
9141
9414
  scratch.previews.clear();
@@ -9602,7 +9875,7 @@ var cloneAction = {
9602
9875
  }
9603
9876
  if (ops.length > 0) {
9604
9877
  if (scratch.applyOps) scratch.applyOps(ops, "Clone");
9605
- else scratch.scene.applyBatch(ops, "Clone", defaultCommitAdapter(scratch.scene));
9878
+ else scratch.scene.applyBatch(ops, "Clone", defaultCommitAdapter(scratch.scene, selection.adapterMethods));
9606
9879
  }
9607
9880
  scratch.previews.clear();
9608
9881
  },
@@ -9782,6 +10055,11 @@ function makeViewportZoomAction(opts = {}) {
9782
10055
  spec: { kind: "wheel", mods: { ctrl: true } },
9783
10056
  opts: { params: { kind: "wheel" } }
9784
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
+ },
9785
10063
  // Cmd+= → zoom in (also accepts Cmd+Shift+= which is Cmd++ on many keyboards)
9786
10064
  {
9787
10065
  spec: { kind: "key", key: "=", mods: { mod: true, shift: "optional" } },
@@ -9815,14 +10093,21 @@ function makeViewportZoomAction(opts = {}) {
9815
10093
  view.animate(target, { ...tweenOpts, ms: ms ?? tweenOpts.ms });
9816
10094
  };
9817
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
+ };
9818
10104
  switch (kind) {
9819
- case "wheel": {
9820
- const deltaY = params?.deltaY ?? 0;
9821
- const clientX = params?.clientX ?? 0;
9822
- const clientY = params?.clientY ?? 0;
9823
- 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);
9824
10110
  break;
9825
- }
9826
10111
  case "in":
9827
10112
  stepTo(zoomAt(stepFrom(), keyAnchor(view), KEY_STEP, clamp));
9828
10113
  break;
@@ -10623,6 +10908,150 @@ function makePinchZoomAction(opts = {}) {
10623
10908
  };
10624
10909
  }
10625
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
+ }
10626
11055
 
10627
11056
  // src/interactions/actions/defaults/delete.ts
10628
11057
  function hostIndex(scene, id) {
@@ -10649,6 +11078,8 @@ function buildDeleteOps(scene, ids, label) {
10649
11078
  var deleteAction = {
10650
11079
  id: "delete",
10651
11080
  label: "Delete",
11081
+ icon: createElement(DeleteIcon),
11082
+ group: "edit",
10652
11083
  // Suppressed while any tool is mid-gesture — accidentally hitting
10653
11084
  // Delete during a drag shouldn't wipe the selection out from under
10654
11085
  // the in-flight handle.
@@ -10671,25 +11102,20 @@ var deleteAction = {
10671
11102
  const ops = buildDeleteOps(scene, ids, "Delete");
10672
11103
  if (ops.length > 0) {
10673
11104
  if (applyOps) applyOps(ops, "Delete");
10674
- else scene.applyBatch(ops, "Delete", defaultCommitAdapter(scene));
11105
+ else scene.applyBatch(ops, "Delete", defaultCommitAdapter(scene, selection.adapterMethods));
10675
11106
  }
10676
11107
  selection.set([]);
10677
11108
  }
10678
11109
  },
10679
- enabled: () => true
11110
+ enabled: requiresSelection
10680
11111
  };
10681
- function requiresSelection(deps) {
10682
- const selection = deps?.selection;
10683
- if (!selection || selection.get().length === 0) {
10684
- return ActionDisabledReason.SelectionRequired;
10685
- }
10686
- return true;
10687
- }
10688
11112
 
10689
11113
  // src/interactions/actions/defaults/clipboard.ts
10690
11114
  var clipboardCopyAction = {
10691
11115
  id: "clipboard.copy",
10692
11116
  label: "Copy",
11117
+ icon: createElement(CopyIcon),
11118
+ group: "clipboard",
10693
11119
  defaultBinding: { kind: "key", key: "c", mods: { mod: true } },
10694
11120
  // Same gate as `duplicate`: a mode that can neither edit the page nor own a
10695
11121
  // selection has nothing to copy. Notably excludes `text-edit`, where Cmd+C
@@ -10709,6 +11135,8 @@ var clipboardCopyAction = {
10709
11135
  var clipboardCutAction = {
10710
11136
  id: "clipboard.cut",
10711
11137
  label: "Cut",
11138
+ icon: createElement(CutIcon),
11139
+ group: "clipboard",
10712
11140
  defaultBinding: { kind: "key", key: "x", mods: { mod: true } },
10713
11141
  eligible: { capability: "edits-page" },
10714
11142
  requires: ["clipboard", "scene", "selection", "applyOps"],
@@ -10726,13 +11154,31 @@ var clipboardCutAction = {
10726
11154
  const ops = buildDeleteOps(scene, ids, "Cut");
10727
11155
  if (ops.length > 0) {
10728
11156
  if (applyOps) applyOps(ops, "Cut");
10729
- else scene.applyBatch(ops, "Cut", defaultCommitAdapter(scene));
11157
+ else scene.applyBatch(ops, "Cut", defaultCommitAdapter(scene, selection.adapterMethods));
10730
11158
  }
10731
11159
  selection.set([]);
10732
11160
  }
10733
11161
  },
10734
11162
  enabled: requiresSelection
10735
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
+ };
10736
11182
 
10737
11183
  // src/interactions/actions/defaults/enterTextEdit.ts
10738
11184
  var enterTextEditAction = {
@@ -10827,8 +11273,6 @@ var selectAllAction = {
10827
11273
  }
10828
11274
  }
10829
11275
  };
10830
-
10831
- // src/interactions/actions/defaults/duplicate.ts
10832
11276
  var DUPLICATE_OFFSET = 12;
10833
11277
  function hasSelectedAncestor(scene, id, set) {
10834
11278
  let parent = scene.get(id)?.parent ?? null;
@@ -10858,6 +11302,8 @@ function copySubtree(scene, id, parent, offset, offsetDescendants, ops, d) {
10858
11302
  var duplicateAction = {
10859
11303
  id: "duplicate",
10860
11304
  label: "Duplicate",
11305
+ icon: createElement(DuplicateIcon),
11306
+ group: "edit",
10861
11307
  defaultBinding: { kind: "key", key: "d", mods: { mod: true } },
10862
11308
  eligible: { capability: ["edits-page", "creates-selection"] },
10863
11309
  // `selection` is read by the `enabled` gate as well as the invoker. An
@@ -10888,7 +11334,7 @@ var duplicateAction = {
10888
11334
  }
10889
11335
  if (ops.length === 0) return;
10890
11336
  if (applyOps) applyOps(ops, "Duplicate");
10891
- else scene.applyBatch(ops, "Duplicate", defaultCommitAdapter(scene));
11337
+ else scene.applyBatch(ops, "Duplicate", defaultCommitAdapter(scene, selection.adapterMethods));
10892
11338
  selection.set(newIds);
10893
11339
  }
10894
11340
  },
@@ -10924,8 +11370,6 @@ function withKitRegistry(registry2) {
10924
11370
  }
10925
11371
  };
10926
11372
  }
10927
-
10928
- // src/interactions/actions/defaults/group.ts
10929
11373
  var containerIdCounter = 0;
10930
11374
  function freshContainerId() {
10931
11375
  return asNodeId(`n${(containerIdCounter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`);
@@ -10933,6 +11377,8 @@ function freshContainerId() {
10933
11377
  var groupAction = {
10934
11378
  id: "group",
10935
11379
  label: "Group",
11380
+ icon: createElement(GroupIcon),
11381
+ group: "structure",
10936
11382
  defaultBinding: { kind: "key", key: "g", mods: { mod: true } },
10937
11383
  eligible: { capability: "edits-page" },
10938
11384
  requires: ["scene", "selection", "applyOps", "poseDescriptor", "poseComposition"],
@@ -10992,15 +11438,17 @@ var groupAction = {
10992
11438
  }));
10993
11439
  }
10994
11440
  if (applyOps) applyOps(ops, "Group");
10995
- else scene.applyBatch(ops, "Group", defaultCommitAdapter(scene));
11441
+ else scene.applyBatch(ops, "Group", defaultCommitAdapter(scene, selection.adapterMethods));
10996
11442
  selection.set([containerId]);
10997
11443
  }
10998
11444
  },
10999
- enabled: () => true
11445
+ enabled: requiresSelection
11000
11446
  };
11001
11447
  var ungroupAction = {
11002
11448
  id: "ungroup",
11003
11449
  label: "Ungroup",
11450
+ icon: createElement(UngroupIcon),
11451
+ group: "structure",
11004
11452
  defaultBinding: { kind: "key", key: "g", mods: { mod: true, shift: true } },
11005
11453
  eligible: { capability: "edits-page" },
11006
11454
  requires: ["scene", "selection", "poseComposition"],
@@ -11041,13 +11489,22 @@ var ungroupAction = {
11041
11489
  });
11042
11490
  }
11043
11491
  },
11044
- 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
+ }
11045
11498
  };
11046
-
11047
- // src/interactions/actions/defaults/undoRedo.ts
11499
+ function historyCan(deps, which) {
11500
+ const history = deps?.history;
11501
+ return history?.[which]?.() ? true : ActionDisabledReason.NotApplicable;
11502
+ }
11048
11503
  var undoAction = {
11049
11504
  id: "undo",
11050
11505
  label: "Undo",
11506
+ icon: createElement(UndoIcon),
11507
+ group: "history",
11051
11508
  defaultBinding: { kind: "key", key: "z", mods: { mod: true } },
11052
11509
  requires: ["history"],
11053
11510
  invoker: {
@@ -11055,11 +11512,14 @@ var undoAction = {
11055
11512
  run: (deps) => {
11056
11513
  deps.history?.undo?.();
11057
11514
  }
11058
- }
11515
+ },
11516
+ enabled: (deps) => historyCan(deps, "canUndo")
11059
11517
  };
11060
11518
  var redoAction = {
11061
11519
  id: "redo",
11062
11520
  label: "Redo",
11521
+ icon: createElement(RedoIcon),
11522
+ group: "history",
11063
11523
  defaultBinding: { kind: "key", key: "z", mods: { mod: true, shift: true } },
11064
11524
  requires: ["history"],
11065
11525
  invoker: {
@@ -11067,7 +11527,8 @@ var redoAction = {
11067
11527
  run: (deps) => {
11068
11528
  deps.history?.redo?.();
11069
11529
  }
11070
- }
11530
+ },
11531
+ enabled: (deps) => historyCan(deps, "canRedo")
11071
11532
  };
11072
11533
 
11073
11534
  // src/interactions/actions/flip/helpers.ts
@@ -11171,8 +11632,8 @@ function localMirror(axis, frame, id, pivotWorld) {
11171
11632
  rotation: 0
11172
11633
  });
11173
11634
  const theta = at.rotation ?? 0;
11174
- const mirror2 = axis === "x" ? [-1, 0, 0, 1, 0, 0] : [1, 0, 0, -1, 0, 0];
11175
- 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));
11176
11637
  return multiply$1(translate$1(at.x, at.y), multiply$1(turned, translate$1(-at.x, -at.y)));
11177
11638
  }
11178
11639
  function flipSelection(selection, scene, axis, pivot, applyOps, geometryProjection, geom, poseComposition) {
@@ -11207,11 +11668,16 @@ function flipSelection(selection, scene, axis, pivot, applyOps, geometryProjecti
11207
11668
  }
11208
11669
  if (ops.length === 0) return;
11209
11670
  if (applyOps) applyOps(ops, "Flip");
11210
- else scene.applyBatch(ops, "Flip", defaultCommitAdapter(scene));
11671
+ else scene.applyBatch(ops, "Flip", defaultCommitAdapter(scene, selection.adapterMethods));
11211
11672
  }
11212
11673
  var flipAction = {
11213
11674
  id: "flip",
11214
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
+ ],
11215
11681
  defaultBinding: [
11216
11682
  { spec: { kind: "key", key: ["h", "H"], mods: { shift: true } }, opts: { params: { axis: "x" } } },
11217
11683
  { spec: { kind: "key", key: ["v", "V"], mods: { shift: true } }, opts: { params: { axis: "y" } } }
@@ -11294,7 +11760,7 @@ function nudgeSelection(selection, scene, dx, dy, applyOps, geometryProjection,
11294
11760
  }
11295
11761
  if (ops.length === 0) return;
11296
11762
  if (applyOps) applyOps(ops, "Nudge");
11297
- else scene.applyBatch(ops, "Nudge", defaultCommitAdapter(scene));
11763
+ else scene.applyBatch(ops, "Nudge", defaultCommitAdapter(scene, selection.adapterMethods));
11298
11764
  }
11299
11765
  function makeNudgeAction(dir2) {
11300
11766
  return {
@@ -11524,20 +11990,22 @@ function createMoveToIndexOp(args) {
11524
11990
  };
11525
11991
  }
11526
11992
  registerOpFactory("moveToIndex", (args) => createMoveToIndexOp(args));
11527
-
11528
- // src/interactions/actions/defaults/reorder.ts
11529
11993
  function reorderSelection(selection, scene, direction, distance, applyOps) {
11530
11994
  const ids = selection.get();
11531
11995
  if (ids.length === 0) return;
11532
11996
  const reorderDir = direction === "forward" ? distance === "extreme" ? "front" : "forward" : distance === "extreme" ? "back" : "backward";
11533
11997
  const op = createReorderOp({ ids, direction: reorderDir, label: "Reorder" });
11534
11998
  if (applyOps) applyOps([op], "Reorder");
11535
- else scene.applyBatch([op], "Reorder", defaultCommitAdapter(scene));
11999
+ else scene.applyBatch([op], "Reorder", defaultCommitAdapter(scene, selection.adapterMethods));
11536
12000
  }
11537
12001
  var reorderForwardAction = {
11538
12002
  id: "reorder.forward",
11539
12003
  label: "Bring Forward",
11540
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
+ ],
11541
12009
  defaultBinding: [
11542
12010
  {
11543
12011
  // Modifier matching is strict, so Shift must be absent here — listing
@@ -11583,6 +12051,10 @@ var reorderBackwardAction = {
11583
12051
  id: "reorder.backward",
11584
12052
  label: "Send Backward",
11585
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
+ ],
11586
12058
  defaultBinding: [
11587
12059
  {
11588
12060
  spec: { kind: "key", key: ["["], mods: { mod: true } },
@@ -11615,7 +12087,7 @@ var reorderBackwardAction = {
11615
12087
  },
11616
12088
  enabled: requiresSelection
11617
12089
  };
11618
- var SVG_BASE = {
12090
+ var SVG_BASE3 = {
11619
12091
  viewBox: "0 0 16 16",
11620
12092
  width: 16,
11621
12093
  height: 16,
@@ -11628,42 +12100,42 @@ var AXIS = {
11628
12100
  fill: "none"
11629
12101
  };
11630
12102
  function AlignLeftIcon() {
11631
- return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE, children: [
12103
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE3, children: [
11632
12104
  /* @__PURE__ */ jsx("line", { x1: "2", y1: "2", x2: "2", y2: "14", ...AXIS }),
11633
12105
  /* @__PURE__ */ jsx("rect", { x: "2.5", y: "3", width: "9", height: "3" }),
11634
12106
  /* @__PURE__ */ jsx("rect", { x: "2.5", y: "9", width: "6", height: "3" })
11635
12107
  ] });
11636
12108
  }
11637
12109
  function AlignRightIcon() {
11638
- return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE, children: [
12110
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE3, children: [
11639
12111
  /* @__PURE__ */ jsx("line", { x1: "14", y1: "2", x2: "14", y2: "14", ...AXIS }),
11640
12112
  /* @__PURE__ */ jsx("rect", { x: "4.5", y: "3", width: "9", height: "3" }),
11641
12113
  /* @__PURE__ */ jsx("rect", { x: "7.5", y: "9", width: "6", height: "3" })
11642
12114
  ] });
11643
12115
  }
11644
12116
  function AlignTopIcon() {
11645
- return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE, children: [
12117
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE3, children: [
11646
12118
  /* @__PURE__ */ jsx("line", { x1: "2", y1: "2", x2: "14", y2: "2", ...AXIS }),
11647
12119
  /* @__PURE__ */ jsx("rect", { x: "3", y: "2.5", width: "3", height: "9" }),
11648
12120
  /* @__PURE__ */ jsx("rect", { x: "9", y: "2.5", width: "3", height: "6" })
11649
12121
  ] });
11650
12122
  }
11651
12123
  function AlignBottomIcon() {
11652
- return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE, children: [
12124
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE3, children: [
11653
12125
  /* @__PURE__ */ jsx("line", { x1: "2", y1: "14", x2: "14", y2: "14", ...AXIS }),
11654
12126
  /* @__PURE__ */ jsx("rect", { x: "3", y: "4.5", width: "3", height: "9" }),
11655
12127
  /* @__PURE__ */ jsx("rect", { x: "9", y: "7.5", width: "3", height: "6" })
11656
12128
  ] });
11657
12129
  }
11658
12130
  function AlignCenterXIcon() {
11659
- return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE, children: [
12131
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE3, children: [
11660
12132
  /* @__PURE__ */ jsx("line", { x1: "8", y1: "2", x2: "8", y2: "14", ...AXIS }),
11661
12133
  /* @__PURE__ */ jsx("rect", { x: "3.5", y: "3", width: "9", height: "3" }),
11662
12134
  /* @__PURE__ */ jsx("rect", { x: "5", y: "9", width: "6", height: "3" })
11663
12135
  ] });
11664
12136
  }
11665
12137
  function AlignCenterYIcon() {
11666
- return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE, children: [
12138
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE3, children: [
11667
12139
  /* @__PURE__ */ jsx("line", { x1: "2", y1: "8", x2: "14", y2: "8", ...AXIS }),
11668
12140
  /* @__PURE__ */ jsx("rect", { x: "3", y: "3.5", width: "3", height: "9" }),
11669
12141
  /* @__PURE__ */ jsx("rect", { x: "9", y: "5", width: "3", height: "6" })
@@ -11777,7 +12249,7 @@ function planDistribute(bounds, axis, mode) {
11777
12249
  }
11778
12250
  return targets;
11779
12251
  }
11780
- var SVG_BASE2 = {
12252
+ var SVG_BASE4 = {
11781
12253
  viewBox: "0 0 16 16",
11782
12254
  width: 16,
11783
12255
  height: 16,
@@ -11785,14 +12257,14 @@ var SVG_BASE2 = {
11785
12257
  "aria-hidden": true
11786
12258
  };
11787
12259
  function DistributeHorizontalIcon() {
11788
- return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE2, children: [
12260
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE4, children: [
11789
12261
  /* @__PURE__ */ jsx("rect", { x: "1", y: "4", width: "3", height: "8" }),
11790
12262
  /* @__PURE__ */ jsx("rect", { x: "6.5", y: "4", width: "3", height: "8" }),
11791
12263
  /* @__PURE__ */ jsx("rect", { x: "12", y: "4", width: "3", height: "8" })
11792
12264
  ] });
11793
12265
  }
11794
12266
  function DistributeVerticalIcon() {
11795
- return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE2, children: [
12267
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE4, children: [
11796
12268
  /* @__PURE__ */ jsx("rect", { x: "4", y: "1", width: "8", height: "3" }),
11797
12269
  /* @__PURE__ */ jsx("rect", { x: "4", y: "6.5", width: "8", height: "3" }),
11798
12270
  /* @__PURE__ */ jsx("rect", { x: "4", y: "12", width: "8", height: "3" })
@@ -11962,7 +12434,7 @@ function applyBooleanOp(adapter, op) {
11962
12434
  dispatchApplyBatch(adapter, ops, LABEL[op]);
11963
12435
  return { kind: "applied", resultIds: newNodes.map((n) => n.id) };
11964
12436
  }
11965
- var SVG_BASE3 = {
12437
+ var SVG_BASE5 = {
11966
12438
  viewBox: "0 0 20 20",
11967
12439
  width: 20,
11968
12440
  height: 20,
@@ -11972,7 +12444,7 @@ var SVG_BASE3 = {
11972
12444
  "aria-hidden": true
11973
12445
  };
11974
12446
  function UnionIcon() {
11975
- return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE3, children: [
12447
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE5, children: [
11976
12448
  /* @__PURE__ */ jsx("circle", { cx: "7", cy: "10", r: "5", fill: "currentColor", stroke: "none" }),
11977
12449
  /* @__PURE__ */ jsx("circle", { cx: "13", cy: "10", r: "5", fill: "currentColor", stroke: "none" }),
11978
12450
  /* @__PURE__ */ jsx("circle", { cx: "7", cy: "10", r: "5" }),
@@ -11980,7 +12452,7 @@ function UnionIcon() {
11980
12452
  ] });
11981
12453
  }
11982
12454
  function IntersectIcon() {
11983
- return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE3, children: [
12455
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE5, children: [
11984
12456
  /* @__PURE__ */ jsx("circle", { cx: "7", cy: "10", r: "5" }),
11985
12457
  /* @__PURE__ */ jsx("circle", { cx: "13", cy: "10", r: "5" }),
11986
12458
  /* @__PURE__ */ jsx(
@@ -11994,7 +12466,7 @@ function IntersectIcon() {
11994
12466
  ] });
11995
12467
  }
11996
12468
  function SubtractIcon() {
11997
- return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE3, children: [
12469
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE5, children: [
11998
12470
  /* @__PURE__ */ jsx("circle", { cx: "7", cy: "10", r: "5" }),
11999
12471
  /* @__PURE__ */ jsx("circle", { cx: "13", cy: "10", r: "5" }),
12000
12472
  /* @__PURE__ */ jsx(
@@ -12008,7 +12480,7 @@ function SubtractIcon() {
12008
12480
  ] });
12009
12481
  }
12010
12482
  function ExcludeIcon() {
12011
- return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE3, children: [
12483
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE5, children: [
12012
12484
  /* @__PURE__ */ jsx("circle", { cx: "7", cy: "10", r: "5" }),
12013
12485
  /* @__PURE__ */ jsx("circle", { cx: "13", cy: "10", r: "5" }),
12014
12486
  /* @__PURE__ */ jsx(
@@ -12023,14 +12495,14 @@ function ExcludeIcon() {
12023
12495
  ] });
12024
12496
  }
12025
12497
  function DivideIcon() {
12026
- return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE3, children: [
12498
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE5, children: [
12027
12499
  /* @__PURE__ */ jsx("circle", { cx: "7", cy: "10", r: "5" }),
12028
12500
  /* @__PURE__ */ jsx("circle", { cx: "13", cy: "10", r: "5" }),
12029
12501
  /* @__PURE__ */ jsx("line", { x1: "10", y1: "6", x2: "10", y2: "14" })
12030
12502
  ] });
12031
12503
  }
12032
12504
  function CropIcon() {
12033
- return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE3, children: [
12505
+ return /* @__PURE__ */ jsxs("svg", { ...SVG_BASE5, children: [
12034
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" }) }) }),
12035
12507
  /* @__PURE__ */ jsx("circle", { cx: "9", cy: "10", r: "5" }),
12036
12508
  /* @__PURE__ */ jsx(
@@ -12486,7 +12958,7 @@ function createPaintAction(spec) {
12486
12958
  }
12487
12959
  if (ops.length > 0) {
12488
12960
  if (scratch.applyOps) scratch.applyOps(ops, spec.label);
12489
- else scratch.scene.applyBatch(ops, spec.label, defaultCommitAdapter(scratch.scene));
12961
+ else scratch.scene.applyBatch(ops, spec.label, defaultCommitAdapter(scratch.scene, selection.adapterMethods));
12490
12962
  }
12491
12963
  scratch.previews.clear();
12492
12964
  },
@@ -12537,14 +13009,23 @@ function mergeAlphaFromPrev(picked, prev) {
12537
13009
  // src/util/paint.ts
12538
13010
  var DEFAULT_FILL_COLOR = "#ffffffff";
12539
13011
  var DEFAULT_STROKE_COLOR = "#000000ff";
12540
- var DEFAULT_PALETTE = [
12541
- "#7fb069",
12542
- "#d4a574",
12543
- "#a48bd4",
12544
- "#7ab8d4",
12545
- "#d47a7a"
12546
- ];
12547
- 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);
12548
13029
  function solid(color) {
12549
13030
  const a = getAlpha01(color);
12550
13031
  const rgb = color.length === 9 && color.startsWith("#") ? color.slice(0, 7) : color;
@@ -13047,7 +13528,7 @@ var ingestAction = {
13047
13528
  insert,
13048
13529
  applyOps: (ops, label) => {
13049
13530
  if (applyOps) applyOps(ops, label ?? "Ingest");
13050
- else scene.applyBatch(ops, label ?? "Ingest", { ...defaultCommitAdapter(scene), ...selection.adapterMethods });
13531
+ else scene.applyBatch(ops, label ?? "Ingest", defaultCommitAdapter(scene, selection.adapterMethods));
13051
13532
  },
13052
13533
  scene,
13053
13534
  selection,
@@ -13074,8 +13555,8 @@ var KIT_STANDARD_DESCRIPTORS = [
13074
13555
  escapeAction,
13075
13556
  cancelGestureAction,
13076
13557
  selectAllAction,
13077
- deleteAction,
13078
13558
  duplicateAction,
13559
+ deleteAction,
13079
13560
  groupAction,
13080
13561
  ungroupAction,
13081
13562
  undoAction,
@@ -13088,11 +13569,11 @@ var KIT_STANDARD_DESCRIPTORS = [
13088
13569
  reorderForwardAction,
13089
13570
  reorderBackwardAction,
13090
13571
  alignLeftAction,
13572
+ alignCenterXAction,
13091
13573
  alignRightAction,
13092
13574
  alignTopAction,
13093
- alignBottomAction,
13094
- alignCenterXAction,
13095
13575
  alignCenterYAction,
13576
+ alignBottomAction,
13096
13577
  distributeHorizontalAction,
13097
13578
  distributeVerticalAction,
13098
13579
  pathfinderUnionAction,
@@ -13130,8 +13611,9 @@ var KIT_STANDARD_DESCRIPTORS = [
13130
13611
  setFillOpacityAction,
13131
13612
  setStrokeOpacityAction,
13132
13613
  ingestAction,
13614
+ clipboardCutAction,
13133
13615
  clipboardCopyAction,
13134
- clipboardCutAction
13616
+ clipboardPasteAction
13135
13617
  ];
13136
13618
  var KIT_STANDARD_ACTION_IDS = KIT_STANDARD_DESCRIPTORS.map((a) => a.id);
13137
13619
  function useStandardActions(opts) {
@@ -14104,7 +14586,7 @@ function createTimeline(register, id, opts) {
14104
14586
  let playhead = 0;
14105
14587
  let prevPlayhead = -Infinity;
14106
14588
  let done = false;
14107
- let live = true;
14589
+ let live2 = true;
14108
14590
  let cancelled = false;
14109
14591
  let wantPaused = opts.autoplay === false;
14110
14592
  let wantScale = 1;
@@ -14192,12 +14674,12 @@ function createTimeline(register, id, opts) {
14192
14674
  opts.onDone?.();
14193
14675
  }
14194
14676
  if (playhead < duration) return false;
14195
- live = false;
14677
+ live2 = false;
14196
14678
  return true;
14197
14679
  };
14198
14680
  const onCancel = () => {
14199
14681
  cancelled = true;
14200
- live = false;
14682
+ live2 = false;
14201
14683
  booker?.retract();
14202
14684
  };
14203
14685
  const base = register({ id, cancelKey: opts.cancelKey, tick, onCancel });
@@ -14205,11 +14687,11 @@ function createTimeline(register, id, opts) {
14205
14687
  const rearm = () => {
14206
14688
  if (cancelled || playhead >= duration) return;
14207
14689
  done = false;
14208
- if (live) return;
14690
+ if (live2) return;
14209
14691
  booker?.rearm();
14210
14692
  lastVirtual = 0;
14211
14693
  offset = playhead;
14212
- live = true;
14694
+ live2 = true;
14213
14695
  register({ id, cancelKey: opts.cancelKey, keepExisting: true, tick, onCancel });
14214
14696
  if (wantPaused) base.pause();
14215
14697
  base.setTimeScale(wantScale);
@@ -14237,8 +14719,8 @@ function createTimeline(register, id, opts) {
14237
14719
  wantScale = s;
14238
14720
  base.setTimeScale(s);
14239
14721
  },
14240
- timeScale: () => live ? base.timeScale() : wantScale,
14241
- isPaused: () => live ? base.isPaused() : wantPaused,
14722
+ timeScale: () => live2 ? base.timeScale() : wantScale,
14723
+ isPaused: () => live2 ? base.isPaused() : wantPaused,
14242
14724
  seek(t) {
14243
14725
  booker?.retract();
14244
14726
  offset = t - lastVirtual;
@@ -15650,17 +16132,53 @@ function resolveFillPattern(fill) {
15650
16132
  }
15651
16133
 
15652
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;
15653
16139
  var cache6 = /* @__PURE__ */ new Map();
15654
16140
  var subscribers = /* @__PURE__ */ new Set();
15655
- 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() {
15656
16148
  if (typeof Image === "undefined" || typeof createImageBitmap === "undefined") {
15657
16149
  throw new Error("weasel imageCache: no DOM image loader in this environment");
15658
16150
  }
16151
+ }
16152
+ async function decodeImage(src) {
15659
16153
  const img = new Image();
15660
16154
  img.crossOrigin = "anonymous";
15661
16155
  img.src = src;
15662
16156
  await img.decode();
15663
- 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));
15664
16182
  };
15665
16183
  var loader = defaultLoader;
15666
16184
  function notify() {
@@ -15672,7 +16190,11 @@ function startLoad(src) {
15672
16190
  loader(src).then(
15673
16191
  (bitmap) => {
15674
16192
  if (cache6.get(src) !== entry) return;
15675
- 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
+ });
15676
16198
  notify();
15677
16199
  },
15678
16200
  () => {
@@ -15682,11 +16204,54 @@ function startLoad(src) {
15682
16204
  }
15683
16205
  );
15684
16206
  }
15685
- 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) {
15686
16248
  const entry = cache6.get(src);
15687
- if (entry) return entry.bitmap;
15688
- startLoad(src);
15689
- 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;
15690
16255
  }
15691
16256
  function imageStatus(src) {
15692
16257
  return cache6.get(src)?.status ?? "idle";
@@ -16008,6 +16573,21 @@ function pathForShape(d, p) {
16008
16573
  return { kind: "rect", x: p.x, y: p.y, width: p.width, height: p.height };
16009
16574
  }
16010
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
+ }
16011
16591
  var IMAGE_PAINTER = {
16012
16592
  id: "kit:image",
16013
16593
  matches: (node) => {
@@ -16018,7 +16598,7 @@ var IMAGE_PAINTER = {
16018
16598
  paint: (node, pose, ctx) => {
16019
16599
  const d = node.data;
16020
16600
  const p = pose;
16021
- 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));
16022
16602
  if (bmp) {
16023
16603
  return [{
16024
16604
  kind: "image",
@@ -16027,7 +16607,10 @@ var IMAGE_PAINTER = {
16027
16607
  y: p.y,
16028
16608
  w: p.width,
16029
16609
  h: p.height,
16030
- ...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 } : {}
16031
16614
  }];
16032
16615
  }
16033
16616
  const error = ctx?.resolveImage ? false : imageStatus(d.image.src) === "error";
@@ -16273,9 +16856,13 @@ function scenePickSource(scene, opts = {}) {
16273
16856
  const n = scene.get(asNodeId(id));
16274
16857
  return n ? effectivePose(scene, n) : void 0;
16275
16858
  },
16276
- getParent: (id) => scene.get(asNodeId(id))?.parent ?? null
16277
- } : null;
16278
- const poseOf = getPose ? (node) => getPose(node.id) : chain ? (node) => composeWorldPose(chain, node.id, composition.compose) : (node) => effectivePose(scene, node);
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
+ }
16864
+ } : null;
16865
+ const poseOf = getPose ? (node) => getPose(node.id) : chain ? (node) => composeWorldPose(chain, node.id, composition.compose) : (node) => effectivePose(scene, node);
16279
16866
  const childrenOf = (id) => scene.childrenOf(id);
16280
16867
  const depOf2 = (id) => {
16281
16868
  const dep = scene.get(id);
@@ -16758,6 +17345,14 @@ useTextTool.options = {
16758
17345
  description: "Raise or lower the range off the baseline, at a smaller size.",
16759
17346
  default: void 0
16760
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
+ },
16761
17356
  fontFamily: {
16762
17357
  kind: "font-family",
16763
17358
  name: "Font",
@@ -16867,44 +17462,9 @@ function anchorCount(s) {
16867
17462
  return (s.current ? s.current.anchors.length : 0) + s.finishedSubpaths.reduce((n, sp) => n + sp.anchors.length, 0);
16868
17463
  }
16869
17464
  function buildPath(subpaths, trailing) {
16870
- const b = new PathBuilder();
16871
17465
  const all = [...subpaths];
16872
17466
  if (trailing && trailing.anchors.length > 0) all.push(trailing);
16873
- for (const sp of all) {
16874
- if (sp.anchors.length === 0) continue;
16875
- const first = sp.anchors[0];
16876
- b.moveTo(first.x, first.y);
16877
- for (let i = 1; i < sp.anchors.length; i++) {
16878
- const prev = sp.anchors[i - 1];
16879
- const curr = sp.anchors[i];
16880
- const out = prev.outHandle;
16881
- const inH = curr.inHandle ?? mirrorHandle(prev, out);
16882
- if (out || curr.inHandle) {
16883
- const c1 = out ?? { x: prev.x, y: prev.y };
16884
- const c2 = inH ?? { x: curr.x, y: curr.y };
16885
- b.curveTo(c1.x, c1.y, c2.x, c2.y, curr.x, curr.y);
16886
- } else {
16887
- b.lineTo(curr.x, curr.y);
16888
- }
16889
- }
16890
- if (sp.closed) {
16891
- const last = sp.anchors[sp.anchors.length - 1];
16892
- const first0 = sp.anchors[0];
16893
- const out = last.outHandle;
16894
- const inH = first0.inHandle ?? mirrorHandle(last, out);
16895
- if (out || first0.inHandle) {
16896
- const c1 = out ?? { x: last.x, y: last.y };
16897
- const c2 = inH ?? { x: first0.x, y: first0.y };
16898
- b.curveTo(c1.x, c1.y, c2.x, c2.y, first0.x, first0.y);
16899
- }
16900
- b.close();
16901
- }
16902
- }
16903
- return b.build();
16904
- }
16905
- function mirrorHandle(anchor, out) {
16906
- if (!out || anchor.altBroken) return void 0;
16907
- 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));
16908
17468
  }
16909
17469
  function usePenTool(options) {
16910
17470
  const { wrapPath, adapter, autoSelect = true, autoCommitOnClose = true, closeHitRadius = 8, anchorSnapRadius = 8, snapPoint: snapPoint2 } = options;
@@ -16949,6 +17509,64 @@ function usePenTool(options) {
16949
17509
  if (optsRef.current.autoSelect) optsRef.current.adapter.setSelection([id]);
16950
17510
  resetScratch(s);
16951
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
+ }, []);
16952
17570
  const pickUpEndpoint = useCallback((deps, wx, wy) => {
16953
17571
  const scratch = scratchRef.current;
16954
17572
  if (scratch.current || scratch.finishedSubpaths.length > 0) return false;
@@ -16971,9 +17589,9 @@ function usePenTool(options) {
16971
17589
  const radius = optsRef.current.anchorSnapRadius;
16972
17590
  if (radius > 0) {
16973
17591
  const hit = nearestPathAnchor(pathsNear(deps, x, y, radius), { x, y }, radius, viewScale(deps));
16974
- if (hit) return { x: hit.x, y: hit.y };
17592
+ if (hit) return { x: hit.x, y: hit.y, hit };
16975
17593
  }
16976
- return snap2(x, y);
17594
+ return { ...snap2(x, y), hit: null };
16977
17595
  }, [snap2]);
16978
17596
  const withinCloseRadius = useCallback(
16979
17597
  (deps, ax, ay, wx, wy) => {
@@ -16988,7 +17606,7 @@ function usePenTool(options) {
16988
17606
  id: "pen.placeAnchor",
16989
17607
  label: "Pen \u2014 place anchor",
16990
17608
  eligible: { capability: "creates-paths" },
16991
- requires: ["view", "areaSelect", "editAnchors"],
17609
+ requires: ["view", "areaSelect", "editAnchors", "scene", "applyOps"],
16992
17610
  invoker: {
16993
17611
  timing: "immediate",
16994
17612
  run: (deps, params) => {
@@ -16998,7 +17616,7 @@ function usePenTool(options) {
16998
17616
  forceRenderRef.current();
16999
17617
  return;
17000
17618
  }
17001
- const { x: wx, y: wy } = placeAt(deps, p.pressX, p.pressY);
17619
+ const { x: wx, y: wy, hit } = placeAt(deps, p.pressX, p.pressY);
17002
17620
  const scratch = s();
17003
17621
  if (scratch.current && scratch.current.anchors.length >= 3) {
17004
17622
  const first = scratch.current.anchors[0];
@@ -17012,8 +17630,10 @@ function usePenTool(options) {
17012
17630
  return;
17013
17631
  }
17014
17632
  }
17633
+ const join = joinable(scratch, deps, hit);
17015
17634
  if (!scratch.current) scratch.current = { anchors: [], closed: false };
17016
17635
  scratch.current.anchors.push({ x: wx, y: wy });
17636
+ if (join) commitJoin(scratch, deps, hit);
17017
17637
  forceRenderRef.current();
17018
17638
  }
17019
17639
  }
@@ -17045,32 +17665,35 @@ function usePenTool(options) {
17045
17665
  id: "pen.dragHandle",
17046
17666
  label: "Pen \u2014 drag out a bezier handle",
17047
17667
  eligible: { capability: "creates-paths" },
17048
- requires: ["view", "areaSelect", "editAnchors"],
17668
+ requires: ["view", "areaSelect", "editAnchors", "scene", "applyOps"],
17049
17669
  invoker: {
17050
17670
  timing: "ongoing",
17051
17671
  start: (ctx) => {
17052
17672
  const scratch = s();
17053
17673
  const origin = ctx.drag?.start ?? ctx.world;
17054
17674
  const pickedUp = pickUpEndpoint(ctx.deps, origin.x, origin.y);
17675
+ let joinHit = null;
17055
17676
  if (!pickedUp) {
17056
- 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;
17057
17679
  if (!scratch.current) scratch.current = { anchors: [], closed: false };
17058
17680
  scratch.current.anchors.push({ x: ax, y: ay });
17059
17681
  }
17060
17682
  scratch.draggingHandleAt = scratch.current.anchors.length - 1;
17061
- applyOutHandle(scratch, ctx.world, ctx.modifiers.shift, optsRef.current.snapPoint);
17062
17683
  if (ctx.modifiers.alt) {
17063
17684
  scratch.current.anchors[scratch.draggingHandleAt].altBroken = true;
17064
17685
  }
17686
+ const linked = !pickedUp;
17687
+ applyOutHandle(scratch, ctx.world, ctx.modifiers.shift, linked, optsRef.current.snapPoint);
17065
17688
  forceRenderRef.current();
17066
17689
  return {
17067
17690
  onMove: (moveCtx) => {
17068
17691
  const sm = s();
17069
17692
  if (sm.draggingHandleAt === null) return;
17070
- applyOutHandle(sm, moveCtx.world, moveCtx.modifiers.shift, optsRef.current.snapPoint);
17071
17693
  if (moveCtx.modifiers.alt && sm.current) {
17072
17694
  sm.current.anchors[sm.draggingHandleAt].altBroken = true;
17073
17695
  }
17696
+ applyOutHandle(sm, moveCtx.world, moveCtx.modifiers.shift, linked, optsRef.current.snapPoint);
17074
17697
  forceRenderRef.current();
17075
17698
  },
17076
17699
  onEnd: (endCtx, reason) => {
@@ -17087,12 +17710,13 @@ function usePenTool(options) {
17087
17710
  return;
17088
17711
  }
17089
17712
  if (se.draggingHandleAt !== null) {
17090
- applyOutHandle(se, endCtx.world, endCtx.modifiers.shift, optsRef.current.snapPoint);
17091
17713
  if (endCtx.modifiers.alt && se.current) {
17092
17714
  se.current.anchors[se.draggingHandleAt].altBroken = true;
17093
17715
  }
17716
+ applyOutHandle(se, endCtx.world, endCtx.modifiers.shift, linked, optsRef.current.snapPoint);
17094
17717
  se.draggingHandleAt = null;
17095
17718
  }
17719
+ if (joinHit && se.current) commitJoin(se, ctx.deps, joinHit);
17096
17720
  forceRenderRef.current();
17097
17721
  }
17098
17722
  };
@@ -17133,7 +17757,7 @@ function usePenTool(options) {
17133
17757
  }
17134
17758
  }
17135
17759
  ];
17136
- }, [commit, placeAt, withinCloseRadius, pickUpEndpoint]);
17760
+ }, [commit, placeAt, withinCloseRadius, pickUpEndpoint, joinable, commitJoin]);
17137
17761
  return useMemo(() => {
17138
17762
  return defineTool({
17139
17763
  id: "pen",
@@ -17191,7 +17815,7 @@ usePenTool.prefs = {
17191
17815
  }
17192
17816
  }
17193
17817
  };
17194
- function applyOutHandle(s, target, shift, snap2) {
17818
+ function applyOutHandle(s, target, shift, linked, snap2) {
17195
17819
  if (s.current === null || s.draggingHandleAt === null) return;
17196
17820
  const anchor = s.current.anchors[s.draggingHandleAt];
17197
17821
  const to = snap2 ? snap2(target) : target;
@@ -17203,6 +17827,7 @@ function applyOutHandle(s, target, shift, snap2) {
17203
17827
  dy = c.dy;
17204
17828
  }
17205
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 };
17206
17831
  }
17207
17832
  function viewScale(deps) {
17208
17833
  return deps.view?.get().scale ?? { x: 1, y: 1 };
@@ -17655,9 +18280,24 @@ function usePencilTool() {
17655
18280
  }
17656
18281
 
17657
18282
  // src/canvas/defaultDrawOne.ts
17658
- 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) {
17659
18298
  const painter = findNodeShape(node);
17660
- 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) : [];
17661
18301
  const data = node.data;
17662
18302
  if (data?.label && data.text == null) {
17663
18303
  const p = pose;
@@ -18723,11 +19363,11 @@ function buildSceneTree(adapter, drawOne, view, forLayer, derivedPathOf) {
18723
19363
  const buckets = /* @__PURE__ */ new Map();
18724
19364
  for (const l of layers) buckets.set(l.id, []);
18725
19365
  const compose2 = adapter.composePose?.bind(adapter);
18726
- function visit(id, ancestorClips, parentWorld) {
19366
+ function visit(id, ancestorClips, parentFrame) {
18727
19367
  const node = adapter.getNode(id);
18728
19368
  if (!node) return;
18729
19369
  const local = adapter.getPose(id);
18730
- const pose = compose2 && parentWorld !== null ? compose2(parentWorld, local) : local;
19370
+ const pose = compose2 && parentFrame !== null ? compose2(parentFrame, local) : local;
18731
19371
  const paints = forLayer === void 0 || node.layer === forLayer;
18732
19372
  const self = paints ? drawOne(node, pose, view) : [];
18733
19373
  let ownClips = ancestorClips;
@@ -18751,7 +19391,8 @@ function buildSceneTree(adapter, drawOne, view, forLayer, derivedPathOf) {
18751
19391
  bucket.push(ownClips.length > 0 ? wrapInClips(self, ownClips) : { kind: "group", children: self });
18752
19392
  }
18753
19393
  }
18754
- 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);
18755
19396
  }
18756
19397
  for (const rootId of adapter.getChildren(null)) visit(rootId, [], null);
18757
19398
  const out = [];
@@ -19961,191 +20602,613 @@ function wireSceneSlotToScene(slot, scene, alphaFor, colorOverrides) {
19961
20602
  alphaFor: composeAlphaFor(scene, alphaFor)
19962
20603
  };
19963
20604
  }
19964
-
19965
- // src/core/scene/poseOverrides.ts
19966
- function createPoseOverrides(getNode, onInvalidate) {
19967
- const entries = /* @__PURE__ */ new Map();
19968
- const listeners = /* @__PURE__ */ new Set();
19969
- let generation2 = 0;
19970
- function invalidate(id) {
19971
- const node = getNode(id);
19972
- if (node) dropPoseKeyedMemoSlots(node);
19973
- 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;
19974
20610
  }
19975
- function published() {
19976
- generation2++;
19977
- 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;
19978
20632
  }
19979
- return {
19980
- set(id, entry) {
19981
- entries.set(id, entry);
19982
- invalidate(id);
19983
- 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));
19984
20715
  },
19985
- get(id) {
19986
- 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;
19987
20723
  },
19988
- has(id) {
19989
- 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);
19990
20729
  },
19991
- ids() {
19992
- return [...entries.keys()];
20730
+ getParent(id) {
20731
+ const n = scene.get(asNodeId(id));
20732
+ return n?.parent ?? null;
19993
20733
  },
19994
- clear(id) {
19995
- if (!entries.delete(id)) return;
19996
- invalidate(id);
19997
- published();
20734
+ definesFrame(id) {
20735
+ const n = scene.get(asNodeId(id));
20736
+ return n === void 0 || definesFrame(n);
19998
20737
  },
19999
- clearAll() {
20000
- if (entries.size === 0) return;
20001
- for (const id of entries.keys()) invalidate(id);
20002
- entries.clear();
20003
- published();
20738
+ getWorldPose(id) {
20739
+ if (!composes) return adapter.getPose(id);
20740
+ return composeWorldPose(adapter, id, composition.compose);
20004
20741
  },
20005
- commit() {
20006
- for (const id of entries.keys()) invalidate(id);
20007
- 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);
20008
20776
  },
20009
- subscribe(fn) {
20010
- listeners.add(fn);
20011
- return () => {
20012
- listeners.delete(fn);
20013
- };
20777
+ setParent(id, parentId) {
20778
+ scene.move(asNodeId(id), parentId === null ? null : asNodeId(parentId));
20014
20779
  },
20015
- getGeneration() {
20016
- return generation2;
20017
- }
20018
- };
20019
- }
20020
-
20021
- // src/core/scene/scene.ts
20022
- var defaultIdCounter = 0;
20023
- var defaultGenerateId = () => asNodeId(`n${(defaultIdCounter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`);
20024
- function layerFromSerialized(spec) {
20025
- const base = {
20026
- id: spec.id,
20027
- visible: spec.visible ?? true,
20028
- locked: spec.locked ?? false
20029
- };
20030
- return spec.kind === "user" ? { ...base, kind: "user", name: spec.name ?? spec.id } : { ...base, kind: "system" };
20031
- }
20032
- function createScene(options) {
20033
- if (!options.systemLayers || options.systemLayers.length === 0) {
20034
- throw new Error("useScene: systemLayers must declare at least one layer");
20035
- }
20036
- const state = {
20037
- nodes: /* @__PURE__ */ new Map(),
20038
- roots: [],
20039
- layers: [],
20040
- layerIndex: /* @__PURE__ */ new Map()
20041
- };
20042
- const registry2 = withKitRegistry(options.registry ?? {});
20043
- const fnFields = createNodeFnFields(registry2);
20044
- const fnField = (name) => fnFields.find((f) => f.spec.field === name);
20045
- const dependents = createDependentsIndex();
20046
- const childDerived = /* @__PURE__ */ new Set();
20047
- function invalidateDependents(id) {
20048
- invalidateChildDerivedFrom(state.nodes.get(id)?.parent ?? null);
20049
- if (dependents.isEmpty()) return;
20050
- const subtree = [id];
20051
- descendants(id, subtree);
20052
- for (const src of subtree) {
20053
- for (const dep of dependents.transitiveDependentsOf(src)) {
20054
- const node = state.nodes.get(dep);
20055
- if (node !== void 0) dropPoseKeyedMemoSlots(node);
20056
- }
20057
- }
20058
- }
20059
- function invalidateChildDerivedFrom(start) {
20060
- if (childDerived.size === 0) return;
20061
- let cursor = start;
20062
- while (cursor !== null) {
20063
- const node = state.nodes.get(cursor);
20064
- if (node === void 0) return;
20065
- if (childDerived.has(cursor)) {
20066
- dropPoseKeyedMemoSlots(node);
20067
- for (const dep of dependents.transitiveDependentsOf(cursor)) {
20068
- const d = state.nodes.get(dep);
20069
- 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);
20070
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);
20071
20810
  }
20072
- cursor = node.parent;
20073
- }
20074
- }
20075
- function trackChildDerived(id, dependsOn) {
20076
- if (dependsOn === "children") childDerived.add(id);
20077
- else childDerived.delete(id);
20078
- }
20079
- function retarget(id, dependsOn) {
20080
- const node = requireNode(id);
20081
- if (dependsOn === void 0) delete node.dependsOn;
20082
- else node.dependsOn = dependsOn;
20083
- dependents.add(id, dependsOn === void 0 || dependsOn === "children" ? [] : dependsOn);
20084
- trackChildDerived(id, dependsOn);
20085
- dropPoseKeyedMemoSlots(requireNode(id));
20086
- invalidateDependents(id);
20087
- }
20088
- for (let i = 0; i < options.systemLayers.length; i++) {
20089
- const spec = options.systemLayers[i];
20090
- if (state.layerIndex.has(spec.id)) {
20091
- throw new Error(`useScene: duplicate system layer id "${spec.id}"`);
20092
- }
20093
- state.layers.push({
20094
- kind: "system",
20095
- id: spec.id,
20096
- visible: spec.visible ?? true,
20097
- locked: spec.locked ?? false
20098
- });
20099
- state.layerIndex.set(spec.id, i);
20100
- }
20101
- const generateId = options.generateId ?? defaultGenerateId;
20102
- const listeners = /* @__PURE__ */ new Set();
20103
- const registered = /* @__PURE__ */ new Map();
20104
- let historyAdapterAccessor = null;
20105
- function bindOpToHistoryAdapter(op) {
20106
- return {
20107
- name: op.name,
20108
- args: op.args,
20109
- label: op.label,
20110
- coalesceKey: op.coalesceKey,
20111
- apply: () => {
20112
- const get = historyAdapterAccessor;
20113
- if (!get) {
20114
- dwarn("scene", `restored op "${op.name ?? "?"}" has no history adapter \u2014 skipping (setHistoryAdapter was not wired)`);
20115
- return "noop";
20116
- }
20117
- try {
20118
- return op.apply(get());
20119
- } catch (err) {
20120
- dwarn("scene", `restored op "${op.name ?? "?"}" failed against the history adapter \u2014 skipping: ${String(err)}`);
20121
- 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
+ });
20122
20933
  }
20123
- },
20124
- invert: () => bindOpToHistoryAdapter(op.invert())
20125
- };
20126
- }
20127
- let selection = [];
20128
- const overrides = createPoseOverrides(
20129
- (id) => state.nodes.get(id),
20130
- invalidateDependents
20131
- );
20132
- const history = createHistory(void 0, {
20133
- selection: {
20134
- get: () => selection,
20135
- set: (ids) => {
20136
- selection = reachable(ids);
20137
- notify2();
20934
+ };
20935
+ for (const id of ids) {
20936
+ if (scene.ancestorsOf(asNodeId(id)).some((a) => idSet.has(a))) continue;
20937
+ capture(id, true);
20138
20938
  }
20939
+ return { items };
20139
20940
  },
20140
- ...options.coalesceWindowMs !== void 0 ? { coalesceWindowMs: options.coalesceWindowMs } : {},
20141
- ...options.historyLimit !== void 0 ? { historyLimit: options.historyLimit } : {},
20142
- onEvict: (entry) => {
20143
- pruneCacheForDroppedOps(entry.forwardOps);
20144
- pruneCacheForDroppedOps(entry.baseOps);
20145
- },
20146
- // Restore-time rebuild: scene-registered kinds (kit:* + consumer
20147
- // registerOp) rebuild via makeOp; external ops rebuild via the global
20148
- // op-factory registry, wrapped in the lazy history-adapter binding.
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;
20995
+ },
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 };
21000
+ }
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.
20149
21212
  // Return null only when the global registry also misses — the engine
20150
21213
  // then places its no-op placeholder. (The engine's own global-registry
20151
21214
  // fallback never fires for scene histories: this hook already
@@ -22018,7 +23081,7 @@ function CanvasView(props) {
22018
23081
  const viewSelection = selectionProp ?? (selectionOptions ? ownSelection : void 0);
22019
23082
  const [internalView, setInternalView] = useState(() => normalizeView(defaultView ?? IDENTITY_VIEW2));
22020
23083
  const effectiveView = viewProp ?? internalView;
22021
- const live = useRef({
23084
+ const live2 = useRef({
22022
23085
  view: effectiveView,
22023
23086
  bounds,
22024
23087
  layers,
@@ -22032,8 +23095,8 @@ function CanvasView(props) {
22032
23095
  // surface's otherwise. Filled in below, once the surface's is in hand.
22033
23096
  selection: viewSelection ?? ownSelection
22034
23097
  });
22035
- live.current = {
22036
- ...live.current,
23098
+ live2.current = {
23099
+ ...live2.current,
22037
23100
  view: effectiveView,
22038
23101
  bounds,
22039
23102
  layers,
@@ -22045,7 +23108,7 @@ function CanvasView(props) {
22045
23108
  viewSelection
22046
23109
  };
22047
23110
  const rectAt = useCallback((outer, dims) => {
22048
- const b = live.current.bounds;
23111
+ const b = live2.current.bounds;
22049
23112
  return typeof b === "function" ? b(outer, dims) : b;
22050
23113
  }, []);
22051
23114
  const rectNow = useCallback(() => {
@@ -22053,18 +23116,18 @@ function CanvasView(props) {
22053
23116
  return rectAt(surface?.view() ?? IDENTITY_VIEW2, surface?.dims() ?? UNMEASURED_DIMS);
22054
23117
  }, [registry2, rectAt]);
22055
23118
  const cameraAt = useCallback((outer, dims) => {
22056
- const v = live.current.view;
23119
+ const v = live2.current.view;
22057
23120
  return normalizeView(typeof v === "function" ? v(outer, dims) : v);
22058
23121
  }, []);
22059
23122
  const camera = useCallback(() => {
22060
23123
  const surface = registry2?.surface();
22061
23124
  return cameraAt(surface?.view() ?? IDENTITY_VIEW2, surface?.dims() ?? UNMEASURED_DIMS);
22062
23125
  }, [registry2, cameraAt]);
22063
- const drawnLayers = useCallback(() => paintedLayers(registry2?.surface()?.layers() ?? [], live.current), [registry2]);
23126
+ const drawnLayers = useCallback(() => paintedLayers(registry2?.surface()?.layers() ?? [], live2.current), [registry2]);
22064
23127
  const [sceneLayerGate] = useState(createSceneLayerGate);
22065
- 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]);
22066
23129
  const setView = useCallback((next) => {
22067
- const { viewBounds: vb, onViewChange: cb, viewProp: controlled } = live.current;
23130
+ const { viewBounds: vb, onViewChange: cb, viewProp: controlled } = live2.current;
22068
23131
  const rect = rectNow();
22069
23132
  const valid = normalizeView(next);
22070
23133
  const clamped = vb ? clampView(valid, vb, { width: rect.w, height: rect.h }) : valid;
@@ -22107,11 +23170,11 @@ function CanvasView(props) {
22107
23170
  ...createDispatcherPreviewSources(() => dispatcherRef.current)
22108
23171
  }), []);
22109
23172
  const selection = viewSelection ?? inputs?.selectionApi ?? ownSelection;
22110
- live.current.selection = selection;
23173
+ live2.current.selection = selection;
22111
23174
  const inputsRef = useRef(inputs);
22112
23175
  inputsRef.current = inputs;
22113
23176
  const ruleInputs = useCallback(() => ({
22114
- selection: live.current.selection.get(),
23177
+ selection: live2.current.selection.get(),
22115
23178
  view: camera(),
22116
23179
  action: dispatcherRef.current.getActiveAction()
22117
23180
  }), [camera]);
@@ -22152,553 +23215,169 @@ function CanvasView(props) {
22152
23215
  world.y,
22153
23216
  camera(),
22154
23217
  { width: rect.w, height: rect.h },
22155
- helpersRef.current,
22156
- drawnLayers()
22157
- );
22158
- if (extra) {
22159
- const claim = extra.hit;
22160
- return {
22161
- kind: `layer:${extra.layerId}`,
22162
- owner: extra.layerId,
22163
- strength: claim.strength ?? "shared",
22164
- ...claim.claimedKinds !== void 0 ? { claimedKinds: claim.claimedKinds } : {},
22165
- ...claim.cursor !== void 0 ? { cursor: claim.cursor } : {},
22166
- ...claim.initialScratch !== void 0 ? { payload: claim.initialScratch } : {}
22167
- };
22168
- }
22169
- return inner(world);
22170
- };
22171
- }, [getAnchorState, rectNow, registry2, targetScale, camera, drawnLayers]);
22172
- const classifyTarget = useMemo(() => {
22173
- const inner = buildClassifyTarget(
22174
- () => live.current.selection.get(),
22175
- (wx, wy) => {
22176
- const i = inputsRef.current;
22177
- const frame = { ...camera(), layerIsPainted };
22178
- if (i?.pickBest) return i.pickBest(wx, wy, frame);
22179
- const ids = i?.pickEvery?.(wx, wy, frame) ?? [];
22180
- return ids.length > 0 ? ids[ids.length - 1] : null;
22181
- },
22182
- (id2) => inputsRef.current?.kindOfNode?.(id2)
22183
- );
22184
- return (world) => inner(world);
22185
- }, [camera, layerIsPainted]);
22186
- const registration = useMemo(() => ({
22187
- id,
22188
- order,
22189
- interactive,
22190
- paint,
22191
- layer: createViewportLayer({
22192
- id,
22193
- label: label ?? id,
22194
- source: drawnLayers,
22195
- view: cameraAt,
22196
- bounds: (outer, dims) => rectAt(outer, dims),
22197
- // The surface half of the envelope passes through; the view half is
22198
- // this view's, so its layers paint its chrome rather than the
22199
- // surface's.
22200
- data: (outer) => ({ ...outer, ...helpersRef.current }),
22201
- ...background !== void 0 ? { background } : {}
22202
- }),
22203
- layerIsPainted,
22204
- target: {
22205
- dispatcher: dispatcherRef.current,
22206
- affordanceAt,
22207
- classifyTarget,
22208
- clientToWorld: clientToWorldHere,
22209
- deps: () => ({
22210
- view: viewApi,
22211
- ingestion: ingestionApi,
22212
- // An Escape here cancels what is in flight here.
22213
- dispatcher: { cancelAll: (reason) => dispatcherRef.current.cancelAll(reason) },
22214
- // Only a view with its own selection overlays that one; otherwise the
22215
- // surface's answer stands, wrappers (`selectionMode`) included.
22216
- ...live.current.viewSelection ? { selection: live.current.viewSelection } : {}
22217
- }),
22218
- // Action eligibility is chrome-caps evaluated against this view, so a
22219
- // rule that hides an action in one panel cannot decline a gesture in
22220
- // another.
22221
- getRuleCtx: () => inputsRef.current?.chromeCaps?.ruleCtx(ruleInputs())
22222
- }
22223
- }), [
22224
- id,
22225
- order,
22226
- interactive,
22227
- paint,
22228
- label,
22229
- background,
22230
- rectAt,
22231
- cameraAt,
22232
- viewApi,
22233
- affordanceAt,
22234
- classifyTarget,
22235
- clientToWorldHere,
22236
- ruleInputs,
22237
- ingestionApi,
22238
- drawnLayers,
22239
- layerIsPainted
22240
- ]);
22241
- useEffect(() => {
22242
- if (!registry2) return;
22243
- return registry2.register(registration);
22244
- }, [registry2, registration]);
22245
- useEffect(() => {
22246
- registry2?.surface()?.requestRedraw();
22247
- }, [registry2, effectiveView]);
22248
- return null;
22249
- }
22250
- function ActionsProviderIfRoot({ children }) {
22251
- const parent = useActionsRegistry();
22252
- if (parent) return /* @__PURE__ */ jsx(ActionsScope, { children });
22253
- return /* @__PURE__ */ jsx(ActionsProvider, { children });
22254
- }
22255
- function useToolActions(tools) {
22256
- const registry2 = useActionsRegistry();
22257
- useEffect(() => {
22258
- if (!registry2) return;
22259
- const unregisters = [];
22260
- for (const tool of [...Object.values(tools.registry), ...tools.ambient]) {
22261
- for (const action of tool.actions ?? []) {
22262
- unregisters.push(registry2.register(action));
22263
- }
22264
- }
22265
- return () => {
22266
- for (const u of unregisters) u();
22267
- };
22268
- }, [registry2, tools.registry, tools.ambient]);
22269
- }
22270
- function PointerProviderIfRoot({ children }) {
22271
- const parent = usePointerContext();
22272
- if (parent) return /* @__PURE__ */ jsx(Fragment, { children });
22273
- return /* @__PURE__ */ jsx(PointerContextProvider, { children });
22274
- }
22275
- function PointerPublisher({
22276
- canvasRef
22277
- }) {
22278
- const ctx = usePointerContext();
22279
- const registry2 = useOptionalViewRegistry();
22280
- useEffect(() => {
22281
- if (!ctx) return;
22282
- const el = canvasRef.current;
22283
- if (!el) return;
22284
- const publish = (pointerId, clientX, clientY) => {
22285
- const target = registry2?.resolver.at(pointerId, clientX, clientY);
22286
- const rect = el.getBoundingClientRect();
22287
- const [worldX, worldY] = clientToWorld(
22288
- clientX,
22289
- clientY,
22290
- target?.origin ?? rect,
22291
- target?.view ?? { x: 0, y: 0, scale: { x: 1, y: 1 } }
22292
- );
22293
- ctx.pointerRef.current = { worldX, worldY };
22294
- };
22295
- const onMove = (e) => publish(e.pointerId, e.clientX, e.clientY);
22296
- const onLeave = (e) => {
22297
- if (e.buttons !== 0) {
22298
- publish(e.pointerId, e.clientX, e.clientY);
22299
- return;
22300
- }
22301
- ctx.pointerRef.current = null;
22302
- };
22303
- const onDocMove = (e) => {
22304
- if (e.target === el) return;
22305
- if (e.buttons === 0) return;
22306
- publish(e.pointerId, e.clientX, e.clientY);
22307
- };
22308
- el.addEventListener("pointermove", onMove);
22309
- el.addEventListener("pointerleave", onLeave);
22310
- document.addEventListener("pointermove", onDocMove);
22311
- return () => {
22312
- el.removeEventListener("pointermove", onMove);
22313
- el.removeEventListener("pointerleave", onLeave);
22314
- document.removeEventListener("pointermove", onDocMove);
22315
- };
22316
- }, [ctx, canvasRef, registry2]);
22317
- return null;
22318
- }
22319
- function flattenVerts2(poly2) {
22320
- const flat = new Array(poly2.length * 2);
22321
- for (let i = 0; i < poly2.length; i++) {
22322
- flat[i * 2] = poly2[i].x;
22323
- flat[i * 2 + 1] = poly2[i].y;
22324
- }
22325
- return flat;
22326
- }
22327
- function polygonContainsRectCenter(poly2, rect) {
22328
- return pointInPolygon(flattenVerts2(poly2), rect.x + rect.width / 2, rect.y + rect.height / 2);
22329
- }
22330
- function polygonContainsRect(poly2, rect) {
22331
- if (poly2.length < 3) return false;
22332
- const flat = flattenVerts2(poly2);
22333
- if (!pointInPolygon(flat, rect.x, rect.y)) return false;
22334
- if (!pointInPolygon(flat, rect.x + rect.width, rect.y)) return false;
22335
- if (!pointInPolygon(flat, rect.x + rect.width, rect.y + rect.height)) return false;
22336
- if (!pointInPolygon(flat, rect.x, rect.y + rect.height)) return false;
22337
- return !anyPolyEdgeCrossesRect(poly2, rect);
22338
- }
22339
- function polygonIntersectsRect(poly2, rect) {
22340
- if (poly2.length < 3) return false;
22341
- const ab = polygonAabb(poly2);
22342
- if (ab.x + ab.width < rect.x || ab.x > rect.x + rect.width) return false;
22343
- if (ab.y + ab.height < rect.y || ab.y > rect.y + rect.height) return false;
22344
- for (const v of poly2) {
22345
- if (v.x >= rect.x && v.x <= rect.x + rect.width && v.y >= rect.y && v.y <= rect.y + rect.height) return true;
22346
- }
22347
- const flat = flattenVerts2(poly2);
22348
- if (pointInPolygon(flat, rect.x, rect.y)) return true;
22349
- if (pointInPolygon(flat, rect.x + rect.width, rect.y)) return true;
22350
- if (pointInPolygon(flat, rect.x + rect.width, rect.y + rect.height)) return true;
22351
- if (pointInPolygon(flat, rect.x, rect.y + rect.height)) return true;
22352
- return anyPolyEdgeCrossesRect(poly2, rect);
22353
- }
22354
- function polygonAabb(poly2) {
22355
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
22356
- for (const v of poly2) {
22357
- if (v.x < minX) minX = v.x;
22358
- if (v.y < minY) minY = v.y;
22359
- if (v.x > maxX) maxX = v.x;
22360
- if (v.y > maxY) maxY = v.y;
22361
- }
22362
- return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
22363
- }
22364
- function anyPolyEdgeCrossesRect(poly2, rect) {
22365
- const r0x = rect.x, r0y = rect.y;
22366
- const r1x = rect.x + rect.width, r1y = rect.y;
22367
- const r2x = rect.x + rect.width, r2y = rect.y + rect.height;
22368
- const r3x = rect.x, r3y = rect.y + rect.height;
22369
- for (let i = 0, j = poly2.length - 1; i < poly2.length; j = i++) {
22370
- const a = poly2[j], b = poly2[i];
22371
- if (segmentsCross(a.x, a.y, b.x, b.y, r0x, r0y, r1x, r1y)) return true;
22372
- if (segmentsCross(a.x, a.y, b.x, b.y, r1x, r1y, r2x, r2y)) return true;
22373
- if (segmentsCross(a.x, a.y, b.x, b.y, r2x, r2y, r3x, r3y)) return true;
22374
- if (segmentsCross(a.x, a.y, b.x, b.y, r3x, r3y, r0x, r0y)) return true;
22375
- }
22376
- return false;
22377
- }
22378
-
22379
- // src/canvas/sceneAdapter.ts
22380
- function walkClipAware(scene, poseBounds, nodeTest, poseComposition) {
22381
- return pickWalk(scenePickSource(scene, poseComposition ? { poseComposition } : {}), {
22382
- hits: (node, pose) => nodeTest(node, pose),
22383
- clipAdmits: (clip, _node, pose) => pathIntersectsRect(clip, poseBounds(pose))
22384
- });
22385
- }
22386
- var adapterFallbackIdCounter = 0;
22387
- function adapterFallbackId() {
22388
- return `n${(adapterFallbackIdCounter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
22389
- }
22390
- var pasteIdCounter = 0;
22391
- function pasteNodeId() {
22392
- return `paste-${(pasteIdCounter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
22393
- }
22394
- function copyField(value) {
22395
- if (Array.isArray(value)) return [...value];
22396
- if (value !== null && typeof value === "object") return { ...value };
22397
- return value;
22398
- }
22399
- function sceneToAdapter(scene, options = {}) {
22400
- const visibleLayers = () => {
22401
- const out = /* @__PURE__ */ new Set();
22402
- for (const layer of scene.layers) {
22403
- if (layer.visible) out.add(layer.id);
22404
- }
22405
- return out;
22406
- };
22407
- const sel = options.selection;
22408
- const getSelection = sel?.getSelection ?? sel?.get ?? (() => []);
22409
- const setSelection = sel?.setSelection ?? sel?.set ?? (() => {
22410
- });
22411
- const d = options.poseDescriptor ?? AUTO_POSE_DESCRIPTOR;
22412
- const poseBounds = (p) => visualBoundsViaDescriptor(p, d);
22413
- const translate3 = (p, dx, dy, node) => translatePoseViaDescriptor(
22414
- p,
22415
- dx,
22416
- dy,
22417
- node === void 0 ? d : poseDescriptorForNode(d, node)
22418
- );
22419
- const composition = options.poseComposition;
22420
- const composes = composition !== void 0 && composition.closure !== "identity";
22421
- if (composes && options.cascadeContainerPose) {
22422
- throw new Error(
22423
- "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."
22424
- );
22425
- }
22426
- const adapter = {
22427
- getNode(id) {
22428
- return scene.get(asNodeId(id));
22429
- },
22430
- getNodes() {
22431
- const visible = visibleLayers();
22432
- const out = [];
22433
- for (const n of scene.renderOrderNodes()) {
22434
- if (visible.has(n.layer)) out.push(n);
22435
- }
22436
- return out;
22437
- },
22438
- getPose(id) {
22439
- const nid = asNodeId(id);
22440
- const n = scene.get(nid);
22441
- if (!n) throw new Error(`sceneToAdapter: unknown node "${id}"`);
22442
- return effectivePose(scene, n);
22443
- },
22444
- getParent(id) {
22445
- const n = scene.get(asNodeId(id));
22446
- return n?.parent ?? null;
22447
- },
22448
- getWorldPose(id) {
22449
- if (!composes) return adapter.getPose(id);
22450
- return composeWorldPose(adapter, id, composition.compose);
22451
- },
22452
- // Only present when it would do something: the render walk skips the fold
22453
- // entirely when this is absent, which is the absolute-pose path.
22454
- ...composes ? { composePose: composition.compose } : {},
22455
- setPose(id, pose) {
22456
- if (options.cascadeContainerPose) {
22457
- const node = scene.get(asNodeId(id));
22458
- if (node && node.kind === "container") {
22459
- const g = poseDescriptorForNode(d, node);
22460
- const before = g.getBounds(node.pose);
22461
- const after = g.getBounds(pose);
22462
- const dx = after.x - before.x;
22463
- const dy = after.y - before.y;
22464
- if (dx !== 0 || dy !== 0) {
22465
- const descIds = [];
22466
- const collect = (rootId) => {
22467
- for (const cid of scene.childrenOf(asNodeId(rootId))) {
22468
- descIds.push(cid);
22469
- collect(cid);
22470
- }
22471
- };
22472
- collect(id);
22473
- scene.batch("setPose", () => {
22474
- scene.setPose(asNodeId(id), pose);
22475
- for (const cid of descIds) {
22476
- const cn = scene.get(asNodeId(cid));
22477
- if (!cn) continue;
22478
- scene.setPose(asNodeId(cid), translate3(cn.pose, dx, dy, cn));
22479
- }
22480
- });
22481
- return;
22482
- }
22483
- }
22484
- }
22485
- scene.setPose(asNodeId(id), pose);
22486
- },
22487
- setParent(id, parentId) {
22488
- scene.move(asNodeId(id), parentId === null ? null : asNodeId(parentId));
22489
- },
22490
- getChildren(id) {
22491
- if (id === null) return [...scene.roots];
22492
- return [...scene.childrenOf(asNodeId(id))];
22493
- },
22494
- setChildOrder(parentId, ids) {
22495
- scene.batch("Reorder", () => {
22496
- for (let i = 0; i < ids.length; i++) {
22497
- const current = parentId === null ? scene.roots : scene.childrenOf(asNodeId(parentId));
22498
- if (current[i] === ids[i]) continue;
22499
- scene.reorder(asNodeId(ids[i]), i);
22500
- }
22501
- });
22502
- },
22503
- getLayers() {
22504
- return scene.layers.map((l) => ({ id: l.id, visible: l.visible }));
22505
- },
22506
- ...options.layouts ? {
22507
- getLayout: typeof options.layouts === "function" ? options.layouts : (id) => options.layouts[id] ?? null
22508
- } : {},
22509
- // Unified applyOps: when a label is supplied the ops are wrapped in a
22510
- // batch checkpoint (history-aware path used by Move/Resize/Rotate/Insert/
22511
- // Delete tools). When a journal is active (via scene's getActiveJournal
22512
- // option), scene.applyBatch routes to the journal instead of recording a
22513
- // new parent-history entry. Without a label the ops are applied transiently
22514
- // via applyOpsTo (AreaSelectAdapter contract — no checkpoint).
22515
- applyOps(ops, label) {
22516
- if (label !== void 0) {
22517
- scene.applyBatch(ops, label, this);
22518
- } else {
22519
- applyOpsTo(this, ops);
22520
- }
22521
- },
22522
- // Node-mutation surface used by kit InsertOps / DeleteOps (e.g.
22523
- // useNest wrapping the selection in a new container node, or its
22524
- // inverse). Re-adds via the full structural spec so the round-trip
22525
- // survives undo/redo; removes by id.
22526
- insertNode(node, index) {
22527
- scene.add({
22528
- kind: node.kind,
22529
- layer: node.layer,
22530
- pose: node.pose,
22531
- data: node.data,
22532
- id: node.id,
22533
- ...index !== void 0 ? { index } : {},
22534
- ...node.parent !== null ? { parent: node.parent } : {},
22535
- // Derivation travels with the node, or undo of a delete brings an
22536
- // edge back as a static path that never follows its endpoints again.
22537
- ...node.dependsOn !== void 0 ? { dependsOn: node.dependsOn } : {},
22538
- ...fnFieldsOfNode(node)
22539
- });
22540
- },
22541
- removeNode(id) {
22542
- scene.remove(asNodeId(id));
22543
- },
22544
- setData(id, data) {
22545
- scene.update(asNodeId(id), { data });
22546
- },
22547
- getRemovalClosure(ids) {
22548
- return [...scene.removalClosure(ids.map(asNodeId))];
22549
- },
22550
- // AreaSelectAdapter surface — included unconditionally so plain
22551
- // `useSelectTool(sceneToAdapter(scene, { selection }))` Just Works for the
22552
- // marquee gesture. `applyOps` (above) dispatches transiently when called
22553
- // without a label, matching the AreaSelectAdapter contract; `hitTestArea`
22554
- // does an AABB-vs-AABB scan over `scene.renderOrder()`, reading bounds
22555
- // through the pose descriptor.
22556
- getSelection,
22557
- setSelection,
22558
- hitTestArea(rect) {
22559
- return walkClipAware(scene, poseBounds, (_n, pose) => {
22560
- const b = poseBounds(pose);
22561
- 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;
22562
- }, composition);
22563
- },
22564
- hitTestLasso(polygon, mode) {
22565
- if (polygon.length < 3) return [];
22566
- return walkClipAware(scene, poseBounds, (_n, pose) => {
22567
- const b = poseBounds(pose);
22568
- return mode === "centers" ? polygonContainsRectCenter(polygon, b) : mode === "enclosed" ? polygonContainsRect(polygon, b) : polygonIntersectsRect(polygon, b);
22569
- }, composition);
22570
- },
22571
- // commitInsert (gesture-time leaf insert) is opt-in: present only when
22572
- // `options.commitInsert` is. The full insertNode/removeNode mutators
22573
- // above are always present so kit-side InsertOp / DeleteOp round-trip
22574
- // through scene.add / scene.remove regardless.
22575
- //
22576
- // Contract: this is a pure factory — it constructs a Node spec but does
22577
- // NOT touch the scene. `useInsert`'s onEnd dispatches an `InsertOp`
22578
- // whose apply() calls `adapter.insertNode` → `scene.add`, which is the
22579
- // sole insertion path. Calling `scene.add` here too would double-insert,
22580
- // and the second add throws on id collision (silently dropping the
22581
- // gesture's pointer-up cleanup and breaking the next drag).
22582
- ...options.commitInsert ? {
22583
- commitInsert: (bounds) => {
22584
- const created = options.commitInsert(bounds);
22585
- if (!created) return null;
22586
- const layer = options.insertLayer ?? "default";
22587
- const id = asNodeId(created.id ?? adapterFallbackId());
22588
- return {
22589
- kind: "leaf",
22590
- id,
22591
- layer,
22592
- pose: created.pose,
22593
- data: created.data,
22594
- parent: null
22595
- };
22596
- }
22597
- } : {},
22598
- // Clipboard seam (`useClipboardOps` / the paste ingestion path).
22599
- //
22600
- // `snapshotSelection` captures adapter-shaped node copies; `commitPaste`
22601
- // is a pure factory like `commitInsert` above — it mints fresh nodes but
22602
- // NEVER touches the scene. Insertion happens via the hook's InsertOps
22603
- // (one per created node, parents before children), which is what makes a
22604
- // whole paste a single undo entry.
22605
- snapshotSelection(ids) {
22606
- const idSet = new Set(ids);
22607
- const taken = /* @__PURE__ */ new Set();
22608
- const items = [];
22609
- const poseFor = (n, isRoot) => isRoot && composes ? adapter.getWorldPose(n.id) : n.pose;
22610
- const capture = (id, isRoot = false) => {
22611
- if (taken.has(id)) return;
22612
- const n = scene.get(asNodeId(id));
22613
- if (!n) return;
22614
- taken.add(id);
22615
- if (n.kind === "container") {
22616
- items.push({
22617
- kind: "container",
22618
- id: n.id,
22619
- layer: n.layer,
22620
- parent: n.parent,
22621
- pose: copyField(poseFor(n, isRoot)),
22622
- data: copyField(n.data),
22623
- children: [...n.children],
22624
- ...n.clipFromPose ? { clipFromPose: n.clipFromPose } : {}
22625
- });
22626
- for (const cid of scene.childrenOf(asNodeId(id))) capture(cid);
22627
- } else {
22628
- items.push({
22629
- kind: "leaf",
22630
- id: n.id,
22631
- layer: n.layer,
22632
- parent: n.parent,
22633
- pose: copyField(poseFor(n, isRoot)),
22634
- data: copyField(n.data)
22635
- });
22636
- }
22637
- };
22638
- for (const id of ids) {
22639
- if (scene.ancestorsOf(asNodeId(id)).some((a) => idSet.has(a))) continue;
22640
- capture(id, true);
22641
- }
22642
- return { items };
22643
- },
22644
- commitPaste(clipboard, offset, ctx) {
22645
- const src = clipboard.items;
22646
- if (src.length === 0) return [];
22647
- const idMap = /* @__PURE__ */ new Map();
22648
- for (const item of src) idMap.set(item.id, asNodeId(pasteNodeId()));
22649
- const isRoot = (item) => item.parent === null || !idMap.has(item.parent);
22650
- let dx = offset.dx;
22651
- let dy = offset.dy;
22652
- if (ctx?.dropPoint) {
22653
- let minX = Infinity;
22654
- let minY = Infinity;
22655
- for (const item of src) {
22656
- if (!isRoot(item)) continue;
22657
- const b = poseBounds(item.pose);
22658
- if (b.x < minX) minX = b.x;
22659
- if (b.y < minY) minY = b.y;
22660
- }
22661
- if (Number.isFinite(minX) && Number.isFinite(minY)) {
22662
- dx = ctx.dropPoint.worldX - minX;
22663
- dy = ctx.dropPoint.worldY - minY;
22664
- }
22665
- }
22666
- const out = [];
22667
- for (const item of src) {
22668
- const base = {
22669
- id: idMap.get(item.id),
22670
- layer: item.layer,
22671
- parent: isRoot(item) ? null : idMap.get(item.parent),
22672
- pose: translate3(copyField(item.pose), dx, dy),
22673
- data: copyField(item.data)
23218
+ helpersRef.current,
23219
+ drawnLayers()
23220
+ );
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 } : {}
22674
23230
  };
22675
- if (item.kind === "container") {
22676
- out.push({
22677
- ...base,
22678
- kind: "container",
22679
- children: item.children.filter((c) => idMap.has(c)).map((c) => idMap.get(c)),
22680
- ...item.clipFromPose ? { clipFromPose: item.clipFromPose } : {}
22681
- });
22682
- } else {
22683
- out.push({ ...base, kind: "leaf" });
22684
- }
22685
23231
  }
22686
- return out;
22687
- },
22688
- // Constant cascade: the hook re-snapshots the just-created items, so
22689
- // repeated pastes stack +12/+12 each time.
22690
- getPasteOffset() {
22691
- 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())
22692
23285
  }
22693
- };
22694
- 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;
22695
23312
  }
22696
- function useSceneAdapter(scene, options = {}) {
22697
- const { selection, commitInsert, insertLayer, layouts, poseDescriptor, cascadeContainerPose } = options;
22698
- return useMemo(
22699
- () => sceneToAdapter(scene, { selection, commitInsert, insertLayer, layouts, poseDescriptor, cascadeContainerPose }),
22700
- [scene, selection, commitInsert, insertLayer, layouts, poseDescriptor, cascadeContainerPose]
22701
- );
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;
22702
23381
  }
22703
23382
  function aabbOfPose(pose, descriptor = AUTO_POSE_DESCRIPTOR) {
22704
23383
  return descriptor.getBounds(pose);
@@ -22731,18 +23410,7 @@ function rotationAboutCenter(pose, descriptor) {
22731
23410
  // src/canvas/SceneCanvas/useSceneSelectTool.ts
22732
23411
  var DEFAULT_PICK_TOLERANCE_PX = 4;
22733
23412
  function useSceneSelectTool(args) {
22734
- const {
22735
- scene,
22736
- selection,
22737
- geometry,
22738
- selectTool: opts,
22739
- insertTool,
22740
- layouts,
22741
- poseComposition,
22742
- getView,
22743
- alphaOf,
22744
- layerIsPainted
22745
- } = args;
23413
+ const { scene, adapter, geometry, selectTool: opts, getView, alphaOf, layerIsPainted } = args;
22746
23414
  const d = args.poseDescriptor ?? AUTO_POSE_DESCRIPTOR;
22747
23415
  const pickEveryProp = geometry?.pickEvery;
22748
23416
  const boundsOfProp = geometry?.boundsOf;
@@ -22750,62 +23418,6 @@ function useSceneSelectTool(args) {
22750
23418
  const pickTolerancePx = geometry?.pickTolerancePx ?? DEFAULT_PICK_TOLERANCE_PX;
22751
23419
  const moveOptions = opts?.move;
22752
23420
  const snap2 = opts?.snap;
22753
- const commitInsert = insertTool?.create;
22754
- const insertLayer = insertTool?.layer;
22755
- const adapter = useMemo(() => {
22756
- const base = sceneToAdapter(scene, {
22757
- commitInsert,
22758
- insertLayer,
22759
- layouts,
22760
- poseDescriptor: d,
22761
- ...poseComposition ? { poseComposition } : {}
22762
- });
22763
- const framed = poseComposition !== void 0 && poseComposition.closure !== "identity";
22764
- const collectDescendants = (id, out) => {
22765
- for (const cid of scene.childrenOf(asNodeId(id))) {
22766
- out.push(cid);
22767
- collectDescendants(cid, out);
22768
- }
22769
- };
22770
- return {
22771
- ...base,
22772
- setPose(id, pose) {
22773
- const n = scene.get(asNodeId(id));
22774
- if (framed || !n || n.kind !== "container") {
22775
- base.setPose(id, pose);
22776
- return;
22777
- }
22778
- const g = poseDescriptorForNode(d, n);
22779
- const prev = g.getBounds(n.pose);
22780
- const next = g.getBounds(pose);
22781
- const dx = next.x - prev.x;
22782
- const dy = next.y - prev.y;
22783
- if (dx === 0 && dy === 0) {
22784
- base.setPose(id, pose);
22785
- return;
22786
- }
22787
- const desc = [];
22788
- collectDescendants(id, desc);
22789
- scene.batch("move container", () => {
22790
- base.setPose(id, pose);
22791
- for (const cid of desc) {
22792
- const cn = scene.get(asNodeId(cid));
22793
- if (!cn) continue;
22794
- base.setPose(
22795
- cid,
22796
- translatePoseViaDescriptor(cn.pose, dx, dy, poseDescriptorForNode(d, cn))
22797
- );
22798
- }
22799
- });
22800
- },
22801
- // Selection methods, widened to `string[]` so the synthesized adapter
22802
- // satisfies `AreaSelectAdapter` (which keeps the kit-internal adapter
22803
- // contract on plain `string[]`; the brand lives on the public selection
22804
- // surface, not on adapter shapes).
22805
- getSelection: () => [...selection.adapterMethods.getSelection()],
22806
- setSelection: (ids) => selection.adapterMethods.setSelection(ids)
22807
- };
22808
- }, [scene, commitInsert, insertLayer, layouts, selection, d, poseComposition]);
22809
23421
  const wiredMoveOptions = useMemo(() => {
22810
23422
  const merged = { ...moveOptions ?? {} };
22811
23423
  if (snap2) {
@@ -22880,7 +23492,6 @@ function useSceneSelectTool(args) {
22880
23492
  };
22881
23493
  }, [wiredHitBody, adapter]);
22882
23494
  return {
22883
- adapter,
22884
23495
  selectTool,
22885
23496
  pickEvery: wiredHitBody,
22886
23497
  pickBest: wiredPickBest,
@@ -23337,60 +23948,21 @@ var CLOSE_HINT_RADIUS_PX = 8;
23337
23948
  function w2s(wx, wy, view) {
23338
23949
  return [(wx - view.x) * view.scale.x, (wy - view.y) * view.scale.y];
23339
23950
  }
23340
- function mirror(anchor, out) {
23341
- if (!out || anchor.altBroken) return void 0;
23342
- return { x: 2 * anchor.x - out.x, y: 2 * anchor.y - out.y };
23343
- }
23344
23951
  function approximateCircle(cx, cy, r) {
23345
23952
  return circlePath(cx, cy, r);
23346
23953
  }
23347
23954
  function subpathToPath(sp, view) {
23348
23955
  if (sp.anchors.length === 0) return null;
23349
- const cmds = [];
23350
- const xs = [];
23351
- const [sx, sy] = w2s(sp.anchors[0].x, sp.anchors[0].y, view);
23352
- cmds.push(PATH_M);
23353
- xs.push(sx, sy);
23354
- for (let i = 1; i < sp.anchors.length; i++) {
23355
- const prev = sp.anchors[i - 1];
23356
- const curr = sp.anchors[i];
23357
- const out = prev.outHandle;
23358
- const inH = curr.inHandle ?? mirror(prev, out);
23359
- const [tx, ty] = w2s(curr.x, curr.y, view);
23360
- if (out || curr.inHandle) {
23361
- const c1 = out ?? prev;
23362
- const c2 = inH ?? curr;
23363
- const [c1x, c1y] = w2s(c1.x, c1.y, view);
23364
- const [c2x, c2y] = w2s(c2.x, c2.y, view);
23365
- cmds.push(PATH_C);
23366
- xs.push(c1x, c1y, c2x, c2y, tx, ty);
23367
- } else {
23368
- cmds.push(PATH_L);
23369
- xs.push(tx, ty);
23370
- }
23371
- }
23372
- if (sp.closed) {
23373
- const last = sp.anchors[sp.anchors.length - 1];
23374
- const first = sp.anchors[0];
23375
- const out = last.outHandle;
23376
- const inH = first.inHandle ?? mirror(last, out);
23377
- if (out || first.inHandle) {
23378
- const c1 = out ?? last;
23379
- const c2 = inH ?? first;
23380
- const [c1x, c1y] = w2s(c1.x, c1.y, view);
23381
- const [c2x, c2y] = w2s(c2.x, c2.y, view);
23382
- const [tx, ty] = w2s(first.x, first.y, view);
23383
- cmds.push(PATH_C);
23384
- xs.push(c1x, c1y, c2x, c2y, tx, ty);
23385
- }
23386
- cmds.push(PATH_Z);
23387
- }
23388
- return {
23389
- kind: "polygon",
23390
- commands: new Uint8Array(cmds),
23391
- coords: new Float32Array(xs),
23392
- fillRule: "nonzero"
23956
+ const pt = (p) => {
23957
+ const [x, y] = w2s(p.x, p.y, view);
23958
+ return { x, y };
23393
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]);
23394
23966
  }
23395
23967
  function createPenPreviewLayer(opts) {
23396
23968
  const style = { ...DEFAULT_STYLE2, ...opts.style ?? {} };
@@ -23479,10 +24051,10 @@ function createPenPreviewLayer(opts) {
23479
24051
  path: approximateCircle(hx, hy, HANDLE_DOT_RADIUS_PX),
23480
24052
  fill: { fill: "solid", color: style.handleStroke }
23481
24053
  });
23482
- const inSide = mirror(a, a.outHandle);
24054
+ const inSide = a.inHandle;
23483
24055
  if (inSide) {
23484
24056
  const [mx, my] = w2s(inSide.x, inSide.y, view);
23485
- const mirrorLine = {
24057
+ const inLine = {
23486
24058
  kind: "polygon",
23487
24059
  commands: new Uint8Array([PATH_M, PATH_L]),
23488
24060
  coords: new Float32Array([ax, ay, mx, my]),
@@ -23490,7 +24062,7 @@ function createPenPreviewLayer(opts) {
23490
24062
  };
23491
24063
  out.push({
23492
24064
  kind: "path",
23493
- path: mirrorLine,
24065
+ path: inLine,
23494
24066
  stroke: { paint: { fill: "solid", color: style.handleStroke }, width: 1 }
23495
24067
  });
23496
24068
  out.push({
@@ -23706,7 +24278,7 @@ function useBuiltinShapeTools(args) {
23706
24278
  const { scene, adapter, options } = args;
23707
24279
  const seqRef = useRef(0);
23708
24280
  const freshId = (prefix) => asNodeId(`${prefix}-${++seqRef.current}`);
23709
- const nextFill = () => DEFAULT_PALETTE[seqRef.current % DEFAULT_PALETTE.length];
24281
+ const nextFill = () => cycleFill(seqRef.current);
23710
24282
  const layerOf = () => scene.layers[0]?.id ?? "default";
23711
24283
  const makeLeaf = (id, pose, data) => ({
23712
24284
  id,
@@ -23723,6 +24295,15 @@ function useBuiltinShapeTools(args) {
23723
24295
  const polygon = usePolygonTool();
23724
24296
  const star = useStarTool();
23725
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
+ };
23726
24307
  const pen = usePenTool({
23727
24308
  snapPoint: snapPoint2,
23728
24309
  wrapPath: (path, { closed }) => {
@@ -23731,16 +24312,11 @@ function useBuiltinShapeTools(args) {
23731
24312
  },
23732
24313
  adapter: {
23733
24314
  addNode: (carrier) => {
23734
- const id = freshId("pn");
23735
- const color = nextFill();
23736
- const node = makeLeaf(id, carrier.bounds, {
23737
- path: carrier.path,
23738
- fill: carrier.closed ? solid(nextFill()) : null,
23739
- stroke: strokeOf(color, 2)
23740
- });
24315
+ const node = makePenLeaf(carrier);
23741
24316
  adapter.insertNode(node);
23742
- return String(id);
24317
+ return String(node.id);
23743
24318
  },
24319
+ makeNode: (carrier) => makePenLeaf(carrier),
23744
24320
  setSelection: (ids) => adapter.setSelection(ids)
23745
24321
  }
23746
24322
  });
@@ -24010,6 +24586,19 @@ function useEditAnchorsDepSource(scene, selection, adapter, externalState, optio
24010
24586
  }
24011
24587
  const selectedAnchors = effectiveId ? ext?.getSelectedAnchors() ?? EMPTY_ANCHOR_SELECTION : EMPTY_ANCHOR_SELECTION;
24012
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
+ };
24013
24602
  return {
24014
24603
  editingId: effectiveId,
24015
24604
  setEditingId(id) {
@@ -24052,24 +24641,10 @@ function useEditAnchorsDepSource(scene, selection, adapter, externalState, optio
24052
24641
  return { pose: node.pose, data: node.data };
24053
24642
  },
24054
24643
  applyEdit(id, worldPath, label) {
24055
- const wp = worldPath;
24056
- const node = sc.get(id);
24057
- if (!node) return;
24058
- const storage = classifyStorage(node);
24059
- if (!storage) return;
24060
- if (storage.kind === "pose") {
24061
- ad.applyOps([createTransformOp({ id, from: node.pose, to: wp, label })], label);
24062
- return;
24063
- }
24064
- const { pose: newPose, path: aligned } = worldEditToStorage(storage.pose, wp);
24065
- ad.applyOps(
24066
- [
24067
- createTransformOp({ id, from: node.pose, to: newPose, label }),
24068
- createSetDataOp({ id, from: node.data, to: { ...node.data, path: aligned }, label })
24069
- ],
24070
- label
24071
- );
24072
- }
24644
+ const ops = editOps(id, worldPath, label);
24645
+ if (ops.length > 0) ad.applyOps(ops, label);
24646
+ },
24647
+ editOps
24073
24648
  };
24074
24649
  });
24075
24650
  }
@@ -24262,7 +24837,7 @@ function useInsertDepSource(scene, adapter, factories) {
24262
24837
  seq = ++insertSeqRef.current;
24263
24838
  id = asNodeId(`kit-${kind}-${seq}`);
24264
24839
  }
24265
- const color = DEFAULT_PALETTE[seq % DEFAULT_PALETTE.length];
24840
+ const color = cycleFill(seq);
24266
24841
  const fill = solid(color);
24267
24842
  const layer = sc.layers[0]?.id ?? "default";
24268
24843
  const extent = insertPreviewExtent({ shape: kind, bounds, extras });
@@ -24358,15 +24933,16 @@ function useDispatcherDepSource(dispatcher) {
24358
24933
  cancelAll: (reason) => ref.current.cancelAll(reason)
24359
24934
  }));
24360
24935
  }
24361
- function useLayoutDepSource(layouts) {
24362
- const ref = useRef(layouts);
24363
- ref.current = layouts;
24936
+ function useLayoutDepSource(layouts, dropTarget) {
24937
+ const ref = useRef({ layouts, dropTarget });
24938
+ ref.current = { layouts, dropTarget };
24364
24939
  useDepSource("layout", () => ({
24365
24940
  getLayout: (containerId) => {
24366
- const l = ref.current;
24941
+ const l = ref.current.layouts;
24367
24942
  if (!l) return null;
24368
24943
  return typeof l === "function" ? l(containerId) : l[containerId] ?? null;
24369
- }
24944
+ },
24945
+ dropTarget: ref.current.dropTarget
24370
24946
  }));
24371
24947
  }
24372
24948
  function useGeometryProjection(projection) {
@@ -24467,8 +25043,8 @@ var kitImageHandler = {
24467
25043
 
24468
25044
  // src/features/ingestion/svgHandler.ts
24469
25045
  var SVG_MIME = "image/svg+xml";
24470
- var FALLBACK_WIDTH = 300;
24471
- var FALLBACK_HEIGHT = 150;
25046
+ var FALLBACK_WIDTH2 = 300;
25047
+ var FALLBACK_HEIGHT2 = 150;
24472
25048
  function isSvgFileItem(item) {
24473
25049
  if (item.kind !== "file") return false;
24474
25050
  if (item.mime === SVG_MIME) return true;
@@ -24506,8 +25082,8 @@ var defaultMeasure2 = async (file) => {
24506
25082
  img.src = url;
24507
25083
  await img.decode();
24508
25084
  return {
24509
- width: img.naturalWidth || FALLBACK_WIDTH,
24510
- height: img.naturalHeight || FALLBACK_HEIGHT
25085
+ width: img.naturalWidth || FALLBACK_WIDTH2,
25086
+ height: img.naturalHeight || FALLBACK_HEIGHT2
24511
25087
  };
24512
25088
  } finally {
24513
25089
  URL.revokeObjectURL(url);
@@ -25051,6 +25627,7 @@ function SceneCanvasInner(props, ref) {
25051
25627
  insertNodeFactories,
25052
25628
  ingestion,
25053
25629
  layouts,
25630
+ layoutDropTarget,
25054
25631
  poseComposition,
25055
25632
  geometryProjection,
25056
25633
  routing,
@@ -25182,6 +25759,12 @@ function SceneCanvasInner(props, ref) {
25182
25759
  }, [scene, selectionOptions, selectionMode]);
25183
25760
  const internalSelection = useSelection(derivedSelectionOptions);
25184
25761
  const baseSelection = selectionProp ?? internalSelection;
25762
+ const baseAdapterMethods = baseSelection.adapterMethods;
25763
+ const readOnlyAdapterMethods = useMemo(() => ({
25764
+ getSelection: () => baseAdapterMethods.getSelection(),
25765
+ setSelection: () => {
25766
+ }
25767
+ }), [baseAdapterMethods]);
25185
25768
  const selection = useMemo(() => {
25186
25769
  if (selectionMode !== "none") return baseSelection;
25187
25770
  const noopSet = () => {
@@ -25193,9 +25776,10 @@ function SceneCanvasInner(props, ref) {
25193
25776
  remove: noopSet,
25194
25777
  toggle: noopSet,
25195
25778
  clear: noopSet,
25196
- applyClick: noopSet
25779
+ applyClick: noopSet,
25780
+ adapterMethods: readOnlyAdapterMethods
25197
25781
  };
25198
- }, [baseSelection, selectionMode]);
25782
+ }, [baseSelection, selectionMode, readOnlyAdapterMethods]);
25199
25783
  const currentSelection = selection.current;
25200
25784
  const selectionKinds = useMemo(() => {
25201
25785
  if (currentSelection.length === 0) return void 0;
@@ -25287,9 +25871,20 @@ function SceneCanvasInner(props, ref) {
25287
25871
  const [sceneLayerGate] = useState(createSceneLayerGate);
25288
25872
  const layerIsPainted = useCallback((layerId) => sceneLayerGate(surfaceViewRegistry?.surface()?.layers() ?? [], layerPaintRef.current)(layerId), [surfaceViewRegistry, sceneLayerGate]);
25289
25873
  const viewLayerGate = layerVisibility !== void 0 || layerOrder !== void 0 ? layerIsPainted : void 0;
25290
- 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({
25291
25886
  scene,
25292
- selection,
25887
+ adapter,
25293
25888
  poseDescriptor: descriptor,
25294
25889
  geometry,
25295
25890
  // The pick tolerance is declared in screen pixels; this is what converts it.
@@ -25299,10 +25894,7 @@ function SceneCanvasInner(props, ref) {
25299
25894
  // that once per walk instead of once per candidate.
25300
25895
  ...alphaFor ? { alphaOf: composedAlphaFor } : {},
25301
25896
  ...viewLayerGate ? { layerIsPainted: viewLayerGate } : {},
25302
- selectTool: selectToolWithDefaults,
25303
- ...insertTool ? { insertTool } : {},
25304
- ...layouts ? { layouts } : {},
25305
- ...poseComposition ? { poseComposition } : {}
25897
+ selectTool: selectToolWithDefaults
25306
25898
  });
25307
25899
  const getNodeAtPoint = useMemo(() => {
25308
25900
  if (!internalPickEvery) return void 0;
@@ -25814,6 +26406,7 @@ function SceneCanvasInner(props, ref) {
25814
26406
  editAnchorsExternalState,
25815
26407
  anchorEditingAllowed,
25816
26408
  layouts,
26409
+ layoutDropTarget,
25817
26410
  poseComposition,
25818
26411
  insertNodeFactories,
25819
26412
  snapPoint: toolOptions?.snapPoint,
@@ -26064,6 +26657,7 @@ function StandardActionsRegistrar({
26064
26657
  anchorEditingAllowed,
26065
26658
  poseComposition,
26066
26659
  layouts,
26660
+ layoutDropTarget,
26067
26661
  insertNodeFactories,
26068
26662
  snapPoint: snapPoint2,
26069
26663
  canvasRef,
@@ -26112,7 +26706,7 @@ function StandardActionsRegistrar({
26112
26706
  useViewportActions({ pan: viewportPanEnabled, zoom: viewportZoom, pinchZoom: viewportPinchZoom });
26113
26707
  useAreaSelectDepSource(scene, selection, poseDescriptor, poseComposition, alphaOf);
26114
26708
  useNodeAtPointDepSource(pickEvery);
26115
- useLayoutDepSource(layouts);
26709
+ useLayoutDepSource(layouts, layoutDropTarget);
26116
26710
  useInsertDepSource(scene, adapter, insertNodeFactories);
26117
26711
  useSnapDepSource(snapPoint2);
26118
26712
  useIngestionDepSource(canvasRef, () => currentViewRef.current, ingestionResolveSrc, ingestionSvg, ingestionClipboard);
@@ -26305,7 +26899,8 @@ function shapeSchema(opts = {}) {
26305
26899
  letterSpacing: { kind: "number", name: "Tracking", description: "Extra advance per glyph, world units.", default: 0, step: 0.1 },
26306
26900
  underline: { kind: "boolean", name: "Underline", description: "Underline the text.", default: false, control: "toggle", short: "U", pair: "Decoration" },
26307
26901
  strikethrough: { kind: "boolean", name: "Strikethrough", description: "Strike through the text.", default: false, control: "toggle", short: "S", pair: "Decoration" },
26308
- 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" }] }
26309
26904
  }
26310
26905
  },
26311
26906
  paragraph: {
@@ -27332,6 +27927,20 @@ function createTextLayer(opts) {
27332
27927
  }
27333
27928
  };
27334
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
+ }
27335
27944
  function pointInTextPose(x, y, pose, opts = {}) {
27336
27945
  const p = opts.padding ?? 0;
27337
27946
  return x >= pose.x - p && x <= pose.x + pose.width + p && y >= pose.y - p && y <= pose.y + pose.height + p;
@@ -27350,8 +27959,8 @@ function caretIndexAt(x, y, pose) {
27350
27959
  break;
27351
27960
  }
27352
27961
  }
27353
- const { cells } = line;
27354
- if (cells.length === 0) return line.srcEnd;
27962
+ if (line.cells.length === 0) return line.srcEnd;
27963
+ const cells = sourceClusters(line.cells);
27355
27964
  const visual = cells.map((_, i) => i).sort((a, b) => cells[a].x - cells[b].x);
27356
27965
  for (const i of visual) {
27357
27966
  const c = cells[i];
@@ -27384,6 +27993,7 @@ function solidColor(p) {
27384
27993
  if ("color" in p) return p.color;
27385
27994
  return null;
27386
27995
  }
27996
+ var TRANSFORMS = /* @__PURE__ */ new Set(["none", "uppercase", "lowercase", "capitalize"]);
27387
27997
  var EMPTY_STYLE = {
27388
27998
  bold: false,
27389
27999
  italic: false,
@@ -27439,6 +28049,8 @@ function styleStateFromElement(el, parent) {
27439
28049
  const shift = parseFloat(rawShift);
27440
28050
  if (Number.isFinite(shift)) next.baselineShift = shift;
27441
28051
  }
28052
+ const tt = el.style.textTransform;
28053
+ if (TRANSFORMS.has(tt)) next.textTransform = tt;
27442
28054
  if (el.style.fontFamily) next.fontFamily = el.style.fontFamily;
27443
28055
  if (el.style.color) next.color = el.style.color;
27444
28056
  const ls = el.style.letterSpacing;
@@ -27452,7 +28064,7 @@ function styleStateFromElement(el, parent) {
27452
28064
  return next;
27453
28065
  }
27454
28066
  function styleEquals(a, b) {
27455
- 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;
27456
28068
  }
27457
28069
  function toRun(text, style) {
27458
28070
  const run2 = { text };
@@ -27468,6 +28080,7 @@ function toRun(text, style) {
27468
28080
  if (style.script != null) run2.script = style.script;
27469
28081
  if (style.baselineShift != null) run2.baselineShift = style.baselineShift;
27470
28082
  if (style.fontScale != null) run2.fontScale = style.fontScale;
28083
+ if (style.textTransform != null) run2.textTransform = style.textTransform;
27471
28084
  return run2;
27472
28085
  }
27473
28086
  function domToRuns(parent) {
@@ -27531,6 +28144,7 @@ function runsToDom(runs, parent) {
27531
28144
  span.setAttribute("data-baseline-shift", String(run2.baselineShift));
27532
28145
  }
27533
28146
  if (run2.letterSpacing != null) span.style.letterSpacing = `${run2.letterSpacing}px`;
28147
+ if (run2.textTransform != null) span.style.textTransform = run2.textTransform;
27534
28148
  parent.appendChild(span);
27535
28149
  }
27536
28150
  }
@@ -27590,7 +28204,8 @@ var STYLE_KEYS = [
27590
28204
  "stroke",
27591
28205
  "script",
27592
28206
  "baselineShift",
27593
- "fontScale"
28207
+ "fontScale",
28208
+ "textTransform"
27594
28209
  ];
27595
28210
  var FLAG_KEYS = /* @__PURE__ */ new Set([
27596
28211
  "bold",
@@ -28231,6 +28846,7 @@ function applyOverlayStyle(el, style) {
28231
28846
  if (style.underline) decorations.push("underline");
28232
28847
  if (style.strikethrough) decorations.push("line-through");
28233
28848
  el.style.textDecoration = decorations.length > 0 ? decorations.join(" ") : "none";
28849
+ el.style.textTransform = style.textTransform;
28234
28850
  el.style.whiteSpace = style.wrap ? "pre-wrap" : "pre";
28235
28851
  el.style.overflowWrap = "normal";
28236
28852
  el.style.wordBreak = "normal";
@@ -28432,7 +29048,8 @@ var OVERRIDES = [
28432
29048
  "fill",
28433
29049
  "script",
28434
29050
  "baselineShift",
28435
- "fontScale"
29051
+ "fontScale",
29052
+ "textTransform"
28436
29053
  ];
28437
29054
  function effectiveRangeStyle(range, style, paint) {
28438
29055
  const resolved = resolveTextStyle(style, paint);
@@ -28440,7 +29057,8 @@ function effectiveRangeStyle(range, style, paint) {
28440
29057
  const out = {
28441
29058
  fontFamily: resolved.fontFamily,
28442
29059
  fontSize: resolved.fontSize,
28443
- letterSpacing: resolved.letterSpacing
29060
+ letterSpacing: resolved.letterSpacing,
29061
+ textTransform: resolved.textTransform
28444
29062
  };
28445
29063
  if (resolved.fill !== null) out.fill = resolved.fill;
28446
29064
  for (const key of FLAGS) {
@@ -31560,6 +32178,6 @@ function snapPoint(opts) {
31560
32178
  };
31561
32179
  }
31562
32180
 
31563
- 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 };
31564
- //# sourceMappingURL=chunk-QEZL53V6.js.map
31565
- //# sourceMappingURL=chunk-QEZL53V6.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