effibemviewer 0.1.1__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- effibemviewer/__init__.py +7 -3
- effibemviewer/__main__.py +76 -5
- effibemviewer/gltf.py +110 -33
- effibemviewer/templates/effibemviewer.css.j2 +120 -0
- effibemviewer/templates/effibemviewer.html.j2 +162 -0
- effibemviewer/templates/effibemviewer.js.j2 +630 -0
- effibemviewer/templates/gltf_viewer.html.j2 +137 -584
- {effibemviewer-0.1.1.dist-info → effibemviewer-0.2.0.dist-info}/METADATA +1 -1
- effibemviewer-0.2.0.dist-info/RECORD +12 -0
- effibemviewer-0.1.1.dist-info/RECORD +0 -9
- {effibemviewer-0.1.1.dist-info → effibemviewer-0.2.0.dist-info}/WHEEL +0 -0
- {effibemviewer-0.1.1.dist-info → effibemviewer-0.2.0.dist-info}/entry_points.txt +0 -0
- {effibemviewer-0.1.1.dist-info → effibemviewer-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,15 +1,24 @@
|
|
|
1
|
-
<script type="importmap">
|
|
2
|
-
{
|
|
3
|
-
"imports": {
|
|
4
|
-
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
|
|
5
|
-
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
|
|
6
|
-
}
|
|
7
|
-
}
|
|
8
|
-
</script>
|
|
9
|
-
|
|
10
1
|
<style>
|
|
11
|
-
#
|
|
12
|
-
|
|
2
|
+
#header {
|
|
3
|
+
display: flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
padding: 8px 16px;
|
|
6
|
+
background: #fff;
|
|
7
|
+
border-bottom: 1px solid #e0e0e0;
|
|
8
|
+
font-family: sans-serif;
|
|
9
|
+
}
|
|
10
|
+
#header img {
|
|
11
|
+
height: 32px;
|
|
12
|
+
margin-right: 12px;
|
|
13
|
+
}
|
|
14
|
+
#header h1 {
|
|
15
|
+
margin: 0;
|
|
16
|
+
font-size: 18px;
|
|
17
|
+
font-weight: 600;
|
|
18
|
+
color: #333;
|
|
19
|
+
}
|
|
20
|
+
.effibem-viewer { width: 100%; height: {{ height }}; position: relative; }
|
|
21
|
+
.effibem-viewer .controls {
|
|
13
22
|
position: absolute;
|
|
14
23
|
top: 10px;
|
|
15
24
|
right: 10px;
|
|
@@ -20,9 +29,9 @@
|
|
|
20
29
|
font-size: 12px;
|
|
21
30
|
z-index: 100;
|
|
22
31
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
.effibem-viewer .controls label { display: block; margin: 4px 0; cursor: pointer; }
|
|
33
|
+
.effibem-viewer .controls input { margin-right: 6px; }
|
|
34
|
+
.effibem-viewer .info-panel {
|
|
26
35
|
display: none;
|
|
27
36
|
position: absolute;
|
|
28
37
|
background: rgba(255,255,255,0.95);
|
|
@@ -35,12 +44,12 @@
|
|
|
35
44
|
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
|
36
45
|
pointer-events: none;
|
|
37
46
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
.badge {
|
|
47
|
+
.effibem-viewer .info-panel h4 { margin: 0 0 8px 0; font-size: 13px; }
|
|
48
|
+
.effibem-viewer .info-panel .info-row { margin: 4px 0; }
|
|
49
|
+
.effibem-viewer .info-panel .info-row.emphasized { background: #1a73e8; color: white; margin: 4px -8px; padding: 4px 8px; border-radius: 4px; font-weight: 600; }
|
|
50
|
+
.effibem-viewer .info-panel .info-row.emphasized .info-label { color: white; }
|
|
51
|
+
.effibem-viewer .info-panel .info-label { color: #666; }
|
|
52
|
+
.effibem-viewer .badge {
|
|
44
53
|
display: inline-block;
|
|
45
54
|
padding: 2px 8px;
|
|
46
55
|
font-size: 11px;
|
|
@@ -48,15 +57,53 @@
|
|
|
48
57
|
border-radius: 10px;
|
|
49
58
|
text-transform: capitalize;
|
|
50
59
|
}
|
|
51
|
-
.badge-success { background-color: #198754; color: white; }
|
|
52
|
-
.badge-danger { background-color: #dc3545; color: white; }
|
|
60
|
+
.effibem-viewer .badge-success { background-color: #198754; color: white; }
|
|
61
|
+
.effibem-viewer .badge-danger { background-color: #dc3545; color: white; }
|
|
62
|
+
.effibem-viewer .diagnostics-section { display: none; }
|
|
63
|
+
.effibem-viewer.include-diagnostics .diagnostics-section { display: block; }
|
|
64
|
+
.effibem-loader {
|
|
65
|
+
position: absolute;
|
|
66
|
+
top: 50%;
|
|
67
|
+
left: 50%;
|
|
68
|
+
transform: translate(-50%, -50%);
|
|
69
|
+
text-align: center;
|
|
70
|
+
font-family: sans-serif;
|
|
71
|
+
z-index: 50;
|
|
72
|
+
}
|
|
73
|
+
.effibem-loader.hidden { display: none; }
|
|
74
|
+
.effibem-loader h2 {
|
|
75
|
+
margin: 0 0 8px 0;
|
|
76
|
+
font-size: 18px;
|
|
77
|
+
font-weight: 600;
|
|
78
|
+
color: #333;
|
|
79
|
+
}
|
|
80
|
+
.effibem-loader p {
|
|
81
|
+
margin: 0 0 16px 0;
|
|
82
|
+
font-size: 13px;
|
|
83
|
+
color: #666;
|
|
84
|
+
}
|
|
85
|
+
.effibem-loader input[type="file"] {
|
|
86
|
+
font-size: 14px;
|
|
87
|
+
}
|
|
53
88
|
</style>
|
|
54
89
|
|
|
55
|
-
<
|
|
56
|
-
<
|
|
90
|
+
<header id="header">
|
|
91
|
+
<img src="{{ logo_path }}" alt="EffiBEM Logo">
|
|
92
|
+
<h1>EffiBEM Viewer</h1>
|
|
93
|
+
</header>
|
|
94
|
+
|
|
95
|
+
<div id="viewer" class="effibem-viewer">
|
|
96
|
+
{% if loader_mode %}
|
|
97
|
+
<div id="loaderPrompt" class="effibem-loader">
|
|
98
|
+
<h2>EffiBEM Viewer</h2>
|
|
99
|
+
<p>Select an OpenStudio GLTF file to visualize</p>
|
|
100
|
+
<input type="file" id="fileInput" accept=".gltf,.json">
|
|
101
|
+
</div>
|
|
102
|
+
{% endif %}
|
|
103
|
+
<div class="controls">
|
|
57
104
|
<div style="margin-bottom: 8px;">
|
|
58
105
|
<label style="display: inline;"><strong>Render By</strong></label>
|
|
59
|
-
<select
|
|
106
|
+
<select class="renderBy" style="margin-left: 8px; font-size: 12px;">
|
|
60
107
|
<option value="surfaceType">Surface Type</option>
|
|
61
108
|
<option value="boundary">Boundary</option>
|
|
62
109
|
<option value="construction">Construction</option>
|
|
@@ -67,581 +114,87 @@
|
|
|
67
114
|
</div>
|
|
68
115
|
<div style="margin: 8px 0;">
|
|
69
116
|
<label style="display: inline;"><strong>Show Story</strong></label>
|
|
70
|
-
<select
|
|
117
|
+
<select class="showStory" style="margin-left: 8px; font-size: 12px;">
|
|
71
118
|
<option value="">All Stories</option>
|
|
72
119
|
</select>
|
|
73
120
|
</div>
|
|
74
121
|
<hr style="margin: 8px 0; border: none; border-top: 1px solid #ccc;">
|
|
75
122
|
<strong>Surface Filters</strong>
|
|
76
|
-
<label><input type="checkbox"
|
|
77
|
-
<label><input type="checkbox"
|
|
78
|
-
<label><input type="checkbox"
|
|
79
|
-
<label><input type="checkbox"
|
|
80
|
-
<label><input type="checkbox"
|
|
81
|
-
<label><input type="checkbox"
|
|
82
|
-
<label><input type="checkbox"
|
|
123
|
+
<label><input type="checkbox" class="showFloors" checked> Floors</label>
|
|
124
|
+
<label><input type="checkbox" class="showWalls" checked> Walls</label>
|
|
125
|
+
<label><input type="checkbox" class="showRoofs" checked> Roofs/Ceilings</label>
|
|
126
|
+
<label><input type="checkbox" class="showWindows" checked> Windows</label>
|
|
127
|
+
<label><input type="checkbox" class="showDoors" checked> Doors</label>
|
|
128
|
+
<label><input type="checkbox" class="showShading" checked> Shading</label>
|
|
129
|
+
<label><input type="checkbox" class="showPartitions" checked> Partitions</label>
|
|
83
130
|
<hr style="margin: 8px 0; border: none; border-top: 1px solid #ccc;">
|
|
84
|
-
<label><input type="checkbox"
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
131
|
+
<label><input type="checkbox" class="showEdges" checked> Show Edges</label>
|
|
132
|
+
<div class="diagnostics-section">
|
|
133
|
+
<hr style="margin: 8px 0; border: none; border-top: 1px solid #ccc;">
|
|
134
|
+
<strong>Geometry Diagnostics</strong>
|
|
135
|
+
<label><input type="checkbox" class="showOnlyNonConvexSurfaces"> Non-Convex Surfaces Only</label>
|
|
136
|
+
<label><input type="checkbox" class="showOnlyIncorrectlyOriented"> Incorrectly Oriented Only</label>
|
|
137
|
+
<label><input type="checkbox" class="showOnlyNonConvexSpaces"> Non-Convex Spaces Only</label>
|
|
138
|
+
<label><input type="checkbox" class="showOnlyNonEnclosedSpaces"> Non-Enclosed Spaces Only</label>
|
|
139
|
+
</div>
|
|
93
140
|
</div>
|
|
94
|
-
<div
|
|
95
|
-
<h4
|
|
96
|
-
<div class="info-row
|
|
97
|
-
<div class="info-row
|
|
98
|
-
<div class="info-row
|
|
99
|
-
<div class="info-row
|
|
100
|
-
<div class="info-row
|
|
101
|
-
<div class="info-row
|
|
102
|
-
<div class="info-row
|
|
103
|
-
<div class="info-row
|
|
104
|
-
<div class="info-row
|
|
105
|
-
<div class="info-row
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
141
|
+
<div class="info-panel">
|
|
142
|
+
<h4 class="info-name"></h4>
|
|
143
|
+
<div class="info-row info-type-row"><span class="info-label">Surface Type:</span> <span class="info-type"></span></div>
|
|
144
|
+
<div class="info-row info-space-row"><span class="info-label">Space:</span> <span class="info-space"></span></div>
|
|
145
|
+
<div class="info-row info-spaceType-row"><span class="info-label">Space Type:</span> <span class="info-spaceType"></span></div>
|
|
146
|
+
<div class="info-row info-thermalZone-row"><span class="info-label">Thermal Zone:</span> <span class="info-thermalZone"></span></div>
|
|
147
|
+
<div class="info-row info-buildingStory-row"><span class="info-label">Building Story:</span> <span class="info-buildingStory"></span></div>
|
|
148
|
+
<div class="info-row info-construction-row"><span class="info-label">Construction:</span> <span class="info-construction"></span></div>
|
|
149
|
+
<div class="info-row info-boundary-row"><span class="info-label">Boundary:</span> <span class="info-boundary"></span></div>
|
|
150
|
+
<div class="info-row info-boundaryObject-row"><span class="info-label">Adjacent To:</span> <span class="info-boundaryObject"></span></div>
|
|
151
|
+
<div class="info-row info-sunExposure-row"><span class="info-label">Sun Exposure:</span> <span class="info-sunExposure"></span></div>
|
|
152
|
+
<div class="info-row info-windExposure-row"><span class="info-label">Wind Exposure:</span> <span class="info-windExposure"></span></div>
|
|
153
|
+
<div class="diagnostics-section">
|
|
154
|
+
<div class="info-row info-convex-row"><span class="info-label">Convex:</span> <span class="info-convex"></span></div>
|
|
155
|
+
<div class="info-row info-correctlyOriented-row"><span class="info-label">Correctly Oriented:</span> <span class="info-correctlyOriented"></span></div>
|
|
156
|
+
<div class="info-row info-spaceConvex-row"><span class="info-label">Space Convex:</span> <span class="info-spaceConvex"></span></div>
|
|
157
|
+
<div class="info-row info-spaceEnclosed-row"><span class="info-label">Space Enclosed:</span> <span class="info-spaceEnclosed"></span></div>
|
|
158
|
+
</div>
|
|
112
159
|
</div>
|
|
113
160
|
</div>
|
|
114
161
|
|
|
115
|
-
<script type="
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const container = document.getElementById("viewer");
|
|
121
|
-
|
|
122
|
-
const scene = new THREE.Scene();
|
|
123
|
-
scene.background = new THREE.Color(0xf5f5f5);
|
|
124
|
-
|
|
125
|
-
const camera = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 0.1, 5000);
|
|
126
|
-
|
|
127
|
-
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
128
|
-
renderer.setSize(container.clientWidth, container.clientHeight);
|
|
129
|
-
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
130
|
-
container.appendChild(renderer.domElement);
|
|
131
|
-
|
|
132
|
-
const controls = new OrbitControls(camera, renderer.domElement);
|
|
133
|
-
|
|
134
|
-
window.addEventListener('resize', () => {
|
|
135
|
-
camera.aspect = container.clientWidth / container.clientHeight;
|
|
136
|
-
camera.updateProjectionMatrix();
|
|
137
|
-
renderer.setSize(container.clientWidth, container.clientHeight);
|
|
138
|
-
requestRenderIfNotRequested();
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
scene.add(new THREE.AmbientLight(0x888888));
|
|
142
|
-
scene.add(new THREE.HemisphereLight(0xffffff, 0x444444, 0.6));
|
|
143
|
-
const dirLight = new THREE.DirectionalLight(0xffffff, 0.6);
|
|
144
|
-
dirLight.position.set(1, 2, 1);
|
|
145
|
-
scene.add(dirLight);
|
|
146
|
-
|
|
147
|
-
// Collect all meshes for filtering and selection
|
|
148
|
-
const sceneObjects = [];
|
|
149
|
-
// Map mesh -> edge lines
|
|
150
|
-
const objectEdges = new Map();
|
|
151
|
-
// Map mesh -> back face object
|
|
152
|
-
const backObjects = new Map();
|
|
153
|
-
// Map back object -> front object (for selection)
|
|
154
|
-
const backToFront = new Map();
|
|
155
|
-
|
|
156
|
-
// Selection state
|
|
157
|
-
const raycaster = new THREE.Raycaster();
|
|
158
|
-
const mouse = new THREE.Vector2();
|
|
159
|
-
let selectedObject = null;
|
|
160
|
-
let originalMaterial = null;
|
|
161
|
-
|
|
162
|
-
const selectedMaterial = new THREE.MeshStandardMaterial({
|
|
163
|
-
color: 0xffff00,
|
|
164
|
-
emissive: 0x444400,
|
|
165
|
-
side: THREE.DoubleSide
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
const infoPanel = document.getElementById('info-panel');
|
|
169
|
-
|
|
170
|
-
let selectedBackWasVisible = false;
|
|
171
|
-
|
|
172
|
-
function selectObject(obj, clickX, clickY) {
|
|
173
|
-
// Restore previous selection
|
|
174
|
-
if (selectedObject && originalMaterial) {
|
|
175
|
-
selectedObject.material = originalMaterial;
|
|
176
|
-
// Restore back object visibility
|
|
177
|
-
const prevBackObj = backObjects.get(selectedObject);
|
|
178
|
-
if (prevBackObj) {
|
|
179
|
-
prevBackObj.visible = selectedBackWasVisible;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (obj) {
|
|
184
|
-
selectedObject = obj;
|
|
185
|
-
originalMaterial = obj.material;
|
|
186
|
-
obj.material = selectedMaterial;
|
|
187
|
-
|
|
188
|
-
// Hide back object so yellow selection shows through
|
|
189
|
-
const backObj = backObjects.get(obj);
|
|
190
|
-
if (backObj) {
|
|
191
|
-
selectedBackWasVisible = backObj.visible;
|
|
192
|
-
backObj.visible = false;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Update info panel with all available details
|
|
196
|
-
const data = obj.userData;
|
|
197
|
-
const renderMode = document.getElementById('renderBy').value;
|
|
198
|
-
document.getElementById('info-name').textContent = data.name || 'Unknown';
|
|
199
|
-
|
|
200
|
-
// Map renderBy values to info row IDs
|
|
201
|
-
const renderByToRowId = {
|
|
202
|
-
'surfaceType': 'type',
|
|
203
|
-
'boundary': 'boundary',
|
|
204
|
-
'construction': 'construction',
|
|
205
|
-
'thermalZone': 'thermalZone',
|
|
206
|
-
'spaceType': 'spaceType',
|
|
207
|
-
'buildingStory': 'buildingStory',
|
|
208
|
-
};
|
|
209
|
-
const emphasizedRowId = renderByToRowId[renderMode];
|
|
210
|
-
|
|
211
|
-
// Helper to show/hide rows with emphasis
|
|
212
|
-
const setRow = (id, value) => {
|
|
213
|
-
const row = document.getElementById(`info-${id}-row`);
|
|
214
|
-
const span = document.getElementById(`info-${id}`);
|
|
215
|
-
if (value) {
|
|
216
|
-
span.textContent = value;
|
|
217
|
-
row.style.display = 'block';
|
|
218
|
-
row.classList.toggle('emphasized', id === emphasizedRowId);
|
|
219
|
-
} else {
|
|
220
|
-
row.style.display = 'none';
|
|
221
|
-
row.classList.remove('emphasized');
|
|
222
|
-
}
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
setRow('type', data.surfaceType);
|
|
226
|
-
setRow('space', data.spaceName);
|
|
227
|
-
setRow('spaceType', data.spaceTypeName);
|
|
228
|
-
setRow('thermalZone', data.thermalZoneName);
|
|
229
|
-
setRow('buildingStory', data.buildingStoryName);
|
|
230
|
-
setRow('construction', data.constructionName);
|
|
231
|
-
setRow('boundary', data.outsideBoundaryCondition);
|
|
232
|
-
setRow('boundaryObject', data.outsideBoundaryConditionObjectName);
|
|
233
|
-
setRow('sunExposure', data.sunExposure);
|
|
234
|
-
setRow('windExposure', data.windExposure);
|
|
235
|
-
|
|
236
|
-
{% if include_geometry_diagnostics %}
|
|
237
|
-
// Diagnostic fields with pill badge styling
|
|
238
|
-
const setDiagRow = (id, value) => {
|
|
239
|
-
const row = document.getElementById(`info-${id}-row`);
|
|
240
|
-
const span = document.getElementById(`info-${id}`);
|
|
241
|
-
if (row && value !== undefined) {
|
|
242
|
-
span.innerHTML = `<span class="badge ${value ? 'badge-success' : 'badge-danger'}">${value}</span>`;
|
|
243
|
-
row.style.display = 'block';
|
|
244
|
-
} else if (row) {
|
|
245
|
-
row.style.display = 'none';
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
setDiagRow('convex', data.convex);
|
|
250
|
-
setDiagRow('correctlyOriented', data.correctlyOriented);
|
|
251
|
-
setDiagRow('spaceConvex', data.spaceConvex);
|
|
252
|
-
setDiagRow('spaceEnclosed', data.spaceEnclosed);
|
|
253
|
-
{% endif %}
|
|
254
|
-
|
|
255
|
-
// Position panel to the right of click, with offset
|
|
256
|
-
const rect = container.getBoundingClientRect();
|
|
257
|
-
let left = clickX - rect.left + 15;
|
|
258
|
-
let top = clickY - rect.top - 10;
|
|
259
|
-
|
|
260
|
-
// Keep panel within container bounds
|
|
261
|
-
infoPanel.style.display = 'block';
|
|
262
|
-
const panelRect = infoPanel.getBoundingClientRect();
|
|
263
|
-
if (left + panelRect.width > container.clientWidth) {
|
|
264
|
-
left = clickX - rect.left - panelRect.width - 15;
|
|
265
|
-
}
|
|
266
|
-
if (top + panelRect.height > container.clientHeight) {
|
|
267
|
-
top = container.clientHeight - panelRect.height - 10;
|
|
268
|
-
}
|
|
269
|
-
if (top < 10) top = 10;
|
|
270
|
-
|
|
271
|
-
infoPanel.style.left = left + 'px';
|
|
272
|
-
infoPanel.style.top = top + 'px';
|
|
273
|
-
} else {
|
|
274
|
-
selectedObject = null;
|
|
275
|
-
originalMaterial = null;
|
|
276
|
-
infoPanel.style.display = 'none';
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Track mouse position for click vs drag detection
|
|
281
|
-
let mouseDownPos = { x: 0, y: 0 };
|
|
282
|
-
|
|
283
|
-
renderer.domElement.addEventListener('mousedown', (event) => {
|
|
284
|
-
mouseDownPos.x = event.clientX;
|
|
285
|
-
mouseDownPos.y = event.clientY;
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
renderer.domElement.addEventListener('click', (event) => {
|
|
289
|
-
// Ignore if this was a drag (camera orbit)
|
|
290
|
-
const dx = event.clientX - mouseDownPos.x;
|
|
291
|
-
const dy = event.clientY - mouseDownPos.y;
|
|
292
|
-
if (Math.abs(dx) > 5 || Math.abs(dy) > 5) return;
|
|
293
|
-
|
|
294
|
-
const rect = renderer.domElement.getBoundingClientRect();
|
|
295
|
-
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
|
296
|
-
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
|
297
|
-
|
|
298
|
-
raycaster.setFromCamera(mouse, camera);
|
|
299
|
-
|
|
300
|
-
// Include both front and back objects for picking
|
|
301
|
-
const visibleObjects = sceneObjects.filter(obj => obj.visible);
|
|
302
|
-
const visibleBackObjects = [...backObjects.values()].filter(obj => obj.visible);
|
|
303
|
-
const allPickable = [...visibleObjects, ...visibleBackObjects];
|
|
304
|
-
|
|
305
|
-
const intersects = raycaster.intersectObjects(allPickable);
|
|
306
|
-
|
|
307
|
-
if (intersects.length > 0) {
|
|
308
|
-
let hitObj = intersects[0].object;
|
|
309
|
-
// If we hit a back object, resolve to its front object
|
|
310
|
-
if (backToFront.has(hitObj)) {
|
|
311
|
-
hitObj = backToFront.get(hitObj);
|
|
312
|
-
}
|
|
313
|
-
selectObject(hitObj, event.clientX, event.clientY);
|
|
314
|
-
} else {
|
|
315
|
-
selectObject(null);
|
|
162
|
+
<script type="importmap">
|
|
163
|
+
{
|
|
164
|
+
"imports": {
|
|
165
|
+
"three": "https://cdn.jsdelivr.net/npm/three@0.182.0/build/three.module.js",
|
|
166
|
+
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.182.0/examples/jsm/"
|
|
316
167
|
}
|
|
317
|
-
|
|
318
|
-
requestRenderIfNotRequested();
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
function updateVisibility() {
|
|
322
|
-
const showFloors = document.getElementById('showFloors').checked;
|
|
323
|
-
const showWalls = document.getElementById('showWalls').checked;
|
|
324
|
-
const showRoofs = document.getElementById('showRoofs').checked;
|
|
325
|
-
const showWindows = document.getElementById('showWindows').checked;
|
|
326
|
-
const showDoors = document.getElementById('showDoors').checked;
|
|
327
|
-
const showShading = document.getElementById('showShading').checked;
|
|
328
|
-
const showPartitions = document.getElementById('showPartitions').checked;
|
|
329
|
-
const showEdges = document.getElementById('showEdges').checked;
|
|
330
|
-
const showStory = document.getElementById('showStory').value;
|
|
331
|
-
|
|
332
|
-
sceneObjects.forEach(obj => {
|
|
333
|
-
const surfaceType = obj.userData?.surfaceType || '';
|
|
334
|
-
const storyName = obj.userData?.buildingStoryName || '';
|
|
335
|
-
let visible = true;
|
|
336
|
-
|
|
337
|
-
// Filter by surface type
|
|
338
|
-
if (surfaceType === 'Floor') visible = showFloors;
|
|
339
|
-
else if (surfaceType === 'Wall') visible = showWalls;
|
|
340
|
-
else if (surfaceType === 'RoofCeiling') visible = showRoofs;
|
|
341
|
-
else if (surfaceType.includes('Window') || surfaceType.includes('Skylight') || surfaceType.includes('TubularDaylight') || surfaceType === 'GlassDoor') visible = showWindows;
|
|
342
|
-
else if (surfaceType.includes('Door')) visible = showDoors;
|
|
343
|
-
else if (surfaceType.includes('Shading')) visible = showShading;
|
|
344
|
-
else if (surfaceType === 'InteriorPartitionSurface') visible = showPartitions;
|
|
345
|
-
|
|
346
|
-
// Filter by story
|
|
347
|
-
if (visible && showStory && storyName !== showStory) {
|
|
348
|
-
visible = false;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
{% if include_geometry_diagnostics %}
|
|
352
|
-
// Geometry diagnostic filters
|
|
353
|
-
const showOnlyNonConvexSurfaces = document.getElementById('showOnlyNonConvexSurfaces').checked;
|
|
354
|
-
const showOnlyIncorrectlyOriented = document.getElementById('showOnlyIncorrectlyOriented').checked;
|
|
355
|
-
const showOnlyNonConvexSpaces = document.getElementById('showOnlyNonConvexSpaces').checked;
|
|
356
|
-
const showOnlyNonEnclosedSpaces = document.getElementById('showOnlyNonEnclosedSpaces').checked;
|
|
357
|
-
|
|
358
|
-
if (visible && showOnlyNonConvexSurfaces && obj.userData.convex !== false) {
|
|
359
|
-
visible = false;
|
|
360
|
-
}
|
|
361
|
-
if (visible && showOnlyIncorrectlyOriented && obj.userData.correctlyOriented !== false) {
|
|
362
|
-
visible = false;
|
|
363
|
-
}
|
|
364
|
-
if (visible && showOnlyNonConvexSpaces && obj.userData.spaceConvex !== false) {
|
|
365
|
-
visible = false;
|
|
366
|
-
}
|
|
367
|
-
if (visible && showOnlyNonEnclosedSpaces && obj.userData.spaceEnclosed !== false) {
|
|
368
|
-
visible = false;
|
|
369
|
-
}
|
|
370
|
-
{% endif %}
|
|
371
|
-
|
|
372
|
-
obj.visible = visible;
|
|
373
|
-
|
|
374
|
-
// Sync edge visibility
|
|
375
|
-
const edges = objectEdges.get(obj);
|
|
376
|
-
if (edges) {
|
|
377
|
-
edges.visible = visible && showEdges;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Sync back object visibility
|
|
381
|
-
const backObj = backObjects.get(obj);
|
|
382
|
-
if (backObj) {
|
|
383
|
-
backObj.visible = visible;
|
|
384
|
-
}
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
requestRenderIfNotRequested();
|
|
388
168
|
}
|
|
169
|
+
</script>
|
|
389
170
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
{
|
|
396
|
-
['showOnlyNonConvexSurfaces', 'showOnlyIncorrectlyOriented', 'showOnlyNonConvexSpaces', 'showOnlyNonEnclosedSpaces'].forEach(id => {
|
|
397
|
-
document.getElementById(id).addEventListener('change', updateVisibility);
|
|
398
|
-
});
|
|
171
|
+
{% if embedded %}
|
|
172
|
+
<script type="module">
|
|
173
|
+
{{ js_lib_content }}
|
|
174
|
+
</script>
|
|
175
|
+
{% else %}
|
|
176
|
+
<script type="module" src="{{ js_lib_path }}"></script>
|
|
399
177
|
{% endif %}
|
|
400
178
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
'Floor': 0xbfbfbf, 'Wall': 0xebe2c5, 'RoofCeiling': 0xca9595,
|
|
412
|
-
'Window': 0xc0e2eb, 'GlassDoor': 0xc0e2eb, 'Skylight': 0xc0e2eb,
|
|
413
|
-
'TubularDaylightDome': 0xc0e2eb, 'TubularDaylightDiffuser': 0xc0e2eb,
|
|
414
|
-
'Door': 0xcabc95, 'OverheadDoor': 0xcabc95,
|
|
415
|
-
'SiteShading': 0xbbd1dc, 'BuildingShading': 0xd8cbe5, 'SpaceShading': 0xb7c5e0,
|
|
416
|
-
'InteriorPartitionSurface': 0xd5e2cf, 'AirWall': 0xc0e2eb,
|
|
417
|
-
};
|
|
418
|
-
const boundaryColors = {
|
|
419
|
-
'Surface': 0x009900, 'Adiabatic': 0xff0000, 'Space': 0xff0000,
|
|
420
|
-
'Outdoors': 0xa3cccc, 'Outdoors_Sun': 0x28cccc, 'Outdoors_Wind': 0x099fa2, 'Outdoors_SunWind': 0x4477a1,
|
|
421
|
-
'Ground': 0xccb77a, 'Foundation': 0x751e7a,
|
|
422
|
-
'OtherSideCoefficients': 0x3f3f3f, 'OtherSideConditionsModel': 0x99004c,
|
|
423
|
-
};
|
|
424
|
-
|
|
425
|
-
// Generate consistent color from string (for dynamic values like thermal zones)
|
|
426
|
-
function stringToColor(str) {
|
|
427
|
-
if (!str) return 0xcccccc;
|
|
428
|
-
let hash = 0;
|
|
429
|
-
for (let i = 0; i < str.length; i++) {
|
|
430
|
-
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
431
|
-
}
|
|
432
|
-
// Generate HSL with good saturation and lightness
|
|
433
|
-
const h = Math.abs(hash) % 360;
|
|
434
|
-
return new THREE.Color(`hsl(${h}, 65%, 55%)`).getHex();
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// Cache for dynamic colors
|
|
438
|
-
const dynamicColors = {};
|
|
439
|
-
function getDynamicColor(category, name) {
|
|
440
|
-
const key = `${category}_${name}`;
|
|
441
|
-
if (!dynamicColors[key]) {
|
|
442
|
-
dynamicColors[key] = stringToColor(name);
|
|
443
|
-
}
|
|
444
|
-
return dynamicColors[key];
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function getColorsForObject(obj, renderMode) {
|
|
448
|
-
const data = obj.userData;
|
|
449
|
-
let colorExt, colorInt;
|
|
450
|
-
|
|
451
|
-
switch (renderMode) {
|
|
452
|
-
case 'surfaceType':
|
|
453
|
-
colorExt = surfaceTypeColors[data.surfaceType] ?? 0xcccccc;
|
|
454
|
-
colorInt = surfaceTypeColorsInt[data.surfaceType] ?? 0xeeeeee;
|
|
455
|
-
break;
|
|
456
|
-
case 'boundary':
|
|
457
|
-
const bc = data.outsideBoundaryCondition || 'Outdoors';
|
|
458
|
-
// Combine sun/wind exposure for outdoor surfaces
|
|
459
|
-
let boundaryKey = bc;
|
|
460
|
-
if (bc === 'Outdoors') {
|
|
461
|
-
const sun = data.sunExposure === 'SunExposed';
|
|
462
|
-
const wind = data.windExposure === 'WindExposed';
|
|
463
|
-
if (sun && wind) boundaryKey = 'Outdoors_SunWind';
|
|
464
|
-
else if (sun) boundaryKey = 'Outdoors_Sun';
|
|
465
|
-
else if (wind) boundaryKey = 'Outdoors_Wind';
|
|
466
|
-
}
|
|
467
|
-
colorExt = boundaryColors[boundaryKey] ?? boundaryColors[bc] ?? 0xcccccc;
|
|
468
|
-
colorInt = colorExt;
|
|
469
|
-
break;
|
|
470
|
-
case 'construction':
|
|
471
|
-
colorExt = getDynamicColor('construction', data.constructionName);
|
|
472
|
-
colorInt = colorExt;
|
|
473
|
-
break;
|
|
474
|
-
case 'thermalZone':
|
|
475
|
-
colorExt = getDynamicColor('thermalZone', data.thermalZoneName);
|
|
476
|
-
colorInt = colorExt;
|
|
477
|
-
break;
|
|
478
|
-
case 'spaceType':
|
|
479
|
-
colorExt = getDynamicColor('spaceType', data.spaceTypeName);
|
|
480
|
-
colorInt = colorExt;
|
|
481
|
-
break;
|
|
482
|
-
case 'buildingStory':
|
|
483
|
-
colorExt = getDynamicColor('buildingStory', data.buildingStoryName);
|
|
484
|
-
colorInt = colorExt;
|
|
485
|
-
break;
|
|
486
|
-
default:
|
|
487
|
-
colorExt = 0xcccccc;
|
|
488
|
-
colorInt = 0xeeeeee;
|
|
179
|
+
{% if loader_mode %}
|
|
180
|
+
<script type="module">
|
|
181
|
+
const options = { includeGeometryDiagnostics: {{ include_geometry_diagnostics | tojson }} };
|
|
182
|
+
const viewer = new EffiBEMViewer('viewer', options);
|
|
183
|
+
|
|
184
|
+
document.getElementById('fileInput').addEventListener('change', (e) => {
|
|
185
|
+
const file = e.target.files[0];
|
|
186
|
+
if (file) {
|
|
187
|
+
document.getElementById('loaderPrompt').classList.add('hidden');
|
|
188
|
+
viewer.loadFromFileObject(file);
|
|
489
189
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
const renderMode = document.getElementById('renderBy').value;
|
|
495
|
-
sceneObjects.forEach(obj => {
|
|
496
|
-
const { colorExt, colorInt } = getColorsForObject(obj, renderMode);
|
|
497
|
-
obj.material.color.setHex(colorExt);
|
|
498
|
-
const backObj = backObjects.get(obj);
|
|
499
|
-
if (backObj) {
|
|
500
|
-
backObj.material.color.setHex(colorInt);
|
|
501
|
-
}
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
requestRenderIfNotRequested();
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
document.getElementById('renderBy').addEventListener('change', updateRenderMode);
|
|
508
|
-
|
|
190
|
+
});
|
|
191
|
+
</script>
|
|
192
|
+
{% else %}
|
|
193
|
+
<script type="module">
|
|
509
194
|
const gltfData = {{ gltf_data | tojson(indent=indent) }};
|
|
510
195
|
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
"",
|
|
515
|
-
(gltf) => {
|
|
516
|
-
scene.add(gltf.scene);
|
|
517
|
-
|
|
518
|
-
// Collect all meshes with userData for filtering
|
|
519
|
-
const renderMode = document.getElementById('renderBy').value;
|
|
520
|
-
const edgeMaterial = new THREE.LineBasicMaterial({ color: 0x000000 });
|
|
521
|
-
|
|
522
|
-
gltf.scene.traverse(obj => {
|
|
523
|
-
if (obj.isMesh && obj.userData?.surfaceType) {
|
|
524
|
-
sceneObjects.push(obj);
|
|
525
|
-
|
|
526
|
-
const { colorExt, colorInt } = getColorsForObject(obj, renderMode);
|
|
527
|
-
|
|
528
|
-
// Front face material
|
|
529
|
-
obj.material = new THREE.MeshPhongMaterial({
|
|
530
|
-
color: colorExt,
|
|
531
|
-
specular: 0x222222,
|
|
532
|
-
shininess: 30,
|
|
533
|
-
side: THREE.FrontSide
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
// Create back face object with interior color
|
|
537
|
-
const backObj = obj.clone();
|
|
538
|
-
backObj.material = new THREE.MeshPhongMaterial({
|
|
539
|
-
color: colorInt,
|
|
540
|
-
specular: 0x222222,
|
|
541
|
-
shininess: 30,
|
|
542
|
-
side: THREE.BackSide
|
|
543
|
-
});
|
|
544
|
-
obj.parent.add(backObj);
|
|
545
|
-
backObjects.set(obj, backObj);
|
|
546
|
-
backToFront.set(backObj, obj);
|
|
547
|
-
|
|
548
|
-
// Create edge lines for this mesh
|
|
549
|
-
const edgesGeometry = new THREE.EdgesGeometry(obj.geometry);
|
|
550
|
-
const edges = new THREE.LineSegments(edgesGeometry, edgeMaterial);
|
|
551
|
-
edges.position.copy(obj.position);
|
|
552
|
-
edges.rotation.copy(obj.rotation);
|
|
553
|
-
edges.scale.copy(obj.scale);
|
|
554
|
-
obj.parent.add(edges);
|
|
555
|
-
objectEdges.set(obj, edges);
|
|
556
|
-
}
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
// Populate story dropdown with unique values
|
|
560
|
-
const storySelect = document.getElementById('showStory');
|
|
561
|
-
const storyNames = [...new Set(sceneObjects.map(o => o.userData?.buildingStoryName).filter(Boolean))].sort();
|
|
562
|
-
storyNames.forEach(name => {
|
|
563
|
-
const option = document.createElement('option');
|
|
564
|
-
option.value = name;
|
|
565
|
-
option.textContent = name;
|
|
566
|
-
storySelect.appendChild(option);
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
// Position camera using bounding box from GLTF metadata
|
|
570
|
-
const bbox = gltfData.scenes?.[0]?.extras?.boundingbox;
|
|
571
|
-
|
|
572
|
-
// Add axes (X=red, Y=green, Z=blue) - converted from OpenStudio Z-up to Three.js Y-up
|
|
573
|
-
const axisSize = bbox ? bbox.lookAtR * 4 : 10;
|
|
574
|
-
|
|
575
|
-
const xAxisGeometry = new THREE.BufferGeometry().setFromPoints([
|
|
576
|
-
new THREE.Vector3(0, 0, 0), new THREE.Vector3(axisSize, 0, 0)
|
|
577
|
-
]);
|
|
578
|
-
scene.add(new THREE.Line(xAxisGeometry, new THREE.LineBasicMaterial({ color: 0xff0000 })));
|
|
579
|
-
|
|
580
|
-
// OpenStudio Y -> Three.js -Z
|
|
581
|
-
const yAxisGeometry = new THREE.BufferGeometry().setFromPoints([
|
|
582
|
-
new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, -axisSize)
|
|
583
|
-
]);
|
|
584
|
-
scene.add(new THREE.Line(yAxisGeometry, new THREE.LineBasicMaterial({ color: 0x00ff00 })));
|
|
585
|
-
|
|
586
|
-
// OpenStudio Z -> Three.js Y
|
|
587
|
-
const zAxisGeometry = new THREE.BufferGeometry().setFromPoints([
|
|
588
|
-
new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, axisSize, 0)
|
|
589
|
-
]);
|
|
590
|
-
scene.add(new THREE.Line(zAxisGeometry, new THREE.LineBasicMaterial({ color: 0x0000ff })));
|
|
591
|
-
|
|
592
|
-
// North axis (orange) if northAxis is set
|
|
593
|
-
const northAxis = gltfData.scenes?.[0]?.extras?.northAxis;
|
|
594
|
-
if (northAxis && northAxis !== 0) {
|
|
595
|
-
const northAxisRad = -northAxis * Math.PI / 180.0;
|
|
596
|
-
const northAxisGeometry = new THREE.BufferGeometry().setFromPoints([
|
|
597
|
-
new THREE.Vector3(0, 0, 0),
|
|
598
|
-
new THREE.Vector3(-Math.sin(northAxisRad) * axisSize, 0, -Math.cos(northAxisRad) * axisSize)
|
|
599
|
-
]);
|
|
600
|
-
scene.add(new THREE.Line(northAxisGeometry, new THREE.LineBasicMaterial({ color: 0xff9933 })));
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
if (bbox) {
|
|
604
|
-
// Convert from OpenStudio coords (Z-up) to Three.js coords (Y-up)
|
|
605
|
-
const lookAt = new THREE.Vector3(bbox.lookAtX, bbox.lookAtZ, -bbox.lookAtY);
|
|
606
|
-
const radius = 2.5 * bbox.lookAtR;
|
|
607
|
-
|
|
608
|
-
// Position camera at an angle (similar to -30, 30 degrees)
|
|
609
|
-
const theta = -30 * Math.PI / 180;
|
|
610
|
-
const phi = 30 * Math.PI / 180;
|
|
611
|
-
camera.position.set(
|
|
612
|
-
radius * Math.cos(theta) * Math.cos(phi) + lookAt.x,
|
|
613
|
-
radius * Math.sin(phi) + lookAt.y,
|
|
614
|
-
-radius * Math.sin(theta) * Math.cos(phi) + lookAt.z
|
|
615
|
-
);
|
|
616
|
-
|
|
617
|
-
controls.target.copy(lookAt);
|
|
618
|
-
controls.update();
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
requestRenderIfNotRequested();
|
|
622
|
-
},
|
|
623
|
-
(e) => console.error(e)
|
|
624
|
-
);
|
|
625
|
-
|
|
626
|
-
// Render on demand (not every frame) to save CPU
|
|
627
|
-
let renderRequested = false;
|
|
628
|
-
|
|
629
|
-
function render() {
|
|
630
|
-
renderRequested = false;
|
|
631
|
-
controls.update();
|
|
632
|
-
renderer.render(scene, camera);
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
function requestRenderIfNotRequested() {
|
|
636
|
-
if (!renderRequested) {
|
|
637
|
-
renderRequested = true;
|
|
638
|
-
requestAnimationFrame(render);
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
// Re-render when controls change (orbit, zoom, pan)
|
|
643
|
-
controls.addEventListener('change', requestRenderIfNotRequested);
|
|
644
|
-
|
|
645
|
-
// Re-render on window resize
|
|
646
|
-
window.addEventListener('resize', requestRenderIfNotRequested);
|
|
196
|
+
const options = { includeGeometryDiagnostics: {{ include_geometry_diagnostics | tojson }} };
|
|
197
|
+
const viewer = new EffiBEMViewer('viewer', options);
|
|
198
|
+
viewer.loadFromJSON(gltfData);
|
|
647
199
|
</script>
|
|
200
|
+
{% endif %}
|