Skip to content
Open
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
62 changes: 61 additions & 1 deletion src/components/EditorHeader/ControlPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ import {
} from "../../hooks";
import { enterFullscreen, exitFullscreen } from "../../utils/fullscreen";
import { dataURItoBlob } from "../../utils/utils";
import { IconAddArea, IconAddNote, IconAddTable } from "../../icons";
import { arrangeTables } from "../../utils/arrangeTables";
import { IconAddArea, IconAddNote, IconAddTable, IconAutoArrange } from "../../icons";
import LayoutDropdown from "./LayoutDropdown";
import Sidesheet from "./SideSheet/Sidesheet";
import Modal from "./Modal/Modal";
Expand Down Expand Up @@ -748,6 +749,41 @@ export default function ControlPanel({ title, setTitle, lastSaved }) {
setLayout((prev) => ({ ...prev, dbmlEditor: !prev.dbmlEditor }));
};
const save = () => setSaveState(State.SAVING);

const autoArrange = (algorithm = 'force-directed') => {
if (layout.readOnly || tables.length === 0) return;

// Store original positions for undo
const originalPositions = tables.map(table => ({
id: table.id,
x: table.x,
y: table.y
}));

// Apply layout algorithm
const diagram = { tables: [...tables], relationships };
arrangeTables(diagram, algorithm);

// Update tables with new positions
diagram.tables.forEach((table, index) => {
updateTable(tables[index].id, { x: table.x, y: table.y });
});

// Add to undo stack
setUndoStack(prev => [...prev, {
action: Action.MOVE,
bulk: true,
message: t("auto_arrange"),
elements: diagram.tables.map((table, index) => ({
id: tables[index].id,
type: ObjectType.TABLE,
undo: originalPositions.find(op => op.id === tables[index].id),
redo: { x: table.x, y: table.y }
}))
}]);
setRedoStack([]);
};

