diff --git a/frontend/src/components/Dashboard/DashboardGrid.tsx b/frontend/src/components/Dashboard/DashboardGrid.tsx
index 6bb4fae..b93dc53 100644
--- a/frontend/src/components/Dashboard/DashboardGrid.tsx
+++ b/frontend/src/components/Dashboard/DashboardGrid.tsx
@@ -24,6 +24,7 @@ import { useMosaicWebRTCConnection } from "@/hooks/useMosaicWebRTCConnection.ts"
import { useRobotInfo } from "@/hooks/useRobotInfo.ts";
import { RobotConnector, type TabConfig, type WidgetConfig } from "@/mosaic";
import { DASHBOARD_STORAGE_KEYS } from "@/utils";
+import { getWidgetDescriptor } from "@/widgets/_utils/widgetRegistry.ts";
const ResponsiveGridLayout = WidthProvider(Responsive);
@@ -157,7 +158,10 @@ export default function DashboardGrid({ tabId }: DashboardGridProps) {
connectors: widget.connectors.map((connector) => {
return new RobotConnector(connector.robotId, connector.connectorId);
}),
- params: widget.params,
+ params: {
+ ...getWidgetDescriptor(widget.type)?.getDefaultParams(),
+ ...widget.params,
+ },
onUpdateWidgetParams: (params?: any) => {
onUpdateWidgetParams(widget.id, params);
},
diff --git a/frontend/src/components/Dashboard/Widgets/ErrorWidget.tsx b/frontend/src/components/Dashboard/Widgets/ErrorWidget.tsx
index 1a840c6..cce7c0e 100644
--- a/frontend/src/components/Dashboard/Widgets/ErrorWidget.tsx
+++ b/frontend/src/components/Dashboard/Widgets/ErrorWidget.tsx
@@ -31,14 +31,7 @@ export function ErrorWidget({ error }: ErrorWidgetProps) {
-
+
Error
@@ -50,4 +43,4 @@ export function ErrorWidget({ error }: ErrorWidgetProps) {
);
-}
\ No newline at end of file
+}
diff --git a/frontend/src/components/Dashboard/Widgets/MosaicWidgetContext.tsx b/frontend/src/components/Dashboard/Widgets/MosaicWidgetContext.tsx
index ad3e3bf..4e775bf 100644
--- a/frontend/src/components/Dashboard/Widgets/MosaicWidgetContext.tsx
+++ b/frontend/src/components/Dashboard/Widgets/MosaicWidgetContext.tsx
@@ -12,4 +12,4 @@ export function useMosaicWidget(): WidgetConfig {
return ctx;
}
-export { MosaicWidgetContext };
\ No newline at end of file
+export { MosaicWidgetContext };
diff --git a/frontend/src/components/Dashboard/Widgets/WidgetComponents.tsx b/frontend/src/components/Dashboard/Widgets/WidgetComponents.tsx
index 86d7275..fac2ee5 100644
--- a/frontend/src/components/Dashboard/Widgets/WidgetComponents.tsx
+++ b/frontend/src/components/Dashboard/Widgets/WidgetComponents.tsx
@@ -13,7 +13,7 @@ import { WidgetHeader } from "./WidgetHeader.tsx";
interface WidgetFrameProps {
children?: ReactNode;
widgetConfig: WidgetConfig;
- error?: string;
+ error?: string | null;
}
export function WidgetRoot({ widgetConfig, children, error }: WidgetFrameProps) {
diff --git a/frontend/src/components/Dashboard/Widgets/WidgetDescriptor.ts b/frontend/src/components/Dashboard/Widgets/WidgetDescriptor.ts
index 0acb2d5..7d8cbfa 100644
--- a/frontend/src/components/Dashboard/Widgets/WidgetDescriptor.ts
+++ b/frontend/src/components/Dashboard/Widgets/WidgetDescriptor.ts
@@ -1,6 +1,6 @@
import type { StoreType } from "@/mosaic/store/interface/mosaic-store.ts";
-export abstract class WidgetDescriptor {
+export abstract class WidgetDescriptor> {
public abstract getName(): string;
public getRequiredStoreType(): StoreType | null {
@@ -27,7 +27,11 @@ export abstract class WidgetDescriptor {
return false;
}
- public validateParams(_params: Record): string | null {
+ public getDefaultParams(): T {
+ return {} as T;
+ }
+
+ public validateParams(_params: T): string | null {
return null;
}
diff --git a/frontend/src/components/WidgetCustomize/StoreDataPanel.tsx b/frontend/src/components/WidgetCustomize/StoreDataPanel.tsx
index 9d780a4..09aed2f 100644
--- a/frontend/src/components/WidgetCustomize/StoreDataPanel.tsx
+++ b/frontend/src/components/WidgetCustomize/StoreDataPanel.tsx
@@ -6,8 +6,16 @@ interface ReceivableDataPanelProps {
defaultData: string;
}
+function tryFormatJson(value: string): string {
+ try {
+ return JSON.stringify(JSON.parse(value), null, 2);
+ } catch {
+ return value;
+ }
+}
+
function ReceivableDataPanel({ onInject, defaultData }: ReceivableDataPanelProps) {
- const [input, setInput] = useState(defaultData);
+ const [input, setInput] = useState(() => tryFormatJson(defaultData));
return (
@@ -18,9 +26,10 @@ function ReceivableDataPanel({ onInject, defaultData }: ReceivableDataPanelProps
size="sm"
value={input}
onChange={(e) => setInput(e.target.value)}
+ onBlur={(e) => setInput(tryFormatJson(e.target.value))}
placeholder="Enter raw data string..."
fontFamily="mono"
- rows={4}
+ rows={6}
/>
);
-}
\ No newline at end of file
+}
diff --git a/frontend/src/components/WidgetCustomize/WidgetCustomizePage.tsx b/frontend/src/components/WidgetCustomize/WidgetCustomizePage.tsx
index d81f069..39e5479 100644
--- a/frontend/src/components/WidgetCustomize/WidgetCustomizePage.tsx
+++ b/frontend/src/components/WidgetCustomize/WidgetCustomizePage.tsx
@@ -1,5 +1,5 @@
import { Box, Heading, Separator, Text, VStack } from "@chakra-ui/react";
-import { useContext, useEffect, useRef, useState } from "react";
+import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import type { RobotConfig, WidgetConfig } from "@/mosaic";
import type { MosaicStore } from "@/mosaic/store/interface/mosaic-store.ts";
@@ -95,7 +95,7 @@ export function WidgetCustomizePage() {
// Store interaction
const [storeType, setStoreType] = useState<"receivable" | "sendable" | "media" | null>(null);
- const [sentDataLog, setSentDataLog] = useState([]);
+ const [sentDataLog, setSentDataLog] = useState<{ time: string; data: string }[]>([]);
// Keep refs for cleanup
const currentStoreRef = useRef(null);
@@ -126,16 +126,16 @@ export function WidgetCustomizePage() {
const mockChannel = {
readyState: "open" as RTCDataChannelState,
send: (data: string) => {
- setSentDataLog((prev) =>
- [`[${new Date().toLocaleTimeString()}] ${data}`, ...prev].slice(0, 100),
- );
+ const now = new Date();
+ const time = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`;
+ setSentDataLog((prev) => [{ time, data }, ...prev].slice(0, 100));
},
} as unknown as RTCDataChannel;
store.setDataChannel(mockChannel);
}
setStoreType(store.getStoreType());
- setWidgetParams({});
+ setWidgetParams(getWidgetDescriptor(widgetType)?.getDefaultParams() ?? {});
setWidgetPosition({ x: 0, y: 0, w: 8, h: 6 });
updateRobotInfo(new RobotInfo(TEST_ROBOT_ID, "Test Robot", 6, fakeRobotConfig));
@@ -153,9 +153,10 @@ export function WidgetCustomizePage() {
const handleApply = () => {
if (!widgetType || !connectorId || !connectorType) return;
- // Release this component's own store ref (not the widget's ref)
+ // Force-delete the store for this connector so Phase 2 always creates a fresh store
+ // of the newly selected type. The widget's own cleanup will release its ref gracefully.
if (currentConnectorRef.current) {
- storeManager.releaseStore(currentConnectorRef.current);
+ storeManager.forceDeleteStore(currentConnectorRef.current);
currentStoreRef.current = null;
currentConnectorRef.current = null;
}
@@ -207,16 +208,22 @@ export function WidgetCustomizePage() {
}
};
- const widgetConfig: WidgetConfig | null = appliedConfig
- ? {
- id: "test-widget",
- type: appliedConfig.widgetType,
- position: widgetPosition,
- connectors: [new RobotConnector(TEST_ROBOT_ID, appliedConfig.connectorId)],
- params: widgetParams,
- onUpdateWidgetParams: (params) => setWidgetParams(params ?? {}),
- }
- : null;
+ const handleUpdateWidgetParams = useCallback((params?: any) => setWidgetParams(params ?? {}), []);
+
+ const widgetConfig: WidgetConfig | null = useMemo(
+ () =>
+ appliedConfig
+ ? {
+ id: "test-widget",
+ type: appliedConfig.widgetType,
+ position: widgetPosition,
+ connectors: [new RobotConnector(TEST_ROBOT_ID, appliedConfig.connectorId)],
+ params: widgetParams,
+ onUpdateWidgetParams: handleUpdateWidgetParams,
+ }
+ : null,
+ [appliedConfig, widgetPosition, widgetParams, handleUpdateWidgetParams],
+ );
const isApplyDisabled = !widgetType || !connectorId || !connectorType;
diff --git a/frontend/src/mosaic/store/store-manager.ts b/frontend/src/mosaic/store/store-manager.ts
index 2a8bac5..7b73cf5 100644
--- a/frontend/src/mosaic/store/store-manager.ts
+++ b/frontend/src/mosaic/store/store-manager.ts
@@ -69,6 +69,10 @@ export class StoreManager {
return store;
}
+ public forceDeleteStore(robotConnector: RobotConnector): void {
+ this.mosaicStores.delete(robotConnector);
+ }
+
public releaseStore(robotConnector: RobotConnector): boolean {
const store = this.mosaicStores.get(robotConnector);
if (store === undefined) return false;
diff --git a/frontend/src/mosaic/webrtc/webrtc-connection.ts b/frontend/src/mosaic/webrtc/webrtc-connection.ts
index 7888a6f..ab04311 100644
--- a/frontend/src/mosaic/webrtc/webrtc-connection.ts
+++ b/frontend/src/mosaic/webrtc/webrtc-connection.ts
@@ -209,7 +209,6 @@ export class WebRTCConnection {
this.peerConnection = peerConnection;
peerConnection.onicecandidate = this.onicecandidate.bind(this);
peerConnection.onconnectionstatechange = this.onconnectionstatechange.bind(this);
- peerConnection.ontrack = this.ontrack.bind(this);
return peerConnection;
}
@@ -359,24 +358,6 @@ export class WebRTCConnection {
}
}
- private ontrack(event: RTCTrackEvent): void {
- console.log(
- `Received remote track id: ${event.track.id},
- stream id: ${event.streams?.[0]?.id}
- kind: ${event.track.kind}`,
- );
-
- if (event.track.kind !== "video" || !event.streams?.[0]) {
- console.log(`[${this.robotInfo.id}] Video track is not of kind video or has no stream`);
- return;
- }
-
- const stream = event.streams[0];
- this.mediaStreams.set(stream.id, stream);
-
- // TODO: need to connect to the store
- }
-
private onConnectionConnected(): void {
console.log(
`[${this.robotInfo.id}] Connection established!, notifying related stores: `,
diff --git a/frontend/src/widgets/ClearpathPlatformPowerViewerWidget/ClearpathPlatformPowerViewerWidget.tsx b/frontend/src/widgets/ClearpathPlatformPowerViewerWidget/ClearpathPlatformPowerViewerWidget.tsx
index 4e8e0a3..879d830 100644
--- a/frontend/src/widgets/ClearpathPlatformPowerViewerWidget/ClearpathPlatformPowerViewerWidget.tsx
+++ b/frontend/src/widgets/ClearpathPlatformPowerViewerWidget/ClearpathPlatformPowerViewerWidget.tsx
@@ -7,7 +7,7 @@ import { MdBatteryFull, MdPower } from "react-icons/md";
import type { JsonReceivableStore } from "@/stores/JsonReceivableStore/JsonReceivableStore.ts";
import type { WidgetProps } from "@/widgets/index.ts";
-import { WidgetRoot } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
+import { MosaicWidget } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
import { useMosaicStore } from "@/hooks/useMosaicStore.ts";
interface PowerData {
@@ -113,80 +113,90 @@ export default function ClearpathPlatformPowerViewerWidget({ widgetConfig }: Wid
};
}, [widgetConfig]);
- if (!data) {
- return (
-
-
-
- Waiting for data...
-
-
-
- );
- }
-
return (
-
-
- {/* Connection Status */}
-
- }
- label="Battery"
- connected={data.battery_connected === 1}
- />
- }
- label="Charger"
- connected={data.charger_connected === 1}
- />
-
-
-
-
- {/* Voltages | Currents */}
-
-
-
- Voltages
+
+
+ {!data ? (
+
+
+ Waiting for data...
-
-
-
-
-
-
-
- Currents
+
+ ) : (
+
+ {/* Connection Status */}
+
+ }
+ label="Battery"
+ connected={data.battery_connected === 1}
+ />
+ }
+ label="Charger"
+ connected={data.charger_connected === 1}
+ />
+
+
+
+
+ {/* Voltages | Currents */}
+
+
+
+ Voltages
+
+
+
+
+
+
+
+
+ Currents
+
+
+
+
+
+
+
+
+
+ {/* Timestamp */}
+
+ {formatTimestamp(data.timestamp)}
-
-
-
-
-
-
-
- {/* Timestamp */}
-
- {formatTimestamp(data.timestamp)}
-
-
-
+ )}
+
+
);
}
diff --git a/frontend/src/widgets/ClearpathPlatformPowerViewerWidget/WidgetDescriptor.ts b/frontend/src/widgets/ClearpathPlatformPowerViewerWidget/WidgetDescriptor.ts
new file mode 100644
index 0000000..40f6b39
--- /dev/null
+++ b/frontend/src/widgets/ClearpathPlatformPowerViewerWidget/WidgetDescriptor.ts
@@ -0,0 +1,31 @@
+import type { StoreType } from "@/mosaic/store/interface/mosaic-store.ts";
+
+import { WidgetDescriptor } from "@/components/Dashboard/Widgets/WidgetDescriptor.ts";
+
+export default class ClearpathPlatformPowerViewerWidgetDescriptor extends WidgetDescriptor {
+ public getName(): string {
+ return "ClearpathPlatformPowerViewerWidget";
+ }
+
+ public getRequiredStoreType(): StoreType {
+ return "receivable";
+ }
+
+ public getDefaultInjectData(): string {
+ return JSON.stringify({
+ battery_connected: 1,
+ charger_connected: 0,
+ measured_currents: {
+ left_driver_current: "2.15 A",
+ mcu_and_user_port_current: "0.87 A",
+ right_driver_current: "2.03 A",
+ },
+ measured_voltages: {
+ battery_voltage: "25.6 V",
+ left_driver_voltage: "24.9 V",
+ right_driver_voltage: "25.1 V",
+ },
+ timestamp: 1716000000000000,
+ });
+ }
+}
diff --git a/frontend/src/widgets/ConnectionCheckWidget/ConnectionCheckWidget.tsx b/frontend/src/widgets/ConnectionCheckWidget/ConnectionCheckWidget.tsx
index beceb12..c989ec7 100644
--- a/frontend/src/widgets/ConnectionCheckWidget/ConnectionCheckWidget.tsx
+++ b/frontend/src/widgets/ConnectionCheckWidget/ConnectionCheckWidget.tsx
@@ -6,7 +6,7 @@ import type { ConnectionCheckReceiverStore } from "@/stores/ConnectionCheckRecei
import type { ConnectionCheckSenderStore } from "@/stores/ConnectionCheckSenderStore/ConnectionCheckSenderStore.ts";
import type { WidgetProps } from "@/widgets/index.ts";
-import { WidgetRoot } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
+import { MosaicWidget } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
import { useMosaicStore } from "@/hooks/useMosaicStore.ts";
const MAX_MESSAGES = 200;
@@ -99,21 +99,23 @@ export default function ConnectionCheckWidget({ widgetConfig }: WidgetProps) {
}, [widgetConfig]);
return (
-
-
-
- {connectionCheckingMessages.map((msg, i) => {
- const latency = msg.messageReceived - msg.messageCreated;
- return (
-
- latency: {(latency * 0.5).toFixed(3)} ms
- {/*created: {formatTimestamp(msg.messageCreated)}*/}
- {/*received: {formatTimestamp(msg.messageReceived)}*/}
-
- );
- })}
-
-
-
+
+
+
+
+ {connectionCheckingMessages.map((msg, i) => {
+ const latency = msg.messageReceived - msg.messageCreated;
+ return (
+
+ latency: {(latency * 0.5).toFixed(3)} ms
+ {/*created: {formatTimestamp(msg.messageCreated)}*/}
+ {/*received: {formatTimestamp(msg.messageReceived)}*/}
+
+ );
+ })}
+
+
+
+
);
}
diff --git a/frontend/src/widgets/ConnectionCheckWidget/WidgetDescriptor.ts b/frontend/src/widgets/ConnectionCheckWidget/WidgetDescriptor.ts
new file mode 100644
index 0000000..53cf058
--- /dev/null
+++ b/frontend/src/widgets/ConnectionCheckWidget/WidgetDescriptor.ts
@@ -0,0 +1,25 @@
+import { WidgetDescriptor } from "@/components/Dashboard/Widgets/WidgetDescriptor.ts";
+
+export default class ConnectionCheckWidgetDescriptor extends WidgetDescriptor {
+ public getName(): string {
+ return "ConnectionCheckWidget";
+ }
+
+ public getMinStoreNumber(): number {
+ return 2;
+ }
+
+ public getMaxStoreNumber(): number {
+ return 2;
+ }
+
+ public getMinRobotConnectorNumber(): number {
+ return 2;
+ }
+
+ public getMaxRobotConnectorNumber(): number {
+ return 2;
+ }
+
+ // TODO: sender/receiver 두 커넥터를 동시에 사용하는 구조라 단일 inject로 표현 불가
+}
diff --git a/frontend/src/widgets/DelayCheckWidget/DelayCheckWidget.tsx b/frontend/src/widgets/DelayCheckWidget/DelayCheckWidget.tsx
index 919bf48..a7ce8d2 100644
--- a/frontend/src/widgets/DelayCheckWidget/DelayCheckWidget.tsx
+++ b/frontend/src/widgets/DelayCheckWidget/DelayCheckWidget.tsx
@@ -6,7 +6,7 @@ import type { ConnectionCheckReceiverStore } from "@/stores/ConnectionCheckRecei
import type { ConnectionCheckSenderStore } from "@/stores/ConnectionCheckSenderStore/ConnectionCheckSenderStore.ts";
import type { WidgetProps } from "@/widgets/index.ts";
-import { WidgetRoot } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
+import { MosaicWidget } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
import { useMosaicStore } from "@/hooks/useMosaicStore.ts";
import { useRobotInfo } from "@/hooks/useRobotInfo.ts";
@@ -153,118 +153,120 @@ export default function DelayCheckWidget({ widgetConfig }: WidgetProps) {
}, [widgetConfig]);
return (
-
-
- {/* Stats + controls */}
-
- {/* Left: stats in two rows */}
-
-
- {(["Mean", "Std"] as const).map((label) => (
-
- {label}
-
- {stats?.[label.toLowerCase() as "mean" | "std"]?.toFixed(3) ?? "—"}
+
+
+
+ {/* Stats + controls */}
+
+ {/* Left: stats in two rows */}
+
+
+ {(["Mean", "Std"] as const).map((label) => (
+
+ {label}
+
+ {stats?.[label.toLowerCase() as "mean" | "std"]?.toFixed(3) ?? "—"}
+
-
- ))}
-
-
- {(["p50", "p95", "p99"] as const).map((label) => (
-
- {label}
-
- {stats?.[label]?.toFixed(3) ?? "—"}
+ ))}
+
+
+ {(["p50", "p95", "p99"] as const).map((label) => (
+
+ {label}
+
+ {stats?.[label]?.toFixed(3) ?? "—"}
+
-
- ))}
-
-
+ ))}
+
+
- {/* Right: [Large + switch] | [Save CSV] */}
-
-
-
- Large
+ {/* Right: [Large + switch] | [Save CSV] */}
+
+
+
+ Large
+
+ {
+ largeModeRef.current = e.checked;
+ setLargeMode(e.checked);
+ }}
+ >
+
+
+
+
+
- {
- largeModeRef.current = e.checked;
- setLargeMode(e.checked);
- }}
+
-
-
+ Save CSV
+
+
-
- {/* Data table */}
-
-
-
-
- {TABLE_HEADERS.map((col) => (
-
- {col}
-
- ))}
-
-
-
- {connectionCheckingMessages.map((msg, i) => {
- const latency = (msg.messageReceived - msg.messageCreated) * 0.5;
- return (
-
-
- {i + 1}
-
-
- {latency.toFixed(3)}
+ {/* Data table */}
+
+
+
+
+ {TABLE_HEADERS.map((col) => (
+
+ {col}
-
- {formatTimestamp(msg.messageCreated)}
-
-
- {formatTimestamp(msg.messageReceived)}
+ ))}
+
+
+
+ {connectionCheckingMessages.map((msg, i) => {
+ const latency = (msg.messageReceived - msg.messageCreated) * 0.5;
+ return (
+
+
+ {i + 1}
+
+
+ {latency.toFixed(3)}
+
+
+ {formatTimestamp(msg.messageCreated)}
+
+
+ {formatTimestamp(msg.messageReceived)}
+
-
- );
- })}
+ );
+ })}
+
-
-
+
+
);
}
diff --git a/frontend/src/widgets/DelayCheckWidget/WidgetDescriptor.ts b/frontend/src/widgets/DelayCheckWidget/WidgetDescriptor.ts
new file mode 100644
index 0000000..91a106c
--- /dev/null
+++ b/frontend/src/widgets/DelayCheckWidget/WidgetDescriptor.ts
@@ -0,0 +1,25 @@
+import { WidgetDescriptor } from "@/components/Dashboard/Widgets/WidgetDescriptor.ts";
+
+export default class DelayCheckWidgetDescriptor extends WidgetDescriptor {
+ public getName(): string {
+ return "DelayCheckWidget";
+ }
+
+ public getMinStoreNumber(): number {
+ return 2;
+ }
+
+ public getMaxStoreNumber(): number {
+ return 2;
+ }
+
+ public getMinRobotConnectorNumber(): number {
+ return 2;
+ }
+
+ public getMaxRobotConnectorNumber(): number {
+ return 2;
+ }
+
+ // TODO: sender/receiver 두 커넥터를 동시에 사용하는 구조라 단일 inject로 표현 불가
+}
diff --git a/frontend/src/widgets/ImuState3DViewerWidget/ImuState3DViewerWidget.tsx b/frontend/src/widgets/ImuState3DViewerWidget/ImuState3DViewerWidget.tsx
index 5acb355..2968b68 100644
--- a/frontend/src/widgets/ImuState3DViewerWidget/ImuState3DViewerWidget.tsx
+++ b/frontend/src/widgets/ImuState3DViewerWidget/ImuState3DViewerWidget.tsx
@@ -7,7 +7,7 @@ import * as THREE from "three";
import type { JsonReceivableStore } from "@/stores/JsonReceivableStore/JsonReceivableStore.ts";
import type { WidgetProps } from "@/widgets/index.ts";
-import { WidgetRoot } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
+import { MosaicWidget } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
import { useMosaicStore } from "@/hooks/useMosaicStore.ts";
interface ImuData {
@@ -117,78 +117,80 @@ export default function ImuState3DViewerWidget({ widgetConfig }: WidgetProps) {
}, [widgetConfig]);
return (
-
-
- {/* 3D Orientation Viewer */}
-
-
-
-
- {/* Sensor Values */}
-
-
-
- Angular Velocity
-
- {(["X", "Y", "Z"] as const).map((axis, i) => (
-
- ))}
-
-
-
-
- Linear Accel
-
- {(["X", "Y", "Z"] as const).map((axis, i) => (
-
- ))}
-
-
-
-
+
+
+
+ {/* 3D Orientation Viewer */}
+
+
+
+
+ {/* Sensor Values */}
+
+
+
+ Angular Velocity
+
+ {(["X", "Y", "Z"] as const).map((axis, i) => (
+
+ ))}
+
+
+
+
+ Linear Accel
+
+ {(["X", "Y", "Z"] as const).map((axis, i) => (
+
+ ))}
+
+
+
+
+
);
}
diff --git a/frontend/src/widgets/ImuState3DViewerWidget/WidgetDescriptor.ts b/frontend/src/widgets/ImuState3DViewerWidget/WidgetDescriptor.ts
new file mode 100644
index 0000000..c02dd0b
--- /dev/null
+++ b/frontend/src/widgets/ImuState3DViewerWidget/WidgetDescriptor.ts
@@ -0,0 +1,21 @@
+import type { StoreType } from "@/mosaic/store/interface/mosaic-store.ts";
+
+import { WidgetDescriptor } from "@/components/Dashboard/Widgets/WidgetDescriptor.ts";
+
+export default class ImuState3DViewerWidgetDescriptor extends WidgetDescriptor {
+ public getName(): string {
+ return "ImuState3DViewerWidget";
+ }
+
+ public getRequiredStoreType(): StoreType {
+ return "receivable";
+ }
+
+ public getDefaultInjectData(): string {
+ return JSON.stringify({
+ angular_velocity: [0.01, -0.02, 0.05],
+ linear_acceleration: [0.12, -0.05, 9.81],
+ orientation: [1.0, 0.0, 0.0, 0.0],
+ });
+ }
+}
diff --git a/frontend/src/widgets/JsonViewerWidget/JsonViewerSetting.tsx b/frontend/src/widgets/JsonViewerWidget/JsonViewerSetting.tsx
index 10f6108..6590b54 100644
--- a/frontend/src/widgets/JsonViewerWidget/JsonViewerSetting.tsx
+++ b/frontend/src/widgets/JsonViewerWidget/JsonViewerSetting.tsx
@@ -1,14 +1,11 @@
import { VStack, Text } from "@chakra-ui/react";
import { Controller, useForm } from "react-hook-form";
-import { MosaicWidget } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
import { useMosaicWidget } from "@/components/Dashboard/Widgets/MosaicWidgetContext.tsx";
+import { MosaicWidget } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
import { Checkbox } from "@/components/ui/checkbox.tsx";
import { Field } from "@/components/ui/field.tsx";
-
-export type JsonViewerParams = {
- cumulative: boolean;
-};
+import { JsonViewerParams } from "@/widgets/JsonViewerWidget/WidgetDescriptor.ts";
export function JsonViewerSetting() {
const widgetConfig = useMosaicWidget();
diff --git a/frontend/src/widgets/JsonViewerWidget/JsonViewerWidget.tsx b/frontend/src/widgets/JsonViewerWidget/JsonViewerWidget.tsx
index 21e3e75..758f16e 100644
--- a/frontend/src/widgets/JsonViewerWidget/JsonViewerWidget.tsx
+++ b/frontend/src/widgets/JsonViewerWidget/JsonViewerWidget.tsx
@@ -23,6 +23,8 @@ export default function JsonViewerWidget({ widgetConfig }: WidgetProps) {
return;
}
+ console.log("store", store);
+
const unsubscribe = store.subscribe((data) => {
if (widgetConfig.params.cumulative) {
setData((prevData) => [...prevData, data]);
diff --git a/frontend/src/widgets/JsonViewerWidget/WidgetDescriptor.ts b/frontend/src/widgets/JsonViewerWidget/WidgetDescriptor.ts
index 224fc80..68a613a 100644
--- a/frontend/src/widgets/JsonViewerWidget/WidgetDescriptor.ts
+++ b/frontend/src/widgets/JsonViewerWidget/WidgetDescriptor.ts
@@ -2,7 +2,11 @@ import type { StoreType } from "@/mosaic/store/interface/mosaic-store.ts";
import { WidgetDescriptor } from "@/components/Dashboard/Widgets/WidgetDescriptor.ts";
-export default class JsonViewerWidgetDescriptor extends WidgetDescriptor {
+export type JsonViewerParams = {
+ cumulative: boolean;
+};
+
+export default class JsonViewerWidgetDescriptor extends WidgetDescriptor {
public getName(): string {
return "JsonViewerWidget";
}
@@ -11,7 +15,19 @@ export default class JsonViewerWidgetDescriptor extends WidgetDescriptor {
return "receivable";
}
+ public supportCustomParams(): boolean {
+ return true;
+ }
+
+ public getDefaultParams(): JsonViewerParams {
+ return {
+ cumulative: false,
+ };
+ }
+
public getDefaultInjectData(): string {
- return '{"message": "Hello, World!"}';
+ return JSON.stringify({
+ message: "Hello, World!",
+ });
}
}
diff --git a/frontend/src/widgets/LaserScanPolarViewerWidget/LaserScanPolarViewerWidget.tsx b/frontend/src/widgets/LaserScanPolarViewerWidget/LaserScanPolarViewerWidget.tsx
index c4f5762..855b659 100644
--- a/frontend/src/widgets/LaserScanPolarViewerWidget/LaserScanPolarViewerWidget.tsx
+++ b/frontend/src/widgets/LaserScanPolarViewerWidget/LaserScanPolarViewerWidget.tsx
@@ -4,7 +4,7 @@ import { useEffect, useRef, useState } from "react";
import type { JsonReceivableStore } from "@/stores/JsonReceivableStore/JsonReceivableStore.ts";
import type { WidgetProps } from "@/widgets/index.ts";
-import { WidgetRoot } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
+import { MosaicWidget } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
import { useMosaicStore } from "@/hooks/useMosaicStore.ts";
interface LaserScanData {
@@ -153,10 +153,19 @@ export default function LaserScanPolarViewerWidget({ widgetConfig }: WidgetProps
}, [data]);
return (
-
-
-
-
-
+
+
+
+
+
+
+
);
}
diff --git a/frontend/src/widgets/LaserScanPolarViewerWidget/WidgetDescriptor.ts b/frontend/src/widgets/LaserScanPolarViewerWidget/WidgetDescriptor.ts
new file mode 100644
index 0000000..c4237a7
--- /dev/null
+++ b/frontend/src/widgets/LaserScanPolarViewerWidget/WidgetDescriptor.ts
@@ -0,0 +1,28 @@
+import type { StoreType } from "@/mosaic/store/interface/mosaic-store.ts";
+
+import { WidgetDescriptor } from "@/components/Dashboard/Widgets/WidgetDescriptor.ts";
+
+export default class LaserScanPolarViewerWidgetDescriptor extends WidgetDescriptor {
+ public getName(): string {
+ return "LaserScanPolarViewerWidget";
+ }
+
+ public getRequiredStoreType(): StoreType {
+ return "receivable";
+ }
+
+ public getDefaultInjectData(): string {
+ // 12 points, 30° increments — close obstacles on one side, open on the other
+ return JSON.stringify({
+ angle_min: -3.14159,
+ angle_max: 3.14159,
+ angle_increment: 0.5236,
+ range_min: 0.1,
+ range_max: 10.0,
+ ranges: [3.0, 2.5, 1.8, 1.2, 1.8, 2.5, 3.0, 5.0, 7.0, 8.5, 9.0, 8.5],
+ intensities: [],
+ scan_time: 0.1,
+ time_increment: 0.0001,
+ });
+ }
+}
diff --git a/frontend/src/widgets/MediaViewerWidget/MediaViewerSetting.tsx b/frontend/src/widgets/MediaViewerWidget/MediaViewerSetting.tsx
index 4a2b1cc..b71d785 100644
--- a/frontend/src/widgets/MediaViewerWidget/MediaViewerSetting.tsx
+++ b/frontend/src/widgets/MediaViewerWidget/MediaViewerSetting.tsx
@@ -1,15 +1,11 @@
import { VStack, Text } from "@chakra-ui/react";
import { Controller, useForm } from "react-hook-form";
-import { MosaicWidget } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
import { useMosaicWidget } from "@/components/Dashboard/Widgets/MosaicWidgetContext.tsx";
+import { MosaicWidget } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
import { Checkbox } from "@/components/ui/checkbox.tsx";
import { Field } from "@/components/ui/field.tsx";
-
-export type MediaViewerParams = {
- flipH: boolean;
- flipV: boolean;
-};
+import { MediaViewerParams } from "@/widgets/MediaViewerWidget/WidgetDescriptor.ts";
export function MediaViewerSetting() {
const widgetConfig = useMosaicWidget();
@@ -31,11 +27,7 @@ export function MediaViewerSetting() {
Media Viewer Settings
-
+
-
+
);
-}
\ No newline at end of file
+}
diff --git a/frontend/src/widgets/MediaViewerWidget/MediaViewerWidget.tsx b/frontend/src/widgets/MediaViewerWidget/MediaViewerWidget.tsx
index c4ff283..f4b383d 100644
--- a/frontend/src/widgets/MediaViewerWidget/MediaViewerWidget.tsx
+++ b/frontend/src/widgets/MediaViewerWidget/MediaViewerWidget.tsx
@@ -129,68 +129,57 @@ export default function MediaViewerWidget({ widgetConfig }: WidgetProps) {
}, []);
return (
-
+
- {error ? (
-
-
- ⚠️
-
- {error}
-
- ) : (
- <>
-
-
-
+
+
+
+
+ {isPlaying ? "⏸️" : "▶️"}
+
+
+
-
-
- {isPlaying ? "⏸️" : "▶️"}
-
-
-
- ⛶
-
-
-
- >
- )}
+ ⛶
+
+
+
);
-}
\ No newline at end of file
+}
diff --git a/frontend/src/widgets/MediaViewerWidget/WidgetDescriptor.ts b/frontend/src/widgets/MediaViewerWidget/WidgetDescriptor.ts
index 16890d9..e139bd8 100644
--- a/frontend/src/widgets/MediaViewerWidget/WidgetDescriptor.ts
+++ b/frontend/src/widgets/MediaViewerWidget/WidgetDescriptor.ts
@@ -2,7 +2,12 @@ import type { StoreType } from "@/mosaic/store/interface/mosaic-store.ts";
import { WidgetDescriptor } from "@/components/Dashboard/Widgets/WidgetDescriptor.ts";
-export default class MediaViewerWidgetDescriptor extends WidgetDescriptor {
+export type MediaViewerParams = {
+ flipH: boolean;
+ flipV: boolean;
+};
+
+export default class MediaViewerWidgetDescriptor extends WidgetDescriptor {
public getName(): string {
return "MediaViewerWidget";
}
@@ -15,7 +20,14 @@ export default class MediaViewerWidgetDescriptor extends WidgetDescriptor {
return true;
}
- public validateParams(_params: Record): string | null {
+ public getDefaultParams(): MediaViewerParams {
+ return {
+ flipH: false,
+ flipV: false,
+ };
+ }
+
+ public validateParams(_params: MediaViewerParams): string | null {
return null;
}
-}
\ No newline at end of file
+}
diff --git a/frontend/src/widgets/MediaViewerWithStatWidget/MediaViewerWithStatWidget.tsx b/frontend/src/widgets/MediaViewerWithStatWidget/MediaViewerWithStatWidget.tsx
index b20803b..f0c1844 100644
--- a/frontend/src/widgets/MediaViewerWithStatWidget/MediaViewerWithStatWidget.tsx
+++ b/frontend/src/widgets/MediaViewerWithStatWidget/MediaViewerWithStatWidget.tsx
@@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { MediaStreamStore, StreamStats } from "@/stores/MediaStreamStore/MediaStreamStore.ts";
import type { WidgetProps } from "@/widgets/index.ts";
-import { WidgetRoot } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
+import { MosaicWidget } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
import { useMosaicStore } from "@/hooks/useMosaicStore.ts";
import { useRobotInfo } from "@/hooks/useRobotInfo.ts";
@@ -163,22 +163,8 @@ export default function MediaViewerWithStatWidget({ widgetConfig }: WidgetProps)
}, []);
return (
-
- {error ? (
-
-
- ⚠️
-
- {error}
-
- ) : (
+
+
{/* Video area */}
@@ -258,8 +244,8 @@ export default function MediaViewerWithStatWidget({ widgetConfig }: WidgetProps)
- )}
-
+
+
);
}
diff --git a/frontend/src/widgets/MediaViewerWithStatWidget/WidgetDescriptor.ts b/frontend/src/widgets/MediaViewerWithStatWidget/WidgetDescriptor.ts
new file mode 100644
index 0000000..72bf2bd
--- /dev/null
+++ b/frontend/src/widgets/MediaViewerWithStatWidget/WidgetDescriptor.ts
@@ -0,0 +1,13 @@
+import type { StoreType } from "@/mosaic/store/interface/mosaic-store.ts";
+
+import { WidgetDescriptor } from "@/components/Dashboard/Widgets/WidgetDescriptor.ts";
+
+export default class MediaViewerWithStatWidgetDescriptor extends WidgetDescriptor {
+ public getName(): string {
+ return "MediaViewerWithStatWidget";
+ }
+
+ public getRequiredStoreType(): StoreType {
+ return "media";
+ }
+}
diff --git a/frontend/src/widgets/NotFoundWidget/NotFoundWidget.tsx b/frontend/src/widgets/NotFoundWidget/NotFoundWidget.tsx
index f12c42a..aaac20b 100644
--- a/frontend/src/widgets/NotFoundWidget/NotFoundWidget.tsx
+++ b/frontend/src/widgets/NotFoundWidget/NotFoundWidget.tsx
@@ -3,63 +3,65 @@ import { LuPuzzle, LuWrench } from "react-icons/lu";
import type { WidgetProps } from "@/widgets/index.ts";
-import { WidgetRoot } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
+import { MosaicWidget } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
export default function NotFoundWidget({ widgetConfig }: WidgetProps) {
return (
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Unsupported Widget
+
+
+ This widget type is not available in this build.
+
+
+
+
+
-
-
-
-
- Unsupported Widget
-
-
- This widget type is not available in this build.
+
+ Requested type
-
-
+ {widgetConfig.type}
+
-
-
- Requested type
-
- {widgetConfig.type}
-
-
-
-
-
- Add the matching widget component in
-
- /Dashboard/widgets
-
- and register by filename.
-
-
-
-
-
-
+
+
+
+ Add the matching widget component in
+
+ /Dashboard/widgets
+
+ and register by filename.
+
+
+
+
+
+
+
);
}
diff --git a/frontend/src/widgets/OpenStreetMapViewerWidget/OpenStreetMapViewerWidget.tsx b/frontend/src/widgets/OpenStreetMapViewerWidget/OpenStreetMapViewerWidget.tsx
index 9c48d37..1ff97c2 100644
--- a/frontend/src/widgets/OpenStreetMapViewerWidget/OpenStreetMapViewerWidget.tsx
+++ b/frontend/src/widgets/OpenStreetMapViewerWidget/OpenStreetMapViewerWidget.tsx
@@ -8,7 +8,7 @@ import { MapContainer, Marker, Popup, TileLayer } from "react-leaflet";
import type { JsonReceivableStore } from "@/stores/JsonReceivableStore/JsonReceivableStore.ts";
import type { WidgetProps } from "@/widgets/index.ts";
-import { WidgetRoot } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
+import { MosaicWidget } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
import { useMosaicStore } from "@/hooks/useMosaicStore.ts";
import { useRobotInfo } from "@/hooks/useRobotInfo.ts";
import "leaflet/dist/leaflet.css";
@@ -230,84 +230,86 @@ export default function OpenStreetMapViewerWidget({ widgetConfig }: WidgetProps)
};
return (
-
-
-
-
- {robotsWithCoordinate.map(({ robotId, coordinate: markerCoordinate }) => (
- {
- centerMapOnRobot(robotId);
- },
- }}
- >
-
-
- {robotNameById[robotId] ?? robotId}
-
-
- {markerCoordinate.latitude.toFixed(6)}, {markerCoordinate.longitude.toFixed(6)}
-
-
-
- ))}
-
-
-
-
- {robotIds.map((robotId) => {
- const robotName = robotNameById[robotId] ?? robotId;
- const hasCoordinate = !!robotGpsStateMap[robotId]?.coordinate;
- const isSelected = robotId === activeRobotId;
-
- return (
-
- );
- })}
-
+
+ {markerCoordinate.latitude.toFixed(6)}, {markerCoordinate.longitude.toFixed(6)}
+
+
+
+ ))}
+
+
+
+
+ {robotIds.map((robotId) => {
+ const robotName = robotNameById[robotId] ?? robotId;
+ const hasCoordinate = !!robotGpsStateMap[robotId]?.coordinate;
+ const isSelected = robotId === activeRobotId;
+
+ return (
+
+ );
+ })}
+
+
-
-
+
+
);
}
diff --git a/frontend/src/widgets/OpenStreetMapViewerWidget/WidgetDescriptor.ts b/frontend/src/widgets/OpenStreetMapViewerWidget/WidgetDescriptor.ts
new file mode 100644
index 0000000..b552db8
--- /dev/null
+++ b/frontend/src/widgets/OpenStreetMapViewerWidget/WidgetDescriptor.ts
@@ -0,0 +1,24 @@
+import type { StoreType } from "@/mosaic/store/interface/mosaic-store.ts";
+
+import { WidgetDescriptor } from "@/components/Dashboard/Widgets/WidgetDescriptor.ts";
+
+export default class OpenStreetMapViewerWidgetDescriptor extends WidgetDescriptor {
+ public getName(): string {
+ return "OpenStreetMapViewerWidget";
+ }
+
+ public getRequiredStoreType(): StoreType {
+ return "receivable";
+ }
+
+ public getMaxRobotConnectorNumber(): number {
+ return -1;
+ }
+
+ public getDefaultInjectData(): string {
+ return JSON.stringify({
+ latitude: 36.3504,
+ longitude: 127.3845,
+ });
+ }
+}
diff --git a/frontend/src/widgets/PointCloud2DViewerV2Widget/PointCloud2DViewerV2Widget.tsx b/frontend/src/widgets/PointCloud2DViewerV2Widget/PointCloud2DViewerV2Widget.tsx
index a941120..762da4b 100644
--- a/frontend/src/widgets/PointCloud2DViewerV2Widget/PointCloud2DViewerV2Widget.tsx
+++ b/frontend/src/widgets/PointCloud2DViewerV2Widget/PointCloud2DViewerV2Widget.tsx
@@ -9,7 +9,7 @@ import type {
} from "@/stores/@types/pointcloud.ts";
import type { WidgetProps } from "@/widgets/index.ts";
-import { WidgetRoot } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
+import { MosaicWidget } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
import { useMosaicStore } from "@/hooks/useMosaicStore.ts";
import AngleIndicator from "./AngleIndicator.tsx";
@@ -209,7 +209,7 @@ export default function PointCloud2DViewerV2Widget({ widgetConfig }: WidgetProps
}
};
- const footerInfo = [
+ const additionalInfo = [
{
label: "Points",
value: pointCount.toLocaleString(),
@@ -225,23 +225,10 @@ export default function PointCloud2DViewerV2Widget({ widgetConfig }: WidgetProps
];
return (
-
- {error ? (
-
-
- ⚠️
-
- {error}
-
- ) : (
-
+
+
+
+
@@ -256,7 +243,7 @@ export default function PointCloud2DViewerV2Widget({ widgetConfig }: WidgetProps
/>
- )}
-
+
+
);
}
diff --git a/frontend/src/widgets/PointCloud2DViewerV2Widget/WidgetDescriptor.ts b/frontend/src/widgets/PointCloud2DViewerV2Widget/WidgetDescriptor.ts
new file mode 100644
index 0000000..d8cf407
--- /dev/null
+++ b/frontend/src/widgets/PointCloud2DViewerV2Widget/WidgetDescriptor.ts
@@ -0,0 +1,15 @@
+import type { StoreType } from "@/mosaic/store/interface/mosaic-store.ts";
+
+import { WidgetDescriptor } from "@/components/Dashboard/Widgets/WidgetDescriptor.ts";
+
+export default class PointCloud2DViewerV2WidgetDescriptor extends WidgetDescriptor {
+ public getName(): string {
+ return "PointCloud2DViewerV2Widget";
+ }
+
+ public getRequiredStoreType(): StoreType {
+ return "receivable";
+ }
+
+ // TODO: PointCloudData는 binary chunk 구조를 포함하므로 JSON inject 불가
+}
diff --git a/frontend/src/widgets/PointCloud2DViewerWidget/PointCloud2DViewerWidget.tsx b/frontend/src/widgets/PointCloud2DViewerWidget/PointCloud2DViewerWidget.tsx
index a216f03..76f56c5 100644
--- a/frontend/src/widgets/PointCloud2DViewerWidget/PointCloud2DViewerWidget.tsx
+++ b/frontend/src/widgets/PointCloud2DViewerWidget/PointCloud2DViewerWidget.tsx
@@ -1,4 +1,3 @@
-import { Box, Flex } from "@chakra-ui/react";
import { useEffect, useRef, useState } from "react";
import type { ReceivableStore } from "@/mosaic/store/interface/receivable-store.ts";
@@ -9,7 +8,7 @@ import type {
} from "@/stores/@types/pointcloud.ts";
import type { WidgetProps } from "@/widgets/index.ts";
-import { WidgetRoot } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
+import { MosaicWidget } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
import { useMosaicStore } from "@/hooks/useMosaicStore.ts";
import AngleIndicator from "@/widgets/PointCloud2DViewerWidget/AngleIndicator.tsx";
@@ -182,7 +181,7 @@ export default function PointCloud2DViewerWidget({ widgetConfig }: WidgetProps)
setLastUpdate(new Date());
};
- const footerInfo = [
+ const additionalInfo = [
{
label: "Points",
value: pointCount.toLocaleString(),
@@ -194,35 +193,20 @@ export default function PointCloud2DViewerWidget({ widgetConfig }: WidgetProps)
];
return (
-
- {error ? (
-
-
- ⚠️
-
- {error}
-
- ) : (
- <>
-
-
- >
- )}
-
+
+
+
+
+
+
+
);
}
diff --git a/frontend/src/widgets/PointCloud2DViewerWidget/WidgetDescriptor.ts b/frontend/src/widgets/PointCloud2DViewerWidget/WidgetDescriptor.ts
new file mode 100644
index 0000000..a975959
--- /dev/null
+++ b/frontend/src/widgets/PointCloud2DViewerWidget/WidgetDescriptor.ts
@@ -0,0 +1,15 @@
+import type { StoreType } from "@/mosaic/store/interface/mosaic-store.ts";
+
+import { WidgetDescriptor } from "@/components/Dashboard/Widgets/WidgetDescriptor.ts";
+
+export default class PointCloud2DViewerWidgetDescriptor extends WidgetDescriptor {
+ public getName(): string {
+ return "PointCloud2DViewerWidget";
+ }
+
+ public getRequiredStoreType(): StoreType {
+ return "receivable";
+ }
+
+ // TODO: PointCloudData는 binary chunk 구조를 포함하므로 JSON inject 불가
+}
diff --git a/frontend/src/widgets/PointCloud3DViewerWidget/PointCloud3DViewerSetting.tsx b/frontend/src/widgets/PointCloud3DViewerWidget/PointCloud3DViewerSetting.tsx
new file mode 100644
index 0000000..0ef3b62
--- /dev/null
+++ b/frontend/src/widgets/PointCloud3DViewerWidget/PointCloud3DViewerSetting.tsx
@@ -0,0 +1,119 @@
+import { ButtonGroup, Button, Slider, Text, VStack, HStack } from "@chakra-ui/react";
+import { Controller, useForm } from "react-hook-form";
+
+import { useMosaicWidget } from "@/components/Dashboard/Widgets/MosaicWidgetContext.tsx";
+import { MosaicWidget } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
+import { Checkbox } from "@/components/ui/checkbox.tsx";
+import { Field } from "@/components/ui/field.tsx";
+import { PointCloud3DViewerParams } from "@/widgets/PointCloud3DViewerWidget/WidgetDescriptor.ts";
+
+import type { ColorMode } from "./colorMapping.ts";
+
+const COLOR_MODE_OPTIONS: { value: ColorMode; label: string }[] = [
+ { value: "height", label: "Height" },
+ { value: "intensity", label: "Intensity" },
+ { value: "depth", label: "Depth" },
+ { value: "hybrid", label: "Hybrid" },
+];
+
+export function PointCloud3DViewerSetting() {
+ const { params } = useMosaicWidget();
+
+ const useFormReturn = useForm({
+ mode: "onBlur",
+ criteriaMode: "all",
+ defaultValues: {
+ colorMode: (params?.colorMode as ColorMode) ?? "height",
+ pointSize: (params?.pointSize as number) ?? 0.05,
+ showAxes: (params?.showAxes as boolean) ?? false,
+ autoRotate: (params?.autoRotate as boolean) ?? false,
+ },
+ });
+
+ const { control, watch } = useFormReturn;
+ const colorMode = watch("colorMode");
+ const pointSize = watch("pointSize");
+
+ return (
+
+
+
+ (
+
+ {COLOR_MODE_OPTIONS.map((opt) => (
+
+ ))}
+
+ )}
+ />
+
+
+
+ (
+ field.onChange(details.value[0] / 1000)}
+ min={10}
+ max={200}
+ step={5}
+ w="100%"
+ >
+
+
+
+
+
+
+
+ )}
+ />
+
+
+
+
+ (
+ field.onChange(checked)}
+ >
+ Enable
+
+ )}
+ />
+
+
+
+ (
+ field.onChange(checked)}
+ >
+ Enable
+
+ )}
+ />
+
+
+
+
+ );
+}
diff --git a/frontend/src/widgets/PointCloud3DViewerWidget/PointCloud3DViewerWidget.tsx b/frontend/src/widgets/PointCloud3DViewerWidget/PointCloud3DViewerWidget.tsx
index 4f11206..97fb3fb 100644
--- a/frontend/src/widgets/PointCloud3DViewerWidget/PointCloud3DViewerWidget.tsx
+++ b/frontend/src/widgets/PointCloud3DViewerWidget/PointCloud3DViewerWidget.tsx
@@ -1,4 +1,4 @@
-import { Box, Flex } from "@chakra-ui/react";
+import { Box, Button } from "@chakra-ui/react";
import { useEffect, useRef, useState } from "react";
import type { ReceivableStore } from "@/mosaic/store/interface/receivable-store.ts";
@@ -9,14 +9,13 @@ import type {
} from "@/stores/@types/pointcloud.ts";
import type { WidgetProps } from "@/widgets/index.ts";
-import { WidgetRoot } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
+import { MosaicWidget } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
import { useMosaicStore } from "@/hooks/useMosaicStore.ts";
import type { ColorMode } from "./colorMapping.ts";
-import ColorModeSelector from "./ColorModeSelector.tsx";
+import { PointCloud3DViewerSetting } from "./PointCloud3DViewerSetting.tsx";
import { ThreeScene } from "./ThreeScene.ts";
-import ViewControls from "./ViewControls.tsx";
export default function PointCloud3DViewerWidget({ widgetConfig }: WidgetProps) {
const { getOrCreateStore, releaseStore } = useMosaicStore();
@@ -27,12 +26,12 @@ export default function PointCloud3DViewerWidget({ widgetConfig }: WidgetProps)
const [lastUpdate, setLastUpdate] = useState(null);
const [error, setError] = useState(null);
const [pointCount, setPointCount] = useState(0);
- const [colorMode, setColorMode] = useState("height");
- const colorModeRef = useRef("height");
- const [pointSize, setPointSize] = useState(0.05);
- const [showAxes, setShowAxes] = useState(false);
- const [autoRotate, setAutoRotate] = useState(false);
- const [cameraPosition, setCameraPosition] = useState({ x: -5, y: 0, z: 3 });
+
+ const colorMode = (widgetConfig.params?.colorMode as ColorMode) ?? "height";
+ const colorModeRef = useRef(colorMode);
+ const pointSize = (widgetConfig.params?.pointSize as number) ?? 0.05;
+ const showAxes = (widgetConfig.params?.showAxes as boolean) ?? false;
+ const autoRotate = (widgetConfig.params?.autoRotate as boolean) ?? false;
const lastPCMetaRef = useRef(null);
const lastPCPointsRef = useRef(null);
@@ -62,15 +61,7 @@ export default function PointCloud3DViewerWidget({ widgetConfig }: WidgetProps)
resizeObserver.observe(container);
resizeObserverRef.current = resizeObserver;
- // Update camera position periodically
- const intervalId = setInterval(() => {
- if (scene) {
- setCameraPosition(scene.getCameraPosition());
- }
- }, 100);
-
return () => {
- clearInterval(intervalId);
resizeObserver.disconnect();
scene.dispose();
threeSceneRef.current = null;
@@ -144,54 +135,24 @@ export default function PointCloud3DViewerWidget({ widgetConfig }: WidgetProps)
};
}, [widgetConfig]);
- // Handle color mode change
- const handleColorModeChange = (mode: ColorMode) => {
- setColorMode(mode);
- colorModeRef.current = mode;
- const scene = threeSceneRef.current;
- if (scene && lastPCPointsRef.current && lastPCMetaRef.current) {
- scene.updatePoints(lastPCPointsRef.current, lastPCMetaRef.current, mode);
- }
- };
-
- // Handle point size change
- const handlePointSizeChange = (size: number) => {
- setPointSize(size);
- const scene = threeSceneRef.current;
- if (scene) {
- scene.setPointSize(size);
- }
- };
-
- // Handle show axes toggle
- const handleShowAxesToggle = () => {
- const newValue = !showAxes;
- setShowAxes(newValue);
- const scene = threeSceneRef.current;
- if (scene) {
- scene.setShowAxes(newValue);
- }
- };
-
- // Handle auto-rotate toggle
- const handleAutoRotateToggle = () => {
- const newValue = !autoRotate;
- setAutoRotate(newValue);
+ // params 변경 시 Three.js scene에 즉시 반영
+ useEffect(() => {
const scene = threeSceneRef.current;
- if (scene) {
- scene.setAutoRotate(newValue);
+ if (!scene) return;
+ colorModeRef.current = colorMode;
+ scene.setPointSize(pointSize);
+ scene.setShowAxes(showAxes);
+ scene.setAutoRotate(autoRotate);
+ if (lastPCPointsRef.current && lastPCMetaRef.current) {
+ scene.updatePoints(lastPCPointsRef.current, lastPCMetaRef.current, colorMode);
}
- };
+ }, [colorMode, pointSize, showAxes, autoRotate]);
- // Handle reset camera
const handleResetCamera = () => {
- const scene = threeSceneRef.current;
- if (scene) {
- scene.resetCamera();
- }
+ threeSceneRef.current?.resetCamera();
};
- const footerInfo = [
+ const additionalInfo = [
{
label: "Points",
value: pointCount.toLocaleString(),
@@ -203,49 +164,23 @@ export default function PointCloud3DViewerWidget({ widgetConfig }: WidgetProps)
];
return (
-
- {error ? (
-
+
+
+
+
+
+
-
- ⚠️
-
- {error}
-
- ) : (
-
- {/* Color Mode Controls */}
-
-
- {/* View Controls */}
-
-
- {/* Three.js Container */}
-
-
- )}
-
+ w="100%"
+ position="relative"
+ borderRadius="6px"
+ overflow="hidden"
+ />
+
+
);
}
diff --git a/frontend/src/widgets/PointCloud3DViewerWidget/WidgetDescriptor.ts b/frontend/src/widgets/PointCloud3DViewerWidget/WidgetDescriptor.ts
new file mode 100644
index 0000000..dc1b7bc
--- /dev/null
+++ b/frontend/src/widgets/PointCloud3DViewerWidget/WidgetDescriptor.ts
@@ -0,0 +1,36 @@
+import type { StoreType } from "@/mosaic/store/interface/mosaic-store.ts";
+
+import { WidgetDescriptor } from "@/components/Dashboard/Widgets/WidgetDescriptor.ts";
+import { ColorMode } from "@/widgets/PointCloud3DViewerWidget/colorMapping.ts";
+
+export type PointCloud3DViewerParams = {
+ colorMode: ColorMode;
+ pointSize: number;
+ showAxes: boolean;
+ autoRotate: boolean;
+};
+
+export default class PointCloud3DViewerWidgetDescriptor extends WidgetDescriptor {
+ public getName(): string {
+ return "PointCloud3DViewerWidget";
+ }
+
+ public getRequiredStoreType(): StoreType {
+ return "receivable";
+ }
+
+ public supportCustomParams(): boolean {
+ return true;
+ }
+
+ public getDefaultParams(): PointCloud3DViewerParams {
+ return {
+ colorMode: "height",
+ pointSize: 0.05,
+ showAxes: false,
+ autoRotate: false,
+ };
+ }
+
+ // TODO: PointCloudData는 binary chunk 구조를 포함하므로 JSON inject 불가
+}
diff --git a/frontend/src/widgets/ThumbstickSenderWidget/ThumbstickSenderSetting.tsx b/frontend/src/widgets/ThumbstickSenderWidget/ThumbstickSenderSetting.tsx
new file mode 100644
index 0000000..558562d
--- /dev/null
+++ b/frontend/src/widgets/ThumbstickSenderWidget/ThumbstickSenderSetting.tsx
@@ -0,0 +1,49 @@
+import { VStack } from "@chakra-ui/react";
+import { Controller, useForm } from "react-hook-form";
+
+import { useMosaicWidget } from "@/components/Dashboard/Widgets/MosaicWidgetContext.tsx";
+import { MosaicWidget } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
+import { Checkbox } from "@/components/ui/checkbox.tsx";
+import { Field } from "@/components/ui/field.tsx";
+import { ThumbstickParams } from "@/widgets/ThumbstickSenderWidget/WidgetDescriptor.ts";
+
+export function ThumbstickSenderSetting() {
+ const widgetConfig = useMosaicWidget();
+ const useFormReturn = useForm({
+ mode: "onBlur",
+ defaultValues: {
+ holonomic: widgetConfig.params.holonomic ?? false,
+ },
+ });
+
+ const {
+ formState: { errors },
+ control,
+ } = useFormReturn;
+
+ return (
+
+
+
+ (
+ field.onChange(checked)}
+ >
+ Enable holonomic mode
+
+ )}
+ />
+
+
+
+ );
+}
diff --git a/frontend/src/widgets/ThumbstickSenderWidget/ThumbstickSenderWidget.tsx b/frontend/src/widgets/ThumbstickSenderWidget/ThumbstickSenderWidget.tsx
index 1c7d44c..be3ab4a 100644
--- a/frontend/src/widgets/ThumbstickSenderWidget/ThumbstickSenderWidget.tsx
+++ b/frontend/src/widgets/ThumbstickSenderWidget/ThumbstickSenderWidget.tsx
@@ -1,14 +1,15 @@
import { Box } from "@chakra-ui/react";
-import { useEffect, useRef, useState } from "react";
+import { CSSProperties, useEffect, useRef, useState } from "react";
import type { SendableStore } from "@/mosaic/store/interface/sendable-store.ts";
import type { Thumbstick } from "@/stores/@types/thumbstick.ts";
import type { WidgetProps } from "@/widgets/index.ts";
-import { WidgetRoot } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
-import { Checkbox } from "@/components/ui/checkbox.tsx";
+import { MosaicWidget } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
import { useMosaicStore } from "@/hooks/useMosaicStore.ts";
+import { ThumbstickSenderSetting } from "./ThumbstickSenderSetting.tsx";
+
const SEND_INTERVAL_MS = 30;
const KNOB_SIZE_RATIO = 0.2; // knob diameter as fraction of pad width
@@ -21,9 +22,9 @@ function toThumbstick(dx: number, dy: number, holonomic: boolean, maxRadius: num
}
export default function ThumbstickSenderWidget({ widgetConfig }: WidgetProps) {
- const { getOrCreateStore } = useMosaicStore();
+ const { getOrCreateStore, releaseStore } = useMosaicStore();
const storeRef = useRef | null>(null);
- const [holonomic, setHolonomic] = useState(false);
+ const holonomic: boolean = widgetConfig.params?.holonomic ?? false;
const [knobPos, setKnobPos] = useState({ x: 0, y: 0 });
const [isDragging, setIsDragging] = useState(false);
const isDraggingRef = useRef(false);
@@ -33,7 +34,7 @@ export default function ThumbstickSenderWidget({ widgetConfig }: WidgetProps) {
power: 0,
holonomic: false,
});
- const holonomicRef = useRef(false);
+ const holonomicRef = useRef(holonomic);
const padRef = useRef(null);
useEffect(() => {
@@ -44,15 +45,10 @@ export default function ThumbstickSenderWidget({ widgetConfig }: WidgetProps) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
- // releaseStore(connector)
+ releaseStore(connector);
};
}, [widgetConfig]);
- // Keep holonomicRef in sync with state for use inside interval callback
- useEffect(() => {
- holonomicRef.current = holonomic;
- }, [holonomic]);
-
const updateKnob = (clientX: number, clientY: number) => {
if (!padRef.current) return;
const rect = padRef.current.getBoundingClientRect();
@@ -88,14 +84,23 @@ export default function ThumbstickSenderWidget({ widgetConfig }: WidgetProps) {
};
return (
-
-
- {/* Container that fills available space */}
-
+
+
+
+
+
+ {/* Square pad: fills available space, always a square */}
+
{
+ console.log("send!");
storeRef.current?.send(thumbstickRef.current);
}, SEND_INTERVAL_MS);
}
@@ -162,14 +168,7 @@ export default function ThumbstickSenderWidget({ widgetConfig }: WidgetProps) {
/>
-
- {/* Holonomic toggle */}
-
- setHolonomic(!!e.checked)}>
- Holonomic
-
-
-
-
+
+
);
}
diff --git a/frontend/src/widgets/ThumbstickSenderWidget/WidgetDescriptor.ts b/frontend/src/widgets/ThumbstickSenderWidget/WidgetDescriptor.ts
new file mode 100644
index 0000000..c096859
--- /dev/null
+++ b/frontend/src/widgets/ThumbstickSenderWidget/WidgetDescriptor.ts
@@ -0,0 +1,27 @@
+import type { StoreType } from "@/mosaic/store/interface/mosaic-store.ts";
+
+import { WidgetDescriptor } from "@/components/Dashboard/Widgets/WidgetDescriptor.ts";
+
+export type ThumbstickParams = {
+ holonomic: boolean;
+};
+
+export default class ThumbstickSenderWidgetDescriptor extends WidgetDescriptor {
+ public getName(): string {
+ return "ThumbstickSenderWidget";
+ }
+
+ public getRequiredStoreType(): StoreType {
+ return "sendable";
+ }
+
+ public supportCustomParams(): boolean {
+ return true;
+ }
+
+ public getDefaultParams(): ThumbstickParams {
+ return {
+ holonomic: false,
+ };
+ }
+}
diff --git a/frontend/src/widgets/WASDSenderWidget/WidgetDescriptor.ts b/frontend/src/widgets/WASDSenderWidget/WidgetDescriptor.ts
index 7b9c42b..429d5ac 100644
--- a/frontend/src/widgets/WASDSenderWidget/WidgetDescriptor.ts
+++ b/frontend/src/widgets/WASDSenderWidget/WidgetDescriptor.ts
@@ -10,4 +10,4 @@ export default class WASDSenderWidgetDescriptor extends WidgetDescriptor {
public getRequiredStoreType(): StoreType {
return "sendable";
}
-}
\ No newline at end of file
+}