@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/browser.js CHANGED
@@ -1,4 +1,4 @@
1
- //#region src/core-internal/assets.ts
1
+ //#region src/core/shapes/assets.ts
2
2
  var ASSET_REF_PATTERN = /^asset:[a-z0-9](?:[a-z0-9.-]{0,61}[a-z0-9])?:[A-Za-z0-9._~-]+$/;
3
3
  function isAssetRef(value) {
4
4
  return typeof value === "string" && ASSET_REF_PATTERN.test(value) && new TextEncoder().encode(value).length <= 512;
@@ -20,12 +20,17 @@ var AssetResolutionError = class extends Error {
20
20
  var ASSET_CACHE_BYTES_DEFAULT = 256 * 1024 * 1024;
21
21
  var ASSET_CACHE_BYTES_MIN = 64 * 1024 * 1024;
22
22
  var ASSET_CACHE_BYTES_MAX = 512 * 1024 * 1024;
23
+ var SUPPORTED_ASSET_MEDIA_TYPES = [
24
+ "image/png",
25
+ "image/jpeg",
26
+ "image/webp"
27
+ ];
23
28
  function clampAssetCacheBytes(value) {
24
29
  if (value === void 0 || !Number.isFinite(value)) return ASSET_CACHE_BYTES_DEFAULT;
25
30
  return Math.min(ASSET_CACHE_BYTES_MAX, Math.max(ASSET_CACHE_BYTES_MIN, Math.floor(value)));
26
31
  }
27
32
  //#endregion
28
- //#region src/core-internal/identifiers.ts
33
+ //#region src/core/document/identifiers.ts
29
34
  function documentId(value) {
30
35
  if (!value) throw new TypeError("DocumentId cannot be empty");
31
36
  return value;
@@ -226,7 +231,7 @@ function validation(message, path) {
226
231
  return new DocumentRecoveryError("DOCUMENT_VALIDATION_FAILED", message, void 0, path);
227
232
  }
228
233
  //#endregion
229
- //#region src/core-internal/affine2d.ts
234
+ //#region src/core/shapes/affine2d.ts
230
235
  var IDENTITY = [
231
236
  1,
232
237
  0,
@@ -310,7 +315,7 @@ function avgScale(m) {
310
315
  return (Math.hypot(m[0], m[1]) + Math.hypot(m[2], m[3])) / 2;
311
316
  }
312
317
  //#endregion
313
- //#region src/core-internal/boardSearch.ts
318
+ //#region src/core/document/boardSearch.ts
314
319
  function hay(s) {
315
320
  return s.toLowerCase();
316
321
  }
@@ -375,7 +380,7 @@ function searchBoard(query, board) {
375
380
  return hits;
376
381
  }
377
382
  //#endregion
378
- //#region src/core-internal/types.ts
383
+ //#region src/core/shapes/types.ts
379
384
  /** Erasure level at which a point counts as fully erased. */
380
385
  var ERASE_THRESHOLD = .95;
381
386
  function cloneStroke(stroke) {
@@ -442,13 +447,12 @@ function measureTable(table) {
442
447
  height: table.rowHeights.reduce((sum, h) => sum + h, 0)
443
448
  };
444
449
  }
445
- var BOARD_COLOR = "#FFFFFF";
446
450
  var FOG_COLOR = "#FFFFFF";
447
451
  function cloneImage(img) {
448
452
  return { ...img };
449
453
  }
450
454
  //#endregion
451
- //#region src/core-internal/clusterStore.ts
455
+ //#region src/core/document/clusterStore.ts
452
456
  /** A stroke joins a cluster last written to within this window at full reach. */
453
457
  var RECENT_MS = 8e3;
454
458
  /** Older clusters are still joinable, at half the gap threshold. */
@@ -619,7 +623,7 @@ function clamp$2(v, lo, hi) {
619
623
  return Math.min(hi, Math.max(lo, v));
620
624
  }
621
625
  //#endregion
622
- //#region src/core-internal/extensionTypes.ts
626
+ //#region src/core/shapes/extensionTypes.ts
623
627
  function cloneCustomObject(object) {
624
628
  return {
625
629
  ...object,
@@ -629,7 +633,7 @@ function cloneCustomObject(object) {
629
633
  };
630
634
  }
631
635
  //#endregion
632
- //#region src/core-internal/kitchenTimer.ts
636
+ //#region src/core/shapes/kitchenTimer.ts
633
637
  var TIMER_DEFAULT_DURATION_MS = 300 * 1e3;
634
638
  function cloneTimer(timer) {
635
639
  return { ...timer };
@@ -679,7 +683,7 @@ function formatTimer(ms) {
679
683
  return `${Math.floor(total / 60)}:${(total % 60).toString().padStart(2, "0")}`;
680
684
  }
681
685
  //#endregion
682
- //#region src/core-internal/itemLock.ts
686
+ //#region src/core/shapes/itemLock.ts
683
687
  /**
684
688
  * Apply or clear a lock in place. Unlock always drops the holder so a stale
685
689
  * name cannot linger on an unlocked item.
@@ -716,7 +720,7 @@ function serializeLock(item) {
716
720
  };
717
721
  }
718
722
  //#endregion
719
- //#region src/core-internal/document.ts
723
+ //#region src/core/document/document.ts
720
724
  /**
721
725
  * The one place a live Stroke becomes its wire representation — in
722
726
  * particular, points collapse from {x,y,pressure,erase?} objects into
@@ -1275,7 +1279,7 @@ function computeBBox(stroke) {
1275
1279
  return world;
1276
1280
  }
1277
1281
  //#endregion
1278
- //#region src/core-internal/commands.ts
1282
+ //#region src/core/commands/commands.ts
1279
1283
  /** One drawing action — a marker stroke, or a shape's strokes as one unit. */
1280
1284
  var AddStrokesCommand = class {
1281
1285
  constructor(strokes) {
@@ -1617,7 +1621,7 @@ var LockItemsCommand = class {
1617
1621
  }
1618
1622
  };
1619
1623
  //#endregion
1620
- //#region src/core-internal/history.ts
1624
+ //#region src/core/history/history.ts
1621
1625
  var LIMIT = 200;
1622
1626
  var History = class {
1623
1627
  constructor(doc) {
@@ -1678,7 +1682,7 @@ var History = class {
1678
1682
  }
1679
1683
  };
1680
1684
  //#endregion
1681
- //#region src/core-internal/ops.ts
1685
+ //#region src/core/events/ops.ts
1682
1686
  var ADDED = [
1683
1687
  ["added", "strokes"],
1684
1688
  ["notesAdded", "notes"],
@@ -1732,7 +1736,7 @@ function changeToOps(change, restoring = false) {
1732
1736
  return ops;
1733
1737
  }
1734
1738
  //#endregion
1735
- //#region src/core-internal/ribbonEdges.ts
1739
+ //#region src/core/shapes/ribbonEdges.ts
1736
1740
  var MIN_WIDTH_FACTOR = .35;
1737
1741
  var END_TAPER = .55;
1738
1742
  function ribbonEdges(points, baseWidth) {
@@ -1764,7 +1768,7 @@ function ribbonEdges(points, baseWidth) {
1764
1768
  return out;
1765
1769
  }
1766
1770
  //#endregion
1767
- //#region src/core-internal/spatialIndex.ts
1771
+ //#region src/core/document/spatialIndex.ts
1768
1772
  var CELL = 8;
1769
1773
  var SpatialIndex = class {
1770
1774
  constructor(doc) {
@@ -1837,7 +1841,7 @@ function cellsOf(box) {
1837
1841
  return keys;
1838
1842
  }
1839
1843
  //#endregion
1840
- //#region src/core-internal/stamps.ts
1844
+ //#region src/core/shapes/stamps.ts
1841
1845
  var STAMPS = [
1842
1846
  {
1843
1847
  kind: "star",
@@ -1885,7 +1889,7 @@ function isStampKind(value) {
1885
1889
  return typeof value === "string" && STAMPS.some((s) => s.kind === value);
1886
1890
  }
1887
1891
  //#endregion
1888
- //#region src/core-internal/svg.ts
1892
+ //#region src/persistence/serialization/svg.ts
1889
1893
  /** Matches the key light used by the board shader and note shadows. */
1890
1894
  var LIGHT = {
1891
1895
  x: -.25,
@@ -1895,12 +1899,19 @@ var LIGHT = {
1895
1899
  var MARGIN = 4;
1896
1900
  var NOTE_FONT_RATIO = 44 / 512;
1897
1901
  var NOTE_PAD_RATIO = 40 / 512;
1902
+ /** Matches the theme system's light-preset `boardSurface` default (theme.ts) — kept in sync manually, since core-internal stays decoupled from the theme layer. */
1903
+ var DEFAULT_BACKGROUND_COLOR = "#f5f5f3";
1898
1904
  /**
1899
1905
  * `registry` is optional and only enables rendering Custom objects through
1900
1906
  * their own `describe()` — without it (or for an object whose extension
1901
1907
  * isn't in it), Custom objects still export via the standard fallback
1902
1908
  * placeholder (`fallback.bounds`/`label`), never silently dropped.
1903
1909
  *
1910
+ * `backgroundColor` should be the Host's actual resolved `boardTheme.surface`
1911
+ * so an export matches what was on screen; defaults to the theme system's
1912
+ * own light-preset default when the caller doesn't have one on hand. The
1913
+ * board's reference grid, if any, is a screen-only aid and never exported.
1914
+ *
1904
1915
  * `resolvedAssets` (ticket #23) maps an Asset reference to an already-
1905
1916
  * resolved `data:` URI — see `assetExport.ts`'s `exportDocumentSVGWithAssets`,
1906
1917
  * which is the only intended caller that ever passes one. Without it, every
@@ -1908,7 +1919,7 @@ var NOTE_PAD_RATIO = 40 / 512;
1908
1919
  * placeholder instead of guessing at a URL; a legacy `src`-backed image is
1909
1920
  * unaffected either way.
1910
1921
  */
1911
- function documentToSVG(doc, now = 0, registry, resolvedAssets) {
1922
+ function documentToSVG(doc, now = 0, registry, backgroundColor = DEFAULT_BACKGROUND_COLOR, resolvedAssets) {
1912
1923
  const notes = doc.notes ?? [];
1913
1924
  const texts = doc.textBlocks ?? [];
1914
1925
  const tables = doc.tables ?? [];
@@ -1921,7 +1932,7 @@ function documentToSVG(doc, now = 0, registry, resolvedAssets) {
1921
1932
  const highlights = doc.strokes.filter((s) => s.tool === "highlighter");
1922
1933
  const ink = doc.strokes.filter((s) => s.tool !== "highlighter");
1923
1934
  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${view}">\n<defs>\n${defs}\n</defs>\n${[
1924
- `<rect x="${fmt(bounds.minX)}" y="${fmt(bounds.minY)}" width="${fmt(bounds.width)}" height="${fmt(bounds.height)}" fill="${BOARD_COLOR}"/>`,
1935
+ `<rect x="${fmt(bounds.minX)}" y="${fmt(bounds.minY)}" width="${fmt(bounds.width)}" height="${fmt(bounds.height)}" fill="${backgroundColor}"/>`,
1925
1936
  ...images.map((image) => imageToElement(image, resolvedAssets)),
1926
1937
  ...tables.map(tableToGroup),
1927
1938
  ...highlights.flatMap(strokeToPaths),
@@ -2245,7 +2256,7 @@ function fmt(n) {
2245
2256
  return String(Math.round(n * 100) / 100);
2246
2257
  }
2247
2258
  //#endregion
2248
- //#region src/core-internal/sceneGeometry.ts
2259
+ //#region src/core/shapes/sceneGeometry.ts
2249
2260
  var EMPTY_BOUNDS = {
2250
2261
  x: 0,
2251
2262
  y: 0,
@@ -2309,7 +2320,7 @@ function textCorners(x, y, text, fontSize, align, m) {
2309
2320
  return rectCorners(x + (align === "center" ? -width / 2 : align === "end" ? -width : 0), y, width, height, m);
2310
2321
  }
2311
2322
  //#endregion
2312
- //#region src/core-internal/customObjects.ts
2323
+ //#region src/core/shapes/customObjects.ts
2313
2324
  var FALLBACK_LABEL_MAX_LENGTH = 80;
2314
2325
  function toReadonly(object) {
2315
2326
  return object;
@@ -2438,7 +2449,7 @@ function extensionIdFromType(type) {
2438
2449
  return slash === -1 ? type : type.slice(0, slash);
2439
2450
  }
2440
2451
  //#endregion
2441
- //#region src/core-internal/extensionRegistry.ts
2452
+ //#region src/core/shapes/extensionRegistry.ts
2442
2453
  function buildExtensionRegistry(extensions) {
2443
2454
  const list = extensions ?? [];
2444
2455
  const diagnostics = [];
@@ -2535,7 +2546,7 @@ function isWellFormedNamespacedId(id) {
2535
2546
  return isWellFormedId(id) && id.includes("/") && !id.startsWith("/") && !id.endsWith("/");
2536
2547
  }
2537
2548
  //#endregion
2538
- //#region src/core-internal/assetValidation.ts
2549
+ //#region src/persistence/adapters/assetValidation.ts
2539
2550
  function sniffImageMediaType(bytes) {
2540
2551
  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";
2541
2552
  if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
@@ -2622,7 +2633,7 @@ function validateDecodedDimensions(width, height) {
2622
2633
  };
2623
2634
  }
2624
2635
  //#endregion
2625
- //#region src/assetController.ts
2636
+ //#region src/persistence/adapters/assetController.ts
2626
2637
  var FAILURE_COALESCE_MS = 4e3;
2627
2638
  var AssetController = class {
2628
2639
  constructor(resolver, options) {
@@ -2801,7 +2812,7 @@ function toAssetResolutionError(cause, ref) {
2801
2812
  return new AssetResolutionError("unknown", true, cause instanceof Error ? cause.message : String(cause), ref);
2802
2813
  }
2803
2814
  //#endregion
2804
- //#region src/core-internal/assetExport.ts
2815
+ //#region src/persistence/serialization/assetRefCollector.ts
2805
2816
  /** Every unique Asset reference in the document, and which object(s) use it. */
2806
2817
  function collectDocumentAssetRefs(doc, registry) {
2807
2818
  const refs = /* @__PURE__ */ new Map();
@@ -2828,7 +2839,7 @@ function collectSceneAssetRefs(node, objectId, add) {
2828
2839
  if (node.kind === "group") for (const child of node.children) collectSceneAssetRefs(child, objectId, add);
2829
2840
  }
2830
2841
  //#endregion
2831
- //#region src/assetExport.ts
2842
+ //#region src/persistence/serialization/assetExport.ts
2832
2843
  /** Strict mode's rejection — never contains credentials, a fetchable location, or a raw resolver message. */
2833
2844
  var SvgAssetExportError = class extends Error {
2834
2845
  constructor(missingAssets) {
@@ -2837,7 +2848,7 @@ var SvgAssetExportError = class extends Error {
2837
2848
  this.name = "SvgAssetExportError";
2838
2849
  }
2839
2850
  };
2840
- async function exportDocumentSVGWithAssets(doc, now, registry, assetController, options) {
2851
+ async function exportDocumentSVGWithAssets(doc, now, registry, assetController, backgroundColor, options) {
2841
2852
  const refs = collectDocumentAssetRefs(doc, registry);
2842
2853
  const resolvedAssets = /* @__PURE__ */ new Map();
2843
2854
  const missingAssets = [];
@@ -2873,7 +2884,7 @@ async function exportDocumentSVGWithAssets(doc, now, registry, assetController,
2873
2884
  }
2874
2885
  if (missingAssets.length > 0 && options?.missingAssets !== "placeholder") throw new SvgAssetExportError(missingAssets);
2875
2886
  return {
2876
- svg: documentToSVG(doc, now, registry, resolvedAssets),
2887
+ svg: documentToSVG(doc, now, registry, backgroundColor, resolvedAssets),
2877
2888
  missingAssets
2878
2889
  };
2879
2890
  }
@@ -2884,7 +2895,7 @@ function toDataUri(bytes, mediaType) {
2884
2895
  return `data:${mediaType};base64,${btoa(binary)}`;
2885
2896
  }
2886
2897
  //#endregion
2887
- //#region src/assetIngestion.ts
2898
+ //#region src/persistence/adapters/assetIngestion.ts
2888
2899
  async function decodeDimensionsWithImageBitmap(bytes, mediaType) {
2889
2900
  const blob = new Blob([bytes], { type: mediaType });
2890
2901
  let bitmap;
@@ -35530,7 +35541,7 @@ var WebGLRenderer = class {
35530
35541
  }
35531
35542
  };
35532
35543
  //#endregion
35533
- //#region ../../src/scrawl/data/auditEvents.ts
35544
+ //#region src/core/events/auditEvents.ts
35534
35545
  var BY_LABEL = {
35535
35546
  draw: {
35536
35547
  action: "create",
@@ -35650,7 +35661,7 @@ function lowerFirst(s) {
35650
35661
  return s.charAt(0).toLowerCase() + s.slice(1);
35651
35662
  }
35652
35663
  //#endregion
35653
- //#region ../../src/scrawl/assets/AssetTextureCache.ts
35664
+ //#region src/renderer/shapes/AssetTextureCache.ts
35654
35665
  var AssetTextureCache = class {
35655
35666
  constructor(assetController) {
35656
35667
  this.assetController = assetController;
@@ -35741,7 +35752,7 @@ function toDecodeError(cause, ref) {
35741
35752
  return new AssetResolutionError("decode-failed", false, cause instanceof Error ? cause.message : String(cause), ref);
35742
35753
  }
35743
35754
  //#endregion
35744
- //#region ../../src/scrawl/engine/raycast.ts
35755
+ //#region src/renderer/canvas/raycast.ts
35745
35756
  var origin$1 = new Vector3();
35746
35757
  var target = new Vector3();
35747
35758
  function pointerToBoard(clientX, clientY, canvas, camera) {
@@ -35772,7 +35783,7 @@ function pointerToPlane(clientX, clientY, canvas, camera, z) {
35772
35783
  };
35773
35784
  }
35774
35785
  //#endregion
35775
- //#region ../../src/scrawl/engine/fog.ts
35786
+ //#region src/renderer/canvas/fog.ts
35776
35787
  function createFogUniforms(color) {
35777
35788
  return {
35778
35789
  uFogColor: { value: new Color(color) },
@@ -35793,7 +35804,28 @@ var FOG_GLSL = `
35793
35804
  }
35794
35805
  `;
35795
35806
  //#endregion
35796
- //#region ../../src/scrawl/engine/boardSurface.ts
35807
+ //#region src/renderer/canvas/boardSurface.ts
35808
+ var GRID_MODE_CODE = {
35809
+ none: 0,
35810
+ line: 1,
35811
+ dot: 2
35812
+ };
35813
+ var TEXTURE_CODE = {
35814
+ flat: 0,
35815
+ textured: 1
35816
+ };
35817
+ /** Used when a caller doesn't supply one (e.g. ScrawlEngine constructed without a Host theme). */
35818
+ var DEFAULT_BOARD_SURFACE_THEME = {
35819
+ color: "#f5f5f3",
35820
+ gridMode: "none",
35821
+ gridColor: "#deded8",
35822
+ gridSpacing: 5,
35823
+ gridLineWidth: 1,
35824
+ surfaceTexture: "flat"
35825
+ };
35826
+ /** Grid alpha fades out below this on-screen spacing (px) — otherwise it aliases into a solid wash when zoomed out. */
35827
+ var GRID_FADE_MIN_PX = 3;
35828
+ var GRID_FADE_MAX_PX = 8;
35797
35829
  var VERTEX$1 = `
35798
35830
  varying vec3 vWorldPos;
35799
35831
  void main() {
@@ -35807,6 +35839,13 @@ var FRAGMENT = `
35807
35839
  uniform vec3 uFogColor;
35808
35840
  uniform float uFogNear;
35809
35841
  uniform float uFogFar;
35842
+ uniform float uGridMode;
35843
+ uniform vec3 uGridColor;
35844
+ uniform float uGridSpacing;
35845
+ uniform float uWorldPerPixel;
35846
+ uniform float uGridLineWidthPx;
35847
+ uniform float uGridFade;
35848
+ uniform float uSurfaceTexture;
35810
35849
  varying vec3 vWorldPos;
35811
35850
 
35812
35851
  float hash(vec2 p) {
@@ -35825,33 +35864,69 @@ var FRAGMENT = `
35825
35864
 
35826
35865
  ${FOG_GLSL}
35827
35866
 
35867
+ // Distance from p's nearest axis-aligned grid line, in world units.
35868
+ // A crisp, ~uGridLineWidthPx-wide line: full alpha to its half-width, a
35869
+ // ~1px anti-aliased falloff beyond that — not a wide, soft-edged band.
35870
+ float gridLineAlpha(vec2 p) {
35871
+ vec2 cell = mod(p, uGridSpacing);
35872
+ vec2 distToLine = min(cell, uGridSpacing - cell);
35873
+ float halfWidth = 0.5 * uGridLineWidthPx * uWorldPerPixel;
35874
+ float aa = uWorldPerPixel;
35875
+ vec2 lineAlpha2 = 1.0 - smoothstep(halfWidth - aa * 0.5, halfWidth + aa * 0.5, distToLine);
35876
+ return max(lineAlpha2.x, lineAlpha2.y);
35877
+ }
35878
+
35879
+ // Distance from p's nearest grid intersection, in world units. Dot radius
35880
+ // matches the configured line width in pixels, same anti-aliasing.
35881
+ float gridDotAlpha(vec2 p) {
35882
+ vec2 cell = mod(p, uGridSpacing) - uGridSpacing * 0.5;
35883
+ float dist = length(cell);
35884
+ float radius = uGridLineWidthPx * uWorldPerPixel;
35885
+ float aa = uWorldPerPixel;
35886
+ return 1.0 - smoothstep(radius - aa * 0.5, radius + aa * 0.5, dist);
35887
+ }
35888
+
35828
35889
  void main() {
35829
35890
  vec3 col = uColor;
35830
35891
 
35831
- // Melamine micro-texture: two octaves of value noise, +-1.75% brightness.
35832
- float n = vnoise(vWorldPos.xy * 2.3) * 0.6 + vnoise(vWorldPos.xy * 9.7) * 0.4;
35833
- col *= 1.0 + (n - 0.5) * 0.035;
35892
+ if (uSurfaceTexture > 0.5) {
35893
+ // Melamine micro-texture: two octaves of value noise, +-1.75% brightness.
35894
+ float n = vnoise(vWorldPos.xy * 2.3) * 0.6 + vnoise(vWorldPos.xy * 9.7) * 0.4;
35895
+ col *= 1.0 + (n - 0.5) * 0.035;
35834
35896
 
35835
- // Satin sheen: Fresnel rim + a specular band from the key light. Both are
35836
- // ~zero in the straight-on working view and bloom as the camera tilts.
35837
- vec3 V = normalize(cameraPosition - vWorldPos);
35838
- vec3 N = vec3(0.0, 0.0, 1.0);
35839
- float fres = pow(1.0 - max(dot(N, V), 0.0), 3.0);
35840
- vec3 L = normalize(vec3(-0.25, 0.4, 0.88));
35841
- vec3 H = normalize(L + V);
35842
- float spec = pow(max(dot(N, H), 0.0), 90.0);
35843
- col += (fres * 0.18 + spec * 0.06) * vec3(1.0);
35897
+ // Satin sheen: Fresnel rim + a specular band from the key light. Both
35898
+ // are ~zero in the straight-on working view and bloom as the camera tilts.
35899
+ vec3 V = normalize(cameraPosition - vWorldPos);
35900
+ vec3 N = vec3(0.0, 0.0, 1.0);
35901
+ float fres = pow(1.0 - max(dot(N, V), 0.0), 3.0);
35902
+ vec3 L = normalize(vec3(-0.25, 0.4, 0.88));
35903
+ vec3 H = normalize(L + V);
35904
+ float spec = pow(max(dot(N, H), 0.0), 90.0);
35905
+ col += (fres * 0.18 + spec * 0.06) * vec3(1.0);
35906
+ }
35907
+
35908
+ if (uGridMode > 0.5 && uGridFade > 0.001) {
35909
+ float gridAlpha = (uGridMode < 1.5 ? gridLineAlpha(vWorldPos.xy) : gridDotAlpha(vWorldPos.xy)) * uGridFade;
35910
+ col = mix(col, uGridColor, gridAlpha);
35911
+ }
35844
35912
 
35845
35913
  col = applyBoardFog(col, vWorldPos);
35846
35914
  gl_FragColor = vec4(col, 1.0);
35847
35915
  }
35848
35916
  `;
35849
- function createBoardSurface(fog) {
35917
+ function createBoardSurface(fog, theme) {
35850
35918
  const material = new ShaderMaterial({
35851
35919
  vertexShader: VERTEX$1,
35852
35920
  fragmentShader: FRAGMENT,
35853
35921
  uniforms: {
35854
- uColor: { value: new Color(BOARD_COLOR) },
35922
+ uColor: { value: new Color(theme.color) },
35923
+ uGridMode: { value: GRID_MODE_CODE[theme.gridMode] },
35924
+ uGridColor: { value: new Color(theme.gridColor) },
35925
+ uGridSpacing: { value: Math.max(.001, theme.gridSpacing) },
35926
+ uGridLineWidthPx: { value: Math.max(.1, theme.gridLineWidth) },
35927
+ uWorldPerPixel: { value: .05 },
35928
+ uGridFade: { value: 0 },
35929
+ uSurfaceTexture: { value: TEXTURE_CODE[theme.surfaceTexture] },
35855
35930
  ...fog
35856
35931
  }
35857
35932
  });
@@ -35859,11 +35934,34 @@ function createBoardSurface(fog) {
35859
35934
  mesh.name = "board-surface";
35860
35935
  return mesh;
35861
35936
  }
35862
- /** Keep the plane centered under the view and larger than the fog horizon. */
35863
- function updateBoardSurface(mesh, centerX, centerY, fogFar) {
35937
+ /** Applies a theme change (initial or live) everything except the per-frame fade/pixel-ratio. */
35938
+ function setBoardSurfaceTheme(mesh, theme) {
35939
+ const uniforms = mesh.material.uniforms;
35940
+ uniforms.uColor.value.set(theme.color);
35941
+ uniforms.uGridMode.value = GRID_MODE_CODE[theme.gridMode];
35942
+ uniforms.uGridColor.value.set(theme.gridColor);
35943
+ uniforms.uGridSpacing.value = Math.max(.001, theme.gridSpacing);
35944
+ uniforms.uGridLineWidthPx.value = Math.max(.1, theme.gridLineWidth);
35945
+ uniforms.uSurfaceTexture.value = TEXTURE_CODE[theme.surfaceTexture];
35946
+ }
35947
+ /** Keep the plane centered under the view, larger than the fog horizon, and the grid's on-screen weight/fade current. */
35948
+ function updateBoardSurface(mesh, centerX, centerY, fogFar, worldPerPixel) {
35864
35949
  mesh.position.set(centerX, centerY, 0);
35865
35950
  const size = fogFar * 2.5;
35866
35951
  mesh.scale.set(size, size, 1);
35952
+ const uniforms = mesh.material.uniforms;
35953
+ uniforms.uWorldPerPixel.value = worldPerPixel;
35954
+ const t = (uniforms.uGridSpacing.value / Math.max(1e-6, worldPerPixel) - GRID_FADE_MIN_PX) / (GRID_FADE_MAX_PX - GRID_FADE_MIN_PX);
35955
+ uniforms.uGridFade.value = MathUtils.clamp(t, 0, 1);
35956
+ }
35957
+ /** Temporarily zeroes the grid's contribution (for export snapshots) and returns a restore function. */
35958
+ function suppressBoardSurfaceGrid(mesh) {
35959
+ const uniforms = mesh.material.uniforms;
35960
+ const previous = uniforms.uGridFade.value;
35961
+ uniforms.uGridFade.value = 0;
35962
+ return () => {
35963
+ uniforms.uGridFade.value = previous;
35964
+ };
35867
35965
  }
35868
35966
  var MIN_HEIGHT = 40;
35869
35967
  var MAX_HEIGHT = 1500;
@@ -36025,9 +36123,9 @@ var CameraRig = class {
36025
36123
  }
36026
36124
  };
36027
36125
  //#endregion
36028
- //#region ../../src/scrawl/engine/scene.ts
36126
+ //#region src/renderer/canvas/scene.ts
36029
36127
  var SceneRig = class {
36030
- constructor(canvas) {
36128
+ constructor(canvas, boardTheme) {
36031
36129
  this.renderer = new WebGLRenderer({
36032
36130
  canvas,
36033
36131
  antialias: true
@@ -36037,7 +36135,7 @@ var SceneRig = class {
36037
36135
  this.scene = new Scene();
36038
36136
  this.cameraRig = new CameraRig(canvas.clientWidth / Math.max(1, canvas.clientHeight));
36039
36137
  this.fog = createFogUniforms(FOG_COLOR);
36040
- this.board = createBoardSurface(this.fog);
36138
+ this.board = createBoardSurface(this.fog, boardTheme);
36041
36139
  this.scene.add(this.board);
36042
36140
  this.inkGroup = new Group();
36043
36141
  this.inkGroup.name = "ink";
@@ -36054,9 +36152,17 @@ var SceneRig = class {
36054
36152
  renderFrame(dtMs) {
36055
36153
  this.cameraRig.update(dtMs);
36056
36154
  updateFogForCamera(this.fog, this.cameraRig.height);
36057
- updateBoardSurface(this.board, this.cameraRig.center.x, this.cameraRig.center.y, this.fog.uFogFar.value);
36155
+ updateBoardSurface(this.board, this.cameraRig.center.x, this.cameraRig.center.y, this.fog.uFogFar.value, this.cameraRig.worldPerPixel());
36058
36156
  this.renderer.render(this.scene, this.cameraRig.camera);
36059
36157
  }
36158
+ /** Live theme update — the board controller's identity stays fixed across theme changes; this is how a new one reaches the render engine. */
36159
+ applyBoardTheme(theme) {
36160
+ setBoardSurfaceTheme(this.board, theme);
36161
+ }
36162
+ /** Zeroes the grid for one render pass (PNG export never bakes it in) and returns a restore function. */
36163
+ suppressGrid() {
36164
+ return suppressBoardSurfaceGrid(this.board);
36165
+ }
36060
36166
  dispose() {
36061
36167
  this.board.geometry.dispose();
36062
36168
  this.board.material.dispose();
@@ -36064,7 +36170,7 @@ var SceneRig = class {
36064
36170
  }
36065
36171
  };
36066
36172
  //#endregion
36067
- //#region ../../src/scrawl/ink/inkMaterial.ts
36173
+ //#region src/renderer/shapes/inkMaterial.ts
36068
36174
  var VERTEX = `
36069
36175
  attribute float aAlpha;
36070
36176
  varying vec2 vUv;
@@ -36195,7 +36301,7 @@ var InkMaterials = class {
36195
36301
  }
36196
36302
  };
36197
36303
  //#endregion
36198
- //#region ../../src/scrawl/ink/ribbonGeometry.ts
36304
+ //#region src/renderer/shapes/ribbonGeometry.ts
36199
36305
  /** Ink floats a hair above the board to avoid z-fighting with the surface. */
36200
36306
  var INK_Z = .02;
36201
36307
  /** Highlighter always renders under opaque ink (build prompt §4.1). */
@@ -36246,7 +36352,7 @@ function buildRibbonGeometry(points, baseWidth, z = INK_Z) {
36246
36352
  return geometry;
36247
36353
  }
36248
36354
  //#endregion
36249
- //#region ../../src/scrawl/ink/strokeRenderer.ts
36355
+ //#region src/renderer/shapes/strokeRenderer.ts
36250
36356
  var strokeZ = (stroke) => stroke.tool === "highlighter" ? HIGHLIGHT_Z : INK_Z;
36251
36357
  /**
36252
36358
  * Derives ink meshes from the document by subscription — the document never
@@ -36379,7 +36485,7 @@ function syncMatrix(mesh, stroke) {
36379
36485
  mesh.matrixWorldNeedsUpdate = true;
36380
36486
  }
36381
36487
  //#endregion
36382
- //#region ../../src/scrawl/scene-objects/markerProp.ts
36488
+ //#region src/renderer/shapes/markerProp.ts
36383
36489
  var BODY_LENGTH = 11;
36384
36490
  var BODY_RADIUS = .85;
36385
36491
  var TIP_LENGTH = 1.6;
@@ -36475,7 +36581,7 @@ var MarkerProp = class {
36475
36581
  }
36476
36582
  };
36477
36583
  //#endregion
36478
- //#region ../../src/scrawl/scene-objects/noteGeometry.ts
36584
+ //#region src/renderer/shapes/noteGeometry.ts
36479
36585
  /**
36480
36586
  * Height of the sheet above the board at (u, v) ∈ [0,1]².
36481
36587
  * v = 1 is the adhesive edge (always 0); u = 1 is the peel corner side.
@@ -36519,7 +36625,7 @@ function applyCurl(geometry, curl, peel) {
36519
36625
  color.needsUpdate = true;
36520
36626
  }
36521
36627
  //#endregion
36522
- //#region ../../src/scrawl/scene-objects/stickyNotes.ts
36628
+ //#region src/renderer/shapes/stickyNotes.ts
36523
36629
  var TEXTURE_SIZE = 512;
36524
36630
  var FOCUS_COLOR$4 = 2450411;
36525
36631
  var CURL = .3;
@@ -43095,7 +43201,7 @@ SYNCABLE_PROPS.forEach((prop) => {
43095
43201
  new Box3();
43096
43202
  new Color();
43097
43203
  //#endregion
43098
- //#region ../../src/scrawl/scene-objects/tables.ts
43204
+ //#region src/renderer/shapes/tables.ts
43099
43205
  var FONT_URL$2 = "/fonts/inter-medium.woff";
43100
43206
  var TABLE_Z = .012;
43101
43207
  var HEADER_Z = .014;
@@ -43444,7 +43550,7 @@ var TablesRenderer = class {
43444
43550
  }
43445
43551
  };
43446
43552
  //#endregion
43447
- //#region ../../src/scrawl/scene-objects/textBlocks.ts
43553
+ //#region src/renderer/text/textBlocks.ts
43448
43554
  var FONT_URL$1 = "/fonts/inter-medium.woff";
43449
43555
  var TEXT_Z = .02;
43450
43556
  var FOCUS_COLOR$2 = 2450411;
@@ -43548,7 +43654,7 @@ var TextBlocksRenderer = class {
43548
43654
  }
43549
43655
  };
43550
43656
  //#endregion
43551
- //#region ../../src/scrawl/scene-objects/comments.ts
43657
+ //#region src/renderer/shapes/comments.ts
43552
43658
  var PIN_Z = .04;
43553
43659
  var PIN_WIDTH = 3.6;
43554
43660
  var PIN_HEIGHT = 3.6;
@@ -43716,7 +43822,7 @@ function drawBubblePath(ctx, x, y, w, h, r, tailX, tailY, tailW) {
43716
43822
  ctx.closePath();
43717
43823
  }
43718
43824
  //#endregion
43719
- //#region ../../src/scrawl/scene-objects/images.ts
43825
+ //#region src/renderer/shapes/images.ts
43720
43826
  var IMAGE_Z = .015;
43721
43827
  var FRAME_BORDER = .4;
43722
43828
  var FOCUS_COLOR$1 = 2450411;
@@ -43926,7 +44032,7 @@ function applyPlaceholderTexture(visual, label) {
43926
44032
  visual.imageMesh.material.needsUpdate = true;
43927
44033
  }
43928
44034
  //#endregion
43929
- //#region ../../src/scrawl/scene-objects/kitchenTimers.ts
44035
+ //#region src/renderer/shapes/kitchenTimers.ts
43930
44036
  var FACE_SIZE = 256;
43931
44037
  var BODY_COLOR = 14753096;
43932
44038
  var KNOB_COLOR = 16448250;
@@ -44124,7 +44230,7 @@ function renderTimerFace(timer, canvas, now = Date.now()) {
44124
44230
  return c;
44125
44231
  }
44126
44232
  //#endregion
44127
- //#region ../../src/scrawl/scene-objects/customObjects.ts
44233
+ //#region src/renderer/shapes/customObjects.ts
44128
44234
  var FONT_URL = "/fonts/inter-medium.woff";
44129
44235
  var CUSTOM_Z = .02;
44130
44236
  var FOCUS_COLOR = 2450411;
@@ -44486,7 +44592,7 @@ function disposeObject3D(obj) {
44486
44592
  });
44487
44593
  }
44488
44594
  //#endregion
44489
- //#region ../../src/scrawl/tools/hitTest.ts
44595
+ //#region src/interaction/pointer/hitTest.ts
44490
44596
  /** Topmost (most recently added) stroke under the point, or null. */
44491
44597
  function hitStroke(doc, index, p, slop) {
44492
44598
  const candidates = index.query(p.x - slop, p.y - slop, p.x + slop, p.y + slop);
@@ -44527,7 +44633,7 @@ function distToSegmentSq(p, a, b) {
44527
44633
  return dx * dx + dy * dy;
44528
44634
  }
44529
44635
  //#endregion
44530
- //#region ../../src/scrawl/tools/eraserTool.ts
44636
+ //#region src/interaction/tools/eraserTool.ts
44531
44637
  var DECAY_PER_PASS = .4;
44532
44638
  /** A point can take another decay pass after this long — rubbing works. */
44533
44639
  var REARM_MS = 250;
@@ -44655,7 +44761,7 @@ function partitionByErasure(points) {
44655
44761
  return runs;
44656
44762
  }
44657
44763
  //#endregion
44658
- //#region ../../src/scrawl/tools/noteDrag.ts
44764
+ //#region src/interaction/tools/noteDrag.ts
44659
44765
  /**
44660
44766
  * Drag the focused note in X/Y. The grab is resolved on the note's own
44661
44767
  * z-plane, not the board, so the note doesn't jump under a tilted ray.
@@ -44722,7 +44828,7 @@ var NoteDragState = class {
44722
44828
  }
44723
44829
  };
44724
44830
  //#endregion
44725
- //#region ../../src/scrawl/tools/imageDrag.ts
44831
+ //#region src/interaction/tools/imageDrag.ts
44726
44832
  /** Drag the focused image in X/Y; one command per drag. */
44727
44833
  var ImageDragState = class {
44728
44834
  constructor(ctx) {
@@ -44774,7 +44880,7 @@ var ImageDragState = class {
44774
44880
  }
44775
44881
  };
44776
44882
  //#endregion
44777
- //#region ../../src/scrawl/tools/timerDrag.ts
44883
+ //#region src/interaction/tools/timerDrag.ts
44778
44884
  /** Drag the focused timer in X/Y; one command per drag. */
44779
44885
  var TimerDragState = class {
44780
44886
  constructor(ctx) {
@@ -44826,7 +44932,7 @@ var TimerDragState = class {
44826
44932
  }
44827
44933
  };
44828
44934
  //#endregion
44829
- //#region ../../src/scrawl/tools/shapes.ts
44935
+ //#region src/interaction/tools/shapeGeometry.ts
44830
44936
  /** Uniform synthetic pressure; the ink shader supplies the hand-drawn feel. */
44831
44937
  var PRESSURE = .7;
44832
44938
  var MIN_ELLIPSE_SEGMENTS = 24;
@@ -45015,7 +45121,7 @@ function snappedEnd(a, b) {
45015
45121
  };
45016
45122
  }
45017
45123
  //#endregion
45018
- //#region ../../src/scrawl/tools/shapeTool.ts
45124
+ //#region src/interaction/tools/shapeTool.ts
45019
45125
  var MIN_DRAG_PX$1 = 4;
45020
45126
  /** Drag out a shape; live preview through the real ink material. */
45021
45127
  var ShapeTool = class {
@@ -45046,7 +45152,7 @@ var ShapeTool = class {
45046
45152
  const strokes = this.polylines.map((points) => ({
45047
45153
  id: crypto.randomUUID(),
45048
45154
  color: this.ctx.inkColor(),
45049
- baseWidth: this.ctx.baseWidth(),
45155
+ baseWidth: this.ctx.shapeStrokeWidth(),
45050
45156
  points
45051
45157
  }));
45052
45158
  this.ctx.clusters.assign(strokes[0]);
@@ -45080,13 +45186,13 @@ var ShapeTool = class {
45080
45186
  this.previews[i].visible = polyline !== void 0;
45081
45187
  if (polyline) {
45082
45188
  this.previews[i].geometry.dispose();
45083
- this.previews[i].geometry = buildRibbonGeometry(polyline, this.ctx.baseWidth());
45189
+ this.previews[i].geometry = buildRibbonGeometry(polyline, this.ctx.shapeStrokeWidth());
45084
45190
  }
45085
45191
  }
45086
45192
  }
45087
45193
  };
45088
45194
  //#endregion
45089
- //#region ../../src/scrawl/tools/tableDrag.ts
45195
+ //#region src/interaction/tools/tableDrag.ts
45090
45196
  function getHandleCursor(handle) {
45091
45197
  switch (handle) {
45092
45198
  case "nw":
@@ -45206,7 +45312,7 @@ var TableDragState = class {
45206
45312
  }
45207
45313
  };
45208
45314
  //#endregion
45209
- //#region ../../src/scrawl/tools/tableTool.ts
45315
+ //#region src/interaction/tools/tableTool.ts
45210
45316
  var MIN_DRAG_PX = 6;
45211
45317
  var TableTool = class {
45212
45318
  constructor(ctx) {
@@ -45337,7 +45443,7 @@ var TableTool = class {
45337
45443
  }
45338
45444
  };
45339
45445
  //#endregion
45340
- //#region ../../src/scrawl/tools/commentTool.ts
45446
+ //#region src/interaction/tools/commentTool.ts
45341
45447
  /** Click on the board to drop a new comment pin or open an existing one. */
45342
45448
  var CommentTool = class {
45343
45449
  constructor(ctx) {
@@ -45372,7 +45478,7 @@ var CommentTool = class {
45372
45478
  }
45373
45479
  };
45374
45480
  //#endregion
45375
- //#region ../../src/scrawl/tools/textDrag.ts
45481
+ //#region src/interaction/tools/textDrag.ts
45376
45482
  /** Drag the focused text block in X/Y; one command per drag. */
45377
45483
  var TextDragState = class {
45378
45484
  constructor(ctx) {
@@ -45424,7 +45530,7 @@ var TextDragState = class {
45424
45530
  }
45425
45531
  };
45426
45532
  //#endregion
45427
- //#region ../../src/scrawl/tools/fsm.ts
45533
+ //#region src/interaction/tools/fsm.ts
45428
45534
  var ToolStateMachine = class {
45429
45535
  constructor() {
45430
45536
  this.states = /* @__PURE__ */ new Map();
@@ -45626,7 +45732,7 @@ var IdleState = class {
45626
45732
  }
45627
45733
  };
45628
45734
  //#endregion
45629
- //#region ../../src/scrawl/tools/gizmo.ts
45735
+ //#region src/renderer/selection/gizmo.ts
45630
45736
  var GIZMO_Z = .05;
45631
45737
  var HANDLE_PX = 9;
45632
45738
  var HANDLE_HIT_PX = 12;
@@ -45857,7 +45963,7 @@ function boxesEqual(a, b) {
45857
45963
  return a.minX === b.minX && a.minY === b.minY && a.maxX === b.maxX && a.maxY === b.maxY;
45858
45964
  }
45859
45965
  //#endregion
45860
- //#region ../../src/scrawl/ink/pressure.ts
45966
+ //#region src/interaction/tools/pressure.ts
45861
45967
  var SIM_MAX = 1;
45862
45968
  var SIM_MIN = .3;
45863
45969
  var SMOOTHING = .25;
@@ -45879,7 +45985,7 @@ function clamp(v, lo, hi) {
45879
45985
  return Math.min(hi, Math.max(lo, v));
45880
45986
  }
45881
45987
  //#endregion
45882
- //#region ../../src/scrawl/ink/strokeBuilder.ts
45988
+ //#region src/interaction/tools/strokeBuilder.ts
45883
45989
  var StrokeBuilder = class {
45884
45990
  constructor() {
45885
45991
  this.points = [];
@@ -45929,7 +46035,7 @@ function catmullRom(v0, v1, v2, v3, t) {
45929
46035
  return .5 * (2 * v1 + (-v0 + v2) * t + (2 * v0 - 5 * v1 + 4 * v2 - v3) * t2 + (-v0 + 3 * v1 - 3 * v2 + v3) * t3);
45930
46036
  }
45931
46037
  //#endregion
45932
- //#region ../../src/scrawl/tools/markerTool.ts
46038
+ //#region src/interaction/tools/markerTool.ts
45933
46039
  /** Sample spacing while drawing, in screen pixels. */
45934
46040
  var MIN_SAMPLE_PX = 1.5;
45935
46041
  /** A highlighter is a chisel tip: near-constant width, little pressure play. */
@@ -46032,7 +46138,7 @@ var MarkerTool = class {
46032
46138
  }
46033
46139
  };
46034
46140
  //#endregion
46035
- //#region ../../src/scrawl/tools/orbitTool.ts
46141
+ //#region src/interaction/tools/orbitTool.ts
46036
46142
  /**
46037
46143
  * Orbit-preview (build prompt §4.2): alt-drag tilts the camera off-axis to
46038
46144
  * read the board's material and depth; releasing eases back to the working
@@ -46067,7 +46173,7 @@ var OrbitTool = class {
46067
46173
  }
46068
46174
  };
46069
46175
  //#endregion
46070
- //#region ../../src/scrawl/tools/panTool.ts
46176
+ //#region src/interaction/tools/panTool.ts
46071
46177
  /** Grab-pan: content follows the pointer. */
46072
46178
  var PanTool = class {
46073
46179
  constructor(ctx) {
@@ -46097,7 +46203,7 @@ var PanTool = class {
46097
46203
  }
46098
46204
  };
46099
46205
  //#endregion
46100
- //#region ../../src/scrawl/tools/selection.ts
46206
+ //#region src/renderer/selection/selection.ts
46101
46207
  /** The set of selected stroke ids. Drops ids the document no longer has. */
46102
46208
  var SelectionManager = class {
46103
46209
  constructor(doc) {
@@ -46148,7 +46254,7 @@ var SelectionManager = class {
46148
46254
  }
46149
46255
  };
46150
46256
  //#endregion
46151
- //#region ../../src/scrawl/tools/selectTool.ts
46257
+ //#region src/interaction/tools/selectTool.ts
46152
46258
  var HIT_SLOP_PX = 4;
46153
46259
  var CLICK_TOLERANCE_PX = 3;
46154
46260
  var MIN_SCALE = .05;
@@ -46364,7 +46470,7 @@ function safeRatio(numerator, denominator) {
46364
46470
  return raw;
46365
46471
  }
46366
46472
  //#endregion
46367
- //#region ../../src/scrawl/tools/customTool.ts
46473
+ //#region src/interaction/tools/customTool.ts
46368
46474
  function extensionIdFromToolId(toolId) {
46369
46475
  const slash = toolId.indexOf("/");
46370
46476
  return slash === -1 ? toolId : toolId.slice(0, slash);
@@ -46479,12 +46585,19 @@ var ExtensionCommandBatch = class {
46479
46585
  }
46480
46586
  };
46481
46587
  //#endregion
46482
- //#region ../../src/scrawl/ScrawlEngine.ts
46588
+ //#region src/composition/ScrawlEngine.ts
46483
46589
  /** Marker stroke width in board units (~1% of the default view height). */
46484
46590
  var BASE_WIDTH = .5;
46485
46591
  /** Highlighter chisel width — wide enough to cover a line of handwriting. */
46486
46592
  var HIGHLIGHT_WIDTH = 2.2;
46487
46593
  /**
46594
+ * Shape (rectangle, ellipse, arrow, ...) border width in board units.
46595
+ * Deliberately thinner than BASE_WIDTH: a shape border reads as a crisp,
46596
+ * structural line, not an expressive marker stroke — this is also the
46597
+ * fallback when no Host theme configures ScrawlTheme.shapeStrokeWidth.
46598
+ */
46599
+ var DEFAULT_SHAPE_STROKE_WIDTH = .15;
46600
+ /**
46488
46601
  * Identity of a rendered selection badge: which item, its lock state, and
46489
46602
  * where it sits on screen (rounded to the pixel, so sub-pixel camera drift
46490
46603
  * does not churn React).
@@ -46505,9 +46618,11 @@ function emptyExtensionRegistry() {
46505
46618
  * React never reaches past this class.
46506
46619
  */
46507
46620
  var ScrawlEngine = class {
46508
- constructor(canvas, documentId$1, extensionRegistry = emptyExtensionRegistry(), assetController = new AssetController(void 0)) {
46621
+ constructor(canvas, documentId$1, extensionRegistry = emptyExtensionRegistry(), assetController = new AssetController(void 0), boardTheme = DEFAULT_BOARD_SURFACE_THEME, shapeStrokeWidth = DEFAULT_SHAPE_STROKE_WIDTH) {
46509
46622
  this.canvas = canvas;
46510
46623
  this.extensionRegistry = extensionRegistry;
46624
+ this.boardTheme = boardTheme;
46625
+ this.shapeStrokeWidth = shapeStrokeWidth;
46511
46626
  this.onToolChange = null;
46512
46627
  this.onZoomChange = null;
46513
46628
  this.onAuditEvent = null;
@@ -46554,7 +46669,7 @@ var ScrawlEngine = class {
46554
46669
  this.stampKind = "star";
46555
46670
  this.timerDurationMs = TIMER_DEFAULT_DURATION_MS;
46556
46671
  this.readOnly = false;
46557
- this.scene = new SceneRig(canvas);
46672
+ this.scene = new SceneRig(canvas, boardTheme);
46558
46673
  this.document = new BoardDocument(documentId(documentId$1));
46559
46674
  this.history = new History(this.document);
46560
46675
  this.history.onCommand = (command, kind) => {
@@ -46602,6 +46717,7 @@ var ScrawlEngine = class {
46602
46717
  setTool: (t) => this.setTool(t),
46603
46718
  inkColor: () => this.tool === "highlighter" ? this.highlightColor : this.color,
46604
46719
  baseWidth: () => this.tool === "highlighter" ? HIGHLIGHT_WIDTH : BASE_WIDTH,
46720
+ shapeStrokeWidth: () => this.shapeStrokeWidth,
46605
46721
  activeTool: () => this.tool,
46606
46722
  isSpaceHeld: () => this.spaceHeld,
46607
46723
  pointerToBoard: (e) => pointerToBoard(e.clientX, e.clientY, canvas, this.scene.cameraRig.camera),
@@ -46861,6 +46977,15 @@ var ScrawlEngine = class {
46861
46977
  height: rig.height
46862
46978
  };
46863
46979
  }
46980
+ /** Live update of the board surface color/grid — the engine's identity stays fixed across theme changes. */
46981
+ applyBoardTheme(theme) {
46982
+ this.boardTheme = theme;
46983
+ this.scene.applyBoardTheme(theme);
46984
+ }
46985
+ /** Live update of the shape tool's border width (board units). Already-drawn shapes keep the width they were drawn with. */
46986
+ setShapeStrokeWidth(width) {
46987
+ this.shapeStrokeWidth = width;
46988
+ }
46864
46989
  /**
46865
46990
  * Snap to a peer's camera. No-op while drawing so pointer-to-board
46866
46991
  * mapping does not warp the in-progress stroke.
@@ -46879,7 +47004,7 @@ var ScrawlEngine = class {
46879
47004
  }
46880
47005
  /** The saved document as standalone SVG — a pure walk of the wire format. */
46881
47006
  exportSVG() {
46882
- return documentToSVG(this.document.toJSON(), Date.now(), this.extensionRegistry);
47007
+ return documentToSVG(this.document.toJSON(), Date.now(), this.extensionRegistry, this.boardTheme.color);
46883
47008
  }
46884
47009
  /**
46885
47010
  * Content-framed PNG snapshot. Renders once offscreen-style with UI props
@@ -46915,6 +47040,7 @@ var ScrawlEngine = class {
46915
47040
  this.notesRenderer.setFocus(null);
46916
47041
  this.textsRenderer.setFocus(null);
46917
47042
  this.renderer.setGhostsVisible(false);
47043
+ const restoreGrid = this.scene.suppressGrid();
46918
47044
  try {
46919
47045
  rig.yaw = 0;
46920
47046
  rig.pitch = 0;
@@ -46925,6 +47051,7 @@ var ScrawlEngine = class {
46925
47051
  const dataUrl = this.scene.renderer.domElement.toDataURL("image/png");
46926
47052
  return await (await fetch(dataUrl)).blob();
46927
47053
  } finally {
47054
+ restoreGrid();
46928
47055
  rig.center.x = saved.x;
46929
47056
  rig.center.y = saved.y;
46930
47057
  rig.height = saved.height;
@@ -47920,7 +48047,7 @@ var ScrawlEngine = class {
47920
48047
  }
47921
48048
  };
47922
48049
  //#endregion
47923
- //#region src/controller-internal.ts
48050
+ //#region src/composition/controller-internal.ts
47924
48051
  function createBoardController(options) {
47925
48052
  return createBoardControllerWithEngine(options);
47926
48053
  }
@@ -47934,7 +48061,16 @@ function createBoardControllerWithEngine(options, existingEngine) {
47934
48061
  }
47935
48062
  const extensionRegistry = registryResult.registry;
47936
48063
  const assetController = new AssetController(options.assetResolver, { cacheBytes: options.assetCacheBytes });
47937
- const engine = existingEngine ?? (options.canvas ? new ScrawlEngine(options.canvas, options.document.id, extensionRegistry, assetController) : null);
48064
+ let currentBoardTheme = {
48065
+ color: options.boardTheme?.surface ?? DEFAULT_BOARD_SURFACE_THEME.color,
48066
+ gridMode: options.boardTheme?.gridMode ?? DEFAULT_BOARD_SURFACE_THEME.gridMode,
48067
+ gridColor: options.boardTheme?.gridColor ?? DEFAULT_BOARD_SURFACE_THEME.gridColor,
48068
+ gridSpacing: options.boardTheme?.gridSpacing ?? DEFAULT_BOARD_SURFACE_THEME.gridSpacing,
48069
+ gridLineWidth: options.boardTheme?.gridLineWidth ?? DEFAULT_BOARD_SURFACE_THEME.gridLineWidth,
48070
+ surfaceTexture: options.boardTheme?.surfaceTexture ?? DEFAULT_BOARD_SURFACE_THEME.surfaceTexture
48071
+ };
48072
+ let currentShapeStrokeWidth = options.boardTheme?.shapeStrokeWidth ?? .15;
48073
+ const engine = existingEngine ?? (options.canvas ? new ScrawlEngine(options.canvas, options.document.id, extensionRegistry, assetController, currentBoardTheme, currentShapeStrokeWidth) : null);
47938
48074
  const document = engine?.document ?? new BoardDocument(documentId(options.document.id));
47939
48075
  const history = engine?.history ?? new History(document);
47940
48076
  const createId = options.createId ?? (() => crypto.randomUUID());
@@ -48418,6 +48554,21 @@ function createBoardControllerWithEngine(options, existingEngine) {
48418
48554
  changed();
48419
48555
  }
48420
48556
  },
48557
+ boardTheme: { set(theme) {
48558
+ currentBoardTheme = {
48559
+ color: theme.surface ?? currentBoardTheme.color,
48560
+ gridMode: theme.gridMode ?? currentBoardTheme.gridMode,
48561
+ gridColor: theme.gridColor ?? currentBoardTheme.gridColor,
48562
+ gridSpacing: theme.gridSpacing ?? currentBoardTheme.gridSpacing,
48563
+ gridLineWidth: theme.gridLineWidth ?? currentBoardTheme.gridLineWidth,
48564
+ surfaceTexture: theme.surfaceTexture ?? currentBoardTheme.surfaceTexture
48565
+ };
48566
+ engine?.applyBoardTheme(currentBoardTheme);
48567
+ if (theme.shapeStrokeWidth !== void 0) {
48568
+ currentShapeStrokeWidth = theme.shapeStrokeWidth;
48569
+ engine?.setShapeStrokeWidth(currentShapeStrokeWidth);
48570
+ }
48571
+ } },
48421
48572
  view: {
48422
48573
  fit() {
48423
48574
  assertActive();
@@ -48722,10 +48873,10 @@ function createBoardControllerWithEngine(options, existingEngine) {
48722
48873
  gather: (nextView) => engine?.applyGatherView(nextView) ?? false
48723
48874
  },
48724
48875
  export: {
48725
- svg: () => engine ? engine.exportSVG() : documentToSVG(document.toJSON(), Date.now(), extensionRegistry),
48876
+ svg: () => engine ? engine.exportSVG() : documentToSVG(document.toJSON(), Date.now(), extensionRegistry, currentBoardTheme.color),
48726
48877
  png: (options) => engine ? engine.exportPNG(options?.maxSide, { awaitAssets: options?.awaitAssets }) : Promise.resolve(null),
48727
48878
  json: () => document.toJSON(),
48728
- svgAsync: (svgOptions) => exportDocumentSVGWithAssets(document.toJSON(), Date.now(), extensionRegistry, assetController, svgOptions)
48879
+ svgAsync: (svgOptions) => exportDocumentSVGWithAssets(document.toJSON(), Date.now(), extensionRegistry, assetController, currentBoardTheme.color, svgOptions)
48729
48880
  },
48730
48881
  assets: { async ingest(bytes, mediaType, name, signal) {
48731
48882
  assertActive();
@@ -49187,4 +49338,4 @@ function createLocalBoard(options) {
49187
49338
  };
49188
49339
  }
49189
49340
  //#endregion
49190
- export { STAMPS, createBoardController, createLocalBoard, isStampKind, stampDataUrl };
49341
+ export { AssetResolutionError, STAMPS, SUPPORTED_ASSET_MEDIA_TYPES, assetRef, clampAssetCacheBytes, cloneCustomObject, createBoardController, createLocalBoard, isAssetRef, isStampKind, stampDataUrl };