From a33a4f92968f507e2256a9efd8e2f21f0b3eba78 Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Fri, 24 Apr 2026 07:10:19 +0100 Subject: [PATCH] viewer.html: implement clip= clipping planes applyClipPlanes() parses all repeatable clip= params (6 floats: point + normal in IFC world coords), transforms each via toThree() into Three.js space, and sets world.renderer.three.clippingPlanes. syncCameraUrl already preserves clip= params when rebuilding the URL on camera movement. Co-Authored-By: Claude Sonnet 4.6 --- .beads/issues.jsonl | 2 +- forgejo/custom/public/assets/viewer.html | 28 ++++++++++++++++++++---- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index ea8a16a..caca9a0 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,4 +1,4 @@ -{"id":"ifcurl-bab","title":"Implement clip= parameter in viewer.html","description":"The ifc:// spec defines a repeatable clip= parameter (6 floats: point x,y,z + normal x,y,z in IFC world coords). The Python ifcurl renderer handles it, but viewer.html ignores it entirely — no parsing, no Three.js clipping planes, no round-trip into the URL on camera sync. Implement: parse clip= from the ifc:// URL, apply as THREE.Plane clipping planes to the renderer (with IFC→Three.js coordinate transform), and include current clip planes when rebuilding the URL.","status":"open","priority":2,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-24T05:45:58Z","created_by":"Bruno Postle","updated_at":"2026-04-24T05:45:58Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"ifcurl-bab","title":"Implement clip= parameter in viewer.html","description":"The ifc:// spec defines a repeatable clip= parameter (6 floats: point x,y,z + normal x,y,z in IFC world coords). The Python ifcurl renderer handles it, but viewer.html ignores it entirely — no parsing, no Three.js clipping planes, no round-trip into the URL on camera sync. Implement: parse clip= from the ifc:// URL, apply as THREE.Plane clipping planes to the renderer (with IFC→Three.js coordinate transform), and include current clip planes when rebuilding the URL.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T05:45:58Z","created_by":"Bruno Postle","updated_at":"2026-04-24T06:09:31Z","started_at":"2026-04-24T06:08:44Z","closed_at":"2026-04-24T06:09:31Z","close_reason":"applyClipPlanes() parses all clip= params, transforms IFC→Three.js coords via toThree(), constructs THREE.Plane objects and sets on world.renderer.three.clippingPlanes. syncCameraUrl already preserves clip= params unchanged.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ifcurl-yju","title":"viewer.html: structured form UI for ifc:// URL components","description":"Replace the single ifc:// URL text input with individual labelled fields for each component: host/repository, ref (branch/tag/commit), path (file within repo), selector (IfcOpenShell selector), camera, fov/scale. Editing any field should update the ifc:// URL and reload the model. The raw ifc:// URL should still be visible/copyable. This makes the viewer usable without needing to hand-edit a URL string.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-23T21:07:11Z","created_by":"Bruno Postle","updated_at":"2026-04-23T21:32:17Z","started_at":"2026-04-23T21:12:42Z","closed_at":"2026-04-23T21:32:17Z","close_reason":"Structured form UI implemented with repo/ref/path/selector fields; crash recovered, changes committed.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ifcurl-2zf","title":"viewer.html: FOV control and restore fov=/scale= from URL","description":"Two gaps: (1) No UI to change the camera FOV — add a control (e.g. a small number input or slider in the toolbar) so the user can adjust perspective FOV or orthographic scale. (2) applyCameraParam reads position/direction but ignores the fov= and scale= params written by syncCameraUrl — restore these when placing camera from URL so a shared link opens at exactly the recorded viewpoint.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-23T20:42:24Z","created_by":"Bruno Postle","updated_at":"2026-04-23T21:07:42Z","started_at":"2026-04-23T20:52:48Z","closed_at":"2026-04-23T21:07:42Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ifcurl-yga","title":"viewer.html: implement ifc:// selector= parameter","description":"The ifc:// URL spec supports a selector= param using IfcOpenShell selector syntax (e.g. selector=IfcWall, selector=IfcSlab.Name='Floor'). The viewer currently ignores it. Implement selector filtering in the browser: parse the selector= value, query matching elements from the loaded FragmentsModel, and apply highlight/isolate/ghost visibility to them. @thatopen/components has Highlighter and Hider components that may be usable. The selector syntax used is the IfcOpenShell Python selector syntax — a JS port or subset may be needed.","notes":"Selector syntax docs: https://docs.ifcopenshell.org/ifcopenshell-python/selector_syntax.html — Python implementation in ifcopenshell.util.selector module. Need a JS port or subset. Syntax includes: IfcType, .Attribute='value', #GlobalId, [pset.prop], boolean operators (+ for union, , for intersection within group).","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-23T20:42:22Z","created_by":"Bruno Postle","updated_at":"2026-04-24T05:58:30Z","started_at":"2026-04-24T05:51:01Z","closed_at":"2026-04-24T05:58:30Z","close_reason":"applySelector() implemented: byCategory classification + prefix subtype matching + hider.isolate(). Supports simple type names and + union. Attribute filters noted as future work.","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/forgejo/custom/public/assets/viewer.html b/forgejo/custom/public/assets/viewer.html index 9d84849..e89790a 100644 --- a/forgejo/custom/public/assets/viewer.html +++ b/forgejo/custom/public/assets/viewer.html @@ -313,6 +313,19 @@ history.replaceState(null, "", pageUrl.toString()); } + // ----------------------------------------------------------------------- + // Apply clip= planes from the ifc:// URL to the Three.js renderer. + // Each clip= is 6 floats: point x,y,z + normal x,y,z in IFC world coords. + // ----------------------------------------------------------------------- + function applyClipPlanes(world, clips) { + const planes = clips.map(([px, py, pz, nx, ny, nz]) => { + const p3 = toThree(px, py, pz); + const n3 = toThree(nx, ny, nz).normalize(); + return new THREE.Plane(n3, -n3.dot(p3)); + }); + world.renderer.three.clippingPlanes = planes; + } + // ----------------------------------------------------------------------- // Apply selector= by isolating matching IFC elements. // Supports simple type names and + union: "IfcWall+IfcSlab". @@ -476,10 +489,17 @@ } model.useCamera(threeCamera); - const qIdxSel = ifcUrl.indexOf("?"); - const selectorStr = new URLSearchParams( - qIdxSel < 0 ? "" : ifcUrl.slice(qIdxSel + 1) - ).get("selector") ?? ""; + const qIdxPost = ifcUrl.indexOf("?"); + const qsPost = new URLSearchParams( + qIdxPost < 0 ? "" : ifcUrl.slice(qIdxPost + 1) + ); + + const clips = qsPost.getAll("clip") + .map(s => s.split(",").map(Number)) + .filter(v => v.length === 6 && !v.some(isNaN)); + if (clips.length) applyClipPlanes(world, clips); + + const selectorStr = qsPost.get("selector") ?? ""; if (selectorStr) { statusEl.textContent = "Applying selector…"; await applySelector(components, selectorStr);