@tsdraw/core 0.6.2 → 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
@@ -298,11 +298,16 @@ var CanvasRenderer = class {
298
298
  }
299
299
  ctx.restore();
300
300
  }
301
+ // Paints a single stroke
301
302
  paintStroke(ctx, shape) {
302
303
  const width = (STROKE_WIDTHS[shape.props.size] ?? 3.5) * shape.props.scale;
303
304
  const samples = flattenSegments(shape);
304
305
  if (samples.length === 0) return;
305
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
+ }
306
311
  if (shape.props.dash !== "draw") {
307
312
  this.paintDashedStroke(ctx, samples, width, color, shape.props.dash);
308
313
  return;
@@ -349,6 +354,30 @@ var CanvasRenderer = class {
349
354
  ctx.stroke();
350
355
  ctx.restore();
351
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
+ }
352
381
  };
353
382
  var PRESSURE_FLOOR = 0.025;
354
383
  var STYLUS_CURVE = (t) => t * 0.65 + Math.sin(t * Math.PI / 2) * 0.35;
@@ -362,7 +391,7 @@ function remap(value, inRange, outRange, clamp = false) {
362
391
  return outLo + (outHi - outLo) * clamped;
363
392
  }
364
393
  function strokeConfig(shape, width) {
365
- const done = shape.props.isComplete;
394
+ const done = shape.props.isComplete || shape.props.isClosed === true;
366
395
  if (shape.props.isPen) {
367
396
  return {
368
397
  size: 1 + width * 1.2,
@@ -1035,6 +1064,236 @@ var PenDrawingState = class extends StateNode {
1035
1064
  }
1036
1065
  };
1037
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
+
1038
1297
  // src/tools/eraser/states/EraserIdleState.ts
1039
1298
  var EraserIdleState = class extends StateNode {
1040
1299
  static id = "eraser_idle";
@@ -1348,6 +1607,7 @@ var Editor = class {
1348
1607
  drawStyle = {
1349
1608
  color: "black",
1350
1609
  dash: "draw",
1610
+ fill: "none",
1351
1611
  size: "m"
1352
1612
  };
1353
1613
  toolStateContext;
@@ -1407,6 +1667,8 @@ var Editor = class {
1407
1667
  getDefaultToolDefinitions() {
1408
1668
  return [
1409
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] },
1410
1672
  { id: "eraser", initialStateId: EraserIdleState.id, stateConstructors: [EraserIdleState, EraserPointingState, EraserErasingState] },
1411
1673
  { id: "select", initialStateId: SelectIdleState.id, stateConstructors: [SelectIdleState] },
1412
1674
  { id: "hand", initialStateId: HandIdleState.id, stateConstructors: [HandIdleState, HandDraggingState] }
@@ -1511,7 +1773,12 @@ var Editor = class {
1511
1773
  }
1512
1774
  loadSessionStateSnapshot(snapshot) {
1513
1775
  this.setViewport(snapshot.viewport);
1514
- 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
+ });
1515
1782
  if (this.tools.hasTool(snapshot.currentToolId)) {
1516
1783
  this.setCurrentTool(snapshot.currentToolId);
1517
1784
  }
@@ -1893,6 +2160,8 @@ function applyResize(editor, handle, startBounds, startShapes, pointer, lockAspe
1893
2160
  }
1894
2161
 
1895
2162
  exports.CanvasRenderer = CanvasRenderer;
2163
+ exports.CircleDrawingState = CircleDrawingState;
2164
+ exports.CircleIdleState = CircleIdleState;
1896
2165
  exports.DEFAULT_COLORS = DEFAULT_COLORS;
1897
2166
  exports.DRAG_DISTANCE_SQUARED = DRAG_DISTANCE_SQUARED;
1898
2167
  exports.DocumentStore = DocumentStore;
@@ -1909,6 +2178,8 @@ exports.PenDrawingState = PenDrawingState;
1909
2178
  exports.PenIdleState = PenIdleState;
1910
2179
  exports.STROKE_WIDTHS = STROKE_WIDTHS;
1911
2180
  exports.SelectIdleState = SelectIdleState;
2181
+ exports.SquareDrawingState = SquareDrawingState;
2182
+ exports.SquareIdleState = SquareIdleState;
1912
2183
  exports.StateNode = StateNode;
1913
2184
  exports.ToolManager = ToolManager;
1914
2185
  exports.applyMove = applyMove;