Skip to content

Commit 5a240a5

Browse files
committed
feat(Masonry): [tower] added utilities for towers to get parent children bounding boxes
1 parent f54d809 commit 5a240a5

File tree

3 files changed

+353
-200
lines changed

3 files changed

+353
-200
lines changed

modules/masonry/src/brick/model/model.ts

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import type { TConnectionPoints as TCP } from '../../tree/model/model';
1616
import { generateBrickData } from '../utils/path';
1717
import type { TInputUnion } from '../utils/path';
1818
import { getLabelWidth } from '../utils/textMeasurement';
19+
import type { ExtendedTowerNode } from '../../tower/view/components/TowerView';
20+
import { calculateCompleteSubtreeDimensions } from '../../tower/utils/towerUtils';
1921

2022
export abstract class BrickModel implements IBrick {
2123
protected _uuid: string;
@@ -386,34 +388,54 @@ export default class CompoundBrick extends BrickModel implements IBrickCompound
386388
public setBoundingBoxNest(extents: TExtent[]): void {
387389
this._bboxNest = extents;
388390
}
389-
391+
390392
/**
391393
* Recursively update bounding box and connection points to fit nested children.
392394
* Call this after all children are attached, before rendering.
393395
*/
394-
public updateLayoutWithChildren(nestedChildren: BrickModel[]): void {
395-
// If there are nested children, calculate the total bounding box
396+
public updateLayoutWithChildren(nestedChildren: BrickModel[], allNodes: Map<string, ExtendedTowerNode>): void {
396397
if (nestedChildren && nestedChildren.length > 0) {
397-
// Calculate the bounding box that fits all nested children
398-
let _minX = 0,
399-
_minY = 0,
400-
maxX = 0,
401-
maxY = 0;
402-
nestedChildren.forEach((child) => {
403-
const bbox = child.boundingBox;
404-
// For simplicity, assume children are stacked vertically for now
405-
maxY += bbox.h;
406-
maxX = Math.max(maxX, bbox.w);
407-
});
408-
// Expand this brick's bboxNest to fit the children
409-
this._bboxNest = [{ w: maxX, h: maxY }];
398+
let totalHeight = 0;
399+
let maxWidth = 0;
400+
401+
// Recursively calculate the total height and max width of all descendants
402+
const calculateSubtreeDimensions = (children: BrickModel[]): { h: number; w: number } => {
403+
let height = 0;
404+
let width = 0;
405+
children.forEach(child => {
406+
const childNode = Array.from(allNodes.values()).find(n => n.brick.uuid === child.uuid);
407+
if (childNode) {
408+
const { w, h } = calculateCompleteSubtreeDimensions(childNode.brick.uuid, allNodes, new Map<string, { w: number; h: number }>());
409+
height += h;
410+
width = Math.max(width, w);
411+
}
412+
});
413+
return { h: height, w: width };
414+
};
415+
416+
const { h, w } = calculateSubtreeDimensions(nestedChildren);
417+
totalHeight = h;
418+
maxWidth = w;
419+
420+
// Update bboxNest to reflect the full subtree dimensions
421+
this._bboxNest = [{ w: maxWidth, h: totalHeight }];
410422
} else {
411423
this._bboxNest = [];
412424
}
413-
// Update geometry with new bboxNest
414425
this.updateGeometry();
415426
}
416427

428+
// Helper method to get nested children based on the tower structure
429+
public getNestedChildren(allNodes: Map<string, ExtendedTowerNode>): BrickModel[] {
430+
const children: BrickModel[] = [];
431+
allNodes.forEach(node => {
432+
if (node.parent?.brick.uuid === this.uuid && node.isNested) {
433+
children.push(node.brick as BrickModel); // Cast IBrick to BrickModel
434+
}
435+
});
436+
return children;
437+
}
438+
417439
public override get renderProps(): TBrickRenderPropsCompound {
418440
return {
419441
...this.getCommonRenderProps(),
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import type { ExtendedTowerNode } from '../view/components/TowerView';
2+
import CompoundBrick from '../../brick/model/model';
3+
4+
// Helper function to get children of a node from the tower structure
5+
export function getNodeChildren(
6+
nodeId: string,
7+
allNodes: Map<string, ExtendedTowerNode>,
8+
): {
9+
nested: ExtendedTowerNode[];
10+
args: ExtendedTowerNode[];
11+
stacked: ExtendedTowerNode[];
12+
} {
13+
const nested: ExtendedTowerNode[] = [];
14+
const args: ExtendedTowerNode[] = [];
15+
const stacked: ExtendedTowerNode[] = [];
16+
17+
for (const [_, node] of allNodes) {
18+
if (node.parent?.brick.uuid === nodeId) {
19+
if (node.isNested) {
20+
nested.push(node);
21+
} else if (node.argIndex !== undefined) {
22+
args.push(node);
23+
} else {
24+
stacked.push(node);
25+
}
26+
}
27+
}
28+
29+
args.sort((a, b) => (a.argIndex || 0) - (b.argIndex || 0));
30+
return { nested, args, stacked };
31+
}
32+
33+
// Helper function to get all descendants of a node
34+
export function getAllDescendants(
35+
nodeId: string,
36+
allNodes: Map<string, ExtendedTowerNode>,
37+
): ExtendedTowerNode[] {
38+
const descendants: ExtendedTowerNode[] = [];
39+
40+
const gatherDescendants = (currentNodeId: string) => {
41+
allNodes.forEach(childNode => {
42+
if (childNode.parent && childNode.parent.brick.uuid === currentNodeId) {
43+
descendants.push(childNode);
44+
gatherDescendants(childNode.brick.uuid);
45+
}
46+
});
47+
};
48+
49+
gatherDescendants(nodeId);
50+
return descendants;
51+
}
52+
53+
// Calculate nested area including all descendants within the nested region
54+
export function calculateNestedAreaDimensions(
55+
compoundNodeId: string,
56+
allNodes: Map<string, ExtendedTowerNode>,
57+
memoMap: Map<string, { w: number; h: number }>,
58+
): { w: number; h: number } {
59+
const cacheKey = `nested_${compoundNodeId}`;
60+
if (memoMap.has(cacheKey)) {
61+
return memoMap.get(cacheKey)!;
62+
}
63+
64+
const compoundNode = allNodes.get(compoundNodeId);
65+
if (!compoundNode) {
66+
const result = { w: 0, h: 0 };
67+
memoMap.set(cacheKey, result);
68+
return result;
69+
}
70+
71+
const children = getNodeChildren(compoundNodeId, allNodes);
72+
73+
if (children.nested.length === 0) {
74+
const result = { w: 0, h: 0 };
75+
memoMap.set(cacheKey, result);
76+
return result;
77+
}
78+
79+
let totalNestedHeight = 0;
80+
let totalNestedWidth = 0;
81+
82+
// Recursively process all nested children and their descendants
83+
const processNestedChain = (nodeId: string) => {
84+
const node = allNodes.get(nodeId);
85+
if (!node) return;
86+
87+
const { h, w } = calculateCompleteSubtreeDimensions(nodeId, allNodes, memoMap);
88+
totalNestedHeight += h;
89+
totalNestedWidth = Math.max(totalNestedWidth, w);
90+
91+
const childChildren = getNodeChildren(nodeId, allNodes);
92+
childChildren.nested.forEach(child => processNestedChain(child.brick.uuid));
93+
childChildren.stacked.forEach(child => processNestedChain(child.brick.uuid)); // Include stacked chains as part of nested area
94+
};
95+
96+
children.nested.forEach(nestedChild => {
97+
processNestedChain(nestedChild.brick.uuid);
98+
});
99+
100+
const result = { w: totalNestedWidth, h: totalNestedHeight };
101+
memoMap.set(cacheKey, result);
102+
return result;
103+
}
104+
105+
// Calculate complete subtree dimensions WITHOUT calling updateLayoutWithChildren
106+
export function calculateCompleteSubtreeDimensions(
107+
rootNodeId: string,
108+
allNodes: Map<string, ExtendedTowerNode>,
109+
memoMap: Map<string, { w: number; h: number }>,
110+
): { w: number; h: number } {
111+
if (memoMap.has(rootNodeId)) {
112+
return memoMap.get(rootNodeId)!;
113+
}
114+
115+
const rootNode = allNodes.get(rootNodeId);
116+
if (!rootNode) {
117+
const result = { w: 0, h: 0 };
118+
memoMap.set(rootNodeId, result);
119+
return result;
120+
}
121+
122+
const children = getNodeChildren(rootNodeId, allNodes);
123+
124+
let totalWidth = rootNode.brick.boundingBox.w || 0; // Fallback to 0 if undefined
125+
let totalHeight = rootNode.brick.boundingBox.h || 0; // Fallback to 0 if undefined
126+
127+
// Handle nested children
128+
if (children.nested.length > 0 && rootNode.brick.connectionPoints?.nested) {
129+
const nestedAreaDims = calculateNestedAreaDimensions(rootNodeId, allNodes, memoMap);
130+
const nestedPoint = rootNode.brick.connectionPoints.nested;
131+
totalHeight = Math.max(totalHeight, nestedPoint.y + nestedAreaDims.h);
132+
totalWidth = Math.max(totalWidth, nestedPoint.x + nestedAreaDims.w);
133+
}
134+
135+
// Handle argument children
136+
if (children.args.length > 0 && rootNode.brick.connectionPoints?.args) {
137+
children.args.forEach(argChild => {
138+
const argIndex = argChild.argIndex || 0;
139+
if (argIndex < rootNode.brick.connectionPoints.args!.length) {
140+
const argPoint = rootNode.brick.connectionPoints.args![argIndex];
141+
const argSubtreeDims = calculateCompleteSubtreeDimensions(argChild.brick.uuid, allNodes, memoMap);
142+
totalWidth = Math.max(totalWidth, argPoint.x + argSubtreeDims.w);
143+
totalHeight = Math.max(totalHeight, argPoint.y + argSubtreeDims.h);
144+
}
145+
});
146+
}
147+
148+
// Handle stacked children
149+
if (children.stacked.length > 0) {
150+
let totalStackedHeight = 0;
151+
let maxStackedWidth = 0;
152+
children.stacked.forEach(stackedChild => {
153+
const stackedSubtreeDims = calculateCompleteSubtreeDimensions(stackedChild.brick.uuid, allNodes, memoMap);
154+
totalStackedHeight += stackedSubtreeDims.h;
155+
maxStackedWidth = Math.max(maxStackedWidth, stackedSubtreeDims.w);
156+
});
157+
totalHeight += totalStackedHeight;
158+
totalWidth = Math.max(totalWidth, maxStackedWidth);
159+
}
160+
161+
// Handle nested chains under simple bricks
162+
if (rootNode.brick.type === 'Simple' && children.nested.length > 0) {
163+
let nestedHeight = 0;
164+
let nestedWidth = 0;
165+
children.nested.forEach(nestedChild => {
166+
const nestedSubtreeDims = calculateCompleteSubtreeDimensions(nestedChild.brick.uuid, allNodes, memoMap);
167+
nestedHeight += nestedSubtreeDims.h;
168+
nestedWidth = Math.max(nestedWidth, nestedSubtreeDims.w);
169+
});
170+
totalHeight += nestedHeight;
171+
totalWidth = Math.max(totalWidth, nestedWidth);
172+
}
173+
174+
const result = { w: totalWidth, h: totalHeight };
175+
memoMap.set(rootNodeId, result);
176+
return result;
177+
}
178+
179+
export function computeBoundingBoxes(
180+
allNodes: Map<string, ExtendedTowerNode>,
181+
): Map<string, { w: number; h: number }> {
182+
const bbMap = new Map<string, { w: number; h: number }>();
183+
const memoMap = new Map<string, { w: number; h: number }>();
184+
185+
allNodes.forEach((node, nodeId) => {
186+
const subtreeDims = calculateCompleteSubtreeDimensions(nodeId, allNodes, memoMap);
187+
bbMap.set(nodeId, subtreeDims);
188+
});
189+
190+
return bbMap;
191+
}
192+
193+
// Enhanced debug function to show the exact problem
194+
export function debugBoundingBoxCalculation(
195+
allNodes: Map<string, ExtendedTowerNode>,
196+
bbMap: Map<string, { w: number; h: number }>,
197+
): void {
198+
if ((window as any).__debugBoundingBoxRan) return;
199+
(window as any).__debugBoundingBoxRan = true;
200+
201+
console.log('=== DETAILED BOUNDING BOX DEBUG ===');
202+
203+
const compoundBricksWithNested = Array.from(allNodes.values()).filter(node =>
204+
node.brick instanceof CompoundBrick &&
205+
getNodeChildren(node.brick.uuid, allNodes).nested.length > 0
206+
);
207+
208+
compoundBricksWithNested.forEach((node) => {
209+
const nodeId = node.brick.uuid;
210+
const children = getNodeChildren(nodeId, allNodes);
211+
const bb = bbMap.get(nodeId);
212+
const memoMap = new Map<string, { w: number; h: number }>();
213+
const nestedAreaDims = calculateNestedAreaDimensions(nodeId, allNodes, memoMap);
214+
215+
const brickLabel = node.brick.name || node.brick.type || 'Unknown';
216+
217+
console.log(`\nANALYZING "${brickLabel}" (${node.brick.type})`);
218+
console.log(` Original brick: ${node.brick.boundingBox.w}×${node.brick.boundingBox.h}`);
219+
console.log(` Calculated total: ${bb?.w || 0}×${bb?.h || 0}`);
220+
221+
if (node.brick.connectionPoints.nested) {
222+
const nestedPoint = node.brick.connectionPoints.nested;
223+
console.log(` Nested connection point: (${nestedPoint.x}, ${nestedPoint.y})`);
224+
console.log(` Nested area needed: ${nestedAreaDims.w}×${nestedAreaDims.h}`);
225+
console.log(` Calculation: ${nestedPoint.y} + ${nestedAreaDims.h} = ${nestedPoint.y + nestedAreaDims.h}`);
226+
227+
const expectedHeight = Math.max(node.brick.boundingBox.h, nestedPoint.y + nestedAreaDims.h);
228+
const actualHeight = bb?.h || 0;
229+
230+
console.log(` Expected final height: ${expectedHeight}`);
231+
console.log(` ${actualHeight === expectedHeight ? 'Pass' : 'Failed'} Actual final height: ${actualHeight}`);
232+
233+
if (Math.abs(expectedHeight - actualHeight) > 1) {
234+
console.log(` MISMATCH DETECTED: ${Math.abs(expectedHeight - actualHeight)} pixels difference`);
235+
}
236+
}
237+
238+
console.log(` Nested children details:`);
239+
children.nested.forEach((child, index) => {
240+
const childBB = bbMap.get(child.brick.uuid);
241+
const childLabel = child.brick.name || child.brick.type || 'Unknown';
242+
const childOriginal = child.brick.boundingBox;
243+
244+
console.log(` ${index + 1}. "${childLabel}":`);
245+
console.log(` Original: ${childOriginal.w}×${childOriginal.h}`);
246+
console.log(` Calculated: ${childBB?.w || 0}×${childBB?.h || 0}`);
247+
});
248+
});
249+
250+
console.log('\n=== END DETAILED DEBUG ===\n');
251+
252+
setTimeout(() => {
253+
(window as any).__debugBoundingBoxRan = false;
254+
}, 5000);
255+
}

0 commit comments

Comments
 (0)