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} /> + 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 */} - - - - - - {data && } - - - - - - - - {/* 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 */} + + + + + + {data && } + + + + + + + + {/* 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} - - ) : ( - <> - ); -} \ 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 +}