@weasel-js/core 0.7.0 → 0.7.2

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.
@@ -1,14 +1,14 @@
1
- import { defineViewportTool, defineTool, RESERVED_ID_PREFIXES, RESERVED_ID_NAMES } from './chunk-FSZEXVCR.js';
1
+ import { defineViewportTool, reportRouteConflicts, defineTool, RESERVED_ID_PREFIXES, RESERVED_ID_NAMES } from './chunk-SYM6RAM4.js';
2
2
  import { scratchKey, createTransformOp, snap, getScratch, createReparentOp, setScratch } from './chunk-VOVKONXA.js';
3
3
  import { fixedCornerOf, DEFAULT_RESIZE_BEHAVIORS, CORNER_ANCHORS, cornerPoint } from './chunk-BGGZ4CVF.js';
4
- import { AUTO_POSE_DESCRIPTOR, RECT_POSE_DESCRIPTOR, resolveUnit, meanScale, pathPoseDescriptor, polygonFromPoints, starPath, regularPolygonPath, linePath, ellipsePath, rectPath, boundsOfPath, PATH_M, PATH_L, PATH_Z, PATH_Q, PATH_C, DEFAULT_FLATTEN_TOLERANCE, flattenQuadratic, flattenCubic, PathBuilder, pointInPath, strokeHitTest, transformPath, flattenCubicWithArcLen, flattenQuadraticWithArcLen, translatePath, PATH_CMD_LENGTHS, pointToGridCell } from './chunk-CQNKCG34.js';
4
+ import { AUTO_POSE_DESCRIPTOR, RECT_POSE_DESCRIPTOR, resolveUnit, meanScale, pathPoseDescriptor, polygonFromPoints, starPath, regularPolygonPath, linePath, ellipsePath, rectPath, strokeHitTest, boundsOfPath, PATH_M, PATH_L, PATH_Z, PATH_Q, PATH_C, DEFAULT_FLATTEN_TOLERANCE, flattenQuadratic, flattenCubic, PathBuilder, pointInPath, transformPath, flattenCubicWithArcLen, flattenQuadraticWithArcLen, translatePath, PATH_CMD_LENGTHS, pointToGridCell } from './chunk-CQNKCG34.js';
5
5
  import { dispatchApplyBatch, dlog, dwarn, usePointerContext, PointerContextProvider, applyOpsTo, extractWeaselClipboardFromSvg, parseWeaselClipboardText, materializePaste, WEASEL_CLIPBOARD_MIME, WEASEL_CLIPBOARD_MIME_WEB, sniffWeaselClipboardText } from './chunk-775XXAHR.js';
6
6
  import { createSetSelectionOp } from './chunk-BHVYVFGV.js';
7
7
  import { registerOpFactory, createDeleteOp, createInsertOp, rebuildOp } from './chunk-GVCNT7UH.js';
8
8
  import { getTexture } from './chunk-Y52N27PF.js';
9
9
  import { createContext, createElement, forwardRef, useMemo, useRef, useState, useCallback, useImperativeHandle, useEffect, useSyncExternalStore, useContext, useReducer, useLayoutEffect } from 'react';
10
- import { subscribeGlyphReady, DEFAULT_BAKE_BUDGET, TEXT_VERT_SRC, TEXT_FRAG_SRC, TEXT_SDF_UNIFORMS, TEXT_SDF_ATTRIBUTES, TEXT_FRAG_R8_SRC, markAllFontsNotUploaded, resetBakeBudget, resolveFontVariant, syncDynamicPageTexture, ensureFontTexture, dynamicPageTextureId, textureCacheKey, resolveGlyphFallback } from '@weasel-js/font';
11
- export { isCanvasFont, isCanvasFont as isCanvasFont2, registerCanvasFont, registerCanvasFont as registerCanvasFont2, registerFont, registerFont as registerFont2, subscribeGlyphReady, subscribeGlyphReady as subscribeGlyphReady2, unregisterCanvasFont, unregisterCanvasFont as unregisterCanvasFont2 } from '@weasel-js/font';
10
+ import { subscribeGlyphReady, DEFAULT_BAKE_BUDGET, TEXT_VERT_SRC, TEXT_FRAG_SRC, TEXT_SDF_UNIFORMS, TEXT_SDF_ATTRIBUTES, TEXT_FRAG_R8_SRC, markAllFontsNotUploaded, resetBakeBudget, glyphGeneration, syncDynamicPageTexture, ensureFontTexture, dynamicPageTextureId, textureCacheKey, resolveFontVariant, resolveGlyphFallback, glyphOutline } from '@weasel-js/font';
11
+ export { canQueryLocalFonts, canQueryLocalFonts as canQueryLocalFonts2, enableLocalFontOutlines, enableLocalFontOutlines as enableLocalFontOutlines2, hasFontOutlines, hasFontOutlines as hasFontOutlines2, isCanvasFont, isCanvasFont as isCanvasFont2, listFontOutlines, listFontOutlines as listFontOutlines2, outlineStatus, outlineStatus as outlineStatus2, registerCanvasFont, registerCanvasFont as registerCanvasFont2, registerFont, registerFont as registerFont2, registerFontOutlines, registerFontOutlines as registerFontOutlines2, subscribeGlyphReady, subscribeGlyphReady as subscribeGlyphReady2, unregisterCanvasFont, unregisterCanvasFont as unregisterCanvasFont2, unregisterFontOutlines, unregisterFontOutlines as unregisterFontOutlines2 } from '@weasel-js/font';
12
12
  import earcut from 'earcut';
13
13
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
14
14
  import { applyToPoint, rotateAboutPoint, boxToBox, pointInPolygon, segmentsCross } from '@weasel-js/geom';
@@ -18,6 +18,9 @@ import { NORMAL, IMPLICIT_TAGS } from '@weasel-js/modes';
18
18
  import { createHistory } from '@weasel-js/history';
19
19
  export * from '@weasel-js/history';
20
20
 
21
+ // src/version.ts
22
+ var VERSION = "0.7.2" ;
23
+
21
24
  // src/features/grid/roundToCell.ts
22
25
  function roundToCell(value, cellSize) {
23
26
  return Math.round(value / cellSize) * cellSize || 0;
@@ -2067,11 +2070,11 @@ function sameFill(a, b) {
2067
2070
  function groupKey(family, weight, style, synthetic, fill, source, page) {
2068
2071
  return `${family}|${weight}|${style}|${synthetic.bold ? 1 : 0}${synthetic.italic ? 1 : 0}|${fillKey(fill)}|${source}|${page}`;
2069
2072
  }
2070
- function getOrCreateGroup(ctx, run, resolved, page) {
2073
+ function getOrCreateGroup(ctx, run, resolved, page, sourceOverride) {
2071
2074
  const atlasFamily = resolved.resolved.family;
2072
2075
  const resolvedWeight = resolved.resolved.weight;
2073
2076
  const resolvedStyle = resolved.resolved.style;
2074
- const source = resolved.source;
2077
+ const source = sourceOverride ?? resolved.source;
2075
2078
  const key = groupKey(
2076
2079
  atlasFamily,
2077
2080
  resolvedWeight,
@@ -2091,7 +2094,8 @@ function getOrCreateGroup(ctx, run, resolved, page) {
2091
2094
  source,
2092
2095
  page,
2093
2096
  fill: run.fill,
2094
- quads: []
2097
+ quads: [],
2098
+ glyphs: []
2095
2099
  };
2096
2100
  ctx.groups.set(key, g);
2097
2101
  }
@@ -2230,6 +2234,10 @@ function layoutRuns(runs, opts, origin) {
2230
2234
  while (i < entries.length) {
2231
2235
  const e = entries[i];
2232
2236
  if (e.isNewline) {
2237
+ if (cur.entries.length === 0) {
2238
+ cur.height = Math.max(cur.height, e.fontSize * opts.lineHeight);
2239
+ cur.blank = e;
2240
+ }
2233
2241
  commitLine();
2234
2242
  i++;
2235
2243
  continue;
@@ -2263,6 +2271,13 @@ function layoutRuns(runs, opts, origin) {
2263
2271
  i = j;
2264
2272
  }
2265
2273
  if (cur.entries.length > 0) commitLine();
2274
+ function outlineFor(e) {
2275
+ const min = opts.outlineMinSize;
2276
+ if (min === void 0 || e.fontSize < min) return null;
2277
+ if (e.resolved.synthetic.bold) return null;
2278
+ const r = e.resolved.resolved;
2279
+ return glyphOutline(r.family, r.weight, r.style, e.cp);
2280
+ }
2266
2281
  const decorations = [];
2267
2282
  let span = null;
2268
2283
  function flushSpan() {
@@ -2279,6 +2294,7 @@ function layoutRuns(runs, opts, origin) {
2279
2294
  decorations.push({ kind: "strikethrough", x0: s.x0, y0, x1: s.x1, y1: y0 + thickness, fill: s.fill });
2280
2295
  }
2281
2296
  }
2297
+ const lineBoxes = [];
2282
2298
  let penY = origin.y;
2283
2299
  let maxLineWidth = 0;
2284
2300
  const finiteWidth = Number.isFinite(opts.maxWidth) ? opts.maxWidth : 0;
@@ -2291,7 +2307,10 @@ function layoutRuns(runs, opts, origin) {
2291
2307
  const slack = finiteWidth - line.width;
2292
2308
  return opts.align === "center" ? slack / 2 : slack;
2293
2309
  })();
2294
- let penX = origin.x + alignShift;
2310
+ const lineX0 = origin.x + alignShift;
2311
+ const baselineSource = line.entries[0] ?? line.blank;
2312
+ const lineBaselineY = baselineSource ? penY + baselineSource.font.common.base * (baselineSource.fontSize / baselineSource.font.info.size) : penY;
2313
+ let penX = lineX0;
2295
2314
  for (const e of line.entries) {
2296
2315
  penX += e.kerningBefore;
2297
2316
  const step = e.advance + e.tracking;
@@ -2315,6 +2334,20 @@ function layoutRuns(runs, opts, origin) {
2315
2334
  } else {
2316
2335
  flushSpan();
2317
2336
  }
2337
+ const outlineD = outlineFor(e);
2338
+ if (outlineD !== null) {
2339
+ const group2 = getOrCreateGroup(ctx, e.run, e.resolved, 0, "outline");
2340
+ group2.glyphs.push({
2341
+ d: outlineD,
2342
+ key: `${e.resolved.resolved.family}|${e.resolved.resolved.weight}|${e.resolved.resolved.style}|${e.cp}`,
2343
+ x: penX,
2344
+ baselineY,
2345
+ // Em space is unit-scale, so world units per em is just the size.
2346
+ scale: e.fontSize
2347
+ });
2348
+ penX += step;
2349
+ continue;
2350
+ }
2318
2351
  if (e.advance === 0 || e.glyph.width === 0 || e.glyph.page < 0) {
2319
2352
  penX += step;
2320
2353
  continue;
@@ -2334,15 +2367,54 @@ function layoutRuns(runs, opts, origin) {
2334
2367
  penX += step;
2335
2368
  }
2336
2369
  flushSpan();
2370
+ lineBoxes.push({
2371
+ x0: lineX0,
2372
+ y0: penY,
2373
+ x1: lineX0 + line.width,
2374
+ y1: penY + line.height,
2375
+ baselineY: lineBaselineY
2376
+ });
2337
2377
  maxLineWidth = Math.max(maxLineWidth, line.width);
2338
2378
  penY += line.height;
2339
2379
  }
2340
2380
  return {
2341
2381
  groups: [...ctx.groups.values()],
2342
2382
  decorations,
2383
+ lines: lineBoxes,
2343
2384
  bounds: { width: maxLineWidth, height: penY - origin.y }
2344
2385
  };
2345
2386
  }
2387
+ var LAYOUT_CACHE_VARIANT_LIMIT = 8;
2388
+ var cache2 = /* @__PURE__ */ new WeakMap();
2389
+ function outlineBucket(runs, min) {
2390
+ if (min === void 0) return -1;
2391
+ const sizes = /* @__PURE__ */ new Set();
2392
+ for (const r of runs) sizes.add(r.fontSize);
2393
+ let n = 0;
2394
+ for (const size of sizes) if (size >= min) n++;
2395
+ return n;
2396
+ }
2397
+ function variantKey(runs, opts, origin) {
2398
+ return `${opts.maxWidth}|${opts.lineHeight}|${opts.align}|${outlineBucket(runs, opts.outlineMinSize)}|${origin.x}|${origin.y}`;
2399
+ }
2400
+ function cachedLayoutRuns(runs, opts, origin) {
2401
+ const fonts = glyphGeneration();
2402
+ let entry = cache2.get(runs);
2403
+ if (entry === void 0) {
2404
+ entry = { generation: fonts, byVariant: /* @__PURE__ */ new Map() };
2405
+ cache2.set(runs, entry);
2406
+ } else if (entry.generation !== fonts) {
2407
+ entry.generation = fonts;
2408
+ entry.byVariant.clear();
2409
+ }
2410
+ const key = variantKey(runs, opts, origin);
2411
+ const hit = entry.byVariant.get(key);
2412
+ if (hit !== void 0) return hit;
2413
+ const laid = layoutRuns(runs, opts, origin);
2414
+ if (entry.byVariant.size >= LAYOUT_CACHE_VARIANT_LIMIT) entry.byVariant.clear();
2415
+ entry.byVariant.set(key, laid);
2416
+ return laid;
2417
+ }
2346
2418
 
2347
2419
  // src/features/text/verticalAlign.ts
2348
2420
  function verticalAlignOffset(align, boxHeight, textHeight) {
@@ -2351,170 +2423,445 @@ function verticalAlignOffset(align, boxHeight, textHeight) {
2351
2423
  return align === "center" ? slack / 2 : slack;
2352
2424
  }
2353
2425
 
2354
- // src/renderer/draw.ts
2355
- function setColorMatrixUniforms(ctx, prog) {
2356
- const gl = ctx.gl;
2357
- const cm = ctx.state.colorMatrix;
2358
- const m4 = new Float32Array(16);
2359
- for (let row = 0; row < 4; row++) {
2360
- for (let col = 0; col < 4; col++) {
2361
- m4[col * 4 + row] = cm[row * 5 + col];
2426
+ // src/features/paths/pathFromD.ts
2427
+ var NUM_RE = /[+-]?(?:\d*\.\d+|\d+\.?)(?:[eE][+-]?\d+)?/g;
2428
+ function tokenize(d) {
2429
+ const out = [];
2430
+ let i = 0;
2431
+ while (i < d.length) {
2432
+ const ch = d[i];
2433
+ if (/[A-Za-z]/.test(ch)) {
2434
+ let j = i + 1;
2435
+ while (j < d.length && !/[A-Za-z]/.test(d[j])) j++;
2436
+ const argSlice = d.slice(i + 1, j);
2437
+ out.push({ cmd: ch, args: readNumbers(argSlice) });
2438
+ i = j;
2439
+ } else {
2440
+ i++;
2362
2441
  }
2363
2442
  }
2364
- const mLoc = prog.uniform("u_colorMatrix");
2365
- const bLoc = prog.uniform("u_colorBias");
2366
- if (mLoc !== void 0) gl.uniformMatrix4fv(mLoc, false, m4);
2367
- if (bLoc !== void 0) gl.uniform4f(bLoc, cm[4], cm[9], cm[14], cm[19]);
2368
- }
2369
- function fillMeshHandle(ctx, path) {
2370
- if (ctx.flattenTolerance !== void 0) {
2371
- return ctx.meshCache.uploadTransient(tessellate(path, { flattenTolerance: ctx.flattenTolerance }));
2372
- }
2373
- return ctx.meshCache.handleFor(getMesh(path));
2374
- }
2375
- function dispatch(ctx, cmd) {
2376
- switch (cmd.kind) {
2377
- case "group":
2378
- return drawGroup(ctx, cmd);
2379
- case "path":
2380
- return drawPath(ctx, cmd);
2381
- case "text":
2382
- return drawText(ctx, cmd);
2383
- case "image":
2384
- return drawImage(ctx, cmd);
2385
- case "shader":
2386
- return drawShader(ctx, cmd);
2387
- }
2443
+ return out;
2388
2444
  }
2389
- var warnedUniforms = /* @__PURE__ */ new Set();
2390
- function warnOnceUniform(programId, name) {
2391
- const key = `${programId}:${name}`;
2392
- if (warnedUniforms.has(key)) return;
2393
- warnedUniforms.add(key);
2394
- const isDev3 = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
2395
- if (isDev3) {
2396
- console.warn(`weasel drawShader: uniform "${name}" not found in program "${programId}". Check spelling, ensure it's used in the shader (unused uniforms are optimized away by the driver).`);
2445
+ function readNumbers(s) {
2446
+ const out = [];
2447
+ let m;
2448
+ NUM_RE.lastIndex = 0;
2449
+ while ((m = NUM_RE.exec(s)) !== null) {
2450
+ const n = Number(m[0]);
2451
+ if (Number.isFinite(n)) out.push(n);
2397
2452
  }
2453
+ return out;
2398
2454
  }
2399
- function setUniform(gl, loc, value, textureCache, nextTexUnit) {
2400
- if (value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Float32Array) && "id" in value && typeof value.id === "string") {
2401
- const handle = value;
2402
- const entry = getTexture(handle.id);
2403
- if (!entry) {
2404
- const isDev3 = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
2405
- if (isDev3) console.warn(`weasel setUniform: TextureHandle "${handle.id}" not registered`);
2406
- return;
2455
+ var ARG_COUNTS = {
2456
+ M: 2,
2457
+ L: 2,
2458
+ H: 1,
2459
+ V: 1,
2460
+ C: 6,
2461
+ S: 4,
2462
+ Q: 4,
2463
+ T: 2,
2464
+ A: 7,
2465
+ Z: 0
2466
+ };
2467
+ function pathFromD(d, onWarn) {
2468
+ const tokens = tokenize(d);
2469
+ const b = new PathBuilder();
2470
+ const st = {
2471
+ x: 0,
2472
+ y: 0,
2473
+ startX: 0,
2474
+ startY: 0,
2475
+ lastCubicCtrl: null,
2476
+ lastQuadCtrl: null
2477
+ };
2478
+ for (const { cmd, args } of tokens) {
2479
+ const upper = cmd.toUpperCase();
2480
+ const relative = cmd !== upper;
2481
+ const argCount = ARG_COUNTS[upper];
2482
+ if (argCount === void 0) {
2483
+ onWarn?.(`unknown path command: ${cmd}`);
2484
+ continue;
2407
2485
  }
2408
- const unit2 = nextTexUnit.value++;
2409
- textureCache.upload(handle.id, entry.source);
2410
- textureCache.bind(handle.id, unit2);
2411
- gl.uniform1i(loc, unit2);
2412
- return;
2413
- }
2414
- if (value instanceof Float32Array) {
2415
- if (value.length === 9) {
2416
- gl.uniformMatrix3fv(loc, false, value);
2417
- } else if (value.length === 16) {
2418
- gl.uniformMatrix4fv(loc, false, value);
2419
- } else {
2420
- const isDev3 = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
2421
- if (isDev3) throw new TypeError(`weasel setUniform: Float32Array must be length 9 (mat3) or 16 (mat4), got ${value.length}`);
2486
+ if (upper === "Z") {
2487
+ b.close();
2488
+ st.x = st.startX;
2489
+ st.y = st.startY;
2490
+ st.lastCubicCtrl = null;
2491
+ st.lastQuadCtrl = null;
2492
+ continue;
2422
2493
  }
2423
- return;
2424
- }
2425
- if (Array.isArray(value)) {
2426
- const arr = value;
2427
- switch (arr.length) {
2428
- case 2:
2429
- gl.uniform2fv(loc, arr);
2430
- break;
2431
- case 3:
2432
- gl.uniform3fv(loc, arr);
2433
- break;
2434
- case 4:
2435
- gl.uniform4fv(loc, arr);
2436
- break;
2437
- default: {
2438
- const isDev3 = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
2439
- if (isDev3) throw new TypeError(`weasel setUniform: array length ${arr.length} not supported`);
2440
- }
2494
+ const groups = argCount === 0 ? 1 : Math.floor(args.length / argCount);
2495
+ for (let g = 0; g < groups; g++) {
2496
+ const a = args.slice(g * argCount, (g + 1) * argCount);
2497
+ const effectiveUpper = upper === "M" && g > 0 ? "L" : upper;
2498
+ runCommand(effectiveUpper, relative, a, st, b);
2441
2499
  }
2442
- return;
2443
- }
2444
- if (typeof value === "number") {
2445
- gl.uniform1f(loc, value);
2446
- return;
2447
2500
  }
2501
+ return b.build();
2448
2502
  }
2449
- function drawShader(ctx, cmd) {
2450
- const { gl, programRegistry, quadVbo, quadIbo, textureCache } = ctx;
2451
- const program = programRegistry.get(cmd.program.id);
2452
- if (!program) {
2453
- console.warn(
2454
- `weasel drawShader: program "${cmd.program.id}" not compiled on this renderer. Call renderer.registerProgram(handle) after the module-level registerProgram().`
2455
- );
2456
- return;
2457
- }
2458
- if (!quadVbo || !quadIbo) {
2459
- console.warn("weasel drawShader: quad geometry not initialized");
2460
- return;
2461
- }
2462
- gl.useProgram(program.handle);
2463
- const aPosLoc = program.attribute("a_position");
2464
- const aUvLoc = program.attribute("a_uv");
2465
- gl.bindBuffer(gl.ARRAY_BUFFER, quadVbo);
2466
- if (aPosLoc !== void 0) {
2467
- gl.enableVertexAttribArray(aPosLoc);
2468
- gl.vertexAttribPointer(aPosLoc, 2, gl.FLOAT, false, 16, 0);
2469
- }
2470
- if (aUvLoc !== void 0) {
2471
- gl.enableVertexAttribArray(aUvLoc);
2472
- gl.vertexAttribPointer(aUvLoc, 2, gl.FLOAT, false, 16, 8);
2473
- }
2474
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, quadIbo);
2475
- const proj = mat3.screenToClip(ctx.widthCss, ctx.heightCss);
2476
- const uProj = program.uniform("u_proj");
2477
- if (uProj !== void 0) gl.uniformMatrix3fv(uProj, false, proj);
2478
- const uBounds = program.uniform("u_bounds");
2479
- if (uBounds !== void 0) {
2480
- gl.uniform4f(uBounds, cmd.bounds.x, cmd.bounds.y, cmd.bounds.w, cmd.bounds.h);
2481
- }
2482
- const uView = program.uniform("u_view");
2483
- if (uView !== void 0) gl.uniformMatrix3fv(uView, false, ctx.state.transform);
2484
- const nextTexUnit = { value: 1 };
2485
- for (const [name, value] of Object.entries(cmd.uniforms)) {
2486
- const loc = program.uniform(name);
2487
- if (loc === void 0) {
2488
- warnOnceUniform(cmd.program.id, name);
2489
- continue;
2503
+ function runCommand(upper, relative, a, st, b) {
2504
+ const rx = relative ? st.x : 0;
2505
+ const ry = relative ? st.y : 0;
2506
+ switch (upper) {
2507
+ case "M": {
2508
+ const x = a[0] + rx;
2509
+ const y = a[1] + ry;
2510
+ b.moveTo(x, y);
2511
+ st.x = x;
2512
+ st.y = y;
2513
+ st.startX = x;
2514
+ st.startY = y;
2515
+ st.lastCubicCtrl = null;
2516
+ st.lastQuadCtrl = null;
2517
+ return;
2490
2518
  }
2491
- setUniform(gl, loc, value, textureCache, nextTexUnit);
2492
- }
2493
- applyClipTest(ctx);
2494
- gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
2495
- if (aPosLoc !== void 0) gl.disableVertexAttribArray(aPosLoc);
2496
- if (aUvLoc !== void 0) gl.disableVertexAttribArray(aUvLoc);
2497
- gl.bindBuffer(gl.ARRAY_BUFFER, null);
2498
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
2499
- }
2500
- function drawGroup(ctx, cmd) {
2501
- ctx.state.push({
2502
- transform: cmd.transform,
2503
- alpha: cmd.alpha,
2504
- colorMatrix: cmd.colorMatrix
2505
- });
2506
- if (cmd.clip) {
2507
- const newDepth = ctx.clipDepth + 1;
2508
- if (newDepth > 7) {
2509
- ctx.state.pop();
2510
- throw new Error(
2511
- "weasel: clip nesting depth exceeded (max 7). You can't nest more than 7 levels of clipped containers in a single draw tree. Flatten the hierarchy or compose poses outside the scene graph."
2512
- );
2519
+ case "L": {
2520
+ const x = a[0] + rx;
2521
+ const y = a[1] + ry;
2522
+ b.lineTo(x, y);
2523
+ st.x = x;
2524
+ st.y = y;
2525
+ st.lastCubicCtrl = null;
2526
+ st.lastQuadCtrl = null;
2527
+ return;
2513
2528
  }
2514
- pushClip(ctx, cmd.clip, newDepth);
2515
- ctx.clipDepth = newDepth;
2516
- }
2517
- for (const child of cmd.children) dispatch(ctx, child);
2529
+ case "H": {
2530
+ const x = a[0] + (relative ? st.x : 0);
2531
+ b.lineTo(x, st.y);
2532
+ st.x = x;
2533
+ st.lastCubicCtrl = null;
2534
+ st.lastQuadCtrl = null;
2535
+ return;
2536
+ }
2537
+ case "V": {
2538
+ const y = a[0] + (relative ? st.y : 0);
2539
+ b.lineTo(st.x, y);
2540
+ st.y = y;
2541
+ st.lastCubicCtrl = null;
2542
+ st.lastQuadCtrl = null;
2543
+ return;
2544
+ }
2545
+ case "C": {
2546
+ const x1 = a[0] + rx, y1 = a[1] + ry;
2547
+ const x2 = a[2] + rx, y2 = a[3] + ry;
2548
+ const x = a[4] + rx, y = a[5] + ry;
2549
+ b.curveTo(x1, y1, x2, y2, x, y);
2550
+ st.x = x;
2551
+ st.y = y;
2552
+ st.lastCubicCtrl = { x: x2, y: y2 };
2553
+ st.lastQuadCtrl = null;
2554
+ return;
2555
+ }
2556
+ case "S": {
2557
+ const ref = st.lastCubicCtrl ?? { x: st.x, y: st.y };
2558
+ const x1 = 2 * st.x - ref.x;
2559
+ const y1 = 2 * st.y - ref.y;
2560
+ const x2 = a[0] + rx, y2 = a[1] + ry;
2561
+ const x = a[2] + rx, y = a[3] + ry;
2562
+ b.curveTo(x1, y1, x2, y2, x, y);
2563
+ st.x = x;
2564
+ st.y = y;
2565
+ st.lastCubicCtrl = { x: x2, y: y2 };
2566
+ st.lastQuadCtrl = null;
2567
+ return;
2568
+ }
2569
+ case "Q": {
2570
+ const x1 = a[0] + rx, y1 = a[1] + ry;
2571
+ const x = a[2] + rx, y = a[3] + ry;
2572
+ b.quadTo(x1, y1, x, y);
2573
+ st.x = x;
2574
+ st.y = y;
2575
+ st.lastQuadCtrl = { x: x1, y: y1 };
2576
+ st.lastCubicCtrl = null;
2577
+ return;
2578
+ }
2579
+ case "T": {
2580
+ const ref = st.lastQuadCtrl ?? { x: st.x, y: st.y };
2581
+ const x1 = 2 * st.x - ref.x;
2582
+ const y1 = 2 * st.y - ref.y;
2583
+ const x = a[0] + rx, y = a[1] + ry;
2584
+ b.quadTo(x1, y1, x, y);
2585
+ st.x = x;
2586
+ st.y = y;
2587
+ st.lastQuadCtrl = { x: x1, y: y1 };
2588
+ st.lastCubicCtrl = null;
2589
+ return;
2590
+ }
2591
+ case "A": {
2592
+ const rxA = Math.abs(a[0]);
2593
+ const ryA = Math.abs(a[1]);
2594
+ const xRot = a[2];
2595
+ const largeArc = a[3] !== 0;
2596
+ const sweep = a[4] !== 0;
2597
+ const ex = a[5] + rx;
2598
+ const ey = a[6] + ry;
2599
+ arcToCubics(st.x, st.y, ex, ey, rxA, ryA, xRot, largeArc, sweep, b);
2600
+ st.x = ex;
2601
+ st.y = ey;
2602
+ st.lastCubicCtrl = null;
2603
+ st.lastQuadCtrl = null;
2604
+ return;
2605
+ }
2606
+ default:
2607
+ return;
2608
+ }
2609
+ }
2610
+ function arcToCubics(x1, y1, x2, y2, rx, ry, xAxisRotationDeg, largeArc, sweep, b) {
2611
+ if (x1 === x2 && y1 === y2) return;
2612
+ if (rx === 0 || ry === 0) {
2613
+ b.lineTo(x2, y2);
2614
+ return;
2615
+ }
2616
+ const phi = xAxisRotationDeg * Math.PI / 180;
2617
+ const cosPhi = Math.cos(phi);
2618
+ const sinPhi = Math.sin(phi);
2619
+ const dx = (x1 - x2) / 2;
2620
+ const dy = (y1 - y2) / 2;
2621
+ const x1p = cosPhi * dx + sinPhi * dy;
2622
+ const y1p = -sinPhi * dx + cosPhi * dy;
2623
+ let rxs = rx * rx;
2624
+ let rys = ry * ry;
2625
+ const x1ps = x1p * x1p;
2626
+ const y1ps = y1p * y1p;
2627
+ const lambda = x1ps / rxs + y1ps / rys;
2628
+ let rxFixed = rx;
2629
+ let ryFixed = ry;
2630
+ if (lambda > 1) {
2631
+ const sqrtLambda = Math.sqrt(lambda);
2632
+ rxFixed = sqrtLambda * rx;
2633
+ ryFixed = sqrtLambda * ry;
2634
+ rxs = rxFixed * rxFixed;
2635
+ rys = ryFixed * ryFixed;
2636
+ }
2637
+ const sign = largeArc === sweep ? -1 : 1;
2638
+ const num = rxs * rys - rxs * y1ps - rys * x1ps;
2639
+ const den = rxs * y1ps + rys * x1ps;
2640
+ const factor = sign * Math.sqrt(Math.max(0, num / den));
2641
+ const cxp = factor * (rxFixed * y1p) / ryFixed;
2642
+ const cyp = factor * -(ryFixed * x1p) / rxFixed;
2643
+ const cx = cosPhi * cxp - sinPhi * cyp + (x1 + x2) / 2;
2644
+ const cy = sinPhi * cxp + cosPhi * cyp + (y1 + y2) / 2;
2645
+ const angle = (ux, uy, vx, vy) => {
2646
+ const dot = ux * vx + uy * vy;
2647
+ const len = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy));
2648
+ let a = Math.acos(Math.max(-1, Math.min(1, dot / len)));
2649
+ if (ux * vy - uy * vx < 0) a = -a;
2650
+ return a;
2651
+ };
2652
+ const theta1 = angle(1, 0, (x1p - cxp) / rxFixed, (y1p - cyp) / ryFixed);
2653
+ let dTheta = angle(
2654
+ (x1p - cxp) / rxFixed,
2655
+ (y1p - cyp) / ryFixed,
2656
+ (-x1p - cxp) / rxFixed,
2657
+ (-y1p - cyp) / ryFixed
2658
+ );
2659
+ if (!sweep && dTheta > 0) dTheta -= 2 * Math.PI;
2660
+ else if (sweep && dTheta < 0) dTheta += 2 * Math.PI;
2661
+ const segCount = Math.max(1, Math.ceil(Math.abs(dTheta) / (Math.PI / 2)));
2662
+ const segAngle = dTheta / segCount;
2663
+ const t = 4 / 3 * Math.tan(segAngle / 4);
2664
+ let prevX = x1;
2665
+ let prevY = y1;
2666
+ let prevDx = -Math.sin(theta1) * rxFixed;
2667
+ let prevDy = Math.cos(theta1) * ryFixed;
2668
+ for (let i = 1; i <= segCount; i++) {
2669
+ const theta = theta1 + i * segAngle;
2670
+ const cosT = Math.cos(theta);
2671
+ const sinT = Math.sin(theta);
2672
+ const px = cosPhi * (cosT * rxFixed) - sinPhi * (sinT * ryFixed) + cx;
2673
+ const py = sinPhi * (cosT * rxFixed) + cosPhi * (sinT * ryFixed) + cy;
2674
+ const dxNew = -sinT * rxFixed;
2675
+ const dyNew = cosT * ryFixed;
2676
+ const c1x = prevX + cosPhi * (t * prevDx) - sinPhi * (t * prevDy);
2677
+ const c1y = prevY + sinPhi * (t * prevDx) + cosPhi * (t * prevDy);
2678
+ const c2x = px - cosPhi * (t * dxNew) + sinPhi * (t * dyNew);
2679
+ const c2y = py - sinPhi * (t * dxNew) - cosPhi * (t * dyNew);
2680
+ b.curveTo(c1x, c1y, c2x, c2y, px, py);
2681
+ prevX = px;
2682
+ prevY = py;
2683
+ prevDx = dxNew;
2684
+ prevDy = dyNew;
2685
+ }
2686
+ }
2687
+
2688
+ // src/renderer/cache/outlineMeshCache.ts
2689
+ var OUTLINE_FLATTEN_TOLERANCE = 1 / 4096;
2690
+ var OUTLINE_MESH_CACHE_LIMIT = 2048;
2691
+ var cache3 = /* @__PURE__ */ new Map();
2692
+ function outlineMesh(key, d) {
2693
+ const cached = cache3.get(key);
2694
+ if (cached !== void 0) return cached;
2695
+ if (cache3.size >= OUTLINE_MESH_CACHE_LIMIT) cache3 = /* @__PURE__ */ new Map();
2696
+ const mesh = tessellate(pathFromD(d), { flattenTolerance: OUTLINE_FLATTEN_TOLERANCE });
2697
+ cache3.set(key, mesh);
2698
+ return mesh;
2699
+ }
2700
+
2701
+ // src/renderer/draw.ts
2702
+ function setColorMatrixUniforms(ctx, prog) {
2703
+ const gl = ctx.gl;
2704
+ const cm = ctx.state.colorMatrix;
2705
+ const m4 = new Float32Array(16);
2706
+ for (let row = 0; row < 4; row++) {
2707
+ for (let col = 0; col < 4; col++) {
2708
+ m4[col * 4 + row] = cm[row * 5 + col];
2709
+ }
2710
+ }
2711
+ const mLoc = prog.uniform("u_colorMatrix");
2712
+ const bLoc = prog.uniform("u_colorBias");
2713
+ if (mLoc !== void 0) gl.uniformMatrix4fv(mLoc, false, m4);
2714
+ if (bLoc !== void 0) gl.uniform4f(bLoc, cm[4], cm[9], cm[14], cm[19]);
2715
+ }
2716
+ function fillMeshHandle(ctx, path) {
2717
+ if (ctx.flattenTolerance !== void 0) {
2718
+ return ctx.meshCache.uploadTransient(tessellate(path, { flattenTolerance: ctx.flattenTolerance }));
2719
+ }
2720
+ return ctx.meshCache.handleFor(getMesh(path));
2721
+ }
2722
+ function dispatch(ctx, cmd) {
2723
+ switch (cmd.kind) {
2724
+ case "group":
2725
+ return drawGroup(ctx, cmd);
2726
+ case "path":
2727
+ return drawPath(ctx, cmd);
2728
+ case "text":
2729
+ return drawText(ctx, cmd);
2730
+ case "image":
2731
+ return drawImage(ctx, cmd);
2732
+ case "shader":
2733
+ return drawShader(ctx, cmd);
2734
+ }
2735
+ }
2736
+ var warnedUniforms = /* @__PURE__ */ new Set();
2737
+ function warnOnceUniform(programId, name) {
2738
+ const key = `${programId}:${name}`;
2739
+ if (warnedUniforms.has(key)) return;
2740
+ warnedUniforms.add(key);
2741
+ const isDev3 = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
2742
+ if (isDev3) {
2743
+ console.warn(`weasel drawShader: uniform "${name}" not found in program "${programId}". Check spelling, ensure it's used in the shader (unused uniforms are optimized away by the driver).`);
2744
+ }
2745
+ }
2746
+ function setUniform(gl, loc, value, textureCache, nextTexUnit) {
2747
+ if (value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Float32Array) && "id" in value && typeof value.id === "string") {
2748
+ const handle = value;
2749
+ const entry = getTexture(handle.id);
2750
+ if (!entry) {
2751
+ const isDev3 = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
2752
+ if (isDev3) console.warn(`weasel setUniform: TextureHandle "${handle.id}" not registered`);
2753
+ return;
2754
+ }
2755
+ const unit2 = nextTexUnit.value++;
2756
+ textureCache.upload(handle.id, entry.source);
2757
+ textureCache.bind(handle.id, unit2);
2758
+ gl.uniform1i(loc, unit2);
2759
+ return;
2760
+ }
2761
+ if (value instanceof Float32Array) {
2762
+ if (value.length === 9) {
2763
+ gl.uniformMatrix3fv(loc, false, value);
2764
+ } else if (value.length === 16) {
2765
+ gl.uniformMatrix4fv(loc, false, value);
2766
+ } else {
2767
+ const isDev3 = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
2768
+ if (isDev3) throw new TypeError(`weasel setUniform: Float32Array must be length 9 (mat3) or 16 (mat4), got ${value.length}`);
2769
+ }
2770
+ return;
2771
+ }
2772
+ if (Array.isArray(value)) {
2773
+ const arr = value;
2774
+ switch (arr.length) {
2775
+ case 2:
2776
+ gl.uniform2fv(loc, arr);
2777
+ break;
2778
+ case 3:
2779
+ gl.uniform3fv(loc, arr);
2780
+ break;
2781
+ case 4:
2782
+ gl.uniform4fv(loc, arr);
2783
+ break;
2784
+ default: {
2785
+ const isDev3 = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
2786
+ if (isDev3) throw new TypeError(`weasel setUniform: array length ${arr.length} not supported`);
2787
+ }
2788
+ }
2789
+ return;
2790
+ }
2791
+ if (typeof value === "number") {
2792
+ gl.uniform1f(loc, value);
2793
+ return;
2794
+ }
2795
+ }
2796
+ function drawShader(ctx, cmd) {
2797
+ const { gl, programRegistry, quadVbo, quadIbo, textureCache } = ctx;
2798
+ const program = programRegistry.get(cmd.program.id);
2799
+ if (!program) {
2800
+ console.warn(
2801
+ `weasel drawShader: program "${cmd.program.id}" not compiled on this renderer. Call renderer.registerProgram(handle) after the module-level registerProgram().`
2802
+ );
2803
+ return;
2804
+ }
2805
+ if (!quadVbo || !quadIbo) {
2806
+ console.warn("weasel drawShader: quad geometry not initialized");
2807
+ return;
2808
+ }
2809
+ gl.useProgram(program.handle);
2810
+ const aPosLoc = program.attribute("a_position");
2811
+ const aUvLoc = program.attribute("a_uv");
2812
+ gl.bindBuffer(gl.ARRAY_BUFFER, quadVbo);
2813
+ if (aPosLoc !== void 0) {
2814
+ gl.enableVertexAttribArray(aPosLoc);
2815
+ gl.vertexAttribPointer(aPosLoc, 2, gl.FLOAT, false, 16, 0);
2816
+ }
2817
+ if (aUvLoc !== void 0) {
2818
+ gl.enableVertexAttribArray(aUvLoc);
2819
+ gl.vertexAttribPointer(aUvLoc, 2, gl.FLOAT, false, 16, 8);
2820
+ }
2821
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, quadIbo);
2822
+ const proj = mat3.screenToClip(ctx.widthCss, ctx.heightCss);
2823
+ const uProj = program.uniform("u_proj");
2824
+ if (uProj !== void 0) gl.uniformMatrix3fv(uProj, false, proj);
2825
+ const uBounds = program.uniform("u_bounds");
2826
+ if (uBounds !== void 0) {
2827
+ gl.uniform4f(uBounds, cmd.bounds.x, cmd.bounds.y, cmd.bounds.w, cmd.bounds.h);
2828
+ }
2829
+ const uView = program.uniform("u_view");
2830
+ if (uView !== void 0) gl.uniformMatrix3fv(uView, false, ctx.state.transform);
2831
+ const nextTexUnit = { value: 1 };
2832
+ for (const [name, value] of Object.entries(cmd.uniforms)) {
2833
+ const loc = program.uniform(name);
2834
+ if (loc === void 0) {
2835
+ warnOnceUniform(cmd.program.id, name);
2836
+ continue;
2837
+ }
2838
+ setUniform(gl, loc, value, textureCache, nextTexUnit);
2839
+ }
2840
+ applyClipTest(ctx);
2841
+ gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
2842
+ if (aPosLoc !== void 0) gl.disableVertexAttribArray(aPosLoc);
2843
+ if (aUvLoc !== void 0) gl.disableVertexAttribArray(aUvLoc);
2844
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
2845
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
2846
+ }
2847
+ function drawGroup(ctx, cmd) {
2848
+ ctx.state.push({
2849
+ transform: cmd.transform,
2850
+ alpha: cmd.alpha,
2851
+ colorMatrix: cmd.colorMatrix
2852
+ });
2853
+ if (cmd.clip) {
2854
+ const newDepth = ctx.clipDepth + 1;
2855
+ if (newDepth > 7) {
2856
+ ctx.state.pop();
2857
+ throw new Error(
2858
+ "weasel: clip nesting depth exceeded (max 7). You can't nest more than 7 levels of clipped containers in a single draw tree. Flatten the hierarchy or compose poses outside the scene graph."
2859
+ );
2860
+ }
2861
+ pushClip(ctx, cmd.clip, newDepth);
2862
+ ctx.clipDepth = newDepth;
2863
+ }
2864
+ for (const child of cmd.children) dispatch(ctx, child);
2518
2865
  if (cmd.clip) {
2519
2866
  popClip(ctx, cmd.clip, ctx.clipDepth - 1);
2520
2867
  ctx.clipDepth -= 1;
@@ -2877,36 +3224,36 @@ function drawPathStrokeStenciled(ctx, cmd, align) {
2877
3224
  gl.disable(gl.STENCIL_TEST);
2878
3225
  gl.bindVertexArray(null);
2879
3226
  }
3227
+ var OUTLINE_MIN_SCREEN_PX = 48;
3228
+ function modelScale(m) {
3229
+ const sx = Math.hypot(m[0], m[1]);
3230
+ const sy = Math.hypot(m[3], m[4]);
3231
+ return Math.sqrt(sx * sy) || 1;
3232
+ }
2880
3233
  function drawText(ctx, cmd) {
2881
3234
  const style = resolveTextStyle(cmd.style);
2882
3235
  const lineHeight = style.lineHeight;
2883
3236
  const align = cmd.align ?? style.align;
2884
3237
  const maxWidth = cmd.maxWidth ?? Infinity;
2885
- const laid = layoutRuns(
3238
+ const minScreen = ctx.textOutlineMinScreenSize ?? OUTLINE_MIN_SCREEN_PX;
3239
+ const outlineMinSize = Number.isFinite(minScreen) ? minScreen / modelScale(ctx.state.transform) : void 0;
3240
+ const laid = cachedLayoutRuns(
2886
3241
  cmd.runs,
2887
- { maxWidth, lineHeight, align },
3242
+ { maxWidth, lineHeight, align, outlineMinSize },
2888
3243
  { x: cmd.x, y: cmd.y }
2889
3244
  );
2890
3245
  if (laid.groups.length === 0 && laid.decorations.length === 0) return;
2891
3246
  const dy = verticalAlignOffset(cmd.verticalAlign, cmd.height, laid.bounds.height);
2892
- if (dy !== 0) {
2893
- for (const group of laid.groups) {
2894
- for (const q of group.quads) {
2895
- q.y0 += dy;
2896
- q.y1 += dy;
2897
- q.baselineY += dy;
2898
- }
2899
- }
2900
- for (const d of laid.decorations) {
2901
- d.y0 += dy;
2902
- d.y1 += dy;
2903
- }
2904
- }
2905
3247
  const gl = ctx.gl;
2906
3248
  applyClipTest(ctx);
2907
3249
  const preparedPrograms = /* @__PURE__ */ new Set();
2908
3250
  let currentProg = null;
2909
3251
  for (const group of laid.groups) {
3252
+ if (group.source === "outline") {
3253
+ drawTextOutlineGroup(ctx, group, dy);
3254
+ currentProg = null;
3255
+ continue;
3256
+ }
2910
3257
  const prog = group.source === "canvas" ? ctx.textSdfR8 : ctx.textSdf;
2911
3258
  if (prog !== currentProg) {
2912
3259
  gl.useProgram(prog.handle);
@@ -2918,11 +3265,48 @@ function drawText(ctx, cmd) {
2918
3265
  setColorMatrixUniforms(ctx, prog);
2919
3266
  gl.uniform1f(prog.uniform("u_alpha"), ctx.state.alpha);
2920
3267
  }
2921
- drawTextGroup(ctx, group, prog);
3268
+ drawTextGroup(ctx, group, prog, dy);
2922
3269
  }
2923
- drawTextDecorations(ctx, laid.decorations);
3270
+ drawTextDecorations(ctx, laid.decorations, dy);
2924
3271
  }
2925
- function drawTextDecorations(ctx, decorations) {
3272
+ var SYNTHETIC_ITALIC_RADIANS = 0.2094;
3273
+ function drawTextOutlineGroup(ctx, group, dy) {
3274
+ const mesh = outlineGroupMesh(group, dy);
3275
+ if (!mesh) return;
3276
+ drawPathFillByKind(ctx, group.fill, ctx.meshCache.uploadTransient(mesh));
3277
+ }
3278
+ function outlineGroupMesh(group, dy) {
3279
+ const parts = [];
3280
+ let vertexFloats = 0;
3281
+ let indexCount = 0;
3282
+ for (const glyph of group.glyphs) {
3283
+ const mesh = outlineMesh(glyph.key, glyph.d);
3284
+ if (mesh.indices.length === 0) continue;
3285
+ parts.push({ mesh, glyph });
3286
+ vertexFloats += mesh.vertices.length;
3287
+ indexCount += mesh.indices.length;
3288
+ }
3289
+ if (parts.length === 0) return null;
3290
+ const shear = group.synthetic.italic ? Math.tan(SYNTHETIC_ITALIC_RADIANS) : 0;
3291
+ const vertices = new Float32Array(vertexFloats);
3292
+ const indices = new Uint32Array(indexCount);
3293
+ let vi = 0;
3294
+ let ii = 0;
3295
+ let base = 0;
3296
+ for (const { mesh, glyph } of parts) {
3297
+ const { x, baselineY, scale: scale2 } = glyph;
3298
+ for (let k = 0; k < mesh.vertices.length; k += 2) {
3299
+ const ex = mesh.vertices[k];
3300
+ const ey = mesh.vertices[k + 1];
3301
+ vertices[vi++] = x + (ex - ey * shear) * scale2;
3302
+ vertices[vi++] = baselineY + dy + ey * scale2;
3303
+ }
3304
+ for (let k = 0; k < mesh.indices.length; k++) indices[ii++] = base + mesh.indices[k];
3305
+ base += mesh.vertices.length / 2;
3306
+ }
3307
+ return { vertices, indices };
3308
+ }
3309
+ function drawTextDecorations(ctx, decorations, dy) {
2926
3310
  if (decorations.length === 0) return;
2927
3311
  const batches = /* @__PURE__ */ new Map();
2928
3312
  for (const d of decorations) {
@@ -2945,14 +3329,15 @@ function drawTextDecorations(ctx, decorations) {
2945
3329
  const vertices = new Float32Array(rects.length * 4 * 2);
2946
3330
  let vi = 0;
2947
3331
  for (const d of rects) {
3332
+ const dy0 = d.y0 + dy, dy1 = d.y1 + dy;
2948
3333
  vertices[vi++] = d.x0;
2949
- vertices[vi++] = d.y0;
3334
+ vertices[vi++] = dy0;
2950
3335
  vertices[vi++] = d.x1;
2951
- vertices[vi++] = d.y0;
3336
+ vertices[vi++] = dy0;
2952
3337
  vertices[vi++] = d.x0;
2953
- vertices[vi++] = d.y1;
3338
+ vertices[vi++] = dy1;
2954
3339
  vertices[vi++] = d.x1;
2955
- vertices[vi++] = d.y1;
3340
+ vertices[vi++] = dy1;
2956
3341
  }
2957
3342
  const indices = new Uint32Array(rects.length * 6);
2958
3343
  let ii = 0;
@@ -2990,7 +3375,7 @@ function drawTextDecorations(ctx, decorations) {
2990
3375
  gl.deleteBuffer(ibo);
2991
3376
  }
2992
3377
  }
2993
- function drawTextGroup(ctx, group, prog) {
3378
+ function drawTextGroup(ctx, group, prog, dy) {
2994
3379
  if (group.source === "canvas") {
2995
3380
  if (!syncDynamicPageTexture(ctx.textureCache, group.page)) return;
2996
3381
  } else {
@@ -3001,26 +3386,27 @@ function drawTextGroup(ctx, group, prog) {
3001
3386
  const vertices = new Float32Array(group.quads.length * 4 * 5);
3002
3387
  let vi = 0;
3003
3388
  for (const q of group.quads) {
3389
+ const y0 = q.y0 + dy, y1 = q.y1 + dy, by = q.baselineY + dy;
3004
3390
  vertices[vi++] = q.x0;
3005
- vertices[vi++] = q.y0;
3391
+ vertices[vi++] = y0;
3006
3392
  vertices[vi++] = q.u0;
3007
3393
  vertices[vi++] = q.v0;
3008
- vertices[vi++] = q.baselineY;
3394
+ vertices[vi++] = by;
3009
3395
  vertices[vi++] = q.x1;
3010
- vertices[vi++] = q.y0;
3396
+ vertices[vi++] = y0;
3011
3397
  vertices[vi++] = q.u1;
3012
3398
  vertices[vi++] = q.v0;
3013
- vertices[vi++] = q.baselineY;
3399
+ vertices[vi++] = by;
3014
3400
  vertices[vi++] = q.x0;
3015
- vertices[vi++] = q.y1;
3401
+ vertices[vi++] = y1;
3016
3402
  vertices[vi++] = q.u0;
3017
3403
  vertices[vi++] = q.v1;
3018
- vertices[vi++] = q.baselineY;
3404
+ vertices[vi++] = by;
3019
3405
  vertices[vi++] = q.x1;
3020
- vertices[vi++] = q.y1;
3406
+ vertices[vi++] = y1;
3021
3407
  vertices[vi++] = q.u1;
3022
3408
  vertices[vi++] = q.v1;
3023
- vertices[vi++] = q.baselineY;
3409
+ vertices[vi++] = by;
3024
3410
  }
3025
3411
  const indices = new Uint32Array(group.quads.length * 6);
3026
3412
  let ii = 0;
@@ -3068,7 +3454,7 @@ function drawTextGroup(ctx, group, prog) {
3068
3454
  const synthBoldAmount = group.synthetic.bold ? 0.08 : 0;
3069
3455
  const uSynthBold = prog.uniform("u_synthBold");
3070
3456
  if (uSynthBold !== void 0) gl.uniform1f(uSynthBold, synthBoldAmount);
3071
- const synthItalicAmount = group.synthetic.italic ? 0.2094 : 0;
3457
+ const synthItalicAmount = group.synthetic.italic ? SYNTHETIC_ITALIC_RADIANS : 0;
3072
3458
  const uSynthItalic = prog.uniform("u_synthItalic");
3073
3459
  if (uSynthItalic !== void 0) gl.uniform1f(uSynthItalic, synthItalicAmount);
3074
3460
  const texId = group.source === "canvas" ? dynamicPageTextureId(group.page) : textureCacheKey(group.family, group.weight, group.style);
@@ -3249,6 +3635,7 @@ var WeaselRenderer = class {
3249
3635
  imageMinification;
3250
3636
  flattenTolerance;
3251
3637
  bakeBudget;
3638
+ textOutlineMinScreenSize;
3252
3639
  contextLost = false;
3253
3640
  boundOnLost = (e) => this.onContextLost(e);
3254
3641
  boundOnRestored = () => this.onContextRestored();
@@ -3268,6 +3655,7 @@ var WeaselRenderer = class {
3268
3655
  this.imageMinification = opts.imageMinification ?? "linear";
3269
3656
  this.flattenTolerance = opts.flattenTolerance;
3270
3657
  this.bakeBudget = opts.bakeBudget ?? DEFAULT_BAKE_BUDGET;
3658
+ this.textOutlineMinScreenSize = opts.textOutlineMinScreenSize ?? OUTLINE_MIN_SCREEN_PX;
3271
3659
  this.canvas = opts.canvas ?? null;
3272
3660
  if (this.canvas) {
3273
3661
  this.canvas.width = Math.round(opts.width * opts.dpr);
@@ -3511,7 +3899,8 @@ var WeaselRenderer = class {
3511
3899
  widthCss: this.widthCss,
3512
3900
  heightCss: this.heightCss,
3513
3901
  clipDepth: 0,
3514
- flattenTolerance: this.flattenTolerance
3902
+ flattenTolerance: this.flattenTolerance,
3903
+ textOutlineMinScreenSize: this.textOutlineMinScreenSize
3515
3904
  };
3516
3905
  for (const cmd of commands) dispatch(ctx, cmd);
3517
3906
  this.meshCache.freeTransient();
@@ -8058,15 +8447,25 @@ var reorderForwardAction = {
8058
8447
  group: "reorder",
8059
8448
  defaultBinding: [
8060
8449
  {
8061
- spec: { kind: "key", key: ["]", "}"], mods: { mod: true } },
8450
+ // Modifier matching is strict, so Shift must be absent here listing
8451
+ // the shifted `'}'` on this binding would be dead weight, since you
8452
+ // can't type it without the Shift this spec forbids.
8453
+ spec: { kind: "key", key: ["]"], mods: { mod: true } },
8062
8454
  opts: { params: { distance: "adjacent" } }
8063
8455
  },
8064
8456
  {
8065
- // Cmd+Alt+] → bring-to-front. NOT Cmd+Shift+] Chrome on macOS reserves
8066
- // Cmd+Shift+[ / Cmd+Shift+] for tab switching and never delivers them to
8067
- // the page. On macOS Option+] emits the character '‘' (U+2018); whether
8068
- // Cmd suppresses that transform is browser-dependent, so we match both the
8069
- // bracket and the Option-produced char (US layout). matchKey is
8457
+ // Cmd+Shift+] → bring-to-front, the convention every drawing app uses.
8458
+ // With Shift held the US-layout key reports as '}', so both characters
8459
+ // are listed. Some browsers bind Cmd+Shift+[ / ] to tab switching and
8460
+ // may consume the event before the page sees it hence the Cmd+Alt
8461
+ // binding below, which nothing reserves.
8462
+ spec: { kind: "key", key: ["]", "}"], mods: { mod: true, shift: true } },
8463
+ opts: { params: { distance: "extreme" } }
8464
+ },
8465
+ {
8466
+ // Cmd+Alt+] → bring-to-front. On macOS Option+] emits '‘' (U+2018);
8467
+ // whether Cmd suppresses that transform is browser-dependent, so match
8468
+ // both the bracket and the Option-produced char (US layout). matchKey is
8070
8469
  // character-based, so non-US layouts may need a rebind.
8071
8470
  spec: { kind: "key", key: ["]", "\u2018"], mods: { mod: true, alt: true } },
8072
8471
  opts: { params: { distance: "extreme" } }
@@ -8093,13 +8492,17 @@ var reorderBackwardAction = {
8093
8492
  group: "reorder",
8094
8493
  defaultBinding: [
8095
8494
  {
8096
- spec: { kind: "key", key: ["[", "{"], mods: { mod: true } },
8495
+ spec: { kind: "key", key: ["["], mods: { mod: true } },
8097
8496
  opts: { params: { distance: "adjacent" } }
8098
8497
  },
8099
8498
  {
8100
- // Cmd+Alt+[ → send-to-back. See the bring-to-front note above: avoids
8101
- // Chrome's reserved Cmd+Shift+[ tab-switch shortcut. On macOS Option+[
8102
- // emits '“' (U+201C), included alongside the bracket for robustness.
8499
+ // Cmd+Shift+[ → send-to-back. See the bring-to-front notes above.
8500
+ spec: { kind: "key", key: ["[", "{"], mods: { mod: true, shift: true } },
8501
+ opts: { params: { distance: "extreme" } }
8502
+ },
8503
+ {
8504
+ // Cmd+Alt+[ → send-to-back, the binding no browser reserves. On macOS
8505
+ // Option+[ emits '“' (U+201C), included alongside the bracket.
8103
8506
  spec: { kind: "key", key: ["[", "\u201C"], mods: { mod: true, alt: true } },
8104
8507
  opts: { params: { distance: "extreme" } }
8105
8508
  }
@@ -10437,6 +10840,9 @@ function useHoverTracking(args) {
10437
10840
  }
10438
10841
 
10439
10842
  // src/interactions/dispatcher/dispatcher.ts
10843
+ function pointerGestureId(pointerId) {
10844
+ return `pointer-${pointerId ?? "mouse"}`;
10845
+ }
10440
10846
  var DEV2 = (() => {
10441
10847
  try {
10442
10848
  return Boolean(import.meta.env?.DEV);
@@ -10577,7 +10983,7 @@ function createDispatcher(opts) {
10577
10983
  return `key-held-${event.key}`;
10578
10984
  }
10579
10985
  if (event.kind === "pointerdown" || event.kind === "pointermove" || event.kind === "pointerup" || event.kind === "pointercancel") {
10580
- return `pointer-mouse`;
10986
+ return pointerGestureId(event.pointerId);
10581
10987
  }
10582
10988
  if (event.kind === "multitouch") {
10583
10989
  return `multitouch-${event.fingers}`;
@@ -11314,6 +11720,7 @@ function useGestureDispatcher(opts) {
11314
11720
  const ev = {
11315
11721
  kind: "pointerdown",
11316
11722
  target: e.target,
11723
+ pointerId: e.pointerId,
11317
11724
  x: w.x,
11318
11725
  y: w.y,
11319
11726
  clientX: e.clientX,
@@ -11343,6 +11750,7 @@ function useGestureDispatcher(opts) {
11343
11750
  shiftKey: e.shiftKey
11344
11751
  });
11345
11752
  if (activePointers.size >= 2) {
11753
+ for (const id of activePointers) bufferedDown.delete(id);
11346
11754
  const { centroid, spread } = computeMultiTouchGeometry(pointerPositions);
11347
11755
  if (!multiTouchStart) {
11348
11756
  multiTouchStart = { fingers: activePointers.size, centroid: { ...centroid } };
@@ -11393,6 +11801,7 @@ function useGestureDispatcher(opts) {
11393
11801
  const w = toWorld(e.clientX, e.clientY);
11394
11802
  const ev = {
11395
11803
  kind: "pointermove",
11804
+ pointerId: e.pointerId,
11396
11805
  x: w.x,
11397
11806
  y: w.y,
11398
11807
  clientX: e.clientX,
@@ -11516,10 +11925,11 @@ function useGestureDispatcher(opts) {
11516
11925
  }
11517
11926
  const downForClick = lastPointerDown.get(e.pointerId);
11518
11927
  const movedDuringDrag = downForClick ? downForClick.clientX !== e.clientX || downForClick.clientY !== e.clientY : true;
11519
- const hadDragInFlight = dispatcher.inFlight().has("pointer-mouse") && movedDuringDrag;
11928
+ const hadDragInFlight = dispatcher.inFlight().has(pointerGestureId(e.pointerId)) && movedDuringDrag;
11520
11929
  const wUp = toWorld(e.clientX, e.clientY);
11521
11930
  const ev = {
11522
11931
  kind: "pointerup",
11932
+ pointerId: e.pointerId,
11523
11933
  x: wUp.x,
11524
11934
  y: wUp.y,
11525
11935
  clientX: e.clientX,
@@ -11594,6 +12004,7 @@ function useGestureDispatcher(opts) {
11594
12004
  multiTouchStart = null;
11595
12005
  const ev = {
11596
12006
  kind: "pointercancel",
12007
+ pointerId: e.pointerId,
11597
12008
  altKey: e.altKey,
11598
12009
  ctrlKey: e.ctrlKey,
11599
12010
  metaKey: e.metaKey,
@@ -12062,6 +12473,17 @@ function useTools(opts) {
12062
12473
  if (hotkeyRef.current) dlog("tools", "hotkey disengaged:", hotkeyRef.current);
12063
12474
  ctx.popHotkey();
12064
12475
  }, [ctx]);
12476
+ const lastConflictSigRef = useRef(null);
12477
+ const toolSetSig = Object.keys(opts.registry).join(",") + "|" + (opts.ambient ?? []).map((t) => t.id).join(",");
12478
+ useEffect(() => {
12479
+ if (process.env.NODE_ENV === "production") return;
12480
+ if (lastConflictSigRef.current === toolSetSig) return;
12481
+ lastConflictSigRef.current = toolSetSig;
12482
+ reportRouteConflicts({
12483
+ registry: registryRef.current,
12484
+ ambient: ambientRef.current
12485
+ });
12486
+ }, [toolSetSig]);
12065
12487
  const has = useCallback(
12066
12488
  (id) => id in registryRef.current || ambientRef.current.some((t) => t.id === id),
12067
12489
  []
@@ -12670,17 +13092,123 @@ function pathContainsPolygon(path, polygon) {
12670
13092
  (p) => p.x >= path.x && p.x <= path.x + path.width && p.y >= path.y && p.y <= path.y + path.height
12671
13093
  );
12672
13094
  }
12673
- return polygonContainsPolygon(extractVertices(path), polygon);
13095
+ return polygonContainsPolygon(extractVertices(path), polygon);
13096
+ }
13097
+ function pathIntersectsPolygon(path, polygon) {
13098
+ if (path.kind === "rect") {
13099
+ return polygonsIntersect(rectToVerts(path), polygon);
13100
+ }
13101
+ return polygonsIntersect(extractVertices(path), polygon);
13102
+ }
13103
+
13104
+ // src/features/text/runs.ts
13105
+ function toRuns(input) {
13106
+ if (typeof input === "string") {
13107
+ return input.length === 0 ? [] : [{ text: input }];
13108
+ }
13109
+ for (let i = 0; i < input.length; i++) {
13110
+ const r = input[i];
13111
+ if (typeof r?.text !== "string") {
13112
+ throw new Error(`toRuns: run at index ${i} is missing string \`text\``);
13113
+ }
13114
+ }
13115
+ return input;
13116
+ }
13117
+ function runsToPlainText(runs) {
13118
+ let out = "";
13119
+ for (const r of runs) out += r.text;
13120
+ return out;
13121
+ }
13122
+ function escapeMarkdown(s) {
13123
+ return s.replace(/\\/g, "\\\\").replace(/\*/g, "\\*");
13124
+ }
13125
+ function runsToMarkdown(runs) {
13126
+ let out = "";
13127
+ for (const r of runs) {
13128
+ const escaped = escapeMarkdown(r.text);
13129
+ if (r.bold && r.italic) out += `***${escaped}***`;
13130
+ else if (r.bold) out += `**${escaped}**`;
13131
+ else if (r.italic) out += `*${escaped}*`;
13132
+ else out += escaped;
13133
+ }
13134
+ return out;
13135
+ }
13136
+ function markdownToRuns(input) {
13137
+ const runs = [];
13138
+ let bold = false;
13139
+ let italic = false;
13140
+ let buf = "";
13141
+ let i = 0;
13142
+ function flush() {
13143
+ if (buf.length === 0) return;
13144
+ const run = { text: buf };
13145
+ if (bold) run.bold = true;
13146
+ if (italic) run.italic = true;
13147
+ runs.push(run);
13148
+ buf = "";
13149
+ }
13150
+ while (i < input.length) {
13151
+ const ch = input[i];
13152
+ if (ch === "\\" && i + 1 < input.length && "*\\".includes(input[i + 1])) {
13153
+ buf += input[i + 1];
13154
+ i += 2;
13155
+ continue;
13156
+ }
13157
+ if (ch === "*") {
13158
+ let count = 0;
13159
+ while (i + count < input.length && input[i + count] === "*") count++;
13160
+ flush();
13161
+ if (count >= 3) {
13162
+ bold = !bold;
13163
+ italic = !italic;
13164
+ i += 3;
13165
+ } else if (count === 2) {
13166
+ bold = !bold;
13167
+ i += 2;
13168
+ } else {
13169
+ italic = !italic;
13170
+ i += 1;
13171
+ }
13172
+ continue;
13173
+ }
13174
+ buf += ch;
13175
+ i++;
13176
+ }
13177
+ flush();
13178
+ return runs;
12674
13179
  }
12675
- function pathIntersectsPolygon(path, polygon) {
12676
- if (path.kind === "rect") {
12677
- return polygonsIntersect(rectToVerts(path), polygon);
13180
+
13181
+ // src/features/text/lineBoxes.ts
13182
+ function textLineBoxes(pose, opts = {}) {
13183
+ const padding = opts.padding ?? 0;
13184
+ const style = resolveTextStyle(pose.style);
13185
+ const source = pose.runs && pose.runs.length > 0 ? pose.runs : pose.text;
13186
+ const runs = resolveRuns(toRuns(source), style);
13187
+ const laid = layoutRuns(
13188
+ runs,
13189
+ {
13190
+ maxWidth: opts.maxWidth ?? pose.width,
13191
+ lineHeight: style.lineHeight,
13192
+ align: style.align
13193
+ },
13194
+ { x: pose.x, y: pose.y }
13195
+ );
13196
+ const dy = verticalAlignOffset(pose.verticalAlign, pose.height, laid.bounds.height);
13197
+ const out = [];
13198
+ for (const line of laid.lines) {
13199
+ if (!opts.includeEmpty && line.x1 <= line.x0) continue;
13200
+ out.push({
13201
+ x: line.x0 - padding,
13202
+ y: line.y0 + dy - padding,
13203
+ width: line.x1 - line.x0 + padding * 2,
13204
+ height: line.y1 - line.y0 + padding * 2
13205
+ });
12678
13206
  }
12679
- return polygonsIntersect(extractVertices(path), polygon);
13207
+ return out;
12680
13208
  }
12681
13209
 
12682
13210
  // src/features/images/imageCache.ts
12683
- var cache2 = /* @__PURE__ */ new Map();
13211
+ var cache4 = /* @__PURE__ */ new Map();
12684
13212
  var subscribers = /* @__PURE__ */ new Set();
12685
13213
  var defaultLoader = async (src) => {
12686
13214
  if (typeof Image === "undefined" || typeof createImageBitmap === "undefined") {
@@ -12698,28 +13226,28 @@ function notify() {
12698
13226
  }
12699
13227
  function startLoad(src) {
12700
13228
  const entry = { status: "loading" };
12701
- cache2.set(src, entry);
13229
+ cache4.set(src, entry);
12702
13230
  loader(src).then(
12703
13231
  (bitmap) => {
12704
- if (cache2.get(src) !== entry) return;
12705
- cache2.set(src, { status: "ready", bitmap });
13232
+ if (cache4.get(src) !== entry) return;
13233
+ cache4.set(src, { status: "ready", bitmap });
12706
13234
  notify();
12707
13235
  },
12708
13236
  () => {
12709
- if (cache2.get(src) !== entry) return;
12710
- cache2.set(src, { status: "error" });
13237
+ if (cache4.get(src) !== entry) return;
13238
+ cache4.set(src, { status: "error" });
12711
13239
  notify();
12712
13240
  }
12713
13241
  );
12714
13242
  }
12715
13243
  function getImageBitmap(src) {
12716
- const entry = cache2.get(src);
13244
+ const entry = cache4.get(src);
12717
13245
  if (entry) return entry.bitmap;
12718
13246
  startLoad(src);
12719
13247
  return void 0;
12720
13248
  }
12721
13249
  function imageStatus(src) {
12722
- return cache2.get(src)?.status ?? "idle";
13250
+ return cache4.get(src)?.status ?? "idle";
12723
13251
  }
12724
13252
  function subscribeImageReady(cb) {
12725
13253
  subscribers.add(cb);
@@ -12728,7 +13256,39 @@ function subscribeImageReady(cb) {
12728
13256
  };
12729
13257
  }
12730
13258
 
13259
+ // src/core/scene/nodeMemo.ts
13260
+ var MEMO = /* @__PURE__ */ new WeakMap();
13261
+ var generation = 0;
13262
+ function nodeMemo(node, slot, pose, derive) {
13263
+ let record = MEMO.get(node);
13264
+ if (record === void 0) {
13265
+ record = { generation, slots: /* @__PURE__ */ new Map() };
13266
+ MEMO.set(node, record);
13267
+ } else if (record.generation !== generation) {
13268
+ record.generation = generation;
13269
+ record.slots.clear();
13270
+ }
13271
+ const data = node.data;
13272
+ const hit = record.slots.get(slot);
13273
+ if (hit !== void 0 && hit.pose === pose && hit.data === data) {
13274
+ return hit.value;
13275
+ }
13276
+ const value = derive();
13277
+ if (hit !== void 0) {
13278
+ hit.pose = pose;
13279
+ hit.data = data;
13280
+ hit.value = value;
13281
+ } else {
13282
+ record.slots.set(slot, { pose, data, value });
13283
+ }
13284
+ return value;
13285
+ }
13286
+ function bumpNodeMemoGeneration() {
13287
+ generation++;
13288
+ }
13289
+
12731
13290
  // src/canvas/NodeShape.ts
13291
+ var DEFAULT_INK = { filled: true, strokeWidth: 0 };
12732
13292
  var PAINTERS = {
12733
13293
  high: [],
12734
13294
  normal: []
@@ -12736,12 +13296,25 @@ var PAINTERS = {
12736
13296
  function registerNodeShape(painter, opts = {}) {
12737
13297
  const list = opts.priority === "high" ? PAINTERS.high : PAINTERS.normal;
12738
13298
  list.push(painter);
13299
+ bumpNodeMemoGeneration();
12739
13300
  return () => {
12740
13301
  const i = list.indexOf(painter);
12741
13302
  if (i >= 0) list.splice(i, 1);
13303
+ bumpNodeMemoGeneration();
12742
13304
  };
12743
13305
  }
13306
+ var PAINTER_SLOT = "shape:painter";
13307
+ var SILHOUETTE_SLOT = "shape:silhouette";
13308
+ var PAINT_SLOT = "shape:paint";
12744
13309
  function findNodeShape(node) {
13310
+ return nodeMemo(
13311
+ node,
13312
+ PAINTER_SLOT,
13313
+ void 0,
13314
+ () => matchNodeShape(node)
13315
+ );
13316
+ }
13317
+ function matchNodeShape(node) {
12745
13318
  for (const p of PAINTERS.high) {
12746
13319
  if (p.matches(node)) {
12747
13320
  return p;
@@ -12755,10 +13328,22 @@ function findNodeShape(node) {
12755
13328
  return void 0;
12756
13329
  }
12757
13330
  function findShapeSilhouette(node, pose) {
12758
- const sil = findNodeShape(node)?.silhouette?.(node, pose) ?? null;
12759
- if (!sil) return null;
12760
- const r = poseRotationOf(pose);
12761
- return r ? rotatePathAround(sil, r.cx, r.cy, r.rotation) : sil;
13331
+ return nodeMemo(node, SILHOUETTE_SLOT, pose, () => {
13332
+ const sil = findNodeShape(node)?.silhouette?.(node, pose) ?? null;
13333
+ const r = sil ? poseRotationOf(pose) : null;
13334
+ return sil && r ? rotatePathAround(sil, r.cx, r.cy, r.rotation) : sil;
13335
+ });
13336
+ }
13337
+ function findShapeInk(node, pose) {
13338
+ return findNodeShape(node)?.ink?.(node, pose) ?? null;
13339
+ }
13340
+ function shapeCoversPoint(node, pose, x, y, opts = {}) {
13341
+ const sil = findShapeSilhouette(node, pose);
13342
+ if (sil === null) return true;
13343
+ const ink = findShapeInk(node, pose) ?? DEFAULT_INK;
13344
+ if (ink.filled && pathContainsPoint(sil, x, y)) return true;
13345
+ const reach = ink.strokeWidth / 2 + (opts.tolerance ?? 0);
13346
+ return reach > 0 && strokeHitTest(sil, x, y, reach);
12762
13347
  }
12763
13348
  function getNodeShapes() {
12764
13349
  return [...PAINTERS.high, ...PAINTERS.normal];
@@ -12769,21 +13354,86 @@ var TEXT_PAINTER = {
12769
13354
  const d = node.data;
12770
13355
  return d?.text != null;
12771
13356
  },
12772
- paint: (node, pose) => {
13357
+ // Memoized, and not for its own sake — `resolveTextStyle` + `resolveRuns`
13358
+ // are cheap (0.14 ms/frame at 1000 text nodes). It is the enabler: the
13359
+ // renderer's layout cache keys on the `ResolvedRun[]` array identity, and
13360
+ // `layoutRuns` costs 25.9 ms/frame for 200 wrapped paragraphs. Handing back
13361
+ // a fresh runs array per frame would leave that cache missing on every
13362
+ // draw, exactly as a fresh `Path` per frame defeated the mesh cache. Both
13363
+ // resolvers are pure style merging — no font reads — so `(data, pose)` is
13364
+ // the whole key. See PAINT_SLOT.
13365
+ paint: (node, pose) => nodeMemo(node, PAINT_SLOT, pose, () => {
12773
13366
  const d = node.data;
12774
13367
  const p = pose;
12775
- const fontSize = d.style?.fontSize ?? 16;
12776
- const y = p.y + fontSize;
13368
+ const y = p.y;
12777
13369
  return d.runs && d.runs.length > 0 ? [textCommandFromRuns(p.x, y, d.runs, d.style, void 0, p.height)] : [textCommand(p.x, y, d.text, d.style, void 0, p.height)];
13370
+ }),
13371
+ // The pose is a *wrap box*, not a bounding box — "Away" in a 300-unit box
13372
+ // leaves most of it empty, and a pose-rect silhouette claims all of it. The
13373
+ // union of the line boxes is what the node actually covers, so picking,
13374
+ // lasso and clipping stop grabbing blank space. `Infinity` because `paint`
13375
+ // above deliberately does not forward `maxWidth`: this text does not wrap,
13376
+ // and boxes measured against `p.width` would wrap where the paint did not.
13377
+ //
13378
+ // `null` rather than an empty path when there are no non-blank lines: an
13379
+ // empty text node would otherwise become unpickable, and a caller reading
13380
+ // "no silhouette" falls back to the pose rect, which is the behavior an
13381
+ // empty box wants.
13382
+ silhouette: (node, pose) => {
13383
+ const d = node.data;
13384
+ const p = pose;
13385
+ const boxes = textLineBoxes(
13386
+ {
13387
+ x: p.x,
13388
+ y: p.y,
13389
+ width: p.width,
13390
+ height: p.height,
13391
+ text: d.text,
13392
+ runs: d.runs,
13393
+ style: d.style
13394
+ },
13395
+ { maxWidth: Infinity }
13396
+ );
13397
+ if (boxes.length === 0) return null;
13398
+ return rectsToPath(boxes);
12778
13399
  }
12779
13400
  };
13401
+ function rectsToPath(rects) {
13402
+ const commands = new Uint8Array(rects.length * 6);
13403
+ const coords = new Float32Array(rects.length * 10);
13404
+ let ci = 0;
13405
+ let cmd = 0;
13406
+ for (const r of rects) {
13407
+ const { x, y, width: w, height: h } = r;
13408
+ commands[cmd++] = PATH_M;
13409
+ coords[ci++] = x;
13410
+ coords[ci++] = y;
13411
+ commands[cmd++] = PATH_L;
13412
+ coords[ci++] = x + w;
13413
+ coords[ci++] = y;
13414
+ commands[cmd++] = PATH_L;
13415
+ coords[ci++] = x + w;
13416
+ coords[ci++] = y + h;
13417
+ commands[cmd++] = PATH_L;
13418
+ coords[ci++] = x;
13419
+ coords[ci++] = y + h;
13420
+ commands[cmd++] = PATH_L;
13421
+ coords[ci++] = x;
13422
+ coords[ci++] = y;
13423
+ commands[cmd++] = PATH_Z;
13424
+ }
13425
+ return { kind: "polygon", commands, coords, fillRule: "nonzero" };
13426
+ }
12780
13427
  var PATH_PAINTER = {
12781
13428
  id: "kit:path",
12782
13429
  matches: (node) => {
12783
13430
  const d = node.data;
12784
13431
  return d?.path != null;
12785
13432
  },
12786
- paint: (node, pose) => {
13433
+ // Memoized: a pure function of `(data, pose)`, and `pathInPoseFrame`
13434
+ // allocates on every call but the identity (`kind: 'rect'`) branch. See
13435
+ // PAINT_SLOT.
13436
+ paint: (node, pose) => nodeMemo(node, PAINT_SLOT, pose, () => {
12787
13437
  const d = node.data;
12788
13438
  const projected = pathInPoseFrame(d.path, pose);
12789
13439
  const hasStroke = !!d.stroke && d.stroke !== "none" && (d.strokeWidth ?? 0) > 0;
@@ -12796,10 +13446,23 @@ var PATH_PAINTER = {
12796
13446
  ...hasStroke ? { stroke: { paint: { color: d.stroke }, width: d.strokeWidth ?? 1 } } : {}
12797
13447
  };
12798
13448
  return [cmd];
12799
- },
13449
+ }),
12800
13450
  silhouette: (node, pose) => {
12801
13451
  const d = node.data;
12802
13452
  return pathInPoseFrame(d.path, pose);
13453
+ },
13454
+ // Mirrors the fill/stroke decisions `paint` makes above — same reads, same
13455
+ // 'none' handling, same "no fill declared and a stroke present means
13456
+ // stroke-only" fallback. Kept in step with `paint` by
13457
+ // `NodeShape.ink.test.ts`, which asserts the two agree.
13458
+ ink: (node) => {
13459
+ const d = node.data;
13460
+ const hasStroke = !!d.stroke && d.stroke !== "none" && (d.strokeWidth ?? 0) > 0;
13461
+ const fillColor = d.fill ?? d.color;
13462
+ return {
13463
+ filled: fillColor !== "none" && (fillColor !== void 0 || !hasStroke),
13464
+ strokeWidth: hasStroke ? d.strokeWidth ?? 1 : 0
13465
+ };
12803
13466
  }
12804
13467
  };
12805
13468
  var SHAPE_PAINTER = {
@@ -12808,7 +13471,10 @@ var SHAPE_PAINTER = {
12808
13471
  const s = node.data?.shape;
12809
13472
  return s != null && SHAPE_KINDS.has(s);
12810
13473
  },
12811
- paint: (node, pose) => {
13474
+ // Memoized: a pure function of `(data, pose)`, and `pathForShape` builds a
13475
+ // fresh `Uint8Array` + `Float32Array` for every ellipse, polygon and star.
13476
+ // See PAINT_SLOT.
13477
+ paint: (node, pose) => nodeMemo(node, PAINT_SLOT, pose, () => {
12812
13478
  const d = node.data;
12813
13479
  const path = pathForShape(d, pose);
12814
13480
  return [{
@@ -12817,10 +13483,17 @@ var SHAPE_PAINTER = {
12817
13483
  fill: { color: d.fill ?? d.color ?? "#888" },
12818
13484
  ...d.stroke && (d.strokeWidth ?? 0) > 0 ? { stroke: { paint: { color: d.stroke }, width: d.strokeWidth ?? 1 } } : {}
12819
13485
  }];
12820
- },
13486
+ }),
12821
13487
  silhouette: (node, pose) => {
12822
13488
  const d = node.data;
12823
13489
  return pathForShape(d, pose);
13490
+ },
13491
+ // `paint` above always emits a fill (defaulting to '#888'), so this shape
13492
+ // family is always filled — unlike `kit:path`, it has no stroke-only form.
13493
+ ink: (node) => {
13494
+ const d = node.data;
13495
+ const hasStroke = !!d.stroke && (d.strokeWidth ?? 0) > 0;
13496
+ return { filled: true, strokeWidth: hasStroke ? d.strokeWidth ?? 1 : 0 };
12824
13497
  }
12825
13498
  };
12826
13499
  var SHAPE_KINDS = /* @__PURE__ */ new Set(["rect", "ellipse", "polygon", "star"]);
@@ -12951,15 +13624,29 @@ var MULTI_RESIZE_TARGET_ID = "__weasel:multi-selection";
12951
13624
  // src/tools/builtin/select/useSelectTool.ts
12952
13625
  function useSelectTool(adapter, options) {
12953
13626
  const poseBoundsFn = options.poseBounds ?? ((p) => p);
13627
+ const preciseLeaves = options.leafPicking === "silhouette";
13628
+ const tolerance = options.pickTolerance ?? 0;
12954
13629
  const pickEveryFn = options.pickEvery ?? ((worldX, worldY) => {
12955
13630
  const hier = adapter;
13631
+ const covers = (node, pose, b) => {
13632
+ const t = preciseLeaves ? tolerance : 0;
13633
+ if (!(worldX >= b.x - t && worldX <= b.x + b.width + t && worldY >= b.y - t && worldY <= b.y + b.height + t)) {
13634
+ return false;
13635
+ }
13636
+ if (!preciseLeaves) return true;
13637
+ return shapeCoversPoint(
13638
+ node,
13639
+ pose,
13640
+ worldX,
13641
+ worldY,
13642
+ { tolerance }
13643
+ );
13644
+ };
12956
13645
  if (typeof hier.getChildren !== "function" || typeof hier.getNode !== "function") {
12957
13646
  const out2 = [];
12958
13647
  for (const obj of adapter.getNodes()) {
12959
- const b = poseBoundsFn(adapter.getPose(obj.id));
12960
- if (worldX >= b.x && worldX <= b.x + b.width && worldY >= b.y && worldY <= b.y + b.height) {
12961
- out2.push(obj.id);
12962
- }
13648
+ const pose = adapter.getPose(obj.id);
13649
+ if (covers(obj, pose, poseBoundsFn(pose))) out2.push(obj.id);
12963
13650
  }
12964
13651
  return out2;
12965
13652
  }
@@ -12997,10 +13684,7 @@ function useSelectTool(adapter, options) {
12997
13684
  for (const clip of ancestorClips) {
12998
13685
  if (!pathContainsPoint(clip, worldX, worldY)) continue nextChild;
12999
13686
  }
13000
- const b = poseBoundsFn(pose);
13001
- if (worldX >= b.x && worldX <= b.x + b.width && worldY >= b.y && worldY <= b.y + b.height) {
13002
- out.push(childId);
13003
- }
13687
+ if (covers(node, pose, poseBoundsFn(pose))) out.push(childId);
13004
13688
  }
13005
13689
  }
13006
13690
  }
@@ -13218,6 +13902,101 @@ function useSelectTool(adapter, options) {
13218
13902
  );
13219
13903
  }
13220
13904
 
13905
+ // src/affordances/hitAffordanceRegions.ts
13906
+ function hitAffordanceRegions(affordances, wx, wy, state, view, isVisible) {
13907
+ for (let i = affordances.length - 1; i >= 0; i--) {
13908
+ const a = affordances[i];
13909
+ if (isVisible && !isVisible(a.id)) continue;
13910
+ const regions = a.regions(state);
13911
+ let best = null;
13912
+ for (let j = 0; j < regions.length; j++) {
13913
+ const region = regions[j];
13914
+ const xf = transformOf(state, region.targetId);
13915
+ if (!hitRegion(region, wx, wy, xf, view)) continue;
13916
+ const d2 = regionDistance2(region, wx, wy, xf);
13917
+ if (best === null || d2 <= best.d2) best = { region, d2 };
13918
+ }
13919
+ if (best) {
13920
+ return {
13921
+ affordanceId: a.id,
13922
+ regionId: best.region.id,
13923
+ region: best.region,
13924
+ binding: best.region.bind()
13925
+ };
13926
+ }
13927
+ }
13928
+ return null;
13929
+ }
13930
+ function regionDistance2(region, wx, wy, xf) {
13931
+ const s = region.shape;
13932
+ const local = s.kind === "point" ? { x: s.x, y: s.y } : s.kind === "annulus" ? { x: s.cx, y: s.cy } : { x: s.x + s.width / 2, y: s.y + s.height / 2 };
13933
+ const w = localToWorld(xf, local.x, local.y);
13934
+ const dx = w.x - wx;
13935
+ const dy = w.y - wy;
13936
+ return dx * dx + dy * dy;
13937
+ }
13938
+ var IDENTITY_XF = { cx: 0, cy: 0, cos: 1, sin: 0, identity: true };
13939
+ function transformOf(state, targetId) {
13940
+ if (targetId === null) return IDENTITY_XF;
13941
+ const b = state.boundsOf(targetId);
13942
+ if (!b) return IDENTITY_XF;
13943
+ const r = poseRotationOf(b);
13944
+ if (!r) return IDENTITY_XF;
13945
+ return {
13946
+ cx: r.cx,
13947
+ cy: r.cy,
13948
+ cos: Math.cos(r.rotation),
13949
+ sin: Math.sin(r.rotation),
13950
+ identity: false
13951
+ };
13952
+ }
13953
+ function localToWorld(xf, lx, ly) {
13954
+ if (xf.identity) return { x: lx, y: ly };
13955
+ const dx = lx - xf.cx;
13956
+ const dy = ly - xf.cy;
13957
+ return {
13958
+ x: xf.cx + xf.cos * dx - xf.sin * dy,
13959
+ y: xf.cy + xf.sin * dx + xf.cos * dy
13960
+ };
13961
+ }
13962
+ function worldToLocal(xf, wx, wy) {
13963
+ if (xf.identity) return { x: wx, y: wy };
13964
+ const dx = wx - xf.cx;
13965
+ const dy = wy - xf.cy;
13966
+ return {
13967
+ x: xf.cx + xf.cos * dx + xf.sin * dy,
13968
+ y: xf.cy - xf.sin * dx + xf.cos * dy
13969
+ };
13970
+ }
13971
+ function annulusSemiAxes(shape, view) {
13972
+ const bandPx = shape.minBandPx ?? 0;
13973
+ if (bandPx === 0) return { rx: shape.rx, ry: shape.ry };
13974
+ const band = bandPx / meanScale(view.scale);
13975
+ return {
13976
+ rx: Math.max(shape.rx, shape.innerWidth / 2 + band),
13977
+ ry: Math.max(shape.ry, shape.innerHeight / 2 + band)
13978
+ };
13979
+ }
13980
+ function hitRegion(region, wx, wy, xf, view) {
13981
+ const local = worldToLocal(xf, wx, wy);
13982
+ if (region.shape.kind === "point") {
13983
+ const radiusWorld = region.shape.hitRadiusPx / meanScale(view.scale);
13984
+ return Math.abs(local.x - region.shape.x) <= radiusWorld && Math.abs(local.y - region.shape.y) <= radiusWorld;
13985
+ }
13986
+ if (region.shape.kind === "annulus") {
13987
+ const s = region.shape;
13988
+ const insideInner = local.x >= s.innerX && local.x <= s.innerX + s.innerWidth && local.y >= s.innerY && local.y <= s.innerY + s.innerHeight;
13989
+ if (insideInner) return false;
13990
+ const { rx, ry } = annulusSemiAxes(s, view);
13991
+ if (rx <= 0 || ry <= 0) return false;
13992
+ const ex = (local.x - s.cx) / rx;
13993
+ const ey = (local.y - s.cy) / ry;
13994
+ return ex * ex + ey * ey <= 1;
13995
+ }
13996
+ const r = region.shape;
13997
+ return local.x >= r.x && local.x <= r.x + r.width && local.y >= r.y && local.y <= r.y + r.height;
13998
+ }
13999
+
13221
14000
  // src/affordances/composeAffordanceLayer.ts
13222
14001
  function composeAffordanceLayer(id, label, affordances) {
13223
14002
  return {
@@ -13243,21 +14022,11 @@ function composeAffordanceLayer(id, label, affordances) {
13243
14022
  }
13244
14023
  return out;
13245
14024
  },
13246
- hitTest: (wx, wy, state, view, _dims, isVisible) => {
13247
- for (let i = affordances.length - 1; i >= 0; i--) {
13248
- const a = affordances[i];
13249
- if (isVisible && !isVisible(a.id)) continue;
13250
- const regs = a.regions(state);
13251
- for (let j = regs.length - 1; j >= 0; j--) {
13252
- const region = regs[j];
13253
- const xf = transformOf(state, region.targetId);
13254
- if (hitRegion(region, wx, wy, xf, view)) {
13255
- return region.bind();
13256
- }
13257
- }
13258
- }
13259
- return null;
13260
- }
14025
+ // The walk itself lives in `hitAffordanceRegions` so this layer and
14026
+ // `buildAffordanceAt` share one implementation rather than two that can
14027
+ // drift. This wrapper exists because `RenderLayer.hitTest` wants only the
14028
+ // binding back.
14029
+ hitTest: (wx, wy, state, view, _dims, isVisible) => hitAffordanceRegions(affordances, wx, wy, state, view, isVisible)?.binding ?? null
13261
14030
  };
13262
14031
  }
13263
14032
  function asChromeState(data) {
@@ -13291,11 +14060,12 @@ function recordRegionHitbox(debug, affordanceId, region, xf, view) {
13291
14060
  }
13292
14061
  if (region.shape.kind === "annulus") {
13293
14062
  const s = region.shape;
14063
+ const { rx, ry } = annulusSemiAxes(s, view);
13294
14064
  const pts = [
13295
- localToWorld(xf, s.cx + s.rx, s.cy),
13296
- localToWorld(xf, s.cx - s.rx, s.cy),
13297
- localToWorld(xf, s.cx, s.cy + s.ry),
13298
- localToWorld(xf, s.cx, s.cy - s.ry)
14065
+ localToWorld(xf, s.cx + rx, s.cy),
14066
+ localToWorld(xf, s.cx - rx, s.cy),
14067
+ localToWorld(xf, s.cx, s.cy + ry),
14068
+ localToWorld(xf, s.cx, s.cy - ry)
13299
14069
  ];
13300
14070
  const minX = Math.min(...pts.map((p) => p.x));
13301
14071
  const minY = Math.min(...pts.map((p) => p.y));
@@ -13322,39 +14092,6 @@ function recordRegionHitbox(debug, affordanceId, region, xf, view) {
13322
14092
  ...xf.identity ? {} : { rotation: Math.atan2(xf.sin, xf.cos) }
13323
14093
  });
13324
14094
  }
13325
- var IDENTITY_XF = { cx: 0, cy: 0, cos: 1, sin: 0, identity: true };
13326
- function transformOf(state, targetId) {
13327
- if (targetId === null) return IDENTITY_XF;
13328
- const b = state.boundsOf(targetId);
13329
- if (!b) return IDENTITY_XF;
13330
- const r = poseRotationOf(b);
13331
- if (!r) return IDENTITY_XF;
13332
- return {
13333
- cx: r.cx,
13334
- cy: r.cy,
13335
- cos: Math.cos(r.rotation),
13336
- sin: Math.sin(r.rotation),
13337
- identity: false
13338
- };
13339
- }
13340
- function localToWorld(xf, lx, ly) {
13341
- if (xf.identity) return { x: lx, y: ly };
13342
- const dx = lx - xf.cx;
13343
- const dy = ly - xf.cy;
13344
- return {
13345
- x: xf.cx + xf.cos * dx - xf.sin * dy,
13346
- y: xf.cy + xf.sin * dx + xf.cos * dy
13347
- };
13348
- }
13349
- function worldToLocal(xf, wx, wy) {
13350
- if (xf.identity) return { x: wx, y: wy };
13351
- const dx = wx - xf.cx;
13352
- const dy = wy - xf.cy;
13353
- return {
13354
- x: xf.cx + xf.cos * dx + xf.sin * dy,
13355
- y: xf.cy - xf.sin * dx + xf.cos * dy
13356
- };
13357
- }
13358
14095
  function paintRegion(region, xf, view, state, viewT, out) {
13359
14096
  const paint = region.paint;
13360
14097
  if (paint.kind === "square") {
@@ -13377,13 +14114,13 @@ function paintRegion(region, xf, view, state, viewT, out) {
13377
14114
  if (region.shape.kind !== "annulus") return;
13378
14115
  const s = region.shape;
13379
14116
  const insetLocal = (paint.insetPx ?? 0) / meanScale(view.scale);
13380
- const cmd = annulusCommand(s, insetLocal, xf, viewT, paint.fill, paint.stroke);
14117
+ const cmd = annulusCommand(s, annulusSemiAxes(s, view), insetLocal, xf, viewT, paint.fill, paint.stroke);
13381
14118
  if (cmd) out.push(cmd);
13382
14119
  return;
13383
14120
  }
13384
14121
  if (paint.kind === "custom") {
13385
14122
  const ctx = {
13386
- world: worldOf(region, xf),
14123
+ world: worldOf(region, xf, view),
13387
14124
  local: region.shape,
13388
14125
  view,
13389
14126
  state
@@ -13392,9 +14129,9 @@ function paintRegion(region, xf, view, state, viewT, out) {
13392
14129
  return;
13393
14130
  }
13394
14131
  }
13395
- function annulusCommand(shape, insetLocal, xf, viewT, fill, stroke) {
13396
- const rx = shape.rx - insetLocal;
13397
- const ry = shape.ry - insetLocal;
14132
+ function annulusCommand(shape, semiAxes, insetLocal, xf, viewT, fill, stroke) {
14133
+ const rx = semiAxes.rx - insetLocal;
14134
+ const ry = semiAxes.ry - insetLocal;
13398
14135
  if (rx <= 0 || ry <= 0) return null;
13399
14136
  const cx = shape.cx;
13400
14137
  const cy = shape.cy;
@@ -13460,18 +14197,19 @@ function annulusCommand(shape, insetLocal, xf, viewT, fill, stroke) {
13460
14197
  ...stroke ? { stroke } : {}
13461
14198
  };
13462
14199
  }
13463
- function worldOf(region, xf) {
14200
+ function worldOf(region, xf, view) {
13464
14201
  if (region.shape.kind === "point") {
13465
14202
  const w = localToWorld(xf, region.shape.x, region.shape.y);
13466
14203
  return { x: w.x, y: w.y };
13467
14204
  }
13468
14205
  if (region.shape.kind === "annulus") {
13469
14206
  const s = region.shape;
14207
+ const { rx, ry } = annulusSemiAxes(s, view);
13470
14208
  const pts = [
13471
- localToWorld(xf, s.cx + s.rx, s.cy),
13472
- localToWorld(xf, s.cx - s.rx, s.cy),
13473
- localToWorld(xf, s.cx, s.cy + s.ry),
13474
- localToWorld(xf, s.cx, s.cy - s.ry)
14209
+ localToWorld(xf, s.cx + rx, s.cy),
14210
+ localToWorld(xf, s.cx - rx, s.cy),
14211
+ localToWorld(xf, s.cx, s.cy + ry),
14212
+ localToWorld(xf, s.cx, s.cy - ry)
13475
14213
  ];
13476
14214
  const minX2 = Math.min(...pts.map((p) => p.x));
13477
14215
  const minY2 = Math.min(...pts.map((p) => p.y));
@@ -13490,24 +14228,6 @@ function worldOf(region, xf) {
13490
14228
  const maxY = Math.max(a.y, b.y, c.y, d.y);
13491
14229
  return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
13492
14230
  }
13493
- function hitRegion(region, wx, wy, xf, view) {
13494
- const local = worldToLocal(xf, wx, wy);
13495
- if (region.shape.kind === "point") {
13496
- const radiusWorld = region.shape.hitRadiusPx / meanScale(view.scale);
13497
- return Math.abs(local.x - region.shape.x) <= radiusWorld && Math.abs(local.y - region.shape.y) <= radiusWorld;
13498
- }
13499
- if (region.shape.kind === "annulus") {
13500
- const s = region.shape;
13501
- const insideInner = local.x >= s.innerX && local.x <= s.innerX + s.innerWidth && local.y >= s.innerY && local.y <= s.innerY + s.innerHeight;
13502
- if (insideInner) return false;
13503
- if (s.rx <= 0 || s.ry <= 0) return false;
13504
- const ex = (local.x - s.cx) / s.rx;
13505
- const ey = (local.y - s.cy) / s.ry;
13506
- return ex * ex + ey * ey <= 1;
13507
- }
13508
- const r = region.shape;
13509
- return local.x >= r.x && local.x <= r.x + r.width && local.y >= r.y && local.y <= r.y + r.height;
13510
- }
13511
14231
 
13512
14232
  // src/affordances/rotationHandle.ts
13513
14233
  var DEFAULT_PAINT = {
@@ -13538,13 +14258,8 @@ function createRotationAffordance(opts = {}) {
13538
14258
  const target = pickTarget(state);
13539
14259
  if (!target) return [];
13540
14260
  const b = target.bounds;
13541
- const minBand = bandPx;
13542
14261
  const halfW = b.width / 2;
13543
14262
  const halfH = b.height / 2;
13544
- const naturalRx = halfW * Math.SQRT2;
13545
- const naturalRy = halfH * Math.SQRT2;
13546
- const rx = Math.max(naturalRx, halfW + minBand);
13547
- const ry = Math.max(naturalRy, halfH + minBand);
13548
14263
  const region = {
13549
14264
  id: "selection.rotation-handle",
13550
14265
  targetId: target.id,
@@ -13552,17 +14267,24 @@ function createRotationAffordance(opts = {}) {
13552
14267
  kind: "annulus",
13553
14268
  cx: b.x + halfW,
13554
14269
  cy: b.y + halfH,
13555
- rx,
13556
- ry,
14270
+ rx: halfW * Math.SQRT2,
14271
+ ry: halfH * Math.SQRT2,
13557
14272
  innerX: b.x,
13558
14273
  innerY: b.y,
13559
14274
  innerWidth: b.width,
13560
- innerHeight: b.height
14275
+ innerHeight: b.height,
14276
+ minBandPx: bandPx
13561
14277
  },
13562
14278
  ...annulusPaint ? { paint: annulusPaint } : {},
14279
+ hitKind: "rotate-handle",
13563
14280
  cursor,
13564
14281
  bind: () => ({
13565
- initialScratch: { targetId: target.id }
14282
+ initialScratch: {
14283
+ targetId: target.id,
14284
+ // Pivot. `rotateAction` recomputes its own union pivot for
14285
+ // multi-selection, but the single-target case reads this.
14286
+ fixedPoint: { x: b.x + halfW, y: b.y + halfH }
14287
+ }
13566
14288
  })
13567
14289
  };
13568
14290
  return [region];
@@ -13630,8 +14352,7 @@ function useRotateTool(adapter, options) {
13630
14352
  const ghosts = ghostOverlay.draw(data, view, dims);
13631
14353
  const aff = affordanceOverlay.draw(toChromeState(data), view, dims);
13632
14354
  return [...ghosts, ...aff];
13633
- },
13634
- hitTest: (wx, wy, data, view, dims) => affordanceOverlay.hitTest(wx, wy, toChromeState(data), view, dims)
14355
+ }
13635
14356
  }),
13636
14357
  [ghostOverlay, affordanceOverlay]
13637
14358
  );
@@ -14476,12 +15197,12 @@ function defaultDrawOne(node, pose, ctx) {
14476
15197
  const data = node.data;
14477
15198
  if (data?.label && data.text == null) {
14478
15199
  const p = pose;
14479
- primary.push(textCommand(
15200
+ return [...primary, textCommand(
14480
15201
  p.x + 6,
14481
15202
  p.y + 14,
14482
15203
  data.label,
14483
15204
  { fontFamily: "sans-serif", fontSize: 11, fill: { fill: "solid", color: "rgba(0,0,0,0.7)" } }
14484
- ));
15205
+ )];
14485
15206
  }
14486
15207
  return primary;
14487
15208
  }
@@ -17546,28 +18267,32 @@ function aabbOfPose(pose) {
17546
18267
  );
17547
18268
  }
17548
18269
  var DEFAULT_PATH_STROKE_SLOP = 4;
17549
- function poseContains(pose, wx, wy) {
18270
+ function poseContains(pose, wx, wy, tolerance) {
17550
18271
  if (isPathLike(pose)) {
17551
18272
  if (pointInPath(pose, wx, wy)) return true;
17552
- return strokeHitTest(pose, wx, wy, DEFAULT_PATH_STROKE_SLOP);
18273
+ return strokeHitTest(pose, wx, wy, tolerance ?? DEFAULT_PATH_STROKE_SLOP);
17553
18274
  }
17554
18275
  const b = aabbOfPose(pose);
17555
- return wx >= b.x && wx <= b.x + b.width && wy >= b.y && wy <= b.y + b.height;
18276
+ const t = tolerance ?? 0;
18277
+ return wx >= b.x - t && wx <= b.x + b.width + t && wy >= b.y - t && wy <= b.y + b.height + t;
17556
18278
  }
17557
- function poseContainsRotated(pose, wx, wy) {
18279
+ function poseContainsRotated(pose, wx, wy, tolerance) {
17558
18280
  const r = poseRotationOf(pose);
17559
18281
  if (r) {
17560
18282
  const [lx, ly] = applyToPoint(rotateAboutPoint(r.cx, r.cy, -r.rotation), wx, wy);
17561
- return poseContains(pose, lx, ly);
18283
+ return poseContains(pose, lx, ly, tolerance);
17562
18284
  }
17563
- return poseContains(pose, wx, wy);
18285
+ return poseContains(pose, wx, wy, tolerance);
17564
18286
  }
17565
18287
 
17566
18288
  // src/canvas/SceneCanvas/useSceneSelectTool.ts
18289
+ var DEFAULT_PICK_TOLERANCE_PX = 4;
17567
18290
  function useSceneSelectTool(args) {
17568
- const { scene, selection, geometry, selectTool: opts, insertTool, layouts } = args;
18291
+ const { scene, selection, geometry, selectTool: opts, insertTool, layouts, getView } = args;
17569
18292
  const pickEveryProp = geometry?.pickEvery;
17570
18293
  const boundsOfProp = geometry?.boundsOf;
18294
+ const shapePicking = (geometry?.picking ?? "shape") === "shape";
18295
+ const pickTolerancePx = geometry?.pickTolerancePx ?? DEFAULT_PICK_TOLERANCE_PX;
17571
18296
  const moveOptions = opts?.move;
17572
18297
  const rotateOptions = opts?.rotate;
17573
18298
  const snap2 = opts?.snap;
@@ -17658,14 +18383,17 @@ function useSceneSelectTool(args) {
17658
18383
  if (r == null) return [];
17659
18384
  return Array.isArray(r) ? r : [r];
17660
18385
  }
18386
+ const tolerance = pickTolerancePx / meanScale(getView?.()?.scale ?? { x: 1, y: 1 });
17661
18387
  const out = [];
17662
18388
  for (const id of scene.renderOrder()) {
17663
18389
  const n = scene.get(id);
17664
- if (n && poseContainsRotated(n.pose, wx, wy)) out.push(n.id);
18390
+ if (!n || !poseContainsRotated(n.pose, wx, wy, tolerance)) continue;
18391
+ if (shapePicking && !shapeCoversPoint(n, n.pose, wx, wy, { tolerance })) continue;
18392
+ out.push(n.id);
17665
18393
  }
17666
18394
  return out;
17667
18395
  };
17668
- }, [scene, pickEveryProp]);
18396
+ }, [scene, pickEveryProp, shapePicking, pickTolerancePx, getView]);
17669
18397
  const wiredBoundsOf = useMemo(() => {
17670
18398
  return (id) => {
17671
18399
  if (boundsOfProp) return boundsOfProp(id);
@@ -18417,77 +19145,76 @@ function enumerateAnchors(path) {
18417
19145
  };
18418
19146
  anchors.push(a);
18419
19147
  lastAnchor = a;
18420
- ci += 4;
18421
- break;
18422
- }
18423
- case PATH_Z: {
18424
- lastAnchor = null;
18425
- break;
18426
- }
18427
- }
18428
- }
18429
- return anchors;
18430
- }
18431
- function withCoord(path, coordIndex, x, y) {
18432
- const next = new Float32Array(path.coords);
18433
- next[coordIndex] = x;
18434
- next[coordIndex + 1] = y;
18435
- return { kind: "polygon", commands: path.commands, coords: next, fillRule: path.fillRule };
18436
- }
18437
-
18438
- // src/interactions/actions/edit-anchors/handles.ts
18439
- function hitAnchor(path, worldX, worldY, threshold) {
18440
- const anchors = enumerateAnchors(path);
18441
- let bestControl = null;
18442
- let bestAnchor = null;
18443
- const t2 = threshold * threshold;
18444
- for (const a of anchors) {
18445
- if (a.controlIn) {
18446
- const dx2 = a.controlIn.x - worldX;
18447
- const dy2 = a.controlIn.y - worldY;
18448
- const d22 = dx2 * dx2 + dy2 * dy2;
18449
- if (d22 <= t2 && (!bestControl || d22 < bestControl.d2)) {
18450
- bestControl = {
18451
- d2: d22,
18452
- hit: { anchorIndex: a.anchorIndex, kind: "controlIn", coordIndex: a.controlIn.coordIndex }
18453
- };
18454
- }
18455
- }
18456
- if (a.controlOut) {
18457
- const dx2 = a.controlOut.x - worldX;
18458
- const dy2 = a.controlOut.y - worldY;
18459
- const d22 = dx2 * dx2 + dy2 * dy2;
18460
- if (d22 <= t2 && (!bestControl || d22 < bestControl.d2)) {
18461
- bestControl = {
18462
- d2: d22,
18463
- hit: { anchorIndex: a.anchorIndex, kind: "controlOut", coordIndex: a.controlOut.coordIndex }
18464
- };
19148
+ ci += 4;
19149
+ break;
19150
+ }
19151
+ case PATH_Z: {
19152
+ lastAnchor = null;
19153
+ break;
18465
19154
  }
18466
- }
18467
- const dx = a.x - worldX;
18468
- const dy = a.y - worldY;
18469
- const d2 = dx * dx + dy * dy;
18470
- if (d2 <= t2 && (!bestAnchor || d2 < bestAnchor.d2)) {
18471
- bestAnchor = {
18472
- d2,
18473
- hit: { anchorIndex: a.anchorIndex, kind: "anchor", coordIndex: a.coordIndex }
18474
- };
18475
19155
  }
18476
19156
  }
18477
- if (bestControl) return bestControl.hit;
18478
- if (bestAnchor) return bestAnchor.hit;
18479
- return null;
19157
+ return anchors;
19158
+ }
19159
+ function withCoord(path, coordIndex, x, y) {
19160
+ const next = new Float32Array(path.coords);
19161
+ next[coordIndex] = x;
19162
+ next[coordIndex + 1] = y;
19163
+ return { kind: "polygon", commands: path.commands, coords: next, fillRule: path.fillRule };
18480
19164
  }
18481
19165
 
18482
- // src/canvas/affordanceAt.ts
18483
- var HANDLE_HIT_RADIUS = 8;
18484
- var ROTATE_DISTANCE = DEFAULT_ROTATION_HANDLE_DISTANCE;
18485
- function dist22(ax, ay, bx, by) {
18486
- const dx = ax - bx;
18487
- const dy = ay - by;
18488
- return dx * dx + dy * dy;
19166
+ // src/affordances/cornerResize.ts
19167
+ var DEFAULT_FILL3 = "#d4c4a8";
19168
+ var DEFAULT_STROKE = "#1a130d";
19169
+ function createCornerResizeAffordance(opts = {}) {
19170
+ const {
19171
+ handleHitRadius = 8,
19172
+ handleSize = 8,
19173
+ fill = DEFAULT_FILL3,
19174
+ stroke = DEFAULT_STROKE
19175
+ } = opts;
19176
+ const paint = {
19177
+ kind: "square",
19178
+ sizePx: handleSize,
19179
+ fill: { color: fill },
19180
+ stroke: { paint: { color: stroke }, width: 1 }
19181
+ };
19182
+ return {
19183
+ id: "selection.resize-handles",
19184
+ regions(state) {
19185
+ const target = pickRenderTarget(state);
19186
+ if (!target) return [];
19187
+ const b = target.bounds;
19188
+ const xf = transformOf(state, target.id);
19189
+ return CORNER_ANCHORS.map((c) => {
19190
+ const corner = cornerPoint(b, c);
19191
+ const fixedLocal = fixedCornerOf(b, c.anchor);
19192
+ const fixedPoint = localToWorld(xf, fixedLocal.x, fixedLocal.y);
19193
+ return {
19194
+ id: `corner-${c.tag}`,
19195
+ targetId: target.id,
19196
+ shape: { kind: "point", x: corner.x, y: corner.y, hitRadiusPx: handleHitRadius },
19197
+ paint,
19198
+ hitKind: c.kind,
19199
+ // Diagonal by fixed-corner parity: a matched-axis anchor (min-min /
19200
+ // max-max fixed) means the dragged corner sits on the ↘ diagonal;
19201
+ // mixed axes sit on the ↗ diagonal. Not rotation-aware — a rotated
19202
+ // target keeps the unrotated hint, the same policy as every
19203
+ // mainstream editor short of Figma.
19204
+ cursor: c.anchor.x === c.anchor.y ? "nwse-resize" : "nesw-resize",
19205
+ bind: () => ({
19206
+ initialScratch: {
19207
+ anchor: c.anchor,
19208
+ targetId: target.id,
19209
+ fixedPoint
19210
+ }
19211
+ })
19212
+ };
19213
+ });
19214
+ }
19215
+ };
18489
19216
  }
18490
- function pickResizeTarget(state) {
19217
+ function pickRenderTarget(state) {
18491
19218
  if (state.multiActive && state.unionBounds) {
18492
19219
  return { id: MULTI_RESIZE_TARGET_ID, bounds: state.unionBounds };
18493
19220
  }
@@ -18498,110 +19225,130 @@ function pickResizeTarget(state) {
18498
19225
  }
18499
19226
  return null;
18500
19227
  }
18501
- function cornersFor(b) {
18502
- const r = poseRotationOf(b);
18503
- const place = (px, py) => r ? rotatePoint(px, py, r.cx, r.cy, r.rotation) : { x: px, y: py };
18504
- return CORNER_ANCHORS.map((c) => {
18505
- const corner = cornerPoint(b, c);
18506
- const fixed = fixedCornerOf(b, c.anchor);
18507
- const wp = place(corner.x, corner.y);
18508
- const fp = place(fixed.x, fixed.y);
18509
- return { worldX: wp.x, worldY: wp.y, kind: c.kind, anchor: c.anchor, fixedX: fp.x, fixedY: fp.y };
18510
- });
18511
- }
18512
- function buildAffordanceAt(getChromeState, hitRadius = HANDLE_HIT_RADIUS, rotateDistance = ROTATE_DISTANCE, getAnchorState, getIsVisible) {
18513
- return function affordanceAt({ x: wx, y: wy }) {
18514
- const state = getChromeState();
18515
- const isVisible = getIsVisible?.() ?? (() => true);
18516
- const hr = typeof hitRadius === "function" ? hitRadius() : hitRadius;
18517
- const rd = typeof rotateDistance === "function" ? rotateDistance() : rotateDistance;
18518
- const r2 = hr * hr;
18519
- if (isVisible("selection.resize-handles")) {
18520
- const resizeTarget = pickResizeTarget(state);
18521
- if (resizeTarget) {
18522
- for (const c of cornersFor(resizeTarget.bounds)) {
18523
- if (dist22(wx, wy, c.worldX, c.worldY) <= r2) {
18524
- return {
18525
- kind: c.kind,
18526
- fixedPoint: { x: c.fixedX, y: c.fixedY },
18527
- targetIds: [resizeTarget.id],
18528
- anchor: c.anchor,
18529
- // Diagonal by fixed-corner parity: a matched-axis anchor
18530
- // (min-min / max-max fixed) means the dragged corner sits on
18531
- // the ↘ diagonal; mixed axes sit on the ↗ diagonal. Cursor is
18532
- // not rotation-aware — a rotated target keeps the unrotated
18533
- // hint (same policy as every mainstream editor short of Figma).
18534
- cursor: c.anchor.x === c.anchor.y ? "nwse-resize" : "nesw-resize"
18535
- };
18536
- }
18537
- }
18538
- }
19228
+
19229
+ // src/affordances/pathAnchors.ts
19230
+ var PATH_ANCHOR_CHROME_ID = "path-edit.anchors";
19231
+ function createPathAnchorAffordances(getAnchorState, opts = {}) {
19232
+ const { hitRadius = 8, cursor = "pointer" } = opts;
19233
+ const editablePaths = (state) => {
19234
+ const anchorState = getAnchorState();
19235
+ if (!anchorState) return [];
19236
+ const out = [];
19237
+ for (const id of state.selection) {
19238
+ const pose = anchorState.getPose(id);
19239
+ if (!pose || pose.kind !== "polygon") continue;
19240
+ out.push({ id, path: pose });
18539
19241
  }
18540
- if (isVisible("selection.rotation-handle")) {
18541
- const rotateTarget = pickResizeTarget(state);
18542
- if (rotateTarget) {
18543
- const { x: bx, y: by, width: bw, height: bh } = rotateTarget.bounds;
18544
- const halfW = bw / 2;
18545
- const halfH = bh / 2;
18546
- const centerX = bx + halfW;
18547
- const centerY = by + halfH;
18548
- const r = poseRotationOf(rotateTarget.bounds);
18549
- const lp = r ? rotatePoint(wx, wy, centerX, centerY, -r.rotation) : { x: wx, y: wy };
18550
- const insideAabb = lp.x >= bx && lp.x <= bx + bw && lp.y >= by && lp.y <= by + bh;
18551
- if (!insideAabb) {
18552
- const rx = Math.max(halfW * Math.SQRT2, halfW + rd);
18553
- const ry = Math.max(halfH * Math.SQRT2, halfH + rd);
18554
- if (rx > 0 && ry > 0) {
18555
- const ex = (lp.x - centerX) / rx;
18556
- const ey = (lp.y - centerY) / ry;
18557
- if (ex * ex + ey * ey <= 1) {
18558
- return {
18559
- kind: "rotate-handle",
18560
- fixedPoint: { x: centerX, y: centerY },
18561
- // pivot
18562
- targetIds: [rotateTarget.id],
18563
- cursor: "grab"
18564
- };
18565
- }
18566
- }
19242
+ return out;
19243
+ };
19244
+ const point = (id, targetId, x, y, scratch, hitKind) => ({
19245
+ id,
19246
+ // Anchor coordinates are already in the path's own space, which for an
19247
+ // editable path is world space — the pose rotation that `targetId` would
19248
+ // introduce is baked into the coords. Anchoring to `null` keeps the
19249
+ // framework from applying it twice.
19250
+ targetId: null,
19251
+ shape: { kind: "point", x, y, hitRadiusPx: hitRadius },
19252
+ hitKind,
19253
+ cursor,
19254
+ bind: () => ({ initialScratch: { ...scratch, targetId } })
19255
+ });
19256
+ const anchors = {
19257
+ id: PATH_ANCHOR_CHROME_ID,
19258
+ regions(state) {
19259
+ const out = [];
19260
+ for (const { id, path } of editablePaths(state)) {
19261
+ for (const a of enumerateAnchors(path)) {
19262
+ out.push(point(
19263
+ `${id}:anchor:${a.anchorIndex}`,
19264
+ id,
19265
+ a.x,
19266
+ a.y,
19267
+ { targetId: id, anchorIndex: a.anchorIndex, part: "anchor" },
19268
+ `anchor:${a.anchorIndex}`
19269
+ ));
18567
19270
  }
18568
19271
  }
19272
+ return out;
18569
19273
  }
18570
- if (isVisible("path-edit.anchors") && getAnchorState) {
19274
+ };
19275
+ const controls = {
19276
+ id: PATH_ANCHOR_CHROME_ID,
19277
+ regions(state) {
18571
19278
  const anchorState = getAnchorState();
18572
- if (anchorState) {
18573
- const { editingId } = anchorState;
18574
- for (const id of state.selection) {
18575
- const pose = anchorState.getPose(id);
18576
- if (!pose || pose.kind !== "polygon") continue;
18577
- const polygon = pose;
18578
- const inEditMode = editingId === id;
18579
- if (inEditMode) {
18580
- const hit = hitAnchor(polygon, wx, wy, hr);
18581
- if (hit) {
18582
- const kindStr = hit.kind === "anchor" ? `anchor:${hit.anchorIndex}` : hit.kind === "controlIn" ? `controlIn:${hit.anchorIndex}` : `controlOut:${hit.anchorIndex}`;
18583
- return { kind: kindStr, targetIds: [id] };
18584
- }
18585
- } else {
18586
- const anchors = enumerateAnchors(polygon);
18587
- const r2anchor = hr * hr;
18588
- let best = null;
18589
- for (const a of anchors) {
18590
- const dx = a.x - wx;
18591
- const dy = a.y - wy;
18592
- const d2 = dx * dx + dy * dy;
18593
- if (d2 <= r2anchor && (!best || d2 < best.d2)) {
18594
- best = { d2, anchorIndex: a.anchorIndex };
18595
- }
18596
- }
18597
- if (best !== null) {
18598
- return { kind: `anchor:${best.anchorIndex}`, targetIds: [id] };
18599
- }
19279
+ const editingId = anchorState?.editingId ?? null;
19280
+ if (editingId === null) return [];
19281
+ const out = [];
19282
+ for (const { id, path } of editablePaths(state)) {
19283
+ if (id !== editingId) continue;
19284
+ for (const a of enumerateAnchors(path)) {
19285
+ if (a.controlIn) {
19286
+ out.push(point(
19287
+ `${id}:controlIn:${a.anchorIndex}`,
19288
+ id,
19289
+ a.controlIn.x,
19290
+ a.controlIn.y,
19291
+ { targetId: id, anchorIndex: a.anchorIndex, part: "controlIn" },
19292
+ `controlIn:${a.anchorIndex}`
19293
+ ));
19294
+ }
19295
+ if (a.controlOut) {
19296
+ out.push(point(
19297
+ `${id}:controlOut:${a.anchorIndex}`,
19298
+ id,
19299
+ a.controlOut.x,
19300
+ a.controlOut.y,
19301
+ { targetId: id, anchorIndex: a.anchorIndex, part: "controlOut" },
19302
+ `controlOut:${a.anchorIndex}`
19303
+ ));
18600
19304
  }
18601
19305
  }
18602
19306
  }
19307
+ return out;
18603
19308
  }
18604
- return null;
19309
+ };
19310
+ return [anchors, controls];
19311
+ }
19312
+
19313
+ // src/canvas/affordanceAt.ts
19314
+ var HANDLE_HIT_RADIUS = 8;
19315
+ var ANCHOR_HIT_RADIUS = 8;
19316
+ function buildAffordanceAt(opts) {
19317
+ const {
19318
+ getChromeState,
19319
+ getView,
19320
+ handleHitRadius = HANDLE_HIT_RADIUS,
19321
+ anchorHitRadius = ANCHOR_HIT_RADIUS,
19322
+ rotateBandPx = DEFAULT_ROTATION_HANDLE_DISTANCE,
19323
+ getAnchorState,
19324
+ getIsVisible
19325
+ } = opts;
19326
+ const affordances = [
19327
+ createRotationAffordance({ bandPx: rotateBandPx, paint: null }),
19328
+ createCornerResizeAffordance({ handleHitRadius }),
19329
+ ...getAnchorState ? createPathAnchorAffordances(getAnchorState, { hitRadius: anchorHitRadius }) : []
19330
+ ];
19331
+ return function affordanceAt({ x: wx, y: wy }) {
19332
+ const hit = hitAffordanceRegions(
19333
+ affordances,
19334
+ wx,
19335
+ wy,
19336
+ getChromeState(),
19337
+ getView(),
19338
+ getIsVisible?.()
19339
+ );
19340
+ return hit ? toAffordanceHit(hit) : null;
19341
+ };
19342
+ }
19343
+ function toAffordanceHit(hit) {
19344
+ const scratch = hit.binding.initialScratch ?? {};
19345
+ return {
19346
+ kind: hit.region.hitKind ?? `${hit.affordanceId}:${hit.regionId}`,
19347
+ ...scratch.targetId !== void 0 ? { targetIds: [scratch.targetId] } : {},
19348
+ ...scratch.anchor !== void 0 ? { anchor: scratch.anchor } : {},
19349
+ ...scratch.fixedPoint !== void 0 ? { fixedPoint: scratch.fixedPoint } : {},
19350
+ ...hit.region.cursor !== void 0 ? { cursor: hit.region.cursor } : {},
19351
+ ...hit.binding.initialScratch !== void 0 ? { payload: hit.binding.initialScratch } : {}
18605
19352
  };
18606
19353
  }
18607
19354
  function buildClassifyTarget(getSelection, pickBest, kindOfNode) {
@@ -19881,7 +20628,10 @@ function mergeLayersWithDefaults(user) {
19881
20628
  }
19882
20629
  var BUNDLE_TOOLS = {
19883
20630
  minimal: ["select", "hand"],
19884
- standard: ["select", "rotate", "hand", "rect", "ellipse", "line", "pencil"],
20631
+ // No `pencil`: freehand is a specialist instrument, not part of the
20632
+ // everyday shape-drawing set. It stays in `exhaustive`, which means
20633
+ // everything.
20634
+ standard: ["select", "rotate", "hand", "rect", "ellipse", "line"],
19885
20635
  exhaustive: [
19886
20636
  "select",
19887
20637
  "rotate",
@@ -20094,6 +20844,8 @@ function SceneCanvasInner(props, ref) {
20094
20844
  scene,
20095
20845
  selection,
20096
20846
  geometry,
20847
+ // The pick tolerance is declared in screen pixels; this is what converts it.
20848
+ getView: () => currentViewRef.current,
20097
20849
  selectTool: selectToolWithDefaults,
20098
20850
  ...insertTool ? { insertTool } : {},
20099
20851
  ...layouts ? { layouts } : {}
@@ -20701,8 +21453,8 @@ function GestureDispatcherMounter({
20701
21453
  }, []);
20702
21454
  const affordanceAt = useMemo(() => {
20703
21455
  if (!selectionRef || !boundsOf2 || !viewRef) return void 0;
20704
- return buildAffordanceAt(
20705
- () => {
21456
+ return buildAffordanceAt({
21457
+ getChromeState: () => {
20706
21458
  const sel = selectionRef.current;
20707
21459
  const selection = sel?.current ?? [];
20708
21460
  const multiActive2 = selection.length >= 2;
@@ -20728,21 +21480,18 @@ function GestureDispatcherMounter({
20728
21480
  }
20729
21481
  };
20730
21482
  },
20731
- // Hit radius / rotate-band distance in WORLD units, derived from the
20732
- // screen-px visual size divided by current view scale. Without this
20733
- // the world-unit radius shrinks below the screen-px handle at zoom > 1
20734
- // and clicks on the handle slip past affordance hit-test fall through
20735
- // to body classification → move binding fires instead of resize.
20736
- () => HANDLE_HIT_RADIUS / meanScale(viewRef.current?.scale ?? { x: 1, y: 1 }),
20737
- () => DEFAULT_ROTATION_HANDLE_DISTANCE / meanScale(viewRef.current?.scale ?? { x: 1, y: 1 }),
21483
+ // Radii are declared in screen pixels and converted against this view.
21484
+ // The caller used to do that division itself (`8 / meanScale(scale)`),
21485
+ // in two places, with a comment explaining what breaks if you forget.
21486
+ getView: () => viewRef.current ?? { x: 0, y: 0, scale: { x: 1, y: 1 } },
20738
21487
  getAnchorState,
20739
21488
  // Chrome-caps resolver: keep the affordance hit-test in sync with what
20740
21489
  // the renderer is actually painting. Without this, a click on a (no
20741
21490
  // longer visible) resize handle position still classifies as a resize
20742
21491
  // handle — e.g. an anchor drag in path-edit mode resizes the path's
20743
21492
  // bounding box instead of moving the anchor.
20744
- getIsVisibleForCanvas ? () => getIsVisibleForCanvas() : void 0
20745
- );
21493
+ ...getIsVisibleForCanvas ? { getIsVisible: () => getIsVisibleForCanvas() } : {}
21494
+ });
20746
21495
  }, [selectionRef, boundsOf2, viewRef, getAnchorState, getIsVisibleForCanvas]);
20747
21496
  const classifyTarget = useMemo(() => {
20748
21497
  if (!selectionRef || !pickEvery || !viewRef) return void 0;
@@ -21502,166 +22251,39 @@ function createChildrenLayer(opts) {
21502
22251
  };
21503
22252
  }
21504
22253
 
21505
- // src/affordances/cornerResize.ts
21506
- var DEFAULT_FILL3 = "#d4c4a8";
21507
- var DEFAULT_STROKE = "#1a130d";
21508
- function createCornerResizeAffordance(opts = {}) {
21509
- const {
21510
- handleHitRadius = 8,
21511
- handleSize = 8,
21512
- fill = DEFAULT_FILL3,
21513
- stroke = DEFAULT_STROKE
21514
- } = opts;
21515
- const paint = {
21516
- kind: "square",
21517
- sizePx: handleSize,
21518
- fill: { color: fill },
21519
- stroke: { paint: { color: stroke }, width: 1 }
21520
- };
21521
- return {
21522
- id: "selection.resize-handles",
21523
- regions(state) {
21524
- const target = pickRenderTarget(state);
21525
- if (!target) return [];
21526
- const b = target.bounds;
21527
- const corners = CORNER_ANCHORS.map((c) => {
21528
- const p = cornerPoint(b, c);
21529
- return { lx: p.x, ly: p.y, anchor: c.anchor, tag: c.tag };
21530
- });
21531
- return corners.map(({ lx, ly, anchor, tag }) => ({
21532
- id: `corner-${tag}`,
21533
- targetId: target.id,
21534
- shape: { kind: "point", x: lx, y: ly, hitRadiusPx: handleHitRadius },
21535
- paint,
21536
- bind: () => ({
21537
- initialScratch: { anchor, targetId: target.id }
21538
- })
21539
- }));
21540
- }
21541
- };
21542
- }
21543
- function pickRenderTarget(state) {
21544
- if (state.multiActive && state.unionBounds) {
21545
- return { id: MULTI_RESIZE_TARGET_ID, bounds: state.unionBounds };
21546
- }
21547
- if (state.selection.length === 1) {
21548
- const id = state.selection[0];
21549
- const b = state.boundsOf(id);
21550
- return b ? { id, bounds: b } : null;
21551
- }
21552
- return null;
21553
- }
21554
-
21555
22254
  // src/features/text/renderLabel.ts
21556
22255
  function renderLabel(ctx, text, x, y, options = {}) {
21557
22256
  const {
21558
22257
  align = "center",
21559
22258
  fontSize = 13,
21560
22259
  renderText,
21561
- width,
21562
- height,
21563
- padX = 4,
21564
- padY = 1,
21565
- cornerRadius = 3
21566
- } = options;
21567
- ctx.save();
21568
- ctx.font = `${fontSize}px sans-serif`;
21569
- ctx.textBaseline = "top";
21570
- const contentW = width ?? ctx.measureText(text).width;
21571
- const contentH = height ?? fontSize;
21572
- const w = contentW + padX * 2;
21573
- const h = contentH + padY * 2;
21574
- const rx = align === "center" ? x - w / 2 : x - padX;
21575
- ctx.fillStyle = "rgba(0, 0, 0, 0.75)";
21576
- ctx.beginPath();
21577
- ctx.roundRect(rx, y - padY, w, h, cornerRadius);
21578
- ctx.fill();
21579
- ctx.textAlign = align === "center" ? "center" : "left";
21580
- const tx = align === "center" ? x : x;
21581
- (renderText ?? defaultLabelTextRenderer)(ctx, text, tx, y);
21582
- ctx.restore();
21583
- }
21584
- var defaultLabelTextRenderer = (ctx, text, x, y) => {
21585
- ctx.fillStyle = "#FFFFFF";
21586
- ctx.fillText(text, x, y);
21587
- };
21588
-
21589
- // src/features/text/runs.ts
21590
- function toRuns(input) {
21591
- if (typeof input === "string") {
21592
- return input.length === 0 ? [] : [{ text: input }];
21593
- }
21594
- for (let i = 0; i < input.length; i++) {
21595
- const r = input[i];
21596
- if (typeof r?.text !== "string") {
21597
- throw new Error(`toRuns: run at index ${i} is missing string \`text\``);
21598
- }
21599
- }
21600
- return input;
21601
- }
21602
- function runsToPlainText(runs) {
21603
- let out = "";
21604
- for (const r of runs) out += r.text;
21605
- return out;
21606
- }
21607
- function escapeMarkdown(s) {
21608
- return s.replace(/\\/g, "\\\\").replace(/\*/g, "\\*");
21609
- }
21610
- function runsToMarkdown(runs) {
21611
- let out = "";
21612
- for (const r of runs) {
21613
- const escaped = escapeMarkdown(r.text);
21614
- if (r.bold && r.italic) out += `***${escaped}***`;
21615
- else if (r.bold) out += `**${escaped}**`;
21616
- else if (r.italic) out += `*${escaped}*`;
21617
- else out += escaped;
21618
- }
21619
- return out;
21620
- }
21621
- function markdownToRuns(input) {
21622
- const runs = [];
21623
- let bold = false;
21624
- let italic = false;
21625
- let buf = "";
21626
- let i = 0;
21627
- function flush() {
21628
- if (buf.length === 0) return;
21629
- const run = { text: buf };
21630
- if (bold) run.bold = true;
21631
- if (italic) run.italic = true;
21632
- runs.push(run);
21633
- buf = "";
21634
- }
21635
- while (i < input.length) {
21636
- const ch = input[i];
21637
- if (ch === "\\" && i + 1 < input.length && "*\\".includes(input[i + 1])) {
21638
- buf += input[i + 1];
21639
- i += 2;
21640
- continue;
21641
- }
21642
- if (ch === "*") {
21643
- let count = 0;
21644
- while (i + count < input.length && input[i + count] === "*") count++;
21645
- flush();
21646
- if (count >= 3) {
21647
- bold = !bold;
21648
- italic = !italic;
21649
- i += 3;
21650
- } else if (count === 2) {
21651
- bold = !bold;
21652
- i += 2;
21653
- } else {
21654
- italic = !italic;
21655
- i += 1;
21656
- }
21657
- continue;
21658
- }
21659
- buf += ch;
21660
- i++;
21661
- }
21662
- flush();
21663
- return runs;
22260
+ width,
22261
+ height,
22262
+ padX = 4,
22263
+ padY = 1,
22264
+ cornerRadius = 3
22265
+ } = options;
22266
+ ctx.save();
22267
+ ctx.font = `${fontSize}px sans-serif`;
22268
+ ctx.textBaseline = "top";
22269
+ const contentW = width ?? ctx.measureText(text).width;
22270
+ const contentH = height ?? fontSize;
22271
+ const w = contentW + padX * 2;
22272
+ const h = contentH + padY * 2;
22273
+ const rx = align === "center" ? x - w / 2 : x - padX;
22274
+ ctx.fillStyle = "rgba(0, 0, 0, 0.75)";
22275
+ ctx.beginPath();
22276
+ ctx.roundRect(rx, y - padY, w, h, cornerRadius);
22277
+ ctx.fill();
22278
+ ctx.textAlign = align === "center" ? "center" : "left";
22279
+ const tx = align === "center" ? x : x;
22280
+ (renderText ?? defaultLabelTextRenderer)(ctx, text, tx, y);
22281
+ ctx.restore();
21664
22282
  }
22283
+ var defaultLabelTextRenderer = (ctx, text, x, y) => {
22284
+ ctx.fillStyle = "#FFFFFF";
22285
+ ctx.fillText(text, x, y);
22286
+ };
21665
22287
 
21666
22288
  // src/features/text/markdownText.ts
21667
22289
  function layoutMarkdown(runs, maxWidth, fontSize, measure3, lineHeightFactor = 1.3) {
@@ -22801,502 +23423,240 @@ function nestedHitTester(adapter, opts) {
22801
23423
  if (wx >= b.x && wx <= b.x + b.width && wy >= b.y && wy <= b.y + b.height) {
22802
23424
  return o.id;
22803
23425
  }
22804
- }
22805
- return null;
22806
- };
22807
- const pickOutermost = (wx, wy) => {
22808
- const leaf = hitLeaf(wx, wy);
22809
- return leaf === null ? null : ancestorChain(leaf)[0];
22810
- };
22811
- const pickBest = (wx, wy, alt, selection) => {
22812
- const leaf = hitLeaf(wx, wy);
22813
- if (leaf === null) return null;
22814
- const chain = ancestorChain(leaf);
22815
- if (!alt) return chain[0];
22816
- for (let i = chain.length - 1; i >= 0; i--) {
22817
- if (selection.includes(chain[i])) {
22818
- return chain[Math.min(i + 1, chain.length - 1)];
22819
- }
22820
- }
22821
- return chain[chain.length - 1];
22822
- };
22823
- return { pickOutermost, pickBest };
22824
- }
22825
-
22826
- // src/features/paths/pathDistance.ts
22827
- var BEZIER_SAMPLES = 16;
22828
- function pointSegDist2(px, py, ax, ay, bx, by) {
22829
- const dx = bx - ax, dy = by - ay;
22830
- const len2 = dx * dx + dy * dy;
22831
- if (len2 === 0) {
22832
- const ex2 = px - ax, ey2 = py - ay;
22833
- return ex2 * ex2 + ey2 * ey2;
22834
- }
22835
- let t = ((px - ax) * dx + (py - ay) * dy) / len2;
22836
- if (t < 0) t = 0;
22837
- else if (t > 1) t = 1;
22838
- const cx = ax + dx * t, cy = ay + dy * t;
22839
- const ex = px - cx, ey = py - cy;
22840
- return ex * ex + ey * ey;
22841
- }
22842
- function pointCubicDist2(px, py, x0, y0, x1, y1, x2, y2, x3, y3) {
22843
- let prevX = x0, prevY = y0;
22844
- let best = Infinity;
22845
- for (let i = 1; i <= BEZIER_SAMPLES; i++) {
22846
- const t = i / BEZIER_SAMPLES;
22847
- const u = 1 - t;
22848
- const sx = u * u * u * x0 + 3 * u * u * t * x1 + 3 * u * t * t * x2 + t * t * t * x3;
22849
- const sy = u * u * u * y0 + 3 * u * u * t * y1 + 3 * u * t * t * y2 + t * t * t * y3;
22850
- const d2 = pointSegDist2(px, py, prevX, prevY, sx, sy);
22851
- if (d2 < best) best = d2;
22852
- prevX = sx;
22853
- prevY = sy;
22854
- }
22855
- return best;
22856
- }
22857
- function pointToRectPerimeterDist(px, py, rx, ry, rw, rh) {
22858
- const dx = Math.max(rx - px, px - (rx + rw), 0);
22859
- const dy = Math.max(ry - py, py - (ry + rh), 0);
22860
- if (dx > 0 || dy > 0) return Math.hypot(dx, dy);
22861
- const left = px - rx, right = rx + rw - px;
22862
- const top = py - ry, bottom = ry + rh - py;
22863
- return Math.min(left, right, top, bottom);
22864
- }
22865
- function pathDistanceToPoint(path, px, py) {
22866
- if (path.kind === "rect") {
22867
- return pointToRectPerimeterDist(px, py, path.x, path.y, path.width, path.height);
22868
- }
22869
- return polygonPathDistance(path, px, py);
22870
- }
22871
- function polygonPathDistance(path, px, py) {
22872
- const { commands, coords } = path;
22873
- let ci = 0;
22874
- let startX = 0, startY = 0;
22875
- let curX = 0, curY = 0;
22876
- let best = Infinity;
22877
- for (let i = 0; i < commands.length; i++) {
22878
- const cmd = commands[i];
22879
- if (cmd === PATH_M) {
22880
- startX = coords[ci];
22881
- startY = coords[ci + 1];
22882
- curX = startX;
22883
- curY = startY;
22884
- ci += 2;
22885
- } else if (cmd === PATH_L) {
22886
- const x = coords[ci], y = coords[ci + 1];
22887
- const d2 = pointSegDist2(px, py, curX, curY, x, y);
22888
- if (d2 < best) best = d2;
22889
- curX = x;
22890
- curY = y;
22891
- ci += 2;
22892
- } else if (cmd === PATH_C) {
22893
- const c1x = coords[ci], c1y = coords[ci + 1];
22894
- const c2x = coords[ci + 2], c2y = coords[ci + 3];
22895
- const x = coords[ci + 4], y = coords[ci + 5];
22896
- const d2 = pointCubicDist2(px, py, curX, curY, c1x, c1y, c2x, c2y, x, y);
22897
- if (d2 < best) best = d2;
22898
- curX = x;
22899
- curY = y;
22900
- ci += 6;
22901
- } else if (cmd === PATH_Q) {
22902
- const qx = coords[ci], qy = coords[ci + 1];
22903
- const x = coords[ci + 2], y = coords[ci + 3];
22904
- const c1x = curX + 2 / 3 * (qx - curX);
22905
- const c1y = curY + 2 / 3 * (qy - curY);
22906
- const c2x = x + 2 / 3 * (qx - x);
22907
- const c2y = y + 2 / 3 * (qy - y);
22908
- const d2 = pointCubicDist2(px, py, curX, curY, c1x, c1y, c2x, c2y, x, y);
22909
- if (d2 < best) best = d2;
22910
- curX = x;
22911
- curY = y;
22912
- ci += 4;
22913
- } else if (cmd === PATH_Z) {
22914
- const d2 = pointSegDist2(px, py, curX, curY, startX, startY);
22915
- if (d2 < best) best = d2;
22916
- curX = startX;
22917
- curY = startY;
22918
- }
22919
- }
22920
- return best === Infinity ? Infinity : Math.sqrt(best);
22921
- }
22922
-
22923
- // src/features/paths/pathLayer.ts
22924
- var PLACEHOLDER_FILL = { color: "#ffffff" };
22925
- var PLACEHOLDER_STROKE = { paint: { color: "#ffffff" }, width: 1 };
22926
- function resolveOverride(base, override, tMs) {
22927
- if (!override) return base;
22928
- if (typeof override === "function") {
22929
- if (!base) return base;
22930
- const result = override(base, tMs);
22931
- if (result.length !== base.length) return base;
22932
- return result;
22933
- }
22934
- return override;
22935
- }
22936
- function createPathLayer(opts) {
22937
- const {
22938
- id = "paths",
22939
- label = "Paths",
22940
- getNodes,
22941
- getPath,
22942
- getFill,
22943
- getStroke,
22944
- isHidden,
22945
- getVertexColors,
22946
- getStrokeVertexColors,
22947
- getStrokeVertexWidths,
22948
- colorOverrides,
22949
- now = () => typeof performance !== "undefined" ? performance.now() : Date.now()
22950
- } = opts;
22951
- const warned = /* @__PURE__ */ new Set();
22952
- const isDev3 = typeof import.meta !== "undefined" && import.meta.env && import.meta.env.DEV;
22953
- return {
22954
- id,
22955
- label,
22956
- draw: () => {
22957
- const children = [];
22958
- const nodes = getNodes();
22959
- for (let idx = 0; idx < nodes.length; idx++) {
22960
- const node = nodes[idx];
22961
- if (isHidden?.(node)) continue;
22962
- const path = getPath(node);
22963
- const fillFromHook = getFill?.(node);
22964
- const strokeFromHook = getStroke?.(node);
22965
- const baseVColors = getVertexColors?.(node);
22966
- const baseStrokeVColors = getStrokeVertexColors?.(node);
22967
- const strokeVWidths = getStrokeVertexWidths?.(node);
22968
- const nodeId = node.id ?? String(idx);
22969
- const tMs = colorOverrides ? now() : 0;
22970
- const fillOverride = colorOverrides?.get(nodeId, "fill");
22971
- const strokeOverride = colorOverrides?.get(nodeId, "stroke");
22972
- const vColors = resolveOverride(baseVColors, fillOverride, tMs);
22973
- const strokeVColors = resolveOverride(baseStrokeVColors, strokeOverride, tMs);
22974
- const nodeKey = nodeId;
22975
- const anchorCount2 = countPathAnchors(path);
22976
- const expectedLen = 4 * anchorCount2;
22977
- let useVColors = null;
22978
- if (vColors != null) {
22979
- if (vColors.length === expectedLen) {
22980
- useVColors = vColors;
22981
- } else if (isDev3) {
22982
- const key = `${id}:${nodeKey}:fill`;
22983
- if (!warned.has(key)) {
22984
- warned.add(key);
22985
- console.warn(
22986
- `[createPathLayer ${id}] node ${nodeKey}: fill vertexColors length ${vColors.length}, expected ${expectedLen}; dropping`
22987
- );
22988
- }
22989
- }
22990
- }
22991
- let useStrokeVColors = null;
22992
- if (strokeVColors != null) {
22993
- if (strokeVColors.length === expectedLen) {
22994
- useStrokeVColors = strokeVColors;
22995
- } else if (isDev3) {
22996
- const key = `${id}:${nodeKey}:stroke`;
22997
- if (!warned.has(key)) {
22998
- warned.add(key);
22999
- console.warn(
23000
- `[createPathLayer ${id}] node ${nodeKey}: stroke vertexColors length ${strokeVColors.length}, expected ${expectedLen}; dropping`
23001
- );
23002
- }
23003
- }
23004
- }
23005
- const fill = fillFromHook != null ? fillFromHook : useVColors != null ? PLACEHOLDER_FILL : void 0;
23006
- let useStrokeVWidths = null;
23007
- if (strokeVWidths != null) {
23008
- if (strokeVWidths.length === anchorCount2) {
23009
- useStrokeVWidths = strokeVWidths;
23010
- } else if (isDev3) {
23011
- const key = `${id}:${nodeKey}:strokeWidths`;
23012
- if (!warned.has(key)) {
23013
- warned.add(key);
23014
- console.warn(
23015
- `[createPathLayer ${id}] node ${nodeKey}: stroke vertexWidths length ${strokeVWidths.length}, expected ${anchorCount2}; dropping`
23016
- );
23017
- }
23018
- }
23019
- }
23020
- const baseStroke = strokeFromHook != null ? strokeFromHook : useStrokeVColors != null || useStrokeVWidths != null ? PLACEHOLDER_STROKE : void 0;
23021
- const stroke = baseStroke != null ? {
23022
- ...baseStroke,
23023
- ...useStrokeVColors != null ? { vertexColors: useStrokeVColors } : {},
23024
- ...useStrokeVWidths != null ? { vertexWidths: useStrokeVWidths } : {}
23025
- } : baseStroke;
23026
- if (fill == null && stroke == null) continue;
23027
- children.push({
23028
- kind: "path",
23029
- path,
23030
- ...fill != null ? { fill } : {},
23031
- ...stroke != null ? { stroke } : {},
23032
- ...useVColors != null ? { vertexColors: useVColors } : {}
23033
- });
23426
+ }
23427
+ return null;
23428
+ };
23429
+ const pickOutermost = (wx, wy) => {
23430
+ const leaf = hitLeaf(wx, wy);
23431
+ return leaf === null ? null : ancestorChain(leaf)[0];
23432
+ };
23433
+ const pickBest = (wx, wy, alt, selection) => {
23434
+ const leaf = hitLeaf(wx, wy);
23435
+ if (leaf === null) return null;
23436
+ const chain = ancestorChain(leaf);
23437
+ if (!alt) return chain[0];
23438
+ for (let i = chain.length - 1; i >= 0; i--) {
23439
+ if (selection.includes(chain[i])) {
23440
+ return chain[Math.min(i + 1, chain.length - 1)];
23034
23441
  }
23035
- return children;
23036
23442
  }
23443
+ return chain[chain.length - 1];
23037
23444
  };
23445
+ return { pickOutermost, pickBest };
23038
23446
  }
23039
23447
 
23040
- // src/features/paths/pathFromD.ts
23041
- var NUM_RE = /[+-]?(?:\d*\.\d+|\d+\.?)(?:[eE][+-]?\d+)?/g;
23042
- function tokenize(d) {
23043
- const out = [];
23044
- let i = 0;
23045
- while (i < d.length) {
23046
- const ch = d[i];
23047
- if (/[A-Za-z]/.test(ch)) {
23048
- let j = i + 1;
23049
- while (j < d.length && !/[A-Za-z]/.test(d[j])) j++;
23050
- const argSlice = d.slice(i + 1, j);
23051
- out.push({ cmd: ch, args: readNumbers(argSlice) });
23052
- i = j;
23053
- } else {
23054
- i++;
23055
- }
23448
+ // src/features/paths/pathDistance.ts
23449
+ var BEZIER_SAMPLES = 16;
23450
+ function pointSegDist2(px, py, ax, ay, bx, by) {
23451
+ const dx = bx - ax, dy = by - ay;
23452
+ const len2 = dx * dx + dy * dy;
23453
+ if (len2 === 0) {
23454
+ const ex2 = px - ax, ey2 = py - ay;
23455
+ return ex2 * ex2 + ey2 * ey2;
23056
23456
  }
23057
- return out;
23457
+ let t = ((px - ax) * dx + (py - ay) * dy) / len2;
23458
+ if (t < 0) t = 0;
23459
+ else if (t > 1) t = 1;
23460
+ const cx = ax + dx * t, cy = ay + dy * t;
23461
+ const ex = px - cx, ey = py - cy;
23462
+ return ex * ex + ey * ey;
23058
23463
  }
23059
- function readNumbers(s) {
23060
- const out = [];
23061
- let m;
23062
- NUM_RE.lastIndex = 0;
23063
- while ((m = NUM_RE.exec(s)) !== null) {
23064
- const n = Number(m[0]);
23065
- if (Number.isFinite(n)) out.push(n);
23464
+ function pointCubicDist2(px, py, x0, y0, x1, y1, x2, y2, x3, y3) {
23465
+ let prevX = x0, prevY = y0;
23466
+ let best = Infinity;
23467
+ for (let i = 1; i <= BEZIER_SAMPLES; i++) {
23468
+ const t = i / BEZIER_SAMPLES;
23469
+ const u = 1 - t;
23470
+ const sx = u * u * u * x0 + 3 * u * u * t * x1 + 3 * u * t * t * x2 + t * t * t * x3;
23471
+ const sy = u * u * u * y0 + 3 * u * u * t * y1 + 3 * u * t * t * y2 + t * t * t * y3;
23472
+ const d2 = pointSegDist2(px, py, prevX, prevY, sx, sy);
23473
+ if (d2 < best) best = d2;
23474
+ prevX = sx;
23475
+ prevY = sy;
23066
23476
  }
23067
- return out;
23477
+ return best;
23068
23478
  }
23069
- var ARG_COUNTS = {
23070
- M: 2,
23071
- L: 2,
23072
- H: 1,
23073
- V: 1,
23074
- C: 6,
23075
- S: 4,
23076
- Q: 4,
23077
- T: 2,
23078
- A: 7,
23079
- Z: 0
23080
- };
23081
- function pathFromD(d, onWarn) {
23082
- const tokens = tokenize(d);
23083
- const b = new PathBuilder();
23084
- const st = {
23085
- x: 0,
23086
- y: 0,
23087
- startX: 0,
23088
- startY: 0,
23089
- lastCubicCtrl: null,
23090
- lastQuadCtrl: null
23091
- };
23092
- for (const { cmd, args } of tokens) {
23093
- const upper = cmd.toUpperCase();
23094
- const relative = cmd !== upper;
23095
- const argCount = ARG_COUNTS[upper];
23096
- if (argCount === void 0) {
23097
- onWarn?.(`unknown path command: ${cmd}`);
23098
- continue;
23099
- }
23100
- if (upper === "Z") {
23101
- b.close();
23102
- st.x = st.startX;
23103
- st.y = st.startY;
23104
- st.lastCubicCtrl = null;
23105
- st.lastQuadCtrl = null;
23106
- continue;
23107
- }
23108
- const groups = argCount === 0 ? 1 : Math.floor(args.length / argCount);
23109
- for (let g = 0; g < groups; g++) {
23110
- const a = args.slice(g * argCount, (g + 1) * argCount);
23111
- const effectiveUpper = upper === "M" && g > 0 ? "L" : upper;
23112
- runCommand(effectiveUpper, relative, a, st, b);
23113
- }
23479
+ function pointToRectPerimeterDist(px, py, rx, ry, rw, rh) {
23480
+ const dx = Math.max(rx - px, px - (rx + rw), 0);
23481
+ const dy = Math.max(ry - py, py - (ry + rh), 0);
23482
+ if (dx > 0 || dy > 0) return Math.hypot(dx, dy);
23483
+ const left = px - rx, right = rx + rw - px;
23484
+ const top = py - ry, bottom = ry + rh - py;
23485
+ return Math.min(left, right, top, bottom);
23486
+ }
23487
+ function pathDistanceToPoint(path, px, py) {
23488
+ if (path.kind === "rect") {
23489
+ return pointToRectPerimeterDist(px, py, path.x, path.y, path.width, path.height);
23114
23490
  }
23115
- return b.build();
23491
+ return polygonPathDistance(path, px, py);
23116
23492
  }
23117
- function runCommand(upper, relative, a, st, b) {
23118
- const rx = relative ? st.x : 0;
23119
- const ry = relative ? st.y : 0;
23120
- switch (upper) {
23121
- case "M": {
23122
- const x = a[0] + rx;
23123
- const y = a[1] + ry;
23124
- b.moveTo(x, y);
23125
- st.x = x;
23126
- st.y = y;
23127
- st.startX = x;
23128
- st.startY = y;
23129
- st.lastCubicCtrl = null;
23130
- st.lastQuadCtrl = null;
23131
- return;
23132
- }
23133
- case "L": {
23134
- const x = a[0] + rx;
23135
- const y = a[1] + ry;
23136
- b.lineTo(x, y);
23137
- st.x = x;
23138
- st.y = y;
23139
- st.lastCubicCtrl = null;
23140
- st.lastQuadCtrl = null;
23141
- return;
23142
- }
23143
- case "H": {
23144
- const x = a[0] + (relative ? st.x : 0);
23145
- b.lineTo(x, st.y);
23146
- st.x = x;
23147
- st.lastCubicCtrl = null;
23148
- st.lastQuadCtrl = null;
23149
- return;
23150
- }
23151
- case "V": {
23152
- const y = a[0] + (relative ? st.y : 0);
23153
- b.lineTo(st.x, y);
23154
- st.y = y;
23155
- st.lastCubicCtrl = null;
23156
- st.lastQuadCtrl = null;
23157
- return;
23158
- }
23159
- case "C": {
23160
- const x1 = a[0] + rx, y1 = a[1] + ry;
23161
- const x2 = a[2] + rx, y2 = a[3] + ry;
23162
- const x = a[4] + rx, y = a[5] + ry;
23163
- b.curveTo(x1, y1, x2, y2, x, y);
23164
- st.x = x;
23165
- st.y = y;
23166
- st.lastCubicCtrl = { x: x2, y: y2 };
23167
- st.lastQuadCtrl = null;
23168
- return;
23169
- }
23170
- case "S": {
23171
- const ref = st.lastCubicCtrl ?? { x: st.x, y: st.y };
23172
- const x1 = 2 * st.x - ref.x;
23173
- const y1 = 2 * st.y - ref.y;
23174
- const x2 = a[0] + rx, y2 = a[1] + ry;
23175
- const x = a[2] + rx, y = a[3] + ry;
23176
- b.curveTo(x1, y1, x2, y2, x, y);
23177
- st.x = x;
23178
- st.y = y;
23179
- st.lastCubicCtrl = { x: x2, y: y2 };
23180
- st.lastQuadCtrl = null;
23181
- return;
23182
- }
23183
- case "Q": {
23184
- const x1 = a[0] + rx, y1 = a[1] + ry;
23185
- const x = a[2] + rx, y = a[3] + ry;
23186
- b.quadTo(x1, y1, x, y);
23187
- st.x = x;
23188
- st.y = y;
23189
- st.lastQuadCtrl = { x: x1, y: y1 };
23190
- st.lastCubicCtrl = null;
23191
- return;
23192
- }
23193
- case "T": {
23194
- const ref = st.lastQuadCtrl ?? { x: st.x, y: st.y };
23195
- const x1 = 2 * st.x - ref.x;
23196
- const y1 = 2 * st.y - ref.y;
23197
- const x = a[0] + rx, y = a[1] + ry;
23198
- b.quadTo(x1, y1, x, y);
23199
- st.x = x;
23200
- st.y = y;
23201
- st.lastQuadCtrl = { x: x1, y: y1 };
23202
- st.lastCubicCtrl = null;
23203
- return;
23204
- }
23205
- case "A": {
23206
- const rxA = Math.abs(a[0]);
23207
- const ryA = Math.abs(a[1]);
23208
- const xRot = a[2];
23209
- const largeArc = a[3] !== 0;
23210
- const sweep = a[4] !== 0;
23211
- const ex = a[5] + rx;
23212
- const ey = a[6] + ry;
23213
- arcToCubics(st.x, st.y, ex, ey, rxA, ryA, xRot, largeArc, sweep, b);
23214
- st.x = ex;
23215
- st.y = ey;
23216
- st.lastCubicCtrl = null;
23217
- st.lastQuadCtrl = null;
23218
- return;
23493
+ function polygonPathDistance(path, px, py) {
23494
+ const { commands, coords } = path;
23495
+ let ci = 0;
23496
+ let startX = 0, startY = 0;
23497
+ let curX = 0, curY = 0;
23498
+ let best = Infinity;
23499
+ for (let i = 0; i < commands.length; i++) {
23500
+ const cmd = commands[i];
23501
+ if (cmd === PATH_M) {
23502
+ startX = coords[ci];
23503
+ startY = coords[ci + 1];
23504
+ curX = startX;
23505
+ curY = startY;
23506
+ ci += 2;
23507
+ } else if (cmd === PATH_L) {
23508
+ const x = coords[ci], y = coords[ci + 1];
23509
+ const d2 = pointSegDist2(px, py, curX, curY, x, y);
23510
+ if (d2 < best) best = d2;
23511
+ curX = x;
23512
+ curY = y;
23513
+ ci += 2;
23514
+ } else if (cmd === PATH_C) {
23515
+ const c1x = coords[ci], c1y = coords[ci + 1];
23516
+ const c2x = coords[ci + 2], c2y = coords[ci + 3];
23517
+ const x = coords[ci + 4], y = coords[ci + 5];
23518
+ const d2 = pointCubicDist2(px, py, curX, curY, c1x, c1y, c2x, c2y, x, y);
23519
+ if (d2 < best) best = d2;
23520
+ curX = x;
23521
+ curY = y;
23522
+ ci += 6;
23523
+ } else if (cmd === PATH_Q) {
23524
+ const qx = coords[ci], qy = coords[ci + 1];
23525
+ const x = coords[ci + 2], y = coords[ci + 3];
23526
+ const c1x = curX + 2 / 3 * (qx - curX);
23527
+ const c1y = curY + 2 / 3 * (qy - curY);
23528
+ const c2x = x + 2 / 3 * (qx - x);
23529
+ const c2y = y + 2 / 3 * (qy - y);
23530
+ const d2 = pointCubicDist2(px, py, curX, curY, c1x, c1y, c2x, c2y, x, y);
23531
+ if (d2 < best) best = d2;
23532
+ curX = x;
23533
+ curY = y;
23534
+ ci += 4;
23535
+ } else if (cmd === PATH_Z) {
23536
+ const d2 = pointSegDist2(px, py, curX, curY, startX, startY);
23537
+ if (d2 < best) best = d2;
23538
+ curX = startX;
23539
+ curY = startY;
23219
23540
  }
23220
- default:
23221
- return;
23222
23541
  }
23542
+ return best === Infinity ? Infinity : Math.sqrt(best);
23223
23543
  }
23224
- function arcToCubics(x1, y1, x2, y2, rx, ry, xAxisRotationDeg, largeArc, sweep, b) {
23225
- if (x1 === x2 && y1 === y2) return;
23226
- if (rx === 0 || ry === 0) {
23227
- b.lineTo(x2, y2);
23228
- return;
23229
- }
23230
- const phi = xAxisRotationDeg * Math.PI / 180;
23231
- const cosPhi = Math.cos(phi);
23232
- const sinPhi = Math.sin(phi);
23233
- const dx = (x1 - x2) / 2;
23234
- const dy = (y1 - y2) / 2;
23235
- const x1p = cosPhi * dx + sinPhi * dy;
23236
- const y1p = -sinPhi * dx + cosPhi * dy;
23237
- let rxs = rx * rx;
23238
- let rys = ry * ry;
23239
- const x1ps = x1p * x1p;
23240
- const y1ps = y1p * y1p;
23241
- const lambda = x1ps / rxs + y1ps / rys;
23242
- let rxFixed = rx;
23243
- let ryFixed = ry;
23244
- if (lambda > 1) {
23245
- const sqrtLambda = Math.sqrt(lambda);
23246
- rxFixed = sqrtLambda * rx;
23247
- ryFixed = sqrtLambda * ry;
23248
- rxs = rxFixed * rxFixed;
23249
- rys = ryFixed * ryFixed;
23544
+
23545
+ // src/features/paths/pathLayer.ts
23546
+ var PLACEHOLDER_FILL = { color: "#ffffff" };
23547
+ var PLACEHOLDER_STROKE = { paint: { color: "#ffffff" }, width: 1 };
23548
+ function resolveOverride(base, override, tMs) {
23549
+ if (!override) return base;
23550
+ if (typeof override === "function") {
23551
+ if (!base) return base;
23552
+ const result = override(base, tMs);
23553
+ if (result.length !== base.length) return base;
23554
+ return result;
23250
23555
  }
23251
- const sign = largeArc === sweep ? -1 : 1;
23252
- const num = rxs * rys - rxs * y1ps - rys * x1ps;
23253
- const den = rxs * y1ps + rys * x1ps;
23254
- const factor = sign * Math.sqrt(Math.max(0, num / den));
23255
- const cxp = factor * (rxFixed * y1p) / ryFixed;
23256
- const cyp = factor * -(ryFixed * x1p) / rxFixed;
23257
- const cx = cosPhi * cxp - sinPhi * cyp + (x1 + x2) / 2;
23258
- const cy = sinPhi * cxp + cosPhi * cyp + (y1 + y2) / 2;
23259
- const angle = (ux, uy, vx, vy) => {
23260
- const dot = ux * vx + uy * vy;
23261
- const len = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy));
23262
- let a = Math.acos(Math.max(-1, Math.min(1, dot / len)));
23263
- if (ux * vy - uy * vx < 0) a = -a;
23264
- return a;
23556
+ return override;
23557
+ }
23558
+ function createPathLayer(opts) {
23559
+ const {
23560
+ id = "paths",
23561
+ label = "Paths",
23562
+ getNodes,
23563
+ getPath,
23564
+ getFill,
23565
+ getStroke,
23566
+ isHidden,
23567
+ getVertexColors,
23568
+ getStrokeVertexColors,
23569
+ getStrokeVertexWidths,
23570
+ colorOverrides,
23571
+ now = () => typeof performance !== "undefined" ? performance.now() : Date.now()
23572
+ } = opts;
23573
+ const warned = /* @__PURE__ */ new Set();
23574
+ const isDev3 = typeof import.meta !== "undefined" && import.meta.env && import.meta.env.DEV;
23575
+ return {
23576
+ id,
23577
+ label,
23578
+ draw: () => {
23579
+ const children = [];
23580
+ const nodes = getNodes();
23581
+ for (let idx = 0; idx < nodes.length; idx++) {
23582
+ const node = nodes[idx];
23583
+ if (isHidden?.(node)) continue;
23584
+ const path = getPath(node);
23585
+ const fillFromHook = getFill?.(node);
23586
+ const strokeFromHook = getStroke?.(node);
23587
+ const baseVColors = getVertexColors?.(node);
23588
+ const baseStrokeVColors = getStrokeVertexColors?.(node);
23589
+ const strokeVWidths = getStrokeVertexWidths?.(node);
23590
+ const nodeId = node.id ?? String(idx);
23591
+ const tMs = colorOverrides ? now() : 0;
23592
+ const fillOverride = colorOverrides?.get(nodeId, "fill");
23593
+ const strokeOverride = colorOverrides?.get(nodeId, "stroke");
23594
+ const vColors = resolveOverride(baseVColors, fillOverride, tMs);
23595
+ const strokeVColors = resolveOverride(baseStrokeVColors, strokeOverride, tMs);
23596
+ const nodeKey = nodeId;
23597
+ const anchorCount2 = countPathAnchors(path);
23598
+ const expectedLen = 4 * anchorCount2;
23599
+ let useVColors = null;
23600
+ if (vColors != null) {
23601
+ if (vColors.length === expectedLen) {
23602
+ useVColors = vColors;
23603
+ } else if (isDev3) {
23604
+ const key = `${id}:${nodeKey}:fill`;
23605
+ if (!warned.has(key)) {
23606
+ warned.add(key);
23607
+ console.warn(
23608
+ `[createPathLayer ${id}] node ${nodeKey}: fill vertexColors length ${vColors.length}, expected ${expectedLen}; dropping`
23609
+ );
23610
+ }
23611
+ }
23612
+ }
23613
+ let useStrokeVColors = null;
23614
+ if (strokeVColors != null) {
23615
+ if (strokeVColors.length === expectedLen) {
23616
+ useStrokeVColors = strokeVColors;
23617
+ } else if (isDev3) {
23618
+ const key = `${id}:${nodeKey}:stroke`;
23619
+ if (!warned.has(key)) {
23620
+ warned.add(key);
23621
+ console.warn(
23622
+ `[createPathLayer ${id}] node ${nodeKey}: stroke vertexColors length ${strokeVColors.length}, expected ${expectedLen}; dropping`
23623
+ );
23624
+ }
23625
+ }
23626
+ }
23627
+ const fill = fillFromHook != null ? fillFromHook : useVColors != null ? PLACEHOLDER_FILL : void 0;
23628
+ let useStrokeVWidths = null;
23629
+ if (strokeVWidths != null) {
23630
+ if (strokeVWidths.length === anchorCount2) {
23631
+ useStrokeVWidths = strokeVWidths;
23632
+ } else if (isDev3) {
23633
+ const key = `${id}:${nodeKey}:strokeWidths`;
23634
+ if (!warned.has(key)) {
23635
+ warned.add(key);
23636
+ console.warn(
23637
+ `[createPathLayer ${id}] node ${nodeKey}: stroke vertexWidths length ${strokeVWidths.length}, expected ${anchorCount2}; dropping`
23638
+ );
23639
+ }
23640
+ }
23641
+ }
23642
+ const baseStroke = strokeFromHook != null ? strokeFromHook : useStrokeVColors != null || useStrokeVWidths != null ? PLACEHOLDER_STROKE : void 0;
23643
+ const stroke = baseStroke != null ? {
23644
+ ...baseStroke,
23645
+ ...useStrokeVColors != null ? { vertexColors: useStrokeVColors } : {},
23646
+ ...useStrokeVWidths != null ? { vertexWidths: useStrokeVWidths } : {}
23647
+ } : baseStroke;
23648
+ if (fill == null && stroke == null) continue;
23649
+ children.push({
23650
+ kind: "path",
23651
+ path,
23652
+ ...fill != null ? { fill } : {},
23653
+ ...stroke != null ? { stroke } : {},
23654
+ ...useVColors != null ? { vertexColors: useVColors } : {}
23655
+ });
23656
+ }
23657
+ return children;
23658
+ }
23265
23659
  };
23266
- const theta1 = angle(1, 0, (x1p - cxp) / rxFixed, (y1p - cyp) / ryFixed);
23267
- let dTheta = angle(
23268
- (x1p - cxp) / rxFixed,
23269
- (y1p - cyp) / ryFixed,
23270
- (-x1p - cxp) / rxFixed,
23271
- (-y1p - cyp) / ryFixed
23272
- );
23273
- if (!sweep && dTheta > 0) dTheta -= 2 * Math.PI;
23274
- else if (sweep && dTheta < 0) dTheta += 2 * Math.PI;
23275
- const segCount = Math.max(1, Math.ceil(Math.abs(dTheta) / (Math.PI / 2)));
23276
- const segAngle = dTheta / segCount;
23277
- const t = 4 / 3 * Math.tan(segAngle / 4);
23278
- let prevX = x1;
23279
- let prevY = y1;
23280
- let prevDx = -Math.sin(theta1) * rxFixed;
23281
- let prevDy = Math.cos(theta1) * ryFixed;
23282
- for (let i = 1; i <= segCount; i++) {
23283
- const theta = theta1 + i * segAngle;
23284
- const cosT = Math.cos(theta);
23285
- const sinT = Math.sin(theta);
23286
- const px = cosPhi * (cosT * rxFixed) - sinPhi * (sinT * ryFixed) + cx;
23287
- const py = sinPhi * (cosT * rxFixed) + cosPhi * (sinT * ryFixed) + cy;
23288
- const dxNew = -sinT * rxFixed;
23289
- const dyNew = cosT * ryFixed;
23290
- const c1x = prevX + cosPhi * (t * prevDx) - sinPhi * (t * prevDy);
23291
- const c1y = prevY + sinPhi * (t * prevDx) + cosPhi * (t * prevDy);
23292
- const c2x = px - cosPhi * (t * dxNew) + sinPhi * (t * dyNew);
23293
- const c2y = py - sinPhi * (t * dxNew) - cosPhi * (t * dyNew);
23294
- b.curveTo(c1x, c1y, c2x, c2y, px, py);
23295
- prevX = px;
23296
- prevY = py;
23297
- prevDx = dxNew;
23298
- prevDy = dyNew;
23299
- }
23300
23660
  }
23301
23661
 
23302
23662
  // src/features/paths/compose.ts
@@ -24763,6 +25123,50 @@ function useDragRadial(options = {}) {
24763
25123
  }
24764
25124
  }), [start, move, gesture.end, gesture.cancel]);
24765
25125
  }
25126
+
25127
+ // src/interactions/actions/edit-anchors/handles.ts
25128
+ function hitAnchor(path, worldX, worldY, threshold) {
25129
+ const anchors = enumerateAnchors(path);
25130
+ let bestControl = null;
25131
+ let bestAnchor = null;
25132
+ const t2 = threshold * threshold;
25133
+ for (const a of anchors) {
25134
+ if (a.controlIn) {
25135
+ const dx2 = a.controlIn.x - worldX;
25136
+ const dy2 = a.controlIn.y - worldY;
25137
+ const d22 = dx2 * dx2 + dy2 * dy2;
25138
+ if (d22 <= t2 && (!bestControl || d22 < bestControl.d2)) {
25139
+ bestControl = {
25140
+ d2: d22,
25141
+ hit: { anchorIndex: a.anchorIndex, kind: "controlIn", coordIndex: a.controlIn.coordIndex }
25142
+ };
25143
+ }
25144
+ }
25145
+ if (a.controlOut) {
25146
+ const dx2 = a.controlOut.x - worldX;
25147
+ const dy2 = a.controlOut.y - worldY;
25148
+ const d22 = dx2 * dx2 + dy2 * dy2;
25149
+ if (d22 <= t2 && (!bestControl || d22 < bestControl.d2)) {
25150
+ bestControl = {
25151
+ d2: d22,
25152
+ hit: { anchorIndex: a.anchorIndex, kind: "controlOut", coordIndex: a.controlOut.coordIndex }
25153
+ };
25154
+ }
25155
+ }
25156
+ const dx = a.x - worldX;
25157
+ const dy = a.y - worldY;
25158
+ const d2 = dx * dx + dy * dy;
25159
+ if (d2 <= t2 && (!bestAnchor || d2 < bestAnchor.d2)) {
25160
+ bestAnchor = {
25161
+ d2,
25162
+ hit: { anchorIndex: a.anchorIndex, kind: "anchor", coordIndex: a.coordIndex }
25163
+ };
25164
+ }
25165
+ }
25166
+ if (bestControl) return bestControl.hit;
25167
+ if (bestAnchor) return bestAnchor.hit;
25168
+ return null;
25169
+ }
24766
25170
  function useDistribute(adapter, options = {}) {
24767
25171
  const adapterRef = useRef(adapter);
24768
25172
  adapterRef.current = adapter;
@@ -26197,7 +26601,7 @@ function hslToRgbUnit(h, s, l) {
26197
26601
  }
26198
26602
 
26199
26603
  // src/layout/snaps.ts
26200
- function dist23(a, b) {
26604
+ function dist22(a, b) {
26201
26605
  const dx = a.x - b.x;
26202
26606
  const dy = a.y - b.y;
26203
26607
  return dx * dx + dy * dy;
@@ -26205,9 +26609,9 @@ function dist23(a, b) {
26205
26609
  function nearestOf(targets, pointer) {
26206
26610
  if (targets.length === 0) return null;
26207
26611
  let best = targets[0];
26208
- let bestD = dist23(best.origin, pointer);
26612
+ let bestD = dist22(best.origin, pointer);
26209
26613
  for (let i = 1; i < targets.length; i++) {
26210
- const d = dist23(targets[i].origin, pointer);
26614
+ const d = dist22(targets[i].origin, pointer);
26211
26615
  if (d < bestD) {
26212
26616
  bestD = d;
26213
26617
  best = targets[i];
@@ -26227,7 +26631,7 @@ function nearestWithin(opts) {
26227
26631
  pickTarget(targets, pointer) {
26228
26632
  const got = nearestOf(targets, pointer);
26229
26633
  if (got === null) return null;
26230
- return dist23(got.origin, pointer) <= tol2 ? got : null;
26634
+ return dist22(got.origin, pointer) <= tol2 ? got : null;
26231
26635
  }
26232
26636
  };
26233
26637
  }
@@ -26466,6 +26870,6 @@ function snapPoint(opts) {
26466
26870
  };
26467
26871
  }
26468
26872
 
26469
- export { ALWAYS, ActionDisabledReason, ActionsProvider, ActionsProviderIfRoot, ActiveToolContextProvider, ActiveToolContextProviderIfRoot, BUNDLE_TOOLS, CURVE_REPS, ColorOverrideRegistry, CropIcon, CursorCoordsHud, DEFAULT_ALPHA_DECAY, DEFAULT_ALPHA_MIN, DEFAULT_DEBUG_THEME, DEFAULT_FILL_COLOR, DEFAULT_HANDLE_SIZE2 as DEFAULT_HANDLE_SIZE, DEFAULT_PALETTE, DEFAULT_ROTATION_HANDLE_DISTANCE, DEFAULT_STROKE_COLOR, DEFAULT_TEXT_STYLE, DEFAULT_VELOCITY_DECAY, DepRegistryProvider, DepRegistryProviderIfRoot, DivideIcon, EASINGS, EllipseIcon, ExcludeIcon, EyedropperIcon, FALLBACK_FIT_VIEW, GHOST_STROKE, HandIcon, IDENTITY_COLOR_MATRIX, IDENTITY_POSE_COMPOSITION, IntersectIcon, KIT_SHAPE_KINDS, LassoIcon, LineIcon, MIXED, MOVE_ANCHORS, MinimapCanvas, NEVER, PenIcon, PencilIcon, PickHud, PointerProviderIfRoot, PolygonIcon, RECT_ALIGN_PROJECTION, RectIcon, SPRING_PRESETS, SceneCanvas, SceneViewCanvas, SelectIcon, SelectionContextProvider, SelectionContextProviderIfRoot, ShaderCompileError, StarIcon, SubtractIcon, TextIcon, UnionIcon, UnknownIcon, WeaselProvider, WeaselRenderer, aabbCenter, actionIs, alignDeltaFor, alignInsertBehavior, alignMoveBehavior, alignResizeBehavior, alignedStrokeRect, always, and, animateLifecycle, animateOnSetPose, applyBooleanOp, applyHitExistingGate, applyStyleToRange, areaSelectAction, arrayAdapter, asNodeId, bezierCubic, bezierQuadratic, buildChromeCtx, buildGradientRamp, buildRuleCtx, buildSceneViewCommands, canBringForward, canSendBackward, capabilityAll, capabilityIn, capabilityIs, capabilityNot, caretIndexAt, cellAt, charOffsetToDomPosition, clampView, clearSelectionAction, clientToCanvas, cloneAction, composeAffordanceLayer, composePath, composeRectPose, composeSelectionPose, composeWorldPose, computeFitView2 as computeFitView, computeIndicatorCommand, computeWheelAction, cond, constrainTo45, containedThenNearest, countPathAnchors, createCellHighlightLayer, createChildrenLayer, createCornerResizeAffordance, createDebugOverlayLayer, createDebugSink, createDispatcher, createGridLayer, createGuidesLayer, createHistory2 as createHistory, createMarkdownRenderer, createMoveToIndexOp, createNodeProperties, createNodeRouting, createParallaxLayer, createPathEditingOverlayLayer, createPathLayer, createPenPreviewLayer, createReorderOp, createRotationAffordance, createScene, createSelectionHandlesLayer, createSelectionOutlineLayer, createSelectionOverlayLayer, createSetDataOp, createSetLayerOp, createSetPathOp, createSetTextOp, createTextLayer, createViewportLayer, cycleVertexColors, decomposePath, decomposeRectPose, defaultCommitAdapter, defaultDrawOne, defaultLabelTextRenderer, defaultNodeProperties, defaultNodeRouting, defaultVisibilityRules, deriveAlignmentGuides, deriveParallaxView, describeRule, domPositionToCharOffset, domToRuns, drawLayers, easeIn, easeInBack, easeInBounce, easeInCirc, easeInCubic, easeInElastic, easeInExpo, easeInOut, easeInOutBack, easeInOutBounce, easeInOutCirc, easeInOutCubic, easeInOutElastic, easeInOutExpo, easeInOutQuad, easeInOutQuart, easeInOutQuint, easeInOutSine, easeInQuad, easeInQuart, easeInQuint, easeInSine, easeOut, easeOutBack, easeOutBounce, easeOutCirc, easeOutCubic2 as easeOutCubic, easeOutElastic, easeOutExpo, easeOutQuad, easeOutQuart, easeOutQuint, easeOutSine, editAnchorsAction, enterTextEditAction, enumerateAnchors, evaluate, evaluateEnabled, findNodeShape, findShapeSilhouette, fitTextPose, fitToBounds, fitViewToBounds, fitZoom, focused, fontString, forEachCoalesced, freeform, gateLayer, gesturing, getAlpha01, getImageBitmap, getNodeShapes, getStylusData, hexToRgba, hitAnchor, hitRotationHandle, hovering, hoveringSelected, imageStatus, inferredNodeProperties, inferredNodeRouting, insertAction, isEditableTarget2 as isEditableTarget, lassoSelectAction, layoutMarkdown, lerpColorArray, lerpOklab, lerpOklch, linear, makeViewportZoomAction, markdownToRuns, mat3, matchAlignment, matchesKeyBinding, measureText, measureTextBounds, measuredWidth, mergeAlphaFromPrev, modeIn, modeIs, modeNot, modifierHeld, momentum, moveAction, multiActive, nearest, nearestWithin, nestedHitTester, never, none, normalizeHex, not, nurbs, oklabToOklch, oklabToSrgbU8, oklchToOklab, openFilePicker, or, parseColor, parseColorToRgba255, parseDebugFlags, pathContainsPoint, pathContainsPolygon, pathContainsRect, pathDistanceToPoint, pathDivide, pathExclude, pathFromD, pathInPoseFrame, pathInWorld, pathIntersect, pathIntersectsPolygon, pathIntersectsRect, pathOriginProjection, pathSubtract, pathToAnchors, pathUnion, pickTopMostHit, pinchZoomAction, planPixelRender, pointInRotatedRect, pointInTextPose, poseRotationOf, pressureToWidth, rainbowVertexColors, rebaseLocalPose, rectCorners, registerContentHandler, registerNodeShape, registerProgram, renderLabel, renderSceneToCanvas, renderSceneToPixels, resizeAction, resolveColor, resolveRuns, resolveTextStyle, resolveVisibility, rgbaToHex, rotateAction, rotateAroundAABBCenter, rotatePathAround, rotatePoint, rotatedRectCorners, rotationDegreesUnit, rotationHandle, roundToCell, runsToDom, runsToMarkdown, runsToPlainText, sceneFromJSON, sceneNodeClientRect, sceneToAdapter, screenToWorld, selectFromLasso, selectionAtLeast, selectionEmpty, selectionIs, sliceAction, snapPoint, solidVertexColors, specificity, spiro, splitPathByLine, splitSubpaths, springPose, springVertexColors, srgbU8ToOklab, staggerVertexColors, startThresholdDrag, styleAtRange, subscribeImageReady, tessellate, tessellateStroke, textCommand, tileGrid, toHex8, toRuns, translatePoseViaDescriptor, translateRectPose, tweenPose, tweenVertexColors, unionBounds, unionBoundsPath, useAction, useActionsRegistry, useActiveToolContext, useAlign, useAnimator, useArrayAdapter, useAutoCenter, useBooleans, useBooleansAdapter, useCanvasFocus, useCanvasSize, useDecayLoop, useDepRegistry, useDepSource, useDistribute, useDragGesture, useDragHandle, useDragRadial, useDragRect, useDropZone, useEllipseTool, useEyedropperTool, useGestureDispatcher, useGridCellHover, useGuides, useHandTool, useHandleDrag, useHoverTracking, useImageTool, useKeybindings, useLassoTool, useLineTool, useOptionalActiveToolContext, useOptionalDepRegistry, usePenTool, usePencilTool, usePinchGesture, usePinchZoomTool, usePointerStylus, usePolygonTool, usePublishSelection, useRectTool, useResizePolicy, useRotateTool, useScene, useSceneAdapter, useSceneTextEdit, useSelectTool, useSelection, useSelectionContext, useSimulation, useSliceDep, useStandardActions, useStarTool, useTextEdit, useTextTool, useTools, useVelocityTracker, useViewAnimation, useViewTween, useZoom, verticalAlignOffset, viewToMat3, viewToTransform, viewportDragPanAction, viewportZoomAction, when, withAlpha01, withCoord, worldEditToStorage, worldPoseLookup, worldToScreen, zoomAt, zoomAtLeast };
26470
- //# sourceMappingURL=chunk-7F3SDUJ4.js.map
26471
- //# sourceMappingURL=chunk-7F3SDUJ4.js.map
26873
+ export { ALWAYS, ActionDisabledReason, ActionsProvider, ActionsProviderIfRoot, ActiveToolContextProvider, ActiveToolContextProviderIfRoot, BUNDLE_TOOLS, CURVE_REPS, ColorOverrideRegistry, CropIcon, CursorCoordsHud, DEFAULT_ALPHA_DECAY, DEFAULT_ALPHA_MIN, DEFAULT_DEBUG_THEME, DEFAULT_FILL_COLOR, DEFAULT_HANDLE_SIZE2 as DEFAULT_HANDLE_SIZE, DEFAULT_PALETTE, DEFAULT_ROTATION_HANDLE_DISTANCE, DEFAULT_STROKE_COLOR, DEFAULT_TEXT_STYLE, DEFAULT_VELOCITY_DECAY, DepRegistryProvider, DepRegistryProviderIfRoot, DivideIcon, EASINGS, EllipseIcon, ExcludeIcon, EyedropperIcon, FALLBACK_FIT_VIEW, GHOST_STROKE, HandIcon, IDENTITY_COLOR_MATRIX, IDENTITY_POSE_COMPOSITION, IntersectIcon, KIT_SHAPE_KINDS, LassoIcon, LineIcon, MIXED, MOVE_ANCHORS, MinimapCanvas, NEVER, OUTLINE_MIN_SCREEN_PX, PATH_ANCHOR_CHROME_ID, PenIcon, PencilIcon, PickHud, PointerProviderIfRoot, PolygonIcon, RECT_ALIGN_PROJECTION, RectIcon, SPRING_PRESETS, SceneCanvas, SceneViewCanvas, SelectIcon, SelectionContextProvider, SelectionContextProviderIfRoot, ShaderCompileError, StarIcon, SubtractIcon, TextIcon, UnionIcon, UnknownIcon, VERSION, WeaselProvider, WeaselRenderer, aabbCenter, actionIs, alignDeltaFor, alignInsertBehavior, alignMoveBehavior, alignResizeBehavior, alignedStrokeRect, always, and, animateLifecycle, animateOnSetPose, annulusSemiAxes, applyBooleanOp, applyHitExistingGate, applyStyleToRange, areaSelectAction, arrayAdapter, asNodeId, bezierCubic, bezierQuadratic, buildChromeCtx, buildGradientRamp, buildRuleCtx, buildSceneViewCommands, canBringForward, canSendBackward, capabilityAll, capabilityIn, capabilityIs, capabilityNot, caretIndexAt, cellAt, charOffsetToDomPosition, clampView, clearSelectionAction, clientToCanvas, cloneAction, composeAffordanceLayer, composePath, composeRectPose, composeSelectionPose, composeWorldPose, computeFitView2 as computeFitView, computeIndicatorCommand, computeWheelAction, cond, constrainTo45, containedThenNearest, countPathAnchors, createCellHighlightLayer, createChildrenLayer, createCornerResizeAffordance, createDebugOverlayLayer, createDebugSink, createDispatcher, createGridLayer, createGuidesLayer, createHistory2 as createHistory, createMarkdownRenderer, createMoveToIndexOp, createNodeProperties, createNodeRouting, createParallaxLayer, createPathAnchorAffordances, createPathEditingOverlayLayer, createPathLayer, createPenPreviewLayer, createReorderOp, createRotationAffordance, createScene, createSelectionHandlesLayer, createSelectionOutlineLayer, createSelectionOverlayLayer, createSetDataOp, createSetLayerOp, createSetPathOp, createSetTextOp, createTextLayer, createViewportLayer, cycleVertexColors, decomposePath, decomposeRectPose, defaultCommitAdapter, defaultDrawOne, defaultLabelTextRenderer, defaultNodeProperties, defaultNodeRouting, defaultVisibilityRules, deriveAlignmentGuides, deriveParallaxView, describeRule, domPositionToCharOffset, domToRuns, drawLayers, easeIn, easeInBack, easeInBounce, easeInCirc, easeInCubic, easeInElastic, easeInExpo, easeInOut, easeInOutBack, easeInOutBounce, easeInOutCirc, easeInOutCubic, easeInOutElastic, easeInOutExpo, easeInOutQuad, easeInOutQuart, easeInOutQuint, easeInOutSine, easeInQuad, easeInQuart, easeInQuint, easeInSine, easeOut, easeOutBack, easeOutBounce, easeOutCirc, easeOutCubic2 as easeOutCubic, easeOutElastic, easeOutExpo, easeOutQuad, easeOutQuart, easeOutQuint, easeOutSine, editAnchorsAction, enterTextEditAction, enumerateAnchors, evaluate, evaluateEnabled, findNodeShape, findShapeInk, findShapeSilhouette, fitTextPose, fitToBounds, fitViewToBounds, fitZoom, focused, fontString, forEachCoalesced, freeform, gateLayer, gesturing, getAlpha01, getImageBitmap, getNodeShapes, getStylusData, hexToRgba, hitAffordanceRegions, hitAnchor, hitRotationHandle, hovering, hoveringSelected, imageStatus, inferredNodeProperties, inferredNodeRouting, insertAction, isEditableTarget2 as isEditableTarget, lassoSelectAction, layoutMarkdown, lerpColorArray, lerpOklab, lerpOklch, linear, makeViewportZoomAction, markdownToRuns, mat3, matchAlignment, matchesKeyBinding, measureText, measureTextBounds, measuredWidth, mergeAlphaFromPrev, modeIn, modeIs, modeNot, modifierHeld, momentum, moveAction, multiActive, nearest, nearestWithin, nestedHitTester, never, none, normalizeHex, not, nurbs, oklabToOklch, oklabToSrgbU8, oklchToOklab, openFilePicker, or, parseColor, parseColorToRgba255, parseDebugFlags, pathContainsPoint, pathContainsPolygon, pathContainsRect, pathDistanceToPoint, pathDivide, pathExclude, pathFromD, pathInPoseFrame, pathInWorld, pathIntersect, pathIntersectsPolygon, pathIntersectsRect, pathOriginProjection, pathSubtract, pathToAnchors, pathUnion, pickTopMostHit, pinchZoomAction, planPixelRender, pointInRotatedRect, pointInTextPose, poseRotationOf, pressureToWidth, rainbowVertexColors, rebaseLocalPose, rectCorners, registerContentHandler, registerNodeShape, registerProgram, renderLabel, renderSceneToCanvas, renderSceneToPixels, resizeAction, resolveColor, resolveRuns, resolveTextStyle, resolveVisibility, rgbaToHex, rotateAction, rotateAroundAABBCenter, rotatePathAround, rotatePoint, rotatedRectCorners, rotationDegreesUnit, rotationHandle, roundToCell, runsToDom, runsToMarkdown, runsToPlainText, sceneFromJSON, sceneNodeClientRect, sceneToAdapter, screenToWorld, selectFromLasso, selectionAtLeast, selectionEmpty, selectionIs, shapeCoversPoint, sliceAction, snapPoint, solidVertexColors, specificity, spiro, splitPathByLine, splitSubpaths, springPose, springVertexColors, srgbU8ToOklab, staggerVertexColors, startThresholdDrag, styleAtRange, subscribeImageReady, tessellate, tessellateStroke, textCommand, textLineBoxes, tileGrid, toHex8, toRuns, translatePoseViaDescriptor, translateRectPose, tweenPose, tweenVertexColors, unionBounds, unionBoundsPath, useAction, useActionsRegistry, useActiveToolContext, useAlign, useAnimator, useArrayAdapter, useAutoCenter, useBooleans, useBooleansAdapter, useCanvasFocus, useCanvasSize, useDecayLoop, useDepRegistry, useDepSource, useDistribute, useDragGesture, useDragHandle, useDragRadial, useDragRect, useDropZone, useEllipseTool, useEyedropperTool, useGestureDispatcher, useGridCellHover, useGuides, useHandTool, useHandleDrag, useHoverTracking, useImageTool, useKeybindings, useLassoTool, useLineTool, useOptionalActiveToolContext, useOptionalDepRegistry, usePenTool, usePencilTool, usePinchGesture, usePinchZoomTool, usePointerStylus, usePolygonTool, usePublishSelection, useRectTool, useResizePolicy, useRotateTool, useScene, useSceneAdapter, useSceneTextEdit, useSelectTool, useSelection, useSelectionContext, useSimulation, useSliceDep, useStandardActions, useStarTool, useTextEdit, useTextTool, useTools, useVelocityTracker, useViewAnimation, useViewTween, useZoom, verticalAlignOffset, viewToMat3, viewToTransform, viewportDragPanAction, viewportZoomAction, when, withAlpha01, withCoord, worldEditToStorage, worldPoseLookup, worldToScreen, zoomAt, zoomAtLeast };
26874
+ //# sourceMappingURL=chunk-5F3PIERN.js.map
26875
+ //# sourceMappingURL=chunk-5F3PIERN.js.map