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.
- 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.2.dist-info → effibemviewer-0.2.0.dist-info}/METADATA +1 -1
- effibemviewer-0.2.0.dist-info/RECORD +12 -0
- effibemviewer-0.1.2.dist-info/RECORD +0 -9
- {effibemviewer-0.1.2.dist-info → effibemviewer-0.2.0.dist-info}/WHEEL +0 -0
- {effibemviewer-0.1.2.dist-info → effibemviewer-0.2.0.dist-info}/entry_points.txt +0 -0
- {effibemviewer-0.1.2.dist-info → effibemviewer-0.2.0.dist-info}/licenses/LICENSE +0 -0
effibemviewer/__init__.py
CHANGED
|
@@ -2,20 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
__author__ = """Julien Marrec"""
|
|
4
4
|
__email__ = 'contact@effibem.com'
|
|
5
|
-
__version__ = '0.
|
|
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
|
]
|
effibemviewer/__main__.py
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
|
|
4
|
-
from effibemviewer.gltf import
|
|
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"
|
|
5
19
|
|
|
6
20
|
|
|
7
21
|
def main():
|
|
@@ -21,10 +35,56 @@ def main():
|
|
|
21
35
|
help="Include geometry diagnostics (convex, correctly oriented, etc.)",
|
|
22
36
|
)
|
|
23
37
|
parser.add_argument(
|
|
24
|
-
"-o", "--output", type=Path, default=Path("
|
|
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",
|
|
25
62
|
)
|
|
26
63
|
args = parser.parse_args()
|
|
27
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
|
+
|
|
28
88
|
if args.model:
|
|
29
89
|
import openstudio
|
|
30
90
|
|
|
@@ -34,11 +94,22 @@ def main():
|
|
|
34
94
|
else:
|
|
35
95
|
print("No model file provided, using example model")
|
|
36
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))
|
|
37
103
|
|
|
38
|
-
|
|
39
|
-
|
|
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,
|
|
40
110
|
)
|
|
41
|
-
|
|
111
|
+
args.output.write_text(html_content)
|
|
112
|
+
print(f"Generated: {args.output}")
|
|
42
113
|
|
|
43
114
|
|
|
44
115
|
if __name__ == "__main__":
|
effibemviewer/gltf.py
CHANGED
|
@@ -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
|
|
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 = "
|
|
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
|
|
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 "
|
|
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("
|
|
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
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
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
|
-
|
|
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 © 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 %}
|