@tsdraw/core 0.6.0 → 0.7.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.cjs CHANGED
@@ -150,6 +150,12 @@ var DocumentStore = class {
150
150
  loadSnapshot(snapshot) {
151
151
  const pageState = cloneValue(snapshot.page);
152
152
  const normalizedOrder = [...snapshot.order].filter((shapeId) => pageState.shapes[shapeId] != null);
153
+ const orderedSet = new Set(normalizedOrder);
154
+ for (const shapeId of Object.keys(pageState.shapes)) {
155
+ if (!orderedSet.has(shapeId)) {
156
+ normalizedOrder.push(shapeId);
157
+ }
158
+ }
153
159
  this.state = {
154
160
  id: pageState.id,
155
161
  shapes: pageState.shapes,
@@ -292,11 +298,16 @@ var CanvasRenderer = class {
292
298
  }
293
299
  ctx.restore();
294
300
  }
301
+ // Paints a single stroke
295
302
  paintStroke(ctx, shape) {
296
303
  const width = (STROKE_WIDTHS[shape.props.size] ?? 3.5) * shape.props.scale;
297
304
  const samples = flattenSegments(shape);
298
305
  if (samples.length === 0) return;
299
306
  const color = resolveThemeColor(shape.props.color, this.theme);
307
+ const fillStyle = shape.props.fill ?? "none";
308
+ if (shape.props.isClosed && fillStyle !== "none") {
309
+ this.paintClosedShapeFill(ctx, samples, color, fillStyle);
310
+ }
300
311
  if (shape.props.dash !== "draw") {
301
312
  this.paintDashedStroke(ctx, samples, width, color, shape.props.dash);
302
313
  return;
@@ -343,6 +354,30 @@ var CanvasRenderer = class {
343
354
  ctx.stroke();
344
355
  ctx.restore();
345
356
  }
357
+ // Closed shapes are shapes where their start and end point are the same
358
+ paintClosedShapeFill(ctx, samples, color, fillStyle) {
359
+ if (samples.length < 3) return;
360
+ ctx.save();
361
+ ctx.beginPath();
362
+ ctx.moveTo(samples[0].x, samples[0].y);
363
+ for (let i = 1; i < samples.length; i++) {
364
+ const sample = samples[i];
365
+ ctx.lineTo(sample.x, sample.y);
366
+ }
367
+ ctx.closePath();
368
+ if (fillStyle === "solid") {
369
+ ctx.fillStyle = color;
370
+ ctx.globalAlpha = 0.55;
371
+ } else if (fillStyle === "none") {
372
+ ctx.fillStyle = this.theme === "dark" ? "#0f0f0f" : "#fafafa";
373
+ ctx.globalAlpha = 1;
374
+ } else {
375
+ ctx.fillStyle = color;
376
+ ctx.globalAlpha = 0.28;
377
+ }
378
+ ctx.fill();
379
+ ctx.restore();
380
+ }
346
381
  };
347
382
  var PRESSURE_FLOOR = 0.025;
348
383
  var STYLUS_CURVE = (t) => t * 0.65 + Math.sin(t * Math.PI / 2) * 0.35;
@@ -356,7 +391,7 @@ function remap(value, inRange, outRange, clamp = false) {
356
391
  return outLo + (outHi - outLo) * clamped;
357
392
  }
358
393
  function strokeConfig(shape, width) {
359
- const done = shape.props.isComplete;
394
+ const done = shape.props.isComplete || shape.props.isClosed === true;
360
395
  if (shape.props.isPen) {
361
396
  return {
362
397
  size: 1 + width * 1.2,
@@ -865,13 +900,14 @@ var PenDrawingState = class extends StateNode {
865
900
  type: "straight",
866
901
  path: encodePoints([prevEnd, { ...anchorPt, z: pressure }])
867
902
  };
903
+ const withStraightSeg = [...segments, seg];
868
904
  this.editor.updateShapes([
869
905
  {
870
906
  id,
871
907
  type: "draw",
872
908
  props: {
873
- segments: [...segments, seg],
874
- isClosed: this.detectClosure(segments, size, scale)
909
+ segments: withStraightSeg,
910
+ isClosed: this.detectClosure(withStraightSeg, size, scale)
875
911
  }
876
912
  }
877
913
  ]);
@@ -947,7 +983,7 @@ var PenDrawingState = class extends StateNode {
947
983
  type: "draw",
948
984
  props: {
949
985
  segments: updated,
950
- isClosed: this.detectClosure(segments, size, scale)
986
+ isClosed: this.detectClosure(updated, size, scale)
951
987
  }
952
988
  }
953
989
  ]);
@@ -1028,6 +1064,236 @@ var PenDrawingState = class extends StateNode {
1028
1064
  }
1029
1065
  };
1030
1066
 
1067
+ // src/tools/square/states/SquareIdleState.ts
1068
+ var SquareIdleState = class extends StateNode {
1069
+ static id = "square_idle";
1070
+ onPointerDown(info) {
1071
+ this.ctx.transition("square_drawing", info);
1072
+ }
1073
+ };
1074
+
1075
+ // src/tools/geometric/states/GeometricDrawingState.ts
1076
+ var GeometricDrawingState = class extends StateNode {
1077
+ currentShapeId = null;
1078
+ startedAt = { point: { x: 0, y: 0, z: 0.5 } };
1079
+ onEnter(info) {
1080
+ this.startedAt = info ?? { point: { x: 0, y: 0, z: 0.5 } };
1081
+ const originPoint = this.editor.input.getOriginPagePoint();
1082
+ const drawStyle = this.editor.getCurrentDrawStyle();
1083
+ const nextShapeId = this.editor.createShapeId();
1084
+ const config = this.getConfig();
1085
+ this.editor.createShape({
1086
+ id: nextShapeId,
1087
+ type: "draw",
1088
+ x: originPoint.x,
1089
+ y: originPoint.y,
1090
+ props: {
1091
+ color: drawStyle.color,
1092
+ dash: drawStyle.dash,
1093
+ fill: drawStyle.fill,
1094
+ size: drawStyle.size,
1095
+ scale: 1,
1096
+ isPen: false,
1097
+ isComplete: false,
1098
+ isClosed: true,
1099
+ segments: config.buildSegments(1, 1)
1100
+ }
1101
+ });
1102
+ this.currentShapeId = nextShapeId;
1103
+ }
1104
+ onPointerMove() {
1105
+ const activeShape = this.getActiveShape();
1106
+ if (!activeShape) return;
1107
+ const config = this.getConfig();
1108
+ const originPoint = this.editor.input.getOriginPagePoint();
1109
+ const cursorPoint = this.editor.input.getCurrentPagePoint();
1110
+ const shapeBounds = this.editor.input.getShiftKey() ? config.buildConstrainedBounds(originPoint.x, originPoint.y, cursorPoint.x, cursorPoint.y) : config.buildUnconstrainedBounds(originPoint.x, originPoint.y, cursorPoint.x, cursorPoint.y);
1111
+ this.editor.store.updateShape(activeShape.id, {
1112
+ x: shapeBounds.x,
1113
+ y: shapeBounds.y,
1114
+ props: {
1115
+ ...activeShape.props,
1116
+ segments: config.buildSegments(shapeBounds.width, shapeBounds.height),
1117
+ isClosed: true
1118
+ }
1119
+ });
1120
+ }
1121
+ onPointerUp() {
1122
+ this.completeShape();
1123
+ }
1124
+ onCancel() {
1125
+ this.removeCurrentShape();
1126
+ this.ctx.transition(this.getConfig().idleStateId, this.startedAt);
1127
+ }
1128
+ onInterrupt() {
1129
+ this.completeShape();
1130
+ }
1131
+ onKeyDown() {
1132
+ this.onPointerMove();
1133
+ }
1134
+ onKeyUp() {
1135
+ this.onPointerMove();
1136
+ }
1137
+ completeShape() {
1138
+ const activeShape = this.getActiveShape();
1139
+ const config = this.getConfig();
1140
+ if (!activeShape) {
1141
+ this.ctx.transition(config.idleStateId, this.startedAt);
1142
+ return;
1143
+ }
1144
+ const originPoint = this.editor.input.getOriginPagePoint();
1145
+ const cursorPoint = this.editor.input.getCurrentPagePoint();
1146
+ const finalizedBounds = this.editor.input.getIsDragging() ? this.editor.input.getShiftKey() ? config.buildConstrainedBounds(originPoint.x, originPoint.y, cursorPoint.x, cursorPoint.y) : config.buildUnconstrainedBounds(originPoint.x, originPoint.y, cursorPoint.x, cursorPoint.y) : config.buildDefaultBounds(originPoint.x, originPoint.y);
1147
+ this.editor.store.updateShape(activeShape.id, {
1148
+ x: finalizedBounds.x,
1149
+ y: finalizedBounds.y,
1150
+ props: {
1151
+ ...activeShape.props,
1152
+ fill: this.editor.getCurrentDrawStyle().fill,
1153
+ isComplete: true,
1154
+ isClosed: true,
1155
+ segments: config.buildSegments(finalizedBounds.width, finalizedBounds.height)
1156
+ }
1157
+ });
1158
+ this.currentShapeId = null;
1159
+ this.ctx.transition(config.idleStateId);
1160
+ }
1161
+ removeCurrentShape() {
1162
+ if (!this.currentShapeId) return;
1163
+ this.editor.store.deleteShapes([this.currentShapeId]);
1164
+ this.currentShapeId = null;
1165
+ }
1166
+ getActiveShape() {
1167
+ if (!this.currentShapeId) return null;
1168
+ const shape = this.editor.getShape(this.currentShapeId);
1169
+ if (!shape || shape.type !== "draw") return null;
1170
+ return shape;
1171
+ }
1172
+ };
1173
+
1174
+ // src/tools/geometric/geometricShapeHelpers.ts
1175
+ var MIN_SIDE_LENGTH = 1;
1176
+ var DEFAULT_RECTANGLE_WIDTH = 180;
1177
+ var DEFAULT_RECTANGLE_HEIGHT = 120;
1178
+ var DEFAULT_ELLIPSE_WIDTH = 180;
1179
+ var DEFAULT_ELLIPSE_HEIGHT = 120;
1180
+ function toSizedBounds(anchorX, anchorY, cursorX, cursorY, forceEqualSides) {
1181
+ const rawDeltaX = cursorX - anchorX;
1182
+ const rawDeltaY = cursorY - anchorY;
1183
+ const sideLength = Math.max(Math.abs(rawDeltaX), Math.abs(rawDeltaY), MIN_SIDE_LENGTH);
1184
+ if (forceEqualSides) {
1185
+ const nextDeltaX = rawDeltaX < 0 ? -sideLength : sideLength;
1186
+ const nextDeltaY = rawDeltaY < 0 ? -sideLength : sideLength;
1187
+ return normalizeBounds(anchorX, anchorY, anchorX + nextDeltaX, anchorY + nextDeltaY);
1188
+ }
1189
+ return normalizeBounds(anchorX, anchorY, cursorX, cursorY);
1190
+ }
1191
+ function normalizeBounds(startX, startY, endX, endY) {
1192
+ const x = Math.min(startX, endX);
1193
+ const y = Math.min(startY, endY);
1194
+ const width = Math.max(Math.abs(endX - startX), MIN_SIDE_LENGTH);
1195
+ const height = Math.max(Math.abs(endY - startY), MIN_SIDE_LENGTH);
1196
+ return { x, y, width, height };
1197
+ }
1198
+ function buildSquareBounds(anchorX, anchorY, cursorX, cursorY) {
1199
+ return toSizedBounds(anchorX, anchorY, cursorX, cursorY, true);
1200
+ }
1201
+ function buildRectangleBounds(anchorX, anchorY, cursorX, cursorY) {
1202
+ return toSizedBounds(anchorX, anchorY, cursorX, cursorY, false);
1203
+ }
1204
+ function buildDefaultCenteredRectangleBounds(centerX, centerY) {
1205
+ const halfWidth = DEFAULT_RECTANGLE_WIDTH / 2;
1206
+ const halfHeight = DEFAULT_RECTANGLE_HEIGHT / 2;
1207
+ return {
1208
+ x: centerX - halfWidth,
1209
+ y: centerY - halfHeight,
1210
+ width: DEFAULT_RECTANGLE_WIDTH,
1211
+ height: DEFAULT_RECTANGLE_HEIGHT
1212
+ };
1213
+ }
1214
+ function buildRectangleSegments(width, height) {
1215
+ const topLeft = { x: 0, y: 0, z: 0.5 };
1216
+ const topRight = { x: width, y: 0, z: 0.5 };
1217
+ const bottomRight = { x: width, y: height, z: 0.5 };
1218
+ const bottomLeft = { x: 0, y: height, z: 0.5 };
1219
+ return [
1220
+ { type: "straight", path: encodePoints([topLeft, topRight]) },
1221
+ { type: "straight", path: encodePoints([topRight, bottomRight]) },
1222
+ { type: "straight", path: encodePoints([bottomRight, bottomLeft]) },
1223
+ { type: "straight", path: encodePoints([bottomLeft, topLeft]) }
1224
+ ];
1225
+ }
1226
+ function buildCircleBounds(anchorX, anchorY, cursorX, cursorY) {
1227
+ return toSizedBounds(anchorX, anchorY, cursorX, cursorY, true);
1228
+ }
1229
+ function buildEllipseBounds(anchorX, anchorY, cursorX, cursorY) {
1230
+ return toSizedBounds(anchorX, anchorY, cursorX, cursorY, false);
1231
+ }
1232
+ function buildDefaultCenteredEllipseBounds(centerX, centerY) {
1233
+ const halfWidth = DEFAULT_ELLIPSE_WIDTH / 2;
1234
+ const halfHeight = DEFAULT_ELLIPSE_HEIGHT / 2;
1235
+ return {
1236
+ x: centerX - halfWidth,
1237
+ y: centerY - halfHeight,
1238
+ width: DEFAULT_ELLIPSE_WIDTH,
1239
+ height: DEFAULT_ELLIPSE_HEIGHT
1240
+ };
1241
+ }
1242
+ function buildEllipseSegments(width, height) {
1243
+ const centerX = width / 2;
1244
+ const centerY = height / 2;
1245
+ const radiusX = width / 2;
1246
+ const radiusY = height / 2;
1247
+ const sampleCount = 64;
1248
+ const sampledPoints = [];
1249
+ for (let sampleIndex = 0; sampleIndex <= sampleCount; sampleIndex += 1) {
1250
+ const progress = sampleIndex / sampleCount;
1251
+ const angle = progress * Math.PI * 2;
1252
+ sampledPoints.push({
1253
+ x: centerX + Math.cos(angle) * radiusX,
1254
+ y: centerY + Math.sin(angle) * radiusY,
1255
+ z: 0.5
1256
+ });
1257
+ }
1258
+ return [{ type: "free", path: encodePoints(sampledPoints) }];
1259
+ }
1260
+
1261
+ // src/tools/square/states/SquareDrawingState.ts
1262
+ var SquareDrawingState = class extends GeometricDrawingState {
1263
+ static id = "square_drawing";
1264
+ getConfig() {
1265
+ return {
1266
+ idleStateId: "square_idle",
1267
+ buildConstrainedBounds: buildSquareBounds,
1268
+ buildUnconstrainedBounds: buildRectangleBounds,
1269
+ buildDefaultBounds: buildDefaultCenteredRectangleBounds,
1270
+ buildSegments: buildRectangleSegments
1271
+ };
1272
+ }
1273
+ };
1274
+
1275
+ // src/tools/circle/states/CircleIdleState.ts
1276
+ var CircleIdleState = class extends StateNode {
1277
+ static id = "circle_idle";
1278
+ onPointerDown(info) {
1279
+ this.ctx.transition("circle_drawing", info);
1280
+ }
1281
+ };
1282
+
1283
+ // src/tools/circle/states/CircleDrawingState.ts
1284
+ var CircleDrawingState = class extends GeometricDrawingState {
1285
+ static id = "circle_drawing";
1286
+ getConfig() {
1287
+ return {
1288
+ idleStateId: "circle_idle",
1289
+ buildConstrainedBounds: buildCircleBounds,
1290
+ buildUnconstrainedBounds: buildEllipseBounds,
1291
+ buildDefaultBounds: buildDefaultCenteredEllipseBounds,
1292
+ buildSegments: buildEllipseSegments
1293
+ };
1294
+ }
1295
+ };
1296
+
1031
1297
  // src/tools/eraser/states/EraserIdleState.ts
1032
1298
  var EraserIdleState = class extends StateNode {
1033
1299
  static id = "eraser_idle";
@@ -1341,6 +1607,7 @@ var Editor = class {
1341
1607
  drawStyle = {
1342
1608
  color: "black",
1343
1609
  dash: "draw",
1610
+ fill: "none",
1344
1611
  size: "m"
1345
1612
  };
1346
1613
  toolStateContext;
@@ -1400,6 +1667,8 @@ var Editor = class {
1400
1667
  getDefaultToolDefinitions() {
1401
1668
  return [
1402
1669
  { id: "pen", initialStateId: PenIdleState.id, stateConstructors: [PenIdleState, PenDrawingState] },
1670
+ { id: "square", initialStateId: SquareIdleState.id, stateConstructors: [SquareIdleState, SquareDrawingState] },
1671
+ { id: "circle", initialStateId: CircleIdleState.id, stateConstructors: [CircleIdleState, CircleDrawingState] },
1403
1672
  { id: "eraser", initialStateId: EraserIdleState.id, stateConstructors: [EraserIdleState, EraserPointingState, EraserErasingState] },
1404
1673
  { id: "select", initialStateId: SelectIdleState.id, stateConstructors: [SelectIdleState] },
1405
1674
  { id: "hand", initialStateId: HandIdleState.id, stateConstructors: [HandIdleState, HandDraggingState] }
@@ -1463,10 +1732,11 @@ var Editor = class {
1463
1732
  this.emitChange();
1464
1733
  }
1465
1734
  setViewport(partial) {
1735
+ const rawZoom = partial.zoom ?? this.viewport.zoom;
1466
1736
  this.viewport = {
1467
1737
  x: partial.x ?? this.viewport.x,
1468
1738
  y: partial.y ?? this.viewport.y,
1469
- zoom: partial.zoom ?? this.viewport.zoom
1739
+ zoom: Math.max(0.1, Math.min(4, rawZoom))
1470
1740
  };
1471
1741
  this.emitChange();
1472
1742
  }
@@ -1503,7 +1773,12 @@ var Editor = class {
1503
1773
  }
1504
1774
  loadSessionStateSnapshot(snapshot) {
1505
1775
  this.setViewport(snapshot.viewport);
1506
- this.setCurrentDrawStyle(snapshot.drawStyle);
1776
+ this.setCurrentDrawStyle({
1777
+ color: snapshot.drawStyle.color,
1778
+ dash: snapshot.drawStyle.dash,
1779
+ fill: snapshot.drawStyle.fill ?? "none",
1780
+ size: snapshot.drawStyle.size
1781
+ });
1507
1782
  if (this.tools.hasTool(snapshot.currentToolId)) {
1508
1783
  this.setCurrentTool(snapshot.currentToolId);
1509
1784
  }
@@ -1885,6 +2160,8 @@ function applyResize(editor, handle, startBounds, startShapes, pointer, lockAspe
1885
2160
  }
1886
2161
 
1887
2162
  exports.CanvasRenderer = CanvasRenderer;
2163
+ exports.CircleDrawingState = CircleDrawingState;
2164
+ exports.CircleIdleState = CircleIdleState;
1888
2165
  exports.DEFAULT_COLORS = DEFAULT_COLORS;
1889
2166
  exports.DRAG_DISTANCE_SQUARED = DRAG_DISTANCE_SQUARED;
1890
2167
  exports.DocumentStore = DocumentStore;
@@ -1901,6 +2178,8 @@ exports.PenDrawingState = PenDrawingState;
1901
2178
  exports.PenIdleState = PenIdleState;
1902
2179
  exports.STROKE_WIDTHS = STROKE_WIDTHS;
1903
2180
  exports.SelectIdleState = SelectIdleState;
2181
+ exports.SquareDrawingState = SquareDrawingState;
2182
+ exports.SquareIdleState = SquareIdleState;
1904
2183
  exports.StateNode = StateNode;
1905
2184
  exports.ToolManager = ToolManager;
1906
2185
  exports.applyMove = applyMove;