mirror of
https://github.com/brunopostle/ifcurl.git
synced 2026-07-12 02:08:13 +00:00
viewer.html: interactive clipping plane tool
Replaces the raw renderer.clippingPlanes approach with OBC.Clipper: URL-specified clip= planes are seeded as draggable SimplePlanes. Toolbar '✂ clip' toggle + double-click on model surface to add planes; '⊗ clips' button (shown when planes exist) to clear all. syncCameraUrl now serialises current plane state back to the ifc:// URL on every camera move or plane drag/create/delete. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a33a4f9296
commit
13c8687027
2 changed files with 70 additions and 10 deletions
|
|
@ -19,7 +19,7 @@
|
|||
{"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}
|
||||
{"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":"Bruno Postle","metadata":"{}"},{"issue_id":"ifcurl-i1s","depends_on_id":"ifcurl-4yu","type":"blocks","created_at":"2026-04-23T07:14:11Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"ifcurl-i1s","depends_on_id":"ifcurl-ach","type":"blocks","created_at":"2026-04-23T06:48:10Z","created_by":"Bruno Postle","metadata":"{}"},{"issue_id":"ifcurl-i1s","depends_on_id":"ifcurl-ahd","type":"blocks","created_at":"2026-04-23T07:02:54Z","created_by":"Bruno Postle","metadata":"{}"}],"dependency_count":4,"dependent_count":1,"comment_count":0}
|
||||
{"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":"Bruno Postle","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
|
||||
{"id":"ifcurl-8s8","title":"viewer.html: interactive tool to set clipping planes","description":"Currently clip= planes can only be set by hand-editing the URL. Add an interactive clipping tool to the viewer: a UI control (e.g. toolbar button) that lets the user drag/place a clipping plane against the loaded model. The plane position and normal should be written back into the ifc:// URL as clip= parameters so the view is shareable. Consider using ThatOpen's clipper component (OBC.Clipper) if available, or a Three.js-based approach.","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-24T05:49:22Z","created_by":"Bruno Postle","updated_at":"2026-04-24T05:49:22Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"id":"ifcurl-8s8","title":"viewer.html: interactive tool to set clipping planes","description":"Currently clip= planes can only be set by hand-editing the URL. Add an interactive clipping tool to the viewer: a UI control (e.g. toolbar button) that lets the user drag/place a clipping plane against the loaded model. The plane position and normal should be written back into the ifc:// URL as clip= parameters so the view is shareable. Consider using ThatOpen's clipper component (OBC.Clipper) if available, or a Three.js-based approach.","status":"closed","priority":3,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-24T05:49:22Z","created_by":"Bruno Postle","updated_at":"2026-04-24T06:23:14Z","started_at":"2026-04-24T06:11:49Z","closed_at":"2026-04-24T06:23:14Z","close_reason":"setupClipper() uses OBC.Clipper: seeds URL planes as draggable SimplePlanes, toolbar '✂ clip' toggle for double-click placement, '⊗ clips' clear button. syncCameraUrl extended to serialise current planes back to URL.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"id":"ifcurl-07a","title":"Cache size control for bare repo clones","description":"git.py clones remote repos as bare repos under ~/.cache/ifcurl/\u003chash\u003e/repo.git with no size limit or eviction policy. Large repos accumulate indefinitely. Add a configurable max cache size (e.g. via env var IFCURL_CACHE_MAX_GB or a config file) with an LRU eviction policy based on last-access time of each repo directory. Should also expose a CLI command (ifcurl cache --list, --prune, --clear) to inspect and manage the cache manually.","status":"open","priority":3,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-24T05:45:59Z","created_by":"Bruno Postle","updated_at":"2026-04-24T05:45:59Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"id":"ifcurl-ts5","title":"Assess Go test strategy for Forgejo plugin patch","description":"The Forgejo integration is a low-maintenance source patch (footer.tmpl, viewer.html, /viewer and /proxy HTTP handlers). Evaluate whether adding Go tests is feasible without coupling to Forgejo's full test suite: can the handler logic be extracted into a small testable package, or do tests have to live inside the Forgejo tree? If the cost of keeping tests green across Forgejo upstream upgrades is too high, document the decision and close. Outcome: either a small test file or a documented rationale for no Go tests.","status":"open","priority":3,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-04-24T05:45:57Z","created_by":"Bruno Postle","updated_at":"2026-04-24T05:45:57Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"id":"ifcurl-hvw","title":"Add JavaScript tests for viewer.html URL logic","description":"viewer.html contains pure-JS functions (parseIfcUrl, buildIfcUrl, toRawUrl, syncCameraUrl) with no test coverage. Add a lightweight JS test harness (e.g. a standalone test.html or Node-runnable test.js using the native test runner) that unit-tests these functions: round-trip URL parse/build, toRawUrl for each host type (GitHub, GitLab, Forgejo), and camera parameter serialisation. No browser automation needed — pure function tests.","status":"open","priority":3,"issue_type":"task","owner":"bruno@postle.net","created_at":"2026-04-24T05:45:56Z","created_by":"Bruno Postle","updated_at":"2026-04-24T05:45:56Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,15 @@
|
|||
#ref-input { flex: 2; }
|
||||
#path-input { flex: 2; }
|
||||
#selector-input { flex: 1; }
|
||||
/* Toolbar buttons */
|
||||
.tb-btn {
|
||||
font-size: 12px; color: #c0c0c8; cursor: pointer; flex-shrink: 0;
|
||||
background: rgba(255,255,255,0.07); border: 1px solid rgba(255,255,255,0.12);
|
||||
border-radius: 4px; padding: 3px 8px; white-space: nowrap;
|
||||
}
|
||||
.tb-btn:hover { background: rgba(255,255,255,0.14); }
|
||||
.tb-btn.active { color: #a8c8ff; background: rgba(80,120,255,0.2);
|
||||
border-color: rgba(80,120,255,0.5); }
|
||||
/* Status */
|
||||
#status {
|
||||
position: fixed; bottom: 12px; left: 50%;
|
||||
|
|
@ -77,6 +86,8 @@
|
|||
<input id="url-input" class="ti" type="text"
|
||||
placeholder="Paste ifc:// URL and press Enter, or fill fields below"
|
||||
spellcheck="false">
|
||||
<button id="clip-btn" class="tb-btn" title="Double-click model surface to add clipping plane">✂ clip</button>
|
||||
<button id="clip-clear-btn" class="tb-btn" title="Remove all clipping planes" style="display:none">⊗ clips</button>
|
||||
<div id="fov-wrap">
|
||||
FOV <input id="fov-input" class="ti" type="number" min="10" max="120" step="1"
|
||||
title="Field of view (degrees)">°
|
||||
|
|
@ -132,6 +143,7 @@
|
|||
|
||||
let cameraControls = null;
|
||||
let threeCamera = null;
|
||||
let activeClipper = null;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Parse an ifc:// URL into its components.
|
||||
|
|
@ -304,6 +316,17 @@
|
|||
qs.delete("fov");
|
||||
fovInput.value = "";
|
||||
}
|
||||
// Replace original clip= params with current interactive planes.
|
||||
qs.delete("clip");
|
||||
if (activeClipper) {
|
||||
for (const [, plane] of activeClipper.list) {
|
||||
const n3 = plane.three.normal;
|
||||
const p3 = n3.clone().multiplyScalar(-plane.three.constant);
|
||||
const ni = toIfc(n3), pi = toIfc(p3);
|
||||
qs.append("clip", [f(pi.x),f(pi.y),f(pi.z),f(ni.x),f(ni.y),f(ni.z)].join(","));
|
||||
}
|
||||
}
|
||||
|
||||
const newIfcUrl = base + "?" + qs.toString().replace(/%2C/g, ",");
|
||||
|
||||
urlInput.value = newIfcUrl;
|
||||
|
|
@ -314,16 +337,53 @@
|
|||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Apply clip= planes from the ifc:// URL to the Three.js renderer.
|
||||
// Each clip= is 6 floats: point x,y,z + normal x,y,z in IFC world coords.
|
||||
// Set up OBC.Clipper for interactive clipping planes.
|
||||
// Seeds initial planes from the URL, wires toolbar buttons, and syncs
|
||||
// plane state back to the URL on create/delete/drag.
|
||||
// -----------------------------------------------------------------------
|
||||
function applyClipPlanes(world, clips) {
|
||||
const planes = clips.map(([px, py, pz, nx, ny, nz]) => {
|
||||
const p3 = toThree(px, py, pz);
|
||||
const n3 = toThree(nx, ny, nz).normalize();
|
||||
return new THREE.Plane(n3, -n3.dot(p3));
|
||||
function setupClipper(components, world, clips) {
|
||||
const clipper = components.get(OBC.Clipper);
|
||||
clipper.setup();
|
||||
activeClipper = clipper;
|
||||
|
||||
const clipBtn = document.getElementById("clip-btn");
|
||||
const clipClearBtn = document.getElementById("clip-clear-btn");
|
||||
const container = document.getElementById("viewer");
|
||||
|
||||
function updateClearBtn() {
|
||||
clipClearBtn.style.display = clipper.list.size > 0 ? "" : "none";
|
||||
}
|
||||
|
||||
// Seed planes from URL as interactive planes.
|
||||
for (const [px, py, pz, nx, ny, nz] of clips) {
|
||||
clipper.createFromNormalAndCoplanarPoint(
|
||||
world, toThree(nx, ny, nz).normalize(), toThree(px, py, pz)
|
||||
);
|
||||
}
|
||||
updateClearBtn();
|
||||
|
||||
// Double-click on model surface adds a plane when clip mode is active.
|
||||
let clipMode = false;
|
||||
clipBtn.addEventListener("click", () => {
|
||||
clipMode = !clipMode;
|
||||
clipBtn.classList.toggle("active", clipMode);
|
||||
container.style.cursor = clipMode ? "crosshair" : "";
|
||||
});
|
||||
world.renderer.three.clippingPlanes = planes;
|
||||
container.addEventListener("dblclick", async () => {
|
||||
if (!clipMode) return;
|
||||
await clipper.create(world);
|
||||
syncCameraUrl(threeCamera);
|
||||
updateClearBtn();
|
||||
});
|
||||
|
||||
clipClearBtn.addEventListener("click", () => {
|
||||
clipper.deleteAll();
|
||||
syncCameraUrl(threeCamera);
|
||||
updateClearBtn();
|
||||
});
|
||||
|
||||
// Sync URL whenever a plane is dragged.
|
||||
clipper.onAfterDrag.add(() => syncCameraUrl(threeCamera));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
|
|
@ -497,7 +557,7 @@
|
|||
const clips = qsPost.getAll("clip")
|
||||
.map(s => s.split(",").map(Number))
|
||||
.filter(v => v.length === 6 && !v.some(isNaN));
|
||||
if (clips.length) applyClipPlanes(world, clips);
|
||||
setupClipper(components, world, clips);
|
||||
|
||||
const selectorStr = qsPost.get("selector") ?? "";
|
||||
if (selectorStr) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue