@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/react.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
  function isAssetRef(value) {
7
7
  return typeof value === "string" && ASSET_REF_PATTERN.test(value) && new TextEncoder().encode(value).length <= 512;
@@ -33,7 +33,7 @@ function clampAssetCacheBytes(value) {
33
33
  return Math.min(ASSET_CACHE_BYTES_MAX, Math.max(ASSET_CACHE_BYTES_MIN, Math.floor(value)));
34
34
  }
35
35
  //#endregion
36
- //#region src/core-internal/identifiers.ts
36
+ //#region src/core/document/identifiers.ts
37
37
  function documentId(value) {
38
38
  if (!value) throw new TypeError("DocumentId cannot be empty");
39
39
  return value;
@@ -234,7 +234,7 @@ function validation(message, path) {
234
234
  return new DocumentRecoveryError("DOCUMENT_VALIDATION_FAILED", message, void 0, path);
235
235
  }
236
236
  //#endregion
237
- //#region src/core-internal/affine2d.ts
237
+ //#region src/core/shapes/affine2d.ts
238
238
  var IDENTITY = [
239
239
  1,
240
240
  0,
@@ -318,7 +318,7 @@ function avgScale(m) {
318
318
  return (Math.hypot(m[0], m[1]) + Math.hypot(m[2], m[3])) / 2;
319
319
  }
320
320
  //#endregion
321
- //#region src/core-internal/boardSearch.ts
321
+ //#region src/core/document/boardSearch.ts
322
322
  function hay(s) {
323
323
  return s.toLowerCase();
324
324
  }
@@ -383,7 +383,7 @@ function searchBoard(query, board) {
383
383
  return hits;
384
384
  }
385
385
  //#endregion
386
- //#region src/core-internal/types.ts
386
+ //#region src/core/shapes/types.ts
387
387
  /** Erasure level at which a point counts as fully erased. */
388
388
  var ERASE_THRESHOLD = .95;
389
389
  function cloneStroke(stroke) {
@@ -455,7 +455,7 @@ function cloneImage(img) {
455
455
  return { ...img };
456
456
  }
457
457
  //#endregion
458
- //#region src/core-internal/clusterStore.ts
458
+ //#region src/core/document/clusterStore.ts
459
459
  /** A stroke joins a cluster last written to within this window at full reach. */
460
460
  var RECENT_MS = 8e3;
461
461
  /** Older clusters are still joinable, at half the gap threshold. */
@@ -626,7 +626,7 @@ function clamp$3(v, lo, hi) {
626
626
  return Math.min(hi, Math.max(lo, v));
627
627
  }
628
628
  //#endregion
629
- //#region src/core-internal/extensionTypes.ts
629
+ //#region src/core/shapes/extensionTypes.ts
630
630
  function cloneCustomObject(object) {
631
631
  return {
632
632
  ...object,
@@ -636,7 +636,7 @@ function cloneCustomObject(object) {
636
636
  };
637
637
  }
638
638
  //#endregion
639
- //#region src/core-internal/kitchenTimer.ts
639
+ //#region src/core/shapes/kitchenTimer.ts
640
640
  var TIMER_DEFAULT_DURATION_MS = 300 * 1e3;
641
641
  var TIMER_PRESETS_MS = [
642
642
  6e4,
@@ -692,7 +692,7 @@ function formatTimer(ms) {
692
692
  return `${Math.floor(total / 60)}:${(total % 60).toString().padStart(2, "0")}`;
693
693
  }
694
694
  //#endregion
695
- //#region src/core-internal/itemLock.ts
695
+ //#region src/core/shapes/itemLock.ts
696
696
  /**
697
697
  * Apply or clear a lock in place. Unlock always drops the holder so a stale
698
698
  * name cannot linger on an unlocked item.
@@ -729,7 +729,7 @@ function serializeLock(item) {
729
729
  };
730
730
  }
731
731
  //#endregion
732
- //#region src/core-internal/document.ts
732
+ //#region src/core/document/document.ts
733
733
  /**
734
734
  * The one place a live Stroke becomes its wire representation — in
735
735
  * particular, points collapse from {x,y,pressure,erase?} objects into
@@ -1288,7 +1288,7 @@ function computeBBox(stroke) {
1288
1288
  return world;
1289
1289
  }
1290
1290
  //#endregion
1291
- //#region src/core-internal/commands.ts
1291
+ //#region src/core/commands/commands.ts
1292
1292
  /** One drawing action — a marker stroke, or a shape's strokes as one unit. */
1293
1293
  var AddStrokesCommand = class {
1294
1294
  constructor(strokes) {
@@ -1630,7 +1630,7 @@ var LockItemsCommand = class {
1630
1630
  }
1631
1631
  };
1632
1632
  //#endregion
1633
- //#region src/core-internal/history.ts
1633
+ //#region src/core/history/history.ts
1634
1634
  var LIMIT = 200;
1635
1635
  var History = class {
1636
1636
  constructor(doc) {
@@ -1691,7 +1691,7 @@ var History = class {
1691
1691
  }
1692
1692
  };
1693
1693
  //#endregion
1694
- //#region src/core-internal/ops.ts
1694
+ //#region src/core/events/ops.ts
1695
1695
  var ADDED = [
1696
1696
  ["added", "strokes"],
1697
1697
  ["notesAdded", "notes"],
@@ -1745,10 +1745,18 @@ function changeToOps(change, restoring = false) {
1745
1745
  return ops;
1746
1746
  }
1747
1747
  //#endregion
1748
- //#region src/core-internal/ribbonEdges.ts
1748
+ //#region src/core/shapes/ribbonEdges.ts
1749
1749
  var MIN_WIDTH_FACTOR = .35;
1750
1750
  var END_TAPER = .55;
1751
- function ribbonEdges(points, baseWidth) {
1751
+ /**
1752
+ * @param handDrawn Default `true`: ink's organic feel — width follows
1753
+ * per-point pressure, and both ends taper. Pass `false` for a geometric
1754
+ * shape outline, which has no real pressure signal and isn't a pen stroke
1755
+ * with a lift-off: it renders at the exact `baseWidth`, uniformly, with no
1756
+ * taper at its start/end (which, for a closed shape, is the same point —
1757
+ * tapering it would visibly pinch just that one corner).
1758
+ */
1759
+ function ribbonEdges(points, baseWidth, handDrawn = true) {
1752
1760
  const pts = points.length === 1 ? [points[0], {
1753
1761
  ...points[0],
1754
1762
  x: points[0].x + baseWidth * .05
@@ -1763,8 +1771,8 @@ function ribbonEdges(points, baseWidth) {
1763
1771
  const len = Math.hypot(dx, dy) || 1;
1764
1772
  dx /= len;
1765
1773
  dy /= len;
1766
- let width = baseWidth * (MIN_WIDTH_FACTOR + (1 - MIN_WIDTH_FACTOR) * pts[i].pressure);
1767
- if (i === 0 || i === n - 1) width *= END_TAPER;
1774
+ let width = handDrawn ? baseWidth * (MIN_WIDTH_FACTOR + (1 - MIN_WIDTH_FACTOR) * pts[i].pressure) : baseWidth;
1775
+ if (handDrawn && (i === 0 || i === n - 1)) width *= END_TAPER;
1768
1776
  const hw = width / 2;
1769
1777
  out[i] = {
1770
1778
  lx: pts[i].x - dy * hw,
@@ -1777,7 +1785,7 @@ function ribbonEdges(points, baseWidth) {
1777
1785
  return out;
1778
1786
  }
1779
1787
  //#endregion
1780
- //#region src/core-internal/spatialIndex.ts
1788
+ //#region src/core/document/spatialIndex.ts
1781
1789
  var CELL = 8;
1782
1790
  var SpatialIndex = class {
1783
1791
  constructor(doc) {
@@ -1850,7 +1858,7 @@ function cellsOf(box) {
1850
1858
  return keys;
1851
1859
  }
1852
1860
  //#endregion
1853
- //#region src/core-internal/stamps.ts
1861
+ //#region src/core/shapes/stamps.ts
1854
1862
  var STAMPS = [
1855
1863
  {
1856
1864
  kind: "star",
@@ -1895,7 +1903,7 @@ function stampDataUrl(kind) {
1895
1903
  return `data:image/svg+xml;utf8,${encodeURIComponent(SVG[kind])}`;
1896
1904
  }
1897
1905
  //#endregion
1898
- //#region src/core-internal/presence.ts
1906
+ //#region src/collaboration/presence/presence.ts
1899
1907
  /** Keep beacons clear of typical chrome at the top and bottom of the Board. */
1900
1908
  var BEACON_INSET = {
1901
1909
  top: 72,
@@ -1931,7 +1939,7 @@ function clamp$2(value, min, max) {
1931
1939
  return Math.min(max, Math.max(min, value));
1932
1940
  }
1933
1941
  //#endregion
1934
- //#region src/core-internal/svg.ts
1942
+ //#region src/persistence/serialization/svg.ts
1935
1943
  /** Matches the key light used by the board shader and note shadows. */
1936
1944
  var LIGHT = {
1937
1945
  x: -.25,
@@ -2073,7 +2081,7 @@ function strokeToPaths(stroke) {
2073
2081
  y,
2074
2082
  pressure,
2075
2083
  ...erase > 0 ? { erase } : {}
2076
- })), stroke.baseWidth);
2084
+ })), stroke.baseWidth, stroke.tool !== "shape");
2077
2085
  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])})"` : "";
2078
2086
  const baseOpacity = stroke.tool === "highlighter" ? .4 : 1;
2079
2087
  const paths = [];
@@ -2298,7 +2306,7 @@ function fmt(n) {
2298
2306
  return String(Math.round(n * 100) / 100);
2299
2307
  }
2300
2308
  //#endregion
2301
- //#region src/core-internal/sceneGeometry.ts
2309
+ //#region src/core/shapes/sceneGeometry.ts
2302
2310
  var EMPTY_BOUNDS = {
2303
2311
  x: 0,
2304
2312
  y: 0,
@@ -2362,7 +2370,7 @@ function textCorners(x, y, text, fontSize, align, m) {
2362
2370
  return rectCorners(x + (align === "center" ? -width / 2 : align === "end" ? -width : 0), y, width, height, m);
2363
2371
  }
2364
2372
  //#endregion
2365
- //#region src/core-internal/customObjects.ts
2373
+ //#region src/core/shapes/customObjects.ts
2366
2374
  var FALLBACK_LABEL_MAX_LENGTH = 80;
2367
2375
  function toReadonly(object) {
2368
2376
  return object;
@@ -2491,7 +2499,7 @@ function extensionIdFromType(type) {
2491
2499
  return slash === -1 ? type : type.slice(0, slash);
2492
2500
  }
2493
2501
  //#endregion
2494
- //#region src/core-internal/extensionRegistry.ts
2502
+ //#region src/core/shapes/extensionRegistry.ts
2495
2503
  function buildExtensionRegistry(extensions) {
2496
2504
  const list = extensions ?? [];
2497
2505
  const diagnostics = [];
@@ -2588,7 +2596,7 @@ function isWellFormedNamespacedId(id) {
2588
2596
  return isWellFormedId(id) && id.includes("/") && !id.startsWith("/") && !id.endsWith("/");
2589
2597
  }
2590
2598
  //#endregion
2591
- //#region src/core-internal/assetValidation.ts
2599
+ //#region src/persistence/adapters/assetValidation.ts
2592
2600
  function sniffImageMediaType(bytes) {
2593
2601
  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";
2594
2602
  if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
@@ -2675,7 +2683,7 @@ function validateDecodedDimensions(width, height) {
2675
2683
  };
2676
2684
  }
2677
2685
  //#endregion
2678
- //#region src/assetController.ts
2686
+ //#region src/persistence/adapters/assetController.ts
2679
2687
  var FAILURE_COALESCE_MS = 4e3;
2680
2688
  var AssetController = class {
2681
2689
  constructor(resolver, options) {
@@ -2854,7 +2862,7 @@ function toAssetResolutionError(cause, ref) {
2854
2862
  return new AssetResolutionError("unknown", true, cause instanceof Error ? cause.message : String(cause), ref);
2855
2863
  }
2856
2864
  //#endregion
2857
- //#region src/core-internal/assetExport.ts
2865
+ //#region src/persistence/serialization/assetRefCollector.ts
2858
2866
  /** Every unique Asset reference in the document, and which object(s) use it. */
2859
2867
  function collectDocumentAssetRefs(doc, registry) {
2860
2868
  const refs = /* @__PURE__ */ new Map();
@@ -2881,7 +2889,7 @@ function collectSceneAssetRefs(node, objectId, add) {
2881
2889
  if (node.kind === "group") for (const child of node.children) collectSceneAssetRefs(child, objectId, add);
2882
2890
  }
2883
2891
  //#endregion
2884
- //#region src/assetExport.ts
2892
+ //#region src/persistence/serialization/assetExport.ts
2885
2893
  /** Strict mode's rejection — never contains credentials, a fetchable location, or a raw resolver message. */
2886
2894
  var SvgAssetExportError = class extends Error {
2887
2895
  constructor(missingAssets) {
@@ -2937,7 +2945,7 @@ function toDataUri(bytes, mediaType) {
2937
2945
  return `data:${mediaType};base64,${btoa(binary)}`;
2938
2946
  }
2939
2947
  //#endregion
2940
- //#region src/assetIngestion.ts
2948
+ //#region src/persistence/adapters/assetIngestion.ts
2941
2949
  async function decodeDimensionsWithImageBitmap(bytes, mediaType) {
2942
2950
  const blob = new Blob([bytes], { type: mediaType });
2943
2951
  let bitmap;
@@ -35583,7 +35591,7 @@ var WebGLRenderer = class {
35583
35591
  }
35584
35592
  };
35585
35593
  //#endregion
35586
- //#region ../../src/scrawl/data/auditEvents.ts
35594
+ //#region src/core/events/auditEvents.ts
35587
35595
  var BY_LABEL = {
35588
35596
  draw: {
35589
35597
  action: "create",
@@ -35703,7 +35711,7 @@ function lowerFirst(s) {
35703
35711
  return s.charAt(0).toLowerCase() + s.slice(1);
35704
35712
  }
35705
35713
  //#endregion
35706
- //#region ../../src/scrawl/assets/AssetTextureCache.ts
35714
+ //#region src/renderer/shapes/AssetTextureCache.ts
35707
35715
  var AssetTextureCache = class {
35708
35716
  constructor(assetController) {
35709
35717
  this.assetController = assetController;
@@ -35794,7 +35802,7 @@ function toDecodeError(cause, ref) {
35794
35802
  return new AssetResolutionError("decode-failed", false, cause instanceof Error ? cause.message : String(cause), ref);
35795
35803
  }
35796
35804
  //#endregion
35797
- //#region ../../src/scrawl/engine/raycast.ts
35805
+ //#region src/renderer/canvas/raycast.ts
35798
35806
  var origin$1 = new Vector3();
35799
35807
  var target = new Vector3();
35800
35808
  function pointerToBoard(clientX, clientY, canvas, camera) {
@@ -35825,7 +35833,7 @@ function pointerToPlane(clientX, clientY, canvas, camera, z) {
35825
35833
  };
35826
35834
  }
35827
35835
  //#endregion
35828
- //#region ../../src/scrawl/engine/fog.ts
35836
+ //#region src/renderer/canvas/fog.ts
35829
35837
  function createFogUniforms(color) {
35830
35838
  return {
35831
35839
  uFogColor: { value: new Color(color) },
@@ -35846,18 +35854,24 @@ var FOG_GLSL = `
35846
35854
  }
35847
35855
  `;
35848
35856
  //#endregion
35849
- //#region ../../src/scrawl/engine/boardSurface.ts
35857
+ //#region src/renderer/canvas/boardSurface.ts
35850
35858
  var GRID_MODE_CODE = {
35851
35859
  none: 0,
35852
35860
  line: 1,
35853
35861
  dot: 2
35854
35862
  };
35863
+ var TEXTURE_CODE = {
35864
+ flat: 0,
35865
+ textured: 1
35866
+ };
35855
35867
  /** Used when a caller doesn't supply one (e.g. ScrawlEngine constructed without a Host theme). */
35856
35868
  var DEFAULT_BOARD_SURFACE_THEME = {
35857
35869
  color: "#f5f5f3",
35858
35870
  gridMode: "none",
35859
35871
  gridColor: "#deded8",
35860
- gridSpacing: 5
35872
+ gridSpacing: 5,
35873
+ gridLineWidth: 1,
35874
+ surfaceTexture: "flat"
35861
35875
  };
35862
35876
  /** Grid alpha fades out below this on-screen spacing (px) — otherwise it aliases into a solid wash when zoomed out. */
35863
35877
  var GRID_FADE_MIN_PX = 3;
@@ -35878,8 +35892,10 @@ var FRAGMENT = `
35878
35892
  uniform float uGridMode;
35879
35893
  uniform vec3 uGridColor;
35880
35894
  uniform float uGridSpacing;
35881
- uniform float uGridLineWorld;
35895
+ uniform float uWorldPerPixel;
35896
+ uniform float uGridLineWidthPx;
35882
35897
  uniform float uGridFade;
35898
+ uniform float uSurfaceTexture;
35883
35899
  varying vec3 vWorldPos;
35884
35900
 
35885
35901
  float hash(vec2 p) {
@@ -35899,37 +35915,45 @@ var FRAGMENT = `
35899
35915
  ${FOG_GLSL}
35900
35916
 
35901
35917
  // Distance from p's nearest axis-aligned grid line, in world units.
35918
+ // A crisp, ~uGridLineWidthPx-wide line: full alpha to its half-width, a
35919
+ // ~1px anti-aliased falloff beyond that — not a wide, soft-edged band.
35902
35920
  float gridLineAlpha(vec2 p) {
35903
35921
  vec2 cell = mod(p, uGridSpacing);
35904
35922
  vec2 distToLine = min(cell, uGridSpacing - cell);
35905
- vec2 lineAlpha2 = 1.0 - smoothstep(uGridLineWorld, uGridLineWorld * 2.0, distToLine);
35923
+ float halfWidth = 0.5 * uGridLineWidthPx * uWorldPerPixel;
35924
+ float aa = uWorldPerPixel;
35925
+ vec2 lineAlpha2 = 1.0 - smoothstep(halfWidth - aa * 0.5, halfWidth + aa * 0.5, distToLine);
35906
35926
  return max(lineAlpha2.x, lineAlpha2.y);
35907
35927
  }
35908
35928
 
35909
- // Distance from p's nearest grid intersection, in world units.
35929
+ // Distance from p's nearest grid intersection, in world units. Dot radius
35930
+ // matches the configured line width in pixels, same anti-aliasing.
35910
35931
  float gridDotAlpha(vec2 p) {
35911
35932
  vec2 cell = mod(p, uGridSpacing) - uGridSpacing * 0.5;
35912
35933
  float dist = length(cell);
35913
- float radius = uGridLineWorld * 1.5;
35914
- return 1.0 - smoothstep(radius, radius * 2.0, dist);
35934
+ float radius = uGridLineWidthPx * uWorldPerPixel;
35935
+ float aa = uWorldPerPixel;
35936
+ return 1.0 - smoothstep(radius - aa * 0.5, radius + aa * 0.5, dist);
35915
35937
  }
35916
35938
 
35917
35939
  void main() {
35918
35940
  vec3 col = uColor;
35919
35941
 
35920
- // Melamine micro-texture: two octaves of value noise, +-1.75% brightness.
35921
- float n = vnoise(vWorldPos.xy * 2.3) * 0.6 + vnoise(vWorldPos.xy * 9.7) * 0.4;
35922
- col *= 1.0 + (n - 0.5) * 0.035;
35942
+ if (uSurfaceTexture > 0.5) {
35943
+ // Melamine micro-texture: two octaves of value noise, +-1.75% brightness.
35944
+ float n = vnoise(vWorldPos.xy * 2.3) * 0.6 + vnoise(vWorldPos.xy * 9.7) * 0.4;
35945
+ col *= 1.0 + (n - 0.5) * 0.035;
35923
35946
 
35924
- // Satin sheen: Fresnel rim + a specular band from the key light. Both are
35925
- // ~zero in the straight-on working view and bloom as the camera tilts.
35926
- vec3 V = normalize(cameraPosition - vWorldPos);
35927
- vec3 N = vec3(0.0, 0.0, 1.0);
35928
- float fres = pow(1.0 - max(dot(N, V), 0.0), 3.0);
35929
- vec3 L = normalize(vec3(-0.25, 0.4, 0.88));
35930
- vec3 H = normalize(L + V);
35931
- float spec = pow(max(dot(N, H), 0.0), 90.0);
35932
- col += (fres * 0.18 + spec * 0.06) * vec3(1.0);
35947
+ // Satin sheen: Fresnel rim + a specular band from the key light. Both
35948
+ // are ~zero in the straight-on working view and bloom as the camera tilts.
35949
+ vec3 V = normalize(cameraPosition - vWorldPos);
35950
+ vec3 N = vec3(0.0, 0.0, 1.0);
35951
+ float fres = pow(1.0 - max(dot(N, V), 0.0), 3.0);
35952
+ vec3 L = normalize(vec3(-0.25, 0.4, 0.88));
35953
+ vec3 H = normalize(L + V);
35954
+ float spec = pow(max(dot(N, H), 0.0), 90.0);
35955
+ col += (fres * 0.18 + spec * 0.06) * vec3(1.0);
35956
+ }
35933
35957
 
35934
35958
  if (uGridMode > 0.5 && uGridFade > 0.001) {
35935
35959
  float gridAlpha = (uGridMode < 1.5 ? gridLineAlpha(vWorldPos.xy) : gridDotAlpha(vWorldPos.xy)) * uGridFade;
@@ -35949,8 +35973,10 @@ function createBoardSurface(fog, theme) {
35949
35973
  uGridMode: { value: GRID_MODE_CODE[theme.gridMode] },
35950
35974
  uGridColor: { value: new Color(theme.gridColor) },
35951
35975
  uGridSpacing: { value: Math.max(.001, theme.gridSpacing) },
35952
- uGridLineWorld: { value: .05 },
35976
+ uGridLineWidthPx: { value: Math.max(.1, theme.gridLineWidth) },
35977
+ uWorldPerPixel: { value: .05 },
35953
35978
  uGridFade: { value: 0 },
35979
+ uSurfaceTexture: { value: TEXTURE_CODE[theme.surfaceTexture] },
35954
35980
  ...fog
35955
35981
  }
35956
35982
  });
@@ -35958,13 +35984,15 @@ function createBoardSurface(fog, theme) {
35958
35984
  mesh.name = "board-surface";
35959
35985
  return mesh;
35960
35986
  }
35961
- /** Applies a theme change (initial or live) — everything except the per-frame fade/line-width. */
35987
+ /** Applies a theme change (initial or live) — everything except the per-frame fade/pixel-ratio. */
35962
35988
  function setBoardSurfaceTheme(mesh, theme) {
35963
35989
  const uniforms = mesh.material.uniforms;
35964
35990
  uniforms.uColor.value.set(theme.color);
35965
35991
  uniforms.uGridMode.value = GRID_MODE_CODE[theme.gridMode];
35966
35992
  uniforms.uGridColor.value.set(theme.gridColor);
35967
35993
  uniforms.uGridSpacing.value = Math.max(.001, theme.gridSpacing);
35994
+ uniforms.uGridLineWidthPx.value = Math.max(.1, theme.gridLineWidth);
35995
+ uniforms.uSurfaceTexture.value = TEXTURE_CODE[theme.surfaceTexture];
35968
35996
  }
35969
35997
  /** Keep the plane centered under the view, larger than the fog horizon, and the grid's on-screen weight/fade current. */
35970
35998
  function updateBoardSurface(mesh, centerX, centerY, fogFar, worldPerPixel) {
@@ -35972,7 +36000,7 @@ function updateBoardSurface(mesh, centerX, centerY, fogFar, worldPerPixel) {
35972
36000
  const size = fogFar * 2.5;
35973
36001
  mesh.scale.set(size, size, 1);
35974
36002
  const uniforms = mesh.material.uniforms;
35975
- uniforms.uGridLineWorld.value = worldPerPixel;
36003
+ uniforms.uWorldPerPixel.value = worldPerPixel;
35976
36004
  const t = (uniforms.uGridSpacing.value / Math.max(1e-6, worldPerPixel) - GRID_FADE_MIN_PX) / (GRID_FADE_MAX_PX - GRID_FADE_MIN_PX);
35977
36005
  uniforms.uGridFade.value = MathUtils.clamp(t, 0, 1);
35978
36006
  }
@@ -36145,7 +36173,7 @@ var CameraRig = class {
36145
36173
  }
36146
36174
  };
36147
36175
  //#endregion
36148
- //#region ../../src/scrawl/engine/scene.ts
36176
+ //#region src/renderer/canvas/scene.ts
36149
36177
  var SceneRig = class {
36150
36178
  constructor(canvas, boardTheme) {
36151
36179
  this.renderer = new WebGLRenderer({
@@ -36192,7 +36220,7 @@ var SceneRig = class {
36192
36220
  }
36193
36221
  };
36194
36222
  //#endregion
36195
- //#region ../../src/scrawl/ink/inkMaterial.ts
36223
+ //#region src/renderer/shapes/inkMaterial.ts
36196
36224
  var VERTEX = `
36197
36225
  attribute float aAlpha;
36198
36226
  varying vec2 vUv;
@@ -36323,15 +36351,15 @@ var InkMaterials = class {
36323
36351
  }
36324
36352
  };
36325
36353
  //#endregion
36326
- //#region ../../src/scrawl/ink/ribbonGeometry.ts
36354
+ //#region src/renderer/shapes/ribbonGeometry.ts
36327
36355
  /** Ink floats a hair above the board to avoid z-fighting with the surface. */
36328
36356
  var INK_Z = .02;
36329
36357
  /** Highlighter always renders under opaque ink (build prompt §4.1). */
36330
36358
  var HIGHLIGHT_Z = .016;
36331
36359
  /** Ghosts sit under everything so live strokes always read on top. */
36332
36360
  var GHOST_Z = .012;
36333
- function buildRibbonGeometry(points, baseWidth, z = INK_Z) {
36334
- const edges = ribbonEdges(points, baseWidth);
36361
+ function buildRibbonGeometry(points, baseWidth, z = INK_Z, handDrawn = true) {
36362
+ const edges = ribbonEdges(points, baseWidth, handDrawn);
36335
36363
  const n = edges.length;
36336
36364
  const positions = new Float32Array(n * 2 * 3);
36337
36365
  const uvs = new Float32Array(n * 2 * 2);
@@ -36374,8 +36402,10 @@ function buildRibbonGeometry(points, baseWidth, z = INK_Z) {
36374
36402
  return geometry;
36375
36403
  }
36376
36404
  //#endregion
36377
- //#region ../../src/scrawl/ink/strokeRenderer.ts
36405
+ //#region src/renderer/shapes/strokeRenderer.ts
36378
36406
  var strokeZ = (stroke) => stroke.tool === "highlighter" ? HIGHLIGHT_Z : INK_Z;
36407
+ /** Shapes (rect/ellipse/line/arrow/...) are geometric outlines, not pressure-sensitive ink. */
36408
+ var handDrawn = (stroke) => stroke.tool !== "shape";
36379
36409
  /**
36380
36410
  * Derives ink meshes from the document by subscription — the document never
36381
36411
  * knows the renderer exists. Also hosts session-only ghosts (decision Q7)
@@ -36400,12 +36430,12 @@ var StrokeRenderer = class {
36400
36430
  this.ghostGroup.visible = visible;
36401
36431
  }
36402
36432
  /** Leave a smeared residue of an erased span (never persisted). */
36403
- addGhost(points, color, baseWidth) {
36433
+ addGhost(points, color, baseWidth, handDrawn = true) {
36404
36434
  if (points.length < 2) return;
36405
36435
  const mesh = new Mesh(buildRibbonGeometry(points.map((p) => ({
36406
36436
  ...p,
36407
36437
  erase: 0
36408
- })), baseWidth * 1.7, GHOST_Z), this.materials.ghost(color));
36438
+ })), baseWidth * 1.7, GHOST_Z, handDrawn), this.materials.ghost(color));
36409
36439
  this.ghostGroup.add(mesh);
36410
36440
  }
36411
36441
  /**
@@ -36417,7 +36447,7 @@ var StrokeRenderer = class {
36417
36447
  if (this.meshes.has(stroke.id) || this.retiredLive.has(stroke.id) || stroke.points.length < 2) return;
36418
36448
  this.liveUser.set(stroke.id, userId);
36419
36449
  const existing = this.liveMeshes.get(stroke.id);
36420
- const geometry = buildRibbonGeometry(stroke.points, stroke.baseWidth, strokeZ(stroke));
36450
+ const geometry = buildRibbonGeometry(stroke.points, stroke.baseWidth, strokeZ(stroke), handDrawn(stroke));
36421
36451
  if (existing) {
36422
36452
  existing.geometry.dispose();
36423
36453
  existing.geometry = geometry;
@@ -36477,7 +36507,7 @@ var StrokeRenderer = class {
36477
36507
  const mesh = this.meshes.get(stroke.id);
36478
36508
  if (mesh) {
36479
36509
  mesh.geometry.dispose();
36480
- mesh.geometry = buildRibbonGeometry(stroke.points, stroke.baseWidth, strokeZ(stroke));
36510
+ mesh.geometry = buildRibbonGeometry(stroke.points, stroke.baseWidth, strokeZ(stroke), handDrawn(stroke));
36481
36511
  }
36482
36512
  }
36483
36513
  for (const stroke of change.transformed) {
@@ -36486,7 +36516,7 @@ var StrokeRenderer = class {
36486
36516
  }
36487
36517
  }
36488
36518
  create(stroke) {
36489
- const mesh = new Mesh(buildRibbonGeometry(stroke.points, stroke.baseWidth, strokeZ(stroke)), stroke.tool === "highlighter" ? this.materials.highlight(stroke.color) : this.materials.get(stroke.color));
36519
+ 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));
36490
36520
  mesh.matrixAutoUpdate = false;
36491
36521
  syncMatrix(mesh, stroke);
36492
36522
  this.meshes.set(stroke.id, mesh);
@@ -36507,7 +36537,7 @@ function syncMatrix(mesh, stroke) {
36507
36537
  mesh.matrixWorldNeedsUpdate = true;
36508
36538
  }
36509
36539
  //#endregion
36510
- //#region ../../src/scrawl/scene-objects/markerProp.ts
36540
+ //#region src/renderer/shapes/markerProp.ts
36511
36541
  var BODY_LENGTH = 11;
36512
36542
  var BODY_RADIUS = .85;
36513
36543
  var TIP_LENGTH = 1.6;
@@ -36603,7 +36633,7 @@ var MarkerProp = class {
36603
36633
  }
36604
36634
  };
36605
36635
  //#endregion
36606
- //#region ../../src/scrawl/scene-objects/noteGeometry.ts
36636
+ //#region src/renderer/shapes/noteGeometry.ts
36607
36637
  /**
36608
36638
  * Height of the sheet above the board at (u, v) ∈ [0,1]².
36609
36639
  * v = 1 is the adhesive edge (always 0); u = 1 is the peel corner side.
@@ -36647,7 +36677,7 @@ function applyCurl(geometry, curl, peel) {
36647
36677
  color.needsUpdate = true;
36648
36678
  }
36649
36679
  //#endregion
36650
- //#region ../../src/scrawl/scene-objects/stickyNotes.ts
36680
+ //#region src/renderer/shapes/stickyNotes.ts
36651
36681
  var TEXTURE_SIZE = 512;
36652
36682
  var FOCUS_COLOR$4 = 2450411;
36653
36683
  var CURL = .3;
@@ -43223,7 +43253,7 @@ SYNCABLE_PROPS.forEach((prop) => {
43223
43253
  new Box3();
43224
43254
  new Color();
43225
43255
  //#endregion
43226
- //#region ../../src/scrawl/scene-objects/tables.ts
43256
+ //#region src/renderer/shapes/tables.ts
43227
43257
  var FONT_URL$2 = "/fonts/inter-medium.woff";
43228
43258
  var TABLE_Z = .012;
43229
43259
  var HEADER_Z = .014;
@@ -43572,7 +43602,7 @@ var TablesRenderer = class {
43572
43602
  }
43573
43603
  };
43574
43604
  //#endregion
43575
- //#region ../../src/scrawl/scene-objects/textBlocks.ts
43605
+ //#region src/renderer/text/textBlocks.ts
43576
43606
  var FONT_URL$1 = "/fonts/inter-medium.woff";
43577
43607
  var TEXT_Z = .02;
43578
43608
  var FOCUS_COLOR$2 = 2450411;
@@ -43676,7 +43706,7 @@ var TextBlocksRenderer = class {
43676
43706
  }
43677
43707
  };
43678
43708
  //#endregion
43679
- //#region ../../src/scrawl/scene-objects/comments.ts
43709
+ //#region src/renderer/shapes/comments.ts
43680
43710
  var PIN_Z = .04;
43681
43711
  var PIN_WIDTH = 3.6;
43682
43712
  var PIN_HEIGHT = 3.6;
@@ -43844,7 +43874,7 @@ function drawBubblePath(ctx, x, y, w, h, r, tailX, tailY, tailW) {
43844
43874
  ctx.closePath();
43845
43875
  }
43846
43876
  //#endregion
43847
- //#region ../../src/scrawl/scene-objects/images.ts
43877
+ //#region src/renderer/shapes/images.ts
43848
43878
  var IMAGE_Z = .015;
43849
43879
  var FRAME_BORDER = .4;
43850
43880
  var FOCUS_COLOR$1 = 2450411;
@@ -44054,7 +44084,7 @@ function applyPlaceholderTexture(visual, label) {
44054
44084
  visual.imageMesh.material.needsUpdate = true;
44055
44085
  }
44056
44086
  //#endregion
44057
- //#region ../../src/scrawl/scene-objects/kitchenTimers.ts
44087
+ //#region src/renderer/shapes/kitchenTimers.ts
44058
44088
  var FACE_SIZE = 256;
44059
44089
  var BODY_COLOR = 14753096;
44060
44090
  var KNOB_COLOR = 16448250;
@@ -44252,7 +44282,7 @@ function renderTimerFace(timer, canvas, now = Date.now()) {
44252
44282
  return c;
44253
44283
  }
44254
44284
  //#endregion
44255
- //#region ../../src/scrawl/scene-objects/customObjects.ts
44285
+ //#region src/renderer/shapes/customObjects.ts
44256
44286
  var FONT_URL = "/fonts/inter-medium.woff";
44257
44287
  var CUSTOM_Z = .02;
44258
44288
  var FOCUS_COLOR = 2450411;
@@ -44614,7 +44644,7 @@ function disposeObject3D(obj) {
44614
44644
  });
44615
44645
  }
44616
44646
  //#endregion
44617
- //#region ../../src/scrawl/tools/hitTest.ts
44647
+ //#region src/interaction/pointer/hitTest.ts
44618
44648
  /** Topmost (most recently added) stroke under the point, or null. */
44619
44649
  function hitStroke(doc, index, p, slop) {
44620
44650
  const candidates = index.query(p.x - slop, p.y - slop, p.x + slop, p.y + slop);
@@ -44625,9 +44655,10 @@ function hitStroke(doc, index, p, slop) {
44625
44655
  const scale = m ? avgScale(m) : 1;
44626
44656
  let prev = null;
44627
44657
  let prevWidth = 0;
44658
+ const handDrawn = stroke.tool !== "shape";
44628
44659
  for (const point of stroke.points) {
44629
44660
  const world = m ? apply(m, point) : point;
44630
- const width = stroke.baseWidth * (MIN_WIDTH_FACTOR + (1 - MIN_WIDTH_FACTOR) * point.pressure) * scale;
44661
+ const width = (handDrawn ? stroke.baseWidth * (MIN_WIDTH_FACTOR + (1 - MIN_WIDTH_FACTOR) * point.pressure) : stroke.baseWidth) * scale;
44631
44662
  if (prev) {
44632
44663
  const reach = Math.max(width, prevWidth) / 2 + slop;
44633
44664
  if (distToSegmentSq(p, prev, world) <= reach * reach) {
@@ -44655,7 +44686,7 @@ function distToSegmentSq(p, a, b) {
44655
44686
  return dx * dx + dy * dy;
44656
44687
  }
44657
44688
  //#endregion
44658
- //#region ../../src/scrawl/tools/eraserTool.ts
44689
+ //#region src/interaction/tools/eraserTool.ts
44659
44690
  var DECAY_PER_PASS = .4;
44660
44691
  /** A point can take another decay pass after this long — rubbing works. */
44661
44692
  var REARM_MS = 250;
@@ -44761,7 +44792,7 @@ var EraserTool = class {
44761
44792
  ...p,
44762
44793
  ...apply(m, p)
44763
44794
  })) : run.points;
44764
- this.ctx.renderer.addGhost(world, current.color, current.baseWidth * (m ? avgScale(m) : 1));
44795
+ this.ctx.renderer.addGhost(world, current.color, current.baseWidth * (m ? avgScale(m) : 1), current.tool !== "shape");
44765
44796
  }
44766
44797
  }
44767
44798
  doc.removeStrokes(before.map((s) => s.id));
@@ -44783,7 +44814,7 @@ function partitionByErasure(points) {
44783
44814
  return runs;
44784
44815
  }
44785
44816
  //#endregion
44786
- //#region ../../src/scrawl/tools/noteDrag.ts
44817
+ //#region src/interaction/tools/noteDrag.ts
44787
44818
  /**
44788
44819
  * Drag the focused note in X/Y. The grab is resolved on the note's own
44789
44820
  * z-plane, not the board, so the note doesn't jump under a tilted ray.
@@ -44850,7 +44881,7 @@ var NoteDragState = class {
44850
44881
  }
44851
44882
  };
44852
44883
  //#endregion
44853
- //#region ../../src/scrawl/tools/imageDrag.ts
44884
+ //#region src/interaction/tools/imageDrag.ts
44854
44885
  /** Drag the focused image in X/Y; one command per drag. */
44855
44886
  var ImageDragState = class {
44856
44887
  constructor(ctx) {
@@ -44902,7 +44933,7 @@ var ImageDragState = class {
44902
44933
  }
44903
44934
  };
44904
44935
  //#endregion
44905
- //#region ../../src/scrawl/tools/timerDrag.ts
44936
+ //#region src/interaction/tools/timerDrag.ts
44906
44937
  /** Drag the focused timer in X/Y; one command per drag. */
44907
44938
  var TimerDragState = class {
44908
44939
  constructor(ctx) {
@@ -44954,7 +44985,7 @@ var TimerDragState = class {
44954
44985
  }
44955
44986
  };
44956
44987
  //#endregion
44957
- //#region ../../src/scrawl/tools/shapes.ts
44988
+ //#region src/interaction/tools/shapeGeometry.ts
44958
44989
  /** Uniform synthetic pressure; the ink shader supplies the hand-drawn feel. */
44959
44990
  var PRESSURE = .7;
44960
44991
  var MIN_ELLIPSE_SEGMENTS = 24;
@@ -44994,10 +45025,7 @@ function rect(a, b, step) {
44994
45025
  a
44995
45026
  ];
44996
45027
  const out = [];
44997
- for (let i = 0; i < corners.length - 1; i++) {
44998
- const edge = sampleSegment(corners[i], corners[i + 1], step);
44999
- out.push(...i === 0 ? edge : edge.slice(1));
45000
- }
45028
+ for (let i = 0; i < corners.length - 1; i++) out.push(...sampleSegment(corners[i], corners[i + 1], step));
45001
45029
  return out;
45002
45030
  }
45003
45031
  function ellipse(a, b, step) {
@@ -45068,10 +45096,7 @@ function polygon(sides, a, b, step, startAngle = -Math.PI / 2) {
45068
45096
  }
45069
45097
  vertices.push(vertices[0]);
45070
45098
  const out = [];
45071
- for (let i = 0; i < vertices.length - 1; i++) {
45072
- const edge = sampleSegment(vertices[i], vertices[i + 1], step);
45073
- out.push(...i === 0 ? edge : edge.slice(1));
45074
- }
45099
+ for (let i = 0; i < vertices.length - 1; i++) out.push(...sampleSegment(vertices[i], vertices[i + 1], step));
45075
45100
  return out;
45076
45101
  }
45077
45102
  function star(points, a, b, step) {
@@ -45095,10 +45120,7 @@ function star(points, a, b, step) {
45095
45120
  }
45096
45121
  vertices.push(vertices[0]);
45097
45122
  const out = [];
45098
- for (let i = 0; i < vertices.length - 1; i++) {
45099
- const edge = sampleSegment(vertices[i], vertices[i + 1], step);
45100
- out.push(...i === 0 ? edge : edge.slice(1));
45101
- }
45123
+ for (let i = 0; i < vertices.length - 1; i++) out.push(...sampleSegment(vertices[i], vertices[i + 1], step));
45102
45124
  return out;
45103
45125
  }
45104
45126
  function heart(a, b, step) {
@@ -45143,7 +45165,7 @@ function snappedEnd(a, b) {
45143
45165
  };
45144
45166
  }
45145
45167
  //#endregion
45146
- //#region ../../src/scrawl/tools/shapeTool.ts
45168
+ //#region src/interaction/tools/shapeTool.ts
45147
45169
  var MIN_DRAG_PX$1 = 4;
45148
45170
  /** Drag out a shape; live preview through the real ink material. */
45149
45171
  var ShapeTool = class {
@@ -45174,7 +45196,8 @@ var ShapeTool = class {
45174
45196
  const strokes = this.polylines.map((points) => ({
45175
45197
  id: crypto.randomUUID(),
45176
45198
  color: this.ctx.inkColor(),
45177
- baseWidth: this.ctx.baseWidth(),
45199
+ baseWidth: this.ctx.shapeStrokeWidth(),
45200
+ tool: "shape",
45178
45201
  points
45179
45202
  }));
45180
45203
  this.ctx.clusters.assign(strokes[0]);
@@ -45208,13 +45231,13 @@ var ShapeTool = class {
45208
45231
  this.previews[i].visible = polyline !== void 0;
45209
45232
  if (polyline) {
45210
45233
  this.previews[i].geometry.dispose();
45211
- this.previews[i].geometry = buildRibbonGeometry(polyline, this.ctx.baseWidth());
45234
+ this.previews[i].geometry = buildRibbonGeometry(polyline, this.ctx.shapeStrokeWidth(), void 0, false);
45212
45235
  }
45213
45236
  }
45214
45237
  }
45215
45238
  };
45216
45239
  //#endregion
45217
- //#region ../../src/scrawl/tools/tableDrag.ts
45240
+ //#region src/interaction/tools/tableDrag.ts
45218
45241
  function getHandleCursor(handle) {
45219
45242
  switch (handle) {
45220
45243
  case "nw":
@@ -45334,7 +45357,7 @@ var TableDragState = class {
45334
45357
  }
45335
45358
  };
45336
45359
  //#endregion
45337
- //#region ../../src/scrawl/tools/tableTool.ts
45360
+ //#region src/interaction/tools/tableTool.ts
45338
45361
  var MIN_DRAG_PX = 6;
45339
45362
  var TableTool = class {
45340
45363
  constructor(ctx) {
@@ -45465,7 +45488,7 @@ var TableTool = class {
45465
45488
  }
45466
45489
  };
45467
45490
  //#endregion
45468
- //#region ../../src/scrawl/tools/commentTool.ts
45491
+ //#region src/interaction/tools/commentTool.ts
45469
45492
  /** Click on the board to drop a new comment pin or open an existing one. */
45470
45493
  var CommentTool = class {
45471
45494
  constructor(ctx) {
@@ -45500,7 +45523,7 @@ var CommentTool = class {
45500
45523
  }
45501
45524
  };
45502
45525
  //#endregion
45503
- //#region ../../src/scrawl/tools/textDrag.ts
45526
+ //#region src/interaction/tools/textDrag.ts
45504
45527
  /** Drag the focused text block in X/Y; one command per drag. */
45505
45528
  var TextDragState = class {
45506
45529
  constructor(ctx) {
@@ -45552,7 +45575,7 @@ var TextDragState = class {
45552
45575
  }
45553
45576
  };
45554
45577
  //#endregion
45555
- //#region ../../src/scrawl/tools/fsm.ts
45578
+ //#region src/interaction/tools/fsm.ts
45556
45579
  var ToolStateMachine = class {
45557
45580
  constructor() {
45558
45581
  this.states = /* @__PURE__ */ new Map();
@@ -45754,7 +45777,7 @@ var IdleState = class {
45754
45777
  }
45755
45778
  };
45756
45779
  //#endregion
45757
- //#region ../../src/scrawl/tools/gizmo.ts
45780
+ //#region src/renderer/selection/gizmo.ts
45758
45781
  var GIZMO_Z = .05;
45759
45782
  var HANDLE_PX = 9;
45760
45783
  var HANDLE_HIT_PX = 12;
@@ -45985,7 +46008,7 @@ function boxesEqual(a, b) {
45985
46008
  return a.minX === b.minX && a.minY === b.minY && a.maxX === b.maxX && a.maxY === b.maxY;
45986
46009
  }
45987
46010
  //#endregion
45988
- //#region ../../src/scrawl/ink/pressure.ts
46011
+ //#region src/interaction/tools/pressure.ts
45989
46012
  var SIM_MAX = 1;
45990
46013
  var SIM_MIN = .3;
45991
46014
  var SMOOTHING = .25;
@@ -46007,7 +46030,7 @@ function clamp(v, lo, hi) {
46007
46030
  return Math.min(hi, Math.max(lo, v));
46008
46031
  }
46009
46032
  //#endregion
46010
- //#region ../../src/scrawl/ink/strokeBuilder.ts
46033
+ //#region src/interaction/tools/strokeBuilder.ts
46011
46034
  var StrokeBuilder = class {
46012
46035
  constructor() {
46013
46036
  this.points = [];
@@ -46057,7 +46080,7 @@ function catmullRom(v0, v1, v2, v3, t) {
46057
46080
  return .5 * (2 * v1 + (-v0 + v2) * t + (2 * v0 - 5 * v1 + 4 * v2 - v3) * t2 + (-v0 + 3 * v1 - 3 * v2 + v3) * t3);
46058
46081
  }
46059
46082
  //#endregion
46060
- //#region ../../src/scrawl/tools/markerTool.ts
46083
+ //#region src/interaction/tools/markerTool.ts
46061
46084
  /** Sample spacing while drawing, in screen pixels. */
46062
46085
  var MIN_SAMPLE_PX = 1.5;
46063
46086
  /** A highlighter is a chisel tip: near-constant width, little pressure play. */
@@ -46160,7 +46183,7 @@ var MarkerTool = class {
46160
46183
  }
46161
46184
  };
46162
46185
  //#endregion
46163
- //#region ../../src/scrawl/tools/orbitTool.ts
46186
+ //#region src/interaction/tools/orbitTool.ts
46164
46187
  /**
46165
46188
  * Orbit-preview (build prompt §4.2): alt-drag tilts the camera off-axis to
46166
46189
  * read the board's material and depth; releasing eases back to the working
@@ -46195,7 +46218,7 @@ var OrbitTool = class {
46195
46218
  }
46196
46219
  };
46197
46220
  //#endregion
46198
- //#region ../../src/scrawl/tools/panTool.ts
46221
+ //#region src/interaction/tools/panTool.ts
46199
46222
  /** Grab-pan: content follows the pointer. */
46200
46223
  var PanTool = class {
46201
46224
  constructor(ctx) {
@@ -46225,7 +46248,7 @@ var PanTool = class {
46225
46248
  }
46226
46249
  };
46227
46250
  //#endregion
46228
- //#region ../../src/scrawl/tools/selection.ts
46251
+ //#region src/renderer/selection/selection.ts
46229
46252
  /** The set of selected stroke ids. Drops ids the document no longer has. */
46230
46253
  var SelectionManager = class {
46231
46254
  constructor(doc) {
@@ -46276,7 +46299,7 @@ var SelectionManager = class {
46276
46299
  }
46277
46300
  };
46278
46301
  //#endregion
46279
- //#region ../../src/scrawl/tools/selectTool.ts
46302
+ //#region src/interaction/tools/selectTool.ts
46280
46303
  var HIT_SLOP_PX = 4;
46281
46304
  var CLICK_TOLERANCE_PX = 3;
46282
46305
  var MIN_SCALE = .05;
@@ -46492,7 +46515,7 @@ function safeRatio(numerator, denominator) {
46492
46515
  return raw;
46493
46516
  }
46494
46517
  //#endregion
46495
- //#region ../../src/scrawl/tools/customTool.ts
46518
+ //#region src/interaction/tools/customTool.ts
46496
46519
  function extensionIdFromToolId(toolId) {
46497
46520
  const slash = toolId.indexOf("/");
46498
46521
  return slash === -1 ? toolId : toolId.slice(0, slash);
@@ -46607,12 +46630,19 @@ var ExtensionCommandBatch = class {
46607
46630
  }
46608
46631
  };
46609
46632
  //#endregion
46610
- //#region ../../src/scrawl/ScrawlEngine.ts
46633
+ //#region src/composition/ScrawlEngine.ts
46611
46634
  /** Marker stroke width in board units (~1% of the default view height). */
46612
46635
  var BASE_WIDTH = .5;
46613
46636
  /** Highlighter chisel width — wide enough to cover a line of handwriting. */
46614
46637
  var HIGHLIGHT_WIDTH = 2.2;
46615
46638
  /**
46639
+ * Shape (rectangle, ellipse, arrow, ...) border width in board units.
46640
+ * Deliberately thinner than BASE_WIDTH: a shape border reads as a crisp,
46641
+ * structural line, not an expressive marker stroke — this is also the
46642
+ * fallback when no Host theme configures ScrawlTheme.shapeStrokeWidth.
46643
+ */
46644
+ var DEFAULT_SHAPE_STROKE_WIDTH = .15;
46645
+ /**
46616
46646
  * Identity of a rendered selection badge: which item, its lock state, and
46617
46647
  * where it sits on screen (rounded to the pixel, so sub-pixel camera drift
46618
46648
  * does not churn React).
@@ -46633,10 +46663,11 @@ function emptyExtensionRegistry() {
46633
46663
  * React never reaches past this class.
46634
46664
  */
46635
46665
  var ScrawlEngine = class {
46636
- constructor(canvas, documentId$1, extensionRegistry = emptyExtensionRegistry(), assetController = new AssetController(void 0), boardTheme = DEFAULT_BOARD_SURFACE_THEME) {
46666
+ constructor(canvas, documentId$1, extensionRegistry = emptyExtensionRegistry(), assetController = new AssetController(void 0), boardTheme = DEFAULT_BOARD_SURFACE_THEME, shapeStrokeWidth = DEFAULT_SHAPE_STROKE_WIDTH) {
46637
46667
  this.canvas = canvas;
46638
46668
  this.extensionRegistry = extensionRegistry;
46639
46669
  this.boardTheme = boardTheme;
46670
+ this.shapeStrokeWidth = shapeStrokeWidth;
46640
46671
  this.onToolChange = null;
46641
46672
  this.onZoomChange = null;
46642
46673
  this.onAuditEvent = null;
@@ -46731,6 +46762,7 @@ var ScrawlEngine = class {
46731
46762
  setTool: (t) => this.setTool(t),
46732
46763
  inkColor: () => this.tool === "highlighter" ? this.highlightColor : this.color,
46733
46764
  baseWidth: () => this.tool === "highlighter" ? HIGHLIGHT_WIDTH : BASE_WIDTH,
46765
+ shapeStrokeWidth: () => this.shapeStrokeWidth,
46734
46766
  activeTool: () => this.tool,
46735
46767
  isSpaceHeld: () => this.spaceHeld,
46736
46768
  pointerToBoard: (e) => pointerToBoard(e.clientX, e.clientY, canvas, this.scene.cameraRig.camera),
@@ -46995,6 +47027,10 @@ var ScrawlEngine = class {
46995
47027
  this.boardTheme = theme;
46996
47028
  this.scene.applyBoardTheme(theme);
46997
47029
  }
47030
+ /** Live update of the shape tool's border width (board units). Already-drawn shapes keep the width they were drawn with. */
47031
+ setShapeStrokeWidth(width) {
47032
+ this.shapeStrokeWidth = width;
47033
+ }
46998
47034
  /**
46999
47035
  * Snap to a peer's camera. No-op while drawing so pointer-to-board
47000
47036
  * mapping does not warp the in-progress stroke.
@@ -48056,7 +48092,7 @@ var ScrawlEngine = class {
48056
48092
  }
48057
48093
  };
48058
48094
  //#endregion
48059
- //#region src/controller-internal.ts
48095
+ //#region src/composition/controller-internal.ts
48060
48096
  function createBoardController(options) {
48061
48097
  return createBoardControllerWithEngine(options);
48062
48098
  }
@@ -48074,9 +48110,12 @@ function createBoardControllerWithEngine(options, existingEngine) {
48074
48110
  color: options.boardTheme?.surface ?? DEFAULT_BOARD_SURFACE_THEME.color,
48075
48111
  gridMode: options.boardTheme?.gridMode ?? DEFAULT_BOARD_SURFACE_THEME.gridMode,
48076
48112
  gridColor: options.boardTheme?.gridColor ?? DEFAULT_BOARD_SURFACE_THEME.gridColor,
48077
- gridSpacing: options.boardTheme?.gridSpacing ?? DEFAULT_BOARD_SURFACE_THEME.gridSpacing
48113
+ gridSpacing: options.boardTheme?.gridSpacing ?? DEFAULT_BOARD_SURFACE_THEME.gridSpacing,
48114
+ gridLineWidth: options.boardTheme?.gridLineWidth ?? DEFAULT_BOARD_SURFACE_THEME.gridLineWidth,
48115
+ surfaceTexture: options.boardTheme?.surfaceTexture ?? DEFAULT_BOARD_SURFACE_THEME.surfaceTexture
48078
48116
  };
48079
- const engine = existingEngine ?? (options.canvas ? new ScrawlEngine(options.canvas, options.document.id, extensionRegistry, assetController, currentBoardTheme) : null);
48117
+ let currentShapeStrokeWidth = options.boardTheme?.shapeStrokeWidth ?? .15;
48118
+ const engine = existingEngine ?? (options.canvas ? new ScrawlEngine(options.canvas, options.document.id, extensionRegistry, assetController, currentBoardTheme, currentShapeStrokeWidth) : null);
48080
48119
  const document = engine?.document ?? new BoardDocument(documentId(options.document.id));
48081
48120
  const history = engine?.history ?? new History(document);
48082
48121
  const createId = options.createId ?? (() => crypto.randomUUID());
@@ -48565,9 +48604,15 @@ function createBoardControllerWithEngine(options, existingEngine) {
48565
48604
  color: theme.surface ?? currentBoardTheme.color,
48566
48605
  gridMode: theme.gridMode ?? currentBoardTheme.gridMode,
48567
48606
  gridColor: theme.gridColor ?? currentBoardTheme.gridColor,
48568
- gridSpacing: theme.gridSpacing ?? currentBoardTheme.gridSpacing
48607
+ gridSpacing: theme.gridSpacing ?? currentBoardTheme.gridSpacing,
48608
+ gridLineWidth: theme.gridLineWidth ?? currentBoardTheme.gridLineWidth,
48609
+ surfaceTexture: theme.surfaceTexture ?? currentBoardTheme.surfaceTexture
48569
48610
  };
48570
48611
  engine?.applyBoardTheme(currentBoardTheme);
48612
+ if (theme.shapeStrokeWidth !== void 0) {
48613
+ currentShapeStrokeWidth = theme.shapeStrokeWidth;
48614
+ engine?.setShapeStrokeWidth(currentShapeStrokeWidth);
48615
+ }
48571
48616
  } },
48572
48617
  view: {
48573
48618
  fit() {
@@ -49338,7 +49383,7 @@ function createLocalBoard(options) {
49338
49383
  };
49339
49384
  }
49340
49385
  //#endregion
49341
- //#region src/theme.ts
49386
+ //#region src/ui/theme.ts
49342
49387
  var scrawlThemePresets = Object.freeze({
49343
49388
  light: Object.freeze({
49344
49389
  surface: "#ffffff",
@@ -49347,6 +49392,7 @@ var scrawlThemePresets = Object.freeze({
49347
49392
  text: "#1c1c1a",
49348
49393
  textMuted: "#62625c",
49349
49394
  edge: "#d4d4cc",
49395
+ primary: "#1856a3",
49350
49396
  focus: "#1856a3",
49351
49397
  selection: "#d9e8f7",
49352
49398
  danger: "#a3302b",
@@ -49365,9 +49411,12 @@ var scrawlThemePresets = Object.freeze({
49365
49411
  motionEasing: "cubic-bezier(0.2, 0, 0, 1)",
49366
49412
  density: "comfortable",
49367
49413
  boardSurface: "#f5f5f3",
49414
+ boardSurfaceTexture: "flat",
49368
49415
  gridMode: "none",
49369
49416
  gridColor: "#deded8",
49370
- gridSpacing: 5
49417
+ gridSpacing: 5,
49418
+ gridLineWidth: 1,
49419
+ shapeStrokeWidth: .15
49371
49420
  }),
49372
49421
  dark: Object.freeze({
49373
49422
  surface: "#171816",
@@ -49376,6 +49425,7 @@ var scrawlThemePresets = Object.freeze({
49376
49425
  text: "#f4f4ee",
49377
49426
  textMuted: "#b7b8ae",
49378
49427
  edge: "#484a42",
49428
+ primary: "#9dc8ef",
49379
49429
  focus: "#9dc8ef",
49380
49430
  selection: "#314b63",
49381
49431
  danger: "#ffaaa3",
@@ -49394,9 +49444,12 @@ var scrawlThemePresets = Object.freeze({
49394
49444
  motionEasing: "cubic-bezier(0.2, 0, 0, 1)",
49395
49445
  density: "comfortable",
49396
49446
  boardSurface: "#232420",
49447
+ boardSurfaceTexture: "flat",
49397
49448
  gridMode: "none",
49398
49449
  gridColor: "#33352f",
49399
- gridSpacing: 5
49450
+ gridSpacing: 5,
49451
+ gridLineWidth: 1,
49452
+ shapeStrokeWidth: .15
49400
49453
  })
49401
49454
  });
49402
49455
  var variableNames = {
@@ -49406,6 +49459,7 @@ var variableNames = {
49406
49459
  text: "--scrawl-color-text",
49407
49460
  textMuted: "--scrawl-color-text-muted",
49408
49461
  edge: "--scrawl-color-edge",
49462
+ primary: "--scrawl-color-primary",
49409
49463
  focus: "--scrawl-color-focus",
49410
49464
  selection: "--scrawl-color-selection",
49411
49465
  danger: "--scrawl-color-danger",
@@ -49424,9 +49478,12 @@ var variableNames = {
49424
49478
  motionEasing: "--scrawl-motion-easing",
49425
49479
  density: "--scrawl-density",
49426
49480
  boardSurface: "--scrawl-board-surface",
49481
+ boardSurfaceTexture: "--scrawl-board-surface-texture",
49427
49482
  gridMode: "--scrawl-grid-mode",
49428
49483
  gridColor: "--scrawl-grid-color",
49429
- gridSpacing: "--scrawl-grid-spacing"
49484
+ gridSpacing: "--scrawl-grid-spacing",
49485
+ gridLineWidth: "--scrawl-grid-line-width",
49486
+ shapeStrokeWidth: "--scrawl-shape-stroke-width"
49430
49487
  };
49431
49488
  var colorTokens = /* @__PURE__ */ new Set([
49432
49489
  "surface",
@@ -49435,6 +49492,7 @@ var colorTokens = /* @__PURE__ */ new Set([
49435
49492
  "text",
49436
49493
  "textMuted",
49437
49494
  "edge",
49495
+ "primary",
49438
49496
  "focus",
49439
49497
  "selection",
49440
49498
  "danger",
@@ -49482,10 +49540,22 @@ function validateScrawlTheme(theme) {
49482
49540
  token,
49483
49541
  message: "Expected none, line, or dot"
49484
49542
  });
49543
+ else if (token === "boardSurfaceTexture" && value !== "flat" && value !== "textured") diagnostics.push({
49544
+ token,
49545
+ message: "Expected flat or textured"
49546
+ });
49485
49547
  else if (token === "gridSpacing" && (!finiteNumber(value) || value < .5 || value > 50)) diagnostics.push({
49486
49548
  token,
49487
49549
  message: "Expected spacing 0.5-50 board units"
49488
49550
  });
49551
+ else if (token === "gridLineWidth" && (!finiteNumber(value) || value < .5 || value > 8)) diagnostics.push({
49552
+ token,
49553
+ message: "Expected line width 0.5-8px"
49554
+ });
49555
+ else if (token === "shapeStrokeWidth" && (!finiteNumber(value) || value < .01 || value > 5)) diagnostics.push({
49556
+ token,
49557
+ message: "Expected stroke width 0.01-5 board units"
49558
+ });
49489
49559
  else if ((token.endsWith("Family") || token.startsWith("elevation") || token === "motionEasing") && (typeof value !== "string" || !value.trim() || /[;{}]/.test(value))) diagnostics.push({
49490
49560
  token,
49491
49561
  message: "Expected a safe CSS value"
@@ -49556,12 +49626,12 @@ function luminance(color) {
49556
49626
  return channels[0] * .2126 + channels[1] * .7152 + channels[2] * .0722;
49557
49627
  }
49558
49628
  function formatValue(token, value) {
49559
- if (token === "baseFontSize" || token === "controlRadius" || token === "panelRadius") return `${value}px`;
49629
+ if (token === "baseFontSize" || token === "controlRadius" || token === "panelRadius" || token === "gridLineWidth") return `${value}px`;
49560
49630
  if (token === "motionDuration") return `${value}ms`;
49561
49631
  return String(value);
49562
49632
  }
49563
49633
  //#endregion
49564
- //#region src/inline-editing.tsx
49634
+ //#region src/ui/dialogs/inline-editing.tsx
49565
49635
  /** Inline note, text, and table-cell editors — driven entirely by public controller events and commands. */
49566
49636
  function InlineEditors({ controller, renderPortal }) {
49567
49637
  const [editor, setEditor] = useState(null);
@@ -49792,7 +49862,7 @@ function InlineEditors({ controller, renderPortal }) {
49792
49862
  }))] });
49793
49863
  }
49794
49864
  //#endregion
49795
- //#region src/style-shelf.tsx
49865
+ //#region src/ui/toolbar/style-shelf.tsx
49796
49866
  var ERASER_RADII = [
49797
49867
  ["Fine", 1],
49798
49868
  ["Medium", 2],
@@ -49955,7 +50025,7 @@ function StyleShelf({ controller, snapshot }) {
49955
50025
  });
49956
50026
  }
49957
50027
  //#endregion
49958
- //#region src/default-ui.tsx
50028
+ //#region src/ui/default-ui.tsx
49959
50029
  var PRIMARY_TOOLS = [
49960
50030
  {
49961
50031
  tool: "select",
@@ -50064,7 +50134,7 @@ var INKS = [
50064
50134
  ["Forest", "#315d46"],
50065
50135
  ["Oxblood", "#773f3b"]
50066
50136
  ];
50067
- function DefaultBoardChrome({ controller, snapshot, renderPortal, className, style, regions, slots }) {
50137
+ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, style, regions, slots, icons }) {
50068
50138
  const [panel, setPanel] = useState(null);
50069
50139
  const [query, setQuery] = useState("");
50070
50140
  const [announcement, setAnnouncement] = useState("Board controls ready");
@@ -50218,7 +50288,10 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
50218
50288
  onClick: () => chooseTool(tool),
50219
50289
  title: `${label} (${key})`,
50220
50290
  type: "button",
50221
- children: label
50291
+ children: icons?.[tool] ? /* @__PURE__ */ jsx("span", {
50292
+ "aria-hidden": "true",
50293
+ children: icons[tool]
50294
+ }) : label
50222
50295
  }, tool)), /* @__PURE__ */ jsxs("label", {
50223
50296
  className: "scrawl-board__tool-select",
50224
50297
  children: [/* @__PURE__ */ jsx("span", { children: "More tools" }), /* @__PURE__ */ jsxs("select", {
@@ -50484,7 +50557,7 @@ function download(blob, name) {
50484
50557
  URL.revokeObjectURL(url);
50485
50558
  }
50486
50559
  //#endregion
50487
- //#region src/presence.tsx
50560
+ //#region src/ui/panels/presence.tsx
50488
50561
  var labelStyle = {
50489
50562
  borderRadius: 999,
50490
50563
  color: "#fff",
@@ -50644,9 +50717,12 @@ function ScrawlProvider({ controller, children, preset = "light", theme, portalC
50644
50717
  useEffect(() => {
50645
50718
  controller.boardTheme.set({
50646
50719
  surface: resolved.values.boardSurface,
50720
+ surfaceTexture: resolved.values.boardSurfaceTexture,
50647
50721
  gridMode: resolved.values.gridMode,
50648
50722
  gridColor: resolved.values.gridColor,
50649
- gridSpacing: resolved.values.gridSpacing
50723
+ gridSpacing: resolved.values.gridSpacing,
50724
+ gridLineWidth: resolved.values.gridLineWidth,
50725
+ shapeStrokeWidth: resolved.values.shapeStrokeWidth
50650
50726
  });
50651
50727
  }, [controller, resolved]);
50652
50728
  useDeferredDisposal(controller, disposeOnUnmount);
@@ -50678,7 +50754,7 @@ function ScrawlProvider({ controller, children, preset = "light", theme, portalC
50678
50754
  })
50679
50755
  });
50680
50756
  }
50681
- function Scrawl({ children, preset, theme, portalContainer, className, style, onReady, onError, onThemeDiagnostic, canvas: suppliedCanvas, ...options }) {
50757
+ function Scrawl({ children, preset, theme, portalContainer, className, style, onReady, onError, onThemeDiagnostic, canvas: suppliedCanvas, icons, ...options }) {
50682
50758
  const [runtime, setRuntime] = useState(null);
50683
50759
  const runtimeRef = useRef(null);
50684
50760
  const ownedCanvasRef = useRef(null);
@@ -50699,9 +50775,12 @@ function Scrawl({ children, preset, theme, portalContainer, className, style, on
50699
50775
  const resolved = resolveScrawlTheme(preset, theme);
50700
50776
  const boardTheme = {
50701
50777
  surface: resolved.values.boardSurface,
50778
+ surfaceTexture: resolved.values.boardSurfaceTexture,
50702
50779
  gridMode: resolved.values.gridMode,
50703
50780
  gridColor: resolved.values.gridColor,
50704
- gridSpacing: resolved.values.gridSpacing
50781
+ gridSpacing: resolved.values.gridSpacing,
50782
+ gridLineWidth: resolved.values.gridLineWidth,
50783
+ shapeStrokeWidth: resolved.values.shapeStrokeWidth
50705
50784
  };
50706
50785
  controller = createBoardController({
50707
50786
  ...options,
@@ -50771,7 +50850,7 @@ function Scrawl({ children, preset, theme, portalContainer, className, style, on
50771
50850
  onThemeDiagnostic,
50772
50851
  className,
50773
50852
  style,
50774
- children: [runtime.canvas && /* @__PURE__ */ jsx(ScrawlCanvas, { element: runtime.canvas }), children ?? /* @__PURE__ */ jsx(ScrawlDefaultUI, {})]
50853
+ children: [runtime.canvas && /* @__PURE__ */ jsx(ScrawlCanvas, { element: runtime.canvas }), children ?? /* @__PURE__ */ jsx(ScrawlDefaultUI, { icons })]
50775
50854
  });
50776
50855
  }
50777
50856
  function ScrawlCanvas({ element, className, style, "aria-label": ariaLabel = "Scrawl Board" }) {
@@ -50807,10 +50886,11 @@ function ScrawlCanvas({ element, className, style, "aria-label": ariaLabel = "Sc
50807
50886
  style
50808
50887
  });
50809
50888
  }
50810
- function ScrawlDefaultUI({ className, style, regions, slots }) {
50889
+ function ScrawlDefaultUI({ className, style, regions, slots, icons }) {
50811
50890
  return /* @__PURE__ */ jsx(DefaultBoardChrome, {
50812
50891
  className,
50813
50892
  controller: useScrawlController(),
50893
+ icons,
50814
50894
  regions,
50815
50895
  renderPortal: (children) => /* @__PURE__ */ jsx(ScrawlPortal, { children }),
50816
50896
  slots,