const recentlyOpenedDiagrams = useLiveQuery(() =>
db.diagrams.orderBy("lastModified").reverse().limit(10).toArray(),
);
Expand Down Expand Up @@ -1287,6 +1323,21 @@ export default function ControlPanel({ title, setTitle, lastSaved }) {
function: copyAsImage,
shortcut: "Ctrl+Alt+C",
},
auto_arrange: {
children: [
{
name: t("force_directed"),
function: () => autoArrange('force-directed'),
disabled: layout.readOnly || tables.length === 0,
},
{
name: t("hierarchical"),
function: () => autoArrange('hierarchical'),
disabled: layout.readOnly || tables.length === 0,
},
],
function: () => {},
},
},
view: {
header: {
Expand Down Expand Up @@ -1733,6 +1784,15 @@ export default function ControlPanel({ title, setTitle, lastSaved }) {
<IconAddNote />
</button>
</Tooltip>
<Tooltip content={t("auto_arrange")} position="bottom">
<button
className="py-1 px-2 hover-2 rounded-sm flex items-center disabled:opacity-50"
onClick={() => autoArrange('force-directed')}
disabled={layout.readOnly || tables.length === 0}
>
<IconAutoArrange />
</button>
</Tooltip>
<Divider layout="vertical" margin="8px" />
<Tooltip content={t("save")} position="bottom">
<button
Expand Down
15 changes: 15 additions & 0 deletions src/data/datatypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@ const binaryRegex = /^[01]+$/;

/* eslint-disable no-unused-vars */
const defaultTypesBase = {
MYPRIMETYPE: {
type: "MYPRIMETYPE",
color: intColor,
checkDefault: (field) => {
if (!/^-?\d+$/.test(field.default)) {
return false;
}
const value = Number.parseInt(field.default, 10);
return value > 0 && value % 2 === 1;
},
hasCheck: true,
isSized: false,
hasPrecision: false,
canIncrement: false,
},
INT: {
type: "INT",
color: intColor,
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ const en = {
add_table: "Add table",
add_area: "Add area",
add_note: "Add note",
auto_arrange: "Auto Arrange",
force_directed: "Force-Directed Layout",
hierarchical: "Hierarchical Layout",
add_type: "Add type",
tables: "Tables",
relationships: "Relationships",
Expand Down
22 changes: 22 additions & 0 deletions src/icons/IconAutoArrange.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from "react";

Check failure on line 1 in src/icons/IconAutoArrange.jsx

View workflow job for this annotation

GitHub Actions / build (20.x)

'React' is defined but never used

export default function IconAutoArrange() {
return (
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="3" y="3" width="7" height="7" />
<rect x="14" y="3" width="7" height="7" />
<rect x="14" y="14" width="7" height="7" />
<rect x="3" y="14" width="7" height="7" />
<path d="M8 8l8 8M16 8l-8 8" />
</svg>
);
}
1 change: 1 addition & 0 deletions src/icons/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as IconAddTable } from "./IconAddTable";
export { default as IconAddArea } from "./IconAddArea";
export { default as IconAddNote } from "./IconAddNote";
export { default as IconAutoArrange } from "./IconAutoArrange";
206 changes: 201 additions & 5 deletions src/utils/arrangeTables.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,208 @@
tableColorStripHeight,
tableFieldHeight,
tableHeaderHeight,
tableWidth,
} from "../data/constants";

export function arrangeTables(diagram) {
// Force-directed layout algorithm
function forceDirectedLayout(tables, relationships, options = {}) {
const {
iterations = 100,
repulsionStrength = 5000,
attractionStrength = 0.001,
centerForce = 0.01,
minDistance = 250
} = options;

// Initialize positions if not set
tables.forEach((table, i) => {

Check failure on line 19 in src/utils/arrangeTables.js

View workflow job for this annotation

GitHub Actions / build (20.x)

'i' is defined but never used
if (table.x === undefined) table.x = Math.random() * 800;
if (table.y === undefined) table.y = Math.random() * 600;
});

// Build adjacency list
const adjacency = {};
relationships.forEach(rel => {
const { startTableId, endTableId } = rel;
if (!adjacency[startTableId]) adjacency[startTableId] = [];
if (!adjacency[endTableId]) adjacency[endTableId] = [];
adjacency[startTableId].push(endTableId);
adjacency[endTableId].push(startTableId);
});

// Force simulation
for (let iter = 0; iter < iterations; iter++) {
const forces = {};

// Initialize forces
tables.forEach(table => {
forces[table.id] = { x: 0, y: 0 };
});

// Repulsion between all nodes
for (let i = 0; i < tables.length; i++) {
for (let j = i + 1; j < tables.length; j++) {
const table1 = tables[i];
const table2 = tables[j];

const dx = table2.x - table1.x;
const dy = table2.y - table1.y;
const distance = Math.sqrt(dx * dx + dy * dy) || 1;

if (distance < minDistance) {
const force = repulsionStrength / (distance * distance);
const fx = (dx / distance) * force;
const fy = (dy / distance) * force;

forces[table1.id].x -= fx;
forces[table1.id].y -= fy;
forces[table2.id].x += fx;
forces[table2.id].y += fy;
}
}
}

// Attraction along edges
relationships.forEach(rel => {
const table1 = tables.find(t => t.id === rel.startTableId);
const table2 = tables.find(t => t.id === rel.endTableId);

if (table1 && table2) {
const dx = table2.x - table1.x;
const dy = table2.y - table1.y;
const distance = Math.sqrt(dx * dx + dy * dy) || 1;

const force = distance * attractionStrength;
const fx = (dx / distance) * force;
const fy = (dy / distance) * force;

forces[table1.id].x += fx;
forces[table1.id].y += fy;
forces[table2.id].x -= fx;
forces[table2.id].y -= fy;
}
});

// Center gravity
const centerX = 400;
const centerY = 300;

tables.forEach(table => {
const dx = centerX - table.x;
const dy = centerY - table.y;
forces[table.id].x += dx * centerForce;
forces[table.id].y += dy * centerForce;
});

// Apply forces
tables.forEach(table => {
table.x += forces[table.id].x;
table.y += forces[table.id].y;
});
}
}

// Hierarchical layout algorithm
function hierarchicalLayout(tables, relationships) {
// Build adjacency list
const adjacency = {};
const inDegree = {};

tables.forEach(table => {
adjacency[table.id] = [];
inDegree[table.id] = 0;
});

relationships.forEach(rel => {
adjacency[rel.startTableId].push(rel.endTableId);
inDegree[rel.endTableId]++;
});

// Topological sort to determine layers
const layers = [];
const visited = new Set();
const queue = [];

// Find nodes with no incoming edges
tables.forEach(table => {
if (inDegree[table.id] === 0) {
queue.push(table);
visited.add(table.id);
}
});

while (queue.length > 0) {
const currentLayer = [...queue];
queue.length = 0;
layers.push(currentLayer);

currentLayer.forEach(table => {
adjacency[table.id].forEach(neighborId => {
if (!visited.has(neighborId)) {
inDegree[neighborId]--;
if (inDegree[neighborId] === 0) {
const neighbor = tables.find(t => t.id === neighborId);
if (neighbor) {
queue.push(neighbor);
visited.add(neighborId);
}
}
}
});
});
}

// Add remaining nodes (in case of cycles)
tables.forEach(table => {
if (!visited.has(table.id)) {
if (!layers[layers.length - 1]) layers.push([]);
layers[layers.length - 1].push(table);
}
});

// Position nodes
const verticalGap = 200;
const horizontalGap = tableWidth + 50;

layers.forEach((layer, layerIndex) => {
const layerWidth = layer.length * horizontalGap;
const startX = (800 - layerWidth) / 2;

layer.forEach((table, tableIndex) => {
table.x = startX + tableIndex * horizontalGap;
table.y = 50 + layerIndex * verticalGap;
});
});
}

// Enhanced arrange function with algorithm selection
export function arrangeTables(diagram, algorithm = 'force-directed') {
const { tables, relationships } = diagram;

if (tables.length === 0) return;

switch (algorithm) {
case 'force-directed':
forceDirectedLayout(tables, relationships);
break;
case 'hierarchical':
hierarchicalLayout(tables, relationships);
break;
default:
// Simple grid layout as fallback
simpleGridLayout(tables);
break;
}
}

// Keep original simple layout as fallback
function simpleGridLayout(tables) {
let maxHeight = -1;
const tableWidth = 200;
const gapX = 54;
const gapY = 40;
diagram.tables.forEach((table, i) => {
if (i < diagram.tables.length / 2) {

tables.forEach((table, i) => {
if (i < tables.length / 2) {
table.x = i * tableWidth + (i + 1) * gapX;
table.y = gapY;
const height =
Expand All @@ -19,9 +212,12 @@
tableColorStripHeight;
maxHeight = Math.max(height, maxHeight);
} else {
const index = diagram.tables.length - i - 1;
const index = tables.length - i - 1;
table.x = index * tableWidth + (index + 1) * gapX;
table.y = maxHeight + 2 * gapY;
}
});
}

// Export individual algorithms for direct use
export { forceDirectedLayout, hierarchicalLayout };
Loading