mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
init: the first public commit for AFFiNE
This commit is contained in:
126
libs/components/board-commands/src/align-shapes.ts
Normal file
126
libs/components/board-commands/src/align-shapes.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { Vec } from '@tldraw/vec';
|
||||
import { Utils } from '@tldraw/core';
|
||||
import {
|
||||
AlignType,
|
||||
TldrawCommand,
|
||||
TDShapeType,
|
||||
} from '@toeverything/components/board-types';
|
||||
import { TLDR } from '@toeverything/components/board-state';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
|
||||
export function alignShapes(
|
||||
app: TldrawApp,
|
||||
ids: string[],
|
||||
type: AlignType
|
||||
): TldrawCommand {
|
||||
const { currentPageId } = app;
|
||||
|
||||
const initialShapes = ids.map(id => app.getShape(id));
|
||||
|
||||
const boundsForShapes = initialShapes.map(shape => {
|
||||
return {
|
||||
id: shape.id,
|
||||
point: [...shape.point],
|
||||
bounds: TLDR.get_bounds(shape),
|
||||
};
|
||||
});
|
||||
|
||||
const commonBounds = Utils.getCommonBounds(
|
||||
boundsForShapes.map(({ bounds }) => bounds)
|
||||
);
|
||||
|
||||
const midX = commonBounds.minX + commonBounds.width / 2;
|
||||
const midY = commonBounds.minY + commonBounds.height / 2;
|
||||
|
||||
const deltaMap = Object.fromEntries(
|
||||
boundsForShapes.map(({ id, point, bounds }) => {
|
||||
return [
|
||||
id,
|
||||
{
|
||||
prev: point,
|
||||
next: {
|
||||
[AlignType.CenterVertical]: [
|
||||
point[0],
|
||||
midY - bounds.height / 2,
|
||||
],
|
||||
[AlignType.CenterHorizontal]: [
|
||||
midX - bounds.width / 2,
|
||||
point[1],
|
||||
],
|
||||
[AlignType.Top]: [point[0], commonBounds.minY],
|
||||
[AlignType.Bottom]: [
|
||||
point[0],
|
||||
commonBounds.maxY - bounds.height,
|
||||
],
|
||||
[AlignType.Left]: [commonBounds.minX, point[1]],
|
||||
[AlignType.Right]: [
|
||||
commonBounds.maxX - bounds.width,
|
||||
point[1],
|
||||
],
|
||||
}[type],
|
||||
},
|
||||
];
|
||||
})
|
||||
);
|
||||
|
||||
const { before, after } = TLDR.mutate_shapes(
|
||||
app.state,
|
||||
ids,
|
||||
shape => {
|
||||
if (!deltaMap[shape.id]) return shape;
|
||||
return { point: deltaMap[shape.id].next };
|
||||
},
|
||||
currentPageId
|
||||
);
|
||||
|
||||
initialShapes.forEach(shape => {
|
||||
if (shape.type === TDShapeType.Group) {
|
||||
const delta = Vec.sub(
|
||||
after[shape.id].point!,
|
||||
before[shape.id].point!
|
||||
);
|
||||
|
||||
shape.children.forEach(id => {
|
||||
const child = app.getShape(id);
|
||||
before[child.id] = { point: child.point };
|
||||
after[child.id] = { point: Vec.add(child.point, delta) };
|
||||
});
|
||||
|
||||
delete before[shape.id];
|
||||
delete after[shape.id];
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
id: 'align',
|
||||
before: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: before,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: ids,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: after,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: ids,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
18
libs/components/board-commands/src/change-page.ts
Normal file
18
libs/components/board-commands/src/change-page.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { TldrawCommand } from '@toeverything/components/board-types';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
|
||||
export function changePage(app: TldrawApp, pageId: string): TldrawCommand {
|
||||
return {
|
||||
id: 'change_page',
|
||||
before: {
|
||||
appState: {
|
||||
currentPageId: app.currentPageId,
|
||||
},
|
||||
},
|
||||
after: {
|
||||
appState: {
|
||||
currentPageId: pageId,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
71
libs/components/board-commands/src/create-page.ts
Normal file
71
libs/components/board-commands/src/create-page.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import type {
|
||||
TldrawCommand,
|
||||
TDPage,
|
||||
} from '@toeverything/components/board-types';
|
||||
import { Utils, TLPageState } from '@tldraw/core';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
|
||||
export function createPage(
|
||||
app: TldrawApp,
|
||||
center: number[],
|
||||
pageId = Utils.uniqueId()
|
||||
): TldrawCommand {
|
||||
const { currentPageId } = app;
|
||||
|
||||
const topPage = Object.values(app.state.document.pages).sort(
|
||||
(a, b) => (b.childIndex || 0) - (a.childIndex || 0)
|
||||
)[0];
|
||||
|
||||
const nextChildIndex = topPage?.childIndex ? topPage?.childIndex + 1 : 1;
|
||||
|
||||
// TODO: Iterate the name better
|
||||
const nextName = `New Page`;
|
||||
|
||||
const page: TDPage = {
|
||||
id: pageId,
|
||||
name: nextName,
|
||||
childIndex: nextChildIndex,
|
||||
shapes: {},
|
||||
bindings: {},
|
||||
};
|
||||
|
||||
const pageState: TLPageState = {
|
||||
id: pageId,
|
||||
selectedIds: [],
|
||||
camera: { point: center, zoom: 1 },
|
||||
editingId: undefined,
|
||||
bindingId: undefined,
|
||||
hoveredId: undefined,
|
||||
pointedId: undefined,
|
||||
};
|
||||
|
||||
return {
|
||||
id: 'create_page',
|
||||
before: {
|
||||
appState: {
|
||||
currentPageId,
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[pageId]: undefined,
|
||||
},
|
||||
pageStates: {
|
||||
[pageId]: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
appState: {
|
||||
currentPageId: page.id,
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[pageId]: page,
|
||||
},
|
||||
pageStates: {
|
||||
[pageId]: pageState,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
65
libs/components/board-commands/src/create-shapes.ts
Normal file
65
libs/components/board-commands/src/create-shapes.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import type {
|
||||
Patch,
|
||||
TDShape,
|
||||
TldrawCommand,
|
||||
TDBinding,
|
||||
} from '@toeverything/components/board-types';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
|
||||
export function createShapes(
|
||||
app: TldrawApp,
|
||||
shapes: TDShape[],
|
||||
bindings: TDBinding[] = []
|
||||
): TldrawCommand {
|
||||
const { currentPageId } = app;
|
||||
|
||||
const beforeShapes: Record<string, Patch<TDShape> | undefined> = {};
|
||||
const afterShapes: Record<string, Patch<TDShape> | undefined> = {};
|
||||
|
||||
shapes.forEach(shape => {
|
||||
beforeShapes[shape.id] = undefined;
|
||||
afterShapes[shape.id] = shape;
|
||||
});
|
||||
|
||||
const beforeBindings: Record<string, Patch<TDBinding> | undefined> = {};
|
||||
const afterBindings: Record<string, Patch<TDBinding> | undefined> = {};
|
||||
|
||||
bindings.forEach(binding => {
|
||||
beforeBindings[binding.id] = undefined;
|
||||
afterBindings[binding.id] = binding;
|
||||
});
|
||||
|
||||
return {
|
||||
id: 'create',
|
||||
before: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: beforeShapes,
|
||||
bindings: beforeBindings,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: [...app.selectedIds],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: afterShapes,
|
||||
bindings: afterBindings,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: shapes.map(shape => shape.id),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
57
libs/components/board-commands/src/delete-page.ts
Normal file
57
libs/components/board-commands/src/delete-page.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { TldrawCommand } from '@toeverything/components/board-types';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
|
||||
export function deletePage(app: TldrawApp, pageId: string): TldrawCommand {
|
||||
const {
|
||||
currentPageId,
|
||||
document: { pages, pageStates },
|
||||
} = app;
|
||||
|
||||
const pagesArr = Object.values(pages).sort(
|
||||
(a, b) => (a.childIndex || 0) - (b.childIndex || 0)
|
||||
);
|
||||
|
||||
const currentIndex = pagesArr.findIndex(page => page.id === pageId);
|
||||
|
||||
let nextCurrentPageId: string;
|
||||
|
||||
if (pageId === currentPageId) {
|
||||
if (currentIndex === pagesArr.length - 1) {
|
||||
nextCurrentPageId = pagesArr[pagesArr.length - 2].id;
|
||||
} else {
|
||||
nextCurrentPageId = pagesArr[currentIndex + 1].id;
|
||||
}
|
||||
} else {
|
||||
nextCurrentPageId = currentPageId;
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'delete_page',
|
||||
before: {
|
||||
appState: {
|
||||
currentPageId: pageId,
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[pageId]: { ...pages[pageId] },
|
||||
},
|
||||
pageStates: {
|
||||
[pageId]: { ...pageStates[pageId] },
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
appState: {
|
||||
currentPageId: nextCurrentPageId,
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[pageId]: undefined,
|
||||
},
|
||||
pageStates: {
|
||||
[pageId]: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
66
libs/components/board-commands/src/delete-shapes.ts
Normal file
66
libs/components/board-commands/src/delete-shapes.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import type {
|
||||
TDAsset,
|
||||
TDAssets,
|
||||
TldrawCommand,
|
||||
} from '@toeverything/components/board-types';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
import { removeShapesFromPage } from './shared/remove-shapes-from-page';
|
||||
|
||||
const removeAssetsFromDocument = (assets: TDAssets, idsToRemove: string[]) => {
|
||||
const afterAssets: Record<string, TDAsset | undefined> = { ...assets };
|
||||
idsToRemove.forEach(id => (afterAssets[id] = undefined));
|
||||
return afterAssets;
|
||||
};
|
||||
|
||||
export function deleteShapes(
|
||||
app: TldrawApp,
|
||||
ids: string[],
|
||||
pageId = app.currentPageId
|
||||
): TldrawCommand {
|
||||
const {
|
||||
pageState,
|
||||
selectedIds,
|
||||
document: { assets: beforeAssets },
|
||||
} = app;
|
||||
const { before, after, assetsToRemove } = removeShapesFromPage(
|
||||
app.state,
|
||||
ids,
|
||||
pageId
|
||||
);
|
||||
const afterAssets = removeAssetsFromDocument(beforeAssets, assetsToRemove);
|
||||
|
||||
return {
|
||||
id: 'delete',
|
||||
before: {
|
||||
document: {
|
||||
assets: beforeAssets,
|
||||
pages: {
|
||||
[pageId]: before,
|
||||
},
|
||||
pageStates: {
|
||||
[pageId]: { selectedIds: [...app.selectedIds] },
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
document: {
|
||||
assets: afterAssets,
|
||||
pages: {
|
||||
[pageId]: after,
|
||||
},
|
||||
pageStates: {
|
||||
[pageId]: {
|
||||
selectedIds: selectedIds.filter(
|
||||
id => !ids.includes(id)
|
||||
),
|
||||
hoveredId:
|
||||
pageState.hoveredId &&
|
||||
ids.includes(pageState.hoveredId)
|
||||
? undefined
|
||||
: pageState.hoveredId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
187
libs/components/board-commands/src/distribute-shapes.ts
Normal file
187
libs/components/board-commands/src/distribute-shapes.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { Utils } from '@tldraw/core';
|
||||
import {
|
||||
DistributeType,
|
||||
TDShape,
|
||||
TldrawCommand,
|
||||
TDShapeType,
|
||||
} from '@toeverything/components/board-types';
|
||||
import { TLDR } from '@toeverything/components/board-state';
|
||||
import Vec from '@tldraw/vec';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
|
||||
export function distributeShapes(
|
||||
app: TldrawApp,
|
||||
ids: string[],
|
||||
type: DistributeType
|
||||
): TldrawCommand {
|
||||
const { currentPageId } = app;
|
||||
|
||||
const initialShapes = ids.map(id => app.getShape(id));
|
||||
|
||||
const deltaMap = Object.fromEntries(
|
||||
getDistributions(initialShapes, type).map(d => [d.id, d])
|
||||
);
|
||||
|
||||
const { before, after } = TLDR.mutate_shapes(
|
||||
app.state,
|
||||
ids.filter(id => deltaMap[id] !== undefined),
|
||||
shape => ({ point: deltaMap[shape.id].next }),
|
||||
currentPageId
|
||||
);
|
||||
|
||||
initialShapes.forEach(shape => {
|
||||
if (shape.type === TDShapeType.Group) {
|
||||
const delta = Vec.sub(
|
||||
after[shape.id].point!,
|
||||
before[shape.id].point!
|
||||
);
|
||||
|
||||
shape.children.forEach(id => {
|
||||
const child = app.getShape(id);
|
||||
before[child.id] = { point: child.point };
|
||||
after[child.id] = { point: Vec.add(child.point, delta) };
|
||||
});
|
||||
|
||||
delete before[shape.id];
|
||||
delete after[shape.id];
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
id: 'distribute',
|
||||
before: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: { shapes: before },
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: ids,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: { shapes: after },
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: ids,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getDistributions(initialShapes: TDShape[], type: DistributeType) {
|
||||
const entries = initialShapes.map(shape => {
|
||||
const utils = TLDR.get_shape_util(shape);
|
||||
return {
|
||||
id: shape.id,
|
||||
point: [...shape.point],
|
||||
bounds: utils.getBounds(shape),
|
||||
center: utils.getCenter(shape),
|
||||
};
|
||||
});
|
||||
|
||||
const len = entries.length;
|
||||
const commonBounds = Utils.getCommonBounds(
|
||||
entries.map(({ bounds }) => bounds)
|
||||
);
|
||||
|
||||
const results: { id: string; prev: number[]; next: number[] }[] = [];
|
||||
|
||||
switch (type) {
|
||||
case DistributeType.Horizontal: {
|
||||
const span = entries.reduce((a, c) => a + c.bounds.width, 0);
|
||||
|
||||
if (span > commonBounds.width) {
|
||||
const left = entries.sort(
|
||||
(a, b) => a.bounds.minX - b.bounds.minX
|
||||
)[0];
|
||||
|
||||
const right = entries.sort(
|
||||
(a, b) => b.bounds.maxX - a.bounds.maxX
|
||||
)[0];
|
||||
|
||||
const entriesToMove = entries
|
||||
.filter(a => a !== left && a !== right)
|
||||
.sort((a, b) => a.center[0] - b.center[0]);
|
||||
|
||||
const step = (right.center[0] - left.center[0]) / (len - 1);
|
||||
|
||||
const x = left.center[0] + step;
|
||||
|
||||
entriesToMove.forEach(({ id, point, bounds }, i) => {
|
||||
results.push({
|
||||
id,
|
||||
prev: point,
|
||||
next: [x + step * i - bounds.width / 2, bounds.minY],
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const entriesToMove = entries.sort(
|
||||
(a, b) => a.center[0] - b.center[0]
|
||||
);
|
||||
|
||||
let x = commonBounds.minX;
|
||||
const step = (commonBounds.width - span) / (len - 1);
|
||||
|
||||
entriesToMove.forEach(({ id, point, bounds }) => {
|
||||
results.push({ id, prev: point, next: [x, bounds.minY] });
|
||||
x += bounds.width + step;
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case DistributeType.Vertical: {
|
||||
const span = entries.reduce((a, c) => a + c.bounds.height, 0);
|
||||
|
||||
if (span > commonBounds.height) {
|
||||
const top = entries.sort(
|
||||
(a, b) => a.bounds.minY - b.bounds.minY
|
||||
)[0];
|
||||
|
||||
const bottom = entries.sort(
|
||||
(a, b) => b.bounds.maxY - a.bounds.maxY
|
||||
)[0];
|
||||
|
||||
const entriesToMove = entries
|
||||
.filter(a => a !== top && a !== bottom)
|
||||
.sort((a, b) => a.center[1] - b.center[1]);
|
||||
|
||||
const step = (bottom.center[1] - top.center[1]) / (len - 1);
|
||||
|
||||
const y = top.center[1] + step;
|
||||
|
||||
entriesToMove.forEach(({ id, point, bounds }, i) => {
|
||||
results.push({
|
||||
id,
|
||||
prev: point,
|
||||
next: [bounds.minX, y + step * i - bounds.height / 2],
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const entriesToMove = entries.sort(
|
||||
(a, b) => a.center[1] - b.center[1]
|
||||
);
|
||||
|
||||
let y = commonBounds.minY;
|
||||
const step = (commonBounds.height - span) / (len - 1);
|
||||
|
||||
entriesToMove.forEach(({ id, point, bounds }) => {
|
||||
results.push({ id, prev: point, next: [bounds.minX, y] });
|
||||
y += bounds.height + step;
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
69
libs/components/board-commands/src/duplicate-page.ts
Normal file
69
libs/components/board-commands/src/duplicate-page.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { TldrawCommand } from '@toeverything/components/board-types';
|
||||
import { Utils } from '@tldraw/core';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
|
||||
export function duplicatePage(app: TldrawApp, pageId: string): TldrawCommand {
|
||||
const newId = Utils.uniqueId();
|
||||
const {
|
||||
currentPageId,
|
||||
page,
|
||||
pageState: { camera },
|
||||
} = app;
|
||||
|
||||
const nextPage = {
|
||||
...page,
|
||||
id: newId,
|
||||
name: page.name + ' Copy',
|
||||
shapes: Object.fromEntries(
|
||||
Object.entries(page.shapes).map(([id, shape]) => {
|
||||
return [
|
||||
id,
|
||||
{
|
||||
...shape,
|
||||
parentId:
|
||||
shape.parentId === pageId ? newId : shape.parentId,
|
||||
},
|
||||
];
|
||||
})
|
||||
),
|
||||
};
|
||||
|
||||
return {
|
||||
id: 'duplicate_page',
|
||||
before: {
|
||||
appState: {
|
||||
currentPageId,
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[newId]: undefined,
|
||||
},
|
||||
pageStates: {
|
||||
[newId]: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
appState: {
|
||||
currentPageId: newId,
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[newId]: nextPage,
|
||||
},
|
||||
pageStates: {
|
||||
[newId]: {
|
||||
...page,
|
||||
id: newId,
|
||||
selectedIds: [],
|
||||
camera: { ...camera },
|
||||
editingId: undefined,
|
||||
bindingId: undefined,
|
||||
hoveredId: undefined,
|
||||
pointedId: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
206
libs/components/board-commands/src/duplicate-shapes.ts
Normal file
206
libs/components/board-commands/src/duplicate-shapes.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { Utils } from '@tldraw/core';
|
||||
import { Vec } from '@tldraw/vec';
|
||||
import { TLDR } from '@toeverything/components/board-state';
|
||||
import type {
|
||||
PagePartial,
|
||||
TldrawCommand,
|
||||
TDShape,
|
||||
} from '@toeverything/components/board-types';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
|
||||
export function duplicateShapes(
|
||||
app: TldrawApp,
|
||||
ids: string[],
|
||||
point?: number[]
|
||||
): TldrawCommand {
|
||||
const { selectedIds, currentPageId, page, shapes } = app;
|
||||
|
||||
const before: PagePartial = {
|
||||
shapes: {},
|
||||
bindings: {},
|
||||
};
|
||||
|
||||
const after: PagePartial = {
|
||||
shapes: {},
|
||||
bindings: {},
|
||||
};
|
||||
|
||||
const duplicateMap: Record<string, string> = {};
|
||||
|
||||
const shapesToDuplicate = ids
|
||||
.map(id => app.getShape(id))
|
||||
.filter(shape => !ids.includes(shape.parentId));
|
||||
|
||||
// Create duplicates
|
||||
shapesToDuplicate.forEach(shape => {
|
||||
const duplicatedId = Utils.uniqueId();
|
||||
before.shapes[duplicatedId] = undefined;
|
||||
|
||||
after.shapes[duplicatedId] = {
|
||||
...Utils.deepClone(shape),
|
||||
id: duplicatedId,
|
||||
childIndex: TLDR.get_child_index_above(
|
||||
app.state,
|
||||
shape.id,
|
||||
currentPageId
|
||||
),
|
||||
};
|
||||
|
||||
if (shape.children) {
|
||||
after.shapes[duplicatedId]!.children = [];
|
||||
}
|
||||
|
||||
if (shape.parentId !== currentPageId) {
|
||||
const parent = app.getShape(shape.parentId);
|
||||
|
||||
before.shapes[parent.id] = {
|
||||
...before.shapes[parent.id],
|
||||
children: parent.children,
|
||||
};
|
||||
|
||||
after.shapes[parent.id] = {
|
||||
...after.shapes[parent.id],
|
||||
children: [
|
||||
...(after.shapes[parent.id] || parent).children!,
|
||||
duplicatedId,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
duplicateMap[shape.id] = duplicatedId;
|
||||
});
|
||||
|
||||
// If the shapes have children, then duplicate those too
|
||||
shapesToDuplicate.forEach(shape => {
|
||||
if (shape.children) {
|
||||
shape.children.forEach(childId => {
|
||||
const child = app.getShape(childId);
|
||||
const duplicatedId = Utils.uniqueId();
|
||||
const duplicatedParentId = duplicateMap[shape.id];
|
||||
before.shapes[duplicatedId] = undefined;
|
||||
after.shapes[duplicatedId] = {
|
||||
...Utils.deepClone(child),
|
||||
id: duplicatedId,
|
||||
parentId: duplicatedParentId,
|
||||
childIndex: TLDR.get_child_index_above(
|
||||
app.state,
|
||||
child.id,
|
||||
currentPageId
|
||||
),
|
||||
};
|
||||
duplicateMap[childId] = duplicatedId;
|
||||
after.shapes[duplicateMap[shape.id]]?.children?.push(
|
||||
duplicatedId
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Which ids did we end up duplicating?
|
||||
const dupedShapeIds = new Set(Object.keys(duplicateMap));
|
||||
|
||||
// Handle bindings that effect duplicated shapes
|
||||
Object.values(page.bindings)
|
||||
.filter(
|
||||
binding =>
|
||||
dupedShapeIds.has(binding.fromId) ||
|
||||
dupedShapeIds.has(binding.toId)
|
||||
)
|
||||
.forEach(binding => {
|
||||
if (dupedShapeIds.has(binding.fromId)) {
|
||||
if (dupedShapeIds.has(binding.toId)) {
|
||||
// If the binding is between two duplicating shapes then
|
||||
// duplicate the binding, too
|
||||
const duplicatedBindingId = Utils.uniqueId();
|
||||
|
||||
const duplicatedBinding = {
|
||||
...Utils.deepClone(binding),
|
||||
id: duplicatedBindingId,
|
||||
fromId: duplicateMap[binding.fromId],
|
||||
toId: duplicateMap[binding.toId],
|
||||
};
|
||||
|
||||
before.bindings[duplicatedBindingId] = undefined;
|
||||
after.bindings[duplicatedBindingId] = duplicatedBinding;
|
||||
|
||||
// Change the duplicated shape's handle so that it reference
|
||||
// the duplicated binding
|
||||
const boundShape = after.shapes[duplicatedBinding.fromId];
|
||||
Object.values(boundShape!.handles!).forEach(handle => {
|
||||
if (handle!.bindingId === binding.id) {
|
||||
handle!.bindingId = duplicatedBindingId;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// If only the fromId is selected, delete the binding on
|
||||
// the duplicated shape's handles
|
||||
const boundShape =
|
||||
after.shapes[duplicateMap[binding.fromId]];
|
||||
Object.values(boundShape!.handles!).forEach(handle => {
|
||||
if (handle!.bindingId === binding.id) {
|
||||
handle!.bindingId = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Now move the shapes
|
||||
|
||||
const shapesToMove = Object.values(after.shapes) as TDShape[];
|
||||
|
||||
if (point) {
|
||||
const commonBounds = Utils.getCommonBounds(
|
||||
shapesToMove.map(shape => TLDR.get_bounds(shape))
|
||||
);
|
||||
const center = Utils.getBoundsCenter(commonBounds);
|
||||
shapesToMove.forEach(shape => {
|
||||
// Could be a group
|
||||
if (!shape.point) return;
|
||||
shape.point = Vec.sub(point, Vec.sub(center, shape.point));
|
||||
});
|
||||
} else {
|
||||
const offset = [16, 16];
|
||||
shapesToMove.forEach(shape => {
|
||||
// Could be a group
|
||||
if (!shape.point) return;
|
||||
shape.point = Vec.add(shape.point, offset);
|
||||
});
|
||||
}
|
||||
|
||||
// Unlock any locked shapes
|
||||
shapesToMove.forEach(shape => {
|
||||
if (shape.isLocked) {
|
||||
shape.isLocked = false;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
id: 'duplicate',
|
||||
before: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: before,
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: { selectedIds },
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: after,
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: Array.from(dupedShapeIds.values()).map(
|
||||
id => duplicateMap[id]
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
101
libs/components/board-commands/src/flip-shapes.ts
Normal file
101
libs/components/board-commands/src/flip-shapes.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { FlipType } from '@toeverything/components/board-types';
|
||||
import { TLBoundsCorner, Utils } from '@tldraw/core';
|
||||
import type { TldrawCommand } from '@toeverything/components/board-types';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
import { TLDR } from '@toeverything/components/board-state';
|
||||
|
||||
export function flipShapes(
|
||||
app: TldrawApp,
|
||||
ids: string[],
|
||||
type: FlipType
|
||||
): TldrawCommand {
|
||||
const { selectedIds, currentPageId, shapes } = app;
|
||||
|
||||
const boundsForShapes = shapes.map(shape => TLDR.get_bounds(shape));
|
||||
|
||||
const commonBounds = Utils.getCommonBounds(boundsForShapes);
|
||||
|
||||
const { before, after } = TLDR.mutate_shapes(
|
||||
app.state,
|
||||
ids,
|
||||
shape => {
|
||||
const shapeBounds = TLDR.get_bounds(shape);
|
||||
|
||||
switch (type) {
|
||||
case FlipType.Horizontal: {
|
||||
const newShapeBounds =
|
||||
Utils.getRelativeTransformedBoundingBox(
|
||||
commonBounds,
|
||||
commonBounds,
|
||||
shapeBounds,
|
||||
true,
|
||||
false
|
||||
);
|
||||
|
||||
return TLDR.get_shape_util(shape).transform(
|
||||
shape,
|
||||
newShapeBounds,
|
||||
{
|
||||
type: TLBoundsCorner.TopLeft,
|
||||
scaleX: -1,
|
||||
scaleY: 1,
|
||||
initialShape: shape,
|
||||
transformOrigin: [0.5, 0.5],
|
||||
}
|
||||
);
|
||||
}
|
||||
case FlipType.Vertical: {
|
||||
const newShapeBounds =
|
||||
Utils.getRelativeTransformedBoundingBox(
|
||||
commonBounds,
|
||||
commonBounds,
|
||||
shapeBounds,
|
||||
false,
|
||||
true
|
||||
);
|
||||
|
||||
return TLDR.get_shape_util(shape).transform(
|
||||
shape,
|
||||
newShapeBounds,
|
||||
{
|
||||
type: TLBoundsCorner.TopLeft,
|
||||
scaleX: 1,
|
||||
scaleY: -1,
|
||||
initialShape: shape,
|
||||
transformOrigin: [0.5, 0.5],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
currentPageId
|
||||
);
|
||||
|
||||
return {
|
||||
id: 'flip',
|
||||
before: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: { shapes: before },
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: { shapes: after },
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: ids,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
254
libs/components/board-commands/src/group-shapes.ts
Normal file
254
libs/components/board-commands/src/group-shapes.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import { TDShape, TDShapeType } from '@toeverything/components/board-types';
|
||||
import { Utils } from '@tldraw/core';
|
||||
import type {
|
||||
Patch,
|
||||
TldrawCommand,
|
||||
TDBinding,
|
||||
} from '@toeverything/components/board-types';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
import { TLDR } from '@toeverything/components/board-state';
|
||||
|
||||
export function groupShapes(
|
||||
app: TldrawApp,
|
||||
ids: string[],
|
||||
groupId: string,
|
||||
pageId: string
|
||||
): TldrawCommand | undefined {
|
||||
if (ids.length < 2) return;
|
||||
|
||||
const beforeShapes: Record<string, Patch<TDShape | undefined>> = {};
|
||||
const afterShapes: Record<string, Patch<TDShape | undefined>> = {};
|
||||
|
||||
const beforeBindings: Record<string, Patch<TDBinding | undefined>> = {};
|
||||
const afterBindings: Record<string, Patch<TDBinding | undefined>> = {};
|
||||
|
||||
const idsToGroup = [...ids];
|
||||
const shapesToGroup: TDShape[] = [];
|
||||
const deletedGroupIds: string[] = [];
|
||||
const otherEffectedGroups: TDShape[] = [];
|
||||
|
||||
// Collect all of the shapes to group (and their ids)
|
||||
for (const id of ids) {
|
||||
const shape = app.getShape(id);
|
||||
if (shape.isLocked) continue;
|
||||
|
||||
if (shape.children === undefined) {
|
||||
shapesToGroup.push(shape);
|
||||
} else {
|
||||
const childIds = shape.children.filter(
|
||||
id => !app.getShape(id).isLocked
|
||||
);
|
||||
otherEffectedGroups.push(shape);
|
||||
idsToGroup.push(...childIds);
|
||||
shapesToGroup.push(
|
||||
...childIds.map(id => app.getShape(id)).filter(Boolean)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Can we create this group?
|
||||
|
||||
// Do the shapes have the same parent?
|
||||
if (
|
||||
shapesToGroup.every(
|
||||
shape => shape.parentId === shapesToGroup[0].parentId
|
||||
)
|
||||
) {
|
||||
// Is the common parent a shape (not the page)?
|
||||
if (shapesToGroup[0].parentId !== pageId) {
|
||||
const commonParent = app.getShape(shapesToGroup[0].parentId);
|
||||
// Are all of the common parent's shapes selected?
|
||||
if (commonParent.children?.length === idsToGroup.length) {
|
||||
// Don't create a group if that group would be the same as the
|
||||
// existing group.
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A flattened array of shapes from the page
|
||||
const flattenedShapes = TLDR.flatten_page(app.state, pageId);
|
||||
|
||||
// A map of shapes to their index in flattendShapes
|
||||
const shapeIndexMap = Object.fromEntries(
|
||||
shapesToGroup.map(shape => [shape.id, flattenedShapes.indexOf(shape)])
|
||||
);
|
||||
|
||||
// An array of shapes in order by their index in flattendShapes
|
||||
const sortedShapes = shapesToGroup.sort(
|
||||
(a, b) => shapeIndexMap[a.id] - shapeIndexMap[b.id]
|
||||
);
|
||||
|
||||
// The parentId is always the current page
|
||||
const groupParentId = pageId; // sortedShapes[0].parentId
|
||||
|
||||
// The childIndex should be the lowest index of the selected shapes
|
||||
// with a parent that is the current page; or else the child index
|
||||
// of the lowest selected shape.
|
||||
const groupChildIndex = (
|
||||
sortedShapes.filter(shape => shape.parentId === pageId)[0] ||
|
||||
sortedShapes[0]
|
||||
).childIndex;
|
||||
|
||||
// The shape's point is the min point of its childrens' common bounds
|
||||
const groupBounds = Utils.getCommonBounds(
|
||||
shapesToGroup.map(shape => TLDR.get_bounds(shape))
|
||||
);
|
||||
|
||||
// Create the group
|
||||
beforeShapes[groupId] = undefined;
|
||||
|
||||
afterShapes[groupId] = TLDR.get_shape_util(TDShapeType.Group).create({
|
||||
id: groupId,
|
||||
childIndex: groupChildIndex,
|
||||
parentId: groupParentId,
|
||||
point: [groupBounds.minX, groupBounds.minY],
|
||||
size: [groupBounds.width, groupBounds.height],
|
||||
children: sortedShapes.map(shape => shape.id),
|
||||
workspace: app.document.id,
|
||||
});
|
||||
|
||||
// Reparent shapes to the new group
|
||||
sortedShapes.forEach((shape, index) => {
|
||||
// If the shape is part of a different group, mark the parent shape for cleanup
|
||||
if (shape.parentId !== pageId) {
|
||||
const parentShape = app.getShape(shape.parentId);
|
||||
otherEffectedGroups.push(parentShape);
|
||||
}
|
||||
|
||||
beforeShapes[shape.id] = {
|
||||
...beforeShapes[shape.id],
|
||||
parentId: shape.parentId,
|
||||
childIndex: shape.childIndex,
|
||||
};
|
||||
|
||||
afterShapes[shape.id] = {
|
||||
...afterShapes[shape.id],
|
||||
parentId: groupId,
|
||||
childIndex: index + 1,
|
||||
};
|
||||
});
|
||||
|
||||
// Clean up effected parents
|
||||
while (otherEffectedGroups.length > 0) {
|
||||
const shape = otherEffectedGroups.pop();
|
||||
if (!shape) break;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const nextChildren = (beforeShapes[shape.id]?.children ||
|
||||
shape.children)!.filter(
|
||||
childId =>
|
||||
childId &&
|
||||
!(
|
||||
idsToGroup.includes(childId) ||
|
||||
deletedGroupIds.includes(childId)
|
||||
)
|
||||
);
|
||||
|
||||
// If the parent has no children, remove it
|
||||
if (nextChildren.length === 0) {
|
||||
beforeShapes[shape.id] = shape;
|
||||
afterShapes[shape.id] = undefined;
|
||||
|
||||
// And if that parent is part of a different group, mark it for cleanup
|
||||
// (This is necessary only when we implement nested groups.)
|
||||
if (shape.parentId !== pageId) {
|
||||
deletedGroupIds.push(shape.id);
|
||||
otherEffectedGroups.push(app.getShape(shape.parentId));
|
||||
}
|
||||
} else {
|
||||
beforeShapes[shape.id] = {
|
||||
...beforeShapes[shape.id],
|
||||
children: shape.children,
|
||||
};
|
||||
|
||||
afterShapes[shape.id] = {
|
||||
...afterShapes[shape.id],
|
||||
children: nextChildren,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This code is copied from delete.command. Create a shared helper!
|
||||
|
||||
const { bindings } = app;
|
||||
|
||||
const deletedGroupIdsSet = new Set(deletedGroupIds);
|
||||
|
||||
// We also need to delete bindings that reference the deleted shapes
|
||||
bindings.forEach(binding => {
|
||||
for (const id of [binding.toId, binding.fromId]) {
|
||||
// If the binding references a deleted shape...
|
||||
if (deletedGroupIdsSet.has(id)) {
|
||||
// Delete this binding
|
||||
beforeBindings[binding.id] = binding;
|
||||
afterBindings[binding.id] = undefined;
|
||||
|
||||
// Let's also look each the bound shape...
|
||||
const shape = app.getShape(id);
|
||||
|
||||
// If the bound shape has a handle that references the deleted binding...
|
||||
if (shape.handles) {
|
||||
Object.values(shape.handles)
|
||||
.filter(handle => handle.bindingId === binding.id)
|
||||
.forEach(handle => {
|
||||
// Save the binding reference in the before patch
|
||||
beforeShapes[id] = {
|
||||
...beforeShapes[id],
|
||||
handles: {
|
||||
...beforeShapes[id]?.handles,
|
||||
[handle.id]: { bindingId: binding.id },
|
||||
},
|
||||
};
|
||||
|
||||
// Unless we're currently deleting the shape, remove the
|
||||
// binding reference from the after patch
|
||||
if (!deletedGroupIds.includes(id)) {
|
||||
afterShapes[id] = {
|
||||
...afterShapes[id],
|
||||
handles: {
|
||||
...afterShapes[id]?.handles,
|
||||
[handle.id]: { bindingId: undefined },
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
id: 'group',
|
||||
before: {
|
||||
document: {
|
||||
pages: {
|
||||
[pageId]: {
|
||||
shapes: beforeShapes,
|
||||
bindings: beforeBindings,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[pageId]: {
|
||||
selectedIds: ids,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
document: {
|
||||
pages: {
|
||||
[pageId]: {
|
||||
shapes: afterShapes,
|
||||
bindings: beforeBindings,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[pageId]: {
|
||||
selectedIds: [groupId],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
24
libs/components/board-commands/src/index.ts
Normal file
24
libs/components/board-commands/src/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export * from './align-shapes';
|
||||
export * from './change-page';
|
||||
export * from './create-page';
|
||||
export * from './create-shapes';
|
||||
export * from './delete-page';
|
||||
export * from './delete-shapes';
|
||||
export * from './distribute-shapes';
|
||||
export * from './duplicate-page';
|
||||
export * from './duplicate-shapes';
|
||||
export * from './flip-shapes';
|
||||
export * from './group-shapes';
|
||||
export * from './move-shapes-to-page';
|
||||
export * from './reorder-shapes';
|
||||
export * from './rename-page';
|
||||
export * from './reset-bounds';
|
||||
export * from './rotate-shapes';
|
||||
export * from './stretch-shapes';
|
||||
export * from './style-shapes';
|
||||
export * from './toggle-shapes-decoration';
|
||||
export * from './toggle-shapes-prop';
|
||||
export * from './translate-shapes';
|
||||
export * from './ungroup-shapes';
|
||||
export * from './update-shapes';
|
||||
export * from './set-shapes-props';
|
||||
231
libs/components/board-commands/src/move-shapes-to-page.ts
Normal file
231
libs/components/board-commands/src/move-shapes-to-page.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import type {
|
||||
ArrowShape,
|
||||
PagePartial,
|
||||
TldrawCommand,
|
||||
TDShape,
|
||||
} from '@toeverything/components/board-types';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
import { TLDR } from '@toeverything/components/board-state';
|
||||
import { Utils, TLBounds } from '@tldraw/core';
|
||||
import { Vec } from '@tldraw/vec';
|
||||
|
||||
export function moveShapesToPage(
|
||||
app: TldrawApp,
|
||||
ids: string[],
|
||||
viewportBounds: TLBounds,
|
||||
fromPageId: string,
|
||||
toPageId: string
|
||||
): TldrawCommand {
|
||||
const { page } = app;
|
||||
|
||||
const fromPage: Record<string, PagePartial> = {
|
||||
before: {
|
||||
shapes: {},
|
||||
bindings: {},
|
||||
},
|
||||
after: {
|
||||
shapes: {},
|
||||
bindings: {},
|
||||
},
|
||||
};
|
||||
|
||||
const toPage: Record<string, PagePartial> = {
|
||||
before: {
|
||||
shapes: {},
|
||||
bindings: {},
|
||||
},
|
||||
after: {
|
||||
shapes: {},
|
||||
bindings: {},
|
||||
},
|
||||
};
|
||||
|
||||
// Collect all the shapes to move and their keys.
|
||||
const movingShapeIds = new Set<string>();
|
||||
const shapesToMove = new Set<TDShape>();
|
||||
|
||||
ids.map(id => app.getShape(id, fromPageId))
|
||||
.filter(shape => !shape.isLocked)
|
||||
.forEach(shape => {
|
||||
movingShapeIds.add(shape.id);
|
||||
shapesToMove.add(shape);
|
||||
if (shape.children !== undefined) {
|
||||
shape.children.forEach(childId => {
|
||||
movingShapeIds.add(childId);
|
||||
shapesToMove.add(app.getShape(childId, fromPageId));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Where should we put start putting shapes on the "to" page?
|
||||
const startingChildIndex = TLDR.get_top_child_index(app.state, toPageId);
|
||||
|
||||
// Which shapes are we moving?
|
||||
const movingShapes = Array.from(shapesToMove.values());
|
||||
|
||||
movingShapes.forEach((shape, i) => {
|
||||
// Remove the shape from the fromPage
|
||||
fromPage['before'].shapes[shape.id] = shape;
|
||||
fromPage['after'].shapes[shape.id] = undefined;
|
||||
|
||||
// But the moved shape on the "to" page
|
||||
toPage['before'].shapes[shape.id] = undefined;
|
||||
toPage['after'].shapes[shape.id] = shape;
|
||||
|
||||
// If the shape's parent isn't moving too, reparent the shape to
|
||||
// the "to" page, at the top of the z stack
|
||||
if (!movingShapeIds.has(shape.parentId)) {
|
||||
toPage['after'].shapes[shape.id] = {
|
||||
...shape,
|
||||
parentId: toPageId,
|
||||
childIndex: startingChildIndex + i,
|
||||
};
|
||||
|
||||
// If the shape was in a group, then pull the shape from the
|
||||
// parent's children array.
|
||||
if (shape.parentId !== fromPageId) {
|
||||
const parent = app.getShape(shape.parentId, fromPageId);
|
||||
fromPage['before'].shapes[parent.id] = {
|
||||
children: parent.children,
|
||||
};
|
||||
|
||||
fromPage['after'].shapes[parent.id] = {
|
||||
children: parent.children!.filter(
|
||||
childId => childId !== shape.id
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle bindings that effect duplicated shapes
|
||||
Object.values(page.bindings)
|
||||
.filter(
|
||||
binding =>
|
||||
movingShapeIds.has(binding.fromId) ||
|
||||
movingShapeIds.has(binding.toId)
|
||||
)
|
||||
.forEach(binding => {
|
||||
// Always delete the binding from the from page
|
||||
|
||||
fromPage['before'].bindings[binding.id] = binding;
|
||||
fromPage['after'].bindings[binding.id] = undefined;
|
||||
|
||||
// Delete the reference from the binding's fromShape
|
||||
|
||||
const fromBoundShape = app.getShape(binding.fromId, fromPageId);
|
||||
|
||||
// Will we be copying this binding to the new page?
|
||||
|
||||
const shouldCopy =
|
||||
movingShapeIds.has(binding.fromId) &&
|
||||
movingShapeIds.has(binding.toId);
|
||||
|
||||
if (shouldCopy) {
|
||||
// Just move the binding to the new page
|
||||
toPage['before'].bindings[binding.id] = undefined;
|
||||
toPage['after'].bindings[binding.id] = binding;
|
||||
} else {
|
||||
if (movingShapeIds.has(binding.fromId)) {
|
||||
// If we are only moving the "from" shape, we need to delete
|
||||
// the binding reference from the "from" shapes handles
|
||||
const fromShape = app.getShape(binding.fromId, fromPageId);
|
||||
const handle = Object.values(fromBoundShape.handles!).find(
|
||||
handle => handle.bindingId === binding.id
|
||||
)!;
|
||||
|
||||
// Remove the handle from the shape on the toPage
|
||||
|
||||
const handleId = handle.id as keyof ArrowShape['handles'];
|
||||
|
||||
const toPageShape = toPage['after'].shapes[fromShape.id]!;
|
||||
|
||||
toPageShape.handles = {
|
||||
...toPageShape.handles,
|
||||
[handleId]: {
|
||||
...toPageShape.handles![handleId],
|
||||
bindingId: undefined,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// If we are only moving the "to" shape, we need to delete
|
||||
// the binding reference from the "from" shape's handles
|
||||
const fromShape = app.getShape(binding.fromId, fromPageId);
|
||||
const handle = Object.values(fromBoundShape.handles!).find(
|
||||
handle => handle.bindingId === binding.id
|
||||
)!;
|
||||
|
||||
fromPage['before'].shapes[fromShape.id] = {
|
||||
handles: { [handle.id]: { bindingId: binding.id } },
|
||||
};
|
||||
|
||||
fromPage['after'].shapes[fromShape.id] = {
|
||||
handles: { [handle.id]: { bindingId: undefined } },
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Finally, center camera on selection
|
||||
|
||||
const toPageState = app.state.document.pageStates[toPageId];
|
||||
|
||||
const bounds = Utils.getCommonBounds(
|
||||
movingShapes.map(shape => TLDR.get_bounds(shape))
|
||||
);
|
||||
|
||||
const zoom = TLDR.get_camera_zoom(
|
||||
viewportBounds.width < viewportBounds.height
|
||||
? (viewportBounds.width - 128) / bounds.width
|
||||
: (viewportBounds.height - 128) / bounds.height
|
||||
);
|
||||
|
||||
const mx = (viewportBounds.width - bounds.width * zoom) / 2 / zoom;
|
||||
const my = (viewportBounds.height - bounds.height * zoom) / 2 / zoom;
|
||||
|
||||
const point = Vec.toFixed(Vec.add([-bounds.minX, -bounds.minY], [mx, my]));
|
||||
|
||||
return {
|
||||
id: 'move_to_page',
|
||||
before: {
|
||||
appState: {
|
||||
currentPageId: fromPageId,
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[fromPageId]: fromPage['before'],
|
||||
[toPageId]: toPage['before'],
|
||||
},
|
||||
pageStates: {
|
||||
[fromPageId]: { selectedIds: ids },
|
||||
[toPageId]: {
|
||||
selectedIds: toPageState.selectedIds,
|
||||
camera: toPageState.camera,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
appState: {
|
||||
currentPageId: toPageId,
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[fromPageId]: fromPage['after'],
|
||||
[toPageId]: toPage['after'],
|
||||
},
|
||||
pageStates: {
|
||||
[fromPageId]: { selectedIds: [] },
|
||||
[toPageId]: {
|
||||
selectedIds: ids,
|
||||
camera: {
|
||||
zoom,
|
||||
point,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
28
libs/components/board-commands/src/rename-page.ts
Normal file
28
libs/components/board-commands/src/rename-page.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { TldrawCommand } from '@toeverything/components/board-types';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
|
||||
export function renamePage(
|
||||
app: TldrawApp,
|
||||
pageId: string,
|
||||
name: string
|
||||
): TldrawCommand {
|
||||
const { page } = app;
|
||||
|
||||
return {
|
||||
id: 'rename_page',
|
||||
before: {
|
||||
document: {
|
||||
pages: {
|
||||
[pageId]: { name: page.name },
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
document: {
|
||||
pages: {
|
||||
[pageId]: { name: name },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
265
libs/components/board-commands/src/reorder-shapes.ts
Normal file
265
libs/components/board-commands/src/reorder-shapes.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
import {
|
||||
MoveType,
|
||||
TDShape,
|
||||
TldrawCommand,
|
||||
} from '@toeverything/components/board-types';
|
||||
import { TLDR } from '@toeverything/components/board-state';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
|
||||
export function reorderShapes(
|
||||
app: TldrawApp,
|
||||
ids: string[],
|
||||
type: MoveType
|
||||
): TldrawCommand {
|
||||
const { currentPageId, page } = app;
|
||||
|
||||
// Get the unique parent ids for the selected elements
|
||||
const parentIds = new Set(ids.map(id => app.getShape(id).parentId));
|
||||
|
||||
let result: {
|
||||
before: Record<string, Partial<TDShape>>;
|
||||
after: Record<string, Partial<TDShape>>;
|
||||
} = { before: {}, after: {} };
|
||||
|
||||
let startIndex: number;
|
||||
let startChildIndex: number;
|
||||
let step: number;
|
||||
|
||||
// Collect shapes with common parents into a table under their parent id
|
||||
Array.from(parentIds.values()).forEach(parentId => {
|
||||
let sortedChildren: TDShape[] = [];
|
||||
if (parentId === page.id) {
|
||||
sortedChildren = Object.values(page.shapes).sort(
|
||||
(a, b) => a.childIndex - b.childIndex
|
||||
);
|
||||
} else {
|
||||
const parent = app.getShape(parentId);
|
||||
if (!parent.children) throw Error('No children in parent!');
|
||||
|
||||
sortedChildren = parent.children
|
||||
.map(childId => app.getShape(childId))
|
||||
.sort((a, b) => a.childIndex - b.childIndex);
|
||||
}
|
||||
|
||||
const sortedChildIds = sortedChildren.map(shape => shape.id);
|
||||
|
||||
const sortedIndicesToMove = ids
|
||||
.filter(id => sortedChildIds.includes(id))
|
||||
.map(id => sortedChildIds.indexOf(id))
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
if (sortedIndicesToMove.length === sortedChildIds.length) return;
|
||||
|
||||
switch (type) {
|
||||
case MoveType.ToBack: {
|
||||
// a b c
|
||||
// Initial 1 2 3 4 5 6 7
|
||||
// Final .25 .5 .75 1 3 6 7
|
||||
// a b c
|
||||
|
||||
// Find the lowest "open" index
|
||||
for (let i = 0; i < sortedChildIds.length; i++) {
|
||||
if (sortedIndicesToMove.includes(i)) continue;
|
||||
startIndex = i;
|
||||
break;
|
||||
}
|
||||
|
||||
// Find the lowest child index that isn't in sortedIndicesToMove
|
||||
startChildIndex = sortedChildren[startIndex].childIndex;
|
||||
|
||||
// Find the step for each additional child
|
||||
step = startChildIndex / (sortedIndicesToMove.length + 1);
|
||||
|
||||
// Get the results of moving the selected shapes below the first open index's shape
|
||||
result = TLDR.mutate_shapes(
|
||||
app.state,
|
||||
sortedIndicesToMove
|
||||
.map(i => sortedChildren[i].id)
|
||||
.reverse(),
|
||||
(shape, i) => ({
|
||||
childIndex: startChildIndex - (i + 1) * step,
|
||||
}),
|
||||
currentPageId
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
case MoveType.ToFront: {
|
||||
// a b c
|
||||
// Initial 1 2 3 4 5 6 7
|
||||
// Final 1 3 6 7 8 9 10
|
||||
// a b c
|
||||
|
||||
// Find the highest "open" index
|
||||
for (let i = sortedChildIds.length - 1; i >= 0; i--) {
|
||||
if (sortedIndicesToMove.includes(i)) continue;
|
||||
startIndex = i;
|
||||
break;
|
||||
}
|
||||
|
||||
// Find the lowest child index that isn't in sortedIndicesToMove
|
||||
startChildIndex = sortedChildren[startIndex].childIndex;
|
||||
|
||||
// Find the step for each additional child
|
||||
step = 1;
|
||||
|
||||
// Get the results of moving the selected shapes below the first open index's shape
|
||||
result = TLDR.mutate_shapes(
|
||||
app.state,
|
||||
sortedIndicesToMove.map(i => sortedChildren[i].id),
|
||||
(shape, i) => ({
|
||||
childIndex: startChildIndex + (i + 1),
|
||||
}),
|
||||
currentPageId
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
case MoveType.Backward: {
|
||||
// a b c
|
||||
// Initial 1 2 3 4 5 6 7
|
||||
// Final .5 1 1.66 2.33 3 6 7
|
||||
// a b c
|
||||
|
||||
const indexMap: Record<string, number> = {};
|
||||
|
||||
// Starting from the top...
|
||||
for (let i = sortedChildIds.length - 1; i >= 0; i--) {
|
||||
// If we found a moving index...
|
||||
if (sortedIndicesToMove.includes(i)) {
|
||||
for (let j = i; j >= 0; j--) {
|
||||
// iterate downward until we find an open spot
|
||||
if (!sortedIndicesToMove.includes(j)) {
|
||||
// i = the index of the first closed spot
|
||||
// j = the index of the first open spot
|
||||
|
||||
const endChildIndex =
|
||||
sortedChildren[j].childIndex;
|
||||
let startChildIndex: number;
|
||||
let step: number;
|
||||
|
||||
if (j === 0) {
|
||||
// We're moving below the first child, start from
|
||||
// half of its child index.
|
||||
|
||||
startChildIndex = endChildIndex / 2;
|
||||
step = endChildIndex / 2 / (i - j + 1);
|
||||
} else {
|
||||
// Start from the child index of the child below the
|
||||
// child above.
|
||||
startChildIndex =
|
||||
sortedChildren[j - 1].childIndex;
|
||||
step =
|
||||
(endChildIndex - startChildIndex) /
|
||||
(i - j + 1);
|
||||
startChildIndex += step;
|
||||
}
|
||||
|
||||
for (let k = 0; k < i - j; k++) {
|
||||
indexMap[sortedChildren[j + k + 1].id] =
|
||||
startChildIndex + step * k;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.values(indexMap).length > 0) {
|
||||
// Get the results of moving the selected shapes below the first open index's shape
|
||||
result = TLDR.mutate_shapes(
|
||||
app.state,
|
||||
sortedIndicesToMove.map(i => sortedChildren[i].id),
|
||||
shape => ({
|
||||
childIndex: indexMap[shape.id],
|
||||
}),
|
||||
currentPageId
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case MoveType.Forward: {
|
||||
// a b c
|
||||
// Initial 1 2 3 4 5 6 7
|
||||
// Final 1 3 3.5 6 7 8 9
|
||||
// a b c
|
||||
|
||||
const indexMap: Record<string, number> = {};
|
||||
|
||||
// Starting from the top...
|
||||
for (let i = 0; i < sortedChildIds.length; i++) {
|
||||
// If we found a moving index...
|
||||
if (sortedIndicesToMove.includes(i)) {
|
||||
// Search for the first open spot above this one
|
||||
for (let j = i; j < sortedChildIds.length; j++) {
|
||||
if (!sortedIndicesToMove.includes(j)) {
|
||||
// i = the low index of the first closed spot
|
||||
// j = the high index of the first open spot
|
||||
|
||||
startChildIndex = sortedChildren[j].childIndex;
|
||||
|
||||
const step =
|
||||
j === sortedChildIds.length - 1
|
||||
? 1
|
||||
: (sortedChildren[j + 1].childIndex -
|
||||
startChildIndex) /
|
||||
(j - i + 1);
|
||||
|
||||
for (let k = 0; k < j - i; k++) {
|
||||
indexMap[sortedChildren[i + k].id] =
|
||||
startChildIndex + step * (k + 1);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.values(indexMap).length > 0) {
|
||||
// Get the results of moving the selected shapes below the first open index's shape
|
||||
result = TLDR.mutate_shapes(
|
||||
app.state,
|
||||
sortedIndicesToMove.map(i => sortedChildren[i].id),
|
||||
shape => ({
|
||||
childIndex: indexMap[shape.id],
|
||||
}),
|
||||
currentPageId
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
id: 'move',
|
||||
before: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: { shapes: result.before },
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: ids,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: { shapes: result.after },
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: ids,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
46
libs/components/board-commands/src/reset-bounds.ts
Normal file
46
libs/components/board-commands/src/reset-bounds.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { TldrawCommand } from '@toeverything/components/board-types';
|
||||
import { TLDR } from '@toeverything/components/board-state';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
|
||||
export function resetBounds(
|
||||
app: TldrawApp,
|
||||
ids: string[],
|
||||
pageId: string
|
||||
): TldrawCommand {
|
||||
const { currentPageId } = app;
|
||||
|
||||
const { before, after } = TLDR.mutate_shapes(
|
||||
app.state,
|
||||
ids,
|
||||
shape => app.getShapeUtil(shape).onDoubleClickBoundsHandle?.(shape),
|
||||
pageId
|
||||
);
|
||||
|
||||
return {
|
||||
id: 'reset_bounds',
|
||||
before: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: { shapes: before },
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: ids,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: { shapes: after },
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: ids,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
83
libs/components/board-commands/src/rotate-shapes.ts
Normal file
83
libs/components/board-commands/src/rotate-shapes.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Utils } from '@tldraw/core';
|
||||
import type {
|
||||
TldrawCommand,
|
||||
TDShape,
|
||||
} from '@toeverything/components/board-types';
|
||||
import { TLDR } from '@toeverything/components/board-state';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
|
||||
const PI2 = Math.PI * 2;
|
||||
|
||||
export function rotateShapes(
|
||||
app: TldrawApp,
|
||||
ids: string[],
|
||||
delta = -PI2 / 4
|
||||
): TldrawCommand | void {
|
||||
const { currentPageId } = app;
|
||||
|
||||
// The shapes for the before patch
|
||||
const before: Record<string, Partial<TDShape>> = {};
|
||||
|
||||
// The shapes for the after patch
|
||||
const after: Record<string, Partial<TDShape>> = {};
|
||||
|
||||
// Find the shapes that we want to rotate.
|
||||
// We don't rotate groups: we rotate their children instead.
|
||||
const shapesToRotate = ids
|
||||
.flatMap(id => {
|
||||
const shape = app.getShape(id);
|
||||
return shape.children
|
||||
? shape.children.map(childId => app.getShape(childId))
|
||||
: shape;
|
||||
})
|
||||
.filter(shape => !shape.isLocked);
|
||||
|
||||
// Find the common center to all shapes
|
||||
// This is the point that we'll rotate around
|
||||
const origin = Utils.getBoundsCenter(
|
||||
Utils.getCommonBounds(
|
||||
shapesToRotate.map(shape => TLDR.get_bounds(shape))
|
||||
)
|
||||
);
|
||||
|
||||
// Find the rotate mutations for each shape
|
||||
shapesToRotate.forEach(shape => {
|
||||
const change = TLDR.get_rotated_shape_mutation(
|
||||
shape,
|
||||
TLDR.get_center(shape),
|
||||
origin,
|
||||
delta
|
||||
);
|
||||
if (!change) return;
|
||||
before[shape.id] = TLDR.get_before_shape(shape, change);
|
||||
after[shape.id] = change;
|
||||
});
|
||||
|
||||
return {
|
||||
id: 'rotate',
|
||||
before: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: { shapes: before },
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: ids,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: { shapes: after },
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: ids,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
61
libs/components/board-commands/src/set-shapes-props.ts
Normal file
61
libs/components/board-commands/src/set-shapes-props.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type {
|
||||
TDShape,
|
||||
TldrawCommand,
|
||||
} from '@toeverything/components/board-types';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
|
||||
export function setShapesProps<T extends TDShape>(
|
||||
app: TldrawApp,
|
||||
ids: string[],
|
||||
partial: Partial<T>
|
||||
): TldrawCommand {
|
||||
const { currentPageId, selectedIds } = app;
|
||||
|
||||
const initialShapes = ids
|
||||
.map(id => app.getShape<T>(id))
|
||||
.filter(shape => (partial['isLocked'] ? true : !shape.isLocked));
|
||||
|
||||
const before: Record<string, Partial<TDShape>> = {};
|
||||
const after: Record<string, Partial<TDShape>> = {};
|
||||
|
||||
const keys = Object.keys(partial) as (keyof T)[];
|
||||
|
||||
initialShapes.forEach(shape => {
|
||||
before[shape.id] = Object.fromEntries(
|
||||
keys.map(key => [key, shape[key]])
|
||||
);
|
||||
after[shape.id] = partial;
|
||||
});
|
||||
|
||||
return {
|
||||
id: 'set_props',
|
||||
before: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: before,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: after,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import { TLDR } from '@toeverything/components/board-state';
|
||||
import type {
|
||||
ArrowShape,
|
||||
GroupShape,
|
||||
PagePartial,
|
||||
TDSnapshot,
|
||||
} from '@toeverything/components/board-types';
|
||||
|
||||
export function removeShapesFromPage(
|
||||
data: TDSnapshot,
|
||||
ids: string[],
|
||||
pageId: string
|
||||
) {
|
||||
const before: PagePartial = {
|
||||
shapes: {},
|
||||
bindings: {},
|
||||
};
|
||||
|
||||
const after: PagePartial = {
|
||||
shapes: {},
|
||||
bindings: {},
|
||||
};
|
||||
|
||||
const parentsToUpdate: GroupShape[] = [];
|
||||
const deletedIds = new Set();
|
||||
const assetsToRemove = new Set<string>();
|
||||
|
||||
// These are the shapes we're definitely going to delete
|
||||
|
||||
ids.filter(id => !TLDR.get_shape(data, id, pageId).isLocked).forEach(id => {
|
||||
deletedIds.add(id);
|
||||
const shape = TLDR.get_shape(data, id, pageId);
|
||||
before.shapes[id] = shape;
|
||||
after.shapes[id] = undefined;
|
||||
|
||||
// Also delete the shape's children
|
||||
|
||||
if (shape.children !== undefined) {
|
||||
shape.children.forEach(childId => {
|
||||
deletedIds.add(childId);
|
||||
const child = TLDR.get_shape(data, childId, pageId);
|
||||
before.shapes[childId] = child;
|
||||
after.shapes[childId] = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
if (shape.parentId !== pageId) {
|
||||
parentsToUpdate.push(TLDR.get_shape(data, shape.parentId, pageId));
|
||||
}
|
||||
|
||||
if (shape.assetId) {
|
||||
assetsToRemove.add(shape.assetId);
|
||||
}
|
||||
});
|
||||
|
||||
parentsToUpdate.forEach(parent => {
|
||||
if (ids.includes(parent.id)) return;
|
||||
deletedIds.add(parent.id);
|
||||
before.shapes[parent.id] = { children: parent.children };
|
||||
after.shapes[parent.id] = {
|
||||
children: parent.children.filter(id => !ids.includes(id)),
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
if (after.shapes[parent.id]?.children!.length === 0) {
|
||||
after.shapes[parent.id] = undefined;
|
||||
before.shapes[parent.id] = TLDR.get_shape(data, parent.id, pageId);
|
||||
}
|
||||
});
|
||||
|
||||
// Recursively check for empty parents?
|
||||
|
||||
const page = TLDR.get_page(data, pageId);
|
||||
|
||||
// We also need to delete bindings that reference the deleted shapes
|
||||
Object.values(page.bindings)
|
||||
.filter(
|
||||
binding =>
|
||||
deletedIds.has(binding.fromId) || deletedIds.has(binding.toId)
|
||||
)
|
||||
.forEach(binding => {
|
||||
for (const id of [binding.toId, binding.fromId]) {
|
||||
// If the binding references a deleted shape...
|
||||
if (after.shapes[id] === undefined) {
|
||||
// Delete this binding
|
||||
before.bindings[binding.id] = binding;
|
||||
after.bindings[binding.id] = undefined;
|
||||
|
||||
// Let's also look each the bound shape...
|
||||
const shape = page.shapes[id];
|
||||
|
||||
// If the bound shape has a handle that references the deleted binding...
|
||||
if (shape && shape.handles) {
|
||||
Object.values(shape.handles)
|
||||
.filter(handle => handle.bindingId === binding.id)
|
||||
.forEach(handle => {
|
||||
// Save the binding reference in the before patch
|
||||
before.shapes[id] = {
|
||||
...before.shapes[id],
|
||||
handles: {
|
||||
...before.shapes[id]?.handles,
|
||||
[handle.id]: {
|
||||
...before.shapes[id]?.handles?.[
|
||||
handle.id as keyof ArrowShape['handles']
|
||||
],
|
||||
bindingId: binding.id,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Unless we're currently deleting the shape, remove the
|
||||
// binding reference from the after patch
|
||||
if (!deletedIds.has(id)) {
|
||||
after.shapes[id] = {
|
||||
...after.shapes[id],
|
||||
handles: {
|
||||
...after.shapes[id]?.handles,
|
||||
[handle.id]: {
|
||||
...after.shapes[id]?.handles?.[
|
||||
handle.id as keyof ArrowShape['handles']
|
||||
],
|
||||
bindingId: undefined,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// If any other shapes are using the deleted assets, don't remove them
|
||||
Object.values(data.document.pages)
|
||||
.flatMap(page => Object.values(page.shapes))
|
||||
.forEach(shape => {
|
||||
if (
|
||||
'assetId' in shape &&
|
||||
shape.assetId &&
|
||||
!deletedIds.has(shape.id)
|
||||
) {
|
||||
assetsToRemove.delete(shape.assetId);
|
||||
}
|
||||
});
|
||||
|
||||
return { before, after, assetsToRemove: Array.from(assetsToRemove) };
|
||||
}
|
||||
114
libs/components/board-commands/src/stretch-shapes.ts
Normal file
114
libs/components/board-commands/src/stretch-shapes.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { TLBoundsCorner, Utils } from '@tldraw/core';
|
||||
import { StretchType, TDShapeType } from '@toeverything/components/board-types';
|
||||
import type { TldrawCommand } from '@toeverything/components/board-types';
|
||||
import { TLDR } from '@toeverything/components/board-state';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
|
||||
export function stretchShapes(
|
||||
app: TldrawApp,
|
||||
ids: string[],
|
||||
type: StretchType
|
||||
): TldrawCommand {
|
||||
const { currentPageId, selectedIds } = app;
|
||||
|
||||
const initialShapes = ids.map(id => app.getShape(id));
|
||||
|
||||
const boundsForShapes = initialShapes.map(shape => TLDR.get_bounds(shape));
|
||||
|
||||
const commonBounds = Utils.getCommonBounds(boundsForShapes);
|
||||
|
||||
const idsToMutate = ids
|
||||
.flatMap(id => {
|
||||
const shape = app.getShape(id);
|
||||
return shape.children ? shape.children : shape.id;
|
||||
})
|
||||
.filter(id => !app.getShape(id).isLocked);
|
||||
|
||||
const { before, after } = TLDR.mutate_shapes(
|
||||
app.state,
|
||||
idsToMutate,
|
||||
shape => {
|
||||
const bounds = TLDR.get_bounds(shape);
|
||||
|
||||
switch (type) {
|
||||
case StretchType.Horizontal: {
|
||||
const newBounds = {
|
||||
...bounds,
|
||||
minX: commonBounds.minX,
|
||||
maxX: commonBounds.maxX,
|
||||
width: commonBounds.width,
|
||||
};
|
||||
|
||||
return TLDR.get_shape_util(shape).transformSingle(
|
||||
shape,
|
||||
newBounds,
|
||||
{
|
||||
type: TLBoundsCorner.TopLeft,
|
||||
scaleX: newBounds.width / bounds.width,
|
||||
scaleY: 1,
|
||||
initialShape: shape,
|
||||
transformOrigin: [0.5, 0.5],
|
||||
}
|
||||
);
|
||||
}
|
||||
case StretchType.Vertical: {
|
||||
const newBounds = {
|
||||
...bounds,
|
||||
minY: commonBounds.minY,
|
||||
maxY: commonBounds.maxY,
|
||||
height: commonBounds.height,
|
||||
};
|
||||
|
||||
return TLDR.get_shape_util(shape).transformSingle(
|
||||
shape,
|
||||
newBounds,
|
||||
{
|
||||
type: TLBoundsCorner.TopLeft,
|
||||
scaleX: 1,
|
||||
scaleY: newBounds.height / bounds.height,
|
||||
initialShape: shape,
|
||||
transformOrigin: [0.5, 0.5],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
currentPageId
|
||||
);
|
||||
|
||||
initialShapes.forEach(shape => {
|
||||
if (shape.type === TDShapeType.Group) {
|
||||
delete before[shape.id];
|
||||
delete after[shape.id];
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
id: 'stretch',
|
||||
before: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: { shapes: before },
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: { shapes: after },
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: ids,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
100
libs/components/board-commands/src/style-shapes.ts
Normal file
100
libs/components/board-commands/src/style-shapes.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import {
|
||||
Patch,
|
||||
ShapeStyles,
|
||||
TldrawCommand,
|
||||
TDShape,
|
||||
// TDShapeType,
|
||||
// TextShape
|
||||
} from '@toeverything/components/board-types';
|
||||
import { TLDR } from '@toeverything/components/board-state';
|
||||
import { Vec } from '@tldraw/vec';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
|
||||
export function styleShapes(
|
||||
app: TldrawApp,
|
||||
ids: string[],
|
||||
changes: Partial<ShapeStyles>
|
||||
): TldrawCommand {
|
||||
const { currentPageId, selectedIds } = app;
|
||||
|
||||
const shapeIdsToMutate = ids
|
||||
.flatMap(id => TLDR.get_document_branch(app.state, id, currentPageId))
|
||||
.filter(id => !app.getShape(id).isLocked);
|
||||
|
||||
const beforeShapes: Record<string, Patch<TDShape>> = {};
|
||||
const afterShapes: Record<string, Patch<TDShape>> = {};
|
||||
|
||||
shapeIdsToMutate
|
||||
.map(id => app.getShape(id))
|
||||
.filter(shape => !shape.isLocked)
|
||||
.forEach(shape => {
|
||||
beforeShapes[shape.id] = {
|
||||
style: {
|
||||
...Object.fromEntries(
|
||||
Object.keys(changes).map(key => [
|
||||
key,
|
||||
shape.style[key as keyof typeof shape.style],
|
||||
])
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
afterShapes[shape.id] = {
|
||||
style: changes,
|
||||
};
|
||||
|
||||
// if (shape.type === TDShapeType.Text) {
|
||||
// beforeShapes[shape.id].point = shape.point;
|
||||
// afterShapes[shape.id].point = Vec.toFixed(
|
||||
// Vec.add(
|
||||
// shape.point,
|
||||
// Vec.sub(
|
||||
// app.getShapeUtil(shape).getCenter(shape),
|
||||
// app.getShapeUtil(shape).getCenter({
|
||||
// ...shape,
|
||||
// style: { ...shape.style, ...changes }
|
||||
// } as TextShape)
|
||||
// )
|
||||
// )
|
||||
// );
|
||||
// }
|
||||
});
|
||||
|
||||
return {
|
||||
id: 'style',
|
||||
before: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: beforeShapes,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: selectedIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
appState: {
|
||||
currentStyle: { ...app.appState.currentStyle },
|
||||
},
|
||||
},
|
||||
after: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: afterShapes,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: ids,
|
||||
},
|
||||
},
|
||||
},
|
||||
appState: {
|
||||
currentStyle: changes,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Decoration } from '@toeverything/components/board-types';
|
||||
import type {
|
||||
Patch,
|
||||
ArrowShape,
|
||||
TldrawCommand,
|
||||
} from '@toeverything/components/board-types';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
|
||||
export function toggleShapesDecoration(
|
||||
app: TldrawApp,
|
||||
ids: string[],
|
||||
decorationId: 'start' | 'end'
|
||||
): TldrawCommand {
|
||||
const { currentPageId, selectedIds } = app;
|
||||
|
||||
const beforeShapes: Record<string, Patch<ArrowShape>> = Object.fromEntries(
|
||||
ids.map(id => [
|
||||
id,
|
||||
{
|
||||
decorations: {
|
||||
[decorationId]:
|
||||
app.getShape<ArrowShape>(id).decorations?.[
|
||||
decorationId
|
||||
],
|
||||
},
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
const afterShapes: Record<string, Patch<ArrowShape>> = Object.fromEntries(
|
||||
ids
|
||||
.filter(id => !app.getShape(id).isLocked)
|
||||
.map(id => [
|
||||
id,
|
||||
{
|
||||
decorations: {
|
||||
[decorationId]: app.getShape<ArrowShape>(id)
|
||||
.decorations?.[decorationId]
|
||||
? undefined
|
||||
: Decoration.Arrow,
|
||||
},
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
return {
|
||||
id: 'toggle_decorations',
|
||||
before: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: { shapes: beforeShapes },
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: { shapes: afterShapes },
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: ids,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
59
libs/components/board-commands/src/toggle-shapes-prop.ts
Normal file
59
libs/components/board-commands/src/toggle-shapes-prop.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type {
|
||||
TDShape,
|
||||
TldrawCommand,
|
||||
} from '@toeverything/components/board-types';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
|
||||
export function toggleShapesProp(
|
||||
app: TldrawApp,
|
||||
ids: string[],
|
||||
prop: keyof TDShape
|
||||
): TldrawCommand {
|
||||
const { currentPageId } = app;
|
||||
|
||||
const initialShapes = ids
|
||||
.map(id => app.getShape(id))
|
||||
.filter(shape => (prop === 'isLocked' ? true : !shape.isLocked));
|
||||
|
||||
const isAllToggled = initialShapes.every(shape => shape[prop]);
|
||||
|
||||
const before: Record<string, Partial<TDShape>> = {};
|
||||
const after: Record<string, Partial<TDShape>> = {};
|
||||
|
||||
initialShapes.forEach(shape => {
|
||||
before[shape.id] = { [prop]: shape[prop] };
|
||||
after[shape.id] = { [prop]: !isAllToggled };
|
||||
});
|
||||
|
||||
return {
|
||||
id: 'toggle',
|
||||
before: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: before,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: ids,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: {
|
||||
shapes: after,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: ids,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
113
libs/components/board-commands/src/translate-shapes.ts
Normal file
113
libs/components/board-commands/src/translate-shapes.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { Vec } from '@tldraw/vec';
|
||||
import { TLDR } from '@toeverything/components/board-state';
|
||||
import type {
|
||||
TldrawCommand,
|
||||
PagePartial,
|
||||
} from '@toeverything/components/board-types';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
|
||||
export function translateShapes(
|
||||
app: TldrawApp,
|
||||
ids: string[],
|
||||
delta: number[]
|
||||
): TldrawCommand {
|
||||
const { currentPageId, selectedIds } = app;
|
||||
|
||||
// Clear session cache
|
||||
app.rotationInfo.selectedIds = [...selectedIds];
|
||||
|
||||
const before: PagePartial = {
|
||||
shapes: {},
|
||||
bindings: {},
|
||||
};
|
||||
|
||||
const after: PagePartial = {
|
||||
shapes: {},
|
||||
bindings: {},
|
||||
};
|
||||
|
||||
const idsToMutate = ids
|
||||
.flatMap(id => {
|
||||
const shape = app.getShape(id);
|
||||
return shape.children ? shape.children : shape.id;
|
||||
})
|
||||
.filter(id => !app.getShape(id).isLocked);
|
||||
|
||||
const change = TLDR.mutate_shapes(
|
||||
app.state,
|
||||
idsToMutate,
|
||||
shape => ({
|
||||
point: Vec.toFixed(Vec.add(shape.point, delta)),
|
||||
}),
|
||||
currentPageId
|
||||
);
|
||||
|
||||
before.shapes = change.before;
|
||||
after.shapes = change.after;
|
||||
|
||||
// Delete bindings from nudged shapes, unless both bound and bound-to shapes are selected
|
||||
const bindingsToDelete = TLDR.get_bindings(app.state, currentPageId).filter(
|
||||
binding => ids.includes(binding.fromId) && !ids.includes(binding.toId)
|
||||
);
|
||||
|
||||
bindingsToDelete.forEach(binding => {
|
||||
before.bindings[binding.id] = binding;
|
||||
after.bindings[binding.id] = undefined;
|
||||
|
||||
for (const id of [binding.toId, binding.fromId]) {
|
||||
// Let's also look at the bound shape...
|
||||
const shape = app.getShape(id);
|
||||
|
||||
if (!shape.handles) continue;
|
||||
|
||||
// If the bound shape has a handle that references the deleted binding, delete that reference
|
||||
|
||||
Object.values(shape.handles)
|
||||
.filter(handle => handle.bindingId === binding.id)
|
||||
.forEach(handle => {
|
||||
before.shapes[id] = {
|
||||
...before.shapes[id],
|
||||
handles: {
|
||||
...before.shapes[id]?.handles,
|
||||
[handle.id]: { bindingId: binding.id },
|
||||
},
|
||||
};
|
||||
after.shapes[id] = {
|
||||
...after.shapes[id],
|
||||
handles: {
|
||||
...after.shapes[id]?.handles,
|
||||
[handle.id]: { bindingId: undefined },
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
id: 'translate',
|
||||
before: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: before,
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: ids,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
document: {
|
||||
pages: {
|
||||
[currentPageId]: after,
|
||||
},
|
||||
pageStates: {
|
||||
[currentPageId]: {
|
||||
selectedIds: ids,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
170
libs/components/board-commands/src/ungroup-shapes.ts
Normal file
170
libs/components/board-commands/src/ungroup-shapes.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import { TLDR } from '@toeverything/components/board-state';
|
||||
import type {
|
||||
Patch,
|
||||
GroupShape,
|
||||
TDBinding,
|
||||
TDShape,
|
||||
TldrawCommand,
|
||||
} from '@toeverything/components/board-types';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
|
||||
export function ungroupShapes(
|
||||
app: TldrawApp,
|
||||
selectedIds: string[],
|
||||
groupShapes: GroupShape[],
|
||||
pageId: string
|
||||
): TldrawCommand | undefined {
|
||||
const { bindings } = app;
|
||||
|
||||
const beforeShapes: Record<string, Patch<TDShape | undefined>> = {};
|
||||
const afterShapes: Record<string, Patch<TDShape | undefined>> = {};
|
||||
|
||||
const beforeBindings: Record<string, Patch<TDBinding | undefined>> = {};
|
||||
const afterBindings: Record<string, Patch<TDBinding | undefined>> = {};
|
||||
|
||||
const beforeSelectedIds = selectedIds;
|
||||
const afterSelectedIds = selectedIds.filter(
|
||||
id => !groupShapes.find(shape => shape.id === id)
|
||||
);
|
||||
|
||||
// The group shape
|
||||
groupShapes
|
||||
.filter(shape => !shape.isLocked)
|
||||
.forEach(groupShape => {
|
||||
const shapesToReparent: TDShape[] = [];
|
||||
const deletedGroupIds: string[] = [];
|
||||
|
||||
// Remove the group shape in the next state
|
||||
beforeShapes[groupShape.id] = groupShape;
|
||||
afterShapes[groupShape.id] = undefined;
|
||||
|
||||
// Select its children in the next state
|
||||
groupShape.children.forEach(id => {
|
||||
afterSelectedIds.push(id);
|
||||
const shape = app.getShape(id, pageId);
|
||||
shapesToReparent.push(shape);
|
||||
});
|
||||
|
||||
// We'll start placing the shapes at this childIndex
|
||||
const startingChildIndex = groupShape.childIndex;
|
||||
|
||||
// And we'll need to fit them under this child index
|
||||
const endingChildIndex = TLDR.get_child_index_above(
|
||||
app.state,
|
||||
groupShape.id,
|
||||
pageId
|
||||
);
|
||||
|
||||
const step =
|
||||
(endingChildIndex - startingChildIndex) /
|
||||
shapesToReparent.length;
|
||||
|
||||
// An array of shapes in order by their child index
|
||||
const sortedShapes = shapesToReparent.sort(
|
||||
(a, b) => a.childIndex - b.childIndex
|
||||
);
|
||||
|
||||
// Reparent shapes to the page
|
||||
sortedShapes.forEach((shape, index) => {
|
||||
beforeShapes[shape.id] = {
|
||||
parentId: shape.parentId,
|
||||
childIndex: shape.childIndex,
|
||||
};
|
||||
|
||||
afterShapes[shape.id] = {
|
||||
parentId: pageId,
|
||||
childIndex: startingChildIndex + step * index,
|
||||
};
|
||||
});
|
||||
|
||||
// We also need to delete bindings that reference the deleted shapes
|
||||
bindings
|
||||
.filter(
|
||||
binding =>
|
||||
binding.toId === groupShape.id ||
|
||||
binding.fromId === groupShape.id
|
||||
)
|
||||
.forEach(binding => {
|
||||
for (const id of [binding.toId, binding.fromId]) {
|
||||
// If the binding references the deleted group...
|
||||
if (afterShapes[id] === undefined) {
|
||||
// Delete the binding
|
||||
beforeBindings[binding.id] = binding;
|
||||
afterBindings[binding.id] = undefined;
|
||||
|
||||
// Let's also look each the bound shape...
|
||||
const shape = app.getShape(id, pageId);
|
||||
|
||||
// If the bound shape has a handle that references the deleted binding...
|
||||
if (shape.handles) {
|
||||
Object.values(shape.handles)
|
||||
.filter(
|
||||
handle =>
|
||||
handle.bindingId === binding.id
|
||||
)
|
||||
.forEach(handle => {
|
||||
// Save the binding reference in the before patch
|
||||
beforeShapes[id] = {
|
||||
...beforeShapes[id],
|
||||
handles: {
|
||||
...beforeShapes[id]?.handles,
|
||||
[handle.id]: {
|
||||
bindingId: binding.id,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Unless we're currently deleting the shape, remove the
|
||||
// binding reference from the after patch
|
||||
if (!deletedGroupIds.includes(id)) {
|
||||
afterShapes[id] = {
|
||||
...afterShapes[id],
|
||||
handles: {
|
||||
...afterShapes[id]?.handles,
|
||||
[handle.id]: {
|
||||
bindingId: undefined,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
id: 'ungroup',
|
||||
before: {
|
||||
document: {
|
||||
pages: {
|
||||
[pageId]: {
|
||||
shapes: beforeShapes,
|
||||
bindings: beforeBindings,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[pageId]: {
|
||||
selectedIds: beforeSelectedIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
document: {
|
||||
pages: {
|
||||
[pageId]: {
|
||||
shapes: afterShapes,
|
||||
bindings: beforeBindings,
|
||||
},
|
||||
},
|
||||
pageStates: {
|
||||
[pageId]: {
|
||||
selectedIds: afterSelectedIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
43
libs/components/board-commands/src/update-shapes.ts
Normal file
43
libs/components/board-commands/src/update-shapes.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type {
|
||||
TldrawCommand,
|
||||
TDShape,
|
||||
} from '@toeverything/components/board-types';
|
||||
import { TLDR } from '@toeverything/components/board-state';
|
||||
import type { TldrawApp } from '@toeverything/components/board-state';
|
||||
|
||||
export function updateShapes(
|
||||
app: TldrawApp,
|
||||
updates: ({ id: string } & Partial<TDShape>)[],
|
||||
pageId: string
|
||||
): TldrawCommand {
|
||||
const ids = updates.map(update => update.id);
|
||||
|
||||
const change = TLDR.mutate_shapes(
|
||||
app.state,
|
||||
ids.filter(id => !app.getShape(id, pageId).isLocked),
|
||||
(shape, i) => updates[i],
|
||||
pageId
|
||||
);
|
||||
|
||||
return {
|
||||
id: 'update',
|
||||
before: {
|
||||
document: {
|
||||
pages: {
|
||||
[pageId]: {
|
||||
shapes: change.before,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
document: {
|
||||
pages: {
|
||||
[pageId]: {
|
||||
shapes: change.after,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user