@tsdraw/core 0.4.0 → 0.5.1

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") {
@@ -213,105 +253,26 @@ function zoomViewport(viewport, factor, centerX, centerY) {
213
253
  }
214
254
 
215
255
  // src/utils/colors.ts
256
+ var DARK_COLORS = {
257
+ black: "#f0f0f0",
258
+ grey: "#aeb8c2",
259
+ "light-violet": "#cf6ef5",
260
+ violet: "#a83ce0",
261
+ blue: "#5b7dff",
262
+ "light-blue": "#4fb3ff",
263
+ yellow: "#f4b13a",
264
+ orange: "#ef7a24",
265
+ green: "#1fb27a",
266
+ "light-green": "#4ecb66",
267
+ "light-red": "#ff6f78",
268
+ red: "#f24343",
269
+ white: "#ffffff"
270
+ };
216
271
  function resolveThemeColor(colorStyle, theme) {
217
- const paletteColor = DEFAULT_COLORS[colorStyle];
218
- if (!paletteColor) return colorStyle;
219
- if (theme === "light") return paletteColor;
220
- return invertAndHueRotate180(paletteColor);
221
- }
222
- function invertAndHueRotate180(color) {
223
- const rgb = parseHexColor(color);
224
- if (!rgb) return color;
225
- const inverted = {
226
- r: 255 - rgb.r,
227
- g: 255 - rgb.g,
228
- b: 255 - rgb.b
229
- };
230
- const hsl = rgbToHsl(inverted.r, inverted.g, inverted.b);
231
- const rotated = hslToRgb((hsl.h + 180) % 360, hsl.s, hsl.l);
232
- return rgbToHex(rotated.r, rotated.g, rotated.b);
233
- }
234
- function parseHexColor(color) {
235
- const normalized = color.trim().toLowerCase();
236
- if (!normalized.startsWith("#")) return null;
237
- if (normalized.length === 4) {
238
- return {
239
- r: parseInt(normalized[1] + normalized[1], 16),
240
- g: parseInt(normalized[2] + normalized[2], 16),
241
- b: parseInt(normalized[3] + normalized[3], 16)
242
- };
243
- }
244
- if (normalized.length !== 7) return null;
245
- return {
246
- r: parseInt(normalized.slice(1, 3), 16),
247
- g: parseInt(normalized.slice(3, 5), 16),
248
- b: parseInt(normalized.slice(5, 7), 16)
249
- };
250
- }
251
- function rgbToHex(r, g, b) {
252
- return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
253
- }
254
- function toHex(value) {
255
- return Math.round(Math.max(0, Math.min(255, value))).toString(16).padStart(2, "0");
256
- }
257
- function rgbToHsl(r, g, b) {
258
- const red = r / 255;
259
- const green = g / 255;
260
- const blue = b / 255;
261
- const maxChannel = Math.max(red, green, blue);
262
- const minChannel = Math.min(red, green, blue);
263
- const delta = maxChannel - minChannel;
264
- const lightness = (maxChannel + minChannel) / 2;
265
- if (delta === 0) {
266
- return { h: 0, s: 0, l: lightness };
267
- }
268
- const saturation = lightness > 0.5 ? delta / (2 - maxChannel - minChannel) : delta / (maxChannel + minChannel);
269
- let hue = 0;
270
- if (maxChannel === red) {
271
- hue = ((green - blue) / delta + (green < blue ? 6 : 0)) * 60;
272
- } else if (maxChannel === green) {
273
- hue = ((blue - red) / delta + 2) * 60;
274
- } else {
275
- hue = ((red - green) / delta + 4) * 60;
276
- }
277
- return { h: hue, s: saturation, l: lightness };
278
- }
279
- function hslToRgb(h, s, l) {
280
- if (s === 0) {
281
- const channel = l * 255;
282
- return { r: channel, g: channel, b: channel };
283
- }
284
- const chroma = (1 - Math.abs(2 * l - 1)) * s;
285
- const hueSegment = h / 60;
286
- const x = chroma * (1 - Math.abs(hueSegment % 2 - 1));
287
- let red = 0;
288
- let green = 0;
289
- let blue = 0;
290
- if (hueSegment >= 0 && hueSegment < 1) {
291
- red = chroma;
292
- green = x;
293
- } else if (hueSegment < 2) {
294
- red = x;
295
- green = chroma;
296
- } else if (hueSegment < 3) {
297
- green = chroma;
298
- blue = x;
299
- } else if (hueSegment < 4) {
300
- green = x;
301
- blue = chroma;
302
- } else if (hueSegment < 5) {
303
- red = x;
304
- blue = chroma;
305
- } else {
306
- red = chroma;
307
- blue = x;
308
- }
309
- const match = l - chroma / 2;
310
- return {
311
- r: (red + match) * 255,
312
- g: (green + match) * 255,
313
- b: (blue + match) * 255
314
- };
272
+ const lightThemeColor = DEFAULT_COLORS[colorStyle];
273
+ if (!lightThemeColor) return colorStyle;
274
+ if (theme === "light") return lightThemeColor;
275
+ return DARK_COLORS[colorStyle] ?? lightThemeColor;
315
276
  }
