mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-12 15:46:29 +08:00
306 lines
10 KiB
TypeScript
306 lines
10 KiB
TypeScript
import { Utils } from '@tldraw/core';
|
|
import { Vec } from '@tldraw/vec';
|
|
import type { TldrawApp } from '@toeverything/components/board-state';
|
|
import {
|
|
DrawShape,
|
|
SessionType,
|
|
TDStatus,
|
|
TldrawCommand,
|
|
TldrawPatch,
|
|
} from '@toeverything/components/board-types';
|
|
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,
|
|
};
|
|
};
|
|
}
|