gd-sofa-utils 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gd_sofa_utils-0.0.1/.gitignore +13 -0
- gd_sofa_utils-0.0.1/.gitlab-ci.yml +118 -0
- gd_sofa_utils-0.0.1/LICENSE +16 -0
- gd_sofa_utils-0.0.1/PKG-INFO +33 -0
- gd_sofa_utils-0.0.1/README.md +11 -0
- gd_sofa_utils-0.0.1/docs/api.md +37 -0
- gd_sofa_utils-0.0.1/docs/extra.css +3 -0
- gd_sofa_utils-0.0.1/docs/guide.md +123 -0
- gd_sofa_utils-0.0.1/docs/index.md +34 -0
- gd_sofa_utils-0.0.1/docs/setup.js +214 -0
- gd_sofa_utils-0.0.1/examples/general_example.py +25 -0
- gd_sofa_utils-0.0.1/gd_sofa_utils.egg-info/PKG-INFO +33 -0
- gd_sofa_utils-0.0.1/gd_sofa_utils.egg-info/SOURCES.txt +37 -0
- gd_sofa_utils-0.0.1/gd_sofa_utils.egg-info/dependency_links.txt +1 -0
- gd_sofa_utils-0.0.1/gd_sofa_utils.egg-info/requires.txt +15 -0
- gd_sofa_utils-0.0.1/gd_sofa_utils.egg-info/top_level.txt +1 -0
- gd_sofa_utils-0.0.1/gdsofa/__init__.py +8 -0
- gd_sofa_utils-0.0.1/gdsofa/comps/__init__.py +2 -0
- gd_sofa_utils-0.0.1/gdsofa/comps/_build_dynamic_lib.py +81 -0
- gd_sofa_utils-0.0.1/gdsofa/comps/_dyn.py +2184 -0
- gd_sofa_utils-0.0.1/gdsofa/comps/base_component.py +330 -0
- gd_sofa_utils-0.0.1/gdsofa/controller/__init__.py +2 -0
- gd_sofa_utils-0.0.1/gdsofa/controller/controller.py +254 -0
- gd_sofa_utils-0.0.1/gdsofa/controller/controller_data.py +22 -0
- gd_sofa_utils-0.0.1/gdsofa/core/__init__.py +3 -0
- gd_sofa_utils-0.0.1/gdsofa/core/component.py +59 -0
- gd_sofa_utils-0.0.1/gdsofa/core/links.py +66 -0
- gd_sofa_utils-0.0.1/gdsofa/core/node.py +427 -0
- gd_sofa_utils-0.0.1/gdsofa/load_SOFA.py +30 -0
- gd_sofa_utils-0.0.1/gdsofa/sofa_parameters.py +161 -0
- gd_sofa_utils-0.0.1/gdsofa/sofawrap.py +151 -0
- gd_sofa_utils-0.0.1/gdsofa/utils.py +162 -0
- gd_sofa_utils-0.0.1/gdsofa/visual.py +33 -0
- gd_sofa_utils-0.0.1/mkdocs.yml +55 -0
- gd_sofa_utils-0.0.1/pyproject.toml +37 -0
- gd_sofa_utils-0.0.1/setup.cfg +4 -0
- gd_sofa_utils-0.0.1/tests/README.md +31 -0
- gd_sofa_utils-0.0.1/tests/test_scene.py +273 -0
- gd_sofa_utils-0.0.1/uv.lock +1203 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
stages:
|
|
2
|
+
- test
|
|
3
|
+
- build
|
|
4
|
+
- publish
|
|
5
|
+
- deploy_doc
|
|
6
|
+
|
|
7
|
+
image: python:3.12
|
|
8
|
+
|
|
9
|
+
variables:
|
|
10
|
+
GIT_DEPTH: "0" # setuptools-scm needs full history/tags
|
|
11
|
+
GIT_FETCH_EXTRA_FLAGS: "--tags" # make sure tags are fetched
|
|
12
|
+
PIP_DISABLE_PIP_VERSION_CHECK: "1"
|
|
13
|
+
PYTHONDONTWRITEBYTECODE: "1"
|
|
14
|
+
PYTHONUNBUFFERED: "1"
|
|
15
|
+
UV_CACHE_DIR: "$CI_PROJECT_DIR/.uv-cache"
|
|
16
|
+
|
|
17
|
+
cache:
|
|
18
|
+
key: "${CI_PROJECT_NAME}"
|
|
19
|
+
paths:
|
|
20
|
+
- .venv/
|
|
21
|
+
- .uv-cache/
|
|
22
|
+
|
|
23
|
+
before_script:
|
|
24
|
+
- python -m pip install -U pip
|
|
25
|
+
- python -m pip install -U uv
|
|
26
|
+
|
|
27
|
+
test:
|
|
28
|
+
stage: test
|
|
29
|
+
tags:
|
|
30
|
+
- ci.inria.fr
|
|
31
|
+
- small
|
|
32
|
+
script:
|
|
33
|
+
- uv sync --frozen --group dev
|
|
34
|
+
- uv run pytest
|
|
35
|
+
rules:
|
|
36
|
+
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
|
|
37
|
+
- when: never
|
|
38
|
+
|
|
39
|
+
build:
|
|
40
|
+
stage: build
|
|
41
|
+
tags:
|
|
42
|
+
- ci.inria.fr
|
|
43
|
+
- small
|
|
44
|
+
needs: ["test"]
|
|
45
|
+
script:
|
|
46
|
+
- uv sync --frozen --group dev
|
|
47
|
+
- uv run python -m build
|
|
48
|
+
- |
|
|
49
|
+
python - <<'PY'
|
|
50
|
+
import os, re, glob, subprocess, sys
|
|
51
|
+
tag = os.environ.get("CI_COMMIT_TAG", "")
|
|
52
|
+
|
|
53
|
+
m = re.fullmatch(r"v(\d+\.\d+\.\d+)", tag)
|
|
54
|
+
if not m:
|
|
55
|
+
raise SystemExit(f"Invalid tag format: {tag!r} (expected vX.Y.Z)")
|
|
56
|
+
tag_ver = m.group(1)
|
|
57
|
+
|
|
58
|
+
wheels = glob.glob("dist/*.whl")
|
|
59
|
+
if not wheels:
|
|
60
|
+
raise SystemExit("No wheel found in dist/")
|
|
61
|
+
wheel = wheels[0]
|
|
62
|
+
|
|
63
|
+
subprocess.check_call([sys.executable, "-m", "pip", "install", "--no-deps", "--force-reinstall", wheel])
|
|
64
|
+
import importlib.metadata as md
|
|
65
|
+
ver = md.version("gd-sofa-utils")
|
|
66
|
+
if ver != tag_ver:
|
|
67
|
+
raise SystemExit(f"Version mismatch: wheel={ver!r} tag={tag_ver!r}")
|
|
68
|
+
print(f"OK: wheel version {ver} matches tag {tag}")
|
|
69
|
+
PY
|
|
70
|
+
artifacts:
|
|
71
|
+
when: always
|
|
72
|
+
expire_in: 7 days
|
|
73
|
+
paths:
|
|
74
|
+
- dist/
|
|
75
|
+
rules:
|
|
76
|
+
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
|
|
77
|
+
- when: never
|
|
78
|
+
|
|
79
|
+
publish_pypi:
|
|
80
|
+
stage: publish
|
|
81
|
+
tags:
|
|
82
|
+
- ci.inria.fr
|
|
83
|
+
- small
|
|
84
|
+
needs: ["build"]
|
|
85
|
+
script:
|
|
86
|
+
- uv sync --frozen --group dev
|
|
87
|
+
- |
|
|
88
|
+
python - <<'PY'
|
|
89
|
+
import os
|
|
90
|
+
if not os.environ.get("PYPI_API_TOKEN"):
|
|
91
|
+
raise SystemExit("Missing PYPI_API_TOKEN CI variable.")
|
|
92
|
+
PY
|
|
93
|
+
- export TWINE_USERNAME="__token__"
|
|
94
|
+
- export TWINE_PASSWORD="$PYPI_API_TOKEN"
|
|
95
|
+
- |
|
|
96
|
+
if [ -n "$PYPI_REPOSITORY_URL" ]; then
|
|
97
|
+
uv run twine upload --repository-url "$PYPI_REPOSITORY_URL" dist/*
|
|
98
|
+
else
|
|
99
|
+
uv run twine upload dist/*
|
|
100
|
+
fi
|
|
101
|
+
rules:
|
|
102
|
+
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
|
|
103
|
+
- when: never
|
|
104
|
+
|
|
105
|
+
pages:
|
|
106
|
+
stage: deploy_doc
|
|
107
|
+
tags:
|
|
108
|
+
- ci.inria.fr
|
|
109
|
+
- small
|
|
110
|
+
rules:
|
|
111
|
+
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
|
112
|
+
script:
|
|
113
|
+
- uv sync --frozen --extra docs
|
|
114
|
+
- uv run mkdocs build --strict --site-dir public
|
|
115
|
+
artifacts:
|
|
116
|
+
paths:
|
|
117
|
+
- public
|
|
118
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
The GNU General Public License (GPL)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Gaëtan Desrues, Inria, France
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify
|
|
6
|
+
it under the terms of the GNU General Public License as published by
|
|
7
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
(at your option) any later version.
|
|
9
|
+
|
|
10
|
+
This program is distributed in the hope that it will be useful,
|
|
11
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
GNU General Public License for more details.
|
|
14
|
+
|
|
15
|
+
You should have received a copy of the GNU General Public License
|
|
16
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gd-sofa-utils
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Requires-Python: ==3.12.*
|
|
5
|
+
Description-Content-Type: text/markdown
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: gd-py-utils
|
|
8
|
+
Requires-Dist: randomname
|
|
9
|
+
Requires-Dist: colour
|
|
10
|
+
Requires-Dist: param
|
|
11
|
+
Requires-Dist: treelib
|
|
12
|
+
Requires-Dist: numpy
|
|
13
|
+
Requires-Dist: pandas
|
|
14
|
+
Provides-Extra: docs
|
|
15
|
+
Requires-Dist: mkdocs; extra == "docs"
|
|
16
|
+
Requires-Dist: mkdocs-material; extra == "docs"
|
|
17
|
+
Requires-Dist: mkdocs-mermaid2-plugin; extra == "docs"
|
|
18
|
+
Requires-Dist: mkdocs-snippets; extra == "docs"
|
|
19
|
+
Requires-Dist: mkdocs-exclude; extra == "docs"
|
|
20
|
+
Requires-Dist: mkdocstrings-python; extra == "docs"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# gdsofa
|
|
24
|
+
|
|
25
|
+
**gdsofa** is a lightweight Python library for building and running [SOFA](https://www.sofa-framework.org/) (Simulation Open Framework Architecture) scenes programmatically. It provides a scene graph API (nodes, components, parameters, controllers) and a runner for headless or GUI simulations.
|
|
26
|
+
|
|
27
|
+
[**Docs**]()
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install gd-sofa-utils
|
|
33
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# gdsofa
|
|
2
|
+
|
|
3
|
+
**gdsofa** is a lightweight Python library for building and running [SOFA](https://www.sofa-framework.org/) (Simulation Open Framework Architecture) scenes programmatically. It provides a scene graph API (nodes, components, parameters, controllers) and a runner for headless or GUI simulations.
|
|
4
|
+
|
|
5
|
+
[**Docs**]()
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install gd-sofa-utils
|
|
11
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
## Package
|
|
4
|
+
|
|
5
|
+
::: gdsofa
|
|
6
|
+
|
|
7
|
+
## Scene and runner
|
|
8
|
+
|
|
9
|
+
::: gdsofa.sofawrap
|
|
10
|
+
|
|
11
|
+
## Parameters
|
|
12
|
+
|
|
13
|
+
::: gdsofa.sofa_parameters
|
|
14
|
+
|
|
15
|
+
## SOFA loader
|
|
16
|
+
|
|
17
|
+
::: gdsofa.load_SOFA
|
|
18
|
+
|
|
19
|
+
## Controllers
|
|
20
|
+
|
|
21
|
+
::: gdsofa.controller
|
|
22
|
+
|
|
23
|
+
## Core (nodes, components, links)
|
|
24
|
+
|
|
25
|
+
::: gdsofa.core
|
|
26
|
+
|
|
27
|
+
## Utilities
|
|
28
|
+
|
|
29
|
+
::: gdsofa.utils
|
|
30
|
+
|
|
31
|
+
## SOFA component wrappers
|
|
32
|
+
|
|
33
|
+
::: gdsofa.comps.base_component
|
|
34
|
+
|
|
35
|
+
## Visual flags
|
|
36
|
+
|
|
37
|
+
::: gdsofa.visual
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# User guide
|
|
2
|
+
|
|
3
|
+
## Requirements
|
|
4
|
+
|
|
5
|
+
gdsofa requires a SOFA build. Set the **`SOFA_ROOT`** environment variable to your SOFA build directory (the path that contains `lib/python3/site-packages` or `lib/python/site-packages`). The library uses it to extend `sys.path` and load SOFA’s Python bindings.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
export SOFA_ROOT=/path/to/sofa/build
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
If `SOFA_ROOT` is missing, `load_SOFA()` and `RunSofa` will raise an error.
|
|
12
|
+
|
|
13
|
+
gdsofa also depends on [gdutils](https://gdutils-a2ef81.gitlabpages.inria.fr/) for JSON I/O, timing, and iterable helpers (see [Utilities](#utilities)).
|
|
14
|
+
|
|
15
|
+
## Scene graph
|
|
16
|
+
|
|
17
|
+
The scene is a tree of **nodes**. Each node can have child nodes and **components** (SOFA objects).
|
|
18
|
+
|
|
19
|
+
### Root and nodes
|
|
20
|
+
|
|
21
|
+
- **`RootNode(**kw)`** — Creates the root node of the scene and adds a default visual manager loop. Use this as the top-level node.
|
|
22
|
+
- **`Node(name, parent=None, gravity=None, **kwargs)`** — Creates a child node. You typically create children with **`parent.add_child(name, **kwargs)`**.
|
|
23
|
+
- Add components to a node with **`node + component`** (e.g. `root + DefaultAnimationLoop()`).
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import gdsofa as gs
|
|
29
|
+
|
|
30
|
+
root = gs.RootNode()
|
|
31
|
+
root + gs.DefaultAnimationLoop()
|
|
32
|
+
|
|
33
|
+
# Add a child node and components
|
|
34
|
+
child = root.add_child("my_node")
|
|
35
|
+
child + gs.MechanicalObject()
|
|
36
|
+
# ... more components
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Core types
|
|
40
|
+
|
|
41
|
+
- **Components** — Use the classes from `gdsofa.comps` (e.g. `DefaultAnimationLoop`, `MechanicalObject`, `EulerImplicitSolver`, `MeshVTKLoader`, `Gravity`). They are added to nodes with `+`.
|
|
42
|
+
- **Links** — Use `Link` and `MultiLink` from `gdsofa.core` to reference other components (e.g. for mappings or loaders).
|
|
43
|
+
- **Objects** — For custom SOFA types, use `Object(class_name, **kwargs)` from `gdsofa.core`.
|
|
44
|
+
|
|
45
|
+
## Parameters
|
|
46
|
+
|
|
47
|
+
**`BaseSOFAParams`** holds simulation and I/O settings. Main attributes:
|
|
48
|
+
|
|
49
|
+
- **`out_dir`** — Output directory (created if missing).
|
|
50
|
+
- **`n`** — Number of iterations (default 100).
|
|
51
|
+
- **`dt`** — Time step (default 1; often overridden, e.g. 0.005).
|
|
52
|
+
- **`scale`** — Scaling for SOFA loaders.
|
|
53
|
+
- **`data_path`** — Optional data input directory.
|
|
54
|
+
|
|
55
|
+
You can load/save parameters as JSON:
|
|
56
|
+
|
|
57
|
+
- **`params.dump_json(fname)`** / **`params.save()`** — Write current parameters (e.g. to `out_dir/params.json`).
|
|
58
|
+
- **`BaseSOFAParams.from_json(fname)`** / **`BaseSOFAParams.from_dir(dname)`** — Load from file or from a directory containing `params.json`.
|
|
59
|
+
|
|
60
|
+
Example:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
params = gs.BaseSOFAParams(out_dir="/path/to/out", n=200, dt=0.005)
|
|
64
|
+
params.save()
|
|
65
|
+
# later:
|
|
66
|
+
params = gs.BaseSOFAParams.from_dir("/path/to/out")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Controllers
|
|
70
|
+
|
|
71
|
+
Custom logic per time step is done with **controllers**. Subclass **`BaseSOFAController`** and override:
|
|
72
|
+
|
|
73
|
+
- **`before_animate(ctx)`** — Called before each simulation step.
|
|
74
|
+
- **`after_animate(ctx)`** — Called after each step.
|
|
75
|
+
|
|
76
|
+
The controller receives `root` (the gdsofa root node) and `params` (the `BaseSOFAParams` instance). Use `self.get_node(name)` and `self.root.find(...)` to reach nodes and components.
|
|
77
|
+
|
|
78
|
+
Pass controller **classes** (not instances) to **`RunSofa`**:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
class MyController(gs.BaseSOFAController):
|
|
82
|
+
def after_animate(self, ctx):
|
|
83
|
+
# e.g. log or export data
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
sofa = gs.RunSofa(root, params, MyController)
|
|
87
|
+
sofa.run(gui=False)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Running
|
|
91
|
+
|
|
92
|
+
**`RunSofa(root, params=None, *controllers)`** builds the SOFA scene from the gdsofa graph and optionally attaches controllers.
|
|
93
|
+
|
|
94
|
+
- **`params`** — Optional. If omitted, a default `BaseSOFAParams()` is used (you should set at least `out_dir` for file output).
|
|
95
|
+
- **`*controllers`** — Optional controller classes.
|
|
96
|
+
|
|
97
|
+
Methods:
|
|
98
|
+
|
|
99
|
+
- **`run(gui=False, std_to_file=False, viewer="qglviewer", title="MyProject")`** — Runs the simulation.
|
|
100
|
+
- **`gui=True`** — Opens the SOFA GUI (requires Qt and SOFA GUI plugins).
|
|
101
|
+
- **`gui=False`** — Headless: runs `n` steps (from `params`).
|
|
102
|
+
- **`std_to_file=True`** — Redirects stdout/stderr to files under `params.out_dir` (requires `out_dir` set).
|
|
103
|
+
- **`to_file(fname=None, clean_paths=False, doc=None)`** — Exports the scene as a Python script. Default path is `params.out_dir/sofa_scene.py` if `out_dir` is set.
|
|
104
|
+
- **`import_plugins()`** — Called during `__init__`; imports SOFA plugins required by the scene graph.
|
|
105
|
+
|
|
106
|
+
Example (headless with output directory):
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
params = gs.BaseSOFAParams(out_dir="/path/to/out", n=100, dt=0.005)
|
|
110
|
+
sofa = gs.RunSofa(root, params)
|
|
111
|
+
result = sofa.run(gui=False, std_to_file=True)
|
|
112
|
+
result.save() # writes run_stats.json under out_dir
|
|
113
|
+
sofa.to_file() # writes sofa_scene.py under out_dir
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Utilities
|
|
117
|
+
|
|
118
|
+
**`gdsofa.utils`** provides helpers used internally and available for your scripts:
|
|
119
|
+
|
|
120
|
+
- **From [gdutils](https://gdutils-a2ef81.gitlabpages.inria.fr/)**: `load_json`, `dump_json`, `Timer`, and `as_iterable` (alias of gdutils’ `get_iterable`). gdsofa re-exports these to avoid redundancy; see the gdutils docs for full details.
|
|
121
|
+
- **gdsofa-specific**: `dump_path` (create directory and return path), `Munch` / `munchify` (dict with attribute access), `JsonEncoder` (Path and numpy in JSON), `StdRedirect`, and helpers such as `ensure_ext`, `path_insert_before`, `random_name`, `unique_id`.
|
|
122
|
+
|
|
123
|
+
For path management relative to your script (e.g. output directories), you can use **`gd.fPath(__file__, "out", mkdir=True)`** from gdutils directly.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# gdsofa
|
|
2
|
+
|
|
3
|
+
**gdsofa** is a lightweight Python library for building and running [SOFA](https://www.sofa-framework.org/) (Simulation Open Framework Architecture) scenes programmatically. It provides a scene graph API (nodes, components, parameters, controllers) and a runner for headless or GUI simulations.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Python 3.12 (same as SOFAPython3 version)
|
|
8
|
+
- A SOFA build: set the **`SOFA_ROOT`** environment variable to your SOFA build directory.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install gd-sofa-utils
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
Build a minimal scene, attach parameters, and run it (GUI or headless):
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
import gdsofa as gs
|
|
22
|
+
|
|
23
|
+
root = gs.RootNode()
|
|
24
|
+
root + gs.DefaultAnimationLoop()
|
|
25
|
+
|
|
26
|
+
params = gs.BaseSOFAParams(out_dir="/path/to/out")
|
|
27
|
+
sofa = gs.RunSofa(root, params)
|
|
28
|
+
sofa.run(gui=True)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Next steps:
|
|
32
|
+
|
|
33
|
+
- [User guide](guide.md) — requirements, scene graph, parameters, controllers, running.
|
|
34
|
+
- [API Reference](api.md) — full API documentation.
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// ---- Utils ---------------------------------------------------------------
|
|
5
|
+
|
|
6
|
+
const onReady = (cb) => {
|
|
7
|
+
if (typeof document$ !== "undefined" && document$?.subscribe) {
|
|
8
|
+
document$.subscribe(({ body }) => cb(body));
|
|
9
|
+
} else {
|
|
10
|
+
document.addEventListener("DOMContentLoaded", () => cb(document));
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// ---- KaTeX auto-render ---------------------------------------------------
|
|
15
|
+
|
|
16
|
+
const renderMath = (root) => {
|
|
17
|
+
if (!window.renderMathInElement) return;
|
|
18
|
+
|
|
19
|
+
renderMathInElement(root, {
|
|
20
|
+
delimiters: [
|
|
21
|
+
{ left: "$$", right: "$$", display: true },
|
|
22
|
+
{ left: "$", right: "$", display: false },
|
|
23
|
+
{ left: "\\(", right: "\\)", display: false },
|
|
24
|
+
{ left: "\\[", right: "\\]", display: true },
|
|
25
|
+
],
|
|
26
|
+
throwOnError: false,
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// ---- Mermaid init (once) -------------------------------------------------
|
|
31
|
+
|
|
32
|
+
let mermaidInited = false;
|
|
33
|
+
const initMermaidOnce = () => {
|
|
34
|
+
if (mermaidInited) return;
|
|
35
|
+
if (!window.mermaid?.initialize) return;
|
|
36
|
+
|
|
37
|
+
// mkdocs-mermaid2-plugin usually drives rendering;
|
|
38
|
+
// we only ensure mermaid is configured.
|
|
39
|
+
mermaid.initialize({ startOnLoad: true });
|
|
40
|
+
mermaidInited = true;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// ---- Mermaid popup modal + pan/zoom --------------------------------------
|
|
44
|
+
|
|
45
|
+
let panZoom = null;
|
|
46
|
+
|
|
47
|
+
const destroyPanZoom = () => {
|
|
48
|
+
if (!panZoom) return;
|
|
49
|
+
try {
|
|
50
|
+
panZoom.destroy();
|
|
51
|
+
} catch {
|
|
52
|
+
// ignore
|
|
53
|
+
}
|
|
54
|
+
panZoom = null;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const ensureMermaidModal = () => {
|
|
58
|
+
let modal = document.getElementById("mermaid-modal");
|
|
59
|
+
if (modal) return modal;
|
|
60
|
+
|
|
61
|
+
modal = document.createElement("div");
|
|
62
|
+
modal.id = "mermaid-modal";
|
|
63
|
+
modal.style.cssText = `
|
|
64
|
+
position: fixed; inset: 0; z-index: 9999;
|
|
65
|
+
display: none; align-items: center; justify-content: center;
|
|
66
|
+
background: rgba(0,0,0,.55);
|
|
67
|
+
padding: 24px;
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
const panel = document.createElement("div");
|
|
71
|
+
panel.id = "mermaid-modal-panel";
|
|
72
|
+
panel.style.cssText = `
|
|
73
|
+
position: relative;
|
|
74
|
+
width: min(1200px, 96vw);
|
|
75
|
+
height: min(800px, 92vh);
|
|
76
|
+
background: var(--md-default-bg-color, #fff);
|
|
77
|
+
border-radius: 12px;
|
|
78
|
+
box-shadow: 0 10px 30px rgba(0,0,0,.35);
|
|
79
|
+
overflow: hidden;
|
|
80
|
+
`;
|
|
81
|
+
|
|
82
|
+
const close = document.createElement("button");
|
|
83
|
+
close.type = "button";
|
|
84
|
+
close.textContent = "✕";
|
|
85
|
+
close.setAttribute("aria-label", "Close");
|
|
86
|
+
close.style.cssText = `
|
|
87
|
+
position: absolute; top: 10px; right: 10px; z-index: 3;
|
|
88
|
+
border: 0; border-radius: 10px;
|
|
89
|
+
padding: 6px 10px; cursor: pointer;
|
|
90
|
+
background: rgba(0,0,0,.08);
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
const content = document.createElement("div");
|
|
94
|
+
content.id = "mermaid-modal-content";
|
|
95
|
+
content.style.cssText = `
|
|
96
|
+
width: 100%; height: 100%;
|
|
97
|
+
overflow: hidden;
|
|
98
|
+
padding: 0;
|
|
99
|
+
`;
|
|
100
|
+
|
|
101
|
+
const hide = () => {
|
|
102
|
+
destroyPanZoom();
|
|
103
|
+
modal.style.display = "none";
|
|
104
|
+
// Clean content to release references
|
|
105
|
+
content.replaceChildren();
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
close.addEventListener("click", hide);
|
|
109
|
+
modal.addEventListener("click", (e) => {
|
|
110
|
+
if (e.target === modal) hide();
|
|
111
|
+
});
|
|
112
|
+
document.addEventListener("keydown", (e) => {
|
|
113
|
+
if (e.key === "Escape" && modal.style.display !== "none") hide();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
panel.appendChild(close);
|
|
117
|
+
panel.appendChild(content);
|
|
118
|
+
modal.appendChild(panel);
|
|
119
|
+
document.body.appendChild(modal);
|
|
120
|
+
return modal;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const openMermaidInModal = (svg) => {
|
|
124
|
+
const modal = ensureMermaidModal();
|
|
125
|
+
const content = document.getElementById("mermaid-modal-content");
|
|
126
|
+
|
|
127
|
+
destroyPanZoom();
|
|
128
|
+
|
|
129
|
+
// Wrap to allow svg-pan-zoom to size properly
|
|
130
|
+
const wrap = document.createElement("div");
|
|
131
|
+
wrap.style.cssText = `
|
|
132
|
+
width: 100%;
|
|
133
|
+
height: 100%;
|
|
134
|
+
overflow: hidden;
|
|
135
|
+
touch-action: none;
|
|
136
|
+
background: var(--md-default-bg-color, #fff);
|
|
137
|
+
`;
|
|
138
|
+
|
|
139
|
+
const clone = svg.cloneNode(true);
|
|
140
|
+
|
|
141
|
+
// Responsive sizing; pan/zoom handles viewBox transforms
|
|
142
|
+
clone.removeAttribute("width");
|
|
143
|
+
clone.removeAttribute("height");
|
|
144
|
+
clone.style.width = "100%";
|
|
145
|
+
clone.style.height = "100%";
|
|
146
|
+
clone.style.display = "block";
|
|
147
|
+
|
|
148
|
+
wrap.appendChild(clone);
|
|
149
|
+
content.replaceChildren(wrap);
|
|
150
|
+
|
|
151
|
+
modal.style.display = "flex";
|
|
152
|
+
|
|
153
|
+
if (window.svgPanZoom) {
|
|
154
|
+
// Needs to be in DOM + visible
|
|
155
|
+
requestAnimationFrame(() => {
|
|
156
|
+
panZoom = svgPanZoom(clone, {
|
|
157
|
+
zoomEnabled: true,
|
|
158
|
+
panEnabled: true,
|
|
159
|
+
controlIconsEnabled: true,
|
|
160
|
+
fit: true,
|
|
161
|
+
center: true,
|
|
162
|
+
minZoom: 0.1,
|
|
163
|
+
maxZoom: 50,
|
|
164
|
+
dblClickZoomEnabled: true,
|
|
165
|
+
mouseWheelZoomEnabled: true,
|
|
166
|
+
preventMouseEventsDefault: true,
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const attachMermaidPopupButtons = (root) => {
|
|
173
|
+
const blocks = root.querySelectorAll(".mermaid");
|
|
174
|
+
for (const block of blocks) {
|
|
175
|
+
const svg = block.querySelector("svg");
|
|
176
|
+
if (!svg) continue;
|
|
177
|
+
|
|
178
|
+
if (block.dataset.popupAttached === "1") continue;
|
|
179
|
+
block.dataset.popupAttached = "1";
|
|
180
|
+
|
|
181
|
+
const btn = document.createElement("button");
|
|
182
|
+
btn.type = "button";
|
|
183
|
+
btn.textContent = "Open";
|
|
184
|
+
btn.style.cssText = `
|
|
185
|
+
margin: 6px 0 10px 0;
|
|
186
|
+
padding: 4px 10px;
|
|
187
|
+
border-radius: 10px;
|
|
188
|
+
border: 1px solid rgba(0,0,0,.15);
|
|
189
|
+
background: transparent;
|
|
190
|
+
cursor: pointer;
|
|
191
|
+
font: inherit;
|
|
192
|
+
`;
|
|
193
|
+
|
|
194
|
+
btn.addEventListener("click", () => openMermaidInModal(svg));
|
|
195
|
+
|
|
196
|
+
block.parentElement?.insertBefore(btn, block);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// ---- Main hook -----------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
onReady((body) => {
|
|
203
|
+
initMermaidOnce();
|
|
204
|
+
renderMath(body);
|
|
205
|
+
|
|
206
|
+
// Mermaid rendering can be async; retry briefly to catch SVG insertion
|
|
207
|
+
let tries = 0;
|
|
208
|
+
const tick = () => {
|
|
209
|
+
attachMermaidPopupButtons(body);
|
|
210
|
+
if (++tries < 12) setTimeout(tick, 150);
|
|
211
|
+
};
|
|
212
|
+
tick();
|
|
213
|
+
});
|
|
214
|
+
})();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import gdutils as gd
|
|
4
|
+
import gdsofa as gs
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main():
|
|
8
|
+
out = gd.fPath(__file__, "out")
|
|
9
|
+
|
|
10
|
+
root = gs.RootNode()
|
|
11
|
+
root + gs.DefaultAnimationLoop()
|
|
12
|
+
|
|
13
|
+
params = gs.BaseSOFAParams(out_dir=out)
|
|
14
|
+
|
|
15
|
+
sofa = gs.RunSofa(root, params)
|
|
16
|
+
sofa.run(gui=True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
log = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
logging.basicConfig(level=logging.INFO)
|
|
23
|
+
log = gd.get_logger()
|
|
24
|
+
|
|
25
|
+
main()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gd-sofa-utils
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Requires-Python: ==3.12.*
|
|
5
|
+
Description-Content-Type: text/markdown
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: gd-py-utils
|
|
8
|
+
Requires-Dist: randomname
|
|
9
|
+
Requires-Dist: colour
|
|
10
|
+
Requires-Dist: param
|
|
11
|
+
Requires-Dist: treelib
|
|
12
|
+
Requires-Dist: numpy
|
|
13
|
+
Requires-Dist: pandas
|
|
14
|
+
Provides-Extra: docs
|
|
15
|
+
Requires-Dist: mkdocs; extra == "docs"
|
|
16
|
+
Requires-Dist: mkdocs-material; extra == "docs"
|
|
17
|
+
Requires-Dist: mkdocs-mermaid2-plugin; extra == "docs"
|
|
18
|
+
Requires-Dist: mkdocs-snippets; extra == "docs"
|
|
19
|
+
Requires-Dist: mkdocs-exclude; extra == "docs"
|
|
20
|
+
Requires-Dist: mkdocstrings-python; extra == "docs"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# gdsofa
|
|
24
|
+
|
|
25
|
+
**gdsofa** is a lightweight Python library for building and running [SOFA](https://www.sofa-framework.org/) (Simulation Open Framework Architecture) scenes programmatically. It provides a scene graph API (nodes, components, parameters, controllers) and a runner for headless or GUI simulations.
|
|
26
|
+
|
|
27
|
+
[**Docs**]()
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install gd-sofa-utils
|
|
33
|
+
```
|