feat(map): refine sector inspector styling
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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 1–3. LOD 4–5 используют глобальную WGS84-гратику́лу; камера выбирает только LOD и видимую область.</small>
|
<small className="catalog-map-inspector__note">Фиксированная московская ENU-адресация задаёт неизменные сектора на LOD 1–3. LOD 4–5 используют глобальную 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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 и не являются источником адресации;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
@@ -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")),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user