@scrawl-board/board 0.1.0-beta.0 → 0.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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;
@@ -23,12 +23,17 @@ var AssetResolutionError = class extends Error {
23
23
  var ASSET_CACHE_BYTES_DEFAULT = 256 * 1024 * 1024;
24
24
  var ASSET_CACHE_BYTES_MIN = 64 * 1024 * 1024;
25
25
  var ASSET_CACHE_BYTES_MAX = 512 * 1024 * 1024;
26
+ var SUPPORTED_ASSET_MEDIA_TYPES = [
27
+ "image/png",
28
+ "image/jpeg",
29
+ "image/webp"
30
+ ];
26
31
  function clampAssetCacheBytes(value) {
27
32
  if (value === void 0 || !Number.isFinite(value)) return ASSET_CACHE_BYTES_DEFAULT;
28
33
  return Math.min(ASSET_CACHE_BYTES_MAX, Math.max(ASSET_CACHE_BYTES_MIN, Math.floor(value)));
29
34
  }
30
35
  //#endregion
31
- //#region src/core-internal/identifiers.ts
36
+ //#region src/core/document/identifiers.ts
32
37
  function documentId(value) {
33
38
  if (!value) throw new TypeError("DocumentId cannot be empty");
34
39
  return value;
@@ -229,7 +234,7 @@ function validation(message, path) {
229
234
  return new DocumentRecoveryError("DOCUMENT_VALIDATION_FAILED", message, void 0, path);
230
235
  }
231
236
  //#endregion
232
- //#region src/core-internal/affine2d.ts
237
+ //#region src/core/shapes/affine2d.ts
233
238
  var IDENTITY = [
234
239
  1,
235
240
  0,
@@ -313,7 +318,7 @@ function avgScale(m) {
313
318
  return (Math.hypot(m[0], m[1]) + Math.hypot(m[2], m[3])) / 2;
314
319
  }
315
320
  //#endregion
316
- //#region src/core-internal/boardSearch.ts
321
+ //#region src/core/document/boardSearch.ts
317
322
  function hay(s) {
318
323
  return s.toLowerCase();
319
324
  }
@@ -378,7 +383,7 @@ function searchBoard(query, board) {
378
383
  return hits;
379
384
  }
380
385
  //#endregion
381
- //#region src/core-internal/types.ts
386
+ //#region src/core/shapes/types.ts
382
387
  /** Erasure level at which a point counts as fully erased. */
383
388
  var ERASE_THRESHOLD = .95;
384
389
  function cloneStroke(stroke) {
@@ -445,13 +450,12 @@ function measureTable(table) {
445
450
  height: table.rowHeights.reduce((sum, h) => sum + h, 0)
446
451
  };
447
452
  }
448
- var BOARD_COLOR = "#FFFFFF";
449
453
  var FOG_COLOR = "#FFFFFF";
450
454
  function cloneImage(img) {
451
455
  return { ...img };
452
456
  }
453
457
  //#endregion
454
- //#region src/core-internal/clusterStore.ts
458
+ //#region src/core/document/clusterStore.ts
455
459
  /** A stroke joins a cluster last written to within this window at full reach. */
456
460
  var RECENT_MS = 8e3;
457
461
  /** Older clusters are still joinable, at half the gap threshold. */
@@ -622,7 +626,7 @@ function clamp$3(v, lo, hi) {
622
626
  return Math.min(hi, Math.max(lo, v));
623
627
  }
624
628
  //#endregion
625
- //#region src/core-internal/extensionTypes.ts
629
+ //#region src/core/shapes/extensionTypes.ts
626
630
  function cloneCustomObject(object) {
627
631
  return {
628
632
  ...object,
@@ -632,7 +636,7 @@ function cloneCustomObject(object) {
632
636
  };
633
637
  }
634
638
  //#endregion
635
- //#region src/core-internal/kitchenTimer.ts
639
+ //#region src/core/shapes/kitchenTimer.ts
636
640
  var TIMER_DEFAULT_DURATION_MS = 300 * 1e3;
637
641
  var TIMER_PRESETS_MS = [
638
642
  6e4,
@@ -688,7 +692,7 @@ function formatTimer(ms) {
688
692
  return `${Math.floor(total / 60)}:${(total % 60).toString().padStart(2, "0")}`;
689
693
  }
690
694
  //#endregion
691
- //#region src/core-internal/itemLock.ts
695
+ //#region src/core/shapes/itemLock.ts
692
696
  /**
693
697
  * Apply or clear a lock in place. Unlock always drops the holder so a stale
694
698
  * name cannot linger on an unlocked item.
@@ -725,7 +729,7 @@ function serializeLock(item) {
725
729
  };
726
730
  }
727
731
  //#endregion
728
- //#region src/core-internal/document.ts
732
+ //#region src/core/document/document.ts
729
733
  /**
730
734
  * The one place a live Stroke becomes its wire representation — in
731
735
  * particular, points collapse from {x,y,pressure,erase?} objects into
@@ -1284,7 +1288,7 @@ function computeBBox(stroke) {
1284
1288
  return world;
1285
1289
  }
1286
1290
  //#endregion
1287
- //#region src/core-internal/commands.ts
1291
+ //#region src/core/commands/commands.ts
1288
1292
  /** One drawing action — a marker stroke, or a shape's strokes as one unit. */
1289
1293
  var AddStrokesCommand = class {
1290
1294
  constructor(strokes) {
@@ -1626,7 +1630,7 @@ var LockItemsCommand = class {
1626
1630
  }
1627
1631
  };
1628
1632
  //#endregion
1629
- //#region src/core-internal/history.ts
1633
+ //#region src/core/history/history.ts
1630
1634
  var LIMIT = 200;
1631
1635
  var History = class {
1632
1636
  constructor(doc) {
@@ -1687,7 +1691,7 @@ var History = class {
1687
1691
  }
1688
1692
  };
1689
1693
  //#endregion
1690
- //#region src/core-internal/ops.ts
1694
+ //#region src/core/events/ops.ts
1691
1695
  var ADDED = [
1692
1696
  ["added", "strokes"],
1693
1697
  ["notesAdded", "notes"],
@@ -1741,7 +1745,7 @@ function changeToOps(change, restoring = false) {
1741
1745
  return ops;
1742
1746
  }
1743
1747
  //#endregion
1744
- //#region src/core-internal/ribbonEdges.ts
1748
+ //#region src/core/shapes/ribbonEdges.ts
1745
1749
  var MIN_WIDTH_FACTOR = .35;
1746
1750
  var END_TAPER = .55;
1747
1751
  function ribbonEdges(points, baseWidth) {
@@ -1773,7 +1777,7 @@ function ribbonEdges(points, baseWidth) {
1773
1777
  return out;
1774
1778
  }
1775
1779
  //#endregion
1776
- //#region src/core-internal/spatialIndex.ts
1780
+ //#region src/core/document/spatialIndex.ts
1777
1781
  var CELL = 8;
1778
1782
  var SpatialIndex = class {
1779
1783
  constructor(doc) {
@@ -1846,7 +1850,7 @@ function cellsOf(box) {
1846
1850
  return keys;
1847
1851
  }
1848
1852
  //#endregion
1849
- //#region src/core-internal/stamps.ts
1853
+ //#region src/core/shapes/stamps.ts
1850
1854
  var STAMPS = [
1851
1855
  {
1852
1856
  kind: "star",
@@ -1891,7 +1895,7 @@ function stampDataUrl(kind) {
1891
1895
  return `data:image/svg+xml;utf8,${encodeURIComponent(SVG[kind])}`;
1892
1896
  }
1893
1897
  //#endregion
1894
- //#region src/core-internal/presence.ts
1898
+ //#region src/collaboration/presence/presence.ts
1895
1899
  /** Keep beacons clear of typical chrome at the top and bottom of the Board. */
1896
1900
  var BEACON_INSET = {
1897
1901
  top: 72,
@@ -1927,7 +1931,7 @@ function clamp$2(value, min, max) {
1927
1931
  return Math.min(max, Math.max(min, value));
1928
1932
  }
1929
1933
  //#endregion
1930
- //#region src/core-internal/svg.ts
1934
+ //#region src/persistence/serialization/svg.ts
1931
1935
  /** Matches the key light used by the board shader and note shadows. */
1932
1936
  var LIGHT = {
1933
1937
  x: -.25,
@@ -1937,12 +1941,19 @@ var LIGHT = {
1937
1941
  var MARGIN = 4;
1938
1942
  var NOTE_FONT_RATIO = 44 / 512;
1939
1943
  var NOTE_PAD_RATIO = 40 / 512;
1944
+ /** Matches the theme system's light-preset `boardSurface` default (theme.ts) — kept in sync manually, since core-internal stays decoupled from the theme layer. */
1945
+ var DEFAULT_BACKGROUND_COLOR = "#f5f5f3";
1940
1946
  /**
1941
1947
  * `registry` is optional and only enables rendering Custom objects through
1942
1948
  * their own `describe()` — without it (or for an object whose extension
1943
1949
  * isn't in it), Custom objects still export via the standard fallback
1944
1950
  * placeholder (`fallback.bounds`/`label`), never silently dropped.
1945
1951
  *
1952
+ * `backgroundColor` should be the Host's actual resolved `boardTheme.surface`
1953
+ * so an export matches what was on screen; defaults to the theme system's
1954
+ * own light-preset default when the caller doesn't have one on hand. The
1955
+ * board's reference grid, if any, is a screen-only aid and never exported.
1956
+ *
1946
1957
  * `resolvedAssets` (ticket #23) maps an Asset reference to an already-
1947
1958
  * resolved `data:` URI — see `assetExport.ts`'s `exportDocumentSVGWithAssets`,
1948
1959
  * which is the only intended caller that ever passes one. Without it, every
@@ -1950,7 +1961,7 @@ var NOTE_PAD_RATIO = 40 / 512;
1950
1961
  * placeholder instead of guessing at a URL; a legacy `src`-backed image is
1951
1962
  * unaffected either way.
1952
1963
  */
1953
- function documentToSVG(doc, now = 0, registry, resolvedAssets) {
1964
+ function documentToSVG(doc, now = 0, registry, backgroundColor = DEFAULT_BACKGROUND_COLOR, resolvedAssets) {
1954
1965
  const notes = doc.notes ?? [];
1955
1966
  const texts = doc.textBlocks ?? [];
1956
1967
  const tables = doc.tables ?? [];
@@ -1963,7 +1974,7 @@ function documentToSVG(doc, now = 0, registry, resolvedAssets) {
1963
1974
  const highlights = doc.strokes.filter((s) => s.tool === "highlighter");
1964
1975
  const ink = doc.strokes.filter((s) => s.tool !== "highlighter");
1965
1976
  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${view}">\n<defs>\n${defs}\n</defs>\n${[
1966
- `<rect x="${fmt(bounds.minX)}" y="${fmt(bounds.minY)}" width="${fmt(bounds.width)}" height="${fmt(bounds.height)}" fill="${BOARD_COLOR}"/>`,
1977
+ `<rect x="${fmt(bounds.minX)}" y="${fmt(bounds.minY)}" width="${fmt(bounds.width)}" height="${fmt(bounds.height)}" fill="${backgroundColor}"/>`,
1967
1978
  ...images.map((image) => imageToElement(image, resolvedAssets)),
1968
1979
  ...tables.map(tableToGroup),
1969
1980
  ...highlights.flatMap(strokeToPaths),
@@ -2287,7 +2298,7 @@ function fmt(n) {
2287
2298
  return String(Math.round(n * 100) / 100);
2288
2299
  }
2289
2300
  //#endregion
2290
- //#region src/core-internal/sceneGeometry.ts
2301
+ //#region src/core/shapes/sceneGeometry.ts
2291
2302
  var EMPTY_BOUNDS = {
2292
2303
  x: 0,
2293
2304
  y: 0,
@@ -2351,7 +2362,7 @@ function textCorners(x, y, text, fontSize, align, m) {
2351
2362
  return rectCorners(x + (align === "center" ? -width / 2 : align === "end" ? -width : 0), y, width, height, m);
2352
2363
  }
2353
2364
  //#endregion
2354
- //#region src/core-internal/customObjects.ts
2365
+ //#region src/core/shapes/customObjects.ts
2355
2366
  var FALLBACK_LABEL_MAX_LENGTH = 80;
2356
2367
  function toReadonly(object) {
2357
2368
  return object;
@@ -2480,7 +2491,7 @@ function extensionIdFromType(type) {
2480
2491
  return slash === -1 ? type : type.slice(0, slash);
2481
2492
  }
2482
2493
  //#endregion
2483
- //#region src/core-internal/extensionRegistry.ts
2494
+ //#region src/core/shapes/extensionRegistry.ts
2484
2495
  function buildExtensionRegistry(extensions) {
2485
2496
  const list = extensions ?? [];
2486
2497
  const diagnostics = [];
@@ -2577,7 +2588,7 @@ function isWellFormedNamespacedId(id) {
2577
2588
  return isWellFormedId(id) && id.includes("/") && !id.startsWith("/") && !id.endsWith("/");
2578
2589
  }
2579
2590
  //#endregion
2580
- //#region src/core-internal/assetValidation.ts
2591
+ //#region src/persistence/adapters/assetValidation.ts
2581
2592
  function sniffImageMediaType(bytes) {
2582
2593
  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";
2583
2594
  if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
@@ -2664,7 +2675,7 @@ function validateDecodedDimensions(width, height) {
2664
2675
  };
2665
2676
  }
2666
2677
  //#endregion
2667
- //#region src/assetController.ts
2678
+ //#region src/persistence/adapters/assetController.ts
2668
2679
  var FAILURE_COALESCE_MS = 4e3;
2669
2680
  var AssetController = class {
2670
2681
  constructor(resolver, options) {
@@ -2843,7 +2854,7 @@ function toAssetResolutionError(cause, ref) {
2843
2854
  return new AssetResolutionError("unknown", true, cause instanceof Error ? cause.message : String(cause), ref);
2844
2855
  }
2845
2856
  //#endregion
2846
- //#region src/core-internal/assetExport.ts
2857
+ //#region src/persistence/serialization/assetRefCollector.ts
2847
2858
  /** Every unique Asset reference in the document, and which object(s) use it. */
2848
2859
  function collectDocumentAssetRefs(doc, registry) {
2849
2860
  const refs = /* @__PURE__ */ new Map();
@@ -2870,7 +2881,7 @@ function collectSceneAssetRefs(node, objectId, add) {
2870
2881
  if (node.kind === "group") for (const child of node.children) collectSceneAssetRefs(child, objectId, add);
2871
2882
  }
2872
2883
  //#endregion
2873
- //#region src/assetExport.ts
2884
+ //#region src/persistence/serialization/assetExport.ts
2874
2885
  /** Strict mode's rejection — never contains credentials, a fetchable location, or a raw resolver message. */
2875
2886
  var SvgAssetExportError = class extends Error {
2876
2887
  constructor(missingAssets) {
@@ -2879,7 +2890,7 @@ var SvgAssetExportError = class extends Error {
2879
2890
  this.name = "SvgAssetExportError";
2880
2891
  }
2881
2892
  };
2882
- async function exportDocumentSVGWithAssets(doc, now, registry, assetController, options) {
2893
+ async function exportDocumentSVGWithAssets(doc, now, registry, assetController, backgroundColor, options) {
2883
2894
  const refs = collectDocumentAssetRefs(doc, registry);
2884
2895
  const resolvedAssets = /* @__PURE__ */ new Map();
2885
2896
  const missingAssets = [];
@@ -2915,7 +2926,7 @@ async function exportDocumentSVGWithAssets(doc, now, registry, assetController,
2915
2926
  }
2916
2927
  if (missingAssets.length > 0 && options?.missingAssets !== "placeholder") throw new SvgAssetExportError(missingAssets);
2917
2928
  return {
2918
- svg: documentToSVG(doc, now, registry, resolvedAssets),
2929
+ svg: documentToSVG(doc, now, registry, backgroundColor, resolvedAssets),
2919
2930
  missingAssets
2920
2931
  };
2921
2932
  }
@@ -2926,7 +2937,7 @@ function toDataUri(bytes, mediaType) {
2926
2937
  return `data:${mediaType};base64,${btoa(binary)}`;
2927
2938
  }
2928
2939
  //#endregion
2929
- //#region src/assetIngestion.ts
2940
+ //#region src/persistence/adapters/assetIngestion.ts
2930
2941
  async function decodeDimensionsWithImageBitmap(bytes, mediaType) {
2931
2942
  const blob = new Blob([bytes], { type: mediaType });
2932
2943
  let bitmap;
@@ -35572,7 +35583,7 @@ var WebGLRenderer = class {
35572
35583
  }
35573
35584
  };
35574
35585
  //#endregion
35575
- //#region ../../src/scrawl/data/auditEvents.ts
35586
+ //#region src/core/events/auditEvents.ts
35576
35587
  var BY_LABEL = {
35577
35588
  draw: {
35578
35589
  action: "create",
@@ -35692,7 +35703,7 @@ function lowerFirst(s) {
35692
35703
  return s.charAt(0).toLowerCase() + s.slice(1);
35693
35704
  }
35694
35705
  //#endregion
35695
- //#region ../../src/scrawl/assets/AssetTextureCache.ts
35706
+ //#region src/renderer/shapes/AssetTextureCache.ts
35696
35707
  var AssetTextureCache = class {
35697
35708
  constructor(assetController) {
35698
35709
  this.assetController = assetController;
@@ -35783,7 +35794,7 @@ function toDecodeError(cause, ref) {
35783
35794
  return new AssetResolutionError("decode-failed", false, cause instanceof Error ? cause.message : String(cause), ref);
35784
35795
  }
35785
35796
  //#endregion
35786
- //#region ../../src/scrawl/engine/raycast.ts
35797
+ //#region src/renderer/canvas/raycast.ts
35787
35798
  var origin$1 = new Vector3();
35788
35799
  var target = new Vector3();
35789
35800
  function pointerToBoard(clientX, clientY, canvas, camera) {
@@ -35814,7 +35825,7 @@ function pointerToPlane(clientX, clientY, canvas, camera, z) {
35814
35825
  };
35815
35826
  }
35816
35827
  //#endregion
35817
- //#region ../../src/scrawl/engine/fog.ts
35828
+ //#region src/renderer/canvas/fog.ts
35818
35829
  function createFogUniforms(color) {
35819
35830
  return {
35820
35831
  uFogColor: { value: new Color(color) },
@@ -35835,7 +35846,28 @@ var FOG_GLSL = `
35835
35846
  }
35836
35847
  `;
35837
35848
  //#endregion
35838
- //#region ../../src/scrawl/engine/boardSurface.ts
35849
+ //#region src/renderer/canvas/boardSurface.ts
35850
+ var GRID_MODE_CODE = {
35851
+ none: 0,
35852
+ line: 1,
35853
+ dot: 2
35854
+ };
35855
+ var TEXTURE_CODE = {
35856
+ flat: 0,
35857
+ textured: 1
35858
+ };
35859
+ /** Used when a caller doesn't supply one (e.g. ScrawlEngine constructed without a Host theme). */
35860
+ var DEFAULT_BOARD_SURFACE_THEME = {
35861
+ color: "#f5f5f3",
35862
+ gridMode: "none",
35863
+ gridColor: "#deded8",
35864
+ gridSpacing: 5,
35865
+ gridLineWidth: 1,
35866
+ surfaceTexture: "flat"
35867
+ };
35868
+ /** Grid alpha fades out below this on-screen spacing (px) — otherwise it aliases into a solid wash when zoomed out. */
35869
+ var GRID_FADE_MIN_PX = 3;
35870
+ var GRID_FADE_MAX_PX = 8;
35839
35871
  var VERTEX$1 = `
35840
35872
  varying vec3 vWorldPos;
35841
35873
  void main() {
@@ -35849,6 +35881,13 @@ var FRAGMENT = `
35849
35881
  uniform vec3 uFogColor;
35850
35882
  uniform float uFogNear;
35851
35883
  uniform float uFogFar;
35884
+ uniform float uGridMode;
35885
+ uniform vec3 uGridColor;
35886
+ uniform float uGridSpacing;
35887
+ uniform float uWorldPerPixel;
35888
+ uniform float uGridLineWidthPx;
35889
+ uniform float uGridFade;
35890
+ uniform float uSurfaceTexture;
35852
35891
  varying vec3 vWorldPos;
35853
35892
 
35854
35893
  float hash(vec2 p) {
@@ -35867,33 +35906,69 @@ var FRAGMENT = `
35867
35906
 
35868
35907
  ${FOG_GLSL}
35869
35908
 
35909
+ // Distance from p's nearest axis-aligned grid line, in world units.
35910
+ // A crisp, ~uGridLineWidthPx-wide line: full alpha to its half-width, a
35911
+ // ~1px anti-aliased falloff beyond that — not a wide, soft-edged band.
35912
+ float gridLineAlpha(vec2 p) {
35913
+ vec2 cell = mod(p, uGridSpacing);
35914
+ vec2 distToLine = min(cell, uGridSpacing - cell);
35915
+ float halfWidth = 0.5 * uGridLineWidthPx * uWorldPerPixel;
35916
+ float aa = uWorldPerPixel;
35917
+ vec2 lineAlpha2 = 1.0 - smoothstep(halfWidth - aa * 0.5, halfWidth + aa * 0.5, distToLine);
35918
+ return max(lineAlpha2.x, lineAlpha2.y);
35919
+ }
35920
+
35921
+ // Distance from p's nearest grid intersection, in world units. Dot radius
35922
+ // matches the configured line width in pixels, same anti-aliasing.
35923
+ float gridDotAlpha(vec2 p) {
35924
+ vec2 cell = mod(p, uGridSpacing) - uGridSpacing * 0.5;
35925
+ float dist = length(cell);
35926
+ float radius = uGridLineWidthPx * uWorldPerPixel;
35927
+ float aa = uWorldPerPixel;
35928
+ return 1.0 - smoothstep(radius - aa * 0.5, radius + aa * 0.5, dist);
35929
+ }
35930
+
35870
35931
  void main() {
35871
35932
  vec3 col = uColor;
35872
35933
 
35873
- // Melamine micro-texture: two octaves of value noise, +-1.75% brightness.
35874
- float n = vnoise(vWorldPos.xy * 2.3) * 0.6 + vnoise(vWorldPos.xy * 9.7) * 0.4;
35875
- col *= 1.0 + (n - 0.5) * 0.035;
35934
+ if (uSurfaceTexture > 0.5) {
35935
+ // Melamine micro-texture: two octaves of value noise, +-1.75% brightness.
35936
+ float n = vnoise(vWorldPos.xy * 2.3) * 0.6 + vnoise(vWorldPos.xy * 9.7) * 0.4;
35937
+ col *= 1.0 + (n - 0.5) * 0.035;
35876
35938
 
35877
- // Satin sheen: Fresnel rim + a specular band from the key light. Both are
35878
- // ~zero in the straight-on working view and bloom as the camera tilts.
35879
- vec3 V = normalize(cameraPosition - vWorldPos);
35880
- vec3 N = vec3(0.0, 0.0, 1.0);
35881
- float fres = pow(1.0 - max(dot(N, V), 0.0), 3.0);
35882
- vec3 L = normalize(vec3(-0.25, 0.4, 0.88));
35883
- vec3 H = normalize(L + V);
35884
- float spec = pow(max(dot(N, H), 0.0), 90.0);
35885
- col += (fres * 0.18 + spec * 0.06) * vec3(1.0);
35939
+ // Satin sheen: Fresnel rim + a specular band from the key light. Both
35940
+ // are ~zero in the straight-on working view and bloom as the camera tilts.
35941
+ vec3 V = normalize(cameraPosition - vWorldPos);
35942
+ vec3 N = vec3(0.0, 0.0, 1.0);
35943
+ float fres = pow(1.0 - max(dot(N, V), 0.0), 3.0);
35944
+ vec3 L = normalize(vec3(-0.25, 0.4, 0.88));
35945
+ vec3 H = normalize(L + V);
35946
+ float spec = pow(max(dot(N, H), 0.0), 90.0);
35947
+ col += (fres * 0.18 + spec * 0.06) * vec3(1.0);
35948
+ }
35949
+
35950
+ if (uGridMode > 0.5 && uGridFade > 0.001) {
35951
+ float gridAlpha = (uGridMode < 1.5 ? gridLineAlpha(vWorldPos.xy) : gridDotAlpha(vWorldPos.xy)) * uGridFade;
35952
+ col = mix(col, uGridColor, gridAlpha);
35953
+ }
35886
35954
 
35887
35955
  col = applyBoardFog(col, vWorldPos);
35888
35956
  gl_FragColor = vec4(col, 1.0);
35889
35957
  }
35890
35958
  `;
35891
- function createBoardSurface(fog) {
35959
+ function createBoardSurface(fog, theme) {
35892
35960
  const material = new ShaderMaterial({
35893
35961
  vertexShader: VERTEX$1,
35894
35962
  fragmentShader: FRAGMENT,
35895
35963
  uniforms: {
35896
- uColor: { value: new Color(BOARD_COLOR) },
35964
+ uColor: { value: new Color(theme.color) },
35965
+ uGridMode: { value: GRID_MODE_CODE[theme.gridMode] },
35966
+ uGridColor: { value: new Color(theme.gridColor) },
35967
+ uGridSpacing: { value: Math.max(.001, theme.gridSpacing) },
35968
+ uGridLineWidthPx: { value: Math.max(.1, theme.gridLineWidth) },
35969
+ uWorldPerPixel: { value: .05 },
35970
+ uGridFade: { value: 0 },
35971
+ uSurfaceTexture: { value: TEXTURE_CODE[theme.surfaceTexture] },
35897
35972
  ...fog
35898
35973
  }
35899
35974
  });
@@ -35901,11 +35976,34 @@ function createBoardSurface(fog) {
35901
35976
  mesh.name = "board-surface";
35902
35977
  return mesh;
35903
35978
  }
35904
- /** Keep the plane centered under the view and larger than the fog horizon. */
35905
- function updateBoardSurface(mesh, centerX, centerY, fogFar) {
35979
+ /** Applies a theme change (initial or live) everything except the per-frame fade/pixel-ratio. */
35980
+ function setBoardSurfaceTheme(mesh, theme) {
35981
+ const uniforms = mesh.material.uniforms;
35982
+ uniforms.uColor.value.set(theme.color);
35983
+ uniforms.uGridMode.value = GRID_MODE_CODE[theme.gridMode];
35984
+ uniforms.uGridColor.value.set(theme.gridColor);
35985
+ uniforms.uGridSpacing.value = Math.max(.001, theme.gridSpacing);
35986
+ uniforms.uGridLineWidthPx.value = Math.max(.1, theme.gridLineWidth);
35987
+ uniforms.uSurfaceTexture.value = TEXTURE_CODE[theme.surfaceTexture];
35988
+ }
35989
+ /** Keep the plane centered under the view, larger than the fog horizon, and the grid's on-screen weight/fade current. */
35990
+ function updateBoardSurface(mesh, centerX, centerY, fogFar, worldPerPixel) {
35906
35991
  mesh.position.set(centerX, centerY, 0);
35907
35992
  const size = fogFar * 2.5;
35908
35993
  mesh.scale.set(size, size, 1);
35994
+ const uniforms = mesh.material.uniforms;
35995
+ uniforms.uWorldPerPixel.value = worldPerPixel;
35996
+ const t = (uniforms.uGridSpacing.value / Math.max(1e-6, worldPerPixel) - GRID_FADE_MIN_PX) / (GRID_FADE_MAX_PX - GRID_FADE_MIN_PX);
35997
+ uniforms.uGridFade.value = MathUtils.clamp(t, 0, 1);
35998
+ }
35999
+ /** Temporarily zeroes the grid's contribution (for export snapshots) and returns a restore function. */
36000
+ function suppressBoardSurfaceGrid(mesh) {
36001
+ const uniforms = mesh.material.uniforms;
36002
+ const previous = uniforms.uGridFade.value;
36003
+ uniforms.uGridFade.value = 0;
36004
+ return () => {
36005
+ uniforms.uGridFade.value = previous;
36006
+ };
35909
36007
  }
35910
36008
  var MIN_HEIGHT = 40;
35911
36009
  var MAX_HEIGHT = 1500;
@@ -36067,9 +36165,9 @@ var CameraRig = class {
36067
36165
  }
36068
36166
  };
36069
36167
  //#endregion
36070
- //#region ../../src/scrawl/engine/scene.ts
36168
+ //#region src/renderer/canvas/scene.ts
36071
36169
  var SceneRig = class {
36072
- constructor(canvas) {
36170
+ constructor(canvas, boardTheme) {
36073
36171
  this.renderer = new WebGLRenderer({
36074
36172
  canvas,
36075
36173
  antialias: true
@@ -36079,7 +36177,7 @@ var SceneRig = class {
36079
36177
  this.scene = new Scene();
36080
36178
  this.cameraRig = new CameraRig(canvas.clientWidth / Math.max(1, canvas.clientHeight));
36081
36179
  this.fog = createFogUniforms(FOG_COLOR);
36082
- this.board = createBoardSurface(this.fog);
36180
+ this.board = createBoardSurface(this.fog, boardTheme);
36083
36181
  this.scene.add(this.board);
36084
36182
  this.inkGroup = new Group();
36085
36183
  this.inkGroup.name = "ink";
@@ -36096,9 +36194,17 @@ var SceneRig = class {
36096
36194
  renderFrame(dtMs) {
36097
36195
  this.cameraRig.update(dtMs);
36098
36196
  updateFogForCamera(this.fog, this.cameraRig.height);
36099
- updateBoardSurface(this.board, this.cameraRig.center.x, this.cameraRig.center.y, this.fog.uFogFar.value);
36197
+ updateBoardSurface(this.board, this.cameraRig.center.x, this.cameraRig.center.y, this.fog.uFogFar.value, this.cameraRig.worldPerPixel());
36100
36198
  this.renderer.render(this.scene, this.cameraRig.camera);
36101
36199
  }
36200
+ /** Live theme update — the board controller's identity stays fixed across theme changes; this is how a new one reaches the render engine. */
36201
+ applyBoardTheme(theme) {
36202
+ setBoardSurfaceTheme(this.board, theme);
36203
+ }
36204
+ /** Zeroes the grid for one render pass (PNG export never bakes it in) and returns a restore function. */
36205
+ suppressGrid() {
36206
+ return suppressBoardSurfaceGrid(this.board);
36207
+ }
36102
36208
  dispose() {
36103
36209
  this.board.geometry.dispose();
36104
36210
  this.board.material.dispose();
@@ -36106,7 +36212,7 @@ var SceneRig = class {
36106
36212
  }
36107
36213
  };
36108
36214
  //#endregion
36109
- //#region ../../src/scrawl/ink/inkMaterial.ts
36215
+ //#region src/renderer/shapes/inkMaterial.ts
36110
36216
  var VERTEX = `
36111
36217
  attribute float aAlpha;
36112
36218
  varying vec2 vUv;
@@ -36237,7 +36343,7 @@ var InkMaterials = class {
36237
36343
  }
36238
36344
  };
36239
36345
  //#endregion
36240
- //#region ../../src/scrawl/ink/ribbonGeometry.ts
36346
+ //#region src/renderer/shapes/ribbonGeometry.ts
36241
36347
  /** Ink floats a hair above the board to avoid z-fighting with the surface. */
36242
36348
  var INK_Z = .02;
36243
36349
  /** Highlighter always renders under opaque ink (build prompt §4.1). */
@@ -36288,7 +36394,7 @@ function buildRibbonGeometry(points, baseWidth, z = INK_Z) {
36288
36394
  return geometry;
36289
36395
  }
36290
36396
  //#endregion
36291
- //#region ../../src/scrawl/ink/strokeRenderer.ts
36397
+ //#region src/renderer/shapes/strokeRenderer.ts
36292
36398
  var strokeZ = (stroke) => stroke.tool === "highlighter" ? HIGHLIGHT_Z : INK_Z;
36293
36399
  /**
36294
36400
  * Derives ink meshes from the document by subscription — the document never
@@ -36421,7 +36527,7 @@ function syncMatrix(mesh, stroke) {
36421
36527
  mesh.matrixWorldNeedsUpdate = true;
36422
36528
  }
36423
36529
  //#endregion
36424
- //#region ../../src/scrawl/scene-objects/markerProp.ts
36530
+ //#region src/renderer/shapes/markerProp.ts
36425
36531
  var BODY_LENGTH = 11;
36426
36532
  var BODY_RADIUS = .85;
36427
36533
  var TIP_LENGTH = 1.6;
@@ -36517,7 +36623,7 @@ var MarkerProp = class {
36517
36623
  }
36518
36624
  };
36519
36625
  //#endregion
36520
- //#region ../../src/scrawl/scene-objects/noteGeometry.ts
36626
+ //#region src/renderer/shapes/noteGeometry.ts
36521
36627
  /**
36522
36628
  * Height of the sheet above the board at (u, v) ∈ [0,1]².
36523
36629
  * v = 1 is the adhesive edge (always 0); u = 1 is the peel corner side.
@@ -36561,7 +36667,7 @@ function applyCurl(geometry, curl, peel) {
36561
36667
  color.needsUpdate = true;
36562
36668
  }
36563
36669
  //#endregion
36564
- //#region ../../src/scrawl/scene-objects/stickyNotes.ts
36670
+ //#region src/renderer/shapes/stickyNotes.ts
36565
36671
  var TEXTURE_SIZE = 512;
36566
36672
  var FOCUS_COLOR$4 = 2450411;
36567
36673
  var CURL = .3;
@@ -43137,7 +43243,7 @@ SYNCABLE_PROPS.forEach((prop) => {
43137
43243
  new Box3();
43138
43244
  new Color();
43139
43245
  //#endregion
43140
- //#region ../../src/scrawl/scene-objects/tables.ts
43246
+ //#region src/renderer/shapes/tables.ts
43141
43247
  var FONT_URL$2 = "/fonts/inter-medium.woff";
43142
43248
  var TABLE_Z = .012;
43143
43249
  var HEADER_Z = .014;
@@ -43486,7 +43592,7 @@ var TablesRenderer = class {
43486
43592
  }
43487
43593
  };
43488
43594
  //#endregion
43489
- //#region ../../src/scrawl/scene-objects/textBlocks.ts
43595
+ //#region src/renderer/text/textBlocks.ts
43490
43596
  var FONT_URL$1 = "/fonts/inter-medium.woff";
43491
43597
  var TEXT_Z = .02;
43492
43598
  var FOCUS_COLOR$2 = 2450411;
@@ -43590,7 +43696,7 @@ var TextBlocksRenderer = class {
43590
43696
  }
43591
43697
  };
43592
43698
  //#endregion
43593
- //#region ../../src/scrawl/scene-objects/comments.ts
43699
+ //#region src/renderer/shapes/comments.ts
43594
43700
  var PIN_Z = .04;
43595
43701
  var PIN_WIDTH = 3.6;
43596
43702
  var PIN_HEIGHT = 3.6;
@@ -43758,7 +43864,7 @@ function drawBubblePath(ctx, x, y, w, h, r, tailX, tailY, tailW) {
43758
43864
  ctx.closePath();
43759
43865
  }
43760
43866
  //#endregion
43761
- //#region ../../src/scrawl/scene-objects/images.ts
43867
+ //#region src/renderer/shapes/images.ts
43762
43868
  var IMAGE_Z = .015;
43763
43869
  var FRAME_BORDER = .4;
43764
43870
  var FOCUS_COLOR$1 = 2450411;
@@ -43968,7 +44074,7 @@ function applyPlaceholderTexture(visual, label) {
43968
44074
  visual.imageMesh.material.needsUpdate = true;
43969
44075
  }
43970
44076
  //#endregion
43971
- //#region ../../src/scrawl/scene-objects/kitchenTimers.ts
44077
+ //#region src/renderer/shapes/kitchenTimers.ts
43972
44078
  var FACE_SIZE = 256;
43973
44079
  var BODY_COLOR = 14753096;
43974
44080
  var KNOB_COLOR = 16448250;
@@ -44166,7 +44272,7 @@ function renderTimerFace(timer, canvas, now = Date.now()) {
44166
44272
  return c;
44167
44273
  }
44168
44274
  //#endregion
44169
- //#region ../../src/scrawl/scene-objects/customObjects.ts
44275
+ //#region src/renderer/shapes/customObjects.ts
44170
44276
  var FONT_URL = "/fonts/inter-medium.woff";
44171
44277
  var CUSTOM_Z = .02;
44172
44278
  var FOCUS_COLOR = 2450411;
@@ -44528,7 +44634,7 @@ function disposeObject3D(obj) {
44528
44634
  });
44529
44635
  }
44530
44636
  //#endregion
44531
- //#region ../../src/scrawl/tools/hitTest.ts
44637
+ //#region src/interaction/pointer/hitTest.ts
44532
44638
  /** Topmost (most recently added) stroke under the point, or null. */
44533
44639
  function hitStroke(doc, index, p, slop) {
44534
44640
  const candidates = index.query(p.x - slop, p.y - slop, p.x + slop, p.y + slop);
@@ -44569,7 +44675,7 @@ function distToSegmentSq(p, a, b) {
44569
44675
  return dx * dx + dy * dy;
44570
44676
  }
44571
44677
  //#endregion
44572
- //#region ../../src/scrawl/tools/eraserTool.ts
44678
+ //#region src/interaction/tools/eraserTool.ts
44573
44679
  var DECAY_PER_PASS = .4;
44574
44680
  /** A point can take another decay pass after this long — rubbing works. */
44575
44681
  var REARM_MS = 250;
@@ -44697,7 +44803,7 @@ function partitionByErasure(points) {
44697
44803
  return runs;
44698
44804
  }
44699
44805
  //#endregion
44700
- //#region ../../src/scrawl/tools/noteDrag.ts
44806
+ //#region src/interaction/tools/noteDrag.ts
44701
44807
  /**
44702
44808
  * Drag the focused note in X/Y. The grab is resolved on the note's own
44703
44809
  * z-plane, not the board, so the note doesn't jump under a tilted ray.
@@ -44764,7 +44870,7 @@ var NoteDragState = class {
44764
44870
  }
44765
44871
  };
44766
44872
  //#endregion
44767
- //#region ../../src/scrawl/tools/imageDrag.ts
44873
+ //#region src/interaction/tools/imageDrag.ts
44768
44874
  /** Drag the focused image in X/Y; one command per drag. */
44769
44875
  var ImageDragState = class {
44770
44876
  constructor(ctx) {
@@ -44816,7 +44922,7 @@ var ImageDragState = class {
44816
44922
  }
44817
44923
  };
44818
44924
  //#endregion
44819
- //#region ../../src/scrawl/tools/timerDrag.ts
44925
+ //#region src/interaction/tools/timerDrag.ts
44820
44926
  /** Drag the focused timer in X/Y; one command per drag. */
44821
44927
  var TimerDragState = class {
44822
44928
  constructor(ctx) {
@@ -44868,7 +44974,7 @@ var TimerDragState = class {
44868
44974
  }
44869
44975
  };
44870
44976
  //#endregion
44871
- //#region ../../src/scrawl/tools/shapes.ts
44977
+ //#region src/interaction/tools/shapeGeometry.ts
44872
44978
  /** Uniform synthetic pressure; the ink shader supplies the hand-drawn feel. */
44873
44979
  var PRESSURE = .7;
44874
44980
  var MIN_ELLIPSE_SEGMENTS = 24;
@@ -45057,7 +45163,7 @@ function snappedEnd(a, b) {
45057
45163
  };
45058
45164
  }
45059
45165
  //#endregion
45060
- //#region ../../src/scrawl/tools/shapeTool.ts
45166
+ //#region src/interaction/tools/shapeTool.ts
45061
45167
  var MIN_DRAG_PX$1 = 4;
45062
45168
  /** Drag out a shape; live preview through the real ink material. */
45063
45169
  var ShapeTool = class {
@@ -45088,7 +45194,7 @@ var ShapeTool = class {
45088
45194
  const strokes = this.polylines.map((points) => ({
45089
45195
  id: crypto.randomUUID(),
45090
45196
  color: this.ctx.inkColor(),
45091
- baseWidth: this.ctx.baseWidth(),
45197
+ baseWidth: this.ctx.shapeStrokeWidth(),
45092
45198
  points
45093
45199
  }));
45094
45200
  this.ctx.clusters.assign(strokes[0]);
@@ -45122,13 +45228,13 @@ var ShapeTool = class {
45122
45228
  this.previews[i].visible = polyline !== void 0;
45123
45229
  if (polyline) {
45124
45230
  this.previews[i].geometry.dispose();
45125
- this.previews[i].geometry = buildRibbonGeometry(polyline, this.ctx.baseWidth());
45231
+ this.previews[i].geometry = buildRibbonGeometry(polyline, this.ctx.shapeStrokeWidth());
45126
45232
  }
45127
45233
  }
45128
45234
  }
45129
45235
  };
45130
45236
  //#endregion
45131
- //#region ../../src/scrawl/tools/tableDrag.ts
45237
+ //#region src/interaction/tools/tableDrag.ts
45132
45238
  function getHandleCursor(handle) {
45133
45239
  switch (handle) {
45134
45240
  case "nw":
@@ -45248,7 +45354,7 @@ var TableDragState = class {
45248
45354
  }
45249
45355
  };
45250
45356
  //#endregion
45251
- //#region ../../src/scrawl/tools/tableTool.ts
45357
+ //#region src/interaction/tools/tableTool.ts
45252
45358
  var MIN_DRAG_PX = 6;
45253
45359
  var TableTool = class {
45254
45360
  constructor(ctx) {
@@ -45379,7 +45485,7 @@ var TableTool = class {
45379
45485
  }
45380
45486
  };
45381
45487
  //#endregion
45382
- //#region ../../src/scrawl/tools/commentTool.ts
45488
+ //#region src/interaction/tools/commentTool.ts
45383
45489
  /** Click on the board to drop a new comment pin or open an existing one. */
45384
45490
  var CommentTool = class {
45385
45491
  constructor(ctx) {
@@ -45414,7 +45520,7 @@ var CommentTool = class {
45414
45520
  }
45415
45521
  };
45416
45522
  //#endregion
45417
- //#region ../../src/scrawl/tools/textDrag.ts
45523
+ //#region src/interaction/tools/textDrag.ts
45418
45524
  /** Drag the focused text block in X/Y; one command per drag. */
45419
45525
  var TextDragState = class {
45420
45526
  constructor(ctx) {
@@ -45466,7 +45572,7 @@ var TextDragState = class {
45466
45572
  }
45467
45573
  };
45468
45574
  //#endregion
45469
- //#region ../../src/scrawl/tools/fsm.ts
45575
+ //#region src/interaction/tools/fsm.ts
45470
45576
  var ToolStateMachine = class {
45471
45577
  constructor() {
45472
45578
  this.states = /* @__PURE__ */ new Map();
@@ -45668,7 +45774,7 @@ var IdleState = class {
45668
45774
  }
45669
45775
  };
45670
45776
  //#endregion
45671
- //#region ../../src/scrawl/tools/gizmo.ts
45777
+ //#region src/renderer/selection/gizmo.ts
45672
45778
  var GIZMO_Z = .05;
45673
45779
  var HANDLE_PX = 9;
45674
45780
  var HANDLE_HIT_PX = 12;
@@ -45899,7 +46005,7 @@ function boxesEqual(a, b) {
45899
46005
  return a.minX === b.minX && a.minY === b.minY && a.maxX === b.maxX && a.maxY === b.maxY;
45900
46006
  }
45901
46007
  //#endregion
45902
- //#region ../../src/scrawl/ink/pressure.ts
46008
+ //#region src/interaction/tools/pressure.ts
45903
46009
  var SIM_MAX = 1;
45904
46010
  var SIM_MIN = .3;
45905
46011
  var SMOOTHING = .25;
@@ -45921,7 +46027,7 @@ function clamp(v, lo, hi) {
45921
46027
  return Math.min(hi, Math.max(lo, v));
45922
46028
  }
45923
46029
  //#endregion
45924
- //#region ../../src/scrawl/ink/strokeBuilder.ts
46030
+ //#region src/interaction/tools/strokeBuilder.ts
45925
46031
  var StrokeBuilder = class {
45926
46032
  constructor() {
45927
46033
  this.points = [];
@@ -45971,7 +46077,7 @@ function catmullRom(v0, v1, v2, v3, t) {
45971
46077
  return .5 * (2 * v1 + (-v0 + v2) * t + (2 * v0 - 5 * v1 + 4 * v2 - v3) * t2 + (-v0 + 3 * v1 - 3 * v2 + v3) * t3);
45972
46078
  }
45973
46079
  //#endregion
45974
- //#region ../../src/scrawl/tools/markerTool.ts
46080
+ //#region src/interaction/tools/markerTool.ts
45975
46081
  /** Sample spacing while drawing, in screen pixels. */
45976
46082
  var MIN_SAMPLE_PX = 1.5;
45977
46083
  /** A highlighter is a chisel tip: near-constant width, little pressure play. */
@@ -46074,7 +46180,7 @@ var MarkerTool = class {
46074
46180
  }
46075
46181
  };
46076
46182
  //#endregion
46077
- //#region ../../src/scrawl/tools/orbitTool.ts
46183
+ //#region src/interaction/tools/orbitTool.ts
46078
46184
  /**
46079
46185
  * Orbit-preview (build prompt §4.2): alt-drag tilts the camera off-axis to
46080
46186
  * read the board's material and depth; releasing eases back to the working
@@ -46109,7 +46215,7 @@ var OrbitTool = class {
46109
46215
  }
46110
46216
  };
46111
46217
  //#endregion
46112
- //#region ../../src/scrawl/tools/panTool.ts
46218
+ //#region src/interaction/tools/panTool.ts
46113
46219
  /** Grab-pan: content follows the pointer. */
46114
46220
  var PanTool = class {
46115
46221
  constructor(ctx) {
@@ -46139,7 +46245,7 @@ var PanTool = class {
46139
46245
  }
46140
46246
  };
46141
46247
  //#endregion
46142
- //#region ../../src/scrawl/tools/selection.ts
46248
+ //#region src/renderer/selection/selection.ts
46143
46249
  /** The set of selected stroke ids. Drops ids the document no longer has. */
46144
46250
  var SelectionManager = class {
46145
46251
  constructor(doc) {
@@ -46190,7 +46296,7 @@ var SelectionManager = class {
46190
46296
  }
46191
46297
  };
46192
46298
  //#endregion
46193
- //#region ../../src/scrawl/tools/selectTool.ts
46299
+ //#region src/interaction/tools/selectTool.ts
46194
46300
  var HIT_SLOP_PX = 4;
46195
46301
  var CLICK_TOLERANCE_PX = 3;
46196
46302
  var MIN_SCALE = .05;
@@ -46406,7 +46512,7 @@ function safeRatio(numerator, denominator) {
46406
46512
  return raw;
46407
46513
  }
46408
46514
  //#endregion
46409
- //#region ../../src/scrawl/tools/customTool.ts
46515
+ //#region src/interaction/tools/customTool.ts
46410
46516
  function extensionIdFromToolId(toolId) {
46411
46517
  const slash = toolId.indexOf("/");
46412
46518
  return slash === -1 ? toolId : toolId.slice(0, slash);
@@ -46521,12 +46627,19 @@ var ExtensionCommandBatch = class {
46521
46627
  }
46522
46628
  };
46523
46629
  //#endregion
46524
- //#region ../../src/scrawl/ScrawlEngine.ts
46630
+ //#region src/composition/ScrawlEngine.ts
46525
46631
  /** Marker stroke width in board units (~1% of the default view height). */
46526
46632
  var BASE_WIDTH = .5;
46527
46633
  /** Highlighter chisel width — wide enough to cover a line of handwriting. */
46528
46634
  var HIGHLIGHT_WIDTH = 2.2;
46529
46635
  /**
46636
+ * Shape (rectangle, ellipse, arrow, ...) border width in board units.
46637
+ * Deliberately thinner than BASE_WIDTH: a shape border reads as a crisp,
46638
+ * structural line, not an expressive marker stroke — this is also the
46639
+ * fallback when no Host theme configures ScrawlTheme.shapeStrokeWidth.
46640
+ */
46641
+ var DEFAULT_SHAPE_STROKE_WIDTH = .15;
46642
+ /**
46530
46643
  * Identity of a rendered selection badge: which item, its lock state, and
46531
46644
  * where it sits on screen (rounded to the pixel, so sub-pixel camera drift
46532
46645
  * does not churn React).
@@ -46547,9 +46660,11 @@ function emptyExtensionRegistry() {
46547
46660
  * React never reaches past this class.
46548
46661
  */
46549
46662
  var ScrawlEngine = class {
46550
- constructor(canvas, documentId$1, extensionRegistry = emptyExtensionRegistry(), assetController = new AssetController(void 0)) {
46663
+ constructor(canvas, documentId$1, extensionRegistry = emptyExtensionRegistry(), assetController = new AssetController(void 0), boardTheme = DEFAULT_BOARD_SURFACE_THEME, shapeStrokeWidth = DEFAULT_SHAPE_STROKE_WIDTH) {
46551
46664
  this.canvas = canvas;
46552
46665
  this.extensionRegistry = extensionRegistry;
46666
+ this.boardTheme = boardTheme;
46667
+ this.shapeStrokeWidth = shapeStrokeWidth;
46553
46668
  this.onToolChange = null;
46554
46669
  this.onZoomChange = null;
46555
46670
  this.onAuditEvent = null;
@@ -46596,7 +46711,7 @@ var ScrawlEngine = class {
46596
46711
  this.stampKind = "star";
46597
46712
  this.timerDurationMs = TIMER_DEFAULT_DURATION_MS;
46598
46713
  this.readOnly = false;
46599
- this.scene = new SceneRig(canvas);
46714
+ this.scene = new SceneRig(canvas, boardTheme);
46600
46715
  this.document = new BoardDocument(documentId(documentId$1));
46601
46716
  this.history = new History(this.document);
46602
46717
  this.history.onCommand = (command, kind) => {
@@ -46644,6 +46759,7 @@ var ScrawlEngine = class {
46644
46759
  setTool: (t) => this.setTool(t),
46645
46760
  inkColor: () => this.tool === "highlighter" ? this.highlightColor : this.color,
46646
46761
  baseWidth: () => this.tool === "highlighter" ? HIGHLIGHT_WIDTH : BASE_WIDTH,
46762
+ shapeStrokeWidth: () => this.shapeStrokeWidth,
46647
46763
  activeTool: () => this.tool,
46648
46764
  isSpaceHeld: () => this.spaceHeld,
46649
46765
  pointerToBoard: (e) => pointerToBoard(e.clientX, e.clientY, canvas, this.scene.cameraRig.camera),
@@ -46903,6 +47019,15 @@ var ScrawlEngine = class {
46903
47019
  height: rig.height
46904
47020
  };
46905
47021
  }
47022
+ /** Live update of the board surface color/grid — the engine's identity stays fixed across theme changes. */
47023
+ applyBoardTheme(theme) {
47024
+ this.boardTheme = theme;
47025
+ this.scene.applyBoardTheme(theme);
47026
+ }
47027
+ /** Live update of the shape tool's border width (board units). Already-drawn shapes keep the width they were drawn with. */
47028
+ setShapeStrokeWidth(width) {
47029
+ this.shapeStrokeWidth = width;
47030
+ }
46906
47031
  /**
46907
47032
  * Snap to a peer's camera. No-op while drawing so pointer-to-board
46908
47033
  * mapping does not warp the in-progress stroke.
@@ -46921,7 +47046,7 @@ var ScrawlEngine = class {
46921
47046
  }
46922
47047
  /** The saved document as standalone SVG — a pure walk of the wire format. */
46923
47048
  exportSVG() {
46924
- return documentToSVG(this.document.toJSON(), Date.now(), this.extensionRegistry);
47049
+ return documentToSVG(this.document.toJSON(), Date.now(), this.extensionRegistry, this.boardTheme.color);
46925
47050
  }
46926
47051
  /**
46927
47052
  * Content-framed PNG snapshot. Renders once offscreen-style with UI props
@@ -46957,6 +47082,7 @@ var ScrawlEngine = class {
46957
47082
  this.notesRenderer.setFocus(null);
46958
47083
  this.textsRenderer.setFocus(null);
46959
47084
  this.renderer.setGhostsVisible(false);
47085
+ const restoreGrid = this.scene.suppressGrid();
46960
47086
  try {
46961
47087
  rig.yaw = 0;
46962
47088
  rig.pitch = 0;
@@ -46967,6 +47093,7 @@ var ScrawlEngine = class {
46967
47093
  const dataUrl = this.scene.renderer.domElement.toDataURL("image/png");
46968
47094
  return await (await fetch(dataUrl)).blob();
46969
47095
  } finally {
47096
+ restoreGrid();
46970
47097
  rig.center.x = saved.x;
46971
47098
  rig.center.y = saved.y;
46972
47099
  rig.height = saved.height;
@@ -47962,7 +48089,7 @@ var ScrawlEngine = class {
47962
48089
  }
47963
48090
  };
47964
48091
  //#endregion
47965
- //#region src/controller-internal.ts
48092
+ //#region src/composition/controller-internal.ts
47966
48093
  function createBoardController(options) {
47967
48094
  return createBoardControllerWithEngine(options);
47968
48095
  }
@@ -47976,7 +48103,16 @@ function createBoardControllerWithEngine(options, existingEngine) {
47976
48103
  }
47977
48104
  const extensionRegistry = registryResult.registry;
47978
48105
  const assetController = new AssetController(options.assetResolver, { cacheBytes: options.assetCacheBytes });
47979
- const engine = existingEngine ?? (options.canvas ? new ScrawlEngine(options.canvas, options.document.id, extensionRegistry, assetController) : null);
48106
+ let currentBoardTheme = {
48107
+ color: options.boardTheme?.surface ?? DEFAULT_BOARD_SURFACE_THEME.color,
48108
+ gridMode: options.boardTheme?.gridMode ?? DEFAULT_BOARD_SURFACE_THEME.gridMode,
48109
+ gridColor: options.boardTheme?.gridColor ?? DEFAULT_BOARD_SURFACE_THEME.gridColor,
48110
+ gridSpacing: options.boardTheme?.gridSpacing ?? DEFAULT_BOARD_SURFACE_THEME.gridSpacing,
48111
+ gridLineWidth: options.boardTheme?.gridLineWidth ?? DEFAULT_BOARD_SURFACE_THEME.gridLineWidth,
48112
+ surfaceTexture: options.boardTheme?.surfaceTexture ?? DEFAULT_BOARD_SURFACE_THEME.surfaceTexture
48113
+ };
48114
+ let currentShapeStrokeWidth = options.boardTheme?.shapeStrokeWidth ?? .15;
48115
+ const engine = existingEngine ?? (options.canvas ? new ScrawlEngine(options.canvas, options.document.id, extensionRegistry, assetController, currentBoardTheme, currentShapeStrokeWidth) : null);
47980
48116
  const document = engine?.document ?? new BoardDocument(documentId(options.document.id));
47981
48117
  const history = engine?.history ?? new History(document);
47982
48118
  const createId = options.createId ?? (() => crypto.randomUUID());
@@ -48460,6 +48596,21 @@ function createBoardControllerWithEngine(options, existingEngine) {
48460
48596
  changed();
48461
48597
  }
48462
48598
  },
48599
+ boardTheme: { set(theme) {
48600
+ currentBoardTheme = {
48601
+ color: theme.surface ?? currentBoardTheme.color,
48602
+ gridMode: theme.gridMode ?? currentBoardTheme.gridMode,
48603
+ gridColor: theme.gridColor ?? currentBoardTheme.gridColor,
48604
+ gridSpacing: theme.gridSpacing ?? currentBoardTheme.gridSpacing,
48605
+ gridLineWidth: theme.gridLineWidth ?? currentBoardTheme.gridLineWidth,
48606
+ surfaceTexture: theme.surfaceTexture ?? currentBoardTheme.surfaceTexture
48607
+ };
48608
+ engine?.applyBoardTheme(currentBoardTheme);
48609
+ if (theme.shapeStrokeWidth !== void 0) {
48610
+ currentShapeStrokeWidth = theme.shapeStrokeWidth;
48611
+ engine?.setShapeStrokeWidth(currentShapeStrokeWidth);
48612
+ }
48613
+ } },
48463
48614
  view: {
48464
48615
  fit() {
48465
48616
  assertActive();
@@ -48764,10 +48915,10 @@ function createBoardControllerWithEngine(options, existingEngine) {
48764
48915
  gather: (nextView) => engine?.applyGatherView(nextView) ?? false
48765
48916
  },
48766
48917
  export: {
48767
- svg: () => engine ? engine.exportSVG() : documentToSVG(document.toJSON(), Date.now(), extensionRegistry),
48918
+ svg: () => engine ? engine.exportSVG() : documentToSVG(document.toJSON(), Date.now(), extensionRegistry, currentBoardTheme.color),
48768
48919
  png: (options) => engine ? engine.exportPNG(options?.maxSide, { awaitAssets: options?.awaitAssets }) : Promise.resolve(null),
48769
48920
  json: () => document.toJSON(),
48770
- svgAsync: (svgOptions) => exportDocumentSVGWithAssets(document.toJSON(), Date.now(), extensionRegistry, assetController, svgOptions)
48921
+ svgAsync: (svgOptions) => exportDocumentSVGWithAssets(document.toJSON(), Date.now(), extensionRegistry, assetController, currentBoardTheme.color, svgOptions)
48771
48922
  },
48772
48923
  assets: { async ingest(bytes, mediaType, name, signal) {
48773
48924
  assertActive();
@@ -49229,7 +49380,7 @@ function createLocalBoard(options) {
49229
49380
  };
49230
49381
  }
49231
49382
  //#endregion
49232
- //#region src/theme.ts
49383
+ //#region src/ui/theme.ts
49233
49384
  var scrawlThemePresets = Object.freeze({
49234
49385
  light: Object.freeze({
49235
49386
  surface: "#ffffff",
@@ -49254,7 +49405,14 @@ var scrawlThemePresets = Object.freeze({
49254
49405
  elevationHigh: "0 8px 20px rgba(28, 28, 26, 0.18)",
49255
49406
  motionDuration: 140,
49256
49407
  motionEasing: "cubic-bezier(0.2, 0, 0, 1)",
49257
- density: "comfortable"
49408
+ density: "comfortable",
49409
+ boardSurface: "#f5f5f3",
49410
+ boardSurfaceTexture: "flat",
49411
+ gridMode: "none",
49412
+ gridColor: "#deded8",
49413
+ gridSpacing: 5,
49414
+ gridLineWidth: 1,
49415
+ shapeStrokeWidth: .15
49258
49416
  }),
49259
49417
  dark: Object.freeze({
49260
49418
  surface: "#171816",
@@ -49279,7 +49437,14 @@ var scrawlThemePresets = Object.freeze({
49279
49437
  elevationHigh: "0 8px 20px rgba(0, 0, 0, 0.38)",
49280
49438
  motionDuration: 140,
49281
49439
  motionEasing: "cubic-bezier(0.2, 0, 0, 1)",
49282
- density: "comfortable"
49440
+ density: "comfortable",
49441
+ boardSurface: "#232420",
49442
+ boardSurfaceTexture: "flat",
49443
+ gridMode: "none",
49444
+ gridColor: "#33352f",
49445
+ gridSpacing: 5,
49446
+ gridLineWidth: 1,
49447
+ shapeStrokeWidth: .15
49283
49448
  })
49284
49449
  });
49285
49450
  var variableNames = {
@@ -49305,7 +49470,14 @@ var variableNames = {
49305
49470
  elevationHigh: "--scrawl-elevation-high",
49306
49471
  motionDuration: "--scrawl-motion-duration",
49307
49472
  motionEasing: "--scrawl-motion-easing",
49308
- density: "--scrawl-density"
49473
+ density: "--scrawl-density",
49474
+ boardSurface: "--scrawl-board-surface",
49475
+ boardSurfaceTexture: "--scrawl-board-surface-texture",
49476
+ gridMode: "--scrawl-grid-mode",
49477
+ gridColor: "--scrawl-grid-color",
49478
+ gridSpacing: "--scrawl-grid-spacing",
49479
+ gridLineWidth: "--scrawl-grid-line-width",
49480
+ shapeStrokeWidth: "--scrawl-shape-stroke-width"
49309
49481
  };
49310
49482
  var colorTokens = /* @__PURE__ */ new Set([
49311
49483
  "surface",
@@ -49318,7 +49490,9 @@ var colorTokens = /* @__PURE__ */ new Set([
49318
49490
  "selection",
49319
49491
  "danger",
49320
49492
  "warning",
49321
- "success"
49493
+ "success",
49494
+ "boardSurface",
49495
+ "gridColor"
49322
49496
  ]);
49323
49497
  var knownTokens = new Set(Object.keys(scrawlThemePresets.light));
49324
49498
  function validateScrawlTheme(theme) {
@@ -49355,6 +49529,26 @@ function validateScrawlTheme(theme) {
49355
49529
  token,
49356
49530
  message: "Expected comfortable or compact"
49357
49531
  });
49532
+ else if (token === "gridMode" && value !== "none" && value !== "line" && value !== "dot") diagnostics.push({
49533
+ token,
49534
+ message: "Expected none, line, or dot"
49535
+ });
49536
+ else if (token === "boardSurfaceTexture" && value !== "flat" && value !== "textured") diagnostics.push({
49537
+ token,
49538
+ message: "Expected flat or textured"
49539
+ });
49540
+ else if (token === "gridSpacing" && (!finiteNumber(value) || value < .5 || value > 50)) diagnostics.push({
49541
+ token,
49542
+ message: "Expected spacing 0.5-50 board units"
49543
+ });
49544
+ else if (token === "gridLineWidth" && (!finiteNumber(value) || value < .5 || value > 8)) diagnostics.push({
49545
+ token,
49546
+ message: "Expected line width 0.5-8px"
49547
+ });
49548
+ else if (token === "shapeStrokeWidth" && (!finiteNumber(value) || value < .01 || value > 5)) diagnostics.push({
49549
+ token,
49550
+ message: "Expected stroke width 0.01-5 board units"
49551
+ });
49358
49552
  else if ((token.endsWith("Family") || token.startsWith("elevation") || token === "motionEasing") && (typeof value !== "string" || !value.trim() || /[;{}]/.test(value))) diagnostics.push({
49359
49553
  token,
49360
49554
  message: "Expected a safe CSS value"
@@ -49425,12 +49619,12 @@ function luminance(color) {
49425
49619
  return channels[0] * .2126 + channels[1] * .7152 + channels[2] * .0722;
49426
49620
  }
49427
49621
  function formatValue(token, value) {
49428
- if (token === "baseFontSize" || token === "controlRadius" || token === "panelRadius") return `${value}px`;
49622
+ if (token === "baseFontSize" || token === "controlRadius" || token === "panelRadius" || token === "gridLineWidth") return `${value}px`;
49429
49623
  if (token === "motionDuration") return `${value}ms`;
49430
49624
  return String(value);
49431
49625
  }
49432
49626
  //#endregion
49433
- //#region src/inline-editing.tsx
49627
+ //#region src/ui/dialogs/inline-editing.tsx
49434
49628
  /** Inline note, text, and table-cell editors — driven entirely by public controller events and commands. */
49435
49629
  function InlineEditors({ controller, renderPortal }) {
49436
49630
  const [editor, setEditor] = useState(null);
@@ -49661,7 +49855,7 @@ function InlineEditors({ controller, renderPortal }) {
49661
49855
  }))] });
49662
49856
  }
49663
49857
  //#endregion
49664
- //#region src/style-shelf.tsx
49858
+ //#region src/ui/toolbar/style-shelf.tsx
49665
49859
  var ERASER_RADII = [
49666
49860
  ["Fine", 1],
49667
49861
  ["Medium", 2],
@@ -49824,7 +50018,7 @@ function StyleShelf({ controller, snapshot }) {
49824
50018
  });
49825
50019
  }
49826
50020
  //#endregion
49827
- //#region src/default-ui.tsx
50021
+ //#region src/ui/default-ui.tsx
49828
50022
  var PRIMARY_TOOLS = [
49829
50023
  {
49830
50024
  tool: "select",
@@ -50353,7 +50547,7 @@ function download(blob, name) {
50353
50547
  URL.revokeObjectURL(url);
50354
50548
  }
50355
50549
  //#endregion
50356
- //#region src/presence.tsx
50550
+ //#region src/ui/panels/presence.tsx
50357
50551
  var labelStyle = {
50358
50552
  borderRadius: 999,
50359
50553
  color: "#fff",
@@ -50510,6 +50704,17 @@ function ScrawlProvider({ controller, children, preset = "light", theme, portalC
50510
50704
  style
50511
50705
  ]);
50512
50706
  useEffect(() => reportThemeDiagnostics(resolved.diagnostics, onThemeDiagnostic), [resolved, onThemeDiagnostic]);
50707
+ useEffect(() => {
50708
+ controller.boardTheme.set({
50709
+ surface: resolved.values.boardSurface,
50710
+ surfaceTexture: resolved.values.boardSurfaceTexture,
50711
+ gridMode: resolved.values.gridMode,
50712
+ gridColor: resolved.values.gridColor,
50713
+ gridSpacing: resolved.values.gridSpacing,
50714
+ gridLineWidth: resolved.values.gridLineWidth,
50715
+ shapeStrokeWidth: resolved.values.shapeStrokeWidth
50716
+ });
50717
+ }, [controller, resolved]);
50513
50718
  useDeferredDisposal(controller, disposeOnUnmount);
50514
50719
  const value = useMemo(() => ({
50515
50720
  controller,
@@ -50557,8 +50762,19 @@ function Scrawl({ children, preset, theme, portalContainer, className, style, on
50557
50762
  try {
50558
50763
  if (!runtimeRef.current) {
50559
50764
  const canvas = suppliedCanvas === void 0 ? ownedCanvasRef.current : suppliedCanvas;
50765
+ const resolved = resolveScrawlTheme(preset, theme);
50766
+ const boardTheme = {
50767
+ surface: resolved.values.boardSurface,
50768
+ surfaceTexture: resolved.values.boardSurfaceTexture,
50769
+ gridMode: resolved.values.gridMode,
50770
+ gridColor: resolved.values.gridColor,
50771
+ gridSpacing: resolved.values.gridSpacing,
50772
+ gridLineWidth: resolved.values.gridLineWidth,
50773
+ shapeStrokeWidth: resolved.values.shapeStrokeWidth
50774
+ };
50560
50775
  controller = createBoardController({
50561
50776
  ...options,
50777
+ boardTheme,
50562
50778
  ...canvas ? { canvas } : {}
50563
50779
  });
50564
50780
  runtimeRef.current = {
@@ -50794,4 +51010,4 @@ function ScrawlBoard({ documentId, initialDocument, onReady, className, style })
50794
51010
  });
50795
51011
  }
50796
51012
  //#endregion
50797
- export { DefaultBoardChrome, InlineEditors, MultiplayerCursors, Scrawl, ScrawlBoard, ScrawlCanvas, ScrawlDefaultUI, ScrawlPortal, ScrawlProvider, StyleShelf, resolveScrawlTheme, scrawlThemePresets, useScrawlController, useScrawlSnapshot, useScrawlTheme, validateScrawlTheme };
51013
+ export { AssetResolutionError, DefaultBoardChrome, InlineEditors, MultiplayerCursors, SUPPORTED_ASSET_MEDIA_TYPES, Scrawl, ScrawlBoard, ScrawlCanvas, ScrawlDefaultUI, ScrawlPortal, ScrawlProvider, StyleShelf, assetRef, clampAssetCacheBytes, cloneCustomObject, isAssetRef, resolveScrawlTheme, scrawlThemePresets, useScrawlController, useScrawlSnapshot, useScrawlTheme, validateScrawlTheme };