openseespy-viewer 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.
- openseespy_viewer-0.1.0/PKG-INFO +96 -0
- openseespy_viewer-0.1.0/README.md +76 -0
- openseespy_viewer-0.1.0/openseespy_viewer.egg-info/PKG-INFO +96 -0
- openseespy_viewer-0.1.0/openseespy_viewer.egg-info/SOURCES.txt +13 -0
- openseespy_viewer-0.1.0/openseespy_viewer.egg-info/dependency_links.txt +1 -0
- openseespy_viewer-0.1.0/openseespy_viewer.egg-info/entry_points.txt +2 -0
- openseespy_viewer-0.1.0/openseespy_viewer.egg-info/requires.txt +3 -0
- openseespy_viewer-0.1.0/openseespy_viewer.egg-info/top_level.txt +1 -0
- openseespy_viewer-0.1.0/pyproject.toml +37 -0
- openseespy_viewer-0.1.0/setup.cfg +4 -0
- openseespy_viewer-0.1.0/viewer/__init__.py +6 -0
- openseespy_viewer-0.1.0/viewer/__main__.py +25 -0
- openseespy_viewer-0.1.0/viewer/model.py +73 -0
- openseespy_viewer-0.1.0/viewer/viewer.py +224 -0
- openseespy_viewer-0.1.0/viewer/watcher.py +41 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openseespy-viewer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Live visual previewer for OpenSeesPy models
|
|
5
|
+
Author: Igor Barcelos
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/igor-barcelos/openseespy-viewer
|
|
8
|
+
Keywords: opensees,openseespy,structural,visualization,pyvista
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Education
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: numpy
|
|
18
|
+
Requires-Dist: pyvista
|
|
19
|
+
Requires-Dist: watchdog
|
|
20
|
+
|
|
21
|
+
# openseespy-viewer
|
|
22
|
+
|
|
23
|
+
Viewer for openseespy models. Monitors your model file(s) and produces a live 3D drawing that updates each time you save. Works with 2D/3dof and 3D/6dof models.
|
|
24
|
+
|
|
25
|
+
### Installation
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
pip install openseespy-viewer
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or install from source:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
pip install .
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Usage
|
|
38
|
+
|
|
39
|
+
**Python API:**
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from viewer import view
|
|
43
|
+
|
|
44
|
+
# Single file
|
|
45
|
+
view('my_model.py')
|
|
46
|
+
|
|
47
|
+
# Multiple files
|
|
48
|
+
view('nodes.py', 'elements.py', 'boundary.py')
|
|
49
|
+
|
|
50
|
+
# With custom style options
|
|
51
|
+
view('my_model.py', bg_colour='white', node_size=16, ele_width=3)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Command line:**
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Basic usage
|
|
58
|
+
openseespy-viewer my_model.py
|
|
59
|
+
|
|
60
|
+
# Multiple files with custom refresh rate
|
|
61
|
+
openseespy-viewer nodes.py elements.py --refresh 1.0
|
|
62
|
+
|
|
63
|
+
# Or via python -m
|
|
64
|
+
python -m viewer my_model.py
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Screenshots
|
|
68
|
+
|
|
69
|
+
Press **S** in the viewer window to save a screenshot. Screenshots are saved to the `images/` folder with incrementing names (`model_screenshot_001.png`, `model_screenshot_002.png`, etc.). The counter resets each time you restart the viewer. This is useful for sharing your model with AI tools like Claude Code — just ask it to look at the screenshot.
|
|
70
|
+
|
|
71
|
+
### Examples
|
|
72
|
+
|
|
73
|
+

