Desktop: Remove web_sys text measuring to fix node graph layer widths (#3455)
Some checks are pending
Editor: Dev & CI / build (push) Waiting to run
Editor: Dev & CI / cargo-deny (push) Waiting to run

* Remove web_sys text measuring

* Improve export

* Fix top of layer stack
This commit is contained in:
Adam Gerhant 2025-12-07 04:11:42 -08:00 committed by GitHub
parent 3926337b44
commit 9fc98cf03f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 72 additions and 112 deletions

View file

@ -3,8 +3,7 @@ use std::path::PathBuf;
use super::utility_types::misc::{GroupFolderType, SnappingState};
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
use crate::messages::portfolio::document::data_panel::DataPanelMessage;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::overlays::utility_types::OverlaysType;
use crate::messages::portfolio::document::overlays::utility_types::{OverlayContext, OverlaysType};
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GridSnapping};
use crate::messages::portfolio::utility_types::PanelType;

View file

@ -1,6 +1,5 @@
use super::node_graph::document_node_definitions;
use super::node_graph::utility_types::Transform;
use super::overlays::utility_types::Pivot;
use super::utility_types::error::EditorError;
use super::utility_types::misc::{GroupFolderType, SNAP_FUNCTIONS_FOR_BOUNDING_BOXES, SNAP_FUNCTIONS_FOR_PATHS, SnappingOptions, SnappingState};
use super::utility_types::network_interface::{self, NodeNetworkInterface, TransactionStatus};
@ -14,7 +13,7 @@ use crate::messages::portfolio::document::graph_operation::utility_types::Transf
use crate::messages::portfolio::document::node_graph::NodeGraphMessageContext;
use crate::messages::portfolio::document::node_graph::utility_types::FrontendGraphDataType;
use crate::messages::portfolio::document::overlays::grid_overlays::{grid_overlay, overlay_options};
use crate::messages::portfolio::document::overlays::utility_types::{OverlaysType, OverlaysVisibilitySettings};
use crate::messages::portfolio::document::overlays::utility_types::{OverlaysType, OverlaysVisibilitySettings, Pivot};
use crate::messages::portfolio::document::properties_panel::properties_panel_message_handler::PropertiesPanelMessageContext;
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, PTZ};

View file

@ -2,8 +2,17 @@ pub mod grid_overlays;
mod overlays_message;
mod overlays_message_handler;
pub mod utility_functions;
#[cfg_attr(not(target_family = "wasm"), path = "utility_types_vello.rs")]
pub mod utility_types;
// Native (nonwasm)
#[cfg(not(target_family = "wasm"))]
pub mod utility_types_native;
#[cfg(not(target_family = "wasm"))]
pub use utility_types_native as utility_types;
// WebAssembly
#[cfg(target_family = "wasm")]
pub mod utility_types_web;
#[cfg(target_family = "wasm")]
pub use utility_types_web as utility_types;
#[doc(inline)]
pub use overlays_message::{OverlaysMessage, OverlaysMessageDiscriminant};

View file

@ -6,9 +6,11 @@ use crate::messages::tool::common_functionality::shape_editor::{SelectedLayerSta
use crate::messages::tool::tool_messages::tool_prelude::{DocumentMessageHandler, PreferencesMessageHandler};
use glam::{DAffine2, DVec2};
use graphene_std::subpath::{Bezier, BezierHandles};
use graphene_std::text::{Font, FontCache, TextAlign, TextContext, TypesettingConfig};
use graphene_std::vector::misc::ManipulatorPointId;
use graphene_std::vector::{PointId, SegmentId, Vector};
use std::collections::HashMap;
use std::sync::{LazyLock, Mutex};
use wasm_bindgen::JsCast;
pub fn overlay_canvas_element() -> Option<web_sys::HtmlCanvasElement> {
@ -218,3 +220,35 @@ pub fn path_endpoint_overlays(document: &DocumentMessageHandler, shape_editor: &
}
}
}
// Global lazy initialized font cache and text context
pub static GLOBAL_FONT_CACHE: LazyLock<FontCache> = LazyLock::new(|| {
let mut font_cache = FontCache::default();
// Initialize with the hardcoded font used by overlay text
const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf");
let font = Font::new("Source Sans Pro".to_string(), "Regular".to_string());
font_cache.insert(font, String::new(), FONT_DATA.to_vec());
font_cache
});
pub static GLOBAL_TEXT_CONTEXT: LazyLock<Mutex<TextContext>> = LazyLock::new(|| Mutex::new(TextContext::default()));
pub fn text_width(text: &str, font_size: f64) -> f64 {
let typesetting = TypesettingConfig {
font_size,
line_height_ratio: 1.2,
character_spacing: 0.0,
max_width: None,
max_height: None,
tilt: 0.0,
align: TextAlign::Left,
};
// Load Source Sans Pro font data
// 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.
// TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size.
let font = Font::new("Source Sans Pro".to_string(), "Regular".to_string());
let mut text_context = GLOBAL_TEXT_CONTEXT.lock().expect("Failed to lock global text context");
let bounds = text_context.bounding_box(text, &font, &GLOBAL_FONT_CACHE, typesetting, false);
bounds.x
}

