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.
Files changed (39) hide show
  1. gd_sofa_utils-0.0.1/.gitignore +13 -0
  2. gd_sofa_utils-0.0.1/.gitlab-ci.yml +118 -0
  3. gd_sofa_utils-0.0.1/LICENSE +16 -0
  4. gd_sofa_utils-0.0.1/PKG-INFO +33 -0
  5. gd_sofa_utils-0.0.1/README.md +11 -0
  6. gd_sofa_utils-0.0.1/docs/api.md +37 -0
  7. gd_sofa_utils-0.0.1/docs/extra.css +3 -0
  8. gd_sofa_utils-0.0.1/docs/guide.md +123 -0
  9. gd_sofa_utils-0.0.1/docs/index.md +34 -0
  10. gd_sofa_utils-0.0.1/docs/setup.js +214 -0
  11. gd_sofa_utils-0.0.1/examples/general_example.py +25 -0
  12. gd_sofa_utils-0.0.1/gd_sofa_utils.egg-info/PKG-INFO +33 -0
  13. gd_sofa_utils-0.0.1/gd_sofa_utils.egg-info/SOURCES.txt +37 -0
  14. gd_sofa_utils-0.0.1/gd_sofa_utils.egg-info/dependency_links.txt +1 -0
  15. gd_sofa_utils-0.0.1/gd_sofa_utils.egg-info/requires.txt +15 -0
  16. gd_sofa_utils-0.0.1/gd_sofa_utils.egg-info/top_level.txt +1 -0
  17. gd_sofa_utils-0.0.1/gdsofa/__init__.py +8 -0
  18. gd_sofa_utils-0.0.1/gdsofa/comps/__init__.py +2 -0
  19. gd_sofa_utils-0.0.1/gdsofa/comps/_build_dynamic_lib.py +81 -0
  20. gd_sofa_utils-0.0.1/gdsofa/comps/_dyn.py +2184 -0
  21. gd_sofa_utils-0.0.1/gdsofa/comps/base_component.py +330 -0
  22. gd_sofa_utils-0.0.1/gdsofa/controller/__init__.py +2 -0
  23. gd_sofa_utils-0.0.1/gdsofa/controller/controller.py +254 -0
  24. gd_sofa_utils-0.0.1/gdsofa/controller/controller_data.py +22 -0
  25. gd_sofa_utils-0.0.1/gdsofa/core/__init__.py +3 -0
  26. gd_sofa_utils-0.0.1/gdsofa/core/component.py +59 -0
  27. gd_sofa_utils-0.0.1/gdsofa/core/links.py +66 -0
  28. gd_sofa_utils-0.0.1/gdsofa/core/node.py +427 -0
  29. gd_sofa_utils-0.0.1/gdsofa/load_SOFA.py +30 -0
  30. gd_sofa_utils-0.0.1/gdsofa/sofa_parameters.py +161 -0
  31. gd_sofa_utils-0.0.1/gdsofa/sofawrap.py +151 -0
  32. gd_sofa_utils-0.0.1/gdsofa/utils.py +162 -0
  33. gd_sofa_utils-0.0.1/gdsofa/visual.py +33 -0
  34. gd_sofa_utils-0.0.1/mkdocs.yml +55 -0
  35. gd_sofa_utils-0.0.1/pyproject.toml +37 -0
  36. gd_sofa_utils-0.0.1/setup.cfg +4 -0
  37. gd_sofa_utils-0.0.1/tests/README.md +31 -0
  38. gd_sofa_utils-0.0.1/tests/test_scene.py +273 -0
  39. gd_sofa_utils-0.0.1/uv.lock +1203 -0
@@ -0,0 +1,13 @@
1
+ __pycache__
2
+ .venv
3
+ out
4
+ *.egg-info
5
+ dist
6
+ .pytest_cache
7
+ .coverage
8
+ site
9
+ public
10
+ output
11
+ *.ini
12
+ *.view
13
+
@@ -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,3 @@
1
+ .md-main__inner {
2
+ padding-bottom: 3rem;
3
+ }
@@ -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
+ ```