effibemviewer 0.1.1__tar.gz → 0.2.0__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.1
3
+ Version: 0.2.0
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.1'
5
+ __version__ = '0.2.0'
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,116 @@
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 main():
22
+ """Command-line interface for generating GLTF viewer HTML from an OpenStudio model."""
23
+ parser = argparse.ArgumentParser(description="Generate GLTF viewer HTML from OpenStudio model")
24
+ # -m, --model: Path to the OpenStudio model file (optional, defaults to an example model)
25
+ parser.add_argument(
26
+ "-m",
27
+ "--model",
28
+ type=Path,
29
+ help="Path to the OpenStudio model file (optional, defaults to an example model)",
30
+ )
31
+ parser.add_argument(
32
+ "-g",
33
+ "--geometry-diagnostics",
34
+ action="store_true",
35
+ help="Include geometry diagnostics (convex, correctly oriented, etc.)",
36
+ )
37
+ parser.add_argument(
38
+ "-o", "--output", type=Path, default=Path("viewer.html"), help="Output HTML file path (default: viewer.html)"
39
+ )
40
+ # --embedded and --cdn are mutually exclusive options for how to include the JS library
41
+ lib_mode = parser.add_mutually_exclusive_group()
42
+ lib_mode.add_argument(
43
+ "--embedded",
44
+ action="store_true",
45
+ help="Embed JS library inline in HTML (default: generate separate effibemviewer.js file)",
46
+ )
47
+ lib_mode.add_argument(
48
+ "--cdn",
49
+ action="store_true",
50
+ help="Reference JS library from CDN instead of embedding or generating local file",
51
+ )
52
+
53
+ parser.add_argument(
54
+ "--pretty",
55
+ action="store_true",
56
+ help="Pretty-print the JSON output in the HTML (default: compact JSON)",
57
+ )
58
+ parser.add_argument(
59
+ "--loader",
60
+ action="store_true",
61
+ help="Generate a loader HTML with file input instead of embedding model data",
62
+ )
63
+ args = parser.parse_args()
64
+
65
+ # Determine paths (relative to output HTML)
66
+ output_dir = args.output.parent
67
+ output_dir.mkdir(parents=True, exist_ok=True)
68
+
69
+ if not args.embedded and not args.cdn:
70
+ js_lib_path = output_dir / JS_LIB_NAME
71
+ js_lib_path.write_text(get_js_library())
72
+ css_lib_path = output_dir / CSS_LIB_NAME
73
+ css_lib_path.write_text(get_css_library())
74
+
75
+ print(f"Generated: {js_lib_path} and {css_lib_path}")
76
+
77
+ if args.loader:
78
+ # Loader mode: generate HTML with file input, no model data
79
+ html_content = generate_loader_html(
80
+ include_geometry_diagnostics=args.geometry_diagnostics,
81
+ embedded=args.embedded,
82
+ cdn=args.cdn,
83
+ )
84
+ args.output.write_text(html_content)
85
+ print(f"Generated: {args.output}")
86
+ return
87
+
88
+ if args.model:
89
+ import openstudio
90
+
91
+ if not args.model.is_file():
92
+ raise ValueError(f"Error: Model file '{args.model}' does not exist.")
93
+ model = openstudio.model.Model.load(args.model).get()
94
+ else:
95
+ print("No model file provided, using example model")
96
+ model = create_example_model(include_geometry_diagnostics=args.geometry_diagnostics)
97
+ model.save(output_dir / "example_model.osm", True)
98
+ gltf_data = model_to_gltf_json(model=model, include_geometry_diagnostics=args.geometry_diagnostics)
99
+ indent = 2 if args.pretty else None
100
+ import json
101
+
102
+ (output_dir / "example_model.gltf").write_text(json.dumps(gltf_data, indent=indent))
103
+
104
+ html_content = model_to_gltf_html(
105
+ model=model,
106
+ pretty_json=args.pretty,
107
+ include_geometry_diagnostics=args.geometry_diagnostics,
108
+ embedded=args.embedded,
109
+ cdn=args.cdn,
110
+ )
111
+ args.output.write_text(html_content)
112
+ print(f"Generated: {args.output}")
113
+
114
+
115
+ if __name__ == "__main__":
116
+ 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,71 @@ 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
+ ) -> HTML | IFrame:
75
106
  """Display an OpenStudio model in a Jupyter notebook.
76
107
 
77
108
  Args:
@@ -83,23 +114,68 @@ def display_model(
83
114
  Returns:
84
115
  IPython display object (HTML or IFrame)
85
116
  """
117
+ fragment = model_to_gltf_html(
118
+ model=model,
119
+ height=height,
120
+ pretty_json=False,
121
+ include_geometry_diagnostics=include_geometry_diagnostics,
122
+ embedded=True,
123
+ loader_mode=False,
124
+ script_only=True,
125
+ )
126
+ if not use_iframe:
127
+ from IPython.display import HTML
128
+
129
+ return HTML(fragment)
130
+
86
131
  import base64
87
132
 
88
- from IPython.display import HTML, IFrame
133
+ from IPython.display import IFrame
134
+
135
+ full_html = f"""<!DOCTYPE html>
136
+ <html>
137
+ <head>
138
+ </head>
139
+ <body style='margin:0'>
140
+ {fragment}
141
+ </body>
142
+ </html>"""
143
+ data_url = f"data:text/html;base64,{base64.b64encode(full_html.encode()).decode()}"
144
+ # Parse height for IFrame (needs integer pixels)
145
+ h = int(height.replace("px", "")) if height.endswith("px") else 500
146
+ return IFrame(src=data_url, width="100%", height=h)
89
147
 
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
148
+
149
+ def generate_loader_html(
150
+ height: str = "100vh",
151
+ include_geometry_diagnostics: bool = False,
152
+ embedded: bool = True,
153
+ cdn: bool = False,
154
+ ) -> str:
155
+ """Generate a standalone HTML page with a file input for loading GLTF files.
156
+
157
+ Args:
158
+ height: CSS height value (default "100vh" for full viewport)
159
+ include_geometry_diagnostics: If True, enable geometry diagnostic display
160
+ embedded: If True, inline the JS library. If False, reference external JS file.
161
+ cdn: If True, reference JS/CSS from jsDelivr CDN (overrides embedded)
162
+
163
+ Returns:
164
+ str: Full HTML page with file input for loading GLTF files
165
+ """
166
+ template = env.get_template("effibemviewer.html.j2")
167
+
168
+ html = template.render(
169
+ height=height,
170
+ gltf_data=None,
171
+ indent=None,
172
+ include_geometry_diagnostics=include_geometry_diagnostics,
173
+ embedded=embedded,
174
+ loader_mode=True,
175
+ script_only=False,
176
+ cdn_base_url=CDN_BASE_URL if cdn else None,
101
177
  )
102
- return HTML(fragment)
178
+ return html
103
179
 
104
180
 
105
181
  def create_example_model(include_geometry_diagnostics: bool = False) -> openstudio.model.Model:
@@ -135,6 +211,7 @@ def create_example_model(include_geometry_diagnostics: bool = False) -> openstud
135
211
 
136
212
  if include_geometry_diagnostics:
137
213
  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
214
+ # Make one surface incorrectly oriented
215
+ surface.setVertices(openstudio.reverse(surface.vertices()))
139
216
 
140
217
  return model
@@ -0,0 +1,120 @@
1
+ html, body {
2
+ height: 100%;
3
+ margin: 0;
4
+ }
5
+ body {
6
+ display: flex;
7
+ flex-direction: column;
8
+ }
9
+ #header {
10
+ display: flex;
11
+ align-items: center;
12
+ padding: 8px 16px;
13
+ background: #fff;
14
+ border-bottom: 1px solid #e0e0e0;
15
+ font-family: sans-serif;
16
+ flex-shrink: 0;
17
+ }
18
+ #header img {
19
+ height: 32px;
20
+ margin-right: 12px;
21
+ }
22
+ #header h1 {
23
+ margin: 0;
24
+ font-size: 18px;
25
+ font-weight: 600;
26
+ color: #333;
27
+ }
28
+ .effibem-viewer {
29
+ width: 100%;
30
+ height: {{ height }};
31
+ flex: 1;
32
+ position: relative;
33
+ }
34
+ footer {
35
+ flex-shrink: 0;
36
+ padding: 8px 16px;
37
+ background: #f5f5f5;
38
+ border-top: 1px solid #e0e0e0;
39
+ font-family: sans-serif;
40
+ font-size: 12px;
41
+ color: #666;
42
+ text-align: center;
43
+ }
44
+ footer a {
45
+ color: #1a73e8;
46
+ text-decoration: none;
47
+ }
48
+ footer a:hover {
49
+ text-decoration: underline;
50
+ }
51
+ footer p {
52
+ margin: 0;
53
+ }
54
+ .effibem-viewer .controls {
55
+ position: absolute;
56
+ top: 10px;
57
+ right: 10px;
58
+ background: rgba(255,255,255,0.9);
59
+ padding: 10px;
60
+ border-radius: 4px;
61
+ font-family: sans-serif;
62
+ font-size: 12px;
63
+ z-index: 100;
64
+ }
65
+ .effibem-viewer .controls label { display: block; margin: 4px 0; cursor: pointer; }
66
+ .effibem-viewer .controls input { margin-right: 6px; }
67
+ .effibem-viewer .info-panel {
68
+ display: none;
69
+ position: absolute;
70
+ background: rgba(255,255,255,0.95);
71
+ padding: 10px 14px;
72
+ border-radius: 4px;
73
+ font-family: sans-serif;
74
+ font-size: 12px;
75
+ z-index: 100;
76
+ max-width: 300px;
77
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
78
+ pointer-events: none;
79
+ }
80
+ .effibem-viewer .info-panel h4 { margin: 0 0 8px 0; font-size: 13px; }
81
+ .effibem-viewer .info-panel .info-row { margin: 4px 0; }
82
+ .effibem-viewer .info-panel .info-row.emphasized { background: #1a73e8; color: white; margin: 4px -8px; padding: 4px 8px; border-radius: 4px; font-weight: 600; }
83
+ .effibem-viewer .info-panel .info-row.emphasized .info-label { color: white; }
84
+ .effibem-viewer .info-panel .info-label { color: #666; }
85
+ .effibem-viewer .badge {
86
+ display: inline-block;
87
+ padding: 2px 8px;
88
+ font-size: 11px;
89
+ font-weight: 600;
90
+ border-radius: 10px;
91
+ text-transform: capitalize;
92
+ }
93
+ .effibem-viewer .badge-success { background-color: #198754; color: white; }
94
+ .effibem-viewer .badge-danger { background-color: #dc3545; color: white; }
95
+ .effibem-viewer .diagnostics-section { display: none; }
96
+ .effibem-viewer.include-diagnostics .diagnostics-section { display: block; }
97
+ .effibem-loader {
98
+ position: absolute;
99
+ top: 50%;
100
+ left: 50%;
101
+ transform: translate(-50%, -50%);
102
+ text-align: center;
103
+ font-family: sans-serif;
104
+ z-index: 50;
105
+ }
106
+ .effibem-loader.hidden { display: none; }
107
+ .effibem-loader h2 {
108
+ margin: 0 0 8px 0;
109
+ font-size: 18px;
110
+ font-weight: 600;
111
+ color: #333;
112
+ }
113
+ .effibem-loader p {
114
+ margin: 0 0 16px 0;
115
+ font-size: 13px;
116
+ color: #666;
117
+ }
118
+ .effibem-loader input[type="file"] {
119
+ font-size: 14px;
120
+ }
@@ -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 - OpenStudio Model to GLTF with ThreeJS">
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">
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>. All rights reserved.</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 %}