From c37cc2f8812ba25e5758962a490cc0a77d678d46 Mon Sep 17 00:00:00 2001 From: saumyashahi Date: Fri, 20 Jun 2025 19:31:42 +0530 Subject: [PATCH 01/19] feat(masonry): unify path and bounding box logic in generateBrickData --- modules/masonry/src/brick/utils/path.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/modules/masonry/src/brick/utils/path.ts b/modules/masonry/src/brick/utils/path.ts index 77ee127e..5c2d9452 100644 --- a/modules/masonry/src/brick/utils/path.ts +++ b/modules/masonry/src/brick/utils/path.ts @@ -342,7 +342,7 @@ function _generateBottom(config: { // ----------------------------------------------------------- // Main function to generate the path based on the configuration -export function generatePath(config: TInputType1 | TInputType2 | TInputType3): { +function generatePath(config: TInputType1 | TInputType2 | TInputType3): { path: string; } { const hasNotchTop = config.type !== 'type2' && config.hasNotchAbove; @@ -401,7 +401,7 @@ export function generatePath(config: TInputType1 | TInputType2 | TInputType3): { // function to calculate the bounding box values -export function getBoundingBox(config: TInputUnion): TBBox { +function getBoundingBox(config: TInputUnion): TBBox { const { strokeWidth, bBoxLabel, @@ -468,3 +468,18 @@ export function getBoundingBox(config: TInputUnion): TBBox { h: height, }; } + + +// single export function to return brick data +export function generateBrickData(config: TInputType1 | TInputType2 | TInputType3): { + path: string; + boundingBox: TBBox; +} { + const {path} = generatePath(config); + const boundingBox = getBoundingBox(config); + + return { + path, + boundingBox, + }; +} From cd82bc33fabb8ec2cb233968d0728173bafda27a Mon Sep 17 00:00:00 2001 From: saumyashahi Date: Fri, 20 Jun 2025 19:33:47 +0530 Subject: [PATCH 02/19] test(masonry): update tests to match unified generateBrickData output --- .../masonry/src/brick/utils/spec/path.spec.ts | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/modules/masonry/src/brick/utils/spec/path.spec.ts b/modules/masonry/src/brick/utils/spec/path.spec.ts index 3e1285d5..15e365c9 100644 --- a/modules/masonry/src/brick/utils/spec/path.spec.ts +++ b/modules/masonry/src/brick/utils/spec/path.spec.ts @@ -1,4 +1,4 @@ -import { generatePath } from '../path'; +import { generateBrickData } from '../path'; import type { TInputUnion } from '../path'; describe('Masonry: Brick > Path Generation', () => { @@ -118,10 +118,22 @@ describe('Masonry: Brick > Path Generation', () => { ]; testCases.forEach(({ name, input }) => { - it(`generates path correctly: ${name}`, () => { - const result = generatePath(input as TInputUnion); - expect(typeof result.path).toBe('string'); - expect(result.path.length).toBeGreaterThan(0); + it(`generates path and bounding box correctly: ${name}`, () => { + const { path, boundingBox } = generateBrickData(input as TInputUnion); + + // Validate the path + expect(typeof path).toBe('string'); + expect(path.length).toBeGreaterThan(0); + + // Validate the bounding box + expect(typeof boundingBox).toBe('object'); + expect(boundingBox).toHaveProperty('w'); + expect(boundingBox).toHaveProperty('h'); + expect(typeof boundingBox.w).toBe('number'); + expect(typeof boundingBox.h).toBe('number'); + expect(boundingBox.w).toBeGreaterThan(0); + expect(boundingBox.h).toBeGreaterThan(0); }); }); + }); From 9647e9d589158ce695a25e4557975933fb580967 Mon Sep 17 00:00:00 2001 From: saumyashahi Date: Fri, 20 Jun 2025 19:37:28 +0530 Subject: [PATCH 03/19] feat(masonry): add centroid calculation for top and bottom connection points --- modules/masonry/src/brick/utils/path.ts | 39 ++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/modules/masonry/src/brick/utils/path.ts b/modules/masonry/src/brick/utils/path.ts index 5c2d9452..9fbe0389 100644 --- a/modules/masonry/src/brick/utils/path.ts +++ b/modules/masonry/src/brick/utils/path.ts @@ -341,7 +341,7 @@ function _generateBottom(config: { // ----------------------------------------------------------- -// Main function to generate the path based on the configuration +// function to generate the path based on the configuration function generatePath(config: TInputType1 | TInputType2 | TInputType3): { path: string; } { @@ -469,6 +469,43 @@ function getBoundingBox(config: TInputUnion): TBBox { }; } +// functions to calculate coordinates of the connection points + +type TCentroid = { x: number; y: number }; + +// Centroid calculation for Top Notch +function getTopCentroid(config: TInputUnion): TCentroid | undefined { + if (config.type === 'type2' || !config.hasNotchAbove) return undefined; + + return { + x: CORNER_RADIUS + OFFSET_NOTCH_TOP + WIDTH_NOTCH_TOP / 2, + y: 1, + }; +} + + +// Centroid calculation for Bottom Notch +function getBottomCentroid( + config: TInputUnion, + boundingBox: TBBox, + leftEdge: number +): TCentroid | undefined { + if (config.type === 'type2' || !config.hasNotchBelow) return undefined; + + if (config.type !== 'type3') { + return { + x: CORNER_RADIUS + OFFSET_NOTCH_BOTTOM + WIDTH_NOTCH_BOTTOM / 2, + y: 1, + }; + } + + return { + x: + CORNER_RADIUS + OFFSET_NOTCH_TOP + WIDTH_NOTCH_TOP/2 + + OFFSET_NOTCH_BOTTOM + WIDTH_NOTCH_BOTTOM / 2, //used the logic for top notch centroid + y: CORNER_RADIUS + leftEdge + CORNER_RADIUS + 1, + }; +} // single export function to return brick data export function generateBrickData(config: TInputType1 | TInputType2 | TInputType3): { From 7c7fc54d454c3882fd34c5fd72e54806cef8ac4e Mon Sep 17 00:00:00 2001 From: saumyashahi Date: Fri, 20 Jun 2025 20:51:13 +0530 Subject: [PATCH 04/19] feat(masonry): add centroid calculation for right and left connection points --- modules/masonry/src/brick/utils/path.ts | 71 +++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/modules/masonry/src/brick/utils/path.ts b/modules/masonry/src/brick/utils/path.ts index 9fbe0389..ff1b066e 100644 --- a/modules/masonry/src/brick/utils/path.ts +++ b/modules/masonry/src/brick/utils/path.ts @@ -507,6 +507,77 @@ function getBottomCentroid( }; } + +// Calculate centroids for right connector notch +function getRightCentroids(config: TInputUnion, boundingBox: TBBox): TCentroid[] { + const { bBoxArgs, bBoxLabel, strokeWidth } = config; + + // No argument = no right notches + if (!bBoxArgs.length) return []; + + const centroids: TCentroid[] = []; + + const labelHeight = Math.max(MIN_LABEL_HEIGHT, bBoxLabel.h); + const requiredMinimum = strokeWidth / 2 + labelHeight + strokeWidth / 2; + const argHeightsSum = bBoxArgs.reduce((sum, arg) => sum + arg.h, 0); + + const extra = Math.max(0, requiredMinimum - argHeightsSum); + + let verticalOffset = CORNER_RADIUS; // top-right corner arc + + for (let i = 0; i < bBoxArgs.length; i++) { + const extraPerArg = extra / bBoxArgs.length; + const argBox = bBoxArgs[i]; + + // v4, v-3, v10, v-3, v4 = total 12 + const fixedNotchHeight = 12; + + const variableLength = Math.max( + 0, + argBox.h + extraPerArg - + ( + strokeWidth / 2 + + CORNER_RADIUS + + HEIGHT_NOTCH_RIGHT + + CORNER_RADIUS + + strokeWidth / 2 + + CORNER_RADIUS + strokeWidth + CORNER_RADIUS + ) + ); + + // Centroid placed in the middle of v10 + const centroidY = verticalOffset + 6; + + centroids.push({ + x: boundingBox.w - 6, // notch is 12 wide, so center is at 6 from right edge + y: centroidY, + }); + + verticalOffset += fixedNotchHeight; // 12 units fixed + + if (variableLength > 0) { + verticalOffset += variableLength; + } + + if (i < bBoxArgs.length - 1) { + verticalOffset += CORNER_RADIUS + strokeWidth + CORNER_RADIUS; // 4 + 2 + 4 = 10 units + } + } + + return centroids; +} + +// Calculate centroid for left connector notch +function getLeftCentroid(config: TInputUnion, boundingBox: TBBox): TCentroid | undefined { + if (config.type !== 'type2') return undefined; + + return { + x: -7, // notch is made of h -6, h -2 → center = -7 + y: CORNER_RADIUS + CONN_NOTCH_WIDTH / 2, + }; +} + + // single export function to return brick data export function generateBrickData(config: TInputType1 | TInputType2 | TInputType3): { path: string; From 6d415139ccc544e2626baf72c68c1b285a5ce5bf Mon Sep 17 00:00:00 2001 From: saumyashahi Date: Fri, 20 Jun 2025 21:00:42 +0530 Subject: [PATCH 05/19] feat(masonry): include connection points in generateBrickData output --- modules/masonry/src/brick/utils/path.ts | 42 ++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/modules/masonry/src/brick/utils/path.ts b/modules/masonry/src/brick/utils/path.ts index ff1b066e..8e53bfcb 100644 --- a/modules/masonry/src/brick/utils/path.ts +++ b/modules/masonry/src/brick/utils/path.ts @@ -191,7 +191,7 @@ function _generateLeft(config: { hasNotch: boolean; strokeWidth: number; rightVertical: number; -}): string[] { +}): { path: string[]; leftEdge: number } { const { type, hasNotch, rightVertical } = config; const path: string[] = []; @@ -210,7 +210,7 @@ function _generateLeft(config: { path.push(`v -${leftEdge.toFixed(2)}`); } - return path; + return { path, leftEdge }; } // function to generate the nested path for type3 bricks @@ -344,6 +344,7 @@ function _generateBottom(config: { // function to generate the path based on the configuration function generatePath(config: TInputType1 | TInputType2 | TInputType3): { path: string; + leftEdge: number; } { const hasNotchTop = config.type !== 'type2' && config.hasNotchAbove; @@ -385,17 +386,21 @@ function generatePath(config: TInputType1 | TInputType2 | TInputType3): { bBoxNesting: bBoxNesting, }); - const left = _generateLeft({ + const leftResult = _generateLeft({ type: config.type, hasNotch: true, strokeWidth: config.strokeWidth, - rightVertical: rightVertical, + rightVertical, }); + const left = leftResult.path; + const leftEdge = leftResult.leftEdge; + const segments = [...top, ...right, ...bottom, ...left]; return { path: ['M 0,0', ...segments].join(' '), + leftEdge: leftEdge, }; } @@ -578,16 +583,43 @@ function getLeftCentroid(config: TInputUnion, boundingBox: TBBox): TCentroid | u } +function getConnectionPoints( + config: TInputUnion, + boundingBox: TBBox, + leftEdge: number +): { + top?: TCentroid; + right: TCentroid[]; + bottom?: TCentroid; + left?: TCentroid; +} { + return { + top: getTopCentroid(config), + right: getRightCentroids(config, boundingBox), + bottom: getBottomCentroid(config, boundingBox, leftEdge), + left: getLeftCentroid(config, boundingBox), + }; +} + + // single export function to return brick data export function generateBrickData(config: TInputType1 | TInputType2 | TInputType3): { path: string; boundingBox: TBBox; + connectionPoints: { + top?: TCentroid; + right: TCentroid[]; + bottom?: TCentroid; + left?: TCentroid; + }; } { - const {path} = generatePath(config); + const { path, leftEdge } = generatePath(config); const boundingBox = getBoundingBox(config); + const connectionPoints = getConnectionPoints(config, boundingBox, leftEdge); return { path, boundingBox, + connectionPoints, }; } From 13fa25e4040d2bc28a8da21e518014229cc7e165 Mon Sep 17 00:00:00 2001 From: saumyashahi Date: Fri, 20 Jun 2025 21:03:21 +0530 Subject: [PATCH 06/19] test(masonry): validate connection points in brick data output --- .../masonry/src/brick/utils/spec/path.spec.ts | 52 ++++++++++++++----- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/modules/masonry/src/brick/utils/spec/path.spec.ts b/modules/masonry/src/brick/utils/spec/path.spec.ts index 15e365c9..8101dae6 100644 --- a/modules/masonry/src/brick/utils/spec/path.spec.ts +++ b/modules/masonry/src/brick/utils/spec/path.spec.ts @@ -118,22 +118,48 @@ describe('Masonry: Brick > Path Generation', () => { ]; testCases.forEach(({ name, input }) => { - it(`generates path and bounding box correctly: ${name}`, () => { - const { path, boundingBox } = generateBrickData(input as TInputUnion); + it(`generates path, bounding box, and connection points correctly: ${name}`, () => { + const { path, boundingBox, connectionPoints } = generateBrickData(input as TInputUnion); - // Validate the path - expect(typeof path).toBe('string'); - expect(path.length).toBeGreaterThan(0); + // Validate the path + expect(typeof path).toBe('string'); + expect(path.length).toBeGreaterThan(0); - // Validate the bounding box - expect(typeof boundingBox).toBe('object'); - expect(boundingBox).toHaveProperty('w'); - expect(boundingBox).toHaveProperty('h'); - expect(typeof boundingBox.w).toBe('number'); - expect(typeof boundingBox.h).toBe('number'); - expect(boundingBox.w).toBeGreaterThan(0); - expect(boundingBox.h).toBeGreaterThan(0); + // Validate the bounding box + expect(typeof boundingBox).toBe('object'); + expect(boundingBox).toHaveProperty('w'); + expect(boundingBox).toHaveProperty('h'); + expect(typeof boundingBox.w).toBe('number'); + expect(typeof boundingBox.h).toBe('number'); + expect(boundingBox.w).toBeGreaterThan(0); + expect(boundingBox.h).toBeGreaterThan(0); + + // Validate connection points + expect(connectionPoints).toHaveProperty('right'); + expect(Array.isArray(connectionPoints.right)).toBe(true); + connectionPoints.right.forEach((pt) => { + expect(pt).toHaveProperty('x'); + expect(pt).toHaveProperty('y'); + expect(typeof pt.x).toBe('number'); + expect(typeof pt.y).toBe('number'); }); + + if (connectionPoints.left) { + expect(typeof connectionPoints.left.x).toBe('number'); + expect(typeof connectionPoints.left.y).toBe('number'); + } + + if (connectionPoints.top) { + expect(typeof connectionPoints.top.x).toBe('number'); + expect(typeof connectionPoints.top.y).toBe('number'); + } + + if (connectionPoints.bottom) { + expect(typeof connectionPoints.bottom.x).toBe('number'); + expect(typeof connectionPoints.bottom.y).toBe('number'); + } + }); + }); }); From eb14c9bda8ec02bb8e3e11ac518d34f339b36559 Mon Sep 17 00:00:00 2001 From: Saumya Shahi Date: Sat, 21 Jun 2025 19:40:01 +0530 Subject: [PATCH 07/19] feat(masonry): add connectionPoints property to IBrick interface for connections --- modules/masonry/src/brick/@types/brick.d.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/masonry/src/brick/@types/brick.d.ts b/modules/masonry/src/brick/@types/brick.d.ts index 45ab4bd9..a098dd2f 100644 --- a/modules/masonry/src/brick/@types/brick.d.ts +++ b/modules/masonry/src/brick/@types/brick.d.ts @@ -65,6 +65,8 @@ export type TBrickRenderPropsCompound = TBrickRenderProps & { isFolded: boolean; }; +import { TConnectionPoints } from '../../tree/model/model'; + /** * @interface * Type definition of a brick (any type). @@ -75,6 +77,7 @@ export interface IBrick { get type(): TBrickType; set scale(value: number); get boundingBox(): TExtent; + connectionPoints: TConnectionPoints; get visualState(): TVisualState; set visualState(value: TVisualState); From 457b361b156f3c801d884fbfa97fb54969b488dc Mon Sep 17 00:00:00 2001 From: Saumya Shahi Date: Sat, 21 Jun 2025 19:46:15 +0530 Subject: [PATCH 08/19] feat(masonry): implement hierarchical brick connection logic for (top-bottom) and (right-left) connections --- modules/masonry/src/brick/model/model.ts | 97 +++++++++++++++--------- 1 file changed, 60 insertions(+), 37 deletions(-) diff --git a/modules/masonry/src/brick/model/model.ts b/modules/masonry/src/brick/model/model.ts index 3b150964..74c8f04d 100644 --- a/modules/masonry/src/brick/model/model.ts +++ b/modules/masonry/src/brick/model/model.ts @@ -12,6 +12,9 @@ import type { IBrickCompound, TBrickRenderPropsCompound, } from '../@types/brick'; +import type { TConnectionPoints } from '../../tree/model/model'; +import { generateBrickData } from '../utils/path'; +import type { TInputUnion } from '../utils/path'; export abstract class BrickModel implements IBrick { protected _uuid: string; @@ -30,6 +33,8 @@ export abstract class BrickModel implements IBrick { protected _visualState: TVisualState = 'default'; protected _isActionMenuOpen = false; protected _isVisible = true; + public connectionPoints: TConnectionPoints; + protected _boundingBox: TExtent = { w: 0, h: 0 }; constructor(params: { uuid: string; @@ -57,6 +62,7 @@ export abstract class BrickModel implements IBrick { this._shadow = params.shadow; this._tooltip = params.tooltip; this._bboxArgs = params.bboxArgs; + this.connectionPoints = { right: [] }; // Default init } // IBrick interface @@ -91,6 +97,10 @@ export abstract class BrickModel implements IBrick { this._isVisible = value; } + public get boundingBox(): TExtent { + return this._boundingBox; + } + protected getCommonRenderProps(): TBrickRenderProps { return { path: '', @@ -110,9 +120,6 @@ export abstract class BrickModel implements IBrick { }; } - /** Must compute the overall bounding box. */ - public abstract get boundingBox(): TExtent; - /** Must assemble the full render props for this brick. */ public abstract get renderProps(): TBrickRenderProps; } @@ -125,8 +132,6 @@ export class SimpleBrick extends BrickModel implements IBrickSimple { private _topNotch: boolean; private _bottomNotch: boolean; - private _boundingBox: TExtent = { w: 0, h: 0 }; - constructor(params: { uuid: string; name: string; @@ -138,7 +143,6 @@ export class SimpleBrick extends BrickModel implements IBrickSimple { shadow: boolean; tooltip?: string; scale: number; - bboxArgs: TExtent[]; topNotch: boolean; bottomNotch: boolean; @@ -160,6 +164,22 @@ export class SimpleBrick extends BrickModel implements IBrickSimple { this._topNotch = params.topNotch; this._bottomNotch = params.bottomNotch; + this.updateGeometry(); + } + + public updateGeometry() { + const config: TInputUnion = { + type: 'type1', + strokeWidth: this._strokeWidth, + scaleFactor: this._scale, + bBoxLabel: { w: this._label.length * 8, h: 20 }, + bBoxArgs: this._bboxArgs, + hasNotchAbove: this._topNotch, + hasNotchBelow: this._bottomNotch, + }; + const data = generateBrickData(config); + this.connectionPoints = data.connectionPoints; + this._boundingBox = data.boundingBox; } public get topNotch(): boolean { @@ -170,13 +190,6 @@ export class SimpleBrick extends BrickModel implements IBrickSimple { return this._bottomNotch; } - public get boundingBox(): TExtent { - return this._boundingBox; - } - public set boundingBox(box: TExtent) { - this._boundingBox = box; - } - public override get renderProps(): TBrickRenderPropsSimple { return { ...this.getCommonRenderProps(), @@ -194,8 +207,6 @@ export class ExpressionBrick extends BrickModel implements IBrickExpression { private _value?: boolean | number | string; private _isValueSelectOpen: boolean; - private _boundingBox: TExtent = { w: 0, h: 0 }; - constructor(params: { uuid: string; name: string; @@ -227,7 +238,21 @@ export class ExpressionBrick extends BrickModel implements IBrickExpression { }); this._value = params.value; - this._isValueSelectOpen = params.isValueSelectOpen ?? false; + this._isValueSelectOpen = params.isValueSelectOpen || false; + this.updateGeometry(); + } + + public updateGeometry() { + const config: TInputUnion = { + type: 'type2', + strokeWidth: this._strokeWidth, + scaleFactor: this._scale, + bBoxLabel: { w: this._label.length * 8, h: 20 }, + bBoxArgs: this._bboxArgs, + }; + const data = generateBrickData(config); + this.connectionPoints = data.connectionPoints; + this._boundingBox = data.boundingBox; } public get value(): boolean | number | string | undefined { @@ -241,13 +266,6 @@ export class ExpressionBrick extends BrickModel implements IBrickExpression { this._isValueSelectOpen = open; } - public get boundingBox(): TExtent { - return this._boundingBox; - } - public set boundingBox(box: TExtent) { - this._boundingBox = box; - } - public override get renderProps(): TBrickRenderPropsExpression { return { ...this.getCommonRenderProps(), @@ -267,8 +285,6 @@ export default class CompoundBrick extends BrickModel implements IBrickCompound private _isFolded: boolean; private _bboxNest: TExtent[]; - private _boundingBox: TExtent = { w: 0, h: 0 }; - constructor(params: { uuid: string; name: string; @@ -280,13 +296,9 @@ export default class CompoundBrick extends BrickModel implements IBrickCompound shadow: boolean; tooltip?: string; scale: number; - bboxArgs: TExtent[]; - bboxNest: TExtent[]; - isFolded?: boolean; - topNotch: boolean; bottomNotch: boolean; }) { @@ -308,7 +320,25 @@ export default class CompoundBrick extends BrickModel implements IBrickCompound this._topNotch = params.topNotch; this._bottomNotch = params.bottomNotch; this._bboxNest = params.bboxNest; - this._isFolded = params.isFolded ?? false; + this._isFolded = params.isFolded || false; + this.updateGeometry(); + } + + public updateGeometry() { + const config: TInputUnion = { + type: 'type3', + strokeWidth: this._strokeWidth, + scaleFactor: this._scale, + bBoxLabel: { w: this._label.length * 8, h: 20 }, + bBoxArgs: this._bboxArgs, + hasNotchAbove: this._topNotch, + hasNotchBelow: this._bottomNotch, + bBoxNesting: this._bboxNest, + secondaryLabel: true, + }; + const data = generateBrickData(config); + this.connectionPoints = data.connectionPoints; + this._boundingBox = data.boundingBox; } public get topNotch(): boolean { @@ -334,13 +364,6 @@ export default class CompoundBrick extends BrickModel implements IBrickCompound this._bboxNest = extents; } - public get boundingBox(): TExtent { - return this._boundingBox; - } - public set boundingBox(box: TExtent) { - this._boundingBox = box; - } - public override get renderProps(): TBrickRenderPropsCompound { return { ...this.getCommonRenderProps(), From b75bba680b77072715514279f5c97f75ba5baf35 Mon Sep 17 00:00:00 2001 From: Saumya Shahi Date: Sat, 21 Jun 2025 19:46:45 +0530 Subject: [PATCH 09/19] feat(masonry): update path generation utilities to support connection points --- modules/masonry/src/brick/utils/path.ts | 199 +++++++++--------- .../masonry/src/brick/utils/spec/path.spec.ts | 70 +++--- 2 files changed, 130 insertions(+), 139 deletions(-) diff --git a/modules/masonry/src/brick/utils/path.ts b/modules/masonry/src/brick/utils/path.ts index 8e53bfcb..e9bfece0 100644 --- a/modules/masonry/src/brick/utils/path.ts +++ b/modules/masonry/src/brick/utils/path.ts @@ -404,15 +404,9 @@ function generatePath(config: TInputType1 | TInputType2 | TInputType3): { }; } - -// function to calculate the bounding box values +// function to calculate the bounding box values function getBoundingBox(config: TInputUnion): TBBox { - const { - strokeWidth, - bBoxLabel, - bBoxArgs, - type, - } = config; + const { strokeWidth, bBoxLabel, bBoxArgs, type } = config; const hasArgs = bBoxArgs.length > 0; const labelWidth = Math.max(MIN_LABEL_WIDTH, bBoxLabel.w); @@ -428,12 +422,11 @@ function getBoundingBox(config: TInputUnion): TBBox { CORNER_RADIUS; // Base width as per _generateTop and _generateBottom logic - const baseWidth = - CORNER_RADIUS + OFFSET_NOTCH_TOP + WIDTH_NOTCH_TOP + variableTopWidth; + const baseWidth = CORNER_RADIUS + OFFSET_NOTCH_TOP + WIDTH_NOTCH_TOP + variableTopWidth; const width = hasArgs - ? baseWidth + OFFSET_NOTCH_RIGHT + CORNER_RADIUS + strokeWidth/2 - : baseWidth + CORNER_RADIUS +strokeWidth/2; + ? baseWidth + OFFSET_NOTCH_RIGHT + CORNER_RADIUS + strokeWidth / 2 + : baseWidth + CORNER_RADIUS + strokeWidth / 2; // Get rightVertical from _generateRight const { vertical: rightVertical } = _generateRight({ @@ -443,13 +436,10 @@ function getBoundingBox(config: TInputUnion): TBBox { bBoxArgs, }); - let height = rightVertical + CORNER_RADIUS + (type !== 'type3' ? strokeWidth/2 : 0); + let height = rightVertical + CORNER_RADIUS + (type !== 'type3' ? strokeWidth / 2 : 0); if (type === 'type3') { - const { - bBoxNesting, - secondaryLabel, - } = config as TInputType3; + const { bBoxNesting, secondaryLabel } = config as TInputType3; // Reuse nested path logic let nestingHeight = bBoxNesting.reduce((sum, box) => sum + box.h, 0); @@ -460,10 +450,13 @@ function getBoundingBox(config: TInputUnion): TBBox { ? strokeWidth / 2 + labelHeight + strokeWidth / 2 - CORNER_RADIUS * 2 : 4; - const nestedTotal = OUTER_CORNER_RADIUS + + const nestedTotal = + OUTER_CORNER_RADIUS + (nestingHeight - (strokeWidth / 2 + OUTER_CORNER_RADIUS * 2 + strokeWidth / 2)) + - OUTER_CORNER_RADIUS + CORNER_RADIUS + - labelAreaHeight + CORNER_RADIUS; + OUTER_CORNER_RADIUS + + CORNER_RADIUS + + labelAreaHeight + + CORNER_RADIUS; height += nestedTotal; } @@ -480,128 +473,128 @@ type TCentroid = { x: number; y: number }; // Centroid calculation for Top Notch function getTopCentroid(config: TInputUnion): TCentroid | undefined { - if (config.type === 'type2' || !config.hasNotchAbove) return undefined; + if (config.type === 'type2' || !config.hasNotchAbove) return undefined; - return { - x: CORNER_RADIUS + OFFSET_NOTCH_TOP + WIDTH_NOTCH_TOP / 2, - y: 1, - }; + return { + x: CORNER_RADIUS + OFFSET_NOTCH_TOP + WIDTH_NOTCH_TOP / 2, + y: 1, + }; } - // Centroid calculation for Bottom Notch function getBottomCentroid( - config: TInputUnion, - boundingBox: TBBox, - leftEdge: number + config: TInputUnion, + boundingBox: TBBox, + leftEdge: number, ): TCentroid | undefined { - if (config.type === 'type2' || !config.hasNotchBelow) return undefined; + if (config.type === 'type2' || !config.hasNotchBelow) return undefined; + + if (config.type !== 'type3') { + return { + x: CORNER_RADIUS + OFFSET_NOTCH_BOTTOM + WIDTH_NOTCH_BOTTOM / 2, + y: boundingBox.h - 1, // Place at the bottom edge + }; + } - if (config.type !== 'type3') { return { - x: CORNER_RADIUS + OFFSET_NOTCH_BOTTOM + WIDTH_NOTCH_BOTTOM / 2, - y: 1, + x: + CORNER_RADIUS + + OFFSET_NOTCH_TOP + + WIDTH_NOTCH_TOP / 2 + + OFFSET_NOTCH_BOTTOM + + WIDTH_NOTCH_BOTTOM / 2, //used the logic for top notch centroid + y: boundingBox.h - 1, // Place at the bottom edge for type3 as well }; - } - - return { - x: - CORNER_RADIUS + OFFSET_NOTCH_TOP + WIDTH_NOTCH_TOP/2 + - OFFSET_NOTCH_BOTTOM + WIDTH_NOTCH_BOTTOM / 2, //used the logic for top notch centroid - y: CORNER_RADIUS + leftEdge + CORNER_RADIUS + 1, - }; } - // Calculate centroids for right connector notch function getRightCentroids(config: TInputUnion, boundingBox: TBBox): TCentroid[] { - const { bBoxArgs, bBoxLabel, strokeWidth } = config; + const { bBoxArgs, bBoxLabel, strokeWidth } = config; - // No argument = no right notches - if (!bBoxArgs.length) return []; + // No argument = no right notches + if (!bBoxArgs.length) return []; - const centroids: TCentroid[] = []; + const centroids: TCentroid[] = []; - const labelHeight = Math.max(MIN_LABEL_HEIGHT, bBoxLabel.h); - const requiredMinimum = strokeWidth / 2 + labelHeight + strokeWidth / 2; - const argHeightsSum = bBoxArgs.reduce((sum, arg) => sum + arg.h, 0); + const labelHeight = Math.max(MIN_LABEL_HEIGHT, bBoxLabel.h); + const requiredMinimum = strokeWidth / 2 + labelHeight + strokeWidth / 2; + const argHeightsSum = bBoxArgs.reduce((sum, arg) => sum + arg.h, 0); - const extra = Math.max(0, requiredMinimum - argHeightsSum); + const extra = Math.max(0, requiredMinimum - argHeightsSum); - let verticalOffset = CORNER_RADIUS; // top-right corner arc + let verticalOffset = CORNER_RADIUS; // top-right corner arc - for (let i = 0; i < bBoxArgs.length; i++) { - const extraPerArg = extra / bBoxArgs.length; - const argBox = bBoxArgs[i]; + for (let i = 0; i < bBoxArgs.length; i++) { + const extraPerArg = extra / bBoxArgs.length; + const argBox = bBoxArgs[i]; - // v4, v-3, v10, v-3, v4 = total 12 - const fixedNotchHeight = 12; + // v4, v-3, v10, v-3, v4 = total 12 + const fixedNotchHeight = 12; - const variableLength = Math.max( - 0, - argBox.h + extraPerArg - - ( - strokeWidth / 2 + - CORNER_RADIUS + - HEIGHT_NOTCH_RIGHT + - CORNER_RADIUS + - strokeWidth / 2 + - CORNER_RADIUS + strokeWidth + CORNER_RADIUS - ) - ); + const variableLength = Math.max( + 0, + argBox.h + + extraPerArg - + (strokeWidth / 2 + + CORNER_RADIUS + + HEIGHT_NOTCH_RIGHT + + CORNER_RADIUS + + strokeWidth / 2 + + CORNER_RADIUS + + strokeWidth + + CORNER_RADIUS), + ); - // Centroid placed in the middle of v10 - const centroidY = verticalOffset + 6; + // Centroid placed in the middle of v10 + const centroidY = verticalOffset + 6; - centroids.push({ - x: boundingBox.w - 6, // notch is 12 wide, so center is at 6 from right edge - y: centroidY, - }); + centroids.push({ + x: boundingBox.w - 6, // notch is 12 wide, so center is at 6 from right edge + y: centroidY, + }); - verticalOffset += fixedNotchHeight; // 12 units fixed + verticalOffset += fixedNotchHeight; // 12 units fixed - if (variableLength > 0) { - verticalOffset += variableLength; - } + if (variableLength > 0) { + verticalOffset += variableLength; + } - if (i < bBoxArgs.length - 1) { - verticalOffset += CORNER_RADIUS + strokeWidth + CORNER_RADIUS; // 4 + 2 + 4 = 10 units + if (i < bBoxArgs.length - 1) { + verticalOffset += CORNER_RADIUS + strokeWidth + CORNER_RADIUS; // 4 + 2 + 4 = 10 units + } } - } - return centroids; + return centroids; } -// Calculate centroid for left connector notch +// Calculate centroid for left connector notch function getLeftCentroid(config: TInputUnion, boundingBox: TBBox): TCentroid | undefined { - if (config.type !== 'type2') return undefined; + if (config.type !== 'type2') return undefined; - return { - x: -7, // notch is made of h -6, h -2 → center = -7 - y: CORNER_RADIUS + CONN_NOTCH_WIDTH / 2, - }; + return { + x: -7, // notch is made of h -6, h -2 → center = -7 + y: CORNER_RADIUS + CONN_NOTCH_WIDTH / 2, + }; } - function getConnectionPoints( - config: TInputUnion, - boundingBox: TBBox, - leftEdge: number + config: TInputUnion, + boundingBox: TBBox, + leftEdge: number, ): { - top?: TCentroid; - right: TCentroid[]; - bottom?: TCentroid; - left?: TCentroid; + top?: TCentroid; + right: TCentroid[]; + bottom?: TCentroid; + left?: TCentroid; } { - return { - top: getTopCentroid(config), - right: getRightCentroids(config, boundingBox), - bottom: getBottomCentroid(config, boundingBox, leftEdge), - left: getLeftCentroid(config, boundingBox), - }; + return { + top: getTopCentroid(config), + right: getRightCentroids(config, boundingBox), + bottom: getBottomCentroid(config, boundingBox, leftEdge), + left: getLeftCentroid(config, boundingBox), + }; } - // single export function to return brick data export function generateBrickData(config: TInputType1 | TInputType2 | TInputType3): { path: string; diff --git a/modules/masonry/src/brick/utils/spec/path.spec.ts b/modules/masonry/src/brick/utils/spec/path.spec.ts index 8101dae6..7f7604ac 100644 --- a/modules/masonry/src/brick/utils/spec/path.spec.ts +++ b/modules/masonry/src/brick/utils/spec/path.spec.ts @@ -119,47 +119,45 @@ describe('Masonry: Brick > Path Generation', () => { testCases.forEach(({ name, input }) => { it(`generates path, bounding box, and connection points correctly: ${name}`, () => { - const { path, boundingBox, connectionPoints } = generateBrickData(input as TInputUnion); + const { path, boundingBox, connectionPoints } = generateBrickData(input as TInputUnion); - // Validate the path - expect(typeof path).toBe('string'); - expect(path.length).toBeGreaterThan(0); + // Validate the path + expect(typeof path).toBe('string'); + expect(path.length).toBeGreaterThan(0); - // Validate the bounding box - expect(typeof boundingBox).toBe('object'); - expect(boundingBox).toHaveProperty('w'); - expect(boundingBox).toHaveProperty('h'); - expect(typeof boundingBox.w).toBe('number'); - expect(typeof boundingBox.h).toBe('number'); - expect(boundingBox.w).toBeGreaterThan(0); - expect(boundingBox.h).toBeGreaterThan(0); + // Validate the bounding box + expect(typeof boundingBox).toBe('object'); + expect(boundingBox).toHaveProperty('w'); + expect(boundingBox).toHaveProperty('h'); + expect(typeof boundingBox.w).toBe('number'); + expect(typeof boundingBox.h).toBe('number'); + expect(boundingBox.w).toBeGreaterThan(0); + expect(boundingBox.h).toBeGreaterThan(0); - // Validate connection points - expect(connectionPoints).toHaveProperty('right'); - expect(Array.isArray(connectionPoints.right)).toBe(true); - connectionPoints.right.forEach((pt) => { - expect(pt).toHaveProperty('x'); - expect(pt).toHaveProperty('y'); - expect(typeof pt.x).toBe('number'); - expect(typeof pt.y).toBe('number'); - }); - - if (connectionPoints.left) { - expect(typeof connectionPoints.left.x).toBe('number'); - expect(typeof connectionPoints.left.y).toBe('number'); - } + // Validate connection points + expect(connectionPoints).toHaveProperty('right'); + expect(Array.isArray(connectionPoints.right)).toBe(true); + connectionPoints.right.forEach((pt) => { + expect(pt).toHaveProperty('x'); + expect(pt).toHaveProperty('y'); + expect(typeof pt.x).toBe('number'); + expect(typeof pt.y).toBe('number'); + }); - if (connectionPoints.top) { - expect(typeof connectionPoints.top.x).toBe('number'); - expect(typeof connectionPoints.top.y).toBe('number'); - } + if (connectionPoints.left) { + expect(typeof connectionPoints.left.x).toBe('number'); + expect(typeof connectionPoints.left.y).toBe('number'); + } - if (connectionPoints.bottom) { - expect(typeof connectionPoints.bottom.x).toBe('number'); - expect(typeof connectionPoints.bottom.y).toBe('number'); - } - }); + if (connectionPoints.top) { + expect(typeof connectionPoints.top.x).toBe('number'); + expect(typeof connectionPoints.top.y).toBe('number'); + } + if (connectionPoints.bottom) { + expect(typeof connectionPoints.bottom.x).toBe('number'); + expect(typeof connectionPoints.bottom.y).toBe('number'); + } + }); }); - }); From dfbd0f91a4ddeb9804e1394e86141c5579a6093a Mon Sep 17 00:00:00 2001 From: Saumya Shahi Date: Sat, 21 Jun 2025 19:47:05 +0530 Subject: [PATCH 10/19] feat(masonry): update view components and stories to use new connection points system --- .../src/brick/view/components/compound.tsx | 37 +++++++++++++------ .../src/brick/view/components/expression.tsx | 25 +++++++------ .../src/brick/view/components/simple.tsx | 25 +++++++------ 3 files changed, 51 insertions(+), 36 deletions(-) diff --git a/modules/masonry/src/brick/view/components/compound.tsx b/modules/masonry/src/brick/view/components/compound.tsx index bd5fe127..8ffda4c3 100644 --- a/modules/masonry/src/brick/view/components/compound.tsx +++ b/modules/masonry/src/brick/view/components/compound.tsx @@ -1,6 +1,6 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import type { TBrickRenderPropsCompound } from '../../@types/brick'; -import { generatePath, getBoundingBox } from '../../utils/path'; +import { generateBrickData } from '../../utils/path'; const FONT_HEIGHT = 16; @@ -56,8 +56,12 @@ export const CompoundBrickView: React.FC = (props) => isFolded, } = props; - const { w: labelW, h: labelH, ascent } = measureLabel(label, FONT_HEIGHT); - const bBoxLabel = { w: labelW, h: labelH }; + // Memoize bBoxLabel to prevent unnecessary recalculations + const bBoxLabel = useMemo(() => { + const { w: labelW, h: labelH } = measureLabel(label, FONT_HEIGHT); + return { w: labelW, h: labelH }; + }, [label]); + const bBoxNesting = bboxNest; const [shape, setShape] = useState<{ path: string; w: number; h: number }>(() => { @@ -72,9 +76,8 @@ export const CompoundBrickView: React.FC = (props) => bBoxNesting, secondaryLabel: !isFolded, }; - const { path } = generatePath(cfg); - const { w, h } = getBoundingBox(cfg); - return { path, w, h }; + const brickData = generateBrickData(cfg); + return { path: brickData.path, w: brickData.boundingBox.w, h: brickData.boundingBox.h }; }); useEffect(() => { @@ -89,10 +92,20 @@ export const CompoundBrickView: React.FC = (props) => bBoxNesting, secondaryLabel: !isFolded, }; - const { path } = generatePath(cfg); - const { w, h } = getBoundingBox(cfg); - setShape({ path, w, h }); - }, [label, strokeWidth, scale, bboxArgs, topNotch, bottomNotch, bboxNest, isFolded]); + const brickData = generateBrickData(cfg); + setShape({ path: brickData.path, w: brickData.boundingBox.w, h: brickData.boundingBox.h }); + }, [ + label, + strokeWidth, + scale, + bboxArgs, + topNotch, + bottomNotch, + bboxNest, + isFolded, + bBoxLabel, + bBoxNesting, + ]); if (!isVisible) return null; @@ -122,7 +135,7 @@ export const CompoundBrickView: React.FC = (props) => {labelType === 'text' && ( = (props // value, isValueSelectOpen, // if needed later } = props; - const { w: labelW, h: labelH, ascent } = measureLabel(label, FONT_HEIGHT); - const bBoxLabel = { w: labelW, h: labelH }; + // Memoize bBoxLabel to prevent unnecessary recalculations + const bBoxLabel = useMemo(() => { + const { w: labelW, h: labelH } = measureLabel(label, FONT_HEIGHT); + return { w: labelW, h: labelH }; + }, [label]); const [shape, setShape] = useState<{ path: string; w: number; h: number }>(() => { const cfg = { @@ -63,9 +66,8 @@ export const ExpressionBrickView: React.FC = (props bBoxLabel, bBoxArgs: bboxArgs, }; - const { path } = generatePath(cfg); - const { w, h } = getBoundingBox(cfg); - return { path, w, h }; + const brickData = generateBrickData(cfg); + return { path: brickData.path, w: brickData.boundingBox.w, h: brickData.boundingBox.h }; }); useEffect(() => { @@ -76,10 +78,9 @@ export const ExpressionBrickView: React.FC = (props bBoxLabel, bBoxArgs: bboxArgs, }; - const { path } = generatePath(cfg); - const { w, h } = getBoundingBox(cfg); - setShape({ path, w, h }); - }, [label, strokeWidth, scale, bboxArgs]); + const brickData = generateBrickData(cfg); + setShape({ path: brickData.path, w: brickData.boundingBox.w, h: brickData.boundingBox.h }); + }, [label, strokeWidth, scale, bboxArgs, bBoxLabel]); if (!isVisible) return null; @@ -109,7 +110,7 @@ export const ExpressionBrickView: React.FC = (props {labelType === 'text' && ( = (props) => { bottomNotch, } = props; - const { w: labelW, h: labelH, ascent } = measureLabel(label, FONT_HEIGHT); - const bBoxLabel = { w: labelW, h: labelH }; + // Memoize bBoxLabel to prevent unnecessary recalculations + const bBoxLabel = useMemo(() => { + const { w: labelW, h: labelH } = measureLabel(label, FONT_HEIGHT); + return { w: labelW, h: labelH }; + }, [label]); const [shape, setShape] = useState<{ path: string; w: number; h: number }>(() => { const cfg = { @@ -66,9 +69,8 @@ export const SimpleBrickView: React.FC = (props) => { hasNotchAbove: topNotch, hasNotchBelow: bottomNotch, }; - const { path } = generatePath(cfg); - const { w, h } = getBoundingBox(cfg); - return { path, w, h }; + const brickData = generateBrickData(cfg); + return { path: brickData.path, w: brickData.boundingBox.w, h: brickData.boundingBox.h }; }); useEffect(() => { @@ -81,10 +83,9 @@ export const SimpleBrickView: React.FC = (props) => { hasNotchAbove: topNotch, hasNotchBelow: bottomNotch, }; - const { path } = generatePath(cfg); - const { w, h } = getBoundingBox(cfg); - setShape({ path, w, h }); - }, [label, strokeWidth, scale, bboxArgs, topNotch, bottomNotch]); + const brickData = generateBrickData(cfg); + setShape({ path: brickData.path, w: brickData.boundingBox.w, h: brickData.boundingBox.h }); + }, [label, strokeWidth, scale, bboxArgs, topNotch, bottomNotch, bBoxLabel]); if (!isVisible) return null; @@ -111,7 +112,7 @@ export const SimpleBrickView: React.FC = (props) => { {labelType === 'text' && ( Date: Sat, 21 Jun 2025 19:47:39 +0530 Subject: [PATCH 11/19] feat(masonry): add BrickTreeManager for brick trees management with connection validation --- modules/masonry/src/tree/model/model.ts | 499 ++++++++++++++++++ .../masonry/src/tree/model/spec/model.spec.ts | 262 +++++++++ 2 files changed, 761 insertions(+) create mode 100644 modules/masonry/src/tree/model/model.ts create mode 100644 modules/masonry/src/tree/model/spec/model.spec.ts diff --git a/modules/masonry/src/tree/model/model.ts b/modules/masonry/src/tree/model/model.ts new file mode 100644 index 00000000..7102991c --- /dev/null +++ b/modules/masonry/src/tree/model/model.ts @@ -0,0 +1,499 @@ +import { v4 as uuidv4 } from 'uuid'; +import type { IBrick, TBrickType } from '../../brick/@types/brick'; +import type { SimpleBrick, ExpressionBrick } from '../../brick/model/model'; +import type CompoundBrick from '../../brick/model/model'; + +// Point type +export type TPoint = { + x: number; + y: number; +}; + +// Notch type +export type TNotchType = 'top-bottom' | 'right-left' | 'left-right'; + +// Connection point types +export type TConnectionPoint = { + x: number; + y: number; +}; + +export type TConnectionPoints = { + top?: TConnectionPoint; + right: TConnectionPoint[]; + bottom?: TConnectionPoint; + left?: TConnectionPoint; + nested?: TConnectionPoint; +}; + +// Connection types between bricks +export type TConnectionType = 'top-bottom' | 'left-right' | 'right-left' | 'nested'; + +// Connection between two bricks +export type TBrickConnection = { + from: string; + to: string; + fromNotchId: string; + toNotchId: string; + type: TNotchType; +}; + +// Tree node representing a brick in the tree +export type TTreeNode = { + brick: IBrick; + position: TPoint; + parent: TTreeNode | null; + connectedNotches: Set; +}; + +// Tree structure representing connected bricks +export type TTree = { + id: string; + nodes: Map; + connections: TBrickConnection[]; +}; + +// Connection validation result +export type TConnectionValidation = { + isValid: boolean; + reason?: string; +}; + +// Main tree manager class +export default class BrickTreeManager { + private trees: TTree[] = []; + private nextTreeId = 1; + + constructor() {} + + /** + * Finds the ID of a notch on a brick based on connection point coordinates. + * @param brick The brick to search on. + * @param point The connection point coordinates. + * @returns The notch ID (e.g., 'left', 'right_0') or null if not found. + */ + private findNotchId(brick: IBrick, point: TConnectionPoint): string | null { + const { connectionPoints } = brick; + const tolerance = 1.5; // Use a slightly larger tolerance + + if ( + connectionPoints.top && + Math.hypot(connectionPoints.top.x - point.x, connectionPoints.top.y - point.y) < + tolerance + ) { + return 'top'; + } + if ( + connectionPoints.bottom && + Math.hypot(connectionPoints.bottom.x - point.x, connectionPoints.bottom.y - point.y) < + tolerance + ) { + return 'bottom'; + } + if ( + connectionPoints.left && + Math.hypot(connectionPoints.left.x - point.x, connectionPoints.left.y - point.y) < + tolerance + ) { + return 'left'; + } + for (let i = 0; i < connectionPoints.right.length; i++) { + const rightPoint = connectionPoints.right[i]; + if (Math.hypot(rightPoint.x - point.x, rightPoint.y - point.y) < tolerance) { + return `right_${i}`; + } + } + return null; + } + + /** + * Creates a new tree with a single brick + */ + public createTree(brick: IBrick, position: TPoint): TTree { + const treeId = `tree_${this.nextTreeId++}`; + const node: TTreeNode = { + brick, + position, + parent: null, + connectedNotches: new Set(), + }; + const tree: TTree = { + id: treeId, + nodes: new Map([[brick.uuid, node]]), + connections: [], + }; + this.trees.push(tree); + return tree; + } + + /** + * Adds a brick to an existing tree + */ + public addBrickToTree(treeId: string, brick: IBrick, parentBrickId: string, position: TPoint) { + const tree = this.trees.find((t) => t.id === treeId); + if (!tree) return; + + const parentNode = tree.nodes.get(parentBrickId); + if (!parentNode) return; + + const node: TTreeNode = { + brick, + position, + parent: parentNode, + connectedNotches: new Set(), + }; + tree.nodes.set(brick.uuid, node); + } + + /** + * Connects two trees or bricks with validation + */ + public connectBricks( + fromBrickId: string, + toBrickId: string, + fromPoint: TConnectionPoint, + toPoint: TConnectionPoint, + connectionType: TNotchType, + ): string | null { + const fromBrickNode = this.getBrickNode(fromBrickId); + const toBrickNode = this.getBrickNode(toBrickId); + + if (!fromBrickNode || !toBrickNode) return null; + + console.log(`Connection points - fromPoint:`, fromPoint, `toPoint:`, toPoint); + console.log(`From brick connection points:`, fromBrickNode.brick.connectionPoints); + console.log(`To brick connection points:`, toBrickNode.brick.connectionPoints); + + const fromNotchId = this.findNotchId(fromBrickNode.brick, fromPoint); + const toNotchId = this.findNotchId(toBrickNode.brick, toPoint); + + console.log( + `Connecting ${fromBrickId} to ${toBrickId}: fromNotchId=${fromNotchId}, toNotchId=${toNotchId}`, + ); + + if (!fromNotchId || !toNotchId) { + console.error('Could not determine notch IDs for connection'); + return null; + } + + console.log(`From brick connected notches:`, Array.from(fromBrickNode.connectedNotches)); + console.log(`To brick connected notches:`, Array.from(toBrickNode.connectedNotches)); + + if ( + fromBrickNode.connectedNotches.has(fromNotchId) || + toBrickNode.connectedNotches.has(toNotchId) + ) { + console.error('One or both notches are already connected'); + return null; + } + + fromBrickNode.connectedNotches.add(fromNotchId); + toBrickNode.connectedNotches.add(toNotchId); + + const fromTree = this.findTreeByBrickId(fromBrickId); + const toTree = this.findTreeByBrickId(toBrickId); + + if (!fromTree || !toTree) return null; + + const connection: TBrickConnection = { + from: fromBrickId, + to: toBrickId, + fromNotchId, + toNotchId, + type: connectionType, + }; + + if (fromTree.id === toTree.id) { + fromTree.connections.push(connection); + this.updateParentChildRelationships(fromBrickId, toBrickId, connection.type); + console.log(`Added connection to existing tree ${fromTree.id}`); + return fromTree.id; + } + + const mergedTree = this.mergeTrees(fromTree, toTree, connection); + this.updateParentChildRelationships(fromBrickId, toBrickId, connection.type); + console.log(`Merged trees into ${mergedTree.id}`); + return mergedTree.id; + } + + private mergeTrees(tree1: TTree, tree2: TTree, connection: TBrickConnection): TTree { + tree2.nodes.forEach((node, brickId) => { + tree1.nodes.set(brickId, node); + }); + tree2.connections.forEach((conn) => { + tree1.connections.push(conn); + }); + tree1.connections.push(connection); + this.trees = this.trees.filter((t) => t.id !== tree2.id); + return tree1; + } + + /** + * Updates parent-child relationships based on connection type + */ + private updateParentChildRelationships( + fromBrickId: string, + toBrickId: string, + notchType: TNotchType, + ) { + const fromNode = this.getBrickNode(fromBrickId); + const toNode = this.getBrickNode(toBrickId); + if (!fromNode || !toNode) return; + + if (notchType === 'top-bottom' || notchType === 'right-left') { + toNode.parent = fromNode; + } else if (notchType === 'left-right') { + fromNode.parent = toNode; + } + } + + /** + * Disconnects a brick from its tree + * Handles hierarchical relationships like a folder structure: + * - If disconnecting a parent, all children remain connected to it + * - If disconnecting a child, it becomes a separate tree + */ + public disconnectBrick(brickId: string): { + removedConnections: TBrickConnection[]; + newTreeIds: string[]; + } { + const brickNode = this.getBrickNode(brickId); + if (!brickNode) return { removedConnections: [], newTreeIds: [] }; + + const originalTree = this.findTreeByBrickId(brickId); + if (!originalTree) return { removedConnections: [], newTreeIds: [] }; + + console.log(`Disconnecting brick ${brickId} from tree ${originalTree.id}`); + console.log(`Original tree connections:`, originalTree.connections); + + // Collect all descendant nodes of the disconnected brick (including the brick itself) + const nodesToMove = new Map(); + const stack: TTreeNode[] = [brickNode]; + const visited = new Set([brickNode.brick.uuid]); + + while (stack.length > 0) { + const currentNode = stack.pop()!; + nodesToMove.set(currentNode.brick.uuid, currentNode); + + this.getBrickChildren(currentNode.brick.uuid).forEach((child) => { + if (!visited.has(child.brick.uuid)) { + visited.add(child.brick.uuid); + stack.push(child); + } + }); + } + + console.log(`Nodes to move:`, Array.from(nodesToMove.keys())); + + // Find all connections that need to be removed from the original tree + // This includes connections between nodes being moved and connections to/from external nodes + const connectionsToRemove = originalTree.connections.filter((conn) => { + const fromInNewTree = nodesToMove.has(conn.from); + const toInNewTree = nodesToMove.has(conn.to); + + // Remove connections where both nodes are moving to the new tree + // OR connections where one node is moving and the other stays in original tree + const shouldRemove = + (fromInNewTree && toInNewTree) || + (fromInNewTree && !toInNewTree) || + (!fromInNewTree && toInNewTree); + + console.log( + `Connection ${conn.from} -> ${conn.to}: fromInNewTree=${fromInNewTree}, toInNewTree=${toInNewTree}, shouldRemove=${shouldRemove}`, + ); + + return shouldRemove; + }); + + console.log(`Connections to remove:`, connectionsToRemove); + + if (connectionsToRemove.length === 0) return { removedConnections: [], newTreeIds: [] }; + + // Remove connections from original tree + this.removeConnections(originalTree, connectionsToRemove); + + // Remove nodes from original tree + nodesToMove.forEach((node, brickId) => { + originalTree.nodes.delete(brickId); + }); + + // Create new tree with the disconnected brick as root + const newTree = this.createTree(brickNode.brick, brickNode.position); + + // Add all descendant nodes to the new tree + nodesToMove.forEach((node, brickId) => { + if (brickId !== brickNode.brick.uuid) { + // Don't add the root twice + newTree.nodes.set(brickId, node); + } + }); + + // Move connections between nodes in the new tree to the new tree + const connectionsToMove = connectionsToRemove.filter( + (conn) => nodesToMove.has(conn.from) && nodesToMove.has(conn.to), + ); + newTree.connections = connectionsToMove; + + // Update parent relationships for the new tree + // The disconnected brick becomes the root (no parent) + brickNode.parent = null; + + // All other nodes in the new tree should have their parent relationships preserved + // but only if their parent is also in the new tree + nodesToMove.forEach((node, brickId) => { + if (brickId !== brickNode.brick.uuid && node.parent) { + if (!nodesToMove.has(node.parent.brick.uuid)) { + // If parent is not in the new tree, this node becomes a direct child of the root + node.parent = brickNode; + } + } + }); + + // Clean up original tree if it's empty + if (originalTree.nodes.size === 0) { + this.trees = this.trees.filter((t) => t.id !== originalTree.id); + } + + console.log(`Returning ${connectionsToRemove.length} removed connections`); + return { removedConnections: connectionsToRemove, newTreeIds: [newTree.id] }; + } + + private removeConnections(tree: TTree, connectionsToRemove: TBrickConnection[]) { + connectionsToRemove.forEach((conn) => { + const fromNode = this.getBrickNode(conn.from); + const toNode = this.getBrickNode(conn.to); + if (fromNode && conn.fromNotchId) fromNode.connectedNotches.delete(conn.fromNotchId); + if (toNode && conn.toNotchId) toNode.connectedNotches.delete(conn.toNotchId); + if (toNode && toNode.parent?.brick.uuid === conn.from) toNode.parent = null; + if (fromNode && fromNode.parent?.brick.uuid === conn.to) fromNode.parent = null; + }); + tree.connections = tree.connections.filter((c) => !connectionsToRemove.includes(c)); + } + + /** + * Gets a tree by ID + */ + public getTree(treeId: string): TTree | undefined { + return this.trees.find((t) => t.id === treeId); + } + + /** + * Gets all trees + */ + public getAllTrees(): TTree[] { + return this.trees; + } + + /** + * Gets all brick IDs in a tree + */ + public getBricksInTree(treeId: string): IBrick[] { + const tree = this.getTree(treeId); + return tree ? Array.from(tree.nodes.values()).map((n) => n.brick) : []; + } + + /** + * Checks if two bricks are connected + */ + public areBricksConnected(brickId1: string, brickId2: string): boolean { + for (const tree of this.trees) { + if ( + tree.connections.some( + (c) => + (c.from === brickId1 && c.to === brickId2) || + (c.from === brickId2 && c.to === brickId1), + ) + ) { + return true; + } + } + return false; + } + + /** + * Gets all connections for a brick + */ + public getBrickConnections(brickId: string): TBrickConnection[] { + const tree = this.getTree(brickId); + if (!tree) return []; + + return tree.connections.filter((conn) => conn.from === brickId || conn.to === brickId); + } + + /** + * Moves a brick within its tree + */ + public moveBrick(brickId: string, newPosition: { x: number; y: number }): boolean { + const tree = this.getTree(brickId); + if (!tree) return false; + + const node = tree.nodes.get(brickId); + if (!node) return false; + + node.position = newPosition; + + return true; + } + + /** + * Gets the path from root to a specific brick + */ + public getPathToBrick(brickId: string): string[] { + const tree = this.getTree(brickId); + if (!tree) return []; + + const path: string[] = []; + let currentBrickId = brickId; + + while (currentBrickId) { + path.unshift(currentBrickId); + const node = tree.nodes.get(currentBrickId); + currentBrickId = node?.parent?.brick.uuid || ''; + } + + return path; + } + + /** + * Gets all children of a brick + */ + public getBrickChildren(brickId: string): TTreeNode[] { + const children: TTreeNode[] = []; + for (const tree of this.trees) { + for (const node of tree.nodes.values()) { + if (node.parent?.brick.uuid === brickId) { + children.push(node); + } + } + } + return children; + } + + /** + * Gets the parent of a brick + */ + public getBrickParent(brickId: string): string | undefined { + const node = this.getBrickNode(brickId); + return node?.parent?.brick.uuid; + } + + /** + * Clears all trees + */ + public clear(): void { + this.trees = []; + this.nextTreeId = 1; + } + + private getBrickNode(brickId: string): TTreeNode | undefined { + for (const tree of this.trees) { + const node = tree.nodes.get(brickId); + if (node) return node; + } + return undefined; + } + + private findTreeByBrickId(brickId: string): TTree | undefined { + return this.trees.find((tree) => tree.nodes.has(brickId)); + } +} diff --git a/modules/masonry/src/tree/model/spec/model.spec.ts b/modules/masonry/src/tree/model/spec/model.spec.ts new file mode 100644 index 00000000..74664414 --- /dev/null +++ b/modules/masonry/src/tree/model/spec/model.spec.ts @@ -0,0 +1,262 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import BrickTreeManager from './model'; +import { SimpleBrick, ExpressionBrick } from '../../brick/model/model'; +import CompoundBrick from '../../brick/model/model'; +import { IBrick } from '../../brick/@types/brick'; + +describe('BrickTreeManager', () => { + let treeManager: BrickTreeManager; + let simpleBrick1: IBrick; + let simpleBrick2: IBrick; + let expressionBrick: IBrick; + let expressionBrick2: IBrick; + let compoundBrick: IBrick; + let simpleBrickNoNotch: IBrick; + + beforeEach(() => { + treeManager = new BrickTreeManager(); + + simpleBrick1 = new SimpleBrick({ + uuid: 'simple1', + name: 'Simple 1', + label: 's1', + labelType: 'text', + colorBg: 'red', + colorFg: 'white', + strokeColor: 'black', + shadow: false, + scale: 1, + bboxArgs: [{ w: 10, h: 20 }], // Add bboxArgs to generate right connection points + topNotch: true, + bottomNotch: true, + }); + + // Use SimpleBrick for top-bottom tests + simpleBrick2 = new SimpleBrick({ + uuid: 'simple2', + name: 'Simple 2', + label: 's2', + labelType: 'text', + colorBg: 'green', + colorFg: 'white', + strokeColor: 'black', + shadow: false, + scale: 1, + bboxArgs: [{ w: 10, h: 20 }], + topNotch: true, + bottomNotch: true, + }); + + // Use ExpressionBrick for right-to-left tests + expressionBrick = new ExpressionBrick({ + uuid: 'expr1', + name: 'Expression 1', + label: 'e1', + labelType: 'text', + colorBg: 'blue', + colorFg: 'white', + strokeColor: 'black', + shadow: false, + scale: 1, + bboxArgs: [], // Expression bricks have a left notch by default + }); + expressionBrick2 = new ExpressionBrick({ + uuid: 'expr2', + name: 'Expression 2', + label: 'e2', + labelType: 'text', + colorBg: 'orange', + colorFg: 'white', + strokeColor: 'black', + shadow: false, + scale: 1, + bboxArgs: [], + }); + + compoundBrick = new CompoundBrick({ + uuid: 'comp1', + name: 'Compound 1', + label: 'c1', + labelType: 'text', + colorBg: 'purple', + colorFg: 'white', + strokeColor: 'black', + shadow: false, + scale: 1, + bboxArgs: [{ w: 10, h: 20 }], // Add bboxArgs to generate right connection points + bboxNest: [], + topNotch: true, + bottomNotch: true, + }); + + simpleBrickNoNotch = new SimpleBrick({ + uuid: 'no-notch', + name: 'No Notch', + label: 'nn', + labelType: 'text', + colorBg: 'grey', + colorFg: 'white', + strokeColor: 'black', + shadow: false, + scale: 1, + bboxArgs: [], + topNotch: true, + bottomNotch: false, + }); + }); + + describe('Tree Creation', () => { + it('should create a new tree with a single brick', () => { + const tree = treeManager.createTree(simpleBrick1, { x: 0, y: 0 }); + expect(tree.id).toBe('tree_1'); + }); + }); + + describe('Hierarchical Tree Behavior (Folder-like Structure)', () => { + it('should disconnect a parent and keep children', () => { + const tree = treeManager.createTree(simpleBrick1, { x: 0, y: 0 }); + treeManager.addBrickToTree(tree.id, simpleBrick2, simpleBrick1.uuid, { x: 0, y: 50 }); + treeManager.connectBricks( + simpleBrick1.uuid, + simpleBrick2.uuid, + simpleBrick1.connectionPoints.bottom!, + simpleBrick2.connectionPoints.top!, + 'top-bottom', + ); + const result = treeManager.disconnectBrick(simpleBrick2.uuid); + expect(result.newTreeIds).toHaveLength(1); + expect(result.removedConnections).toHaveLength(1); + }); + }); + + describe('Right-to-Left "Puzzle" Connections', () => { + it('should connect two bricks with a right-to-left connection', () => { + treeManager.createTree(simpleBrick1, { x: 0, y: 0 }); + treeManager.createTree(expressionBrick, { x: 120, y: 0 }); + const newTreeId = treeManager.connectBricks( + simpleBrick1.uuid, + expressionBrick.uuid, + simpleBrick1.connectionPoints.right[0], + expressionBrick.connectionPoints.left!, + 'right-left', + ); + const tree = treeManager.getTree(newTreeId!); + expect(tree?.connections[0].fromNotchId).toBe('right_0'); + expect(tree?.connections[0].toNotchId).toBe('left'); + expect(treeManager.getBrickParent(expressionBrick.uuid)).toBe(simpleBrick1.uuid); + }); + + it('should not connect if a notch is already in use', () => { + const treeId = treeManager.createTree(simpleBrick1, { x: 0, y: 0 }).id; + treeManager.addBrickToTree(treeId, expressionBrick, simpleBrick1.uuid, { + x: 120, + y: 0, + }); + treeManager.connectBricks( + simpleBrick1.uuid, + expressionBrick.uuid, + simpleBrick1.connectionPoints.right[0], + expressionBrick.connectionPoints.left!, + 'right-left', + ); + treeManager.createTree(expressionBrick2, { x: 120, y: 50 }); + const result = treeManager.connectBricks( + simpleBrick1.uuid, + expressionBrick2.uuid, + simpleBrick1.connectionPoints.right[0], + expressionBrick2.connectionPoints.left!, + 'right-left', + ); + expect(result).toBeNull(); + }); + + it('should disconnect a right-to-left connection', () => { + treeManager.createTree(simpleBrick1, { x: 0, y: 0 }); + treeManager.createTree(expressionBrick, { x: 120, y: 0 }); + treeManager.connectBricks( + simpleBrick1.uuid, + expressionBrick.uuid, + simpleBrick1.connectionPoints.right[0], + expressionBrick.connectionPoints.left!, + 'right-left', + ); + const result = treeManager.disconnectBrick(expressionBrick.uuid); + expect(result.removedConnections).toHaveLength(1); + expect(result.newTreeIds).toHaveLength(1); + expect(treeManager.areBricksConnected(simpleBrick1.uuid, expressionBrick.uuid)).toBe( + false, + ); + }); + }); + + // Re-introducing Tower Model Test Cases + describe('Tower Model Test Cases', () => { + it('Test Case 1: should connect multiple bricks in a chain', () => { + const treeId = treeManager.createTree(simpleBrick1, { x: 0, y: 0 }).id; + treeManager.addBrickToTree(treeId, simpleBrick2, simpleBrick1.uuid, { x: 0, y: 50 }); + treeManager.addBrickToTree(treeId, compoundBrick, simpleBrick2.uuid, { x: 0, y: 100 }); + + treeManager.connectBricks( + simpleBrick1.uuid, + simpleBrick2.uuid, + simpleBrick1.connectionPoints.bottom!, + simpleBrick2.connectionPoints.top!, + 'top-bottom', + ); + treeManager.connectBricks( + simpleBrick2.uuid, + compoundBrick.uuid, + simpleBrick2.connectionPoints.bottom!, + (compoundBrick as any).connectionPoints.top!, + 'top-bottom', + ); + expect(treeManager.getAllTrees()).toHaveLength(1); + expect(treeManager.getBricksInTree('tree_1')).toHaveLength(3); + }); + + it('Test Case 2: should disconnect a brick from the middle of a chain', () => { + const treeId = treeManager.createTree(simpleBrick1, { x: 0, y: 0 }).id; + treeManager.addBrickToTree(treeId, simpleBrick2, simpleBrick1.uuid, { x: 0, y: 50 }); + treeManager.addBrickToTree(treeId, compoundBrick, simpleBrick2.uuid, { x: 0, y: 100 }); + treeManager.connectBricks( + simpleBrick1.uuid, + simpleBrick2.uuid, + simpleBrick1.connectionPoints.bottom!, + simpleBrick2.connectionPoints.top!, + 'top-bottom', + ); + treeManager.connectBricks( + simpleBrick2.uuid, + compoundBrick.uuid, + simpleBrick2.connectionPoints.bottom!, + (compoundBrick as any).connectionPoints.top!, + 'top-bottom', + ); + + const result = treeManager.disconnectBrick(simpleBrick2.uuid); + expect(result.removedConnections).toHaveLength(2); + expect(result.newTreeIds).toHaveLength(1); // Disconnected brick forms its own tree + }); + + it('Test Case 3: should not connect when notch is already connected', () => { + treeManager.createTree(simpleBrick1, { x: 0, y: 0 }); + treeManager.createTree(simpleBrick2, { x: 0, y: 50 }); + treeManager.connectBricks( + simpleBrick1.uuid, + simpleBrick2.uuid, + simpleBrick1.connectionPoints.bottom!, + simpleBrick2.connectionPoints.top!, + 'top-bottom', + ); + treeManager.createTree(compoundBrick, { x: 0, y: 100 }); + const result = treeManager.connectBricks( + simpleBrick1.uuid, + compoundBrick.uuid, + simpleBrick1.connectionPoints.bottom!, + (compoundBrick as any).connectionPoints.top!, + 'top-bottom', + ); + expect(result).toBeNull(); + }); + }); +}); From 9ad8c14739120702e1f57c6eec4b8521c019790d Mon Sep 17 00:00:00 2001 From: Saumya Shahi Date: Sat, 21 Jun 2025 19:51:33 +0530 Subject: [PATCH 12/19] fix(masonry): add .js extension to BrickTreeManager import tests --- modules/masonry/src/tree/model/spec/model.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/masonry/src/tree/model/spec/model.spec.ts b/modules/masonry/src/tree/model/spec/model.spec.ts index 74664414..5b895d71 100644 --- a/modules/masonry/src/tree/model/spec/model.spec.ts +++ b/modules/masonry/src/tree/model/spec/model.spec.ts @@ -1,8 +1,8 @@ import { describe, it, expect, beforeEach } from 'vitest'; -import BrickTreeManager from './model'; -import { SimpleBrick, ExpressionBrick } from '../../brick/model/model'; -import CompoundBrick from '../../brick/model/model'; -import { IBrick } from '../../brick/@types/brick'; +import BrickTreeManager from '../model.js'; +import { SimpleBrick, ExpressionBrick } from '../../../brick/model/model'; +import CompoundBrick from '../../../brick/model/model'; +import { IBrick } from '../../../brick/@types/brick'; describe('BrickTreeManager', () => { let treeManager: BrickTreeManager; From 4dcef61bf55b20d64c2ef15e5e3ce9ba900cbbdc Mon Sep 17 00:00:00 2001 From: Saumya Shahi Date: Mon, 23 Jun 2025 01:24:40 +0530 Subject: [PATCH 13/19] chore(masonry): replace debug console.log statements with comments in tree model --- modules/masonry/src/tree/model/model.ts | 55 ++++++++++--------------- 1 file changed, 22 insertions(+), 33 deletions(-) diff --git a/modules/masonry/src/tree/model/model.ts b/modules/masonry/src/tree/model/model.ts index 7102991c..f478516e 100644 --- a/modules/masonry/src/tree/model/model.ts +++ b/modules/masonry/src/tree/model/model.ts @@ -160,24 +160,22 @@ export default class BrickTreeManager { if (!fromBrickNode || !toBrickNode) return null; - console.log(`Connection points - fromPoint:`, fromPoint, `toPoint:`, toPoint); - console.log(`From brick connection points:`, fromBrickNode.brick.connectionPoints); - console.log(`To brick connection points:`, toBrickNode.brick.connectionPoints); + // Validate that both bricks exist and can be connected + // The connection points represent the specific notches where bricks will connect const fromNotchId = this.findNotchId(fromBrickNode.brick, fromPoint); const toNotchId = this.findNotchId(toBrickNode.brick, toPoint); - console.log( - `Connecting ${fromBrickId} to ${toBrickId}: fromNotchId=${fromNotchId}, toNotchId=${toNotchId}`, - ); + // Find the specific notch IDs for both bricks based on their connection points + // These IDs are used to track which notches are occupied if (!fromNotchId || !toNotchId) { console.error('Could not determine notch IDs for connection'); return null; } - console.log(`From brick connected notches:`, Array.from(fromBrickNode.connectedNotches)); - console.log(`To brick connected notches:`, Array.from(toBrickNode.connectedNotches)); + // Check if the notches are already connected to other bricks + // A notch can only be connected to one other notch at a time if ( fromBrickNode.connectedNotches.has(fromNotchId) || @@ -187,6 +185,7 @@ export default class BrickTreeManager { return null; } + // Mark both notches as connected to prevent future connections fromBrickNode.connectedNotches.add(fromNotchId); toBrickNode.connectedNotches.add(toNotchId); @@ -204,15 +203,15 @@ export default class BrickTreeManager { }; if (fromTree.id === toTree.id) { + // Both bricks are already in the same tree, just add the new connection fromTree.connections.push(connection); this.updateParentChildRelationships(fromBrickId, toBrickId, connection.type); - console.log(`Added connection to existing tree ${fromTree.id}`); return fromTree.id; } + // Bricks are in different trees, merge them into a single tree const mergedTree = this.mergeTrees(fromTree, toTree, connection); this.updateParentChildRelationships(fromBrickId, toBrickId, connection.type); - console.log(`Merged trees into ${mergedTree.id}`); return mergedTree.id; } @@ -263,10 +262,8 @@ export default class BrickTreeManager { const originalTree = this.findTreeByBrickId(brickId); if (!originalTree) return { removedConnections: [], newTreeIds: [] }; - console.log(`Disconnecting brick ${brickId} from tree ${originalTree.id}`); - console.log(`Original tree connections:`, originalTree.connections); - - // Collect all descendant nodes of the disconnected brick (including the brick itself) + // Step 1: Collect all descendant nodes that will move with the disconnected brick + // This includes the brick itself and all its children (hierarchical behavior) const nodesToMove = new Map(); const stack: TTreeNode[] = [brickNode]; const visited = new Set([brickNode.brick.uuid]); @@ -275,6 +272,7 @@ export default class BrickTreeManager { const currentNode = stack.pop()!; nodesToMove.set(currentNode.brick.uuid, currentNode); + // Add all children of the current node to the stack for processing this.getBrickChildren(currentNode.brick.uuid).forEach((child) => { if (!visited.has(child.brick.uuid)) { visited.add(child.brick.uuid); @@ -283,10 +281,10 @@ export default class BrickTreeManager { }); } - console.log(`Nodes to move:`, Array.from(nodesToMove.keys())); - - // Find all connections that need to be removed from the original tree - // This includes connections between nodes being moved and connections to/from external nodes + // Step 2: Identify connections that need to be removed from the original tree + // Remove connections where: + // - Both nodes are moving to the new tree (internal connections) + // - One node is moving and the other stays (cross-tree connections) const connectionsToRemove = originalTree.connections.filter((conn) => { const fromInNewTree = nodesToMove.has(conn.from); const toInNewTree = nodesToMove.has(conn.to); @@ -298,29 +296,21 @@ export default class BrickTreeManager { (fromInNewTree && !toInNewTree) || (!fromInNewTree && toInNewTree); - console.log( - `Connection ${conn.from} -> ${conn.to}: fromInNewTree=${fromInNewTree}, toInNewTree=${toInNewTree}, shouldRemove=${shouldRemove}`, - ); - return shouldRemove; }); - console.log(`Connections to remove:`, connectionsToRemove); - if (connectionsToRemove.length === 0) return { removedConnections: [], newTreeIds: [] }; - // Remove connections from original tree + // Step 3: Remove connections and nodes from the original tree this.removeConnections(originalTree, connectionsToRemove); - - // Remove nodes from original tree nodesToMove.forEach((node, brickId) => { originalTree.nodes.delete(brickId); }); - // Create new tree with the disconnected brick as root + // Step 4: Create a new tree with the disconnected brick as root const newTree = this.createTree(brickNode.brick, brickNode.position); - // Add all descendant nodes to the new tree + // Step 5: Add all descendant nodes to the new tree nodesToMove.forEach((node, brickId) => { if (brickId !== brickNode.brick.uuid) { // Don't add the root twice @@ -328,13 +318,13 @@ export default class BrickTreeManager { } }); - // Move connections between nodes in the new tree to the new tree + // Step 6: Move internal connections to the new tree const connectionsToMove = connectionsToRemove.filter( (conn) => nodesToMove.has(conn.from) && nodesToMove.has(conn.to), ); newTree.connections = connectionsToMove; - // Update parent relationships for the new tree + // Step 7: Update parent relationships for the new tree // The disconnected brick becomes the root (no parent) brickNode.parent = null; @@ -349,12 +339,11 @@ export default class BrickTreeManager { } }); - // Clean up original tree if it's empty + // Step 8: Clean up original tree if it's empty if (originalTree.nodes.size === 0) { this.trees = this.trees.filter((t) => t.id !== originalTree.id); } - console.log(`Returning ${connectionsToRemove.length} removed connections`); return { removedConnections: connectionsToRemove, newTreeIds: [newTree.id] }; } From 9e2e50ee23efb1bd9b6d96b0f462a7642bde335c Mon Sep 17 00:00:00 2001 From: Saumya Shahi Date: Mon, 23 Jun 2025 01:33:54 +0530 Subject: [PATCH 14/19] chore(masonry): fix all lint errors and warnings --- modules/masonry/src/brick/utils/path.ts | 24 +++++++++---------- modules/masonry/src/tree/model/model.ts | 5 +--- .../masonry/src/tree/model/spec/model.spec.ts | 22 +++-------------- 3 files changed, 16 insertions(+), 35 deletions(-) diff --git a/modules/masonry/src/brick/utils/path.ts b/modules/masonry/src/brick/utils/path.ts index e9bfece0..405411c1 100644 --- a/modules/masonry/src/brick/utils/path.ts +++ b/modules/masonry/src/brick/utils/path.ts @@ -55,7 +55,7 @@ function _generateNotchTop(): string[] { } // function for Bottom Notch generation -function _generateNotchBottom(strokeWidth: number): string[] { +function _generateNotchBottom(_strokeWidth: number): string[] { return [ //, 'h -1', @@ -87,7 +87,7 @@ function _generateTop(config: { strokeWidth: number; bBoxLabel: TBBox; }): string[] { - const { type, hasNotch, strokeWidth, bBoxLabel, hasArgs } = config; + const { type: _type, hasNotch, strokeWidth, bBoxLabel, hasArgs } = config; // Corner Radius + Offset + Notch + Variable width + Corner Radius = Stroke Width/2 + LabelBounding Box width + Stroke Width/2 if (hasArgs) { @@ -192,25 +192,25 @@ function _generateLeft(config: { strokeWidth: number; rightVertical: number; }): { path: string[]; leftEdge: number } { - const { type, hasNotch, rightVertical } = config; + const { type: _type, hasNotch, rightVertical } = config; const path: string[] = []; - let leftEdge = rightVertical; + let _leftEdge = rightVertical; // For vertical leg — match exact vertical height from right - if (type === 'type2') { - leftEdge -= CONN_NOTCH_WIDTH + CORNER_RADIUS; - path.push(`v -${leftEdge}`); + if (_type === 'type2') { + _leftEdge -= CONN_NOTCH_WIDTH + CORNER_RADIUS; + path.push(`v -${_leftEdge}`); if (hasNotch) { path.push(..._generateNotchLeft()); } } else { - leftEdge -= CORNER_RADIUS; - path.push(`v -${leftEdge.toFixed(2)}`); + _leftEdge -= CORNER_RADIUS; + path.push(`v -${_leftEdge.toFixed(2)}`); } - return { path, leftEdge }; + return { path, leftEdge: _leftEdge }; } // function to generate the nested path for type3 bricks @@ -485,7 +485,7 @@ function getTopCentroid(config: TInputUnion): TCentroid | undefined { function getBottomCentroid( config: TInputUnion, boundingBox: TBBox, - leftEdge: number, + _leftEdge: number, ): TCentroid | undefined { if (config.type === 'type2' || !config.hasNotchBelow) return undefined; @@ -568,7 +568,7 @@ function getRightCentroids(config: TInputUnion, boundingBox: TBBox): TCentroid[] } // Calculate centroid for left connector notch -function getLeftCentroid(config: TInputUnion, boundingBox: TBBox): TCentroid | undefined { +function getLeftCentroid(config: TInputUnion, _boundingBox: TBBox): TCentroid | undefined { if (config.type !== 'type2') return undefined; return { diff --git a/modules/masonry/src/tree/model/model.ts b/modules/masonry/src/tree/model/model.ts index f478516e..fceafa3f 100644 --- a/modules/masonry/src/tree/model/model.ts +++ b/modules/masonry/src/tree/model/model.ts @@ -1,7 +1,4 @@ -import { v4 as uuidv4 } from 'uuid'; -import type { IBrick, TBrickType } from '../../brick/@types/brick'; -import type { SimpleBrick, ExpressionBrick } from '../../brick/model/model'; -import type CompoundBrick from '../../brick/model/model'; +import type { IBrick } from '../../brick/@types/brick'; // Point type export type TPoint = { diff --git a/modules/masonry/src/tree/model/spec/model.spec.ts b/modules/masonry/src/tree/model/spec/model.spec.ts index 5b895d71..db84ed97 100644 --- a/modules/masonry/src/tree/model/spec/model.spec.ts +++ b/modules/masonry/src/tree/model/spec/model.spec.ts @@ -11,7 +11,6 @@ describe('BrickTreeManager', () => { let expressionBrick: IBrick; let expressionBrick2: IBrick; let compoundBrick: IBrick; - let simpleBrickNoNotch: IBrick; beforeEach(() => { treeManager = new BrickTreeManager(); @@ -88,21 +87,6 @@ describe('BrickTreeManager', () => { topNotch: true, bottomNotch: true, }); - - simpleBrickNoNotch = new SimpleBrick({ - uuid: 'no-notch', - name: 'No Notch', - label: 'nn', - labelType: 'text', - colorBg: 'grey', - colorFg: 'white', - strokeColor: 'black', - shadow: false, - scale: 1, - bboxArgs: [], - topNotch: true, - bottomNotch: false, - }); }); describe('Tree Creation', () => { @@ -207,7 +191,7 @@ describe('BrickTreeManager', () => { simpleBrick2.uuid, compoundBrick.uuid, simpleBrick2.connectionPoints.bottom!, - (compoundBrick as any).connectionPoints.top!, + (compoundBrick as IBrick).connectionPoints.top!, 'top-bottom', ); expect(treeManager.getAllTrees()).toHaveLength(1); @@ -229,7 +213,7 @@ describe('BrickTreeManager', () => { simpleBrick2.uuid, compoundBrick.uuid, simpleBrick2.connectionPoints.bottom!, - (compoundBrick as any).connectionPoints.top!, + (compoundBrick as IBrick).connectionPoints.top!, 'top-bottom', ); @@ -253,7 +237,7 @@ describe('BrickTreeManager', () => { simpleBrick1.uuid, compoundBrick.uuid, simpleBrick1.connectionPoints.bottom!, - (compoundBrick as any).connectionPoints.top!, + (compoundBrick as IBrick).connectionPoints.top!, 'top-bottom', ); expect(result).toBeNull(); From ae77b0a7a1fd53ac76a12d3886eb479561ee4dc6 Mon Sep 17 00:00:00 2001 From: Justin Charles Date: Mon, 23 Jun 2025 03:06:03 +0530 Subject: [PATCH 15/19] feat(masonry): add prop to extract bbox and connpoints Signed-off-by: Justin Charles --- .../src/brick/view/components/compound.tsx | 23 +++++++++++++++-- .../src/brick/view/components/expression.tsx | 23 +++++++++++++++-- .../src/brick/view/components/simple.tsx | 25 ++++++++++++++++--- 3 files changed, 64 insertions(+), 7 deletions(-) diff --git a/modules/masonry/src/brick/view/components/compound.tsx b/modules/masonry/src/brick/view/components/compound.tsx index 8ffda4c3..f0d5e894 100644 --- a/modules/masonry/src/brick/view/components/compound.tsx +++ b/modules/masonry/src/brick/view/components/compound.tsx @@ -35,7 +35,18 @@ function measureLabel(label: string, fontSize: number) { }; } -export const CompoundBrickView: React.FC = (props) => { +type TConnectionPoints = { + top?: { x: number; y: number }; + right: { x: number; y: number }[]; + bottom?: { x: number; y: number }; + left?: { x: number; y: number }; +}; + +type PropsWithMetrics = TBrickRenderPropsCompound & { + RenderMetrics?: (bbox: { w: number; h: number }, connectionPoints: TConnectionPoints) => void; +}; + +export const CompoundBrickView: React.FC = (props) => { const { label, labelType, @@ -54,6 +65,7 @@ export const CompoundBrickView: React.FC = (props) => bottomNotch, bboxNest, isFolded, + RenderMetrics, } = props; // Memoize bBoxLabel to prevent unnecessary recalculations @@ -77,7 +89,11 @@ export const CompoundBrickView: React.FC = (props) => secondaryLabel: !isFolded, }; const brickData = generateBrickData(cfg); - return { path: brickData.path, w: brickData.boundingBox.w, h: brickData.boundingBox.h }; + return { + path: brickData.path, + w: brickData.boundingBox.w, + h: brickData.boundingBox.h, + }; }); useEffect(() => { @@ -93,6 +109,9 @@ export const CompoundBrickView: React.FC = (props) => secondaryLabel: !isFolded, }; const brickData = generateBrickData(cfg); + if (RenderMetrics) { + RenderMetrics(brickData.boundingBox, brickData.connectionPoints); + } setShape({ path: brickData.path, w: brickData.boundingBox.w, h: brickData.boundingBox.h }); }, [ label, diff --git a/modules/masonry/src/brick/view/components/expression.tsx b/modules/masonry/src/brick/view/components/expression.tsx index 7f3dc388..314dba98 100644 --- a/modules/masonry/src/brick/view/components/expression.tsx +++ b/modules/masonry/src/brick/view/components/expression.tsx @@ -34,7 +34,18 @@ function measureLabel(label: string, fontSize: number) { }; } -export const ExpressionBrickView: React.FC = (props) => { +type TConnectionPoints = { + top?: { x: number; y: number }; + right: { x: number; y: number }[]; + bottom?: { x: number; y: number }; + left?: { x: number; y: number }; +}; + +type PropsWithMetrics = TBrickRenderPropsExpression & { + RenderMetrics?: (bbox: { w: number; h: number }, connectionPoints: TConnectionPoints) => void; +}; + +export const ExpressionBrickView: React.FC = (props) => { const { label, labelType, @@ -49,6 +60,7 @@ export const ExpressionBrickView: React.FC = (props visualState, isActionMenuOpen, isVisible, + RenderMetrics, // value, isValueSelectOpen, // if needed later } = props; @@ -67,7 +79,11 @@ export const ExpressionBrickView: React.FC = (props bBoxArgs: bboxArgs, }; const brickData = generateBrickData(cfg); - return { path: brickData.path, w: brickData.boundingBox.w, h: brickData.boundingBox.h }; + return { + path: brickData.path, + w: brickData.boundingBox.w, + h: brickData.boundingBox.h, + }; }); useEffect(() => { @@ -79,6 +95,9 @@ export const ExpressionBrickView: React.FC = (props bBoxArgs: bboxArgs, }; const brickData = generateBrickData(cfg); + if (RenderMetrics) { + RenderMetrics(brickData.boundingBox, brickData.connectionPoints); + } setShape({ path: brickData.path, w: brickData.boundingBox.w, h: brickData.boundingBox.h }); }, [label, strokeWidth, scale, bboxArgs, bBoxLabel]); diff --git a/modules/masonry/src/brick/view/components/simple.tsx b/modules/masonry/src/brick/view/components/simple.tsx index e0e19ce6..330733fe 100644 --- a/modules/masonry/src/brick/view/components/simple.tsx +++ b/modules/masonry/src/brick/view/components/simple.tsx @@ -6,7 +6,7 @@ const FONT_HEIGHT = 16; const PADDING = { top: 4, - right: 8, + right: 10, bottom: 4, left: 8, }; @@ -34,7 +34,18 @@ function measureLabel(label: string, fontSize: number) { }; } -export const SimpleBrickView: React.FC = (props) => { +type TConnectionPoints = { + top?: { x: number; y: number }; + right: { x: number; y: number }[]; + bottom?: { x: number; y: number }; + left?: { x: number; y: number }; +}; + +type PropsWithMetrics = TBrickRenderPropsSimple & { + RenderMetrics?: (bbox: { w: number; h: number }, connectionPoints: TConnectionPoints) => void; +}; + +export const SimpleBrickView: React.FC = (props) => { const { label, labelType, @@ -51,6 +62,7 @@ export const SimpleBrickView: React.FC = (props) => { isVisible, topNotch, bottomNotch, + RenderMetrics, } = props; // Memoize bBoxLabel to prevent unnecessary recalculations @@ -70,7 +82,11 @@ export const SimpleBrickView: React.FC = (props) => { hasNotchBelow: bottomNotch, }; const brickData = generateBrickData(cfg); - return { path: brickData.path, w: brickData.boundingBox.w, h: brickData.boundingBox.h }; + return { + path: brickData.path, + w: brickData.boundingBox.w, + h: brickData.boundingBox.h, + }; }); useEffect(() => { @@ -84,6 +100,9 @@ export const SimpleBrickView: React.FC = (props) => { hasNotchBelow: bottomNotch, }; const brickData = generateBrickData(cfg); + if (RenderMetrics) { + RenderMetrics(brickData.boundingBox, brickData.connectionPoints); + } setShape({ path: brickData.path, w: brickData.boundingBox.w, h: brickData.boundingBox.h }); }, [label, strokeWidth, scale, bboxArgs, topNotch, bottomNotch, bBoxLabel]); From 60aca042df4f3e767b55aa07a9e2b8d98cda0879 Mon Sep 17 00:00:00 2001 From: Justin Charles Date: Mon, 23 Jun 2025 03:25:07 +0530 Subject: [PATCH 16/19] feat(Masonry): add public variables to store bbox ad conn points in brick model Signed-off-by: Justin Charles --- modules/masonry/src/brick/model/model.ts | 36 +++++++++++++++--------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/modules/masonry/src/brick/model/model.ts b/modules/masonry/src/brick/model/model.ts index 74c8f04d..87a3994f 100644 --- a/modules/masonry/src/brick/model/model.ts +++ b/modules/masonry/src/brick/model/model.ts @@ -12,7 +12,7 @@ import type { IBrickCompound, TBrickRenderPropsCompound, } from '../@types/brick'; -import type { TConnectionPoints } from '../../tree/model/model'; +import type { TConnectionPoints as TCP } from '../../tree/model/model'; import { generateBrickData } from '../utils/path'; import type { TInputUnion } from '../utils/path'; @@ -33,7 +33,8 @@ export abstract class BrickModel implements IBrick { protected _visualState: TVisualState = 'default'; protected _isActionMenuOpen = false; protected _isVisible = true; - public connectionPoints: TConnectionPoints; + + protected _connectionPoints: TCP = { right: [] }; protected _boundingBox: TExtent = { w: 0, h: 0 }; constructor(params: { @@ -62,10 +63,11 @@ export abstract class BrickModel implements IBrick { this._shadow = params.shadow; this._tooltip = params.tooltip; this._bboxArgs = params.bboxArgs; - this.connectionPoints = { right: [] }; // Default init + + this.boundingBox = { w: 0, h: 0 }; + this.connectionPoints = { right: [] }; } - // IBrick interface get uuid() { return this._uuid; } @@ -100,6 +102,16 @@ export abstract class BrickModel implements IBrick { public get boundingBox(): TExtent { return this._boundingBox; } + public set boundingBox(box: TExtent) { + this._boundingBox = box; + } + + public get connectionPoints(): TCP { + return this._connectionPoints; + } + public set connectionPoints(points: TCP) { + this._connectionPoints = points; + } protected getCommonRenderProps(): TBrickRenderProps { return { @@ -126,7 +138,7 @@ export abstract class BrickModel implements IBrick { /** * @class - * Final concrete class for Simple-statement bricks., + * Final concrete class for Simple-statement bricks. */ export class SimpleBrick extends BrickModel implements IBrickSimple { private _topNotch: boolean; @@ -178,14 +190,15 @@ export class SimpleBrick extends BrickModel implements IBrickSimple { hasNotchBelow: this._bottomNotch, }; const data = generateBrickData(config); + + // use the public setters this.connectionPoints = data.connectionPoints; - this._boundingBox = data.boundingBox; + this.boundingBox = data.boundingBox; } public get topNotch(): boolean { return this._topNotch; } - public get bottomNotch(): boolean { return this._bottomNotch; } @@ -252,13 +265,12 @@ export class ExpressionBrick extends BrickModel implements IBrickExpression { }; const data = generateBrickData(config); this.connectionPoints = data.connectionPoints; - this._boundingBox = data.boundingBox; + this.boundingBox = data.boundingBox; } public get value(): boolean | number | string | undefined { return this._value; } - public get isValueSelectOpen(): boolean { return this._isValueSelectOpen; } @@ -338,28 +350,24 @@ export default class CompoundBrick extends BrickModel implements IBrickCompound }; const data = generateBrickData(config); this.connectionPoints = data.connectionPoints; - this._boundingBox = data.boundingBox; + this.boundingBox = data.boundingBox; } public get topNotch(): boolean { return this._topNotch; } - public get bottomNotch(): boolean { return this._bottomNotch; } - public get bboxNest(): TExtent[] { return this._bboxNest; } - public get isFolded(): boolean { return this._isFolded; } public set isFolded(v: boolean) { this._isFolded = v; } - public setBoundingBoxNest(extents: TExtent[]): void { this._bboxNest = extents; } From 65c7fade14be206963a5de7e3eac70871b061c0b Mon Sep 17 00:00:00 2001 From: Justin Charles Date: Mon, 23 Jun 2025 03:54:10 +0530 Subject: [PATCH 17/19] docs(masonry): Add algorithm for parsing the tree Signed-off-by: Justin Charles --- modules/masonry/docs/technical-specification/Algorithm_Tree.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 modules/masonry/docs/technical-specification/Algorithm_Tree.md diff --git a/modules/masonry/docs/technical-specification/Algorithm_Tree.md b/modules/masonry/docs/technical-specification/Algorithm_Tree.md new file mode 100644 index 00000000..e69de29b From 4042477728a9108d412f69775aecdf06687d7d53 Mon Sep 17 00:00:00 2001 From: Justin Charles Date: Mon, 23 Jun 2025 03:55:51 +0530 Subject: [PATCH 18/19] "docs(masonry): Add algorithm for tree parsing" --- .../technical-specification/Algorithm_Tree.md | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) diff --git a/modules/masonry/docs/technical-specification/Algorithm_Tree.md b/modules/masonry/docs/technical-specification/Algorithm_Tree.md index e69de29b..aec7ecf8 100644 --- a/modules/masonry/docs/technical-specification/Algorithm_Tree.md +++ b/modules/masonry/docs/technical-specification/Algorithm_Tree.md @@ -0,0 +1,171 @@ +# Tower Parsing & Layout Algorithm + +This document describes a **stack-based post-order traversal** that computes each brick’s SVG path, bounding box, and notch‐connection points **after** all of its children have been measured. It handles arbitrarily deep nesting, distinguishes **expression** vs. **simple** vs. **compound** bricks, and avoids JavaScript call‐stack limits. + +--- + +## Data Structures + +```ts +// Represents one node in the tree (only UUID is needed here) +type TreeNode = { + uuid: string +} + +// Holds the computed metrics for a brick +type Metrics = { + path: string + bbox: { w: number; h: number } + connectionPoints: ConnectionPoints +} + +// Frame in our explicit stack +type Frame = { + node: TreeNode + visited: boolean +} + +// Work‐in‐progress structures +const metricsMap: Map = new Map() +const stack: Frame[] = [] +``` + +The model must expose: + +```ts +getRoots(): string[] + +getBrickType(uuid: string): 'expression' | 'simple' | 'compound' + +getExpressionArgs(uuid: string): string[] + +getNestedBlocks(uuid: string): string[] + +// Optionally for simple/compound +type StatementChildren = { top: string; bottom: string } +getStatementChildren(uuid: string): StatementChildren + +getBrickProps(uuid: string): { + label: string + type: 'expression' | 'simple' | 'compound' + strokeWidth: number + scale: number + topNotch: boolean + bottomNotch: boolean + fontSize: number +} +``` + +Shared utilities: + +```ts +generatePath(config): string +getBoundingBox(config): { w: number; h: number } +deriveNotches(path, bbox): ConnectionPoints +measureLabel(text, fontSize): { w: number; h: number; ascent: number; descent: number } +``` + +--- + +## Algorithm Steps + +1. **Initialize Work Structures** + + - Empty metrics map + + ```pseudo + metricsMap ← {} + stack ← [] + ``` + + - Seed roots + + ```pseudo + for each rootUuid in model.getRoots(): + stack.push({ node: { uuid: rootUuid }, visited: false }) + ``` + + We mark `visited = false` on first encounter (“children not yet handled”) and will re-push the same node with `visited = true` (“ready to compute”) after its children. + +2. **Process Frames Until Done** + + ```pseudo + while stack is not empty: + (currentNode, visited) ← stack.pop() + + if visited == false: + // --- First encounter: enqueue children, defer parent --- + stack.push({ node: currentNode, visited: true }) + + // 1) Determine children based on brick type + type ← model.getBrickType(currentNode.uuid) + if type == 'expression': + children ← [] + else if type == 'simple': + children ← model.getExpressionArgs(currentNode.uuid) + + model.getStatementChildren(currentNode.uuid) + else if type == 'compound': + children ← model.getNestedBlocks(currentNode.uuid) + + model.getExpressionArgs(currentNode.uuid) + + model.getStatementChildren(currentNode.uuid) + + // 2) Push children so they come off *before* the parent’s second visit + for each childUuid in reverse(children): + stack.push({ node: { uuid: childUuid }, visited: false }) + + else: + // --- Second encounter: all children are ready, compute metrics --- + // 1) Gather child bounding boxes + childBBoxes ← [] + for each cUuid in model.getExpressionArgs(currentNode.uuid) + + model.getNestedBlocks(currentNode.uuid) + + model.getStatementChildren(currentNode.uuid): + childBBoxes.push(metricsMap[cUuid].bbox) + + // 2) Fetch brick props and measure its label + props ← model.getBrickProps(currentNode.uuid) + labelBox ← measureLabel(props.label, props.fontSize) + + // 3) Assemble config + config = { + type: props.type, + strokeWidth: props.strokeWidth, + scaleFactor: props.scale, + bBoxLabel: { w: labelBox.w, h: labelBox.h }, + bBoxArgs: childBBoxes, + hasNotchAbove: props.topNotch, + hasNotchBelow: props.bottomNotch + } + + // 4) Compute path, bbox, notch‐coords + path ← generatePath(config) + bbox ← getBoundingBox(config) + connectionPoints ← deriveNotches(path, bbox) + + // 5) Store into metricsMap + metricsMap[currentNode.uuid] = { path, bbox, connectionPoints } + ``` + + **Key invariant**: When a node’s frame is popped with `visited = true`, all of its children (and their entire subtrees) have already been computed and stored in `metricsMap`. + +3. **Completion** + + When the stack empties, `metricsMap` contains the final layout data for every brick: + + - SVG outline (`path`) + - Dimensions (`bbox`) + - Notch coordinates (`connectionPoints`) + + You can now feed these into your React components or canvas renderer in a single, child‐first batch. + +--- + +## Why This Approach + +- **Post‐order traversal** ensures each statement/compound block sizes itself around fully measured plugged‐in bricks. +- **Explicit stack** avoids recursion limits—safe for arbitrary nesting depth. +- **Type‐aware child selection** respects the two “directions” of plug‐ins: + - Expression bricks never act as parents (no children). + - Simple bricks only host expression‐slot arguments (and top/bottom chain). + - Compound bricks first nest entire inner‐blocks, then expression args, then statement chaining. +- **No extra bookkeeping**: the “two‐push visited‐flag” trick implicitly tracks when children are done without counters or complex state. From 2f6e763e614b7662e8737c452fa9cd2e1f6b7d5b Mon Sep 17 00:00:00 2001 From: Justin Charles Date: Mon, 23 Jun 2025 04:10:43 +0530 Subject: [PATCH 19/19] chore(masonry): fix doc linting errors Signed-off-by: Justin Charles --- .../technical-specification/Algorithm_Tree.md | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/modules/masonry/docs/technical-specification/Algorithm_Tree.md b/modules/masonry/docs/technical-specification/Algorithm_Tree.md index aec7ecf8..33356102 100644 --- a/modules/masonry/docs/technical-specification/Algorithm_Tree.md +++ b/modules/masonry/docs/technical-specification/Algorithm_Tree.md @@ -1,6 +1,9 @@ # Tower Parsing & Layout Algorithm -This document describes a **stack-based post-order traversal** that computes each brick’s SVG path, bounding box, and notch‐connection points **after** all of its children have been measured. It handles arbitrarily deep nesting, distinguishes **expression** vs. **simple** vs. **compound** bricks, and avoids JavaScript call‐stack limits. +This document describes a **stack-based post-order traversal** that computes each brick’s SVG path, +bounding box, and notch‐connection points **after** all of its children have been measured. It +handles arbitrarily deep nesting, distinguishes **expression** vs. **simple** vs. **compound** +bricks, and avoids JavaScript call‐stack limits. --- @@ -85,7 +88,8 @@ measureLabel(text, fontSize): { w: number; h: number; ascent: number; descent: n stack.push({ node: { uuid: rootUuid }, visited: false }) ``` - We mark `visited = false` on first encounter (“children not yet handled”) and will re-push the same node with `visited = true` (“ready to compute”) after its children. + We mark `visited = false` on first encounter (“children not yet handled”) and will re-push the +same node with `visited = true` (“ready to compute”) after its children. 2. **Process Frames Until Done** @@ -146,7 +150,8 @@ measureLabel(text, fontSize): { w: number; h: number; ascent: number; descent: n metricsMap[currentNode.uuid] = { path, bbox, connectionPoints } ``` - **Key invariant**: When a node’s frame is popped with `visited = true`, all of its children (and their entire subtrees) have already been computed and stored in `metricsMap`. + **Key invariant**: When a node’s frame is popped with `visited = true`, all of its children (and +their entire subtrees) have already been computed and stored in `metricsMap`. 3. **Completion** @@ -156,16 +161,19 @@ measureLabel(text, fontSize): { w: number; h: number; ascent: number; descent: n - Dimensions (`bbox`) - Notch coordinates (`connectionPoints`) - You can now feed these into your React components or canvas renderer in a single, child‐first batch. + You can now feed these into your React components or canvas renderer in a single, child‐first +batch. --- ## Why This Approach -- **Post‐order traversal** ensures each statement/compound block sizes itself around fully measured plugged‐in bricks. +- **Post‐order traversal** ensures each statement/compound block sizes itself around fully measured +plugged‐in bricks. - **Explicit stack** avoids recursion limits—safe for arbitrary nesting depth. - **Type‐aware child selection** respects the two “directions” of plug‐ins: - Expression bricks never act as parents (no children). - Simple bricks only host expression‐slot arguments (and top/bottom chain). - Compound bricks first nest entire inner‐blocks, then expression args, then statement chaining. -- **No extra bookkeeping**: the “two‐push visited‐flag” trick implicitly tracks when children are done without counters or complex state. +- **No extra bookkeeping**: the “two‐push visited‐flag” trick implicitly tracks when children are +done without counters or complex state.