tldraw
Version:
A tiny little drawing editor.
386 lines (385 loc) • 13.2 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 Resizing_exports = {};
__export(Resizing_exports, {
Resizing: () => Resizing,
rotateSelectionHandle: () => rotateSelectionHandle
});
module.exports = __toCommonJS(Resizing_exports);
var import_editor = require("@tldraw/editor");
var import_selectHelpers = require("../selectHelpers");
class Resizing extends import_editor.StateNode {
static id = "resizing";
info = {};
markId = "";
// A switch to detect when the user is holding ctrl
didHoldCommand = false;
// we transition into the resizing state from the geo pointing state, which starts with a shape of size w: 1, h: 1,
// so if the user drags x: +50, y: +50 after mouseDown, the shape will be w: 51, h: 51, which is too many pixels, alas
// so we allow passing a further offset into this state to negate such issues
creationCursorOffset = { x: 0, y: 0 };
snapshot = {};
onEnter(info) {
const { isCreating = false, creatingMarkId, creationCursorOffset = { x: 0, y: 0 } } = info;
this.info = info;
this.didHoldCommand = false;
this.parent.setCurrentToolIdMask(info.onInteractionEnd);
this.creationCursorOffset = creationCursorOffset;
this.snapshot = this._createSnapshot();
this.markId = "";
if (isCreating) {
if (creatingMarkId) {
this.markId = creatingMarkId;
} else {
const markId = this.editor.getMarkIdMatching(
`creating:${this.editor.getOnlySelectedShapeId()}`
);
if (markId) {
this.markId = markId;
}
}
} else {
this.markId = this.editor.markHistoryStoppingPoint("starting resizing");
}
if (isCreating) {
this.editor.setCursor({ type: "cross", rotation: 0 });
}
this.handleResizeStart();
this.updateShapes();
}
onTick({ elapsed }) {
const { editor } = this;
editor.edgeScrollManager.updateEdgeScrolling(elapsed);
}
onPointerMove() {
this.updateShapes();
}
onKeyDown() {
this.updateShapes();
}
onKeyUp() {
this.updateShapes();
}
onPointerUp() {
this.complete();
}
onComplete() {
this.complete();
}
onCancel() {
this.cancel();
}
cancel() {
this.editor.bailToMark(this.markId);
if (this.info.onInteractionEnd) {
this.editor.setCurrentTool(this.info.onInteractionEnd, {});
} else {
this.parent.transition("idle");
}
}
complete() {
(0, import_selectHelpers.kickoutOccludedShapes)(this.editor, this.snapshot.selectedShapeIds);
this.handleResizeEnd();
if (this.info.isCreating && this.info.onCreate) {
this.info.onCreate?.(this.editor.getOnlySelectedShape());
return;
}
if (this.editor.getInstanceState().isToolLocked && this.info.onInteractionEnd) {
this.editor.setCurrentTool(this.info.onInteractionEnd, {});
return;
}
this.parent.transition("idle");
}
handleResizeStart() {
const { shapeSnapshots } = this.snapshot;
const changes = [];
shapeSnapshots.forEach(({ shape }) => {
const util = this.editor.getShapeUtil(shape);
const change = util.onResizeStart?.(shape);
if (change) {
changes.push(change);
}
});
if (changes.length > 0) {
this.editor.updateShapes(changes);
}
}
handleResizeEnd() {
const { shapeSnapshots } = this.snapshot;
const changes = [];
shapeSnapshots.forEach(({ shape }) => {
const current = this.editor.getShape(shape.id);
const util = this.editor.getShapeUtil(shape);
const change = util.onResizeEnd?.(shape, current);
if (change) {
changes.push(change);
}
});
if (changes.length > 0) {
this.editor.updateShapes(changes);
}
}
updateShapes() {
const { altKey, shiftKey } = this.editor.inputs;
const {
frames,
shapeSnapshots,
selectionBounds,
cursorHandleOffset,
selectedShapeIds,
selectionRotation,
canShapesDeform
} = this.snapshot;
let isAspectRatioLocked = shiftKey || !canShapesDeform;
if (shapeSnapshots.size === 1) {
const onlySnapshot = [...shapeSnapshots.values()][0];
if (this.editor.isShapeOfType(onlySnapshot.shape, "text")) {
isAspectRatioLocked = !(this.info.handle === "left" || this.info.handle === "right");
}
}
const { ctrlKey } = this.editor.inputs;
const currentPagePoint = this.editor.inputs.currentPagePoint.clone().sub(cursorHandleOffset).sub(this.creationCursorOffset);
const originPagePoint = this.editor.inputs.originPagePoint.clone().sub(cursorHandleOffset);
if (this.editor.getInstanceState().isGridMode && !ctrlKey) {
const { gridSize } = this.editor.getDocumentSettings();
currentPagePoint.snapToGrid(gridSize);
}
const dragHandle = this.info.handle;
const scaleOriginHandle = rotateSelectionHandle(dragHandle, Math.PI);
this.editor.snaps.clearIndicators();
const shouldSnap = this.editor.user.getIsSnapMode() ? !ctrlKey : ctrlKey;
if (shouldSnap && selectionRotation % import_editor.HALF_PI === 0) {
const { nudge } = this.editor.snaps.shapeBounds.snapResizeShapes({
dragDelta: import_editor.Vec.Sub(currentPagePoint, originPagePoint),
initialSelectionPageBounds: this.snapshot.initialSelectionPageBounds,
handle: rotateSelectionHandle(dragHandle, selectionRotation),
isAspectRatioLocked,
isResizingFromCenter: altKey
});
currentPagePoint.add(nudge);
}
const scaleOriginPage = import_editor.Vec.RotWith(
altKey ? selectionBounds.center : selectionBounds.getHandlePoint(scaleOriginHandle),
selectionBounds.point,
selectionRotation
);
const distanceFromScaleOriginNow = import_editor.Vec.Sub(currentPagePoint, scaleOriginPage).rot(
-selectionRotation
);
const distanceFromScaleOriginAtStart = import_editor.Vec.Sub(originPagePoint, scaleOriginPage).rot(
-selectionRotation
);
const scale = import_editor.Vec.DivV(distanceFromScaleOriginNow, distanceFromScaleOriginAtStart);
if (!Number.isFinite(scale.x)) scale.x = 1;
if (!Number.isFinite(scale.y)) scale.y = 1;
const isXLocked = dragHandle === "top" || dragHandle === "bottom";
const isYLocked = dragHandle === "left" || dragHandle === "right";
if (isAspectRatioLocked) {
if (isYLocked) {
scale.y = Math.abs(scale.x);
} else if (isXLocked) {
scale.x = Math.abs(scale.y);
} else if (Math.abs(scale.x) > Math.abs(scale.y)) {
scale.y = Math.abs(scale.x) * (scale.y < 0 ? -1 : 1);
} else {
scale.x = Math.abs(scale.y) * (scale.x < 0 ? -1 : 1);
}
} else {
if (isXLocked) {
scale.x = 1;
}
if (isYLocked) {
scale.y = 1;
}
}
if (!this.info.isCreating) {
this.updateCursor({
dragHandle,
isFlippedX: scale.x < 0,
isFlippedY: scale.y < 0,
rotation: selectionRotation
});
}
for (const id of shapeSnapshots.keys()) {
const snapshot = shapeSnapshots.get(id);
this.editor.resizeShape(id, scale, {
initialShape: snapshot.shape,
initialBounds: snapshot.bounds,
initialPageTransform: snapshot.pageTransform,
dragHandle,
mode: selectedShapeIds.length === 1 && id === selectedShapeIds[0] ? "resize_bounds" : "scale_shape",
scaleOrigin: scaleOriginPage,
isAspectRatioLocked,
scaleAxisRotation: selectionRotation,
skipStartAndEndCallbacks: true
});
}
if (this.editor.inputs.ctrlKey) {
this.didHoldCommand = true;
for (const { id, children } of frames) {
if (!children.length) continue;
const initial = shapeSnapshots.get(id).shape;
const current = this.editor.getShape(id);
if (!(initial && current)) continue;
const dx = current.x - initial.x;
const dy = current.y - initial.y;
const delta = new import_editor.Vec(dx, dy).rot(-initial.rotation);
if (delta.x !== 0 || delta.y !== 0) {
for (const child of children) {
this.editor.updateShape({
id: child.id,
type: child.type,
x: child.x - delta.x,
y: child.y - delta.y
});
}
}
}
} else if (this.didHoldCommand) {
this.didHoldCommand = false;
for (const { children } of frames) {
if (!children.length) continue;
for (const child of children) {
this.editor.updateShape({
id: child.id,
type: child.type,
x: child.x,
y: child.y
});
}
}
}
}
// ---
updateCursor({
dragHandle,
isFlippedX,
isFlippedY,
rotation
}) {
const nextCursor = { ...this.editor.getInstanceState().cursor };
switch (dragHandle) {
case "top_left":
case "bottom_right": {
nextCursor.type = "nwse-resize";
if (isFlippedX !== isFlippedY) {
nextCursor.type = "nesw-resize";
}
break;
}
case "top_right":
case "bottom_left": {
nextCursor.type = "nesw-resize";
if (isFlippedX !== isFlippedY) {
nextCursor.type = "nwse-resize";
}
break;
}
}
nextCursor.rotation = rotation;
this.editor.setCursor(nextCursor);
}
onExit() {
this.parent.setCurrentToolIdMask(void 0);
this.editor.setCursor({ type: "default", rotation: 0 });
this.editor.snaps.clearIndicators();
}
_createSnapshot() {
const selectedShapeIds = this.editor.getSelectedShapeIds();
const selectionRotation = this.editor.getSelectionRotation();
const {
inputs: { originPagePoint }
} = this.editor;
const selectionBounds = this.editor.getSelectionRotatedPageBounds();
const dragHandlePoint = import_editor.Vec.RotWith(
selectionBounds.getHandlePoint(this.info.handle),
selectionBounds.point,
selectionRotation
);
const cursorHandleOffset = import_editor.Vec.Sub(originPagePoint, dragHandlePoint);
const shapeSnapshots = /* @__PURE__ */ new Map();
const frames = [];
selectedShapeIds.forEach((id) => {
const shape = this.editor.getShape(id);
if (shape) {
if (shape.type === "frame") {
frames.push({
id,
children: (0, import_editor.compact)(
this.editor.getSortedChildIdsForParent(shape).map((id2) => this.editor.getShape(id2))
)
});
}
shapeSnapshots.set(shape.id, this._createShapeSnapshot(shape));
if (this.editor.isShapeOfType(shape, "frame") && selectedShapeIds.length === 1)
return;
this.editor.visitDescendants(shape.id, (descendantId) => {
const descendent = this.editor.getShape(descendantId);
if (descendent) {
shapeSnapshots.set(descendent.id, this._createShapeSnapshot(descendent));
if (this.editor.isShapeOfType(descendent, "frame")) {
return false;
}
}
});
}
});
const canShapesDeform = ![...shapeSnapshots.values()].some(
(shape) => !(0, import_editor.areAnglesCompatible)(shape.pageRotation, selectionRotation) || shape.isAspectRatioLocked
);
return {
shapeSnapshots,
selectionBounds,
cursorHandleOffset,
selectionRotation,
selectedShapeIds,
canShapesDeform,
initialSelectionPageBounds: this.editor.getSelectionPageBounds(),
frames
};
}
_createShapeSnapshot(shape) {
const pageTransform = this.editor.getShapePageTransform(shape);
const util = this.editor.getShapeUtil(shape);
return {
shape,
bounds: this.editor.getShapeGeometry(shape).bounds,
pageTransform,
pageRotation: import_editor.Mat.Decompose(pageTransform).rotation,
isAspectRatioLocked: util.isAspectRatioLocked(shape)
};
}
}
const ORDERED_SELECTION_HANDLES = [
"top",
"top_right",
"right",
"bottom_right",
"bottom",
"bottom_left",
"left",
"top_left"
];
function rotateSelectionHandle(handle, rotation) {
rotation = rotation % import_editor.PI2;
const numSteps = Math.round(rotation / (import_editor.PI / 4));
const currentIndex = ORDERED_SELECTION_HANDLES.indexOf(handle);
return ORDERED_SELECTION_HANDLES[(currentIndex + numSteps) % ORDERED_SELECTION_HANDLES.length];
}
//# sourceMappingURL=Resizing.js.map