tldraw
Version:
A tiny little drawing editor.
541 lines (540 loc) • 19.8 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;
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