UNPKG

tldraw

Version:

A tiny little drawing editor.

543 lines (542 loc) • 21.3 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; // Cache for current segment's points to avoid repeated b64 decode/encode currentSegmentPoints = []; markId = null; onEnter(info) { this.markId = null; this.info = info; this.lastRecordedPoint = this.editor.inputs.getCurrentPagePoint().clone(); this.startShape(); } onPointerMove() { const { inputs } = this.editor; const isPen = inputs.getIsPen(); if (this.isPen && !isPen) { if (this.markId) { this.editor.bailToMark(this.markId); this.startShape(); return; } } if (this.isPenOrStylus) { const currentPagePoint = inputs.getCurrentPagePoint(); if (import_editor.Vec.Dist(currentPagePoint, this.lastRecordedPoint) >= 1 / this.editor.getZoomLevel()) { this.lastRecordedPoint = 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.getCurrentPagePoint().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.getCurrentPagePoint().clone(); break; } case "starting_straight": { this.pagePointWhereNextSegmentChanged = null; this.segmentMode = "free"; break; } } } this.updateDrawingShape(); } onExit() { this.editor.snaps.clearIndicators(); this.pagePointWhereCurrentSegmentChanged = this.editor.inputs.getCurrentPagePoint().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 = import_editor.b64Vecs.decodeFirstPoint(segments[0].path); const lastSegment = segments[segments.length - 1]; const lastPoint = import_editor.b64Vecs.decodeLastPoint(lastSegment.path); return firstPoint !== null && lastPoint !== null && firstPoint !== lastPoint && this.currentLineLength > strokeWidth * 4 * scale && import_editor.Vec.DistMin(firstPoint, lastPoint, strokeWidth * 2 * scale); } startShape() { const inputs = this.editor.inputs; const originPagePoint = inputs.getOriginPagePoint(); const isPen = inputs.getIsPen(); this.markId = this.editor.markHistoryStoppingPoint("draw start"); const { z = 0.5 } = this.info.point; this.isPen = isPen; this.isPenOrStylus = isPen && z !== 0 || z > 0 && z < 0.5 || z > 0.5 && z < 1; const pressure = this.isPenOrStylus ? z * 1.25 : 0.5; this.segmentMode = this.editor.inputs.getShiftKey() ? "straight" : "free"; this.didJustShiftClickToExtendPreviousShapeLine = false; this.lastRecordedPoint = originPagePoint.clone(); if (this.initialShape) { const shape2 = this.editor.getShape(this.initialShape.id); if (shape2 && this.segmentMode === "straight") { this.didJustShiftClickToExtendPreviousShapeLine = true; const prevSegment = (0, import_editor.last)(shape2.props.segments); if (!prevSegment) throw Error("Expected a previous segment!"); const prevPoint = import_editor.b64Vecs.decodeLastPoint(prevSegment.path); if (!prevPoint) throw Error("Expected a previous point!"); const { x, y } = this.editor.getPointInShapeSpace(shape2, originPagePoint).toFixed(); const newSegment = { type: this.segmentMode, path: import_editor.b64Vecs.encodePoints([ { 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(shape2.id), prevPoint ); this.pagePointWhereCurrentSegmentChanged = prevPointPageSpace; this.pagePointWhereNextSegmentChanged = null; const segments = [...shape2.props.segments, newSegment]; if (this.currentLineLength < import_default_shape_constants.STROKE_SIZES[shape2.props.size] * 4) { this.currentLineLength = this.getLineLength(segments); } const shapePartial = { id: shape2.id, type: this.shapeType, props: { segments } }; if (this.canClose()) { ; shapePartial.props.isClosed = this.getIsClosed( segments, shape2.props.size, shape2.props.scale ); } this.editor.updateShapes([shapePartial]); return; } } this.pagePointWhereCurrentSegmentChanged = originPagePoint.clone(); const id = (0, import_editor.createShapeId)(); const initialPoint = new import_editor.Vec(0, 0, +pressure.toFixed(2)); this.currentSegmentPoints = [initialPoint]; this.editor.createShape({ id, type: this.shapeType, x: originPagePoint.x, y: originPagePoint.y, props: { isPen: this.isPenOrStylus, scale: this.editor.getResizeScaleFactor(), segments: [ { type: this.segmentMode, path: import_editor.b64Vecs.encodePoints([initialPoint]) } ] } }); const shape = this.editor.getShape(id); if (!shape) { this.cancel(); return; } 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 currentPagePoint = inputs.getCurrentPagePoint(); const { x, y, z } = this.editor.getPointInShapeSpace(shape, currentPagePoint).toFixed(); const pressure = this.isPenOrStylus ? +(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.getCurrentPagePoint()) > 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 = import_editor.b64Vecs.decodeLastPoint(prevSegment.path); 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", path: import_editor.b64Vecs.encodePoints([prevLastPoint, newLastPoint]) }; const transform = this.editor.getShapePageTransform(shape); this.pagePointWhereCurrentSegmentChanged = import_editor.Mat.applyToPoint(transform, prevLastPoint); } else { newSegment = { type: "straight", path: import_editor.b64Vecs.encodePoints([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.getCurrentPagePoint()) > 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 = import_editor.b64Vecs.decodeLastPoint(prevStraightSegment.path); if (!prevPoint) { throw Error("No previous point!"); } const interpolatedPoints = import_editor.Vec.PointsBetween(prevPoint, newPoint, 6).map( (p) => new import_editor.Vec((0, import_editor.toFixed)(p.x), (0, import_editor.toFixed)(p.y), (0, import_editor.toFixed)(p.z)) ); this.currentSegmentPoints = interpolatedPoints; const newFreeSegment = { type: "free", path: import_editor.b64Vecs.encodePoints(interpolatedPoints) }; 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 inputs2 = this.editor.inputs; const ctrlKey = inputs2.getCtrlKey(); const currentPagePoint2 = inputs2.getCurrentPagePoint(); 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.getIsDragging()) { shouldSnapToAngle = !ctrlKey; this.didJustShiftClickToExtendPreviousShapeLine = false; } else { } } else { shouldSnapToAngle = !ctrlKey; } let newPoint2 = this.editor.getPointInShapeSpace(shape, currentPagePoint2).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 = import_editor.b64Vecs.decodeFirstPoint(segment.path); const lastPoint = import_editor.b64Vecs.decodeLastPoint(segment.path); 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 = import_editor.b64Vecs.decodeFirstPoint(snapSegment.path); const lastPoint = import_editor.b64Vecs.decodeLastPoint(snapSegment.path); if (!first || !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, currentPagePoint2); const snappedAngle = (0, import_editor.snapAngle)(currentAngle, 24); const angleDiff = snappedAngle - currentAngle; pagePoint = import_editor.Vec.RotWith( currentPagePoint2, pagePointWhereCurrentSegmentChanged, angleDiff ); } else { pagePoint = currentPagePoint2.clone(); } newPoint2 = this.editor.getPointInShapeSpace(shape, pagePoint).toFixed().toJson(); } this.currentLineLength += newSegments.length && import_editor.b64Vecs.decodeFirstPoint(newSegment.path) ? import_editor.Vec.Dist(import_editor.b64Vecs.decodeFirstPoint(newSegment.path), import_editor.Vec.From(newPoint2)) : 0; newSegments[newSegments.length - 1] = { ...newSegment, type: "straight", path: import_editor.b64Vecs.encodePoints([ import_editor.b64Vecs.decodeFirstPoint(newSegment.path), import_editor.Vec.From(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 cachedPoints = this.currentSegmentPoints; if (cachedPoints.length && this.mergeNextPoint) { const lastPoint = cachedPoints[cachedPoints.length - 1]; lastPoint.x = newPoint.x; lastPoint.y = newPoint.y; lastPoint.z = lastPoint.z ? Math.max(lastPoint.z, newPoint.z) : newPoint.z; } else { this.currentLineLength += cachedPoints.length ? import_editor.Vec.Dist(cachedPoints[cachedPoints.length - 1], newPoint) : 0; cachedPoints.push(new import_editor.Vec(newPoint.x, newPoint.y, newPoint.z)); } const newSegments = segments.slice(); const newSegment = newSegments[newSegments.length - 1]; newSegments[newSegments.length - 1] = { ...newSegment, path: import_editor.b64Vecs.encodePoints(cachedPoints) }; 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 (cachedPoints.length > this.util.options.maxPointsPerShape) { this.editor.updateShapes([{ id, type: this.shapeType, props: { isComplete: true } }]); const newShapeId = (0, import_editor.createShapeId)(); const props = this.editor.getShape(id).props; if (!this.editor.canCreateShapes([newShapeId])) return this.cancel(); const currentPagePoint2 = inputs.getCurrentPagePoint(); const initialPoint = new import_editor.Vec(0, 0, this.isPenOrStylus ? +(z * 1.25).toFixed() : 0.5); this.currentSegmentPoints = [initialPoint]; this.editor.createShape({ id: newShapeId, type: this.shapeType, x: (0, import_editor.toFixed)(currentPagePoint2.x), y: (0, import_editor.toFixed)(currentPagePoint2.y), props: { isPen: this.isPenOrStylus, scale: props.scale, segments: [ { type: "free", path: import_editor.b64Vecs.encodePoints([initialPoint]) } ] } }); const shape2 = this.editor.getShape(newShapeId); if (!shape2) { return this.cancel(); } this.initialShape = (0, import_editor.structuredClone)(shape2); this.mergeNextPoint = false; this.lastRecordedPoint = currentPagePoint2.clone(); this.currentLineLength = 0; } break; } } } getLineLength(segments) { let length = 0; for (let j = 0; j < segments.length; j++) { const points = import_editor.b64Vecs.decodePoints(segments[j].path); for (let i = 0; i < points.length - 1; i++) { length += import_editor.Vec.Dist2(points[i], points[i + 1]); } } return Math.sqrt(length); } onPointerUp() { this.complete(); } onCancel() { this.cancel(); } onComplete() { this.complete(); } onInterrupt() { if (this.editor.inputs.getIsDragging()) { 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