feat(map): refine sector inspector styling

This commit is contained in:
Codex
2026-08-06 12:10:26 +03:00
parent d4827009d4
commit e8729401fd
11 changed files with 437 additions and 26 deletions
+115 -8
View File
@@ -316,6 +316,11 @@ export type GridLodProfile = {
volumeMinimumHeightMeters: number;
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,