From 83a8e087edd0f25a5930ab9596a11d2f58187597 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 08:41:33 +0000 Subject: [PATCH 1/9] Sync beads after main merge https://claude.ai/code/session_01NBsytCd6L7UQPiKbcLn4kn --- .beads/issues.jsonl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index f0da9f2..037284c 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -2,6 +2,7 @@ {"_type":"issue","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} {"_type":"issue","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} {"_type":"issue","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} +{"_type":"issue","id":"ifcurl-k0x","title":"Server: add /clash endpoint for geometry intersection detection","description":"Add GET and POST /clash endpoints to service.py. The endpoint accepts an ifc:// URL with two selectors (selector_a and selector_b query parameters; if selector_b is omitted, test selector_a elements against all other model elements). It runs IfcOpenShell geometry intersection detection in the sandbox: bounding-box pre-filter to find candidate pairs, then mesh-level intersection test for confirmed clashes. Returns JSON {pairs: [[guidA, guidB], ...]} listing every clashing element pair. The combined set of clashing GUIDs can then be used to construct a visibility=clash URL. Sandbox the detection via a new _sandboxed_clash(ifc_bytes, selector_a, selector_b) function in render_service.py. Results for immutable refs may be cached at Tier 4.","status":"open","priority":2,"issue_type":"feature","owner":"noreply@anthropic.com","created_at":"2026-05-09T08:28:49Z","created_by":"Claude","updated_at":"2026-05-09T08:28:49Z","dependency_count":0,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"ifcurl-19h","title":"Renderer: implement clash visibility mode","description":"In render.py _add_shape, add a clash branch between the ghost and isolate/else cases. Clash should render selected elements in a distinct red colour (define _CLASH_COLOR e.g. (220, 50, 50)) and non-selected elements in their normal material colours (same as highlight mode but for non-selected). Currently the clash value falls through to the else/isolate branch incorrectly. Update the _add_shape docstring to list clash as a supported mode. Also update the render() docstring to mention clash in the visibility parameter description.","status":"open","priority":2,"issue_type":"feature","owner":"noreply@anthropic.com","created_at":"2026-05-09T08:08:32Z","created_by":"Claude","updated_at":"2026-05-09T08:08:32Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"ifcurl-8rf","title":"Server: add /query endpoint for attribute/property retrieval","description":"Add GET and POST /query endpoints to service.py (mirroring the /select pattern). The endpoint resolves an ifc:// URL's selector, then for each matched element retrieves the attribute or property value named by the query parameter (dot-notation: Name, Pset_WallCommon.FireRating). Returns JSON {GlobalId: value, ...}, excluding elements with no GlobalId and attributes that resolve to entity references rather than scalars. Add _sandboxed_query(ifc_bytes, selector, query_path) to render_service.py and a matching _query_via_socket() delegation path. Reuse Tier 2/3 caches for IFC bytes and GUID sets. No Tier 4 caching — responses depend on the query path, not just the URL hash.","status":"open","priority":2,"issue_type":"feature","owner":"noreply@anthropic.com","created_at":"2026-05-09T08:08:29Z","created_by":"Claude","updated_at":"2026-05-09T08:08:29Z","dependency_count":0,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"ifcurl-b1c","title":"Implement OpenCDE BCF API on Forgejo","description":"Expose a BCF 3.0 REST API as a Python route group in the preview service, proxied at /bcf/3.0/ on the Forgejo hostname via nginx/Caddy.\n\n## Architecture\n\nPython routes added to service.py alongside the existing /preview and /bcf endpoints. No new Forgejo Go code — the service forwards the user's Authorization: Bearer header verbatim to Forgejo's REST API on localhost, so Forgejo's own auth middleware enforces per-user permissions. No service account or machine token required.\n\nBCF \u003c-\u003e ifc:// conversion stays entirely in Python: bcf.py (ifc:// -\u003e BCF, already exists) and the reverse direction implemented in ifcurl-l50.\n\n## Design\n\nThe BCF API is a stateless translation layer over Forgejo's REST API. No Forgejo recompilation, no schema changes, no new Go code.\n\n**Storage:** ifc:// URLs in issue and comment bodies are the viewpoint format. This is already what the viewer Issue button produces. The BCF\u003c-\u003eifc:// conversion is specified in SPECIFICATION.md §6.\n\n**Mapping:**\n\n| BCF resource | Forgejo |\n|---|---|\n| Project | Repository (project_id = owner/repo) |\n| Topic | Issue |\n| Topic guid | Derived from issue ID |\n| Comment | Issue comment |\n| Viewpoint | ifc:// URL found in issue/comment body |\n| Viewpoint guid | Derived from comment ID |\n| Snapshot | ifc:// URL proxied through preview service |\n\n**Auth:** BCF client sends Authorization: Bearer \u003cforgejo-token\u003e. Service forwards it to Forgejo REST API. No credential storage, no service token.\n\n**Read viewpoints:** scan issue/comment bodies for ifc:// URLs, convert each to a BCF viewpoint object on the fly via the §6 mapping.\n\n**Write viewpoints:** convert incoming BCF viewpoint to an ifc:// URL (ifcurl-l50), post as a new Forgejo comment.\n\n**Snapshot:** GET .../viewpoints/{guid}/snapshot proxies the stored ifc:// URL through the existing /preview endpoint.\n\n## Minimum viable endpoints\n\n- GET /bcf/3.0/projects\n- GET /bcf/3.0/projects/{id}\n- GET/POST /bcf/3.0/projects/{id}/topics\n- GET/PUT /bcf/3.0/projects/{id}/topics/{guid}\n- GET/POST /bcf/3.0/projects/{id}/topics/{guid}/comments\n- GET/POST /bcf/3.0/projects/{id}/topics/{guid}/viewpoints\n- GET /bcf/3.0/projects/{id}/topics/{guid}/viewpoints/{guid}\n- GET /bcf/3.0/projects/{id}/topics/{guid}/viewpoints/{guid}/snapshot\n- GET /bcf/3.0/projects/{id}/extensions (static minimal response)\n\n## Prerequisites\n\n- ifcurl-lwl: Foundation API discovery so BCF clients can locate this endpoint via OpenCDE auto-discovery\n- ifcurl-l50: BCF -\u003e ifc:// URL conversion needed for POST /viewpoints","status":"closed","priority":2,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-28T22:55:11Z","created_by":"Bruno Postle","updated_at":"2026-04-29T18:39:11Z","closed_at":"2026-04-29T18:39:11Z","close_reason":"Closed","dependencies":[{"issue_id":"ifcurl-b1c","depends_on_id":"ifcurl-l50","type":"blocks","created_at":"2026-04-29T18:44:06Z","created_by":"Claude","metadata":"{}"},{"issue_id":"ifcurl-b1c","depends_on_id":"ifcurl-lwl","type":"blocks","created_at":"2026-04-29T18:44:05Z","created_by":"Claude","metadata":"{}"}],"dependency_count":2,"dependent_count":0,"comment_count":0} @@ -48,7 +49,7 @@ {"_type":"issue","id":"ifcurl-ei8","title":"Fix syncCameraUrl: convert WebGL coords to IFC world space","description":"syncCameraUrl in viewer.py currently records camera.position/direction/up directly from Three.js, which are in WebGL/Three.js coordinate space (Y-up, different scale/orientation than IFC). The ifc:// URL spec expects IFC world coordinates (Z-up, matching BCF viewpoint conventions). Need to find the IFC→Three.js transform that @thatopen/components applies when loading a model, then apply its inverse in syncCameraUrl before serialising the camera params. Repos cloned for reference: ~/src/engine_components and ~/src/engine_fragment.","status":"closed","priority":2,"issue_type":"bug","owner":"bruno@postle.net","created_at":"2026-04-23T05:53:06Z","created_by":"Bruno Postle","updated_at":"2026-04-23T06:14:36Z","closed_at":"2026-04-23T06:14:36Z","close_reason":"Split into atomic tasks: ifcurl-ldk, ifcurl-j2s, ifcurl-cmd","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"ifcurl-i1s","title":"Test Phase 3b web viewer end-to-end","description":"After deploying the footer template (ifcurl-ach), test the full flow: open an .ifc file in Forgejo at localhost:3000, click 'View in 3D', verify the viewer loads the model. CDN versions (@thatopen/components 2.4.0, three 0.160.0, web-ifc 0.0.66) may need adjustment if the API has changed.","status":"closed","priority":2,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-23T05:48:01Z","created_by":"Bruno Postle","updated_at":"2026-04-23T20:05:04Z","started_at":"2026-04-23T19:54:56Z","closed_at":"2026-04-23T20:05:04Z","close_reason":"Closed","dependencies":[{"issue_id":"ifcurl-i1s","depends_on_id":"ifcurl-49o","type":"blocks","created_at":"2026-04-23T07:14:12Z","created_by":"Claude","metadata":"{}"},{"issue_id":"ifcurl-i1s","depends_on_id":"ifcurl-4yu","type":"blocks","created_at":"2026-04-23T07:14:11Z","created_by":"Claude","metadata":"{}"},{"issue_id":"ifcurl-i1s","depends_on_id":"ifcurl-ach","type":"blocks","created_at":"2026-04-23T06:48:10Z","created_by":"Claude","metadata":"{}"},{"issue_id":"ifcurl-i1s","depends_on_id":"ifcurl-ahd","type":"blocks","created_at":"2026-04-23T07:02:54Z","created_by":"Claude","metadata":"{}"}],"dependency_count":4,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"ifcurl-ach","title":"Deploy Phase 3b footer template to Forgejo","description":"The footer.tmpl file that injects a 'View in 3D' button on .ifc file pages has been written but not yet deployed. Deploy it to /etc/forgejo/templates/custom/footer.tmpl and verify the button appears when viewing an .ifc file on localhost:3000.","status":"closed","priority":2,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-04-23T05:47:53Z","created_by":"Bruno Postle","updated_at":"2026-04-23T06:14:35Z","closed_at":"2026-04-23T06:14:35Z","close_reason":"Split into atomic tasks: ifcurl-ohq, ifcurl-49o","dependencies":[{"issue_id":"ifcurl-ach","depends_on_id":"ifcurl-ahd","type":"blocks","created_at":"2026-04-23T07:02:40Z","created_by":"Claude","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} -{"_type":"issue","id":"ifcurl-982","title":"Viewer: implement clash visibility mode display","description":"Implement visibility=clash in the Bonsai viewer. Selected elements should be displayed with a solid red clash-indicator style (full opacity, distinct red material) while all other elements render normally — equivalent to highlight mode but using red rather than orange for selected elements. The viewer should also be able to construct ifc:// URLs with visibility=clash from a set of clashing element GUIDs (e.g. from a clash detection result). When exporting to BCF, clash is treated as highlight (BCF has no equivalent distinction).","status":"open","priority":3,"issue_type":"feature","owner":"noreply@anthropic.com","created_at":"2026-05-09T08:08:45Z","created_by":"Claude","updated_at":"2026-05-09T08:08:45Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"ifcurl-982","title":"Viewer: implement clash visibility mode display","description":"Implement visibility=clash in the Bonsai viewer. Selected elements (GUIDs pre-identified as clashing, typically sourced from the server /clash endpoint) should be displayed with a solid red clash-indicator style (full opacity, distinct red material) while all other elements render normally — equivalent to highlight mode but using red rather than orange for selected elements. The viewer should be able to construct ifc://...\u0026visibility=clash URLs from a set of GUID pairs returned by /clash. When exporting to BCF, clash is treated as highlight (BCF has no equivalent distinction).","status":"open","priority":3,"issue_type":"feature","owner":"noreply@anthropic.com","created_at":"2026-05-09T08:08:45Z","created_by":"Claude","updated_at":"2026-05-09T08:28:44Z","dependencies":[{"issue_id":"ifcurl-982","depends_on_id":"ifcurl-k0x","type":"blocks","created_at":"2026-05-09T08:28:57Z","created_by":"Claude","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"ifcurl-4u3","title":"Viewer: query creation and editing UI","description":"Add UI controls in the Bonsai viewer for composing and modifying queries. Users need: a text field for the selector expression, a text field for the query path (dot-notation, e.g. Pset_WallCommon.FireRating), and an Apply button that updates the active ifc:// URL and triggers a re-query. The UI should also provide a copy-URL button. Changing the selector or query fields should update the URL in the address bar and the displayed 3D view/table without requiring a full page reload.","status":"open","priority":3,"issue_type":"feature","owner":"noreply@anthropic.com","created_at":"2026-05-09T08:08:42Z","created_by":"Claude","updated_at":"2026-05-09T08:08:42Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"ifcurl-2iv","title":"Viewer: display query results in a table panel","description":"When an ifc:// URL contains a query parameter, the viewer (Bonsai) should call the preview service /query endpoint and display the returned {GlobalId: value} mapping in a table panel alongside the 3D view. The table should have columns for element GlobalId (or Name if resolvable from the model) and the queried value. When both query and camera/visibility parameters are present, show the 3D view and property table together. The table should update whenever the URL's selector or query changes.","status":"open","priority":3,"issue_type":"feature","owner":"noreply@anthropic.com","created_at":"2026-05-09T08:08:39Z","created_by":"Claude","updated_at":"2026-05-09T08:08:39Z","dependencies":[{"issue_id":"ifcurl-2iv","depends_on_id":"ifcurl-8rf","type":"blocks","created_at":"2026-05-09T08:08:49Z","created_by":"Claude","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"ifcurl-xfi","title":"Implement OpenCDE document picker (select-documents flow)","description":"The OpenCDE Documents API defines a browser-redirect flow for document discovery. Implement POST /documents/1.0/select-documents which returns a select_documents_url. That URL serves a small HTML page: owner/repo selector then file-tree browser. When user selects an IFC file, generate the document_id and redirect to the callback_url with document_ids[] params.","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-29T20:38:16Z","created_by":"Bruno Postle","updated_at":"2026-04-29T20:38:16Z","dependency_count":0,"dependent_count":0,"comment_count":0} @@ -117,5 +118,5 @@ {"_type":"issue","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":"Claude","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","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":"Claude","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","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":"Claude","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} -{"_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."} {"_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":"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."} From b560551f3cb3c08b64cd5f7dc2efb57b28cc082d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 08:46:26 +0000 Subject: [PATCH 2/9] Implement clash visibility mode and /query endpoint (ifcurl-19h, ifcurl-8rf) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit render.py: add _CLASH_COLOR and clash branch in _add_shape — selected elements rendered in red, non-selected in normal material colours. render_service.py: add _sandboxed_query(ifc_bytes, selector, query_path) and /query endpoint. Dot-notation query_path splits into pset/property (Pset_WallCommon.FireRating) or falls back to direct IFC attribute (Name). Returns {GlobalId: str(value)}, excluding None and entity references. service.py: add QueryRequest model, _query_via_socket() for socket delegation, and GET/POST /query endpoints reusing Tier 2 byte cache. https://claude.ai/code/session_01NBsytCd6L7UQPiKbcLn4kn --- .beads/issues.jsonl | 2 +- ifcurl/render.py | 9 ++++ ifcurl/render_service.py | 60 +++++++++++++++++++++++++ ifcurl/service.py | 94 +++++++++++++++++++++++++++++++++++++++- 4 files changed, 163 insertions(+), 2 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 037284c..ac32d94 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -3,7 +3,7 @@ {"_type":"issue","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} {"_type":"issue","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} {"_type":"issue","id":"ifcurl-k0x","title":"Server: add /clash endpoint for geometry intersection detection","description":"Add GET and POST /clash endpoints to service.py. The endpoint accepts an ifc:// URL with two selectors (selector_a and selector_b query parameters; if selector_b is omitted, test selector_a elements against all other model elements). It runs IfcOpenShell geometry intersection detection in the sandbox: bounding-box pre-filter to find candidate pairs, then mesh-level intersection test for confirmed clashes. Returns JSON {pairs: [[guidA, guidB], ...]} listing every clashing element pair. The combined set of clashing GUIDs can then be used to construct a visibility=clash URL. Sandbox the detection via a new _sandboxed_clash(ifc_bytes, selector_a, selector_b) function in render_service.py. Results for immutable refs may be cached at Tier 4.","status":"open","priority":2,"issue_type":"feature","owner":"noreply@anthropic.com","created_at":"2026-05-09T08:28:49Z","created_by":"Claude","updated_at":"2026-05-09T08:28:49Z","dependency_count":0,"dependent_count":1,"comment_count":0} -{"_type":"issue","id":"ifcurl-19h","title":"Renderer: implement clash visibility mode","description":"In render.py _add_shape, add a clash branch between the ghost and isolate/else cases. Clash should render selected elements in a distinct red colour (define _CLASH_COLOR e.g. (220, 50, 50)) and non-selected elements in their normal material colours (same as highlight mode but for non-selected). Currently the clash value falls through to the else/isolate branch incorrectly. Update the _add_shape docstring to list clash as a supported mode. Also update the render() docstring to mention clash in the visibility parameter description.","status":"open","priority":2,"issue_type":"feature","owner":"noreply@anthropic.com","created_at":"2026-05-09T08:08:32Z","created_by":"Claude","updated_at":"2026-05-09T08:08:32Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"ifcurl-19h","title":"Renderer: implement clash visibility mode","description":"In render.py _add_shape, add a clash branch between the ghost and isolate/else cases. Clash should render selected elements in a distinct red colour (define _CLASH_COLOR e.g. (220, 50, 50)) and non-selected elements in their normal material colours (same as highlight mode but for non-selected). Currently the clash value falls through to the else/isolate branch incorrectly. Update the _add_shape docstring to list clash as a supported mode. Also update the render() docstring to mention clash in the visibility parameter description.","status":"in_progress","priority":2,"issue_type":"feature","assignee":"Claude","owner":"noreply@anthropic.com","created_at":"2026-05-09T08:08:32Z","created_by":"Claude","updated_at":"2026-05-09T08:44:22Z","started_at":"2026-05-09T08:44:22Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"ifcurl-8rf","title":"Server: add /query endpoint for attribute/property retrieval","description":"Add GET and POST /query endpoints to service.py (mirroring the /select pattern). The endpoint resolves an ifc:// URL's selector, then for each matched element retrieves the attribute or property value named by the query parameter (dot-notation: Name, Pset_WallCommon.FireRating). Returns JSON {GlobalId: value, ...}, excluding elements with no GlobalId and attributes that resolve to entity references rather than scalars. Add _sandboxed_query(ifc_bytes, selector, query_path) to render_service.py and a matching _query_via_socket() delegation path. Reuse Tier 2/3 caches for IFC bytes and GUID sets. No Tier 4 caching — responses depend on the query path, not just the URL hash.","status":"open","priority":2,"issue_type":"feature","owner":"noreply@anthropic.com","created_at":"2026-05-09T08:08:29Z","created_by":"Claude","updated_at":"2026-05-09T08:08:29Z","dependency_count":0,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"ifcurl-b1c","title":"Implement OpenCDE BCF API on Forgejo","description":"Expose a BCF 3.0 REST API as a Python route group in the preview service, proxied at /bcf/3.0/ on the Forgejo hostname via nginx/Caddy.\n\n## Architecture\n\nPython routes added to service.py alongside the existing /preview and /bcf endpoints. No new Forgejo Go code — the service forwards the user's Authorization: Bearer header verbatim to Forgejo's REST API on localhost, so Forgejo's own auth middleware enforces per-user permissions. No service account or machine token required.\n\nBCF \u003c-\u003e ifc:// conversion stays entirely in Python: bcf.py (ifc:// -\u003e BCF, already exists) and the reverse direction implemented in ifcurl-l50.\n\n## Design\n\nThe BCF API is a stateless translation layer over Forgejo's REST API. No Forgejo recompilation, no schema changes, no new Go code.\n\n**Storage:** ifc:// URLs in issue and comment bodies are the viewpoint format. This is already what the viewer Issue button produces. The BCF\u003c-\u003eifc:// conversion is specified in SPECIFICATION.md §6.\n\n**Mapping:**\n\n| BCF resource | Forgejo |\n|---|---|\n| Project | Repository (project_id = owner/repo) |\n| Topic | Issue |\n| Topic guid | Derived from issue ID |\n| Comment | Issue comment |\n| Viewpoint | ifc:// URL found in issue/comment body |\n| Viewpoint guid | Derived from comment ID |\n| Snapshot | ifc:// URL proxied through preview service |\n\n**Auth:** BCF client sends Authorization: Bearer \u003cforgejo-token\u003e. Service forwards it to Forgejo REST API. No credential storage, no service token.\n\n**Read viewpoints:** scan issue/comment bodies for ifc:// URLs, convert each to a BCF viewpoint object on the fly via the §6 mapping.\n\n**Write viewpoints:** convert incoming BCF viewpoint to an ifc:// URL (ifcurl-l50), post as a new Forgejo comment.\n\n**Snapshot:** GET .../viewpoints/{guid}/snapshot proxies the stored ifc:// URL through the existing /preview endpoint.\n\n## Minimum viable endpoints\n\n- GET /bcf/3.0/projects\n- GET /bcf/3.0/projects/{id}\n- GET/POST /bcf/3.0/projects/{id}/topics\n- GET/PUT /bcf/3.0/projects/{id}/topics/{guid}\n- GET/POST /bcf/3.0/projects/{id}/topics/{guid}/comments\n- GET/POST /bcf/3.0/projects/{id}/topics/{guid}/viewpoints\n- GET /bcf/3.0/projects/{id}/topics/{guid}/viewpoints/{guid}\n- GET /bcf/3.0/projects/{id}/topics/{guid}/viewpoints/{guid}/snapshot\n- GET /bcf/3.0/projects/{id}/extensions (static minimal response)\n\n## Prerequisites\n\n- ifcurl-lwl: Foundation API discovery so BCF clients can locate this endpoint via OpenCDE auto-discovery\n- ifcurl-l50: BCF -\u003e ifc:// URL conversion needed for POST /viewpoints","status":"closed","priority":2,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-28T22:55:11Z","created_by":"Bruno Postle","updated_at":"2026-04-29T18:39:11Z","closed_at":"2026-04-29T18:39:11Z","close_reason":"Closed","dependencies":[{"issue_id":"ifcurl-b1c","depends_on_id":"ifcurl-l50","type":"blocks","created_at":"2026-04-29T18:44:06Z","created_by":"Claude","metadata":"{}"},{"issue_id":"ifcurl-b1c","depends_on_id":"ifcurl-lwl","type":"blocks","created_at":"2026-04-29T18:44:05Z","created_by":"Claude","metadata":"{}"}],"dependency_count":2,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"ifcurl-962","title":"forgejo: replace Go markdown extension with JS, eliminate required Go compile","description":"ifc_url.go is a Goldmark AST extension that requires patching and recompiling Forgejo to work. This is painful when dev environment (Fedora) differs from deployment (Docker) — compiler flags, static linking, build tags must all match. The goal is to make the ifc:// preview feature work with zero Go compilation, via pure JS asset deployment.\n\nPlan:\n1. Add querySelectorAll('a[href^=\"ifc://\"]') scanning to ifcurl.js — replace matched anchors with figure elements (preview image + viewer link + ifc:// URL). Goldmark already renders [title](ifc://...) as \u003ca href=\"ifc://...\"\u003etitle\u003c/a\u003e without any extension, so this covers all markdown link syntax.\n2. Change the Issue button in viewer.js to emit [view](ifc://...) markdown link syntax instead of a bare URL — so that user-generated issue bodies produce linkable anchors Goldmark will render, not plain text.\n3. The existing Go extension (ifc_url.go) and its markdown.go registration can be removed on the next Forgejo upgrade; the current patched binary continues to work in the meantime.\n\nAcceptance: after this change, deploying new ifcurl features only requires copying JS assets — no Forgejo rebuild needed.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-27T06:58:15Z","created_by":"Bruno Postle","updated_at":"2026-04-27T07:25:09Z","started_at":"2026-04-27T06:58:48Z","closed_at":"2026-04-27T07:25:09Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} diff --git a/ifcurl/render.py b/ifcurl/render.py index 02dde2d..4bec111 100644 --- a/ifcurl/render.py +++ b/ifcurl/render.py @@ -69,6 +69,9 @@ except ImportError: # Highlight colour used in 'highlight' visibility mode (orange). _HIGHLIGHT_COLOR = (255, 140, 0) +# Clash colour used in 'clash' visibility mode (red). +_CLASH_COLOR = (220, 50, 50) + # Diff colours: green=added, blue=modified, red=removed, grey=unchanged ghost. _DIFF_ADDED_COLOR = (50, 200, 50) _DIFF_MODIFIED_COLOR = (50, 100, 200) @@ -160,6 +163,7 @@ def _add_shape( highlight — non-selected shown normally, selected shown in highlight colour ghost — selected shown normally, non-selected shown as grey + translucent isolate — only selected elements are added (non-selected are skipped) + clash — non-selected shown normally, selected shown in clash colour (red) When *diff_ids* is provided the diff colouring takes precedence: green — step ID in diff_ids["added"] @@ -231,6 +235,11 @@ def _add_shape( color, opacity = _material_color_and_opacity(mat) else: color, opacity = (180, 180, 180), 0.10 + elif visibility == "clash": + if is_selected: + color, opacity = _CLASH_COLOR, 1.0 + else: + color, opacity = _material_color_and_opacity(mat) else: # isolate — non-selected already returned above color, opacity = _material_color_and_opacity(mat) diff --git a/ifcurl/render_service.py b/ifcurl/render_service.py index a2d7902..1da74de 100644 --- a/ifcurl/render_service.py +++ b/ifcurl/render_service.py @@ -106,6 +106,42 @@ def _sandboxed_select(ifc_bytes: bytes, selector: str) -> list[str]: return [e.GlobalId for e in matched if hasattr(e, "GlobalId") and e.GlobalId] +def _sandboxed_query(ifc_bytes: bytes, selector: str, query_path: str) -> dict[str, str]: + """Parse model, run selector, retrieve query_path value for each element. + + query_path uses dot notation: a bare name is an IFC attribute (e.g. 'Name'); + a dotted name is 'PsetName.PropertyName' (e.g. 'Pset_WallCommon.FireRating'). + Returns {GlobalId: str(value)} for elements that have a GlobalId and a + scalar value at query_path. Entity references and None values are excluded. + """ + import ifcopenshell + import ifcopenshell.util.element + import ifcopenshell.util.selector + + model = ifcopenshell.file.from_string(ifc_bytes.decode()) + matched = list(ifcopenshell.util.selector.filter_elements(model, selector)) + + result: dict[str, str] = {} + if "." in query_path: + pset_name, prop_name = query_path.split(".", 1) + else: + pset_name, prop_name = None, query_path + + for element in matched: + guid = getattr(element, "GlobalId", None) + if not guid: + continue + if pset_name is not None: + value = ifcopenshell.util.element.get_pset(element, pset_name, prop=prop_name) + else: + value = getattr(element, prop_name, None) + if value is None or hasattr(value, "id"): + continue + result[guid] = str(value) + + return result + + def _sandboxed_diff( head_bytes: bytes, base_bytes: bytes, @@ -229,6 +265,30 @@ async def select_endpoint( ) +@app.post("/query") +async def query_endpoint( + ifc: UploadFile = File(...), + selector: str = Form(...), + query_path: str = Form(...), +) -> Response: + """Retrieve a property/attribute value for each selected element. + + Returns JSON ``{"": "", …}``. + """ + ifc_bytes = await ifc.read() + + try: + result = run_sandboxed(_sandboxed_query, ifc_bytes, selector, query_path) + except SandboxCrashError as exc: + raise HTTPException(status_code=422, detail=f"IFC parse/query crashed: {exc}") from exc + except SandboxTimeoutError as exc: + raise HTTPException(status_code=503, detail=f"Query timed out: {exc}") from exc + except Exception as exc: + raise HTTPException(status_code=422, detail=f"Query failed: {exc}") from exc + + return Response(content=json.dumps(result), media_type="application/json") + + @app.post("/render_diff") async def render_diff_endpoint( head_ifc: UploadFile = File(...), diff --git a/ifcurl/service.py b/ifcurl/service.py index 8003ff2..e561058 100644 --- a/ifcurl/service.py +++ b/ifcurl/service.py @@ -56,7 +56,7 @@ from ifcurl.bcf_api import router as bcf_router from ifcurl.documents_api import router as documents_router from ifcurl.git import diff_text as git_diff_text from ifcurl.git import fetch_ifc -from ifcurl.render_service import _sandboxed_diff, _sandboxed_pipeline, _sandboxed_select +from ifcurl.render_service import _sandboxed_diff, _sandboxed_pipeline, _sandboxed_query, _sandboxed_select from ifcurl.sandbox import SandboxCrashError, SandboxTimeoutError, run_sandboxed from ifcurl.url import IfcUrl @@ -126,6 +126,24 @@ def _select_via_socket(ifc_bytes: bytes, selector: str) -> list[str]: return resp.json()["guids"] +def _query_via_socket(ifc_bytes: bytes, selector: str, query_path: str) -> dict[str, str]: + """Delegate query execution to the render service over the Unix socket.""" + import httpx + + try: + with httpx.Client(transport=httpx.HTTPTransport(uds=_RENDER_SOCKET)) as client: + resp = client.post( + "http://render/query", + files={"ifc": ifc_bytes}, + data={"selector": selector, "query_path": query_path}, + timeout=_RENDER_TIMEOUT, + ) + except httpx.TransportError as exc: + raise HTTPException(status_code=503, detail=f"Render service unavailable: {exc}") from exc + _check_render_response(resp) + return resp.json() + + def _diff_via_socket( head_bytes: bytes, base_bytes: bytes, @@ -405,6 +423,11 @@ class SelectRequest(BaseModel): token: str | None = None +class QueryRequest(BaseModel): + url: str + token: str | None = None + + class BcfRequest(BaseModel): url: str title: str = "IFC View" @@ -709,6 +732,75 @@ def select(request: SelectRequest) -> JSONResponse: return JSONResponse({"guids": guids}) +@app.get("/query") +def query_get(url: str, token: str | None = None) -> JSONResponse: + """GET variant of POST /query.""" + return query(QueryRequest(url=url, token=token)) + + +@app.post("/query") +def query(request: QueryRequest) -> JSONResponse: + """Retrieve a property or attribute value for each element matched by the selector. + + The URL must contain both ``selector=`` and ``query=`` parameters. The + ``query`` value uses dot notation: a bare name is an IFC attribute + (e.g. ``Name``); a dotted name addresses a property set + (e.g. ``Pset_WallCommon.FireRating``). + + Returns ``{"": "", …}`` JSON. Elements with no GlobalId + or whose query path resolves to an entity reference are excluded. + Results are not cached at Tier 4 — the response depends on the query path + as well as the URL, making a shared cache key impractical. + """ + try: + ifc_url = IfcUrl.parse(request.url) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + if ifc_url.path is None: + raise HTTPException(status_code=400, detail="URL has no 'path' parameter") + if not ifc_url.selector: + raise HTTPException(status_code=400, detail="URL has no 'selector' parameter") + if not ifc_url.query: + raise HTTPException(status_code=400, detail="URL has no 'query' parameter") + + _ssrf_check(ifc_url) + + token = request.token + if token is None and ifc_url.host: + token = get_token_for_host(ifc_url.host) + + try: + hexsha, ifc_bytes, _ = fetch_ifc(ifc_url, token=token) + except ImportError as exc: + raise HTTPException(status_code=503, detail=str(exc)) from exc + except ValueError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + + _check_ifc_size(ifc_bytes) + + cached_bytes = _t2_get(hexsha, ifc_url.path) + ifc_bytes = cached_bytes if cached_bytes is not None else ifc_bytes + if cached_bytes is None: + _t2_put(hexsha, ifc_url.path, ifc_bytes) + + try: + if _RENDER_SOCKET: + result = _query_via_socket(ifc_bytes, ifc_url.selector, ifc_url.query) + else: + result = run_sandboxed(_sandboxed_query, ifc_bytes, ifc_url.selector, ifc_url.query) + except SandboxCrashError as exc: + raise HTTPException( + status_code=422, detail=f"IFC parse/query crashed: {exc}" + ) from exc + except SandboxTimeoutError as exc: + raise HTTPException(status_code=503, detail=f"Query timed out: {exc}") from exc + except Exception as exc: + raise HTTPException(status_code=422, detail=f"Query failed: {exc}") from exc + + return JSONResponse(result) + + @app.get("/foundation/versions") def foundation_versions(request: Request) -> JSONResponse: """OpenCDE Foundation API discovery endpoint. From 39243e343a531005e1147e6810a39f403d85fb20 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 08:46:40 +0000 Subject: [PATCH 3/9] Close ifcurl-8rf in beads https://claude.ai/code/session_01NBsytCd6L7UQPiKbcLn4kn --- .beads/issues.jsonl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index ac32d94..a3dabdc 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -3,8 +3,8 @@ {"_type":"issue","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} {"_type":"issue","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} {"_type":"issue","id":"ifcurl-k0x","title":"Server: add /clash endpoint for geometry intersection detection","description":"Add GET and POST /clash endpoints to service.py. The endpoint accepts an ifc:// URL with two selectors (selector_a and selector_b query parameters; if selector_b is omitted, test selector_a elements against all other model elements). It runs IfcOpenShell geometry intersection detection in the sandbox: bounding-box pre-filter to find candidate pairs, then mesh-level intersection test for confirmed clashes. Returns JSON {pairs: [[guidA, guidB], ...]} listing every clashing element pair. The combined set of clashing GUIDs can then be used to construct a visibility=clash URL. Sandbox the detection via a new _sandboxed_clash(ifc_bytes, selector_a, selector_b) function in render_service.py. Results for immutable refs may be cached at Tier 4.","status":"open","priority":2,"issue_type":"feature","owner":"noreply@anthropic.com","created_at":"2026-05-09T08:28:49Z","created_by":"Claude","updated_at":"2026-05-09T08:28:49Z","dependency_count":0,"dependent_count":1,"comment_count":0} -{"_type":"issue","id":"ifcurl-19h","title":"Renderer: implement clash visibility mode","description":"In render.py _add_shape, add a clash branch between the ghost and isolate/else cases. Clash should render selected elements in a distinct red colour (define _CLASH_COLOR e.g. (220, 50, 50)) and non-selected elements in their normal material colours (same as highlight mode but for non-selected). Currently the clash value falls through to the else/isolate branch incorrectly. Update the _add_shape docstring to list clash as a supported mode. Also update the render() docstring to mention clash in the visibility parameter description.","status":"in_progress","priority":2,"issue_type":"feature","assignee":"Claude","owner":"noreply@anthropic.com","created_at":"2026-05-09T08:08:32Z","created_by":"Claude","updated_at":"2026-05-09T08:44:22Z","started_at":"2026-05-09T08:44:22Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"ifcurl-8rf","title":"Server: add /query endpoint for attribute/property retrieval","description":"Add GET and POST /query endpoints to service.py (mirroring the /select pattern). The endpoint resolves an ifc:// URL's selector, then for each matched element retrieves the attribute or property value named by the query parameter (dot-notation: Name, Pset_WallCommon.FireRating). Returns JSON {GlobalId: value, ...}, excluding elements with no GlobalId and attributes that resolve to entity references rather than scalars. Add _sandboxed_query(ifc_bytes, selector, query_path) to render_service.py and a matching _query_via_socket() delegation path. Reuse Tier 2/3 caches for IFC bytes and GUID sets. No Tier 4 caching — responses depend on the query path, not just the URL hash.","status":"open","priority":2,"issue_type":"feature","owner":"noreply@anthropic.com","created_at":"2026-05-09T08:08:29Z","created_by":"Claude","updated_at":"2026-05-09T08:08:29Z","dependency_count":0,"dependent_count":1,"comment_count":0} +{"_type":"issue","id":"ifcurl-19h","title":"Renderer: implement clash visibility mode","description":"In render.py _add_shape, add a clash branch between the ghost and isolate/else cases. Clash should render selected elements in a distinct red colour (define _CLASH_COLOR e.g. (220, 50, 50)) and non-selected elements in their normal material colours (same as highlight mode but for non-selected). Currently the clash value falls through to the else/isolate branch incorrectly. Update the _add_shape docstring to list clash as a supported mode. Also update the render() docstring to mention clash in the visibility parameter description.","status":"closed","priority":2,"issue_type":"feature","assignee":"Claude","owner":"noreply@anthropic.com","created_at":"2026-05-09T08:08:32Z","created_by":"Claude","updated_at":"2026-05-09T08:45:01Z","started_at":"2026-05-09T08:44:22Z","closed_at":"2026-05-09T08:45:01Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"ifcurl-8rf","title":"Server: add /query endpoint for attribute/property retrieval","description":"Add GET and POST /query endpoints to service.py (mirroring the /select pattern). The endpoint resolves an ifc:// URL's selector, then for each matched element retrieves the attribute or property value named by the query parameter (dot-notation: Name, Pset_WallCommon.FireRating). Returns JSON {GlobalId: value, ...}, excluding elements with no GlobalId and attributes that resolve to entity references rather than scalars. Add _sandboxed_query(ifc_bytes, selector, query_path) to render_service.py and a matching _query_via_socket() delegation path. Reuse Tier 2/3 caches for IFC bytes and GUID sets. No Tier 4 caching — responses depend on the query path, not just the URL hash.","status":"closed","priority":2,"issue_type":"feature","assignee":"Claude","owner":"noreply@anthropic.com","created_at":"2026-05-09T08:08:29Z","created_by":"Claude","updated_at":"2026-05-09T08:46:31Z","started_at":"2026-05-09T08:45:02Z","closed_at":"2026-05-09T08:46:31Z","close_reason":"Closed","dependency_count":0,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"ifcurl-b1c","title":"Implement OpenCDE BCF API on Forgejo","description":"Expose a BCF 3.0 REST API as a Python route group in the preview service, proxied at /bcf/3.0/ on the Forgejo hostname via nginx/Caddy.\n\n## Architecture\n\nPython routes added to service.py alongside the existing /preview and /bcf endpoints. No new Forgejo Go code — the service forwards the user's Authorization: Bearer header verbatim to Forgejo's REST API on localhost, so Forgejo's own auth middleware enforces per-user permissions. No service account or machine token required.\n\nBCF \u003c-\u003e ifc:// conversion stays entirely in Python: bcf.py (ifc:// -\u003e BCF, already exists) and the reverse direction implemented in ifcurl-l50.\n\n## Design\n\nThe BCF API is a stateless translation layer over Forgejo's REST API. No Forgejo recompilation, no schema changes, no new Go code.\n\n**Storage:** ifc:// URLs in issue and comment bodies are the viewpoint format. This is already what the viewer Issue button produces. The BCF\u003c-\u003eifc:// conversion is specified in SPECIFICATION.md §6.\n\n**Mapping:**\n\n| BCF resource | Forgejo |\n|---|---|\n| Project | Repository (project_id = owner/repo) |\n| Topic | Issue |\n| Topic guid | Derived from issue ID |\n| Comment | Issue comment |\n| Viewpoint | ifc:// URL found in issue/comment body |\n| Viewpoint guid | Derived from comment ID |\n| Snapshot | ifc:// URL proxied through preview service |\n\n**Auth:** BCF client sends Authorization: Bearer \u003cforgejo-token\u003e. Service forwards it to Forgejo REST API. No credential storage, no service token.\n\n**Read viewpoints:** scan issue/comment bodies for ifc:// URLs, convert each to a BCF viewpoint object on the fly via the §6 mapping.\n\n**Write viewpoints:** convert incoming BCF viewpoint to an ifc:// URL (ifcurl-l50), post as a new Forgejo comment.\n\n**Snapshot:** GET .../viewpoints/{guid}/snapshot proxies the stored ifc:// URL through the existing /preview endpoint.\n\n## Minimum viable endpoints\n\n- GET /bcf/3.0/projects\n- GET /bcf/3.0/projects/{id}\n- GET/POST /bcf/3.0/projects/{id}/topics\n- GET/PUT /bcf/3.0/projects/{id}/topics/{guid}\n- GET/POST /bcf/3.0/projects/{id}/topics/{guid}/comments\n- GET/POST /bcf/3.0/projects/{id}/topics/{guid}/viewpoints\n- GET /bcf/3.0/projects/{id}/topics/{guid}/viewpoints/{guid}\n- GET /bcf/3.0/projects/{id}/topics/{guid}/viewpoints/{guid}/snapshot\n- GET /bcf/3.0/projects/{id}/extensions (static minimal response)\n\n## Prerequisites\n\n- ifcurl-lwl: Foundation API discovery so BCF clients can locate this endpoint via OpenCDE auto-discovery\n- ifcurl-l50: BCF -\u003e ifc:// URL conversion needed for POST /viewpoints","status":"closed","priority":2,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-28T22:55:11Z","created_by":"Bruno Postle","updated_at":"2026-04-29T18:39:11Z","closed_at":"2026-04-29T18:39:11Z","close_reason":"Closed","dependencies":[{"issue_id":"ifcurl-b1c","depends_on_id":"ifcurl-l50","type":"blocks","created_at":"2026-04-29T18:44:06Z","created_by":"Claude","metadata":"{}"},{"issue_id":"ifcurl-b1c","depends_on_id":"ifcurl-lwl","type":"blocks","created_at":"2026-04-29T18:44:05Z","created_by":"Claude","metadata":"{}"}],"dependency_count":2,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"ifcurl-962","title":"forgejo: replace Go markdown extension with JS, eliminate required Go compile","description":"ifc_url.go is a Goldmark AST extension that requires patching and recompiling Forgejo to work. This is painful when dev environment (Fedora) differs from deployment (Docker) — compiler flags, static linking, build tags must all match. The goal is to make the ifc:// preview feature work with zero Go compilation, via pure JS asset deployment.\n\nPlan:\n1. Add querySelectorAll('a[href^=\"ifc://\"]') scanning to ifcurl.js — replace matched anchors with figure elements (preview image + viewer link + ifc:// URL). Goldmark already renders [title](ifc://...) as \u003ca href=\"ifc://...\"\u003etitle\u003c/a\u003e without any extension, so this covers all markdown link syntax.\n2. Change the Issue button in viewer.js to emit [view](ifc://...) markdown link syntax instead of a bare URL — so that user-generated issue bodies produce linkable anchors Goldmark will render, not plain text.\n3. The existing Go extension (ifc_url.go) and its markdown.go registration can be removed on the next Forgejo upgrade; the current patched binary continues to work in the meantime.\n\nAcceptance: after this change, deploying new ifcurl features only requires copying JS assets — no Forgejo rebuild needed.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-27T06:58:15Z","created_by":"Bruno Postle","updated_at":"2026-04-27T07:25:09Z","started_at":"2026-04-27T06:58:48Z","closed_at":"2026-04-27T07:25:09Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"ifcurl-22n","title":"viewer: add ifc:// URL and clickable image link to embedded snapshots","description":"Forgejo commit pages and pull requests embed IFC snapshots as visual diffs. Each snapshot should have two additions: (1) the ifc:// URL that addresses this exact view at the commit hash displayed as text alongside the image, and (2) the snapshot image itself should be a clickable link that opens the viewer at this view. This makes snapshots navigable and shareable.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-27T05:36:16Z","created_by":"Bruno Postle","updated_at":"2026-04-27T05:59:52Z","started_at":"2026-04-27T05:59:51Z","closed_at":"2026-04-27T05:59:52Z","close_reason":"Closed","dependencies":[{"issue_id":"ifcurl-22n","depends_on_id":"ifcurl-5a6","type":"blocks","created_at":"2026-04-27T06:37:10Z","created_by":"Claude","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} @@ -118,5 +118,5 @@ {"_type":"issue","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":"Claude","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","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":"Claude","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","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":"Claude","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} -{"_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":"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."} +{"_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/*."} From ccf692b168661c9dfe49eeb0b5a6f8fab65791af Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 08:53:51 +0000 Subject: [PATCH 4/9] Add /clash endpoint for geometry intersection detection (ifcurl-k0x) render_service.py: _sandboxed_clash(ifc_bytes, selector_a, selector_b) uses ifcopenshell.geom.tree.clash_collision_many for hard-clash detection (mesh- level interpenetration, touching excluded). When selector_b is None, tests set A against all other IfcProduct elements. Deduplicates pairs via frozenset. render_service.py: /clash endpoint wraps _sandboxed_clash in the sandbox. service.py: ClashRequest model, _clash_via_socket() for socket delegation, and GET/POST /clash endpoints. URL selector= is set A; optional selector_b body/query param is set B. Returns {"pairs": [["guidA", "guidB"], ...]}. Reuses Tier 2 byte cache. https://claude.ai/code/session_01NBsytCd6L7UQPiKbcLn4kn --- .beads/issues.jsonl | 2 +- ifcurl/render_service.py | 80 ++++++++++++++++++++++++++++++++ ifcurl/service.py | 99 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 179 insertions(+), 2 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index a3dabdc..46329d0 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -2,7 +2,7 @@ {"_type":"issue","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} {"_type":"issue","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} {"_type":"issue","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} -{"_type":"issue","id":"ifcurl-k0x","title":"Server: add /clash endpoint for geometry intersection detection","description":"Add GET and POST /clash endpoints to service.py. The endpoint accepts an ifc:// URL with two selectors (selector_a and selector_b query parameters; if selector_b is omitted, test selector_a elements against all other model elements). It runs IfcOpenShell geometry intersection detection in the sandbox: bounding-box pre-filter to find candidate pairs, then mesh-level intersection test for confirmed clashes. Returns JSON {pairs: [[guidA, guidB], ...]} listing every clashing element pair. The combined set of clashing GUIDs can then be used to construct a visibility=clash URL. Sandbox the detection via a new _sandboxed_clash(ifc_bytes, selector_a, selector_b) function in render_service.py. Results for immutable refs may be cached at Tier 4.","status":"open","priority":2,"issue_type":"feature","owner":"noreply@anthropic.com","created_at":"2026-05-09T08:28:49Z","created_by":"Claude","updated_at":"2026-05-09T08:28:49Z","dependency_count":0,"dependent_count":1,"comment_count":0} +{"_type":"issue","id":"ifcurl-k0x","title":"Server: add /clash endpoint for geometry intersection detection","description":"Add GET and POST /clash endpoints to service.py. The endpoint accepts an ifc:// URL with two selectors (selector_a and selector_b query parameters; if selector_b is omitted, test selector_a elements against all other model elements). It runs IfcOpenShell geometry intersection detection in the sandbox: bounding-box pre-filter to find candidate pairs, then mesh-level intersection test for confirmed clashes. Returns JSON {pairs: [[guidA, guidB], ...]} listing every clashing element pair. The combined set of clashing GUIDs can then be used to construct a visibility=clash URL. Sandbox the detection via a new _sandboxed_clash(ifc_bytes, selector_a, selector_b) function in render_service.py. Results for immutable refs may be cached at Tier 4.","status":"closed","priority":2,"issue_type":"feature","assignee":"Claude","owner":"noreply@anthropic.com","created_at":"2026-05-09T08:28:49Z","created_by":"Claude","updated_at":"2026-05-09T08:53:40Z","started_at":"2026-05-09T08:52:02Z","closed_at":"2026-05-09T08:53:40Z","close_reason":"Closed","dependency_count":0,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"ifcurl-19h","title":"Renderer: implement clash visibility mode","description":"In render.py _add_shape, add a clash branch between the ghost and isolate/else cases. Clash should render selected elements in a distinct red colour (define _CLASH_COLOR e.g. (220, 50, 50)) and non-selected elements in their normal material colours (same as highlight mode but for non-selected). Currently the clash value falls through to the else/isolate branch incorrectly. Update the _add_shape docstring to list clash as a supported mode. Also update the render() docstring to mention clash in the visibility parameter description.","status":"closed","priority":2,"issue_type":"feature","assignee":"Claude","owner":"noreply@anthropic.com","created_at":"2026-05-09T08:08:32Z","created_by":"Claude","updated_at":"2026-05-09T08:45:01Z","started_at":"2026-05-09T08:44:22Z","closed_at":"2026-05-09T08:45:01Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"ifcurl-8rf","title":"Server: add /query endpoint for attribute/property retrieval","description":"Add GET and POST /query endpoints to service.py (mirroring the /select pattern). The endpoint resolves an ifc:// URL's selector, then for each matched element retrieves the attribute or property value named by the query parameter (dot-notation: Name, Pset_WallCommon.FireRating). Returns JSON {GlobalId: value, ...}, excluding elements with no GlobalId and attributes that resolve to entity references rather than scalars. Add _sandboxed_query(ifc_bytes, selector, query_path) to render_service.py and a matching _query_via_socket() delegation path. Reuse Tier 2/3 caches for IFC bytes and GUID sets. No Tier 4 caching — responses depend on the query path, not just the URL hash.","status":"closed","priority":2,"issue_type":"feature","assignee":"Claude","owner":"noreply@anthropic.com","created_at":"2026-05-09T08:08:29Z","created_by":"Claude","updated_at":"2026-05-09T08:46:31Z","started_at":"2026-05-09T08:45:02Z","closed_at":"2026-05-09T08:46:31Z","close_reason":"Closed","dependency_count":0,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"ifcurl-b1c","title":"Implement OpenCDE BCF API on Forgejo","description":"Expose a BCF 3.0 REST API as a Python route group in the preview service, proxied at /bcf/3.0/ on the Forgejo hostname via nginx/Caddy.\n\n## Architecture\n\nPython routes added to service.py alongside the existing /preview and /bcf endpoints. No new Forgejo Go code — the service forwards the user's Authorization: Bearer header verbatim to Forgejo's REST API on localhost, so Forgejo's own auth middleware enforces per-user permissions. No service account or machine token required.\n\nBCF \u003c-\u003e ifc:// conversion stays entirely in Python: bcf.py (ifc:// -\u003e BCF, already exists) and the reverse direction implemented in ifcurl-l50.\n\n## Design\n\nThe BCF API is a stateless translation layer over Forgejo's REST API. No Forgejo recompilation, no schema changes, no new Go code.\n\n**Storage:** ifc:// URLs in issue and comment bodies are the viewpoint format. This is already what the viewer Issue button produces. The BCF\u003c-\u003eifc:// conversion is specified in SPECIFICATION.md §6.\n\n**Mapping:**\n\n| BCF resource | Forgejo |\n|---|---|\n| Project | Repository (project_id = owner/repo) |\n| Topic | Issue |\n| Topic guid | Derived from issue ID |\n| Comment | Issue comment |\n| Viewpoint | ifc:// URL found in issue/comment body |\n| Viewpoint guid | Derived from comment ID |\n| Snapshot | ifc:// URL proxied through preview service |\n\n**Auth:** BCF client sends Authorization: Bearer \u003cforgejo-token\u003e. Service forwards it to Forgejo REST API. No credential storage, no service token.\n\n**Read viewpoints:** scan issue/comment bodies for ifc:// URLs, convert each to a BCF viewpoint object on the fly via the §6 mapping.\n\n**Write viewpoints:** convert incoming BCF viewpoint to an ifc:// URL (ifcurl-l50), post as a new Forgejo comment.\n\n**Snapshot:** GET .../viewpoints/{guid}/snapshot proxies the stored ifc:// URL through the existing /preview endpoint.\n\n## Minimum viable endpoints\n\n- GET /bcf/3.0/projects\n- GET /bcf/3.0/projects/{id}\n- GET/POST /bcf/3.0/projects/{id}/topics\n- GET/PUT /bcf/3.0/projects/{id}/topics/{guid}\n- GET/POST /bcf/3.0/projects/{id}/topics/{guid}/comments\n- GET/POST /bcf/3.0/projects/{id}/topics/{guid}/viewpoints\n- GET /bcf/3.0/projects/{id}/topics/{guid}/viewpoints/{guid}\n- GET /bcf/3.0/projects/{id}/topics/{guid}/viewpoints/{guid}/snapshot\n- GET /bcf/3.0/projects/{id}/extensions (static minimal response)\n\n## Prerequisites\n\n- ifcurl-lwl: Foundation API discovery so BCF clients can locate this endpoint via OpenCDE auto-discovery\n- ifcurl-l50: BCF -\u003e ifc:// URL conversion needed for POST /viewpoints","status":"closed","priority":2,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-28T22:55:11Z","created_by":"Bruno Postle","updated_at":"2026-04-29T18:39:11Z","closed_at":"2026-04-29T18:39:11Z","close_reason":"Closed","dependencies":[{"issue_id":"ifcurl-b1c","depends_on_id":"ifcurl-l50","type":"blocks","created_at":"2026-04-29T18:44:06Z","created_by":"Claude","metadata":"{}"},{"issue_id":"ifcurl-b1c","depends_on_id":"ifcurl-lwl","type":"blocks","created_at":"2026-04-29T18:44:05Z","created_by":"Claude","metadata":"{}"}],"dependency_count":2,"dependent_count":0,"comment_count":0} diff --git a/ifcurl/render_service.py b/ifcurl/render_service.py index 1da74de..9edda7a 100644 --- a/ifcurl/render_service.py +++ b/ifcurl/render_service.py @@ -106,6 +106,59 @@ def _sandboxed_select(ifc_bytes: bytes, selector: str) -> list[str]: return [e.GlobalId for e in matched if hasattr(e, "GlobalId") and e.GlobalId] +def _sandboxed_clash( + ifc_bytes: bytes, selector_a: str, selector_b: str | None +) -> list[list[str]]: + """Detect geometric clashes between two sets of IFC elements. + + Uses ifcopenshell.geom.tree.clash_collision_many for mesh-level + interpenetration detection (hard clashes only, touching excluded). + Returns [[guidA, guidB], ...] for every clashing pair. Elements with no + GlobalId or no renderable geometry are silently skipped. + """ + import ifcopenshell + import ifcopenshell.geom + import ifcopenshell.util.selector + + model = ifcopenshell.file.from_string(ifc_bytes.decode()) + settings = ifcopenshell.geom.settings() + settings.set("use-world-coords", True) + + elems_a = list(ifcopenshell.util.selector.filter_elements(model, selector_a)) + ids_a = {e.id() for e in elems_a} + + if selector_b: + elems_b = list(ifcopenshell.util.selector.filter_elements(model, selector_b)) + else: + elems_b = [ + e + for e in model.by_type("IfcProduct") + if e.id() not in ids_a and getattr(e, "GlobalId", None) + ] + + if not elems_a or not elems_b: + return [] + + tree = ifcopenshell.geom.tree() + tree.add_file(model, settings) + + clashes = tree.clash_collision_many(elems_a, elems_b, allow_touching=False) + + pairs: list[list[str]] = [] + seen: set[frozenset[str]] = set() + for c in clashes: + guid_a = getattr(c.a, "GlobalId", None) + guid_b = getattr(c.b, "GlobalId", None) + if not guid_a or not guid_b: + continue + key = frozenset((guid_a, guid_b)) + if key not in seen: + seen.add(key) + pairs.append([guid_a, guid_b]) + + return pairs + + def _sandboxed_query(ifc_bytes: bytes, selector: str, query_path: str) -> dict[str, str]: """Parse model, run selector, retrieve query_path value for each element. @@ -265,6 +318,33 @@ async def select_endpoint( ) +@app.post("/clash") +async def clash_endpoint( + ifc: UploadFile = File(...), + selector_a: str = Form(...), + selector_b: str = Form(""), +) -> Response: + """Detect clashing element pairs. + + Returns JSON ``{"pairs": [["guidA", "guidB"], ...]}``. + An empty ``selector_b`` means test set A against all other model elements. + """ + ifc_bytes = await ifc.read() + + try: + pairs = run_sandboxed( + _sandboxed_clash, ifc_bytes, selector_a, selector_b or None + ) + except SandboxCrashError as exc: + raise HTTPException(status_code=422, detail=f"IFC clash detection crashed: {exc}") from exc + except SandboxTimeoutError as exc: + raise HTTPException(status_code=503, detail=f"Clash detection timed out: {exc}") from exc + except Exception as exc: + raise HTTPException(status_code=422, detail=f"Clash detection failed: {exc}") from exc + + return Response(content=json.dumps({"pairs": pairs}), media_type="application/json") + + @app.post("/query") async def query_endpoint( ifc: UploadFile = File(...), diff --git a/ifcurl/service.py b/ifcurl/service.py index e561058..960af24 100644 --- a/ifcurl/service.py +++ b/ifcurl/service.py @@ -56,7 +56,7 @@ from ifcurl.bcf_api import router as bcf_router from ifcurl.documents_api import router as documents_router from ifcurl.git import diff_text as git_diff_text from ifcurl.git import fetch_ifc -from ifcurl.render_service import _sandboxed_diff, _sandboxed_pipeline, _sandboxed_query, _sandboxed_select +from ifcurl.render_service import _sandboxed_clash, _sandboxed_diff, _sandboxed_pipeline, _sandboxed_query, _sandboxed_select from ifcurl.sandbox import SandboxCrashError, SandboxTimeoutError, run_sandboxed from ifcurl.url import IfcUrl @@ -126,6 +126,26 @@ def _select_via_socket(ifc_bytes: bytes, selector: str) -> list[str]: return resp.json()["guids"] +def _clash_via_socket( + ifc_bytes: bytes, selector_a: str, selector_b: str | None +) -> list[list[str]]: + """Delegate clash detection to the render service over the Unix socket.""" + import httpx + + try: + with httpx.Client(transport=httpx.HTTPTransport(uds=_RENDER_SOCKET)) as client: + resp = client.post( + "http://render/clash", + files={"ifc": ifc_bytes}, + data={"selector_a": selector_a, "selector_b": selector_b or ""}, + timeout=_RENDER_TIMEOUT, + ) + except httpx.TransportError as exc: + raise HTTPException(status_code=503, detail=f"Render service unavailable: {exc}") from exc + _check_render_response(resp) + return resp.json()["pairs"] + + def _query_via_socket(ifc_bytes: bytes, selector: str, query_path: str) -> dict[str, str]: """Delegate query execution to the render service over the Unix socket.""" import httpx @@ -428,6 +448,13 @@ class QueryRequest(BaseModel): token: str | None = None +class ClashRequest(BaseModel): + url: str + selector_b: str | None = None + """Second element set to test against. When omitted, set A is tested against all other model elements.""" + token: str | None = None + + class BcfRequest(BaseModel): url: str title: str = "IFC View" @@ -801,6 +828,76 @@ def query(request: QueryRequest) -> JSONResponse: return JSONResponse(result) +@app.get("/clash") +def clash_get(url: str, selector_b: str | None = None, token: str | None = None) -> JSONResponse: + """GET variant of POST /clash.""" + return clash(ClashRequest(url=url, selector_b=selector_b, token=token)) + + +@app.post("/clash") +def clash(request: ClashRequest) -> JSONResponse: + """Detect geometric clashes between two sets of elements. + + Set A is defined by the URL's ``selector=`` parameter. Set B is defined + by ``selector_b``; when omitted, set A is tested against all other + model elements. + + Uses ifcopenshell hard-clash (interpenetration) detection. Returns + ``{"pairs": [["", ""], …]}``. + """ + try: + ifc_url = IfcUrl.parse(request.url) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + if ifc_url.path is None: + raise HTTPException(status_code=400, detail="URL has no 'path' parameter") + if not ifc_url.selector: + raise HTTPException(status_code=400, detail="URL has no 'selector' parameter") + + _ssrf_check(ifc_url) + + token = request.token + if token is None and ifc_url.host: + token = get_token_for_host(ifc_url.host) + + try: + hexsha, ifc_bytes, _ = fetch_ifc(ifc_url, token=token) + except ImportError as exc: + raise HTTPException(status_code=503, detail=str(exc)) from exc + except ValueError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + + _check_ifc_size(ifc_bytes) + + cached_bytes = _t2_get(hexsha, ifc_url.path) + ifc_bytes = cached_bytes if cached_bytes is not None else ifc_bytes + if cached_bytes is None: + _t2_put(hexsha, ifc_url.path, ifc_bytes) + + try: + if _RENDER_SOCKET: + pairs = _clash_via_socket(ifc_bytes, ifc_url.selector, request.selector_b) + else: + pairs = run_sandboxed( + _sandboxed_clash, ifc_bytes, ifc_url.selector, request.selector_b + ) + except SandboxCrashError as exc: + raise HTTPException( + status_code=422, detail=f"IFC clash detection crashed: {exc}" + ) from exc + except SandboxTimeoutError as exc: + raise HTTPException( + status_code=503, detail=f"Clash detection timed out: {exc}" + ) from exc + except Exception as exc: + raise HTTPException( + status_code=422, detail=f"Clash detection failed: {exc}" + ) from exc + + return JSONResponse({"pairs": pairs}) + + @app.get("/foundation/versions") def foundation_versions(request: Request) -> JSONResponse: """OpenCDE Foundation API discovery endpoint. From 6ffc4a2b2967f0e562c896eca317efb732b14b27 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 08:57:54 +0000 Subject: [PATCH 5/9] Simplify /clash: single selector, detect collisions within selected set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop selector_b — the endpoint now takes the URL's selector= and tests all matched elements against each other, consistent with the spec's model. Self-pairs and duplicates are filtered; requires at least 2 matched elements. https://claude.ai/code/session_01NBsytCd6L7UQPiKbcLn4kn --- ifcurl/render_service.py | 35 +++++++++-------------------------- ifcurl/service.py | 29 ++++++++++------------------- 2 files changed, 19 insertions(+), 45 deletions(-) diff --git a/ifcurl/render_service.py b/ifcurl/render_service.py index 9edda7a..ae8b08e 100644 --- a/ifcurl/render_service.py +++ b/ifcurl/render_service.py @@ -106,10 +106,8 @@ def _sandboxed_select(ifc_bytes: bytes, selector: str) -> list[str]: return [e.GlobalId for e in matched if hasattr(e, "GlobalId") and e.GlobalId] -def _sandboxed_clash( - ifc_bytes: bytes, selector_a: str, selector_b: str | None -) -> list[list[str]]: - """Detect geometric clashes between two sets of IFC elements. +def _sandboxed_clash(ifc_bytes: bytes, selector: str) -> list[list[str]]: + """Detect geometric clashes within the elements matched by selector. Uses ifcopenshell.geom.tree.clash_collision_many for mesh-level interpenetration detection (hard clashes only, touching excluded). @@ -124,32 +122,21 @@ def _sandboxed_clash( settings = ifcopenshell.geom.settings() settings.set("use-world-coords", True) - elems_a = list(ifcopenshell.util.selector.filter_elements(model, selector_a)) - ids_a = {e.id() for e in elems_a} - - if selector_b: - elems_b = list(ifcopenshell.util.selector.filter_elements(model, selector_b)) - else: - elems_b = [ - e - for e in model.by_type("IfcProduct") - if e.id() not in ids_a and getattr(e, "GlobalId", None) - ] - - if not elems_a or not elems_b: + elems = list(ifcopenshell.util.selector.filter_elements(model, selector)) + if len(elems) < 2: return [] tree = ifcopenshell.geom.tree() tree.add_file(model, settings) - clashes = tree.clash_collision_many(elems_a, elems_b, allow_touching=False) + clashes = tree.clash_collision_many(elems, elems, allow_touching=False) pairs: list[list[str]] = [] seen: set[frozenset[str]] = set() for c in clashes: guid_a = getattr(c.a, "GlobalId", None) guid_b = getattr(c.b, "GlobalId", None) - if not guid_a or not guid_b: + if not guid_a or not guid_b or guid_a == guid_b: continue key = frozenset((guid_a, guid_b)) if key not in seen: @@ -321,20 +308,16 @@ async def select_endpoint( @app.post("/clash") async def clash_endpoint( ifc: UploadFile = File(...), - selector_a: str = Form(...), - selector_b: str = Form(""), + selector: str = Form(...), ) -> Response: - """Detect clashing element pairs. + """Detect clashing element pairs within the selected set. Returns JSON ``{"pairs": [["guidA", "guidB"], ...]}``. - An empty ``selector_b`` means test set A against all other model elements. """ ifc_bytes = await ifc.read() try: - pairs = run_sandboxed( - _sandboxed_clash, ifc_bytes, selector_a, selector_b or None - ) + pairs = run_sandboxed(_sandboxed_clash, ifc_bytes, selector) except SandboxCrashError as exc: raise HTTPException(status_code=422, detail=f"IFC clash detection crashed: {exc}") from exc except SandboxTimeoutError as exc: diff --git a/ifcurl/service.py b/ifcurl/service.py index 960af24..e99a892 100644 --- a/ifcurl/service.py +++ b/ifcurl/service.py @@ -126,9 +126,7 @@ def _select_via_socket(ifc_bytes: bytes, selector: str) -> list[str]: return resp.json()["guids"] -def _clash_via_socket( - ifc_bytes: bytes, selector_a: str, selector_b: str | None -) -> list[list[str]]: +def _clash_via_socket(ifc_bytes: bytes, selector: str) -> list[list[str]]: """Delegate clash detection to the render service over the Unix socket.""" import httpx @@ -137,7 +135,7 @@ def _clash_via_socket( resp = client.post( "http://render/clash", files={"ifc": ifc_bytes}, - data={"selector_a": selector_a, "selector_b": selector_b or ""}, + data={"selector": selector}, timeout=_RENDER_TIMEOUT, ) except httpx.TransportError as exc: @@ -450,8 +448,6 @@ class QueryRequest(BaseModel): class ClashRequest(BaseModel): url: str - selector_b: str | None = None - """Second element set to test against. When omitted, set A is tested against all other model elements.""" token: str | None = None @@ -829,21 +825,18 @@ def query(request: QueryRequest) -> JSONResponse: @app.get("/clash") -def clash_get(url: str, selector_b: str | None = None, token: str | None = None) -> JSONResponse: +def clash_get(url: str, token: str | None = None) -> JSONResponse: """GET variant of POST /clash.""" - return clash(ClashRequest(url=url, selector_b=selector_b, token=token)) + return clash(ClashRequest(url=url, token=token)) @app.post("/clash") def clash(request: ClashRequest) -> JSONResponse: - """Detect geometric clashes between two sets of elements. + """Detect geometric clashes within the elements matched by the selector. - Set A is defined by the URL's ``selector=`` parameter. Set B is defined - by ``selector_b``; when omitted, set A is tested against all other - model elements. - - Uses ifcopenshell hard-clash (interpenetration) detection. Returns - ``{"pairs": [["", ""], …]}``. + The URL's ``selector=`` parameter defines the set of elements to test + against each other. Uses ifcopenshell hard-clash (interpenetration) + detection. Returns ``{"pairs": [["", ""], …]}``. """ try: ifc_url = IfcUrl.parse(request.url) @@ -877,11 +870,9 @@ def clash(request: ClashRequest) -> JSONResponse: try: if _RENDER_SOCKET: - pairs = _clash_via_socket(ifc_bytes, ifc_url.selector, request.selector_b) + pairs = _clash_via_socket(ifc_bytes, ifc_url.selector) else: - pairs = run_sandboxed( - _sandboxed_clash, ifc_bytes, ifc_url.selector, request.selector_b - ) + pairs = run_sandboxed(_sandboxed_clash, ifc_bytes, ifc_url.selector) except SandboxCrashError as exc: raise HTTPException( status_code=422, detail=f"IFC clash detection crashed: {exc}" From 8d13757c4d81add1d05340704fff8070825f716e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 09:03:11 +0000 Subject: [PATCH 6/9] Viewer: add clash visibility mode (ifcurl-982) viewer.html: add 'clash' option to the visibility diff --git a/forgejo/custom/public/assets/viewer.js b/forgejo/custom/public/assets/viewer.js index b33edfb..049f82f 100644 --- a/forgejo/custom/public/assets/viewer.js +++ b/forgejo/custom/public/assets/viewer.js @@ -501,6 +501,8 @@ async function applySelector(components, model, selectorStr, srcUrl, visibility new THREE.Color(0x888888), 0.15); } else if (visibility === "isolate") { await hider.isolate(matching); + } else if (visibility === "clash") { + await applyFragmentStyle(components, matching, new THREE.Color(0xdc3232), 1); } else { // highlight (default): colour-overlay the selected elements, all others visible. await applyFragmentStyle(components, matching, new THREE.Color(0xff8800), 1); @@ -536,6 +538,8 @@ async function applySelector(components, model, selectorStr, srcUrl, visibility new THREE.Color(0x888888), 0.15); } else if (visibility === "isolate") { await hider.isolate(matchingMap); + } else if (visibility === "clash") { + await applyFragmentStyle(components, matchingMap, new THREE.Color(0xdc3232), 1); } else { await applyFragmentStyle(components, matchingMap, new THREE.Color(0xff8800), 1); } From ea0ad56bda12ceaad2e896c536af0db4903973cd Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 09:26:34 +0000 Subject: [PATCH 7/9] Update beads state https://claude.ai/code/session_01NBsytCd6L7UQPiKbcLn4kn --- .beads/issues.jsonl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index c429ff0..5c72589 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -49,7 +49,7 @@ {"_type":"issue","id":"ifcurl-ei8","title":"Fix syncCameraUrl: convert WebGL coords to IFC world space","description":"syncCameraUrl in viewer.py currently records camera.position/direction/up directly from Three.js, which are in WebGL/Three.js coordinate space (Y-up, different scale/orientation than IFC). The ifc:// URL spec expects IFC world coordinates (Z-up, matching BCF viewpoint conventions). Need to find the IFC→Three.js transform that @thatopen/components applies when loading a model, then apply its inverse in syncCameraUrl before serialising the camera params. Repos cloned for reference: ~/src/engine_components and ~/src/engine_fragment.","status":"closed","priority":2,"issue_type":"bug","owner":"bruno@postle.net","created_at":"2026-04-23T05:53:06Z","created_by":"Bruno Postle","updated_at":"2026-04-23T06:14:36Z","closed_at":"2026-04-23T06:14:36Z","close_reason":"Split into atomic tasks: ifcurl-ldk, ifcurl-j2s, ifcurl-cmd","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"ifcurl-i1s","title":"Test Phase 3b web viewer end-to-end","description":"After deploying the footer template (ifcurl-ach), test the full flow: open an .ifc file in Forgejo at localhost:3000, click 'View in 3D', verify the viewer loads the model. CDN versions (@thatopen/components 2.4.0, three 0.160.0, web-ifc 0.0.66) may need adjustment if the API has changed.","status":"closed","priority":2,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-23T05:48:01Z","created_by":"Bruno Postle","updated_at":"2026-04-23T20:05:04Z","started_at":"2026-04-23T19:54:56Z","closed_at":"2026-04-23T20:05:04Z","close_reason":"Closed","dependencies":[{"issue_id":"ifcurl-i1s","depends_on_id":"ifcurl-49o","type":"blocks","created_at":"2026-04-23T07:14:12Z","created_by":"Claude","metadata":"{}"},{"issue_id":"ifcurl-i1s","depends_on_id":"ifcurl-4yu","type":"blocks","created_at":"2026-04-23T07:14:11Z","created_by":"Claude","metadata":"{}"},{"issue_id":"ifcurl-i1s","depends_on_id":"ifcurl-ach","type":"blocks","created_at":"2026-04-23T06:48:10Z","created_by":"Claude","metadata":"{}"},{"issue_id":"ifcurl-i1s","depends_on_id":"ifcurl-ahd","type":"blocks","created_at":"2026-04-23T07:02:54Z","created_by":"Claude","metadata":"{}"}],"dependency_count":4,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"ifcurl-ach","title":"Deploy Phase 3b footer template to Forgejo","description":"The footer.tmpl file that injects a 'View in 3D' button on .ifc file pages has been written but not yet deployed. Deploy it to /etc/forgejo/templates/custom/footer.tmpl and verify the button appears when viewing an .ifc file on localhost:3000.","status":"closed","priority":2,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-04-23T05:47:53Z","created_by":"Bruno Postle","updated_at":"2026-04-23T06:14:35Z","closed_at":"2026-04-23T06:14:35Z","close_reason":"Split into atomic tasks: ifcurl-ohq, ifcurl-49o","dependencies":[{"issue_id":"ifcurl-ach","depends_on_id":"ifcurl-ahd","type":"blocks","created_at":"2026-04-23T07:02:40Z","created_by":"Claude","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} -{"_type":"issue","id":"ifcurl-982","title":"Viewer: implement clash visibility mode display","description":"Implement visibility=clash in the Bonsai viewer. Selected elements (GUIDs pre-identified as clashing, typically sourced from the server /clash endpoint) should be displayed with a solid red clash-indicator style (full opacity, distinct red material) while all other elements render normally — equivalent to highlight mode but using red rather than orange for selected elements. The viewer should be able to construct ifc://...\u0026visibility=clash URLs from a set of GUID pairs returned by /clash. When exporting to BCF, clash is treated as highlight (BCF has no equivalent distinction).","status":"in_progress","priority":3,"issue_type":"feature","assignee":"Claude","owner":"noreply@anthropic.com","created_at":"2026-05-09T08:08:45Z","created_by":"Claude","updated_at":"2026-05-09T09:02:27Z","started_at":"2026-05-09T09:02:27Z","dependencies":[{"issue_id":"ifcurl-982","depends_on_id":"ifcurl-k0x","type":"blocks","created_at":"2026-05-09T08:28:57Z","created_by":"Claude","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"ifcurl-982","title":"Viewer: implement clash visibility mode display","description":"Implement visibility=clash in the Bonsai viewer. Selected elements (GUIDs pre-identified as clashing, typically sourced from the server /clash endpoint) should be displayed with a solid red clash-indicator style (full opacity, distinct red material) while all other elements render normally — equivalent to highlight mode but using red rather than orange for selected elements. The viewer should be able to construct ifc://...\u0026visibility=clash URLs from a set of GUID pairs returned by /clash. When exporting to BCF, clash is treated as highlight (BCF has no equivalent distinction).","status":"closed","priority":3,"issue_type":"feature","assignee":"Claude","owner":"noreply@anthropic.com","created_at":"2026-05-09T08:08:45Z","created_by":"Claude","updated_at":"2026-05-09T09:03:00Z","started_at":"2026-05-09T09:02:27Z","closed_at":"2026-05-09T09:03:00Z","close_reason":"Closed","dependencies":[{"issue_id":"ifcurl-982","depends_on_id":"ifcurl-k0x","type":"blocks","created_at":"2026-05-09T08:28:57Z","created_by":"Claude","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"ifcurl-4u3","title":"Viewer: query creation and editing UI","description":"Add UI controls in the Bonsai viewer for composing and modifying queries. Users need: a text field for the selector expression, a text field for the query path (dot-notation, e.g. Pset_WallCommon.FireRating), and an Apply button that updates the active ifc:// URL and triggers a re-query. The UI should also provide a copy-URL button. Changing the selector or query fields should update the URL in the address bar and the displayed 3D view/table without requiring a full page reload.","status":"open","priority":3,"issue_type":"feature","owner":"noreply@anthropic.com","created_at":"2026-05-09T08:08:42Z","created_by":"Claude","updated_at":"2026-05-09T08:08:42Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"ifcurl-2iv","title":"Viewer: display query results in a table panel","description":"When an ifc:// URL contains a query parameter, the viewer (Bonsai) should call the preview service /query endpoint and display the returned {GlobalId: value} mapping in a table panel alongside the 3D view. The table should have columns for element GlobalId (or Name if resolvable from the model) and the queried value. When both query and camera/visibility parameters are present, show the 3D view and property table together. The table should update whenever the URL's selector or query changes.","status":"open","priority":3,"issue_type":"feature","owner":"noreply@anthropic.com","created_at":"2026-05-09T08:08:39Z","created_by":"Claude","updated_at":"2026-05-09T08:08:39Z","dependencies":[{"issue_id":"ifcurl-2iv","depends_on_id":"ifcurl-8rf","type":"blocks","created_at":"2026-05-09T08:08:49Z","created_by":"Claude","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"ifcurl-xfi","title":"Implement OpenCDE document picker (select-documents flow)","description":"The OpenCDE Documents API defines a browser-redirect flow for document discovery. Implement POST /documents/1.0/select-documents which returns a select_documents_url. That URL serves a small HTML page: owner/repo selector then file-tree browser. When user selects an IFC file, generate the document_id and redirect to the callback_url with document_ids[] params.","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-29T20:38:16Z","created_by":"Bruno Postle","updated_at":"2026-04-29T20:38:16Z","dependency_count":0,"dependent_count":0,"comment_count":0} @@ -118,5 +118,5 @@ {"_type":"issue","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":"Claude","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","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":"Claude","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","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":"Claude","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} -{"_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":"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."} +{"_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/*."} From 38161604f9d485f3ba71a4817e0088a2cc03f621 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 11:09:55 +0000 Subject: [PATCH 8/9] Viewer: query field and results table (ifcurl-2iv, ifcurl-4u3) viewer-url.js: parseIfcUrl returns query field; buildIfcUrl gains optional fifth query param, included in the QS when non-empty. viewer.html: add 'query' text field (label + input) in toolbar row 2 after the visibility selector. Add #query-panel (fixed bottom-centre) with a scrollable table showing GlobalId | value rows, a header displaying the query path, and a close button. viewer.js: populate queryInput from parsed URL on load; include query in buildIfcUrl() wrapper; wire queryInput into Enter-key and focus/blur listeners. Add applyQuery(srcUrl, queryPath) which calls GET /query, populates the table, and shows the panel. Called from main() after applySelector when both selector and query params are present. viewer-url.test.js: add tests for parseIfcUrl query field, buildIfcUrl with query, and buildIfcUrl omitting empty query. https://claude.ai/code/session_01NBsytCd6L7UQPiKbcLn4kn --- .beads/issues.jsonl | 6 +-- forgejo/custom/public/assets/viewer-url.js | 4 +- forgejo/custom/public/assets/viewer.html | 44 ++++++++++++++++ forgejo/custom/public/assets/viewer.js | 60 ++++++++++++++++++++-- forgejo/tests/viewer-url.test.js | 16 ++++++ 5 files changed, 123 insertions(+), 7 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 5c72589..18e6968 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -50,8 +50,8 @@ {"_type":"issue","id":"ifcurl-i1s","title":"Test Phase 3b web viewer end-to-end","description":"After deploying the footer template (ifcurl-ach), test the full flow: open an .ifc file in Forgejo at localhost:3000, click 'View in 3D', verify the viewer loads the model. CDN versions (@thatopen/components 2.4.0, three 0.160.0, web-ifc 0.0.66) may need adjustment if the API has changed.","status":"closed","priority":2,"issue_type":"task","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-23T05:48:01Z","created_by":"Bruno Postle","updated_at":"2026-04-23T20:05:04Z","started_at":"2026-04-23T19:54:56Z","closed_at":"2026-04-23T20:05:04Z","close_reason":"Closed","dependencies":[{"issue_id":"ifcurl-i1s","depends_on_id":"ifcurl-49o","type":"blocks","created_at":"2026-04-23T07:14:12Z","created_by":"Claude","metadata":"{}"},{"issue_id":"ifcurl-i1s","depends_on_id":"ifcurl-4yu","type":"blocks","created_at":"2026-04-23T07:14:11Z","created_by":"Claude","metadata":"{}"},{"issue_id":"ifcurl-i1s","depends_on_id":"ifcurl-ach","type":"blocks","created_at":"2026-04-23T06:48:10Z","created_by":"Claude","metadata":"{}"},{"issue_id":"ifcurl-i1s","depends_on_id":"ifcurl-ahd","type":"blocks","created_at":"2026-04-23T07:02:54Z","created_by":"Claude","metadata":"{}"}],"dependency_count":4,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"ifcurl-ach","title":"Deploy Phase 3b footer template to Forgejo","description":"The footer.tmpl file that injects a 'View in 3D' button on .ifc file pages has been written but not yet deployed. Deploy it to /etc/forgejo/templates/custom/footer.tmpl and verify the button appears when viewing an .ifc file on localhost:3000.","status":"closed","priority":2,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-04-23T05:47:53Z","created_by":"Bruno Postle","updated_at":"2026-04-23T06:14:35Z","closed_at":"2026-04-23T06:14:35Z","close_reason":"Split into atomic tasks: ifcurl-ohq, ifcurl-49o","dependencies":[{"issue_id":"ifcurl-ach","depends_on_id":"ifcurl-ahd","type":"blocks","created_at":"2026-04-23T07:02:40Z","created_by":"Claude","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"ifcurl-982","title":"Viewer: implement clash visibility mode display","description":"Implement visibility=clash in the Bonsai viewer. Selected elements (GUIDs pre-identified as clashing, typically sourced from the server /clash endpoint) should be displayed with a solid red clash-indicator style (full opacity, distinct red material) while all other elements render normally — equivalent to highlight mode but using red rather than orange for selected elements. The viewer should be able to construct ifc://...\u0026visibility=clash URLs from a set of GUID pairs returned by /clash. When exporting to BCF, clash is treated as highlight (BCF has no equivalent distinction).","status":"closed","priority":3,"issue_type":"feature","assignee":"Claude","owner":"noreply@anthropic.com","created_at":"2026-05-09T08:08:45Z","created_by":"Claude","updated_at":"2026-05-09T09:03:00Z","started_at":"2026-05-09T09:02:27Z","closed_at":"2026-05-09T09:03:00Z","close_reason":"Closed","dependencies":[{"issue_id":"ifcurl-982","depends_on_id":"ifcurl-k0x","type":"blocks","created_at":"2026-05-09T08:28:57Z","created_by":"Claude","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"ifcurl-4u3","title":"Viewer: query creation and editing UI","description":"Add UI controls in the Bonsai viewer for composing and modifying queries. Users need: a text field for the selector expression, a text field for the query path (dot-notation, e.g. Pset_WallCommon.FireRating), and an Apply button that updates the active ifc:// URL and triggers a re-query. The UI should also provide a copy-URL button. Changing the selector or query fields should update the URL in the address bar and the displayed 3D view/table without requiring a full page reload.","status":"open","priority":3,"issue_type":"feature","owner":"noreply@anthropic.com","created_at":"2026-05-09T08:08:42Z","created_by":"Claude","updated_at":"2026-05-09T08:08:42Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"ifcurl-2iv","title":"Viewer: display query results in a table panel","description":"When an ifc:// URL contains a query parameter, the viewer (Bonsai) should call the preview service /query endpoint and display the returned {GlobalId: value} mapping in a table panel alongside the 3D view. The table should have columns for element GlobalId (or Name if resolvable from the model) and the queried value. When both query and camera/visibility parameters are present, show the 3D view and property table together. The table should update whenever the URL's selector or query changes.","status":"open","priority":3,"issue_type":"feature","owner":"noreply@anthropic.com","created_at":"2026-05-09T08:08:39Z","created_by":"Claude","updated_at":"2026-05-09T08:08:39Z","dependencies":[{"issue_id":"ifcurl-2iv","depends_on_id":"ifcurl-8rf","type":"blocks","created_at":"2026-05-09T08:08:49Z","created_by":"Claude","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"ifcurl-4u3","title":"Viewer: query creation and editing UI","description":"Add UI controls in the Bonsai viewer for composing and modifying queries. Users need: a text field for the selector expression, a text field for the query path (dot-notation, e.g. Pset_WallCommon.FireRating), and an Apply button that updates the active ifc:// URL and triggers a re-query. The UI should also provide a copy-URL button. Changing the selector or query fields should update the URL in the address bar and the displayed 3D view/table without requiring a full page reload.","status":"closed","priority":3,"issue_type":"feature","assignee":"Claude","owner":"noreply@anthropic.com","created_at":"2026-05-09T08:08:42Z","created_by":"Claude","updated_at":"2026-05-09T11:09:43Z","started_at":"2026-05-09T11:06:39Z","closed_at":"2026-05-09T11:09:43Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"issue","id":"ifcurl-2iv","title":"Viewer: display query results in a table panel","description":"When an ifc:// URL contains a query parameter, the viewer (Bonsai) should call the preview service /query endpoint and display the returned {GlobalId: value} mapping in a table panel alongside the 3D view. The table should have columns for element GlobalId (or Name if resolvable from the model) and the queried value. When both query and camera/visibility parameters are present, show the 3D view and property table together. The table should update whenever the URL's selector or query changes.","status":"closed","priority":3,"issue_type":"feature","assignee":"Claude","owner":"noreply@anthropic.com","created_at":"2026-05-09T08:08:39Z","created_by":"Claude","updated_at":"2026-05-09T11:09:43Z","started_at":"2026-05-09T11:06:38Z","closed_at":"2026-05-09T11:09:43Z","close_reason":"Closed","dependencies":[{"issue_id":"ifcurl-2iv","depends_on_id":"ifcurl-8rf","type":"blocks","created_at":"2026-05-09T08:08:49Z","created_by":"Claude","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"ifcurl-xfi","title":"Implement OpenCDE document picker (select-documents flow)","description":"The OpenCDE Documents API defines a browser-redirect flow for document discovery. Implement POST /documents/1.0/select-documents which returns a select_documents_url. That URL serves a small HTML page: owner/repo selector then file-tree browser. When user selects an IFC file, generate the document_id and redirect to the callback_url with document_ids[] params.","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-29T20:38:16Z","created_by":"Bruno Postle","updated_at":"2026-04-29T20:38:16Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"ifcurl-bil","title":"Implement OpenCDE OAuth2 proxy routes","description":"The OpenCDE Foundation API requires OAuth2 endpoints. Forgejo already has OAuth2 support — proxy through to it. Routes needed: GET /foundation/1.0/oauth2/auth_url (return Forgejo OAuth2 auth URL), POST /foundation/1.0/oauth2/token (proxy code exchange to Forgejo token endpoint), POST /foundation/1.0/oauth2/token_refresh (proxy refresh). Requires a one-time step of registering an OAuth2 app in Forgejo admin and storing client_id/client_secret as env vars.","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-29T20:38:15Z","created_by":"Bruno Postle","updated_at":"2026-04-29T20:38:15Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"ifcurl-k3v","title":"Add GET /documents/1.0/document-metadata/{document_id}","description":"The OpenCDE Documents API spec includes a GET endpoint for individual file metadata. Implement it: decode the document_id, fetch the file's latest info from Forgejo (name, size, last-modified), return as a metadata response.","status":"closed","priority":3,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-29T20:38:13Z","created_by":"Bruno Postle","updated_at":"2026-04-29T21:25:07Z","started_at":"2026-04-29T21:22:34Z","closed_at":"2026-04-29T21:25:07Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} @@ -118,5 +118,5 @@ {"_type":"issue","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":"Claude","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","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":"Claude","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","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":"Claude","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} -{"_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."} {"_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":"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-url.js b/forgejo/custom/public/assets/viewer-url.js index 920fc4a..75a3bb2 100644 --- a/forgejo/custom/public/assets/viewer-url.js +++ b/forgejo/custom/public/assets/viewer-url.js @@ -28,17 +28,19 @@ export function parseIfcUrl(raw) { host, userAt, repoSuffix, ref, path: qs.get("path") ?? "", selector: qs.get("selector") ?? "", + query: qs.get("query") ?? "", camera: qs.get("camera") ?? "", fov: qs.get("fov") ?? "", scale: qs.get("scale") ?? "", }; } -export function buildIfcUrl(repo, ref, path, selector) { +export function buildIfcUrl(repo, ref, path, selector, query) { if (!repo || !ref) return null; const qs = new URLSearchParams(); if (path) qs.set("path", path); if (selector) qs.set("selector", selector); + if (query) qs.set("query", query); const qsStr = qs.toString().replace(/%2C/g, ",").replace(/%2B/g, "+").replace(/%24/g, "$"); return `ifc://${repo}@${ref}${qsStr ? "?" + qsStr : ""}`; } diff --git a/forgejo/custom/public/assets/viewer.html b/forgejo/custom/public/assets/viewer.html index 4d96cb9..0f4b334 100644 --- a/forgejo/custom/public/assets/viewer.html +++ b/forgejo/custom/public/assets/viewer.html @@ -111,6 +111,33 @@ } .meta-row.storey-row.active .meta-name { color: #a8c8ff; } .meta-count { font-size: 10px; color: #808088; flex-shrink: 0; } + /* Query results panel */ + #query-panel { + position: fixed; bottom: 44px; left: 50%; transform: translateX(-50%); + z-index: 10; + background: rgba(28,28,30,0.92); backdrop-filter: blur(8px); + border: 1px solid rgba(255,255,255,0.12); border-radius: 6px; + padding: 8px 10px; min-width: 320px; max-width: 640px; + max-height: 200px; overflow-y: auto; + } + #query-panel-header { + display: flex; align-items: center; justify-content: space-between; + margin-bottom: 6px; + } + #query-panel-header span { + font-size: 10px; color: #606068; + text-transform: uppercase; letter-spacing: 0.06em; + } + #query-table { width: 100%; border-collapse: collapse; font-size: 11px; } + #query-table th { + text-align: left; color: #808088; font-weight: normal; + padding: 1px 6px 4px 0; border-bottom: 1px solid rgba(255,255,255,0.08); + } + #query-table td { + padding: 2px 6px 2px 0; color: #f0f0f0; font-family: monospace; + border-bottom: 1px solid rgba(255,255,255,0.05); + max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + } /* Element properties panel */ #props-panel { position: fixed; bottom: 44px; right: 12px; z-index: 10; @@ -194,6 +221,13 @@ +
+ + +
+