3D/2D Viewer
Purpose
Interactive web-based viewer for IFC building models with multiple overlay systems. Uses Three.js for rendering and web-ifc for parsing IFC geometry in the browser.
File
templates/viewer.html — single-page application, self-contained.
Selection-aware shell (persistent Inspector + context ribbon)
The viewer is selection-aware: the same selection is visible across every view through two shared UI elements:
- Inspector panel (right rail, 320 px expanded / 32 px collapsed) — full
context for the selected element/surface/room. Contains two stacked sections:
Selection (top,
#inspectorSelection) and Jobs (bottom,#inspectorJobs— the JobPanel lives here, not as a free-floating bottom-left panel). - Context ribbon (bottom of the main column, 42 px) — one-line breadcrumb
of the current selection with its key numbers:
Raum 1 · Wand · 13,44 m² · U=0,99 · Φ=426 W. Click the ribbon to jump to the element's home view.
Both subscribe to the same in-memory selectionStore. Views dispatch selections
on click (3D canvas, sidebar element, Heizlast surface row, Roombook row). The
Inspector + ribbon are pure views over the existing global bundles
(heizlastData, sizingData, roombookData, presetsData, elementData) —
they never cache. A bundleBus fires whenever any bundle is reassigned, so
editing a U-value in the Heizlast table re-renders the Inspector without a
reselect. See CLAUDE.md §"Selection-aware viewer" for the data-flow diagram.


Layout management (CSS grid, no z-index war)
| Element | Grid area | Notes |
|---|---|---|
| Top-nav | header (row 1) |
unchanged five-tab nav |
| Main view area | main (row 2 left) |
shrinks when Inspector is expanded |
| Context ribbon | ribbon (row 3 left) |
42 px; hides nothing unless empty |
| Inspector panel | inspector (rows 2+3 right) |
320 px expanded / 32 px collapsed rail |
Minimum viewport width for the full three-column layout: 1280 px. Below that, Inspector auto-collapses to a 32 px rail on load. Mobile is explicitly out of scope — the collapsed rail still shows selection + job badges for awareness.

App shell: Detail / 3D / Roombook / Presets / Heizlast / Export / Versionen
Served by /project-viewer/<project>. Top nav swaps between views without
reloading; the IFC is downloaded once on page load and shared across views.
| View | Hash | Content |
|---|---|---|
| Detail (default) | #detail |
Project name, files + sizes, cache status dots, model-load indicator, shortcut buttons. Top of Detail shows the 3-Step Workflow Wizard (1 Overrides prüfen · 2 Review exportieren · 3 Revision hochladen), source-agnostic. |
| 3D | #3d |
The Three.js viewer (sections below) |
| Roombook | #roombook |
Typed table of IfcSpaces: name / long_name / purpose (text) · usage_type / shielding (enum) · design_temp / ach_min / volume (number) — IFC + override columns, renderer driven by backend field_specs |
| Presets | #presets |
Per-model Heizlast project parameters: Standort card (city dropdown + PLZ input → auto-fills θ_e from /climate/de), θ_e, n₅₀, ΔU_TB, shielding default, reheat, U-value defaults per surface type (wall external/internal/to-unheated, window, door external/internal, floor ground/internal, roof, ceiling to unheated). Save-on-blur via /heizlast_presets/<file> |
| Heizlast | #heizlast |
Top band with building total Φ_HL + presets summary; per-storey room cards (collapsible). Each card shows usage chip, θ_int, V, Φ_HL with Φ_T/Φ_V/Φ_RH breakdown. Expandable body has two tabs: Flächen (default — the auto-extracted bounding surfaces with inline editable area/U/adjacency/f_k cells; U-source chips pset / preset / default / override; click-row → 3D highlight) and Auslegung (Heizflächen sizing — see Heizflächenauslegung). The → IFC column is a single status dot per row (green = at least one field flagged for writeback); clicking jumps to the Export panel. |
| Export | #export |
Export Preview — tree of every value that will be stamped into the review IFC, grouped by IfcSpace / IfcWall / IfcWindow / IfcDoor / IfcSlab / IfcProject. Per-leaf + per-group + global toggles. See IFC write-back for field semantics. |
| Versionen | #versions |
Architect-package timeline. Each card = one architect IFC + its caches + a nested list of stamped review IFCs. See Version workflow. |














- The render loop pauses when not on 3D (a
viewerActiveflag gatescontrols.update()+renderer.render()inside the RAF callback), so non-3D views don't burn CPU. - Switching back to 3D re-runs
resize()+positionOverlayPanels()in arequestAnimationFrameso canvas size and overlay-panel cascade are correct after the wrapper becomes visible. - The Detail view uses
/project-info/<project>— a single round trip for all file sizes and cache flags (spaces,connection_graph,wall_zones,roombook,heizlast,heizlast_presets) — to avoid queue-up behind the large IFC download. - Data split across pages: project-level Heizlast knobs live in Presets;
per-room inputs (usage/Ti/ACH/volume) in Roombook; auto-extracted surfaces
and their per-surface overrides on Heizlast. See
CLAUDE.md§"Heizlast System" for the full formula and fingerprint/invalidation logic.
Views

3D Perspective View (default)
- Orbit controls: drag to rotate, scroll to zoom, right-drag to pan
- Click element in 3D → highlights blue, dims others, shows properties panel
- Click element in sidebar → flies camera to element, highlights it
- Click empty space → deselects all
2D Plan View

- Toggle: "2D Plan" / "3D View" button
- Orthographic camera looking straight down
- Section plane clips walls at ~40% height (shows door openings)
- White edge outlines on walls (
EdgesGeometry) —edgeGroupis a child ofallMeshGroup(not scene root) so outlines inherit the centering offset - Darkened slab color for contrast
- Pan and zoom (rotation disabled)
- Brighter ambient lighting for flat appearance
Overlay Systems
Element Sidebar
- Groups elements by IFC type (IfcDoor, IfcSlab, IfcSpace, IfcWall, etc.)
- Eye button (👁) per element: hide/show in 3D
- Search filter: live text filtering
- Properties panel: shows Type, Name, GlobalId, ExpressID on selection
Space Containers
- Green transparent boxes showing room volumes
- Hidden by default, visible when a space is clicked in the sidebar
- Geometry computed server-side (
/spaces/endpoint) from IFCIfcExtrudedAreaSolid - Positioned using auto-detected coordinate mapping (
perm/sgn/off)
Connection Graph
- "Calculate Graph" button: computes the graph server-side, saves JSON cache. Becomes "Recalculate Graph" after first run
- "Show Graph" button: appears after cache exists, loads and renders the overlay. Toggles to "Hide Graph"
- Green spheres: internal connection points
- Orange spheres: external junction points (where elements meet)
- Green/orange lines: internal/external edges
- Data from
/connection_graph/<filename>endpoint (?force=1to recompute)
MEP Cable Routes
- Toggle: "Show Routes" / "Hide Routes"
- Orange cylinder tubes: cable path segments
- Yellow emissive spheres: light fixtures
- White boxes: switches
- Data from
/analyzeendpoint withtool=mep_routing
Installation Zones
- "Calculate Zones" button: computes zones server-side via background job. Becomes "Recalculate Zones" after first run
- Checkboxes: "Wall Zones", "Ceiling Zones" (independent toggles)
- Dark red/yellow/green/blue overlays on wall and ceiling surfaces
- Wall zones: horizontal/vertical installation corridors per DIN 18015-3
- Ceiling zones: ZD-r perimeter bands (solid ceiling) or full fly zone (false ceiling)
- Options panel with checkboxes: "Abgehängte Decke", "Doppelter Boden"
- Batched rendering: all quads per color category merged into one geometry (~8 meshes total)
- Zone data cached in JS; storey filter rebuilds merged geometry from cache
- Data from
/wall_zones/<filename>?mode=...endpoint
Storey Filter

- Radio-button panel (top-left overlay, below model panel)
- Lists all
IfcBuildingStoreyelements with name and elevation - "All Storeys" (default) or select one storey to isolate
- Filters: meshes, edge lines, wall zones, and ceiling zones
- Panel hidden when IFC file has no storeys
- Data from
/storeys/<filename>endpoint
Background Job System

The viewer never blocks on a heavy compute. Every expensive GET (/spaces,
/connection_graph, /wall_zones, /roombook, /heizlast) either returns
the cached result (200) or enqueues a single-worker FIFO job (202 + job_id);
the client polls and re-fetches once done. See CLAUDE.md §"Job Runner System"
for the backend side and documentation/06_api_endpoints.md for the endpoint
contract.
- Fixed bottom-left panel on
#jobPanel(outside the view containers) so it stays visible across Detail / 3D / Roombook / Presets / Heizlast — when a job runs while you're on the Heizlast tab, you still see progress. - Each entry shows operation name, current phase with
phases_completed/total_phases(e.g., "Extract Elements (3/6)"), elapsed time, spinner. - Three rendered sections in order:
- running (spinner + phase name)
- queued ("Queued · position N" — FIFO order)
- completed / failed (check / × icon)
- Polls
GET /jobs/<id>at 50 ms for the very first check (catches near-instant jobs) and 1 s after that while anything is active. - Reconnect on page load:
GET /jobs?filename=&project=restores any running/queued jobs into the panel. fetchOrJob(url)(also inviewer.html) is the generic helper for any consumer that calls a cache-or-enqueue endpoint — it returns JSON on 200 or awaits the job on 202, then re-fetches.- Panel auto-hides 5 s after all jobs finish.
Coordinate Mapping
The Problem
IFC files use one coordinate system. web-ifc (with COORDINATE_TO_ORIGIN) may remap
and shift axes when loading geometry. All backend data (routes, graph, zones, spaces)
is computed in IFC world coordinates, but must be transformed to match the Three.js scene.
Auto-Detection Algorithm
- Compute IFC world bounding box (from
/spaces/endpoint:ifcWorldMin/Max) - Compute Three.js model bounding box (
THREE.Box3.setFromObject) - Try all 48 permutation+sign combinations (6 axis permutations × 8 sign combos)
- For each candidate: check size match (tolerance 1.0m), compute offset
- Pick mapping with minimum error (transformed space bbox fits within model bbox)
Result
coordMapping = {
perm: [0, 2, 1], // perm[threeAxis] = ifcAxis
sgn: [1, -1, 1], // sign flip per axis
off: [0.0, 3.2, -10.4] // translation offset
}
function ifcToThree(ifcPoint) {
return new THREE.Vector3(
sgn[0] * ifcPoint[perm[0]] + off[0],
sgn[1] * ifcPoint[perm[1]] + off[1],
sgn[2] * ifcPoint[perm[2]] + off[2],
);
}
Usage
All overlay rendering functions call ifcToThree() to transform IFC coordinates
before creating Three.js geometry. This single function handles the full mapping.
Performance Optimizations
- Gzip on heavy JSON endpoints via Flask-Compress (
/connection_graph,/wall_zones,/heizlast,/project-info,/spaces,/sizing). Threshold 500 B — small responses ship uncompressed, large ones shrink 4-5×. RespectsAccept-Encoding: gzip; falls back to identity for clients that don't accept gzip. - Prefetch
/spaceson Detail view activation so the 3D view opens instantly (cache is warm by the time the user hits#3d). - IFC download progress bar: the
Loading IFC model…overlay shows a real progress bar usingfetch+ReadableStream+Content-Length. On a 50 MB model over a slow connection the user sees the percentage tick up instead of a blank spinner. - Debounced save-on-blur: Roombook / Presets / Heizlast surface edits use
a 300 ms debounce (
_debouncehelper). Prevents POST storms from change+blur double-fire on the same input; Enter flushes immediately. - Material sharing: materials cached by RGBA color key (~50 shared instances instead
of one per geometry). Uses
MeshLambertMaterial(cheaper than PBRMeshStandardMaterial). - Frustum culling: bounding boxes computed per geometry so Three.js skips off-screen meshes
- Renderer settings: shadows disabled, pixel ratio capped at 2,
powerPreference: 'high-performance' - Zone batching: all zone quads per color category merged into single
BufferGeometry(~8 meshes instead of ~50,000 for large models)
Finding rooms / surfaces (Ctrl+F replacement)
Every long list has a built-in search input — this replaces browser native Ctrl+F in anticipation of per-list virtualization:
- Element sidebar (3D view): existing
#searchInputfilters items by name/type. - Roombook (
#rbSearch): fuzzy matches on name, long_name, storey, GlobalId. Escape clears; Enter jumps to the first match. Debounced 120 ms. - Heizlast surface tables: per-room search box appears when a room has more than 6 surfaces. Matches on type, source, adjacency.

Result counts render next to each search input (24 / 312).
Glossar-Tooltips (Hover-Erklärungen)
Every technical abbreviation in the viewer — q, Grenze, A_eff, f_aktiv,
ΔT_ln, n, θ_e, n₅₀, ΔU_TB, Φ_HL/T/V/RH, U-Quelle chips, f_k,
P (m), and more — carries a dotted underline and reveals a tooltip on hover
or keyboard focus. Each entry has a short description, optional formula, and a
standards reference (DIN EN 12831, EN 442, EN 1264, EN ISO 13370).


Architecture (templates/viewer.html):
- A single GLOSSARY object in module scope is the source of truth for every
term's label, unit, short description, optional formula, and ref.
- The gl(key, override?) helper emits a <span class="gl-term" data-gl="…">
chip that the renderSizingCandidateRow, renderSurfaceTableIntoContainer,
renderHeizlast, renderPresets, renderInspectorSurface, and
renderInspectorRoom call sites wrap around labels.
- One page-level #glTooltip div; document-level delegated listeners for
mouseover / mouseout / focusin / focusout / Escape drive it. No
dependencies.
- Keyboard-accessible via tabindex="0" on each chip.
Coverage today — Auslegung candidate rows, Heizlast column headers, the
.u-src chip, Heizlast top band, per-room header totals, Inspector surface
and room cards, all Presets form labels (PRESET_FIELDS[i].gl key).
Known gaps / Phase 3:
- Extend to Roombook column headers (usage_type, shielding, ach_min etc.).
- A dedicated /docs/glossary reference page for print-out / onboarding.
- English translations (currently German-only).
Phase 2 follow-ups (not shipped in this pass)
Triggers for revisiting documented in the plan file (plan-mode-weve-prancy-rabin.md):
- True row virtualization (IntersectionObserver) on Roombook + Heizlast surface
tables + element sidebar. Helper (
createVirtualList) is already implemented inviewer.html— wire-up deferred until real-world models consistently exceed 300 rooms or 1000+ element sidebar items. Current DOM-toggle search works fine at today's scale. - Structured navigation (P2-1): chip filters (usage type, Auffälligkeiten)
- storey-collapse grouping persisted in
localStorage. - Saved scroll position per session (P2-2):
sessionStoragerestoration of{selectedCompositeID, activeView, scrollY_per_view}on reload. - Lazy per-storey chunks on
/connection_graph+/wall_zones(P2-3). - Icon rail nav (P2-4) and keyboard shortcuts (P2-7).
- 3D scene idle-disposal (MVP-4e, deferred): dispose geometries/materials after 60 s off-tab to free memory on long sessions. Feature-flag planned.
- Explicitly not shipping: command palette (Ctrl+K) and split viewport — the ribbon delivers the 10 % of split-viewport value; a palette doesn't clear the bar on a 5-tab app.
State Management
Key JavaScript variables in the module scope:
| Variable | Type | Purpose |
|---|---|---|
elementMeshes |
Map |
Three.js meshes per IFC element |
elementData |
Map |
Element metadata |
coordMapping |
{perm, sgn, off} | IFC↔Three.js coordinate transform |
routeGroup |
THREE.Group | Cable route overlay meshes |
connGraphGroup |
THREE.Group | Connection graph overlay |
wallZoneGroup |
THREE.Group | Installation zone overlays (renderOrder 1) |
ceilingZoneGroup |
THREE.Group | Ceiling zone overlays (renderOrder 1) |
edgeGroup |
THREE.Group | Wall outlines for 2D plan (child of allMeshGroup) |
is2DPlan |
boolean | Current view mode |
wallZoneMode |
string | "normal" | "false_ceiling" | "false_floor" |
zoneDataCache |
object | null | Cached zone JSON for fast storey rebuilds |
storeyData |
array | Storey list from /storeys/ endpoint |
activeStoreyID |
number | null | Selected storey (null = all) |