From b062d65b61310b84ed82811d3f2d97d724e5880c Mon Sep 17 00:00:00 2001 From: taka231 Date: Sat, 14 Nov 2026 10:19:43 +0900 Subject: [PATCH 1/2] Add automatic simulation playback with configurable speed --- .../edit/Editor/components/FrameControl.tsx | 171 ++++++++++++++---- .../edit/Editor/store/slices/core/index.ts | 42 +++-- .../edit/Editor/store/slices/core/types.ts | 18 ++ 3 files changed, 180 insertions(+), 51 deletions(-) diff --git a/src/pages/edit/Editor/components/FrameControl.tsx b/src/pages/edit/Editor/components/FrameControl.tsx index d44a059..b3a7858 100644 --- a/src/pages/edit/Editor/components/FrameControl.tsx +++ b/src/pages/edit/Editor/components/FrameControl.tsx @@ -1,59 +1,150 @@ import { ChevronLeft, ChevronRight } from "@mui/icons-material"; -import { IconButton, Paper } from "@mui/material"; +import { Box, IconButton, Paper, TextField, ToggleButton } from "@mui/material"; +import { useState } from "react"; +import { useInterval } from "react-use"; import { useComponentEditorStore } from "../store"; +import { + isValidPlaybackSpeed, + MAX_PLAYBACK_SPEED, + MIN_PLAYBACK_SPEED, +} from "../store/slices/core/types"; export default function CCComponentEditorFrameControl() { - const componentEditorState = useComponentEditorStore()(); + const componentEditorStore = useComponentEditorStore(); + const componentEditorState = componentEditorStore(); + const [speedInput, setSpeedInput] = useState( + String(componentEditorState.playbackSpeed), + ); + const isAutomatic = componentEditorState.playbackMode === "auto"; + const isSpeedValid = isValidPlaybackSpeed(Number(speedInput)); + + useInterval( + () => { + const state = componentEditorStore.getState(); + if (state.editorMode === "play" && state.playbackMode === "auto") { + state.setTimeStep(state.timeStep + 1); + } + }, + componentEditorState.editorMode === "play" && isAutomatic + ? 1000 / componentEditorState.playbackSpeed + : null, + ); if (componentEditorState.editorMode === "edit") return null; return ( - - { - componentEditorState.setTimeStep(componentEditorState.timeStep - 1); + - - - { - const value = Number.parseInt(e.target.value, 10); - if (!Number.isNaN(value) && value > 0) { - componentEditorState.setTimeStep(value - 1); + { + componentEditorState.setPlaybackMode( + isAutomatic ? "manual" : "auto", + ); + }} + > + Auto + + + { + componentEditorState.setTimeStep( + componentEditorState.timeStep - 1, + ); + }} + > + + + { + const value = Number(e.target.value); + if (Number.isSafeInteger(value) && value > 0) { + componentEditorState.setTimeStep(value - 1); + } + }} + /> + { + componentEditorState.setTimeStep( + componentEditorState.timeStep + 1, + ); + }} + > + + + + - { - componentEditorState.setTimeStep(componentEditorState.timeStep + 1); - }} - > - - - + slotProps={{ + htmlInput: { + min: MIN_PLAYBACK_SPEED, + max: MAX_PLAYBACK_SPEED, + step: "any", + }, + }} + onChange={(e) => { + setSpeedInput(e.target.value); + componentEditorState.setPlaybackSpeed(Number(e.target.value)); + }} + onBlur={() => { + setSpeedInput( + String(componentEditorStore.getState().playbackSpeed), + ); + }} + /> + + ); } diff --git a/src/pages/edit/Editor/store/slices/core/index.ts b/src/pages/edit/Editor/store/slices/core/index.ts index f6ee5f1..c127bf5 100644 --- a/src/pages/edit/Editor/store/slices/core/index.ts +++ b/src/pages/edit/Editor/store/slices/core/index.ts @@ -14,6 +14,7 @@ import type { ComponentEditorSliceCreator } from "../../types"; import { type EditorStoreCoreSlice, type InputValueKey, + isValidPlaybackSpeed, serializeInputValueKey, } from "./types"; @@ -35,6 +36,8 @@ export const createComponentEditorStoreCoreSlice: ComponentEditorSliceCreator< return { editorMode: "edit", timeStep: 0, + playbackMode: "manual", + playbackSpeed: 1, selectedNodeIds: new Set(), rangeSelect: null, selectedConnectionIds: new Set(), @@ -54,17 +57,22 @@ export const createComponentEditorStoreCoreSlice: ComponentEditorSliceCreator< ); 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; + // Inherit the latest earlier input without updating the store during simulation. + const prefix = `${inputValueKey.componentPinId}:`; + let latestTimeStep = -1; + let previousValue: SimulationValue | undefined; + for (const [key, value] of get().inputValues) { + if (!key.startsWith(prefix)) continue; + const timeStep = Number(key.slice(prefix.length)); + if ( + timeStep < inputValueKey.timeStep && + timeStep > latestTimeStep + ) { + latestTimeStep = timeStep; + previousValue = value; + } } + if (previousValue) return previousValue; // If not found, initialize the value based on the bit width of the pin const componentPin = nullthrows( @@ -97,11 +105,23 @@ export const createComponentEditorStoreCoreSlice: ComponentEditorSliceCreator< set((state) => ({ ...state, rangeSelect })); }, setEditorMode(mode) { - set((state) => ({ ...state, editorMode: mode })); + set((state) => ({ + ...state, + editorMode: mode, + playbackMode: "manual", + })); }, setTimeStep(timeStep: number) { set((state) => ({ ...state, timeStep })); }, + setPlaybackMode(playbackMode) { + if (get().editorMode !== "play") return; + set((state) => ({ ...state, playbackMode })); + }, + setPlaybackSpeed(playbackSpeed) { + if (!isValidPlaybackSpeed(playbackSpeed)) return; + set((state) => ({ ...state, playbackSpeed })); + }, selectNode(ids: CCNodeId[], exclusive: boolean) { set((state) => ({ ...state, diff --git a/src/pages/edit/Editor/store/slices/core/types.ts b/src/pages/edit/Editor/store/slices/core/types.ts index 5f88bb7..c9863a1 100644 --- a/src/pages/edit/Editor/store/slices/core/types.ts +++ b/src/pages/edit/Editor/store/slices/core/types.ts @@ -9,6 +9,19 @@ export type EditorMode = EditorModeEdit | EditorModePlay; export type EditorModeEdit = "edit"; export type EditorModePlay = "play"; +export type PlaybackMode = "manual" | "auto"; + +export const MIN_PLAYBACK_SPEED = 0.1; +export const MAX_PLAYBACK_SPEED = 60; + +export function isValidPlaybackSpeed(speed: number): boolean { + return ( + Number.isFinite(speed) && + speed >= MIN_PLAYBACK_SPEED && + speed <= MAX_PLAYBACK_SPEED + ); +} + export type RangeSelect = { start: Vector2; end: Vector2 } | null; export type TimeStep = number; @@ -29,6 +42,9 @@ export type NodePinPropertyEditorTarget = { export type EditorStoreCoreSlice = { editorMode: EditorMode; timeStep: number; + playbackMode: PlaybackMode; + /** Simulation steps per second. */ + playbackSpeed: number; selectedNodeIds: Set; rangeSelect: RangeSelect; setRangeSelect(rangeSelect: RangeSelect): void; @@ -42,6 +58,8 @@ export type EditorStoreCoreSlice = { setInputValue(inputValueKey: InputValueKey, value: SimulationValue): void; setEditorMode(mode: EditorMode): void; setTimeStep(timeStep: number): void; + setPlaybackMode(mode: PlaybackMode): void; + setPlaybackSpeed(speed: number): void; selectNode(ids: CCNodeId[], exclusive: boolean): void; unselectNode(ids: CCNodeId[]): void; selectConnection(ids: CCConnectionId[], exclusive: boolean): void; From 709936c341ba9d725ce9b26aad4c6e49710e2d6b Mon Sep 17 00:00:00 2001 From: taka231 Date: Sat, 14 Nov 2026 10:19:43 +0900 Subject: [PATCH 2/2] Add row-major and column-major pixel ordering to Display --- .../components/Display/ConfigSettingButton.tsx | 18 ++++++++++++++++++ .../renderer/Node/components/Display/index.tsx | 6 +++++- src/store/intrinsics/definitions.ts | 5 ++++- src/store/intrinsics/types.ts | 5 ++++- 4 files changed, 31 insertions(+), 3 deletions(-) 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 b44d29b..d72e591 100644 --- a/src/pages/edit/Editor/renderer/Node/components/Display/ConfigSettingButton.tsx +++ b/src/pages/edit/Editor/renderer/Node/components/Display/ConfigSettingButton.tsx @@ -3,6 +3,7 @@ import { Button, FormLabel, IconButton, + MenuItem, Popover, Stack, TextField, @@ -97,6 +98,23 @@ export function CCComponentEditorRendererNodeDisplayRendererConfigSettingButton( }} /> + { + const pixelOrder = e.target.value; + if (pixelOrder === "row-major" || pixelOrder === "column-major") { + setNewConfig((prev) => ({ ...prev, pixelOrder })); + } + }} + size="small" + fullWidth + sx={{ mt: 2 }} + > + Row-major + Column-major +