Skip to content

Commit

Permalink
feat: AutomaticTablePlacement
Browse files Browse the repository at this point in the history
  • Loading branch information
dineug committed Nov 29, 2023
1 parent 20d8d2c commit 640f132
Show file tree
Hide file tree
Showing 41 changed files with 1,018 additions and 48 deletions.
4 changes: 2 additions & 2 deletions packages/erd-editor/.storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,15 @@ const preview: Preview = {
globalTypes: {
grayColor: {
description: 'Theme gray color',
defaultValue: GrayColor.gray,
defaultValue: GrayColor.slate,
toolbar: {
title: 'GrayColor',
items: Object.values(GrayColor),
},
},
accentColor: {
description: 'Theme accent color',
defaultValue: AccentColor.gray,
defaultValue: AccentColor.indigo,
toolbar: {
title: 'AccentColor',
items: Object.values(AccentColor),
Expand Down
1 change: 1 addition & 0 deletions packages/erd-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"color": "^4.2.3",
"d3": "7.8.5",
"deepmerge": "^4.3.1",
"framer-motion": "^10.16.5",
"highlight-words-core": "^1.2.2",
"html-to-image": "^1.11.11",
"lodash-es": "^4.17.21",
Expand Down
2 changes: 2 additions & 0 deletions packages/erd-editor/src/components/erd-editor/ErdEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { appContext, createAppContext } from '@/components/appContext';
import Erd from '@/components/erd/Erd';
import GlobalStyles from '@/components/global-styles/GlobalStyles';
import Theme from '@/components/theme/Theme';
import ToastContainer from '@/components/toast-container/ToastContainer';
import Toolbar from '@/components/toolbar/Toolbar';
import Visualization from '@/components/visualization/Visualization';
import { TOOLBAR_HEIGHT } from '@/constants/layout';
Expand Down Expand Up @@ -146,6 +147,7 @@ const ErdEditor: FC<ErdEditorProps, ErdEditorElement> = (props, ctx) => {
? html`<${Visualization} />`
: null}
</div>
<${ToastContainer} />
${text.span}
</div>
`;
Expand Down
19 changes: 18 additions & 1 deletion packages/erd-editor/src/components/erd/Erd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import {
} from '@dineug/r-html';

import { useAppContext } from '@/components/appContext';
import AutomaticTablePlacement, {
TablePoint,
} from '@/components/erd/automatic-table-placement/AutomaticTablePlacement';
import Canvas from '@/components/erd/canvas/Canvas';
import DragSelect from '@/components/erd/drag-select/DragSelect';
import ErdContextMenu, {
Expand All @@ -25,6 +28,7 @@ import {
streamScrollToAction,
streamZoomLevelAction,
} from '@/engine/modules/settings/atom.actions';
import { moveToTableAction } from '@/engine/modules/table/atom.actions';
import { useUnmounted } from '@/hooks/useUnmounted';
import { isMouseEvent } from '@/utils/domEvent';
import { closeColorPickerAction } from '@/utils/emitter';
Expand Down Expand Up @@ -158,6 +162,11 @@ const Erd: FC<ErdProps> = (props, ctx) => {
store.dispatch(changeColorAllAction$(color));
};

const handleChangeAutomaticTablePlacement = (tables: TablePoint[]) => {
const { store } = app.value;
store.dispatch(tables.map(moveToTableAction));
};

onMounted(() => {
const { store, emitter } = app.value;
const $root = root.value;
Expand All @@ -184,7 +193,7 @@ const Erd: FC<ErdProps> = (props, ctx) => {
return () => {
const { store } = app.value;
const {
editor: { drawRelationship },
editor: { drawRelationship, runAutomaticTablePlacement },
} = store.state;

const cursor = drawRelationship
Expand Down Expand Up @@ -234,6 +243,14 @@ const Erd: FC<ErdProps> = (props, ctx) => {
/>
`
: null}
${runAutomaticTablePlacement
? html`
<${AutomaticTablePlacement}
app=${app}
.onChange=${handleChangeAutomaticTablePlacement}
/>
`
: null}
</div>
`;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { css } from '@dineug/r-html';

export const root = css`
display: flex;
width: 100%;
height: 100%;
overflow: hidden;
position: absolute;
top: 0;
left: 0;
background-color: var(--canvas-boundary-background);
`;

export const container = css`
display: flex;
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
pointer-events: none;
.minimap-viewport {
display: none;
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { toJson } from '@dineug/erd-editor-schema';
import { createRef, FC, html, Ref, useProvider } from '@dineug/r-html';
import { createInRange } from '@dineug/shared';
import { round } from 'lodash-es';

import {
AppContext,
appContext,
createAppContext,
} from '@/components/appContext';
import Canvas from '@/components/erd/canvas/Canvas';
import Minimap from '@/components/erd/minimap/Minimap';
import Button from '@/components/primitives/button/Button';
import Toast from '@/components/primitives/toast/Toast';
import { CANVAS_ZOOM_MIN } from '@/constants/schema';
import { endAutomaticTablePlacementAction } from '@/engine/modules/editor/atom.actions';
import { initialLoadJsonAction$ } from '@/engine/modules/editor/generator.actions';
import {
changeZoomLevelAction,
scrollToAction,
} from '@/engine/modules/settings/atom.actions';
import { query } from '@/utils/collection/query';
import { openToastAction } from '@/utils/emitter';
import { closePromise } from '@/utils/promise';

import * as styles from './AutomaticTablePlacement.styles';
import { createAutomaticTablePlacement } from './createAutomaticTablePlacement';

export type AutomaticTablePlacementProps = {
app: Ref<AppContext>;
root: Ref<HTMLDivElement>;
onChange: (tables: TablePoint[]) => void;
};

export type TablePoint = {
id: string;
x: number;
y: number;
};

const AutomaticTablePlacement: FC<AutomaticTablePlacementProps> = (
props,
ctx
) => {
const canvas = createRef<HTMLDivElement>();
const prevApp = props.app.value;
const appContextValue = createAppContext({
toWidth: prevApp.toWidth,
});
useProvider(ctx, appContext, appContextValue);

const {
store: { state: prevState },
emitter,
} = prevApp;
const { store } = appContextValue;

const zoomInRange = createInRange(CANVAS_ZOOM_MIN, 0.7);
const zoomLevelInRange = (zoom: number) => round(zoomInRange(zoom), 2);

store.dispatchSync(
initialLoadJsonAction$(toJson(prevState)),
changeZoomLevelAction({
value: zoomLevelInRange(
prevState.editor.viewport.width / prevState.settings.width
),
}),
scrollToAction({
scrollLeft:
-1 *
(prevState.settings.width / 2 - prevState.editor.viewport.width / 2),
scrollTop:
-1 *
(prevState.settings.height / 2 - prevState.editor.viewport.height / 2),
})
);

const {
doc: { tableIds },
collections,
} = store.state;

const tables = query(collections)
.collection('tableEntities')
.selectByIds(tableIds);

const [close, onClose] = closePromise();

const handleCancel = () => {
onClose();
prevApp.store.dispatch(endAutomaticTablePlacementAction());
};

if (!tables.length) {
handleCancel();
emitter.emit(
openToastAction({
message: html`<${Toast} description="Not found tables" />`,
})
);
return () => null;
}

try {
const simulation = createAutomaticTablePlacement(store.state);

const handleStop = () => {
simulation.stop();
props.onChange(
tables.map(table => ({
id: table.id,
x: table.ui.x,
y: table.ui.y,
}))
);
handleCancel();
};

emitter.emit(
openToastAction({
close,
message: html`
<${Toast}
description="Automatic Table Placement..."
action=${html`
<${Button} size="1" text=${'Stop'} .onClick=${handleStop} />
<${Button} size="1" text=${'Cancel'} .onClick=${handleCancel} />
`}
/>
`,
})
);

simulation.on('end', handleStop);
} catch (e) {
handleCancel();
}

return () =>
html`
<div class=${styles.root}>
<div class=${styles.container}>
<${Canvas} root=${props.root} canvas=${canvas} />
<${Minimap} />
</div>
</div>
`;
};

export default AutomaticTablePlacement;
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import {
forceCollide,
forceLink,
forceManyBody,
forceSimulation,
forceX,
forceY,
} from 'd3';

import { RootState } from '@/engine/state';
import { Table } from '@/internal-types';
import { calcTableHeight, calcTableWidths } from '@/utils/calcTable';
import { query } from '@/utils/collection/query';
import { relationshipSort } from '@/utils/draw-relationship/sort';

type Node = {
id: string;
r: number;
x: number;
y: number;
ref: Table;
};

type Link = {
source: string;
target: string;
};

function createNodes(
state: RootState,
x: number,
y: number
): [Array<Node>, Array<Link>] {
const {
doc: { tableIds, relationshipIds },
collections,
} = state;
const tables = query(collections)
.collection('tableEntities')
.selectByIds(tableIds);
const relationships = query(collections)
.collection('relationshipEntities')
.selectByIds(relationshipIds);

const nodes: Node[] = [];
const links: Link[] = [];
const linkIdSet = new Set<string>();

tables.forEach(table => {
const width = calcTableWidths(table, state).width;
const height = calcTableHeight(table);
nodes.push({
id: table.id,
r: (width + height) / 4,
x,
y,
ref: table,
});
});

relationships.forEach(relationship => {
const { start, end } = relationship;
const linkId = `${start.tableId}-${end.tableId}`;

if (start.tableId !== end.tableId && !linkIdSet.has(linkId)) {
links.push({
source: start.tableId,
target: end.tableId,
});
linkIdSet.add(linkId);
}
});

return [nodes, links];
}

export function createAutomaticTablePlacement(state: RootState) {
const { settings } = state;
const centerX = settings.width / 2;
const centerY = settings.height / 2;
const [nodes, links] = createNodes(state, centerX, centerY);

return forceSimulation(nodes)
.force(
'link',
forceLink(links).id((d: any) => d.id)
)
.force(
'collide',
forceCollide().radius((d: any) => 100 + d.r)
)
.force('charge', forceManyBody())
.force('x', forceX(centerX))
.force('y', forceY(centerY))
.on('tick', () => {
nodes.forEach(({ r, x, y, ref }) => {
ref.ui.x = x - r;
ref.ui.y = y - r;
});
relationshipSort(state);
});
}
10 changes: 6 additions & 4 deletions packages/erd-editor/src/components/erd/canvas/Canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,12 @@ const Canvas: FC<CanvasProps> = (props, ctx) => {
)}
${bHas(show, Show.relationship) ? html`<${CanvasSvg} />` : null}
${drawRelationship?.start
? html`<${DrawRelationship}
root=${props.root}
draw=${drawRelationship}
/>`
? html`
<${DrawRelationship}
root=${props.root}
draw=${drawRelationship}
/>
`
: null}
</div>
`;
Expand Down
Loading

0 comments on commit 640f132

Please sign in to comment.