Swisstopo + OSM Importer + Höhenlinien + Bulk-Op Performance

Swisstopo Iter 3:
- Ortho-Drape: TIN-Mesh aus Terrain-Grid mit per-vertex UVs + PictureFrame-Material
- Project-Cache: TIFs werden neben .3dm gespeichert (SMB-shareable)
- Layer-Restruktur: 80_swisstopo/{Terrain, Luftbild} Sub-Ebenen
- TIFs direkt (kein PNG-Downsampling) für volle Auflösung
- UV-Inset gegen weisse Streifen zwischen Kacheln
- Hoehenlinien (2D, swissALTI3D) auf aktives Geschoss OKFF projiziert
- TIN-Mesh + Schichtenmodell aus Contours (separate Optionen)
- TLM3D entfernt (swisstopo liefert nur GDB/SHP, kein DXF)

OSM Importer (neu):
- rhino/osm.py: Overpass-API-Client
- src/OsmApp.jsx: React-Dialog mit Adresse + Radius + 7 Kategorien
- Strassen/Gebäude/Wasser/Wasserläufe/Parks/Wald/Fusswege (Codes 7101-7107)
- ElementeApp: PillGroup "Importer" mit Swisstopo + OSM Buttons

Sub-Ebenen — rekursiv durch hierarchische Ebenen:
- Visibility-Toggle: slimEbene rekursiv (children bleiben erhalten)
- Settings-Dialog: _find_sublayer_by_code_recursive + _replace_in_tree
- Hatch Auto-Fill: refresh_layer_fills + _fill_signature + _ebene_fill_for_layer
  alle rekursiv durch children
- EbenenSettingsApp: flattenEbenen-Helper

Bulk-Op Performance (Delete/Cut/etc.):
- _USER_BULK_CMDS + _BULK_ACTIVE_KEY Sticky-Flag
- CommandBegin: doc.Views.RedrawEnabled = False + Listener-Bail aktiv
- CommandEnd: RedrawEnabled restore + 1× Redraw + Selection-Refresh
- Bail-outs in dimensionen.on_idle/on_select, elemente._on_idle_selection,
  gestaltung.on_idle_flush/on_delete
