Metadata-Version: 2.4
Name: magpylib-studio
Version: 0.1.3
Summary: Headless magpylib editing engine for GUI/LLM frontends (e.g. a VS Code extension backend).
Project-URL: Homepage, https://github.com/magpylib/magpylib-studio
Project-URL: Bug Tracker, https://github.com/magpylib/magpylib-studio/issues
Author-email: Alexandre Boisselet <alexabois+magpylib@gmail.com>
License-Expression: BSD-3-Clause
License-File: LICENSE
Classifier: Development Status :: 1 - Planning
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.11
Requires-Dist: magpylib>=5.2
Requires-Dist: plotly>=5
Provides-Extra: dev
Requires-Dist: jsonschema>=4; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Description-Content-Type: text/markdown

# magpylib-studio

Headless **magpylib editing engine** plus a **VS Code extension** built on it —
a GUI _and_ LLM studio for magnetic scenes. The engine owns a magpylib scene and
exposes everything a frontend needs over a tiny JSON-RPC protocol on stdio; the
presentation layer (VS Code webview, Solara, a CLI…) is a thin client.

The VS Code side is in [`vscode-extension/`](vscode-extension/) — scene tree
with the construction history in it, schema-driven inspector, variables with
sliders, 3D view, field maps and sweeps, an editable script tab, and Copilot
chat tools.

