Skip to main content

thread_engine/
engine.rs

1//! The one-call embedder facade: [`Loom`].
2//!
3//! [`crate::navigator::Navigator`] is the engine, but it asks you to thread a
4//! [`WorldLoader`] through every call. `Loom` owns the loader for you: construct
5//! it once with your renderer's loader, `open()` a Locator, and pump
6//! `update(dt, player_pos)` each frame. That's the whole browser loop. Everything
7//! a browser needs to *decide* — resolve, arm, veilwalk, focus — is inside; you
8//! supply only how to *draw* (the loader) and where the player is.
9//!
10//! ```no_run
11//! use thread_engine::{Loom, WorldLoader};
12//! use thread_engine::glam::Vec3;
13//! # fn demo<L: WorldLoader>(my_loader: L) {
14//! let mut loom = Loom::new("worlds", my_loader);
15//! loom.open("thread://pixygon.io", Vec3::ZERO);
16//! loop {
17//!     if let Some(spawn) = loom.update(1.0 / 60.0, /* player_pos */ Vec3::ZERO) {
18//!         // teleport the player to `spawn`
19//!     }
20//!     // draw loom.portals(), react to loom.focused(pos), etc.
21//! }
22//! # }
23//! ```
24//!
25//! Use this when your renderer's loader can be *owned* by the engine (an embedded
26//! view, a headless tool, a from-scratch browser). When the loader must borrow
27//! shared renderer state per frame — as Infinite's does, since its `SceneRenderer`
28//! is shared with the rest of the app — drive [`Navigator`] directly instead and
29//! pass a fresh loader each frame.
30
31use std::path::{Path, PathBuf};
32
33use glam::Vec3;
34
35use crate::navigator::{Focus, Navigator, PortalNav, SkyNav, WorldLoader};
36
37/// A ready-to-pump browser core: a [`Navigator`] wired to an owned [`WorldLoader`].
38pub struct Loom<L: WorldLoader> {
39    nav: Navigator,
40    loader: L,
41}
42
43impl<L: WorldLoader> Loom<L> {
44    /// Build a browser core over `loader`. `worlds_root` is the local dev-resolver
45    /// root (where `thread://host/path` falls back to `<root>/<path>/world.json`).
46    pub fn new(worlds_root: impl Into<PathBuf>, loader: L) -> Self {
47        Self {
48            nav: Navigator::new(worlds_root),
49            loader,
50        }
51    }
52
53    /// Open a world by Locator (the entry to the live Thread). The world loads on
54    /// a subsequent [`update`](Self::update) once the async resolve+fetch lands.
55    pub fn open(&mut self, locator: &str, anchor: Vec3) {
56        self.nav.open(locator, anchor);
57    }
58
59    /// Load a local `world.json` directly (the offline / startup path). Returns
60    /// the spawn to place the player at.
61    pub fn load_file(&mut self, path: &Path, anchor: Vec3) -> Option<Vec3> {
62        self.nav.load_file(&mut self.loader, path, anchor)
63    }
64
65    /// Advance one frame: finish an in-flight veilwalk, or begin one if the player
66    /// stepped into a veil. Returns `Some(spawn)` when the player should be moved.
67    pub fn update(&mut self, dt: f32, player_pos: Vec3) -> Option<Vec3> {
68        self.nav.update(dt, &mut self.loader, player_pos)
69    }
70
71    // --- read-through accessors (the browser's HUD/UI reads these) ---
72
73    /// Title of the active world.
74    pub fn title(&self) -> Option<&str> {
75        self.nav.title()
76    }
77    /// The active world's one-line description (may be empty) — arrival cards.
78    pub fn description(&self) -> Option<&str> {
79        self.nav.world_description()
80    }
81    /// The active world's stable id (its presence room key).
82    pub fn world_id(&self) -> &str {
83        self.nav.world_id()
84    }
85    /// The active world's sky/atmosphere.
86    pub fn sky(&self) -> Option<SkyNav> {
87        self.nav.sky()
88    }
89    /// The veils in the active world (render as doorways / minimap / walk toward).
90    pub fn portals(&self) -> &[PortalNav] {
91        self.nav.portals()
92    }
93    /// The active world's presence relays (primary first, then fallbacks).
94    pub fn presence_relays(&self) -> &[String] {
95        self.nav.presence_relays()
96    }
97    /// The inspectable placement the player is standing at, if any.
98    pub fn focused(&self, player_pos: Vec3) -> Option<Focus> {
99        self.nav.focused(player_pos)
100    }
101    /// The veil the player is standing in front of, as `(label, destination)`.
102    pub fn near_portal(&self, player_pos: Vec3) -> Option<(&str, &str)> {
103        self.nav.near_portal(player_pos)
104    }
105    /// Whether a veilwalk is in flight (show a spinner).
106    pub fn is_traveling(&self) -> bool {
107        self.nav.is_traveling()
108    }
109    /// Whether the engine is idle + armed (a veil will fire on contact).
110    pub fn armed(&self) -> bool {
111        self.nav.armed()
112    }
113    /// The most recent veilwalk failure, if any.
114    pub fn last_error(&self) -> Option<&str> {
115        self.nav.last_error()
116    }
117    /// The active world's Locator (the address bar), if it arrived by one.
118    pub fn locator(&self) -> Option<&str> {
119        self.nav.locator()
120    }
121    /// The active world's raw manifest text (view-source).
122    pub fn world_source(&self) -> Option<&str> {
123        self.nav.world_source()
124    }
125
126    // --- behaviors (the sandboxed interactivity layer) ---
127
128    /// The player interacted with whatever they're standing at → the [`Action`]s
129    /// (see [`crate::behavior`]) its bound behavior module asks the browser to
130    /// perform. Empty when nothing is bound (fall back to Codex/web focus).
131    pub fn interact(&mut self, player_pos: Vec3) -> Vec<crate::behavior::Action> {
132        self.nav.interact(player_pos)
133    }
134    /// Advance the active world's ticking behavior modules — the heartbeat.
135    /// Returns the Actions ticking Weft behaviors request.
136    pub fn tick_behaviors(&mut self, dt: f32) -> Vec<crate::behavior::Action> {
137        self.nav.tick_behaviors(dt)
138    }
139
140    // --- session history (the web's back/forward/home, engine-level) ---
141
142    /// Veilwalk back to the previous world. Returns whether a walk began.
143    pub fn back(&mut self, anchor: Vec3) -> bool {
144        self.nav.back(anchor)
145    }
146    /// Veilwalk forward again after `back()`.
147    pub fn forward(&mut self, anchor: Vec3) -> bool {
148        self.nav.forward(anchor)
149    }
150    /// Veilwalk to the session's home world (by default, where it opened).
151    pub fn go_home(&mut self, anchor: Vec3) -> bool {
152        self.nav.go_home(anchor)
153    }
154
155    /// Borrow the loader (e.g. to read what it recorded) …
156    pub fn loader(&self) -> &L {
157        &self.loader
158    }
159    /// … or the navigator, for the full read API.
160    pub fn navigator(&self) -> &Navigator {
161        &self.nav
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use crate::navigator::LoadedWorldMeta;
169    use infinite_manifest::WorldManifest;
170
171    struct CountLoader(u32);
172    impl WorldLoader for CountLoader {
173        fn load(
174            &mut self,
175            m: &WorldManifest,
176            _assets: &crate::assets::AssetSource,
177            anchor: Vec3,
178        ) -> LoadedWorldMeta {
179            self.0 += 1;
180            LoadedWorldMeta {
181                title: m.world.title.clone(),
182                world_id: m.world.id.clone(),
183                spawn: anchor,
184                portals: vec![],
185                interactables: vec![],
186                sky: SkyNav::default(),
187                presence_relays: vec![],
188                colliders: vec![],
189            }
190        }
191    }
192
193    #[test]
194    fn facade_loads_a_local_world_through_the_owned_loader() {
195        // Author a tiny world on disk in a temp dir, then open it via the facade.
196        let dir = std::env::temp_dir().join("loom_facade_test");
197        let _ = std::fs::create_dir_all(&dir);
198        let file = dir.join("world.json");
199        std::fs::write(
200            &file,
201            r#"{ "thread": "thread/0.1", "world": { "id": "w", "title": "Facade World" },
202                "spawns": [{ "name": "entry", "position": [0, 0, 2] }] }"#,
203        )
204        .unwrap();
205
206        let mut loom = Loom::new(&dir, CountLoader(0));
207        let spawn = loom.load_file(&file, Vec3::new(1.0, 0.0, 1.0));
208        // Arrivals land STANDING: floor anchor + the capsule's feet drop.
209        assert_eq!(
210            spawn,
211            Some(Vec3::new(1.0, crate::navigator::FEET_DROP, 1.0))
212        );
213        assert_eq!(loom.title(), Some("Facade World"));
214        assert_eq!(loom.loader().0, 1, "the facade drove the owned loader");
215    }
216}