effibemviewer 0.1.2__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.
@@ -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
- #viewer { width: 100%; height: {{ height }}; position: relative; }
12
- #controls {
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
- #controls label { display: block; margin: 4px 0; cursor: pointer; }
24
- #controls input { margin-right: 6px; }
25
- #info-panel {
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
- #info-panel h4 { margin: 0 0 8px 0; font-size: 13px; }
39
- #info-panel .info-row { margin: 4px 0; }
40
- #info-panel .info-row.emphasized { background: #1a73e8; color: white; margin: 4px -8px; padding: 4px 8px; border-radius: 4px; font-weight: 600; }
41
- #info-panel .info-row.emphasized .info-label { color: white; }
42
- #info-panel .info-label { color: #666; }
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
- <div id="viewer">
56
- <div id="controls">
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 id="renderBy" style="margin-left: 8px; font-size: 12px;">
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 id="showStory" style="margin-left: 8px; font-size: 12px;">
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" id="showFloors" checked> Floors</label>
77
- <label><input type="checkbox" id="showWalls" checked> Walls</label>
78
- <label><input type="checkbox" id="showRoofs" checked> Roofs/Ceilings</label>
79
- <label><input type="checkbox" id="showWindows" checked> Windows</label>
80
- <label><input type="checkbox" id="showDoors" checked> Doors</label>
81
- <label><input type="checkbox" id="showShading" checked> Shading</label>
82
- <label><input type="checkbox" id="showPartitions" checked> Partitions</label>
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" id="showEdges" checked> Show Edges</label>
85
- {% if include_geometry_diagnostics %}
86
- <hr style="margin: 8px 0; border: none; border-top: 1px solid #ccc;">
87
- <strong>Geometry Diagnostics</strong>
88
- <label><input type="checkbox" id="showOnlyNonConvexSurfaces"> Non-Convex Surfaces Only</label>
89
- <label><input type="checkbox" id="showOnlyIncorrectlyOriented"> Incorrectly Oriented Only</label>
90
- <label><input type="checkbox" id="showOnlyNonConvexSpaces"> Non-Convex Spaces Only</label>
91
- <label><input type="checkbox" id="showOnlyNonEnclosedSpaces"> Non-Enclosed Spaces Only</label>
92
- {% endif %}
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 id="info-panel">
95
- <h4 id="info-name"></h4>
96
- <div class="info-row" id="info-type-row"><span class="info-label">Surface Type:</span> <span id="info-type"></span></div>
97
- <div class="info-row" id="info-space-row"><span class="info-label">Space:</span> <span id="info-space"></span></div>
98
- <div class="info-row" id="info-spaceType-row"><span class="info-label">Space Type:</span> <span id="info-spaceType"></span></div>
99
- <div class="info-row" id="info-thermalZone-row"><span class="info-label">Thermal Zone:</span> <span id="info-thermalZone"></span></div>
100
- <div class="info-row" id="info-buildingStory-row"><span class="info-label">Building Story:</span> <span id="info-buildingStory"></span></div>
101
- <div class="info-row" id="info-construction-row"><span class="info-label">Construction:</span> <span id="info-construction"></span></div>
102
- <div class="info-row" id="info-boundary-row"><span class="info-label">Boundary:</span> <span id="info-boundary"></span></div>
103
- <div class="info-row" id="info-boundaryObject-row"><span class="info-label">Adjacent To:</span> <span id="info-boundaryObject"></span></div>
104
- <div class="info-row" id="info-sunExposure-row"><span class="info-label">Sun Exposure:</span> <span id="info-sunExposure"></span></div>
105
- <div class="info-row" id="info-windExposure-row"><span class="info-label">Wind Exposure:</span> <span id="info-windExposure"></span></div>
106
- {% if include_geometry_diagnostics %}
107
- <div class="info-row" id="info-convex-row"><span class="info-label">Convex:</span> <span id="info-convex"></span></div>
108
- <div class="info-row" id="info-correctlyOriented-row"><span class="info-label">Correctly Oriented:</span> <span id="info-correctlyOriented"></span></div>
109
- <div class="info-row" id="info-spaceConvex-row"><span class="info-label">Space Convex:</span> <span id="info-spaceConvex"></span></div>
110
- <div class="info-row" id="info-spaceEnclosed-row"><span class="info-label">Space Enclosed:</span> <span id="info-spaceEnclosed"></span></div>
111
- {% endif %}
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="module">
116
- import * as THREE from "three";
117
- import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
118
- import { OrbitControls } from "three/addons/controls/OrbitControls.js";
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
- // Add event listeners to checkboxes and dropdowns
391
- ['showFloors', 'showWalls', 'showRoofs', 'showWindows', 'showDoors', 'showShading', 'showPartitions', 'showEdges'].forEach(id => {
392
- document.getElementById(id).addEventListener('change', updateVisibility);
393
- });
394
- document.getElementById('showStory').addEventListener('change', updateVisibility);
395
- {% if include_geometry_diagnostics %}
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
- // Color definitions for render modes
402
- const surfaceTypeColors = {
403
- 'Floor': 0x808080, 'Wall': 0xccb266, 'RoofCeiling': 0x994c4c,
404
- 'Window': 0x66b2cc, 'GlassDoor': 0x66b2cc, 'Skylight': 0x66b2cc,
405
- 'TubularDaylightDome': 0x66b2cc, 'TubularDaylightDiffuser': 0x66b2cc,
406
- 'Door': 0x99854c, 'OverheadDoor': 0x99854c,
407
- 'SiteShading': 0x4b7c95, 'BuildingShading': 0x714c99, 'SpaceShading': 0x4c6eb2,
408
- 'InteriorPartitionSurface': 0x9ebc8f, 'AirWall': 0x66b2cc,
409
- };
410
- const surfaceTypeColorsInt = {
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
- return { colorExt, colorInt };
491
- }
492
-
493
- function updateRenderMode() {
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 loader = new GLTFLoader();
512
- loader.parse(
513
- JSON.stringify(gltfData),
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 %}