Make Data panel scalars selectable inline, denoise float display, and standardize breadcrumb string truncation (#4470)

* Make Data panel scalars selectable inline, denoise float display, and move breadcrumb truncation to Rust

* Code review fixes

* Remove denoising from f32
This commit is contained in:
Keavon Chambers
2026-08-22 01:28:23 -07:00
committed by GitHub
parent 2fa6b743fe
commit c8667edacc
14 changed files with 253 additions and 187 deletions

View File

@@ -233,6 +233,24 @@ pub struct ColorInput {
pub on_commit: WidgetCallback<()>,
}
/// Shortens a breadcrumb label to fit the trail, replacing the tail with an ellipsis.
/// Quotes delimit a string value, so a quoted label budgets only the text between them.
pub fn truncate_breadcrumb_label(label: &str) -> String {
const MAX_CHARACTERS: usize = 40;
let quoted = label.len() >= 2 && label.starts_with('"') && label.ends_with('"');
let content = if quoted { &label[1..label.len() - 1] } else { label };
if content.chars().count() <= MAX_CHARACTERS {
return label.to_string();
}
let mut truncated: String = content.chars().take(MAX_CHARACTERS - 1).collect();
truncated.push('…');
if quoted { format!("\"{truncated}\"") } else { truncated }
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder)]
#[derivative(Debug, PartialEq)]

View File

