diff --git a/src/common/rect.ts b/src/common/rect.ts new file mode 100644 index 0000000..713f92d --- /dev/null +++ b/src/common/rect.ts @@ -0,0 +1,31 @@ +import { type Vector2, vector2 } from "./vector2"; + +export type Rect = { + position: Vector2; + size: Vector2; +}; +export const rect = { + fromPoint: (point: Vector2): Rect => ({ + position: point, + size: vector2.zero, + }), + bounds: (points: Vector2[]): Rect => { + const [first, ...rest] = points; + if (!first) throw new Error("No points provided"); + const rect = { + position: first, + size: vector2.zero, + }; + for (const point of rest) { + rect.position.x = Math.min(rect.position.x, point.x); + rect.position.y = Math.min(rect.position.y, point.y); + rect.size.x = Math.max(rect.size.x, point.x - rect.position.x); + rect.size.y = Math.max(rect.size.y, point.y - rect.position.y); + } + return rect; + }, + shift: (rect: Rect, offset: Vector2): Rect => ({ + position: vector2.add(rect.position, offset), + size: rect.size, + }), +}; diff --git a/src/pages/edit/Editor/components/NodePinPropertyEditor.tsx b/src/pages/edit/Editor/components/NodePinPropertyEditor.tsx index 06ba705..50441c3 100644 --- a/src/pages/edit/Editor/components/NodePinPropertyEditor.tsx +++ b/src/pages/edit/Editor/components/NodePinPropertyEditor.tsx @@ -1 +1,189 @@ -export function CCComponentEditorNodePinPropertyEditor() {} +import { Button, Popover, Stack, TextField, Typography } from "@mui/material"; +import { zip } from "lodash-es"; +import nullthrows from "nullthrows"; +import { useState } from "react"; +import invariant from "tiny-invariant"; +import { rect } from "../../../../common/rect"; +import { IntrinsicComponentDefinition } from "../../../../store/intrinsics/base"; +import { CCNodePinStore } from "../../../../store/nodePin"; +import { useStore } from "../../../../store/react"; +import getCCComponentEditorRendererNodeGeometry from "../renderer/Node.geometry"; +import { useComponentEditorStore } from "../store"; + +export function CCComponentEditorNodePinPropertyEditor() { + const { store } = useStore(); + const componentEditorStore = useComponentEditorStore(); + const target = componentEditorStore((s) => s.nodePinPropertyEditorTarget); + const setTarget = componentEditorStore( + (s) => s.setNodePinPropertyEditorTarget, + ); + const [newBitWidthList, setNewBitWidthList] = useState(null); // null means no change + if (!target) return null; + + const componentPin = nullthrows( + store.componentPins.get(target.componentPinId), + ); + const component = nullthrows(store.components.get(componentPin.componentId)); + const nodePins = store.nodePins + .getManyByNodeIdAndComponentPinId(target.nodeId, target.componentPinId) + .toSorted((a, b) => a.order - b.order); + invariant( + nodePins.every((p) => p.userSpecifiedBitWidth !== null), + "NodePinPropertyEditor can only be used for node pins with user specified bit width", + ); + const componentPinAttributes = nullthrows( + IntrinsicComponentDefinition.intrinsicComponentPinAttributesByComponentPinId.get( + target.componentPinId, + ), + "NodePinPropertyEditor can only be used for intrinsic component pins", + ); + + const getBoundingClientRect = (): DOMRect => { + const geometry = getCCComponentEditorRendererNodeGeometry( + store, + target.nodeId, + ); + const nodePinCanvasPositions = nodePins.map((nodePin) => + componentEditorStore + .getState() + .fromStageToCanvas( + nullthrows(geometry.nodePinPositionById.get(nodePin.id)), + ), + ); + const bounds = rect.shift( + rect.bounds(nodePinCanvasPositions), + componentEditorStore.getState().getRendererPosition(), + ); + return new DOMRect( + bounds.position.x, + bounds.position.y, + bounds.size.x, + bounds.size.y, + ); + }; + + const bitWidthList = + newBitWidthList ?? + nodePins.map((nodePin) => nullthrows(nodePin.userSpecifiedBitWidth)); + + const isTouched = Boolean(newBitWidthList); + const isValid = bitWidthList.every((bitWidth) => bitWidth > 0); + + const onClose = () => { + setTarget(null); + setNewBitWidthList(null); + }; + + return ( + + + {componentPin.name} ({component.name}) + + + Specify the bit width to assign to the pin. + +
{ + e.preventDefault(); + let maxOrder = 0; + for (const [nodePin, bitWidth] of zip(nodePins, bitWidthList)) { + // Create new NodePin + if (!nodePin && bitWidth) { + store.nodePins.register( + CCNodePinStore.create({ + componentPinId: target.componentPinId, + nodeId: target.nodeId, + order: ++maxOrder, + userSpecifiedBitWidth: bitWidth, + }), + ); + continue; + } + // Delete old NodePin + if (nodePin && !bitWidth) { + store.nodePins.unregister(nodePin.id); + continue; + } + // Update NodePin + if (nodePin && bitWidth) { + maxOrder = nodePin.order; // nodePins are sorted by order + if (nodePin.userSpecifiedBitWidth !== bitWidth) + store.nodePins.update(nodePin.id, { + userSpecifiedBitWidth: bitWidth, + }); + continue; + } + throw new Error("Unreachable"); + } + onClose(); + }} + > + + {bitWidthList.map((bitWidth, index) => { + return ( + { + const newValue = Number.parseInt(e.target.value, 10); + if (newValue >= 0 || e.target.value === "") + setNewBitWidthList(bitWidthList.with(index, newValue || 0)); + }} + error={bitWidth <= 0} + size="small" + /> + ); + })} + + + {componentPinAttributes.isSplittable && ( + <> + + + + )} + + +
+
+ ); +} diff --git a/src/pages/edit/Editor/index.tsx b/src/pages/edit/Editor/index.tsx index a32aa52..f75baa2 100644 --- a/src/pages/edit/Editor/index.tsx +++ b/src/pages/edit/Editor/index.tsx @@ -7,6 +7,7 @@ import type { CCComponentId } from "../../../store/component"; import { useStore } from "../../../store/react"; import CCComponentEditorContextMenu from "./components/ContextMenu"; import CCComponentEditorGrid from "./components/Grid"; +import { CCComponentEditorNodePinPropertyEditor } from "./components/NodePinPropertyEditor"; import CCComponentEditorTitleBar from "./components/TitleBar"; import CCComponentEditorViewModeSwitcher from "./components/ViewModeSwitcher"; import CCComponentEditorRenderer from "./renderer"; @@ -46,6 +47,7 @@ function CCComponentEditorContent({ /> + {isComponentPropertyDialogOpen && ( {store.nodePins.getManyByNodeId(nodeId).map((nodePin) => { diff --git a/src/pages/edit/Editor/renderer/NodePin.tsx b/src/pages/edit/Editor/renderer/NodePin.tsx index 3b4225d..ffb4f4a 100644 --- a/src/pages/edit/Editor/renderer/NodePin.tsx +++ b/src/pages/edit/Editor/renderer/NodePin.tsx @@ -14,14 +14,17 @@ import getCCComponentEditorRendererNodeGeometry from "./Node.geometry"; const NODE_PIN_POSITION_SENSITIVITY = 10; -export type CCComponentEditorRendererNodeProps = { +export type CCComponentEditorRendererNodePinProps = { nodePinId: CCNodePinId; position: Vector2; }; +export const CCComponentEditorRendererNodePinConstants = { + SIZE: 10, +}; export default function CCComponentEditorRendererNodePin({ nodePinId, position, -}: CCComponentEditorRendererNodeProps) { +}: CCComponentEditorRendererNodePinProps) { const { store } = useStore(); const componentEditorState = useComponentEditorStore()(); const nodePin = nullthrows(store.nodePins.get(nodePinId)); @@ -117,6 +120,13 @@ export default function CCComponentEditorRendererNodePin({ } setDraggingState(null); }, + onClick: () => { + if (nodePin.userSpecifiedBitWidth === null) return; + componentEditorState.setNodePinPropertyEditorTarget({ + componentPinId: nodePin.componentPinId, + nodeId: nodePin.nodeId, + }); + }, }); const isSimulationMode = useComponentEditorStore()( @@ -179,28 +189,44 @@ export default function CCComponentEditorRendererNodePin({ )} - - 3 - + {nodePin.userSpecifiedBitWidth !== null && ( + = 100 + ? 4 + : nodePin.userSpecifiedBitWidth >= 10 + ? 6 + : 8 + } + fill={theme.palette.textPrimary} + > + {nodePin.userSpecifiedBitWidth >= 100 + ? "99+" + : nodePin.userSpecifiedBitWidth} + + )} { @@ -47,7 +49,6 @@ export default function CCComponentEditorRenderer() { ); }} > - Component editor {connectionIds.map((connectionId) => ( ({ + ...state, + nodePinPropertyEditorTarget: target, + })); + }, /** @private */ inputValues: new Map(), getInputValue(componentPinId: CCComponentPinId) { diff --git a/src/pages/edit/Editor/store/slices/core/types.ts b/src/pages/edit/Editor/store/slices/core/types.ts index 1f11208..052a3d1 100644 --- a/src/pages/edit/Editor/store/slices/core/types.ts +++ b/src/pages/edit/Editor/store/slices/core/types.ts @@ -13,6 +13,11 @@ export type RangeSelect = { start: Vector2; end: Vector2 } | null; export type InputValueKey = CCComponentPinId; +export type NodePinPropertyEditorTarget = { + nodeId: CCNodeId; + componentPinId: CCComponentPinId; +}; + export type EditorStoreCoreSlice = { editorMode: EditorMode; timeStep: number; @@ -20,6 +25,10 @@ export type EditorStoreCoreSlice = { rangeSelect: RangeSelect; setRangeSelect(rangeSelect: RangeSelect): void; selectedConnectionIds: Set; + nodePinPropertyEditorTarget: NodePinPropertyEditorTarget | null; + setNodePinPropertyEditorTarget( + target: NodePinPropertyEditorTarget | null, + ): void; inputValues: Map; getInputValue(componentPinId: CCComponentPinId): SimulationValue; setInputValue(componentPinId: CCComponentPinId, value: SimulationValue): void; diff --git a/src/pages/edit/Editor/store/slices/perspective/index.tsx b/src/pages/edit/Editor/store/slices/perspective/index.tsx index 9e5512e..593c87d 100644 --- a/src/pages/edit/Editor/store/slices/perspective/index.tsx +++ b/src/pages/edit/Editor/store/slices/perspective/index.tsx @@ -1,4 +1,3 @@ -import * as matrix from "transformation-matrix"; import { vector2 } from "../../../../../../common/vector2"; import type { ComponentEditorSliceCreator } from "../../types"; import type { PerspectiveStoreSlice } from "./types"; @@ -7,19 +6,21 @@ const createComponentEditorStorePerspectiveSlice: ComponentEditorSliceCreator< PerspectiveStoreSlice > = () => { let resizeObserver: ResizeObserver | null; - let resizeObserverObservedElement: SVGSVGElement | null; + let rendererElement: SVGSVGElement | null; const registerRendererElement = (element: SVGSVGElement | null) => { if (!resizeObserver) return; - if (resizeObserverObservedElement) - resizeObserver.unobserve(resizeObserverObservedElement); + if (rendererElement) resizeObserver.unobserve(rendererElement); if (element) resizeObserver.observe(element); - resizeObserverObservedElement = element; + rendererElement = element; }; return { define: (set, get) => ({ perspective: { center: vector2.zero, scale: 1 }, rendererSize: vector2.zero, - userPerspectiveTransformation: matrix.identity(), + getRendererPosition: () => { + const rect = rendererElement?.getBoundingClientRect(); + return rect ? { x: rect.left, y: rect.top } : vector2.zero; + }, setPerspective: (perspective) => set((s) => ({ ...s, perspective })), registerRendererElement, fromCanvasToStage: (point) => diff --git a/src/pages/edit/Editor/store/slices/perspective/types.ts b/src/pages/edit/Editor/store/slices/perspective/types.ts index 1c774e1..3382c56 100644 --- a/src/pages/edit/Editor/store/slices/perspective/types.ts +++ b/src/pages/edit/Editor/store/slices/perspective/types.ts @@ -4,6 +4,7 @@ import type { Vector2 } from "../../../../../../common/vector2"; export type PerspectiveStoreSlice = { perspective: Perspective; rendererSize: Vector2; + getRendererPosition: () => Vector2; setPerspective: (perspective: Perspective) => void; registerRendererElement: (element: SVGSVGElement | null) => void; fromCanvasToStage: (point: Vector2) => Vector2; diff --git a/src/pages/edit/SidePanel.tsx b/src/pages/edit/SidePanel.tsx index 4bc4692..4c3f8a5 100644 --- a/src/pages/edit/SidePanel.tsx +++ b/src/pages/edit/SidePanel.tsx @@ -33,6 +33,7 @@ function ComponentRenderer({ componentId }: { componentId: CCComponentId }) { alignItems: "center", marginTop: "4px", border: `2px solid ${theme.palette.black}`, + borderRadius: "2px", background: theme.palette.white, }} > @@ -100,12 +101,14 @@ export default function SidePanel(sidePanelProps: SidePanelProps) { - - - ), + slotProps={{ + input: { + startAdornment: ( + + + + ), + }, }} value={searchText} onChange={(e) => { diff --git a/src/store/node.ts b/src/store/node.ts index c59584b..3d3b74d 100644 --- a/src/store/node.ts +++ b/src/store/node.ts @@ -116,10 +116,10 @@ export class CCNodeStore extends EventEmitter { * @param value new position */ update(id: CCNodeId, value: Pick): void { - const node = this.#nodes.get(id); - invariant(node); - this.#nodes.set(id, { ...node, ...value }); - this.emit("didUpdate", node); + const existingNode = nullthrows(this.#nodes.get(id)); + const newNode = { ...existingNode, ...value }; + this.#nodes.set(id, newNode); + this.emit("didUpdate", newNode); } /** diff --git a/src/store/nodePin.ts b/src/store/nodePin.ts index 471cd84..87560b4 100644 --- a/src/store/nodePin.ts +++ b/src/store/nodePin.ts @@ -21,6 +21,7 @@ export type CCNodePinStoreEvents = { didRegister(pin: CCNodePin): void; willUnregister(pin: CCNodePin): void; didUnregister(pin: CCNodePin): void; + didUpdate(pin: CCNodePin): void; }; export class CCNodePinStore extends EventEmitter { @@ -116,6 +117,13 @@ export class CCNodePinStore extends EventEmitter { this.#markedAsDeleted.delete(id); } + update(id: CCNodePinId, value: Pick) { + const existingNodePin = nullthrows(this.#nodePins.get(id)); + const newNodePin = { ...existingNodePin, ...value }; + this.#nodePins.set(id, newNodePin); + this.emit("didUpdate", newNodePin); + } + /** * Get a pin by id * @param id id of pin @@ -151,6 +159,15 @@ export class CCNodePinStore extends EventEmitter { return [...this.#nodePins.values()].filter((pin) => pin.nodeId === nodeId); } + getManyByNodeIdAndComponentPinId( + nodeId: CCNodeId, + componentPinId: CCComponentPinId, + ): CCNodePin[] { + return [...this.#nodePins.values()].filter( + (pin) => pin.nodeId === nodeId && pin.componentPinId === componentPinId, + ); + } + /** * Get the multiplexability of a node pin * @param pinId id of pin @@ -227,16 +244,19 @@ export class CCNodePinStore extends EventEmitter { * @returns a new pin */ static create( - partialPin: Omit, + partialPin: Omit & + Partial>, ): CCNodePin { const attributes = IntrinsicComponentDefinition.intrinsicComponentPinAttributesByComponentPinId.get( partialPin.componentPinId, ); return { - id: crypto.randomUUID() as CCNodePinId, - userSpecifiedBitWidth: attributes?.isBitWidthConfigurable ? 1 : null, ...partialPin, + id: crypto.randomUUID() as CCNodePinId, + userSpecifiedBitWidth: + partialPin.userSpecifiedBitWidth ?? + (attributes?.isBitWidthConfigurable ? 1 : null), }; }