Skip to content

Commit 3c0fac3

Browse files
Merge pull request #511 from gridaco/canary
Grida Canvas - Outline Mode
2 parents d9fe80e + 46a7050 commit 3c0fac3

File tree

28 files changed

+1301
-180
lines changed

28 files changed

+1301
-180
lines changed

crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
version https://git-lfs.github.com/spec/v1
2-
oid sha256:a8761491f638c3b4591d3ab9c1d370eda5a29bfd327e2a935e0f9941c922b75b
3-
size 12873266
2+
oid sha256:dfacee3fc7189fd9e9ef8c69e97c71839200eb0fc53101a4186a3e085fc775bb
3+
size 12882632

crates/grida-canvas-wasm/lib/modules/canvas-bindings.d.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,7 @@ declare namespace canvas {
5050
cols: number,
5151
rows: number
5252
): void;
53-
_pointer_move(
54-
state: GridaCanvasApplicationPtr,
55-
x: number,
56-
y: number
57-
): void;
53+
_pointer_move(state: GridaCanvasApplicationPtr, x: number, y: number): void;
5854
_add_font(
5955
state: GridaCanvasApplicationPtr,
6056
family_ptr: number,
@@ -183,5 +179,15 @@ declare namespace canvas {
183179
state: GridaCanvasApplicationPtr,
184180
stable: boolean
185181
): void;
182+
183+
_runtime_renderer_set_render_policy_flags(
184+
state: GridaCanvasApplicationPtr,
185+
flags: number
186+
): void;
187+
188+
_runtime_renderer_set_outline_mode(
189+
state: GridaCanvasApplicationPtr,
190+
enable: boolean
191+
): void;
186192
}
187193
}

crates/grida-canvas-wasm/lib/modules/canvas.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,16 @@ export class Scene {
402402
this.module._runtime_renderer_set_pixel_preview_stable(this.appptr, stable);
403403
}
404404

405+
runtime_renderer_set_render_policy_flags(flags: number) {
406+
this._assertAlive();
407+
this.module._runtime_renderer_set_render_policy_flags(this.appptr, flags);
408+
}
409+
410+
runtime_renderer_set_outline_mode(enable: boolean) {
411+
this._assertAlive();
412+
this.module._runtime_renderer_set_outline_mode(this.appptr, enable);
413+
}
414+
405415
// ====================================================================================================
406416
// DEVTOOLS
407417
// ====================================================================================================

crates/grida-canvas-wasm/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@grida/canvas-wasm",
3-
"version": "0.90.0-canary.3",
3+
"version": "0.90.0-canary.4",
44
"private": false,
55
"description": "WASM bindings for Grida Canvas",
66
"keywords": [

crates/grida-canvas-wasm/src/wasm_application.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,38 @@ pub unsafe extern "C" fn runtime_renderer_set_pixel_preview_stable(
609609
}
610610
}
611611

