Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
96ee175
feat: add geoportal shadow simulation
mxfh Aug 25, 2026
d501f89
feat: add shadow terrain runtime
mxfh Aug 25, 2026
9cbc0be
feat: refine shadow simulation rendering
mxfh Aug 25, 2026
a98fc9e
feat(geoportal): harden shadow simulation
mxfh Aug 25, 2026
86b2168
feat(geoportal): complete shadow simulation integration
mxfh Aug 26, 2026
f813734
fix(geoportal): stabilize shadow terrain and layer drops
mxfh Aug 26, 2026
0398374
fix(geoportal): reduce shadow scene rebuilds
mxfh Aug 26, 2026
fd50f35
fix(geoportal): integrate LoD2 into shared shadow scene
mxfh Aug 26, 2026
312c606
feat(geoportal): refine shadow simulation controls and rendering
mxfh Aug 26, 2026
8d9a1aa
feat(geoportal): add shadow projection diagnostics
mxfh Aug 26, 2026
dc99040
feat(geoportal): tile shadow buffers and light them atmospherically
mxfh Aug 26, 2026
2542b87
perf(geoportal): keep the shadow simulation off the per-frame hot path
mxfh Aug 26, 2026
0139212
fix(geoportal): sweep terrain shadow coverage from the view, not the fit
mxfh Aug 26, 2026
bcd09dc
fix(geoportal): keep the shadow terrain sweep static and hole-free
mxfh Aug 26, 2026
535a644
fix(geoportal): make the single shadow buffer's depth test hold up
mxfh Aug 27, 2026
b3f2b8e
feat(geoportal): keep a resident base blanket of the terrain
mxfh Aug 27, 2026
1132b18
feat(geoportal): sample the sun as a disc for real penumbras
mxfh Aug 27, 2026
d8ef2bd
fix(geoportal): the view refines and downloads before the sun coverage
mxfh Aug 27, 2026
17a1509
fix(geoportal): judge terrain error against the surface, not sea level
mxfh Aug 27, 2026
a5726c5
feat(geoportal): quality ladder in the main panel, leaner time ribbon
mxfh Aug 27, 2026
1637327
fix(geoportal): usable quality ladder, panel padding
mxfh Aug 27, 2026
d852829
fix(geoportal): sink terrain backfill below the coarse-error band
mxfh Aug 27, 2026
2ad7810
feat(geoportal): three quality levels, soft samples at full size
mxfh Aug 27, 2026
29e9e85
fix(geoportal): contact shadows stay sharp under the soft sun
mxfh Aug 27, 2026
2ddfa34
feat(geoportal): 32 sun samples through static accumulation
mxfh Aug 27, 2026
68e25ba
feat(geoportal): compact static time ribbon with a click-to-set time
mxfh Aug 27, 2026
cc184ed
refactor(geoportal): native time input instead of the antd picker
mxfh Aug 27, 2026
76f998f
feat(geoportal): quality levels bite in soft mode, Max by default
mxfh Aug 27, 2026
d4a562c
style(geoportal): one type size and colour across the options rows
mxfh Aug 27, 2026
d077d60
fix(geoportal): a superseded tile batch never blanks visible ground
mxfh Aug 27, 2026
2a91ff4
fix(addons): sit the ribbon time on the baseline and pause accumulati…
mxfh Aug 27, 2026
98158ef
feat(addons): stealth the Kurvenansicht and Debug controls until hovered
mxfh Aug 27, 2026
11f59af
feat(geoportal): move the shadow controls into the layer info view
mxfh Aug 27, 2026
8c0a288
fix(geoportal): survive the date picker and revive the soft-shadow av…
mxfh Aug 27, 2026
a41aaef
refactor(mapping): extract the shadow simulation addon
mxfh Aug 28, 2026
523d50f
fix(mapping): prioritize terrain and stabilize shadow coverage
mxfh Aug 28, 2026
5b5adbe
fix(mapping): make the shadow-simulation addon a source-only library
mxfh Aug 28, 2026
ec12d72
fix(playgrounds): follow moved pointcloud helpers in pointcloud-stories
mxfh Aug 28, 2026
4e384c7
fix(mapping): stabilize shadow tile coverage and sampling
mxfh Aug 28, 2026
d9da075
fix: streamline shadow tile loading and URL sync
mxfh Aug 28, 2026
2ec1068
fix(mapping): stabilize shadow tiles and diagnostics
mxfh Aug 28, 2026
cd5bf8b
fix: start sun disc shadows after tile loading
mxfh Aug 28, 2026
4c958e8
fix(mapping): settle shadow frames and bound tile loading
mxfh Aug 31, 2026
672d17c
fix(mapping): stabilize textured mesh shadows
mxfh Aug 31, 2026
bdac624
fix shadow tile traversal and hmr teardown
mxfh Sep 1, 2026
77c33e9
fix shadow camera tile coverage
mxfh Sep 1, 2026
ee59e5f
fix tile loading and sun shadow accumulation
mxfh Sep 1, 2026
9d8633d
fix LoD2 city surface orientation
mxfh Sep 1, 2026
78023b7
fix mesh tile refinement after camera moves
mxfh Sep 1, 2026
a57bd3e
feat add atmospheric sky to shadow simulation
mxfh Sep 1, 2026
1dc326f
fix atmospheric lighting across render paths
mxfh Sep 1, 2026
fa697bc
fix shadow debug hmr teardown
mxfh Sep 1, 2026
a224861
fix mesh refinement after view changes
mxfh Sep 1, 2026
c7760bd
wip 3d tile loaders improvements
mxfh Sep 2, 2026
74d55d3
fix(mapping): stop refetching external tilesets every frame
mxfh Sep 2, 2026
65139ea
fix(mapping): make 3d tiles loading progressive
mxfh Sep 2, 2026
d453475
fix(mapping): prioritize visible 3d tiles
mxfh Sep 2, 2026
4902135
fix(mapping): bound shadow tile traversal
mxfh Sep 2, 2026
fafd335
fix(mapping): derive shadow tiles from visible receivers
mxfh Sep 2, 2026
d2b6307
fix shadow caster traversal and accumulation
mxfh Sep 2, 2026
29ff889
feat(mapping): improve shadow traversal diagnostics
mxfh Sep 2, 2026
d28848b
fix(mapping): align shadow debug camera visualization
mxfh Sep 2, 2026
6f81edc
fix(mapping): stabilize atmospheric backdrop
mxfh Sep 2, 2026
865f8a5
feat(shadow): stabilize atmosphere and add live sun view
mxfh Sep 2, 2026
866b654
fix(shadow): stabilize terrain and pan rendering
mxfh Sep 2, 2026
6219239
wip/poc stacking
mxfh Sep 3, 2026
d1d703b
feat(maplibre): drape map style onto shadow terrain
mxfh Sep 3, 2026
f7f1c89
feat: drape map style onto shadow terrain
mxfh Sep 3, 2026
368a219
feat: refine shadow terrain map stacking
mxfh Sep 3, 2026
2c0ddcb
fix(shadow): stabilize terrain stacking and tile traversal
mxfh Sep 3, 2026
6814e8a
perf(shadow): rate-limit label overlay maintenance and keep line labe…
mxfh Sep 3, 2026
b466919
fix(shadow): cast building shadows from both faces of closed solids
mxfh Sep 3, 2026
41ad4dd
feat(shadow): mesh drape with draped labels, sun-styled overlay and t…
mxfh Sep 3, 2026
8fc6d60
feat(mesh): drape the mesh label style without the shadow simulation
mxfh Sep 4, 2026
46ab2bb
fix(maplibre): suppress terrain skirts below Three terrain
mxfh Sep 4, 2026
cc101a3
fix(shadow): stabilize terrain startup and defer soft shadows
mxfh Sep 4, 2026
751cbb0
refactor shadow simulation state and rendering
mxfh Sep 4, 2026
99befce
consolidate solar calculations on astronomy engine
mxfh Sep 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"version": 8,
"name": "Wuppertal Mesh 2024",
"sources": {},
"layers": [],
"metadata": {
"carmaConf": {
"instant": true,
"layerInfo": {
"title": "Wuppertal Mesh 2024",
"description": "3D-Tiles-Mesh mit Clay-Material für die gemeinsame Three.js-Szene",
"keywords": []
},
"threeTiles": {
"url": "https://wupp-3d-data.cismet.de/mesh2024/tileset.json",
"origin": [7.15, 51.256],
"shader": {
"kind": "clay",
"color": "#d8d1c4",
"roughness": 0.92,
"metalness": 0
},
"errorTarget": 8,
"requestConcurrency": 2
}
}
}
}
7 changes: 7 additions & 0 deletions apps/geoportal/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import { useManageLayers } from "./hooks/useManageLayers";
import { useSyncToken } from "./hooks/useSyncToken";
import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts";
import { useMeasurementLayerButton } from "./hooks/useMeasurementLayerButton";
import { useShadowSimulationLayerButton } from "./hooks/useShadowSimulationLayerButton";
import { useGeoportalAppSearchParams } from "./hooks/use-geoportal-app-search-params";
import { useAdhocFeatureRehydrate } from "./hooks/use-adhoc-feature-rehydrate";

Expand Down Expand Up @@ -174,6 +175,11 @@ function MeasurementLayerSyncInner() {
return null;
}

function ShadowSimulationLayerSyncInner() {
useShadowSimulationLayerButton();
return null;
}

function GeoportalAppSearchParamsIntegration() {
useGeoportalAppSearchParams();
return null;
Expand Down Expand Up @@ -276,6 +282,7 @@ function App({
>
<AnnotationProvider>
<MeasurementLayerSyncInner />
<ShadowSimulationLayerSyncInner />
<ErrorBoundary FallbackComponent={AppErrorFallback}>
<AdhocFeatureRehydration />
<div className={TAILWIND_CLASSNAMES_FULLSCREEN_FIXED}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { describe, expect, it } from "vitest";

import type { BackgroundLayer } from "@carma-mapping/layers";

import { geoportalBackgroundToLibreLayers } from "./geoportalBackgroundToLibreLayers";

const background = {
id: "karte",
title: "Stadtplan",
visible: true,
opacity: 0.8,
layers: "rvrGrundriss@100|amtlich@90|rvrSchriftNT@100",
} as BackgroundLayer;

describe("Geoportal shaded terrain background composition", () => {
it("replaces raster-labelled bases with a separable vector basemap", () => {
const layers = geoportalBackgroundToLibreLayers(
background,
{
amtlich: {
type: "tiles",
url: "https://example.test/city-map/{z}/{x}/{y}.png",
},
rvrGrundriss: {
type: "wmts",
url: "https://example.test/opaque-ground-plan",
layers: "ground-plan",
},
rvrSchriftNT: {
type: "wmts-nt",
url: "https://example.test/labels",
layers: "labels",
transparent: true,
},
basemap_relief: {
type: "vector",
style: "https://example.test/vector-basemap.json",
},
},
{ shadowTerrainActive: true }
);

expect(layers).toHaveLength(1);
expect(
layers.some(
(layer) => "layers" in layer && layer.layers === "ground-plan"
)
).toBe(false);
expect(layers).toEqual([
expect.objectContaining({
type: "vector",
name: "bg-basemap_relief",
style: "https://example.test/vector-basemap.json",
opacity: 0.8,
}),
]);
});

it("keeps the authored background unchanged without shaded terrain", () => {
const layers = geoportalBackgroundToLibreLayers(background, {
amtlich: {
type: "tiles",
url: "https://example.test/city-map/{z}/{x}/{y}.png",
},
rvrGrundriss: {
type: "wmts",
url: "https://example.test/opaque-ground-plan",
layers: "ground-plan",
},
rvrSchriftNT: {
type: "wmts-nt",
url: "https://example.test/labels",
layers: "labels",
transparent: true,
},
basemap_relief: {
type: "vector",
style: "https://example.test/vector-basemap.json",
},
});

expect(layers).toHaveLength(3);
expect(
layers.some(
(layer) => "layers" in layer && layer.layers === "ground-plan"
)
).toBe(true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,23 @@ type NamedLayerConfig = {
maxNativeZoom?: number;
};

type GeoportalBackgroundLibreOptions = {
terrainMeshActive?: boolean;
shadowTerrainActive?: boolean;
};

// Raster bases bake place names into their ground pixels. Shaded terrain uses
// the existing vector basemap instead so its ground can be projected while
// point-based place names remain a separate symbol pass above Three.
const TERRAIN_MESH_OVERLAY_LAYERS = "basemap_relief@100";
const TERRAIN_MESH_REPLACED_LAYER_NAMES = new Set([
"amtlich",
"amtlichBasiskarte",
"rvrGrundriss",
"rvrSchriftNT",
"basemap_relief",
]);

const isTransparent = (value: unknown): boolean => {
if (typeof value === "boolean") return value;
if (typeof value === "string") return value.toLowerCase() === "true";
Expand All @@ -21,7 +38,8 @@ const isTransparent = (value: unknown): boolean => {

export const geoportalBackgroundToLibreLayers = (
backgroundLayer: BackgroundLayer | null | undefined,
extraNamedLayers?: Record<string, NamedLayerConfig>
extraNamedLayers?: Record<string, NamedLayerConfig>,
options: GeoportalBackgroundLibreOptions = {}
): LibreLayer[] => {
if (!backgroundLayer || !backgroundLayer.visible) {
return [];
Expand All @@ -34,11 +52,24 @@ export const geoportalBackgroundToLibreLayers = (
...extraNamedLayers,
};
const layerOpacity = backgroundLayer.opacity ?? 1;
const separateLocationLabels =
options.terrainMeshActive === true || options.shadowTerrainActive === true;
// All named layers of a background spec belong to the single background
// button, so they share one id and their loading states aggregate.
const carmaLayerId = backgroundLayer.id;

for (const spec of backgroundLayer.layers.split("|")) {
const originalLayerSpecs = backgroundLayer.layers
.split("|")
.filter(
(spec) =>
!separateLocationLabels ||
!TERRAIN_MESH_REPLACED_LAYER_NAMES.has(spec.split("@")[0])
);
const layerSpecs = separateLocationLabels
? [...originalLayerSpecs, ...TERRAIN_MESH_OVERLAY_LAYERS.split("|")]
: originalLayerSpecs;

for (const spec of layerSpecs) {
const [name, opacityStr] = spec.split("@");
const cfg = namedLayers[name];
if (!cfg) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { describe, expect, it } from "vitest";

import type { Layer } from "@carma-mapping/layers";

import {
geoportalLayersToLibreLayers,
layerProvidesTerrainMesh,
parseThreeTilesLayer,
} from "./geoportalLayersToLibreLayers";

const buildLayer = (threeTiles: unknown): Layer =>
({
id: "custom:mesh-2024",
title: "Wuppertal Mesh 2024",
visible: true,
opacity: 0.75,
layerType: "vector",
conf: { threeTiles },
props: { style: { version: 8, sources: {}, layers: [] } },
} as unknown as Layer);

describe("Geoportal 3D Tiles layer conversion", () => {
it("maps a declared clay shader into the shared Three.js layer contract", () => {
const layer = buildLayer({
url: "https://example.test/tileset.json",
origin: [7.15, 51.256],
shader: {
kind: "clay",
color: "#d8d1c4",
roughness: 0.92,
metalness: 0,
},
errorTarget: 8,
requestConcurrency: 2,
});

expect(geoportalLayersToLibreLayers([layer])).toEqual([
{
type: "three-tiles",
name: "Wuppertal Mesh 2024",
carmaLayerId: "custom:mesh-2024",
url: "https://example.test/tileset.json",
origin: [7.15, 51.256],
shader: {
kind: "clay",
color: "#d8d1c4",
roughness: 0.92,
metalness: 0,
},
errorTarget: 8,
requestConcurrency: 2,
opacity: 0.75,
},
]);
});

it("rejects unknown shader contracts", () => {
expect(
parseThreeTilesLayer(
buildLayer({
url: "https://example.test/tileset.json",
shader: { kind: "custom-glsl", color: "#fff" },
})
)
).toBeNull();
});

it("recognizes the remote photogrammetry mesh before its style is fetched", () => {
const layer = {
id: "mesh-2024",
title: "3D-MeshX 2024",
visible: true,
layerType: "vector",
props: {
style: "https://tiles.cismet.de/lod2/mesh2024.style.json",
},
} as Layer;

expect(layerProvidesTerrainMesh(layer)).toBe(true);
expect(layerProvidesTerrainMesh({ ...layer, visible: false })).toBe(false);
});

it("recognizes inline mesh metadata and direct terrain mesh declarations", () => {
const taggedLayer = {
id: "inline-mesh",
visible: true,
props: {
style: {
version: 8,
metadata: {
carmaConf: { layerInfo: { tags: ["Basis", "Mesh"] } },
},
sources: {},
layers: [],
},
},
} as unknown as Layer;
const directLayer = {
id: "direct-mesh",
visible: true,
conf: { threeTiles: { providesTerrain: true } },
} as unknown as Layer;

expect(layerProvidesTerrainMesh(taggedLayer)).toBe(true);
expect(layerProvidesTerrainMesh(directLayer)).toBe(true);
});
});
Loading
Loading