Tidy-tree getting closer, now algorithm works

This commit is contained in:
Knut Sveidqvist
2025-06-11 20:58:54 +02:00
parent d2463f41b5
commit c13ce2a5c0
5 changed files with 107 additions and 59 deletions

View File

@@ -114,11 +114,14 @@
root((mindmap))
Origins
Tools
Pen and paper
Mermaid
e
Third
Forth
a
b
</pre>
<pre id="diagram4" class="mermaid">
<pre id="diagram4" class="mermaid2">
---
config:
layout: elk

View File

@@ -83,7 +83,7 @@
"@vitest/spy": "^3.0.6",
"@vitest/ui": "^3.0.6",
"ajv": "^8.17.1",
"chokidar": "^4.0.3",
"chokidar": "^3.6.0",
"concurrently": "^9.1.2",
"cors": "^2.8.5",
"cpy-cli": "^5.0.0",

View File

@@ -106,7 +106,7 @@
"@types/stylis": "^4.2.7",
"@types/uuid": "^10.0.0",
"ajv": "^8.17.1",
"chokidar": "^4.0.3",
"chokidar": "^3.6.0",
"concurrently": "^9.1.2",
"csstree-validator": "^4.0.1",
"globby": "^14.0.2",

View File

@@ -47,12 +47,14 @@ export function executeTidyTreeLayout(
let rightBoundingBox = null;
if (leftTree) {
console.log('combineAndPositionTrees leftTree', JSON.stringify(leftTree));
const leftLayoutResult = layout.layout(leftTree);
leftResult = leftLayoutResult.result;
leftBoundingBox = leftLayoutResult.boundingBox;
}
if (rightTree) {
console.log('combineAndPositionTrees leftTree', JSON.stringify(rightTree));
const rightLayoutResult = layout.layout(rightTree);
rightResult = rightLayoutResult.result;
rightBoundingBox = rightLayoutResult.boundingBox;
@@ -64,7 +66,8 @@ export function executeTidyTreeLayout(
leftResult,
rightResult,
leftBoundingBox,
rightBoundingBox
rightBoundingBox,
data
);
const positionedEdges = calculateEdgePositions(data.edges, positionedNodes);
@@ -167,6 +170,7 @@ function convertToDualTreeFormat(data: LayoutData): {
/**
* Build a subtree from a list of root children
* For horizontal trees, we need to transpose width/height since the tree will be rotated 90°
*/
function buildSubTree(
rootChildren: string[],
@@ -181,7 +185,7 @@ function buildSubTree(
children: rootChildren
.map((childId) => nodeMap.get(childId))
.filter((child): child is Node => child !== undefined)
.map((child) => convertNodeToTidyTree(child, children, nodeMap)),
.map((child) => convertNodeToTidyTreeTransposed(child, children, nodeMap)),
};
return virtualRoot;
@@ -189,8 +193,9 @@ function buildSubTree(
/**
* Recursively convert a node and its children to tidy-tree format
* This version transposes width/height for horizontal tree layout
*/
function convertNodeToTidyTree(
function convertNodeToTidyTreeTransposed(
node: Node,
children: Map<string, string[]>,
nodeMap: Map<string, Node>
@@ -199,12 +204,15 @@ function convertNodeToTidyTree(
const childNodes = childIds
.map((childId) => nodeMap.get(childId))
.filter((child): child is Node => child !== undefined)
.map((child) => convertNodeToTidyTree(child, children, nodeMap));
.map((child) => convertNodeToTidyTreeTransposed(child, children, nodeMap));
// Transpose width and height for horizontal layout
// When tree grows horizontally, the original width becomes the height in the layout
// and the original height becomes the width in the layout
return {
id: node.id,
width: node.width || 100,
height: node.height || 50,
width: node.height || 50, // Original height becomes layout width
height: node.width || 100, // Original width becomes layout height
children: childNodes.length > 0 ? childNodes : undefined,
// Store original node data for later use
_originalNode: node,
@@ -219,40 +227,88 @@ function combineAndPositionTrees(
rootNode: TidyTreeNode,
leftResult: TidyTreeNode | null,
rightResult: TidyTreeNode | null,
leftBoundingBox: any,
rightBoundingBox: any
_leftBoundingBox: any,
_rightBoundingBox: any,
_data: LayoutData
): PositionedNode[] {
const positionedNodes: PositionedNode[] = [];
console.log('combineAndPositionTrees', {
leftResult,
rightResult,
});
// Calculate root position (center of the layout)
const rootX = 0;
const rootY = 0;
// Position the root node
positionedNodes.push({
id: rootNode.id,
x: rootX,
y: rootY,
});
// Calculate spacing between trees
const treeSpacing = 150; // Horizontal spacing from root to tree
// First, collect node positions for each tree separately
const leftTreeNodes: PositionedNode[] = [];
const rightTreeNodes: PositionedNode[] = [];
// Position left tree (grows to the left)
if (leftResult && leftResult.children) {
positionLeftTreeBidirectional(leftResult.children, positionedNodes, rootX - treeSpacing, rootY);
if (leftResult?.children) {
positionLeftTreeBidirectional(leftResult.children, leftTreeNodes, rootX - treeSpacing, rootY);
}
// Position right tree (grows to the right)
if (rightResult && rightResult.children) {
if (rightResult?.children) {
positionRightTreeBidirectional(
rightResult.children,
positionedNodes,
rightTreeNodes,
rootX + treeSpacing,
rootY
);
}
// Calculate center points for each tree separately
let leftTreeCenterY = 0;
let rightTreeCenterY = 0;
if (leftTreeNodes.length > 0) {
const leftMinY = Math.min(...leftTreeNodes.map((node) => node.y));
const leftMaxY = Math.max(...leftTreeNodes.map((node) => node.y));
leftTreeCenterY = (leftMinY + leftMaxY) / 2;
}
if (rightTreeNodes.length > 0) {
const rightMinY = Math.min(...rightTreeNodes.map((node) => node.y));
const rightMaxY = Math.max(...rightTreeNodes.map((node) => node.y));
rightTreeCenterY = (rightMinY + rightMaxY) / 2;
}
// Calculate different offsets for each tree to center them around the root
const leftTreeOffset = -leftTreeCenterY;
const rightTreeOffset = -rightTreeCenterY;
// Add the centered root
positionedNodes.push({
id: String(rootNode.id),
x: rootX,
y: rootY, // Root stays at center
});
// Add left tree nodes with their specific offset
leftTreeNodes.forEach((node) => {
positionedNodes.push({
id: node.id,
x: node.x,
y: node.y + leftTreeOffset,
});
});
// Add right tree nodes with their specific offset
rightTreeNodes.forEach((node) => {
positionedNodes.push({
id: node.id,
x: node.x,
y: node.y + rightTreeOffset,
});
});
return positionedNodes;
}
@@ -267,15 +323,16 @@ function positionLeftTreeBidirectional(
offsetY: number
): void {
nodes.forEach((node) => {
// Rotate 90 degrees counterclockwise: (x,y) -> (-y, x)
// Then mirror horizontally for left growth: (-y, x) -> (y, x)
const rotatedX = node.y || 0; // Use y as new x (grows left)
const rotatedY = node.x || 0; // Use x as new y (vertical spread)
// For left tree: transpose the tidy-tree coordinates
// Tidy-tree Y becomes our X distance from root (grows left)
// Tidy-tree X becomes our Y position (tree levels) - this gives proper spacing
const distanceFromRoot = node.y ?? 0; // How far left from root
const treeLevel = node.x ?? 0; // Use X coordinate for tree level (proper vertical spacing)
positionedNodes.push({
id: node.id,
x: offsetX - rotatedX, // Negative to grow left from root
y: offsetY + rotatedY,
id: String(node.id),
x: offsetX - distanceFromRoot, // Negative to grow left from root
y: offsetY + treeLevel, // Use tidy-tree's Y as Y (tree levels)
});
if (node.children) {
@@ -295,14 +352,16 @@ function positionRightTreeBidirectional(
offsetY: number
): void {
nodes.forEach((node) => {
// Rotate 90 degrees clockwise: (x,y) -> (y, -x)
const rotatedX = node.y || 0; // Use y as new x (grows right)
const rotatedY = -(node.x || 0); // Use -x as new y (vertical spread)
// For right tree: transpose the tidy-tree coordinates
// Tidy-tree Y becomes our X distance from root (grows right)
// Tidy-tree X becomes our Y position (tree levels) - this gives proper spacing
const distanceFromRoot = node.y ?? 0; // How far right from root
const treeLevel = node.x ?? 0; // Use X coordinate for tree level (proper vertical spacing)
positionedNodes.push({
id: node.id,
x: offsetX + rotatedX, // Positive to grow right from root
y: offsetY + rotatedY,
id: String(node.id),
x: offsetX + distanceFromRoot, // Positive to grow right from root
y: offsetY + treeLevel, // Use tidy-tree's Y as Y (tree levels)
});
if (node.children) {
@@ -324,8 +383,8 @@ function calculateEdgePositions(
});
return edges.map((edge) => {
const startPos = nodePositions.get(edge.start) || { x: 0, y: 0 };
const endPos = nodePositions.get(edge.end) || { x: 0, y: 0 };
const startPos = nodePositions.get(edge.start || '') || { x: 0, y: 0 };
const endPos = nodePositions.get(edge.end || '') || { x: 0, y: 0 };
// Calculate midpoint for edge
const midX = (startPos.x + endPos.x) / 2;
@@ -333,8 +392,8 @@ function calculateEdgePositions(
return {
id: edge.id,
source: edge.start,
target: edge.end,
source: edge.start || '',
target: edge.end || '',
startX: startPos.x,
startY: startPos.y,
midX,

22
pnpm-lock.yaml generated
View File

@@ -74,8 +74,8 @@ importers:
specifier: ^8.17.1
version: 8.17.1
chokidar:
specifier: ^4.0.3
version: 4.0.3
specifier: ^3.6.0
version: 3.6.0
concurrently:
specifier: ^9.1.2
version: 9.1.2
@@ -330,8 +330,8 @@ importers:
specifier: ^8.17.1
version: 8.17.1
chokidar:
specifier: ^4.0.3
version: 4.0.3
specifier: ^3.6.0
version: 3.6.0
concurrently:
specifier: ^9.1.2
version: 9.1.2
@@ -4550,10 +4550,6 @@ packages:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
chrome-trace-event@1.0.4:
resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==}
engines: {node: '>=6.0'}
@@ -8417,10 +8413,6 @@ packages:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
readdirp@4.1.2:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
real-require@0.2.0:
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
engines: {node: '>= 12.13.0'}
@@ -15448,10 +15440,6 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
chokidar@4.0.3:
dependencies:
readdirp: 4.1.2
chrome-trace-event@1.0.4: {}
ci-info@3.9.0: {}
@@ -20160,8 +20148,6 @@ snapshots:
dependencies:
picomatch: 2.3.1
readdirp@4.1.2: {}
real-require@0.2.0: {}
rechoir@0.6.2: