@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 +194 -102
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +64 -1
- package/dist/index.d.ts +64 -1
- package/dist/index.js +193 -103
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
220
|
-
if (!
|
|
221
|
-
if (theme === "light") return
|
|
222
|
-
return
|
|
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
|
-
|
|
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.
|
|
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.
|
|
1397
|
-
|
|
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;
|