mirror of
https://github.com/brunopostle/ifcurl.git
synced 2026-07-12 10:18:14 +00:00
Remove PLAN.md (tasks now in beads), add syncCameraUrl to viewer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
00f3f6789c
commit
c9af45b354
3 changed files with 47 additions and 286 deletions
|
|
@ -19,7 +19,7 @@ ifcurl serve --host 0.0.0.0 --port 9000
|
||||||
|
|
||||||
`ifcurl` implements the `ifc://` URL scheme — a way to address a specific view of an IFC model stored in a Git repository. A single URL encodes the git source, an optional IfcOpenShell selector, and an optional camera viewpoint.
|
`ifcurl` implements the `ifc://` URL scheme — a way to address a specific view of an IFC model stored in a Git repository. A single URL encodes the git source, an optional IfcOpenShell selector, and an optional camera viewpoint.
|
||||||
|
|
||||||
Full spec: `SPECIFICATION.md`. Full development plan: `PLAN.md`.
|
Full spec: `SPECIFICATION.md`. Development tasks tracked in beads (`bd ready`).
|
||||||
|
|
||||||
## URL structure
|
## URL structure
|
||||||
|
|
||||||
|
|
|
||||||
285
PLAN.md
285
PLAN.md
|
|
@ -1,285 +0,0 @@
|
||||||
# IFC URL — Development Plan
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
IFC URL requires work across four distinct areas:
|
|
||||||
|
|
||||||
1. A Python core library and CLI tool
|
|
||||||
2. A preview service with Gitea integration
|
|
||||||
3. Desktop app integration (Bonsai, IFC Viewer)
|
|
||||||
4. The federation extension to `IFCDOCUMENTREFERENCE`
|
|
||||||
|
|
||||||
The Python core is the foundation for all other work. The Gitea integration
|
|
||||||
and desktop app work can proceed in parallel once the core exists.
|
|
||||||
|
|
||||||
### Language ownership
|
|
||||||
|
|
||||||
| Component | Language | Rationale |
|
|
||||||
|---|---|---|
|
|
||||||
| URL parsing, git fetch, selector execution, rendering | Python | Built on IfcOpenShell |
|
|
||||||
| Bonsai plugin | Python | Bonsai is a Blender/Python addon |
|
|
||||||
| CLI tool | Python | Wraps the core library |
|
|
||||||
| Preview service | Python | Wraps the core library |
|
|
||||||
| Gitea plugin | Go | Gitea is written in Go; the plugin only calls an HTTP endpoint |
|
|
||||||
| Web viewer integration | JavaScript | Future, not in current scope |
|
|
||||||
|
|
||||||
The Python core is a strong candidate for eventual contribution to
|
|
||||||
IfcOpenShell, but this is not a prerequisite for any phase.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 1 — Python core and CLI tool
|
|
||||||
|
|
||||||
Build a Python library that resolves an `ifc://` URL and produces a rendered
|
|
||||||
PNG. This validates the spec against real IFC files and is the foundation for
|
|
||||||
all subsequent work.
|
|
||||||
|
|
||||||
### Library responsibilities
|
|
||||||
|
|
||||||
- Parse an `ifc://` URL into its components
|
|
||||||
- Infer git transport from URL structure (SSH, HTTPS, local)
|
|
||||||
- Fetch the IFC file from the repository at the specified ref
|
|
||||||
- Execute the IfcOpenShell selector against the loaded model
|
|
||||||
- Apply camera (perspective and orthographic), clipping planes, and
|
|
||||||
visibility mode
|
|
||||||
- Render a static PNG
|
|
||||||
|
|
||||||
### CLI interface
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ifc-url render "ifc://example.com/org/repo@abc123?path=model.ifc&selector=IfcWall&fov=60"
|
|
||||||
ifc-url render "ifc://example.com/org/repo@heads/main?path=model.ifc&camera=0,0,8,0,0,-1,0,1,0&scale=50"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Notes
|
|
||||||
|
|
||||||
- IfcOpenShell handles selector execution, model parsing, and geometry
|
|
||||||
- Rendering requires a headless approach; options include IfcOpenShell's own
|
|
||||||
geometry processing with a software or headless OpenGL renderer
|
|
||||||
- Git access uses the platform credential store for HTTPS and ssh-agent or
|
|
||||||
key files for SSH
|
|
||||||
- The CLI accepts the URL as a plain string so it can be called from
|
|
||||||
non-Python contexts
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 2 — Preview service
|
|
||||||
|
|
||||||
Wrap the Phase 1 library in an HTTP service for use by Gitea and other
|
|
||||||
consumers.
|
|
||||||
|
|
||||||
### Endpoint
|
|
||||||
|
|
||||||
```
|
|
||||||
POST /preview
|
|
||||||
body: { url: "ifc://..." }
|
|
||||||
response: image/png
|
|
||||||
```
|
|
||||||
|
|
||||||
### Caching
|
|
||||||
|
|
||||||
| Tier | Cache key | Contents | Notes |
|
|
||||||
|---|---|---|---|
|
|
||||||
| 2 | commit hash | Parsed model structure | Shared across requests |
|
|
||||||
| 3 | commit hash + selector | Resolved GUID set | |
|
|
||||||
| 4 | full URL hash | Rendered PNG | Not applied to mutable refs |
|
|
||||||
|
|
||||||
Mutable refs (`@heads/`, `@HEAD`) must not be cached at tier 4 since the
|
|
||||||
underlying commit may change. Commit hash refs are immutable and can be
|
|
||||||
cached indefinitely at all tiers.
|
|
||||||
|
|
||||||
### Authentication
|
|
||||||
|
|
||||||
- **Co-located with Gitea** — uses the requesting user's Gitea session token,
|
|
||||||
passed from the Gitea plugin
|
|
||||||
- **Standalone** — sideloaded token configuration per git host
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 3 — Gitea integration
|
|
||||||
|
|
||||||
Add `ifc://` link detection and preview embedding to Gitea.
|
|
||||||
|
|
||||||
### Go plugin responsibilities
|
|
||||||
|
|
||||||
- Detect `ifc://` links in post and comment content
|
|
||||||
- Call the preview service with the URL and the current user's session token
|
|
||||||
- Embed the returned PNG inline with a link that opens the URL
|
|
||||||
- Degrade gracefully when the preview service is unavailable (show the raw
|
|
||||||
URL rather than failing)
|
|
||||||
|
|
||||||
### Notes
|
|
||||||
|
|
||||||
- The plugin has no IFC awareness; all IFC logic lives in the preview service
|
|
||||||
- Repeated embeds of the same URL across different posts do not trigger
|
|
||||||
re-renders — the preview service cache handles deduplication
|
|
||||||
- The preview service may run as a companion process on the same host or
|
|
||||||
as a separately configured external service
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 3b — Web IFC viewer
|
|
||||||
|
|
||||||
Add a 3D viewer that opens in a separate browser window when viewing an `.ifc`
|
|
||||||
file in Forgejo.
|
|
||||||
|
|
||||||
### Architecture
|
|
||||||
|
|
||||||
- **Forgejo Go patch**: when rendering the file view for a `.ifc` file, inject
|
|
||||||
a "View in 3D" button that opens
|
|
||||||
`http://localhost:8000/viewer?url=ifc://...` in a new tab. The ifc:// URL
|
|
||||||
encodes the current host, repo, ref, and file path.
|
|
||||||
|
|
||||||
- **`/viewer` endpoint** in the ifcurl preview service: returns a self-contained
|
|
||||||
HTML page. No server-side IFC processing — the browser does all rendering.
|
|
||||||
|
|
||||||
- **Browser rendering**: the viewer page loads `@thatopen/components` (and its
|
|
||||||
`web-ifc` + Three.js peer dependencies) from a CDN via an ES module importmap.
|
|
||||||
It constructs the Forgejo raw file URL from the ifc:// URL parameters and
|
|
||||||
fetches the IFC bytes directly from Forgejo.
|
|
||||||
|
|
||||||
### Current scope (Phase 3b)
|
|
||||||
|
|
||||||
- Display the full model for whatever ref is shown in Forgejo.
|
|
||||||
- Basic orbit/pan/zoom camera controls.
|
|
||||||
- No filtering or selection UI yet.
|
|
||||||
|
|
||||||
### Future scope
|
|
||||||
|
|
||||||
- Element filtering and selection using `@thatopen/components` classifier and
|
|
||||||
highlighter APIs.
|
|
||||||
- Camera state serialised to an `ifc://` URL so the user can copy a view link
|
|
||||||
(feeds into Phase 4 Bonsai "Copy view URL").
|
|
||||||
- Diff highlighting driven by ifcmerge output (see Phase 3c).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 3c — ifcmerge integration
|
|
||||||
|
|
||||||
`ifcmerge` is a git custom merge driver that resolves conflicts in `.ifc` files
|
|
||||||
that git's built-in 3-way merge cannot handle. Where two branches have both
|
|
||||||
modified an IFC file (a fork), git would report an unresolvable conflict;
|
|
||||||
ifcmerge performs a semantics-aware merge and produces a valid merged IFC file.
|
|
||||||
|
|
||||||
### Git merge driver configuration
|
|
||||||
|
|
||||||
Register ifcmerge as the git merge driver for `.ifc` files on the Forgejo
|
|
||||||
server:
|
|
||||||
|
|
||||||
```
|
|
||||||
# /etc/gitconfig or ~forgejo/.gitconfig
|
|
||||||
[merge "ifcmerge"]
|
|
||||||
name = IFC merge driver
|
|
||||||
driver = ifcmerge %O %A %B %L
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
# .gitattributes in each repo, or server-side gitattributes
|
|
||||||
*.ifc merge=ifcmerge
|
|
||||||
```
|
|
||||||
|
|
||||||
With this in place, Forgejo's existing "merge automatically" path will invoke
|
|
||||||
ifcmerge via git when merging `.ifc` files, and the PR merge button will work
|
|
||||||
without any Go-side changes for the common case.
|
|
||||||
|
|
||||||
### Forgejo integration
|
|
||||||
|
|
||||||
- Verify that Forgejo's merge-ability check (the "these branches can be merged
|
|
||||||
automatically" indicator) correctly reflects ifcmerge's ability to merge
|
|
||||||
`.ifc` files — investigate whether the check runs a trial merge via git or
|
|
||||||
uses a heuristic.
|
|
||||||
- If needed, patch Forgejo to run a trial `git merge --no-commit` to get an
|
|
||||||
accurate result for IFC files.
|
|
||||||
|
|
||||||
### 3D diff view (future)
|
|
||||||
|
|
||||||
- After a merge completes, surface a "View merge in 3D" button in the PR 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.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 4 — Bonsai integration
|
|
||||||
|
|
||||||
Add `ifc://` handling to Bonsai.
|
|
||||||
|
|
||||||
### Protocol handler
|
|
||||||
|
|
||||||
Register `ifc://` as an OS protocol handler on installation (Windows,
|
|
||||||
macOS, Linux). On receiving a URL, resolve the git source, load the model,
|
|
||||||
and apply the view state from the URL parameters.
|
|
||||||
|
|
||||||
### "Copy view URL"
|
|
||||||
|
|
||||||
A button that generates an `ifc://` URL from the current Bonsai state:
|
|
||||||
|
|
||||||
- Git remote, ref, and file path of the open model
|
|
||||||
- Active selection converted to an IfcOpenShell selector
|
|
||||||
- Current camera converted to the `camera` + `fov` or `scale` format,
|
|
||||||
in IFC world coordinates
|
|
||||||
- Active clipping planes as `clip` parameters
|
|
||||||
- Current visibility mode
|
|
||||||
|
|
||||||
The button always generates a commit hash ref by default. Generating a
|
|
||||||
`@heads/` ref is a secondary option for authoring links intended to track
|
|
||||||
a branch.
|
|
||||||
|
|
||||||
### Federation
|
|
||||||
|
|
||||||
When opening a model via `ifc://`, Bonsai resolves `IFCDOCUMENTREFERENCE`
|
|
||||||
locations using the repo and ref from the root file's URL as context.
|
|
||||||
`ifc://` locations in linked model references are resolved recursively.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 5 — IFC Viewer integration
|
|
||||||
|
|
||||||
Equivalent to Phase 4 for the IFC Viewer desktop application.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 6 — Federation extension
|
|
||||||
|
|
||||||
Extend the `IFCDOCUMENTREFERENCE` location field in Bonsai to accept
|
|
||||||
`ifc://` URLs, enabling cross-repo and cross-server federation.
|
|
||||||
|
|
||||||
### Tasks
|
|
||||||
|
|
||||||
- When saving a linked model reference, 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
|
|
||||||
- Resolve `ifc://` location fields through the Phase 4 URL resolver
|
|
||||||
|
|
||||||
The transformation matrix in `IFCDOCUMENTREFERENCE` is unaffected by this
|
|
||||||
change. Phase 4 must be complete before this phase begins.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Future work
|
|
||||||
|
|
||||||
### BCF export
|
|
||||||
|
|
||||||
An additional preview service endpoint returning a BCF file:
|
|
||||||
|
|
||||||
```
|
|
||||||
POST /bcf
|
|
||||||
body: { url: "ifc://..." }
|
|
||||||
response: application/zip
|
|
||||||
```
|
|
||||||
|
|
||||||
Resolves the selector to a GUID set and constructs a BCF viewpoint from the
|
|
||||||
camera, clipping planes, and visibility state. No implementation is planned
|
|
||||||
before Phase 2 is complete.
|
|
||||||
|
|
||||||
### Web viewer integration
|
|
||||||
|
|
||||||
JavaScript integration with web-based IFC viewers (IFC.js, xeokit) and
|
|
||||||
browser-side forum embedding. Not in current scope.
|
|
||||||
|
|
||||||
### IfcOpenShell contribution
|
|
||||||
|
|
||||||
Once the Python core is stable, the URL parsing and resolution logic is a
|
|
||||||
candidate for contribution to IfcOpenShell as a shared utility. This is not
|
|
||||||
a prerequisite for any phase.
|
|
||||||
|
|
@ -74,6 +74,7 @@ VIEWER_HTML = """\
|
||||||
<div id="status">Loading\u2026</div>
|
<div id="status">Loading\u2026</div>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
|
import * as THREE from "three";
|
||||||
import * as OBC from "@thatopen/components";
|
import * as OBC from "@thatopen/components";
|
||||||
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
|
@ -84,6 +85,45 @@ VIEWER_HTML = """\
|
||||||
urlEl.textContent = ifcUrl || "No ifc:// URL provided";
|
urlEl.textContent = ifcUrl || "No ifc:// URL provided";
|
||||||
urlEl.title = ifcUrl;
|
urlEl.title = ifcUrl;
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Rebuild the ifc:// URL with the current camera position, direction, and
|
||||||
|
// up vector, update the toolbar display and the browser address bar.
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
function syncCameraUrl(camera) {
|
||||||
|
const pos = camera.position;
|
||||||
|
const dir = new THREE.Vector3();
|
||||||
|
camera.getWorldDirection(dir);
|
||||||
|
const up = camera.up;
|
||||||
|
|
||||||
|
const f = v => parseFloat(v.toFixed(4));
|
||||||
|
const cameraParam = [
|
||||||
|
f(pos.x), f(pos.y), f(pos.z),
|
||||||
|
f(dir.x), f(dir.y), f(dir.z),
|
||||||
|
f(up.x), f(up.y), f(up.z),
|
||||||
|
].join(",");
|
||||||
|
|
||||||
|
const qIdx = ifcUrl.indexOf("?");
|
||||||
|
const base = qIdx < 0 ? ifcUrl : ifcUrl.slice(0, qIdx);
|
||||||
|
const qs = new URLSearchParams(qIdx < 0 ? "" : ifcUrl.slice(qIdx + 1));
|
||||||
|
qs.set("camera", cameraParam);
|
||||||
|
if (camera.isPerspectiveCamera) {
|
||||||
|
qs.set("fov", f(camera.fov).toString());
|
||||||
|
qs.delete("scale");
|
||||||
|
} else {
|
||||||
|
// OrthographicCamera: world-space viewport height as scale
|
||||||
|
qs.set("scale", f(camera.top - camera.bottom).toString());
|
||||||
|
qs.delete("fov");
|
||||||
|
}
|
||||||
|
const newIfcUrl = base + "?" + qs.toString().replace(/%2C/g, ",");
|
||||||
|
|
||||||
|
urlEl.textContent = newIfcUrl;
|
||||||
|
urlEl.title = newIfcUrl;
|
||||||
|
|
||||||
|
const pageUrl = new URL(window.location.href);
|
||||||
|
pageUrl.searchParams.set("url", newIfcUrl);
|
||||||
|
history.replaceState(null, "", pageUrl.toString());
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Convert an ifc:// URL to a same-origin /proxy?url=... request so the
|
// Convert an ifc:// URL to a same-origin /proxy?url=... request so the
|
||||||
// browser never makes a cross-origin request for the IFC bytes.
|
// browser never makes a cross-origin request for the IFC bytes.
|
||||||
|
|
@ -188,6 +228,12 @@ VIEWER_HTML = """\
|
||||||
model.useCamera(world.camera.three);
|
model.useCamera(world.camera.three);
|
||||||
await fragments.core.update(true);
|
await fragments.core.update(true);
|
||||||
|
|
||||||
|
// Show initial camera position in URL, then keep it live.
|
||||||
|
syncCameraUrl(world.camera.three);
|
||||||
|
world.camera.controls.addEventListener("rest", () => {
|
||||||
|
syncCameraUrl(world.camera.three);
|
||||||
|
});
|
||||||
|
|
||||||
statusEl.textContent = "";
|
statusEl.textContent = "";
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
statusEl.textContent = `Render error: ${err.message}`;
|
statusEl.textContent = `Render error: ${err.message}`;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue