@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/index.js CHANGED
@@ -1,8 +1,10 @@
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
+ /** Hard ceiling on a reference's own wire length — independent of any resource limit below. */
7
+ var ASSET_REF_MAX_BYTES = 512;
6
8
  function isAssetRef(value) {
7
9
  return typeof value === "string" && ASSET_REF_PATTERN.test(value) && new TextEncoder().encode(value).length <= 512;
8
10
  }
@@ -20,15 +22,26 @@ var AssetResolutionError = class extends Error {
20
22
  this.name = "AssetResolutionError";
21
23
  }
22
24
  };
25
+ var ASSET_MAX_ENCODED_BYTES = 20 * 1024 * 1024;
26
+ var ASSET_MAX_DIMENSION_PX = 8192;
27
+ var ASSET_MAX_DECODED_MEGAPIXELS = 40;
28
+ var ASSET_MAX_CONCURRENT_RESOLUTIONS = 6;
23
29
  var ASSET_CACHE_BYTES_DEFAULT = 256 * 1024 * 1024;
24
30
  var ASSET_CACHE_BYTES_MIN = 64 * 1024 * 1024;
25
31
  var ASSET_CACHE_BYTES_MAX = 512 * 1024 * 1024;
32
+ var ASSET_EXPORT_MAX_ENCODED_BYTES = 100 * 1024 * 1024;
33
+ var ASSET_EXPORT_MAX_DECODED_MEGAPIXELS = 100;
34
+ var SUPPORTED_ASSET_MEDIA_TYPES = [
35
+ "image/png",
36
+ "image/jpeg",
37
+ "image/webp"
38
+ ];
26
39
  function clampAssetCacheBytes(value) {
27
40
  if (value === void 0 || !Number.isFinite(value)) return ASSET_CACHE_BYTES_DEFAULT;
28
41
  return Math.min(ASSET_CACHE_BYTES_MAX, Math.max(ASSET_CACHE_BYTES_MIN, Math.floor(value)));
29
42
  }
30
43
  //#endregion
