Skip to content

Commit 051d754

Browse files
committed
feat(masonry): [tower] seperate tower logic (one connected stack), simplify code and add stories
Signed-off-by: karan-palan <[email protected]>
1 parent 702bed0 commit 051d754

File tree

4 files changed

+824
-0
lines changed

4 files changed

+824
-0
lines changed
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import type { IBrick } from '../../@types/brick';
2+
import type {
3+
TPoint,
4+
TNotchType,
5+
TBrickConnection,
6+
TConnectionValidation,
7+
} from '../../@types/tower';
8+
9+
/**
10+
* Public representation of a node inside a tower.
11+
*/
12+
export interface ITowerNode {
13+
brick: IBrick;
14+
position: TPoint;
15+
parent: ITowerNode | null;
16+
connectedNotches: Set<string>;
17+
isNested?: boolean;
18+
argIndex?: number;
19+
}
20+
21+
/**
22+
* A Tower represents one connected graph / stack of bricks.
23+
*/
24+
export default class TowerModel {
25+
readonly id: string;
26+
27+
// Internal graph data
28+
private readonly nodes = new Map<string, ITowerNode>();
29+
private connections: TBrickConnection[] = [];
30+
31+
constructor(id: string, rootBrick: IBrick, position: TPoint) {
32+
this.id = id;
33+
const rootNode: ITowerNode = {
34+
brick: rootBrick,
35+
position,
36+
parent: null,
37+
connectedNotches: new Set(),
38+
};
39+
this.nodes.set(rootBrick.uuid, rootNode);
40+
}
41+
42+
/** All bricks currently in this tower */
43+
get bricks(): IBrick[] {
44+
return Array.from(this.nodes.values()).map((n) => n.brick);
45+
}
46+
47+
/** All physical connections inside this tower */
48+
get allConnections(): readonly TBrickConnection[] {
49+
return this.connections;
50+
}
51+
52+
hasBrick(brickId: string): boolean {
53+
return this.nodes.has(brickId);
54+
}
55+
56+
/** Direct accessors guarded by readonly wrappers */
57+
getNode(brickId: string): ITowerNode | undefined {
58+
return this.nodes.get(brickId);
59+
}
60+
61+
nodesArray(): ITowerNode[] {
62+
return Array.from(this.nodes.values());
63+
}
64+
65+
/** Position helpers */
66+
getBrickPosition(brickId: string): TPoint | undefined {
67+
return this.nodes.get(brickId)?.position;
68+
}
69+
setBrickPosition(brickId: string, pos: TPoint): void {
70+
const node = this.nodes.get(brickId);
71+
if (node) node.position = pos;
72+
}
73+
74+
/**
75+
* Add a child brick under an existing parent brick inside this tower.
76+
*/
77+
addBrick(parentId: string, brick: IBrick, position: TPoint): void {
78+
const parentNode = this.nodes.get(parentId);
79+
if (!parentNode) throw new Error('Parent brick not found in tower');
80+
81+
const node: ITowerNode = {
82+
brick,
83+
position,
84+
parent: parentNode,
85+
connectedNotches: new Set(),
86+
};
87+
this.nodes.set(brick.uuid, node);
88+
}
89+
90+
/**
91+
* Add an argument brick to a parent brick at a specific argument slot.
92+
*/
93+
addArgumentBrick(parentId: string, brick: IBrick, position: TPoint, argIndex?: number): void {
94+
const parentNode = this.nodes.get(parentId);
95+
if (!parentNode) throw new Error('Parent brick not found in tower');
96+
97+
const node: ITowerNode = {
98+
brick,
99+
position,
100+
parent: parentNode,
101+
connectedNotches: new Set(),
102+
argIndex,
103+
};
104+
this.nodes.set(brick.uuid, node);
105+
}
106+
107+
/**
108+
* Add a nested brick inside a compound brick.
109+
*/
110+
addNestedBrick(parentId: string, brick: IBrick, position: TPoint): void {
111+
const parentNode = this.nodes.get(parentId);
112+
if (!parentNode) throw new Error('Parent brick not found in tower');
113+
114+
const node: ITowerNode = {
115+
brick,
116+
position,
117+
parent: parentNode,
118+
connectedNotches: new Set(),
119+
isNested: true,
120+
};
121+
this.nodes.set(brick.uuid, node);
122+
}
123+
124+
/**
125+
* Connect two bricks inside this tower.
126+
*/
127+
connectBricks(
128+
fromBrickId: string,
129+
toBrickId: string,
130+
fromNotchId: string,
131+
toNotchId: string,
132+
type: TNotchType,
133+
): TConnectionValidation {
134+
const fromNode = this.nodes.get(fromBrickId);
135+
const toNode = this.nodes.get(toBrickId);
136+
if (!fromNode || !toNode) return { isValid: false, reason: 'Brick(s) not in this tower' };
137+
138+
if (fromNode.connectedNotches.has(fromNotchId) || toNode.connectedNotches.has(toNotchId)) {
139+
return { isValid: false, reason: 'One or both notches already connected' };
140+
}
141+
142+
fromNode.connectedNotches.add(fromNotchId);
143+
toNode.connectedNotches.add(toNotchId);
144+
145+
this.connections.push({
146+
from: fromBrickId,
147+
to: toBrickId,
148+
fromNotchId,
149+
toNotchId,
150+
type,
151+
});
152+
153+
if (type === 'top-bottom' || type === 'right-left') {
154+
toNode.parent = fromNode;
155+
} else if (type === 'left-right') {
156+
fromNode.parent = toNode;
157+
}
158+
return { isValid: true };
159+
}
160+
161+
/**
162+
* Merge all nodes & connections from `other` into this tower.
163+
* Duplicates (by brick uuid) are ignored.
164+
*/
165+
mergeIn(other: TowerModel): void {
166+
other.nodes.forEach((node, id) => {
167+
if (!this.nodes.has(id)) {
168+
this.nodes.set(id, node);
169+
}
170+
});
171+
172+
// Avoid pushing duplicate connections
173+
const existing = new Set(this.connections.map((c) => JSON.stringify(c)));
174+
other.allConnections.forEach((c) => {
175+
const key = JSON.stringify(c);
176+
if (!existing.has(key)) this.connections.push(c);
177+
});
178+
}
179+
180+
/**
181+
* Detach a subtree starting from `brickId`, returning a **new** `TowerModel`.
182+
*/
183+
detachSubTree(
184+
brickId: string,
185+
newTowerId: string,
186+
): {
187+
detachedTower: TowerModel;
188+
removedConnections: TBrickConnection[];
189+
} {
190+
const startNode = this.nodes.get(brickId);
191+
if (!startNode) throw new Error('Brick not found in tower');
192+
193+
// collect all nodes in the subtree (DFS)
194+
const nodesToMove = new Map<string, ITowerNode>();
195+
const stack: ITowerNode[] = [startNode];
196+
while (stack.length) {
197+
const n = stack.pop()!;
198+
nodesToMove.set(n.brick.uuid, n);
199+
this.nodes.forEach((child) => {
200+
if (child.parent?.brick.uuid === n.brick.uuid) stack.push(child);
201+
});
202+
}
203+
204+
// create the new tower
205+
const rootPos = { ...startNode.position };
206+
const detached = new TowerModel(newTowerId, startNode.brick, rootPos);
207+
208+
nodesToMove.forEach((node, id) => {
209+
if (id === brickId) return; // root already exists in detached
210+
detached.nodes.set(id, node);
211+
});
212+
213+
// move / prune connections
214+
const removedConnections: TBrickConnection[] = [];
215+
this.connections = this.connections.filter((conn) => {
216+
const inSubtree = nodesToMove.has(conn.from) && nodesToMove.has(conn.to);
217+
if (inSubtree) {
218+
detached.connections.push(conn);
219+
return false; // remove from original tower
220+
}
221+
const touchesSubtree = nodesToMove.has(conn.from) || nodesToMove.has(conn.to);
222+
if (touchesSubtree) removedConnections.push(conn);
223+
return !touchesSubtree;
224+
});
225+
226+
// finally delete nodes from original tower
227+
nodesToMove.forEach((_, id) => this.nodes.delete(id));
228+
229+
return { detachedTower: detached, removedConnections };
230+
}
231+
}

0 commit comments

Comments
 (0)