@tsdraw/core 0.5.1 → 0.6.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
@@ -74,6 +74,11 @@ interface TsdrawEditorSnapshot {
74
74
  document: TsdrawDocumentSnapshot;
75
75
  state: TsdrawSessionStateSnapshot;
76
76
  }
77
+ interface TsdrawHistorySnapshot {
78
+ version: 1;
79
+ undoStack: TsdrawDocumentSnapshot[];
80
+ redoStack: TsdrawDocumentSnapshot[];
81
+ }
77
82
  interface DocumentStoreSnapshot {
78
83
  page: PageState;
79
84
  order: ShapeId[];
@@ -289,7 +294,16 @@ declare class Editor {
289
294
  private drawStyle;
290
295
  private readonly toolStateContext;
291
296
  private readonly listeners;
297
+ private readonly historyListeners;
298
+ private undoStack;
299
+ private redoStack;
300
+ private lastDocumentSnapshot;
301
+ private suppressHistoryCapture;
302
+ private historyBatchDepth;
303
+ private historyBatchStartSnapshot;
304
+ private historyBatchChanged;
292
305
  constructor(opts?: EditorOptions);
306
+ private captureDocumentHistory;
293
307
  registerToolDefinition(toolDefinition: ToolDefinition): void;
294
308
  private getDefaultToolDefinitions;
295
309
  createShapeId(): ShapeId;
@@ -331,13 +345,25 @@ declare class Editor {
331
345
  selectedShapeIds?: ShapeId[];
332
346
  }): TsdrawEditorSnapshot;
333
347
  loadPersistenceSnapshot(snapshot: Partial<TsdrawEditorSnapshot>): ShapeId[];
348
+ getHistorySnapshot(): TsdrawHistorySnapshot;
349
+ loadHistorySnapshot(snapshot: TsdrawHistorySnapshot | null | undefined): void;
350
+ clearRedoHistory(): void;
351
+ beginHistoryEntry(): void;
352
+ endHistoryEntry(): void;
353
+ canUndo(): boolean;
354
+ canRedo(): boolean;
355
+ undo(): boolean;
356
+ redo(): boolean;
334
357
  listen(listener: EditorListener): () => void;
358
+ listenHistory(listener: EditorListener): () => void;
335
359
  screenToPage(screenX: number, screenY: number): {
336
360
  x: number;
337
361
  y: number;
338
362
  };
339
363
  render(ctx: CanvasRenderingContext2D): void;
340
364
  private emitChange;
365
+ private emitHistoryChange;
366
+ private runWithoutHistoryCapture;
341
367
  }
342
368
 
