Skip to content

Commit a201552

Browse files
jochem25claude
andcommitted
fix: ghost mode selected element now renders at full opacity
Replace material-level opacity manipulation with fragment-level FragmentsModel.setOpacity()/resetOpacity()/setColor() API. This correctly handles per-element opacity even when IFC elements share batched meshes, fixing the bug where the selected element remained transparent in ghost mode. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3674eb5 commit a201552

3 files changed

Lines changed: 91 additions & 101 deletions

File tree

STATUS.md

Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,53 @@
1-
# Project Status — 2026-03-27
1+
# Project Status — 2026-03-29
22

3-
## Huidige fase: Fase 4 (BCF Platform Integratie) — deployed
3+
## Huidige fase: Fase 7 (Hybrid Cloud + Multi-tenant) — deployed
44

55
### Wat is af
66
- Fase 0: Research & validatie
77
- Fase 1: Engine + CLI
88
- Fase 2: Web interface (FastAPI + React)
99
- Fase 3: 3D Viewer koppeling
1010
- Deployment setup (Docker + Hetzner)
11-
- **Fase 4: BCF Issue Management** — gebouwd + deployed
12-
- **Fase 5: Chrome Design System** — ribbon, backstage, i18n, theming
13-
- **Fase 6: Nextcloud Cloud Storage** — WebDAV, save/open dialog
11+
- Fase 4: BCF Issue Management — gebouwd + deployed
12+
- Fase 5: Chrome Design System — ribbon, backstage, i18n, theming
13+
- Fase 6: Nextcloud Cloud Storage — WebDAV, save/open dialog
14+
- **Fase 7a: Project Management** — PostgreSQL backend, REST API, frontend UI
15+
- **Fase 7b: Hybrid Nextcloud I/O** — volume mount reads, WebDAV writes, multi-tenant
1416

