effibemviewer 0.1.2__tar.gz → 0.2.1__tar.gz

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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: effibemviewer
3
- Version: 0.1.2
3
+ Version: 0.2.1
4
4
  Summary: A 3D viewer for OpenStudio building energy models using GLTF and Three.js
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -2,20 +2,24 @@
2
2
 
3
3
  __author__ = """Julien Marrec"""
4
4
  __email__ = 'contact@effibem.com'
5
- __version__ = '0.1.2'
5
+ __version__ = '0.2.1'
6
6
 
7
7
  from effibemviewer.gltf import (
8
8
  create_example_model,
9
9
  display_model,
10
+ generate_loader_html,
11
+ get_css_library,
12
+ get_js_library,
10
13
  model_to_gltf_html,
11
14
  model_to_gltf_json,
12
- model_to_gltf_script,
13
15
  )
14
16
 
15
17
  __all__ = [
16
18
  "create_example_model",
17
19
  "display_model",
20
+ "generate_loader_html",
21
+ "get_css_library",
22
+ "get_js_library",
18
23
  "model_to_gltf_html",
19
24
  "model_to_gltf_json",
20
- "model_to_gltf_script",
21
25
  ]
@@ -0,0 +1,123 @@
1
+ import argparse
2
+ from pathlib import Path
3
+
4
+ from effibemviewer.gltf import (
5
+ create_example_model,
6
+ generate_loader_html,
7
+ get_css_library,
8
+ get_js_library,
9
+ model_to_gltf_html,
10
+ model_to_gltf_json,
11
+ )
12
+
13
+ # Asset paths within the package
14
+ ASSETS_DIR = Path(__file__).parent.parent / "docs" / "assets"
15
+
16
+ BASE_NAME = "effibemviewer"
17
+ JS_LIB_NAME = f"{BASE_NAME}.js"
18
+ CSS_LIB_NAME = f"{BASE_NAME}.css"
19
+
20
+
21
+ def get_parser() -> argparse.ArgumentParser:
22
+ """Create and return the argument parser for the CLI."""
23
+ parser = argparse.ArgumentParser(
24
+ prog="python -m effibemviewer",
25
+ description="Generate GLTF viewer HTML from OpenStudio model",
26
+ )
27
+ parser.add_argument(
28
+ "-m",
29
+ "--model",
30
+ type=Path,
31
+ help="Path to the OpenStudio model file (optional, defaults to an example model)",
32
+ )
33
+ parser.add_argument(
34
+ "-g",
35
+ "--geometry-diagnostics",
36
+ action="store_true",
37
+ help="Include geometry diagnostics (convex, correctly oriented, etc.)",
38
+ )
39
+ parser.add_argument(
40
+ "-o", "--output", type=Path, default=Path("viewer.html"), help="Output HTML file path (default: viewer.html)"
41
+ )
42
+ # --embedded and --cdn are mutually exclusive options for how to include the JS library
43
+ lib_mode = parser.add_mutually_exclusive_group()
44
+ lib_mode.add_argument(
45
+ "--embedded",
46
+ action="store_true",
47
+ help="Embed JS library inline in HTML (default: generate separate effibemviewer.js file)",
48
+ )
49
+ lib_mode.add_argument(
50
+ "--cdn",
51
+ action="store_true",
52
+ help="Reference JS library from CDN instead of embedding or generating local file",
53
+ )
54
+ parser.add_argument(
55
+ "--pretty",
56
+ action="store_true",
57
+ help="Pretty-print the JSON output in the HTML (default: compact JSON)",
58
+ )
59
+ parser.add_argument(
60
+ "--loader",
61
+ action="store_true",
62
+ help="Generate a loader HTML with file input instead of embedding model data",
63
+ )
64
+ return parser
65
+
66
+
67
+ def main():
68
+ """Command-line interface for generating GLTF viewer HTML from an OpenStudio model."""
69
+ parser = get_parser()
70
+ args = parser.parse_args()
71
+
72
+ # Determine paths (relative to output HTML)
73
+ output_dir = args.output.parent
74
+ output_dir.mkdir(parents=True, exist_ok=True)
75
+
76
+ if not args.embedded and not args.cdn:
77
+ js_lib_path = output_dir / JS_LIB_NAME
78
+ js_lib_path.write_text(get_js_library())
79
+ css_lib_path = output_dir / CSS_LIB_NAME
80
+ css_lib_path.write_text(get_css_library())
81
+
82
+ print(f"Generated: {js_lib_path} and {css_lib_path}")
83
+
84
+ if args.loader:
85
+ # Loader mode: generate HTML with file input, no model data
86
+ html_content = generate_loader_html(
87
+ include_geometry_diagnostics=args.geometry_diagnostics,
88
+ embedded=args.embedded,
89
+ cdn=args.cdn,
90
+ )
91
+ args.output.write_text(html_content)
92
+ print(f"Generated: {args.output}")
93
+ return
94
+
95
+ if args.model:
96
+ import openstudio
97
+
98
+ if not args.model.is_file():
99
+ raise ValueError(f"Error: Model file '{args.model}' does not exist.")
100
+ model = openstudio.model.Model.load(args.model).get()
101
+ else:
102
+ print("No model file provided, using example model")
103
+ model = create_example_model(include_geometry_diagnostics=args.geometry_diagnostics)
104
+ model.save(output_dir / "example_model.osm", True)
105
+ gltf_data = model_to_gltf_json(model=model, include_geometry_diagnostics=args.geometry_diagnostics)
106
+ indent = 2 if args.pretty else None
107
+ import json
108
+
109
+ (output_dir / "example_model.gltf").write_text(json.dumps(gltf_data, indent=indent))
110
+
111
+ html_content = model_to_gltf_html(
112
+ model=model,
113
+ pretty_json=args.pretty,
114
+ include_geometry_diagnostics=args.geometry_diagnostics,
115
+ embedded=args.embedded,
116
+ cdn=args.cdn,
117
+ )
118
+ args.output.write_text(html_content)
119
+ print(f"Generated: {args.output}")
120
+
121
+
122
+ if __name__ == "__main__":
123
+ main()
@@ -1,8 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
1
5
  import openstudio
2
6
  from jinja2 import Environment, PackageLoader
3
7
 
8
+ from effibemviewer import __version__
9
+
10
+ if TYPE_CHECKING:
11
+ from IPython.display import HTML, IFrame
12
+
4
13
  env = Environment(loader=PackageLoader("effibemviewer", "templates"))
5
14
 
15
+ CDN_BASE_URL = f"https://cdn.jsdelivr.net/gh/jmarrec/effibemviewer@v{__version__}/public/cdn"
16
+
6
17
 
7
18
  def model_to_gltf_json(model: openstudio.model.Model, include_geometry_diagnostics: bool = False) -> dict:
8
19
  """Convert an OpenStudio model to GLTF JSON format (dict).
@@ -27,51 +38,72 @@ def model_to_gltf_json(model: openstudio.model.Model, include_geometry_diagnosti
27
38
  return ft.modelToGLTFJSON(model)
28
39
 
29
40
 
30
- def model_to_gltf_script(
41
+ def get_js_library() -> str:
42
+ """Get the EffiBEMViewer JavaScript library content.
43
+
44
+ Returns:
45
+ str: The JavaScript library content (uses bare specifiers, requires importmap)
46
+ """
47
+ template = env.get_template("effibemviewer.js.j2")
48
+ return template.render()
49
+
50
+
51
+ def get_css_library(height: str = "100vh") -> str:
52
+ """Get the EffiBEMViewer CSS library content.
53
+
54
+ Returns:
55
+ str: The CSS library content
56
+ """
57
+ template = env.get_template("effibemviewer.css.j2")
58
+ return template.render(height=height)
59
+
60
+
61
+ def model_to_gltf_html(
31
62
  model: openstudio.model.Model,
32
- height: str = "500px",
63
+ height: str = "100vh",
33
64
  pretty_json: bool = False,
34
65
  include_geometry_diagnostics: bool = False,
66
+ embedded: bool = True,
67
+ loader_mode: bool = False,
68
+ script_only: bool = False,
69
+ cdn: bool = False,
35
70
  ) -> str:
36
- """Generate HTML/JS fragment to render an OpenStudio model as GLTF.
71
+ """Generate a full standalone HTML page for viewing an OpenStudio model.
37
72
 
38
73
  Args:
39
74
  model: OpenStudio model to render
40
- height: CSS height value (default "500px", use "100vh" for full viewport)
75
+ height: CSS height value (default "100vh" for full viewport)
41
76
  pretty_json: If True, format JSON with indentation
42
77
  include_geometry_diagnostics: If True, include geometry diagnostic info
78
+ embedded: If True, inline the JS library. If False, reference external JS file.
79
+ loader_mode: If True, generate file-input loader instead of embedding model data
80
+ script_only: If True, generate only the script fragment (for Jupyter)
81
+ cdn: If True, reference JS/CSS from jsDelivr CDN (overrides embedded)
43
82
  """
44
83
  data = model_to_gltf_json(model=model, include_geometry_diagnostics=include_geometry_diagnostics)
45
84
 
46
- template = env.get_template("gltf_viewer.html.j2")
85
+ template = env.get_template("effibemviewer.html.j2")
47
86
  indent = 2 if pretty_json else None
87
+
48
88
  return template.render(
49
89
  height=height,
50
90
  gltf_data=data,
51
91
  indent=indent,
52
92
  include_geometry_diagnostics=include_geometry_diagnostics,
93
+ embedded=embedded,
94
+ loader_mode=loader_mode,
95
+ script_only=script_only,
96
+ cdn_base_url=CDN_BASE_URL if cdn else None,
53
97
  )
54
98
 
55
99
 
56
- def model_to_gltf_html(
57
- model: openstudio.model.Model,
58
- height: str = "100vh",
59
- pretty_json: bool = False,
60
- include_geometry_diagnostics: bool = False,
61
- ) -> str:
62
- """Generate a full standalone HTML page for viewing an OpenStudio model."""
63
- fragment = model_to_gltf_script(
64
- model=model, height=height, pretty_json=pretty_json, include_geometry_diagnostics=include_geometry_diagnostics
65
- )
66
- return f"<!DOCTYPE html><html><head></head><body style='margin:0'>{fragment}</body></html>"
67
-
68
-
69
100
  def display_model(
70
101
  model: openstudio.model.Model,
71
102
  height: str = "500px",
72
103
  use_iframe: bool = False,
73
104
  include_geometry_diagnostics: bool = False,
74
- ):
105
+ cdn: bool = False,
106
+ ) -> HTML | IFrame:
75
107
  """Display an OpenStudio model in a Jupyter notebook.
76
108
 
77
109
  Args:
@@ -79,27 +111,83 @@ def display_model(
79
111
  height: CSS height value (default "500px")
80
112
  use_iframe: If True, use IFrame for nbclassic compatibility
81
113
  include_geometry_diagnostics: If True, include geometry diagnostic info
114
+ cdn: If True, load JS/CSS from CDN (better caching on re-runs)
82
115
 
83
116
  Returns:
84
117
  IPython display object (HTML or IFrame)
85
118
  """
119
+ fragment = model_to_gltf_html(
120
+ model=model,
121
+ height=height,
122
+ pretty_json=False,
123
+ include_geometry_diagnostics=include_geometry_diagnostics,
124
+ embedded=True,
125
+ loader_mode=False,
126
+ script_only=True,
127
+ cdn=cdn,
128
+ )
129
+ if not use_iframe:
130
+ from IPython.display import HTML
131
+
132
+ return HTML(fragment)
133
+
86
134
  import base64
135
+ import datetime
136
+
137
+ from IPython.display import IFrame
138
+
139
+ current_year = datetime.datetime.now().year
140
+ footer = f"""<p>
141
+ Copyright &copy; 2026 - {current_year} <a href="https://effibem.com" target="_blank">EffiBEM EURL</a>
142
+ </p>"""
143
+ full_html = f"""<!DOCTYPE html>
144
+ <html>
145
+ <head>
146
+ </head>
147
+ <body style='margin:0'>
148
+ {fragment}
149
+
150
+ <footer>
151
+ {footer}
152
+ </footer>
153
+ </body>
154
+ </html>"""
155
+ data_url = f"data:text/html;base64,{base64.b64encode(full_html.encode()).decode()}"
156
+ # Parse height for IFrame (needs integer pixels)
157
+ h = int(height.replace("px", "")) if height.endswith("px") else 500
158
+ return IFrame(src=data_url, width="100%", height=h)
159
+
160
+
161
+ def generate_loader_html(
162
+ height: str = "100vh",
163
+ include_geometry_diagnostics: bool = False,
164
+ embedded: bool = True,
165
+ cdn: bool = False,
166
+ ) -> str:
167
+ """Generate a standalone HTML page with a file input for loading GLTF files.
87
168
 
88
- from IPython.display import HTML, IFrame
169
+ Args:
170
+ height: CSS height value (default "100vh" for full viewport)
171
+ include_geometry_diagnostics: If True, enable geometry diagnostic display
172
+ embedded: If True, inline the JS library. If False, reference external JS file.
173
+ cdn: If True, reference JS/CSS from jsDelivr CDN (overrides embedded)
89
174
 
90
- if use_iframe:
91
- full_html = model_to_gltf_html(
92
- model=model, height=height, include_geometry_diagnostics=include_geometry_diagnostics
93
- )
94
- data_url = f"data:text/html;base64,{base64.b64encode(full_html.encode()).decode()}"
95
- # Parse height for IFrame (needs integer pixels)
96
- h = int(height.replace("px", "")) if height.endswith("px") else 500
97
- return IFrame(src=data_url, width="100%", height=h)
98
-
99
- fragment = model_to_gltf_script(
100
- model=model, height=height, include_geometry_diagnostics=include_geometry_diagnostics
175
+ Returns:
176
+ str: Full HTML page with file input for loading GLTF files
177
+ """
178
+ template = env.get_template("effibemviewer.html.j2")
179
+
180
+ html = template.render(
181
+ height=height,
182
+ gltf_data=None,
183
+ indent=None,
184
+ include_geometry_diagnostics=include_geometry_diagnostics,
185
+ embedded=embedded,
186
+ loader_mode=True,
187
+ script_only=False,
188
+ cdn_base_url=CDN_BASE_URL if cdn else None,
101
189
  )
102
- return HTML(fragment)
190
+ return html
103
191
 
104
192
 
105
193
  def create_example_model(include_geometry_diagnostics: bool = False) -> openstudio.model.Model:
@@ -135,6 +223,7 @@ def create_example_model(include_geometry_diagnostics: bool = False) -> openstud
135
223
 
136
224
  if include_geometry_diagnostics:
137
225
  surface = next(s for s in space_clone.surfaces() if s.surfaceType() == "Wall")
138
- surface.setVertices(openstudio.reverse(surface.vertices())) # Make one surface incorrectly oriented
226
+ # Make one surface incorrectly oriented
227
+ surface.setVertices(openstudio.reverse(surface.vertices()))
139
228
 
140
229
  return model
