From 264cd0016298774437bd07b0f9030266ff5a3516 Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Mon, 8 Jun 2026 08:02:54 +0100 Subject: [PATCH] Fit button: frame the selected object when one is selected MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fit button always framed the whole model. Now it tracks the currently selected element(s) — from click-to-inspect or a query-panel row — in selectedLocalIds, and fits the camera to their merged bounding box (model.getMergedBox). Selection is cleared on deselect and on model reload, so with nothing selected fit-to-all is unchanged. Closes ifcurl-qql Co-Authored-By: Claude Opus 4.8 (1M context) --- .beads/issues.jsonl | 3 ++- forgejo/custom/public/assets/viewer.js | 27 ++++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 3dbcc8e..75f1475 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -3,6 +3,7 @@ {"id":"ifcurl-p60","title":"Viewer: no element click-to-identify — can't get GlobalId or properties","description":"There is no way to click on a mesh in the viewer and see which IFC element it represents. Reviewers can see geometry but cannot reference specific elements — they can't discover a GlobalId to put in a selector URL, can't see the element name/type/Psets, and can't say 'this specific wall' in a BCF or Forgejo comment. This breaks the collaboration workflow at its foundation: every useful feedback comment needs to identify specific elements. Need: click or hover on a surface to highlight it and show a properties panel (name, type, GlobalId, key Psets). The GlobalId should be copyable into the selector field or URL. The ThatOpen components library provides the tools for this (OBC.IfcRelationsIndexer, element property queries).","status":"closed","priority":1,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T11:02:33Z","created_by":"Bruno Postle","updated_at":"2026-04-24T11:47:23Z","started_at":"2026-04-24T11:14:57Z","closed_at":"2026-04-24T11:47:23Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ifcurl-4l7","title":"viewer.html: scale= URLs don't load in orthographic mode","description":"applyCameraParam() reads the camera= and fov= parameters from the URL and applies them, but never reads scale= or switches the camera to orthographic (parallel) projection. URLs like ?camera=...\u0026scale=50 will load in perspective mode instead of orthographic. The OBC OrthoPerspectiveCamera can switch modes, but the code path to set parallel projection and apply the scale value is missing from applyCameraParam.","status":"closed","priority":1,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T09:04:51Z","created_by":"Bruno Postle","updated_at":"2026-04-24T10:56:29Z","started_at":"2026-04-24T10:50:19Z","closed_at":"2026-04-24T10:56:29Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ifcurl-362","title":"Add missing server-config files: gitconfig-ifcmerge and gitattributes","description":"forgejo/README.md documents deploying two files that don't exist in the repo: forgejo/server-config/gitconfig-ifcmerge and forgejo/server-config/gitattributes. Both are required for the ifcmerge git merge driver to work in bare Forgejo repos. The README even includes 'sudo cp forgejo/server-config/gitattributes /etc/gitattributes' and 'sudo git config --system include.path .../gitconfig-ifcmerge', but the files themselves are absent. Anyone following the deployment instructions will hit a 'file not found' error. Need to create both files with the content described in the README.","status":"closed","priority":1,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T09:04:31Z","created_by":"Bruno Postle","updated_at":"2026-04-24T10:51:47Z","started_at":"2026-04-24T10:50:15Z","closed_at":"2026-04-24T10:51:47Z","close_reason":"Files already exist: forgejo/server-config/gitconfig-ifcmerge and gitattributes are both present and correct. Missed in initial review.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"ifcurl-qql","title":"Fit button: fit camera to selected object when one is selected","description":"When the 'fit' button is clicked while an object is selected (via click-to-inspect or query panel), the camera should fit to that object's bounding box rather than the whole model. With no selection, fit-to-all behaviour is unchanged.\n\nCurrently fit-btn always calls cameraControls.fitToBox(loadedModel.box). Selection in the viewer is transient (computed in the highlighter select handler and discarded). Need to (1) persist the currently-selected localIds + model, and (2) compute a bbox for that subset using the fragments box API (getBoxes/getMergedBox/BoundingBoxer found in viewer-deps.js), then fitToBox that.","status":"in_progress","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-08T06:55:30Z","created_by":"Bruno Postle","updated_at":"2026-06-08T06:55:45Z","started_at":"2026-06-08T06:55:45Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ifcurl-i5c","title":"Document picker: private repos not visible — picker needs to use OAuth2 access token","description":"The select-documents UI at /documents/1.0/select-documents/ui calls the Forgejo repos API without authentication, so only public repos appear in the dropdown. In a real OpenCDE integration the client would have obtained an access_token via the OAuth2 flow before invoking the document picker. The picker should accept the access_token (e.g. passed in the callback_url context or as a query param) and forward it as Authorization: Bearer to the Forgejo API so private repos are visible.","status":"closed","priority":2,"issue_type":"bug","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-07T22:41:36Z","created_by":"Bruno Postle","updated_at":"2026-06-07T22:56:37Z","started_at":"2026-06-07T22:48:07Z","closed_at":"2026-06-07T22:56:37Z","close_reason":"Picker forwards OAuth2 access token as Authorization: Bearer to Forgejo API; private repos now visible","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ifcurl-e9g","title":"Security review: viewer.js and viewer HTML","description":"Perform a security review of the browser-side viewer code. Files to cover: forgejo/custom/public/assets/viewer.js and forgejo/custom/public/assets/viewer.html (or equivalent). Areas to examine: ifc:// URL parsing and handling of untrusted URL components before passing to API calls or displaying in the UI, postMessage or cross-origin communication patterns, innerHTML or other unsafe DOM sinks fed from URL parameters or API responses, camera/selector parameter injection into API URLs, BCF file parsing (Zip content, XML parsing) for injection or path traversal, any eval() or dynamic script execution, handling of Forgejo API responses in the metadata/query panels, content security policy considerations, handling of the ifc:// protocol in OS-registered handler (ifcurl-open/register). Produce a prioritised list of findings with recommended fixes.","status":"closed","priority":2,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-07T11:51:09Z","created_by":"Bruno Postle","updated_at":"2026-06-07T13:50:24Z","started_at":"2026-06-07T11:51:34Z","closed_at":"2026-06-07T13:50:24Z","close_reason":"Two findings fixed: stored XSS in populateMetaPanel via unescaped IFC model data (added hesc() helper), sessionStorage CSS injection in viewer.html snapshot overlay (validated data:image/jpeg;base64, prefix). No other high-confidence findings.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"ifcurl-b99","title":"Security review: Python API service (service.py, bcf_api.py, documents_api.py)","description":"Perform a security review of the core Python API service code. Files to cover: ifcurl/service.py (rate limiting, SSRF protection, caching, all endpoints), ifcurl/bcf_api.py (BCF 3.0 REST routes, Forgejo proxy, GUID encoding), ifcurl/documents_api.py (OpenCDE Documents API, select-documents picker), ifcurl/foundation_api.py (OAuth2 proxy routes). Areas to examine: SSRF protection gaps (is _ssrf_check applied consistently to all endpoints that fetch remote content?), sandbox escape paths (does sandboxed rendering fully isolate arbitrary IFC input?), injection risks in Forgejo API proxying (header forwarding, response handling), authentication bypass in BCF routes, document_id encoding/decoding robustness, input validation on all Pydantic models, any path traversal in file or cache operations, tier-4 cache poisoning via mutable/immutable ref misclassification. Produce a prioritised list of findings with recommended fixes.","status":"closed","priority":2,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-06-07T11:51:06Z","created_by":"Bruno Postle","updated_at":"2026-06-07T13:50:05Z","started_at":"2026-06-07T11:51:32Z","closed_at":"2026-06-07T13:50:05Z","close_reason":"Three findings fixed: hostname SSRF bypass in _is_private_ip (now resolves DNS, fail-safe on error), SSRF+token exfiltration in BCF snapshot endpoint (now validates URL via _ssrf_check, drops caller token), X-Forwarded-Host in download URLs noted as medium/deployment concern.","dependency_count":0,"dependent_count":0,"comment_count":0} @@ -145,6 +146,6 @@ {"id":"ifcurl-vam","title":"Phase 6: Bonsai offer ifc:// URL when saving linked model reference","description":"When saving an IFCDOCUMENTREFERENCE in Bonsai, offer the option to write the location as an ifc:// URL rather than a relative path. When a relative path would traverse above the repository root, prompt the user to convert it to an ifc:// URL. Requires Phase 4 federation (ifcurl-bqt).","status":"deferred","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-23T05:49:16Z","created_by":"Bruno Postle","updated_at":"2026-04-23T06:15:13Z","defer_until":"2026-12-01T00:00:00Z","dependencies":[{"issue_id":"ifcurl-vam","depends_on_id":"ifcurl-bqt","type":"blocks","created_at":"2026-04-23T06:49:23Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"id":"ifcurl-9k2","title":"Phase 5: IFC Viewer integration (equivalent of Phase 4)","description":"Add ifc:// OS protocol handler and 'Copy view URL' button to the IFC Viewer desktop application, equivalent to the Bonsai Phase 4 integration. Depends on Phase 4 being complete as a reference implementation.","status":"deferred","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-23T05:49:08Z","created_by":"Bruno Postle","updated_at":"2026-04-23T06:15:12Z","defer_until":"2026-12-01T00:00:00Z","dependencies":[{"issue_id":"ifcurl-9k2","depends_on_id":"ifcurl-0oj","type":"blocks","created_at":"2026-04-23T06:49:14Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"id":"ifcurl-prc","title":"Phase 3c: 3D diff view for IFC PR merges","description":"After a PR merge completes, surface a 'View merge in 3D' button using the Phase 3b viewer. Use @thatopen/components highlighter to colour added, removed, and modified elements differently, driven by comparing the merged IFC against either parent. Requires Phase 3b viewer (ifcurl-i1s) to be working.","status":"closed","priority":4,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-23T05:48:30Z","created_by":"Bruno Postle","updated_at":"2026-04-26T17:15:07Z","closed_at":"2026-04-26T17:15:07Z","close_reason":"Rendered diff already implemented in footer.tmpl Case 3; static PNG via /render_diff endpoint, not interactive 3D","dependencies":[{"issue_id":"ifcurl-prc","depends_on_id":"ifcurl-i1s","type":"blocks","created_at":"2026-04-23T06:48:36Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} -{"_type":"memory","key":"ifcurl-service-names-are-ifcurl-api-service-and","value":"ifcurl service names are ifcurl-api.service and ifcurl-render.service (not ifcurl.service). On the remote deployment server (Python 3.12), the package install path is /opt/ifcurl/lib/python3.12/site-packages/ifcurl/ and the Python binary is /opt/ifcurl/bin/python3. (The local dev machine uses Python 3.14 — adjust the python3.XX path component to match whichever host you are on.)"} {"_type":"memory","key":"forgejo-build-and-deploy-procedure-go-build-c","value":"Forgejo build and deploy procedure: go build -C /home/bruno/src/forgejo -tags 'sqlite sqlite_unlock_notify' -ldflags \"-X 'forgejo.org/modules/setting.StaticRootPath=/usr/share/forgejo'\" -o /home/bruno/src/forgejo/forgejo . — then sudo cp forgejo /usr/bin/forgejo. StaticRootPath=/usr/share/forgejo. CustomPath=/var/lib/forgejo/custom. Custom templates go to /var/lib/forgejo/custom/templates/custom/ (e.g. footer.tmpl → /var/lib/forgejo/custom/templates/custom/footer.tmpl). Custom static files go to /var/lib/forgejo/custom/public/assets/ (NOT /etc/forgejo/public/assets/) and are served at /assets/*."} +{"_type":"memory","key":"ifcurl-service-names-are-ifcurl-api-service-and","value":"ifcurl service names are ifcurl-api.service and ifcurl-render.service (not ifcurl.service). On the remote deployment server (Python 3.12), the package install path is /opt/ifcurl/lib/python3.12/site-packages/ifcurl/ and the Python binary is /opt/ifcurl/bin/python3. (The local dev machine uses Python 3.14 — adjust the python3.XX path component to match whichever host you are on.)"} {"_type":"memory","key":"torawurl-in-viewer-url-js-must-support-github","value":"toRawUrl in viewer-url.js must support GitHub, GitLab, Forgejo/Gitea (codeberg.org, self-hosted), and localhost. Tests in forgejo/tests/viewer-url.test.js cover all four. The localhost check (http vs https) is based on host.startsWith('localhost'), so subdomain.localhost gets https."} diff --git a/forgejo/custom/public/assets/viewer.js b/forgejo/custom/public/assets/viewer.js index 1c883e6..1f21417 100644 --- a/forgejo/custom/public/assets/viewer.js +++ b/forgejo/custom/public/assets/viewer.js @@ -45,6 +45,9 @@ let activeClipper = null; let loadedModel = null; let rendererCanvas = null; let currentGuids = []; +// localIds of the currently inspected/selected element(s), or null when nothing +// is selected. Used by the fit button to frame the selection instead of the model. +let selectedLocalIds = null; // Raw HTTP URL of the loaded IFC file, captured once at model load time. // Used to decide whether a loadUrl call needs a full reload or an in-place update. let modelRawUrl = null; @@ -397,8 +400,20 @@ newIssueBtn.addEventListener("click", () => { window.open(issueUrl, "_blank", "noopener"); }); -document.getElementById("fit-btn").addEventListener("click", () => { - if (cameraControls && loadedModel && !loadedModel.box.isEmpty()) +document.getElementById("fit-btn").addEventListener("click", async () => { + if (!cameraControls || !loadedModel) return; + if (selectedLocalIds?.length) { + try { + const box = await loadedModel.getMergedBox(selectedLocalIds); + if (box && !box.isEmpty()) { + await cameraControls.fitToBox(box, true); + return; + } + } catch (err) { + console.warn("Fit to selection failed, falling back to model:", err.message); + } + } + if (!loadedModel.box.isEmpty()) cameraControls.fitToBox(loadedModel.box, true); }); @@ -777,6 +792,7 @@ async function applyQuery(srcUrl, queryPath) { if (!components || !loadedModel) return; const localIds = (await loadedModel.getLocalIdsByGuids([guid])).filter(id => id !== null); if (!localIds.length) return; + selectedLocalIds = localIds; const items = { [loadedModel.modelId]: new Set(localIds) }; await applyFragmentStyle(components, items, new THREE.Color(0xff8800), 1); }); @@ -1305,6 +1321,7 @@ async function main() { try { const model = await ifcLoader.load(buffer, true, "model"); loadedModel = model; + selectedLocalIds = null; modelRawUrl = toRawUrl(currentIfcUrl); world.scene.three.add(model.object); @@ -1335,6 +1352,11 @@ async function main() { highlighter = components.get(OBCF.Highlighter); highlighter.setup({ world }); highlighter.events.select.onHighlight.add(async (fragmentIdMap) => { + selectedLocalIds = Object.values(fragmentIdMap) + .filter(Boolean) + .flatMap(ids => [...ids]) + .filter(id => id != null); + if (!selectedLocalIds.length) selectedLocalIds = null; for (const localIds of Object.values(fragmentIdMap)) { if (!localIds) continue; const localId = [...localIds][0]; @@ -1355,6 +1377,7 @@ async function main() { } }); highlighter.events.select.onClear.add(() => { + selectedLocalIds = null; propsPanel.style.display = "none"; }); } catch (err) {