@scrawl-board/board 0.1.0-beta.1 → 0.1.0-beta.3

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.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
2
2
  import { createPortal } from "react-dom";
3
3
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
- //#region src/core-internal/assets.ts
4
+ //#region src/core/shapes/assets.ts
5
5
  var ASSET_REF_PATTERN = /^asset:[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?:[A-Za-z0-9._~-]+$/;
6
6
  /** Hard ceiling on a reference's own wire length — independent of any resource limit below. */
7
7
  var ASSET_REF_MAX_BYTES = 512;
@@ -41,7 +41,7 @@ function clampAssetCacheBytes(value) {
41
41
  return Math.min(ASSET_CACHE_BYTES_MAX, Math.max(ASSET_CACHE_BYTES_MIN, Math.floor(value)));
42
42
  }
43
43
  //#endregion
44
- //#region src/core-internal/identifiers.ts
44
+ //#region src/core/document/identifiers.ts
45
45
  function documentId(value) {
46
46
  if (!value) throw new TypeError("DocumentId cannot be empty");
47
47
  return value;
@@ -51,7 +51,7 @@ function strokeId(value) {
51
51
  return value;
52
52
  }
53
53
  //#endregion
54
- //#region src/document-schema.ts
54
+ //#region src/core/document/document-schema.ts
55
55
  var CURRENT_DOCUMENT_SCHEMA_VERSION = 1;
56
56
  var DocumentRecoveryError = class extends Error {
57
57
  constructor(code, message, originalBytes, path) {
@@ -259,7 +259,7 @@ function validation(message, path) {
259
259
  return new DocumentRecoveryError("DOCUMENT_VALIDATION_FAILED", message, void 0, path);
260
260
  }
261
261
  //#endregion
262
- //#region src/core-internal/affine2d.ts
262
+ //#region src/core/shapes/affine2d.ts
263
263
  var IDENTITY = [
264
264
  1,
265
265
  0,
@@ -343,7 +343,7 @@ function avgScale(m) {
343
343
  return (Math.hypot(m[0], m[1]) + Math.hypot(m[2], m[3])) / 2;
344
344
  }
345
345
  //#endregion
346
- //#region src/core-internal/boardSearch.ts
346
+ //#region src/core/document/boardSearch.ts
347
347
  function hay(s) {
348
348
  return s.toLowerCase();
349
349
  }
@@ -408,7 +408,7 @@ function searchBoard(query, board) {
408
408
  return hits;
409
409
  }
410
410
  //#endregion
411
- //#region src/core-internal/types.ts
411
+ //#region src/core/shapes/types.ts
412
412
  /** Erasure level at which a point counts as fully erased. */
413
413
  var ERASE_THRESHOLD = .95;
414
414
  function cloneStroke(stroke) {
@@ -484,7 +484,7 @@ function cloneImage(img) {
484
484
  return { ...img };
485
485
  }
486
486
  //#endregion
487
- //#region src/core-internal/clusterStore.ts
487
+ //#region src/core/document/clusterStore.ts
488
488
  /** A stroke joins a cluster last written to within this window at full reach. */
489
489
  var RECENT_MS = 8e3;
490
490
  /** Older clusters are still joinable, at half the gap threshold. */
@@ -655,7 +655,7 @@ function clamp$3(v, lo, hi) {
655
655
  return Math.min(hi, Math.max(lo, v));
656
656
  }
657
657
  //#endregion
658
- //#region src/core-internal/extensionTypes.ts
658
+ //#region src/core/shapes/extensionTypes.ts
659
659
  function cloneCustomObject(object) {
660
660
  return {
661
661
  ...object,
@@ -665,7 +665,7 @@ function cloneCustomObject(object) {
665
665
  };
666
666
  }
667
667
  //#endregion
668
- //#region src/core-internal/kitchenTimer.ts
668
+ //#region src/core/shapes/kitchenTimer.ts
669
669
  var TIMER_DEFAULT_SIZE = 12;
670
670
  var TIMER_DEFAULT_DURATION_MS = 300 * 1e3;
671
671
  var TIMER_PRESETS_MS = [
@@ -722,7 +722,7 @@ function formatTimer(ms) {
722
722
  return `${Math.floor(total / 60)}:${(total % 60).toString().padStart(2, "0")}`;
723
723
  }
724
724
  //#endregion
725
- //#region src/core-internal/itemLock.ts
725
+ //#region src/core/shapes/itemLock.ts
726
726
  /**
727
727
  * Apply or clear a lock in place. Unlock always drops the holder so a stale
728
728
  * name cannot linger on an unlocked item.
@@ -759,7 +759,7 @@ function serializeLock(item) {
759
759
  };
760
760
  }
761
761
  //#endregion
762
- //#region src/core-internal/document.ts
762
+ //#region src/core/document/document.ts
763
763
  /**
764
764
  * The one place a live Stroke becomes its wire representation — in
765
765
  * particular, points collapse from {x,y,pressure,erase?} objects into
@@ -1318,7 +1318,7 @@ function computeBBox(stroke) {
1318
1318
  return world;
1319
1319
  }
1320
1320
  //#endregion
1321
- //#region src/core-internal/commands.ts
1321
+ //#region src/core/commands/commands.ts
1322
1322
  /** One drawing action — a marker stroke, or a shape's strokes as one unit. */
1323
1323
  var AddStrokesCommand = class {
1324
1324
  constructor(strokes) {
@@ -1660,7 +1660,7 @@ var LockItemsCommand = class {
1660
1660
  }
1661
1661
  };
1662
1662
  //#endregion
1663
- //#region src/core-internal/history.ts
1663
+ //#region src/core/history/history.ts
1664
1664
  var LIMIT = 200;
1665
1665
  var History = class {
1666
1666
  constructor(doc) {
@@ -1721,7 +1721,7 @@ var History = class {
1721
1721
  }
1722
1722
  };
1723
1723
  //#endregion
1724
- //#region src/core-internal/ops.ts
1724
+ //#region src/core/events/ops.ts
1725
1725
  var ADDED = [
1726
1726
  ["added", "strokes"],
1727
1727
  ["notesAdded", "notes"],
@@ -1775,10 +1775,18 @@ function changeToOps(change, restoring = false) {
1775
1775
  return ops;
1776
1776
  }
1777
1777
  //#endregion
1778
- //#region src/core-internal/ribbonEdges.ts
1778
+ //#region src/core/shapes/ribbonEdges.ts
1779
1779
  var MIN_WIDTH_FACTOR = .35;
1780
1780
  var END_TAPER = .55;
1781
- function ribbonEdges(points, baseWidth) {
1781
+ /**
1782
+ * @param handDrawn Default `true`: ink's organic feel — width follows
1783
+ * per-point pressure, and both ends taper. Pass `false` for a geometric
1784
+ * shape outline, which has no real pressure signal and isn't a pen stroke
1785
+ * with a lift-off: it renders at the exact `baseWidth`, uniformly, with no
1786
+ * taper at its start/end (which, for a closed shape, is the same point —
1787
+ * tapering it would visibly pinch just that one corner).
1788
+ */
1789
+ function ribbonEdges(points, baseWidth, handDrawn = true) {
1782
1790
  const pts = points.length === 1 ? [points[0], {
1783
1791
  ...points[0],
1784
1792
  x: points[0].x + baseWidth * .05
@@ -1793,8 +1801,8 @@ function ribbonEdges(points, baseWidth) {
1793
1801
  const len = Math.hypot(dx, dy) || 1;
1794
1802
  dx /= len;
1795
1803
  dy /= len;
1796
- let width = baseWidth * (MIN_WIDTH_FACTOR + (1 - MIN_WIDTH_FACTOR) * pts[i].pressure);
1797
- if (i === 0 || i === n - 1) width *= END_TAPER;
1804
+ let width = handDrawn ? baseWidth * (MIN_WIDTH_FACTOR + (1 - MIN_WIDTH_FACTOR) * pts[i].pressure) : baseWidth;
1805
+ if (handDrawn && (i === 0 || i === n - 1)) width *= END_TAPER;
1798
1806
  const hw = width / 2;
1799
1807
  out[i] = {
1800
1808
  lx: pts[i].x - dy * hw,
@@ -1807,7 +1815,7 @@ function ribbonEdges(points, baseWidth) {
1807
1815
  return out;
1808
1816
  }
1809
1817
  //#endregion
1810
- //#region src/core-internal/spatialIndex.ts
1818
+ //#region src/core/document/spatialIndex.ts
1811
1819
  var CELL = 8;
1812
1820
  var SpatialIndex = class {
1813
1821
  constructor(doc) {
@@ -1880,7 +1888,7 @@ function cellsOf(box) {
1880
1888
  return keys;
1881
1889
  }
1882
1890
  //#endregion
1883
- //#region src/core-internal/stamps.ts
1891
+ //#region src/core/shapes/stamps.ts
1884
1892
  var STAMP_SIZE = 6;
1885
1893
  var STAMPS = [
1886
1894
  {
@@ -1929,7 +1937,7 @@ function isStampKind(value) {
1929
1937
  return typeof value === "string" && STAMPS.some((s) => s.kind === value);
1930
1938
  }
1931
1939
  //#endregion
1932
- //#region src/core-internal/presence.ts
1940
+ //#region src/collaboration/presence/presence.ts
1933
1941
  /** Keep beacons clear of typical chrome at the top and bottom of the Board. */
1934
1942
  var BEACON_INSET = {
1935
1943
  top: 72,
@@ -1965,7 +1973,7 @@ function clamp$2(value, min, max) {
1965
1973
  return Math.min(max, Math.max(min, value));
1966
1974
  }
1967
1975
  //#endregion
1968
- //#region src/core-internal/svg.ts
1976
+ //#region src/persistence/serialization/svg.ts
1969
1977
  /** Matches the key light used by the board shader and note shadows. */
1970
1978
  var LIGHT = {
1971
1979
  x: -.25,
@@ -2107,7 +2115,7 @@ function strokeToPaths(stroke) {
2107
2115
  y,
2108
2116
  pressure,
2109
2117
  ...erase > 0 ? { erase } : {}
2110
- })), stroke.baseWidth);
2118
+ })), stroke.baseWidth, stroke.tool !== "shape");
2111
2119
  const transform = stroke.matrix ? ` transform="matrix(${fmt(stroke.matrix[0])} ${fmt(-stroke.matrix[1])} ${fmt(-stroke.matrix[2])} ${fmt(stroke.matrix[3])} ${fmt(stroke.matrix[4])} ${fmt(-stroke.matrix[5])})"` : "";
2112
2120
  const baseOpacity = stroke.tool === "highlighter" ? .4 : 1;
2113
2121
  const paths = [];
@@ -2336,7 +2344,7 @@ function fmt(n) {
2336
2344
  var SDK_PACKAGE_NAME = "@scrawl-board/board";
2337
2345
  var SDK_DEVELOPMENT_VERSION = "0.0.0-development";
2338
2346
  //#endregion
2339
- //#region src/core-internal/sceneGeometry.ts
2347
+ //#region src/core/shapes/sceneGeometry.ts
2340
2348
  var EMPTY_BOUNDS = {
2341
2349
  x: 0,
2342
2350
  y: 0,
@@ -2400,7 +2408,7 @@ function textCorners(x, y, text, fontSize, align, m) {
2400
2408
  return rectCorners(x + (align === "center" ? -width / 2 : align === "end" ? -width : 0), y, width, height, m);
2401
2409
  }
2402
2410
  //#endregion
2403
- //#region src/core-internal/customObjects.ts
2411
+ //#region src/core/shapes/customObjects.ts
2404
2412
  var FALLBACK_LABEL_MAX_LENGTH = 80;
2405
2413
  function toReadonly(object) {
2406
2414
  return object;
@@ -2529,7 +2537,7 @@ function extensionIdFromType(type) {
2529
2537
  return slash === -1 ? type : type.slice(0, slash);
2530
2538
  }
2531
2539
  //#endregion
2532
- //#region src/core-internal/extensionRegistry.ts
2540
+ //#region src/core/shapes/extensionRegistry.ts
2533
2541
  function buildExtensionRegistry(extensions) {
2534
2542
  const list = extensions ?? [];
2535
2543
  const diagnostics = [];
@@ -2626,7 +2634,7 @@ function isWellFormedNamespacedId(id) {
2626
2634
  return isWellFormedId(id) && id.includes("/") && !id.startsWith("/") && !id.endsWith("/");
2627
2635
  }
2628
2636
  //#endregion
2629
- //#region src/core-internal/assetValidation.ts
2637
+ //#region src/persistence/adapters/assetValidation.ts
2630
2638
  function sniffImageMediaType(bytes) {
2631
2639
  if (bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71 && bytes[4] === 13 && bytes[5] === 10 && bytes[6] === 26 && bytes[7] === 10) return "image/png";
2632
2640
  if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
@@ -2713,7 +2721,7 @@ function validateDecodedDimensions(width, height) {
2713
2721
  };
2714
2722
  }
2715
2723
  //#endregion
2716
- //#region src/assetController.ts
2724
+ //#region src/persistence/adapters/assetController.ts
2717
2725
  var FAILURE_COALESCE_MS = 4e3;
2718
2726
  var AssetController = class {
2719
2727
  constructor(resolver, options) {
@@ -2892,7 +2900,7 @@ function toAssetResolutionError(cause, ref) {
2892
2900
  return new AssetResolutionError("unknown", true, cause instanceof Error ? cause.message : String(cause), ref);
2893
2901
  }
2894
2902
  //#endregion
2895
- //#region src/core-internal/assetExport.ts
2903
+ //#region src/persistence/serialization/assetRefCollector.ts
2896
2904
  /** Every unique Asset reference in the document, and which object(s) use it. */
2897
2905
  function collectDocumentAssetRefs(doc, registry) {
2898
2906
  const refs = /* @__PURE__ */ new Map();
@@ -2919,7 +2927,7 @@ function collectSceneAssetRefs(node, objectId, add) {
2919
2927
  if (node.kind === "group") for (const child of node.children) collectSceneAssetRefs(child, objectId, add);
2920
2928
  }
2921
2929
  //#endregion
2922
- //#region src/assetExport.ts
2930
+ //#region src/persistence/serialization/assetExport.ts
2923
2931
  /** Strict mode's rejection — never contains credentials, a fetchable location, or a raw resolver message. */
2924
2932
  var SvgAssetExportError = class extends Error {
2925
2933
  constructor(missingAssets) {
@@ -2975,7 +2983,7 @@ function toDataUri(bytes, mediaType) {
2975
2983
  return `data:${mediaType};base64,${btoa(binary)}`;
2976
2984
  }
2977
2985
  //#endregion
2978
- //#region src/assetIngestion.ts
2986
+ //#region src/persistence/adapters/assetIngestion.ts
2979
2987
  async function decodeDimensionsWithImageBitmap(bytes, mediaType) {
2980
2988
  const blob = new Blob([bytes], { type: mediaType });
2981
2989
  let bitmap;
@@ -35621,7 +35629,7 @@ var WebGLRenderer = class {
35621
35629
  }
35622
35630
  };
35623
35631
  //#endregion
35624
- //#region ../../src/scrawl/data/auditEvents.ts
35632
+ //#region src/core/events/auditEvents.ts
35625
35633
  var BY_LABEL = {
35626
35634
  draw: {
35627
35635
  action: "create",
@@ -35741,7 +35749,7 @@ function lowerFirst(s) {
35741
35749
  return s.charAt(0).toLowerCase() + s.slice(1);
35742
35750
  }
35743
35751
  //#endregion
35744
- //#region ../../src/scrawl/assets/AssetTextureCache.ts
35752
+ //#region src/renderer/shapes/AssetTextureCache.ts
35745
35753
  var AssetTextureCache = class {
35746
35754
  constructor(assetController) {
35747
35755
  this.assetController = assetController;
@@ -35832,7 +35840,7 @@ function toDecodeError(cause, ref) {
35832
35840
  return new AssetResolutionError("decode-failed", false, cause instanceof Error ? cause.message : String(cause), ref);
35833
35841
  }
35834
35842
  //#endregion
35835
- //#region ../../src/scrawl/engine/raycast.ts
35843
+ //#region src/renderer/canvas/raycast.ts
35836
35844
  var origin$1 = new Vector3();
35837
35845
  var target = new Vector3();
35838
35846
  function pointerToBoard(clientX, clientY, canvas, camera) {
@@ -35863,7 +35871,7 @@ function pointerToPlane(clientX, clientY, canvas, camera, z) {
35863
35871
  };
35864
35872
  }
35865
35873
  //#endregion
35866
- //#region ../../src/scrawl/engine/fog.ts
35874
+ //#region src/renderer/canvas/fog.ts
35867
35875
  function createFogUniforms(color) {
35868
35876
  return {
35869
35877
  uFogColor: { value: new Color(color) },
@@ -35884,18 +35892,24 @@ var FOG_GLSL = `
35884
35892
  }
35885
35893
  `;
35886
35894
  //#endregion
35887
- //#region ../../src/scrawl/engine/boardSurface.ts
35895
+ //#region src/renderer/canvas/boardSurface.ts
35888
35896
  var GRID_MODE_CODE = {
35889
35897
  none: 0,
35890
35898
  line: 1,
35891
35899
  dot: 2
35892
35900
  };
35901
+ var TEXTURE_CODE = {
35902
+ flat: 0,
35903
+ textured: 1
35904
+ };
35893
35905
  /** Used when a caller doesn't supply one (e.g. ScrawlEngine constructed without a Host theme). */
35894
35906
  var DEFAULT_BOARD_SURFACE_THEME = {
35895
35907
  color: "#f5f5f3",
35896
35908
  gridMode: "none",
35897
35909
  gridColor: "#deded8",
35898
- gridSpacing: 5
35910
+ gridSpacing: 5,
35911
+ gridLineWidth: 1,
35912
+ surfaceTexture: "flat"
35899
35913
  };
35900
35914
  /** Grid alpha fades out below this on-screen spacing (px) — otherwise it aliases into a solid wash when zoomed out. */
35901
35915
  var GRID_FADE_MIN_PX = 3;
@@ -35916,8 +35930,10 @@ var FRAGMENT = `
35916
35930
  uniform float uGridMode;
35917
35931
  uniform vec3 uGridColor;
35918
35932
  uniform float uGridSpacing;
35919
- uniform float uGridLineWorld;
35933
+ uniform float uWorldPerPixel;
35934
+ uniform float uGridLineWidthPx;
35920
35935
  uniform float uGridFade;
35936
+ uniform float uSurfaceTexture;
35921
35937
  varying vec3 vWorldPos;
35922
35938
 
35923
35939
  float hash(vec2 p) {
@@ -35937,37 +35953,45 @@ var FRAGMENT = `
35937
35953
  ${FOG_GLSL}
35938
35954
 
35939
35955
  // Distance from p's nearest axis-aligned grid line, in world units.
35956
+ // A crisp, ~uGridLineWidthPx-wide line: full alpha to its half-width, a
35957
+ // ~1px anti-aliased falloff beyond that — not a wide, soft-edged band.
35940
35958
  float gridLineAlpha(vec2 p) {
35941
35959
  vec2 cell = mod(p, uGridSpacing);
35942
35960
  vec2 distToLine = min(cell, uGridSpacing - cell);
35943
- vec2 lineAlpha2 = 1.0 - smoothstep(uGridLineWorld, uGridLineWorld * 2.0, distToLine);
35961
+ float halfWidth = 0.5 * uGridLineWidthPx * uWorldPerPixel;
35962
+ float aa = uWorldPerPixel;
35963
+ vec2 lineAlpha2 = 1.0 - smoothstep(halfWidth - aa * 0.5, halfWidth + aa * 0.5, distToLine);
35944
35964
  return max(lineAlpha2.x, lineAlpha2.y);
35945
35965
  }
35946
35966
 
35947
- // Distance from p's nearest grid intersection, in world units.
35967
+ // Distance from p's nearest grid intersection, in world units. Dot radius
35968
+ // matches the configured line width in pixels, same anti-aliasing.
35948
35969
  float gridDotAlpha(vec2 p) {
35949
35970
  vec2 cell = mod(p, uGridSpacing) - uGridSpacing * 0.5;
35950
35971
  float dist = length(cell);
35951
- float radius = uGridLineWorld * 1.5;
35952
- return 1.0 - smoothstep(radius, radius * 2.0, dist);
35972
+ float radius = uGridLineWidthPx * uWorldPerPixel;
35973
+ float aa = uWorldPerPixel;
35974
+ return 1.0 - smoothstep(radius - aa * 0.5, radius + aa * 0.5, dist);
35953
35975
  }
35954
35976
 
35955
35977
  void main() {
35956
35978
  vec3 col = uColor;
35957
35979
 
35958
- // Melamine micro-texture: two octaves of value noise, +-1.75% brightness.
35959
- float n = vnoise(vWorldPos.xy * 2.3) * 0.6 + vnoise(vWorldPos.xy * 9.7) * 0.4;
35960
- col *= 1.0 + (n - 0.5) * 0.035;
35980
+ if (uSurfaceTexture > 0.5) {
35981
+ // Melamine micro-texture: two octaves of value noise, +-1.75% brightness.
35982
+ float n = vnoise(vWorldPos.xy * 2.3) * 0.6 + vnoise(vWorldPos.xy * 9.7) * 0.4;
35983
+ col *= 1.0 + (n - 0.5) * 0.035;
35961
35984
 
35962
- // Satin sheen: Fresnel rim + a specular band from the key light. Both are
35963
- // ~zero in the straight-on working view and bloom as the camera tilts.
35964
- vec3 V = normalize(cameraPosition - vWorldPos);
35965
- vec3 N = vec3(0.0, 0.0, 1.0);
35966
- float fres = pow(1.0 - max(dot(N, V), 0.0), 3.0);
35967
- vec3 L = normalize(vec3(-0.25, 0.4, 0.88));
35968
- vec3 H = normalize(L + V);
35969
- float spec = pow(max(dot(N, H), 0.0), 90.0);
35970
- col += (fres * 0.18 + spec * 0.06) * vec3(1.0);
35985
+ // Satin sheen: Fresnel rim + a specular band from the key light. Both
35986
+ // are ~zero in the straight-on working view and bloom as the camera tilts.
35987
+ vec3 V = normalize(cameraPosition - vWorldPos);
35988
+ vec3 N = vec3(0.0, 0.0, 1.0);
35989
+ float fres = pow(1.0 - max(dot(N, V), 0.0), 3.0);
35990
+ vec3 L = normalize(vec3(-0.25, 0.4, 0.88));
35991
+ vec3 H = normalize(L + V);
35992
+ float spec = pow(max(dot(N, H), 0.0), 90.0);
35993
+ col += (fres * 0.18 + spec * 0.06) * vec3(1.0);
35994
+ }
35971
35995
 
35972
35996
  if (uGridMode > 0.5 && uGridFade > 0.001) {
35973
35997
  float gridAlpha = (uGridMode < 1.5 ? gridLineAlpha(vWorldPos.xy) : gridDotAlpha(vWorldPos.xy)) * uGridFade;
@@ -35987,8 +36011,10 @@ function createBoardSurface(fog, theme) {
35987
36011
  uGridMode: { value: GRID_MODE_CODE[theme.gridMode] },
35988
36012
  uGridColor: { value: new Color(theme.gridColor) },
35989
36013
  uGridSpacing: { value: Math.max(.001, theme.gridSpacing) },
35990
- uGridLineWorld: { value: .05 },
36014
+ uGridLineWidthPx: { value: Math.max(.1, theme.gridLineWidth) },
36015
+ uWorldPerPixel: { value: .05 },
35991
36016
  uGridFade: { value: 0 },
36017
+ uSurfaceTexture: { value: TEXTURE_CODE[theme.surfaceTexture] },
35992
36018
  ...fog
35993
36019
  }
35994
36020
  });
@@ -35996,13 +36022,15 @@ function createBoardSurface(fog, theme) {
35996
36022
  mesh.name = "board-surface";
35997
36023
  return mesh;
35998
36024
  }
35999
- /** Applies a theme change (initial or live) — everything except the per-frame fade/line-width. */
36025
+ /** Applies a theme change (initial or live) — everything except the per-frame fade/pixel-ratio. */
36000
36026
  function setBoardSurfaceTheme(mesh, theme) {
36001
36027
  const uniforms = mesh.material.uniforms;
36002
36028
  uniforms.uColor.value.set(theme.color);
36003
36029
  uniforms.uGridMode.value = GRID_MODE_CODE[theme.gridMode];
36004
36030
  uniforms.uGridColor.value.set(theme.gridColor);
36005
36031
  uniforms.uGridSpacing.value = Math.max(.001, theme.gridSpacing);
36032
+ uniforms.uGridLineWidthPx.value = Math.max(.1, theme.gridLineWidth);
36033
+ uniforms.uSurfaceTexture.value = TEXTURE_CODE[theme.surfaceTexture];
36006
36034
  }
36007
36035
  /** Keep the plane centered under the view, larger than the fog horizon, and the grid's on-screen weight/fade current. */
36008
36036
  function updateBoardSurface(mesh, centerX, centerY, fogFar, worldPerPixel) {
@@ -36010,7 +36038,7 @@ function updateBoardSurface(mesh, centerX, centerY, fogFar, worldPerPixel) {
36010
36038
  const size = fogFar * 2.5;
36011
36039
  mesh.scale.set(size, size, 1);
36012
36040
  const uniforms = mesh.material.uniforms;
36013
- uniforms.uGridLineWorld.value = worldPerPixel;
36041
+ uniforms.uWorldPerPixel.value = worldPerPixel;
36014
36042
  const t = (uniforms.uGridSpacing.value / Math.max(1e-6, worldPerPixel) - GRID_FADE_MIN_PX) / (GRID_FADE_MAX_PX - GRID_FADE_MIN_PX);
36015
36043
  uniforms.uGridFade.value = MathUtils.clamp(t, 0, 1);
36016
36044
  }
@@ -36183,7 +36211,7 @@ var CameraRig = class {
36183
36211
  }
36184
36212
  };
36185
36213
  //#endregion
36186
- //#region ../../src/scrawl/engine/scene.ts
36214
+ //#region src/renderer/canvas/scene.ts
36187
36215
  var SceneRig = class {
36188
36216
  constructor(canvas, boardTheme) {
36189
36217
  this.renderer = new WebGLRenderer({
@@ -36230,7 +36258,7 @@ var SceneRig = class {
36230
36258
  }
36231
36259
  };
36232
36260
  //#endregion
36233
- //#region ../../src/scrawl/ink/inkMaterial.ts
36261
+ //#region src/renderer/shapes/inkMaterial.ts
36234
36262
  var VERTEX = `
36235
36263
  attribute float aAlpha;
36236
36264
  varying vec2 vUv;
@@ -36361,15 +36389,15 @@ var InkMaterials = class {
36361
36389
  }
36362
36390
  };
36363
36391
  //#endregion
36364
- //#region ../../src/scrawl/ink/ribbonGeometry.ts
36392
+ //#region src/renderer/shapes/ribbonGeometry.ts
36365
36393
  /** Ink floats a hair above the board to avoid z-fighting with the surface. */
36366
36394
  var INK_Z = .02;
36367
36395
  /** Highlighter always renders under opaque ink (build prompt §4.1). */
36368
36396
  var HIGHLIGHT_Z = .016;
36369
36397
  /** Ghosts sit under everything so live strokes always read on top. */
36370
36398
  var GHOST_Z = .012;
36371
- function buildRibbonGeometry(points, baseWidth, z = INK_Z) {
36372
- const edges = ribbonEdges(points, baseWidth);
36399
+ function buildRibbonGeometry(points, baseWidth, z = INK_Z, handDrawn = true) {
36400
+ const edges = ribbonEdges(points, baseWidth, handDrawn);
36373
36401
  const n = edges.length;
36374
36402
  const positions = new Float32Array(n * 2 * 3);
36375
36403
  const uvs = new Float32Array(n * 2 * 2);
@@ -36412,8 +36440,10 @@ function buildRibbonGeometry(points, baseWidth, z = INK_Z) {
36412
36440
  return geometry;
36413
36441
  }
36414
36442
  //#endregion
36415
- //#region ../../src/scrawl/ink/strokeRenderer.ts
36443
+ //#region src/renderer/shapes/strokeRenderer.ts
36416
36444
  var strokeZ = (stroke) => stroke.tool === "highlighter" ? HIGHLIGHT_Z : INK_Z;
36445
+ /** Shapes (rect/ellipse/line/arrow/...) are geometric outlines, not pressure-sensitive ink. */
36446
+ var handDrawn = (stroke) => stroke.tool !== "shape";
36417
36447
  /**
36418
36448
  * Derives ink meshes from the document by subscription — the document never
36419
36449
  * knows the renderer exists. Also hosts session-only ghosts (decision Q7)
@@ -36438,12 +36468,12 @@ var StrokeRenderer = class {
36438
36468
  this.ghostGroup.visible = visible;
36439
36469
  }
36440
36470
  /** Leave a smeared residue of an erased span (never persisted). */
36441
- addGhost(points, color, baseWidth) {
36471
+ addGhost(points, color, baseWidth, handDrawn = true) {
36442
36472
  if (points.length < 2) return;
36443
36473
  const mesh = new Mesh(buildRibbonGeometry(points.map((p) => ({
36444
36474
  ...p,
36445
36475
  erase: 0
36446
- })), baseWidth * 1.7, GHOST_Z), this.materials.ghost(color));
36476
+ })), baseWidth * 1.7, GHOST_Z, handDrawn), this.materials.ghost(color));
36447
36477
  this.ghostGroup.add(mesh);
36448
36478
  }
36449
36479
  /**
@@ -36455,7 +36485,7 @@ var StrokeRenderer = class {
36455
36485
  if (this.meshes.has(stroke.id) || this.retiredLive.has(stroke.id) || stroke.points.length < 2) return;
36456
36486
  this.liveUser.set(stroke.id, userId);
36457
36487
  const existing = this.liveMeshes.get(stroke.id);
36458
- const geometry = buildRibbonGeometry(stroke.points, stroke.baseWidth, strokeZ(stroke));
36488
+ const geometry = buildRibbonGeometry(stroke.points, stroke.baseWidth, strokeZ(stroke), handDrawn(stroke));
36459
36489
  if (existing) {
36460
36490
  existing.geometry.dispose();
36461
36491
  existing.geometry = geometry;
@@ -36515,7 +36545,7 @@ var StrokeRenderer = class {
36515
36545
  const mesh = this.meshes.get(stroke.id);
36516
36546
  if (mesh) {
36517
36547
  mesh.geometry.dispose();
36518
- mesh.geometry = buildRibbonGeometry(stroke.points, stroke.baseWidth, strokeZ(stroke));
36548
+ mesh.geometry = buildRibbonGeometry(stroke.points, stroke.baseWidth, strokeZ(stroke), handDrawn(stroke));
36519
36549
  }
36520
36550
  }
36521
36551
  for (const stroke of change.transformed) {
@@ -36524,7 +36554,7 @@ var StrokeRenderer = class {
36524
36554
  }
36525
36555
  }
36526
36556
  create(stroke) {
36527
- const mesh = new Mesh(buildRibbonGeometry(stroke.points, stroke.baseWidth, strokeZ(stroke)), stroke.tool === "highlighter" ? this.materials.highlight(stroke.color) : this.materials.get(stroke.color));
36557
+ const mesh = new Mesh(buildRibbonGeometry(stroke.points, stroke.baseWidth, strokeZ(stroke), handDrawn(stroke)), stroke.tool === "highlighter" ? this.materials.highlight(stroke.color) : this.materials.get(stroke.color));
36528
36558
  mesh.matrixAutoUpdate = false;
36529
36559
  syncMatrix(mesh, stroke);
36530
36560
  this.meshes.set(stroke.id, mesh);
@@ -36545,7 +36575,7 @@ function syncMatrix(mesh, stroke) {
36545
36575
  mesh.matrixWorldNeedsUpdate = true;
36546
36576
  }
36547
36577
  //#endregion
36548
- //#region ../../src/scrawl/scene-objects/markerProp.ts
36578
+ //#region src/renderer/shapes/markerProp.ts
36549
36579
  var BODY_LENGTH = 11;
36550
36580
  var BODY_RADIUS = .85;
36551
36581
  var TIP_LENGTH = 1.6;
@@ -36641,7 +36671,7 @@ var MarkerProp = class {
36641
36671
  }
36642
36672
  };
36643
36673
  //#endregion
36644
- //#region ../../src/scrawl/scene-objects/noteGeometry.ts
36674
+ //#region src/renderer/shapes/noteGeometry.ts
36645
36675
  /**
36646
36676
  * Height of the sheet above the board at (u, v) ∈ [0,1]².
36647
36677
  * v = 1 is the adhesive edge (always 0); u = 1 is the peel corner side.
@@ -36685,7 +36715,7 @@ function applyCurl(geometry, curl, peel) {
36685
36715
  color.needsUpdate = true;
36686
36716
  }
36687
36717
  //#endregion
36688
- //#region ../../src/scrawl/scene-objects/stickyNotes.ts
36718
+ //#region src/renderer/shapes/stickyNotes.ts
36689
36719
  var TEXTURE_SIZE = 512;
36690
36720
  var FOCUS_COLOR$4 = 2450411;
36691
36721
  var CURL = .3;
@@ -43261,7 +43291,7 @@ SYNCABLE_PROPS.forEach((prop) => {
43261
43291
  new Box3();
43262
43292
  new Color();
43263
43293
  //#endregion
43264
- //#region ../../src/scrawl/scene-objects/tables.ts
43294
+ //#region src/renderer/shapes/tables.ts
43265
43295
  var FONT_URL$2 = "/fonts/inter-medium.woff";
43266
43296
  var TABLE_Z = .012;
43267
43297
  var HEADER_Z = .014;
@@ -43610,7 +43640,7 @@ var TablesRenderer = class {
43610
43640
  }
43611
43641
  };
43612
43642
  //#endregion
43613
- //#region ../../src/scrawl/scene-objects/textBlocks.ts
43643
+ //#region src/renderer/text/textBlocks.ts
43614
43644
  var FONT_URL$1 = "/fonts/inter-medium.woff";
43615
43645
  var TEXT_Z = .02;
43616
43646
  var FOCUS_COLOR$2 = 2450411;
@@ -43714,7 +43744,7 @@ var TextBlocksRenderer = class {
43714
43744
  }
43715
43745
  };
43716
43746
  //#endregion
43717
- //#region ../../src/scrawl/scene-objects/comments.ts
43747
+ //#region src/renderer/shapes/comments.ts
43718
43748
  var PIN_Z = .04;
43719
43749
  var PIN_WIDTH = 3.6;
43720
43750
  var PIN_HEIGHT = 3.6;
@@ -43882,7 +43912,7 @@ function drawBubblePath(ctx, x, y, w, h, r, tailX, tailY, tailW) {
43882
43912
  ctx.closePath();
43883
43913
  }
43884
43914
  //#endregion
43885
- //#region ../../src/scrawl/scene-objects/images.ts
43915
+ //#region src/renderer/shapes/images.ts
43886
43916
  var IMAGE_Z = .015;
43887
43917
  var FRAME_BORDER = .4;
43888
43918
  var FOCUS_COLOR$1 = 2450411;
@@ -44092,7 +44122,7 @@ function applyPlaceholderTexture(visual, label) {
44092
44122
  visual.imageMesh.material.needsUpdate = true;
44093
44123
  }
44094
44124
  //#endregion
44095
- //#region ../../src/scrawl/scene-objects/kitchenTimers.ts
44125
+ //#region src/renderer/shapes/kitchenTimers.ts
44096
44126
  var FACE_SIZE = 256;
44097
44127
  var BODY_COLOR = 14753096;
44098
44128
  var KNOB_COLOR = 16448250;
@@ -44290,7 +44320,7 @@ function renderTimerFace(timer, canvas, now = Date.now()) {
44290
44320
  return c;
44291
44321
  }
44292
44322
  //#endregion
44293
- //#region ../../src/scrawl/scene-objects/customObjects.ts
44323
+ //#region src/renderer/shapes/customObjects.ts
44294
44324
  var FONT_URL = "/fonts/inter-medium.woff";
44295
44325
  var CUSTOM_Z = .02;
44296
44326
  var FOCUS_COLOR = 2450411;
@@ -44652,7 +44682,7 @@ function disposeObject3D(obj) {
44652
44682
  });
44653
44683
  }
44654
44684
  //#endregion
44655
- //#region ../../src/scrawl/tools/hitTest.ts
44685
+ //#region src/interaction/pointer/hitTest.ts
44656
44686
  /** Topmost (most recently added) stroke under the point, or null. */
44657
44687
  function hitStroke(doc, index, p, slop) {
44658
44688
  const candidates = index.query(p.x - slop, p.y - slop, p.x + slop, p.y + slop);
@@ -44663,9 +44693,10 @@ function hitStroke(doc, index, p, slop) {
44663
44693
  const scale = m ? avgScale(m) : 1;
44664
44694
  let prev = null;
44665
44695
  let prevWidth = 0;
44696
+ const handDrawn = stroke.tool !== "shape";
44666
44697
  for (const point of stroke.points) {
44667
44698
  const world = m ? apply(m, point) : point;
44668
- const width = stroke.baseWidth * (MIN_WIDTH_FACTOR + (1 - MIN_WIDTH_FACTOR) * point.pressure) * scale;
44699
+ const width = (handDrawn ? stroke.baseWidth * (MIN_WIDTH_FACTOR + (1 - MIN_WIDTH_FACTOR) * point.pressure) : stroke.baseWidth) * scale;
44669
44700
  if (prev) {
44670
44701
  const reach = Math.max(width, prevWidth) / 2 + slop;
44671
44702
  if (distToSegmentSq(p, prev, world) <= reach * reach) {
@@ -44693,7 +44724,7 @@ function distToSegmentSq(p, a, b) {
44693
44724
  return dx * dx + dy * dy;
44694
44725
  }
44695
44726
  //#endregion
44696
- //#region ../../src/scrawl/tools/eraserTool.ts
44727
+ //#region src/interaction/tools/eraserTool.ts
44697
44728
  var DECAY_PER_PASS = .4;
44698
44729
  /** A point can take another decay pass after this long — rubbing works. */
44699
44730
  var REARM_MS = 250;
@@ -44799,7 +44830,7 @@ var EraserTool = class {
44799
44830
  ...p,
44800
44831
  ...apply(m, p)
44801
44832
  })) : run.points;
44802
- this.ctx.renderer.addGhost(world, current.color, current.baseWidth * (m ? avgScale(m) : 1));
44833
+ this.ctx.renderer.addGhost(world, current.color, current.baseWidth * (m ? avgScale(m) : 1), current.tool !== "shape");
44803
44834
  }
44804
44835
  }
44805
44836
  doc.removeStrokes(before.map((s) => s.id));
@@ -44821,7 +44852,7 @@ function partitionByErasure(points) {
44821
44852
  return runs;
44822
44853
  }
44823
44854
  //#endregion
44824
- //#region ../../src/scrawl/tools/noteDrag.ts
44855
+ //#region src/interaction/tools/noteDrag.ts
44825
44856
  /**
44826
44857
  * Drag the focused note in X/Y. The grab is resolved on the note's own
44827
44858
  * z-plane, not the board, so the note doesn't jump under a tilted ray.
@@ -44888,7 +44919,7 @@ var NoteDragState = class {
44888
44919
  }
44889
44920
  };
44890
44921
  //#endregion
44891
- //#region ../../src/scrawl/tools/imageDrag.ts
44922
+ //#region src/interaction/tools/imageDrag.ts
44892
44923
  /** Drag the focused image in X/Y; one command per drag. */
44893
44924
  var ImageDragState = class {
44894
44925
  constructor(ctx) {
@@ -44940,7 +44971,7 @@ var ImageDragState = class {
44940
44971
  }
44941
44972
  };
44942
44973
  //#endregion
44943
- //#region ../../src/scrawl/tools/timerDrag.ts
44974
+ //#region src/interaction/tools/timerDrag.ts
44944
44975
  /** Drag the focused timer in X/Y; one command per drag. */
44945
44976
  var TimerDragState = class {
44946
44977
  constructor(ctx) {
@@ -44992,7 +45023,7 @@ var TimerDragState = class {
44992
45023
  }
44993
45024
  };
44994
45025
  //#endregion
44995
- //#region ../../src/scrawl/tools/shapes.ts
45026
+ //#region src/interaction/tools/shapeGeometry.ts
44996
45027
  /** Uniform synthetic pressure; the ink shader supplies the hand-drawn feel. */
44997
45028
  var PRESSURE = .7;
44998
45029
  var MIN_ELLIPSE_SEGMENTS = 24;
@@ -45032,10 +45063,7 @@ function rect(a, b, step) {
45032
45063
  a
45033
45064
  ];
45034
45065
  const out = [];
45035
- for (let i = 0; i < corners.length - 1; i++) {
45036
- const edge = sampleSegment(corners[i], corners[i + 1], step);
45037
- out.push(...i === 0 ? edge : edge.slice(1));
45038
- }
45066
+ for (let i = 0; i < corners.length - 1; i++) out.push(...sampleSegment(corners[i], corners[i + 1], step));
45039
45067
  return out;
45040
45068
  }
45041
45069
  function ellipse(a, b, step) {
@@ -45106,10 +45134,7 @@ function polygon(sides, a, b, step, startAngle = -Math.PI / 2) {
45106
45134
  }
45107
45135
  vertices.push(vertices[0]);
45108
45136
  const out = [];
45109
- for (let i = 0; i < vertices.length - 1; i++) {
45110
- const edge = sampleSegment(vertices[i], vertices[i + 1], step);
45111
- out.push(...i === 0 ? edge : edge.slice(1));
45112
- }
45137
+ for (let i = 0; i < vertices.length - 1; i++) out.push(...sampleSegment(vertices[i], vertices[i + 1], step));
45113
45138
  return out;
45114
45139
  }
45115
45140
  function star(points, a, b, step) {
@@ -45133,10 +45158,7 @@ function star(points, a, b, step) {
45133
45158
  }
45134
45159
  vertices.push(vertices[0]);
45135
45160
  const out = [];
45136
- for (let i = 0; i < vertices.length - 1; i++) {
45137
- const edge = sampleSegment(vertices[i], vertices[i + 1], step);
45138
- out.push(...i === 0 ? edge : edge.slice(1));
45139
- }
45161
+ for (let i = 0; i < vertices.length - 1; i++) out.push(...sampleSegment(vertices[i], vertices[i + 1], step));
45140
45162
  return out;
45141
45163
  }
45142
45164
  function heart(a, b, step) {
@@ -45181,7 +45203,7 @@ function snappedEnd(a, b) {
45181
45203
  };
45182
45204
  }
45183
45205
  //#endregion
45184
- //#region ../../src/scrawl/tools/shapeTool.ts
45206
+ //#region src/interaction/tools/shapeTool.ts
45185
45207
  var MIN_DRAG_PX$1 = 4;
45186
45208
  /** Drag out a shape; live preview through the real ink material. */
45187
45209
  var ShapeTool = class {
@@ -45212,7 +45234,8 @@ var ShapeTool = class {
45212
45234
  const strokes = this.polylines.map((points) => ({
45213
45235
  id: crypto.randomUUID(),
45214
45236
  color: this.ctx.inkColor(),
45215
- baseWidth: this.ctx.baseWidth(),
45237
+ baseWidth: this.ctx.shapeStrokeWidth(),
45238
+ tool: "shape",
45216
45239
  points
45217
45240
  }));
45218
45241
  this.ctx.clusters.assign(strokes[0]);
@@ -45246,13 +45269,13 @@ var ShapeTool = class {
45246
45269
  this.previews[i].visible = polyline !== void 0;
45247
45270
  if (polyline) {
45248
45271
  this.previews[i].geometry.dispose();
45249
- this.previews[i].geometry = buildRibbonGeometry(polyline, this.ctx.baseWidth());
45272
+ this.previews[i].geometry = buildRibbonGeometry(polyline, this.ctx.shapeStrokeWidth(), void 0, false);
45250
45273
  }
45251
45274
  }
45252
45275
  }
45253
45276
  };
45254
45277
  //#endregion
45255
- //#region ../../src/scrawl/tools/tableDrag.ts
45278
+ //#region src/interaction/tools/tableDrag.ts
45256
45279
  function getHandleCursor(handle) {
45257
45280
  switch (handle) {
45258
45281
  case "nw":
@@ -45372,7 +45395,7 @@ var TableDragState = class {
45372
45395
  }
45373
45396
  };
45374
45397
  //#endregion
45375
- //#region ../../src/scrawl/tools/tableTool.ts
45398
+ //#region src/interaction/tools/tableTool.ts
45376
45399
  var MIN_DRAG_PX = 6;
45377
45400
  var TableTool = class {
45378
45401
  constructor(ctx) {
@@ -45503,7 +45526,7 @@ var TableTool = class {
45503
45526
  }
45504
45527
  };
45505
45528
  //#endregion
45506
- //#region ../../src/scrawl/tools/commentTool.ts
45529
+ //#region src/interaction/tools/commentTool.ts
45507
45530
  /** Click on the board to drop a new comment pin or open an existing one. */
45508
45531
  var CommentTool = class {
45509
45532
  constructor(ctx) {
@@ -45538,7 +45561,7 @@ var CommentTool = class {
45538
45561
  }
45539
45562
  };
45540
45563
  //#endregion
45541
- //#region ../../src/scrawl/tools/textDrag.ts
45564
+ //#region src/interaction/tools/textDrag.ts
45542
45565
  /** Drag the focused text block in X/Y; one command per drag. */
45543
45566
  var TextDragState = class {
45544
45567
  constructor(ctx) {
@@ -45590,7 +45613,7 @@ var TextDragState = class {
45590
45613
  }
45591
45614
  };
45592
45615
  //#endregion
45593
- //#region ../../src/scrawl/tools/fsm.ts
45616
+ //#region src/interaction/tools/fsm.ts
45594
45617
  var ToolStateMachine = class {
45595
45618
  constructor() {
45596
45619
  this.states = /* @__PURE__ */ new Map();
@@ -45792,7 +45815,7 @@ var IdleState = class {
45792
45815
  }
45793
45816
  };
45794
45817
  //#endregion
45795
- //#region ../../src/scrawl/tools/gizmo.ts
45818
+ //#region src/renderer/selection/gizmo.ts
45796
45819
  var GIZMO_Z = .05;
45797
45820
  var HANDLE_PX = 9;
45798
45821
  var HANDLE_HIT_PX = 12;
@@ -46023,7 +46046,7 @@ function boxesEqual(a, b) {
46023
46046
  return a.minX === b.minX && a.minY === b.minY && a.maxX === b.maxX && a.maxY === b.maxY;
46024
46047
  }
46025
46048
  //#endregion
46026
- //#region ../../src/scrawl/ink/pressure.ts
46049
+ //#region src/interaction/tools/pressure.ts
46027
46050
  var SIM_MAX = 1;
46028
46051
  var SIM_MIN = .3;
46029
46052
  var SMOOTHING = .25;
@@ -46045,7 +46068,7 @@ function clamp(v, lo, hi) {
46045
46068
  return Math.min(hi, Math.max(lo, v));
46046
46069
  }
46047
46070
  //#endregion
46048
- //#region ../../src/scrawl/ink/strokeBuilder.ts
46071
+ //#region src/interaction/tools/strokeBuilder.ts
46049
46072
  var StrokeBuilder = class {
46050
46073
  constructor() {
46051
46074
  this.points = [];
@@ -46095,7 +46118,7 @@ function catmullRom(v0, v1, v2, v3, t) {
46095
46118
  return .5 * (2 * v1 + (-v0 + v2) * t + (2 * v0 - 5 * v1 + 4 * v2 - v3) * t2 + (-v0 + 3 * v1 - 3 * v2 + v3) * t3);
46096
46119
  }
46097
46120
  //#endregion
46098
- //#region ../../src/scrawl/tools/markerTool.ts
46121
+ //#region src/interaction/tools/markerTool.ts
46099
46122
  /** Sample spacing while drawing, in screen pixels. */
46100
46123
  var MIN_SAMPLE_PX = 1.5;
46101
46124
  /** A highlighter is a chisel tip: near-constant width, little pressure play. */
@@ -46198,7 +46221,7 @@ var MarkerTool = class {
46198
46221
  }
46199
46222
  };
46200
46223
  //#endregion
46201
- //#region ../../src/scrawl/tools/orbitTool.ts
46224
+ //#region src/interaction/tools/orbitTool.ts
46202
46225
  /**
46203
46226
  * Orbit-preview (build prompt §4.2): alt-drag tilts the camera off-axis to
46204
46227
  * read the board's material and depth; releasing eases back to the working
@@ -46233,7 +46256,7 @@ var OrbitTool = class {
46233
46256
  }
46234
46257
  };
46235
46258
  //#endregion
46236
- //#region ../../src/scrawl/tools/panTool.ts
46259
+ //#region src/interaction/tools/panTool.ts
46237
46260
  /** Grab-pan: content follows the pointer. */
46238
46261
  var PanTool = class {
46239
46262
  constructor(ctx) {
@@ -46263,7 +46286,7 @@ var PanTool = class {
46263
46286
  }
46264
46287
  };
46265
46288
  //#endregion
46266
- //#region ../../src/scrawl/tools/selection.ts
46289
+ //#region src/renderer/selection/selection.ts
46267
46290
  /** The set of selected stroke ids. Drops ids the document no longer has. */
46268
46291
  var SelectionManager = class {
46269
46292
  constructor(doc) {
@@ -46314,7 +46337,7 @@ var SelectionManager = class {
46314
46337
  }
46315
46338
  };
46316
46339
  //#endregion
46317
- //#region ../../src/scrawl/tools/selectTool.ts
46340
+ //#region src/interaction/tools/selectTool.ts
46318
46341
  var HIT_SLOP_PX = 4;
46319
46342
  var CLICK_TOLERANCE_PX = 3;
46320
46343
  var MIN_SCALE = .05;
@@ -46530,7 +46553,7 @@ function safeRatio(numerator, denominator) {
46530
46553
  return raw;
46531
46554
  }
46532
46555
  //#endregion
46533
- //#region ../../src/scrawl/tools/customTool.ts
46556
+ //#region src/interaction/tools/customTool.ts
46534
46557
  function extensionIdFromToolId(toolId) {
46535
46558
  const slash = toolId.indexOf("/");
46536
46559
  return slash === -1 ? toolId : toolId.slice(0, slash);
@@ -46645,12 +46668,19 @@ var ExtensionCommandBatch = class {
46645
46668
  }
46646
46669
  };
46647
46670
  //#endregion
46648
- //#region ../../src/scrawl/ScrawlEngine.ts
46671
+ //#region src/composition/ScrawlEngine.ts
46649
46672
  /** Marker stroke width in board units (~1% of the default view height). */
46650
46673
  var BASE_WIDTH = .5;
46651
46674
  /** Highlighter chisel width — wide enough to cover a line of handwriting. */
46652
46675
  var HIGHLIGHT_WIDTH = 2.2;
46653
46676
  /**
46677
+ * Shape (rectangle, ellipse, arrow, ...) border width in board units.
46678
+ * Deliberately thinner than BASE_WIDTH: a shape border reads as a crisp,
46679
+ * structural line, not an expressive marker stroke — this is also the
46680
+ * fallback when no Host theme configures ScrawlTheme.shapeStrokeWidth.
46681
+ */
46682
+ var DEFAULT_SHAPE_STROKE_WIDTH = .15;
46683
+ /**
46654
46684
  * Identity of a rendered selection badge: which item, its lock state, and
46655
46685
  * where it sits on screen (rounded to the pixel, so sub-pixel camera drift
46656
46686
  * does not churn React).
@@ -46671,10 +46701,11 @@ function emptyExtensionRegistry() {
46671
46701
  * React never reaches past this class.
46672
46702
  */
46673
46703
  var ScrawlEngine = class {
46674
- constructor(canvas, documentId$1, extensionRegistry = emptyExtensionRegistry(), assetController = new AssetController(void 0), boardTheme = DEFAULT_BOARD_SURFACE_THEME) {
46704
+ constructor(canvas, documentId$1, extensionRegistry = emptyExtensionRegistry(), assetController = new AssetController(void 0), boardTheme = DEFAULT_BOARD_SURFACE_THEME, shapeStrokeWidth = DEFAULT_SHAPE_STROKE_WIDTH) {
46675
46705
  this.canvas = canvas;
46676
46706
  this.extensionRegistry = extensionRegistry;
46677
46707
  this.boardTheme = boardTheme;
46708
+ this.shapeStrokeWidth = shapeStrokeWidth;
46678
46709
  this.onToolChange = null;
46679
46710
  this.onZoomChange = null;
46680
46711
  this.onAuditEvent = null;
@@ -46769,6 +46800,7 @@ var ScrawlEngine = class {
46769
46800
  setTool: (t) => this.setTool(t),
46770
46801
  inkColor: () => this.tool === "highlighter" ? this.highlightColor : this.color,
46771
46802
  baseWidth: () => this.tool === "highlighter" ? HIGHLIGHT_WIDTH : BASE_WIDTH,
46803
+ shapeStrokeWidth: () => this.shapeStrokeWidth,
46772
46804
  activeTool: () => this.tool,
46773
46805
  isSpaceHeld: () => this.spaceHeld,
46774
46806
  pointerToBoard: (e) => pointerToBoard(e.clientX, e.clientY, canvas, this.scene.cameraRig.camera),
@@ -47033,6 +47065,10 @@ var ScrawlEngine = class {
47033
47065
  this.boardTheme = theme;
47034
47066
  this.scene.applyBoardTheme(theme);
47035
47067
  }
47068
+ /** Live update of the shape tool's border width (board units). Already-drawn shapes keep the width they were drawn with. */
47069
+ setShapeStrokeWidth(width) {
47070
+ this.shapeStrokeWidth = width;
47071
+ }
47036
47072
  /**
47037
47073
  * Snap to a peer's camera. No-op while drawing so pointer-to-board
47038
47074
  * mapping does not warp the in-progress stroke.
@@ -48094,7 +48130,7 @@ var ScrawlEngine = class {
48094
48130
  }
48095
48131
  };
48096
48132
  //#endregion
48097
- //#region src/controller-internal.ts
48133
+ //#region src/composition/controller-internal.ts
48098
48134
  function createBoardController(options) {
48099
48135
  return createBoardControllerWithEngine(options);
48100
48136
  }
@@ -48112,9 +48148,12 @@ function createBoardControllerWithEngine(options, existingEngine) {
48112
48148
  color: options.boardTheme?.surface ?? DEFAULT_BOARD_SURFACE_THEME.color,
48113
48149
  gridMode: options.boardTheme?.gridMode ?? DEFAULT_BOARD_SURFACE_THEME.gridMode,
48114
48150
  gridColor: options.boardTheme?.gridColor ?? DEFAULT_BOARD_SURFACE_THEME.gridColor,
48115
- gridSpacing: options.boardTheme?.gridSpacing ?? DEFAULT_BOARD_SURFACE_THEME.gridSpacing
48151
+ gridSpacing: options.boardTheme?.gridSpacing ?? DEFAULT_BOARD_SURFACE_THEME.gridSpacing,
48152
+ gridLineWidth: options.boardTheme?.gridLineWidth ?? DEFAULT_BOARD_SURFACE_THEME.gridLineWidth,
48153
+ surfaceTexture: options.boardTheme?.surfaceTexture ?? DEFAULT_BOARD_SURFACE_THEME.surfaceTexture
48116
48154
  };
48117
- const engine = existingEngine ?? (options.canvas ? new ScrawlEngine(options.canvas, options.document.id, extensionRegistry, assetController, currentBoardTheme) : null);
48155
+ let currentShapeStrokeWidth = options.boardTheme?.shapeStrokeWidth ?? .15;
48156
+ const engine = existingEngine ?? (options.canvas ? new ScrawlEngine(options.canvas, options.document.id, extensionRegistry, assetController, currentBoardTheme, currentShapeStrokeWidth) : null);
48118
48157
  const document = engine?.document ?? new BoardDocument(documentId(options.document.id));
48119
48158
  const history = engine?.history ?? new History(document);
48120
48159
  const createId = options.createId ?? (() => crypto.randomUUID());
@@ -48603,9 +48642,15 @@ function createBoardControllerWithEngine(options, existingEngine) {
48603
48642
  color: theme.surface ?? currentBoardTheme.color,
48604
48643
  gridMode: theme.gridMode ?? currentBoardTheme.gridMode,
48605
48644
  gridColor: theme.gridColor ?? currentBoardTheme.gridColor,
48606
- gridSpacing: theme.gridSpacing ?? currentBoardTheme.gridSpacing
48645
+ gridSpacing: theme.gridSpacing ?? currentBoardTheme.gridSpacing,
48646
+ gridLineWidth: theme.gridLineWidth ?? currentBoardTheme.gridLineWidth,
48647
+ surfaceTexture: theme.surfaceTexture ?? currentBoardTheme.surfaceTexture
48607
48648
  };
48608
48649
  engine?.applyBoardTheme(currentBoardTheme);
48650
+ if (theme.shapeStrokeWidth !== void 0) {
48651
+ currentShapeStrokeWidth = theme.shapeStrokeWidth;
48652
+ engine?.setShapeStrokeWidth(currentShapeStrokeWidth);
48653
+ }
48609
48654
  } },
48610
48655
  view: {
48611
48656
  fit() {
@@ -49376,7 +49421,7 @@ function createLocalBoard(options) {
49376
49421
  };
49377
49422
  }
49378
49423
  //#endregion
49379
- //#region src/theme.ts
49424
+ //#region src/ui/theme.ts
49380
49425
  var scrawlThemePresets = Object.freeze({
49381
49426
  light: Object.freeze({
49382
49427
  surface: "#ffffff",
@@ -49385,6 +49430,7 @@ var scrawlThemePresets = Object.freeze({
49385
49430
  text: "#1c1c1a",
49386
49431
  textMuted: "#62625c",
49387
49432
  edge: "#d4d4cc",
49433
+ primary: "#1856a3",
49388
49434
  focus: "#1856a3",
49389
49435
  selection: "#d9e8f7",
49390
49436
  danger: "#a3302b",
@@ -49403,9 +49449,12 @@ var scrawlThemePresets = Object.freeze({
49403
49449
  motionEasing: "cubic-bezier(0.2, 0, 0, 1)",
49404
49450
  density: "comfortable",
49405
49451
  boardSurface: "#f5f5f3",
49452
+ boardSurfaceTexture: "flat",
49406
49453
  gridMode: "none",
49407
49454
  gridColor: "#deded8",
49408
- gridSpacing: 5
49455
+ gridSpacing: 5,
49456
+ gridLineWidth: 1,
49457
+ shapeStrokeWidth: .15
49409
49458
  }),
49410
49459
  dark: Object.freeze({
49411
49460
  surface: "#171816",
@@ -49414,6 +49463,7 @@ var scrawlThemePresets = Object.freeze({
49414
49463
  text: "#f4f4ee",
49415
49464
  textMuted: "#b7b8ae",
49416
49465
  edge: "#484a42",
49466
+ primary: "#9dc8ef",
49417
49467
  focus: "#9dc8ef",
49418
49468
  selection: "#314b63",
49419
49469
  danger: "#ffaaa3",
@@ -49432,9 +49482,12 @@ var scrawlThemePresets = Object.freeze({
49432
49482
  motionEasing: "cubic-bezier(0.2, 0, 0, 1)",
49433
49483
  density: "comfortable",
49434
49484
  boardSurface: "#232420",
49485
+ boardSurfaceTexture: "flat",
49435
49486
  gridMode: "none",
49436
49487
  gridColor: "#33352f",
49437
- gridSpacing: 5
49488
+ gridSpacing: 5,
49489
+ gridLineWidth: 1,
49490
+ shapeStrokeWidth: .15
49438
49491
  })
49439
49492
  });
49440
49493
  var variableNames = {
@@ -49444,6 +49497,7 @@ var variableNames = {
49444
49497
  text: "--scrawl-color-text",
49445
49498
  textMuted: "--scrawl-color-text-muted",
49446
49499
  edge: "--scrawl-color-edge",
49500
+ primary: "--scrawl-color-primary",
49447
49501
  focus: "--scrawl-color-focus",
49448
49502
  selection: "--scrawl-color-selection",
49449
49503
  danger: "--scrawl-color-danger",
@@ -49462,9 +49516,12 @@ var variableNames = {
49462
49516
  motionEasing: "--scrawl-motion-easing",
49463
49517
  density: "--scrawl-density",
49464
49518
  boardSurface: "--scrawl-board-surface",
49519
+ boardSurfaceTexture: "--scrawl-board-surface-texture",
49465
49520
  gridMode: "--scrawl-grid-mode",
49466
49521
  gridColor: "--scrawl-grid-color",
49467
- gridSpacing: "--scrawl-grid-spacing"
49522
+ gridSpacing: "--scrawl-grid-spacing",
49523
+ gridLineWidth: "--scrawl-grid-line-width",
49524
+ shapeStrokeWidth: "--scrawl-shape-stroke-width"
49468
49525
  };
49469
49526
  var colorTokens = /* @__PURE__ */ new Set([
49470
49527
  "surface",
@@ -49473,6 +49530,7 @@ var colorTokens = /* @__PURE__ */ new Set([
49473
49530
  "text",
49474
49531
  "textMuted",
49475
49532
  "edge",
49533
+ "primary",
49476
49534
  "focus",
49477
49535
  "selection",
49478
49536
  "danger",
@@ -49520,10 +49578,22 @@ function validateScrawlTheme(theme) {
49520
49578
  token,
49521
49579
  message: "Expected none, line, or dot"
49522
49580
  });
49581
+ else if (token === "boardSurfaceTexture" && value !== "flat" && value !== "textured") diagnostics.push({
49582
+ token,
49583
+ message: "Expected flat or textured"
49584
+ });
49523
49585
  else if (token === "gridSpacing" && (!finiteNumber(value) || value < .5 || value > 50)) diagnostics.push({
49524
49586
  token,
49525
49587
  message: "Expected spacing 0.5-50 board units"
49526
49588
  });
49589
+ else if (token === "gridLineWidth" && (!finiteNumber(value) || value < .5 || value > 8)) diagnostics.push({
49590
+ token,
49591
+ message: "Expected line width 0.5-8px"
49592
+ });
49593
+ else if (token === "shapeStrokeWidth" && (!finiteNumber(value) || value < .01 || value > 5)) diagnostics.push({
49594
+ token,
49595
+ message: "Expected stroke width 0.01-5 board units"
49596
+ });
49527
49597
  else if ((token.endsWith("Family") || token.startsWith("elevation") || token === "motionEasing") && (typeof value !== "string" || !value.trim() || /[;{}]/.test(value))) diagnostics.push({
49528
49598
  token,
49529
49599
  message: "Expected a safe CSS value"
@@ -49594,12 +49664,12 @@ function luminance(color) {
49594
49664
  return channels[0] * .2126 + channels[1] * .7152 + channels[2] * .0722;
49595
49665
  }
49596
49666
  function formatValue(token, value) {
49597
- if (token === "baseFontSize" || token === "controlRadius" || token === "panelRadius") return `${value}px`;
49667
+ if (token === "baseFontSize" || token === "controlRadius" || token === "panelRadius" || token === "gridLineWidth") return `${value}px`;
49598
49668
  if (token === "motionDuration") return `${value}ms`;
49599
49669
  return String(value);
49600
49670
  }
49601
49671
  //#endregion
49602
- //#region src/inline-editing.tsx
49672
+ //#region src/ui/dialogs/inline-editing.tsx
49603
49673
  /** Inline note, text, and table-cell editors — driven entirely by public controller events and commands. */
49604
49674
  function InlineEditors({ controller, renderPortal }) {
49605
49675
  const [editor, setEditor] = useState(null);
@@ -49830,7 +49900,7 @@ function InlineEditors({ controller, renderPortal }) {
49830
49900
  }))] });
49831
49901
  }
49832
49902
  //#endregion
49833
- //#region src/style-shelf.tsx
49903
+ //#region src/ui/toolbar/style-shelf.tsx
49834
49904
  var ERASER_RADII = [
49835
49905
  ["Fine", 1],
49836
49906
  ["Medium", 2],
@@ -49993,7 +50063,7 @@ function StyleShelf({ controller, snapshot }) {
49993
50063
  });
49994
50064
  }
49995
50065
  //#endregion
49996
- //#region src/default-ui.tsx
50066
+ //#region src/ui/default-ui.tsx
49997
50067
  var PRIMARY_TOOLS = [
49998
50068
  {
49999
50069
  tool: "select",
@@ -50102,7 +50172,7 @@ var INKS = [
50102
50172
  ["Forest", "#315d46"],
50103
50173
  ["Oxblood", "#773f3b"]
50104
50174
  ];
50105
- function DefaultBoardChrome({ controller, snapshot, renderPortal, className, style, regions, slots }) {
50175
+ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, style, regions, slots, icons }) {
50106
50176
  const [panel, setPanel] = useState(null);
50107
50177
  const [query, setQuery] = useState("");
50108
50178
  const [announcement, setAnnouncement] = useState("Board controls ready");
@@ -50256,7 +50326,10 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
50256
50326
  onClick: () => chooseTool(tool),
50257
50327
  title: `${label} (${key})`,
50258
50328
  type: "button",
50259
- children: label
50329
+ children: icons?.[tool] ? /* @__PURE__ */ jsx("span", {
50330
+ "aria-hidden": "true",
50331
+ children: icons[tool]
50332
+ }) : label
50260
50333
  }, tool)), /* @__PURE__ */ jsxs("label", {
50261
50334
  className: "scrawl-board__tool-select",
50262
50335
  children: [/* @__PURE__ */ jsx("span", { children: "More tools" }), /* @__PURE__ */ jsxs("select", {
@@ -50522,7 +50595,7 @@ function download(blob, name) {
50522
50595
  URL.revokeObjectURL(url);
50523
50596
  }
50524
50597
  //#endregion
50525
- //#region src/presence.tsx
50598
+ //#region src/ui/panels/presence.tsx
50526
50599
  var labelStyle = {
50527
50600
  borderRadius: 999,
50528
50601
  color: "#fff",
@@ -50682,9 +50755,12 @@ function ScrawlProvider({ controller, children, preset = "light", theme, portalC
50682
50755
  useEffect(() => {
50683
50756
  controller.boardTheme.set({
50684
50757
  surface: resolved.values.boardSurface,
50758
+ surfaceTexture: resolved.values.boardSurfaceTexture,
50685
50759
  gridMode: resolved.values.gridMode,
50686
50760
  gridColor: resolved.values.gridColor,
50687
- gridSpacing: resolved.values.gridSpacing
50761
+ gridSpacing: resolved.values.gridSpacing,
50762
+ gridLineWidth: resolved.values.gridLineWidth,
50763
+ shapeStrokeWidth: resolved.values.shapeStrokeWidth
50688
50764
  });
50689
50765
  }, [controller, resolved]);
50690
50766
  useDeferredDisposal(controller, disposeOnUnmount);
@@ -50716,7 +50792,7 @@ function ScrawlProvider({ controller, children, preset = "light", theme, portalC
50716
50792
  })
50717
50793
  });
50718
50794
  }
50719
- function Scrawl({ children, preset, theme, portalContainer, className, style, onReady, onError, onThemeDiagnostic, canvas: suppliedCanvas, ...options }) {
50795
+ function Scrawl({ children, preset, theme, portalContainer, className, style, onReady, onError, onThemeDiagnostic, canvas: suppliedCanvas, icons, ...options }) {
50720
50796
  const [runtime, setRuntime] = useState(null);
50721
50797
  const runtimeRef = useRef(null);
50722
50798
  const ownedCanvasRef = useRef(null);
@@ -50737,9 +50813,12 @@ function Scrawl({ children, preset, theme, portalContainer, className, style, on
50737
50813
  const resolved = resolveScrawlTheme(preset, theme);
50738
50814
  const boardTheme = {
50739
50815
  surface: resolved.values.boardSurface,
50816
+ surfaceTexture: resolved.values.boardSurfaceTexture,
50740
50817
  gridMode: resolved.values.gridMode,
50741
50818
  gridColor: resolved.values.gridColor,
50742
- gridSpacing: resolved.values.gridSpacing
50819
+ gridSpacing: resolved.values.gridSpacing,
50820
+ gridLineWidth: resolved.values.gridLineWidth,
50821
+ shapeStrokeWidth: resolved.values.shapeStrokeWidth
50743
50822
  };
50744
50823
  controller = createBoardController({
50745
50824
  ...options,
@@ -50809,7 +50888,7 @@ function Scrawl({ children, preset, theme, portalContainer, className, style, on
50809
50888
  onThemeDiagnostic,
50810
50889
  className,
50811
50890
  style,
50812
- children: [runtime.canvas && /* @__PURE__ */ jsx(ScrawlCanvas, { element: runtime.canvas }), children ?? /* @__PURE__ */ jsx(ScrawlDefaultUI, {})]
50891
+ children: [runtime.canvas && /* @__PURE__ */ jsx(ScrawlCanvas, { element: runtime.canvas }), children ?? /* @__PURE__ */ jsx(ScrawlDefaultUI, { icons })]
50813
50892
  });
50814
50893
  }
50815
50894
  function ScrawlCanvas({ element, className, style, "aria-label": ariaLabel = "Scrawl Board" }) {
@@ -50845,10 +50924,11 @@ function ScrawlCanvas({ element, className, style, "aria-label": ariaLabel = "Sc
50845
50924
  style
50846
50925
  });
50847
50926
  }
50848
- function ScrawlDefaultUI({ className, style, regions, slots }) {
50927
+ function ScrawlDefaultUI({ className, style, regions, slots, icons }) {
50849
50928
  return /* @__PURE__ */ jsx(DefaultBoardChrome, {
50850
50929
  className,
50851
50930
  controller: useScrawlController(),
50931
+ icons,
50852
50932
  regions,
50853
50933
  renderPortal: (children) => /* @__PURE__ */ jsx(ScrawlPortal, { children }),
50854
50934
  slots,