effibemviewer 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026, Julien Marrec
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,51 @@
1
+ Metadata-Version: 2.4
2
+ Name: effibemviewer
3
+ Version: 0.1.0
4
+ Summary: A 3D viewer for OpenStudio building energy models using GLTF and Three.js
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Author: Julien Marrec
8
+ Author-email: contact@effibem.com
9
+ Requires-Python: >=3.10,<4.0
10
+ Classifier: Development Status :: 2 - Pre-Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Natural Language :: English
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Requires-Dist: jinja2 (>=3.1,<4.0)
21
+ Requires-Dist: openstudio (>=3.4,<4.0)
22
+ Project-URL: Changelog, https://github.com/jmarrec/effibemviewer/blob/main/CHANGELOG.md
23
+ Project-URL: Documentation, https://jmarrec.github.io/effibemviewer
24
+ Project-URL: Homepage, https://effibem.com
25
+ Project-URL: Issues, https://github.com/jmarrec/effibemviewer/issues
26
+ Project-URL: Repository, https://github.com/jmarrec/effibemviewer
27
+ Description-Content-Type: text/markdown
28
+
29
+ # EffiBEM Viewer
30
+
31
+
32
+ [![pypi](https://img.shields.io/pypi/v/effibemviewer.svg)](https://pypi.org/project/effibemviewer/)
33
+ [![python](https://img.shields.io/pypi/pyversions/effibemviewer.svg)](https://pypi.org/project/effibemviewer/)
34
+ [![Build Status](https://github.com/jmarrec/effibemviewer/actions/workflows/dev.yml/badge.svg)](https://github.com/jmarrec/effibemviewer/actions/workflows/dev.yml)
35
+ [![codecov](https://codecov.io/gh/jmarrec/effibemviewer/branch/main/graphs/badge.svg)](https://codecov.io/github/jmarrec/effibemviewer)
36
+
37
+
38
+ A 3D viewer for OpenStudio building energy models using GLTF and Three.js, that can create a standalone HTML page or be embedded into a Jupyter Notebook.
39
+
40
+ A Jupyter Notebook [Gltf_notebook.ipynb](Gltf_notebook.ipynb) can serve as a small demonstration of the features.
41
+
42
+
43
+ * Documentation: <https://jmarrec.github.io/effibemviewer>
44
+ * GitHub: <https://github.com/jmarrec/effibemviewer>
45
+ * PyPI: <https://pypi.org/project/effibemviewer/>
46
+
47
+
48
+ This is free software (MIT License) contributed by [EffiBEM](https://effibem.com).
49
+
50
+ Leveraging software, [EffiBEM](https://effibem.com) specializes in providing new ways to streamline your workflows and create new tools that work with limited inputs for your specific applications. We also offer support and training services on BEM simulation engines (OpenStudio and EnergyPlus).
51
+
@@ -0,0 +1,22 @@
1
+ # EffiBEM Viewer
2
+
3
+
4
+ [![pypi](https://img.shields.io/pypi/v/effibemviewer.svg)](https://pypi.org/project/effibemviewer/)
5
+ [![python](https://img.shields.io/pypi/pyversions/effibemviewer.svg)](https://pypi.org/project/effibemviewer/)
6
+ [![Build Status](https://github.com/jmarrec/effibemviewer/actions/workflows/dev.yml/badge.svg)](https://github.com/jmarrec/effibemviewer/actions/workflows/dev.yml)
7
+ [![codecov](https://codecov.io/gh/jmarrec/effibemviewer/branch/main/graphs/badge.svg)](https://codecov.io/github/jmarrec/effibemviewer)
8
+
9
+
10
+ A 3D viewer for OpenStudio building energy models using GLTF and Three.js, that can create a standalone HTML page or be embedded into a Jupyter Notebook.
11
+
12
+ A Jupyter Notebook [Gltf_notebook.ipynb](Gltf_notebook.ipynb) can serve as a small demonstration of the features.
13
+
14
+
15
+ * Documentation: <https://jmarrec.github.io/effibemviewer>
16
+ * GitHub: <https://github.com/jmarrec/effibemviewer>
17
+ * PyPI: <https://pypi.org/project/effibemviewer/>
18
+
19
+
20
+ This is free software (MIT License) contributed by [EffiBEM](https://effibem.com).
21
+
22
+ Leveraging software, [EffiBEM](https://effibem.com) specializes in providing new ways to streamline your workflows and create new tools that work with limited inputs for your specific applications. We also offer support and training services on BEM simulation engines (OpenStudio and EnergyPlus).
@@ -0,0 +1,21 @@
1
+ """Top-level package for EffiBEMViewer."""
2
+
3
+ __author__ = """Julien Marrec"""
4
+ __email__ = 'contact@effibem.com'
5
+ __version__ = '0.1.0'
6
+
7
+ from effibemviewer.gltf import (
8
+ create_example_model,
9
+ display_model,
10
+ model_to_gltf_html,
11
+ model_to_gltf_json,
12
+ model_to_gltf_script,
13
+ )
14
+
15
+ __all__ = [
16
+ "create_example_model",
17
+ "display_model",
18
+ "model_to_gltf_html",
19
+ "model_to_gltf_json",
20
+ "model_to_gltf_script",
21
+ ]
@@ -0,0 +1,45 @@
1
+ import argparse
2
+ from pathlib import Path
3
+
4
+ from effibemviewer.gltf import create_example_model, model_to_gltf_html
5
+
6
+
7
+ def main():
8
+ """Command-line interface for generating GLTF viewer HTML from an OpenStudio model."""
9
+ parser = argparse.ArgumentParser(description="Generate GLTF viewer HTML from OpenStudio model")
10
+ # -m, --model: Path to the OpenStudio model file (optional, defaults to an example model)
11
+ parser.add_argument(
12
+ "-m",
13
+ "--model",
14
+ type=Path,
15
+ help="Path to the OpenStudio model file (optional, defaults to an example model)",
16
+ )
17
+ parser.add_argument(
18
+ "-g",
19
+ "--geometry-diagnostics",
20
+ action="store_true",
21
+ help="Include geometry diagnostics (convex, correctly oriented, etc.)",
22
+ )
23
+ parser.add_argument(
24
+ "-o", "--output", type=Path, default=Path("test.html"), help="Output HTML file path (default: test.html)"
25
+ )
26
+ args = parser.parse_args()
27
+
28
+ if args.model:
29
+ import openstudio
30
+
31
+ if not args.model.is_file():
32
+ raise ValueError(f"Error: Model file '{args.model}' does not exist.")
33
+ model = openstudio.model.Model.load(args.model).get()
34
+ else:
35
+ print("No model file provided, using example model")
36
+ model = create_example_model(include_geometry_diagnostics=args.geometry_diagnostics)
37
+
38
+ args.output.write_text(
39
+ model_to_gltf_html(model=model, pretty_json=True, include_geometry_diagnostics=args.geometry_diagnostics)
40
+ )
41
+ model.save("model.osm", True)
42
+
43
+
44
+ if __name__ == "__main__":
45
+ main()
@@ -0,0 +1,140 @@
1
+ import openstudio
2
+ from jinja2 import Environment, PackageLoader
3
+
4
+ env = Environment(loader=PackageLoader("effibemviewer", "templates"))
5
+
6
+
7
+ def model_to_gltf_json(model: openstudio.model.Model, include_geometry_diagnostics: bool = False) -> dict:
8
+ """Convert an OpenStudio model to GLTF JSON format (dict).
9
+
10
+ Args:
11
+ model: OpenStudio model to convert
12
+ include_geometry_diagnostics: If True, include geometry diagnostic info in the output
13
+
14
+ Returns:
15
+ dict: GLTF JSON data representing the model
16
+
17
+ Raises:
18
+ ValueError: If geometry diagnostics are requested but not supported by the OpenStudio version
19
+ """
20
+ ft = openstudio.gltf.GltfForwardTranslator()
21
+ if include_geometry_diagnostics:
22
+ if not callable(getattr(openstudio.gltf.GltfForwardTranslator, "setIncludeGeometryDiagnostics", None)):
23
+ raise ValueError(
24
+ "Geometry diagnostics not supported in this version of OpenStudio. Please update to use this feature."
25
+ )
26
+ ft.setIncludeGeometryDiagnostics(True)
27
+ return ft.modelToGLTFJSON(model)
28
+
29
+
30
+ def model_to_gltf_script(
31
+ model: openstudio.model.Model,
32
+ height: str = "500px",
33
+ pretty_json: bool = False,
34
+ include_geometry_diagnostics: bool = False,
35
+ ) -> str:
36
+ """Generate HTML/JS fragment to render an OpenStudio model as GLTF.
37
+
38
+ Args:
39
+ model: OpenStudio model to render
40
+ height: CSS height value (default "500px", use "100vh" for full viewport)
41
+ pretty_json: If True, format JSON with indentation
42
+ include_geometry_diagnostics: If True, include geometry diagnostic info
43
+ """
44
+ data = model_to_gltf_json(model=model, include_geometry_diagnostics=include_geometry_diagnostics)
45
+
46
+ template = env.get_template("gltf_viewer.html.j2")
47
+ indent = 2 if pretty_json else None
48
+ return template.render(
49
+ height=height,
50
+ gltf_data=data,
51
+ indent=indent,
52
+ include_geometry_diagnostics=include_geometry_diagnostics,
53
+ )
54
+
55
+
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
+ def display_model(
70
+ model: openstudio.model.Model,
71
+ height: str = "500px",
72
+ use_iframe: bool = False,
73
+ include_geometry_diagnostics: bool = False,
74
+ ):
75
+ """Display an OpenStudio model in a Jupyter notebook.
76
+
77
+ Args:
78
+ model: OpenStudio model to render
79
+ height: CSS height value (default "500px")
80
+ use_iframe: If True, use IFrame for nbclassic compatibility
81
+ include_geometry_diagnostics: If True, include geometry diagnostic info
82
+
83
+ Returns:
84
+ IPython display object (HTML or IFrame)
85
+ """
86
+ import base64
87
+
88
+ from IPython.display import HTML, IFrame
89
+
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
101
+ )
102
+ return HTML(fragment)
103
+
104
+
105
+ def create_example_model(include_geometry_diagnostics: bool = False) -> openstudio.model.Model:
106
+ """Create an example OpenStudio model with two stories and optionally geometry diagnostics.
107
+
108
+ Args:
109
+ include_geometry_diagnostics: If True, will reverse a surface on purpose
110
+
111
+ Returns:
112
+ model (openstudio.model.Model): Example model
113
+ """
114
+ model = openstudio.model.exampleModel()
115
+ space = model.getSpaceByName("Space 1").get()
116
+ # Move space type assignment from Building to Space, so I can have one without it
117
+ space_type = space.spaceType().get()
118
+ [space.setSpaceType(space_type) for space in model.getSpaces()]
119
+ b = model.getBuilding()
120
+ b.setNorthAxis(45)
121
+ b.resetSpaceType()
122
+
123
+ space_clone = space.clone(model).to_Space().get()
124
+ space_clone.setName("Space Level 2")
125
+ space_clone.resetSpaceType()
126
+ # Set it above the original space for better viewing
127
+ z = space.boundingBox().maxZ().get()
128
+ assert z == 3.0
129
+ space_clone.setZOrigin(z)
130
+ story2 = openstudio.model.BuildingStory(model)
131
+ story2.setName("Second Story")
132
+ story2.setNominalZCoordinate(z)
133
+ story2.setNominalFloortoFloorHeight(z)
134
+ space_clone.setBuildingStory(story2)
135
+
136
+ if include_geometry_diagnostics:
137
+ 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
139
+
140
+ return model
@@ -0,0 +1,647 @@
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
+ <style>
11
+ #viewer { width: 100%; height: {{ height }}; position: relative; }
12
+ #controls {
13
+ position: absolute;
14
+ top: 10px;
15
+ right: 10px;
16
+ background: rgba(255,255,255,0.9);
17
+ padding: 10px;
18
+ border-radius: 4px;
19
+ font-family: sans-serif;
20
+ font-size: 12px;
21
+ z-index: 100;
22
+ }
23
+ #controls label { display: block; margin: 4px 0; cursor: pointer; }
24
+ #controls input { margin-right: 6px; }
25
+ #info-panel {
26
+ display: none;
27
+ position: absolute;
28
+ background: rgba(255,255,255,0.95);
29
+ padding: 10px 14px;
30
+ border-radius: 4px;
31
+ font-family: sans-serif;
32
+ font-size: 12px;
33
+ z-index: 100;
34
+ max-width: 300px;
35
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
36
+ pointer-events: none;
37
+ }
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 {
44
+ display: inline-block;
45
+ padding: 2px 8px;
46
+ font-size: 11px;
47
+ font-weight: 600;
48
+ border-radius: 10px;
49
+ text-transform: capitalize;
50
+ }
51
+ .badge-success { background-color: #198754; color: white; }
52
+ .badge-danger { background-color: #dc3545; color: white; }
53
+ </style>
54
+
55
+ <div id="viewer">
56
+ <div id="controls">
57
+ <div style="margin-bottom: 8px;">
58
+ <label style="display: inline;"><strong>Render By</strong></label>
59
+ <select id="renderBy" style="margin-left: 8px; font-size: 12px;">
60
+ <option value="surfaceType">Surface Type</option>
61
+ <option value="boundary">Boundary</option>
62
+ <option value="construction">Construction</option>
63
+ <option value="thermalZone">Thermal Zone</option>
64
+ <option value="spaceType">Space Type</option>
65
+ <option value="buildingStory">Building Story</option>
66
+ </select>
67
+ </div>
68
+ <div style="margin: 8px 0;">
69
+ <label style="display: inline;"><strong>Show Story</strong></label>
70
+ <select id="showStory" style="margin-left: 8px; font-size: 12px;">
71
+ <option value="">All Stories</option>
72
+ </select>
73
+ </div>
74
+ <hr style="margin: 8px 0; border: none; border-top: 1px solid #ccc;">
75
+ <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>
83
+ <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 %}
93
+ </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 %}
112
+ </div>
113
+ </div>
114
+
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);
316
+ }
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
+ }
389
+
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
+ });
399
+ {% endif %}
400
+
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;
489
+ }
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
+
509
+ const gltfData = {{ gltf_data | tojson(indent=indent) }};
510
+
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);
647
+ </script>
@@ -0,0 +1,169 @@
1
+ [tool.poetry]
2
+ name = "effibemviewer"
3
+ version = "0.1.0"
4
+ description = "A 3D viewer for OpenStudio building energy models using GLTF and Three.js"
5
+ authors = ["Julien Marrec <contact@effibem.com>"]
6
+ readme = "README.md"
7
+ license = "MIT"
8
+ classifiers = [
9
+ "Development Status :: 2 - Pre-Alpha",
10
+ "Intended Audience :: Developers",
11
+ "License :: OSI Approved :: MIT License",
12
+ "Natural Language :: English",
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3.10",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ ]
18
+ packages = [
19
+ { include = "effibemviewer" },
20
+ { include = "tests", format = "sdist" },
21
+ ]
22
+
23
+ [tool.poetry.urls]
24
+ Homepage = "https://effibem.com"
25
+ Documentation = "https://jmarrec.github.io/effibemviewer"
26
+ Repository = "https://github.com/jmarrec/effibemviewer"
27
+ Changelog = "https://github.com/jmarrec/effibemviewer/blob/main/CHANGELOG.md"
28
+ Issues = "https://github.com/jmarrec/effibemviewer/issues"
29
+
30
+ [tool.poetry.scripts]
31
+ effibemviewer = "effibemviewer.__main__:main"
32
+
33
+ [tool.poetry.dependencies]
34
+ python = ">=3.10,<4.0"
35
+ openstudio = { version = "^3.4", allow-prereleases = true }
36
+ jinja2 = "^3.1"
37
+
38
+ [tool.poetry.group.dev.dependencies]
39
+ black = "^26.1"
40
+ isort = "^7.0"
41
+ pytest = "^9.0"
42
+ pytest-cov = "^6.0"
43
+ mypy = "^1.19"
44
+ flake8 = "^7.0"
45
+ flake8-pyproject = "^1.2"
46
+ twine = "^6.0"
47
+ bump2version = "^1.0.1"
48
+
49
+ [tool.poetry.group.doc.dependencies]
50
+ mkdocs = "^1.6.1"
51
+ mkdocs-include-markdown-plugin = "^7.2.1"
52
+ mkdocs-material = "^9.7.1"
53
+ mkdocstrings = "^1.0.2"
54
+ mkdocstrings-python = "^2.0.1"
55
+ mkdocs-autorefs = "^1.4.3"
56
+
57
+ [tool.black]
58
+ line-length = 120
59
+ skip-string-normalization = true
60
+ target-version = ['py310', 'py311', 'py312']
61
+ include = '\.pyi?$'
62
+ exclude = '''
63
+ /(
64
+ \.eggs
65
+ | \.git
66
+ | \.hg
67
+ | \.mypy_cache
68
+ | \.tox
69
+ | \.venv
70
+ | _build
71
+ | buck-out
72
+ | build
73
+ | dist
74
+ )/
75
+ '''
76
+
77
+ [tool.isort]
78
+ multi_line_output = 3
79
+ include_trailing_comma = true
80
+ force_grid_wrap = 0
81
+ use_parentheses = true
82
+ ensure_newline_before_comments = true
83
+ line_length = 120
84
+ skip_gitignore = true
85
+
86
+ [tool.flake8]
87
+ max-line-length = 120
88
+ extend-ignore = ["E402", "D100"]
89
+ per-file-ignores = ["__init__.py:F401,D104"]
90
+ docstring-convention = "google"
91
+ exclude = [
92
+ ".git",
93
+ "__pycache__",
94
+ "setup.py",
95
+ "build",
96
+ "dist",
97
+ "docs",
98
+ "releases",
99
+ ".venv",
100
+ ".tox",
101
+ ".mypy_cache",
102
+ ".pytest_cache",
103
+ ".vscode",
104
+ ".github",
105
+ ]
106
+
107
+ [tool.mypy]
108
+ ignore_missing_imports = true
109
+
110
+ [tool.coverage.run]
111
+ # omit =
112
+
113
+ [tool.coverage.report]
114
+ exclude_lines = [
115
+ "pragma: no cover",
116
+ "def __repr__",
117
+ "if self.debug:",
118
+ "if settings.DEBUG",
119
+ "raise AssertionError",
120
+ "raise NotImplementedError",
121
+ "if 0:",
122
+ "if __name__ == .__main__.:",
123
+ "def main",
124
+ ]
125
+
126
+ [build-system]
127
+ requires = ["poetry-core>=1.0.0"]
128
+ build-backend = "poetry.core.masonry.api"
129
+
130
+ [tool.tox]
131
+ env_list = ["py310", "py312", "format", "lint", "build"]
132
+
133
+ [tool.tox.env_run_base]
134
+ allowlist_externals = ["poetry"]
135
+ pass_env = ["*"]
136
+ set_env = {PYTHONPATH = "{tox_root}", PYTHONWARNINGS = "ignore"}
137
+ commands_pre = [["poetry", "install", "--with", "dev"]]
138
+
139
+ [tool.tox.env.py310]
140
+ commands = [["poetry", "run", "pytest", "--cov=effibemviewer", "--cov-branch", "--cov-report=xml", "--cov-report=term-missing", "tests"]]
141
+
142
+ [tool.tox.env.py312]
143
+ commands = [["poetry", "run", "pytest", "--cov=effibemviewer", "--cov-branch", "--cov-report=xml", "--cov-report=term-missing", "tests"]]
144
+
145
+ [tool.tox.env.format]
146
+ commands = [
147
+ ["poetry", "run", "isort", "effibemviewer"],
148
+ ["poetry", "run", "black", "effibemviewer", "tests"]
149
+ ]
150
+
151
+ [tool.tox.env.lint]
152
+ commands = [
153
+ ["poetry", "run", "flake8", "effibemviewer", "tests"],
154
+ ["poetry", "run", "mypy", "effibemviewer", "tests"]
155
+ ]
156
+
157
+ [tool.tox.env.build]
158
+ commands_pre = [["poetry", "install", "--with", "dev,doc"]]
159
+ commands = [
160
+ ["poetry", "build"],
161
+ ["poetry", "run", "mkdocs", "build"],
162
+ ["poetry", "run", "twine", "check", "dist/*"]
163
+ ]
164
+
165
+ [tool.tox.gh-actions]
166
+ python = """
167
+ 3.10: py310
168
+ 3.12: py312, format, lint, build
169
+ """
File without changes
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env python
2
+ """Tests for `effibemviewer` gltf."""
3
+
4
+ import openstudio
5
+
6
+ from effibemviewer import create_example_model
7
+
8
+
9
+ def test_create_example_model():
10
+ """Test creating an example OpenStudio model."""
11
+ model = create_example_model(include_geometry_diagnostics=True)
12
+ assert isinstance(model, openstudio.model.Model)