Skip to content
6 changes: 6 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
*,
*::before,
*::after {
box-sizing: border-box;
}

:root {
font-family: sans-serif;
}
Expand Down
94 changes: 42 additions & 52 deletions src/pages/edit/Editor/components/NodePinPropertyEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -180,31 +169,32 @@ export function CCComponentEditorNodePinPropertyEditor() {
})}
</Stack>
<Stack direction="row" gap={1} sx={{ mt: 1 }}>
{componentPinAttributes.isSplittable && (
<>
<Button
type="button"
variant="outlined"
size="small"
onClick={() => {
setNewBitWidthList([...bitWidthList, 1]);
}}
>
Add
</Button>
<Button
type="button"
variant="outlined"
size="small"
disabled={bitWidthList.length <= 1}
onClick={() => {
setNewBitWidthList(bitWidthList.slice(0, -1));
}}
>
Remove
</Button>
</>
)}
{componentPinAttributes.bitWidthPolicy.type === "configurable" &&
componentPinAttributes.bitWidthPolicy.isSplittable && (
<>
<Button
type="button"
variant="outlined"
size="small"
onClick={() => {
setNewBitWidthList([...bitWidthList, 1]);
}}
>
Add
</Button>
<Button
type="button"
variant="outlined"
size="small"
disabled={bitWidthList.length <= 1}
onClick={() => {
setNewBitWidthList(bitWidthList.slice(0, -1));
}}
>
Remove
</Button>
</>
)}
<Button
type="submit"
variant="contained"
Expand Down
3 changes: 3 additions & 0 deletions src/pages/edit/Editor/components/ViewModeSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Edit, PlayArrow } from "@mui/icons-material";
import { Fab } from "@mui/material";
import { useCanSimulate } from "../../../../store/react/selectors";
import { useComponentEditorStore } from "../store";

export default function CCComponentEditorViewModeSwitcher() {
const componentEditorState = useComponentEditorStore()();
const canSimulate = useCanSimulate(componentEditorState.componentId);

return (
<Fab
Expand All @@ -15,6 +17,7 @@ export default function CCComponentEditorViewModeSwitcher() {
);
componentEditorState.setTimeStep(0);
}}
disabled={!canSimulate}
>
{componentEditorState.editorMode === "edit" ? <PlayArrow /> : <Edit />}
</Fab>
Expand Down
23 changes: 13 additions & 10 deletions src/pages/edit/Editor/renderer/ComponentPin/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand All @@ -30,24 +30,27 @@ 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)),
),
onClick:
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),
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/pages/edit/Editor/renderer/Connection/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
24 changes: 24 additions & 0 deletions src/pages/edit/Editor/renderer/Node/components/Const/geometry.ts
Original file line number Diff line number Diff line change
@@ -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(300, 200),
nodePinOffsetById: new Map<CCNodePinId, Vector2>([
[nullthrows(source.outputNodePinIds[0]), vector2.create(300, 100)],
]),
};
}
95 changes: 95 additions & 0 deletions src/pages/edit/Editor/renderer/Node/components/Const/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Stack, styled, ToggleButton, ToggleButtonGroup } from "@mui/material";
import { useState } from "react";
import type {
CCIntrinsicComponentConstSpec,
CCIntrinsicComponentConstSpecConfigViewMode as Mode,
} from "../../../../../../../store/intrinsics/types";
import { useStore } from "../../../../../../../store/react";
import type { CCComponentEditorRendererNodeRendererProps } from "../../types";
import { CCComponentEditorRendererNodeDefaultRenderer } from "../Default";
import { ccComponentEditorRendererNodeConstLayoutConstants as constants } from "./geometry";
import { formatConstData, parseConstData } from "./value";

const modes: Record<Mode, string> = {
binary: "Binary",
hex: "Hex",
"utf-8": "UTF-8",
};

const Editor = styled("textarea")(({ theme }) => ({
padding: theme.spacing(1),
flex: 1,
resize: "none",
borderStyle: "solid",
borderWidth: "1px",
borderRadius: theme.shape.borderRadius,
outline: "none",
fontFamily: "monospace",
wordBreak: "break-all",
}));