316
277
  var CanvasRenderer = class {
317
278
  theme = "light";
@@ -1288,10 +1249,68 @@ var HandDraggingState = class extends StateNode {
1288
1249
  }
1289
1250
  };
1290
1251
 
1252
+ // src/persistence/snapshots.ts
1253
+ var PAGE_RECORD_ID = "page:current";
1254
+ function cloneValue2(value) {
1255
+ if (typeof structuredClone === "function") {
1256
+ return structuredClone(value);
1257
+ }
1258
+ return JSON.parse(JSON.stringify(value));
1259
+ }
1260
+ function asDrawShape(value) {
1261
+ return cloneValue2(value);
1262
+ }
1263
+ function documentSnapshotToRecords(snapshot) {
1264
+ const shapeIds = [...snapshot.order].filter((id) => snapshot.page.shapes[id] != null);
1265
+ const pageRecord = {
1266
+ id: PAGE_RECORD_ID,
1267
+ typeName: "page",
1268
+ pageId: snapshot.page.id,
1269
+ shapeIds,
1270
+ erasingShapeIds: [...snapshot.page.erasingShapeIds]
1271
+ };
1272
+ const shapeRecords = shapeIds.map((shapeId) => snapshot.page.shapes[shapeId]).filter((shape) => shape != null).map((shape) => ({
1273
+ id: shape.id,
1274
+ typeName: "shape",
1275
+ shape: asDrawShape(shape)
1276
+ }));
1277
+ return [pageRecord, ...shapeRecords];
1278
+ }
1279
+ function recordsToDocumentSnapshot(records) {
1280
+ const pageRecord = records.find((record) => record.typeName === "page");
1281
+ if (!pageRecord) {
1282
+ return null;
1283
+ }
1284
+ const shapeRecordMap = /* @__PURE__ */ new Map();
1285
+ for (const record of records) {
1286
+ if (record.typeName === "shape") {
1287
+ shapeRecordMap.set(record.id, record);
1288
+ }
1289
+ }
1290
+ const shapes = {};
1291
+ const order = [];
1292
+ for (const shapeId of pageRecord.shapeIds) {
1293
+ const shapeRecord = shapeRecordMap.get(shapeId);
1294
+ if (!shapeRecord) continue;
1295
+ shapes[shapeId] = asDrawShape(shapeRecord.shape);
1296
+ order.push(shapeId);
1297
+ }
1298
+ return {
1299
+ page: {
1300
+ id: pageRecord.pageId,
1301
+ shapes,
1302
+ erasingShapeIds: [...pageRecord.erasingShapeIds].filter((shapeId) => shapes[shapeId] != null)
1303
+ },
1304
+ order
1305
+ };
1306
+ }
1307
+
1291
1308
  // src/editor/Editor.ts
1292
1309
  var shapeIdCounter = 0;
1310
+ var shapeIdRuntimeSeed = Math.random().toString(36).slice(2, 8);
1293
1311
  function createShapeId() {
1294
- return `shape:${String(++shapeIdCounter).padStart(6, "0")}`;
1312
+ shapeIdCounter += 1;
1313
+ return `shape:${Date.now().toString(36)}-${shapeIdRuntimeSeed}-${shapeIdCounter.toString(36)}`;
1295
1314
  }
