tldraw
Version:
A tiny little drawing editor.
543 lines (542 loc) • 21.3 kB
JavaScript
"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