diff --git a/apps/control-station/src/App.tsx b/apps/control-station/src/App.tsx index 98f49a9..a0c5609 100644 --- a/apps/control-station/src/App.tsx +++ b/apps/control-station/src/App.tsx @@ -137,6 +137,14 @@ export default function App() { runtime.state?.observationSources ?? [], runtime.setObservationSourceActive, ); + const focusedObservationSource = runtime.state?.observationSources?.find( + (source) => source.id === observationLayout.focusedSourceId, + ); + const observationFullscreenActive = Boolean( + activeDefinition?.kind === "spatial" + ? observationLayout.maximizedFloatingSourceId || focusedObservationSource?.modality === "point-cloud" + : activeDefinition?.kind === "cameras" && focusedObservationSource, + ); useEffect(() => { const remote = runtime.state?.viewerSettings; @@ -290,6 +298,7 @@ export default function App() { ; rect?: ObservationWindowRect; maximized: boolean; @@ -37,10 +72,33 @@ export function FloatingObservationWindow({ onActivate: () => void; onClose: () => void; }) { + const [bounds, setBounds] = useState<{ width: number; height: number } | null>(null); + + useLayoutEffect(() => { + const element = boundsRef.current; + if (!element) return; + + const measure = () => { + const next = { width: element.clientWidth, height: element.clientHeight }; + setBounds((current) => ( + current?.width === next.width && current.height === next.height ? current : next + )); + }; + + measure(); + const observer = new ResizeObserver(measure); + observer.observe(element); + return () => observer.disconnect(); + }, [boundsRef]); + + const initialRect = bounds ? initialObservationWindowRect(index, count, bounds) : null; + const windowRect = rect ?? initialRect; + if (!windowRect) return null; + return ( {source.capabilities.timelineMode === "live-only" ? "Эфир без буфера" : "Временная шкала"} )} - minWidth={280} - minHeight={190} + minWidth={Math.min(280, windowRect.width)} + minHeight={Math.min(190, windowRect.height)} resizable={source.capabilities.resizable} active={active} zIndex={maximized ? 15 : active ? 9 : 7} diff --git a/apps/control-station/src/styles/base.css b/apps/control-station/src/styles/base.css index 8497ff2..76c3e63 100644 --- a/apps/control-station/src/styles/base.css +++ b/apps/control-station/src/styles/base.css @@ -15,6 +15,7 @@ body, body { margin: 0; + font-family: var(--nodedc-font-family); } #root[data-nodedc-theme="dark"], @@ -331,7 +332,6 @@ body { display: grid; grid-template-columns: minmax(12rem, 0.9fr) minmax(0, 1.1fr); gap: 1rem; - border-top: 1px solid var(--station-hairline); padding: 0.7rem 0; } diff --git a/apps/control-station/src/styles/device.css b/apps/control-station/src/styles/device.css index ee1116b..c3e3137 100644 --- a/apps/control-station/src/styles/device.css +++ b/apps/control-station/src/styles/device.css @@ -20,7 +20,6 @@ display: grid; min-width: 0; gap: 0.9rem; - border: 1px solid var(--station-hairline); border-radius: 1rem; background: rgb(255 255 255 / 0.025); padding: 1rem; @@ -74,8 +73,6 @@ .device-model-card dl { display: grid; gap: 0.42rem; - border-top: 1px solid var(--station-hairline); - border-bottom: 1px solid var(--station-hairline); padding: 0.7rem 0; } @@ -441,7 +438,6 @@ justify-content: space-between; gap: 1rem; margin-top: 1rem; - border-top: 1px solid var(--station-hairline); padding-top: 0.9rem; } @@ -469,7 +465,6 @@ min-width: 0; grid-template-columns: minmax(7rem, 0.72fr) minmax(0, 1.28fr); gap: 1rem; - border-top: 1px solid var(--station-hairline); padding: 0.7rem 0; } diff --git a/apps/control-station/src/styles/observation.css b/apps/control-station/src/styles/observation.css index f73c2db..04d8ee0 100644 --- a/apps/control-station/src/styles/observation.css +++ b/apps/control-station/src/styles/observation.css @@ -69,6 +69,7 @@ .observation-source-menu { overflow: hidden; + font-family: var(--nodedc-font-family); } .observation-source-menu__head, @@ -81,7 +82,6 @@ align-items: flex-end; justify-content: space-between; gap: 1rem; - border-bottom: 1px solid var(--station-hairline); } .observation-source-menu__head strong, @@ -92,13 +92,13 @@ .observation-source-menu__head strong { margin-top: 0.34rem; color: var(--nodedc-text-primary); - font-size: 0.76rem; + font-size: var(--nodedc-font-size-md); } .observation-source-menu__head small, .observation-source-menu__foot { color: var(--nodedc-text-muted); - font-size: 0.55rem; + font-size: var(--nodedc-font-size-xs); line-height: 1.4; } @@ -176,7 +176,7 @@ i[data-availability="error"] { } .observation-source-menu__foot { - border-top: 1px solid var(--station-hairline); + border-top: 0; } .observation-media__asset, @@ -307,13 +307,21 @@ i[data-availability="error"] { background: #06070a; } +.floating-observation-window.nodedc-material-rim::after { + display: none; +} + +.floating-observation-window[data-active="true"] { + box-shadow: 0 1.5rem 4rem rgb(0 0 0 / 0.34); +} + .observation-timeline { display: grid; min-width: 0; grid-template-columns: auto auto minmax(0, 1fr) auto auto; align-items: center; gap: 0.6rem; - border: 1px solid rgb(255 255 255 / 0.055); + border: 0; border-radius: 0.9rem; background: rgb(9 10 13 / 0.78); padding: 0.4rem; @@ -432,7 +440,7 @@ i[data-availability="error"] { min-height: 18rem; border-radius: 1rem; background: #07080a; - box-shadow: inset 0 0 0 1px var(--station-hairline); + box-shadow: none; } .camera-slot { diff --git a/apps/control-station/src/styles/shell.css b/apps/control-station/src/styles/shell.css index a7832e0..37032be 100644 --- a/apps/control-station/src/styles/shell.css +++ b/apps/control-station/src/styles/shell.css @@ -29,6 +29,36 @@ background: rgb(var(--nodedc-danger-rgb)); } +.control-station[data-observation-fullscreen="true"] .nodedc-app-shell__content { + z-index: calc(var(--nodedc-layer-panel) + 1); + right: var(--nodedc-app-page-pad); + left: var(--nodedc-app-page-pad); + width: auto; + transition: none; +} + +.control-station[data-observation-fullscreen="true"] .nodedc-app-shell__navigation { + pointer-events: none; +} + +.control-station[data-observation-fullscreen="true"] .nodedc-application-panel { + grid-template-rows: minmax(0, 1fr); + gap: 0; + background: #06070a; + padding: 0; + box-shadow: none; + animation: none; +} + +.control-station[data-observation-fullscreen="true"] .nodedc-application-panel__head { + display: none; +} + +.control-station[data-observation-fullscreen="true"] .nodedc-application-panel__body { + height: 100%; + overflow: hidden; +} + .landing-stage { position: relative; min-width: 0; @@ -79,7 +109,6 @@ display: grid; width: min(24rem, 30vw); gap: 0.6rem; - border: 1px solid rgb(255 255 255 / 0.055); border-radius: 1.25rem; background: rgb(10 10 12 / 0.58); padding: 1rem; diff --git a/apps/control-station/src/styles/spatial.css b/apps/control-station/src/styles/spatial.css index 1e204ae..5919e91 100644 --- a/apps/control-station/src/styles/spatial.css +++ b/apps/control-station/src/styles/spatial.css @@ -32,7 +32,7 @@ overflow: hidden; border-radius: 1rem; background: #06070a; - box-shadow: inset 0 0 0 1px var(--station-hairline); + box-shadow: none; } .rerun-viewport, @@ -45,6 +45,19 @@ min-height: 0; } +.rerun-viewport { + --rerun-tab-bar-height: 24px; + overflow: hidden; +} + +/* Rerun WebViewer 0.34.1 draws its selected recording tab inside WASM. + The native panels are already disabled; crop only that fixed tab row. */ +.rerun-viewport__canvas { + top: calc(-1 * var(--rerun-tab-bar-height)); + bottom: auto; + height: calc(100% + var(--rerun-tab-bar-height)); +} + .rerun-viewport__canvas canvas { display: block; width: 100% !important; @@ -63,7 +76,7 @@ display: flex; align-items: center; gap: 0.8rem; - border: 1px solid var(--station-hairline); + border: 0; border-radius: 1rem; background: rgb(10 11 14 / 0.82); padding: 0.9rem 1rem; @@ -179,7 +192,7 @@ width: 3.4rem; height: 3.4rem; place-items: center; - border: 1px solid var(--station-hairline-strong); + border: 0; border-radius: 50%; background: rgb(255 255 255 / 0.035); color: var(--nodedc-text-secondary); @@ -204,7 +217,7 @@ .scene-timeline { position: absolute; z-index: 3; - border: 1px solid rgb(255 255 255 / 0.055); + border: 0; background: rgb(9 10 13 / 0.74); backdrop-filter: blur(16px); } @@ -240,7 +253,7 @@ align-items: baseline; justify-content: space-between; gap: 1rem; - border-bottom: 1px solid var(--station-hairline); + border-bottom: 0; padding: 0.38rem 0; } diff --git a/apps/control-station/src/styles/workspaces.css b/apps/control-station/src/styles/workspaces.css index 7aee791..3e8d568 100644 --- a/apps/control-station/src/styles/workspaces.css +++ b/apps/control-station/src/styles/workspaces.css @@ -16,7 +16,6 @@ min-width: 0; grid-template-columns: minmax(14rem, 0.72fr) minmax(0, 1.6fr); gap: 1.6rem; - border-top: 1px solid var(--station-hairline); padding-top: 1.2rem; } @@ -47,7 +46,6 @@ grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 0.8rem; - border-bottom: 1px solid var(--station-hairline); padding: 0.78rem 0; } @@ -270,7 +268,7 @@ overflow: hidden; border-radius: 1rem; background: #090a0c; - box-shadow: inset 0 0 0 1px var(--station-hairline); + box-shadow: none; } .camera-slot[data-primary="true"] { @@ -347,7 +345,7 @@ overflow: hidden; border-radius: 1rem; background: #0a0b0d; - box-shadow: inset 0 0 0 1px var(--station-hairline); + box-shadow: none; } .map-canvas__grid, @@ -484,7 +482,6 @@ } .timeline-ruler span { - border-left: 1px solid var(--station-hairline); padding-left: 0.2rem; } @@ -498,7 +495,6 @@ grid-template-columns: 7.5rem minmax(0, 1fr) 10rem; align-items: center; gap: 1rem; - border-top: 1px solid var(--station-hairline); padding: 0.82rem 0; } diff --git a/apps/control-station/src/workspaces/Workspaces.tsx b/apps/control-station/src/workspaces/Workspaces.tsx index 4a7d62b..30201b8 100644 --- a/apps/control-station/src/workspaces/Workspaces.tsx +++ b/apps/control-station/src/workspaces/Workspaces.tsx @@ -301,7 +301,10 @@ function SpatialWorkspace({ const viewerStatusTone = viewerStatus === "ready" ? "success" : viewerStatus === "error" ? "danger" : "neutral"; return ( -
+
СЦЕНА 3D · RERUN @@ -325,120 +328,125 @@ function SpatialWorkspace({ data-primary-focused={pointCloudFocused ? "true" : undefined} data-media-maximized={floatingSourceMaximized ? "true" : undefined} > - {sourceUrl.trim() && pointCloudVisible ? ( - - ) : ( - - )} - - {pointCloudFocused ? ( - - ) : !floatingSourceMaximized ? ( -
- - {pointCloudSource?.capabilities.fullscreen && pointCloudVisible && sourceUrl.trim() ? ( - - ) : null} -
- ) : null} + ) : ( + + )} -
- ВИЗУАЛЬНЫЙ ДВИЖОК - {viewerStatusLabel} - {viewerMessage ? {viewerMessage} : null} -
+ {pointCloudFocused ? ( + + ) : !floatingSourceMaximized ? ( +
+ + {pointCloudSource?.capabilities.fullscreen && pointCloudVisible && sourceUrl.trim() ? ( + + ) : null} +
+ ) : null} -
-
- КАДР/С - {formatNumber(frameRate)} +
+ ВИЗУАЛЬНЫЙ ДВИЖОК + {viewerStatusLabel} + {viewerMessage ? {viewerMessage} : null}
-
- Точек - {points === null ? "—" : points.toLocaleString("ru-RU", { maximumFractionDigits: 0 })} -
-
- До публикации - {formatNumber(latency)} мс -
-
- {!pointCloudFocused && !floatingSourceMaximized && state?.sourceMode && state.sourceMode !== "idle" && !sourceUrl.trim() ? ( -
- - - Локальный поток {sourceModeLabel(state.sourceMode).toLocaleLowerCase("ru-RU")} активен, - Rerun-мост запускается и опубликует адрес автоматически. - +
+
+ КАДР/С + {formatNumber(frameRate)} +
+
+ Точек + {points === null ? "—" : points.toLocaleString("ru-RU", { maximumFractionDigits: 0 })} +
+
+ До публикации + {formatNumber(latency)} мс +
- ) : null} - {!pointCloudFocused && !floatingSourceMaximized && selection ? ( -
- Выбрано - {selection.entityPath} - {selection.viewName ? {selection.viewName} : null} -
- ) : null} + {!pointCloudFocused && !floatingSourceMaximized && state?.sourceMode && state.sourceMode !== "idle" && !sourceUrl.trim() ? ( +
+ + + Локальный поток {sourceModeLabel(state.sourceMode).toLocaleLowerCase("ru-RU")} активен, + Rerun-мост запускается и опубликует адрес автоматически. + +
+ ) : null} - {!pointCloudFocused && !floatingSourceMaximized ? ( - - ) : null} + {!pointCloudFocused && !floatingSourceMaximized && selection ? ( +
+ Выбрано + {selection.entityPath} + {selection.viewName ? {selection.viewName} : null} +
+ ) : null} - {!pointCloudFocused ? visibleMediaSources.map((source, index) => ( - observationLayout.setWindowRect(source.id, rect)} - onMaximizedChange={(maximized) => - observationLayout.setFloatingMaximized(source.id, maximized)} - onActivate={() => observationLayout.activateFloatingSource(source.id)} - onClose={() => void observationLayout.hideSource(source.id)} - /> - )) : null} + {!pointCloudFocused && !floatingSourceMaximized ? ( + + ) : null} + + {!pointCloudFocused ? visibleMediaSources.map((source, index) => ( + observationLayout.setWindowRect(source.id, rect)} + onMaximizedChange={(maximized) => + observationLayout.setFloatingMaximized(source.id, maximized)} + onActivate={() => observationLayout.activateFloatingSource(source.id)} + onClose={() => { + if (observationLayout.pendingSourceIds.has(source.id)) return; + observationLayout.setFloatingMaximized(source.id, false); + void observationLayout.hideSource(source.id); + }} + /> + )) : null}
@@ -461,6 +469,7 @@ function CameraSourceCard({ selectedPeer, onToggle, onFocus, + onClose, }: { source: ObservationSourceDescriptor; focused: boolean; @@ -469,6 +478,7 @@ function CameraSourceCard({ selectedPeer: boolean; onToggle: () => void; onFocus: () => void; + onClose: () => void; }) { const deliveryActive = Boolean( (source.delivery || source.previewUrl) && @@ -500,7 +510,7 @@ function CameraSourceCard({