UNPKG

tldraw

Version:

A tiny little drawing editor.

541 lines (540 loc) • 19.8 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var Drawing_exports = {}; __export(Drawing_exports, { Drawing: () => Drawing }); module.exports = __toCommonJS(Drawing_exports); var import_editor = require("@tldraw/editor"); var import_default_shape_constants = require("../../shared/default-shape-constants"); class Drawing extends import_editor.StateNode { static id = "drawing"; info = {}; initialShape; shapeType = this.parent.id === "highlight" ? "highlight" : "draw"; util = this.editor.getShapeUtil(this.shapeType); isPen = false; isPenOrStylus = false; segmentMode = "free"; didJustShiftClickToExtendPreviousShapeLine = false; pagePointWhereCurrentSegmentChanged = {}; pagePointWhereNextSegmentChanged = null; lastRecordedPoint = {}; mergeNextPoint = false; currentLineLength = 0; markId = null; onEnter(info) { this.markId = null; this.info = info; this.lastRecordedPoint = this.editor.inputs.currentPagePoint.clone(); this.startShape(); } onPointerMove() { const { inputs } = this.editor; if (this.isPen && !inputs.isPen) { if (this.markId) { this.editor.bailToMark(this.markId); this.startShape(); return; } } if (this.isPenOrStylus) { if (import_editor.Vec.Dist(inputs.currentPagePoint, this.lastRecordedPoint) >= 1 / this.editor.getZoomLevel()) { this.lastRecordedPoint = inputs.currentPagePoint.clone(); this.mergeNextPoint = false; } else { this.mergeNextPoint = true; } } else { this.mergeNextPoint = false; } this.updateDrawingShape(); } onKeyDown(info) { if (info.key === "Shift") { switch (this.segmentMode) { case "free": { this.segmentMode = "starting_straight"; this.pagePointWhereNextSegmentChanged = this.editor.inputs.currentPagePoint.clone(); break; } case "starting_free": { this.segmentMode = "starting_straight"; } } } this.updateDrawingShape(); } onKeyUp(info) { if (info.key === "Shift") { this.editor.snaps.clearIndicators(); switch (this.segmentMode) { case "straight": { this.segmentMode = "starting_free"; this.pagePointWhereNextSegmentChanged = this.editor.inputs.currentPagePoint.clone(); break; } case "starting_straight": { this.pagePointWhereNextSegmentChanged = null; this.segmentMode = "free"; break; } } } this.updateDrawingShape(); } onExit() { this.editor.snaps.clearIndicators(); this.pagePointWhereCurrentSegmentChanged = this.editor.inputs.currentPagePoint.clone(); } canClose() { return this.shapeType !== "highlight"; } getIsClosed(segments, size, scale) { if (!this.canClose()) return false; const strokeWidth = import_default_shape_constants.STROKE_SIZES[size]; const firstPoint = segments[0].points[0]; const lastSegment = segments[segments.length - 1]; const lastPoint = lastSegment.points[lastSegment.points.length - 1]; return firstPoint !== lastPoint && this.currentLineLength > strokeWidth * 4 * scale && import_editor.Vec.DistMin(firstPoint, lastPoint, strokeWidth * 2 * scale); } startShape() { const { inputs: { originPagePoint, isPen } } = this.editor; this.markId = this.editor.markHistoryStoppingPoint("draw start"); const { z = 0.5 } = this.info.point; this.isPen = isPen; this.isPenOrStylus = isPen || z > 0 && z < 0.5 || z > 0.5 && z < 1; const pressure = this.isPenOrStylus ? z * 1.25 : 0.5; this.segmentMode = this.editor.inputs.shiftKey ? "straight" : "free"; this.didJustShiftClickToExtendPreviousShapeLine = false; this.lastRecordedPoint = originPagePoint.clone(); if (this.initialShape) { const shape = this.editor.getShape(this.initialShape.id); if (shape && this.segmentMode === "straight") { this.didJustShiftClickToExtendPreviousShapeLine = true; const prevSegment = (0, import_editor.last)(shape.props.segments); if (!prevSegment) throw Error("Expected a previous segment!"); const prevPoint = (0, import_editor.last)(prevSegment.points); if (!prevPoint) throw Error("Expected a previous point!"); const { x, y } = this.editor.getPointInShapeSpace(shape, originPagePoint).toFixed(); const newSegment = { type: this.segmentMode, points: [ { x: prevPoint.x, y: prevPoint.y, z: +pressure.toFixed(2) }, { x, y, z: +pressure.toFixed(2) } ] }; const prevPointPageSpace = import_editor.Mat.applyToPoint( this.editor.getShapePageTransform(shape.id), prevPoint ); this.pagePointWhereCurrentSegmentChanged = prevPointPageSpace; this.pagePointWhereNextSegmentChanged = null; const segments = [...shape.props.segments, newSegment]; if (this.currentLineLength < import_default_shape_constants.STROKE_SIZES[shape.props.size] * 4) { this.currentLineLength = this.getLineLength(segments); } const shapePartial = { id: shape.id, type: this.shapeType, props: { segments } }; if (this.canClose()) { ; shapePartial.props.isClosed = this.getIsClosed( segments, shape.props.size, shape.props.scale ); } this.editor.updateShapes([shapePartial]); return; } } this.pagePointWhereCurrentSegmentChanged = originPagePoint.clone(); const id = (0, import_editor.createShapeId)(); this.editor.createShapes([ { id, type: this.shapeType, x: originPagePoint.x, y: originPagePoint.y, props: { isPen: this.isPenOrStylus, scale: this.editor.user.getIsDynamicResizeMode() ? 1 / this.editor.getZoomLevel() : 1, segments: [ { type: this.segmentMode, points: [ { x: 0, y: 0, z: +pressure.toFixed(2) } ] } ] } } ]); this.currentLineLength = 0; this.initialShape = this.editor.getShape(id); } updateDrawingShape() { const { initialShape } = this; const { inputs } = this.editor; if (!initialShape) return; const { id, props: { size, scale } } = initialShape; const shape = this.editor.getShape(id); if (!shape) return; const { segments } = shape.props; const { x, y, z } = this.editor.getPointInShapeSpace(shape, inputs.currentPagePoint).toFixed(); const pressure = this.isPenOrStylus ? +(inputs.currentPagePoint.z * 1.25).toFixed(2) : 0.5; const newPoint = { x, y, z: pressure }; switch (this.segmentMode) { case "starting_straight": { const { pagePointWhereNextSegmentChanged } = this; if (pagePointWhereNextSegmentChanged === null) { throw Error("We should have a point where the segment changed"); } const hasMovedFarEnough = import_editor.Vec.Dist2(pagePointWhereNextSegmentChanged, inputs.currentPagePoint) > this.editor.options.dragDistanceSquared; if (hasMovedFarEnough) { this.pagePointWhereCurrentSegmentChanged = this.pagePointWhereNextSegmentChanged.clone(); this.pagePointWhereNextSegmentChanged = null; this.segmentMode = "straight"; const prevSegment = (0, import_editor.last)(segments); if (!prevSegment) throw Error("Expected a previous segment!"); const prevLastPoint = (0, import_editor.last)(prevSegment.points); if (!prevLastPoint) throw Error("Expected a previous last point!"); let newSegment; const newLastPoint = this.editor.getPointInShapeSpace(shape, this.pagePointWhereCurrentSegmentChanged).toFixed().toJson(); if (prevSegment.type === "straight") { this.currentLineLength += import_editor.Vec.Dist(prevLastPoint, newLastPoint); newSegment = { type: "straight", points: [{ ...prevLastPoint }, newLastPoint] }; const transform = this.editor.getShapePageTransform(shape); this.pagePointWhereCurrentSegmentChanged = import_editor.Mat.applyToPoint(transform, prevLastPoint); } else { newSegment = { type: "straight", points: [newLastPoint, newPoint] }; } const shapePartial = { id, type: this.shapeType, props: { segments: [...segments, newSegment] } }; if (this.canClose()) { ; shapePartial.props.isClosed = this.getIsClosed( segments, size, scale ); } this.editor.updateShapes([shapePartial]); } break; } case "starting_free": { const { pagePointWhereNextSegmentChanged } = this; if (pagePointWhereNextSegmentChanged === null) { throw Error("We should have a point where the segment changed"); } const hasMovedFarEnough = import_editor.Vec.Dist2(pagePointWhereNextSegmentChanged, inputs.currentPagePoint) > this.editor.options.dragDistanceSquared; if (hasMovedFarEnough) { this.pagePointWhereCurrentSegmentChanged = this.pagePointWhereNextSegmentChanged.clone(); this.pagePointWhereNextSegmentChanged = null; this.segmentMode = "free"; const newSegments = segments.slice(); const prevStraightSegment = newSegments[newSegments.length - 1]; const prevPoint = (0, import_editor.last)(prevStraightSegment.points); if (!prevPoint) { throw Error("No previous point!"); } const newFreeSegment = { type: "free", points: [ ...import_editor.Vec.PointsBetween(prevPoint, newPoint, 6).map((p) => ({ x: (0, import_editor.toFixed)(p.x), y: (0, import_editor.toFixed)(p.y), z: (0, import_editor.toFixed)(p.z) })) ] }; const finalSegments = [...newSegments, newFreeSegment]; if (this.currentLineLength < import_default_shape_constants.STROKE_SIZES[shape.props.size] * 4) { this.currentLineLength = this.getLineLength(finalSegments); } const shapePartial = { id, type: this.shapeType, props: { segments: finalSegments } }; if (this.canClose()) { ; shapePartial.props.isClosed = this.getIsClosed( finalSegments, size, scale ); } this.editor.updateShapes([shapePartial]); } break; } case "straight": { const newSegments = segments.slice(); const newSegment = newSegments[newSegments.length - 1]; const { pagePointWhereCurrentSegmentChanged } = this; const { ctrlKey, currentPagePoint } = this.editor.inputs; if (!pagePointWhereCurrentSegmentChanged) throw Error("We should have a point where the segment changed"); let pagePoint; let shouldSnapToAngle = false; if (this.didJustShiftClickToExtendPreviousShapeLine) { if (this.editor.inputs.isDragging) { shouldSnapToAngle = !ctrlKey; this.didJustShiftClickToExtendPreviousShapeLine = false; } else { } } else { shouldSnapToAngle = !ctrlKey; } let newPoint2 = this.editor.getPointInShapeSpace(shape, currentPagePoint).toFixed().toJson(); let didSnap = false; let snapSegment = void 0; const shouldSnap = this.editor.user.getIsSnapMode() ? !ctrlKey : ctrlKey; if (shouldSnap) { if (newSegments.length > 2) { let nearestPoint = void 0; let minDistance = 8 / this.editor.getZoomLevel(); for (let i = 0, n = segments.length - 2; i < n; i++) { const segment = segments[i]; if (!segment) break; if (segment.type === "free") continue; const first = segment.points[0]; const lastPoint = (0, import_editor.last)(segment.points); if (!(first && lastPoint)) continue; const nearestPointOnSegment = import_editor.Vec.NearestPointOnLineSegment( first, lastPoint, newPoint2 ); if (import_editor.Vec.DistMin(nearestPointOnSegment, newPoint2, minDistance)) { nearestPoint = nearestPointOnSegment.toFixed().toJson(); minDistance = import_editor.Vec.Dist(nearestPointOnSegment, newPoint2); snapSegment = segment; break; } } if (nearestPoint) { didSnap = true; newPoint2 = nearestPoint; } } } if (didSnap && snapSegment) { const transform = this.editor.getShapePageTransform(shape); const first = snapSegment.points[0]; const lastPoint = (0, import_editor.last)(snapSegment.points); if (!lastPoint) throw Error("Expected a last point!"); const A = import_editor.Mat.applyToPoint(transform, first); const B = import_editor.Mat.applyToPoint(transform, lastPoint); const snappedPoint = import_editor.Mat.applyToPoint(transform, newPoint2); this.editor.snaps.setIndicators([ { id: (0, import_editor.uniqueId)(), type: "points", points: [A, snappedPoint, B] } ]); } else { this.editor.snaps.clearIndicators(); if (shouldSnapToAngle) { const currentAngle = import_editor.Vec.Angle(pagePointWhereCurrentSegmentChanged, currentPagePoint); const snappedAngle = (0, import_editor.snapAngle)(currentAngle, 24); const angleDiff = snappedAngle - currentAngle; pagePoint = import_editor.Vec.RotWith( currentPagePoint, pagePointWhereCurrentSegmentChanged, angleDiff ); } else { pagePoint = currentPagePoint; } newPoint2 = this.editor.getPointInShapeSpace(shape, pagePoint).toFixed().toJson(); } this.currentLineLength += import_editor.Vec.Dist(newSegment.points[0], newPoint2); newSegments[newSegments.length - 1] = { ...newSegment, type: "straight", points: [newSegment.points[0], newPoint2] }; const shapePartial = { id, type: this.shapeType, props: { segments: newSegments } }; if (this.canClose()) { ; shapePartial.props.isClosed = this.getIsClosed( segments, size, scale ); } this.editor.updateShapes([shapePartial]); break; } case "free": { const newSegments = segments.slice(); const newSegment = newSegments[newSegments.length - 1]; const newPoints = [...newSegment.points]; if (newPoints.length && this.mergeNextPoint) { const { z: z2 } = newPoints[newPoints.length - 1]; newPoints[newPoints.length - 1] = { x: newPoint.x, y: newPoint.y, z: z2 ? Math.max(z2, newPoint.z) : newPoint.z }; } else { this.currentLineLength += import_editor.Vec.Dist(newPoints[newPoints.length - 1], newPoint); newPoints.push(newPoint); } newSegments[newSegments.length - 1] = { ...newSegment, points: newPoints }; if (this.currentLineLength < import_default_shape_constants.STROKE_SIZES[shape.props.size] * 4) { this.currentLineLength = this.getLineLength(newSegments); } const shapePartial = { id, type: this.shapeType, props: { segments: newSegments } }; if (this.canClose()) { ; shapePartial.props.isClosed = this.getIsClosed( newSegments, size, scale ); } this.editor.updateShapes([shapePartial]); if (newPoints.length > this.editor.options.maxPointsPerDrawShape) { this.editor.updateShapes([{ id, type: this.shapeType, props: { isComplete: true } }]); const newShapeId = (0, import_editor.createShapeId)(); const props = this.editor.getShape(id).props; this.editor.createShapes([ { id: newShapeId, type: this.shapeType, x: (0, import_editor.toFixed)(inputs.currentPagePoint.x), y: (0, import_editor.toFixed)(inputs.currentPagePoint.y), props: { isPen: this.isPenOrStylus, scale: props.scale, segments: [ { type: "free", points: [{ x: 0, y: 0, z: this.isPenOrStylus ? +(z * 1.25).toFixed() : 0.5 }] } ] } } ]); this.initialShape = (0, import_editor.structuredClone)(this.editor.getShape(newShapeId)); this.mergeNextPoint = false; this.lastRecordedPoint = inputs.currentPagePoint.clone(); this.currentLineLength = 0; } break; } } } getLineLength(segments) { let length = 0; for (const segment of segments) { for (let i = 0; i < segment.points.length - 1; i++) { const A = segment.points[i]; const B = segment.points[i + 1]; length += import_editor.Vec.Dist2(B, A); } } return Math.sqrt(length); } onPointerUp() { this.complete(); } onCancel() { this.cancel(); } onComplete() { this.complete(); } onInterrupt() { if (this.editor.inputs.isDragging) { return; } if (this.markId) { this.editor.bailToMark(this.markId); } this.cancel(); } complete() { const { initialShape } = this; if (!initialShape) return; this.editor.updateShapes([ { id: initialShape.id, type: initialShape.type, props: { isComplete: true } } ]); this.parent.transition("idle"); } cancel() { this.parent.transition("idle", this.info); } } //# sourceMappingURL=Drawing.js.map