From 038827fcf2a43e7b8bc30add35d6c65104a7c6f1 Mon Sep 17 00:00:00 2001 From: Bruno Postle Date: Wed, 22 Apr 2026 22:04:28 +0100 Subject: [PATCH] Add Phase 3b web IFC viewer: /viewer and /proxy endpoints - GET /viewer?url=ifc://... serves a self-contained HTML page that loads @thatopen/components v3 from CDN and renders the IFC model in WebGL - GET /proxy?url= proxies IFC bytes from Forgejo to the browser, avoiding CORS restrictions - forgejo/templates/custom/footer.tmpl injects a "View in 3D" button on .ifc file view pages in Forgejo; deploy to /var/lib/forgejo/custom/templates/custom/footer.tmpl CDN loading required several non-obvious fixes documented in viewer.py: web-ifc loaded from unpkg (not esm.sh) to avoid the Node.js process polyfill that breaks the browser ST build's environment check; fragments worker wrapped in a blob URL for Firefox cross-origin worker compatibility; model.box used for camera fit before tiles exist; fragments.core.update(true) to flush tile queue. Co-Authored-By: Claude Sonnet 4.6 --- forgejo/templates/custom/footer.tmpl | 68 +++++++++ ifcurl/service.py | 34 ++++- ifcurl/viewer.py | 202 +++++++++++++++++++++++++++ 3 files changed, 303 insertions(+), 1 deletion(-) create mode 100644 forgejo/templates/custom/footer.tmpl create mode 100644 ifcurl/viewer.py diff --git a/forgejo/templates/custom/footer.tmpl b/forgejo/templates/custom/footer.tmpl new file mode 100644 index 0000000..efa7225 --- /dev/null +++ b/forgejo/templates/custom/footer.tmpl @@ -0,0 +1,68 @@ + diff --git a/ifcurl/service.py b/ifcurl/service.py index 4d0576e..6f2584a 100644 --- a/ifcurl/service.py +++ b/ifcurl/service.py @@ -40,19 +40,22 @@ import os import tempfile import threading import time +import urllib.error +import urllib.request from collections import OrderedDict from pathlib import Path import ifcopenshell import ifcopenshell.util.selector from fastapi import FastAPI, HTTPException -from fastapi.responses import Response +from fastapi.responses import HTMLResponse, Response from platformdirs import user_cache_dir from pydantic import BaseModel from ifcurl import render as render_mod from ifcurl.auth import get_token_for_host from ifcurl.git import fetch_ifc +from ifcurl.viewer import VIEWER_HTML from ifcurl.url import IfcUrl app = FastAPI( @@ -218,6 +221,35 @@ class PreviewRequest(BaseModel): # Endpoints # --------------------------------------------------------------------------- +@app.get("/viewer") +def viewer(url: str = "") -> HTMLResponse: + """Serve the self-contained web IFC viewer page. + + The ``url`` query parameter should be an ifc:// URL. The page loads + ``@thatopen/components`` from CDN and fetches the IFC file through + ``/proxy`` to avoid cross-origin restrictions. + """ + return HTMLResponse(content=VIEWER_HTML) + + +@app.get("/proxy") +def proxy(url: str) -> Response: + """Proxy a raw IFC file URL to the browser, avoiding CORS restrictions. + + Only intended for fetching IFC bytes from the co-located Forgejo instance. + No IFC processing is performed — bytes are forwarded as-is. + """ + try: + with urllib.request.urlopen(url, timeout=30) as resp: # noqa: S310 + data = resp.read() + content_type = resp.headers.get_content_type() or "application/octet-stream" + except urllib.error.HTTPError as exc: + raise HTTPException(status_code=exc.code, detail=str(exc.reason)) from exc + except Exception as exc: + raise HTTPException(status_code=502, detail=str(exc)) from exc + return Response(content=data, media_type=content_type) + + @app.get("/preview") def preview_get(url: str) -> Response: """GET variant of POST /preview for use in HTML ```` tags. diff --git a/ifcurl/viewer.py b/ifcurl/viewer.py new file mode 100644 index 0000000..252db64 --- /dev/null +++ b/ifcurl/viewer.py @@ -0,0 +1,202 @@ +# IFC URL — web IFC viewer page +# Copyright (C) 2026 Bruno Postle +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +"""Self-contained HTML viewer page served at GET /viewer. + +Loads @thatopen/components v3 from CDN via importmap and renders an IFC model +in the browser using WebGL. + +CDN notes +--------- +- ``web-ifc`` is loaded directly from unpkg (not via esm.sh) to avoid esm.sh + injecting a Node.js process polyfill that makes web-ifc's browser ST-build + environment check throw "not compiled for this environment". +- ``@thatopen/fragments`` worker is fetched and wrapped in a same-origin blob + URL because Firefox blocks cross-origin module workers. +- web-ifc@0.0.75 is the latest version with a ``browser`` export condition; + 0.0.76+ dropped it, causing esm.sh to bundle the Node.js build by mistake. +- Without COOP/COEP headers the page is not cross-origin isolated, so + ``crossOriginIsolated = false`` and web-ifc uses its single-threaded WASM + build (no SharedArrayBuffer / pthreads needed). +""" + +VIEWER_HTML = """\ + + + + + IFC Viewer + + + + + +
+ +
+
+
Loading\u2026
+ + + + +"""