- Verhindert das sichtbare "Runterzählen" pro Element bei Bulk-Delete

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 02:42:45 +02:00
parent 1e6bc68156
commit b425421fdd
13 changed files with 1667 additions and 174 deletions
+38 -7
View File
@@ -29,14 +29,45 @@ export default function EbenenSettingsApp() {
return () => document.removeEventListener('contextmenu', blockContext)
}, [])
const sortedEbenen = [...ebenen].sort((a, b) => {
const ca = parseInt(a.code, 10), cb = parseInt(b.code, 10)
if (!isNaN(ca) && !isNaN(cb)) return ca - cb
return (a.code || '').localeCompare(b.code || '')
})
// Flach durch Tree iterieren — Sub-Ebenen sind verschachtelt in children
const flattenEbenen = (list, depth = 0) => {
const out = []
for (const e of list) {
if (!e || typeof e !== 'object') continue
out.push({ ...e, _depth: depth })
if (Array.isArray(e.children) && e.children.length) {
out.push(...flattenEbenen(e.children, depth + 1))
}
}
return out
}
const flatEbenen = flattenEbenen(ebenen)
const currentEbene = ebenen.find(e => e.code === selectedCode)
|| ebenen.find(e => e.code === originalCode)
// Sort: nur Top-Level (depth=0) numerisch sortieren — Children stehen
// direkt hinter ihrem Parent. Beim Picker zeigt das die Hierarchie.
const sortedEbenen = (() => {
const tops = flatEbenen.filter(e => e._depth === 0)
tops.sort((a, b) => {
const ca = parseInt(a.code, 10), cb = parseInt(b.code, 10)
if (!isNaN(ca) && !isNaN(cb)) return ca - cb
return (a.code || '').localeCompare(b.code || '')
})
const out = []
for (const top of tops) {
out.push(top)
// Children direkt anhaengen, ebenfalls per Code sortiert
const kids = flatEbenen.filter(e =>
e._depth === 1 &&
flatEbenen.find(p => p._depth === 0 && Array.isArray(p.children)
&& p.children.some(c => c.code === e.code))?.code === top.code)
kids.sort((a, b) => (a.code || '').localeCompare(b.code || ''))
out.push(...kids)
}
return out
})()
const currentEbene = flatEbenen.find(e => e.code === selectedCode)
|| flatEbenen.find(e => e.code === originalCode)
|| initial.ebene
|| null
+7 -4
View File
@@ -6,7 +6,7 @@ import {
createFenster, createTuer, createAussparung, createTreppe,
createStuetze, createTraeger, createRaum,
exportRaeume,
openSwisstopo, openSwisstopoDialog,
openSwisstopo, openSwisstopoDialog, openOsmDialog,
updateElement, deleteElement, regenerateAllElements,
} from './lib/rhinoBridge'
@@ -478,10 +478,13 @@ function NeuesElementSection({ noGeschoss, activeName }) {
onClick={() => createRaum({})} />
</PillGroup>
<PillGroup label="Swisstopo">
<PillButton icon="download" label="Importer…"
hint="Vollautomatischer Import via swisstopo STAC-API: Adresse suchen, Radius wählen, Gebäude + Terrain + Luftbild holen — Tiles werden gecacht"
<PillGroup label="Importer">
<PillButton icon="download" label="Swisstopo"
hint="Vollautomatischer Import via swisstopo STAC-API: Adresse suchen, Radius wählen, Gebäude + Terrain + Luftbild holen"
onClick={() => openSwisstopoDialog()} />
<PillButton icon="public" label="OSM"
hint="OpenStreetMap-Daten via Overpass-API als 2D-Linien: Strassen, Gebäudeumrisse, Wasser, Grünflächen, Wege"
onClick={() => openOsmDialog()} />
<PillButton icon="map" label="Karte"
hint="Öffnet map.geo.admin.ch im Browser zur visuellen Inspektion"
onClick={() => openSwisstopo('both')} />
+309
View File
@@ -0,0 +1,309 @@
import { useState, useEffect, useRef } from 'react'
import Icon from './components/Icon'
import { onMessage, notifyReady } from './lib/rhinoBridge'
function send(type, payload = {}) {
if (!window.RHINO_MODE) { console.log('[OSM] →', type, payload); return }
document.title = 'RHINOMSG::' + JSON.stringify({ type, payload })
}
function Field({ label, hint, children }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, padding: '6px 0' }}>
{label && <span className="label-xs">{label}</span>}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>{children}</div>
{hint && (
<span style={{ fontSize: 9, color: 'var(--text-muted)', lineHeight: 1.4 }}>{hint}</span>
)}
</div>
)
}
function SectionLabel({ children }) {
return (
<div style={{
fontSize: 9, color: 'var(--text-muted)', fontWeight: 600,
letterSpacing: 0.5, textTransform: 'uppercase',
padding: '10px 0 4px',
borderTop: '1px solid var(--border-light)',
marginTop: 8,
}}>{children}</div>
)
}
function Radio({ value, options, onChange }) {
return (
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
{options.map(o => (
<button key={o.value}
onClick={() => onChange(o.value)}
className={value === o.value ? 'btn-contained' : 'btn-outlined'}
style={{ padding: '4px 10px', fontSize: 10 }}
>
{o.label}
</button>
))}
</div>
)
}
// OSM-Kategorien — Keys matchen das Backend (osm.py CATEGORIES).
const CATEGORIES = [
{ key: 'streets', label: 'Strassen', icon: 'route',
hint: 'Autobahn/Hauptstrasse/Quartierstrasse → Polylinien' },
{ key: 'buildings', label: 'Gebäudeumrisse', icon: 'apartment',
hint: 'building=* Umrisse als geschlossene Polylinien' },
{ key: 'water', label: 'Wasser (Flächen)', icon: 'water',
hint: 'natural=water (Seen, Teiche)' },
{ key: 'waterways', label: 'Wasserläufe', icon: 'waves',
hint: 'waterway=river/stream/canal' },
{ key: 'parks', label: 'Parks', icon: 'park',
hint: 'leisure=park/garden' },
{ key: 'forest', label: 'Wald & Grün', icon: 'forest',
hint: 'landuse=forest/grass/meadow' },
{ key: 'footpaths', label: 'Fuss-/Radwege', icon: 'directions_walk',
hint: 'highway=footway/path/track/cycleway' },
]
export default function OsmApp() {
// Standort
const [searchText, setSearchText] = useState('')
const [center, setCenter] = useState(null)
const [searching, setSearching] = useState(false)
// Optionen
const [radius, setRadius] = useState(200)
const [selected, setSelected] = useState({
streets: true, buildings: true, waterways: true,
parks: true, forest: true,
water: false, footpaths: false,
})
const [shift, setShift] = useState(true)
const [autoZoom, setAutoZoom] = useState(true)
const [replaceExisting, setReplaceExisting] = useState(true)
// Live-Log
const [logs, setLogs] = useState([])
const [running, setRunning] = useState(false)
const [done, setDone] = useState(false)
const logRef = useRef(null)
useEffect(() => {
onMessage('GEOCODE_RESULT', ({ result }) => {
setSearching(false)
if (result && result.e != null && result.n != null) {
setCenter({ e: result.e, n: result.n, label: result.label || searchText })
} else {
setCenter(null)
addLog('Keine Adresse gefunden')
}
})
onMessage('OSM_LOG', ({ msg }) => addLog(msg))
onMessage('IMPORT_DONE', ({ count }) => {
setRunning(false); setDone(true)
addLog(`✓ Fertig — ${count} OSM-Objekt(e) importiert`)
})
notifyReady()
const blockContext = (ev) => ev.preventDefault()
document.addEventListener('contextmenu', blockContext)
return () => document.removeEventListener('contextmenu', blockContext)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight
}, [logs])
const addLog = (m) => setLogs(l => [...l, m])
const handleSearch = () => {
const t = searchText.trim()
if (!t) return
setSearching(true); setCenter(null)
addLog(`Suche '${t}'...`)
send('GEOCODE', { text: t })
}
const handleManualCoords = (eRaw, nRaw) => {
const e = parseFloat(eRaw), n = parseFloat(nRaw)
if (e > 2000000 && n > 1000000) {
setCenter({ e, n, label: `LV95 manuell` })
} else {
setCenter(null)
}
}
const handleImport = () => {
if (!center) { addLog('Bitte zuerst einen Standort wählen'); return }
const cats = Object.entries(selected).filter(([, v]) => v).map(([k]) => k)
if (cats.length === 0) { addLog('Mindestens eine Kategorie auswählen'); return }
setLogs([]); setRunning(true); setDone(false)
send('RUN_OSM_IMPORT', {
centerE: center.e, centerN: center.n,
radius: Number(radius),
categories: cats,
shiftToOrigin: shift,
autoZoom,
replaceExisting,
})
}
const toggleCat = (key) => {
setSelected(s => ({ ...s, [key]: !s[key] }))
}
return (
<div style={{
position: 'absolute', inset: 0,
background: 'var(--bg-dialog)',
display: 'flex', flexDirection: 'column',
fontFamily: 'var(--font)', color: 'var(--text-primary)',
overflow: 'hidden',
}}>
<div style={{ flex: 1, overflowY: 'auto', padding: '10px 16px' }}>
<SectionLabel>Standort</SectionLabel>
<Field label="ADRESSE / ORT" hint='z.B. "Bahnhofstrasse 1, Zürich"'>
<input
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') handleSearch() }}
placeholder="Adresse oder Ortsname"
style={{ flex: 1, fontSize: 11, padding: '5px 8px' }}
/>
<button
className="btn-outlined"
onClick={handleSearch}
disabled={searching || !searchText.trim()}
style={{ padding: '4px 10px', fontSize: 11 }}
>
{searching ? '…' : 'Suchen'}
</button>
</Field>
<Field label="ODER LV95-KOORDS (E / N)"
hint="Falls aus Swisstopo-Import übernommen">
<input placeholder="E"
onChange={(e) => handleManualCoords(e.target.value, center?.n || '')}
style={{ width: 110, fontSize: 11, fontFamily: 'DM Mono, monospace', padding: '5px 8px' }} />
<span style={{ color: 'var(--text-muted)' }}>/</span>
<input placeholder="N"
onChange={(e) => handleManualCoords(center?.e || '', e.target.value)}
style={{ width: 110, fontSize: 11, fontFamily: 'DM Mono, monospace', padding: '5px 8px' }} />
</Field>
{center && (
<div style={{
display: 'flex', alignItems: 'center', gap: 8,
padding: '8px 10px',
background: 'var(--accent-dim)',
border: '1px solid var(--accent-border)',
borderRadius: 'var(--r)',
marginTop: 4,
}}>
<Icon name="location_on" size={14} style={{ color: 'var(--accent)' }} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 11, fontWeight: 500, overflow: 'hidden',
textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{center.label}</div>
<div style={{ fontSize: 9, fontFamily: 'DM Mono, monospace',
color: 'var(--text-muted)' }}>
E {Math.round(center.e)} · N {Math.round(center.n)}
</div>
</div>
</div>
)}
<SectionLabel>Bereich</SectionLabel>
<Field label="RADIUS">
<Radio value={radius}
options={[
{ value: 100, label: '100 m' },
{ value: 200, label: '200 m' },
{ value: 500, label: '500 m' },
{ value: 1000, label: '1 km' },
]}
onChange={setRadius} />
</Field>
<SectionLabel>Kategorien</SectionLabel>
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{CATEGORIES.map(cat => (
<label key={cat.key}
style={{ display: 'flex', alignItems: 'center', gap: 8,
fontSize: 11, cursor: 'pointer', padding: '3px 0' }}
title={cat.hint}>
<input type="checkbox" checked={!!selected[cat.key]}
onChange={() => toggleCat(cat.key)} />
<Icon name={cat.icon} size={13} />
<span>{cat.label}</span>
</label>
))}
</div>
<SectionLabel>Positionierung</SectionLabel>
<Field label="ORIGIN"
hint="LV95-Koords sind im Mio-Bereich. Auf 0/0/0 verschiebt zum aktiven Standort.">
<Radio value={shift ? 'origin' : 'lv95'}
options={[
{ value: 'origin', label: 'Auf Welt-Origin verschieben' },
{ value: 'lv95', label: 'Original LV95 lassen' },
]}
onChange={(v) => setShift(v === 'origin')} />
</Field>
<Field label="">
<label style={{ display: 'flex', alignItems: 'center', gap: 6,
fontSize: 11, cursor: 'pointer' }}>
<input type="checkbox" checked={autoZoom}
onChange={(e) => setAutoZoom(e.target.checked)} />
Auto-Zoom auf importierte Objekte
</label>
</Field>
<Field label=""
hint="Bestehende OSM-Objekte (Tag dossier_osm_kind) werden vorher gelöscht.">
<label style={{ display: 'flex', alignItems: 'center', gap: 6,
fontSize: 11, cursor: 'pointer' }}>
<input type="checkbox" checked={replaceExisting}
onChange={(e) => setReplaceExisting(e.target.checked)} />
Bestehende OSM-Objekte vorher löschen
</label>
</Field>
<SectionLabel>Status</SectionLabel>
<div ref={logRef} style={{
height: 140, overflowY: 'auto',
padding: 8, fontSize: 10,
fontFamily: 'DM Mono, monospace',
background: 'var(--bg-base)',
border: '1px solid var(--border-light)',
borderRadius: 'var(--r)',
color: 'var(--text-secondary)',
whiteSpace: 'pre-wrap',
}}>
{logs.length === 0
? <span style={{ color: 'var(--text-muted)' }}>Bereit</span>
: logs.map((l, i) => <div key={i}>{l}</div>)}
{running && <div style={{ color: 'var(--accent)' }}>Läuft</div>}
</div>
</div>
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '8px 12px',
borderTop: '1px solid var(--border)',
background: 'var(--bg-section)',
}}>
<div style={{ fontSize: 9, color: 'var(--text-muted)', flex: 1 }}>
Quelle: Overpass-API · © OpenStreetMap-Mitwirkende (ODbL)
</div>
<button className="btn-text" onClick={() => send('CANCEL')}>Abbrechen</button>
<button className="btn-contained"
onClick={handleImport}
disabled={running || !center}>
<Icon name="download" size={12} />
{running ? 'Lädt…' : 'Importieren'}
</button>
</div>
</div>
)
}
+77 -5
View File
@@ -57,9 +57,19 @@ export default function SwisstopoApp() {
// Optionen
const [radius, setRadius] = useState(100)
const [getBuild, setGetBuild] = useState(true)
const [buildVersion, setBuildVersion] = useState('v2') // v2 (stabil) / v3 (beta)
const [buildVariant, setBuildVariant] = useState('separated')
const [getTerrain, setGetTerrain] = useState(false)
const [getOrtho, setGetOrtho] = useState(false)
const [getContours, setGetContours] = useState(false)
const [getContourTin,setGetContourTin]= useState(false)
const [getContourSchicht, setGetContourSchicht] = useState(false)
const [contourInt, setContourInt] = useState('2.0')
// TLM3D deaktiviert: swisstopo liefert nur GDB/SHP/GPKG — kein DXF.
// Rhino kann das nicht nativ importieren; OSM-Importer ist die Alternative
// fuer Vektordaten (Strassen/Wasser/Gebaeude).
const getTlm = false
const tlmKinds = {}
const [shift, setShift] = useState(true)
const [autoZoom, setAutoZoom] = useState(true)
const [replaceExisting, setReplaceExisting] = useState(true)
@@ -124,14 +134,21 @@ export default function SwisstopoApp() {
const handleImport = () => {
if (!center) { addLog('Bitte zuerst einen Standort wählen'); return }
if (!getBuild && !getTerrain) { addLog('Mindestens Gebäude oder Terrain auswählen'); return }
if (!getBuild && !getTerrain && !getContours && !getContourTin && !getContourSchicht && !getTlm) {
addLog('Mindestens eine Datenquelle wählen'); return
}
setLogs([])
setRunning(true)
setDone(false)
const kinds = []
if (getBuild) kinds.push('buildings')
if (getTerrain) kinds.push('terrain')
if (getBuild) kinds.push('buildings')
if (getTerrain) kinds.push('terrain')
if (getOrtho && getTerrain) kinds.push('ortho')
if (getContours) kinds.push('contours')
if (getContourTin) kinds.push('contour_tin')
if (getContourSchicht)kinds.push('contour_schicht')
if (getTlm) kinds.push('tlm')
const tlmList = Object.entries(tlmKinds).filter(([, v]) => v).map(([k]) => k)
send('RUN_IMPORT', {
centerE: center.e,
centerN: center.n,
@@ -142,7 +159,10 @@ export default function SwisstopoApp() {
replaceExisting,
clipToBbox,
terrainResolution: terrainRes,
buildVersion,
buildVariant,
contourInterval: contourInt,
tlmKinds: tlmList,
})
}
@@ -237,10 +257,23 @@ export default function SwisstopoApp() {
<label style={{ display: 'flex', alignItems: 'center', gap: 6,
fontSize: 11, cursor: 'pointer' }}>
<input type="checkbox" checked={getBuild} onChange={(e) => setGetBuild(e.target.checked)} />
<Icon name="location_city" size={13} /> Bestand-Gebäude (swissBUILDINGS3D 3.0, DWG)
<Icon name="location_city" size={13} /> Bestand-Gebäude (swissBUILDINGS3D, DWG)
</label>
</Field>
{getBuild && (
<Field label="VERSION"
hint="2.0 = stabil, kein Solid/Separated-Split (alle Kategorien auf eigenen DXF-Layern innerhalb einer DWG). 3.0 = neuer, Beta — kann manchmal Probleme mit Variant-Erkennung haben.">
<Radio
value={buildVersion}
options={[
{ value: 'v2', label: '2.0 (stabil)' },
{ value: 'v3', label: '3.0 (beta)' },
]}
onChange={setBuildVersion}
/>
</Field>
)}
{getBuild && buildVersion === 'v3' && (
<Field label="GEBÄUDE-VARIANTE"
hint="Solid: ein geschlossenes Solid pro Gebäude (klein, schnell). Separated: Dach/Fassade/Wand als separate Objekte (mehr Detail, ermoeglicht z.B. Dach auszublenden).">
<Radio
@@ -284,6 +317,45 @@ export default function SwisstopoApp() {
</label>
</Field>
<Field label=""
hint="2D-Höhenlinien aus dem swissALTI3D-DEM. Werden flach auf die OKFF-Ebene des aktiven Geschosses gelegt — direkt zeichnungstauglich. Unabhängig vom 3D-Mesh.">
<label style={{ display: 'flex', alignItems: 'center', gap: 6,
fontSize: 11, cursor: 'pointer' }}>
<input type="checkbox" checked={getContours}
onChange={(e) => setGetContours(e.target.checked)} />
<Icon name="terrain" size={13} /> Höhenlinien (2D, auf aktivem Geschoss)
</label>
</Field>
<Field label=""
hint="3D-TIN-Mesh aus den Vertices der Höhenlinien — Delaunay-trianguliert. Stilisierter Topo-Look mit weniger Polygonen als das DEM-Mesh.">
<label style={{ display: 'flex', alignItems: 'center', gap: 6,
fontSize: 11, cursor: 'pointer' }}>
<input type="checkbox" checked={getContourTin}
onChange={(e) => setGetContourTin(e.target.checked)} />
<Icon name="lan" size={13} /> TIN-Mesh aus Höhenlinien
</label>
</Field>
<Field label=""
hint="Schichtenmodell: jede geschlossene Höhenlinie wird zur planaren Fläche auf ihrer Z-Höhe — der architektonische Pappmodell-Look.">
<label style={{ display: 'flex', alignItems: 'center', gap: 6,
fontSize: 11, cursor: 'pointer' }}>
<input type="checkbox" checked={getContourSchicht}
onChange={(e) => setGetContourSchicht(e.target.checked)} />
<Icon name="stacks" size={13} /> Schichtenmodell aus Höhenlinien
</label>
</Field>
{(getContours || getContourTin || getContourSchicht) && (
<Field label="HÖHEN-ABSTAND">
<Radio value={contourInt}
options={[
{ value: '1.0', label: '1 m (fein)' },
{ value: '2.0', label: '2 m (Standard)' },
{ value: '5.0', label: '5 m (grob)' },
]}
onChange={setContourInt} />
</Field>
)}
<SectionLabel>Positionierung</SectionLabel>
<Field label="ORIGIN"
@@ -352,7 +424,7 @@ export default function SwisstopoApp() {
background: 'var(--bg-section)',
}}>
<div style={{ flex: 1, fontSize: 10, color: 'var(--text-muted)' }}>
{center ? `Tiles werden gecacht in ~/Library/Caches/Dossier/swisstopo/` : 'Wähle zuerst einen Standort'}
{center ? `Tiles werden im Projekt-Ordner neben der .3dm gecacht (Fallback: ~/Library/Caches/Dossier/swisstopo/ wenn ungespeichert)` : 'Wähle zuerst einen Standort'}
</div>
<button className="btn-text" onClick={() => send('CANCEL', {})}
disabled={running}>
+1
View File
@@ -261,6 +261,7 @@ export function exportRaeume() { send('EXPORT_RAEUME', {}) }
export function openSwisstopo(mode) { send('OPEN_SWISSTOPO', { mode: mode || 'both' }) }
export function importSwisstopo(kind) { send('IMPORT_SWISSTOPO', { kind: kind || 'buildings' }) }
export function openSwisstopoDialog() { send('OPEN_SWISSTOPO_DIALOG', {}) }
export function openOsmDialog() { send('OPEN_OSM_DIALOG', {}) }
export function updateElement(id, patch) { send('UPDATE_ELEMENT', { id, ...(patch || {}) }) }
export function deleteElement(id) { send('DELETE_ELEMENT', { id }) }
// Backwards-Compat-Aliases
+2
View File
@@ -10,6 +10,7 @@ import LayerCombinationsApp from './LayerCombinationsApp.jsx'
import AusschnittSettingsApp from './AusschnittSettingsApp.jsx'
import LayoutDialogApp from './LayoutDialogApp.jsx'
import SwisstopoApp from './SwisstopoApp.jsx'
import OsmApp from './OsmApp.jsx'
import GestaltungApp from './GestaltungApp.jsx'
import AusschnitteApp from './AusschnitteApp.jsx'
import MassstabApp from './MassstabApp.jsx'
@@ -38,6 +39,7 @@ const RootApp = mode === 'gestaltung' ? GestaltungApp
: mode === 'ausschnitt_settings' ? AusschnittSettingsApp
: mode === 'layout_dialog' ? LayoutDialogApp
: mode === 'swisstopo' ? SwisstopoApp
: mode === 'osm' ? OsmApp
: App
window.onerror = function (msg, src, line, col, err) {