diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..7063d88 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,6 @@ +// This file contains constants used across the application. + +export const DEFAULT_TOPIC = "/aggregated_svgs"; +export const DEFAULT_VIEWBOX_ASPECT_RATIO = 0.6; +// Potentially add INITIAL_VIEWBOX_WIDTH = 10000; if needed elsewhere, +// but it's currently well-encapsulated in usePanelConfig's defaultConfig. diff --git a/src/crane_visualizer_panel.tsx b/src/crane_visualizer_panel.tsx index 22b0616..3844c2d 100644 --- a/src/crane_visualizer_panel.tsx +++ b/src/crane_visualizer_panel.tsx @@ -1,5 +1,4 @@ -import * as React from "react"; -import { useCallback, useLayoutEffect, useState, useEffect, useMemo } from "react"; +import React, { useCallback, useLayoutEffect, useState, useEffect, FC, memo } from "react"; import { PanelExtensionContext, SettingsTree, @@ -8,10 +7,15 @@ import { MessageEvent, Topic, Subscription, - Immutable + Immutable, } from "@foxglove/studio"; import ReactDOM from "react-dom"; import { StrictMode } from "react"; +import { usePanZoom } from "./hooks/usePanZoom"; +import { usePanelConfig } from "./hooks/usePanelConfig"; // Hook import +import { PanelConfig, NamespaceConfig } from "./settings_utils"; // Type imports +import { createNamespaceFields, handleSettingsAction } from "./settings_utils"; // Import utils +import { DEFAULT_TOPIC, DEFAULT_VIEWBOX_ASPECT_RATIO } from "./constants"; // Import constants interface SvgPrimitiveArray { layer: string; // "parent/child1/child2"のような階層パス @@ -22,21 +26,10 @@ interface SvgLayerArray { svg_primitive_arrays: SvgPrimitiveArray[]; } -interface PanelConfig { - backgroundColor: string; - message: string; - viewBoxWidth: number; - namespaces: { - [key: string]: { - visible: boolean; - children?: { [key: string]: { visible: boolean; children?: any } }; - }; - }; -} +// PanelConfig and NamespaceConfig are now imported from ../settings_utils.ts -const defaultConfig: PanelConfig = { +const defaultConfigForHook: PanelConfig = { backgroundColor: "#585858ff", - message: "", viewBoxWidth: 10000, namespaces: {}, }; @@ -44,57 +37,62 @@ const defaultConfig: PanelConfig = { const CraneVisualizer: React.FC<{ context: PanelExtensionContext }> = ({ context }) => { const [viewBox, setViewBox] = useState("-5000 -3000 10000 6000"); - const [config, setConfig] = useState(defaultConfig); - const [topic, setTopic] = useState("/aggregated_svgs"); - const [topics, setTopics] = useState>(); - const [messages, setMessages] = useState>(); + const { handleMouseDown, handleWheel } = usePanZoom({ initialViewBox: viewBox, setViewBox }); + const { + config, + setBackgroundColor, + setViewBoxWidth, + setNamespaceVisibility, + initializeNamespaces, + } = usePanelConfig({ defaultConfig: defaultConfigForHook, initialState: context.initialState as Partial }); + const [topic, setTopic] = useState(DEFAULT_TOPIC); // Use constant for default topic + const [topics, setTopics] = useState>(); // Stores list of all topics + const [messages, setMessages] = useState>(); // Stores current frame messages const [renderDone, setRenderDone] = useState<(() => void) | undefined>(); - const [recv_num, setRecvNum] = useState(0); - const [latest_msg, setLatestMsg] = useState(); + const [receivedMessageCount, setReceivedMessageCount] = useState(0); // Renamed from recv_num + const [latest_msg, setLatestMsg] = useState(); // Stores the latest SvgLayerArray message + // Resets the SVG viewBox to its default position and zoom level based on the current config.viewBoxWidth. const resetViewBox = useCallback(() => { const x = -config.viewBoxWidth / 2; - const aspectRatio = 0.6; // 元のアスペクト比 (6000 / 10000) - const height = config.viewBoxWidth * aspectRatio; + const height = config.viewBoxWidth * DEFAULT_VIEWBOX_ASPECT_RATIO; // Use constant const y = -height / 2; setViewBox(`${x} ${y} ${config.viewBoxWidth} ${height}`); - }, [setViewBox, config]); + }, [setViewBox, config.viewBoxWidth]); + // Effect to handle 'Ctrl+0' for resetting the view. useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.ctrlKey && event.key === "0") { - const x = -config.viewBoxWidth / 2; - const aspectRatio = 0.6; // 元のアスペクト比 (6000 / 10000) - const height = config.viewBoxWidth * aspectRatio; - const y = -height / 2; - setViewBox(`${x} ${y} ${config.viewBoxWidth} ${height}`); + event.preventDefault(); // Prevent browser default action for Ctrl+0 + resetViewBox(); } }; document.addEventListener("keydown", handleKeyDown); - return () => { document.removeEventListener("keydown", handleKeyDown); }; - }, [resetViewBox, config]); + }, [resetViewBox]); // Depends on resetViewBox callback - // トピックが設定されたときにサブスクライブする + // Subscribes to the selected topic when the topic or context changes. useEffect(() => { - const subscription: Subscription = { topic: topic }; - context.subscribe([subscription]); - }, [topic]); + // Panel is responsible for managing its subscriptions + context.subscribe([{ topic }]); + return () => { + // Unsubscribe from all topics when the topic changes or the panel is unmounted + context.unsubscribeAll(); + }; + }, [topic, context]); // Added context as a dependency, as context.subscribe/unsubscribe are used. + // Saves the current panel configuration when it changes. useLayoutEffect(() => { context.saveState(config); - }, [config, context]); + }, [config, context]); // Depends on config and context.saveState - useLayoutEffect(() => { - const savedConfig = context.initialState as PanelConfig | undefined; - if (savedConfig) { - setConfig((prevConfig) => ({ ...prevConfig, ...savedConfig, namespaces: savedConfig.namespaces || prevConfig.namespaces })); - } - }, [context, setConfig]); + // The useLayoutEffect that loaded context.initialState is now handled by usePanelConfig. + // Updates the panel settings editor when config or topic-related states change. useEffect(() => { const updatePanelSettings = () => { const panelSettings: SettingsTree = { @@ -102,182 +100,103 @@ const CraneVisualizer: React.FC<{ context: PanelExtensionContext }> = ({ context general: { label: "General", fields: { - topic: { label: "トピック名", input: "string", value: topic }, - backgroundColor: { label: "背景色", input: "rgba", value: config.backgroundColor }, - viewBoxWidth: { label: "ViewBox 幅", input: "number", value: config.viewBoxWidth }, + topic: { label: "Topic Name", input: "string", value: topic }, //トピック名 -> Topic Name + backgroundColor: { label: "Background Color", input: "rgba", value: config.backgroundColor }, //背景色 -> Background Color + viewBoxWidth: { label: "ViewBox Width", input: "number", value: config.viewBoxWidth }, //ViewBox 幅 -> ViewBox Width }, }, namespaces: { - label: "名前空間", + label: "Namespaces", //名前空間 -> Namespaces fields: createNamespaceFields(config.namespaces), }, }, actionHandler: (action: SettingsTreeAction) => { - const path = action.payload.path.join("."); - switch (action.action) { - case "update": - if (path == "general.topic") { - setTopic(action.payload.value as string); - } else if (path == "general.backgroundColor") { - setConfig((prevConfig) => ({ ...prevConfig, backgroundColor: action.payload.value as string })); - } else if (path == "general.viewBoxWidth") { - setConfig((prevConfig) => ({ ...prevConfig, viewBoxWidth: action.payload.value as number })); - } else if (path == "general.viewBoxHeight") { - setConfig((prevConfig) => ({ ...prevConfig, viewBoxHeight: action.payload.value as number })); - } - else if (action.payload.path[0] == "namespaces") { - const pathParts = path.split("."); - const namespacePath = pathParts.slice(1, -1); - const leafNamespace = pathParts[pathParts.length - 1]; - let currentNs = config.namespaces; - for (const ns of namespacePath) { - currentNs = currentNs[ns].children || {}; - } - currentNs[leafNamespace].visible = action.payload.value as boolean; - } - break; - case "perform-node-action": - break; - } + handleSettingsAction(action, { + setTopic, + setBackgroundColor, + setViewBoxWidth, + setNamespaceVisibility, + }); }, }; context.updatePanelSettingsEditor(panelSettings); }; updatePanelSettings(); - }, [context, config]); + }, [context, config, topic, setTopic, setBackgroundColor, setViewBoxWidth, setNamespaceVisibility]); // Added setTopic to dependencies - const createNamespaceFields = (namespaces: PanelConfig["namespaces"]) => { - const fields: { [key: string]: SettingsTreeField } = {}; - const addFieldsRecursive = (ns: { [key: string]: any }, path: string[] = []) => { - for (const [name, { visible, children }] of Object.entries(ns)) { - const currentPath = [...path, name]; - const key = currentPath.join("."); - fields[key] = { - label: name, - input: "boolean", - value: visible, - help: "名前空間の表示/非表示", - }; - if (children) { - addFieldsRecursive(children, currentPath); - } - } - }; - addFieldsRecursive(namespaces); - return fields; - }; + // createNamespaceFields moved to ../settings_utils.ts - // メッセージ受信時の処理 + // This layout effect sets up the callback for Foxglove Studio to signal rendering. + // It provides `done` which should be called when the panel has completed its rendering pass. + // It also receives the current frame's messages and the list of all available topics. useLayoutEffect(() => { + // renderState contains currentFrame and topics. + // done is a callback to signal that rendering is complete. context.onRender = (renderState, done) => { - setRenderDone(() => done); - setMessages(renderState.currentFrame); - setTopics(renderState.topics); + setRenderDone(() => done); // Store the done callback to be called after state updates. + setMessages(renderState.currentFrame); // Update messages from the current frame. + if (renderState.topics) { // Update the list of all available topics. + setTopics(renderState.topics); + } }; - context.watch("topics"); - context.watch("currentFrame"); + context.watch("topics"); // Watch for changes in topic list. + context.watch("currentFrame"); // Watch for new messages. - }, [context, topic]); + }, [context]); // Effect only needs to run once to set up the onRender handler, and if context changes. + // Processes incoming messages when `messages` or `topic` state changes. + // It filters messages for the selected topic, updates the latest message, + // increments a counter, and initializes namespaces based on received SVG layers. useEffect(() => { if (messages) { for (const message of messages) { if (message.topic === topic) { const msg = message.message as SvgLayerArray; setLatestMsg(msg); - setRecvNum(recv_num + 1); + setReceivedMessageCount((prevCount) => prevCount + 1); // Use new setter - // 初期化時にconfig.namespacesを設定 - setConfig((prevConfig) => { - const newNamespaces = { ...prevConfig.namespaces }; - msg.svg_primitive_arrays.forEach((svg_primitive_array) => { - if (!newNamespaces[svg_primitive_array.layer]) { - newNamespaces[svg_primitive_array.layer] = { visible: true }; - } - }); - return { ...prevConfig, namespaces: newNamespaces }; - }); + const layers = msg.svg_primitive_arrays.map(arr => arr.layer); + initializeNamespaces(layers); // Initialize namespaces from the new message } } } - }, [messages]); + }, [messages, topic, initializeNamespaces]); // Depends on messages, topic, and initializeNamespaces. - // invoke the done callback once the render is complete + // This effect calls the `done` callback received from `onRender` after the panel has processed + // new messages and updated its state, signaling to Foxglove Studio that the render pass is complete. useEffect(() => { - renderDone?.(); - }, [renderDone]); + if (renderDone) { + renderDone(); + } + }, [renderDone, latest_msg, config]); // Call done when renderDone is set and after relevant states (latest_msg, config) are updated. - const handleCheckboxChange = (layer: string) => { - setConfig((prevConfig) => { - const newNamespaces = { ...prevConfig.namespaces }; - if (!newNamespaces[layer]) { - newNamespaces[layer] = { visible: true }; - } - newNamespaces[layer].visible = !newNamespaces[layer].visible; - return { ...prevConfig, namespaces: newNamespaces }; - }); - }; +// InfoDisplay component to show Topic and Received Message Count +const InfoDisplay: FC<{ topic: string; receivedMessageCount: number }> = memo(({ topic, receivedMessageCount }) => ( +
+

