Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve overlay rendering for DPI scaling #2073

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
24 changes: 14 additions & 10 deletions editor/src/messages/portfolio/document/overlays/grid_overlays.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ fn grid_overlay_rectangular_dot(document: &DocumentMessageHandler, overlay_conte
primary_end = (primary_end / spacing.x).floor() * spacing.x + origin.x % spacing.x;

// Round to avoid floating point errors
let total_dots = ((primary_end - primary_start) / spacing.x).round();
let total_dots = ((primary_end - primary_start) / spacing.x * overlay_context.dpr()).round();

for line_index in 0..=((max - min) / spacing.y).ceil() as i32 {
let secondary_pos = (((min - origin.y) / spacing.y).ceil() + line_index as f64) * spacing.y + origin.y;
Expand All @@ -82,10 +82,9 @@ fn grid_overlay_rectangular_dot(document: &DocumentMessageHandler, overlay_conte
let x_per_dot = (end.x - start.x) / total_dots;
for dot_index in 0..=total_dots as usize {
let exact_x = x_per_dot * dot_index as f64;
overlay_context.pixel(
document_to_viewport.transform_point2(DVec2::new(start.x + exact_x, start.y)).round(),
Some(&("#".to_string() + &grid_color.rgba_hex())),
)
let dot_position = document_to_viewport.transform_point2(DVec2::new(start.x + exact_x, start.y));

overlay_context.pixel(dot_position, Some(&("#".to_string() + &grid_color.rgba_hex())))
}
}
}
Expand All @@ -95,6 +94,7 @@ fn grid_overlay_isometric(document: &DocumentMessageHandler, overlay_context: &m
let cmp = |a: &f64, b: &f64| a.partial_cmp(b).unwrap();
let origin = document.snapping_state.grid.origin;
let document_to_viewport = document.navigation_handler.calculate_offset_transform(overlay_context.size / 2., &document.document_ptz);
let dpr = overlay_context.dpr();

let bounds = document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, overlay_context.size]);
let tan_a = angle_a.to_radians().tan();
Expand All @@ -110,8 +110,9 @@ fn grid_overlay_isometric(document: &DocumentMessageHandler, overlay_context: &m
let min_y = bounds.0.iter().map(|&corner| corner.y).min_by(cmp).unwrap_or_default();
let max_y = bounds.0.iter().map(|&corner| corner.y).max_by(cmp).unwrap_or_default();
let spacing = isometric_spacing.x;
for line_index in 0..=((max_x - min_x) / spacing).ceil() as i32 {
let x_pos = (((min_x - origin.x) / spacing).ceil() + line_index as f64) * spacing + origin.x;

for line_index in 0..=((max_x - min_x) / spacing * dpr).ceil() as i32 {
let x_pos = (((min_x - origin.x) / spacing).ceil() + line_index as f64 / dpr) * spacing + origin.x;
let start = DVec2::new(x_pos, min_y);
let end = DVec2::new(x_pos, max_y);
overlay_context.line(
Expand Down Expand Up @@ -146,6 +147,7 @@ fn grid_overlay_isometric_dot(document: &DocumentMessageHandler, overlay_context
let cmp = |a: &f64, b: &f64| a.partial_cmp(b).unwrap();
let origin = document.snapping_state.grid.origin;
let document_to_viewport = document.navigation_handler.calculate_offset_transform(overlay_context.size / 2., &document.document_ptz);
let dpr = overlay_context.dpr();

let bounds = document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, overlay_context.size]);
let tan_a = angle_a.to_radians().tan();
Expand All @@ -166,24 +168,26 @@ fn grid_overlay_isometric_dot(document: &DocumentMessageHandler, overlay_context
let min_y = bounds.0.into_iter().min_by(|a, b| inverse_project(a).partial_cmp(&inverse_project(b)).unwrap()).unwrap_or_default();
let max_y = bounds.0.into_iter().max_by(|a, b| inverse_project(a).partial_cmp(&inverse_project(b)).unwrap()).unwrap_or_default();
let spacing_y = isometric_spacing.y;
let lines = ((inverse_project(&max_y) - inverse_project(&min_y)) / spacing_y).ceil() as i32;
let lines = ((inverse_project(&max_y) - inverse_project(&min_y)) / spacing_y * dpr).ceil() as i32;

let cos_a = angle_a.to_radians().cos();
// If cos_a is 0 then there will be no intersections and thus no dots should be drawn
if cos_a.abs() <= 0.00001 {
return;
}

let dash_length = (spacing_x / cos_a) * document_to_viewport.matrix2.x_axis.length() * dpr;
let x_offset = (((min_x - origin.x) / spacing_x).ceil()) * spacing_x + origin.x - min_x;
for line_index in 0..=lines {
let y_pos = (((inverse_project(&min_y) - origin.y) / spacing_y).ceil() + line_index as f64) * spacing_y + origin.y;
let y_pos = (((inverse_project(&min_y) - origin.y) / spacing_y).ceil() + line_index as f64 / dpr) * spacing_y + origin.y;
let start = DVec2::new(min_x + x_offset, project(&DVec2::new(min_x + x_offset, y_pos)));
let end = DVec2::new(max_x + x_offset, project(&DVec2::new(max_x + x_offset, y_pos)));

overlay_context.dashed_line(
document_to_viewport.transform_point2(start),
document_to_viewport.transform_point2(end),
Some(&("#".to_string() + &grid_color.rgba_hex())),
Some((spacing_x / cos_a) * document_to_viewport.matrix2.x_axis.length()),
Some(dash_length),
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,31 @@ impl MessageHandler<OverlaysMessage, OverlaysMessageData<'_>> for OverlaysMessag
context.dyn_into().expect("Context should be a canvas 2d context")
});

let size = ipp.viewport_bounds.size().as_uvec2();
let logical_size = ipp.viewport_bounds.size();
let window = web_sys::window().expect("no global `window` exists");
let device_pixel_ratio = window.device_pixel_ratio();

context.clear_rect(0., 0., ipp.viewport_bounds.size().x, ipp.viewport_bounds.size().y);
// Set the canvas buffer size to match the device pixel ratio
let physical_width = logical_size.x * device_pixel_ratio;
let physical_height = logical_size.y * device_pixel_ratio;
canvas.set_width(physical_width as u32);
canvas.set_height(physical_height as u32);

// Scale the context to maintain correct drawing dimensions
// context.scale(device_pixel_ratio, device_pixel_ratio).unwrap();
context.clear_rect(0., 0., logical_size.x, logical_size.y);

if overlays_visible {
responses.add(DocumentMessage::GridOverlays(OverlayContext {
render_context: context.clone(),
size: size.as_dvec2(),
size: logical_size,
device_pixel_ratio,
}));
for provider in &self.overlay_providers {
responses.add(provider(OverlayContext {
render_context: context.clone(),
size: size.as_dvec2(),
size: logical_size,
device_pixel_ratio,
}));
}
}
Expand Down
93 changes: 56 additions & 37 deletions editor/src/messages/portfolio/document/overlays/utility_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,36 @@ pub struct OverlayContext {
#[specta(skip)]
pub render_context: web_sys::CanvasRenderingContext2d,
pub size: DVec2,
pub device_pixel_ratio: f64,
}
// Message hashing isn't used but is required by the message system macros
impl core::hash::Hash for OverlayContext {
fn hash<H: std::hash::Hasher>(&self, _state: &mut H) {}
}

impl OverlayContext {
pub fn dpr(&self) -> f64 {
self.device_pixel_ratio
}

pub fn align_to_pixel(&self, coord: DVec2) -> DVec2 {
(coord * self.dpr()).ceil() / self.dpr() - DVec2::splat(0.5 / self.dpr())
}

pub fn adjusted_size(&self, size: f64) -> f64 {
let sqrt_dpr = self.dpr().sqrt();
size * ((sqrt_dpr * 10.0).ceil() / 10.0).max(1.0)
}

pub fn quad(&mut self, quad: Quad, color_fill: Option<&str>) {
self.render_context.begin_path();
self.render_context.move_to(quad.0[3].x.round() - 0.5, quad.0[3].y.round() - 0.5);

let start = self.align_to_pixel(quad.0[3]);
self.render_context.move_to(start.x, start.y);

for i in 0..4 {
self.render_context.line_to(quad.0[i].x.round() - 0.5, quad.0[i].y.round() - 0.5);
let point = self.align_to_pixel(quad.0[i]);
self.render_context.line_to(point.x, point.y);
}
if let Some(color_fill) = color_fill {
self.render_context.set_fill_style_str(color_fill);
Expand All @@ -50,12 +68,14 @@ impl OverlayContext {
}

pub fn dashed_line(&mut self, start: DVec2, end: DVec2, color: Option<&str>, dash_width: Option<f64>) {
let start = start.round() - DVec2::splat(0.5);
let end = end.round() - DVec2::splat(0.5);
let start = self.align_to_pixel(start);
let end = self.align_to_pixel(end);

if let Some(dash_width) = dash_width {
let scaled_dash_width = self.adjusted_size(dash_width);
let array = js_sys::Array::new();
array.push(&JsValue::from(1));
array.push(&JsValue::from(dash_width - 1.));
array.push(&JsValue::from(self.dpr()));
array.push(&JsValue::from(scaled_dash_width - self.dpr()));
self.render_context
.set_line_dash(&JsValue::from(array))
.map_err(|error| log::warn!("Error drawing dashed line: {:?}", error))
Expand All @@ -75,12 +95,11 @@ impl OverlayContext {
}

pub fn manipulator_handle(&mut self, position: DVec2, selected: bool) {
let position = position.round() - DVec2::splat(0.5);
let position = self.align_to_pixel(position);
let radius = self.adjusted_size(MANIPULATOR_GROUP_MARKER_SIZE / 2.);

self.render_context.begin_path();
self.render_context
.arc(position.x, position.y, MANIPULATOR_GROUP_MARKER_SIZE / 2., 0., TAU)
.expect("Failed to draw the circle");
self.render_context.arc(position.x, position.y, radius, 0., TAU).expect("Failed to draw the circle");

let fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE };
self.render_context.set_fill_style_str(fill);
Expand All @@ -96,11 +115,11 @@ impl OverlayContext {
}

pub fn square(&mut self, position: DVec2, size: Option<f64>, color_fill: Option<&str>, color_stroke: Option<&str>) {
let size = size.unwrap_or(MANIPULATOR_GROUP_MARKER_SIZE);
let size = self.adjusted_size(size.unwrap_or(MANIPULATOR_GROUP_MARKER_SIZE));
let color_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE);
let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE);

let position = position.round() - DVec2::splat(0.5);
let position = self.align_to_pixel(position);
let corner = position - DVec2::splat(size) / 2.;

self.render_context.begin_path();
Expand All @@ -112,10 +131,10 @@ impl OverlayContext {
}

pub fn pixel(&mut self, position: DVec2, color: Option<&str>) {
let size = 1.;
let size = self.adjusted_size(MANIPULATOR_GROUP_MARKER_SIZE);
let color_fill = color.unwrap_or(COLOR_OVERLAY_WHITE);

let position = position.round() - DVec2::splat(0.5);
let position = self.align_to_pixel(position);
let corner = position - DVec2::splat(size) / 2.;

self.render_context.begin_path();
Expand All @@ -127,7 +146,8 @@ impl OverlayContext {
pub fn circle(&mut self, position: DVec2, radius: f64, color_fill: Option<&str>, color_stroke: Option<&str>) {
let color_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE);
let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE);
let position = position.round();
let position = self.align_to_pixel(position);
let radius = self.adjusted_size(radius);
self.render_context.begin_path();
self.render_context.arc(position.x, position.y, radius, 0., TAU).expect("Failed to draw the circle");
self.render_context.set_fill_style_str(color_fill);
Expand All @@ -136,19 +156,21 @@ impl OverlayContext {
self.render_context.stroke();
}
pub fn pivot(&mut self, position: DVec2) {
let (x, y) = (position.round() - DVec2::splat(0.5)).into();
let position = self.align_to_pixel(position);
let (x, y) = position.into();

// Circle

let radius = self.adjusted_size(PIVOT_DIAMETER / 2.);
self.render_context.begin_path();
self.render_context.arc(x, y, PIVOT_DIAMETER / 2., 0., TAU).expect("Failed to draw the circle");
self.render_context.arc(x, y, radius, 0., TAU).expect("Failed to draw the circle");
self.render_context.set_fill_style_str(COLOR_OVERLAY_YELLOW);
self.render_context.fill();

// Crosshair

// Round line caps add half the stroke width to the length on each end, so we subtract that here before halving to get the radius
let crosshair_radius = (PIVOT_CROSSHAIR_LENGTH - PIVOT_CROSSHAIR_THICKNESS) / 2.;
let crosshair_radius = ((PIVOT_CROSSHAIR_LENGTH - PIVOT_CROSSHAIR_THICKNESS) / 2.) * self.dpr();

self.render_context.set_stroke_style_str(COLOR_OVERLAY_YELLOW);
self.render_context.set_line_cap("round");
Expand Down Expand Up @@ -200,6 +222,7 @@ impl OverlayContext {

pub fn outline(&mut self, subpaths: impl Iterator<Item = impl Borrow<Subpath<PointId>>>, transform: DAffine2) {
self.render_context.begin_path();
self.render_context.set_line_width(self.adjusted_size(1.0));
for subpath in subpaths {
let subpath = subpath.borrow();
let mut curves = subpath.iter().peekable();
Expand All @@ -208,31 +231,23 @@ impl OverlayContext {
continue;
};

self.render_context.move_to(transform.transform_point2(first.start()).x, transform.transform_point2(first.start()).y);
let start = self.align_to_pixel(transform.transform_point2(first.start()));
self.render_context.move_to(start.x, start.y);
for curve in curves {
match curve.handles {
bezier_rs::BezierHandles::Linear => {
let a = transform.transform_point2(curve.end());
let a = a.round() - DVec2::splat(0.5);

self.render_context.line_to(a.x, a.y)
let end = self.align_to_pixel(transform.transform_point2(curve.end()));
self.render_context.line_to(end.x, end.y)
}
bezier_rs::BezierHandles::Quadratic { handle } => {
let a = transform.transform_point2(handle);
let b = transform.transform_point2(curve.end());
let a = a.round() - DVec2::splat(0.5);
let b = b.round() - DVec2::splat(0.5);

let a = self.align_to_pixel(transform.transform_point2(handle));
let b = self.align_to_pixel(transform.transform_point2(curve.end()));
self.render_context.quadratic_curve_to(a.x, a.y, b.x, b.y)
}
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => {
let a = transform.transform_point2(handle_start);
let b = transform.transform_point2(handle_end);
let c = transform.transform_point2(curve.end());
let a = a.round() - DVec2::splat(0.5);
let b = b.round() - DVec2::splat(0.5);
let c = c.round() - DVec2::splat(0.5);

let a = self.align_to_pixel(transform.transform_point2(handle_start));
let b = self.align_to_pixel(transform.transform_point2(handle_end));
let c = self.align_to_pixel(transform.transform_point2(curve.end()));
self.render_context.bezier_curve_to(a.x, a.y, b.x, b.y, c.x, c.y)
}
}
Expand All @@ -248,7 +263,11 @@ impl OverlayContext {
}

pub fn text(&self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) {
let font_size = 12.0;

self.render_context.set_font(&format!("{}px Source Sans Pro, Arial, sans-serif", font_size));
let metrics = self.render_context.measure_text(text).expect("Failed to measure the text dimensions");

let x = match pivot[0] {
Pivot::Start => padding,
Pivot::Middle => -(metrics.actual_bounding_box_right() + metrics.actual_bounding_box_left()) / 2.,
Expand All @@ -260,7 +279,8 @@ impl OverlayContext {
Pivot::End => -padding,
};

let [a, b, c, d, e, f] = (transform * DAffine2::from_translation(DVec2::new(x, y))).to_cols_array();
let position = self.align_to_pixel(DVec2::new(x, y));
let [a, b, c, d, e, f] = (transform * DAffine2::from_translation(position)).to_cols_array();
self.render_context.set_transform(a, b, c, d, e, f).expect("Failed to rotate the render context to the specified angle");

if let Some(background) = background_color {
Expand All @@ -273,7 +293,6 @@ impl OverlayContext {
);
}

self.render_context.set_font("12px Source Sans Pro, Arial, sans-serif");
self.render_context.set_fill_style_str(font_color);
self.render_context.fill_text(text, 0., 0.).expect("Failed to draw the text at the calculated position");
self.render_context.reset_transform().expect("Failed to reset the render context transform");
Expand Down
38 changes: 32 additions & 6 deletions editor/src/messages/tool/common_functionality/snapping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,11 +322,17 @@ impl SnapManager {
let layer_bounds = document.metadata().transform_to_document(layer) * Quad::from_box(bounds);
let screen_bounds = document.metadata().document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, snap_data.input.viewport_bounds.size()]);
if screen_bounds.intersects(layer_bounds) {
if !self.alignment_candidates.as_ref().is_some_and(|candidates| candidates.len() > 100) {
self.alignment_candidates.get_or_insert_with(Vec::new).push(layer);
}
if quad.intersects(layer_bounds) && !self.candidates.as_ref().is_some_and(|candidates| candidates.len() > 10) {
self.candidates.get_or_insert_with(Vec::new).push(layer);
let center = layer_bounds.center();
let distance = quad.center().distance(center);
let max_distance = snap_tolerance(document) * 10.0;

if distance <= max_distance {
if !self.alignment_candidates.as_ref().is_some_and(|candidates| candidates.len() > 100) {
self.alignment_candidates.get_or_insert_with(Vec::new).push(layer);
}
if quad.intersects(layer_bounds) && !self.candidates.as_ref().is_some_and(|candidates| candidates.len() > 10) {
self.candidates.get_or_insert_with(Vec::new).push(layer);
}
}
}
}
Expand All @@ -338,8 +344,27 @@ impl SnapManager {

self.candidates = None;
self.alignment_candidates = None;
for layer in LayerNodeIdentifier::ROOT_PARENT.children(document.metadata()) {

let screen_bounds = document.metadata().document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, snap_data.input.viewport_bounds.size()]);

let visible_layers: Vec<_> = LayerNodeIdentifier::ROOT_PARENT
.children(document.metadata())
.filter(|&layer| {
if let Some(bounds) = document.metadata().bounding_box_with_transform(layer, DAffine2::IDENTITY) {
let layer_bounds = document.metadata().transform_to_document(layer) * Quad::from_box(bounds);
screen_bounds.intersects(layer_bounds)
} else {
false
}
})
.collect();

for layer in visible_layers {
self.add_candidates(layer, snap_data, quad);

if self.alignment_candidates.as_ref().is_some_and(|candidates| candidates.len() >= 100) && self.candidates.as_ref().is_some_and(|candidates| candidates.len() >= 10) {
break;
}
}

if self.alignment_candidates.as_ref().is_some_and(|candidates| candidates.len() > 100) {
Expand Down Expand Up @@ -440,6 +465,7 @@ impl SnapManager {

pub fn draw_overlays(&mut self, snap_data: SnapData, overlay_context: &mut OverlayContext) {
let to_viewport = snap_data.document.metadata().document_to_viewport;
overlay_context.render_context.scale(overlay_context.device_pixel_ratio, overlay_context.device_pixel_ratio).unwrap();
if let Some(ind) = &self.indicator {
for curve in &ind.curves {
let Some(curve) = curve else { continue };
Expand Down