export function CCComponentEditorRendererNodeConstRenderer(
props: CCComponentEditorRendererNodeRendererProps,
) {
const { store } = useStore();
const config = props.node.config as CCIntrinsicComponentConstSpec["config"];

const [text, setText] = useState(() =>
formatConstData(config.data, config.mode),
);
const [isInvalid, setIsInvalid] = useState(false);

const updateConfig = (newConfig: CCIntrinsicComponentConstSpec["config"]) =>
store.nodes.update(props.node.id, { config: newConfig });

const changeMode = (mode: Mode) => {
updateConfig({ ...config, mode });
setText(formatConstData(config.data, mode));
setIsInvalid(false);
};

const changeText = (newText: string) => {
setText(newText);
const data = parseConstData(newText, config.mode);
setIsInvalid(!data);
if (data) updateConfig({ ...config, data });
};

return (
<>
<CCComponentEditorRendererNodeDefaultRenderer {...props} />
<foreignObject
x={constants.padding}
y={constants.padding}
width={props.geometry.rect.size.x - constants.padding * 2 - 30}
height={props.geometry.rect.size.y - constants.padding * 2}
onPointerDown={(e) => e.stopPropagation()}
>
<Stack sx={{ height: "100%", gap: 0.5 }}>
<ToggleButtonGroup
value={config.mode}
onChange={(_, value) => {
if (!Object.hasOwn(modes, value)) return;
changeMode(value as Mode);
}}
exclusive
size="small"
sx={{ width: "100%" }}
>
{Object.entries(modes).map(([mode, label]) => (
<ToggleButton key={mode} value={mode} sx={{ flex: 1, p: 0.25 }}>
{label}
</ToggleButton>
))}
</ToggleButtonGroup>
<Editor
value={text}
onChange={(e) => changeText(e.target.value)}
spellCheck={false}
sx={{ borderColor: isInvalid ? "error.main" : "divider" }}
/>
</Stack>
</foreignObject>
</>
);
}
68 changes: 68 additions & 0 deletions src/pages/edit/Editor/renderer/Node/components/Const/value.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { chunk } from "es-toolkit";
import type { CCIntrinsicComponentConstSpecConfigViewMode as Mode } from "../../../../../../../store/intrinsics/types";
import type { SimulationValue } from "../../../../../../../store/simulation";

const bitsPerHexDigit = 4;
const bitsPerByte = 8;

/** Bits are ordered from the most significant one, as in `SimulationValue`. */
function toBits(value: number, bitCount: number): boolean[] {
return Array.from(
{ length: bitCount },
(_, i) => ((value >> (bitCount - 1 - i)) & 1) === 1,
);
}

function fromBits(bits: boolean[]): number {
return bits.reduce((value, bit) => value * 2 + (bit ? 1 : 0), 0);
}

/** Pads the head with zeros so that the length becomes a multiple of `unit`. */
function padBits(bits: SimulationValue, unit: number): boolean[] {
const padding = (unit - (bits.length % unit)) % unit;
return [...Array<boolean>(padding).fill(false), ...bits];
}

export function formatConstData(data: SimulationValue, mode: Mode): string {
switch (mode) {
case "binary":
return data.map((bit) => (bit ? "1" : "0")).join("");
case "hex":
return chunk(padBits(data, bitsPerHexDigit), bitsPerHexDigit)
.map((digit) => fromBits(digit).toString(16))
.join("");
case "utf-8": {
const bytes = Uint8Array.from(
chunk(padBits(data, bitsPerByte), bitsPerByte),
fromBits,
);
return new TextDecoder().decode(bytes);
}
}
}

/** Returns null if `text` is not a valid representation in `mode`. */
export function parseConstData(
text: string,
mode: Mode,
): SimulationValue | null {
switch (mode) {
case "binary": {
const digits = text.replace(/\s/g, "");
if (!/^[01]+$/.test(digits)) return null;
return [...digits].map((digit) => digit === "1");
}
case "hex": {
const digits = text.replace(/\s/g, "").replace(/^0x/i, "");
if (!/^[0-9a-f]+$/i.test(digits)) return null;
return [...digits].flatMap((digit) =>
toBits(Number.parseInt(digit, 16), bitsPerHexDigit),
);
}
case "utf-8": {
const bytes = new TextEncoder().encode(text);
if (bytes.length === 0) return null;
return [...bytes].flatMap((byte) => toBits(byte, bitsPerByte));
}
}
}
Loading
Loading