Skip to content

Commit a805426

Browse files
authored
Merge pull request #442 from sugarlabs/gsoc-dmp-2025/week-3/saumya
GSoC/DMP 2025 Week 3: feat(Masonry): Implement Brick Tree Model and add Connection points
2 parents 95487a2 + 2f6e763 commit a805426

File tree

10 files changed

+1326
-126
lines changed

10 files changed

+1326
-126
lines changed
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# Tower Parsing & Layout Algorithm
2+
3+
This document describes a **stack-based post-order traversal** that computes each brick’s SVG path,
4+
bounding box, and notch‐connection points **after** all of its children have been measured. It
5+
handles arbitrarily deep nesting, distinguishes **expression** vs. **simple** vs. **compound**
6+
bricks, and avoids JavaScript call‐stack limits.
7+
8+
---
9+
10+
## Data Structures
11+
12+
```ts
13+
// Represents one node in the tree (only UUID is needed here)
14+
type TreeNode = {
15+
uuid: string
16+
}
17+
18+
// Holds the computed metrics for a brick
19+
type Metrics = {
20+
path: string
21+
bbox: { w: number; h: number }
22+
connectionPoints: ConnectionPoints
23+
}
24+
25+
// Frame in our explicit stack
26+
type Frame = {
27+
node: TreeNode
28+
visited: boolean
29+
}
30+
31+
// Work‐in‐progress structures
32+
const metricsMap: Map<string, Metrics> = new Map()
33+
const stack: Frame[] = []
34+
```
35+
36+
The model must expose:
37+
38+
```ts
39+
getRoots(): string[]
40+
41+
getBrickType(uuid: string): 'expression' | 'simple' | 'compound'
42+
43+
getExpressionArgs(uuid: string): string[]
44+
45+
getNestedBlocks(uuid: string): string[]
46+
47+
// Optionally for simple/compound
48+
type StatementChildren = { top: string; bottom: string }
49+
getStatementChildren(uuid: string): StatementChildren
50+
51+
getBrickProps(uuid: string): {
52+
label: string
53+
type: 'expression' | 'simple' | 'compound'
54+
strokeWidth: number
55+
scale: number
56+
topNotch: boolean
57+
bottomNotch: boolean
58+
fontSize: number
59+
}
60+
```
61+
62+
Shared utilities:
63+
64+
```ts
65+
generatePath(config): string
66+
getBoundingBox(config): { w: number; h: number }
67+
deriveNotches(path, bbox): ConnectionPoints
68+
measureLabel(text, fontSize): { w: number; h: number; ascent: number; descent: number }
69+
```
70+
71+
---
72+
73+
## Algorithm Steps
74+
75+
1. **Initialize Work Structures**
76+
77+
- Empty metrics map
78+
79+
```pseudo
80+
metricsMap ← {}
81+
stack ← []
82+
```
83+
84+
- Seed roots
85+
86+
```pseudo
87+
for each rootUuid in model.getRoots():
88+
stack.push({ node: { uuid: rootUuid }, visited: false })
89+
```
90+
91+
We mark `visited = false` on first encounter (“children not yet handled”) and will re-push the
92+
same node with `visited = true` (“ready to compute”) after its children.
93+
94+
2. **Process Frames Until Done**
95+
96+
```pseudo
97+
while stack is not empty:
98+
(currentNode, visited) ← stack.pop()
99+
100+
if visited == false:
101+
// --- First encounter: enqueue children, defer parent ---
102+
stack.push({ node: currentNode, visited: true })
103+
104+
// 1) Determine children based on brick type
105+
type ← model.getBrickType(currentNode.uuid)
106+
if type == 'expression':
107+
children ← []
108+
else if type == 'simple':
109+
children ← model.getExpressionArgs(currentNode.uuid)
110+
+ model.getStatementChildren(currentNode.uuid)
111+
else if type == 'compound':
112+
children ← model.getNestedBlocks(currentNode.uuid)
113+
+ model.getExpressionArgs(currentNode.uuid)
114+
+ model.getStatementChildren(currentNode.uuid)
115+
116+
// 2) Push children so they come off *before* the parent’s second visit
117+
for each childUuid in reverse(children):
118+
stack.push({ node: { uuid: childUuid }, visited: false })
119+
120+
else:
121+
// --- Second encounter: all children are ready, compute metrics ---
122+
// 1) Gather child bounding boxes
123+
childBBoxes ← []
124+
for each cUuid in model.getExpressionArgs(currentNode.uuid)
125+
+ model.getNestedBlocks(currentNode.uuid)
126+
+ model.getStatementChildren(currentNode.uuid):
127+
childBBoxes.push(metricsMap[cUuid].bbox)
128+
129+
// 2) Fetch brick props and measure its label
130+
props ← model.getBrickProps(currentNode.uuid)
131+
labelBox ← measureLabel(props.label, props.fontSize)
132+
133+
// 3) Assemble config
134+
config = {
135+
type: props.type,
136+
strokeWidth: props.strokeWidth,
137+
scaleFactor: props.scale,
138+
bBoxLabel: { w: labelBox.w, h: labelBox.h },
139+
bBoxArgs: childBBoxes,
140+
hasNotchAbove: props.topNotch,
141+
hasNotchBelow: props.bottomNotch
142+
}
143+
144+
// 4) Compute path, bbox, notch‐coords
145+
path ← generatePath(config)
146+
bbox ← getBoundingBox(config)
147+
connectionPoints ← deriveNotches(path, bbox)
148+
149+
// 5) Store into metricsMap
150+
metricsMap[currentNode.uuid] = { path, bbox, connectionPoints }
151+
```
152+
153+
**Key invariant**: When a node’s frame is popped with `visited = true`, all of its children (and
154+
their entire subtrees) have already been computed and stored in `metricsMap`.
155+
156+
3. **Completion**
157+
158+
When the stack empties, `metricsMap` contains the final layout data for every brick:
159+
160+
- SVG outline (`path`)
161+
- Dimensions (`bbox`)
162+
- Notch coordinates (`connectionPoints`)
163+
164+
You can now feed these into your React components or canvas renderer in a single, child‐first
165+
batch.
166+
167+
---
168+
169+
## Why This Approach
170+
171+
- **Post‐order traversal** ensures each statement/compound block sizes itself around fully measured
172+
plugged‐in bricks.
173+
- **Explicit stack** avoids recursion limits—safe for arbitrary nesting depth.
174+
- **Type‐aware child selection** respects the two “directions” of plug‐ins:
175+
- Expression bricks never act as parents (no children).
176+
- Simple bricks only host expression‐slot arguments (and top/bottom chain).
177+
- Compound bricks first nest entire inner‐blocks, then expression args, then statement chaining.
178+
- **No extra bookkeeping**: the “two‐push visited‐flag” trick implicitly tracks when children are
179+
done without counters or complex state.

modules/masonry/src/brick/@types/brick.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ export type TBrickRenderPropsCompound = TBrickRenderProps & {
6565
isFolded: boolean;
6666
};
6767

68+
import { TConnectionPoints } from '../../tree/model/model';
69+
6870
/**
6971
* @interface
7072
* Type definition of a brick (any type).
@@ -75,6 +77,7 @@ export interface IBrick {
7577
get type(): TBrickType;
7678
set scale(value: number);
7779
get boundingBox(): TExtent;
80+
connectionPoints: TConnectionPoints;
7881

7982
get visualState(): TVisualState;
8083
set visualState(value: TVisualState);

0 commit comments

Comments
 (0)