Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support edgeless tidy up #8516

Merged
merged 1 commit into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/affine/block-embed/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/block-std": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.1.67",
"@blocksuite/icons": "^2.1.68",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
Expand Down
5 changes: 3 additions & 2 deletions packages/affine/block-surface/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ export {
} from './surface-spec.js';
export { SurfaceBlockTransformer } from './surface-transformer.js';
export { AStarRunner } from './utils/a-star.js';
export { LayoutableMindmapElementModel } from './utils/mindmap/utils.js';
export { RoughCanvas } from './utils/rough/canvas.js';
export type { Options } from './utils/rough/core.js';

export { sortIndex } from './utils/sort.js';
export type { Options } from './utils/rough/core.js';

import {
almostEqual,
Expand Down Expand Up @@ -92,6 +92,7 @@ import {
moveMindMapSubtree,
showMergeIndicator,
} from './utils/mindmap/utils.js';
export { sortIndex } from './utils/sort.js';

export const ConnectorUtils = {
isConnectorAndBindingsAllSelected,
Expand Down
2 changes: 1 addition & 1 deletion packages/affine/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/block-std": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.1.67",
"@blocksuite/icons": "^2.1.68",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
Expand Down
2 changes: 1 addition & 1 deletion packages/affine/data-view/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/block-std": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.1.67",
"@blocksuite/icons": "^2.1.68",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
Expand Down
6 changes: 4 additions & 2 deletions packages/blocks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"@blocksuite/block-std": "workspace:*",
"@blocksuite/data-view": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.1.67",
"@blocksuite/icons": "^2.1.68",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
Expand All @@ -50,6 +50,7 @@
"html2canvas": "^1.4.1",
"katex": "^0.16.11",
"lit": "^3.2.0",
"lodash.chunk": "^4.2.0",
"mdast-util-gfm-autolink-literal": "^2.0.1",
"mdast-util-gfm-strikethrough": "^2.0.0",
"mdast-util-gfm-table": "^2.0.0",
Expand Down Expand Up @@ -107,6 +108,7 @@
],
"devDependencies": {
"@types/dompurify": "^3.0.5",
"@types/katex": "^0.16.7"
"@types/katex": "^0.16.7",
"@types/lodash.chunk": "^4.2.9"
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { LayoutableMindmapElementModel } from '@blocksuite/affine-block-surface';
import {
AlignBottomIcon,
AlignDistributeHorizontallyIcon,
Expand All @@ -11,17 +12,26 @@ import {
} from '@blocksuite/affine-components/icons';
import {
ConnectorElementModel,
GroupElementModel,
EdgelessTextBlockModel,
MindmapElementModel,
} from '@blocksuite/affine-model';
import {
type GfxContainerElement,
type GfxModel,
isGfxContainerElm,
} from '@blocksuite/block-std/gfx';
import { Bound, WithDisposable } from '@blocksuite/global/utils';
import { html, LitElement, nothing } from 'lit';
import { AutoTidyUpIcon, ResizeTidyUpIcon } from '@blocksuite/icons/lit';
import { css, html, LitElement, nothing, type TemplateResult } from 'lit';
import { property } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import chunk from 'lodash.chunk';

import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js';

const enum Alignment {
AutoArrange = 'Auto arrange',
AutoResize = 'Resize & Align',
Bottom = 'Align bottom',
DistributeHorizontally = 'Distribute horizontally',
DistributeVertically = 'Distribute vertically',
Expand All @@ -32,7 +42,15 @@ const enum Alignment {
Vertically = 'Align vertically',
}

const ALIGNMENT_LIST = [
const ALIGN_HEIGHT = 200;
const ALIGN_PADDING = 20;

interface AlignmentIcon {
name: Alignment;
content: TemplateResult<1>;
}

const HORIZONTAL_ALIGNMENT: AlignmentIcon[] = [
{
name: Alignment.Left,
content: AlignLeftIcon,
Expand All @@ -49,10 +67,9 @@ const ALIGNMENT_LIST = [
name: Alignment.DistributeHorizontally,
content: AlignDistributeHorizontallyIcon,
},
{
name: 'separator',
content: html`<editor-toolbar-separator></editor-toolbar-separator>`,
},
];

const VERTICAL_ALIGNMENT: AlignmentIcon[] = [
{
name: Alignment.Top,
content: AlignTopIcon,
Expand All @@ -69,9 +86,33 @@ const ALIGNMENT_LIST = [
name: Alignment.DistributeVertically,
content: AlignDistributeVerticallyIcon,
},
] as const;
];

const AUTO_ALIGNMENT: AlignmentIcon[] = [
{
name: Alignment.AutoArrange,
content: AutoTidyUpIcon({ width: '20px', height: '20px' }),
},
{
name: Alignment.AutoResize,
content: ResizeTidyUpIcon({ width: '20px', height: '20px' }),
},
];

export class EdgelessAlignButton extends WithDisposable(LitElement) {
static override styles = css`
.align-menu-content {
max-width: 120px;
flex-wrap: wrap;
padding: 8px 2px;
}
.align-menu-separator {
width: 120px;
height: 1px;
background-color: var(--affine-background-tertiary-color);
}
`;

private get elements() {
return this.edgeless.service.selection.selectedElements;
}
Expand Down Expand Up @@ -102,9 +143,60 @@ export class EdgelessAlignButton extends WithDisposable(LitElement) {
case Alignment.DistributeVertically:
this._alignDistributeVertically();
break;
case Alignment.AutoArrange:
this._alignAutoArrange();
break;
case Alignment.AutoResize:
this._alignAutoResize();
break;
}
}

private _alignAutoArrange() {
const chunks = this._splitElementsToChunks(this.elements);
// update element XY
const startX: number = chunks[0][0].elementBound.x;
let startY: number = chunks[0][0].elementBound.y;
chunks.forEach(items => {
let posX = startX;
let maxHeight = 0;
items.forEach(ele => {
const { x: eleX, y: eleY } = ele.elementBound;
const bound = Bound.deserialize(ele.xywh);
const xOffset = bound.x - eleX;
const yOffset = bound.y - eleY;
bound.x = posX + xOffset;
bound.y = startY + yOffset;
this._updateXYWH(ele, bound);
if (ele.elementBound.h > maxHeight) {
maxHeight = ele.elementBound.h;
}
posX += ele.elementBound.w + ALIGN_PADDING;
});
startY += maxHeight + ALIGN_PADDING;
});
}

private _alignAutoResize() {
// resize to fixed height
this.elements.forEach(ele => {
if (
ele instanceof ConnectorElementModel ||
ele instanceof EdgelessTextBlockModel ||
ele instanceof LayoutableMindmapElementModel
) {
return;
}
const bound = Bound.deserialize(ele.xywh);
const scale = ALIGN_HEIGHT / ele.elementBound.h;
bound.h = scale * bound.h;
bound.w = scale * bound.w;
this._updateXYWH(ele, bound);
});
// arrange
this._alignAutoArrange();
}

private _alignBottom() {
const { elements } = this;
const bounds = elements.map(a => a.elementBound);
Expand Down Expand Up @@ -232,16 +324,60 @@ export class EdgelessAlignButton extends WithDisposable(LitElement) {
});
}

private _splitElementsToChunks(models: GfxModel[]) {
const sortByCenterX = (a: GfxModel, b: GfxModel) =>
a.elementBound.center[0] - b.elementBound.center[0];
const sortByCenterY = (a: GfxModel, b: GfxModel) =>
a.elementBound.center[1] - b.elementBound.center[1];
const elements = models.filter(ele => {
if (
ele instanceof ConnectorElementModel &&
(ele.target.id || ele.source.id)
) {
return false;
}
return true;
});
elements.sort(sortByCenterY);
const chunks = chunk(elements, 4);
chunks.forEach(items => items.sort(sortByCenterX));
return chunks;
}

private _updatChildElementsXYWH(
container: GfxContainerElement,
targetBound: Bound
) {
const containerBound = Bound.deserialize(container.xywh);
const scaleX = targetBound.w / containerBound.w;
const scaleY = targetBound.h / containerBound.h;
container.childElements.forEach(child => {
const childBound = Bound.deserialize(child.xywh);
childBound.x = targetBound.x + scaleX * (childBound.x - containerBound.x);
childBound.y = targetBound.y + scaleY * (childBound.y - containerBound.y);
childBound.w = scaleX * childBound.w;
childBound.h = scaleY * childBound.h;
this._updateXYWH(child, childBound);
});
}

private _updateXYWH(ele: BlockSuite.EdgelessModel, bound: Bound) {
if (ele instanceof ConnectorElementModel) {
ele.moveTo(bound);
} else if (ele instanceof GroupElementModel) {
const groupBound = Bound.deserialize(ele.xywh);
ele.childElements.forEach(child => {
const newBound = Bound.deserialize(child.xywh);
newBound.x += bound.x - groupBound.x;
newBound.y += bound.y - groupBound.y;
this._updateXYWH(child, newBound);
} else if (ele instanceof LayoutableMindmapElementModel) {
akumatus marked this conversation as resolved.
Show resolved Hide resolved
const rootId = ele.tree.id;
const rootEle = ele.childElements.find(child => child.id === rootId);
if (rootEle) {
const rootBound = Bound.deserialize(rootEle.xywh);
rootBound.x += bound.x - ele.x;
rootBound.y += bound.y - ele.y;
this._updateXYWH(rootEle, rootBound);
}
ele.layout();
} else if (isGfxContainerElm(ele)) {
this._updatChildElementsXYWH(ele, bound);
this.edgeless.service.updateElement(ele.id, {
xywh: bound.serialize(),
});
} else {
this.edgeless.service.updateElement(ele.id, {
Expand All @@ -250,6 +386,26 @@ export class EdgelessAlignButton extends WithDisposable(LitElement) {
}
}

private renderIcons(icons: AlignmentIcon[]) {
return html`
${repeat(
icons,
(item, index) => item.name + index,
({ name, content }) => {
return html`
<editor-icon-button
aria-label=${name}
.tooltip=${name}
@click=${() => this._align(name)}
>
${content}
</editor-icon-button>
`;
}
)}
`;
}

override firstUpdated() {
this._disposables.add(
this.edgeless.service.selection.slots.updated.on(() =>
Expand All @@ -270,23 +426,11 @@ export class EdgelessAlignButton extends WithDisposable(LitElement) {
</editor-icon-button>
`}
>
<div>
${repeat(
ALIGNMENT_LIST,
(item, index) => item.name + index,
({ name, content }) => {
if (name === 'separator') return content;
return html`
<editor-icon-button
aria-label=${name}
.tooltip=${name}
@click=${() => this._align(name)}
>
${content}
</editor-icon-button>
`;
}
)}
<div class="align-menu-content">
${this.renderIcons(HORIZONTAL_ALIGNMENT)}
${this.renderIcons(VERTICAL_ALIGNMENT)}
<div class="align-menu-separator"></div>
${this.renderIcons(AUTO_ALIGNMENT)}
</div>
</editor-menu-button>
`;
Expand Down
Loading
Loading