15-
### Gedaan in deze sessie (2026-03-27)
16-
- Uitklapbare IFC ruimtelijke structuur in ModelBrowser
17-
- PropertyExtractor: `extractSpatialTree()` + `getContainedElements()` + `getAllGlobalIds()`
18-
- ViewerEngine: bridge methoden voor spatial tree extractie
19-
- CenterPanel: event handlers (spatial-tree-request/response, contained-elements-request/response)
20-
- SpatialSubTree component: inline boom per model met lazy-loaded element groepen
21-
- ModelBrowser: expand chevron per geladen model, lazy tree loading
22-
- Oude standalone SpatialTree component verwijderd
23-
- 100% client-side, geen backend dependency
24-
- Model visibility toggle werkend (ViewerEngine ↔ store bridge)
25-
- Element isolatie (ghost mode): selectie → alles transparant grijs, geselecteerd element opaque verdigris
26-
- Highlight-overlay aanpak (geen base material modificatie)
27-
- `isolateElement()` / `clearIsolation()` in ViewerEngine
28-
- CenterPanel synct automatisch met selectedElementId
29-
- Ribbon Beeld-tab: Passend, Herstel camera, en Reset weergave knoppen werkend
30-
- Cache-Control headers op index.html (no-cache) en /assets/ (immutable, 1yr)
17+
### Gedaan in vorige sessie (2026-03-28)
18+
- Project management systeem gebouwd (full-stack):
19+
- Backend: async SQLAlchemy 2.0 + PostgreSQL (SQLite fallback)
20+
- ORM models: Project + ProjectFile
21+
- REST API: /api/v2/projects CRUD + file upload/download/delete
22+
- Frontend: IProjectStorage interface + ServerProjectStorage + LocalProjectStorage
23+
- .bvp bestandsformaat voor lokale projecten (File System Access API)
24+
- ProjectList component in Backstage + i18n (NL + EN)
25+
- AppShell integratie met handleOpenProject
26+
- Hybrid Nextcloud I/O migratie:
27+
- server/tenant_config.py: multi-tenant config loader (tenants.json)
28+
- server/volume_reader.py: directe filesystem reads van NC volume mount
29+
- server/routers/cloud.py: refactored cloud router (was inline in main.py)
30+
- NextcloudClient: multi-tenant factory (from_tenant) + client registry
31+
- docker-compose.yml: NC data volume (ro), tenant config mount
32+
- config/tenants.json: 3BM tenant configuratie
33+
- Ghost mode pogingen (nog niet werkend):
34+
- Opacity 0.85→0.15: transparantie zelf werkt nu
35+
- Reset knop werkt
36+
- Geselecteerd element wordt nog NIET opaque getoond
37+
- Poging 1: mesh-level material restore → werkt niet (IFC batcht elementen in shared meshes)
38+
- Poging 2: fragment highlight overlay met opacity 1.0 → werkt ook niet
39+
- Moet dieper onderzocht worden hoe That Open Engine fragment highlights werken
40+
- Deployed naar Hetzner (commits a2d3b11, 5c8ba74, d12678f, 3674eb5)
41+
42+
### Gedaan in deze sessie (2026-03-29)
43+
- **Ghost mode bug opgelost:**
44+
- Oorzaak: material-level opacity (`mat.opacity = 0.15`) beïnvloedt alle elementen in shared meshes door IFC batching
45+
- Fragment highlight overlay werd door transparante materials heen gerenderd (Three.js render order: opaque → transparent)
46+
- Fix: vervangen door `FragmentsModel.setOpacity()` / `resetOpacity()` / `setColor()` — fragment-level per-element controle
47+
- Verwijderd: `savedMaterials` map, `allGuidsCache`, `restoreMaterials()` methode
48+
- Geselecteerd element krijgt nu correct volle opacity + Verdigris kleur (#44B6A8)
3149

3250
### Nog te doen
33-
- Element isolatie finetunen (testen of highlight overlay correct werkt bij grote modellen)
34-
- BCF Platform OIDC auth (bcfSlice) nog op oude flow — aparte fix nodig
35-
- BCF ZIP import testen in BIMcollab/Solibri/Revit
36-
- Push issues naar BCF Platform testen
51+
- NC_SERVICE_PASS_3BM instellen in .env op server → cloud wordt actief
52+
- Cloud status testen na wachtwoord configuratie
53+
- OIDC tenant claim koppelen aan tenant selectie (nu hardcoded default "3bm")

TODO.md

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,38 @@
11
# TODO
22

3+
## Bug: Ghost mode — geselecteerd element niet opaque
4+
- [x] Oorzaak: material-level opacity beïnvloedt alle elementen in shared meshes (IFC batching)
5+
- [x] Fragment highlight overlay werkt niet omdat transparante materials erover heen renderen
6+
- [x] Fix: FragmentsModel.setOpacity() / resetOpacity() / setColor() per element (fragment-level)
7+
- [x] Verwijderd: savedMaterials map, allGuidsCache, restoreMaterials(), material traversal
8+
9+
## Blocker: NC service account
10+
- [ ] NC_SERVICE_PASS_3BM instellen in `/opt/openaec/bim-validator/.env` op server
11+
- [ ] `sudo docker compose up -d` na .env aanmaken
12+
- [ ] Verifieer: `curl http://localhost:8000/api/cloud/status``enabled: true, connected: true`
13+
314
## Deploy
415
- [x] OIDC client registreren in Authentik
516
- [x] `.env` aanmaken met VITE_OIDC config
617
- [x] Dockerfile updaten met OIDC build args
7-
- [x] Auth omgebouwd naar Authentik proxy headers (geen redirect URI nodig)
8-
- [ ] Deploy: `ssh jochem@bim.open-aec.com``cd /opt/openaec/bim-validator && sudo git pull && sudo docker compose build --no-cache && sudo docker compose up -d`
18+
- [x] Auth omgebouwd naar Authentik proxy headers
19+
- [x] Project management systeem deployed (PostgreSQL + REST API)
20+
- [x] Hybrid Nextcloud I/O deployed (volume mount + WebDAV)
921

1022
## Testen
11-
- [ ] Element isolatie: ghost mode testen met grote/meerdere modellen
23+
- [ ] Cloud: project listing via volume mount (70_BIM bestanden)
24+
- [ ] Cloud: BCF save via WebDAV → zichtbaar in Nextcloud
25+
- [ ] Cloud: IFC download via volume mount (performance test)
1226
- [ ] Proxy auth: login via Authentik → user zichtbaar in TitleBar
1327
- [ ] BCF ZIP downloaden → importeren in BIMcollab/Solibri/Revit
1428
- [ ] Push issues naar BCF Platform → verifieer op platform UI
1529
- [ ] Cross-site SSO: login validator → open platform → zelfde projecten
16-
- [ ] Project aanmaken vanuit validator → zichtbaar op platform
1730

18-
## BCF Platform Auth
19-
- [ ] bcfSlice OIDC flow fixen (aparte client, redirect URI registreren) of omzetten naar proxy-based tokens
31+
## Multi-tenant
32+
- [ ] OIDC tenant claim koppelen aan tenant selectie (nu hardcoded "3bm")
33+
- [ ] Frontend: tenant-aware API calls (tenant query param)
2034

21-
## Fase 5: Polish & Launch
35+
## Fase 8: Polish & Launch
2236
- [ ] 3BM branding
2337
- [ ] Landing page
2438
- [ ] SSL certificaat (automatisch via Caddy)

viewer/src/engine/ViewerEngine.ts

Lines changed: 28 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -101,19 +101,9 @@ export class ViewerEngine {
101101
/** Property extractors per model, keyed by modelId. Lazy initialized. */
102102
private propertyExtractors = new Map<string, PropertyExtractor>();
103103

104-
/** Cached all-GlobalIds per model for isolation mode. */
105-
private allGuidsCache = new Map<string, string[]>();
106-
107104
/** Whether isolation mode is active. */
108105
private _isolated = false;
109106

110-
111-
/** Saved material state for isolation restore. */
112-
private savedMaterials = new Map<
113-
THREE.Material,
114-
{ opacity: number; transparent: boolean }
115-
>();
116-
117107
/** Active section planes. */
118108
private sectionPlanes: THREE.Plane[] = [];
119109

@@ -494,79 +484,49 @@ export class ViewerEngine {
494484
}
495485

496486
/**
497-
* Isolate an element: ghost everything transparent,
498-
* then highlight the selected element with an opaque overlay.
487+
* Isolate an element: ghost everything at fragment level,
488+
* then restore the selected element to full opacity with accent color.
499489
*
500-
* Material-level opacity ghosts all geometry (fast).
501-
* Fragment-level highlight renders the selected element on top (per-element accurate).
490+
* Uses FragmentsModel.setOpacity() for per-element control (works correctly
491+
* even when elements share meshes in batched IFC geometry).
502492
*/
503493
async isolateElement(globalId: string): Promise<void> {
504-
if (!this.world) return;
494+
if (!this.world || !this.fragments) return;
505495

506496
// Restore any previous isolation first
507-
this.restoreMaterials();
508-
await this.clearHighlights();
509-
510-
// Ghost ALL meshes at the material level
511-
for (const obj of this.modelObjects.values()) {
512-
obj.traverse((child) => {
513-
if (!(child instanceof THREE.Mesh)) return;
514-
const materials = Array.isArray(child.material)
515-
? child.material
516-
: [child.material];
517-
for (const mat of materials) {
518-
if (!mat || this.savedMaterials.has(mat)) continue;
519-
this.savedMaterials.set(mat, {
520-
opacity: mat.opacity,
521-
transparent: mat.transparent,
522-
});
523-
mat.transparent = true;
524-
mat.opacity = GHOST_OPACITY;
525-
mat.needsUpdate = true;
526-
}
527-
});
528-
}
529-
530-
// Highlight selected element with opaque overlay (fragment-level, per-element)
531-
if (this.fragments) {
532-
try {
533-
const modelIdMap = await this.fragments.guidsToModelIdMap([globalId]);
534-
if (Object.keys(modelIdMap).length > 0) {
535-
const style: FRAGS.MaterialDefinition = {
536-
color: new THREE.Color("#44B6A8"), // Verdigris
537-
renderedFaces: FRAGS.RenderedFaces.TWO,
538-
opacity: 1.0,
539-
transparent: false,
540-
};
541-
await this.fragments.highlight(style, modelIdMap);
542-
}
543-
} catch {
544-
// Highlight failed — ghost-only mode still works
497+
await this.clearIsolation();
498+
499+
// Get the selected element's model ID map
500+
const selectedMap = await this.fragments.guidsToModelIdMap([globalId]);
501+
502+
// Ghost all elements, then restore + colorize the selected one
503+
for (const [modelId, model] of this.fragments.list) {
504+
// Ghost ALL items in this model at fragment level
505+
await model.setOpacity(undefined, GHOST_OPACITY);
506+
507+
// If this model contains the selected element, restore it
508+
const selectedIds = selectedMap[modelId];
509+
if (selectedIds && selectedIds.size > 0) {
510+
const ids = [...selectedIds];
511+
await model.resetOpacity(ids);
512+
await model.setColor(ids, new THREE.Color("#44B6A8")); // Verdigris accent
545513
}
546514
}
547515

548516
this._isolated = true;
549517
}
550518

551519
/**
552-
* Restore all materials to their original state.
520+
* Clear isolation: reset all fragment opacities and colors.
553521
*/
554-
private restoreMaterials(): void {
555-
for (const [mat, saved] of this.savedMaterials) {
556-
mat.opacity = saved.opacity;
557-
mat.transparent = saved.transparent;
558-
mat.needsUpdate = true;
522+
async clearIsolation(): Promise<void> {
523+
if (!this._isolated || !this.fragments) return;
524+
525+
for (const [, model] of this.fragments.list) {
526+
await model.resetOpacity(undefined);
527+
await model.resetColor(undefined);
559528
}
560-
this.savedMaterials.clear();
561-
}
562529

563-
/**
564-
* Clear isolation: restore all materials and remove highlight overlay.
565-
*/
566-
async clearIsolation(): Promise<void> {
567-
if (!this._isolated) return;
568-
this.restoreMaterials();
569-
await this.clearHighlights();
570530
this._isolated = false;
571531
}
572532

@@ -859,7 +819,6 @@ export class ViewerEngine {
859819
this.modelObjects.clear();
860820
this.modelBoxes.clear();
861821
this.modelBytes.clear();
862-
this.allGuidsCache.clear();
863822
this._isolated = false;
864823

865824
for (const extractor of this.propertyExtractors.values()) {

0 commit comments

Comments
 (0)