From 4e37d913d7150b1c2bb0afe2aff0bae54a731e11 Mon Sep 17 00:00:00 2001 From: chelproc Date: Sat, 6 Jun 2026 12:48:54 +0900 Subject: [PATCH 1/6] Refactor node layout to use relative offsets and add play-mode guards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Separate node layout (relative pin offsets) from geometry (absolute positions), replacing the single geometry calculator with a layout → geometry two-step. Switch node SVG wrapper from to so child elements can use percentage dimensions. Update Display node to compute its size from config resolution and add a config settings button stub. Disable the ViewModeSwitcher play button when any input pin is unconnected. Remove redundant node/connection event listeners from the simulation trigger. Key changes covered: - Layout / LayoutSource types split out from Geometry, with nodePinOffsetById replacing absolute positions - in Node/index.tsx so Default can use width="100%" - Display/geometry.ts now scales with config.resolution and exports layout constants - New ConfigSettingButton component for Display nodes - isEachInputPinConnected utility in component.ts gating the play button - executeSimulation decoupled from raw store events in the core slice --- .../components/NodePinPropertyEditor.tsx | 43 +++++------ .../Editor/components/ViewModeSwitcher.tsx | 6 ++ .../Editor/renderer/ComponentPin/index.tsx | 2 +- .../edit/Editor/renderer/Connection/index.tsx | 2 +- .../Node/components/Default/geometry.ts | 71 ++++++++----------- .../Node/components/Default/index.tsx | 12 ++-- .../Display/ConfigSettingButton.tsx | 26 +++++++ .../Node/components/Display/geometry.ts | 51 ++++++------- .../Node/components/Display/index.tsx | 48 ++++++------- .../edit/Editor/renderer/Node/geometry.ts | 67 +++++++++++------ src/pages/edit/Editor/renderer/Node/index.tsx | 23 ++++-- src/pages/edit/Editor/renderer/Node/types.ts | 16 +++-- .../edit/Editor/renderer/NodePin/index.tsx | 2 +- .../edit/Editor/store/slices/core/index.ts | 5 -- src/store/component.ts | 25 +++++++ src/store/connection.ts | 29 ++++++++ 16 files changed, 262 insertions(+), 166 deletions(-) create mode 100644 src/pages/edit/Editor/renderer/Node/components/Display/ConfigSettingButton.tsx diff --git a/src/pages/edit/Editor/components/NodePinPropertyEditor.tsx b/src/pages/edit/Editor/components/NodePinPropertyEditor.tsx index d1deb88..2dff789 100644 --- a/src/pages/edit/Editor/components/NodePinPropertyEditor.tsx +++ b/src/pages/edit/Editor/components/NodePinPropertyEditor.tsx @@ -8,7 +8,7 @@ import { CCConnectionStore } from "../../../../store/connection"; import { IntrinsicComponentDefinition } from "../../../../store/intrinsics/base"; import { CCNodePinStore } from "../../../../store/nodePin"; import { useStore } from "../../../../store/react"; -import getCCComponentEditorRendererNodeGeometry from "../renderer/Node/geometry"; +import { getCCComponentEditorRendererNodeGeometry } from "../renderer/Node/geometry"; import { useComponentEditorStore } from "../store"; export function CCComponentEditorNodePinPropertyEditor() { @@ -123,33 +123,22 @@ export function CCComponentEditorNodePinPropertyEditor() { nodePin.id, ); for (const connection of connections) { - const anotherNodePinId = - connection.from === nodePin.id - ? connection.to - : connection.from; - const fromNodePinId = - connection.from === nodePin.id - ? nodePin.id - : anotherNodePinId; - const toNodePinId = - connection.from === nodePin.id - ? anotherNodePinId - : nodePin.id; + const from = connection.from; + const to = connection.to; const parentComponentId = connection.parentComponentId; - store.connections.unregister([connection.id]); - if ( - store.nodePins.isConnectable(nodePin.id, anotherNodePinId) - ) { - // reconnect if still connectable after bit width change - store.connections.register( - CCConnectionStore.create({ - parentComponentId, - from: fromNodePinId, - to: toNodePinId, - bentPortion: 0.5, - }), - ); - } + store.connections.unregister([connection.id]).then(() => { + if (store.nodePins.isConnectable(from, to)) { + // reconnect if still connectable after bit width change + store.connections.register( + CCConnectionStore.create({ + parentComponentId, + from, + to, + bentPortion: 0.5, + }), + ); + } + }); } } continue; diff --git a/src/pages/edit/Editor/components/ViewModeSwitcher.tsx b/src/pages/edit/Editor/components/ViewModeSwitcher.tsx index dabfb5d..da79a9a 100644 --- a/src/pages/edit/Editor/components/ViewModeSwitcher.tsx +++ b/src/pages/edit/Editor/components/ViewModeSwitcher.tsx @@ -1,9 +1,12 @@ import { Edit, PlayArrow } from "@mui/icons-material"; import { Fab } from "@mui/material"; +import { isEachInputPinConnected } from "../../../../store/component"; +import { useStore } from "../../../../store/react"; import { useComponentEditorStore } from "../store"; export default function CCComponentEditorViewModeSwitcher() { const componentEditorState = useComponentEditorStore()(); + const { store } = useStore(); return ( {componentEditorState.editorMode === "edit" ? : } diff --git a/src/pages/edit/Editor/renderer/ComponentPin/index.tsx b/src/pages/edit/Editor/renderer/ComponentPin/index.tsx index 1fc775a..a0ce9a2 100644 --- a/src/pages/edit/Editor/renderer/ComponentPin/index.tsx +++ b/src/pages/edit/Editor/renderer/ComponentPin/index.tsx @@ -5,7 +5,7 @@ import { useStore } from "../../../../../store/react"; import { wrappingIncrementSimulationValue } from "../../../../../store/simulation"; import { useComponentEditorStore } from "../../store"; import { stringifySimulationValue } from "../../store/slices/core"; -import getCCComponentEditorRendererNodeGeometry from "./../Node/geometry"; +import { getCCComponentEditorRendererNodeGeometry } from "./../Node/geometry"; export type CCComponentEditorRendererComponentPinProps = { nodePinId: CCNodePinId; }; diff --git a/src/pages/edit/Editor/renderer/Connection/index.tsx b/src/pages/edit/Editor/renderer/Connection/index.tsx index f1748c3..8808bdd 100644 --- a/src/pages/edit/Editor/renderer/Connection/index.tsx +++ b/src/pages/edit/Editor/renderer/Connection/index.tsx @@ -9,7 +9,7 @@ import ensureStoreItem from "../../../../../store/react/error"; import { useNode } from "../../../../../store/react/selectors"; import { useComponentEditorStore } from "../../store"; import { stringifySimulationValue } from "../../store/slices/core/index"; -import getCCComponentEditorRendererNodeGeometry from "../Node/geometry"; +import { getCCComponentEditorRendererNodeGeometry } from "../Node/geometry"; export type CCComponentEditorRendererConnectionEndpoint = { direction: CCComponentPinType; diff --git a/src/pages/edit/Editor/renderer/Node/components/Default/geometry.ts b/src/pages/edit/Editor/renderer/Node/components/Default/geometry.ts index a6aedb5..4c40bed 100644 --- a/src/pages/edit/Editor/renderer/Node/components/Default/geometry.ts +++ b/src/pages/edit/Editor/renderer/Node/components/Default/geometry.ts @@ -1,50 +1,41 @@ -import { type Vector2, vector2 } from "../../../../../../../common/vector2"; +import type { Vector2 } from "../../../../../../../common/vector2"; import type { CCNodePinId } from "../../../../../../../store/nodePin"; import type { - CCComponentEditorRendererNodeGeometryCalculator, - CCComponentEditorRendererNodeGeometrySource, + CCComponentEditorRendererNodeLayout, + CCComponentEditorRendererNodeLayoutSource, } from "../../types"; const width = 100; const gapY = 20; const paddingY = 15; -export const ccComponentRendererNodeDefaultGeometryCalculator: CCComponentEditorRendererNodeGeometryCalculator = - (source: CCComponentEditorRendererNodeGeometrySource) => { - const size: Vector2 = { - x: width, - y: - gapY * - Math.max( - source.inputNodePinIds.length, - source.outputNodePinIds.length, - ) + - paddingY * 2, - }; +export function ccComponentRendererNodeDefaultLayoutCalculator( + source: CCComponentEditorRendererNodeLayoutSource, +): CCComponentEditorRendererNodeLayout { + const size: Vector2 = { + x: width, + y: + gapY * + Math.max( + source.inputNodePinIds.length, + source.outputNodePinIds.length, + ) + + paddingY * 2, + }; - const nodePinPositionById = new Map(); - for (const [index, nodePinId] of source.inputNodePinIds.entries()) { - nodePinPositionById.set(nodePinId, { - x: source.position.x - size.x / 2, - y: - source.position.y + - gapY * (index - source.inputNodePinIds.length / 2 + 0.5), - }); - } - for (const [index, nodePinId] of source.outputNodePinIds.entries()) { - nodePinPositionById.set(nodePinId, { - x: source.position.x + size.x / 2, - y: - source.position.y + - gapY * (index - source.outputNodePinIds.length / 2 + 0.5), - }); - } + const nodePinOffsetById = new Map(); - return { - rect: { - position: vector2.sub(source.position, vector2.div(size, 2)), - size, - }, - nodePinPositionById, - }; - }; + const startYIn = + size.y / 2 - (gapY * (source.inputNodePinIds.length - 1)) / 2; + for (const [index, pinId] of source.inputNodePinIds.entries()) { + nodePinOffsetById.set(pinId, { x: 0, y: startYIn + gapY * index }); + } + + const startYOut = + size.y / 2 - (gapY * (source.outputNodePinIds.length - 1)) / 2; + for (const [index, pinId] of source.outputNodePinIds.entries()) { + nodePinOffsetById.set(pinId, { x: size.x, y: startYOut + gapY * index }); + } + + return { size, nodePinOffsetById }; +} diff --git a/src/pages/edit/Editor/renderer/Node/components/Default/index.tsx b/src/pages/edit/Editor/renderer/Node/components/Default/index.tsx index 133721b..292cbe7 100644 --- a/src/pages/edit/Editor/renderer/Node/components/Default/index.tsx +++ b/src/pages/edit/Editor/renderer/Node/components/Default/index.tsx @@ -8,18 +8,18 @@ export function CCComponentEditorRendererNodeDefaultRenderer( <> {props.component.name} { + e.stopPropagation(); + }} + disableTouchRipple + disableFocusRipple + > + + + ); +} diff --git a/src/pages/edit/Editor/renderer/Node/components/Display/geometry.ts b/src/pages/edit/Editor/renderer/Node/components/Display/geometry.ts index 88b09be..9a94731 100644 --- a/src/pages/edit/Editor/renderer/Node/components/Display/geometry.ts +++ b/src/pages/edit/Editor/renderer/Node/components/Display/geometry.ts @@ -1,33 +1,34 @@ +import nullthrows from "nullthrows"; import { type Vector2, vector2 } from "../../../../../../../common/vector2"; +import type { CCIntrinsicComponentDisplaySpec } from "../../../../../../../store/intrinsics/types"; import type { CCNodePinId } from "../../../../../../../store/nodePin"; import type { - CCComponentEditorRendererNodeGeometryCalculator, - CCComponentEditorRendererNodeGeometrySource, + CCComponentEditorRendererNodeLayout, + CCComponentEditorRendererNodeLayoutSource, } from "../../types"; -const width = 320; -const height = 200; +const size = { x: 320, y: 200 }; -export const ccComponentRendererNodeDisplayGeometryCalculator: CCComponentEditorRendererNodeGeometryCalculator = - (source: CCComponentEditorRendererNodeGeometrySource) => { - const size: Vector2 = { - x: width, - y: height, - }; +export const ccComponentEditorRendererNodeDisplayLayoutConstants = { + padding: 8, + gridSize: 12, + gridSizeDisplayWidth: 60, +}; - return { - rect: { - position: vector2.sub(source.position, vector2.div(size, 2)), - size, - }, - nodePinPositionById: new Map( - source.inputNodePinIds.map( - (id) => - [ - id, - vector2.create(source.position.x - size.x / 2, source.position.y), - ] as const, - ), - ), - }; +export function ccComponentRendererNodeDisplayLayoutCalculator( + source: CCComponentEditorRendererNodeLayoutSource, +): CCComponentEditorRendererNodeLayout { + const { padding, gridSize, gridSizeDisplayWidth } = + ccComponentEditorRendererNodeDisplayLayoutConstants; + const config = source.config as CCIntrinsicComponentDisplaySpec["config"]; + + return { + size: { + x: gridSizeDisplayWidth + gridSize * config.resolution.x + padding * 2, + y: gridSize * config.resolution.y + padding * 2, + }, + nodePinOffsetById: new Map([ + [nullthrows(source.inputNodePinIds[0]), vector2.create(0, size.y / 2)], + ]), }; +} diff --git a/src/pages/edit/Editor/renderer/Node/components/Display/index.tsx b/src/pages/edit/Editor/renderer/Node/components/Display/index.tsx index a80634f..d63a48c 100644 --- a/src/pages/edit/Editor/renderer/Node/components/Display/index.tsx +++ b/src/pages/edit/Editor/renderer/Node/components/Display/index.tsx @@ -5,6 +5,9 @@ import type { CCIntrinsicComponentDisplaySpec } from "../../../../../../../store import { useStore } from "../../../../../../../store/react"; import { useComponentEditorStore } from "../../../../store"; import type { CCComponentEditorRendererNodeRendererProps } from "../../types"; +import { CCComponentEditorRendererNodeDefaultRenderer } from "../Default"; +import { CCComponentEditorRendererNodeDisplayRendererConfigSettingButton } from "./ConfigSettingButton"; +import { ccComponentEditorRendererNodeDisplayLayoutConstants } from "./geometry"; export function CCComponentEditorRendererNodeDisplayRenderer( props: CCComponentEditorRendererNodeRendererProps, @@ -23,38 +26,27 @@ export function CCComponentEditorRendererNodeDisplayRenderer( ? editorState.getNodePinValue(inputNodePin.id) : undefined; + const { padding, gridSize, gridSizeDisplayWidth } = + ccComponentEditorRendererNodeDisplayLayoutConstants; + return ( <> - - - Display - + {config.resolution.x}x{config.resolution.y} + + + {Array(config.resolution.y) .keys() .map((y) => @@ -63,10 +55,10 @@ export function CCComponentEditorRendererNodeDisplayRenderer( .map((x) => ( -> = { +const specialLayoutCalculators: { + [key in CCIntrinsicComponentType]?: ( + source: CCComponentEditorRendererNodeLayoutSource, + ) => CCComponentEditorRendererNodeLayout; +} = { [ccIntrinsicComponentTypes.DISPLAY]: - ccComponentRendererNodeDisplayGeometryCalculator, + ccComponentRendererNodeDisplayLayoutCalculator, }; -export default function getCCComponentEditorRendererNodeGeometry( +export function getCCComponentEditorRendererNodeLayout( store: CCStore, nodeId: CCNodeId, -) { +): CCComponentEditorRendererNodeLayout { const node = nullthrows(store.nodes.get(nodeId)); const component = nullthrows(store.components.get(node.componentId)); const nodePins = store.nodePins.getManyByNodeId(nodeId); - const source: CCComponentEditorRendererNodeGeometrySource = { - position: node.position, + const layoutCalculator = + (component.intrinsicType && + specialLayoutCalculators[component.intrinsicType]) ?? + ccComponentRendererNodeDefaultLayoutCalculator; + + return layoutCalculator({ + config: node.config, inputNodePinIds: nodePins .filter((np) => { const cp = nullthrows(store.componentPins.get(np.componentPinId)); @@ -44,11 +50,32 @@ export default function getCCComponentEditorRendererNodeGeometry( return cp.type === "output"; }) .map((np) => np.id), + }); +} + +export function ccComponentEditorRendererLayoutToGeometry( + layout: CCComponentEditorRendererNodeLayout, + nodePosition: Vector2, +): CCComponentEditorRendererNodeGeometry { + const position = vector2.sub(nodePosition, vector2.div(layout.size, 2)); + return { + rect: { position: position, size: layout.size }, + nodePinPositionById: new Map( + layout.nodePinOffsetById + .entries() + .map(([nodePinId, offset]) => [ + nodePinId, + vector2.add(position, offset), + ]), + ), }; +} - const calculator = - (component.intrinsicType && - specialGeometryCalculators[component.intrinsicType]) ?? - ccComponentRendererNodeDefaultGeometryCalculator; - return calculator(source); +export function getCCComponentEditorRendererNodeGeometry( + store: CCStore, + nodeId: CCNodeId, +): CCComponentEditorRendererNodeGeometry { + const node = nullthrows(store.nodes.get(nodeId)); + const layout = getCCComponentEditorRendererNodeLayout(store, nodeId); + return ccComponentEditorRendererLayoutToGeometry(layout, node.position); } diff --git a/src/pages/edit/Editor/renderer/Node/index.tsx b/src/pages/edit/Editor/renderer/Node/index.tsx index 58dfe46..add0190 100644 --- a/src/pages/edit/Editor/renderer/Node/index.tsx +++ b/src/pages/edit/Editor/renderer/Node/index.tsx @@ -12,7 +12,10 @@ import { useComponentEditorStore } from "../../store"; import CCComponentEditorRendererNodePin from "../NodePin"; import { CCComponentEditorRendererNodeDefaultRenderer } from "./components/Default"; import { CCComponentEditorRendererNodeDisplayRenderer } from "./components/Display"; -import getCCComponentEditorRendererNodeGeometry from "./geometry"; +import { + ccComponentEditorRendererLayoutToGeometry, + getCCComponentEditorRendererNodeLayout, +} from "./geometry"; import type { CCComponentEditorRendererNodeRendererNodeState, CCComponentEditorRendererNodeRendererProps, @@ -44,7 +47,11 @@ const CCComponentEditorRendererNode = ensureStoreItem( vector2.zero, ); - const geometry = getCCComponentEditorRendererNodeGeometry(store, nodeId); + const layout = getCCComponentEditorRendererNodeLayout(store, nodeId); + const geometry = ccComponentEditorRendererLayoutToGeometry( + layout, + node.position, + ); const Renderer = (component.intrinsicType && specialRenderers[component.intrinsicType]) || CCComponentEditorRendererNodeDefaultRenderer; @@ -86,8 +93,13 @@ const CCComponentEditorRendererNode = ensureStoreItem( return ( <> - {/** biome-ignore lint/a11y/noStaticElementInteractions: SVG */} - - + {store.nodePins.getManyByNodeId(nodeId).map((nodePin) => ( ; }; -export type CCComponentEditorRendererNodeGeometrySource = { - position: Vector2; +export type CCComponentEditorRendererNodeLayoutSource = { + config: CCNode["config"]; inputNodePinIds: CCNodePinId[]; outputNodePinIds: CCNodePinId[]; }; @@ -26,6 +28,6 @@ export type CCComponentEditorRendererNodeGeometry = { nodePinPositionById: Map; }; -export type CCComponentEditorRendererNodeGeometryCalculator = ( - source: CCComponentEditorRendererNodeGeometrySource, -) => CCComponentEditorRendererNodeGeometry; +export type CCComponentEditorRendererNodeRendererNodeState = { + isSelected: boolean; +}; diff --git a/src/pages/edit/Editor/renderer/NodePin/index.tsx b/src/pages/edit/Editor/renderer/NodePin/index.tsx index 1c25056..c1a5c98 100644 --- a/src/pages/edit/Editor/renderer/NodePin/index.tsx +++ b/src/pages/edit/Editor/renderer/NodePin/index.tsx @@ -14,7 +14,7 @@ import { CCComponentEditorRendererConnectionCore, type CCComponentEditorRendererConnectionEndpoint, } from "./../Connection"; -import getCCComponentEditorRendererNodeGeometry from "./../Node/geometry"; +import { getCCComponentEditorRendererNodeGeometry } from "./../Node/geometry"; const NODE_PIN_POSITION_SENSITIVITY = 10; diff --git a/src/pages/edit/Editor/store/slices/core/index.ts b/src/pages/edit/Editor/store/slices/core/index.ts index ada2720..4240d94 100644 --- a/src/pages/edit/Editor/store/slices/core/index.ts +++ b/src/pages/edit/Editor/store/slices/core/index.ts @@ -191,11 +191,6 @@ export const createComponentEditorStoreCoreSlice: ComponentEditorSliceCreator< } if (isUpdated) editorStore.setState((s) => ({ ...s })); }; - store.nodes.on("didRegister", executeSimulation); - store.nodes.on("didUpdate", executeSimulation); - store.nodes.on("didUnregister", executeSimulation); - store.connections.on("didRegister", executeSimulation); - store.connections.on("didUnregister", executeSimulation); editorStore.subscribe(executeSimulation); }, }; diff --git a/src/store/component.ts b/src/store/component.ts index b2835e5..fc1bf11 100644 --- a/src/store/component.ts +++ b/src/store/component.ts @@ -219,3 +219,28 @@ export function validateAllComponents(store: CCStore) { validateComponent(store, component.id); } } + +export function isEachInputPinConnected( + store: CCStore, + componentId: CCComponentId, +) { + const component = nullthrows(store.components.get(componentId)); + if (component.intrinsicType) return true; + const nodes = store.nodes.getManyByParentComponentId(componentId); + for (const node of nodes) { + const nodePins = store.nodePins.getManyByNodeId(node.id); + for (const nodePin of nodePins) { + const componentPin = nullthrows( + store.componentPins.get(nodePin.componentPinId), + ); + if (componentPin.type === "input") { + const connectionsAssociatedWithNodePin = + store.connections.getConnectionsByNodePinId(nodePin.id); + if (connectionsAssociatedWithNodePin.length === 0) { + return false; + } + } + } + } + return true; +} diff --git a/src/store/connection.ts b/src/store/connection.ts index 938e59a..4d0d332 100644 --- a/src/store/connection.ts +++ b/src/store/connection.ts @@ -61,6 +61,35 @@ export class CCConnectionStore extends EventEmitter { this.unregister(connections.map((connection) => connection.id)); } }); + this.#store.connections.on("didRegister", (connection) => { + const component = nullthrows( + this.#store.components.get(connection.parentComponentId), + ); + const nodes = this.#store.nodes.getManyByComponentId(component.id); + for (const node of nodes) { + const nodePins = this.#store.nodePins.getManyByNodeId(node.id); + for (const nodePin of nodePins) { + const connections = this.getConnectionsByNodePinId(nodePin.id); + for (const connection of connections) { + const parentComponentId = connection.parentComponentId; + const from = connection.from; + const to = connection.to; + this.unregister([connection.id]).then(() => { + if (this.#store.nodePins.isConnectable(from, to)) { + this.register( + CCConnectionStore.create({ + from, + to, + parentComponentId, + bentPortion: 0.5, + }), + ); + } + }); + } + } + } + }); } /** From 1c94d8a84eae2225402573de9ca87f471e51a96a Mon Sep 17 00:00:00 2001 From: chelproc Date: Sun, 14 Jun 2026 18:21:24 +0900 Subject: [PATCH 2/6] Add display config editing and cache node pin bit widths - Make the display ConfigSettingButton functional: open a Popover form to edit resolution (x/y) and persist via store.nodes.update - Compute display node size from config inside the layout calculator - Cache getNodePinBitWidthStatus results, invalidating on node/pin/ connection register/unregister/update events - Switch input value bit-width init to getNodePinBitWidthStatus and deprecate getComponentPinBitWidthStatus - Convert InputValueKey from a tuple to a { componentPinId, timeStep } object with a serializeInputValueKey helper - Add a reactive useCanSimulate selector and use it to drive the ViewModeSwitcher disabled state --- .../Editor/components/ViewModeSwitcher.tsx | 9 +- .../Display/ConfigSettingButton.tsx | 123 +++++++++++++++--- .../Node/components/Display/geometry.ts | 12 +- .../Node/components/Display/index.tsx | 8 +- .../edit/Editor/store/slices/core/index.ts | 63 +++++---- .../edit/Editor/store/slices/core/types.ts | 8 +- src/store/componentPin.ts | 1 + src/store/nodePin.ts | 19 ++- src/store/react/selectors.ts | 40 +++++- 9 files changed, 225 insertions(+), 58 deletions(-) diff --git a/src/pages/edit/Editor/components/ViewModeSwitcher.tsx b/src/pages/edit/Editor/components/ViewModeSwitcher.tsx index da79a9a..4421f34 100644 --- a/src/pages/edit/Editor/components/ViewModeSwitcher.tsx +++ b/src/pages/edit/Editor/components/ViewModeSwitcher.tsx @@ -1,12 +1,11 @@ import { Edit, PlayArrow } from "@mui/icons-material"; import { Fab } from "@mui/material"; -import { isEachInputPinConnected } from "../../../../store/component"; -import { useStore } from "../../../../store/react"; +import { useCanSimulate } from "../../../../store/react/selectors"; import { useComponentEditorStore } from "../store"; export default function CCComponentEditorViewModeSwitcher() { const componentEditorState = useComponentEditorStore()(); - const { store } = useStore(); + const canSimulate = useCanSimulate(componentEditorState.componentId); return ( {componentEditorState.editorMode === "edit" ? : } diff --git a/src/pages/edit/Editor/renderer/Node/components/Display/ConfigSettingButton.tsx b/src/pages/edit/Editor/renderer/Node/components/Display/ConfigSettingButton.tsx index 5830639..b44d29b 100644 --- a/src/pages/edit/Editor/renderer/Node/components/Display/ConfigSettingButton.tsx +++ b/src/pages/edit/Editor/renderer/Node/components/Display/ConfigSettingButton.tsx @@ -1,26 +1,117 @@ import { SettingsOutlined } from "@mui/icons-material"; -import { IconButton } from "@mui/material"; -import type { CCNodeId } from "../../../../../../../store/node"; -import type { CCComponentEditorRendererNodeGeometry } from "../../types"; +import { + Button, + FormLabel, + IconButton, + Popover, + Stack, + TextField, + Typography, +} from "@mui/material"; +import { useState } from "react"; +import type { CCIntrinsicComponentDisplaySpec } from "../../../../../../../store/intrinsics/types"; type Props = { - nodeId: CCNodeId; - geometry: CCComponentEditorRendererNodeGeometry; + config: CCIntrinsicComponentDisplaySpec["config"]; + onConfigChange: (config: CCIntrinsicComponentDisplaySpec["config"]) => void; }; export function CCComponentEditorRendererNodeDisplayRendererConfigSettingButton( - _props: Props, + props: Props, ) { + const [anchorEl, setAnchorEl] = useState(null); + const [newConfig, setNewConfig] = useState(props.config); + return ( - { - e.stopPropagation(); - }} - disableTouchRipple - disableFocusRipple - > - - + <> + e.stopPropagation()} + onClick={(e) => { + e.stopPropagation(); + setAnchorEl(e.currentTarget); + setNewConfig(props.config); + }} + > + + + setAnchorEl(null)} + slotProps={{ + root: { onPointerDown: (e) => e.stopPropagation() }, + paper: { sx: { width: "300px", p: 2 } }, + }} + > +
{ + e.preventDefault(); + props.onConfigChange(newConfig); + setAnchorEl(null); + }} + > + + Display Settings + + + Resolution + + + + setNewConfig((prev) => ({ + ...prev, + resolution: { + ...prev.resolution, + x: parseInt(e.target.value, 10) || 0, + }, + })) + } + size="small" + sx={{ flex: 1 }} + slotProps={{ + htmlInput: { inputMode: "numeric", sx: { textAlign: "end" } }, + }} + /> + + x + + + setNewConfig((prev) => ({ + ...prev, + resolution: { + ...prev.resolution, + y: parseInt(e.target.value, 10) || 0, + }, + })) + } + size="small" + sx={{ flex: 1 }} + slotProps={{ + htmlInput: { inputMode: "numeric", sx: { textAlign: "end" } }, + }} + /> + + + + + +
+
+ ); } diff --git a/src/pages/edit/Editor/renderer/Node/components/Display/geometry.ts b/src/pages/edit/Editor/renderer/Node/components/Display/geometry.ts index 9a94731..e4eb639 100644 --- a/src/pages/edit/Editor/renderer/Node/components/Display/geometry.ts +++ b/src/pages/edit/Editor/renderer/Node/components/Display/geometry.ts @@ -7,8 +7,6 @@ import type { CCComponentEditorRendererNodeLayoutSource, } from "../../types"; -const size = { x: 320, y: 200 }; - export const ccComponentEditorRendererNodeDisplayLayoutConstants = { padding: 8, gridSize: 12, @@ -22,11 +20,13 @@ export function ccComponentRendererNodeDisplayLayoutCalculator( ccComponentEditorRendererNodeDisplayLayoutConstants; const config = source.config as CCIntrinsicComponentDisplaySpec["config"]; + const size = { + x: gridSizeDisplayWidth + gridSize * config.resolution.x + padding * 2, + y: gridSize * config.resolution.y + padding * 2, + }; + return { - size: { - x: gridSizeDisplayWidth + gridSize * config.resolution.x + padding * 2, - y: gridSize * config.resolution.y + padding * 2, - }, + size, nodePinOffsetById: new Map([ [nullthrows(source.inputNodePinIds[0]), vector2.create(0, size.y / 2)], ]), diff --git a/src/pages/edit/Editor/renderer/Node/components/Display/index.tsx b/src/pages/edit/Editor/renderer/Node/components/Display/index.tsx index d63a48c..12c0cdc 100644 --- a/src/pages/edit/Editor/renderer/Node/components/Display/index.tsx +++ b/src/pages/edit/Editor/renderer/Node/components/Display/index.tsx @@ -13,7 +13,11 @@ export function CCComponentEditorRendererNodeDisplayRenderer( props: CCComponentEditorRendererNodeRendererProps, ) { const { store } = useStore(); + const config = props.node.config as CCIntrinsicComponentDisplaySpec["config"]; + const updateConfig = (newConfig: CCIntrinsicComponentDisplaySpec["config"]) => + store.nodes.update(props.node.id, { config: newConfig }); + const inputNodePin = nullthrows( store.nodePins .getManyByNodeId(props.node.id) @@ -43,8 +47,8 @@ export function CCComponentEditorRendererNodeDisplayRenderer( {Array(config.resolution.y) diff --git a/src/pages/edit/Editor/store/slices/core/index.ts b/src/pages/edit/Editor/store/slices/core/index.ts index 4240d94..243d3da 100644 --- a/src/pages/edit/Editor/store/slices/core/index.ts +++ b/src/pages/edit/Editor/store/slices/core/index.ts @@ -11,7 +11,11 @@ import type { // import type { CCComponentId } from "../../../../../../store/component"; import simulateComponent from "../../../../../../store/simulation"; import type { ComponentEditorSliceCreator } from "../../types"; -import type { EditorStoreCoreSlice, InputValueKey } from "./types"; +import { + type EditorStoreCoreSlice, + type InputValueKey, + serializeInputValueKey, +} from "./types"; export function stringifySimulationValue(value: SimulationValue): string { const binary = value.map((v) => (v ? "1" : "0")).join(""); @@ -44,30 +48,39 @@ export const createComponentEditorStoreCoreSlice: ComponentEditorSliceCreator< /** @private */ inputValues: new Map(), getInputValue(inputValueKey: InputValueKey) { - const value = get().inputValues.get(JSON.stringify(inputValueKey)); - if (!value) { - const previousTimeStepValue = get().inputValues.get( - JSON.stringify([inputValueKey[0], inputValueKey[1] - 1]), - ); - if (previousTimeStepValue) { - get().setInputValue(inputValueKey, previousTimeStepValue); - return previousTimeStepValue; - } - const bitWidthStatus = - store.componentPins.getComponentPinBitWidthStatus( - inputValueKey[0], - ); - if (bitWidthStatus.isFixed) { - const newValue = new Array(bitWidthStatus.bitWidth).fill(false); - return newValue; - } - if (bitWidthStatus.fixMode === "manual") { - throw new Error("Cannot determine bit width"); - } - const newValue = [false]; - return newValue; + // If value exists for the current time step, return it + const value = get().inputValues.get( + serializeInputValueKey(inputValueKey), + ); + if (value) return value; + + // If not, try to find the value from the previous time step + const previousTimeStepValue = get().inputValues.get( + serializeInputValueKey({ + ...inputValueKey, + timeStep: inputValueKey.timeStep - 1, + }), + ); + if (previousTimeStepValue) { + get().setInputValue(inputValueKey, previousTimeStepValue); + return previousTimeStepValue; } - return value; + + // If not found, initialize the value based on the bit width of the pin + const componentPin = nullthrows( + store.componentPins.get(inputValueKey.componentPinId), + ); + const bitWidthStatus = store.nodePins.getNodePinBitWidthStatus( + nullthrows( + componentPin.implementation, + "Cannot get input value for intrinsic component pin", + ), + ); + return bitWidthStatus.isFixed + ? // If the bit width is fixed, initialize the value with the specified bit width + new Array(bitWidthStatus.bitWidth).fill(false) + : // If the bit width is not fixed, initialize the single-bit value as false + [false]; }, setInputValue(inputValueKey: InputValueKey, value: SimulationValue) { set((state) => { @@ -178,7 +191,7 @@ export const createComponentEditorStoreCoreSlice: ComponentEditorSliceCreator< if (pin.type === "input") { inputValues.set( pin.id, - editorState.getInputValue([pin.id, timeStep]), + editorState.getInputValue({ componentPinId: pin.id, timeStep }), ); } } diff --git a/src/pages/edit/Editor/store/slices/core/types.ts b/src/pages/edit/Editor/store/slices/core/types.ts index bf8916b..5f88bb7 100644 --- a/src/pages/edit/Editor/store/slices/core/types.ts +++ b/src/pages/edit/Editor/store/slices/core/types.ts @@ -13,7 +13,13 @@ export type RangeSelect = { start: Vector2; end: Vector2 } | null; export type TimeStep = number; -export type InputValueKey = [CCComponentPinId, TimeStep]; +export type InputValueKey = { + componentPinId: CCComponentPinId; + timeStep: TimeStep; +}; +export function serializeInputValueKey(key: InputValueKey): string { + return `${key.componentPinId}:${key.timeStep}`; +} export type NodePinPropertyEditorTarget = { nodeId: CCNodeId; diff --git a/src/store/componentPin.ts b/src/store/componentPin.ts index 0376f4d..fc7f692 100644 --- a/src/store/componentPin.ts +++ b/src/store/componentPin.ts @@ -208,6 +208,7 @@ export class CCComponentPinStore extends EventEmitter * Get the bit width status of a component pin * @param pinId id of pin * @returns bit width status of the pin + * @deprecated use getNodePinBitWidthStatus instead */ getComponentPinBitWidthStatus( pinId: CCComponentPinId, diff --git a/src/store/nodePin.ts b/src/store/nodePin.ts index 7a1d91a..cf239b1 100644 --- a/src/store/nodePin.ts +++ b/src/store/nodePin.ts @@ -37,6 +37,12 @@ export class CCNodePinStore extends EventEmitter { #markedAsDeleted: Set = new Set(); + #bitWidthCache: Map = new Map(); + + #clearBitWidthCache(): void { + this.#bitWidthCache.clear(); + } + /** * Constructor of CCNodePinStore * @param store store @@ -54,6 +60,10 @@ export class CCNodePinStore extends EventEmitter { } mount() { + this.#store.connections.on("didRegister", () => this.#clearBitWidthCache()); + this.#store.connections.on("didUnregister", () => + this.#clearBitWidthCache(), + ); this.#store.nodes.on("didRegister", (node) => { const componentPins = this.#store.componentPins.getManyByComponentId( node.componentId, @@ -105,6 +115,7 @@ export class CCNodePinStore extends EventEmitter { invariant(this.#store.componentPins.get(nodePin.componentPinId)); invariant(this.#store.nodes.get(nodePin.nodeId)); this.#nodePins.set(nodePin.id, nodePin); + this.#clearBitWidthCache(); this.emit("didRegister", nodePin); } @@ -119,6 +130,7 @@ export class CCNodePinStore extends EventEmitter { this.emit("willUnregister", nodePin); this.#nodePins.delete(nodePin.id); }); + this.#clearBitWidthCache(); this.emit("didUnregister", nodePin); this.#markedAsDeleted.delete(id); } @@ -127,6 +139,7 @@ export class CCNodePinStore extends EventEmitter { const existingNodePin = nullthrows(this.#nodePins.get(id)); const newNodePin = { ...existingNodePin, ...value }; this.#nodePins.set(id, newNodePin); + this.#clearBitWidthCache(); this.emit("didUpdate", newNodePin); } @@ -181,6 +194,8 @@ export class CCNodePinStore extends EventEmitter { * @returns bit width status of the pin */ getNodePinBitWidthStatus(nodePinId: CCNodePinId): CCNodePinBitWidthStatus { + const cached = this.#bitWidthCache.get(nodePinId); + if (cached) return cached; const traverseNodePinBitWidthStatus = ( targetNodePinId: CCNodePinId, seen: Set, @@ -294,7 +309,9 @@ export class CCNodePinStore extends EventEmitter { } return givenComponentPinBitWidthStatus; }; - return traverseNodePinBitWidthStatus(nodePinId, new Set()); + const result = traverseNodePinBitWidthStatus(nodePinId, new Set()); + this.#bitWidthCache.set(nodePinId, result); + return result; } isMarkedAsDeleted(id: CCNodePinId) { diff --git a/src/store/react/selectors.ts b/src/store/react/selectors.ts index b6fc032..8ab7ac8 100644 --- a/src/store/react/selectors.ts +++ b/src/store/react/selectors.ts @@ -1,7 +1,11 @@ import memoizeOne from "memoize-one"; import nullthrows from "nullthrows"; import { useCallback, useMemo, useSyncExternalStore } from "react"; -import type { CCComponent, CCComponentId } from "../component"; +import { + type CCComponent, + type CCComponentId, + isEachInputPinConnected, +} from "../component"; import type { CCComponentPin } from "../componentPin"; import type { CCNode, CCNodeId } from "../node"; import type { CCNodePin } from "../nodePin"; @@ -185,3 +189,37 @@ export function useNodePins(nodeId: CCNodeId) { ); return useSyncExternalStore(subscribe, getSnapshot); } + +export function useCanSimulate(componentId: CCComponentId) { + const { store } = useStore(); + const getSnapshot = useCallback( + () => isEachInputPinConnected(store, componentId), + [store, componentId], + ); + const subscribe = useCallback( + (onStoreChange: () => void) => { + store.nodes.on("didRegister", onStoreChange); + store.nodes.on("didUnregister", onStoreChange); + store.nodePins.on("didRegister", onStoreChange); + store.nodePins.on("didUnregister", onStoreChange); + store.componentPins.on("didRegister", onStoreChange); + store.componentPins.on("didUpdate", onStoreChange); + store.componentPins.on("didUnregister", onStoreChange); + store.connections.on("didRegister", onStoreChange); + store.connections.on("didUnregister", onStoreChange); + return () => { + store.nodes.off("didRegister", onStoreChange); + store.nodes.off("didUnregister", onStoreChange); + store.nodePins.off("didRegister", onStoreChange); + store.nodePins.off("didUnregister", onStoreChange); + store.componentPins.off("didRegister", onStoreChange); + store.componentPins.off("didUpdate", onStoreChange); + store.componentPins.off("didUnregister", onStoreChange); + store.connections.off("didRegister", onStoreChange); + store.connections.off("didUnregister", onStoreChange); + }; + }, + [store], + ); + return useSyncExternalStore(subscribe, getSnapshot); +} From 164e0b241e750ec64f1ca0cbd2d9bf42d7dc7bc7 Mon Sep 17 00:00:00 2001 From: chelproc Date: Sat, 8 Aug 2026 11:18:25 +0900 Subject: [PATCH 3/6] fix data structure and key generation of input value --- .../Editor/renderer/ComponentPin/index.tsx | 21 +++++++++++-------- .../edit/Editor/store/slices/core/index.ts | 2 +- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/pages/edit/Editor/renderer/ComponentPin/index.tsx b/src/pages/edit/Editor/renderer/ComponentPin/index.tsx index a0ce9a2..a91daee 100644 --- a/src/pages/edit/Editor/renderer/ComponentPin/index.tsx +++ b/src/pages/edit/Editor/renderer/ComponentPin/index.tsx @@ -30,10 +30,10 @@ export default function CCComponentEditorRendererComponentPin({ label: stringifySimulationValue( type === "input" ? nullthrows( - componentEditorState.getInputValue([ - interfaceComponentPin.id, - componentEditorState.timeStep, - ]), + componentEditorState.getInputValue({ + componentPinId: interfaceComponentPin.id, + timeStep: componentEditorState.timeStep, + }), ) : nullthrows(componentEditorState.getNodePinValue(nodePinId)), ), @@ -41,13 +41,16 @@ export default function CCComponentEditorRendererComponentPin({ type === "input" ? () => { const nodePinValue = nullthrows( - componentEditorState.getInputValue([ - interfaceComponentPin.id, - componentEditorState.timeStep, - ]), + componentEditorState.getInputValue({ + componentPinId: interfaceComponentPin.id, + timeStep: componentEditorState.timeStep, + }), ); componentEditorState.setInputValue( - [interfaceComponentPin.id, componentEditorState.timeStep], + { + componentPinId: interfaceComponentPin.id, + timeStep: componentEditorState.timeStep, + }, wrappingIncrementSimulationValue(nodePinValue), ); } diff --git a/src/pages/edit/Editor/store/slices/core/index.ts b/src/pages/edit/Editor/store/slices/core/index.ts index 243d3da..f6ee5f1 100644 --- a/src/pages/edit/Editor/store/slices/core/index.ts +++ b/src/pages/edit/Editor/store/slices/core/index.ts @@ -87,7 +87,7 @@ export const createComponentEditorStoreCoreSlice: ComponentEditorSliceCreator< return { ...state, inputValues: new Map(state.inputValues).set( - JSON.stringify(inputValueKey), + serializeInputValueKey(inputValueKey), value, ), }; From c0147e29d480c343288ee2c9897ea63ee5615e75 Mon Sep 17 00:00:00 2001 From: chelproc Date: Sat, 8 Aug 2026 12:21:49 +0900 Subject: [PATCH 4/6] Add Const intrinsic component and fix display pixel index order - Add CONST intrinsic component with fixed-bit-width output driven by configured data, plus a dedicated renderer and layout calculator - Pass node config to intrinsic evaluate functions - Fix Display renderer pixel indexing to use row-major order directly - Remove FlipFlop debug log; log pin bit widths on fixed-width mismatch --- .../Node/components/Const/geometry.ts | 24 ++++++++ .../renderer/Node/components/Const/index.tsx | 59 +++++++++++++++++++ .../Node/components/Display/index.tsx | 5 +- .../edit/Editor/renderer/Node/geometry.ts | 3 + src/pages/edit/Editor/renderer/Node/index.tsx | 2 + src/store/intrinsics/base.ts | 22 +++---- src/store/intrinsics/definitions.ts | 40 ++++++++++++- src/store/intrinsics/types.ts | 9 +++ src/store/nodePin.ts | 2 + src/store/simulation.ts | 2 +- 10 files changed, 150 insertions(+), 18 deletions(-) create mode 100644 src/pages/edit/Editor/renderer/Node/components/Const/geometry.ts create mode 100644 src/pages/edit/Editor/renderer/Node/components/Const/index.tsx diff --git a/src/pages/edit/Editor/renderer/Node/components/Const/geometry.ts b/src/pages/edit/Editor/renderer/Node/components/Const/geometry.ts new file mode 100644 index 0000000..10a27d2 --- /dev/null +++ b/src/pages/edit/Editor/renderer/Node/components/Const/geometry.ts @@ -0,0 +1,24 @@ +import nullthrows from "nullthrows"; +import { type Vector2, vector2 } from "../../../../../../../common/vector2"; +import type { CCNodePinId } from "../../../../../../../store/nodePin"; +import type { + CCComponentEditorRendererNodeLayout, + CCComponentEditorRendererNodeLayoutSource, +} from "../../types"; + +export const ccComponentEditorRendererNodeConstLayoutConstants = { + padding: 8, + gridSize: 12, + gridSizeDisplayWidth: 60, +}; + +export function ccComponentRendererNodeConstLayoutCalculator( + source: CCComponentEditorRendererNodeLayoutSource, +): CCComponentEditorRendererNodeLayout { + return { + size: vector2.create(400, 100), + nodePinOffsetById: new Map([ + [nullthrows(source.outputNodePinIds[0]), vector2.create(400, 50)], + ]), + }; +} diff --git a/src/pages/edit/Editor/renderer/Node/components/Const/index.tsx b/src/pages/edit/Editor/renderer/Node/components/Const/index.tsx new file mode 100644 index 0000000..0e02f6f --- /dev/null +++ b/src/pages/edit/Editor/renderer/Node/components/Const/index.tsx @@ -0,0 +1,59 @@ +import { useMemo } from "react"; +import type { CCIntrinsicComponentConstSpec } from "../../../../../../../store/intrinsics/types"; +import type { CCComponentEditorRendererNodeRendererProps } from "../../types"; +import { CCComponentEditorRendererNodeDefaultRenderer } from "../Default"; +import { ccComponentEditorRendererNodeConstLayoutConstants as constants } from "./geometry"; + +export function CCComponentEditorRendererNodeConstRenderer( + props: CCComponentEditorRendererNodeRendererProps, +) { + const config = props.node.config as CCIntrinsicComponentConstSpec["config"]; + const sampleData = useMemo( + () => Array.from({ length: config.data.length }, () => Math.random() < 0.5), + [config.data], + ); + + return ( + <> + + + + + {Array(Math.ceil(config.data.length / 8)) + .fill(null) + .map((_, rowIndex) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: temporary workaround + + {Array(8) + .fill(null) + .map((_, colIndex) => { + const index = rowIndex * 8 + colIndex; + if (index >= config.data.length) return null; + return ( + + ))} + +
+ ); + })} +
+
+ + ); +} diff --git a/src/pages/edit/Editor/renderer/Node/components/Display/index.tsx b/src/pages/edit/Editor/renderer/Node/components/Display/index.tsx index 12c0cdc..bc23f89 100644 --- a/src/pages/edit/Editor/renderer/Node/components/Display/index.tsx +++ b/src/pages/edit/Editor/renderer/Node/components/Display/index.tsx @@ -64,10 +64,7 @@ export function CCComponentEditorRendererNodeDisplayRenderer( width={gridSize} height={gridSize} fill={ - inputValue?.[ - config.resolution.x * config.resolution.y - - (1 + x + config.resolution.x * y) - ] + inputValue?.[x + config.resolution.x * y] ? theme.palette.black : theme.palette.white } diff --git a/src/pages/edit/Editor/renderer/Node/geometry.ts b/src/pages/edit/Editor/renderer/Node/geometry.ts index 9eb4edc..45c6853 100644 --- a/src/pages/edit/Editor/renderer/Node/geometry.ts +++ b/src/pages/edit/Editor/renderer/Node/geometry.ts @@ -6,6 +6,7 @@ import { ccIntrinsicComponentTypes, } from "../../../../../store/intrinsics/types"; import type { CCNodeId } from "../../../../../store/node"; +import { ccComponentRendererNodeConstLayoutCalculator } from "./components/Const/geometry"; import { ccComponentRendererNodeDefaultLayoutCalculator } from "./components/Default/geometry"; import { ccComponentRendererNodeDisplayLayoutCalculator } from "./components/Display/geometry"; import type { @@ -21,6 +22,8 @@ const specialLayoutCalculators: { } = { [ccIntrinsicComponentTypes.DISPLAY]: ccComponentRendererNodeDisplayLayoutCalculator, + [ccIntrinsicComponentTypes.CONST]: + ccComponentRendererNodeConstLayoutCalculator, }; export function getCCComponentEditorRendererNodeLayout( diff --git a/src/pages/edit/Editor/renderer/Node/index.tsx b/src/pages/edit/Editor/renderer/Node/index.tsx index add0190..9a09029 100644 --- a/src/pages/edit/Editor/renderer/Node/index.tsx +++ b/src/pages/edit/Editor/renderer/Node/index.tsx @@ -10,6 +10,7 @@ import ensureStoreItem from "../../../../../store/react/error"; import { useComponent, useNode } from "../../../../../store/react/selectors"; import { useComponentEditorStore } from "../../store"; import CCComponentEditorRendererNodePin from "../NodePin"; +import { CCComponentEditorRendererNodeConstRenderer } from "./components/Const"; import { CCComponentEditorRendererNodeDefaultRenderer } from "./components/Default"; import { CCComponentEditorRendererNodeDisplayRenderer } from "./components/Display"; import { @@ -29,6 +30,7 @@ const specialRenderers: Partial< > = { [ccIntrinsicComponentTypes.DISPLAY]: CCComponentEditorRendererNodeDisplayRenderer, + [ccIntrinsicComponentTypes.CONST]: CCComponentEditorRendererNodeConstRenderer, }; export type CCComponentEditorRendererNodeProps = { diff --git a/src/store/intrinsics/base.ts b/src/store/intrinsics/base.ts index 348f45f..33c5c82 100644 --- a/src/store/intrinsics/base.ts +++ b/src/store/intrinsics/base.ts @@ -48,17 +48,23 @@ type IntrinsicComponentPinAttributes = { isBitWidthConfigurable?: boolean; isSplittable?: boolean; }; + +type IntrinsicComponentEvaluationFunction< + Spec extends CCIntrinsicComponentSpec, +> = ( + context: ComponentEvaluationContext, + nodeId: CCNodeId, + shape: CCIntrinsicComponentShape, + config: Spec["config"], +) => boolean; + type Props = { type: CCIntrinsicComponentType; name: string; in: Record>; out: Record>; initialConfig: Spec["config"]; - evaluate: ( - context: ComponentEvaluationContext, - nodeId: CCNodeId, - shape: CCIntrinsicComponentShape, - ) => boolean; // returns whether evaluation succeeded + evaluate: IntrinsicComponentEvaluationFunction; }; export class IntrinsicComponentDefinition< Spec extends CCIntrinsicComponentSpec = CCIntrinsicComponentSpec, @@ -71,11 +77,7 @@ export class IntrinsicComponentDefinition< readonly inputPin: Record; readonly outputPin: Record; readonly initialConfig: Spec["config"]; - readonly evaluate: ( - context: ComponentEvaluationContext, - nodeId: CCNodeId, - shape: CCIntrinsicComponentShape, - ) => boolean; + readonly evaluate: IntrinsicComponentEvaluationFunction; private static _lastIndex = 0; diff --git a/src/store/intrinsics/definitions.ts b/src/store/intrinsics/definitions.ts index 4b9d1d5..3f8e59d 100644 --- a/src/store/intrinsics/definitions.ts +++ b/src/store/intrinsics/definitions.ts @@ -5,6 +5,7 @@ import type { CCComponentPinId } from "../componentPin"; import { IntrinsicComponentDefinition } from "./base"; import { type CCIntrinsicComponentBinaryOperatorSpec, + type CCIntrinsicComponentConstSpec, type CCIntrinsicComponentDisplaySpec, type CCIntrinsicComponentInputSpec, type CCIntrinsicComponentNullaryOperatorSpec, @@ -320,7 +321,6 @@ export const flipflop = const outputShape = shape.outputShape.Out; invariant(outputShape[0] && !outputShape[1]); const nodePinIdToValue = context.currentFrame.nodes.get(nodeId)?.pins; - console.log("FlipFlop evaluate", { nodeId, inputShape, outputShape }); const previousValue = context.previousFrame?.nodes .get(nodeId) @@ -353,6 +353,31 @@ export const display = evaluate: () => true, }); +export const const_ = + new IntrinsicComponentDefinition({ + type: ccIntrinsicComponentTypes.CONST, + name: "Const", + in: {}, + out: { + Out: { + name: "Out", + bitWidthPolicy: { + type: "fixed", + calculateBitWidth: (config) => config.data.length, + }, + }, + }, + initialConfig: { data: new Array(8).fill(false) }, + evaluate: (context, nodeId, shape, config) => { + const outputShape = shape.outputShape.Out; + invariant(outputShape[0] && !outputShape[1]); + const nodePinIdToValue = context.currentFrame.nodes.get(nodeId)?.pins; + if (!nodePinIdToValue) return false; + nodePinIdToValue.set(outputShape[0].nodePinId, config.data); + return true; + }, + }); + export const definitions: { [t in CCIntrinsicComponentType]: IntrinsicComponentDefinition< CCIntrinsicComponentSpecByType[t] @@ -369,6 +394,7 @@ export const definitions: { [ccIntrinsicComponentTypes.BROADCAST]: broadcast, [ccIntrinsicComponentTypes.FLIPFLOP]: flipflop, [ccIntrinsicComponentTypes.DISPLAY]: display, + [ccIntrinsicComponentTypes.CONST]: const_, [ccIntrinsicComponentTypes.TRUE]: true_, [ccIntrinsicComponentTypes.FALSE]: false_, }; @@ -376,13 +402,21 @@ export const definitions: { export const definitionByComponentId = new Map< CCComponentId, IntrinsicComponentDefinition ->(Object.values(definitions).map((definition) => [definition.id, definition])); +>( + Object.values(definitions).map((definition) => [ + definition.id, + definition as IntrinsicComponentDefinition, + ]), +); export const definitionByComponentPinId = new Map< CCComponentPinId, IntrinsicComponentDefinition >( Object.values(definitions).flatMap((definition) => - definition.allPins.map((pin) => [pin.id, definition]), + definition.allPins.map((pin) => [ + pin.id, + definition as IntrinsicComponentDefinition, + ]), ), ); diff --git a/src/store/intrinsics/types.ts b/src/store/intrinsics/types.ts index 8044d90..3aaa604 100644 --- a/src/store/intrinsics/types.ts +++ b/src/store/intrinsics/types.ts @@ -1,5 +1,6 @@ import type { JsonValue } from "type-fest"; import type { Vector2 } from "../../common/vector2"; +import type { SimulationValue } from "../simulation"; export const ccIntrinsicComponentTypes = { AND: "AND", @@ -13,6 +14,7 @@ export const ccIntrinsicComponentTypes = { DECOMPOSE: "DECOMPOSE", FLIPFLOP: "FLIPFLOP", DISPLAY: "DISPLAY", + CONST: "CONST", TRUE: "TRUE", FALSE: "FALSE", } as const; @@ -60,6 +62,12 @@ export type CCIntrinsicComponentDisplaySpec = { config: { resolution: Vector2 }; }; +export type CCIntrinsicComponentConstSpec = { + in: never; + out: "Out"; + config: { data: SimulationValue }; +}; + export type CCIntrinsicComponentSpecByType = { [ccIntrinsicComponentTypes.AND]: CCIntrinsicComponentBinaryOperatorSpec; [ccIntrinsicComponentTypes.OR]: CCIntrinsicComponentBinaryOperatorSpec; @@ -72,6 +80,7 @@ export type CCIntrinsicComponentSpecByType = { [ccIntrinsicComponentTypes.BROADCAST]: CCIntrinsicComponentUnaryOperatorSpec; [ccIntrinsicComponentTypes.FLIPFLOP]: CCIntrinsicComponentUnaryOperatorSpec; [ccIntrinsicComponentTypes.DISPLAY]: CCIntrinsicComponentDisplaySpec; + [ccIntrinsicComponentTypes.CONST]: CCIntrinsicComponentConstSpec; [ccIntrinsicComponentTypes.TRUE]: CCIntrinsicComponentNullaryOperatorSpec; [ccIntrinsicComponentTypes.FALSE]: CCIntrinsicComponentNullaryOperatorSpec; }; diff --git a/src/store/nodePin.ts b/src/store/nodePin.ts index cf239b1..86aa01a 100644 --- a/src/store/nodePin.ts +++ b/src/store/nodePin.ts @@ -380,6 +380,8 @@ export class CCNodePinStore extends EventEmitter { console.warn( `Cannot connect pins with fixed bit width: ${aNodePin.id} and ${bNodePin.id}`, ); + console.log(`Bit width of ${aNodePin.id}: ${aBitWidthStatus.bitWidth}`); + console.log(`Bit width of ${bNodePin.id}: ${bBitWidthStatus.bitWidth}`); return aBitWidthStatus.bitWidth === bBitWidthStatus.bitWidth; } return true; diff --git a/src/store/simulation.ts b/src/store/simulation.ts index 11761e6..a9e77c5 100644 --- a/src/store/simulation.ts +++ b/src/store/simulation.ts @@ -100,7 +100,7 @@ function simulateIntrinsic( const componentDefinition = definitionByComponentId.get(componentId); invariant(componentDefinition); const shape = createIntrinsicComponentShape(store, nodeId, context); - return componentDefinition.evaluate(context, nodeId, shape); + return componentDefinition.evaluate(context, nodeId, shape, node.config); } function simulateNode( From 5d40ef72efb19071f23f0f8b5a154f4e8fd50d74 Mon Sep 17 00:00:00 2001 From: Rn86222 Date: Sat, 8 Aug 2026 12:24:25 +0900 Subject: [PATCH 5/6] fix(store): resolve pin bit widths from the node they belong to The bit width of a pin was resolved by getComponentPinBitWidthStatus, which only knows the component pin definition. A `fixed` policy was therefore evaluated against `definition.initialConfig`, so changing the resolution of a display never changed the bit width of its Pixels pin. For the same reason aggregate and decompose could not be evaluated there at all and were special cased into a `manual` fix mode, with their widths recomputed by hand in CCNodePinStore. Split the two concerns: getComponentPinBitWidthStatus now only reports how a width is determined (`automatic` or `nodeDependent`), and getNodePinBitWidthStatus resolves it from the config of the node and the manual bit widths of its pins. The `fixed` policy is renamed to `calculated` to match, and the duplicated per-node calculations for aggregate and decompose are gone. Bit widths are cached, so CCNodeStore now emits `didUpdateConfig`, kept separate from `didUpdate` so that frequent position updates do not drop the cache. Because a width can now change after a connection was made, connections whose ends no longer agree are dropped when the config changes. Also derive whether a pin is configurable and splittable from its bit width policy instead of duplicating it on the pin attributes. Co-Authored-By: Claude Opus 5 (1M context) --- .../components/NodePinPropertyEditor.tsx | 51 ++-- src/store/componentPin.ts | 53 ++-- src/store/connection.ts | 20 ++ src/store/intrinsics/base.ts | 81 ++--- src/store/intrinsics/definitions.ts | 13 +- src/store/node.ts | 8 + src/store/nodePin.test.ts | 284 ++++++++++++++++++ src/store/nodePin.ts | 167 +++++----- 8 files changed, 502 insertions(+), 175 deletions(-) create mode 100644 src/store/nodePin.test.ts diff --git a/src/pages/edit/Editor/components/NodePinPropertyEditor.tsx b/src/pages/edit/Editor/components/NodePinPropertyEditor.tsx index 2dff789..57db9ed 100644 --- a/src/pages/edit/Editor/components/NodePinPropertyEditor.tsx +++ b/src/pages/edit/Editor/components/NodePinPropertyEditor.tsx @@ -169,31 +169,32 @@ export function CCComponentEditorNodePinPropertyEditor() { })} - {componentPinAttributes.isSplittable && ( - <> - - - - )} + {componentPinAttributes.bitWidthPolicy.type === "configurable" && + componentPinAttributes.bitWidthPolicy.isSplittable && ( + <> + + + + )}