@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.cjs CHANGED
@@ -71,6 +71,12 @@ function decodePathToPoints(segments, ox, oy) {
71
71
  }
72
72
 
73
73
  // src/store/documentStore.ts
74
+ function cloneValue(value) {
75
+ if (typeof structuredClone === "function") {
76
+ return structuredClone(value);
77
+ }
78
+ return JSON.parse(JSON.stringify(value));
79
+ }
74
80
  var DocumentStore = class {
75
81
  state = {
76
82
  id: "page-1",
@@ -78,6 +84,7 @@ var DocumentStore = class {
78
84
  erasingShapeIds: []
79
85
  };
80
86
  order = [];
87
+ listeners = /* @__PURE__ */ new Set();
81
88
  getPage() {
82
89
  return this.state;
83
90
  }
@@ -98,15 +105,18 @@ var DocumentStore = class {
98
105
  }
99
106
  setErasingShapes(ids) {
100
107
  this.state.erasingShapeIds = ids;
108
+ this.emitChange();
101
109
  }
102
110
  createShape(shape) {
103
111
  this.state.shapes[shape.id] = shape;
104
112
  this.order.push(shape.id);
113
+ this.emitChange();
105
114
  }
106
115
  updateShape(id, partial) {
107
116
  const existing = this.state.shapes[id];
108
117
  if (!existing) return;
109
118
  this.state.shapes[id] = { ...existing, ...partial, id };
119
+ this.emitChange();
110
120
  }
111
121
  deleteShapes(ids) {
112
122
  for (const id of ids) {
@@ -114,6 +124,7 @@ var DocumentStore = class {
114
124
  this.order = this.order.filter((i) => i !== id);
115
125
  }
116
126
  this.state.erasingShapeIds = this.state.erasingShapeIds.filter((i) => !ids.includes(i));
127
+ this.emitChange();
117
128
  }
118
129
  getCurrentPageShapes() {
119
130
  return Object.values(this.state.shapes);
@@ -129,6 +140,35 @@ var DocumentStore = class {
129
140
  }
130
141
  return ids;
131
142
  }
143
+ getSnapshot() {
144
+ return {
145
+ page: cloneValue(this.state),
146
+ order: [...this.order]
147
+ };
148
+ }
149
+ // Load snapshot into the document when loading a persistence snapshot (so on page reload)
150
+ loadSnapshot(snapshot) {
151
+ const pageState = cloneValue(snapshot.page);
152
+ const normalizedOrder = [...snapshot.order].filter((shapeId) => pageState.shapes[shapeId] != null);
153
+ this.state = {
154
+ id: pageState.id,
155
+ shapes: pageState.shapes,
156
+ erasingShapeIds: pageState.erasingShapeIds.filter((shapeId) => pageState.shapes[shapeId] != null)
157
+ };
158
+ this.order = normalizedOrder;
159
+ this.emitChange();
160
+ }
161
+ listen(listener) {
162
+ this.listeners.add(listener);
163
+ return () => {
164
+ this.listeners.delete(listener);
165
+ };
166
+ }
167
+ emitChange() {
168
+ for (const listener of this.listeners) {
169
+ listener();
170
+ }
171
+ }
132
172
  };
133
173
  function getShapeBounds(shape) {
134
174
  if (shape.type !== "draw") {
@@ -215,105 +255,26 @@ function zoomViewport(viewport, factor, centerX, centerY) {
215
255
  }
216
256
 
217
257
  // src/utils/colors.ts
258
+ var DARK_COLORS = {
259
+ black: "#f0f0f0",
260
+ grey: "#aeb8c2",
261
+ "light-violet": "#cf6ef5",
262
+ violet: "#a83ce0",
263
+ blue: "#5b7dff",
264
+ "light-blue": "#4fb3ff",
265
+ yellow: "#f4b13a",
266
+ orange: "#ef7a24",
267
+ green: "#1fb27a",
268
+ "light-green": "#4ecb66",
269
+ "light-red": "#ff6f78",
270
+ red: "#f24343",
271
+ white: "#ffffff"
272
+ };
218
273
  function resolveThemeColor(colorStyle, theme) {
219
- const paletteColor = DEFAULT_COLORS[colorStyle];
220
- if (!paletteColor) return colorStyle;
221
- if (theme === "light") return paletteColor;
222
- return invertAndHueRotate180(paletteColor);
223
- }
224
- function invertAndHueRotate180(color) {
225
- const rgb = parseHexColor(color);
226
- if (!rgb) return color;
227
- const inverted = {
228
- r: 255 - rgb.r,
229
- g: 255 - rgb.g,
230
- b: 255 - rgb.b
231
- };
232
- const hsl = rgbToHsl(inverted.r, inverted.g, inverted.b);
233
- const rotated = hslToRgb((hsl.h + 180) % 360, hsl.s, hsl.l);
234
- return rgbToHex(rotated.r, rotated.g, rotated.b);
235
- }
236
- function parseHexColor(color) {
237
- const normalized = color.trim().toLowerCase();
238
- if (!normalized.startsWith("#")) return null;
239
- if (normalized.length === 4) {
240
- return {
241
- r: parseInt(normalized[1] + normalized[1], 16),
242
- g: parseInt(normalized[2] + normalized[2], 16),
243
- b: parseInt(normalized[3] + normalized[3], 16)
244
- };
245
- }
246
- if (normalized.length !== 7) return null;
247
- return {
248
- r: parseInt(normalized.slice(1, 3), 16),
249
- g: parseInt(normalized.slice(3, 5), 16),
250
- b: parseInt(normalized.slice(5, 7), 16)
251
- };
252
- }
253
- function rgbToHex(r, g, b) {
254
- return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
255
- }
256
- function toHex(value) {
257
- return Math.round(Math.max(0, Math.min(255, value))).toString(16).padStart(2, "0");
258
- }
259
- function rgbToHsl(r, g, b) {
260
- const red = r / 255;
261
- const green = g / 255;
262
- const blue = b / 255;
263
- const maxChannel = Math.max(red, green, blue);
264
- const minChannel = Math.min(red, green, blue);
265
- const delta = maxChannel - minChannel;
266
- const lightness = (maxChannel + minChannel) / 2;
267
- if (delta === 0) {
268
- return { h: 0, s: 0, l: lightness };
269
- }
270
- const saturation = lightness > 0.5 ? delta / (2 - maxChannel - minChannel) : delta / (maxChannel + minChannel);
271
- let hue = 0;
272
- if (maxChannel === red) {
273
- hue = ((green - blue) / delta + (green < blue ? 6 : 0)) * 60;
274
- } else if (maxChannel === green) {
275
- hue = ((blue - red) / delta + 2) * 60;
276
- } else {
277
- hue = ((red - green) / delta + 4) * 60;
278
- }
279
- return { h: hue, s: saturation, l: lightness };
280
- }
281
- function hslToRgb(h, s, l) {
282
- if (s === 0) {
283
- const channel = l * 255;
284
- return { r: channel, g: channel, b: channel };
285
- }
286
- const chroma = (1 - Math.abs(2 * l - 1)) * s;
287
- const hueSegment = h / 60;
288
- const x = chroma * (1 - Math.abs(hueSegment % 2 - 1));
289
- let red = 0;
290
- let green = 0;
291
- let blue = 0;
292
- if (hueSegment >= 0 && hueSegment < 1) {
293
- red = chroma;
294
- green = x;
295
- } else if (hueSegment < 2) {
296
- red = x;
297
- green = chroma;
298
- } else if (hueSegment < 3) {
299
- green = chroma;
300
- blue = x;
301
- } else if (hueSegment < 4) {
302
- green = x;
303
- blue = chroma;
304
- } else if (hueSegment < 5) {
305
- red = x;
306
- blue = chroma;
307
- } else {
308
- red = chroma;
309
- blue = x;
310
- }
311
- const match = l - chroma / 2;
312
- return {
313
- r: (red + match) * 255,
314
- g: (green + match) * 255,
315
- b: (blue + match) * 255
316
- };
274
+ const lightThemeColor = DEFAULT_COLORS[colorStyle];
275
+ if (!lightThemeColor) return colorStyle;
276
+ if (theme === "light") return lightThemeColor;
277
+ return DARK_COLORS[colorStyle] ?? lightThemeColor;
317
278
  }
318
279
  var CanvasRenderer = class {
319
280
  theme = "light";
@@ -1290,10 +1251,68 @@ var HandDraggingState = class extends StateNode {
1290
1251
  }
1291
1252
  };
1292
1253
 
1254
+ // src/persistence/snapshots.ts
1255
+ var PAGE_RECORD_ID = "page:current";
1256
+ function cloneValue2(value) {
1257
+ if (typeof structuredClone === "function") {
1258
+ return structuredClone(value);
1259
+ }
1260
+ return JSON.parse(JSON.stringify(value));
1261
+ }
1262
+ function asDrawShape(value) {
1263
+ return cloneValue2(value);
1264
+ }
1265
+ function documentSnapshotToRecords(snapshot) {
1266
+ const shapeIds = [...snapshot.order].filter((id) => snapshot.page.shapes[id] != null);
1267
+ const pageRecord = {
1268
+ id: PAGE_RECORD_ID,
1269
+ typeName: "page",
1270
+ pageId: snapshot.page.id,
1271
+ shapeIds,
1272
+ erasingShapeIds: [...snapshot.page.erasingShapeIds]
1273
+ };
1274
+ const shapeRecords = shapeIds.map((shapeId) => snapshot.page.shapes[shapeId]).filter((shape) => shape != null).map((shape) => ({
1275
+ id: shape.id,
1276
+ typeName: "shape",
1277
+ shape: asDrawShape(shape)
1278
+ }));
1279
+ return [pageRecord, ...shapeRecords];
1280
+ }
1281
+ function recordsToDocumentSnapshot(records) {
1282
+ const pageRecord = records.find((record) => record.typeName === "page");
1283
+ if (!pageRecord) {
1284
+ return null;
1285
+ }
1286
+ const shapeRecordMap = /* @__PURE__ */ new Map();
1287
+ for (const record of records) {
1288
+ if (record.typeName === "shape") {
1289
+ shapeRecordMap.set(record.id, record);
1290
+ }
1291
+ }
1292
+ const shapes = {};
1293
+ const order = [];
1294
+ for (const shapeId of pageRecord.shapeIds) {
1295
+ const shapeRecord = shapeRecordMap.get(shapeId);
1296
+ if (!shapeRecord) continue;
1297
+ shapes[shapeId] = asDrawShape(shapeRecord.shape);
1298
+ order.push(shapeId);
1299
+ }
1300
+ return {
1301
+ page: {
1302
+ id: pageRecord.pageId,
1303
+ shapes,
1304
+ erasingShapeIds: [...pageRecord.erasingShapeIds].filter((shapeId) => shapes[shapeId] != null)
1305
+ },
1306
+ order
1307
+ };
1308
+ }
1309
+
1293
1310
  // src/editor/Editor.ts
1294
1311
  var shapeIdCounter = 0;
1312
+ var shapeIdRuntimeSeed = Math.random().toString(36).slice(2, 8);
1295
1313
  function createShapeId() {
1296
- return `shape:${String(++shapeIdCounter).padStart(6, "0")}`;
1314
+ shapeIdCounter += 1;
1315
+ return `shape:${Date.now().toString(36)}-${shapeIdRuntimeSeed}-${shapeIdCounter.toString(36)}`;
1297
1316
  }
1298
1317
  var Editor = class {
1299
1318
  store = new DocumentStore();
@@ -1309,9 +1328,11 @@ var Editor = class {
1309
1328
  size: "m"
1310
1329
  };
1311
1330
  toolStateContext;
1331
+ listeners = /* @__PURE__ */ new Set();
1312
1332
  // Creates a new editor instance with the given options (with defaults if not provided)
1313
1333
  constructor(opts = {}) {
1314
1334
  this.options = { dragDistanceSquared: opts.dragDistanceSquared ?? DRAG_DISTANCE_SQUARED };
1335
+ this.store.listen(() => this.emitChange());
1315
1336
  this.toolStateContext = {
1316
1337
  transition: (id, info) => this.tools.transition(id, info)
1317
1338
  };
@@ -1321,7 +1342,7 @@ var Editor = class {
1321
1342
  for (const customTool of opts.toolDefinitions ?? []) {
1322
1343
  this.registerToolDefinition(customTool);
1323
1344
  }
1324
- this.tools.setCurrentTool(opts.initialToolId ?? "pen");
1345
+ this.setCurrentTool(opts.initialToolId ?? "pen");
1325
1346
  }
1326
1347
  registerToolDefinition(toolDefinition) {
1327
1348
  for (const stateConstructor of toolDefinition.stateConstructors) {
@@ -1382,6 +1403,7 @@ var Editor = class {
1382
1403
  }
1383
1404
  setCurrentTool(id) {
1384
1405
  this.tools.setCurrentTool(id);
1406
+ this.emitChange();
1385
1407
  }
1386
1408
  getCurrentToolId() {
1387
1409
  return this.tools.getCurrentToolId();
@@ -1391,10 +1413,73 @@ var Editor = class {
1391
1413
  }
1392
1414
  setCurrentDrawStyle(partial) {
1393
1415
  this.drawStyle = { ...this.drawStyle, ...partial };
1416
+ this.emitChange();
1417
+ }
1418
+ setViewport(partial) {
1419
+ this.viewport = {
1420
+ x: partial.x ?? this.viewport.x,
1421
+ y: partial.y ?? this.viewport.y,
1422
+ zoom: partial.zoom ?? this.viewport.zoom
1423
+ };
1424
+ this.emitChange();
1394
1425
  }
1395
1426
  panBy(dx, dy) {
1396
- this.viewport.x += dx;
1397
- this.viewport.y += dy;
1427
+ this.setViewport({
1428
+ x: this.viewport.x + dx,
1429
+ y: this.viewport.y + dy
1430
+ });
1431
+ }
1432
+ getDocumentSnapshot() {
1433
+ return {
1434
+ records: documentSnapshotToRecords(this.store.getSnapshot())
1435
+ };
1436
+ }
1437
+ loadDocumentSnapshot(snapshot) {
1438
+ const documentSnapshot = recordsToDocumentSnapshot(snapshot.records);
1439
+ if (!documentSnapshot) return;
1440
+ this.store.loadSnapshot(documentSnapshot);
1441
+ }
1442
+ getSessionStateSnapshot(args) {
1443
+ return {
1444
+ version: 1,
1445
+ viewport: {
1446
+ x: this.viewport.x,
1447
+ y: this.viewport.y,
1448
+ zoom: this.viewport.zoom
1449
+ },
1450
+ currentToolId: this.getCurrentToolId(),
1451
+ drawStyle: this.getCurrentDrawStyle(),
1452
+ selectedShapeIds: [...args?.selectedShapeIds ?? []]
1453
+ };
1454
+ }
1455
+ loadSessionStateSnapshot(snapshot) {
1456
+ this.setViewport(snapshot.viewport);
1457
+ this.setCurrentDrawStyle(snapshot.drawStyle);
1458
+ if (this.tools.hasTool(snapshot.currentToolId)) {
1459
+ this.setCurrentTool(snapshot.currentToolId);
1460
+ }
1461
+ return [...snapshot.selectedShapeIds];
1462
+ }
1463
+ getPersistenceSnapshot(args) {
1464
+ return {
1465
+ document: this.getDocumentSnapshot(),
1466
+ state: this.getSessionStateSnapshot(args)
1467
+ };
1468
+ }
1469
+ loadPersistenceSnapshot(snapshot) {
1470
+ if (snapshot.document) {
1471
+ this.loadDocumentSnapshot(snapshot.document);
1472
+ }
1473
+ if (snapshot.state) {
1474
+ return this.loadSessionStateSnapshot(snapshot.state);
1475
+ }
1476
+ return [];
1477
+ }
1478
+ listen(listener) {
1479
+ this.listeners.add(listener);
1480
+ return () => {
1481
+ this.listeners.delete(listener);
1482
+ };
1398
1483
  }
1399
1484
  // Convert screen coords to page coords
1400
1485
  screenToPage(screenX, screenY) {
@@ -1407,6 +1492,11 @@ var Editor = class {
1407
1492
  const visible = shapes.filter((s) => !erasingIds.has(s.id));
1408
1493
  this.renderer.render(ctx, this.viewport, visible);
1409
1494
  }
1495
+ emitChange() {
1496
+ for (const listener of this.listeners) {
1497
+ listener();
1498
+ }
1499
+ }
1410
1500
  };
1411
1501
 
1412
1502
  // src/tools/select/selectHelpers.ts
@@ -1684,6 +1774,7 @@ exports.decodeLastPoint = decodeLastPoint;
1684
1774
  exports.decodePathToPoints = decodePathToPoints;
1685
1775
  exports.decodePoints = decodePoints;
1686
1776
  exports.distance = distance;
1777
+ exports.documentSnapshotToRecords = documentSnapshotToRecords;
1687
1778
  exports.encodePoints = encodePoints;
1688
1779
  exports.getSelectionBoundsPage = getSelectionBoundsPage;
1689
1780
  exports.getShapeBounds = getShapeBounds2;
@@ -1696,6 +1787,7 @@ exports.padBounds = padBounds;
1696
1787
  exports.pageToScreen = pageToScreen;
1697
1788
  exports.panViewport = panViewport;
1698
1789
  exports.pointHitsShape = pointHitsShape;
1790
+ exports.recordsToDocumentSnapshot = recordsToDocumentSnapshot;
1699
1791
  exports.resolveThemeColor = resolveThemeColor;
1700
1792
  exports.rotatePoint = rotatePoint;
1701
1793
  exports.screenToPage = screenToPage;