Skip to content

Commit 422cf75

Browse files
authored
Merge pull request #55 from ut-code/support-node-pin-bit-width-configuration
Support NodePin bit width configuration
2 parents b8e4cf2 + e89e243 commit 422cf75

File tree

13 files changed

+328
-38
lines changed

13 files changed

+328
-38
lines changed

src/common/rect.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { type Vector2, vector2 } from "./vector2";
2+
3+
export type Rect = {
4+
position: Vector2;
5+
size: Vector2;
6+
};
7+
export const rect = {
8+
fromPoint: (point: Vector2): Rect => ({
9+
position: point,
10+
size: vector2.zero,
11+
}),
12+
bounds: (points: Vector2[]): Rect => {
13+
const [first, ...rest] = points;
14+
if (!first) throw new Error("No points provided");
15+
const rect = {
16+
position: first,
17+
size: vector2.zero,
18+
};
19+
for (const point of rest) {
20+
rect.position.x = Math.min(rect.position.x, point.x);
21+
rect.position.y = Math.min(rect.position.y, point.y);
22+
rect.size.x = Math.max(rect.size.x, point.x - rect.position.x);
23+
rect.size.y = Math.max(rect.size.y, point.y - rect.position.y);
24+
}
25+
return rect;
26+
},
27+
shift: (rect: Rect, offset: Vector2): Rect => ({
28+
position: vector2.add(rect.position, offset),
29+
size: rect.size,
30+
}),
31+
};
Lines changed: 189 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,189 @@
1-
export function CCComponentEditorNodePinPropertyEditor() {}
1+
import { Button, Popover, Stack, TextField, Typography } from "@mui/material";
2+
import { zip } from "lodash-es";
3+
import nullthrows from "nullthrows";
4+
import { useState } from "react";
5+
import invariant from "tiny-invariant";
6+
import { rect } from "../../../../common/rect";
7+
import { IntrinsicComponentDefinition } from "../../../../store/intrinsics/base";
8+
import { CCNodePinStore } from "../../../../store/nodePin";
9+
import { useStore } from "../../../../store/react";
10+
import getCCComponentEditorRendererNodeGeometry from "../renderer/Node.geometry";
11+
import { useComponentEditorStore } from "../store";
12+
13+
export function CCComponentEditorNodePinPropertyEditor() {
14+
const { store } = useStore();
15+
const componentEditorStore = useComponentEditorStore();
16+
const target = componentEditorStore((s) => s.nodePinPropertyEditorTarget);
17+
const setTarget = componentEditorStore(
18+
(s) => s.setNodePinPropertyEditorTarget,
19+
);
20+
const [newBitWidthList, setNewBitWidthList] = useState<number[] | null>(null); // null means no change
21+
if (!target) return null;
22+
23+
const componentPin = nullthrows(
24+
store.componentPins.get(target.componentPinId),
25+
);
26+
const component = nullthrows(store.components.get(componentPin.componentId));
27+
const nodePins = store.nodePins
28+
.getManyByNodeIdAndComponentPinId(target.nodeId, target.componentPinId)
29+
.toSorted((a, b) => a.order - b.order);
30+
invariant(
31+
nodePins.every((p) => p.userSpecifiedBitWidth !== null),
32+
"NodePinPropertyEditor can only be used for node pins with user specified bit width",
33+
);
34+
const componentPinAttributes = nullthrows(
35+
IntrinsicComponentDefinition.intrinsicComponentPinAttributesByComponentPinId.get(
36+
target.componentPinId,
37+
),
38+
"NodePinPropertyEditor can only be used for intrinsic component pins",
39+
);
40+
41+
const getBoundingClientRect = (): DOMRect => {
42+
const geometry = getCCComponentEditorRendererNodeGeometry(
43+
store,
44+
target.nodeId,
45+
);
46+
const nodePinCanvasPositions = nodePins.map((nodePin) =>
47+
componentEditorStore
48+
.getState()
49+
.fromStageToCanvas(
50+
nullthrows(geometry.nodePinPositionById.get(nodePin.id)),
51+
),
52+
);
53+
const bounds = rect.shift(
54+
rect.bounds(nodePinCanvasPositions),
55+
componentEditorStore.getState().getRendererPosition(),
56+
);
57+
return new DOMRect(
58+
bounds.position.x,
59+
bounds.position.y,
60+
bounds.size.x,
61+
bounds.size.y,
62+
);
63+
};
64+
65+
const bitWidthList =
66+
newBitWidthList ??
67+
nodePins.map((nodePin) => nullthrows(nodePin.userSpecifiedBitWidth));
68+
69+
const isTouched = Boolean(newBitWidthList);
70+
const isValid = bitWidthList.every((bitWidth) => bitWidth > 0);
71+
72+
const onClose = () => {
73+
setTarget(null);
74+
setNewBitWidthList(null);
75+
};
76+
77+
return (
78+
<Popover
79+
open
80+
anchorEl={{ nodeType: 1, getBoundingClientRect }}
81+
transformOrigin={{
82+
vertical: "center",
83+
horizontal: componentPin.type === "input" ? "right" : "left",
84+
}}
85+
slotProps={{ paper: { sx: { p: 2, width: 250 } } }}
86+
onClose={onClose}
87+
>
88+
<Typography variant="h6">
89+
{componentPin.name} ({component.name})
90+
</Typography>
91+
<Typography variant="caption" color="text.secondary">
92+
Specify the bit width to assign to the pin.
93+
</Typography>
94+
<form
95+
onSubmit={(e) => {
96+
e.preventDefault();
97+
let maxOrder = 0;
98+
for (const [nodePin, bitWidth] of zip(nodePins, bitWidthList)) {
99+
// Create new NodePin
100+
if (!nodePin && bitWidth) {
101+
store.nodePins.register(
102+
CCNodePinStore.create({
103+
componentPinId: target.componentPinId,
104+
nodeId: target.nodeId,
105+
order: ++maxOrder,
106+
userSpecifiedBitWidth: bitWidth,
107+
}),
108+
);
109+
continue;
110+
}
111+
// Delete old NodePin
112+
if (nodePin && !bitWidth) {
113+
store.nodePins.unregister(nodePin.id);
114+
continue;
115+
}
116+
// Update NodePin
117+
if (nodePin && bitWidth) {
118+
maxOrder = nodePin.order; // nodePins are sorted by order
119+
if (nodePin.userSpecifiedBitWidth !== bitWidth)
120+
store.nodePins.update(nodePin.id, {
121+
userSpecifiedBitWidth: bitWidth,
122+
});
123+
continue;
124+
}
125+
throw new Error("Unreachable");
126+
}
127+
onClose();
128+
}}
129+
>
130+
<Stack gap={0.5} sx={{ mt: 1 }}>
131+
{bitWidthList.map((bitWidth, index) => {
132+
return (
133+
<TextField
134+
// biome-ignore lint/suspicious/noArrayIndexKey: bitWidth is only identified by index in this component
135+
key={index}
136+
type="number"
137+
value={bitWidth || ""}
138+
slotProps={{ htmlInput: { min: 1 } }}
139+
onChange={(e) => {
140+
const newValue = Number.parseInt(e.target.value, 10);
141+
if (newValue >= 0 || e.target.value === "")
142+
setNewBitWidthList(bitWidthList.with(index, newValue || 0));
143+
}}
144+
error={bitWidth <= 0}
145+
size="small"
146+
/>
147+
);
148+
})}
149+
</Stack>
150+
<Stack direction="row" gap={1} sx={{ mt: 1 }}>
151+
{componentPinAttributes.isSplittable && (
152+
<>
153+
<Button
154+
type="button"
155+
variant="outlined"
156+
size="small"
157+
onClick={() => {
158+
setNewBitWidthList([...bitWidthList, 1]);
159+
}}
160+
>
161+
Add
162+
</Button>
163+
<Button
164+
type="button"
165+
variant="outlined"
166+
size="small"
167+
disabled={bitWidthList.length <= 1}
168+
onClick={() => {
169+
setNewBitWidthList(bitWidthList.slice(0, -1));
170+
}}
171+
>
172+
Remove
173+
</Button>
174+
</>
175+
)}
176+
<Button
177+
type="submit"
178+
variant="contained"
179+
size="small"
180+
sx={{ ml: "auto" }}
181+
disabled={!isTouched || !isValid}
182+
>
183+
Apply
184+
</Button>
185+
</Stack>
186+
</form>
187+
</Popover>
188+
);
189+
}

