mirror of
https://github.com/brunopostle/ifcurl.git
synced 2026-07-12 10:18:14 +00:00
viewer.html: structured form UI for ifc:// URL components
Adds a second toolbar row with labelled fields for repo, ref, path, and selector. Editing any field rebuilds and reloads the ifc:// URL. The raw URL in row 1 remains editable and copyable. Closes ifcurl-yju. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
05ff461ec5
commit
60596ef1a7
2 changed files with 153 additions and 67 deletions
|
|
@ -1,4 +1,4 @@
|
|||
{"id":"ifcurl-yju","title":"viewer.html: structured form UI for ifc:// URL components","description":"Replace the single ifc:// URL text input with individual labelled fields for each component: host/repository, ref (branch/tag/commit), path (file within repo), selector (IfcOpenShell selector), camera, fov/scale. Editing any field should update the ifc:// URL and reload the model. The raw ifc:// URL should still be visible/copyable. This makes the viewer usable without needing to hand-edit a URL string.","status":"open","priority":2,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-23T21:07:11Z","created_by":"Bruno Postle","updated_at":"2026-04-23T21:07:11Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"id":"ifcurl-yju","title":"viewer.html: structured form UI for ifc:// URL components","description":"Replace the single ifc:// URL text input with individual labelled fields for each component: host/repository, ref (branch/tag/commit), path (file within repo), selector (IfcOpenShell selector), camera, fov/scale. Editing any field should update the ifc:// URL and reload the model. The raw ifc:// URL should still be visible/copyable. This makes the viewer usable without needing to hand-edit a URL string.","status":"in_progress","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-23T21:07:11Z","created_by":"Bruno Postle","updated_at":"2026-04-23T21:12:42Z","started_at":"2026-04-23T21:12:42Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"id":"ifcurl-2zf","title":"viewer.html: FOV control and restore fov=/scale= from URL","description":"Two gaps: (1) No UI to change the camera FOV — add a control (e.g. a small number input or slider in the toolbar) so the user can adjust perspective FOV or orthographic scale. (2) applyCameraParam reads position/direction but ignores the fov= and scale= params written by syncCameraUrl — restore these when placing camera from URL so a shared link opens at exactly the recorded viewpoint.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-23T20:42:24Z","created_by":"Bruno Postle","updated_at":"2026-04-23T21:07:42Z","started_at":"2026-04-23T20:52:48Z","closed_at":"2026-04-23T21:07:42Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"id":"ifcurl-yga","title":"viewer.html: implement ifc:// selector= parameter","description":"The ifc:// URL spec supports a selector= param using IfcOpenShell selector syntax (e.g. selector=IfcWall, selector=IfcSlab.Name='Floor'). The viewer currently ignores it. Implement selector filtering in the browser: parse the selector= value, query matching elements from the loaded FragmentsModel, and apply highlight/isolate/ghost visibility to them. @thatopen/components has Highlighter and Hider components that may be usable. The selector syntax used is the IfcOpenShell Python selector syntax — a JS port or subset may be needed.","notes":"Selector syntax docs: https://docs.ifcopenshell.org/ifcopenshell-python/selector_syntax.html — Python implementation in ifcopenshell.util.selector module. Need a JS port or subset. Syntax includes: IfcType, .Attribute='value', #GlobalId, [pset.prop], boolean operators (+ for union, , for intersection within group).","status":"open","priority":2,"issue_type":"feature","owner":"bruno@postle.net","created_at":"2026-04-23T20:42:22Z","created_by":"Bruno Postle","updated_at":"2026-04-23T20:52:20Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
{"id":"ifcurl-set","title":"viewer.html: generalise toRawUrl() to support GitHub, GitLab etc.","description":"toRawUrl() currently only handles Forgejo/Gitea HTTPS URLs, mapping ifc://host/org/repo@hash?path=file to http(s)://host/org/repo/raw/commit/hash/file. This needs to: (1) detect the git host type and construct the correct raw file URL — GitHub → raw.githubusercontent.com/org/repo/hash/file; GitLab → host/org/repo/-/raw/hash/file; Forgejo/Gitea → host/org/repo/raw/commit/hash/file; (2) handle SSH ifc:// URLs (user@host form per the spec) by stripping the user@ prefix and rewriting to HTTPS for browser fetching — ifc://git@github.com/org/repo@hash becomes https://raw.githubusercontent.com/org/repo/hash/file.","status":"closed","priority":2,"issue_type":"feature","assignee":"Bruno Postle","owner":"bruno@postle.net","created_at":"2026-04-23T20:42:09Z","created_by":"Bruno Postle","updated_at":"2026-04-23T21:07:42Z","started_at":"2026-04-23T20:52:47Z","closed_at":"2026-04-23T21:07:42Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||
|
|
|
|||
|
|
@ -8,30 +8,38 @@
|
|||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { background: #1c1c1e; color: #f0f0f0;
|
||||
font-family: system-ui, sans-serif; overflow: hidden; }
|
||||
#viewer { position: fixed; inset: 0; top: var(--toolbar-h, 40px); }
|
||||
#viewer { position: fixed; inset: 0; top: var(--toolbar-h, 72px); }
|
||||
#toolbar {
|
||||
position: fixed; top: 0; left: 0; right: 0; z-index: 10;
|
||||
display: flex; align-items: center; gap: 8px; padding: 8px 12px;
|
||||
display: flex; flex-direction: column; gap: 6px;
|
||||
padding: 8px 12px;
|
||||
background: rgba(28,28,30,0.85); backdrop-filter: blur(8px);
|
||||
border-bottom: 1px solid rgba(255,255,255,0.08);
|
||||
}
|
||||
#url-input {
|
||||
flex: 1; font-size: 12px; color: #f0f0f0; font-family: monospace;
|
||||
.trow { display: flex; align-items: center; gap: 6px; }
|
||||
/* Shared input style */
|
||||
.ti {
|
||||
font-size: 12px; color: #f0f0f0; font-family: monospace;
|
||||
background: rgba(255,255,255,0.07); border: 1px solid rgba(255,255,255,0.12);
|
||||
border-radius: 4px; padding: 3px 8px; outline: none; min-width: 0;
|
||||
border-radius: 4px; padding: 3px 7px; outline: none; min-width: 0;
|
||||
}
|
||||
#url-input:focus { border-color: rgba(255,255,255,0.3); }
|
||||
#url-input::placeholder { color: #606068; }
|
||||
.ti:focus { border-color: rgba(255,255,255,0.3); }
|
||||
.ti::placeholder { color: #505058; }
|
||||
/* Row 1 */
|
||||
#url-input { flex: 1; }
|
||||
#fov-wrap {
|
||||
display: flex; align-items: center; gap: 4px; flex-shrink: 0;
|
||||
font-size: 11px; color: #a0a0a8; white-space: nowrap;
|
||||
font-size: 11px; color: #a0a0a8;
|
||||
}
|
||||
#fov-input {
|
||||
width: 44px; font-size: 11px; color: #f0f0f0; font-family: monospace;
|
||||
background: rgba(255,255,255,0.07); border: 1px solid rgba(255,255,255,0.12);
|
||||
border-radius: 4px; padding: 3px 6px; outline: none; text-align: right;
|
||||
}
|
||||
#fov-input:focus { border-color: rgba(255,255,255,0.3); }
|
||||
#fov-input { width: 44px; text-align: right; }
|
||||
/* Row 2 structured fields */
|
||||
.field { display: flex; align-items: center; gap: 4px; min-width: 0; }
|
||||
.field label { font-size: 10px; color: #808088; flex-shrink: 0; }
|
||||
#repo-input { flex: 3; }
|
||||
#ref-input { flex: 2; }
|
||||
#path-input { flex: 2; }
|
||||
#selector-input { flex: 1; }
|
||||
/* Status */
|
||||
#status {
|
||||
position: fixed; bottom: 12px; left: 50%;
|
||||
transform: translateX(-50%); z-index: 10;
|
||||
|
|
@ -64,9 +72,38 @@
|
|||
</head>
|
||||
<body>
|
||||
<div id="toolbar">
|
||||
<input id="url-input" type="text" placeholder="Paste ifc:// URL and press Enter" spellcheck="false">
|
||||
<div id="fov-wrap">
|
||||
FOV <input id="fov-input" type="number" min="10" max="120" step="1" title="Field of view (degrees)">°
|
||||
<!-- Row 1: raw URL + FOV -->
|
||||
<div class="trow">
|
||||
<input id="url-input" class="ti" type="text"
|
||||
placeholder="Paste ifc:// URL and press Enter, or fill fields below"
|
||||
spellcheck="false">
|
||||
<div id="fov-wrap">
|
||||
FOV <input id="fov-input" class="ti" type="number" min="10" max="120" step="1"
|
||||
title="Field of view (degrees)">°
|
||||
</div>
|
||||
</div>
|
||||
<!-- Row 2: structured fields -->
|
||||
<div class="trow">
|
||||
<div class="field" style="flex:3">
|
||||
<label>repo</label>
|
||||
<input id="repo-input" class="ti" type="text"
|
||||
placeholder="host/org/repo or git@host/org/repo" spellcheck="false">
|
||||
</div>
|
||||
<div class="field" style="flex:2">
|
||||
<label>ref</label>
|
||||
<input id="ref-input" class="ti" type="text"
|
||||
placeholder="heads/main or commit hash" spellcheck="false">
|
||||
</div>
|
||||
<div class="field" style="flex:2">
|
||||
<label>path</label>
|
||||
<input id="path-input" class="ti" type="text"
|
||||
placeholder="model.ifc" spellcheck="false">
|
||||
</div>
|
||||
<div class="field" style="flex:1">
|
||||
<label>selector</label>
|
||||
<input id="selector-input" class="ti" type="text"
|
||||
placeholder="IfcWall" spellcheck="false">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="viewer"></div>
|
||||
|
|
@ -82,17 +119,79 @@
|
|||
const urlInput = document.getElementById("url-input");
|
||||
const fovInput = document.getElementById("fov-input");
|
||||
const toolbar = document.getElementById("toolbar");
|
||||
const repoInput = document.getElementById("repo-input");
|
||||
const refInput = document.getElementById("ref-input");
|
||||
const pathInput = document.getElementById("path-input");
|
||||
const selectorInput = document.getElementById("selector-input");
|
||||
|
||||
// Keep viewer below the toolbar so the canvas never overlaps the input.
|
||||
// Keep viewer below the full toolbar height.
|
||||
const resizeObs = new ResizeObserver(() => {
|
||||
document.documentElement.style.setProperty("--toolbar-h", toolbar.offsetHeight + "px");
|
||||
});
|
||||
resizeObs.observe(toolbar);
|
||||
|
||||
// Module-level references set once the world is initialised.
|
||||
let cameraControls = null;
|
||||
let threeCamera = null;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Parse an ifc:// URL into its components.
|
||||
// -----------------------------------------------------------------------
|
||||
function parseIfcUrl(raw) {
|
||||
if (!raw?.startsWith("ifc://")) return null;
|
||||
const body = raw.slice("ifc://".length);
|
||||
|
||||
const slashIdx = body.indexOf("/");
|
||||
const authority = slashIdx < 0 ? body : body.slice(0, slashIdx);
|
||||
const pathAndQuery = slashIdx < 0 ? "" : body.slice(slashIdx + 1);
|
||||
|
||||
const atInAuth = authority.indexOf("@");
|
||||
const userAt = atInAuth < 0 ? "" : authority.slice(0, atInAuth + 1);
|
||||
const host = atInAuth < 0 ? authority : authority.slice(atInAuth + 1);
|
||||
|
||||
const qIdx = pathAndQuery.indexOf("?");
|
||||
const pathPart = qIdx < 0 ? pathAndQuery : pathAndQuery.slice(0, qIdx);
|
||||
const qs = new URLSearchParams(qIdx < 0 ? "" : pathAndQuery.slice(qIdx + 1));
|
||||
|
||||
const atIdx = pathPart.lastIndexOf("@");
|
||||
const repoSuffix = atIdx < 0 ? pathPart : pathPart.slice(0, atIdx);
|
||||
const ref = atIdx < 0 ? "" : pathPart.slice(atIdx + 1);
|
||||
|
||||
return {
|
||||
repo: userAt + host + "/" + repoSuffix,
|
||||
host, userAt, repoSuffix, ref,
|
||||
path: qs.get("path") ?? "",
|
||||
selector: qs.get("selector") ?? "",
|
||||
camera: qs.get("camera") ?? "",
|
||||
fov: qs.get("fov") ?? "",
|
||||
scale: qs.get("scale") ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Build an ifc:// URL from structured fields (no camera — will fit to box).
|
||||
// -----------------------------------------------------------------------
|
||||
function buildIfcUrl() {
|
||||
const repo = repoInput.value.trim();
|
||||
const ref = refInput.value.trim();
|
||||
const path = pathInput.value.trim();
|
||||
const sel = selectorInput.value.trim();
|
||||
if (!repo || !ref) return null;
|
||||
|
||||
const qs = new URLSearchParams();
|
||||
if (path) qs.set("path", path);
|
||||
if (sel) qs.set("selector", sel);
|
||||
const qsStr = qs.toString().replace(/%2C/g, ",");
|
||||
return `ifc://${repo}@${ref}${qsStr ? "?" + qsStr : ""}`;
|
||||
}
|
||||
|
||||
// Populate structured fields from the current ifcUrl.
|
||||
const parsed = parseIfcUrl(ifcUrl);
|
||||
if (parsed) {
|
||||
repoInput.value = parsed.repo;
|
||||
refInput.value = parsed.ref;
|
||||
pathInput.value = parsed.path;
|
||||
selectorInput.value = parsed.selector;
|
||||
}
|
||||
urlInput.value = ifcUrl;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
|
|
@ -108,10 +207,18 @@
|
|||
if (e.key === "Enter") { const v = urlInput.value.trim(); if (v) loadUrl(v); }
|
||||
});
|
||||
|
||||
// Disable camera controls while inputs have focus so keystrokes reach them.
|
||||
function onInputFocus() { if (cameraControls) cameraControls.enabled = false; }
|
||||
function onInputBlur() { if (cameraControls) cameraControls.enabled = true; }
|
||||
for (const el of [urlInput, fovInput]) {
|
||||
// Pressing Enter in any structured field rebuilds the URL and navigates.
|
||||
for (const el of [repoInput, refInput, pathInput, selectorInput]) {
|
||||
el.addEventListener("keydown", e => {
|
||||
if (e.key === "Enter") { const u = buildIfcUrl(); if (u) loadUrl(u); }
|
||||
});
|
||||
}
|
||||
|
||||
// Disable camera controls while any input has focus.
|
||||
function onInputFocus() { if (cameraControls) cameraControls.enabled = false; }
|
||||
function onInputBlur() { if (cameraControls) cameraControls.enabled = true; }
|
||||
const allInputs = [urlInput, fovInput, repoInput, refInput, pathInput, selectorInput];
|
||||
for (const el of allInputs) {
|
||||
el.addEventListener("focus", onInputFocus);
|
||||
el.addEventListener("blur", onInputBlur);
|
||||
el.addEventListener("pointerdown", e => e.stopPropagation());
|
||||
|
|
@ -128,8 +235,7 @@
|
|||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Drag-and-drop: accept ifc:// URLs dragged onto the page.
|
||||
// Also handles viewer page URLs containing ?url=ifc://...
|
||||
// Drag-and-drop: accept ifc:// URLs or viewer page URLs.
|
||||
// -----------------------------------------------------------------------
|
||||
document.body.addEventListener("dragover", e => {
|
||||
e.preventDefault();
|
||||
|
|
@ -160,7 +266,7 @@
|
|||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Coordinate transforms: IFC world space (Z-up) ↔ Three.js (Y-up).
|
||||
// Coordinate transforms: IFC (Z-up) ↔ Three.js (Y-up).
|
||||
// IFC → Three.js: (x, y, z) → (x, z, -y)
|
||||
// Three.js → IFC: (x, y, z) → (x, -z, y)
|
||||
// -----------------------------------------------------------------------
|
||||
|
|
@ -168,8 +274,8 @@
|
|||
const toThree = (x, y, z) => new THREE.Vector3(x, z, -y);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Rebuild the ifc:// URL with the current camera viewpoint (IFC coords)
|
||||
// and push to the toolbar and browser address bar.
|
||||
// Sync camera viewpoint (in IFC coords) into the URL and toolbar row 1.
|
||||
// The structured fields in row 2 are not affected by camera movement.
|
||||
// -----------------------------------------------------------------------
|
||||
function syncCameraUrl(camera) {
|
||||
const pos = camera.position;
|
||||
|
|
@ -177,7 +283,7 @@
|
|||
camera.getWorldDirection(dir);
|
||||
const up = camera.up;
|
||||
|
||||
const f = v => parseFloat(v.toFixed(4));
|
||||
const f = v => parseFloat(v.toFixed(4));
|
||||
const ip = toIfc(pos), id = toIfc(dir), iu = toIfc(up);
|
||||
const cameraParam = [
|
||||
f(ip.x), f(ip.y), f(ip.z),
|
||||
|
|
@ -208,13 +314,13 @@
|
|||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Place the camera from the ifc:// URL's camera= / fov= / scale= params.
|
||||
// Returns true if a camera param was found and applied.
|
||||
// Place the camera from ifc:// URL camera=/fov=/scale= params.
|
||||
// -----------------------------------------------------------------------
|
||||
async function applyCameraParam(controls, camera) {
|
||||
const qIdx = ifcUrl.indexOf("?");
|
||||
if (qIdx < 0) return false;
|
||||
const qs = new URLSearchParams(ifcUrl.slice(qIdx + 1));
|
||||
|
||||
const camStr = qs.get("camera");
|
||||
if (!camStr) return false;
|
||||
const v = camStr.split(",").map(Number);
|
||||
|
|
@ -235,38 +341,28 @@
|
|||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Convert an ifc:// URL to an HTTPS raw file URL for browser fetching.
|
||||
//
|
||||
// Handles:
|
||||
// - Optional SSH user prefix: ifc://git@host/... → strip user@
|
||||
// - GitHub: → https://raw.githubusercontent.com/org/repo/ref/path
|
||||
// - GitLab: → https://host/org/repo/-/raw/ref/path
|
||||
// - Forgejo/Gitea: → https://host/org/repo/raw/{commit|branch|tag}/ref/path
|
||||
// Convert an ifc:// URL to an HTTPS raw file URL.
|
||||
// Handles SSH user@ prefix, GitHub, GitLab, and Forgejo/Gitea.
|
||||
// -----------------------------------------------------------------------
|
||||
function toRawUrl(raw) {
|
||||
const body = raw.slice("ifc://".length);
|
||||
|
||||
// Split authority (host) from path+query at the first "/"
|
||||
const slashIdx = body.indexOf("/");
|
||||
const slashIdx = body.indexOf("/");
|
||||
const authority = slashIdx < 0 ? body : body.slice(0, slashIdx);
|
||||
const pathAndQuery = slashIdx < 0 ? "" : body.slice(slashIdx + 1);
|
||||
|
||||
// Strip optional SSH user@ from authority
|
||||
const atInAuth = authority.indexOf("@");
|
||||
const host = atInAuth < 0 ? authority : authority.slice(atInAuth + 1);
|
||||
|
||||
// Split query string
|
||||
const qIdx = pathAndQuery.indexOf("?");
|
||||
const qIdx = pathAndQuery.indexOf("?");
|
||||
const pathPart = qIdx < 0 ? pathAndQuery : pathAndQuery.slice(0, qIdx);
|
||||
const qs = new URLSearchParams(qIdx < 0 ? "" : pathAndQuery.slice(qIdx + 1));
|
||||
const qs = new URLSearchParams(qIdx < 0 ? "" : pathAndQuery.slice(qIdx + 1));
|
||||
const filePath = qs.get("path") ?? "";
|
||||
|
||||
// Split repoPath from ref at the last "@" in the path portion
|
||||
const atIdx = pathPart.lastIndexOf("@");
|
||||
const atIdx = pathPart.lastIndexOf("@");
|
||||
const repoPath = atIdx < 0 ? pathPart : pathPart.slice(0, atIdx);
|
||||
const ref = atIdx < 0 ? "" : pathPart.slice(atIdx + 1);
|
||||
|
||||
// Normalise ref for hosts that don't use gitnamespace prefixes
|
||||
const plainRef = ref.startsWith("heads/") ? ref.slice(6)
|
||||
: ref.startsWith("tags/") ? ref.slice(5)
|
||||
: ref;
|
||||
|
|
@ -274,13 +370,10 @@
|
|||
if (host === "github.com" || host.endsWith(".github.com")) {
|
||||
return `https://raw.githubusercontent.com/${repoPath}/${plainRef}/${filePath}`;
|
||||
}
|
||||
|
||||
if (host === "gitlab.com" || host.includes("gitlab")) {
|
||||
const proto = host.startsWith("localhost") ? "http" : "https";
|
||||
return `${proto}://${host}/${repoPath}/-/raw/${plainRef}/${filePath}`;
|
||||
}
|
||||
|
||||
// Forgejo / Gitea: map gitnamespace ref to URL segment
|
||||
const refSeg = ref.startsWith("heads/") ? "branch/" + ref.slice(6)
|
||||
: ref.startsWith("tags/") ? "tag/" + ref.slice(5)
|
||||
: "commit/" + ref;
|
||||
|
|
@ -293,13 +386,12 @@
|
|||
// -----------------------------------------------------------------------
|
||||
async function main() {
|
||||
if (!ifcUrl) {
|
||||
statusEl.textContent = "Paste an ifc:// URL above or drag one onto the page.";
|
||||
statusEl.textContent = "Fill in the fields above and press Enter, or paste an ifc:// URL.";
|
||||
return;
|
||||
}
|
||||
|
||||
statusEl.textContent = "Loading…";
|
||||
|
||||
// --- World setup ---
|
||||
const components = new OBC.Components();
|
||||
const worlds = components.get(OBC.Worlds);
|
||||
const world = worlds.create();
|
||||
|
|
@ -314,25 +406,21 @@
|
|||
cameraControls = world.camera.controls;
|
||||
threeCamera = world.camera.three;
|
||||
|
||||
// --- FragmentsManager ---
|
||||
// Firefox blocks cross-origin module workers; wrap in a blob URL.
|
||||
const fragments = components.get(OBC.FragmentsManager);
|
||||
const fragments = components.get(OBC.FragmentsManager);
|
||||
const workerResp = await fetch(
|
||||
"https://unpkg.com/@thatopen/fragments@3.3.1/dist/Worker/worker.mjs"
|
||||
);
|
||||
const workerUrl = URL.createObjectURL(
|
||||
fragments.init(URL.createObjectURL(
|
||||
new Blob([await workerResp.text()], { type: "application/javascript" })
|
||||
);
|
||||
fragments.init(workerUrl);
|
||||
));
|
||||
|
||||
// --- IfcLoader ---
|
||||
const ifcLoader = components.get(OBC.IfcLoader);
|
||||
await ifcLoader.setup({
|
||||
autoSetWasm: false,
|
||||
wasm: { path: "https://unpkg.com/web-ifc@0.0.75/", absolute: true },
|
||||
});
|
||||
|
||||
// --- Fetch IFC bytes ---
|
||||
statusEl.textContent = "Fetching IFC file…";
|
||||
let buffer;
|
||||
try {
|
||||
|
|
@ -344,22 +432,20 @@
|
|||
return;
|
||||
}
|
||||
|
||||
// --- Parse and render ---
|
||||
statusEl.textContent = "Parsing IFC…";
|
||||
try {
|
||||
const model = await ifcLoader.load(buffer, true, "model");
|
||||
const model = await ifcLoader.load(buffer, true, "model");
|
||||
world.scene.three.add(model.object);
|
||||
|
||||
const box = model.box;
|
||||
const hasCam = await applyCameraParam(world.camera.controls, threeCamera);
|
||||
if (!hasCam && !box.isEmpty()) {
|
||||
await world.camera.controls.fitToBox(box, false);
|
||||
const hasCam = await applyCameraParam(cameraControls, threeCamera);
|
||||
if (!hasCam && !model.box.isEmpty()) {
|
||||
await cameraControls.fitToBox(model.box, false);
|
||||
}
|
||||
model.useCamera(threeCamera);
|
||||
await fragments.core.update(true);
|
||||
|
||||
syncCameraUrl(threeCamera);
|
||||
world.camera.controls.addEventListener("rest", () => syncCameraUrl(threeCamera));
|
||||
cameraControls.addEventListener("rest", () => syncCameraUrl(threeCamera));
|
||||
|
||||
statusEl.textContent = "";
|
||||
} catch (err) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue