mirror of
https://github.com/brunopostle/ifcurl.git
synced 2026-07-12 02:08:13 +00:00
Clicking a query results row highlighted the element in the 3D canvas but gave no feedback in the list itself. Track the active row and toggle an .active class, matching the existing storey-row pattern. Closes ifcurl-cmm Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
298 lines
12 KiB
HTML
298 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<!-- Copyright 2026 The Forgejo Authors. All rights reserved. -->
|
|
<!-- SPDX-License-Identifier: MIT -->
|
|
<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; }
|
|
#viewer { position: fixed; inset: 0; top: var(--toolbar-h, 72px); }
|
|
#toolbar {
|
|
position: fixed; top: 0; left: 0; right: 0; z-index: 10;
|
|
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);
|
|
}
|
|
.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 7px; outline: none; min-width: 0;
|
|
}
|
|
.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;
|
|
}
|
|
#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; }
|
|
/* 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); }
|
|
#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;
|
|
}
|
|
/* BCF panel */
|
|
#bcf-comment { flex: 3; }
|
|
#bcf-title { flex: 2; }
|
|
/* Status */
|
|
#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; }
|
|
#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;
|
|
}
|
|
body.drag-over::after {
|
|
content: "Drop ifc:// URL or .bcf file";
|
|
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;
|
|
}
|
|
/* 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; }
|
|
/* 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;
|
|
}
|
|
#query-table tbody tr:hover { background: rgba(255,255,255,0.07); }
|
|
#query-table tbody tr.active td { background: rgba(80,120,255,0.2); color: #a8c8ff; }
|
|
/* 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;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<script>
|
|
(function() {
|
|
try {
|
|
var snap = sessionStorage.getItem("viewerSnapshot");
|
|
if (!snap || !snap.startsWith("data:image/jpeg;base64,")) return;
|
|
if (!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>
|
|
<div id="toolbar">
|
|
<!-- 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">
|
|
<button id="copy-url-btn" class="tb-btn" title="Copy ifc:// URL to clipboard">⎘</button>
|
|
<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>
|
|
<button id="fit-btn" class="tb-btn" title="Fit model in view">fit</button>
|
|
<button id="meta-btn" class="tb-btn" title="Model overview: types and storeys">☰</button>
|
|
<button id="bcf-btn" class="tb-btn" title="Export current view as a BCF issue">BCF</button>
|
|
<button id="new-issue-btn" class="tb-btn" title="Open new issue with this view URL">Issue</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)">°
|
|
</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" list="ref-list"
|
|
placeholder="heads/main or commit hash" spellcheck="false">
|
|
<button id="ref-pick-btn" class="tb-btn" title="Browse branches and tags">▾</button>
|
|
</div>
|
|
<datalist id="ref-list"></datalist>
|
|
<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>
|
|
<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:2">
|
|
<label>selector</label>
|
|
<input id="selector-input" class="ti" type="text"
|
|
placeholder="IfcWall (viewer: type names only)"
|
|
title="Type-name union: IfcWall, IfcWall+IfcSlab. Property/attribute filters are applied server-side only."
|
|
spellcheck="false">
|
|
</div>
|
|
<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>
|
|
<option value="clash">clash</option>
|
|
</select>
|
|
</div>
|
|
<div class="field" style="flex:2">
|
|
<label>query</label>
|
|
<input id="query-input" class="ti" type="text"
|
|
placeholder="Name or Pset_X.Property"
|
|
title="Direct IFC attribute: Name, Description. Property set (dot notation required): Pset_WallCommon.FireRating, Qto_WallBaseQuantities.NetVolume"
|
|
spellcheck="false">
|
|
</div>
|
|
</div>
|
|
<!-- Row 3: BCF export/import form (hidden until BCF button clicked) -->
|
|
<div id="bcf-row" class="trow" style="display:none">
|
|
<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">
|
|
<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>
|
|
</div>
|
|
<div id="viewer"></div>
|
|
<div id="meta-panel" style="display:none"></div>
|
|
<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>
|
|
<div id="status"></div>
|
|
<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>
|
|
|
|
<script type="module" src="./viewer.js"></script>
|
|
</body>
|
|
</html>
|