diff --git a/src/pages/edit/Editor/components/NodePinPropertyEditor.tsx b/src/pages/edit/Editor/components/NodePinPropertyEditor.tsx index d1deb88..57db9ed 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; @@ -180,31 +169,32 @@ export function CCComponentEditorNodePinPropertyEditor() { })} - {componentPinAttributes.isSplittable && ( - <> - - - - )} + {componentPinAttributes.bitWidthPolicy.type === "configurable" && + componentPinAttributes.bitWidthPolicy.isSplittable && ( + <> + + + + )} + + + + + + ); +} 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..e4eb639 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; +export const ccComponentEditorRendererNodeDisplayLayoutConstants = { + padding: 8, + gridSize: 12, + gridSizeDisplayWidth: 60, +}; -export const ccComponentRendererNodeDisplayGeometryCalculator: CCComponentEditorRendererNodeGeometryCalculator = - (source: CCComponentEditorRendererNodeGeometrySource) => { - const size: Vector2 = { - x: width, - y: height, - }; +export function ccComponentRendererNodeDisplayLayoutCalculator( + source: CCComponentEditorRendererNodeLayoutSource, +): CCComponentEditorRendererNodeLayout { + const { padding, gridSize, gridSizeDisplayWidth } = + ccComponentEditorRendererNodeDisplayLayoutConstants; + const config = source.config as CCIntrinsicComponentDisplaySpec["config"]; - 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, - ), - ), - }; + const size = { + x: gridSizeDisplayWidth + gridSize * config.resolution.x + padding * 2, + y: gridSize * config.resolution.y + padding * 2, }; + + return { + 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 a80634f..bc23f89 100644 --- a/src/pages/edit/Editor/renderer/Node/components/Display/index.tsx +++ b/src/pages/edit/Editor/renderer/Node/components/Display/index.tsx @@ -5,12 +5,19 @@ 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, ) { 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) @@ -23,38 +30,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,15 +59,12 @@ export function CCComponentEditorRendererNodeDisplayRenderer( .map((x) => ( -> = { +const specialLayoutCalculators: { + [key in CCIntrinsicComponentType]?: ( + source: CCComponentEditorRendererNodeLayoutSource, + ) => CCComponentEditorRendererNodeLayout; +} = { [ccIntrinsicComponentTypes.DISPLAY]: - ccComponentRendererNodeDisplayGeometryCalculator, + ccComponentRendererNodeDisplayLayoutCalculator, + [ccIntrinsicComponentTypes.CONST]: + ccComponentRendererNodeConstLayoutCalculator, }; -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 +53,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..9a09029 100644 --- a/src/pages/edit/Editor/renderer/Node/index.tsx +++ b/src/pages/edit/Editor/renderer/Node/index.tsx @@ -10,9 +10,13 @@ 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 getCCComponentEditorRendererNodeGeometry from "./geometry"; +import { + ccComponentEditorRendererLayoutToGeometry, + getCCComponentEditorRendererNodeLayout, +} from "./geometry"; import type { CCComponentEditorRendererNodeRendererNodeState, CCComponentEditorRendererNodeRendererProps, @@ -26,6 +30,7 @@ const specialRenderers: Partial< > = { [ccIntrinsicComponentTypes.DISPLAY]: CCComponentEditorRendererNodeDisplayRenderer, + [ccIntrinsicComponentTypes.CONST]: CCComponentEditorRendererNodeConstRenderer, }; export type CCComponentEditorRendererNodeProps = { @@ -44,7 +49,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 +95,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..f6ee5f1 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,37 +48,46 @@ 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) => { return { ...state, inputValues: new Map(state.inputValues).set( - JSON.stringify(inputValueKey), + serializeInputValueKey(inputValueKey), value, ), }; @@ -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 }), ); } } @@ -191,11 +204,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/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/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/componentPin.ts b/src/store/componentPin.ts index 0376f4d..2171493 100644 --- a/src/store/componentPin.ts +++ b/src/store/componentPin.ts @@ -5,7 +5,7 @@ import type { Opaque } from "type-fest"; import type CCStore from "."; import type { CCComponentId } from "./component"; import { IntrinsicComponentDefinition } from "./intrinsics/base"; -import { aggregate, decompose, input, output } from "./intrinsics/definitions"; +import { input, output } from "./intrinsics/definitions"; import type { CCNodePinId } from "./nodePin"; export type CCComponentPin = { @@ -36,11 +36,13 @@ export type CCNodePinBitWidthStatus = /** * The bit width status of a component pin definition. * - `isFixed: false, fixMode: "automatic"` — the bit width is not yet determined and will be inferred automatically from connections. - * - `isFixed: false, fixMode: "manual"` — the bit width is not yet determined and must be specified manually by the user. - * - `isFixed: true` — the bit width is known and available as `bitWidth`. + * - `isFixed: false, fixMode: "nodeDependent"` — the bit width differs per node instance + * (it comes from the config of the node and/or the bit widths manually specified for its + * pins), so it can only be resolved by {@link CCNodePinStore.getNodePinBitWidthStatus}. + * - `isFixed: true` — the bit width is determined by the component definition itself and available as `bitWidth`. */ export type CCComponentPinBitWidthStatus = - | { isFixed: false; fixMode: "automatic" | "manual" } + | { isFixed: false; fixMode: "automatic" | "nodeDependent" } | { isFixed: true; bitWidth: number }; export type CCComponentPinStoreEvents = { @@ -205,7 +207,9 @@ export class CCComponentPinStore extends EventEmitter } /** - * Get the bit width status of a component pin + * Get how the bit width of a component pin is determined. It is resolved only as far + * as the component definition allows; use {@link CCNodePinStore.getNodePinBitWidthStatus} + * to get the width of a concrete node pin. * @param pinId id of pin * @returns bit width status of the pin */ @@ -219,33 +223,19 @@ export class CCComponentPinStore extends EventEmitter const intrinsicPinAttributes = IntrinsicComponentDefinition.getPinAttributesByPinId(pin.id); if (intrinsicPinAttributes) { - // TODO: This is a temporary workaround to allow the bit width of aggregate and decompose pins to be calculated outside of the normal inference process. - // We should eventually refactor the bit width inference process to handle these cases more elegantly. - if (pin.id === aggregate.outputPin.Out.id) - return { isFixed: false, fixMode: "manual" }; - if (pin.id === decompose.inputPin.In.id) - return { isFixed: false, fixMode: "manual" }; - - if (intrinsicPinAttributes.bitWidthPolicy.type === "inferred") - return { isFixed: false, fixMode: "automatic" }; - if (intrinsicPinAttributes.bitWidthPolicy.type === "configurable") - return { isFixed: false, fixMode: "manual" }; - if (intrinsicPinAttributes.bitWidthPolicy.type === "fixed") { - const definition = nullthrows( - IntrinsicComponentDefinition.getByComponentId(pin.componentId), - `Intrinsic component definition not found for component ID: ${pin.componentId}`, - ); - return { - isFixed: true, - bitWidth: intrinsicPinAttributes.bitWidthPolicy.calculateBitWidth( - definition.initialConfig, - {}, - ), - }; + switch (intrinsicPinAttributes.bitWidthPolicy.type) { + case "inferred": + return { isFixed: false, fixMode: "automatic" }; + // Both policies need the node the pin belongs to (its config and the bit widths + // manually specified for its pins), which is unknown at the component pin level. + case "configurable": + case "calculated": + return { isFixed: false, fixMode: "nodeDependent" }; + default: + throw new Error( + `Unknown bit width policy: ${intrinsicPinAttributes.bitWidthPolicy satisfies never}`, + ); } - throw new Error( - `Unknown bit width policy: ${intrinsicPinAttributes.bitWidthPolicy satisfies never}`, - ); } // User-defined components diff --git a/src/store/connection.ts b/src/store/connection.ts index 938e59a..3290c49 100644 --- a/src/store/connection.ts +++ b/src/store/connection.ts @@ -61,6 +61,55 @@ export class CCConnectionStore extends EventEmitter { this.unregister(connections.map((connection) => connection.id)); } }); + // A config change can change the bit width of a pin (e.g. the resolution of a + // display), which invalidates the connections that were valid when they were made. + // CCStore mounts CCNodePinStore first, so its bit width cache is already dropped. + this.#store.nodes.on("didUpdateConfig", (node) => { + // TODO: Dropping a connection can in turn invalidate another one. Resolving that + // needs a repeated sweep, which the re-validation on `didRegister` below lacks too. + const invalidatedConnections = this.getMany().filter( + (connection) => + connection.parentComponentId === node.parentComponentId && + !this.#store.nodePins.hasCompatibleBitWidths( + connection.from, + connection.to, + ), + ); + if (invalidatedConnections.length > 0) { + this.unregister( + invalidatedConnections.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, + }), + ); + } + }); + } + } + } + }); } /** diff --git a/src/store/intrinsics/base.ts b/src/store/intrinsics/base.ts index 348f45f..d676c26 100644 --- a/src/store/intrinsics/base.ts +++ b/src/store/intrinsics/base.ts @@ -29,12 +29,20 @@ export type Context = { componentId: CCComponentId; }; +/** + * How the bit width of an intrinsic component pin is determined. + * - `inferred` — inferred from the pins it is transitively connected to. + * - `calculated` — derived from the config of the node and the bit widths manually + * specified for its pins, so it can only be resolved for a concrete node. + * - `configurable` — specified manually by the user on each node pin. `isSplittable` + * tells whether the pin may be split into multiple node pins. + */ type IntrinsicComponentPinBitWidthPolicy< Spec extends CCIntrinsicComponentSpec, > = | { type: "inferred" } | { - type: "fixed"; + type: "calculated"; calculateBitWidth: ( config: Spec["config"], manualBitWidths: Partial>, @@ -45,20 +53,31 @@ type IntrinsicComponentPinBitWidthPolicy< type IntrinsicComponentPinAttributes = { name: string; bitWidthPolicy: IntrinsicComponentPinBitWidthPolicy; - isBitWidthConfigurable?: boolean; - isSplittable?: boolean; }; + +/** + * The attributes of an intrinsic component pin, along with the `key` identifying it + * within its component definition (e.g. `In`, `Out`, `Pixels`). + */ +export type RegisteredIntrinsicComponentPinAttributes = + IntrinsicComponentPinAttributes & { key: string }; + +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 +90,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; @@ -101,41 +116,36 @@ export class IntrinsicComponentDefinition< IntrinsicComponentDefinition._byId.set(this.id, this); this.evaluate = props.evaluate; - this.inputPin = mapValues(props.in, (attributes) => { - const pin: CCComponentPin = { - id: this._generateId() as CCComponentPinId, - componentId: this.id, - type: "input", - implementation: null, - order: this._lastLocalIndex++, - name: attributes.name, - }; - IntrinsicComponentDefinition._pinAttributesByPinId.set( - pin.id, - attributes as IntrinsicComponentPinAttributes, - ); - this.allPins.push(pin); - return pin; - }); - this.outputPin = mapValues(props.out, (attributes) => { - const pin: CCComponentPin = { - id: this._generateId() as CCComponentPinId, - componentId: this.id, - type: "output", - implementation: null, - order: this._lastLocalIndex++, - name: attributes.name, - }; - IntrinsicComponentDefinition._pinAttributesByPinId.set( - pin.id, - attributes as IntrinsicComponentPinAttributes, - ); - this.allPins.push(pin); - return pin; - }); + this.inputPin = mapValues(props.in, (attributes, key) => + this._registerPin("input", key, attributes), + ); + this.outputPin = mapValues(props.out, (attributes, key) => + this._registerPin("output", key, attributes), + ); this.initialConfig = props.initialConfig; } + private _registerPin( + type: CCComponentPin["type"], + key: string, + attributes: IntrinsicComponentPinAttributes, + ): CCComponentPin { + const pin: CCComponentPin = { + id: this._generateId() as CCComponentPinId, + componentId: this.id, + type, + implementation: null, + order: this._lastLocalIndex++, + name: attributes.name, + }; + IntrinsicComponentDefinition._pinAttributesByPinId.set(pin.id, { + ...(attributes as IntrinsicComponentPinAttributes), + key, + }); + this.allPins.push(pin); + return pin; + } + private static _byId: Map = new Map(); static getByComponentId(componentId: CCComponentId) { @@ -144,8 +154,9 @@ export class IntrinsicComponentDefinition< private static _pinAttributesByPinId: Map< CCComponentPinId, - IntrinsicComponentPinAttributes + RegisteredIntrinsicComponentPinAttributes > = new Map(); + /** @returns the attributes of the pin, or null if it is not an intrinsic component pin */ static getPinAttributesByPinId(pinId: CCComponentPinId) { return ( IntrinsicComponentDefinition._pinAttributesByPinId.get(pinId) ?? null diff --git a/src/store/intrinsics/definitions.ts b/src/store/intrinsics/definitions.ts index 4b9d1d5..790c276 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, @@ -187,15 +188,13 @@ export const aggregate = In: { name: "In", bitWidthPolicy: { type: "configurable", isSplittable: true }, - isBitWidthConfigurable: true, - isSplittable: true, }, }, out: { Out: { name: "Out", bitWidthPolicy: { - type: "fixed", + type: "calculated", calculateBitWidth: (_, manualBitWidths) => manualBitWidths?.In?.reduce((sum, bitWidth) => sum + bitWidth, 0) ?? 1, @@ -229,7 +228,7 @@ export const decompose = In: { name: "In", bitWidthPolicy: { - type: "fixed", + type: "calculated", calculateBitWidth: (_, manualBitWidths) => manualBitWidths?.Out?.reduce( (sum, bitWidth) => sum + bitWidth, @@ -242,8 +241,6 @@ export const decompose = Out: { name: "Out", bitWidthPolicy: { type: "configurable", isSplittable: true }, - isBitWidthConfigurable: true, - isSplittable: true, }, }, initialConfig: null, @@ -275,14 +272,13 @@ export const broadcast = in: { In: { name: "In", - bitWidthPolicy: { type: "fixed", calculateBitWidth: () => 1 }, + bitWidthPolicy: { type: "calculated", calculateBitWidth: () => 1 }, }, }, out: { Out: { name: "Out", bitWidthPolicy: { type: "configurable", isSplittable: false }, - isBitWidthConfigurable: true, }, }, initialConfig: null, @@ -320,7 +316,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) @@ -342,7 +337,7 @@ export const display = Pixels: { name: "Pixels", bitWidthPolicy: { - type: "fixed", + type: "calculated", calculateBitWidth: (config) => config.resolution.x * config.resolution.y, }, @@ -353,6 +348,31 @@ export const display = evaluate: () => true, }); +export const const_ = + new IntrinsicComponentDefinition({ + type: ccIntrinsicComponentTypes.CONST, + name: "Const", + in: {}, + out: { + Out: { + name: "Out", + bitWidthPolicy: { + type: "calculated", + 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 +389,7 @@ export const definitions: { [ccIntrinsicComponentTypes.BROADCAST]: broadcast, [ccIntrinsicComponentTypes.FLIPFLOP]: flipflop, [ccIntrinsicComponentTypes.DISPLAY]: display, + [ccIntrinsicComponentTypes.CONST]: const_, [ccIntrinsicComponentTypes.TRUE]: true_, [ccIntrinsicComponentTypes.FALSE]: false_, }; @@ -376,13 +397,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/node.ts b/src/store/node.ts index 8135dd4..22efd1d 100644 --- a/src/store/node.ts +++ b/src/store/node.ts @@ -25,6 +25,11 @@ export type CCNodeStoreEvents = { willUnregister(node: CCNode): void; didUnregister(node: CCNode): void; didUpdate(node: CCNode): void; + /** + * Emitted in addition to `didUpdate` when the config of a node changed, so that + * listeners depending on the config are not woken up by frequent position updates. + */ + didUpdateConfig(node: CCNode): void; }; export const ccNodeStoreChangeEventTypes: (keyof CCNodeStoreEvents)[] = [ "didRegister", @@ -145,6 +150,9 @@ export class CCNodeStore extends EventEmitter { const existingNode = nullthrows(this.#nodes.get(id)); const newNode = { ...existingNode, ...value }; this.#nodes.set(id, newNode); + if ("config" in value && value.config !== existingNode.config) { + this.emit("didUpdateConfig", newNode); + } this.emit("didUpdate", newNode); } diff --git a/src/store/nodePin.test.ts b/src/store/nodePin.test.ts new file mode 100644 index 0000000..d723b2c --- /dev/null +++ b/src/store/nodePin.test.ts @@ -0,0 +1,284 @@ +import nullthrows from "nullthrows"; +import { describe, expect, it } from "vitest"; +import CCStore from "."; +import { type CCComponent, CCComponentStore } from "./component"; +import type { CCComponentPinId } from "./componentPin"; +import { CCConnectionStore } from "./connection"; +import type { IntrinsicComponentDefinition } from "./intrinsics/base"; +import * as intrinsics from "./intrinsics/definitions"; +import { type CCNodeId, CCNodeStore } from "./node"; +import { CCNodePinStore } from "./nodePin"; + +function createRootStore() { + const store = new CCStore(); + store.mount(); + const rootComponent = CCComponentStore.create({ name: "Root" }); + store.components.register(rootComponent); + return { store, rootComponent }; +} + +function registerNode( + store: CCStore, + rootComponent: CCComponent, + // biome-ignore lint/suspicious/noExplicitAny: the spec of the definition is irrelevant here + definition: IntrinsicComponentDefinition, +) { + const node = CCNodeStore.create({ + parentComponentId: rootComponent.id, + componentId: definition.component.id, + position: { x: 0, y: 0 }, + }); + store.nodes.register(node); + return node; +} + +function getNodePinBitWidth( + store: CCStore, + nodeId: CCNodeId, + componentPinId: CCComponentPinId, +) { + const nodePin = nullthrows( + store.nodePins + .getManyByNodeId(nodeId) + .find((pin) => pin.componentPinId === componentPinId), + ); + return store.nodePins.getNodePinBitWidthStatus(nodePin.id); +} + +describe("Node pin store", () => { + describe("getNodePinBitWidthStatus", () => { + it("should calculate the bit width of a display pin from the config of its node", () => { + const { store, rootComponent } = createRootStore(); + const displayNode = registerNode( + store, + rootComponent, + intrinsics.display, + ); + + expect( + getNodePinBitWidth( + store, + displayNode.id, + intrinsics.display.inputPin.Pixels.id, + ), + ).toEqual({ isFixed: true, bitWidth: 20 * 15 }); + + store.nodes.update(displayNode.id, { + config: { resolution: { x: 4, y: 3 } }, + }); + + expect( + getNodePinBitWidth( + store, + displayNode.id, + intrinsics.display.inputPin.Pixels.id, + ), + ).toEqual({ isFixed: true, bitWidth: 4 * 3 }); + }); + + it("should propagate the bit width of a display pin to the pins connected to it", () => { + const { store, rootComponent } = createRootStore(); + const displayNode = registerNode( + store, + rootComponent, + intrinsics.display, + ); + store.nodes.update(displayNode.id, { + config: { resolution: { x: 4, y: 3 } }, + }); + const inputNode = registerNode(store, rootComponent, intrinsics.input); + const inputNodePin = nullthrows( + store.nodePins + .getManyByNodeId(inputNode.id) + .find( + (pin) => pin.componentPinId === intrinsics.input.outputPin.Out.id, + ), + ); + const pixelsNodePin = nullthrows( + store.nodePins + .getManyByNodeId(displayNode.id) + .find( + (pin) => + pin.componentPinId === intrinsics.display.inputPin.Pixels.id, + ), + ); + store.connections.register( + CCConnectionStore.create({ + parentComponentId: rootComponent.id, + from: inputNodePin.id, + to: pixelsNodePin.id, + bentPortion: 0.5, + }), + ); + + expect(store.nodePins.getNodePinBitWidthStatus(inputNodePin.id)).toEqual({ + isFixed: true, + bitWidth: 4 * 3, + }); + }); + it("should sum up the manual bit widths of the input pins of an aggregate node", () => { + const { store, rootComponent } = createRootStore(); + const node = registerNode(store, rootComponent, intrinsics.aggregate); + const firstInputNodePin = nullthrows( + store.nodePins + .getManyByNodeId(node.id) + .find( + (pin) => pin.componentPinId === intrinsics.aggregate.inputPin.In.id, + ), + ); + store.nodePins.update(firstInputNodePin.id, { manualBitWidth: 5 }); + store.nodePins.register( + CCNodePinStore.create({ + nodeId: node.id, + componentPinId: intrinsics.aggregate.inputPin.In.id, + order: firstInputNodePin.order + 1, + manualBitWidth: 3, + }), + ); + + expect( + store.nodePins.getNodePinBitWidthStatus(firstInputNodePin.id), + ).toEqual({ isFixed: true, bitWidth: 5 }); + expect( + getNodePinBitWidth( + store, + node.id, + intrinsics.aggregate.outputPin.Out.id, + ), + ).toEqual({ isFixed: true, bitWidth: 8 }); + }); + + it("should sum up the manual bit widths of the output pins of a decompose node", () => { + const { store, rootComponent } = createRootStore(); + const node = registerNode(store, rootComponent, intrinsics.decompose); + const firstOutputNodePin = nullthrows( + store.nodePins + .getManyByNodeId(node.id) + .find( + (pin) => + pin.componentPinId === intrinsics.decompose.outputPin.Out.id, + ), + ); + store.nodePins.update(firstOutputNodePin.id, { manualBitWidth: 2 }); + store.nodePins.register( + CCNodePinStore.create({ + nodeId: node.id, + componentPinId: intrinsics.decompose.outputPin.Out.id, + order: firstOutputNodePin.order + 1, + manualBitWidth: 6, + }), + ); + + expect( + getNodePinBitWidth(store, node.id, intrinsics.decompose.inputPin.In.id), + ).toEqual({ isFixed: true, bitWidth: 8 }); + }); + + it("should reject a node pin of a configurable component pin without a manual bit width", () => { + const { store, rootComponent } = createRootStore(); + const node = registerNode(store, rootComponent, intrinsics.broadcast); + const outputNodePin = nullthrows( + store.nodePins + .getManyByNodeId(node.id) + .find( + (pin) => + pin.componentPinId === intrinsics.broadcast.outputPin.Out.id, + ), + ); + store.nodePins.update(outputNodePin.id, { manualBitWidth: null }); + + expect(() => + store.nodePins.getNodePinBitWidthStatus(outputNodePin.id), + ).toThrow(/must have a positive manual bit width/); + }); + + it("should broadcast a single bit to the manually specified bit width", () => { + const { store, rootComponent } = createRootStore(); + const node = registerNode(store, rootComponent, intrinsics.broadcast); + const outputNodePin = nullthrows( + store.nodePins + .getManyByNodeId(node.id) + .find( + (pin) => + pin.componentPinId === intrinsics.broadcast.outputPin.Out.id, + ), + ); + store.nodePins.update(outputNodePin.id, { manualBitWidth: 7 }); + + expect( + getNodePinBitWidth(store, node.id, intrinsics.broadcast.inputPin.In.id), + ).toEqual({ isFixed: true, bitWidth: 1 }); + expect(store.nodePins.getNodePinBitWidthStatus(outputNodePin.id)).toEqual( + { + isFixed: true, + bitWidth: 7, + }, + ); + }); + }); + + describe("bit width consistency of connections", () => { + function connectBroadcastToDisplay(bitWidth: number) { + const { store, rootComponent } = createRootStore(); + const displayNode = registerNode( + store, + rootComponent, + intrinsics.display, + ); + const broadcastNode = registerNode( + store, + rootComponent, + intrinsics.broadcast, + ); + const pixelsNodePin = nullthrows( + store.nodePins + .getManyByNodeId(displayNode.id) + .find( + (pin) => + pin.componentPinId === intrinsics.display.inputPin.Pixels.id, + ), + ); + const broadcastOutNodePin = nullthrows( + store.nodePins + .getManyByNodeId(broadcastNode.id) + .find( + (pin) => + pin.componentPinId === intrinsics.broadcast.outputPin.Out.id, + ), + ); + store.nodePins.update(broadcastOutNodePin.id, { + manualBitWidth: bitWidth, + }); + store.connections.register( + CCConnectionStore.create({ + parentComponentId: rootComponent.id, + from: broadcastOutNodePin.id, + to: pixelsNodePin.id, + bentPortion: 0.5, + }), + ); + expect(store.connections.getMany()).toHaveLength(1); + return { store, displayNode }; + } + + it("should drop the connections that a display config change makes inconsistent", () => { + const { store, displayNode } = connectBroadcastToDisplay(20 * 15); + + store.nodes.update(displayNode.id, { + config: { resolution: { x: 4, y: 3 } }, + }); + + expect(store.connections.getMany()).toHaveLength(0); + }); + + it("should keep the connections that stay consistent across a display config change", () => { + const { store, displayNode } = connectBroadcastToDisplay(20 * 15); + + store.nodes.update(displayNode.id, { + config: { resolution: { x: 15, y: 20 } }, + }); + + expect(store.connections.getMany()).toHaveLength(1); + }); + }); +}); diff --git a/src/store/nodePin.ts b/src/store/nodePin.ts index 7a1d91a..f37a7ce 100644 --- a/src/store/nodePin.ts +++ b/src/store/nodePin.ts @@ -5,7 +5,6 @@ import type { Opaque } from "type-fest"; import type CCStore from "."; import type { CCComponentPinId, CCNodePinBitWidthStatus } from "./componentPin"; import { IntrinsicComponentDefinition } from "./intrinsics/base"; -import { aggregate, broadcast, decompose } from "./intrinsics/definitions"; import type { CCNodeId } from "./node"; export type CCNodePinId = Opaque; @@ -37,6 +36,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 +59,12 @@ export class CCNodePinStore extends EventEmitter { } mount() { + this.#store.connections.on("didRegister", () => this.#clearBitWidthCache()); + this.#store.connections.on("didUnregister", () => + this.#clearBitWidthCache(), + ); + // A calculated bit width may be derived from the config of its node + this.#store.nodes.on("didUpdateConfig", () => this.#clearBitWidthCache()); this.#store.nodes.on("didRegister", (node) => { const componentPins = this.#store.componentPins.getManyByComponentId( node.componentId, @@ -105,6 +116,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 +131,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 +140,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); } @@ -174,6 +188,38 @@ export class CCNodePinStore extends EventEmitter { ); } + static #requireManualBitWidth(nodePin: CCNodePin): number { + invariant( + nodePin.manualBitWidth !== null && nodePin.manualBitWidth > 0, + `Node pin ${nodePin.id} of a configurable component pin must have a positive manual bit width, but got ${nodePin.manualBitWidth}`, + ); + return nodePin.manualBitWidth; + } + + /** + * Collect the manually specified bit widths of the pins of a single node, grouped by + * the pin key of its intrinsic component definition (e.g. `In`, `Out`) and ordered by + * the order of the node pins. + * @param nodePins all pins of one node + * @returns the bit widths of the pins of configurable component pins + */ + static #collectManualBitWidths( + nodePins: CCNodePin[], + ): Partial> { + const manualBitWidths: Partial> = {}; + for (const nodePin of nodePins.toSorted((a, b) => a.order - b.order)) { + const attributes = IntrinsicComponentDefinition.getPinAttributesByPinId( + nodePin.componentPinId, + ); + // Only a configurable pin carries a manual bit width; the others are null + if (attributes?.bitWidthPolicy.type !== "configurable") continue; + const bitWidths = manualBitWidths[attributes.key] ?? []; + bitWidths.push(CCNodePinStore.#requireManualBitWidth(nodePin)); + manualBitWidths[attributes.key] = bitWidths; + } + return manualBitWidths; + } + /** * Get the bit width status of a node pin * @param pinId id of pin @@ -181,101 +227,79 @@ 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, ): CCNodePinBitWidthStatus => { - const { - nodeId: targetNodeId, - componentPinId: targetComponentPinId, - manualBitWidth, - } = nullthrows(this.get(targetNodePinId)); + const targetNodePin = nullthrows(this.get(targetNodePinId)); + const { nodeId: targetNodeId, componentPinId: targetComponentPinId } = + targetNodePin; seen.add(targetNodeId); const targetNode = nullthrows(this.#store.nodes.get(targetNodeId)); const targetNodePins = this.getManyByNodeId(targetNode.id); - const givenComponentPinBitWidthStatus = - this.#store.componentPins.getComponentPinBitWidthStatus( + const attributes = + IntrinsicComponentDefinition.getPinAttributesByPinId( targetComponentPinId, ); - if (givenComponentPinBitWidthStatus.isFixed) { - return givenComponentPinBitWidthStatus; - } - if (givenComponentPinBitWidthStatus.fixMode === "manual") { - const componentPin = - this.#store.componentPins.get(targetComponentPinId); - invariant(componentPin); - switch (componentPin.id) { - case nullthrows(aggregate.inputPin.In.id): - case nullthrows(broadcast.outputPin.Out.id): - case nullthrows(decompose.outputPin.Out.id): - invariant( - manualBitWidth, - "aggregate inputPin, broadcast outputPin, or decompose outputPin must have a manual bit width", - ); + if (attributes) { + switch (attributes.bitWidthPolicy.type) { + case "configurable": return { isFixed: true, - bitWidth: manualBitWidth, + bitWidth: CCNodePinStore.#requireManualBitWidth(targetNodePin), }; - case nullthrows(aggregate.outputPin.Out.id): { - const bitWidth = targetNodePins - .filter((pin) => { - const componentPin = this.#store.componentPins.get( - pin.componentPinId, - ); - invariant(componentPin); - return componentPin.type === "input"; - }) - .reduce((acc, pin) => { - invariant(pin.manualBitWidth); - return acc + pin.manualBitWidth; - }, 0); + case "calculated": return { isFixed: true, - bitWidth, + bitWidth: attributes.bitWidthPolicy.calculateBitWidth( + targetNode.config, + CCNodePinStore.#collectManualBitWidths(targetNodePins), + ), }; - } - case nullthrows(decompose.inputPin.In.id): { - const bitWidth = targetNodePins - .filter((pin) => { - const componentPin = this.#store.componentPins.get( - pin.componentPinId, - ); - invariant(componentPin); - return componentPin.type === "output"; - }) - .reduce((acc, pin) => { - invariant(pin.manualBitWidth); - return acc + pin.manualBitWidth; - }, 0); - return { - isFixed: true, - bitWidth, - }; - } + case "inferred": + // Resolved from the pins it is connected to, below + break; default: throw new Error( - `Bit width status of ${componentPin.id} is undecidable`, + `Unknown bit width policy: ${attributes.bitWidthPolicy satisfies never}`, ); } + } else { + // A user defined component pin is fixed by the implementation of its component + const componentPinBitWidthStatus = + this.#store.componentPins.getComponentPinBitWidthStatus( + targetComponentPinId, + ); + if (componentPinBitWidthStatus.isFixed) { + return componentPinBitWidthStatus; + } } - for (const targetNodePin of targetNodePins) { - const targetComponentPinBitWidthStatus = + // The bit width of an inferred pin is shared with the other inferred pins of its + // node, so any of them may be the one that is connected to a pin of a known width. + for (const siblingNodePin of targetNodePins) { + const siblingComponentPinBitWidthStatus = this.#store.componentPins.getComponentPinBitWidthStatus( - targetNodePin.componentPinId, + siblingNodePin.componentPinId, ); - if (targetComponentPinBitWidthStatus.isFixed) { + if (siblingComponentPinBitWidthStatus.isFixed) { continue; } - if (targetComponentPinBitWidthStatus.fixMode === "manual") { - throw new Error("unreachable"); + if (siblingComponentPinBitWidthStatus.fixMode === "nodeDependent") { + // The bit width of a node dependent pin never propagates to its sibling + // pins, so no intrinsic component mixes it with inferred pins. + throw new Error( + `Component pin ${siblingNodePin.componentPinId} must not mix a node dependent bit width with inferred sibling pins`, + ); } const connections = nullthrows( - this.#store.connections.getConnectionsByNodePinId(targetNodePin.id), + this.#store.connections.getConnectionsByNodePinId(siblingNodePin.id), ); for (const connection of connections) { const componentPin = nullthrows( - this.#store.componentPins.get(targetNodePin.componentPinId), + this.#store.componentPins.get(siblingNodePin.componentPinId), ); const connectedNodePinId = componentPin.type === "input" ? connection.from : connection.to; @@ -292,9 +316,11 @@ export class CCNodePinStore extends EventEmitter { } } } - return givenComponentPinBitWidthStatus; + return { isFixed: false }; }; - return traverseNodePinBitWidthStatus(nodePinId, new Set()); + const result = traverseNodePinBitWidthStatus(nodePinId, new Set()); + this.#bitWidthCache.set(nodePinId, result); + return result; } isMarkedAsDeleted(id: CCNodePinId) { @@ -357,17 +383,29 @@ export class CCNodePinStore extends EventEmitter { console.warn(`Input pin already has a connection: ${bNodePin.id}`); return false; } - const aBitWidthStatus = this.getNodePinBitWidthStatus(a); - const bBitWidthStatus = this.getNodePinBitWidthStatus(b); - if (aBitWidthStatus.isFixed && bBitWidthStatus.isFixed) { + if (!this.hasCompatibleBitWidths(a, b)) { console.warn( - `Cannot connect pins with fixed bit width: ${aNodePin.id} and ${bNodePin.id}`, + `Cannot connect pins with conflicting bit widths: ${aNodePin.id} and ${bNodePin.id}`, ); - return aBitWidthStatus.bitWidth === bBitWidthStatus.bitWidth; + return false; } return true; } + /** + * Check whether two node pins can carry the same value. A pin whose bit width is not + * fixed yet adapts to the pin it is connected to, so it is compatible with any pin. + * @param a id of a pin + * @param b id of the other pin + * @returns whether the bit widths of the two pins do not conflict + */ + hasCompatibleBitWidths(a: CCNodePinId, b: CCNodePinId): boolean { + const aBitWidthStatus = this.getNodePinBitWidthStatus(a); + const bBitWidthStatus = this.getNodePinBitWidthStatus(b); + if (!aBitWidthStatus.isFixed || !bBitWidthStatus.isFixed) return true; + return aBitWidthStatus.bitWidth === bBitWidthStatus.bitWidth; + } + /** * Create a new pin * @param partialPin pin without `id` @@ -385,7 +423,7 @@ export class CCNodePinStore extends EventEmitter { id: crypto.randomUUID() as CCNodePinId, manualBitWidth: partialPin.manualBitWidth ?? - (attributes?.isBitWidthConfigurable ? 1 : null), + (attributes?.bitWidthPolicy.type === "configurable" ? 1 : null), }; } 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); +} 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(