@fieldnotes/core 0.58.0 → 0.60.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 {
@@ -469,6 +471,32 @@ interface ExportImageOptions extends ExportResourceOptions, HtmlExportOptions {
469
471
  padding?: number;
470
472
  background?: string;
471
473
  filter?: (element: CanvasElement) => boolean;
474
+ /**
475
+ * Optional world-space crop. When set it replaces the computed content
476
+ * bounds; `padding` still applies around it. Elements outside the region
477
+ * clip naturally. A region export with no intersecting elements still
478
+ * produces a background-filled image.
479
+ */
480
+ region?: {
481
+ x: number;
482
+ y: number;
483
+ w: number;
484
+ h: number;
485
+ };
486
+ /**
487
+ * Output encoding. Defaults to 'png'. JPEG output is opaque — a transparent
488
+ * `background` renders as black; keep the default `#ffffff` (or any opaque color)
489
+ * for JPEG exports.
490
+ */
491
+ format?: 'png' | 'jpeg';
492
+ /** Encoder quality in (0, 1]. Only meaningful for 'jpeg'. */
493
+ quality?: number;
494
+ /**
495
+ * 'exact' (default) throws when scale × bounds exceeds maxDimension/maxPixels.
496
+ * 'fit' reduces the effective scale to the largest value that satisfies both
497
+ * caps (never above the requested scale).
498
+ */
499
+ scaleMode?: 'exact' | 'fit';
472
500
  }
473
501
  type ExportAssetErrorReason = 'load' | 'timeout' | 'encode';
474
502
  interface ExportAssetError {
@@ -532,6 +560,11 @@ declare function getElementStyle(element: CanvasElement): ElementStyle;
532
560
 
533
561
  type RotateDirection = 'cw' | 'ccw';
534
562
 
563
+ interface SelectionStyleDetails {
564
+ common: ElementStyle;
565
+ applicable: readonly (keyof ElementStyle)[];
566
+ mixed: readonly (keyof ElementStyle)[];
567
+ }
535
568
  type AlignEdge = 'left' | 'center-x' | 'right' | 'top' | 'middle' | 'bottom';
536
569
  type DistributeAxis = 'horizontal' | 'vertical';
537
570
 
@@ -611,6 +644,11 @@ declare class Viewport {
611
644
  private minimap;
612
645
  private readonly htmlRenderers;
613
646
  private readonly resizeListeners;
647
+ private readonly selectionListeners;
648
+ private detachSelectionSource;
649
+ private unsubToolRegister;
650
+ private pendingSelectionPrune;
651
+ private unsubRecorderEnd;
614
652
  constructor(container: HTMLElement, options?: ViewportOptions);
615
653
  get ctx(): CanvasRenderingContext2D | null;
616
654
  get snapToGrid(): boolean;
@@ -704,12 +742,34 @@ declare class Viewport {
704
742
  getGridInfo(): GridInfo | null;
705
743
  onGridChange(listener: (info: GridInfo | null) => void): () => void;
706
744
  private getSelectTool;
745
+ private pruneSelection;
746
+ private handleRemovedElement;
747
+ private static isSelectionSource;
748
+ private emitSelectionChange;
749
+ private attachSelectionSource;
750
+ /**
751
+ * getSelectedIds() and the onSelectionChange emitter never surface stale ids:
752
+ * once the enclosing history transaction completes, both reflect
753
+ * the current selection.
754
+ */
707
755
  getSelectedIds(): string[];
708
756
  runAction(action: string): void;
709
757
  canPaste(): boolean;
710
758
  openContextMenu(screenPos: Point): void;
759
+ /**
760
+ * Persistent, viewport-owned selection-change emitter. Subscribing works
761
+ * regardless of whether a select tool is registered yet; it forwards
762
+ * events from whichever select tool is currently attached via
763
+ * `toolManager.onRegister`. Never delivers stale ids once the enclosing
764
+ * history transaction completes.
765
+ */
711
766
  onSelectionChange(listener: () => void): () => void;
712
767
  getSelectionStyle(): ElementStyle | null;
768
+ /**
769
+ * Unlike `getSelectionStyle()` — which returns `{}` for a style-less
770
+ * selection — this returns `null` when no style field is applicable.
771
+ */
772
+ getSelectionStyleDetails(): SelectionStyleDetails | null;
713
773
  applyStyleToSelection(style: ElementStyle): void;
714
774
  groupSelection(): void;
715
775
  ungroupSelection(): void;
@@ -1729,6 +1789,6 @@ declare class TemplateTool implements Tool {
1729
1789
  private notifyOptionsChange;
1730
1790
  }
1731
1791
 
1732
- declare const VERSION = "0.58.0";
1792
+ declare const VERSION = "0.60.0";
1733
1793
 
1734
- 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 };
1794
+ 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 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, 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 };
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 {
@@ -469,6 +471,32 @@ interface ExportImageOptions extends ExportResourceOptions, HtmlExportOptions {
469
471
  padding?: number;
470
472
  background?: string;
471
473
  filter?: (element: CanvasElement) => boolean;
474
+ /**
475
+ * Optional world-space crop. When set it replaces the computed content
476
+ * bounds; `padding` still applies around it. Elements outside the region
477
+ * clip naturally. A region export with no intersecting elements still
478
+ * produces a background-filled image.
479
+ */
480
+ region?: {
481
+ x: number;
482
+ y: number;
483
+ w: number;
484
+ h: number;
485
+ };
486
+ /**
487
+ * Output encoding. Defaults to 'png'. JPEG output is opaque — a transparent
488
+ * `background` renders as black; keep the default `#ffffff` (or any opaque color)
489
+ * for JPEG exports.
490
+ */
491
+ format?: 'png' | 'jpeg';
492
+ /** Encoder quality in (0, 1]. Only meaningful for 'jpeg'. */
493
+ quality?: number;
494
+ /**
495
+ * 'exact' (default) throws when scale × bounds exceeds maxDimension/maxPixels.
496
+ * 'fit' reduces the effective scale to the largest value that satisfies both
497
+ * caps (never above the requested scale).
498
+ */
499
+ scaleMode?: 'exact' | 'fit';
472
500
  }
473
501
  type ExportAssetErrorReason = 'load' | 'timeout' | 'encode';
474
502
  interface ExportAssetError {
@@ -532,6 +560,11 @@ declare function getElementStyle(element: CanvasElement): ElementStyle;
532
560
 
533
561
  type RotateDirection = 'cw' | 'ccw';
534
562
 
563
+ interface SelectionStyleDetails {
564
+ common: ElementStyle;
565
+ applicable: readonly (keyof ElementStyle)[];
566
+ mixed: readonly (keyof ElementStyle)[];
567
+ }
535
568
  type AlignEdge = 'left' | 'center-x' | 'right' | 'top' | 'middle' | 'bottom';
536
569
  type DistributeAxis = 'horizontal' | 'vertical';
537
570
 
@@ -611,6 +644,11 @@ declare class Viewport {
611
644
  private minimap;
612
645
  private readonly htmlRenderers;
613
646
  private readonly resizeListeners;
647
+ private readonly selectionListeners;
648
+ private detachSelectionSource;
649
+ private unsubToolRegister;
650
+ private pendingSelectionPrune;
651
+ private unsubRecorderEnd;
614
652
  constructor(container: HTMLElement, options?: ViewportOptions);
615
653
  get ctx(): CanvasRenderingContext2D | null;
616
654
  get snapToGrid(): boolean;
@@ -704,12 +742,34 @@ declare class Viewport {
704
742
  getGridInfo(): GridInfo | null;
705
743
  onGridChange(listener: (info: GridInfo | null) => void): () => void;
706
744
  private getSelectTool;
745
+ private pruneSelection;
746
+ private handleRemovedElement;
747
+ private static isSelectionSource;
748
+ private emitSelectionChange;
749
+ private attachSelectionSource;
750
+ /**
751
+ * getSelectedIds() and the onSelectionChange emitter never surface stale ids:
752
+ * once the enclosing history transaction completes, both reflect
753
+ * the current selection.
754
+ */
707
755
  getSelectedIds(): string[];
708
756
  runAction(action: string): void;
709
757
  canPaste(): boolean;
710
758
  openContextMenu(screenPos: Point): void;
759
+ /**
760
+ * Persistent, viewport-owned selection-change emitter. Subscribing works
761
+ * regardless of whether a select tool is registered yet; it forwards
762
+ * events from whichever select tool is currently attached via
763
+ * `toolManager.onRegister`. Never delivers stale ids once the enclosing
764
+ * history transaction completes.
765
+ */
711
766
  onSelectionChange(listener: () => void): () => void;
712
767
  getSelectionStyle(): ElementStyle | null;
768
+ /**
769
+ * Unlike `getSelectionStyle()` — which returns `{}` for a style-less
770
+ * selection — this returns `null` when no style field is applicable.
771
+ */
772
+ getSelectionStyleDetails(): SelectionStyleDetails | null;
713
773
  applyStyleToSelection(style: ElementStyle): void;
714
774
  groupSelection(): void;
715
775
  ungroupSelection(): void;
@@ -1729,6 +1789,6 @@ declare class TemplateTool implements Tool {
1729
1789
  private notifyOptionsChange;
1730
1790
  }
1731
1791
 
1732
- declare const VERSION = "0.58.0";
1792
+ declare const VERSION = "0.60.0";
1733
1793
 
1734
- 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 };
1794
+ 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 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, 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 };
package/dist/index.js CHANGED
@@ -5170,6 +5170,7 @@ var ToolManager = class {
5170
5170
  tools = /* @__PURE__ */ new Map();
5171
5171
  current = null;
5172
5172
  changeListeners = /* @__PURE__ */ new Set();
5173
+ registerListeners = /* @__PURE__ */ new Set();
5173
5174
  get activeTool() {
5174
5175
  return this.current;
5175
5176
  }
@@ -5178,6 +5179,7 @@ var ToolManager = class {
5178
5179
  }
5179
5180
  register(tool) {
5180
5181
  this.tools.set(tool.name, tool);
5182
+ this.registerListeners.forEach((fn) => fn(tool));
5181
5183
  }
5182
5184
  getTool(name) {
5183
5185
  return this.tools.get(name);
@@ -5203,6 +5205,10 @@ var ToolManager = class {
5203
5205
  this.changeListeners.add(listener);
5204
5206
  return () => this.changeListeners.delete(listener);
5205
5207
  }
5208
+ onRegister(listener) {
5209
+ this.registerListeners.add(listener);
5210
+ return () => this.registerListeners.delete(listener);
5211
+ }
5206
5212
  };
5207
5213
 
5208
5214
  // src/history/history-stack.ts
@@ -5394,6 +5400,7 @@ var HistoryRecorder = class {
5394
5400
  generation = 0;
5395
5401
  updateSnapshots = /* @__PURE__ */ new Map();
5396
5402
  unsubscribers;
5403
+ transactionEndListeners = /* @__PURE__ */ new Set();
5397
5404
  pause() {
5398
5405
  this.recording = false;
5399
5406
  }
@@ -5411,19 +5418,35 @@ var HistoryRecorder = class {
5411
5418
  get currentTransactionId() {
5412
5419
  return this.transaction !== null ? this.generation : null;
5413
5420
  }
5421
+ onTransactionEnd(listener) {
5422
+ this.transactionEndListeners.add(listener);
5423
+ return () => this.transactionEndListeners.delete(listener);
5424
+ }
5425
+ notifyTransactionEnd() {
5426
+ for (const listener of this.transactionEndListeners) {
5427
+ try {
5428
+ listener();
5429
+ } catch {
5430
+ }
5431
+ }
5432
+ }
5414
5433
  commit() {
5415
5434
  if (!this.transaction) return;
5416
5435
  const finalCommands = this.flushUpdateSnapshots();
5417
5436
  const all = [...this.transaction, ...finalCommands];
5418
5437
  this.transaction = null;
5419
5438
  this.updateSnapshots.clear();
5420
- if (all.length === 0) return;
5421
- const first = all[0];
5422
- this.stack.push(all.length === 1 && first ? first : new BatchCommand(all));
5439
+ if (all.length > 0) {
5440
+ const first = all[0];
5441
+ this.stack.push(all.length === 1 && first ? first : new BatchCommand(all));
5442
+ }
5443
+ this.notifyTransactionEnd();
5423
5444
  }
5424
5445
  rollback() {
5446
+ const wasOpen = this.transaction !== null;
5425
5447
  this.transaction = null;
5426
5448
  this.updateSnapshots.clear();
5449
+ if (wasOpen) this.notifyTransactionEnd();
5427
5450
  }
5428
5451
  destroy() {
5429
5452
  this.unsubscribers.forEach((fn) => fn());
@@ -5753,6 +5776,19 @@ function computeBounds(elements, padding) {
5753
5776
  h: maxY - minY + padding * 2
5754
5777
  };
5755
5778
  }
5779
+ function resolveExportBounds(region, elements, padding) {
5780
+ if (!region) return computeBounds(elements, padding);
5781
+ const finite = Number.isFinite(region.x) && Number.isFinite(region.y) && Number.isFinite(region.w) && Number.isFinite(region.h);
5782
+ if (!finite || region.w <= 0 || region.h <= 0) {
5783
+ throw new RangeError("region must have finite coordinates and positive w/h");
5784
+ }
5785
+ return {
5786
+ x: region.x - padding,
5787
+ y: region.y - padding,
5788
+ w: region.w + padding * 2,
5789
+ h: region.h + padding * 2
5790
+ };
5791
+ }
5756
5792
  function renderGridForBounds(ctx, grid, bounds) {
5757
5793
  const visibleBounds = {
5758
5794
  minX: bounds.x,
@@ -5795,6 +5831,29 @@ function nonNegativeOption(value, fallback, name) {
5795
5831
  }
5796
5832
  return resolved;
5797
5833
  }
5834
+ function fitExportScale(bounds, requestedScale, options) {
5835
+ const maxDimension = positiveOption(options.maxDimension, DEFAULT_MAX_DIMENSION, "maxDimension");
5836
+ const maxPixels = positiveOption(options.maxPixels, DEFAULT_MAX_PIXELS, "maxPixels");
5837
+ const fits = (scale) => {
5838
+ const w = Math.ceil(bounds.w * scale);
5839
+ const h = Math.ceil(bounds.h * scale);
5840
+ return w <= maxDimension && h <= maxDimension && w * h <= maxPixels;
5841
+ };
5842
+ if (fits(requestedScale)) return requestedScale;
5843
+ const analytical = Math.min(
5844
+ maxDimension / Math.max(bounds.w, bounds.h),
5845
+ Math.sqrt(maxPixels) / Math.sqrt(bounds.w) / Math.sqrt(bounds.h)
5846
+ );
5847
+ let hi = Math.min(requestedScale, analytical);
5848
+ if (fits(hi)) return hi;
5849
+ let lo = 0;
5850
+ for (let i = 0; i < 40; i++) {
5851
+ const mid = (lo + hi) / 2;
5852
+ if (fits(mid)) lo = mid;
5853
+ else hi = mid;
5854
+ }
5855
+ return lo;
5856
+ }
5798
5857
  function assertExportSize(width, height, options) {
5799
5858
  const maxDimension = positiveOption(options.maxDimension, DEFAULT_MAX_DIMENSION, "maxDimension");
5800
5859
  const maxPixels = positiveOption(options.maxPixels, DEFAULT_MAX_PIXELS, "maxPixels");
@@ -5869,10 +5928,23 @@ function loadImages(elements, options = {}) {
5869
5928
  });
5870
5929
  }
5871
5930
  async function exportImage(store, options = {}, layerManager) {
5872
- const scale = positiveOption(options.scale, 2, "scale");
5931
+ const requestedScale = positiveOption(options.scale, 2, "scale");
5932
+ const scaleMode = options.scaleMode ?? "exact";
5933
+ if (scaleMode !== "exact" && scaleMode !== "fit") {
5934
+ throw new RangeError(`scaleMode must be 'exact' or 'fit'`);
5935
+ }
5873
5936
  const padding = nonNegativeOption(options.padding, 0, "padding");
5874
5937
  validateExportResourceOptions(options);
5875
5938
  validateHtmlExportOptions(options);
5939
+ const format = options.format ?? "png";
5940
+ if (format !== "png" && format !== "jpeg") {
5941
+ throw new RangeError(`format must be 'png' or 'jpeg'`);
5942
+ }
5943
+ if (options.quality !== void 0) {
5944
+ if (!Number.isFinite(options.quality) || options.quality <= 0 || options.quality > 1) {
5945
+ throw new RangeError("quality must be a finite number in (0, 1]");
5946
+ }
5947
+ }
5876
5948
  const background = options.background ?? "#ffffff";
5877
5949
  const filter = options.filter;
5878
5950
  const allElements = store.getAll();
@@ -5880,8 +5952,9 @@ async function exportImage(store, options = {}, layerManager) {
5880
5952
  if (filter) {
5881
5953
  visibleElements = visibleElements.filter(filter);
5882
5954
  }
5883
- const bounds = computeBounds(visibleElements, padding);
5955
+ const bounds = resolveExportBounds(options.region, visibleElements, padding);
5884
5956
  if (!bounds) return null;
5957
+ const scale = scaleMode === "fit" ? fitExportScale(bounds, requestedScale, options) : requestedScale;
5885
5958
  const width = Math.ceil(bounds.w * scale);
5886
5959
  const height = Math.ceil(bounds.h * scale);
5887
5960
  assertExportSize(width, height, options);
@@ -5983,8 +6056,9 @@ async function exportImage(store, options = {}, layerManager) {
5983
6056
  renderGridForBounds(ctx, grid, bounds);
5984
6057
  ctx.restore();
5985
6058
  }
6059
+ const mimeType = format === "jpeg" ? "image/jpeg" : "image/png";
5986
6060
  return new Promise((resolve) => {
5987
- canvas.toBlob((blob) => resolve(blob), "image/png");
6061
+ canvas.toBlob((blob) => resolve(blob), mimeType, options.quality);
5988
6062
  });
5989
6063
  }
5990
6064
 
@@ -7664,6 +7738,14 @@ function rotateElementPatch(el, bounds, pivot, delta) {
7664
7738
  }
7665
7739
 
7666
7740
  // src/canvas/selection-ops.ts
7741
+ var STYLE_FIELDS = [
7742
+ "color",
7743
+ "fillColor",
7744
+ "strokeWidth",
7745
+ "opacity",
7746
+ "fontSize",
7747
+ "strokeStyle"
7748
+ ];
7667
7749
  function sharedValue(values) {
7668
7750
  const present = values.filter((v) => v !== void 0);
7669
7751
  if (present.length === 0) return void 0;
@@ -7698,6 +7780,42 @@ var SelectionOps = class {
7698
7780
  if (strokeStyle !== void 0) result.strokeStyle = strokeStyle;
7699
7781
  return result;
7700
7782
  }
7783
+ /**
7784
+ * Unlike `getStyle()` — which returns `{}` for a selection whose elements
7785
+ * have no applicable style fields (e.g. images) — this returns `null` when
7786
+ * no field is applicable, so callers can distinguish "nothing to show" from
7787
+ * "everything shared but empty".
7788
+ */
7789
+ getStyleDetails() {
7790
+ const ids = this.deps.getSelectedIds();
7791
+ if (ids.length === 0) return null;
7792
+ const styles = [];
7793
+ for (const id of ids) {
7794
+ const el = this.deps.store.getById(id);
7795
+ if (el) styles.push(getElementStyle(el));
7796
+ }
7797
+ if (styles.length === 0) return null;
7798
+ const common = {};
7799
+ const applicable = [];
7800
+ const mixed = [];
7801
+ for (const field of STYLE_FIELDS) {
7802
+ const allValues = styles.map((s) => s[field]);
7803
+ const values = [];
7804
+ for (const v of allValues) {
7805
+ if (v !== void 0) values.push(v);
7806
+ }
7807
+ if (values.length === 0) continue;
7808
+ applicable.push(field);
7809
+ const distinct = new Set(values);
7810
+ if (distinct.size > 1) {
7811
+ mixed.push(field);
7812
+ } else {
7813
+ common[field] = values[0];
7814
+ }
7815
+ }
7816
+ if (applicable.length === 0) return null;
7817
+ return { common, applicable, mixed };
7818
+ }
7701
7819
  applyStyle(style) {
7702
7820
  const ids = this.deps.getSelectedIds();
7703
7821
  if (ids.length === 0) return;
@@ -8072,7 +8190,7 @@ var ViewportInteractions = class {
8072
8190
  var EMPTY_IDS = [];
8073
8191
  function noop() {
8074
8192
  }
8075
- var Viewport = class {
8193
+ var Viewport = class _Viewport {
8076
8194
  constructor(container, options = {}) {
8077
8195
  this.container = container;
8078
8196
  this.camera = new Camera(options.camera);
@@ -8081,6 +8199,13 @@ var Viewport = class {
8081
8199
  this.store = new ElementStore();
8082
8200
  this.layerManager = new LayerManager(this.store);
8083
8201
  this.toolManager = new ToolManager();
8202
+ this.unsubToolRegister = this.toolManager.onRegister((tool) => {
8203
+ if (_Viewport.isSelectionSource(tool)) this.attachSelectionSource(tool);
8204
+ });
8205
+ const existingSelect = this.getSelectTool();
8206
+ if (existingSelect && _Viewport.isSelectionSource(existingSelect)) {
8207
+ this.attachSelectionSource(existingSelect);
8208
+ }
8084
8209
  this.renderer = new ElementRenderer();
8085
8210
  this.renderer.setStore(this.store);
8086
8211
  this.renderer.setCamera(this.camera);
@@ -8115,6 +8240,11 @@ var Viewport = class {
8115
8240
  this.dropHandler = options.onDrop;
8116
8241
  this.history = new HistoryStack();
8117
8242
  this.historyRecorder = new HistoryRecorder(this.store, this.history, this.layerManager);
8243
+ this.unsubRecorderEnd = this.historyRecorder.onTransactionEnd(() => {
8244
+ if (!this.pendingSelectionPrune) return;
8245
+ this.pendingSelectionPrune = false;
8246
+ this.pruneSelection();
8247
+ });
8118
8248
  this.selectionOps = new SelectionOps({
8119
8249
  store: this.store,
8120
8250
  recorder: this.historyRecorder,
@@ -8234,6 +8364,7 @@ var Viewport = class {
8234
8364
  this.domNodeManager.removeDomNode(el.id);
8235
8365
  this.renderLoop.markLayerDirty(el.layerId);
8236
8366
  this.requestRender();
8367
+ this.handleRemovedElement(el.id);
8237
8368
  }),
8238
8369
  this.store.on("update", ({ previous, current }) => {
8239
8370
  if (current.type === "grid") this.gridController.syncContext();
@@ -8248,6 +8379,7 @@ var Viewport = class {
8248
8379
  this.renderLoop.markAllLayersDirty();
8249
8380
  this.gridController.syncContext();
8250
8381
  this.requestRender();
8382
+ this.pruneSelection();
8251
8383
  })
8252
8384
  ];
8253
8385
  this.layerManager.on("change", () => {
@@ -8317,6 +8449,13 @@ var Viewport = class {
8317
8449
  minimap = null;
8318
8450
  htmlRenderers = /* @__PURE__ */ new Map();
8319
8451
  resizeListeners = /* @__PURE__ */ new Set();
8452
+ selectionListeners = /* @__PURE__ */ new Set();
8453
+ detachSelectionSource = null;
8454
+ unsubToolRegister = () => {
8455
+ };
8456
+ pendingSelectionPrune = false;
8457
+ unsubRecorderEnd = () => {
8458
+ };
8320
8459
  get ctx() {
8321
8460
  return this.canvasEl.getContext("2d");
8322
8461
  }
@@ -8575,6 +8714,42 @@ var Viewport = class {
8575
8714
  getSelectTool() {
8576
8715
  return this.toolManager.getTool("select");
8577
8716
  }
8717
+ pruneSelection() {
8718
+ const tool = this.getSelectTool();
8719
+ if (!tool) return;
8720
+ const ids = tool.selectedIds;
8721
+ const filtered = ids.filter((id) => this.store.getById(id) !== void 0);
8722
+ if (filtered.length !== ids.length) tool.setSelection(filtered);
8723
+ }
8724
+ handleRemovedElement(id) {
8725
+ if (!this.getSelectedIds().includes(id)) return;
8726
+ if (this.historyRecorder.currentTransactionId !== null) {
8727
+ this.pendingSelectionPrune = true;
8728
+ return;
8729
+ }
8730
+ this.pruneSelection();
8731
+ }
8732
+ static isSelectionSource(tool) {
8733
+ const candidate = tool;
8734
+ return tool.name === "select" && typeof candidate.onSelectionChange === "function" && typeof candidate.setSelection === "function";
8735
+ }
8736
+ emitSelectionChange() {
8737
+ for (const listener of this.selectionListeners) {
8738
+ try {
8739
+ listener();
8740
+ } catch {
8741
+ }
8742
+ }
8743
+ }
8744
+ attachSelectionSource(tool) {
8745
+ this.detachSelectionSource?.();
8746
+ this.detachSelectionSource = tool.onSelectionChange(() => this.emitSelectionChange());
8747
+ }
8748
+ /**
8749
+ * getSelectedIds() and the onSelectionChange emitter never surface stale ids:
8750
+ * once the enclosing history transaction completes, both reflect
8751
+ * the current selection.
8752
+ */
8578
8753
  getSelectedIds() {
8579
8754
  return this.getSelectTool()?.selectedIds ?? EMPTY_IDS;
8580
8755
  }
@@ -8608,13 +8783,29 @@ var Viewport = class {
8608
8783
  if (items.length === 0) return;
8609
8784
  this.contextMenu.open(items, screenPos);
8610
8785
  }
8786
+ /**
8787
+ * Persistent, viewport-owned selection-change emitter. Subscribing works
8788
+ * regardless of whether a select tool is registered yet; it forwards
8789
+ * events from whichever select tool is currently attached via
8790
+ * `toolManager.onRegister`. Never delivers stale ids once the enclosing
8791
+ * history transaction completes.
8792
+ */
8611
8793
  onSelectionChange(listener) {
8612
- const tool = this.getSelectTool();
8613
- return tool ? tool.onSelectionChange(listener) : noop;
8794
+ this.selectionListeners.add(listener);
8795
+ return () => {
8796
+ this.selectionListeners.delete(listener);
8797
+ };
8614
8798
  }
8615
8799
  getSelectionStyle() {
8616
8800
  return this.selectionOps.getStyle();
8617
8801
  }
8802
+ /**
8803
+ * Unlike `getSelectionStyle()` — which returns `{}` for a style-less
8804
+ * selection — this returns `null` when no style field is applicable.
8805
+ */
8806
+ getSelectionStyleDetails() {
8807
+ return this.selectionOps.getStyleDetails();
8808
+ }
8618
8809
  applyStyleToSelection(style) {
8619
8810
  this.selectionOps.applyStyle(style);
8620
8811
  }
@@ -8663,6 +8854,11 @@ var Viewport = class {
8663
8854
  this.inputHandler.destroy();
8664
8855
  this.unsubCamera();
8665
8856
  this.unsubToolChange();
8857
+ this.unsubToolRegister();
8858
+ this.unsubRecorderEnd();
8859
+ this.detachSelectionSource?.();
8860
+ this.detachSelectionSource = null;
8861
+ this.selectionListeners.clear();
8666
8862
  this.unsubStore.forEach((fn) => fn());
8667
8863
  this.resizeObserver?.disconnect();
8668
8864
  this.resizeObserver = null;
@@ -12264,7 +12460,7 @@ var PingTool = class {
12264
12460
  };
12265
12461
 
12266
12462
  // src/index.ts
12267
- var VERSION = "0.58.0";
12463
+ var VERSION = "0.60.0";
12268
12464
  export {
12269
12465
  ArrowTool,
12270
12466
  AutoSave,