mirror of
https://github.com/penpot/penpot.git
synced 2026-09-18 18:06:14 +00:00
1097 lines
30 KiB
Rust
1097 lines
30 KiB
Rust
#[cfg(target_arch = "wasm32")]
|
||
mod emscripten;
|
||
mod error;
|
||
mod math;
|
||
mod mem;
|
||
mod performance;
|
||
mod render;
|
||
mod shapes;
|
||
mod state;
|
||
mod tiles;
|
||
mod utils;
|
||
mod uuid;
|
||
mod view;
|
||
mod wapi;
|
||
mod wasm;
|
||
|
||
use std::collections::HashMap;
|
||
|
||
#[allow(unused_imports)]
|
||
use crate::error::{Error, Result};
|
||
use crate::state::TextEditorState;
|
||
use macros::wasm_error;
|
||
use math::{Bounds, Matrix};
|
||
use mem::SerializableResult;
|
||
use shapes::{StructureEntry, StructureEntryType, TransformEntry};
|
||
use skia_safe as skia;
|
||
use state::State;
|
||
use utils::uuid_from_u32_quartet;
|
||
use uuid::Uuid;
|
||
|
||
pub(crate) static mut STATE: Option<Box<State>> = None;
|
||
pub(crate) static mut TEXT_EDITOR_STATE: *mut TextEditorState = std::ptr::null_mut();
|
||
|
||
#[inline(always)]
|
||
pub fn get_text_editor_state() -> &'static mut TextEditorState {
|
||
unsafe {
|
||
debug_assert!(!TEXT_EDITOR_STATE.is_null(), "Text Editor state is null");
|
||
&mut *TEXT_EDITOR_STATE
|
||
}
|
||
}
|
||
|
||
// FIXME: These with_state* macros should be using our CriticalError instead of expect.
|
||
// But to do that, we need to not use them at domain-level (i.e. in business logic), just
|
||
// in the context of the wasm call.
|
||
#[macro_export]
|
||
macro_rules! with_state_mut {
|
||
($state:ident, $block:block) => {{
|
||
let $state = unsafe {
|
||
#[allow(static_mut_refs)]
|
||
STATE.as_mut()
|
||
}
|
||
.expect("Got an invalid state pointer");
|
||
$block
|
||
}};
|
||
}
|
||
|
||
#[macro_export]
|
||
macro_rules! with_state {
|
||
($state:ident, $block:block) => {{
|
||
let $state = unsafe {
|
||
#[allow(static_mut_refs)]
|
||
STATE.as_ref()
|
||
}
|
||
.expect("Got an invalid state pointer");
|
||
$block
|
||
}};
|
||
}
|
||
|
||
#[macro_export]
|
||
macro_rules! with_current_shape_mut {
|
||
($state:ident, |$shape:ident: &mut Shape| $block:block) => {
|
||
let $state = unsafe {
|
||
#[allow(static_mut_refs)]
|
||
STATE.as_mut()
|
||
}
|
||
.expect("Got an invalid state pointer");
|
||
|
||
$state.touch_current();
|
||
|
||
if let Some($shape) = $state.current_shape_mut() {
|
||
$block
|
||
}
|
||
};
|
||
}
|
||
|
||
#[macro_export]
|
||
macro_rules! with_current_shape {
|
||
($state:ident, |$shape:ident: &Shape| $block:block) => {
|
||
let $state = unsafe {
|
||
#[allow(static_mut_refs)]
|
||
STATE.as_ref()
|
||
}
|
||
.expect("Got an invalid state pointer");
|
||
if let Some($shape) = $state.current_shape() {
|
||
$block
|
||
}
|
||
};
|
||
}
|
||
|
||
#[macro_export]
|
||
macro_rules! with_state_mut_current_shape {
|
||
($state:ident, |$shape:ident: &Shape| $block:block) => {
|
||
let $state = unsafe {
|
||
#[allow(static_mut_refs)]
|
||
STATE.as_mut()
|
||
}
|
||
.expect("Got an invalid state pointer");
|
||
if let Some($shape) = $state.current_shape() {
|
||
$block
|
||
}
|
||
};
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn init(width: i32, height: i32) -> Result<()> {
|
||
let state_box = Box::new(State::try_new(width, height)?);
|
||
unsafe {
|
||
STATE = Some(state_box);
|
||
TEXT_EDITOR_STATE = Box::into_raw(Box::new(TextEditorState::new()));
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_browser(browser: u8) -> Result<()> {
|
||
with_state_mut!(state, {
|
||
state.set_browser(browser);
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn clean_up() -> Result<()> {
|
||
with_state_mut!(state, {
|
||
// Cancel the current animation frame if it exists so
|
||
// it won't try to render without context
|
||
let render_state = state.render_state_mut();
|
||
render_state.cancel_animation_frame();
|
||
});
|
||
unsafe { STATE = None }
|
||
mem::free_bytes()?;
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_render_options(debug: u32, dpr: f32) -> Result<()> {
|
||
with_state_mut!(state, {
|
||
let render_state = state.render_state_mut();
|
||
render_state.set_debug_flags(debug);
|
||
render_state.set_dpr(dpr)?;
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_viewport_interest_area_threshold(
|
||
viewport_interest_area_threshold: i32,
|
||
) -> Result<()> {
|
||
with_state_mut!(state, {
|
||
let render_state = state.render_state_mut();
|
||
render_state.set_viewport_interest_area_threshold(viewport_interest_area_threshold);
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_max_blocking_time_ms(max_blocking_time_ms: i32) -> Result<()> {
|
||
with_state_mut!(state, {
|
||
let render_state = state.render_state_mut();
|
||
render_state.set_max_blocking_time_ms(max_blocking_time_ms);
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_node_batch_threshold(node_batch_threshold: i32) -> Result<()> {
|
||
with_state_mut!(state, {
|
||
let render_state = state.render_state_mut();
|
||
render_state.set_node_batch_threshold(node_batch_threshold);
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_blur_downscale_threshold(blur_downscale_threshold: f32) -> Result<()> {
|
||
with_state_mut!(state, {
|
||
let render_state = state.render_state_mut();
|
||
render_state.set_blur_downscale_threshold(blur_downscale_threshold);
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_antialias_threshold(threshold: f32) -> Result<()> {
|
||
with_state_mut!(state, {
|
||
state.render_state_mut().set_antialias_threshold(threshold);
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_max_atlas_texture_size(max_px: i32) -> Result<()> {
|
||
with_state_mut!(state, {
|
||
state
|
||
.render_state_mut()
|
||
.surfaces
|
||
.set_max_atlas_texture_size(max_px);
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_canvas_background(raw_color: u32) -> Result<()> {
|
||
with_state_mut!(state, {
|
||
let color = skia::Color::new(raw_color);
|
||
state.set_background_color(color);
|
||
state.rebuild_tiles_shallow();
|
||
});
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn render(timestamp: i32) -> Result<()> {
|
||
with_state_mut!(state, {
|
||
state.rebuild_touched_tiles();
|
||
// Drain the throttled modifier-tile invalidation accumulated
|
||
// since the previous rAF. set_modifiers skips this work during
|
||
// interactive_transform; we do it once here, with the current
|
||
// modifier set, so the cost is paid once per rAF rather than
|
||
// once per pointer move.
|
||
if state.render_state.options.is_interactive_transform() {
|
||
let ids = state.shapes.modifier_ids();
|
||
if !ids.is_empty() {
|
||
state.rebuild_modifier_tiles(ids)?;
|
||
}
|
||
}
|
||
state
|
||
.start_render_loop(timestamp)
|
||
.map_err(|_| Error::RecoverableError("Error rendering".to_string()))?;
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn render_sync() -> Result<()> {
|
||
with_state_mut!(state, {
|
||
state.rebuild_tiles();
|
||
state
|
||
.render_sync(0)
|
||
.map_err(|_| Error::RecoverableError("Error rendering".to_string()))?;
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn render_sync_shape(a: u32, b: u32, c: u32, d: u32) -> Result<()> {
|
||
with_state_mut!(state, {
|
||
let id = uuid_from_u32_quartet(a, b, c, d);
|
||
state.use_shape(id);
|
||
|
||
// look for an existing root shape, and create it if missing
|
||
let mut was_root_missing = false;
|
||
if !state.shapes.has(&Uuid::nil()) {
|
||
state.shapes.add_shape(Uuid::nil());
|
||
was_root_missing = true;
|
||
}
|
||
|
||
if was_root_missing {
|
||
state.set_parent_for_current_shape(Uuid::nil());
|
||
}
|
||
|
||
state.rebuild_tiles_from(Some(&id));
|
||
state
|
||
.render_sync_shape(&id, 0)
|
||
.map_err(|e| Error::RecoverableError(e.to_string()))?;
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn render_from_cache(_: i32) -> Result<()> {
|
||
with_state_mut!(state, {
|
||
// Don't cancel the animation frame — let the async render
|
||
// continue populating the tile HashMap in the background.
|
||
// process_animation_frame skips flush_and_submit in fast
|
||
// mode so it won't present stale Target content. The
|
||
// tile HashMap is position-independent, so tiles rendered
|
||
// for the old viewport can be reused by the next full
|
||
// render at the new viewport position.
|
||
state.render_from_cache();
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_preview_mode(enabled: bool) -> Result<()> {
|
||
with_state_mut!(state, {
|
||
state.render_state.set_preview_mode(enabled);
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn render_preview() -> Result<()> {
|
||
with_state_mut!(state, {
|
||
state.render_preview(performance::get_time());
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
/// Enter bulk-loading mode. While active, `state.loading` is `true`.
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn begin_loading() -> Result<()> {
|
||
with_state_mut!(state, {
|
||
state.loading = true;
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
/// Leave bulk-loading mode. Should be called after the first
|
||
/// render so the loading flag is available during that render.
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn end_loading() -> Result<()> {
|
||
with_state_mut!(state, {
|
||
state.loading = false;
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
/// Draw a full-screen loading overlay (background + "Loading…" text).
|
||
/// Called from CLJS right after begin_loading so the user sees
|
||
/// immediate feedback while shapes are being processed.
|
||
/// NOTE:
|
||
/// This is currently not being used, but it's set there for testing purposes on
|
||
/// upcoming tasks
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn render_loading_overlay() -> Result<()> {
|
||
with_state_mut!(state, {
|
||
state.render_state.render_loading_overlay();
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn process_animation_frame(timestamp: i32) -> Result<()> {
|
||
let result = with_state_mut!(state, { state.process_animation_frame(timestamp) });
|
||
if let Err(err) = result {
|
||
eprintln!("process_animation_frame error: {}", err);
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn reset_canvas() -> Result<()> {
|
||
with_state_mut!(state, {
|
||
state.render_state_mut().reset_canvas();
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn resize_viewbox(width: i32, height: i32) -> Result<()> {
|
||
with_state_mut!(state, {
|
||
state.resize(width, height)?;
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_view(zoom: f32, x: f32, y: f32) -> Result<()> {
|
||
with_state_mut!(state, {
|
||
performance::begin_measure!("set_view");
|
||
let render_state = state.render_state_mut();
|
||
render_state.set_view(zoom, x, y);
|
||
performance::end_measure!("set_view");
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[cfg(feature = "profile-macros")]
|
||
static mut VIEW_INTERACTION_START: i32 = 0;
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_view_start() -> Result<()> {
|
||
with_state_mut!(state, {
|
||
#[cfg(feature = "profile-macros")]
|
||
unsafe {
|
||
VIEW_INTERACTION_START = performance::get_time();
|
||
}
|
||
performance::begin_measure!("set_view_start");
|
||
state.render_state.options.set_fast_mode(true);
|
||
// If a previous two-pass rebuild was mid-flight, discard its
|
||
// intent — the new gesture supersedes it.
|
||
state.render_state.options.set_defer_effects(false);
|
||
performance::end_measure!("set_view_start");
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
/// Finishes a view interaction (zoom or pan). Rebuilds the tile index
|
||
/// and, for zoom changes, invalidates the tile texture cache so the
|
||
/// subsequent render re-draws tiles at full quality.
|
||
/// For pure pan (same zoom), cached tiles are preserved so only
|
||
/// newly-visible tiles need rendering.
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_view_end() -> Result<()> {
|
||
with_state_mut!(state, {
|
||
performance::begin_measure!("set_view_end");
|
||
state.render_state.options.set_fast_mode(false);
|
||
state.render_state.cancel_animation_frame();
|
||
|
||
let scale = state.render_state.get_scale();
|
||
state
|
||
.render_state
|
||
.tile_viewbox
|
||
.update(state.render_state.viewbox, scale);
|
||
|
||
if state.render_state.options.is_profile_rebuild_tiles() {
|
||
state.rebuild_tiles();
|
||
} else if state.render_state.zoom_changed() {
|
||
// Zoom changed: tile sizes differ so all cached tile
|
||
// textures are invalid (wrong scale). Rebuild the tile
|
||
// index and clear the tile texture cache, but *preserve*
|
||
// the cache canvas so render_from_cache can show a scaled
|
||
// preview of the old content while new tiles render.
|
||
state.render_state.rebuild_tile_index(&state.shapes);
|
||
state.render_state.surfaces.invalidate_tile_cache();
|
||
// Start the progressive two-pass rebuild. Pass 1 renders
|
||
// tiles without blur/shadow for fast feedback; when it
|
||
// completes, process_animation_frame flips this off and
|
||
// kicks pass 2 which adds the effects back in place.
|
||
state.render_state.options.set_defer_effects(true);
|
||
} else {
|
||
// Pure pan at the same zoom level: tile contents have not
|
||
// changed — only the viewport position moved. Update the
|
||
// tile index (which tiles are in the interest area) but
|
||
// keep cached tile textures so the render can blit them
|
||
// instead of re-drawing every visible tile from scratch.
|
||
state.render_state.rebuild_tile_index(&state.shapes);
|
||
}
|
||
performance::end_measure!("set_view_end");
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
/// Enter interactive transform mode (drag / resize / rotate of a
|
||
/// shape). Activates the same expensive-effect skipping as pan/zoom
|
||
/// (`fast_mode`) but keeps per-frame flushing enabled so the Target is
|
||
/// presented every rAF, and triggers atlas-backed backdrops so
|
||
/// invalidated tiles do not appear sequentially or flicker.
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_modifiers_start() -> Result<()> {
|
||
with_state_mut!(state, {
|
||
performance::begin_measure!("set_modifiers_start");
|
||
state.render_state.options.set_fast_mode(true);
|
||
state.render_state.options.set_interactive_transform(true);
|
||
// Force the next interactive-rAF to re-seed Target by blitting
|
||
// the persistent atlas as a backdrop. After that, Target is
|
||
// kept coherent tile-by-tile by the render walker for the rest
|
||
// of the gesture, so subsequent rAFs skip the full-viewport
|
||
// blit.
|
||
state.render_state.interactive_backdrop_drawn = false;
|
||
// Keep the default interest area during drag. Reducing it to 1
|
||
// caused ghost-shape artifacts: tiles invalidated mid-drag that
|
||
// moved outside the smaller interest area never re-rendered, so
|
||
// the atlas kept stale content. Default (3) keeps the queue
|
||
// larger but ensures everything that could be invalidated also
|
||
// gets re-rendered.
|
||
performance::end_measure!("set_modifiers_start");
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
/// Leave interactive transform mode and cancel any pending async
|
||
/// render scheduled under it. The caller is responsible for triggering
|
||
/// a final full-quality render (typically via `_render`) once the
|
||
/// modifiers have been committed.
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_modifiers_end() -> Result<()> {
|
||
with_state_mut!(state, {
|
||
performance::begin_measure!("set_modifiers_end");
|
||
state.render_state.options.set_fast_mode(false);
|
||
state.render_state.options.set_interactive_transform(false);
|
||
state.render_state.cancel_animation_frame();
|
||
performance::end_measure!("set_modifiers_end");
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn clear_focus_mode() -> Result<()> {
|
||
with_state_mut!(state, {
|
||
state.clear_focus_mode();
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_focus_mode() -> Result<()> {
|
||
let bytes = mem::bytes();
|
||
|
||
let entries: Vec<Uuid> = bytes
|
||
.chunks(size_of::<<Uuid as SerializableResult>::BytesType>())
|
||
.map(|data| Uuid::try_from(data).map_err(|e| Error::RecoverableError(e.to_string())))
|
||
.collect::<Result<Vec<Uuid>>>()?;
|
||
|
||
with_state_mut!(state, {
|
||
state.set_focus_mode(entries);
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn init_shapes_pool(capacity: usize) -> Result<()> {
|
||
with_state_mut!(state, {
|
||
state.init_shapes_pool(capacity);
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn use_shape(a: u32, b: u32, c: u32, d: u32) -> Result<()> {
|
||
with_state_mut!(state, {
|
||
let id = uuid_from_u32_quartet(a, b, c, d);
|
||
state.use_shape(id);
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn touch_shape(a: u32, b: u32, c: u32, d: u32) -> Result<()> {
|
||
with_state_mut!(state, {
|
||
let shape_id = uuid_from_u32_quartet(a, b, c, d);
|
||
state.touch_shape(shape_id);
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_parent(a: u32, b: u32, c: u32, d: u32) -> Result<()> {
|
||
with_state_mut!(state, {
|
||
let id = uuid_from_u32_quartet(a, b, c, d);
|
||
state.set_parent_for_current_shape(id);
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_shape_masked_group(masked: bool) -> Result<()> {
|
||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||
shape.set_masked(masked);
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_shape_selrect(left: f32, top: f32, right: f32, bottom: f32) -> Result<()> {
|
||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||
shape.set_selrect(left, top, right, bottom);
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_shape_clip_content(clip_content: bool) -> Result<()> {
|
||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||
shape.set_clip(clip_content);
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_shape_rotation(rotation: f32) -> Result<()> {
|
||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||
shape.set_rotation(rotation);
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_shape_transform(
|
||
a: f32,
|
||
b: f32,
|
||
c: f32,
|
||
d: f32,
|
||
e: f32,
|
||
f: f32,
|
||
) -> Result<()> {
|
||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||
shape.set_transform(a, b, c, d, e, f);
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn add_shape_child(a: u32, b: u32, c: u32, d: u32) -> Result<()> {
|
||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||
let id = uuid_from_u32_quartet(a, b, c, d);
|
||
shape.add_child(id);
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
fn set_children_set(entries: Vec<Uuid>) -> Result<()> {
|
||
let mut deleted = Vec::new();
|
||
let mut parent_id = None;
|
||
|
||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||
parent_id = Some(shape.id);
|
||
(_, deleted) = shape.compute_children_differences(&entries);
|
||
shape.children = entries.clone();
|
||
|
||
for id in entries {
|
||
state.touch_shape(id);
|
||
if let Some(children_shape) = state.shapes.get_mut(&id) {
|
||
children_shape.set_deleted(false);
|
||
}
|
||
}
|
||
});
|
||
|
||
with_state_mut!(state, {
|
||
let Some(parent_id) = parent_id else {
|
||
return Err(Error::RecoverableError(
|
||
"set_children_set: Parent ID not found".to_string(),
|
||
));
|
||
};
|
||
|
||
for id in deleted {
|
||
state.delete_shape_children(parent_id, id);
|
||
state.touch_shape(id);
|
||
}
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_children_0() -> Result<()> {
|
||
let entries = vec![];
|
||
set_children_set(entries)?;
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_children_1(a1: u32, b1: u32, c1: u32, d1: u32) -> Result<()> {
|
||
let entries = vec![uuid_from_u32_quartet(a1, b1, c1, d1)];
|
||
set_children_set(entries)?;
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_children_2(
|
||
a1: u32,
|
||
b1: u32,
|
||
c1: u32,
|
||
d1: u32,
|
||
a2: u32,
|
||
b2: u32,
|
||
c2: u32,
|
||
d2: u32,
|
||
) -> Result<()> {
|
||
let entries = vec![
|
||
uuid_from_u32_quartet(a1, b1, c1, d1),
|
||
uuid_from_u32_quartet(a2, b2, c2, d2),
|
||
];
|
||
set_children_set(entries)?;
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_children_3(
|
||
a1: u32,
|
||
b1: u32,
|
||
c1: u32,
|
||
d1: u32,
|
||
a2: u32,
|
||
b2: u32,
|
||
c2: u32,
|
||
d2: u32,
|
||
a3: u32,
|
||
b3: u32,
|
||
c3: u32,
|
||
d3: u32,
|
||
) -> Result<()> {
|
||
let entries = vec![
|
||
uuid_from_u32_quartet(a1, b1, c1, d1),
|
||
uuid_from_u32_quartet(a2, b2, c2, d2),
|
||
uuid_from_u32_quartet(a3, b3, c3, d3),
|
||
];
|
||
set_children_set(entries)?;
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_children_4(
|
||
a1: u32,
|
||
b1: u32,
|
||
c1: u32,
|
||
d1: u32,
|
||
a2: u32,
|
||
b2: u32,
|
||
c2: u32,
|
||
d2: u32,
|
||
a3: u32,
|
||
b3: u32,
|
||
c3: u32,
|
||
d3: u32,
|
||
a4: u32,
|
||
b4: u32,
|
||
c4: u32,
|
||
d4: u32,
|
||
) -> Result<()> {
|
||
let entries = vec![
|
||
uuid_from_u32_quartet(a1, b1, c1, d1),
|
||
uuid_from_u32_quartet(a2, b2, c2, d2),
|
||
uuid_from_u32_quartet(a3, b3, c3, d3),
|
||
uuid_from_u32_quartet(a4, b4, c4, d4),
|
||
];
|
||
set_children_set(entries)?;
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_children_5(
|
||
a1: u32,
|
||
b1: u32,
|
||
c1: u32,
|
||
d1: u32,
|
||
a2: u32,
|
||
b2: u32,
|
||
c2: u32,
|
||
d2: u32,
|
||
a3: u32,
|
||
b3: u32,
|
||
c3: u32,
|
||
d3: u32,
|
||
a4: u32,
|
||
b4: u32,
|
||
c4: u32,
|
||
d4: u32,
|
||
a5: u32,
|
||
b5: u32,
|
||
c5: u32,
|
||
d5: u32,
|
||
) -> Result<()> {
|
||
let entries = vec![
|
||
uuid_from_u32_quartet(a1, b1, c1, d1),
|
||
uuid_from_u32_quartet(a2, b2, c2, d2),
|
||
uuid_from_u32_quartet(a3, b3, c3, d3),
|
||
uuid_from_u32_quartet(a4, b4, c4, d4),
|
||
uuid_from_u32_quartet(a5, b5, c5, d5),
|
||
];
|
||
set_children_set(entries)?;
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_children() -> Result<()> {
|
||
let bytes = mem::bytes_or_empty();
|
||
|
||
let entries: Vec<Uuid> = bytes
|
||
.chunks(size_of::<<Uuid as SerializableResult>::BytesType>())
|
||
.map(|data| Uuid::try_from(data).map_err(|e| Error::CriticalError(e.to_string())))
|
||
.collect::<Result<Vec<Uuid>>>()?;
|
||
|
||
set_children_set(entries)?;
|
||
|
||
if !bytes.is_empty() {
|
||
mem::free_bytes()?;
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn is_image_cached(
|
||
a: u32,
|
||
b: u32,
|
||
c: u32,
|
||
d: u32,
|
||
is_thumbnail: bool,
|
||
) -> Result<bool> {
|
||
with_state_mut!(state, {
|
||
let id = uuid_from_u32_quartet(a, b, c, d);
|
||
let result = state.render_state().has_image(&id, is_thumbnail);
|
||
Ok(result)
|
||
})
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_shape_svg_raw_content() -> Result<()> {
|
||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||
let bytes = mem::bytes();
|
||
let svg_raw_content = String::from_utf8(bytes)
|
||
.map_err(|e| Error::RecoverableError(e.to_string()))?
|
||
.trim_end_matches('\0')
|
||
.to_string();
|
||
shape.set_svg_raw_content(svg_raw_content);
|
||
});
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_shape_opacity(opacity: f32) -> Result<()> {
|
||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||
shape.set_opacity(opacity);
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_shape_hidden(hidden: bool) -> Result<()> {
|
||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||
shape.set_hidden(hidden);
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_shape_corners(r1: f32, r2: f32, r3: f32, r4: f32) -> Result<()> {
|
||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||
shape.set_corners((r1, r2, r3, r4));
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn get_selection_rect() -> Result<*mut u8> {
|
||
let bytes = mem::bytes();
|
||
|
||
let entries: Vec<Uuid> = bytes
|
||
.chunks(16)
|
||
.map(|bytes| {
|
||
uuid_from_u32_quartet(
|
||
u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
|
||
u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
|
||
u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]),
|
||
u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]),
|
||
)
|
||
})
|
||
.collect();
|
||
|
||
let result_bound = with_state_mut!(state, {
|
||
let bbs: Vec<_> = entries
|
||
.iter()
|
||
.flat_map(|id| state.shapes.get(id).map(|b| b.bounds()))
|
||
.collect();
|
||
|
||
if bbs.len() == 1 {
|
||
bbs[0]
|
||
} else {
|
||
Bounds::join_bounds(&bbs)
|
||
}
|
||
});
|
||
|
||
let width = result_bound.width();
|
||
let height = result_bound.height();
|
||
let center = result_bound.center();
|
||
let transform = result_bound.transform_matrix().unwrap_or(Matrix::default());
|
||
|
||
let mut bytes = vec![0; 40];
|
||
bytes[0..4].clone_from_slice(&width.to_le_bytes());
|
||
bytes[4..8].clone_from_slice(&height.to_le_bytes());
|
||
bytes[8..12].clone_from_slice(¢er.x.to_le_bytes());
|
||
bytes[12..16].clone_from_slice(¢er.y.to_le_bytes());
|
||
bytes[16..20].clone_from_slice(&transform[0].to_le_bytes());
|
||
bytes[20..24].clone_from_slice(&transform[3].to_le_bytes());
|
||
bytes[24..28].clone_from_slice(&transform[1].to_le_bytes());
|
||
bytes[28..32].clone_from_slice(&transform[4].to_le_bytes());
|
||
bytes[32..36].clone_from_slice(&transform[2].to_le_bytes());
|
||
bytes[36..40].clone_from_slice(&transform[5].to_le_bytes());
|
||
Ok(mem::write_bytes(bytes))
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_structure_modifiers() -> Result<()> {
|
||
let bytes = mem::bytes();
|
||
|
||
let entries: Vec<StructureEntry> = bytes
|
||
.chunks(44)
|
||
.map(|chunk| {
|
||
let data = chunk
|
||
.try_into()
|
||
.map_err(|_| Error::CriticalError("Invalid StructureEntry bytes".to_string()))?;
|
||
Ok(StructureEntry::from_bytes(data))
|
||
})
|
||
.collect::<Result<Vec<_>>>()?;
|
||
|
||
with_state_mut!(state, {
|
||
let mut structure = HashMap::new();
|
||
let mut scale_content = HashMap::new();
|
||
for entry in entries {
|
||
match entry.entry_type {
|
||
StructureEntryType::ScaleContent => {
|
||
let Some(shape) = state.shapes.get(&entry.id) else {
|
||
continue;
|
||
};
|
||
for id in shape.all_children(&state.shapes, true, true) {
|
||
scale_content.insert(id, entry.value);
|
||
}
|
||
}
|
||
_ => {
|
||
structure.entry(entry.parent).or_insert_with(Vec::new);
|
||
structure
|
||
.get_mut(&entry.parent)
|
||
.ok_or(Error::CriticalError(
|
||
"Parent not found for entry".to_string(),
|
||
))?
|
||
.push(entry);
|
||
}
|
||
}
|
||
}
|
||
if !scale_content.is_empty() {
|
||
state.shapes.set_scale_content(scale_content);
|
||
}
|
||
if !structure.is_empty() {
|
||
state.shapes.set_structure(structure);
|
||
}
|
||
});
|
||
|
||
mem::free_bytes()?;
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn clean_modifiers() -> Result<()> {
|
||
with_state_mut!(state, {
|
||
let prev_modifier_ids = state.shapes.clean_all();
|
||
// Skip the tile-cache cleanup during interactive transform: the
|
||
// per-rAF `rebuild_modifier_tiles` in `render()` already evicts
|
||
// the same tiles for the active modifier set, so the eviction
|
||
// here is redundant and doubles the per-emission cost.
|
||
if !prev_modifier_ids.is_empty() && !state.render_state.options.is_interactive_transform() {
|
||
state
|
||
.render_state
|
||
.update_tiles_shapes(&prev_modifier_ids, &mut state.shapes)?;
|
||
}
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn set_modifiers() -> Result<()> {
|
||
let bytes = mem::bytes();
|
||
|
||
let entries: Vec<TransformEntry> = bytes
|
||
.chunks(size_of::<<TransformEntry as SerializableResult>::BytesType>())
|
||
.map(|data| TransformEntry::try_from(data).map_err(|e| Error::CriticalError(e.to_string())))
|
||
.collect::<Result<Vec<_>>>()?;
|
||
|
||
let mut modifiers = HashMap::new();
|
||
let mut ids = Vec::<Uuid>::new();
|
||
for entry in entries {
|
||
modifiers.insert(entry.id, entry.transform);
|
||
ids.push(entry.id);
|
||
}
|
||
|
||
with_state_mut!(state, {
|
||
state.set_modifiers(modifiers);
|
||
// Throttle: skip per-pointer-move tile invalidation. The render
|
||
// entry (`render`) drains the current modifier set once per rAF
|
||
// and calls rebuild_modifier_tiles then. With ~3 pointer moves
|
||
// per rAF, this cuts tile invalidations by 3× and removes the
|
||
// PAF backlog.
|
||
if !state.render_state.options.is_interactive_transform() {
|
||
state.rebuild_modifier_tiles(ids)?;
|
||
}
|
||
});
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn start_temp_objects() -> Result<()> {
|
||
unsafe {
|
||
#[allow(static_mut_refs)]
|
||
let mut state = STATE.take().ok_or(Error::CriticalError(
|
||
"Got an invalid state pointer".to_string(),
|
||
))?;
|
||
state = Box::new(state.start_temp_objects()?);
|
||
STATE = Some(state);
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn end_temp_objects() -> Result<()> {
|
||
unsafe {
|
||
#[allow(static_mut_refs)]
|
||
let mut state = STATE.take().ok_or(Error::CriticalError(
|
||
"Got an invalid state pointer".to_string(),
|
||
))?;
|
||
state = Box::new(state.end_temp_objects()?);
|
||
STATE = Some(state);
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
#[no_mangle]
|
||
#[wasm_error]
|
||
pub extern "C" fn render_shape_pixels(
|
||
a: u32,
|
||
b: u32,
|
||
c: u32,
|
||
d: u32,
|
||
scale: f32,
|
||
) -> Result<*mut u8> {
|
||
let id = uuid_from_u32_quartet(a, b, c, d);
|
||
|
||
if !scale.is_finite() {
|
||
return Err(Error::CriticalError("Scale is not finite".to_string()));
|
||
}
|
||
|
||
with_state_mut!(state, {
|
||
let (data, width, height) =
|
||
state.render_shape_pixels(&id, scale, performance::get_time())?;
|
||
|
||
let len = data.len() as u32;
|
||
let mut buf = Vec::with_capacity(4 + data.len());
|
||
buf.extend_from_slice(&len.to_le_bytes());
|
||
buf.extend_from_slice(&width.to_le_bytes());
|
||
buf.extend_from_slice(&height.to_le_bytes());
|
||
buf.extend_from_slice(&data);
|
||
Ok(mem::write_bytes(buf))
|
||
})
|
||
}
|
||
|
||
#[no_mangle]
|
||
pub extern "C" fn render_stats() {
|
||
with_state!(state, {
|
||
state.render_state.print_stats();
|
||
})
|
||
}
|
||
|
||
fn main() {
|
||
#[cfg(target_arch = "wasm32")]
|
||
init_gl!();
|
||
}
|