View file

@ -3,6 +3,7 @@ use crate::consts::{
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,
PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, RESIZE_HANDLE_SIZE, SKEW_TRIANGLE_OFFSET, SKEW_TRIANGLE_SIZE,
};
use crate::messages::portfolio::document::overlays::utility_functions::{GLOBAL_FONT_CACHE, GLOBAL_TEXT_CONTEXT};
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::prelude::Message;
use crate::messages::prelude::ViewportMessageHandler;
@ -13,30 +14,17 @@ use graphene_std::Color;
use graphene_std::math::quad::Quad;
use graphene_std::subpath::{self, Subpath};
use graphene_std::table::Table;
use graphene_std::text::TextContext;
use graphene_std::text::{Font, FontCache, TextAlign, TypesettingConfig};
use graphene_std::text::{Font, TextAlign, TypesettingConfig};
use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::misc::point_to_dvec2;
use graphene_std::vector::{PointId, SegmentId, Vector};
use kurbo::{self, BezPath, ParamCurve};
use kurbo::{Affine, PathSeg};
use std::collections::HashMap;
use std::sync::{Arc, LazyLock, Mutex, MutexGuard};
use std::sync::{Arc, Mutex, MutexGuard};
use vello::Scene;
use vello::peniko;
// Global lazy initialized font cache and text context
static GLOBAL_FONT_CACHE: LazyLock<FontCache> = LazyLock::new(|| {
let mut font_cache = FontCache::default();
// Initialize with the hardcoded font used by overlay text
const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf");
let font = Font::new("Source Sans Pro".to_string(), "Regular".to_string());
font_cache.insert(font, String::new(), FONT_DATA.to_vec());
font_cache
});
static GLOBAL_TEXT_CONTEXT: LazyLock<Mutex<TextContext>> = LazyLock::new(|| Mutex::new(TextContext::default()));
pub type OverlayProvider = fn(OverlayContext) -> Message;
pub fn empty_provider() -> OverlayProvider {
@ -393,10 +381,6 @@ impl OverlayContext {
self.internal().fill_path_pattern(subpaths, transform, color);
}
pub fn get_width(&self, text: &str) -> f64 {
self.internal().get_width(text)
}
pub fn text(&self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) {
let mut internal = self.internal();
internal.text(text, font_color, background_color, transform, padding, pivot);
@ -1034,29 +1018,6 @@ impl OverlayContextInternal {
self.scene.fill(peniko::Fill::NonZero, self.get_transform(), &brush, None, &path);
}
fn get_width(&mut self, text: &str) -> f64 {
// Use the actual text-to-path system to get precise text width
const FONT_SIZE: f64 = 12.0;
let typesetting = TypesettingConfig {
font_size: FONT_SIZE,
line_height_ratio: 1.2,
character_spacing: 0.0,
max_width: None,
max_height: None,
tilt: 0.0,
align: TextAlign::Left,
};
// Load Source Sans Pro font data
// 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.
// TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size.
let font = Font::new("Source Sans Pro".to_string(), "Regular".to_string());
let mut text_context = GLOBAL_TEXT_CONTEXT.lock().expect("Failed to lock global text context");
let bounds = text_context.bounding_box(text, &font, &GLOBAL_FONT_CACHE, typesetting, false);
bounds.x
}
fn text(&mut self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) {
// Use the proper text-to-path system for accurate text rendering
const FONT_SIZE: f64 = 12.0;

View file

@ -962,10 +962,6 @@ impl OverlayContext {
self.render_context.fill();
}
pub fn get_width(&self, text: &str) -> f64 {
self.render_context.measure_text(text).expect("Failed to measure text dimensions").width()
}
pub fn text(&self, text: &str, font_color: &str, background_color: Option<&str>, transform: DAffine2, padding: f64, pivot: [Pivot; 2]) {
let metrics = self.render_context.measure_text(text).expect("Failed to measure the text dimensions");
let x = match pivot[0] {

View file

@ -9,6 +9,7 @@ use crate::consts::{EXPORTS_TO_RIGHT_EDGE_PIXEL_GAP, EXPORTS_TO_TOP_EDGE_PIXEL_G
use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext;
use crate::messages::portfolio::document::node_graph::document_node_definitions::{DocumentNodeDefinition, resolve_document_node_type};
use crate::messages::portfolio::document::node_graph::utility_types::{Direction, FrontendClickTargets, FrontendGraphDataType, FrontendGraphInput, FrontendGraphOutput};
use crate::messages::portfolio::document::overlays::utility_functions::text_width;
use crate::messages::portfolio::document::utility_types::network_interface::resolved_types::ResolvedDocumentNodeTypes;
use crate::messages::portfolio::document::utility_types::wires::{GraphWireStyle, WirePath, WirePathUpdate, build_vector_wire};
use crate::messages::tool::common_functionality::graph_modification_utils;
@ -1052,7 +1053,11 @@ impl NodeNetworkInterface {
log::error!("Could not get downstream_connectors in primary_output_connected_to_layer");
return false;
};
let downstream_nodes = downstream_connectors.iter().filter_map(|connector| connector.node_id()).collect::<Vec<_>>();
let downstream_nodes = downstream_connectors
.iter()
.filter_map(|connector| connector.node_id().filter(|_| connector.input_index() == 0))
.collect::<Vec<_>>();
downstream_nodes.iter().any(|node_id| self.is_layer(node_id, network_path))
}
@ -1314,57 +1319,6 @@ impl NodeNetworkInterface {
.any(|id| id == potentially_upstream_node)
}
#[cfg(not(target_family = "wasm"))]
fn text_width(&self, node_id: &NodeId, network_path: &[NodeId]) -> Option<f64> {
warn!("Failed to find width of {node_id:#?} in network_path {network_path:?} due to non-wasm arch");
Some(0.)
}
#[cfg(target_family = "wasm")]
fn text_width(&self, node_id: &NodeId, network_path: &[NodeId]) -> Option<f64> {
let document = web_sys::window().unwrap().document().unwrap();
let div = match document.create_element("div") {
Ok(div) => div,
Err(err) => {
log::error!("Error creating div: {:?}", err);
return None;
}
};
// Set the div's style to make it offscreen and single line
match div.set_attribute("style", "position: absolute; top: -9999px; left: -9999px; white-space: nowrap;") {
Err(err) => {
log::error!("Error setting attribute: {:?}", err);
return None;
}
_ => {}
};
let name = self.display_name(node_id, network_path);
div.set_text_content(Some(&name));
// Append the div to the document body
match document.body().unwrap().append_child(&div) {
Err(err) => {
log::error!("Error setting adding child to document {:?}", err);
return None;
}
_ => {}
};
// Measure the width
let text_width = div.get_bounding_client_rect().width();
// Remove the div from the document
match document.body().unwrap().remove_child(&div) {
Err(_) => log::error!("Could not remove child when rendering text"),
_ => {}
};
Some(text_width)
}
pub fn from_old_network(old_network: OldNodeNetwork) -> Self {
let mut node_network = NodeNetwork::default();
let mut network_metadata = NodeNetworkMetadata::default();
@ -2121,19 +2075,19 @@ impl NodeNetworkInterface {
}
pub fn load_layer_width(&mut self, node_id: &NodeId, network_path: &[NodeId]) {
const GAP_WIDTH: f64 = 8.;
const FONT_SIZE: f64 = 14.;
let left_thumbnail_padding = GRID_SIZE as f64 / 2.;
let thumbnail_width = 3. * GRID_SIZE as f64;
let gap_width = 8.;
let text_width = self.text_width(node_id, network_path).unwrap_or_else(|| {
log::error!("Could not get text width for node {node_id}");
0.
});
let layer_text = self.display_name(node_id, network_path);
let text_width = text_width(&layer_text, FONT_SIZE);
let grip_padding = 4.;
let grip_width = 8.;
let icon_overhang_width = GRID_SIZE as f64 / 2.;
let layer_width_pixels = left_thumbnail_padding + thumbnail_width + gap_width + text_width + grip_padding + grip_width + icon_overhang_width;
let layer_width_pixels = left_thumbnail_padding + thumbnail_width + GAP_WIDTH + text_width + grip_padding + grip_width + icon_overhang_width;
let layer_width = ((layer_width_pixels / 24.).ceil() as u32).max(8);
let Some(node_metadata) = self.node_metadata_mut(node_id, network_path) else {

View file

@ -1,5 +1,6 @@
use crate::consts::{ARC_SNAP_THRESHOLD, GIZMO_HIDE_THRESHOLD};
use crate::messages::message::Message;
use crate::messages::portfolio::document::overlays::utility_functions::text_width;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::InputConnector;
@ -176,7 +177,11 @@ impl SweepAngleGizmo {
.to_degrees();
let text = format!("{}°", format_rounded(display_angle, 2));
let text_texture_width = overlay_context.get_width(&text) / 2.;
const FONT_SIZE: f64 = 12.;
let text_width = text_width(&text, FONT_SIZE);
let text_texture_width = text_width / 2.;
let transform = calculate_arc_text_transform(angle, offset_angle, center, text_texture_width);

View file

@ -1,5 +1,6 @@
use crate::consts::{ANGLE_MEASURE_RADIUS_FACTOR, ARC_MEASURE_RADIUS_FACTOR_RANGE, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GRAY, SLOWING_DIVISOR};
use crate::messages::input_mapper::utility_types::input_mouse::{DocumentPosition, ViewportPosition};
use crate::messages::portfolio::document::overlays::utility_functions::text_width;
use crate::messages::portfolio::document::overlays::utility_types::{OverlayProvider, Pivot};
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::misc::PTZ;
@ -288,7 +289,9 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
angle_in_degrees
};
let text = format!("{}°", format_rounded(display_angle, 2));
let text_texture_width = overlay_context.get_width(&text) / 2.;
const FONT_SIZE: f64 = 12.;
let text_texture_width = text_width(&text, FONT_SIZE) / 2.;
let text_texture_height = 12.;
let text_angle_on_unit_circle = DVec2::from_angle((angle % TAU) / 2. + offset_angle);
let text_texture_position = DVec2::new(