|
| 1 | +# Python Bindings |
| 2 | + |
| 3 | +Python drives the engine through the command stream. You build a command as plain |
| 4 | +data, submit it, and read the reply. The bindings live at |
| 5 | +`crates/nightshade-api/bindings/python` and wrap that stream in a small ergonomic |
| 6 | +layer, so the spinning cube is the whole pitch: |
| 7 | + |
| 8 | +```python |
| 9 | +import nightshade as ns |
| 10 | + |
| 11 | +app = ns.open() |
| 12 | +cube = app.spawn_cube([0.0, 0.5, 0.0]) |
| 13 | +app.set_color(cube, [0.2, 0.6, 1.0, 1.0]) |
| 14 | +while app.frame(): |
| 15 | + app.rotate(cube, [0.0, 1.0, 0.0], app.delta_time()) |
| 16 | +``` |
| 17 | + |
| 18 | +## Building |
| 19 | + |
| 20 | +Needs a Rust toolchain and `maturin`. From the binding directory: |
| 21 | + |
| 22 | +``` |
| 23 | +just develop # build the extension into the active environment |
| 24 | +just test # headless tests (the GPU render test is gated off) |
| 25 | +just build # produce an abi3 wheel in ./dist |
| 26 | +``` |
| 27 | + |
| 28 | +The wheel is `abi3`, so one build serves CPython 3.8 and up. A window needs a |
| 29 | +display and a GPU. For headless work, `ns.render_to_image` draws one offscreen |
| 30 | +frame after a setup batch. |
| 31 | + |
| 32 | +## The model |
| 33 | + |
| 34 | +The extension is a thin forwarder. It opens a window, advances a frame, and |
| 35 | +submits commands through the same `submit_commands` the rest of the api uses. No |
| 36 | +`World`, no engine internals cross the boundary, only commands in and replies |
| 37 | +out. Commands and replies travel as json, so the bridge has no per-object |
| 38 | +conversion layer. |
| 39 | + |
| 40 | +`App` is the open window. Its ergonomic methods build a command and submit it: |
| 41 | +`spawn_cube`, `spawn_sphere`, `spawn_floor`, `set_color`, `set_metallic_roughness`, |
| 42 | +`set_emissive`, `set_position`, `rotate`, `set_scale`, `set_parent`, `despawn`, |
| 43 | +`point_light`, `set_sun`, `set_background`, `orbit_camera`, `fly_camera`, |
| 44 | +`look_at`, `play_animation`, `delta_time`, `elapsed_seconds`, `key_down`, |
| 45 | +`key_pressed`, `mouse_clicked`, `wasd`, `clicked_entity`, `entity_under_cursor`, |
| 46 | +`cursor_on_ground`, and more. |
| 47 | + |
| 48 | +Spawns return an `Entity` handle. Pass it straight back into any command that |
| 49 | +takes one. |
| 50 | + |
| 51 | +```python |
| 52 | +ball = app.spawn_sphere([0.0, 4.0, 0.0]) |
| 53 | +app.set_metallic_roughness(ball, 0.9, 0.2) |
| 54 | +hit = app.clicked_entity() |
| 55 | +if hit is not None: |
| 56 | + app.set_color(hit, [1.0, 0.3, 0.2, 1.0]) |
| 57 | +``` |
| 58 | + |
| 59 | +## The full surface |
| 60 | + |
| 61 | +The ergonomic methods cover the common calls. For anything else, `submit` and |
| 62 | +`submit_batch` take the whole command surface as dicts in the |
| 63 | +`{"Variant": {fields}}` form. A batch can reference an entity an earlier command |
| 64 | +produced, so it builds and wires a scene in one call. |
| 65 | + |
| 66 | +```python |
| 67 | +app.submit({"SetExposure": {"exposure": 1.2}}) |
| 68 | +app.submit_batch([ |
| 69 | + {"SpawnCube": {"position": [-1.5, 0.5, 0.0]}}, |
| 70 | + {"SpawnSphere": {"position": [1.5, 0.5, 0.0]}}, |
| 71 | +]) |
| 72 | +``` |
| 73 | + |
| 74 | +`ns.command_schema()` returns the json schema for the command surface, and |
| 75 | +`ns.command_reply_schema()` the reply schema, both as dicts. They are the source |
| 76 | +a binding language generates its encoder and decoder from. |
| 77 | + |
| 78 | +## Input |
| 79 | + |
| 80 | +Key names are plain strings: letters, digits, `"space"`, `"enter"`, `"escape"`, |
| 81 | +`"tab"`, the arrows `"left"`/`"right"`/`"up"`/`"down"`, and `"shift"`/`"ctrl"`/ |
| 82 | +`"alt"`. The `ns.keys` namespace holds the common ones. Mouse buttons are |
| 83 | +`0` left, `1` middle, `2` right. |
| 84 | + |
| 85 | +```python |
| 86 | +while app.frame(): |
| 87 | + if app.key_down("space"): |
| 88 | + ... |
| 89 | + move = app.wasd() |
| 90 | +``` |
| 91 | + |
| 92 | +## Not bound yet |
| 93 | + |
| 94 | +The participant event stream (`push_command` and `drain_events`) is not exposed. |
| 95 | +Its `Event` and `Command` types need a serde wire form that matches their |
| 96 | +packed-integer entity schema before the binding can carry them. The command |
| 97 | +stream above is the whole surface for now. |
0 commit comments