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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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;
@@ -30,7 +30,7 @@ function clampAssetCacheBytes(value) {
30
30
  return Math.min(ASSET_CACHE_BYTES_MAX, Math.max(ASSET_CACHE_BYTES_MIN, Math.floor(value)));
31
31
  }
32
32
  //#endregion
33
- //#region src/core-internal/identifiers.ts
33
+ //#region src/core/document/identifiers.ts
34
34
  function documentId(value) {
35
35
  if (!value) throw new TypeError("DocumentId cannot be empty");
36
36
  return value;
@@ -231,7 +231,7 @@ function validation(message, path) {
231
231
  return new DocumentRecoveryError("DOCUMENT_VALIDATION_FAILED", message, void 0, path);
232
232
  }
233
233
  //#endregion
234
- //#region src/core-internal/affine2d.ts
234
+ //#region src/core/shapes/affine2d.ts
235
235
  var IDENTITY = [
236
236
  1,
237
237
  0,
@@ -315,7 +315,7 @@ function avgScale(m) {
315
315
  return (Math.hypot(m[0], m[1]) + Math.hypot(m[2], m[3])) / 2;
316
316
  }
317
317
  //#endregion
318
- //#region src/core-internal/boardSearch.ts
318
+ //#region src/core/document/boardSearch.ts
319
319
  function hay(s) {
320
320
  return s.toLowerCase();
321
321
  }
@@ -380,7 +380,7 @@ function searchBoard(query, board) {
380
380
  return hits;
381
381
  }
382
382
  //#endregion
383
- //#region src/core-internal/types.ts
383
+ //#region src/core/shapes/types.ts
384
384
  /** Erasure level at which a point counts as fully erased. */
385
385
  var ERASE_THRESHOLD = .95;
386
386
  function cloneStroke(stroke) {
@@ -452,7 +452,7 @@ function cloneImage(img) {
452
452
  return { ...img };
453
453
  }
454
454
  //#endregion
455
- //#region src/core-internal/clusterStore.ts
455
+ //#region src/core/document/clusterStore.ts
456
456
  /** A stroke joins a cluster last written to within this window at full reach. */
457
457
  var RECENT_MS = 8e3;
458
458
  /** Older clusters are still joinable, at half the gap threshold. */
@@ -623,7 +623,7 @@ function clamp$2(v, lo, hi) {
623
623
  return Math.min(hi, Math.max(lo, v));
624
624
  }
625
625
  //#endregion
626
- //#region src/core-internal/extensionTypes.ts
626
+ //#region src/core/shapes/extensionTypes.ts
627
627
  function cloneCustomObject(object) {
628
628
  return {
629
629
  ...object,
@@ -633,7 +633,7 @@ function cloneCustomObject(object) {
633
633
  };
634
634
  }
635
635
  //#endregion
636
- //#region src/core-internal/kitchenTimer.ts
636
+ //#region src/core/shapes/kitchenTimer.ts
637
637
  var TIMER_DEFAULT_DURATION_MS = 300 * 1e3;
638
638
  function cloneTimer(timer) {
639
639
  return { ...timer };
@@ -683,7 +683,7 @@ function formatTimer(ms) {
683
683
  return `${Math.floor(total / 60)}:${(total % 60).toString().padStart(2, "0")}`;
684
684
  }
685
685
  //#endregion
686
- //#region src/core-internal/itemLock.ts
686
+ //#region src/core/shapes/itemLock.ts
687
687
  /**
688
688
  * Apply or clear a lock in place. Unlock always drops the holder so a stale
689
689
  * name cannot linger on an unlocked item.
@@ -720,7 +720,7 @@ function serializeLock(item) {
720
720
  };
721
721
  }
722
722
  //#endregion
723
- //#region src/core-internal/document.ts
723
+ //#region src/core/document/document.ts
724
724
  /**
725
725
  * The one place a live Stroke becomes its wire representation — in
726
726
  * particular, points collapse from {x,y,pressure,erase?} objects into
@@ -1279,7 +1279,7 @@ function computeBBox(stroke) {
1279
1279
  return world;
1280
1280
  }
1281
1281
  //#endregion
1282
- //#region src/core-internal/commands.ts
1282
+ //#region src/core/commands/commands.ts
1283
1283
  /** One drawing action — a marker stroke, or a shape's strokes as one unit. */
1284
1284
  var AddStrokesCommand = class {
1285
1285
  constructor(strokes) {
@@ -1621,7 +1621,7 @@ var LockItemsCommand = class {
1621
1621
  }
1622
1622
  };
1623
1623
  //#endregion
1624
- //#region src/core-internal/history.ts
1624
+ //#region src/core/history/history.ts
1625
1625
  var LIMIT = 200;
1626
1626
  var History = class {
1627
1627
  constructor(doc) {
@@ -1682,7 +1682,7 @@ var History = class {
1682
1682
  }
1683
1683
  };
1684
1684
  //#endregion
1685
- //#region src/core-internal/ops.ts
1685
+ //#region src/core/events/ops.ts
1686
1686
  var ADDED = [
1687
1687
  ["added", "strokes"],
1688
1688
  ["notesAdded", "notes"],
@@ -1736,10 +1736,18 @@ function changeToOps(change, restoring = false) {
1736
1736
  return ops;
1737
1737
  }
1738
1738
  //#endregion
1739
- //#region src/core-internal/ribbonEdges.ts
1739
+ //#region src/core/shapes/ribbonEdges.ts
1740
1740
  var MIN_WIDTH_FACTOR = .35;
1741
1741
  var END_TAPER = .55;
1742
- function ribbonEdges(points, baseWidth) {
1742
+ /**
1743
+ * @param handDrawn Default `true`: ink's organic feel — width follows
1744
+ * per-point pressure, and both ends taper. Pass `false` for a geometric
1745
+ * shape outline, which has no real pressure signal and isn't a pen stroke
1746
+ * with a lift-off: it renders at the exact `baseWidth`, uniformly, with no
1747
+ * taper at its start/end (which, for a closed shape, is the same point —
1748
+ * tapering it would visibly pinch just that one corner).
1749
+ */
1750
+ function ribbonEdges(points, baseWidth, handDrawn = true) {
1743
1751
  const pts = points.length === 1 ? [points[0], {
1744
1752
  ...points[0],
1745
1753
  x: points[0].x + baseWidth * .05
@@ -1754,8 +1762,8 @@ function ribbonEdges(points, baseWidth) {
1754
1762
  const len = Math.hypot(dx, dy) || 1;
1755
1763
  dx /= len;
1756
1764
  dy /= len;
1757
- let width = baseWidth * (MIN_WIDTH_FACTOR + (1 - MIN_WIDTH_FACTOR) * pts[i].pressure);
1758
- if (i === 0 || i === n - 1) width *= END_TAPER;
1765
+ let width = handDrawn ? baseWidth * (MIN_WIDTH_FACTOR + (1 - MIN_WIDTH_FACTOR) * pts[i].pressure) : baseWidth;
1766
+ if (handDrawn && (i === 0 || i === n - 1)) width *= END_TAPER;
1759
1767
  const hw = width / 2;
1760
1768
  out[i] = {
1761
1769
  lx: pts[i].x - dy * hw,
@@ -1768,7 +1776,7 @@ function ribbonEdges(points, baseWidth) {
1768
1776
  return out;
1769
1777
  }
1770
1778
  //#endregion
1771
- //#region src/core-internal/spatialIndex.ts
1779
+ //#region src/core/document/spatialIndex.ts
1772
1780
  var CELL = 8;
1773
1781
  var SpatialIndex = class {
1774
1782
  constructor(doc) {
@@ -1841,7 +1849,7 @@ function cellsOf(box) {
1841
1849
  return keys;
1842
1850
  }
1843
1851
  //#endregion
1844
- //#region src/core-internal/stamps.ts
1852
+ //#region src/core/shapes/stamps.ts
1845
1853
  var STAMPS = [
1846
1854
  {
1847
1855
  kind: "star",
@@ -1889,7 +1897,7 @@ function isStampKind(value) {
1889
1897
  return typeof value === "string" && STAMPS.some((s) => s.kind === value);
1890
1898
  }
1891
1899
  //#endregion
1892
- //#region src/core-internal/svg.ts
1900
+ //#region src/persistence/serialization/svg.ts
1893
1901
  /** Matches the key light used by the board shader and note shadows. */
1894
1902
  var LIGHT = {
1895
1903
  x: -.25,
@@ -2031,7 +2039,7 @@ function strokeToPaths(stroke) {
2031
2039
  y,
2032
2040
  pressure,
2033
2041
  ...erase > 0 ? { erase } : {}
2034
- })), stroke.baseWidth);
2042
+ })), stroke.baseWidth, stroke.tool !== "shape");
2035
2043
  const transform = stroke.matrix ? ` transform="matrix(${fmt(stroke.matrix[0])} ${fmt(-stroke.matrix[1])} ${fmt(-stroke.matrix[2])} ${fmt(stroke.matrix[3])} ${fmt(stroke.matrix[4])} ${fmt(-stroke.matrix[5])})"` : "";
2036
2044
  const baseOpacity = stroke.tool === "highlighter" ? .4 : 1;
2037
2045
  const paths = [];
@@ -2256,7 +2264,7 @@ function fmt(n) {
2256
2264
  return String(Math.round(n * 100) / 100);
2257
2265
  }
2258
2266
  //#endregion
2259
- //#region src/core-internal/sceneGeometry.ts
2267
+ //#region src/core/shapes/sceneGeometry.ts
2260
2268
  var EMPTY_BOUNDS = {
2261
2269
  x: 0,
2262
2270
  y: 0,
@@ -2320,7 +2328,7 @@ function textCorners(x, y, text, fontSize, align, m) {
2320
2328
  return rectCorners(x + (align === "center" ? -width / 2 : align === "end" ? -width : 0), y, width, height, m);
2321
2329
  }
2322
2330
  //#endregion
2323
- //#region src/core-internal/customObjects.ts
2331
+ //#region src/core/shapes/customObjects.ts
2324
2332
  var FALLBACK_LABEL_MAX_LENGTH = 80;
2325
2333
  function toReadonly(object) {
2326
2334
  return object;
@@ -2449,7 +2457,7 @@ function extensionIdFromType(type) {
2449
2457
  return slash === -1 ? type : type.slice(0, slash);
2450
2458
  }
2451
2459
  //#endregion
2452
- //#region src/core-internal/extensionRegistry.ts
2460
+ //#region src/core/shapes/extensionRegistry.ts
2453
2461
  function buildExtensionRegistry(extensions) {
2454
2462
  const list = extensions ?? [];
2455
2463
  const diagnostics = [];
@@ -2546,7 +2554,7 @@ function isWellFormedNamespacedId(id) {
2546
2554
  return isWellFormedId(id) && id.includes("/") && !id.startsWith("/") && !id.endsWith("/");
2547
2555
  }
2548
2556
  //#endregion
2549
- //#region src/core-internal/assetValidation.ts
2557
+ //#region src/persistence/adapters/assetValidation.ts
2550
2558
  function sniffImageMediaType(bytes) {
2551
2559
  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";
2552
2560
  if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
@@ -2633,7 +2641,7 @@ function validateDecodedDimensions(width, height) {
2633
2641
  };
2634
2642
  }
2635
2643
  //#endregion
2636
- //#region src/assetController.ts
2644
+ //#region src/persistence/adapters/assetController.ts
2637
2645
  var FAILURE_COALESCE_MS = 4e3;
2638
2646
  var AssetController = class {
2639
2647
  constructor(resolver, options) {
@@ -2812,7 +2820,7 @@ function toAssetResolutionError(cause, ref) {
2812
2820
  return new AssetResolutionError("unknown", true, cause instanceof Error ? cause.message : String(cause), ref);
2813
2821
  }
2814
2822
  //#endregion
2815
- //#region src/core-internal/assetExport.ts
2823
+ //#region src/persistence/serialization/assetRefCollector.ts
2816
2824
  /** Every unique Asset reference in the document, and which object(s) use it. */
2817
2825
  function collectDocumentAssetRefs(doc, registry) {
2818
2826
  const refs = /* @__PURE__ */ new Map();
@@ -2839,7 +2847,7 @@ function collectSceneAssetRefs(node, objectId, add) {
2839
2847
  if (node.kind === "group") for (const child of node.children) collectSceneAssetRefs(child, objectId, add);
2840
2848
  }
2841
2849
  //#endregion
2842
- //#region src/assetExport.ts
2850
+ //#region src/persistence/serialization/assetExport.ts
2843
2851
  /** Strict mode's rejection — never contains credentials, a fetchable location, or a raw resolver message. */
2844
2852
  var SvgAssetExportError = class extends Error {
2845
2853
  constructor(missingAssets) {
@@ -2895,7 +2903,7 @@ function toDataUri(bytes, mediaType) {
2895
2903
  return `data:${mediaType};base64,${btoa(binary)}`;
2896
2904
  }
2897
2905
  //#endregion
2898
- //#region src/assetIngestion.ts
2906
+ //#region src/persistence/adapters/assetIngestion.ts
2899
2907
  async function decodeDimensionsWithImageBitmap(bytes, mediaType) {
2900
2908
  const blob = new Blob([bytes], { type: mediaType });
2901
2909
  let bitmap;
@@ -35541,7 +35549,7 @@ var WebGLRenderer = class {
35541
35549
  }
35542
35550
  };
35543
35551
  //#endregion
35544
- //#region ../../src/scrawl/data/auditEvents.ts
35552
+ //#region src/core/events/auditEvents.ts
35545
35553
  var BY_LABEL = {
35546
35554
  draw: {
35547
35555
  action: "create",
@@ -35661,7 +35669,7 @@ function lowerFirst(s) {
35661
35669
  return s.charAt(0).toLowerCase() + s.slice(1);
35662
35670
  }
35663
35671
  //#endregion
35664
- //#region ../../src/scrawl/assets/AssetTextureCache.ts
35672
+ //#region src/renderer/shapes/AssetTextureCache.ts
35665
35673
  var AssetTextureCache = class {
35666
35674
  constructor(assetController) {
35667
35675
  this.assetController = assetController;
@@ -35752,7 +35760,7 @@ function toDecodeError(cause, ref) {
35752
35760
  return new AssetResolutionError("decode-failed", false, cause instanceof Error ? cause.message : String(cause), ref);
35753
35761
  }
35754
35762
  //#endregion
35755
- //#region ../../src/scrawl/engine/raycast.ts
35763
+ //#region src/renderer/canvas/raycast.ts
35756
35764
  var origin$1 = new Vector3();
35757
35765
  var target = new Vector3();
35758
35766
  function pointerToBoard(clientX, clientY, canvas, camera) {
@@ -35783,7 +35791,7 @@ function pointerToPlane(clientX, clientY, canvas, camera, z) {
35783
35791
  };
35784
35792
  }
35785
35793
  //#endregion
35786
- //#region ../../src/scrawl/engine/fog.ts
35794
+ //#region src/renderer/canvas/fog.ts
35787
35795
  function createFogUniforms(color) {
35788
35796
  return {
35789
35797
  uFogColor: { value: new Color(color) },
@@ -35804,18 +35812,24 @@ var FOG_GLSL = `
35804
35812
  }
35805
35813
  `;
35806
35814
  //#endregion
35807
- //#region ../../src/scrawl/engine/boardSurface.ts
35815
+ //#region src/renderer/canvas/boardSurface.ts
35808
35816
  var GRID_MODE_CODE = {
35809
35817
  none: 0,
35810
35818
  line: 1,
35811
35819
  dot: 2
35812
35820
  };
35821
+ var TEXTURE_CODE = {
35822
+ flat: 0,
35823
+ textured: 1
35824
+ };
35813
35825
  /** Used when a caller doesn't supply one (e.g. ScrawlEngine constructed without a Host theme). */
35814
35826
  var DEFAULT_BOARD_SURFACE_THEME = {
35815
35827
  color: "#f5f5f3",
35816
35828
  gridMode: "none",
35817
35829
  gridColor: "#deded8",
35818
- gridSpacing: 5
35830
+ gridSpacing: 5,
35831
+ gridLineWidth: 1,
35832
+ surfaceTexture: "flat"
35819
35833
  };
35820
35834
  /** Grid alpha fades out below this on-screen spacing (px) — otherwise it aliases into a solid wash when zoomed out. */
35821
35835
  var GRID_FADE_MIN_PX = 3;
@@ -35836,8 +35850,10 @@ var FRAGMENT = `
35836
35850
  uniform float uGridMode;
35837
35851
  uniform vec3 uGridColor;
35838
35852
  uniform float uGridSpacing;
35839
- uniform float uGridLineWorld;
35853
+ uniform float uWorldPerPixel;
35854
+ uniform float uGridLineWidthPx;
35840
35855
  uniform float uGridFade;
35856
+ uniform float uSurfaceTexture;
35841
35857
  varying vec3 vWorldPos;
35842
35858
 
35843
35859
  float hash(vec2 p) {
@@ -35857,37 +35873,45 @@ var FRAGMENT = `
35857
35873
  ${FOG_GLSL}
35858
35874
 
35859
35875
  // Distance from p's nearest axis-aligned grid line, in world units.
35876
+ // A crisp, ~uGridLineWidthPx-wide line: full alpha to its half-width, a
35877
+ // ~1px anti-aliased falloff beyond that — not a wide, soft-edged band.
35860
35878
  float gridLineAlpha(vec2 p) {
35861
35879
  vec2 cell = mod(p, uGridSpacing);
35862
35880
  vec2 distToLine = min(cell, uGridSpacing - cell);
35863
- vec2 lineAlpha2 = 1.0 - smoothstep(uGridLineWorld, uGridLineWorld * 2.0, distToLine);
35881
+ float halfWidth = 0.5 * uGridLineWidthPx * uWorldPerPixel;
35882
+ float aa = uWorldPerPixel;
35883
+ vec2 lineAlpha2 = 1.0 - smoothstep(halfWidth - aa * 0.5, halfWidth + aa * 0.5, distToLine);
35864
35884
  return max(lineAlpha2.x, lineAlpha2.y);
35865
35885
  }
35866
35886
 
35867
- // Distance from p's nearest grid intersection, in world units.
35887
+ // Distance from p's nearest grid intersection, in world units. Dot radius
35888
+ // matches the configured line width in pixels, same anti-aliasing.
35868
35889
  float gridDotAlpha(vec2 p) {
35869
35890
  vec2 cell = mod(p, uGridSpacing) - uGridSpacing * 0.5;
35870
35891
  float dist = length(cell);
35871
- float radius = uGridLineWorld * 1.5;
35872
- return 1.0 - smoothstep(radius, radius * 2.0, dist);
35892
+ float radius = uGridLineWidthPx * uWorldPerPixel;
35893
+ float aa = uWorldPerPixel;
35894
+ return 1.0 - smoothstep(radius - aa * 0.5, radius + aa * 0.5, dist);
35873
35895
  }
35874
35896
 
35875
35897
  void main() {
35876
35898
  vec3 col = uColor;
35877
35899
 
35878
- // Melamine micro-texture: two octaves of value noise, +-1.75% brightness.
35879
- float n = vnoise(vWorldPos.xy * 2.3) * 0.6 + vnoise(vWorldPos.xy * 9.7) * 0.4;
35880
- col *= 1.0 + (n - 0.5) * 0.035;
35900
+ if (uSurfaceTexture > 0.5) {
35901
+ // Melamine micro-texture: two octaves of value noise, +-1.75% brightness.
35902
+ float n = vnoise(vWorldPos.xy * 2.3) * 0.6 + vnoise(vWorldPos.xy * 9.7) * 0.4;
35903
+ col *= 1.0 + (n - 0.5) * 0.035;
35881
35904
 
35882
- // Satin sheen: Fresnel rim + a specular band from the key light. Both are
35883
- // ~zero in the straight-on working view and bloom as the camera tilts.
35884
- vec3 V = normalize(cameraPosition - vWorldPos);
35885
- vec3 N = vec3(0.0, 0.0, 1.0);
35886
- float fres = pow(1.0 - max(dot(N, V), 0.0), 3.0);
35887
- vec3 L = normalize(vec3(-0.25, 0.4, 0.88));
35888
- vec3 H = normalize(L + V);
35889
- float spec = pow(max(dot(N, H), 0.0), 90.0);
35890
- col += (fres * 0.18 + spec * 0.06) * vec3(1.0);
35905
+ // Satin sheen: Fresnel rim + a specular band from the key light. Both
35906
+ // are ~zero in the straight-on working view and bloom as the camera tilts.
35907
+ vec3 V = normalize(cameraPosition - vWorldPos);
35908
+ vec3 N = vec3(0.0, 0.0, 1.0);
35909
+ float fres = pow(1.0 - max(dot(N, V), 0.0), 3.0);
35910
+ vec3 L = normalize(vec3(-0.25, 0.4, 0.88));
35911
+ vec3 H = normalize(L + V);
35912
+ float spec = pow(max(dot(N, H), 0.0), 90.0);
35913
+ col += (fres * 0.18 + spec * 0.06) * vec3(1.0);
35914
+ }
35891
35915
 
35892
35916
  if (uGridMode > 0.5 && uGridFade > 0.001) {
35893
35917
  float gridAlpha = (uGridMode < 1.5 ? gridLineAlpha(vWorldPos.xy) : gridDotAlpha(vWorldPos.xy)) * uGridFade;
@@ -35907,8 +35931,10 @@ function createBoardSurface(fog, theme) {
35907
35931
  uGridMode: { value: GRID_MODE_CODE[theme.gridMode] },
35908
35932
  uGridColor: { value: new Color(theme.gridColor) },
35909
35933
  uGridSpacing: { value: Math.max(.001, theme.gridSpacing) },
35910
- uGridLineWorld: { value: .05 },
35934
+ uGridLineWidthPx: { value: Math.max(.1, theme.gridLineWidth) },
35935
+ uWorldPerPixel: { value: .05 },
35911
35936
  uGridFade: { value: 0 },
35937
+ uSurfaceTexture: { value: TEXTURE_CODE[theme.surfaceTexture] },
35912
35938
  ...fog
35913
35939
  }
35914
35940
  });
@@ -35916,13 +35942,15 @@ function createBoardSurface(fog, theme) {
35916
35942
  mesh.name = "board-surface";
35917
35943
  return mesh;
35918
35944
  }
35919
- /** Applies a theme change (initial or live) — everything except the per-frame fade/line-width. */
35945
+ /** Applies a theme change (initial or live) — everything except the per-frame fade/pixel-ratio. */
35920
35946
  function setBoardSurfaceTheme(mesh, theme) {
35921
35947
  const uniforms = mesh.material.uniforms;
35922
35948
  uniforms.uColor.value.set(theme.color);
35923
35949
  uniforms.uGridMode.value = GRID_MODE_CODE[theme.gridMode];
35924
35950
  uniforms.uGridColor.value.set(theme.gridColor);
35925
35951
  uniforms.uGridSpacing.value = Math.max(.001, theme.gridSpacing);
35952
+ uniforms.uGridLineWidthPx.value = Math.max(.1, theme.gridLineWidth);
35953
+ uniforms.uSurfaceTexture.value = TEXTURE_CODE[theme.surfaceTexture];
35926
35954
  }
35927
35955
  /** Keep the plane centered under the view, larger than the fog horizon, and the grid's on-screen weight/fade current. */
35928
35956
  function updateBoardSurface(mesh, centerX, centerY, fogFar, worldPerPixel) {
@@ -35930,7 +35958,7 @@ function updateBoardSurface(mesh, centerX, centerY, fogFar, worldPerPixel) {
35930
35958
  const size = fogFar * 2.5;
35931
35959
  mesh.scale.set(size, size, 1);
35932
35960
  const uniforms = mesh.material.uniforms;
35933
- uniforms.uGridLineWorld.value = worldPerPixel;
35961
+ uniforms.uWorldPerPixel.value = worldPerPixel;
35934
35962
  const t = (uniforms.uGridSpacing.value / Math.max(1e-6, worldPerPixel) - GRID_FADE_MIN_PX) / (GRID_FADE_MAX_PX - GRID_FADE_MIN_PX);
35935
35963
  uniforms.uGridFade.value = MathUtils.clamp(t, 0, 1);
35936
35964
  }
@@ -36103,7 +36131,7 @@ var CameraRig = class {
36103
36131
  }
36104
36132
  };
36105
36133
  //#endregion
36106
- //#region ../../src/scrawl/engine/scene.ts
36134
+ //#region src/renderer/canvas/scene.ts
36107
36135
  var SceneRig = class {
36108
36136
  constructor(canvas, boardTheme) {
36109
36137
  this.renderer = new WebGLRenderer({
@@ -36150,7 +36178,7 @@ var SceneRig = class {
36150
36178
  }
36151
36179
  };
36152
36180
  //#endregion
36153
- //#region ../../src/scrawl/ink/inkMaterial.ts
36181
+ //#region src/renderer/shapes/inkMaterial.ts
36154
36182
  var VERTEX = `
36155
36183
  attribute float aAlpha;
36156
36184
  varying vec2 vUv;
@@ -36281,15 +36309,15 @@ var InkMaterials = class {
36281
36309
  }
36282
36310
  };
36283
36311
  //#endregion
36284
- //#region ../../src/scrawl/ink/ribbonGeometry.ts
36312
+ //#region src/renderer/shapes/ribbonGeometry.ts
36285
36313
  /** Ink floats a hair above the board to avoid z-fighting with the surface. */
36286
36314
  var INK_Z = .02;
36287
36315
  /** Highlighter always renders under opaque ink (build prompt §4.1). */
36288
36316
  var HIGHLIGHT_Z = .016;
36289
36317
  /** Ghosts sit under everything so live strokes always read on top. */
36290
36318
  var GHOST_Z = .012;
36291
- function buildRibbonGeometry(points, baseWidth, z = INK_Z) {
36292
- const edges = ribbonEdges(points, baseWidth);
36319
+ function buildRibbonGeometry(points, baseWidth, z = INK_Z, handDrawn = true) {
36320
+ const edges = ribbonEdges(points, baseWidth, handDrawn);
36293
36321
  const n = edges.length;
36294
36322
  const positions = new Float32Array(n * 2 * 3);
36295
36323
  const uvs = new Float32Array(n * 2 * 2);
@@ -36332,8 +36360,10 @@ function buildRibbonGeometry(points, baseWidth, z = INK_Z) {
36332
36360
  return geometry;
36333
36361
  }
36334
36362
  //#endregion
36335
- //#region ../../src/scrawl/ink/strokeRenderer.ts
36363
+ //#region src/renderer/shapes/strokeRenderer.ts
36336
36364
  var strokeZ = (stroke) => stroke.tool === "highlighter" ? HIGHLIGHT_Z : INK_Z;
36365
+ /** Shapes (rect/ellipse/line/arrow/...) are geometric outlines, not pressure-sensitive ink. */
36366
+ var handDrawn = (stroke) => stroke.tool !== "shape";
36337
36367
  /**
36338
36368
  * Derives ink meshes from the document by subscription — the document never
36339
36369
  * knows the renderer exists. Also hosts session-only ghosts (decision Q7)
@@ -36358,12 +36388,12 @@ var StrokeRenderer = class {
36358
36388
  this.ghostGroup.visible = visible;
36359
36389
  }
36360
36390
  /** Leave a smeared residue of an erased span (never persisted). */
36361
- addGhost(points, color, baseWidth) {
36391
+ addGhost(points, color, baseWidth, handDrawn = true) {
36362
36392
  if (points.length < 2) return;
36363
36393
  const mesh = new Mesh(buildRibbonGeometry(points.map((p) => ({
36364
36394
  ...p,
36365
36395
  erase: 0
36366
- })), baseWidth * 1.7, GHOST_Z), this.materials.ghost(color));
36396
+ })), baseWidth * 1.7, GHOST_Z, handDrawn), this.materials.ghost(color));
36367
36397
  this.ghostGroup.add(mesh);
36368
36398
  }
36369
36399
  /**
@@ -36375,7 +36405,7 @@ var StrokeRenderer = class {
36375
36405
  if (this.meshes.has(stroke.id) || this.retiredLive.has(stroke.id) || stroke.points.length < 2) return;
36376
36406
  this.liveUser.set(stroke.id, userId);
36377
36407
  const existing = this.liveMeshes.get(stroke.id);
36378
- const geometry = buildRibbonGeometry(stroke.points, stroke.baseWidth, strokeZ(stroke));
36408
+ const geometry = buildRibbonGeometry(stroke.points, stroke.baseWidth, strokeZ(stroke), handDrawn(stroke));
36379
36409
  if (existing) {
36380
36410
  existing.geometry.dispose();
36381
36411
  existing.geometry = geometry;
@@ -36435,7 +36465,7 @@ var StrokeRenderer = class {
36435
36465
  const mesh = this.meshes.get(stroke.id);
36436
36466
  if (mesh) {
36437
36467
  mesh.geometry.dispose();
36438
- mesh.geometry = buildRibbonGeometry(stroke.points, stroke.baseWidth, strokeZ(stroke));
36468
+ mesh.geometry = buildRibbonGeometry(stroke.points, stroke.baseWidth, strokeZ(stroke), handDrawn(stroke));
36439
36469
  }
36440
36470
  }
36441
36471
  for (const stroke of change.transformed) {
@@ -36444,7 +36474,7 @@ var StrokeRenderer = class {
36444
36474
  }
36445
36475
  }
36446
36476
  create(stroke) {
36447
- const mesh = new Mesh(buildRibbonGeometry(stroke.points, stroke.baseWidth, strokeZ(stroke)), stroke.tool === "highlighter" ? this.materials.highlight(stroke.color) : this.materials.get(stroke.color));
36477
+ const mesh = new Mesh(buildRibbonGeometry(stroke.points, stroke.baseWidth, strokeZ(stroke), handDrawn(stroke)), stroke.tool === "highlighter" ? this.materials.highlight(stroke.color) : this.materials.get(stroke.color));
36448
36478
  mesh.matrixAutoUpdate = false;
36449
36479
  syncMatrix(mesh, stroke);
36450
36480
  this.meshes.set(stroke.id, mesh);
@@ -36465,7 +36495,7 @@ function syncMatrix(mesh, stroke) {
36465
36495
  mesh.matrixWorldNeedsUpdate = true;
36466
36496
  }
36467
36497
  //#endregion
36468
- //#region ../../src/scrawl/scene-objects/markerProp.ts
36498
+ //#region src/renderer/shapes/markerProp.ts
36469
36499
  var BODY_LENGTH = 11;
36470
36500
  var BODY_RADIUS = .85;
36471
36501
  var TIP_LENGTH = 1.6;
@@ -36561,7 +36591,7 @@ var MarkerProp = class {
36561
36591
  }
36562
36592
  };
36563
36593
  //#endregion
36564
- //#region ../../src/scrawl/scene-objects/noteGeometry.ts
36594
+ //#region src/renderer/shapes/noteGeometry.ts
36565
36595
  /**
36566
36596
  * Height of the sheet above the board at (u, v) ∈ [0,1]².
36567
36597
  * v = 1 is the adhesive edge (always 0); u = 1 is the peel corner side.
@@ -36605,7 +36635,7 @@ function applyCurl(geometry, curl, peel) {
36605
36635
  color.needsUpdate = true;
36606
36636
  }
36607
36637
  //#endregion
36608
- //#region ../../src/scrawl/scene-objects/stickyNotes.ts
36638
+ //#region src/renderer/shapes/stickyNotes.ts
36609
36639
  var TEXTURE_SIZE = 512;
36610
36640
  var FOCUS_COLOR$4 = 2450411;
36611
36641
  var CURL = .3;
@@ -43181,7 +43211,7 @@ SYNCABLE_PROPS.forEach((prop) => {
43181
43211
  new Box3();
43182
43212
  new Color();
43183
43213
  //#endregion
43184
- //#region ../../src/scrawl/scene-objects/tables.ts
43214
+ //#region src/renderer/shapes/tables.ts
43185
43215
  var FONT_URL$2 = "/fonts/inter-medium.woff";
43186
43216
  var TABLE_Z = .012;
43187
43217
  var HEADER_Z = .014;
@@ -43530,7 +43560,7 @@ var TablesRenderer = class {
43530
43560
  }
43531
43561
  };
43532
43562
  //#endregion
43533
- //#region ../../src/scrawl/scene-objects/textBlocks.ts
43563
+ //#region src/renderer/text/textBlocks.ts
43534
43564
  var FONT_URL$1 = "/fonts/inter-medium.woff";
43535
43565
  var TEXT_Z = .02;
43536
43566
  var FOCUS_COLOR$2 = 2450411;
@@ -43634,7 +43664,7 @@ var TextBlocksRenderer = class {
43634
43664
  }
43635
43665
  };
43636
43666
  //#endregion
43637
- //#region ../../src/scrawl/scene-objects/comments.ts
43667
+ //#region src/renderer/shapes/comments.ts
43638
43668
  var PIN_Z = .04;
43639
43669
  var PIN_WIDTH = 3.6;
43640
43670
  var PIN_HEIGHT = 3.6;
@@ -43802,7 +43832,7 @@ function drawBubblePath(ctx, x, y, w, h, r, tailX, tailY, tailW) {
43802
43832
  ctx.closePath();
43803
43833
  }
43804
43834
  //#endregion
43805
- //#region ../../src/scrawl/scene-objects/images.ts
43835
+ //#region src/renderer/shapes/images.ts
43806
43836
  var IMAGE_Z = .015;
43807
43837
  var FRAME_BORDER = .4;
43808
43838
  var FOCUS_COLOR$1 = 2450411;
@@ -44012,7 +44042,7 @@ function applyPlaceholderTexture(visual, label) {
44012
44042
  visual.imageMesh.material.needsUpdate = true;
44013
44043
  }
44014
44044
  //#endregion
44015
- //#region ../../src/scrawl/scene-objects/kitchenTimers.ts
44045
+ //#region src/renderer/shapes/kitchenTimers.ts
44016
44046
  var FACE_SIZE = 256;
44017
44047
  var BODY_COLOR = 14753096;
44018
44048
  var KNOB_COLOR = 16448250;
@@ -44210,7 +44240,7 @@ function renderTimerFace(timer, canvas, now = Date.now()) {
44210
44240
  return c;
44211
44241
  }
44212
44242
  //#endregion
44213
- //#region ../../src/scrawl/scene-objects/customObjects.ts
44243
+ //#region src/renderer/shapes/customObjects.ts
44214
44244
  var FONT_URL = "/fonts/inter-medium.woff";
44215
44245
  var CUSTOM_Z = .02;
44216
44246
  var FOCUS_COLOR = 2450411;
@@ -44572,7 +44602,7 @@ function disposeObject3D(obj) {
44572
44602
  });
44573
44603
  }
44574
44604
  //#endregion
44575
- //#region ../../src/scrawl/tools/hitTest.ts
44605
+ //#region src/interaction/pointer/hitTest.ts
44576
44606
  /** Topmost (most recently added) stroke under the point, or null. */
44577
44607
  function hitStroke(doc, index, p, slop) {
44578
44608
  const candidates = index.query(p.x - slop, p.y - slop, p.x + slop, p.y + slop);
@@ -44583,9 +44613,10 @@ function hitStroke(doc, index, p, slop) {
44583
44613
  const scale = m ? avgScale(m) : 1;
44584
44614
  let prev = null;
44585
44615
  let prevWidth = 0;
44616
+ const handDrawn = stroke.tool !== "shape";
44586
44617
  for (const point of stroke.points) {
44587
44618
  const world = m ? apply(m, point) : point;
44588
- const width = stroke.baseWidth * (MIN_WIDTH_FACTOR + (1 - MIN_WIDTH_FACTOR) * point.pressure) * scale;
44619
+ const width = (handDrawn ? stroke.baseWidth * (MIN_WIDTH_FACTOR + (1 - MIN_WIDTH_FACTOR) * point.pressure) : stroke.baseWidth) * scale;
44589
44620
  if (prev) {
44590
44621
  const reach = Math.max(width, prevWidth) / 2 + slop;
44591
44622
  if (distToSegmentSq(p, prev, world) <= reach * reach) {
@@ -44613,7 +44644,7 @@ function distToSegmentSq(p, a, b) {
44613
44644
  return dx * dx + dy * dy;
44614
44645
  }
44615
44646
  //#endregion
44616
- //#region ../../src/scrawl/tools/eraserTool.ts
44647
+ //#region src/interaction/tools/eraserTool.ts
44617
44648
  var DECAY_PER_PASS = .4;
44618
44649
  /** A point can take another decay pass after this long — rubbing works. */
44619
44650
  var REARM_MS = 250;
@@ -44719,7 +44750,7 @@ var EraserTool = class {
44719
44750
  ...p,
44720
44751
  ...apply(m, p)
44721
44752
  })) : run.points;
44722
- this.ctx.renderer.addGhost(world, current.color, current.baseWidth * (m ? avgScale(m) : 1));
44753
+ this.ctx.renderer.addGhost(world, current.color, current.baseWidth * (m ? avgScale(m) : 1), current.tool !== "shape");
44723
44754
  }
44724
44755
  }
44725
44756
  doc.removeStrokes(before.map((s) => s.id));
@@ -44741,7 +44772,7 @@ function partitionByErasure(points) {
44741
44772
  return runs;
44742
44773
  }
44743
44774
  //#endregion
44744
- //#region ../../src/scrawl/tools/noteDrag.ts
44775
+ //#region src/interaction/tools/noteDrag.ts
44745
44776
  /**
44746
44777
  * Drag the focused note in X/Y. The grab is resolved on the note's own
44747
44778
  * z-plane, not the board, so the note doesn't jump under a tilted ray.
@@ -44808,7 +44839,7 @@ var NoteDragState = class {
44808
44839
  }
44809
44840
  };
44810
44841
  //#endregion
44811
- //#region ../../src/scrawl/tools/imageDrag.ts
44842
+ //#region src/interaction/tools/imageDrag.ts
44812
44843
  /** Drag the focused image in X/Y; one command per drag. */
44813
44844
  var ImageDragState = class {
44814
44845
  constructor(ctx) {
@@ -44860,7 +44891,7 @@ var ImageDragState = class {
44860
44891
  }
44861
44892
  };
44862
44893
  //#endregion
44863
- //#region ../../src/scrawl/tools/timerDrag.ts
44894
+ //#region src/interaction/tools/timerDrag.ts
44864
44895
  /** Drag the focused timer in X/Y; one command per drag. */
44865
44896
  var TimerDragState = class {
44866
44897
  constructor(ctx) {
@@ -44912,7 +44943,7 @@ var TimerDragState = class {
44912
44943
  }
44913
44944
  };
44914
44945
  //#endregion
44915
- //#region ../../src/scrawl/tools/shapes.ts
44946
+ //#region src/interaction/tools/shapeGeometry.ts
44916
44947
  /** Uniform synthetic pressure; the ink shader supplies the hand-drawn feel. */
44917
44948
  var PRESSURE = .7;
44918
44949
  var MIN_ELLIPSE_SEGMENTS = 24;
@@ -44952,10 +44983,7 @@ function rect(a, b, step) {
44952
44983
  a
44953
44984
  ];
44954
44985
  const out = [];
44955
- for (let i = 0; i < corners.length - 1; i++) {
44956
- const edge = sampleSegment(corners[i], corners[i + 1], step);
44957
- out.push(...i === 0 ? edge : edge.slice(1));
44958
- }
44986
+ for (let i = 0; i < corners.length - 1; i++) out.push(...sampleSegment(corners[i], corners[i + 1], step));
44959
44987
  return out;
44960
44988
  }
44961
44989
  function ellipse(a, b, step) {
@@ -45026,10 +45054,7 @@ function polygon(sides, a, b, step, startAngle = -Math.PI / 2) {
45026
45054
  }
45027
45055
  vertices.push(vertices[0]);
45028
45056
  const out = [];
45029
- for (let i = 0; i < vertices.length - 1; i++) {
45030
- const edge = sampleSegment(vertices[i], vertices[i + 1], step);
45031
- out.push(...i === 0 ? edge : edge.slice(1));
45032
- }
45057
+ for (let i = 0; i < vertices.length - 1; i++) out.push(...sampleSegment(vertices[i], vertices[i + 1], step));
45033
45058
  return out;
45034
45059
  }
45035
45060
  function star(points, a, b, step) {
@@ -45053,10 +45078,7 @@ function star(points, a, b, step) {
45053
45078
  }
45054
45079
  vertices.push(vertices[0]);
45055
45080
  const out = [];
45056
- for (let i = 0; i < vertices.length - 1; i++) {
45057
- const edge = sampleSegment(vertices[i], vertices[i + 1], step);
45058
- out.push(...i === 0 ? edge : edge.slice(1));
45059
- }
45081
+ for (let i = 0; i < vertices.length - 1; i++) out.push(...sampleSegment(vertices[i], vertices[i + 1], step));
45060
45082
  return out;
45061
45083
  }
45062
45084
  function heart(a, b, step) {
@@ -45101,7 +45123,7 @@ function snappedEnd(a, b) {
45101
45123
  };
45102
45124
  }
45103
45125
  //#endregion
45104
- //#region ../../src/scrawl/tools/shapeTool.ts
45126
+ //#region src/interaction/tools/shapeTool.ts
45105
45127
  var MIN_DRAG_PX$1 = 4;
45106
45128
  /** Drag out a shape; live preview through the real ink material. */
45107
45129
  var ShapeTool = class {
@@ -45132,7 +45154,8 @@ var ShapeTool = class {
45132
45154
  const strokes = this.polylines.map((points) => ({
45133
45155
  id: crypto.randomUUID(),
45134
45156
  color: this.ctx.inkColor(),
45135
- baseWidth: this.ctx.baseWidth(),
45157
+ baseWidth: this.ctx.shapeStrokeWidth(),
45158
+ tool: "shape",
45136
45159
  points
45137
45160
  }));
45138
45161
  this.ctx.clusters.assign(strokes[0]);
@@ -45166,13 +45189,13 @@ var ShapeTool = class {
45166
45189
  this.previews[i].visible = polyline !== void 0;
45167
45190
  if (polyline) {
45168
45191
  this.previews[i].geometry.dispose();
45169
- this.previews[i].geometry = buildRibbonGeometry(polyline, this.ctx.baseWidth());
45192
+ this.previews[i].geometry = buildRibbonGeometry(polyline, this.ctx.shapeStrokeWidth(), void 0, false);
45170
45193
  }
45171
45194
  }
45172
45195
  }
45173
45196
  };
45174
45197
  //#endregion
45175
- //#region ../../src/scrawl/tools/tableDrag.ts
45198
+ //#region src/interaction/tools/tableDrag.ts
45176
45199
  function getHandleCursor(handle) {
45177
45200
  switch (handle) {
45178
45201
  case "nw":
@@ -45292,7 +45315,7 @@ var TableDragState = class {
45292
45315
  }
45293
45316
  };
45294
45317
  //#endregion
45295
- //#region ../../src/scrawl/tools/tableTool.ts
45318
+ //#region src/interaction/tools/tableTool.ts
45296
45319
  var MIN_DRAG_PX = 6;
45297
45320
  var TableTool = class {
45298
45321
  constructor(ctx) {
@@ -45423,7 +45446,7 @@ var TableTool = class {
45423
45446
  }
45424
45447
  };
45425
45448
  //#endregion
45426
- //#region ../../src/scrawl/tools/commentTool.ts
45449
+ //#region src/interaction/tools/commentTool.ts
45427
45450
  /** Click on the board to drop a new comment pin or open an existing one. */
45428
45451
  var CommentTool = class {
45429
45452
  constructor(ctx) {
@@ -45458,7 +45481,7 @@ var CommentTool = class {
45458
45481
  }
45459
45482
  };
45460
45483
  //#endregion
45461
- //#region ../../src/scrawl/tools/textDrag.ts
45484
+ //#region src/interaction/tools/textDrag.ts
45462
45485
  /** Drag the focused text block in X/Y; one command per drag. */
45463
45486
  var TextDragState = class {
45464
45487
  constructor(ctx) {
@@ -45510,7 +45533,7 @@ var TextDragState = class {
45510
45533
  }
45511
45534
  };
45512
45535
  //#endregion
45513
- //#region ../../src/scrawl/tools/fsm.ts
45536
+ //#region src/interaction/tools/fsm.ts
45514
45537
  var ToolStateMachine = class {
45515
45538
  constructor() {
45516
45539
  this.states = /* @__PURE__ */ new Map();
@@ -45712,7 +45735,7 @@ var IdleState = class {
45712
45735
  }
45713
45736
  };
45714
45737
  //#endregion
45715
- //#region ../../src/scrawl/tools/gizmo.ts
45738
+ //#region src/renderer/selection/gizmo.ts
45716
45739
  var GIZMO_Z = .05;
45717
45740
  var HANDLE_PX = 9;
45718
45741
  var HANDLE_HIT_PX = 12;
@@ -45943,7 +45966,7 @@ function boxesEqual(a, b) {
45943
45966
  return a.minX === b.minX && a.minY === b.minY && a.maxX === b.maxX && a.maxY === b.maxY;
45944
45967
  }
45945
45968
  //#endregion
45946
- //#region ../../src/scrawl/ink/pressure.ts
45969
+ //#region src/interaction/tools/pressure.ts
45947
45970
  var SIM_MAX = 1;
45948
45971
  var SIM_MIN = .3;
45949
45972
  var SMOOTHING = .25;
@@ -45965,7 +45988,7 @@ function clamp(v, lo, hi) {
45965
45988
  return Math.min(hi, Math.max(lo, v));
45966
45989
  }
45967
45990
  //#endregion
45968
- //#region ../../src/scrawl/ink/strokeBuilder.ts
45991
+ //#region src/interaction/tools/strokeBuilder.ts
45969
45992
  var StrokeBuilder = class {
45970
45993
  constructor() {
45971
45994
  this.points = [];
@@ -46015,7 +46038,7 @@ function catmullRom(v0, v1, v2, v3, t) {
46015
46038
  return .5 * (2 * v1 + (-v0 + v2) * t + (2 * v0 - 5 * v1 + 4 * v2 - v3) * t2 + (-v0 + 3 * v1 - 3 * v2 + v3) * t3);
46016
46039
  }
46017
46040
  //#endregion
46018
- //#region ../../src/scrawl/tools/markerTool.ts
46041
+ //#region src/interaction/tools/markerTool.ts
46019
46042
  /** Sample spacing while drawing, in screen pixels. */
46020
46043
  var MIN_SAMPLE_PX = 1.5;
46021
46044
  /** A highlighter is a chisel tip: near-constant width, little pressure play. */
@@ -46118,7 +46141,7 @@ var MarkerTool = class {
46118
46141
  }
46119
46142
  };
46120
46143
  //#endregion
46121
- //#region ../../src/scrawl/tools/orbitTool.ts
46144
+ //#region src/interaction/tools/orbitTool.ts
46122
46145
  /**
46123
46146
  * Orbit-preview (build prompt §4.2): alt-drag tilts the camera off-axis to
46124
46147
  * read the board's material and depth; releasing eases back to the working
@@ -46153,7 +46176,7 @@ var OrbitTool = class {
46153
46176
  }
46154
46177
  };
46155
46178
  //#endregion
46156
- //#region ../../src/scrawl/tools/panTool.ts
46179
+ //#region src/interaction/tools/panTool.ts
46157
46180
  /** Grab-pan: content follows the pointer. */
46158
46181
  var PanTool = class {
46159
46182
  constructor(ctx) {
@@ -46183,7 +46206,7 @@ var PanTool = class {
46183
46206
  }
46184
46207
  };
46185
46208
  //#endregion
46186
- //#region ../../src/scrawl/tools/selection.ts
46209
+ //#region src/renderer/selection/selection.ts
46187
46210
  /** The set of selected stroke ids. Drops ids the document no longer has. */
46188
46211
  var SelectionManager = class {
46189
46212
  constructor(doc) {
@@ -46234,7 +46257,7 @@ var SelectionManager = class {
46234
46257
  }
46235
46258
  };
46236
46259
  //#endregion
46237
- //#region ../../src/scrawl/tools/selectTool.ts
46260
+ //#region src/interaction/tools/selectTool.ts
46238
46261
  var HIT_SLOP_PX = 4;
46239
46262
  var CLICK_TOLERANCE_PX = 3;
46240
46263
  var MIN_SCALE = .05;
@@ -46450,7 +46473,7 @@ function safeRatio(numerator, denominator) {
46450
46473
  return raw;
46451
46474
  }
46452
46475
  //#endregion
46453
- //#region ../../src/scrawl/tools/customTool.ts
46476
+ //#region src/interaction/tools/customTool.ts
46454
46477
  function extensionIdFromToolId(toolId) {
46455
46478
  const slash = toolId.indexOf("/");
46456
46479
  return slash === -1 ? toolId : toolId.slice(0, slash);
@@ -46565,12 +46588,19 @@ var ExtensionCommandBatch = class {
46565
46588
  }
46566
46589
  };
46567
46590
  //#endregion
46568
- //#region ../../src/scrawl/ScrawlEngine.ts
46591
+ //#region src/composition/ScrawlEngine.ts
46569
46592
  /** Marker stroke width in board units (~1% of the default view height). */
46570
46593
  var BASE_WIDTH = .5;
46571
46594
  /** Highlighter chisel width — wide enough to cover a line of handwriting. */
46572
46595
  var HIGHLIGHT_WIDTH = 2.2;
46573
46596
  /**
46597
+ * Shape (rectangle, ellipse, arrow, ...) border width in board units.
46598
+ * Deliberately thinner than BASE_WIDTH: a shape border reads as a crisp,
46599
+ * structural line, not an expressive marker stroke — this is also the
46600
+ * fallback when no Host theme configures ScrawlTheme.shapeStrokeWidth.
46601
+ */
46602
+ var DEFAULT_SHAPE_STROKE_WIDTH = .15;
46603
+ /**
46574
46604
  * Identity of a rendered selection badge: which item, its lock state, and
46575
46605
  * where it sits on screen (rounded to the pixel, so sub-pixel camera drift
46576
46606
  * does not churn React).
@@ -46591,10 +46621,11 @@ function emptyExtensionRegistry() {
46591
46621
  * React never reaches past this class.
46592
46622
  */
46593
46623
  var ScrawlEngine = class {
46594
- constructor(canvas, documentId$1, extensionRegistry = emptyExtensionRegistry(), assetController = new AssetController(void 0), boardTheme = DEFAULT_BOARD_SURFACE_THEME) {
46624
+ constructor(canvas, documentId$1, extensionRegistry = emptyExtensionRegistry(), assetController = new AssetController(void 0), boardTheme = DEFAULT_BOARD_SURFACE_THEME, shapeStrokeWidth = DEFAULT_SHAPE_STROKE_WIDTH) {
46595
46625
  this.canvas = canvas;
46596
46626
  this.extensionRegistry = extensionRegistry;
46597
46627
  this.boardTheme = boardTheme;
46628
+ this.shapeStrokeWidth = shapeStrokeWidth;
46598
46629
  this.onToolChange = null;
46599
46630
  this.onZoomChange = null;
46600
46631
  this.onAuditEvent = null;
@@ -46689,6 +46720,7 @@ var ScrawlEngine = class {
46689
46720
  setTool: (t) => this.setTool(t),
46690
46721
  inkColor: () => this.tool === "highlighter" ? this.highlightColor : this.color,
46691
46722
  baseWidth: () => this.tool === "highlighter" ? HIGHLIGHT_WIDTH : BASE_WIDTH,
46723
+ shapeStrokeWidth: () => this.shapeStrokeWidth,
46692
46724
  activeTool: () => this.tool,
46693
46725
  isSpaceHeld: () => this.spaceHeld,
46694
46726
  pointerToBoard: (e) => pointerToBoard(e.clientX, e.clientY, canvas, this.scene.cameraRig.camera),
@@ -46953,6 +46985,10 @@ var ScrawlEngine = class {
46953
46985
  this.boardTheme = theme;
46954
46986
  this.scene.applyBoardTheme(theme);
46955
46987
  }
46988
+ /** Live update of the shape tool's border width (board units). Already-drawn shapes keep the width they were drawn with. */
46989
+ setShapeStrokeWidth(width) {
46990
+ this.shapeStrokeWidth = width;
46991
+ }
46956
46992
  /**
46957
46993
  * Snap to a peer's camera. No-op while drawing so pointer-to-board
46958
46994
  * mapping does not warp the in-progress stroke.
@@ -48014,7 +48050,7 @@ var ScrawlEngine = class {
48014
48050
  }
48015
48051
  };
48016
48052
  //#endregion
48017
- //#region src/controller-internal.ts
48053
+ //#region src/composition/controller-internal.ts
48018
48054
  function createBoardController(options) {
48019
48055
  return createBoardControllerWithEngine(options);
48020
48056
  }
@@ -48032,9 +48068,12 @@ function createBoardControllerWithEngine(options, existingEngine) {
48032
48068
  color: options.boardTheme?.surface ?? DEFAULT_BOARD_SURFACE_THEME.color,
48033
48069
  gridMode: options.boardTheme?.gridMode ?? DEFAULT_BOARD_SURFACE_THEME.gridMode,
48034
48070
  gridColor: options.boardTheme?.gridColor ?? DEFAULT_BOARD_SURFACE_THEME.gridColor,
48035
- gridSpacing: options.boardTheme?.gridSpacing ?? DEFAULT_BOARD_SURFACE_THEME.gridSpacing
48071
+ gridSpacing: options.boardTheme?.gridSpacing ?? DEFAULT_BOARD_SURFACE_THEME.gridSpacing,
48072
+ gridLineWidth: options.boardTheme?.gridLineWidth ?? DEFAULT_BOARD_SURFACE_THEME.gridLineWidth,
48073
+ surfaceTexture: options.boardTheme?.surfaceTexture ?? DEFAULT_BOARD_SURFACE_THEME.surfaceTexture
48036
48074
  };
48037
- const engine = existingEngine ?? (options.canvas ? new ScrawlEngine(options.canvas, options.document.id, extensionRegistry, assetController, currentBoardTheme) : null);
48075
+ let currentShapeStrokeWidth = options.boardTheme?.shapeStrokeWidth ?? .15;
48076
+ const engine = existingEngine ?? (options.canvas ? new ScrawlEngine(options.canvas, options.document.id, extensionRegistry, assetController, currentBoardTheme, currentShapeStrokeWidth) : null);
48038
48077
  const document = engine?.document ?? new BoardDocument(documentId(options.document.id));
48039
48078
  const history = engine?.history ?? new History(document);
48040
48079
  const createId = options.createId ?? (() => crypto.randomUUID());
@@ -48523,9 +48562,15 @@ function createBoardControllerWithEngine(options, existingEngine) {
48523
48562
  color: theme.surface ?? currentBoardTheme.color,
48524
48563
  gridMode: theme.gridMode ?? currentBoardTheme.gridMode,
48525
48564
  gridColor: theme.gridColor ?? currentBoardTheme.gridColor,
48526
- gridSpacing: theme.gridSpacing ?? currentBoardTheme.gridSpacing
48565
+ gridSpacing: theme.gridSpacing ?? currentBoardTheme.gridSpacing,
48566
+ gridLineWidth: theme.gridLineWidth ?? currentBoardTheme.gridLineWidth,
48567
+ surfaceTexture: theme.surfaceTexture ?? currentBoardTheme.surfaceTexture
48527
48568
  };
48528
48569
  engine?.applyBoardTheme(currentBoardTheme);
48570
+ if (theme.shapeStrokeWidth !== void 0) {
48571
+ currentShapeStrokeWidth = theme.shapeStrokeWidth;
48572
+ engine?.setShapeStrokeWidth(currentShapeStrokeWidth);
48573
+ }
48529
48574
  } },
48530
48575
  view: {
48531
48576
  fit() {