Skip to content

Commit 9fc98cf

Browse files
authored
Desktop: Remove web_sys text measuring to fix node graph layer widths (#3455)
* Remove web_sys text measuring * Improve export * Fix top of layer stack
1 parent 3926337 commit 9fc98cf

File tree

10 files changed

+72
-112
lines changed

10 files changed

+72
-112
lines changed
File renamed without changes.

editor/src/messages/portfolio/document/document_message.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ use std::path::PathBuf;
33
use super::utility_types::misc::{GroupFolderType, SnappingState};
44
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
55
use crate::messages::portfolio::document::data_panel::DataPanelMessage;
6-
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
7-
use crate::messages::portfolio::document::overlays::utility_types::OverlaysType;
6+
use crate::messages::portfolio::document::overlays::utility_types::{OverlayContext, OverlaysType};
87
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
98
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GridSnapping};
109
use crate::messages::portfolio::utility_types::PanelType;

editor/src/messages/portfolio/document/document_message_handler.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use super::node_graph::document_node_definitions;
22
use super::node_graph::utility_types::Transform;
3-
use super::overlays::utility_types::Pivot;
43
use super::utility_types::error::EditorError;
54
use super::utility_types::misc::{GroupFolderType, SNAP_FUNCTIONS_FOR_BOUNDING_BOXES, SNAP_FUNCTIONS_FOR_PATHS, SnappingOptions, SnappingState};
65
use super::utility_types::network_interface::{self, NodeNetworkInterface, TransactionStatus};
@@ -14,7 +13,7 @@ use crate::messages::portfolio::document::graph_operation::utility_types::Transf
1413
use crate::messages::portfolio::document::node_graph::NodeGraphMessageContext;
1514
use crate::messages::portfolio::document::node_graph::utility_types::FrontendGraphDataType;
1615
use crate::messages::portfolio::document::overlays::grid_overlays::{grid_overlay, overlay_options};
17-
use crate::messages::portfolio::document::overlays::utility_types::{OverlaysType, OverlaysVisibilitySettings};
16+
use crate::messages::portfolio::document::overlays::utility_types::{OverlaysType, OverlaysVisibilitySettings, Pivot};
1817
use crate::messages::portfolio::document::properties_panel::properties_panel_message_handler::PropertiesPanelMessageContext;
1918
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
2019
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, PTZ};

editor/src/messages/portfolio/document/overlays/mod.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,17 @@ pub mod grid_overlays;
22
mod overlays_message;
33
mod overlays_message_handler;
44
pub mod utility_functions;
5-
#[cfg_attr(not(target_family = "wasm"), path = "utility_types_vello.rs")]
6-
pub mod utility_types;
5+
// Native (non‑wasm)
6+
#[cfg(not(target_family = "wasm"))]
7+
pub mod utility_types_native;
8+
#[cfg(not(target_family = "wasm"))]
9+
pub use utility_types_native as utility_types;
10+
11+
// WebAssembly
12+
#[cfg(target_family = "wasm")]
13+
pub mod utility_types_web;
14+
#[cfg(target_family = "wasm")]
15+
pub use utility_types_web as utility_types;
716

817
#[doc(inline)]
918
pub use overlays_message::{OverlaysMessage, OverlaysMessageDiscriminant};

editor/src/messages/portfolio/document/overlays/utility_functions.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerSta
66
use crate::messages::tool::tool_messages::tool_prelude::{DocumentMessageHandler, PreferencesMessageHandler};
77
use glam::{DAffine2, DVec2};
88
use graphene_std::subpath::{Bezier, BezierHandles};
9+
use graphene_std::text::{Font, FontCache, TextAlign, TextContext, TypesettingConfig};
910
use graphene_std::vector::misc::ManipulatorPointId;
1011
use graphene_std::vector::{PointId, SegmentId, Vector};
1112
use std::collections::HashMap;
13+
use std::sync::{LazyLock, Mutex};
1214
use wasm_bindgen::JsCast;
1315

1416
pub fn overlay_canvas_element() -> Option<web_sys::HtmlCanvasElement> {
@@ -218,3 +220,35 @@ pub fn path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: &
218220
}
219221
}
220222
}
223+
224+
// Global lazy initialized font cache and text context
225+
pub static GLOBAL_FONT_CACHE: LazyLock<FontCache> = LazyLock::new(|| {
226+
let mut font_cache = FontCache::default();
227+
// Initialize with the hardcoded font used by overlay text
228+
const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf");
229+
let font = Font::new("Source Sans Pro".to_string(), "Regular".to_string());
230+
font_cache.insert(font, String::new(), FONT_DATA.to_vec());
231+
font_cache
232+
});
233+
234+
pub static GLOBAL_TEXT_CONTEXT: LazyLock<Mutex<TextContext>> = LazyLock::new(|| Mutex::new(TextContext::default()));
235+
236+
pub fn text_width(text: &str, font_size: f64) -> f64 {
237+
let typesetting = TypesettingConfig {
238+
font_size,
239+
line_height_ratio: 1.2,
240+
character_spacing: 0.0,
241+
max_width: None,
242+
max_height: None,
243+
tilt: 0.0,
244+
align: TextAlign::Left,
245+
};
246+
247+
// Load Source Sans Pro font data
248+
// TODO: Grab this from the node_modules folder (either with `include_bytes!` or ideally at runtime) instead of checking the font file into the repo.
249+
// TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size.
250+
let font = Font::new("Source Sans Pro".to_string(), "Regular".to_string());
251+
let mut text_context = GLOBAL_TEXT_CONTEXT.lock().expect("Failed to lock global text context");
252+
let bounds = text_context.bounding_box(text, &font, &GLOBAL_FONT_CACHE, typesetting, false);
253+
bounds.x
254+
}

editor/src/messages/portfolio/document/overlays/utility_types_vello.rs renamed to editor/src/messages/portfolio/document/overlays/utility_types_native.rs

Lines changed: 3 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::consts::{
33
COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE,
44
PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, RESIZE_HANDLE_SIZE, SKEW_TRIANGLE_OFFSET, SKEW_TRIANGLE_SIZE,
55
};
6+
use crate::messages::portfolio::document::overlays::utility_functions::{GLOBAL_FONT_CACHE, GLOBAL_TEXT_CONTEXT};
67
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
78
use crate::messages::prelude::Message;
89
use crate::messages::prelude::ViewportMessageHandler;
@@ -13,30 +14,17 @@ use graphene_std::Color;
1314
use graphene_std::math::quad::Quad;
1415
use graphene_std::subpath::{self, Subpath};
1516
use graphene_std::table::Table;
16-
use graphene_std::text::TextContext;
17-
use graphene_std::text::{Font, FontCache, TextAlign, TypesettingConfig};
17+
use graphene_std::text::{Font, TextAlign, TypesettingConfig};
1818
use graphene_std::vector::click_target::ClickTargetType;
1919
use graphene_std::vector::misc::point_to_dvec2;
2020
use graphene_std::vector::{PointId, SegmentId, Vector};
2121
use kurbo::{self, BezPath, ParamCurve};
2222
use kurbo::{Affine, PathSeg};
2323
use std::collections::HashMap;
24-
use std::sync::{Arc, LazyLock, Mutex, MutexGuard};
24+
use std::sync::{Arc, Mutex, MutexGuard};
2525
use vello::Scene;
2626
use vello::peniko;
2727

28-
// Global lazy initialized font cache and text context
29-
static GLOBAL_FONT_CACHE: LazyLock<FontCache> = LazyLock::new(|| {
30-
let mut font_cache = FontCache::default();
31-
// Initialize with the hardcoded font used by overlay text
32-
const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf");
33-
let font = Font::new("Source Sans Pro".to_string(), "Regular".to_string());
34-
font_cache.insert(font, String::new(), FONT_DATA.to_vec());
35-
font_cache
36-
});
37-
38-
static GLOBAL_TEXT_CONTEXT: LazyLock<Mutex<TextContext>> = LazyLock::new(|| Mutex::new(TextContext::default()));
39-
4028
pub type OverlayProvider = fn(OverlayContext) -> Message;
4129

