nightshade-api 0.53.0

Procedural high level API for the nightshade game engine
Documentation
//! Leptos-free wire types spoken between a page and its render worker,
//! compiled under either the `leptos` or the `offscreen` feature. Pixel
//! quantities are physical surface pixels (CSS pixels times the device pixel
//! ratio), origin at the canvas top-left.
//!
//! [`ToWorker`] and [`FromWorker`] are the control plane the host owns:
//! init, resize, input, picking, and status. Application traffic rides a
//! separate envelope field as the message pair a [`WorkerProtocol`] impl
//! declares, with optional binary [`Attachments`] transferred alongside.

use serde::{Deserialize, Serialize};
use serde_json::Value;

/// Envelope field carrying the serialized control message in every
/// `postMessage`.
pub const MESSAGE_KEY: &str = "message";
/// Envelope field carrying the transferred `OffscreenCanvas` (on `Init` only).
pub const CANVAS_KEY: &str = "canvas";
/// Envelope field carrying a serialized application message: the
/// [`WorkerProtocol::Incoming`] type page to worker, the
/// [`WorkerProtocol::Outgoing`] type worker to page.
pub const APP_KEY: &str = "app";
/// Envelope field carrying named binary payloads as a `{ name: Uint8Array }`
/// object whose buffers ride the transfer list.
pub const ATTACHMENTS_KEY: &str = "attachments";

/// Named binary payloads accompanying an application message: dropped asset
/// bytes, a glTF with its resources, a saved file.
pub type Attachments = Vec<(String, Vec<u8>)>;

/// The application message pair a page and its worker speak over the
/// [`APP_KEY`] envelope field, from the worker's point of view:
/// `Incoming` arrives from the page, `Outgoing` returns to it. The control
/// plane (init, resize, input, picking, status) is not part of the
/// protocol; the host owns it. Both types serialize and deserialize
/// because each crosses the wire in one direction and is decoded on the
/// other side.
pub trait WorkerProtocol {
    type Incoming: Serialize + serde::de::DeserializeOwned + 'static;
    type Outgoing: Serialize + serde::de::DeserializeOwned + Send + Sync + 'static;
}

/// The untyped protocol: JSON values both ways. What `run_offscreen` and
/// the default `use_engine` speak, so a small app never declares message
/// enums.
pub struct ValueProtocol;

impl WorkerProtocol for ValueProtocol {
    type Incoming = Value;
    type Outgoing = Value;
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum TouchPhase {
    Started,
    Moved,
    Ended,
    Cancelled,
}

/// The selected entity as reported to the page: display data only. The
/// worker keeps the live selection and hands it to the custom message
/// handler; nothing here resolves back into an entity.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SelectedEntity {
    /// Raw entity index without its generation, for display.
    pub id: u32,
    /// The entity name, empty when unnamed.
    pub name: String,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ToWorker {
    Init {
        width: f32,
        height: f32,
    },
    Resize {
        width: f32,
        height: f32,
    },
    PointerMove {
        x: f32,
        y: f32,
    },
    /// Raw relative motion for first-person look while the pointer is
    /// locked, in physical pixels.
    PointerMotion {
        dx: f32,
        dy: f32,
    },
    PointerButton {
        button: u8,
        pressed: bool,
    },
    Wheel {
        delta: f32,
    },
    Touch {
        id: u64,
        phase: TouchPhase,
        x: f32,
        y: f32,
    },
    Key {
        code: String,
        pressed: bool,
        text: Option<String>,
    },
    Pick {
        x: f32,
        y: f32,
    },
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum FromWorker {
    Ready { adapter: String },
    Stats { fps: f32, entity_count: u32 },
    Selected { detail: Option<SelectedEntity> },
}

#[cfg(test)]
mod tests {
    use super::*;

    fn to_worker_roundtrips(message: ToWorker) {
        let value = serde_json::to_value(&message).expect("serialize");
        let back: ToWorker = serde_json::from_value(value).expect("deserialize");
        assert_eq!(format!("{message:?}"), format!("{back:?}"));
    }

    #[test]
    fn to_worker_survives_the_wire() {
        to_worker_roundtrips(ToWorker::Init {
            width: 800.0,
            height: 600.0,
        });
        to_worker_roundtrips(ToWorker::PointerButton {
            button: 2,
            pressed: true,
        });
        to_worker_roundtrips(ToWorker::PointerMotion { dx: -3.5, dy: 1.0 });
        to_worker_roundtrips(ToWorker::Touch {
            id: 7,
            phase: TouchPhase::Moved,
            x: 1.0,
            y: 2.0,
        });
        to_worker_roundtrips(ToWorker::Key {
            code: "KeyW".to_string(),
            pressed: true,
            text: Some("w".to_string()),
        });
    }

    #[test]
    fn from_worker_survives_the_wire() {
        let message = FromWorker::Selected {
            detail: Some(SelectedEntity {
                id: 3,
                name: "Cube 3".to_string(),
            }),
        };
        let value = serde_json::to_value(&message).expect("serialize");
        let back: FromWorker = serde_json::from_value(value).expect("deserialize");
        assert_eq!(format!("{message:?}"), format!("{back:?}"));
    }
}