mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-07 15:55:00 +00:00
Add reference point input to the Mirror node
This commit is contained in:
parent
d39308c048
commit
471ef87801
18 changed files with 387 additions and 258 deletions
|
@ -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)
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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(_)
|
||||
|
|
|
@ -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<PivotInput>,
|
||||
pub on_update: WidgetCallback<ReferencePointInput>,
|
||||
|
||||
#[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<PivotPosition> for Option<DVec2> {
|
||||
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<DVec2> 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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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::<VectorDataTable>() => vector_data_widget(default_info).into(),
|
||||
Some(x) if x == TypeId::of::<RasterFrame>() || x == TypeId::of::<ImageFrameTable<Color>>() || x == TypeId::of::<TextureFrameTable>() => raster_widget(default_info).into(),
|
||||
Some(x) if x == TypeId::of::<GraphicGroupTable>() => group_widget(default_info).into(),
|
||||
Some(x) if x == TypeId::of::<ReferencePoint>() => reference_point_widget(default_info, false).into(),
|
||||
Some(x) if x == TypeId::of::<Footprint>() => footprint_widget(default_info, &mut extra_widgets),
|
||||
Some(x) if x == TypeId::of::<BlendMode>() => blend_mode_widget(default_info),
|
||||
Some(x) if x == TypeId::of::<RealTimeMode>() => 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<WidgetHolder> {
|
||||
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>) -> LayoutGroup {
|
||||
let ParameterWidgetsInfo { document_node, node_id, index, .. } = parameter_widgets_info;
|
||||
|
||||
|
|
|
@ -828,6 +828,34 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> 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);
|
||||
|
|
|
@ -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<DVec2>,
|
||||
/// 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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}
|
||||
<PivotInput {...exclude(pivotInput)} on:position={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
{@const referencePointInput = narrowWidgetProps(component.props, "ReferencePointInput")}
|
||||
{#if referencePointInput}
|
||||
<ReferencePointInput {...exclude(referencePointInput)} on:value={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
{/if}
|
||||
{@const popoverButton = narrowWidgetProps(component.props, "PopoverButton")}
|
||||
{#if popoverButton}
|
||||
|
|
|
@ -1,115 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import type { PivotPosition } from "@graphite/messages";
|
||||
|
||||
const dispatch = createEventDispatcher<{ position: PivotPosition }>();
|
||||
|
||||
export let position: string;
|
||||
export let disabled = false;
|
||||
|
||||
function setPosition(newPosition: PivotPosition) {
|
||||
dispatch("position", newPosition);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="pivot-input" class:disabled>
|
||||
<button on:click={() => setPosition("TopLeft")} class="row-1 col-1" class:active={position === "TopLeft"} tabindex="-1" {disabled}><div /></button>
|
||||
<button on:click={() => setPosition("TopCenter")} class="row-1 col-2" class:active={position === "TopCenter"} tabindex="-1" {disabled}><div /></button>
|
||||
<button on:click={() => setPosition("TopRight")} class="row-1 col-3" class:active={position === "TopRight"} tabindex="-1" {disabled}><div /></button>
|
||||
<button on:click={() => setPosition("CenterLeft")} class="row-2 col-1" class:active={position === "CenterLeft"} tabindex="-1" {disabled}><div /></button>
|
||||
<button on:click={() => setPosition("Center")} class="row-2 col-2" class:active={position === "Center"} tabindex="-1" {disabled}><div /></button>
|
||||
<button on:click={() => setPosition("CenterRight")} class="row-2 col-3" class:active={position === "CenterRight"} tabindex="-1" {disabled}><div /></button>
|
||||
<button on:click={() => setPosition("BottomLeft")} class="row-3 col-1" class:active={position === "BottomLeft"} tabindex="-1" {disabled}><div /></button>
|
||||
<button on:click={() => setPosition("BottomCenter")} class="row-3 col-2" class:active={position === "BottomCenter"} tabindex="-1" {disabled}><div /></button>
|
||||
<button on:click={() => setPosition("BottomRight")} class="row-3 col-3" class:active={position === "BottomRight"} tabindex="-1" {disabled}><div /></button>
|
||||
</div>
|
||||
|
||||
<style lang="scss" global>
|
||||
.pivot-input {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
--pivot-border-color: var(--color-5-dullgray);
|
||||
--pivot-fill-active: var(--color-e-nearwhite);
|
||||
|
||||
button {
|
||||
position: absolute;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--color-1-nearblack);
|
||||
border: 1px solid var(--pivot-border-color);
|
||||
|
||||
&.active {
|
||||
border-color: transparent;
|
||||
background: var(--pivot-fill-active);
|
||||
}
|
||||
|
||||
&.col-1::before,
|
||||
&.col-2::before {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
width: 2px;
|
||||
height: 0;
|
||||
border-top: 1px solid var(--pivot-border-color);
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: -3px;
|
||||
}
|
||||
|
||||
&.row-1::after,
|
||||
&.row-2::after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
border-left: 1px solid var(--pivot-border-color);
|
||||
position: absolute;
|
||||
bottom: -3px;
|
||||
right: 1px;
|
||||
}
|
||||
|
||||
&.row-1 {
|
||||
top: 3px;
|
||||
}
|
||||
&.col-1 {
|
||||
left: 3px;
|
||||
}
|
||||
|
||||
&.row-2 {
|
||||
top: 10px;
|
||||
}
|
||||
&.col-2 {
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
&.row-3 {
|
||||
top: 17px;
|
||||
}
|
||||
&.col-3 {
|
||||
left: 17px;
|
||||
}
|
||||
|
||||
// Click targets that extend 1px beyond the borders of each square
|
||||
div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 2px;
|
||||
margin: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.disabled) button:not(.active):hover {
|
||||
border-color: transparent;
|
||||
background: var(--color-6-lowergray);
|
||||
}
|
||||
|
||||
&.disabled button {
|
||||
--pivot-border-color: var(--color-4-dimgray);
|
||||
--pivot-fill-active: var(--color-8-uppergray);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,115 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import type { ReferencePoint } from "@graphite/messages";
|
||||
|
||||
const dispatch = createEventDispatcher<{ value: ReferencePoint }>();
|
||||
|
||||
export let value: string;
|
||||
export let disabled = false;
|
||||
|
||||
function setValue(newValue: ReferencePoint) {
|
||||
dispatch("value", newValue);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="reference-point-input" class:disabled>
|
||||
<button on:click={() => setValue("TopLeft")} class="row-1 col-1" class:active={value === "TopLeft"} tabindex="-1" {disabled}><div /></button>
|
||||
<button on:click={() => setValue("TopCenter")} class="row-1 col-2" class:active={value === "TopCenter"} tabindex="-1" {disabled}><div /></button>
|
||||
<button on:click={() => setValue("TopRight")} class="row-1 col-3" class:active={value === "TopRight"} tabindex="-1" {disabled}><div /></button>
|
||||
<button on:click={() => setValue("CenterLeft")} class="row-2 col-1" class:active={value === "CenterLeft"} tabindex="-1" {disabled}><div /></button>
|
||||
<button on:click={() => setValue("Center")} class="row-2 col-2" class:active={value === "Center"} tabindex="-1" {disabled}><div /></button>
|
||||
<button on:click={() => setValue("CenterRight")} class="row-2 col-3" class:active={value === "CenterRight"} tabindex="-1" {disabled}><div /></button>
|
||||
<button on:click={() => setValue("BottomLeft")} class="row-3 col-1" class:active={value === "BottomLeft"} tabindex="-1" {disabled}><div /></button>
|
||||
<button on:click={() => setValue("BottomCenter")} class="row-3 col-2" class:active={value === "BottomCenter"} tabindex="-1" {disabled}><div /></button>
|
||||
<button on:click={() => setValue("BottomRight")} class="row-3 col-3" class:active={value === "BottomRight"} tabindex="-1" {disabled}><div /></button>
|
||||
</div>
|
||||
|
||||
<style lang="scss" global>
|
||||
.reference-point-input {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
--reference-point-border-color: var(--color-5-dullgray);
|
||||
--reference-point-fill-active: var(--color-e-nearwhite);
|
||||
|
||||
button {
|
||||
position: absolute;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--color-1-nearblack);
|
||||
border: 1px solid var(--reference-point-border-color);
|
||||
|
||||
&.active {
|
||||
border-color: transparent;
|
||||
background: var(--reference-point-fill-active);
|
||||
}
|
||||
|
||||
&.col-1::before,
|
||||
&.col-2::before {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
width: 2px;
|
||||
height: 0;
|
||||
border-top: 1px solid var(--reference-point-border-color);
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: -3px;
|
||||
}
|
||||
|
||||
&.row-1::after,
|
||||
&.row-2::after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
border-left: 1px solid var(--reference-point-border-color);
|
||||
position: absolute;
|
||||
bottom: -3px;
|
||||
right: 1px;
|
||||
}
|
||||
|
||||
&.row-1 {
|
||||
top: 3px;
|
||||
}
|
||||
&.col-1 {
|
||||
left: 3px;
|
||||
}
|
||||
|
||||
&.row-2 {
|
||||
top: 10px;
|
||||
}
|
||||
&.col-2 {
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
&.row-3 {
|
||||
top: 17px;
|
||||
}
|
||||
&.col-3 {
|
||||
left: 17px;
|
||||
}
|
||||
|
||||
// Click targets that extend 1px beyond the borders of each square
|
||||
div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 2px;
|
||||
margin: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.disabled) button:not(.active):hover {
|
||||
border-color: transparent;
|
||||
background: var(--color-6-lowergray);
|
||||
}
|
||||
|
||||
&.disabled button {
|
||||
--reference-point-border-color: var(--color-4-dimgray);
|
||||
--reference-point-fill-active: var(--color-8-uppergray);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -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" },
|
||||
|
|
|
@ -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<ClickTarget>) {}
|
||||
|
@ -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<NodeId>) {
|
||||
|
@ -882,7 +895,7 @@ impl GraphicElementRendered for ImageFrameTable<Color> {
|
|||
}
|
||||
}
|
||||
|
||||
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<P: Primitive> 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<Color> {
|
|||
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<Color> {
|
|||
}
|
||||
}
|
||||
|
||||
fn bounding_box(&self, _transform: DAffine2) -> Option<[DVec2; 2]> {
|
||||
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> {
|
||||
None
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -242,3 +242,104 @@ async fn freeze_real_time<T: 'n + 'static>(
|
|||
|
||||
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<DVec2> {
|
||||
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<ReferencePoint> for Option<DVec2> {
|
||||
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<DVec2> 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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<I: 'n + Send>(
|
||||
_: impl Ctx,
|
||||
#[implementations(GraphicGroupTable, VectorDataTable, ImageFrameTable<Color>)] instance: Instances<I>,
|
||||
#[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 {
|
||||
|
|
|
@ -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<Color>),
|
||||
VectorModification(Box<graphene_core::vector::VectorModification>),
|
||||
CentroidType(graphene_core::vector::misc::CentroidType),
|
||||
|
@ -302,6 +304,32 @@ impl TaggedValue {
|
|||
None
|
||||
}
|
||||
|
||||
fn to_reference_point(input: &str) -> Option<ReferencePoint> {
|
||||
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::<Color>() => to_color(string).map(TaggedValue::Color)?,
|
||||
x if x == TypeId::of::<Option<Color>>() => to_color(string).map(|color| TaggedValue::OptionalColor(Some(color)))?,
|
||||
x if x == TypeId::of::<Fill>() => to_color(string).map(|color| TaggedValue::Fill(Fill::solid(color)))?,
|
||||
x if x == TypeId::of::<ReferencePoint>() => to_reference_point(string).map(TaggedValue::ReferencePoint)?,
|
||||
_ => return None,
|
||||
};
|
||||
Some(ty)
|
||||
|
|
|
@ -58,6 +58,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => ()]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => Vec<f64>]),
|
||||
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<graphene_core::Color>]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::Fill]),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue