init: the first public commit for AFFiNE

This commit is contained in:
DarkSky
2022-07-22 15:49:21 +08:00
commit e3e3741393
1451 changed files with 108124 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
import { Utils, TLPointerEventHandler } from '@tldraw/core';
import Vec from '@tldraw/vec';
import { Arrow } from '@toeverything/components/board-shapes';
import { SessionType, TDShapeType } from '@toeverything/components/board-types';
import { BaseTool, BaseToolStatus } from '@toeverything/components/board-state';
import { services } from '@toeverything/datasource/db-service';
export class ArrowTool extends BaseTool {
override type = TDShapeType.Arrow as const;
/* ----------------- Event Handlers ----------------- */
override onPointerDown: TLPointerEventHandler = () => {
if (this.status !== BaseToolStatus.Idle) return;
const {
currentPoint,
currentGrid,
settings: { showGrid },
appState: { currentPageId, currentStyle },
document: { id: workspace },
} = this.app;
const childIndex = this.getNextChildIndex();
const id = Utils.uniqueId();
const newShape = Arrow.create({
id,
parentId: currentPageId,
childIndex,
point: showGrid
? Vec.snap(currentPoint, currentGrid)
: currentPoint,
style: { ...currentStyle },
workspace,
});
this.app.patchCreate([newShape]);
this.app.startSession(SessionType.Arrow, newShape.id, 'end', true);
this.set_status(BaseToolStatus.Creating);
};
}

View File

@@ -0,0 +1 @@
export * from './arrow-tool';

View File

@@ -0,0 +1,86 @@
import { Utils, TLPointerEventHandler } from '@tldraw/core';
import { Draw } from '@toeverything/components/board-shapes';
import { SessionType, TDShapeType } from '@toeverything/components/board-types';
import { BaseTool } from '@toeverything/components/board-state';
enum Status {
Idle = 'idle',
Creating = 'creating',
Extending = 'extending',
Pinching = 'pinching',
}
export class DrawTool extends BaseTool {
override type = TDShapeType.Draw as const;
private last_shape_id?: string;
override onEnter = () => {
this.last_shape_id = undefined;
};
override onCancel = () => {
switch (this.status) {
case Status.Idle: {
this.app.selectTool('select');
break;
}
default: {
this.set_status(Status.Idle);
break;
}
}
this.app.cancelSession();
};
/* ----------------- Event Handlers ----------------- */
override onPointerDown: TLPointerEventHandler = info => {
if (this.status !== Status.Idle) return;
const {
currentPoint,
appState: { currentPageId, currentStyle },
document: { id: workspace },
} = this.app;
const previous =
this.last_shape_id && this.app.getShape(this.last_shape_id);
if (info.shiftKey && previous) {
// Extend the previous shape
this.app.startSession(SessionType.Draw, previous.id);
this.set_status(Status.Extending);
} else {
// Create a new shape
const childIndex = this.getNextChildIndex();
const id = Utils.uniqueId();
const newShape = Draw.create({
id,
parentId: currentPageId,
childIndex,
point: currentPoint,
style: { ...currentStyle },
workspace,
});
this.last_shape_id = id;
this.app.patchCreate([newShape]);
this.app.startSession(SessionType.Draw, id);
this.set_status(Status.Creating);
}
};
override onPointerMove: TLPointerEventHandler = () => {
switch (this.status) {
case Status.Extending:
case Status.Creating: {
this.app.updateSession();
}
}
};
override onPointerUp: TLPointerEventHandler = () => {
this.app.completeSession();
this.set_status(Status.Idle);
};
}

View File

@@ -0,0 +1 @@
export * from './draw-tool';

View File

@@ -0,0 +1,61 @@
/* eslint-disable no-restricted-syntax */
import { TLPointerEventHandler } from '@tldraw/core';
import Vec from '@tldraw/vec';
import { Editor } from '@toeverything/components/board-shapes';
import { TDShapeType } from '@toeverything/components/board-types';
import { BaseTool, BaseToolStatus } from '@toeverything/components/board-state';
import { services } from '@toeverything/datasource/db-service';
export class EditorTool extends BaseTool {
override type = TDShapeType.Editor as const;
/* ----------------- Event Handlers ----------------- */
// It is not allowed to drag and drop the size when it is created, it is directly treated as a click event, and the Group is created asynchronously
override onPointerDown: TLPointerEventHandler = () => {
if (this.status !== BaseToolStatus.Idle) return;
const {
currentPoint,
currentGrid,
settings: { showGrid },
appState: { currentPageId, currentStyle },
document: { id: workspace },
} = this.app;
const childIndex = this.getNextChildIndex();
const createGroup = async () => {
this.set_status(BaseToolStatus.Creating);
const group = await services.api.editorBlock.create({
workspace,
type: 'group',
parentId: currentPageId,
});
await services.api.editorBlock.create({
workspace,
type: 'text',
parentId: group.id,
});
const newShape = Editor.create({
id: group.id,
rootBlockId: group.id,
affineId: group.id,
parentId: currentPageId,
childIndex,
point: showGrid
? Vec.snap(currentPoint, currentGrid)
: currentPoint,
style: { ...currentStyle },
workspace,
});
// In order to make the cursor just positioned at the beginning of the first line, it needs to be adjusted according to the padding newShape.point = Vec.sub(newShape.point, [50, 30]);
this.app.patchCreate([newShape]);
this.app.select(group.id);
this.app.setEditingId(group.id);
this.set_status(BaseToolStatus.Idle);
this.app.selectTool('select');
};
createGroup();
};
}

View File

@@ -0,0 +1 @@
export * from './editor-tool';

View File

@@ -0,0 +1,49 @@
import { Utils, TLPointerEventHandler, TLBoundsCorner } from '@tldraw/core';
import Vec from '@tldraw/vec';
import { Ellipse } from '@toeverything/components/board-shapes';
import { SessionType, TDShapeType } from '@toeverything/components/board-types';
import { BaseTool, BaseToolStatus } from '@toeverything/components/board-state';
export class EllipseTool extends BaseTool {
override type = TDShapeType.Ellipse as const;
/* ----------------- Event Handlers ----------------- */
override onPointerDown: TLPointerEventHandler = () => {
if (this.status !== BaseToolStatus.Idle) return;
const {
currentPoint,
currentGrid,
settings: { showGrid },
appState: { currentPageId, currentStyle },
document: { id: workspace },
} = this.app;
const childIndex = this.getNextChildIndex();
const id = Utils.uniqueId();
const newShape = Ellipse.create({
id,
parentId: currentPageId,
childIndex,
point: showGrid
? Vec.snap(currentPoint, currentGrid)
: currentPoint,
style: { ...currentStyle },
workspace,
});
this.app.patchCreate([newShape]);
this.app.startSession(
SessionType.TransformSingle,
newShape.id,
TLBoundsCorner.BottomRight,
true
);
this.set_status(BaseToolStatus.Creating);
};
}

View File

@@ -0,0 +1 @@
export * from './ellipse-tool';

View File

@@ -0,0 +1,91 @@
import Vec from '@tldraw/vec';
import type { TLPointerEventHandler } from '@tldraw/core';
import { SessionType, DEAD_ZONE } from '@toeverything/components/board-types';
import { BaseTool } from '@toeverything/components/board-state';
enum Status {
Idle = 'idle',
Pointing = 'pointing',
Erasing = 'erasing',
}
export class EraseTool extends BaseTool {
override type = 'erase' as const;
override status: Status = Status.Idle;
/* ----------------- Event Handlers ----------------- */
override onPointerDown: TLPointerEventHandler = () => {
if (this.status !== Status.Idle) return;
this.set_status(Status.Pointing);
};
override onPointerMove: TLPointerEventHandler = info => {
switch (this.status) {
case Status.Pointing: {
if (Vec.dist(info.origin, info.point) > DEAD_ZONE) {
this.app.startSession(SessionType.Erase);
this.app.updateSession();
this.set_status(Status.Erasing);
}
break;
}
case Status.Erasing: {
this.app.updateSession();
}
}
};
override onPointerUp: TLPointerEventHandler = () => {
switch (this.status) {
case Status.Pointing: {
const shapeIdsAtPoint = this.app.shapes
.filter(shape => !shape.isLocked)
.filter(shape =>
this.app
.getShapeUtil(shape)
.hitTestPoint(shape, this.app.currentPoint)
)
.flatMap(shape =>
shape.children
? [shape.id, ...shape.children]
: shape.id
);
this.app.delete(shapeIdsAtPoint);
break;
}
case Status.Erasing: {
this.app.completeSession();
// Should the app go back to the previous state, the select
// state, or stay in the eraser state?
// if (this.previous) {
// this.app.selectTool(this.previous)
// } else {
// this.app.selectTool('select')
// }
}
}
this.set_status(Status.Idle);
};
override onCancel = () => {
if (this.status === Status.Idle) {
if (this.previous) {
this.app.selectTool(this.previous);
} else {
this.app.selectTool('select');
}
} else {
this.set_status(Status.Idle);
}
this.app.cancelSession();
};
}

View File

@@ -0,0 +1 @@
export * from './erase-tool';

View File

@@ -0,0 +1,52 @@
import {
Utils,
TLPointerEventHandler,
TLBoundsCorner,
TLBoundsHandleEventHandler,
} from '@tldraw/core';
import Vec from '@tldraw/vec';
import { Frame } from '@toeverything/components/board-shapes';
import { SessionType, TDShapeType } from '@toeverything/components/board-types';
import { BaseTool, BaseToolStatus } from '@toeverything/components/board-state';
export class FrameTool extends BaseTool {
override type = TDShapeType.Frame as const;
/* ----------------- Event Handlers ----------------- */
override onPointerDown: TLPointerEventHandler = () => {
if (this.status !== BaseToolStatus.Idle) return;
const {
currentPoint,
currentGrid,
settings: { showGrid },
appState: { currentPageId, currentStyle },
document: { id: workspace },
} = this.app;
const childIndex = 0;
const id = Utils.uniqueId();
const newShape = Frame.create({
id,
parentId: currentPageId,
childIndex,
point: showGrid
? Vec.snap(currentPoint, currentGrid)
: currentPoint,
style: { ...currentStyle },
workspace,
});
this.app.patchCreate([newShape]);
this.app.startSession(
SessionType.TransformSingle,
newShape.id,
TLBoundsCorner.BottomRight,
true
);
this.set_status(BaseToolStatus.Creating);
};
}

View File

@@ -0,0 +1,51 @@
import { TLPointerEventHandler } from '@tldraw/core';
// import { Draw } from '@toeverything/components/board-shapes';
import { Vec } from '@tldraw/vec';
import { TDShapeType } from '@toeverything/components/board-types';
import { BaseTool } from '@toeverything/components/board-state';
enum Status {
Idle = 'idle',
Pointing = 'pointing',
Draw = 'draw',
}
export class HandDrawTool extends BaseTool {
override type = TDShapeType.HandDraw as const;
override status: Status = Status.Idle;
/* ----------------- Event Handlers ----------------- */
override onPointerDown: TLPointerEventHandler = () => {
if (this.app.readOnly) return;
if (this.status !== Status.Idle) return;
this.set_status(Status.Pointing);
};
override onPointerMove: TLPointerEventHandler = (info, e) => {
if (this.app.readOnly) return;
const delta = Vec.div(info.delta, this.app.camera.zoom);
const prev = this.app.camera.point;
const next = Vec.sub(prev, delta);
if (Vec.isEqual(next, prev)) return;
switch (this.status) {
case Status.Pointing: {
this.app.pan(Vec.neg(delta));
break;
}
}
};
override onPointerUp: TLPointerEventHandler = () => {
this.set_status(Status.Idle);
};
override onCancel = () => {
this.set_status(Status.Idle);
};
}

View File

@@ -0,0 +1 @@
export * from './hand-draw-tool';

View File

@@ -0,0 +1,49 @@
import { Utils, TLPointerEventHandler, TLBoundsCorner } from '@tldraw/core';
import Vec from '@tldraw/vec';
import { Hexagon } from '@toeverything/components/board-shapes';
import { SessionType, TDShapeType } from '@toeverything/components/board-types';
import { BaseTool, BaseToolStatus } from '@toeverything/components/board-state';
export class HexagonTool extends BaseTool {
override type = TDShapeType.Triangle as const;
/* ----------------- Event Handlers ----------------- */
override onPointerDown: TLPointerEventHandler = () => {
if (this.status !== BaseToolStatus.Idle) return;
const {
currentPoint,
currentGrid,
settings: { showGrid },
appState: { currentPageId, currentStyle },
document: { id: workspace },
} = this.app;
const childIndex = this.getNextChildIndex();
const id = Utils.uniqueId();
const newShape = Hexagon.create({
id,
parentId: currentPageId,
childIndex,
point: showGrid
? Vec.snap(currentPoint, currentGrid)
: currentPoint,
style: { ...currentStyle },
workspace,
});
this.app.patchCreate([newShape]);
this.app.startSession(
SessionType.TransformSingle,
newShape.id,
TLBoundsCorner.BottomRight,
true
);
this.set_status(BaseToolStatus.Creating);
};
}

View File

@@ -0,0 +1 @@
export * from './hexagon-tool';

View File

@@ -0,0 +1,95 @@
import { Utils, TLPointerEventHandler } from '@tldraw/core';
import { Draw } from '@toeverything/components/board-shapes';
import {
SessionType,
TDShapeType,
DashStyle,
StrokeWidth,
} from '@toeverything/components/board-types';
import { BaseTool } from '@toeverything/components/board-state';
enum Status {
Idle = 'idle',
Creating = 'creating',
Extending = 'extending',
Pinching = 'pinching',
}
export class HighlightTool extends BaseTool {
override type = TDShapeType.Highlight as const;
private last_shape_id?: string;
override onEnter = () => {
this.last_shape_id = undefined;
};
override onCancel = () => {
switch (this.status) {
case Status.Idle: {
this.app.selectTool('select');
break;
}
default: {
this.set_status(Status.Idle);
break;
}
}
this.app.cancelSession();
};
/* ----------------- Event Handlers ----------------- */
override onPointerDown: TLPointerEventHandler = info => {
if (this.status !== Status.Idle) return;
const {
currentPoint,
appState: { currentPageId, currentStyle },
document: { id: workspace },
} = this.app;
const previous =
this.last_shape_id && this.app.getShape(this.last_shape_id);
if (info.shiftKey && previous) {
// Extend the previous shape
this.app.startSession(SessionType.Draw, previous.id);
this.set_status(Status.Extending);
} else {
// Create a new shape
const id = Utils.uniqueId();
const childIndex = this.getNextChildIndex();
const newShape = Draw.create({
id,
parentId: currentPageId,
childIndex,
point: currentPoint,
style: {
...currentStyle,
strokeWidth: StrokeWidth.s4,
dash: DashStyle.Solid,
},
workspace,
});
this.last_shape_id = id;
this.app.patchCreate([newShape]);
this.app.startSession(SessionType.Draw, id);
this.set_status(Status.Creating);
}
};
override onPointerMove: TLPointerEventHandler = () => {
switch (this.status) {
case Status.Extending:
case Status.Creating: {
this.app.updateSession();
}
}
};
override onPointerUp: TLPointerEventHandler = () => {
this.app.completeSession();
this.set_status(Status.Idle);
};
}

View File

@@ -0,0 +1 @@
export * from './highlight-tool';

View File

@@ -0,0 +1,64 @@
import { TDShapeType, TDToolType } from '@toeverything/components/board-types';
import { ArrowTool } from './arrow-tool';
import { LineTool } from './line-tool';
import { DrawTool } from './draw-tool';
import { PencilTool } from './pencil-tool';
import { HighlightTool } from './highlight-tool';
import { EllipseTool } from './ellipse-tool';
import { RectangleTool } from './rectangle-tool';
import { TriangleTool } from './triangle-tool';
import { SelectTool } from './select-tool';
import { EraseTool } from './erase-tool';
import { EditorTool } from './editor-tool';
import { HexagonTool } from './hexagon-tool';
import { PentagramTool } from './pentagram-tool';
import { WhiteArrowTool } from './white-arrow-tool';
import { LaserTool } from './laser-tool';
import { HandDrawTool } from './hand-draw';
import { FrameTool } from './frame-tool/frame-tool';
export interface ToolsMap {
select: typeof SelectTool;
erase: typeof EraseTool;
[TDShapeType.Draw]: typeof DrawTool;
[TDShapeType.Ellipse]: typeof EllipseTool;
[TDShapeType.Rectangle]: typeof RectangleTool;
[TDShapeType.Triangle]: typeof TriangleTool;
[TDShapeType.Hexagon]: typeof HexagonTool;
[TDShapeType.Pentagram]: typeof PentagramTool;
[TDShapeType.Line]: typeof LineTool;
[TDShapeType.Arrow]: typeof ArrowTool;
[TDShapeType.Pencil]: typeof PencilTool;
[TDShapeType.Highlight]: typeof HighlightTool;
[TDShapeType.Editor]: typeof EditorTool;
[TDShapeType.WhiteArrow]: typeof WhiteArrowTool;
[TDShapeType.HandDraw]: typeof HandDrawTool;
[TDShapeType.Laser]: typeof LaserTool;
[TDShapeType.Frame]: typeof FrameTool;
}
export type ToolOfType<K extends TDToolType> = ToolsMap[K];
export type ArgsOfType<K extends TDToolType> = ConstructorParameters<
ToolOfType<K>
>;
export const tools: { [K in TDToolType]: ToolsMap[K] } = {
select: SelectTool,
erase: EraseTool,
[TDShapeType.Draw]: DrawTool,
[TDShapeType.Pencil]: PencilTool,
[TDShapeType.Highlight]: HighlightTool,
[TDShapeType.Ellipse]: EllipseTool,
[TDShapeType.Rectangle]: RectangleTool,
[TDShapeType.Triangle]: TriangleTool,
[TDShapeType.Pentagram]: PentagramTool,
[TDShapeType.Line]: LineTool,
[TDShapeType.Arrow]: ArrowTool,
[TDShapeType.Editor]: EditorTool,
[TDShapeType.Hexagon]: HexagonTool,
[TDShapeType.WhiteArrow]: WhiteArrowTool,
[TDShapeType.Laser]: LaserTool,
[TDShapeType.HandDraw]: HandDrawTool,
[TDShapeType.Frame]: FrameTool,
};

View File

@@ -0,0 +1 @@
export * from './laser-tool';

View File

@@ -0,0 +1,69 @@
import { TLPointerEventHandler } from '@tldraw/core';
// import { Draw } from '@toeverything/components/board-shapes';
import { Vec } from '@tldraw/vec';
import { SessionType, TDShapeType } from '@toeverything/components/board-types';
import { BaseTool } from '@toeverything/components/board-state';
enum Status {
Idle = 'idle',
Pointing = 'pointing',
Laser = 'laser',
}
export class LaserTool extends BaseTool {
override type = TDShapeType.Laser as const;
override status: Status = Status.Idle;
/* ----------------- Event Handlers ----------------- */
override onPointerDown: TLPointerEventHandler = () => {
if (this.app.readOnly) return;
if (this.status !== Status.Idle) return;
this.set_status(Status.Pointing);
};
override onPointerMove: TLPointerEventHandler = info => {
if (this.app.readOnly) return;
switch (this.status) {
case Status.Pointing: {
if (Vec.dist(info.origin, info.point) > 3) {
this.app.startSession(SessionType.Laser);
this.app.updateSession();
this.set_status(Status.Laser);
}
break;
}
case Status.Laser: {
this.app.updateSession();
}
}
};
override onPointerUp: TLPointerEventHandler = () => {
if (this.app.readOnly) return;
switch (this.status) {
case Status.Laser: {
this.app.completeSession();
}
}
this.set_status(Status.Idle);
};
override onCancel = () => {
if (this.status === Status.Idle) {
if (this.previous) {
this.app.selectTool(this.previous);
} else {
this.app.selectTool('select');
}
} else {
this.set_status(Status.Idle);
}
this.app.cancelSession();
};
}

View File

@@ -0,0 +1 @@
export * from './line-tool';

View File

@@ -0,0 +1,47 @@
import { Utils, TLPointerEventHandler } from '@tldraw/core';
import Vec from '@tldraw/vec';
import { Arrow } from '@toeverything/components/board-shapes';
import { SessionType, TDShapeType } from '@toeverything/components/board-types';
import { BaseTool, BaseToolStatus } from '@toeverything/components/board-state';
export class LineTool extends BaseTool {
override type = TDShapeType.Line as const;
/* ----------------- Event Handlers ----------------- */
override onPointerDown: TLPointerEventHandler = () => {
if (this.status !== BaseToolStatus.Idle) return;
const {
currentPoint,
currentGrid,
settings: { showGrid },
appState: { currentPageId, currentStyle },
document: { id: workspace },
} = this.app;
const childIndex = this.getNextChildIndex();
const id = Utils.uniqueId();
const newShape = Arrow.create({
id,
parentId: currentPageId,
childIndex,
point: showGrid
? Vec.snap(currentPoint, currentGrid)
: currentPoint,
decorations: {
start: undefined,
end: undefined,
},
style: { ...currentStyle },
workspace,
});
this.app.patchCreate([newShape]);
this.app.startSession(SessionType.Arrow, newShape.id, 'end', true);
this.set_status(BaseToolStatus.Creating);
};
}

View File

@@ -0,0 +1 @@
export * from './pencil-tool';

View File

@@ -0,0 +1,90 @@
import { Utils, TLPointerEventHandler } from '@tldraw/core';
import { Draw } from '@toeverything/components/board-shapes';
import {
DashStyle,
SessionType,
TDShapeType,
} from '@toeverything/components/board-types';
import { BaseTool } from '@toeverything/components/board-state';
enum Status {
Idle = 'idle',
Creating = 'creating',
Extending = 'extending',
Pinching = 'pinching',
}
export class PencilTool extends BaseTool {
override type = TDShapeType.Pencil as const;
private last_shape_id?: string;
override onEnter = () => {
this.last_shape_id = undefined;
};
override onCancel = () => {
switch (this.status) {
case Status.Idle: {
this.app.selectTool('select');
break;
}
default: {
this.set_status(Status.Idle);
break;
}
}
this.app.cancelSession();
};
/* ----------------- Event Handlers ----------------- */
override onPointerDown: TLPointerEventHandler = info => {
if (this.status !== Status.Idle) return;
const {
currentPoint,
appState: { currentPageId, currentStyle },
document: { id: workspace },
} = this.app;
const previous =
this.last_shape_id && this.app.getShape(this.last_shape_id);
if (info.shiftKey && previous) {
// Extend the previous shape
this.app.startSession(SessionType.Draw, previous.id);
this.set_status(Status.Extending);
} else {
// Create a new shape
const id = Utils.uniqueId();
const childIndex = this.getNextChildIndex();
const newShape = Draw.create({
id,
parentId: currentPageId,
childIndex,
point: currentPoint,
style: { ...currentStyle, dash: DashStyle.Solid },
workspace,
});
this.last_shape_id = id;
this.app.patchCreate([newShape]);
this.app.startSession(SessionType.Draw, id);
this.set_status(Status.Creating);
}
};
override onPointerMove: TLPointerEventHandler = () => {
switch (this.status) {
case Status.Extending:
case Status.Creating: {
this.app.updateSession();
}
}
};
override onPointerUp: TLPointerEventHandler = () => {
this.app.completeSession();
this.set_status(Status.Idle);
};
}

View File

@@ -0,0 +1 @@
export * from './pentagram-tool';

View File

@@ -0,0 +1,49 @@
import { Utils, TLPointerEventHandler, TLBoundsCorner } from '@tldraw/core';
import Vec from '@tldraw/vec';
import { Pentagram } from '@toeverything/components/board-shapes';
import { SessionType, TDShapeType } from '@toeverything/components/board-types';
import { BaseTool, BaseToolStatus } from '@toeverything/components/board-state';
export class PentagramTool extends BaseTool {
override type = TDShapeType.Pentagram as const;
/* ----------------- Event Handlers ----------------- */
override onPointerDown: TLPointerEventHandler = () => {
if (this.status !== BaseToolStatus.Idle) return;
const {
currentPoint,
currentGrid,
settings: { showGrid },
appState: { currentPageId, currentStyle },
document: { id: workspace },
} = this.app;
const childIndex = this.getNextChildIndex();
const id = Utils.uniqueId();
const newShape = Pentagram.create({
id,
parentId: currentPageId,
childIndex,
point: showGrid
? Vec.snap(currentPoint, currentGrid)
: currentPoint,
style: { ...currentStyle },
workspace,
});
this.app.patchCreate([newShape]);
this.app.startSession(
SessionType.TransformSingle,
newShape.id,
TLBoundsCorner.BottomRight,
true
);
this.set_status(BaseToolStatus.Creating);
};
}

View File

@@ -0,0 +1 @@
export * from './rectangle-tool';

View File

@@ -0,0 +1,49 @@
import { Utils, TLPointerEventHandler, TLBoundsCorner } from '@tldraw/core';
import Vec from '@tldraw/vec';
import { Rectangle } from '@toeverything/components/board-shapes';
import { SessionType, TDShapeType } from '@toeverything/components/board-types';
import { BaseTool, BaseToolStatus } from '@toeverything/components/board-state';
export class RectangleTool extends BaseTool {
override type = TDShapeType.Rectangle as const;
/* ----------------- Event Handlers ----------------- */
override onPointerDown: TLPointerEventHandler = () => {
if (this.status !== BaseToolStatus.Idle) return;
const {
currentPoint,
currentGrid,
settings: { showGrid },
appState: { currentPageId, currentStyle },
document: { id: workspace },
} = this.app;
const childIndex = this.getNextChildIndex();
const id = Utils.uniqueId();
const newShape = Rectangle.create({
id,
parentId: currentPageId,
childIndex,
point: showGrid
? Vec.snap(currentPoint, currentGrid)
: currentPoint,
style: { ...currentStyle },
workspace,
});
this.app.patchCreate([newShape]);
this.app.startSession(
SessionType.TransformSingle,
newShape.id,
TLBoundsCorner.BottomRight,
true
);
this.set_status(BaseToolStatus.Creating);
};
}

View File

@@ -0,0 +1 @@
export * from './select-tool';

View File

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

View File

@@ -0,0 +1 @@
export * from './triangle-tool';

View File

@@ -0,0 +1,49 @@
import { Utils, TLPointerEventHandler, TLBoundsCorner } from '@tldraw/core';
import Vec from '@tldraw/vec';
import { Triangle } from '@toeverything/components/board-shapes';
import { SessionType, TDShapeType } from '@toeverything/components/board-types';
import { BaseTool, BaseToolStatus } from '@toeverything/components/board-state';
export class TriangleTool extends BaseTool {
override type = TDShapeType.Triangle as const;
/* ----------------- Event Handlers ----------------- */
override onPointerDown: TLPointerEventHandler = () => {
if (this.status !== BaseToolStatus.Idle) return;
const {
currentPoint,
currentGrid,
settings: { showGrid },
appState: { currentPageId, currentStyle },
document: { id: workspace },
} = this.app;
const childIndex = this.getNextChildIndex();
const id = Utils.uniqueId();
const newShape = Triangle.create({
id,
parentId: currentPageId,
childIndex,
point: showGrid
? Vec.snap(currentPoint, currentGrid)
: currentPoint,
style: { ...currentStyle },
workspace,
});
this.app.patchCreate([newShape]);
this.app.startSession(
SessionType.TransformSingle,
newShape.id,
TLBoundsCorner.BottomRight,
true
);
this.set_status(BaseToolStatus.Creating);
};
}

View File

@@ -0,0 +1 @@
export * from './white-arrow-tool';

View File

@@ -0,0 +1,49 @@
import { Utils, TLPointerEventHandler, TLBoundsCorner } from '@tldraw/core';
import Vec from '@tldraw/vec';
import { WhiteArrow } from '@toeverything/components/board-shapes';
import { SessionType, TDShapeType } from '@toeverything/components/board-types';
import { BaseTool, BaseToolStatus } from '@toeverything/components/board-state';
export class WhiteArrowTool extends BaseTool {
override type = TDShapeType.Triangle as const;
/* ----------------- Event Handlers ----------------- */
override onPointerDown: TLPointerEventHandler = () => {
if (this.status !== BaseToolStatus.Idle) return;
const {
currentPoint,
currentGrid,
settings: { showGrid },
appState: { currentPageId, currentStyle },
document: { id: workspace },
} = this.app;
const childIndex = this.getNextChildIndex();
const id = Utils.uniqueId();
const newShape = WhiteArrow.create({
id,
parentId: currentPageId,
childIndex,
point: showGrid
? Vec.snap(currentPoint, currentGrid)
: currentPoint,
style: { ...currentStyle },
workspace,
});
this.app.patchCreate([newShape]);
this.app.startSession(
SessionType.TransformSingle,
newShape.id,
TLBoundsCorner.BottomRight,
true
);
this.set_status(BaseToolStatus.Creating);
};
}