@@ -64,6 +64,8 @@ pub struct TextLabel {
pub italic: bool,
pub monospace: bool,
pub multiline: bool,
pub enquote: bool,
pub selectable: bool,
#[serde(rename = "centerAlign")]
pub center_align: bool,
#[serde(rename = "tableAlign")]

View File

@@ -13,6 +13,7 @@ use graphene_std::color::SRGBA8;
use graphene_std::extract_xy::XY;
use graphene_std::gradient::Gradient;
use graphene_std::list::{Item, List, NodeIdPath};
use graphene_std::math::float_noise::round_away_float_noise;
use graphene_std::memo::IORecord;
use graphene_std::raster::{
CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, LuminanceCalculation, NoiseType, RedGreenBlue, RedGreenBlueAlpha, RelativeAbsolute, SelectiveColorChoice,
@@ -150,7 +151,8 @@ impl DataPanelMessageHandler {
// Element path breadcrumbs
if !layout_data.breadcrumbs.is_empty() {
let breadcrumb = BreadcrumbTrailButtons::new(layout_data.breadcrumbs)
let labels = layout_data.breadcrumbs.iter().map(|label| truncate_breadcrumb_label(label)).collect();
let breadcrumb = BreadcrumbTrailButtons::new(labels)
.on_update(|&len| DataPanelMessage::TruncateElementPath { len: len as usize }.into())
.widget_instance();
widgets.push(breadcrumb);
@@ -796,18 +798,41 @@ impl TableItemLayout for Gradient {
}
}
macro_rules! impl_table_item_layout_for_number {
($($ty:ty => $type_name:literal),* $(,)?) => {
$(
impl TableItemLayout for $ty {
fn type_name() -> &'static str {
$type_name
}
fn identifier(&self) -> String {
format!("{self}")
}
fn value_widgets(&self, _target: PathStep, _data: &LayoutData) -> Vec<WidgetInstance> {
vec![TextLabel::new(self.identifier()).selectable(true).narrow(true).widget_instance()]
}
}
)*
}
}
impl_table_item_layout_for_number!(
f32 => "Number (f32)",
u32 => "Number (u32)",
i32 => "Number (i32)",
u64 => "Number (u64)",
i64 => "Number (i64)",
);
// Denoised so 0.1 + 0.2 reads as 0.3 rather than 0.30000000000000004. We don't do this for f32 because it lacks precision to reliably distinguish between intentional digits and noise.
impl TableItemLayout for f64 {
fn type_name() -> &'static str {
"Number (f64)"
"Number"
}
fn identifier(&self) -> String {
format!("{self}")
format!("{}", round_away_float_noise(*self))
}
// Values fall back to the default drill-in button (labeled via `identifier`); the value page shows the rich `NumberInput`.
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![LayoutGroup::row(vec![
NumberInput::new(Some(*self)).disabled(true).max_width(220).display_decimal_places(20).widget_instance(),
])]
fn value_widgets(&self, _target: PathStep, _data: &LayoutData) -> Vec<WidgetInstance> {
vec![TextLabel::new(self.identifier()).selectable(true).narrow(true).widget_instance()]
}
}
@@ -816,88 +841,10 @@ impl TableItemLayout for u8 {
"Byte"
}
fn identifier(&self) -> String {
format!("{self:02X}")
format!("0x{self:02X} ({self})")
}
// Values fall back to the default drill-in button (labeled with the hex string via `identifier`); the value page shows the same hex value as a label.
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![LayoutGroup::row(vec![TextLabel::new(self.identifier()).widget_instance()])]
}
}
impl TableItemLayout for f32 {
fn type_name() -> &'static str {
"Number (f32)"
}
fn identifier(&self) -> String {
format!("{self}")
}
// Values fall back to the default drill-in button (labeled via `identifier`); the value page shows the rich `NumberInput`.
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![LayoutGroup::row(vec![
NumberInput::new(Some(*self as f64)).disabled(true).max_width(220).display_decimal_places(20).widget_instance(),
])]
}
}
impl TableItemLayout for u32 {
fn type_name() -> &'static str {
"Number (u32)"
}
fn identifier(&self) -> String {
format!("{self}")
}
// Values fall back to the default drill-in button (labeled via `identifier`); the value page shows the rich `NumberInput`.
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![LayoutGroup::row(vec![
NumberInput::new(Some(*self as f64)).disabled(true).max_width(220).display_decimal_places(20).widget_instance(),
])]
}
}
impl TableItemLayout for i32 {
fn type_name() -> &'static str {
"Number (i32)"
}
fn identifier(&self) -> String {
format!("{self}")
}
// Values fall back to the default drill-in button (labeled via `identifier`); the value page shows the rich `NumberInput`.
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![LayoutGroup::row(vec![
NumberInput::new(Some(*self as f64)).disabled(true).max_width(220).display_decimal_places(20).widget_instance(),
])]
}
}
impl TableItemLayout for i64 {
fn type_name() -> &'static str {
"Number (i64)"
}
fn identifier(&self) -> String {
format!("{self}")
}
// Values fall back to the default drill-in button (labeled via `identifier`); the value page shows the rich `NumberInput`.
// TODO: Make this robust for large i64 values that don't fit in f64 (beyond roughly 2^53), as with u64.
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![LayoutGroup::row(vec![
NumberInput::new(Some(*self as f64)).disabled(true).max_width(220).display_decimal_places(20).widget_instance(),
])]
}
}
impl TableItemLayout for u64 {
fn type_name() -> &'static str {
"Number (u64)"
}
fn identifier(&self) -> String {
format!("{self}")
}
// Values fall back to the default drill-in button (labeled via `identifier`); the value page shows the rich `NumberInput`.
// TODO: Make this robust for large u64 values that don't fit in f64 (above roughly 2^53). Perhaps using a bigint kind of approach through the widget's data flow.
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![LayoutGroup::row(vec![
NumberInput::new(Some(*self as f64)).disabled(true).max_width(220).display_decimal_places(20).widget_instance(),
])]
fn value_widgets(&self, _target: PathStep, _data: &LayoutData) -> Vec<WidgetInstance> {
vec![TextLabel::new(self.identifier()).selectable(true).narrow(true).widget_instance()]
}
}
@@ -921,20 +868,50 @@ impl TableItemLayout for String {
"String"
}
fn identifier(&self) -> String {
// Show the first line, and if there are more, indicate that with an ellipsis
let first_line = self.lines().next().unwrap_or("");
if self.lines().count() > 1 {
format!("\"{}\"", first_line)
} else {
format!("\"{}\"", first_line)
}
format!("\"{}\"", string_preview(self))
}
fn layout_with_breadcrumb(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
data.breadcrumbs.push(character_count_label(self));
self.value_page(data)
}
// The preview truncates for length and line breaks, so a button beside it reaches the full text, labeled by length
fn value_widgets(&self, target: PathStep, _data: &LayoutData) -> Vec<WidgetInstance> {
vec![
TextLabel::new(string_preview(self)).enquote(true).selectable(true).monospace(true).narrow(true).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
TextButton::new(character_count_label(self))
.on_update(move |_| DataPanelMessage::PushToElementPath { step: target.clone() }.into())
.narrow(true)
.widget_instance(),
]
}
// Values fall back to the default drill-in button (labeled with the truncated quoted preview via `identifier`); the value page shows the full multi-line text in a `TextAreaInput`.
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![LayoutGroup::row(vec![TextAreaInput::new(self.to_string()).monospace(true).disabled(true).widget_instance()])]
}
}
fn character_count_label(value: &str) -> String {
let character_count = value.chars().count();
format!("{character_count} Char{}", if character_count == 1 { "" } else { "s" })
}
/// Shortens a string to fit a table cell, cutting at the first line break or 40 characters with an ellipsis.
/// The 40 matches `truncate_breadcrumb_label`, so a preview reaching the trail isn't cut twice.
fn string_preview(value: &str) -> String {
const MAX_CHARACTERS: usize = 40;
let first_line = value.lines().next().unwrap_or_default();
let cut_by_line_break = value.contains('\n');
let cut_by_length = first_line.chars().count() > MAX_CHARACTERS;
if !cut_by_line_break && !cut_by_length {
return value.to_string();
}
first_line.chars().take(MAX_CHARACTERS - 1).chain(['…']).collect()
}
impl TableItemLayout for Option<f64> {
fn type_name() -> &'static str {
"Option<f64>"
@@ -943,7 +920,12 @@ impl TableItemLayout for Option<f64> {
"Option<f64>".to_string()
}
fn value_widgets(&self, _target: PathStep, _data: &LayoutData) -> Vec<WidgetInstance> {
vec![TextLabel::new(format!("{self:?}")).narrow(true).widget_instance()]
let text = match self {
Some(value) => format!("Some({})", round_away_float_noise(*value)),
None => "None".to_string(),
};
vec![TextLabel::new(text).selectable(true).narrow(true).widget_instance()]
}
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![LayoutGroup::row(self.value_widgets(PathStep::Element(0), _data))]
@@ -988,7 +970,7 @@ impl TableItemLayout for DAffine2 {
"Transform".to_string()
}
fn value_widgets(&self, _target: PathStep, _data: &LayoutData) -> Vec<WidgetInstance> {
vec![TextLabel::new(format_transform_matrix(*self)).narrow(true).widget_instance()]
transform_widgets(*self)
}
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![LayoutGroup::row(self.value_widgets(PathStep::Element(0), _data))]
@@ -1003,8 +985,7 @@ impl TableItemLayout for Affine2 {
"Transform".to_string()
}
fn value_widgets(&self, _target: PathStep, _data: &LayoutData) -> Vec<WidgetInstance> {
let matrix = DAffine2::from_cols_array(&self.to_cols_array().map(|x| x as f64));
vec![TextLabel::new(format_transform_matrix(matrix)).narrow(true).widget_instance()]
transform_widgets(DAffine2::from_cols_array(&self.to_cols_array().map(|x| x as f64)))
}
fn value_page(&self, _data: &mut LayoutData) -> Vec<LayoutGroup> {
vec![LayoutGroup::row(self.value_widgets(PathStep::Element(0), _data))]
@@ -1385,7 +1366,8 @@ fn drilldown_attribute_layout(any: &dyn Any, data: &mut LayoutData) -> Option<Ve
None
}
fn format_transform_matrix(transform: DAffine2) -> String {
/// Decomposes a transform into location, rotation, and scale, each headed by the icon of its Layer menu action.
fn transform_widgets(transform: DAffine2) -> Vec<WidgetInstance> {
let (scale, angle, translation) = if transform.matrix2.determinant().abs() <= f64::EPSILON {
let [col_0, col_1] = transform.matrix2.to_cols_array_2d().map(|[x, y]| DVec2::new(x, y));
@@ -1403,15 +1385,24 @@ fn format_transform_matrix(transform: DAffine2) -> String {
} else {
transform.to_scale_angle_translation()
};
let rotation = format_rounded(angle.to_degrees(), 3);
format!(
"Location: ({} px, {} px) — Rotation: {rotation}° — Scale: ({}x, {}x)",
format_rounded(translation.x, 3),
format_rounded(translation.y, 3),
format_rounded(scale.x, 3),
format_rounded(scale.y, 3)
)
let location_text = format!("({} px, {} px)", format_rounded(translation.x, 3), format_rounded(translation.y, 3));
let rotation_text = format!("{}°", format_rounded(angle.to_degrees(), 3));
let scale_text = format!("({}x, {}x)", format_rounded(scale.x, 3), format_rounded(scale.y, 3));
vec![
IconLabel::new("TransformationGrab").tooltip_label("Location").widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(),
TextLabel::new(location_text).narrow(true).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
IconLabel::new("TransformationRotate").tooltip_label("Rotation").widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(),
TextLabel::new(rotation_text).narrow(true).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
IconLabel::new("TransformationScale").tooltip_label("Scale").widget_instance(),
Separator::new(SeparatorStyle::Related).widget_instance(),
TextLabel::new(scale_text).narrow(true).widget_instance(),
]
}
fn format_dvec2(value: DVec2) -> String {

View File

@@ -2429,7 +2429,7 @@ impl NodeGraphMessageHandler {
if subgraph_path_names_length >= 2 {
widgets.extend([
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
BreadcrumbTrailButtons::new(subgraph_path_names)
BreadcrumbTrailButtons::new(subgraph_path_names.iter().map(|name| truncate_breadcrumb_label(name)).collect())
.on_update(move |index| {
DocumentMessage::ExitNestedNetwork {
steps_back: subgraph_path_names_length - (*index as usize) - 1,

View File

@@ -16,6 +16,7 @@ use crate::messages::tool::utility_types::*;
use glam::{DAffine2, DMat2, DVec2};
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use graphene_std::math::float_noise::round_away_float_noise;
use graphene_std::vector::algorithms::shapes::{arc_bezpath, regular_polygon_bezpath, star_polygon_bezpath};
use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::misc::{ArcType, GridType, SpiralType, dvec2_to_point};
@@ -570,26 +571,6 @@ pub fn format_rounded(value: f64, precision: usize) -> String {
}
}
/// Recovers the intended number from floating point imprecision noise when that can be done reliably, e.g. 0.30000000000000004 -> 0.3.
/// Rounding to each significant digit count from 1 to 12, the first candidate within a relative 1e-13 of the original is accepted.
/// Actual high-precision values (like 0.3333333333333333) never pass the tolerance and are returned unchanged.
pub fn round_away_float_noise(value: f64) -> f64 {
if value == 0. || !value.is_finite() {
return if value == 0. { 0. } else { value };
}
let exponent = value.abs().log10().floor() as i32;
for significant_digits in 1..=12 {
let scale = 10_f64.powi(significant_digits - 1 - exponent);
let rounded = (value * scale).round() / scale;
if ((rounded - value) / value).abs() < 1e-13 {
return rounded;
}
}
value
}
/// Gives the approximated angle to display in degrees, given an angle in degrees.
pub fn calculate_display_angle(angle: f64) -> f64 {
if angle.is_sign_positive() {
@@ -634,37 +615,6 @@ pub fn extract_grid_parameters(layer: LayerNodeIdentifier, document: &DocumentMe
mod tests {
use super::*;
#[test]
fn round_away_float_noise_snaps_noisy_values() {
assert_eq!(round_away_float_noise(0.1 + 0.2), 0.3);
assert_eq!(round_away_float_noise(0.3000000000000012), 0.3);
assert_eq!(round_away_float_noise(2.99999999999993), 3.);
assert_eq!(round_away_float_noise(45.00000000000001), 45.);
}
#[test]
fn round_away_float_noise_keeps_honest_values() {
assert_eq!(round_away_float_noise(1. / 3.), 1. / 3.);
assert_eq!(round_away_float_noise(0.2394023940209349), 0.2394023940209349);
assert_eq!(round_away_float_noise(0.25), 0.25);
assert_eq!(round_away_float_noise(-17.5), -17.5);
}
#[test]
fn round_away_float_noise_keeps_deliberate_values_with_zero_runs() {
assert_eq!(round_away_float_noise(0.30000005), 0.30000005);
assert_eq!(round_away_float_noise(0.3000000000001), 0.3000000000001);
assert_eq!(round_away_float_noise(1.00000001), 1.00000001);
assert_eq!(round_away_float_noise(2.9999993), 2.9999993);
}
#[test]
fn round_away_float_noise_normalizes_zero() {
let result = round_away_float_noise(-0.);
assert_eq!(result, 0.);
assert!(result.is_sign_positive());
}
#[test]
fn format_rounded_trims_trailing_zeros_when_exact() {
assert_eq!(format_rounded(0.25, 2), "0.25");

View File

@@ -12,31 +12,11 @@
export let tooltipShortcut: ActionShortcut | undefined = undefined;
// Callbacks
export let action: (index: number) => void;
function truncate(label: string): string {
let maxLength = 40;
if (label.length <= maxLength) return label;
let truncated = label;
const hasQuotes = label.startsWith(`"`) && label.endsWith(`"`);
if (hasQuotes) {
truncated = label.slice(1, -1);
maxLength -= 2;
}
truncated = truncated.slice(0, maxLength - 1) + "…";
if (hasQuotes) truncated = `"${truncated}"`;
return truncated;
}
</script>
<LayoutRow class="breadcrumb-trail-buttons" {tooltipLabel} {tooltipDescription} {tooltipShortcut}>
{#each labels as label, index}
<TextButton label={truncate(label)} emphasized={index === labels.length - 1} {disabled} action={() => !disabled && index !== labels.length - 1 && action(index)} />
<TextButton {label} emphasized={index === labels.length - 1} {disabled} action={() => !disabled && index !== labels.length - 1 && action(index)} />
{/each}
</LayoutRow>

View File

@@ -205,6 +205,10 @@
textarea {
color: var(--color-8-uppergray);
}
input {
pointer-events: none;
}
}
}
</style>

View File

@@ -19,6 +19,8 @@
export let italic = false;
export let monospace = false;
export let multiline = false;
export let enquote = false;
export let selectable = false;
export let centerAlign = false;
export let tableAlign = false;
// Sizing
@@ -76,6 +78,8 @@
class:italic
class:monospace
class:multiline
class:enquote
class:selectable
class:center-align={centerAlign}
class:table-align={tableAlign}
style:min-width={minWidthCharacters ? `${minWidthCharacters}ch` : minWidth > 0 ? `${minWidth}px` : undefined}
@@ -125,6 +129,17 @@
margin: 4px 0;
}
&.enquote::before,
&.enquote::after {
content: '"';
user-select: none;
}
&.selectable {
user-select: text;
cursor: text;
}
&.center-align {
text-align: center;
}

View File

@@ -128,6 +128,7 @@ export function onPointerMove(e: PointerEvent, editor: EditorWrapper, documentSt
export function onPointerDown(e: PointerEvent, editor: EditorWrapper, dialogStore: DialogStore) {
potentiallyRestoreCanvasFocus(e);
potentiallyClearTextSelection(e);
const inFloatingMenu = e.target instanceof Element && e.target.closest("[data-floating-menu-content]");
const isTargetingCanvas = !inFloatingMenu && e.target instanceof Element && e.target.closest("[data-viewport], [data-viewport-container], [data-node-graph]");
@@ -351,6 +352,20 @@ function targetIsTextField(target: EventTarget | HTMLElement | undefined): boole
);
}
function potentiallyClearTextSelection(e: PointerEvent) {
const target = e.target instanceof Element ? e.target : undefined;
if (target && (targetIsTextField(target) || window.getComputedStyle(target).userSelect !== "none")) return;
// A text control's selection lives in its shadow tree, which the document's `Selection` reports as collapsed and cannot clear, so each control holding one is collapsed through its own API
const controls = window.document.querySelectorAll<HTMLInputElement | HTMLTextAreaElement>("textarea, input[type='text']");
controls.forEach((control) => {
const caret = control.selectionStart;
if (typeof caret === "number" && caret !== control.selectionEnd) control.setSelectionRange(caret, caret);
});
window.getSelection()?.removeAllRanges();
}
function potentiallyRestoreCanvasFocus(e: Event) {
const appElement = window.document.querySelector("[data-app-container]");
const app = appElement instanceof HTMLElement ? appElement : undefined;

View File

@@ -0,0 +1,71 @@
use std::fmt::Write;
/// Recovers the intended number from floating point imprecision noise when that can be done reliably, e.g. 0.30000000000000004 -> 0.3.
/// Rounding to each significant digit count from 1 to 12, the first candidate within a relative 1e-13 of the original is accepted.
/// Actual high-precision values (like 0.3333333333333333) never pass the tolerance and are returned unchanged.
/// f64 only, as f32 lacks precision to reliably distinguish between intentional digits and noise.
pub fn round_away_float_noise(value: f64) -> f64 {
if value == 0. || !value.is_finite() {
return if value == 0. { 0. } else { value };
}
// Candidates come from decimal formatting rather than scaling by a power of ten, which is inexact enough to invent
// noise of its own: it turns 1e300 into 9.999999999999999e299 and 999999.9999999 into 999999.9999999999.
// One buffer serves every candidate, since the digit counts are tried in turn.
let mut buffer = String::with_capacity(32);
for significant_digits in 1..=12 {
buffer.clear();
let _ = write!(buffer, "{value:.*e}", significant_digits - 1);
let Ok(rounded) = buffer.parse::<f64>() else { continue };
if ((rounded - value) / value).abs() < 1e-13 {
return rounded;
}
}
value
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_away_float_noise_snaps_noisy_values() {
assert_eq!(round_away_float_noise(0.1 + 0.2), 0.3);
assert_eq!(round_away_float_noise(0.3000000000000012), 0.3);
assert_eq!(round_away_float_noise(2.99999999999993), 3.);
assert_eq!(round_away_float_noise(45.00000000000001), 45.);
}
#[test]
fn round_away_float_noise_keeps_honest_values() {
assert_eq!(round_away_float_noise(1. / 3.), 1. / 3.);
assert_eq!(round_away_float_noise(0.2394023940209349), 0.2394023940209349);
assert_eq!(round_away_float_noise(0.25), 0.25);
assert_eq!(round_away_float_noise(-17.5), -17.5);
}
#[test]
fn round_away_float_noise_keeps_deliberate_values_with_zero_runs() {
assert_eq!(round_away_float_noise(0.30000005), 0.30000005);
assert_eq!(round_away_float_noise(0.3000000000001), 0.3000000000001);
assert_eq!(round_away_float_noise(1.00000001), 1.00000001);
assert_eq!(round_away_float_noise(2.9999993), 2.9999993);
}
#[test]
fn round_away_float_noise_normalizes_zero() {
let result = round_away_float_noise(-0.);
assert_eq!(result, 0.);
assert!(result.is_sign_positive());
}
#[test]
fn round_away_float_noise_keeps_extreme_magnitudes_exact() {
assert_eq!(round_away_float_noise(1e300), 1e300);
assert_eq!(round_away_float_noise(1.5e300), 1.5e300);
assert_eq!(round_away_float_noise(1e-300), 1e-300);
assert_eq!(round_away_float_noise(f64::MIN_POSITIVE), f64::MIN_POSITIVE);
}
}

View File

@@ -1,4 +1,5 @@
pub mod bbox;
pub mod float_noise;
pub mod polynomial;
pub mod quad;
pub mod rect;

View File

@@ -1,6 +1,7 @@
use crate::Node;
use crate::math::float_noise::round_away_float_noise;
use crate::transform::Footprint;
use glam::DVec2;
use glam::{DAffine2, DVec2};
use std::future::Future;
use std::marker::PhantomData;
@@ -45,11 +46,26 @@ pub trait Convert<T, C>: Sized {
fn convert(self, footprint: Footprint, converter: C) -> impl Future<Output = T> + Send;
}
impl<T: ToString + Send> Convert<String, ()> for T {
/// Converts this type into a `String` using its `ToString` implementation.
/// Implements the [`Convert`] trait for formatting a type into a `String` via [`ToString`].
macro_rules! impl_convert_to_string {
($($from:ty),* $(,)?) => {
$(
impl Convert<String, ()> for $from {
#[inline]
async fn convert(self, _: Footprint, _converter: ()) -> String {
self.to_string()
}
}
)*
};
}
impl_convert_to_string!(f32, u32, u64, i32, i64, bool, DVec2, DAffine2);
// Denoised so 0.1 + 0.2 reaches the string as "0.3" rather than "0.30000000000000004"
impl Convert<String, ()> for f64 {
#[inline]
async fn convert(self, _: Footprint, _converter: ()) -> String {
self.to_string()
round_away_float_noise(self).to_string()
}
}

View File

@@ -67,6 +67,7 @@ pub mod repeat {
}
pub mod math {
pub use core_types::math::float_noise;
pub use core_types::math::quad;
pub mod math_ext {

View File

@@ -9,6 +9,7 @@ mod to_path;
use convert_case::{Boundary, Converter, pattern};
use core_types::graphene_hash::CacheHash;
use core_types::list::{Item, List};
use core_types::math::float_noise::round_away_float_noise;
use core_types::registry::types::{SignedInteger, TextArea};
use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractVarArgs, OwnedContextImpl};
use dyn_any::DynAny;
@@ -299,6 +300,7 @@ fn format_number(
start_at_10000: Item<bool>,
) -> Item<String> {
let (number, attributes) = number.into_parts();
let number = round_away_float_noise(number);
let (decimal_places, fixed_decimals, use_thousands_separator, start_at_10000) =
(*decimal_places.element(), *fixed_decimals.element(), *use_thousands_separator.element(), *start_at_10000.element());
let decimal_separator = decimal_separator.element().clone();