@@ -0,0 +1,125 @@
1
+ /**
2
+ * EffiBEM Viewer - A Three.js-based viewer for OpenStudio GLTF models
3
+ * https://github.com/jmarrec/effibemviewer
4
+ * https://effibem.com
5
+ */
6
+ html, body {
7
+ height: 100%;
8
+ margin: 0;
9
+ }
10
+ body {
11
+ display: flex;
12
+ flex-direction: column;
13
+ }
14
+ #header {
15
+ display: flex;
16
+ align-items: center;
17
+ padding: 8px 16px;
18
+ background: #fff;
19
+ border-bottom: 1px solid #e0e0e0;
20
+ font-family: sans-serif;
21
+ flex-shrink: 0;
22
+ }
23
+ #header img {
24
+ height: 32px;
25
+ margin-right: 12px;
26
+ }
27
+ #header h1 {
28
+ margin: 0;
29
+ font-size: 18px;
30
+ font-weight: 600;
31
+ color: #333;
32
+ }
33
+ .effibem-viewer {
34
+ width: 100%;
35
+ height: {{ height }};
36
+ flex: 1;
37
+ position: relative;
38
+ }
39
+ footer {
40
+ flex-shrink: 0;
41
+ padding: 8px 16px;
42
+ background: #f5f5f5;
43
+ border-top: 1px solid #e0e0e0;
44
+ font-family: sans-serif;
45
+ font-size: 12px;
46
+ color: #666;
47
+ text-align: center;
48
+ }
49
+ footer a {
50
+ color: #1a73e8;
51
+ text-decoration: none;
52
+ }
53
+ footer a:hover {
54
+ text-decoration: underline;
55
+ }
56
+ footer p {
57
+ margin: 0;
58
+ }
59
+ .effibem-viewer .controls {
60
+ position: absolute;
61
+ top: 10px;
62
+ right: 10px;
63
+ background: rgba(255,255,255,0.9);
64
+ padding: 10px;
65
+ border-radius: 4px;
66
+ font-family: sans-serif;
67
+ font-size: 12px;
68
+ z-index: 100;
69
+ }
70
+ .effibem-viewer .controls label { display: block; margin: 4px 0; cursor: pointer; }
71
+ .effibem-viewer .controls input { margin-right: 6px; }
72
+ .effibem-viewer .info-panel {
73
+ display: none;
74
+ position: absolute;
75
+ background: rgba(255,255,255,0.95);
76
+ padding: 10px 14px;
77
+ border-radius: 4px;
78
+ font-family: sans-serif;
79
+ font-size: 12px;
80
+ z-index: 100;
81
+ max-width: 300px;
82
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
83
+ pointer-events: none;
84
+ }
85
+ .effibem-viewer .info-panel h4 { margin: 0 0 8px 0; font-size: 13px; }
86
+ .effibem-viewer .info-panel .info-row { margin: 4px 0; }
87
+ .effibem-viewer .info-panel .info-row.emphasized { background: #1a73e8; color: white; margin: 4px -8px; padding: 4px 8px; border-radius: 4px; font-weight: 600; }
88
+ .effibem-viewer .info-panel .info-row.emphasized .info-label { color: white; }
89
+ .effibem-viewer .info-panel .info-label { color: #666; }
90
+ .effibem-viewer .badge {
91
+ display: inline-block;
92
+ padding: 2px 8px;
93
+ font-size: 11px;
94
+ font-weight: 600;
95
+ border-radius: 10px;
96
+ text-transform: capitalize;
97
+ }
98
+ .effibem-viewer .badge-success { background-color: #198754; color: white; }
99
+ .effibem-viewer .badge-danger { background-color: #dc3545; color: white; }
100
+ .effibem-viewer .diagnostics-section { display: none; }
101
+ .effibem-viewer.include-diagnostics .diagnostics-section { display: block; }
102
+ .effibem-loader {
103
+ position: absolute;
104
+ top: 50%;
105
+ left: 50%;
106
+ transform: translate(-50%, -50%);
107
+ text-align: center;
108
+ font-family: sans-serif;
109
+ z-index: 50;
110
+ }
111
+ .effibem-loader.hidden { display: none; }
112
+ .effibem-loader h2 {
113
+ margin: 0 0 8px 0;
114
+ font-size: 18px;
115
+ font-weight: 600;
116
+ color: #333;
117
+ }
118
+ .effibem-loader p {
119
+ margin: 0 0 16px 0;
120
+ font-size: 13px;
121
+ color: #666;
122
+ }
123
+ .effibem-loader input[type="file"] {
124
+ font-size: 14px;
125
+ }
@@ -0,0 +1,162 @@
1
+ {% if not script_only %}
2
+ <!DOCTYPE html>
3
+ <html>
4
+ <head>
5
+ <meta charset="utf-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <meta name="description" content="EffiBEM Viewer - A Three.js-based viewer for OpenStudio GLTF modelsS">
8
+ <meta name="author" content="EffiBEM EURL">
9
+ <meta name="keywords" content="EnergyPlus, OpenStudio, Python, GLTF, Jupyter">
10
+ <title>EffiBEM Viewer</title>
11
+
12
+ <!-- Favicons -->
13
+ <link rel="icon" type="image/png" href="https://effibem.com/images/ico/favicon-96x96.png" sizes="96x96">
14
+ <link rel="icon" type="image/svg+xml" href="https://effibem.com/images/ico/favicon.svg">
15
+ <link rel="shortcut icon" href="https://effibem.com/images/ico/favicon.ico">
16
+ <link rel="apple-touch-icon" sizes="180x180" href="https://effibem.com/images/ico/apple-touch-icon.png">
17
+ <meta name="apple-mobile-web-app-title" content="EffiBEM">
18
+ <link rel="manifest" href="https://effibem.com/images/ico/site.webmanifest">
19
+ {% endif %}
20
+
21
+ {# CDN takes precedence over embedded, so check cdn_base_url first #}
22
+ {% if cdn_base_url %}
23
+ <link rel="stylesheet" href="{{ cdn_base_url }}/effibemviewer.css">
24
+ {% elif embedded %}
25
+ <style>
26
+ {% include "effibemviewer.css.j2" %}
27
+ </style>
28
+ {% else %}
29
+ <link rel="stylesheet" href="./effibemviewer.css">
30
+ {% endif %}
31
+
32
+ {% if not script_only %}
33
+ </head>
34
+
35
+ <body>
36
+ <header id="header">
37
+ <img src="https://effibem.com/images/logo.png" alt="EffiBEM Logo">
38
+ <h1>EffiBEM Viewer</h1>
39
+ </header>
40
+
41
+ {% if loader_mode %}
42
+ <div id="loaderPrompt" class="effibem-loader">
43
+ <h2>EffiBEM Viewer</h2>
44
+ <p>Select an OpenStudio GLTF file to visualize</p>
45
+ <input type="file" id="fileInput" accept=".gltf,.json">
46
+ </div>
47
+ {% endif %}
48
+ {% endif %}
49
+
50
+ <div id="viewer" class="effibem-viewer" style="height: {{ height }};">
51
+
52
+ <div class="controls">
53
+ <div style="margin-bottom: 8px;">
54
+ <label style="display: inline;"><strong>Render By</strong></label>
55
+ <select class="renderBy" style="margin-left: 8px; font-size: 12px;">
56
+ <option value="surfaceType">Surface Type</option>
57
+ <option value="boundary">Boundary</option>
58
+ <option value="construction">Construction</option>
59
+ <option value="thermalZone">Thermal Zone</option>
60
+ <option value="spaceType">Space Type</option>
61
+ <option value="buildingStory">Building Story</option>
62
+ </select>
63
+ </div>
64
+ <div style="margin: 8px 0;">
65
+ <label style="display: inline;"><strong>Show Story</strong></label>
66
+ <select class="showStory" style="margin-left: 8px; font-size: 12px;">
67
+ <option value="">All Stories</option>
68
+ </select>
69
+ </div>
70
+ <hr style="margin: 8px 0; border: none; border-top: 1px solid #ccc;">
71
+ <strong>Surface Filters</strong>
72
+ <label><input type="checkbox" class="showFloors" checked> Floors</label>
73
+ <label><input type="checkbox" class="showWalls" checked> Walls</label>
74
+ <label><input type="checkbox" class="showRoofs" checked> Roofs/Ceilings</label>
75
+ <label><input type="checkbox" class="showWindows" checked> Windows</label>
76
+ <label><input type="checkbox" class="showDoors" checked> Doors</label>
77
+ <label><input type="checkbox" class="showShading" checked> Shading</label>
78
+ <label><input type="checkbox" class="showPartitions" checked> Partitions</label>
79
+ <hr style="margin: 8px 0; border: none; border-top: 1px solid #ccc;">
80
+ <label><input type="checkbox" class="showEdges" checked> Show Edges</label>
81
+ <div class="diagnostics-section">
82
+ <hr style="margin: 8px 0; border: none; border-top: 1px solid #ccc;">
83
+ <strong>Geometry Diagnostics</strong>
84
+ <label><input type="checkbox" class="showOnlyNonConvexSurfaces"> Non-Convex Surfaces Only</label>
85
+ <label><input type="checkbox" class="showOnlyIncorrectlyOriented"> Incorrectly Oriented Only</label>
86
+ <label><input type="checkbox" class="showOnlyNonConvexSpaces"> Non-Convex Spaces Only</label>
87
+ <label><input type="checkbox" class="showOnlyNonEnclosedSpaces"> Non-Enclosed Spaces Only</label>
88
+ </div>
89
+ </div>
90
+
91
+ <div class="info-panel">
92
+ <h4 class="info-name"></h4>
93
+ <div class="info-row info-type-row"><span class="info-label">Surface Type:</span> <span class="info-type"></span></div>
94
+ <div class="info-row info-space-row"><span class="info-label">Space:</span> <span class="info-space"></span></div>
95
+ <div class="info-row info-spaceType-row"><span class="info-label">Space Type:</span> <span class="info-spaceType"></span></div>
96
+ <div class="info-row info-thermalZone-row"><span class="info-label">Thermal Zone:</span> <span class="info-thermalZone"></span></div>
97
+ <div class="info-row info-buildingStory-row"><span class="info-label">Building Story:</span> <span class="info-buildingStory"></span></div>
98
+ <div class="info-row info-construction-row"><span class="info-label">Construction:</span> <span class="info-construction"></span></div>
99
+ <div class="info-row info-boundary-row"><span class="info-label">Boundary:</span> <span class="info-boundary"></span></div>
100
+ <div class="info-row info-boundaryObject-row"><span class="info-label">Adjacent To:</span> <span class="info-boundaryObject"></span></div>
101
+ <div class="info-row info-sunExposure-row"><span class="info-label">Sun Exposure:</span> <span class="info-sunExposure"></span></div>
102
+ <div class="info-row info-windExposure-row"><span class="info-label">Wind Exposure:</span> <span class="info-windExposure"></span></div>
103
+ <div class="diagnostics-section">
104
+ <div class="info-row info-convex-row"><span class="info-label">Convex:</span> <span class="info-convex"></span></div>
105
+ <div class="info-row info-correctlyOriented-row"><span class="info-label">Correctly Oriented:</span> <span class="info-correctlyOriented"></span></div>
106
+ <div class="info-row info-spaceConvex-row"><span class="info-label">Space Convex:</span> <span class="info-spaceConvex"></span></div>
107
+ <div class="info-row info-spaceEnclosed-row"><span class="info-label">Space Enclosed:</span> <span class="info-spaceEnclosed"></span></div>
108
+ </div>
109
+ </div>
110
+ </div>
111
+
112
+ {% if not script_only %}
113
+ <footer>
114
+ <p>Copyright &copy; 2026 - {{ current_year }} <a href="https://effibem.com" target="_blank">EffiBEM EURL</a></p>
115
+ </footer>
116
+ {% endif %}
117
+
118
+ <script type="importmap">
119
+ {
120
+ "imports": {
121
+ "three": "https://cdn.jsdelivr.net/npm/three@0.182.0/build/three.module.js",
122
+ "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.182.0/examples/jsm/"
123
+ }
124
+ }
125
+ </script>
126
+
127
+ {# CDN takes precedence over embedded, so check cdn_base_url first #}
128
+ {% if cdn_base_url %}
129
+ <script type="module" src="{{ cdn_base_url }}/effibemviewer.js"></script>
130
+ {% elif embedded %}
131
+ <script type="module">
132
+ {% include "effibemviewer.js.j2" %}
133
+ </script>
134
+ {% else %}
135
+ <script type="module" src="./effibemviewer.js"></script>
136
+ {% endif %}
137
+
138
+ <script type="module">
139
+ {% if loader_mode %}
140
+ const options = { includeGeometryDiagnostics: {{ include_geometry_diagnostics | tojson }} };
141
+ const viewer = new EffiBEMViewer('viewer', options);
142
+
143
+ document.getElementById('fileInput').addEventListener('change', (e) => {
144
+ const file = e.target.files[0];
145
+ if (file) {
146
+ document.getElementById('loaderPrompt').classList.add('hidden');
147
+ viewer.loadFromFileObject(file);
148
+ }
149
+ });
150
+ {% else %}
151
+ const gltfData = {{ gltf_data | tojson(indent=indent) }};
152
+
153
+ const options = { includeGeometryDiagnostics: {{ include_geometry_diagnostics | tojson }} };
154
+ const viewer = new EffiBEMViewer('viewer', options);
155
+ viewer.loadFromJSON(gltfData);
156
+ {% endif %}
157
+ </script>
158
+
159
+ {% if not script_only %}
160
+ </body>
161
+ </html>
162
+ {% endif %}