mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 12:55:00 +00:00
769 lines
25 KiB
TypeScript
769 lines
25 KiB
TypeScript
import {
|
|
TLBoundsCorner,
|
|
TLBoundsEdge,
|
|
TLBoundsEventHandler,
|
|
TLBoundsHandleEventHandler,
|
|
TLCanvasEventHandler,
|
|
TLPointerEventHandler,
|
|
TLKeyboardEventHandler,
|
|
TLShapeCloneHandler,
|
|
Utils,
|
|
} from '@tldraw/core';
|
|
import {
|
|
SessionType,
|
|
TDShapeType,
|
|
CLONING_DISTANCE,
|
|
DEAD_ZONE,
|
|
} from '@toeverything/components/board-types';
|
|
import { BaseTool, TLDR } from '@toeverything/components/board-state';
|
|
import Vec from '@tldraw/vec';
|
|
|
|
enum Status {
|
|
Idle = 'idle',
|
|
Creating = 'creating',
|
|
Pinching = 'pinching',
|
|
PointingCanvas = 'pointingCanvas',
|
|
PointingHandle = 'pointingHandle',
|
|
PointingBounds = 'pointingBounds',
|
|
PointingClone = 'pointingClone',
|
|
TranslatingClone = 'translatingClone',
|
|
PointingBoundsHandle = 'pointingBoundsHandle',
|
|
TranslatingHandle = 'translatingHandle',
|
|
Translating = 'translating',
|
|
Transforming = 'transforming',
|
|
Rotating = 'rotating',
|
|
Brushing = 'brushing',
|
|
GridCloning = 'gridCloning',
|
|
ClonePainting = 'clonePainting',
|
|
}
|
|
|
|
export class SelectTool extends BaseTool<Status> {
|
|
override type = 'select' as const;
|
|
|
|
pointedId?: string;
|
|
|
|
selectedGroupId?: string;
|
|
|
|
pointedHandleId?: 'start' | 'end' | 'bend';
|
|
|
|
pointedBoundsHandle?:
|
|
| TLBoundsCorner
|
|
| TLBoundsEdge
|
|
| 'rotate'
|
|
| 'center'
|
|
| 'left'
|
|
| 'right';
|
|
|
|
pointedLinkHandleId?: 'left' | 'center' | 'right';
|
|
|
|
/* --------------------- Methods -------------------- */
|
|
|
|
private deselect(id: string) {
|
|
this.app.select(...this.app.selectedIds.filter(oid => oid !== id));
|
|
}
|
|
|
|
private select(id: string) {
|
|
this.app.select(id);
|
|
}
|
|
|
|
private push_select(id: string) {
|
|
const shape = this.app.getShape(id);
|
|
this.app.select(
|
|
...this.app.selectedIds.filter(oid => oid !== shape.parentId),
|
|
id
|
|
);
|
|
}
|
|
|
|
private select_none() {
|
|
this.app.selectNone();
|
|
}
|
|
|
|
override onEnter = () => {
|
|
this.set_status(Status.Idle);
|
|
};
|
|
|
|
override onExit = () => {
|
|
this.set_status(Status.Idle);
|
|
};
|
|
|
|
clonePaint = (point: number[]) => {
|
|
if (this.app.selectedIds.length === 0) return;
|
|
|
|
const shapes = this.app.selectedIds.map(id => this.app.getShape(id));
|
|
|
|
const bounds = Utils.expandBounds(
|
|
Utils.getCommonBounds(shapes.map(TLDR.get_bounds)),
|
|
16
|
|
);
|
|
|
|
const center = Utils.getBoundsCenter(bounds);
|
|
|
|
const size = [bounds.width, bounds.height];
|
|
|
|
const gridPoint = [
|
|
center[0] +
|
|
size[0] *
|
|
Math.floor((point[0] + size[0] / 2 - center[0]) / size[0]),
|
|
center[1] +
|
|
size[1] *
|
|
Math.floor((point[1] + size[1] / 2 - center[1]) / size[1]),
|
|
];
|
|
|
|
const centeredBounds = Utils.centerBounds(bounds, gridPoint);
|
|
|
|
const hit = this.app.shapes.some(shape =>
|
|
TLDR.get_shape_util(shape).hitTestBounds(shape, centeredBounds)
|
|
);
|
|
|
|
if (!hit) {
|
|
this.app.duplicate(this.app.selectedIds, gridPoint);
|
|
}
|
|
};
|
|
|
|
getShapeClone = (
|
|
id: string,
|
|
side:
|
|
| 'top'
|
|
| 'right'
|
|
| 'bottom'
|
|
| 'left'
|
|
| 'topLeft'
|
|
| 'topRight'
|
|
| 'bottomRight'
|
|
| 'bottomLeft'
|
|
) => {
|
|
const shape = this.app.getShape(id);
|
|
const utils = TLDR.get_shape_util(shape);
|
|
|
|
if (utils.canClone) {
|
|
const bounds = utils.getBounds(shape);
|
|
|
|
const center = utils.getCenter(shape);
|
|
|
|
let point = {
|
|
top: [
|
|
bounds.minX,
|
|
bounds.minY - (bounds.height + CLONING_DISTANCE),
|
|
],
|
|
right: [bounds.maxX + CLONING_DISTANCE, bounds.minY],
|
|
bottom: [bounds.minX, bounds.maxY + CLONING_DISTANCE],
|
|
left: [
|
|
bounds.minX - (bounds.width + CLONING_DISTANCE),
|
|
bounds.minY,
|
|
],
|
|
topLeft: [
|
|
bounds.minX - (bounds.width + CLONING_DISTANCE),
|
|
bounds.minY - (bounds.height + CLONING_DISTANCE),
|
|
],
|
|
topRight: [
|
|
bounds.maxX + CLONING_DISTANCE,
|
|
bounds.minY - (bounds.height + CLONING_DISTANCE),
|
|
],
|
|
bottomLeft: [
|
|
bounds.minX - (bounds.width + CLONING_DISTANCE),
|
|
bounds.maxY + CLONING_DISTANCE,
|
|
],
|
|
bottomRight: [
|
|
bounds.maxX + CLONING_DISTANCE,
|
|
bounds.maxY + CLONING_DISTANCE,
|
|
],
|
|
}[side];
|
|
|
|
if (shape.rotation !== 0) {
|
|
const newCenter = Vec.add(point, [
|
|
bounds.width / 2,
|
|
bounds.height / 2,
|
|
]);
|
|
|
|
const rotatedCenter = Vec.rotWith(
|
|
newCenter,
|
|
center,
|
|
shape.rotation || 0
|
|
);
|
|
|
|
point = Vec.sub(rotatedCenter, [
|
|
bounds.width / 2,
|
|
bounds.height / 2,
|
|
]);
|
|
}
|
|
|
|
const id = Utils.uniqueId();
|
|
|
|
const clone = {
|
|
...shape,
|
|
id,
|
|
point,
|
|
};
|
|
|
|
// if (clone.type === TDShapeType.Sticky) {
|
|
// clone.text = '';
|
|
// }
|
|
|
|
return clone;
|
|
}
|
|
|
|
return;
|
|
};
|
|
|
|
/* ----------------- Event Handlers ----------------- */
|
|
|
|
override onCancel = () => {
|
|
if (this.app.pageState.editingId) {
|
|
this.app.setEditingId();
|
|
} else {
|
|
this.select_none();
|
|
}
|
|
this.app.cancelSession();
|
|
this.set_status(Status.Idle);
|
|
};
|
|
|
|
override onKeyDown: TLKeyboardEventHandler = (key, info, e) => {
|
|
switch (key) {
|
|
case 'Escape': {
|
|
this.onCancel();
|
|
break;
|
|
}
|
|
case 'Tab': {
|
|
if (
|
|
!this.app.pageState.editingId &&
|
|
this.status === Status.Idle &&
|
|
this.app.selectedIds.length === 1
|
|
) {
|
|
const [selectedId] = this.app.selectedIds;
|
|
const clonedShape = this.getShapeClone(selectedId, 'right');
|
|
|
|
if (clonedShape) {
|
|
this.app.createShapes(clonedShape);
|
|
this.set_status(Status.Idle);
|
|
// if (clonedShape.type === TDShapeType.Sticky) {
|
|
// this.app.select(clonedShape.id);
|
|
// this.app.setEditingId(clonedShape.id);
|
|
// }
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 'Meta':
|
|
case 'Control':
|
|
case 'Alt': {
|
|
this.app.updateSession();
|
|
break;
|
|
}
|
|
case 'Enter': {
|
|
const { pageState } = this.app;
|
|
if (
|
|
pageState.selectedIds.length === 1 &&
|
|
!pageState.editingId
|
|
) {
|
|
this.app.setEditingId(pageState.selectedIds[0]);
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
override onKeyUp: TLKeyboardEventHandler = (key, info) => {
|
|
if (
|
|
this.status === Status.ClonePainting &&
|
|
!(info.altKey && info.shiftKey)
|
|
) {
|
|
this.set_status(Status.Idle);
|
|
return;
|
|
}
|
|
|
|
/* noop */
|
|
if (key === 'Meta' || key === 'Control' || key === 'Alt') {
|
|
this.app.updateSession();
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Keyup is handled on BaseTool
|
|
|
|
// Pointer Events (generic)
|
|
|
|
override onPointerMove: TLPointerEventHandler = (info, e) => {
|
|
const { originPoint, currentPoint } = this.app;
|
|
|
|
switch (this.status) {
|
|
case Status.PointingBoundsHandle: {
|
|
if (!this.pointedBoundsHandle)
|
|
throw Error('No pointed bounds handle');
|
|
if (Vec.dist(originPoint, currentPoint) > DEAD_ZONE) {
|
|
if (this.pointedBoundsHandle === 'rotate') {
|
|
// Stat a rotate session
|
|
this.set_status(Status.Rotating);
|
|
this.app.startSession(SessionType.Rotate);
|
|
} else if (
|
|
this.pointedBoundsHandle === 'center' ||
|
|
this.pointedBoundsHandle === 'left' ||
|
|
this.pointedBoundsHandle === 'right'
|
|
) {
|
|
this.set_status(Status.Translating);
|
|
this.app.startSession(
|
|
SessionType.Translate,
|
|
false,
|
|
this.pointedBoundsHandle
|
|
);
|
|
} else {
|
|
// Stat a transform session
|
|
this.set_status(Status.Transforming);
|
|
const idsToTransform = this.app.selectedIds.flatMap(
|
|
id =>
|
|
TLDR.get_document_branch(
|
|
this.app.state,
|
|
id,
|
|
this.app.currentPageId
|
|
)
|
|
);
|
|
if (idsToTransform.length === 1) {
|
|
// if only one shape is selected, transform single
|
|
this.app.startSession(
|
|
SessionType.TransformSingle,
|
|
idsToTransform[0],
|
|
this.pointedBoundsHandle
|
|
);
|
|
} else {
|
|
// otherwise, transform
|
|
this.app.startSession(
|
|
SessionType.Transform,
|
|
this.pointedBoundsHandle
|
|
);
|
|
}
|
|
}
|
|
|
|
// Also update the session with the current point
|
|
this.app.updateSession();
|
|
}
|
|
break;
|
|
}
|
|
case Status.PointingCanvas: {
|
|
if (Vec.dist(originPoint, currentPoint) > DEAD_ZONE) {
|
|
this.app.startSession(SessionType.Brush);
|
|
this.set_status(Status.Brushing);
|
|
}
|
|
break;
|
|
}
|
|
case Status.PointingClone: {
|
|
if (Vec.dist(originPoint, currentPoint) > DEAD_ZONE) {
|
|
this.set_status(Status.TranslatingClone);
|
|
this.app.startSession(SessionType.Translate);
|
|
this.app.updateSession();
|
|
}
|
|
break;
|
|
}
|
|
case Status.PointingBounds: {
|
|
if (Vec.dist(originPoint, currentPoint) > DEAD_ZONE) {
|
|
this.set_status(Status.Translating);
|
|
this.app.startSession(SessionType.Translate);
|
|
this.app.updateSession();
|
|
}
|
|
break;
|
|
}
|
|
case Status.PointingHandle: {
|
|
if (Vec.dist(originPoint, currentPoint) > DEAD_ZONE) {
|
|
this.set_status(Status.TranslatingHandle);
|
|
const selectedShape = this.app.getShape(
|
|
this.app.selectedIds[0]
|
|
);
|
|
if (selectedShape) {
|
|
if (this.pointedHandleId === 'bend') {
|
|
this.app.startSession(
|
|
SessionType.Handle,
|
|
selectedShape.id,
|
|
this.pointedHandleId
|
|
);
|
|
this.app.updateSession();
|
|
} else {
|
|
this.app.startSession(
|
|
SessionType.Arrow,
|
|
selectedShape.id,
|
|
this.pointedHandleId,
|
|
false
|
|
);
|
|
this.app.updateSession();
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case Status.ClonePainting: {
|
|
this.clonePaint(currentPoint);
|
|
break;
|
|
}
|
|
default: {
|
|
if (this.app.session) {
|
|
this.app.updateSession();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
override onPointerDown: TLPointerEventHandler = (info, e) => {
|
|
if (info.target === 'canvas' && this.status === Status.Idle) {
|
|
const { currentPoint } = this.app;
|
|
|
|
if (info.spaceKey && e.buttons === 1) return;
|
|
|
|
if (this.status === Status.Idle && info.altKey && info.shiftKey) {
|
|
this.set_status(Status.ClonePainting);
|
|
this.clonePaint(currentPoint);
|
|
return;
|
|
}
|
|
|
|
// Unless the user is holding shift or meta, clear the current selection
|
|
if (!info.shiftKey) {
|
|
this.app.onShapeBlur();
|
|
|
|
if (info.altKey && this.app.selectedIds.length > 0) {
|
|
this.app.duplicate(this.app.selectedIds, currentPoint);
|
|
return;
|
|
}
|
|
|
|
this.select_none();
|
|
}
|
|
|
|
this.set_status(Status.PointingCanvas);
|
|
}
|
|
};
|
|
|
|
override onPointerUp: TLPointerEventHandler = info => {
|
|
if (
|
|
this.status === Status.TranslatingClone ||
|
|
this.status === Status.PointingClone
|
|
) {
|
|
if (this.pointedId) {
|
|
this.app.completeSession();
|
|
this.app.setEditingId(this.pointedId);
|
|
}
|
|
this.set_status(Status.Idle);
|
|
this.pointedId = undefined;
|
|
return;
|
|
}
|
|
|
|
if (this.status === Status.PointingBounds) {
|
|
if (info.target === 'bounds') {
|
|
// If we just clicked the selecting bounds's background,
|
|
// clear the selection
|
|
this.select_none();
|
|
} else if (this.app.isSelected(info.target)) {
|
|
// If we're holding shift...
|
|
if (info.shiftKey) {
|
|
// unless we just shift-selected the shape, remove it from
|
|
// the selected shapes
|
|
if (this.pointedId !== info.target) {
|
|
this.deselect(info.target);
|
|
}
|
|
} else {
|
|
// If we have other selected shapes, select this one instead
|
|
if (
|
|
this.pointedId !== info.target &&
|
|
this.app.selectedIds.length > 1
|
|
) {
|
|
this.select(info.target);
|
|
}
|
|
}
|
|
} else if (this.pointedId === info.target) {
|
|
if (this.app.getShape(info.target).isLocked) return;
|
|
// If the target is not selected and was just pointed
|
|
// on pointer down...
|
|
if (info.shiftKey) {
|
|
this.push_select(info.target);
|
|
} else {
|
|
this.select(info.target);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Complete the current session, if any; and reset the status
|
|
this.app.completeSession();
|
|
this.set_status(Status.Idle);
|
|
this.pointedBoundsHandle = undefined;
|
|
this.pointedHandleId = undefined;
|
|
this.pointedId = undefined;
|
|
};
|
|
|
|
// Canvas
|
|
|
|
override onDoubleClickCanvas: TLCanvasEventHandler = () => {
|
|
// Needs debugging
|
|
// const { currentPoint } = this.app
|
|
// this.app.selectTool(TDShapeType.Text)
|
|
// this.setStatus(Status.Idle)
|
|
// this.app.createTextShapeAtPoint(currentPoint)
|
|
};
|
|
|
|
// Shape
|
|
override onPointShape: TLPointerEventHandler = (info, e) => {
|
|
if (info.spaceKey && e.buttons === 1) return;
|
|
|
|
if (this.app.getShape(info.target).isLocked) return;
|
|
|
|
const { editingId, hoveredId } = this.app.pageState;
|
|
|
|
if (editingId && info.target !== editingId) {
|
|
this.app.onShapeBlur();
|
|
}
|
|
|
|
// While holding command and shift, select or deselect
|
|
// the shape, ignoring any group that may contain it. Yikes!
|
|
if (
|
|
(this.status === Status.Idle ||
|
|
this.status === Status.PointingBounds) &&
|
|
info.metaKey &&
|
|
info.shiftKey &&
|
|
hoveredId
|
|
) {
|
|
this.pointedId = hoveredId;
|
|
|
|
if (this.app.isSelected(hoveredId)) {
|
|
this.deselect(hoveredId);
|
|
} else {
|
|
this.push_select(hoveredId);
|
|
this.set_status(Status.PointingBounds);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (this.status === Status.PointingBounds) {
|
|
// The pointed id should be the shape's group, if it belongs
|
|
// to a group, or else the shape itself, if it is on the page.
|
|
const { parentId } = this.app.getShape(info.target);
|
|
this.pointedId =
|
|
parentId === this.app.currentPageId ? info.target : parentId;
|
|
return;
|
|
}
|
|
|
|
if (this.status === Status.Idle) {
|
|
this.set_status(Status.PointingBounds);
|
|
if (info.metaKey) {
|
|
if (!info.shiftKey) {
|
|
this.select_none();
|
|
}
|
|
|
|
this.app.startSession(SessionType.Brush);
|
|
|
|
this.set_status(Status.Brushing);
|
|
return;
|
|
}
|
|
|
|
// If we've clicked on a shape that is inside of a group,
|
|
// then select the group rather than the shape.
|
|
let shapeIdToSelect: string;
|
|
const { parentId } = this.app.getShape(info.target);
|
|
|
|
// If the pointed shape is a child of the page, select the
|
|
// target shape and clear the selected group id.
|
|
if (parentId === this.app.currentPageId) {
|
|
shapeIdToSelect = info.target;
|
|
this.selectedGroupId = undefined;
|
|
} else {
|
|
// If the parent is some other group...
|
|
if (parentId === this.selectedGroupId) {
|
|
// If that group is the selected group, then select
|
|
// the target shape.
|
|
shapeIdToSelect = info.target;
|
|
} else {
|
|
// Otherwise, select the group and clear the selected
|
|
// group id.
|
|
shapeIdToSelect = parentId;
|
|
|
|
this.selectedGroupId = undefined;
|
|
}
|
|
}
|
|
|
|
if (!this.app.isSelected(shapeIdToSelect)) {
|
|
// Set the pointed ID to the shape that was clicked.
|
|
this.pointedId = shapeIdToSelect;
|
|
|
|
// If the shape is not selected: then if the user is pressing shift,
|
|
// add the shape to the current selection; otherwise, set the shape as
|
|
// the only selected shape.
|
|
if (info.shiftKey) {
|
|
this.push_select(shapeIdToSelect);
|
|
} else {
|
|
this.select(shapeIdToSelect);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
override onDoubleClickShape: TLPointerEventHandler = info => {
|
|
const shape = this.app.getShape(info.target);
|
|
|
|
if (shape.isLocked) {
|
|
this.app.select(info.target);
|
|
return;
|
|
}
|
|
|
|
// If we can edit the shape (and if we can select the shape) then
|
|
// start editing
|
|
if (
|
|
TLDR.get_shape_util(shape.type).canEdit &&
|
|
(shape.parentId === this.app.currentPageId ||
|
|
shape.parentId === this.selectedGroupId)
|
|
) {
|
|
this.app.setEditingId(info.target);
|
|
}
|
|
|
|
// If the shape is the child of a group, then drill into the group?
|
|
if (shape.parentId !== this.app.currentPageId) {
|
|
this.selectedGroupId = shape.parentId;
|
|
}
|
|
this.app.select(info.target);
|
|
};
|
|
|
|
override onRightPointShape: TLPointerEventHandler = info => {
|
|
if (!this.app.isSelected(info.target)) {
|
|
this.app.select(info.target);
|
|
}
|
|
};
|
|
|
|
override onHoverShape: TLPointerEventHandler = info => {
|
|
this.app.setHoveredId(info.target);
|
|
};
|
|
|
|
override onUnhoverShape: TLPointerEventHandler = info => {
|
|
const { currentPageId: oldCurrentPageId } = this.app;
|
|
|
|
// Wait a frame; and if we haven't changed the hovered id,
|
|
// clear the current hovered id
|
|
requestAnimationFrame(() => {
|
|
if (
|
|
oldCurrentPageId === this.app.currentPageId &&
|
|
this.app.pageState.hoveredId === info.target
|
|
) {
|
|
this.app.setHoveredId(undefined);
|
|
}
|
|
});
|
|
};
|
|
|
|
/* --------------------- Bounds --------------------- */
|
|
|
|
override onPointBounds: TLBoundsEventHandler = info => {
|
|
if (info.metaKey) {
|
|
if (!info.shiftKey) {
|
|
this.select_none();
|
|
}
|
|
|
|
this.app.startSession(SessionType.Brush);
|
|
|
|
this.set_status(Status.Brushing);
|
|
return;
|
|
}
|
|
|
|
this.set_status(Status.PointingBounds);
|
|
};
|
|
|
|
override onRightPointBounds: TLPointerEventHandler = (info, e) => {
|
|
e.stopPropagation();
|
|
};
|
|
|
|
override onReleaseBounds: TLBoundsEventHandler = () => {
|
|
if (
|
|
this.status === Status.Translating ||
|
|
this.status === Status.Brushing
|
|
) {
|
|
this.app.completeSession();
|
|
}
|
|
|
|
this.set_status(Status.Idle);
|
|
};
|
|
|
|
/* ----------------- Bounds Handles ----------------- */
|
|
|
|
override onPointBoundsHandle: TLBoundsHandleEventHandler = info => {
|
|
this.pointedBoundsHandle = info.target;
|
|
this.set_status(Status.PointingBoundsHandle);
|
|
};
|
|
|
|
override onDoubleClickBoundsHandle: TLBoundsHandleEventHandler = info => {
|
|
switch (info.target) {
|
|
case 'center':
|
|
case 'left':
|
|
case 'right': {
|
|
this.app.select(
|
|
...TLDR.get_linked_shape_ids(
|
|
this.app.state,
|
|
this.app.currentPageId,
|
|
info.target,
|
|
info.shiftKey
|
|
)
|
|
);
|
|
break;
|
|
}
|
|
default: {
|
|
if (this.app.selectedIds.length === 1) {
|
|
this.app.resetBounds(this.app.selectedIds);
|
|
const shape = this.app.getShape(this.app.selectedIds[0]);
|
|
if ('label' in shape) {
|
|
this.app.setEditingId(shape.id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
override onReleaseBoundsHandle: TLBoundsHandleEventHandler = () => {
|
|
this.set_status(Status.Idle);
|
|
};
|
|
|
|
/* --------------------- Handles -------------------- */
|
|
|
|
override onPointHandle: TLPointerEventHandler = info => {
|
|
this.pointedHandleId = info.target as 'start' | 'end';
|
|
this.set_status(Status.PointingHandle);
|
|
};
|
|
|
|
override onDoubleClickHandle: TLPointerEventHandler = info => {
|
|
if (info.target === 'bend') {
|
|
const { selectedIds } = this.app;
|
|
if (selectedIds.length !== 1) return;
|
|
const shape = this.app.getShape(selectedIds[0]);
|
|
if (
|
|
TLDR.get_shape_util(shape.type).canEdit &&
|
|
(shape.parentId === this.app.currentPageId ||
|
|
shape.parentId === this.selectedGroupId)
|
|
) {
|
|
this.app.setEditingId(shape.id);
|
|
}
|
|
return;
|
|
}
|
|
|
|
this.app.toggleDecoration(info.target);
|
|
};
|
|
|
|
override onReleaseHandle: TLPointerEventHandler = () => {
|
|
this.set_status(Status.Idle);
|
|
};
|
|
|
|
/* ---------------------- Misc ---------------------- */
|
|
|
|
override onShapeClone: TLShapeCloneHandler = info => {
|
|
const selectedShapeId = this.app.selectedIds[0];
|
|
|
|
const clonedShape = this.getShapeClone(selectedShapeId, info.target);
|
|
|
|
if (
|
|
info.target === 'left' ||
|
|
info.target === 'right' ||
|
|
info.target === 'top' ||
|
|
info.target === 'bottom'
|
|
) {
|
|
if (clonedShape) {
|
|
this.app.createShapes(clonedShape);
|
|
|
|
// Now start pointing the bounds, so that a user can start
|
|
// dragging to reposition if they wish.
|
|
this.pointedId = clonedShape.id;
|
|
this.set_status(Status.PointingClone);
|
|
}
|
|
} else {
|
|
this.set_status(Status.GridCloning);
|
|
this.app.startSession(SessionType.Grid, selectedShapeId);
|
|
}
|
|
};
|
|
}
|