feat(map): adopt DC Default scene

This commit is contained in:
DCCONSTRUCTIONS
2026-07-29 17:55:44 +03:00
parent 5a06194962
commit b91d3b913e
9 changed files with 1299 additions and 331 deletions
@@ -0,0 +1,681 @@
import {
Checker,
ColorField,
ControlRow,
Inspector,
RangeControl,
StatusBadge,
} from "@nodedc/ui-react";
import type {
MapInspectorSection,
MapViewDocument,
} from "../../core/map/mapView";
import type { useMapGatewayHealth } from "../../core/map/useMapGatewayHealth";
export const editableMapInspectorSections = new Set<MapInspectorSection>([
"base-terrain",
"atmosphere-light",
"buildings",
"grid-lod",
"camera",
"tile-cache",
]);
export function MapSettingsInspector({
draft,
updateDraft,
gatewayHealth,
}: {
draft: MapViewDocument;
updateDraft: (
update: (current: MapViewDocument) => MapViewDocument,
) => void;
gatewayHealth: ReturnType<typeof useMapGatewayHealth>;
}) {
return (
<Inspector
singleOpen
sections={createInspectorSections(draft, updateDraft, gatewayHealth)}
openSections={draft.view.inspectorOpenSections.filter(
(section) => editableMapInspectorSections.has(section),
)}
activeId={draft.view.inspectorOpenSections[0]}
onOpenSectionsChange={(sections) => {
updateDraft((current) => {
current.view.inspectorOpenSections = sections.filter(
(section): section is MapInspectorSection =>
editableMapInspectorSections.has(section as MapInspectorSection),
).slice(0, 1);
return current;
});
}}
/>
);
}
function createInspectorSections(
draft: MapViewDocument,
updateDraft: (
update: (current: MapViewDocument) => MapViewDocument,
) => void,
gatewayHealth: ReturnType<typeof useMapGatewayHealth>,
) {
const view = draft.view;
const settings = view.visualSettings;
return [
{
id: "base-terrain",
label: "Подложка и terrain",
description: "provider-neutral surface",
content: (
<div className="mission-map__inspector-controls">
<ControlRow label="Подложка">
<strong>Cesium World Imagery</strong>
</ControlRow>
<Checker
label="Спутниковая подложка"
checked={view.layerVisibility.imagery}
onChange={(checked) => updateDraft((current) => {
current.view.layerVisibility.imagery = checked;
return current;
})}
/>
<Checker
label="Terrain"
checked={view.layerVisibility.terrain}
onChange={(checked) => updateDraft((current) => {
current.view.layerVisibility.terrain = checked;
return current;
})}
/>
<RangeControl
label="Вертикальное преувеличение рельефа"
value={settings.terrainExaggeration * 100}
min={25}
max={300}
step={1}
formatValue={(value) => `${(value / 100).toFixed(2)}×`}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.terrainExaggeration = value / 100;
return current;
})}
/>
<Checker
label="Монохромная поверхность"
checked={settings.monochromeEnabled}
onChange={(checked) => updateDraft((current) => {
current.view.visualSettings.monochromeEnabled = checked;
return current;
})}
/>
<ControlRow label="Цвет монохрома">
<ColorField
label="Цвет монохромной поверхности"
value={settings.monochromeColor}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.monochromeColor = value;
return current;
})}
/>
</ControlRow>
<RangeControl
label="Яркость"
value={settings.imageryBrightness}
min={0}
max={200}
formatValue={(value) => `${value}%`}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.imageryBrightness = value;
return current;
})}
/>
<RangeControl
label="Контраст"
value={settings.imageryContrast}
min={0}
max={200}
formatValue={(value) => `${value}%`}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.imageryContrast = value;
return current;
})}
/>
<RangeControl
label="Насыщенность"
value={settings.imagerySaturation}
min={0}
max={200}
formatValue={(value) => `${value}%`}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.imagerySaturation = value;
return current;
})}
/>
<RangeControl
label="Гамма"
value={settings.imageryGamma}
min={0}
max={300}
formatValue={(value) => `${value}%`}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.imageryGamma = value;
return current;
})}
/>
<RangeControl
label="Оттенок"
value={settings.imageryHue}
min={-180}
max={180}
formatValue={(value) => `${value}°`}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.imageryHue = value;
return current;
})}
/>
<RangeControl
label="Прозрачность imagery"
value={settings.imageryAlpha}
min={0}
max={100}
formatValue={(value) => `${value}%`}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.imageryAlpha = value;
return current;
})}
/>
<ControlRow label="Цвет планеты">
<ColorField
label="Цвет terrain без imagery"
value={settings.globeColor}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.globeColor = value;
return current;
})}
/>
</ControlRow>
<ControlRow label="Фон сцены">
<ColorField
label="Цвет фона сцены"
value={settings.backgroundColor}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.backgroundColor = value;
return current;
})}
/>
</ControlRow>
<RangeControl
label="Высота карты"
value={view.mapHeight}
min={420}
max={1080}
step={1}
formatValue={(value) => `${value} px`}
onChange={(value) => updateDraft((current) => {
current.view.mapHeight = value;
return current;
})}
/>
</div>
),
},
{
id: "atmosphere-light",
label: "Атмосфера и освещение",
description: "scene / color correction",
content: (
<div className="mission-map__inspector-controls">
<Checker
label="Показывать атмосферу"
checked={settings.atmosphereEnabled}
onChange={(checked) => updateDraft((current) => {
current.view.visualSettings.atmosphereEnabled = checked;
return current;
})}
/>
<RangeControl
label="Атмосфера: оттенок"
value={settings.atmosphereHue}
min={-100}
max={100}
formatValue={(value) => `${value}%`}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.atmosphereHue = value;
return current;
})}
/>
<RangeControl
label="Атмосфера: насыщенность"
value={settings.atmosphereSaturation}
min={-100}
max={100}
formatValue={(value) => `${value}%`}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.atmosphereSaturation = value;
return current;
})}
/>
<RangeControl
label="Атмосфера: яркость"
value={settings.atmosphereBrightness}
min={-100}
max={100}
formatValue={(value) => `${value}%`}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.atmosphereBrightness = value;
return current;
})}
/>
<Checker
label="Туман"
checked={settings.fogEnabled}
onChange={(checked) => updateDraft((current) => {
current.view.visualSettings.fogEnabled = checked;
return current;
})}
/>
<RangeControl
label="Плотность тумана"
value={settings.fogDensity}
min={0}
max={100}
formatValue={(value) => `${(value / 10_000).toFixed(4)}`}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.fogDensity = value;
return current;
})}
/>
<Checker
label="Солнечное освещение"
checked={settings.sunEnabled}
onChange={(checked) => updateDraft((current) => {
current.view.visualSettings.sunEnabled = checked;
return current;
})}
/>
<RangeControl
label="Час солнца"
value={settings.sunHour}
min={0}
max={24}
formatValue={(value) => `${value}:00 UTC`}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.sunHour = value;
return current;
})}
/>
<RangeControl
label="Интенсивность света"
value={settings.sunIntensity}
min={0}
max={200}
formatValue={(value) => `${value}%`}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.sunIntensity = value;
return current;
})}
/>
<Checker
label="Тени"
checked={settings.shadowsEnabled}
onChange={(checked) => updateDraft((current) => {
current.view.visualSettings.shadowsEnabled = checked;
return current;
})}
/>
</div>
),
},
{
id: "buildings",
label: "3D здания",
description: "3D Tiles / detail",
content: (
<div className="mission-map__inspector-controls">
<Checker
label="Показывать 3D здания"
checked={view.layerVisibility.buildings}
onChange={(checked) => updateDraft((current) => {
current.view.layerVisibility.buildings = checked;
return current;
})}
/>
<ControlRow label="Цвет">
<ColorField
label="Цвет зданий"
value={settings.buildingsColor}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.buildingsColor = value;
return current;
})}
/>
</ControlRow>
<RangeControl
label="Прозрачность"
value={Math.round(settings.buildingsOpacity * 100)}
min={0}
max={100}
formatValue={(value) => `${value}%`}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.buildingsOpacity = value / 100;
return current;
})}
/>
<RangeControl
label="Детализация"
value={settings.buildingsMaximumScreenSpaceError}
min={4}
max={32}
formatValue={(value) => `SSE ${value}`}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.buildingsMaximumScreenSpaceError = value;
return current;
})}
/>
</div>
),
},
{
id: "grid-lod",
label: "Сетка и LOD",
description: "first adapter control",
content: (
<div className="mission-map__inspector-controls">
<Checker
label="3D-сетка"
checked={view.layerVisibility.grid}
onChange={(checked) => updateDraft((current) => {
current.view.layerVisibility.grid = checked;
return current;
})}
/>
<Checker
label="LOD по высоте камеры"
checked={settings.gridLodEnabled}
onChange={(checked) => updateDraft((current) => {
current.view.visualSettings.gridLodEnabled = checked;
return current;
})}
/>
<RangeControl
label="Высота над поверхностью"
value={settings.gridHeightMeters}
min={0}
max={1000}
formatValue={(value) => `${value} м`}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.gridHeightMeters = value;
return current;
})}
/>
<RangeControl
label="LOD 1: до высоты"
value={settings.gridLod1MaxHeightKm}
min={1}
max={50}
formatValue={(value) => `${value} км`}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.gridLod1MaxHeightKm = value;
return current;
})}
/>
<RangeControl
label="LOD 1: шаг"
value={settings.gridLod1StepKm}
min={1}
max={10}
formatValue={(value) => `${value} км`}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.gridLod1StepKm = value;
return current;
})}
/>
<RangeControl
label="LOD 2: до высоты"
value={settings.gridLod2MaxHeightKm}
min={10}
max={200}
formatValue={(value) => `${value} км`}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.gridLod2MaxHeightKm = value;
return current;
})}
/>
<RangeControl
label="LOD 2: шаг"
value={settings.gridLod2StepKm}
min={1}
max={25}
formatValue={(value) => `${value} км`}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.gridLod2StepKm = value;
return current;
})}
/>
<RangeControl
label="LOD 3: шаг"
value={settings.gridLod3StepKm}
min={5}
max={100}
formatValue={(value) => `${value} км`}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.gridLod3StepKm = value;
return current;
})}
/>
<RangeControl
label="Радиус сетки"
value={settings.gridRadiusKm}
min={5}
max={150}
formatValue={(value) => `${value} км`}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.gridRadiusKm = value;
return current;
})}
/>
<ControlRow label="Цвет линий">
<ColorField
label="Цвет линий сетки"
value={settings.gridColor}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.gridColor = value;
return current;
})}
/>
</ControlRow>
<RangeControl
label="Толщина линий"
value={settings.gridLineWidth}
min={1}
max={8}
formatValue={(value) => `${value} px`}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.gridLineWidth = value;
return current;
})}
/>
<RangeControl
label="Прозрачность сетки"
value={settings.gridOpacity}
min={0}
max={100}
formatValue={(value) => `${value}%`}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.gridOpacity = value;
return current;
})}
/>
<Checker
label="Точки в пересечениях"
checked={settings.gridDotsEnabled}
onChange={(checked) => updateDraft((current) => {
current.view.visualSettings.gridDotsEnabled = checked;
return current;
})}
/>
<RangeControl
label="Размер точки"
value={settings.gridDotsSize}
min={2}
max={28}
formatValue={(value) => `${value} px`}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.gridDotsSize = value;
return current;
})}
/>
<ControlRow label="Цвет точек">
<ColorField
label="Цвет точек сетки"
value={settings.gridDotsColor}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.gridDotsColor = value;
return current;
})}
/>
</ControlRow>
<RangeControl
label="Прозрачность точек"
value={settings.gridDotsOpacity}
min={0}
max={100}
formatValue={(value) => `${value}%`}
onChange={(value) => updateDraft((current) => {
current.view.visualSettings.gridDotsOpacity = value;
return current;
})}
/>
</div>
),
},
{
id: "camera",
label: "Анимация камеры",
description: "geodesic spiral survey",
content: (
<div className="mission-map__inspector-controls">
<Checker
label="Разрешить анимацию камеры"
checked={settings.cameraAnimationEnabled}
onChange={(checked) => updateDraft((current) => {
current.view.visualSettings.cameraAnimationEnabled = checked;
return current;
})}
/>
<ControlRow label="Сохранённая позиция">
<StatusBadge tone={view.camera ? "success" : "neutral"}>
{view.camera ? "Есть" : "Не задана"}
</StatusBadge>
</ControlRow>
</div>
),
},
{
id: "tile-cache",
label: "TileCache",
description: "Platform Map Gateway",
content: (
<div
className="mission-map__inspector-controls"
data-health-code={gatewayHealth.code ?? undefined}
>
<Checker
label="Использовать общий TileCache"
checked={view.cacheIntent.enabled}
onChange={(checked) => updateDraft((current) => {
current.view.cacheIntent.enabled = checked;
return current;
})}
/>
<Checker
label="Не перезаписывать существующие тайлы"
checked={view.cacheIntent.noOverwrite}
disabled={!view.cacheIntent.enabled}
onChange={(checked) => updateDraft((current) => {
current.view.cacheIntent.noOverwrite = checked;
return current;
})}
/>
<CacheHealth health={gatewayHealth} />
</div>
),
},
];
}
function CacheHealth({
health,
}: {
health: ReturnType<typeof useMapGatewayHealth>;
}) {
if (health.state === "loading" || health.state === "idle") {
return (
<ControlRow label="Состояние">
<StatusBadge tone="neutral">Проверяем</StatusBadge>
</ControlRow>
);
}
if (!health.snapshot) {
return (
<ControlRow label="Состояние">
<StatusBadge tone="danger">Недоступен</StatusBadge>
</ControlRow>
);
}
const { cache } = health.snapshot;
return (
<dl className="mission-map__cache-health">
<div>
<dt>Хранилище</dt>
<dd>
<StatusBadge tone={cache.persistent ? "success" : "warning"}>
{cache.persistent ? "Постоянное" : "Временное"}
</StatusBadge>
</dd>
</div>
<div>
<dt>Объекты</dt>
<dd>{cache.entries.toLocaleString("ru-RU")}</dd>
</div>
<div>
<dt>Объём</dt>
<dd>
{formatBytes(cache.bytes)}
{cache.maxBytes === null ? "" : ` / ${formatBytes(cache.maxBytes)}`}
</dd>
</div>
<div>
<dt>Заполнение</dt>
<dd>
<StatusBadge
tone={
cache.atCapacity || health.state === "stale"
? "warning"
: "success"
}
>
{cache.atCapacity
? "Лимит"
: health.state === "stale"
? "Устарело"
: "Норма"}
</StatusBadge>
</dd>
</div>
</dl>
);
}
function formatBytes(value: number): string {
if (value < 1024) return `${value} Б`;
const units = ["КБ", "МБ", "ГБ", "ТБ"];
let amount = value / 1024;
let unit = units[0];
for (let index = 1; index < units.length && amount >= 1024; index += 1) {
amount /= 1024;
unit = units[index];
}
return `${amount.toFixed(amount >= 10 ? 1 : 2)} ${unit}`;
}