139 lines
9.8 KiB
Markdown
139 lines
9.8 KiB
Markdown
# Spatial Lab
|
|
|
|
An agent-operated mathematical geometry workbench for designing intricate architectural structures before building the final environment in Blender. The editable source is a construction graph of curves, transported frames, sections, surfaces and transforms. The browser observes the current design; authoring happens through Python or CLI transactions.
|
|
|
|
**Working loop:** inspect → edit parameters or graph → observe → validate → export → update Blender → inspect again. A project remains editable throughout this loop, with persistent undo/redo. Nothing depends on reconstructing shapes from an image.
|
|
|
|
## Run
|
|
|
|
Clone this repository with Git LFS to retrieve the Blender example:
|
|
|
|
```sh
|
|
git clone https://github.com/emil28092005/spatial-lab.git
|
|
cd spatial-lab
|
|
git lfs pull
|
|
```
|
|
|
|
Python 3.11+ and a browser with WebGL2 are required. There are no runtime Python dependencies, CDN assets or accounts to configure. From this directory:
|
|
|
|
```sh
|
|
./spatial-lab serve examples/braided_atrium.spatial.json --port 8767
|
|
```
|
|
|
|
Open <http://127.0.0.1:8767>. The launcher also works by absolute path from another working directory. `python3 -m spatial_lab` is equivalent when run from this directory. Optional installation: `python3 -m pip install -e .` provides the `spatial-lab` command.
|
|
|
|
The viewer automatically rebuilds on source changes. Drag to orbit, Shift-drag to pan, scroll to zoom. Use Top/Front/Side, Fit, Surface/Wire, node selection, Isolate and Section Z to inspect the structure. Viewer controls never edit geometry. Invalid source displays an error and labels the previous valid view as stale. The HTTP server binds only to localhost and exposes no write API.
|
|
|
|
## Iterative authoring
|
|
|
|
```sh
|
|
./spatial-lab init out/my-study.json --example simple
|
|
./spatial-lab inspect out/my-study.json
|
|
./spatial-lab param out/my-study.json height 12
|
|
./spatial-lab undo out/my-study.json
|
|
./spatial-lab redo out/my-study.json
|
|
./spatial-lab build out/my-study.json --out out/my-study.bundle.json
|
|
./spatial-lab check out/my-study.bundle.json
|
|
./spatial-lab ops
|
|
```
|
|
|
|
Use `init --example atrium` for the larger demo. Existing files are never overwritten by `init`. Command output is JSON; validation failures return exit code 2.
|
|
|
|
For several related changes, write a JSON array and apply it with `./spatial-lab patch PROJECT changes.json`:
|
|
|
|
```json
|
|
[
|
|
{"action":"set_parameter","name":"width","value":1.8},
|
|
{"action":"patch_node","id":"braided-galleries","patch":{"params":{"count":2}}}
|
|
]
|
|
```
|
|
|
|
The entire graph must build successfully before any edit is saved. `upsert_node` adds/replaces an operation; `patch_node` merges its `params` and `inputs`; `remove_node` rejects a node with dependents unless `cascade:true`. `node PROJECT node.json` and `remove PROJECT ID --cascade` are CLI shortcuts. Persistent history retains 64 revisions beside the project in `.spatial_lab/`. Direct external JSON edits start a new undo chain. Keep source projects in Git for long-term history.
|
|
|
|
Python uses the same transaction layer:
|
|
|
|
```python
|
|
from spatial_lab import Project
|
|
|
|
project = Project("examples/braided_atrium.spatial.json")
|
|
bundle = project.apply([
|
|
{"action": "set_parameter", "name": "width", "value": 1.8},
|
|
])
|
|
project.travel("undo")
|
|
```
|
|
|
|
## Construction vocabulary
|
|
|
|
All built-in operators currently use version 1. Node IDs remain stable during edits. Inputs reference other node IDs; graph order is resolved automatically. Set `visible:false` on intermediate nodes to avoid exporting duplicate construction geometry. Hidden nodes still evaluate and feed downstream operations.
|
|
|
|
| Operator | Inputs | Main parameters |
|
|
| --- | --- | --- |
|
|
| `curve` | — | `xyz`: three expressions in `t`; `domain` (default `[0,"tau"]`); `segments` (128); `closed` (false); optional `max_chord_error` in metres |
|
|
| `polyline` | — | `points`: 3D vectors; `closed` (false); do not repeat the first point |
|
|
| `frames` | `path` | `up` (`[0,0,1]`); `twist_degrees` (0); parallel transport with closed-loop seam correction |
|
|
| `sweep` | `frames` | `profile`: a simple closed 2D polygon in local X/Z; `cap` (true) |
|
|
| `surface` | — | `xyz` in `u,v`; `u_domain`,`v_domain` (`[0,1]`); `u_segments` (32), `v_segments` (16); `wrap_u`,`wrap_v` (false); `thickness` (0) |
|
|
| `loft` | `sections`: list of curves | Corresponding closed contours with equal point counts; `cap` (true), requiring planar ends |
|
|
| `transform` | `source` | `translate`, `rotate` (XYZ Euler degrees), `scale` (positive scalar or 3D vector); curve or mesh input |
|
|
| `repeat` | `source`: mesh | `count` (2); `translate`,`rotate`,`scale` define a cumulative step; instances share geometry |
|
|
| `merge` | `sources`: list of meshes | Groups parts without performing a boolean union |
|
|
|
|
Numeric fields accept arithmetic expressions using project parameters, `pi`, `tau`, `e`, trigonometry and basic math functions. Global parameters are constants: they cannot refer to one another. Formula evaluation uses a bounded arithmetic AST, not Python `eval`. Local frames use X=right, Y=tangent, Z=up. A closed frame loop requires twist to be a multiple of 360°. Positive-scale transforms preserve orientation. Cumulative repetition applies the step matrix repeatedly, so translation can rotate or scale between instances.
|
|
|
|
Curves have explicit sampling resolution. `estimated_chord_error_m` measures deviation at interval midpoints; it is an estimate, not a certified maximum or adaptive subdivision. Inspect it and increase `segments` when needed. Thin shells offset sampled normals; large offsets can intersect themselves.
|
|
|
|
## Exact Blender handoff
|
|
|
|
```sh
|
|
./spatial-lab build examples/braided_atrium.spatial.json --out out/braided_atrium.bundle.json
|
|
blender -b --factory-startup --python-exit-code 1 --python blender/bridge.py -- \
|
|
--bundle out/braided_atrium.bundle.json \
|
|
--save out/Braided_Atrium.blend --report out/blender_verification.json
|
|
```
|
|
|
|
For a live Blender session (including through Blender MCP), load the bridge once, select the intended dedicated scene, then repeat `import_bundle` after rebuilding:
|
|
|
|
```python
|
|
import importlib.util
|
|
from pathlib import Path
|
|
|
|
lab = Path("/absolute/path/to/spatial-lab")
|
|
spec = importlib.util.spec_from_file_location("spatial_bridge", lab / "blender/bridge.py")
|
|
bridge = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(bridge)
|
|
scene = bridge.create_scene("Spatial study") # Reuse this scene on subsequent imports.
|
|
bridge.import_bundle(lab / "out/braided_atrium.bundle.json", scene)
|
|
report = bridge.verify_bundle(lab / "out/braided_atrium.bundle.json", scene)
|
|
assert report["passed"], report
|
|
```
|
|
|
|
The importer updates objects by stable ID and prunes only obsolete objects tagged with this project ID in the supplied scene. Existing object identity, modifiers and object-level material assignments survive updates. It replaces the owned base mesh and transforms: do final base-mesh editing on a separate copy or after the mathematical design is finished. Per-face authored material assignments are not preserved. Other scene objects are untouched. Imported guides are wire meshes and frame empties. Scene unit scale must be 1.
|
|
|
|
The JSON contract is documented in [FORMAT.md](docs/FORMAT.md). Vertices, triangle indices and transformation matrices are transferred directly. Verification compares base-mesh coordinates in local and world space, exact connectivity/winding, IDs and matrices; artist modifiers are excluded. The supplied demo was tested in Blender 5.2.2 LTS with maximum world-coordinate error below 0.000004 m. No manual reconstruction or random regeneration occurs during import.
|
|
|
|
Ready-made artifacts: [Blender scene](examples/verified/Braided_Atrium.blend), [technical bundle](examples/verified/braided_atrium.bundle.json), [verification report](examples/verified/blender_verification.json). Retrieve the `.blend` with Git LFS when cloning the repository.
|
|
|
|
## Extend
|
|
|
|
`examples/wave_extension.py` demonstrates a composable curve-deformation operator:
|
|
|
|
```sh
|
|
./spatial-lab --plugin examples/wave_extension.py ops
|
|
```
|
|
|
|
Register a function with `@register_operator(name, version=1, inputs=(...))`. It receives `(context, params, resolved_inputs)` and returns a `curve`, `frames` or `mesh` entity. Use `context.number`, `vec`, `count` and `expression` to evaluate bounded numeric parameters. See built-ins in `spatial_lab/operators.py` for entity shapes. Load the same plugin for every authoring/build/viewer command using that operator. Blender can import the already evaluated bundle without the plugin. Plugins are explicitly trusted local Python code; project JSON cannot auto-load them.
|
|
|
|
## Validation and current limits
|
|
|
|
```sh
|
|
python3 -m unittest discover -s tests -v
|
|
node --check spatial_lab/web/app.js
|
|
blender -b --factory-startup --python-exit-code 1 --python tests/blender_roundtrip.py
|
|
```
|
|
|
|
The Blender integration test changes geometry, removes an instance, checks artist-work preservation, undoes the transaction and verifies the restored bundle. It writes a report, bundle and `.blend` into `out/`. Kernel tests include analytical volumes, frame orthogonality, closed seams, concave caps, graph dependencies, safe expressions, deterministic hashes and transactional history. HTTP tests cover live updates and error handling.
|
|
|
|
Version 0.1 checks triangle degeneracy, edge incidence/orientation, expected boundaries, finite coordinates and transfer parity. It does **not** solve constraints, detect 3D self-intersections/collisions, prove walkability, perform solid booleans or implement portals/non-Euclidean runtime space. The braided atrium is a geometric study, not a finished playable level or cinematic scene. Those remain subsequent design and Blender/engine stages. Engine-specific adapters, UVs, textures and production detailing are deliberately outside the current mathematical handoff.
|
|
|
|
Runtime limits include 256 nodes, 4,096 exported objects, 200,000 vertices per mesh and 500,000 unique vertices per bundle. Large compositions should be split into projects. [Design notes](docs/DESIGN.md) describe the architecture and viewer choices.
|