src/pages/edit/Editor/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { CCComponentId } from "../../../store/component";
77
import { useStore } from "../../../store/react";
88
import CCComponentEditorContextMenu from "./components/ContextMenu";
99
import CCComponentEditorGrid from "./components/Grid";
10+
import { CCComponentEditorNodePinPropertyEditor } from "./components/NodePinPropertyEditor";
1011
import CCComponentEditorTitleBar from "./components/TitleBar";
1112
import CCComponentEditorViewModeSwitcher from "./components/ViewModeSwitcher";
1213
import CCComponentEditorRenderer from "./renderer";
@@ -46,6 +47,7 @@ function CCComponentEditorContent({
4647
/>
4748
<CCComponentEditorViewModeSwitcher />
4849
<CCComponentEditorContextMenu onEditComponent={onEditComponent} />
50+
<CCComponentEditorNodePinPropertyEditor />
4951
{isComponentPropertyDialogOpen && (
5052
<ComponentPropertyDialog
5153
defaultName={component.name}

src/pages/edit/Editor/renderer/Node.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ const CCComponentEditorRendererNode = ensureStoreItem(
9090
: theme.palette.textPrimary
9191
}
9292
strokeWidth={2}
93+
rx={2}
9394
/>
9495
</g>
9596
{store.nodePins.getManyByNodeId(nodeId).map((nodePin) => {

src/pages/edit/Editor/renderer/NodePin.tsx

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,17 @@ import getCCComponentEditorRendererNodeGeometry from "./Node.geometry";
1414

1515
const NODE_PIN_POSITION_SENSITIVITY = 10;
1616

17-
export type CCComponentEditorRendererNodeProps = {
17+
export type CCComponentEditorRendererNodePinProps = {
1818
nodePinId: CCNodePinId;
1919
position: Vector2;
2020
};
21+
export const CCComponentEditorRendererNodePinConstants = {
22+
SIZE: 10,
23+
};
2124
export default function CCComponentEditorRendererNodePin({
2225
nodePinId,
2326
position,
24-
}: CCComponentEditorRendererNodeProps) {
27+
}: CCComponentEditorRendererNodePinProps) {
2528
const { store } = useStore();
2629
const componentEditorState = useComponentEditorStore()();
2730
const nodePin = nullthrows(store.nodePins.get(nodePinId));
@@ -117,6 +120,13 @@ export default function CCComponentEditorRendererNodePin({
117120
}
118121
setDraggingState(null);
119122
},
123+
onClick: () => {
124+
if (nodePin.userSpecifiedBitWidth === null) return;
125+
componentEditorState.setNodePinPropertyEditorTarget({
126+
componentPinId: nodePin.componentPinId,
127+
nodeId: nodePin.nodeId,
128+
});
129+
},
120130
});
121131

122132
const isSimulationMode = useComponentEditorStore()(
@@ -179,28 +189,44 @@ export default function CCComponentEditorRendererNodePin({
179189
)}
180190
<g {...draggableProps} style={{ cursor: "pointer" }}>
181191
<rect
182-
x={position.x - 5}
183-
y={position.y - 5}
184-
width={10}
185-
height={10}
192+
x={position.x - CCComponentEditorRendererNodePinConstants.SIZE / 2}
193+
y={position.y - CCComponentEditorRendererNodePinConstants.SIZE / 2}
194+
width={CCComponentEditorRendererNodePinConstants.SIZE}
195+
height={CCComponentEditorRendererNodePinConstants.SIZE}
186196
rx={3}
187197
fill={theme.palette.white}
188198
stroke={theme.palette.textPrimary}
189199
strokeWidth={2}
190200
/>
191-
<text
192-
x={position.x}
193-
y={position.y}
194-
textAnchor="middle"
195-
dominantBaseline="central"
196-
fontSize={7}
197-
fill={theme.palette.textPrimary}
198-
>
199-
3
200-
</text>
201+
{nodePin.userSpecifiedBitWidth !== null && (
202+
<text
203+
x={position.x}
204+
y={position.y}
205+
textAnchor="middle"
206+
dominantBaseline="central"
207+
fontSize={
208+
nodePin.userSpecifiedBitWidth >= 100
209+
? 4
210+
: nodePin.userSpecifiedBitWidth >= 10
211+
? 6
212+
: 8
213+
}
214+
fill={theme.palette.textPrimary}
215+
>
216+
{nodePin.userSpecifiedBitWidth >= 100
217+
? "99+"
218+
: nodePin.userSpecifiedBitWidth}
219+
</text>
220+
)}
201221
</g>
202222
<text
203-
x={position.x + { input: 10, output: -10 }[pinType]}
223+
x={
224+
position.x +
225+
{
226+
input: CCComponentEditorRendererNodePinConstants.SIZE,
227+
output: -CCComponentEditorRendererNodePinConstants.SIZE,
228+
}[pinType]
229+
}
204230
y={position.y}
205231
textAnchor={{ input: "start", output: "end" }[pinType]}
206232
dominantBaseline="central"

src/pages/edit/Editor/renderer/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export default function CCComponentEditorRenderer() {
1919
const connectionIds = useConnectionIds(componentEditorState.componentId);
2020

2121
return (
22+
// biome-ignore lint/a11y/noSvgWithoutTitle: This svg is not a graphic
2223
<svg
2324
ref={componentEditorState.registerRendererElement}
2425
style={{
@@ -27,6 +28,7 @@ export default function CCComponentEditorRenderer() {
2728
left: 0,
2829
width: "100%",
2930
height: "100%",
31+
userSelect: "none",
3032
}}
3133
viewBox={[viewBox.x, viewBox.y, viewBox.width, viewBox.height].join(" ")}
3234
onDragOver={(e) => {
@@ -47,7 +49,6 @@ export default function CCComponentEditorRenderer() {
4749
);
4850
}}
4951
>
50-
<title>Component editor</title>
5152
<CCComponentEditorRendererBackground />
5253
{connectionIds.map((connectionId) => (
5354
<CCComponentEditorRendererConnection

src/pages/edit/Editor/store/slices/core/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ export const createComponentEditorStoreCoreSlice: ComponentEditorSliceCreator<
3737
selectedNodeIds: new Set(),
3838
rangeSelect: null,
3939
selectedConnectionIds: new Set(),
40+
nodePinPropertyEditorTarget: null,
41+
setNodePinPropertyEditorTarget(target) {
42+
set((state) => ({
43+
...state,
44+
nodePinPropertyEditorTarget: target,
45+
}));
46+
},
4047
/** @private */
4148
inputValues: new Map(),
4249
getInputValue(componentPinId: CCComponentPinId) {

0 commit comments

Comments
 (0)