4230
pub fn empty_provider() -> OverlayProvider {
@@ -393,10 +381,6 @@ impl OverlayContext {
393381
self.internal().fill_path_pattern(subpaths, transform, color);
394382
}
395383

396-
pub fn get_width(&self, text: &str) -> f64 {
397-
self.internal().get_width(text)
398-
}
399-
400384
pub fn text(&self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) {
401385
let mut internal = self.internal();
402386
internal.text(text, font_color, background_color, transform, padding, pivot);
@@ -1034,29 +1018,6 @@ impl OverlayContextInternal {
10341018
self.scene.fill(peniko::Fill::NonZero, self.get_transform(), &brush, None, &path);
10351019
}
10361020

1037-
fn get_width(&mut self, text: &str) -> f64 {
1038-
// Use the actual text-to-path system to get precise text width
1039-
const FONT_SIZE: f64 = 12.0;
1040-
1041-
let typesetting = TypesettingConfig {
1042-
font_size: FONT_SIZE,
1043-
line_height_ratio: 1.2,
1044-
character_spacing: 0.0,
1045-
max_width: None,
1046-
max_height: None,
1047-
tilt: 0.0,
1048-
align: TextAlign::Left,
1049-
};
1050-
1051-
// Load Source Sans Pro font data
1052-
// TODO: Grab this from the node_modules folder (either with `include_bytes!` or ideally at runtime) instead of checking the font file into the repo.
1053-
// TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size.
1054-
let font = Font::new("Source Sans Pro".to_string(), "Regular".to_string());
1055-
let mut text_context = GLOBAL_TEXT_CONTEXT.lock().expect("Failed to lock global text context");
1056-
let bounds = text_context.bounding_box(text, &font, &GLOBAL_FONT_CACHE, typesetting, false);
1057-
bounds.x
1058-
}
1059-
10601021
fn text(&mut self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) {
10611022
// Use the proper text-to-path system for accurate text rendering
10621023
const FONT_SIZE: f64 = 12.0;

editor/src/messages/portfolio/document/overlays/utility_types.rs renamed to editor/src/messages/portfolio/document/overlays/utility_types_web.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -962,10 +962,6 @@ impl OverlayContext {
962962
self.render_context.fill();
963963
}
964964

965-
pub fn get_width(&self, text: &str) -> f64 {
966-
self.render_context.measure_text(text).expect("Failed to measure text dimensions").width()
967-
}
968-
969965
pub fn text(&self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) {
970966
let metrics = self.render_context.measure_text(text).expect("Failed to measure the text dimensions");
971967
let x = match pivot[0] {

editor/src/messages/portfolio/document/utility_types/network_interface.rs

Lines changed: 12 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::consts::{EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP, EXPORTS_TO_TOP_EDGE_PIXEL_G
99
use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext;
1010
use crate::messages::portfolio::document::node_graph::document_node_definitions::{DocumentNodeDefinition, resolve_document_node_type};
1111
use crate::messages::portfolio::document::node_graph::utility_types::{Direction, FrontendClickTargets, FrontendGraphDataType, FrontendGraphInput, FrontendGraphOutput};
12+
use crate::messages::portfolio::document::overlays::utility_functions::text_width;
1213
use crate::messages::portfolio::document::utility_types::network_interface::resolved_types::ResolvedDocumentNodeTypes;
1314
use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePath, WirePathUpdate, build_vector_wire};
1415
use crate::messages::tool::common_functionality::graph_modification_utils;
@@ -1052,7 +1053,11 @@ impl NodeNetworkInterface {
10521053
log::error!("Could not get downstream_connectors in primary_output_connected_to_layer");
10531054
return false;
10541055
};
1055-
let downstream_nodes = downstream_connectors.iter().filter_map(|connector| connector.node_id()).collect::<Vec<_>>();
1056+
1057+
let downstream_nodes = downstream_connectors
1058+
.iter()
1059+
.filter_map(|connector| connector.node_id().filter(|_| connector.input_index() == 0))
1060+
.collect::<Vec<_>>();
10561061
downstream_nodes.iter().any(|node_id| self.is_layer(node_id, network_path))
10571062
}
10581063

@@ -1314,57 +1319,6 @@ impl NodeNetworkInterface {
13141319
.any(|id| id == potentially_upstream_node)
13151320
}
13161321

1317-
#[cfg(not(target_family = "wasm"))]
1318-
fn text_width(&self, node_id: &NodeId, network_path: &[NodeId]) -> Option<f64> {
1319-
warn!("Failed to find width of {node_id:#?} in network_path {network_path:?} due to non-wasm arch");
1320-
Some(0.)
1321-
}
1322-
1323-
#[cfg(target_family = "wasm")]
1324-
fn text_width(&self, node_id: &NodeId, network_path: &[NodeId]) -> Option<f64> {
1325-
let document = web_sys::window().unwrap().document().unwrap();
1326-
let div = match document.create_element("div") {
1327-
Ok(div) => div,
1328-
Err(err) => {
1329-
log::error!("Error creating div: {:?}", err);
1330-
return None;
1331-
}
1332-
};
1333-
1334-
// Set the div's style to make it offscreen and single line
1335-
match div.set_attribute("style", "position: absolute; top: -9999px; left: -9999px; white-space: nowrap;") {
1336-
Err(err) => {
1337-
log::error!("Error setting attribute: {:?}", err);
1338-
return None;
1339-
}
1340-
_ => {}
1341-
};
1342-
1343-
let name = self.display_name(node_id, network_path);
1344-
1345-
div.set_text_content(Some(&name));
1346-
1347-
// Append the div to the document body
1348-
match document.body().unwrap().append_child(&div) {
1349-
Err(err) => {
1350-
log::error!("Error setting adding child to document {:?}", err);
1351-
return None;
1352-
}
1353-
_ => {}
1354-
};
1355-
1356-
// Measure the width
1357-
let text_width = div.get_bounding_client_rect().width();
1358-
1359-
// Remove the div from the document
1360-
match document.body().unwrap().remove_child(&div) {
1361-
Err(_) => log::error!("Could not remove child when rendering text"),
1362-
_ => {}
1363-
};
1364-
1365-
Some(text_width)
1366-
}
1367-
13681322
pub fn from_old_network(old_network: OldNodeNetwork) -> Self {
13691323
let mut node_network = NodeNetwork::default();
13701324
let mut network_metadata = NodeNetworkMetadata::default();
@@ -2121,19 +2075,19 @@ impl NodeNetworkInterface {
21212075
}
21222076

21232077
pub fn load_layer_width(&mut self, node_id: &NodeId, network_path: &[NodeId]) {
2078+
const GAP_WIDTH: f64 = 8.;
2079+
const FONT_SIZE: f64 = 14.;
21242080
let left_thumbnail_padding = GRID_SIZE as f64 / 2.;
21252081
let thumbnail_width = 3. * GRID_SIZE as f64;
2126-
let gap_width = 8.;
2127-
let text_width = self.text_width(node_id, network_path).unwrap_or_else(|| {
2128-
log::error!("Could not get text width for node {node_id}");
2129-
0.
2130-
});
2082+
let layer_text = self.display_name(node_id, network_path);
2083+
2084+
let text_width = text_width(&layer_text, FONT_SIZE);
21312085

21322086
let grip_padding = 4.;
21332087
let grip_width = 8.;
21342088
let icon_overhang_width = GRID_SIZE as f64 / 2.;
21352089

2136-
let layer_width_pixels = left_thumbnail_padding + thumbnail_width + gap_width + text_width + grip_padding + grip_width + icon_overhang_width;
2090+
let layer_width_pixels = left_thumbnail_padding + thumbnail_width + GAP_WIDTH + text_width + grip_padding + grip_width + icon_overhang_width;
21372091
let layer_width = ((layer_width_pixels / 24.).ceil() as u32).max(8);
21382092

21392093
let Some(node_metadata) = self.node_metadata_mut(node_id, network_path) else {

editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/sweep_angle_gizmo.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::consts::{ARC_SNAP_THRESHOLD, GIZMO_HIDE_THRESHOLD};
22
use crate::messages::message::Message;
3+
use crate::messages::portfolio::document::overlays::utility_functions::text_width;
34
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
45
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
56
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
@@ -176,7 +177,11 @@ impl SweepAngleGizmo {
176177
.to_degrees();
177178

178179
let text = format!("{}°", format_rounded(display_angle, 2));
179-
let text_texture_width = overlay_context.get_width(&text) / 2.;
180+
const FONT_SIZE: f64 = 12.;
181+
182+
let text_width = text_width(&text, FONT_SIZE);
183+
184+
let text_texture_width = text_width / 2.;
180185

181186
let transform = calculate_arc_text_transform(angle, offset_angle, center, text_texture_width);
182187

editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::consts::{ANGLE_MEASURE_RADIUS_FACTOR, ARC_MEASURE_RADIUS_FACTOR_RANGE, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GRAY, SLOWING_DIVISOR};
22
use crate::messages::input_mapper::utility_types::input_mouse::{DocumentPosition, ViewportPosition};
3+
use crate::messages::portfolio::document::overlays::utility_functions::text_width;
34
use crate::messages::portfolio::document::overlays::utility_types::{OverlayProvider, Pivot};
45
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
56
use crate::messages::portfolio::document::utility_types::misc::PTZ;
@@ -288,7 +289,9 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
288289
angle_in_degrees
289290
};
290291
let text = format!("{}°", format_rounded(display_angle, 2));
291-
let text_texture_width = overlay_context.get_width(&text) / 2.;
292+
const FONT_SIZE: f64 = 12.;
293+
294+
let text_texture_width = text_width(&text, FONT_SIZE) / 2.;
292295
let text_texture_height = 12.;
293296
let text_angle_on_unit_circle = DVec2::from_angle((angle % TAU) / 2. + offset_angle);
294297
let text_texture_position = DVec2::new(

0 commit comments

Comments
 (0)