@weasel-js/labkit 0.7.2 → 0.8.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.
@@ -21,6 +21,7 @@ var GESTURE_DESCRIPTORS = [
21
21
  { name: "keyUp", hasTarget: false, arg: { name: "key", values: "free" } },
22
22
  { name: "keyHeld", hasTarget: false, arg: { name: "key", values: "free" } },
23
23
  { name: "contextMenu", hasTarget: true },
24
+ { name: "longPress", hasTarget: true },
24
25
  { name: "multiTouchTap", hasTarget: false, arg: { name: "fingers", values: ["2", "3", "4"] } }
25
26
  ];
26
27
  var BY_NAME = new Map(
@@ -318,6 +319,8 @@ function actionClause(parsed, required) {
318
319
  return `the user taps with ${parsed.arg ?? "multiple"} fingers`;
319
320
  case "contextMenu":
320
321
  return `the user ${modPrefix}opens the context menu${target}`;
322
+ case "longPress":
323
+ return `the user ${modPrefix}presses and holds${target}`;
321
324
  case "click":
322
325
  return `the user ${modPrefix}clicks${target}`;
323
326
  case "pointerDown":
@@ -502,6 +505,11 @@ function matchSpec(e, spec, isMac, phaseCtx) {
502
505
  if (!matchModifiers(e, spec.mods, isMac)) return false;
503
506
  return matchTarget(e.target, spec.target, e.bodyTarget, e.bodyKind);
504
507
  }
508
+ case "longPress": {
509
+ if (e.kind !== "longpress") return false;
510
+ if (!matchModifiers(e, spec.mods, isMac)) return false;
511
+ return matchTarget(e.affordance, spec.target, e.bodyTarget, e.bodyKind);
512
+ }
505
513
  case "drag": {
506
514
  if (e.kind !== "pointerdown") return false;
507
515
  if (e.stage === "press") return false;
@@ -540,7 +548,7 @@ function matchSpec(e, spec, isMac, phaseCtx) {
540
548
  }
541
549
  var EMPTY_ENGAGED = /* @__PURE__ */ new Set();
542
550
 
543
- // ../core/dist/chunk-SYM6RAM4.js
551
+ // ../core/dist/chunk-6DAUIMTO.js
544
552
  var routing_exports = {};
545
553
  __export(routing_exports, {
546
554
  GESTURE_DESCRIPTORS: () => GESTURE_DESCRIPTORS,
@@ -613,6 +621,7 @@ var SPEC_KIND_TO_GESTURE = {
613
621
  click: "click",
614
622
  doubleClick: "dblTap",
615
623
  contextMenu: "contextMenu",
624
+ longPress: "longPress",
616
625
  drag: "drag",
617
626
  pointerDown: "pointerDown",
618
627
  multiTouch: void 0,
@@ -873,7 +882,7 @@ function transformCoords(coords, m) {
873
882
  return out;
874
883
  }
875
884
 
876
- // ../core/dist/chunk-CQNKCG34.js
885
+ // ../core/dist/chunk-GLZLSPGJ.js
877
886
  function resolveUnit(v, unitSystem) {
878
887
  if (typeof v === "number") return v;
879
888
  if (!unitSystem) {
@@ -1584,7 +1593,7 @@ function createDeleteOp(args) {
1584
1593
  }
1585
1594
  registerOpFactory("delete", (args) => createDeleteOp(args));
1586
1595
 
1587
- // ../core/dist/chunk-VOVKONXA.js
1596
+ // ../core/dist/chunk-BPRHGTLQ.js
1588
1597
  function createTransformOp(args) {
1589
1598
  const { id, from, to, label, coalesceKey = `transform:${id}` } = args;
1590
1599
  return {
@@ -1665,7 +1674,7 @@ function snap(strategy, opts = {}) {
1665
1674
  };
1666
1675
  }
1667
1676
 
1668
- // ../core/dist/chunk-BGGZ4CVF.js
1677
+ // ../core/dist/chunk-CMBE45MT.js
1669
1678
  function lockAspectWithModifier(opts = {}) {
1670
1679
  const { key = "shift" } = opts;
1671
1680
  return {
@@ -2210,17 +2219,17 @@ function dynamicPageTextureId(page) {
2210
2219
  return `weasel-dyn-sdf-page-${page}`;
2211
2220
  }
2212
2221
  var uploadedVersions = /* @__PURE__ */ new WeakMap();
2213
- function syncDynamicPageTexture(cache5, pageIndex) {
2222
+ function syncDynamicPageTexture(cache6, pageIndex) {
2214
2223
  const page = pages[pageIndex];
2215
2224
  if (!page) return false;
2216
2225
  const id = dynamicPageTextureId(pageIndex);
2217
- let seen = uploadedVersions.get(cache5);
2226
+ let seen = uploadedVersions.get(cache6);
2218
2227
  if (!seen) {
2219
2228
  seen = /* @__PURE__ */ new Map();
2220
- uploadedVersions.set(cache5, seen);
2229
+ uploadedVersions.set(cache6, seen);
2221
2230
  }
2222
- if (!cache5.has(id)) {
2223
- cache5.uploadR8(id, PAGE_SIZE, PAGE_SIZE, page.data);
2231
+ if (!cache6.has(id)) {
2232
+ cache6.uploadR8(id, PAGE_SIZE, PAGE_SIZE, page.data);
2224
2233
  seen.set(pageIndex, page.version);
2225
2234
  return true;
2226
2235
  }
@@ -2233,7 +2242,7 @@ function syncDynamicPageTexture(cache5, pageIndex) {
2233
2242
  const src = (patch.y + row) * PAGE_SIZE + patch.x;
2234
2243
  tight.set(page.data.subarray(src, src + patch.w), row * patch.w);
2235
2244
  }
2236
- cache5.subImageR8(id, patch.x, patch.y, patch.w, patch.h, tight);
2245
+ cache6.subImageR8(id, patch.x, patch.y, patch.w, patch.h, tight);
2237
2246
  }
2238
2247
  seen.set(pageIndex, page.version);
2239
2248
  return true;
@@ -2441,6 +2450,23 @@ var slots = /* @__PURE__ */ new Map();
2441
2450
  function slotKey(family, weight, style) {
2442
2451
  return `${family}|${weight}|${style}`;
2443
2452
  }
2453
+ function closeContours(d) {
2454
+ let out = "";
2455
+ let start = 0;
2456
+ for (let i = 1; i < d.length; i++) {
2457
+ const c = d[i];
2458
+ if (c !== "M" && c !== "m") continue;
2459
+ out += closeOne(d.slice(start, i));
2460
+ start = i;
2461
+ }
2462
+ return out + closeOne(d.slice(start));
2463
+ }
2464
+ function closeOne(contour) {
2465
+ const trimmed = contour.trimEnd();
2466
+ if (trimmed.length === 0) return "";
2467
+ const last = trimmed[trimmed.length - 1];
2468
+ return last === "Z" || last === "z" ? trimmed : `${trimmed}Z`;
2469
+ }
2444
2470
  function glyphOutline(family, weight, style, cp) {
2445
2471
  const slot = slots.get(slotKey(family, weight, style));
2446
2472
  if (!slot) return null;
@@ -2453,7 +2479,8 @@ function glyphOutline(family, weight, style, cp) {
2453
2479
  if (cached2 !== void 0) return cached2;
2454
2480
  let d = null;
2455
2481
  try {
2456
- d = slot.face.glyphD(cp);
2482
+ const raw = slot.face.glyphD(cp);
2483
+ d = raw === null ? null : closeContours(raw);
2457
2484
  } catch (err) {
2458
2485
  warnOnce(
2459
2486
  `glyph|${slotKey(family, weight, style)}`,
@@ -2982,7 +3009,7 @@ function serialToEntry(se, custom, logger) {
2982
3009
  };
2983
3010
  }
2984
3011
 
2985
- // ../core/dist/chunk-5F3PIERN.js
3012
+ // ../core/dist/chunk-DLV5F4NY.js
2986
3013
  function worldToScreen(worldX, worldY, view) {
2987
3014
  return [view.panX + worldX * view.zoom.x, view.panY + worldY * view.zoom.y];
2988
3015
  }
@@ -4355,6 +4382,16 @@ function extractPolygon(p, opts) {
4355
4382
  out.push(next);
4356
4383
  return next;
4357
4384
  };
4385
+ const dropDuplicateClosingPoint = () => {
4386
+ if (!pts || !aA || !aB || !aT) return;
4387
+ const n = pts.length / 2;
4388
+ if (n < 2) return;
4389
+ if (pts[0] !== pts[(n - 1) * 2] || pts[1] !== pts[(n - 1) * 2 + 1]) return;
4390
+ pts.length -= 2;
4391
+ aA.length -= 1;
4392
+ aB.length -= 1;
4393
+ aT.length -= 1;
4394
+ };
4358
4395
  const commit = (target) => {
4359
4396
  if (!pts || !aA || !aB || !aT) return;
4360
4397
  target.anchorA = new Uint32Array(aA);
@@ -4438,7 +4475,10 @@ function extractPolygon(p, opts) {
4438
4475
  break;
4439
4476
  }
4440
4477
  case PATH_Z2: {
4441
- if (current) current.closed = true;
4478
+ if (current) {
4479
+ current.closed = true;
4480
+ dropDuplicateClosingPoint();
4481
+ }
4442
4482
  break;
4443
4483
  }
4444
4484
  default:
@@ -4919,8 +4959,20 @@ function sameFill(a, b) {
4919
4959
  }
4920
4960
  return false;
4921
4961
  }
4922
- function groupKey(family, weight, style, synthetic, fill, source, page) {
4923
- return `${family}|${weight}|${style}|${synthetic.bold ? 1 : 0}${synthetic.italic ? 1 : 0}|${fillKey(fill)}|${source}|${page}`;
4962
+ function strokeKey(s) {
4963
+ if (!s) return "-";
4964
+ return [
4965
+ fillKey(s.paint),
4966
+ s.width ?? 1,
4967
+ s.join ?? "miter",
4968
+ s.cap ?? "butt",
4969
+ s.miterLimit ?? "",
4970
+ s.align ?? "center",
4971
+ (s.dash ?? []).join(",")
4972
+ ].join(":");
4973
+ }
4974
+ function groupKey(family, weight, style, synthetic, fill, stroke, source, page) {
4975
+ return `${family}|${weight}|${style}|${synthetic.bold ? 1 : 0}${synthetic.italic ? 1 : 0}|${fillKey(fill)}|${strokeKey(stroke)}|${source}|${page}`;
4924
4976
  }
4925
4977
  function getOrCreateGroup(ctx, run, resolved, page, sourceOverride) {
4926
4978
  const atlasFamily = resolved.resolved.family;
@@ -4933,6 +4985,7 @@ function getOrCreateGroup(ctx, run, resolved, page, sourceOverride) {
4933
4985
  resolvedStyle,
4934
4986
  resolved.synthetic,
4935
4987
  run.fill,
4988
+ source === "outline" ? run.stroke : void 0,
4936
4989
  source,
4937
4990
  page
4938
4991
  );
@@ -4946,6 +4999,9 @@ function getOrCreateGroup(ctx, run, resolved, page, sourceOverride) {
4946
4999
  source,
4947
5000
  page,
4948
5001
  fill: run.fill,
5002
+ // Only the outline tier can paint it, and carrying it on an SDF group
5003
+ // would be a promise the renderer cannot keep.
5004
+ ...source === "outline" && run.stroke !== void 0 ? { stroke: run.stroke } : {},
4949
5005
  quads: [],
4950
5006
  glyphs: []
4951
5007
  };
@@ -5542,6 +5598,34 @@ function outlineMesh(key, d) {
5542
5598
  cache3.set(key, mesh);
5543
5599
  return mesh;
5544
5600
  }
5601
+ var OUTLINE_STROKE_MESH_CACHE_LIMIT = 2048;
5602
+ var WIDTH_QUANTUM = 1 / 1024;
5603
+ var cache4 = /* @__PURE__ */ new Map();
5604
+ function quantizeEmWidth(emWidth) {
5605
+ return Math.round(emWidth / WIDTH_QUANTUM) * WIDTH_QUANTUM;
5606
+ }
5607
+ function outlineStrokeMesh(glyphKey, d, emWidth, stroke) {
5608
+ if (!(emWidth > 0)) return null;
5609
+ const key = [
5610
+ glyphKey,
5611
+ emWidth,
5612
+ stroke.join ?? "miter",
5613
+ stroke.cap ?? "butt",
5614
+ stroke.miterLimit ?? "",
5615
+ stroke.align ?? "center",
5616
+ (stroke.dash ?? []).join(",")
5617
+ ].join("|");
5618
+ const cached2 = cache4.get(key);
5619
+ if (cached2 !== void 0) return cached2.indices.length === 0 ? null : cached2;
5620
+ if (cache4.size >= OUTLINE_STROKE_MESH_CACHE_LIMIT) cache4 = /* @__PURE__ */ new Map();
5621
+ const mesh = tessellateStroke(
5622
+ pathFromD(d),
5623
+ { ...stroke, width: emWidth },
5624
+ { flattenTolerance: OUTLINE_FLATTEN_TOLERANCE }
5625
+ );
5626
+ cache4.set(key, mesh);
5627
+ return mesh.indices.length === 0 ? null : mesh;
5628
+ }
5545
5629
  function setColorMatrixUniforms(ctx, prog) {
5546
5630
  const gl = ctx.gl;
5547
5631
  const cm = ctx.state.colorMatrix;
@@ -6115,16 +6199,30 @@ function drawText(ctx, cmd) {
6115
6199
  var SYNTHETIC_ITALIC_RADIANS = 0.2094;
6116
6200
  function drawTextOutlineGroup(ctx, group, dy) {
6117
6201
  const mesh = outlineGroupMesh(group, dy);
6118
- if (!mesh) return;
6119
- drawPathFillByKind(ctx, group.fill, ctx.meshCache.uploadTransient(mesh));
6202
+ if (mesh) drawPathFillByKind(ctx, group.fill, ctx.meshCache.uploadTransient(mesh));
6203
+ if (!group.stroke) return;
6204
+ const strokeMesh = outlineGroupStrokeMesh(group, dy);
6205
+ if (strokeMesh) {
6206
+ drawPathFillByKind(ctx, group.stroke.paint, ctx.meshCache.uploadTransient(strokeMesh));
6207
+ }
6120
6208
  }
6121
6209
  function outlineGroupMesh(group, dy) {
6210
+ return mergeGlyphMeshes(group, dy, (glyph) => outlineMesh(glyph.key, glyph.d));
6211
+ }
6212
+ function outlineGroupStrokeMesh(group, dy) {
6213
+ const stroke = group.stroke;
6214
+ if (!stroke) return null;
6215
+ const width = stroke.width ?? 1;
6216
+ if (!(width > 0)) return null;
6217
+ return mergeGlyphMeshes(group, dy, (glyph) => glyph.scale > 0 ? outlineStrokeMesh(glyph.key, glyph.d, quantizeEmWidth(width / glyph.scale), stroke) : null);
6218
+ }
6219
+ function mergeGlyphMeshes(group, dy, meshFor) {
6122
6220
  const parts = [];
6123
6221
  let vertexFloats = 0;
6124
6222
  let indexCount = 0;
6125
6223
  for (const glyph of group.glyphs) {
6126
- const mesh = outlineMesh(glyph.key, glyph.d);
6127
- if (mesh.indices.length === 0) continue;
6224
+ const mesh = meshFor(glyph);
6225
+ if (!mesh || mesh.indices.length === 0) continue;
6128
6226
  parts.push({ mesh, glyph });
6129
6227
  vertexFloats += mesh.vertices.length;
6130
6228
  indexCount += mesh.indices.length;
@@ -6964,6 +7062,103 @@ function clientToCanvas(canvas, clientX, clientY) {
6964
7062
  function clientToCanvasRect(rect, clientX, clientY) {
6965
7063
  return [clientX - rect.left, clientY - rect.top];
6966
7064
  }
7065
+ var COARSE_TARGET_SCALE = 1.75;
7066
+ function resolveDeviceProfile(detected, overrides) {
7067
+ const coarsePointer2 = overrides?.coarsePointer ?? detected.coarsePointer;
7068
+ const canHover2 = overrides?.canHover ?? detected.canHover;
7069
+ const dpr = overrides?.dpr ?? detected.dpr;
7070
+ const targetScale = overrides?.targetScale ?? (coarsePointer2 ? COARSE_TARGET_SCALE : 1);
7071
+ return { coarsePointer: coarsePointer2, canHover: canHover2, dpr, targetScale };
7072
+ }
7073
+ var DEFAULT_DEVICE_PROFILE = resolveDeviceProfile({
7074
+ coarsePointer: false,
7075
+ canHover: true,
7076
+ dpr: 1
7077
+ });
7078
+ var COARSE_QUERY = "(pointer: coarse)";
7079
+ var HOVER_QUERY = "(hover: hover)";
7080
+ function hasMatchMedia() {
7081
+ return typeof window !== "undefined" && typeof window.matchMedia === "function";
7082
+ }
7083
+ function detectOnce() {
7084
+ const dpr = typeof window !== "undefined" && window.devicePixelRatio || 1;
7085
+ if (!hasMatchMedia()) {
7086
+ return {
7087
+ coarsePointer: DEFAULT_DEVICE_PROFILE.coarsePointer,
7088
+ canHover: DEFAULT_DEVICE_PROFILE.canHover,
7089
+ dpr
7090
+ };
7091
+ }
7092
+ return {
7093
+ coarsePointer: window.matchMedia(COARSE_QUERY).matches,
7094
+ canHover: window.matchMedia(HOVER_QUERY).matches,
7095
+ dpr
7096
+ };
7097
+ }
7098
+ function useDetectedFacts() {
7099
+ const [facts, setFacts] = useState(detectOnce);
7100
+ useEffect(() => {
7101
+ if (!hasMatchMedia()) return;
7102
+ const coarseMq = window.matchMedia(COARSE_QUERY);
7103
+ const hoverMq = window.matchMedia(HOVER_QUERY);
7104
+ const onCoarse = (e) => setFacts((f) => ({ ...f, coarsePointer: e.matches }));
7105
+ const onHover = (e) => setFacts((f) => ({ ...f, canHover: e.matches }));
7106
+ coarseMq.addEventListener("change", onCoarse);
7107
+ hoverMq.addEventListener("change", onHover);
7108
+ let disposed = false;
7109
+ let densityMq = null;
7110
+ const onDensity = () => {
7111
+ if (disposed) return;
7112
+ const next = window.devicePixelRatio || 1;
7113
+ setFacts((f) => f.dpr === next ? f : { ...f, dpr: next });
7114
+ arm();
7115
+ };
7116
+ function arm() {
7117
+ if (disposed) return;
7118
+ densityMq?.removeEventListener("change", onDensity);
7119
+ densityMq = window.matchMedia(
7120
+ `(resolution: ${window.devicePixelRatio || 1}dppx)`
7121
+ );
7122
+ densityMq.addEventListener("change", onDensity);
7123
+ }
7124
+ arm();
7125
+ setFacts({
7126
+ coarsePointer: coarseMq.matches,
7127
+ canHover: hoverMq.matches,
7128
+ dpr: window.devicePixelRatio || 1
7129
+ });
7130
+ return () => {
7131
+ disposed = true;
7132
+ coarseMq.removeEventListener("change", onCoarse);
7133
+ hoverMq.removeEventListener("change", onHover);
7134
+ densityMq?.removeEventListener("change", onDensity);
7135
+ };
7136
+ }, []);
7137
+ return facts;
7138
+ }
7139
+ var DeviceProfileContext = createContext(null);
7140
+ function useDeviceProfile(overrides) {
7141
+ const provided = useContext(DeviceProfileContext);
7142
+ const detected = useDetectedFacts();
7143
+ const base = provided ?? detected;
7144
+ const { coarsePointer: coarsePointer2, canHover: canHover2, dpr, targetScale } = overrides ?? {};
7145
+ return useMemo(
7146
+ () => resolveDeviceProfile(base, {
7147
+ ...coarsePointer2 !== void 0 ? { coarsePointer: coarsePointer2 } : {},
7148
+ ...canHover2 !== void 0 ? { canHover: canHover2 } : {},
7149
+ ...dpr !== void 0 ? { dpr } : {},
7150
+ ...targetScale !== void 0 ? { targetScale } : {}
7151
+ }),
7152
+ [base.coarsePointer, base.canHover, base.dpr, coarsePointer2, canHover2, dpr, targetScale]
7153
+ );
7154
+ }
7155
+ function DeviceProfileProvider({ value, children }) {
7156
+ const profile = useDeviceProfile(value);
7157
+ return createElement(DeviceProfileContext.Provider, { value: profile }, children);
7158
+ }
7159
+ var HANDLE_BASE_PX = 8;
7160
+ var ANCHOR_HIT_BASE_PX = 8;
7161
+ var ROTATION_HANDLE_BASE_PX = 24;
6967
7162
  function zoomAt(view, anchor, factor, opts) {
6968
7163
  const fx = typeof factor === "number" ? factor : factor.x;
6969
7164
  const fy = typeof factor === "number" ? factor : factor.y;
@@ -8694,6 +8889,10 @@ var clearSelectionAction = {
8694
8889
  return true;
8695
8890
  }
8696
8891
  };
8892
+ var counter = 0;
8893
+ function freshNodeId() {
8894
+ return asNodeId(`n${(counter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`);
8895
+ }
8697
8896
  function translatePose(pose, dx, dy) {
8698
8897
  const p = pose;
8699
8898
  return {
@@ -8702,10 +8901,6 @@ function translatePose(pose, dx, dy) {
8702
8901
  y: (p["y"] ?? 0) + dy
8703
8902
  };
8704
8903
  }
8705
- var cloneIdCounter = 0;
8706
- function freshCloneId() {
8707
- return asNodeId(`n${(cloneIdCounter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`);
8708
- }
8709
8904
  var cloneAction = {
8710
8905
  id: "clone",
8711
8906
  label: "Clone",
@@ -8772,7 +8967,7 @@ var cloneAction = {
8772
8967
  if (origin === void 0 || !originNode) continue;
8773
8968
  const newPose = translatePose(origin, dx, dy);
8774
8969
  const node = {
8775
- id: freshCloneId(),
8970
+ id: freshNodeId(),
8776
8971
  kind: originNode.kind,
8777
8972
  layer: originNode.layer,
8778
8973
  pose: newPose,
@@ -9555,6 +9750,119 @@ var pinchZoomAction = {
9555
9750
  },
9556
9751
  enabled: () => true
9557
9752
  };
9753
+ function hostIndex(scene, id) {
9754
+ const node = scene.get(id);
9755
+ const siblings = node?.parent == null ? scene.roots : scene.childrenOf(node.parent);
9756
+ return siblings.indexOf(id);
9757
+ }
9758
+ function hasSelectedAncestor(scene, id, set) {
9759
+ let parent = scene.get(id)?.parent ?? null;
9760
+ while (parent != null) {
9761
+ if (set.has(parent)) return true;
9762
+ parent = scene.get(parent)?.parent ?? null;
9763
+ }
9764
+ return false;
9765
+ }
9766
+ function buildDeleteOps(scene, ids, label) {
9767
+ const set = new Set(ids);
9768
+ const ops = [];
9769
+ for (const id of ids) {
9770
+ const node = scene.get(id);
9771
+ if (!node) continue;
9772
+ if (hasSelectedAncestor(scene, id, set)) continue;
9773
+ ops.push(createDeleteOp({
9774
+ node,
9775
+ index: hostIndex(scene, id),
9776
+ label
9777
+ }));
9778
+ }
9779
+ return ops;
9780
+ }
9781
+ var deleteAction = {
9782
+ id: "delete",
9783
+ label: "Delete",
9784
+ // Suppressed while any tool is mid-gesture — accidentally hitting
9785
+ // Delete during a drag shouldn't wipe the selection out from under
9786
+ // the in-flight handle.
9787
+ defaultBinding: {
9788
+ kind: "key",
9789
+ key: ["Delete", "Backspace"],
9790
+ phase: [{ channel: "*", phase: "initial" }]
9791
+ },
9792
+ eligible: { capability: "edits-page" },
9793
+ requires: ["scene", "selection", "applyOps"],
9794
+ invoker: {
9795
+ timing: "immediate",
9796
+ run: (deps) => {
9797
+ const selection = deps.selection;
9798
+ const scene = deps.scene;
9799
+ const applyOps = deps.applyOps;
9800
+ if (!selection || !scene) return;
9801
+ const ids = selection.get();
9802
+ if (ids.length === 0) return;
9803
+ const ops = buildDeleteOps(scene, ids, "Delete");
9804
+ if (ops.length > 0) {
9805
+ if (applyOps) applyOps(ops, "Delete");
9806
+ else scene.applyBatch(ops, "Delete", defaultCommitAdapter(scene));
9807
+ }
9808
+ selection.set([]);
9809
+ }
9810
+ },
9811
+ enabled: () => true
9812
+ };
9813
+ function requiresSelection(deps) {
9814
+ const selection = deps?.selection;
9815
+ if (!selection || selection.get().length === 0) {
9816
+ return ActionDisabledReason.SelectionRequired;
9817
+ }
9818
+ return true;
9819
+ }
9820
+ var clipboardCopyAction = {
9821
+ id: "clipboard.copy",
9822
+ label: "Copy",
9823
+ defaultBinding: { kind: "key", key: "c", mods: { mod: true } },
9824
+ // Same gate as `duplicate`: a mode that can neither edit the page nor own a
9825
+ // selection has nothing to copy. Notably excludes `text-edit`, where Cmd+C
9826
+ // must stay the browser's own copy of the caret range.
9827
+ eligible: { capability: ["edits-page", "creates-selection"] },
9828
+ // `selection` is read by the `enabled` gate, not the invoker — the
9829
+ // dispatcher builds deps for both, and an undeclared read is undefined.
9830
+ requires: ["clipboard", "selection"],
9831
+ invoker: {
9832
+ timing: "immediate",
9833
+ run: (deps) => {
9834
+ deps.clipboard?.copy();
9835
+ }
9836
+ },
9837
+ enabled: requiresSelection
9838
+ };
9839
+ var clipboardCutAction = {
9840
+ id: "clipboard.cut",
9841
+ label: "Cut",
9842
+ defaultBinding: { kind: "key", key: "x", mods: { mod: true } },
9843
+ eligible: { capability: "edits-page" },
9844
+ requires: ["clipboard", "scene", "selection", "applyOps"],
9845
+ invoker: {
9846
+ timing: "immediate",
9847
+ run: (deps) => {
9848
+ const clipboard = deps.clipboard;
9849
+ const selection = deps.selection;
9850
+ const scene = deps.scene;
9851
+ const applyOps = deps.applyOps;
9852
+ if (!clipboard || !selection || !scene) return;
9853
+ const ids = selection.get();
9854
+ if (ids.length === 0) return;
9855
+ clipboard.copy();
9856
+ const ops = buildDeleteOps(scene, ids, "Cut");
9857
+ if (ops.length > 0) {
9858
+ if (applyOps) applyOps(ops, "Cut");
9859
+ else scene.applyBatch(ops, "Cut", defaultCommitAdapter(scene));
9860
+ }
9861
+ selection.set([]);
9862
+ }
9863
+ },
9864
+ enabled: requiresSelection
9865
+ };
9558
9866
  var enterTextEditAction = {
9559
9867
  id: "enterTextEdit",
9560
9868
  label: "Enter text edit",
@@ -9679,24 +9987,45 @@ var selectAllAction = {
9679
9987
  }
9680
9988
  }
9681
9989
  };
9682
- function hostIndex(scene, id) {
9990
+ var DUPLICATE_OFFSET = 12;
9991
+ function translatePose2(pose, dx, dy) {
9992
+ const p = pose;
9993
+ return { ...p, x: (p["x"] ?? 0) + dx, y: (p["y"] ?? 0) + dy };
9994
+ }
9995
+ function hasSelectedAncestor2(scene, id, set) {
9996
+ let parent = scene.get(id)?.parent ?? null;
9997
+ while (parent != null) {
9998
+ if (set.has(parent)) return true;
9999
+ parent = scene.get(parent)?.parent ?? null;
10000
+ }
10001
+ return false;
10002
+ }
10003
+ function copySubtree(scene, id, parent, offset, offsetDescendants, ops) {
9683
10004
  const node = scene.get(id);
9684
- const siblings = node?.parent == null ? scene.roots : scene.childrenOf(node.parent);
9685
- return siblings.indexOf(id);
10005
+ if (!node) return null;
10006
+ const newId = freshNodeId();
10007
+ const copy = {
10008
+ ...node,
10009
+ id: newId,
10010
+ pose: translatePose2(node.pose, offset.dx, offset.dy),
10011
+ parent
10012
+ };
10013
+ ops.push(createInsertOp({ node: copy, label: "Duplicate" }));
10014
+ const childOffset = offsetDescendants ? offset : { dx: 0, dy: 0 };
10015
+ for (const child of scene.childrenOf(id)) {
10016
+ copySubtree(scene, child, newId, childOffset, offsetDescendants, ops);
10017
+ }
10018
+ return newId;
9686
10019
  }
9687
- var deleteAction = {
9688
- id: "delete",
9689
- label: "Delete",
9690
- // Suppressed while any tool is mid-gesture accidentally hitting
9691
- // Delete during a drag shouldn't wipe the selection out from under
9692
- // the in-flight handle.
9693
- defaultBinding: {
9694
- kind: "key",
9695
- key: ["Delete", "Backspace"],
9696
- phase: [{ channel: "*", phase: "initial" }]
9697
- },
9698
- eligible: { capability: "edits-page" },
9699
- requires: ["scene", "selection", "applyOps"],
10020
+ var duplicateAction = {
10021
+ id: "duplicate",
10022
+ label: "Duplicate",
10023
+ defaultBinding: { kind: "key", key: "d", mods: { mod: true } },
10024
+ eligible: { capability: ["edits-page", "creates-selection"] },
10025
+ // `selection` is read by the `enabled` gate as well as the invoker. An
10026
+ // undeclared read throws inside the dev-build deps Proxy before the gate can
10027
+ // answer, which is how this action spent a while doing nothing at all.
10028
+ requires: ["scene", "selection", "applyOps", "poseComposition"],
9700
10029
  invoker: {
9701
10030
  timing: "immediate",
9702
10031
  run: (deps) => {
@@ -9706,40 +10035,22 @@ var deleteAction = {
9706
10035
  if (!selection || !scene) return;
9707
10036
  const ids = selection.get();
9708
10037
  if (ids.length === 0) return;
10038
+ const set = new Set(ids);
10039
+ const offset = { dx: DUPLICATE_OFFSET, dy: DUPLICATE_OFFSET };
9709
10040
  const ops = [];
10041
+ const newIds = [];
9710
10042
  for (const id of ids) {
9711
- const node = scene.get(id);
10043
+ const nid = asNodeId(id);
10044
+ const node = scene.get(nid);
9712
10045
  if (!node) continue;
9713
- ops.push(createDeleteOp({
9714
- node,
9715
- index: hostIndex(scene, id),
9716
- label: "Delete"
9717
- }));
9718
- }
9719
- if (ops.length > 0) {
9720
- if (applyOps) applyOps(ops, "Delete");
9721
- else scene.applyBatch(ops, "Delete", defaultCommitAdapter(scene));
10046
+ if (hasSelectedAncestor2(scene, nid, set)) continue;
10047
+ const newId = copySubtree(scene, nid, node.parent ?? null, offset, deps.poseComposition === void 0, ops);
10048
+ if (newId) newIds.push(newId);
9722
10049
  }
9723
- selection.set([]);
9724
- }
9725
- },
9726
- enabled: () => true
9727
- };
9728
- function requiresSelection(deps) {
9729
- const selection = deps?.selection;
9730
- if (!selection || selection.get().length === 0) {
9731
- return ActionDisabledReason.SelectionRequired;
9732
- }
9733
- return true;
9734
- }
9735
- var duplicateAction = {
9736
- id: "duplicate",
9737
- label: "Duplicate",
9738
- defaultBinding: { kind: "key", key: "d", mods: { mod: true } },
9739
- eligible: { capability: ["edits-page", "creates-selection"] },
9740
- invoker: {
9741
- timing: "immediate",
9742
- run: (_deps, params) => {
10050
+ if (ops.length === 0) return;
10051
+ if (applyOps) applyOps(ops, "Duplicate");
10052
+ else scene.applyBatch(ops, "Duplicate", defaultCommitAdapter(scene));
10053
+ selection.set(newIds);
9743
10054
  }
9744
10055
  },
9745
10056
  enabled: requiresSelection
@@ -11719,7 +12030,8 @@ function resolveTextStyle(style) {
11719
12030
  selectionColor: style.selectionColor ?? null,
11720
12031
  letterSpacing: style.letterSpacing ?? DEFAULT_TEXT_STYLE.letterSpacing,
11721
12032
  underline: style.underline ?? DEFAULT_TEXT_STYLE.underline,
11722
- strikethrough: style.strikethrough ?? DEFAULT_TEXT_STYLE.strikethrough
12033
+ strikethrough: style.strikethrough ?? DEFAULT_TEXT_STYLE.strikethrough,
12034
+ ...style.stroke !== void 0 ? { stroke: style.stroke } : {}
11723
12035
  };
11724
12036
  }
11725
12037
  function numericWeight(w) {
@@ -11740,6 +12052,9 @@ function resolveRuns(runs, style) {
11740
12052
  fontWeight: run.bold ? 700 : baseWeight,
11741
12053
  fontStyle: run.italic ? "italic" : style.fontStyle,
11742
12054
  fill: run.fill ?? style.fill,
12055
+ // Unlike the decorations below, a run's stroke *replaces* the node's
12056
+ // rather than adding to it — there is only one outline to paint.
12057
+ ...(run.stroke ?? style.stroke) !== void 0 ? { stroke: run.stroke ?? style.stroke } : {},
11743
12058
  letterSpacing: run.letterSpacing ?? style.letterSpacing,
11744
12059
  // `||`, not `??`: run-level decorations are additive over the node style.
11745
12060
  underline: run.underline || style.underline,
@@ -12116,7 +12431,9 @@ var KIT_STANDARD_DESCRIPTORS = [
12116
12431
  setStrokeAction,
12117
12432
  setFillOpacityAction,
12118
12433
  setStrokeOpacityAction,
12119
- ingestAction
12434
+ ingestAction,
12435
+ clipboardCopyAction,
12436
+ clipboardCutAction
12120
12437
  ];
12121
12438
  function useStandardActions(opts) {
12122
12439
  const optsRef = useRef(opts);
@@ -12281,6 +12598,14 @@ function evaluateSelector(s, ctx) {
12281
12598
  const resizable2 = ctx.selectionResizable ?? true;
12282
12599
  if (resizable2 !== s.resizable) return false;
12283
12600
  }
12601
+ if (s.coarsePointer !== void 0) {
12602
+ const device = ctx.device ?? DEFAULT_DEVICE_PROFILE;
12603
+ if (device.coarsePointer !== s.coarsePointer) return false;
12604
+ }
12605
+ if (s.canHover !== void 0) {
12606
+ const device = ctx.device ?? DEFAULT_DEVICE_PROFILE;
12607
+ if (device.canHover !== s.canHover) return false;
12608
+ }
12284
12609
  return true;
12285
12610
  }
12286
12611
  var MAX_RULE_DEPTH = 10;
@@ -12350,6 +12675,8 @@ cond({ selection: { empty: true } });
12350
12675
  cond({ when: (c) => c.multiActive });
12351
12676
  cond({ hovering: true });
12352
12677
  cond({ hoveringSelected: true });
12678
+ cond({ coarsePointer: true });
12679
+ cond({ canHover: true });
12353
12680
  var DEFAULT_ALLOWED_CAPABILITIES = /* @__PURE__ */ new Set([
12354
12681
  ...NORMAL.allows,
12355
12682
  ...IMPLICIT_TAGS
@@ -13255,6 +13582,44 @@ function useGestureDispatcher(opts) {
13255
13582
  const DRAG_THRESHOLD_PX = 4;
13256
13583
  const bufferedDown = /* @__PURE__ */ new Map();
13257
13584
  const lastPointerDown = /* @__PURE__ */ new Map();
13585
+ const LONG_PRESS_MS = 500;
13586
+ const longPressTimers = /* @__PURE__ */ new Map();
13587
+ const cancelLongPress = (pointerId) => {
13588
+ const t = longPressTimers.get(pointerId);
13589
+ if (t !== void 0) {
13590
+ clearTimeout(t);
13591
+ longPressTimers.delete(pointerId);
13592
+ }
13593
+ };
13594
+ const cancelAllLongPress = () => {
13595
+ for (const t of longPressTimers.values()) clearTimeout(t);
13596
+ longPressTimers.clear();
13597
+ };
13598
+ const fireLongPress = (pointerId) => {
13599
+ const down = lastPointerDown.get(pointerId);
13600
+ if (!down) return;
13601
+ const shared = {
13602
+ target: down.target,
13603
+ altKey: down.altKey,
13604
+ ctrlKey: down.ctrlKey,
13605
+ metaKey: down.metaKey,
13606
+ shiftKey: down.shiftKey,
13607
+ ...down.bodyTarget !== void 0 ? { bodyTarget: down.bodyTarget } : {},
13608
+ ...down.bodyKind !== void 0 ? { bodyKind: down.bodyKind } : {}
13609
+ };
13610
+ const result = dispatch2({
13611
+ kind: "longpress",
13612
+ x: down.worldX,
13613
+ y: down.worldY,
13614
+ clientX: down.clientX,
13615
+ clientY: down.clientY,
13616
+ ...down.affordance !== void 0 ? { affordance: down.affordance } : {},
13617
+ ...shared
13618
+ });
13619
+ if (result === "unhandled") {
13620
+ dispatch2({ kind: "contextmenu", ...shared });
13621
+ }
13622
+ };
13258
13623
  const DOUBLE_CLICK_MAX_MS = 600;
13259
13624
  const DOUBLE_CLICK_MAX_PX = 8;
13260
13625
  const onKeyDown = (e) => {
@@ -13358,6 +13723,7 @@ function useGestureDispatcher(opts) {
13358
13723
  bufferedDown.set(e.pointerId, { ev, clientX: e.clientX, clientY: e.clientY });
13359
13724
  dispatch2({ ...ev, stage: "press" });
13360
13725
  lastPointerDown.set(e.pointerId, {
13726
+ target: e.target,
13361
13727
  clientX: e.clientX,
13362
13728
  clientY: e.clientY,
13363
13729
  worldX: w.x,
@@ -13370,7 +13736,19 @@ function useGestureDispatcher(opts) {
13370
13736
  metaKey: e.metaKey,
13371
13737
  shiftKey: e.shiftKey
13372
13738
  });
13739
+ if ((e.pointerType === "touch" || e.pointerType === "pen") && activePointers.size === 1) {
13740
+ cancelLongPress(e.pointerId);
13741
+ longPressTimers.set(
13742
+ e.pointerId,
13743
+ setTimeout(() => {
13744
+ longPressTimers.delete(e.pointerId);
13745
+ if (disposed) return;
13746
+ fireLongPress(e.pointerId);
13747
+ }, LONG_PRESS_MS)
13748
+ );
13749
+ }
13373
13750
  if (activePointers.size >= 2) {
13751
+ cancelAllLongPress();
13374
13752
  for (const id of activePointers) bufferedDown.delete(id);
13375
13753
  const { centroid, spread } = computeMultiTouchGeometry(pointerPositions);
13376
13754
  if (!multiTouchStart) {
@@ -13401,6 +13779,7 @@ function useGestureDispatcher(opts) {
13401
13779
  const dy = e.clientY - buffered.clientY;
13402
13780
  if (Math.hypot(dx, dy) >= DRAG_THRESHOLD_PX) {
13403
13781
  bufferedDown.delete(e.pointerId);
13782
+ cancelLongPress(e.pointerId);
13404
13783
  dispatch2(buffered.ev);
13405
13784
  }
13406
13785
  }
@@ -13514,6 +13893,7 @@ function useGestureDispatcher(opts) {
13514
13893
  clearHoverCursor();
13515
13894
  };
13516
13895
  const onPointerUp = (e) => {
13896
+ cancelLongPress(e.pointerId);
13517
13897
  const prevSize = activePointers.size;
13518
13898
  activePointers.delete(e.pointerId);
13519
13899
  pointerPositions.delete(e.pointerId);
@@ -13618,6 +13998,7 @@ function useGestureDispatcher(opts) {
13618
13998
  }, 0);
13619
13999
  };
13620
14000
  const onPointerCancel = (e) => {
14001
+ cancelLongPress(e.pointerId);
13621
14002
  activePointers.delete(e.pointerId);
13622
14003
  pointerPositions.delete(e.pointerId);
13623
14004
  lastPointerDown.delete(e.pointerId);
@@ -13740,6 +14121,7 @@ function useGestureDispatcher(opts) {
13740
14121
  window.removeEventListener("paste", onPaste);
13741
14122
  canvas?.classList.remove(DROPOVER_CLASS);
13742
14123
  clearHoverCursor();
14124
+ cancelAllLongPress();
13743
14125
  disposed = true;
13744
14126
  dispatcher.cancelAll("cancel");
13745
14127
  };
@@ -14552,7 +14934,7 @@ function textLineBoxes(pose, opts = {}) {
14552
14934
  }
14553
14935
  return out;
14554
14936
  }
14555
- var cache4 = /* @__PURE__ */ new Map();
14937
+ var cache5 = /* @__PURE__ */ new Map();
14556
14938
  var subscribers2 = /* @__PURE__ */ new Set();
14557
14939
  var defaultLoader = async (src) => {
14558
14940
  if (typeof Image === "undefined" || typeof createImageBitmap === "undefined") {
@@ -14570,28 +14952,28 @@ function notify() {
14570
14952
  }
14571
14953
  function startLoad(src) {
14572
14954
  const entry = { status: "loading" };
14573
- cache4.set(src, entry);
14955
+ cache5.set(src, entry);
14574
14956
  loader(src).then(
14575
14957
  (bitmap) => {
14576
- if (cache4.get(src) !== entry) return;
14577
- cache4.set(src, { status: "ready", bitmap });
14958
+ if (cache5.get(src) !== entry) return;
14959
+ cache5.set(src, { status: "ready", bitmap });
14578
14960
  notify();
14579
14961
  },
14580
14962
  () => {
14581
- if (cache4.get(src) !== entry) return;
14582
- cache4.set(src, { status: "error" });
14963
+ if (cache5.get(src) !== entry) return;
14964
+ cache5.set(src, { status: "error" });
14583
14965
  notify();
14584
14966
  }
14585
14967
  );
14586
14968
  }
14587
14969
  function getImageBitmap(src) {
14588
- const entry = cache4.get(src);
14970
+ const entry = cache5.get(src);
14589
14971
  if (entry) return entry.bitmap;
14590
14972
  startLoad(src);
14591
14973
  return void 0;
14592
14974
  }
14593
14975
  function imageStatus(src) {
14594
- return cache4.get(src)?.status ?? "idle";
14976
+ return cache5.get(src)?.status ?? "idle";
14595
14977
  }
14596
14978
  function subscribeImageReady(cb) {
14597
14979
  subscribers2.add(cb);
@@ -14685,6 +15067,13 @@ function shapeCoversPoint(node, pose, x, y, opts = {}) {
14685
15067
  const reach = ink.strokeWidth / 2 + (opts.tolerance ?? 0);
14686
15068
  return reach > 0 && strokeHitTest(sil, x, y, reach);
14687
15069
  }
15070
+ function withLeafStroke(style, stroke, strokeWidth) {
15071
+ if (style?.stroke !== void 0) return style;
15072
+ if (!stroke || stroke === "none") return style;
15073
+ const width = strokeWidth ?? 1;
15074
+ if (width <= 0) return style;
15075
+ return { ...style, stroke: { paint: { color: stroke }, width } };
15076
+ }
14688
15077
  var TEXT_PAINTER = {
14689
15078
  id: "kit:text",
14690
15079
  matches: (node) => {
@@ -14703,7 +15092,8 @@ var TEXT_PAINTER = {
14703
15092
  const d = node.data;
14704
15093
  const p = pose;
14705
15094
  const y = p.y;
14706
- return d.runs && d.runs.length > 0 ? [textCommandFromRuns(p.x, y, d.runs, d.style, void 0, p.height)] : [textCommand(p.x, y, d.text, d.style, void 0, p.height)];
15095
+ const style = withLeafStroke(d.style, d.stroke, d.strokeWidth);
15096
+ return d.runs && d.runs.length > 0 ? [textCommandFromRuns(p.x, y, d.runs, style, void 0, p.height)] : [textCommand(p.x, y, d.text, style, void 0, p.height)];
14707
15097
  }),
14708
15098
  // The pose is a *wrap box*, not a bounding box — "Away" in a 300-unit box
14709
15099
  // leaves most of it empty, and a pose-rect silhouette claims all of it. The
@@ -16753,7 +17143,7 @@ function buildChromeState(args) {
16753
17143
  }
16754
17144
  };
16755
17145
  }
16756
- var DEFAULT_ROTATION_HANDLE_DISTANCE = 24;
17146
+ var DEFAULT_ROTATION_HANDLE_DISTANCE = ROTATION_HANDLE_BASE_PX;
16757
17147
  function rotationHandle(pose, distance = DEFAULT_ROTATION_HANDLE_DISTANCE) {
16758
17148
  const rotation = pose.rotation ?? 0;
16759
17149
  const c = aabbCenter(pose);
@@ -16813,7 +17203,7 @@ var DEFAULT_HANDLE_OUTLINE = {
16813
17203
  paint: { fill: "solid", color: "#1a130d" },
16814
17204
  width: 1
16815
17205
  };
16816
- var DEFAULT_HANDLE_SIZE = 8;
17206
+ var DEFAULT_HANDLE_SIZE = HANDLE_BASE_PX;
16817
17207
  function defaultHandlesOf(b) {
16818
17208
  return [
16819
17209
  { x: b.x, y: b.y },
@@ -20212,8 +20602,8 @@ var DEFAULT_FILL3 = "#d4c4a8";
20212
20602
  var DEFAULT_STROKE = "#1a130d";
20213
20603
  function createCornerResizeAffordance(opts = {}) {
20214
20604
  const {
20215
- handleHitRadius = 8,
20216
- handleSize = 8,
20605
+ handleHitRadius = HANDLE_BASE_PX,
20606
+ handleSize = HANDLE_BASE_PX,
20217
20607
  fill = DEFAULT_FILL3,
20218
20608
  stroke = DEFAULT_STROKE
20219
20609
  } = opts;
@@ -20351,8 +20741,8 @@ function createPathAnchorAffordances(getAnchorState, opts = {}) {
20351
20741
  };
20352
20742
  return [anchors, controls];
20353
20743
  }
20354
- var HANDLE_HIT_RADIUS = 8;
20355
- var ANCHOR_HIT_RADIUS = 8;
20744
+ var HANDLE_HIT_RADIUS = HANDLE_BASE_PX;
20745
+ var ANCHOR_HIT_RADIUS = ANCHOR_HIT_BASE_PX;
20356
20746
  function buildAffordanceAt(opts) {
20357
20747
  const {
20358
20748
  getChromeState,
@@ -21567,7 +21957,6 @@ function makeGetNodeAtPoint(pickEvery, nodeResolver) {
21567
21957
  return { id, kind, pose, data };
21568
21958
  };
21569
21959
  }
21570
- var DEFAULT_HANDLE_SIZE2 = 8;
21571
21960
  var IS_DEV = (() => {
21572
21961
  try {
21573
21962
  return Boolean(import.meta.env?.DEV);
@@ -21586,10 +21975,10 @@ function recordCoordTrace(entry) {
21586
21975
  coordTrace.push(entry);
21587
21976
  if (coordTrace.length > COORD_TRACE_LIMIT) coordTrace.shift();
21588
21977
  }
21589
- function mergeLayersWithDefaults(user) {
21978
+ function mergeLayersWithDefaults(user, targetScale = 1) {
21590
21979
  const defaults = {
21591
21980
  scene: { drawOne: defaultDrawOne },
21592
- selectionOverlay: { handles: { size: DEFAULT_HANDLE_SIZE2 } }
21981
+ selectionOverlay: { handles: { size: HANDLE_BASE_PX * targetScale } }
21593
21982
  };
21594
21983
  if (!user) return defaults;
21595
21984
  const result = { ...user };
@@ -21677,8 +22066,10 @@ function SceneCanvasInner(props, ref) {
21677
22066
  chromeVisibility,
21678
22067
  getActiveMode,
21679
22068
  getFocused: getFocusedProp,
22069
+ device,
21680
22070
  ...rest
21681
22071
  } = props;
22072
+ const deviceProfile = useDeviceProfile(device);
21682
22073
  const isSerialized = (s) => typeof s === "object" && s != null && s.version === 1 && Array.isArray(s.nodes);
21683
22074
  const [bakedScene] = useState(
21684
22075
  () => isSerialized(sceneInput) ? sceneFromJSON(sceneInput, {}) : null
@@ -21796,7 +22187,7 @@ function SceneCanvasInner(props, ref) {
21796
22187
  }
21797
22188
  }), []);
21798
22189
  const selectToolWithDefaults = useMemo(() => ({
21799
- handleHitRadius: DEFAULT_HANDLE_SIZE2,
22190
+ handleHitRadius: HANDLE_BASE_PX * deviceProfile.targetScale,
21800
22191
  // Shift-click belongs to the anchor selection while a path is being
21801
22192
  // anchor-edited; see `UseSelectToolOptions.extendClickLocked`.
21802
22193
  extendClickLocked: () => effectivePathEditingId() !== "",
@@ -21806,7 +22197,7 @@ function SceneCanvasInner(props, ref) {
21806
22197
  // pipeline (audit 3.4). The classifier is now `select.pick`, which
21807
22198
  // declares that capability itself — one rule, evaluated in one place.
21808
22199
  ...selectToolOpts
21809
- }), [selectToolOpts]);
22200
+ }), [selectToolOpts, deviceProfile.targetScale]);
21810
22201
  const selectionRef = useRef(selection);
21811
22202
  selectionRef.current = selection;
21812
22203
  const kindClassifier = useMemo(() => {
@@ -21964,8 +22355,8 @@ function SceneCanvasInner(props, ref) {
21964
22355
  };
21965
22356
  }, [Boolean(onDoubleClick)]);
21966
22357
  const mergedLayers = useMemo(
21967
- () => mergeLayersWithDefaults(layers),
21968
- [layers]
22358
+ () => mergeLayersWithDefaults(layers, deviceProfile.targetScale),
22359
+ [layers, deviceProfile.targetScale]
21969
22360
  );
21970
22361
  const resolvedActions = useMemo(() => {
21971
22362
  if (selectToolOpts?.rotate !== false) return actions;
@@ -22024,6 +22415,8 @@ function SceneCanvasInner(props, ref) {
22024
22415
  chromeVisibilityRef.current = effectiveChromeVisibility;
22025
22416
  const getActiveModeRef = useRef(getActiveMode);
22026
22417
  getActiveModeRef.current = getActiveMode;
22418
+ const deviceProfileRef = useRef(deviceProfile);
22419
+ deviceProfileRef.current = deviceProfile;
22027
22420
  const resizablePredRef = useRef(selectToolOpts?.resize?.resizable);
22028
22421
  resizablePredRef.current = selectToolOpts?.resize?.resizable;
22029
22422
  const buildCurrentRuleCtx = useCallback(() => {
@@ -22045,7 +22438,8 @@ function SceneCanvasInner(props, ref) {
22045
22438
  mode: modeInfo.id,
22046
22439
  allowedCapabilities: modeInfo.allowedCapabilities,
22047
22440
  selectionResizable,
22048
- editingAnchors: effectivePathEditingId() !== ""
22441
+ editingAnchors: effectivePathEditingId() !== "",
22442
+ device: deviceProfileRef.current
22049
22443
  };
22050
22444
  }, [dispatcher, getHover]);
22051
22445
  const anchorEditingAllowed = useMemo(
@@ -22300,7 +22694,7 @@ function SceneCanvasInner(props, ref) {
22300
22694
  testHookRef.current?._markReady();
22301
22695
  }
22302
22696
  }, []);
22303
- return /* @__PURE__ */ jsx(DepRegistryProviderIfRoot, { children: /* @__PURE__ */ jsx(PointerProviderIfRoot, { children: /* @__PURE__ */ jsxs(ActionsProviderIfRoot, { children: [
22697
+ return /* @__PURE__ */ jsx(DeviceProfileProvider, { value: device, children: /* @__PURE__ */ jsx(DepRegistryProviderIfRoot, { children: /* @__PURE__ */ jsx(PointerProviderIfRoot, { children: /* @__PURE__ */ jsxs(ActionsProviderIfRoot, { children: [
22304
22698
  canvas,
22305
22699
  /* @__PURE__ */ jsx(PointerPublisher, { canvasRef: internalCanvasRef, viewRef: currentViewRef }),
22306
22700
  /* @__PURE__ */ jsx(
@@ -22363,7 +22757,7 @@ function SceneCanvasInner(props, ref) {
22363
22757
  }
22364
22758
  ),
22365
22759
  children
22366
- ] }) }) });
22760
+ ] }) }) }) });
22367
22761
  }
22368
22762
  function ToolKeybindingsMounter({
22369
22763
  internalTools,
@@ -23152,5 +23546,5 @@ function oklchToOklab(L, C, h) {
23152
23546
  }
23153
23547
 
23154
23548
  export { FALLBACK_FIT_VIEW, MinimapCanvas, SceneViewCanvas, UnknownIcon, buildSceneViewCommands, computeFitView2, computeIndicatorCommand, eligibleTool, evaluateEnabled, oklabToSrgbU8, oklchToOklab, renderSceneToCanvas, rgbaToHex, useActionsRegistry, useOptionalDepRegistry };
23155
- //# sourceMappingURL=chunk-5XDFILHZ.js.map
23156
- //# sourceMappingURL=chunk-5XDFILHZ.js.map
23549
+ //# sourceMappingURL=chunk-6V7ROR7G.js.map
23550
+ //# sourceMappingURL=chunk-6V7ROR7G.js.map