diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 40f7049..40a778a 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -48,7 +48,7 @@ {"id":"ifcurl-dag","title":"Viewer: no model metadata panel — disciplines, spatial structure, element counts","description":"Beyond individual element selection, reviewers and designers need a model overview: what disciplines/types are present, how many elements, what building storeys exist, what's the spatial structure (site/building/storey/space hierarchy). A collapsible side panel showing IFC spatial tree and type breakdown would give immediate context and allow navigating by storey or discipline. Without this, reviewers must already know the model structure before opening the viewer.","status":"closed","priority":3,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T11:04:55Z","created_by":"Bruno Postle","updated_at":"2026-04-25T19:04:25Z","started_at":"2026-04-25T18:38:17Z","closed_at":"2026-04-25T19:04:25Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ifcurl-ov6","title":"Viewer selector only supports type names — property filters require server round-trip","description":"The viewer's applySelector() only supports IFC type names and their unions ('IfcWall+IfcSlab'). Full IfcOpenShell selector syntax — filter by property value (Pset_WallCommon.FireRating=2HR), by GlobalId, by name (.Name='Core Wall'), by spatial containment — is not supported. For collaboration, reviewers need to select specific elements ('all walls with fire rating 2HR that aren't compliant') not just all walls. The service's /preview endpoint already handles full selector syntax server-side. The viewer should forward complex selectors to the service and use the rendered image, or fetch the matching GlobalIds from a new /resolve-selector service endpoint and apply them in the viewer.","status":"closed","priority":3,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T11:04:38Z","created_by":"Bruno Postle","updated_at":"2026-04-25T19:19:28Z","started_at":"2026-04-25T19:15:47Z","closed_at":"2026-04-25T19:19:28Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ifcurl-f3p","title":"footer.tmpl: 'View in 3D' button absent from file history (git log) page","description":"The 'View in 3D' button is injected on the file view page (showing one commit of a file). It is not shown on the file history page (the list of commits that touched a file), which is where users naturally go to see how a model evolved over time. Each entry in the history list already has a commit hash in its href. Injecting a 'View in 3D' link or button alongside each history entry would let reviewers quickly compare model states across commits — which is a primary collaboration use case.","status":"closed","priority":3,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T11:04:23Z","created_by":"Bruno Postle","updated_at":"2026-04-24T14:04:56Z","started_at":"2026-04-24T13:21:51Z","closed_at":"2026-04-24T14:04:56Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"ifcurl-4rj","title":"Viewer: no branch/ref switcher — can't navigate model history or compare versions","description":"The viewer loads a single ifc:// URL and has no UI to switch to a different ref. A reviewer wanting to see 'what did this look like on main before the PR' or 'show me the v1.2 tag' must manually edit the URL. For a collaboration tool, being able to flip between 'current PR branch' and 'base branch' at the same camera view is essential for review. Consider a ref dropdown or input in the toolbar that reconstructs the URL with a different @ref, preserving camera/clip/selector state. The structured fields already expose repo/ref/path — the ref field is editable but not prominently labelled as a version picker.","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-24T11:04:20Z","created_by":"Bruno Postle","updated_at":"2026-04-24T11:04:20Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"ifcurl-4rj","title":"Viewer: no branch/ref switcher — can't navigate model history or compare versions","description":"The viewer loads a single ifc:// URL and has no UI to switch to a different ref. A reviewer wanting to see 'what did this look like on main before the PR' or 'show me the v1.2 tag' must manually edit the URL. For a collaboration tool, being able to flip between 'current PR branch' and 'base branch' at the same camera view is essential for review. Consider a ref dropdown or input in the toolbar that reconstructs the URL with a different @ref, preserving camera/clip/selector state. The structured fields already expose repo/ref/path — the ref field is editable but not prominently labelled as a version picker.","status":"closed","priority":3,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T11:04:20Z","created_by":"Bruno Postle","updated_at":"2026-04-25T19:28:14Z","started_at":"2026-04-25T19:25:40Z","closed_at":"2026-04-25T19:28:14Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ifcurl-np8","title":"Viewer depends on CDN (unpkg, esm.sh) — broken in air-gapped or restricted networks","description":"viewer.html loads all JavaScript from public CDNs: three.js from esm.sh, @thatopen/components and fragments from esm.sh/unpkg, web-ifc WASM from unpkg, the fragment worker from unpkg, JSZip from esm.sh. In corporate BIM environments with restricted internet (common in construction/engineering), the viewer will fail to load entirely. There is no documented self-hosted path. Consider: document how to serve the dependencies from the Forgejo instance's own /assets/ directory, or provide a bundled viewer.html as a build artifact. The viewer worker fetch ('https://unpkg.com/@thatopen/fragments@3.3.1/dist/Worker/worker.mjs') is already fetched at runtime with a special workaround for Firefox — this is the most fragile CDN dependency.","status":"closed","priority":3,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T11:04:04Z","created_by":"Bruno Postle","updated_at":"2026-04-25T17:25:36Z","started_at":"2026-04-25T12:09:35Z","closed_at":"2026-04-25T17:25:36Z","close_reason":"viewer.html now loads all JS from /assets/ (bundled at build time via npm+esbuild). No CDN dependencies remain.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ifcurl-sa0","title":"Viewer: no load progress — large IFC files appear to hang","description":"The viewer status shows 'Fetching IFC file…' and 'Parsing IFC…' as plain text but gives no indication of how far along the download or parse is. IFC files in real projects are often 20–200MB. The fetch() call and the web-ifc WASM parser both block for seconds with no visual feedback. A progress bar or percentage indicator during fetch (using response.body ReadableStream + Content-Length header) and a spinner during parse would prevent users abandoning the page thinking it crashed. The status element exists and is already used — it just needs richer updates.","status":"closed","priority":3,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T11:03:58Z","created_by":"Bruno Postle","updated_at":"2026-04-24T11:47:23Z","started_at":"2026-04-24T11:14:56Z","closed_at":"2026-04-24T11:47:23Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ifcurl-4xl","title":"viewer.html: selector support is a subset of IfcOpenShell syntax — not documented","description":"applySelector() in the viewer only handles type-name union syntax ('IfcWall+IfcSlab'). The comment says 'Attribute/property filters not yet supported', but this limitation is not visible to users and not documented in README.md or SPECIFICATION.md. A user pasting a URL with 'selector=IfcWall,+Pset_WallCommon.FireRating=2HR' will see all elements rather than the filtered set, with no error. Should add a user-visible note or at minimum document the viewer's selector limitations.","status":"closed","priority":3,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T10:41:49Z","created_by":"Bruno Postle","updated_at":"2026-04-24T14:04:57Z","started_at":"2026-04-24T13:22:10Z","closed_at":"2026-04-24T14:04:57Z","close_reason":"Closed","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 34d5dc3..4549825 100644 --- a/forgejo/custom/public/assets/viewer.html +++ b/forgejo/custom/public/assets/viewer.html @@ -160,9 +160,11 @@
- +
+
@@ -291,6 +293,11 @@ } } + // Populate ref datalist in background (non-blocking). + if (parsed?.host && parsed?.repoSuffix) { + populateRefList(parsed.host, parsed.repoSuffix).catch(() => { /* silent */ }); + } + // ----------------------------------------------------------------------- // Navigate to the viewer with a different ifc:// URL. // ----------------------------------------------------------------------- @@ -304,13 +311,49 @@ if (e.key === "Enter") { const v = urlInput.value.trim(); if (v) loadUrl(v); } }); + // Build URL from structured fields, preserving camera/clip/fov/scale from + // the current URL so that ref/path changes keep the viewer's camera state. + function loadFromFields() { + if (threeCamera) syncCameraUrl(threeCamera); + const structural = buildIfcUrl(); + if (!structural) return; + const current = urlInput.value.trim(); + const qI = current.indexOf("?"); + if (qI < 0) { loadUrl(structural); return; } + const currentQs = new URLSearchParams(current.slice(qI + 1)); + const qI2 = structural.indexOf("?"); + const structBase = qI2 < 0 ? structural : structural.slice(0, qI2); + const structQs = new URLSearchParams(qI2 < 0 ? "" : structural.slice(qI2 + 1)); + for (const key of ["camera", "fov", "scale"]) { + const v = currentQs.get(key); + if (v) structQs.set(key, v); else structQs.delete(key); + } + structQs.delete("clip"); + for (const v of currentQs.getAll("clip")) structQs.append("clip", v); + const qsStr = structQs.toString() + .replace(/%2C/g, ",").replace(/%2B/g, "+").replace(/%24/g, "$"); + loadUrl(structBase + (qsStr ? "?" + qsStr : "")); + } + // Pressing Enter in any structured field rebuilds the URL and navigates. for (const el of [repoInput, refInput, pathInput, selectorInput]) { - el.addEventListener("keydown", e => { - if (e.key === "Enter") { const u = buildIfcUrl(); if (u) loadUrl(u); } - }); + el.addEventListener("keydown", e => { if (e.key === "Enter") loadFromFields(); }); } + // ▾ button: clear + focus the ref field so the datalist shows all refs. + const refPickBtn = document.getElementById("ref-pick-btn"); + refPickBtn.addEventListener("click", () => { + refInput.value = ""; + refInput.focus(); + }); + + // Auto-navigate when user selects a datalist option (exact match). + refInput.addEventListener("input", () => { + const dl = document.getElementById("ref-list"); + const known = [...dl.options].map(o => o.value); + if (known.includes(refInput.value.trim())) loadFromFields(); + }); + // Disable camera controls while any input has focus. function onInputFocus() { if (cameraControls) cameraControls.enabled = false; } function onInputBlur() { if (cameraControls) cameraControls.enabled = true; } @@ -396,6 +439,8 @@ el.addEventListener("pointerdown", e => e.stopPropagation()); el.addEventListener("mousedown", e => e.stopPropagation()); } + refPickBtn.addEventListener("pointerdown", e => e.stopPropagation()); + refPickBtn.addEventListener("mousedown", e => e.stopPropagation()); fovInput.addEventListener("change", () => { if (!threeCamera?.isPerspectiveCamera) return; @@ -979,6 +1024,36 @@ } } + // ----------------------------------------------------------------------- + // Ref picker: populate with branches + tags. + // ----------------------------------------------------------------------- + async function populateRefList(host, repoSuffix) { + const proto = host.startsWith("localhost") ? "http" : "https"; + const base = `${proto}://${host}/api/v1/repos/${repoSuffix}`; + const sig = AbortSignal.timeout(4000); + const [branchRes, tagRes] = await Promise.allSettled([ + fetch(`${base}/branches?limit=50`, { signal: sig }), + fetch(`${base}/tags?limit=50`, { signal: sig }), + ]); + const dl = document.getElementById("ref-list"); + if (branchRes.status === "fulfilled" && branchRes.value.ok) { + const branches = await branchRes.value.json().catch(() => []); + for (const b of (branches ?? [])) { + const opt = document.createElement("option"); + opt.value = `heads/${b.name}`; + dl.appendChild(opt); + } + } + if (tagRes.status === "fulfilled" && tagRes.value.ok) { + const tags = await tagRes.value.json().catch(() => []); + for (const t of (tags ?? [])) { + const opt = document.createElement("option"); + opt.value = `tags/${t.name}`; + dl.appendChild(opt); + } + } + } + // ----------------------------------------------------------------------- // Commit-pin indicator helpers. // -----------------------------------------------------------------------