Shared playback model
The shared map core now provides Rust playback computation, reused by desktop camera, route and line animation. The common route playback facade is connected for browsers, desktop scripts and independent native libraries; scenePlayback/sceneRoutePlayback are true while sceneLinePlayback/sceneTracks are true; see line playback. Existing common lifecycle, static resources and time are documented in Scene clock.
Rust usage
The complete example below computes positions only. Coordinates use the caller's chosen plane; no CRS conversion is implied. For actual rendering, download the complete native rendering example and run it in the map-core repository:
cargo run --locked -p sqs-map --example shared_playback_model -- /tmp/playback-frames
It uses a real MapSession and saves nine frames, checking positions, pause, completion, hiding and output failure. It is a caller-driven Rust model example, not an unpublished C/JavaScript playback facade.
use sqs_map::playback::{Completion, Playback, RouteTrack};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let route = RouteTrack::new(vec![[0., 0.], [10., 20.]], vec![0., 1000.])?;
let mut current = Playback::new(route.duration_ms(), false, 0.)?;
current.play()?;
let round = current.epoch();
let mut candidate = current.clone();
candidate.advance(500., true)?;
let frame = route.sample(candidate.position_ms())?;
assert_eq!(frame.position, [5., 10.]);
// Accept the host's target frame first, then publish; discard on failure.
current = candidate;
current.pause();
assert!(!current.advance(1000., true)?);
assert_eq!(current.completion(round), Completion::Pending);
current.play()?;
current.advance(500., true)?;
assert_eq!(current.completion(round), Completion::Completed);
Ok(())
}
Playback and resource rules
Time uses milliseconds and host-injected monotonic intervals. Pause freezes time; resume retains position; a new round cancels completion waits captured for the old round. Loops never complete naturally. The host must settle waits on stop, resource failure or removal. Owner stop preserves an already completed finite round; explicit playback stop cancels it. Only explicit waits retain requests. The host manages retained data and models after the script exits.
Routes accept 2–100000 points and strictly increasing offsets starting at zero, with a maximum duration of one day. Sampling interpolates in time, preserves dwell points and smooths heading along the shortest angular path. RouteTrack points use a host plane, not implicit longitude/latitude; desktop projection remains host-owned.
LineTiming accepts durationMs, delayMs, loop, direction, easing and fill through serde. Directions are forward/reverse/alternate, easing is linear/ease-in-out, and fill is forwards/none. Tracks accepts color/opacity/widthScale curves, at most 32 keys each, strictly increasing offsets from 0 to 1, #RRGGBB or #RRGGBBAA colors, opacity from 0 to 1 and width scale from 0 to 8. Deserialization validates immutable prepared values.
Script syntax and platform scope
Public camera code continues to prefer map.easeTo and map.flyTo, informed by duration/easing semantics in MapLibre AnimationOptions. Scene-owned routes, line animation and completion waits are SqsMap extensions; do not assume MapLibre offers the same facade.
Desktop compatibility APIs map.scenes, map.playbacks and map.animations retain their parameters and permissions; their calculations now delegate to the shared core. Copying them into browsers or independent libraries does not make them available there. The common route API now connects source identity, data revisions, atomic display commits and completion waits. Line animation and tracks continue in later phases.
Current Linux acceptance includes 10 model checks, an executable Rust documentation example, seven outcomes through the actual desktop wait adapter and QuickJS, and nine real software-Vulkan frames. External Windows/macOS, mobile devices, hardware backends, full GUI and formal binary releases require separate acceptance.