From 7f343f84573236953c6782d5e0e9db93dc4778e9 Mon Sep 17 00:00:00 2001 From: Universe Date: Sun, 29 Mar 2026 04:27:09 +0900 Subject: [PATCH 01/15] feat(cg): introduce Tray node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Tray — a canvas-level organizational primitive analogous to Figma SECTION. Has explicit dimensions, fills, strokes, and corner radius; never clips or applies effects; children are freely-placed root-level containers. - schema: TrayNodeRec, NodeTypeTag::Tray, GeoNodeKind::Tray - FlatBuffers: Tray table added to grida.fbs; generated grida.rs updated - I/O: From for TrayNodeRec; FBS encode/decode round-trip - layout: Tray excluded from Taffy (independently-positioned children) - painter: Tray renders fills/strokes/corner_radius without clip_path - geometry: geo_input_from_schema() helper; Tray always resolves from schema - fixtures: l0_tray scene with five Tray variants - docs: docs/wg/feat-tray/index.md design doc --- .../grida-canvas/examples/fixtures/README.md | 25 ++ .../grida-canvas/examples/fixtures/l0_tray.rs | 249 +++++++++++++++ crates/grida-canvas/examples/fixtures/mod.rs | 1 + .../examples/tool_gen_fixtures.rs | 1 + crates/grida-canvas/examples/tool_io_grida.rs | 2 + crates/grida-canvas/examples/tool_io_svg.rs | 1 + crates/grida-canvas/src/cache/geometry.rs | 92 ++++-- crates/grida-canvas/src/io/generated/grida.rs | 256 +++++++++++++-- crates/grida-canvas/src/io/id_converter.rs | 1 + crates/grida-canvas/src/io/io_grida.rs | 26 ++ crates/grida-canvas/src/io/io_grida_fbs.rs | 105 ++++++- crates/grida-canvas/src/layout/engine.rs | 20 +- crates/grida-canvas/src/layout/into_taffy.rs | 1 + crates/grida-canvas/src/node/factory.rs | 29 ++ crates/grida-canvas/src/node/scene_graph.rs | 29 ++ crates/grida-canvas/src/node/schema.rs | 89 ++++++ crates/grida-canvas/src/painter/geometry.rs | 28 ++ crates/grida-canvas/src/painter/layer.rs | 71 +++++ .../src/painter/painter_debug_node.rs | 54 ++++ crates/grida-canvas/src/resources/mod.rs | 4 + crates/grida-canvas/src/surface/ui/render.rs | 292 +++++++++++++++--- crates/grida-dev/src/bench/load_bench.rs | 1 + docs/wg/feat-tray/index.md | 179 +++++++++++ .../nodes/node.tsx | 1 + fixtures/test-grida/L0.grida | Bin 66444 -> 71108 bytes format/grida.fbs | 17 + packages/grida-canvas-io-figma/lib.ts | 12 +- packages/grida-canvas-io/format.ts | 131 ++++++++ packages/grida-canvas-schema/grida.ts | 27 ++ 29 files changed, 1655 insertions(+), 89 deletions(-) create mode 100644 crates/grida-canvas/examples/fixtures/l0_tray.rs create mode 100644 docs/wg/feat-tray/index.md diff --git a/crates/grida-canvas/examples/fixtures/README.md b/crates/grida-canvas/examples/fixtures/README.md index a81d8ef141..32aa2170db 100644 --- a/crates/grida-canvas/examples/fixtures/README.md +++ b/crates/grida-canvas/examples/fixtures/README.md @@ -412,6 +412,31 @@ scene "L0 Layout Transform" --- +## L0-tray + +Tray node (Figma SECTION equivalent) with explicit dimensions, fills, strokes, corner radius. All children are Container (frame) nodes — the realistic SECTION usage pattern. + +```text +scene "L0 Tray" +├─ tray 500×250 light grey-blue fill, grey border 1px +│ ├─ container 200×180 white fill, corner_radius=8 +│ └─ container 200×180 blue fill, corner_radius=8 +├─ tray 400×250 alice blue fill, cornflower blue border 2px, corner_radius=16 +│ ├─ container 100×180 pink card, corner_radius=6 +│ ├─ container 100×180 green card, corner_radius=6 +│ └─ container 100×180 blue card, corner_radius=6 +├─ tray 250×180 linen fill, tan border (overflow demo — no clip) +│ └─ container 250×160 green fill (overflows tray bounds — fully visible) +├─ tray 250×180 opacity=0.5, pink fill, red border 2px, corner_radius=8 +│ ├─ container 100×120 purple fill, corner_radius=6 +│ └─ container 100×120 magenta fill, corner_radius=6 +└─ tray 200×180 empty tray (no children), grey fill + border +``` + +**Exercises:** TrayNodeRec (explicit dimensions, fills, strokes, corner_radius, opacity), Container children (the primary Tray use case), child overflow (no clip), empty tray. + +--- + ## Not included (rationale) | Topic | Reason | diff --git a/crates/grida-canvas/examples/fixtures/l0_tray.rs b/crates/grida-canvas/examples/fixtures/l0_tray.rs new file mode 100644 index 0000000000..f8f6b13fc4 --- /dev/null +++ b/crates/grida-canvas/examples/fixtures/l0_tray.rs @@ -0,0 +1,249 @@ +use super::*; +use std::collections::HashMap; + +/// Helper: create a Container node at (x, y) with given size and fill color. +fn frame(x: f32, y: f32, w: f32, h: f32, fill: Paint) -> Node { + Node::Container(ContainerNodeRec { + active: true, + opacity: 1.0, + blend_mode: LayerBlendMode::PassThrough, + mask: None, + rotation: 0.0, + position: LayoutPositioningBasis::Inset(EdgeInsets { + top: y, + right: 0.0, + bottom: 0.0, + left: x, + }), + layout_container: LayoutContainerStyle::default(), + layout_dimensions: LayoutDimensionStyle { + layout_target_width: Some(w), + layout_target_height: Some(h), + layout_min_width: None, + layout_max_width: None, + layout_min_height: None, + layout_max_height: None, + layout_target_aspect_ratio: None, + }, + layout_child: None, + corner_radius: RectangularCornerRadius::default(), + corner_smoothing: CornerSmoothing(0.0), + fills: Paints::new(vec![fill]), + strokes: Paints::default(), + stroke_style: StrokeStyle::default(), + stroke_width: StrokeWidth::Uniform(0.0), + effects: LayerEffects::default(), + clip: true, + }) +} + +/// Helper: create a Container with rounded corners. +fn frame_rounded(x: f32, y: f32, w: f32, h: f32, fill: Paint, radius: f32) -> Node { + Node::Container(ContainerNodeRec { + active: true, + opacity: 1.0, + blend_mode: LayerBlendMode::PassThrough, + mask: None, + rotation: 0.0, + position: LayoutPositioningBasis::Inset(EdgeInsets { + top: y, + right: 0.0, + bottom: 0.0, + left: x, + }), + layout_container: LayoutContainerStyle::default(), + layout_dimensions: LayoutDimensionStyle { + layout_target_width: Some(w), + layout_target_height: Some(h), + layout_min_width: None, + layout_max_width: None, + layout_min_height: None, + layout_max_height: None, + layout_target_aspect_ratio: None, + }, + layout_child: None, + corner_radius: { + use cg::cg::types::Radius; + let r = Radius::circular(radius); + RectangularCornerRadius { + tl: r, + tr: r, + bl: r, + br: r, + } + }, + corner_smoothing: CornerSmoothing(0.0), + fills: Paints::new(vec![fill]), + strokes: Paints::default(), + stroke_style: StrokeStyle::default(), + stroke_width: StrokeWidth::Uniform(0.0), + effects: LayerEffects::default(), + clip: true, + }) +} + +/// Helper: create a Tray node. +fn tray( + x: f32, + y: f32, + w: f32, + h: f32, + fill: Paint, + stroke: Paint, + stroke_w: f32, + radius: f32, + opacity: f32, +) -> Node { + Node::Tray(TrayNodeRec { + active: true, + opacity, + blend_mode: LayerBlendMode::PassThrough, + mask: None, + rotation: 0.0, + position: LayoutPositioningBasis::Inset(EdgeInsets { + top: y, + right: 0.0, + bottom: 0.0, + left: x, + }), + layout_dimensions: LayoutDimensionStyle { + layout_target_width: Some(w), + layout_target_height: Some(h), + layout_min_width: None, + layout_max_width: None, + layout_min_height: None, + layout_max_height: None, + layout_target_aspect_ratio: None, + }, + corner_radius: if radius > 0.0 { + use cg::cg::types::Radius; + let r = Radius::circular(radius); + RectangularCornerRadius { + tl: r, + tr: r, + bl: r, + br: r, + } + } else { + RectangularCornerRadius::default() + }, + corner_smoothing: CornerSmoothing(0.0), + fills: Paints::new(vec![fill]), + strokes: Paints::new(vec![stroke]), + stroke_style: StrokeStyle::default(), + stroke_width: StrokeWidth::Uniform(stroke_w), + }) +} + +/// Tray node features: explicit dimensions, fills, strokes, corner radius, +/// container children treated as root-level frames (no clipping, no effects, no layout). +pub fn build() -> Scene { + // ── [1] Tray with two container children ────────────────────────────── + // Simulates a Figma SECTION holding two frames side by side. + let tray1 = tray( + 0.0, + 0.0, + 500.0, + 250.0, + solid(245, 245, 250, 255), // light grey-blue fill + solid(180, 180, 200, 255), // grey border + 1.0, + 0.0, + 1.0, + ); + let frame1a = frame_rounded(20.0, 30.0, 200.0, 180.0, solid(255, 255, 255, 255), 8.0); + let frame1b = frame_rounded(260.0, 30.0, 200.0, 180.0, solid(59, 100, 220, 255), 8.0); + + // ── [4] Tray with rounded corners and nested containers ─────────────── + // Demonstrates Tray corner_radius + containers inside. + let tray2 = tray( + 540.0, + 0.0, + 400.0, + 250.0, + solid(240, 248, 255, 255), // alice blue fill + solid(100, 149, 237, 255), // cornflower blue border + 2.0, + 16.0, + 1.0, + ); + // Three card-like containers inside + let card_a = frame_rounded(20.0, 30.0, 100.0, 180.0, solid(255, 240, 240, 255), 6.0); + let card_b = frame_rounded(140.0, 30.0, 100.0, 180.0, solid(240, 255, 240, 255), 6.0); + let card_c = frame_rounded(260.0, 30.0, 100.0, 180.0, solid(240, 240, 255, 255), 6.0); + + // ── [8] Tray with overflowing container (no clip) ───────────────────── + // Demonstrates that Tray never clips its children, unlike Container. + let tray3 = tray( + 0.0, + 290.0, + 250.0, + 180.0, + solid(255, 250, 240, 255), // linen fill + solid(200, 180, 150, 255), // tan border + 1.0, + 0.0, + 1.0, + ); + // Container intentionally overflows the tray bounds — should be fully visible. + let overflow_frame = frame(80.0, 60.0, 250.0, 160.0, solid(59, 180, 75, 200)); + + // ── [10] Tray at 50% opacity with container children ────────────────── + let tray4 = tray( + 300.0, + 290.0, + 250.0, + 180.0, + solid(255, 220, 220, 255), // light red fill + solid(220, 100, 100, 255), // red border + 2.0, + 8.0, + 0.5, + ); + let frame4a = frame_rounded(15.0, 30.0, 100.0, 120.0, solid(128, 60, 200, 255), 6.0); + let frame4b = frame_rounded(130.0, 30.0, 100.0, 120.0, solid(200, 60, 128, 255), 6.0); + + // ── [13] Empty tray (no children) ───────────────────────────────────── + // A valid tray with no content — a named region waiting for frames. + let tray5 = tray( + 600.0, + 290.0, + 200.0, + 180.0, + solid(230, 230, 230, 255), // light grey fill + solid(160, 160, 160, 255), // grey border + 1.0, + 0.0, + 1.0, + ); + + // ── Tree ──────────────────────────────────────────────────────────── + let mut links = HashMap::new(); + links.insert(1u64, vec![2u64, 3]); // tray1 → frame1a, frame1b + links.insert(4u64, vec![5, 6, 7]); // tray2 → card_a, card_b, card_c + links.insert(8u64, vec![9u64]); // tray3 → overflow_frame + links.insert(10u64, vec![11, 12]); // tray4 → frame4a, frame4b + // tray5 (13) has no children + + build_scene( + "L0 Tray", + None, + vec![ + (1, tray1), + (2, frame1a), + (3, frame1b), + (4, tray2), + (5, card_a), + (6, card_b), + (7, card_c), + (8, tray3), + (9, overflow_frame), + (10, tray4), + (11, frame4a), + (12, frame4b), + (13, tray5), + ], + links, + vec![1, 4, 8, 10, 13], + ) +} diff --git a/crates/grida-canvas/examples/fixtures/mod.rs b/crates/grida-canvas/examples/fixtures/mod.rs index b3720325e5..d07cdc6a88 100644 --- a/crates/grida-canvas/examples/fixtures/mod.rs +++ b/crates/grida-canvas/examples/fixtures/mod.rs @@ -31,6 +31,7 @@ pub mod l0_shapes; pub mod l0_strokes; pub mod l0_strokes_rect; pub mod l0_strokes_varwidth; +pub mod l0_tray; pub mod l0_type; pub mod l0_type_attributed; pub mod l0_type_features; diff --git a/crates/grida-canvas/examples/tool_gen_fixtures.rs b/crates/grida-canvas/examples/tool_gen_fixtures.rs index 59f2a0793a..469c4961ed 100644 --- a/crates/grida-canvas/examples/tool_gen_fixtures.rs +++ b/crates/grida-canvas/examples/tool_gen_fixtures.rs @@ -49,6 +49,7 @@ fn main() { ), ("L0-container", fixtures::l0_container::build()), ("L0-group", fixtures::l0_group::build()), + ("L0-tray", fixtures::l0_tray::build()), ("L0-layout-position", fixtures::l0_layout_position::build()), ("L0-layout-flex", fixtures::l0_layout_flex::build()), ( diff --git a/crates/grida-canvas/examples/tool_io_grida.rs b/crates/grida-canvas/examples/tool_io_grida.rs index 89d5562683..3f94613290 100644 --- a/crates/grida-canvas/examples/tool_io_grida.rs +++ b/crates/grida-canvas/examples/tool_io_grida.rs @@ -197,6 +197,7 @@ fn inspect_json(bytes: &[u8], cli: &Cli) { fn classify_json_node(node: &io_grida::JSONNode) -> &'static str { match node { io_grida::JSONNode::Group(_) => "group", + io_grida::JSONNode::Tray(_) => "tray", io_grida::JSONNode::Container(_) => "container", io_grida::JSONNode::Vector(_) => "vector", io_grida::JSONNode::Path(_) => "path", @@ -450,6 +451,7 @@ fn classify_node(node: &Node) -> &'static str { Node::InitialContainer(_) => "initial_container", Node::Container(_) => "container", Node::Group(_) => "group", + Node::Tray(_) => "tray", Node::Vector(_) => "vector", Node::Path(_) => "path", Node::BooleanOperation(_) => "boolean", diff --git a/crates/grida-canvas/examples/tool_io_svg.rs b/crates/grida-canvas/examples/tool_io_svg.rs index 682d82bc7f..56eeed8d33 100644 --- a/crates/grida-canvas/examples/tool_io_svg.rs +++ b/crates/grida-canvas/examples/tool_io_svg.rs @@ -238,6 +238,7 @@ fn classify_node(node: &Node) -> &'static str { Node::Image(_) => "image", Node::TextSpan(_) => "tspan", Node::AttributedText(_) => "attributed_text", + Node::Tray(_) => "tray", Node::Error(_) => "error", } } diff --git a/crates/grida-canvas/src/cache/geometry.rs b/crates/grida-canvas/src/cache/geometry.rs index 29b6c19dd0..9ce66996c3 100644 --- a/crates/grida-canvas/src/cache/geometry.rs +++ b/crates/grida-canvas/src/cache/geometry.rs @@ -403,6 +403,47 @@ impl GeometryCache { union_world_bounds } + // Tray has explicit dimensions (like Container) but no clipping. + // Children render inside the tray bounds. + GeoNodeKind::Tray => { + let local_bounds = Rectangle { + x: 0.0, + y: 0.0, + width: geo.width, + height: geo.height, + }; + + let world_transform = parent_world.compose(&geo.transform); + let world_bounds = transform_rect(&local_bounds, &world_transform); + let render_bounds = inflate_rect_sides(world_bounds, &geo.render_bounds_inflation); + + if let Some(children) = graph.get_children(id) { + for child_id in children { + Self::build_recursive( + child_id, + &world_transform, + Some(*id), + cache, + graph, + geo_inputs, + ); + } + } + + let entry = GeometryEntry { + transform: geo.transform, + absolute_transform: world_transform, + bounding_box: local_bounds, + absolute_bounding_box: world_bounds, + absolute_render_bounds: render_bounds, + parent: parent_id, + dirty_transform: false, + dirty_bounds: false, + }; + cache.entries.insert(*id, entry); + world_bounds + } + GeoNodeKind::TextSpan => { let local_bounds = Rectangle { x: 0.0, @@ -503,6 +544,21 @@ impl GeometryCache { // Layout resolution — NodeGeoData + LayoutResult → GeoInput // --------------------------------------------------------------------------- +/// Build a `GeoInput` directly from schema data, bypassing any layout result. +fn geo_input_from_schema(geo: &NodeGeoData) -> GeoInput { + GeoInput { + transform: AffineTransform::new( + geo.schema_transform.x(), + geo.schema_transform.y(), + geo.rotation, + ), + width: geo.schema_width, + height: geo.schema_height, + kind: geo.kind, + render_bounds_inflation: geo.render_bounds_inflation, + } +} + /// Resolve layout-dependent fields from `NodeGeoData` + `LayoutResult`. /// /// For most nodes this is a lightweight copy from the pre-extracted data @@ -536,28 +592,24 @@ fn resolve_layout( render_bounds_inflation: geo.render_bounds_inflation, }, GeoNodeKind::Container => { - let (x, y, width, height) = - if let Some(computed) = layout_result.and_then(|r| r.get(id)) { - (computed.x, computed.y, computed.width, computed.height) - } else { - // Fallback to schema data when layout result is missing. - // This happens for orphan nodes not reachable from scene roots, - // or when layout is skipped entirely (layout_result == None). - ( - geo.schema_transform.x(), - geo.schema_transform.y(), - geo.schema_width, - geo.schema_height, - ) - }; - GeoInput { - transform: AffineTransform::new(x, y, geo.rotation), - width, - height, - kind: geo.kind, - render_bounds_inflation: geo.render_bounds_inflation, + if let Some(computed) = layout_result.and_then(|r| r.get(id)) { + GeoInput { + transform: AffineTransform::new(computed.x, computed.y, geo.rotation), + width: computed.width, + height: computed.height, + kind: geo.kind, + render_bounds_inflation: geo.render_bounds_inflation, + } + } else { + // Fallback to schema data when layout result is missing. + // This happens for orphan nodes not reachable from scene roots, + // or when layout is skipped entirely (layout_result == None). + geo_input_from_schema(geo) } } + // Tray has explicit dimensions but never participates in Taffy layout, + // so it always uses schema data directly. + GeoNodeKind::Tray => geo_input_from_schema(geo), GeoNodeKind::TextSpan => { let layout = layout_result.and_then(|r| r.get(id)); const MIN_SIZE_DIRTY_HACK: f32 = 1.0; diff --git a/crates/grida-canvas/src/io/generated/grida.rs b/crates/grida-canvas/src/io/generated/grida.rs index 737db6dd1a..15be8339bc 100644 --- a/crates/grida-canvas/src/io/generated/grida.rs +++ b/crates/grida-canvas/src/io/generated/grida.rs @@ -10,13 +10,14 @@ pub mod grida { #[deprecated(since = "2.0.0", note = "Use associated constants instead. This will no longer be generated in 2021.")] pub const ENUM_MIN_NODE_TYPE: u8 = 0; #[deprecated(since = "2.0.0", note = "Use associated constants instead. This will no longer be generated in 2021.")] -pub const ENUM_MAX_NODE_TYPE: u8 = 14; +pub const ENUM_MAX_NODE_TYPE: u8 = 15; #[deprecated(since = "2.0.0", note = "Use associated constants instead. This will no longer be generated in 2021.")] #[allow(non_camel_case_types)] -pub const ENUM_VALUES_NODE_TYPE: [NodeType; 15] = [ +pub const ENUM_VALUES_NODE_TYPE: [NodeType; 16] = [ NodeType::Exception, NodeType::Scene, NodeType::Group, + NodeType::Tray, NodeType::InitialContainer, NodeType::Container, NodeType::BooleanOperation, @@ -40,25 +41,27 @@ impl NodeType { pub const Exception: Self = Self(0); pub const Scene: Self = Self(1); pub const Group: Self = Self(2); - pub const InitialContainer: Self = Self(3); - pub const Container: Self = Self(4); - pub const BooleanOperation: Self = Self(5); - pub const Rectangle: Self = Self(6); - pub const Ellipse: Self = Self(7); - pub const Polygon: Self = Self(8); - pub const RegularPolygon: Self = Self(9); - pub const RegularStarPolygon: Self = Self(10); - pub const Path: Self = Self(11); - pub const Line: Self = Self(12); - pub const Vector: Self = Self(13); - pub const TextSpan: Self = Self(14); + pub const Tray: Self = Self(3); + pub const InitialContainer: Self = Self(4); + pub const Container: Self = Self(5); + pub const BooleanOperation: Self = Self(6); + pub const Rectangle: Self = Self(7); + pub const Ellipse: Self = Self(8); + pub const Polygon: Self = Self(9); + pub const RegularPolygon: Self = Self(10); + pub const RegularStarPolygon: Self = Self(11); + pub const Path: Self = Self(12); + pub const Line: Self = Self(13); + pub const Vector: Self = Self(14); + pub const TextSpan: Self = Self(15); pub const ENUM_MIN: u8 = 0; - pub const ENUM_MAX: u8 = 14; + pub const ENUM_MAX: u8 = 15; pub const ENUM_VALUES: &'static [Self] = &[ Self::Exception, Self::Scene, Self::Group, + Self::Tray, Self::InitialContainer, Self::Container, Self::BooleanOperation, @@ -78,6 +81,7 @@ impl NodeType { Self::Exception => Some("Exception"), Self::Scene => Some("Scene"), Self::Group => Some("Group"), + Self::Tray => Some("Tray"), Self::InitialContainer => Some("InitialContainer"), Self::Container => Some("Container"), Self::BooleanOperation => Some("BooleanOperation"), @@ -3860,10 +3864,10 @@ pub struct LayoutPositioningBasisUnionTableOffset {} #[deprecated(since = "2.0.0", note = "Use associated constants instead. This will no longer be generated in 2021.")] pub const ENUM_MIN_NODE: u8 = 0; #[deprecated(since = "2.0.0", note = "Use associated constants instead. This will no longer be generated in 2021.")] -pub const ENUM_MAX_NODE: u8 = 12; +pub const ENUM_MAX_NODE: u8 = 13; #[deprecated(since = "2.0.0", note = "Use associated constants instead. This will no longer be generated in 2021.")] #[allow(non_camel_case_types)] -pub const ENUM_VALUES_NODE: [Node; 13] = [ +pub const ENUM_VALUES_NODE: [Node; 14] = [ Node::NONE, Node::UnknownNode, Node::SceneNode, @@ -3877,6 +3881,7 @@ pub const ENUM_VALUES_NODE: [Node; 13] = [ Node::TextSpanNode, Node::PathNode, Node::AttributedTextNode, + Node::TrayNode, ]; #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] @@ -3897,9 +3902,10 @@ impl Node { pub const TextSpanNode: Self = Self(10); pub const PathNode: Self = Self(11); pub const AttributedTextNode: Self = Self(12); + pub const TrayNode: Self = Self(13); pub const ENUM_MIN: u8 = 0; - pub const ENUM_MAX: u8 = 12; + pub const ENUM_MAX: u8 = 13; pub const ENUM_VALUES: &'static [Self] = &[ Self::NONE, Self::UnknownNode, @@ -3914,6 +3920,7 @@ impl Node { Self::TextSpanNode, Self::PathNode, Self::AttributedTextNode, + Self::TrayNode, ]; /// Returns the variant's name or "" if unknown. pub fn variant_name(self) -> Option<&'static str> { @@ -3931,6 +3938,7 @@ impl Node { Self::TextSpanNode => Some("TextSpanNode"), Self::PathNode => Some("PathNode"), Self::AttributedTextNode => Some("AttributedTextNode"), + Self::TrayNode => Some("TrayNode"), _ => None, } } @@ -17445,6 +17453,195 @@ impl ::core::fmt::Debug for GroupNode<'_> { ds.finish() } } +pub enum TrayNodeOffset {} +#[derive(Copy, Clone, PartialEq)] + +/// Node variant: Tray. +/// +/// A canvas-level organizational primitive (maps to Figma SECTION). +/// Has explicit dimensions, fills, strokes, corner radius. +/// No effects, no layout, no clipping. +/// Children are freely placed and treated as root-level containers. +pub struct TrayNode<'a> { + pub _tab: ::flatbuffers::Table<'a>, +} + +impl<'a> ::flatbuffers::Follow<'a> for TrayNode<'a> { + type Inner = TrayNode<'a>; + #[inline] + unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { + Self { _tab: unsafe { ::flatbuffers::Table::new(buf, loc) } } + } +} + +impl<'a> TrayNode<'a> { + pub const VT_NODE: ::flatbuffers::VOffsetT = 4; + pub const VT_LAYER: ::flatbuffers::VOffsetT = 6; + pub const VT_STROKE_GEOMETRY: ::flatbuffers::VOffsetT = 8; + pub const VT_CORNER_RADIUS: ::flatbuffers::VOffsetT = 10; + pub const VT_FILL_PAINTS: ::flatbuffers::VOffsetT = 12; + pub const VT_STROKE_PAINTS: ::flatbuffers::VOffsetT = 14; + + #[inline] + pub unsafe fn init_from_table(table: ::flatbuffers::Table<'a>) -> Self { + TrayNode { _tab: table } + } + #[allow(unused_mut)] + pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr, A: ::flatbuffers::Allocator + 'bldr>( + _fbb: &'mut_bldr mut ::flatbuffers::FlatBufferBuilder<'bldr, A>, + args: &'args TrayNodeArgs<'args> + ) -> ::flatbuffers::WIPOffset> { + let mut builder = TrayNodeBuilder::new(_fbb); + if let Some(x) = args.stroke_paints { builder.add_stroke_paints(x); } + if let Some(x) = args.fill_paints { builder.add_fill_paints(x); } + if let Some(x) = args.corner_radius { builder.add_corner_radius(x); } + if let Some(x) = args.stroke_geometry { builder.add_stroke_geometry(x); } + if let Some(x) = args.layer { builder.add_layer(x); } + if let Some(x) = args.node { builder.add_node(x); } + builder.finish() + } + + + #[inline] + pub fn node(&self) -> SystemNodeTrait<'a> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::<::flatbuffers::ForwardsUOffset>(TrayNode::VT_NODE, None).unwrap()} + } + #[inline] + pub fn layer(&self) -> LayerTrait<'a> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::<::flatbuffers::ForwardsUOffset>(TrayNode::VT_LAYER, None).unwrap()} + } + #[inline] + pub fn stroke_geometry(&self) -> Option> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::<::flatbuffers::ForwardsUOffset>(TrayNode::VT_STROKE_GEOMETRY, None)} + } + #[inline] + pub fn corner_radius(&self) -> Option> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::<::flatbuffers::ForwardsUOffset>(TrayNode::VT_CORNER_RADIUS, None)} + } + #[inline] + pub fn fill_paints(&self) -> Option<::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::<::flatbuffers::ForwardsUOffset<::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>>(TrayNode::VT_FILL_PAINTS, None)} + } + #[inline] + pub fn stroke_paints(&self) -> Option<::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::<::flatbuffers::ForwardsUOffset<::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>>(TrayNode::VT_STROKE_PAINTS, None)} + } +} + +impl ::flatbuffers::Verifiable for TrayNode<'_> { + #[inline] + fn run_verifier( + v: &mut ::flatbuffers::Verifier, pos: usize + ) -> Result<(), ::flatbuffers::InvalidFlatbuffer> { + v.visit_table(pos)? + .visit_field::<::flatbuffers::ForwardsUOffset>("node", Self::VT_NODE, true)? + .visit_field::<::flatbuffers::ForwardsUOffset>("layer", Self::VT_LAYER, true)? + .visit_field::<::flatbuffers::ForwardsUOffset>("stroke_geometry", Self::VT_STROKE_GEOMETRY, false)? + .visit_field::<::flatbuffers::ForwardsUOffset>("corner_radius", Self::VT_CORNER_RADIUS, false)? + .visit_field::<::flatbuffers::ForwardsUOffset<::flatbuffers::Vector<'_, ::flatbuffers::ForwardsUOffset>>>("fill_paints", Self::VT_FILL_PAINTS, false)? + .visit_field::<::flatbuffers::ForwardsUOffset<::flatbuffers::Vector<'_, ::flatbuffers::ForwardsUOffset>>>("stroke_paints", Self::VT_STROKE_PAINTS, false)? + .finish(); + Ok(()) + } +} +pub struct TrayNodeArgs<'a> { + pub node: Option<::flatbuffers::WIPOffset>>, + pub layer: Option<::flatbuffers::WIPOffset>>, + pub stroke_geometry: Option<::flatbuffers::WIPOffset>>, + pub corner_radius: Option<::flatbuffers::WIPOffset>>, + pub fill_paints: Option<::flatbuffers::WIPOffset<::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>>>, + pub stroke_paints: Option<::flatbuffers::WIPOffset<::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>>>, +} +impl<'a> Default for TrayNodeArgs<'a> { + #[inline] + fn default() -> Self { + TrayNodeArgs { + node: None, // required field + layer: None, // required field + stroke_geometry: None, + corner_radius: None, + fill_paints: None, + stroke_paints: None, + } + } +} + +pub struct TrayNodeBuilder<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> { + fbb_: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, + start_: ::flatbuffers::WIPOffset<::flatbuffers::TableUnfinishedWIPOffset>, +} +impl<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> TrayNodeBuilder<'a, 'b, A> { + #[inline] + pub fn add_node(&mut self, node: ::flatbuffers::WIPOffset>) { + self.fbb_.push_slot_always::<::flatbuffers::WIPOffset>(TrayNode::VT_NODE, node); + } + #[inline] + pub fn add_layer(&mut self, layer: ::flatbuffers::WIPOffset>) { + self.fbb_.push_slot_always::<::flatbuffers::WIPOffset>(TrayNode::VT_LAYER, layer); + } + #[inline] + pub fn add_stroke_geometry(&mut self, stroke_geometry: ::flatbuffers::WIPOffset>) { + self.fbb_.push_slot_always::<::flatbuffers::WIPOffset>(TrayNode::VT_STROKE_GEOMETRY, stroke_geometry); + } + #[inline] + pub fn add_corner_radius(&mut self, corner_radius: ::flatbuffers::WIPOffset>) { + self.fbb_.push_slot_always::<::flatbuffers::WIPOffset>(TrayNode::VT_CORNER_RADIUS, corner_radius); + } + #[inline] + pub fn add_fill_paints(&mut self, fill_paints: ::flatbuffers::WIPOffset<::flatbuffers::Vector<'b , ::flatbuffers::ForwardsUOffset>>>) { + self.fbb_.push_slot_always::<::flatbuffers::WIPOffset<_>>(TrayNode::VT_FILL_PAINTS, fill_paints); + } + #[inline] + pub fn add_stroke_paints(&mut self, stroke_paints: ::flatbuffers::WIPOffset<::flatbuffers::Vector<'b , ::flatbuffers::ForwardsUOffset>>>) { + self.fbb_.push_slot_always::<::flatbuffers::WIPOffset<_>>(TrayNode::VT_STROKE_PAINTS, stroke_paints); + } + #[inline] + pub fn new(_fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>) -> TrayNodeBuilder<'a, 'b, A> { + let start = _fbb.start_table(); + TrayNodeBuilder { + fbb_: _fbb, + start_: start, + } + } + #[inline] + pub fn finish(self) -> ::flatbuffers::WIPOffset> { + let o = self.fbb_.end_table(self.start_); + self.fbb_.required(o, TrayNode::VT_NODE,"node"); + self.fbb_.required(o, TrayNode::VT_LAYER,"layer"); + ::flatbuffers::WIPOffset::new(o.value()) + } +} + +impl ::core::fmt::Debug for TrayNode<'_> { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + let mut ds = f.debug_struct("TrayNode"); + ds.field("node", &self.node()); + ds.field("layer", &self.layer()); + ds.field("stroke_geometry", &self.stroke_geometry()); + ds.field("corner_radius", &self.corner_radius()); + ds.field("fill_paints", &self.fill_paints()); + ds.field("stroke_paints", &self.stroke_paints()); + ds.finish() + } +} pub enum BooleanOperationNodeOffset {} #[derive(Copy, Clone, PartialEq)] @@ -19238,6 +19435,21 @@ impl<'a> NodeSlot<'a> { } } + #[inline] + #[allow(non_snake_case)] + pub fn node_as_tray_node(&self) -> Option> { + if self.node_type() == Node::TrayNode { + self.node().map(|t| { + // Safety: + // Created from a valid Table for this object + // Which contains a valid union in this slot + unsafe { TrayNode::init_from_table(t) } + }) + } else { + None + } + } + } impl ::flatbuffers::Verifiable for NodeSlot<'_> { @@ -19260,6 +19472,7 @@ impl ::flatbuffers::Verifiable for NodeSlot<'_> { Node::TextSpanNode => v.verify_union_variant::<::flatbuffers::ForwardsUOffset>("Node::TextSpanNode", pos), Node::PathNode => v.verify_union_variant::<::flatbuffers::ForwardsUOffset>("Node::PathNode", pos), Node::AttributedTextNode => v.verify_union_variant::<::flatbuffers::ForwardsUOffset>("Node::AttributedTextNode", pos), + Node::TrayNode => v.verify_union_variant::<::flatbuffers::ForwardsUOffset>("Node::TrayNode", pos), _ => Ok(()), } })? @@ -19398,6 +19611,13 @@ impl ::core::fmt::Debug for NodeSlot<'_> { ds.field("node", &"InvalidFlatbuffer: Union discriminant does not match value.") } }, + Node::TrayNode => { + if let Some(x) = self.node_as_tray_node() { + ds.field("node", &x) + } else { + ds.field("node", &"InvalidFlatbuffer: Union discriminant does not match value.") + } + }, _ => { let x: Option<()> = None; ds.field("node", &x) diff --git a/crates/grida-canvas/src/io/id_converter.rs b/crates/grida-canvas/src/io/id_converter.rs index d82c82457b..046bfb1e5f 100644 --- a/crates/grida-canvas/src/io/id_converter.rs +++ b/crates/grida-canvas/src/io/id_converter.rs @@ -157,6 +157,7 @@ impl IdConverter { fn convert_json_node(json_node: JSONNode) -> Node { match json_node { JSONNode::Group(group) => Node::Group(GroupNodeRec::from(group)), + JSONNode::Tray(tray) => Node::Tray(TrayNodeRec::from(tray)), JSONNode::Container(container) => Node::Container(ContainerNodeRec::from(container)), JSONNode::Vector(vector) => Node::from(JSONNode::Vector(vector)), JSONNode::Path(path) => Node::from(JSONNode::Path(path)), diff --git a/crates/grida-canvas/src/io/io_grida.rs b/crates/grida-canvas/src/io/io_grida.rs index a95b9d2c44..d6a5f74721 100644 --- a/crates/grida-canvas/src/io/io_grida.rs +++ b/crates/grida-canvas/src/io/io_grida.rs @@ -870,6 +870,8 @@ pub struct JSONUnknownNodeProperties { pub enum JSONNode { #[serde(rename = "group")] Group(JSONGroupNode), + #[serde(rename = "tray")] + Tray(JSONGroupNode), #[serde(rename = "container", alias = "component")] Container(JSONContainerNode), #[serde(rename = "vector")] @@ -902,6 +904,7 @@ impl JSONNode { pub fn name(&self) -> Option<&str> { match self { JSONNode::Group(n) => n.base.name.as_deref(), + JSONNode::Tray(n) => n.base.name.as_deref(), JSONNode::Container(n) => n.base.name.as_deref(), JSONNode::Vector(n) => n.base.name.as_deref(), JSONNode::Path(n) => n.base.name.as_deref(), @@ -1313,6 +1316,28 @@ impl From for GroupNodeRec { } } +impl From for TrayNodeRec { + fn from(node: JSONGroupNode) -> Self { + // TODO: JSONNode::Tray reuses JSONGroupNode shape; visual fields default + // until a dedicated JSONTrayNode is introduced. + TrayNodeRec { + active: node.base.active, + opacity: node.base.opacity, + blend_mode: node.base.blend_mode.into(), + mask: node.base.mask.map(|m| m.into()), + rotation: 0.0, + position: Default::default(), + layout_dimensions: Default::default(), + corner_radius: Default::default(), + corner_smoothing: Default::default(), + fills: Paints::default(), + strokes: Paints::default(), + stroke_style: StrokeStyle::default(), + stroke_width: StrokeWidth::default(), + } + } +} + impl From for ContainerNodeRec { fn from(node: JSONContainerNode) -> Self { // Build stroke width early before any moves @@ -2052,6 +2077,7 @@ impl From for Node { fn from(node: JSONNode) -> Self { match node { JSONNode::Group(group) => Node::Group(group.into()), + JSONNode::Tray(tray) => Node::Tray(tray.into()), JSONNode::Container(container) => Node::Container(container.into()), JSONNode::TextSpan(text) => Node::TextSpan(text.into()), JSONNode::Vector(vector) => vector.into(), diff --git a/crates/grida-canvas/src/io/io_grida_fbs.rs b/crates/grida-canvas/src/io/io_grida_fbs.rs index dd7752fe59..574b19e94c 100644 --- a/crates/grida-canvas/src/io/io_grida_fbs.rs +++ b/crates/grida-canvas/src/io/io_grida_fbs.rs @@ -57,7 +57,7 @@ use crate::node::{ GroupNodeRec, InitialContainerNodeRec, LayerEffects, LayoutChildStyle, LayoutContainerStyle, LayoutDimensionStyle, LayoutPositioningBasis, LineNodeRec, Node, PathNodeRec, RectangleNodeRec, RegularPolygonNodeRec, RegularStarPolygonNodeRec, Scene, - Size, StrokeStyle, TextSpanNodeRec, VectorNodeRec, + Size, StrokeStyle, TextSpanNodeRec, TrayNodeRec, VectorNodeRec, }, }; use crate::vectornetwork::{ @@ -332,6 +332,9 @@ fn decode_all_inner(bytes: &[u8]) -> Result { fbs::Node::GroupNode => { decode_layer_node!(slot, node_as_group_node, decode_group_node); } + fbs::Node::TrayNode => { + decode_layer_node!(slot, node_as_tray_node, decode_tray_node); + } fbs::Node::ContainerNode => { decode_layer_node!(slot, node_as_container_node, decode_container_node); } @@ -1529,6 +1532,35 @@ fn decode_group_node( }) } +fn decode_tray_node(lc: &LayerCommon, layer: &fbs::LayerTrait<'_>, tn: &fbs::TrayNode<'_>) -> Node { + let layout = layer.layout(); + let position = layout + .as_ref() + .map(decode_layout_position) + .unwrap_or_default(); + let layout_dimensions = layout + .as_ref() + .map(decode_layout_dimension_style) + .unwrap_or_default(); + let (stroke_style, stroke_width) = decode_rectangular_stroke_geometry(tn.stroke_geometry()); + + Node::Tray(TrayNodeRec { + active: lc.active, + opacity: lc.opacity, + blend_mode: lc.blend_mode, + mask: lc.mask, + rotation: lc.rotation, + position, + layout_dimensions, + corner_radius: decode_corner_radius(tn.corner_radius()), + corner_smoothing: decode_corner_smoothing(tn.corner_radius()), + fills: decode_paints_vec(tn.fill_paints()), + strokes: decode_paints_vec(tn.stroke_paints()), + stroke_style, + stroke_width, + }) +} + fn decode_container_node( lc: &LayerCommon, layer: &fbs::LayerTrait<'_>, @@ -2447,6 +2479,7 @@ fn encode_node<'a, A: flatbuffers::Allocator + 'a>( BasicShapeFields::RegularStarPolygon(r), ), Node::Group(r) => encode_group_node(fbb, r, node_id, parent_id, position), + Node::Tray(r) => encode_tray_node(fbb, r, node_id, parent_id, position), Node::Line(r) => encode_line_node(fbb, r, node_id, parent_id, position), Node::Vector(r) => encode_vector_node(fbb, r, node_id, parent_id, position), Node::TextSpan(r) => encode_text_span_node(fbb, r, node_id, parent_id, position), @@ -3655,6 +3688,76 @@ fn encode_group_node<'a, A: flatbuffers::Allocator + 'a>( make_node_slot(fbb, fbs::Node::GroupNode, gn.as_union_value()) } +fn encode_tray_node<'a, A: flatbuffers::Allocator + 'a>( + fbb: &mut flatbuffers::FlatBufferBuilder<'a, A>, + r: &TrayNodeRec, + node_id: &str, + parent_id: &str, + position: &str, +) -> flatbuffers::WIPOffset> { + let sys = encode_system_node_trait(fbb, node_id, "", r.active, false); + // Tray has position + dimensions but no layout_container or layout_child + let layout = encode_container_layout( + fbb, + &r.position, + &r.layout_dimensions, + &LayoutContainerStyle::default(), + &None, + ); + let default_effects = LayerEffects::default(); + let layer = encode_layer_trait( + fbb, + &LayerTraitInput { + parent_id, + position, + opacity: r.opacity, + blend_mode: r.blend_mode, + mask: r.mask, + effects: &default_effects, + post_layout_transform: rotation_degrees_to_transform(r.rotation), + layout: Some(layout), + }, + ); + + // Corner radius + let rcr = encode_rectangular_corner_radius(&r.corner_radius); + let cr_trait = fbs::RectangularCornerRadiusTrait::create( + fbb, + &fbs::RectangularCornerRadiusTraitArgs { + rectangular_corner_radius: Some(&rcr), + corner_smoothing: r.corner_smoothing.0, + }, + ); + + // Stroke geometry + let stroke_style_offset = encode_stroke_style(fbb, &r.stroke_style); + let rsw = encode_rectangular_stroke_width(&r.stroke_width); + let sg = fbs::RectangularStrokeGeometryTrait::create( + fbb, + &fbs::RectangularStrokeGeometryTraitArgs { + stroke_style: Some(stroke_style_offset), + rectangular_stroke_width: rsw.as_ref(), + stroke_width_profile: None, + }, + ); + + let fill_offsets = encode_paints(fbb, &r.fills); + let stroke_offsets = encode_paints(fbb, &r.strokes); + + let tn = fbs::TrayNode::create( + fbb, + &fbs::TrayNodeArgs { + node: Some(sys), + layer: Some(layer), + corner_radius: Some(cr_trait), + stroke_geometry: Some(sg), + fill_paints: fill_offsets, + stroke_paints: stroke_offsets, + }, + ); + make_node_slot(fbb, fbs::Node::TrayNode, tn.as_union_value()) +} + /// Unified data source for BasicShapeNode encoding. enum BasicShapeFields<'a> { Rectangle(&'a RectangleNodeRec), diff --git a/crates/grida-canvas/src/layout/engine.rs b/crates/grida-canvas/src/layout/engine.rs index 817031244c..40d0c8810f 100644 --- a/crates/grida-canvas/src/layout/engine.rs +++ b/crates/grida-canvas/src/layout/engine.rs @@ -242,6 +242,13 @@ impl LayoutEngine { // Size derived from children bounds (dynamic) (0.0, 0.0) } + Node::Tray(n) => { + // Tray has explicit dimensions + ( + n.layout_dimensions.layout_target_width.unwrap_or(0.0), + n.layout_dimensions.layout_target_height.unwrap_or(0.0), + ) + } Node::Error(n) => (n.size.width, n.size.height), Node::InitialContainer(_) => (0.0, 0.0), // Size set by viewport } @@ -280,6 +287,7 @@ impl LayoutEngine { let t = n.transform.unwrap_or_default(); (t.x(), t.y()) } + Node::Tray(n) => (n.position.x().unwrap_or(0.0), n.position.y().unwrap_or(0.0)), Node::BooleanOperation(n) => { let t = n.transform.unwrap_or_default(); (t.x(), t.y()) @@ -289,11 +297,15 @@ impl LayoutEngine { /// Check if a node type participates in Taffy layout (using NodeTypeTag). /// - /// Virtual grouping nodes (Group, BooleanOperation) are excluded — they - /// have no intrinsic size, don't constrain children, and their bounds are - /// derived from children. Their children form independent Taffy subtrees. + /// Group and BooleanOperation are excluded because their bounds are derived + /// from children. Tray is excluded because its children are independently + /// positioned and do not form a Taffy subtree. All three form independent + /// Taffy subtrees rooted at each of their children. fn is_layout_node_tag(tag: NodeTypeTag) -> bool { - !matches!(tag, NodeTypeTag::Group | NodeTypeTag::BooleanOperation) + !matches!( + tag, + NodeTypeTag::Group | NodeTypeTag::Tray | NodeTypeTag::BooleanOperation + ) } /// Recursively build Taffy tree for a node and its descendants. diff --git a/crates/grida-canvas/src/layout/into_taffy.rs b/crates/grida-canvas/src/layout/into_taffy.rs index 2f1ad14f26..afc83ebce4 100644 --- a/crates/grida-canvas/src/layout/into_taffy.rs +++ b/crates/grida-canvas/src/layout/into_taffy.rs @@ -300,6 +300,7 @@ pub fn node_to_taffy_style(node: &Node, _graph: &SceneGraph, _node_id: &NodeId) ..grida_style_default() }, Node::Group(_) => grida_style_default(), + Node::Tray(_) => grida_style_default(), Node::BooleanOperation(_) => grida_style_default(), } } diff --git a/crates/grida-canvas/src/node/factory.rs b/crates/grida-canvas/src/node/factory.rs index 9c98735476..e591410a9d 100644 --- a/crates/grida-canvas/src/node/factory.rs +++ b/crates/grida-canvas/src/node/factory.rs @@ -164,6 +164,35 @@ impl NodeFactory { } } + /// Creates a new tray node with default values (Figma SECTION equivalent) + pub fn create_tray_node(&self) -> TrayNodeRec { + TrayNodeRec { + active: true, + opacity: Self::DEFAULT_OPACITY, + blend_mode: LayerBlendMode::default(), + mask: None, + rotation: 0.0, + position: Default::default(), + layout_dimensions: LayoutDimensionStyle { + layout_target_width: Some(Self::DEFAULT_SIZE.width), + layout_target_height: Some(Self::DEFAULT_SIZE.height), + ..Default::default() + }, + corner_radius: Default::default(), + corner_smoothing: Default::default(), + fills: Paints::default(), + strokes: Default::default(), + stroke_style: StrokeStyle { + stroke_align: Self::DEFAULT_STROKE_ALIGN, + stroke_cap: StrokeCap::default(), + stroke_join: StrokeJoin::default(), + stroke_miter_limit: StrokeMiterLimit::default(), + stroke_dash_array: None, + }, + stroke_width: StrokeWidth::Uniform(Self::DEFAULT_STROKE_WIDTH), + } + } + /// Creates a new container node with default values pub fn create_container_node(&self) -> ContainerNodeRec { ContainerNodeRec { diff --git a/crates/grida-canvas/src/node/scene_graph.rs b/crates/grida-canvas/src/node/scene_graph.rs index da90117c7b..f397c56cca 100644 --- a/crates/grida-canvas/src/node/scene_graph.rs +++ b/crates/grida-canvas/src/node/scene_graph.rs @@ -59,6 +59,7 @@ pub type SceneGraphResult = Result; #[repr(u8)] pub enum GeoNodeKind { Group, + Tray, InitialContainer, Container, BooleanOperation, @@ -156,6 +157,34 @@ pub fn extract_geo_data(node: &Node) -> NodeGeoData { render_bounds_inflation: RenderBoundsInflation::ZERO, // union of children rotation: 0.0, }, + Node::Tray(n) => { + let fallback_x = n.position.x().unwrap_or(0.0); + let fallback_y = n.position.y().unwrap_or(0.0); + let schema_transform = AffineTransform::new(fallback_x, fallback_y, n.rotation); + + let render_bounds_inflation = if let Some(rect_stroke) = n.rectangular_stroke_width() { + compute_inflation_rectangular( + &rect_stroke, + n.stroke_style.stroke_align, + &super::schema::LayerEffects::default(), + ) + } else { + compute_inflation_uniform( + n.render_bounds_stroke_width(), + n.stroke_style.stroke_align, + &super::schema::LayerEffects::default(), + ) + }; + + NodeGeoData { + schema_transform, + schema_width: n.layout_dimensions.layout_target_width.unwrap_or(0.0), + schema_height: n.layout_dimensions.layout_target_height.unwrap_or(0.0), + kind: GeoNodeKind::Tray, + render_bounds_inflation, + rotation: n.rotation, + } + } Node::InitialContainer(_) => NodeGeoData { schema_transform: AffineTransform::identity(), schema_width: 0.0, diff --git a/crates/grida-canvas/src/node/schema.rs b/crates/grida-canvas/src/node/schema.rs index c6ddabea9b..a5666619ad 100644 --- a/crates/grida-canvas/src/node/schema.rs +++ b/crates/grida-canvas/src/node/schema.rs @@ -905,6 +905,7 @@ pub enum NodeTypeTag { Container, Error, Group, + Tray, Rectangle, Ellipse, Polygon, @@ -992,6 +993,16 @@ pub fn extract_layer_core(node: &Node) -> NodeLayerCore { node_type: NodeTypeTag::Group, is_flex: false, }, + Node::Tray(n) => NodeLayerCore { + active: n.active, + opacity: n.opacity, + blend_mode: n.blend_mode, + mask: n.mask, + clips_content: false, + has_effects: false, // Tray never has effects + node_type: NodeTypeTag::Tray, + is_flex: false, + }, Node::Rectangle(n) => NodeLayerCore { active: n.active, opacity: n.opacity, @@ -1121,6 +1132,7 @@ pub enum Node { Container(ContainerNodeRec), Error(ErrorNodeRec), Group(GroupNodeRec), + Tray(TrayNodeRec), Rectangle(RectangleNodeRec), Ellipse(EllipseNodeRec), Polygon(PolygonNodeRec), @@ -1145,6 +1157,7 @@ impl NodeTrait for Node { match self { Node::Error(n) => n.active, Node::Group(n) => n.active, + Node::Tray(n) => n.active, Node::Container(n) => n.active, Node::InitialContainer(n) => n.active, Node::Rectangle(n) => n.active, @@ -1167,6 +1180,7 @@ impl Node { pub fn mask(&self) -> Option { match self { Node::Group(n) => n.mask, + Node::Tray(n) => n.mask, Node::Container(n) => n.mask, Node::InitialContainer(_) => None, Node::Rectangle(n) => n.mask, @@ -1193,6 +1207,7 @@ impl Node { Node::Container(n) => n.opacity, Node::Error(n) => n.opacity, Node::Group(n) => n.opacity, + Node::Tray(n) => n.opacity, Node::Rectangle(n) => n.opacity, Node::Ellipse(n) => n.opacity, Node::Polygon(n) => n.opacity, @@ -1215,6 +1230,7 @@ impl Node { Node::Container(_) => "Frame", Node::Error(_) => "Error", Node::Group(_) => "Group", + Node::Tray(_) => "Tray", Node::Rectangle(_) => "Rectangle", Node::Ellipse(_) => "Ellipse", Node::Polygon(_) => "Polygon", @@ -1238,6 +1254,7 @@ impl Node { Node::Error(_) => LayerBlendMode::PassThrough, Node::Container(n) => n.blend_mode, Node::Group(n) => n.blend_mode, + Node::Tray(n) => n.blend_mode, Node::Rectangle(n) => n.blend_mode, Node::Ellipse(n) => n.blend_mode, Node::Polygon(n) => n.blend_mode, @@ -1260,6 +1277,7 @@ impl Node { Node::InitialContainer(_) => None, Node::Error(_) => None, Node::Group(_) => None, + Node::Tray(_) => None, Node::Container(n) => Some(&n.effects), Node::Rectangle(n) => Some(&n.effects), Node::Ellipse(n) => Some(&n.effects), @@ -1292,6 +1310,7 @@ impl Node { Node::InitialContainer(_) | Node::Container(_) | Node::Group(_) + | Node::Tray(_) | Node::BooleanOperation(_) ) } @@ -1420,6 +1439,35 @@ pub struct GroupNodeRec { pub transform: Option, } +/// Tray node — a canvas-level organizational primitive (Figma SECTION). +/// +/// Has explicit dimensions, fills, strokes, and corner radius (unlike Group). +/// No layout, no clipping, no effects. +/// Children are freely placed and treated as root-level containers. +#[derive(Debug, Clone)] +pub struct TrayNodeRec { + pub active: bool, + + pub opacity: f32, + pub blend_mode: LayerBlendMode, + pub mask: Option, + + pub rotation: f32, + + /// positioning + pub position: LayoutPositioningBasis, + + /// Explicit width/height (unlike Group which derives size from children). + pub layout_dimensions: LayoutDimensionStyle, + + pub corner_radius: RectangularCornerRadius, + pub corner_smoothing: CornerSmoothing, + pub fills: Paints, + pub strokes: Paints, + pub stroke_style: StrokeStyle, + pub stroke_width: StrokeWidth, +} + #[derive(Debug, Clone)] pub struct ContainerNodeRec { pub active: bool, @@ -1516,6 +1564,47 @@ impl NodeGeometryMixin for ContainerNodeRec { } } +impl TrayNodeRec { + pub fn to_own_shape(&self) -> RRectShape { + RRectShape { + width: self.layout_dimensions.layout_target_width.unwrap_or(0.0), + height: self.layout_dimensions.layout_target_height.unwrap_or(0.0), + corner_radius: self.corner_radius, + } + } +} + +impl NodeFillsMixin for TrayNodeRec { + fn set_fill(&mut self, fill: Paint) { + self.fills = Paints::new([fill]); + } + + fn set_fills(&mut self, fills: Paints) { + self.fills = fills; + } +} + +impl NodeGeometryMixin for TrayNodeRec { + fn has_stroke_geometry(&self) -> bool { + !self.stroke_width.is_none() && self.strokes.is_visible() + } + + fn render_bounds_stroke_width(&self) -> f32 { + if self.has_stroke_geometry() { + self.stroke_width.max() + } else { + 0.0 + } + } + + fn rectangular_stroke_width(&self) -> Option { + match &self.stroke_width { + StrokeWidth::Rectangular(rect_stroke) => Some(rect_stroke.clone()), + _ => None, + } + } +} + /// Initial Container Block - Viewport-filling flex container /// /// Similar to `` in DOM. Fills viewport and positions direct children diff --git a/crates/grida-canvas/src/painter/geometry.rs b/crates/grida-canvas/src/painter/geometry.rs index 1c4971c0df..4d86b8b91a 100644 --- a/crates/grida-canvas/src/painter/geometry.rs +++ b/crates/grida-canvas/src/painter/geometry.rs @@ -272,6 +272,34 @@ pub fn build_shape(node: &Node, bounds: &Rectangle) -> PainterShape { PainterShape::from_rect(rect) } } + Node::Tray(n) => { + // Tray uses resolved bounds (like Container) with optional corner radius + let width = bounds.width; + let height = bounds.height; + + let r = n.corner_radius; + if !r.is_zero() { + if n.corner_smoothing.value() > 0.0 { + let smooth = OrthogonalSmoothRRectShape { + width, + height, + corner_radius: n.corner_radius, + corner_smoothing: n.corner_smoothing, + }; + PainterShape::from_path(build_orthogonal_smooth_rrect_path(&smooth)) + } else { + let rrect = build_rrect(&RRectShape { + width, + height, + corner_radius: n.corner_radius, + }); + PainterShape::from_rrect(rrect) + } + } else { + let rect = Rect::from_xywh(0.0, 0.0, width, height); + PainterShape::from_rect(rect) + } + } Node::Error(n) => { let rect = Rect::from_xywh(0.0, 0.0, n.size.width, n.size.height); PainterShape::from_rect(rect) diff --git a/crates/grida-canvas/src/painter/layer.rs b/crates/grida-canvas/src/painter/layer.rs index ab55fab10f..e681f81657 100644 --- a/crates/grida-canvas/src/painter/layer.rs +++ b/crates/grida-canvas/src/painter/layer.rs @@ -514,6 +514,77 @@ impl LayerList { mask: n.mask, } } + Node::Tray(n) => { + // Tray renders like a simplified Container — has fills, strokes, corner_radius, + // explicit dimensions. No effects, no clipping, no render surface. + let opacity = parent_opacity * n.opacity; + let bounds = scene_cache + .geometry() + .get_world_bounds(id) + .expect("Geometry must exist"); + let shape = build_shape(node, &bounds); + let size = Size { + width: bounds.width, + height: bounds.height, + }; + let stroke_path = Self::compute_rectangular_stroke_path( + &n.stroke_width, + &n.corner_radius, + &n.stroke_style, + &size, + &shape, + ); + + let fills = Self::filter_visible_paints(&n.fills); + let strokes = Self::filter_visible_paints(&n.strokes); + let stroke_overlaps_fill = + !matches!(n.stroke_style.stroke_align, StrokeAlign::Outside); + let non_overlapping_fill_path = Self::compute_non_overlapping_fill_path( + &shape, + stroke_path.as_ref(), + stroke_overlaps_fill, + &fills, + &strokes, + n.opacity, + ); + + let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { + base: PainterPictureLayerBase { + id: id.clone(), + z_index: out.len(), + opacity, + blend_mode: n.blend_mode, + transform, + clip_path: None, // Tray never clips + }, + shape, + effects: LayerEffects::default(), // Tray has no effects + strokes, + fills, + stroke_path, + marker_start_shape: StrokeMarkerPreset::None, + marker_end_shape: StrokeMarkerPreset::None, + stroke_width: 0.0, + stroke_overlaps_fill, + non_overlapping_fill_path, + }); + out.push(LayerEntry { + id: id.clone(), + layer: layer.clone(), + }); + + // Children (no clipping — Tray never clips) + let children = graph.get_children(id).map(|c| c.as_slice()).unwrap_or(&[]); + let child_commands = + Self::build_render_commands(children, graph, scene_cache, opacity, out); + + let mut commands = vec![PainterRenderCommand::Draw(layer)]; + commands.extend(child_commands); + FlattenResult { + commands, + mask: n.mask, + } + } Node::Container(n) => { let opacity = parent_opacity * n.opacity; let bounds = scene_cache diff --git a/crates/grida-canvas/src/painter/painter_debug_node.rs b/crates/grida-canvas/src/painter/painter_debug_node.rs index 4e1d08b3ed..d1b21e830c 100644 --- a/crates/grida-canvas/src/painter/painter_debug_node.rs +++ b/crates/grida-canvas/src/painter/painter_debug_node.rs @@ -535,6 +535,60 @@ impl<'a> NodePainter<'a> { match node { Node::Error(n) => self.draw_error_node(n), Node::Group(n) => self.draw_group_node_recursively(id, n, graph, cache), + // Tray renders like Container — has fills, strokes, corner_radius, explicit dimensions. + Node::Tray(n) => { + // Get pre-computed local transform from geometry cache + let local_transform = cache + .get_transform(id) + .expect("Geometry must exist - pipeline bug"); + + self.painter.with_transform(&local_transform.matrix, || { + self.painter.with_opacity(n.opacity, None, || { + let bounds = cache + .get_world_bounds(id) + .expect("Geometry must exist - pipeline bug"); + let shape = build_shape(node, &bounds); + let identity_transform = + math2::transform::AffineTransform::identity().matrix; + + self.painter.with_blendmode( + n.blend_mode, + &shape, + &LayerEffects::default(), // Tray has no effects + &identity_transform, + None, + || { + // Paint fills + self.painter.draw_fills(&shape, &n.fills); + + // Draw children (no clipping — Tray never clips) + if let Some(children) = graph.get_children(id) { + for child_id in children { + if let Ok(child) = graph.get_node(child_id) { + self.draw_node_recursively( + child_id, child, graph, cache, + ); + } + } + } + + // Paint strokes + let stroke_width = n.render_bounds_stroke_width(); + self.painter.draw_strokes( + &shape, + &n.strokes, + stroke_width, + n.stroke_style.stroke_align, + n.stroke_style.stroke_cap, + n.stroke_style.stroke_join, + n.stroke_style.stroke_miter_limit, + n.stroke_style.stroke_dash_array.as_ref(), + ); + }, + ); + }); + }); + } Node::Container(n) => { // Get pre-computed local transform from geometry cache let local_transform = cache diff --git a/crates/grida-canvas/src/resources/mod.rs b/crates/grida-canvas/src/resources/mod.rs index 5de3010be4..4f732dd388 100644 --- a/crates/grida-canvas/src/resources/mod.rs +++ b/crates/grida-canvas/src/resources/mod.rs @@ -215,6 +215,10 @@ fn extract_image_urls(scene: &Scene) -> Vec { } } } + Node::Tray(n) => { + collect_image_urls_from_paints(&n.fills, &mut urls); + collect_image_urls_from_paints(&n.strokes, &mut urls); + } // Group, InitialContainer, and Error nodes have no paint data. Node::Group(_) | Node::InitialContainer(_) | Node::Error(_) => {} } diff --git a/crates/grida-canvas/src/surface/ui/render.rs b/crates/grida-canvas/src/surface/ui/render.rs index 409263cf06..e0e4b4561f 100644 --- a/crates/grida-canvas/src/surface/ui/render.rs +++ b/crates/grida-canvas/src/surface/ui/render.rs @@ -1,6 +1,8 @@ use crate::cache::scene::SceneCache; +use crate::cg::types::Paint as CGPaint; use crate::devtools::surface_overlay::SurfaceOverlayConfig; use crate::node::scene_graph::SceneGraph; +use crate::node::schema::{Node, NodeId}; use crate::runtime::camera::Camera2D; use crate::runtime::font_repository::FontRepository; use crate::surface::state::SurfaceState; @@ -33,6 +35,22 @@ const TITLE_BAR_HEIGHT: f32 = 24.0; /// Minimum screen-space node width (logical px) to show a title bar. const MIN_TITLE_WIDTH: f32 = 20.0; +/// Padding inside the tray badge pill (logical px). +const BADGE_PAD_X: f32 = 8.0; +const BADGE_PAD_Y: f32 = 4.0; +/// Corner radius of the tray badge pill. +const BADGE_RADIUS: f32 = 4.0; +/// Extra gap (logical px) between the badge pill bottom and the tray content top. +const BADGE_GAP_Y: f32 = 6.0; +/// Fallback badge background color when the tray has no solid fill. +const BADGE_BG_FALLBACK: Color = Color::from_argb(30, 120, 120, 120); +/// Alpha multiplier applied to the badge background on hover to darken it. +const BADGE_HOVER_DARKEN: f32 = 0.9; +/// Darken factor for the adaptive badge stroke (slightly darker than the fill). +const BADGE_STROKE_DARKEN: f32 = 0.8; +/// Badge stroke width (logical px). +const BADGE_STROKE_WIDTH: f32 = 1.0; + thread_local! { static FONT_SIZE_METER: Font = Font::new( crate::fonts::embedded::typeface(crate::fonts::embedded::geistmono::BYTES), @@ -62,6 +80,57 @@ fn scaled_font(base: &Font, dpr: f32) -> Font { f } +/// Visual style variant for a node label. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum LabelVariant { + /// Plain text label (frame title style) — used for root containers. + Plain, + /// Badge with a background pill — used for Tray nodes. + Badge, +} + +/// A collected node label to render. +struct NodeLabel { + node_id: NodeId, + name: Option, + variant: LabelVariant, + /// For Badge variant: the tray's own background color (first solid fill), + /// used as the pill fill for good aesthetics. + badge_bg: Option, +} + +/// Extract the first active solid fill color from a `Paints` collection, +/// converted to a Skia `Color`. +fn first_solid_fill(paints: &crate::cg::types::Paints) -> Option { + for p in paints.iter() { + if let CGPaint::Solid(s) = p { + if s.active { + return Some(s.color.into()); + } + } + } + None +} + +/// Perceived brightness using BT.601 luma coefficients. +/// Returns `true` when the colour is light (→ use black text). +#[inline] +fn is_light_color(c: Color) -> bool { + let luma = 0.299 * c.r() as f32 + 0.587 * c.g() as f32 + 0.114 * c.b() as f32; + luma > 128.0 +} + +/// Darken a colour by multiplying each RGB channel by `factor` (0..1). +#[inline] +fn darken(c: Color, factor: f32) -> Color { + Color::from_argb( + c.a(), + (c.r() as f32 * factor) as u8, + (c.g() as f32 * factor) as u8, + (c.b() as f32 * factor) as u8, + ) +} + pub struct SurfaceUI; impl SurfaceUI { @@ -167,6 +236,67 @@ impl SurfaceUI { }); } + /// Collect nodes that should display a title label. + /// + /// Labels are shown for: + /// - Root nodes (containers and trays at the scene root) + /// - "Root-like" containers: containers that are direct children of a Tray + /// (since Tray children are treated as root-level frames) + fn collect_labeled_nodes(graph: &SceneGraph) -> Vec { + let mut labels = Vec::new(); + + for &root_id in graph.roots() { + let Ok(node) = graph.get_node(&root_id) else { + continue; + }; + + match node { + Node::Tray(tray) => { + let bg = first_solid_fill(&tray.fills); + + // Tray itself gets a badge label + labels.push(NodeLabel { + node_id: root_id, + name: graph.get_name(&root_id).map(str::to_owned), + variant: LabelVariant::Badge, + badge_bg: bg, + }); + + // Tray's direct children that are containers get plain labels + // (they are "root-like" — treated as top-level frames) + if let Some(children) = graph.get_children(&root_id) { + for &child_id in children { + if let Ok(child_node) = graph.get_node(&child_id) { + if matches!(child_node, Node::Container(_)) { + labels.push(NodeLabel { + node_id: child_id, + name: graph.get_name(&child_id).map(str::to_owned), + variant: LabelVariant::Plain, + badge_bg: None, + }); + } + } + } + } + } + Node::Container(_) => { + // Root containers get plain labels (frame title bars) + labels.push(NodeLabel { + node_id: root_id, + name: graph.get_name(&root_id).map(str::to_owned), + variant: LabelVariant::Plain, + badge_bg: None, + }); + } + _ => { + // Other root nodes (groups, shapes, etc.) — no label + } + } + } + + labels + } + /// Draw title labels above nodes and register hit regions for them. /// /// Uses the Paragraph API with `FontCollection` from the shared @@ -195,9 +325,33 @@ impl SurfaceUI { families.push(f.as_str()); } - // Iterate root nodes directly — no HashSet needed. - for &node_id in graph.roots() { - let world_bounds = match cache.geometry.get_world_bounds(&node_id) { + // Pre-scaled badge metrics (constant across labels) + let badge_pad_x = BADGE_PAD_X * dpr; + let badge_pad_y = BADGE_PAD_Y * dpr; + let badge_radius = BADGE_RADIUS * dpr; + let badge_gap_y = BADGE_GAP_Y * dpr; + + // Base text style — reused for every label (only foreground paint varies) + let wght_coord = skia_safe::font_arguments::variation_position::Coordinate { + axis: skia_safe::FourByteTag::from(('w', 'g', 'h', 't')), + value: 500.0, + }; + let variation_position = skia_safe::font_arguments::VariationPosition { + coordinates: &[wght_coord], + }; + let font_args = + skia_safe::FontArguments::new().set_variation_design_position(variation_position); + + let labeled_nodes = Self::collect_labeled_nodes(graph); + + for NodeLabel { + node_id, + name, + variant, + badge_bg, + } in &labeled_nodes + { + let world_bounds = match cache.geometry.get_world_bounds(node_id) { Some(b) => b, None => continue, }; @@ -213,19 +367,35 @@ impl SurfaceUI { continue; } - let label: &str = graph.get_name(&node_id).unwrap_or_else(|| { - graph - .get_node(&node_id) - .map(|n| n.type_label()) - .unwrap_or("?") + let label: &str = name.as_deref().unwrap_or_else(|| match variant { + LabelVariant::Badge => "Tray", + LabelVariant::Plain => "Container", }); - let is_selected = surface.selection.contains(&node_id); - let is_hovered = surface.hover.hovered() == Some(&node_id); - let color = if is_selected || is_hovered { - ACCENT_COLOR - } else { - MUTED_COLOR + let is_selected = surface.selection.contains(node_id); + let is_hovered = surface.hover.hovered() == Some(node_id); + + // ── Determine text colour ────────────────────────────── + // + // Badge: black or white, chosen by background luminance. + // Does NOT change on hover / select. + // Plain: muted grey normally, accent blue when highlighted. + let text_color = match variant { + LabelVariant::Badge => { + let bg = badge_bg.unwrap_or(BADGE_BG_FALLBACK); + if is_light_color(bg) { + Color::BLACK + } else { + Color::WHITE + } + } + LabelVariant::Plain => { + if is_selected || is_hovered { + ACCENT_COLOR + } else { + MUTED_COLOR + } + } }; // Build a single-line paragraph with ellipsis truncation @@ -242,48 +412,92 @@ impl SurfaceUI { skia_safe::font_style::Width::NORMAL, skia_safe::font_style::Slant::Upright, )); - // Set explicit wght variation so variable fallback fonts - // (e.g. Noto Sans KR) render at Medium (500) weight, - // matching the primary Geist font. - let wght_coord = skia_safe::font_arguments::variation_position::Coordinate { - axis: skia_safe::FourByteTag::from(('w', 'g', 'h', 't')), - value: 500.0, - }; - let variation_position = skia_safe::font_arguments::VariationPosition { - coordinates: &[wght_coord], - }; - let font_args = - skia_safe::FontArguments::new().set_variation_design_position(variation_position); text_style.set_font_arguments(&font_args); let mut fg_paint = Paint::default(); - fg_paint.set_color(color); + fg_paint.set_color(text_color); fg_paint.set_anti_alias(true); text_style.set_foreground_paint(&fg_paint); paragraph_style.set_text_style(&text_style); + // For Badge variant, limit paragraph width to leave room for padding + let para_max_width = match variant { + LabelVariant::Badge => (screen_width - badge_pad_x * 2.0).max(0.0), + LabelVariant::Plain => screen_width, + }; + let mut builder = textlayout::ParagraphBuilder::new(¶graph_style, fonts.font_collection()); builder.push_style(&text_style); builder.add_text(label); let mut paragraph = builder.build(); - paragraph.layout(screen_width); + paragraph.layout(para_max_width); - let text_width = paragraph.max_intrinsic_width().min(screen_width); + let text_width = paragraph.max_intrinsic_width().min(para_max_width); let para_height = paragraph.height(); let title_x = screen_tl[0]; let title_y = screen_tl[1] - title_height; - // Vertically center the paragraph within the title bar - let text_y = title_y + (title_height - para_height) * 0.5; - - paragraph.paint(canvas, Point::new(title_x, text_y)); - - let hit_rect = Rect::from_xywh(title_x, title_y, text_width, title_height); - hit_regions.push(HitRegion { - screen_rect: hit_rect, - action: OverlayAction::SelectNode(node_id), - }); + match variant { + LabelVariant::Badge => { + // Draw a background pill behind the label, with extra gap below + let pill_w = text_width + badge_pad_x * 2.0; + let pill_h = para_height + badge_pad_y * 2.0; + let pill_x = title_x; + // Position pill so its bottom edge sits `badge_gap_y` above the tray top + let pill_y = screen_tl[1] - pill_h - badge_gap_y; + + let pill_rect = Rect::from_xywh(pill_x, pill_y, pill_w, pill_h); + let rrect = RRect::new_rect_xy(pill_rect, badge_radius, badge_radius); + + // Badge background: tray fill colour. + // - hover → darken the background + // - select → no change (original) + let base_bg = badge_bg.unwrap_or(BADGE_BG_FALLBACK); + let bg_color = if is_hovered && !is_selected { + darken(base_bg, BADGE_HOVER_DARKEN) + } else { + base_bg + }; + + let mut bg_paint = Paint::default(); + bg_paint.set_color(bg_color); + bg_paint.set_style(PaintStyle::Fill); + bg_paint.set_anti_alias(true); + canvas.draw_rrect(rrect, &bg_paint); + + // Adaptive stroke — slightly darker than the fill + let stroke_color = darken(base_bg, BADGE_STROKE_DARKEN); + let mut stroke_paint = Paint::default(); + stroke_paint.set_color(stroke_color); + stroke_paint.set_style(PaintStyle::Stroke); + stroke_paint.set_stroke_width(BADGE_STROKE_WIDTH * dpr); + stroke_paint.set_anti_alias(true); + canvas.draw_rrect(rrect, &stroke_paint); + + // Draw text inside the pill + let text_x = pill_x + badge_pad_x; + let text_y = pill_y + badge_pad_y; + paragraph.paint(canvas, Point::new(text_x, text_y)); + + // Hit region covers the pill + hit_regions.push(HitRegion { + screen_rect: pill_rect, + action: OverlayAction::SelectNode(*node_id), + }); + } + LabelVariant::Plain => { + // Plain text label (original frame title style) + let text_y = title_y + (title_height - para_height) * 0.5; + paragraph.paint(canvas, Point::new(title_x, text_y)); + + let hit_rect = Rect::from_xywh(title_x, title_y, text_width, title_height); + hit_regions.push(HitRegion { + screen_rect: hit_rect, + action: OverlayAction::SelectNode(*node_id), + }); + } + } } } } diff --git a/crates/grida-dev/src/bench/load_bench.rs b/crates/grida-dev/src/bench/load_bench.rs index bd08901c0d..fe78129851 100644 --- a/crates/grida-dev/src/bench/load_bench.rs +++ b/crates/grida-dev/src/bench/load_bench.rs @@ -219,6 +219,7 @@ fn layout_diff(scene: &Scene, width: i32, height: i32, threshold: f32) { cg::node::schema::Node::BooleanOperation(_) => "BoolOp", cg::node::schema::Node::InitialContainer(_) => "ICB", cg::node::schema::Node::AttributedText(_) => "AttributedText", + cg::node::schema::Node::Tray(_) => "Tray", cg::node::schema::Node::Error(_) => "Error", }) .unwrap_or("?"); diff --git a/docs/wg/feat-tray/index.md b/docs/wg/feat-tray/index.md new file mode 100644 index 0000000000..57ba101e59 --- /dev/null +++ b/docs/wg/feat-tray/index.md @@ -0,0 +1,179 @@ +--- +title: Tray Node (tray) +format: md +tags: + - internal + - wg + - tray + - canvas + - scene-graph +--- + +# Tray Node - `tray` + +> A canvas-level organizational primitive for grouping design elements without participating in layout. + +| feature id | status | description | PRs | +| ---------- | ------ | --------------------------------------- | --- | +| `tray` | beta | Non-layout organizational grouping node | - | + +--- + +## Abstract + +A Tray is a lightweight, non-structural node that exists on the canvas to visually and hierarchically group Containers and other elements. It provides organizational clarity to a Scene without influencing how its children are positioned, sized, or rendered. + +Think of it as a labeled surface on the canvas. Things sit _on_ a Tray. The Tray itself does nothing to them. + +--- + +## Naming and Figma Mapping + +| Figma | Grida | Role | +| ----------- | --------- | -------------------------------------- | +| Page/Canvas | Scene | The root working surface | +| Frame | Container | A layout-participating structural node | +| Section | **Tray** | A non-layout organizational grouping | +| Group | Group | A temporary, transform-linked grouping | + +The name "Tray" was chosen to reflect the node's passive, infrastructure-like nature. A tray holds things without transforming them. It has no opinions about its contents. It is the most forgettable node in the tree -- by design. + +**Why not "Section"?** Section is vague and implies structural significance (like HTML `
`). **Why not "Board"/"Artboard"?** These carry legacy semantics from Sketch/Illustrator where artboards define export boundaries and clipping. Tray is deliberately mundane. + +--- + +## Node Hierarchy + +``` +Scene ++-- Tray ("Authentication flows") +| +-- Container (Login screen) +| +-- Container (Signup screen) +| +-- Container (Forgot password) ++-- Tray ("Dashboard") +| +-- Tray ("Dashboard -- Overview") <-- nested tray +| | +-- Container (Summary view) +| | +-- Container (Stats panel) +| +-- Container (Settings page) ++-- Container (Standalone prototype) +``` + +--- + +## Goals + +- **Organizational grouping**: Cluster related Containers under a named, visible boundary when a Scene has many root-level Containers. +- **Hierarchy participation**: A Tray is a real node in the scene graph. It appears in the layer panel, has children, and can be a parent. It is not metadata or an annotation. +- **Figma compatibility**: Maps directly to Figma `SECTION` on import/export. Round-trips cleanly. +- **Nestability**: Trays can contain other Trays. Nesting is shallow in practice but unrestricted in depth. +- **Minimal cognitive load**: Users should never need to think about what a Tray "does" -- it doesn't do anything. + +## Non-Goals + +- **No layout**: No auto-layout, flex, grid, or positioning logic. Children are freely placed. This is permanent, not a v1 limitation. +- **No rendering in output**: Invisible in exported designs. Canvas-only organizational aid. +- **No clipping**: Children can visually extend beyond the Tray's bounds. +- **No styling**: No fills, strokes, effects, blend modes. Label and boundary indicator only. +- **No nesting under non-Tray parents**: A Tray cannot be a child of Container, Group, or any other non-Tray node. Trays live at Scene level or nested under other Trays. This constraint is permanent. +- **Does not replace Group**: Groups are temporary, transform-linked wrappers. Trays are persistent, named organizational boundaries. They coexist. + +--- + +## Constraints + +| Rule | Detail | +| ------------- | --------------------------------------------------------- | +| **Parent** | Must be `Scene` or `Tray` | +| **Children** | Any node type: Container, Group, Text, Tray, shapes, etc. | +| **Layout** | None. Always `none`. Not configurable. | +| **Rendering** | Canvas-only. Never appears in exported output. | +| **Clipping** | None. Children can overflow. | +| **Styling** | Label and boundary only. No fills, strokes, or effects. | + +### Validation Invariants + +``` +Tray.parent in { Scene, Tray } +Tray.layout = none // invariant, not a default +Tray.fills = empty // not supported +Tray.strokes = empty // not supported +Tray.effects = empty // not supported +Tray.clip = false // invariant +``` + +--- + +## Implementation Status + +Tray is implemented as a first-class node type with visual rendering across all layers. + +### What's Done + +**Format Schema** (`format/grida.fbs`): + +- `Tray` in `NodeType` enum +- `TrayNode` table with SystemNodeTrait, LayerTrait, stroke_geometry, corner_radius, fill_paints, stroke_paints +- `TrayNode` in the `Node` union + +**Rust** (`crates/grida-canvas/`): + +- `NodeTypeTag::Tray`, `Node::Tray(TrayNodeRec)` in `node/schema.rs` +- `TrayNodeRec` struct: active, opacity, blend_mode, mask, rotation, position, layout_dimensions, corner_radius, corner_smoothing, fills, strokes, stroke_style, stroke_width +- `NodeFillsMixin`, `NodeGeometryMixin`, `to_own_shape()` impls +- Full wiring: `extract_layer_core`, all trait impls, `extract_geo_data` (`GeoNodeKind::Tray`) +- Geometry cache: Tray has explicit bounds (like Container), not derived from children (unlike Group) +- Painter: renders fills, strokes, corner_radius — like Container but simpler (no effects, no clipping, no render surface) +- `build_shape`: Tray case for rect/rrect/smooth-rrect based on corner_radius +- Layout: excluded from Taffy (`is_layout_node_tag` returns false); uses schema position/dimensions directly +- FBS encode/decode: full round-trip with fills, strokes, corner_radius, dimensions +- JSON format: `JSONNode::Tray` variant in `io_grida.rs` (defaults for visual fields until dedicated JSONTrayNode) +- Factory: `NodeFactory::create_tray_node()` with explicit dimensions +- Resources: image URL extraction from Tray fills/strokes + +**TypeScript** (`packages/grida-canvas-schema/grida.ts`): + +- `TrayNode` interface (extends IBaseNode, ISceneNode, IBlend, IPositioning, ICornerRadius, IStroke, IFill) +- Added to `LayerNode` union, `UnknownNode`, `NodePrototype` + +**Figma Import** (`packages/grida-canvas-io-figma/lib.ts`): + +- `SECTION` case produces `type: "tray"` with fills, strokes, corner_radius, positioning + +**FBS I/O** (`packages/grida-canvas-io/format.ts`): + +- Full encode/decode with fills, strokes, corner_radius, stroke_geometry + +**DOM Renderer** (`editor/grida-canvas-react-renderer-dom/`): + +- Tray uses "background" fill mode (renders fills visually) + +### What's Left (future work) + +- **Figma Export**: Map `tray` back to Figma `SECTION` on export. +- **Editor Layer Panel**: Distinct visual treatment for Tray nodes. Drag-and-drop parent constraints. +- **Canvas Rendering**: Tray-specific editor-only chrome (labeled boundary region, section name badge). +- **JSON format**: Dedicated `JSONTrayNode` struct with visual fields (currently uses `JSONGroupNode` with defaults). + +--- + +## FAQ + +**Why not a Container with `layout: none`?** +A Container without layout is still a Container -- it supports clipping, effects, layout containers, and can be nested anywhere. A Tray supports fills, strokes, and corner radius, but has no clipping, no effects, no layout, and lives only at the canvas root level. Modeling it as a restricted Container means every Container feature needs a "but not if it's a Tray" check. Separate primitives are cleaner. + +**Can a Container be a direct child of a Tray?** +Yes. That is the primary use case. + +**Can a Tray be empty?** +Yes. An empty Tray is valid -- a named region waiting for content. + +**Will Trays ever gain layout capabilities?** +No. If you need layout, use a Container. The entire value of a Tray is that it does nothing. + +--- + +## Related + +- [WG: Layout Model](../feat-layout) -- layout system that Tray explicitly opts out of +- [WG: Figma Import](../feat-fig) -- SECTION import/export pipeline +- [WG: Schema Naming Conventions](../feat-schema/naming-conventions.md) -- property naming standards diff --git a/editor/grida-canvas-react-renderer-dom/nodes/node.tsx b/editor/grida-canvas-react-renderer-dom/nodes/node.tsx index 0e67616827..32849fbba4 100644 --- a/editor/grida-canvas-react-renderer-dom/nodes/node.tsx +++ b/editor/grida-canvas-react-renderer-dom/nodes/node.tsx @@ -210,6 +210,7 @@ const fillings = { scene: "background", boolean: "none", group: "none", + tray: "background", tspan: "color", text: "color", container: "background", diff --git a/fixtures/test-grida/L0.grida b/fixtures/test-grida/L0.grida index 5ba9c4e7a9d14f48b9fca0f8dac7142a2f87dc07..8693319103c5b2441d85062b85bef6144da41449 100644 GIT binary patch delta 11494 zcmaJ{4^)&_cE2-#paCR;11jQVM5ETEqaiMvAb-A(H50>HV@X0RHN=vXb&Wflkd#pB zQ0uXr)TYEQb**)`ldNl4<61XMQcl+6(v(dn)|10pnp$hEYpLsc(482=5<~d z_-5$JIgj7>-o5YM`@8qO_x*w0f1W<@X8N2gB6?uMgSDHlOxWqTzWb92I~&;j%AAFf zxwyD+n}=>>cxY^#Xy>OGxJLBq6{4<-L<9dxl>M(n^{0sXeo9n!0`l(>?fy2=k-s9k z+yU7)VWWkpbu;EXLgZgdG;%-MQlhP;L}PHWGmYrKF}1J2L&fOVBDnE%1eoTbey@l6JRa&L4;`_I!V^R-Ul27S z`1t1t_9oH3F|ZpY8oYr3&}u&ck&h8*oM;H-%C8cg8bSMIgsAzKVDSs0j!TIAe~AwM z4-A0$!Jk7Bts7w%oQK{yOg;-oXvfbG75_Vo{~NM^>|~!NivA2^gUH;6L}!q#ihm)B zodkYBbnKsrO5caw0ixw-1Mk7#KN5{ceu4=|>DY0k2(A9dP<)r@Mjw>^0kU`C_-&-U zmni*52;heZ>=;u0_h5Atf&GAJ_)VhT!M46c~J!L z9ik?bZtQPSLbOQBexjnkLB!u8s@{t_z6wJY80`V`ZlWcq@wvYQQ?xTL!B7_r?Sk_^ zLm=Cs_s1~WjxxMR6ly~T{|LsPN9tQ)=Q*PF&w@l4_zle8hWXDR16y&u1@f;^g#1`I zv8Rz%wC<-+%Vrq)10WVt^^-_n6H)&YFtQ1ge;;XUB&z#87~BZ9kE28l(0L5HkAnFl z2)G^&eis?p06PyOnBT#ibH`r7*Fg9cOsYi?CI$_#s)1rP z(p!b}ttJYhwN}9K{cv(0biWLS<pcE?*Xb{=?ZOHE;>Re8g^;=l= z*av! zXdaC5IQIb_zV7*W!v3#wja64iwpMccAQq}x19=u?2!deGnt9jk$U4|%{Y zwiREZRmzUH6k{w~j((nVeM91i8RHI?IySQ*&y#unnj!}eJGS4N;-j1j5Fe&wX0U@* zKeQ?{7S9Z+F^PZQb@0xa69~J;`mghx&MM~eTGU_paXenxT5285@MZNuZA+6pY5G6n^s#u$)^a(#!@Sj~uo+ktF*mxGf&R?%hUV}{hX{!ZJjwthB$!OS8A zju}Ft%Z{=&zXl3o4QsOSC-3LR~igdqschowO6(|g1 zOh>NE`_d;;-mkBRF1vD zl}wMTHMCT!RRqQ-VMwY~{9S9^ZGpmmOx2O=YRw&$YCU}cx|}U{wX$GC>npc$zi+z^7 zCV6e{JSfM@5~%42HKQGzC>$>@C=-dgZFT9Ei7aeX2*!+NpspK-)4Hw3Yyb{k{i%5c z3^=sw4obFp_&J^RH|EUDyo~5i3HVp zmu|7h!bYiJ%%}rusw$jDvsz?Cr$L9Qo^^1NYLF@r8bnzvA0yko01Tl8QPsyXa#l4f zxm2RhbhC(Vtp_A@)=vWVD_R3v1)eNh-o?CXWA zR}Mlxgr;e6U(@oV5~&X#=ian(3j&3mP)ntCc0$qmP?<>38gS|E7FpPMRWN282Wnb9 zGKTjg#?C;k0c1LB9ZXbc72dbhY8P6ooCV{iRe-S~G);^9npQKGCf}F!(I%b{CLIz3gCu z6kZ_W%7>6@mmtaGCe?tkMl?-|`57$p?qF6u*42!Ko;vIHY5CqmUZJsHO-az`Mqe{-5Lwu05{wyLKux1n#_;wh zxWmi^G7X=D2^x5-YeOiShBrfn=q}C$$cNE14eo0i!D;rCr^1nAR>_=zuN!JQR9&1a zX~OEfvXEdk02&1!)5V{8OX z)8Rg*3!U;Ae4TtEegwIu)0t)E-sOv&Pjv-4?<+G2I>*u1basl&Z|oL~8ofYGCn{rj z4|niBAk#4%Owhq$e-PP*NPR-cTWx(fKae}5#w7gG+pLNCa|7e9p}N~_d-T)i`Mp^h zv!m5k5s?<;|2=x{_hrid--$n{RVdpa+UKpgte@ZzhJd<0g9b=6%J%1aIV)NkAl3{77AUh(8%r_M+E^ zri;maXi7HHbEe)2UWZ&4vkuu>;ENna07+{TTc-V{;u(Z3%`GIRJF8v=>aj){3mMIV zF=IDS7q(5tkZ%WP0ol)72OWa>Pi#XLJF+JkPRo&XI*g&qXqp=LHMO$bskejEkZWok z*|P1v(Ypi?rS;?STCs`3r1l>(~0KDwwp+G1v;0Ms|1~M=xaI$MHVuS2*!*d zpr+F=WBAzW;6@8_}AX9I|)wOzXqalG%gGge-=Qx2pu@$?h#juL4c43GTz5?B1?f zQ+MxT$eDXJ+F%~<-kEp9ILD!4zQ=lF(b6r`e9FucsOxOTC5W&wCKxsRjw$}imo096 zW`SU|=8$9aXtHhDsCftrQ-sX^SypB6q171aK+`1Jl!lOq zV;2e8r;jQ#iG^KQG3^=n>4+1!xt|WoCz9$lMC= zdBsyRdO+m3tvSdxh+Z*!1%0(^e0D^fufu>2;<=S8kpta`QpF#x?FpT!5v4S z9OrdFo}<%oLFvm%49-z2k^i8<-xfF>!)wI`W=#I1^V(WcWZP#&*k6r||HhUJJ-r$G z3gv9^!Dn*ZK^jzkl9D&Z34&YJ<&xqp+=vOrj6RrcoIf>rTvDi0+(EX1UJXGXwK4Y; zI?3A)L2?i)Xy1nWf3@g#mEd99eZScJbhah%kR z(9_9FpCc#PX2h5rcaVyepQPl~VyaGFw!|4X3Itf`0Alu;N)d_}c z&KKFs#QCKuL9CzWgXElE@;Ku7qufwQIFAhDH@wv{8 zuMff^tXI1mr{OZ@?T5@7yx$+W`{#I|!ytaDc&~CzbPl}e^B*2R0vDY>eV}wJ<4O7J zbIt9P;L6M{!FIC^0T%nGZC?+*zM<(t)vX4W2(;XDGA1J4hgoK1CX`cF(pHELj_Ia4zdlLsYbS7)cDRkXN4Vt zLQ)X;hMfhLh)Y&j>v}%-`CeMCMkNnjSkz2i{pHeE#VbcpOYi!Q{%?##y;gHDS7>vsuRxz=Hzt?#*9wH zT3R|ad8)wpzYfvEQ7Ra(i3#p7HMO*Ni7!Nr!bF``u}7TKDzaOR3fFTB z`Lv2v?)~f8RoAXQQ^maX{jk@imyQL_x%h0XO>|hV4;GP(YIkEkI!-R~4__+=nr|5tv;dxO|Cwbw$*F>-32N zwxxq?1iolcFtc_ukezRFFtIza#81H}M6x^Invm;TuC8&VFBz}1UR#{+r^l2x_Naf@ zTP^ZD`lx!A$X$kxUNb`r<+*hwEU{i98hnfxfj)+kQc{4aI-Oi&KCp%bL*@`G`q7^=mmlQn238M&#@m( zSMND5#yCp%o;!}osi_)v%~iE(H+wNgbkjTthau~=rTLK|8NsuggFFkCXvD$oK=!uF uLHDx03@hxF<%b;kX|)#c$%@%OYFf7ZKmHFN*LI=+ delta 7275 zcmZWt4Nz3q6+Wye#1LEo`4e?r@h6t1h=?R2;9FugY7=UxC59@ojIkNV*(B6)EETlW z(v(`0#}JyK$!du;2{k6s#2Av0ggTp)TAHzmHPjH}Seg=wmbBJVU%zwiyN_jI=JLIJ z&bjA)-#z!feeZQWAKm!l=(JcOx?}B~B~OlAx1w>qd7@>-0UIAnn^T*Hi^KmR>i8#7 z=jTLgJ|hZ!OjL1}sI&(`?-8BpB+58RRQEE`$n)?&PE__&qUs+I9eohKjmWW%sGt;M zd_?_AiH_Y&l$(j1>Bu>O=#QAE-W5gt6h%E2(TVGb`5)EN;Smh_F9w4|&s9wJZw&eu z0=^^)4igoFSm+8-*JY3yLXv-g$RN>*+JBXm5P0hsObdoRic{XL_=7)s+Wkaz6df$ zG2aUi^&7<3wnN!-SOGN8vk3e()E`37GjO(H;$LCi4q|13An-JZJ%ttdB~-P7*ppcP zUtl#_F#ZXQZ^rm0j5jeJB`SFgtGFNdbE4j#fj}cs7RuK36QXjo+yW;VQnZ2xh>|vA1*?$!J6QTn z$a6pTMI}UR0Dc?eD=_{(tiZjvE{FeHi2o+)-&BTWMXR_6jMlm!Eq_-kAA1jkb zv}Fkqq4{$c!-;lv5lEm>HdNh$K(xLrEZGd(Nb^5B=Zq#J~QEm6LyhhJ{>wKZ%}Jy9#MJJ_hZPoZOAu7A>V5mB*C{8~YZZ@3yapght4?YNN-_83%uyjPtBW zvhk3e`^dOC^2rT;7!os)C&+o%+&OXHloDkV{!P)A`NG7s!mVMpj%XfJSHxNl1U-e9 zhlUCRdCJCi-8khjjN6=N-m=!~I`M+JaFREr0v4^pg^2sH46nFkacgTN3%O7U;uZ3s z@e1mA#bU^)7B)jh6_~`{U|AfOgvMhs(3-@LjcFt202g1oU~)+zklC%Y(O&g*S@y#s zPgOVvw@$=G#hC|Ed^v9PlF)RaxzAosRXNGBcGpqS<)MHSnHM4BMRQ+_cS0xDkZU{> zi8*w%#!G0DO5xOa1Tk7|mk{wA-GV`57|05_#(m1RqTj}yK+e6##?d+Q=%SiqvedZr zR?8|jcTb!(B?*i%LfBWuNSTWZ!fcH(mc?84__5rgCz_iJ4+MjS#Tc#2tC6Rz$GANB z%!i^qu7;P)Yq8#xEZDRZUFf|NrO@A9BY7w)ywKHQ@ruWgQ6hZ&zFcr=lIvz47xEG$ z@w|gJj>=s^rHF+~PUt!F?dhwFPQcQErt8Lit(><{_yJ{Ahv3%-6xpPBM=<-?;w`3T zmB%R_ub5U`CO*GWAs963fx37Bv3XYPuyHbwb0^q1swS7JV}+|KUf(jyDv;u}DYF!B z_y$$H?l4=%WL3QPLZL#I$HnWxXk9doJZ%z|;1qmPywv07qc?g}I*?0C(Zx%SmEvvg zl{{mM7mXY`Uhx<*&IljBuM56DxjyE57|3((v~g7GV(LX4TvE8WCFU(t*3Mc38Kr2t za@t2!jPA0-ML#=2RRt`7y+J{vNr;uC1Vzb2;F!BXM zMkP=elIc`$Tj47M_I&YM-z6wub%QpJs>`8LP}Az_)O{|h1iFGm=CUcb&+3Gq>1(On z*HYa#DXINow)~sMR+yy@zbaB&5Ed!*%48+=l1kx7?G1}<6JNk+7YrF^fLbckX?$$E zF9h6_>GrJ$veZpBj!IobU2&sQhnFfD;Zk!!o#|_-+}Bd;1&Iu*VG@e!Sd9G3c_jiXYtDIe6}ie>+B z0`@u3kCu(9zCQJ)yh`Pl4SRHo#KNop>WT)`9K}5TJjMf}>Q0(h+}@~6^V2zr6Lz8W z*rJ;sR*77xh~VHO7|56<_GDifPzxv)8$>DeowH*v*f+%^WMTITBsX*mtBHKOSq#(hjBlFm*YdvN$Mrl~`I zvhooYN&oRFYB%jwDV*I@6Bb({zJRet5YHz-Eu~UyA!Cam(KSe7*)H@M9Vtuka1ClO zX(>WE2^s>}O&=#N&*_H02TjZ3zLu4NN<=<@oPEPwmE=vSL0F`$58X=E`znPat1~RN zL3{yYzhKBX0o1bE#D;eW#yW3FA&}*)w9%1;$M3^xV~eaDd&7jwa>JH{re$$o%c@M` z>(PfzzRq~d+&9ge(uuT@veFZjtaz2ekrj=;j_npY0ppBd$QS`?S^c=K+%WCtK?S)vQuDvKqr;%fuHjDg;ADJy6REhz+mhf_r?E zfh;S*Mn@K2`Km!p%et>uhvn<=~AqWmJ{{noM~F{{pyWtbTU+ zJs#LH(X=e?YgvIb%i_DAZY(Pve&&H2tK^Q7E~5`wD0<1x0P~#%(zJPpM3f z{3i6Z{0i}Tj4HvP(FoM?cZdxOZDRtEbEn(r%z1Y~n7gy*c<14YW#aZlJb^BSzJD&9&O5|AAc@F@Z&UKEfWwn8REPIYHxp(V zQHRZ`ixR(~mJ?sQWLfc~Bp5Q**!}aEhgn>*(UHy{nxFzMsoV$ZI*RxkLg7*Lj-lxS zavzbh95ieEkHS;%>jL^`m~YNao^l8Tcw&1(bE79MzeLj173~LVHRWRS8x{b~3$*&Q>@>+_OpKg2{~ zWbuWC=uMTzIXuVE*Q9I2=QrvFLq!WAPWnCFqh z6Aj9-Xj%^UwVblq<8Sd-kVDHsKKTxCEpjgCxuE89^qfUs>j{d_ZyXj389hL)r$cP` zY-!^;Co|8i z%3BBzPXIq%K|Y@FnvIT3EM)}FK zIZClxPFOB_`DnVz+(#BUVfC5gPnZ{e=3b1}?UfU@5XpIF#Vp7CZ0^l>x)!K3nF!ab zF(4!Y#;{<}$gorKoibLYP4>A3gDVc&IiEA9<|oF-fe34mT1~F_0!D#g(5M1xHKoe7qTI&M?c7%)=fL4A13NHPhpY2md~=e+RjBPO4%6gkES^Kl(TD5&T;AeK+9cQ5 zui@Qc;Z3+EYWB4XhJ20SlWksJmbkVdwP%sE;)hw VZ2!r5rm@iL0)W@cbp<7V{y+AOPK^Kn diff --git a/format/grida.fbs b/format/grida.fbs index 4bc5ac455d..9bc7924af4 100644 --- a/format/grida.fbs +++ b/format/grida.fbs @@ -260,6 +260,7 @@ enum NodeType : ubyte { // groups and containers Group, + Tray, InitialContainer, Container, BooleanOperation, @@ -1381,6 +1382,21 @@ table GroupNode { layer:LayerTrait (required); } +/// Node variant: Tray. +/// +/// A canvas-level organizational primitive (maps to Figma SECTION). +/// Has explicit dimensions, fills, strokes, corner radius. +/// No effects, no layout, no clipping. +/// Children are freely placed and treated as root-level containers. +table TrayNode { + node: SystemNodeTrait (required); + layer:LayerTrait (required); + stroke_geometry:RectangularStrokeGeometryTrait; + corner_radius:RectangularCornerRadiusTrait; + fill_paints:[PaintStackItem]; + stroke_paints:[PaintStackItem]; +} + // TODO: review the shape, should boolop have direct child ? /// Node variant: Boolean operation. table BooleanOperationNode { @@ -1561,6 +1577,7 @@ union Node { TextSpanNode, PathNode, // SVG path data (render-only, no vector network) AttributedTextNode, + TrayNode, } // ----------------------------------------------------------------------------- diff --git a/packages/grida-canvas-io-figma/lib.ts b/packages/grida-canvas-io-figma/lib.ts index 0f5cb0ba7c..a1edecf69a 100644 --- a/packages/grida-canvas-io-figma/lib.ts +++ b/packages/grida-canvas-io-figma/lib.ts @@ -1579,14 +1579,12 @@ export namespace iofigma { blendMode: "PASS_THROUGH", }), ...positioning_trait(node, parent), - ...fills_trait(node.fills, context, imageRefsUsed), + ...fills_trait(node.fills ?? [], context, imageRefsUsed), ...stroke_trait(node, context, imageRefsUsed), - ...style_trait({}), - ...corner_radius_trait({ cornerRadius: 0 }), - ...container_layout_trait({}, false), - type: "container", - clips_content: false, - } satisfies grida.program.nodes.ContainerNode; + ...rectangular_stroke_width_trait(node), + ...corner_radius_trait(node), + type: "tray", + } satisfies grida.program.nodes.TrayNode; } // case "COMPONENT": diff --git a/packages/grida-canvas-io/format.ts b/packages/grida-canvas-io/format.ts index 32df341071..cd863459a3 100644 --- a/packages/grida-canvas-io/format.ts +++ b/packages/grida-canvas-io/format.ts @@ -383,6 +383,7 @@ export namespace format { ["rectangle", fbs.NodeType.Rectangle], ["tspan", fbs.NodeType.TextSpan], ["group", fbs.NodeType.Group], + ["tray", fbs.NodeType.Tray], ["ellipse", fbs.NodeType.Ellipse], ["line", fbs.NodeType.Line], ["vector", fbs.NodeType.Vector], @@ -400,6 +401,7 @@ export namespace format { [fbs.NodeType.Rectangle, "rectangle"], [fbs.NodeType.TextSpan, "tspan"], [fbs.NodeType.Group, "group"], + [fbs.NodeType.Tray, "tray"], [fbs.NodeType.Ellipse, "ellipse"], [fbs.NodeType.Line, "line"], [fbs.NodeType.Vector, "vector"], @@ -2126,6 +2128,61 @@ export namespace format { nodeType = fbs.Node.GroupNode; break; } + case "tray": { + const trayNode = node as grida.program.nodes.TrayNode; + + // Encode traits and paints + const strokeGeometryOffset = + format.shape.encode.rectangularStrokeGeometryTrait(builder, { + stroke_cap: trayNode.stroke_cap, + stroke_join: trayNode.stroke_join, + stroke_width: trayNode.stroke_width, + stroke_dash_array: trayNode.stroke_dash_array, + rectangular_stroke_width_top: + trayNode.rectangular_stroke_width_top, + rectangular_stroke_width_right: + trayNode.rectangular_stroke_width_right, + rectangular_stroke_width_bottom: + trayNode.rectangular_stroke_width_bottom, + rectangular_stroke_width_left: + trayNode.rectangular_stroke_width_left, + }); + const cornerRadiusOffset = + format.shape.encode.rectangularCornerRadiusTrait(builder, { + rectangular_corner_radius_top_left: + trayNode.rectangular_corner_radius_top_left, + rectangular_corner_radius_top_right: + trayNode.rectangular_corner_radius_top_right, + rectangular_corner_radius_bottom_left: + trayNode.rectangular_corner_radius_bottom_left, + rectangular_corner_radius_bottom_right: + trayNode.rectangular_corner_radius_bottom_right, + corner_smoothing: trayNode.corner_smoothing, + }); + const fillPaintsFiltered = paints(trayNode, "fill"); + const fillPaintsOffset = format.paint.encode.fillPaints( + builder, + fillPaintsFiltered, + fbs.TrayNode.createFillPaintsVector + ); + const strokePaintsFiltered = paints(trayNode, "stroke"); + const strokePaintsOffset = format.paint.encode.strokePaints( + builder, + strokePaintsFiltered, + fbs.TrayNode.createStrokePaintsVector + ); + + fbs.TrayNode.startTrayNode(builder); + fbs.TrayNode.addNode(builder, systemNodeTraitOffset); + fbs.TrayNode.addLayer(builder, layerOffset); + fbs.TrayNode.addStrokeGeometry(builder, strokeGeometryOffset); + fbs.TrayNode.addCornerRadius(builder, cornerRadiusOffset); + fbs.TrayNode.addFillPaints(builder, fillPaintsOffset); + fbs.TrayNode.addStrokePaints(builder, strokePaintsOffset); + nodeOffset = fbs.TrayNode.endTrayNode(builder); + nodeType = fbs.Node.TrayNode; + break; + } default: { // Fallback to UnknownNode (only has SystemNodeTrait, no layer) fbs.UnknownNode.startUnknownNode(builder); @@ -6171,6 +6228,69 @@ export namespace format { ...(effects || {}), } satisfies grida.program.nodes.GroupNode; } + + /** + * Decodes TrayNode — has fills, strokes, corner radius, explicit dimensions. + */ + export function tray( + n: fbs.TrayNode, + id: string, + systemNode: fbs.SystemNodeTrait, + layer: fbs.LayerTrait | null, + opacity: number, + layoutFields: ReturnType, + effects?: grida.program.nodes.i.IEffects + ): grida.program.nodes.TrayNode { + const strokeGeometry = n.strokeGeometry(); + const cornerRadius = n.cornerRadius(); + const fillPaints = format.paint.decode.fillPaints(n); + const strokePaints = format.paint.decode.strokePaints(n); + + const strokeGeometryProps = + format.shape.decode.rectangularStrokeGeometryTrait(strokeGeometry); + const cornerRadiusProps = + format.shape.decode.rectangularCornerRadiusTrait(cornerRadius); + + const baseName = systemNode.name() ?? "node"; + + return { + type: "tray", + id, + name: baseName, + active: systemNode.active(), + locked: systemNode.locked(), + opacity, + ...(fillPaints ? { fill_paints: fillPaints } : {}), + ...(strokePaints ? { stroke_paints: strokePaints } : {}), + stroke_width: + format.shape.decode.deriveStrokeWidth(strokeGeometryProps), + stroke_cap: strokeGeometryProps.stroke_cap, + stroke_join: strokeGeometryProps.stroke_join, + ...(strokeGeometryProps.stroke_dash_array + ? { stroke_dash_array: strokeGeometryProps.stroke_dash_array } + : {}), + rectangular_corner_radius_top_left: + cornerRadiusProps.rectangular_corner_radius_top_left, + rectangular_corner_radius_top_right: + cornerRadiusProps.rectangular_corner_radius_top_right, + rectangular_corner_radius_bottom_left: + cornerRadiusProps.rectangular_corner_radius_bottom_left, + rectangular_corner_radius_bottom_right: + cornerRadiusProps.rectangular_corner_radius_bottom_right, + corner_smoothing: cornerRadiusProps.corner_smoothing, + rectangular_stroke_width_top: + strokeGeometryProps.rectangular_stroke_width_top, + rectangular_stroke_width_right: + strokeGeometryProps.rectangular_stroke_width_right, + rectangular_stroke_width_bottom: + strokeGeometryProps.rectangular_stroke_width_bottom, + rectangular_stroke_width_left: + strokeGeometryProps.rectangular_stroke_width_left, + ...layoutFields, + layout_positioning: layoutFields.layout_positioning ?? "relative", + ...(effects || {}), + } satisfies grida.program.nodes.TrayNode; + } } /** @@ -6436,6 +6556,17 @@ export namespace format { decodedEffects ); break; + case fbs.Node.TrayNode: + nodes[id] = nodeTypes.tray( + typedNode as fbs.TrayNode, + id, + systemNode, + layer, + opacity, + layoutFields, + decodedEffects + ); + break; default: nodes[id] = nodeTypes.group( typedNode as fbs.GroupNode, diff --git a/packages/grida-canvas-schema/grida.ts b/packages/grida-canvas-schema/grida.ts index fec5dabdd1..bb4f9bc4ee 100644 --- a/packages/grida-canvas-schema/grida.ts +++ b/packages/grida-canvas-schema/grida.ts @@ -1221,6 +1221,7 @@ export namespace grida.program.nodes { export type LayerNode = | BooleanPathOperationNode | GroupNode + | TrayNode | TextSpanNode | AttributedTextNode | ImageNode @@ -1288,6 +1289,7 @@ export namespace grida.program.nodes { export type UnknownNode = Omit< Partial & Partial & + Partial & Partial & Partial & Partial & @@ -1329,6 +1331,10 @@ export namespace grida.program.nodes { Omit, __base_scene_node_properties | "children"> & __IPrototypeNodeChildren >; + export type TrayNodePrototype = __TPrototypeNode< + Omit, __base_scene_node_properties | "children"> & + __IPrototypeNodeChildren + >; export type TextNodePrototype = __TPrototypeNode< Omit, __base_scene_node_properties> >; @@ -1372,6 +1378,7 @@ export namespace grida.program.nodes { export type NodePrototype = | BooleanPathOperationNodePrototype | GroupNodePrototype + | TrayNodePrototype | TextNodePrototype | ImageNodePrototype | VideoNodePrototype @@ -2227,6 +2234,25 @@ export namespace grida.program.nodes { // } + /** + * Tray Node — canvas-level organizational primitive (Figma SECTION). + * + * Has explicit dimensions, fills, strokes, corner radius. + * No effects, no layout, no clipping. + * Children are freely placed and treated as root-level containers. + */ + export interface TrayNode + extends i.IBaseNode, + i.ISceneNode, + i.IBlend, + i.IPositioning, + i.ICornerRadius, + i.IStroke, + i.IFill { + type: "tray"; + // + } + /** * Boolean Path Operation Node * @@ -2806,6 +2832,7 @@ export namespace grida.program.nodes { // TODO: case "boolean": case "group": + case "tray": case "component": case "instance": case "template_instance": { From 338f786ecb4c943f82aaf431c301c38dc5e9b830 Mon Sep 17 00:00:00 2001 From: Universe Date: Sun, 29 Mar 2026 04:27:41 +0900 Subject: [PATCH 02/15] generate nocache --- packages/grida-format/turbo.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/grida-format/turbo.json b/packages/grida-format/turbo.json index e57a702c74..a9a970e665 100644 --- a/packages/grida-format/turbo.json +++ b/packages/grida-format/turbo.json @@ -2,6 +2,7 @@ "extends": ["//"], "tasks": { "generate": { + "cache": false, "inputs": ["$TURBO_DEFAULT$", "../../format/grida.fbs"], "outputs": ["src/grida.ts", "src/grida/**"] }, From 1ca70d74385eea5498fa2a1119da7006913e6847 Mon Sep 17 00:00:00 2001 From: Universe Date: Sun, 29 Mar 2026 04:28:06 +0900 Subject: [PATCH 03/15] feat(grida-dev): add SurfaceUI overlay measurement for benchmarks Introduce a new `--overlay` flag for benchmarks to measure the combined cost of content rendering and SurfaceUI overlay drawing. This includes updates to the benchmark arguments, runner, and relevant documentation. The overlay cost is now accounted for in performance metrics, allowing for better analysis of rendering performance with UI elements. Additionally, ensure GPU benchmarks are run sequentially to avoid unreliable results due to resource contention. --- .agents/skills/cg-perf/SKILL.md | 73 ++++++++ crates/grida-canvas/src/runtime/scene.rs | 11 ++ crates/grida-canvas/src/surface/ui/render.rs | 127 +++++++++----- crates/grida-dev/src/bench/args.rs | 7 + crates/grida-dev/src/bench/runner.rs | 174 +++++++++++++++---- 5 files changed, 323 insertions(+), 69 deletions(-) diff --git a/.agents/skills/cg-perf/SKILL.md b/.agents/skills/cg-perf/SKILL.md index c5c791d540..711d8de4f1 100644 --- a/.agents/skills/cg-perf/SKILL.md +++ b/.agents/skills/cg-perf/SKILL.md @@ -134,6 +134,36 @@ reports `min/p50/p95/p99/MAX` plus per-stage breakdown and settle cost. | `frameloop` | 16/50/80/120/200/300/500ms interval | **Real FrameLoop path** — the only bench that captures stable-frame jank during panning (see below) | | `resize` | alternating viewport sizes | `--resize` flag. Measures `resize()` + `redraw()` cost per cycle (layout rebuild + cache invalidation + repaint) | +**SurfaceUI overlay measurement (`--overlay`):** + +By default, benchmarks measure content rendering only — the SurfaceUI +overlay (frame titles, node badges, hit regions) is **not** included. +Pass `--overlay` to include overlay drawing after each content flush, +matching the real `Application::frame()` pipeline where +`draw_and_flush_devtools_overlay()` runs after `Renderer::flush()`. + +```sh +# A/B test: content only vs content + overlay (MUST be sequential, never parallel) +cargo run -p grida-dev --release -- bench ./fixtures/test-grida/L0.grida --scene 22 --frames 200 && \ +cargo run -p grida-dev --release -- bench ./fixtures/test-grida/L0.grida --scene 22 --frames 200 --overlay + +# Bulk report with overlay +cargo run -p grida-dev --release -- bench-report ./fixtures/ --frames 100 --overlay --output overlay.json +``` + +The overlay cost is opt-in because it is a devtools feature, not user +content. Overlay cost scales with visible labeled nodes — viewport +culling skips off-screen labels, so zoomed-in views are nearly free. +At fit-zoom on large scenes (yrr-main, 437 labels visible), overlay +adds ~1.8ms per frame (paragraph layout dominates). At typical editing +zoom, the cost drops to ~190µs or less. + +| Question | Flag | +| ---------------------------------------- | -------------- | +| Is content rendering fast enough? | (no flag) | +| Is the overlay adding visible latency? | `--overlay` | +| Compare content-only vs content+overlay? | Run both, diff | + The `realtime` scenarios use actual `thread::sleep()` between frames and simulate the native viewer's 240Hz tick thread + settle countdown. These produce frame timings that match what users actually see, @@ -187,6 +217,7 @@ of scenes, configs, and operations. The naming convention is | Are there frame drops during gestures? | Check `p99` and `MAX` in scenario stats | | Is slow panning janky (stable frame spikes)? | `frameloop` scenarios (real FrameLoop path) | | Is resize janky? | Single-scene GPU bench with `--resize` | +| Is the SurfaceUI overlay causing slowdowns? | A/B with `--overlay` flag on GPU bench | --- @@ -194,6 +225,11 @@ of scenes, configs, and operations. The naming convention is **Every performance change follows this sequence. No exceptions.** +**Critical: all GPU benchmarks must run sequentially.** Never run two +bench processes at the same time — GPU contention, CPU cache thrashing, +and memory bandwidth competition produce unreliable numbers. Chain A/B +runs with `&&`. + ### Step 1: Baseline Run the bulk benchmark report BEFORE any changes. Save the JSON output @@ -418,6 +454,29 @@ loading a scene. These are failure modes learned from experience. Each one has caused real bugs or wasted time. +### Never run GPU benchmarks in parallel + +GPU benchmarks must run **sequentially**, one at a time. Running two +bench processes simultaneously on the same machine causes GPU pipeline +contention, CPU cache thrashing, and memory bandwidth competition — +all of which distort timing data. When doing A/B comparisons (e.g. +with/without `--overlay`, before/after an optimization), always chain +the runs with `&&`: + +```sh +# CORRECT — sequential +cargo run -p grida-dev --release -- bench file.grida --scene 0 --frames 100 && \ +cargo run -p grida-dev --release -- bench file.grida --scene 0 --frames 100 --overlay + +# WRONG — parallel (results are unreliable) +cargo run ... --frames 100 & +cargo run ... --frames 100 --overlay & +``` + +This applies to all GPU benchmark invocations: `bench`, `bench-report`, +and any combination thereof. Criterion (CPU raster) is less sensitive +but should still be run alone for best accuracy. + ### GPU and raster backends behave differently An optimization that helps on GPU may hurt on raster, and vice versa. @@ -482,6 +541,20 @@ frame gets a cache hit. Without recapture, every frame after settle is also a full draw, producing 7fps instead of 100+fps. The capture guard should be `if self.backend.is_gpu()` — NOT `if !plan.stable`. +### SurfaceUI overlay is not free + +The SurfaceUI overlay (frame titles, node badges, hit regions) runs +after content `flush()` and requires a second GPU flush. The overlay +cost is dominated by Skia paragraph creation (one per visible label) — +viewport culling skips off-screen labels, and style objects are hoisted +out of the per-label loop. On scenes with many labeled nodes at +fit-zoom (e.g. yrr-main with 437 labels), the overlay adds ~1.8ms per +frame. At typical editing zoom, most labels are culled and cost drops +to ~190µs. Standard benchmarks exclude overlay by default — use +`--overlay` to include it. If the app feels slower after adding new +overlay features (badges, labels, decorations), use the A/B overlay +benchmark to quantify the regression before optimizing. + ### Layout is the cold-start bottleneck, not rendering For large documents (100K+ nodes), `load_scene` dominates cold start diff --git a/crates/grida-canvas/src/runtime/scene.rs b/crates/grida-canvas/src/runtime/scene.rs index ac184e3bbc..dcdc7cb29e 100644 --- a/crates/grida-canvas/src/runtime/scene.rs +++ b/crates/grida-canvas/src/runtime/scene.rs @@ -1397,6 +1397,17 @@ impl Renderer { } } + /// Submit any pending overlay draws to the GPU. + /// + /// Call this after drawing overlays on [`Self::canvas()`] to make the + /// additional pixels visible. This is the same GPU submit that + /// `Application::draw_and_flush_devtools_overlay` performs after painting + /// selection outlines, frame title badges, and the size meter. + pub fn flush_overlay(&mut self) { + let surface = unsafe { &mut *self.backend.get_surface() }; + Self::gpu_flush(surface); + } + /// Invoke the request redraw callback. fn request_redraw(&self) { if let Some(cb) = &self.request_redraw { diff --git a/crates/grida-canvas/src/surface/ui/render.rs b/crates/grida-canvas/src/surface/ui/render.rs index e0e4b4561f..0f173ac6a4 100644 --- a/crates/grida-canvas/src/surface/ui/render.rs +++ b/crates/grida-canvas/src/surface/ui/render.rs @@ -317,6 +317,17 @@ impl SurfaceUI { let view = camera.view_matrix(); let font_size = FRAME_TITLE_FONT_SIZE * dpr; + // Screen-space viewport bounds for culling (with generous margin for + // title bars that sit above / below the node). + let vp_size = camera.get_size(); + let screen_w = vp_size.width * dpr; + let screen_h = vp_size.height * dpr; + let margin = title_height + BADGE_GAP_Y * dpr + 50.0 * dpr; + let vp_top = -margin; + let vp_bottom = screen_h + margin; + let vp_left = -margin; + let vp_right = screen_w + margin; + // Build font families list: primary + user fallback fonts let fallbacks = fonts.user_fallback_families(); let mut families: Vec<&str> = Vec::with_capacity(1 + fallbacks.len()); @@ -331,7 +342,7 @@ impl SurfaceUI { let badge_radius = BADGE_RADIUS * dpr; let badge_gap_y = BADGE_GAP_Y * dpr; - // Base text style — reused for every label (only foreground paint varies) + // ── Hoisted style objects (shared across all labels) ──────────── let wght_coord = skia_safe::font_arguments::variation_position::Coordinate { axis: skia_safe::FourByteTag::from(('w', 'g', 'h', 't')), value: 500.0, @@ -342,6 +353,48 @@ impl SurfaceUI { let font_args = skia_safe::FontArguments::new().set_variation_design_position(variation_position); + let font_style = skia_safe::FontStyle::new( + skia_safe::font_style::Weight::MEDIUM, + skia_safe::font_style::Width::NORMAL, + skia_safe::font_style::Slant::Upright, + ); + + // Base paragraph style — shared across all labels (immutable fields). + let mut base_para_style = textlayout::ParagraphStyle::new(); + base_para_style.set_max_lines(1); + base_para_style.set_ellipsis("\u{2026}"); + base_para_style.set_apply_rounding_hack(false); + + // Pre-create text-colour paints (only 4 possible colours) + let mut paint_black = Paint::default(); + paint_black.set_color(Color::BLACK); + paint_black.set_anti_alias(true); + + let mut paint_white = Paint::default(); + paint_white.set_color(Color::WHITE); + paint_white.set_anti_alias(true); + + let mut paint_muted = Paint::default(); + paint_muted.set_color(MUTED_COLOR); + paint_muted.set_anti_alias(true); + + let mut paint_accent = Paint::default(); + paint_accent.set_color(ACCENT_COLOR); + paint_accent.set_anti_alias(true); + + // Pre-create badge stroke paint (reuse, only change color per label) + let mut stroke_paint = Paint::default(); + stroke_paint.set_style(PaintStyle::Stroke); + stroke_paint.set_stroke_width(BADGE_STROKE_WIDTH * dpr); + stroke_paint.set_anti_alias(true); + + // Pre-create badge fill paint (reuse, only change color per label) + let mut fill_paint = Paint::default(); + fill_paint.set_style(PaintStyle::Fill); + fill_paint.set_anti_alias(true); + + let font_collection = fonts.font_collection(); + let labeled_nodes = Self::collect_labeled_nodes(graph); for NodeLabel { @@ -356,6 +409,7 @@ impl SurfaceUI { None => continue, }; + // Transform node's top-left and top-right to screen space let screen_tl = math2::vector2::transform([world_bounds.x, world_bounds.y], &view); let screen_tr = math2::vector2::transform( [world_bounds.x + world_bounds.width, world_bounds.y], @@ -367,6 +421,23 @@ impl SurfaceUI { continue; } + // ── Viewport culling ─────────────────────────────────────── + // The title bar sits above the node (Plain) or above with a gap + // (Badge). If the title region is entirely outside the viewport, + // skip the expensive paragraph creation. + let label_top = screen_tl[1] - title_height - badge_gap_y; + let label_bottom = screen_tl[1]; + let label_left = screen_tl[0]; + let label_right = screen_tl[0] + screen_width; + + if label_bottom < vp_top + || label_top > vp_bottom + || label_right < vp_left + || label_left > vp_right + { + continue; + } + let label: &str = name.as_deref().unwrap_or_else(|| match variant { LabelVariant::Badge => "Tray", LabelVariant::Plain => "Container", @@ -375,48 +446,35 @@ impl SurfaceUI { let is_selected = surface.selection.contains(node_id); let is_hovered = surface.hover.hovered() == Some(node_id); - // ── Determine text colour ────────────────────────────── - // - // Badge: black or white, chosen by background luminance. - // Does NOT change on hover / select. - // Plain: muted grey normally, accent blue when highlighted. - let text_color = match variant { + // ── Determine text colour paint ───────────────────────────── + let fg_paint = match variant { LabelVariant::Badge => { let bg = badge_bg.unwrap_or(BADGE_BG_FALLBACK); if is_light_color(bg) { - Color::BLACK + &paint_black } else { - Color::WHITE + &paint_white } } LabelVariant::Plain => { if is_selected || is_hovered { - ACCENT_COLOR + &paint_accent } else { - MUTED_COLOR + &paint_muted } } }; - // Build a single-line paragraph with ellipsis truncation - let mut paragraph_style = textlayout::ParagraphStyle::new(); - paragraph_style.set_max_lines(1); - paragraph_style.set_ellipsis("\u{2026}"); - paragraph_style.set_apply_rounding_hack(false); - + // Build text style with the chosen foreground paint let mut text_style = textlayout::TextStyle::new(); text_style.set_font_size(font_size); text_style.set_font_families(&families); - text_style.set_font_style(skia_safe::FontStyle::new( - skia_safe::font_style::Weight::MEDIUM, - skia_safe::font_style::Width::NORMAL, - skia_safe::font_style::Slant::Upright, - )); + text_style.set_font_style(font_style); text_style.set_font_arguments(&font_args); - let mut fg_paint = Paint::default(); - fg_paint.set_color(text_color); - fg_paint.set_anti_alias(true); - text_style.set_foreground_paint(&fg_paint); + text_style.set_foreground_paint(fg_paint); + + // Clone the base paragraph style and apply text style + let mut paragraph_style = base_para_style.clone(); paragraph_style.set_text_style(&text_style); // For Badge variant, limit paragraph width to leave room for padding @@ -425,8 +483,7 @@ impl SurfaceUI { LabelVariant::Plain => screen_width, }; - let mut builder = - textlayout::ParagraphBuilder::new(¶graph_style, fonts.font_collection()); + let mut builder = textlayout::ParagraphBuilder::new(¶graph_style, font_collection); builder.push_style(&text_style); builder.add_text(label); let mut paragraph = builder.build(); @@ -460,19 +517,11 @@ impl SurfaceUI { base_bg }; - let mut bg_paint = Paint::default(); - bg_paint.set_color(bg_color); - bg_paint.set_style(PaintStyle::Fill); - bg_paint.set_anti_alias(true); - canvas.draw_rrect(rrect, &bg_paint); + fill_paint.set_color(bg_color); + canvas.draw_rrect(rrect, &fill_paint); // Adaptive stroke — slightly darker than the fill - let stroke_color = darken(base_bg, BADGE_STROKE_DARKEN); - let mut stroke_paint = Paint::default(); - stroke_paint.set_color(stroke_color); - stroke_paint.set_style(PaintStyle::Stroke); - stroke_paint.set_stroke_width(BADGE_STROKE_WIDTH * dpr); - stroke_paint.set_anti_alias(true); + stroke_paint.set_color(darken(base_bg, BADGE_STROKE_DARKEN)); canvas.draw_rrect(rrect, &stroke_paint); // Draw text inside the pill diff --git a/crates/grida-dev/src/bench/args.rs b/crates/grida-dev/src/bench/args.rs index c8e3b98307..e5a62f8c77 100644 --- a/crates/grida-dev/src/bench/args.rs +++ b/crates/grida-dev/src/bench/args.rs @@ -25,6 +25,10 @@ pub struct BenchArgs { /// Run the resize benchmark (alternates between two viewport sizes). #[arg(long = "resize", default_value_t = false)] pub resize: bool, + /// Draw SurfaceUI overlay (frame titles, badges) on each frame. + /// Measures the combined cost of content rendering + overlay drawing. + #[arg(long = "overlay", default_value_t = false)] + pub overlay: bool, } #[derive(Args, Debug)] @@ -43,4 +47,7 @@ pub struct BenchReportArgs { /// Output file path for the JSON report (stdout if omitted). #[arg(long = "output")] pub output: Option, + /// Draw SurfaceUI overlay (frame titles, badges) on each frame. + #[arg(long = "overlay", default_value_t = false)] + pub overlay: bool, } diff --git a/crates/grida-dev/src/bench/runner.rs b/crates/grida-dev/src/bench/runner.rs index 51b57ac5d5..9a8d6dac8d 100644 --- a/crates/grida-dev/src/bench/runner.rs +++ b/crates/grida-dev/src/bench/runner.rs @@ -2,11 +2,14 @@ use super::args::{BenchArgs, BenchReportArgs}; use super::report::*; use anyhow::{anyhow, Result}; use cg::cg::prelude::*; +use cg::devtools::surface_overlay::SurfaceOverlayConfig; use cg::node::factory::NodeFactory; use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::{Node, Scene, Size}; use cg::runtime::frame_loop::{FrameLoop, FrameQuality}; use cg::runtime::scene::FrameFlushResult; +use cg::surface::state::SurfaceState; +use cg::surface::ui::{HitRegions, SurfaceUI}; use cg::window::headless::HeadlessGpu; use math2::transform::AffineTransform; use std::path::{Path, PathBuf}; @@ -16,6 +19,49 @@ use std::time::Instant; // Shared helpers // --------------------------------------------------------------------------- +/// Reusable state for drawing the SurfaceUI overlay during benchmarks. +/// Created once per bench run when `--overlay` is set; passed to `measure_frame`. +struct OverlayBenchState { + surface_state: SurfaceState, + config: SurfaceOverlayConfig, + hit_regions: HitRegions, +} + +impl OverlayBenchState { + fn new() -> Self { + Self { + surface_state: SurfaceState::default(), + config: SurfaceOverlayConfig { + dpr: 1.0, + show_frame_titles: true, + show_size_meter: false, + text_baseline_decoration: false, + }, + hit_regions: HitRegions::new(), + } + } + + /// Draw `SurfaceUI` onto the renderer's canvas and flush the GPU. + fn draw(&mut self, renderer: &mut cg::runtime::scene::Renderer) { + let graph = renderer.scene.as_ref().map(|s| &s.graph); + let cache = renderer.get_cache(); + let camera = &renderer.camera; + let fonts = &renderer.fonts; + let canvas = renderer.canvas(); + SurfaceUI::draw( + canvas, + &self.surface_state, + camera, + cache, + &self.config, + &mut self.hit_regions, + graph, + fonts, + ); + renderer.flush_overlay(); + } +} + fn count_effects_nodes(renderer: &cg::runtime::scene::Renderer) -> usize { renderer .scene @@ -43,11 +89,17 @@ fn warmup(renderer: &mut cg::runtime::scene::Renderer) { } } -/// Measure a single frame including queue + flush. +/// Measure a single frame including queue + flush + optional overlay. /// Returns (total_us, queue_us, draw_us, mid_flush_us, compositor_us, flush_us). +/// +/// When `overlay` is `Some`, [`SurfaceUI::draw`] is called after the content +/// flush — matching the real `Application::frame()` pipeline. The overlay cost +/// is included in `total_us` but not in the per-stage breakdown, mirroring how +/// the native viewer accounts for it. fn measure_frame( renderer: &mut cg::runtime::scene::Renderer, stable: bool, + overlay: Option<&mut OverlayBenchState>, ) -> Option<(u64, u64, u64, u64, u64, u64)> { let t0 = Instant::now(); if stable { @@ -60,7 +112,11 @@ fn measure_frame( let t1 = Instant::now(); match renderer.flush() { FrameFlushResult::OK(stats) => { - let _flush_total = t1.elapsed().as_micros() as u64; + // Draw the SurfaceUI overlay if enabled (after content flush, + // before recording total time — same order as Application::frame). + if let Some(ov) = overlay { + ov.draw(renderer); + } let total = t0.elapsed().as_micros() as u64; Some(( total, @@ -79,7 +135,12 @@ fn measure_frame( /// Uses CONTINUOUS panning (one direction, then reverses) to trigger cache misses /// and expose frame drop outliers during area discovery/culling. /// Measures queue + flush per frame. Ends with a settle (stable) frame. -fn run_pan_pass_at(renderer: &mut cg::runtime::scene::Renderer, frames: u32, dx: f32) -> PassStats { +fn run_pan_pass_at( + renderer: &mut cg::runtime::scene::Renderer, + frames: u32, + dx: f32, + mut overlay: Option, +) -> PassStats { let wall_start = Instant::now(); let mut frame_times = Vec::with_capacity(frames as usize); let mut queue_us_acc = Vec::with_capacity(frames as usize); @@ -96,7 +157,7 @@ fn run_pan_pass_at(renderer: &mut cg::runtime::scene::Renderer, frames: u32, dx: for i in 0..frames { let d = if i < half { dx } else { -dx }; renderer.camera.translate(d, 0.0); - if let Some((total, q, d, mf, c, f)) = measure_frame(renderer, false) { + if let Some((total, q, d, mf, c, f)) = measure_frame(renderer, false, overlay.as_mut()) { frame_times.push(total); queue_us_acc.push(q); draw_us_acc.push(d); @@ -123,8 +184,12 @@ fn run_pan_pass_at(renderer: &mut cg::runtime::scene::Renderer, frames: u32, dx: } /// Legacy: pan at dx=5.0 (default). -fn run_pan_pass(renderer: &mut cg::runtime::scene::Renderer, frames: u32) -> PassStats { - run_pan_pass_at(renderer, frames, 5.0) +fn run_pan_pass( + renderer: &mut cg::runtime::scene::Renderer, + frames: u32, + overlay: Option, +) -> PassStats { + run_pan_pass_at(renderer, frames, 5.0, overlay) } /// Run a zoom pass with configurable step and range. @@ -135,6 +200,7 @@ fn run_zoom_pass_at( step: f32, z_min: f32, z_max: f32, + mut overlay: Option, ) -> PassStats { let start_z = (z_min + z_max) / 2.0; renderer.camera.set_zoom(start_z); @@ -166,7 +232,7 @@ fn run_zoom_pass_at( z = next_z; } renderer.camera.set_zoom(z); - if let Some((total, q, d, mf, c, f)) = measure_frame(renderer, false) { + if let Some((total, q, d, mf, c, f)) = measure_frame(renderer, false, overlay.as_mut()) { frame_times.push(total); queue_us_acc.push(q); draw_us_acc.push(d); @@ -195,8 +261,12 @@ fn run_zoom_pass_at( } /// Legacy: zoom with step=0.02, range 0.5-2.0. -fn run_zoom_pass(renderer: &mut cg::runtime::scene::Renderer, frames: u32) -> PassStats { - run_zoom_pass_at(renderer, frames, 0.02, 0.5, 2.0) +fn run_zoom_pass( + renderer: &mut cg::runtime::scene::Renderer, + frames: u32, + overlay: Option, +) -> PassStats { + run_zoom_pass_at(renderer, frames, 0.02, 0.5, 2.0, overlay) } /// Measure a single stable (settle) frame including queue + flush. @@ -398,6 +468,7 @@ fn compute_pass_stats( /// previous direction). fn run_zigzag_pan_pass( renderer: &mut cg::runtime::scene::Renderer, + mut overlay: Option, frames: u32, dx: f32, dy: f32, @@ -422,7 +493,8 @@ fn run_zigzag_pan_pass( for _ in 0..seg_len { let sx = if going_right { dx } else { -dx }; renderer.camera.translate(sx, dy); - if let Some((total, q, d, mf, c, f)) = measure_frame(renderer, false) { + if let Some((total, q, d, mf, c, f)) = measure_frame(renderer, false, overlay.as_mut()) + { frame_times.push(total); queue_us_acc.push(q); draw_us_acc.push(d); @@ -442,7 +514,7 @@ fn run_zigzag_pan_pass( if emitted >= frames { break; } - if let Some((total, q, d, mf, c, f)) = measure_frame(renderer, true) { + if let Some((total, q, d, mf, c, f)) = measure_frame(renderer, true, overlay.as_mut()) { frame_times.push(total); queue_us_acc.push(q); draw_us_acc.push(d); @@ -480,6 +552,7 @@ fn run_zigzag_pan_pass( /// `radius` is in world-space units. The circle completes over `frames` frames. fn run_circle_pan_pass( renderer: &mut cg::runtime::scene::Renderer, + mut overlay: Option, frames: u32, radius: f32, ) -> PassStats { @@ -506,7 +579,7 @@ fn run_circle_pan_pass( prev_y = y; renderer.camera.translate(dx, dy); - if let Some((total, q, d, mf, c, f)) = measure_frame(renderer, false) { + if let Some((total, q, d, mf, c, f)) = measure_frame(renderer, false, overlay.as_mut()) { frame_times.push(total); queue_us_acc.push(q); draw_us_acc.push(d); @@ -551,6 +624,7 @@ fn run_circle_pan_pass( /// native viewer, including settle-induced frame drops at their natural frequency. fn run_realtime_pan_pass( renderer: &mut cg::runtime::scene::Renderer, + mut overlay: Option, scroll_interval_ms: f64, dx: f32, dy: f32, @@ -590,7 +664,9 @@ fn run_realtime_pan_pass( settle_countdown -= 1; if settle_countdown == 0 { // Settle fires — this is the expensive frame - if let Some((total, q, d, mf, c, f)) = measure_frame(renderer, true) { + if let Some((total, q, d, mf, c, f)) = + measure_frame(renderer, true, overlay.as_mut()) + { settle_times.push(total); frame_times.push(total); queue_us_acc.push(q); @@ -609,7 +685,8 @@ fn run_realtime_pan_pass( renderer.camera.translate(dx, dy); settle_countdown = settle_ticks; // Reset countdown on every scroll - if let Some((total, q, d, mf, c, f)) = measure_frame(renderer, false) { + if let Some((total, q, d, mf, c, f)) = measure_frame(renderer, false, overlay.as_mut()) + { frame_times.push(total); queue_us_acc.push(q); draw_us_acc.push(d); @@ -783,6 +860,7 @@ fn run_frameloop_pan_pass( #[allow(dead_code)] fn run_realtime_diagnostic( renderer: &mut cg::runtime::scene::Renderer, + mut overlay: Option, scroll_interval_ms: f64, dx: f32, dy: f32, @@ -815,7 +893,9 @@ fn run_realtime_diagnostic( if settle_countdown > 0 { settle_countdown -= 1; if settle_countdown == 0 { - if let Some((total, _q, d, _mf, _c, _f)) = measure_frame(renderer, true) { + if let Some((total, _q, d, _mf, _c, _f)) = + measure_frame(renderer, true, overlay.as_mut()) + { let marker = if total > 1000 { " <<<" } else { "" }; eprintln!( "{:>8.1} {:>6} {:>8} {:>8} settle{marker}", @@ -830,7 +910,9 @@ fn run_realtime_diagnostic( if clock >= next_scroll && clock < duration_ms { renderer.camera.translate(dx, dy); settle_countdown = settle_ticks; - if let Some((total, _q, d, _mf, _c, _f)) = measure_frame(renderer, false) { + if let Some((total, _q, d, _mf, _c, _f)) = + measure_frame(renderer, false, overlay.as_mut()) + { let note = if d > 0 { "full draw" } else { "cache hit" }; let marker = if total > 1000 { " <<<" } else { "" }; eprintln!( @@ -846,6 +928,7 @@ fn run_realtime_diagnostic( fn run_pan_with_settle_pass( renderer: &mut cg::runtime::scene::Renderer, + mut overlay: Option, frames: u32, dx: f32, settle_interval: u32, @@ -868,7 +951,7 @@ fn run_pan_with_settle_pass( renderer.camera.translate(d, 0.0); // Interaction frame (unstable) - if let Some((total, q, dr, mf, c, f)) = measure_frame(renderer, false) { + if let Some((total, q, dr, mf, c, f)) = measure_frame(renderer, false, overlay.as_mut()) { frame_times.push(total); queue_us_acc.push(q); draw_us_acc.push(dr); @@ -882,7 +965,8 @@ fn run_pan_with_settle_pass( // Insert settle frame at interval (simulates native viewer countdown) if since_settle >= settle_interval && i < frames - 1 { since_settle = 0; - if let Some((total, q, dr, mf, c, f)) = measure_frame(renderer, true) { + if let Some((total, q, dr, mf, c, f)) = measure_frame(renderer, true, overlay.as_mut()) + { settle_times.push(total); // Include in overall stats — this IS a real frame the user sees frame_times.push(total); @@ -920,6 +1004,7 @@ fn run_pan_with_settle_pass( #[allow(dead_code)] fn run_pan_settle_diagnostic( renderer: &mut cg::runtime::scene::Renderer, + mut overlay: Option, frames: u32, dx: f32, settle_interval: u32, @@ -934,7 +1019,7 @@ fn run_pan_settle_diagnostic( for i in 0..frames { renderer.camera.translate(dx, 0.0); - if let Some((total, q, d, _mf, _c, _f)) = measure_frame(renderer, false) { + if let Some((total, q, d, _mf, _c, _f)) = measure_frame(renderer, false, overlay.as_mut()) { let list = 0; // Not available from measure_frame let marker = if total > 1000 { " <<<" } else { "" }; eprintln!( @@ -946,7 +1031,9 @@ fn run_pan_settle_diagnostic( since_settle += 1; if since_settle >= settle_interval && i < frames - 1 { since_settle = 0; - if let Some((total, q, d, _mf, _c, _f)) = measure_frame(renderer, true) { + if let Some((total, q, d, _mf, _c, _f)) = + measure_frame(renderer, true, overlay.as_mut()) + { let marker = if total > 1000 { " <<<" } else { "" }; eprintln!( "{:>5} {:>4} {:>8} {:>8} {:>8} {:>8}{marker}", @@ -1087,7 +1174,15 @@ fn run_scenarios( renderer: &mut cg::runtime::scene::Renderer, frames: u32, fit_zoom: f32, + overlay: bool, ) -> Vec { + let ov = || { + if overlay { + Some(OverlayBenchState::new()) + } else { + None + } + }; let (pan_scenarios, zoom_scenarios) = standard_scenarios(fit_zoom); let mut results = Vec::new(); @@ -1102,7 +1197,7 @@ fn run_scenarios( let _ = renderer.flush(); } - let stats = run_pan_pass_at(renderer, frames, ps.dx); + let stats = run_pan_pass_at(renderer, frames, ps.dx, ov()); results.push(ScenarioResult { name: ps.name.to_string(), kind: "pan".to_string(), @@ -1160,7 +1255,7 @@ fn run_scenarios( let _ = renderer.flush(); } - let stats = run_circle_pan_pass(renderer, frames, cs.radius); + let stats = run_circle_pan_pass(renderer, ov(), frames, cs.radius); results.push(ScenarioResult { name: cs.name.to_string(), kind: "circle_pan".to_string(), @@ -1237,6 +1332,7 @@ fn run_scenarios( let stats = run_zigzag_pan_pass( renderer, + ov(), frames, zz.dx, zz.dy, @@ -1257,7 +1353,7 @@ fn run_scenarios( } for zs in &zoom_scenarios { - let stats = run_zoom_pass_at(renderer, frames, zs.step, zs.z_min, zs.z_max); + let stats = run_zoom_pass_at(renderer, frames, zs.step, zs.z_min, zs.z_max, ov()); results.push(ScenarioResult { name: zs.name.to_string(), kind: "zoom".to_string(), @@ -1318,7 +1414,7 @@ fn run_scenarios( let _ = renderer.flush(); } - let stats = run_pan_with_settle_pass(renderer, frames, ss.dx, ss.settle_interval); + let stats = run_pan_with_settle_pass(renderer, ov(), frames, ss.dx, ss.settle_interval); results.push(ScenarioResult { name: ss.name.to_string(), kind: "pan_with_settle".to_string(), @@ -1388,6 +1484,7 @@ fn run_scenarios( let stats = run_realtime_pan_pass( renderer, + ov(), rt.scroll_interval_ms, rt.dx, rt.dy, @@ -1656,9 +1753,19 @@ pub async fn run_bench(args: BenchArgs, load_scenes: impl AsyncSceneLoader) -> R return Ok(()); } + let ov_flag = args.overlay; + let ov = || { + if ov_flag { + Some(OverlayBenchState::new()) + } else { + None + } + }; + println!("Overlay: {}", if ov_flag { "ON" } else { "OFF" }); + // --- Legacy Pan --- println!("=== Pan benchmark ({} frames, continuous) ===", args.frames); - let pan = run_pan_pass(&mut renderer, args.frames); + let pan = run_pan_pass(&mut renderer, args.frames, ov()); println!(" avg: {:>7} us ({:>6.1} fps)", pan.avg_us, pan.fps); println!( " min: {:>7} us p50: {:>7} us p95: {:>7} us p99: {:>7} us MAX: {:>7} us", @@ -1671,7 +1778,7 @@ pub async fn run_bench(args: BenchArgs, load_scenes: impl AsyncSceneLoader) -> R // --- Legacy Zoom --- println!("\n=== Zoom benchmark ({} frames) ===", args.frames); - let zoom = run_zoom_pass(&mut renderer, args.frames); + let zoom = run_zoom_pass(&mut renderer, args.frames, ov()); println!( " avg: {:>7} us ({:>6.1} fps) p50: {:>7} us p95: {:>7} us", zoom.avg_us, zoom.fps, zoom.p50_us, zoom.p95_us @@ -1684,7 +1791,7 @@ pub async fn run_bench(args: BenchArgs, load_scenes: impl AsyncSceneLoader) -> R // --- Expanded Scenarios --- println!("\n=== Expanded Scenarios ({} frames each) ===", args.frames); renderer.fit_camera_to_scene(); - let scenarios = run_scenarios(&mut renderer, args.frames, fit_zoom); + let scenarios = run_scenarios(&mut renderer, args.frames, fit_zoom, ov_flag); for s in &scenarios { println!( "\n [{:25}] ({}) {:>7} us avg ({:>6.1} fps) min: {:>7} p50: {:>7} p95: {:>7} p99: {:>7} MAX: {:>7}", @@ -1771,12 +1878,19 @@ pub async fn run_bench_report( let effects_count = count_effects_nodes(&renderer); // Legacy passes (back-compat) - let pan = run_pan_pass(&mut renderer, args.frames); - let zoom = run_zoom_pass(&mut renderer, args.frames); + let ov = || { + if args.overlay { + Some(OverlayBenchState::new()) + } else { + None + } + }; + let pan = run_pan_pass(&mut renderer, args.frames, ov()); + let zoom = run_zoom_pass(&mut renderer, args.frames, ov()); // Expanded scenario matrix renderer.fit_camera_to_scene(); - let scenarios = run_scenarios(&mut renderer, args.frames, fit_zoom); + let scenarios = run_scenarios(&mut renderer, args.frames, fit_zoom, args.overlay); drop(renderer); From 414c9687f50ef56dc32908e5fc9214e28f325d26 Mon Sep 17 00:00:00 2001 From: Universe Date: Sun, 29 Mar 2026 04:36:33 +0900 Subject: [PATCH 04/15] chore --- crates/grida-canvas/src/surface/ui/render.rs | 2 +- crates/grida-dev/src/bench/runner.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/grida-canvas/src/surface/ui/render.rs b/crates/grida-canvas/src/surface/ui/render.rs index 0f173ac6a4..e31ef795ef 100644 --- a/crates/grida-canvas/src/surface/ui/render.rs +++ b/crates/grida-canvas/src/surface/ui/render.rs @@ -33,7 +33,7 @@ const SIZE_METER_RADIUS: f32 = 4.0; /// Height of the frame title bar (logical px) const TITLE_BAR_HEIGHT: f32 = 24.0; /// Minimum screen-space node width (logical px) to show a title bar. -const MIN_TITLE_WIDTH: f32 = 20.0; +const MIN_TITLE_WIDTH: f32 = 40.0; /// Padding inside the tray badge pill (logical px). const BADGE_PAD_X: f32 = 8.0; diff --git a/crates/grida-dev/src/bench/runner.rs b/crates/grida-dev/src/bench/runner.rs index 9a8d6dac8d..f204f3f3ac 100644 --- a/crates/grida-dev/src/bench/runner.rs +++ b/crates/grida-dev/src/bench/runner.rs @@ -109,7 +109,6 @@ fn measure_frame( } let queue_us = t0.elapsed().as_micros() as u64; - let t1 = Instant::now(); match renderer.flush() { FrameFlushResult::OK(stats) => { // Draw the SurfaceUI overlay if enabled (after content flush, From 4b575094f963a24b077758834e3a7975abdd53ce Mon Sep 17 00:00:00 2001 From: Universe Date: Sun, 29 Mar 2026 05:30:14 +0900 Subject: [PATCH 05/15] feat(editor): add Tray tool with Shift+F shortcut, hierarchy constraints, and full editor integration - Add "tray" to insert ToolMode, bind Shift+F hotkey - Implement initialNode factory with defaults (white fill, 10% black stroke, corner radius 2) - Add TrayWidget DOM renderer, register in node switch and renderer index - Enforce tray parent constraint: tray can only be child of Scene or another Tray - self_moveNode rejects invalid reparenting - Canvas translate hierarchy change respects tray constraint - Insert-and-resize auto-wrap checks moveNode return value before adjusting position - Add tray to graph policy (can_be_parent), layer tree (isItemFolder), insertion targeting - Add allows_hierarchy_change for tray (children can enter/exit during canvas drag) - Add ICSSDimension, IRectangularShapeTrait, rotation to TrayNode schema - Add tray to property supports map (fill, stroke, cornerRadius, children, strokeWidth) - Add SquareDashedIcon for tray in layer panel - Add user-facing docs, manual test cases, and 8 unit tests (all passing) Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/editor/features/README.md | 1 + docs/editor/features/tray.md | 62 ++++ docs/editor/shortcuts/index.md | 23 +- .../nodes/index.ts | 2 + .../nodes/node.tsx | 1 + .../nodes/tray.tsx | 19 ++ .../starterkit-hierarchy/tree-node.tsx | 1 + .../starterkit-icons/node-type-icon.tsx | 4 + .../starterkit-toolbar/utils.ts | 2 + .../grida-canvas-react/viewport/hotkeys.tsx | 4 + editor/grida-canvas/editor.i.ts | 1 + editor/grida-canvas/policy.ts | 1 + .../reducers/event-target.reducer.ts | 35 +- .../methods/__tests__/move-tray.test.ts | 309 ++++++++++++++++++ editor/grida-canvas/reducers/methods/move.ts | 9 + .../reducers/methods/transform.ts | 8 + .../reducers/tools/initial-node.ts | 29 +- .../grida-canvas/utils/insertion-targeting.ts | 14 +- editor/grida-canvas/utils/supports.ts | 20 +- packages/grida-canvas-io-figma/lib.ts | 4 +- packages/grida-canvas-schema/grida.ts | 206 +++++++----- test/canvas-tray-node-behavior.md | 107 ++++++ 22 files changed, 745 insertions(+), 117 deletions(-) create mode 100644 docs/editor/features/tray.md create mode 100644 editor/grida-canvas-react-renderer-dom/nodes/tray.tsx create mode 100644 editor/grida-canvas/reducers/methods/__tests__/move-tray.test.ts create mode 100644 test/canvas-tray-node-behavior.md diff --git a/docs/editor/features/README.md b/docs/editor/features/README.md index 9d0509fbbe..85547000ba 100644 --- a/docs/editor/features/README.md +++ b/docs/editor/features/README.md @@ -8,3 +8,4 @@ - [copy & paste images](./copy-paste-image.md) - [copy & paste SVG](./copy-paste-svg.md) - [copy & paste from Figma](./copy-paste-figma.md) +- [tray (organizational section)](./tray.md) diff --git a/docs/editor/features/tray.md b/docs/editor/features/tray.md new file mode 100644 index 0000000000..d4889ac3f1 --- /dev/null +++ b/docs/editor/features/tray.md @@ -0,0 +1,62 @@ +--- +title: Tray +format: md +--- + +# Tray + +A Tray is a lightweight organizational container for grouping frames and other elements on your canvas. Think of it as a labeled surface — things sit _on_ a Tray, and the Tray itself doesn't affect how they behave. + +## When to Use a Tray + +Use Trays to organize your canvas when you have many frames. For example: + +- Group all login/signup screens under an "Authentication" Tray +- Keep dashboard views together in a "Dashboard" Tray +- Separate mobile and desktop variants into their own Trays + +## Creating a Tray + +| Method | How | +| ------------------------- | -------------------------------------------------------------------- | +| Keyboard shortcut | Press **Shift + F**, then click and drag on the canvas | +| Draw over existing frames | The Tray will automatically contain any frames inside the drawn area | + +## How Trays Differ from Containers (Frames) + +| | Tray | Container (Frame) | +| -------------- | --------------------------------- | ----------------------- | +| **Purpose** | Organize your canvas | Structure your design | +| **Layout** | None — children are freely placed | Auto-layout, flex, flow | +| **Clipping** | No — children can overflow | Yes — clips content | +| **Effects** | None | Shadows, blurs, etc. | +| **In exports** | Not visible | Rendered | +| **Nesting** | Only inside Scene or other Trays | Anywhere | + +## Nesting Rules + +- Trays live at the **top level** of your scene, or inside other Trays +- You **cannot** put a Tray inside a Container or Group +- You **can** put anything inside a Tray: Containers, Groups, shapes, text, other Trays + +## Default Appearance + +New Trays are created with: + +- White background fill +- Subtle border (black at 10% opacity) +- Slightly rounded corners + +You can change fills, strokes, and corner radius in the inspector, just like a Container. + +## Keyboard Shortcut + +| Action | Shortcut | +| ----------- | ------------- | +| Insert Tray | **Shift + F** | + +This follows the pattern: **F** = Frame (Container), **Shift + F** = Tray. + +## Figma Compatibility + +Trays map directly to Figma **Sections**. When you import a Figma file, Sections become Trays. The round-trip is clean — Trays export back as Sections. diff --git a/docs/editor/shortcuts/index.md b/docs/editor/shortcuts/index.md index a9effd7a18..5816f302e5 100644 --- a/docs/editor/shortcuts/index.md +++ b/docs/editor/shortcuts/index.md @@ -41,6 +41,7 @@ Last updated: Based on keybindings_sheet array and useHotkeys calls in hotkeys.t | Line tool | `L` | `L` | Draw line | | Arrow tool | `⇧ + L` | `⇧ + L` | Draw arrow (line with arrowhead) | | Container tool | `A` or `F` | `A` or `F` | Insert container | +| Tray tool | `⇧ + F` | `⇧ + F` | Insert tray (organizational section) | | Path tool | `P` | `P` | Draw path (Pen tool) | | Pencil tool | `⇧ + P` | `⇧ + P` | Draw with pencil | | Brush tool | `B` | `B` | Brush tool | @@ -158,18 +159,18 @@ Last updated: Based on keybindings_sheet array and useHotkeys calls in hotkeys.t ## View & Zoom -| Action | macOS | Windows/Linux | Description | -| -------------------- | ---------------------- | ---------------------------- | -------------------------------------------------- | -| Zoom to fit | `⇧ + 1` or `⇧ + 9` | `⇧ + 1` or `⇧ + 9` | Zoom to fit all content | -| Zoom to selection | `⇧ + 2` | `⇧ + 2` | Zoom to the current selection | -| Zoom to 100% | `⇧ + 0` | `⇧ + 0` | Zoom to 100% | -| Zoom in | `⌘ + =` or `⌘ + Plus` | `Ctrl + =` or `Ctrl + Plus` | Zoom in | -| Zoom out | `⌘ + -` or `⌘ + Minus` | `Ctrl + -` or `Ctrl + Minus` | Zoom out | -| Toggle ruler | `⇧ + R` | `⇧ + R` | Toggle ruler visibility | -| Toggle pixel grid | `⇧ + '` | `⇧ + '` | Toggle pixel grid visibility | +| Action | macOS | Windows/Linux | Description | +| -------------------- | ---------------------- | ---------------------------- | ------------------------------------------------- | +| Zoom to fit | `⇧ + 1` or `⇧ + 9` | `⇧ + 1` or `⇧ + 9` | Zoom to fit all content | +| Zoom to selection | `⇧ + 2` | `⇧ + 2` | Zoom to the current selection | +| Zoom to 100% | `⇧ + 0` | `⇧ + 0` | Zoom to 100% | +| Zoom in | `⌘ + =` or `⌘ + Plus` | `Ctrl + =` or `Ctrl + Plus` | Zoom in | +| Zoom out | `⌘ + -` or `⌘ + Minus` | `Ctrl + -` or `Ctrl + Minus` | Zoom out | +| Toggle ruler | `⇧ + R` | `⇧ + R` | Toggle ruler visibility | +| Toggle pixel grid | `⇧ + '` | `⇧ + '` | Toggle pixel grid visibility | | Toggle pixel preview | `⌘ + ⇧ + ⌥ + P` | `Ctrl + ⇧ + Alt + P` | Toggle pixel preview (Disabled ↔ last used 1x/2x) | -| Toggle outline mode | `⌘ + ⇧ + O` or `⌘ + Y` | `Ctrl + ⇧ + O` or `Ctrl + Y` | Toggle outline mode (wireframe) | -| Preview | `⇧ + Space` | `⇧ + Space` | Preview current selection | +| Toggle outline mode | `⌘ + ⇧ + O` or `⌘ + Y` | `Ctrl + ⇧ + O` or `Ctrl + Y` | Toggle outline mode (wireframe) | +| Preview | `⇧ + Space` | `⇧ + Space` | Preview current selection | ## Brush Tools diff --git a/editor/grida-canvas-react-renderer-dom/nodes/index.ts b/editor/grida-canvas-react-renderer-dom/nodes/index.ts index 61e44b0b20..d125edffc6 100644 --- a/editor/grida-canvas-react-renderer-dom/nodes/index.ts +++ b/editor/grida-canvas-react-renderer-dom/nodes/index.ts @@ -1,4 +1,5 @@ import { ContainerWidget } from "./container"; +import { TrayWidget } from "./tray"; import { TextSpanWidget } from "./tspan"; import { ImageWidget } from "./image"; import { VideoWidget } from "./video"; @@ -15,6 +16,7 @@ import { RegularStarPolygonWidget } from "./star"; export namespace ReactNodeRenderers { export const container = ContainerWidget; + export const tray = TrayWidget; export const component = ContainerWidget; // TODO: export const iframe = IFrameWidget; export const vector = VectorWidget; diff --git a/editor/grida-canvas-react-renderer-dom/nodes/node.tsx b/editor/grida-canvas-react-renderer-dom/nodes/node.tsx index 32849fbba4..3045566750 100644 --- a/editor/grida-canvas-react-renderer-dom/nodes/node.tsx +++ b/editor/grida-canvas-react-renderer-dom/nodes/node.tsx @@ -84,6 +84,7 @@ export function NodeElement

>({ return r; } case "container": + case "tray": case "image": case "video": case "tspan": diff --git a/editor/grida-canvas-react-renderer-dom/nodes/tray.tsx b/editor/grida-canvas-react-renderer-dom/nodes/tray.tsx new file mode 100644 index 0000000000..2788504202 --- /dev/null +++ b/editor/grida-canvas-react-renderer-dom/nodes/tray.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import grida from "@grida/schema"; +import queryattributes from "./utils/attributes"; + +export const TrayWidget = ({ + style, + children, + ...props +}: React.PropsWithChildren< + grida.program.document.IComputedNodeReactRenderProps +>) => { + return ( +

+ {children} +
+ ); +}; + +TrayWidget.type = "tray"; diff --git a/editor/grida-canvas-react-starter-kit/starterkit-hierarchy/tree-node.tsx b/editor/grida-canvas-react-starter-kit/starterkit-hierarchy/tree-node.tsx index fd3dcd79bf..7c47ceca53 100644 --- a/editor/grida-canvas-react-starter-kit/starterkit-hierarchy/tree-node.tsx +++ b/editor/grida-canvas-react-starter-kit/starterkit-hierarchy/tree-node.tsx @@ -298,6 +298,7 @@ export function NodeHierarchyList() { return ( node.type === "scene" || node.type === "container" || + node.type === "tray" || node.type === "group" || node.type === "boolean" ); diff --git a/editor/grida-canvas-react-starter-kit/starterkit-icons/node-type-icon.tsx b/editor/grida-canvas-react-starter-kit/starterkit-icons/node-type-icon.tsx index 282d3cacc8..d7982e1a3c 100644 --- a/editor/grida-canvas-react-starter-kit/starterkit-icons/node-type-icon.tsx +++ b/editor/grida-canvas-react-starter-kit/starterkit-icons/node-type-icon.tsx @@ -16,6 +16,7 @@ import { GlobeIcon, GroupIcon, Half2Icon, + LayersIcon, } from "@radix-ui/react-icons"; import { SquaresUniteIcon } from "lucide-react"; @@ -53,6 +54,7 @@ export function NodeTypeIcon({ return ; case "image": return ; + case "text": case "tspan": return ; case "instance": @@ -77,6 +79,8 @@ export function NodeTypeIcon({ return ; case "bitmap": return ; + case "tray": + return ; case "group": return ; case "boolean": diff --git a/editor/grida-canvas-react-starter-kit/starterkit-toolbar/utils.ts b/editor/grida-canvas-react-starter-kit/starterkit-toolbar/utils.ts index c60cece571..702bcf1f37 100644 --- a/editor/grida-canvas-react-starter-kit/starterkit-toolbar/utils.ts +++ b/editor/grida-canvas-react-starter-kit/starterkit-toolbar/utils.ts @@ -10,6 +10,7 @@ export type ToolbarToolType = | "polygon" | "star" | "container" + | "tray" | "image" | "line" | "arrow" @@ -65,6 +66,7 @@ export function toolbar_value_to_cursormode( case "hand": return { type: "hand" }; case "container": + case "tray": case "ellipse": case "image": case "rectangle": diff --git a/editor/grida-canvas-react/viewport/hotkeys.tsx b/editor/grida-canvas-react/viewport/hotkeys.tsx index c5a35b3496..347e3d6cd1 100644 --- a/editor/grida-canvas-react/viewport/hotkeys.tsx +++ b/editor/grida-canvas-react/viewport/hotkeys.tsx @@ -807,6 +807,10 @@ export function useEditorHotKeys() { editor.surface.surfaceSetTool({ type: "insert", node: "container" }); }); + useHotkeys("shift+f", () => { + editor.surface.surfaceSetTool({ type: "insert", node: "tray" }); + }); + useHotkeys("r", () => { editor.surface.surfaceSetTool({ type: "insert", node: "rectangle" }); }); diff --git a/editor/grida-canvas/editor.i.ts b/editor/grida-canvas/editor.i.ts index 77280d5286..b25606ba58 100644 --- a/editor/grida-canvas/editor.i.ts +++ b/editor/grida-canvas/editor.i.ts @@ -785,6 +785,7 @@ export namespace editor.state { | "text" | "image" | "container" + | "tray" | "rectangle" | "ellipse" | "polygon" diff --git a/editor/grida-canvas/policy.ts b/editor/grida-canvas/policy.ts index dd78135386..1662c1ae9f 100644 --- a/editor/grida-canvas/policy.ts +++ b/editor/grida-canvas/policy.ts @@ -18,6 +18,7 @@ export const EDITOR_GRAPH_POLICY: tree.graph.IGraphPolicy [], + getNodeIdsFromPointerEvent: () => [], + getNodeIdsFromEnvelope: () => [], + getNodeAbsoluteBoundingRect: () => ({ + x: 0, + y: 0, + width: 100, + height: 100, + }), + getNodeAbsoluteRotation: () => 0, +}; + +function createContext(): ReducerContext { + return { + geometry: geometryStub, + vector: undefined, + viewport: { width: 1000, height: 1000 }, + backend: "dom" as const, + paint_constraints: { fill: "fill", stroke: "stroke" }, + idgen: grida.id.noop.generator, + }; +} + +function trayNode( + id: string, + name: string +): grida.program.nodes.TrayNode { + return { + id, + type: "tray", + name, + active: true, + locked: false, + layout_positioning: "absolute", + layout_inset_left: 0, + layout_inset_top: 0, + layout_target_width: 500, + layout_target_height: 500, + rotation: 0, + opacity: 1, + corner_radius: 2, + stroke_width: 0, + stroke_cap: "butt", + stroke_join: "miter", + stroke_align: "inside", + }; +} + +function containerNode( + id: string, + name: string +): grida.program.nodes.ContainerNode { + return { + id, + type: "container", + name, + active: true, + locked: false, + layout_positioning: "absolute", + layout_inset_left: 0, + layout_inset_top: 0, + layout_target_width: 100, + layout_target_height: 100, + rotation: 0, + opacity: 1, + z_index: 0, + corner_radius: 0, + layout_mode: "flow", + layout_direction: "horizontal", + layout_main_axis_alignment: "start", + layout_cross_axis_alignment: "start", + layout_main_axis_gap: 0, + layout_cross_axis_gap: 0, + stroke_width: 0, + stroke_cap: "butt", + stroke_join: "miter", + stroke_align: "inside", + clips_content: true, + fill: { + type: "solid", + color: color.colorformats.RGBA32F.WHITE, + active: true, + }, + }; +} + +function rectNode( + id: string, + name: string +): grida.program.nodes.RectangleNode { + return { + id, + type: "rectangle", + name, + active: true, + locked: false, + layout_positioning: "absolute", + layout_inset_left: 0, + layout_inset_top: 0, + layout_target_width: 50, + layout_target_height: 50, + rotation: 0, + opacity: 1, + z_index: 0, + corner_radius: 0, + stroke_width: 0, + stroke_cap: "butt", + stroke_join: "miter", + fill: { + type: "solid", + color: color.colorformats.RGBA32F.BLACK, + active: true, + }, + }; +} + +function sceneNode(): grida.program.nodes.SceneNode { + return { + type: "scene", + id: "scene", + name: "Scene", + active: true, + locked: false, + constraints: { children: "multiple" }, + guides: [], + edges: [], + background_color: null, + }; +} + +/** + * Creates a document with: + * scene + * +-- tray1 + * | +-- container1 + * | +-- rect1 + * +-- tray2 + * +-- container2 + */ +function createDocument(): grida.program.document.Document { + return { + scenes_ref: ["scene"], + links: { + scene: ["tray1", "tray2", "container2"], + tray1: ["container1", "rect1"], + tray2: [], + }, + nodes: { + scene: sceneNode(), + tray1: trayNode("tray1", "Tray 1"), + tray2: trayNode("tray2", "Tray 2"), + container1: containerNode("container1", "Container 1"), + container2: containerNode("container2", "Container 2"), + rect1: rectNode("rect1", "Rectangle 1"), + }, + entry_scene_id: "scene", + bitmaps: {}, + images: {}, + properties: {}, + }; +} + +function createState() { + return editor.state.init({ + editable: true, + debug: false, + document: createDocument(), + templates: {}, + }); +} + +function dispatch( + state: editor.state.IEditorState, + action: Action, + context: ReducerContext +) { + const [next] = reducer(state, action, context); + return next; +} + +describe("Tray Move Constraints", () => { + const context = createContext(); + + describe("Tray parent constraint: Tray can only be child of Scene or Tray", () => { + test("move tray into container — rejected", () => { + let state = createState(); + state = dispatch( + state, + { type: "mv", source: ["tray1"], target: "container2" }, + context + ); + // tray1 should still be a child of scene + expect(state.document.links["scene"]).toContain("tray1"); + expect(state.document.links["container2"] ?? []).not.toContain("tray1"); + }); + + test("move tray into another tray — accepted", () => { + let state = createState(); + state = dispatch( + state, + { type: "mv", source: ["tray2"], target: "tray1" }, + context + ); + // tray2 should now be a child of tray1 + expect(state.document.links["tray1"]).toContain("tray2"); + expect(state.document.links["scene"]).not.toContain("tray2"); + }); + + test("move tray to scene root — accepted", () => { + let state = createState(); + // First move tray2 into tray1 + state = dispatch( + state, + { type: "mv", source: ["tray2"], target: "tray1" }, + context + ); + expect(state.document.links["tray1"]).toContain("tray2"); + + // Then move it back to scene root + state = dispatch( + state, + { type: "mv", source: ["tray2"], target: "scene" }, + context + ); + expect(state.document.links["scene"]).toContain("tray2"); + expect(state.document.links["tray1"]).not.toContain("tray2"); + }); + }); + + describe("Tray as parent: accepts any child node type", () => { + test("move container into tray — accepted", () => { + let state = createState(); + state = dispatch( + state, + { type: "mv", source: ["container2"], target: "tray1" }, + context + ); + expect(state.document.links["tray1"]).toContain("container2"); + expect(state.document.links["scene"]).not.toContain("container2"); + }); + + test("move rectangle into tray — accepted", () => { + let state = createState(); + // rect1 is already in tray1, move it to tray2 + state = dispatch( + state, + { type: "mv", source: ["rect1"], target: "tray2" }, + context + ); + expect(state.document.links["tray2"]).toContain("rect1"); + expect(state.document.links["tray1"]).not.toContain("rect1"); + }); + }); + + describe("Moving nodes out of tray", () => { + test("move container out of tray to scene root — accepted", () => { + let state = createState(); + // container1 is in tray1, move it to scene root + state = dispatch( + state, + { type: "mv", source: ["container1"], target: "scene" }, + context + ); + expect(state.document.links["scene"]).toContain("container1"); + expect(state.document.links["tray1"]).not.toContain("container1"); + }); + + test("move container from tray to another container — accepted", () => { + let state = createState(); + // container1 is in tray1, move it into container2 + state = dispatch( + state, + { type: "mv", source: ["container1"], target: "container2" }, + context + ); + expect(state.document.links["container2"]).toContain("container1"); + expect(state.document.links["tray1"]).not.toContain("container1"); + }); + }); + + describe("Cycle prevention with trays", () => { + test("move tray into its own child — rejected (cycle)", () => { + let state = createState(); + // First nest tray2 inside tray1 + state = dispatch( + state, + { type: "mv", source: ["tray2"], target: "tray1" }, + context + ); + expect(state.document.links["tray1"]).toContain("tray2"); + + // Now try to move tray1 into tray2 — would create a cycle + state = dispatch( + state, + { type: "mv", source: ["tray1"], target: "tray2" }, + context + ); + // tray1 should still be at scene root + expect(state.document.links["scene"]).toContain("tray1"); + }); + }); +}); diff --git a/editor/grida-canvas/reducers/methods/move.ts b/editor/grida-canvas/reducers/methods/move.ts index 1dd8eb72cb..72891978d4 100644 --- a/editor/grida-canvas/reducers/methods/move.ts +++ b/editor/grida-canvas/reducers/methods/move.ts @@ -32,6 +32,15 @@ export function self_moveNode( return false; } + // Tray can only be a child of Scene or another Tray + const source_node = dq.__getNodeById(draft, source_id); + if (source_node.type === "tray" && target_id !== draft.scene_id) { + const target_node = dq.__getNodeById(draft, target_id); + if (target_node.type !== "tray") { + return false; + } + } + // Use Graph.mv() - mutates draft.document directly (scene is now a node!) const graph = new tree.graph.Graph(draft.document, EDITOR_GRAPH_POLICY); graph.mv(source_id, target_id, order); diff --git a/editor/grida-canvas/reducers/methods/transform.ts b/editor/grida-canvas/reducers/methods/transform.ts index f70e2cd581..001722b0eb 100644 --- a/editor/grida-canvas/reducers/methods/transform.ts +++ b/editor/grida-canvas/reducers/methods/transform.ts @@ -32,6 +32,7 @@ function allows_hierarchy_change( switch (node_type) { case "scene": case "container": + case "tray": return true; case "group": case "boolean": @@ -300,6 +301,13 @@ function __self_update_gesture_transform_translate( } } + // Tray can only be a child of Scene or another Tray + const moving_node = dq.__getNodeById(draft, node_id); + if (moving_node.type === "tray" && new_parent_id !== null) { + const target_node = dq.__getNodeById(draft, new_parent_id); + if (target_node.type !== "tray") return; + } + is_parent_changed = true; // Use Graph.mv() - mutates draft.document directly (scene is now a node!) diff --git a/editor/grida-canvas/reducers/tools/initial-node.ts b/editor/grida-canvas/reducers/tools/initial-node.ts index ec5b8dcdfa..e7908403d3 100644 --- a/editor/grida-canvas/reducers/tools/initial-node.ts +++ b/editor/grida-canvas/reducers/tools/initial-node.ts @@ -56,7 +56,8 @@ export default function initialNode( | "rectangle" | "polygon" | "star" - | "line", + | "line" + | "tray", idfac: () => string, seed: Partial> = {}, constraints: { @@ -303,6 +304,32 @@ export default function initialNode( ...seed, } satisfies grida.program.nodes.RegularStarPolygonNode; } + case "tray": { + const tray_stroke: cg.Paint = { + type: "solid", + color: kolor.colorformats.newRGBA32F(0, 0, 0, 0.1), + active: true, + }; + return { + ...base, + ...position, + opacity: 1, + blend_mode: cg.def.LAYER_BLENDMODE, + type: "tray", + corner_radius: 2, + layout_target_width: 100, + layout_target_height: 100, + rotation: 0, + fill: constraints.fill === "fill_paints" ? undefined : white, + fill_paints: constraints.fill === "fill_paints" ? [white] : undefined, + stroke_paints: [tray_stroke], + stroke_width: 1, + stroke_align: "inside", + stroke_cap: "butt", + stroke_join: "miter", + ...seed, + } satisfies grida.program.nodes.TrayNode; + } case "line": { return { ...base, diff --git a/editor/grida-canvas/utils/insertion-targeting.ts b/editor/grida-canvas/utils/insertion-targeting.ts index b69f96c9e0..a34ff73842 100644 --- a/editor/grida-canvas/utils/insertion-targeting.ts +++ b/editor/grida-canvas/utils/insertion-targeting.ts @@ -24,7 +24,7 @@ export function resolveInsertTargetParent( if (!node) return null; - if (node.type === "container") { + if (node.type === "container" || node.type === "tray") { return node_id; } @@ -51,20 +51,22 @@ export function resolvePasteTargetParents( .map((node_id) => { const node = dq.__getNodeById(state, node_id); - // If node is a container, use it as target parent (paste as child) - if (node.type === "container") { + // If node is a container or tray, use it as target parent (paste as child) + if (node.type === "container" || node.type === "tray") { return node_id; } // Otherwise, use its parent as target parent (paste as sibling) const parent_id = dq.getParentId(state.document_ctx, node_id); - // Parent can be null (scene) or a container + // Parent can be null (scene) or a container/tray if (!parent_id) return null; const parent = dq.__getNodeById(state, parent_id); - // Only return valid container parents - return parent?.type === "container" ? parent_id : null; + // Only return valid container/tray parents + return parent?.type === "container" || parent?.type === "tray" + ? parent_id + : null; }) .filter((target_id) => { // Ensure target parent is not one of the originals diff --git a/editor/grida-canvas/utils/supports.ts b/editor/grida-canvas/utils/supports.ts index 6547e08851..c6a45af42e 100644 --- a/editor/grida-canvas/utils/supports.ts +++ b/editor/grida-canvas/utils/supports.ts @@ -67,6 +67,7 @@ const dom_supports: Record> = { "tspan", "richtext", "container", + "tray", "component", ], fill_paints: [], @@ -75,6 +76,7 @@ const dom_supports: Record> = { "image", "video", "container", + "tray", "component", "instance", ], @@ -83,18 +85,20 @@ const dom_supports: Record> = { "image", "video", "container", + "tray", "component", "instance", ], - border: ["container", "component", "instance", "image", "video"], - children: ["container", "component", "instance"], - stroke: ["vector", "line", "rectangle", "ellipse", "polygon", "star"], + border: ["container", "tray", "component", "instance", "image", "video"], + children: ["container", "tray", "component", "instance"], + stroke: ["vector", "line", "rectangle", "ellipse", "polygon", "star", "tray"], stroke_paints: [], strokeWidth: [ "rectangle", "image", "video", "container", + "tray", "component", "instance", "vector", @@ -109,6 +113,7 @@ const dom_supports: Record> = { "image", "video", "container", + "tray", "component", "instance", ], @@ -134,6 +139,7 @@ const canvas_supports: Record> = { "tspan", "richtext", "container", + "tray", "component", "boolean", ], @@ -145,6 +151,7 @@ const canvas_supports: Record> = { "video", "vector", "container", + "tray", "component", "instance", "boolean", @@ -154,13 +161,15 @@ const canvas_supports: Record> = { "image", "video", "container", + "tray", "component", "instance", ], border: [], - children: ["container", "component", "instance", "boolean"], + children: ["container", "tray", "component", "instance", "boolean"], stroke: [ "container", + "tray", "rectangle", "image", "video", @@ -178,6 +187,7 @@ const canvas_supports: Record> = { ], stroke_paints: [ "container", + "tray", "rectangle", "image", "video", @@ -195,6 +205,7 @@ const canvas_supports: Record> = { ], strokeWidth: [ "container", + "tray", "rectangle", "image", "video", @@ -210,6 +221,7 @@ const canvas_supports: Record> = { ], strokeWidth4: [ "container", + "tray", "rectangle", "image", "video", diff --git a/packages/grida-canvas-io-figma/lib.ts b/packages/grida-canvas-io-figma/lib.ts index a1edecf69a..8b7928f2bb 100644 --- a/packages/grida-canvas-io-figma/lib.ts +++ b/packages/grida-canvas-io-figma/lib.ts @@ -1581,8 +1581,8 @@ export namespace iofigma { ...positioning_trait(node, parent), ...fills_trait(node.fills ?? [], context, imageRefsUsed), ...stroke_trait(node, context, imageRefsUsed), - ...rectangular_stroke_width_trait(node), - ...corner_radius_trait(node), + ...rectangular_stroke_width_trait(node as any), + ...corner_radius_trait(node as any), type: "tray", } satisfies grida.program.nodes.TrayNode; } diff --git a/packages/grida-canvas-schema/grida.ts b/packages/grida-canvas-schema/grida.ts index bb4f9bc4ee..1761cb79a5 100644 --- a/packages/grida-canvas-schema/grida.ts +++ b/packages/grida-canvas-schema/grida.ts @@ -883,7 +883,8 @@ export namespace grida.program.document { * ``` */ export interface IDocumentDefinition - extends IImagesRepository, + extends + IImagesRepository, IBitmapsRepository, document.INodesGraph, IDocumentProperties { @@ -929,9 +930,7 @@ export namespace grida.program.document { * TODO: safely remove this */ export interface Scene - extends document.ISceneBackground, - document.I2DGuides, - document.IEdges { + extends document.ISceneBackground, document.I2DGuides, document.IEdges { type: "scene"; /** @@ -1059,7 +1058,8 @@ export namespace grida.program.document { */ export namespace template { export interface IUserDefinedTemplateNodeReactComponentRenderProps

- extends nodes.i.IBaseNode, + extends + nodes.i.IBaseNode, nodes.i.ISceneNode, nodes.i.ICSSStylable, nodes.i.IExpandable { @@ -1889,7 +1889,8 @@ export namespace grida.program.nodes { * @deprecated */ export interface ICSSStylable - extends IStylable, + extends + IStylable, IBlend, ILayerMaskType, IRotation, @@ -1909,12 +1910,11 @@ export namespace grida.program.nodes { /** * @deprecated */ - export interface IComputedCSSStylable - extends __ReplaceSubset< - ICSSStylable, - IFill, - { fill: cg.Paint } - > {} + export interface IComputedCSSStylable extends __ReplaceSubset< + ICSSStylable, + IFill, + { fill: cg.Paint } + > {} export interface IMouseCursor { cursor?: cg.SystemMouseCursor; @@ -2073,8 +2073,7 @@ export namespace grida.program.nodes { * a set of properties that can be applied to a text node, but not to a textspan */ export interface ITextNodeStyle - extends ITextStyle, - IFill { + extends ITextStyle, IFill { /** * @default "left" */ @@ -2085,12 +2084,11 @@ export namespace grida.program.nodes { text_align_vertical: cg.TextAlignVertical; } - export interface IComputedTextNodeStyle - extends __ReplaceSubset< - ITextNodeStyle, - IFill, - { fill: cg.Paint } - > {} + export interface IComputedTextNodeStyle extends __ReplaceSubset< + ITextNodeStyle, + IFill, + { fill: cg.Paint } + > {} export interface ITextValue { text: props.PropsTextValue | null; @@ -2153,17 +2151,13 @@ export namespace grida.program.nodes { } export interface IBasicShapeTrait - extends i.ICornerRadius, - i.IFill, - i.IStroke {} + extends i.ICornerRadius, i.IFill, i.IStroke {} export interface IRectangularShapeTrait - extends IRectangularCornerRadius, - IRectangularStrokeWidth {} + extends IRectangularCornerRadius, IRectangularStrokeWidth {} export interface ILayoutTrait - extends ILayoutTargetAspectRatio, - IPositioning { + extends ILayoutTargetAspectRatio, IPositioning { rotation: number; layout_target_width: css.LengthPercentage | "auto"; layout_target_height: css.LengthPercentage | "auto"; @@ -2172,9 +2166,7 @@ export namespace grida.program.nodes { export interface ILayoutChildTrait extends ILayoutTrait {} export interface ILayoutContainerTrait - extends ILayoutTrait, - Partial, - IFlexContainer {} + extends ILayoutTrait, Partial, IFlexContainer {} export interface IHotspotTrait extends IHrefable, IMouseCursor {} } @@ -2205,7 +2197,8 @@ export namespace grida.program.nodes { * They can contain multiple children based on their constraints. */ export interface SceneNode - extends i.IBaseNode, + extends + i.IBaseNode, i.ISceneNode, document.ISceneBackground, document.I2DGuides, @@ -2226,10 +2219,7 @@ export namespace grida.program.nodes { * [GroupNode] is not supported in the html/svg backend. */ export interface GroupNode - extends i.IBaseNode, - i.ISceneNode, - i.IBlend, - i.IPositioning { + extends i.IBaseNode, i.ISceneNode, i.IBlend, i.IPositioning { type: "group"; // } @@ -2242,15 +2232,18 @@ export namespace grida.program.nodes { * Children are freely placed and treated as root-level containers. */ export interface TrayNode - extends i.IBaseNode, + extends + i.IBaseNode, i.ISceneNode, i.IBlend, i.IPositioning, + i.ICSSDimension, i.ICornerRadius, + i.IRectangularShapeTrait, i.IStroke, i.IFill { type: "tray"; - // + rotation: number; } /** @@ -2259,7 +2252,8 @@ export namespace grida.program.nodes { * [BooleanPathOperationNode] is not supported in the html/svg backend. */ export interface BooleanPathOperationNode - extends i.IBaseNode, + extends + i.IBaseNode, i.ISceneNode, i.ILayerTrait, i.ILayoutChildTrait, @@ -2271,7 +2265,8 @@ export namespace grida.program.nodes { } export interface TextSpanNode - extends i.IBaseNode, + extends + i.IBaseNode, i.ISceneNode, i.ILayerTrait, i.ILayoutChildTrait, @@ -2290,12 +2285,11 @@ export namespace grida.program.nodes { // text_auto_resize: "none" | "width" | "height" | "auto"; } - export interface ComputedTextSpanNode - extends __ReplaceSubset< - TextSpanNode, - i.ITextValue & i.ITextStyle, - i.IComputedTextValue & i.IComputedTextNodeStyle - > { + export interface ComputedTextSpanNode extends __ReplaceSubset< + TextSpanNode, + i.ITextValue & i.ITextStyle, + i.IComputedTextValue & i.IComputedTextNodeStyle + > { readonly type: "tspan"; max_lines?: number | null; } @@ -2320,7 +2314,8 @@ export namespace grida.program.nodes { * Mirrors the Rust `AttributedString` + node wrapper and FBS `AttributedTextNode`. */ export interface AttributedTextNode - extends i.IBaseNode, + extends + i.IBaseNode, i.ISceneNode, i.ILayerTrait, i.ILayoutChildTrait, @@ -2350,7 +2345,8 @@ export namespace grida.program.nodes { } export interface ImageNode - extends i.IBaseNode, + extends + i.IBaseNode, i.ISceneNode, i.ILayerTrait, i.ILayoutChildTrait, @@ -2363,8 +2359,11 @@ export namespace grida.program.nodes { alt?: string; } - export interface ComputedImageNode - extends __ReplaceSubset { + export interface ComputedImageNode extends __ReplaceSubset< + ImageNode, + i.ISourceValue, + { src: string } + > { readonly type: "image"; } @@ -2378,7 +2377,8 @@ export namespace grida.program.nodes { * RichText can hold any html-like text content, including text spans, links, images, etc. */ export interface HTMLRichTextNode - extends i.IBaseNode, + extends + i.IBaseNode, i.ISceneNode, i.IBlend, i.ICSSStylable, @@ -2388,17 +2388,17 @@ export namespace grida.program.nodes { readonly type: "richtext"; } - export interface ComputedHTMLRichTextNode - extends __ReplaceSubset< - HTMLRichTextNode, - i.IHTMLRichTextValue, - { html: string } - > { + export interface ComputedHTMLRichTextNode extends __ReplaceSubset< + HTMLRichTextNode, + i.IHTMLRichTextValue, + { html: string } + > { readonly type: "richtext"; } export interface VideoNode - extends i.IBaseNode, + extends + i.IBaseNode, i.ISceneNode, i.ILayerTrait, i.ILayoutChildTrait, @@ -2418,13 +2418,17 @@ export namespace grida.program.nodes { autoplay: boolean; } - export interface ComputedVideoNode - extends __ReplaceSubset { + export interface ComputedVideoNode extends __ReplaceSubset< + VideoNode, + i.ISourceValue, + { src: string } + > { readonly type: "video"; } export interface ContainerNode - extends i.IBaseNode, + extends + i.IBaseNode, i.ISceneNode, i.ILayerTrait, i.ILayoutContainerTrait, @@ -2438,8 +2442,11 @@ export namespace grida.program.nodes { // } - export interface ComputedContainerNode - extends __ReplaceSubset { + export interface ComputedContainerNode extends __ReplaceSubset< + ContainerNode, + {}, + {} + > { readonly type: "container"; // } @@ -2450,7 +2457,8 @@ export namespace grida.program.nodes { * The use and rendering of iframe node is limited by the environment. */ export interface HTMLIFrameNode - extends i.IBaseNode, + extends + i.IBaseNode, i.ISceneNode, i.ICSSStylable, i.ICornerRadius, @@ -2459,8 +2467,11 @@ export namespace grida.program.nodes { readonly type: "iframe"; } - export interface ComputedHTMLIFrameNode - extends __ReplaceSubset { + export interface ComputedHTMLIFrameNode extends __ReplaceSubset< + HTMLIFrameNode, + i.ISourceValue, + { src: string } + > { readonly type: "iframe"; } @@ -2476,7 +2487,8 @@ export namespace grida.program.nodes { * The bitmap data can by found in {@link document.IBitmapsRepository} images[this.id].data */ export interface BitmapNode - extends i.IBaseNode, + extends + i.IBaseNode, i.ISceneNode, i.ILayerTrait, i.ILayoutChildTrait, @@ -2488,7 +2500,8 @@ export namespace grida.program.nodes { export type ComputedBitmapNode = BitmapNode; export interface RegularPolygonNode - extends i.IBaseNode, + extends + i.IBaseNode, i.ISceneNode, i.ILayerTrait, i.ILayoutChildTrait, @@ -2499,7 +2512,8 @@ export namespace grida.program.nodes { } export interface RegularStarPolygonNode - extends i.IBaseNode, + extends + i.IBaseNode, i.ISceneNode, i.ILayerTrait, i.ILayoutChildTrait, @@ -2511,7 +2525,8 @@ export namespace grida.program.nodes { } export interface VectorNode - extends i.IBaseNode, + extends + i.IBaseNode, i.ISceneNode, i.ILayerTrait, i.ILayoutChildTrait, @@ -2539,8 +2554,11 @@ export namespace grida.program.nodes { marker_end_shape?: cg.StrokeMarkerPreset; } - export interface ComputedVectorNode - extends __ReplaceSubset, i.IFill> { + export interface ComputedVectorNode extends __ReplaceSubset< + VectorNode, + i.IFill, + i.IFill + > { readonly type: "vector"; } @@ -2556,7 +2574,8 @@ export namespace grida.program.nodes { * @see {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path} */ export interface PathNode - extends i.IBaseNode, + extends + i.IBaseNode, i.ISceneNode, i.ILayerTrait, i.ILayoutChildTrait, @@ -2567,8 +2586,11 @@ export namespace grida.program.nodes { fill_rule?: cg.FillRule; } - export interface ComputedPathNode - extends __ReplaceSubset, i.IFill> { + export interface ComputedPathNode extends __ReplaceSubset< + PathNode, + i.IFill, + i.IFill + > { readonly type: "path"; } @@ -2586,7 +2608,8 @@ export namespace grida.program.nodes { * */ export interface LineNode - extends i.IBaseNode, + extends + i.IBaseNode, i.ISceneNode, i.ILayerTrait, i.IHotspotTrait, @@ -2622,7 +2645,8 @@ export namespace grida.program.nodes { * */ export interface RectangleNode - extends i.IBaseNode, + extends + i.IBaseNode, i.ISceneNode, i.ILayerTrait, i.ILayoutChildTrait, @@ -2635,12 +2659,11 @@ export namespace grida.program.nodes { /** * {@link RectangleNode} with computed properties */ - export interface ComputedRectangleNode - extends __ReplaceSubset< - RectangleNode, - i.IFill, - i.IFill - > { + export interface ComputedRectangleNode extends __ReplaceSubset< + RectangleNode, + i.IFill, + i.IFill + > { readonly type: "rectangle"; } @@ -2651,7 +2674,8 @@ export namespace grida.program.nodes { * - [Env:SVG] on svg rendering, this will be rendered as `` with `cx`, `cy`, `rx`, `ry` attributes calculated from the `width`, `height` and `x`, `y` properties. */ export interface EllipseNode - extends i.IBaseNode, + extends + i.IBaseNode, i.ISceneNode, i.ILayerTrait, i.ILayoutChildTrait, @@ -2664,14 +2688,18 @@ export namespace grida.program.nodes { /** * {@link EllipseNode} with computed properties */ - export interface ComputedEllipseNode - extends __ReplaceSubset, i.IFill> { + export interface ComputedEllipseNode extends __ReplaceSubset< + EllipseNode, + i.IFill, + i.IFill + > { readonly type: "ellipse"; } // export interface ComponentNode - extends i.IBaseNode, + extends + i.IBaseNode, i.ISceneNode, i.ILayerTrait, i.ILayoutContainerTrait, @@ -2687,7 +2715,8 @@ export namespace grida.program.nodes { export type ComputedComponentNode = ComponentNode; export interface InstanceNode - extends i.IBaseNode, + extends + i.IBaseNode, i.ISceneNode, i.ILayerTrait, i.ILayoutContainerTrait, @@ -2713,7 +2742,8 @@ export namespace grida.program.nodes { * This is useful when you have a complex structure with custom loggics and state management, use this node and expose only customizable nodes and properties. */ export interface TemplateInstanceNode - extends i.IBaseNode, + extends + i.IBaseNode, i.ISceneNode, i.IHotspotTrait, i.IPositioning, diff --git a/test/canvas-tray-node-behavior.md b/test/canvas-tray-node-behavior.md new file mode 100644 index 0000000000..ee42d51992 --- /dev/null +++ b/test/canvas-tray-node-behavior.md @@ -0,0 +1,107 @@ +--- +id: TC-CANVAS-TRAY-001 +title: Tray Node Hierarchy Constraints and Interaction Behavior +module: canvas +area: hierarchy +tags: [tray, hierarchy, reparent, insert, container, scene, drag, auto-wrap] +status: untested +severity: high +date: 2026-03-29 +automatable: partial +covered_by: + - editor/grida-canvas/reducers/methods/__tests__/move-tray.test.ts +--- + +## Behavior + +A Tray is a non-layout organizational grouping node. It lives at Scene root level (or nested under another Tray) and can contain any child nodes. It behaves like a Container for hierarchy purposes (children can enter/exit, auto-wrap on draw) but has strict parent constraints. + +### Tray Parent Constraint + +A Tray can only be a child of: + +- **Scene** (root level) +- **Another Tray** (nesting) + +A Tray **cannot** be a child of Container, Group, Boolean, or any other node type. This constraint is permanent and enforced in all code paths: layer tree drag-and-drop, canvas translate hierarchy change, auto-wrap on insert, and paste targeting. + +### Tray as Parent + +A Tray can contain any child node type: Container, Group, Text, shapes, other Trays, etc. It appears as an expandable folder in the layer panel. + +### Auto-Wrap Behavior + +When a Container or Tray is drawn over existing sibling nodes, those siblings are auto-reparented into the new node. If the auto-wrap target is a Tray inside a Container (violating the parent constraint), the move is rejected and the Tray's position is not modified. + +### Canvas Translate Hierarchy Change + +When dragging a child node on the canvas, it can enter/exit a Tray the same way it enters/exits a Container. When dragging a Tray itself, it can only be dropped into Scene root or another Tray — dragging it over a Container does not reparent it. + +## Steps + +### TC-TRAY-001a: Insert Tray at Scene Root + +1. Press `Shift+F` to activate the Tray tool +2. Click and drag on empty canvas area +3. Expected: a Tray node is created at Scene root level +4. Expected: Tray appears in layer panel with dashed-square icon, expandable + +### TC-TRAY-001b: Insert Tray Inside Another Tray + +1. Select an existing Tray +2. Press `Shift+F` and draw inside the selected Tray +3. Expected: nested Tray is created as a child of the outer Tray + +### TC-TRAY-001c: Auto-Wrap — Container Over Tray (Rejected) + +1. Have a Tray at Scene root with some content +2. Press `A` or `F` to activate Container tool +3. Draw a Container that encompasses the Tray +4. Expected: Container is created, but Tray is **not** reparented into it +5. Expected: Tray's position does **not** change + +### TC-TRAY-001d: Auto-Wrap — Tray Over Containers (Accepted) + +1. Have two Containers at Scene root +2. Press `Shift+F` and draw a Tray encompassing both Containers +3. Expected: both Containers are reparented into the new Tray +4. Expected: Containers' visual positions are preserved (insets adjusted) + +### TC-TRAY-001e: Canvas Drag — Move Node Into Tray + +1. Have a Container and a Tray as siblings at Scene root +2. Drag the Container over the Tray bounds +3. Expected: Container is reparented into the Tray (visual position preserved) + +### TC-TRAY-001f: Canvas Drag — Move Node Out of Tray + +1. Have a Container inside a Tray +2. Drag the Container outside the Tray bounds +3. Expected: Container escapes to Scene root (visual position preserved) + +### TC-TRAY-001g: Canvas Drag — Tray Into Container (Rejected) + +1. Have a Tray and a large Container as siblings +2. Drag the Tray over the Container +3. Expected: Tray is **not** reparented into the Container +4. Expected: Tray remains at Scene root + +### TC-TRAY-001h: Layer Panel — Drag Tray Into Container (Rejected) + +1. In the layer panel, drag a Tray node onto a Container node +2. Expected: the move is rejected, Tray stays at its current level + +### TC-TRAY-001i: Layer Panel — Drag Node Into/Out of Tray + +1. Drag a Container from Scene root into a Tray in the layer panel +2. Expected: Container becomes a child of the Tray +3. Drag it back out to Scene root +4. Expected: Container returns to Scene root + +## Notes + +- Tray defaults: white fill, black 10% opacity stroke (1px inside), corner radius 2 +- Tray has no layout, no clipping, no effects — children are freely placed +- Shortcut: `Shift+F` (Frame variant — F = Container, Shift+F = Tray) +- The parent constraint is enforced by `self_moveNode` (returns false) and the caller must check the return value before adjusting positions +- Unit tests: `editor/grida-canvas/reducers/methods/__tests__/move-tray.test.ts` From 8bb2cddabb8e8cfc14537faffba690b8dc71a2c7 Mon Sep 17 00:00:00 2001 From: Universe Date: Sun, 29 Mar 2026 05:52:43 +0900 Subject: [PATCH 06/15] feat(editor): integrate Tray tool into toolbar with shortcut support - Add Tray tool option to the toolbar and update the corresponding UI components - Implement shortcut for Tray tool using Shift+F - Refactor ToolGroupItem to ToolsGroup for better management of tool options - Ensure Tray tool is available in both the playground and starter kit toolbars --- .../playground/uxhost-actions.ts | 7 ++++ .../playground/uxhost-toolbar.tsx | 35 +++++++++++++------ .../starterkit-toolbar/index.tsx | 35 ++++++++++++++----- 3 files changed, 59 insertions(+), 18 deletions(-) diff --git a/editor/grida-canvas-hosted/playground/uxhost-actions.ts b/editor/grida-canvas-hosted/playground/uxhost-actions.ts index 4554eb06a4..9a00246c81 100644 --- a/editor/grida-canvas-hosted/playground/uxhost-actions.ts +++ b/editor/grida-canvas-hosted/playground/uxhost-actions.ts @@ -346,6 +346,13 @@ export const actions = { keybindings: [kb(KeyCode.KeyA, 0), kb(KeyCode.KeyF, 0)], }, + ["workbench.surface.cursor.tray"]: { + name: "tray", + description: "Tray tool", + command: "editor.surface.action.setTool.tray", + keybindings: kb(KeyCode.KeyF, M.Shift), + }, + ["workbench.surface.cursor.pencil"]: { name: "pencil", description: "Pencil tool", diff --git a/editor/grida-canvas-hosted/playground/uxhost-toolbar.tsx b/editor/grida-canvas-hosted/playground/uxhost-toolbar.tsx index c6ea51bb84..99b5e95c23 100644 --- a/editor/grida-canvas-hosted/playground/uxhost-toolbar.tsx +++ b/editor/grida-canvas-hosted/playground/uxhost-toolbar.tsx @@ -9,7 +9,7 @@ import { PopoverTrigger, } from "@/components/ui/popover"; import kolor from "@grida/color"; -import { Cross2Icon, FrameIcon } from "@radix-ui/react-icons"; +import { Cross2Icon } from "@radix-ui/react-icons"; import { toolmode_to_toolbar_value, toolbar_value_to_cursormode, @@ -98,15 +98,30 @@ export function PlaygroundToolbar() { }} /> - - - + setOpen(o ? "container" : null)} + options={[ + { + value: "container", + label: "Container", + shortcut: keyboardShortcutText( + "workbench.surface.cursor.container" + ), + }, + { + value: "tray", + label: "Tray", + shortcut: keyboardShortcutText("workbench.surface.cursor.tray"), + }, + ]} + onValueChange={(v) => { + editor.surface.surfaceSetTool( + toolbar_value_to_cursormode(v as ToolbarToolType) + ); + }} + /> - - - + setOpen(o ? "container" : null)} + options={[ + { + value: "container", + label: "Container", + shortcut: keyboardShortcutText( + "workbench.surface.cursor.container" + ), + }, + { + value: "tray", + label: "Tray", + shortcut: keyboardShortcutText("workbench.surface.cursor.tray"), + }, + ]} + onValueChange={(v) => { + editor.surface.surfaceSetTool( + toolbar_value_to_cursormode(v as ToolbarToolType) + ); + }} + /> ; case "container": return ; + case "tray": + return ; case "text": return ; case "rectangle": From cae2868b966401a93d94a0930333a2c96ec3c984 Mon Sep 17 00:00:00 2001 From: Universe Date: Sun, 29 Mar 2026 06:20:56 +0900 Subject: [PATCH 07/15] =?UTF-8?q?chore(cg):=20bump=20skia-safe=200.91.0=20?= =?UTF-8?q?=E2=86=92=200.93.1,=20migrate=20deprecated=20gradient=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrate gradient.rs from deprecated Shader::*_gradient() methods to shaders::linear_gradient/radial_gradient/sweep_gradient with the new GradientColors + Gradient + Interpolation types. Verified 7/8 gradient goldens byte-identical; radial has max Δ6/255 from upstream Skia rasterizer change (m132→m134), not the API migration. Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 60 +++-- crates/grida-canvas/Cargo.toml | 2 +- crates/grida-canvas/src/painter/gradient.rs | 261 +++++++++----------- crates/grida-dev/Cargo.toml | 2 +- 4 files changed, 157 insertions(+), 168 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9818f7d557..545f907dc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3803,9 +3803,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" dependencies = [ "serde_core", ] @@ -3869,9 +3869,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "skia-bindings" -version = "0.91.0" +version = "0.93.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2dd9bbd87993cbe4c0caa5fa6ca69c7d2ed38dcf5cd967b200392840c3bc666" +checksum = "2359f7e30c9da3f322f8ca3d4ec0abbc12a40035ce758309db0cdab07b5d4476" dependencies = [ "bindgen", "cc", @@ -3881,14 +3881,14 @@ dependencies = [ "regex", "serde_json", "tar", - "toml 0.9.8", + "toml 1.0.7+spec-1.1.0", ] [[package]] name = "skia-safe" -version = "0.91.0" +version = "0.93.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f97f791b2d9d915947b424d76f212dc2d1c47c09a18e30546ab0edf0a8af89f" +checksum = "7f9e837ea9d531c9efee8f980bfcdb7226b21db0285b0c3171d8be745829f940" dependencies = [ "base64 0.22.1", "bitflags 2.9.1", @@ -4539,11 +4539,26 @@ checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ "indexmap", "serde_core", - "serde_spanned 1.0.3", + "serde_spanned 1.0.4", "toml_datetime 0.7.3", "toml_parser", "toml_writer", - "winnow", + "winnow 0.7.13", +] + +[[package]] +name = "toml" +version = "1.0.7+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd28d57d8a6f6e458bc0b8784f8fdcc4b99a437936056fa122cb234f18656a96" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.0.4", + "toml_datetime 1.0.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.0", ] [[package]] @@ -4564,6 +4579,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_datetime" +version = "1.0.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.26" @@ -4574,23 +4598,23 @@ dependencies = [ "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.9", - "winnow", + "winnow 0.7.13", ] [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.0.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" dependencies = [ - "winnow", + "winnow 1.0.0", ] [[package]] name = "toml_writer" -version = "1.0.4" +version = "1.0.7+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" +checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d" [[package]] name = "tower" @@ -5490,6 +5514,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" + [[package]] name = "wit-bindgen-rt" version = "0.39.0" diff --git a/crates/grida-canvas/Cargo.toml b/crates/grida-canvas/Cargo.toml index dce6104bba..80aca1a4ca 100644 --- a/crates/grida-canvas/Cargo.toml +++ b/crates/grida-canvas/Cargo.toml @@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] # skia -skia-safe = { version = "0.91.0", features = [ +skia-safe = { version = "0.93.1", features = [ "gpu", "gl", "textlayout", diff --git a/crates/grida-canvas/src/painter/gradient.rs b/crates/grida-canvas/src/painter/gradient.rs index 9b0e0d831a..855a8eec8a 100644 --- a/crates/grida-canvas/src/painter/gradient.rs +++ b/crates/grida-canvas/src/painter/gradient.rs @@ -1,19 +1,36 @@ use crate::{cg::prelude::*, sk::sk_matrix}; +use skia_safe::gradient_shader::{Gradient, GradientColors, Interpolation}; -fn build_gradient_stops(stops: &[GradientStop], opacity: f32) -> (Vec, Vec) { +fn build_gradient_stops( + stops: &[GradientStop], + opacity: f32, +) -> (Vec, Vec) { let mut colors = Vec::with_capacity(stops.len()); let mut positions = Vec::with_capacity(stops.len()); for stop in stops { let CGColor { r, g, b, a } = stop.color; let alpha = (a as f32 * opacity).round().clamp(0.0, 255.0) as u8; - colors.push(skia_safe::Color::from_argb(alpha, r, g, b)); + colors.push(skia_safe::Color4f::from(skia_safe::Color::from_argb( + alpha, r, g, b, + ))); positions.push(stop.offset); } (colors, positions) } +fn make_gradient<'a>( + colors: &'a [skia_safe::Color4f], + positions: &'a [f32], + tile_mode: skia_safe::TileMode, +) -> Gradient<'a> { + Gradient::new( + GradientColors::new(colors, Some(positions), tile_mode, None), + Interpolation::default(), + ) +} + pub fn gradient_paint(paint: &GradientPaint, size: (f32, f32)) -> skia_safe::Paint { match paint { GradientPaint::Linear(gradient) => linear_gradient_paint(gradient, size), @@ -38,18 +55,11 @@ pub fn linear_gradient_paint( let p1 = skia_safe::Point::new(uv1.u(), uv1.v()); let p2 = skia_safe::Point::new(uv2.u(), uv2.v()); - if let Some(shader) = skia_safe::Shader::linear_gradient( - (p1, p2), - &colors[..], - Some(&positions[..]), - gradient.tile_mode.into(), - None, - Some(&matrix), - ) { + let grad = make_gradient(&colors, &positions, gradient.tile_mode.into()); + if let Some(shader) = skia_safe::shaders::linear_gradient((p1, p2), &grad, Some(&matrix)) { paint.set_shader(shader); } - // Apply paint-level opacity using Skia's built-in alpha property paint.set_alpha_f(gradient.opacity); paint.set_anti_alias(true); paint @@ -69,27 +79,21 @@ pub fn linear_gradient_shader( let start_point = skia_safe::Point::new(start_uv.u(), start_uv.v()); let end_point = skia_safe::Point::new(end_uv.u(), end_uv.v()); - if let Some(shader) = skia_safe::Shader::linear_gradient( - (start_point, end_point), - &colors[..], - Some(&positions[..]), - gradient.tile_mode.into(), - None, - Some(&matrix), - ) { - // Apply paint-level opacity at the shader level for stacking - if gradient.opacity < 1.0 { - let opacity_color = - skia_safe::Color::from_argb((gradient.opacity * 255.0) as u8, 255, 255, 255); - let opacity_shader = skia_safe::shaders::color(opacity_color); - let final_shader = - skia_safe::shaders::blend(skia_safe::BlendMode::DstIn, shader, opacity_shader); - Some(final_shader) - } else { - Some(shader) - } + let grad = make_gradient(&colors, &positions, gradient.tile_mode.into()); + let shader = + skia_safe::shaders::linear_gradient((start_point, end_point), &grad, Some(&matrix))?; + + if gradient.opacity < 1.0 { + let opacity_color = + skia_safe::Color::from_argb((gradient.opacity * 255.0) as u8, 255, 255, 255); + let opacity_shader = skia_safe::shaders::color(opacity_color); + Some(skia_safe::shaders::blend( + skia_safe::BlendMode::DstIn, + shader, + opacity_shader, + )) } else { - None + Some(shader) } } @@ -103,19 +107,13 @@ pub fn radial_gradient_paint( let mut matrix = skia_safe::Matrix::scale((x, y)); matrix.pre_concat(&sk_matrix(gradient.transform.matrix)); - if let Some(shader) = skia_safe::Shader::radial_gradient( - (0.5, 0.5), - 0.5, - &colors[..], - Some(&positions[..]), - gradient.tile_mode.into(), - None, - Some(&matrix), - ) { + let grad = make_gradient(&colors, &positions, gradient.tile_mode.into()); + if let Some(shader) = + skia_safe::shaders::radial_gradient(((0.5_f32, 0.5_f32), 0.5_f32), &grad, Some(&matrix)) + { paint.set_shader(shader); } - // Apply paint-level opacity using Skia's built-in alpha property paint.set_alpha_f(gradient.opacity); paint.set_anti_alias(true); paint @@ -130,52 +128,41 @@ pub fn radial_gradient_shader( let mut matrix = skia_safe::Matrix::scale((x, y)); matrix.pre_concat(&sk_matrix(gradient.transform.matrix)); - if let Some(shader) = skia_safe::Shader::radial_gradient( - (0.5, 0.5), - 0.5, - &colors[..], - Some(&positions[..]), - gradient.tile_mode.into(), - None, - Some(&matrix), - ) { - // Apply paint-level opacity at the shader level for stacking - if gradient.opacity < 1.0 { - let opacity_color = - skia_safe::Color::from_argb((gradient.opacity * 255.0) as u8, 255, 255, 255); - let opacity_shader = skia_safe::shaders::color(opacity_color); - let final_shader = - skia_safe::shaders::blend(skia_safe::BlendMode::DstIn, shader, opacity_shader); - Some(final_shader) - } else { - Some(shader) - } + let grad = make_gradient(&colors, &positions, gradient.tile_mode.into()); + let shader = + skia_safe::shaders::radial_gradient(((0.5_f32, 0.5_f32), 0.5_f32), &grad, Some(&matrix))?; + + if gradient.opacity < 1.0 { + let opacity_color = + skia_safe::Color::from_argb((gradient.opacity * 255.0) as u8, 255, 255, 255); + let opacity_shader = skia_safe::shaders::color(opacity_color); + Some(skia_safe::shaders::blend( + skia_safe::BlendMode::DstIn, + shader, + opacity_shader, + )) } else { - None + Some(shader) } } pub fn sweep_gradient_paint(gradient: &SweepGradientPaint, (x, y): (f32, f32)) -> skia_safe::Paint { let mut paint = skia_safe::Paint::default(); - let (colors, positions) = build_gradient_stops(&gradient.stops, 1.0); let mut matrix = skia_safe::Matrix::scale((x, y)); matrix.pre_concat(&sk_matrix(gradient.transform.matrix)); - if let Some(shader) = skia_safe::Shader::sweep_gradient( - (0.5, 0.5), - skia_safe::gradient_shader::GradientShaderColors::Colors(&colors), - Some(&positions[..]), - skia_safe::TileMode::Clamp, - Some((0.0, 360.0)), - None, + let grad = make_gradient(&colors, &positions, skia_safe::TileMode::Clamp); + if let Some(shader) = skia_safe::shaders::sweep_gradient( + (0.5_f32, 0.5_f32), + (0.0_f32, 360.0_f32), + &grad, Some(&matrix), ) { paint.set_shader(shader); } - // Apply paint-level opacity using Skia's built-in alpha property paint.set_alpha_f(gradient.opacity); paint.set_anti_alias(true); paint @@ -190,28 +177,25 @@ pub fn sweep_gradient_shader( let mut matrix = skia_safe::Matrix::scale((x, y)); matrix.pre_concat(&sk_matrix(gradient.transform.matrix)); - if let Some(shader) = skia_safe::Shader::sweep_gradient( - (0.5, 0.5), - skia_safe::gradient_shader::GradientShaderColors::Colors(&colors), - Some(&positions[..]), - skia_safe::TileMode::Clamp, - Some((0.0, 360.0)), - None, + let grad = make_gradient(&colors, &positions, skia_safe::TileMode::Clamp); + let shader = skia_safe::shaders::sweep_gradient( + (0.5_f32, 0.5_f32), + (0.0_f32, 360.0_f32), + &grad, Some(&matrix), - ) { - // Apply paint-level opacity at the shader level for stacking - if gradient.opacity < 1.0 { - let opacity_color = - skia_safe::Color::from_argb((gradient.opacity * 255.0) as u8, 255, 255, 255); - let opacity_shader = skia_safe::shaders::color(opacity_color); - let final_shader = - skia_safe::shaders::blend(skia_safe::BlendMode::DstIn, shader, opacity_shader); - Some(final_shader) - } else { - Some(shader) - } + )?; + + if gradient.opacity < 1.0 { + let opacity_color = + skia_safe::Color::from_argb((gradient.opacity * 255.0) as u8, 255, 255, 255); + let opacity_shader = skia_safe::shaders::color(opacity_color); + Some(skia_safe::shaders::blend( + skia_safe::BlendMode::DstIn, + shader, + opacity_shader, + )) } else { - None + Some(shader) } } @@ -223,14 +207,9 @@ pub fn diamond_gradient_paint( let (colors, positions) = build_gradient_stops(&gradient.stops, 1.0); - let base = skia_safe::Shader::linear_gradient( - ((0.0, 0.0), (1.0, 0.0)), - &colors[..], - Some(&positions[..]), - skia_safe::TileMode::Clamp, - None, - None, - ); + let grad = make_gradient(&colors, &positions, skia_safe::TileMode::Clamp); + let base = + skia_safe::shaders::linear_gradient(((0.0_f32, 0.0_f32), (1.0_f32, 0.0_f32)), &grad, None); if let Some(base_shader) = base { const SKSL: &str = r#" @@ -257,7 +236,6 @@ pub fn diamond_gradient_paint( } } - // Apply paint-level opacity using Skia's built-in alpha property paint.set_alpha_f(gradient.opacity); paint.set_anti_alias(true); paint @@ -269,60 +247,41 @@ pub fn diamond_gradient_shader( ) -> Option { let (colors, positions) = build_gradient_stops(&gradient.stops, 1.0); - let base = skia_safe::Shader::linear_gradient( - ((0.0, 0.0), (1.0, 0.0)), - &colors[..], - Some(&positions[..]), - skia_safe::TileMode::Clamp, - None, - None, - ); + let grad = make_gradient(&colors, &positions, skia_safe::TileMode::Clamp); + let base = + skia_safe::shaders::linear_gradient(((0.0_f32, 0.0_f32), (1.0_f32, 0.0_f32)), &grad, None)?; + + const SKSL: &str = r#" + uniform shader gradient; + half4 main(float2 coord) { + float2 p = coord - float2(0.5, 0.5); + float t = (abs(p.x) + abs(p.y)) * 2.0; + t = clamp(t, 0.0, 1.0); + return gradient.eval(float2(t, 0.0)); + } + "#; - if let Some(base_shader) = base { - const SKSL: &str = r#" - uniform shader gradient; - half4 main(float2 coord) { - float2 p = coord - float2(0.5, 0.5); - float t = (abs(p.x) + abs(p.y)) * 2.0; - t = clamp(t, 0.0, 1.0); - return gradient.eval(float2(t, 0.0)); - } - "#; + let effect = skia_safe::RuntimeEffect::make_for_shader(SKSL, None).ok()?; - if let Ok(effect) = skia_safe::RuntimeEffect::make_for_shader(SKSL, None) { - let mut matrix = skia_safe::Matrix::scale((x, y)); - matrix.pre_concat(&sk_matrix(gradient.transform.matrix)); + let mut matrix = skia_safe::Matrix::scale((x, y)); + matrix.pre_concat(&sk_matrix(gradient.transform.matrix)); - if let Some(shader) = effect.make_shader( - skia_safe::Data::new_copy(&[]), - &[base_shader.into()], - Some(&matrix), - ) { - // Apply paint-level opacity at the shader level for stacking - if gradient.opacity < 1.0 { - let opacity_color = skia_safe::Color::from_argb( - (gradient.opacity * 255.0) as u8, - 255, - 255, - 255, - ); - let opacity_shader = skia_safe::shaders::color(opacity_color); - let final_shader = skia_safe::shaders::blend( - skia_safe::BlendMode::DstIn, - shader, - opacity_shader, - ); - Some(final_shader) - } else { - Some(shader) - } - } else { - None - } - } else { - None - } + let shader = effect.make_shader( + skia_safe::Data::new_copy(&[]), + &[base.into()], + Some(&matrix), + )?; + + if gradient.opacity < 1.0 { + let opacity_color = + skia_safe::Color::from_argb((gradient.opacity * 255.0) as u8, 255, 255, 255); + let opacity_shader = skia_safe::shaders::color(opacity_color); + Some(skia_safe::shaders::blend( + skia_safe::BlendMode::DstIn, + shader, + opacity_shader, + )) } else { - None + Some(shader) } } diff --git a/crates/grida-dev/Cargo.toml b/crates/grida-dev/Cargo.toml index 97edadd8fa..25f0d62860 100644 --- a/crates/grida-dev/Cargo.toml +++ b/crates/grida-dev/Cargo.toml @@ -26,7 +26,7 @@ glutin = "0.32.0" glutin-winit = "0.5.0" raw-window-handle = "0.6.0" winit = "0.30.0" -skia-safe = { version = "0.91.0", features = [ +skia-safe = { version = "0.93.1", features = [ "gpu", "gl", "textlayout", From 5830e68c987f46ac3d3294982b7dc91a6d282038 Mon Sep 17 00:00:00 2001 From: Universe Date: Sun, 29 Mar 2026 06:27:39 +0900 Subject: [PATCH 08/15] fix(cg): migrate remaining deprecated gradient API in examples Update golden_sk_mask, golden_sk_text_backdrop_blur_path, and skia_bench_cache_image examples from Shader::*_gradient() to shaders::*_gradient() with the new Gradient/GradientColors types. Resolves all cargo check warnings. Co-Authored-By: Claude Opus 4.6 --- .../grida-canvas/examples/golden_sk_mask.rs | 38 ++++++++----------- .../golden_sk_text_backdrop_blur_path.rs | 15 +++----- .../skia_bench/skia_bench_cache_image.rs | 27 +++++++------ 3 files changed, 37 insertions(+), 43 deletions(-) diff --git a/crates/grida-canvas/examples/golden_sk_mask.rs b/crates/grida-canvas/examples/golden_sk_mask.rs index 0aa39531d6..132ac62edd 100644 --- a/crates/grida-canvas/examples/golden_sk_mask.rs +++ b/crates/grida-canvas/examples/golden_sk_mask.rs @@ -1,7 +1,7 @@ use cg::cg::prelude::*; use skia_safe::{ - luma_color_filter, surfaces, BlendMode, Color, Paint, Path, PathBuilder, Point, Rect, Shader, - TileMode, + gradient_shader, luma_color_filter, surfaces, BlendMode, Color, Paint, Path, PathBuilder, + Point, Rect, TileMode, }; fn draw_demo_content(canvas: &skia_safe::Canvas, area: Rect) { @@ -89,16 +89,13 @@ fn draw_alpha_mask(canvas: &skia_safe::Canvas, area: Rect) { Color::from_argb(0, 128, 128, 128), // transparent grey at edge ]; let positions = [0.0_f32, 1.0_f32]; - let shader = Shader::radial_gradient( - center, - radius, - &colors[..], - Some(&positions[..]), - TileMode::Clamp, - None, - None, - ) - .unwrap(); + let colors4f: Vec<_> = colors.iter().map(|c| skia_safe::Color4f::from(*c)).collect(); + let grad = gradient_shader::Gradient::new( + gradient_shader::GradientColors::new(&colors4f, Some(&positions), TileMode::Clamp, None), + gradient_shader::Interpolation::default(), + ); + let shader = + skia_safe::shaders::radial_gradient((center, radius), &grad, None).unwrap(); // Apply mask via DstIn blending let mut mask_paint = Paint::default(); @@ -125,16 +122,13 @@ fn draw_luminance_mask(canvas: &skia_safe::Canvas, area: Rect) { Color::from_argb(255, 40, 40, 40), // dark grey edge ]; let positions = [0.0_f32, 1.0_f32]; - let shader = Shader::radial_gradient( - center, - radius, - &colors[..], - Some(&positions[..]), - TileMode::Clamp, - None, - None, - ) - .unwrap(); + let colors4f: Vec<_> = colors.iter().map(|c| skia_safe::Color4f::from(*c)).collect(); + let grad = gradient_shader::Gradient::new( + gradient_shader::GradientColors::new(&colors4f, Some(&positions), TileMode::Clamp, None), + gradient_shader::Interpolation::default(), + ); + let shader = + skia_safe::shaders::radial_gradient((center, radius), &grad, None).unwrap(); // Built-in luma color filter to convert luminance to alpha let cf = luma_color_filter::new(); diff --git a/crates/grida-canvas/examples/golden_sk_text_backdrop_blur_path.rs b/crates/grida-canvas/examples/golden_sk_text_backdrop_blur_path.rs index 40d1d7f0fc..bbdfa71409 100644 --- a/crates/grida-canvas/examples/golden_sk_text_backdrop_blur_path.rs +++ b/crates/grida-canvas/examples/golden_sk_text_backdrop_blur_path.rs @@ -94,15 +94,12 @@ fn draw_background_image(canvas: &sk::Canvas, width: i32, height: i32) { ]; let positions = vec![0.0, 0.33, 0.66, 1.0]; - if let Some(shader) = sk::Shader::radial_gradient( - center, - radius, - &*colors, - Some(&*positions), - sk::TileMode::Clamp, - None, - None, - ) { + let colors4f: Vec<_> = colors.iter().map(|c| sk::Color4f::from(*c)).collect(); + let grad = sk::gradient_shader::Gradient::new( + sk::gradient_shader::GradientColors::new(&colors4f, Some(&positions), sk::TileMode::Clamp, None), + sk::gradient_shader::Interpolation::default(), + ); + if let Some(shader) = sk::shaders::radial_gradient((center, radius), &grad, None) { paint.set_shader(shader); canvas.draw_rect(Rect::from_wh(width as f32, height as f32), &paint); } diff --git a/crates/grida-canvas/examples/skia_bench/skia_bench_cache_image.rs b/crates/grida-canvas/examples/skia_bench/skia_bench_cache_image.rs index 1a6a590f93..a0f2b90857 100644 --- a/crates/grida-canvas/examples/skia_bench/skia_bench_cache_image.rs +++ b/crates/grida-canvas/examples/skia_bench/skia_bench_cache_image.rs @@ -118,12 +118,14 @@ fn draw_complex(canvas: &Canvas) { Color::from_argb(0xFF, 0x00, 0x00, 0xFF), // Blue ]; let positions = [0.0, 0.5, 1.0]; - let shader = Shader::linear_gradient( + let colors4f: Vec<_> = colors.iter().map(|c| Color4f::from(*c)).collect(); + let grad = gradient_shader::Gradient::new( + gradient_shader::GradientColors::new(&colors4f, Some(&positions), TileMode::Clamp, None), + gradient_shader::Interpolation::default(), + ); + let shader = shaders::linear_gradient( (Point::new(0.0, 0.0), Point::new(width, height)), - &colors[..], - Some(&positions[..]), - TileMode::Clamp, - None, + &grad, None, ) .unwrap(); @@ -196,13 +198,14 @@ fn draw_complex(canvas: &Canvas) { Color::from_argb(0x00, 0xFF, 0xFF, 0xFF), // Transparent ]; let radial_positions = [0.0, 1.0]; - let radial_shader = Shader::radial_gradient( - Point::new(width / 2.0, height / 2.0), - width.min(height) / 2.0, - &radial_colors[..], - Some(&radial_positions[..]), - TileMode::Clamp, - None, + let radial_colors4f: Vec<_> = radial_colors.iter().map(|c| Color4f::from(*c)).collect(); + let radial_grad = gradient_shader::Gradient::new( + gradient_shader::GradientColors::new(&radial_colors4f, Some(&radial_positions), TileMode::Clamp, None), + gradient_shader::Interpolation::default(), + ); + let radial_shader = shaders::radial_gradient( + (Point::new(width / 2.0, height / 2.0), width.min(height) / 2.0), + &radial_grad, None, ) .unwrap(); From 6d92009312dd53fb2babc6eaf0cb4216275b6a6d Mon Sep 17 00:00:00 2001 From: Universe Date: Sun, 29 Mar 2026 07:17:24 +0900 Subject: [PATCH 09/15] fmt --- crates/grida-canvas/examples/golden_sk_mask.rs | 16 ++++++++++------ .../golden_sk_text_backdrop_blur_path.rs | 7 ++++++- .../skia_bench/skia_bench_cache_image.rs | 12 ++++++++++-- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/crates/grida-canvas/examples/golden_sk_mask.rs b/crates/grida-canvas/examples/golden_sk_mask.rs index 132ac62edd..90e472f19f 100644 --- a/crates/grida-canvas/examples/golden_sk_mask.rs +++ b/crates/grida-canvas/examples/golden_sk_mask.rs @@ -89,13 +89,15 @@ fn draw_alpha_mask(canvas: &skia_safe::Canvas, area: Rect) { Color::from_argb(0, 128, 128, 128), // transparent grey at edge ]; let positions = [0.0_f32, 1.0_f32]; - let colors4f: Vec<_> = colors.iter().map(|c| skia_safe::Color4f::from(*c)).collect(); + let colors4f: Vec<_> = colors + .iter() + .map(|c| skia_safe::Color4f::from(*c)) + .collect(); let grad = gradient_shader::Gradient::new( gradient_shader::GradientColors::new(&colors4f, Some(&positions), TileMode::Clamp, None), gradient_shader::Interpolation::default(), ); - let shader = - skia_safe::shaders::radial_gradient((center, radius), &grad, None).unwrap(); + let shader = skia_safe::shaders::radial_gradient((center, radius), &grad, None).unwrap(); // Apply mask via DstIn blending let mut mask_paint = Paint::default(); @@ -122,13 +124,15 @@ fn draw_luminance_mask(canvas: &skia_safe::Canvas, area: Rect) { Color::from_argb(255, 40, 40, 40), // dark grey edge ]; let positions = [0.0_f32, 1.0_f32]; - let colors4f: Vec<_> = colors.iter().map(|c| skia_safe::Color4f::from(*c)).collect(); + let colors4f: Vec<_> = colors + .iter() + .map(|c| skia_safe::Color4f::from(*c)) + .collect(); let grad = gradient_shader::Gradient::new( gradient_shader::GradientColors::new(&colors4f, Some(&positions), TileMode::Clamp, None), gradient_shader::Interpolation::default(), ); - let shader = - skia_safe::shaders::radial_gradient((center, radius), &grad, None).unwrap(); + let shader = skia_safe::shaders::radial_gradient((center, radius), &grad, None).unwrap(); // Built-in luma color filter to convert luminance to alpha let cf = luma_color_filter::new(); diff --git a/crates/grida-canvas/examples/golden_sk_text_backdrop_blur_path.rs b/crates/grida-canvas/examples/golden_sk_text_backdrop_blur_path.rs index bbdfa71409..98a745a92e 100644 --- a/crates/grida-canvas/examples/golden_sk_text_backdrop_blur_path.rs +++ b/crates/grida-canvas/examples/golden_sk_text_backdrop_blur_path.rs @@ -96,7 +96,12 @@ fn draw_background_image(canvas: &sk::Canvas, width: i32, height: i32) { let colors4f: Vec<_> = colors.iter().map(|c| sk::Color4f::from(*c)).collect(); let grad = sk::gradient_shader::Gradient::new( - sk::gradient_shader::GradientColors::new(&colors4f, Some(&positions), sk::TileMode::Clamp, None), + sk::gradient_shader::GradientColors::new( + &colors4f, + Some(&positions), + sk::TileMode::Clamp, + None, + ), sk::gradient_shader::Interpolation::default(), ); if let Some(shader) = sk::shaders::radial_gradient((center, radius), &grad, None) { diff --git a/crates/grida-canvas/examples/skia_bench/skia_bench_cache_image.rs b/crates/grida-canvas/examples/skia_bench/skia_bench_cache_image.rs index a0f2b90857..d8f11212c6 100644 --- a/crates/grida-canvas/examples/skia_bench/skia_bench_cache_image.rs +++ b/crates/grida-canvas/examples/skia_bench/skia_bench_cache_image.rs @@ -200,11 +200,19 @@ fn draw_complex(canvas: &Canvas) { let radial_positions = [0.0, 1.0]; let radial_colors4f: Vec<_> = radial_colors.iter().map(|c| Color4f::from(*c)).collect(); let radial_grad = gradient_shader::Gradient::new( - gradient_shader::GradientColors::new(&radial_colors4f, Some(&radial_positions), TileMode::Clamp, None), + gradient_shader::GradientColors::new( + &radial_colors4f, + Some(&radial_positions), + TileMode::Clamp, + None, + ), gradient_shader::Interpolation::default(), ); let radial_shader = shaders::radial_gradient( - (Point::new(width / 2.0, height / 2.0), width.min(height) / 2.0), + ( + Point::new(width / 2.0, height / 2.0), + width.min(height) / 2.0, + ), &radial_grad, None, ) From 008ea91aa3e610601464b541a6f11674fda82076 Mon Sep 17 00:00:00 2001 From: Universe Date: Sun, 29 Mar 2026 08:00:39 +0900 Subject: [PATCH 10/15] feat(agents): add vision skill for local image querying via Ollama MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces .agents/skills/vision/ with ask.py — a uv-run script that queries images through a local Ollama vision model (qwen3.5, gemma3, etc.) without loading them into the main agent context. Supports describe, custom prompts, model selection, ping health check, and system info. Co-Authored-By: Claude Opus 4.6 --- .agents/skills/vision/SKILL.md | 171 ++++++++++++++ .agents/skills/vision/scripts/ask.py | 328 +++++++++++++++++++++++++++ 2 files changed, 499 insertions(+) create mode 100644 .agents/skills/vision/SKILL.md create mode 100644 .agents/skills/vision/scripts/ask.py diff --git a/.agents/skills/vision/SKILL.md b/.agents/skills/vision/SKILL.md new file mode 100644 index 0000000000..348b75f29f --- /dev/null +++ b/.agents/skills/vision/SKILL.md @@ -0,0 +1,171 @@ +--- +name: vision +description: > + Query images with a local Ollama vision model without loading the image + into the main agent context. Use when you need to describe a screenshot, + check whether rendered content is present, detect overlapping elements, or + ask any visual question about a PNG/JPEG/WebP file. Requires Ollama running + locally with a vision-capable model (qwen3.5, gemma3, llava, etc.). + Script: .agents/skills/vision/scripts/ask.py. + Trigger phrases: "describe image", "what does this screenshot show", + "does the canvas contain content", "check screenshot visually", + "look at this image", "any overlapping elements", "vision query". +--- + +# Vision — Local Image Querying via Ollama + +Ask natural-language questions about images without passing them to the main +agent as visual input. Useful for verifying screenshots, annotating assets, +or building automated checks around visual output. + +## When to Use This Skill + +- Describing a screenshot for a PR description or user-facing document +- Checking whether an automated browser run produced visible canvas content +- Asking "do any elements overlap?" on a rendered output +- Any question where the answer is in the pixels but you don't want to use + vision tokens in the main context + +--- + +## Quick Reference + +All commands use `uv run` — dependencies are installed automatically. + +```sh +SCRIPT=.agents/skills/vision/scripts/ask.py + +# health check (fast, no image, confirms Ollama + model respond) +uv run $SCRIPT --ping + +# system info — memory, storage, installed models +uv run $SCRIPT --info +uv run $SCRIPT --memory +uv run $SCRIPT --storage + +# describe an image (default prompt) +uv run $SCRIPT path/to/image.png + +# explicit shortcut +uv run $SCRIPT path/to/image.png describe + +# custom question +uv run $SCRIPT path/to/image.png \ + --prompt "Do you see any overlapping UI elements?" + +uv run $SCRIPT canvas.png \ + --prompt "Does this canvas contain any designed content, or is it empty?" + +# use a specific model +uv run $SCRIPT image.png --model gemma3 + +# list available vision-capable models +uv run $SCRIPT --list-models +``` + +--- + +## Prerequisites + +Ollama must be running locally. The script connects to `http://localhost:11434` +and fails immediately if it cannot reach it. + +```sh +# start Ollama (if not already running) +ollama serve + +# install a vision model (first time, pick one) +ollama pull qwen3.5 # recommended — best low-cost vision as of 2026-03 +ollama pull gemma3 # alternative +ollama pull llava # widely available fallback +``` + +The script **does not install models**. If no vision model is available it +prints the list of installed models and a `pull` suggestion, then exits. + +**`uv` is required** to run the script (handles dependency installation +automatically). No `requirements.txt` or manual `pip install` needed. + +--- + +## Model Selection + +When `--model` is not specified the script picks the first installed model +from this preference list (updated periodically): + +| Priority | Model | Notes | +| -------- | ----------------- | ---------------------------------------- | +| 1 | `qwen3.5` | Best low-cost vision model as of 2026-03 | +| 2 | `qwen2.5vl` | Previous generation, still strong | +| 3 | `gemma3` | Strong alternative, multimodal | +| 4 | `llama3.2-vision` | Meta vision variant | +| 5 | `llava` | Widely installed fallback | + +A model is considered vision-capable when its name contains: `qwen`, `gemma3`, +`vl`, `vision`, `llava`, `moondream`, `minicpm-v`, or similar fragments. +Non-vision text models are filtered out automatically. + +--- + +## System Info + +Before running a heavy query, check whether the machine has enough resources. +This is optional — the script does not enforce limits — but useful context +for deciding whether to proceed or skip. + +```sh +uv run $SCRIPT --info # memory + storage + model list +uv run $SCRIPT --memory # just memory +uv run $SCRIPT --storage # just storage +``` + +Tip: on machines with ≤8 GB RAM, large vision models may cause swapping or +OOM. Consider using a smaller model variant or skipping the query. + +--- + +## Behavior + +- **Fails fast** if Ollama is unreachable or no vision model is installed. + Exit code is non-zero; the error message includes a `hint` or `pull` command. +- **Sequential only** — Ollama is a single-worker process. Never call `ask.py` + in parallel (e.g. two concurrent tool calls). Queue calls one at a time. +- **No side effects** beyond the local Ollama process. +- **Auto-installs deps** via `uv` inline script metadata (PEP 723). Only + dependency is the `ollama` Python package. +- Supported formats: `.png`, `.jpg`, `.jpeg`, `.webp`, `.gif`, `.bmp`. + +--- + +## Typical Agent Workflow + +1. A tool (browser automation, screenshot capture, golden renderer) writes + an image to disk. +2. Call `ask.py` with a targeted prompt suited to the task. +3. Parse the text response to decide the next action. + +```sh +# Quick sanity check first +uv run $SCRIPT --ping + +# Verify a browser screenshot has content before including it in a doc +uv run $SCRIPT /tmp/preview.png \ + --prompt "Answer with YES or NO: does this screenshot show any visible UI content, shapes, or text?" + +# Describe a golden render for a PR description +uv run $SCRIPT crates/grida-canvas/goldens/progressive_blur.png \ + --prompt "Describe what visual effect is shown. Be specific about blur, colors, and shapes." +``` + +--- + +## Troubleshooting + +| Symptom | Cause | Fix | +| -------------------------------- | -------------------------------- | ----------------------------------------- | +| `cannot reach Ollama` | Ollama not running | `ollama serve` | +| `no vision-capable models found` | Only text models installed | `ollama pull qwen3.5` | +| `model 'X' is not available` | Model name typo or not installed | `--list-models` to see what's installed | +| Slow response | Large model on CPU | Use a smaller variant (e.g. `qwen3.5:3b`) | +| Vague or wrong answer | Generic prompt | Write a more specific `--prompt` | +| `'ollama' package not found` | Not using `uv run` | Run with `uv run ask.py` instead | diff --git a/.agents/skills/vision/scripts/ask.py b/.agents/skills/vision/scripts/ask.py new file mode 100644 index 0000000000..c228d87484 --- /dev/null +++ b/.agents/skills/vision/scripts/ask.py @@ -0,0 +1,328 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.11" +# dependencies = ["ollama"] +# /// +""" +ask.py — Query an image with a local Ollama vision model. + +Run with uv (auto-installs dependencies): + uv run ask.py # describe the image + uv run ask.py describe # explicit describe + uv run ask.py --prompt "..." # custom question + uv run ask.py --model # specify model + uv run ask.py --list-models # show available vision models + uv run ask.py --ping # quick health check + uv run ask.py --memory # show system memory info + uv run ask.py --storage # show available disk storage + uv run ask.py --info # show memory + storage + models +""" + +import argparse +import shutil +import sys +from pathlib import Path + +try: + import ollama +except ImportError: + print( + "error: 'ollama' package not found.\n" + " run this script with: uv run ask.py ...\n" + " or install manually: pip install ollama", + file=sys.stderr, + ) + sys.exit(1) + +# Ordered by preference. First available wins when --model is not specified. +# Update this list every ~6 months as better low-cost vision models emerge. +# Last updated: 2026-03 — qwen3.5 added as top pick. +PREFERRED_VISION_MODELS = [ + "qwen3.5", + "qwen2.5vl", + "gemma3", + "llama3.2-vision", + "llava", +] + +# Name fragments that reliably indicate vision capability. +VISION_NAME_FRAGMENTS = [ + "qwen", + "gemma3", + "vl", + "vision", + "llava", + "moondream", + "bakllava", + "cogvlm", + "minicpm-v", + "phi3.5-vision", +] + +DESCRIBE_PROMPT = ( + "Describe this image concisely. " + "Note layout, content, colors, and any visible text or UI elements." +) + + +# --------------------------------------------------------------------------- +# Ollama helpers +# --------------------------------------------------------------------------- + + +def _check_server() -> None: + """Fail fast if Ollama is not reachable.""" + try: + ollama.list() + except Exception as e: + print( + f"error: cannot reach Ollama — {e}\n" + "hint: make sure Ollama is running (`ollama serve`)", + file=sys.stderr, + ) + sys.exit(1) + + +def _list_models() -> list[str]: + """Return names of all installed models.""" + resp = ollama.list() + return [m.model for m in resp.models] + + +def is_vision_model(name: str) -> bool: + low = name.lower() + return any(frag in low for frag in VISION_NAME_FRAGMENTS) + + +def list_vision_models() -> list[str]: + return [m for m in _list_models() if is_vision_model(m)] + + +def pick_model(requested: str | None) -> str: + available = list_vision_models() + + if not available: + installed = _list_models() + if installed: + print( + f"error: no vision-capable models found.\n" + f" installed models: {', '.join(installed)}\n" + f" install one with: ollama pull qwen3.5", + file=sys.stderr, + ) + else: + print( + "error: no models installed.\n" + " install one with: ollama pull qwen3.5", + file=sys.stderr, + ) + sys.exit(1) + + if requested: + matches = [m for m in available if m == requested or m.startswith(requested + ":")] + if not matches: + print( + f"error: model '{requested}' is not available or not a vision model.\n" + f" available vision models: {', '.join(available)}", + file=sys.stderr, + ) + sys.exit(1) + return matches[0] + + # Pick first preferred model that is installed. + for preferred in PREFERRED_VISION_MODELS: + for installed in available: + if installed == preferred or installed.startswith(preferred + ":"): + return installed + + return available[0] + + +# --------------------------------------------------------------------------- +# Commands +# --------------------------------------------------------------------------- + + +def cmd_ping(model: str) -> None: + """Send a trivial text prompt — no image — to verify model responds.""" + print(f"pinging {model} …") + resp = ollama.generate(model=model, prompt="Reply with exactly: pong") + print(resp.response.strip()) + + +def cmd_ask(image_path: Path, prompt: str, model: str) -> None: + """Send an image + prompt to the model.""" + resp = ollama.generate( + model=model, + prompt=prompt, + images=[str(image_path)], + ) + print(resp.response.strip()) + + +def cmd_list_models() -> None: + _check_server() + all_models = _list_models() + if not all_models: + print("No models installed. Install one with: ollama pull qwen3.5") + return + + vision = [m for m in all_models if is_vision_model(m)] + other = [m for m in all_models if not is_vision_model(m)] + + if vision: + print("Vision-capable models (usable with ask.py):") + for name in vision: + print(f" {name}") + else: + print("No vision-capable models found.") + print("Install one with: ollama pull qwen3.5") + + if other: + print("\nOther installed models (text-only, not usable with ask.py):") + for name in other: + print(f" {name}") + + +# --------------------------------------------------------------------------- +# System info +# --------------------------------------------------------------------------- + + +def _gb(n: int) -> str: + return f"{n / 1024 ** 3:.1f} GB" + + +def cmd_memory() -> None: + import platform + + system = platform.system() + + if system == "Darwin": + import subprocess + + try: + pages_free = 0 + pages_inactive = 0 + page_size = 4096 + result = subprocess.run(["vm_stat"], capture_output=True, text=True, timeout=5) + for line in result.stdout.splitlines(): + if "page size of" in line: + page_size = int(line.split()[-2]) + elif line.startswith("Pages free:"): + pages_free = int(line.split()[-1].rstrip(".")) + elif line.startswith("Pages inactive:"): + pages_inactive = int(line.split()[-1].rstrip(".")) + result2 = subprocess.run( + ["sysctl", "-n", "hw.memsize"], capture_output=True, text=True, timeout=5 + ) + total = int(result2.stdout.strip()) + available = (pages_free + pages_inactive) * page_size + pct_free = available / total * 100 + print(f"memory: {_gb(total)} total, {_gb(available)} available ({pct_free:.0f}% free)") + except Exception as e: + print(f"memory: unavailable ({e})") + + elif system == "Linux": + try: + info = {} + with open("/proc/meminfo") as f: + for line in f: + key, val = line.split(":") + info[key.strip()] = int(val.strip().split()[0]) * 1024 + total = info["MemTotal"] + available = info.get("MemAvailable", info.get("MemFree", 0)) + pct_free = available / total * 100 + print(f"memory: {_gb(total)} total, {_gb(available)} available ({pct_free:.0f}% free)") + except Exception as e: + print(f"memory: unavailable ({e})") + + else: + print("memory: unavailable (unsupported platform)") + + +def cmd_storage(path: str = "/") -> None: + usage = shutil.disk_usage(path) + pct_free = usage.free / usage.total * 100 + print(f"storage: {_gb(usage.total)} total, {_gb(usage.free)} available ({pct_free:.0f}% free) [{path}]") + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Query an image with a local Ollama vision model.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__, + ) + parser.add_argument("image", nargs="?", help="Path to the image file") + parser.add_argument( + "shortcut", + nargs="?", + choices=["describe"], + help="Shortcut command (describe = default prompt)", + ) + parser.add_argument("--prompt", "-p", help="Custom question about the image") + parser.add_argument("--model", "-m", help="Ollama model name to use") + parser.add_argument("--list-models", "-l", action="store_true", help="List available vision models and exit") + parser.add_argument("--memory", action="store_true", help="Show system memory (total / available)") + parser.add_argument("--storage", action="store_true", help="Show disk storage (total / available)") + parser.add_argument("--info", action="store_true", help="Show memory + storage + models") + parser.add_argument("--ping", action="store_true", help="Quick health check — no image needed") + + args = parser.parse_args() + + # --ping + if args.ping: + _check_server() + model = pick_model(args.model) + cmd_ping(model) + return + + # --list-models + if args.list_models: + cmd_list_models() + return + + # --info combines memory + storage + models + if args.info: + cmd_memory() + cmd_storage() + print() + cmd_list_models() + return + + # --memory / --storage standalone + if args.memory or args.storage: + if args.memory: + cmd_memory() + if args.storage: + cmd_storage() + return + + # Image query + if not args.image: + parser.print_help() + sys.exit(1) + + image_path = Path(args.image) + if not image_path.exists(): + print(f"error: file not found: {image_path}", file=sys.stderr) + sys.exit(1) + + suffix = image_path.suffix.lower() + if suffix not in {".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp"}: + print(f"error: unsupported image format: {suffix}", file=sys.stderr) + sys.exit(1) + + _check_server() + prompt = args.prompt or DESCRIBE_PROMPT + model = pick_model(args.model) + cmd_ask(image_path, prompt, model) + + +if __name__ == "__main__": + main() From 325be707de9f879a8da456e916be84cda20bb405 Mon Sep 17 00:00:00 2001 From: Universe Date: Sun, 29 Mar 2026 08:12:58 +0900 Subject: [PATCH 11/15] feat(tray): add positional layout encoding and update documentation - Introduced `encode_positional_layout` function for tray nodes, allowing for layout encoding without container or child styles. - Updated tray documentation to clarify Figma export limitations and enhanced node hierarchy details. - Adjusted event target reducer to handle tray-specific insertion logic. - Modified test case metadata to reflect area changes and updated date. --- crates/grida-canvas/src/io/io_grida_fbs.rs | 33 +++++++++++++++---- docs/editor/features/tray.md | 2 +- docs/wg/feat-tray/index.md | 13 ++++---- .../reducers/event-target.reducer.ts | 5 ++- test/canvas-tray-node-behavior.md | 4 +-- 5 files changed, 39 insertions(+), 18 deletions(-) diff --git a/crates/grida-canvas/src/io/io_grida_fbs.rs b/crates/grida-canvas/src/io/io_grida_fbs.rs index 574b19e94c..df02762c60 100644 --- a/crates/grida-canvas/src/io/io_grida_fbs.rs +++ b/crates/grida-canvas/src/io/io_grida_fbs.rs @@ -3391,6 +3391,31 @@ fn encode_shape_layout<'a, A: flatbuffers::Allocator + 'a>( ) } +/// Build a LayoutStyle for tray nodes (position + dimensions, no layout_container or layout_child). +fn encode_positional_layout<'a, A: flatbuffers::Allocator + 'a>( + fbb: &mut flatbuffers::FlatBufferBuilder<'a, A>, + position: &LayoutPositioningBasis, + dimensions: &LayoutDimensionStyle, +) -> flatbuffers::WIPOffset> { + let (pos_type, pos_offset) = encode_layout_position(fbb, position); + let dims = encode_dimensions_with_aspect( + fbb, + dimensions.layout_target_width, + dimensions.layout_target_height, + dimensions.layout_target_aspect_ratio, + ); + fbs::LayoutStyle::create( + fbb, + &fbs::LayoutStyleArgs { + layout_position_type: pos_type, + layout_position: pos_offset, + layout_dimensions: Some(dims), + layout_container: None, + layout_child: None, + }, + ) +} + /// Build a LayoutStyle for container nodes (position, dimensions, container style, child style). fn encode_container_layout<'a, A: flatbuffers::Allocator + 'a>( fbb: &mut flatbuffers::FlatBufferBuilder<'a, A>, @@ -3697,13 +3722,7 @@ fn encode_tray_node<'a, A: flatbuffers::Allocator + 'a>( ) -> flatbuffers::WIPOffset> { let sys = encode_system_node_trait(fbb, node_id, "", r.active, false); // Tray has position + dimensions but no layout_container or layout_child - let layout = encode_container_layout( - fbb, - &r.position, - &r.layout_dimensions, - &LayoutContainerStyle::default(), - &None, - ); + let layout = encode_positional_layout(fbb, &r.position, &r.layout_dimensions); let default_effects = LayerEffects::default(); let layer = encode_layer_trait( fbb, diff --git a/docs/editor/features/tray.md b/docs/editor/features/tray.md index d4889ac3f1..81d5afaa08 100644 --- a/docs/editor/features/tray.md +++ b/docs/editor/features/tray.md @@ -59,4 +59,4 @@ This follows the pattern: **F** = Frame (Container), **Shift + F** = Tray. ## Figma Compatibility -Trays map directly to Figma **Sections**. When you import a Figma file, Sections become Trays. The round-trip is clean — Trays export back as Sections. +Trays map directly to Figma **Sections**. When you import a Figma file, Sections become Trays. Export back to Figma Sections is not yet supported. diff --git a/docs/wg/feat-tray/index.md b/docs/wg/feat-tray/index.md index 57ba101e59..0e054ab6ac 100644 --- a/docs/wg/feat-tray/index.md +++ b/docs/wg/feat-tray/index.md @@ -44,7 +44,7 @@ The name "Tray" was chosen to reflect the node's passive, infrastructure-like na ## Node Hierarchy -``` +```text Scene +-- Tray ("Authentication flows") | +-- Container (Login screen) @@ -64,7 +64,7 @@ Scene - **Organizational grouping**: Cluster related Containers under a named, visible boundary when a Scene has many root-level Containers. - **Hierarchy participation**: A Tray is a real node in the scene graph. It appears in the layer panel, has children, and can be a parent. It is not metadata or an annotation. -- **Figma compatibility**: Maps directly to Figma `SECTION` on import/export. Round-trips cleanly. +- **Figma compatibility**: Maps directly to Figma `SECTION` on import. Export back to Figma `SECTION` is not yet implemented. - **Nestability**: Trays can contain other Trays. Nesting is shallow in practice but unrestricted in depth. - **Minimal cognitive load**: Users should never need to think about what a Tray "does" -- it doesn't do anything. @@ -73,7 +73,8 @@ Scene - **No layout**: No auto-layout, flex, grid, or positioning logic. Children are freely placed. This is permanent, not a v1 limitation. - **No rendering in output**: Invisible in exported designs. Canvas-only organizational aid. - **No clipping**: Children can visually extend beyond the Tray's bounds. -- **No styling**: No fills, strokes, effects, blend modes. Label and boundary indicator only. +- **No effects**: No shadows, blurs, or blend-mode overrides. Canvas-only boundary indicator. +- **Styling supported**: Fills, strokes, and corner radius are supported and rendered in the canvas editor. They are not visible in exported output. - **No nesting under non-Tray parents**: A Tray cannot be a child of Container, Group, or any other non-Tray node. Trays live at Scene level or nested under other Trays. This constraint is permanent. - **Does not replace Group**: Groups are temporary, transform-linked wrappers. Trays are persistent, named organizational boundaries. They coexist. @@ -88,15 +89,13 @@ Scene | **Layout** | None. Always `none`. Not configurable. | | **Rendering** | Canvas-only. Never appears in exported output. | | **Clipping** | None. Children can overflow. | -| **Styling** | Label and boundary only. No fills, strokes, or effects. | +| **Styling** | Fills, strokes, and corner radius supported. No effects (shadows, blurs). | ### Validation Invariants -``` +```text Tray.parent in { Scene, Tray } Tray.layout = none // invariant, not a default -Tray.fills = empty // not supported -Tray.strokes = empty // not supported Tray.effects = empty // not supported Tray.clip = false // invariant ``` diff --git a/editor/grida-canvas/reducers/event-target.reducer.ts b/editor/grida-canvas/reducers/event-target.reducer.ts index cf91af8305..9ee1834df4 100644 --- a/editor/grida-canvas/reducers/event-target.reducer.ts +++ b/editor/grida-canvas/reducers/event-target.reducer.ts @@ -138,7 +138,10 @@ function __self_evt_on_click( ); break; case "insert": - const parent = __get_insertion_target(draft); + const parent = + draft.tool.node === "tray" + ? __get_tray_insertion_target(draft) + : __get_insertion_target(draft); const nnode = initialNode( draft.tool.node, diff --git a/test/canvas-tray-node-behavior.md b/test/canvas-tray-node-behavior.md index ee42d51992..665c63cd70 100644 --- a/test/canvas-tray-node-behavior.md +++ b/test/canvas-tray-node-behavior.md @@ -2,11 +2,11 @@ id: TC-CANVAS-TRAY-001 title: Tray Node Hierarchy Constraints and Interaction Behavior module: canvas -area: hierarchy +area: tray tags: [tray, hierarchy, reparent, insert, container, scene, drag, auto-wrap] status: untested severity: high -date: 2026-03-29 +date: 2026-03-28 automatable: partial covered_by: - editor/grida-canvas/reducers/methods/__tests__/move-tray.test.ts From 42b7148e2d583b95a17260c16b199d60d84b7a47 Mon Sep 17 00:00:00 2001 From: Universe Date: Sun, 29 Mar 2026 18:11:07 +0900 Subject: [PATCH 12/15] feat(canvas-react): show tray title bars and label tray-child containers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror Rust collect_labeled_nodes() logic: trays get badge-style bars, root containers and tray-child containers get plain labels. Thread parentNodeId through FloatingBar → useSingleSelection so tray-child overlays reposition when the parent tray moves. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../viewport/surface-hooks.ts | 9 +- .../grida-canvas-react/viewport/surface.tsx | 121 +++++++++++++----- .../viewport/ui/floating-bar.tsx | 8 +- 3 files changed, 103 insertions(+), 35 deletions(-) diff --git a/editor/grida-canvas-react/viewport/surface-hooks.ts b/editor/grida-canvas-react/viewport/surface-hooks.ts index ce7c99e166..97b3ce6801 100644 --- a/editor/grida-canvas-react/viewport/surface-hooks.ts +++ b/editor/grida-canvas-react/viewport/surface-hooks.ts @@ -296,14 +296,21 @@ export function useSingleSelection( node_id: string, { enabled, + parentNodeId, }: { enabled: boolean; + /** When set, recompute position when this parent node changes (e.g. tray child containers). */ + parentNodeId?: string; } = { enabled: true } ): SurfaceSingleSelection | undefined { const instance = useCurrentEditor(); const { document, document_ctx } = useDocumentState(); const { transform } = useTransformState(); const node = document.nodes[node_id]; + // When a parent moves, the child's absolute position changes even though + // the child's own node data is unchanged. Including the parent node + // object in the effect deps lets us pick up that change cheaply. + const parentNode = parentNodeId ? document.nodes[parentNodeId] : undefined; const [data, setData] = useState( undefined @@ -411,7 +418,7 @@ export function useSingleSelection( }, }, }); - }, [node, node_id, transform, enabled]); + }, [node, node_id, transform, enabled, parentNode]); return data; } diff --git a/editor/grida-canvas-react/viewport/surface.tsx b/editor/grida-canvas-react/viewport/surface.tsx index 4a025a0f8b..6145fb96c0 100644 --- a/editor/grida-canvas-react/viewport/surface.tsx +++ b/editor/grida-canvas-react/viewport/surface.tsx @@ -665,29 +665,77 @@ function RootFramesBarOverlay() { const { document } = useDocumentState(); const scene = useCurrentSceneState(); - const rootframes = useMemo(() => { - const children = scene.children_refs.map((id) => document.nodes[id]); - return children.filter( - (n) => - n && - n.active !== false && - (n.type === "container" || - n.type === "template_instance" || - n.type === "component" || - n.type === "instance") - ); - }, [scene.children_refs, document.nodes]); + // Mirrors Rust collect_labeled_nodes(): + // - Trays at scene root → badge-style label + // - Containers at scene root → plain label + // - Containers that are direct children of a root Tray → plain label + // (they are "root-like" — tray children are treated as top-level frames) + const labeledNodes = useMemo(() => { + type LabelVariant = "badge" | "plain"; + type LabeledNode = { + node: grida.program.nodes.Node; + variant: LabelVariant; + /** For tray-child containers, the parent tray id. */ + parentNodeId?: string; + }; + + const labels: LabeledNode[] = []; + const { nodes, links } = document; + + const isContainer = (n: grida.program.nodes.Node) => + n.type === "container" || + n.type === "template_instance" || + n.type === "component" || + n.type === "instance"; + + for (const rootId of scene.children_refs) { + const node = nodes[rootId]; + if (!node || node.active === false) continue; + + if (node.type === "tray") { + // Tray itself gets a badge label + labels.push({ node, variant: "badge" }); + + // Tray's direct container children get plain labels (root-like) + const trayChildren = links[rootId]; + if (trayChildren) { + for (const childId of trayChildren) { + const child = nodes[childId]; + if (child && child.active !== false && isContainer(child)) { + labels.push({ + node: child, + variant: "plain", + parentNodeId: rootId, + }); + } + } + } + } else if (isContainer(node)) { + // Root container gets a plain label + labels.push({ node, variant: "plain" }); + } + } + + return labels; + }, [scene.children_refs, document.nodes, document.links]); - if (scene.constraints.children === "single") { - const rootframe = rootframes[0]; - if (!rootframe) return null; + const isSingleMode = scene.constraints.children === "single"; + + const nodeState = (node_id: string) => + selection.includes(node_id) + ? ("active" as const) + : hovered_node_id === node_id + ? ("hover" as const) + : ("idle" as const); + + if (isSingleMode) { + const first = labeledNodes[0]; + if (!first) return null; return ( - - {/* Single-mode: full styling with padding, rounded corners, and background */} - {/* Use padding-bottom on wrapper instead of margin to ensure events work in the gap */} +

- + {" (single mode)"}
@@ -698,26 +746,29 @@ function RootFramesBarOverlay() { return ( <> - {rootframes.map((node) => ( + {labeledNodes.map(({ node, variant, parentNodeId }) => ( - {/* Multi-mode: plain text, no styling */} - {/* Use padding-bottom on wrapper instead of margin to ensure events work in the gap */} -
-
- + {variant === "badge" ? ( + // Badge: tray-style title bar with rounded bar and background +
+
+ +
-
+ ) : ( + // Plain: text-only title bar for containers +
+
+ +
+
+ )} ))} @@ -728,11 +779,14 @@ function NodeTitleBar({ node, node_id, state, + parentNodeId, children, }: React.PropsWithChildren<{ node: grida.program.nodes.Node; node_id: string; state: "idle" | "hover" | "active"; + /** Pass parent id so the floating bar repositions when the parent moves. */ + parentNodeId?: string; }>) { const editor = useCurrentEditor(); @@ -775,6 +829,7 @@ function NodeTitleBar({ node_id={node_id} state={state} isComponentConsumer={is_direct_component_consumer(node.type)} + parentNodeId={parentNodeId} > {children} diff --git a/editor/grida-canvas-react/viewport/ui/floating-bar.tsx b/editor/grida-canvas-react/viewport/ui/floating-bar.tsx index efd510782f..e8735ee924 100644 --- a/editor/grida-canvas-react/viewport/ui/floating-bar.tsx +++ b/editor/grida-canvas-react/viewport/ui/floating-bar.tsx @@ -7,6 +7,8 @@ interface BarProps { node_id: string; state: "idle" | "hover" | "active"; isComponentConsumer?: boolean; + /** When set, recompute position when this parent node moves. */ + parentNodeId?: string; } export function FloatingBar({ @@ -14,9 +16,13 @@ export function FloatingBar({ children, state, isComponentConsumer, + parentNodeId, ...porps }: React.HtmlHTMLAttributes & BarProps) { - const data = useSingleSelection(porps.node_id); + const data = useSingleSelection(porps.node_id, { + enabled: true, + parentNodeId, + }); return (
Date: Sun, 29 Mar 2026 19:02:50 +0900 Subject: [PATCH 13/15] refactor(import): rename and enhance Grida import functionality - Renamed `ImportFromGridaFileJsonDialog` to `ImportFromGridaDialog` for clarity. - Updated file acceptance criteria to include `.grida1` alongside `.grida`. - Improved user feedback for file import processes and error handling. - Adjusted related components and hooks to reflect the new naming and functionality. --- .../playground/uxhost-menu.tsx | 18 +++--- .../starterkit-import/from-grida.tsx | 56 +++++++++---------- .../starterkit-import/index.ts | 2 +- .../grida-canvas-react/use-data-transfer.ts | 7 ++- packages/grida-canvas-io/index.ts | 38 ++++++++++++- 5 files changed, 79 insertions(+), 42 deletions(-) diff --git a/editor/grida-canvas-hosted/playground/uxhost-menu.tsx b/editor/grida-canvas-hosted/playground/uxhost-menu.tsx index a1b2f214a1..0c43e12f71 100644 --- a/editor/grida-canvas-hosted/playground/uxhost-menu.tsx +++ b/editor/grida-canvas-hosted/playground/uxhost-menu.tsx @@ -64,7 +64,7 @@ import Link from "next/link"; import { toast } from "sonner"; import { ImportFromFigmaDialog, - ImportFromGridaFileJsonDialog, + ImportFromGridaDialog, } from "@/grida-canvas-react-starter-kit/starterkit-import"; import { canvas_examples } from "./examples"; import { sitemap } from "@/www/data/sitemap"; @@ -91,7 +91,7 @@ export function PlaygroundMenuContent({ } = {}) { const instance = useCurrentEditor(); const importFromFigmaDialog = useDialogState("import-from-figma"); - const importFromJson = useDialogState("import-from-json", { + const importFromGrida = useDialogState("import-from-grida", { refreshkey: true, }); const settingsDialog = useDialogState("settings"); @@ -156,9 +156,9 @@ export function PlaygroundMenuContent({ return ( <> - { if ( file.assets?.images && @@ -254,7 +254,7 @@ export function PlaygroundMenuContent({ @@ -308,12 +308,12 @@ export function PlaygroundMenuContent({ function FileMenuContent({ onExport, - onImportJson, + onImportGrida, onImportImage, onImportFigma, }: { onExport: () => void; - onImportJson: () => void; + onImportGrida: () => void; onImportImage: () => void; onImportFigma: () => void; }) { @@ -321,7 +321,7 @@ function FileMenuContent({ File - + Open .grida diff --git a/editor/grida-canvas-react-starter-kit/starterkit-import/from-grida.tsx b/editor/grida-canvas-react-starter-kit/starterkit-import/from-grida.tsx index e4a2bd9edc..ff016372dd 100644 --- a/editor/grida-canvas-react-starter-kit/starterkit-import/from-grida.tsx +++ b/editor/grida-canvas-react-starter-kit/starterkit-import/from-grida.tsx @@ -14,7 +14,15 @@ import { toast } from "sonner"; import { io } from "@grida/io"; import { FileDropzone } from "./file-dropzone"; -export function ImportFromGridaFileJsonDialog({ +const ACCEPTED_EXTENSIONS = [".grida", ".grida1"] as const; +const ACCEPT = ACCEPTED_EXTENSIONS.join(","); + +function isAcceptedFile(file: File): boolean { + const name = file.name.toLowerCase(); + return ACCEPTED_EXTENSIONS.some((ext) => name.endsWith(ext)); +} + +export function ImportFromGridaDialog({ onImport, ...props }: React.ComponentProps & { @@ -22,27 +30,20 @@ export function ImportFromGridaFileJsonDialog({ }) { const [selectedFile, setSelectedFile] = useState(null); - const validateFile = (file: File) => { - return ( - file.name.toLowerCase().endsWith(".grida") || - file.name.toLowerCase().endsWith(".json") - ); - }; - const handleFileImport = async () => { - if (selectedFile) { - try { - const doc = await io.load(selectedFile); - onImport?.(doc); - toast.success("File successfully imported!"); - props.onOpenChange?.(false); // Close the dialog - setSelectedFile(null); - } catch (error) { - toast.error("Failed to parse the file. Please check the format."); - console.error(error); - } - } else { + if (!selectedFile) { toast.error("No file selected."); + return; + } + try { + const doc = await io.load(selectedFile); + onImport?.(doc); + toast.success("File successfully imported!"); + props.onOpenChange?.(false); + setSelectedFile(null); + } catch (error) { + toast.error("Failed to parse the file. Please check the format."); + console.error(error); } }; @@ -50,22 +51,21 @@ export function ImportFromGridaFileJsonDialog({ - Import from .grida File + Open .grida - Import a document from a .grida or .json file. -
- Supported file formats: .grida, .json + Import a document from a .grida or{" "} + .grida1 file.
- + {selectedFile && (

diff --git a/editor/grida-canvas-react-starter-kit/starterkit-import/index.ts b/editor/grida-canvas-react-starter-kit/starterkit-import/index.ts index e16c421f1d..ebeec8dffa 100644 --- a/editor/grida-canvas-react-starter-kit/starterkit-import/index.ts +++ b/editor/grida-canvas-react-starter-kit/starterkit-import/index.ts @@ -1,2 +1,2 @@ -export { ImportFromGridaFileJsonDialog } from "./from-grida"; +export { ImportFromGridaDialog } from "./from-grida"; export { ImportFromFigmaDialog, type FetchNodeResult } from "./from-figma"; diff --git a/editor/grida-canvas-react/use-data-transfer.ts b/editor/grida-canvas-react/use-data-transfer.ts index 652e14d3ec..791a18f6e4 100644 --- a/editor/grida-canvas-react/use-data-transfer.ts +++ b/editor/grida-canvas-react/use-data-transfer.ts @@ -606,8 +606,11 @@ export function useDataTransferEventTarget() { continue; } - // Check for .grida files and show helpful message - if (file.name.toLowerCase().endsWith(".grida")) { + // Check for .grida / .grida1 files and show helpful message + if ( + file.name.toLowerCase().endsWith(".grida") || + file.name.toLowerCase().endsWith(".grida1") + ) { toast.info("Use [File] > [Open .grida] to import .grida files"); continue; } diff --git a/packages/grida-canvas-io/index.ts b/packages/grida-canvas-io/index.ts index 8435e077df..9b0b725f7f 100644 --- a/packages/grida-canvas-io/index.ts +++ b/packages/grida-canvas-io/index.ts @@ -658,7 +658,7 @@ export namespace io { } export namespace fileformat { - export type Kind = "grida" | "zip" | "unknown"; + export type Kind = "grida" | "zip" | "grida1" | "unknown"; export type Detected = | { kind: "grida"; bytes: Uint8Array } @@ -672,6 +672,7 @@ export namespace io { bitmaps: Record; }; } + | { kind: "grida1"; model: SnapshotDocumentModel } | { kind: "unknown"; bytes?: Uint8Array }; /** @@ -754,6 +755,18 @@ export namespace io { return { kind: "grida", bytes }; } + // `.grida1` — JSON snapshot (the sidecar format found inside `.grida` archives) + if (file.name.toLowerCase().endsWith(".grida1")) { + try { + const model = snapshot.parse(await file.text()); + if (model?.version && model?.document) { + return { kind: "grida1", model }; + } + } catch { + // malformed JSON; fall through + } + } + return { kind: "unknown" }; } } @@ -814,7 +827,28 @@ export namespace io { } satisfies LoadedDocument; } - throw new Error(`Unsupported file type: ${file.type}`); + // JSON snapshot (`document.grida1` sidecar format) + if (detected.kind === "grida1") { + const { model } = detected; + if (!grida.program.document.isSchemaCompatible(model.version)) { + throw new Error( + `schema incompatible: file version "${model.version}" is not compatible with current "${grida.program.document.SCHEMA_VERSION}"` + ); + } + const doc = model.document; + return { + version: grida.program.document.SCHEMA_VERSION, + document: { + ...doc, + images: doc.images ?? {}, + bitmaps: doc.bitmaps ?? {}, + }, + } satisfies LoadedDocument; + } + + throw new Error( + `Unsupported file: "${file.name}" (type=${file.type || "unknown"}). Expected .grida or .grida1.` + ); } /** From 14b2157ce674f171a9bc04ce7f9c293001cb8019 Mon Sep 17 00:00:00 2001 From: Universe Date: Sun, 29 Mar 2026 19:08:09 +0900 Subject: [PATCH 14/15] feat(query): add backend-agnostic paint grouping API with WASM acceleration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce `IDocumentPropertiesQueryProvider` with `queryPaintGroups(ids, target, options)` that abstracts paint aggregation across DOM and canvas (WASM) backends. Rust side: `query::paint` module with hash-based O(n) grouping, recursive/flat traversal, early-exit limit, and `PaintSource`/`ChildrenIter` traits for testability. WASM side: `query_paint_groups` C-ABI export accepting explicit node IDs with recursive flag, plus JS paint format conversion (externally-tagged enum → cg.Paint). React side: `useMixedPaints` now delegates to the provider and suppresses re-queries during active gestures (translate/scale/rotate) since paints are stable during geometric mutations. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../lib/modules/canvas-bindings.d.ts | 8 + .../grida-canvas-wasm/lib/modules/canvas.ts | 104 ++++++ .../grida-canvas-wasm/src/wasm_application.rs | 82 +++++ crates/grida-canvas/src/node/schema.rs | 25 ++ .../src/{query.rs => query/hierarchy.rs} | 0 crates/grida-canvas/src/query/mod.rs | 4 + crates/grida-canvas/src/query/paint.rs | 341 ++++++++++++++++++ .../use-mixed-properties.ts | 113 +++--- editor/grida-canvas/backends/dom-content.ts | 69 ++++ editor/grida-canvas/backends/wasm.ts | 21 ++ editor/grida-canvas/editor.i.ts | 39 ++ editor/grida-canvas/editor.ts | 14 +- 12 files changed, 751 insertions(+), 69 deletions(-) rename crates/grida-canvas/src/{query.rs => query/hierarchy.rs} (100%) create mode 100644 crates/grida-canvas/src/query/mod.rs create mode 100644 crates/grida-canvas/src/query/paint.rs diff --git a/crates/grida-canvas-wasm/lib/modules/canvas-bindings.d.ts b/crates/grida-canvas-wasm/lib/modules/canvas-bindings.d.ts index 1023d08417..f5e2fc9737 100644 --- a/crates/grida-canvas-wasm/lib/modules/canvas-bindings.d.ts +++ b/crates/grida-canvas-wasm/lib/modules/canvas-bindings.d.ts @@ -199,6 +199,14 @@ declare namespace canvas { json_ptr: number, json_len: number ): void; + _query_paint_groups( + state: GridaCanvasApplicationPtr, + ids_json_ptr: number, + ids_json_len: number, + target: number, + recursive: boolean, + limit: number + ): Ptr; _set_surface_overlay_config( state: GridaCanvasApplicationPtr, json_ptr: number, diff --git a/crates/grida-canvas-wasm/lib/modules/canvas.ts b/crates/grida-canvas-wasm/lib/modules/canvas.ts index 2a66266076..d8fac81c1f 100644 --- a/crates/grida-canvas-wasm/lib/modules/canvas.ts +++ b/crates/grida-canvas-wasm/lib/modules/canvas.ts @@ -668,6 +668,54 @@ export class Scene { this._free_string(ptr, len); } + /** + * Collect and group active paints across a set of nodes. + * + * Returns groups of identical paints with the node IDs that share each paint. + * Uses hash-based grouping (O(n)) instead of pairwise deep equality. + * + * Paint objects are converted from Rust's externally-tagged enum format to + * the JS `cg.Paint` format (internally tagged with `type` field, RGBA32F colors). + * + * @param ids - Node IDs to query. + * @param target - "fill" or "stroke" + * @param options.recursive - Include descendant subtrees (default: true). + * @param options.limit - Max distinct paint groups to return (0 = unlimited). + */ + queryPaintGroups( + ids: string[], + target: "fill" | "stroke" = "fill", + options?: { recursive?: boolean; limit?: number } + ): Array<{ paint: any; node_ids: string[] }> { + this._assertAlive(); + const recursive = options?.recursive ?? true; + const limit = options?.limit ?? 0; + const targetCode = target === "stroke" ? 1 : 0; + + const idsJson = JSON.stringify(ids); + const [idsPtr, idsLen] = this._alloc_string(idsJson); + const ptr = this.module._query_paint_groups( + this.appptr, + idsPtr, + idsLen - 1, + targetCode, + recursive, + limit + ); + this._free_string(idsPtr, idsLen); + + if (ptr === 0) return []; + const str = ffi.readLenPrefixedString(this.module, ptr); + const raw = JSON.parse(str) as Array<{ + paint: Record; + node_ids: string[]; + }>; + return raw.map((g) => ({ + paint: convertRustPaintToJS(g.paint), + node_ids: g.node_ids, + })); + } + /** * Configure surface overlay rendering (size meter, frame titles, etc.). */ @@ -1095,3 +1143,59 @@ export type TextEditCommand = | { type: "SelectAll" } | { type: "Undo" } | { type: "Redo" }; + +// --------------------------------------------------------------------------- +// Paint format conversion: Rust externally-tagged enum → JS cg.Paint +// --------------------------------------------------------------------------- + +/** Map from Rust enum variant name to JS `type` string. */ +const PAINT_TYPE_MAP: Record = { + Solid: "solid", + LinearGradient: "linear_gradient", + RadialGradient: "radial_gradient", + SweepGradient: "sweep_gradient", + DiamondGradient: "diamond_gradient", + Image: "image", +}; + +/** + * Convert a CGColor from Rust u8 array `[r, g, b, a]` to JS RGBA32F object. + */ +function convertColor(c: number[]): { r: number; g: number; b: number; a: number } { + return { r: c[0] / 255, g: c[1] / 255, b: c[2] / 255, a: c[3] / 255 }; +} + +/** + * Convert a GradientStop from Rust format to JS format. + */ +function convertStop(stop: { offset: number; color: number[] }): { + offset: number; + color: { r: number; g: number; b: number; a: number }; +} { + return { offset: stop.offset, color: convertColor(stop.color) }; +} + +/** + * Convert a Paint from Rust's externally-tagged serde format to the JS + * cg.Paint format (internally tagged with `type`, RGBA32F colors). + * + * Rust: `{ "Solid": { active: true, color: [255, 0, 0, 255], blend_mode: "normal" } }` + * JS: `{ type: "solid", active: true, color: { r: 1, g: 0, b: 0, a: 1 }, blend_mode: "normal" }` + */ +function convertRustPaintToJS(paint: Record): any { + const variant = Object.keys(paint)[0]; + const data = paint[variant]; + const type = PAINT_TYPE_MAP[variant] ?? variant.toLowerCase(); + + const result: any = { type, ...data }; + + // Convert colors from u8 arrays to RGBA32F objects + if (data.color && Array.isArray(data.color)) { + result.color = convertColor(data.color); + } + if (data.stops && Array.isArray(data.stops)) { + result.stops = data.stops.map(convertStop); + } + + return result; +} diff --git a/crates/grida-canvas-wasm/src/wasm_application.rs b/crates/grida-canvas-wasm/src/wasm_application.rs index 601c8829d5..955efc4152 100644 --- a/crates/grida-canvas-wasm/src/wasm_application.rs +++ b/crates/grida-canvas-wasm/src/wasm_application.rs @@ -458,6 +458,88 @@ pub unsafe extern "C" fn surface_set_selection( } } +#[no_mangle] +/// js::_query_paint_groups +/// +/// Collect and group active paints across a set of nodes. +/// Returns a length-prefixed JSON array of `{ paint, node_ids }` groups. +/// +/// - `ids_json_ptr` / `ids_json_len`: JSON array of user-facing node ID strings +/// - `target`: 0 = fill, 1 = stroke +/// - `recursive`: whether to include descendant subtrees +/// - `limit`: max number of distinct paint groups to return (0 = unlimited) +pub unsafe extern "C" fn query_paint_groups( + app: *const UnknownTargetApplication, + ids_json_ptr: *const u8, + ids_json_len: u32, + target: u32, + recursive: bool, + limit: u32, +) -> *const u8 { + let Some(app) = app.as_ref() else { + return std::ptr::null(); + }; + let Some(scene) = app.renderer().scene.as_ref() else { + return alloc_len_prefixed(b"[]"); + }; + + // Parse user-facing IDs from JSON + let slice = std::slice::from_raw_parts(ids_json_ptr, ids_json_len as usize); + let Ok(user_ids) = serde_json::from_slice::>(slice) else { + return alloc_len_prefixed(b"[]"); + }; + + let internal_ids: Vec = user_ids + .iter() + .filter_map(|uid| app.user_id_to_internal(uid)) + .collect(); + + if internal_ids.is_empty() { + return alloc_len_prefixed(b"[]"); + } + + let paint_target = match target { + 1 => cg::query::paint::PaintTarget::Stroke, + _ => cg::query::paint::PaintTarget::Fill, + }; + let limit = if limit == 0 { + None + } else { + Some(limit as usize) + }; + + let groups = cg::query::paint::query_paint_groups( + &scene.graph, + &scene.graph, + &internal_ids, + paint_target, + recursive, + limit, + ); + + // Serialize with user-facing string IDs + #[derive(Serialize)] + struct PaintGroupOut { + paint: cg::cg::types::Paint, + node_ids: Vec, + } + + let out: Vec = groups + .into_iter() + .map(|g| PaintGroupOut { + paint: g.paint, + node_ids: g + .node_ids + .into_iter() + .filter_map(|id| app.internal_id_to_user(id)) + .collect(), + }) + .collect(); + + let json = serde_json::to_vec(&out).unwrap_or_else(|_| b"[]".to_vec()); + alloc_len_prefixed(&json) +} + #[no_mangle] /// js::_set_surface_overlay_config /// diff --git a/crates/grida-canvas/src/node/schema.rs b/crates/grida-canvas/src/node/schema.rs index a5666619ad..3b7ca6cec2 100644 --- a/crates/grida-canvas/src/node/schema.rs +++ b/crates/grida-canvas/src/node/schema.rs @@ -1246,6 +1246,31 @@ impl Node { } } + /// Returns the node's fill paints, if it has any. + /// + /// `Error`, `Group`, and `Line` have no fills and return `None`. + /// `Image` wraps its single `ImagePaint` into a one-element `Paints`. + pub fn fills(&self) -> Option<&Paints> { + match self { + Node::InitialContainer(_) => None, + Node::Container(n) => Some(&n.fills), + Node::Tray(n) => Some(&n.fills), + Node::Rectangle(n) => Some(&n.fills), + Node::Ellipse(n) => Some(&n.fills), + Node::Polygon(n) => Some(&n.fills), + Node::RegularPolygon(n) => Some(&n.fills), + Node::RegularStarPolygon(n) => Some(&n.fills), + Node::TextSpan(n) => Some(&n.fills), + Node::AttributedText(n) => Some(&n.fills), + Node::Path(n) => Some(&n.fills), + Node::Vector(n) => Some(&n.fills), + Node::BooleanOperation(n) => Some(&n.fills), + // Image has a single ImagePaint, not a Paints stack + Node::Image(_) => None, + Node::Error(_) | Node::Group(_) | Node::Line(_) => None, + } + } + /// Returns the node's blend mode. /// `InitialContainer` and `Error` default to `PassThrough`. pub fn blend_mode(&self) -> LayerBlendMode { diff --git a/crates/grida-canvas/src/query.rs b/crates/grida-canvas/src/query/hierarchy.rs similarity index 100% rename from crates/grida-canvas/src/query.rs rename to crates/grida-canvas/src/query/hierarchy.rs diff --git a/crates/grida-canvas/src/query/mod.rs b/crates/grida-canvas/src/query/mod.rs new file mode 100644 index 0000000000..f145c05259 --- /dev/null +++ b/crates/grida-canvas/src/query/mod.rs @@ -0,0 +1,4 @@ +mod hierarchy; +pub mod paint; + +pub use hierarchy::*; diff --git a/crates/grida-canvas/src/query/paint.rs b/crates/grida-canvas/src/query/paint.rs new file mode 100644 index 0000000000..d5398077c8 --- /dev/null +++ b/crates/grida-canvas/src/query/paint.rs @@ -0,0 +1,341 @@ +//! Paint aggregation queries for sets of nodes. +//! +//! Collects and groups paints across a set of nodes (optionally including +//! their descendants), using hash-based grouping (O(n)) instead of pairwise +//! deep equality (O(n²)). +//! +//! The core function [`query_paint_groups`] is agnostic to concrete +//! scene graph implementations — it operates through the [`PaintSource`] +//! and [`ChildrenIter`] traits. + +use crate::cg::types::Paint; +use crate::node::schema::NodeId; +use std::collections::hash_map::DefaultHasher; +use std::collections::{HashMap, HashSet}; +use std::hash::Hasher; + +/// Where to look for paints on each node. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PaintTarget { + Fill, + Stroke, +} + +/// Trait for retrieving active fill/stroke paints from a node by ID. +pub trait PaintSource { + /// Return the active paints for `id` in the given `target` slot. + /// Returns an empty slice if the node has no paints for this target. + fn active_paints(&self, id: &NodeId, target: PaintTarget) -> Vec; +} + +/// Trait for iterating the direct children of a node. +pub trait ChildrenIter { + /// Return the direct children of `id`, or an empty slice if it has none. + fn children(&self, id: &NodeId) -> Option<&Vec>; +} + +/// A group of nodes sharing the same paint. +#[derive(Debug, Clone)] +pub struct PaintGroup { + /// The shared paint value. + pub paint: Paint, + /// Node IDs that have this paint (deduplicated). + pub node_ids: Vec, +} + +/// Collect and group active paints across a set of nodes. +/// +/// # Arguments +/// +/// - `ids` — Root node IDs to query. +/// - `target` — Which paint slot to read (`Fill` or `Stroke`). +/// - `recursive` — When `true`, each id is expanded into its full subtree +/// (DFS, iterative) before collecting paints. When `false`, only the +/// listed ids are queried. +/// - `limit` — Stop creating new groups once this many distinct paints have +/// been found (`None` = unlimited). Nodes are still added to existing +/// groups after the limit is reached. +/// +/// # Algorithm +/// +/// 1. (If recursive) expand each id into its full subtree via DFS. +/// 2. For each node, collect active paints for the given `target`. +/// 3. Group paints by hash (using `Paint::hash_for_cache`). +/// On hash collision, fall back to `PartialEq` to confirm identity. +/// 4. Stop creating new groups once `limit` is reached. +/// +/// Complexity: O(N) where N = total nodes visited, +/// assuming low hash collision rate. +pub fn query_paint_groups( + source: &impl PaintSource, + tree: &impl ChildrenIter, + ids: &[NodeId], + target: PaintTarget, + recursive: bool, + limit: Option, +) -> Vec { + let mut visited = HashSet::new(); + let mut hash_to_indices: HashMap> = HashMap::new(); + let mut groups: Vec = Vec::new(); + + let limit_reached = |groups: &Vec, limit: Option| -> bool { + limit.map_or(false, |l| groups.len() >= l) + }; + + if recursive { + // DFS: expand each id into its full subtree + let mut stack: Vec = Vec::new(); + for &id in ids.iter().rev() { + stack.push(id); + } + + while let Some(id) = stack.pop() { + if !visited.insert(id) { + continue; + } + + collect_paints( + source, + id, + target, + &mut groups, + &mut hash_to_indices, + limit, + &limit_reached, + ); + + if let Some(children) = tree.children(&id) { + for child_id in children.iter().rev() { + if !visited.contains(child_id) { + stack.push(*child_id); + } + } + } + } + } else { + // Flat: only query the listed ids + for &id in ids { + if !visited.insert(id) { + continue; + } + collect_paints( + source, + id, + target, + &mut groups, + &mut hash_to_indices, + limit, + &limit_reached, + ); + } + } + + groups +} + +/// Collect paints from a single node and merge into the groups table. +fn collect_paints( + source: &impl PaintSource, + id: NodeId, + target: PaintTarget, + groups: &mut Vec, + hash_to_indices: &mut HashMap>, + limit: Option, + limit_reached: &dyn Fn(&Vec, Option) -> bool, +) { + let paints = source.active_paints(&id, target); + for paint in paints { + let hash = { + let mut h = DefaultHasher::new(); + paint.hash_for_cache(&mut h); + h.finish() + }; + + let mut found = false; + if let Some(indices) = hash_to_indices.get(&hash) { + for &idx in indices { + if groups[idx].paint == paint { + if !groups[idx].node_ids.contains(&id) { + groups[idx].node_ids.push(id); + } + found = true; + break; + } + } + } + + if !found { + if limit_reached(groups, limit) { + continue; + } + let idx = groups.len(); + groups.push(PaintGroup { + paint, + node_ids: vec![id], + }); + hash_to_indices.entry(hash).or_default().push(idx); + } + } +} + +/// Backwards-compatible alias — queries with `recursive = true`. +pub fn query_selection_paints( + source: &impl PaintSource, + tree: &impl ChildrenIter, + selection: &[NodeId], + target: PaintTarget, + limit: Option, +) -> Vec { + query_paint_groups(source, tree, selection, target, true, limit) +} + +// --------------------------------------------------------------------------- +// Implementations for SceneGraph +// --------------------------------------------------------------------------- + +impl ChildrenIter for crate::node::scene_graph::SceneGraph { + fn children(&self, id: &NodeId) -> Option<&Vec> { + self.get_children(id) + } +} + +impl PaintSource for crate::node::scene_graph::SceneGraph { + fn active_paints(&self, id: &NodeId, target: PaintTarget) -> Vec { + let Ok(node) = self.get_node(id) else { + return Vec::new(); + }; + + match target { + PaintTarget::Fill => match node.fills() { + Some(paints) => paints.iter().filter(|p| p.active()).cloned().collect(), + None => Vec::new(), + }, + PaintTarget::Stroke => { + // TODO: add Node::strokes() accessor and use it here + Vec::new() + } + } + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + use crate::cg::color::CGColor; + use crate::cg::types::{BlendMode, SolidPaint}; + + /// Minimal test scene for paint queries. + struct TestScene { + children: HashMap>, + paints: HashMap>, + } + + impl ChildrenIter for TestScene { + fn children(&self, id: &NodeId) -> Option<&Vec> { + self.children.get(id) + } + } + + impl PaintSource for TestScene { + fn active_paints(&self, id: &NodeId, _target: PaintTarget) -> Vec { + self.paints.get(id).cloned().unwrap_or_default() + } + } + + fn solid(r: u8, g: u8, b: u8) -> Paint { + Paint::Solid(SolidPaint { + active: true, + color: CGColor::from_rgba(r, g, b, 255), + blend_mode: BlendMode::Normal, + }) + } + + #[test] + fn groups_identical_paints() { + let red = solid(255, 0, 0); + let blue = solid(0, 0, 255); + + let scene = TestScene { + children: HashMap::new(), + paints: HashMap::from([ + (1, vec![red.clone()]), + (2, vec![red.clone()]), + (3, vec![blue.clone()]), + ]), + }; + + let groups = query_paint_groups(&scene, &scene, &[1, 2, 3], PaintTarget::Fill, true, None); + assert_eq!(groups.len(), 2); + + let red_group = groups.iter().find(|g| g.paint == red).unwrap(); + assert_eq!(red_group.node_ids.len(), 2); + + let blue_group = groups.iter().find(|g| g.paint == blue).unwrap(); + assert_eq!(blue_group.node_ids.len(), 1); + } + + #[test] + fn traverses_children_recursive() { + let red = solid(255, 0, 0); + + let scene = TestScene { + children: HashMap::from([(1, vec![2, 3])]), + paints: HashMap::from([(2, vec![red.clone()]), (3, vec![red.clone()])]), + }; + + let groups = query_paint_groups(&scene, &scene, &[1], PaintTarget::Fill, true, None); + assert_eq!(groups.len(), 1); + assert_eq!(groups[0].node_ids.len(), 2); + } + + #[test] + fn non_recursive_skips_children() { + let red = solid(255, 0, 0); + + let scene = TestScene { + children: HashMap::from([(1, vec![2, 3])]), + paints: HashMap::from([ + (1, vec![red.clone()]), + (2, vec![red.clone()]), + (3, vec![red.clone()]), + ]), + }; + + // Non-recursive: only queries node 1, not its children + let groups = query_paint_groups(&scene, &scene, &[1], PaintTarget::Fill, false, None); + assert_eq!(groups.len(), 1); + assert_eq!(groups[0].node_ids.len(), 1); + assert_eq!(groups[0].node_ids[0], 1); + } + + #[test] + fn respects_limit() { + let red = solid(255, 0, 0); + let blue = solid(0, 0, 255); + let green = solid(0, 255, 0); + + let scene = TestScene { + children: HashMap::new(), + paints: HashMap::from([(1, vec![red]), (2, vec![blue]), (3, vec![green])]), + }; + + let groups = + query_paint_groups(&scene, &scene, &[1, 2, 3], PaintTarget::Fill, true, Some(2)); + assert_eq!(groups.len(), 2); + } + + #[test] + fn empty_ids() { + let scene = TestScene { + children: HashMap::new(), + paints: HashMap::new(), + }; + + let groups = query_paint_groups(&scene, &scene, &[], PaintTarget::Fill, true, None); + assert!(groups.is_empty()); + } +} diff --git a/editor/grida-canvas-react/use-mixed-properties.ts b/editor/grida-canvas-react/use-mixed-properties.ts index b52f50723c..8de6704560 100644 --- a/editor/grida-canvas-react/use-mixed-properties.ts +++ b/editor/grida-canvas-react/use-mixed-properties.ts @@ -1,7 +1,5 @@ import { useCallback, useMemo } from "react"; -import { editor } from "@/grida-canvas"; import { useCurrentEditor, useEditorState } from "./use-editor"; -import { dq } from "@/grida-canvas/query"; import grida from "@grida/schema"; import mixed, { type KeyIgnoreFn, @@ -9,7 +7,6 @@ import mixed, { type PropertyCompareFn, } from "@grida/mixed-properties"; import type cg from "@grida/cg"; -import equal from "fast-deep-equal"; type WithId> = T & { id: string }; @@ -79,80 +76,60 @@ export function useMixedProperties>( } /** - * @deprecated expensive + * Query fill paints across the current selection and all descendants. * - * @todo This function is expensive because it resolves all paint values at once. - * The UI only initially shows a partial set (n values). Optimize this by: - * - First limiting by n and early exiting - * - Adding a method to load all values on demand + * Delegates to `editor.propertiesQuery` which dispatches to the active backend: + * - **canvas (WASM)**: hash-based O(n) grouping in Rust + * - **DOM**: JS-side traversal with deep-equality grouping + * + * Re-queries are suppressed during active gestures (translate, scale, rotate, + * etc.) since those only affect geometry, not paints. */ export function useMixedPaints() { const instance = useCurrentEditor(); - /** - * Subscribe ONLY to: - * - `selection` - * - derived `ids` (selection + recursive children) - * - active fill paints for those ids - * - * This avoids re-rendering on unrelated document mutations. - */ + + // Subscribe to selection + gesture + document. + // During an active gesture, `document` changes on every pointer move (transforms), + // but paints are stable — so we freeze the document identity while gesturing. + // + // TODO: Replace gesture-based gating with fine-grained change tracking. + // Editor should emit typed change events (e.g. "geometry" | "paint" | "style" | …) + // so subscribers can react only to relevant mutations instead of inferring + // staleness from gesture state. This would also cover non-gesture paint + // changes (e.g. undo/redo, programmatic mutations) without the idle check. const slice = useEditorState( instance, - (state) => { - const selection = state.selection; - - // selection & its recursive children (stable insertion order) - const idsSet = new Set(); - for (const id of selection) { - idsSet.add(id); - for (const childId of dq.getChildren(state.document_ctx, id, true)) { - idsSet.add(childId); - } - } - const ids = Array.from(idsSet); - - const paintEntries: Array<{ nodeId: string; paint: cg.Paint }> = []; - for (const nodeId of ids) { - const node = state.document.nodes[ - nodeId - ] as grida.program.nodes.UnknownNode; - if (!node) continue; - - const { paints } = editor.resolvePaints(node, "fill", 0); - const activePaints = paints.filter((p) => p?.active !== false); - for (const paint of activePaints) { - paintEntries.push({ nodeId, paint }); - } - } - - return { selection, ids, paintEntries }; - }, - equal + (state) => ({ + selection: state.selection, + gestureIdle: state.gesture.type === "idle", + // Only track document identity when idle — during gestures, paints + // can't change so we keep the stale reference to prevent re-queries. + document: state.gesture.type === "idle" ? state.document : null, + }), + (a, b) => + a.gestureIdle === b.gestureIdle && + a.document === b.document && + a.selection.length === b.selection.length && + a.selection.every((id, i) => id === b.selection[i]) ); - // TODO: @grida/mixed-properties should support array properties (e.g., fill_paints[] per node) - // Once array handling is added to mixed(), replace this custom logic with normalized nodes + mixed() - const paints = useMemo(() => { - // Group by paint value (using deep equality) - const paintGroups: Array<{ value: cg.Paint; ids: string[] }> = []; + const { selection } = slice; - for (const { nodeId, paint } of slice.paintEntries) { - // Find existing group with same paint value - const existingGroup = paintGroups.find((group) => - equal(group.value, paint) - ); + const paints = useMemo( + () => instance.propertiesQuery.queryPaintGroups(selection, "fill", { recursive: true }), + [slice, instance] + ); - if (existingGroup) { - if (!existingGroup.ids.includes(nodeId)) { - existingGroup.ids.push(nodeId); - } - } else { - paintGroups.push({ value: paint, ids: [nodeId] }); + // Collect all node IDs across groups for display-gating + const ids = useMemo(() => { + const set = new Set(); + for (const group of paints) { + for (const id of group.ids) { + set.add(id); } } - - return paintGroups; - }, [slice.paintEntries]); + return Array.from(set); + }, [paints]); const setPaint = useCallback( ( @@ -168,10 +145,10 @@ export function useMixedPaints() { return useMemo(() => { return { - selection: slice.selection, - ids: slice.ids, + selection, + ids, paints, setPaint, }; - }, [slice.selection, slice.ids, paints, setPaint]); + }, [selection, ids, paints, setPaint]); } diff --git a/editor/grida-canvas/backends/dom-content.ts b/editor/grida-canvas/backends/dom-content.ts index 42b108cefc..43fa312091 100644 --- a/editor/grida-canvas/backends/dom-content.ts +++ b/editor/grida-canvas/backends/dom-content.ts @@ -1,8 +1,12 @@ import grida from "@grida/schema"; import cmath from "@grida/cmath"; +import type cg from "@grida/cg"; import type { Editor } from "../editor"; import { editor } from ".."; import { domapi } from "./dom"; +import { dq } from "../query"; +import { editor as editorUtils } from "../editor.i"; +import equal from "fast-deep-equal"; class DOMContentApi { constructor(readonly containerId: string) {} @@ -124,3 +128,68 @@ export class DOMGeometryQueryInterfaceProvider return rect; } } + +export class DOMPropertiesQueryProvider + implements editor.api.IDocumentPropertiesQueryProvider +{ + constructor(readonly editor: Editor) {} + + queryPaintGroups( + ids: string[], + target: "fill" | "stroke", + options?: { recursive?: boolean; limit?: number } + ): editor.api.PaintGroup[] { + const recursive = options?.recursive ?? true; + const limit = options?.limit ?? 0; + const state = this.editor.getSnapshot(); + + // Expand into subtrees if recursive + const nodeIds = new Set(); + for (const id of ids) { + nodeIds.add(id); + if (recursive) { + for (const childId of dq.getChildren(state.document_ctx, id, true)) { + nodeIds.add(childId); + } + } + } + + // Collect active paints per node + const paintEntries: Array<{ nodeId: string; paint: cg.Paint }> = []; + for (const nodeId of nodeIds) { + const node = state.document.nodes[ + nodeId + ] as grida.program.nodes.UnknownNode; + if (!node) continue; + + const { paints } = editorUtils.resolvePaints(node, target, 0); + const activePaints = paints.filter((p) => p?.active !== false); + for (const paint of activePaints) { + paintEntries.push({ nodeId, paint }); + } + } + + // Group by paint value (deep equality) + const groups: editor.api.PaintGroup[] = []; + for (const { nodeId, paint } of paintEntries) { + if (limit > 0 && groups.length >= limit) { + const existing = groups.find((g) => equal(g.value, paint)); + if (existing && !existing.ids.includes(nodeId)) { + existing.ids.push(nodeId); + } + continue; + } + + const existing = groups.find((g) => equal(g.value, paint)); + if (existing) { + if (!existing.ids.includes(nodeId)) { + existing.ids.push(nodeId); + } + } else { + groups.push({ value: paint, ids: [nodeId] }); + } + } + + return groups; + } +} diff --git a/editor/grida-canvas/backends/wasm.ts b/editor/grida-canvas/backends/wasm.ts index 5c1b380cbd..955e3ae551 100644 --- a/editor/grida-canvas/backends/wasm.ts +++ b/editor/grida-canvas/backends/wasm.ts @@ -46,6 +46,27 @@ export class CanvasWasmGeometryQueryInterfaceProvider } } +export class CanvasWasmPropertiesQueryProvider + implements editor.api.IDocumentPropertiesQueryProvider +{ + constructor( + readonly editor: Editor, + readonly surface: Scene + ) {} + + queryPaintGroups( + ids: string[], + target: "fill" | "stroke", + options?: { recursive?: boolean; limit?: number } + ): editor.api.PaintGroup[] { + const groups = this.surface.queryPaintGroups(ids, target, options); + return groups.map((g) => ({ + value: g.paint, + ids: g.node_ids, + })); + } +} + export class CanvasWasmDefaultExportInterfaceProvider implements editor.api.IDocumentExporterInterfaceProvider { diff --git a/editor/grida-canvas/editor.i.ts b/editor/grida-canvas/editor.i.ts index b25606ba58..f32306b288 100644 --- a/editor/grida-canvas/editor.i.ts +++ b/editor/grida-canvas/editor.i.ts @@ -2593,6 +2593,45 @@ export namespace editor.api { ): cmath.Rectangle | null; } + /** + * Aggregated paint query result: a paint value shared by one or more nodes. + */ + export interface PaintGroup { + /** The paint value (matches `cg.Paint` shape). */ + value: cg.Paint; + /** Node IDs that share this exact paint. */ + ids: string[]; + } + + /** + * Backend-agnostic provider for querying aggregated property data + * across sets of nodes. + * + * - **canvas (WASM)**: delegates traversal, collection, and hash-based + * grouping to Rust — O(n) with optional early-exit `limit`. + * - **DOM**: performs the same logic in JS using the editor state tree. + */ + export interface IDocumentPropertiesQueryProvider { + /** + * Collect and group active paints across the given nodes. + * + * @param ids - Root node IDs to query. + * @param target - Which paint slot to query (`"fill"` or `"stroke"`). + * @param options - Query options. + * @param options.recursive - When `true` (default), include all + * descendants of each id. When `false`, only query the listed ids. + * @param options.limit - Maximum number of distinct paint groups to + * return. `0` means unlimited. + * @returns An array of paint groups, each containing the shared paint + * value and the node IDs that use it. + */ + queryPaintGroups( + ids: string[], + target: "fill" | "stroke", + options?: { recursive?: boolean; limit?: number } + ): PaintGroup[]; + } + export interface IDocumentImageExportInterfaceProvider { /** * exports the node as an image diff --git a/editor/grida-canvas/editor.ts b/editor/grida-canvas/editor.ts index 4c7fc5e2fd..033c6cd23c 100644 --- a/editor/grida-canvas/editor.ts +++ b/editor/grida-canvas/editor.ts @@ -19,7 +19,9 @@ import { CanvasWasmDefaultExportInterfaceProvider, CanvasWasmSVGInterfaceProvider, CanvasWasmMarkdownInterfaceProvider, + CanvasWasmPropertiesQueryProvider, } from "./backends"; +import { DOMPropertiesQueryProvider } from "./backends/dom-content"; import { domapi } from "./backends/dom"; import { dq } from "@/grida-canvas/query"; import { @@ -2783,6 +2785,11 @@ export class Editor return this._m_font_parser; } + _m_properties_query: editor.api.IDocumentPropertiesQueryProvider; + public get propertiesQuery() { + return this._m_properties_query; + } + private readonly _fontManager: DocumentFontManager; readonly onMount?: (surface: Scene) => void; @@ -2871,7 +2878,7 @@ export class Editor this._m_geometry = typeof geometry === "function" ? geometry(this) : geometry; - // + this._m_properties_query = new DOMPropertiesQueryProvider(this); if (interfaces?.exporter) { this._m_exporter = resolveWithEditorInstance(this, interfaces.exporter); @@ -3060,6 +3067,11 @@ export class Editor surface ); + this._m_properties_query = new CanvasWasmPropertiesQueryProvider( + this, + surface + ); + this._do_legacy_warmup(); // Start polling for missing images — the renderer records refs it needs From 5bfcac71fbec6adf492a8e1851e0e3a46d9fffd1 Mon Sep 17 00:00:00 2001 From: Universe Date: Sun, 29 Mar 2026 19:25:12 +0900 Subject: [PATCH 15/15] wasm 0.91.0-canary.15 --- crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js | 2 +- crates/grida-canvas-wasm/lib/bin/grida_canvas_wasm.wasm | 4 ++-- crates/grida-canvas-wasm/package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js b/crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js index 39a79ae5ac..e72689ec60 100644 --- a/crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js +++ b/crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js @@ -1,2 +1,2 @@ -var createGridaCanvas=(()=>{var _scriptName=globalThis.document?.currentScript?.src;return async function(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};if(typeof __filename!="undefined"){_scriptName=__filename}else{}var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var EXITSTATUS;var isFileURI=filename=>filename.startsWith("file://");var readyPromiseResolve,readyPromiseReject;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;if(!Module["noFSInit"]&&!FS.initialized)FS.init();TTY.init();wasmExports["Tg"]();FS.ignorePermissions=false}function preMain(){}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile("grida_canvas_wasm.wasm")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var exceptionCaught=[];var uncaughtExceptionCount=0;var ___cxa_begin_catch=ptr=>{var info=new ExceptionInfo(ptr);if(!info.get_caught()){info.set_caught(true);uncaughtExceptionCount--}info.set_rethrown(false);exceptionCaught.push(info);return ___cxa_get_exception_ptr(ptr)};var exceptionLast=0;var ___cxa_end_catch=()=>{_setThrew(0,0);var info=exceptionCaught.pop();___cxa_decrement_exception_refcount(info.excPtr);exceptionLast=0};class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var setTempRet0=val=>__emscripten_tempret_set(val);var findMatchingCatch=args=>{var thrown=exceptionLast;if(!thrown){setTempRet0(0);return 0}var info=new ExceptionInfo(thrown);info.set_adjusted_ptr(thrown);var thrownType=info.get_type();if(!thrownType){setTempRet0(0);return thrown}for(var caughtType of args){if(caughtType===0||caughtType===thrownType){break}var adjusted_ptr_addr=info.ptr+16;if(___cxa_can_catch(caughtType,thrownType,adjusted_ptr_addr)){setTempRet0(caughtType);return thrown}}setTempRet0(thrownType);return thrown};var ___cxa_find_matching_catch_2=()=>findMatchingCatch([]);var ___cxa_find_matching_catch_3=arg0=>findMatchingCatch([arg0]);var ___cxa_find_matching_catch_4=(arg0,arg1)=>findMatchingCatch([arg0,arg1]);var ___cxa_rethrow=()=>{var info=exceptionCaught.pop();if(!info){abort("no exception to throw")}var ptr=info.excPtr;if(!info.get_rethrown()){exceptionCaught.push(info);info.set_rethrown(true);info.set_caught(false);uncaughtExceptionCount++}___cxa_increment_exception_refcount(ptr);exceptionLast=ptr;throw exceptionLast};var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);___cxa_increment_exception_refcount(ptr);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var ___cxa_uncaught_exceptions=()=>uncaughtExceptionCount;var ___resumeException=ptr=>{if(!exceptionLast){exceptionLast=ptr}throw exceptionLast};var syscallGetVarargI=()=>{var ret=HEAP32[+SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret};var syscallGetVarargP=syscallGetVarargI;var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.slice(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.slice(0,-1)}return root+dir},basename:path=>path&&path.match(/([^\/]+|\/)\/*$/)[1],join:(...paths)=>PATH.normalize(paths.join("/")),join2:(l,r)=>PATH.normalize(l+"/"+r)};var initRandomFill=()=>{if(ENVIRONMENT_IS_NODE){var nodeCrypto=require("node:crypto");return view=>nodeCrypto.randomFillSync(view)}return view=>crypto.getRandomValues(view)};var randomFill=view=>{(randomFill=initRandomFill())(view)};var PATH_FS={resolve:(...args)=>{var resolvedPath="",resolvedAbsolute=false;for(var i=args.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?args[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).slice(1);to=PATH_FS.resolve(to).slice(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var FS_stdin_getChar_buffer=[];var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var intArrayFromString=(stringy,dontAddNull,length)=>{var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array};var FS_stdin_getChar=()=>{if(!FS_stdin_getChar_buffer.length){var result=null;if(ENVIRONMENT_IS_NODE){var BUFSIZE=256;var buf=Buffer.alloc(BUFSIZE);var bytesRead=0;var fd=process.stdin.fd;try{bytesRead=fs.readSync(fd,buf,0,BUFSIZE)}catch(e){if(e.toString().includes("EOF"))bytesRead=0;else throw e}if(bytesRead>0){result=buf.slice(0,bytesRead).toString("utf-8")}}else if(globalThis.window?.prompt){result=window.prompt("Input: ");if(result!==null){result+="\n"}}else{}if(!result){return null}FS_stdin_getChar_buffer=intArrayFromString(result,true)}return FS_stdin_getChar_buffer.shift()};var TTY={ttys:[],init(){},shutdown(){},register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close(stream){stream.tty.ops.fsync(stream.tty)},fsync(stream){stream.tty.ops.fsync(stream.tty)},read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){out(UTF8ArrayToString(tty.output));tty.output=[]}},ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(tty,optional_actions,data){return 0},ioctl_tiocgwinsz(tty){return[24,80]}},default_tty1_ops:{put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output?.length>0){err(UTF8ArrayToString(tty.output));tty.output=[]}}}};var zeroMemory=(ptr,size)=>HEAPU8.fill(0,ptr,ptr+size);var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var mmapAlloc=size=>{size=alignMemory(size,65536);var ptr=_emscripten_builtin_memalign(65536,size);if(ptr)zeroMemory(ptr,size);return ptr};var MEMFS={ops_table:null,mount(mount){return MEMFS.createNode(null,"/",16895,0)},createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}MEMFS.ops_table||={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}};var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.atime=node.mtime=node.ctime=Date.now();if(parent){parent.contents[name]=node;parent.atime=parent.mtime=parent.ctime=node.atime}return node},getFileDataAsTypedArray(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.atime);attr.mtime=new Date(node.mtime);attr.ctime=new Date(node.ctime);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){for(const key of["mode","atime","mtime","ctime"]){if(attr[key]!=null){node[key]=attr[key]}}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){if(!MEMFS.doesNotExistError){MEMFS.doesNotExistError=new FS.ErrnoError(44);MEMFS.doesNotExistError.stack=""}throw MEMFS.doesNotExistError},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){if(FS.isDir(old_node.mode)){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}FS.hashRemoveNode(new_node)}delete old_node.parent.contents[old_node.name];new_dir.contents[new_name]=old_node;old_node.name=new_name;new_dir.ctime=new_dir.mtime=old_node.parent.ctime=old_node.parent.mtime=Date.now()},unlink(parent,name){delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},readdir(node){return[".","..",...Object.keys(node.contents)]},symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length{var flagModes={r:0,"r+":2,w:512|64|1,"w+":512|64|2,a:1024|64|1,"a+":1024|64|2};var flags=flagModes[str];if(typeof flags=="undefined"){throw new Error(`Unknown file open mode: ${str}`)}return flags};var FS_getMode=(canRead,canWrite)=>{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode};var asyncLoad=async url=>{var arrayBuffer=await readAsync(url);return new Uint8Array(arrayBuffer)};var FS_createDataFile=(...args)=>FS.createDataFile(...args);var getUniqueRunDependency=id=>id;var runDependencies=0;var dependenciesFulfilled=null;var removeRunDependency=id=>{runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}};var addRunDependency=id=>{runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)};var preloadPlugins=[];var FS_handledByPreloadPlugin=async(byteArray,fullname)=>{if(typeof Browser!="undefined")Browser.init();for(var plugin of preloadPlugins){if(plugin["canHandle"](fullname)){return plugin["handle"](byteArray,fullname)}}return byteArray};var FS_preloadFile=async(parent,name,url,canRead,canWrite,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);addRunDependency(dep);try{var byteArray=url;if(typeof url=="string"){byteArray=await asyncLoad(url)}byteArray=await FS_handledByPreloadPlugin(byteArray,fullname);preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}}finally{removeRunDependency(dep)}};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{FS_preloadFile(parent,name,url,canRead,canWrite,dontCreateFile,canOwn,preFinish).then(onload).catch(onerror)};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,filesystems:null,syncFSRequests:0,readFiles:{},ErrnoError:class{name="ErrnoError";constructor(errno){this.errno=errno}},FSStream:class{shared={};get object(){return this.node}set object(val){this.node=val}get isRead(){return(this.flags&2097155)!==1}get isWrite(){return(this.flags&2097155)!==0}get isAppend(){return this.flags&1024}get flags(){return this.shared.flags}set flags(val){this.shared.flags=val}get position(){return this.shared.position}set position(val){this.shared.position=val}},FSNode:class{node_ops={};stream_ops={};readMode=292|73;writeMode=146;mounted=null;constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.rdev=rdev;this.atime=this.mtime=this.ctime=Date.now()}get read(){return(this.mode&this.readMode)===this.readMode}set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}get write(){return(this.mode&this.writeMode)===this.writeMode}set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}get isFolder(){return FS.isDir(this.mode)}get isDevice(){return FS.isChrdev(this.mode)}},lookupPath(path,opts={}){if(!path){throw new FS.ErrnoError(44)}opts.follow_mount??=true;if(!PATH.isAbs(path)){path=FS.cwd()+"/"+path}linkloop:for(var nlinks=0;nlinks<40;nlinks++){var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i>>0)%FS.nameTable.length},hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode(parent,name,mode,rdev){var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode(node){FS.hashRemoveNode(node)},isRoot(node){return node===node.parent},isMountpoint(node){return!!node.mounted},isFile(mode){return(mode&61440)===32768},isDir(mode){return(mode&61440)===16384},isLink(mode){return(mode&61440)===40960},isChrdev(mode){return(mode&61440)===8192},isBlkdev(mode){return(mode&61440)===24576},isFIFO(mode){return(mode&61440)===4096},isSocket(mode){return(mode&49152)===49152},flagsToPermissionString(flag){var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}else if(perms.includes("w")&&!(node.mode&146)){return 2}else if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate(dir,name){if(!FS.isDir(dir.mode)){return 54}try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&(512|64)){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},checkOpExists(op,err){if(!op){throw new FS.ErrnoError(err)}return op},MAX_OPEN_FDS:4096,nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}return stream},getStream:fd=>FS.streams[fd],createStream(stream,fd=-1){stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream(fd){FS.streams[fd]=null},dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);stream.stream_ops?.dup?.(stream);return stream},doSetAttr(stream,node,attr){var setattr=stream?.stream_ops.setattr;var arg=setattr?stream:node;setattr??=node.node_ops.setattr;FS.checkOpExists(setattr,63);setattr(arg,attr)},chrdev_stream_ops:{open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;stream.stream_ops.open?.(stream)},llseek(){throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push(...m.mounts)}return mounts},syncfs(populate,callback){if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}for(var mount of mounts){if(mount.type.syncfs){mount.type.syncfs(mount,populate,done)}else{done(null)}}},mount(type,opts,mountpoint){var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type,opts,mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);for(var[hash,current]of Object.entries(FS.nameTable)){while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}}node.mounted=null;var idx=node.mount.mounts.indexOf(mount);node.mount.mounts.splice(idx,1)},lookup(parent,name){return parent.node_ops.lookup(parent,name)},mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name){throw new FS.ErrnoError(28)}if(name==="."||name===".."){throw new FS.ErrnoError(20)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},statfs(path){return FS.statfsNode(FS.lookupPath(path,{follow:true}).node)},statfsStream(stream){return FS.statfsNode(stream.node)},statfsNode(node){var rtn={bsize:4096,frsize:4096,blocks:1e6,bfree:5e5,bavail:5e5,files:FS.nextInode,ffree:FS.nextInode-1,fsid:42,flags:2,namelen:255};if(node.node_ops.statfs){Object.assign(rtn,node.node_ops.statfs(node.mount.opts.root))}return rtn},create(path,mode=438){mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode=511){mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split("/");var d="";for(var dir of dirs){if(!dir)continue;if(d||PATH.isAbs(path))d+="/";d+=dir;try{FS.mkdir(d,mode)}catch(e){if(e.errno!=20)throw e}}},mkdev(path,mode,dev){if(typeof dev=="undefined"){dev=mode;mode=438}mode|=8192;return FS.mknod(path,mode,dev)},symlink(oldpath,newpath){if(!PATH_FS.resolve(oldpath)){throw new FS.ErrnoError(44)}var lookup=FS.lookupPath(newpath,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var newname=PATH.basename(newpath);var errCode=FS.mayCreate(parent,newname);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.symlink){throw new FS.ErrnoError(63)}return parent.node_ops.symlink(parent,newname,oldpath)},rename(old_path,new_path){var old_dirname=PATH.dirname(old_path);var new_dirname=PATH.dirname(new_path);var old_name=PATH.basename(old_path);var new_name=PATH.basename(new_path);var lookup,old_dir,new_dir;lookup=FS.lookupPath(old_path,{parent:true});old_dir=lookup.node;lookup=FS.lookupPath(new_path,{parent:true});new_dir=lookup.node;if(!old_dir||!new_dir)throw new FS.ErrnoError(44);if(old_dir.mount!==new_dir.mount){throw new FS.ErrnoError(75)}var old_node=FS.lookupNode(old_dir,old_name);var relative=PATH_FS.relative(old_path,new_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(28)}relative=PATH_FS.relative(new_path,old_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(55)}var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(old_node===new_node){return}var isdir=FS.isDir(old_node.mode);var errCode=FS.mayDelete(old_dir,old_name,isdir);if(errCode){throw new FS.ErrnoError(errCode)}errCode=new_node?FS.mayDelete(new_dir,new_name,isdir):FS.mayCreate(new_dir,new_name);if(errCode){throw new FS.ErrnoError(errCode)}if(!old_dir.node_ops.rename){throw new FS.ErrnoError(63)}if(FS.isMountpoint(old_node)||new_node&&FS.isMountpoint(new_node)){throw new FS.ErrnoError(10)}if(new_dir!==old_dir){errCode=FS.nodePermissions(old_dir,"w");if(errCode){throw new FS.ErrnoError(errCode)}}FS.hashRemoveNode(old_node);try{old_dir.node_ops.rename(old_node,new_dir,new_name);old_node.parent=new_dir}catch(e){throw e}finally{FS.hashAddNode(old_node)}},rmdir(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,true);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.rmdir){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.rmdir(parent,name);FS.destroyNode(node)},readdir(path){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var readdir=FS.checkOpExists(node.node_ops.readdir,54);return readdir(node)},unlink(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,false);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.unlink){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.unlink(parent,name);FS.destroyNode(node)},readlink(path){var lookup=FS.lookupPath(path);var link=lookup.node;if(!link){throw new FS.ErrnoError(44)}if(!link.node_ops.readlink){throw new FS.ErrnoError(28)}return link.node_ops.readlink(link)},stat(path,dontFollow){var lookup=FS.lookupPath(path,{follow:!dontFollow});var node=lookup.node;var getattr=FS.checkOpExists(node.node_ops.getattr,63);return getattr(node)},fstat(fd){var stream=FS.getStreamChecked(fd);var node=stream.node;var getattr=stream.stream_ops.getattr;var arg=getattr?stream:node;getattr??=node.node_ops.getattr;FS.checkOpExists(getattr,63);return getattr(arg)},lstat(path){return FS.stat(path,true)},doChmod(stream,node,mode,dontFollow){FS.doSetAttr(stream,node,{mode:mode&4095|node.mode&~4095,ctime:Date.now(),dontFollow})},chmod(path,mode,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChmod(null,node,mode,dontFollow)},lchmod(path,mode){FS.chmod(path,mode,true)},fchmod(fd,mode){var stream=FS.getStreamChecked(fd);FS.doChmod(stream,stream.node,mode,false)},doChown(stream,node,dontFollow){FS.doSetAttr(stream,node,{timestamp:Date.now(),dontFollow})},chown(path,uid,gid,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChown(null,node,dontFollow)},lchown(path,uid,gid){FS.chown(path,uid,gid,true)},fchown(fd,uid,gid){var stream=FS.getStreamChecked(fd);FS.doChown(stream,stream.node,false)},doTruncate(stream,node,len){if(FS.isDir(node.mode)){throw new FS.ErrnoError(31)}if(!FS.isFile(node.mode)){throw new FS.ErrnoError(28)}var errCode=FS.nodePermissions(node,"w");if(errCode){throw new FS.ErrnoError(errCode)}FS.doSetAttr(stream,node,{size:len,timestamp:Date.now()})},truncate(path,len){if(len<0){throw new FS.ErrnoError(28)}var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:true});node=lookup.node}else{node=path}FS.doTruncate(null,node,len)},ftruncate(fd,len){var stream=FS.getStreamChecked(fd);if(len<0||(stream.flags&2097155)===0){throw new FS.ErrnoError(28)}FS.doTruncate(stream,stream.node,len)},utime(path,atime,mtime){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var setattr=FS.checkOpExists(node.node_ops.setattr,63);setattr(node,{atime,mtime})},open(path,flags,mode=438){if(path===""){throw new FS.ErrnoError(44)}flags=typeof flags=="string"?FS_modeStringToFlags(flags):flags;if(flags&64){mode=mode&4095|32768}else{mode=0}var node;var isDirPath;if(typeof path=="object"){node=path}else{isDirPath=path.endsWith("/");var lookup=FS.lookupPath(path,{follow:!(flags&131072),noent_okay:true});node=lookup.node;path=lookup.path}var created=false;if(flags&64){if(node){if(flags&128){throw new FS.ErrnoError(20)}}else if(isDirPath){throw new FS.ErrnoError(31)}else{node=FS.mknod(path,mode|511,0);created=true}}if(!node){throw new FS.ErrnoError(44)}if(FS.isChrdev(node.mode)){flags&=~512}if(flags&65536&&!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}if(!created){var errCode=FS.mayOpen(node,flags);if(errCode){throw new FS.ErrnoError(errCode)}}if(flags&512&&!created){FS.truncate(node,0)}flags&=~(128|512|131072);var stream=FS.createStream({node,path:FS.getPath(node),flags,seekable:true,position:0,stream_ops:node.stream_ops,ungotten:[],error:false});if(stream.stream_ops.open){stream.stream_ops.open(stream)}if(created){FS.chmod(node,mode&511)}if(Module["logReadFiles"]&&!(flags&1)){if(!(path in FS.readFiles)){FS.readFiles[path]=1}}return stream},close(stream){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(stream.getdents)stream.getdents=null;try{if(stream.stream_ops.close){stream.stream_ops.close(stream)}}catch(e){throw e}finally{FS.closeStream(stream.fd)}stream.fd=null},isClosed(stream){return stream.fd===null},llseek(stream,offset,whence){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(!stream.seekable||!stream.stream_ops.llseek){throw new FS.ErrnoError(70)}if(whence!=0&&whence!=1&&whence!=2){throw new FS.ErrnoError(28)}stream.position=stream.stream_ops.llseek(stream,offset,whence);stream.ungotten=[];return stream.position},read(stream,buffer,offset,length,position){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.read){throw new FS.ErrnoError(28)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesRead=stream.stream_ops.read(stream,buffer,offset,length,position);if(!seeking)stream.position+=bytesRead;return bytesRead},write(stream,buffer,offset,length,position,canOwn){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.write){throw new FS.ErrnoError(28)}if(stream.seekable&&stream.flags&1024){FS.llseek(stream,0,2)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesWritten=stream.stream_ops.write(stream,buffer,offset,length,position,canOwn);if(!seeking)stream.position+=bytesWritten;return bytesWritten},mmap(stream,length,position,prot,flags){if((prot&2)!==0&&(flags&2)===0&&(stream.flags&2097155)!==2){throw new FS.ErrnoError(2)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(2)}if(!stream.stream_ops.mmap){throw new FS.ErrnoError(43)}if(!length){throw new FS.ErrnoError(28)}return stream.stream_ops.mmap(stream,length,position,prot,flags)},msync(stream,buffer,offset,length,mmapFlags){if(!stream.stream_ops.msync){return 0}return stream.stream_ops.msync(stream,buffer,offset,length,mmapFlags)},ioctl(stream,cmd,arg){if(!stream.stream_ops.ioctl){throw new FS.ErrnoError(59)}return stream.stream_ops.ioctl(stream,cmd,arg)},readFile(path,opts={}){opts.flags=opts.flags||0;opts.encoding=opts.encoding||"binary";if(opts.encoding!=="utf8"&&opts.encoding!=="binary"){abort(`Invalid encoding type "${opts.encoding}"`)}var stream=FS.open(path,opts.flags);var stat=FS.stat(path);var length=stat.size;var buf=new Uint8Array(length);FS.read(stream,buf,0,length,0);if(opts.encoding==="utf8"){buf=UTF8ArrayToString(buf)}FS.close(stream);return buf},writeFile(path,data,opts={}){opts.flags=opts.flags||577;var stream=FS.open(path,opts.flags,opts.mode);if(typeof data=="string"){data=new Uint8Array(intArrayFromString(data,true))}if(ArrayBuffer.isView(data)){FS.write(stream,data,0,data.byteLength,undefined,opts.canOwn)}else{abort("Unsupported data type")}FS.close(stream)},cwd:()=>FS.currentPath,chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories(){FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices(){FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length,llseek:()=>0});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomFill(randomBuffer);randomLeft=randomBuffer.byteLength}return randomBuffer[--randomLeft]};FS.createDevice("/dev","random",randomByte);FS.createDevice("/dev","urandom",randomByte);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories(){FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount(){var node=FS.createNode(proc_self,"fd",16895,73);node.stream_ops={llseek:MEMFS.stream_ops.llseek};node.node_ops={lookup(parent,name){var fd=+name;var stream=FS.getStreamChecked(fd);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path},id:fd+1};ret.parent=ret;return ret},readdir(){return Array.from(FS.streams.entries()).filter(([k,v])=>v).map(([k,v])=>k.toString())}};return node}},{},"/proc/self/fd")},createStandardStreams(input,output,error){if(input){FS.createDevice("/dev","stdin",input)}else{FS.symlink("/dev/tty","/dev/stdin")}if(output){FS.createDevice("/dev","stdout",null,output)}else{FS.symlink("/dev/tty","/dev/stdout")}if(error){FS.createDevice("/dev","stderr",null,error)}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1)},staticInit(){FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS}},init(input,output,error){FS.initialized=true;input??=Module["stdin"];output??=Module["stdout"];error??=Module["stderr"];FS.createStandardStreams(input,output,error)},quit(){FS.initialized=false;for(var stream of FS.streams){if(stream){FS.close(stream)}}},findObject(path,dontResolveLastLink){var ret=FS.analyzePath(path,dontResolveLastLink);if(!ret.exists){return null}return ret.object},analyzePath(path,dontResolveLastLink){try{var lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});path=lookup.path}catch(e){}var ret={isRoot:false,exists:false,error:0,name:null,path:null,object:null,parentExists:false,parentPath:null,parentObject:null};try{var lookup=FS.lookupPath(path,{parent:true});ret.parentExists=true;ret.parentPath=lookup.path;ret.parentObject=lookup.node;ret.name=PATH.basename(path);lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});ret.exists=true;ret.path=lookup.path;ret.object=lookup.node;ret.name=lookup.node.name;ret.isRoot=lookup.path==="/"}catch(e){ret.error=e.errno}return ret},createPath(parent,path,canRead,canWrite){parent=typeof parent=="string"?parent:FS.getPath(parent);var parts=path.split("/").reverse();while(parts.length){var part=parts.pop();if(!part)continue;var current=PATH.join2(parent,part);try{FS.mkdir(current)}catch(e){if(e.errno!=20)throw e}parent=current}return current},createFile(parent,name,properties,canRead,canWrite){var path=PATH.join2(typeof parent=="string"?parent:FS.getPath(parent),name);var mode=FS_getMode(canRead,canWrite);return FS.create(path,mode)},createDataFile(parent,name,data,canRead,canWrite,canOwn){var path=name;if(parent){parent=typeof parent=="string"?parent:FS.getPath(parent);path=name?PATH.join2(parent,name):parent}var mode=FS_getMode(canRead,canWrite);var node=FS.create(path,mode);if(data){if(typeof data=="string"){var arr=new Array(data.length);for(var i=0,len=data.length;ithis.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]}setDataGetter(getter){this.getter=getter}cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))abort("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)abort("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)abort("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))abort("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")abort("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true}get length(){if(!this.lengthKnown){this.cacheLength()}return this._length}get chunkSize(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}if(globalThis.XMLHttpRequest){if(!ENVIRONMENT_IS_WORKER)abort("Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc");var lazyArray=new LazyUint8Array;var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};for(const[key,fn]of Object.entries(node.stream_ops)){stream_ops[key]=(...args)=>{FS.forceLoadFile(node);return fn(...args)}}function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr,allocated:true}};node.stream_ops=stream_ops;return node}};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var SYSCALLS={calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return dir+"/"+path},writeStat(buf,stat){HEAPU32[buf>>2]=stat.dev;HEAPU32[buf+4>>2]=stat.mode;HEAPU32[buf+8>>2]=stat.nlink;HEAPU32[buf+12>>2]=stat.uid;HEAPU32[buf+16>>2]=stat.gid;HEAPU32[buf+20>>2]=stat.rdev;HEAP64[buf+24>>3]=BigInt(stat.size);HEAP32[buf+32>>2]=4096;HEAP32[buf+36>>2]=stat.blocks;var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();HEAP64[buf+40>>3]=BigInt(Math.floor(atime/1e3));HEAPU32[buf+48>>2]=atime%1e3*1e3*1e3;HEAP64[buf+56>>3]=BigInt(Math.floor(mtime/1e3));HEAPU32[buf+64>>2]=mtime%1e3*1e3*1e3;HEAP64[buf+72>>3]=BigInt(Math.floor(ctime/1e3));HEAPU32[buf+80>>2]=ctime%1e3*1e3*1e3;HEAP64[buf+88>>3]=BigInt(stat.ino);return 0},writeStatFs(buf,stats){HEAPU32[buf+4>>2]=stats.bsize;HEAPU32[buf+60>>2]=stats.bsize;HEAP64[buf+8>>3]=BigInt(stats.blocks);HEAP64[buf+16>>3]=BigInt(stats.bfree);HEAP64[buf+24>>3]=BigInt(stats.bavail);HEAP64[buf+32>>3]=BigInt(stats.files);HEAP64[buf+40>>3]=BigInt(stats.ffree);HEAPU32[buf+48>>2]=stats.fsid;HEAPU32[buf+64>>2]=stats.flags;HEAPU32[buf+56>>2]=stats.namelen},doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream},varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=syscallGetVarargI();if(arg<0){return-28}while(FS.streams[arg]){arg++}var newStream;newStream=FS.dupStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=syscallGetVarargI();stream.flags|=arg;return 0}case 12:{var arg=syscallGetVarargP();var offset=0;HEAP16[arg+offset>>1]=2;return 0}case 13:case 14:return 0}return-28}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_fstat64(fd,buf){try{return SYSCALLS.writeStat(buf,FS.fstat(fd))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);function ___syscall_getcwd(buf,size){try{if(size===0)return-28;var cwd=FS.cwd();var cwdLengthInBytes=lengthBytesUTF8(cwd)+1;if(size>2]=termios.c_iflag||0;HEAP32[argp+4>>2]=termios.c_oflag||0;HEAP32[argp+8>>2]=termios.c_cflag||0;HEAP32[argp+12>>2]=termios.c_lflag||0;for(var i=0;i<32;i++){HEAP8[argp+i+17]=termios.c_cc[i]||0}return 0}return 0}case 21510:case 21511:case 21512:{if(!stream.tty)return-59;return 0}case 21506:case 21507:case 21508:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcsets){var argp=syscallGetVarargP();var c_iflag=HEAP32[argp>>2];var c_oflag=HEAP32[argp+4>>2];var c_cflag=HEAP32[argp+8>>2];var c_lflag=HEAP32[argp+12>>2];var c_cc=[];for(var i=0;i<32;i++){c_cc.push(HEAP8[argp+i+17])}return stream.tty.ops.ioctl_tcsets(stream.tty,op,{c_iflag,c_oflag,c_cflag,c_lflag,c_cc})}return 0}case 21519:{if(!stream.tty)return-59;var argp=syscallGetVarargP();HEAP32[argp>>2]=0;return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21537:case 21531:{var argp=syscallGetVarargP();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tiocgwinsz){var winsize=stream.tty.ops.ioctl_tiocgwinsz(stream.tty);var argp=syscallGetVarargP();HEAP16[argp>>1]=winsize[0];HEAP16[argp+2>>1]=winsize[1]}return 0}case 21524:{if(!stream.tty)return-59;return 0}case 21515:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_lstat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.lstat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_newfstatat(dirfd,path,buf,flags){try{path=SYSCALLS.getStr(path);var nofollow=flags&256;var allowEmpty=flags&4096;flags=flags&~6400;path=SYSCALLS.calculateAt(dirfd,path,allowEmpty);return SYSCALLS.writeStat(buf,nofollow?FS.lstat(path):FS.stat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?syscallGetVarargI():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_stat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.stat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __abort_js=()=>abort("");var __emscripten_throw_longjmp=()=>{throw Infinity};var INT53_MAX=9007199254740992;var INT53_MIN=-9007199254740992;var bigintToI53Checked=num=>numINT53_MAX?NaN:Number(num);function __gmtime_js(time,tmPtr){time=bigintToI53Checked(time);var date=new Date(time*1e3);HEAP32[tmPtr>>2]=date.getUTCSeconds();HEAP32[tmPtr+4>>2]=date.getUTCMinutes();HEAP32[tmPtr+8>>2]=date.getUTCHours();HEAP32[tmPtr+12>>2]=date.getUTCDate();HEAP32[tmPtr+16>>2]=date.getUTCMonth();HEAP32[tmPtr+20>>2]=date.getUTCFullYear()-1900;HEAP32[tmPtr+24>>2]=date.getUTCDay();var start=Date.UTC(date.getUTCFullYear(),0,1,0,0,0,0);var yday=(date.getTime()-start)/(1e3*60*60*24)|0;HEAP32[tmPtr+28>>2]=yday}function __mmap_js(len,prot,flags,fd,offset,allocated,addr){offset=bigintToI53Checked(offset);try{var stream=SYSCALLS.getStreamFromFD(fd);var res=FS.mmap(stream,len,offset,prot,flags);var ptr=res.ptr;HEAP32[allocated>>2]=res.allocated;HEAPU32[addr>>2]=ptr;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function __munmap_js(addr,len,prot,flags,fd,offset){offset=bigintToI53Checked(offset);try{var stream=SYSCALLS.getStreamFromFD(fd);if(prot&2){SYSCALLS.doMsync(addr,stream,len,flags,offset)}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __tzset_js=(timezone,daylight,std_name,dst_name)=>{var currentYear=(new Date).getFullYear();var winter=new Date(currentYear,0,1);var summer=new Date(currentYear,6,1);var winterOffset=winter.getTimezoneOffset();var summerOffset=summer.getTimezoneOffset();var stdTimezoneOffset=Math.max(winterOffset,summerOffset);HEAPU32[timezone>>2]=stdTimezoneOffset*60;HEAP32[daylight>>2]=Number(winterOffset!=summerOffset);var extractZone=timezoneOffset=>{var sign=timezoneOffset>=0?"-":"+";var absOffset=Math.abs(timezoneOffset);var hours=String(Math.floor(absOffset/60)).padStart(2,"0");var minutes=String(absOffset%60).padStart(2,"0");return`UTC${sign}${hours}${minutes}`};var winterName=extractZone(winterOffset);var summerName=extractZone(summerOffset);if(summerOffsetperformance.now();var _emscripten_date_now=()=>Date.now();var nowIsMonotonic=1;var checkWasiClock=clock_id=>clock_id>=0&&clock_id<=3;function _clock_time_get(clk_id,ignored_precision,ptime){ignored_precision=bigintToI53Checked(ignored_precision);if(!checkWasiClock(clk_id)){return 28}var now;if(clk_id===0){now=_emscripten_date_now()}else if(nowIsMonotonic){now=_emscripten_get_now()}else{return 52}var nsec=Math.round(now*1e3*1e3);HEAP64[ptime>>3]=BigInt(nsec);return 0}var getHeapMax=()=>2147483648;var _emscripten_get_heap_max=()=>getHeapMax();var GLctx;var webgl_enable_ANGLE_instanced_arrays=ctx=>{var ext=ctx.getExtension("ANGLE_instanced_arrays");if(ext){ctx["vertexAttribDivisor"]=(index,divisor)=>ext["vertexAttribDivisorANGLE"](index,divisor);ctx["drawArraysInstanced"]=(mode,first,count,primcount)=>ext["drawArraysInstancedANGLE"](mode,first,count,primcount);ctx["drawElementsInstanced"]=(mode,count,type,indices,primcount)=>ext["drawElementsInstancedANGLE"](mode,count,type,indices,primcount);return 1}};var webgl_enable_OES_vertex_array_object=ctx=>{var ext=ctx.getExtension("OES_vertex_array_object");if(ext){ctx["createVertexArray"]=()=>ext["createVertexArrayOES"]();ctx["deleteVertexArray"]=vao=>ext["deleteVertexArrayOES"](vao);ctx["bindVertexArray"]=vao=>ext["bindVertexArrayOES"](vao);ctx["isVertexArray"]=vao=>ext["isVertexArrayOES"](vao);return 1}};var webgl_enable_WEBGL_draw_buffers=ctx=>{var ext=ctx.getExtension("WEBGL_draw_buffers");if(ext){ctx["drawBuffers"]=(n,bufs)=>ext["drawBuffersWEBGL"](n,bufs);return 1}};var webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.dibvbi=ctx.getExtension("WEBGL_draw_instanced_base_vertex_base_instance"));var webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.mdibvbi=ctx.getExtension("WEBGL_multi_draw_instanced_base_vertex_base_instance"));var webgl_enable_EXT_polygon_offset_clamp=ctx=>!!(ctx.extPolygonOffsetClamp=ctx.getExtension("EXT_polygon_offset_clamp"));var webgl_enable_EXT_clip_control=ctx=>!!(ctx.extClipControl=ctx.getExtension("EXT_clip_control"));var webgl_enable_WEBGL_polygon_mode=ctx=>!!(ctx.webglPolygonMode=ctx.getExtension("WEBGL_polygon_mode"));var webgl_enable_WEBGL_multi_draw=ctx=>!!(ctx.multiDrawWebgl=ctx.getExtension("WEBGL_multi_draw"));var getEmscriptenSupportedExtensions=ctx=>{var supportedExtensions=["ANGLE_instanced_arrays","EXT_blend_minmax","EXT_disjoint_timer_query","EXT_frag_depth","EXT_shader_texture_lod","EXT_sRGB","OES_element_index_uint","OES_fbo_render_mipmap","OES_standard_derivatives","OES_texture_float","OES_texture_half_float","OES_texture_half_float_linear","OES_vertex_array_object","WEBGL_color_buffer_float","WEBGL_depth_texture","WEBGL_draw_buffers","EXT_color_buffer_float","EXT_conservative_depth","EXT_disjoint_timer_query_webgl2","EXT_texture_norm16","NV_shader_noperspective_interpolation","WEBGL_clip_cull_distance","EXT_clip_control","EXT_color_buffer_half_float","EXT_depth_clamp","EXT_float_blend","EXT_polygon_offset_clamp","EXT_texture_compression_bptc","EXT_texture_compression_rgtc","EXT_texture_filter_anisotropic","KHR_parallel_shader_compile","OES_texture_float_linear","WEBGL_blend_func_extended","WEBGL_compressed_texture_astc","WEBGL_compressed_texture_etc","WEBGL_compressed_texture_etc1","WEBGL_compressed_texture_s3tc","WEBGL_compressed_texture_s3tc_srgb","WEBGL_debug_renderer_info","WEBGL_debug_shaders","WEBGL_lose_context","WEBGL_multi_draw","WEBGL_polygon_mode"];return(ctx.getSupportedExtensions()||[]).filter(ext=>supportedExtensions.includes(ext))};var GL={counter:1,buffers:[],programs:[],framebuffers:[],renderbuffers:[],textures:[],shaders:[],vaos:[],contexts:[],offscreenCanvases:{},queries:[],samplers:[],transformFeedbacks:[],syncs:[],stringCache:{},stringiCache:{},unpackAlignment:4,unpackRowLength:0,recordError:errorCode=>{if(!GL.lastError){GL.lastError=errorCode}},getNewId:table=>{var ret=GL.counter++;for(var i=table.length;i{for(var i=0;i>2]=id}},getSource:(shader,count,string,length)=>{var source="";for(var i=0;i>2]:undefined;source+=UTF8ToString(HEAPU32[string+i*4>>2],len)}return source},createContext:(canvas,webGLContextAttributes)=>{if(!canvas.getContextSafariWebGL2Fixed){canvas.getContextSafariWebGL2Fixed=canvas.getContext;function fixedGetContext(ver,attrs){var gl=canvas.getContextSafariWebGL2Fixed(ver,attrs);return ver=="webgl"==gl instanceof WebGLRenderingContext?gl:null}canvas.getContext=fixedGetContext}var ctx=webGLContextAttributes.majorVersion>1?canvas.getContext("webgl2",webGLContextAttributes):canvas.getContext("webgl",webGLContextAttributes);if(!ctx)return 0;var handle=GL.registerContext(ctx,webGLContextAttributes);return handle},registerContext:(ctx,webGLContextAttributes)=>{var handle=GL.getNewId(GL.contexts);var context={handle,attributes:webGLContextAttributes,version:webGLContextAttributes.majorVersion,GLctx:ctx};if(ctx.canvas)ctx.canvas.GLctxObject=context;GL.contexts[handle]=context;if(typeof webGLContextAttributes.enableExtensionsByDefault=="undefined"||webGLContextAttributes.enableExtensionsByDefault){GL.initExtensions(context)}return handle},makeContextCurrent:contextHandle=>{GL.currentContext=GL.contexts[contextHandle];Module["ctx"]=GLctx=GL.currentContext?.GLctx;return!(contextHandle&&!GLctx)},getContext:contextHandle=>GL.contexts[contextHandle],deleteContext:contextHandle=>{if(GL.currentContext===GL.contexts[contextHandle]){GL.currentContext=null}if(typeof JSEvents=="object"){JSEvents.removeAllHandlersOnTarget(GL.contexts[contextHandle].GLctx.canvas)}if(GL.contexts[contextHandle]?.GLctx.canvas){GL.contexts[contextHandle].GLctx.canvas.GLctxObject=undefined}GL.contexts[contextHandle]=null},initExtensions:context=>{context||=GL.currentContext;if(context.initExtensionsDone)return;context.initExtensionsDone=true;var GLctx=context.GLctx;webgl_enable_WEBGL_multi_draw(GLctx);webgl_enable_EXT_polygon_offset_clamp(GLctx);webgl_enable_EXT_clip_control(GLctx);webgl_enable_WEBGL_polygon_mode(GLctx);webgl_enable_ANGLE_instanced_arrays(GLctx);webgl_enable_OES_vertex_array_object(GLctx);webgl_enable_WEBGL_draw_buffers(GLctx);webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance(GLctx);webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance(GLctx);if(context.version>=2){GLctx.disjointTimerQueryExt=GLctx.getExtension("EXT_disjoint_timer_query_webgl2")}if(context.version<2||!GLctx.disjointTimerQueryExt){GLctx.disjointTimerQueryExt=GLctx.getExtension("EXT_disjoint_timer_query")}for(var ext of getEmscriptenSupportedExtensions(GLctx)){if(!ext.includes("lose_context")&&!ext.includes("debug")){GLctx.getExtension(ext)}}}};var _emscripten_glActiveTexture=x0=>GLctx.activeTexture(x0);var _emscripten_glAttachShader=(program,shader)=>{GLctx.attachShader(GL.programs[program],GL.shaders[shader])};var _emscripten_glBeginQuery=(target,id)=>{GLctx.beginQuery(target,GL.queries[id])};var _emscripten_glBeginQueryEXT=(target,id)=>{GLctx.disjointTimerQueryExt["beginQueryEXT"](target,GL.queries[id])};var _emscripten_glBeginTransformFeedback=x0=>GLctx.beginTransformFeedback(x0);var _emscripten_glBindAttribLocation=(program,index,name)=>{GLctx.bindAttribLocation(GL.programs[program],index,UTF8ToString(name))};var _emscripten_glBindBuffer=(target,buffer)=>{if(target==35051){GLctx.currentPixelPackBufferBinding=buffer}else if(target==35052){GLctx.currentPixelUnpackBufferBinding=buffer}GLctx.bindBuffer(target,GL.buffers[buffer])};var _emscripten_glBindBufferBase=(target,index,buffer)=>{GLctx.bindBufferBase(target,index,GL.buffers[buffer])};var _emscripten_glBindBufferRange=(target,index,buffer,offset,ptrsize)=>{GLctx.bindBufferRange(target,index,GL.buffers[buffer],offset,ptrsize)};var _emscripten_glBindFramebuffer=(target,framebuffer)=>{GLctx.bindFramebuffer(target,GL.framebuffers[framebuffer])};var _emscripten_glBindRenderbuffer=(target,renderbuffer)=>{GLctx.bindRenderbuffer(target,GL.renderbuffers[renderbuffer])};var _emscripten_glBindSampler=(unit,sampler)=>{GLctx.bindSampler(unit,GL.samplers[sampler])};var _emscripten_glBindTexture=(target,texture)=>{GLctx.bindTexture(target,GL.textures[texture])};var _emscripten_glBindTransformFeedback=(target,id)=>{GLctx.bindTransformFeedback(target,GL.transformFeedbacks[id])};var _emscripten_glBindVertexArray=vao=>{GLctx.bindVertexArray(GL.vaos[vao])};var _glBindVertexArray=_emscripten_glBindVertexArray;var _emscripten_glBindVertexArrayOES=_glBindVertexArray;var _emscripten_glBlendColor=(x0,x1,x2,x3)=>GLctx.blendColor(x0,x1,x2,x3);var _emscripten_glBlendEquation=x0=>GLctx.blendEquation(x0);var _emscripten_glBlendEquationSeparate=(x0,x1)=>GLctx.blendEquationSeparate(x0,x1);var _emscripten_glBlendFunc=(x0,x1)=>GLctx.blendFunc(x0,x1);var _emscripten_glBlendFuncSeparate=(x0,x1,x2,x3)=>GLctx.blendFuncSeparate(x0,x1,x2,x3);var _emscripten_glBlitFramebuffer=(x0,x1,x2,x3,x4,x5,x6,x7,x8,x9)=>GLctx.blitFramebuffer(x0,x1,x2,x3,x4,x5,x6,x7,x8,x9);var _emscripten_glBufferData=(target,size,data,usage)=>{if(GL.currentContext.version>=2){if(data&&size){GLctx.bufferData(target,HEAPU8,usage,data,size)}else{GLctx.bufferData(target,size,usage)}return}GLctx.bufferData(target,data?HEAPU8.subarray(data,data+size):size,usage)};var _emscripten_glBufferSubData=(target,offset,size,data)=>{if(GL.currentContext.version>=2){size&&GLctx.bufferSubData(target,offset,HEAPU8,data,size);return}GLctx.bufferSubData(target,offset,HEAPU8.subarray(data,data+size))};var _emscripten_glCheckFramebufferStatus=x0=>GLctx.checkFramebufferStatus(x0);var _emscripten_glClear=x0=>GLctx.clear(x0);var _emscripten_glClearBufferfi=(x0,x1,x2,x3)=>GLctx.clearBufferfi(x0,x1,x2,x3);var _emscripten_glClearBufferfv=(buffer,drawbuffer,value)=>{GLctx.clearBufferfv(buffer,drawbuffer,HEAPF32,value>>2)};var _emscripten_glClearBufferiv=(buffer,drawbuffer,value)=>{GLctx.clearBufferiv(buffer,drawbuffer,HEAP32,value>>2)};var _emscripten_glClearBufferuiv=(buffer,drawbuffer,value)=>{GLctx.clearBufferuiv(buffer,drawbuffer,HEAPU32,value>>2)};var _emscripten_glClearColor=(x0,x1,x2,x3)=>GLctx.clearColor(x0,x1,x2,x3);var _emscripten_glClearDepthf=x0=>GLctx.clearDepth(x0);var _emscripten_glClearStencil=x0=>GLctx.clearStencil(x0);var _emscripten_glClientWaitSync=(sync,flags,timeout)=>{timeout=Number(timeout);return GLctx.clientWaitSync(GL.syncs[sync],flags,timeout)};var _emscripten_glClipControlEXT=(origin,depth)=>{GLctx.extClipControl["clipControlEXT"](origin,depth)};var _emscripten_glColorMask=(red,green,blue,alpha)=>{GLctx.colorMask(!!red,!!green,!!blue,!!alpha)};var _emscripten_glCompileShader=shader=>{GLctx.compileShader(GL.shaders[shader])};var _emscripten_glCompressedTexImage2D=(target,level,internalFormat,width,height,border,imageSize,data)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding||!imageSize){GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,imageSize,data);return}GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,HEAPU8,data,imageSize);return}GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,HEAPU8.subarray(data,data+imageSize))};var _emscripten_glCompressedTexImage3D=(target,level,internalFormat,width,height,depth,border,imageSize,data)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.compressedTexImage3D(target,level,internalFormat,width,height,depth,border,imageSize,data)}else{GLctx.compressedTexImage3D(target,level,internalFormat,width,height,depth,border,HEAPU8,data,imageSize)}};var _emscripten_glCompressedTexSubImage2D=(target,level,xoffset,yoffset,width,height,format,imageSize,data)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding||!imageSize){GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,imageSize,data);return}GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,HEAPU8,data,imageSize);return}GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,HEAPU8.subarray(data,data+imageSize))};var _emscripten_glCompressedTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,imageSize,data)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.compressedTexSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,imageSize,data)}else{GLctx.compressedTexSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,HEAPU8,data,imageSize)}};var _emscripten_glCopyBufferSubData=(x0,x1,x2,x3,x4)=>GLctx.copyBufferSubData(x0,x1,x2,x3,x4);var _emscripten_glCopyTexImage2D=(x0,x1,x2,x3,x4,x5,x6,x7)=>GLctx.copyTexImage2D(x0,x1,x2,x3,x4,x5,x6,x7);var _emscripten_glCopyTexSubImage2D=(x0,x1,x2,x3,x4,x5,x6,x7)=>GLctx.copyTexSubImage2D(x0,x1,x2,x3,x4,x5,x6,x7);var _emscripten_glCopyTexSubImage3D=(x0,x1,x2,x3,x4,x5,x6,x7,x8)=>GLctx.copyTexSubImage3D(x0,x1,x2,x3,x4,x5,x6,x7,x8);var _emscripten_glCreateProgram=()=>{var id=GL.getNewId(GL.programs);var program=GLctx.createProgram();program.name=id;program.maxUniformLength=program.maxAttributeLength=program.maxUniformBlockNameLength=0;program.uniformIdCounter=1;GL.programs[id]=program;return id};var _emscripten_glCreateShader=shaderType=>{var id=GL.getNewId(GL.shaders);GL.shaders[id]=GLctx.createShader(shaderType);return id};var _emscripten_glCullFace=x0=>GLctx.cullFace(x0);var _emscripten_glDeleteBuffers=(n,buffers)=>{for(var i=0;i>2];var buffer=GL.buffers[id];if(!buffer)continue;GLctx.deleteBuffer(buffer);buffer.name=0;GL.buffers[id]=null;if(id==GLctx.currentPixelPackBufferBinding)GLctx.currentPixelPackBufferBinding=0;if(id==GLctx.currentPixelUnpackBufferBinding)GLctx.currentPixelUnpackBufferBinding=0}};var _emscripten_glDeleteFramebuffers=(n,framebuffers)=>{for(var i=0;i>2];var framebuffer=GL.framebuffers[id];if(!framebuffer)continue;GLctx.deleteFramebuffer(framebuffer);framebuffer.name=0;GL.framebuffers[id]=null}};var _emscripten_glDeleteProgram=id=>{if(!id)return;var program=GL.programs[id];if(!program){GL.recordError(1281);return}GLctx.deleteProgram(program);program.name=0;GL.programs[id]=null};var _emscripten_glDeleteQueries=(n,ids)=>{for(var i=0;i>2];var query=GL.queries[id];if(!query)continue;GLctx.deleteQuery(query);GL.queries[id]=null}};var _emscripten_glDeleteQueriesEXT=(n,ids)=>{for(var i=0;i>2];var query=GL.queries[id];if(!query)continue;GLctx.disjointTimerQueryExt["deleteQueryEXT"](query);GL.queries[id]=null}};var _emscripten_glDeleteRenderbuffers=(n,renderbuffers)=>{for(var i=0;i>2];var renderbuffer=GL.renderbuffers[id];if(!renderbuffer)continue;GLctx.deleteRenderbuffer(renderbuffer);renderbuffer.name=0;GL.renderbuffers[id]=null}};var _emscripten_glDeleteSamplers=(n,samplers)=>{for(var i=0;i>2];var sampler=GL.samplers[id];if(!sampler)continue;GLctx.deleteSampler(sampler);sampler.name=0;GL.samplers[id]=null}};var _emscripten_glDeleteShader=id=>{if(!id)return;var shader=GL.shaders[id];if(!shader){GL.recordError(1281);return}GLctx.deleteShader(shader);GL.shaders[id]=null};var _emscripten_glDeleteSync=id=>{if(!id)return;var sync=GL.syncs[id];if(!sync){GL.recordError(1281);return}GLctx.deleteSync(sync);sync.name=0;GL.syncs[id]=null};var _emscripten_glDeleteTextures=(n,textures)=>{for(var i=0;i>2];var texture=GL.textures[id];if(!texture)continue;GLctx.deleteTexture(texture);texture.name=0;GL.textures[id]=null}};var _emscripten_glDeleteTransformFeedbacks=(n,ids)=>{for(var i=0;i>2];var transformFeedback=GL.transformFeedbacks[id];if(!transformFeedback)continue;GLctx.deleteTransformFeedback(transformFeedback);transformFeedback.name=0;GL.transformFeedbacks[id]=null}};var _emscripten_glDeleteVertexArrays=(n,vaos)=>{for(var i=0;i>2];GLctx.deleteVertexArray(GL.vaos[id]);GL.vaos[id]=null}};var _glDeleteVertexArrays=_emscripten_glDeleteVertexArrays;var _emscripten_glDeleteVertexArraysOES=_glDeleteVertexArrays;var _emscripten_glDepthFunc=x0=>GLctx.depthFunc(x0);var _emscripten_glDepthMask=flag=>{GLctx.depthMask(!!flag)};var _emscripten_glDepthRangef=(x0,x1)=>GLctx.depthRange(x0,x1);var _emscripten_glDetachShader=(program,shader)=>{GLctx.detachShader(GL.programs[program],GL.shaders[shader])};var _emscripten_glDisable=x0=>GLctx.disable(x0);var _emscripten_glDisableVertexAttribArray=index=>{GLctx.disableVertexAttribArray(index)};var _emscripten_glDrawArrays=(mode,first,count)=>{GLctx.drawArrays(mode,first,count)};var _emscripten_glDrawArraysInstanced=(mode,first,count,primcount)=>{GLctx.drawArraysInstanced(mode,first,count,primcount)};var _glDrawArraysInstanced=_emscripten_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedANGLE=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedARB=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedBaseInstanceWEBGL=(mode,first,count,instanceCount,baseInstance)=>{GLctx.dibvbi["drawArraysInstancedBaseInstanceWEBGL"](mode,first,count,instanceCount,baseInstance)};var _emscripten_glDrawArraysInstancedEXT=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedNV=_glDrawArraysInstanced;var tempFixedLengthArray=[];var _emscripten_glDrawBuffers=(n,bufs)=>{var bufArray=tempFixedLengthArray[n];for(var i=0;i>2]}GLctx.drawBuffers(bufArray)};var _glDrawBuffers=_emscripten_glDrawBuffers;var _emscripten_glDrawBuffersEXT=_glDrawBuffers;var _emscripten_glDrawBuffersWEBGL=_glDrawBuffers;var _emscripten_glDrawElements=(mode,count,type,indices)=>{GLctx.drawElements(mode,count,type,indices)};var _emscripten_glDrawElementsInstanced=(mode,count,type,indices,primcount)=>{GLctx.drawElementsInstanced(mode,count,type,indices,primcount)};var _glDrawElementsInstanced=_emscripten_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedANGLE=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedARB=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL=(mode,count,type,offset,instanceCount,baseVertex,baseinstance)=>{GLctx.dibvbi["drawElementsInstancedBaseVertexBaseInstanceWEBGL"](mode,count,type,offset,instanceCount,baseVertex,baseinstance)};var _emscripten_glDrawElementsInstancedEXT=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedNV=_glDrawElementsInstanced;var _glDrawElements=_emscripten_glDrawElements;var _emscripten_glDrawRangeElements=(mode,start,end,count,type,indices)=>{_glDrawElements(mode,count,type,indices)};var _emscripten_glEnable=x0=>GLctx.enable(x0);var _emscripten_glEnableVertexAttribArray=index=>{GLctx.enableVertexAttribArray(index)};var _emscripten_glEndQuery=x0=>GLctx.endQuery(x0);var _emscripten_glEndQueryEXT=target=>{GLctx.disjointTimerQueryExt["endQueryEXT"](target)};var _emscripten_glEndTransformFeedback=()=>GLctx.endTransformFeedback();var _emscripten_glFenceSync=(condition,flags)=>{var sync=GLctx.fenceSync(condition,flags);if(sync){var id=GL.getNewId(GL.syncs);sync.name=id;GL.syncs[id]=sync;return id}return 0};var _emscripten_glFinish=()=>GLctx.finish();var _emscripten_glFlush=()=>GLctx.flush();var _emscripten_glFramebufferRenderbuffer=(target,attachment,renderbuffertarget,renderbuffer)=>{GLctx.framebufferRenderbuffer(target,attachment,renderbuffertarget,GL.renderbuffers[renderbuffer])};var _emscripten_glFramebufferTexture2D=(target,attachment,textarget,texture,level)=>{GLctx.framebufferTexture2D(target,attachment,textarget,GL.textures[texture],level)};var _emscripten_glFramebufferTextureLayer=(target,attachment,texture,level,layer)=>{GLctx.framebufferTextureLayer(target,attachment,GL.textures[texture],level,layer)};var _emscripten_glFrontFace=x0=>GLctx.frontFace(x0);var _emscripten_glGenBuffers=(n,buffers)=>{GL.genObject(n,buffers,"createBuffer",GL.buffers)};var _emscripten_glGenFramebuffers=(n,ids)=>{GL.genObject(n,ids,"createFramebuffer",GL.framebuffers)};var _emscripten_glGenQueries=(n,ids)=>{GL.genObject(n,ids,"createQuery",GL.queries)};var _emscripten_glGenQueriesEXT=(n,ids)=>{for(var i=0;i>2]=0;return}var id=GL.getNewId(GL.queries);query.name=id;GL.queries[id]=query;HEAP32[ids+i*4>>2]=id}};var _emscripten_glGenRenderbuffers=(n,renderbuffers)=>{GL.genObject(n,renderbuffers,"createRenderbuffer",GL.renderbuffers)};var _emscripten_glGenSamplers=(n,samplers)=>{GL.genObject(n,samplers,"createSampler",GL.samplers)};var _emscripten_glGenTextures=(n,textures)=>{GL.genObject(n,textures,"createTexture",GL.textures)};var _emscripten_glGenTransformFeedbacks=(n,ids)=>{GL.genObject(n,ids,"createTransformFeedback",GL.transformFeedbacks)};var _emscripten_glGenVertexArrays=(n,arrays)=>{GL.genObject(n,arrays,"createVertexArray",GL.vaos)};var _glGenVertexArrays=_emscripten_glGenVertexArrays;var _emscripten_glGenVertexArraysOES=_glGenVertexArrays;var _emscripten_glGenerateMipmap=x0=>GLctx.generateMipmap(x0);var __glGetActiveAttribOrUniform=(funcName,program,index,bufSize,length,size,type,name)=>{program=GL.programs[program];var info=GLctx[funcName](program,index);if(info){var numBytesWrittenExclNull=name&&stringToUTF8(info.name,name,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull;if(size)HEAP32[size>>2]=info.size;if(type)HEAP32[type>>2]=info.type}};var _emscripten_glGetActiveAttrib=(program,index,bufSize,length,size,type,name)=>__glGetActiveAttribOrUniform("getActiveAttrib",program,index,bufSize,length,size,type,name);var _emscripten_glGetActiveUniform=(program,index,bufSize,length,size,type,name)=>__glGetActiveAttribOrUniform("getActiveUniform",program,index,bufSize,length,size,type,name);var _emscripten_glGetActiveUniformBlockName=(program,uniformBlockIndex,bufSize,length,uniformBlockName)=>{program=GL.programs[program];var result=GLctx.getActiveUniformBlockName(program,uniformBlockIndex);if(!result)return;if(uniformBlockName&&bufSize>0){var numBytesWrittenExclNull=stringToUTF8(result,uniformBlockName,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull}else{if(length)HEAP32[length>>2]=0}};var _emscripten_glGetActiveUniformBlockiv=(program,uniformBlockIndex,pname,params)=>{if(!params){GL.recordError(1281);return}program=GL.programs[program];if(pname==35393){var name=GLctx.getActiveUniformBlockName(program,uniformBlockIndex);HEAP32[params>>2]=name.length+1;return}var result=GLctx.getActiveUniformBlockParameter(program,uniformBlockIndex,pname);if(result===null)return;if(pname==35395){for(var i=0;i>2]=result[i]}}else{HEAP32[params>>2]=result}};var _emscripten_glGetActiveUniformsiv=(program,uniformCount,uniformIndices,pname,params)=>{if(!params){GL.recordError(1281);return}if(uniformCount>0&&uniformIndices==0){GL.recordError(1281);return}program=GL.programs[program];var ids=[];for(var i=0;i>2])}var result=GLctx.getActiveUniforms(program,ids,pname);if(!result)return;var len=result.length;for(var i=0;i>2]=result[i]}};var _emscripten_glGetAttachedShaders=(program,maxCount,count,shaders)=>{var result=GLctx.getAttachedShaders(GL.programs[program]);var len=result.length;if(len>maxCount){len=maxCount}HEAP32[count>>2]=len;for(var i=0;i>2]=id}};var _emscripten_glGetAttribLocation=(program,name)=>GLctx.getAttribLocation(GL.programs[program],UTF8ToString(name));var writeI53ToI64=(ptr,num)=>{HEAPU32[ptr>>2]=num;var lower=HEAPU32[ptr>>2];HEAPU32[ptr+4>>2]=(num-lower)/4294967296};var webglGetExtensions=()=>{var exts=getEmscriptenSupportedExtensions(GLctx);exts=exts.concat(exts.map(e=>"GL_"+e));return exts};var emscriptenWebGLGet=(name_,p,type)=>{if(!p){GL.recordError(1281);return}var ret=undefined;switch(name_){case 36346:ret=1;break;case 36344:if(type!=0&&type!=1){GL.recordError(1280)}return;case 34814:case 36345:ret=0;break;case 34466:var formats=GLctx.getParameter(34467);ret=formats?formats.length:0;break;case 33309:if(GL.currentContext.version<2){GL.recordError(1282);return}ret=webglGetExtensions().length;break;case 33307:case 33308:if(GL.currentContext.version<2){GL.recordError(1280);return}ret=name_==33307?3:0;break}if(ret===undefined){var result=GLctx.getParameter(name_);switch(typeof result){case"number":ret=result;break;case"boolean":ret=result?1:0;break;case"string":GL.recordError(1280);return;case"object":if(result===null){switch(name_){case 34964:case 35725:case 34965:case 36006:case 36007:case 32873:case 34229:case 36662:case 36663:case 35053:case 35055:case 36010:case 35097:case 35869:case 32874:case 36389:case 35983:case 35368:case 34068:{ret=0;break}default:{GL.recordError(1280);return}}}else if(result instanceof Float32Array||result instanceof Uint32Array||result instanceof Int32Array||result instanceof Array){for(var i=0;i>2]=result[i];break;case 2:HEAPF32[p+i*4>>2]=result[i];break;case 4:HEAP8[p+i]=result[i]?1:0;break}}return}else{try{ret=result.name|0}catch(e){GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Unknown object returned from WebGL getParameter(${name_})! (error: ${e})`);return}}break;default:GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Native code calling glGet${type}v(${name_}) and it returns ${result} of type ${typeof result}!`);return}}switch(type){case 1:writeI53ToI64(p,ret);break;case 0:HEAP32[p>>2]=ret;break;case 2:HEAPF32[p>>2]=ret;break;case 4:HEAP8[p]=ret?1:0;break}};var _emscripten_glGetBooleanv=(name_,p)=>emscriptenWebGLGet(name_,p,4);var _emscripten_glGetBufferParameteri64v=(target,value,data)=>{if(!data){GL.recordError(1281);return}writeI53ToI64(data,GLctx.getBufferParameter(target,value))};var _emscripten_glGetBufferParameteriv=(target,value,data)=>{if(!data){GL.recordError(1281);return}HEAP32[data>>2]=GLctx.getBufferParameter(target,value)};var _emscripten_glGetError=()=>{var error=GLctx.getError()||GL.lastError;GL.lastError=0;return error};var _emscripten_glGetFloatv=(name_,p)=>emscriptenWebGLGet(name_,p,2);var _emscripten_glGetFragDataLocation=(program,name)=>GLctx.getFragDataLocation(GL.programs[program],UTF8ToString(name));var _emscripten_glGetFramebufferAttachmentParameteriv=(target,attachment,pname,params)=>{var result=GLctx.getFramebufferAttachmentParameter(target,attachment,pname);if(result instanceof WebGLRenderbuffer||result instanceof WebGLTexture){result=result.name|0}HEAP32[params>>2]=result};var emscriptenWebGLGetIndexed=(target,index,data,type)=>{if(!data){GL.recordError(1281);return}var result=GLctx.getIndexedParameter(target,index);var ret;switch(typeof result){case"boolean":ret=result?1:0;break;case"number":ret=result;break;case"object":if(result===null){switch(target){case 35983:case 35368:ret=0;break;default:{GL.recordError(1280);return}}}else if(result instanceof WebGLBuffer){ret=result.name|0}else{GL.recordError(1280);return}break;default:GL.recordError(1280);return}switch(type){case 1:writeI53ToI64(data,ret);break;case 0:HEAP32[data>>2]=ret;break;case 2:HEAPF32[data>>2]=ret;break;case 4:HEAP8[data]=ret?1:0;break;default:abort("internal emscriptenWebGLGetIndexed() error, bad type: "+type)}};var _emscripten_glGetInteger64i_v=(target,index,data)=>emscriptenWebGLGetIndexed(target,index,data,1);var _emscripten_glGetInteger64v=(name_,p)=>{emscriptenWebGLGet(name_,p,1)};var _emscripten_glGetIntegeri_v=(target,index,data)=>emscriptenWebGLGetIndexed(target,index,data,0);var _emscripten_glGetIntegerv=(name_,p)=>emscriptenWebGLGet(name_,p,0);var _emscripten_glGetInternalformativ=(target,internalformat,pname,bufSize,params)=>{if(bufSize<0){GL.recordError(1281);return}if(!params){GL.recordError(1281);return}var ret=GLctx.getInternalformatParameter(target,internalformat,pname);if(ret===null)return;for(var i=0;i>2]=ret[i]}};var _emscripten_glGetProgramBinary=(program,bufSize,length,binaryFormat,binary)=>{GL.recordError(1282)};var _emscripten_glGetProgramInfoLog=(program,maxLength,length,infoLog)=>{var log=GLctx.getProgramInfoLog(GL.programs[program]);if(log===null)log="(unknown error)";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetProgramiv=(program,pname,p)=>{if(!p){GL.recordError(1281);return}if(program>=GL.counter){GL.recordError(1281);return}program=GL.programs[program];if(pname==35716){var log=GLctx.getProgramInfoLog(program);if(log===null)log="(unknown error)";HEAP32[p>>2]=log.length+1}else if(pname==35719){if(!program.maxUniformLength){var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(var i=0;i>2]=program.maxUniformLength}else if(pname==35722){if(!program.maxAttributeLength){var numActiveAttributes=GLctx.getProgramParameter(program,35721);for(var i=0;i>2]=program.maxAttributeLength}else if(pname==35381){if(!program.maxUniformBlockNameLength){var numActiveUniformBlocks=GLctx.getProgramParameter(program,35382);for(var i=0;i>2]=program.maxUniformBlockNameLength}else{HEAP32[p>>2]=GLctx.getProgramParameter(program,pname)}};var _emscripten_glGetQueryObjecti64vEXT=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param;if(GL.currentContext.version<2){param=GLctx.disjointTimerQueryExt["getQueryObjectEXT"](query,pname)}else{param=GLctx.getQueryParameter(query,pname)}var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}writeI53ToI64(params,ret)};var _emscripten_glGetQueryObjectivEXT=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param=GLctx.disjointTimerQueryExt["getQueryObjectEXT"](query,pname);var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}HEAP32[params>>2]=ret};var _glGetQueryObjecti64vEXT=_emscripten_glGetQueryObjecti64vEXT;var _emscripten_glGetQueryObjectui64vEXT=_glGetQueryObjecti64vEXT;var _emscripten_glGetQueryObjectuiv=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param=GLctx.getQueryParameter(query,pname);var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}HEAP32[params>>2]=ret};var _glGetQueryObjectivEXT=_emscripten_glGetQueryObjectivEXT;var _emscripten_glGetQueryObjectuivEXT=_glGetQueryObjectivEXT;var _emscripten_glGetQueryiv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getQuery(target,pname)};var _emscripten_glGetQueryivEXT=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.disjointTimerQueryExt["getQueryEXT"](target,pname)};var _emscripten_glGetRenderbufferParameteriv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getRenderbufferParameter(target,pname)};var _emscripten_glGetSamplerParameterfv=(sampler,pname,params)=>{if(!params){GL.recordError(1281);return}HEAPF32[params>>2]=GLctx.getSamplerParameter(GL.samplers[sampler],pname)};var _emscripten_glGetSamplerParameteriv=(sampler,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getSamplerParameter(GL.samplers[sampler],pname)};var _emscripten_glGetShaderInfoLog=(shader,maxLength,length,infoLog)=>{var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log="(unknown error)";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetShaderPrecisionFormat=(shaderType,precisionType,range,precision)=>{var result=GLctx.getShaderPrecisionFormat(shaderType,precisionType);HEAP32[range>>2]=result.rangeMin;HEAP32[range+4>>2]=result.rangeMax;HEAP32[precision>>2]=result.precision};var _emscripten_glGetShaderSource=(shader,bufSize,length,source)=>{var result=GLctx.getShaderSource(GL.shaders[shader]);if(!result)return;var numBytesWrittenExclNull=bufSize>0&&source?stringToUTF8(result,source,bufSize):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetShaderiv=(shader,pname,p)=>{if(!p){GL.recordError(1281);return}if(pname==35716){var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log="(unknown error)";var logLength=log?log.length+1:0;HEAP32[p>>2]=logLength}else if(pname==35720){var source=GLctx.getShaderSource(GL.shaders[shader]);var sourceLength=source?source.length+1:0;HEAP32[p>>2]=sourceLength}else{HEAP32[p>>2]=GLctx.getShaderParameter(GL.shaders[shader],pname)}};var stringToNewUTF8=str=>{var size=lengthBytesUTF8(str)+1;var ret=_malloc(size);if(ret)stringToUTF8(str,ret,size);return ret};var _emscripten_glGetString=name_=>{var ret=GL.stringCache[name_];if(!ret){switch(name_){case 7939:ret=stringToNewUTF8(webglGetExtensions().join(" "));break;case 7936:case 7937:case 37445:case 37446:var s=GLctx.getParameter(name_);if(!s){GL.recordError(1280)}ret=s?stringToNewUTF8(s):0;break;case 7938:var webGLVersion=GLctx.getParameter(7938);var glVersion=`OpenGL ES 2.0 (${webGLVersion})`;if(GL.currentContext.version>=2)glVersion=`OpenGL ES 3.0 (${webGLVersion})`;ret=stringToNewUTF8(glVersion);break;case 35724:var glslVersion=GLctx.getParameter(35724);var ver_re=/^WebGL GLSL ES ([0-9]\.[0-9][0-9]?)(?:$| .*)/;var ver_num=glslVersion.match(ver_re);if(ver_num!==null){if(ver_num[1].length==3)ver_num[1]=ver_num[1]+"0";glslVersion=`OpenGL ES GLSL ES ${ver_num[1]} (${glslVersion})`}ret=stringToNewUTF8(glslVersion);break;default:GL.recordError(1280)}GL.stringCache[name_]=ret}return ret};var _emscripten_glGetStringi=(name,index)=>{if(GL.currentContext.version<2){GL.recordError(1282);return 0}var stringiCache=GL.stringiCache[name];if(stringiCache){if(index<0||index>=stringiCache.length){GL.recordError(1281);return 0}return stringiCache[index]}switch(name){case 7939:var exts=webglGetExtensions().map(stringToNewUTF8);stringiCache=GL.stringiCache[name]=exts;if(index<0||index>=stringiCache.length){GL.recordError(1281);return 0}return stringiCache[index];default:GL.recordError(1280);return 0}};var _emscripten_glGetSynciv=(sync,pname,bufSize,length,values)=>{if(bufSize<0){GL.recordError(1281);return}if(!values){GL.recordError(1281);return}var ret=GLctx.getSyncParameter(GL.syncs[sync],pname);if(ret!==null){HEAP32[values>>2]=ret;if(length)HEAP32[length>>2]=1}};var _emscripten_glGetTexParameterfv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAPF32[params>>2]=GLctx.getTexParameter(target,pname)};var _emscripten_glGetTexParameteriv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getTexParameter(target,pname)};var _emscripten_glGetTransformFeedbackVarying=(program,index,bufSize,length,size,type,name)=>{program=GL.programs[program];var info=GLctx.getTransformFeedbackVarying(program,index);if(!info)return;if(name&&bufSize>0){var numBytesWrittenExclNull=stringToUTF8(info.name,name,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull}else{if(length)HEAP32[length>>2]=0}if(size)HEAP32[size>>2]=info.size;if(type)HEAP32[type>>2]=info.type};var _emscripten_glGetUniformBlockIndex=(program,uniformBlockName)=>GLctx.getUniformBlockIndex(GL.programs[program],UTF8ToString(uniformBlockName));var _emscripten_glGetUniformIndices=(program,uniformCount,uniformNames,uniformIndices)=>{if(!uniformIndices){GL.recordError(1281);return}if(uniformCount>0&&(uniformNames==0||uniformIndices==0)){GL.recordError(1281);return}program=GL.programs[program];var names=[];for(var i=0;i>2]));var result=GLctx.getUniformIndices(program,names);if(!result)return;var len=result.length;for(var i=0;i>2]=result[i]}};var jstoi_q=str=>parseInt(str);var webglGetLeftBracePos=name=>name.slice(-1)=="]"&&name.lastIndexOf("[");var webglPrepareUniformLocationsBeforeFirstUse=program=>{var uniformLocsById=program.uniformLocsById,uniformSizeAndIdsByName=program.uniformSizeAndIdsByName,i,j;if(!uniformLocsById){program.uniformLocsById=uniformLocsById={};program.uniformArrayNamesById={};var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(i=0;i0?nm.slice(0,lb):nm;var id=program.uniformIdCounter;program.uniformIdCounter+=sz;uniformSizeAndIdsByName[arrayName]=[sz,id];for(j=0;j{name=UTF8ToString(name);if(program=GL.programs[program]){webglPrepareUniformLocationsBeforeFirstUse(program);var uniformLocsById=program.uniformLocsById;var arrayIndex=0;var uniformBaseName=name;var leftBrace=webglGetLeftBracePos(name);if(leftBrace>0){arrayIndex=jstoi_q(name.slice(leftBrace+1))>>>0;uniformBaseName=name.slice(0,leftBrace)}var sizeAndId=program.uniformSizeAndIdsByName[uniformBaseName];if(sizeAndId&&arrayIndex{var p=GLctx.currentProgram;if(p){var webglLoc=p.uniformLocsById[location];if(typeof webglLoc=="number"){p.uniformLocsById[location]=webglLoc=GLctx.getUniformLocation(p,p.uniformArrayNamesById[location]+(webglLoc>0?`[${webglLoc}]`:""))}return webglLoc}else{GL.recordError(1282)}};var emscriptenWebGLGetUniform=(program,location,params,type)=>{if(!params){GL.recordError(1281);return}program=GL.programs[program];webglPrepareUniformLocationsBeforeFirstUse(program);var data=GLctx.getUniform(program,webglGetUniformLocation(location));if(typeof data=="number"||typeof data=="boolean"){switch(type){case 0:HEAP32[params>>2]=data;break;case 2:HEAPF32[params>>2]=data;break}}else{for(var i=0;i>2]=data[i];break;case 2:HEAPF32[params+i*4>>2]=data[i];break}}}};var _emscripten_glGetUniformfv=(program,location,params)=>{emscriptenWebGLGetUniform(program,location,params,2)};var _emscripten_glGetUniformiv=(program,location,params)=>{emscriptenWebGLGetUniform(program,location,params,0)};var _emscripten_glGetUniformuiv=(program,location,params)=>emscriptenWebGLGetUniform(program,location,params,0);var emscriptenWebGLGetVertexAttrib=(index,pname,params,type)=>{if(!params){GL.recordError(1281);return}var data=GLctx.getVertexAttrib(index,pname);if(pname==34975){HEAP32[params>>2]=data&&data["name"]}else if(typeof data=="number"||typeof data=="boolean"){switch(type){case 0:HEAP32[params>>2]=data;break;case 2:HEAPF32[params>>2]=data;break;case 5:HEAP32[params>>2]=Math.fround(data);break}}else{for(var i=0;i>2]=data[i];break;case 2:HEAPF32[params+i*4>>2]=data[i];break;case 5:HEAP32[params+i*4>>2]=Math.fround(data[i]);break}}}};var _emscripten_glGetVertexAttribIiv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,0)};var _glGetVertexAttribIiv=_emscripten_glGetVertexAttribIiv;var _emscripten_glGetVertexAttribIuiv=_glGetVertexAttribIiv;var _emscripten_glGetVertexAttribPointerv=(index,pname,pointer)=>{if(!pointer){GL.recordError(1281);return}HEAP32[pointer>>2]=GLctx.getVertexAttribOffset(index,pname)};var _emscripten_glGetVertexAttribfv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,2)};var _emscripten_glGetVertexAttribiv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,5)};var _emscripten_glHint=(x0,x1)=>GLctx.hint(x0,x1);var _emscripten_glInvalidateFramebuffer=(target,numAttachments,attachments)=>{var list=tempFixedLengthArray[numAttachments];for(var i=0;i>2]}GLctx.invalidateFramebuffer(target,list)};var _emscripten_glInvalidateSubFramebuffer=(target,numAttachments,attachments,x,y,width,height)=>{var list=tempFixedLengthArray[numAttachments];for(var i=0;i>2]}GLctx.invalidateSubFramebuffer(target,list,x,y,width,height)};var _emscripten_glIsBuffer=buffer=>{var b=GL.buffers[buffer];if(!b)return 0;return GLctx.isBuffer(b)};var _emscripten_glIsEnabled=x0=>GLctx.isEnabled(x0);var _emscripten_glIsFramebuffer=framebuffer=>{var fb=GL.framebuffers[framebuffer];if(!fb)return 0;return GLctx.isFramebuffer(fb)};var _emscripten_glIsProgram=program=>{program=GL.programs[program];if(!program)return 0;return GLctx.isProgram(program)};var _emscripten_glIsQuery=id=>{var query=GL.queries[id];if(!query)return 0;return GLctx.isQuery(query)};var _emscripten_glIsQueryEXT=id=>{var query=GL.queries[id];if(!query)return 0;return GLctx.disjointTimerQueryExt["isQueryEXT"](query)};var _emscripten_glIsRenderbuffer=renderbuffer=>{var rb=GL.renderbuffers[renderbuffer];if(!rb)return 0;return GLctx.isRenderbuffer(rb)};var _emscripten_glIsSampler=id=>{var sampler=GL.samplers[id];if(!sampler)return 0;return GLctx.isSampler(sampler)};var _emscripten_glIsShader=shader=>{var s=GL.shaders[shader];if(!s)return 0;return GLctx.isShader(s)};var _emscripten_glIsSync=sync=>GLctx.isSync(GL.syncs[sync]);var _emscripten_glIsTexture=id=>{var texture=GL.textures[id];if(!texture)return 0;return GLctx.isTexture(texture)};var _emscripten_glIsTransformFeedback=id=>GLctx.isTransformFeedback(GL.transformFeedbacks[id]);var _emscripten_glIsVertexArray=array=>{var vao=GL.vaos[array];if(!vao)return 0;return GLctx.isVertexArray(vao)};var _glIsVertexArray=_emscripten_glIsVertexArray;var _emscripten_glIsVertexArrayOES=_glIsVertexArray;var _emscripten_glLineWidth=x0=>GLctx.lineWidth(x0);var _emscripten_glLinkProgram=program=>{program=GL.programs[program];GLctx.linkProgram(program);program.uniformLocsById=0;program.uniformSizeAndIdsByName={}};var _emscripten_glMultiDrawArraysInstancedBaseInstanceWEBGL=(mode,firsts,counts,instanceCounts,baseInstances,drawCount)=>{GLctx.mdibvbi["multiDrawArraysInstancedBaseInstanceWEBGL"](mode,HEAP32,firsts>>2,HEAP32,counts>>2,HEAP32,instanceCounts>>2,HEAPU32,baseInstances>>2,drawCount)};var _emscripten_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL=(mode,counts,type,offsets,instanceCounts,baseVertices,baseInstances,drawCount)=>{GLctx.mdibvbi["multiDrawElementsInstancedBaseVertexBaseInstanceWEBGL"](mode,HEAP32,counts>>2,type,HEAP32,offsets>>2,HEAP32,instanceCounts>>2,HEAP32,baseVertices>>2,HEAPU32,baseInstances>>2,drawCount)};var _emscripten_glPauseTransformFeedback=()=>GLctx.pauseTransformFeedback();var _emscripten_glPixelStorei=(pname,param)=>{if(pname==3317){GL.unpackAlignment=param}else if(pname==3314){GL.unpackRowLength=param}GLctx.pixelStorei(pname,param)};var _emscripten_glPolygonModeWEBGL=(face,mode)=>{GLctx.webglPolygonMode["polygonModeWEBGL"](face,mode)};var _emscripten_glPolygonOffset=(x0,x1)=>GLctx.polygonOffset(x0,x1);var _emscripten_glPolygonOffsetClampEXT=(factor,units,clamp)=>{GLctx.extPolygonOffsetClamp["polygonOffsetClampEXT"](factor,units,clamp)};var _emscripten_glProgramBinary=(program,binaryFormat,binary,length)=>{GL.recordError(1280)};var _emscripten_glProgramParameteri=(program,pname,value)=>{GL.recordError(1280)};var _emscripten_glQueryCounterEXT=(id,target)=>{GLctx.disjointTimerQueryExt["queryCounterEXT"](GL.queries[id],target)};var _emscripten_glReadBuffer=x0=>GLctx.readBuffer(x0);var computeUnpackAlignedImageSize=(width,height,sizePerPixel)=>{function roundedToNextMultipleOf(x,y){return x+y-1&-y}var plainRowSize=(GL.unpackRowLength||width)*sizePerPixel;var alignedRowSize=roundedToNextMultipleOf(plainRowSize,GL.unpackAlignment);return height*alignedRowSize};var colorChannelsInGlTextureFormat=format=>{var colorChannels={5:3,6:4,8:2,29502:3,29504:4,26917:2,26918:2,29846:3,29847:4};return colorChannels[format-6402]||1};var heapObjectForWebGLType=type=>{type-=5120;if(type==0)return HEAP8;if(type==1)return HEAPU8;if(type==2)return HEAP16;if(type==4)return HEAP32;if(type==6)return HEAPF32;if(type==5||type==28922||type==28520||type==30779||type==30782)return HEAPU32;return HEAPU16};var toTypedArrayIndex=(pointer,heap)=>pointer>>>31-Math.clz32(heap.BYTES_PER_ELEMENT);var emscriptenWebGLGetTexPixelData=(type,format,width,height,pixels,internalFormat)=>{var heap=heapObjectForWebGLType(type);var sizePerPixel=colorChannelsInGlTextureFormat(format)*heap.BYTES_PER_ELEMENT;var bytes=computeUnpackAlignedImageSize(width,height,sizePerPixel);return heap.subarray(toTypedArrayIndex(pixels,heap),toTypedArrayIndex(pixels+bytes,heap))};var _emscripten_glReadPixels=(x,y,width,height,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelPackBufferBinding){GLctx.readPixels(x,y,width,height,format,type,pixels);return}var heap=heapObjectForWebGLType(type);var target=toTypedArrayIndex(pixels,heap);GLctx.readPixels(x,y,width,height,format,type,heap,target);return}var pixelData=emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,format);if(!pixelData){GL.recordError(1280);return}GLctx.readPixels(x,y,width,height,format,type,pixelData)};var _emscripten_glReleaseShaderCompiler=()=>{};var _emscripten_glRenderbufferStorage=(x0,x1,x2,x3)=>GLctx.renderbufferStorage(x0,x1,x2,x3);var _emscripten_glRenderbufferStorageMultisample=(x0,x1,x2,x3,x4)=>GLctx.renderbufferStorageMultisample(x0,x1,x2,x3,x4);var _emscripten_glResumeTransformFeedback=()=>GLctx.resumeTransformFeedback();var _emscripten_glSampleCoverage=(value,invert)=>{GLctx.sampleCoverage(value,!!invert)};var _emscripten_glSamplerParameterf=(sampler,pname,param)=>{GLctx.samplerParameterf(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameterfv=(sampler,pname,params)=>{var param=HEAPF32[params>>2];GLctx.samplerParameterf(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameteri=(sampler,pname,param)=>{GLctx.samplerParameteri(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameteriv=(sampler,pname,params)=>{var param=HEAP32[params>>2];GLctx.samplerParameteri(GL.samplers[sampler],pname,param)};var _emscripten_glScissor=(x0,x1,x2,x3)=>GLctx.scissor(x0,x1,x2,x3);var _emscripten_glShaderBinary=(count,shaders,binaryformat,binary,length)=>{GL.recordError(1280)};var _emscripten_glShaderSource=(shader,count,string,length)=>{var source=GL.getSource(shader,count,string,length);GLctx.shaderSource(GL.shaders[shader],source)};var _emscripten_glStencilFunc=(x0,x1,x2)=>GLctx.stencilFunc(x0,x1,x2);var _emscripten_glStencilFuncSeparate=(x0,x1,x2,x3)=>GLctx.stencilFuncSeparate(x0,x1,x2,x3);var _emscripten_glStencilMask=x0=>GLctx.stencilMask(x0);var _emscripten_glStencilMaskSeparate=(x0,x1)=>GLctx.stencilMaskSeparate(x0,x1);var _emscripten_glStencilOp=(x0,x1,x2)=>GLctx.stencilOp(x0,x1,x2);var _emscripten_glStencilOpSeparate=(x0,x1,x2,x3)=>GLctx.stencilOpSeparate(x0,x1,x2,x3);var _emscripten_glTexImage2D=(target,level,internalFormat,width,height,border,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);var index=toTypedArrayIndex(pixels,heap);GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,heap,index);return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,internalFormat):null;GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixelData)};var _emscripten_glTexImage3D=(target,level,internalFormat,width,height,depth,border,format,type,pixels)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,pixels)}else if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,heap,toTypedArrayIndex(pixels,heap))}else{GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,null)}};var _emscripten_glTexParameterf=(x0,x1,x2)=>GLctx.texParameterf(x0,x1,x2);var _emscripten_glTexParameterfv=(target,pname,params)=>{var param=HEAPF32[params>>2];GLctx.texParameterf(target,pname,param)};var _emscripten_glTexParameteri=(x0,x1,x2)=>GLctx.texParameteri(x0,x1,x2);var _emscripten_glTexParameteriv=(target,pname,params)=>{var param=HEAP32[params>>2];GLctx.texParameteri(target,pname,param)};var _emscripten_glTexStorage2D=(x0,x1,x2,x3,x4)=>GLctx.texStorage2D(x0,x1,x2,x3,x4);var _emscripten_glTexStorage3D=(x0,x1,x2,x3,x4,x5)=>GLctx.texStorage3D(x0,x1,x2,x3,x4,x5);var _emscripten_glTexSubImage2D=(target,level,xoffset,yoffset,width,height,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,heap,toTypedArrayIndex(pixels,heap));return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,0):null;GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixelData)};var _emscripten_glTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)}else if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,heap,toTypedArrayIndex(pixels,heap))}else{GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,null)}};var _emscripten_glTransformFeedbackVaryings=(program,count,varyings,bufferMode)=>{program=GL.programs[program];var vars=[];for(var i=0;i>2]));GLctx.transformFeedbackVaryings(program,vars,bufferMode)};var _emscripten_glUniform1f=(location,v0)=>{GLctx.uniform1f(webglGetUniformLocation(location),v0)};var miniTempWebGLFloatBuffers=[];var _emscripten_glUniform1fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform1fv(webglGetUniformLocation(location),HEAPF32,value>>2,count);return}if(count<=288){var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*4>>2)}GLctx.uniform1fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform1i=(location,v0)=>{GLctx.uniform1i(webglGetUniformLocation(location),v0)};var miniTempWebGLIntBuffers=[];var _emscripten_glUniform1iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform1iv(webglGetUniformLocation(location),HEAP32,value>>2,count);return}if(count<=288){var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2]}}else{var view=HEAP32.subarray(value>>2,value+count*4>>2)}GLctx.uniform1iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform1ui=(location,v0)=>{GLctx.uniform1ui(webglGetUniformLocation(location),v0)};var _emscripten_glUniform1uiv=(location,count,value)=>{count&&GLctx.uniform1uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count)};var _emscripten_glUniform2f=(location,v0,v1)=>{GLctx.uniform2f(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform2fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*2);return}if(count<=144){count*=2;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*8>>2)}GLctx.uniform2fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform2i=(location,v0,v1)=>{GLctx.uniform2i(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform2iv(webglGetUniformLocation(location),HEAP32,value>>2,count*2);return}if(count<=144){count*=2;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*8>>2)}GLctx.uniform2iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform2ui=(location,v0,v1)=>{GLctx.uniform2ui(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2uiv=(location,count,value)=>{count&&GLctx.uniform2uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*2)};var _emscripten_glUniform3f=(location,v0,v1,v2)=>{GLctx.uniform3f(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform3fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*3);return}if(count<=96){count*=3;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*12>>2)}GLctx.uniform3fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform3i=(location,v0,v1,v2)=>{GLctx.uniform3i(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform3iv(webglGetUniformLocation(location),HEAP32,value>>2,count*3);return}if(count<=96){count*=3;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2];view[i+2]=HEAP32[value+(4*i+8)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*12>>2)}GLctx.uniform3iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform3ui=(location,v0,v1,v2)=>{GLctx.uniform3ui(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3uiv=(location,count,value)=>{count&&GLctx.uniform3uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*3)};var _emscripten_glUniform4f=(location,v0,v1,v2,v3)=>{GLctx.uniform4f(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform4fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*4);return}if(count<=72){var view=miniTempWebGLFloatBuffers[4*count];var heap=HEAPF32;value=value>>2;count*=4;for(var i=0;i>2,value+count*16>>2)}GLctx.uniform4fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform4i=(location,v0,v1,v2,v3)=>{GLctx.uniform4i(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform4iv(webglGetUniformLocation(location),HEAP32,value>>2,count*4);return}if(count<=72){count*=4;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2];view[i+2]=HEAP32[value+(4*i+8)>>2];view[i+3]=HEAP32[value+(4*i+12)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*16>>2)}GLctx.uniform4iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform4ui=(location,v0,v1,v2,v3)=>{GLctx.uniform4ui(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4uiv=(location,count,value)=>{count&&GLctx.uniform4uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*4)};var _emscripten_glUniformBlockBinding=(program,uniformBlockIndex,uniformBlockBinding)=>{program=GL.programs[program];GLctx.uniformBlockBinding(program,uniformBlockIndex,uniformBlockBinding)};var _emscripten_glUniformMatrix2fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*4);return}if(count<=72){count*=4;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2];view[i+3]=HEAPF32[value+(4*i+12)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*16>>2)}GLctx.uniformMatrix2fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix2x3fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix2x3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*6)};var _emscripten_glUniformMatrix2x4fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix2x4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*8)};var _emscripten_glUniformMatrix3fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*9);return}if(count<=32){count*=9;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2];view[i+3]=HEAPF32[value+(4*i+12)>>2];view[i+4]=HEAPF32[value+(4*i+16)>>2];view[i+5]=HEAPF32[value+(4*i+20)>>2];view[i+6]=HEAPF32[value+(4*i+24)>>2];view[i+7]=HEAPF32[value+(4*i+28)>>2];view[i+8]=HEAPF32[value+(4*i+32)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*36>>2)}GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix3x2fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix3x2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*6)};var _emscripten_glUniformMatrix3x4fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix3x4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*12)};var _emscripten_glUniformMatrix4fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*16);return}if(count<=18){var view=miniTempWebGLFloatBuffers[16*count];var heap=HEAPF32;value=value>>2;count*=16;for(var i=0;i>2,value+count*64>>2)}GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix4x2fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix4x2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*8)};var _emscripten_glUniformMatrix4x3fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix4x3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*12)};var _emscripten_glUseProgram=program=>{program=GL.programs[program];GLctx.useProgram(program);GLctx.currentProgram=program};var _emscripten_glValidateProgram=program=>{GLctx.validateProgram(GL.programs[program])};var _emscripten_glVertexAttrib1f=(x0,x1)=>GLctx.vertexAttrib1f(x0,x1);var _emscripten_glVertexAttrib1fv=(index,v)=>{GLctx.vertexAttrib1f(index,HEAPF32[v>>2])};var _emscripten_glVertexAttrib2f=(x0,x1,x2)=>GLctx.vertexAttrib2f(x0,x1,x2);var _emscripten_glVertexAttrib2fv=(index,v)=>{GLctx.vertexAttrib2f(index,HEAPF32[v>>2],HEAPF32[v+4>>2])};var _emscripten_glVertexAttrib3f=(x0,x1,x2,x3)=>GLctx.vertexAttrib3f(x0,x1,x2,x3);var _emscripten_glVertexAttrib3fv=(index,v)=>{GLctx.vertexAttrib3f(index,HEAPF32[v>>2],HEAPF32[v+4>>2],HEAPF32[v+8>>2])};var _emscripten_glVertexAttrib4f=(x0,x1,x2,x3,x4)=>GLctx.vertexAttrib4f(x0,x1,x2,x3,x4);var _emscripten_glVertexAttrib4fv=(index,v)=>{GLctx.vertexAttrib4f(index,HEAPF32[v>>2],HEAPF32[v+4>>2],HEAPF32[v+8>>2],HEAPF32[v+12>>2])};var _emscripten_glVertexAttribDivisor=(index,divisor)=>{GLctx.vertexAttribDivisor(index,divisor)};var _glVertexAttribDivisor=_emscripten_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorANGLE=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorARB=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorEXT=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorNV=_glVertexAttribDivisor;var _emscripten_glVertexAttribI4i=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4i(x0,x1,x2,x3,x4);var _emscripten_glVertexAttribI4iv=(index,v)=>{GLctx.vertexAttribI4i(index,HEAP32[v>>2],HEAP32[v+4>>2],HEAP32[v+8>>2],HEAP32[v+12>>2])};var _emscripten_glVertexAttribI4ui=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4ui(x0,x1,x2,x3,x4);var _emscripten_glVertexAttribI4uiv=(index,v)=>{GLctx.vertexAttribI4ui(index,HEAPU32[v>>2],HEAPU32[v+4>>2],HEAPU32[v+8>>2],HEAPU32[v+12>>2])};var _emscripten_glVertexAttribIPointer=(index,size,type,stride,ptr)=>{GLctx.vertexAttribIPointer(index,size,type,stride,ptr)};var _emscripten_glVertexAttribPointer=(index,size,type,normalized,stride,ptr)=>{GLctx.vertexAttribPointer(index,size,type,!!normalized,stride,ptr)};var _emscripten_glViewport=(x0,x1,x2,x3)=>GLctx.viewport(x0,x1,x2,x3);var _emscripten_glWaitSync=(sync,flags,timeout)=>{timeout=Number(timeout);GLctx.waitSync(GL.syncs[sync],flags,timeout)};var wasmTableMirror=[];var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var _emscripten_request_animation_frame_loop=(cb,userData)=>{function tick(timeStamp){if(getWasmTableEntry(cb)(timeStamp,userData)){requestAnimationFrame(tick)}}return requestAnimationFrame(tick)};var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var ENV={};var getExecutableName=()=>thisProgram||"./this.program";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(globalThis.navigator?.language??"C").replace("-","_")+".UTF-8";var env={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:lang,_:getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var _environ_get=(__environ,environ_buf)=>{var bufSize=0;var envp=0;for(var string of getEnvStrings()){var ptr=environ_buf+bufSize;HEAPU32[__environ+envp>>2]=ptr;bufSize+=stringToUTF8(string,ptr,Infinity)+1;envp+=4}return 0};var _environ_sizes_get=(penviron_count,penviron_buf_size)=>{var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;var bufSize=0;for(var string of strings){bufSize+=lengthBytesUTF8(string)+1}HEAPU32[penviron_buf_size>>2]=bufSize;return 0};var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doReadv=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_read(fd,iov,iovcnt,pnum){try{var stream=SYSCALLS.getStreamFromFD(fd);var num=doReadv(stream,iov,iovcnt);HEAPU32[pnum>>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_seek(fd,offset,whence,newOffset){offset=bigintToI53Checked(offset);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);HEAP64[newOffset>>3]=BigInt(stream.position);if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doWritev=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var _glGetIntegerv=_emscripten_glGetIntegerv;var _glGetString=_emscripten_glGetString;var _glGetStringi=_emscripten_glGetStringi;var _llvm_eh_typeid_for=type=>type;function _random_get(buffer,size){try{randomFill(HEAPU8.subarray(buffer,buffer+size));return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};FS.createPreloadedFile=FS_createPreloadedFile;FS.preloadFile=FS_preloadFile;FS.staticInit();for(let i=0;i<32;++i)tempFixedLengthArray.push(new Array(i));var miniTempWebGLFloatBuffersStorage=new Float32Array(288);for(var i=0;i<=288;++i){miniTempWebGLFloatBuffers[i]=miniTempWebGLFloatBuffersStorage.subarray(0,i)}var miniTempWebGLIntBuffersStorage=new Int32Array(288);for(var i=0;i<=288;++i){miniTempWebGLIntBuffers[i]=miniTempWebGLIntBuffersStorage.subarray(0,i)}{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["preloadPlugins"])preloadPlugins=Module["preloadPlugins"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}Module["UTF8ToString"]=UTF8ToString;Module["stringToUTF8"]=stringToUTF8;Module["lengthBytesUTF8"]=lengthBytesUTF8;Module["GL"]=GL;var _malloc,_add_font,_add_image,_add_image_with_rid,_allocate,_apply_scene_transactions,_command,_deallocate,_destroy,_devtools_rendering_set_show_fps_meter,_devtools_rendering_set_show_hit_testing,_devtools_rendering_set_show_ruler,_devtools_rendering_set_show_stats,_devtools_rendering_set_show_tiles,_drain_missing_images,_export_node_as,_get_default_fallback_fonts,_get_image_bytes,_get_image_size,_get_node_absolute_bounding_box,_get_node_id_from_point,_get_node_ids_from_envelope,_get_node_ids_from_point,_grida_fonts_analyze_family,_grida_fonts_free,_grida_fonts_parse_font,_grida_markdown_to_html,_grida_svg_optimize,_grida_svg_to_document,_has_missing_fonts,_highlight_strokes,_init,_init_with_backend,_list_available_fonts,_list_missing_fonts,_load_benchmark_scene,_load_dummy_scene,_load_scene_grida,_load_scene_grida1,_loaded_scene_ids,_pointer_move,_redraw,_resize_surface,_resolve_image,_runtime_renderer_set_layer_compositing,_runtime_renderer_set_outline_mode,_runtime_renderer_set_pixel_preview_scale,_runtime_renderer_set_pixel_preview_stable,_runtime_renderer_set_render_policy_flags,_runtime_renderer_set_skip_layout,_set_debug,_set_default_fallback_fonts,_set_main_camera_transform,_set_surface_overlay_config,_set_verbose,_surface_get_cursor,_surface_get_hovered_node,_surface_get_selected_nodes,_surface_pointer_down,_surface_pointer_move,_surface_pointer_up,_surface_set_selection,_switch_scene,_text_edit_command,_text_edit_enter,_text_edit_exit,_text_edit_get_caret_rect,_text_edit_get_selected_html,_text_edit_get_selected_text,_text_edit_get_selection_rects,_text_edit_get_text,_text_edit_ime_cancel,_text_edit_ime_commit,_text_edit_ime_set_preedit,_text_edit_is_active,_text_edit_paste_html,_text_edit_paste_text,_text_edit_pointer_down,_text_edit_pointer_move,_text_edit_pointer_up,_text_edit_redo,_text_edit_set_color,_text_edit_set_font_family,_text_edit_set_font_size,_text_edit_tick,_text_edit_toggle_bold,_text_edit_toggle_italic,_text_edit_toggle_strikethrough,_text_edit_toggle_underline,_text_edit_undo,_tick,_to_vector_network,_toggle_debug,_main,_emscripten_builtin_memalign,_setThrew,__emscripten_tempret_set,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,___cxa_decrement_exception_refcount,___cxa_increment_exception_refcount,___cxa_can_catch,___cxa_get_exception_ptr,memory,__indirect_function_table,wasmMemory,wasmTable;function assignWasmExports(wasmExports){_malloc=wasmExports["Ug"];_add_font=Module["_add_font"]=wasmExports["Wg"];_add_image=Module["_add_image"]=wasmExports["Xg"];_add_image_with_rid=Module["_add_image_with_rid"]=wasmExports["Yg"];_allocate=Module["_allocate"]=wasmExports["Zg"];_apply_scene_transactions=Module["_apply_scene_transactions"]=wasmExports["_g"];_command=Module["_command"]=wasmExports["$g"];_deallocate=Module["_deallocate"]=wasmExports["ah"];_destroy=Module["_destroy"]=wasmExports["bh"];_devtools_rendering_set_show_fps_meter=Module["_devtools_rendering_set_show_fps_meter"]=wasmExports["ch"];_devtools_rendering_set_show_hit_testing=Module["_devtools_rendering_set_show_hit_testing"]=wasmExports["dh"];_devtools_rendering_set_show_ruler=Module["_devtools_rendering_set_show_ruler"]=wasmExports["eh"];_devtools_rendering_set_show_stats=Module["_devtools_rendering_set_show_stats"]=wasmExports["fh"];_devtools_rendering_set_show_tiles=Module["_devtools_rendering_set_show_tiles"]=wasmExports["gh"];_drain_missing_images=Module["_drain_missing_images"]=wasmExports["hh"];_export_node_as=Module["_export_node_as"]=wasmExports["ih"];_get_default_fallback_fonts=Module["_get_default_fallback_fonts"]=wasmExports["jh"];_get_image_bytes=Module["_get_image_bytes"]=wasmExports["kh"];_get_image_size=Module["_get_image_size"]=wasmExports["lh"];_get_node_absolute_bounding_box=Module["_get_node_absolute_bounding_box"]=wasmExports["mh"];_get_node_id_from_point=Module["_get_node_id_from_point"]=wasmExports["nh"];_get_node_ids_from_envelope=Module["_get_node_ids_from_envelope"]=wasmExports["oh"];_get_node_ids_from_point=Module["_get_node_ids_from_point"]=wasmExports["ph"];_grida_fonts_analyze_family=Module["_grida_fonts_analyze_family"]=wasmExports["qh"];_grida_fonts_free=Module["_grida_fonts_free"]=wasmExports["rh"];_grida_fonts_parse_font=Module["_grida_fonts_parse_font"]=wasmExports["sh"];_grida_markdown_to_html=Module["_grida_markdown_to_html"]=wasmExports["th"];_grida_svg_optimize=Module["_grida_svg_optimize"]=wasmExports["uh"];_grida_svg_to_document=Module["_grida_svg_to_document"]=wasmExports["vh"];_has_missing_fonts=Module["_has_missing_fonts"]=wasmExports["wh"];_highlight_strokes=Module["_highlight_strokes"]=wasmExports["xh"];_init=Module["_init"]=wasmExports["yh"];_init_with_backend=Module["_init_with_backend"]=wasmExports["zh"];_list_available_fonts=Module["_list_available_fonts"]=wasmExports["Ah"];_list_missing_fonts=Module["_list_missing_fonts"]=wasmExports["Bh"];_load_benchmark_scene=Module["_load_benchmark_scene"]=wasmExports["Ch"];_load_dummy_scene=Module["_load_dummy_scene"]=wasmExports["Dh"];_load_scene_grida=Module["_load_scene_grida"]=wasmExports["Eh"];_load_scene_grida1=Module["_load_scene_grida1"]=wasmExports["Fh"];_loaded_scene_ids=Module["_loaded_scene_ids"]=wasmExports["Gh"];_pointer_move=Module["_pointer_move"]=wasmExports["Hh"];_redraw=Module["_redraw"]=wasmExports["Ih"];_resize_surface=Module["_resize_surface"]=wasmExports["Jh"];_resolve_image=Module["_resolve_image"]=wasmExports["Kh"];_runtime_renderer_set_layer_compositing=Module["_runtime_renderer_set_layer_compositing"]=wasmExports["Lh"];_runtime_renderer_set_outline_mode=Module["_runtime_renderer_set_outline_mode"]=wasmExports["Mh"];_runtime_renderer_set_pixel_preview_scale=Module["_runtime_renderer_set_pixel_preview_scale"]=wasmExports["Nh"];_runtime_renderer_set_pixel_preview_stable=Module["_runtime_renderer_set_pixel_preview_stable"]=wasmExports["Oh"];_runtime_renderer_set_render_policy_flags=Module["_runtime_renderer_set_render_policy_flags"]=wasmExports["Ph"];_runtime_renderer_set_skip_layout=Module["_runtime_renderer_set_skip_layout"]=wasmExports["Qh"];_set_debug=Module["_set_debug"]=wasmExports["Rh"];_set_default_fallback_fonts=Module["_set_default_fallback_fonts"]=wasmExports["Sh"];_set_main_camera_transform=Module["_set_main_camera_transform"]=wasmExports["Th"];_set_surface_overlay_config=Module["_set_surface_overlay_config"]=wasmExports["Uh"];_set_verbose=Module["_set_verbose"]=wasmExports["Vh"];_surface_get_cursor=Module["_surface_get_cursor"]=wasmExports["Wh"];_surface_get_hovered_node=Module["_surface_get_hovered_node"]=wasmExports["Xh"];_surface_get_selected_nodes=Module["_surface_get_selected_nodes"]=wasmExports["Yh"];_surface_pointer_down=Module["_surface_pointer_down"]=wasmExports["Zh"];_surface_pointer_move=Module["_surface_pointer_move"]=wasmExports["_h"];_surface_pointer_up=Module["_surface_pointer_up"]=wasmExports["$h"];_surface_set_selection=Module["_surface_set_selection"]=wasmExports["ai"];_switch_scene=Module["_switch_scene"]=wasmExports["bi"];_text_edit_command=Module["_text_edit_command"]=wasmExports["ci"];_text_edit_enter=Module["_text_edit_enter"]=wasmExports["di"];_text_edit_exit=Module["_text_edit_exit"]=wasmExports["ei"];_text_edit_get_caret_rect=Module["_text_edit_get_caret_rect"]=wasmExports["fi"];_text_edit_get_selected_html=Module["_text_edit_get_selected_html"]=wasmExports["gi"];_text_edit_get_selected_text=Module["_text_edit_get_selected_text"]=wasmExports["hi"];_text_edit_get_selection_rects=Module["_text_edit_get_selection_rects"]=wasmExports["ii"];_text_edit_get_text=Module["_text_edit_get_text"]=wasmExports["ji"];_text_edit_ime_cancel=Module["_text_edit_ime_cancel"]=wasmExports["ki"];_text_edit_ime_commit=Module["_text_edit_ime_commit"]=wasmExports["li"];_text_edit_ime_set_preedit=Module["_text_edit_ime_set_preedit"]=wasmExports["mi"];_text_edit_is_active=Module["_text_edit_is_active"]=wasmExports["ni"];_text_edit_paste_html=Module["_text_edit_paste_html"]=wasmExports["oi"];_text_edit_paste_text=Module["_text_edit_paste_text"]=wasmExports["pi"];_text_edit_pointer_down=Module["_text_edit_pointer_down"]=wasmExports["qi"];_text_edit_pointer_move=Module["_text_edit_pointer_move"]=wasmExports["ri"];_text_edit_pointer_up=Module["_text_edit_pointer_up"]=wasmExports["si"];_text_edit_redo=Module["_text_edit_redo"]=wasmExports["ti"];_text_edit_set_color=Module["_text_edit_set_color"]=wasmExports["ui"];_text_edit_set_font_family=Module["_text_edit_set_font_family"]=wasmExports["vi"];_text_edit_set_font_size=Module["_text_edit_set_font_size"]=wasmExports["wi"];_text_edit_tick=Module["_text_edit_tick"]=wasmExports["xi"];_text_edit_toggle_bold=Module["_text_edit_toggle_bold"]=wasmExports["yi"];_text_edit_toggle_italic=Module["_text_edit_toggle_italic"]=wasmExports["zi"];_text_edit_toggle_strikethrough=Module["_text_edit_toggle_strikethrough"]=wasmExports["Ai"];_text_edit_toggle_underline=Module["_text_edit_toggle_underline"]=wasmExports["Bi"];_text_edit_undo=Module["_text_edit_undo"]=wasmExports["Ci"];_tick=Module["_tick"]=wasmExports["Di"];_to_vector_network=Module["_to_vector_network"]=wasmExports["Ei"];_toggle_debug=Module["_toggle_debug"]=wasmExports["Fi"];_main=Module["_main"]=wasmExports["Gi"];_emscripten_builtin_memalign=wasmExports["Hi"];_setThrew=wasmExports["Ii"];__emscripten_tempret_set=wasmExports["Ji"];__emscripten_stack_restore=wasmExports["Ki"];__emscripten_stack_alloc=wasmExports["Li"];_emscripten_stack_get_current=wasmExports["Mi"];___cxa_decrement_exception_refcount=wasmExports["Ni"];___cxa_increment_exception_refcount=wasmExports["Oi"];___cxa_can_catch=wasmExports["Pi"];___cxa_get_exception_ptr=wasmExports["Qi"];memory=wasmMemory=wasmExports["Sg"];__indirect_function_table=wasmTable=wasmExports["Vg"]}var wasmImports={G:___cxa_begin_catch,P:___cxa_end_catch,a:___cxa_find_matching_catch_2,n:___cxa_find_matching_catch_3,ja:___cxa_find_matching_catch_4,Ea:___cxa_rethrow,J:___cxa_throw,db:___cxa_uncaught_exceptions,e:___resumeException,Ha:___syscall_fcntl64,vb:___syscall_fstat64,rb:___syscall_getcwd,wb:___syscall_ioctl,sb:___syscall_lstat64,tb:___syscall_newfstatat,Ja:___syscall_openat,ub:___syscall_stat64,zb:__abort_js,fb:__emscripten_throw_longjmp,lb:__gmtime_js,jb:__mmap_js,kb:__munmap_js,Ab:__tzset_js,yb:_clock_time_get,xb:_emscripten_date_now,hb:_emscripten_get_heap_max,Af:_emscripten_glActiveTexture,Bf:_emscripten_glAttachShader,de:_emscripten_glBeginQuery,Zd:_emscripten_glBeginQueryEXT,Ec:_emscripten_glBeginTransformFeedback,Cf:_emscripten_glBindAttribLocation,Df:_emscripten_glBindBuffer,Bc:_emscripten_glBindBufferBase,Cc:_emscripten_glBindBufferRange,Be:_emscripten_glBindFramebuffer,Ce:_emscripten_glBindRenderbuffer,je:_emscripten_glBindSampler,Ef:_emscripten_glBindTexture,Sb:_emscripten_glBindTransformFeedback,Xe:_emscripten_glBindVertexArray,_e:_emscripten_glBindVertexArrayOES,Ff:_emscripten_glBlendColor,Gf:_emscripten_glBlendEquation,Jd:_emscripten_glBlendEquationSeparate,Hf:_emscripten_glBlendFunc,Id:_emscripten_glBlendFuncSeparate,ve:_emscripten_glBlitFramebuffer,If:_emscripten_glBufferData,Jf:_emscripten_glBufferSubData,De:_emscripten_glCheckFramebufferStatus,Kf:_emscripten_glClear,fc:_emscripten_glClearBufferfi,gc:_emscripten_glClearBufferfv,ic:_emscripten_glClearBufferiv,hc:_emscripten_glClearBufferuiv,Lf:_emscripten_glClearColor,Hd:_emscripten_glClearDepthf,Mf:_emscripten_glClearStencil,se:_emscripten_glClientWaitSync,_c:_emscripten_glClipControlEXT,Nf:_emscripten_glColorMask,Of:_emscripten_glCompileShader,Pf:_emscripten_glCompressedTexImage2D,Rc:_emscripten_glCompressedTexImage3D,Qf:_emscripten_glCompressedTexSubImage2D,Qc:_emscripten_glCompressedTexSubImage3D,ue:_emscripten_glCopyBufferSubData,Gd:_emscripten_glCopyTexImage2D,Rf:_emscripten_glCopyTexSubImage2D,Sc:_emscripten_glCopyTexSubImage3D,Sf:_emscripten_glCreateProgram,Tf:_emscripten_glCreateShader,Uf:_emscripten_glCullFace,Vf:_emscripten_glDeleteBuffers,Ee:_emscripten_glDeleteFramebuffers,Xf:_emscripten_glDeleteProgram,ee:_emscripten_glDeleteQueries,_d:_emscripten_glDeleteQueriesEXT,Fe:_emscripten_glDeleteRenderbuffers,ke:_emscripten_glDeleteSamplers,Yf:_emscripten_glDeleteShader,te:_emscripten_glDeleteSync,Zf:_emscripten_glDeleteTextures,Rb:_emscripten_glDeleteTransformFeedbacks,Ye:_emscripten_glDeleteVertexArrays,$e:_emscripten_glDeleteVertexArraysOES,Fd:_emscripten_glDepthFunc,_f:_emscripten_glDepthMask,Ed:_emscripten_glDepthRangef,Dd:_emscripten_glDetachShader,$f:_emscripten_glDisable,ag:_emscripten_glDisableVertexAttribArray,bg:_emscripten_glDrawArrays,Ve:_emscripten_glDrawArraysInstanced,Md:_emscripten_glDrawArraysInstancedANGLE,Db:_emscripten_glDrawArraysInstancedARB,Se:_emscripten_glDrawArraysInstancedBaseInstanceWEBGL,Xc:_emscripten_glDrawArraysInstancedEXT,Eb:_emscripten_glDrawArraysInstancedNV,Qe:_emscripten_glDrawBuffers,Vc:_emscripten_glDrawBuffersEXT,Nd:_emscripten_glDrawBuffersWEBGL,cg:_emscripten_glDrawElements,We:_emscripten_glDrawElementsInstanced,Ld:_emscripten_glDrawElementsInstancedANGLE,Bb:_emscripten_glDrawElementsInstancedARB,Te:_emscripten_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Cb:_emscripten_glDrawElementsInstancedEXT,Wc:_emscripten_glDrawElementsInstancedNV,Ke:_emscripten_glDrawRangeElements,dg:_emscripten_glEnable,eg:_emscripten_glEnableVertexAttribArray,fe:_emscripten_glEndQuery,$d:_emscripten_glEndQueryEXT,Dc:_emscripten_glEndTransformFeedback,pe:_emscripten_glFenceSync,fg:_emscripten_glFinish,gg:_emscripten_glFlush,Ge:_emscripten_glFramebufferRenderbuffer,He:_emscripten_glFramebufferTexture2D,Hc:_emscripten_glFramebufferTextureLayer,hg:_emscripten_glFrontFace,ig:_emscripten_glGenBuffers,Ie:_emscripten_glGenFramebuffers,ge:_emscripten_glGenQueries,ae:_emscripten_glGenQueriesEXT,Je:_emscripten_glGenRenderbuffers,le:_emscripten_glGenSamplers,jg:_emscripten_glGenTextures,Qb:_emscripten_glGenTransformFeedbacks,Ue:_emscripten_glGenVertexArrays,af:_emscripten_glGenVertexArraysOES,xe:_emscripten_glGenerateMipmap,Cd:_emscripten_glGetActiveAttrib,Bd:_emscripten_glGetActiveUniform,ac:_emscripten_glGetActiveUniformBlockName,bc:_emscripten_glGetActiveUniformBlockiv,dc:_emscripten_glGetActiveUniformsiv,Ad:_emscripten_glGetAttachedShaders,zd:_emscripten_glGetAttribLocation,yd:_emscripten_glGetBooleanv,Xb:_emscripten_glGetBufferParameteri64v,kg:_emscripten_glGetBufferParameteriv,lg:_emscripten_glGetError,mg:_emscripten_glGetFloatv,rc:_emscripten_glGetFragDataLocation,ye:_emscripten_glGetFramebufferAttachmentParameteriv,Yb:_emscripten_glGetInteger64i_v,_b:_emscripten_glGetInteger64v,Fc:_emscripten_glGetIntegeri_v,ng:_emscripten_glGetIntegerv,Ib:_emscripten_glGetInternalformativ,Mb:_emscripten_glGetProgramBinary,og:_emscripten_glGetProgramInfoLog,pg:_emscripten_glGetProgramiv,Wd:_emscripten_glGetQueryObjecti64vEXT,Pd:_emscripten_glGetQueryObjectivEXT,Xd:_emscripten_glGetQueryObjectui64vEXT,he:_emscripten_glGetQueryObjectuiv,be:_emscripten_glGetQueryObjectuivEXT,ie:_emscripten_glGetQueryiv,ce:_emscripten_glGetQueryivEXT,ze:_emscripten_glGetRenderbufferParameteriv,Tb:_emscripten_glGetSamplerParameterfv,Ub:_emscripten_glGetSamplerParameteriv,qg:_emscripten_glGetShaderInfoLog,Td:_emscripten_glGetShaderPrecisionFormat,xd:_emscripten_glGetShaderSource,rg:_emscripten_glGetShaderiv,sg:_emscripten_glGetString,Ze:_emscripten_glGetStringi,Zb:_emscripten_glGetSynciv,wd:_emscripten_glGetTexParameterfv,vd:_emscripten_glGetTexParameteriv,zc:_emscripten_glGetTransformFeedbackVarying,cc:_emscripten_glGetUniformBlockIndex,ec:_emscripten_glGetUniformIndices,tg:_emscripten_glGetUniformLocation,ud:_emscripten_glGetUniformfv,td:_emscripten_glGetUniformiv,sc:_emscripten_glGetUniformuiv,yc:_emscripten_glGetVertexAttribIiv,xc:_emscripten_glGetVertexAttribIuiv,qd:_emscripten_glGetVertexAttribPointerv,sd:_emscripten_glGetVertexAttribfv,rd:_emscripten_glGetVertexAttribiv,pd:_emscripten_glHint,Ud:_emscripten_glInvalidateFramebuffer,Vd:_emscripten_glInvalidateSubFramebuffer,od:_emscripten_glIsBuffer,nd:_emscripten_glIsEnabled,md:_emscripten_glIsFramebuffer,ld:_emscripten_glIsProgram,Pc:_emscripten_glIsQuery,Qd:_emscripten_glIsQueryEXT,kd:_emscripten_glIsRenderbuffer,Wb:_emscripten_glIsSampler,jd:_emscripten_glIsShader,qe:_emscripten_glIsSync,ug:_emscripten_glIsTexture,Pb:_emscripten_glIsTransformFeedback,Gc:_emscripten_glIsVertexArray,Od:_emscripten_glIsVertexArrayOES,vg:_emscripten_glLineWidth,wg:_emscripten_glLinkProgram,Oe:_emscripten_glMultiDrawArraysInstancedBaseInstanceWEBGL,Pe:_emscripten_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Ob:_emscripten_glPauseTransformFeedback,xg:_emscripten_glPixelStorei,Zc:_emscripten_glPolygonModeWEBGL,id:_emscripten_glPolygonOffset,$c:_emscripten_glPolygonOffsetClampEXT,Lb:_emscripten_glProgramBinary,Kb:_emscripten_glProgramParameteri,Yd:_emscripten_glQueryCounterEXT,Re:_emscripten_glReadBuffer,yg:_emscripten_glReadPixels,hd:_emscripten_glReleaseShaderCompiler,Ae:_emscripten_glRenderbufferStorage,we:_emscripten_glRenderbufferStorageMultisample,Nb:_emscripten_glResumeTransformFeedback,gd:_emscripten_glSampleCoverage,me:_emscripten_glSamplerParameterf,Vb:_emscripten_glSamplerParameterfv,ne:_emscripten_glSamplerParameteri,oe:_emscripten_glSamplerParameteriv,zg:_emscripten_glScissor,fd:_emscripten_glShaderBinary,Ag:_emscripten_glShaderSource,Bg:_emscripten_glStencilFunc,Cg:_emscripten_glStencilFuncSeparate,Dg:_emscripten_glStencilMask,Eg:_emscripten_glStencilMaskSeparate,Fg:_emscripten_glStencilOp,Gg:_emscripten_glStencilOpSeparate,Hg:_emscripten_glTexImage2D,Uc:_emscripten_glTexImage3D,Ig:_emscripten_glTexParameterf,Jg:_emscripten_glTexParameterfv,Kg:_emscripten_glTexParameteri,Lg:_emscripten_glTexParameteriv,Le:_emscripten_glTexStorage2D,Jb:_emscripten_glTexStorage3D,Mg:_emscripten_glTexSubImage2D,Tc:_emscripten_glTexSubImage3D,Ac:_emscripten_glTransformFeedbackVaryings,Ng:_emscripten_glUniform1f,Og:_emscripten_glUniform1fv,wf:_emscripten_glUniform1i,xf:_emscripten_glUniform1iv,qc:_emscripten_glUniform1ui,mc:_emscripten_glUniform1uiv,yf:_emscripten_glUniform2f,zf:_emscripten_glUniform2fv,vf:_emscripten_glUniform2i,uf:_emscripten_glUniform2iv,pc:_emscripten_glUniform2ui,lc:_emscripten_glUniform2uiv,tf:_emscripten_glUniform3f,sf:_emscripten_glUniform3fv,rf:_emscripten_glUniform3i,qf:_emscripten_glUniform3iv,oc:_emscripten_glUniform3ui,kc:_emscripten_glUniform3uiv,pf:_emscripten_glUniform4f,of:_emscripten_glUniform4fv,bf:_emscripten_glUniform4i,cf:_emscripten_glUniform4iv,nc:_emscripten_glUniform4ui,jc:_emscripten_glUniform4uiv,$b:_emscripten_glUniformBlockBinding,df:_emscripten_glUniformMatrix2fv,Oc:_emscripten_glUniformMatrix2x3fv,Lc:_emscripten_glUniformMatrix2x4fv,ef:_emscripten_glUniformMatrix3fv,Nc:_emscripten_glUniformMatrix3x2fv,Jc:_emscripten_glUniformMatrix3x4fv,ff:_emscripten_glUniformMatrix4fv,Kc:_emscripten_glUniformMatrix4x2fv,Ic:_emscripten_glUniformMatrix4x3fv,gf:_emscripten_glUseProgram,ed:_emscripten_glValidateProgram,hf:_emscripten_glVertexAttrib1f,dd:_emscripten_glVertexAttrib1fv,cd:_emscripten_glVertexAttrib2f,jf:_emscripten_glVertexAttrib2fv,bd:_emscripten_glVertexAttrib3f,kf:_emscripten_glVertexAttrib3fv,ad:_emscripten_glVertexAttrib4f,lf:_emscripten_glVertexAttrib4fv,Me:_emscripten_glVertexAttribDivisor,Kd:_emscripten_glVertexAttribDivisorANGLE,Gb:_emscripten_glVertexAttribDivisorARB,Yc:_emscripten_glVertexAttribDivisorEXT,Hb:_emscripten_glVertexAttribDivisorNV,wc:_emscripten_glVertexAttribI4i,uc:_emscripten_glVertexAttribI4iv,vc:_emscripten_glVertexAttribI4ui,tc:_emscripten_glVertexAttribI4uiv,Ne:_emscripten_glVertexAttribIPointer,mf:_emscripten_glVertexAttribPointer,nf:_emscripten_glViewport,re:_emscripten_glWaitSync,Xa:_emscripten_request_animation_frame_loop,gb:_emscripten_resize_heap,nb:_environ_get,ob:_environ_sizes_get,Rg:_exit,qa:_fd_close,ib:_fd_pread,Ga:_fd_read,mb:_fd_seek,pa:_fd_write,Pg:_glGetIntegerv,Pa:_glGetString,Qg:_glGetStringi,Rd:invoke_dd,Sd:invoke_dddd,Ca:invoke_diii,Sa:invoke_fdiiii,Ra:invoke_fdiiiii,Qa:invoke_fii,Da:invoke_fiii,t:invoke_fiiidi,V:invoke_fiiif,v:invoke_fiiiidi,s:invoke_i,j:invoke_ii,H:invoke_iif,$a:invoke_iiffi,ua:invoke_iiffiii,f:invoke_iii,ma:invoke_iiifi,g:invoke_iiii,U:invoke_iiiiff,l:invoke_iiiii,cb:invoke_iiiiid,A:invoke_iiiiii,B:invoke_iiiiiii,F:invoke_iiiiiiii,q:invoke_iiiiiiiii,ta:invoke_iiiiiiiiii,fa:invoke_iiiiiiiiiiii,sa:invoke_iiiiiiiiiiiifiij,X:invoke_iij,eb:invoke_j,ka:invoke_ji,r:invoke_jiii,ga:invoke_jiiii,_:invoke_jiijj,L:invoke_jjji,k:invoke_v,Wf:invoke_vff,b:invoke_vi,R:invoke_vid,T:invoke_vif,w:invoke_viff,E:invoke_viffff,ba:invoke_vifffff,Ta:invoke_viffffff,D:invoke_viffi,la:invoke_viffiiiiiii,c:invoke_vii,Wa:invoke_viidii,Q:invoke_viif,u:invoke_viiff,Ma:invoke_viiffii,S:invoke_viifi,xa:invoke_viififii,z:invoke_viifiiifi,d:invoke_viii,I:invoke_viiif,za:invoke_viiiff,x:invoke_viiiffi,M:invoke_viiiffiffii,N:invoke_viiififiiiiiiiiiiii,i:invoke_viiii,Va:invoke_viiiidididii,oa:invoke_viiiif,Aa:invoke_viiiiff,Ba:invoke_viiiiffi,ya:invoke_viiiifi,h:invoke_viiiii,ab:invoke_viiiiif,Ua:invoke_viiiiiffiii,bb:invoke_viiiiifi,m:invoke_viiiiii,La:invoke_viiiiiiff,p:invoke_viiiiiii,W:invoke_viiiiiiii,Ia:invoke_viiiiiiiifij,aa:invoke_viiiiiiiii,O:invoke_viiiiiiiiii,Fb:invoke_viiiiiiiiiifij,va:invoke_viiiiiiiiiii,ea:invoke_viiiiiiiiiiiiiii,Oa:invoke_viiiiiiji,K:invoke_viiij,C:invoke_viiijii,$:invoke_viij,wa:invoke_viijffiiii,o:invoke_viiji,Ka:invoke_viijiffi,ia:invoke_viijii,Ya:invoke_viijiii,da:invoke_viijiiiif,Na:invoke_viijiiiii,na:invoke_viijj,ca:invoke_vij,Z:invoke_viji,Za:invoke_vijififi,y:invoke_vijii,Fa:invoke_vijiifi,_a:invoke_vijiififi,Y:invoke_vijiii,pb:invoke_vijijjiii,ha:invoke_vijjjj,Mc:invoke_vjii,ra:_llvm_eh_typeid_for,qb:_random_get};function invoke_vi(index,a1){var sp=stackSave();try{getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vii(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ii(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ji(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_v(index){var sp=stackSave();try{getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiij(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vff(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viij(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiji(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiff(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiijj(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiji(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vid(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viji(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiif(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiff(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiiifiij(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vif(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffi(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vjii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiffii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiff(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiffi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiifij(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiifij(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijijjiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijiifi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viifiiifi(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiffi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiifi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_i(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiififiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiiiif(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffiffii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiff(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiff(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiif(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiifi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiffi(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viififii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_vijiififi(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijififi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iij(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijj(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jjji(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiifi(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iif(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viifi(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijffiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vij(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viif(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiffiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viff(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vifffff(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiidi(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viidii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiidididii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiiidi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiffiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiijii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffff(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffffff(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiif(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fdiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fdiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_dddd(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_dd(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijjjj(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_j(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiiiid(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_fiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_diii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function callMain(args=[]){var entryFunction=_main;args.unshift(thisProgram);var argc=args.length;var argv=stackAlloc((argc+1)*4);var argv_ptr=argv;for(var arg of args){HEAPU32[argv_ptr>>2]=stringToUTF8OnStack(arg);argv_ptr+=4}HEAPU32[argv_ptr>>2]=0;try{var ret=entryFunction(argc,argv);exitJS(ret,true);return ret}catch(e){return handleException(e)}}function run(args=arguments_){if(runDependencies>0){dependenciesFulfilled=run;return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();preMain();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();var noInitialRun=Module["noInitialRun"]||false;if(!noInitialRun)callMain(args);postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} +var createGridaCanvas=(()=>{var _scriptName=globalThis.document?.currentScript?.src;return async function(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};if(typeof __filename!="undefined"){_scriptName=__filename}else{}var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var EXITSTATUS;var isFileURI=filename=>filename.startsWith("file://");var readyPromiseResolve,readyPromiseReject;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;if(!Module["noFSInit"]&&!FS.initialized)FS.init();TTY.init();wasmExports["Yg"]();FS.ignorePermissions=false}function preMain(){}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile("grida_canvas_wasm.wasm")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var exceptionCaught=[];var uncaughtExceptionCount=0;var ___cxa_begin_catch=ptr=>{var info=new ExceptionInfo(ptr);if(!info.get_caught()){info.set_caught(true);uncaughtExceptionCount--}info.set_rethrown(false);exceptionCaught.push(info);return ___cxa_get_exception_ptr(ptr)};var exceptionLast=0;var ___cxa_end_catch=()=>{_setThrew(0,0);var info=exceptionCaught.pop();___cxa_decrement_exception_refcount(info.excPtr);exceptionLast=0};class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var setTempRet0=val=>__emscripten_tempret_set(val);var findMatchingCatch=args=>{var thrown=exceptionLast;if(!thrown){setTempRet0(0);return 0}var info=new ExceptionInfo(thrown);info.set_adjusted_ptr(thrown);var thrownType=info.get_type();if(!thrownType){setTempRet0(0);return thrown}for(var caughtType of args){if(caughtType===0||caughtType===thrownType){break}var adjusted_ptr_addr=info.ptr+16;if(___cxa_can_catch(caughtType,thrownType,adjusted_ptr_addr)){setTempRet0(caughtType);return thrown}}setTempRet0(thrownType);return thrown};var ___cxa_find_matching_catch_2=()=>findMatchingCatch([]);var ___cxa_find_matching_catch_3=arg0=>findMatchingCatch([arg0]);var ___cxa_find_matching_catch_4=(arg0,arg1)=>findMatchingCatch([arg0,arg1]);var ___cxa_rethrow=()=>{var info=exceptionCaught.pop();if(!info){abort("no exception to throw")}var ptr=info.excPtr;if(!info.get_rethrown()){exceptionCaught.push(info);info.set_rethrown(true);info.set_caught(false);uncaughtExceptionCount++}___cxa_increment_exception_refcount(ptr);exceptionLast=ptr;throw exceptionLast};var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);___cxa_increment_exception_refcount(ptr);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var ___cxa_uncaught_exceptions=()=>uncaughtExceptionCount;var ___resumeException=ptr=>{if(!exceptionLast){exceptionLast=ptr}throw exceptionLast};var syscallGetVarargI=()=>{var ret=HEAP32[+SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret};var syscallGetVarargP=syscallGetVarargI;var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.slice(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.slice(0,-1)}return root+dir},basename:path=>path&&path.match(/([^\/]+|\/)\/*$/)[1],join:(...paths)=>PATH.normalize(paths.join("/")),join2:(l,r)=>PATH.normalize(l+"/"+r)};var initRandomFill=()=>{if(ENVIRONMENT_IS_NODE){var nodeCrypto=require("node:crypto");return view=>nodeCrypto.randomFillSync(view)}return view=>crypto.getRandomValues(view)};var randomFill=view=>{(randomFill=initRandomFill())(view)};var PATH_FS={resolve:(...args)=>{var resolvedPath="",resolvedAbsolute=false;for(var i=args.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?args[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).slice(1);to=PATH_FS.resolve(to).slice(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var FS_stdin_getChar_buffer=[];var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var intArrayFromString=(stringy,dontAddNull,length)=>{var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array};var FS_stdin_getChar=()=>{if(!FS_stdin_getChar_buffer.length){var result=null;if(ENVIRONMENT_IS_NODE){var BUFSIZE=256;var buf=Buffer.alloc(BUFSIZE);var bytesRead=0;var fd=process.stdin.fd;try{bytesRead=fs.readSync(fd,buf,0,BUFSIZE)}catch(e){if(e.toString().includes("EOF"))bytesRead=0;else throw e}if(bytesRead>0){result=buf.slice(0,bytesRead).toString("utf-8")}}else if(globalThis.window?.prompt){result=window.prompt("Input: ");if(result!==null){result+="\n"}}else{}if(!result){return null}FS_stdin_getChar_buffer=intArrayFromString(result,true)}return FS_stdin_getChar_buffer.shift()};var TTY={ttys:[],init(){},shutdown(){},register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close(stream){stream.tty.ops.fsync(stream.tty)},fsync(stream){stream.tty.ops.fsync(stream.tty)},read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){out(UTF8ArrayToString(tty.output));tty.output=[]}},ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(tty,optional_actions,data){return 0},ioctl_tiocgwinsz(tty){return[24,80]}},default_tty1_ops:{put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output?.length>0){err(UTF8ArrayToString(tty.output));tty.output=[]}}}};var zeroMemory=(ptr,size)=>HEAPU8.fill(0,ptr,ptr+size);var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var mmapAlloc=size=>{size=alignMemory(size,65536);var ptr=_emscripten_builtin_memalign(65536,size);if(ptr)zeroMemory(ptr,size);return ptr};var MEMFS={ops_table:null,mount(mount){return MEMFS.createNode(null,"/",16895,0)},createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}MEMFS.ops_table||={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}};var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.atime=node.mtime=node.ctime=Date.now();if(parent){parent.contents[name]=node;parent.atime=parent.mtime=parent.ctime=node.atime}return node},getFileDataAsTypedArray(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.atime);attr.mtime=new Date(node.mtime);attr.ctime=new Date(node.ctime);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){for(const key of["mode","atime","mtime","ctime"]){if(attr[key]!=null){node[key]=attr[key]}}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){if(!MEMFS.doesNotExistError){MEMFS.doesNotExistError=new FS.ErrnoError(44);MEMFS.doesNotExistError.stack=""}throw MEMFS.doesNotExistError},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){if(FS.isDir(old_node.mode)){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}FS.hashRemoveNode(new_node)}delete old_node.parent.contents[old_node.name];new_dir.contents[new_name]=old_node;old_node.name=new_name;new_dir.ctime=new_dir.mtime=old_node.parent.ctime=old_node.parent.mtime=Date.now()},unlink(parent,name){delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},readdir(node){return[".","..",...Object.keys(node.contents)]},symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length{var flagModes={r:0,"r+":2,w:512|64|1,"w+":512|64|2,a:1024|64|1,"a+":1024|64|2};var flags=flagModes[str];if(typeof flags=="undefined"){throw new Error(`Unknown file open mode: ${str}`)}return flags};var FS_getMode=(canRead,canWrite)=>{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode};var asyncLoad=async url=>{var arrayBuffer=await readAsync(url);return new Uint8Array(arrayBuffer)};var FS_createDataFile=(...args)=>FS.createDataFile(...args);var getUniqueRunDependency=id=>id;var runDependencies=0;var dependenciesFulfilled=null;var removeRunDependency=id=>{runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}};var addRunDependency=id=>{runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)};var preloadPlugins=[];var FS_handledByPreloadPlugin=async(byteArray,fullname)=>{if(typeof Browser!="undefined")Browser.init();for(var plugin of preloadPlugins){if(plugin["canHandle"](fullname)){return plugin["handle"](byteArray,fullname)}}return byteArray};var FS_preloadFile=async(parent,name,url,canRead,canWrite,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);addRunDependency(dep);try{var byteArray=url;if(typeof url=="string"){byteArray=await asyncLoad(url)}byteArray=await FS_handledByPreloadPlugin(byteArray,fullname);preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}}finally{removeRunDependency(dep)}};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{FS_preloadFile(parent,name,url,canRead,canWrite,dontCreateFile,canOwn,preFinish).then(onload).catch(onerror)};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,filesystems:null,syncFSRequests:0,readFiles:{},ErrnoError:class{name="ErrnoError";constructor(errno){this.errno=errno}},FSStream:class{shared={};get object(){return this.node}set object(val){this.node=val}get isRead(){return(this.flags&2097155)!==1}get isWrite(){return(this.flags&2097155)!==0}get isAppend(){return this.flags&1024}get flags(){return this.shared.flags}set flags(val){this.shared.flags=val}get position(){return this.shared.position}set position(val){this.shared.position=val}},FSNode:class{node_ops={};stream_ops={};readMode=292|73;writeMode=146;mounted=null;constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.rdev=rdev;this.atime=this.mtime=this.ctime=Date.now()}get read(){return(this.mode&this.readMode)===this.readMode}set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}get write(){return(this.mode&this.writeMode)===this.writeMode}set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}get isFolder(){return FS.isDir(this.mode)}get isDevice(){return FS.isChrdev(this.mode)}},lookupPath(path,opts={}){if(!path){throw new FS.ErrnoError(44)}opts.follow_mount??=true;if(!PATH.isAbs(path)){path=FS.cwd()+"/"+path}linkloop:for(var nlinks=0;nlinks<40;nlinks++){var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i>>0)%FS.nameTable.length},hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode(parent,name,mode,rdev){var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode(node){FS.hashRemoveNode(node)},isRoot(node){return node===node.parent},isMountpoint(node){return!!node.mounted},isFile(mode){return(mode&61440)===32768},isDir(mode){return(mode&61440)===16384},isLink(mode){return(mode&61440)===40960},isChrdev(mode){return(mode&61440)===8192},isBlkdev(mode){return(mode&61440)===24576},isFIFO(mode){return(mode&61440)===4096},isSocket(mode){return(mode&49152)===49152},flagsToPermissionString(flag){var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}else if(perms.includes("w")&&!(node.mode&146)){return 2}else if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate(dir,name){if(!FS.isDir(dir.mode)){return 54}try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&(512|64)){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},checkOpExists(op,err){if(!op){throw new FS.ErrnoError(err)}return op},MAX_OPEN_FDS:4096,nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}return stream},getStream:fd=>FS.streams[fd],createStream(stream,fd=-1){stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream(fd){FS.streams[fd]=null},dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);stream.stream_ops?.dup?.(stream);return stream},doSetAttr(stream,node,attr){var setattr=stream?.stream_ops.setattr;var arg=setattr?stream:node;setattr??=node.node_ops.setattr;FS.checkOpExists(setattr,63);setattr(arg,attr)},chrdev_stream_ops:{open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;stream.stream_ops.open?.(stream)},llseek(){throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push(...m.mounts)}return mounts},syncfs(populate,callback){if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}for(var mount of mounts){if(mount.type.syncfs){mount.type.syncfs(mount,populate,done)}else{done(null)}}},mount(type,opts,mountpoint){var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type,opts,mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);for(var[hash,current]of Object.entries(FS.nameTable)){while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}}node.mounted=null;var idx=node.mount.mounts.indexOf(mount);node.mount.mounts.splice(idx,1)},lookup(parent,name){return parent.node_ops.lookup(parent,name)},mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name){throw new FS.ErrnoError(28)}if(name==="."||name===".."){throw new FS.ErrnoError(20)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},statfs(path){return FS.statfsNode(FS.lookupPath(path,{follow:true}).node)},statfsStream(stream){return FS.statfsNode(stream.node)},statfsNode(node){var rtn={bsize:4096,frsize:4096,blocks:1e6,bfree:5e5,bavail:5e5,files:FS.nextInode,ffree:FS.nextInode-1,fsid:42,flags:2,namelen:255};if(node.node_ops.statfs){Object.assign(rtn,node.node_ops.statfs(node.mount.opts.root))}return rtn},create(path,mode=438){mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode=511){mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split("/");var d="";for(var dir of dirs){if(!dir)continue;if(d||PATH.isAbs(path))d+="/";d+=dir;try{FS.mkdir(d,mode)}catch(e){if(e.errno!=20)throw e}}},mkdev(path,mode,dev){if(typeof dev=="undefined"){dev=mode;mode=438}mode|=8192;return FS.mknod(path,mode,dev)},symlink(oldpath,newpath){if(!PATH_FS.resolve(oldpath)){throw new FS.ErrnoError(44)}var lookup=FS.lookupPath(newpath,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var newname=PATH.basename(newpath);var errCode=FS.mayCreate(parent,newname);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.symlink){throw new FS.ErrnoError(63)}return parent.node_ops.symlink(parent,newname,oldpath)},rename(old_path,new_path){var old_dirname=PATH.dirname(old_path);var new_dirname=PATH.dirname(new_path);var old_name=PATH.basename(old_path);var new_name=PATH.basename(new_path);var lookup,old_dir,new_dir;lookup=FS.lookupPath(old_path,{parent:true});old_dir=lookup.node;lookup=FS.lookupPath(new_path,{parent:true});new_dir=lookup.node;if(!old_dir||!new_dir)throw new FS.ErrnoError(44);if(old_dir.mount!==new_dir.mount){throw new FS.ErrnoError(75)}var old_node=FS.lookupNode(old_dir,old_name);var relative=PATH_FS.relative(old_path,new_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(28)}relative=PATH_FS.relative(new_path,old_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(55)}var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(old_node===new_node){return}var isdir=FS.isDir(old_node.mode);var errCode=FS.mayDelete(old_dir,old_name,isdir);if(errCode){throw new FS.ErrnoError(errCode)}errCode=new_node?FS.mayDelete(new_dir,new_name,isdir):FS.mayCreate(new_dir,new_name);if(errCode){throw new FS.ErrnoError(errCode)}if(!old_dir.node_ops.rename){throw new FS.ErrnoError(63)}if(FS.isMountpoint(old_node)||new_node&&FS.isMountpoint(new_node)){throw new FS.ErrnoError(10)}if(new_dir!==old_dir){errCode=FS.nodePermissions(old_dir,"w");if(errCode){throw new FS.ErrnoError(errCode)}}FS.hashRemoveNode(old_node);try{old_dir.node_ops.rename(old_node,new_dir,new_name);old_node.parent=new_dir}catch(e){throw e}finally{FS.hashAddNode(old_node)}},rmdir(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,true);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.rmdir){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.rmdir(parent,name);FS.destroyNode(node)},readdir(path){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var readdir=FS.checkOpExists(node.node_ops.readdir,54);return readdir(node)},unlink(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,false);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.unlink){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.unlink(parent,name);FS.destroyNode(node)},readlink(path){var lookup=FS.lookupPath(path);var link=lookup.node;if(!link){throw new FS.ErrnoError(44)}if(!link.node_ops.readlink){throw new FS.ErrnoError(28)}return link.node_ops.readlink(link)},stat(path,dontFollow){var lookup=FS.lookupPath(path,{follow:!dontFollow});var node=lookup.node;var getattr=FS.checkOpExists(node.node_ops.getattr,63);return getattr(node)},fstat(fd){var stream=FS.getStreamChecked(fd);var node=stream.node;var getattr=stream.stream_ops.getattr;var arg=getattr?stream:node;getattr??=node.node_ops.getattr;FS.checkOpExists(getattr,63);return getattr(arg)},lstat(path){return FS.stat(path,true)},doChmod(stream,node,mode,dontFollow){FS.doSetAttr(stream,node,{mode:mode&4095|node.mode&~4095,ctime:Date.now(),dontFollow})},chmod(path,mode,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChmod(null,node,mode,dontFollow)},lchmod(path,mode){FS.chmod(path,mode,true)},fchmod(fd,mode){var stream=FS.getStreamChecked(fd);FS.doChmod(stream,stream.node,mode,false)},doChown(stream,node,dontFollow){FS.doSetAttr(stream,node,{timestamp:Date.now(),dontFollow})},chown(path,uid,gid,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChown(null,node,dontFollow)},lchown(path,uid,gid){FS.chown(path,uid,gid,true)},fchown(fd,uid,gid){var stream=FS.getStreamChecked(fd);FS.doChown(stream,stream.node,false)},doTruncate(stream,node,len){if(FS.isDir(node.mode)){throw new FS.ErrnoError(31)}if(!FS.isFile(node.mode)){throw new FS.ErrnoError(28)}var errCode=FS.nodePermissions(node,"w");if(errCode){throw new FS.ErrnoError(errCode)}FS.doSetAttr(stream,node,{size:len,timestamp:Date.now()})},truncate(path,len){if(len<0){throw new FS.ErrnoError(28)}var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:true});node=lookup.node}else{node=path}FS.doTruncate(null,node,len)},ftruncate(fd,len){var stream=FS.getStreamChecked(fd);if(len<0||(stream.flags&2097155)===0){throw new FS.ErrnoError(28)}FS.doTruncate(stream,stream.node,len)},utime(path,atime,mtime){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var setattr=FS.checkOpExists(node.node_ops.setattr,63);setattr(node,{atime,mtime})},open(path,flags,mode=438){if(path===""){throw new FS.ErrnoError(44)}flags=typeof flags=="string"?FS_modeStringToFlags(flags):flags;if(flags&64){mode=mode&4095|32768}else{mode=0}var node;var isDirPath;if(typeof path=="object"){node=path}else{isDirPath=path.endsWith("/");var lookup=FS.lookupPath(path,{follow:!(flags&131072),noent_okay:true});node=lookup.node;path=lookup.path}var created=false;if(flags&64){if(node){if(flags&128){throw new FS.ErrnoError(20)}}else if(isDirPath){throw new FS.ErrnoError(31)}else{node=FS.mknod(path,mode|511,0);created=true}}if(!node){throw new FS.ErrnoError(44)}if(FS.isChrdev(node.mode)){flags&=~512}if(flags&65536&&!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}if(!created){var errCode=FS.mayOpen(node,flags);if(errCode){throw new FS.ErrnoError(errCode)}}if(flags&512&&!created){FS.truncate(node,0)}flags&=~(128|512|131072);var stream=FS.createStream({node,path:FS.getPath(node),flags,seekable:true,position:0,stream_ops:node.stream_ops,ungotten:[],error:false});if(stream.stream_ops.open){stream.stream_ops.open(stream)}if(created){FS.chmod(node,mode&511)}if(Module["logReadFiles"]&&!(flags&1)){if(!(path in FS.readFiles)){FS.readFiles[path]=1}}return stream},close(stream){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(stream.getdents)stream.getdents=null;try{if(stream.stream_ops.close){stream.stream_ops.close(stream)}}catch(e){throw e}finally{FS.closeStream(stream.fd)}stream.fd=null},isClosed(stream){return stream.fd===null},llseek(stream,offset,whence){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(!stream.seekable||!stream.stream_ops.llseek){throw new FS.ErrnoError(70)}if(whence!=0&&whence!=1&&whence!=2){throw new FS.ErrnoError(28)}stream.position=stream.stream_ops.llseek(stream,offset,whence);stream.ungotten=[];return stream.position},read(stream,buffer,offset,length,position){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.read){throw new FS.ErrnoError(28)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesRead=stream.stream_ops.read(stream,buffer,offset,length,position);if(!seeking)stream.position+=bytesRead;return bytesRead},write(stream,buffer,offset,length,position,canOwn){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.write){throw new FS.ErrnoError(28)}if(stream.seekable&&stream.flags&1024){FS.llseek(stream,0,2)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesWritten=stream.stream_ops.write(stream,buffer,offset,length,position,canOwn);if(!seeking)stream.position+=bytesWritten;return bytesWritten},mmap(stream,length,position,prot,flags){if((prot&2)!==0&&(flags&2)===0&&(stream.flags&2097155)!==2){throw new FS.ErrnoError(2)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(2)}if(!stream.stream_ops.mmap){throw new FS.ErrnoError(43)}if(!length){throw new FS.ErrnoError(28)}return stream.stream_ops.mmap(stream,length,position,prot,flags)},msync(stream,buffer,offset,length,mmapFlags){if(!stream.stream_ops.msync){return 0}return stream.stream_ops.msync(stream,buffer,offset,length,mmapFlags)},ioctl(stream,cmd,arg){if(!stream.stream_ops.ioctl){throw new FS.ErrnoError(59)}return stream.stream_ops.ioctl(stream,cmd,arg)},readFile(path,opts={}){opts.flags=opts.flags||0;opts.encoding=opts.encoding||"binary";if(opts.encoding!=="utf8"&&opts.encoding!=="binary"){abort(`Invalid encoding type "${opts.encoding}"`)}var stream=FS.open(path,opts.flags);var stat=FS.stat(path);var length=stat.size;var buf=new Uint8Array(length);FS.read(stream,buf,0,length,0);if(opts.encoding==="utf8"){buf=UTF8ArrayToString(buf)}FS.close(stream);return buf},writeFile(path,data,opts={}){opts.flags=opts.flags||577;var stream=FS.open(path,opts.flags,opts.mode);if(typeof data=="string"){data=new Uint8Array(intArrayFromString(data,true))}if(ArrayBuffer.isView(data)){FS.write(stream,data,0,data.byteLength,undefined,opts.canOwn)}else{abort("Unsupported data type")}FS.close(stream)},cwd:()=>FS.currentPath,chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories(){FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices(){FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length,llseek:()=>0});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomFill(randomBuffer);randomLeft=randomBuffer.byteLength}return randomBuffer[--randomLeft]};FS.createDevice("/dev","random",randomByte);FS.createDevice("/dev","urandom",randomByte);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories(){FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount(){var node=FS.createNode(proc_self,"fd",16895,73);node.stream_ops={llseek:MEMFS.stream_ops.llseek};node.node_ops={lookup(parent,name){var fd=+name;var stream=FS.getStreamChecked(fd);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path},id:fd+1};ret.parent=ret;return ret},readdir(){return Array.from(FS.streams.entries()).filter(([k,v])=>v).map(([k,v])=>k.toString())}};return node}},{},"/proc/self/fd")},createStandardStreams(input,output,error){if(input){FS.createDevice("/dev","stdin",input)}else{FS.symlink("/dev/tty","/dev/stdin")}if(output){FS.createDevice("/dev","stdout",null,output)}else{FS.symlink("/dev/tty","/dev/stdout")}if(error){FS.createDevice("/dev","stderr",null,error)}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1)},staticInit(){FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS}},init(input,output,error){FS.initialized=true;input??=Module["stdin"];output??=Module["stdout"];error??=Module["stderr"];FS.createStandardStreams(input,output,error)},quit(){FS.initialized=false;for(var stream of FS.streams){if(stream){FS.close(stream)}}},findObject(path,dontResolveLastLink){var ret=FS.analyzePath(path,dontResolveLastLink);if(!ret.exists){return null}return ret.object},analyzePath(path,dontResolveLastLink){try{var lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});path=lookup.path}catch(e){}var ret={isRoot:false,exists:false,error:0,name:null,path:null,object:null,parentExists:false,parentPath:null,parentObject:null};try{var lookup=FS.lookupPath(path,{parent:true});ret.parentExists=true;ret.parentPath=lookup.path;ret.parentObject=lookup.node;ret.name=PATH.basename(path);lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});ret.exists=true;ret.path=lookup.path;ret.object=lookup.node;ret.name=lookup.node.name;ret.isRoot=lookup.path==="/"}catch(e){ret.error=e.errno}return ret},createPath(parent,path,canRead,canWrite){parent=typeof parent=="string"?parent:FS.getPath(parent);var parts=path.split("/").reverse();while(parts.length){var part=parts.pop();if(!part)continue;var current=PATH.join2(parent,part);try{FS.mkdir(current)}catch(e){if(e.errno!=20)throw e}parent=current}return current},createFile(parent,name,properties,canRead,canWrite){var path=PATH.join2(typeof parent=="string"?parent:FS.getPath(parent),name);var mode=FS_getMode(canRead,canWrite);return FS.create(path,mode)},createDataFile(parent,name,data,canRead,canWrite,canOwn){var path=name;if(parent){parent=typeof parent=="string"?parent:FS.getPath(parent);path=name?PATH.join2(parent,name):parent}var mode=FS_getMode(canRead,canWrite);var node=FS.create(path,mode);if(data){if(typeof data=="string"){var arr=new Array(data.length);for(var i=0,len=data.length;ithis.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]}setDataGetter(getter){this.getter=getter}cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))abort("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)abort("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)abort("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))abort("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")abort("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true}get length(){if(!this.lengthKnown){this.cacheLength()}return this._length}get chunkSize(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}if(globalThis.XMLHttpRequest){if(!ENVIRONMENT_IS_WORKER)abort("Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc");var lazyArray=new LazyUint8Array;var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};for(const[key,fn]of Object.entries(node.stream_ops)){stream_ops[key]=(...args)=>{FS.forceLoadFile(node);return fn(...args)}}function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr,allocated:true}};node.stream_ops=stream_ops;return node}};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var SYSCALLS={calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return dir+"/"+path},writeStat(buf,stat){HEAPU32[buf>>2]=stat.dev;HEAPU32[buf+4>>2]=stat.mode;HEAPU32[buf+8>>2]=stat.nlink;HEAPU32[buf+12>>2]=stat.uid;HEAPU32[buf+16>>2]=stat.gid;HEAPU32[buf+20>>2]=stat.rdev;HEAP64[buf+24>>3]=BigInt(stat.size);HEAP32[buf+32>>2]=4096;HEAP32[buf+36>>2]=stat.blocks;var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();HEAP64[buf+40>>3]=BigInt(Math.floor(atime/1e3));HEAPU32[buf+48>>2]=atime%1e3*1e3*1e3;HEAP64[buf+56>>3]=BigInt(Math.floor(mtime/1e3));HEAPU32[buf+64>>2]=mtime%1e3*1e3*1e3;HEAP64[buf+72>>3]=BigInt(Math.floor(ctime/1e3));HEAPU32[buf+80>>2]=ctime%1e3*1e3*1e3;HEAP64[buf+88>>3]=BigInt(stat.ino);return 0},writeStatFs(buf,stats){HEAPU32[buf+4>>2]=stats.bsize;HEAPU32[buf+60>>2]=stats.bsize;HEAP64[buf+8>>3]=BigInt(stats.blocks);HEAP64[buf+16>>3]=BigInt(stats.bfree);HEAP64[buf+24>>3]=BigInt(stats.bavail);HEAP64[buf+32>>3]=BigInt(stats.files);HEAP64[buf+40>>3]=BigInt(stats.ffree);HEAPU32[buf+48>>2]=stats.fsid;HEAPU32[buf+64>>2]=stats.flags;HEAPU32[buf+56>>2]=stats.namelen},doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream},varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=syscallGetVarargI();if(arg<0){return-28}while(FS.streams[arg]){arg++}var newStream;newStream=FS.dupStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=syscallGetVarargI();stream.flags|=arg;return 0}case 12:{var arg=syscallGetVarargP();var offset=0;HEAP16[arg+offset>>1]=2;return 0}case 13:case 14:return 0}return-28}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_fstat64(fd,buf){try{return SYSCALLS.writeStat(buf,FS.fstat(fd))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);function ___syscall_getcwd(buf,size){try{if(size===0)return-28;var cwd=FS.cwd();var cwdLengthInBytes=lengthBytesUTF8(cwd)+1;if(size>2]=termios.c_iflag||0;HEAP32[argp+4>>2]=termios.c_oflag||0;HEAP32[argp+8>>2]=termios.c_cflag||0;HEAP32[argp+12>>2]=termios.c_lflag||0;for(var i=0;i<32;i++){HEAP8[argp+i+17]=termios.c_cc[i]||0}return 0}return 0}case 21510:case 21511:case 21512:{if(!stream.tty)return-59;return 0}case 21506:case 21507:case 21508:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcsets){var argp=syscallGetVarargP();var c_iflag=HEAP32[argp>>2];var c_oflag=HEAP32[argp+4>>2];var c_cflag=HEAP32[argp+8>>2];var c_lflag=HEAP32[argp+12>>2];var c_cc=[];for(var i=0;i<32;i++){c_cc.push(HEAP8[argp+i+17])}return stream.tty.ops.ioctl_tcsets(stream.tty,op,{c_iflag,c_oflag,c_cflag,c_lflag,c_cc})}return 0}case 21519:{if(!stream.tty)return-59;var argp=syscallGetVarargP();HEAP32[argp>>2]=0;return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21537:case 21531:{var argp=syscallGetVarargP();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tiocgwinsz){var winsize=stream.tty.ops.ioctl_tiocgwinsz(stream.tty);var argp=syscallGetVarargP();HEAP16[argp>>1]=winsize[0];HEAP16[argp+2>>1]=winsize[1]}return 0}case 21524:{if(!stream.tty)return-59;return 0}case 21515:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_lstat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.lstat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_newfstatat(dirfd,path,buf,flags){try{path=SYSCALLS.getStr(path);var nofollow=flags&256;var allowEmpty=flags&4096;flags=flags&~6400;path=SYSCALLS.calculateAt(dirfd,path,allowEmpty);return SYSCALLS.writeStat(buf,nofollow?FS.lstat(path):FS.stat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?syscallGetVarargI():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_stat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.stat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __abort_js=()=>abort("");var __emscripten_throw_longjmp=()=>{throw Infinity};var INT53_MAX=9007199254740992;var INT53_MIN=-9007199254740992;var bigintToI53Checked=num=>numINT53_MAX?NaN:Number(num);function __gmtime_js(time,tmPtr){time=bigintToI53Checked(time);var date=new Date(time*1e3);HEAP32[tmPtr>>2]=date.getUTCSeconds();HEAP32[tmPtr+4>>2]=date.getUTCMinutes();HEAP32[tmPtr+8>>2]=date.getUTCHours();HEAP32[tmPtr+12>>2]=date.getUTCDate();HEAP32[tmPtr+16>>2]=date.getUTCMonth();HEAP32[tmPtr+20>>2]=date.getUTCFullYear()-1900;HEAP32[tmPtr+24>>2]=date.getUTCDay();var start=Date.UTC(date.getUTCFullYear(),0,1,0,0,0,0);var yday=(date.getTime()-start)/(1e3*60*60*24)|0;HEAP32[tmPtr+28>>2]=yday}function __mmap_js(len,prot,flags,fd,offset,allocated,addr){offset=bigintToI53Checked(offset);try{var stream=SYSCALLS.getStreamFromFD(fd);var res=FS.mmap(stream,len,offset,prot,flags);var ptr=res.ptr;HEAP32[allocated>>2]=res.allocated;HEAPU32[addr>>2]=ptr;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function __munmap_js(addr,len,prot,flags,fd,offset){offset=bigintToI53Checked(offset);try{var stream=SYSCALLS.getStreamFromFD(fd);if(prot&2){SYSCALLS.doMsync(addr,stream,len,flags,offset)}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __tzset_js=(timezone,daylight,std_name,dst_name)=>{var currentYear=(new Date).getFullYear();var winter=new Date(currentYear,0,1);var summer=new Date(currentYear,6,1);var winterOffset=winter.getTimezoneOffset();var summerOffset=summer.getTimezoneOffset();var stdTimezoneOffset=Math.max(winterOffset,summerOffset);HEAPU32[timezone>>2]=stdTimezoneOffset*60;HEAP32[daylight>>2]=Number(winterOffset!=summerOffset);var extractZone=timezoneOffset=>{var sign=timezoneOffset>=0?"-":"+";var absOffset=Math.abs(timezoneOffset);var hours=String(Math.floor(absOffset/60)).padStart(2,"0");var minutes=String(absOffset%60).padStart(2,"0");return`UTC${sign}${hours}${minutes}`};var winterName=extractZone(winterOffset);var summerName=extractZone(summerOffset);if(summerOffsetperformance.now();var _emscripten_date_now=()=>Date.now();var nowIsMonotonic=1;var checkWasiClock=clock_id=>clock_id>=0&&clock_id<=3;function _clock_time_get(clk_id,ignored_precision,ptime){ignored_precision=bigintToI53Checked(ignored_precision);if(!checkWasiClock(clk_id)){return 28}var now;if(clk_id===0){now=_emscripten_date_now()}else if(nowIsMonotonic){now=_emscripten_get_now()}else{return 52}var nsec=Math.round(now*1e3*1e3);HEAP64[ptime>>3]=BigInt(nsec);return 0}var getHeapMax=()=>2147483648;var _emscripten_get_heap_max=()=>getHeapMax();var GLctx;var webgl_enable_ANGLE_instanced_arrays=ctx=>{var ext=ctx.getExtension("ANGLE_instanced_arrays");if(ext){ctx["vertexAttribDivisor"]=(index,divisor)=>ext["vertexAttribDivisorANGLE"](index,divisor);ctx["drawArraysInstanced"]=(mode,first,count,primcount)=>ext["drawArraysInstancedANGLE"](mode,first,count,primcount);ctx["drawElementsInstanced"]=(mode,count,type,indices,primcount)=>ext["drawElementsInstancedANGLE"](mode,count,type,indices,primcount);return 1}};var webgl_enable_OES_vertex_array_object=ctx=>{var ext=ctx.getExtension("OES_vertex_array_object");if(ext){ctx["createVertexArray"]=()=>ext["createVertexArrayOES"]();ctx["deleteVertexArray"]=vao=>ext["deleteVertexArrayOES"](vao);ctx["bindVertexArray"]=vao=>ext["bindVertexArrayOES"](vao);ctx["isVertexArray"]=vao=>ext["isVertexArrayOES"](vao);return 1}};var webgl_enable_WEBGL_draw_buffers=ctx=>{var ext=ctx.getExtension("WEBGL_draw_buffers");if(ext){ctx["drawBuffers"]=(n,bufs)=>ext["drawBuffersWEBGL"](n,bufs);return 1}};var webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.dibvbi=ctx.getExtension("WEBGL_draw_instanced_base_vertex_base_instance"));var webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.mdibvbi=ctx.getExtension("WEBGL_multi_draw_instanced_base_vertex_base_instance"));var webgl_enable_EXT_polygon_offset_clamp=ctx=>!!(ctx.extPolygonOffsetClamp=ctx.getExtension("EXT_polygon_offset_clamp"));var webgl_enable_EXT_clip_control=ctx=>!!(ctx.extClipControl=ctx.getExtension("EXT_clip_control"));var webgl_enable_WEBGL_polygon_mode=ctx=>!!(ctx.webglPolygonMode=ctx.getExtension("WEBGL_polygon_mode"));var webgl_enable_WEBGL_multi_draw=ctx=>!!(ctx.multiDrawWebgl=ctx.getExtension("WEBGL_multi_draw"));var getEmscriptenSupportedExtensions=ctx=>{var supportedExtensions=["ANGLE_instanced_arrays","EXT_blend_minmax","EXT_disjoint_timer_query","EXT_frag_depth","EXT_shader_texture_lod","EXT_sRGB","OES_element_index_uint","OES_fbo_render_mipmap","OES_standard_derivatives","OES_texture_float","OES_texture_half_float","OES_texture_half_float_linear","OES_vertex_array_object","WEBGL_color_buffer_float","WEBGL_depth_texture","WEBGL_draw_buffers","EXT_color_buffer_float","EXT_conservative_depth","EXT_disjoint_timer_query_webgl2","EXT_texture_norm16","NV_shader_noperspective_interpolation","WEBGL_clip_cull_distance","EXT_clip_control","EXT_color_buffer_half_float","EXT_depth_clamp","EXT_float_blend","EXT_polygon_offset_clamp","EXT_texture_compression_bptc","EXT_texture_compression_rgtc","EXT_texture_filter_anisotropic","KHR_parallel_shader_compile","OES_texture_float_linear","WEBGL_blend_func_extended","WEBGL_compressed_texture_astc","WEBGL_compressed_texture_etc","WEBGL_compressed_texture_etc1","WEBGL_compressed_texture_s3tc","WEBGL_compressed_texture_s3tc_srgb","WEBGL_debug_renderer_info","WEBGL_debug_shaders","WEBGL_lose_context","WEBGL_multi_draw","WEBGL_polygon_mode"];return(ctx.getSupportedExtensions()||[]).filter(ext=>supportedExtensions.includes(ext))};var GL={counter:1,buffers:[],programs:[],framebuffers:[],renderbuffers:[],textures:[],shaders:[],vaos:[],contexts:[],offscreenCanvases:{},queries:[],samplers:[],transformFeedbacks:[],syncs:[],stringCache:{},stringiCache:{},unpackAlignment:4,unpackRowLength:0,recordError:errorCode=>{if(!GL.lastError){GL.lastError=errorCode}},getNewId:table=>{var ret=GL.counter++;for(var i=table.length;i{for(var i=0;i>2]=id}},getSource:(shader,count,string,length)=>{var source="";for(var i=0;i>2]:undefined;source+=UTF8ToString(HEAPU32[string+i*4>>2],len)}return source},createContext:(canvas,webGLContextAttributes)=>{if(!canvas.getContextSafariWebGL2Fixed){canvas.getContextSafariWebGL2Fixed=canvas.getContext;function fixedGetContext(ver,attrs){var gl=canvas.getContextSafariWebGL2Fixed(ver,attrs);return ver=="webgl"==gl instanceof WebGLRenderingContext?gl:null}canvas.getContext=fixedGetContext}var ctx=webGLContextAttributes.majorVersion>1?canvas.getContext("webgl2",webGLContextAttributes):canvas.getContext("webgl",webGLContextAttributes);if(!ctx)return 0;var handle=GL.registerContext(ctx,webGLContextAttributes);return handle},registerContext:(ctx,webGLContextAttributes)=>{var handle=GL.getNewId(GL.contexts);var context={handle,attributes:webGLContextAttributes,version:webGLContextAttributes.majorVersion,GLctx:ctx};if(ctx.canvas)ctx.canvas.GLctxObject=context;GL.contexts[handle]=context;if(typeof webGLContextAttributes.enableExtensionsByDefault=="undefined"||webGLContextAttributes.enableExtensionsByDefault){GL.initExtensions(context)}return handle},makeContextCurrent:contextHandle=>{GL.currentContext=GL.contexts[contextHandle];Module["ctx"]=GLctx=GL.currentContext?.GLctx;return!(contextHandle&&!GLctx)},getContext:contextHandle=>GL.contexts[contextHandle],deleteContext:contextHandle=>{if(GL.currentContext===GL.contexts[contextHandle]){GL.currentContext=null}if(typeof JSEvents=="object"){JSEvents.removeAllHandlersOnTarget(GL.contexts[contextHandle].GLctx.canvas)}if(GL.contexts[contextHandle]?.GLctx.canvas){GL.contexts[contextHandle].GLctx.canvas.GLctxObject=undefined}GL.contexts[contextHandle]=null},initExtensions:context=>{context||=GL.currentContext;if(context.initExtensionsDone)return;context.initExtensionsDone=true;var GLctx=context.GLctx;webgl_enable_WEBGL_multi_draw(GLctx);webgl_enable_EXT_polygon_offset_clamp(GLctx);webgl_enable_EXT_clip_control(GLctx);webgl_enable_WEBGL_polygon_mode(GLctx);webgl_enable_ANGLE_instanced_arrays(GLctx);webgl_enable_OES_vertex_array_object(GLctx);webgl_enable_WEBGL_draw_buffers(GLctx);webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance(GLctx);webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance(GLctx);if(context.version>=2){GLctx.disjointTimerQueryExt=GLctx.getExtension("EXT_disjoint_timer_query_webgl2")}if(context.version<2||!GLctx.disjointTimerQueryExt){GLctx.disjointTimerQueryExt=GLctx.getExtension("EXT_disjoint_timer_query")}for(var ext of getEmscriptenSupportedExtensions(GLctx)){if(!ext.includes("lose_context")&&!ext.includes("debug")){GLctx.getExtension(ext)}}}};var _emscripten_glActiveTexture=x0=>GLctx.activeTexture(x0);var _emscripten_glAttachShader=(program,shader)=>{GLctx.attachShader(GL.programs[program],GL.shaders[shader])};var _emscripten_glBeginQuery=(target,id)=>{GLctx.beginQuery(target,GL.queries[id])};var _emscripten_glBeginQueryEXT=(target,id)=>{GLctx.disjointTimerQueryExt["beginQueryEXT"](target,GL.queries[id])};var _emscripten_glBeginTransformFeedback=x0=>GLctx.beginTransformFeedback(x0);var _emscripten_glBindAttribLocation=(program,index,name)=>{GLctx.bindAttribLocation(GL.programs[program],index,UTF8ToString(name))};var _emscripten_glBindBuffer=(target,buffer)=>{if(target==35051){GLctx.currentPixelPackBufferBinding=buffer}else if(target==35052){GLctx.currentPixelUnpackBufferBinding=buffer}GLctx.bindBuffer(target,GL.buffers[buffer])};var _emscripten_glBindBufferBase=(target,index,buffer)=>{GLctx.bindBufferBase(target,index,GL.buffers[buffer])};var _emscripten_glBindBufferRange=(target,index,buffer,offset,ptrsize)=>{GLctx.bindBufferRange(target,index,GL.buffers[buffer],offset,ptrsize)};var _emscripten_glBindFramebuffer=(target,framebuffer)=>{GLctx.bindFramebuffer(target,GL.framebuffers[framebuffer])};var _emscripten_glBindRenderbuffer=(target,renderbuffer)=>{GLctx.bindRenderbuffer(target,GL.renderbuffers[renderbuffer])};var _emscripten_glBindSampler=(unit,sampler)=>{GLctx.bindSampler(unit,GL.samplers[sampler])};var _emscripten_glBindTexture=(target,texture)=>{GLctx.bindTexture(target,GL.textures[texture])};var _emscripten_glBindTransformFeedback=(target,id)=>{GLctx.bindTransformFeedback(target,GL.transformFeedbacks[id])};var _emscripten_glBindVertexArray=vao=>{GLctx.bindVertexArray(GL.vaos[vao])};var _glBindVertexArray=_emscripten_glBindVertexArray;var _emscripten_glBindVertexArrayOES=_glBindVertexArray;var _emscripten_glBlendColor=(x0,x1,x2,x3)=>GLctx.blendColor(x0,x1,x2,x3);var _emscripten_glBlendEquation=x0=>GLctx.blendEquation(x0);var _emscripten_glBlendEquationSeparate=(x0,x1)=>GLctx.blendEquationSeparate(x0,x1);var _emscripten_glBlendFunc=(x0,x1)=>GLctx.blendFunc(x0,x1);var _emscripten_glBlendFuncSeparate=(x0,x1,x2,x3)=>GLctx.blendFuncSeparate(x0,x1,x2,x3);var _emscripten_glBlitFramebuffer=(x0,x1,x2,x3,x4,x5,x6,x7,x8,x9)=>GLctx.blitFramebuffer(x0,x1,x2,x3,x4,x5,x6,x7,x8,x9);var _emscripten_glBufferData=(target,size,data,usage)=>{if(GL.currentContext.version>=2){if(data&&size){GLctx.bufferData(target,HEAPU8,usage,data,size)}else{GLctx.bufferData(target,size,usage)}return}GLctx.bufferData(target,data?HEAPU8.subarray(data,data+size):size,usage)};var _emscripten_glBufferSubData=(target,offset,size,data)=>{if(GL.currentContext.version>=2){size&&GLctx.bufferSubData(target,offset,HEAPU8,data,size);return}GLctx.bufferSubData(target,offset,HEAPU8.subarray(data,data+size))};var _emscripten_glCheckFramebufferStatus=x0=>GLctx.checkFramebufferStatus(x0);var _emscripten_glClear=x0=>GLctx.clear(x0);var _emscripten_glClearBufferfi=(x0,x1,x2,x3)=>GLctx.clearBufferfi(x0,x1,x2,x3);var _emscripten_glClearBufferfv=(buffer,drawbuffer,value)=>{GLctx.clearBufferfv(buffer,drawbuffer,HEAPF32,value>>2)};var _emscripten_glClearBufferiv=(buffer,drawbuffer,value)=>{GLctx.clearBufferiv(buffer,drawbuffer,HEAP32,value>>2)};var _emscripten_glClearBufferuiv=(buffer,drawbuffer,value)=>{GLctx.clearBufferuiv(buffer,drawbuffer,HEAPU32,value>>2)};var _emscripten_glClearColor=(x0,x1,x2,x3)=>GLctx.clearColor(x0,x1,x2,x3);var _emscripten_glClearDepthf=x0=>GLctx.clearDepth(x0);var _emscripten_glClearStencil=x0=>GLctx.clearStencil(x0);var _emscripten_glClientWaitSync=(sync,flags,timeout)=>{timeout=Number(timeout);return GLctx.clientWaitSync(GL.syncs[sync],flags,timeout)};var _emscripten_glClipControlEXT=(origin,depth)=>{GLctx.extClipControl["clipControlEXT"](origin,depth)};var _emscripten_glColorMask=(red,green,blue,alpha)=>{GLctx.colorMask(!!red,!!green,!!blue,!!alpha)};var _emscripten_glCompileShader=shader=>{GLctx.compileShader(GL.shaders[shader])};var _emscripten_glCompressedTexImage2D=(target,level,internalFormat,width,height,border,imageSize,data)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding||!imageSize){GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,imageSize,data);return}GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,HEAPU8,data,imageSize);return}GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,HEAPU8.subarray(data,data+imageSize))};var _emscripten_glCompressedTexImage3D=(target,level,internalFormat,width,height,depth,border,imageSize,data)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.compressedTexImage3D(target,level,internalFormat,width,height,depth,border,imageSize,data)}else{GLctx.compressedTexImage3D(target,level,internalFormat,width,height,depth,border,HEAPU8,data,imageSize)}};var _emscripten_glCompressedTexSubImage2D=(target,level,xoffset,yoffset,width,height,format,imageSize,data)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding||!imageSize){GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,imageSize,data);return}GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,HEAPU8,data,imageSize);return}GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,HEAPU8.subarray(data,data+imageSize))};var _emscripten_glCompressedTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,imageSize,data)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.compressedTexSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,imageSize,data)}else{GLctx.compressedTexSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,HEAPU8,data,imageSize)}};var _emscripten_glCopyBufferSubData=(x0,x1,x2,x3,x4)=>GLctx.copyBufferSubData(x0,x1,x2,x3,x4);var _emscripten_glCopyTexImage2D=(x0,x1,x2,x3,x4,x5,x6,x7)=>GLctx.copyTexImage2D(x0,x1,x2,x3,x4,x5,x6,x7);var _emscripten_glCopyTexSubImage2D=(x0,x1,x2,x3,x4,x5,x6,x7)=>GLctx.copyTexSubImage2D(x0,x1,x2,x3,x4,x5,x6,x7);var _emscripten_glCopyTexSubImage3D=(x0,x1,x2,x3,x4,x5,x6,x7,x8)=>GLctx.copyTexSubImage3D(x0,x1,x2,x3,x4,x5,x6,x7,x8);var _emscripten_glCreateProgram=()=>{var id=GL.getNewId(GL.programs);var program=GLctx.createProgram();program.name=id;program.maxUniformLength=program.maxAttributeLength=program.maxUniformBlockNameLength=0;program.uniformIdCounter=1;GL.programs[id]=program;return id};var _emscripten_glCreateShader=shaderType=>{var id=GL.getNewId(GL.shaders);GL.shaders[id]=GLctx.createShader(shaderType);return id};var _emscripten_glCullFace=x0=>GLctx.cullFace(x0);var _emscripten_glDeleteBuffers=(n,buffers)=>{for(var i=0;i>2];var buffer=GL.buffers[id];if(!buffer)continue;GLctx.deleteBuffer(buffer);buffer.name=0;GL.buffers[id]=null;if(id==GLctx.currentPixelPackBufferBinding)GLctx.currentPixelPackBufferBinding=0;if(id==GLctx.currentPixelUnpackBufferBinding)GLctx.currentPixelUnpackBufferBinding=0}};var _emscripten_glDeleteFramebuffers=(n,framebuffers)=>{for(var i=0;i>2];var framebuffer=GL.framebuffers[id];if(!framebuffer)continue;GLctx.deleteFramebuffer(framebuffer);framebuffer.name=0;GL.framebuffers[id]=null}};var _emscripten_glDeleteProgram=id=>{if(!id)return;var program=GL.programs[id];if(!program){GL.recordError(1281);return}GLctx.deleteProgram(program);program.name=0;GL.programs[id]=null};var _emscripten_glDeleteQueries=(n,ids)=>{for(var i=0;i>2];var query=GL.queries[id];if(!query)continue;GLctx.deleteQuery(query);GL.queries[id]=null}};var _emscripten_glDeleteQueriesEXT=(n,ids)=>{for(var i=0;i>2];var query=GL.queries[id];if(!query)continue;GLctx.disjointTimerQueryExt["deleteQueryEXT"](query);GL.queries[id]=null}};var _emscripten_glDeleteRenderbuffers=(n,renderbuffers)=>{for(var i=0;i>2];var renderbuffer=GL.renderbuffers[id];if(!renderbuffer)continue;GLctx.deleteRenderbuffer(renderbuffer);renderbuffer.name=0;GL.renderbuffers[id]=null}};var _emscripten_glDeleteSamplers=(n,samplers)=>{for(var i=0;i>2];var sampler=GL.samplers[id];if(!sampler)continue;GLctx.deleteSampler(sampler);sampler.name=0;GL.samplers[id]=null}};var _emscripten_glDeleteShader=id=>{if(!id)return;var shader=GL.shaders[id];if(!shader){GL.recordError(1281);return}GLctx.deleteShader(shader);GL.shaders[id]=null};var _emscripten_glDeleteSync=id=>{if(!id)return;var sync=GL.syncs[id];if(!sync){GL.recordError(1281);return}GLctx.deleteSync(sync);sync.name=0;GL.syncs[id]=null};var _emscripten_glDeleteTextures=(n,textures)=>{for(var i=0;i>2];var texture=GL.textures[id];if(!texture)continue;GLctx.deleteTexture(texture);texture.name=0;GL.textures[id]=null}};var _emscripten_glDeleteTransformFeedbacks=(n,ids)=>{for(var i=0;i>2];var transformFeedback=GL.transformFeedbacks[id];if(!transformFeedback)continue;GLctx.deleteTransformFeedback(transformFeedback);transformFeedback.name=0;GL.transformFeedbacks[id]=null}};var _emscripten_glDeleteVertexArrays=(n,vaos)=>{for(var i=0;i>2];GLctx.deleteVertexArray(GL.vaos[id]);GL.vaos[id]=null}};var _glDeleteVertexArrays=_emscripten_glDeleteVertexArrays;var _emscripten_glDeleteVertexArraysOES=_glDeleteVertexArrays;var _emscripten_glDepthFunc=x0=>GLctx.depthFunc(x0);var _emscripten_glDepthMask=flag=>{GLctx.depthMask(!!flag)};var _emscripten_glDepthRangef=(x0,x1)=>GLctx.depthRange(x0,x1);var _emscripten_glDetachShader=(program,shader)=>{GLctx.detachShader(GL.programs[program],GL.shaders[shader])};var _emscripten_glDisable=x0=>GLctx.disable(x0);var _emscripten_glDisableVertexAttribArray=index=>{GLctx.disableVertexAttribArray(index)};var _emscripten_glDrawArrays=(mode,first,count)=>{GLctx.drawArrays(mode,first,count)};var _emscripten_glDrawArraysInstanced=(mode,first,count,primcount)=>{GLctx.drawArraysInstanced(mode,first,count,primcount)};var _glDrawArraysInstanced=_emscripten_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedANGLE=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedARB=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedBaseInstanceWEBGL=(mode,first,count,instanceCount,baseInstance)=>{GLctx.dibvbi["drawArraysInstancedBaseInstanceWEBGL"](mode,first,count,instanceCount,baseInstance)};var _emscripten_glDrawArraysInstancedEXT=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedNV=_glDrawArraysInstanced;var tempFixedLengthArray=[];var _emscripten_glDrawBuffers=(n,bufs)=>{var bufArray=tempFixedLengthArray[n];for(var i=0;i>2]}GLctx.drawBuffers(bufArray)};var _glDrawBuffers=_emscripten_glDrawBuffers;var _emscripten_glDrawBuffersEXT=_glDrawBuffers;var _emscripten_glDrawBuffersWEBGL=_glDrawBuffers;var _emscripten_glDrawElements=(mode,count,type,indices)=>{GLctx.drawElements(mode,count,type,indices)};var _emscripten_glDrawElementsInstanced=(mode,count,type,indices,primcount)=>{GLctx.drawElementsInstanced(mode,count,type,indices,primcount)};var _glDrawElementsInstanced=_emscripten_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedANGLE=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedARB=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL=(mode,count,type,offset,instanceCount,baseVertex,baseinstance)=>{GLctx.dibvbi["drawElementsInstancedBaseVertexBaseInstanceWEBGL"](mode,count,type,offset,instanceCount,baseVertex,baseinstance)};var _emscripten_glDrawElementsInstancedEXT=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedNV=_glDrawElementsInstanced;var _glDrawElements=_emscripten_glDrawElements;var _emscripten_glDrawRangeElements=(mode,start,end,count,type,indices)=>{_glDrawElements(mode,count,type,indices)};var _emscripten_glEnable=x0=>GLctx.enable(x0);var _emscripten_glEnableVertexAttribArray=index=>{GLctx.enableVertexAttribArray(index)};var _emscripten_glEndQuery=x0=>GLctx.endQuery(x0);var _emscripten_glEndQueryEXT=target=>{GLctx.disjointTimerQueryExt["endQueryEXT"](target)};var _emscripten_glEndTransformFeedback=()=>GLctx.endTransformFeedback();var _emscripten_glFenceSync=(condition,flags)=>{var sync=GLctx.fenceSync(condition,flags);if(sync){var id=GL.getNewId(GL.syncs);sync.name=id;GL.syncs[id]=sync;return id}return 0};var _emscripten_glFinish=()=>GLctx.finish();var _emscripten_glFlush=()=>GLctx.flush();var _emscripten_glFramebufferRenderbuffer=(target,attachment,renderbuffertarget,renderbuffer)=>{GLctx.framebufferRenderbuffer(target,attachment,renderbuffertarget,GL.renderbuffers[renderbuffer])};var _emscripten_glFramebufferTexture2D=(target,attachment,textarget,texture,level)=>{GLctx.framebufferTexture2D(target,attachment,textarget,GL.textures[texture],level)};var _emscripten_glFramebufferTextureLayer=(target,attachment,texture,level,layer)=>{GLctx.framebufferTextureLayer(target,attachment,GL.textures[texture],level,layer)};var _emscripten_glFrontFace=x0=>GLctx.frontFace(x0);var _emscripten_glGenBuffers=(n,buffers)=>{GL.genObject(n,buffers,"createBuffer",GL.buffers)};var _emscripten_glGenFramebuffers=(n,ids)=>{GL.genObject(n,ids,"createFramebuffer",GL.framebuffers)};var _emscripten_glGenQueries=(n,ids)=>{GL.genObject(n,ids,"createQuery",GL.queries)};var _emscripten_glGenQueriesEXT=(n,ids)=>{for(var i=0;i>2]=0;return}var id=GL.getNewId(GL.queries);query.name=id;GL.queries[id]=query;HEAP32[ids+i*4>>2]=id}};var _emscripten_glGenRenderbuffers=(n,renderbuffers)=>{GL.genObject(n,renderbuffers,"createRenderbuffer",GL.renderbuffers)};var _emscripten_glGenSamplers=(n,samplers)=>{GL.genObject(n,samplers,"createSampler",GL.samplers)};var _emscripten_glGenTextures=(n,textures)=>{GL.genObject(n,textures,"createTexture",GL.textures)};var _emscripten_glGenTransformFeedbacks=(n,ids)=>{GL.genObject(n,ids,"createTransformFeedback",GL.transformFeedbacks)};var _emscripten_glGenVertexArrays=(n,arrays)=>{GL.genObject(n,arrays,"createVertexArray",GL.vaos)};var _glGenVertexArrays=_emscripten_glGenVertexArrays;var _emscripten_glGenVertexArraysOES=_glGenVertexArrays;var _emscripten_glGenerateMipmap=x0=>GLctx.generateMipmap(x0);var __glGetActiveAttribOrUniform=(funcName,program,index,bufSize,length,size,type,name)=>{program=GL.programs[program];var info=GLctx[funcName](program,index);if(info){var numBytesWrittenExclNull=name&&stringToUTF8(info.name,name,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull;if(size)HEAP32[size>>2]=info.size;if(type)HEAP32[type>>2]=info.type}};var _emscripten_glGetActiveAttrib=(program,index,bufSize,length,size,type,name)=>__glGetActiveAttribOrUniform("getActiveAttrib",program,index,bufSize,length,size,type,name);var _emscripten_glGetActiveUniform=(program,index,bufSize,length,size,type,name)=>__glGetActiveAttribOrUniform("getActiveUniform",program,index,bufSize,length,size,type,name);var _emscripten_glGetActiveUniformBlockName=(program,uniformBlockIndex,bufSize,length,uniformBlockName)=>{program=GL.programs[program];var result=GLctx.getActiveUniformBlockName(program,uniformBlockIndex);if(!result)return;if(uniformBlockName&&bufSize>0){var numBytesWrittenExclNull=stringToUTF8(result,uniformBlockName,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull}else{if(length)HEAP32[length>>2]=0}};var _emscripten_glGetActiveUniformBlockiv=(program,uniformBlockIndex,pname,params)=>{if(!params){GL.recordError(1281);return}program=GL.programs[program];if(pname==35393){var name=GLctx.getActiveUniformBlockName(program,uniformBlockIndex);HEAP32[params>>2]=name.length+1;return}var result=GLctx.getActiveUniformBlockParameter(program,uniformBlockIndex,pname);if(result===null)return;if(pname==35395){for(var i=0;i>2]=result[i]}}else{HEAP32[params>>2]=result}};var _emscripten_glGetActiveUniformsiv=(program,uniformCount,uniformIndices,pname,params)=>{if(!params){GL.recordError(1281);return}if(uniformCount>0&&uniformIndices==0){GL.recordError(1281);return}program=GL.programs[program];var ids=[];for(var i=0;i>2])}var result=GLctx.getActiveUniforms(program,ids,pname);if(!result)return;var len=result.length;for(var i=0;i>2]=result[i]}};var _emscripten_glGetAttachedShaders=(program,maxCount,count,shaders)=>{var result=GLctx.getAttachedShaders(GL.programs[program]);var len=result.length;if(len>maxCount){len=maxCount}HEAP32[count>>2]=len;for(var i=0;i>2]=id}};var _emscripten_glGetAttribLocation=(program,name)=>GLctx.getAttribLocation(GL.programs[program],UTF8ToString(name));var writeI53ToI64=(ptr,num)=>{HEAPU32[ptr>>2]=num;var lower=HEAPU32[ptr>>2];HEAPU32[ptr+4>>2]=(num-lower)/4294967296};var webglGetExtensions=()=>{var exts=getEmscriptenSupportedExtensions(GLctx);exts=exts.concat(exts.map(e=>"GL_"+e));return exts};var emscriptenWebGLGet=(name_,p,type)=>{if(!p){GL.recordError(1281);return}var ret=undefined;switch(name_){case 36346:ret=1;break;case 36344:if(type!=0&&type!=1){GL.recordError(1280)}return;case 34814:case 36345:ret=0;break;case 34466:var formats=GLctx.getParameter(34467);ret=formats?formats.length:0;break;case 33309:if(GL.currentContext.version<2){GL.recordError(1282);return}ret=webglGetExtensions().length;break;case 33307:case 33308:if(GL.currentContext.version<2){GL.recordError(1280);return}ret=name_==33307?3:0;break}if(ret===undefined){var result=GLctx.getParameter(name_);switch(typeof result){case"number":ret=result;break;case"boolean":ret=result?1:0;break;case"string":GL.recordError(1280);return;case"object":if(result===null){switch(name_){case 34964:case 35725:case 34965:case 36006:case 36007:case 32873:case 34229:case 36662:case 36663:case 35053:case 35055:case 36010:case 35097:case 35869:case 32874:case 36389:case 35983:case 35368:case 34068:{ret=0;break}default:{GL.recordError(1280);return}}}else if(result instanceof Float32Array||result instanceof Uint32Array||result instanceof Int32Array||result instanceof Array){for(var i=0;i>2]=result[i];break;case 2:HEAPF32[p+i*4>>2]=result[i];break;case 4:HEAP8[p+i]=result[i]?1:0;break}}return}else{try{ret=result.name|0}catch(e){GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Unknown object returned from WebGL getParameter(${name_})! (error: ${e})`);return}}break;default:GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Native code calling glGet${type}v(${name_}) and it returns ${result} of type ${typeof result}!`);return}}switch(type){case 1:writeI53ToI64(p,ret);break;case 0:HEAP32[p>>2]=ret;break;case 2:HEAPF32[p>>2]=ret;break;case 4:HEAP8[p]=ret?1:0;break}};var _emscripten_glGetBooleanv=(name_,p)=>emscriptenWebGLGet(name_,p,4);var _emscripten_glGetBufferParameteri64v=(target,value,data)=>{if(!data){GL.recordError(1281);return}writeI53ToI64(data,GLctx.getBufferParameter(target,value))};var _emscripten_glGetBufferParameteriv=(target,value,data)=>{if(!data){GL.recordError(1281);return}HEAP32[data>>2]=GLctx.getBufferParameter(target,value)};var _emscripten_glGetError=()=>{var error=GLctx.getError()||GL.lastError;GL.lastError=0;return error};var _emscripten_glGetFloatv=(name_,p)=>emscriptenWebGLGet(name_,p,2);var _emscripten_glGetFragDataLocation=(program,name)=>GLctx.getFragDataLocation(GL.programs[program],UTF8ToString(name));var _emscripten_glGetFramebufferAttachmentParameteriv=(target,attachment,pname,params)=>{var result=GLctx.getFramebufferAttachmentParameter(target,attachment,pname);if(result instanceof WebGLRenderbuffer||result instanceof WebGLTexture){result=result.name|0}HEAP32[params>>2]=result};var emscriptenWebGLGetIndexed=(target,index,data,type)=>{if(!data){GL.recordError(1281);return}var result=GLctx.getIndexedParameter(target,index);var ret;switch(typeof result){case"boolean":ret=result?1:0;break;case"number":ret=result;break;case"object":if(result===null){switch(target){case 35983:case 35368:ret=0;break;default:{GL.recordError(1280);return}}}else if(result instanceof WebGLBuffer){ret=result.name|0}else{GL.recordError(1280);return}break;default:GL.recordError(1280);return}switch(type){case 1:writeI53ToI64(data,ret);break;case 0:HEAP32[data>>2]=ret;break;case 2:HEAPF32[data>>2]=ret;break;case 4:HEAP8[data]=ret?1:0;break;default:abort("internal emscriptenWebGLGetIndexed() error, bad type: "+type)}};var _emscripten_glGetInteger64i_v=(target,index,data)=>emscriptenWebGLGetIndexed(target,index,data,1);var _emscripten_glGetInteger64v=(name_,p)=>{emscriptenWebGLGet(name_,p,1)};var _emscripten_glGetIntegeri_v=(target,index,data)=>emscriptenWebGLGetIndexed(target,index,data,0);var _emscripten_glGetIntegerv=(name_,p)=>emscriptenWebGLGet(name_,p,0);var _emscripten_glGetInternalformativ=(target,internalformat,pname,bufSize,params)=>{if(bufSize<0){GL.recordError(1281);return}if(!params){GL.recordError(1281);return}var ret=GLctx.getInternalformatParameter(target,internalformat,pname);if(ret===null)return;for(var i=0;i>2]=ret[i]}};var _emscripten_glGetProgramBinary=(program,bufSize,length,binaryFormat,binary)=>{GL.recordError(1282)};var _emscripten_glGetProgramInfoLog=(program,maxLength,length,infoLog)=>{var log=GLctx.getProgramInfoLog(GL.programs[program]);if(log===null)log="(unknown error)";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetProgramiv=(program,pname,p)=>{if(!p){GL.recordError(1281);return}if(program>=GL.counter){GL.recordError(1281);return}program=GL.programs[program];if(pname==35716){var log=GLctx.getProgramInfoLog(program);if(log===null)log="(unknown error)";HEAP32[p>>2]=log.length+1}else if(pname==35719){if(!program.maxUniformLength){var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(var i=0;i>2]=program.maxUniformLength}else if(pname==35722){if(!program.maxAttributeLength){var numActiveAttributes=GLctx.getProgramParameter(program,35721);for(var i=0;i>2]=program.maxAttributeLength}else if(pname==35381){if(!program.maxUniformBlockNameLength){var numActiveUniformBlocks=GLctx.getProgramParameter(program,35382);for(var i=0;i>2]=program.maxUniformBlockNameLength}else{HEAP32[p>>2]=GLctx.getProgramParameter(program,pname)}};var _emscripten_glGetQueryObjecti64vEXT=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param;if(GL.currentContext.version<2){param=GLctx.disjointTimerQueryExt["getQueryObjectEXT"](query,pname)}else{param=GLctx.getQueryParameter(query,pname)}var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}writeI53ToI64(params,ret)};var _emscripten_glGetQueryObjectivEXT=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param=GLctx.disjointTimerQueryExt["getQueryObjectEXT"](query,pname);var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}HEAP32[params>>2]=ret};var _glGetQueryObjecti64vEXT=_emscripten_glGetQueryObjecti64vEXT;var _emscripten_glGetQueryObjectui64vEXT=_glGetQueryObjecti64vEXT;var _emscripten_glGetQueryObjectuiv=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param=GLctx.getQueryParameter(query,pname);var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}HEAP32[params>>2]=ret};var _glGetQueryObjectivEXT=_emscripten_glGetQueryObjectivEXT;var _emscripten_glGetQueryObjectuivEXT=_glGetQueryObjectivEXT;var _emscripten_glGetQueryiv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getQuery(target,pname)};var _emscripten_glGetQueryivEXT=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.disjointTimerQueryExt["getQueryEXT"](target,pname)};var _emscripten_glGetRenderbufferParameteriv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getRenderbufferParameter(target,pname)};var _emscripten_glGetSamplerParameterfv=(sampler,pname,params)=>{if(!params){GL.recordError(1281);return}HEAPF32[params>>2]=GLctx.getSamplerParameter(GL.samplers[sampler],pname)};var _emscripten_glGetSamplerParameteriv=(sampler,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getSamplerParameter(GL.samplers[sampler],pname)};var _emscripten_glGetShaderInfoLog=(shader,maxLength,length,infoLog)=>{var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log="(unknown error)";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetShaderPrecisionFormat=(shaderType,precisionType,range,precision)=>{var result=GLctx.getShaderPrecisionFormat(shaderType,precisionType);HEAP32[range>>2]=result.rangeMin;HEAP32[range+4>>2]=result.rangeMax;HEAP32[precision>>2]=result.precision};var _emscripten_glGetShaderSource=(shader,bufSize,length,source)=>{var result=GLctx.getShaderSource(GL.shaders[shader]);if(!result)return;var numBytesWrittenExclNull=bufSize>0&&source?stringToUTF8(result,source,bufSize):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetShaderiv=(shader,pname,p)=>{if(!p){GL.recordError(1281);return}if(pname==35716){var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log="(unknown error)";var logLength=log?log.length+1:0;HEAP32[p>>2]=logLength}else if(pname==35720){var source=GLctx.getShaderSource(GL.shaders[shader]);var sourceLength=source?source.length+1:0;HEAP32[p>>2]=sourceLength}else{HEAP32[p>>2]=GLctx.getShaderParameter(GL.shaders[shader],pname)}};var stringToNewUTF8=str=>{var size=lengthBytesUTF8(str)+1;var ret=_malloc(size);if(ret)stringToUTF8(str,ret,size);return ret};var _emscripten_glGetString=name_=>{var ret=GL.stringCache[name_];if(!ret){switch(name_){case 7939:ret=stringToNewUTF8(webglGetExtensions().join(" "));break;case 7936:case 7937:case 37445:case 37446:var s=GLctx.getParameter(name_);if(!s){GL.recordError(1280)}ret=s?stringToNewUTF8(s):0;break;case 7938:var webGLVersion=GLctx.getParameter(7938);var glVersion=`OpenGL ES 2.0 (${webGLVersion})`;if(GL.currentContext.version>=2)glVersion=`OpenGL ES 3.0 (${webGLVersion})`;ret=stringToNewUTF8(glVersion);break;case 35724:var glslVersion=GLctx.getParameter(35724);var ver_re=/^WebGL GLSL ES ([0-9]\.[0-9][0-9]?)(?:$| .*)/;var ver_num=glslVersion.match(ver_re);if(ver_num!==null){if(ver_num[1].length==3)ver_num[1]=ver_num[1]+"0";glslVersion=`OpenGL ES GLSL ES ${ver_num[1]} (${glslVersion})`}ret=stringToNewUTF8(glslVersion);break;default:GL.recordError(1280)}GL.stringCache[name_]=ret}return ret};var _emscripten_glGetStringi=(name,index)=>{if(GL.currentContext.version<2){GL.recordError(1282);return 0}var stringiCache=GL.stringiCache[name];if(stringiCache){if(index<0||index>=stringiCache.length){GL.recordError(1281);return 0}return stringiCache[index]}switch(name){case 7939:var exts=webglGetExtensions().map(stringToNewUTF8);stringiCache=GL.stringiCache[name]=exts;if(index<0||index>=stringiCache.length){GL.recordError(1281);return 0}return stringiCache[index];default:GL.recordError(1280);return 0}};var _emscripten_glGetSynciv=(sync,pname,bufSize,length,values)=>{if(bufSize<0){GL.recordError(1281);return}if(!values){GL.recordError(1281);return}var ret=GLctx.getSyncParameter(GL.syncs[sync],pname);if(ret!==null){HEAP32[values>>2]=ret;if(length)HEAP32[length>>2]=1}};var _emscripten_glGetTexParameterfv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAPF32[params>>2]=GLctx.getTexParameter(target,pname)};var _emscripten_glGetTexParameteriv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getTexParameter(target,pname)};var _emscripten_glGetTransformFeedbackVarying=(program,index,bufSize,length,size,type,name)=>{program=GL.programs[program];var info=GLctx.getTransformFeedbackVarying(program,index);if(!info)return;if(name&&bufSize>0){var numBytesWrittenExclNull=stringToUTF8(info.name,name,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull}else{if(length)HEAP32[length>>2]=0}if(size)HEAP32[size>>2]=info.size;if(type)HEAP32[type>>2]=info.type};var _emscripten_glGetUniformBlockIndex=(program,uniformBlockName)=>GLctx.getUniformBlockIndex(GL.programs[program],UTF8ToString(uniformBlockName));var _emscripten_glGetUniformIndices=(program,uniformCount,uniformNames,uniformIndices)=>{if(!uniformIndices){GL.recordError(1281);return}if(uniformCount>0&&(uniformNames==0||uniformIndices==0)){GL.recordError(1281);return}program=GL.programs[program];var names=[];for(var i=0;i>2]));var result=GLctx.getUniformIndices(program,names);if(!result)return;var len=result.length;for(var i=0;i>2]=result[i]}};var jstoi_q=str=>parseInt(str);var webglGetLeftBracePos=name=>name.slice(-1)=="]"&&name.lastIndexOf("[");var webglPrepareUniformLocationsBeforeFirstUse=program=>{var uniformLocsById=program.uniformLocsById,uniformSizeAndIdsByName=program.uniformSizeAndIdsByName,i,j;if(!uniformLocsById){program.uniformLocsById=uniformLocsById={};program.uniformArrayNamesById={};var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(i=0;i0?nm.slice(0,lb):nm;var id=program.uniformIdCounter;program.uniformIdCounter+=sz;uniformSizeAndIdsByName[arrayName]=[sz,id];for(j=0;j{name=UTF8ToString(name);if(program=GL.programs[program]){webglPrepareUniformLocationsBeforeFirstUse(program);var uniformLocsById=program.uniformLocsById;var arrayIndex=0;var uniformBaseName=name;var leftBrace=webglGetLeftBracePos(name);if(leftBrace>0){arrayIndex=jstoi_q(name.slice(leftBrace+1))>>>0;uniformBaseName=name.slice(0,leftBrace)}var sizeAndId=program.uniformSizeAndIdsByName[uniformBaseName];if(sizeAndId&&arrayIndex{var p=GLctx.currentProgram;if(p){var webglLoc=p.uniformLocsById[location];if(typeof webglLoc=="number"){p.uniformLocsById[location]=webglLoc=GLctx.getUniformLocation(p,p.uniformArrayNamesById[location]+(webglLoc>0?`[${webglLoc}]`:""))}return webglLoc}else{GL.recordError(1282)}};var emscriptenWebGLGetUniform=(program,location,params,type)=>{if(!params){GL.recordError(1281);return}program=GL.programs[program];webglPrepareUniformLocationsBeforeFirstUse(program);var data=GLctx.getUniform(program,webglGetUniformLocation(location));if(typeof data=="number"||typeof data=="boolean"){switch(type){case 0:HEAP32[params>>2]=data;break;case 2:HEAPF32[params>>2]=data;break}}else{for(var i=0;i>2]=data[i];break;case 2:HEAPF32[params+i*4>>2]=data[i];break}}}};var _emscripten_glGetUniformfv=(program,location,params)=>{emscriptenWebGLGetUniform(program,location,params,2)};var _emscripten_glGetUniformiv=(program,location,params)=>{emscriptenWebGLGetUniform(program,location,params,0)};var _emscripten_glGetUniformuiv=(program,location,params)=>emscriptenWebGLGetUniform(program,location,params,0);var emscriptenWebGLGetVertexAttrib=(index,pname,params,type)=>{if(!params){GL.recordError(1281);return}var data=GLctx.getVertexAttrib(index,pname);if(pname==34975){HEAP32[params>>2]=data&&data["name"]}else if(typeof data=="number"||typeof data=="boolean"){switch(type){case 0:HEAP32[params>>2]=data;break;case 2:HEAPF32[params>>2]=data;break;case 5:HEAP32[params>>2]=Math.fround(data);break}}else{for(var i=0;i>2]=data[i];break;case 2:HEAPF32[params+i*4>>2]=data[i];break;case 5:HEAP32[params+i*4>>2]=Math.fround(data[i]);break}}}};var _emscripten_glGetVertexAttribIiv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,0)};var _glGetVertexAttribIiv=_emscripten_glGetVertexAttribIiv;var _emscripten_glGetVertexAttribIuiv=_glGetVertexAttribIiv;var _emscripten_glGetVertexAttribPointerv=(index,pname,pointer)=>{if(!pointer){GL.recordError(1281);return}HEAP32[pointer>>2]=GLctx.getVertexAttribOffset(index,pname)};var _emscripten_glGetVertexAttribfv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,2)};var _emscripten_glGetVertexAttribiv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,5)};var _emscripten_glHint=(x0,x1)=>GLctx.hint(x0,x1);var _emscripten_glInvalidateFramebuffer=(target,numAttachments,attachments)=>{var list=tempFixedLengthArray[numAttachments];for(var i=0;i>2]}GLctx.invalidateFramebuffer(target,list)};var _emscripten_glInvalidateSubFramebuffer=(target,numAttachments,attachments,x,y,width,height)=>{var list=tempFixedLengthArray[numAttachments];for(var i=0;i>2]}GLctx.invalidateSubFramebuffer(target,list,x,y,width,height)};var _emscripten_glIsBuffer=buffer=>{var b=GL.buffers[buffer];if(!b)return 0;return GLctx.isBuffer(b)};var _emscripten_glIsEnabled=x0=>GLctx.isEnabled(x0);var _emscripten_glIsFramebuffer=framebuffer=>{var fb=GL.framebuffers[framebuffer];if(!fb)return 0;return GLctx.isFramebuffer(fb)};var _emscripten_glIsProgram=program=>{program=GL.programs[program];if(!program)return 0;return GLctx.isProgram(program)};var _emscripten_glIsQuery=id=>{var query=GL.queries[id];if(!query)return 0;return GLctx.isQuery(query)};var _emscripten_glIsQueryEXT=id=>{var query=GL.queries[id];if(!query)return 0;return GLctx.disjointTimerQueryExt["isQueryEXT"](query)};var _emscripten_glIsRenderbuffer=renderbuffer=>{var rb=GL.renderbuffers[renderbuffer];if(!rb)return 0;return GLctx.isRenderbuffer(rb)};var _emscripten_glIsSampler=id=>{var sampler=GL.samplers[id];if(!sampler)return 0;return GLctx.isSampler(sampler)};var _emscripten_glIsShader=shader=>{var s=GL.shaders[shader];if(!s)return 0;return GLctx.isShader(s)};var _emscripten_glIsSync=sync=>GLctx.isSync(GL.syncs[sync]);var _emscripten_glIsTexture=id=>{var texture=GL.textures[id];if(!texture)return 0;return GLctx.isTexture(texture)};var _emscripten_glIsTransformFeedback=id=>GLctx.isTransformFeedback(GL.transformFeedbacks[id]);var _emscripten_glIsVertexArray=array=>{var vao=GL.vaos[array];if(!vao)return 0;return GLctx.isVertexArray(vao)};var _glIsVertexArray=_emscripten_glIsVertexArray;var _emscripten_glIsVertexArrayOES=_glIsVertexArray;var _emscripten_glLineWidth=x0=>GLctx.lineWidth(x0);var _emscripten_glLinkProgram=program=>{program=GL.programs[program];GLctx.linkProgram(program);program.uniformLocsById=0;program.uniformSizeAndIdsByName={}};var _emscripten_glMultiDrawArraysInstancedBaseInstanceWEBGL=(mode,firsts,counts,instanceCounts,baseInstances,drawCount)=>{GLctx.mdibvbi["multiDrawArraysInstancedBaseInstanceWEBGL"](mode,HEAP32,firsts>>2,HEAP32,counts>>2,HEAP32,instanceCounts>>2,HEAPU32,baseInstances>>2,drawCount)};var _emscripten_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL=(mode,counts,type,offsets,instanceCounts,baseVertices,baseInstances,drawCount)=>{GLctx.mdibvbi["multiDrawElementsInstancedBaseVertexBaseInstanceWEBGL"](mode,HEAP32,counts>>2,type,HEAP32,offsets>>2,HEAP32,instanceCounts>>2,HEAP32,baseVertices>>2,HEAPU32,baseInstances>>2,drawCount)};var _emscripten_glPauseTransformFeedback=()=>GLctx.pauseTransformFeedback();var _emscripten_glPixelStorei=(pname,param)=>{if(pname==3317){GL.unpackAlignment=param}else if(pname==3314){GL.unpackRowLength=param}GLctx.pixelStorei(pname,param)};var _emscripten_glPolygonModeWEBGL=(face,mode)=>{GLctx.webglPolygonMode["polygonModeWEBGL"](face,mode)};var _emscripten_glPolygonOffset=(x0,x1)=>GLctx.polygonOffset(x0,x1);var _emscripten_glPolygonOffsetClampEXT=(factor,units,clamp)=>{GLctx.extPolygonOffsetClamp["polygonOffsetClampEXT"](factor,units,clamp)};var _emscripten_glProgramBinary=(program,binaryFormat,binary,length)=>{GL.recordError(1280)};var _emscripten_glProgramParameteri=(program,pname,value)=>{GL.recordError(1280)};var _emscripten_glQueryCounterEXT=(id,target)=>{GLctx.disjointTimerQueryExt["queryCounterEXT"](GL.queries[id],target)};var _emscripten_glReadBuffer=x0=>GLctx.readBuffer(x0);var computeUnpackAlignedImageSize=(width,height,sizePerPixel)=>{function roundedToNextMultipleOf(x,y){return x+y-1&-y}var plainRowSize=(GL.unpackRowLength||width)*sizePerPixel;var alignedRowSize=roundedToNextMultipleOf(plainRowSize,GL.unpackAlignment);return height*alignedRowSize};var colorChannelsInGlTextureFormat=format=>{var colorChannels={5:3,6:4,8:2,29502:3,29504:4,26917:2,26918:2,29846:3,29847:4};return colorChannels[format-6402]||1};var heapObjectForWebGLType=type=>{type-=5120;if(type==0)return HEAP8;if(type==1)return HEAPU8;if(type==2)return HEAP16;if(type==4)return HEAP32;if(type==6)return HEAPF32;if(type==5||type==28922||type==28520||type==30779||type==30782)return HEAPU32;return HEAPU16};var toTypedArrayIndex=(pointer,heap)=>pointer>>>31-Math.clz32(heap.BYTES_PER_ELEMENT);var emscriptenWebGLGetTexPixelData=(type,format,width,height,pixels,internalFormat)=>{var heap=heapObjectForWebGLType(type);var sizePerPixel=colorChannelsInGlTextureFormat(format)*heap.BYTES_PER_ELEMENT;var bytes=computeUnpackAlignedImageSize(width,height,sizePerPixel);return heap.subarray(toTypedArrayIndex(pixels,heap),toTypedArrayIndex(pixels+bytes,heap))};var _emscripten_glReadPixels=(x,y,width,height,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelPackBufferBinding){GLctx.readPixels(x,y,width,height,format,type,pixels);return}var heap=heapObjectForWebGLType(type);var target=toTypedArrayIndex(pixels,heap);GLctx.readPixels(x,y,width,height,format,type,heap,target);return}var pixelData=emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,format);if(!pixelData){GL.recordError(1280);return}GLctx.readPixels(x,y,width,height,format,type,pixelData)};var _emscripten_glReleaseShaderCompiler=()=>{};var _emscripten_glRenderbufferStorage=(x0,x1,x2,x3)=>GLctx.renderbufferStorage(x0,x1,x2,x3);var _emscripten_glRenderbufferStorageMultisample=(x0,x1,x2,x3,x4)=>GLctx.renderbufferStorageMultisample(x0,x1,x2,x3,x4);var _emscripten_glResumeTransformFeedback=()=>GLctx.resumeTransformFeedback();var _emscripten_glSampleCoverage=(value,invert)=>{GLctx.sampleCoverage(value,!!invert)};var _emscripten_glSamplerParameterf=(sampler,pname,param)=>{GLctx.samplerParameterf(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameterfv=(sampler,pname,params)=>{var param=HEAPF32[params>>2];GLctx.samplerParameterf(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameteri=(sampler,pname,param)=>{GLctx.samplerParameteri(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameteriv=(sampler,pname,params)=>{var param=HEAP32[params>>2];GLctx.samplerParameteri(GL.samplers[sampler],pname,param)};var _emscripten_glScissor=(x0,x1,x2,x3)=>GLctx.scissor(x0,x1,x2,x3);var _emscripten_glShaderBinary=(count,shaders,binaryformat,binary,length)=>{GL.recordError(1280)};var _emscripten_glShaderSource=(shader,count,string,length)=>{var source=GL.getSource(shader,count,string,length);GLctx.shaderSource(GL.shaders[shader],source)};var _emscripten_glStencilFunc=(x0,x1,x2)=>GLctx.stencilFunc(x0,x1,x2);var _emscripten_glStencilFuncSeparate=(x0,x1,x2,x3)=>GLctx.stencilFuncSeparate(x0,x1,x2,x3);var _emscripten_glStencilMask=x0=>GLctx.stencilMask(x0);var _emscripten_glStencilMaskSeparate=(x0,x1)=>GLctx.stencilMaskSeparate(x0,x1);var _emscripten_glStencilOp=(x0,x1,x2)=>GLctx.stencilOp(x0,x1,x2);var _emscripten_glStencilOpSeparate=(x0,x1,x2,x3)=>GLctx.stencilOpSeparate(x0,x1,x2,x3);var _emscripten_glTexImage2D=(target,level,internalFormat,width,height,border,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);var index=toTypedArrayIndex(pixels,heap);GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,heap,index);return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,internalFormat):null;GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixelData)};var _emscripten_glTexImage3D=(target,level,internalFormat,width,height,depth,border,format,type,pixels)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,pixels)}else if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,heap,toTypedArrayIndex(pixels,heap))}else{GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,null)}};var _emscripten_glTexParameterf=(x0,x1,x2)=>GLctx.texParameterf(x0,x1,x2);var _emscripten_glTexParameterfv=(target,pname,params)=>{var param=HEAPF32[params>>2];GLctx.texParameterf(target,pname,param)};var _emscripten_glTexParameteri=(x0,x1,x2)=>GLctx.texParameteri(x0,x1,x2);var _emscripten_glTexParameteriv=(target,pname,params)=>{var param=HEAP32[params>>2];GLctx.texParameteri(target,pname,param)};var _emscripten_glTexStorage2D=(x0,x1,x2,x3,x4)=>GLctx.texStorage2D(x0,x1,x2,x3,x4);var _emscripten_glTexStorage3D=(x0,x1,x2,x3,x4,x5)=>GLctx.texStorage3D(x0,x1,x2,x3,x4,x5);var _emscripten_glTexSubImage2D=(target,level,xoffset,yoffset,width,height,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,heap,toTypedArrayIndex(pixels,heap));return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,0):null;GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixelData)};var _emscripten_glTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)}else if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,heap,toTypedArrayIndex(pixels,heap))}else{GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,null)}};var _emscripten_glTransformFeedbackVaryings=(program,count,varyings,bufferMode)=>{program=GL.programs[program];var vars=[];for(var i=0;i>2]));GLctx.transformFeedbackVaryings(program,vars,bufferMode)};var _emscripten_glUniform1f=(location,v0)=>{GLctx.uniform1f(webglGetUniformLocation(location),v0)};var miniTempWebGLFloatBuffers=[];var _emscripten_glUniform1fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform1fv(webglGetUniformLocation(location),HEAPF32,value>>2,count);return}if(count<=288){var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*4>>2)}GLctx.uniform1fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform1i=(location,v0)=>{GLctx.uniform1i(webglGetUniformLocation(location),v0)};var miniTempWebGLIntBuffers=[];var _emscripten_glUniform1iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform1iv(webglGetUniformLocation(location),HEAP32,value>>2,count);return}if(count<=288){var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2]}}else{var view=HEAP32.subarray(value>>2,value+count*4>>2)}GLctx.uniform1iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform1ui=(location,v0)=>{GLctx.uniform1ui(webglGetUniformLocation(location),v0)};var _emscripten_glUniform1uiv=(location,count,value)=>{count&&GLctx.uniform1uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count)};var _emscripten_glUniform2f=(location,v0,v1)=>{GLctx.uniform2f(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform2fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*2);return}if(count<=144){count*=2;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*8>>2)}GLctx.uniform2fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform2i=(location,v0,v1)=>{GLctx.uniform2i(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform2iv(webglGetUniformLocation(location),HEAP32,value>>2,count*2);return}if(count<=144){count*=2;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*8>>2)}GLctx.uniform2iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform2ui=(location,v0,v1)=>{GLctx.uniform2ui(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2uiv=(location,count,value)=>{count&&GLctx.uniform2uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*2)};var _emscripten_glUniform3f=(location,v0,v1,v2)=>{GLctx.uniform3f(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform3fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*3);return}if(count<=96){count*=3;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*12>>2)}GLctx.uniform3fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform3i=(location,v0,v1,v2)=>{GLctx.uniform3i(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform3iv(webglGetUniformLocation(location),HEAP32,value>>2,count*3);return}if(count<=96){count*=3;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2];view[i+2]=HEAP32[value+(4*i+8)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*12>>2)}GLctx.uniform3iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform3ui=(location,v0,v1,v2)=>{GLctx.uniform3ui(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3uiv=(location,count,value)=>{count&&GLctx.uniform3uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*3)};var _emscripten_glUniform4f=(location,v0,v1,v2,v3)=>{GLctx.uniform4f(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform4fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*4);return}if(count<=72){var view=miniTempWebGLFloatBuffers[4*count];var heap=HEAPF32;value=value>>2;count*=4;for(var i=0;i>2,value+count*16>>2)}GLctx.uniform4fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform4i=(location,v0,v1,v2,v3)=>{GLctx.uniform4i(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform4iv(webglGetUniformLocation(location),HEAP32,value>>2,count*4);return}if(count<=72){count*=4;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2];view[i+2]=HEAP32[value+(4*i+8)>>2];view[i+3]=HEAP32[value+(4*i+12)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*16>>2)}GLctx.uniform4iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform4ui=(location,v0,v1,v2,v3)=>{GLctx.uniform4ui(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4uiv=(location,count,value)=>{count&&GLctx.uniform4uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*4)};var _emscripten_glUniformBlockBinding=(program,uniformBlockIndex,uniformBlockBinding)=>{program=GL.programs[program];GLctx.uniformBlockBinding(program,uniformBlockIndex,uniformBlockBinding)};var _emscripten_glUniformMatrix2fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*4);return}if(count<=72){count*=4;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2];view[i+3]=HEAPF32[value+(4*i+12)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*16>>2)}GLctx.uniformMatrix2fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix2x3fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix2x3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*6)};var _emscripten_glUniformMatrix2x4fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix2x4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*8)};var _emscripten_glUniformMatrix3fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*9);return}if(count<=32){count*=9;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2];view[i+3]=HEAPF32[value+(4*i+12)>>2];view[i+4]=HEAPF32[value+(4*i+16)>>2];view[i+5]=HEAPF32[value+(4*i+20)>>2];view[i+6]=HEAPF32[value+(4*i+24)>>2];view[i+7]=HEAPF32[value+(4*i+28)>>2];view[i+8]=HEAPF32[value+(4*i+32)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*36>>2)}GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix3x2fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix3x2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*6)};var _emscripten_glUniformMatrix3x4fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix3x4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*12)};var _emscripten_glUniformMatrix4fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*16);return}if(count<=18){var view=miniTempWebGLFloatBuffers[16*count];var heap=HEAPF32;value=value>>2;count*=16;for(var i=0;i>2,value+count*64>>2)}GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix4x2fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix4x2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*8)};var _emscripten_glUniformMatrix4x3fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix4x3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*12)};var _emscripten_glUseProgram=program=>{program=GL.programs[program];GLctx.useProgram(program);GLctx.currentProgram=program};var _emscripten_glValidateProgram=program=>{GLctx.validateProgram(GL.programs[program])};var _emscripten_glVertexAttrib1f=(x0,x1)=>GLctx.vertexAttrib1f(x0,x1);var _emscripten_glVertexAttrib1fv=(index,v)=>{GLctx.vertexAttrib1f(index,HEAPF32[v>>2])};var _emscripten_glVertexAttrib2f=(x0,x1,x2)=>GLctx.vertexAttrib2f(x0,x1,x2);var _emscripten_glVertexAttrib2fv=(index,v)=>{GLctx.vertexAttrib2f(index,HEAPF32[v>>2],HEAPF32[v+4>>2])};var _emscripten_glVertexAttrib3f=(x0,x1,x2,x3)=>GLctx.vertexAttrib3f(x0,x1,x2,x3);var _emscripten_glVertexAttrib3fv=(index,v)=>{GLctx.vertexAttrib3f(index,HEAPF32[v>>2],HEAPF32[v+4>>2],HEAPF32[v+8>>2])};var _emscripten_glVertexAttrib4f=(x0,x1,x2,x3,x4)=>GLctx.vertexAttrib4f(x0,x1,x2,x3,x4);var _emscripten_glVertexAttrib4fv=(index,v)=>{GLctx.vertexAttrib4f(index,HEAPF32[v>>2],HEAPF32[v+4>>2],HEAPF32[v+8>>2],HEAPF32[v+12>>2])};var _emscripten_glVertexAttribDivisor=(index,divisor)=>{GLctx.vertexAttribDivisor(index,divisor)};var _glVertexAttribDivisor=_emscripten_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorANGLE=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorARB=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorEXT=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorNV=_glVertexAttribDivisor;var _emscripten_glVertexAttribI4i=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4i(x0,x1,x2,x3,x4);var _emscripten_glVertexAttribI4iv=(index,v)=>{GLctx.vertexAttribI4i(index,HEAP32[v>>2],HEAP32[v+4>>2],HEAP32[v+8>>2],HEAP32[v+12>>2])};var _emscripten_glVertexAttribI4ui=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4ui(x0,x1,x2,x3,x4);var _emscripten_glVertexAttribI4uiv=(index,v)=>{GLctx.vertexAttribI4ui(index,HEAPU32[v>>2],HEAPU32[v+4>>2],HEAPU32[v+8>>2],HEAPU32[v+12>>2])};var _emscripten_glVertexAttribIPointer=(index,size,type,stride,ptr)=>{GLctx.vertexAttribIPointer(index,size,type,stride,ptr)};var _emscripten_glVertexAttribPointer=(index,size,type,normalized,stride,ptr)=>{GLctx.vertexAttribPointer(index,size,type,!!normalized,stride,ptr)};var _emscripten_glViewport=(x0,x1,x2,x3)=>GLctx.viewport(x0,x1,x2,x3);var _emscripten_glWaitSync=(sync,flags,timeout)=>{timeout=Number(timeout);GLctx.waitSync(GL.syncs[sync],flags,timeout)};var wasmTableMirror=[];var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var _emscripten_request_animation_frame_loop=(cb,userData)=>{function tick(timeStamp){if(getWasmTableEntry(cb)(timeStamp,userData)){requestAnimationFrame(tick)}}return requestAnimationFrame(tick)};var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var ENV={};var getExecutableName=()=>thisProgram||"./this.program";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(globalThis.navigator?.language??"C").replace("-","_")+".UTF-8";var env={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:lang,_:getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var _environ_get=(__environ,environ_buf)=>{var bufSize=0;var envp=0;for(var string of getEnvStrings()){var ptr=environ_buf+bufSize;HEAPU32[__environ+envp>>2]=ptr;bufSize+=stringToUTF8(string,ptr,Infinity)+1;envp+=4}return 0};var _environ_sizes_get=(penviron_count,penviron_buf_size)=>{var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;var bufSize=0;for(var string of strings){bufSize+=lengthBytesUTF8(string)+1}HEAPU32[penviron_buf_size>>2]=bufSize;return 0};var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doReadv=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_read(fd,iov,iovcnt,pnum){try{var stream=SYSCALLS.getStreamFromFD(fd);var num=doReadv(stream,iov,iovcnt);HEAPU32[pnum>>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_seek(fd,offset,whence,newOffset){offset=bigintToI53Checked(offset);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);HEAP64[newOffset>>3]=BigInt(stream.position);if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doWritev=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var _glGetIntegerv=_emscripten_glGetIntegerv;var _glGetString=_emscripten_glGetString;var _glGetStringi=_emscripten_glGetStringi;var _llvm_eh_typeid_for=type=>type;function _random_get(buffer,size){try{randomFill(HEAPU8.subarray(buffer,buffer+size));return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};FS.createPreloadedFile=FS_createPreloadedFile;FS.preloadFile=FS_preloadFile;FS.staticInit();for(let i=0;i<32;++i)tempFixedLengthArray.push(new Array(i));var miniTempWebGLFloatBuffersStorage=new Float32Array(288);for(var i=0;i<=288;++i){miniTempWebGLFloatBuffers[i]=miniTempWebGLFloatBuffersStorage.subarray(0,i)}var miniTempWebGLIntBuffersStorage=new Int32Array(288);for(var i=0;i<=288;++i){miniTempWebGLIntBuffers[i]=miniTempWebGLIntBuffersStorage.subarray(0,i)}{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["preloadPlugins"])preloadPlugins=Module["preloadPlugins"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}Module["UTF8ToString"]=UTF8ToString;Module["stringToUTF8"]=stringToUTF8;Module["lengthBytesUTF8"]=lengthBytesUTF8;Module["GL"]=GL;var _malloc,_add_font,_add_image,_add_image_with_rid,_allocate,_apply_scene_transactions,_command,_deallocate,_destroy,_devtools_rendering_set_show_fps_meter,_devtools_rendering_set_show_hit_testing,_devtools_rendering_set_show_ruler,_devtools_rendering_set_show_stats,_devtools_rendering_set_show_tiles,_drain_missing_images,_export_node_as,_get_default_fallback_fonts,_get_image_bytes,_get_image_size,_get_node_absolute_bounding_box,_get_node_id_from_point,_get_node_ids_from_envelope,_get_node_ids_from_point,_grida_fonts_analyze_family,_grida_fonts_free,_grida_fonts_parse_font,_grida_markdown_to_html,_grida_svg_optimize,_grida_svg_to_document,_has_missing_fonts,_highlight_strokes,_init,_init_with_backend,_list_available_fonts,_list_missing_fonts,_load_benchmark_scene,_load_dummy_scene,_load_scene_grida,_load_scene_grida1,_loaded_scene_ids,_pointer_move,_query_paint_groups,_redraw,_resize_surface,_resolve_image,_runtime_renderer_set_layer_compositing,_runtime_renderer_set_outline_mode,_runtime_renderer_set_pixel_preview_scale,_runtime_renderer_set_pixel_preview_stable,_runtime_renderer_set_render_policy_flags,_runtime_renderer_set_skip_layout,_set_debug,_set_default_fallback_fonts,_set_main_camera_transform,_set_surface_overlay_config,_set_verbose,_surface_get_cursor,_surface_get_hovered_node,_surface_get_selected_nodes,_surface_pointer_down,_surface_pointer_move,_surface_pointer_up,_surface_set_selection,_switch_scene,_text_edit_command,_text_edit_enter,_text_edit_exit,_text_edit_get_caret_rect,_text_edit_get_selected_html,_text_edit_get_selected_text,_text_edit_get_selection_rects,_text_edit_get_text,_text_edit_ime_cancel,_text_edit_ime_commit,_text_edit_ime_set_preedit,_text_edit_is_active,_text_edit_paste_html,_text_edit_paste_text,_text_edit_pointer_down,_text_edit_pointer_move,_text_edit_pointer_up,_text_edit_redo,_text_edit_set_color,_text_edit_set_font_family,_text_edit_set_font_size,_text_edit_tick,_text_edit_toggle_bold,_text_edit_toggle_italic,_text_edit_toggle_strikethrough,_text_edit_toggle_underline,_text_edit_undo,_tick,_to_vector_network,_toggle_debug,_main,_emscripten_builtin_memalign,_setThrew,__emscripten_tempret_set,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,___cxa_decrement_exception_refcount,___cxa_increment_exception_refcount,___cxa_can_catch,___cxa_get_exception_ptr,memory,__indirect_function_table,wasmMemory,wasmTable;function assignWasmExports(wasmExports){_malloc=wasmExports["Zg"];_add_font=Module["_add_font"]=wasmExports["$g"];_add_image=Module["_add_image"]=wasmExports["ah"];_add_image_with_rid=Module["_add_image_with_rid"]=wasmExports["bh"];_allocate=Module["_allocate"]=wasmExports["ch"];_apply_scene_transactions=Module["_apply_scene_transactions"]=wasmExports["dh"];_command=Module["_command"]=wasmExports["eh"];_deallocate=Module["_deallocate"]=wasmExports["fh"];_destroy=Module["_destroy"]=wasmExports["gh"];_devtools_rendering_set_show_fps_meter=Module["_devtools_rendering_set_show_fps_meter"]=wasmExports["hh"];_devtools_rendering_set_show_hit_testing=Module["_devtools_rendering_set_show_hit_testing"]=wasmExports["ih"];_devtools_rendering_set_show_ruler=Module["_devtools_rendering_set_show_ruler"]=wasmExports["jh"];_devtools_rendering_set_show_stats=Module["_devtools_rendering_set_show_stats"]=wasmExports["kh"];_devtools_rendering_set_show_tiles=Module["_devtools_rendering_set_show_tiles"]=wasmExports["lh"];_drain_missing_images=Module["_drain_missing_images"]=wasmExports["mh"];_export_node_as=Module["_export_node_as"]=wasmExports["nh"];_get_default_fallback_fonts=Module["_get_default_fallback_fonts"]=wasmExports["oh"];_get_image_bytes=Module["_get_image_bytes"]=wasmExports["ph"];_get_image_size=Module["_get_image_size"]=wasmExports["qh"];_get_node_absolute_bounding_box=Module["_get_node_absolute_bounding_box"]=wasmExports["rh"];_get_node_id_from_point=Module["_get_node_id_from_point"]=wasmExports["sh"];_get_node_ids_from_envelope=Module["_get_node_ids_from_envelope"]=wasmExports["th"];_get_node_ids_from_point=Module["_get_node_ids_from_point"]=wasmExports["uh"];_grida_fonts_analyze_family=Module["_grida_fonts_analyze_family"]=wasmExports["vh"];_grida_fonts_free=Module["_grida_fonts_free"]=wasmExports["wh"];_grida_fonts_parse_font=Module["_grida_fonts_parse_font"]=wasmExports["xh"];_grida_markdown_to_html=Module["_grida_markdown_to_html"]=wasmExports["yh"];_grida_svg_optimize=Module["_grida_svg_optimize"]=wasmExports["zh"];_grida_svg_to_document=Module["_grida_svg_to_document"]=wasmExports["Ah"];_has_missing_fonts=Module["_has_missing_fonts"]=wasmExports["Bh"];_highlight_strokes=Module["_highlight_strokes"]=wasmExports["Ch"];_init=Module["_init"]=wasmExports["Dh"];_init_with_backend=Module["_init_with_backend"]=wasmExports["Eh"];_list_available_fonts=Module["_list_available_fonts"]=wasmExports["Fh"];_list_missing_fonts=Module["_list_missing_fonts"]=wasmExports["Gh"];_load_benchmark_scene=Module["_load_benchmark_scene"]=wasmExports["Hh"];_load_dummy_scene=Module["_load_dummy_scene"]=wasmExports["Ih"];_load_scene_grida=Module["_load_scene_grida"]=wasmExports["Jh"];_load_scene_grida1=Module["_load_scene_grida1"]=wasmExports["Kh"];_loaded_scene_ids=Module["_loaded_scene_ids"]=wasmExports["Lh"];_pointer_move=Module["_pointer_move"]=wasmExports["Mh"];_query_paint_groups=Module["_query_paint_groups"]=wasmExports["Nh"];_redraw=Module["_redraw"]=wasmExports["Oh"];_resize_surface=Module["_resize_surface"]=wasmExports["Ph"];_resolve_image=Module["_resolve_image"]=wasmExports["Qh"];_runtime_renderer_set_layer_compositing=Module["_runtime_renderer_set_layer_compositing"]=wasmExports["Rh"];_runtime_renderer_set_outline_mode=Module["_runtime_renderer_set_outline_mode"]=wasmExports["Sh"];_runtime_renderer_set_pixel_preview_scale=Module["_runtime_renderer_set_pixel_preview_scale"]=wasmExports["Th"];_runtime_renderer_set_pixel_preview_stable=Module["_runtime_renderer_set_pixel_preview_stable"]=wasmExports["Uh"];_runtime_renderer_set_render_policy_flags=Module["_runtime_renderer_set_render_policy_flags"]=wasmExports["Vh"];_runtime_renderer_set_skip_layout=Module["_runtime_renderer_set_skip_layout"]=wasmExports["Wh"];_set_debug=Module["_set_debug"]=wasmExports["Xh"];_set_default_fallback_fonts=Module["_set_default_fallback_fonts"]=wasmExports["Yh"];_set_main_camera_transform=Module["_set_main_camera_transform"]=wasmExports["Zh"];_set_surface_overlay_config=Module["_set_surface_overlay_config"]=wasmExports["_h"];_set_verbose=Module["_set_verbose"]=wasmExports["$h"];_surface_get_cursor=Module["_surface_get_cursor"]=wasmExports["ai"];_surface_get_hovered_node=Module["_surface_get_hovered_node"]=wasmExports["bi"];_surface_get_selected_nodes=Module["_surface_get_selected_nodes"]=wasmExports["ci"];_surface_pointer_down=Module["_surface_pointer_down"]=wasmExports["di"];_surface_pointer_move=Module["_surface_pointer_move"]=wasmExports["ei"];_surface_pointer_up=Module["_surface_pointer_up"]=wasmExports["fi"];_surface_set_selection=Module["_surface_set_selection"]=wasmExports["gi"];_switch_scene=Module["_switch_scene"]=wasmExports["hi"];_text_edit_command=Module["_text_edit_command"]=wasmExports["ii"];_text_edit_enter=Module["_text_edit_enter"]=wasmExports["ji"];_text_edit_exit=Module["_text_edit_exit"]=wasmExports["ki"];_text_edit_get_caret_rect=Module["_text_edit_get_caret_rect"]=wasmExports["li"];_text_edit_get_selected_html=Module["_text_edit_get_selected_html"]=wasmExports["mi"];_text_edit_get_selected_text=Module["_text_edit_get_selected_text"]=wasmExports["ni"];_text_edit_get_selection_rects=Module["_text_edit_get_selection_rects"]=wasmExports["oi"];_text_edit_get_text=Module["_text_edit_get_text"]=wasmExports["pi"];_text_edit_ime_cancel=Module["_text_edit_ime_cancel"]=wasmExports["qi"];_text_edit_ime_commit=Module["_text_edit_ime_commit"]=wasmExports["ri"];_text_edit_ime_set_preedit=Module["_text_edit_ime_set_preedit"]=wasmExports["si"];_text_edit_is_active=Module["_text_edit_is_active"]=wasmExports["ti"];_text_edit_paste_html=Module["_text_edit_paste_html"]=wasmExports["ui"];_text_edit_paste_text=Module["_text_edit_paste_text"]=wasmExports["vi"];_text_edit_pointer_down=Module["_text_edit_pointer_down"]=wasmExports["wi"];_text_edit_pointer_move=Module["_text_edit_pointer_move"]=wasmExports["xi"];_text_edit_pointer_up=Module["_text_edit_pointer_up"]=wasmExports["yi"];_text_edit_redo=Module["_text_edit_redo"]=wasmExports["zi"];_text_edit_set_color=Module["_text_edit_set_color"]=wasmExports["Ai"];_text_edit_set_font_family=Module["_text_edit_set_font_family"]=wasmExports["Bi"];_text_edit_set_font_size=Module["_text_edit_set_font_size"]=wasmExports["Ci"];_text_edit_tick=Module["_text_edit_tick"]=wasmExports["Di"];_text_edit_toggle_bold=Module["_text_edit_toggle_bold"]=wasmExports["Ei"];_text_edit_toggle_italic=Module["_text_edit_toggle_italic"]=wasmExports["Fi"];_text_edit_toggle_strikethrough=Module["_text_edit_toggle_strikethrough"]=wasmExports["Gi"];_text_edit_toggle_underline=Module["_text_edit_toggle_underline"]=wasmExports["Hi"];_text_edit_undo=Module["_text_edit_undo"]=wasmExports["Ii"];_tick=Module["_tick"]=wasmExports["Ji"];_to_vector_network=Module["_to_vector_network"]=wasmExports["Ki"];_toggle_debug=Module["_toggle_debug"]=wasmExports["Li"];_main=Module["_main"]=wasmExports["Mi"];_emscripten_builtin_memalign=wasmExports["Ni"];_setThrew=wasmExports["Oi"];__emscripten_tempret_set=wasmExports["Pi"];__emscripten_stack_restore=wasmExports["Qi"];__emscripten_stack_alloc=wasmExports["Ri"];_emscripten_stack_get_current=wasmExports["Si"];___cxa_decrement_exception_refcount=wasmExports["Ti"];___cxa_increment_exception_refcount=wasmExports["Ui"];___cxa_can_catch=wasmExports["Vi"];___cxa_get_exception_ptr=wasmExports["Wi"];memory=wasmMemory=wasmExports["Xg"];__indirect_function_table=wasmTable=wasmExports["_g"]}var wasmImports={I:___cxa_begin_catch,Q:___cxa_end_catch,a:___cxa_find_matching_catch_2,n:___cxa_find_matching_catch_3,ka:___cxa_find_matching_catch_4,La:___cxa_rethrow,K:___cxa_throw,ib:___cxa_uncaught_exceptions,e:___resumeException,Oa:___syscall_fcntl64,zb:___syscall_fstat64,vb:___syscall_getcwd,Ab:___syscall_ioctl,wb:___syscall_lstat64,xb:___syscall_newfstatat,Pa:___syscall_openat,yb:___syscall_stat64,Db:__abort_js,kb:__emscripten_throw_longjmp,qb:__gmtime_js,ob:__mmap_js,pb:__munmap_js,Eb:__tzset_js,Cb:_clock_time_get,Bb:_emscripten_date_now,mb:_emscripten_get_heap_max,Ef:_emscripten_glActiveTexture,Ff:_emscripten_glAttachShader,ge:_emscripten_glBeginQuery,ae:_emscripten_glBeginQueryEXT,Ic:_emscripten_glBeginTransformFeedback,Gf:_emscripten_glBindAttribLocation,Hf:_emscripten_glBindBuffer,Fc:_emscripten_glBindBufferBase,Gc:_emscripten_glBindBufferRange,Ee:_emscripten_glBindFramebuffer,Fe:_emscripten_glBindRenderbuffer,me:_emscripten_glBindSampler,If:_emscripten_glBindTexture,Vb:_emscripten_glBindTransformFeedback,_e:_emscripten_glBindVertexArray,bf:_emscripten_glBindVertexArrayOES,Jf:_emscripten_glBlendColor,Kf:_emscripten_glBlendEquation,Md:_emscripten_glBlendEquationSeparate,Lf:_emscripten_glBlendFunc,Ld:_emscripten_glBlendFuncSeparate,ye:_emscripten_glBlitFramebuffer,Mf:_emscripten_glBufferData,Nf:_emscripten_glBufferSubData,Ge:_emscripten_glCheckFramebufferStatus,Of:_emscripten_glClear,jc:_emscripten_glClearBufferfi,kc:_emscripten_glClearBufferfv,mc:_emscripten_glClearBufferiv,lc:_emscripten_glClearBufferuiv,Pf:_emscripten_glClearColor,Kd:_emscripten_glClearDepthf,Qf:_emscripten_glClearStencil,ve:_emscripten_glClientWaitSync,bd:_emscripten_glClipControlEXT,Rf:_emscripten_glColorMask,Sf:_emscripten_glCompileShader,Tf:_emscripten_glCompressedTexImage2D,Uc:_emscripten_glCompressedTexImage3D,Uf:_emscripten_glCompressedTexSubImage2D,Tc:_emscripten_glCompressedTexSubImage3D,xe:_emscripten_glCopyBufferSubData,Jd:_emscripten_glCopyTexImage2D,Vf:_emscripten_glCopyTexSubImage2D,Vc:_emscripten_glCopyTexSubImage3D,Wf:_emscripten_glCreateProgram,Xf:_emscripten_glCreateShader,Yf:_emscripten_glCullFace,Zf:_emscripten_glDeleteBuffers,He:_emscripten_glDeleteFramebuffers,_f:_emscripten_glDeleteProgram,he:_emscripten_glDeleteQueries,be:_emscripten_glDeleteQueriesEXT,Ie:_emscripten_glDeleteRenderbuffers,ne:_emscripten_glDeleteSamplers,$f:_emscripten_glDeleteShader,we:_emscripten_glDeleteSync,ag:_emscripten_glDeleteTextures,Ub:_emscripten_glDeleteTransformFeedbacks,$e:_emscripten_glDeleteVertexArrays,cf:_emscripten_glDeleteVertexArraysOES,Id:_emscripten_glDepthFunc,bg:_emscripten_glDepthMask,Hd:_emscripten_glDepthRangef,Gd:_emscripten_glDetachShader,cg:_emscripten_glDisable,dg:_emscripten_glDisableVertexAttribArray,eg:_emscripten_glDrawArrays,Ye:_emscripten_glDrawArraysInstanced,Pd:_emscripten_glDrawArraysInstancedANGLE,Hb:_emscripten_glDrawArraysInstancedARB,Ve:_emscripten_glDrawArraysInstancedBaseInstanceWEBGL,_c:_emscripten_glDrawArraysInstancedEXT,Ib:_emscripten_glDrawArraysInstancedNV,Te:_emscripten_glDrawBuffers,Yc:_emscripten_glDrawBuffersEXT,Qd:_emscripten_glDrawBuffersWEBGL,fg:_emscripten_glDrawElements,Ze:_emscripten_glDrawElementsInstanced,Od:_emscripten_glDrawElementsInstancedANGLE,Fb:_emscripten_glDrawElementsInstancedARB,We:_emscripten_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Gb:_emscripten_glDrawElementsInstancedEXT,Zc:_emscripten_glDrawElementsInstancedNV,Ne:_emscripten_glDrawRangeElements,gg:_emscripten_glEnable,hg:_emscripten_glEnableVertexAttribArray,ie:_emscripten_glEndQuery,ce:_emscripten_glEndQueryEXT,Hc:_emscripten_glEndTransformFeedback,se:_emscripten_glFenceSync,ig:_emscripten_glFinish,jg:_emscripten_glFlush,Je:_emscripten_glFramebufferRenderbuffer,Ke:_emscripten_glFramebufferTexture2D,Lc:_emscripten_glFramebufferTextureLayer,kg:_emscripten_glFrontFace,lg:_emscripten_glGenBuffers,Le:_emscripten_glGenFramebuffers,je:_emscripten_glGenQueries,de:_emscripten_glGenQueriesEXT,Me:_emscripten_glGenRenderbuffers,oe:_emscripten_glGenSamplers,mg:_emscripten_glGenTextures,Tb:_emscripten_glGenTransformFeedbacks,Xe:_emscripten_glGenVertexArrays,df:_emscripten_glGenVertexArraysOES,Ae:_emscripten_glGenerateMipmap,Fd:_emscripten_glGetActiveAttrib,Ed:_emscripten_glGetActiveUniform,ec:_emscripten_glGetActiveUniformBlockName,fc:_emscripten_glGetActiveUniformBlockiv,hc:_emscripten_glGetActiveUniformsiv,Dd:_emscripten_glGetAttachedShaders,Cd:_emscripten_glGetAttribLocation,Bd:_emscripten_glGetBooleanv,$b:_emscripten_glGetBufferParameteri64v,ng:_emscripten_glGetBufferParameteriv,og:_emscripten_glGetError,pg:_emscripten_glGetFloatv,vc:_emscripten_glGetFragDataLocation,Be:_emscripten_glGetFramebufferAttachmentParameteriv,ac:_emscripten_glGetInteger64i_v,cc:_emscripten_glGetInteger64v,Jc:_emscripten_glGetIntegeri_v,qg:_emscripten_glGetIntegerv,Lb:_emscripten_glGetInternalformativ,Pb:_emscripten_glGetProgramBinary,rg:_emscripten_glGetProgramInfoLog,sg:_emscripten_glGetProgramiv,Zd:_emscripten_glGetQueryObjecti64vEXT,Sd:_emscripten_glGetQueryObjectivEXT,_d:_emscripten_glGetQueryObjectui64vEXT,ke:_emscripten_glGetQueryObjectuiv,ee:_emscripten_glGetQueryObjectuivEXT,le:_emscripten_glGetQueryiv,fe:_emscripten_glGetQueryivEXT,Ce:_emscripten_glGetRenderbufferParameteriv,Wb:_emscripten_glGetSamplerParameterfv,Yb:_emscripten_glGetSamplerParameteriv,tg:_emscripten_glGetShaderInfoLog,Wd:_emscripten_glGetShaderPrecisionFormat,Ad:_emscripten_glGetShaderSource,ug:_emscripten_glGetShaderiv,vg:_emscripten_glGetString,af:_emscripten_glGetStringi,bc:_emscripten_glGetSynciv,zd:_emscripten_glGetTexParameterfv,yd:_emscripten_glGetTexParameteriv,Dc:_emscripten_glGetTransformFeedbackVarying,gc:_emscripten_glGetUniformBlockIndex,ic:_emscripten_glGetUniformIndices,xg:_emscripten_glGetUniformLocation,xd:_emscripten_glGetUniformfv,wd:_emscripten_glGetUniformiv,wc:_emscripten_glGetUniformuiv,Cc:_emscripten_glGetVertexAttribIiv,Bc:_emscripten_glGetVertexAttribIuiv,td:_emscripten_glGetVertexAttribPointerv,vd:_emscripten_glGetVertexAttribfv,ud:_emscripten_glGetVertexAttribiv,sd:_emscripten_glHint,Xd:_emscripten_glInvalidateFramebuffer,Yd:_emscripten_glInvalidateSubFramebuffer,rd:_emscripten_glIsBuffer,qd:_emscripten_glIsEnabled,pd:_emscripten_glIsFramebuffer,od:_emscripten_glIsProgram,Sc:_emscripten_glIsQuery,Td:_emscripten_glIsQueryEXT,nd:_emscripten_glIsRenderbuffer,_b:_emscripten_glIsSampler,md:_emscripten_glIsShader,te:_emscripten_glIsSync,yg:_emscripten_glIsTexture,Sb:_emscripten_glIsTransformFeedback,Kc:_emscripten_glIsVertexArray,Rd:_emscripten_glIsVertexArrayOES,zg:_emscripten_glLineWidth,Ag:_emscripten_glLinkProgram,Re:_emscripten_glMultiDrawArraysInstancedBaseInstanceWEBGL,Se:_emscripten_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Rb:_emscripten_glPauseTransformFeedback,Bg:_emscripten_glPixelStorei,ad:_emscripten_glPolygonModeWEBGL,ld:_emscripten_glPolygonOffset,cd:_emscripten_glPolygonOffsetClampEXT,Ob:_emscripten_glProgramBinary,Nb:_emscripten_glProgramParameteri,$d:_emscripten_glQueryCounterEXT,Ue:_emscripten_glReadBuffer,Cg:_emscripten_glReadPixels,kd:_emscripten_glReleaseShaderCompiler,De:_emscripten_glRenderbufferStorage,ze:_emscripten_glRenderbufferStorageMultisample,Qb:_emscripten_glResumeTransformFeedback,jd:_emscripten_glSampleCoverage,pe:_emscripten_glSamplerParameterf,Zb:_emscripten_glSamplerParameterfv,qe:_emscripten_glSamplerParameteri,re:_emscripten_glSamplerParameteriv,Dg:_emscripten_glScissor,id:_emscripten_glShaderBinary,Eg:_emscripten_glShaderSource,Fg:_emscripten_glStencilFunc,Gg:_emscripten_glStencilFuncSeparate,Hg:_emscripten_glStencilMask,Ig:_emscripten_glStencilMaskSeparate,Jg:_emscripten_glStencilOp,Kg:_emscripten_glStencilOpSeparate,Lg:_emscripten_glTexImage2D,Xc:_emscripten_glTexImage3D,Mg:_emscripten_glTexParameterf,Ng:_emscripten_glTexParameterfv,Og:_emscripten_glTexParameteri,Pg:_emscripten_glTexParameteriv,Oe:_emscripten_glTexStorage2D,Mb:_emscripten_glTexStorage3D,Qg:_emscripten_glTexSubImage2D,Wc:_emscripten_glTexSubImage3D,Ec:_emscripten_glTransformFeedbackVaryings,Rg:_emscripten_glUniform1f,Sg:_emscripten_glUniform1fv,Af:_emscripten_glUniform1i,Bf:_emscripten_glUniform1iv,uc:_emscripten_glUniform1ui,qc:_emscripten_glUniform1uiv,Cf:_emscripten_glUniform2f,Df:_emscripten_glUniform2fv,yf:_emscripten_glUniform2i,xf:_emscripten_glUniform2iv,tc:_emscripten_glUniform2ui,pc:_emscripten_glUniform2uiv,wf:_emscripten_glUniform3f,vf:_emscripten_glUniform3fv,uf:_emscripten_glUniform3i,tf:_emscripten_glUniform3iv,sc:_emscripten_glUniform3ui,oc:_emscripten_glUniform3uiv,sf:_emscripten_glUniform4f,rf:_emscripten_glUniform4fv,ef:_emscripten_glUniform4i,ff:_emscripten_glUniform4iv,rc:_emscripten_glUniform4ui,nc:_emscripten_glUniform4uiv,dc:_emscripten_glUniformBlockBinding,gf:_emscripten_glUniformMatrix2fv,Rc:_emscripten_glUniformMatrix2x3fv,Pc:_emscripten_glUniformMatrix2x4fv,hf:_emscripten_glUniformMatrix3fv,Qc:_emscripten_glUniformMatrix3x2fv,Nc:_emscripten_glUniformMatrix3x4fv,jf:_emscripten_glUniformMatrix4fv,Oc:_emscripten_glUniformMatrix4x2fv,Mc:_emscripten_glUniformMatrix4x3fv,kf:_emscripten_glUseProgram,hd:_emscripten_glValidateProgram,lf:_emscripten_glVertexAttrib1f,gd:_emscripten_glVertexAttrib1fv,fd:_emscripten_glVertexAttrib2f,mf:_emscripten_glVertexAttrib2fv,ed:_emscripten_glVertexAttrib3f,nf:_emscripten_glVertexAttrib3fv,dd:_emscripten_glVertexAttrib4f,of:_emscripten_glVertexAttrib4fv,Pe:_emscripten_glVertexAttribDivisor,Nd:_emscripten_glVertexAttribDivisorANGLE,Jb:_emscripten_glVertexAttribDivisorARB,$c:_emscripten_glVertexAttribDivisorEXT,Kb:_emscripten_glVertexAttribDivisorNV,Ac:_emscripten_glVertexAttribI4i,yc:_emscripten_glVertexAttribI4iv,zc:_emscripten_glVertexAttribI4ui,xc:_emscripten_glVertexAttribI4uiv,Qe:_emscripten_glVertexAttribIPointer,pf:_emscripten_glVertexAttribPointer,qf:_emscripten_glViewport,ue:_emscripten_glWaitSync,$a:_emscripten_request_animation_frame_loop,lb:_emscripten_resize_heap,sb:_environ_get,tb:_environ_sizes_get,Vg:_exit,ta:_fd_close,nb:_fd_pread,Na:_fd_read,rb:_fd_seek,sa:_fd_write,Tg:_glGetIntegerv,Ta:_glGetString,Ug:_glGetStringi,Ud:invoke_dd,Vd:invoke_dddd,Ja:invoke_diii,Wa:invoke_fdiiii,Va:invoke_fdiiiii,Wg:invoke_fii,Ka:invoke_fiii,w:invoke_fiiidi,Y:invoke_fiiif,x:invoke_fiiiidi,v:invoke_i,j:invoke_ii,J:invoke_iif,db:invoke_iiffi,xa:invoke_iiffiii,f:invoke_iii,ab:invoke_iiif,la:invoke_iiiff,oa:invoke_iiifi,g:invoke_iiii,u:invoke_iiiif,L:invoke_iiiiff,l:invoke_iiiii,hb:invoke_iiiiid,r:invoke_iiiiii,C:invoke_iiiiiii,H:invoke_iiiiiiii,t:invoke_iiiiiiiii,Ua:invoke_iiiiiiiiii,ga:invoke_iiiiiiiiiiii,va:invoke_iiiiiiiiiiiifiij,T:invoke_iij,wg:invoke_iijj,q:invoke_ij,jb:invoke_j,na:invoke_ji,s:invoke_jiii,ha:invoke_jiiii,aa:invoke_jiijj,N:invoke_jjji,k:invoke_v,zf:invoke_vff,b:invoke_vi,S:invoke_vid,X:invoke_vif,y:invoke_viff,G:invoke_viffff,ca:invoke_vifffff,Xa:invoke_viffffff,E:invoke_viffi,wa:invoke_viffiiiiiii,c:invoke_vii,_a:invoke_viidii,R:invoke_viif,F:invoke_viiff,Ra:invoke_viiffii,W:invoke_viifi,Aa:invoke_viififii,B:invoke_viifiiifi,d:invoke_viii,Fa:invoke_viiif,Ca:invoke_viiiff,z:invoke_viiiffi,O:invoke_viiiffiffii,P:invoke_viiififiiiiiiiiiiii,i:invoke_viiii,Za:invoke_viiiidididii,qa:invoke_viiiif,Da:invoke_viiiiff,ra:invoke_viiiiffi,Ba:invoke_viiiifi,h:invoke_viiiii,eb:invoke_viiiiif,Ya:invoke_viiiiiffiii,Ga:invoke_viiiiifi,m:invoke_viiiiii,Qa:invoke_viiiiiiff,p:invoke_viiiiiii,Z:invoke_viiiiiiii,Ia:invoke_viiiiiiiifij,ba:invoke_viiiiiiiii,V:invoke_viiiiiiiiii,gb:invoke_viiiiiiiiiifij,ya:invoke_viiiiiiiiiii,fa:invoke_viiiiiiiiiiiiiii,Sa:invoke_viiiiiiji,M:invoke_viiij,D:invoke_viiijii,U:invoke_viij,za:invoke_viijffiiii,o:invoke_viiji,Ma:invoke_viijiffi,ia:invoke_viijii,Ea:invoke_viijiii,ea:invoke_viijiiiif,ma:invoke_viijiiiii,pa:invoke_viijj,da:invoke_vij,$:invoke_viji,bb:invoke_vijififi,A:invoke_vijii,Ha:invoke_vijiifi,cb:invoke_vijiififi,_:invoke_vijiii,fb:invoke_vijijjiii,ja:invoke_vijjjj,Xb:invoke_vjii,ua:_llvm_eh_typeid_for,ub:_random_get};function invoke_vi(index,a1){var sp=stackSave();try{getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vii(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ii(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ij(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iijj(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ji(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_v(index){var sp=stackSave();try{getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiij(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vff(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viij(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiji(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiff(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiijj(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiji(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iij(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiff(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiif(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vid(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viji(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiiifiij(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vif(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiff(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffi(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vjii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiffii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiff(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiffi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiifij(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiifij(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijijjiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijiifi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viifiiifi(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiffi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiifi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiif(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiififiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiiiif(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffiffii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiff(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiff(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiif(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiifi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiffi(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viififii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_vijiififi(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijififi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijj(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jjji(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiif(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiifi(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iif(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viifi(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijffiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vij(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viif(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiffiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viff(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vifffff(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiidi(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viidii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiidididii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiiidi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiffiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiijii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffff(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffffff(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiif(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fdiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fdiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_dddd(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_dd(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijjjj(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_j(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiiiid(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_fiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_diii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_i(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function callMain(args=[]){var entryFunction=_main;args.unshift(thisProgram);var argc=args.length;var argv=stackAlloc((argc+1)*4);var argv_ptr=argv;for(var arg of args){HEAPU32[argv_ptr>>2]=stringToUTF8OnStack(arg);argv_ptr+=4}HEAPU32[argv_ptr>>2]=0;try{var ret=entryFunction(argc,argv);exitJS(ret,true);return ret}catch(e){return handleException(e)}}function run(args=arguments_){if(runDependencies>0){dependenciesFulfilled=run;return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();preMain();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();var noInitialRun=Module["noInitialRun"]||false;if(!noInitialRun)callMain(args);postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} ;return moduleRtn}})();if(typeof exports==="object"&&typeof module==="object"){module.exports=createGridaCanvas;module.exports.default=createGridaCanvas}else if(typeof define==="function"&&define["amd"])define([],()=>createGridaCanvas); diff --git a/crates/grida-canvas-wasm/lib/bin/grida_canvas_wasm.wasm b/crates/grida-canvas-wasm/lib/bin/grida_canvas_wasm.wasm index 52afceed3e..ced922a6b3 100755 --- a/crates/grida-canvas-wasm/lib/bin/grida_canvas_wasm.wasm +++ b/crates/grida-canvas-wasm/lib/bin/grida_canvas_wasm.wasm @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ce8af1ae2d05609b59c2551769ae7e43b3fafaefea3ec1dcbedc1eda5fcad78 -size 13295075 +oid sha256:91590f518a6549e86f79543e5a03a83b4bcd4ca8253536dea5f976990aeb5ab1 +size 13359335 diff --git a/crates/grida-canvas-wasm/package.json b/crates/grida-canvas-wasm/package.json index a48f5ad3de..7cf7216074 100644 --- a/crates/grida-canvas-wasm/package.json +++ b/crates/grida-canvas-wasm/package.json @@ -1,6 +1,6 @@ { "name": "@grida/canvas-wasm", - "version": "0.91.0-canary.14", + "version": "0.91.0-canary.15", "private": false, "description": "WASM bindings for Grida Canvas", "keywords": [