mirror of
https://github.com/brunopostle/ifcurl.git
synced 2026-07-12 02:08:13 +00:00
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
This commit is contained in:
parent
ea0ad56bda
commit
38161604f9
5 changed files with 123 additions and 7 deletions
|
|
@ -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-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-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-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-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":"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-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-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-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}
|
{"_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-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-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":"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":"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."}
|
||||||
|
|
|
||||||
|
|
@ -28,17 +28,19 @@ export function parseIfcUrl(raw) {
|
||||||
host, userAt, repoSuffix, ref,
|
host, userAt, repoSuffix, ref,
|
||||||
path: qs.get("path") ?? "",
|
path: qs.get("path") ?? "",
|
||||||
selector: qs.get("selector") ?? "",
|
selector: qs.get("selector") ?? "",
|
||||||
|
query: qs.get("query") ?? "",
|
||||||
camera: qs.get("camera") ?? "",
|
camera: qs.get("camera") ?? "",
|
||||||
fov: qs.get("fov") ?? "",
|
fov: qs.get("fov") ?? "",
|
||||||
scale: qs.get("scale") ?? "",
|
scale: qs.get("scale") ?? "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildIfcUrl(repo, ref, path, selector) {
|
export function buildIfcUrl(repo, ref, path, selector, query) {
|
||||||
if (!repo || !ref) return null;
|
if (!repo || !ref) return null;
|
||||||
const qs = new URLSearchParams();
|
const qs = new URLSearchParams();
|
||||||
if (path) qs.set("path", path);
|
if (path) qs.set("path", path);
|
||||||
if (selector) qs.set("selector", selector);
|
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, "$");
|
const qsStr = qs.toString().replace(/%2C/g, ",").replace(/%2B/g, "+").replace(/%24/g, "$");
|
||||||
return `ifc://${repo}@${ref}${qsStr ? "?" + qsStr : ""}`;
|
return `ifc://${repo}@${ref}${qsStr ? "?" + qsStr : ""}`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,33 @@
|
||||||
}
|
}
|
||||||
.meta-row.storey-row.active .meta-name { color: #a8c8ff; }
|
.meta-row.storey-row.active .meta-name { color: #a8c8ff; }
|
||||||
.meta-count { font-size: 10px; color: #808088; flex-shrink: 0; }
|
.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 */
|
/* Element properties panel */
|
||||||
#props-panel {
|
#props-panel {
|
||||||
position: fixed; bottom: 44px; right: 12px; z-index: 10;
|
position: fixed; bottom: 44px; right: 12px; z-index: 10;
|
||||||
|
|
@ -194,6 +221,13 @@
|
||||||
<option value="clash">clash</option>
|
<option value="clash">clash</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field" style="flex:2">
|
||||||
|
<label>query</label>
|
||||||
|
<input id="query-input" class="ti" type="text"
|
||||||
|
placeholder="Name or Pset.Property"
|
||||||
|
title="Attribute or property set value to retrieve for each selected element"
|
||||||
|
spellcheck="false">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Row 3: BCF export/import form (hidden until BCF button clicked) -->
|
<!-- Row 3: BCF export/import form (hidden until BCF button clicked) -->
|
||||||
<div id="bcf-row" class="trow" style="display:none">
|
<div id="bcf-row" class="trow" style="display:none">
|
||||||
|
|
@ -213,6 +247,16 @@
|
||||||
</div>
|
</div>
|
||||||
<div id="viewer"></div>
|
<div id="viewer"></div>
|
||||||
<div id="meta-panel" style="display:none"></div>
|
<div id="meta-panel" style="display:none"></div>
|
||||||
|
<div id="query-panel" style="display:none">
|
||||||
|
<div id="query-panel-header">
|
||||||
|
<span id="query-panel-title">query</span>
|
||||||
|
<button id="query-close-btn" class="tb-btn">✕</button>
|
||||||
|
</div>
|
||||||
|
<table id="query-table">
|
||||||
|
<thead><tr><th>GlobalId</th><th id="query-val-header-col">value</th></tr></thead>
|
||||||
|
<tbody id="query-tbody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
<div id="status"></div>
|
<div id="status"></div>
|
||||||
<div id="props-panel" style="display:none">
|
<div id="props-panel" style="display:none">
|
||||||
<div id="props-panel-header">
|
<div id="props-panel-header">
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,12 @@ const repoInput = document.getElementById("repo-input");
|
||||||
const refInput = document.getElementById("ref-input");
|
const refInput = document.getElementById("ref-input");
|
||||||
const pathInput = document.getElementById("path-input");
|
const pathInput = document.getElementById("path-input");
|
||||||
const selectorInput = document.getElementById("selector-input");
|
const selectorInput = document.getElementById("selector-input");
|
||||||
|
const queryInput = document.getElementById("query-input");
|
||||||
|
const queryPanel = document.getElementById("query-panel");
|
||||||
|
const queryTbody = document.getElementById("query-tbody");
|
||||||
|
const queryPanelTitle = document.getElementById("query-panel-title");
|
||||||
|
const queryValHeader = document.getElementById("query-val-header-col");
|
||||||
|
const queryCloseBtn = document.getElementById("query-close-btn");
|
||||||
const copyUrlBtn = document.getElementById("copy-url-btn");
|
const copyUrlBtn = document.getElementById("copy-url-btn");
|
||||||
const newIssueBtn = document.getElementById("new-issue-btn");
|
const newIssueBtn = document.getElementById("new-issue-btn");
|
||||||
const visibilitySelect = document.getElementById("visibility-select");
|
const visibilitySelect = document.getElementById("visibility-select");
|
||||||
|
|
@ -59,7 +65,8 @@ function removeSnapshot() {
|
||||||
function buildIfcUrl() {
|
function buildIfcUrl() {
|
||||||
return _buildIfcUrl(
|
return _buildIfcUrl(
|
||||||
repoInput.value.trim(), refInput.value.trim(),
|
repoInput.value.trim(), refInput.value.trim(),
|
||||||
pathInput.value.trim(), selectorInput.value.trim()
|
pathInput.value.trim(), selectorInput.value.trim(),
|
||||||
|
queryInput.value.trim()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,6 +77,7 @@ if (parsed) {
|
||||||
refInput.value = parsed.ref;
|
refInput.value = parsed.ref;
|
||||||
pathInput.value = parsed.path;
|
pathInput.value = parsed.path;
|
||||||
selectorInput.value = parsed.selector;
|
selectorInput.value = parsed.selector;
|
||||||
|
queryInput.value = parsed.query;
|
||||||
}
|
}
|
||||||
urlInput.value = ifcUrl;
|
urlInput.value = ifcUrl;
|
||||||
|
|
||||||
|
|
@ -175,10 +183,12 @@ function loadFromFields() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pressing Enter in any structured field rebuilds the URL and navigates.
|
// Pressing Enter in any structured field rebuilds the URL and navigates.
|
||||||
for (const el of [repoInput, refInput, pathInput, selectorInput]) {
|
for (const el of [repoInput, refInput, pathInput, selectorInput, queryInput]) {
|
||||||
el.addEventListener("keydown", e => { if (e.key === "Enter") loadFromFields(); });
|
el.addEventListener("keydown", e => { if (e.key === "Enter") loadFromFields(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
queryCloseBtn.addEventListener("click", () => { queryPanel.style.display = "none"; });
|
||||||
|
|
||||||
// ▾ button: if the ref field already has a value, navigate; otherwise clear
|
// ▾ button: if the ref field already has a value, navigate; otherwise clear
|
||||||
// + focus so the datalist shows all refs.
|
// + focus so the datalist shows all refs.
|
||||||
const refPickBtn = document.getElementById("ref-pick-btn");
|
const refPickBtn = document.getElementById("ref-pick-btn");
|
||||||
|
|
@ -282,7 +292,7 @@ propsCopyBtn.addEventListener("click", async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const allInputs = [urlInput, fovInput, repoInput, refInput, pathInput, selectorInput,
|
const allInputs = [urlInput, fovInput, repoInput, refInput, pathInput, selectorInput,
|
||||||
visibilitySelect, bcfTitleIn, bcfCommentIn];
|
queryInput, visibilitySelect, bcfTitleIn, bcfCommentIn];
|
||||||
for (const el of allInputs) {
|
for (const el of allInputs) {
|
||||||
el.addEventListener("focus", onInputFocus);
|
el.addEventListener("focus", onInputFocus);
|
||||||
el.addEventListener("blur", onInputBlur);
|
el.addEventListener("blur", onInputBlur);
|
||||||
|
|
@ -545,6 +555,45 @@ async function applySelector(components, model, selectorStr, srcUrl, visibility
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Fetch /query results and display them in the query panel.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
async function applyQuery(srcUrl, queryPath) {
|
||||||
|
statusEl.textContent = "Querying…";
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/query?url=${encodeURIComponent(srcUrl)}`);
|
||||||
|
if (!resp.ok) {
|
||||||
|
const detail = await resp.text().catch(() => resp.statusText);
|
||||||
|
statusEl.textContent = `Query error: ${detail}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data = await resp.json();
|
||||||
|
} catch (_) {
|
||||||
|
statusEl.textContent = "Query requires the ifcurl service — /query not reachable";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
statusEl.textContent = "";
|
||||||
|
const entries = Object.entries(data);
|
||||||
|
if (!entries.length) return;
|
||||||
|
|
||||||
|
queryPanelTitle.textContent = queryPath;
|
||||||
|
queryValHeader.textContent = queryPath;
|
||||||
|
queryTbody.innerHTML = "";
|
||||||
|
for (const [guid, value] of entries) {
|
||||||
|
const tr = document.createElement("tr");
|
||||||
|
const tdGuid = document.createElement("td");
|
||||||
|
tdGuid.textContent = guid;
|
||||||
|
const tdVal = document.createElement("td");
|
||||||
|
tdVal.textContent = value;
|
||||||
|
tr.appendChild(tdGuid);
|
||||||
|
tr.appendChild(tdVal);
|
||||||
|
queryTbody.appendChild(tr);
|
||||||
|
}
|
||||||
|
queryPanel.style.display = "";
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Place the camera from ifc:// URL camera=/fov=/scale= params.
|
// Place the camera from ifc:// URL camera=/fov=/scale= params.
|
||||||
// obcCamera is the OBC OrthoPerspectiveCamera instance (world.camera).
|
// obcCamera is the OBC OrthoPerspectiveCamera instance (world.camera).
|
||||||
|
|
@ -1093,6 +1142,7 @@ async function main() {
|
||||||
|
|
||||||
const visibility = qsPost.get("visibility") ?? "highlight";
|
const visibility = qsPost.get("visibility") ?? "highlight";
|
||||||
const selectorStr = qsPost.get("selector") ?? "";
|
const selectorStr = qsPost.get("selector") ?? "";
|
||||||
|
const queryStr = qsPost.get("query") ?? "";
|
||||||
|
|
||||||
// Set up Highlighter before selector so visibility=highlight can use it.
|
// Set up Highlighter before selector so visibility=highlight can use it.
|
||||||
let highlighter = null;
|
let highlighter = null;
|
||||||
|
|
@ -1131,6 +1181,10 @@ async function main() {
|
||||||
await applySelector(components, model, selectorStr, ifcUrl, visibility);
|
await applySelector(components, model, selectorStr, ifcUrl, visibility);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectorStr && queryStr) {
|
||||||
|
await applyQuery(ifcUrl, queryStr);
|
||||||
|
}
|
||||||
|
|
||||||
await fragments.core.update(true);
|
await fragments.core.update(true);
|
||||||
|
|
||||||
syncCameraUrl(threeCamera);
|
syncCameraUrl(threeCamera);
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,11 @@ describe("parseIfcUrl", () => {
|
||||||
assert.equal(r.selector, "IfcWall");
|
assert.equal(r.selector, "IfcWall");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("parses query parameter", () => {
|
||||||
|
const r = parseIfcUrl("ifc://example.com/org/repo@heads/main?selector=IfcWall&query=Pset_WallCommon.FireRating");
|
||||||
|
assert.equal(r.query, "Pset_WallCommon.FireRating");
|
||||||
|
});
|
||||||
|
|
||||||
test("parses camera parameter", () => {
|
test("parses camera parameter", () => {
|
||||||
const r = parseIfcUrl("ifc://example.com/org/repo@heads/main?camera=1,2,3,4,5,6,7,8,9");
|
const r = parseIfcUrl("ifc://example.com/org/repo@heads/main?camera=1,2,3,4,5,6,7,8,9");
|
||||||
assert.equal(r.camera, "1,2,3,4,5,6,7,8,9");
|
assert.equal(r.camera, "1,2,3,4,5,6,7,8,9");
|
||||||
|
|
@ -75,6 +80,7 @@ describe("parseIfcUrl", () => {
|
||||||
const r = parseIfcUrl("ifc://example.com/org/repo@heads/main");
|
const r = parseIfcUrl("ifc://example.com/org/repo@heads/main");
|
||||||
assert.equal(r.path, "");
|
assert.equal(r.path, "");
|
||||||
assert.equal(r.selector, "");
|
assert.equal(r.selector, "");
|
||||||
|
assert.equal(r.query, "");
|
||||||
assert.equal(r.camera, "");
|
assert.equal(r.camera, "");
|
||||||
assert.equal(r.fov, "");
|
assert.equal(r.fov, "");
|
||||||
assert.equal(r.scale, "");
|
assert.equal(r.scale, "");
|
||||||
|
|
@ -121,6 +127,16 @@ describe("buildIfcUrl", () => {
|
||||||
assert.equal(url, "ifc://example.com/org/repo@heads/main?path=model.ifc&selector=IfcWall");
|
assert.equal(url, "ifc://example.com/org/repo@heads/main?path=model.ifc&selector=IfcWall");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("includes query parameter", () => {
|
||||||
|
const url = buildIfcUrl("example.com/org/repo", "heads/main", "model.ifc", "IfcWall", "Pset_WallCommon.FireRating");
|
||||||
|
assert.equal(url, "ifc://example.com/org/repo@heads/main?path=model.ifc&selector=IfcWall&query=Pset_WallCommon.FireRating");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("omits query when empty", () => {
|
||||||
|
const url = buildIfcUrl("example.com/org/repo", "heads/main", "model.ifc", "IfcWall", "");
|
||||||
|
assert.equal(url, "ifc://example.com/org/repo@heads/main?path=model.ifc&selector=IfcWall");
|
||||||
|
});
|
||||||
|
|
||||||
test("does not percent-encode commas in selector", () => {
|
test("does not percent-encode commas in selector", () => {
|
||||||
const url = buildIfcUrl("example.com/org/repo", "heads/main", "", "IfcWall,Name=\"Core\"");
|
const url = buildIfcUrl("example.com/org/repo", "heads/main", "", "IfcWall,Name=\"Core\"");
|
||||||
assert.ok(url.includes(","), "comma should not be encoded");
|
assert.ok(url.includes(","), "comma should not be encoded");
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue