mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-21 20:16:26 +08:00
init: the first public commit for AFFiNE
This commit is contained in:
@@ -0,0 +1,581 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import {
|
||||
ArrowBinding,
|
||||
ArrowShape,
|
||||
TDShape,
|
||||
TDBinding,
|
||||
TDStatus,
|
||||
SessionType,
|
||||
TDShapeType,
|
||||
TldrawPatch,
|
||||
TldrawCommand,
|
||||
} from '@toeverything/components/board-types';
|
||||
import { Vec } from '@tldraw/vec';
|
||||
import { TLDR, deepCopy } from '@toeverything/components/board-state';
|
||||
import { shapeUtils } from '@toeverything/components/board-shapes';
|
||||
import { BaseSession } from './base-session';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
import { Utils } from '@tldraw/core';
|
||||
|
||||
export class ArrowSession extends BaseSession {
|
||||
type = SessionType.Arrow;
|
||||
performanceMode: undefined;
|
||||
status = TDStatus.TranslatingHandle;
|
||||
newStartBindingId = Utils.uniqueId();
|
||||
draggedBindingId = Utils.uniqueId();
|
||||
didBind = false;
|
||||
initialShape: ArrowShape;
|
||||
handleId: 'start' | 'end';
|
||||
bindableShapeIds: string[];
|
||||
initialBinding?: TDBinding;
|
||||
startBindingShapeId?: string;
|
||||
isCreate: boolean;
|
||||
|
||||
constructor(
|
||||
app: TldrawApp,
|
||||
shapeId: string,
|
||||
handleId: 'start' | 'end',
|
||||
isCreate = false
|
||||
) {
|
||||
super(app);
|
||||
|
||||
this.isCreate = isCreate;
|
||||
|
||||
const { currentPageId } = app.state.appState;
|
||||
|
||||
const page = app.state.document.pages[currentPageId];
|
||||
|
||||
this.handleId = handleId;
|
||||
|
||||
this.initialShape = deepCopy(page.shapes[shapeId] as ArrowShape);
|
||||
|
||||
this.bindableShapeIds = TLDR.get_bindable_shape_ids(app.state).filter(
|
||||
id =>
|
||||
!(
|
||||
id === this.initialShape.id ||
|
||||
id === this.initialShape.parentId
|
||||
)
|
||||
);
|
||||
|
||||
// TODO: find out why this the oppositeHandleBindingId is sometimes missing
|
||||
const oppositeHandleBindingId =
|
||||
this.initialShape.handles[handleId === 'start' ? 'end' : 'start']
|
||||
?.bindingId;
|
||||
|
||||
if (oppositeHandleBindingId) {
|
||||
const oppositeToId = page.bindings[oppositeHandleBindingId]?.toId;
|
||||
if (oppositeToId) {
|
||||
this.bindableShapeIds = this.bindableShapeIds.filter(
|
||||
id => id !== oppositeToId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const { originPoint } = this.app;
|
||||
|
||||
if (this.isCreate) {
|
||||
// If we're creating a new shape, should we bind its first point?
|
||||
// The method may return undefined, which is correct if there is no
|
||||
// bindable shape under the pointer.
|
||||
this.startBindingShapeId = this.bindableShapeIds
|
||||
.map(id => page.shapes[id])
|
||||
.filter(shape =>
|
||||
Utils.pointInBounds(
|
||||
originPoint,
|
||||
TLDR.get_shape_util(shape).getBounds(shape)
|
||||
)
|
||||
)
|
||||
.sort((a, b) => {
|
||||
// TODO - We should be smarter here, what's the right logic?
|
||||
return b.childIndex - a.childIndex;
|
||||
})[0]?.id;
|
||||
|
||||
if (this.startBindingShapeId) {
|
||||
this.bindableShapeIds.splice(
|
||||
this.bindableShapeIds.indexOf(this.startBindingShapeId),
|
||||
1
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// If we're editing an existing line, is there a binding already
|
||||
// for the dragging handle?
|
||||
const initialBindingId =
|
||||
this.initialShape.handles[this.handleId].bindingId;
|
||||
|
||||
if (initialBindingId) {
|
||||
this.initialBinding = page.bindings[initialBindingId];
|
||||
} else {
|
||||
// If not, explicitly set this handle to undefined, so that it gets deleted on undo
|
||||
this.initialShape.handles[this.handleId].bindingId = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start = (): TldrawPatch | undefined => void null;
|
||||
|
||||
update = (): TldrawPatch | undefined => {
|
||||
const { initialShape } = this;
|
||||
const {
|
||||
currentPoint,
|
||||
shiftKey,
|
||||
altKey,
|
||||
metaKey,
|
||||
currentGrid,
|
||||
settings: { showGrid },
|
||||
} = this.app;
|
||||
|
||||
const shape = this.app.getShape<ArrowShape>(initialShape.id);
|
||||
|
||||
if (shape.isLocked) return;
|
||||
|
||||
const { handles } = initialShape;
|
||||
|
||||
const handleId = this.handleId as keyof typeof handles;
|
||||
// If the handle can bind, then we need to search bindable shapes for
|
||||
// a binding.
|
||||
if (!handles[handleId].canBind) return;
|
||||
|
||||
// Find the delta (in shape space)
|
||||
let delta = Vec.sub(
|
||||
currentPoint,
|
||||
Vec.add(handles[handleId].point, initialShape.point)
|
||||
);
|
||||
|
||||
if (shiftKey) {
|
||||
const A = altKey
|
||||
? Vec.med(handles.start.point, handles.end.point)
|
||||
: handles[handleId === 'start' ? 'end' : 'start'].point;
|
||||
const B = handles[handleId].point;
|
||||
const C = Vec.add(B, delta);
|
||||
|
||||
const angle = Vec.angle(A, C);
|
||||
|
||||
const adjusted = Vec.rotWith(
|
||||
C,
|
||||
A,
|
||||
Utils.snapAngleToSegments(angle, 24) - angle
|
||||
);
|
||||
|
||||
delta = Vec.add(delta, Vec.sub(adjusted, C));
|
||||
}
|
||||
|
||||
const nextPoint = Vec.add(handles[handleId].point, delta);
|
||||
|
||||
const handleChanges: Partial<any> = {
|
||||
[handleId]: {
|
||||
...handles[handleId],
|
||||
point: showGrid
|
||||
? Vec.snap(nextPoint, currentGrid)
|
||||
: Vec.toFixed(nextPoint),
|
||||
bindingId: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
if (altKey) {
|
||||
// If the user is holding alt key, apply the inverse delta
|
||||
// to the oppoosite handle.
|
||||
const oppositeHandleId = handleId === 'start' ? 'end' : 'start';
|
||||
|
||||
const nextPoint = Vec.sub(handles[oppositeHandleId].point, delta);
|
||||
|
||||
handleChanges[oppositeHandleId] = {
|
||||
...handles[oppositeHandleId],
|
||||
point: showGrid
|
||||
? Vec.snap(nextPoint, currentGrid)
|
||||
: Vec.toFixed(nextPoint),
|
||||
bindingId: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const utils = shapeUtils[TDShapeType.Arrow];
|
||||
const handleChange = utils.onHandleChange?.(
|
||||
initialShape,
|
||||
handleChanges
|
||||
);
|
||||
|
||||
// If the handle changed produced no change, bail here
|
||||
if (!handleChange) return;
|
||||
|
||||
// If nothing changes, we want these to be the same object reference as
|
||||
// before. If it does change, we'll redefine this later on. And if we've
|
||||
// made it this far, the shape should be a new object reference that
|
||||
// incorporates the changes we've made due to the handle movement.
|
||||
const next: {
|
||||
shape: ArrowShape;
|
||||
bindings: Record<string, TDBinding | undefined>;
|
||||
} = {
|
||||
shape: Utils.deepMerge(shape, handleChange),
|
||||
bindings: {},
|
||||
};
|
||||
|
||||
let draggedBinding: ArrowBinding | undefined;
|
||||
|
||||
const draggingHandle = next.shape.handles[this.handleId];
|
||||
|
||||
const oppositeHandle =
|
||||
next.shape.handles[this.handleId === 'start' ? 'end' : 'start'];
|
||||
|
||||
// START BINDING
|
||||
// If we have a start binding shape id, the recompute the binding
|
||||
// point based on the current end handle position
|
||||
if (this.startBindingShapeId) {
|
||||
let nextStartBinding: ArrowBinding | undefined;
|
||||
|
||||
const startTarget = this.app.page.shapes[this.startBindingShapeId];
|
||||
|
||||
const startTargetUtils = TLDR.get_shape_util(startTarget);
|
||||
|
||||
const center = startTargetUtils.getCenter(startTarget);
|
||||
|
||||
const startHandle = next.shape.handles.start;
|
||||
|
||||
const endHandle = next.shape.handles.end;
|
||||
|
||||
const rayPoint = Vec.add(startHandle.point, next.shape.point);
|
||||
|
||||
if (Vec.isEqual(rayPoint, center)) rayPoint[1]++; // Fix bug where ray and center are identical
|
||||
|
||||
const rayOrigin = center;
|
||||
|
||||
const isInsideShape = startTargetUtils.hitTestPoint(
|
||||
startTarget,
|
||||
currentPoint
|
||||
);
|
||||
|
||||
const rayDirection = Vec.uni(Vec.sub(rayPoint, rayOrigin));
|
||||
|
||||
const hasStartBinding =
|
||||
this.app.getBinding(this.newStartBindingId) !== undefined;
|
||||
|
||||
// Don't bind the start handle if both handles are inside of the target shape.
|
||||
if (
|
||||
!metaKey &&
|
||||
!startTargetUtils.hitTestPoint(
|
||||
startTarget,
|
||||
Vec.add(next.shape.point, endHandle.point)
|
||||
)
|
||||
) {
|
||||
nextStartBinding = this.find_binding_point(
|
||||
shape,
|
||||
startTarget,
|
||||
'start',
|
||||
this.newStartBindingId,
|
||||
center,
|
||||
rayOrigin,
|
||||
rayDirection,
|
||||
isInsideShape
|
||||
);
|
||||
}
|
||||
|
||||
if (nextStartBinding && !hasStartBinding) {
|
||||
// Bind the arrow's start handle to the start target
|
||||
this.didBind = true;
|
||||
|
||||
next.bindings[this.newStartBindingId] = nextStartBinding;
|
||||
|
||||
next.shape = Utils.deepMerge(next.shape, {
|
||||
handles: {
|
||||
start: {
|
||||
bindingId: nextStartBinding.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else if (!nextStartBinding && hasStartBinding) {
|
||||
// Remove the start binding
|
||||
this.didBind = false;
|
||||
|
||||
next.bindings[this.newStartBindingId] = undefined;
|
||||
|
||||
next.shape = Utils.deepMerge(initialShape, {
|
||||
handles: {
|
||||
start: {
|
||||
bindingId: undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// DRAGGED POINT BINDING
|
||||
if (!metaKey) {
|
||||
const rayOrigin = Vec.add(oppositeHandle.point, next.shape.point);
|
||||
|
||||
const rayPoint = Vec.add(draggingHandle.point, next.shape.point);
|
||||
|
||||
const rayDirection = Vec.uni(Vec.sub(rayPoint, rayOrigin));
|
||||
|
||||
const startPoint = Vec.add(
|
||||
next.shape.point!,
|
||||
next.shape.handles!.start.point!
|
||||
);
|
||||
|
||||
const endPoint = Vec.add(
|
||||
next.shape.point!,
|
||||
next.shape.handles!.end.point!
|
||||
);
|
||||
|
||||
const targets = this.bindableShapeIds
|
||||
.map(id => this.app.page.shapes[id])
|
||||
.sort((a, b) => b.childIndex - a.childIndex)
|
||||
.filter(shape => {
|
||||
const utils = TLDR.get_shape_util(shape);
|
||||
return ![startPoint, endPoint].every(point =>
|
||||
utils.hitTestPoint(shape, point)
|
||||
);
|
||||
});
|
||||
|
||||
for (const target of targets) {
|
||||
draggedBinding = this.find_binding_point(
|
||||
shape,
|
||||
target,
|
||||
this.handleId,
|
||||
this.draggedBindingId,
|
||||
rayPoint,
|
||||
rayOrigin,
|
||||
rayDirection,
|
||||
altKey
|
||||
);
|
||||
|
||||
if (draggedBinding) break;
|
||||
}
|
||||
}
|
||||
if (draggedBinding) {
|
||||
// Create the dragged point binding
|
||||
this.didBind = true;
|
||||
|
||||
next.bindings[this.draggedBindingId] = draggedBinding;
|
||||
|
||||
next.shape = Utils.deepMerge(next.shape, {
|
||||
handles: {
|
||||
[this.handleId]: {
|
||||
bindingId: this.draggedBindingId,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Remove the dragging point binding
|
||||
this.didBind = this.didBind || false;
|
||||
|
||||
const currentBindingId = shape.handles[this.handleId].bindingId;
|
||||
|
||||
if (currentBindingId !== undefined) {
|
||||
next.bindings[currentBindingId] = undefined;
|
||||
|
||||
next.shape = Utils.deepMerge(next.shape, {
|
||||
handles: {
|
||||
[this.handleId]: {
|
||||
bindingId: undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const change = TLDR.get_shape_util<ArrowShape>(
|
||||
next.shape
|
||||
).onHandleChange?.(next.shape, next.shape.handles);
|
||||
|
||||
return {
|
||||
document: {
|
||||
pages: {
|
||||
[this.app.currentPageId]: {
|
||||
shapes: {
|
||||
[shape.id]: { ...next.shape, ...(change ?? {}) },
|
||||
},
|
||||
bindings: next.bindings,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[this.app.currentPageId]: {
|
||||
bindingId: next.shape.handles[handleId].bindingId,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
cancel = (): TldrawPatch | undefined => {
|
||||
const {
|
||||
initialShape,
|
||||
initialBinding,
|
||||
newStartBindingId,
|
||||
draggedBindingId,
|
||||
} = this;
|
||||
|
||||
const currentShape = TLDR.on_session_complete(
|
||||
this.app.page.shapes[initialShape.id]
|
||||
) as ArrowShape;
|
||||
|
||||
const isDeleting =
|
||||
this.isCreate ||
|
||||
Vec.dist(
|
||||
currentShape.handles.start.point,
|
||||
currentShape.handles.end.point
|
||||
) < 4;
|
||||
|
||||
const afterBindings: Record<string, TDBinding | undefined> = {};
|
||||
|
||||
afterBindings[draggedBindingId] = undefined;
|
||||
|
||||
if (initialBinding) {
|
||||
afterBindings[initialBinding.id] = isDeleting
|
||||
? undefined
|
||||
: initialBinding;
|
||||
}
|
||||
|
||||
if (newStartBindingId) {
|
||||
afterBindings[newStartBindingId] = undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
document: {
|
||||
pages: {
|
||||
[this.app.currentPageId]: {
|
||||
shapes: {
|
||||
[initialShape.id]: isDeleting
|
||||
? undefined
|
||||
: initialShape,
|
||||
},
|
||||
bindings: afterBindings,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[this.app.currentPageId]: {
|
||||
selectedIds: isDeleting ? [] : [initialShape.id],
|
||||
bindingId: undefined,
|
||||
hoveredId: undefined,
|
||||
editingId: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
complete = (): TldrawPatch | TldrawCommand | undefined => {
|
||||
const {
|
||||
initialShape,
|
||||
initialBinding,
|
||||
newStartBindingId,
|
||||
startBindingShapeId,
|
||||
handleId,
|
||||
} = this;
|
||||
|
||||
const currentShape = TLDR.on_session_complete(
|
||||
this.app.page.shapes[initialShape.id]
|
||||
) as ArrowShape;
|
||||
|
||||
const currentBindingId = currentShape.handles[handleId].bindingId;
|
||||
|
||||
const length = Vec.dist(
|
||||
currentShape.handles.start.point,
|
||||
currentShape.handles.end.point
|
||||
);
|
||||
|
||||
if (!(currentBindingId || initialBinding) && length < 4)
|
||||
return this.cancel();
|
||||
|
||||
const beforeBindings: Partial<Record<string, TDBinding>> = {};
|
||||
|
||||
const afterBindings: Partial<Record<string, TDBinding>> = {};
|
||||
|
||||
if (initialBinding) {
|
||||
beforeBindings[initialBinding.id] = this.isCreate
|
||||
? undefined
|
||||
: initialBinding;
|
||||
afterBindings[initialBinding.id] = undefined;
|
||||
}
|
||||
|
||||
if (currentBindingId) {
|
||||
beforeBindings[currentBindingId] = undefined;
|
||||
afterBindings[currentBindingId] =
|
||||
this.app.page.bindings[currentBindingId];
|
||||
}
|
||||
|
||||
if (startBindingShapeId) {
|
||||
beforeBindings[newStartBindingId] = undefined;
|
||||
afterBindings[newStartBindingId] =
|
||||
this.app.page.bindings[newStartBindingId];
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'arrow',
|
||||
before: {
|
||||
document: {
|
||||
pages: {
|
||||
[this.app.currentPageId]: {
|
||||
shapes: {
|
||||
[initialShape.id]: this.isCreate
|
||||
? undefined
|
||||
: initialShape,
|
||||
},
|
||||
bindings: beforeBindings,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[this.app.currentPageId]: {
|
||||
selectedIds: this.isCreate ? [] : [initialShape.id],
|
||||
bindingId: undefined,
|
||||
hoveredId: undefined,
|
||||
editingId: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
document: {
|
||||
pages: {
|
||||
[this.app.currentPageId]: {
|
||||
shapes: {
|
||||
[initialShape.id]: currentShape,
|
||||
},
|
||||
bindings: afterBindings,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[this.app.currentPageId]: {
|
||||
selectedIds: [initialShape.id],
|
||||
bindingId: undefined,
|
||||
hoveredId: undefined,
|
||||
editingId: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
private find_binding_point = (
|
||||
shape: ArrowShape,
|
||||
target: TDShape,
|
||||
handleId: 'start' | 'end',
|
||||
bindingId: string,
|
||||
point: number[],
|
||||
origin: number[],
|
||||
direction: number[],
|
||||
bindAnywhere: boolean
|
||||
) => {
|
||||
const util = TLDR.get_shape_util<TDShape>(target.type);
|
||||
|
||||
const bindingPoint = util.getBindingPoint(
|
||||
target,
|
||||
shape,
|
||||
point, // fix dead center bug
|
||||
origin,
|
||||
direction,
|
||||
bindAnywhere
|
||||
);
|
||||
|
||||
// Not all shapes will produce a binding point
|
||||
if (!bindingPoint) return;
|
||||
|
||||
return {
|
||||
id: bindingId,
|
||||
type: 'arrow',
|
||||
fromId: shape.id,
|
||||
toId: target.id,
|
||||
handleId: handleId,
|
||||
point: Vec.toFixed(bindingPoint.point),
|
||||
distance: bindingPoint.distance,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { BaseSessionType } from '@toeverything/components/board-types';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
|
||||
export abstract class BaseSession extends BaseSessionType {
|
||||
app: TldrawApp;
|
||||
constructor(app: TldrawApp) {
|
||||
super();
|
||||
this.app = app;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import { TLBounds, Utils } from '@tldraw/core';
|
||||
import {
|
||||
SessionType,
|
||||
TldrawPatch,
|
||||
TDStatus,
|
||||
TldrawCommand,
|
||||
} from '@toeverything/components/board-types';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
import { BaseSession } from './base-session';
|
||||
|
||||
export class BrushSession extends BaseSession {
|
||||
type = SessionType.Brush;
|
||||
performanceMode: undefined;
|
||||
status = TDStatus.Brushing;
|
||||
initialSelectedIds: Set<string>;
|
||||
shapesToTest: {
|
||||
id: string;
|
||||
bounds: TLBounds;
|
||||
selectId: string;
|
||||
}[];
|
||||
|
||||
constructor(app: TldrawApp) {
|
||||
super(app);
|
||||
const { currentPageId } = app;
|
||||
this.initialSelectedIds = new Set(this.app.selectedIds);
|
||||
this.shapesToTest = this.app.shapes
|
||||
.filter(
|
||||
shape =>
|
||||
!(
|
||||
shape.isLocked ||
|
||||
shape.isHidden ||
|
||||
shape.parentId !== currentPageId ||
|
||||
this.initialSelectedIds.has(shape.id) ||
|
||||
this.initialSelectedIds.has(shape.parentId)
|
||||
)
|
||||
)
|
||||
.map(shape => ({
|
||||
id: shape.id,
|
||||
bounds: this.app.getShapeUtil(shape).getBounds(shape),
|
||||
selectId: shape.id, //TLDR.getTopParentId(data, shape.id, currentPageId),
|
||||
}));
|
||||
|
||||
this.update();
|
||||
}
|
||||
|
||||
start = (): TldrawPatch | undefined => void null;
|
||||
|
||||
update = (): TldrawPatch | undefined => {
|
||||
const {
|
||||
initialSelectedIds,
|
||||
shapesToTest,
|
||||
app: { metaKey, settings, originPoint, currentPoint },
|
||||
} = this;
|
||||
|
||||
// Create a bounding box between the origin and the new point
|
||||
const brush = Utils.getBoundsFromPoints([originPoint, currentPoint]);
|
||||
|
||||
// Decide weather to select by intersecting or by overlapping
|
||||
// Using a xor to revers the behaviour if the ctrl key is pressed
|
||||
// Do it only if the user choose to enable cad like selection
|
||||
const selectByContain = settings.isCadSelectMode
|
||||
? !metaKey && originPoint[0] < currentPoint[0]
|
||||
: metaKey;
|
||||
|
||||
// Find ids of brushed shapes
|
||||
const hits = new Set<string>();
|
||||
|
||||
const selectedIds = new Set(initialSelectedIds);
|
||||
|
||||
shapesToTest.forEach(({ id, selectId }) => {
|
||||
const shape = this.app.getShape(id);
|
||||
if (!hits.has(selectId)) {
|
||||
const util = this.app.getShapeUtil(shape);
|
||||
if (
|
||||
selectByContain
|
||||
? Utils.boundsContain(brush, util.getBounds(shape))
|
||||
: util.hitTestBounds(shape, brush)
|
||||
) {
|
||||
hits.add(selectId);
|
||||
|
||||
// When brushing a shape, select its top group parent.
|
||||
if (!selectedIds.has(selectId)) {
|
||||
selectedIds.add(selectId);
|
||||
}
|
||||
} else if (selectedIds.has(selectId)) {
|
||||
selectedIds.delete(selectId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const currentSelectedIds = this.app.selectedIds;
|
||||
|
||||
const didChange =
|
||||
selectedIds.size !== currentSelectedIds.length ||
|
||||
currentSelectedIds.some(id => !selectedIds.has(id));
|
||||
|
||||
const afterSelectedIds = didChange
|
||||
? Array.from(selectedIds.values())
|
||||
: currentSelectedIds;
|
||||
|
||||
return {
|
||||
appState: {
|
||||
selectByContain,
|
||||
},
|
||||
document: {
|
||||
pageStates: {
|
||||
[this.app.currentPageId]: {
|
||||
brush,
|
||||
selectedIds: afterSelectedIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
cancel = (): TldrawPatch | undefined => {
|
||||
return {
|
||||
appState: {
|
||||
selectByContain: false,
|
||||
},
|
||||
document: {
|
||||
pageStates: {
|
||||
[this.app.currentPageId]: {
|
||||
brush: null,
|
||||
selectedIds: Array.from(
|
||||
this.initialSelectedIds.values()
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
complete = (): TldrawPatch | TldrawCommand | undefined => {
|
||||
return {
|
||||
appState: {
|
||||
selectByContain: false,
|
||||
},
|
||||
document: {
|
||||
pageStates: {
|
||||
[this.app.currentPageId]: {
|
||||
brush: null,
|
||||
selectedIds: [...this.app.selectedIds],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
import { Utils } from '@tldraw/core';
|
||||
import { Vec } from '@tldraw/vec';
|
||||
import {
|
||||
SessionType,
|
||||
TDStatus,
|
||||
TldrawPatch,
|
||||
TldrawCommand,
|
||||
DrawShape,
|
||||
} from '@toeverything/components/board-types';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
import { BaseSession } from './base-session';
|
||||
|
||||
export class DrawSession extends BaseSession {
|
||||
type = SessionType.Draw;
|
||||
performanceMode: undefined;
|
||||
status = TDStatus.Creating;
|
||||
topLeft: number[];
|
||||
points: number[][];
|
||||
initialShape: DrawShape;
|
||||
lastAdjustedPoint: number[];
|
||||
shiftedPoints: number[][] = [];
|
||||
shapeId: string;
|
||||
isLocked?: boolean;
|
||||
isExtending: boolean;
|
||||
lockedDirection?: 'horizontal' | 'vertical';
|
||||
|
||||
constructor(app: TldrawApp, id: string) {
|
||||
super(app);
|
||||
const { originPoint } = this.app;
|
||||
this.shapeId = id;
|
||||
this.initialShape = this.app.getShape<DrawShape>(id);
|
||||
this.topLeft = [...this.initialShape.point];
|
||||
const currentPoint = [0, 0, originPoint[2] ?? 0.5];
|
||||
const delta = Vec.sub(originPoint, this.topLeft);
|
||||
const initialPoints = this.initialShape.points.map(pt =>
|
||||
Vec.sub(pt, delta).concat(pt[2])
|
||||
);
|
||||
this.isExtending = initialPoints.length > 0;
|
||||
const newPoints: number[][] = [];
|
||||
if (this.isExtending) {
|
||||
const prevPoint = initialPoints[initialPoints.length - 1];
|
||||
newPoints.push(prevPoint, prevPoint);
|
||||
// Continuing with shift
|
||||
const len = Math.ceil(Vec.dist(prevPoint, currentPoint) / 16);
|
||||
for (let i = 0; i < len; i++) {
|
||||
const t = i / (len - 1);
|
||||
newPoints.push(
|
||||
Vec.lrp(prevPoint, currentPoint, t).concat(prevPoint[2])
|
||||
);
|
||||
}
|
||||
} else {
|
||||
newPoints.push(currentPoint);
|
||||
}
|
||||
// Add a first point but don't update the shape yet. We'll update
|
||||
// when the draw session ends; if the user hasn't added additional
|
||||
// points, this single point will be interpreted as a "dot" shape.
|
||||
this.points = [...initialPoints, ...newPoints];
|
||||
this.shiftedPoints = this.points.map(pt =>
|
||||
Vec.add(pt, delta).concat(pt[2])
|
||||
);
|
||||
this.lastAdjustedPoint = this.points[this.points.length - 1];
|
||||
}
|
||||
|
||||
start = () => {
|
||||
const currentPoint = this.app.originPoint;
|
||||
const newAdjustedPoint = [0, 0, currentPoint[2] ?? 0.5];
|
||||
// Add the new adjusted point to the points array
|
||||
this.points.push(newAdjustedPoint);
|
||||
const topLeft = [
|
||||
Math.min(this.topLeft[0], currentPoint[0]),
|
||||
Math.min(this.topLeft[1], currentPoint[1]),
|
||||
];
|
||||
const delta = Vec.sub(topLeft, currentPoint);
|
||||
this.topLeft = topLeft;
|
||||
this.shiftedPoints = this.points.map(pt =>
|
||||
Vec.toFixed(Vec.sub(pt, delta)).concat(pt[2])
|
||||
);
|
||||
|
||||
return {
|
||||
document: {
|
||||
pages: {
|
||||
[this.app.currentPageId]: {
|
||||
shapes: {
|
||||
[this.shapeId]: {
|
||||
point: this.topLeft,
|
||||
points: this.shiftedPoints,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[this.app.currentPageId]: {
|
||||
selectedIds: [this.shapeId],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
update = (): TldrawPatch | undefined => {
|
||||
const { shapeId } = this;
|
||||
const { currentPoint, originPoint, shiftKey } = this.app;
|
||||
|
||||
// Even if we're not locked yet, we base the future locking direction
|
||||
// on the first dimension to reach a threshold, or the bigger dimension
|
||||
// once one or both dimensions have reached the threshold.
|
||||
if (!this.lockedDirection && this.points.length > 1) {
|
||||
const bounds = Utils.getBoundsFromPoints(this.points);
|
||||
if (bounds.width > 8 || bounds.height > 8) {
|
||||
this.lockedDirection =
|
||||
bounds.width > bounds.height ? 'horizontal' : 'vertical';
|
||||
}
|
||||
}
|
||||
|
||||
// Drawing while holding shift will "lock" the pen to either the
|
||||
// x or y axis, depending on the locking direction.
|
||||
if (shiftKey) {
|
||||
if (!this.isLocked && this.points.length > 2) {
|
||||
// If we're locking before knowing what direction we're in, set it
|
||||
// early based on the bigger dimension.
|
||||
if (!this.lockedDirection) {
|
||||
const bounds = Utils.getBoundsFromPoints(this.points);
|
||||
this.lockedDirection =
|
||||
bounds.width > bounds.height
|
||||
? 'horizontal'
|
||||
: 'vertical';
|
||||
}
|
||||
|
||||
this.isLocked = true;
|
||||
// Start locking
|
||||
const returning = [...this.lastAdjustedPoint];
|
||||
|
||||
if (this.lockedDirection === 'vertical') {
|
||||
returning[0] = 0;
|
||||
} else {
|
||||
returning[1] = 0;
|
||||
}
|
||||
|
||||
this.points.push(returning.concat(currentPoint[2]));
|
||||
}
|
||||
} else if (this.isLocked) {
|
||||
this.isLocked = false;
|
||||
}
|
||||
|
||||
if (this.isLocked) {
|
||||
if (this.lockedDirection === 'vertical') {
|
||||
currentPoint[0] = originPoint[0];
|
||||
} else {
|
||||
currentPoint[1] = originPoint[1];
|
||||
}
|
||||
}
|
||||
|
||||
const change = this.addPoint(currentPoint);
|
||||
|
||||
if (!change) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
document: {
|
||||
pages: {
|
||||
[this.app.currentPageId]: {
|
||||
shapes: {
|
||||
[shapeId]: change,
|
||||
},
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[this.app.currentPageId]: {
|
||||
selectedIds: [shapeId],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
cancel = (): TldrawPatch | undefined => {
|
||||
const { shapeId } = this;
|
||||
const pageId = this.app.currentPageId;
|
||||
|
||||
return {
|
||||
document: {
|
||||
pages: {
|
||||
[pageId]: {
|
||||
shapes: {
|
||||
[shapeId]: this.isExtending
|
||||
? this.initialShape
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[pageId]: {
|
||||
selectedIds: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
complete = (): TldrawPatch | TldrawCommand | undefined => {
|
||||
const { shapeId } = this;
|
||||
const pageId = this.app.currentPageId;
|
||||
const shape = this.app.getShape<DrawShape>(shapeId);
|
||||
return {
|
||||
id: 'create_draw',
|
||||
before: {
|
||||
document: {
|
||||
pages: {
|
||||
[pageId]: {
|
||||
shapes: {
|
||||
[shapeId]: this.isExtending
|
||||
? this.initialShape
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[pageId]: {
|
||||
selectedIds: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
document: {
|
||||
pages: {
|
||||
[pageId]: {
|
||||
shapes: {
|
||||
[shapeId]: {
|
||||
...shape,
|
||||
point: Vec.toFixed(shape.point),
|
||||
points: shape.points.map(pt =>
|
||||
Vec.toFixed(pt)
|
||||
),
|
||||
isComplete: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[this.app.currentPageId]: {
|
||||
selectedIds: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
addPoint = (currentPoint: number[]) => {
|
||||
const { originPoint } = this.app;
|
||||
// The new adjusted point
|
||||
const newAdjustedPoint = Vec.toFixed(
|
||||
Vec.sub(currentPoint, originPoint)
|
||||
).concat(currentPoint[2]);
|
||||
|
||||
// Don't add duplicate points.
|
||||
if (Vec.isEqual(this.lastAdjustedPoint, newAdjustedPoint)) return;
|
||||
|
||||
// Add the new adjusted point to the points array
|
||||
this.points.push(newAdjustedPoint);
|
||||
|
||||
// The new adjusted point is now the previous adjusted point.
|
||||
this.lastAdjustedPoint = newAdjustedPoint;
|
||||
|
||||
// Does the input point create a new top left?
|
||||
const prevTopLeft = [...this.topLeft];
|
||||
|
||||
const topLeft = [
|
||||
Math.min(this.topLeft[0], currentPoint[0]),
|
||||
Math.min(this.topLeft[1], currentPoint[1]),
|
||||
];
|
||||
|
||||
const delta = Vec.sub(topLeft, originPoint);
|
||||
|
||||
// Time to shift some points!
|
||||
let points: number[][];
|
||||
|
||||
if (prevTopLeft[0] !== topLeft[0] || prevTopLeft[1] !== topLeft[1]) {
|
||||
this.topLeft = topLeft;
|
||||
// If we have a new top left, then we need to iterate through
|
||||
// the "unshifted" points array and shift them based on the
|
||||
// offset between the new top left and the original top left.
|
||||
points = this.points.map(pt =>
|
||||
Vec.toFixed(Vec.sub(pt, delta)).concat(pt[2])
|
||||
);
|
||||
} else {
|
||||
// If the new top left is the same as the previous top left,
|
||||
// we don't need to shift anything: we just shift the new point
|
||||
// and add it to the shifted points array.
|
||||
points = [
|
||||
...this.shiftedPoints,
|
||||
Vec.sub(newAdjustedPoint, delta).concat(newAdjustedPoint[2]),
|
||||
];
|
||||
}
|
||||
|
||||
this.shiftedPoints = points;
|
||||
|
||||
return {
|
||||
point: this.topLeft,
|
||||
points,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
import { Vec } from '@tldraw/vec';
|
||||
import {
|
||||
SessionType,
|
||||
TDStatus,
|
||||
TDShape,
|
||||
PagePartial,
|
||||
TDBinding,
|
||||
TldrawPatch,
|
||||
TldrawCommand,
|
||||
} from '@toeverything/components/board-types';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
import { BaseSession } from './base-session';
|
||||
|
||||
export class EraseSession extends BaseSession {
|
||||
type = SessionType.Draw;
|
||||
performanceMode: undefined;
|
||||
status = TDStatus.Creating;
|
||||
isLocked?: boolean;
|
||||
lockedDirection?: 'horizontal' | 'vertical';
|
||||
erasedShapes = new Set<TDShape>();
|
||||
erasedBindings = new Set<TDBinding>();
|
||||
initialSelectedShapes: TDShape[];
|
||||
erasableShapes: Set<TDShape>;
|
||||
prevPoint: number[];
|
||||
|
||||
constructor(app: TldrawApp) {
|
||||
super(app);
|
||||
this.prevPoint = [...app.originPoint];
|
||||
this.initialSelectedShapes = this.app.selectedIds.map(id =>
|
||||
this.app.getShape(id)
|
||||
);
|
||||
this.erasableShapes = new Set(
|
||||
this.app.shapes.filter(shape => !shape.isLocked)
|
||||
);
|
||||
}
|
||||
|
||||
start = (): TldrawPatch | undefined => void null;
|
||||
|
||||
update = (): TldrawPatch | undefined => {
|
||||
const { page, shiftKey, originPoint, currentPoint } = this.app;
|
||||
|
||||
if (shiftKey) {
|
||||
if (!this.isLocked && Vec.dist(originPoint, currentPoint) > 4) {
|
||||
// If we're locking before knowing what direction we're in, set it
|
||||
// early based on the bigger dimension.
|
||||
if (!this.lockedDirection) {
|
||||
const delta = Vec.sub(currentPoint, originPoint);
|
||||
this.lockedDirection =
|
||||
delta[0] > delta[1] ? 'horizontal' : 'vertical';
|
||||
}
|
||||
|
||||
this.isLocked = true;
|
||||
}
|
||||
} else if (this.isLocked) {
|
||||
this.isLocked = false;
|
||||
}
|
||||
|
||||
if (this.isLocked) {
|
||||
if (this.lockedDirection === 'vertical') {
|
||||
currentPoint[0] = originPoint[0];
|
||||
} else {
|
||||
currentPoint[1] = originPoint[1];
|
||||
}
|
||||
}
|
||||
|
||||
const newPoint = Vec.toFixed(
|
||||
Vec.add(originPoint, Vec.sub(currentPoint, originPoint))
|
||||
);
|
||||
|
||||
const deletedShapeIds = new Set<string>([]);
|
||||
|
||||
this.erasableShapes.forEach(shape => {
|
||||
if (this.erasedShapes.has(shape)) return;
|
||||
if (
|
||||
this.app
|
||||
.getShapeUtil(shape)
|
||||
.hitTestLineSegment(shape, this.prevPoint, newPoint)
|
||||
) {
|
||||
this.erasedShapes.add(shape);
|
||||
deletedShapeIds.add(shape.id);
|
||||
|
||||
if (shape.children !== undefined) {
|
||||
for (const childId of shape.children) {
|
||||
this.erasedShapes.add(this.app.getShape(childId));
|
||||
deletedShapeIds.add(childId);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Erase bindings that reference deleted shapes
|
||||
|
||||
Object.values(page.bindings).forEach(binding => {
|
||||
for (const id of [binding.toId, binding.fromId]) {
|
||||
if (deletedShapeIds.has(id)) {
|
||||
this.erasedBindings.add(binding);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.erasedShapes.forEach(shape => {
|
||||
// Has the shape been deleted? If so, pull it from the list.
|
||||
if (!this.app.getShape(shape.id)) {
|
||||
this.erasedShapes.delete(shape);
|
||||
this.erasableShapes.delete(shape);
|
||||
deletedShapeIds.delete(shape.id);
|
||||
}
|
||||
});
|
||||
|
||||
const erasedShapes = Array.from(this.erasedShapes.values());
|
||||
|
||||
this.prevPoint = newPoint;
|
||||
|
||||
return {
|
||||
document: {
|
||||
pages: {
|
||||
[page.id]: {
|
||||
shapes: Object.fromEntries(
|
||||
erasedShapes.map(shape => [
|
||||
shape.id,
|
||||
{ isGhost: true },
|
||||
])
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
cancel = (): TldrawPatch | undefined => {
|
||||
const { page } = this.app;
|
||||
|
||||
this.erasedShapes.forEach(shape => {
|
||||
if (!this.app.getShape(shape.id)) {
|
||||
this.erasedShapes.delete(shape);
|
||||
this.erasableShapes.delete(shape);
|
||||
}
|
||||
});
|
||||
|
||||
const erasedShapes = Array.from(this.erasedShapes.values());
|
||||
|
||||
return {
|
||||
document: {
|
||||
pages: {
|
||||
[page.id]: {
|
||||
shapes: Object.fromEntries(
|
||||
erasedShapes.map(shape => [
|
||||
shape.id,
|
||||
{ isGhost: false },
|
||||
])
|
||||
),
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[page.id]: {
|
||||
selectedIds: this.initialSelectedShapes.map(
|
||||
shape => shape.id
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
complete = (): TldrawPatch | TldrawCommand | undefined => {
|
||||
const { page } = this.app;
|
||||
|
||||
this.erasedShapes.forEach(shape => {
|
||||
if (!this.app.getShape(shape.id)) {
|
||||
this.erasedShapes.delete(shape);
|
||||
this.erasableShapes.delete(shape);
|
||||
}
|
||||
});
|
||||
|
||||
this.erasedBindings.forEach(binding => {
|
||||
if (!this.app.getBinding(binding.id)) {
|
||||
this.erasedBindings.delete(binding);
|
||||
}
|
||||
});
|
||||
|
||||
const erasedShapes = Array.from(this.erasedShapes.values());
|
||||
const erasedBindings = Array.from(this.erasedBindings.values());
|
||||
const erasedShapeIds = erasedShapes.map(shape => shape.id);
|
||||
const erasedBindingIds = erasedBindings.map(binding => binding.id);
|
||||
|
||||
const before: PagePartial = {
|
||||
shapes: Object.fromEntries(
|
||||
erasedShapes.map(shape => [shape.id, shape])
|
||||
),
|
||||
bindings: Object.fromEntries(
|
||||
erasedBindings.map(binding => [binding.id, binding])
|
||||
),
|
||||
};
|
||||
|
||||
const after: PagePartial = {
|
||||
shapes: Object.fromEntries(
|
||||
erasedShapes.map(shape => [shape.id, undefined])
|
||||
),
|
||||
bindings: Object.fromEntries(
|
||||
erasedBindings.map(binding => [binding.id, undefined])
|
||||
),
|
||||
};
|
||||
|
||||
// Remove references on any shape's handles to any deleted bindings
|
||||
this.app.shapes.forEach(shape => {
|
||||
if (shape.handles && !after.shapes[shape.id]) {
|
||||
Object.values(shape.handles).forEach(handle => {
|
||||
if (
|
||||
handle.bindingId &&
|
||||
erasedBindingIds.includes(handle.bindingId)
|
||||
) {
|
||||
// Save the binding reference in the before patch
|
||||
before.shapes[shape.id] = {
|
||||
...before.shapes[shape.id],
|
||||
handles: {
|
||||
...before.shapes[shape.id]?.handles,
|
||||
[handle.id]: handle,
|
||||
},
|
||||
};
|
||||
|
||||
// Save the binding reference in the before patch
|
||||
if (!erasedShapeIds.includes(shape.id)) {
|
||||
after.shapes[shape.id] = {
|
||||
...after.shapes[shape.id],
|
||||
handles: {
|
||||
...after.shapes[shape.id]?.handles,
|
||||
[handle.id]: {
|
||||
...handle,
|
||||
bindingId: undefined,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
id: 'erase',
|
||||
before: {
|
||||
document: {
|
||||
pages: {
|
||||
[page.id]: before,
|
||||
},
|
||||
pageStates: {
|
||||
[page.id]: {
|
||||
selectedIds: this.initialSelectedShapes
|
||||
.filter(shape => !!this.app.getShape(shape.id))
|
||||
.map(shape => shape.id),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
document: {
|
||||
pages: {
|
||||
[page.id]: after,
|
||||
},
|
||||
pageStates: {
|
||||
[page.id]: {
|
||||
selectedIds: this.initialSelectedShapes
|
||||
.filter(shape => !!this.app.getShape(shape.id))
|
||||
.filter(
|
||||
shape => !erasedShapeIds.includes(shape.id)
|
||||
)
|
||||
.map(shape => shape.id),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { TLPageState, TLBounds, Utils } from '@tldraw/core';
|
||||
import { Vec } from '@tldraw/vec';
|
||||
import {
|
||||
Patch,
|
||||
TDShape,
|
||||
TDStatus,
|
||||
SessionType,
|
||||
TDShapeType,
|
||||
TldrawPatch,
|
||||
TldrawCommand,
|
||||
} from '@toeverything/components/board-types';
|
||||
import { BaseSession } from './base-session';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
|
||||
export class GridSession extends BaseSession {
|
||||
type = SessionType.Grid;
|
||||
performanceMode: undefined;
|
||||
status = TDStatus.Translating;
|
||||
shape: TDShape;
|
||||
bounds: TLBounds;
|
||||
initialSelectedIds: string[];
|
||||
initialSiblings?: string[];
|
||||
grid: Record<string, string> = {};
|
||||
columns = 1;
|
||||
rows = 1;
|
||||
isCopying = false;
|
||||
|
||||
constructor(app: TldrawApp, id: string) {
|
||||
super(app);
|
||||
this.shape = this.app.getShape(id);
|
||||
this.grid['0_0'] = this.shape.id;
|
||||
this.bounds = this.app.getShapeBounds(id);
|
||||
this.initialSelectedIds = [...this.app.selectedIds];
|
||||
if (this.shape.parentId !== this.app.currentPageId) {
|
||||
this.initialSiblings = this.app
|
||||
.getShape(this.shape.parentId)
|
||||
.children?.filter(id => id !== this.shape.id);
|
||||
}
|
||||
}
|
||||
|
||||
start = (): TldrawPatch | undefined => void null;
|
||||
|
||||
update = (): TldrawPatch | undefined => {
|
||||
const { currentPageId, altKey, shiftKey, currentPoint } = this.app;
|
||||
|
||||
const nextShapes: Patch<Record<string, TDShape>> = {};
|
||||
|
||||
const nextPageState: Patch<TLPageState> = {};
|
||||
|
||||
const center = Utils.getBoundsCenter(this.bounds);
|
||||
|
||||
const offset = Vec.sub(currentPoint, center);
|
||||
|
||||
if (shiftKey) {
|
||||
if (Math.abs(offset[0]) < Math.abs(offset[1])) {
|
||||
offset[0] = 0;
|
||||
} else {
|
||||
offset[1] = 0;
|
||||
}
|
||||
}
|
||||
// use the distance from center to determine the grid
|
||||
|
||||
const gapX = this.bounds.width + 32;
|
||||
const gapY = this.bounds.height + 32;
|
||||
|
||||
const columns = Math.ceil(offset[0] / gapX);
|
||||
const rows = Math.ceil(offset[1] / gapY);
|
||||
|
||||
const minX = Math.min(columns, 0);
|
||||
const minY = Math.min(rows, 0);
|
||||
const maxX = Math.max(columns, 1);
|
||||
const maxY = Math.max(rows, 1);
|
||||
|
||||
const inGrid = new Set<string>();
|
||||
|
||||
const isCopying = altKey;
|
||||
|
||||
if (isCopying !== this.isCopying) {
|
||||
// Recreate shapes copying
|
||||
Object.values(this.grid)
|
||||
.filter(id => id !== this.shape.id)
|
||||
.forEach(id => (nextShapes[id] = undefined));
|
||||
|
||||
this.grid = { '0_0': this.shape.id };
|
||||
|
||||
this.isCopying = isCopying;
|
||||
}
|
||||
|
||||
// Go through grid, adding items in positions
|
||||
// that aren't already filled.
|
||||
for (let x = minX; x < maxX; x++) {
|
||||
for (let y = minY; y < maxY; y++) {
|
||||
const position = `${x}_${y}`;
|
||||
|
||||
inGrid.add(position);
|
||||
|
||||
if (this.grid[position]) continue;
|
||||
|
||||
if (x === 0 && y === 0) continue;
|
||||
|
||||
const clone = this.get_clone(
|
||||
Vec.add(this.shape.point, [x * gapX, y * gapY]),
|
||||
isCopying
|
||||
);
|
||||
|
||||
nextShapes[clone.id] = clone;
|
||||
|
||||
this.grid[position] = clone.id;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any other items from the grid
|
||||
Object.entries(this.grid).forEach(([position, id]) => {
|
||||
if (!inGrid.has(position)) {
|
||||
nextShapes[id] = undefined;
|
||||
delete this.grid[position];
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.values(nextShapes).length === 0) return;
|
||||
|
||||
// Add shapes to parent id
|
||||
if (this.initialSiblings) {
|
||||
nextShapes[this.shape.parentId] = {
|
||||
children: [
|
||||
...this.initialSiblings,
|
||||
...Object.values(this.grid),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: nextShapes,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: nextPageState,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
cancel = (): TldrawPatch | undefined => {
|
||||
const { currentPageId } = this.app;
|
||||
const nextShapes: Record<string, Partial<TDShape> | undefined> = {};
|
||||
|
||||
// Delete clones
|
||||
Object.values(this.grid).forEach(id => {
|
||||
nextShapes[id] = undefined;
|
||||
// TODO: Remove from parent if grouped
|
||||
});
|
||||
|
||||
// Put back the initial shape
|
||||
nextShapes[this.shape.id] = {
|
||||
...nextShapes[this.shape.id],
|
||||
point: this.shape.point,
|
||||
};
|
||||
|
||||
if (this.initialSiblings) {
|
||||
nextShapes[this.shape.parentId] = {
|
||||
children: [...this.initialSiblings, this.shape.id],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: nextShapes,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: [this.shape.id],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
complete = (): TldrawPatch | TldrawCommand | undefined => {
|
||||
const { currentPageId } = this.app;
|
||||
|
||||
const beforeShapes: Patch<Record<string, TDShape>> = {};
|
||||
|
||||
const afterShapes: Patch<Record<string, TDShape>> = {};
|
||||
|
||||
const afterSelectedIds: string[] = [];
|
||||
|
||||
Object.values(this.grid).forEach(id => {
|
||||
beforeShapes[id] = undefined;
|
||||
afterShapes[id] = this.app.getShape(id);
|
||||
afterSelectedIds.push(id);
|
||||
// TODO: Add shape to parent if grouped
|
||||
});
|
||||
|
||||
beforeShapes[this.shape.id] = this.shape;
|
||||
|
||||
// Add shapes to parent id
|
||||
if (this.initialSiblings) {
|
||||
beforeShapes[this.shape.parentId] = {
|
||||
children: [...this.initialSiblings, this.shape.id],
|
||||
};
|
||||
|
||||
afterShapes[this.shape.parentId] = {
|
||||
children: [
|
||||
...this.initialSiblings,
|
||||
...Object.values(this.grid),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// If no new shapes have been created, bail
|
||||
if (afterSelectedIds.length === 1) return;
|
||||
|
||||
return {
|
||||
id: 'grid',
|
||||
before: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: beforeShapes,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: [],
|
||||
hoveredId: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: afterShapes,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: afterSelectedIds,
|
||||
hoveredId: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
private get_clone = (point: number[], copy: boolean) => {
|
||||
const clone = {
|
||||
...this.shape,
|
||||
id: Utils.uniqueId(),
|
||||
point,
|
||||
};
|
||||
|
||||
// if (!copy) {
|
||||
// if (clone.type === TDShapeType.Sticky) {
|
||||
// clone.text = '';
|
||||
// }
|
||||
// }
|
||||
|
||||
return clone;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import { Vec } from '@tldraw/vec';
|
||||
import {
|
||||
SessionType,
|
||||
ShapesWithProp,
|
||||
TldrawCommand,
|
||||
TldrawPatch,
|
||||
TDStatus,
|
||||
} from '@toeverything/components/board-types';
|
||||
import { TLDR } from '@toeverything/components/board-state';
|
||||
import { BaseSession } from './base-session';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
|
||||
export class HandleSession extends BaseSession {
|
||||
type = SessionType.Handle;
|
||||
performanceMode: undefined;
|
||||
status = TDStatus.TranslatingHandle;
|
||||
commandId: string;
|
||||
topLeft: number[];
|
||||
shiftKey = false;
|
||||
initialShape: ShapesWithProp<'handles'>;
|
||||
handleId: string;
|
||||
|
||||
constructor(
|
||||
app: TldrawApp,
|
||||
shapeId: string,
|
||||
handleId: string,
|
||||
commandId = 'move_handle'
|
||||
) {
|
||||
super(app);
|
||||
const { originPoint } = app;
|
||||
this.topLeft = [...originPoint];
|
||||
this.handleId = handleId;
|
||||
this.initialShape = this.app.getShape(shapeId);
|
||||
this.commandId = commandId;
|
||||
}
|
||||
|
||||
start = (): TldrawPatch | undefined => void null;
|
||||
|
||||
update = (): TldrawPatch | undefined => {
|
||||
const {
|
||||
initialShape,
|
||||
app: { currentPageId, currentPoint },
|
||||
} = this;
|
||||
|
||||
const shape = this.app.getShape<ShapesWithProp<'handles'>>(
|
||||
initialShape.id
|
||||
);
|
||||
|
||||
if (shape.isLocked) return void null;
|
||||
|
||||
const handles = shape.handles;
|
||||
|
||||
const handleId = this.handleId as keyof typeof handles;
|
||||
|
||||
const delta = Vec.sub(currentPoint, handles[handleId].point);
|
||||
|
||||
const handleChanges = {
|
||||
[handleId]: {
|
||||
...handles[handleId],
|
||||
point: Vec.sub(
|
||||
Vec.add(handles[handleId].point, delta),
|
||||
shape.point
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
// First update the handle's next point
|
||||
const change = TLDR.get_shape_util(shape).onHandleChange?.(
|
||||
shape,
|
||||
handleChanges
|
||||
);
|
||||
|
||||
if (!change) return;
|
||||
|
||||
return {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: {
|
||||
[shape.id]: change,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
cancel = (): TldrawPatch | undefined => {
|
||||
const {
|
||||
initialShape,
|
||||
app: { currentPageId },
|
||||
} = this;
|
||||
|
||||
return {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: {
|
||||
[initialShape.id]: initialShape,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
complete = (): TldrawPatch | TldrawCommand | undefined => {
|
||||
const {
|
||||
initialShape,
|
||||
app: { currentPageId },
|
||||
} = this;
|
||||
|
||||
return {
|
||||
id: this.commandId,
|
||||
before: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: {
|
||||
[initialShape.id]: initialShape,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: {
|
||||
[initialShape.id]: TLDR.on_session_complete(
|
||||
this.app.getShape(this.initialShape.id)
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { ExceptFirst, SessionType } from '@toeverything/components/board-types';
|
||||
import { ArrowSession } from './arrow-session';
|
||||
import { BrushSession } from './brush-session';
|
||||
import { DrawSession } from './draw-session';
|
||||
import { HandleSession } from './handle-session';
|
||||
import { RotateSession } from './rotate-session';
|
||||
import { TransformSession } from './transform-session';
|
||||
import { TransformSingleSession } from './transform-single-session';
|
||||
import { TranslateSession } from './translate-session';
|
||||
import { EraseSession } from './erase-session';
|
||||
import { GridSession } from './grid-session';
|
||||
import { LaserSession } from './laser-session';
|
||||
|
||||
export type TldrawSession =
|
||||
| ArrowSession
|
||||
| BrushSession
|
||||
| DrawSession
|
||||
| HandleSession
|
||||
| RotateSession
|
||||
| TransformSession
|
||||
| TransformSingleSession
|
||||
| TranslateSession
|
||||
| EraseSession
|
||||
| GridSession;
|
||||
|
||||
export interface SessionsMap {
|
||||
[SessionType.Arrow]: typeof ArrowSession;
|
||||
[SessionType.Brush]: typeof BrushSession;
|
||||
[SessionType.Draw]: typeof DrawSession;
|
||||
[SessionType.Laser]: typeof LaserSession;
|
||||
[SessionType.Erase]: typeof EraseSession;
|
||||
[SessionType.Handle]: typeof HandleSession;
|
||||
[SessionType.Rotate]: typeof RotateSession;
|
||||
[SessionType.Transform]: typeof TransformSession;
|
||||
[SessionType.TransformSingle]: typeof TransformSingleSession;
|
||||
[SessionType.Translate]: typeof TranslateSession;
|
||||
[SessionType.Grid]: typeof GridSession;
|
||||
}
|
||||
|
||||
export type SessionOfType<K extends SessionType> = SessionsMap[K];
|
||||
|
||||
export type SessionArgsOfType<K extends SessionType> = ExceptFirst<
|
||||
ConstructorParameters<SessionOfType<K>>
|
||||
>;
|
||||
|
||||
export const sessions: { [K in SessionType]: SessionsMap[K] } = {
|
||||
[SessionType.Arrow]: ArrowSession,
|
||||
[SessionType.Brush]: BrushSession,
|
||||
[SessionType.Draw]: DrawSession,
|
||||
[SessionType.Laser]: LaserSession,
|
||||
[SessionType.Erase]: EraseSession,
|
||||
[SessionType.Handle]: HandleSession,
|
||||
[SessionType.Rotate]: RotateSession,
|
||||
[SessionType.Transform]: TransformSession,
|
||||
[SessionType.TransformSingle]: TransformSingleSession,
|
||||
[SessionType.Translate]: TranslateSession,
|
||||
[SessionType.Grid]: GridSession,
|
||||
};
|
||||
|
||||
export const getSession = <K extends SessionType>(
|
||||
type: K
|
||||
): SessionOfType<K> => {
|
||||
return sessions[type];
|
||||
};
|
||||
@@ -0,0 +1,106 @@
|
||||
import { Vec } from '@tldraw/vec';
|
||||
import {
|
||||
SessionType,
|
||||
TDStatus,
|
||||
TldrawPatch,
|
||||
TldrawCommand,
|
||||
} from '@toeverything/components/board-types';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
import { BaseSession } from './base-session';
|
||||
export class LaserSession extends BaseSession {
|
||||
type = SessionType.Draw;
|
||||
performanceMode: any = undefined;
|
||||
status = TDStatus.Creating;
|
||||
prevPoint: number[];
|
||||
prevEraseShapesSize = 0;
|
||||
|
||||
constructor(app: TldrawApp) {
|
||||
super(app);
|
||||
this.prevPoint = [...app.originPoint];
|
||||
this.interval = this.loop();
|
||||
}
|
||||
|
||||
interval: any;
|
||||
timestamp1 = 0;
|
||||
timestamp2 = 0;
|
||||
prevErasePoint: number[] = [];
|
||||
|
||||
loop = () => {
|
||||
const now = Date.now();
|
||||
const elapsed1 = now - this.timestamp1;
|
||||
const elapsed2 = now - this.timestamp2;
|
||||
const { laserLine } = this.app.appState;
|
||||
|
||||
let next = [...laserLine];
|
||||
let didUpdate = false;
|
||||
|
||||
if (elapsed1 > 16 && this.prevErasePoint !== this.prevPoint) {
|
||||
didUpdate = true;
|
||||
next = [...laserLine, this.prevPoint];
|
||||
this.prevErasePoint = this.prevPoint;
|
||||
}
|
||||
|
||||
if (elapsed2 > 32) {
|
||||
if (next.length > 1) {
|
||||
didUpdate = true;
|
||||
next.splice(0, Math.ceil(next.length * 0.1));
|
||||
this.timestamp2 = now;
|
||||
}
|
||||
}
|
||||
|
||||
if (didUpdate) {
|
||||
this.app.patchState(
|
||||
{
|
||||
appState: {
|
||||
laserLine: next,
|
||||
},
|
||||
},
|
||||
'eraseline'
|
||||
);
|
||||
}
|
||||
|
||||
this.interval = requestAnimationFrame(this.loop);
|
||||
};
|
||||
|
||||
start = (): TldrawPatch | undefined => void null;
|
||||
|
||||
update = (): TldrawPatch | undefined => {
|
||||
const { originPoint, currentPoint } = this.app;
|
||||
|
||||
const newPoint = Vec.toFixed(
|
||||
Vec.add(originPoint, Vec.sub(currentPoint, originPoint))
|
||||
);
|
||||
|
||||
this.prevPoint = newPoint;
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
cancel = (): TldrawPatch | undefined => {
|
||||
cancelAnimationFrame(this.interval);
|
||||
return {
|
||||
appState: {
|
||||
laserLine: [],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
complete = (): TldrawPatch | TldrawCommand | undefined => {
|
||||
const { page } = this.app;
|
||||
|
||||
cancelAnimationFrame(this.interval);
|
||||
|
||||
return {
|
||||
before: {
|
||||
appState: {
|
||||
laserLine: [],
|
||||
},
|
||||
},
|
||||
after: {
|
||||
appState: {
|
||||
laserLine: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import { Utils } from '@tldraw/core';
|
||||
import { Vec } from '@tldraw/vec';
|
||||
import {
|
||||
SessionType,
|
||||
TldrawCommand,
|
||||
TldrawPatch,
|
||||
TDShape,
|
||||
TDStatus,
|
||||
} from '@toeverything/components/board-types';
|
||||
import { TLDR } from '@toeverything/components/board-state';
|
||||
import { BaseSession } from './base-session';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
|
||||
export class RotateSession extends BaseSession {
|
||||
type = SessionType.Rotate;
|
||||
status = TDStatus.Transforming;
|
||||
performanceMode: undefined;
|
||||
delta = [0, 0];
|
||||
commonBoundsCenter: number[];
|
||||
initialAngle: number;
|
||||
initialShapes: {
|
||||
shape: TDShape;
|
||||
center: number[];
|
||||
}[];
|
||||
changes: Record<string, Partial<TDShape>> = {};
|
||||
|
||||
constructor(app: TldrawApp) {
|
||||
super(app);
|
||||
|
||||
const {
|
||||
app: { currentPageId, pageState, originPoint },
|
||||
} = this;
|
||||
|
||||
const initialShapes = TLDR.get_selected_branch_snapshot(
|
||||
app.state,
|
||||
currentPageId
|
||||
).filter(shape => !shape.isLocked);
|
||||
|
||||
if (initialShapes.length === 0) {
|
||||
throw Error('No selected shapes!');
|
||||
}
|
||||
|
||||
if (app.rotationInfo.selectedIds === pageState.selectedIds) {
|
||||
if (app.rotationInfo.center === undefined) {
|
||||
throw Error('We should have a center for rotation!');
|
||||
}
|
||||
|
||||
this.commonBoundsCenter = app.rotationInfo.center;
|
||||
} else {
|
||||
this.commonBoundsCenter = Utils.getBoundsCenter(
|
||||
Utils.getCommonBounds(initialShapes.map(TLDR.get_bounds))
|
||||
);
|
||||
app.rotationInfo.selectedIds = pageState.selectedIds;
|
||||
app.rotationInfo.center = this.commonBoundsCenter;
|
||||
}
|
||||
|
||||
this.initialShapes = initialShapes
|
||||
.filter(shape => shape.children === undefined)
|
||||
.map(shape => {
|
||||
return {
|
||||
shape,
|
||||
center: this.app.getShapeUtil(shape).getCenter(shape),
|
||||
};
|
||||
});
|
||||
|
||||
this.initialAngle = Vec.angle(this.commonBoundsCenter, originPoint);
|
||||
}
|
||||
|
||||
start = (): TldrawPatch | undefined => void null;
|
||||
|
||||
update = (): TldrawPatch | undefined => {
|
||||
const {
|
||||
commonBoundsCenter,
|
||||
initialShapes,
|
||||
app: { currentPageId, currentPoint, shiftKey },
|
||||
} = this;
|
||||
|
||||
const shapes: Record<string, Partial<TDShape>> = {};
|
||||
|
||||
let directionDelta =
|
||||
Vec.angle(commonBoundsCenter, currentPoint) - this.initialAngle;
|
||||
|
||||
if (shiftKey) {
|
||||
directionDelta = Utils.snapAngleToSegments(directionDelta, 24); // 15 degrees
|
||||
}
|
||||
|
||||
// Update the shapes
|
||||
initialShapes.forEach(({ center, shape }) => {
|
||||
const { rotation = 0 } = shape;
|
||||
let shapeDelta = 0;
|
||||
|
||||
if (shiftKey) {
|
||||
const snappedRotation = Utils.snapAngleToSegments(rotation, 24);
|
||||
shapeDelta = snappedRotation - rotation;
|
||||
}
|
||||
|
||||
const change = TLDR.get_rotated_shape_mutation(
|
||||
shape,
|
||||
center,
|
||||
commonBoundsCenter,
|
||||
shiftKey ? directionDelta + shapeDelta : directionDelta
|
||||
);
|
||||
|
||||
if (change) {
|
||||
shapes[shape.id] = change;
|
||||
}
|
||||
});
|
||||
|
||||
this.changes = shapes;
|
||||
|
||||
return {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
cancel = (): TldrawPatch | undefined => {
|
||||
const {
|
||||
initialShapes,
|
||||
app: { currentPageId },
|
||||
} = this;
|
||||
|
||||
const shapes: Record<string, TDShape> = {};
|
||||
initialShapes.forEach(({ shape }) => (shapes[shape.id] = shape));
|
||||
|
||||
return {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
complete = (): TldrawPatch | TldrawCommand | undefined => {
|
||||
const {
|
||||
initialShapes,
|
||||
app: { currentPageId },
|
||||
} = this;
|
||||
|
||||
const beforeShapes = {} as Record<string, Partial<TDShape>>;
|
||||
const afterShapes = this.changes;
|
||||
|
||||
initialShapes.forEach(({ shape: { id, point, rotation, handles } }) => {
|
||||
beforeShapes[id] = { point, rotation, handles };
|
||||
});
|
||||
|
||||
return {
|
||||
id: 'rotate',
|
||||
before: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: beforeShapes,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: afterShapes,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
import { TLBounds, TLBoundsCorner, TLBoundsEdge, Utils } from '@tldraw/core';
|
||||
import { Vec } from '@tldraw/vec';
|
||||
import type { TLSnapLine, TLBoundsWithCenter } from '@tldraw/core';
|
||||
import {
|
||||
SessionType,
|
||||
TldrawCommand,
|
||||
TldrawPatch,
|
||||
TDShape,
|
||||
TDStatus,
|
||||
SLOW_SPEED,
|
||||
SNAP_DISTANCE,
|
||||
} from '@toeverything/components/board-types';
|
||||
import { TLDR } from '@toeverything/components/board-state';
|
||||
import { BaseSession } from './base-session';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
|
||||
type SnapInfo =
|
||||
| {
|
||||
state: 'empty';
|
||||
}
|
||||
| {
|
||||
state: 'ready';
|
||||
bounds: TLBoundsWithCenter[];
|
||||
};
|
||||
|
||||
export class TransformSession extends BaseSession {
|
||||
type = SessionType.Transform;
|
||||
performanceMode: undefined;
|
||||
status = TDStatus.Transforming;
|
||||
scaleX = 1;
|
||||
scaleY = 1;
|
||||
initialShapes: TDShape[];
|
||||
initialShapeIds: string[];
|
||||
initialSelectedIds: string[];
|
||||
shapeBounds: {
|
||||
initialShape: TDShape;
|
||||
initialShapeBounds: TLBounds;
|
||||
transformOrigin: number[];
|
||||
}[];
|
||||
hasUnlockedShapes: boolean;
|
||||
isAllAspectRatioLocked: boolean;
|
||||
initialCommonBounds: TLBounds;
|
||||
snapInfo: SnapInfo = { state: 'empty' };
|
||||
prevPoint = [0, 0];
|
||||
speed = 1;
|
||||
|
||||
constructor(
|
||||
app: TldrawApp,
|
||||
public transformType:
|
||||
| TLBoundsEdge
|
||||
| TLBoundsCorner = TLBoundsCorner.BottomRight,
|
||||
public isCreate = false
|
||||
) {
|
||||
super(app);
|
||||
this.initialSelectedIds = [...this.app.selectedIds];
|
||||
this.app.rotationInfo.selectedIds = [...this.initialSelectedIds];
|
||||
|
||||
this.initialShapes = TLDR.get_selected_branch_snapshot(
|
||||
this.app.state,
|
||||
this.app.currentPageId
|
||||
).filter(shape => !shape.isLocked);
|
||||
|
||||
this.initialShapeIds = this.initialShapes.map(shape => shape.id);
|
||||
|
||||
this.hasUnlockedShapes = this.initialShapes.length > 0;
|
||||
|
||||
this.isAllAspectRatioLocked = this.initialShapes.every(
|
||||
shape =>
|
||||
shape.isAspectRatioLocked ||
|
||||
TLDR.get_shape_util(shape).isAspectRatioLocked
|
||||
);
|
||||
|
||||
const shapesBounds = Object.fromEntries(
|
||||
this.initialShapes.map(shape => [shape.id, TLDR.get_bounds(shape)])
|
||||
);
|
||||
|
||||
const boundsArr = Object.values(shapesBounds);
|
||||
|
||||
this.initialCommonBounds = Utils.getCommonBounds(boundsArr);
|
||||
|
||||
const initialInnerBounds = Utils.getBoundsFromPoints(
|
||||
boundsArr.map(Utils.getBoundsCenter)
|
||||
);
|
||||
|
||||
// Return a mapping of shapes to bounds together with the relative
|
||||
// positions of the shape's bounds within the common bounds shape.
|
||||
this.shapeBounds = this.initialShapes.map(shape => {
|
||||
const initialShapeBounds = shapesBounds[shape.id];
|
||||
const ic = Utils.getBoundsCenter(initialShapeBounds);
|
||||
|
||||
const ix =
|
||||
(ic[0] - initialInnerBounds.minX) / initialInnerBounds.width;
|
||||
const iy =
|
||||
(ic[1] - initialInnerBounds.minY) / initialInnerBounds.height;
|
||||
|
||||
return {
|
||||
initialShape: shape,
|
||||
initialShapeBounds,
|
||||
transformOrigin: [ix, iy],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
start = (): TldrawPatch | undefined => {
|
||||
this.snapInfo = {
|
||||
state: 'ready',
|
||||
bounds: this.app.shapes
|
||||
.filter(shape => !this.initialShapeIds.includes(shape.id))
|
||||
.map(shape =>
|
||||
Utils.getBoundsWithCenter(TLDR.get_rotated_bounds(shape))
|
||||
),
|
||||
};
|
||||
|
||||
return void null;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
update = (): TldrawPatch | undefined => {
|
||||
const {
|
||||
transformType,
|
||||
shapeBounds,
|
||||
initialCommonBounds,
|
||||
isAllAspectRatioLocked,
|
||||
app: {
|
||||
currentPageId,
|
||||
pageState: { camera },
|
||||
viewport,
|
||||
currentPoint,
|
||||
previousPoint,
|
||||
originPoint,
|
||||
shiftKey,
|
||||
altKey,
|
||||
metaKey,
|
||||
currentGrid,
|
||||
settings: { isSnapping, showGrid },
|
||||
},
|
||||
} = this;
|
||||
|
||||
const shapes = {} as Record<string, TDShape>;
|
||||
|
||||
const delta = altKey
|
||||
? Vec.mul(Vec.sub(currentPoint, originPoint), 2)
|
||||
: Vec.sub(currentPoint, originPoint);
|
||||
|
||||
let newBounds = Utils.getTransformedBoundingBox(
|
||||
initialCommonBounds,
|
||||
transformType,
|
||||
delta,
|
||||
0,
|
||||
shiftKey || isAllAspectRatioLocked
|
||||
);
|
||||
|
||||
if (altKey) {
|
||||
newBounds = {
|
||||
...newBounds,
|
||||
...Utils.centerBounds(
|
||||
newBounds,
|
||||
Utils.getBoundsCenter(initialCommonBounds)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (showGrid) {
|
||||
newBounds = {
|
||||
...newBounds,
|
||||
...Utils.snapBoundsToGrid(newBounds, currentGrid),
|
||||
};
|
||||
}
|
||||
|
||||
// Should we snap?
|
||||
|
||||
const speed = Vec.dist(currentPoint, previousPoint);
|
||||
|
||||
const speedChange = speed - this.speed;
|
||||
|
||||
this.speed = this.speed + speedChange * (speedChange > 1 ? 0.5 : 0.15);
|
||||
|
||||
let snapLines: TLSnapLine[] = [];
|
||||
|
||||
if (
|
||||
((isSnapping && !metaKey) || (!isSnapping && metaKey)) &&
|
||||
this.speed * camera.zoom < SLOW_SPEED &&
|
||||
this.snapInfo.state === 'ready'
|
||||
) {
|
||||
const snapResult = Utils.getSnapPoints(
|
||||
Utils.getBoundsWithCenter(newBounds),
|
||||
this.snapInfo.bounds.filter(
|
||||
bounds =>
|
||||
Utils.boundsContain(viewport, bounds) ||
|
||||
Utils.boundsCollide(viewport, bounds)
|
||||
),
|
||||
SNAP_DISTANCE / camera.zoom
|
||||
);
|
||||
|
||||
if (snapResult) {
|
||||
snapLines = snapResult.snapLines;
|
||||
|
||||
newBounds = Utils.getTransformedBoundingBox(
|
||||
initialCommonBounds,
|
||||
transformType,
|
||||
Vec.sub(delta, snapResult.offset),
|
||||
0,
|
||||
shiftKey || isAllAspectRatioLocked
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Now work backward to calculate a new bounding box for each of the shapes.
|
||||
|
||||
this.scaleX = newBounds.scaleX;
|
||||
this.scaleY = newBounds.scaleY;
|
||||
|
||||
shapeBounds.forEach(
|
||||
({ initialShape, initialShapeBounds, transformOrigin }) => {
|
||||
let newShapeBounds = Utils.getRelativeTransformedBoundingBox(
|
||||
newBounds,
|
||||
initialCommonBounds,
|
||||
initialShapeBounds,
|
||||
this.scaleX < 0,
|
||||
this.scaleY < 0
|
||||
);
|
||||
|
||||
if (showGrid) {
|
||||
newShapeBounds = Utils.snapBoundsToGrid(
|
||||
newShapeBounds,
|
||||
currentGrid
|
||||
);
|
||||
}
|
||||
|
||||
const afterShape = TLDR.transform(
|
||||
this.app.getShape(initialShape.id),
|
||||
newShapeBounds,
|
||||
{
|
||||
type: this.transformType,
|
||||
initialShape,
|
||||
scaleX: this.scaleX,
|
||||
scaleY: this.scaleY,
|
||||
transformOrigin,
|
||||
}
|
||||
);
|
||||
|
||||
shapes[initialShape.id] = afterShape;
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
appState: {
|
||||
snapLines,
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
cancel = (): TldrawPatch | undefined => {
|
||||
const {
|
||||
shapeBounds,
|
||||
app: { currentPageId },
|
||||
} = this;
|
||||
|
||||
const shapes = {} as Record<string, TDShape | undefined>;
|
||||
|
||||
if (this.isCreate) {
|
||||
shapeBounds.forEach(
|
||||
shape => (shapes[shape.initialShape.id] = undefined)
|
||||
);
|
||||
} else {
|
||||
shapeBounds.forEach(
|
||||
shape => (shapes[shape.initialShape.id] = shape.initialShape)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
appState: {
|
||||
snapLines: [],
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: this.isCreate
|
||||
? []
|
||||
: shapeBounds.map(shape => shape.initialShape.id),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
complete = (): TldrawPatch | TldrawCommand | undefined => {
|
||||
const {
|
||||
isCreate,
|
||||
shapeBounds,
|
||||
hasUnlockedShapes,
|
||||
app: { currentPageId },
|
||||
} = this;
|
||||
|
||||
if (!hasUnlockedShapes) return;
|
||||
|
||||
if (
|
||||
this.isCreate &&
|
||||
Vec.dist(this.app.originPoint, this.app.currentPoint) < 2
|
||||
) {
|
||||
return this.cancel();
|
||||
}
|
||||
|
||||
const beforeShapes: Record<string, TDShape | undefined> = {};
|
||||
const afterShapes: Record<string, TDShape> = {};
|
||||
|
||||
let beforeSelectedIds: string[];
|
||||
let afterSelectedIds: string[];
|
||||
|
||||
if (isCreate) {
|
||||
beforeSelectedIds = [];
|
||||
afterSelectedIds = [];
|
||||
shapeBounds.forEach(({ initialShape }) => {
|
||||
beforeShapes[initialShape.id] = undefined;
|
||||
afterShapes[initialShape.id] = this.app.getShape(
|
||||
initialShape.id
|
||||
);
|
||||
});
|
||||
} else {
|
||||
beforeSelectedIds = this.initialSelectedIds;
|
||||
afterSelectedIds = this.initialSelectedIds;
|
||||
shapeBounds.forEach(({ initialShape }) => {
|
||||
beforeShapes[initialShape.id] = initialShape;
|
||||
afterShapes[initialShape.id] = this.app.getShape(
|
||||
initialShape.id
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'transform',
|
||||
before: {
|
||||
appState: {
|
||||
snapLines: [],
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: beforeShapes,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: beforeSelectedIds,
|
||||
hoveredId: undefined,
|
||||
editingId: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
appState: {
|
||||
snapLines: [],
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: afterShapes,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: afterSelectedIds,
|
||||
hoveredId: undefined,
|
||||
editingId: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
import {
|
||||
TLBoundsCorner,
|
||||
TLSnapLine,
|
||||
TLBoundsEdge,
|
||||
Utils,
|
||||
TLBoundsWithCenter,
|
||||
TLBounds,
|
||||
} from '@tldraw/core';
|
||||
import { Vec } from '@tldraw/vec';
|
||||
import {
|
||||
SessionType,
|
||||
TldrawCommand,
|
||||
TldrawPatch,
|
||||
TDShape,
|
||||
TDStatus,
|
||||
SLOW_SPEED,
|
||||
SNAP_DISTANCE,
|
||||
} from '@toeverything/components/board-types';
|
||||
import { TLDR } from '@toeverything/components/board-state';
|
||||
import { BaseSession } from './base-session';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
|
||||
type SnapInfo =
|
||||
| {
|
||||
state: 'empty';
|
||||
}
|
||||
| {
|
||||
state: 'ready';
|
||||
bounds: TLBoundsWithCenter[];
|
||||
};
|
||||
|
||||
export class TransformSingleSession extends BaseSession {
|
||||
type = SessionType.TransformSingle;
|
||||
status = TDStatus.Transforming;
|
||||
performanceMode: undefined;
|
||||
transformType: TLBoundsEdge | TLBoundsCorner;
|
||||
scaleX = 1;
|
||||
scaleY = 1;
|
||||
isCreate: boolean;
|
||||
initialShape: TDShape;
|
||||
initialShapeBounds: TLBounds;
|
||||
initialCommonBounds: TLBounds;
|
||||
snapInfo: SnapInfo = { state: 'empty' };
|
||||
prevPoint = [0, 0];
|
||||
speed = 1;
|
||||
|
||||
constructor(
|
||||
app: TldrawApp,
|
||||
id: string,
|
||||
transformType: TLBoundsEdge | TLBoundsCorner,
|
||||
isCreate = false
|
||||
) {
|
||||
super(app);
|
||||
this.isCreate = isCreate;
|
||||
this.transformType = transformType;
|
||||
|
||||
const shape = this.app.getShape(id);
|
||||
this.initialShape = shape;
|
||||
this.initialShapeBounds = TLDR.get_bounds(shape);
|
||||
this.initialCommonBounds = TLDR.get_rotated_bounds(shape);
|
||||
this.app.rotationInfo.selectedIds = [shape.id];
|
||||
}
|
||||
|
||||
start = (): TldrawPatch | undefined => {
|
||||
this.snapInfo = {
|
||||
state: 'ready',
|
||||
bounds: this.app.shapes
|
||||
.filter(shape => shape.id !== this.initialShape.id)
|
||||
.map(shape =>
|
||||
Utils.getBoundsWithCenter(TLDR.get_rotated_bounds(shape))
|
||||
),
|
||||
};
|
||||
|
||||
return void null;
|
||||
};
|
||||
|
||||
update = (): TldrawPatch | undefined => {
|
||||
const {
|
||||
transformType,
|
||||
initialShape,
|
||||
initialShapeBounds,
|
||||
app: {
|
||||
settings: { isSnapping, showGrid },
|
||||
currentPageId,
|
||||
pageState: { camera },
|
||||
viewport,
|
||||
currentPoint,
|
||||
previousPoint,
|
||||
originPoint,
|
||||
currentGrid,
|
||||
shiftKey,
|
||||
altKey,
|
||||
metaKey,
|
||||
},
|
||||
} = this;
|
||||
|
||||
if (initialShape.isLocked) return void null;
|
||||
|
||||
const shapes = {} as Record<string, Partial<TDShape>>;
|
||||
|
||||
const delta = altKey
|
||||
? Vec.mul(Vec.sub(currentPoint, originPoint), 2)
|
||||
: Vec.sub(currentPoint, originPoint);
|
||||
|
||||
const shape = this.app.getShape(initialShape.id);
|
||||
|
||||
const utils = TLDR.get_shape_util(shape);
|
||||
|
||||
let newBounds = Utils.getTransformedBoundingBox(
|
||||
initialShapeBounds,
|
||||
transformType,
|
||||
delta,
|
||||
shape.rotation,
|
||||
shiftKey || shape.isAspectRatioLocked || utils.isAspectRatioLocked
|
||||
);
|
||||
|
||||
if (altKey) {
|
||||
newBounds = {
|
||||
...newBounds,
|
||||
...Utils.centerBounds(
|
||||
newBounds,
|
||||
Utils.getBoundsCenter(initialShapeBounds)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (showGrid) {
|
||||
newBounds = {
|
||||
...newBounds,
|
||||
...Utils.snapBoundsToGrid(newBounds, currentGrid),
|
||||
};
|
||||
}
|
||||
|
||||
// Should we snap?
|
||||
|
||||
const speed = Vec.dist(currentPoint, previousPoint);
|
||||
|
||||
const speedChange = speed - this.speed;
|
||||
|
||||
this.speed = this.speed + speedChange * (speedChange > 1 ? 0.5 : 0.15);
|
||||
|
||||
let snapLines: TLSnapLine[] = [];
|
||||
|
||||
if (
|
||||
((isSnapping && !metaKey) || (!isSnapping && metaKey)) &&
|
||||
!initialShape.rotation && // not now anyway
|
||||
this.speed * camera.zoom < SLOW_SPEED &&
|
||||
this.snapInfo.state === 'ready'
|
||||
) {
|
||||
const snapResult = Utils.getSnapPoints(
|
||||
Utils.getBoundsWithCenter(newBounds),
|
||||
this.snapInfo.bounds.filter(
|
||||
bounds =>
|
||||
Utils.boundsContain(viewport, bounds) ||
|
||||
Utils.boundsCollide(viewport, bounds)
|
||||
),
|
||||
SNAP_DISTANCE / camera.zoom
|
||||
);
|
||||
|
||||
if (snapResult) {
|
||||
snapLines = snapResult.snapLines;
|
||||
|
||||
newBounds = Utils.getTransformedBoundingBox(
|
||||
initialShapeBounds,
|
||||
transformType,
|
||||
Vec.sub(delta, snapResult.offset),
|
||||
shape.rotation,
|
||||
shiftKey ||
|
||||
shape.isAspectRatioLocked ||
|
||||
utils.isAspectRatioLocked
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const afterShape = TLDR.get_shape_util(shape).transformSingle(
|
||||
shape,
|
||||
newBounds,
|
||||
{
|
||||
initialShape,
|
||||
type: this.transformType,
|
||||
scaleX: newBounds.scaleX,
|
||||
scaleY: newBounds.scaleY,
|
||||
transformOrigin: [0.5, 0.5],
|
||||
}
|
||||
);
|
||||
|
||||
if (afterShape) {
|
||||
shapes[shape.id] = afterShape;
|
||||
}
|
||||
|
||||
if (showGrid && afterShape && afterShape.point) {
|
||||
afterShape.point = Vec.snap(afterShape.point, currentGrid);
|
||||
}
|
||||
|
||||
return {
|
||||
appState: {
|
||||
snapLines,
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
cancel = (): TldrawPatch | undefined => {
|
||||
const {
|
||||
initialShape,
|
||||
app: { currentPageId },
|
||||
} = this;
|
||||
|
||||
const shapes = {} as Record<string, TDShape | undefined>;
|
||||
|
||||
if (this.isCreate) {
|
||||
shapes[initialShape.id] = undefined;
|
||||
} else {
|
||||
shapes[initialShape.id] = initialShape;
|
||||
}
|
||||
|
||||
return {
|
||||
appState: {
|
||||
snapLines: [],
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: this.isCreate ? [] : [initialShape.id],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
complete = (): TldrawPatch | TldrawCommand | undefined => {
|
||||
const {
|
||||
initialShape,
|
||||
app: { currentPageId },
|
||||
} = this;
|
||||
|
||||
if (initialShape.isLocked) return;
|
||||
|
||||
if (
|
||||
this.isCreate &&
|
||||
Vec.dist(this.app.originPoint, this.app.currentPoint) < 2
|
||||
) {
|
||||
return this.cancel();
|
||||
}
|
||||
|
||||
const beforeShapes = {} as Record<string, Partial<TDShape> | undefined>;
|
||||
const afterShapes = {} as Record<string, Partial<TDShape>>;
|
||||
|
||||
beforeShapes[initialShape.id] = this.isCreate
|
||||
? undefined
|
||||
: initialShape;
|
||||
|
||||
afterShapes[initialShape.id] = TLDR.on_session_complete(
|
||||
this.app.getShape(initialShape.id)
|
||||
);
|
||||
|
||||
return {
|
||||
id: 'transform_single',
|
||||
before: {
|
||||
appState: {
|
||||
snapLines: [],
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: beforeShapes,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: this.isCreate ? [] : [initialShape.id],
|
||||
editingId: undefined,
|
||||
hoveredId: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
appState: {
|
||||
snapLines: [],
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: afterShapes,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: [initialShape.id],
|
||||
editingId: undefined,
|
||||
hoveredId: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import {
|
||||
SessionType,
|
||||
TldrawCommand,
|
||||
TldrawPatch,
|
||||
TDStatus,
|
||||
RectangleShape,
|
||||
TriangleShape,
|
||||
EllipseShape,
|
||||
ArrowShape,
|
||||
} from '@toeverything/components/board-types';
|
||||
import { TLDR } from '@toeverything/components/board-state';
|
||||
import { BaseSession } from './base-session';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
import type { TLBounds } from '@tldraw/core';
|
||||
|
||||
export class TranslateLabelSession extends BaseSession {
|
||||
type = SessionType.Handle;
|
||||
performanceMode: undefined;
|
||||
status = TDStatus.TranslatingHandle;
|
||||
initialShape: RectangleShape | TriangleShape | EllipseShape | ArrowShape;
|
||||
initialShapeBounds: TLBounds;
|
||||
|
||||
constructor(app: TldrawApp, shapeId: string) {
|
||||
super(app);
|
||||
this.initialShape = this.app.getShape<
|
||||
RectangleShape | TriangleShape | EllipseShape | ArrowShape
|
||||
>(shapeId);
|
||||
this.initialShapeBounds = this.app.getShapeBounds(shapeId);
|
||||
}
|
||||
|
||||
start = (): TldrawPatch | undefined => void null;
|
||||
|
||||
update = (): TldrawPatch | undefined => {
|
||||
const {
|
||||
initialShapeBounds,
|
||||
app: { currentPageId, currentPoint },
|
||||
} = this;
|
||||
|
||||
const newHandlePoint = [
|
||||
Math.max(
|
||||
0,
|
||||
Math.min(1, currentPoint[0] / initialShapeBounds.width)
|
||||
),
|
||||
Math.max(
|
||||
0,
|
||||
Math.min(1, currentPoint[1] / initialShapeBounds.height)
|
||||
),
|
||||
];
|
||||
|
||||
// First update the handle's next point
|
||||
const change = {
|
||||
handlePoint: newHandlePoint,
|
||||
} as Partial<
|
||||
RectangleShape | TriangleShape | EllipseShape | ArrowShape
|
||||
>;
|
||||
|
||||
return {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: {
|
||||
[this.initialShape.id]: change,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
cancel = (): TldrawPatch | undefined => {
|
||||
const {
|
||||
initialShape,
|
||||
app: { currentPageId },
|
||||
} = this;
|
||||
|
||||
return {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: {
|
||||
[initialShape.id]: initialShape,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
complete = (): TldrawPatch | TldrawCommand | undefined => {
|
||||
const {
|
||||
initialShape,
|
||||
app: { currentPageId },
|
||||
} = this;
|
||||
|
||||
return {
|
||||
before: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: {
|
||||
[initialShape.id]: initialShape,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: {
|
||||
[initialShape.id]: TLDR.on_session_complete(
|
||||
this.app.getShape(this.initialShape.id)
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,822 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import {
|
||||
TLPageState,
|
||||
Utils,
|
||||
TLBoundsWithCenter,
|
||||
TLSnapLine,
|
||||
TLBounds,
|
||||
} from '@tldraw/core';
|
||||
import { Vec } from '@tldraw/vec';
|
||||
import {
|
||||
TDShape,
|
||||
TDBinding,
|
||||
TldrawCommand,
|
||||
TDStatus,
|
||||
ArrowShape,
|
||||
Patch,
|
||||
GroupShape,
|
||||
SessionType,
|
||||
ArrowBinding,
|
||||
TldrawPatch,
|
||||
TDShapeType,
|
||||
SLOW_SPEED,
|
||||
SNAP_DISTANCE,
|
||||
} from '@toeverything/components/board-types';
|
||||
import { TLDR } from '@toeverything/components/board-state';
|
||||
import { BaseSession } from './base-session';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
|
||||
type CloneInfo =
|
||||
| {
|
||||
state: 'empty';
|
||||
}
|
||||
| {
|
||||
state: 'ready';
|
||||
cloneMap: Record<string, string>;
|
||||
clones: TDShape[];
|
||||
clonedBindings: ArrowBinding[];
|
||||
};
|
||||
|
||||
type SnapInfo =
|
||||
| {
|
||||
state: 'empty';
|
||||
}
|
||||
| {
|
||||
state: 'ready';
|
||||
others: TLBoundsWithCenter[];
|
||||
bounds: TLBoundsWithCenter[];
|
||||
};
|
||||
|
||||
export class TranslateSession extends BaseSession {
|
||||
performanceMode: undefined;
|
||||
type = SessionType.Translate;
|
||||
status = TDStatus.Translating;
|
||||
delta = [0, 0];
|
||||
prev = [0, 0];
|
||||
prevPoint = [0, 0];
|
||||
speed = 1;
|
||||
cloneInfo: CloneInfo = {
|
||||
state: 'empty',
|
||||
};
|
||||
snapInfo: SnapInfo = {
|
||||
state: 'empty',
|
||||
};
|
||||
snapLines: TLSnapLine[] = [];
|
||||
isCloning = false;
|
||||
isCreate: boolean;
|
||||
link: 'left' | 'right' | 'center' | false;
|
||||
|
||||
initialIds: Set<string>;
|
||||
hasUnlockedShapes: boolean;
|
||||
initialSelectedIds: string[];
|
||||
initialCommonBounds: TLBounds;
|
||||
initialShapes: TDShape[];
|
||||
initialParentChildren: Record<string, string[]>;
|
||||
bindingsToDelete: ArrowBinding[];
|
||||
|
||||
constructor(
|
||||
app: TldrawApp,
|
||||
isCreate = false,
|
||||
link: 'left' | 'right' | 'center' | false = false
|
||||
) {
|
||||
super(app);
|
||||
this.isCreate = isCreate;
|
||||
this.link = link;
|
||||
|
||||
const { currentPageId, selectedIds, page } = this.app;
|
||||
this.initialSelectedIds = [...selectedIds];
|
||||
|
||||
const selectedShapes = (
|
||||
link
|
||||
? TLDR.get_linked_shape_ids(
|
||||
this.app.state,
|
||||
currentPageId,
|
||||
link,
|
||||
false
|
||||
)
|
||||
: selectedIds
|
||||
)
|
||||
.map(id => this.app.getShape(id))
|
||||
.filter(shape => !shape.isLocked);
|
||||
|
||||
const selectedShapeIds = new Set(selectedShapes.map(shape => shape.id));
|
||||
|
||||
selectedIds.forEach(item => {
|
||||
// let shap = this.app.page.shapes[selectedIds[]];
|
||||
const shap = this.app.getShape(item);
|
||||
if (shap.type === TDShapeType.Frame) {
|
||||
Object.entries(this.app.page.shapes).map(([id, shapItem]) => {
|
||||
if (id !== shap.id) {
|
||||
if (
|
||||
Utils.boundsContain(
|
||||
TLDR.get_bounds(shap),
|
||||
TLDR.get_bounds(shapItem)
|
||||
)
|
||||
) {
|
||||
selectedShapes.push(shapItem);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// }
|
||||
this.hasUnlockedShapes = selectedShapes.length > 0;
|
||||
this.initialShapes = Array.from(
|
||||
new Set(
|
||||
selectedShapes
|
||||
.filter(shape => !selectedShapeIds.has(shape.parentId))
|
||||
.flatMap(shape => {
|
||||
return shape.children
|
||||
? [
|
||||
shape,
|
||||
...shape.children.map(childId =>
|
||||
this.app.getShape(childId)
|
||||
),
|
||||
]
|
||||
: [shape];
|
||||
})
|
||||
).values()
|
||||
);
|
||||
|
||||
this.initialIds = new Set(this.initialShapes.map(shape => shape.id));
|
||||
|
||||
this.bindingsToDelete = [];
|
||||
|
||||
Object.values(page.bindings)
|
||||
.filter(
|
||||
binding =>
|
||||
this.initialIds.has(binding.fromId) ||
|
||||
this.initialIds.has(binding.toId)
|
||||
)
|
||||
.forEach(binding => {
|
||||
if (this.initialIds.has(binding.fromId)) {
|
||||
if (!this.initialIds.has(binding.toId)) {
|
||||
this.bindingsToDelete.push(binding);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.initialParentChildren = {};
|
||||
|
||||
this.initialShapes
|
||||
.map(s => s.parentId)
|
||||
.filter(id => id !== page.id)
|
||||
.forEach(id => {
|
||||
this.initialParentChildren[id] =
|
||||
this.app.getShape(id).children!;
|
||||
});
|
||||
|
||||
this.initialCommonBounds = Utils.getCommonBounds(
|
||||
this.initialShapes.map(TLDR.get_rotated_bounds)
|
||||
);
|
||||
|
||||
this.app.rotationInfo.selectedIds = [...this.app.selectedIds];
|
||||
}
|
||||
|
||||
start = (): TldrawPatch | undefined => {
|
||||
const {
|
||||
bindingsToDelete,
|
||||
initialIds,
|
||||
app: { currentPageId, page },
|
||||
} = this;
|
||||
|
||||
const allBounds: TLBoundsWithCenter[] = [];
|
||||
const otherBounds: TLBoundsWithCenter[] = [];
|
||||
|
||||
Object.values(page.shapes).forEach(shape => {
|
||||
const bounds = Utils.getBoundsWithCenter(
|
||||
TLDR.get_rotated_bounds(shape)
|
||||
);
|
||||
allBounds.push(bounds);
|
||||
if (!initialIds.has(shape.id)) {
|
||||
otherBounds.push(bounds);
|
||||
}
|
||||
});
|
||||
|
||||
this.snapInfo = {
|
||||
state: 'ready',
|
||||
bounds: allBounds,
|
||||
others: otherBounds,
|
||||
};
|
||||
|
||||
if (bindingsToDelete.length === 0) return;
|
||||
|
||||
const nextBindings: Patch<Record<string, TDBinding>> = {};
|
||||
|
||||
bindingsToDelete.forEach(
|
||||
binding => (nextBindings[binding.id] = undefined)
|
||||
);
|
||||
|
||||
return {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
bindings: nextBindings,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
update = (): TldrawPatch | undefined => {
|
||||
const {
|
||||
initialParentChildren,
|
||||
initialShapes,
|
||||
initialCommonBounds,
|
||||
bindingsToDelete,
|
||||
app: {
|
||||
pageState: { camera },
|
||||
settings: { isSnapping, showGrid },
|
||||
currentPageId,
|
||||
viewport,
|
||||
selectedIds,
|
||||
currentPoint,
|
||||
previousPoint,
|
||||
originPoint,
|
||||
altKey,
|
||||
shiftKey,
|
||||
metaKey,
|
||||
currentGrid,
|
||||
},
|
||||
} = this;
|
||||
|
||||
const nextBindings: Patch<Record<string, TDBinding>> = {};
|
||||
const nextShapes: Patch<Record<string, TDShape>> = {};
|
||||
const nextPageState: Patch<TLPageState> = {};
|
||||
|
||||
let delta = Vec.sub(currentPoint, originPoint);
|
||||
|
||||
let didChangeCloning = false;
|
||||
|
||||
if (!this.isCreate) {
|
||||
if (altKey && !this.isCloning) {
|
||||
this.isCloning = true;
|
||||
didChangeCloning = true;
|
||||
} else if (!altKey && this.isCloning) {
|
||||
this.isCloning = false;
|
||||
didChangeCloning = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shiftKey) {
|
||||
if (Math.abs(delta[0]) < Math.abs(delta[1])) {
|
||||
delta[0] = 0;
|
||||
} else {
|
||||
delta[1] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Should we snap?
|
||||
|
||||
// Speed is used to decide which snap points to use. At a high
|
||||
// speed, we don't use any snap points. At a low speed, we only
|
||||
// allow center-to-center snap points. At very low speed, we
|
||||
// enable all snap points (still preferring middle snaps). We're
|
||||
// using an acceleration function here to smooth the changes in
|
||||
// speed, but we also want the speed to accelerate faster than
|
||||
// it decelerates.
|
||||
|
||||
const speed = Vec.dist(currentPoint, previousPoint);
|
||||
|
||||
const change = speed - this.speed;
|
||||
|
||||
this.speed = this.speed + change * (change > 1 ? 0.5 : 0.15);
|
||||
|
||||
this.snapLines = [];
|
||||
|
||||
if (
|
||||
((isSnapping && !metaKey) || (!isSnapping && metaKey)) &&
|
||||
this.speed * camera.zoom < SLOW_SPEED &&
|
||||
this.snapInfo.state === 'ready'
|
||||
) {
|
||||
const snapResult = Utils.getSnapPoints(
|
||||
Utils.getBoundsWithCenter(
|
||||
showGrid
|
||||
? Utils.snapBoundsToGrid(
|
||||
Utils.translateBounds(initialCommonBounds, delta),
|
||||
currentGrid
|
||||
)
|
||||
: Utils.translateBounds(initialCommonBounds, delta)
|
||||
),
|
||||
(this.isCloning
|
||||
? this.snapInfo.bounds
|
||||
: this.snapInfo.others
|
||||
).filter(bounds => {
|
||||
return (
|
||||
Utils.boundsContain(viewport, bounds) ||
|
||||
Utils.boundsCollide(viewport, bounds)
|
||||
);
|
||||
}),
|
||||
SNAP_DISTANCE / camera.zoom
|
||||
);
|
||||
|
||||
if (snapResult) {
|
||||
this.snapLines = snapResult.snapLines;
|
||||
delta = Vec.sub(delta, snapResult.offset);
|
||||
}
|
||||
}
|
||||
|
||||
// We've now calculated the "delta", or difference between the
|
||||
// cursor's position (real or adjusted by snaps or axis locking)
|
||||
// and the cursor's original position ("origin").
|
||||
|
||||
// The "movement" is the actual change of position between this
|
||||
// computed position and the previous computed position.
|
||||
|
||||
this.prev = delta;
|
||||
|
||||
// If cloning...
|
||||
if (this.isCloning) {
|
||||
// Not Cloning -> Cloning
|
||||
if (didChangeCloning) {
|
||||
if (this.cloneInfo.state === 'empty') {
|
||||
this.create_clone_info();
|
||||
}
|
||||
|
||||
if (this.cloneInfo.state === 'empty') {
|
||||
throw Error;
|
||||
}
|
||||
|
||||
const { clones, clonedBindings } = this.cloneInfo;
|
||||
|
||||
this.isCloning = true;
|
||||
|
||||
// Put back any bindings we deleted
|
||||
bindingsToDelete.forEach(
|
||||
binding => (nextBindings[binding.id] = binding)
|
||||
);
|
||||
|
||||
// Move original shapes back to start
|
||||
initialShapes.forEach(
|
||||
shape => (nextShapes[shape.id] = { point: shape.point })
|
||||
);
|
||||
|
||||
// Add the clones to the page
|
||||
clones.forEach(clone => {
|
||||
nextShapes[clone.id] = { ...clone };
|
||||
|
||||
// Add clones to non-selected parents
|
||||
if (
|
||||
clone.parentId !== currentPageId &&
|
||||
!selectedIds.includes(clone.parentId)
|
||||
) {
|
||||
const children =
|
||||
nextShapes[clone.parentId]?.children ||
|
||||
initialParentChildren[clone.parentId];
|
||||
|
||||
if (!children.includes(clone.id)) {
|
||||
nextShapes[clone.parentId] = {
|
||||
...nextShapes[clone.parentId],
|
||||
children: [...children, clone.id],
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add the cloned bindings
|
||||
for (const binding of clonedBindings) {
|
||||
nextBindings[binding.id] = binding;
|
||||
}
|
||||
|
||||
// Set the selected ids to the clones
|
||||
nextPageState.selectedIds = clones.map(clone => clone.id);
|
||||
|
||||
// Either way, move the clones
|
||||
clones.forEach(clone => {
|
||||
nextShapes[clone.id] = {
|
||||
...clone,
|
||||
point: showGrid
|
||||
? Vec.snap(
|
||||
Vec.toFixed(Vec.add(clone.point, delta)),
|
||||
currentGrid
|
||||
)
|
||||
: Vec.toFixed(Vec.add(clone.point, delta)),
|
||||
};
|
||||
});
|
||||
} else {
|
||||
if (this.cloneInfo.state === 'empty') throw Error;
|
||||
|
||||
const { clones } = this.cloneInfo;
|
||||
|
||||
clones.forEach(clone => {
|
||||
nextShapes[clone.id] = {
|
||||
point: showGrid
|
||||
? Vec.snap(
|
||||
Vec.toFixed(Vec.add(clone.point, delta)),
|
||||
currentGrid
|
||||
)
|
||||
: Vec.toFixed(Vec.add(clone.point, delta)),
|
||||
};
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// If not cloning...
|
||||
|
||||
// Cloning -> Not Cloning
|
||||
if (didChangeCloning) {
|
||||
if (this.cloneInfo.state === 'empty') throw Error;
|
||||
|
||||
const { clones, clonedBindings } = this.cloneInfo;
|
||||
|
||||
this.isCloning = false;
|
||||
|
||||
// Delete the bindings
|
||||
bindingsToDelete.forEach(
|
||||
binding => (nextBindings[binding.id] = undefined)
|
||||
);
|
||||
|
||||
// Remove the clones from parents
|
||||
clones.forEach(clone => {
|
||||
if (clone.parentId !== currentPageId) {
|
||||
nextShapes[clone.parentId] = {
|
||||
...nextShapes[clone.parentId],
|
||||
children: initialParentChildren[clone.parentId],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Delete the clones (including any parent clones)
|
||||
clones.forEach(clone => (nextShapes[clone.id] = undefined));
|
||||
|
||||
// Move the original shapes back to the cursor position
|
||||
initialShapes.forEach(shape => {
|
||||
nextShapes[shape.id] = {
|
||||
point: showGrid
|
||||
? Vec.snap(
|
||||
Vec.toFixed(Vec.add(shape.point, delta)),
|
||||
currentGrid
|
||||
)
|
||||
: Vec.toFixed(Vec.add(shape.point, delta)),
|
||||
};
|
||||
});
|
||||
|
||||
// Delete the cloned bindings
|
||||
for (const binding of clonedBindings) {
|
||||
nextBindings[binding.id] = undefined;
|
||||
}
|
||||
|
||||
// Set selected ids
|
||||
nextPageState.selectedIds = initialShapes.map(
|
||||
shape => shape.id
|
||||
);
|
||||
} else {
|
||||
// Move the shapes by the delta
|
||||
initialShapes.forEach(shape => {
|
||||
// const current = (nextShapes[shape.id] || this.app.getShape(shape.id)) as TDShape
|
||||
|
||||
nextShapes[shape.id] = {
|
||||
point: showGrid
|
||||
? Vec.snap(
|
||||
Vec.toFixed(Vec.add(shape.point, delta)),
|
||||
currentGrid
|
||||
)
|
||||
: Vec.toFixed(Vec.add(shape.point, delta)),
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
appState: {
|
||||
snapLines: this.snapLines,
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: nextShapes,
|
||||
bindings: nextBindings,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: nextPageState,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
cancel = (): TldrawPatch | undefined => {
|
||||
const {
|
||||
initialShapes,
|
||||
initialSelectedIds,
|
||||
bindingsToDelete,
|
||||
app: { currentPageId },
|
||||
} = this;
|
||||
|
||||
const nextBindings: Record<string, Partial<TDBinding> | undefined> = {};
|
||||
const nextShapes: Record<string, Partial<TDShape> | undefined> = {};
|
||||
const nextPageState: Partial<TLPageState> = {
|
||||
editingId: undefined,
|
||||
hoveredId: undefined,
|
||||
};
|
||||
|
||||
// Put back any deleted bindings
|
||||
bindingsToDelete.forEach(
|
||||
binding => (nextBindings[binding.id] = binding)
|
||||
);
|
||||
|
||||
if (this.isCreate) {
|
||||
initialShapes.forEach(({ id }) => (nextShapes[id] = undefined));
|
||||
nextPageState.selectedIds = [];
|
||||
} else {
|
||||
// Put initial shapes back to where they started
|
||||
initialShapes.forEach(
|
||||
({ id, point }) =>
|
||||
(nextShapes[id] = { ...nextShapes[id], point })
|
||||
);
|
||||
nextPageState.selectedIds = initialSelectedIds;
|
||||
}
|
||||
|
||||
if (this.cloneInfo.state === 'ready') {
|
||||
const { clones, clonedBindings } = this.cloneInfo;
|
||||
// Delete clones
|
||||
clones.forEach(clone => (nextShapes[clone.id] = undefined));
|
||||
|
||||
// Delete cloned bindings
|
||||
clonedBindings.forEach(
|
||||
binding => (nextBindings[binding.id] = undefined)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
appState: {
|
||||
snapLines: [],
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: nextShapes,
|
||||
bindings: nextBindings,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: nextPageState,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
complete = (): TldrawPatch | TldrawCommand | undefined => {
|
||||
const {
|
||||
initialShapes,
|
||||
initialParentChildren,
|
||||
bindingsToDelete,
|
||||
app: { currentPageId },
|
||||
} = this;
|
||||
|
||||
const beforeBindings: Patch<Record<string, TDBinding>> = {};
|
||||
const beforeShapes: Patch<Record<string, TDShape>> = {};
|
||||
|
||||
const afterBindings: Patch<Record<string, TDBinding>> = {};
|
||||
const afterShapes: Patch<Record<string, TDShape>> = {};
|
||||
|
||||
if (this.isCloning) {
|
||||
if (this.cloneInfo.state === 'empty') {
|
||||
this.create_clone_info();
|
||||
}
|
||||
|
||||
if (this.cloneInfo.state !== 'ready') throw Error;
|
||||
const { clones, clonedBindings } = this.cloneInfo;
|
||||
|
||||
// Update the clones
|
||||
clones.forEach(clone => {
|
||||
beforeShapes[clone.id] = undefined;
|
||||
|
||||
afterShapes[clone.id] = this.app.getShape(clone.id);
|
||||
|
||||
if (clone.parentId !== currentPageId) {
|
||||
beforeShapes[clone.parentId] = {
|
||||
...beforeShapes[clone.parentId],
|
||||
children: initialParentChildren[clone.parentId],
|
||||
};
|
||||
|
||||
afterShapes[clone.parentId] = {
|
||||
...afterShapes[clone.parentId],
|
||||
children: this.app.getShape<GroupShape>(clone.parentId)
|
||||
.children,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Update the cloned bindings
|
||||
clonedBindings.forEach(binding => {
|
||||
beforeBindings[binding.id] = undefined;
|
||||
afterBindings[binding.id] = this.app.getBinding(binding.id);
|
||||
});
|
||||
} else {
|
||||
// If we aren't cloning, then update the initial shapes
|
||||
initialShapes.forEach(shape => {
|
||||
beforeShapes[shape.id] = this.isCreate
|
||||
? undefined
|
||||
: {
|
||||
...beforeShapes[shape.id],
|
||||
point: shape.point,
|
||||
};
|
||||
|
||||
afterShapes[shape.id] = {
|
||||
...afterShapes[shape.id],
|
||||
...(this.isCreate
|
||||
? this.app.getShape(shape.id)
|
||||
: { point: this.app.getShape(shape.id).point }),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Update the deleted bindings and any associated shapes
|
||||
bindingsToDelete.forEach(binding => {
|
||||
beforeBindings[binding.id] = binding;
|
||||
|
||||
for (const id of [binding.toId, binding.fromId]) {
|
||||
// Let's also look at the bound shape...
|
||||
const shape = this.app.getShape(id);
|
||||
|
||||
// If the bound shape has a handle that references the deleted binding, delete that reference
|
||||
if (!shape.handles) continue;
|
||||
|
||||
Object.values(shape.handles)
|
||||
.filter(handle => handle.bindingId === binding.id)
|
||||
.forEach(handle => {
|
||||
beforeShapes[id] = { ...beforeShapes[id], handles: {} };
|
||||
|
||||
afterShapes[id] = { ...afterShapes[id], handles: {} };
|
||||
|
||||
// There should be before and after shapes
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
beforeShapes[id]!.handles![
|
||||
handle.id as keyof ArrowShape['handles']
|
||||
] = {
|
||||
bindingId: binding.id,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
afterShapes[id]!.handles![
|
||||
handle.id as keyof ArrowShape['handles']
|
||||
] = {
|
||||
bindingId: undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
id: 'translate',
|
||||
before: {
|
||||
appState: {
|
||||
snapLines: [],
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: beforeShapes,
|
||||
bindings: beforeBindings,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: this.isCreate
|
||||
? []
|
||||
: [...this.initialSelectedIds],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
appState: {
|
||||
snapLines: [],
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: afterShapes,
|
||||
bindings: afterBindings,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: [...this.app.selectedIds],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
private create_clone_info = () => {
|
||||
// Create clones when as they're needed.
|
||||
// Consider doing this work in a worker.
|
||||
|
||||
const {
|
||||
initialShapes,
|
||||
initialParentChildren,
|
||||
app: { selectedIds, currentPageId, page },
|
||||
} = this;
|
||||
|
||||
const cloneMap: Record<string, string> = {};
|
||||
const clonedBindingsMap: Record<string, string> = {};
|
||||
const clonedBindings: TDBinding[] = [];
|
||||
|
||||
// Create clones of selected shapes
|
||||
const clones: TDShape[] = [];
|
||||
|
||||
initialShapes.forEach(shape => {
|
||||
const newId = Utils.uniqueId();
|
||||
|
||||
initialParentChildren[newId] = initialParentChildren[shape.id];
|
||||
|
||||
cloneMap[shape.id] = newId;
|
||||
|
||||
const clone = {
|
||||
...Utils.deepClone(shape),
|
||||
id: newId,
|
||||
parentId: shape.parentId,
|
||||
childIndex: TLDR.get_child_index_above(
|
||||
this.app.state,
|
||||
shape.id,
|
||||
currentPageId
|
||||
),
|
||||
};
|
||||
|
||||
if (clone.type === TDShapeType.Video) {
|
||||
const element = document.getElementById(
|
||||
shape.id + '_video'
|
||||
) as HTMLVideoElement;
|
||||
if (element)
|
||||
clone.currentTime =
|
||||
(element.currentTime + 16) % element.duration;
|
||||
}
|
||||
|
||||
clones.push(clone);
|
||||
});
|
||||
|
||||
clones.forEach(clone => {
|
||||
if (clone.children !== undefined) {
|
||||
clone.children = clone.children.map(
|
||||
childId => cloneMap[childId]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
clones.forEach(clone => {
|
||||
if (selectedIds.includes(clone.parentId)) {
|
||||
clone.parentId = cloneMap[clone.parentId];
|
||||
}
|
||||
});
|
||||
|
||||
// Potentially confusing name here: these are the ids of the
|
||||
// original shapes that were cloned, not their clones' ids.
|
||||
const clonedShapeIds = new Set(Object.keys(cloneMap));
|
||||
|
||||
// Create cloned bindings for shapes where both to and from shapes are selected
|
||||
// (if the user clones, then we will create a new binding for the clones)
|
||||
Object.values(page.bindings)
|
||||
.filter(
|
||||
binding =>
|
||||
clonedShapeIds.has(binding.fromId) ||
|
||||
clonedShapeIds.has(binding.toId)
|
||||
)
|
||||
.forEach(binding => {
|
||||
if (clonedShapeIds.has(binding.fromId)) {
|
||||
if (clonedShapeIds.has(binding.toId)) {
|
||||
const cloneId = Utils.uniqueId();
|
||||
const cloneBinding = {
|
||||
...Utils.deepClone(binding),
|
||||
id: cloneId,
|
||||
fromId: cloneMap[binding.fromId] || binding.fromId,
|
||||
toId: cloneMap[binding.toId] || binding.toId,
|
||||
};
|
||||
clonedBindingsMap[binding.id] = cloneId;
|
||||
clonedBindings.push(cloneBinding);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Assign new binding ids to clones (or delete them!)
|
||||
clones.forEach(clone => {
|
||||
if (clone.handles) {
|
||||
if (clone.handles) {
|
||||
for (const id in clone.handles) {
|
||||
const handle =
|
||||
clone.handles[id as keyof ArrowShape['handles']];
|
||||
handle.bindingId = handle.bindingId
|
||||
? clonedBindingsMap[handle.bindingId]
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
clones.forEach(clone => {
|
||||
if (page.shapes[clone.id]) {
|
||||
throw Error("uh oh, we didn't clone correctly");
|
||||
}
|
||||
});
|
||||
|
||||
this.cloneInfo = {
|
||||
state: 'ready',
|
||||
clones,
|
||||
cloneMap,
|
||||
clonedBindings,
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user