![The Halbach example open in VS Code: the scene tree listing each ring's construction steps and the copies a pattern made, the variables panel with sliders for n, radius, gap, stagger and tilt, the 3D view, and the script tab showing the same scene as parametric magpylib.](docs/halbach.png)

_The `halbach` example: twenty magnets from two `create` steps and two circular
patterns. Dragging `n` rebuilds both rings; the script tab on the right is the
same scene, and saving an edit to it rebuilds the scene from what you wrote._

## Try it out

```sh
code --install-extension magpylib.magpylib-studio-vscode
```

That is the whole install — the Python side is not a second step you do first.
Open a folder, click the magpylib icon in the activity bar and press **Load
Example Scene**; the first time the extension needs the engine it offers
**Install the Engine**, which either offers you the interpreter the Python
extension already selected — named, because installing into someone else's
interpreter is not the same act as making one — or makes a `.venv` in the
workspace, installs `magpylib-studio` into it and points
`magpylib-studio.pythonPath` at it for you. With [uv][uv] present it fetches a
matching Python too, so even the ≥ 3.11 floor is not something to arrange in
advance.

Full walkthrough in the
[extension README](vscode-extension/README.md#try-it-out). To work on the
extension rather than use it, see [Development](#development).

[uv]: https://docs.astral.sh/uv/

## Install the engine on its own

```sh
pip install magpylib-studio
```

That is enough: the engine works with **released magpylib** (≥ 5.2). The
optional [property-tree branch][branch] adds a first-class style API and
path-valued physics properties (`current=[100, 200, 300]`):

```sh
pip install "magpylib @ git+https://github.com/magpylib/magpylib@feat/improve-style"
```

`magpylib_studio/style_compat.py` detects which one you have. On released
magpylib it reproduces the four style operations the engine needs from
`style.update()` / `style.as_dict()`, and falls back to a generated copy of the
branch's JSON Schema (`style_schemas.json`) — the two style trees are the same
shape, so the inspector keeps real widgets (enum dropdowns, ranges, colour
pickers) either way. The test suite runs against both.

[branch]: https://github.com/magpylib/magpylib/tree/feat/improve-style

### Development

```sh
git clone https://github.com/magpylib/magpylib-studio.git
cd magpylib-studio

# the engine
uv venv --python 3.13 .venv
VIRTUAL_ENV=$PWD/.venv uv pip install -e ".[dev]"
.venv/bin/python -m pytest -q

# the extension (from vscode-extension/)
npm install
npm run compile     # tsc + eslint + webview, contribution and version checks
npm test            # thirteen tests in a real Extension Development Host
```

Then open **the repo root** in VS Code and press `F5` — not the
`vscode-extension/` folder: the launch config is at the root so the engine stays
in the workspace you can edit, and F5 compiles before it launches. A second
window opens, the Extension Development Host, with `sandbox/` as its workspace.

Hooks run on every push via pre-commit.ci (`.pre-commit-config.yaml`), and
locally with `pre-commit run --all-files`: ruff and prettier for formatting,
ruff for linting (the ruleset is pinned in `pyproject.toml`, because ruff's
defaults move), plus workflow and `pyproject` validation. The one commit that
only reformats is listed in `.git-blame-ignore-revs`;
`git config blame.ignoreRevsFile .git-blame-ignore-revs` makes local blame skip
it, as GitHub already does.

## Design decisions

- **The document is a log, and the object tree is a projection of it.**
  `doc["events"]` holds everything that built the scene — `create`, `remove`,
  `reparent`, the transforms and the patterns — and every build folds it from
  the start, so `doc["objects"]` is regenerated rather than stored. Strip it
  from a document and the log reconstructs the same scene, ids and field
  included. Editing an early event therefore re-applies everything after it for
  free; what it breaks is reported rather than blocking the edit.
- **What a thing _is_ is edited; what happened _to_ it is appended.** An
  object's type, parameters and style live on its `create` event and are changed
  in place — dragging a slider must not write history — while moves, rotations,
  removals and reparents go on the end. That one distinction is what keeps the
  log finite _and_ meaningful.
- **Transforms are recorded magpylib calls, not derived poses.** The log holds
  `move`, `rotate_from_angax`, … as they were made, so magpylib owns every
  semantic: paths, anchors, `start`, and group transforms carrying a subtree.
- **Scenes are parametric.** Any numeric value may be an expression over the
  document's variables (`"=360/n"`), evaluated from its AST against an
  allow-list — never `eval`, because a document is something you open from
  someone else. A variable is not always a quantity: one bounded by `options`
  holds a name (`"z"` for an axis), which gets a dropdown for the same reason
  min/max gets a slider, and is enforced the same way. `sweep()` re-folds the
  scene once per value of a variable, which is affordable because a rebuild is
  milliseconds.
- **One schema contract.** The same JSON Schema drives the inspector widgets
  _and_ the LLM tool inputs.
- **Validation is shared.** Every edit goes through magpylib, and a bad edit is
  _reported_ (`{"ok": false, "error": …}`), not raised — so a GUI shows an error
  and an LLM self-corrects. There is no second validation layer.
- **The saved file is the document, and it is versioned.** A scene saves as
  `.magpy.json` — exactly what `to_dict()` returns, so the format the engine
  works in is the format on disk, with no serializer in between to disagree with
  it. It carries a `version`, because a file outlives the program that wrote it:
  an older one is migrated, and a _newer_ one is refused rather than read
  half-way and saved back with the parts we did not understand missing. Fields
  the engine does not recognise are carried through, which is the only form of
  forward compatibility a document can actually have. A script is an export, not
  a save: it loses slider bounds and hidden flags, and nothing else — measured,
  not assumed.
- **Document canonical, script generated — and read back two ways.**
  `to_script()` emits runnable magpylib code, patterns included (as the loops
  they mean), **folding the log in order** rather than declaring everything up
  front: where an object is created relative to the steps around it is part of
  the scene, and an object added to an already-patterned group must not end up
  inside every copy. `apply_script()` **parses** it when it is still in that
  shape, so variables, event order and arrangements survive and the whole
  document round-trips byte-identically; anything else — a loop of your own, a
  helper, numpy — is _executed_ with `show()` intercepted, as `load_script()`
  always did, and what that flattens is reported.

## JSON-RPC protocol (stdio)

The host spawns `python -m magpylib_studio` and exchanges one JSON object per
line — no ports, no framework.

```
-> {"id": 1, "method": "get_schema", "params": {"object_id": "cube"}}
<- {"id": 1, "result": { ...JSON Schema... }}
<- {"id": 2, "error": {"type": "KeyError", "message": "..."}}
```

| group     | methods                                                                                                                                      |
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| inspect   | `list_objects` · `get_schema` · `get_values` (style) · `get_params` (physics) · `get_transform` · `get_history`                              |
| structure | `add_object` · `remove_object` · `copy_object` · `move_object` (reparent) · `set_visible`                                                    |
| edit      | `apply_edit` (style) · `set_param` · `reset_style`                                                                                           |
| transform | `move` · `rotate` · `set_transform` · `clear_path` · `set_pixel_grid`                                                                        |
| patterns  | `duplicate_around` (circular) · `duplicate_along` (linear; twice = a grid) · `mirror`                                                        |
| variables | `get_variables` · `set_variable` · `set_variable_bounds` · `remove_variable` · `unknown_variables` · `expression_help` · `check_expression`  |
| history   | `get_events` · `edit_event` · `move_event` · `remove_event` · `set_rollback`                                                                 |
| view      | `get_figure` (3D) · `get_field_figure` (along a sensor path) · `get_field_map` (plane heatmap) · `get_sweep_figure`                          |
| field     | `get_field` — summed B/H at points or along a sensor · `sweep` — the field against a variable                                                |
| undo      | `undo` · `redo` · `goto_history`                                                                                                             |
| I/O       | `load_scene` · `load_script` · `apply_script` · `load_captured` · `list_examples` · `load_example` · `clear_scene` · `to_dict` · `to_script` |
| bulk      | `batch` — many mutating ops in one call, one undo step                                                                                       |

Mutating methods return `{"ok": bool, "error"?: str}`. Everything is
JSON-serializable in both directions.

Try it:

```sh
printf '%s\n' \
  '{"id":1,"method":"load_example"}' \
  '{"id":2,"method":"list_objects"}' \
  '{"id":3,"method":"get_field","params":{"points":[[0,0,0]]}}' \
  '{"id":4,"method":"to_script"}' \
| python -m magpylib_studio
```

## Status

The engine is covered by 104 tests against both magpylib versions
(`.venv/bin/python -m pytest -q`).

The extension is checked at three levels, all wired into `npm run compile` so
they run before every F5 and before packaging: type-checking and ESLint over
both the host code and the webview scripts; two contribution checks (every
declared command registered, every menu clause matching a context value the tree
can set, every palette entry safe to invoke with no argument); and a DOM harness
that runs a panel's real script against a real engine
(`npm run inspect -- halbach`). On top of that, `npm test` runs thirteen
integration tests **inside a real Extension Development Host** — activation, the
engine subprocess answering through the virtual `scene.json`, a removal taking a
pattern's copies with it, the script tab applying an edit on save, the engine
being killed mid-session and coming back holding the same scene, and the whole
save/open path: a file that opens back as the same scene, a save that goes to
the file it came from without asking, a document from a newer version being
refused without disturbing the open one, and the crash backup being written and
restorable.

Both suites and the packaging run in CI on every push, the engine against **both
magpylib versions** — the claim above used to be checked by hand. Pushing a `v*`
tag builds the `.vsix` and attaches it to a GitHub release.

Both halves are published: the engine on PyPI (`pip install magpylib-studio`)
and the extension on the Marketplace under the `magpylib` publisher. That order
mattered — an extension whose first act is "now go and pip install this git URL"
fails at first contact, before anyone sees a feature — and it is what lets
**Install the Engine** set the engine up for you rather than tell you to. The
`.vsix` attached to each release is the same build, for anyone who would rather
install it by hand.

See [CONTINUE.md](CONTINUE.md) for the current state and what is next.

## License

BSD-3-Clause — the same as the other packages built on [magpylib][mplib]
([magpylib-force][force], [magpylib-material-response][matresp]). The core
library itself is BSD-2-Clause; its satellites are all 3-clause, and this is one
of those. See [LICENSE](LICENSE).

[mplib]: https://github.com/magpylib/magpylib
[force]: https://github.com/magpylib/magpylib-force
[matresp]: https://github.com/magpylib/magpylib-material-response
