feat(map): refine sector inspector styling

This commit is contained in:
Codex
2026-08-06 12:10:26 +03:00
parent d4827009d4
commit e8729401fd
11 changed files with 437 additions and 26 deletions
+115 -8
View File
@@ -316,6 +316,11 @@ export type GridLodProfile = {
volumeMinimumHeightMeters: number; volumeMinimumHeightMeters: number;
volumeMaximumHeightMeters: number; volumeMaximumHeightMeters: number;
volumeBandHeightMeters: number; volumeBandHeightMeters: number;
selectionFillColor: string;
selectionFillOpacityPercent: number;
selectionOutlineColor: string;
selectionOutlineWidthPx: number;
selectionOutlineOpacityPercent: number;
}; };
export type GridVolumeSelection = { export type GridVolumeSelection = {
@@ -1006,6 +1011,11 @@ type LocalGridAddressing = {
volumeMinimumHeightMeters: number; volumeMinimumHeightMeters: number;
volumeMaximumHeightMeters: number; volumeMaximumHeightMeters: number;
volumeBandHeightMeters: number; volumeBandHeightMeters: number;
selectionFillColor: string;
selectionFillOpacityPercent: number;
selectionOutlineColor: string;
selectionOutlineWidthPx: number;
selectionOutlineOpacityPercent: number;
}; };
type GraticuleGridAddressing = { type GraticuleGridAddressing = {
mode: "graticule"; mode: "graticule";
@@ -1017,6 +1027,11 @@ type GraticuleGridAddressing = {
lineColor: string; lineColor: string;
lineOpacity: number; lineOpacity: number;
majorLineWidthMultiplier: number; majorLineWidthMultiplier: number;
selectionFillColor: string;
selectionFillOpacityPercent: number;
selectionOutlineColor: string;
selectionOutlineWidthPx: number;
selectionOutlineOpacityPercent: number;
}; };
type GridAddressing = LocalGridAddressing | GraticuleGridAddressing; type GridAddressing = LocalGridAddressing | GraticuleGridAddressing;
type GridResources = { type GridResources = {
@@ -1525,7 +1540,56 @@ function materializeGridLayer(plan: Exclude<GridLayerPlan, HiddenGridPlan>, seri
return plan.mode === "3d" ? materializeLocalGrid(plan, serial) : materializeGraticule(plan, serial); return plan.mode === "3d" ? materializeLocalGrid(plan, serial) : materializeGraticule(plan, serial);
} }
const GRID_SELECTION_COLOR = Color.fromCssColorString("#35CFFF"); const DEFAULT_GRID_SELECTION_COLOR = "#35cfff";
type GridSelectionStyle = {
fillColor: Color;
outlineColor: Color;
outlineWidthPixels: number;
};
function safeGridSelectionColor(value: unknown) {
if (typeof value === "string" && value.trim()) {
try {
const parsed = Color.fromCssColorString(value.trim());
if (parsed) return parsed;
} catch {
// Persisted profiles may predate strict colour validation. Rendering a
// stable selection is preferable to failing the whole Cesium layer.
}
}
return Color.fromCssColorString(DEFAULT_GRID_SELECTION_COLOR);
}
function safeGridSelectionPercent(value: unknown, fallback: number) {
const candidate = typeof value === "number" ? value : Number.NaN;
return clamp(Number.isFinite(candidate) ? candidate : fallback, 0, 100) / 100;
}
function resolveGridSelectionStyle(addressing: GridAddressing): GridSelectionStyle {
const fallbackOutlineWidth = addressing.mode === "3d"
? clamp(2.5 * addressing.majorLineWidthMultiplier, 3, 12)
: clamp(addressing.lineWidthPixels * addressing.majorLineWidthMultiplier * 1.5, 3, 12);
const requestedOutlineWidth = typeof addressing.selectionOutlineWidthPx === "number"
? addressing.selectionOutlineWidthPx
: Number.NaN;
const fallbackFillOpacityPercent = addressing.mode === "graticule"
? 12
: addressing.volumeEnabled ? 10 : 16;
return {
fillColor: safeGridSelectionColor(addressing.selectionFillColor).withAlpha(
safeGridSelectionPercent(addressing.selectionFillOpacityPercent, fallbackFillOpacityPercent),
),
outlineColor: safeGridSelectionColor(addressing.selectionOutlineColor).withAlpha(
safeGridSelectionPercent(addressing.selectionOutlineOpacityPercent, 95),
),
outlineWidthPixels: clamp(
Number.isFinite(requestedOutlineWidth) ? requestedOutlineWidth : fallbackOutlineWidth,
1,
12,
),
};
}
function sampledRange(start: number, end: number, maximumStep: number) { function sampledRange(start: number, end: number, maximumStep: number) {
const span = end - start; const span = end - start;
@@ -1562,6 +1626,7 @@ function materializeLocalSelection(
const dataSource = new CustomDataSource(`nodedc-map-grid-selection-local:${serial}`); const dataSource = new CustomDataSource(`nodedc-map-grid-selection-local:${serial}`);
const crosses = new PolylineCollection(); const crosses = new PolylineCollection();
const selectionStyle = resolveGridSelectionStyle(addressing);
const volumeAvailable = addressing.volumeEnabled const volumeAvailable = addressing.volumeEnabled
&& Number.isFinite(addressing.volumeBandHeightMeters) && Number.isFinite(addressing.volumeBandHeightMeters)
&& addressing.volumeBandHeightMeters > 0 && addressing.volumeBandHeightMeters > 0
@@ -1582,9 +1647,9 @@ function materializeLocalSelection(
const polyline = crosses.add({ const polyline = crosses.add({
id: { kind: "nodedc-grid-selection", sectorId: selection.id, edgeId: id }, id: { kind: "nodedc-grid-selection", sectorId: selection.id, edgeId: id },
positions, positions,
width: clamp(2.5 * addressing.majorLineWidthMultiplier, 3, 12), width: selectionStyle.outlineWidthPixels,
}); });
if (polyline.material?.uniforms) polyline.material.uniforms.color = GRID_SELECTION_COLOR.withAlpha(0.95); if (polyline.material?.uniforms) polyline.material.uniforms.color = selectionStyle.outlineColor;
}; };
addLine("floor", floorPerimeter); addLine("floor", floorPerimeter);
if (ceiling > floor + 1e-6) addLine("ceiling", ceilingPerimeter); if (ceiling > floor + 1e-6) addLine("ceiling", ceilingPerimeter);
@@ -1608,7 +1673,7 @@ function materializeLocalSelection(
polygon: { polygon: {
hierarchy: new PolygonHierarchy(ceilingSurface), hierarchy: new PolygonHierarchy(ceilingSurface),
perPositionHeight: true, perPositionHeight: true,
material: GRID_SELECTION_COLOR.withAlpha(volumeAvailable ? 0.1 : 0.16), material: selectionStyle.fillColor,
outline: false, outline: false,
}, },
}); });
@@ -1621,6 +1686,7 @@ function materializeGraticuleSelection(
serial: number, serial: number,
): GridResources { ): GridResources {
const dataSource = new CustomDataSource(`nodedc-map-grid-selection-graticule:${serial}`); const dataSource = new CustomDataSource(`nodedc-map-grid-selection-graticule:${serial}`);
const selectionStyle = resolveGridSelectionStyle(addressing);
const height = Math.max(0, addressing.heightMeters); const height = Math.max(0, addressing.heightMeters);
const horizontalStep = Math.min(10, Math.max(0.5, addressing.stepDegrees)); const horizontalStep = Math.min(10, Math.max(0.5, addressing.stepDegrees));
const verticalStep = Math.min(10, Math.max(0.5, addressing.stepDegrees)); const verticalStep = Math.min(10, Math.max(0.5, addressing.stepDegrees));
@@ -1643,8 +1709,8 @@ function materializeGraticuleSelection(
id: `${selection.id}/selection-surface`, id: `${selection.id}/selection-surface`,
polyline: { polyline: {
positions: perimeter, positions: perimeter,
width: clamp(addressing.lineWidthPixels * addressing.majorLineWidthMultiplier * 1.5, 3, 12), width: selectionStyle.outlineWidthPixels,
material: GRID_SELECTION_COLOR.withAlpha(0.95), material: selectionStyle.outlineColor,
clampToGround, clampToGround,
arcType: ArcType.RHUMB, arcType: ArcType.RHUMB,
granularity: graticuleGranularity(horizontalStep, clampToGround), granularity: graticuleGranularity(horizontalStep, clampToGround),
@@ -1654,7 +1720,7 @@ function materializeGraticuleSelection(
height: clampToGround ? undefined : height, height: clampToGround ? undefined : height,
heightReference: clampToGround ? HeightReference.CLAMP_TO_GROUND : HeightReference.NONE, heightReference: clampToGround ? HeightReference.CLAMP_TO_GROUND : HeightReference.NONE,
arcType: ArcType.RHUMB, arcType: ArcType.RHUMB,
material: GRID_SELECTION_COLOR.withAlpha(0.12), material: selectionStyle.fillColor,
outline: false, outline: false,
}, },
}); });
@@ -1712,12 +1778,38 @@ class GridLayerController {
: JSON.stringify([addressing.mode, addressing.lod, addressing.stepDegrees, addressing.majorStepDegrees]); : JSON.stringify([addressing.mode, addressing.lod, addressing.stepDegrees, addressing.majorStepDegrees]);
} }
private syncSelectionStyle(plan: Exclude<GridLayerPlan, HiddenGridPlan>) {
let changed = false;
const targetLod = plan.lod.index + 1;
for (const layer of [this.current, this.pending]) {
const addressing = layer?.addressing;
if (!addressing || addressing.mode !== plan.mode || addressing.lod !== targetLod) continue;
if (
addressing.selectionFillColor === plan.lod.selectionFillColor
&& addressing.selectionFillOpacityPercent === plan.lod.selectionFillOpacityPercent
&& addressing.selectionOutlineColor === plan.lod.selectionOutlineColor
&& addressing.selectionOutlineWidthPx === plan.lod.selectionOutlineWidthPx
&& addressing.selectionOutlineOpacityPercent === plan.lod.selectionOutlineOpacityPercent
) continue;
addressing.selectionFillColor = plan.lod.selectionFillColor;
addressing.selectionFillOpacityPercent = plan.lod.selectionFillOpacityPercent;
addressing.selectionOutlineColor = plan.lod.selectionOutlineColor;
addressing.selectionOutlineWidthPx = plan.lod.selectionOutlineWidthPx;
addressing.selectionOutlineOpacityPercent = plan.lod.selectionOutlineOpacityPercent;
changed = true;
}
if (changed) this.refreshSelection(this.pending?.addressing ?? this.current?.addressing);
}
rebuild(presentation: MapPresentation) { rebuild(presentation: MapPresentation) {
// Planning is intentionally cheap. Geometry is materialized only after // Planning is intentionally cheap. Geometry is materialized only after
// key comparison; repeated camera/settings notifications cannot create // key comparison; repeated camera/settings notifications cannot create
// and discard thousands of Cesium objects for an unchanged layer. // and discard thousands of Cesium objects for an unchanged layer.
const plan = planGridLayer(this.viewer, presentation, this.lodIndex); const plan = planGridLayer(this.viewer, presentation, this.lodIndex);
if (plan.key === this.key) return; if (plan.key === this.key) {
if (plan.mode !== "hidden") this.syncSelectionStyle(plan);
return;
}
this.key = plan.key; this.key = plan.key;
this.lodIndex = plan.lodIndex; this.lodIndex = plan.lodIndex;
const epoch = ++this.epoch; const epoch = ++this.epoch;
@@ -1751,6 +1843,11 @@ class GridLayerController {
volumeMinimumHeightMeters: plan.lod.volumeMinimumHeightMeters, volumeMinimumHeightMeters: plan.lod.volumeMinimumHeightMeters,
volumeMaximumHeightMeters: plan.lod.volumeMaximumHeightMeters, volumeMaximumHeightMeters: plan.lod.volumeMaximumHeightMeters,
volumeBandHeightMeters: plan.lod.volumeBandHeightMeters, volumeBandHeightMeters: plan.lod.volumeBandHeightMeters,
selectionFillColor: plan.lod.selectionFillColor,
selectionFillOpacityPercent: plan.lod.selectionFillOpacityPercent,
selectionOutlineColor: plan.lod.selectionOutlineColor,
selectionOutlineWidthPx: plan.lod.selectionOutlineWidthPx,
selectionOutlineOpacityPercent: plan.lod.selectionOutlineOpacityPercent,
} : { } : {
mode: "graticule", mode: "graticule",
lod: plan.lod.index + 1, lod: plan.lod.index + 1,
@@ -1761,6 +1858,11 @@ class GridLayerController {
lineColor: plan.lod.graticuleColor, lineColor: plan.lod.graticuleColor,
lineOpacity: plan.lod.graticuleOpacity, lineOpacity: plan.lod.graticuleOpacity,
majorLineWidthMultiplier: plan.lod.majorLineWidthMultiplier, majorLineWidthMultiplier: plan.lod.majorLineWidthMultiplier,
selectionFillColor: plan.lod.selectionFillColor,
selectionFillOpacityPercent: plan.lod.selectionFillOpacityPercent,
selectionOutlineColor: plan.lod.selectionOutlineColor,
selectionOutlineWidthPx: plan.lod.selectionOutlineWidthPx,
selectionOutlineOpacityPercent: plan.lod.selectionOutlineOpacityPercent,
}; };
const activeAddressing = this.pending?.addressing ?? this.current?.addressing; const activeAddressing = this.pending?.addressing ?? this.current?.addressing;
if (activeAddressing && this.addressingKey(activeAddressing) !== this.addressingKey(addressing)) { if (activeAddressing && this.addressingKey(activeAddressing) !== this.addressingKey(addressing)) {
@@ -1943,6 +2045,11 @@ class GridLayerController {
addressing.lineColor, addressing.lineColor,
addressing.lineOpacity, addressing.lineOpacity,
addressing.majorLineWidthMultiplier, addressing.majorLineWidthMultiplier,
addressing.selectionFillColor,
addressing.selectionFillOpacityPercent,
addressing.selectionOutlineColor,
addressing.selectionOutlineWidthPx,
addressing.selectionOutlineOpacityPercent,
addressing.mode === "3d" ? [ addressing.mode === "3d" ? [
addressing.volumeEnabled, addressing.volumeEnabled,
addressing.volumeMinimumHeightMeters, addressing.volumeMinimumHeightMeters,
+62 -5
View File
@@ -75,6 +75,11 @@ type SectorGridLodProfile = GridLodProfile & {
majorLinesEnabled: boolean; majorLinesEnabled: boolean;
majorLabelsEnabled: boolean; majorLabelsEnabled: boolean;
majorLineWidthMultiplier: number; majorLineWidthMultiplier: number;
selectionFillColor: string;
selectionFillOpacityPercent: number;
selectionOutlineColor: string;
selectionOutlineWidthPx: number;
selectionOutlineOpacityPercent: number;
volumeEnabled: boolean; volumeEnabled: boolean;
volumeMinimumHeightMeters: number; volumeMinimumHeightMeters: number;
volumeMaximumHeightMeters: number; volumeMaximumHeightMeters: number;
@@ -981,12 +986,22 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
const updateGridVolumeRange = (patch: Partial<Pick<SectorGridLodProfile, const updateGridVolumeRange = (patch: Partial<Pick<SectorGridLodProfile,
"volumeMinimumHeightMeters" | "volumeMaximumHeightMeters" | "volumeBandHeightMeters">>) => { "volumeMinimumHeightMeters" | "volumeMaximumHeightMeters" | "volumeBandHeightMeters">>) => {
const minimum = patch.volumeMinimumHeightMeters ?? activeGridLod.volumeMinimumHeightMeters; const minimum = patch.volumeMinimumHeightMeters ?? activeGridLod.volumeMinimumHeightMeters;
const maximum = Math.max(minimum + 1, patch.volumeMaximumHeightMeters ?? activeGridLod.volumeMaximumHeightMeters); const requestedBandHeight = Math.min(
1_000,
Math.max(1, patch.volumeBandHeightMeters ?? activeGridLod.volumeBandHeightMeters),
);
let maximum = Math.min(
10_000,
Math.max(minimum + 1, patch.volumeMaximumHeightMeters ?? activeGridLod.volumeMaximumHeightMeters),
);
if (patch.volumeBandHeightMeters !== undefined) {
maximum = Math.min(10_000, Math.max(maximum, minimum + requestedBandHeight));
}
const span = maximum - minimum; const span = maximum - minimum;
updateGridLod({ updateGridLod({
volumeMinimumHeightMeters: minimum, volumeMinimumHeightMeters: minimum,
volumeMaximumHeightMeters: maximum, volumeMaximumHeightMeters: maximum,
volumeBandHeightMeters: Math.min(span, Math.max(1, patch.volumeBandHeightMeters ?? activeGridLod.volumeBandHeightMeters)), volumeBandHeightMeters: Math.min(span, requestedBandHeight),
}); });
}; };
const setCacheEnabled = (cacheEnabled: boolean) => { const setCacheEnabled = (cacheEnabled: boolean) => {
@@ -1632,7 +1647,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
label: "Сетка и LOD", label: "Сетка и LOD",
description: "first adapter control", description: "first adapter control",
group: "Слои", group: "Слои",
content: <> content: <div className="catalog-map-grid-inspector">
<small className="catalog-map-inspector__note">Фиксированная московская ENU-адресация задаёт неизменные сектора на LOD 13. LOD 45 используют глобальную WGS84-гратику́лу; камера выбирает только LOD и видимую область.</small> <small className="catalog-map-inspector__note">Фиксированная московская ENU-адресация задаёт неизменные сектора на LOD 13. LOD 45 используют глобальную WGS84-гратику́лу; камера выбирает только LOD и видимую область.</small>
<Checker checked={mapSettings.gridVisible} label="Сетка" onChange={(gridVisible) => updateMapSettings({ gridVisible })} /> <Checker checked={mapSettings.gridVisible} label="Сетка" onChange={(gridVisible) => updateMapSettings({ gridVisible })} />
<Checker checked={mapSettings.grid3dEnabled} label="3D-сетка" onChange={(grid3dEnabled) => updateMapSettings({ grid3dEnabled })} /> <Checker checked={mapSettings.grid3dEnabled} label="3D-сетка" onChange={(grid3dEnabled) => updateMapSettings({ grid3dEnabled })} />
@@ -1743,13 +1758,55 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
label="Высота адресного диапазона" label="Высота адресного диапазона"
value={activeGridLod.volumeBandHeightMeters} value={activeGridLod.volumeBandHeightMeters}
min={1} min={1}
max={Math.max(1, activeGridLod.volumeMaximumHeightMeters - activeGridLod.volumeMinimumHeightMeters)} max={1_000}
step={10} step={10}
formatValue={(value) => `${value} м`} formatValue={(value) => `${value} м`}
onChange={(volumeBandHeightMeters) => updateGridVolumeRange({ volumeBandHeightMeters })} onChange={(volumeBandHeightMeters) => updateGridVolumeRange({ volumeBandHeightMeters })}
/> />
<small className="catalog-map-inspector__note">Горизонтальный ID сектора остаётся стабильным. Высотный band добавляется как отдельный адрес внутри выбранной ENU-ячейки.</small> <small className="catalog-map-inspector__note">Горизонтальный ID сектора остаётся стабильным. Высотный band добавляется как отдельный адрес внутри выбранной ENU-ячейки.</small>
</> : null} </> : null}
<ControlRow label="Цвет заливки сектора">
<ColorField
label="Цвет заливки выбранного сектора"
value={activeGridLod.selectionFillColor}
onChange={(selectionFillColor) => updateGridLod({ selectionFillColor })}
/>
</ControlRow>
<RangeControl
label="Прозрачность заливки"
value={activeGridLod.selectionFillOpacityPercent}
min={0}
max={100}
step={1}
formatValue={(value) => `${value}%`}
onChange={(selectionFillOpacityPercent) => updateGridLod({ selectionFillOpacityPercent })}
/>
<ControlRow label="Цвет линии сектора">
<ColorField
label="Цвет линии выбранного сектора"
value={activeGridLod.selectionOutlineColor}
onChange={(selectionOutlineColor) => updateGridLod({ selectionOutlineColor })}
/>
</ControlRow>
<RangeControl
label="Толщина линии"
value={activeGridLod.selectionOutlineWidthPx}
min={1}
max={12}
step={0.25}
formatValue={(value) => `${value} px`}
onChange={(selectionOutlineWidthPx) => updateGridLod({ selectionOutlineWidthPx })}
/>
<RangeControl
label="Прозрачность линии"
value={activeGridLod.selectionOutlineOpacityPercent}
min={0}
max={100}
step={1}
formatValue={(value) => `${value}%`}
onChange={(selectionOutlineOpacityPercent) => updateGridLod({ selectionOutlineOpacityPercent })}
/>
<small className="catalog-map-inspector__note">Оформление применяется к выбранному сектору текущего LOD.</small>
<section className="catalog-map-grid-sector" aria-label="Выбранный сектор"> <section className="catalog-map-grid-sector" aria-label="Выбранный сектор">
<ControlRow label="Выбранный сектор"><strong className="catalog-map-grid-sector-id">{selectedGridSector?.id ?? "Нажмите сектор на карте"}</strong></ControlRow> <ControlRow label="Выбранный сектор"><strong className="catalog-map-grid-sector-id">{selectedGridSector?.id ?? "Нажмите сектор на карте"}</strong></ControlRow>
{selectedGridSector ? <> {selectedGridSector ? <>
@@ -1841,7 +1898,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
</div> : null} </div> : null}
</> : <small className="catalog-map-inspector__note">Кликните ячейку, чтобы получить устойчивый адрес, геометрию и навигацию по соседям.</small>} </> : <small className="catalog-map-inspector__note">Кликните ячейку, чтобы получить устойчивый адрес, геометрию и навигацию по соседям.</small>}
</section> </section>
</>, </div>,
}, },
{ {
id: "map-camera-animation", id: "map-camera-animation",
+5
View File
@@ -32,6 +32,11 @@ export type GridLodProfile = {
volumeMinimumHeightMeters: number; volumeMinimumHeightMeters: number;
volumeMaximumHeightMeters: number; volumeMaximumHeightMeters: number;
volumeBandHeightMeters: number; volumeBandHeightMeters: number;
selectionFillColor: string;
selectionFillOpacityPercent: number;
selectionOutlineColor: string;
selectionOutlineWidthPx: number;
selectionOutlineOpacityPercent: number;
}; };
export type GridLodBand = GridLodProfile & { export type GridLodBand = GridLodProfile & {
index: number; index: number;
+15
View File
@@ -18,6 +18,8 @@ export const DEFAULT_GRID_LOD_PROFILES = Object.freeze([
graticuleStepDegrees: 0.25, graticuleLineWidthPx: 1, graticuleColor: "#f5f5f5", graticuleOpacity: 12, graticuleStepDegrees: 0.25, graticuleLineWidthPx: 1, graticuleColor: "#f5f5f5", graticuleOpacity: 12,
majorLinesEnabled: true, majorLabelsEnabled: true, majorLineWidthMultiplier: 2.5, majorLinesEnabled: true, majorLabelsEnabled: true, majorLineWidthMultiplier: 2.5,
volumeEnabled: true, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 300, volumeBandHeightMeters: 300, volumeEnabled: true, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 300, volumeBandHeightMeters: 300,
selectionFillColor: "#35cfff", selectionFillOpacityPercent: 10,
selectionOutlineColor: "#35cfff", selectionOutlineWidthPx: 6.25, selectionOutlineOpacityPercent: 95,
}), }),
Object.freeze({ Object.freeze({
maxHeightKm: 50, stepKm: 5, mode: "3d", heightMeters: 500, max3dViewAngleDegrees: 30, maxHeightKm: 50, stepKm: 5, mode: "3d", heightMeters: 500, max3dViewAngleDegrees: 30,
@@ -27,6 +29,8 @@ export const DEFAULT_GRID_LOD_PROFILES = Object.freeze([
graticuleStepDegrees: 0.5, graticuleLineWidthPx: 1, graticuleColor: "#f8fbfc", graticuleOpacity: 12, graticuleStepDegrees: 0.5, graticuleLineWidthPx: 1, graticuleColor: "#f8fbfc", graticuleOpacity: 12,
majorLinesEnabled: true, majorLabelsEnabled: true, majorLineWidthMultiplier: 2.5, majorLinesEnabled: true, majorLabelsEnabled: true, majorLineWidthMultiplier: 2.5,
volumeEnabled: true, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 500, volumeBandHeightMeters: 500, volumeEnabled: true, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 500, volumeBandHeightMeters: 500,
selectionFillColor: "#35cfff", selectionFillOpacityPercent: 10,
selectionOutlineColor: "#35cfff", selectionOutlineWidthPx: 6.25, selectionOutlineOpacityPercent: 95,
}), }),
Object.freeze({ Object.freeze({
maxHeightKm: 200, stepKm: 25, mode: "3d", heightMeters: 500, max3dViewAngleDegrees: 30, maxHeightKm: 200, stepKm: 25, mode: "3d", heightMeters: 500, max3dViewAngleDegrees: 30,
@@ -36,6 +40,8 @@ export const DEFAULT_GRID_LOD_PROFILES = Object.freeze([
graticuleStepDegrees: 1, graticuleLineWidthPx: 1, graticuleColor: "#fafafa", graticuleOpacity: 12, graticuleStepDegrees: 1, graticuleLineWidthPx: 1, graticuleColor: "#fafafa", graticuleOpacity: 12,
majorLinesEnabled: true, majorLabelsEnabled: false, majorLineWidthMultiplier: 2, majorLinesEnabled: true, majorLabelsEnabled: false, majorLineWidthMultiplier: 2,
volumeEnabled: false, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 500, volumeBandHeightMeters: 500, volumeEnabled: false, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 500, volumeBandHeightMeters: 500,
selectionFillColor: "#35cfff", selectionFillOpacityPercent: 16,
selectionOutlineColor: "#35cfff", selectionOutlineWidthPx: 5, selectionOutlineOpacityPercent: 95,
}), }),
Object.freeze({ Object.freeze({
maxHeightKm: 800, stepKm: 50, mode: "graticule", heightMeters: 500, max3dViewAngleDegrees: 30, maxHeightKm: 800, stepKm: 50, mode: "graticule", heightMeters: 500, max3dViewAngleDegrees: 30,
@@ -45,6 +51,8 @@ export const DEFAULT_GRID_LOD_PROFILES = Object.freeze([
graticuleStepDegrees: 2, graticuleLineWidthPx: 1, graticuleColor: "#fcfdfd", graticuleOpacity: 12, graticuleStepDegrees: 2, graticuleLineWidthPx: 1, graticuleColor: "#fcfdfd", graticuleOpacity: 12,
majorLinesEnabled: true, majorLabelsEnabled: false, majorLineWidthMultiplier: 2, majorLinesEnabled: true, majorLabelsEnabled: false, majorLineWidthMultiplier: 2,
volumeEnabled: false, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 500, volumeBandHeightMeters: 500, volumeEnabled: false, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 500, volumeBandHeightMeters: 500,
selectionFillColor: "#35cfff", selectionFillOpacityPercent: 12,
selectionOutlineColor: "#35cfff", selectionOutlineWidthPx: 3, selectionOutlineOpacityPercent: 95,
}), }),
Object.freeze({ Object.freeze({
// Engine keeps the last LOD selected above this threshold; the independent // Engine keeps the last LOD selected above this threshold; the independent
@@ -56,6 +64,8 @@ export const DEFAULT_GRID_LOD_PROFILES = Object.freeze([
graticuleStepDegrees: 2, graticuleLineWidthPx: 1, graticuleColor: "#9c9c9c", graticuleOpacity: 8, graticuleStepDegrees: 2, graticuleLineWidthPx: 1, graticuleColor: "#9c9c9c", graticuleOpacity: 8,
majorLinesEnabled: true, majorLabelsEnabled: false, majorLineWidthMultiplier: 2, majorLinesEnabled: true, majorLabelsEnabled: false, majorLineWidthMultiplier: 2,
volumeEnabled: false, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 500, volumeBandHeightMeters: 500, volumeEnabled: false, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 500, volumeBandHeightMeters: 500,
selectionFillColor: "#35cfff", selectionFillOpacityPercent: 12,
selectionOutlineColor: "#35cfff", selectionOutlineWidthPx: 3, selectionOutlineOpacityPercent: 95,
}), }),
]); ]);
@@ -98,6 +108,11 @@ export function gridLodProfile(settings, index) {
volumeMinimumHeightMeters: clamp(finite(source.volumeMinimumHeightMeters, fallback.volumeMinimumHeightMeters), -1_000, 10_000), volumeMinimumHeightMeters: clamp(finite(source.volumeMinimumHeightMeters, fallback.volumeMinimumHeightMeters), -1_000, 10_000),
volumeMaximumHeightMeters: clamp(finite(source.volumeMaximumHeightMeters, fallback.volumeMaximumHeightMeters), -1_000, 10_000), volumeMaximumHeightMeters: clamp(finite(source.volumeMaximumHeightMeters, fallback.volumeMaximumHeightMeters), -1_000, 10_000),
volumeBandHeightMeters: clamp(finite(source.volumeBandHeightMeters, fallback.volumeBandHeightMeters), 1, 10_000), volumeBandHeightMeters: clamp(finite(source.volumeBandHeightMeters, fallback.volumeBandHeightMeters), 1, 10_000),
selectionFillColor: color(source.selectionFillColor, fallback.selectionFillColor),
selectionFillOpacityPercent: clamp(finite(source.selectionFillOpacityPercent, fallback.selectionFillOpacityPercent), 0, 100),
selectionOutlineColor: color(source.selectionOutlineColor, fallback.selectionOutlineColor),
selectionOutlineWidthPx: clamp(finite(source.selectionOutlineWidthPx, fallback.selectionOutlineWidthPx), 1, 12),
selectionOutlineOpacityPercent: clamp(finite(source.selectionOutlineOpacityPercent, fallback.selectionOutlineOpacityPercent), 0, 100),
}; };
} }
+109 -2
View File
@@ -958,6 +958,63 @@ textarea {
white-space: nowrap; white-space: nowrap;
} }
.catalog-map-grid-inspector {
display: grid;
width: 100%;
max-width: 100%;
min-width: 0;
gap: var(--nodedc-space-3);
font-family: var(--nodedc-font-family);
font-size: var(--nodedc-font-size-sm);
}
.catalog-map-grid-inspector :where(*) {
min-width: 0;
max-width: 100%;
font-family: var(--nodedc-font-family);
}
.catalog-map-grid-inspector :where(
.nodedc-checker__label,
.nodedc-range__label,
.nodedc-range__value,
.nodedc-range__editor,
.nodedc-select__value,
.nodedc-select-trigger__label,
.nodedc-control-row__label,
.nodedc-control-row__control,
.catalog-map-inspector__note,
.nodedc-segmented__item,
.nodedc-color-field__text,
small,
span,
strong,
code
),
.catalog-map-grid-inspector .nodedc-button {
font-family: var(--nodedc-font-family);
font-size: var(--nodedc-font-size-sm);
}
.catalog-map-grid-inspector .nodedc-checker__label {
overflow-wrap: anywhere;
text-overflow: clip;
white-space: normal;
}
.catalog-map-grid-inspector :where(
.nodedc-control-row__label,
.nodedc-control-row__control,
.catalog-map-inspector__note,
small,
span,
strong,
code,
.nodedc-button
) {
overflow-wrap: anywhere;
}
.catalog-map-grid-lod-tabs { .catalog-map-grid-lod-tabs {
width: 100%; width: 100%;
min-width: 0; min-width: 0;
@@ -973,22 +1030,70 @@ textarea {
flex: 1 1 0; flex: 1 1 0;
min-width: 0; min-width: 0;
padding-inline: 0.45rem; padding-inline: 0.45rem;
font-size: var(--nodedc-font-size-xs); font-size: var(--nodedc-font-size-sm);
white-space: nowrap; white-space: nowrap;
} }
.catalog-map-grid-sector-id { .catalog-map-grid-sector-id {
display: block;
width: 100%;
min-width: 0; min-width: 0;
overflow-wrap: anywhere; overflow-wrap: anywhere;
text-align: right; text-align: right;
white-space: normal;
} }
.catalog-map-grid-sector { .catalog-map-grid-sector {
display: grid; display: grid;
width: 100%;
max-width: 100%;
min-width: 0; min-width: 0;
gap: 0.55rem; gap: 0.55rem;
border-top: 1px solid color-mix(in srgb, var(--nodedc-glass-outline) 44%, transparent); border-top: 1px solid color-mix(in srgb, var(--nodedc-glass-outline) 44%, transparent);
padding-top: 0.7rem; padding-top: 0.7rem;
font-family: var(--nodedc-font-family);
font-size: var(--nodedc-font-size-sm);
line-height: 1.35;
}
.catalog-map-grid-sector :where(*) {
min-width: 0;
max-width: 100%;
font-family: var(--nodedc-font-family);
}
.catalog-map-grid-sector :where(
.nodedc-control-row__label,
.nodedc-control-row__control,
small,
span,
strong,
code
),
.catalog-map-grid-sector .nodedc-button {
font-size: var(--nodedc-font-size-sm);
line-height: 1.35;
}
.catalog-map-grid-sector .nodedc-control-row {
width: 100%;
grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
align-items: start;
gap: 0.5rem;
}
.catalog-map-grid-sector :where(
.nodedc-control-row__label,
.nodedc-control-row__control,
small,
span,
strong,
code,
.nodedc-button
) {
overflow-wrap: anywhere;
word-break: normal;
white-space: normal;
} }
.catalog-map-grid-sector__facts, .catalog-map-grid-sector__facts,
@@ -1009,7 +1114,10 @@ textarea {
.catalog-map-grid-sector__neighbor, .catalog-map-grid-sector__neighbor,
.catalog-map-grid-sector__volume { .catalog-map-grid-sector__volume {
display: grid; display: grid;
width: 100%;
max-width: 100%;
min-width: 0; min-width: 0;
overflow: hidden;
gap: 0.4rem; gap: 0.4rem;
border-radius: var(--nodedc-radius-control); border-radius: var(--nodedc-radius-control);
background: var(--nodedc-glass-control-bg); background: var(--nodedc-glass-control-bg);
@@ -1020,7 +1128,6 @@ textarea {
.catalog-map-grid-sector__neighbor > code, .catalog-map-grid-sector__neighbor > code,
.catalog-map-grid-sector__volume > code { .catalog-map-grid-sector__volume > code {
color: var(--nodedc-text-muted); color: var(--nodedc-text-muted);
font-size: var(--nodedc-font-size-xs);
line-height: 1.35; line-height: 1.35;
} }
+9 -1
View File
@@ -195,10 +195,18 @@ Per-entity `PolygonGraphics` для массового слоя геозон н
- LOD 1–2 по умолчанию поддерживают отдельный высотный адрес ENU-объёма. - LOD 1–2 по умолчанию поддерживают отдельный высотный адрес ENU-объёма.
Диапазон задаётся как полуоткрытый `[minimum, maximum)`, делится на bands, а Диапазон задаётся как полуоткрытый `[minimum, maximum)`, делится на bands, а
последний band может быть короче. Volume id является дочерним адресом и не последний band может быть короче. Volume id является дочерним адресом и не
подменяет стабильный id горизонтального сектора; подменяет стабильный id горизонтального сектора. Inspector позволяет задать
band до `1000 м`; если он превышает текущий span, верхняя отметка объёма
расширяется вместе с ним, поэтому persisted policy всегда сохраняет
инвариант `band <= maximum - minimum`;
- Cesium материализует пространственную клетку только для выбранного сектора: - Cesium материализует пространственную клетку только для выбранного сектора:
нижнюю и верхнюю рамки, вертикальные рёбра и границы bands. Вся ENU-плоскость нижнюю и верхнюю рамки, вертикальные рёбра и границы bands. Вся ENU-плоскость
не размножается в объёмные entities, поэтому стоимость выбора ограничена; не размножается в объёмные entities, поэтому стоимость выбора ограничена;
- заливка и outline выбранного сектора являются независимыми настройками
каждого LOD: `selectionFillColor`, `selectionFillOpacityPercent`,
`selectionOutlineColor`, `selectionOutlineWidthPx` и
`selectionOutlineOpacityPercent`. Legacy layouts получают визуально
эквивалентные cyan-defaults при миграции;
- minor и major линии собираются раздельно. Major получает только множитель - minor и major линии собираются раздельно. Major получает только множитель
толщины, наследует цвет/прозрачность текущего LOD, а подписи создаются толщины, наследует цвет/прозрачность текущего LOD, а подписи создаются
ограниченным набором вокруг viewport и не являются источником адресации; ограниченным набором вокруг viewport и не являются источником адресации;
+5 -5
View File
@@ -75,11 +75,11 @@
"gridCrossesColor": "#9c9c9c", "gridCrossesColor": "#9c9c9c",
"gridCrossesOpacity": 46, "gridCrossesOpacity": 46,
"gridLodProfiles": [ "gridLodProfiles": [
{ "maxHeightKm": 10, "stepKm": 1, "mode": "3d", "heightMeters": 300, "max3dViewAngleDegrees": 30, "tileSizeKm": 10, "radiusKm": 50, "lineDiameterMeters": 4, "lineColor": "#f5f5f5", "lineOpacity": 12, "dotsEnabled": true, "dotsDiameterMeters": 7, "dotsColor": "#ffffff", "dotsOpacity": 58, "crossesEnabled": false, "crossesLengthMeters": 60, "crossesWidthMeters": 10, "crossesColor": "#9c9c9c", "crossesOpacity": 46, "graticuleStepDegrees": 0.25, "graticuleLineWidthPx": 1, "graticuleColor": "#f5f5f5", "graticuleOpacity": 12, "majorLinesEnabled": true, "majorLabelsEnabled": true, "majorLineWidthMultiplier": 2.5, "volumeEnabled": true, "volumeMinimumHeightMeters": 0, "volumeMaximumHeightMeters": 300, "volumeBandHeightMeters": 300 }, { "maxHeightKm": 10, "stepKm": 1, "mode": "3d", "heightMeters": 300, "max3dViewAngleDegrees": 30, "tileSizeKm": 10, "radiusKm": 50, "lineDiameterMeters": 4, "lineColor": "#f5f5f5", "lineOpacity": 12, "dotsEnabled": true, "dotsDiameterMeters": 7, "dotsColor": "#ffffff", "dotsOpacity": 58, "crossesEnabled": false, "crossesLengthMeters": 60, "crossesWidthMeters": 10, "crossesColor": "#9c9c9c", "crossesOpacity": 46, "graticuleStepDegrees": 0.25, "graticuleLineWidthPx": 1, "graticuleColor": "#f5f5f5", "graticuleOpacity": 12, "majorLinesEnabled": true, "majorLabelsEnabled": true, "majorLineWidthMultiplier": 2.5, "volumeEnabled": true, "volumeMinimumHeightMeters": 0, "volumeMaximumHeightMeters": 300, "volumeBandHeightMeters": 300, "selectionFillColor": "#35cfff", "selectionFillOpacityPercent": 10, "selectionOutlineColor": "#35cfff", "selectionOutlineWidthPx": 6.25, "selectionOutlineOpacityPercent": 95 },
{ "maxHeightKm": 50, "stepKm": 5, "mode": "3d", "heightMeters": 500, "max3dViewAngleDegrees": 30, "tileSizeKm": 10, "radiusKm": 1000, "lineDiameterMeters": 10, "lineColor": "#f8fbfc", "lineOpacity": 12, "dotsEnabled": true, "dotsDiameterMeters": 80, "dotsColor": "#ffffff", "dotsOpacity": 22, "crossesEnabled": false, "crossesLengthMeters": 60, "crossesWidthMeters": 10, "crossesColor": "#9c9c9c", "crossesOpacity": 46, "graticuleStepDegrees": 0.5, "graticuleLineWidthPx": 1, "graticuleColor": "#f8fbfc", "graticuleOpacity": 12, "majorLinesEnabled": true, "majorLabelsEnabled": true, "majorLineWidthMultiplier": 2.5, "volumeEnabled": true, "volumeMinimumHeightMeters": 0, "volumeMaximumHeightMeters": 500, "volumeBandHeightMeters": 500 }, { "maxHeightKm": 50, "stepKm": 5, "mode": "3d", "heightMeters": 500, "max3dViewAngleDegrees": 30, "tileSizeKm": 10, "radiusKm": 1000, "lineDiameterMeters": 10, "lineColor": "#f8fbfc", "lineOpacity": 12, "dotsEnabled": true, "dotsDiameterMeters": 80, "dotsColor": "#ffffff", "dotsOpacity": 22, "crossesEnabled": false, "crossesLengthMeters": 60, "crossesWidthMeters": 10, "crossesColor": "#9c9c9c", "crossesOpacity": 46, "graticuleStepDegrees": 0.5, "graticuleLineWidthPx": 1, "graticuleColor": "#f8fbfc", "graticuleOpacity": 12, "majorLinesEnabled": true, "majorLabelsEnabled": true, "majorLineWidthMultiplier": 2.5, "volumeEnabled": true, "volumeMinimumHeightMeters": 0, "volumeMaximumHeightMeters": 500, "volumeBandHeightMeters": 500, "selectionFillColor": "#35cfff", "selectionFillOpacityPercent": 10, "selectionOutlineColor": "#35cfff", "selectionOutlineWidthPx": 6.25, "selectionOutlineOpacityPercent": 95 },
{ "maxHeightKm": 200, "stepKm": 25, "mode": "3d", "heightMeters": 500, "max3dViewAngleDegrees": 30, "tileSizeKm": 25, "radiusKm": 1000, "lineDiameterMeters": 7, "lineColor": "#fafafa", "lineOpacity": 12, "dotsEnabled": true, "dotsDiameterMeters": 10, "dotsColor": "#ffffff", "dotsOpacity": 58, "crossesEnabled": false, "crossesLengthMeters": 60, "crossesWidthMeters": 10, "crossesColor": "#9c9c9c", "crossesOpacity": 46, "graticuleStepDegrees": 1, "graticuleLineWidthPx": 1, "graticuleColor": "#fafafa", "graticuleOpacity": 12, "majorLinesEnabled": true, "majorLabelsEnabled": false, "majorLineWidthMultiplier": 2, "volumeEnabled": false, "volumeMinimumHeightMeters": 0, "volumeMaximumHeightMeters": 500, "volumeBandHeightMeters": 500 }, { "maxHeightKm": 200, "stepKm": 25, "mode": "3d", "heightMeters": 500, "max3dViewAngleDegrees": 30, "tileSizeKm": 25, "radiusKm": 1000, "lineDiameterMeters": 7, "lineColor": "#fafafa", "lineOpacity": 12, "dotsEnabled": true, "dotsDiameterMeters": 10, "dotsColor": "#ffffff", "dotsOpacity": 58, "crossesEnabled": false, "crossesLengthMeters": 60, "crossesWidthMeters": 10, "crossesColor": "#9c9c9c", "crossesOpacity": 46, "graticuleStepDegrees": 1, "graticuleLineWidthPx": 1, "graticuleColor": "#fafafa", "graticuleOpacity": 12, "majorLinesEnabled": true, "majorLabelsEnabled": false, "majorLineWidthMultiplier": 2, "volumeEnabled": false, "volumeMinimumHeightMeters": 0, "volumeMaximumHeightMeters": 500, "volumeBandHeightMeters": 500, "selectionFillColor": "#35cfff", "selectionFillOpacityPercent": 16, "selectionOutlineColor": "#35cfff", "selectionOutlineWidthPx": 5, "selectionOutlineOpacityPercent": 95 },
{ "maxHeightKm": 800, "stepKm": 50, "mode": "graticule", "heightMeters": 500, "max3dViewAngleDegrees": 30, "tileSizeKm": 10, "radiusKm": 1000, "lineDiameterMeters": 10, "lineColor": "#fcfdfd", "lineOpacity": 12, "dotsEnabled": true, "dotsDiameterMeters": 10, "dotsColor": "#fcfdfd", "dotsOpacity": 58, "crossesEnabled": false, "crossesLengthMeters": 60, "crossesWidthMeters": 10, "crossesColor": "#9c9c9c", "crossesOpacity": 46, "graticuleStepDegrees": 2, "graticuleLineWidthPx": 1, "graticuleColor": "#fcfdfd", "graticuleOpacity": 12, "majorLinesEnabled": true, "majorLabelsEnabled": false, "majorLineWidthMultiplier": 2, "volumeEnabled": false, "volumeMinimumHeightMeters": 0, "volumeMaximumHeightMeters": 500, "volumeBandHeightMeters": 500 }, { "maxHeightKm": 800, "stepKm": 50, "mode": "graticule", "heightMeters": 500, "max3dViewAngleDegrees": 30, "tileSizeKm": 10, "radiusKm": 1000, "lineDiameterMeters": 10, "lineColor": "#fcfdfd", "lineOpacity": 12, "dotsEnabled": true, "dotsDiameterMeters": 10, "dotsColor": "#fcfdfd", "dotsOpacity": 58, "crossesEnabled": false, "crossesLengthMeters": 60, "crossesWidthMeters": 10, "crossesColor": "#9c9c9c", "crossesOpacity": 46, "graticuleStepDegrees": 2, "graticuleLineWidthPx": 1, "graticuleColor": "#fcfdfd", "graticuleOpacity": 12, "majorLinesEnabled": true, "majorLabelsEnabled": false, "majorLineWidthMultiplier": 2, "volumeEnabled": false, "volumeMinimumHeightMeters": 0, "volumeMaximumHeightMeters": 500, "volumeBandHeightMeters": 500, "selectionFillColor": "#35cfff", "selectionFillOpacityPercent": 12, "selectionOutlineColor": "#35cfff", "selectionOutlineWidthPx": 3, "selectionOutlineOpacityPercent": 95 },
{ "maxHeightKm": 3000, "stepKm": 50, "mode": "graticule", "heightMeters": 500, "max3dViewAngleDegrees": 30, "tileSizeKm": 10, "radiusKm": 100, "lineDiameterMeters": 7, "lineColor": "#9c9c9c", "lineOpacity": 12, "dotsEnabled": true, "dotsDiameterMeters": 10, "dotsColor": "#9c9c9c", "dotsOpacity": 58, "crossesEnabled": false, "crossesLengthMeters": 60, "crossesWidthMeters": 10, "crossesColor": "#9c9c9c", "crossesOpacity": 46, "graticuleStepDegrees": 2, "graticuleLineWidthPx": 1, "graticuleColor": "#9c9c9c", "graticuleOpacity": 8, "majorLinesEnabled": true, "majorLabelsEnabled": false, "majorLineWidthMultiplier": 2, "volumeEnabled": false, "volumeMinimumHeightMeters": 0, "volumeMaximumHeightMeters": 500, "volumeBandHeightMeters": 500 } { "maxHeightKm": 3000, "stepKm": 50, "mode": "graticule", "heightMeters": 500, "max3dViewAngleDegrees": 30, "tileSizeKm": 10, "radiusKm": 100, "lineDiameterMeters": 7, "lineColor": "#9c9c9c", "lineOpacity": 12, "dotsEnabled": true, "dotsDiameterMeters": 10, "dotsColor": "#9c9c9c", "dotsOpacity": 58, "crossesEnabled": false, "crossesLengthMeters": 60, "crossesWidthMeters": 10, "crossesColor": "#9c9c9c", "crossesOpacity": 46, "graticuleStepDegrees": 2, "graticuleLineWidthPx": 1, "graticuleColor": "#9c9c9c", "graticuleOpacity": 8, "majorLinesEnabled": true, "majorLabelsEnabled": false, "majorLineWidthMultiplier": 2, "volumeEnabled": false, "volumeMinimumHeightMeters": 0, "volumeMaximumHeightMeters": 500, "volumeBandHeightMeters": 500, "selectionFillColor": "#35cfff", "selectionFillOpacityPercent": 12, "selectionOutlineColor": "#35cfff", "selectionOutlineWidthPx": 3, "selectionOutlineOpacityPercent": 95 }
] ]
}, },
"mapHeight": 620, "mapHeight": 620,
+43 -5
View File
@@ -31,6 +31,11 @@ test("canonical defaults preserve the exact effective MMAP/MOSCOWMAP five-LOD do
volumeMinimumHeightMeters: _volumeMinimumHeightMeters, volumeMinimumHeightMeters: _volumeMinimumHeightMeters,
volumeMaximumHeightMeters: _volumeMaximumHeightMeters, volumeMaximumHeightMeters: _volumeMaximumHeightMeters,
volumeBandHeightMeters: _volumeBandHeightMeters, volumeBandHeightMeters: _volumeBandHeightMeters,
selectionFillColor: _selectionFillColor,
selectionFillOpacityPercent: _selectionFillOpacityPercent,
selectionOutlineColor: _selectionOutlineColor,
selectionOutlineWidthPx: _selectionOutlineWidthPx,
selectionOutlineOpacityPercent: _selectionOutlineOpacityPercent,
...profile ...profile
}) => profile); }) => profile);
assert.deepEqual(donorProfiles, [ assert.deepEqual(donorProfiles, [
@@ -168,12 +173,17 @@ test("canonical defaults preserve the exact effective MMAP/MOSCOWMAP five-LOD do
volumeMinimumHeightMeters: profile.volumeMinimumHeightMeters, volumeMinimumHeightMeters: profile.volumeMinimumHeightMeters,
volumeMaximumHeightMeters: profile.volumeMaximumHeightMeters, volumeMaximumHeightMeters: profile.volumeMaximumHeightMeters,
volumeBandHeightMeters: profile.volumeBandHeightMeters, volumeBandHeightMeters: profile.volumeBandHeightMeters,
selectionFillColor: profile.selectionFillColor,
selectionFillOpacityPercent: profile.selectionFillOpacityPercent,
selectionOutlineColor: profile.selectionOutlineColor,
selectionOutlineWidthPx: profile.selectionOutlineWidthPx,
selectionOutlineOpacityPercent: profile.selectionOutlineOpacityPercent,
})), [ })), [
{ majorLinesEnabled: true, majorLabelsEnabled: true, majorLineWidthMultiplier: 2.5, volumeEnabled: true, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 300, volumeBandHeightMeters: 300 }, { majorLinesEnabled: true, majorLabelsEnabled: true, majorLineWidthMultiplier: 2.5, volumeEnabled: true, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 300, volumeBandHeightMeters: 300, selectionFillColor: "#35cfff", selectionFillOpacityPercent: 10, selectionOutlineColor: "#35cfff", selectionOutlineWidthPx: 6.25, selectionOutlineOpacityPercent: 95 },
{ majorLinesEnabled: true, majorLabelsEnabled: true, majorLineWidthMultiplier: 2.5, volumeEnabled: true, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 500, volumeBandHeightMeters: 500 }, { majorLinesEnabled: true, majorLabelsEnabled: true, majorLineWidthMultiplier: 2.5, volumeEnabled: true, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 500, volumeBandHeightMeters: 500, selectionFillColor: "#35cfff", selectionFillOpacityPercent: 10, selectionOutlineColor: "#35cfff", selectionOutlineWidthPx: 6.25, selectionOutlineOpacityPercent: 95 },
{ majorLinesEnabled: true, majorLabelsEnabled: false, majorLineWidthMultiplier: 2, volumeEnabled: false, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 500, volumeBandHeightMeters: 500 }, { majorLinesEnabled: true, majorLabelsEnabled: false, majorLineWidthMultiplier: 2, volumeEnabled: false, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 500, volumeBandHeightMeters: 500, selectionFillColor: "#35cfff", selectionFillOpacityPercent: 16, selectionOutlineColor: "#35cfff", selectionOutlineWidthPx: 5, selectionOutlineOpacityPercent: 95 },
{ majorLinesEnabled: true, majorLabelsEnabled: false, majorLineWidthMultiplier: 2, volumeEnabled: false, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 500, volumeBandHeightMeters: 500 }, { majorLinesEnabled: true, majorLabelsEnabled: false, majorLineWidthMultiplier: 2, volumeEnabled: false, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 500, volumeBandHeightMeters: 500, selectionFillColor: "#35cfff", selectionFillOpacityPercent: 12, selectionOutlineColor: "#35cfff", selectionOutlineWidthPx: 3, selectionOutlineOpacityPercent: 95 },
{ majorLinesEnabled: true, majorLabelsEnabled: false, majorLineWidthMultiplier: 2, volumeEnabled: false, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 500, volumeBandHeightMeters: 500 }, { majorLinesEnabled: true, majorLabelsEnabled: false, majorLineWidthMultiplier: 2, volumeEnabled: false, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 500, volumeBandHeightMeters: 500, selectionFillColor: "#35cfff", selectionFillOpacityPercent: 12, selectionOutlineColor: "#35cfff", selectionOutlineWidthPx: 3, selectionOutlineOpacityPercent: 95 },
]); ]);
}); });
@@ -210,6 +220,11 @@ test("every LOD owns independent metric and angular spacing and visual fields",
graticuleLineWidthPx: 1 + (index % 3), graticuleLineWidthPx: 1 + (index % 3),
graticuleColor: `#00000${index}`, graticuleColor: `#00000${index}`,
graticuleOpacity: 30 + index, graticuleOpacity: 30 + index,
selectionFillColor: `#12345${index}`,
selectionFillOpacityPercent: 40 + index,
selectionOutlineColor: `#abcde${index}`,
selectionOutlineWidthPx: 4 + index,
selectionOutlineOpacityPercent: 70 + index,
})); }));
const profile = gridLodProfile({ ...settings, gridLodProfiles: profiles }, 3); const profile = gridLodProfile({ ...settings, gridLodProfiles: profiles }, 3);
assert.equal(profile.stepKm, 14); assert.equal(profile.stepKm, 14);
@@ -218,6 +233,29 @@ test("every LOD owns independent metric and angular spacing and visual fields",
assert.equal(profile.graticuleLineWidthPx, 1); assert.equal(profile.graticuleLineWidthPx, 1);
assert.equal(profile.graticuleColor, "#000003"); assert.equal(profile.graticuleColor, "#000003");
assert.equal(profile.graticuleOpacity, 33); assert.equal(profile.graticuleOpacity, 33);
assert.equal(profile.selectionFillColor, "#123453");
assert.equal(profile.selectionFillOpacityPercent, 43);
assert.equal(profile.selectionOutlineColor, "#abcde3");
assert.equal(profile.selectionOutlineWidthPx, 7);
assert.equal(profile.selectionOutlineOpacityPercent, 73);
});
test("selection visuals clamp to their public UI contract and a 1,000 m band is accepted client-side", () => {
const profile = gridLodProfile({
...settings,
gridLodProfiles: DEFAULT_GRID_LOD_PROFILES.map((item, index) => index === 0 ? {
...item,
volumeMaximumHeightMeters: 1_000,
volumeBandHeightMeters: 1_000,
selectionFillOpacityPercent: 110,
selectionOutlineWidthPx: 30,
selectionOutlineOpacityPercent: -5,
} : item),
}, 0);
assert.equal(profile.volumeBandHeightMeters, 1_000);
assert.equal(profile.selectionFillOpacityPercent, 100);
assert.equal(profile.selectionOutlineWidthPx, 12);
assert.equal(profile.selectionOutlineOpacityPercent, 0);
}); });
test("legacy flat visuals migrate while angular graticule spacing stays independent", () => { test("legacy flat visuals migrate while angular graticule spacing stays independent", () => {
+27
View File
@@ -538,6 +538,8 @@ const GRID_LOD_PROFILE_KEYS = new Set([
"crossesColor", "crossesOpacity", "graticuleStepDegrees", "graticuleLineWidthPx", "graticuleColor", "crossesColor", "crossesOpacity", "graticuleStepDegrees", "graticuleLineWidthPx", "graticuleColor",
"graticuleOpacity", "majorLinesEnabled", "majorLabelsEnabled", "majorLineWidthMultiplier", "graticuleOpacity", "majorLinesEnabled", "majorLabelsEnabled", "majorLineWidthMultiplier",
"volumeEnabled", "volumeMinimumHeightMeters", "volumeMaximumHeightMeters", "volumeBandHeightMeters", "volumeEnabled", "volumeMinimumHeightMeters", "volumeMaximumHeightMeters", "volumeBandHeightMeters",
"selectionFillColor", "selectionFillOpacityPercent", "selectionOutlineColor", "selectionOutlineWidthPx",
"selectionOutlineOpacityPercent",
]); ]);
const GRID_LOD_PROFILE_INPUT_KEYS = new Set([...GRID_LOD_PROFILE_KEYS, "lineWidthPx"]); const GRID_LOD_PROFILE_INPUT_KEYS = new Set([...GRID_LOD_PROFILE_KEYS, "lineWidthPx"]);
@@ -564,6 +566,8 @@ const DEFAULT_GRID_LOD_PROFILES = Object.freeze([
graticuleStepDegrees: 0.25, graticuleLineWidthPx: 1, graticuleColor: "#f5f5f5", graticuleOpacity: 12, graticuleStepDegrees: 0.25, graticuleLineWidthPx: 1, graticuleColor: "#f5f5f5", graticuleOpacity: 12,
majorLinesEnabled: true, majorLabelsEnabled: true, majorLineWidthMultiplier: 2.5, majorLinesEnabled: true, majorLabelsEnabled: true, majorLineWidthMultiplier: 2.5,
volumeEnabled: true, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 300, volumeBandHeightMeters: 300, volumeEnabled: true, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 300, volumeBandHeightMeters: 300,
selectionFillColor: "#35cfff", selectionFillOpacityPercent: 10,
selectionOutlineColor: "#35cfff", selectionOutlineWidthPx: 6.25, selectionOutlineOpacityPercent: 95,
}), }),
Object.freeze({ Object.freeze({
maxHeightKm: 50, stepKm: 5, mode: "3d", heightMeters: 500, max3dViewAngleDegrees: 30, maxHeightKm: 50, stepKm: 5, mode: "3d", heightMeters: 500, max3dViewAngleDegrees: 30,
@@ -573,6 +577,8 @@ const DEFAULT_GRID_LOD_PROFILES = Object.freeze([
graticuleStepDegrees: 0.5, graticuleLineWidthPx: 1, graticuleColor: "#f8fbfc", graticuleOpacity: 12, graticuleStepDegrees: 0.5, graticuleLineWidthPx: 1, graticuleColor: "#f8fbfc", graticuleOpacity: 12,
majorLinesEnabled: true, majorLabelsEnabled: true, majorLineWidthMultiplier: 2.5, majorLinesEnabled: true, majorLabelsEnabled: true, majorLineWidthMultiplier: 2.5,
volumeEnabled: true, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 500, volumeBandHeightMeters: 500, volumeEnabled: true, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 500, volumeBandHeightMeters: 500,
selectionFillColor: "#35cfff", selectionFillOpacityPercent: 10,
selectionOutlineColor: "#35cfff", selectionOutlineWidthPx: 6.25, selectionOutlineOpacityPercent: 95,
}), }),
Object.freeze({ Object.freeze({
maxHeightKm: 200, stepKm: 25, mode: "3d", heightMeters: 500, max3dViewAngleDegrees: 30, maxHeightKm: 200, stepKm: 25, mode: "3d", heightMeters: 500, max3dViewAngleDegrees: 30,
@@ -582,6 +588,8 @@ const DEFAULT_GRID_LOD_PROFILES = Object.freeze([
graticuleStepDegrees: 1, graticuleLineWidthPx: 1, graticuleColor: "#fafafa", graticuleOpacity: 12, graticuleStepDegrees: 1, graticuleLineWidthPx: 1, graticuleColor: "#fafafa", graticuleOpacity: 12,
majorLinesEnabled: true, majorLabelsEnabled: false, majorLineWidthMultiplier: 2, majorLinesEnabled: true, majorLabelsEnabled: false, majorLineWidthMultiplier: 2,
volumeEnabled: false, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 500, volumeBandHeightMeters: 500, volumeEnabled: false, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 500, volumeBandHeightMeters: 500,
selectionFillColor: "#35cfff", selectionFillOpacityPercent: 16,
selectionOutlineColor: "#35cfff", selectionOutlineWidthPx: 5, selectionOutlineOpacityPercent: 95,
}), }),
Object.freeze({ Object.freeze({
maxHeightKm: 800, stepKm: 50, mode: "graticule", heightMeters: 500, max3dViewAngleDegrees: 30, maxHeightKm: 800, stepKm: 50, mode: "graticule", heightMeters: 500, max3dViewAngleDegrees: 30,
@@ -591,6 +599,8 @@ const DEFAULT_GRID_LOD_PROFILES = Object.freeze([
graticuleStepDegrees: 2, graticuleLineWidthPx: 1, graticuleColor: "#fcfdfd", graticuleOpacity: 12, graticuleStepDegrees: 2, graticuleLineWidthPx: 1, graticuleColor: "#fcfdfd", graticuleOpacity: 12,
majorLinesEnabled: true, majorLabelsEnabled: false, majorLineWidthMultiplier: 2, majorLinesEnabled: true, majorLabelsEnabled: false, majorLineWidthMultiplier: 2,
volumeEnabled: false, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 500, volumeBandHeightMeters: 500, volumeEnabled: false, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 500, volumeBandHeightMeters: 500,
selectionFillColor: "#35cfff", selectionFillOpacityPercent: 12,
selectionOutlineColor: "#35cfff", selectionOutlineWidthPx: 3, selectionOutlineOpacityPercent: 95,
}), }),
Object.freeze({ Object.freeze({
maxHeightKm: 3_000, stepKm: 50, mode: "graticule", heightMeters: 500, max3dViewAngleDegrees: 30, maxHeightKm: 3_000, stepKm: 50, mode: "graticule", heightMeters: 500, max3dViewAngleDegrees: 30,
@@ -600,6 +610,8 @@ const DEFAULT_GRID_LOD_PROFILES = Object.freeze([
graticuleStepDegrees: 2, graticuleLineWidthPx: 1, graticuleColor: "#9c9c9c", graticuleOpacity: 8, graticuleStepDegrees: 2, graticuleLineWidthPx: 1, graticuleColor: "#9c9c9c", graticuleOpacity: 8,
majorLinesEnabled: true, majorLabelsEnabled: false, majorLineWidthMultiplier: 2, majorLinesEnabled: true, majorLabelsEnabled: false, majorLineWidthMultiplier: 2,
volumeEnabled: false, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 500, volumeBandHeightMeters: 500, volumeEnabled: false, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 500, volumeBandHeightMeters: 500,
selectionFillColor: "#35cfff", selectionFillOpacityPercent: 12,
selectionOutlineColor: "#35cfff", selectionOutlineWidthPx: 3, selectionOutlineOpacityPercent: 95,
}), }),
]); ]);
@@ -618,6 +630,11 @@ function promoteLegacyGridLodProfiles(settings) {
volumeMinimumHeightMeters: _volumeMinimumHeightMeters, volumeMinimumHeightMeters: _volumeMinimumHeightMeters,
volumeMaximumHeightMeters: _volumeMaximumHeightMeters, volumeMaximumHeightMeters: _volumeMaximumHeightMeters,
volumeBandHeightMeters: _volumeBandHeightMeters, volumeBandHeightMeters: _volumeBandHeightMeters,
selectionFillColor: _selectionFillColor,
selectionFillOpacityPercent: _selectionFillOpacityPercent,
selectionOutlineColor: _selectionOutlineColor,
selectionOutlineWidthPx: _selectionOutlineWidthPx,
selectionOutlineOpacityPercent: _selectionOutlineOpacityPercent,
...legacyFallback ...legacyFallback
} = fallback; } = fallback;
return { return {
@@ -679,6 +696,11 @@ function validateGridLodProfiles(value) {
volumeMinimumHeightMeters: profile.volumeMinimumHeightMeters ?? DEFAULT_GRID_LOD_PROFILES[index].volumeMinimumHeightMeters, volumeMinimumHeightMeters: profile.volumeMinimumHeightMeters ?? DEFAULT_GRID_LOD_PROFILES[index].volumeMinimumHeightMeters,
volumeMaximumHeightMeters: profile.volumeMaximumHeightMeters ?? DEFAULT_GRID_LOD_PROFILES[index].volumeMaximumHeightMeters, volumeMaximumHeightMeters: profile.volumeMaximumHeightMeters ?? DEFAULT_GRID_LOD_PROFILES[index].volumeMaximumHeightMeters,
volumeBandHeightMeters: profile.volumeBandHeightMeters ?? DEFAULT_GRID_LOD_PROFILES[index].volumeBandHeightMeters, volumeBandHeightMeters: profile.volumeBandHeightMeters ?? DEFAULT_GRID_LOD_PROFILES[index].volumeBandHeightMeters,
selectionFillColor: profile.selectionFillColor ?? DEFAULT_GRID_LOD_PROFILES[index].selectionFillColor,
selectionFillOpacityPercent: profile.selectionFillOpacityPercent ?? DEFAULT_GRID_LOD_PROFILES[index].selectionFillOpacityPercent,
selectionOutlineColor: profile.selectionOutlineColor ?? DEFAULT_GRID_LOD_PROFILES[index].selectionOutlineColor,
selectionOutlineWidthPx: profile.selectionOutlineWidthPx ?? DEFAULT_GRID_LOD_PROFILES[index].selectionOutlineWidthPx,
selectionOutlineOpacityPercent: profile.selectionOutlineOpacityPercent ?? DEFAULT_GRID_LOD_PROFILES[index].selectionOutlineOpacityPercent,
}; };
delete normalized.lineWidthPx; delete normalized.lineWidthPx;
if (GRID_LOD_PROFILE_KEYS.size !== Object.keys(normalized).length if (GRID_LOD_PROFILE_KEYS.size !== Object.keys(normalized).length
@@ -771,6 +793,11 @@ function validateGridLodProfiles(value) {
volumeMinimumHeightMeters, volumeMinimumHeightMeters,
volumeMaximumHeightMeters, volumeMaximumHeightMeters,
volumeBandHeightMeters, volumeBandHeightMeters,
selectionFillColor: requireHex(normalized.selectionFillColor, code("selectionFillColor")),
selectionFillOpacityPercent: requireNumber(normalized.selectionFillOpacityPercent, 0, 100, code("selectionFillOpacityPercent")),
selectionOutlineColor: requireHex(normalized.selectionOutlineColor, code("selectionOutlineColor")),
selectionOutlineWidthPx: requireNumber(normalized.selectionOutlineWidthPx, 1, 12, code("selectionOutlineWidthPx")),
selectionOutlineOpacityPercent: requireNumber(normalized.selectionOutlineOpacityPercent, 0, 100, code("selectionOutlineOpacityPercent")),
}; };
}); });
} }
+5
View File
@@ -492,6 +492,11 @@ const mapPageSettingsPatchInputSchema = {
volumeMinimumHeightMeters: { type: "number", minimum: -1000, maximum: 10000 }, volumeMinimumHeightMeters: { type: "number", minimum: -1000, maximum: 10000 },
volumeMaximumHeightMeters: { type: "number", minimum: -1000, maximum: 10000 }, volumeMaximumHeightMeters: { type: "number", minimum: -1000, maximum: 10000 },
volumeBandHeightMeters: { type: "number", minimum: 1, maximum: 10000 }, volumeBandHeightMeters: { type: "number", minimum: 1, maximum: 10000 },
selectionFillColor: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" },
selectionFillOpacityPercent: { type: "number", minimum: 0, maximum: 100 },
selectionOutlineColor: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" },
selectionOutlineWidthPx: { type: "number", minimum: 1, maximum: 12 },
selectionOutlineOpacityPercent: { type: "number", minimum: 0, maximum: 100 },
}, },
}, },
}, },
+42
View File
@@ -15,6 +15,11 @@ const extensionKeys = [
"volumeMinimumHeightMeters", "volumeMinimumHeightMeters",
"volumeMaximumHeightMeters", "volumeMaximumHeightMeters",
"volumeBandHeightMeters", "volumeBandHeightMeters",
"selectionFillColor",
"selectionFillOpacityPercent",
"selectionOutlineColor",
"selectionOutlineWidthPx",
"selectionOutlineOpacityPercent",
]; ];
function applicationError(code, statusCode = 400) { function applicationError(code, statusCode = 400) {
@@ -96,3 +101,40 @@ test("flat layouts migrate mode-aware extensions while explicit invalid hierarch
test("Foundry MCP exposes every persisted sector-v2 profile field", () => { test("Foundry MCP exposes every persisted sector-v2 profile field", () => {
for (const key of extensionKeys) assert.match(mcpSource, new RegExp(`\\b${key}: \\{`)); for (const key of extensionKeys) assert.match(mcpSource, new RegExp(`\\b${key}: \\{`));
}); });
test("server persists selected-sector visuals and accepts a 1,000 m address band inside its range", () => {
const profiles = structuredClone(DEFAULT_GRID_LOD_PROFILES);
Object.assign(profiles[0], {
volumeMaximumHeightMeters: 1_000,
volumeBandHeightMeters: 1_000,
selectionFillColor: "#A0B1C2",
selectionFillOpacityPercent: 34,
selectionOutlineColor: "#C3D4E5",
selectionOutlineWidthPx: 7.5,
selectionOutlineOpacityPercent: 81,
});
const validated = structuredClone(serverGrid.validateGridLodProfiles(profiles));
assert.equal(validated[0].volumeBandHeightMeters, 1_000);
assert.equal(validated[0].selectionFillColor, "#a0b1c2");
assert.equal(validated[0].selectionFillOpacityPercent, 34);
assert.equal(validated[0].selectionOutlineColor, "#c3d4e5");
assert.equal(validated[0].selectionOutlineWidthPx, 7.5);
assert.equal(validated[0].selectionOutlineOpacityPercent, 81);
});
test("server rejects selected-sector visual values outside the canonical bounds", () => {
for (const [field, value] of [
["selectionFillColor", "cyan"],
["selectionFillOpacityPercent", 101],
["selectionOutlineColor", "#ffff"],
["selectionOutlineWidthPx", 12.01],
["selectionOutlineOpacityPercent", -1],
]) {
const profiles = structuredClone(DEFAULT_GRID_LOD_PROFILES);
profiles[0][field] = value;
assert.throws(
() => serverGrid.validateGridLodProfiles(profiles),
new RegExp(`invalid_map_grid_lod_profile_1_${field}`),
);
}
});