mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-08 00:05:00 +00:00
Add the first field-based nodes: 'Instance on Points', 'Instance Position', 'Instance Index', as well as 'Grid' (#2574)
* Basic fields * Add 'Extract XY' and 'Split Vector2' nodes * Add 'Instance Index' node * Fix test again * Improve grid generator to support rectangular as well * Avoid crashing --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
184c009f17
commit
435a6daf25
12 changed files with 515 additions and 26 deletions
|
@ -20,6 +20,7 @@ use graphene_core::text::{Font, TypesettingConfig};
|
|||
use graphene_core::transform::Footprint;
|
||||
use graphene_core::vector::VectorDataTable;
|
||||
use graphene_core::*;
|
||||
use graphene_std::ops::XY;
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
#[cfg(feature = "gpu")]
|
||||
use wgpu_executor::{Bindgroup, CommandBuffer, PipelineLayout, ShaderHandle, ShaderInputFrame, WgpuShaderInput};
|
||||
|
@ -900,6 +901,75 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
|||
description: Cow::Borrowed("TODO"),
|
||||
properties: None,
|
||||
},
|
||||
DocumentNodeDefinition {
|
||||
identifier: "Split Vector2",
|
||||
category: "Math: Vector",
|
||||
node_template: NodeTemplate {
|
||||
document_node: DocumentNode {
|
||||
implementation: DocumentNodeImplementation::Network(NodeNetwork {
|
||||
exports: vec![NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(1), 0)],
|
||||
nodes: [
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(concrete!(ImageFrameTable<Color>), 0), NodeInput::value(TaggedValue::XY(XY::X), false)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::ops::ExtractXyNode")),
|
||||
manual_composition: Some(generic!(T)),
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNode {
|
||||
inputs: vec![NodeInput::network(concrete!(ImageFrameTable<Color>), 0), NodeInput::value(TaggedValue::XY(XY::Y), false)],
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::ops::ExtractXyNode")),
|
||||
manual_composition: Some(generic!(T)),
|
||||
..Default::default()
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(id, node)| (NodeId(id as u64), node))
|
||||
.collect(),
|
||||
|
||||
..Default::default()
|
||||
}),
|
||||
inputs: vec![NodeInput::value(TaggedValue::ImageFrame(ImageFrameTable::one_empty_image()), true)],
|
||||
..Default::default()
|
||||
},
|
||||
persistent_node_metadata: DocumentNodePersistentMetadata {
|
||||
input_properties: vec!["Vector2".into()],
|
||||
output_names: vec!["X".to_string(), "Y".to_string()],
|
||||
has_primary_output: false,
|
||||
network_metadata: Some(NodeNetworkMetadata {
|
||||
persistent_metadata: NodeNetworkPersistentMetadata {
|
||||
node_metadata: [
|
||||
DocumentNodeMetadata {
|
||||
persistent_metadata: DocumentNodePersistentMetadata {
|
||||
display_name: "Extract XY".to_string(),
|
||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
DocumentNodeMetadata {
|
||||
persistent_metadata: DocumentNodePersistentMetadata {
|
||||
display_name: "Extract XY".to_string(),
|
||||
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 2)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(id, node)| (NodeId(id as u64), node))
|
||||
.collect(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
description: Cow::Borrowed("TODO"),
|
||||
properties: None,
|
||||
},
|
||||
DocumentNodeDefinition {
|
||||
identifier: "Brush",
|
||||
category: "Raster",
|
||||
|
@ -2889,6 +2959,7 @@ fn static_node_properties() -> NodeProperties {
|
|||
map.insert("exposure_properties".to_string(), Box::new(node_properties::exposure_properties));
|
||||
map.insert("math_properties".to_string(), Box::new(node_properties::math_properties));
|
||||
map.insert("rectangle_properties".to_string(), Box::new(node_properties::rectangle_properties));
|
||||
map.insert("grid_properties".to_string(), Box::new(node_properties::grid_properties));
|
||||
map.insert(
|
||||
"identity_properties".to_string(),
|
||||
Box::new(|_node_id, _context| node_properties::string_properties("The identity node simply passes its data through.")),
|
||||
|
|
|
@ -21,9 +21,10 @@ use graphene_core::vector::misc::CentroidType;
|
|||
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::vector::VectorDataTable;
|
||||
use graphene_std::vector::misc::BooleanOperation;
|
||||
use graphene_std::vector::misc::{BooleanOperation, GridType};
|
||||
use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops};
|
||||
use graphene_std::{GraphicGroupTable, RasterFrame};
|
||||
|
||||
|
@ -168,6 +169,7 @@ pub(crate) fn property_from_type(
|
|||
Some(x) if x == TypeId::of::<RealTimeMode>() => real_time_mode(document_node, node_id, index, name, true),
|
||||
Some(x) if x == TypeId::of::<RedGreenBlue>() => color_channel(document_node, node_id, index, name, true),
|
||||
Some(x) if x == TypeId::of::<RedGreenBlueAlpha>() => rgba_channel(document_node, node_id, index, name, true),
|
||||
Some(x) if x == TypeId::of::<XY>() => xy_components(document_node, node_id, index, name, true),
|
||||
Some(x) if x == TypeId::of::<NoiseType>() => noise_type(document_node, node_id, index, name, true),
|
||||
Some(x) if x == TypeId::of::<FractalType>() => fractal_type(document_node, node_id, index, name, true, false),
|
||||
Some(x) if x == TypeId::of::<CellularDistanceFunction>() => cellular_distance_function(document_node, node_id, index, name, true, false),
|
||||
|
@ -185,6 +187,7 @@ pub(crate) fn property_from_type(
|
|||
.widget_holder(),
|
||||
]
|
||||
.into(),
|
||||
Some(x) if x == TypeId::of::<GridType>() => grid_type_widget(document_node, node_id, index, name, true),
|
||||
Some(x) if x == TypeId::of::<LineCap>() => line_cap_widget(document_node, node_id, index, name, true),
|
||||
Some(x) if x == TypeId::of::<LineJoin>() => line_join_widget(document_node, node_id, index, name, true),
|
||||
Some(x) if x == TypeId::of::<FillType>() => vec![
|
||||
|
@ -567,6 +570,28 @@ pub fn vec2_widget(
|
|||
.widget_holder(),
|
||||
]);
|
||||
}
|
||||
Some(&TaggedValue::F64(value)) => {
|
||||
widgets.extend_from_slice(&[
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
NumberInput::new(Some(value))
|
||||
.label(x)
|
||||
.unit(unit)
|
||||
.min(min.unwrap_or(-((1_u64 << f64::MANTISSA_DIGITS) as f64)))
|
||||
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
|
||||
.on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(input.value.unwrap(), value)), node_id, index))
|
||||
.on_commit(commit_value)
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
NumberInput::new(Some(value))
|
||||
.label(y)
|
||||
.unit(unit)
|
||||
.min(min.unwrap_or(-((1_u64 << f64::MANTISSA_DIGITS) as f64)))
|
||||
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
|
||||
.on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(value, input.value.unwrap())), node_id, index))
|
||||
.on_commit(commit_value)
|
||||
.widget_holder(),
|
||||
]);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
@ -745,6 +770,15 @@ pub fn number_widget(document_node: &DocumentNode, node_id: NodeId, index: usize
|
|||
.widget_holder(),
|
||||
]);
|
||||
}
|
||||
Some(&TaggedValue::DVec2(dvec2)) => widgets.extend_from_slice(&[
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
number_props
|
||||
// We use an arbitrary `y` instead of an arbitrary `x` here because the "Grid" node's "Spacing" value's height should be used from rectangular mode when transferred to "Y Spacing" in isometric mode
|
||||
.value(Some(dvec2.y))
|
||||
.on_update(update_value(move |x: &NumberInput| TaggedValue::F64(x.value.unwrap()), node_id, index))
|
||||
.on_commit(commit_value)
|
||||
.widget_holder(),
|
||||
]),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
@ -840,6 +874,33 @@ pub fn rgba_channel(document_node: &DocumentNode, node_id: NodeId, index: usize,
|
|||
LayoutGroup::Row { widgets }.with_tooltip("Color Channel")
|
||||
}
|
||||
|
||||
pub fn xy_components(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup {
|
||||
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
|
||||
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 LayoutGroup::Row { widgets: vec![] };
|
||||
};
|
||||
if let Some(&TaggedValue::XY(mode)) = input.as_non_exposed_value() {
|
||||
let calculation_modes = [XY::X, XY::Y];
|
||||
let mut entries = Vec::with_capacity(calculation_modes.len());
|
||||
for method in calculation_modes {
|
||||
entries.push(
|
||||
MenuListEntry::new(format!("{method:?}"))
|
||||
.label(method.to_string())
|
||||
.on_update(update_value(move |_| TaggedValue::XY(method), node_id, index))
|
||||
.on_commit(commit_value),
|
||||
);
|
||||
}
|
||||
let entries = vec![entries];
|
||||
|
||||
widgets.extend_from_slice(&[
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
DropdownInput::new(entries).selected_index(Some(mode as u32)).widget_holder(),
|
||||
]);
|
||||
}
|
||||
LayoutGroup::Row { widgets }.with_tooltip("X or Y Component of Vector2")
|
||||
}
|
||||
|
||||
// TODO: Generalize this instead of using a separate function per dropdown menu enum
|
||||
pub fn noise_type(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup {
|
||||
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
|
||||
|
@ -1064,6 +1125,31 @@ pub fn boolean_operation_radio_buttons(document_node: &DocumentNode, node_id: No
|
|||
LayoutGroup::Row { widgets }
|
||||
}
|
||||
|
||||
pub fn grid_type_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup {
|
||||
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
|
||||
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 LayoutGroup::Row { widgets: vec![] };
|
||||
};
|
||||
if let Some(&TaggedValue::GridType(grid_type)) = input.as_non_exposed_value() {
|
||||
let entries = [("Rectangular", GridType::Rectangular), ("Isometric", GridType::Isometric)]
|
||||
.into_iter()
|
||||
.map(|(name, val)| {
|
||||
RadioEntryData::new(format!("{val:?}"))
|
||||
.label(name)
|
||||
.on_update(update_value(move |_| TaggedValue::GridType(val), node_id, index))
|
||||
.on_commit(commit_value)
|
||||
})
|
||||
.collect();
|
||||
|
||||
widgets.extend_from_slice(&[
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
RadioInput::new(entries).selected_index(Some(grid_type as u32)).widget_holder(),
|
||||
]);
|
||||
}
|
||||
LayoutGroup::Row { widgets }
|
||||
}
|
||||
|
||||
pub fn line_cap_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup {
|
||||
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist);
|
||||
let Some(input) = document_node.inputs.get(index) else {
|
||||
|
@ -1484,6 +1570,52 @@ pub(crate) fn _gpu_map_properties(document_node: &DocumentNode, node_id: NodeId,
|
|||
vec![LayoutGroup::Row { widgets: map }]
|
||||
}
|
||||
|
||||
pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
let grid_type_index = 1;
|
||||
let spacing_index = 2;
|
||||
let angles_index = 3;
|
||||
let rows_index = 4;
|
||||
let columns_index = 5;
|
||||
|
||||
let document_node = match get_document_node(node_id, context) {
|
||||
Ok(document_node) => document_node,
|
||||
Err(err) => {
|
||||
log::error!("Could not get document node in exposure_properties: {err}");
|
||||
return Vec::new();
|
||||
}
|
||||
};
|
||||
let grid_type = grid_type_widget(document_node, node_id, grid_type_index, "Grid Type", true);
|
||||
|
||||
let mut widgets = vec![grid_type];
|
||||
|
||||
let Some(grid_type_input) = document_node.inputs.get(grid_type_index) else {
|
||||
log::warn!("A widget failed to be built because its node's input index is invalid.");
|
||||
return vec![];
|
||||
};
|
||||
if let Some(&TaggedValue::GridType(grid_type)) = grid_type_input.as_non_exposed_value() {
|
||||
match grid_type {
|
||||
GridType::Rectangular => {
|
||||
let spacing = vec2_widget(document_node, node_id, spacing_index, "Spacing", "W", "H", " px", Some(0.), add_blank_assist);
|
||||
widgets.push(spacing);
|
||||
}
|
||||
GridType::Isometric => {
|
||||
let spacing = LayoutGroup::Row {
|
||||
widgets: number_widget(document_node, node_id, spacing_index, "Spacing", NumberInput::default().label("H").min(0.).unit(" px"), true),
|
||||
};
|
||||
let angles = vec2_widget(document_node, node_id, angles_index, "Angles", "", "", "°", None, add_blank_assist);
|
||||
widgets.extend([spacing, angles]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let rows = number_widget(document_node, node_id, rows_index, "Rows", NumberInput::default().min(1.), true);
|
||||
let columns = number_widget(document_node, node_id, columns_index, "Columns", NumberInput::default().min(1.), true);
|
||||
|
||||
widgets.extend([LayoutGroup::Row { widgets: rows }, LayoutGroup::Row { widgets: columns }]);
|
||||
|
||||
widgets
|
||||
}
|
||||
|
||||
pub(crate) fn exposure_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
let document_node = match get_document_node(node_id, context) {
|
||||
Ok(document_node) => document_node,
|
||||
|
|
|
@ -176,7 +176,7 @@ fn grid_overlay_isometric_dot(document: &DocumentMessageHandler, overlay_context
|
|||
|
||||
pub fn grid_overlay(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) {
|
||||
match document.snapping_state.grid.grid_type {
|
||||
GridType::Rectangle { spacing } => {
|
||||
GridType::Rectangular { spacing } => {
|
||||
if document.snapping_state.grid.dot_display {
|
||||
grid_overlay_rectangular_dot(document, overlay_context, spacing)
|
||||
} else {
|
||||
|
@ -238,14 +238,14 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
|
|||
RadioInput::new(vec![
|
||||
RadioEntryData::new("rectangular")
|
||||
.label("Rectangular")
|
||||
.on_update(update_val(grid, |grid, _| grid.grid_type = GridType::RECTANGLE)),
|
||||
.on_update(update_val(grid, |grid, _| grid.grid_type = GridType::RECTANGULAR)),
|
||||
RadioEntryData::new("isometric")
|
||||
.label("Isometric")
|
||||
.on_update(update_val(grid, |grid, _| grid.grid_type = GridType::ISOMETRIC)),
|
||||
])
|
||||
.min_width(200)
|
||||
.selected_index(Some(match grid.grid_type {
|
||||
GridType::Rectangle { .. } => 0,
|
||||
GridType::Rectangular { .. } => 0,
|
||||
GridType::Isometric { .. } => 1,
|
||||
}))
|
||||
.widget_holder(),
|
||||
|
@ -291,7 +291,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
|
|||
});
|
||||
|
||||
match grid.grid_type {
|
||||
GridType::Rectangle { spacing } => widgets.push(LayoutGroup::Row {
|
||||
GridType::Rectangular { spacing } => widgets.push(LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("Spacing").table_align(true).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
|
@ -300,7 +300,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
|
|||
.unit(" px")
|
||||
.min(0.)
|
||||
.min_width(98)
|
||||
.on_update(update_origin(grid, |grid| grid.grid_type.rect_spacing().map(|spacing| &mut spacing.x)))
|
||||
.on_update(update_origin(grid, |grid| grid.grid_type.rectangular_spacing().map(|spacing| &mut spacing.x)))
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
NumberInput::new(Some(spacing.y))
|
||||
|
@ -308,7 +308,7 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
|
|||
.unit(" px")
|
||||
.min(0.)
|
||||
.min_width(98)
|
||||
.on_update(update_origin(grid, |grid| grid.grid_type.rect_spacing().map(|spacing| &mut spacing.y)))
|
||||
.on_update(update_origin(grid, |grid| grid.grid_type.rectangular_spacing().map(|spacing| &mut spacing.y)))
|
||||
.widget_holder(),
|
||||
],
|
||||
}),
|
||||
|
|
|
@ -160,26 +160,33 @@ impl Default for PathSnapping {
|
|||
|
||||
#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||
pub enum GridType {
|
||||
Rectangle { spacing: DVec2 },
|
||||
Isometric { y_axis_spacing: f64, angle_a: f64, angle_b: f64 },
|
||||
#[serde(alias = "Rectangle")]
|
||||
Rectangular {
|
||||
spacing: DVec2,
|
||||
},
|
||||
Isometric {
|
||||
y_axis_spacing: f64,
|
||||
angle_a: f64,
|
||||
angle_b: f64,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for GridType {
|
||||
fn default() -> Self {
|
||||
Self::RECTANGLE
|
||||
Self::RECTANGULAR
|
||||
}
|
||||
}
|
||||
|
||||
impl GridType {
|
||||
pub const RECTANGLE: Self = GridType::Rectangle { spacing: DVec2::ONE };
|
||||
pub const RECTANGULAR: Self = GridType::Rectangular { spacing: DVec2::ONE };
|
||||
pub const ISOMETRIC: Self = GridType::Isometric {
|
||||
y_axis_spacing: 1.,
|
||||
angle_a: 30.,
|
||||
angle_b: 30.,
|
||||
};
|
||||
pub fn rect_spacing(&mut self) -> Option<&mut DVec2> {
|
||||
pub fn rectangular_spacing(&mut self) -> Option<&mut DVec2> {
|
||||
match self {
|
||||
Self::Rectangle { spacing } => Some(spacing),
|
||||
Self::Rectangular { spacing } => Some(spacing),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ impl GridSnapper {
|
|||
}
|
||||
lines
|
||||
}
|
||||
|
||||
// Isometric grid has 6 lines around a point, 2 y axis, 2 on the angle a, and 2 on the angle b.
|
||||
fn get_snap_lines_isometric(&self, document_point: DVec2, snap_data: &mut SnapData, y_axis_spacing: f64, angle_a: f64, angle_b: f64) -> Vec<Line> {
|
||||
let document = snap_data.document;
|
||||
|
@ -86,9 +87,10 @@ impl GridSnapper {
|
|||
|
||||
lines
|
||||
}
|
||||
|
||||
fn get_snap_lines(&self, document_point: DVec2, snap_data: &mut SnapData) -> Vec<Line> {
|
||||
match snap_data.document.snapping_state.grid.grid_type {
|
||||
GridType::Rectangle { spacing } => self.get_snap_lines_rectangular(document_point, snap_data, spacing),
|
||||
GridType::Rectangular { spacing } => self.get_snap_lines_rectangular(document_point, snap_data, spacing),
|
||||
GridType::Isometric { y_axis_spacing, angle_a, angle_b } => self.get_snap_lines_isometric(document_point, snap_data, y_axis_spacing, angle_a, angle_b),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -328,6 +328,15 @@ impl OwnedContextImpl {
|
|||
self.animation_time = Some(animation_time);
|
||||
self
|
||||
}
|
||||
pub fn with_vararg(mut self, value: Box<dyn Any + Send + Sync>) -> Self {
|
||||
assert!(self.varargs.is_none_or(|value| value.is_empty()));
|
||||
self.varargs = Some(Arc::new([value]));
|
||||
self
|
||||
}
|
||||
pub fn with_index(mut self, index: usize) -> Self {
|
||||
self.index = Some(index);
|
||||
self
|
||||
}
|
||||
pub fn into_context(self) -> Option<Arc<Self>> {
|
||||
Some(Arc::new(self))
|
||||
}
|
||||
|
|
|
@ -6,7 +6,8 @@ use crate::vector::style::GradientStops;
|
|||
use crate::{Color, Node};
|
||||
use core::marker::PhantomData;
|
||||
use core::ops::{Add, Div, Mul, Rem, Sub};
|
||||
use glam::DVec2;
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DVec2, IVec2, UVec2};
|
||||
use math_parser::ast;
|
||||
use math_parser::context::{EvalContext, NothingMap, ValueProvider};
|
||||
use math_parser::value::{Number, Value};
|
||||
|
@ -284,6 +285,12 @@ fn to_u64<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)]
|
|||
value.to_u64().unwrap()
|
||||
}
|
||||
|
||||
/// Convert an integer to a decimal number of the type f64, which may be the required type for certain node inputs. This will be removed in the future when automatic type conversion is implemented.
|
||||
#[node_macro::node(name("To f64"), category("Math: Numeric"))]
|
||||
fn to_f64<U: num_traits::int::PrimInt>(_: impl Ctx, #[implementations(u32, u64)] value: U) -> f64 {
|
||||
value.to_f64().unwrap()
|
||||
}
|
||||
|
||||
/// The rounding function (round) maps an input value to its nearest whole number. Halfway values are rounded away from zero.
|
||||
#[node_macro::node(category("Math: Numeric"))]
|
||||
fn round<U: num_traits::float::Float>(_: impl Ctx, #[implementations(f64, f32)] value: U) -> U {
|
||||
|
@ -343,10 +350,7 @@ fn clamp<T: core::cmp::PartialOrd>(
|
|||
fn equals<U: core::cmp::PartialEq<T>, T>(
|
||||
_: impl Ctx,
|
||||
#[implementations(f64, &f64, f32, &f32, u32, &u32, DVec2, &DVec2, &str)] value: T,
|
||||
#[implementations(f64, &f64, f32, &f32, u32, &u32, DVec2, &DVec2, &str)]
|
||||
#[min(100.)]
|
||||
#[max(200.)]
|
||||
other_value: U,
|
||||
#[implementations(f64, &f64, f32, &f32, u32, &u32, DVec2, &DVec2, &str)] other_value: U,
|
||||
) -> bool {
|
||||
other_value == value
|
||||
}
|
||||
|
@ -356,10 +360,7 @@ fn equals<U: core::cmp::PartialEq<T>, T>(
|
|||
fn not_equals<U: core::cmp::PartialEq<T>, T>(
|
||||
_: impl Ctx,
|
||||
#[implementations(f64, &f64, f32, &f32, u32, &u32, DVec2, &DVec2, &str)] value: T,
|
||||
#[implementations(f64, &f64, f32, &f32, u32, &u32, DVec2, &DVec2, &str)]
|
||||
#[min(100.)]
|
||||
#[max(200.)]
|
||||
other_value: U,
|
||||
#[implementations(f64, &f64, f32, &f32, u32, &u32, DVec2, &DVec2, &str)] other_value: U,
|
||||
) -> bool {
|
||||
other_value != value
|
||||
}
|
||||
|
@ -491,6 +492,32 @@ fn dot_product(_: impl Ctx, vector_a: DVec2, vector_b: DVec2) -> f64 {
|
|||
vector_a.dot(vector_b)
|
||||
}
|
||||
|
||||
/// Obtain the X or Y component of a vector2.
|
||||
#[node_macro::node(name("Extract XY"), category("Math: Vector"))]
|
||||
fn extract_xy<T: Into<DVec2>>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2)] vector: T, axis: XY) -> f64 {
|
||||
match axis {
|
||||
XY::X => vector.into().x,
|
||||
XY::Y => vector.into().y,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "std", derive(specta::Type))]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, DynAny)]
|
||||
pub enum XY {
|
||||
#[default]
|
||||
X,
|
||||
Y,
|
||||
}
|
||||
impl core::fmt::Display for XY {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
XY::X => write!(f, "X"),
|
||||
XY::Y => write!(f, "Y"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Rename to "Passthrough"
|
||||
/// Passes-through the input value without changing it. This is useful for rerouting wires for organization purposes.
|
||||
#[node_macro::node(skip_impl)]
|
||||
|
|
98
node-graph/gcore/src/vector/algorithms/instance.rs
Normal file
98
node-graph/gcore/src/vector/algorithms/instance.rs
Normal file
|
@ -0,0 +1,98 @@
|
|||
use crate::instances::Instance;
|
||||
use crate::vector::{VectorData, VectorDataTable};
|
||||
use crate::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractIndex, ExtractVarArgs, OwnedContextImpl};
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
||||
#[node_macro::node(name("Instance on Points"), category("Vector: Shape"), path(graphene_core::vector))]
|
||||
async fn instance_on_points(
|
||||
ctx: impl ExtractAll + CloneVarArgs + Ctx,
|
||||
points: VectorDataTable,
|
||||
#[implementations(Context -> VectorDataTable)] instance_node: impl Node<'n, Context<'static>, Output = VectorDataTable>,
|
||||
) -> VectorDataTable {
|
||||
let mut result = VectorDataTable::empty();
|
||||
|
||||
for Instance { instance: points, transform, .. } in points.instances() {
|
||||
for (index, &point) in points.point_domain.positions().iter().enumerate() {
|
||||
let transformed_point = transform.transform_point2(point);
|
||||
|
||||
let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index).with_vararg(Box::new(transformed_point));
|
||||
let instanced = instance_node.eval(new_ctx.into_context()).await;
|
||||
|
||||
for instanced in instanced.instances() {
|
||||
let instanced = result.push_instance(instanced);
|
||||
*instanced.transform *= DAffine2::from_translation(transformed_point);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove once we support empty tables, currently this is here to avoid crashing
|
||||
if result.is_empty() {
|
||||
return VectorDataTable::new(VectorData::empty());
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Attributes"), path(graphene_core::vector))]
|
||||
async fn instance_position(ctx: impl Ctx + ExtractVarArgs) -> DVec2 {
|
||||
match ctx.vararg(0).map(|dynamic| dynamic.downcast_ref::<DVec2>()) {
|
||||
Ok(Some(position)) => return *position,
|
||||
Ok(_) => warn!("Extracted value of incorrect type"),
|
||||
Err(e) => warn!("Cannot extract position vararg: {e:?}"),
|
||||
}
|
||||
Default::default()
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Attributes"), path(graphene_core::vector))]
|
||||
async fn instance_index(ctx: impl Ctx + ExtractIndex) -> f64 {
|
||||
match ctx.try_index() {
|
||||
Some(index) => return index as f64,
|
||||
None => warn!("Extracted value of incorrect type"),
|
||||
}
|
||||
0.
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::Node;
|
||||
use crate::ops::ExtractXyNode;
|
||||
use crate::vector::VectorData;
|
||||
use bezier_rs::Subpath;
|
||||
use glam::DVec2;
|
||||
use std::pin::Pin;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FutureWrapperNode<T: Clone>(T);
|
||||
|
||||
impl<'i, I: Ctx, T: 'i + Clone + Send> Node<'i, I> for FutureWrapperNode<T> {
|
||||
type Output = Pin<Box<dyn core::future::Future<Output = T> + 'i + Send>>;
|
||||
fn eval(&'i self, _input: I) -> Self::Output {
|
||||
let value = self.0.clone();
|
||||
Box::pin(async move { value })
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn instance_on_points_test() {
|
||||
let owned = OwnedContextImpl::default().into_context();
|
||||
let rect = crate::vector::generator_nodes::RectangleNode::new(
|
||||
FutureWrapperNode(()),
|
||||
ExtractXyNode::new(InstancePositionNode {}, FutureWrapperNode(crate::ops::XY::Y)),
|
||||
FutureWrapperNode(2_f64),
|
||||
FutureWrapperNode(false),
|
||||
FutureWrapperNode(0_f64),
|
||||
FutureWrapperNode(false),
|
||||
);
|
||||
|
||||
let positions = [DVec2::new(40., 20.), DVec2::ONE, DVec2::new(-42., 9.), DVec2::new(10., 345.)];
|
||||
let points = VectorDataTable::new(VectorData::from_subpath(Subpath::from_anchors_linear(positions, false)));
|
||||
let repeated = super::instance_on_points(owned, points, &rect).await;
|
||||
assert_eq!(repeated.len(), positions.len());
|
||||
for (position, instanced) in positions.into_iter().zip(repeated.instances()) {
|
||||
let bounds = instanced.instance.bounding_box_with_transform(*instanced.transform).unwrap();
|
||||
assert!(position.abs_diff_eq((bounds[0] + bounds[1]) / 2., 1e-10));
|
||||
assert_eq!((bounds[1] - bounds[0]).x, position.y);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
mod instance;
|
||||
mod merge_by_distance;
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
use super::misc::{AsU64, GridType};
|
||||
use super::{PointId, SegmentId, StrokeId};
|
||||
use crate::Ctx;
|
||||
use crate::vector::{HandleId, VectorData, VectorDataTable};
|
||||
use bezier_rs::Subpath;
|
||||
use glam::DVec2;
|
||||
|
||||
use super::misc::AsU64;
|
||||
|
||||
trait CornerRadius {
|
||||
fn generate(self, size: DVec2, clamped: bool) -> VectorDataTable;
|
||||
}
|
||||
|
@ -36,7 +36,14 @@ impl CornerRadius for [f64; 4] {
|
|||
}
|
||||
|
||||
#[node_macro::node(category("Vector: Shape"))]
|
||||
fn circle(_: impl Ctx, _primary: (), #[default(50.)] radius: f64) -> VectorDataTable {
|
||||
fn circle(
|
||||
_: impl Ctx,
|
||||
_primary: (),
|
||||
#[default(50.)]
|
||||
#[min(0.)]
|
||||
radius: f64,
|
||||
) -> VectorDataTable {
|
||||
let radius = radius.max(0.);
|
||||
VectorDataTable::new(VectorData::from_subpath(Subpath::new_ellipse(DVec2::splat(-radius), DVec2::splat(radius))))
|
||||
}
|
||||
|
||||
|
@ -108,3 +115,129 @@ fn star<T: AsU64>(
|
|||
fn line(_: impl Ctx, _primary: (), #[default((0., -50.))] start: DVec2, #[default((0., 50.))] end: DVec2) -> VectorDataTable {
|
||||
VectorDataTable::new(VectorData::from_subpath(Subpath::new_line(start, end)))
|
||||
}
|
||||
|
||||
trait GridSpacing {
|
||||
fn as_dvec2(&self) -> DVec2;
|
||||
}
|
||||
impl GridSpacing for f64 {
|
||||
fn as_dvec2(&self) -> DVec2 {
|
||||
DVec2::splat(*self)
|
||||
}
|
||||
}
|
||||
impl GridSpacing for DVec2 {
|
||||
fn as_dvec2(&self) -> DVec2 {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector: Shape"), properties("grid_properties"))]
|
||||
fn grid<T: GridSpacing>(
|
||||
_: impl Ctx,
|
||||
_primary: (),
|
||||
grid_type: GridType,
|
||||
#[min(0.)]
|
||||
#[default(10)]
|
||||
#[implementations(f64, DVec2)]
|
||||
spacing: T,
|
||||
#[default(30., 30.)] angles: DVec2,
|
||||
#[default(10)] rows: u32,
|
||||
#[default(10)] columns: u32,
|
||||
) -> VectorDataTable {
|
||||
let (x_spacing, y_spacing) = spacing.as_dvec2().into();
|
||||
let (angle_a, angle_b) = angles.into();
|
||||
|
||||
let mut vector_data = VectorData::empty();
|
||||
let mut segment_id = SegmentId::ZERO;
|
||||
let mut point_id = PointId::ZERO;
|
||||
|
||||
match grid_type {
|
||||
GridType::Rectangular => {
|
||||
// Create rectangular grid points and connect them with line segments
|
||||
for y in 0..rows {
|
||||
for x in 0..columns {
|
||||
// Add current point to the grid
|
||||
let current_index = vector_data.point_domain.ids().len();
|
||||
vector_data.point_domain.push(point_id.next_id(), DVec2::new(x_spacing * x as f64, y_spacing * y as f64));
|
||||
|
||||
// Helper function to connect points with line segments
|
||||
let mut push_segment = |to_index: Option<usize>| {
|
||||
if let Some(other_index) = to_index {
|
||||
vector_data
|
||||
.segment_domain
|
||||
.push(segment_id.next_id(), other_index, current_index, bezier_rs::BezierHandles::Linear, StrokeId::ZERO);
|
||||
}
|
||||
};
|
||||
|
||||
// Connect to the point to the left (horizontal connection)
|
||||
push_segment((x > 0).then(|| current_index - 1));
|
||||
|
||||
// Connect to the point above (vertical connection)
|
||||
push_segment(current_index.checked_sub(columns as usize));
|
||||
}
|
||||
}
|
||||
}
|
||||
GridType::Isometric => {
|
||||
// Calculate isometric grid spacing based on angles
|
||||
let tan_a = angle_a.to_radians().tan();
|
||||
let tan_b = angle_b.to_radians().tan();
|
||||
let spacing = DVec2::new(y_spacing / (tan_a + tan_b), y_spacing);
|
||||
|
||||
// Create isometric grid points and connect them with line segments
|
||||
for y in 0..rows {
|
||||
for x in 0..columns {
|
||||
// Add current point to the grid with offset for odd columns
|
||||
let current_index = vector_data.point_domain.ids().len();
|
||||
vector_data
|
||||
.point_domain
|
||||
.push(point_id.next_id(), DVec2::new(spacing.x * x as f64, spacing.y * (y as f64 - (x % 2) as f64 * 0.5)));
|
||||
|
||||
// Helper function to connect points with line segments
|
||||
let mut push_segment = |to_index: Option<usize>| {
|
||||
if let Some(other_index) = to_index {
|
||||
vector_data
|
||||
.segment_domain
|
||||
.push(segment_id.next_id(), other_index, current_index, bezier_rs::BezierHandles::Linear, StrokeId::ZERO);
|
||||
}
|
||||
};
|
||||
|
||||
// Connect to the point to the left
|
||||
push_segment((x > 0).then(|| current_index - 1));
|
||||
|
||||
// Connect to the point directly above
|
||||
push_segment(current_index.checked_sub(columns as usize));
|
||||
|
||||
// Additional diagonal connections for odd columns (creates hexagonal pattern)
|
||||
if x % 2 == 1 {
|
||||
// Connect to the point diagonally up-right (if not at right edge)
|
||||
push_segment(current_index.checked_sub(columns as usize - 1).filter(|_| x + 1 < columns));
|
||||
|
||||
// Connect to the point diagonally up-left
|
||||
push_segment(current_index.checked_sub(columns as usize + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VectorDataTable::new(vector_data)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn isometric_grid_test() {
|
||||
// Doesn't crash with weird angles
|
||||
grid((), (), GridType::Isometric, 0., (0., 0.).into(), 5, 5);
|
||||
grid((), (), GridType::Isometric, 90., (90., 90.).into(), 5, 5);
|
||||
|
||||
// Works properly
|
||||
let grid = grid((), (), GridType::Isometric, 10., (30., 30.).into(), 5, 5);
|
||||
assert_eq!(grid.one_instance().instance.point_domain.ids().len(), 5 * 5);
|
||||
assert_eq!(grid.one_instance().instance.segment_bezier_iter().count(), 4 * 5 + 4 * 9);
|
||||
for (_, bezier, _, _) in grid.one_instance().instance.segment_bezier_iter() {
|
||||
assert_eq!(bezier.handles, bezier_rs::BezierHandles::Linear);
|
||||
assert!(
|
||||
((bezier.start - bezier.end).length() - 10.).abs() < 1e-5,
|
||||
"Length of {} should be 10",
|
||||
(bezier.start - bezier.end).length()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,3 +85,10 @@ impl AsI64 for f64 {
|
|||
*self as i64
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)]
|
||||
pub enum GridType {
|
||||
#[default]
|
||||
Rectangular,
|
||||
Isometric,
|
||||
}
|
||||
|
|
|
@ -202,6 +202,7 @@ tagged_value! {
|
|||
VecU64(Vec<u64>),
|
||||
NodePath(Vec<NodeId>),
|
||||
VecDVec2(Vec<DVec2>),
|
||||
XY(graphene_core::ops::XY),
|
||||
RedGreenBlue(graphene_core::raster::RedGreenBlue),
|
||||
RealTimeMode(graphene_core::animation::RealTimeMode),
|
||||
RedGreenBlueAlpha(graphene_core::raster::RedGreenBlueAlpha),
|
||||
|
@ -212,6 +213,7 @@ tagged_value! {
|
|||
DomainWarpType(graphene_core::raster::DomainWarpType),
|
||||
RelativeAbsolute(graphene_core::raster::RelativeAbsolute),
|
||||
SelectiveColorChoice(graphene_core::raster::SelectiveColorChoice),
|
||||
GridType(graphene_core::vector::misc::GridType),
|
||||
LineCap(graphene_core::vector::style::LineCap),
|
||||
LineJoin(graphene_core::vector::style::LineJoin),
|
||||
FillType(graphene_core::vector::style::FillType),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue