Skip to content

Commit af2c431

Browse files
committed
Don't round logical coordinates
1 parent 3a80190 commit af2c431

File tree

9 files changed

+32
-86
lines changed

9 files changed

+32
-86
lines changed

editor/src/messages/portfolio/portfolio_message_handler.rs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use crate::messages::prelude::*;
2121
use crate::messages::tool::common_functionality::graph_modification_utils;
2222
use crate::messages::tool::common_functionality::utility_functions::make_path_editable_is_allowed;
2323
use crate::messages::tool::utility_types::{HintData, HintGroup, ToolType};
24+
use crate::messages::viewport::ToPhysical;
2425
use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor};
2526
use derivative::*;
2627
use glam::{DAffine2, DVec2};
@@ -364,15 +365,12 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
364365
let node_to_inspect = self.node_to_inspect();
365366

366367
let scale = viewport.scale();
367-
// Use logical dimensions for viewport resolution (foreignObject sizing)
368-
let logical_resolution = viewport.size().into_dvec2().ceil().as_uvec2();
369368
// Use exact physical dimensions from browser (via ResizeObserver's devicePixelContentBoxSize)
370-
let physical_resolution = viewport.physical_size_uvec2();
369+
let physical_resolution = viewport.size().to_physical().into_dvec2().as_uvec2();
371370

372371
if let Ok(message) = self.executor.submit_node_graph_evaluation(
373372
self.documents.get_mut(document_id).expect("Tried to render non-existent document"),
374373
*document_id,
375-
logical_resolution,
376374
physical_resolution,
377375
scale,
378376
timing_information,
@@ -974,14 +972,12 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
974972
};
975973

976974
let scale = viewport.scale();
977-
// Use logical dimensions for viewport resolution (foreignObject sizing)
978-
let logical_resolution = viewport.size().into_dvec2().ceil().as_uvec2();
979975
// Use exact physical dimensions from browser (via ResizeObserver's devicePixelContentBoxSize)
980-
let physical_resolution = viewport.physical_size_uvec2();
976+
let physical_resolution = viewport.size().to_physical().into_dvec2().round().as_uvec2();
981977

982978
let result = self
983979
.executor
984-
.submit_node_graph_evaluation(document, document_id, logical_resolution, physical_resolution, scale, timing_information, node_to_inspect, ignore_hash);
980+
.submit_node_graph_evaluation(document, document_id, physical_resolution, scale, timing_information, node_to_inspect, ignore_hash);
985981

986982
match result {
987983
Err(description) => {

editor/src/messages/viewport/viewport_message.rs

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,6 @@ use crate::messages::prelude::*;
33
#[impl_message(Message, Viewport)]
44
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
55
pub enum ViewportMessage {
6-
Update {
7-
x: f64,
8-
y: f64,
9-
width: f64,
10-
height: f64,
11-
scale: f64,
12-
physical_width: f64,
13-
physical_height: f64,
14-
},
6+
Update { x: f64, y: f64, width: f64, height: f64, scale: f64 },
157
RepropagateUpdate,
168
}

editor/src/messages/viewport/viewport_message_handler.rs

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ use crate::messages::tool::tool_messages::tool_prelude::DVec2;
66
#[derive(Debug, PartialEq, Clone, Copy, serde::Serialize, serde::Deserialize, specta::Type, ExtractField)]
77
pub struct ViewportMessageHandler {
88
bounds: Bounds,
9-
// Physical bounds in device pixels (exact pixel dimensions from browser)
10-
physical_bounds: Bounds,
119
// Ratio of logical pixels to physical pixels
1210
scale: f64,
1311
}
@@ -18,10 +16,6 @@ impl Default for ViewportMessageHandler {
1816
offset: Point { x: 0.0, y: 0.0 },
1917
size: Point { x: 0.0, y: 0.0 },
2018
},
21-
physical_bounds: Bounds {
22-
offset: Point { x: 0.0, y: 0.0 },
23-
size: Point { x: 0.0, y: 0.0 },
24-
},
2519
scale: 1.0,
2620
}
2721
}
@@ -31,29 +25,14 @@ impl Default for ViewportMessageHandler {
3125
impl MessageHandler<ViewportMessage, ()> for ViewportMessageHandler {
3226
fn process_message(&mut self, message: ViewportMessage, responses: &mut VecDeque<Message>, _: ()) {
3327
match message {
34-
ViewportMessage::Update {
35-
x,
36-
y,
37-
width,
38-
height,
39-
scale,
40-
physical_width,
41-
physical_height,
42-
} => {
28+
ViewportMessage::Update { x, y, width, height, scale } => {
4329
assert_ne!(scale, 0.0, "Viewport scale cannot be zero");
4430
self.scale = scale;
4531

4632
self.bounds = Bounds {
4733
offset: Point { x, y },
4834
size: Point { x: width, y: height },
4935
};
50-
self.physical_bounds = Bounds {
51-
offset: Point { x, y },
52-
size: Point {
53-
x: physical_width,
54-
y: physical_height,
55-
},
56-
};
5736
responses.add(NodeGraphMessage::UpdateNodeGraphWidth);
5837
}
5938
ViewportMessage::RepropagateUpdate => {}
@@ -102,11 +81,6 @@ impl ViewportMessageHandler {
10281
self.bounds.size().into_scaled(self.scale)
10382
}
10483

105-
pub fn physical_size_uvec2(&self) -> glam::UVec2 {
106-
let size = self.physical_bounds.size();
107-
glam::UVec2::new(size.x.ceil() as u32, size.y.ceil() as u32)
108-
}
109-
11084
#[expect(private_bounds)]
11185
pub fn logical<T: Into<Point>>(&self, point: T) -> LogicalPoint {
11286
point.into().convert_to_logical(self.scale)

editor/src/node_graph_executor.rs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,6 @@ impl NodeGraphExecutor {
138138
document: &mut DocumentMessageHandler,
139139
document_id: DocumentId,
140140
viewport_resolution: UVec2,
141-
physical_viewport_resolution: UVec2,
142141
viewport_scale: f64,
143142
time: TimingInformation,
144143
) -> Result<Message, String> {
@@ -149,7 +148,6 @@ impl NodeGraphExecutor {
149148
};
150149
let render_config = RenderConfig {
151150
viewport,
152-
physical_viewport_resolution,
153151
scale: viewport_scale,
154152
time,
155153
export_format: graphene_std::application_io::ExportFormat::Raster,
@@ -173,14 +171,13 @@ impl NodeGraphExecutor {
173171
document: &mut DocumentMessageHandler,
174172
document_id: DocumentId,
175173
viewport_resolution: UVec2,
176-
physical_viewport_resolution: UVec2,
177174
viewport_scale: f64,
178175
time: TimingInformation,
179176
node_to_inspect: Option<NodeId>,
180177
ignore_hash: bool,
181178
) -> Result<Message, String> {
182179
self.update_node_graph(document, node_to_inspect, ignore_hash)?;
183-
self.submit_current_node_graph_evaluation(document, document_id, viewport_resolution, physical_viewport_resolution, viewport_scale, time)
180+
self.submit_current_node_graph_evaluation(document, document_id, viewport_resolution, viewport_scale, time)
184181
}
185182

186183
/// Evaluates a node graph for export
@@ -209,8 +206,6 @@ impl NodeGraphExecutor {
209206
transform,
210207
..Default::default()
211208
},
212-
// For export, logical and physical are the same (no HiDPI scaling)
213-
physical_viewport_resolution: resolution,
214209
scale: export_config.scale_factor,
215210
time: Default::default(),
216211
export_format,
@@ -425,10 +420,9 @@ impl NodeGraphExecutor {
425420
}
426421
let matrix = format_transform_matrix(frame.transform);
427422
let transform = if matrix.is_empty() { String::new() } else { format!(" transform=\"{matrix}\"") };
428-
// Mark viewport canvas with data attribute so it won't be cloned for repeated instances
429423
let svg = format!(
430-
r#"<svg><foreignObject width="{}" height="{}"{transform}><div data-canvas-placeholder="{}" data-is-viewport="true"></div></foreignObject></svg>"#,
431-
frame.resolution.x, frame.resolution.y, frame.surface_id.0
424+
r#"<svg><foreignObject width="{}" height="{}"{transform}><div data-canvas-placeholder="{}" data-physical-width="{}" data-physical-height="{}" data-is-viewport="true"></div></foreignObject></svg>"#,
425+
frame.resolution.x, frame.resolution.y, frame.surface_id.0, frame.physical_resolution.x, frame.physical_resolution.y
432426
);
433427
self.last_svg_canvas = Some(frame);
434428
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });

editor/src/node_graph_executor/runtime.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,8 +289,8 @@ impl NodeRuntime {
289289
let surface = self.wasm_viewport_surface.as_ref().unwrap();
290290

291291
// Use logical resolution for CSS sizing, physical resolution for the actual surface/texture
292-
let logical_resolution = render_config.viewport.resolution;
293-
let physical_resolution = render_config.physical_viewport_resolution;
292+
let physical_resolution = render_config.viewport.resolution;
293+
let logical_resolution = physical_resolution.as_dvec2() / render_config.scale;
294294

295295
// Blit the texture to the surface
296296
let mut encoder = executor.context.device.create_command_encoder(&vello::wgpu::CommandEncoderDescriptor {

frontend/src/utility-functions/viewports.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,11 @@ export function setupViewportResizeObserver(editor: Editor) {
3939
// Get viewport position
4040
const bounds = entry.target.getBoundingClientRect();
4141

42+
const scale = physicalWidth / logicalWidth;
43+
4244
// Send both logical and physical dimensions to the backend
4345
// Logical dimensions are used for CSS/SVG sizing, physical for GPU textures
44-
editor.handle.updateViewport(bounds.x, bounds.y, logicalWidth, logicalHeight, devicePixelRatio, physicalWidth, physicalHeight);
46+
editor.handle.updateViewport(bounds.x, bounds.y, logicalWidth, logicalHeight, scale);
4547
}
4648
});
4749

frontend/wasm/src/editor_api.rs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -518,16 +518,8 @@ impl EditorHandle {
518518

519519
/// Send new viewport info to the backend
520520
#[wasm_bindgen(js_name = updateViewport)]
521-
pub fn update_viewport(&self, x: f64, y: f64, width: f64, height: f64, scale: f64, physical_width: f64, physical_height: f64) {
522-
let message = ViewportMessage::Update {
523-
x,
524-
y,
525-
width,
526-
height,
527-
scale,
528-
physical_width,
529-
physical_height,
530-
};
521+
pub fn update_viewport(&self, x: f64, y: f64, width: f64, height: f64, scale: f64) {
522+
let message = ViewportMessage::Update { x, y, width, height, scale };
531523
self.dispatch(message);
532524
}
533525

node-graph/libraries/application-io/src/lib.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use core_types::transform::Footprint;
22
use dyn_any::{DynAny, StaticType, StaticTypeSized};
3-
use glam::{DAffine2, UVec2};
3+
use glam::{DAffine2, DVec2, UVec2};
44
use std::fmt::Debug;
55
use std::future::Future;
66
use std::hash::{Hash, Hasher};
@@ -24,7 +24,7 @@ impl std::fmt::Display for SurfaceId {
2424
pub struct SurfaceFrame {
2525
pub surface_id: SurfaceId,
2626
/// Logical resolution in CSS pixels (used for foreignObject dimensions)
27-
pub resolution: UVec2,
27+
pub resolution: DVec2,
2828
/// Physical resolution in device pixels (used for actual canvas/texture dimensions)
2929
pub physical_resolution: UVec2,
3030
pub transform: DAffine2,
@@ -108,7 +108,7 @@ impl<S: Size> From<SurfaceHandleFrame<S>> for SurfaceFrame {
108108
Self {
109109
surface_id: x.surface_handle.window_id,
110110
transform: x.transform,
111-
resolution: size,
111+
resolution: size.into(),
112112
physical_resolution: size,
113113
}
114114
}
@@ -239,8 +239,6 @@ pub struct TimingInformation {
239239
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
240240
pub struct RenderConfig {
241241
pub viewport: Footprint,
242-
/// Physical viewport resolution in device pixels (from ResizeObserver's devicePixelContentBoxSize)
243-
pub physical_viewport_resolution: UVec2,
244242
pub scale: f64,
245243
pub export_format: ExportFormat,
246244
pub time: TimingInformation,

node-graph/nodes/gstd/src/render_node.rs

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,7 @@ async fn create_context<'a: 'n>(
120120
}
121121

122122
#[node_macro::node(category(""))]
123-
async fn render<'a: 'n>(
124-
ctx: impl Ctx + ExtractFootprint + ExtractVarArgs,
125-
editor_api: &'a WasmEditorApi,
126-
data: RenderIntermediate,
127-
) -> RenderOutput {
123+
async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, editor_api: &'a WasmEditorApi, data: RenderIntermediate) -> RenderOutput {
128124
let footprint = ctx.footprint();
129125
let render_params = ctx
130126
.vararg(0)
@@ -135,6 +131,10 @@ async fn render<'a: 'n>(
135131
render_params.footprint = *footprint;
136132
let render_params = &render_params;
137133

134+
let scale = render_params.scale;
135+
let physical_resolution = render_params.footprint.resolution;
136+
let logical_resolution = (render_params.footprint.resolution.as_dvec2() / scale).round().as_uvec2();
137+
138138
let RenderIntermediate { ty, mut metadata, contains_artboard } = data;
139139
metadata.apply_transform(footprint.transform);
140140

@@ -145,8 +145,8 @@ async fn render<'a: 'n>(
145145
rendering.leaf_tag("rect", |attributes| {
146146
attributes.push("x", "0");
147147
attributes.push("y", "0");
148-
attributes.push("width", footprint.resolution.x.to_string());
149-
attributes.push("height", footprint.resolution.y.to_string());
148+
attributes.push("width", logical_resolution.x.to_string());
149+
attributes.push("height", logical_resolution.y.to_string());
150150
let matrix = format_transform_matrix(footprint.transform.inverse());
151151
if !matrix.is_empty() {
152152
attributes.push("transform", matrix);
@@ -158,7 +158,7 @@ async fn render<'a: 'n>(
158158
rendering.image_data = svg_data.1.clone();
159159
rendering.svg_defs = svg_data.2.clone();
160160

161-
rendering.wrap_with_transform(footprint.transform, Some(footprint.resolution.as_dvec2()));
161+
rendering.wrap_with_transform(footprint.transform, Some(logical_resolution.as_dvec2()));
162162
RenderOutputType::Svg {
163163
svg: rendering.svg.to_svg_string(),
164164
image_data: rendering.image_data,
@@ -170,21 +170,16 @@ async fn render<'a: 'n>(
170170
};
171171
let (child, context) = Arc::as_ref(vello_data);
172172

173-
// Always apply scale when rendering to texture
174-
let scale = render_params.scale;
175-
176173
let scale_transform = glam::DAffine2::from_scale(glam::DVec2::splat(scale));
177174
let footprint_transform = scale_transform * footprint.transform;
178175
let footprint_transform_vello = vello::kurbo::Affine::new(footprint_transform.to_cols_array());
179176

180177
let mut scene = vello::Scene::new();
181178
scene.append(child, Some(footprint_transform_vello));
182179

183-
let resolution = (footprint.resolution.as_dvec2() * scale).as_uvec2();
184-
185180
// We now replace all transforms which are supposed to be infinite with a transform which covers the entire viewport
186181
// See <https://xi.zulipchat.com/#narrow/channel/197075-vello/topic/Full.20screen.20color.2Fgradients/near/538435044> for more detail
187-
let scaled_infinite_transform = vello::kurbo::Affine::scale_non_uniform(resolution.x as f64, resolution.y as f64);
182+
let scaled_infinite_transform = vello::kurbo::Affine::scale_non_uniform(physical_resolution.x as f64, physical_resolution.y as f64);
188183
let encoding = scene.encoding_mut();
189184
for transform in encoding.transforms.iter_mut() {
190185
if transform.matrix[0] == f32::INFINITY {
@@ -198,7 +193,10 @@ async fn render<'a: 'n>(
198193
}
199194

200195
// Always render to texture (unified path for both WASM and desktop)
201-
let texture = exec.render_vello_scene_to_texture(&scene, resolution, context, background).await.expect("Failed to render Vello scene");
196+
let texture = exec
197+
.render_vello_scene_to_texture(&scene, physical_resolution, context, background)
198+
.await
199+
.expect("Failed to render Vello scene");
202200

203201
RenderOutputType::Texture(ImageTexture { texture })
204202
}

0 commit comments

Comments
 (0)