Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions crates/grida-canvas-wasm/lib/bin/grida_canvas_wasm.wasm
Git LFS file not shown
2 changes: 1 addition & 1 deletion crates/grida-canvas-wasm/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@grida/canvas-wasm",
"version": "0.91.0-canary.15",
"version": "0.91.0-canary.16",
"private": false,
"description": "WASM bindings for Grida Canvas",
"keywords": [
Expand Down
6 changes: 6 additions & 0 deletions crates/grida-canvas-wasm/src/wasm_application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,9 @@ pub unsafe extern "C" fn surface_get_cursor(app: *const UnknownTargetApplication
cg::surface::CursorIcon::Grabbing => 3,
cg::surface::CursorIcon::Crosshair => 4,
cg::surface::CursorIcon::Move => 5,
// Resize/Rotate cursors are handled by the native editor surface.
// The web editor manages its own CSS cursors — map to default.
cg::surface::CursorIcon::Resize(_) | cg::surface::CursorIcon::Rotate(_) => 0,
},
None => 0,
}
Expand Down Expand Up @@ -563,6 +566,8 @@ pub unsafe extern "C" fn set_surface_overlay_config(
show_size_meter: bool,
#[serde(default)]
show_frame_titles: bool,
#[serde(default)]
show_selection_handles: bool,
}
fn default_dpr() -> f32 {
1.0
Expand All @@ -574,6 +579,7 @@ pub unsafe extern "C" fn set_surface_overlay_config(
text_baseline_decoration: cfg.text_baseline_decoration,
show_size_meter: cfg.show_size_meter,
show_frame_titles: cfg.show_frame_titles,
show_selection_handles: cfg.show_selection_handles,
};
}
}
Expand Down
31 changes: 26 additions & 5 deletions crates/grida-canvas/examples/fixtures/l0_image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,42 @@ pub fn build() -> Scene {
image_paint_with(img(), ImagePaintFit::Fit(BoxFit::Contain)),
);

// Transform fit (scale + offset)
// Transform fit (zoom 1.5× centered with slight offset)
let r3 = rect(
gap * 2.0,
0.0,
s,
s,
image_paint_with(
img(),
ImagePaintFit::Transform(AffineTransform::new(10.0, 20.0, 0.0)),
ImagePaintFit::Transform(AffineTransform {
matrix: [[1.5, 0.0, -0.25], [0.0, 1.5, -0.15]],
}),
),
);

// Transform fit with 15° rotation
let r4 = {
let deg: f32 = 15.0;
let rad = deg.to_radians();
let (sin, cos) = rad.sin_cos();
rect(
gap * 3.0,
0.0,
s,
s,
image_paint_with(
img(),
ImagePaintFit::Transform(AffineTransform {
matrix: [[cos, -sin, 0.0], [sin, cos, 0.0]],
}),
),
)
};

// Quarter turns + alignment + Screen blend
let r4 = rect(
gap * 3.0,
let r5 = rect(
gap * 4.0,
0.0,
s,
s,
Expand All @@ -56,5 +77,5 @@ pub fn build() -> Scene {
}),
);

flat_scene("L0 Image", vec![r1, r2, r3, r4])
flat_scene("L0 Image", vec![r1, r2, r3, r4, r5])
}
5 changes: 5 additions & 0 deletions crates/grida-canvas/src/cache/geometry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,11 @@ impl GeometryCache {
}
}

/// Access the full geometry entry for a node.
pub fn get_entry(&self, id: &NodeId) -> Option<&GeometryEntry> {
self.entries.get(id)
}

pub fn get_transform(&self, id: &NodeId) -> Option<AffineTransform> {
self.entries.get(id).map(|e| e.transform)
}
Expand Down
148 changes: 0 additions & 148 deletions crates/grida-canvas/src/devtools/hit_overlay.rs

This file was deleted.

1 change: 0 additions & 1 deletion crates/grida-canvas/src/devtools/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
pub mod fps_overlay;
pub mod hit_overlay;
pub mod ruler_overlay;
pub mod stats_overlay;
pub mod stroke_overlay;
Expand Down
73 changes: 71 additions & 2 deletions crates/grida-canvas/src/devtools/surface_overlay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ pub struct SurfaceOverlayConfig {
pub show_size_meter: bool,
/// Show type labels above root frames and selected nodes.
pub show_frame_titles: bool,
/// Show selection handles (resize knobs, rotation zones) and per-node
/// bounding rects on selected shapes. Defaults to `false`.
///
/// Enabled by the native host (`grida-dev`). The web side keeps this
/// off because the JS editor renders its own overlay handles.
pub show_selection_handles: bool,
}

impl Default for SurfaceOverlayConfig {
Expand All @@ -40,6 +46,7 @@ impl Default for SurfaceOverlayConfig {
text_baseline_decoration: false,
show_size_meter: false,
show_frame_titles: false,
show_selection_handles: false,
}
}
}
Expand Down Expand Up @@ -76,11 +83,18 @@ impl SurfaceOverlay {
}
}

// Draw selection outlines
// Draw selection outlines.
//
// The selection highlight model is:
// shape outline (the actual path) + axis-aligned bounding rect
// + text baseline decoration (when enabled).
//
// This differs from hover which only shows the shape outline (or
// text baseline for text nodes).
let sel_count = surface.selection.len();
if sel_count >= 1 {
for id in surface.selection.iter() {
// Selection: always draw bounding rect
// Draw the shape outline (same as hover but at full alpha).
Self::draw_node_outline(
canvas,
id,
Expand All @@ -91,6 +105,20 @@ impl SurfaceOverlay {
false,
fonts,
);
// Draw the axis-aligned bounding rect for each selected node.
// This gives the "declarative box" look that shows the layout
// bounds in addition to the actual shape path.
// Only drawn when selection handles are enabled (native).
if config.show_selection_handles {
Self::draw_node_bounding_rect(
canvas,
id,
&view_sk,
cache,
SELECTION_COLOR,
1.5,
);
}
// Selection: additionally draw text baseline decoration
if use_text_baseline {
Self::draw_text_baseline(canvas, id, &view_sk, cache, SELECTION_COLOR, fonts);
Expand Down Expand Up @@ -164,6 +192,47 @@ impl SurfaceOverlay {
canvas.draw_path(&path, &paint);
}

/// Draw the axis-aligned bounding rect for a single node in screen space.
///
/// For shapes whose outline *is* their bounding rect (rectangles), this
/// effectively draws a second outline on top — visually identical, so
/// there's no double-stroke artifact. For non-rectangular shapes (ellipses,
/// polygons, paths) the bounding rect provides the "declarative box" frame
/// around the shape.
fn draw_node_bounding_rect(
canvas: &Canvas,
id: &crate::node::schema::NodeId,
view_sk: &Matrix,
cache: &SceneCache,
color: Color,
stroke_width: f32,
) {
let world_bounds = match cache.geometry.get_world_bounds(id) {
Some(b) => b,
None => return,
};

let p1 = view_sk.map_point((world_bounds.x, world_bounds.y));
let p2 = view_sk.map_point((
world_bounds.x + world_bounds.width,
world_bounds.y + world_bounds.height,
));
let screen_rect = skia_safe::Rect::from_ltrb(
p1.x.min(p2.x),
p1.y.min(p2.y),
p1.x.max(p2.x),
p1.y.max(p2.y),
);

let mut paint = Paint::default();
paint.set_color(color);
paint.set_style(PaintStyle::Stroke);
paint.set_stroke_width(stroke_width);
paint.set_anti_alias(true);

canvas.draw_rect(screen_rect, &paint);
}

/// Draw text baseline decoration for a node (no-op if not a text layer).
fn draw_text_baseline(
canvas: &Canvas,
Expand Down
Loading
Loading