@tsdraw/core 0.4.0 → 0.5.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
@@ -39,9 +39,53 @@ declare const DRAG_DISTANCE_SQUARED = 36;
39
39
  declare const DEFAULT_COLORS: Record<string, string>;
40
40
  declare const MAX_POINTS_PER_SHAPE = 200;
41
41
 
42
+ interface TsdrawPageRecord {
43
+ id: string;
44
+ typeName: 'page';
45
+ pageId: string;
46
+ shapeIds: ShapeId[];
47
+ erasingShapeIds: ShapeId[];
48
+ }
49
+ interface TsdrawShapeRecord {
50
+ id: ShapeId;
51
+ typeName: 'shape';
52
+ shape: Shape;
53
+ }
54
+ type TsdrawPersistedRecord = TsdrawPageRecord | TsdrawShapeRecord;
55
+ interface TsdrawDocumentSnapshot {
56
+ records: TsdrawPersistedRecord[];
57
+ }
58
+ interface TsdrawSessionStateSnapshot {
59
+ version: 1;
60
+ viewport: {
61
+ x: number;
62
+ y: number;
63
+ zoom: number;
64
+ };
65
+ currentToolId: string;
66
+ drawStyle: {
67
+ color: ColorStyle;
68
+ dash: DashStyle;
69
+ size: SizeStyle;
70
+ };
71
+ selectedShapeIds: ShapeId[];
72
+ }
73
+ interface TsdrawEditorSnapshot {
74
+ document: TsdrawDocumentSnapshot;
75
+ state: TsdrawSessionStateSnapshot;
76
+ }
77
+ interface DocumentStoreSnapshot {
78
+ page: PageState;
79
+ order: ShapeId[];
80
+ }
81
+ declare function documentSnapshotToRecords(snapshot: DocumentStoreSnapshot): TsdrawPersistedRecord[];
82
+ declare function recordsToDocumentSnapshot(records: TsdrawPersistedRecord[]): DocumentStoreSnapshot | null;
83
+
84
+ type DocumentStoreListener = () => void;
42
85
  declare class DocumentStore {
43
86
  private state;
44
87
  private order;
88
+ private readonly listeners;
45
89
  getPage(): PageState;
46
90
  getShape(id: ShapeId): Shape | undefined;
47
91
  getCurrentPageShapesSorted(): Shape[];
@@ -58,6 +102,10 @@ declare class DocumentStore {
58
102
  maxX: number;
59
103
  maxY: number;
60
104
  }): Set<ShapeId>;
105
+ getSnapshot(): DocumentStoreSnapshot;
106
+ loadSnapshot(snapshot: DocumentStoreSnapshot): void;
107
+ listen(listener: DocumentStoreListener): () => void;
108
+ private emitChange;
61
109
  }
62
110
 
63
111
  interface PointerInput {
@@ -228,6 +276,7 @@ interface EditorOptions {
228
276
  toolDefinitions?: ToolDefinition[];
229
277
  initialToolId?: ToolId;
230
278
  }
279
+ type EditorListener = () => void;
231
280
  declare class Editor {
232
281
  readonly store: DocumentStore;
233
282
  readonly input: InputManager;
@@ -239,6 +288,7 @@ declare class Editor {
239
288
  };
240
289
  private drawStyle;
241
290
  private readonly toolStateContext;
291
+ private readonly listeners;
242
292
  constructor(opts?: EditorOptions);
243
293
  registerToolDefinition(toolDefinition: ToolDefinition): void;
244
294
  private getDefaultToolDefinitions;
@@ -269,12 +319,25 @@ declare class Editor {
269
319
  dash: DashStyle;
270
320
  size: SizeStyle;
271
321
  }>): void;
322
+ setViewport(partial: Partial<Viewport>): void;
272
323
  panBy(dx: number, dy: number): void;
324
+ getDocumentSnapshot(): TsdrawDocumentSnapshot;
325
+ loadDocumentSnapshot(snapshot: TsdrawDocumentSnapshot): void;
326
+ getSessionStateSnapshot(args?: {
327
+ selectedShapeIds?: ShapeId[];
328
+ }): TsdrawSessionStateSnapshot;
329
+ loadSessionStateSnapshot(snapshot: TsdrawSessionStateSnapshot): ShapeId[];
330
+ getPersistenceSnapshot(args?: {
331
+ selectedShapeIds?: ShapeId[];
332
+ }): TsdrawEditorSnapshot;
333
+ loadPersistenceSnapshot(snapshot: Partial<TsdrawEditorSnapshot>): ShapeId[];
334
+ listen(listener: EditorListener): () => void;
273
335
  screenToPage(screenX: number, screenY: number): {
274
336
  x: number;
275
337
  y: number;
276
338
  };
277
339
  render(ctx: CanvasRenderingContext2D): void;
340
+ private emitChange;
278
341
  }