1296
1315
  var Editor = class {
1297
1316
  store = new DocumentStore();
@@ -1307,9 +1326,11 @@ var Editor = class {
1307
1326
  size: "m"
1308
1327
  };
1309
1328
  toolStateContext;
1329
+ listeners = /* @__PURE__ */ new Set();
1310
1330
  // Creates a new editor instance with the given options (with defaults if not provided)
1311
1331
  constructor(opts = {}) {
1312
1332
  this.options = { dragDistanceSquared: opts.dragDistanceSquared ?? DRAG_DISTANCE_SQUARED };
1333
+ this.store.listen(() => this.emitChange());
1313
1334
  this.toolStateContext = {
1314
1335
  transition: (id, info) => this.tools.transition(id, info)
1315
1336
  };
@@ -1319,7 +1340,7 @@ var Editor = class {
1319
1340
  for (const customTool of opts.toolDefinitions ?? []) {
1320
1341
  this.registerToolDefinition(customTool);
1321
1342
  }
1322
- this.tools.setCurrentTool(opts.initialToolId ?? "pen");
1343
+ this.setCurrentTool(opts.initialToolId ?? "pen");
1323
1344
  }
1324
1345
  registerToolDefinition(toolDefinition) {
1325
1346
  for (const stateConstructor of toolDefinition.stateConstructors) {
@@ -1380,6 +1401,7 @@ var Editor = class {
1380
1401
  }
1381
1402
  setCurrentTool(id) {
1382
1403
  this.tools.setCurrentTool(id);
1404
+ this.emitChange();
1383
1405
  }
1384
1406
  getCurrentToolId() {
1385
1407
  return this.tools.getCurrentToolId();
@@ -1389,10 +1411,73 @@ var Editor = class {
1389
1411
  }
1390
1412
  setCurrentDrawStyle(partial) {
1391
1413
  this.drawStyle = { ...this.drawStyle, ...partial };
1414
+ this.emitChange();
1415
+ }
1416
+ setViewport(partial) {
1417
+ this.viewport = {
1418
+ x: partial.x ?? this.viewport.x,
1419
+ y: partial.y ?? this.viewport.y,
1420
+ zoom: partial.zoom ?? this.viewport.zoom
1421
+ };
1422
+ this.emitChange();
1392
1423
  }
1393
1424
  panBy(dx, dy) {
1394
- this.viewport.x += dx;
1395
- this.viewport.y += dy;
1425
+ this.setViewport({
1426
+ x: this.viewport.x + dx,
1427
+ y: this.viewport.y + dy
1428
+ });
1429
+ }
1430
+ getDocumentSnapshot() {
1431
+ return {
1432
+ records: documentSnapshotToRecords(this.store.getSnapshot())
1433
+ };
1434
+ }
1435
+ loadDocumentSnapshot(snapshot) {
1436
+ const documentSnapshot = recordsToDocumentSnapshot(snapshot.records);
1437
+ if (!documentSnapshot) return;
1438
+ this.store.loadSnapshot(documentSnapshot);
1439
+ }
1440
+ getSessionStateSnapshot(args) {
1441
+ return {
1442
+ version: 1,
1443
+ viewport: {
1444
+ x: this.viewport.x,
1445
+ y: this.viewport.y,
1446
+ zoom: this.viewport.zoom
1447
+ },
1448
+ currentToolId: this.getCurrentToolId(),
1449
+ drawStyle: this.getCurrentDrawStyle(),
1450
+ selectedShapeIds: [...args?.selectedShapeIds ?? []]
1451
+ };
1452
+ }
1453
+ loadSessionStateSnapshot(snapshot) {
1454
+ this.setViewport(snapshot.viewport);
1455
+ this.setCurrentDrawStyle(snapshot.drawStyle);
1456
+ if (this.tools.hasTool(snapshot.currentToolId)) {
1457
+ this.setCurrentTool(snapshot.currentToolId);
1458
+ }
1459
+ return [...snapshot.selectedShapeIds];
1460
+ }
1461
+ getPersistenceSnapshot(args) {
1462
+ return {
1463
+ document: this.getDocumentSnapshot(),
1464
+ state: this.getSessionStateSnapshot(args)
1465
+ };
1466
+ }
1467
+ loadPersistenceSnapshot(snapshot) {
1468
+ if (snapshot.document) {
1469
+ this.loadDocumentSnapshot(snapshot.document);
1470
+ }
1471
+ if (snapshot.state) {
1472
+ return this.loadSessionStateSnapshot(snapshot.state);
1473
+ }
1474
+ return [];
1475
+ }
1476
+ listen(listener) {
1477
+ this.listeners.add(listener);
1478
+ return () => {
1479
+ this.listeners.delete(listener);
1480
+ };
1396
1481
  }
1397
1482
  // Convert screen coords to page coords
1398
1483
  screenToPage(screenX, screenY) {
@@ -1405,6 +1490,11 @@ var Editor = class {
1405
1490
  const visible = shapes.filter((s) => !erasingIds.has(s.id));
1406
1491
  this.renderer.render(ctx, this.viewport, visible);
1407
1492
  }
1493
+ emitChange() {
1494
+ for (const listener of this.listeners) {
1495
+ listener();
1496
+ }
1497
+ }
1408
1498
  };
1409
1499
 
1410
1500
  // src/tools/select/selectHelpers.ts
@@ -1648,6 +1738,6 @@ function applyResize(editor, handle, startBounds, startShapes, pointer, lockAspe
1648
1738
  }
1649
1739
  }
1650
1740
 
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 };
1741
+ 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
1742
  //# sourceMappingURL=index.js.map
1653
1743
  //# sourceMappingURL=index.js.map