2026-04-23 20:48:36 +01:00
|
|
|
<!DOCTYPE html>
|
2026-04-24 08:30:56 +01:00
|
|
|
<!-- Copyright 2026 The Forgejo Authors. All rights reserved. -->
|
|
|
|
|
<!-- SPDX-License-Identifier: MIT -->
|
2026-04-23 20:48:36 +01:00
|
|
|
<html lang="en">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="utf-8">
|
|
|
|
|
<title>IFC Viewer</title>
|
|
|
|
|
<link rel="icon" href="data:,">
|
|
|
|
|
<style>
|
|
|
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
|
body { background: #1c1c1e; color: #f0f0f0;
|
|
|
|
|
font-family: system-ui, sans-serif; overflow: hidden; }
|
2026-04-23 22:32:09 +01:00
|
|
|
#viewer { position: fixed; inset: 0; top: var(--toolbar-h, 72px); }
|
2026-04-23 20:48:36 +01:00
|
|
|
#toolbar {
|
|
|
|
|
position: fixed; top: 0; left: 0; right: 0; z-index: 10;
|
2026-04-23 22:32:09 +01:00
|
|
|
display: flex; flex-direction: column; gap: 6px;
|
|
|
|
|
padding: 8px 12px;
|
2026-04-23 20:48:36 +01:00
|
|
|
background: rgba(28,28,30,0.85); backdrop-filter: blur(8px);
|
|
|
|
|
border-bottom: 1px solid rgba(255,255,255,0.08);
|
|
|
|
|
}
|
2026-04-23 22:32:09 +01:00
|
|
|
.trow { display: flex; align-items: center; gap: 6px; }
|
|
|
|
|
/* Shared input style */
|
|
|
|
|
.ti {
|
|
|
|
|
font-size: 12px; color: #f0f0f0; font-family: monospace;
|
2026-04-23 21:15:30 +01:00
|
|
|
background: rgba(255,255,255,0.07); border: 1px solid rgba(255,255,255,0.12);
|
2026-04-23 22:32:09 +01:00
|
|
|
border-radius: 4px; padding: 3px 7px; outline: none; min-width: 0;
|
2026-04-23 20:48:36 +01:00
|
|
|
}
|
2026-04-23 22:32:09 +01:00
|
|
|
.ti:focus { border-color: rgba(255,255,255,0.3); }
|
|
|
|
|
.ti::placeholder { color: #505058; }
|
|
|
|
|
/* Row 1 */
|
|
|
|
|
#url-input { flex: 1; }
|
2026-04-23 22:08:53 +01:00
|
|
|
#fov-wrap {
|
|
|
|
|
display: flex; align-items: center; gap: 4px; flex-shrink: 0;
|
2026-04-23 22:32:09 +01:00
|
|
|
font-size: 11px; color: #a0a0a8;
|
2026-04-23 22:08:53 +01:00
|
|
|
}
|
2026-04-23 22:32:09 +01:00
|
|
|
#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; }
|
2026-04-24 07:24:06 +01:00
|
|
|
/* 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); }
|
2026-04-25 19:19:37 +01:00
|
|
|
#ref-badge {
|
|
|
|
|
font-size: 10px; color: #f0a050; font-family: monospace;
|
|
|
|
|
background: rgba(255,160,80,0.12); border: 1px solid rgba(255,160,80,0.3);
|
|
|
|
|
border-radius: 4px; padding: 2px 6px; white-space: nowrap; flex-shrink: 0;
|
|
|
|
|
cursor: default;
|
|
|
|
|
}
|
2026-04-24 09:59:22 +01:00
|
|
|
/* BCF panel */
|
|
|
|
|
#bcf-comment { flex: 3; }
|
|
|
|
|
#bcf-title { flex: 2; }
|
2026-04-23 22:32:09 +01:00
|
|
|
/* Status */
|
2026-04-23 20:48:36 +01:00
|
|
|
#status {
|
|
|
|
|
position: fixed; bottom: 12px; left: 50%;
|
|
|
|
|
transform: translateX(-50%); z-index: 10;
|
|
|
|
|
font-size: 13px; color: #a0a0a8;
|
|
|
|
|
background: rgba(28,28,30,0.85);
|
|
|
|
|
padding: 4px 12px; border-radius: 4px;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
#status:empty { display: none; }
|
2026-04-26 08:17:34 +01:00
|
|
|
#snapshot-overlay {
|
|
|
|
|
position: fixed; inset: 0; top: var(--toolbar-h, 72px); z-index: 4;
|
|
|
|
|
background: #1c1c1e center/contain no-repeat;
|
|
|
|
|
transition: opacity 0.5s ease;
|
|
|
|
|
}
|
2026-04-23 21:15:30 +01:00
|
|
|
body.drag-over::after {
|
2026-04-24 14:20:17 +01:00
|
|
|
content: "Drop ifc:// URL or .bcf file";
|
2026-04-23 21:15:30 +01:00
|
|
|
position: fixed; inset: 0; z-index: 100;
|
|
|
|
|
display: flex; align-items: center; justify-content: center;
|
|
|
|
|
font-size: 24px; color: #f0f0f0;
|
|
|
|
|
background: rgba(28,28,30,0.7);
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
2026-04-25 20:04:43 +01:00
|
|
|
/* Model metadata panel */
|
|
|
|
|
#meta-panel {
|
|
|
|
|
position: fixed; bottom: 44px; left: 12px; 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: 180px; max-width: 260px;
|
|
|
|
|
max-height: calc(100vh - 130px); overflow-y: auto;
|
|
|
|
|
}
|
|
|
|
|
#meta-panel h3 {
|
|
|
|
|
font-size: 10px; color: #606068;
|
|
|
|
|
text-transform: uppercase; letter-spacing: 0.06em;
|
|
|
|
|
margin-top: 10px; margin-bottom: 3px;
|
|
|
|
|
}
|
|
|
|
|
#meta-panel h3:first-child { margin-top: 0; }
|
|
|
|
|
.meta-row {
|
|
|
|
|
display: flex; align-items: baseline; gap: 4px;
|
|
|
|
|
margin-top: 2px; padding: 1px 3px; border-radius: 3px;
|
|
|
|
|
}
|
|
|
|
|
.meta-row.storey-row { cursor: pointer; }
|
|
|
|
|
.meta-row.storey-row:hover { background: rgba(255,255,255,0.07); }
|
|
|
|
|
.meta-row.storey-row.active { background: rgba(80,120,255,0.2);
|
|
|
|
|
color: #a8c8ff; }
|
|
|
|
|
.meta-name {
|
|
|
|
|
font-size: 11px; color: #d0d0d8; font-family: monospace;
|
|
|
|
|
flex: 1; min-width: 0; overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis; white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
.meta-row.storey-row.active .meta-name { color: #a8c8ff; }
|
|
|
|
|
.meta-count { font-size: 10px; color: #808088; flex-shrink: 0; }
|
2026-05-09 11:09:55 +00:00
|
|
|
/* 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;
|
|
|
|
|
}
|
2026-04-24 12:50:26 +01:00
|
|
|
/* Element properties panel */
|
|
|
|
|
#props-panel {
|
|
|
|
|
position: fixed; bottom: 44px; right: 12px; 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: 200px; max-width: 320px;
|
|
|
|
|
}
|
|
|
|
|
#props-panel-header {
|
|
|
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
}
|
|
|
|
|
#props-panel-header span {
|
|
|
|
|
font-size: 10px; color: #606068;
|
|
|
|
|
text-transform: uppercase; letter-spacing: 0.06em;
|
|
|
|
|
}
|
|
|
|
|
#props-panel .prop-row {
|
|
|
|
|
display: flex; align-items: baseline; gap: 6px; margin-top: 3px;
|
|
|
|
|
}
|
|
|
|
|
#props-panel .prop-label {
|
|
|
|
|
font-size: 10px; color: #808088; flex-shrink: 0; width: 46px;
|
|
|
|
|
}
|
|
|
|
|
#props-panel .prop-value {
|
|
|
|
|
font-size: 12px; color: #f0f0f0; font-family: monospace;
|
|
|
|
|
flex: 1; word-break: break-all;
|
|
|
|
|
}
|
2026-04-23 20:48:36 +01:00
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
2026-05-17 10:23:29 +01:00
|
|
|
<script>
|
|
|
|
|
(function() {
|
|
|
|
|
try {
|
|
|
|
|
var snap = sessionStorage.getItem("viewerSnapshot");
|
|
|
|
|
if (!snap || !new URLSearchParams(location.search).get("url")) return;
|
|
|
|
|
var el = document.createElement("div");
|
|
|
|
|
el.id = "snapshot-overlay";
|
|
|
|
|
el.style.backgroundImage = "url(" + snap + ")";
|
|
|
|
|
document.body.appendChild(el);
|
|
|
|
|
} catch(_) {}
|
|
|
|
|
})();
|
|
|
|
|
</script>
|
2026-04-23 20:48:36 +01:00
|
|
|
<div id="toolbar">
|
2026-04-23 22:32:09 +01:00
|
|
|
<!-- 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">
|
2026-04-26 08:49:47 +01:00
|
|
|
<button id="copy-url-btn" class="tb-btn" title="Copy ifc:// URL to clipboard">⎘</button>
|
2026-04-24 07:24:06 +01:00
|
|
|
<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>
|
2026-04-26 08:17:34 +01:00
|
|
|
<button id="fit-btn" class="tb-btn" title="Fit model in view">fit</button>
|
2026-04-25 20:04:43 +01:00
|
|
|
<button id="meta-btn" class="tb-btn" title="Model overview: types and storeys">☰</button>
|
2026-04-24 09:59:22 +01:00
|
|
|
<button id="bcf-btn" class="tb-btn" title="Export current view as a BCF issue">BCF</button>
|
2026-04-24 12:50:26 +01:00
|
|
|
<button id="new-issue-btn" class="tb-btn" title="Open new issue with this view URL">Issue</button>
|
2026-04-23 22:32:09 +01:00
|
|
|
<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>
|
2026-04-25 20:29:17 +01:00
|
|
|
<input id="ref-input" class="ti" type="text" list="ref-list"
|
2026-04-23 22:32:09 +01:00
|
|
|
placeholder="heads/main or commit hash" spellcheck="false">
|
2026-04-25 20:29:17 +01:00
|
|
|
<button id="ref-pick-btn" class="tb-btn" title="Browse branches and tags">▾</button>
|
2026-04-23 22:32:09 +01:00
|
|
|
</div>
|
2026-04-25 20:29:17 +01:00
|
|
|
<datalist id="ref-list"></datalist>
|
2026-04-25 19:19:37 +01:00
|
|
|
<span id="ref-badge" style="display:none" title="Pinned to this commit — not tracking branch tip"></span>
|
|
|
|
|
<button id="ref-switch-btn" class="tb-btn" style="display:none"></button>
|
2026-04-23 22:32:09 +01:00
|
|
|
<div class="field" style="flex:2">
|
|
|
|
|
<label>path</label>
|
|
|
|
|
<input id="path-input" class="ti" type="text"
|
|
|
|
|
placeholder="model.ifc" spellcheck="false">
|
|
|
|
|
</div>
|
2026-04-26 08:49:47 +01:00
|
|
|
<div class="field" style="flex:2">
|
2026-04-23 22:32:09 +01:00
|
|
|
<label>selector</label>
|
|
|
|
|
<input id="selector-input" class="ti" type="text"
|
2026-04-24 15:45:31 +01:00
|
|
|
placeholder="IfcWall (viewer: type names only)"
|
|
|
|
|
title="Type-name union: IfcWall, IfcWall+IfcSlab. Property/attribute filters are applied server-side only."
|
|
|
|
|
spellcheck="false">
|
2026-04-23 22:32:09 +01:00
|
|
|
</div>
|
2026-04-26 08:17:34 +01:00
|
|
|
<div class="field" id="visibility-field" style="display:none">
|
|
|
|
|
<label>show</label>
|
|
|
|
|
<select id="visibility-select" class="ti" title="How to display non-selected elements">
|
|
|
|
|
<option value="highlight">highlight</option>
|
|
|
|
|
<option value="ghost">ghost</option>
|
|
|
|
|
<option value="isolate">isolate</option>
|
2026-05-09 09:03:11 +00:00
|
|
|
<option value="clash">clash</option>
|
2026-04-26 08:17:34 +01:00
|
|
|
</select>
|
|
|
|
|
</div>
|
2026-05-09 11:09:55 +00:00
|
|
|
<div class="field" style="flex:2">
|
|
|
|
|
<label>query</label>
|
|
|
|
|
<input id="query-input" class="ti" type="text"
|
2026-05-09 15:26:07 +00:00
|
|
|
placeholder="Name or Pset_X.Property"
|
|
|
|
|
title="Direct IFC attribute: Name, Description. Property set (dot notation required): Pset_WallCommon.FireRating, Qto_WallBaseQuantities.NetVolume"
|
2026-05-09 11:09:55 +00:00
|
|
|
spellcheck="false">
|
|
|
|
|
</div>
|
2026-04-23 22:08:53 +01:00
|
|
|
</div>
|
2026-04-24 14:20:17 +01:00
|
|
|
<!-- Row 3: BCF export/import form (hidden until BCF button clicked) -->
|
2026-04-24 09:59:22 +01:00
|
|
|
<div id="bcf-row" class="trow" style="display:none">
|
2026-04-24 14:20:17 +01:00
|
|
|
<button id="bcf-import-btn" class="tb-btn" title="Import a .bcf viewpoint to restore camera and selection">Import .bcf</button>
|
|
|
|
|
<input type="file" id="bcf-import-input" accept=".bcf" style="display:none">
|
2026-04-24 09:59:22 +01:00
|
|
|
<div class="field" style="flex:2">
|
|
|
|
|
<label>title</label>
|
|
|
|
|
<input id="bcf-title" class="ti" type="text" placeholder="IFC View" spellcheck="false">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="field" style="flex:3">
|
|
|
|
|
<label>comment</label>
|
|
|
|
|
<input id="bcf-comment" class="ti" type="text" placeholder="Optional note" spellcheck="false">
|
|
|
|
|
</div>
|
|
|
|
|
<button id="bcf-cancel-btn" class="tb-btn">✕</button>
|
|
|
|
|
<button id="bcf-export-btn" class="tb-btn">Export .bcf</button>
|
|
|
|
|
</div>
|
2026-04-23 20:48:36 +01:00
|
|
|
</div>
|
|
|
|
|
<div id="viewer"></div>
|
2026-04-25 20:04:43 +01:00
|
|
|
<div id="meta-panel" style="display:none"></div>
|
2026-05-09 11:09:55 +00:00
|
|
|
<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>
|
2026-04-23 21:15:30 +01:00
|
|
|
<div id="status"></div>
|
2026-04-24 12:50:26 +01:00
|
|
|
<div id="props-panel" style="display:none">
|
|
|
|
|
<div id="props-panel-header">
|
|
|
|
|
<span>Element</span>
|
|
|
|
|
<button id="props-close-btn" class="tb-btn">✕</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="prop-row">
|
|
|
|
|
<span class="prop-label">type</span>
|
|
|
|
|
<span id="props-type" class="prop-value"></span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="prop-row">
|
|
|
|
|
<span class="prop-label">name</span>
|
|
|
|
|
<span id="props-name" class="prop-value"></span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="prop-row">
|
|
|
|
|
<span class="prop-label">GlobalId</span>
|
|
|
|
|
<span id="props-guid" class="prop-value"></span>
|
|
|
|
|
<button id="props-copy-btn" class="tb-btn" title="Copy GlobalId to clipboard">⎘</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-04-23 20:48:36 +01:00
|
|
|
|
2026-04-26 15:50:28 +01:00
|
|
|
<script type="module" src="./viewer.js"></script>
|
2026-04-23 20:48:36 +01:00
|
|
|
</body>
|
|
|
|
|
</html>
|