feat: finalize corrected-route planning and Rerun recording review

This commit is contained in:
DCCONSTRUCTIONS
2026-09-22 10:10:03 +03:00
parent c804d89b18
commit 2e5d52521f
132 changed files with 14141 additions and 898 deletions
@@ -43,8 +43,7 @@ export function initialObservationWindowRect(
const row = Math.floor(index / columns);
const column = index % columns;
const rowItemCount = Math.min(columns, Math.max(1, count - row * columns));
const rowWidth = rowItemCount * width + Math.max(0, rowItemCount - 1) * WINDOW_GAP;
const rowStart = Math.max(0, bounds.width - WINDOW_INSET - rowWidth);
const rowStart = Math.min(WINDOW_INSET, Math.max(0, bounds.width - width));
return {
x: rowStart + Math.min(column, rowItemCount - 1) * (width + WINDOW_GAP),
@@ -1,4 +1,5 @@
import { Button, Icon, Select } from "@nodedc/ui-react";
import { MAX_ACCUMULATION_SECONDS } from "./sceneSettings";
type ObservationTimelineMode = "live-only" | "buffered" | "recorded";
@@ -18,6 +19,7 @@ export function ObservationTimeline({
playbackRate,
onPlaybackRateChange,
accumulationSeconds,
accumulationMaxSeconds = MAX_ACCUMULATION_SECONDS,
onAccumulationChange,
onAccumulationCommit,
className = "",
@@ -37,6 +39,7 @@ export function ObservationTimeline({
playbackRate?: number;
onPlaybackRateChange?: (rate: number) => void;
accumulationSeconds?: number;
accumulationMaxSeconds?: number;
onAccumulationChange?: (value: number) => void;
onAccumulationCommit?: () => void;
className?: string;
@@ -75,7 +78,7 @@ export function ObservationTimeline({
className="observation-timeline__track"
type="range"
min={0}
max={120}
max={Math.max(1, accumulationMaxSeconds, accumulationValue)}
step={1}
value={accumulationValue}
aria-label="Окно накопления облака точек"
@@ -165,12 +168,15 @@ export function ObservationTimeline({
export function normalizeAccumulationSeconds(value: number): number {
if (!Number.isFinite(value)) return 0;
return Math.min(120, Math.max(0, Math.round(value)));
return Math.max(0, Math.round(value));
}
export function formatAccumulationDuration(value: number): string {
const seconds = normalizeAccumulationSeconds(value);
return seconds === 0 ? "Кадр" : `${seconds} с`;
if (seconds === 0) return "Кадр";
if (seconds < 60) return `${seconds} с`;
const minutes = Math.floor(seconds / 60);
return seconds % 60 === 0 ? `${minutes} мин` : `${minutes} мин ${seconds % 60} с`;
}
export function normalizeTimelineRange(
@@ -1,5 +1,7 @@
import {Checker,ColorField,ControlRow,Inspector,RangeControl,Select} from '@nodedc/ui-react';
import type {SceneSettings,PointColorMode,PointPalette} from './sceneSettings';
import {MAX_ACCUMULATION_SECONDS} from './sceneSettings';
import {formatAccumulationDuration} from './ObservationTimeline';
const colorModeOptions: Array<{ value: PointColorMode; label: string; description: string }> = [
{ value: "intensity", label: "Интенсивность", description: "Значение отражённого сигнала" },
@@ -28,7 +30,7 @@ export function SceneDisplayControls({displayDraft,stageDisplayPatch,commitDispl
{
id: "points",
label: "Облако точек",
description: "Размер и способ окрашивания",
description: "Размер, плотность и окрашивание",
content: (
<div className="inspector-control-stack">
<div
@@ -47,6 +49,17 @@ export function SceneDisplayControls({displayDraft,stageDisplayPatch,commitDispl
onChange={(pointSize) => stageDisplayPatch({ pointSize })}
/>
</div>
{replayPresented ? <>
<RangeControl
label="Прореживание облака"
value={displayDraft.pointDecimationPercent ?? 0}
min={0} max={100} step={0.1}
exactValueBounds={{min: 0, max: 100}}
formatValue={(value) => `${value.toLocaleString('ru-RU')} %`}
onChange={(pointDecimationPercent) => stageDisplayPatch({pointDecimationPercent})}
/>
<p className="scene-window-note">0% все точки, 100% без точек. Предпросмотр обновляется после изменения. Настройки сохраняются при закрытии окна; исходная запись не меняется.</p>
</> : null}
<div className="scene-settings-commit-field" onBlur={flushDisplaySettings}>
<ControlRow label="Атрибут цвета">
<Select
@@ -85,12 +98,6 @@ export function SceneDisplayControls({displayDraft,stageDisplayPatch,commitDispl
</ControlRow>
</div>
) : null}
{replayPresented ? (
<p className="scene-window-note">
Первый новый режим читает индекс архивных точек. Следующие палитры
переключаются из подготовленного цветового кэша без переэкспорта геометрии.
</p>
) : null}
</div>
),
},
@@ -100,6 +107,18 @@ export function SceneDisplayControls({displayDraft,stageDisplayPatch,commitDispl
description: "История облака и траектория",
content: (
<div className="inspector-control-stack">
<RangeControl
label="Диапазон шкалы, мин"
value={(displayDraft.accumulationMaxSeconds ?? MAX_ACCUMULATION_SECONDS) / 60}
min={1} max={30} step={0.1}
exactValueBounds={{min: 1 / 60}}
formatValue={(value) => `${value.toLocaleString('ru-RU', {maximumFractionDigits: 2})} мин`}
onChange={(minutes) => {
const accumulationMaxSeconds = Math.max(1, Math.round(minutes * 60));
stageDisplayPatch({accumulationMaxSeconds,
accumulationSeconds: Math.min(displayDraft.accumulationSeconds, accumulationMaxSeconds)});
}}
/>
<div
className="scene-settings-commit-field"
onPointerUp={flushDisplaySettings}
@@ -110,10 +129,12 @@ export function SceneDisplayControls({displayDraft,stageDisplayPatch,commitDispl
label="Окно накопления"
value={displayDraft.accumulationSeconds}
min={0}
max={120}
max={displayDraft.accumulationMaxSeconds ?? MAX_ACCUMULATION_SECONDS}
exactValueBounds={{min: 0}}
step={1}
formatValue={(value) => (value === 0 ? "Только кадр" : `${value} с`)}
onChange={(accumulationSeconds) => stageDisplayPatch({ accumulationSeconds })}
formatValue={formatAccumulationDuration}
onChange={(accumulationSeconds) => stageDisplayPatch({ accumulationSeconds,
accumulationMaxSeconds: Math.max(accumulationSeconds, displayDraft.accumulationMaxSeconds ?? MAX_ACCUMULATION_SECONDS) })}
/>
</div>
<div className="nodedc-field">
+1 -4
View File
@@ -3,13 +3,11 @@ import {StatusBadge} from '@nodedc/ui-react';
/** The existing scene composition. Hosts supply transport/renderers and authority. */
export function SpatialScene({viewportRef, focused, primaryFocused, mediaMaximized, toolbar,
renderer, deviceControls, sourceControls, status, metrics, timeline, media, overlays, footer,
navigationReady=false}: {
renderer, deviceControls, sourceControls, status, metrics, timeline, media, overlays, footer}: {
viewportRef: RefObject<HTMLDivElement|null>; focused?:boolean; primaryFocused?:boolean;
mediaMaximized?:boolean; toolbar:ReactNode; renderer:ReactNode; deviceControls?:ReactNode;
sourceControls?:ReactNode; status:{label:string;tone:'neutral'|'success'|'warning'|'danger';message?:string;pulse?:boolean};
metrics:ReactNode; timeline?:ReactNode; media?:ReactNode; overlays?:ReactNode; footer?:ReactNode;
navigationReady?:boolean;
}) {
const detailsHidden = primaryFocused || mediaMaximized;
return <div className="spatial-workspace" data-focused={focused?'true':undefined}>
@@ -28,7 +26,6 @@ export function SpatialScene({viewportRef, focused, primaryFocused, mediaMaximiz
<div className="scene-metrics" aria-label="Метрики пространственной сцены">{metrics}</div>
</div>
{overlays}
{navigationReady&&!mediaMaximized&&<div className="scene-navigation-hint" aria-label="Навигация по 3D-сцене"><span>Колесо · зум к курсору</span><span>WASD · свободный проход</span></div>}
{timeline}{media}
</div>
{footer}
-19
View File
@@ -32,25 +32,6 @@
backdrop-filter: blur(18px);
}
.scene-navigation-hint {
position: absolute;
z-index: 11;
right: 0.85rem;
bottom: 4.9rem;
display: flex;
align-items: center;
gap: 0.75rem;
border: 1px solid rgb(255 255 255 / 0.07);
border-radius: 999px;
background: rgb(9 10 13 / 0.7);
color: var(--nodedc-text-muted);
padding: 0.42rem 0.65rem;
font-size: 0.52rem;
font-weight: 680;
pointer-events: none;
backdrop-filter: blur(14px);
}
.scene-source-picker__trigger {
position: relative;
}
+7
View File
@@ -2,9 +2,14 @@ export type SceneProjection = "3d" | "2d" | "map";
export type PointColorMode = "intensity" | "height" | "distance" | "rgb" | "class";
export type PointPalette = "turbo" | "viridis" | "plasma" | "grayscale" | "custom";
/** Default slider travel, not a limit on a recording or exact entry. */
export const MAX_ACCUMULATION_SECONDS = 3 * 60;
export interface SceneSettings {
projection: SceneProjection;
pointSize: number;
pointDecimationPercent?: number;
accumulationMaxSeconds?: number;
colorMode: PointColorMode;
palette: PointPalette;
customColor: string;
@@ -19,6 +24,8 @@ export interface SceneSettings {
export const defaultSceneSettings: SceneSettings = {
projection: "3d",
pointSize: 2.5,
pointDecimationPercent: 0,
accumulationMaxSeconds: MAX_ACCUMULATION_SECONDS,
colorMode: "intensity",
palette: "turbo",
customColor: "#35d7c1",
+8 -5
View File
@@ -367,16 +367,19 @@
.scene-operation-status-stack {
position: absolute;
z-index: 11;
left: 0.85rem;
bottom: 7.15rem;
right: 0.85rem;
top: 0.85rem;
min-height: 2.75rem;
max-width: calc(100% - 8rem);
display: grid;
justify-items: start;
align-content: center;
justify-items: end;
gap: 0.3rem;
}
.scene-operation-status {
display: inline-flex;
max-width: min(24rem, calc(100% - 1.7rem));
max-width: 100%;
align-items: center;
gap: 0.42rem;
border: 1px solid rgb(255 255 255 / 0.07);
@@ -388,7 +391,7 @@
font-size: 0.52rem;
font-weight: 680;
line-height: 1;
white-space: nowrap;
white-space: normal;
backdrop-filter: blur(14px);
}