612+
#[no_mangle]
613+
/// js::_runtime_renderer_set_render_policy_flags
614+
pub unsafe extern "C" fn runtime_renderer_set_render_policy_flags(
615+
app: *mut UnknownTargetApplication,
616+
flags: u32,
617+
) {
618+
if let Some(app) = app.as_mut() {
619+
app.runtime_renderer_set_render_policy_flags(flags);
620+
}
621+
}
622+
623+
#[no_mangle]
624+
/// js::_runtime_renderer_set_outline_mode
625+
///
626+
/// Back-compat shim: delegates to `runtime_renderer_set_render_policy_flags`.
627+
pub unsafe extern "C" fn runtime_renderer_set_outline_mode(
628+
app: *mut UnknownTargetApplication,
629+
enable: bool,
630+
) {
631+
use cg::runtime::render_policy::{
632+
RenderPolicy, RenderPolicyFlags, FLAG_RENDER_OUTLINES_ALWAYS,
633+
};
634+
if let Some(app) = app.as_mut() {
635+
let flags: RenderPolicyFlags = if enable {
636+
FLAG_RENDER_OUTLINES_ALWAYS
637+
} else {
638+
RenderPolicy::STANDARD.to_flags()
639+
};
640+
app.runtime_renderer_set_render_policy_flags(flags);
641+
}
642+
}
643+
612644
#[no_mangle]
613645
/// js::_devtools_rendering_set_show_fps_meter
614646
pub unsafe extern "C" fn devtools_rendering_set_show_fps_meter(
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
//! Golden: Outline Mode (Normal vs Outline)
2+
//!
3+
//! Renders the same scene twice:
4+
//! - Normal rendering (fills, strokes, effects)
5+
//! - Wireframe policy (geometry-only outlines, #444444)
6+
//!
7+
//! Output: `crates/grida-canvas/goldens/outline_mode.png`
8+
9+
use cg::cg::prelude::*;
10+
use cg::node::{
11+
factory::NodeFactory,
12+
scene_graph::{Parent, SceneGraph},
13+
schema::*,
14+
};
15+
use cg::runtime::{
16+
camera::Camera2D,
17+
render_policy::RenderPolicy,
18+
scene::{Backend, FrameFlushResult, Renderer, RendererOptions},
19+
};
20+
use skia_safe::{surfaces, Color, Font, Paint as SkPaint, Rect};
21+
22+
fn build_scene() -> Scene {
23+
let nf = NodeFactory::new();
24+
25+
// 1. Circle (filled)
26+
let mut circle = nf.create_ellipse_node();
27+
// Camera is center-based (world origin at viewport center), so use signed positions.
28+
circle.transform = math2::transform::AffineTransform::new(-32.0, -24.0, 0.0);
29+
circle.size = Size {
30+
width: 44.0,
31+
height: 44.0,
32+
};
33+
circle.set_fill(Paint::Solid(SolidPaint {
34+
color: CGColor::from_rgba(255, 0, 0, 255),
35+
blend_mode: BlendMode::default(),
36+
active: true,
37+
}));
38+
circle.strokes = Paints::new([Paint::Solid(SolidPaint::BLACK)]);
39+
circle.stroke_width = SingularStrokeWidth(Some(3.0));
40+
41+
// 2. Triangle (regular polygon, filled)
42+
let mut triangle = nf.create_regular_polygon_node();
43+
triangle.transform = math2::transform::AffineTransform::new(32.0, -24.0, 0.0);
44+
triangle.size = Size {
45+
width: 44.0,
46+
height: 44.0,
47+
};
48+
triangle.point_count = 3;
49+
triangle.set_fill(Paint::Solid(SolidPaint {
50+
color: CGColor::from_rgba(0, 153, 255, 255),
51+
blend_mode: BlendMode::default(),
52+
active: true,
53+
}));
54+
triangle.strokes = Paints::new([Paint::Solid(SolidPaint::BLACK)]);
55+
triangle.stroke_width = SingularStrokeWidth(Some(3.0));
56+
57+
// 3. Text (single text node)
58+
let mut text = nf.create_text_span_node();
59+
text.transform = math2::transform::AffineTransform::new(-52.0, 28.0, 0.0);
60+
text.text = "Outline".to_string();
61+
text.fills = Paints::new([Paint::Solid(SolidPaint::BLACK)]);
62+
text.text_style.font_size = 26.0;
63+
text.text_style.font_family = "Geist Mono".to_string();
64+
65+
let mut graph = SceneGraph::new();
66+
graph.append_children(
67+
vec![
68+
Node::Ellipse(circle),
69+
Node::RegularPolygon(triangle),
70+
Node::TextSpan(text),
71+
],
72+
Parent::Root,
73+
);
74+
75+
Scene {
76+
name: "Outline Mode Golden".to_string(),
77+
graph,
78+
background_color: Some(CGColor::WHITE),
79+
}
80+
}
81+
82+
fn render_with_outline_mode(outline: bool) -> skia_safe::Image {
83+
let (w, h) = (128u32, 128u32);
84+
let scene = build_scene();
85+
86+
let mut camera = Camera2D::new(Size {
87+
width: w as f32,
88+
height: h as f32,
89+
});
90+
camera.set_zoom(1.0);
91+
92+
let mut renderer = Renderer::new_with_options(
93+
Backend::new_from_raster(w as i32, h as i32),
94+
None,
95+
camera,
96+
RendererOptions {
97+
use_embedded_fonts: true,
98+
},
99+
);
100+
101+
renderer.load_scene(scene);
102+
renderer.set_render_policy(if outline {
103+
RenderPolicy::WIREFRAME_DEFAULT
104+
} else {
105+
RenderPolicy::STANDARD
106+
});
107+
renderer.queue_unstable();
108+
109+
match renderer.flush() {
110+
FrameFlushResult::OK(_) => {}
111+
_ => panic!("expected rendered frame"),
112+
}
113+
114+
let surface = unsafe { &mut *renderer.backend.get_surface() };
115+
let image = surface.image_snapshot();
116+
renderer.free();
117+
image
118+
}
119+
120+
fn main() {
121+
let normal = render_with_outline_mode(false);
122+
let outline = render_with_outline_mode(true);
123+
124+
let padding = 16.0;
125+
let label_h = 18.0;
126+
let out_w = (normal.width() as f32 * 2.0 + padding * 3.0) as i32;
127+
let out_h = (normal.height() as f32 + padding * 2.0 + label_h) as i32;
128+
129+
let mut surface = surfaces::raster_n32_premul((out_w, out_h)).expect("surface");
130+
let canvas = surface.canvas();
131+
canvas.clear(Color::WHITE);
132+
133+
let left_x = padding;
134+
let top_y = padding + label_h;
135+
let right_x = padding * 2.0 + normal.width() as f32;
136+
137+
// Draw images
138+
canvas.draw_image(&normal, (left_x, top_y), None);
139+
canvas.draw_image(&outline, (right_x, top_y), None);
140+
141+
// Labels
142+
let font = Font::new(
143+
cg::fonts::embedded::typeface(cg::fonts::embedded::geistmono::BYTES),
144+
12.0,
145+
);
146+
let mut paint = SkPaint::default();
147+
paint.set_color(Color::BLACK);
148+
paint.set_anti_alias(true);
149+
canvas.draw_str("Normal", (left_x, padding + 12.0), &font, &paint);
150+
canvas.draw_str("Outline Mode", (right_x, padding + 12.0), &font, &paint);
151+
152+
// Frame separators (optional)
153+
let mut border = SkPaint::default();
154+
border.set_color(Color::from_argb(255, 220, 220, 220));
155+
border.set_style(skia_safe::paint::Style::Stroke);
156+
border.set_stroke_width(1.0);
157+
canvas.draw_rect(
158+
Rect::from_xywh(left_x, top_y, normal.width() as f32, normal.height() as f32),
159+
&border,
160+
);
161+
canvas.draw_rect(
162+
Rect::from_xywh(
163+
right_x,
164+
top_y,
165+
normal.width() as f32,
166+
normal.height() as f32,
167+
),
168+
&border,
169+
);
170+
171+
let image = surface.image_snapshot();
172+
let data = image
173+
.encode(None, skia_safe::EncodedImageFormat::PNG, None)
174+
.expect("encode");
175+
std::fs::write(
176+
concat!(env!("CARGO_MANIFEST_DIR"), "/goldens/outline_mode.png"),
177+
data.as_bytes(),
178+
)
179+
.expect("write png");
180+
}
11.4 KB
Loading

crates/grida-canvas/src/cache/picture.rs

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,18 @@ impl Default for PictureCacheStrategy {
2121
#[derive(Debug, Clone)]
2222
pub struct PictureCache {
2323
strategy: PictureCacheStrategy,
24-
node_pictures: HashMap<NodeId, Picture>,
24+
/// Fast-path store for the default render variant (variant key = 0).
25+
default_store: HashMap<NodeId, Picture>,
26+
/// Store for non-default render variants (variant key != 0).
27+
variant_store: HashMap<(NodeId, u64), Picture>,
2528
}
2629

2730
impl PictureCache {
2831
pub fn new() -> Self {
2932
Self {
3033
strategy: PictureCacheStrategy::default(),
31-
node_pictures: HashMap::new(),
34+
default_store: HashMap::new(),
35+
variant_store: HashMap::new(),
3236
}
3337
}
3438

@@ -42,22 +46,44 @@ impl PictureCache {
4246
}
4347

4448
pub fn get_node_picture(&self, id: &NodeId) -> Option<&Picture> {
45-
self.node_pictures.get(id)
49+
self.default_store.get(id)
4650
}
4751

4852
pub fn set_node_picture(&mut self, id: NodeId, picture: Picture) {
49-
self.node_pictures.insert(id, picture);
53+
self.default_store.insert(id, picture);
54+
}
55+
56+
/// Lookup a picture for a node in a specific render variant.
57+
///
58+
/// - `variant_key = 0` resolves to the default fast-path store.
59+
pub fn get_node_picture_variant(&self, id: &NodeId, variant_key: u64) -> Option<&Picture> {
60+
if variant_key == 0 {
61+
return self.default_store.get(id);
62+
}
63+
self.variant_store.get(&(id.clone(), variant_key))
64+
}
65+
66+
/// Store a picture for a node in a specific render variant.
67+
///
68+
/// - `variant_key = 0` resolves to the default fast-path store.
69+
pub fn set_node_picture_variant(&mut self, id: NodeId, variant_key: u64, picture: Picture) {
70+
if variant_key == 0 {
71+
self.default_store.insert(id, picture);
72+
return;
73+
}
74+
self.variant_store.insert((id, variant_key), picture);
5075
}
5176

5277
pub fn len(&self) -> usize {
53-
self.node_pictures.len()
78+
self.default_store.len() + self.variant_store.len()
5479
}
5580

5681
pub fn depth(&self) -> Option<usize> {
5782
self.strategy.depth
5883
}
5984

6085
pub fn invalidate(&mut self) {
61-
self.node_pictures.clear();
86+
self.default_store.clear();
87+
self.variant_store.clear();
6288
}
6389
}

crates/grida-canvas/src/cache/scene.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,22 @@ impl SceneCache {
132132
self.picture.get_node_picture(id)
133133
}
134134

135+
/// Return a picture for a specific node in a specific render variant if cached.
136+
pub fn get_node_picture_variant(&self, id: &NodeId, variant_key: u64) -> Option<&Picture> {
137+
self.picture.get_node_picture_variant(id, variant_key)
138+
}
139+
135140
/// Store a picture for a node.
136141
pub fn set_node_picture(&mut self, id: NodeId, picture: Picture) {
137142
self.picture.set_node_picture(id, picture);
138143
}
139144

145+
/// Store a picture for a node in a specific render variant.
146+
pub fn set_node_picture_variant(&mut self, id: NodeId, variant_key: u64, picture: Picture) {
147+
self.picture
148+
.set_node_picture_variant(id, variant_key, picture);
149+
}
150+
140151
/// Query painter layer indices whose bounds intersect with the given rectangle.
141152
/// This includes layers that are:
142153
/// - Fully contained within the rectangle

0 commit comments

Comments
 (0)