31
- //#region src/core-internal/identifiers.ts
44
+ //#region src/core/document/identifiers.ts
32
45
  function documentId(value) {
33
46
  if (!value) throw new TypeError("DocumentId cannot be empty");
34
47
  return value;
@@ -38,7 +51,7 @@ function strokeId(value) {
38
51
  return value;
39
52
  }
40
53
  //#endregion
41
- //#region src/document-schema.ts
54
+ //#region src/core/document/document-schema.ts
42
55
  var CURRENT_DOCUMENT_SCHEMA_VERSION = 1;
43
56
  var DocumentRecoveryError = class extends Error {
44
57
  constructor(code, message, originalBytes, path) {
@@ -246,7 +259,7 @@ function validation(message, path) {
246
259
  return new DocumentRecoveryError("DOCUMENT_VALIDATION_FAILED", message, void 0, path);
247
260
  }
248
261
  //#endregion
249
- //#region src/core-internal/affine2d.ts
262
+ //#region src/core/shapes/affine2d.ts
250
263
  var IDENTITY = [
251
264
  1,
252
265
  0,
@@ -330,7 +343,7 @@ function avgScale(m) {
330
343
  return (Math.hypot(m[0], m[1]) + Math.hypot(m[2], m[3])) / 2;
331
344
  }
332
345
  //#endregion
333
- //#region src/core-internal/boardSearch.ts
346
+ //#region src/core/document/boardSearch.ts
334
347
  function hay(s) {
335
348
  return s.toLowerCase();
336
349
  }
@@ -395,7 +408,7 @@ function searchBoard(query, board) {
395
408
  return hits;
396
409
  }
397
410
  //#endregion
398
- //#region src/core-internal/types.ts
411
+ //#region src/core/shapes/types.ts
399
412
  /** Erasure level at which a point counts as fully erased. */
400
413
  var ERASE_THRESHOLD = .95;
401
414
  function cloneStroke(stroke) {
@@ -466,13 +479,12 @@ function measureTable(table) {
466
479
  height: table.rowHeights.reduce((sum, h) => sum + h, 0)
467
480
  };
468
481
  }
469
- var BOARD_COLOR = "#FFFFFF";
470
482
  var FOG_COLOR = "#FFFFFF";
471
483
  function cloneImage(img) {
472
484
  return { ...img };
473
485
  }
474
486
  //#endregion
475
- //#region src/core-internal/clusterStore.ts
487
+ //#region src/core/document/clusterStore.ts
476
488
  /** A stroke joins a cluster last written to within this window at full reach. */
477
489
  var RECENT_MS = 8e3;
478
490
  /** Older clusters are still joinable, at half the gap threshold. */
@@ -643,7 +655,7 @@ function clamp$3(v, lo, hi) {
643
655
  return Math.min(hi, Math.max(lo, v));
644
656
  }
645
657
  //#endregion
646
- //#region src/core-internal/extensionTypes.ts
658
+ //#region src/core/shapes/extensionTypes.ts
647
659
  function cloneCustomObject(object) {
648
660
  return {
649
661
  ...object,
@@ -653,7 +665,7 @@ function cloneCustomObject(object) {
653
665
  };
654
666
  }
655
667
  //#endregion
656
- //#region src/core-internal/kitchenTimer.ts
668
+ //#region src/core/shapes/kitchenTimer.ts
657
669
  var TIMER_DEFAULT_SIZE = 12;
658
670
  var TIMER_DEFAULT_DURATION_MS = 300 * 1e3;
659
671
  var TIMER_PRESETS_MS = [
@@ -710,7 +722,7 @@ function formatTimer(ms) {
710
722
  return `${Math.floor(total / 60)}:${(total % 60).toString().padStart(2, "0")}`;
711
723
  }
712
724
  //#endregion
713
- //#region src/core-internal/itemLock.ts
725
+ //#region src/core/shapes/itemLock.ts
714
726
  /**
715
727
  * Apply or clear a lock in place. Unlock always drops the holder so a stale
716
728
  * name cannot linger on an unlocked item.
@@ -747,7 +759,7 @@ function serializeLock(item) {
747
759
  };
748
760
  }
749
761
  //#endregion
750
- //#region src/core-internal/document.ts
762
+ //#region src/core/document/document.ts
751
763
  /**
752
764
  * The one place a live Stroke becomes its wire representation — in
753
765
  * particular, points collapse from {x,y,pressure,erase?} objects into
@@ -1306,7 +1318,7 @@ function computeBBox(stroke) {
1306
1318
  return world;
1307
1319
  }
1308
1320
  //#endregion
1309
- //#region src/core-internal/commands.ts
1321
+ //#region src/core/commands/commands.ts
1310
1322
  /** One drawing action — a marker stroke, or a shape's strokes as one unit. */
1311
1323
  var AddStrokesCommand = class {
1312
1324
  constructor(strokes) {
@@ -1648,7 +1660,7 @@ var LockItemsCommand = class {
1648
1660
  }
1649
1661
  };
1650
1662
  //#endregion
1651
- //#region src/core-internal/history.ts
1663
+ //#region src/core/history/history.ts
1652
1664
  var LIMIT = 200;
1653
1665
  var History = class {
1654
1666
  constructor(doc) {
@@ -1709,7 +1721,7 @@ var History = class {
1709
1721
  }
1710
1722
  };
1711
1723
  //#endregion
1712
- //#region src/core-internal/ops.ts
1724
+ //#region src/core/events/ops.ts
1713
1725
  var ADDED = [
1714
1726
  ["added", "strokes"],
1715
1727
  ["notesAdded", "notes"],
@@ -1763,7 +1775,7 @@ function changeToOps(change, restoring = false) {
1763
1775
  return ops;
1764
1776
  }
1765
1777
  //#endregion
1766
- //#region src/core-internal/ribbonEdges.ts
1778
+ //#region src/core/shapes/ribbonEdges.ts
1767
1779
  var MIN_WIDTH_FACTOR = .35;
1768
1780
  var END_TAPER = .55;
1769
1781
  function ribbonEdges(points, baseWidth) {
@@ -1795,7 +1807,7 @@ function ribbonEdges(points, baseWidth) {
1795
1807
  return out;
1796
1808
  }
1797
1809
  //#endregion
1798
- //#region src/core-internal/spatialIndex.ts
1810
+ //#region src/core/document/spatialIndex.ts
1799
1811
  var CELL = 8;
1800
1812
  var SpatialIndex = class {
1801
1813
  constructor(doc) {
@@ -1868,7 +1880,7 @@ function cellsOf(box) {
1868
1880
  return keys;
1869
1881
  }
1870
1882
  //#endregion
1871
- //#region src/core-internal/stamps.ts
1883
+ //#region src/core/shapes/stamps.ts
1872
1884
  var STAMP_SIZE = 6;
1873
1885
  var STAMPS = [
1874
1886
  {
@@ -1917,7 +1929,7 @@ function isStampKind(value) {
1917
1929
  return typeof value === "string" && STAMPS.some((s) => s.kind === value);
1918
1930
  }
1919
1931
  //#endregion
1920
- //#region src/core-internal/presence.ts
1932
+ //#region src/collaboration/presence/presence.ts
1921
1933
  /** Keep beacons clear of typical chrome at the top and bottom of the Board. */
1922
1934
  var BEACON_INSET = {
1923
1935
  top: 72,
@@ -1953,7 +1965,7 @@ function clamp$2(value, min, max) {
1953
1965
  return Math.min(max, Math.max(min, value));
1954
1966
  }
1955
1967
  //#endregion
1956
- //#region src/core-internal/svg.ts
1968
+ //#region src/persistence/serialization/svg.ts
1957
1969
  /** Matches the key light used by the board shader and note shadows. */
1958
1970
  var LIGHT = {
1959
1971
  x: -.25,
@@ -1963,12 +1975,19 @@ var LIGHT = {
1963
1975
  var MARGIN = 4;
1964
1976
  var NOTE_FONT_RATIO = 44 / 512;
1965
1977
  var NOTE_PAD_RATIO = 40 / 512;
1978
+ /** Matches the theme system's light-preset `boardSurface` default (theme.ts) — kept in sync manually, since core-internal stays decoupled from the theme layer. */
1979
+ var DEFAULT_BACKGROUND_COLOR = "#f5f5f3";
1966
1980
  /**
1967
1981
  * `registry` is optional and only enables rendering Custom objects through
1968
1982
  * their own `describe()` — without it (or for an object whose extension
1969
1983
  * isn't in it), Custom objects still export via the standard fallback
1970
1984
  * placeholder (`fallback.bounds`/`label`), never silently dropped.
1971
1985
  *
1986
+ * `backgroundColor` should be the Host's actual resolved `boardTheme.surface`
1987
+ * so an export matches what was on screen; defaults to the theme system's
1988
+ * own light-preset default when the caller doesn't have one on hand. The
1989
+ * board's reference grid, if any, is a screen-only aid and never exported.
1990
+ *
1972
1991
  * `resolvedAssets` (ticket #23) maps an Asset reference to an already-
1973
1992
  * resolved `data:` URI — see `assetExport.ts`'s `exportDocumentSVGWithAssets`,
1974
1993
  * which is the only intended caller that ever passes one. Without it, every
@@ -1976,7 +1995,7 @@ var NOTE_PAD_RATIO = 40 / 512;
1976
1995
  * placeholder instead of guessing at a URL; a legacy `src`-backed image is
1977
1996
  * unaffected either way.
1978
1997
  */
1979
- function documentToSVG(doc, now = 0, registry, resolvedAssets) {
1998
+ function documentToSVG(doc, now = 0, registry, backgroundColor = DEFAULT_BACKGROUND_COLOR, resolvedAssets) {
1980
1999
  const notes = doc.notes ?? [];
1981
2000
  const texts = doc.textBlocks ?? [];
1982
2001
  const tables = doc.tables ?? [];
@@ -1989,7 +2008,7 @@ function documentToSVG(doc, now = 0, registry, resolvedAssets) {
1989
2008
  const highlights = doc.strokes.filter((s) => s.tool === "highlighter");
1990
2009
  const ink = doc.strokes.filter((s) => s.tool !== "highlighter");
1991
2010
  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${view}">\n<defs>\n${defs}\n</defs>\n${[
1992
- `<rect x="${fmt(bounds.minX)}" y="${fmt(bounds.minY)}" width="${fmt(bounds.width)}" height="${fmt(bounds.height)}" fill="${BOARD_COLOR}"/>`,
2011
+ `<rect x="${fmt(bounds.minX)}" y="${fmt(bounds.minY)}" width="${fmt(bounds.width)}" height="${fmt(bounds.height)}" fill="${backgroundColor}"/>`,
1993
2012
  ...images.map((image) => imageToElement(image, resolvedAssets)),
1994
2013
  ...tables.map(tableToGroup),
1995
2014
  ...highlights.flatMap(strokeToPaths),
@@ -2317,7 +2336,7 @@ function fmt(n) {
2317
2336
  var SDK_PACKAGE_NAME = "@scrawl-board/board";
2318
2337
  var SDK_DEVELOPMENT_VERSION = "0.0.0-development";
2319
2338
  //#endregion
2320
- //#region src/core-internal/sceneGeometry.ts
2339
+ //#region src/core/shapes/sceneGeometry.ts
2321
2340
  var EMPTY_BOUNDS = {
2322
2341
  x: 0,
2323
2342
  y: 0,
@@ -2381,7 +2400,7 @@ function textCorners(x, y, text, fontSize, align, m) {
2381
2400
  return rectCorners(x + (align === "center" ? -width / 2 : align === "end" ? -width : 0), y, width, height, m);
2382
2401
  }
2383
2402
  //#endregion
2384
- //#region src/core-internal/customObjects.ts
2403
+ //#region src/core/shapes/customObjects.ts
2385
2404
  var FALLBACK_LABEL_MAX_LENGTH = 80;
2386
2405
  function toReadonly(object) {
2387
2406
  return object;
@@ -2510,7 +2529,7 @@ function extensionIdFromType(type) {
2510
2529
  return slash === -1 ? type : type.slice(0, slash);
2511
2530
  }
2512
2531
  //#endregion
2513
- //#region src/core-internal/extensionRegistry.ts
2532
+ //#region src/core/shapes/extensionRegistry.ts
2514
2533
  function buildExtensionRegistry(extensions) {
2515
2534
  const list = extensions ?? [];
2516
2535
  const diagnostics = [];
@@ -2607,7 +2626,7 @@ function isWellFormedNamespacedId(id) {
2607
2626
  return isWellFormedId(id) && id.includes("/") && !id.startsWith("/") && !id.endsWith("/");
2608
2627
  }
2609
2628
  //#endregion
2610
- //#region src/core-internal/assetValidation.ts
2629
+ //#region src/persistence/adapters/assetValidation.ts
2611
2630
  function sniffImageMediaType(bytes) {
2612
2631
  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";
2613
2632
  if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
@@ -2694,7 +2713,7 @@ function validateDecodedDimensions(width, height) {
2694
2713
  };
2695
2714
  }
2696
2715
  //#endregion
2697
- //#region src/assetController.ts
2716
+ //#region src/persistence/adapters/assetController.ts
2698
2717
  var FAILURE_COALESCE_MS = 4e3;
2699
2718
  var AssetController = class {
2700
2719
  constructor(resolver, options) {
@@ -2873,7 +2892,7 @@ function toAssetResolutionError(cause, ref) {
2873
2892
  return new AssetResolutionError("unknown", true, cause instanceof Error ? cause.message : String(cause), ref);
2874
2893
  }
2875
2894
  //#endregion
2876
- //#region src/core-internal/assetExport.ts
2895
+ //#region src/persistence/serialization/assetRefCollector.ts
2877
2896
  /** Every unique Asset reference in the document, and which object(s) use it. */
2878
2897
  function collectDocumentAssetRefs(doc, registry) {
2879
2898
  const refs = /* @__PURE__ */ new Map();
@@ -2900,7 +2919,7 @@ function collectSceneAssetRefs(node, objectId, add) {
2900
2919
  if (node.kind === "group") for (const child of node.children) collectSceneAssetRefs(child, objectId, add);
2901
2920
  }
2902
2921
  //#endregion
2903
- //#region src/assetExport.ts
2922
+ //#region src/persistence/serialization/assetExport.ts
2904
2923
  /** Strict mode's rejection — never contains credentials, a fetchable location, or a raw resolver message. */
2905
2924
  var SvgAssetExportError = class extends Error {
2906
2925
  constructor(missingAssets) {
@@ -2909,7 +2928,7 @@ var SvgAssetExportError = class extends Error {
2909
2928
  this.name = "SvgAssetExportError";
2910
2929
  }
2911
2930
  };
2912
- async function exportDocumentSVGWithAssets(doc, now, registry, assetController, options) {
2931
+ async function exportDocumentSVGWithAssets(doc, now, registry, assetController, backgroundColor, options) {
2913
2932
  const refs = collectDocumentAssetRefs(doc, registry);
2914
2933
  const resolvedAssets = /* @__PURE__ */ new Map();
2915
2934
  const missingAssets = [];
@@ -2945,7 +2964,7 @@ async function exportDocumentSVGWithAssets(doc, now, registry, assetController,
2945
2964
  }
2946
2965
  if (missingAssets.length > 0 && options?.missingAssets !== "placeholder") throw new SvgAssetExportError(missingAssets);
2947
2966
  return {
2948
- svg: documentToSVG(doc, now, registry, resolvedAssets),
2967
+ svg: documentToSVG(doc, now, registry, backgroundColor, resolvedAssets),
2949
2968
  missingAssets
2950
2969
  };
2951
2970
  }
@@ -2956,7 +2975,7 @@ function toDataUri(bytes, mediaType) {
2956
2975
  return `data:${mediaType};base64,${btoa(binary)}`;
2957
2976
  }
2958
2977
  //#endregion
2959
- //#region src/assetIngestion.ts
2978
+ //#region src/persistence/adapters/assetIngestion.ts
2960
2979
  async function decodeDimensionsWithImageBitmap(bytes, mediaType) {
2961
2980
  const blob = new Blob([bytes], { type: mediaType });
2962
2981
  let bitmap;
@@ -35602,7 +35621,7 @@ var WebGLRenderer = class {
35602
35621
  }
35603
35622
  };
35604
35623
  //#endregion
35605
- //#region ../../src/scrawl/data/auditEvents.ts
35624
+ //#region src/core/events/auditEvents.ts
35606
35625
  var BY_LABEL = {
35607
35626
  draw: {
35608
35627
  action: "create",
@@ -35722,7 +35741,7 @@ function lowerFirst(s) {
35722
35741
  return s.charAt(0).toLowerCase() + s.slice(1);
35723
35742
  }
35724
35743
  //#endregion
35725
- //#region ../../src/scrawl/assets/AssetTextureCache.ts
35744
+ //#region src/renderer/shapes/AssetTextureCache.ts
35726
35745
  var AssetTextureCache = class {
35727
35746
  constructor(assetController) {
35728
35747
  this.assetController = assetController;
@@ -35813,7 +35832,7 @@ function toDecodeError(cause, ref) {
35813
35832
  return new AssetResolutionError("decode-failed", false, cause instanceof Error ? cause.message : String(cause), ref);
35814
35833
  }
35815
35834
  //#endregion
35816
- //#region ../../src/scrawl/engine/raycast.ts
35835
+ //#region src/renderer/canvas/raycast.ts
35817
35836
  var origin$1 = new Vector3();
35818
35837
  var target = new Vector3();
35819
35838
  function pointerToBoard(clientX, clientY, canvas, camera) {
@@ -35844,7 +35863,7 @@ function pointerToPlane(clientX, clientY, canvas, camera, z) {
35844
35863
  };
35845
35864
  }
35846
35865
  //#endregion
35847
- //#region ../../src/scrawl/engine/fog.ts
35866
+ //#region src/renderer/canvas/fog.ts
35848
35867
  function createFogUniforms(color) {
35849
35868
  return {
35850
35869
  uFogColor: { value: new Color(color) },
@@ -35865,7 +35884,28 @@ var FOG_GLSL = `
35865
35884
  }
35866
35885
  `;
35867
35886
  //#endregion
35868
- //#region ../../src/scrawl/engine/boardSurface.ts
35887
+ //#region src/renderer/canvas/boardSurface.ts
35888
+ var GRID_MODE_CODE = {
35889
+ none: 0,
35890
+ line: 1,
35891
+ dot: 2
35892
+ };
35893
+ var TEXTURE_CODE = {
35894
+ flat: 0,
35895
+ textured: 1
35896
+ };
35897
+ /** Used when a caller doesn't supply one (e.g. ScrawlEngine constructed without a Host theme). */
35898
+ var DEFAULT_BOARD_SURFACE_THEME = {
35899
+ color: "#f5f5f3",
35900
+ gridMode: "none",
35901
+ gridColor: "#deded8",
35902
+ gridSpacing: 5,
35903
+ gridLineWidth: 1,
35904
+ surfaceTexture: "flat"
35905
+ };
35906
+ /** Grid alpha fades out below this on-screen spacing (px) — otherwise it aliases into a solid wash when zoomed out. */
35907
+ var GRID_FADE_MIN_PX = 3;
35908
+ var GRID_FADE_MAX_PX = 8;
35869
35909
  var VERTEX$1 = `
35870
35910
  varying vec3 vWorldPos;
35871
35911
  void main() {
@@ -35879,6 +35919,13 @@ var FRAGMENT = `
35879
35919
  uniform vec3 uFogColor;
35880
35920
  uniform float uFogNear;
35881
35921
  uniform float uFogFar;
35922
+ uniform float uGridMode;
35923
+ uniform vec3 uGridColor;
35924
+ uniform float uGridSpacing;
35925
+ uniform float uWorldPerPixel;
35926
+ uniform float uGridLineWidthPx;
35927
+ uniform float uGridFade;
35928
+ uniform float uSurfaceTexture;
35882
35929
  varying vec3 vWorldPos;
35883
35930
 
35884
35931
  float hash(vec2 p) {
@@ -35897,33 +35944,69 @@ var FRAGMENT = `
35897
35944
 
35898
35945
  ${FOG_GLSL}
35899
35946
 
35947
+ // Distance from p's nearest axis-aligned grid line, in world units.
35948
+ // A crisp, ~uGridLineWidthPx-wide line: full alpha to its half-width, a
35949
+ // ~1px anti-aliased falloff beyond that — not a wide, soft-edged band.
35950
+ float gridLineAlpha(vec2 p) {
35951
+ vec2 cell = mod(p, uGridSpacing);
35952
+ vec2 distToLine = min(cell, uGridSpacing - cell);
35953
+ float halfWidth = 0.5 * uGridLineWidthPx * uWorldPerPixel;
35954
+ float aa = uWorldPerPixel;
35955
+ vec2 lineAlpha2 = 1.0 - smoothstep(halfWidth - aa * 0.5, halfWidth + aa * 0.5, distToLine);
35956
+ return max(lineAlpha2.x, lineAlpha2.y);
35957
+ }
35958
+
35959
+ // Distance from p's nearest grid intersection, in world units. Dot radius
35960
+ // matches the configured line width in pixels, same anti-aliasing.
35961
+ float gridDotAlpha(vec2 p) {
35962
+ vec2 cell = mod(p, uGridSpacing) - uGridSpacing * 0.5;
35963
+ float dist = length(cell);
35964
+ float radius = uGridLineWidthPx * uWorldPerPixel;
35965
+ float aa = uWorldPerPixel;
35966
+ return 1.0 - smoothstep(radius - aa * 0.5, radius + aa * 0.5, dist);
35967
+ }
35968
+
35900
35969
  void main() {
35901
35970
  vec3 col = uColor;
35902
35971
 
35903
- // Melamine micro-texture: two octaves of value noise, +-1.75% brightness.
35904
- float n = vnoise(vWorldPos.xy * 2.3) * 0.6 + vnoise(vWorldPos.xy * 9.7) * 0.4;
35905
- col *= 1.0 + (n - 0.5) * 0.035;
35972
+ if (uSurfaceTexture > 0.5) {
35973
+ // Melamine micro-texture: two octaves of value noise, +-1.75% brightness.
35974
+ float n = vnoise(vWorldPos.xy * 2.3) * 0.6 + vnoise(vWorldPos.xy * 9.7) * 0.4;
35975
+ col *= 1.0 + (n - 0.5) * 0.035;
35906
35976
 
35907
- // Satin sheen: Fresnel rim + a specular band from the key light. Both are
35908
- // ~zero in the straight-on working view and bloom as the camera tilts.
35909
- vec3 V = normalize(cameraPosition - vWorldPos);
35910
- vec3 N = vec3(0.0, 0.0, 1.0);
35911
- float fres = pow(1.0 - max(dot(N, V), 0.0), 3.0);
35912
- vec3 L = normalize(vec3(-0.25, 0.4, 0.88));
35913
- vec3 H = normalize(L + V);
35914
- float spec = pow(max(dot(N, H), 0.0), 90.0);
35915
- col += (fres * 0.18 + spec * 0.06) * vec3(1.0);
35977
+ // Satin sheen: Fresnel rim + a specular band from the key light. Both
35978
+ // are ~zero in the straight-on working view and bloom as the camera tilts.
35979
+ vec3 V = normalize(cameraPosition - vWorldPos);
35980
+ vec3 N = vec3(0.0, 0.0, 1.0);
35981
+ float fres = pow(1.0 - max(dot(N, V), 0.0), 3.0);
35982
+ vec3 L = normalize(vec3(-0.25, 0.4, 0.88));
35983
+ vec3 H = normalize(L + V);
35984
+ float spec = pow(max(dot(N, H), 0.0), 90.0);
35985
+ col += (fres * 0.18 + spec * 0.06) * vec3(1.0);
35986
+ }
35987
+
35988
+ if (uGridMode > 0.5 && uGridFade > 0.001) {
35989
+ float gridAlpha = (uGridMode < 1.5 ? gridLineAlpha(vWorldPos.xy) : gridDotAlpha(vWorldPos.xy)) * uGridFade;
35990
+ col = mix(col, uGridColor, gridAlpha);
35991
+ }
35916
35992
 
35917
35993
  col = applyBoardFog(col, vWorldPos);
35918
35994
  gl_FragColor = vec4(col, 1.0);
35919
35995
  }
35920
35996
  `;
35921
- function createBoardSurface(fog) {
35997
+ function createBoardSurface(fog, theme) {
35922
35998
  const material = new ShaderMaterial({
35923
35999
  vertexShader: VERTEX$1,
35924
36000
  fragmentShader: FRAGMENT,
35925
36001
  uniforms: {
35926
- uColor: { value: new Color(BOARD_COLOR) },
36002
+ uColor: { value: new Color(theme.color) },
36003
+ uGridMode: { value: GRID_MODE_CODE[theme.gridMode] },
36004
+ uGridColor: { value: new Color(theme.gridColor) },
36005
+ uGridSpacing: { value: Math.max(.001, theme.gridSpacing) },
36006
+ uGridLineWidthPx: { value: Math.max(.1, theme.gridLineWidth) },
36007
+ uWorldPerPixel: { value: .05 },
36008
+ uGridFade: { value: 0 },
36009
+ uSurfaceTexture: { value: TEXTURE_CODE[theme.surfaceTexture] },
35927
36010
  ...fog
35928
36011
  }
35929
36012
  });
@@ -35931,11 +36014,34 @@ function createBoardSurface(fog) {
35931
36014
  mesh.name = "board-surface";
35932
36015
  return mesh;
35933
36016
  }
35934
- /** Keep the plane centered under the view and larger than the fog horizon. */
35935
- function updateBoardSurface(mesh, centerX, centerY, fogFar) {
36017
+ /** Applies a theme change (initial or live) everything except the per-frame fade/pixel-ratio. */
36018
+ function setBoardSurfaceTheme(mesh, theme) {
36019
+ const uniforms = mesh.material.uniforms;
36020
+ uniforms.uColor.value.set(theme.color);
36021
+ uniforms.uGridMode.value = GRID_MODE_CODE[theme.gridMode];
36022
+ uniforms.uGridColor.value.set(theme.gridColor);
36023
+ uniforms.uGridSpacing.value = Math.max(.001, theme.gridSpacing);
36024
+ uniforms.uGridLineWidthPx.value = Math.max(.1, theme.gridLineWidth);
36025
+ uniforms.uSurfaceTexture.value = TEXTURE_CODE[theme.surfaceTexture];
36026
+ }
36027
+ /** Keep the plane centered under the view, larger than the fog horizon, and the grid's on-screen weight/fade current. */
36028
+ function updateBoardSurface(mesh, centerX, centerY, fogFar, worldPerPixel) {
35936
36029
  mesh.position.set(centerX, centerY, 0);
35937
36030
  const size = fogFar * 2.5;
35938
36031
  mesh.scale.set(size, size, 1);
36032
+ const uniforms = mesh.material.uniforms;
36033
+ uniforms.uWorldPerPixel.value = worldPerPixel;
36034
+ const t = (uniforms.uGridSpacing.value / Math.max(1e-6, worldPerPixel) - GRID_FADE_MIN_PX) / (GRID_FADE_MAX_PX - GRID_FADE_MIN_PX);
36035
+ uniforms.uGridFade.value = MathUtils.clamp(t, 0, 1);
36036
+ }
36037
+ /** Temporarily zeroes the grid's contribution (for export snapshots) and returns a restore function. */
36038
+ function suppressBoardSurfaceGrid(mesh) {
36039
+ const uniforms = mesh.material.uniforms;
36040
+ const previous = uniforms.uGridFade.value;
36041
+ uniforms.uGridFade.value = 0;
36042
+ return () => {
36043
+ uniforms.uGridFade.value = previous;
36044
+ };
35939
36045
  }
35940
36046
  var MIN_HEIGHT = 40;
35941
36047
  var MAX_HEIGHT = 1500;
@@ -36097,9 +36203,9 @@ var CameraRig = class {
36097
36203
  }
36098
36204
  };
36099
36205
  //#endregion
36100
- //#region ../../src/scrawl/engine/scene.ts
36206
+ //#region src/renderer/canvas/scene.ts
36101
36207
  var SceneRig = class {
36102
- constructor(canvas) {
36208
+ constructor(canvas, boardTheme) {
36103
36209
  this.renderer = new WebGLRenderer({
36104
36210
  canvas,
36105
36211
  antialias: true
@@ -36109,7 +36215,7 @@ var SceneRig = class {
36109
36215
  this.scene = new Scene();
36110
36216
  this.cameraRig = new CameraRig(canvas.clientWidth / Math.max(1, canvas.clientHeight));
36111
36217
  this.fog = createFogUniforms(FOG_COLOR);
36112
- this.board = createBoardSurface(this.fog);
36218
+ this.board = createBoardSurface(this.fog, boardTheme);
36113
36219
  this.scene.add(this.board);
36114
36220
  this.inkGroup = new Group();
36115
36221
  this.inkGroup.name = "ink";
@@ -36126,9 +36232,17 @@ var SceneRig = class {
36126
36232
  renderFrame(dtMs) {
36127
36233
  this.cameraRig.update(dtMs);
36128
36234
  updateFogForCamera(this.fog, this.cameraRig.height);
36129
- updateBoardSurface(this.board, this.cameraRig.center.x, this.cameraRig.center.y, this.fog.uFogFar.value);
36235
+ updateBoardSurface(this.board, this.cameraRig.center.x, this.cameraRig.center.y, this.fog.uFogFar.value, this.cameraRig.worldPerPixel());
36130
36236
  this.renderer.render(this.scene, this.cameraRig.camera);
36131
36237
  }
36238
+ /** Live theme update — the board controller's identity stays fixed across theme changes; this is how a new one reaches the render engine. */
36239
+ applyBoardTheme(theme) {
36240
+ setBoardSurfaceTheme(this.board, theme);
36241
+ }
36242
+ /** Zeroes the grid for one render pass (PNG export never bakes it in) and returns a restore function. */
36243
+ suppressGrid() {
36244
+ return suppressBoardSurfaceGrid(this.board);
36245
+ }
36132
36246
  dispose() {
36133
36247
  this.board.geometry.dispose();
36134
36248
  this.board.material.dispose();
@@ -36136,7 +36250,7 @@ var SceneRig = class {
36136
36250
  }
36137
36251
  };
36138
36252
  //#endregion
36139
- //#region ../../src/scrawl/ink/inkMaterial.ts
36253
+ //#region src/renderer/shapes/inkMaterial.ts
36140
36254
  var VERTEX = `
36141
36255
  attribute float aAlpha;
36142
36256
  varying vec2 vUv;
@@ -36267,7 +36381,7 @@ var InkMaterials = class {
36267
36381
  }
36268
36382
  };
36269
36383
  //#endregion
36270
- //#region ../../src/scrawl/ink/ribbonGeometry.ts
36384
+ //#region src/renderer/shapes/ribbonGeometry.ts
36271
36385
  /** Ink floats a hair above the board to avoid z-fighting with the surface. */
36272
36386
  var INK_Z = .02;
36273
36387
  /** Highlighter always renders under opaque ink (build prompt §4.1). */
@@ -36318,7 +36432,7 @@ function buildRibbonGeometry(points, baseWidth, z = INK_Z) {
36318
36432
  return geometry;
36319
36433
  }
36320
36434
  //#endregion
36321
- //#region ../../src/scrawl/ink/strokeRenderer.ts
36435
+ //#region src/renderer/shapes/strokeRenderer.ts
36322
36436
  var strokeZ = (stroke) => stroke.tool === "highlighter" ? HIGHLIGHT_Z : INK_Z;
36323
36437
  /**
36324
36438
  * Derives ink meshes from the document by subscription — the document never
@@ -36451,7 +36565,7 @@ function syncMatrix(mesh, stroke) {
36451
36565
  mesh.matrixWorldNeedsUpdate = true;
36452
36566
  }
36453
36567
  //#endregion
36454
- //#region ../../src/scrawl/scene-objects/markerProp.ts
36568
+ //#region src/renderer/shapes/markerProp.ts
36455
36569
  var BODY_LENGTH = 11;
36456
36570
  var BODY_RADIUS = .85;
36457
36571
  var TIP_LENGTH = 1.6;
@@ -36547,7 +36661,7 @@ var MarkerProp = class {
36547
36661
  }
36548
36662
  };
36549
36663
  //#endregion
36550
- //#region ../../src/scrawl/scene-objects/noteGeometry.ts
36664
+ //#region src/renderer/shapes/noteGeometry.ts
36551
36665
  /**
36552
36666
  * Height of the sheet above the board at (u, v) ∈ [0,1]².
36553
36667
  * v = 1 is the adhesive edge (always 0); u = 1 is the peel corner side.
@@ -36591,7 +36705,7 @@ function applyCurl(geometry, curl, peel) {
36591
36705
  color.needsUpdate = true;
36592
36706
  }
36593
36707
  //#endregion
36594
- //#region ../../src/scrawl/scene-objects/stickyNotes.ts
36708
+ //#region src/renderer/shapes/stickyNotes.ts
36595
36709
  var TEXTURE_SIZE = 512;
36596
36710
  var FOCUS_COLOR$4 = 2450411;
36597
36711
  var CURL = .3;
@@ -43167,7 +43281,7 @@ SYNCABLE_PROPS.forEach((prop) => {
43167
43281
  new Box3();
43168
43282
  new Color();
43169
43283
  //#endregion
43170
- //#region ../../src/scrawl/scene-objects/tables.ts
43284
+ //#region src/renderer/shapes/tables.ts
43171
43285
  var FONT_URL$2 = "/fonts/inter-medium.woff";
43172
43286
  var TABLE_Z = .012;
43173
43287
  var HEADER_Z = .014;
@@ -43516,7 +43630,7 @@ var TablesRenderer = class {
43516
43630
  }
43517
43631
  };
43518
43632
  //#endregion
43519
- //#region ../../src/scrawl/scene-objects/textBlocks.ts
43633
+ //#region src/renderer/text/textBlocks.ts
43520
43634
  var FONT_URL$1 = "/fonts/inter-medium.woff";
43521
43635
  var TEXT_Z = .02;
43522
43636
  var FOCUS_COLOR$2 = 2450411;
@@ -43620,7 +43734,7 @@ var TextBlocksRenderer = class {
43620
43734
  }
43621
43735
  };
43622
43736
  //#endregion
43623
- //#region ../../src/scrawl/scene-objects/comments.ts
43737
+ //#region src/renderer/shapes/comments.ts
43624
43738
  var PIN_Z = .04;
43625
43739
  var PIN_WIDTH = 3.6;
43626
43740
  var PIN_HEIGHT = 3.6;
@@ -43788,7 +43902,7 @@ function drawBubblePath(ctx, x, y, w, h, r, tailX, tailY, tailW) {
43788
43902
  ctx.closePath();
43789
43903
  }
43790
43904
  //#endregion
43791
- //#region ../../src/scrawl/scene-objects/images.ts
43905
+ //#region src/renderer/shapes/images.ts
43792
43906
  var IMAGE_Z = .015;
43793
43907
  var FRAME_BORDER = .4;
43794
43908
  var FOCUS_COLOR$1 = 2450411;
@@ -43998,7 +44112,7 @@ function applyPlaceholderTexture(visual, label) {
43998
44112
  visual.imageMesh.material.needsUpdate = true;
43999
44113
  }
44000
44114
  //#endregion
44001
- //#region ../../src/scrawl/scene-objects/kitchenTimers.ts
44115
+ //#region src/renderer/shapes/kitchenTimers.ts
44002
44116
  var FACE_SIZE = 256;
44003
44117
  var BODY_COLOR = 14753096;
44004
44118
  var KNOB_COLOR = 16448250;
@@ -44196,7 +44310,7 @@ function renderTimerFace(timer, canvas, now = Date.now()) {
44196
44310
  return c;
44197
44311
  }
44198
44312
  //#endregion
44199
- //#region ../../src/scrawl/scene-objects/customObjects.ts
44313
+ //#region src/renderer/shapes/customObjects.ts
44200
44314
  var FONT_URL = "/fonts/inter-medium.woff";
44201
44315
  var CUSTOM_Z = .02;
44202
44316
  var FOCUS_COLOR = 2450411;
@@ -44558,7 +44672,7 @@ function disposeObject3D(obj) {
44558
44672
  });
44559
44673
  }
44560
44674
  //#endregion
44561
- //#region ../../src/scrawl/tools/hitTest.ts
44675
+ //#region src/interaction/pointer/hitTest.ts
44562
44676
  /** Topmost (most recently added) stroke under the point, or null. */
44563
44677
  function hitStroke(doc, index, p, slop) {
44564
44678
  const candidates = index.query(p.x - slop, p.y - slop, p.x + slop, p.y + slop);
@@ -44599,7 +44713,7 @@ function distToSegmentSq(p, a, b) {
44599
44713
  return dx * dx + dy * dy;
44600
44714
  }
44601
44715
  //#endregion
44602
- //#region ../../src/scrawl/tools/eraserTool.ts
44716
+ //#region src/interaction/tools/eraserTool.ts
44603
44717
  var DECAY_PER_PASS = .4;
44604
44718
  /** A point can take another decay pass after this long — rubbing works. */
44605
44719
  var REARM_MS = 250;
@@ -44727,7 +44841,7 @@ function partitionByErasure(points) {
44727
44841
  return runs;
44728
44842
  }
44729
44843
  //#endregion
44730
- //#region ../../src/scrawl/tools/noteDrag.ts
44844
+ //#region src/interaction/tools/noteDrag.ts
44731
44845
  /**
44732
44846
  * Drag the focused note in X/Y. The grab is resolved on the note's own
44733
44847
  * z-plane, not the board, so the note doesn't jump under a tilted ray.
@@ -44794,7 +44908,7 @@ var NoteDragState = class {
44794
44908
  }
44795
44909
  };
44796
44910
  //#endregion
44797
- //#region ../../src/scrawl/tools/imageDrag.ts
44911
+ //#region src/interaction/tools/imageDrag.ts
44798
44912
  /** Drag the focused image in X/Y; one command per drag. */
44799
44913
  var ImageDragState = class {
44800
44914
  constructor(ctx) {
@@ -44846,7 +44960,7 @@ var ImageDragState = class {
44846
44960
  }
44847
44961
  };
44848
44962
  //#endregion
44849
- //#region ../../src/scrawl/tools/timerDrag.ts
44963
+ //#region src/interaction/tools/timerDrag.ts
44850
44964
  /** Drag the focused timer in X/Y; one command per drag. */
44851
44965
  var TimerDragState = class {
44852
44966
  constructor(ctx) {
@@ -44898,7 +45012,7 @@ var TimerDragState = class {
44898
45012
  }
44899
45013
  };
44900
45014
  //#endregion
44901
- //#region ../../src/scrawl/tools/shapes.ts
45015
+ //#region src/interaction/tools/shapeGeometry.ts
44902
45016
  /** Uniform synthetic pressure; the ink shader supplies the hand-drawn feel. */
44903
45017
  var PRESSURE = .7;
44904
45018
  var MIN_ELLIPSE_SEGMENTS = 24;
@@ -45087,7 +45201,7 @@ function snappedEnd(a, b) {
45087
45201
  };
45088
45202
  }
45089
45203
  //#endregion
45090
- //#region ../../src/scrawl/tools/shapeTool.ts
45204
+ //#region src/interaction/tools/shapeTool.ts
45091
45205
  var MIN_DRAG_PX$1 = 4;
45092
45206
  /** Drag out a shape; live preview through the real ink material. */
45093
45207
  var ShapeTool = class {
@@ -45118,7 +45232,7 @@ var ShapeTool = class {
45118
45232
  const strokes = this.polylines.map((points) => ({
45119
45233
  id: crypto.randomUUID(),
45120
45234
  color: this.ctx.inkColor(),
45121
- baseWidth: this.ctx.baseWidth(),
45235
+ baseWidth: this.ctx.shapeStrokeWidth(),
45122
45236
  points
45123
45237
  }));
45124
45238
  this.ctx.clusters.assign(strokes[0]);
@@ -45152,13 +45266,13 @@ var ShapeTool = class {
45152
45266
  this.previews[i].visible = polyline !== void 0;
45153
45267
  if (polyline) {
45154
45268
  this.previews[i].geometry.dispose();
45155
- this.previews[i].geometry = buildRibbonGeometry(polyline, this.ctx.baseWidth());
45269
+ this.previews[i].geometry = buildRibbonGeometry(polyline, this.ctx.shapeStrokeWidth());
45156
45270
  }
45157
45271
  }
45158
45272
  }
45159
45273
  };
45160
45274
  //#endregion
45161
- //#region ../../src/scrawl/tools/tableDrag.ts
45275
+ //#region src/interaction/tools/tableDrag.ts
45162
45276
  function getHandleCursor(handle) {
45163
45277
  switch (handle) {
45164
45278
  case "nw":
@@ -45278,7 +45392,7 @@ var TableDragState = class {
45278
45392
  }
45279
45393
  };
45280
45394
  //#endregion
45281
- //#region ../../src/scrawl/tools/tableTool.ts
45395
+ //#region src/interaction/tools/tableTool.ts
45282
45396
  var MIN_DRAG_PX = 6;
45283
45397
  var TableTool = class {
45284
45398
  constructor(ctx) {
@@ -45409,7 +45523,7 @@ var TableTool = class {
45409
45523
  }
45410
45524
  };
45411
45525
  //#endregion
45412
- //#region ../../src/scrawl/tools/commentTool.ts
45526
+ //#region src/interaction/tools/commentTool.ts
45413
45527
  /** Click on the board to drop a new comment pin or open an existing one. */
45414
45528
  var CommentTool = class {
45415
45529
  constructor(ctx) {
@@ -45444,7 +45558,7 @@ var CommentTool = class {
45444
45558
  }
45445
45559
  };
45446
45560
  //#endregion
45447
- //#region ../../src/scrawl/tools/textDrag.ts
45561
+ //#region src/interaction/tools/textDrag.ts
45448
45562
  /** Drag the focused text block in X/Y; one command per drag. */
45449
45563
  var TextDragState = class {
45450
45564
  constructor(ctx) {
@@ -45496,7 +45610,7 @@ var TextDragState = class {
45496
45610
  }
45497
45611
  };
45498
45612
  //#endregion
45499
- //#region ../../src/scrawl/tools/fsm.ts
45613
+ //#region src/interaction/tools/fsm.ts
45500
45614
  var ToolStateMachine = class {
45501
45615
  constructor() {
45502
45616
  this.states = /* @__PURE__ */ new Map();
@@ -45698,7 +45812,7 @@ var IdleState = class {
45698
45812
  }
45699
45813
  };
45700
45814
  //#endregion
45701
- //#region ../../src/scrawl/tools/gizmo.ts
45815
+ //#region src/renderer/selection/gizmo.ts
45702
45816
  var GIZMO_Z = .05;
45703
45817
  var HANDLE_PX = 9;
45704
45818
  var HANDLE_HIT_PX = 12;
@@ -45929,7 +46043,7 @@ function boxesEqual(a, b) {
45929
46043
  return a.minX === b.minX && a.minY === b.minY && a.maxX === b.maxX && a.maxY === b.maxY;
45930
46044
  }
45931
46045
  //#endregion
45932
- //#region ../../src/scrawl/ink/pressure.ts
46046
+ //#region src/interaction/tools/pressure.ts
45933
46047
  var SIM_MAX = 1;
45934
46048
  var SIM_MIN = .3;
45935
46049
  var SMOOTHING = .25;
@@ -45951,7 +46065,7 @@ function clamp(v, lo, hi) {
45951
46065
  return Math.min(hi, Math.max(lo, v));
45952
46066
  }
45953
46067
  //#endregion
45954
- //#region ../../src/scrawl/ink/strokeBuilder.ts
46068
+ //#region src/interaction/tools/strokeBuilder.ts
45955
46069
  var StrokeBuilder = class {
45956
46070
  constructor() {
45957
46071
  this.points = [];
@@ -46001,7 +46115,7 @@ function catmullRom(v0, v1, v2, v3, t) {
46001
46115
  return .5 * (2 * v1 + (-v0 + v2) * t + (2 * v0 - 5 * v1 + 4 * v2 - v3) * t2 + (-v0 + 3 * v1 - 3 * v2 + v3) * t3);
46002
46116
  }
46003
46117
  //#endregion
46004
- //#region ../../src/scrawl/tools/markerTool.ts
46118
+ //#region src/interaction/tools/markerTool.ts
46005
46119
  /** Sample spacing while drawing, in screen pixels. */
46006
46120
  var MIN_SAMPLE_PX = 1.5;
46007
46121
  /** A highlighter is a chisel tip: near-constant width, little pressure play. */
@@ -46104,7 +46218,7 @@ var MarkerTool = class {
46104
46218
  }
46105
46219
  };
46106
46220
  //#endregion
46107
- //#region ../../src/scrawl/tools/orbitTool.ts
46221
+ //#region src/interaction/tools/orbitTool.ts
46108
46222
  /**
46109
46223
  * Orbit-preview (build prompt §4.2): alt-drag tilts the camera off-axis to
46110
46224
  * read the board's material and depth; releasing eases back to the working
@@ -46139,7 +46253,7 @@ var OrbitTool = class {
46139
46253
  }
46140
46254
  };
46141
46255
  //#endregion
46142
- //#region ../../src/scrawl/tools/panTool.ts
46256
+ //#region src/interaction/tools/panTool.ts
46143
46257
  /** Grab-pan: content follows the pointer. */
46144
46258
  var PanTool = class {
46145
46259
  constructor(ctx) {
@@ -46169,7 +46283,7 @@ var PanTool = class {
46169
46283
  }
46170
46284
  };
46171
46285
  //#endregion
46172
- //#region ../../src/scrawl/tools/selection.ts
46286
+ //#region src/renderer/selection/selection.ts
46173
46287
  /** The set of selected stroke ids. Drops ids the document no longer has. */
46174
46288
  var SelectionManager = class {
46175
46289
  constructor(doc) {
@@ -46220,7 +46334,7 @@ var SelectionManager = class {
46220
46334
  }
46221
46335
  };
46222
46336
  //#endregion
46223
- //#region ../../src/scrawl/tools/selectTool.ts
46337
+ //#region src/interaction/tools/selectTool.ts
46224
46338
  var HIT_SLOP_PX = 4;
46225
46339
  var CLICK_TOLERANCE_PX = 3;
46226
46340
  var MIN_SCALE = .05;
@@ -46436,7 +46550,7 @@ function safeRatio(numerator, denominator) {
46436
46550
  return raw;
46437
46551
  }
46438
46552
  //#endregion
46439
- //#region ../../src/scrawl/tools/customTool.ts
46553
+ //#region src/interaction/tools/customTool.ts
46440
46554
  function extensionIdFromToolId(toolId) {
46441
46555
  const slash = toolId.indexOf("/");
46442
46556
  return slash === -1 ? toolId : toolId.slice(0, slash);
@@ -46551,12 +46665,19 @@ var ExtensionCommandBatch = class {
46551
46665
  }
46552
46666
  };
46553
46667
  //#endregion
46554
- //#region ../../src/scrawl/ScrawlEngine.ts
46668
+ //#region src/composition/ScrawlEngine.ts
46555
46669
  /** Marker stroke width in board units (~1% of the default view height). */
46556
46670
  var BASE_WIDTH = .5;
46557
46671
  /** Highlighter chisel width — wide enough to cover a line of handwriting. */
46558
46672
  var HIGHLIGHT_WIDTH = 2.2;
46559
46673
  /**
46674
+ * Shape (rectangle, ellipse, arrow, ...) border width in board units.
46675
+ * Deliberately thinner than BASE_WIDTH: a shape border reads as a crisp,
46676
+ * structural line, not an expressive marker stroke — this is also the
46677
+ * fallback when no Host theme configures ScrawlTheme.shapeStrokeWidth.
46678
+ */
46679
+ var DEFAULT_SHAPE_STROKE_WIDTH = .15;
46680
+ /**
46560
46681
  * Identity of a rendered selection badge: which item, its lock state, and
46561
46682
  * where it sits on screen (rounded to the pixel, so sub-pixel camera drift
46562
46683
  * does not churn React).
@@ -46577,9 +46698,11 @@ function emptyExtensionRegistry() {
46577
46698
  * React never reaches past this class.
46578
46699
  */
46579
46700
  var ScrawlEngine = class {
46580
- constructor(canvas, documentId$1, extensionRegistry = emptyExtensionRegistry(), assetController = new AssetController(void 0)) {
46701
+ constructor(canvas, documentId$1, extensionRegistry = emptyExtensionRegistry(), assetController = new AssetController(void 0), boardTheme = DEFAULT_BOARD_SURFACE_THEME, shapeStrokeWidth = DEFAULT_SHAPE_STROKE_WIDTH) {
46581
46702
  this.canvas = canvas;
46582
46703
  this.extensionRegistry = extensionRegistry;
46704
+ this.boardTheme = boardTheme;
46705
+ this.shapeStrokeWidth = shapeStrokeWidth;
46583
46706
  this.onToolChange = null;
46584
46707
  this.onZoomChange = null;
46585
46708
  this.onAuditEvent = null;
@@ -46626,7 +46749,7 @@ var ScrawlEngine = class {
46626
46749
  this.stampKind = "star";
46627
46750
  this.timerDurationMs = TIMER_DEFAULT_DURATION_MS;
46628
46751
  this.readOnly = false;
46629
- this.scene = new SceneRig(canvas);
46752
+ this.scene = new SceneRig(canvas, boardTheme);
46630
46753
  this.document = new BoardDocument(documentId(documentId$1));
46631
46754
  this.history = new History(this.document);
46632
46755
  this.history.onCommand = (command, kind) => {
@@ -46674,6 +46797,7 @@ var ScrawlEngine = class {
46674
46797
  setTool: (t) => this.setTool(t),
46675
46798
  inkColor: () => this.tool === "highlighter" ? this.highlightColor : this.color,
46676
46799
  baseWidth: () => this.tool === "highlighter" ? HIGHLIGHT_WIDTH : BASE_WIDTH,
46800
+ shapeStrokeWidth: () => this.shapeStrokeWidth,
46677
46801
  activeTool: () => this.tool,
46678
46802
  isSpaceHeld: () => this.spaceHeld,
46679
46803
  pointerToBoard: (e) => pointerToBoard(e.clientX, e.clientY, canvas, this.scene.cameraRig.camera),
@@ -46933,6 +47057,15 @@ var ScrawlEngine = class {
46933
47057
  height: rig.height
46934
47058
  };
46935
47059
  }
47060
+ /** Live update of the board surface color/grid — the engine's identity stays fixed across theme changes. */
47061
+ applyBoardTheme(theme) {
47062
+ this.boardTheme = theme;
47063
+ this.scene.applyBoardTheme(theme);
47064
+ }
47065
+ /** Live update of the shape tool's border width (board units). Already-drawn shapes keep the width they were drawn with. */
47066
+ setShapeStrokeWidth(width) {
47067
+ this.shapeStrokeWidth = width;
47068
+ }
46936
47069
  /**
46937
47070
  * Snap to a peer's camera. No-op while drawing so pointer-to-board
46938
47071
  * mapping does not warp the in-progress stroke.
@@ -46951,7 +47084,7 @@ var ScrawlEngine = class {
46951
47084
  }
46952
47085
  /** The saved document as standalone SVG — a pure walk of the wire format. */
46953
47086
  exportSVG() {
46954
- return documentToSVG(this.document.toJSON(), Date.now(), this.extensionRegistry);
47087
+ return documentToSVG(this.document.toJSON(), Date.now(), this.extensionRegistry, this.boardTheme.color);
46955
47088
  }
46956
47089
  /**
46957
47090
  * Content-framed PNG snapshot. Renders once offscreen-style with UI props
@@ -46987,6 +47120,7 @@ var ScrawlEngine = class {
46987
47120
  this.notesRenderer.setFocus(null);
46988
47121
  this.textsRenderer.setFocus(null);
46989
47122
  this.renderer.setGhostsVisible(false);
47123
+ const restoreGrid = this.scene.suppressGrid();
46990
47124
  try {
46991
47125
  rig.yaw = 0;
46992
47126
  rig.pitch = 0;
@@ -46997,6 +47131,7 @@ var ScrawlEngine = class {
46997
47131
  const dataUrl = this.scene.renderer.domElement.toDataURL("image/png");
46998
47132
  return await (await fetch(dataUrl)).blob();
46999
47133
  } finally {
47134
+ restoreGrid();
47000
47135
  rig.center.x = saved.x;
47001
47136
  rig.center.y = saved.y;
47002
47137
  rig.height = saved.height;
@@ -47992,7 +48127,7 @@ var ScrawlEngine = class {
47992
48127
  }
47993
48128
  };
47994
48129
  //#endregion
47995
- //#region src/controller-internal.ts
48130
+ //#region src/composition/controller-internal.ts
47996
48131
  function createBoardController(options) {
47997
48132
  return createBoardControllerWithEngine(options);
47998
48133
  }
@@ -48006,7 +48141,16 @@ function createBoardControllerWithEngine(options, existingEngine) {
48006
48141
  }
48007
48142
  const extensionRegistry = registryResult.registry;
48008
48143
  const assetController = new AssetController(options.assetResolver, { cacheBytes: options.assetCacheBytes });
48009
- const engine = existingEngine ?? (options.canvas ? new ScrawlEngine(options.canvas, options.document.id, extensionRegistry, assetController) : null);
48144
+ let currentBoardTheme = {
48145
+ color: options.boardTheme?.surface ?? DEFAULT_BOARD_SURFACE_THEME.color,
48146
+ gridMode: options.boardTheme?.gridMode ?? DEFAULT_BOARD_SURFACE_THEME.gridMode,
48147
+ gridColor: options.boardTheme?.gridColor ?? DEFAULT_BOARD_SURFACE_THEME.gridColor,
48148
+ gridSpacing: options.boardTheme?.gridSpacing ?? DEFAULT_BOARD_SURFACE_THEME.gridSpacing,
48149
+ gridLineWidth: options.boardTheme?.gridLineWidth ?? DEFAULT_BOARD_SURFACE_THEME.gridLineWidth,
48150
+ surfaceTexture: options.boardTheme?.surfaceTexture ?? DEFAULT_BOARD_SURFACE_THEME.surfaceTexture
48151
+ };
48152
+ let currentShapeStrokeWidth = options.boardTheme?.shapeStrokeWidth ?? .15;
48153
+ const engine = existingEngine ?? (options.canvas ? new ScrawlEngine(options.canvas, options.document.id, extensionRegistry, assetController, currentBoardTheme, currentShapeStrokeWidth) : null);
48010
48154
  const document = engine?.document ?? new BoardDocument(documentId(options.document.id));
48011
48155
  const history = engine?.history ?? new History(document);
48012
48156
  const createId = options.createId ?? (() => crypto.randomUUID());
@@ -48490,6 +48634,21 @@ function createBoardControllerWithEngine(options, existingEngine) {
48490
48634
  changed();
48491
48635
  }
48492
48636
  },
48637
+ boardTheme: { set(theme) {
48638
+ currentBoardTheme = {
48639
+ color: theme.surface ?? currentBoardTheme.color,
48640
+ gridMode: theme.gridMode ?? currentBoardTheme.gridMode,
48641
+ gridColor: theme.gridColor ?? currentBoardTheme.gridColor,
48642
+ gridSpacing: theme.gridSpacing ?? currentBoardTheme.gridSpacing,
48643
+ gridLineWidth: theme.gridLineWidth ?? currentBoardTheme.gridLineWidth,
48644
+ surfaceTexture: theme.surfaceTexture ?? currentBoardTheme.surfaceTexture
48645
+ };
48646
+ engine?.applyBoardTheme(currentBoardTheme);
48647
+ if (theme.shapeStrokeWidth !== void 0) {
48648
+ currentShapeStrokeWidth = theme.shapeStrokeWidth;
48649
+ engine?.setShapeStrokeWidth(currentShapeStrokeWidth);
48650
+ }
48651
+ } },
48493
48652
  view: {
48494
48653
  fit() {
48495
48654
  assertActive();
@@ -48794,10 +48953,10 @@ function createBoardControllerWithEngine(options, existingEngine) {
48794
48953
  gather: (nextView) => engine?.applyGatherView(nextView) ?? false
48795
48954
  },
48796
48955
  export: {
48797
- svg: () => engine ? engine.exportSVG() : documentToSVG(document.toJSON(), Date.now(), extensionRegistry),
48956
+ svg: () => engine ? engine.exportSVG() : documentToSVG(document.toJSON(), Date.now(), extensionRegistry, currentBoardTheme.color),
48798
48957
  png: (options) => engine ? engine.exportPNG(options?.maxSide, { awaitAssets: options?.awaitAssets }) : Promise.resolve(null),
48799
48958
  json: () => document.toJSON(),
48800
- svgAsync: (svgOptions) => exportDocumentSVGWithAssets(document.toJSON(), Date.now(), extensionRegistry, assetController, svgOptions)
48959
+ svgAsync: (svgOptions) => exportDocumentSVGWithAssets(document.toJSON(), Date.now(), extensionRegistry, assetController, currentBoardTheme.color, svgOptions)
48801
48960
  },
48802
48961
  assets: { async ingest(bytes, mediaType, name, signal) {
48803
48962
  assertActive();
@@ -49259,7 +49418,7 @@ function createLocalBoard(options) {
49259
49418
  };
49260
49419
  }
49261
49420
  //#endregion
49262
- //#region src/theme.ts
49421
+ //#region src/ui/theme.ts
49263
49422
  var scrawlThemePresets = Object.freeze({
49264
49423
  light: Object.freeze({
49265
49424
  surface: "#ffffff",
@@ -49284,7 +49443,14 @@ var scrawlThemePresets = Object.freeze({
49284
49443
  elevationHigh: "0 8px 20px rgba(28, 28, 26, 0.18)",
49285
49444
  motionDuration: 140,
49286
49445
  motionEasing: "cubic-bezier(0.2, 0, 0, 1)",
49287
- density: "comfortable"
49446
+ density: "comfortable",
49447
+ boardSurface: "#f5f5f3",
49448
+ boardSurfaceTexture: "flat",
49449
+ gridMode: "none",
49450
+ gridColor: "#deded8",
49451
+ gridSpacing: 5,
49452
+ gridLineWidth: 1,
49453
+ shapeStrokeWidth: .15
49288
49454
  }),
49289
49455
  dark: Object.freeze({
49290
49456
  surface: "#171816",
@@ -49309,7 +49475,14 @@ var scrawlThemePresets = Object.freeze({
49309
49475
  elevationHigh: "0 8px 20px rgba(0, 0, 0, 0.38)",
49310
49476
  motionDuration: 140,
49311
49477
  motionEasing: "cubic-bezier(0.2, 0, 0, 1)",
49312
- density: "comfortable"
49478
+ density: "comfortable",
49479
+ boardSurface: "#232420",
49480
+ boardSurfaceTexture: "flat",
49481
+ gridMode: "none",
49482
+ gridColor: "#33352f",
49483
+ gridSpacing: 5,
49484
+ gridLineWidth: 1,
49485
+ shapeStrokeWidth: .15
49313
49486
  })
49314
49487
  });
49315
49488
  var variableNames = {
@@ -49335,7 +49508,14 @@ var variableNames = {
49335
49508
  elevationHigh: "--scrawl-elevation-high",
49336
49509
  motionDuration: "--scrawl-motion-duration",
49337
49510
  motionEasing: "--scrawl-motion-easing",
49338
- density: "--scrawl-density"
49511
+ density: "--scrawl-density",
49512
+ boardSurface: "--scrawl-board-surface",
49513
+ boardSurfaceTexture: "--scrawl-board-surface-texture",
49514
+ gridMode: "--scrawl-grid-mode",
49515
+ gridColor: "--scrawl-grid-color",
49516
+ gridSpacing: "--scrawl-grid-spacing",
49517
+ gridLineWidth: "--scrawl-grid-line-width",
49518
+ shapeStrokeWidth: "--scrawl-shape-stroke-width"
49339
49519
  };
49340
49520
  var colorTokens = /* @__PURE__ */ new Set([
49341
49521
  "surface",
@@ -49348,7 +49528,9 @@ var colorTokens = /* @__PURE__ */ new Set([
49348
49528
  "selection",
49349
49529
  "danger",
49350
49530
  "warning",
49351
- "success"
49531
+ "success",
49532
+ "boardSurface",
49533
+ "gridColor"
49352
49534
  ]);
49353
49535
  var knownTokens = new Set(Object.keys(scrawlThemePresets.light));
49354
49536
  function validateScrawlTheme(theme) {
@@ -49385,6 +49567,26 @@ function validateScrawlTheme(theme) {
49385
49567
  token,
49386
49568
  message: "Expected comfortable or compact"
49387
49569
  });
49570
+ else if (token === "gridMode" && value !== "none" && value !== "line" && value !== "dot") diagnostics.push({
49571
+ token,
49572
+ message: "Expected none, line, or dot"
49573
+ });
49574
+ else if (token === "boardSurfaceTexture" && value !== "flat" && value !== "textured") diagnostics.push({
49575
+ token,
49576
+ message: "Expected flat or textured"
49577
+ });
49578
+ else if (token === "gridSpacing" && (!finiteNumber(value) || value < .5 || value > 50)) diagnostics.push({
49579
+ token,
49580
+ message: "Expected spacing 0.5-50 board units"
49581
+ });
49582
+ else if (token === "gridLineWidth" && (!finiteNumber(value) || value < .5 || value > 8)) diagnostics.push({
49583
+ token,
49584
+ message: "Expected line width 0.5-8px"
49585
+ });
49586
+ else if (token === "shapeStrokeWidth" && (!finiteNumber(value) || value < .01 || value > 5)) diagnostics.push({
49587
+ token,
49588
+ message: "Expected stroke width 0.01-5 board units"
49589
+ });
49388
49590
  else if ((token.endsWith("Family") || token.startsWith("elevation") || token === "motionEasing") && (typeof value !== "string" || !value.trim() || /[;{}]/.test(value))) diagnostics.push({
49389
49591
  token,
49390
49592
  message: "Expected a safe CSS value"
@@ -49455,12 +49657,12 @@ function luminance(color) {
49455
49657
  return channels[0] * .2126 + channels[1] * .7152 + channels[2] * .0722;
49456
49658
  }
49457
49659
  function formatValue(token, value) {
49458
- if (token === "baseFontSize" || token === "controlRadius" || token === "panelRadius") return `${value}px`;
49660
+ if (token === "baseFontSize" || token === "controlRadius" || token === "panelRadius" || token === "gridLineWidth") return `${value}px`;
49459
49661
  if (token === "motionDuration") return `${value}ms`;
49460
49662
  return String(value);
49461
49663
  }
49462
49664
  //#endregion
49463
- //#region src/inline-editing.tsx
49665
+ //#region src/ui/dialogs/inline-editing.tsx
49464
49666
  /** Inline note, text, and table-cell editors — driven entirely by public controller events and commands. */
49465
49667
  function InlineEditors({ controller, renderPortal }) {
49466
49668
  const [editor, setEditor] = useState(null);
@@ -49691,7 +49893,7 @@ function InlineEditors({ controller, renderPortal }) {
49691
49893
  }))] });
49692
49894
  }
49693
49895
  //#endregion
49694
- //#region src/style-shelf.tsx
49896
+ //#region src/ui/toolbar/style-shelf.tsx
49695
49897
  var ERASER_RADII = [
49696
49898
  ["Fine", 1],
49697
49899
  ["Medium", 2],
@@ -49854,7 +50056,7 @@ function StyleShelf({ controller, snapshot }) {
49854
50056
  });
49855
50057
  }
49856
50058
  //#endregion
49857
- //#region src/default-ui.tsx
50059
+ //#region src/ui/default-ui.tsx
49858
50060
  var PRIMARY_TOOLS = [
49859
50061
  {
49860
50062
  tool: "select",
@@ -50383,7 +50585,7 @@ function download(blob, name) {
50383
50585
  URL.revokeObjectURL(url);
50384
50586
  }
50385
50587
  //#endregion
50386
- //#region src/presence.tsx
50588
+ //#region src/ui/panels/presence.tsx
50387
50589
  var labelStyle = {
50388
50590
  borderRadius: 999,
50389
50591
  color: "#fff",
@@ -50540,6 +50742,17 @@ function ScrawlProvider({ controller, children, preset = "light", theme, portalC
50540
50742
  style
50541
50743
  ]);
50542
50744
  useEffect(() => reportThemeDiagnostics(resolved.diagnostics, onThemeDiagnostic), [resolved, onThemeDiagnostic]);
50745
+ useEffect(() => {
50746
+ controller.boardTheme.set({
50747
+ surface: resolved.values.boardSurface,
50748
+ surfaceTexture: resolved.values.boardSurfaceTexture,
50749
+ gridMode: resolved.values.gridMode,
50750
+ gridColor: resolved.values.gridColor,
50751
+ gridSpacing: resolved.values.gridSpacing,
50752
+ gridLineWidth: resolved.values.gridLineWidth,
50753
+ shapeStrokeWidth: resolved.values.shapeStrokeWidth
50754
+ });
50755
+ }, [controller, resolved]);
50543
50756
  useDeferredDisposal(controller, disposeOnUnmount);
50544
50757
  const value = useMemo(() => ({
50545
50758
  controller,
@@ -50587,8 +50800,19 @@ function Scrawl({ children, preset, theme, portalContainer, className, style, on
50587
50800
  try {
50588
50801
  if (!runtimeRef.current) {
50589
50802
  const canvas = suppliedCanvas === void 0 ? ownedCanvasRef.current : suppliedCanvas;
50803
+ const resolved = resolveScrawlTheme(preset, theme);
50804
+ const boardTheme = {
50805
+ surface: resolved.values.boardSurface,
50806
+ surfaceTexture: resolved.values.boardSurfaceTexture,
50807
+ gridMode: resolved.values.gridMode,
50808
+ gridColor: resolved.values.gridColor,
50809
+ gridSpacing: resolved.values.gridSpacing,
50810
+ gridLineWidth: resolved.values.gridLineWidth,
50811
+ shapeStrokeWidth: resolved.values.shapeStrokeWidth
50812
+ };
50590
50813
  controller = createBoardController({
50591
50814
  ...options,
50815
+ boardTheme,
50592
50816
  ...canvas ? { canvas } : {}
50593
50817
  });
50594
50818
  runtimeRef.current = {
@@ -50824,4 +51048,4 @@ function ScrawlBoard({ documentId, initialDocument, onReady, className, style })
50824
51048
  });
50825
51049
  }
50826
51050
  //#endregion
50827
- export { AddImageCommand, AddNoteCommand, AddStrokesCommand, AddTableCommand, AddTextCommand, AddTimerCommand, BEACON_INSET, BOARD_COLOR, BoardDocument, CURRENT_DOCUMENT_SCHEMA_VERSION, ClusterStore, DefaultBoardChrome, DeleteImageCommand, DeleteNoteCommand, DeleteStrokesCommand, DeleteTableCommand, DeleteTextCommand, DeleteTimerCommand, DocumentRecoveryError, END_TAPER, ERASE_THRESHOLD, EraseCommand, FOG_COLOR, HIGHLIGHT_COLORS, History, IDENTITY, INK_COLORS, InlineEditors, LockItemsCommand, MIN_WIDTH_FACTOR, MultiplayerCursors, NOTE_COLORS, NOTE_DEFAULT_SIZE, NOTE_DEFAULT_Z, NOTE_MAX_Z, NOTE_MIN_Z, NOTE_PEEL_STEP, SDK_DEVELOPMENT_VERSION, SDK_PACKAGE_NAME, STAMPS, STAMP_SIZE, Scrawl, ScrawlBoard, ScrawlCanvas, ScrawlDefaultUI, ScrawlPortal, ScrawlProvider, SpatialIndex, StyleShelf, TABLE_DEFAULT_CELL_HEIGHT, TABLE_DEFAULT_CELL_WIDTH, TABLE_DEFAULT_FONT_SIZE, TEXT_DEFAULT_SIZE, TIMER_DEFAULT_DURATION_MS, TIMER_DEFAULT_SIZE, TIMER_PRESETS_MS, TransformCommand, UpdateImageCommand, UpdateNoteCommand, UpdateTableCommand, UpdateTextCommand, UpdateTimerCommand, apply, applyItemLock, avgScale, canUnlockItem, changeToOps, cloneImage, cloneNote, cloneStroke, cloneTable, cloneText, cloneTimer, createBoardController, createLocalBoard, documentId, documentToSVG, formatTimer, invert, isIdentity, isStampKind, loadDocumentBytes, measureTable, measureTextBlock, migrateDocument, mul, pauseTimer, placePresenceBeacon, resolveScrawlTheme, ribbonEdges, rotationAbout, scalingAbout, scrawlThemePresets, searchBoard, serializeDocument, serializeLock, serializeStroke, setTimerDuration, stampDataUrl, startTimer, strokeId, timerExpired, timerRemaining, toggleTimer, translation, useScrawlController, useScrawlSnapshot, useScrawlTheme, validateScrawlTheme };
51051
+ export { ASSET_CACHE_BYTES_DEFAULT, ASSET_CACHE_BYTES_MAX, ASSET_CACHE_BYTES_MIN, ASSET_EXPORT_MAX_DECODED_MEGAPIXELS, ASSET_EXPORT_MAX_ENCODED_BYTES, ASSET_MAX_CONCURRENT_RESOLUTIONS, ASSET_MAX_DECODED_MEGAPIXELS, ASSET_MAX_DIMENSION_PX, ASSET_MAX_ENCODED_BYTES, ASSET_REF_MAX_BYTES, ASSET_REF_PATTERN, AddImageCommand, AddNoteCommand, AddStrokesCommand, AddTableCommand, AddTextCommand, AddTimerCommand, AssetResolutionError, BEACON_INSET, BoardDocument, CURRENT_DOCUMENT_SCHEMA_VERSION, ClusterStore, DefaultBoardChrome, DeleteImageCommand, DeleteNoteCommand, DeleteStrokesCommand, DeleteTableCommand, DeleteTextCommand, DeleteTimerCommand, DocumentRecoveryError, END_TAPER, ERASE_THRESHOLD, EraseCommand, FOG_COLOR, HIGHLIGHT_COLORS, History, IDENTITY, INK_COLORS, InlineEditors, LockItemsCommand, MIN_WIDTH_FACTOR, MultiplayerCursors, NOTE_COLORS, NOTE_DEFAULT_SIZE, NOTE_DEFAULT_Z, NOTE_MAX_Z, NOTE_MIN_Z, NOTE_PEEL_STEP, SDK_DEVELOPMENT_VERSION, SDK_PACKAGE_NAME, STAMPS, STAMP_SIZE, SUPPORTED_ASSET_MEDIA_TYPES, Scrawl, ScrawlBoard, ScrawlCanvas, ScrawlDefaultUI, ScrawlPortal, ScrawlProvider, SpatialIndex, StyleShelf, TABLE_DEFAULT_CELL_HEIGHT, TABLE_DEFAULT_CELL_WIDTH, TABLE_DEFAULT_FONT_SIZE, TEXT_DEFAULT_SIZE, TIMER_DEFAULT_DURATION_MS, TIMER_DEFAULT_SIZE, TIMER_PRESETS_MS, TransformCommand, UpdateImageCommand, UpdateNoteCommand, UpdateTableCommand, UpdateTextCommand, UpdateTimerCommand, apply, applyItemLock, assetRef, avgScale, canUnlockItem, changeToOps, clampAssetCacheBytes, cloneCustomObject, cloneImage, cloneNote, cloneStroke, cloneTable, cloneText, cloneTimer, createBoardController, createLocalBoard, documentId, documentToSVG, formatTimer, invert, isAssetRef, isIdentity, isStampKind, loadDocumentBytes, measureTable, measureTextBlock, migrateDocument, mul, pauseTimer, placePresenceBeacon, resolveScrawlTheme, ribbonEdges, rotationAbout, scalingAbout, scrawlThemePresets, searchBoard, serializeDocument, serializeLock, serializeStroke, setTimerDuration, stampDataUrl, startTimer, strokeId, timerExpired, timerRemaining, toggleTimer, translation, useScrawlController, useScrawlSnapshot, useScrawlTheme, validateScrawlTheme };