feat(map): refine sector inspector styling
This commit is contained in:
@@ -316,6 +316,11 @@ export type GridLodProfile = {
|
||||
volumeMinimumHeightMeters: number;
|
||||
volumeMaximumHeightMeters: number;
|
||||
volumeBandHeightMeters: number;
|
||||
selectionFillColor: string;
|
||||
selectionFillOpacityPercent: number;
|
||||
selectionOutlineColor: string;
|
||||
selectionOutlineWidthPx: number;
|
||||
selectionOutlineOpacityPercent: number;
|
||||
};
|
||||
|
||||
export type GridVolumeSelection = {
|
||||
@@ -1006,6 +1011,11 @@ type LocalGridAddressing = {
|
||||
volumeMinimumHeightMeters: number;
|
||||
volumeMaximumHeightMeters: number;
|
||||
volumeBandHeightMeters: number;
|
||||
selectionFillColor: string;
|
||||
selectionFillOpacityPercent: number;
|
||||
selectionOutlineColor: string;
|
||||
selectionOutlineWidthPx: number;
|
||||
selectionOutlineOpacityPercent: number;
|
||||
};
|
||||
type GraticuleGridAddressing = {
|
||||
mode: "graticule";
|
||||
@@ -1017,6 +1027,11 @@ type GraticuleGridAddressing = {
|
||||
lineColor: string;
|
||||
lineOpacity: number;
|
||||
majorLineWidthMultiplier: number;
|
||||
selectionFillColor: string;
|
||||
selectionFillOpacityPercent: number;
|
||||
selectionOutlineColor: string;
|
||||
selectionOutlineWidthPx: number;
|
||||
selectionOutlineOpacityPercent: number;
|
||||
};
|
||||
type GridAddressing = LocalGridAddressing | GraticuleGridAddressing;
|
||||
type GridResources = {
|
||||
@@ -1525,7 +1540,56 @@ function materializeGridLayer(plan: Exclude<GridLayerPlan, HiddenGridPlan>, seri
|
||||
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) {
|
||||
const span = end - start;
|
||||
@@ -1562,6 +1626,7 @@ function materializeLocalSelection(
|
||||
|
||||
const dataSource = new CustomDataSource(`nodedc-map-grid-selection-local:${serial}`);
|
||||
const crosses = new PolylineCollection();
|
||||
const selectionStyle = resolveGridSelectionStyle(addressing);
|
||||
const volumeAvailable = addressing.volumeEnabled
|
||||
&& Number.isFinite(addressing.volumeBandHeightMeters)
|
||||
&& addressing.volumeBandHeightMeters > 0
|
||||
@@ -1582,9 +1647,9 @@ function materializeLocalSelection(
|
||||
const polyline = crosses.add({
|
||||
id: { kind: "nodedc-grid-selection", sectorId: selection.id, edgeId: id },
|
||||
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);
|
||||
if (ceiling > floor + 1e-6) addLine("ceiling", ceilingPerimeter);
|
||||
@@ -1608,7 +1673,7 @@ function materializeLocalSelection(
|
||||
polygon: {
|
||||
hierarchy: new PolygonHierarchy(ceilingSurface),
|
||||
perPositionHeight: true,
|
||||
material: GRID_SELECTION_COLOR.withAlpha(volumeAvailable ? 0.1 : 0.16),
|
||||
material: selectionStyle.fillColor,
|
||||
outline: false,
|
||||
},
|
||||
});
|
||||
@@ -1621,6 +1686,7 @@ function materializeGraticuleSelection(
|
||||
serial: number,
|
||||
): GridResources {
|
||||
const dataSource = new CustomDataSource(`nodedc-map-grid-selection-graticule:${serial}`);
|
||||
const selectionStyle = resolveGridSelectionStyle(addressing);
|
||||
const height = Math.max(0, addressing.heightMeters);
|
||||
const horizontalStep = 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`,
|
||||
polyline: {
|
||||
positions: perimeter,
|
||||
width: clamp(addressing.lineWidthPixels * addressing.majorLineWidthMultiplier * 1.5, 3, 12),
|
||||
material: GRID_SELECTION_COLOR.withAlpha(0.95),
|
||||
width: selectionStyle.outlineWidthPixels,
|
||||
material: selectionStyle.outlineColor,
|
||||
clampToGround,
|
||||
arcType: ArcType.RHUMB,
|
||||
granularity: graticuleGranularity(horizontalStep, clampToGround),
|
||||
@@ -1654,7 +1720,7 @@ function materializeGraticuleSelection(
|
||||
height: clampToGround ? undefined : height,
|
||||
heightReference: clampToGround ? HeightReference.CLAMP_TO_GROUND : HeightReference.NONE,
|
||||
arcType: ArcType.RHUMB,
|
||||
material: GRID_SELECTION_COLOR.withAlpha(0.12),
|
||||
material: selectionStyle.fillColor,
|
||||
outline: false,
|
||||
},
|
||||
});
|
||||
@@ -1712,12 +1778,38 @@ class GridLayerController {
|
||||
: 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) {
|
||||
// Planning is intentionally cheap. Geometry is materialized only after
|
||||
// key comparison; repeated camera/settings notifications cannot create
|
||||
// and discard thousands of Cesium objects for an unchanged layer.
|
||||
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.lodIndex = plan.lodIndex;
|
||||
const epoch = ++this.epoch;
|
||||
@@ -1751,6 +1843,11 @@ class GridLayerController {
|
||||
volumeMinimumHeightMeters: plan.lod.volumeMinimumHeightMeters,
|
||||
volumeMaximumHeightMeters: plan.lod.volumeMaximumHeightMeters,
|
||||
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",
|
||||
lod: plan.lod.index + 1,
|
||||
@@ -1761,6 +1858,11 @@ class GridLayerController {
|
||||
lineColor: plan.lod.graticuleColor,
|
||||
lineOpacity: plan.lod.graticuleOpacity,
|
||||
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;
|
||||
if (activeAddressing && this.addressingKey(activeAddressing) !== this.addressingKey(addressing)) {
|
||||
@@ -1943,6 +2045,11 @@ class GridLayerController {
|
||||
addressing.lineColor,
|
||||
addressing.lineOpacity,
|
||||
addressing.majorLineWidthMultiplier,
|
||||
addressing.selectionFillColor,
|
||||
addressing.selectionFillOpacityPercent,
|
||||
addressing.selectionOutlineColor,
|
||||
addressing.selectionOutlineWidthPx,
|
||||
addressing.selectionOutlineOpacityPercent,
|
||||
addressing.mode === "3d" ? [
|
||||
addressing.volumeEnabled,
|
||||
addressing.volumeMinimumHeightMeters,
|
||||
|
||||
@@ -75,6 +75,11 @@ type SectorGridLodProfile = GridLodProfile & {
|
||||
majorLinesEnabled: boolean;
|
||||
majorLabelsEnabled: boolean;
|
||||
majorLineWidthMultiplier: number;
|
||||
selectionFillColor: string;
|
||||
selectionFillOpacityPercent: number;
|
||||
selectionOutlineColor: string;
|
||||
selectionOutlineWidthPx: number;
|
||||
selectionOutlineOpacityPercent: number;
|
||||
volumeEnabled: boolean;
|
||||
volumeMinimumHeightMeters: number;
|
||||
volumeMaximumHeightMeters: number;
|
||||
@@ -981,12 +986,22 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
const updateGridVolumeRange = (patch: Partial<Pick<SectorGridLodProfile,
|
||||
"volumeMinimumHeightMeters" | "volumeMaximumHeightMeters" | "volumeBandHeightMeters">>) => {
|
||||
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;
|
||||
updateGridLod({
|
||||
volumeMinimumHeightMeters: minimum,
|
||||
volumeMaximumHeightMeters: maximum,
|
||||
volumeBandHeightMeters: Math.min(span, Math.max(1, patch.volumeBandHeightMeters ?? activeGridLod.volumeBandHeightMeters)),
|
||||
volumeBandHeightMeters: Math.min(span, requestedBandHeight),
|
||||
});
|
||||
};
|
||||
const setCacheEnabled = (cacheEnabled: boolean) => {
|
||||
@@ -1632,7 +1647,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
label: "Сетка и LOD",
|
||||
description: "first adapter control",
|
||||
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>
|
||||
<Checker checked={mapSettings.gridVisible} label="Сетка" onChange={(gridVisible) => updateMapSettings({ gridVisible })} />
|
||||
<Checker checked={mapSettings.grid3dEnabled} label="3D-сетка" onChange={(grid3dEnabled) => updateMapSettings({ grid3dEnabled })} />
|
||||
@@ -1743,13 +1758,55 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
label="Высота адресного диапазона"
|
||||
value={activeGridLod.volumeBandHeightMeters}
|
||||
min={1}
|
||||
max={Math.max(1, activeGridLod.volumeMaximumHeightMeters - activeGridLod.volumeMinimumHeightMeters)}
|
||||
max={1_000}
|
||||
step={10}
|
||||
formatValue={(value) => `${value} м`}
|
||||
onChange={(volumeBandHeightMeters) => updateGridVolumeRange({ volumeBandHeightMeters })}
|
||||
/>
|
||||
<small className="catalog-map-inspector__note">Горизонтальный ID сектора остаётся стабильным. Высотный band добавляется как отдельный адрес внутри выбранной ENU-ячейки.</small>
|
||||
</> : 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="Выбранный сектор">
|
||||
<ControlRow label="Выбранный сектор"><strong className="catalog-map-grid-sector-id">{selectedGridSector?.id ?? "Нажмите сектор на карте"}</strong></ControlRow>
|
||||
{selectedGridSector ? <>
|
||||
@@ -1841,7 +1898,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
|
||||
</div> : null}
|
||||
</> : <small className="catalog-map-inspector__note">Кликните ячейку, чтобы получить устойчивый адрес, геометрию и навигацию по соседям.</small>}
|
||||
</section>
|
||||
</>,
|
||||
</div>,
|
||||
},
|
||||
{
|
||||
id: "map-camera-animation",
|
||||
|
||||
@@ -32,6 +32,11 @@ export type GridLodProfile = {
|
||||
volumeMinimumHeightMeters: number;
|
||||
volumeMaximumHeightMeters: number;
|
||||
volumeBandHeightMeters: number;
|
||||
selectionFillColor: string;
|
||||
selectionFillOpacityPercent: number;
|
||||
selectionOutlineColor: string;
|
||||
selectionOutlineWidthPx: number;
|
||||
selectionOutlineOpacityPercent: number;
|
||||
};
|
||||
export type GridLodBand = GridLodProfile & {
|
||||
index: number;
|
||||
|
||||
@@ -18,6 +18,8 @@ export const DEFAULT_GRID_LOD_PROFILES = Object.freeze([
|
||||
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,
|
||||
}),
|
||||
Object.freeze({
|
||||
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,
|
||||
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,
|
||||
}),
|
||||
Object.freeze({
|
||||
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,
|
||||
majorLinesEnabled: true, majorLabelsEnabled: false, majorLineWidthMultiplier: 2,
|
||||
volumeEnabled: false, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 500, volumeBandHeightMeters: 500,
|
||||
selectionFillColor: "#35cfff", selectionFillOpacityPercent: 16,
|
||||
selectionOutlineColor: "#35cfff", selectionOutlineWidthPx: 5, selectionOutlineOpacityPercent: 95,
|
||||
}),
|
||||
Object.freeze({
|
||||
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,
|
||||
majorLinesEnabled: true, majorLabelsEnabled: false, majorLineWidthMultiplier: 2,
|
||||
volumeEnabled: false, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 500, volumeBandHeightMeters: 500,
|
||||
selectionFillColor: "#35cfff", selectionFillOpacityPercent: 12,
|
||||
selectionOutlineColor: "#35cfff", selectionOutlineWidthPx: 3, selectionOutlineOpacityPercent: 95,
|
||||
}),
|
||||
Object.freeze({
|
||||
// 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,
|
||||
majorLinesEnabled: true, majorLabelsEnabled: false, majorLineWidthMultiplier: 2,
|
||||
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),
|
||||
volumeMaximumHeightMeters: clamp(finite(source.volumeMaximumHeightMeters, fallback.volumeMaximumHeightMeters), -1_000, 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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
@@ -973,22 +1030,70 @@ textarea {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
padding-inline: 0.45rem;
|
||||
font-size: var(--nodedc-font-size-xs);
|
||||
font-size: var(--nodedc-font-size-sm);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.catalog-map-grid-sector-id {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
text-align: right;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.catalog-map-grid-sector {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
gap: 0.55rem;
|
||||
border-top: 1px solid color-mix(in srgb, var(--nodedc-glass-outline) 44%, transparent);
|
||||
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,
|
||||
@@ -1009,7 +1114,10 @@ textarea {
|
||||
.catalog-map-grid-sector__neighbor,
|
||||
.catalog-map-grid-sector__volume {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
gap: 0.4rem;
|
||||
border-radius: var(--nodedc-radius-control);
|
||||
background: var(--nodedc-glass-control-bg);
|
||||
@@ -1020,7 +1128,6 @@ textarea {
|
||||
.catalog-map-grid-sector__neighbor > code,
|
||||
.catalog-map-grid-sector__volume > code {
|
||||
color: var(--nodedc-text-muted);
|
||||
font-size: var(--nodedc-font-size-xs);
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
|
||||
@@ -195,10 +195,18 @@ Per-entity `PolygonGraphics` для массового слоя геозон н
|
||||
- LOD 1–2 по умолчанию поддерживают отдельный высотный адрес ENU-объёма.
|
||||
Диапазон задаётся как полуоткрытый `[minimum, maximum)`, делится на bands, а
|
||||
последний band может быть короче. Volume id является дочерним адресом и не
|
||||
подменяет стабильный id горизонтального сектора;
|
||||
подменяет стабильный id горизонтального сектора. Inspector позволяет задать
|
||||
band до `1000 м`; если он превышает текущий span, верхняя отметка объёма
|
||||
расширяется вместе с ним, поэтому persisted policy всегда сохраняет
|
||||
инвариант `band <= maximum - minimum`;
|
||||
- Cesium материализует пространственную клетку только для выбранного сектора:
|
||||
нижнюю и верхнюю рамки, вертикальные рёбра и границы bands. Вся ENU-плоскость
|
||||
не размножается в объёмные entities, поэтому стоимость выбора ограничена;
|
||||
- заливка и outline выбранного сектора являются независимыми настройками
|
||||
каждого LOD: `selectionFillColor`, `selectionFillOpacityPercent`,
|
||||
`selectionOutlineColor`, `selectionOutlineWidthPx` и
|
||||
`selectionOutlineOpacityPercent`. Legacy layouts получают визуально
|
||||
эквивалентные cyan-defaults при миграции;
|
||||
- minor и major линии собираются раздельно. Major получает только множитель
|
||||
толщины, наследует цвет/прозрачность текущего LOD, а подписи создаются
|
||||
ограниченным набором вокруг viewport и не являются источником адресации;
|
||||
|
||||
@@ -75,11 +75,11 @@
|
||||
"gridCrossesColor": "#9c9c9c",
|
||||
"gridCrossesOpacity": 46,
|
||||
"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": 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": 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": 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": 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": 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, "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, "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, "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, "selectionFillColor": "#35cfff", "selectionFillOpacityPercent": 12, "selectionOutlineColor": "#35cfff", "selectionOutlineWidthPx": 3, "selectionOutlineOpacityPercent": 95 }
|
||||
]
|
||||
},
|
||||
"mapHeight": 620,
|
||||
|
||||
@@ -31,6 +31,11 @@ test("canonical defaults preserve the exact effective MMAP/MOSCOWMAP five-LOD do
|
||||
volumeMinimumHeightMeters: _volumeMinimumHeightMeters,
|
||||
volumeMaximumHeightMeters: _volumeMaximumHeightMeters,
|
||||
volumeBandHeightMeters: _volumeBandHeightMeters,
|
||||
selectionFillColor: _selectionFillColor,
|
||||
selectionFillOpacityPercent: _selectionFillOpacityPercent,
|
||||
selectionOutlineColor: _selectionOutlineColor,
|
||||
selectionOutlineWidthPx: _selectionOutlineWidthPx,
|
||||
selectionOutlineOpacityPercent: _selectionOutlineOpacityPercent,
|
||||
...profile
|
||||
}) => profile);
|
||||
assert.deepEqual(donorProfiles, [
|
||||
@@ -168,12 +173,17 @@ test("canonical defaults preserve the exact effective MMAP/MOSCOWMAP five-LOD do
|
||||
volumeMinimumHeightMeters: profile.volumeMinimumHeightMeters,
|
||||
volumeMaximumHeightMeters: profile.volumeMaximumHeightMeters,
|
||||
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: 500, volumeBandHeightMeters: 500 },
|
||||
{ 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 },
|
||||
{ majorLinesEnabled: true, majorLabelsEnabled: false, majorLineWidthMultiplier: 2, volumeEnabled: false, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 500, volumeBandHeightMeters: 500 },
|
||||
{ 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, 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, selectionFillColor: "#35cfff", selectionFillOpacityPercent: 16, selectionOutlineColor: "#35cfff", selectionOutlineWidthPx: 5, selectionOutlineOpacityPercent: 95 },
|
||||
{ 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, 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),
|
||||
graticuleColor: `#00000${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);
|
||||
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.graticuleColor, "#000003");
|
||||
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", () => {
|
||||
|
||||
@@ -538,6 +538,8 @@ const GRID_LOD_PROFILE_KEYS = new Set([
|
||||
"crossesColor", "crossesOpacity", "graticuleStepDegrees", "graticuleLineWidthPx", "graticuleColor",
|
||||
"graticuleOpacity", "majorLinesEnabled", "majorLabelsEnabled", "majorLineWidthMultiplier",
|
||||
"volumeEnabled", "volumeMinimumHeightMeters", "volumeMaximumHeightMeters", "volumeBandHeightMeters",
|
||||
"selectionFillColor", "selectionFillOpacityPercent", "selectionOutlineColor", "selectionOutlineWidthPx",
|
||||
"selectionOutlineOpacityPercent",
|
||||
]);
|
||||
|
||||
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,
|
||||
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,
|
||||
}),
|
||||
Object.freeze({
|
||||
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,
|
||||
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,
|
||||
}),
|
||||
Object.freeze({
|
||||
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,
|
||||
majorLinesEnabled: true, majorLabelsEnabled: false, majorLineWidthMultiplier: 2,
|
||||
volumeEnabled: false, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 500, volumeBandHeightMeters: 500,
|
||||
selectionFillColor: "#35cfff", selectionFillOpacityPercent: 16,
|
||||
selectionOutlineColor: "#35cfff", selectionOutlineWidthPx: 5, selectionOutlineOpacityPercent: 95,
|
||||
}),
|
||||
Object.freeze({
|
||||
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,
|
||||
majorLinesEnabled: true, majorLabelsEnabled: false, majorLineWidthMultiplier: 2,
|
||||
volumeEnabled: false, volumeMinimumHeightMeters: 0, volumeMaximumHeightMeters: 500, volumeBandHeightMeters: 500,
|
||||
selectionFillColor: "#35cfff", selectionFillOpacityPercent: 12,
|
||||
selectionOutlineColor: "#35cfff", selectionOutlineWidthPx: 3, selectionOutlineOpacityPercent: 95,
|
||||
}),
|
||||
Object.freeze({
|
||||
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,
|
||||
majorLinesEnabled: true, majorLabelsEnabled: false, majorLineWidthMultiplier: 2,
|
||||
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,
|
||||
volumeMaximumHeightMeters: _volumeMaximumHeightMeters,
|
||||
volumeBandHeightMeters: _volumeBandHeightMeters,
|
||||
selectionFillColor: _selectionFillColor,
|
||||
selectionFillOpacityPercent: _selectionFillOpacityPercent,
|
||||
selectionOutlineColor: _selectionOutlineColor,
|
||||
selectionOutlineWidthPx: _selectionOutlineWidthPx,
|
||||
selectionOutlineOpacityPercent: _selectionOutlineOpacityPercent,
|
||||
...legacyFallback
|
||||
} = fallback;
|
||||
return {
|
||||
@@ -679,6 +696,11 @@ function validateGridLodProfiles(value) {
|
||||
volumeMinimumHeightMeters: profile.volumeMinimumHeightMeters ?? DEFAULT_GRID_LOD_PROFILES[index].volumeMinimumHeightMeters,
|
||||
volumeMaximumHeightMeters: profile.volumeMaximumHeightMeters ?? DEFAULT_GRID_LOD_PROFILES[index].volumeMaximumHeightMeters,
|
||||
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;
|
||||
if (GRID_LOD_PROFILE_KEYS.size !== Object.keys(normalized).length
|
||||
@@ -771,6 +793,11 @@ function validateGridLodProfiles(value) {
|
||||
volumeMinimumHeightMeters,
|
||||
volumeMaximumHeightMeters,
|
||||
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 },
|
||||
volumeMaximumHeightMeters: { type: "number", minimum: -1000, 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",
|
||||
"volumeMaximumHeightMeters",
|
||||
"volumeBandHeightMeters",
|
||||
"selectionFillColor",
|
||||
"selectionFillOpacityPercent",
|
||||
"selectionOutlineColor",
|
||||
"selectionOutlineWidthPx",
|
||||
"selectionOutlineOpacityPercent",
|
||||
];
|
||||
|
||||
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", () => {
|
||||
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