343
369
  declare class PenIdleState extends StateNode {
@@ -524,4 +550,4 @@ declare function decodePathToPoints(segments: {
524
550
  y: number;
525
551
  }[];
526
552
 
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 };
553
+ 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 TsdrawHistorySnapshot, 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
@@ -74,6 +74,11 @@ interface TsdrawEditorSnapshot {
74
74
  document: TsdrawDocumentSnapshot;
75
75
  state: TsdrawSessionStateSnapshot;
76
76
  }
77
+ interface TsdrawHistorySnapshot {
78
+ version: 1;
79
+ undoStack: TsdrawDocumentSnapshot[];
80
+ redoStack: TsdrawDocumentSnapshot[];
81
+ }
77
82
  interface DocumentStoreSnapshot {
78
83
  page: PageState;
79
84
  order: ShapeId[];
@@ -289,7 +294,16 @@ declare class Editor {
289
294
  private drawStyle;
290
295
  private readonly toolStateContext;
291
296
  private readonly listeners;
297
+ private readonly historyListeners;
298
+ private undoStack;
299
+ private redoStack;
300
+ private lastDocumentSnapshot;
301
+ private suppressHistoryCapture;
302
+ private historyBatchDepth;
303
+ private historyBatchStartSnapshot;
304
+ private historyBatchChanged;
292
305
  constructor(opts?: EditorOptions);
306
+ private captureDocumentHistory;
293
307
  registerToolDefinition(toolDefinition: ToolDefinition): void;
294
308
  private getDefaultToolDefinitions;
295
309
  createShapeId(): ShapeId;
@@ -331,13 +345,25 @@ declare class Editor {
331
345
  selectedShapeIds?: ShapeId[];
332
346
  }): TsdrawEditorSnapshot;
333
347
  loadPersistenceSnapshot(snapshot: Partial<TsdrawEditorSnapshot>): ShapeId[];
348
+ getHistorySnapshot(): TsdrawHistorySnapshot;
349
+ loadHistorySnapshot(snapshot: TsdrawHistorySnapshot | null | undefined): void;
350
+ clearRedoHistory(): void;
351
+ beginHistoryEntry(): void;
352
+ endHistoryEntry(): void;
353
+ canUndo(): boolean;
354
+ canRedo(): boolean;
355
+ undo(): boolean;
356
+ redo(): boolean;
334
357
  listen(listener: EditorListener): () => void;
358
+ listenHistory(listener: EditorListener): () => void;
335
359
  screenToPage(screenX: number, screenY: number): {
336
360
  x: number;
337
361
  y: number;
338
362
  };
339
363
  render(ctx: CanvasRenderingContext2D): void;
340
364
  private emitChange;
365
+ private emitHistoryChange;
366
+ private runWithoutHistoryCapture;
341
367
  }
342
368
 
343
369
  declare class PenIdleState extends StateNode {
@@ -524,4 +550,4 @@ declare function decodePathToPoints(segments: {
524
550
  y: number;
525
551
  }[];
526
552
 
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 };
553
+ 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 TsdrawHistorySnapshot, 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
@@ -1308,10 +1308,26 @@ function recordsToDocumentSnapshot(records) {
1308
1308
  // src/editor/Editor.ts
1309
1309
  var shapeIdCounter = 0;
1310
1310
  var shapeIdRuntimeSeed = Math.random().toString(36).slice(2, 8);
1311
+ var MAX_HISTORY_ENTRIES = 100;
1311
1312
  function createShapeId() {
1312
1313
  shapeIdCounter += 1;
1313
1314
  return `shape:${Date.now().toString(36)}-${shapeIdRuntimeSeed}-${shapeIdCounter.toString(36)}`;
1314
1315
  }
1316
+ function cloneDocumentSnapshot(snapshot) {
1317
+ if (typeof structuredClone === "function") {
1318
+ return structuredClone(snapshot);
1319
+ }
1320
+ return JSON.parse(JSON.stringify(snapshot));
1321
+ }
1322
+ function areDocumentSnapshotsEqual(left, right) {
1323
+ if (left.records.length !== right.records.length) return false;
1324
+ for (let i = 0; i < left.records.length; i += 1) {
1325
+ if (JSON.stringify(left.records[i]) !== JSON.stringify(right.records[i])) {
1326
+ return false;
1327
+ }
1328
+ }
1329
+ return true;
1330
+ }
1315
1331
  var Editor = class {
1316
1332
  store = new DocumentStore();
1317
1333
  input = new InputManager();
@@ -1327,10 +1343,22 @@ var Editor = class {
1327
1343
  };
1328
1344
  toolStateContext;
1329
1345
  listeners = /* @__PURE__ */ new Set();
1346
+ historyListeners = /* @__PURE__ */ new Set();
1347
+ undoStack = [];
1348
+ redoStack = [];
1349
+ lastDocumentSnapshot;
1350
+ suppressHistoryCapture = false;
1351
+ historyBatchDepth = 0;
1352
+ historyBatchStartSnapshot = null;
1353
+ historyBatchChanged = false;
1330
1354
  // Creates a new editor instance with the given options (with defaults if not provided)
1331
1355
  constructor(opts = {}) {
1332
1356
  this.options = { dragDistanceSquared: opts.dragDistanceSquared ?? DRAG_DISTANCE_SQUARED };
1333
- this.store.listen(() => this.emitChange());
1357
+ this.lastDocumentSnapshot = this.getDocumentSnapshot();
1358
+ this.store.listen(() => {
1359
+ this.captureDocumentHistory();
1360
+ this.emitChange();
1361
+ });
1334
1362
  this.toolStateContext = {
1335
1363
  transition: (id, info) => this.tools.transition(id, info)
1336
1364
  };
@@ -1341,6 +1369,25 @@ var Editor = class {
1341
1369
  this.registerToolDefinition(customTool);
1342
1370
  }
1343
1371
  this.setCurrentTool(opts.initialToolId ?? "pen");
1372
+ this.lastDocumentSnapshot = this.getDocumentSnapshot();
1373
+ }
1374
+ captureDocumentHistory() {
1375
+ const nextSnapshot = this.getDocumentSnapshot();
1376
+ const previousSnapshot = this.lastDocumentSnapshot;
1377
+ this.lastDocumentSnapshot = nextSnapshot;
1378
+ if (this.suppressHistoryCapture || areDocumentSnapshotsEqual(previousSnapshot, nextSnapshot)) {
1379
+ return;
1380
+ }
1381
+ if (this.historyBatchDepth > 0) {
1382
+ this.historyBatchChanged = true;
1383
+ return;
1384
+ }
1385
+ this.undoStack.push(cloneDocumentSnapshot(previousSnapshot));
1386
+ if (this.undoStack.length > MAX_HISTORY_ENTRIES) {
1387
+ this.undoStack.splice(0, this.undoStack.length - MAX_HISTORY_ENTRIES);
1388
+ }
1389
+ this.redoStack = [];
1390
+ this.emitHistoryChange();
1344
1391
  }
1345
1392
  registerToolDefinition(toolDefinition) {
1346
1393
  for (const stateConstructor of toolDefinition.stateConstructors) {
@@ -1435,7 +1482,9 @@ var Editor = class {
1435
1482
  loadDocumentSnapshot(snapshot) {
1436
1483
  const documentSnapshot = recordsToDocumentSnapshot(snapshot.records);
1437
1484
  if (!documentSnapshot) return;
1438
- this.store.loadSnapshot(documentSnapshot);
1485
+ this.runWithoutHistoryCapture(() => {
1486
+ this.store.loadSnapshot(documentSnapshot);
1487
+ });
1439
1488
  }
1440
1489
  getSessionStateSnapshot(args) {
1441
1490
  return {
@@ -1473,12 +1522,92 @@ var Editor = class {
1473
1522
  }
1474
1523
  return [];
1475
1524
  }
1525
+ getHistorySnapshot() {
1526
+ return {
1527
+ version: 1,
1528
+ undoStack: this.undoStack.map(cloneDocumentSnapshot),
1529
+ redoStack: this.redoStack.map(cloneDocumentSnapshot)
1530
+ };
1531
+ }
1532
+ loadHistorySnapshot(snapshot) {
1533
+ if (!snapshot || snapshot.version !== 1) return;
1534
+ this.undoStack = snapshot.undoStack.map(cloneDocumentSnapshot).slice(-MAX_HISTORY_ENTRIES);
1535
+ this.redoStack = snapshot.redoStack.map(cloneDocumentSnapshot).slice(-MAX_HISTORY_ENTRIES);
1536
+ this.emitHistoryChange();
1537
+ }
1538
+ clearRedoHistory() {
1539
+ if (this.redoStack.length === 0) return;
1540
+ this.redoStack = [];
1541
+ this.emitHistoryChange();
1542
+ }
1543
+ beginHistoryEntry() {
1544
+ if (this.historyBatchDepth === 0) {
1545
+ this.historyBatchStartSnapshot = cloneDocumentSnapshot(this.lastDocumentSnapshot);
1546
+ this.historyBatchChanged = false;
1547
+ }
1548
+ this.historyBatchDepth += 1;
1549
+ }
1550
+ endHistoryEntry() {
1551
+ if (this.historyBatchDepth === 0) return;
1552
+ this.historyBatchDepth -= 1;
1553
+ if (this.historyBatchDepth > 0) return;
1554
+ const startSnapshot = this.historyBatchStartSnapshot;
1555
+ this.historyBatchStartSnapshot = null;
1556
+ if (!startSnapshot) return;
1557
+ const endSnapshot = this.getDocumentSnapshot();
1558
+ this.lastDocumentSnapshot = endSnapshot;
1559
+ const didDocumentChange = this.historyBatchChanged || !areDocumentSnapshotsEqual(startSnapshot, endSnapshot);
1560
+ this.historyBatchChanged = false;
1561
+ if (!didDocumentChange) return;
1562
+ this.undoStack.push(cloneDocumentSnapshot(startSnapshot));
1563
+ if (this.undoStack.length > MAX_HISTORY_ENTRIES) {
1564
+ this.undoStack.splice(0, this.undoStack.length - MAX_HISTORY_ENTRIES);
1565
+ }
1566
+ this.redoStack = [];
1567
+ this.emitHistoryChange();
1568
+ }
1569
+ canUndo() {
1570
+ return this.undoStack.length > 0;
1571
+ }
1572
+ canRedo() {
1573
+ return this.redoStack.length > 0;
1574
+ }
1575
+ undo() {
1576
+ const previousSnapshot = this.undoStack.pop();
1577
+ if (!previousSnapshot) return false;
1578
+ const currentSnapshot = this.getDocumentSnapshot();
1579
+ this.redoStack.push(cloneDocumentSnapshot(currentSnapshot));
1580
+ if (this.redoStack.length > MAX_HISTORY_ENTRIES) {
1581
+ this.redoStack.splice(0, this.redoStack.length - MAX_HISTORY_ENTRIES);
1582
+ }
1583
+ this.loadDocumentSnapshot(previousSnapshot);
1584
+ this.emitHistoryChange();
1585
+ return true;
1586
+ }
1587
+ redo() {
1588
+ const nextSnapshot = this.redoStack.pop();
1589
+ if (!nextSnapshot) return false;
1590
+ const currentSnapshot = this.getDocumentSnapshot();
1591
+ this.undoStack.push(cloneDocumentSnapshot(currentSnapshot));
1592
+ if (this.undoStack.length > MAX_HISTORY_ENTRIES) {
1593
+ this.undoStack.splice(0, this.undoStack.length - MAX_HISTORY_ENTRIES);
1594
+ }
1595
+ this.loadDocumentSnapshot(nextSnapshot);
1596
+ this.emitHistoryChange();
1597
+ return true;
1598
+ }
1476
1599
  listen(listener) {
1477
1600
  this.listeners.add(listener);
1478
1601
  return () => {
1479
1602
  this.listeners.delete(listener);
1480
1603
  };
1481
1604
  }
1605
+ listenHistory(listener) {
1606
+ this.historyListeners.add(listener);
1607
+ return () => {
1608
+ this.historyListeners.delete(listener);
1609
+ };
1610
+ }
1482
1611
  // Convert screen coords to page coords
1483
1612
  screenToPage(screenX, screenY) {
1484
1613
  return screenToPage(this.viewport, screenX, screenY);
@@ -1495,6 +1624,21 @@ var Editor = class {
1495
1624
  listener();
1496
1625
  }
1497
1626
  }
1627
+ emitHistoryChange() {
1628
+ for (const listener of this.historyListeners) {
1629
+ listener();
1630
+ }
1631
+ }
1632
+ runWithoutHistoryCapture(fn) {
1633
+ const previousValue = this.suppressHistoryCapture;
1634
+ this.suppressHistoryCapture = true;
1635
+ try {
1636
+ fn();
1637
+ } finally {
1638
+ this.suppressHistoryCapture = previousValue;
1639
+ this.lastDocumentSnapshot = this.getDocumentSnapshot();
1640
+ }
1641
+ }
1498
1642
  };
1499
1643
 
1500
1644
  // src/tools/select/selectHelpers.ts