279
342
 
280
343
  declare class PenIdleState extends StateNode {
@@ -461,4 +524,4 @@ declare function decodePathToPoints(segments: {
461
524
  y: number;
462
525
  }[];
463
526
 
464
- export { type Bounds, CanvasRenderer, type ColorStyle, DEFAULT_COLORS, DRAG_DISTANCE_SQUARED, type DashStyle, type DefaultToolId, DocumentStore, type DrawSegment, type DrawShape, ERASER_MARGIN, Editor, type EditorOptions, EraserErasingState, EraserIdleState, EraserPointingState, HandDraggingState, HandIdleState, type ICanvasRenderer, type IEditor, InputManager, MAX_POINTS_PER_SHAPE, type PageState, PenDrawingState, PenIdleState, type PointerInput, type ResizeHandle, STROKE_WIDTHS, type SegmentType, SelectIdleState, type SelectionBounds, type Shape, type ShapeId, type SizeStyle, StateNode, type StateNodeConstructor, type ToolDefinition, type ToolId, type ToolKeyInfo, ToolManager, type ToolPointerDownInfo, type ToolPointerMoveInfo, type ToolStateContext, type ToolStateTransitionInfo, type TransformSnapshot, type TsdrawRenderTheme, type Vec3, type Viewport, applyMove, applyResize, applyRotation, boundsContainPoint, boundsIntersect, boundsOf, buildStartPositions, buildTransformSnapshots, closestOnSegment, createViewport, decodeFirstPoint, decodeLastPoint, decodePathToPoints, decodePoints, distance, encodePoints, getSelectionBoundsPage, getShapeBounds, getShapesInBounds, getTopShapeAtPoint, isSelectTool, minDistanceToPolyline, normalizeSelectionBounds, padBounds, pageToScreen, panViewport, pointHitsShape, resolveThemeColor, rotatePoint, screenToPage, segmentHitsShape, segmentTouchesPolyline, setViewport, shapePagePoints, sqDistance, zoomViewport };
527
+ export { type Bounds, CanvasRenderer, type ColorStyle, DEFAULT_COLORS, DRAG_DISTANCE_SQUARED, type DashStyle, type DefaultToolId, DocumentStore, type DocumentStoreSnapshot, type DrawSegment, type DrawShape, ERASER_MARGIN, Editor, type EditorOptions, EraserErasingState, EraserIdleState, EraserPointingState, HandDraggingState, HandIdleState, type ICanvasRenderer, type IEditor, InputManager, MAX_POINTS_PER_SHAPE, type PageState, PenDrawingState, PenIdleState, type PointerInput, type ResizeHandle, STROKE_WIDTHS, type SegmentType, SelectIdleState, type SelectionBounds, type Shape, type ShapeId, type SizeStyle, StateNode, type StateNodeConstructor, type ToolDefinition, type ToolId, type ToolKeyInfo, ToolManager, type ToolPointerDownInfo, type ToolPointerMoveInfo, type ToolStateContext, type ToolStateTransitionInfo, type TransformSnapshot, type TsdrawDocumentSnapshot, type TsdrawEditorSnapshot, type TsdrawPageRecord, type TsdrawPersistedRecord, type TsdrawRenderTheme, type TsdrawSessionStateSnapshot, type TsdrawShapeRecord, type Vec3, type Viewport, applyMove, applyResize, applyRotation, boundsContainPoint, boundsIntersect, boundsOf, buildStartPositions, buildTransformSnapshots, closestOnSegment, createViewport, decodeFirstPoint, decodeLastPoint, decodePathToPoints, decodePoints, distance, documentSnapshotToRecords, encodePoints, getSelectionBoundsPage, getShapeBounds, getShapesInBounds, getTopShapeAtPoint, isSelectTool, minDistanceToPolyline, normalizeSelectionBounds, padBounds, pageToScreen, panViewport, pointHitsShape, recordsToDocumentSnapshot, resolveThemeColor, rotatePoint, screenToPage, segmentHitsShape, segmentTouchesPolyline, setViewport, shapePagePoints, sqDistance, zoomViewport };
package/dist/index.d.ts CHANGED
@@ -39,9 +39,53 @@ declare const DRAG_DISTANCE_SQUARED = 36;
39
39
  declare const DEFAULT_COLORS: Record<string, string>;
40
40
  declare const MAX_POINTS_PER_SHAPE = 200;
41
41
 
42
+ interface TsdrawPageRecord {
43
+ id: string;
44
+ typeName: 'page';
45
+ pageId: string;
46
+ shapeIds: ShapeId[];
47
+ erasingShapeIds: ShapeId[];
48
+ }
49
+ interface TsdrawShapeRecord {
50
+ id: ShapeId;
51
+ typeName: 'shape';
52
+ shape: Shape;
53
+ }
54
+ type TsdrawPersistedRecord = TsdrawPageRecord | TsdrawShapeRecord;
55
+ interface TsdrawDocumentSnapshot {
56
+ records: TsdrawPersistedRecord[];
57
+ }
58
+ interface TsdrawSessionStateSnapshot {
59
+ version: 1;
60
+ viewport: {
61
+ x: number;
62
+ y: number;
63
+ zoom: number;
64
+ };
65
+ currentToolId: string;
66
+ drawStyle: {
67
+ color: ColorStyle;
68
+ dash: DashStyle;
69
+ size: SizeStyle;
70
+ };
71
+ selectedShapeIds: ShapeId[];
72
+ }
73
+ interface TsdrawEditorSnapshot {
74
+ document: TsdrawDocumentSnapshot;
75
+ state: TsdrawSessionStateSnapshot;
76
+ }
77
+ interface DocumentStoreSnapshot {
78
+ page: PageState;
79
+ order: ShapeId[];
80
+ }
81
+ declare function documentSnapshotToRecords(snapshot: DocumentStoreSnapshot): TsdrawPersistedRecord[];
82
+ declare function recordsToDocumentSnapshot(records: TsdrawPersistedRecord[]): DocumentStoreSnapshot | null;
83
+
84
+ type DocumentStoreListener = () => void;
42
85
  declare class DocumentStore {
43
86
  private state;
44
87
  private order;
88
+ private readonly listeners;
45
89
  getPage(): PageState;
46
90
  getShape(id: ShapeId): Shape | undefined;
47
91
  getCurrentPageShapesSorted(): Shape[];
@@ -58,6 +102,10 @@ declare class DocumentStore {
58
102
  maxX: number;
59
103
  maxY: number;
60
104
  }): Set<ShapeId>;
105
+ getSnapshot(): DocumentStoreSnapshot;
106
+ loadSnapshot(snapshot: DocumentStoreSnapshot): void;
107
+ listen(listener: DocumentStoreListener): () => void;
108
+ private emitChange;
61
109
  }
62
110
 
63
111
  interface PointerInput {
@@ -228,6 +276,7 @@ interface EditorOptions {
228
276
  toolDefinitions?: ToolDefinition[];
229
277
  initialToolId?: ToolId;
230
278
  }
279
+ type EditorListener = () => void;
231
280
  declare class Editor {
232
281
  readonly store: DocumentStore;
233
282
  readonly input: InputManager;
@@ -239,6 +288,7 @@ declare class Editor {
239
288
  };
240
289
  private drawStyle;
241
290
  private readonly toolStateContext;
291
+ private readonly listeners;
242
292
  constructor(opts?: EditorOptions);
243
293
  registerToolDefinition(toolDefinition: ToolDefinition): void;
244
294
  private getDefaultToolDefinitions;
@@ -269,12 +319,25 @@ declare class Editor {
269
319
  dash: DashStyle;
270
320
  size: SizeStyle;
271
321
  }>): void;
322
+ setViewport(partial: Partial<Viewport>): void;
272
323
  panBy(dx: number, dy: number): void;
324
+ getDocumentSnapshot(): TsdrawDocumentSnapshot;
325
+ loadDocumentSnapshot(snapshot: TsdrawDocumentSnapshot): void;
326
+ getSessionStateSnapshot(args?: {
327
+ selectedShapeIds?: ShapeId[];
328
+ }): TsdrawSessionStateSnapshot;
329
+ loadSessionStateSnapshot(snapshot: TsdrawSessionStateSnapshot): ShapeId[];
330
+ getPersistenceSnapshot(args?: {
331
+ selectedShapeIds?: ShapeId[];
332
+ }): TsdrawEditorSnapshot;
333
+ loadPersistenceSnapshot(snapshot: Partial<TsdrawEditorSnapshot>): ShapeId[];
334
+ listen(listener: EditorListener): () => void;
273
335
  screenToPage(screenX: number, screenY: number): {
274
336
  x: number;
275
337
  y: number;
276
338
  };
277
339
  render(ctx: CanvasRenderingContext2D): void;
340
+ private emitChange;
278
341
  }
279
342
 
280
343
  declare class PenIdleState extends StateNode {
@@ -461,4 +524,4 @@ declare function decodePathToPoints(segments: {
461
524
  y: number;
462
525
  }[];
463
526
 
464
- export { type Bounds, CanvasRenderer, type ColorStyle, DEFAULT_COLORS, DRAG_DISTANCE_SQUARED, type DashStyle, type DefaultToolId, DocumentStore, type DrawSegment, type DrawShape, ERASER_MARGIN, Editor, type EditorOptions, EraserErasingState, EraserIdleState, EraserPointingState, HandDraggingState, HandIdleState, type ICanvasRenderer, type IEditor, InputManager, MAX_POINTS_PER_SHAPE, type PageState, PenDrawingState, PenIdleState, type PointerInput, type ResizeHandle, STROKE_WIDTHS, type SegmentType, SelectIdleState, type SelectionBounds, type Shape, type ShapeId, type SizeStyle, StateNode, type StateNodeConstructor, type ToolDefinition, type ToolId, type ToolKeyInfo, ToolManager, type ToolPointerDownInfo, type ToolPointerMoveInfo, type ToolStateContext, type ToolStateTransitionInfo, type TransformSnapshot, type TsdrawRenderTheme, type Vec3, type Viewport, applyMove, applyResize, applyRotation, boundsContainPoint, boundsIntersect, boundsOf, buildStartPositions, buildTransformSnapshots, closestOnSegment, createViewport, decodeFirstPoint, decodeLastPoint, decodePathToPoints, decodePoints, distance, encodePoints, getSelectionBoundsPage, getShapeBounds, getShapesInBounds, getTopShapeAtPoint, isSelectTool, minDistanceToPolyline, normalizeSelectionBounds, padBounds, pageToScreen, panViewport, pointHitsShape, resolveThemeColor, rotatePoint, screenToPage, segmentHitsShape, segmentTouchesPolyline, setViewport, shapePagePoints, sqDistance, zoomViewport };
527
+ export { type Bounds, CanvasRenderer, type ColorStyle, DEFAULT_COLORS, DRAG_DISTANCE_SQUARED, type DashStyle, type DefaultToolId, DocumentStore, type DocumentStoreSnapshot, type DrawSegment, type DrawShape, ERASER_MARGIN, Editor, type EditorOptions, EraserErasingState, EraserIdleState, EraserPointingState, HandDraggingState, HandIdleState, type ICanvasRenderer, type IEditor, InputManager, MAX_POINTS_PER_SHAPE, type PageState, PenDrawingState, PenIdleState, type PointerInput, type ResizeHandle, STROKE_WIDTHS, type SegmentType, SelectIdleState, type SelectionBounds, type Shape, type ShapeId, type SizeStyle, StateNode, type StateNodeConstructor, type ToolDefinition, type ToolId, type ToolKeyInfo, ToolManager, type ToolPointerDownInfo, type ToolPointerMoveInfo, type ToolStateContext, type ToolStateTransitionInfo, type TransformSnapshot, type TsdrawDocumentSnapshot, type TsdrawEditorSnapshot, type TsdrawPageRecord, type TsdrawPersistedRecord, type TsdrawRenderTheme, type TsdrawSessionStateSnapshot, type TsdrawShapeRecord, type Vec3, type Viewport, applyMove, applyResize, applyRotation, boundsContainPoint, boundsIntersect, boundsOf, buildStartPositions, buildTransformSnapshots, closestOnSegment, createViewport, decodeFirstPoint, decodeLastPoint, decodePathToPoints, decodePoints, distance, documentSnapshotToRecords, encodePoints, getSelectionBoundsPage, getShapeBounds, getShapesInBounds, getTopShapeAtPoint, isSelectTool, minDistanceToPolyline, normalizeSelectionBounds, padBounds, pageToScreen, panViewport, pointHitsShape, recordsToDocumentSnapshot, resolveThemeColor, rotatePoint, screenToPage, segmentHitsShape, segmentTouchesPolyline, setViewport, shapePagePoints, sqDistance, zoomViewport };
package/dist/index.js CHANGED
@@ -69,6 +69,12 @@ function decodePathToPoints(segments, ox, oy) {
69
69
  }
70
70
 
71
71
  // src/store/documentStore.ts
72
+ function cloneValue(value) {
73
+ if (typeof structuredClone === "function") {
74
+ return structuredClone(value);
75
+ }
76
+ return JSON.parse(JSON.stringify(value));
77
+ }
72
78
  var DocumentStore = class {
73
79
  state = {
74
80
  id: "page-1",
@@ -76,6 +82,7 @@ var DocumentStore = class {
76
82
  erasingShapeIds: []
77
83
  };
78
84
  order = [];
85
+ listeners = /* @__PURE__ */ new Set();
79
86
  getPage() {
80
87
  return this.state;
81
88
  }
@@ -96,15 +103,18 @@ var DocumentStore = class {
96
103
  }
97
104
  setErasingShapes(ids) {
98
105
  this.state.erasingShapeIds = ids;
106
+ this.emitChange();
99
107
  }
100
108
  createShape(shape) {
101
109
  this.state.shapes[shape.id] = shape;
102
110
  this.order.push(shape.id);
111
+ this.emitChange();
103
112
  }
104
113
  updateShape(id, partial) {
105
114
  const existing = this.state.shapes[id];
106
115
  if (!existing) return;
107
116
  this.state.shapes[id] = { ...existing, ...partial, id };
117
+ this.emitChange();
108
118
  }
109
119
  deleteShapes(ids) {
110
120
  for (const id of ids) {
@@ -112,6 +122,7 @@ var DocumentStore = class {
112
122
  this.order = this.order.filter((i) => i !== id);
113
123
  }
114
124
  this.state.erasingShapeIds = this.state.erasingShapeIds.filter((i) => !ids.includes(i));
125
+ this.emitChange();
115
126
  }
116
127
  getCurrentPageShapes() {
117
128
  return Object.values(this.state.shapes);
@@ -127,6 +138,35 @@ var DocumentStore = class {
127
138
  }
128
139
  return ids;
129
140
  }
141
+ getSnapshot() {
142
+ return {
143
+ page: cloneValue(this.state),
144
+ order: [...this.order]
145
+ };
146
+ }
147
+ // Load snapshot into the document when loading a persistence snapshot (so on page reload)
148
+ loadSnapshot(snapshot) {
149
+ const pageState = cloneValue(snapshot.page);
150
+ const normalizedOrder = [...snapshot.order].filter((shapeId) => pageState.shapes[shapeId] != null);
151
+ this.state = {
152
+ id: pageState.id,
153
+ shapes: pageState.shapes,
154
+ erasingShapeIds: pageState.erasingShapeIds.filter((shapeId) => pageState.shapes[shapeId] != null)
155
+ };
156
+ this.order = normalizedOrder;
157
+ this.emitChange();
158
+ }
159
+ listen(listener) {
160
+ this.listeners.add(listener);
161
+ return () => {
162
+ this.listeners.delete(listener);
163
+ };
164
+ }
165
+ emitChange() {
166
+ for (const listener of this.listeners) {
167
+ listener();
168
+ }
169
+ }
130
170
  };
131
171
  function getShapeBounds(shape) {
132
172
  if (shape.type !== "draw") {
@@ -1288,10 +1328,68 @@ var HandDraggingState = class extends StateNode {
1288
1328
  }
1289
1329
  };
1290
1330
 
1331
+ // src/persistence/snapshots.ts
1332
+ var PAGE_RECORD_ID = "page:current";
1333
+ function cloneValue2(value) {
1334
+ if (typeof structuredClone === "function") {
1335
+ return structuredClone(value);
1336
+ }
1337
+ return JSON.parse(JSON.stringify(value));
1338
+ }
1339
+ function asDrawShape(value) {
1340
+ return cloneValue2(value);
1341
+ }
1342
+ function documentSnapshotToRecords(snapshot) {
1343
+ const shapeIds = [...snapshot.order].filter((id) => snapshot.page.shapes[id] != null);
1344
+ const pageRecord = {
1345
+ id: PAGE_RECORD_ID,
1346
+ typeName: "page",
1347
+ pageId: snapshot.page.id,
1348
+ shapeIds,
1349
+ erasingShapeIds: [...snapshot.page.erasingShapeIds]
1350
+ };
1351
+ const shapeRecords = shapeIds.map((shapeId) => snapshot.page.shapes[shapeId]).filter((shape) => shape != null).map((shape) => ({
1352
+ id: shape.id,
1353
+ typeName: "shape",
1354
+ shape: asDrawShape(shape)
1355
+ }));
1356
+ return [pageRecord, ...shapeRecords];
1357
+ }
1358
+ function recordsToDocumentSnapshot(records) {
1359
+ const pageRecord = records.find((record) => record.typeName === "page");
1360
+ if (!pageRecord) {
1361
+ return null;
1362
+ }
1363
+ const shapeRecordMap = /* @__PURE__ */ new Map();
1364
+ for (const record of records) {
1365
+ if (record.typeName === "shape") {
1366
+ shapeRecordMap.set(record.id, record);
1367
+ }
1368
+ }
1369
+ const shapes = {};
1370
+ const order = [];
1371
+ for (const shapeId of pageRecord.shapeIds) {
1372
+ const shapeRecord = shapeRecordMap.get(shapeId);
1373
+ if (!shapeRecord) continue;
1374
+ shapes[shapeId] = asDrawShape(shapeRecord.shape);
1375
+ order.push(shapeId);
1376
+ }
1377
+ return {
1378
+ page: {
1379
+ id: pageRecord.pageId,
1380
+ shapes,
1381
+ erasingShapeIds: [...pageRecord.erasingShapeIds].filter((shapeId) => shapes[shapeId] != null)
1382
+ },
1383
+ order
1384
+ };
1385
+ }
1386
+
1291
1387
  // src/editor/Editor.ts
1292
1388
  var shapeIdCounter = 0;
1389
+ var shapeIdRuntimeSeed = Math.random().toString(36).slice(2, 8);
1293
1390
  function createShapeId() {
1294
- return `shape:${String(++shapeIdCounter).padStart(6, "0")}`;
1391
+ shapeIdCounter += 1;
1392
+ return `shape:${Date.now().toString(36)}-${shapeIdRuntimeSeed}-${shapeIdCounter.toString(36)}`;
1295
1393
  }
1296
1394
  var Editor = class {
1297
1395
  store = new DocumentStore();
@@ -1307,9 +1405,11 @@ var Editor = class {
1307
1405
  size: "m"
1308
1406
  };
1309
1407
  toolStateContext;
1408
+ listeners = /* @__PURE__ */ new Set();
1310
1409
  // Creates a new editor instance with the given options (with defaults if not provided)
1311
1410
  constructor(opts = {}) {
1312
1411
  this.options = { dragDistanceSquared: opts.dragDistanceSquared ?? DRAG_DISTANCE_SQUARED };
1412
+ this.store.listen(() => this.emitChange());
1313
1413
  this.toolStateContext = {
1314
1414
  transition: (id, info) => this.tools.transition(id, info)
1315
1415
  };
@@ -1319,7 +1419,7 @@ var Editor = class {
1319
1419
  for (const customTool of opts.toolDefinitions ?? []) {
1320
1420
  this.registerToolDefinition(customTool);
1321
1421
  }
1322
- this.tools.setCurrentTool(opts.initialToolId ?? "pen");
1422
+ this.setCurrentTool(opts.initialToolId ?? "pen");
1323
1423
  }
1324
1424
  registerToolDefinition(toolDefinition) {
1325
1425
  for (const stateConstructor of toolDefinition.stateConstructors) {
@@ -1380,6 +1480,7 @@ var Editor = class {
1380
1480
  }
1381
1481
  setCurrentTool(id) {
1382
1482
  this.tools.setCurrentTool(id);
1483
+ this.emitChange();
1383
1484
  }
1384
1485
  getCurrentToolId() {
1385
1486
  return this.tools.getCurrentToolId();
@@ -1389,10 +1490,73 @@ var Editor = class {
1389
1490
  }
1390
1491
  setCurrentDrawStyle(partial) {
1391
1492
  this.drawStyle = { ...this.drawStyle, ...partial };
1493
+ this.emitChange();
1494
+ }
1495
+ setViewport(partial) {
1496
+ this.viewport = {
1497
+ x: partial.x ?? this.viewport.x,
1498
+ y: partial.y ?? this.viewport.y,
1499
+ zoom: partial.zoom ?? this.viewport.zoom
1500
+ };
1501
+ this.emitChange();
1392
1502
  }
1393
1503
  panBy(dx, dy) {
1394
- this.viewport.x += dx;
1395
- this.viewport.y += dy;
1504
+ this.setViewport({
1505
+ x: this.viewport.x + dx,
1506
+ y: this.viewport.y + dy
1507
+ });
1508
+ }
1509
+ getDocumentSnapshot() {
1510
+ return {
1511
+ records: documentSnapshotToRecords(this.store.getSnapshot())
1512
+ };
1513
+ }
1514
+ loadDocumentSnapshot(snapshot) {
1515
+ const documentSnapshot = recordsToDocumentSnapshot(snapshot.records);
1516
+ if (!documentSnapshot) return;
1517
+ this.store.loadSnapshot(documentSnapshot);
1518
+ }
1519
+ getSessionStateSnapshot(args) {
1520
+ return {
1521
+ version: 1,
1522
+ viewport: {
1523
+ x: this.viewport.x,
1524
+ y: this.viewport.y,
1525
+ zoom: this.viewport.zoom
1526
+ },
1527
+ currentToolId: this.getCurrentToolId(),
1528
+ drawStyle: this.getCurrentDrawStyle(),
1529
+ selectedShapeIds: [...args?.selectedShapeIds ?? []]
1530
+ };
1531
+ }
1532
+ loadSessionStateSnapshot(snapshot) {
1533
+ this.setViewport(snapshot.viewport);
1534
+ this.setCurrentDrawStyle(snapshot.drawStyle);
1535
+ if (this.tools.hasTool(snapshot.currentToolId)) {
1536
+ this.setCurrentTool(snapshot.currentToolId);
1537
+ }
1538
+ return [...snapshot.selectedShapeIds];
1539
+ }
1540
+ getPersistenceSnapshot(args) {
1541
+ return {
1542
+ document: this.getDocumentSnapshot(),
1543
+ state: this.getSessionStateSnapshot(args)
1544
+ };
1545
+ }
1546
+ loadPersistenceSnapshot(snapshot) {
1547
+ if (snapshot.document) {
1548
+ this.loadDocumentSnapshot(snapshot.document);
1549
+ }
1550
+ if (snapshot.state) {
1551
+ return this.loadSessionStateSnapshot(snapshot.state);
1552
+ }
1553
+ return [];
1554
+ }
1555
+ listen(listener) {
1556
+ this.listeners.add(listener);
1557
+ return () => {
1558
+ this.listeners.delete(listener);
1559
+ };
1396
1560
  }
1397
1561
  // Convert screen coords to page coords
1398
1562
  screenToPage(screenX, screenY) {
@@ -1405,6 +1569,11 @@ var Editor = class {
1405
1569
  const visible = shapes.filter((s) => !erasingIds.has(s.id));
1406
1570
  this.renderer.render(ctx, this.viewport, visible);
1407
1571
  }
1572
+ emitChange() {
1573
+ for (const listener of this.listeners) {
1574
+ listener();
1575
+ }
1576
+ }
1408
1577
  };
1409
1578
 
1410
1579
  // src/tools/select/selectHelpers.ts
@@ -1648,6 +1817,6 @@ function applyResize(editor, handle, startBounds, startShapes, pointer, lockAspe
1648
1817
  }
1649
1818
  }
1650
1819
 
1651
- export { CanvasRenderer, DEFAULT_COLORS, DRAG_DISTANCE_SQUARED, DocumentStore, ERASER_MARGIN, Editor, EraserErasingState, EraserIdleState, EraserPointingState, HandDraggingState, HandIdleState, InputManager, MAX_POINTS_PER_SHAPE, PenDrawingState, PenIdleState, STROKE_WIDTHS, SelectIdleState, StateNode, ToolManager, applyMove, applyResize, applyRotation, boundsContainPoint, boundsIntersect, boundsOf, buildStartPositions, buildTransformSnapshots, closestOnSegment, createViewport, decodeFirstPoint, decodeLastPoint, decodePathToPoints, decodePoints, distance, encodePoints, getSelectionBoundsPage, getShapeBounds2 as getShapeBounds, getShapesInBounds, getTopShapeAtPoint, isSelectTool, minDistanceToPolyline, normalizeSelectionBounds, padBounds, pageToScreen, panViewport, pointHitsShape, resolveThemeColor, rotatePoint, screenToPage, segmentHitsShape, segmentTouchesPolyline, setViewport, shapePagePoints, sqDistance, zoomViewport };
1820
+ export { CanvasRenderer, DEFAULT_COLORS, DRAG_DISTANCE_SQUARED, DocumentStore, ERASER_MARGIN, Editor, EraserErasingState, EraserIdleState, EraserPointingState, HandDraggingState, HandIdleState, InputManager, MAX_POINTS_PER_SHAPE, PenDrawingState, PenIdleState, STROKE_WIDTHS, SelectIdleState, StateNode, ToolManager, applyMove, applyResize, applyRotation, boundsContainPoint, boundsIntersect, boundsOf, buildStartPositions, buildTransformSnapshots, closestOnSegment, createViewport, decodeFirstPoint, decodeLastPoint, decodePathToPoints, decodePoints, distance, documentSnapshotToRecords, encodePoints, getSelectionBoundsPage, getShapeBounds2 as getShapeBounds, getShapesInBounds, getTopShapeAtPoint, isSelectTool, minDistanceToPolyline, normalizeSelectionBounds, padBounds, pageToScreen, panViewport, pointHitsShape, recordsToDocumentSnapshot, resolveThemeColor, rotatePoint, screenToPage, segmentHitsShape, segmentTouchesPolyline, setViewport, shapePagePoints, sqDistance, zoomViewport };
1652
1821
  //# sourceMappingURL=index.js.map
1653
1822
  //# sourceMappingURL=index.js.map