Topic: {topic}

+

Receive num: {receivedMessageCount}

+
+)); return (
-
-
-

Topic: {topic}

-
-
-

Receive num: {recv_num}

-
+ +
+ {/* flexGrow: 1 allows this div to take remaining space */} { - const startX = e.clientX; - const startY = e.clientY; - const [x, y, width, height] = viewBox.split(" ").map(Number); - const handleMouseMove = (e: MouseEvent) => { - const dx = e.clientX - startX; - const dy = e.clientY - startY; - const scaledDx = dx * width / 400; - const scaledDy = dy * height / 400; - setViewBox(`${x - scaledDx} ${y - scaledDy} ${width} ${height}`); - }; - const handleMouseUp = () => { - document.removeEventListener("mousemove", handleMouseMove); - document.removeEventListener("mouseup", handleMouseUp); - }; - document.addEventListener("mousemove", handleMouseMove); - document.addEventListener("mouseup", handleMouseUp); - }} - onWheel={(e) => { - e.preventDefault(); - const [x, y, width, height] = viewBox.split(" ").map(Number); - const scale = e.deltaY > 0 ? 1.2 : 0.8; - let newWidth = width * scale; - let newHeight = height * scale; - const minWidth = width / 10; - const maxWidth = width * 10; - const minHeight = height / 10; - const maxHeight = height * 10; - - newWidth = Math.max(minWidth, Math.min(maxWidth, newWidth)); - newHeight = Math.max(minHeight, Math.min(maxHeight, newHeight)); - - const centerX = x + width / 2; - const centerY = y + height / 2; - const newX = centerX - newWidth / 2; - const newY = centerY - newHeight / 2; - setViewBox(`${newX} ${newY} ${newWidth} ${newHeight}`); - }} + {...{ onMouseDown: handleMouseDown, onWheel: handleWheel }} > - {latest_msg && latest_msg.svg_primitive_arrays.map((svg_primitive_array, index) => ( + {latest_msg && latest_msg.svg_primitive_arrays.map((svg_primitive_array) => ( - {svg_primitive_array.svg_primitives.map((svg_primitive, index) => ( - + {svg_primitive_array.svg_primitives.map((svg_primitive) => ( + ))} ))} @@ -292,7 +211,7 @@ export function initPanel(context: PanelExtensionContext): () => void { , - context.panelElement, + context.panelElement ); return () => { ReactDOM.unmountComponentAtNode(context.panelElement); diff --git a/src/hooks/usePanZoom.ts b/src/hooks/usePanZoom.ts new file mode 100644 index 0000000..46de4a6 --- /dev/null +++ b/src/hooks/usePanZoom.ts @@ -0,0 +1,86 @@ +import { useCallback, Dispatch, SetStateAction, MouseEvent as ReactMouseEvent, WheelEvent as ReactWheelEvent } from "react"; + +const PAN_VIEWBOX_PIXEL_MOTION_DIVISOR = 400; // Higher values make panning slower relative to pixel motion. +const ZOOM_IN_FACTOR = 1.2; +const ZOOM_OUT_FACTOR = 0.8; +const MAX_ZOOM_RATIO = 10; // Represents 10x zoom in or 10x zoom out from initial. + +interface UsePanZoomOptions { + initialViewBox: string; + setViewBox: Dispatch>; +} + +interface PanZoomHandlers { + handleMouseDown: (event: ReactMouseEvent) => void; + handleWheel: (event: ReactWheelEvent) => void; +} + +export const usePanZoom = ({ // No direct React.* usage in function body if types are aliased/imported directly + initialViewBox, + setViewBox, +}: UsePanZoomOptions): PanZoomHandlers => { + const handleMouseDown = useCallback( + (e: ReactMouseEvent) => { + const startX = e.clientX; + const startY = e.clientY; + // Assuming viewBox is a string like "x y width height" + const [vx, vy, vwidth, vheight] = initialViewBox.split(" ").map(Number); + + const handleMouseMove = (event: MouseEvent) => { + const dx = event.clientX - startX; + const dy = event.clientY - startY; + + // Use a scaling factor relative to the viewBox dimensions. + const scaledDx = (dx * vwidth) / PAN_VIEWBOX_PIXEL_MOTION_DIVISOR; + const scaledDy = (dy * vheight) / PAN_VIEWBOX_PIXEL_MOTION_DIVISOR; // Maintain aspect ratio of panning speed + + setViewBox(`${vx - scaledDx} ${vy - scaledDy} ${vwidth} ${vheight}`); + }; + + const handleMouseUp = () => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + }, + [initialViewBox, setViewBox] + ); + + const handleWheel = useCallback( + (e: ReactWheelEvent) => { + e.preventDefault(); + const [x, y, width, height] = initialViewBox.split(" ").map(Number); + const scale = e.deltaY > 0 ? ZOOM_IN_FACTOR : ZOOM_OUT_FACTOR; + + // Define min/max zoom levels based on initial width/height and MAX_ZOOM_RATIO + const minWidth = width / MAX_ZOOM_RATIO; + const maxWidth = width * MAX_ZOOM_RATIO; + const minHeight = height / MAX_ZOOM_RATIO; + const maxHeight = height * MAX_ZOOM_RATIO; + + let newWidth = width * scale; + let newHeight = height * scale; + + // Apply zoom constraints + newWidth = Math.max(minWidth, Math.min(maxWidth, newWidth)); + newHeight = Math.max(minHeight, Math.min(maxHeight, newHeight)); + + // If new dimensions are clamped, adjust scale to maintain aspect ratio (optional) + // This part can be tricky; for now, we'll allow aspect ratio to change slightly at limits + // or ensure clamping logic respects aspect ratio. + // For simplicity, current clamping might alter aspect ratio if one dimension hits limit before other. + + const centerX = x + width / 2; + const centerY = y + height / 2; + const newX = centerX - newWidth / 2; + const newY = centerY - newHeight / 2; + + setViewBox(`${newX} ${newY} ${newWidth} ${newHeight}`); + }, + [initialViewBox, setViewBox] + ); + + return { handleMouseDown, handleWheel }; +}; diff --git a/src/hooks/usePanelConfig.ts b/src/hooks/usePanelConfig.ts new file mode 100644 index 0000000..9423cc3 --- /dev/null +++ b/src/hooks/usePanelConfig.ts @@ -0,0 +1,110 @@ +// This file will contain the usePanelConfig custom React hook. +import { useState, useCallback } from "react"; +import { PanelConfig } from "../settings_utils"; // Import PanelConfig type (NamespaceConfig is used via PanelConfig) + +interface UsePanelConfigOptions { + defaultConfig: PanelConfig; + initialState?: Partial; // initialState from context can be partial +} + +interface PanelConfigUpdaters { + config: PanelConfig; + setBackgroundColor: (color: string) => void; + setViewBoxWidth: (width: number) => void; + setNamespaceVisibility: (layerPath: string | string[], isVisible: boolean) => void; + initializeNamespaces: (layers: string[]) => void; +} + +export const usePanelConfig = ({ + defaultConfig, + initialState, +}: UsePanelConfigOptions): PanelConfigUpdaters => { + const [config, setConfig] = useState(() => { + const mergedConfig = { ...defaultConfig }; + if (initialState) { + mergedConfig.backgroundColor = initialState.backgroundColor ?? defaultConfig.backgroundColor; + mergedConfig.viewBoxWidth = initialState.viewBoxWidth ?? defaultConfig.viewBoxWidth; + // Deep merge for namespaces is more complex, handle it carefully + // For now, simple overwrite, but ideally, it should be a deep merge + mergedConfig.namespaces = initialState.namespaces ? JSON.parse(JSON.stringify(initialState.namespaces)) : defaultConfig.namespaces; + } + return mergedConfig; + }); + + const setBackgroundColor = useCallback((color: string) => { + setConfig((prevConfig) => ({ ...prevConfig, backgroundColor: color })); + }, []); + + const setViewBoxWidth = useCallback((width: number) => { + setConfig((prevConfig) => ({ ...prevConfig, viewBoxWidth: width })); + }, []); + + const setNamespaceVisibility = useCallback( + (layerPath: string | string[], isVisible: boolean) => { + setConfig((prevConfig) => { + const newConfig = JSON.parse(JSON.stringify(prevConfig)); // Deep clone + let current = newConfig.namespaces; + const pathArray = Array.isArray(layerPath) ? layerPath : layerPath.split("/"); // Assuming '/' delimiter if string + + pathArray.forEach((part, index) => { + if (index === pathArray.length - 1) { + if (!current[part]) { + current[part] = { visible: isVisible, children: {} }; // Initialize if not exists + } else { + current[part].visible = isVisible; + } + } else { + if (!current[part]) { + current[part] = { visible: true, children: {} }; // Create path if not exists + } + if (!current[part].children) { + current[part].children = {}; // Ensure children object exists + } + current = current[part].children; + } + }); + return newConfig; + }); + }, + [] + ); + + const initializeNamespaces = useCallback((layers: string[]) => { + setConfig((prevConfig) => { + const newConfig = JSON.parse(JSON.stringify(prevConfig)); // Deep clone + let changed = false; + layers.forEach((layer) => { + // This correctly handles hierarchical layer paths like "parent/child" + const pathArray = layer.split("/"); + let current = newConfig.namespaces; + pathArray.forEach((part, index) => { + if (index === pathArray.length -1 ) { + if(!current[part]) { + current[part] = { visible: true }; + changed = true; + } + } else { + if (!current[part]) { + current[part] = { visible: true, children: {} }; + changed = true; + } + if (!current[part].children) { + current[part].children = {}; // Ensure children object exists + changed = true; + } + current = current[part].children; + } + }); + }); + return changed ? newConfig : prevConfig; + }); + }, []); + + return { + config, + setBackgroundColor, + setViewBoxWidth, + setNamespaceVisibility, + initializeNamespaces, + }; +}; diff --git a/src/settings_utils.ts b/src/settings_utils.ts new file mode 100644 index 0000000..edef4e3 --- /dev/null +++ b/src/settings_utils.ts @@ -0,0 +1,90 @@ +// This file will contain settings-related utility functions and type definitions. + +import { SettingsTreeField, SettingsTreeAction } from "@foxglove/studio"; + +export interface NamespaceConfig { + visible: boolean; + children?: { [key: string]: NamespaceConfig }; +} + +export interface PanelConfig { + backgroundColor: string; + viewBoxWidth: number; + namespaces: { [key: string]: NamespaceConfig }; +} + +export const createNamespaceFields = (namespaces: { [key: string]: NamespaceConfig }): { [key: string]: SettingsTreeField } => { + const fields: { [key: string]: SettingsTreeField } = {}; + const addFieldsRecursive = (ns: { [key: string]: NamespaceConfig }, path: string[] = []) => { + for (const [name, { visible, children }] of Object.entries(ns)) { + const currentPath = [...path, name]; + const key = currentPath.join("."); + fields[key] = { + label: name, + input: "boolean", + value: visible, + help: "Show/hide namespace", // Translated from Japanese + }; + if (children) { + addFieldsRecursive(children, currentPath); + } + } + }; + addFieldsRecursive(namespaces); + return fields; +}; + +interface SettingsUpdaters { + setTopic: (topic: string) => void; + setBackgroundColor: (color: string) => void; + setViewBoxWidth: (width: number) => void; + setNamespaceVisibility: (layerPath: string | string[], isVisible: boolean) => void; +} + +export const handleSettingsAction = ( + action: SettingsTreeAction, + // config: PanelConfig, // config is not directly used for updates, updaters directly change state + updaters: SettingsUpdaters +): void => { + const path = action.payload.path; // This is an array of strings + + if (action.action === "update") { + const topLevelKey = path[0]; + const settingKey = path[1]; // For "general" settings + + switch (topLevelKey) { + case "general": + if (!settingKey) return; // Should not happen with valid paths + switch (settingKey) { + case "topic": + updaters.setTopic(action.payload.value as string); + break; + case "backgroundColor": + updaters.setBackgroundColor(action.payload.value as string); + break; + case "viewBoxWidth": + updaters.setViewBoxWidth(action.payload.value as number); + break; + default: + console.warn(`Unhandled general setting: ${path.join(".")}`); + } + break; + case "namespaces": + // Path for namespaces is like ["namespaces", "parent", "child", "grandchild"] + // The setNamespaceVisibility function takes the path parts after "namespaces" + const namespacePathArray = path.slice(1); + const isVisible = action.payload.value as boolean; + if (namespacePathArray.length > 0) { + updaters.setNamespaceVisibility(namespacePathArray, isVisible); + } else { + console.warn(`Invalid namespace path: ${path.join(".")}`); + } + break; + default: + console.warn(`Unhandled settings action path: ${path.join(".")}`); + } + } else if (action.action === "perform-node-action") { + // Handle any node actions if necessary in the future + console.log(`Node action performed: ${action.payload.id}`); + } +};