canary: rendering fixes, Figma import improvements, and frame plan optimizations#597
canary: rendering fixes, Figma import improvements, and frame plan optimizations#597softmarshmallow merged 19 commits intomainfrom
Conversation
- Updated the color determination logic in the SurfaceUI rendering process to account for both selected and hovered states, improving visual feedback for user interactions. - The color now changes to ACCENT_COLOR when a node is either selected or hovered, enhancing the user experience during surface interactions.
- Enhanced the canvas extraction and conversion logic to include background color properties, allowing for more accurate representation of Figma designs. - Integrated the `@grida/color` library to handle background color formatting during the conversion process. - Updated relevant functions to ensure background colors are preserved and correctly passed to the resulting Grida documents.
- Added a default font size constant to ensure consistent letter spacing calculations when converting styles from Figma. - Updated the letter spacing logic to account for font size, ensuring accurate representation in the resulting Grida documents. - Enhanced handling of letter spacing units to improve compatibility with the Figma API specifications.
…a import - Implemented a new function to extract individual stroke weights from Figma's REST API, allowing for per-side stroke width properties. - Updated the document generation logic to include these stroke widths in the resulting Grida documents. - Added tests to verify the correct mapping of individual stroke weights for both frames and rectangles.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedPull request was closed or merged during review WalkthroughAdds I/O documentation for Figma/Grida/SVG, extends Figma REST/CLI inputs and background/stroke handling, implements text-decoration tests, introduces deferred render planning and a zoom image cache fast path, expands benchmarking scenarios/timings, and updates related rendering, layout, and tooling files. Changes
Sequence Diagram(s)sequenceDiagram
participant UI as Client/UI
participant Renderer
participant Cache as ZoomImageCache
participant GPU
UI->>Renderer: request render_frame(view_matrix, unstable_flag)
Renderer->>Cache: check zoom cache validity (view_matrix, zoom)
alt cache hit (zoom/pan fast path)
Cache-->>Renderer: cached_snapshot + transform
Renderer->>GPU: blit cached_snapshot with residual transform
GPU-->>Renderer: blit complete
else cache miss
Renderer->>Renderer: materialize PlanState -> FramePlan (R-tree, cull, sort)
Renderer->>GPU: full frame draw (compositor, flush)
GPU-->>Renderer: full draw complete
Renderer->>Cache: capture snapshot if eligible (after full draw)
end
Renderer-->>UI: present frame (FrameQuality stable/unstable)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
- Introduced unit tests for JSON roundtrip functionality of text decorations, specifically testing underline and none values. - Enhanced existing roundtrip tests to verify that text decoration properties are correctly preserved during serialization and deserialization. - Updated format roundtrip tests to include various text decoration styles, ensuring comprehensive coverage for text node properties.
- Updated the fig2grida CLI to accept various input formats including .fig, .json, .json.gz, and .zip. - Improved help documentation to reflect the new input options and usage examples. - Added logic to handle gzip decompression for .json.gz files. - Refactored output path determination to accommodate different input file extensions.
compute_clip_path() returns clip paths in the node's local coordinate space. For regular Draw layers this is correct because draw_layer() applies the world transform before clipping. However, draw_render_surface() applies the clip path directly in world space without a preceding transform, causing containers with effects (drop shadow, blur) to clip their children at the wrong position. Transform the clip path from local to world space before storing it on the PainterRenderSurface so it matches the coordinate space where draw_render_surface applies it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Introduced comprehensive documentation for the io-figma, io-grida, and io-svg skills, detailing their architecture, usage, and key files. - Included guidelines for common tasks, input formats, and verification processes to assist developers in working with Figma and Grida file formats. - Added references to relevant files and tests to enhance understanding and facilitate development.
…ved rendering performance - Introduced `DeferredPlan` and `PlanState` to optimize frame rendering by deferring expensive R-tree queries on cache-hit frames. - Added `ZoomImageCache` to store GPU snapshots for zoom operations, allowing for efficient texture scaling during active zooming. - Updated rendering logic to utilize the new caching mechanisms, significantly reducing frame processing time for large scenes. - Enhanced benchmark reporting to include detailed performance metrics for pan and zoom operations, ensuring better insights into rendering efficiency.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c0d7164aec
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if !plan.stable | ||
| && self.backend.is_gpu() | ||
| && self.zoom_image_cache.is_some() | ||
| && plan.camera_change != CameraChangeKind::PanOnly |
There was a problem hiding this comment.
Exclude non-camera invalidations from zoom-cache blits
On GPU backends this condition also matches CameraChangeKind::None, so an unstable redraw triggered by a scene mutation can reuse the previous frame snapshot instead of repainting the scene. text_edit_sync_display_text() is one concrete path: it updates the layer text, calls queue_unstable(), leaves the camera unchanged, and this branch will blit the stale zoom_image_cache, so the edited text does not appear until a later stable frame.
Useful? React with 👍 / 👎.
| if !plan.stable | ||
| && self.backend.is_gpu() | ||
| && self.zoom_image_cache.is_some() | ||
| && plan.camera_change != CameraChangeKind::PanOnly |
There was a problem hiding this comment.
Avoid zoom-cache blits on zoom-out frames
On GPU backends this fast path now runs for ZoomOut/PanAndZoom(false) as well, but a zoom-out reveals world content that was outside the cached viewport. try_zoom_cache_blit() only scales the old snapshot and clears the newly exposed margins to the scene background, so nearby nodes disappear while the user is zooming out and only pop back on the settle frame.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
crates/grida-dev/src/bench/report.rs (1)
45-59:ScenarioParamsis already too lossy for the new scenario kinds.
circle_panhas to stash radius underspeed, whilezigzag,pan_with_settle, andrealtimedrop key knobs entirely (dy, pause/segment sizes, scroll cadence, etc.). Once serialized, those runs are no longer self-describing or reproducible. Consider a per-kind enum or adding the missing fields before tooling starts depending on this shape.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/grida-dev/src/bench/report.rs` around lines 45 - 59, ScenarioParams is too generic and loses per-scenario knobs (e.g., circle_pan stashes radius in speed and zigzag / pan_with_settle / realtime drop dy, pause/segment sizes, scroll cadence), so change the shape to be per-kind and serializable: replace or augment the flat ScenarioParams with a tagged enum (e.g., ScenarioParams::CirclePan { radius: f32, ... }, ::ZigZag { dy: f32, segment_pause: f32, ... }, ::PanWithSettle { settle_time: f32, ... }, ::Realtime { cadence_ms: u32, ... }) or add all missing optional fields with clear names and serde tags; update serde::Serialize derive accordingly so each run remains self-describing and reproducible, and update usages that construct ScenarioParams to use the new enum variants or new fields (look for ScenarioParams construction sites and consumers).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.agents/skills/io-figma/SKILL.md:
- Around line 18-30: The fenced architecture block containing the lines starting
".fig bytes / HTML clipboard" and listing iofigma.fromKiwi*(),
iofigma.fromRest*(), fig2grida-core.ts and fig2grida.ts should include a
language tag to satisfy markdownlint MD040; update the opening fence from ``` to
```text (or another appropriate language) so the block is explicitly marked,
leaving the content unchanged.
In `@crates/grida-canvas/src/runtime/scene.rs`:
- Around line 965-969: The current fast-path condition treats
CameraChangeKind::None as a “zoom-like” change and can incorrectly reuse
zoom_image_cache during non-camera invalidations; update the condition around
the zoom_image_cache check (the block using camera_change and zoom_image_cache)
to explicitly exclude CameraChangeKind::None (e.g., require camera_change !=
CameraChangeKind::None && camera_change != CameraChangeKind::PanOnly) so only
actual camera/zoom gestures reuse the cache; apply the same change to the other
analogous checks referenced (the blocks around lines marked 1101-1105 and
1409-1416).
In `@crates/grida-dev/src/bench/runner.rs`:
- Around line 729-977: run_scenarios currently leaves camera translations from
prior passes, causing later scenarios to start with a moved viewport; fix by
restoring the original fitted camera transform before each scenario iteration
(e.g. save the fitted transform after initial fit and call
renderer.camera.set_transform(saved) or call fit_camera_to_scene(renderer) at
the start of each scenario loop) so that functions like run_pan_pass_at,
run_circle_pan_pass, run_zigzag_pan_pass, run_zoom_pass_at,
run_pan_with_settle_pass and run_realtime_pan_pass always start from the same
baseline; apply this restore before any warmup/translate calls (before
queue_stable/queue_unstable and before warmup()) in each scenario block.
In `@docs/wg/feat-2d/optimization.md`:
- Around line 744-748: Locate the paragraph containing "The zoom image cache..."
that references "item 17" and update the cross-reference so it points to the
actual item that describes border strip rasterization in the settle phase
(search for the section or list item that contains "border strip rasterization"
or "settle phase" and replace "item 17" with that item’s correct identifier or
an explicit anchor link). Ensure the text still reads naturally (e.g., "see item
X" or "see the 'border strip rasterization' section") so readers can find the
follow-up.
In `@packages/grida-canvas-io-figma/fig2grida.ts`:
- Around line 5-16: In main(), detect the input type (by extension: .json,
.json.gz, .zip vs .fig) and reject combinations where REST-style inputs are
passed with CLI flags meant for Kiwi parsing (specifically options.info and
options.pages); if the input is a REST format and either options.info or
options.pages is set, print a clear error and exit (or return non-zero). Update
the validation near where options are read in main() to check options.info and
options.pages against the inferred REST path and reference the REST handling in
fig2grida-core.ts so callers know these flags are unsupported for REST inputs;
alternatively, if you prefer to support them, wire options.pages into the REST
conversion path in fig2grida-core.ts instead of silently ignoring it (but do not
leave the current behavior).
In `@packages/grida-canvas-io-figma/lib.ts`:
- Around line 1593-1597: The code is dividing an already-normalized Kiwi
letterSpacing a second time; in the object property assignment for
letter_spacing replace the expression that divides node.style.letterSpacing by
(node.style.fontSize || DEFAULT_FONT_SIZE) with just node.style.letterSpacing
(or 0 when missing) so you don't normalize pixels twice — i.e., update the
letter_spacing assignment that currently reads node.style.letterSpacing ?
node.style.letterSpacing / (node.style.fontSize || DEFAULT_FONT_SIZE) : 0 to
return the raw node.style.letterSpacing (using DEFAULT_FONT_SIZE only for
fallback presence checks) and let the single normalization occur in the existing
Kiwi normalization logic.
---
Nitpick comments:
In `@crates/grida-dev/src/bench/report.rs`:
- Around line 45-59: ScenarioParams is too generic and loses per-scenario knobs
(e.g., circle_pan stashes radius in speed and zigzag / pan_with_settle /
realtime drop dy, pause/segment sizes, scroll cadence), so change the shape to
be per-kind and serializable: replace or augment the flat ScenarioParams with a
tagged enum (e.g., ScenarioParams::CirclePan { radius: f32, ... }, ::ZigZag {
dy: f32, segment_pause: f32, ... }, ::PanWithSettle { settle_time: f32, ... },
::Realtime { cadence_ms: u32, ... }) or add all missing optional fields with
clear names and serde tags; update serde::Serialize derive accordingly so each
run remains self-describing and reproducible, and update usages that construct
ScenarioParams to use the new enum variants or new fields (look for
ScenarioParams construction sites and consumers).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 7119f29a-ec20-4685-8550-d806e92d3da7
⛔ Files ignored due to path filters (1)
crates/grida-canvas-wasm/lib/bin/grida_canvas_wasm.wasmis excluded by!**/*.wasm
📒 Files selected for processing (22)
.agents/skills/io-figma/SKILL.md.agents/skills/io-figma/scripts/figma_archive.py.agents/skills/io-grida/SKILL.md.agents/skills/io-svg/SKILL.mdcrates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.jscrates/grida-canvas-wasm/package.jsoncrates/grida-canvas/src/io/io_grida.rscrates/grida-canvas/src/painter/layer.rscrates/grida-canvas/src/runtime/scene.rscrates/grida-canvas/src/surface/ui/render.rscrates/grida-canvas/src/window/application.rscrates/grida-canvas/tests/fbs_roundtrip.rscrates/grida-dev/src/bench/report.rscrates/grida-dev/src/bench/runner.rscrates/grida-dev/src/platform/native_application.rsdocs/wg/feat-2d/optimization.mdeditor/app/(embed)/embed/v1/debug/page.tsxpackages/grida-canvas-io-figma/__tests__/iofigma.rest-api.no-geometry.test.tspackages/grida-canvas-io-figma/fig2grida-core.tspackages/grida-canvas-io-figma/fig2grida.tspackages/grida-canvas-io-figma/lib.tspackages/grida-canvas-io/__tests__/format-roundtrip.test.ts
👮 Files not reviewed due to content moderation or server errors (6)
- .agents/skills/io-figma/scripts/figma_archive.py
- editor/app/(embed)/embed/v1/debug/page.tsx
- .agents/skills/io-svg/SKILL.md
- .agents/skills/io-grida/SKILL.md
- packages/grida-canvas-io/tests/format-roundtrip.test.ts
- crates/grida-canvas/src/io/io_grida.rs
| fn run_scenarios( | ||
| renderer: &mut cg::runtime::scene::Renderer, | ||
| frames: u32, | ||
| fit_zoom: f32, | ||
| ) -> Vec<ScenarioResult> { | ||
| let (pan_scenarios, zoom_scenarios) = standard_scenarios(fit_zoom); | ||
| let mut results = Vec::new(); | ||
|
|
||
| for ps in &pan_scenarios { | ||
| renderer.camera.set_zoom(ps.zoom); | ||
| // Warmup at this zoom | ||
| renderer.queue_stable(); | ||
| let _ = renderer.flush(); | ||
| for _ in 0..5 { | ||
| renderer.camera.translate(1.0, 0.0); | ||
| renderer.queue_unstable(); | ||
| let _ = renderer.flush(); | ||
| } | ||
|
|
||
| let stats = run_pan_pass_at(renderer, frames, ps.dx); | ||
| results.push(ScenarioResult { | ||
| name: ps.name.to_string(), | ||
| kind: "pan".to_string(), | ||
| params: ScenarioParams { | ||
| speed: Some(ps.dx), | ||
| zoom: Some(ps.zoom), | ||
| zoom_min: None, | ||
| zoom_max: None, | ||
| }, | ||
| stats, | ||
| }); | ||
| } | ||
| let zoom_wall = zoom_start.elapsed(); | ||
|
|
||
| if zoom_times.is_empty() { | ||
| return ZoomStats { avg_us: 0, fps: 0.0, p50_us: 0, p95_us: 0, p99_us: 0 }; | ||
| // Circle pan scenarios: realistic trackpad gesture. | ||
| // Small radius = tight circles (edges constantly change). | ||
| // Large radius = wide sweeping gesture (more cache misses). | ||
| struct CirclePanScenario { | ||
| name: &'static str, | ||
| radius: f32, | ||
| zoom: f32, | ||
| } | ||
|
|
||
| zoom_times.sort(); | ||
| let n = zoom_times.len(); | ||
| let avg = zoom_wall.as_micros() as u64 / n as u64; | ||
| ZoomStats { | ||
| avg_us: avg, | ||
| fps: 1_000_000.0 / avg as f64, | ||
| p50_us: zoom_times[n / 2], | ||
| p95_us: zoom_times[n * 95 / 100], | ||
| p99_us: zoom_times[n * 99 / 100], | ||
| let zoomed_in_c = (fit_zoom * 4.0).min(10.0); | ||
| let circle_scenarios = vec![ | ||
| CirclePanScenario { name: "circle_small_fit", radius: 200.0, zoom: fit_zoom }, | ||
| CirclePanScenario { name: "circle_large_fit", radius: 2000.0, zoom: fit_zoom }, | ||
| CirclePanScenario { name: "circle_small_zoomed", radius: 200.0, zoom: zoomed_in_c }, | ||
| CirclePanScenario { name: "circle_large_zoomed", radius: 2000.0, zoom: zoomed_in_c }, | ||
| ]; | ||
|
|
||
| for cs in &circle_scenarios { | ||
| renderer.camera.set_zoom(cs.zoom); | ||
| renderer.queue_stable(); | ||
| let _ = renderer.flush(); | ||
| // Small warmup | ||
| for _ in 0..3 { | ||
| renderer.camera.translate(1.0, 0.0); | ||
| renderer.queue_unstable(); | ||
| let _ = renderer.flush(); | ||
| } | ||
|
|
||
| let stats = run_circle_pan_pass(renderer, frames, cs.radius); | ||
| results.push(ScenarioResult { | ||
| name: cs.name.to_string(), | ||
| kind: "circle_pan".to_string(), | ||
| params: ScenarioParams { | ||
| speed: Some(cs.radius), | ||
| zoom: Some(cs.zoom), | ||
| zoom_min: None, | ||
| zoom_max: None, | ||
| }, | ||
| stats, | ||
| }); | ||
| } | ||
|
|
||
| // Zigzag pan scenarios: diagonal back-and-forth like reading a document. | ||
| // "fast" = continuous motion, no pauses. | ||
| // "slow" = pause between each zig/zag segment (settle frames fire, cache goes cold). | ||
| struct ZigzagScenario { | ||
| name: &'static str, | ||
| dx: f32, | ||
| dy: f32, | ||
| segment_frames: u32, | ||
| pause_frames: u32, | ||
| zoom: f32, | ||
| } | ||
|
|
||
| let zoomed_in_z = (fit_zoom * 4.0).min(10.0); | ||
| let zigzag_scenarios = vec![ | ||
| // Fast zigzag: continuous diagonal sweeps, no pauses | ||
| ZigzagScenario { | ||
| name: "zigzag_fast_fit", dx: 30.0, dy: 5.0, | ||
| segment_frames: 20, pause_frames: 0, zoom: fit_zoom, | ||
| }, | ||
| ZigzagScenario { | ||
| name: "zigzag_fast_zoomed", dx: 30.0, dy: 5.0, | ||
| segment_frames: 20, pause_frames: 0, zoom: zoomed_in_z, | ||
| }, | ||
| // Slow zigzag: zig, stop (settle fires), zag, stop (settle fires) | ||
| // pause_frames=3 simulates ~3 settle frames during the "reading" pause | ||
| ZigzagScenario { | ||
| name: "zigzag_slow_fit", dx: 10.0, dy: 3.0, | ||
| segment_frames: 15, pause_frames: 3, zoom: fit_zoom, | ||
| }, | ||
| ZigzagScenario { | ||
| name: "zigzag_slow_zoomed", dx: 10.0, dy: 3.0, | ||
| segment_frames: 15, pause_frames: 3, zoom: zoomed_in_z, | ||
| }, | ||
| ]; | ||
|
|
||
| for zz in &zigzag_scenarios { | ||
| renderer.camera.set_zoom(zz.zoom); | ||
| renderer.queue_stable(); | ||
| let _ = renderer.flush(); | ||
| for _ in 0..3 { | ||
| renderer.camera.translate(1.0, 0.0); | ||
| renderer.queue_unstable(); | ||
| let _ = renderer.flush(); | ||
| } | ||
|
|
||
| let stats = run_zigzag_pan_pass( | ||
| renderer, frames, zz.dx, zz.dy, zz.segment_frames, zz.pause_frames, | ||
| ); | ||
| results.push(ScenarioResult { | ||
| name: zz.name.to_string(), | ||
| kind: "zigzag".to_string(), | ||
| params: ScenarioParams { | ||
| speed: Some(zz.dx), | ||
| zoom: Some(zz.zoom), | ||
| zoom_min: None, | ||
| zoom_max: None, | ||
| }, | ||
| stats, | ||
| }); | ||
| } | ||
|
|
||
| for zs in &zoom_scenarios { | ||
| let stats = run_zoom_pass_at(renderer, frames, zs.step, zs.z_min, zs.z_max); | ||
| results.push(ScenarioResult { | ||
| name: zs.name.to_string(), | ||
| kind: "zoom".to_string(), | ||
| params: ScenarioParams { | ||
| speed: Some(zs.step), | ||
| zoom: None, | ||
| zoom_min: Some(zs.z_min), | ||
| zoom_max: Some(zs.z_max), | ||
| }, | ||
| stats, | ||
| }); | ||
| } | ||
|
|
||
| // Settle-interleaved pan scenarios: simulate native viewer's settle countdown. | ||
| // settle_interval=12 matches the native viewer's 12-tick countdown at 240Hz (~50ms). | ||
| struct SettlePanScenario { | ||
| name: &'static str, | ||
| dx: f32, | ||
| zoom: f32, | ||
| settle_interval: u32, | ||
| } | ||
|
|
||
| let zoomed_in_s = (fit_zoom * 4.0).min(10.0); | ||
| let settle_scenarios = vec![ | ||
| SettlePanScenario { name: "pan_settle_slow_fit", dx: 2.0, zoom: fit_zoom, settle_interval: 12 }, | ||
| SettlePanScenario { name: "pan_settle_fast_fit", dx: 50.0, zoom: fit_zoom, settle_interval: 12 }, | ||
| SettlePanScenario { name: "pan_settle_slow_zoomed", dx: 2.0, zoom: zoomed_in_s, settle_interval: 12 }, | ||
| SettlePanScenario { name: "pan_settle_fast_zoomed", dx: 50.0, zoom: zoomed_in_s, settle_interval: 12 }, | ||
| ]; | ||
|
|
||
| for ss in &settle_scenarios { | ||
| renderer.camera.set_zoom(ss.zoom); | ||
| renderer.queue_stable(); | ||
| let _ = renderer.flush(); | ||
| for _ in 0..5 { | ||
| renderer.camera.translate(1.0, 0.0); | ||
| renderer.queue_unstable(); | ||
| let _ = renderer.flush(); | ||
| } | ||
|
|
||
| let stats = run_pan_with_settle_pass(renderer, frames, ss.dx, ss.settle_interval); | ||
| results.push(ScenarioResult { | ||
| name: ss.name.to_string(), | ||
| kind: "pan_with_settle".to_string(), | ||
| params: ScenarioParams { | ||
| speed: Some(ss.dx), | ||
| zoom: Some(ss.zoom), | ||
| zoom_min: None, | ||
| zoom_max: None, | ||
| }, | ||
| stats, | ||
| }); | ||
| } | ||
|
|
||
| // Realtime event loop simulation scenarios. | ||
| // These use real sleep() and simulate the native viewer's 240Hz tick | ||
| // thread + settle countdown, producing timings that match actual UX. | ||
| struct RealtimeScenario { | ||
| name: &'static str, | ||
| scroll_interval_ms: f64, | ||
| dx: f32, | ||
| dy: f32, | ||
| zoom: f32, | ||
| duration_ms: f64, | ||
| } | ||
|
|
||
| let zoomed_in_rt = (fit_zoom * 4.0).min(10.0); | ||
| let realtime_scenarios = vec![ | ||
| RealtimeScenario { | ||
| name: "rt_pan_fast_fit", scroll_interval_ms: 8.0, | ||
| dx: 2.0, dy: 0.0, zoom: fit_zoom, duration_ms: 2000.0, | ||
| }, | ||
| RealtimeScenario { | ||
| name: "rt_pan_slow_fit", scroll_interval_ms: 100.0, | ||
| dx: 5.0, dy: 0.0, zoom: fit_zoom, duration_ms: 2000.0, | ||
| }, | ||
| RealtimeScenario { | ||
| name: "rt_pan_fast_zoomed", scroll_interval_ms: 8.0, | ||
| dx: 2.0, dy: 0.0, zoom: zoomed_in_rt, duration_ms: 2000.0, | ||
| }, | ||
| RealtimeScenario { | ||
| name: "rt_pan_slow_zoomed", scroll_interval_ms: 100.0, | ||
| dx: 5.0, dy: 0.0, zoom: zoomed_in_rt, duration_ms: 2000.0, | ||
| }, | ||
| ]; | ||
|
|
||
| for rt in &realtime_scenarios { | ||
| renderer.camera.set_zoom(rt.zoom); | ||
| renderer.queue_stable(); | ||
| let _ = renderer.flush(); | ||
| warmup(renderer); | ||
|
|
||
| let stats = run_realtime_pan_pass( | ||
| renderer, rt.scroll_interval_ms, | ||
| rt.dx, rt.dy, rt.duration_ms, 12, | ||
| ); | ||
| results.push(ScenarioResult { | ||
| name: rt.name.to_string(), | ||
| kind: "realtime".to_string(), | ||
| params: ScenarioParams { | ||
| speed: Some(rt.dx), | ||
| zoom: Some(rt.zoom), | ||
| zoom_min: None, | ||
| zoom_max: None, | ||
| }, | ||
| stats, | ||
| }); | ||
| } | ||
|
|
||
| results | ||
| } |
There was a problem hiding this comment.
Reset the camera before every scenario.
Right now each pass inherits the previous pass’s translation. That makes the matrix order-dependent, and some scenarios here are explicitly non-zero-sum (zigzag keeps adding dy, realtime only pans forward, even the warmups translate right), so later runs can benchmark a very different viewport than the original fit.
Save the fitted transform once and restore it before each scenario, or call fit_camera_to_scene() per scenario before applying that scenario’s zoom.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/grida-dev/src/bench/runner.rs` around lines 729 - 977, run_scenarios
currently leaves camera translations from prior passes, causing later scenarios
to start with a moved viewport; fix by restoring the original fitted camera
transform before each scenario iteration (e.g. save the fitted transform after
initial fit and call renderer.camera.set_transform(saved) or call
fit_camera_to_scene(renderer) at the start of each scenario loop) so that
functions like run_pan_pass_at, run_circle_pan_pass, run_zigzag_pan_pass,
run_zoom_pass_at, run_pan_with_settle_pass and run_realtime_pan_pass always
start from the same baseline; apply this restore before any warmup/translate
calls (before queue_stable/queue_unstable and before warmup()) in each scenario
block.
- Expanded the SKILL.md documentation to include detailed descriptions of new performance metrics for pan and zoom operations. - Introduced an expanded scenario matrix covering various gesture types and their impact on rendering performance. - Clarified the significance of `realtime` scenarios to better align benchmark results with user experience. - Added guidelines for ensuring stable frames recapture caches to maintain high frame rates during rendering.
- Added validation to reject --info and --pages flags when the input file is not a .fig format, ensuring proper usage of the CLI. - Updated error messages to inform users about the limitations of these flags with non-.fig inputs.
- Updated the camera change conditions to utilize a more precise check for zoom changes, enhancing the efficiency of the zoom image cache. - Revised documentation to clarify the handling of pan-only and no-change frames in relation to zoom caching, ensuring better performance during rendering. - Adjusted performance metrics in the documentation to reflect the impact of these changes on rendering speed.
- Simplified the `extractCanvases` function to improve readability and maintainability. - Consolidated canvas conversion logic into a new `convertRootsToPackedScene` function, enhancing code organization. - Introduced an `emptyPackedScene` utility to standardize the creation of empty scene documents. - Updated context handling for merging nodes to prevent ID collisions across canvases.
- Updated `figma_archive.py` to support optional export of node renderings as PNGs, improving the archiving process for Figma files. - Enhanced documentation to clarify usage and output structure, including the new `--export` flag for exporting images. - Revised README to reflect changes in the tool's capabilities and usage instructions. - Added logic to handle export settings for nodes, ensuring only nodes with export presets are processed.
- Revised module documentation for the layout engine to clarify its purpose and functionality, emphasizing the handling of layout nodes and virtual grouping nodes. - Improved comments in the code to enhance understanding of layout computation and schema-position correction. - Streamlined the extraction of layouts for all nodes, ensuring clarity on how Taffy and non-Taffy nodes are processed. - Ensured that the layout engine maintains a clean separation of concerns between layout computation and geometry transformation.
…onents - Added `rustfmt` and `clippy` to the toolchain components in `rust-toolchain.toml`, enhancing code formatting and linting capabilities.
Summary
Changed files
Rendering engine (
crates/grida-canvas)painter/layer.rs— fix clip path coordinate space for render surfacessurface/ui/render.rs— improve selection/hover color feedbackruntime/scene.rs— deferred frame plans and zoom image cacheio/io_grida.rs,tests/fbs_roundtrip.rs— text decoration roundtrip testswindow/application.rs— integration with deferred plansFigma import (
packages/grida-canvas-io-figma)Benchmarks & docs
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Performance