diff --git a/editor/src/messages/layout/layout_message_handler.rs b/editor/src/messages/layout/layout_message_handler.rs index a35861d6..a5b33e56 100644 --- a/editor/src/messages/layout/layout_message_handler.rs +++ b/editor/src/messages/layout/layout_message_handler.rs @@ -276,13 +276,13 @@ impl LayoutMessageHandler { responses.add(callback_message); } - Widget::PivotInput(pivot_input) => { + Widget::ReferencePointInput(reference_point_input) => { let callback_message = match action { - WidgetValueAction::Commit => (pivot_input.on_commit.callback)(&()), + WidgetValueAction::Commit => (reference_point_input.on_commit.callback)(&()), WidgetValueAction::Update => { - let update_value = value.as_str().expect("PivotInput update was not of type: u64"); - pivot_input.position = update_value.into(); - (pivot_input.on_update.callback)(pivot_input) + let update_value = value.as_str().expect("ReferencePointInput update was not of type: u64"); + reference_point_input.value = update_value.into(); + (reference_point_input.on_update.callback)(reference_point_input) } }; diff --git a/editor/src/messages/layout/utility_types/layout_widget.rs b/editor/src/messages/layout/utility_types/layout_widget.rs index 6ebf1020..a183a89e 100644 --- a/editor/src/messages/layout/utility_types/layout_widget.rs +++ b/editor/src/messages/layout/utility_types/layout_widget.rs @@ -373,7 +373,7 @@ impl LayoutGroup { Widget::TextInput(x) => &mut x.tooltip, Widget::TextLabel(x) => &mut x.tooltip, Widget::BreadcrumbTrailButtons(x) => &mut x.tooltip, - Widget::InvisibleStandinInput(_) | Widget::PivotInput(_) | Widget::RadioInput(_) | Widget::Separator(_) | Widget::WorkingColorsInput(_) | Widget::NodeCatalog(_) => continue, + Widget::InvisibleStandinInput(_) | Widget::ReferencePointInput(_) | Widget::RadioInput(_) | Widget::Separator(_) | Widget::WorkingColorsInput(_) | Widget::NodeCatalog(_) => continue, }; if val.is_empty() { val.clone_from(&tooltip); @@ -546,7 +546,7 @@ pub enum Widget { NodeCatalog(NodeCatalog), NumberInput(NumberInput), ParameterExposeButton(ParameterExposeButton), - PivotInput(PivotInput), + ReferencePointInput(ReferencePointInput), PopoverButton(PopoverButton), RadioInput(RadioInput), Separator(Separator), @@ -621,7 +621,7 @@ impl DiffUpdate { | Widget::CurveInput(_) | Widget::InvisibleStandinInput(_) | Widget::NodeCatalog(_) - | Widget::PivotInput(_) + | Widget::ReferencePointInput(_) | Widget::RadioInput(_) | Widget::Separator(_) | Widget::TextAreaInput(_) diff --git a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs index 56bf475d..33353dbf 100644 --- a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs @@ -1,9 +1,9 @@ use crate::messages::input_mapper::utility_types::misc::ActionKeys; use crate::messages::layout::utility_types::widget_prelude::*; use derivative::*; -use glam::DVec2; use graphene_core::Color; use graphene_core::raster::curve::Curve; +use graphene_std::transform::ReferencePoint; use graphite_proc_macros::WidgetBuilder; #[derive(Clone, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder, specta::Type)] @@ -411,100 +411,18 @@ pub struct CurveInput { #[derive(Clone, Default, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder, specta::Type)] #[derivative(Debug, PartialEq)] -pub struct PivotInput { +pub struct ReferencePointInput { #[widget_builder(constructor)] - pub position: PivotPosition, + pub value: ReferencePoint, pub disabled: bool, // Callbacks #[serde(skip)] #[derivative(Debug = "ignore", PartialEq = "ignore")] - pub on_update: WidgetCallback, + pub on_update: WidgetCallback, #[serde(skip)] #[derivative(Debug = "ignore", PartialEq = "ignore")] pub on_commit: WidgetCallback<()>, } - -#[derive(Clone, Copy, serde::Serialize, serde::Deserialize, Debug, Default, PartialEq, Eq, specta::Type)] -pub enum PivotPosition { - #[default] - None, - TopLeft, - TopCenter, - TopRight, - CenterLeft, - Center, - CenterRight, - BottomLeft, - BottomCenter, - BottomRight, -} - -impl From<&str> for PivotPosition { - fn from(input: &str) -> Self { - match input { - "None" => PivotPosition::None, - "TopLeft" => PivotPosition::TopLeft, - "TopCenter" => PivotPosition::TopCenter, - "TopRight" => PivotPosition::TopRight, - "CenterLeft" => PivotPosition::CenterLeft, - "Center" => PivotPosition::Center, - "CenterRight" => PivotPosition::CenterRight, - "BottomLeft" => PivotPosition::BottomLeft, - "BottomCenter" => PivotPosition::BottomCenter, - "BottomRight" => PivotPosition::BottomRight, - _ => panic!("Failed parsing unrecognized PivotPosition enum value '{input}'"), - } - } -} - -impl From for Option { - fn from(input: PivotPosition) -> Self { - match input { - PivotPosition::None => None, - PivotPosition::TopLeft => Some(DVec2::new(0., 0.)), - PivotPosition::TopCenter => Some(DVec2::new(0.5, 0.)), - PivotPosition::TopRight => Some(DVec2::new(1., 0.)), - PivotPosition::CenterLeft => Some(DVec2::new(0., 0.5)), - PivotPosition::Center => Some(DVec2::new(0.5, 0.5)), - PivotPosition::CenterRight => Some(DVec2::new(1., 0.5)), - PivotPosition::BottomLeft => Some(DVec2::new(0., 1.)), - PivotPosition::BottomCenter => Some(DVec2::new(0.5, 1.)), - PivotPosition::BottomRight => Some(DVec2::new(1., 1.)), - } - } -} - -impl From for PivotPosition { - fn from(input: DVec2) -> Self { - const TOLERANCE: f64 = 1e-5_f64; - if input.y.abs() < TOLERANCE { - if input.x.abs() < TOLERANCE { - return PivotPosition::TopLeft; - } else if (input.x - 0.5).abs() < TOLERANCE { - return PivotPosition::TopCenter; - } else if (input.x - 1.).abs() < TOLERANCE { - return PivotPosition::TopRight; - } - } else if (input.y - 0.5).abs() < TOLERANCE { - if input.x.abs() < TOLERANCE { - return PivotPosition::CenterLeft; - } else if (input.x - 0.5).abs() < TOLERANCE { - return PivotPosition::Center; - } else if (input.x - 1.).abs() < TOLERANCE { - return PivotPosition::CenterRight; - } - } else if (input.y - 1.).abs() < TOLERANCE { - if input.x.abs() < TOLERANCE { - return PivotPosition::BottomLeft; - } else if (input.x - 0.5).abs() < TOLERANCE { - return PivotPosition::BottomCenter; - } else if (input.x - 1.).abs() < TOLERANCE { - return PivotPosition::BottomRight; - } - } - PivotPosition::None - } -} diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 8117e241..1a73d938 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -23,7 +23,7 @@ use graphene_core::vector::style::{GradientType, LineCap, LineJoin}; use graphene_std::animation::RealTimeMode; use graphene_std::application_io::TextureFrameTable; use graphene_std::ops::XY; -use graphene_std::transform::Footprint; +use graphene_std::transform::{Footprint, ReferencePoint}; use graphene_std::vector::VectorDataTable; use graphene_std::vector::misc::ArcType; use graphene_std::vector::misc::{BooleanOperation, GridType}; @@ -178,6 +178,7 @@ pub(crate) fn property_from_type( Some(x) if x == TypeId::of::() => vector_data_widget(default_info).into(), Some(x) if x == TypeId::of::() || x == TypeId::of::>() || x == TypeId::of::() => raster_widget(default_info).into(), Some(x) if x == TypeId::of::() => group_widget(default_info).into(), + Some(x) if x == TypeId::of::() => reference_point_widget(default_info, false).into(), Some(x) if x == TypeId::of::() => footprint_widget(default_info, &mut extra_widgets), Some(x) if x == TypeId::of::() => blend_mode_widget(default_info), Some(x) if x == TypeId::of::() => real_time_mode_widget(default_info), @@ -291,6 +292,27 @@ pub fn bool_widget(parameter_widgets_info: ParameterWidgetsInfo, checkbox_input: widgets } +pub fn reference_point_widget(parameter_widgets_info: ParameterWidgetsInfo, disabled: bool) -> Vec { + let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info; + + let mut widgets = start_widgets(parameter_widgets_info, FrontendGraphDataType::General); + + let Some(input) = document_node.inputs.get(index) else { + log::warn!("A widget failed to be built because its node's input index is invalid."); + return vec![]; + }; + if let Some(&TaggedValue::ReferencePoint(reference_point)) = input.as_non_exposed_value() { + widgets.extend_from_slice(&[ + Separator::new(SeparatorType::Unrelated).widget_holder(), + ReferencePointInput::new(reference_point) + .on_update(update_value(move |x: &ReferencePointInput| TaggedValue::ReferencePoint(x.value), node_id, index)) + .disabled(disabled) + .widget_holder(), + ]) + } + widgets +} + pub fn footprint_widget(parameter_widgets_info: ParameterWidgetsInfo, extra_widgets: &mut Vec) -> LayoutGroup { let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info; diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 9a428f9f..4b1f2c28 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -828,6 +828,34 @@ impl MessageHandler> for PortfolioMes .set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::Bool(true), false), network_path); } + // Upgrade the Mirror node to add the `reference_point` input and change `offset` from `DVec2` to `f64` + if reference == "Mirror" && inputs_count == 4 { + let node_definition = resolve_document_node_type(reference).unwrap(); + let new_node_template = node_definition.default_node_template(); + let document_node = new_node_template.document_node; + document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone()); + document + .network_interface + .replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata); + + let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path); + + let Some(&TaggedValue::DVec2(old_offset)) = old_inputs[1].as_value() else { return }; + let old_offset = if old_offset.x.abs() > old_offset.y.abs() { old_offset.x } else { old_offset.y }; + + document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); + document.network_interface.set_input( + &InputConnector::node(*node_id, 1), + NodeInput::value(TaggedValue::ReferencePoint(graphene_std::transform::ReferencePoint::Center), false), + network_path, + ); + document + .network_interface + .set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::F64(old_offset), false), network_path); + document.network_interface.set_input(&InputConnector::node(*node_id, 3), old_inputs[2].clone(), network_path); + document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[3].clone(), network_path); + } + // Upgrade artboard name being passed as hidden value input to "To Artboard" if reference == "Artboard" && upgrade_from_before_returning_nested_click_targets { let label = document.network_interface.display_name(node_id, network_path); diff --git a/editor/src/messages/tool/common_functionality/pivot.rs b/editor/src/messages/tool/common_functionality/pivot.rs index 11856589..3abe180f 100644 --- a/editor/src/messages/tool/common_functionality/pivot.rs +++ b/editor/src/messages/tool/common_functionality/pivot.rs @@ -2,11 +2,11 @@ use super::graph_modification_utils; use crate::consts::PIVOT_DIAMETER; -use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::prelude::*; use glam::{DAffine2, DVec2}; +use graphene_std::transform::ReferencePoint; use std::collections::VecDeque; #[derive(Clone, Debug)] @@ -18,7 +18,7 @@ pub struct Pivot { /// The viewspace pivot position (if applicable) pivot: Option, /// The old pivot position in the GUI, used to reduce refreshes of the document bar - old_pivot_position: PivotPosition, + old_pivot_position: ReferencePoint, } impl Default for Pivot { @@ -27,7 +27,7 @@ impl Default for Pivot { normalized_pivot: DVec2::splat(0.5), transform_from_normalized: Default::default(), pivot: Default::default(), - old_pivot_position: PivotPosition::Center, + old_pivot_position: ReferencePoint::Center, } } } @@ -96,7 +96,7 @@ impl Pivot { should_refresh } - pub fn to_pivot_position(&self) -> PivotPosition { + pub fn to_pivot_position(&self) -> ReferencePoint { self.normalized_pivot.into() } diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 277172df..f99a0b63 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -28,6 +28,7 @@ use glam::DMat2; use graph_craft::document::NodeId; use graphene_core::renderer::Quad; use graphene_std::renderer::Rect; +use graphene_std::transform::ReferencePoint; use graphene_std::vector::misc::BooleanOperation; use std::fmt; @@ -96,7 +97,7 @@ pub enum SelectToolMessage { PointerOutsideViewport(SelectToolPointerKeys), SelectOptions(SelectOptionsUpdate), SetPivot { - position: PivotPosition, + position: ReferencePoint, }, } @@ -129,9 +130,9 @@ impl SelectTool { .widget_holder() } - fn pivot_widget(&self, disabled: bool) -> WidgetHolder { - PivotInput::new(self.tool_data.pivot.to_pivot_position()) - .on_update(|pivot_input: &PivotInput| SelectToolMessage::SetPivot { position: pivot_input.position }.into()) + fn pivot_reference_point_widget(&self, disabled: bool) -> WidgetHolder { + ReferencePointInput::new(self.tool_data.pivot.to_pivot_position()) + .on_update(|pivot_input: &ReferencePointInput| SelectToolMessage::SetPivot { position: pivot_input.value }.into()) .disabled(disabled) .widget_holder() } @@ -204,7 +205,7 @@ impl LayoutHolder for SelectTool { // Pivot widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); - widgets.push(self.pivot_widget(self.tool_data.selected_layers_count == 0)); + widgets.push(self.pivot_reference_point_widget(self.tool_data.selected_layers_count == 0)); // Align let disabled = self.tool_data.selected_layers_count < 2; diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 563a80c6..703e57a3 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -315,7 +315,7 @@ impl NodeRuntime { return; } - let bounds = graphic_element.bounding_box(DAffine2::IDENTITY); + let bounds = graphic_element.bounding_box(DAffine2::IDENTITY, true); // Render the thumbnail from a `GraphicElement` into an SVG string let render_params = RenderParams::new(ViewMode::Normal, bounds, true, false, false); diff --git a/frontend/src/components/widgets/WidgetSpan.svelte b/frontend/src/components/widgets/WidgetSpan.svelte index 0f952125..a6331b59 100644 --- a/frontend/src/components/widgets/WidgetSpan.svelte +++ b/frontend/src/components/widgets/WidgetSpan.svelte @@ -19,8 +19,8 @@ import DropdownInput from "@graphite/components/widgets/inputs/DropdownInput.svelte"; import FontInput from "@graphite/components/widgets/inputs/FontInput.svelte"; import NumberInput from "@graphite/components/widgets/inputs/NumberInput.svelte"; - import PivotInput from "@graphite/components/widgets/inputs/PivotInput.svelte"; import RadioInput from "@graphite/components/widgets/inputs/RadioInput.svelte"; + import ReferencePointInput from "@graphite/components/widgets/inputs/ReferencePointInput.svelte"; import TextAreaInput from "@graphite/components/widgets/inputs/TextAreaInput.svelte"; import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte"; import WorkingColorsInput from "@graphite/components/widgets/inputs/WorkingColorsInput.svelte"; @@ -142,9 +142,9 @@ incrementCallbackDecrease={() => widgetValueCommitAndUpdate(index, "Decrement")} /> {/if} - {@const pivotInput = narrowWidgetProps(component.props, "PivotInput")} - {#if pivotInput} - widgetValueCommitAndUpdate(index, detail)} /> + {@const referencePointInput = narrowWidgetProps(component.props, "ReferencePointInput")} + {#if referencePointInput} + widgetValueCommitAndUpdate(index, detail)} /> {/if} {@const popoverButton = narrowWidgetProps(component.props, "PopoverButton")} {#if popoverButton} diff --git a/frontend/src/components/widgets/inputs/PivotInput.svelte b/frontend/src/components/widgets/inputs/PivotInput.svelte deleted file mode 100644 index 28b297b4..00000000 --- a/frontend/src/components/widgets/inputs/PivotInput.svelte +++ /dev/null @@ -1,115 +0,0 @@ - - -
- - - - - - - - - -
- - diff --git a/frontend/src/components/widgets/inputs/ReferencePointInput.svelte b/frontend/src/components/widgets/inputs/ReferencePointInput.svelte new file mode 100644 index 00000000..14531e48 --- /dev/null +++ b/frontend/src/components/widgets/inputs/ReferencePointInput.svelte @@ -0,0 +1,115 @@ + + +
+ + + + + + + + + +
+ + diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index 1cfc0ac2..2272069c 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -1350,10 +1350,10 @@ export class TextLabel extends WidgetProps { tooltip!: string | undefined; } -export type PivotPosition = "None" | "TopLeft" | "TopCenter" | "TopRight" | "CenterLeft" | "Center" | "CenterRight" | "BottomLeft" | "BottomCenter" | "BottomRight"; +export type ReferencePoint = "None" | "TopLeft" | "TopCenter" | "TopRight" | "CenterLeft" | "Center" | "CenterRight" | "BottomLeft" | "BottomCenter" | "BottomRight"; -export class PivotInput extends WidgetProps { - position!: PivotPosition; +export class ReferencePointInput extends WidgetProps { + value!: ReferencePoint; disabled!: boolean; } @@ -1373,7 +1373,7 @@ const widgetSubTypes = [ { value: NodeCatalog, name: "NodeCatalog" }, { value: NumberInput, name: "NumberInput" }, { value: ParameterExposeButton, name: "ParameterExposeButton" }, - { value: PivotInput, name: "PivotInput" }, + { value: ReferencePointInput, name: "ReferencePointInput" }, { value: PopoverButton, name: "PopoverButton" }, { value: RadioInput, name: "RadioInput" }, { value: Separator, name: "Separator" }, diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index 7bd1c2ff..6b1f4f43 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -275,7 +275,7 @@ pub trait GraphicElementRendered { #[cfg(feature = "vello")] fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, _render_params: &RenderParams); - fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]>; + fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]>; // The upstream click targets for each layer are collected during the render so that they do not have to be calculated for each click detection fn add_upstream_click_targets(&self, _click_targets: &mut Vec) {} @@ -330,7 +330,11 @@ impl GraphicElementRendered for GraphicGroupTable { let alpha_blending = *instance.alpha_blending; let mut layer = false; - if let Some(bounds) = self.instance_ref_iter().filter_map(|element| element.instance.bounding_box(transform)).reduce(Quad::combine_bounds) { + if let Some(bounds) = self + .instance_ref_iter() + .filter_map(|element| element.instance.bounding_box(transform, true)) + .reduce(Quad::combine_bounds) + { let blend_mode = match render_params.view_mode { ViewMode::Outline => peniko::Mix::Normal, _ => alpha_blending.blend_mode.into(), @@ -355,9 +359,9 @@ impl GraphicElementRendered for GraphicGroupTable { } } - fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> { + fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]> { self.instance_ref_iter() - .filter_map(|element| element.instance.bounding_box(transform * *element.transform)) + .filter_map(|element| element.instance.bounding_box(transform * *element.transform, include_stroke)) .reduce(Quad::combine_bounds) } @@ -613,9 +617,13 @@ impl GraphicElementRendered for VectorDataTable { } } - fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> { + fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]> { self.instance_ref_iter() .flat_map(|instance| { + if !include_stroke { + return instance.instance.bounding_box_with_transform(transform * *instance.transform); + } + let stroke_width = instance.instance.style.stroke().map(|s| s.weight()).unwrap_or_default(); let miter_limit = instance.instance.style.stroke().map(|s| s.line_join_miter_limit).unwrap_or(1.); @@ -761,12 +769,15 @@ impl GraphicElementRendered for Artboard { } } - fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> { + fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]> { let artboard_bounds = (transform * Quad::from_box([self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2()])).bounding_box(); if self.clip { Some(artboard_bounds) } else { - [self.graphic_group.bounding_box(transform), Some(artboard_bounds)].into_iter().flatten().reduce(Quad::combine_bounds) + [self.graphic_group.bounding_box(transform, include_stroke), Some(artboard_bounds)] + .into_iter() + .flatten() + .reduce(Quad::combine_bounds) } } @@ -808,8 +819,10 @@ impl GraphicElementRendered for ArtboardGroupTable { } } - fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> { - self.instance_ref_iter().filter_map(|instance| instance.instance.bounding_box(transform)).reduce(Quad::combine_bounds) + fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]> { + self.instance_ref_iter() + .filter_map(|instance| instance.instance.bounding_box(transform, include_stroke)) + .reduce(Quad::combine_bounds) } fn collect_metadata(&self, metadata: &mut RenderMetadata, footprint: Footprint, _element_id: Option) { @@ -882,7 +895,7 @@ impl GraphicElementRendered for ImageFrameTable { } } - fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> { + fn bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> { self.instance_ref_iter() .flat_map(|instance| { let transform = transform * *instance.transform; @@ -924,7 +937,7 @@ impl GraphicElementRendered for RasterFrame { let image_transform = transform * self.transform() * DAffine2::from_scale(1. / DVec2::new(image.width as f64, image.height as f64)); let layer = blend_mode != Default::default(); - let Some(bounds) = self.bounding_box(transform) else { return }; + let Some(bounds) = self.bounding_box(transform, true) else { return }; let blending = vello::peniko::BlendMode::new(blend_mode.blend_mode.into(), vello::peniko::Compose::SrcOver); if layer { @@ -964,7 +977,7 @@ impl GraphicElementRendered for RasterFrame { } } - fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> { + fn bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> { let transform = transform * self.transform(); (transform.matrix2.determinant() != 0.).then(|| (transform * Quad::from_box([DVec2::ZERO, DVec2::ONE])).bounding_box()) } @@ -1002,11 +1015,11 @@ impl GraphicElementRendered for GraphicElement { } } - fn bounding_box(&self, transform: DAffine2) -> Option<[DVec2; 2]> { + fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]> { match self { - GraphicElement::VectorData(vector_data) => vector_data.bounding_box(transform), - GraphicElement::RasterFrame(raster) => raster.bounding_box(transform), - GraphicElement::GraphicGroup(graphic_group) => graphic_group.bounding_box(transform), + GraphicElement::VectorData(vector_data) => vector_data.bounding_box(transform, include_stroke), + GraphicElement::RasterFrame(raster) => raster.bounding_box(transform, include_stroke), + GraphicElement::GraphicGroup(graphic_group) => graphic_group.bounding_box(transform, include_stroke), } } @@ -1078,7 +1091,7 @@ impl GraphicElementRendered for P { render.parent_tag("text", text_attributes, |render| render.leaf_node(format!("{self}"))); } - fn bounding_box(&self, _transform: DAffine2) -> Option<[DVec2; 2]> { + fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> { None } @@ -1106,7 +1119,7 @@ impl GraphicElementRendered for Option { render.parent_tag("text", text_attributes, |render| render.leaf_node(color_info)) } - fn bounding_box(&self, _transform: DAffine2) -> Option<[DVec2; 2]> { + fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> { None } @@ -1130,7 +1143,7 @@ impl GraphicElementRendered for Vec { } } - fn bounding_box(&self, _transform: DAffine2) -> Option<[DVec2; 2]> { + fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> { None } diff --git a/node-graph/gcore/src/raster/bbox.rs b/node-graph/gcore/src/raster/bbox.rs index 9a0d9097..133304bc 100644 --- a/node-graph/gcore/src/raster/bbox.rs +++ b/node-graph/gcore/src/raster/bbox.rs @@ -54,6 +54,12 @@ impl AxisAlignedBbox { } } +impl From<(DVec2, DVec2)> for AxisAlignedBbox { + fn from((start, end): (DVec2, DVec2)) -> Self { + Self { start, end } + } +} + #[cfg_attr(not(target_arch = "spirv"), derive(Debug))] #[derive(Clone)] pub struct Bbox { diff --git a/node-graph/gcore/src/transform.rs b/node-graph/gcore/src/transform.rs index 08b0f952..ec7449ab 100644 --- a/node-graph/gcore/src/transform.rs +++ b/node-graph/gcore/src/transform.rs @@ -242,3 +242,104 @@ async fn freeze_real_time( transform_target.eval(ctx.into_context()).await } + +#[derive(Clone, Copy, Debug, Default, Hash, Eq, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize, specta::Type)] +pub enum ReferencePoint { + #[default] + None, + TopLeft, + TopCenter, + TopRight, + CenterLeft, + Center, + CenterRight, + BottomLeft, + BottomCenter, + BottomRight, +} + +impl ReferencePoint { + pub fn point_in_bounding_box(&self, bounding_box: AxisAlignedBbox) -> Option { + let size = bounding_box.size(); + let offset = match self { + ReferencePoint::None => return None, + ReferencePoint::TopLeft => DVec2::ZERO, + ReferencePoint::TopCenter => DVec2::new(size.x / 2., 0.), + ReferencePoint::TopRight => DVec2::new(size.x, 0.), + ReferencePoint::CenterLeft => DVec2::new(0., size.y / 2.), + ReferencePoint::Center => DVec2::new(size.x / 2., size.y / 2.), + ReferencePoint::CenterRight => DVec2::new(size.x, size.y / 2.), + ReferencePoint::BottomLeft => DVec2::new(0., size.y), + ReferencePoint::BottomCenter => DVec2::new(size.x / 2., size.y), + ReferencePoint::BottomRight => DVec2::new(size.x, size.y), + }; + Some(bounding_box.start + offset) + } +} + +impl From<&str> for ReferencePoint { + fn from(input: &str) -> Self { + match input { + "None" => ReferencePoint::None, + "TopLeft" => ReferencePoint::TopLeft, + "TopCenter" => ReferencePoint::TopCenter, + "TopRight" => ReferencePoint::TopRight, + "CenterLeft" => ReferencePoint::CenterLeft, + "Center" => ReferencePoint::Center, + "CenterRight" => ReferencePoint::CenterRight, + "BottomLeft" => ReferencePoint::BottomLeft, + "BottomCenter" => ReferencePoint::BottomCenter, + "BottomRight" => ReferencePoint::BottomRight, + _ => panic!("Failed parsing unrecognized ReferencePosition enum value '{input}'"), + } + } +} + +impl From for Option { + fn from(input: ReferencePoint) -> Self { + match input { + ReferencePoint::None => None, + ReferencePoint::TopLeft => Some(DVec2::new(0., 0.)), + ReferencePoint::TopCenter => Some(DVec2::new(0.5, 0.)), + ReferencePoint::TopRight => Some(DVec2::new(1., 0.)), + ReferencePoint::CenterLeft => Some(DVec2::new(0., 0.5)), + ReferencePoint::Center => Some(DVec2::new(0.5, 0.5)), + ReferencePoint::CenterRight => Some(DVec2::new(1., 0.5)), + ReferencePoint::BottomLeft => Some(DVec2::new(0., 1.)), + ReferencePoint::BottomCenter => Some(DVec2::new(0.5, 1.)), + ReferencePoint::BottomRight => Some(DVec2::new(1., 1.)), + } + } +} + +impl From for ReferencePoint { + fn from(input: DVec2) -> Self { + const TOLERANCE: f64 = 1e-5_f64; + if input.y.abs() < TOLERANCE { + if input.x.abs() < TOLERANCE { + return ReferencePoint::TopLeft; + } else if (input.x - 0.5).abs() < TOLERANCE { + return ReferencePoint::TopCenter; + } else if (input.x - 1.).abs() < TOLERANCE { + return ReferencePoint::TopRight; + } + } else if (input.y - 0.5).abs() < TOLERANCE { + if input.x.abs() < TOLERANCE { + return ReferencePoint::CenterLeft; + } else if (input.x - 0.5).abs() < TOLERANCE { + return ReferencePoint::Center; + } else if (input.x - 1.).abs() < TOLERANCE { + return ReferencePoint::CenterRight; + } + } else if (input.y - 1.).abs() < TOLERANCE { + if input.x.abs() < TOLERANCE { + return ReferencePoint::BottomLeft; + } else if (input.x - 0.5).abs() < TOLERANCE { + return ReferencePoint::BottomCenter; + } else if (input.x - 1.).abs() < TOLERANCE { + return ReferencePoint::BottomRight; + } + } + ReferencePoint::None + } +} diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index 501c4cc9..83c159e3 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -6,7 +6,7 @@ use crate::instances::{Instance, InstanceMut, Instances}; use crate::raster::image::ImageFrameTable; use crate::registry::types::{Angle, Fraction, IntegerCount, Length, Multiplier, Percentage, PixelLength, SeedValue}; use crate::renderer::GraphicElementRendered; -use crate::transform::{Footprint, Transform, TransformMut}; +use crate::transform::{Footprint, ReferencePoint, Transform, TransformMut}; use crate::vector::PointDomain; use crate::vector::style::{LineCap, LineJoin}; use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroupTable, OwnedContextImpl}; @@ -217,7 +217,9 @@ where let mut result_table = GraphicGroupTable::default(); - let Some(bounding_box) = instance.bounding_box(DAffine2::IDENTITY) else { return result_table }; + let Some(bounding_box) = instance.bounding_box(DAffine2::IDENTITY, false) else { + return result_table; + }; let center = (bounding_box[0] + bounding_box[1]) / 2.; @@ -253,7 +255,9 @@ where let mut result_table = GraphicGroupTable::default(); - let Some(bounding_box) = instance.bounding_box(DAffine2::IDENTITY) else { return result_table }; + let Some(bounding_box) = instance.bounding_box(DAffine2::IDENTITY, false) else { + return result_table; + }; let center = (bounding_box[0] + bounding_box[1]) / 2.; let base_transform = DVec2::new(0., radius) - center; @@ -310,7 +314,7 @@ where let random_scale_difference = random_scale_max - random_scale_min; - let instance_bounding_box = instance.bounding_box(DAffine2::IDENTITY).unwrap_or_default(); + let instance_bounding_box = instance.bounding_box(DAffine2::IDENTITY, false).unwrap_or_default(); let instance_center = -0.5 * (instance_bounding_box[0] + instance_bounding_box[1]); let mut scale_rng = rand::rngs::StdRng::seed_from_u64(random_scale_seed.into()); @@ -364,7 +368,8 @@ where async fn mirror( _: impl Ctx, #[implementations(GraphicGroupTable, VectorDataTable, ImageFrameTable)] instance: Instances, - #[default(0., 0.)] center: DVec2, + #[default(ReferencePoint::Center)] reference_point: ReferencePoint, + offset: f64, #[range((-90., 90.))] angle: Angle, #[default(true)] keep_original: bool, ) -> GraphicGroupTable @@ -373,13 +378,18 @@ where { let mut result_table = GraphicGroupTable::default(); - // The mirror center is based on the bounding box for now - let Some(bounding_box) = instance.bounding_box(DAffine2::IDENTITY) else { return result_table }; - let mirror_center = (bounding_box[0] + bounding_box[1]) / 2. + center; - // Normalize the direction vector let normal = DVec2::from_angle(angle.to_radians()); + // The mirror reference is based on the bounding box (at least for now, until we have proper local layer origins) + let Some(bounding_box) = instance.bounding_box(DAffine2::IDENTITY, false) else { + return result_table; + }; + let mirror_reference_point = reference_point + .point_in_bounding_box((bounding_box[0], bounding_box[1]).into()) + .unwrap_or_else(|| (bounding_box[0] + bounding_box[1]) / 2.) + + normal * offset; + // Create the reflection matrix let reflection = DAffine2::from_mat2_translation( glam::DMat2::from_cols( @@ -389,8 +399,8 @@ where DVec2::ZERO, ); - // Apply reflection around the center point - let transform = DAffine2::from_translation(mirror_center) * reflection * DAffine2::from_translation(-mirror_center); + // Apply reflection around the reference point + let transform = DAffine2::from_translation(mirror_reference_point) * reflection * DAffine2::from_translation(-mirror_reference_point); // Add original instance depending on the keep_original flag if keep_original { diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 8d59d0ba..db19e761 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -7,6 +7,7 @@ pub use glam::{DAffine2, DVec2, IVec2, UVec2}; use graphene_core::raster::brush_cache::BrushCache; use graphene_core::raster::{BlendMode, LuminanceCalculation}; use graphene_core::renderer::RenderMetadata; +use graphene_core::transform::ReferencePoint; use graphene_core::uuid::NodeId; use graphene_core::vector::style::Fill; use graphene_core::{Color, MemoHash, Node, Type}; @@ -233,6 +234,7 @@ tagged_value! { DocumentNode(DocumentNode), Curve(graphene_core::raster::curve::Curve), Footprint(graphene_core::transform::Footprint), + ReferencePoint(graphene_core::transform::ReferencePoint), Palette(Vec), VectorModification(Box), CentroidType(graphene_core::vector::misc::CentroidType), @@ -302,6 +304,32 @@ impl TaggedValue { None } + fn to_reference_point(input: &str) -> Option { + let mut choices = input.split("::"); + let (first, second) = (choices.next()?.trim(), choices.next()?.trim()); + if first == "ReferencePoint" { + return Some(match second { + "None" => ReferencePoint::None, + "TopLeft" => ReferencePoint::TopLeft, + "TopCenter" => ReferencePoint::TopCenter, + "TopRight" => ReferencePoint::TopRight, + "CenterLeft" => ReferencePoint::CenterLeft, + "Center" => ReferencePoint::Center, + "CenterRight" => ReferencePoint::CenterRight, + "BottomLeft" => ReferencePoint::BottomLeft, + "BottomCenter" => ReferencePoint::BottomCenter, + "BottomRight" => ReferencePoint::BottomRight, + _ => { + log::error!("Invalid ReferencePoint default type variant: {}", input); + return None; + } + }); + } + + log::error!("Invalid ReferencePoint default type: {}", input); + None + } + match ty { Type::Generic(_) => None, Type::Concrete(concrete_type) => { @@ -320,6 +348,7 @@ impl TaggedValue { x if x == TypeId::of::() => to_color(string).map(TaggedValue::Color)?, x if x == TypeId::of::>() => to_color(string).map(|color| TaggedValue::OptionalColor(Some(color)))?, x if x == TypeId::of::() => to_color(string).map(|color| TaggedValue::Fill(Fill::solid(color)))?, + x if x == TypeId::of::() => to_reference_point(string).map(TaggedValue::ReferencePoint)?, _ => return None, }; Some(ty) diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index deef98cb..f2ba4864 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -58,6 +58,7 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => ()]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Vec]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => BlendMode]), + async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::transform::ReferencePoint]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::BooleanOperation]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Option]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::Fill]),