@fieldnotes/core 0.59.0 → 0.61.0

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.d.cts CHANGED
@@ -410,6 +410,7 @@ declare class ToolManager {
410
410
  private tools;
411
411
  private current;
412
412
  private changeListeners;
413
+ private registerListeners;
413
414
  get activeTool(): Tool | null;
414
415
  get toolNames(): string[];
415
416
  register(tool: Tool): void;
@@ -419,6 +420,7 @@ declare class ToolManager {
419
420
  handlePointerMove(state: PointerState, ctx: ToolContext): void;
420
421
  handlePointerUp(state: PointerState, ctx: ToolContext): void;
421
422
  onChange(listener: (name: string) => void): () => void;
423
+ onRegister(listener: (tool: Tool) => void): () => void;
422
424
  }
423
425
 
424
426
  interface Command {
@@ -558,6 +560,11 @@ declare function getElementStyle(element: CanvasElement): ElementStyle;
558
560
 
559
561
  type RotateDirection = 'cw' | 'ccw';
560
562
 
563
+ interface SelectionStyleDetails {
564
+ common: ElementStyle;
565
+ applicable: readonly (keyof ElementStyle)[];
566
+ mixed: readonly (keyof ElementStyle)[];
567
+ }
561
568
  type AlignEdge = 'left' | 'center-x' | 'right' | 'top' | 'middle' | 'bottom';
562
569
  type DistributeAxis = 'horizontal' | 'vertical';
563
570
 
@@ -637,6 +644,11 @@ declare class Viewport {
637
644
  private minimap;
638
645
  private readonly htmlRenderers;
639
646
  private readonly resizeListeners;
647
+ private readonly selectionListeners;
648
+ private detachSelectionSource;
649
+ private unsubToolRegister;
650
+ private pendingSelectionPrune;
651
+ private unsubRecorderEnd;
640
652
  constructor(container: HTMLElement, options?: ViewportOptions);
641
653
  get ctx(): CanvasRenderingContext2D | null;
642
654
  get snapToGrid(): boolean;
@@ -646,6 +658,18 @@ declare class Viewport {
646
658
  fitToContent(padding?: number): void;
647
659
  /** World-space rectangle currently visible through the canvas. */
648
660
  getVisibleRect(): Bounds;
661
+ /**
662
+ * Size in CSS pixels of the canvas that `getVisibleRect()` measures.
663
+ * Exposed because `canvasEl` is private: consumers can only reach the
664
+ * wrapper (via `domLayer.parentElement`), so without this accessor the
665
+ * canonical size behind `getVisibleRect()` is unreachable and callers
666
+ * resort to `getVisibleRect().w * camera.zoom`. Capture and restore must
667
+ * measure the same element or saved views do not round-trip.
668
+ */
669
+ getCanvasSize(): {
670
+ w: number;
671
+ h: number;
672
+ };
649
673
  /** Centers the camera on a world point without changing zoom. */
650
674
  centerCameraAt(world: Point): void;
651
675
  /**
@@ -730,12 +754,34 @@ declare class Viewport {
730
754
  getGridInfo(): GridInfo | null;
731
755
  onGridChange(listener: (info: GridInfo | null) => void): () => void;
732
756
  private getSelectTool;
757
+ private pruneSelection;
758
+ private handleRemovedElement;
759
+ private static isSelectionSource;
760
+ private emitSelectionChange;
761
+ private attachSelectionSource;
762
+ /**
763
+ * getSelectedIds() and the onSelectionChange emitter never surface stale ids:
764
+ * once the enclosing history transaction completes, both reflect
765
+ * the current selection.
766
+ */
733
767
  getSelectedIds(): string[];
734
768
  runAction(action: string): void;
735
769
  canPaste(): boolean;
736
770
  openContextMenu(screenPos: Point): void;
771
+ /**
772
+ * Persistent, viewport-owned selection-change emitter. Subscribing works
773
+ * regardless of whether a select tool is registered yet; it forwards
774
+ * events from whichever select tool is currently attached via
775
+ * `toolManager.onRegister`. Never delivers stale ids once the enclosing
776
+ * history transaction completes.
777
+ */
737
778
  onSelectionChange(listener: () => void): () => void;
738
779
  getSelectionStyle(): ElementStyle | null;
780
+ /**
781
+ * Unlike `getSelectionStyle()` — which returns `{}` for a style-less
782
+ * selection — this returns `null` when no style field is applicable.
783
+ */
784
+ getSelectionStyleDetails(): SelectionStyleDetails | null;
739
785
  applyStyleToSelection(style: ElementStyle): void;
740
786
  groupSelection(): void;
741
787
  ungroupSelection(): void;
@@ -1350,6 +1396,201 @@ declare class MinimapController {
1350
1396
  private onPointerEnd;
1351
1397
  }
1352
1398
 
1399
+ /**
1400
+ * A viewport-size-independent camera view: the world rectangle to frame.
1401
+ * Restored by contain-fit, so the same view frames the same world content on
1402
+ * any screen size or aspect — a DM's saved zone looks right on a phone and a
1403
+ * TV. Center+zoom would NOT have this property.
1404
+ */
1405
+ interface CameraView {
1406
+ x: number;
1407
+ y: number;
1408
+ w: number;
1409
+ h: number;
1410
+ }
1411
+ /** Captures the currently visible world rect. */
1412
+ declare function captureCameraView(viewport: {
1413
+ getVisibleRect(): {
1414
+ x: number;
1415
+ y: number;
1416
+ w: number;
1417
+ h: number;
1418
+ };
1419
+ }): CameraView;
1420
+ /**
1421
+ * Unclamped contain-fit zoom: the largest zoom at which the whole rect fits.
1422
+ * Contain, never crop — a view whose aspect differs from the canvas shows
1423
+ * extra world content on the short axis.
1424
+ */
1425
+ declare function fitZoomForView(view: CameraView, canvasW: number, canvasH: number): number;
1426
+ /** Camera origin that centers `view` on the canvas at an already-decided zoom. */
1427
+ declare function cameraOriginForView(view: CameraView, zoom: number, canvasW: number, canvasH: number): Point;
1428
+ /**
1429
+ * Writes `view` to `camera`. No-ops on a zero canvas dimension (mount and
1430
+ * visibility races are legitimate); throws on an invalid view or on negative
1431
+ * or non-finite dimensions.
1432
+ */
1433
+ declare function applyCameraView(camera: Camera, view: CameraView, canvasW: number, canvasH: number): void;
1434
+
1435
+ /** A scheduler and its matching canceller. Inseparable by construction. */
1436
+ interface FrameScheduler {
1437
+ requestFrame: (cb: () => void) => number;
1438
+ cancelFrame: (id: number) => void;
1439
+ }
1440
+ interface CameraAnimatorOptions {
1441
+ /** REQUIRED. `element` is used for input listeners only, never measurement. */
1442
+ getCanvasSize: () => {
1443
+ w: number;
1444
+ h: number;
1445
+ };
1446
+ durationMs?: number;
1447
+ easing?: (t: number) => number;
1448
+ interactive?: boolean;
1449
+ frames?: FrameScheduler;
1450
+ now?: () => number;
1451
+ }
1452
+ type CameraAnimationEndReason = 'complete' | 'cancelled' | 'superseded';
1453
+ /**
1454
+ * Animates a camera to a `CameraView`. Standalone controller in the
1455
+ * `PingInput`/`MinimapController` shape: the host owns construction and
1456
+ * disposal, and every timing dependency is injectable for deterministic tests.
1457
+ */
1458
+ declare class CameraAnimator {
1459
+ private readonly camera;
1460
+ private readonly getCanvasSize;
1461
+ private readonly frames;
1462
+ private readonly now;
1463
+ private readonly durationMs;
1464
+ private readonly easing;
1465
+ private rafId;
1466
+ private from;
1467
+ private to;
1468
+ private startedAt;
1469
+ private endListeners;
1470
+ /**
1471
+ * Monotonic operation counter. `animateTo`/`jumpTo` claim a generation
1472
+ * before emitting 'superseded'; if an onEnd listener starts a newer
1473
+ * operation during that emit, the outer call sees a bumped counter and
1474
+ * bails instead of overwriting the nested animation's state. Without this,
1475
+ * the nested animation would run to completion having never reported an end
1476
+ * reason, breaking the exactly-one guarantee the spec makes.
1477
+ */
1478
+ private generation;
1479
+ private lastWrite;
1480
+ private disposed;
1481
+ private detachListeners;
1482
+ constructor(element: HTMLElement, camera: Camera, options: CameraAnimatorOptions);
1483
+ get animating(): boolean;
1484
+ onEnd(listener: (reason: CameraAnimationEndReason) => void): () => void;
1485
+ animateTo(view: CameraView): void;
1486
+ jumpTo(view: CameraView): void;
1487
+ cancel(): void;
1488
+ /**
1489
+ * Terminal. Order is load-bearing: the flag is set BEFORE any listener runs,
1490
+ * because an onEnd listener can call animateTo during the disposal callback.
1491
+ * With the flag set last, that call would start a real animation which the
1492
+ * listener clear then silently discards — a second animation with no end
1493
+ * reason, breaking the exactly-one guarantee.
1494
+ */
1495
+ dispose(): void;
1496
+ /**
1497
+ * Steps 1-3 of the public-call contract. Returns null when the caller must
1498
+ * stop, having already handled termination.
1499
+ *
1500
+ * The disposed check precedes validation deliberately: ordering it after
1501
+ * would make `disposed.animateTo(invalidView)` both required to throw and
1502
+ * required to stay silent. Disposal wins — a terminal animator is inert for
1503
+ * every input, and post-disposal calls are exactly the racy teardown paths
1504
+ * where a throw is least useful.
1505
+ */
1506
+ private validateAndMeasure;
1507
+ private step;
1508
+ private recordWrite;
1509
+ private foreignWrite;
1510
+ /** Terminates an in-flight animation with `reason`. No-op when idle. */
1511
+ private end;
1512
+ private clearFrame;
1513
+ private emit;
1514
+ }
1515
+
1516
+ /**
1517
+ * The wire shape of a focus-request presence payload. Focus is ephemeral by
1518
+ * contract: presence frames only — never elements, undo history, persisted
1519
+ * canvas state, or durable operations. A frame that arrives while a client is
1520
+ * offline is dropped, never queued, and late joiners are never retro-focused.
1521
+ *
1522
+ * `audience` is a delivery hint, NOT a security boundary: the relay broadcasts
1523
+ * presence room-wide and receivers filter by their own role. The payload is a
1524
+ * map rectangle, not secret data; hidden elements stay behind relay `canRead`.
1525
+ */
1526
+ interface FocusPresence {
1527
+ readonly kind: 'focus';
1528
+ readonly x: number;
1529
+ readonly y: number;
1530
+ readonly w: number;
1531
+ readonly h: number;
1532
+ readonly audience: FocusAudience;
1533
+ readonly color?: string;
1534
+ }
1535
+ type FocusAudience = 'all' | 'players' | 'display';
1536
+ declare const FOCUS_PRESENCE_KIND = "focus";
1537
+ /**
1538
+ * The only trust boundary between untyped wire data and the canvas. Mirrors
1539
+ * `isPingPresence`: every field is validated, and `color` is rejected when
1540
+ * defined but not a string, because it flows into canvas styling.
1541
+ *
1542
+ * These rules deliberately match `CameraView` validation, so a payload that
1543
+ * passes here can never make the animator's synchronous target validation
1544
+ * throw on the receive path.
1545
+ */
1546
+ declare function isFocusPresence(data: unknown): data is FocusPresence;
1547
+ /** Builds the presence payload for one local focus request. */
1548
+ declare function toFocusPresence(view: CameraView, audience: FocusAudience, color?: string): FocusPresence;
1549
+
1550
+ type FocusRole = 'dm' | 'player' | 'display';
1551
+ /** The two viewport capabilities the receiver needs; `Viewport` satisfies it. */
1552
+ type RemoteFocusReceiverHost = RemotePingOverlayHost;
1553
+ interface RemoteFocusReceiverOptions {
1554
+ role: FocusRole;
1555
+ animator: CameraAnimator;
1556
+ /** Draw an arrival pulse at the focus target. Default `true`. */
1557
+ pulse?: boolean;
1558
+ pulseColor?: string;
1559
+ pulseDurationMs?: number;
1560
+ pulseRadius?: number;
1561
+ /** Animate the camera (default) or jump instantly. */
1562
+ animate?: boolean;
1563
+ }
1564
+ /**
1565
+ * Applies remote focus requests addressed to this client's role: moves the
1566
+ * camera and marks the target with one pulse.
1567
+ *
1568
+ * The pulse is DELEGATED to a private `RemotePingOverlay` rather than
1569
+ * reimplemented. `renderPingPulse` draws a single frame from an elapsed time,
1570
+ * so an animated pulse needs a rAF loop that re-requests renders, expires the
1571
+ * pulse, and cancels on disposal — machinery that already exists and is tested
1572
+ * there. `maxPingsPerSender: 1` makes a rapid second focus REPLACE the older
1573
+ * pulse instead of leaving two competing markers; different senders keep
1574
+ * separate keys and coexist, which is correct when two DMs share a table.
1575
+ */
1576
+ declare class RemoteFocusReceiver {
1577
+ private readonly role;
1578
+ private readonly animator;
1579
+ private readonly animate;
1580
+ private readonly pulseColor;
1581
+ private readonly overlay;
1582
+ private disposed;
1583
+ constructor(host: RemoteFocusReceiverHost, options: RemoteFocusReceiverOptions);
1584
+ /**
1585
+ * Applies a presence payload from `sender`. Returns `false` for payloads
1586
+ * that are not focus frames, or are addressed to a different role, so hosts
1587
+ * can feed every presence frame through without disturbing other handlers.
1588
+ */
1589
+ apply(from: string, data: unknown): boolean;
1590
+ /** Idempotent. Does NOT dispose the animator — the host owns that. */
1591
+ dispose(): void;
1592
+ }
1593
+
1353
1594
  interface ActiveFormats {
1354
1595
  bold: boolean;
1355
1596
  italic: boolean;
@@ -1755,6 +1996,6 @@ declare class TemplateTool implements Tool {
1755
1996
  private notifyOptionsChange;
1756
1997
  }
1757
1998
 
1758
- declare const VERSION = "0.59.0";
1999
+ declare const VERSION = "0.61.0";
1759
2000
 
1760
- export { type ActiveFormats, type AlignEdge, type ArrowElement, type ArrowStrokeStyle, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, type BackgroundOptions, type BackgroundPattern, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, DEFAULT_NOTE_FONT_SIZE, type DistributeAxis, type ElementChangeMeta, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportAssetError, type ExportAssetErrorReason, type ExportImageOptions, type ExportResourceOptions, type ExportSvgOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type HtmlExportError, type HtmlExportErrorReason, type HtmlExportOptions, type HtmlExportRenderer, type ImageElement, ImageTool, type ImageToolOptions, IndexedDBAdapter, type IndexedDBAdapterOptions, LASER_TRAIL_PRESENCE_KIND, LaserTool, type LaserToolOptions, type LaserTrailEmission, type LaserTrailPresence, type Layer, LayerManager, LocalStorageAdapter, MEASURE_PRESENCE_KIND, type MeasureEmission, type MeasurePresence, MeasureTool, type MeasureToolOptions, type Measurement, MemoryAdapter, MinimapController, type MinimapControllerOptions, type NoteElement, NoteTool, type NoteToolOptions, type OverlayRenderer, PING_PRESENCE_KIND, PencilTool, type PencilToolOptions, type PingEmission, PingInput, type PingInputHost, type PingInputOptions, type PingPresence, PingTool, type PingToolOptions, type Point, type PointerState, RemoteLaserOverlay, type RemoteLaserOverlayHost, type RemoteLaserOverlayOptions, RemoteMeasureOverlay, type RemoteMeasureOverlayHost, type RemoteMeasureOverlayOptions, RemotePingOverlay, type RemotePingOverlayHost, type RemotePingOverlayOptions, type RenderStatsSnapshot, type RotateDirection, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StorageAdapter, type StrokeElement, type StrokePoint, type TemplateElement, type TemplateRenderStyle, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, VERSION, Viewport, type ViewportOptions, boundsIntersect, createArrow, createGrid, createHtmlElement, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportSvg, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementStyle, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInRectangle, getHexCellsInSquare, getHexDistance, isLaserTrailPresence, isMeasurePresence, isNearBezier, isPingPresence, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toLaserTrailPresence, toMeasurePresence, toPingPresence, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
2001
+ export { type ActiveFormats, type AlignEdge, type ArrowElement, type ArrowStrokeStyle, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, type BackgroundOptions, type BackgroundPattern, type Binding, type Bounds, Camera, type CameraAnimationEndReason, CameraAnimator, type CameraAnimatorOptions, type CameraChangeInfo, type CameraOptions, type CameraView, type CanvasElement, type CanvasState, type Command, DEFAULT_NOTE_FONT_SIZE, type DistributeAxis, type ElementChangeMeta, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportAssetError, type ExportAssetErrorReason, type ExportImageOptions, type ExportResourceOptions, type ExportSvgOptions, FOCUS_PRESENCE_KIND, type FocusAudience, type FocusPresence, type FocusRole, type FontSizePreset, type FrameScheduler, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type HtmlExportError, type HtmlExportErrorReason, type HtmlExportOptions, type HtmlExportRenderer, type ImageElement, ImageTool, type ImageToolOptions, IndexedDBAdapter, type IndexedDBAdapterOptions, LASER_TRAIL_PRESENCE_KIND, LaserTool, type LaserToolOptions, type LaserTrailEmission, type LaserTrailPresence, type Layer, LayerManager, LocalStorageAdapter, MEASURE_PRESENCE_KIND, type MeasureEmission, type MeasurePresence, MeasureTool, type MeasureToolOptions, type Measurement, MemoryAdapter, MinimapController, type MinimapControllerOptions, type NoteElement, NoteTool, type NoteToolOptions, type OverlayRenderer, PING_PRESENCE_KIND, PencilTool, type PencilToolOptions, type PingEmission, PingInput, type PingInputHost, type PingInputOptions, type PingPresence, PingTool, type PingToolOptions, type Point, type PointerState, RemoteFocusReceiver, type RemoteFocusReceiverHost, type RemoteFocusReceiverOptions, RemoteLaserOverlay, type RemoteLaserOverlayHost, type RemoteLaserOverlayOptions, RemoteMeasureOverlay, type RemoteMeasureOverlayHost, type RemoteMeasureOverlayOptions, RemotePingOverlay, type RemotePingOverlayHost, type RemotePingOverlayOptions, type RenderStatsSnapshot, type RotateDirection, SelectTool, type SelectionStyleDetails, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StorageAdapter, type StrokeElement, type StrokePoint, type TemplateElement, type TemplateRenderStyle, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, VERSION, Viewport, type ViewportOptions, applyCameraView, boundsIntersect, cameraOriginForView, captureCameraView, createArrow, createGrid, createHtmlElement, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportSvg, fitZoomForView, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementStyle, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInRectangle, getHexCellsInSquare, getHexDistance, isFocusPresence, isLaserTrailPresence, isMeasurePresence, isNearBezier, isPingPresence, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toFocusPresence, toLaserTrailPresence, toMeasurePresence, toPingPresence, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
package/dist/index.d.ts CHANGED
@@ -410,6 +410,7 @@ declare class ToolManager {
410
410
  private tools;
411
411
  private current;
412
412
  private changeListeners;
413
+ private registerListeners;
413
414
  get activeTool(): Tool | null;
414
415
  get toolNames(): string[];
415
416
  register(tool: Tool): void;
@@ -419,6 +420,7 @@ declare class ToolManager {
419
420
  handlePointerMove(state: PointerState, ctx: ToolContext): void;
420
421
  handlePointerUp(state: PointerState, ctx: ToolContext): void;
421
422
  onChange(listener: (name: string) => void): () => void;
423
+ onRegister(listener: (tool: Tool) => void): () => void;
422
424
  }
423
425
 
424
426
  interface Command {
@@ -558,6 +560,11 @@ declare function getElementStyle(element: CanvasElement): ElementStyle;
558
560
 
559
561
  type RotateDirection = 'cw' | 'ccw';
560
562
 
563
+ interface SelectionStyleDetails {
564
+ common: ElementStyle;
565
+ applicable: readonly (keyof ElementStyle)[];
566
+ mixed: readonly (keyof ElementStyle)[];
567
+ }
561
568
  type AlignEdge = 'left' | 'center-x' | 'right' | 'top' | 'middle' | 'bottom';
562
569
  type DistributeAxis = 'horizontal' | 'vertical';
563
570
 
@@ -637,6 +644,11 @@ declare class Viewport {
637
644
  private minimap;
638
645
  private readonly htmlRenderers;
639
646
  private readonly resizeListeners;
647
+ private readonly selectionListeners;
648
+ private detachSelectionSource;
649
+ private unsubToolRegister;
650
+ private pendingSelectionPrune;
651
+ private unsubRecorderEnd;
640
652
  constructor(container: HTMLElement, options?: ViewportOptions);
641
653
  get ctx(): CanvasRenderingContext2D | null;
642
654
  get snapToGrid(): boolean;
@@ -646,6 +658,18 @@ declare class Viewport {
646
658
  fitToContent(padding?: number): void;
647
659
  /** World-space rectangle currently visible through the canvas. */
648
660
  getVisibleRect(): Bounds;
661
+ /**
662
+ * Size in CSS pixels of the canvas that `getVisibleRect()` measures.
663
+ * Exposed because `canvasEl` is private: consumers can only reach the
664
+ * wrapper (via `domLayer.parentElement`), so without this accessor the
665
+ * canonical size behind `getVisibleRect()` is unreachable and callers
666
+ * resort to `getVisibleRect().w * camera.zoom`. Capture and restore must
667
+ * measure the same element or saved views do not round-trip.
668
+ */
669
+ getCanvasSize(): {
670
+ w: number;
671
+ h: number;
672
+ };
649
673
  /** Centers the camera on a world point without changing zoom. */
650
674
  centerCameraAt(world: Point): void;
651
675
  /**
@@ -730,12 +754,34 @@ declare class Viewport {
730
754
  getGridInfo(): GridInfo | null;
731
755
  onGridChange(listener: (info: GridInfo | null) => void): () => void;
732
756
  private getSelectTool;
757
+ private pruneSelection;
758
+ private handleRemovedElement;
759
+ private static isSelectionSource;
760
+ private emitSelectionChange;
761
+ private attachSelectionSource;
762
+ /**
763
+ * getSelectedIds() and the onSelectionChange emitter never surface stale ids:
764
+ * once the enclosing history transaction completes, both reflect
765
+ * the current selection.
766
+ */
733
767
  getSelectedIds(): string[];
734
768
  runAction(action: string): void;
735
769
  canPaste(): boolean;
736
770
  openContextMenu(screenPos: Point): void;
771
+ /**
772
+ * Persistent, viewport-owned selection-change emitter. Subscribing works
773
+ * regardless of whether a select tool is registered yet; it forwards
774
+ * events from whichever select tool is currently attached via
775
+ * `toolManager.onRegister`. Never delivers stale ids once the enclosing
776
+ * history transaction completes.
777
+ */
737
778
  onSelectionChange(listener: () => void): () => void;
738
779
  getSelectionStyle(): ElementStyle | null;
780
+ /**
781
+ * Unlike `getSelectionStyle()` — which returns `{}` for a style-less
782
+ * selection — this returns `null` when no style field is applicable.
783
+ */
784
+ getSelectionStyleDetails(): SelectionStyleDetails | null;
739
785
  applyStyleToSelection(style: ElementStyle): void;
740
786
  groupSelection(): void;
741
787
  ungroupSelection(): void;
@@ -1350,6 +1396,201 @@ declare class MinimapController {
1350
1396
  private onPointerEnd;
1351
1397
  }
1352
1398
 
1399
+ /**
1400
+ * A viewport-size-independent camera view: the world rectangle to frame.
1401
+ * Restored by contain-fit, so the same view frames the same world content on
1402
+ * any screen size or aspect — a DM's saved zone looks right on a phone and a
1403
+ * TV. Center+zoom would NOT have this property.
1404
+ */
1405
+ interface CameraView {
1406
+ x: number;
1407
+ y: number;
1408
+ w: number;
1409
+ h: number;
1410
+ }
1411
+ /** Captures the currently visible world rect. */
1412
+ declare function captureCameraView(viewport: {
1413
+ getVisibleRect(): {
1414
+ x: number;
1415
+ y: number;
1416
+ w: number;
1417
+ h: number;
1418
+ };
1419
+ }): CameraView;
1420
+ /**
1421
+ * Unclamped contain-fit zoom: the largest zoom at which the whole rect fits.
1422
+ * Contain, never crop — a view whose aspect differs from the canvas shows
1423
+ * extra world content on the short axis.
1424
+ */
1425
+ declare function fitZoomForView(view: CameraView, canvasW: number, canvasH: number): number;
1426
+ /** Camera origin that centers `view` on the canvas at an already-decided zoom. */
1427
+ declare function cameraOriginForView(view: CameraView, zoom: number, canvasW: number, canvasH: number): Point;
1428
+ /**
1429
+ * Writes `view` to `camera`. No-ops on a zero canvas dimension (mount and
1430
+ * visibility races are legitimate); throws on an invalid view or on negative
1431
+ * or non-finite dimensions.
1432
+ */
1433
+ declare function applyCameraView(camera: Camera, view: CameraView, canvasW: number, canvasH: number): void;
1434
+
1435
+ /** A scheduler and its matching canceller. Inseparable by construction. */
1436
+ interface FrameScheduler {
1437
+ requestFrame: (cb: () => void) => number;
1438
+ cancelFrame: (id: number) => void;
1439
+ }
1440
+ interface CameraAnimatorOptions {
1441
+ /** REQUIRED. `element` is used for input listeners only, never measurement. */
1442
+ getCanvasSize: () => {
1443
+ w: number;
1444
+ h: number;
1445
+ };
1446
+ durationMs?: number;
1447
+ easing?: (t: number) => number;
1448
+ interactive?: boolean;
1449
+ frames?: FrameScheduler;
1450
+ now?: () => number;
1451
+ }
1452
+ type CameraAnimationEndReason = 'complete' | 'cancelled' | 'superseded';
1453
+ /**
1454
+ * Animates a camera to a `CameraView`. Standalone controller in the
1455
+ * `PingInput`/`MinimapController` shape: the host owns construction and
1456
+ * disposal, and every timing dependency is injectable for deterministic tests.
1457
+ */
1458
+ declare class CameraAnimator {
1459
+ private readonly camera;
1460
+ private readonly getCanvasSize;
1461
+ private readonly frames;
1462
+ private readonly now;
1463
+ private readonly durationMs;
1464
+ private readonly easing;
1465
+ private rafId;
1466
+ private from;
1467
+ private to;
1468
+ private startedAt;
1469
+ private endListeners;
1470
+ /**
1471
+ * Monotonic operation counter. `animateTo`/`jumpTo` claim a generation
1472
+ * before emitting 'superseded'; if an onEnd listener starts a newer
1473
+ * operation during that emit, the outer call sees a bumped counter and
1474
+ * bails instead of overwriting the nested animation's state. Without this,
1475
+ * the nested animation would run to completion having never reported an end
1476
+ * reason, breaking the exactly-one guarantee the spec makes.
1477
+ */
1478
+ private generation;
1479
+ private lastWrite;
1480
+ private disposed;
1481
+ private detachListeners;
1482
+ constructor(element: HTMLElement, camera: Camera, options: CameraAnimatorOptions);
1483
+ get animating(): boolean;
1484
+ onEnd(listener: (reason: CameraAnimationEndReason) => void): () => void;
1485
+ animateTo(view: CameraView): void;
1486
+ jumpTo(view: CameraView): void;
1487
+ cancel(): void;
1488
+ /**
1489
+ * Terminal. Order is load-bearing: the flag is set BEFORE any listener runs,
1490
+ * because an onEnd listener can call animateTo during the disposal callback.
1491
+ * With the flag set last, that call would start a real animation which the
1492
+ * listener clear then silently discards — a second animation with no end
1493
+ * reason, breaking the exactly-one guarantee.
1494
+ */
1495
+ dispose(): void;
1496
+ /**
1497
+ * Steps 1-3 of the public-call contract. Returns null when the caller must
1498
+ * stop, having already handled termination.
1499
+ *
1500
+ * The disposed check precedes validation deliberately: ordering it after
1501
+ * would make `disposed.animateTo(invalidView)` both required to throw and
1502
+ * required to stay silent. Disposal wins — a terminal animator is inert for
1503
+ * every input, and post-disposal calls are exactly the racy teardown paths
1504
+ * where a throw is least useful.
1505
+ */
1506
+ private validateAndMeasure;
1507
+ private step;
1508
+ private recordWrite;
1509
+ private foreignWrite;
1510
+ /** Terminates an in-flight animation with `reason`. No-op when idle. */
1511
+ private end;
1512
+ private clearFrame;
1513
+ private emit;
1514
+ }
1515
+
1516
+ /**
1517
+ * The wire shape of a focus-request presence payload. Focus is ephemeral by
1518
+ * contract: presence frames only — never elements, undo history, persisted
1519
+ * canvas state, or durable operations. A frame that arrives while a client is
1520
+ * offline is dropped, never queued, and late joiners are never retro-focused.
1521
+ *
1522
+ * `audience` is a delivery hint, NOT a security boundary: the relay broadcasts
1523
+ * presence room-wide and receivers filter by their own role. The payload is a
1524
+ * map rectangle, not secret data; hidden elements stay behind relay `canRead`.
1525
+ */
1526
+ interface FocusPresence {
1527
+ readonly kind: 'focus';
1528
+ readonly x: number;
1529
+ readonly y: number;
1530
+ readonly w: number;
1531
+ readonly h: number;
1532
+ readonly audience: FocusAudience;
1533
+ readonly color?: string;
1534
+ }
1535
+ type FocusAudience = 'all' | 'players' | 'display';
1536
+ declare const FOCUS_PRESENCE_KIND = "focus";
1537
+ /**
1538
+ * The only trust boundary between untyped wire data and the canvas. Mirrors
1539
+ * `isPingPresence`: every field is validated, and `color` is rejected when
1540
+ * defined but not a string, because it flows into canvas styling.
1541
+ *
1542
+ * These rules deliberately match `CameraView` validation, so a payload that
1543
+ * passes here can never make the animator's synchronous target validation
1544
+ * throw on the receive path.
1545
+ */
1546
+ declare function isFocusPresence(data: unknown): data is FocusPresence;
1547
+ /** Builds the presence payload for one local focus request. */
1548
+ declare function toFocusPresence(view: CameraView, audience: FocusAudience, color?: string): FocusPresence;
1549
+
1550
+ type FocusRole = 'dm' | 'player' | 'display';
1551
+ /** The two viewport capabilities the receiver needs; `Viewport` satisfies it. */
1552
+ type RemoteFocusReceiverHost = RemotePingOverlayHost;
1553
+ interface RemoteFocusReceiverOptions {
1554
+ role: FocusRole;
1555
+ animator: CameraAnimator;
1556
+ /** Draw an arrival pulse at the focus target. Default `true`. */
1557
+ pulse?: boolean;
1558
+ pulseColor?: string;
1559
+ pulseDurationMs?: number;
1560
+ pulseRadius?: number;
1561
+ /** Animate the camera (default) or jump instantly. */
1562
+ animate?: boolean;
1563
+ }
1564
+ /**
1565
+ * Applies remote focus requests addressed to this client's role: moves the
1566
+ * camera and marks the target with one pulse.
1567
+ *
1568
+ * The pulse is DELEGATED to a private `RemotePingOverlay` rather than
1569
+ * reimplemented. `renderPingPulse` draws a single frame from an elapsed time,
1570
+ * so an animated pulse needs a rAF loop that re-requests renders, expires the
1571
+ * pulse, and cancels on disposal — machinery that already exists and is tested
1572
+ * there. `maxPingsPerSender: 1` makes a rapid second focus REPLACE the older
1573
+ * pulse instead of leaving two competing markers; different senders keep
1574
+ * separate keys and coexist, which is correct when two DMs share a table.
1575
+ */
1576
+ declare class RemoteFocusReceiver {
1577
+ private readonly role;
1578
+ private readonly animator;
1579
+ private readonly animate;
1580
+ private readonly pulseColor;
1581
+ private readonly overlay;
1582
+ private disposed;
1583
+ constructor(host: RemoteFocusReceiverHost, options: RemoteFocusReceiverOptions);
1584
+ /**
1585
+ * Applies a presence payload from `sender`. Returns `false` for payloads
1586
+ * that are not focus frames, or are addressed to a different role, so hosts
1587
+ * can feed every presence frame through without disturbing other handlers.
1588
+ */
1589
+ apply(from: string, data: unknown): boolean;
1590
+ /** Idempotent. Does NOT dispose the animator — the host owns that. */
1591
+ dispose(): void;
1592
+ }
1593
+
1353
1594
  interface ActiveFormats {
1354
1595
  bold: boolean;
1355
1596
  italic: boolean;
@@ -1755,6 +1996,6 @@ declare class TemplateTool implements Tool {
1755
1996
  private notifyOptionsChange;
1756
1997
  }
1757
1998
 
1758
- declare const VERSION = "0.59.0";
1999
+ declare const VERSION = "0.61.0";
1759
2000
 
1760
- export { type ActiveFormats, type AlignEdge, type ArrowElement, type ArrowStrokeStyle, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, type BackgroundOptions, type BackgroundPattern, type Binding, type Bounds, Camera, type CameraChangeInfo, type CameraOptions, type CanvasElement, type CanvasState, type Command, DEFAULT_NOTE_FONT_SIZE, type DistributeAxis, type ElementChangeMeta, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportAssetError, type ExportAssetErrorReason, type ExportImageOptions, type ExportResourceOptions, type ExportSvgOptions, type FontSizePreset, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type HtmlExportError, type HtmlExportErrorReason, type HtmlExportOptions, type HtmlExportRenderer, type ImageElement, ImageTool, type ImageToolOptions, IndexedDBAdapter, type IndexedDBAdapterOptions, LASER_TRAIL_PRESENCE_KIND, LaserTool, type LaserToolOptions, type LaserTrailEmission, type LaserTrailPresence, type Layer, LayerManager, LocalStorageAdapter, MEASURE_PRESENCE_KIND, type MeasureEmission, type MeasurePresence, MeasureTool, type MeasureToolOptions, type Measurement, MemoryAdapter, MinimapController, type MinimapControllerOptions, type NoteElement, NoteTool, type NoteToolOptions, type OverlayRenderer, PING_PRESENCE_KIND, PencilTool, type PencilToolOptions, type PingEmission, PingInput, type PingInputHost, type PingInputOptions, type PingPresence, PingTool, type PingToolOptions, type Point, type PointerState, RemoteLaserOverlay, type RemoteLaserOverlayHost, type RemoteLaserOverlayOptions, RemoteMeasureOverlay, type RemoteMeasureOverlayHost, type RemoteMeasureOverlayOptions, RemotePingOverlay, type RemotePingOverlayHost, type RemotePingOverlayOptions, type RenderStatsSnapshot, type RotateDirection, SelectTool, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StorageAdapter, type StrokeElement, type StrokePoint, type TemplateElement, type TemplateRenderStyle, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, VERSION, Viewport, type ViewportOptions, boundsIntersect, createArrow, createGrid, createHtmlElement, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportSvg, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementStyle, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInRectangle, getHexCellsInSquare, getHexDistance, isLaserTrailPresence, isMeasurePresence, isNearBezier, isPingPresence, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toLaserTrailPresence, toMeasurePresence, toPingPresence, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };
2001
+ export { type ActiveFormats, type AlignEdge, type ArrowElement, type ArrowStrokeStyle, ArrowTool, type ArrowToolOptions, AutoSave, type AutoSaveOptions, type BackgroundOptions, type BackgroundPattern, type Binding, type Bounds, Camera, type CameraAnimationEndReason, CameraAnimator, type CameraAnimatorOptions, type CameraChangeInfo, type CameraOptions, type CameraView, type CanvasElement, type CanvasState, type Command, DEFAULT_NOTE_FONT_SIZE, type DistributeAxis, type ElementChangeMeta, ElementStore, type ElementStyle, type ElementType, type ElementUpdateEvent, EraserTool, type EraserToolOptions, type ExportAssetError, type ExportAssetErrorReason, type ExportImageOptions, type ExportResourceOptions, type ExportSvgOptions, FOCUS_PRESENCE_KIND, type FocusAudience, type FocusPresence, type FocusRole, type FontSizePreset, type FrameScheduler, type GridElement, type GridInfo, HandTool, type HexOrientation, HistoryStack, type HistoryStackOptions, type HtmlElement, type HtmlExportError, type HtmlExportErrorReason, type HtmlExportOptions, type HtmlExportRenderer, type ImageElement, ImageTool, type ImageToolOptions, IndexedDBAdapter, type IndexedDBAdapterOptions, LASER_TRAIL_PRESENCE_KIND, LaserTool, type LaserToolOptions, type LaserTrailEmission, type LaserTrailPresence, type Layer, LayerManager, LocalStorageAdapter, MEASURE_PRESENCE_KIND, type MeasureEmission, type MeasurePresence, MeasureTool, type MeasureToolOptions, type Measurement, MemoryAdapter, MinimapController, type MinimapControllerOptions, type NoteElement, NoteTool, type NoteToolOptions, type OverlayRenderer, PING_PRESENCE_KIND, PencilTool, type PencilToolOptions, type PingEmission, PingInput, type PingInputHost, type PingInputOptions, type PingPresence, PingTool, type PingToolOptions, type Point, type PointerState, RemoteFocusReceiver, type RemoteFocusReceiverHost, type RemoteFocusReceiverOptions, RemoteLaserOverlay, type RemoteLaserOverlayHost, type RemoteLaserOverlayOptions, RemoteMeasureOverlay, type RemoteMeasureOverlayHost, type RemoteMeasureOverlayOptions, RemotePingOverlay, type RemotePingOverlayHost, type RemotePingOverlayOptions, type RenderStatsSnapshot, type RotateDirection, SelectTool, type SelectionStyleDetails, type ShapeElement, type ShapeKind, ShapeTool, type ShapeToolOptions, type ShortcutBindings, type ShortcutOptions, type ShortcutsApi, type Size, type StorageAdapter, type StrokeElement, type StrokePoint, type TemplateElement, type TemplateRenderStyle, type TemplateShape, TemplateTool, type TemplateToolOptions, type TextElement, TextTool, type TextToolOptions, type Tool, type ToolContext, ToolManager, type ToolName, VERSION, Viewport, type ViewportOptions, applyCameraView, boundsIntersect, cameraOriginForView, captureCameraView, createArrow, createGrid, createHtmlElement, createImage, createNote, createShape, createStroke, createTemplate, createText, drawHexPath, exportImage, exportSvg, fitZoomForView, getActiveFormats, getArrowBounds, getArrowControlPoint, getArrowMidpoint, getArrowTangentAngle, getBendFromPoint, getElementBounds, getElementStyle, getElementsBoundingBox, getHexCellsInCone, getHexCellsInLine, getHexCellsInRadius, getHexCellsInRectangle, getHexCellsInSquare, getHexDistance, isFocusPresence, isLaserTrailPresence, isMeasurePresence, isNearBezier, isPingPresence, setFontSize, smartSnap, snapPoint, snapToHexCenter, styleToPatch, toFocusPresence, toLaserTrailPresence, toMeasurePresence, toPingPresence, toggleBold, toggleItalic, toggleStrikethrough, toggleUnderline };