Skip to content

Commit 648fbb3

Browse files
committed
feat(masonry): [workspace] create Workspace manager which perfroms CRUD operations using towers and handles connection logic
Signed-off-by: karan-palan <[email protected]>
1 parent 051d754 commit 648fbb3

File tree

4 files changed

+378
-0
lines changed

4 files changed

+378
-0
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import type { IBrick } from '../../@types/brick';
2+
import TowerModel from '../../tower/model/model';
3+
import type { TPoint, TNotchType } from '../../@types/tower';
4+
5+
/**
6+
* The **Workspace** coordinates *multiple* towers and canvas‑level concerns
7+
* (selection, z‑order, undo-redo, etc.).
8+
*/
9+
export default class WorkspaceManager {
10+
private towers = new Map<string, TowerModel>();
11+
private _nextId = 1;
12+
13+
private genId(prefix = 'tower'): string {
14+
return `${prefix}_${this._nextId++}`;
15+
}
16+
17+
// CRUD operations
18+
createTower(rootBrick: IBrick, position: TPoint): TowerModel {
19+
const id = this.genId();
20+
const tower = new TowerModel(id, rootBrick, position);
21+
this.towers.set(id, tower);
22+
return tower;
23+
}
24+
25+
removeTower(towerId: string): boolean {
26+
return this.towers.delete(towerId);
27+
}
28+
29+
getTower(towerId: string): TowerModel | undefined {
30+
return this.towers.get(towerId);
31+
}
32+
33+
get allTowers(): readonly TowerModel[] {
34+
return Array.from(this.towers.values());
35+
}
36+
37+
clear(): void {
38+
this.towers.clear();
39+
this._nextId = 1;
40+
}
41+
42+
/**
43+
* Attempt to connect bricks that *may* live in different towers.
44+
* If valid and they belong to different towers, the towers are merged.
45+
*/
46+
connectBricksAcrossTowers(
47+
fromBrickId: string,
48+
toBrickId: string,
49+
fromNotchId: string,
50+
toNotchId: string,
51+
type: TNotchType,
52+
): void {
53+
const fromTower = this.findTowerByBrickId(fromBrickId);
54+
const toTower = this.findTowerByBrickId(toBrickId);
55+
if (!fromTower || !toTower) throw new Error('Brick(s) not found');
56+
57+
if (fromTower === toTower) {
58+
const result = fromTower.connectBricks(
59+
fromBrickId,
60+
toBrickId,
61+
fromNotchId,
62+
toNotchId,
63+
type,
64+
);
65+
if (!result.isValid) throw new Error(result.reason);
66+
return;
67+
}
68+
69+
const fromNode = fromTower.getNode(fromBrickId);
70+
const toNode = toTower.getNode(toBrickId);
71+
if (!fromNode || !toNode) throw new Error('Bricks not found in towers');
72+
if (fromNode.connectedNotches.has(fromNotchId) || toNode.connectedNotches.has(toNotchId)) {
73+
throw new Error('One or both notches already connected');
74+
}
75+
76+
fromTower.mergeIn(toTower);
77+
this.towers.delete(toTower.id);
78+
79+
const res = fromTower.connectBricks(fromBrickId, toBrickId, fromNotchId, toNotchId, type);
80+
if (!res.isValid) throw new Error(res.reason);
81+
}
82+
83+
private findTowerByBrickId(brickId: string): TowerModel | undefined {
84+
for (const tower of this.towers.values()) {
85+
if (tower.hasBrick(brickId)) return tower;
86+
}
87+
return undefined;
88+
}
89+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from 'react';
2+
import type WorkspaceManager from '../../model/model';
3+
import TowerView from '../../../tower/view/components/TowerView';
4+
5+
const WorkspaceView: React.FC<{ manager: WorkspaceManager }> = ({ manager }) => (
6+
<svg width={2000} height={1200} style={{ border: '1px solid #aaa', background: '#f9f9f9' }}>
7+
{manager.allTowers.map((tower) => (
8+
<TowerView key={tower.id} tower={tower} />
9+
))}
10+
</svg>
11+
);
12+
export default WorkspaceView;
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import React from 'react';
2+
import { Meta, StoryObj } from '@storybook/react';
3+
import WorkspaceView from '../components/WorkspaceView';
4+
import WorkspaceManager from '../../model/model';
5+
6+
import {
7+
createSimpleBrick,
8+
createExpressionBrick,
9+
createCompoundBrick,
10+
resetFactoryCounter,
11+
} from '../../../brick/utils/brickFactory';
12+
13+
type RootType = 'Simple' | 'Compound';
14+
type BrickType = 'Simple' | 'Compound';
15+
16+
type InteractiveWorkspaceProps = {
17+
numTowers: number;
18+
rootType: RootType;
19+
numArgs: number;
20+
numNested: number;
21+
stackCount: number;
22+
nestedBrickTypes: BrickType[];
23+
};
24+
25+
const InteractiveWorkspace: React.FC<InteractiveWorkspaceProps> = ({
26+
numTowers,
27+
rootType,
28+
numArgs,
29+
numNested,
30+
stackCount,
31+
nestedBrickTypes,
32+
}) => {
33+
resetFactoryCounter();
34+
const manager = new WorkspaceManager();
35+
36+
// Create multiple towers based on configuration
37+
for (let towerIndex = 0; towerIndex < numTowers; towerIndex++) {
38+
const xOffset = (towerIndex % 3) * 300 + 100;
39+
const yOffset = Math.floor(towerIndex / 3) * 400 + 100;
40+
41+
const bboxArgs = Array(numArgs).fill({ w: 60, h: 20 });
42+
43+
// Root Brick
44+
const root =
45+
rootType === 'Simple'
46+
? createSimpleBrick({ label: `Tower ${towerIndex + 1} Root`, bboxArgs })
47+
: createCompoundBrick({ label: `Tower ${towerIndex + 1} Root`, bboxArgs });
48+
49+
const tower = manager.createTower(root, { x: xOffset, y: yOffset });
50+
51+
// Add argument bricks (only expressions)
52+
for (let i = 0; i < numArgs; i++) {
53+
const expr = createExpressionBrick({ label: `Arg ${i + 1}` });
54+
tower.addArgumentBrick(root.uuid, expr, { x: 0, y: 0 }, i);
55+
}
56+
57+
// Add nested bricks (for compound only)
58+
if (root.type === 'Compound') {
59+
for (let i = 0; i < numNested; i++) {
60+
const nestedType = nestedBrickTypes[i % nestedBrickTypes.length] || 'Simple';
61+
const nested =
62+
nestedType === 'Simple'
63+
? createSimpleBrick({ label: `Nested ${i + 1}` })
64+
: createCompoundBrick({ label: `Nested ${i + 1}` });
65+
tower.addNestedBrick(root.uuid, nested, { x: 0, y: 0 });
66+
}
67+
}
68+
69+
// Add stacked bricks
70+
let currentParent = root;
71+
for (let i = 0; i < stackCount; i++) {
72+
const stacked = createSimpleBrick({ label: `Stack ${i + 1}` });
73+
tower.addBrick(currentParent.uuid, stacked, { x: 0, y: 0 });
74+
currentParent = stacked;
75+
}
76+
}
77+
78+
return <WorkspaceView manager={manager} />;
79+
};
80+
81+
const meta: Meta<typeof InteractiveWorkspace> = {
82+
title: 'Workspace/Interactive',
83+
component: InteractiveWorkspace,
84+
argTypes: {
85+
numTowers: {
86+
control: { type: 'range', min: 1, max: 9 },
87+
description: 'Number of towers to create',
88+
},
89+
rootType: {
90+
control: { type: 'select' },
91+
options: ['Simple', 'Compound'],
92+
description: 'Type of root brick for each tower',
93+
},
94+
numArgs: {
95+
control: { type: 'range', min: 0, max: 4 },
96+
description: 'Number of argument slots per root brick',
97+
},
98+
numNested: {
99+
control: { type: 'range', min: 0, max: 4 },
100+
description: 'Number of nested bricks (for compound only)',
101+
},
102+
stackCount: {
103+
control: { type: 'range', min: 0, max: 4 },
104+
description: 'Depth of stacked bricks below root',
105+
},
106+
nestedBrickTypes: {
107+
control: { type: 'check' },
108+
options: ['Simple', 'Compound'],
109+
description: 'Types of nested bricks to use',
110+
},
111+
},
112+
};
113+
114+
export default meta;
115+
type Story = StoryObj<typeof InteractiveWorkspace>;
116+
117+
export const SimpleWorkspace: Story = {
118+
args: {
119+
numTowers: 2,
120+
rootType: 'Simple',
121+
numArgs: 1,
122+
numNested: 0,
123+
stackCount: 2,
124+
nestedBrickTypes: ['Simple'],
125+
},
126+
};
127+
128+
export const ComplexWorkspace: Story = {
129+
args: {
130+
numTowers: 6,
131+
rootType: 'Compound',
132+
numArgs: 3,
133+
numNested: 3,
134+
stackCount: 2,
135+
nestedBrickTypes: ['Simple', 'Compound'],
136+
},
137+
};
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import React from 'react';
2+
import type { Meta, StoryObj } from '@storybook/react';
3+
import WorkspaceView from '../components/WorkspaceView';
4+
import WorkspaceManager from '../../model/model';
5+
6+
import {
7+
createSimpleBrick,
8+
createExpressionBrick,
9+
createCompoundBrick,
10+
resetFactoryCounter,
11+
} from '../../../brick/utils/brickFactory';
12+
13+
const meta: Meta<typeof WorkspaceView> = {
14+
title: 'Workspace/Fixed',
15+
component: WorkspaceView,
16+
};
17+
export default meta;
18+
19+
type Story = StoryObj<typeof WorkspaceView>;
20+
21+
// === Workspace Story 1: Single Tower (adapted from tree SingleBrick) ===
22+
export const SingleTower: Story = {
23+
render: () => {
24+
resetFactoryCounter();
25+
const manager = new WorkspaceManager();
26+
const brick = createSimpleBrick();
27+
manager.createTower(brick, { x: 100, y: 100 });
28+
return <WorkspaceView manager={manager} />;
29+
},
30+
};
31+
32+
// === Workspace Story 2: Multiple Separate Towers (adapted from tree concept) ===
33+
export const MultipleTowers: Story = {
34+
render: () => {
35+
resetFactoryCounter();
36+
const manager = new WorkspaceManager();
37+
38+
// Tower 1: Simple stack (like tree StackedBricks)
39+
const root1 = createSimpleBrick();
40+
const child1 = createSimpleBrick();
41+
const child2 = createSimpleBrick();
42+
const tower1 = manager.createTower(root1, { x: 100, y: 100 });
43+
tower1.addBrick(root1.uuid, child1, { x: 100, y: 130 });
44+
tower1.addBrick(child1.uuid, child2, { x: 100, y: 160 });
45+
46+
// Tower 2: Arguments (like tree ArgumentBricks)
47+
const root2 = createSimpleBrick({
48+
label: 'My Simple',
49+
bboxArgs: [
50+
{ w: 60, h: 40 },
51+
{ w: 60, h: 20 },
52+
],
53+
});
54+
const arg1 = createExpressionBrick({ label: 'Expr A', bboxArgs: [{ w: 60, h: 40 }] });
55+
const arg2 = createExpressionBrick({ label: 'Expr B', bboxArgs: [{ w: 60, h: 20 }] });
56+
const tower2 = manager.createTower(root2, { x: 400, y: 100 });
57+
tower2.addArgumentBrick(root2.uuid, arg1, { x: 0, y: 0 }, 0);
58+
tower2.addArgumentBrick(root2.uuid, arg2, { x: 0, y: 0 }, 1);
59+
60+
// Tower 3: Compound with nested (like tree CompoundWithNested)
61+
const compound = createCompoundBrick({
62+
label: 'Compound with Nested',
63+
bboxArgs: [{ w: 80, h: 40 }],
64+
});
65+
const nested1 = createSimpleBrick();
66+
const nested2 = createSimpleBrick();
67+
const tower3 = manager.createTower(compound, { x: 700, y: 100 });
68+
tower3.addNestedBrick(compound.uuid, nested1, { x: 0, y: 0 });
69+
tower3.addNestedBrick(compound.uuid, nested2, { x: 0, y: 0 });
70+
71+
return <WorkspaceView manager={manager} />;
72+
},
73+
};
74+
75+
// === Workspace Story 3: Full Composite Workspace (adapted from tree FullCompositeTree) ===
76+
export const FullCompositeWorkspace: Story = {
77+
render: () => {
78+
resetFactoryCounter();
79+
const manager = new WorkspaceManager();
80+
81+
// Tower 1: Full composite (like tree FullCompositeTree)
82+
const compound1 = createCompoundBrick({
83+
label: 'Full Composite box with args',
84+
bboxArgs: [
85+
{ w: 100, h: 50 },
86+
{ w: 120, h: 70 },
87+
],
88+
});
89+
const arg1 = createExpressionBrick({ label: 'Expr 1', bboxArgs: [{ w: 100, h: 50 }] });
90+
const arg2 = createExpressionBrick({ label: 'Expr 2', bboxArgs: [{ w: 120, h: 70 }] });
91+
const nested = createSimpleBrick();
92+
const stacked = createSimpleBrick();
93+
const tower1 = manager.createTower(compound1, { x: 100, y: 100 });
94+
tower1.addArgumentBrick(compound1.uuid, arg1, { x: 0, y: 0 }, 0);
95+
tower1.addArgumentBrick(compound1.uuid, arg2, { x: 0, y: 0 }, 1);
96+
tower1.addNestedBrick(compound1.uuid, nested, { x: 0, y: 0 });
97+
tower1.addBrick(compound1.uuid, stacked, { x: 100, y: 160 });
98+
99+
// Tower 2: Another compound
100+
const compound2 = createCompoundBrick({
101+
label: 'Second Compound',
102+
bboxArgs: [{ w: 80, h: 40 }],
103+
});
104+
const tower2 = manager.createTower(compound2, { x: 500, y: 200 });
105+
tower2.addNestedBrick(compound2.uuid, createSimpleBrick(), { x: 0, y: 0 });
106+
107+
return <WorkspaceView manager={manager} />;
108+
},
109+
};
110+
111+
// === Workspace Story 4: Argument Positioning Test (adapted from tree TestArgumentPositioning) ===
112+
export const TestArgumentPositioning: Story = {
113+
render: () => {
114+
resetFactoryCounter();
115+
const manager = new WorkspaceManager();
116+
117+
// Test with different label lengths
118+
const shortLabel = createCompoundBrick({
119+
label: 'Short',
120+
bboxArgs: Array(3).fill({ w: 60, h: 20 }),
121+
});
122+
const longLabel = createCompoundBrick({
123+
label: 'Very Long Label That Should Push Args Further Right',
124+
bboxArgs: Array(3).fill({ w: 60, h: 20 }),
125+
});
126+
127+
const tower1 = manager.createTower(shortLabel, { x: 100, y: 50 });
128+
const tower2 = manager.createTower(longLabel, { x: 100, y: 200 });
129+
130+
// Add argument bricks to both
131+
for (let i = 0; i < 3; i++) {
132+
const expr1 = createExpressionBrick({ label: `Arg ${i + 1}` });
133+
const expr2 = createExpressionBrick({ label: `Arg ${i + 1}` });
134+
tower1.addArgumentBrick(shortLabel.uuid, expr1, { x: 0, y: 0 }, i);
135+
tower2.addArgumentBrick(longLabel.uuid, expr2, { x: 0, y: 0 }, i);
136+
}
137+
138+
return <WorkspaceView manager={manager} />;
139+
},
140+
};

0 commit comments

Comments
 (0)