|
| 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