feat(workbench): integrate operational workflows and map state

This commit is contained in:
2026-09-24 04:34:46 +08:00
parent 1752b9ef84
commit 436d5500f1
76 changed files with 5081 additions and 1937 deletions
@@ -1,6 +1,7 @@
import maplibregl, { type Map as MapLibreMap } from "maplibre-gl";
import {
CITY_LAYERS,
CITY_LIGHT,
CITY_ROOT,
cityManifestSchema,
citySources,
@@ -37,9 +38,10 @@ class CitySceneControl {
private addLayers() {
const map = this.map;
if (!map) return;
map.setLight(CITY_LIGHT);
Object.entries(citySources()).forEach(([id, source]) => map.addSource(id, source));
const firstBusinessLayer = findCitySceneInsertionLayerId(map.getStyle().layers);
CITY_LAYERS.forEach((layer) => map.addLayer(layer, firstBusinessLayer));
const firstHitLayer = findCitySceneInsertionLayerId(map.getStyle().layers);
CITY_LAYERS.forEach((layer) => map.addLayer(layer, firstHitLayer));
this.attribution = new maplibregl.AttributionControl({
compact: true,
customAttribution:
@@ -2,6 +2,7 @@ import type { LayerSpecification } from "maplibre-gl";
import { describe, expect, it } from "vitest";
import {
CITY_LAYERS,
CITY_LIGHT,
cityManifestSchema,
citySources,
findCitySceneInsertionLayerId
@@ -18,7 +19,7 @@ const manifest = {
attribution: "OSM"
};
describe("city scene geographic contract", () => {
it("inserts buildings above every basemap layer and below business layers", () => {
it("inserts buildings after business layers and before transparent hit layers", () => {
const layers = [
{ id: "background", type: "background" },
{ id: "mapbox-light", type: "raster", source: "mapbox-light" },
@@ -28,16 +29,26 @@ describe("city scene geographic contract", () => {
type: "raster",
source: "tianditu-vector-annotation"
},
{ id: "pipes", type: "line", source: "pipes", "source-layer": "pipes" }
{ id: "pipes-flow", type: "line", source: "pipes", "source-layer": "pipes" },
{ id: "pipes-hover", type: "line", source: "pipes", "source-layer": "pipes" },
{ id: "pipes-hit", type: "line", source: "pipes", "source-layer": "pipes" }
] satisfies LayerSpecification[];
expect(findCitySceneInsertionLayerId(layers)).toBe("pipes");
expect(findCitySceneInsertionLayerId(layers)).toBe("pipes-hit");
});
it("adds only the building background and leaves business layers to their existing controls", () => {
expect(Object.keys(citySources())).toEqual(["fengxian-city-buildings"]);
expect(CITY_LAYERS.map((layer) => layer.id)).toEqual(["fengxian-city-buildings"]);
});
it("uses a stable directional light for the engineering white model", () => {
expect(CITY_LIGHT).toEqual({
anchor: "map",
position: [1.5, 225, 35],
color: "#EAF2F4",
intensity: 0.42
});
});
it("accepts WGS84 scene bounds", () => {
expect(cityManifestSchema.safeParse(manifest).success).toBe(true);
});
@@ -1,14 +1,22 @@
import type { LayerSpecification, SourceSpecification } from "maplibre-gl";
import type { LayerSpecification, LightSpecification, SourceSpecification } from "maplibre-gl";
import { z } from "zod";
import { BASE_LAYER_IDS } from "./map-control-config";
import { INTERACTIVE_HIT_LAYER_IDS } from "./layers";
import { MAP_STYLE_TOKENS as colors } from "./map-colors";
export const CITY_ROOT = "/city-model";
export const CITY_PREFIX = "fengxian-city-";
export const CITY_BUILDING_LAYER_ID = CITY_PREFIX + "buildings";
export const CITY_LIGHT = {
anchor: "map",
position: [1.5, 225, 35],
color: "#EAF2F4",
intensity: 0.42
} satisfies LightSpecification;
export function findCitySceneInsertionLayerId(layers: readonly LayerSpecification[]) {
const basemapLayerIds = new Set<string>(BASE_LAYER_IDS);
return layers.find((layer) => layer.id !== "background" && !basemapLayerIds.has(layer.id))?.id;
// Layers painted after a 3D extrusion bypass depth testing, so ground overlays stay below it.
const hitLayerIds = new Set<string>(INTERACTIVE_HIT_LAYER_IDS);
return layers.find((layer) => hitLayerIds.has(layer.id))?.id;
}
export const cityManifestSchema = z
@@ -44,7 +52,7 @@ export function citySources(): Record<string, SourceSpecification> {
export const CITY_LAYERS: LayerSpecification[] = [
{
id: CITY_PREFIX + "buildings",
id: CITY_BUILDING_LAYER_ID,
source: CITY_PREFIX + "buildings",
"source-layer": "buildings",
type: "fill-extrusion",
@@ -26,6 +26,7 @@ export async function exportMapViewImage(map: MapLibreMap, { scale, targetLongEd
const exportHeight = Math.round(sourceHeight * exportScale);
const container = createExportContainer(exportWidth, exportHeight);
const style = cloneMapStyle(map.getStyle());
style.light = map.getLight();
const bounds = map.getBounds();
document.body.appendChild(container);
@@ -37,6 +38,7 @@ export async function exportMapViewImage(map: MapLibreMap, { scale, targetLongEd
zoom: map.getZoom(),
pitch: map.getPitch(),
bearing: map.getBearing(),
antialias: true,
preserveDrawingBuffer: true,
interactive: false,
attributionControl: false,