Skip to content

Commit

Permalink
feat: AStar
Browse files Browse the repository at this point in the history
  • Loading branch information
dineug committed Nov 2, 2024
1 parent 2064697 commit 26782c1
Show file tree
Hide file tree
Showing 9 changed files with 423 additions and 4 deletions.
34 changes: 34 additions & 0 deletions packages/erd-editor/src/components/erd/Erd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
ref,
watch,
} from '@dineug/r-html';
import { arrayHas } from '@dineug/shared';
import { throttle } from 'lodash-es';
import { filter, fromEvent, Subscription, throttleTime } from 'rxjs';

import { useAppContext } from '@/components/appContext';
Expand Down Expand Up @@ -42,8 +44,10 @@ import { streamZoomLevelAction$ } from '@/engine/modules/settings/generator.acti
import { moveToTableAction } from '@/engine/modules/table/atom.actions';
import { HISTORY_LIMIT } from '@/engine/rx-store';
import { useUnmounted } from '@/hooks/useUnmounted';
import { getAStarService } from '@/services/a-star';
import { isMouseEvent } from '@/utils/domEvent';
import { getAbsolutePoint } from '@/utils/dragSelect';
import type { GridObject } from '@/utils/draw-relationship';
import { closeColorPickerAction } from '@/utils/emitter';
import { drag$, DragMove, keyup$ } from '@/utils/globalEventObservable';
import { getRelationshipIcon } from '@/utils/icon';
Expand Down Expand Up @@ -84,6 +88,8 @@ const Erd: FC<ErdProps> = (props, ctx) => {

const { addUnsubscribe } = useUnmounted();

const objectGridMap = new Map<string, GridObject>();

const resetScroll = () => {
if (root.value.scrollTop === 0 && root.value.scrollLeft === 0) {
return;
Expand Down Expand Up @@ -303,6 +309,18 @@ const Erd: FC<ErdProps> = (props, ctx) => {
});
};

const syncObjectGridMap = throttle(() => {
const { store } = app.value;
const {
doc: { tableIds },
} = store.state;
const hasId = arrayHas(tableIds);

Array.from(objectGridMap.keys()).forEach(id => {
!hasId(id) && objectGridMap.delete(id);
});
}, 200);

onMounted(() => {
const { store, emitter, keydown$ } = app.value;
const $root = root.value;
Expand Down Expand Up @@ -352,6 +370,22 @@ const Erd: FC<ErdProps> = (props, ctx) => {
state.diffValue = value;
store.dispatch(changeOpenMapAction({ [Open.diffViewer]: true }));
},
updateObjectGridMap: ({ payload }) => {
objectGridMap.set(payload.id, payload);
syncObjectGridMap();
},
calcPathFinding: ({ payload: { id, start, end, resolve } }) => {
getAStarService()
?.run({
id,
start,
end,
objectGridMap,
})
.then(lines => {
resolve(lines);
});
},
}),
keydown$
.pipe(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { FC, svg } from '@dineug/r-html';
import { FC, nextTick, observable, svg } from '@dineug/r-html';
import { debounce } from 'lodash-es';

import { useAppContext } from '@/components/appContext';
import { StartRelationshipType } from '@/constants/schema';
import { hoverColumnMapAction } from '@/engine/modules/editor/atom.actions';
import { Relationship as RelationshipType } from '@/internal-types';
import { type Point, Relationship as RelationshipType } from '@/internal-types';
import { getRelationshipPath } from '@/utils/draw-relationship/pathFinding';
import { calcPathFindingAction } from '@/utils/emitter';

import { relationshipShape } from './Relationship.template';

Expand All @@ -16,6 +18,12 @@ export type RelationshipProps = {
const Relationship: FC<RelationshipProps> = (props, ctx) => {
const app = useAppContext(ctx);

const state = observable<{
lines: Array<[Point, Point]>;
}>({
lines: [],
});

const handleMouseenter = (relationship: RelationshipType) => {
const { store } = app.value;
store.dispatch(
Expand All @@ -33,19 +41,63 @@ const Relationship: FC<RelationshipProps> = (props, ctx) => {
store.dispatch(hoverColumnMapAction({ columnIds: [] }));
};

let commitCalcPathFinding = false;

const resolve = (lines: Array<[Point, Point]>) => {
if (!lines.length) {
return;
}

commitCalcPathFinding = true;
state.lines = lines;
nextTick(() => {
commitCalcPathFinding = false;
});
};

const calcPathFinding = debounce(
(
payload: Omit<
ReturnType<typeof calcPathFindingAction>['payload'],
'resolve'
>
) => {
const { emitter } = app.value;
emitter.emit(
calcPathFindingAction({
...payload,
resolve,
})
);
},
300
);

return () => {
const { store } = app.value;
const { editor } = store.state;
const { relationship, strokeWidth } = props;
const relationshipPath = getRelationshipPath(relationship);
const { path, line } = relationshipPath;
const lines = path.path.d();
const calcLines = state.lines;
const lines = commitCalcPathFinding ? calcLines : path.path.d();
const shape = relationshipShape(
relationship.relationshipType,
relationshipPath
);
const hover = Boolean(editor.hoverRelationshipMap[relationship.id]);

if (
!commitCalcPathFinding &&
relationship.start.tableId !== relationship.end.tableId
) {
calcPathFinding({
id: relationship.id,
start: path.path.M,
end: path.path.L,
});
}

return svg`
<g
class=${[
Expand Down
18 changes: 18 additions & 0 deletions packages/erd-editor/src/components/erd/minimap/table/Table.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { FC, html } from '@dineug/r-html';
import { debounce } from 'lodash-es';

import { useAppContext } from '@/components/appContext';
import * as styles from '@/components/erd/canvas/table/Table.styles';
import type { Table } from '@/internal-types';
import { calcTableHeight, calcTableWidths } from '@/utils/calcTable';
import { updateObjectGridMapAction } from '@/utils/emitter';

export type TableProps = {
table: Table;
Expand All @@ -12,12 +14,28 @@ export type TableProps = {
const Table: FC<TableProps> = (props, ctx) => {
const app = useAppContext(ctx);

const updateObjectGridMap = debounce(
(payload: ReturnType<typeof updateObjectGridMapAction>['payload']) => {
const { emitter } = app.value;
emitter.emit(updateObjectGridMapAction(payload));
},
200
);

return () => {
const { store } = app.value;
const { table } = props;
const tableWidths = calcTableWidths(table, store.state);
const height = calcTableHeight(table);

updateObjectGridMap({
id: table.id,
x: table.ui.x,
y: table.ui.y,
width: tableWidths.width,
height,
});

return html`
<div
class=${['table', styles.root]}
Expand Down
192 changes: 192 additions & 0 deletions packages/erd-editor/src/services/a-star/AStarFinder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import type { Point } from '@/internal-types';

export class ANode {
x: number;
y: number;
g = Infinity;
h = 0;
f = Infinity;
parent: ANode | null = null;

constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}

class PriorityQueue {
nodes: ANode[] = [];

enqueue(node: ANode) {
this.nodes.push(node);
this.nodes.sort((a, b) => a.f - b.f);
}

dequeue() {
return this.nodes.shift();
}

includes(node: ANode) {
return this.nodes.some(n => n.x === node.x && n.y === node.y);
}
}

function heuristic(a: Point, b: Point) {
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
}

export type Wall = {
minX: number;
maxX: number;
minY: number;
maxY: number;
};

function getNeighbors(
node: ANode,
end: ANode,
walls: Wall[],
boundary: number
) {
const neighbors = [];
const directions = [
[1, 0],
[-1, 0],
[0, 1],
[0, -1],
// [1, 1],
// [-1, -1],
// [1, -1],
// [-1, 1],
];

for (const [dx, dy] of directions) {
const x = node.x + dx;
const y = node.y + dy;

if (Math.abs(x - end.x) > boundary || Math.abs(y - end.y) > boundary) {
continue;
}

const isWall = walls.some(
wall =>
wall.minX <= x && x <= wall.maxX && wall.minY <= y && y <= wall.maxY
);
if (isWall) {
continue;
}

const neighbor = new ANode(x, y);
neighbors.push(neighbor);
}

return neighbors;
}

export function aStar(
start: ANode,
end: ANode,
walls: Wall[],
boundary = 400
): Point[] {
const openSet = new PriorityQueue();
const closedSet = new Set();

start.g = 0;
start.h = heuristic(start, end);
start.f = start.h;
openSet.enqueue(start);

while (openSet.nodes.length > 0) {
const current = openSet.dequeue()!;

if (current.x === end.x && current.y === end.y) {
return reconstructPath(current);
}

closedSet.add(`${current.x},${current.y}`);

const neighbors = getNeighbors(current, end, walls, boundary);
for (const neighbor of neighbors) {
const key = `${neighbor.x},${neighbor.y}`;

if (closedSet.has(key)) {
continue;
}

const tentativeG =
current.g +
(current.x !== neighbor.x && current.y !== neighbor.y ? Math.SQRT2 : 1);

if (tentativeG < neighbor.g) {
neighbor.parent = current;
neighbor.g = tentativeG;
neighbor.h = heuristic(neighbor, end);
neighbor.f = neighbor.g + neighbor.h;

if (!openSet.includes(neighbor)) {
openSet.enqueue(neighbor);
}
}
}
}

return [];
}

function reconstructPath(node: ANode | null): Point[] {
const path: ANode[] = [];
while (node) {
path.push(node);
node = node.parent;
}
return compressPath(path.reverse());
}

function compressPath(nodes: ANode[]): Point[] {
if (nodes.length < 3) {
return nodes.map(node => ({ x: node.x, y: node.y }));
}

const compressed: Point[] = [];

let sx = nodes[0].x;
let sy = nodes[0].y;
let px = nodes[1].x;
let py = nodes[1].y;
let dx = px - sx;
let dy = py - sy;
let lx, ly, ldx, ldy, sq, i;

sq = Math.sqrt(dx * dx + dy * dy);
dx /= sq;
dy /= sq;

compressed.push({ x: sx, y: sy });

for (i = 2; i < nodes.length; i++) {
lx = px;
ly = py;

ldx = dx;
ldy = dy;

px = nodes[i].x;
py = nodes[i].y;

dx = px - lx;
dy = py - ly;

sq = Math.sqrt(dx * dx + dy * dy);
dx /= sq;
dy /= sq;

if (dx !== ldx || dy !== ldy) {
compressed.push({ x: lx, y: ly });
}
}

compressed.push({ x: px, y: py });

return compressed;
}
Loading

0 comments on commit 26782c1

Please sign in to comment.