|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
See the `examples/` directory for sample model files.
|
|
77
|
+
|
|
78
|
+
### Dependencies
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
pip install pyvista numpy watchdog
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Feature wish-list
|
|
85
|
+
|
|
86
|
+
- [x] Plot nodes as they are saved in a watched script
|
|
87
|
+
- [x] Add node labels
|
|
88
|
+
- [x] Add support for elements
|
|
89
|
+
- [x] Add support for fixity conditions
|
|
90
|
+
- [x] Add support for variables and expressions in scripts
|
|
91
|
+
- [x] Add support for watching multiple files (for models made up of many files)
|
|
92
|
+
- [ ] Add support for loads
|
|
93
|
+
- [ ] Add support for nodes created within loop
|
|
94
|
+
- [x] Add support for 3D models
|
|
95
|
+
- [ ] Add support for rotational dofs
|
|
96
|
+
- [x] Screenshot capture (press S in viewer)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# openseespy-viewer
|
|
2
|
+
|
|
3
|
+
Viewer for openseespy models. Monitors your model file(s) and produces a live 3D drawing that updates each time you save. Works with 2D/3dof and 3D/6dof models.
|
|
4
|
+
|
|
5
|
+
### Installation
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
pip install openseespy-viewer
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or install from source:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
pip install .
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Usage
|
|
18
|
+
|
|
19
|
+
**Python API:**
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from viewer import view
|
|
23
|
+
|
|
24
|
+
# Single file
|
|
25
|
+
view('my_model.py')
|
|
26
|
+
|
|
27
|
+
# Multiple files
|
|
28
|
+
view('nodes.py', 'elements.py', 'boundary.py')
|
|
29
|
+
|
|
30
|
+
# With custom style options
|
|
31
|
+
view('my_model.py', bg_colour='white', node_size=16, ele_width=3)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Command line:**
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Basic usage
|
|
38
|
+
openseespy-viewer my_model.py
|
|
39
|
+
|
|
40
|
+
# Multiple files with custom refresh rate
|
|
41
|
+
openseespy-viewer nodes.py elements.py --refresh 1.0
|
|
42
|
+
|
|
43
|
+
# Or via python -m
|
|
44
|
+
python -m viewer my_model.py
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Screenshots
|
|
48
|
+
|
|
49
|
+
Press **S** in the viewer window to save a screenshot. Screenshots are saved to the `images/` folder with incrementing names (`model_screenshot_001.png`, `model_screenshot_002.png`, etc.). The counter resets each time you restart the viewer. This is useful for sharing your model with AI tools like Claude Code — just ask it to look at the screenshot.
|
|
50
|
+
|
|
51
|
+
### Examples
|
|
52
|
+
|
|
53
|
+

|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
See the `examples/` directory for sample model files.
|
|
57
|
+
|
|
58
|
+
### Dependencies
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
pip install pyvista numpy watchdog
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Feature wish-list
|
|
65
|
+
|
|
66
|
+
- [x] Plot nodes as they are saved in a watched script
|
|
67
|
+
- [x] Add node labels
|
|
68
|
+
- [x] Add support for elements
|
|
69
|
+
- [x] Add support for fixity conditions
|
|
70
|
+
- [x] Add support for variables and expressions in scripts
|
|
71
|
+
- [x] Add support for watching multiple files (for models made up of many files)
|
|
72
|
+
- [ ] Add support for loads
|
|
73
|
+
- [ ] Add support for nodes created within loop
|
|
74
|
+
- [x] Add support for 3D models
|
|
75
|
+
- [ ] Add support for rotational dofs
|
|
76
|
+
- [x] Screenshot capture (press S in viewer)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openseespy-viewer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Live visual previewer for OpenSeesPy models
|
|
5
|
+
Author: Igor Barcelos
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/igor-barcelos/openseespy-viewer
|
|
8
|
+
Keywords: opensees,openseespy,structural,visualization,pyvista
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Education
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: numpy
|
|
18
|
+
Requires-Dist: pyvista
|
|
19
|
+
Requires-Dist: watchdog
|
|
20
|
+
|
|
21
|
+
# openseespy-viewer
|
|
22
|
+
|
|
23
|
+
Viewer for openseespy models. Monitors your model file(s) and produces a live 3D drawing that updates each time you save. Works with 2D/3dof and 3D/6dof models.
|
|
24
|
+
|
|
25
|
+
### Installation
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
pip install openseespy-viewer
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or install from source:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
pip install .
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Usage
|
|
38
|
+
|
|
39
|
+
**Python API:**
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from viewer import view
|
|
43
|
+
|
|
44
|
+
# Single file
|
|
45
|
+
view('my_model.py')
|
|
46
|
+
|
|
47
|
+
# Multiple files
|
|
48
|
+
view('nodes.py', 'elements.py', 'boundary.py')
|
|
49
|
+
|
|
50
|
+
# With custom style options
|
|
51
|
+
view('my_model.py', bg_colour='white', node_size=16, ele_width=3)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Command line:**
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Basic usage
|
|
58
|
+
openseespy-viewer my_model.py
|
|
59
|
+
|
|
60
|
+
# Multiple files with custom refresh rate
|
|
61
|
+
openseespy-viewer nodes.py elements.py --refresh 1.0
|
|
62
|
+
|
|
63
|
+
# Or via python -m
|
|
64
|
+
python -m viewer my_model.py
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Screenshots
|
|
68
|
+
|
|
69
|
+
Press **S** in the viewer window to save a screenshot. Screenshots are saved to the `images/` folder with incrementing names (`model_screenshot_001.png`, `model_screenshot_002.png`, etc.). The counter resets each time you restart the viewer. This is useful for sharing your model with AI tools like Claude Code — just ask it to look at the screenshot.
|
|
70
|
+
|
|
71
|
+
### Examples
|
|
72
|
+
|
|
73
|
+

|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
See the `examples/` directory for sample model files.
|
|
77
|
+
|
|
78
|
+
### Dependencies
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
pip install pyvista numpy watchdog
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Feature wish-list
|
|
85
|
+
|
|
86
|
+
- [x] Plot nodes as they are saved in a watched script
|
|
87
|
+
- [x] Add node labels
|
|
88
|
+
- [x] Add support for elements
|
|
89
|
+
- [x] Add support for fixity conditions
|
|
90
|
+
- [x] Add support for variables and expressions in scripts
|
|
91
|
+
- [x] Add support for watching multiple files (for models made up of many files)
|
|
92
|
+
- [ ] Add support for loads
|
|
93
|
+
- [ ] Add support for nodes created within loop
|
|
94
|
+
- [x] Add support for 3D models
|
|
95
|
+
- [ ] Add support for rotational dofs
|
|
96
|
+
- [x] Screenshot capture (press S in viewer)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
openseespy_viewer.egg-info/PKG-INFO
|
|
4
|
+
openseespy_viewer.egg-info/SOURCES.txt
|
|
5
|
+
openseespy_viewer.egg-info/dependency_links.txt
|
|
6
|
+
openseespy_viewer.egg-info/entry_points.txt
|
|
7
|
+
openseespy_viewer.egg-info/requires.txt
|
|
8
|
+
openseespy_viewer.egg-info/top_level.txt
|
|
9
|
+
viewer/__init__.py
|
|
10
|
+
viewer/__main__.py
|
|
11
|
+
viewer/model.py
|
|
12
|
+
viewer/viewer.py
|
|
13
|
+
viewer/watcher.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
viewer
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "openseespy-viewer"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Live visual previewer for OpenSeesPy models"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Igor Barcelos" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["opensees", "openseespy", "structural", "visualization", "pyvista"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Education",
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Topic :: Scientific/Engineering",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"numpy",
|
|
26
|
+
"pyvista",
|
|
27
|
+
"watchdog",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/igor-barcelos/openseespy-viewer"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
include = ["viewer*"]
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
openseespy-viewer = "viewer.__main__:main"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""CLI entry point: python -m openseespy_viewer model1.py [model2.py ...]"""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from .viewer import view
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main():
|
|
8
|
+
parser = argparse.ArgumentParser(
|
|
9
|
+
prog="openseespy-viewer",
|
|
10
|
+
description="Live visual previewer for OpenSeesPy models.",
|
|
11
|
+
)
|
|
12
|
+
parser.add_argument(
|
|
13
|
+
"modelfiles", nargs="+",
|
|
14
|
+
help="One or more OpenSeesPy model files (.py) to visualise.",
|
|
15
|
+
)
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"--refresh", type=float, default=0.5,
|
|
18
|
+
help="Minimum refresh interval in seconds (default: 0.5).",
|
|
19
|
+
)
|
|
20
|
+
args = parser.parse_args()
|
|
21
|
+
view(*args.modelfiles, min_refresh_interval=args.refresh)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if __name__ == "__main__":
|
|
25
|
+
main()
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Mock OpenSeesPy module for intercepting ops.node(), ops.element(), etc."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import types
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Model:
|
|
9
|
+
"""A mock replacement for openseespy.opensees that records model data
|
|
10
|
+
instead of building a real OpenSees model.
|
|
11
|
+
"""
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.nodes = []
|
|
14
|
+
self.elements = []
|
|
15
|
+
self.fixities = []
|
|
16
|
+
self.ndm = 2
|
|
17
|
+
|
|
18
|
+
def model(self, *args):
|
|
19
|
+
args = [str(a) for a in args]
|
|
20
|
+
if '-ndm' in args:
|
|
21
|
+
idx = args.index('-ndm')
|
|
22
|
+
self.ndm = int(args[idx + 1])
|
|
23
|
+
|
|
24
|
+
def node(self, tag, *coords):
|
|
25
|
+
self.nodes.append((int(tag),) + tuple(float(c) for c in coords))
|
|
26
|
+
|
|
27
|
+
def element(self, etype, tag, iNode, jNode, *rest):
|
|
28
|
+
self.elements.append((str(etype), int(tag), int(iNode), int(jNode)))
|
|
29
|
+
|
|
30
|
+
def fix(self, tag, *dofs):
|
|
31
|
+
self.fixities.append((int(tag),) + tuple(int(d) for d in dofs))
|
|
32
|
+
|
|
33
|
+
def __getattr__(self, name):
|
|
34
|
+
# Catch-all: silently ignore any other ops.xxx() call
|
|
35
|
+
return lambda *args, **kwargs: None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def parse_py(pyfiles):
|
|
39
|
+
"""Parse OpenSeesPy (.py) files by mocking the openseespy module and
|
|
40
|
+
executing the user's script. All ops.node/element/fix calls are recorded.
|
|
41
|
+
Returns (nodes, elements, fixities, ndm).
|
|
42
|
+
"""
|
|
43
|
+
mock = Model()
|
|
44
|
+
|
|
45
|
+
mock_package = types.ModuleType('openseespy')
|
|
46
|
+
mock_package.opensees = mock
|
|
47
|
+
mock_package.__path__ = []
|
|
48
|
+
|
|
49
|
+
saved = {}
|
|
50
|
+
for key in ('openseespy', 'openseespy.opensees'):
|
|
51
|
+
if key in sys.modules:
|
|
52
|
+
saved[key] = sys.modules[key]
|
|
53
|
+
|
|
54
|
+
sys.modules['openseespy'] = mock_package
|
|
55
|
+
sys.modules['openseespy.opensees'] = mock
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
for pyfile in pyfiles:
|
|
59
|
+
filepath = os.path.abspath(pyfile)
|
|
60
|
+
with open(filepath, 'r') as f:
|
|
61
|
+
code = f.read()
|
|
62
|
+
exec(compile(code, filepath, 'exec'),
|
|
63
|
+
{'__name__': '__main__', '__file__': filepath})
|
|
64
|
+
except Exception as e:
|
|
65
|
+
print(f'Error parsing {pyfile}: {e}')
|
|
66
|
+
finally:
|
|
67
|
+
for key in ('openseespy', 'openseespy.opensees'):
|
|
68
|
+
if key in saved:
|
|
69
|
+
sys.modules[key] = saved[key]
|
|
70
|
+
elif key in sys.modules:
|
|
71
|
+
del sys.modules[key]
|
|
72
|
+
|
|
73
|
+
return mock.nodes, mock.elements, mock.fixities, mock.ndm
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Main viewer logic: builds the 3D scene and runs the live preview loop."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pyvista as pv
|
|
7
|
+
|
|
8
|
+
SCREENSHOT_DIR = os.path.join(os.getcwd(), 'images')
|
|
9
|
+
_screenshot_counter = 0
|
|
10
|
+
|
|
11
|
+
from .model import parse_py
|
|
12
|
+
from .watcher import start_watcher
|
|
13
|
+
|
|
14
|
+
# Default visual style
|
|
15
|
+
DEFAULTS = dict(
|
|
16
|
+
bg_colour='lightgrey',
|
|
17
|
+
node_colour='black',
|
|
18
|
+
node_size=12,
|
|
19
|
+
ele_colour='black',
|
|
20
|
+
ele_width=2,
|
|
21
|
+
axis_colour='grey',
|
|
22
|
+
label_font_size=10,
|
|
23
|
+
offset_ratio=0.02,
|
|
24
|
+
bc_arrow_scale=0.3,
|
|
25
|
+
min_refresh_interval=0.5,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _nodecoords(nodetag, nodes, ndm):
|
|
30
|
+
"""Returns node coordinates as a 3D point (z=0 for 2D models)."""
|
|
31
|
+
for node in nodes:
|
|
32
|
+
if node[0] == nodetag:
|
|
33
|
+
if ndm == 2:
|
|
34
|
+
return np.array([node[1], node[2], 0.0])
|
|
35
|
+
else:
|
|
36
|
+
return np.array([node[1], node[2], node[3]])
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _build_scene(plotter, modelfiles, ndm, style):
|
|
41
|
+
"""Parse model files and add all geometry to the plotter."""
|
|
42
|
+
nodes, elements, fixities, _ = parse_py(modelfiles)
|
|
43
|
+
|
|
44
|
+
if not nodes:
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
# Build node points array (always 3D; z=0 for 2D models)
|
|
48
|
+
if ndm == 2:
|
|
49
|
+
node_points = np.array([[n[1], n[2], 0.0] for n in nodes])
|
|
50
|
+
else:
|
|
51
|
+
node_points = np.array([[n[1], n[2], n[3]] for n in nodes])
|
|
52
|
+
|
|
53
|
+
node_tags = [n[0] for n in nodes]
|
|
54
|
+
|
|
55
|
+
# Compute model size for offsets
|
|
56
|
+
mins = node_points.min(axis=0)
|
|
57
|
+
maxs = node_points.max(axis=0)
|
|
58
|
+
spans = maxs - mins
|
|
59
|
+
model_size = max(spans.max(), 1.0)
|
|
60
|
+
offset = model_size * style['offset_ratio']
|
|
61
|
+
|
|
62
|
+
# --- Display nodes ---
|
|
63
|
+
node_cloud = pv.PolyData(node_points)
|
|
64
|
+
plotter.add_mesh(node_cloud, color=style['node_colour'],
|
|
65
|
+
point_size=style['node_size'],
|
|
66
|
+
render_points_as_spheres=True)
|
|
67
|
+
|
|
68
|
+
# Node labels
|
|
69
|
+
label_points = node_points + offset
|
|
70
|
+
node_labels = ['N' + str(t) for t in node_tags]
|
|
71
|
+
plotter.add_point_labels(label_points, node_labels,
|
|
72
|
+
font_size=style['label_font_size'],
|
|
73
|
+
text_color='black', font_family='courier',
|
|
74
|
+
bold=True, shape=None, fill_shape=False)
|
|
75
|
+
|
|
76
|
+
# --- Display reference axes ---
|
|
77
|
+
if ndm == 2:
|
|
78
|
+
margin = model_size * 0.15
|
|
79
|
+
x_line = pv.Line([mins[0] - margin, 0, 0], [maxs[0] + margin, 0, 0])
|
|
80
|
+
y_line = pv.Line([0, mins[1] - margin, 0], [0, maxs[1] + margin, 0])
|
|
81
|
+
plotter.add_mesh(x_line, color=style['axis_colour'], line_width=1,
|
|
82
|
+
style='wireframe')
|
|
83
|
+
plotter.add_mesh(y_line, color=style['axis_colour'], line_width=1,
|
|
84
|
+
style='wireframe')
|
|
85
|
+
else:
|
|
86
|
+
view_centre = (mins + maxs) / 2
|
|
87
|
+
view_range = spans.max()
|
|
88
|
+
axis_len = 1.2 * (view_range / 2 + max(abs(view_centre)))
|
|
89
|
+
x_axis = pv.Line([0, 0, 0], [axis_len, 0, 0])
|
|
90
|
+
y_axis = pv.Line([0, 0, 0], [0, axis_len, 0])
|
|
91
|
+
z_axis = pv.Line([0, 0, 0], [0, 0, axis_len])
|
|
92
|
+
plotter.add_mesh(x_axis, color='red', line_width=1)
|
|
93
|
+
plotter.add_mesh(y_axis, color='green', line_width=1)
|
|
94
|
+
plotter.add_mesh(z_axis, color='blue', line_width=1)
|
|
95
|
+
|
|
96
|
+
# --- Display elements ---
|
|
97
|
+
if elements:
|
|
98
|
+
for element in elements:
|
|
99
|
+
iNode = _nodecoords(element[2], nodes, ndm)
|
|
100
|
+
jNode = _nodecoords(element[3], nodes, ndm)
|
|
101
|
+
if iNode is not None and jNode is not None:
|
|
102
|
+
line = pv.Line(iNode, jNode)
|
|
103
|
+
plotter.add_mesh(line, color=style['ele_colour'],
|
|
104
|
+
line_width=style['ele_width'])
|
|
105
|
+
mid = (iNode + jNode) / 2 + offset
|
|
106
|
+
plotter.add_point_labels(
|
|
107
|
+
[mid], ['E' + str(element[1])],
|
|
108
|
+
font_size=style['label_font_size'],
|
|
109
|
+
text_color='black', font_family='courier',
|
|
110
|
+
shape=None, fill_shape=False)
|
|
111
|
+
|
|
112
|
+
# --- Display boundary conditions ---
|
|
113
|
+
if fixities:
|
|
114
|
+
bc_size = model_size * 0.04
|
|
115
|
+
for fixity in fixities:
|
|
116
|
+
coords = _nodecoords(fixity[0], nodes, ndm)
|
|
117
|
+
if coords is None:
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
if ndm == 2:
|
|
121
|
+
if fixity[1] == 1:
|
|
122
|
+
tip = coords - np.array([bc_size * 0.5, 0, 0])
|
|
123
|
+
tri = pv.Triangle([
|
|
124
|
+
[tip[0] - bc_size, tip[1] - bc_size * 0.5, 0],
|
|
125
|
+
[tip[0], tip[1], 0],
|
|
126
|
+
[tip[0] - bc_size, tip[1] + bc_size * 0.5, 0],
|
|
127
|
+
])
|
|
128
|
+
plotter.add_mesh(tri, color='black', style='wireframe',
|
|
129
|
+
line_width=2)
|
|
130
|
+
if fixity[2] == 1:
|
|
131
|
+
tip = coords - np.array([0, bc_size * 0.5, 0])
|
|
132
|
+
tri = pv.Triangle([
|
|
133
|
+
[tip[0] - bc_size * 0.5, tip[1] - bc_size, 0],
|
|
134
|
+
[tip[0], tip[1], 0],
|
|
135
|
+
[tip[0] + bc_size * 0.5, tip[1] - bc_size, 0],
|
|
136
|
+
])
|
|
137
|
+
plotter.add_mesh(tri, color='black', style='wireframe',
|
|
138
|
+
line_width=2)
|
|
139
|
+
if len(fixity) > 3 and fixity[3] == 1:
|
|
140
|
+
circle = pv.Circle(radius=bc_size * 0.6, resolution=32)
|
|
141
|
+
circle.translate(coords, inplace=True)
|
|
142
|
+
plotter.add_mesh(circle, color='black', style='wireframe',
|
|
143
|
+
line_width=2)
|
|
144
|
+
else:
|
|
145
|
+
directions = [
|
|
146
|
+
np.array([1, 0, 0]),
|
|
147
|
+
np.array([0, 1, 0]),
|
|
148
|
+
np.array([0, 0, 1]),
|
|
149
|
+
]
|
|
150
|
+
for i, d in enumerate(directions):
|
|
151
|
+
if fixity[i + 1] == 1:
|
|
152
|
+
start = coords - d * bc_size * 1.5
|
|
153
|
+
arrow = pv.Arrow(start=start, direction=d,
|
|
154
|
+
scale=bc_size * 1.5, tip_length=0.4,
|
|
155
|
+
tip_radius=0.15, shaft_radius=0.05)
|
|
156
|
+
plotter.add_mesh(arrow, color='black')
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def view(*modelfiles, **kwargs):
|
|
160
|
+
"""Open a live viewer for the given OpenSeesPy model file(s).
|
|
161
|
+
|
|
162
|
+
Parameters
|
|
163
|
+
----------
|
|
164
|
+
*modelfiles : str
|
|
165
|
+
One or more paths to OpenSeesPy model files (.py).
|
|
166
|
+
**kwargs :
|
|
167
|
+
Optional style overrides (bg_colour, node_colour, node_size,
|
|
168
|
+
ele_colour, ele_width, axis_colour, label_font_size, offset_ratio,
|
|
169
|
+
min_refresh_interval).
|
|
170
|
+
"""
|
|
171
|
+
if not modelfiles:
|
|
172
|
+
raise ValueError("At least one model file path is required.")
|
|
173
|
+
|
|
174
|
+
style = {**DEFAULTS, **kwargs}
|
|
175
|
+
|
|
176
|
+
# Determine 2D or 3D
|
|
177
|
+
_, _, _, ndm = parse_py(modelfiles)
|
|
178
|
+
|
|
179
|
+
# Start file watcher
|
|
180
|
+
observer, file_handler = start_watcher(modelfiles)
|
|
181
|
+
|
|
182
|
+
# Create plotter
|
|
183
|
+
plotter = pv.Plotter(title='OpenSeesPy Viewer - ' + ', '.join(modelfiles))
|
|
184
|
+
plotter.set_background(style['bg_colour'])
|
|
185
|
+
|
|
186
|
+
# Initial scene build
|
|
187
|
+
_build_scene(plotter, modelfiles, ndm, style)
|
|
188
|
+
|
|
189
|
+
if ndm == 2:
|
|
190
|
+
plotter.view_xy()
|
|
191
|
+
plotter.enable_parallel_projection()
|
|
192
|
+
|
|
193
|
+
def _update(step):
|
|
194
|
+
if not file_handler.changed.is_set():
|
|
195
|
+
return
|
|
196
|
+
file_handler.changed.clear()
|
|
197
|
+
plotter.clear()
|
|
198
|
+
_build_scene(plotter, modelfiles, ndm, style)
|
|
199
|
+
if ndm == 2:
|
|
200
|
+
plotter.view_xy()
|
|
201
|
+
plotter.render()
|
|
202
|
+
|
|
203
|
+
def _screenshot():
|
|
204
|
+
global _screenshot_counter
|
|
205
|
+
os.makedirs(SCREENSHOT_DIR, exist_ok=True)
|
|
206
|
+
_screenshot_counter += 1
|
|
207
|
+
path = os.path.join(SCREENSHOT_DIR, f'model_screenshot_{_screenshot_counter:03d}.png')
|
|
208
|
+
plotter.screenshot(path)
|
|
209
|
+
print(f'Screenshot saved to {path}')
|
|
210
|
+
|
|
211
|
+
plotter.add_key_event('s', _screenshot)
|
|
212
|
+
|
|
213
|
+
plotter.show(interactive_update=True)
|
|
214
|
+
plotter.add_timer_event(
|
|
215
|
+
max_steps=sys.maxsize,
|
|
216
|
+
duration=int(style['min_refresh_interval'] * 1000),
|
|
217
|
+
callback=_update,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
plotter.iren.start()
|
|
222
|
+
finally:
|
|
223
|
+
observer.stop()
|
|
224
|
+
observer.join()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""File watcher that detects changes in model files."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import threading
|
|
5
|
+
from watchdog.observers import Observer
|
|
6
|
+
from watchdog.events import FileSystemEventHandler
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ModelFileHandler(FileSystemEventHandler):
|
|
10
|
+
"""Watches model files for changes and sets a flag when they are modified."""
|
|
11
|
+
def __init__(self, filenames):
|
|
12
|
+
self.filenames = {os.path.abspath(f) for f in filenames}
|
|
13
|
+
self.changed = threading.Event()
|
|
14
|
+
self.changed.set() # trigger initial draw
|
|
15
|
+
|
|
16
|
+
def _check(self, event):
|
|
17
|
+
if os.path.abspath(event.src_path) in self.filenames:
|
|
18
|
+
self.changed.set()
|
|
19
|
+
|
|
20
|
+
def on_modified(self, event):
|
|
21
|
+
self._check(event)
|
|
22
|
+
|
|
23
|
+
def on_created(self, event):
|
|
24
|
+
self._check(event)
|
|
25
|
+
|
|
26
|
+
def on_moved(self, event):
|
|
27
|
+
if os.path.abspath(event.dest_path) in self.filenames:
|
|
28
|
+
self.changed.set()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def start_watcher(modelfiles):
|
|
32
|
+
"""Start a file watcher on the given model files.
|
|
33
|
+
Returns (observer, file_handler).
|
|
34
|
+
"""
|
|
35
|
+
file_handler = ModelFileHandler(modelfiles)
|
|
36
|
+
observer = Observer()
|
|
37
|
+
watched_dirs = {os.path.abspath(os.path.dirname(f) or '.') for f in modelfiles}
|
|
38
|
+
for d in watched_dirs:
|
|
39
|
+
observer.schedule(file_handler, d, recursive=False)
|
|
40
|
+
observer.start()
|
|
41
|
+
return observer, file_handler
|