mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-16 06:38:03 +08:00
Instance tables refactor part 5: unwrap GraphicGroup as multi-row Instance<GraphicElement> tables and move up transforms (#2363)
* Just group * Partly working but without transforms * Remove Transform/TransformMut from GraphicElement and GraphicGroupTable * Fix layers and flattening * Fix transform group handling on the remaining nodes * Change collect metadata * Add transform on vector data. TODO: Remove duplicate transform * Small code tidying-up * Add concatenate node? * Remove ignore_modifications which is always false * Improve transforms * Mostly fix the nested transform cage angle (except leaf layers and skew) * WIP attempt to integrate skew * Fix nesting bounding box * Avoid setting the transform * Fix stroke transforms * Renderer cleanup * Fix tests for repeated elements not given unique point IDs * Suppress cargo-deny warning * Fix upgrade code for graphic group data * Work around rendering issue in Isometric Fountain --------- Co-authored-by: Adam <adamgerhant@gmail.com> Co-authored-by: hypercube <0hypercube@gmail.com>
This commit is contained in:
@@ -147,6 +147,8 @@ impl Gradient {
|
||||
|
||||
/// Adds the gradient def through mutating the first argument, returning the gradient ID.
|
||||
fn render_defs(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> u64 {
|
||||
// TODO: Figure out how to use `self.transform` as part of the gradient transform, since that field (`Gradient::transform`) is currently never read from, it's only written to.
|
||||
|
||||
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
let transformed_bound_transform = element_transform * DAffine2::from_scale_angle_translation(transformed_bounds[1] - transformed_bounds[0], 0., transformed_bounds[0]);
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ use dyn_any::DynAny;
|
||||
|
||||
use core::borrow::Borrow;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use std::collections::HashMap;
|
||||
|
||||
// TODO: Eventually remove this migration document upgrade code
|
||||
pub fn migrate_vector_data<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<VectorDataTable, D::Error> {
|
||||
@@ -338,6 +339,48 @@ impl VectorData {
|
||||
ManipulatorPointId::Anchor(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn concat(&mut self, other: &Self, transform: DAffine2, node_id: u64) {
|
||||
let point_map = other
|
||||
.point_domain
|
||||
.ids()
|
||||
.iter()
|
||||
.filter(|id| self.point_domain.ids().contains(id))
|
||||
.map(|&old| (old, old.generate_from_hash(node_id)))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let segment_map = other
|
||||
.segment_domain
|
||||
.ids()
|
||||
.iter()
|
||||
.filter(|id| self.segment_domain.ids().contains(id))
|
||||
.map(|&old| (old, old.generate_from_hash(node_id)))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let region_map = other
|
||||
.region_domain
|
||||
.ids()
|
||||
.iter()
|
||||
.filter(|id| self.region_domain.ids().contains(id))
|
||||
.map(|&old| (old, old.generate_from_hash(node_id)))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let id_map = IdMap {
|
||||
point_offset: self.point_domain.ids().len(),
|
||||
point_map,
|
||||
segment_map,
|
||||
region_map,
|
||||
};
|
||||
|
||||
self.point_domain.concat(&other.point_domain, transform, &id_map);
|
||||
self.segment_domain.concat(&other.segment_domain, transform, &id_map);
|
||||
self.region_domain.concat(&other.region_domain, transform, &id_map);
|
||||
|
||||
// TODO: properly deal with fills such as gradients
|
||||
self.style = other.style.clone();
|
||||
|
||||
self.colinear_manipulators.extend(other.colinear_manipulators.iter().copied());
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VectorData {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use crate::transform::Transform;
|
||||
use crate::vector::vector_data::{HandleId, VectorData, VectorDataTable};
|
||||
use crate::vector::ConcatElement;
|
||||
use crate::vector::vector_data::{HandleId, VectorData};
|
||||
|
||||
use dyn_any::DynAny;
|
||||
|
||||
@@ -166,16 +164,16 @@ impl PointDomain {
|
||||
self.id.iter().position(|&check_id| check_id == id)
|
||||
}
|
||||
|
||||
fn concat(&mut self, other: &Self, transform: DAffine2, id_map: &IdMap) {
|
||||
pub fn concat(&mut self, other: &Self, transform: DAffine2, id_map: &IdMap) {
|
||||
self.id.extend(other.id.iter().map(|id| *id_map.point_map.get(id).unwrap_or(id)));
|
||||
self.position.extend(other.position.iter().map(|&pos| transform.transform_point2(pos)));
|
||||
}
|
||||
|
||||
fn map_ids(&mut self, id_map: &IdMap) {
|
||||
pub fn map_ids(&mut self, id_map: &IdMap) {
|
||||
self.id.iter_mut().for_each(|id| *id = *id_map.point_map.get(id).unwrap_or(id));
|
||||
}
|
||||
|
||||
fn transform(&mut self, transform: DAffine2) {
|
||||
pub fn transform(&mut self, transform: DAffine2) {
|
||||
for pos in &mut self.position {
|
||||
*pos = transform.transform_point2(*pos);
|
||||
}
|
||||
@@ -364,7 +362,7 @@ impl SegmentDomain {
|
||||
}
|
||||
}
|
||||
|
||||
fn concat(&mut self, other: &Self, transform: DAffine2, id_map: &IdMap) {
|
||||
pub fn concat(&mut self, other: &Self, transform: DAffine2, id_map: &IdMap) {
|
||||
self.id.extend(other.id.iter().map(|id| *id_map.segment_map.get(id).unwrap_or(id)));
|
||||
self.start_point.extend(other.start_point.iter().map(|&index| id_map.point_offset + index));
|
||||
self.end_point.extend(other.end_point.iter().map(|&index| id_map.point_offset + index));
|
||||
@@ -372,11 +370,11 @@ impl SegmentDomain {
|
||||
self.stroke.extend(&other.stroke);
|
||||
}
|
||||
|
||||
fn map_ids(&mut self, id_map: &IdMap) {
|
||||
pub fn map_ids(&mut self, id_map: &IdMap) {
|
||||
self.id.iter_mut().for_each(|id| *id = *id_map.segment_map.get(id).unwrap_or(id));
|
||||
}
|
||||
|
||||
fn transform(&mut self, transform: DAffine2) {
|
||||
pub fn transform(&mut self, transform: DAffine2) {
|
||||
for handles in &mut self.handles {
|
||||
*handles = handles.apply_transformation(|p| transform.transform_point2(p));
|
||||
}
|
||||
@@ -474,7 +472,7 @@ impl RegionDomain {
|
||||
&self.fill
|
||||
}
|
||||
|
||||
fn concat(&mut self, other: &Self, _transform: DAffine2, id_map: &IdMap) {
|
||||
pub fn concat(&mut self, other: &Self, _transform: DAffine2, id_map: &IdMap) {
|
||||
self.id.extend(other.id.iter().map(|id| *id_map.region_map.get(id).unwrap_or(id)));
|
||||
self.segment_range.extend(
|
||||
other
|
||||
@@ -485,7 +483,7 @@ impl RegionDomain {
|
||||
self.fill.extend(&other.fill);
|
||||
}
|
||||
|
||||
fn map_ids(&mut self, id_map: &IdMap) {
|
||||
pub fn map_ids(&mut self, id_map: &IdMap) {
|
||||
self.id.iter_mut().for_each(|id| *id = *id_map.region_map.get(id).unwrap_or(id));
|
||||
self.segment_range
|
||||
.iter_mut()
|
||||
@@ -764,57 +762,10 @@ impl bezier_rs::Identifier for PointId {
|
||||
}
|
||||
}
|
||||
|
||||
impl ConcatElement for VectorData {
|
||||
fn concat(&mut self, other: &Self, transform: glam::DAffine2, node_id: u64) {
|
||||
let new_ids = other
|
||||
.point_domain
|
||||
.id
|
||||
.iter()
|
||||
.filter(|id| self.point_domain.id.contains(id))
|
||||
.map(|&old| (old, old.generate_from_hash(node_id)));
|
||||
let point_map = new_ids.collect::<HashMap<_, _>>();
|
||||
let new_ids = other
|
||||
.segment_domain
|
||||
.id
|
||||
.iter()
|
||||
.filter(|id| self.segment_domain.id.contains(id))
|
||||
.map(|&old| (old, old.generate_from_hash(node_id)));
|
||||
let segment_map = new_ids.collect::<HashMap<_, _>>();
|
||||
let new_ids = other
|
||||
.region_domain
|
||||
.id
|
||||
.iter()
|
||||
.filter(|id| self.region_domain.id.contains(id))
|
||||
.map(|&old| (old, old.generate_from_hash(node_id)));
|
||||
let region_map = new_ids.collect::<HashMap<_, _>>();
|
||||
let id_map = IdMap {
|
||||
point_offset: self.point_domain.ids().len(),
|
||||
point_map,
|
||||
segment_map,
|
||||
region_map,
|
||||
};
|
||||
self.point_domain.concat(&other.point_domain, transform, &id_map);
|
||||
self.segment_domain.concat(&other.segment_domain, transform, &id_map);
|
||||
self.region_domain.concat(&other.region_domain, transform, &id_map);
|
||||
// TODO: properly deal with fills such as gradients
|
||||
self.style = other.style.clone();
|
||||
self.colinear_manipulators.extend(other.colinear_manipulators.iter().copied());
|
||||
}
|
||||
}
|
||||
|
||||
impl ConcatElement for VectorDataTable {
|
||||
fn concat(&mut self, other: &Self, transform: glam::DAffine2, node_id: u64) {
|
||||
for (instance, other_instance) in self.instances_mut().zip(other.instances()) {
|
||||
*instance.alpha_blending = *other_instance.alpha_blending;
|
||||
instance.instance.concat(other_instance.instance, transform * other_instance.transform(), node_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the conversion of ids used when concatenating vector data with conflicting ids.
|
||||
struct IdMap {
|
||||
point_offset: usize,
|
||||
point_map: HashMap<PointId, PointId>,
|
||||
segment_map: HashMap<SegmentId, SegmentId>,
|
||||
region_map: HashMap<RegionId, RegionId>,
|
||||
pub struct IdMap {
|
||||
pub point_offset: usize,
|
||||
pub point_map: HashMap<PointId, PointId>,
|
||||
pub segment_map: HashMap<SegmentId, SegmentId>,
|
||||
pub region_map: HashMap<RegionId, RegionId>,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::misc::CentroidType;
|
||||
use super::style::{Fill, Gradient, GradientStops, Stroke};
|
||||
use super::{PointId, SegmentDomain, SegmentId, StrokeId, VectorData, VectorDataTable};
|
||||
use crate::instances::InstanceMut;
|
||||
use crate::instances::{InstanceMut, Instances};
|
||||
use crate::registry::types::{Angle, Fraction, IntegerCount, Length, SeedValue};
|
||||
use crate::renderer::GraphicElementRendered;
|
||||
use crate::transform::{Footprint, Transform, TransformMut};
|
||||
@@ -15,55 +15,61 @@ use rand::{Rng, SeedableRng};
|
||||
|
||||
/// Implemented for types that can be converted to an iterator of vector data.
|
||||
/// Used for the fill and stroke node so they can be used on VectorData or GraphicGroup
|
||||
trait VectorIterMut {
|
||||
fn vector_iter_mut(&mut self) -> impl Iterator<Item = (&mut VectorData, DAffine2)>;
|
||||
trait VectorDataTableIterMut {
|
||||
fn vector_iter_mut(&mut self) -> impl Iterator<Item = InstanceMut<VectorData>>;
|
||||
}
|
||||
|
||||
impl VectorIterMut for GraphicGroupTable {
|
||||
fn vector_iter_mut(&mut self) -> impl Iterator<Item = (&mut VectorData, DAffine2)> {
|
||||
let parent_transform = self.transform();
|
||||
let instance = self.one_instance_mut().instance;
|
||||
|
||||
impl VectorDataTableIterMut for GraphicGroupTable {
|
||||
fn vector_iter_mut(&mut self) -> impl Iterator<Item = InstanceMut<VectorData>> {
|
||||
// Grab only the direct children
|
||||
instance.iter_mut().filter_map(|(element, _)| element.as_vector_data_mut()).map(move |vector_data| {
|
||||
let transform = parent_transform * vector_data.transform();
|
||||
|
||||
let vector_data_instance = vector_data.one_instance_mut().instance;
|
||||
|
||||
(vector_data_instance, transform)
|
||||
})
|
||||
self.instances_mut()
|
||||
.filter_map(|element| element.instance.as_vector_data_mut())
|
||||
.flat_map(move |vector_data| vector_data.instances_mut())
|
||||
}
|
||||
}
|
||||
|
||||
impl VectorIterMut for VectorDataTable {
|
||||
fn vector_iter_mut(&mut self) -> impl Iterator<Item = (&mut VectorData, DAffine2)> {
|
||||
self.instances_mut().map(|instance| {
|
||||
let transform = instance.transform();
|
||||
(instance.instance, transform)
|
||||
})
|
||||
impl VectorDataTableIterMut for VectorDataTable {
|
||||
fn vector_iter_mut(&mut self) -> impl Iterator<Item = InstanceMut<VectorData>> {
|
||||
self.instances_mut()
|
||||
}
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector))]
|
||||
async fn assign_colors<T: VectorIterMut>(
|
||||
async fn assign_colors<T>(
|
||||
_: impl Ctx,
|
||||
#[implementations(GraphicGroupTable, VectorDataTable)]
|
||||
#[widget(ParsedWidgetOverride::Hidden)]
|
||||
/// The vector elements, or group of vector elements, to apply the fill and/or stroke style to.
|
||||
mut vector_group: T,
|
||||
#[default(true)] fill: bool,
|
||||
#[default(true)]
|
||||
/// Whether to style the fill.
|
||||
fill: bool,
|
||||
/// Whether to style the stroke.
|
||||
stroke: bool,
|
||||
#[widget(ParsedWidgetOverride::Custom = "assign_colors_gradient")] gradient: GradientStops,
|
||||
#[widget(ParsedWidgetOverride::Custom = "assign_colors_gradient")]
|
||||
/// The range of colors to select from.
|
||||
gradient: GradientStops,
|
||||
/// Whether to reverse the gradient.
|
||||
reverse: bool,
|
||||
#[widget(ParsedWidgetOverride::Custom = "assign_colors_randomize")] randomize: bool,
|
||||
#[widget(ParsedWidgetOverride::Custom = "assign_colors_seed")] seed: SeedValue,
|
||||
#[widget(ParsedWidgetOverride::Custom = "assign_colors_repeat_every")] repeat_every: u32,
|
||||
) -> T {
|
||||
#[widget(ParsedWidgetOverride::Custom = "assign_colors_randomize")]
|
||||
/// Whether to randomize the color selection for each element from throughout the gradient.
|
||||
randomize: bool,
|
||||
#[widget(ParsedWidgetOverride::Custom = "assign_colors_seed")]
|
||||
/// The seed used for randomization.
|
||||
seed: SeedValue,
|
||||
#[widget(ParsedWidgetOverride::Custom = "assign_colors_repeat_every")]
|
||||
/// The number of elements to span across the gradient before repeating. A 0 value will span the entire gradient once.
|
||||
repeat_every: u32,
|
||||
) -> T
|
||||
where
|
||||
T: VectorDataTableIterMut + 'n + Send,
|
||||
{
|
||||
let length = vector_group.vector_iter_mut().count();
|
||||
let gradient = if reverse { gradient.reversed() } else { gradient };
|
||||
|
||||
let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into());
|
||||
|
||||
for (i, (vector_data, _)) in vector_group.vector_iter_mut().enumerate() {
|
||||
for (i, vector_data) in vector_group.vector_iter_mut().enumerate() {
|
||||
let factor = match randomize {
|
||||
true => rng.random::<f64>(),
|
||||
false => match repeat_every {
|
||||
@@ -76,11 +82,11 @@ async fn assign_colors<T: VectorIterMut>(
|
||||
let color = gradient.evalute(factor);
|
||||
|
||||
if fill {
|
||||
vector_data.style.set_fill(Fill::Solid(color));
|
||||
vector_data.instance.style.set_fill(Fill::Solid(color));
|
||||
}
|
||||
if stroke {
|
||||
if let Some(stroke) = vector_data.style.stroke().and_then(|stroke| stroke.with_color(&Some(color))) {
|
||||
vector_data.style.set_stroke(stroke);
|
||||
if let Some(stroke) = vector_data.instance.style.stroke().and_then(|stroke| stroke.with_color(&Some(color))) {
|
||||
vector_data.instance.style.set_stroke(stroke);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,7 +95,7 @@ async fn assign_colors<T: VectorIterMut>(
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))]
|
||||
async fn fill<FillTy: Into<Fill> + 'n + Send, TargetTy: VectorIterMut + 'n + Send>(
|
||||
async fn fill<F: Into<Fill> + 'n + Send, V>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
VectorDataTable,
|
||||
@@ -101,7 +107,8 @@ async fn fill<FillTy: Into<Fill> + 'n + Send, TargetTy: VectorIterMut + 'n + Sen
|
||||
GraphicGroupTable,
|
||||
GraphicGroupTable
|
||||
)]
|
||||
mut vector_data: TargetTy,
|
||||
/// The vector elements, or group of vector elements, to apply the fill to.
|
||||
mut vector_data: V,
|
||||
#[implementations(
|
||||
Fill,
|
||||
Option<Color>,
|
||||
@@ -113,22 +120,33 @@ async fn fill<FillTy: Into<Fill> + 'n + Send, TargetTy: VectorIterMut + 'n + Sen
|
||||
Gradient,
|
||||
)]
|
||||
#[default(Color::BLACK)]
|
||||
fill: FillTy,
|
||||
/// The fill to paint the path with.
|
||||
fill: F,
|
||||
_backup_color: Option<Color>,
|
||||
_backup_gradient: Gradient,
|
||||
) -> TargetTy {
|
||||
) -> V
|
||||
where
|
||||
V: VectorDataTableIterMut + 'n + Send,
|
||||
{
|
||||
let fill: Fill = fill.into();
|
||||
for (target, _transform) in vector_data.vector_iter_mut() {
|
||||
target.style.set_fill(fill.clone());
|
||||
for vector in vector_data.vector_iter_mut() {
|
||||
let mut fill = fill.clone();
|
||||
if let Fill::Gradient(gradient) = &mut fill {
|
||||
gradient.transform *= *vector.transform;
|
||||
}
|
||||
vector.instance.style.set_fill(fill);
|
||||
}
|
||||
|
||||
vector_data
|
||||
}
|
||||
|
||||
/// Applies a stroke style to the vector data contained in the input.
|
||||
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("stroke_properties"))]
|
||||
async fn stroke<ColorTy: Into<Option<Color>> + 'n + Send, TargetTy: VectorIterMut + 'n + Send>(
|
||||
async fn stroke<C: Into<Option<Color>> + 'n + Send, V>(
|
||||
_: impl Ctx,
|
||||
#[implementations(VectorDataTable, VectorDataTable, GraphicGroupTable, GraphicGroupTable)] mut vector_data: TargetTy,
|
||||
#[implementations(VectorDataTable, VectorDataTable, GraphicGroupTable, GraphicGroupTable)]
|
||||
/// The vector elements, or group of vector elements, to apply the stroke to.
|
||||
mut vector_data: Instances<V>,
|
||||
#[implementations(
|
||||
Option<Color>,
|
||||
Color,
|
||||
@@ -136,14 +154,26 @@ async fn stroke<ColorTy: Into<Option<Color>> + 'n + Send, TargetTy: VectorIterMu
|
||||
Color,
|
||||
)]
|
||||
#[default(Color::BLACK)]
|
||||
color: ColorTy,
|
||||
#[default(2.)] weight: f64,
|
||||
/// The stroke color.
|
||||
color: C,
|
||||
#[default(2.)]
|
||||
/// The stroke weight.
|
||||
weight: f64,
|
||||
/// The stroke dash lengths. Each length forms a distance in a pattern where the first length is a dash, the second is a gap, and so on. If the list is an odd length, the pattern repeats with solid-gap roles reversed.
|
||||
dash_lengths: Vec<f64>,
|
||||
/// The offset distance from the starting point of the dash pattern.
|
||||
dash_offset: f64,
|
||||
/// The shape of the stroke at open endpoints.
|
||||
line_cap: crate::vector::style::LineCap,
|
||||
/// The curvature of the bent stroke at sharp corners.
|
||||
line_join: LineJoin,
|
||||
#[default(4.)] miter_limit: f64,
|
||||
) -> TargetTy {
|
||||
#[default(4.)]
|
||||
/// The threshold for when a miter-joined stroke is converted to a bevel-joined stroke when a sharp angle becomes pointier than this ratio.
|
||||
miter_limit: f64,
|
||||
) -> Instances<V>
|
||||
where
|
||||
Instances<V>: VectorDataTableIterMut + 'n + Send,
|
||||
{
|
||||
let stroke = Stroke {
|
||||
color: color.into(),
|
||||
weight,
|
||||
@@ -154,110 +184,111 @@ async fn stroke<ColorTy: Into<Option<Color>> + 'n + Send, TargetTy: VectorIterMu
|
||||
line_join_miter_limit: miter_limit,
|
||||
transform: DAffine2::IDENTITY,
|
||||
};
|
||||
for (target, transform) in vector_data.vector_iter_mut() {
|
||||
target.style.set_stroke(Stroke { transform, ..stroke.clone() });
|
||||
for vector in vector_data.vector_iter_mut() {
|
||||
let mut stroke = stroke.clone();
|
||||
stroke.transform *= *vector.transform;
|
||||
vector.instance.style.set_stroke(stroke);
|
||||
}
|
||||
|
||||
vector_data
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
async fn repeat<I: 'n + GraphicElementRendered + Transform + TransformMut + Send>(
|
||||
async fn repeat<I: 'n + Send>(
|
||||
_: impl Ctx,
|
||||
// TODO: Implement other GraphicElementRendered types.
|
||||
#[implementations(VectorDataTable, GraphicGroupTable)] instance: I,
|
||||
#[implementations(VectorDataTable, GraphicGroupTable)] instance: Instances<I>,
|
||||
#[default(100., 100.)]
|
||||
// TODO: When using a custom Properties panel layout in document_node_definitions.rs and this default is set, the widget weirdly doesn't show up in the Properties panel. Investigation is needed.
|
||||
direction: DVec2,
|
||||
angle: Angle,
|
||||
#[default(4)] instances: IntegerCount,
|
||||
) -> GraphicGroupTable {
|
||||
let first_vector_transform = instance.transform();
|
||||
|
||||
) -> GraphicGroupTable
|
||||
where
|
||||
Instances<I>: GraphicElementRendered,
|
||||
{
|
||||
let angle = angle.to_radians();
|
||||
let instances = instances.max(1);
|
||||
let total = (instances - 1) as f64;
|
||||
|
||||
let mut result_table = GraphicGroupTable::default();
|
||||
let result = result_table.one_instance_mut().instance;
|
||||
|
||||
let Some(bounding_box) = instance.bounding_box(DAffine2::IDENTITY) else { return result_table };
|
||||
|
||||
let center = (bounding_box[0] + bounding_box[1]) / 2.;
|
||||
|
||||
for i in 0..instances {
|
||||
let translation = i as f64 * direction / total;
|
||||
let angle = i as f64 * angle / total;
|
||||
let mut new_instance = result.last().map(|(element, _)| element.clone()).unwrap_or(instance.to_graphic_element());
|
||||
new_instance.new_ids_from_hash(None);
|
||||
for index in 0..instances {
|
||||
let angle = index as f64 * angle / total;
|
||||
let translation = index as f64 * direction / total;
|
||||
let modification = DAffine2::from_translation(center) * DAffine2::from_angle(angle) * DAffine2::from_translation(translation) * DAffine2::from_translation(-center);
|
||||
|
||||
let data_transform = new_instance.transform_mut();
|
||||
*data_transform = modification * first_vector_transform;
|
||||
result.push((new_instance, None));
|
||||
let mut new_graphic_element = instance.to_graphic_element().clone();
|
||||
new_graphic_element.new_ids_from_hash(Some(crate::uuid::NodeId(index as u64)));
|
||||
|
||||
let new_instance = result_table.push(new_graphic_element);
|
||||
*new_instance.transform = modification;
|
||||
}
|
||||
|
||||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
async fn circular_repeat<I: 'n + GraphicElementRendered + Transform + TransformMut + Send>(
|
||||
async fn circular_repeat<I: 'n + Send>(
|
||||
_: impl Ctx,
|
||||
// TODO: Implement other GraphicElementRendered types.
|
||||
#[implementations(VectorDataTable, GraphicGroupTable)] instance: I,
|
||||
#[implementations(VectorDataTable, GraphicGroupTable)] instance: Instances<I>,
|
||||
angle_offset: Angle,
|
||||
#[default(5)] radius: f64,
|
||||
#[default(5)] instances: IntegerCount,
|
||||
) -> GraphicGroupTable {
|
||||
let first_vector_transform = instance.transform();
|
||||
) -> GraphicGroupTable
|
||||
where
|
||||
Instances<I>: GraphicElementRendered,
|
||||
{
|
||||
let instances = instances.max(1);
|
||||
|
||||
let mut result_table = GraphicGroupTable::default();
|
||||
let result = result_table.one_instance_mut().instance;
|
||||
|
||||
let Some(bounding_box) = instance.bounding_box(DAffine2::IDENTITY) else { return result_table };
|
||||
|
||||
let center = (bounding_box[0] + bounding_box[1]) / 2.;
|
||||
let base_transform = DVec2::new(0., radius) - center;
|
||||
|
||||
for i in 0..instances {
|
||||
let angle = (std::f64::consts::TAU / instances as f64) * i as f64 + angle_offset.to_radians();
|
||||
let rotation = DAffine2::from_angle(angle);
|
||||
for index in 0..instances {
|
||||
let rotation = DAffine2::from_angle((std::f64::consts::TAU / instances as f64) * index as f64 + angle_offset.to_radians());
|
||||
let modification = DAffine2::from_translation(center) * rotation * DAffine2::from_translation(base_transform);
|
||||
let mut new_instance = result.last().map(|(element, _)| element.clone()).unwrap_or(instance.to_graphic_element());
|
||||
new_instance.new_ids_from_hash(None);
|
||||
|
||||
let data_transform = new_instance.transform_mut();
|
||||
*data_transform = modification * first_vector_transform;
|
||||
result.push((new_instance, None));
|
||||
let mut new_graphic_element = instance.to_graphic_element().clone();
|
||||
new_graphic_element.new_ids_from_hash(Some(crate::uuid::NodeId(index as u64)));
|
||||
|
||||
let new_instance = result_table.push(new_graphic_element);
|
||||
*new_instance.transform = modification;
|
||||
}
|
||||
|
||||
result_table
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
|
||||
async fn copy_to_points<I: GraphicElementRendered + TransformMut + Send + 'n>(
|
||||
async fn copy_to_points<I: 'n + Send>(
|
||||
_: impl Ctx,
|
||||
points: VectorDataTable,
|
||||
#[expose]
|
||||
#[implementations(VectorDataTable, GraphicGroupTable)]
|
||||
instance: I,
|
||||
instance: Instances<I>,
|
||||
#[default(1)] random_scale_min: f64,
|
||||
#[default(1)] random_scale_max: f64,
|
||||
random_scale_bias: f64,
|
||||
random_scale_seed: SeedValue,
|
||||
random_rotation: Angle,
|
||||
random_rotation_seed: SeedValue,
|
||||
) -> GraphicGroupTable {
|
||||
) -> GraphicGroupTable
|
||||
where
|
||||
Instances<I>: GraphicElementRendered,
|
||||
{
|
||||
let points_transform = points.transform();
|
||||
let points = points.one_instance().instance;
|
||||
|
||||
let instance_transform = instance.transform();
|
||||
let points_list = points.instances().flat_map(|element| element.instance.point_domain.positions());
|
||||
|
||||
let random_scale_difference = random_scale_max - random_scale_min;
|
||||
|
||||
let points_list = points.point_domain.positions();
|
||||
|
||||
let instance_bounding_box = instance.bounding_box(DAffine2::IDENTITY).unwrap_or_default();
|
||||
let instance_center = -0.5 * (instance_bounding_box[0] + instance_bounding_box[1]);
|
||||
|
||||
@@ -268,9 +299,8 @@ async fn copy_to_points<I: GraphicElementRendered + TransformMut + Send + 'n>(
|
||||
let do_rotation = random_rotation.abs() > 1e-6;
|
||||
|
||||
let mut result_table = GraphicGroupTable::default();
|
||||
let result = result_table.one_instance_mut().instance;
|
||||
|
||||
for &point in points_list {
|
||||
for (index, &point) in points_list.into_iter().enumerate() {
|
||||
let center_transform = DAffine2::from_translation(instance_center);
|
||||
|
||||
let translation = points_transform.transform_point2(point);
|
||||
@@ -296,12 +326,11 @@ async fn copy_to_points<I: GraphicElementRendered + TransformMut + Send + 'n>(
|
||||
random_scale_min
|
||||
};
|
||||
|
||||
let mut new_instance = result.last().map(|(element, _)| element.clone()).unwrap_or(instance.to_graphic_element());
|
||||
new_instance.new_ids_from_hash(None);
|
||||
let mut new_graphic_element = instance.to_graphic_element().clone();
|
||||
new_graphic_element.new_ids_from_hash(Some(crate::uuid::NodeId(index as u64)));
|
||||
|
||||
let data_transform = new_instance.transform_mut();
|
||||
*data_transform = DAffine2::from_scale_angle_translation(DVec2::splat(scale), rotation, translation) * center_transform * instance_transform;
|
||||
result.push((new_instance, None));
|
||||
let new_instance = result_table.push(new_graphic_element);
|
||||
*new_instance.transform = DAffine2::from_scale_angle_translation(DVec2::splat(scale), rotation, translation) * center_transform;
|
||||
}
|
||||
|
||||
result_table
|
||||
@@ -406,53 +435,40 @@ async fn flatten_vector_elements(_: impl Ctx, graphic_group_input: GraphicGroupT
|
||||
// A node based solution to support passing through vector data could be a network node with a cache node connected to
|
||||
// a flatten vector elements connected to an if else node, another connection from the cache directly
|
||||
// To the if else node, and another connection from the cache to a matches type node connected to the if else node.
|
||||
fn concat_group(graphic_group_table: &GraphicGroupTable, current_transform: DAffine2, result: &mut InstanceMut<VectorData>) {
|
||||
for (element, reference) in graphic_group_table.one_instance().instance.iter() {
|
||||
match element {
|
||||
GraphicElement::VectorData(vector_data) => {
|
||||
for instance in vector_data.instances() {
|
||||
*result.alpha_blending = *instance.alpha_blending;
|
||||
result
|
||||
.instance
|
||||
.concat(instance.instance, current_transform * instance.transform(), reference.map(|node_id| node_id.0).unwrap_or_default());
|
||||
fn flatten_group(graphic_group_table: &GraphicGroupTable, output: &mut InstanceMut<VectorData>) {
|
||||
for current_element in graphic_group_table.instances() {
|
||||
match current_element.instance {
|
||||
GraphicElement::VectorData(vector_data_table) => {
|
||||
// Loop through every row of the VectorDataTable and concatenate each instance's subpath into the output VectorData instance.
|
||||
for vector_data_instance in vector_data_table.instances() {
|
||||
let other = vector_data_instance.instance;
|
||||
let transform = *current_element.transform * *vector_data_instance.transform;
|
||||
let node_id = current_element.source_node_id.map(|node_id| node_id.0).unwrap_or_default();
|
||||
output.instance.concat(other, transform, node_id);
|
||||
|
||||
// Use the last encountered style as the output style
|
||||
output.instance.style = vector_data_instance.instance.style.clone();
|
||||
}
|
||||
}
|
||||
GraphicElement::GraphicGroup(graphic_group) => {
|
||||
concat_group(graphic_group, current_transform * graphic_group.transform(), result);
|
||||
let mut graphic_group = graphic_group.clone();
|
||||
for instance in graphic_group.instances_mut() {
|
||||
*instance.transform = *current_element.transform * *instance.transform;
|
||||
}
|
||||
|
||||
flatten_group(&graphic_group, output);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut result_table = VectorDataTable::default();
|
||||
let mut result_instance = result_table.one_instance_mut();
|
||||
// TODO: This leads to incorrect stroke widths when flattening groups with different transforms.
|
||||
result_instance.instance.style.set_stroke_transform(DAffine2::IDENTITY);
|
||||
concat_group(&graphic_group_input, DAffine2::IDENTITY, &mut result_instance);
|
||||
let mut output_table = VectorDataTable::default();
|
||||
let Some(mut output) = output_table.instances_mut().next() else { return output_table };
|
||||
|
||||
result_table
|
||||
}
|
||||
flatten_group(&graphic_group_input, &mut output);
|
||||
|
||||
pub trait ConcatElement {
|
||||
fn concat(&mut self, other: &Self, transform: DAffine2, node_id: u64);
|
||||
}
|
||||
|
||||
impl ConcatElement for GraphicGroupTable {
|
||||
fn concat(&mut self, other: &Self, transform: DAffine2, _node_id: u64) {
|
||||
let other_transform = other.transform();
|
||||
|
||||
let self_group = self.one_instance_mut().instance;
|
||||
let other_group = other.one_instance().instance;
|
||||
|
||||
// TODO: Decide if we want to keep this behavior whereby the layers are flattened
|
||||
for (mut element, footprint_mapping) in other_group.iter().cloned() {
|
||||
*element.transform_mut() = transform * element.transform() * other_transform;
|
||||
self_group.push((element, footprint_mapping));
|
||||
}
|
||||
|
||||
*self.one_instance_mut().alpha_blending = *other.one_instance().alpha_blending;
|
||||
}
|
||||
output_table
|
||||
}
|
||||
|
||||
#[node_macro::node(category(""), path(graphene_core::vector))]
|
||||
@@ -1077,7 +1093,7 @@ mod test {
|
||||
let instances = 3;
|
||||
let repeated = super::repeat(Footprint::default(), vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE)), direction, 0., instances).await;
|
||||
let vector_data = super::flatten_vector_elements(Footprint::default(), repeated).await;
|
||||
let vector_data = vector_data.one_instance().instance;
|
||||
let vector_data = vector_data.instances().next().unwrap().instance;
|
||||
assert_eq!(vector_data.region_bezier_paths().count(), 3);
|
||||
for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() {
|
||||
assert!((subpath.manipulator_groups()[0].anchor - direction * index as f64 / (instances - 1) as f64).length() < 1e-5);
|
||||
@@ -1089,29 +1105,32 @@ mod test {
|
||||
let instances = 8;
|
||||
let repeated = super::repeat(Footprint::default(), vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE)), direction, 0., instances).await;
|
||||
let vector_data = super::flatten_vector_elements(Footprint::default(), repeated).await;
|
||||
let vector_data = vector_data.one_instance().instance;
|
||||
let vector_data = vector_data.instances().next().unwrap().instance;
|
||||
assert_eq!(vector_data.region_bezier_paths().count(), 8);
|
||||
for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() {
|
||||
assert!((subpath.manipulator_groups()[0].anchor - direction * index as f64 / (instances - 1) as f64).length() < 1e-5);
|
||||
}
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn circle_repeat() {
|
||||
async fn circular_repeat() {
|
||||
let repeated = super::circular_repeat(Footprint::default(), vector_node(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE)), 45., 4., 8).await;
|
||||
let vector_data = super::flatten_vector_elements(Footprint::default(), repeated).await;
|
||||
let vector_data = vector_data.one_instance().instance;
|
||||
let vector_data = vector_data.instances().next().unwrap().instance;
|
||||
assert_eq!(vector_data.region_bezier_paths().count(), 8);
|
||||
|
||||
for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() {
|
||||
let expected_angle = (index as f64 + 1.) * 45.;
|
||||
|
||||
let center = (subpath.manipulator_groups()[0].anchor + subpath.manipulator_groups()[2].anchor) / 2.;
|
||||
let actual_angle = DVec2::Y.angle_to(center).to_degrees();
|
||||
assert!((actual_angle - expected_angle).abs() % 360. < 1e-5);
|
||||
|
||||
assert!((actual_angle - expected_angle).abs() % 360. < 1e-5, "Expected {expected_angle} found {actual_angle}");
|
||||
}
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn bounding_box() {
|
||||
let bounding_box = super::bounding_box((), vector_node(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE))).await;
|
||||
let bounding_box = bounding_box.one_instance().instance;
|
||||
let bounding_box = bounding_box.instances().next().unwrap().instance;
|
||||
assert_eq!(bounding_box.region_bezier_paths().count(), 1);
|
||||
let subpath = bounding_box.region_bezier_paths().next().unwrap().1;
|
||||
assert_eq!(&subpath.anchors()[..4], &[DVec2::NEG_ONE, DVec2::new(1., -1.), DVec2::ONE, DVec2::new(-1., 1.),]);
|
||||
@@ -1119,13 +1138,13 @@ mod test {
|
||||
// Test a VectorData with non-zero rotation
|
||||
let square = VectorData::from_subpath(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE));
|
||||
let mut square = VectorDataTable::new(square);
|
||||
*square.one_instance_mut().transform_mut() *= DAffine2::from_angle(core::f64::consts::FRAC_PI_4);
|
||||
*square.one_instance_mut().transform *= DAffine2::from_angle(core::f64::consts::FRAC_PI_4);
|
||||
let bounding_box = BoundingBoxNode {
|
||||
vector_data: FutureWrapperNode(square),
|
||||
}
|
||||
.eval(Footprint::default())
|
||||
.await;
|
||||
let bounding_box = bounding_box.one_instance().instance;
|
||||
let bounding_box = bounding_box.instances().next().unwrap().instance;
|
||||
assert_eq!(bounding_box.region_bezier_paths().count(), 1);
|
||||
let subpath = bounding_box.region_bezier_paths().next().unwrap().1;
|
||||
let sqrt2 = core::f64::consts::SQRT_2;
|
||||
@@ -1136,15 +1155,19 @@ mod test {
|
||||
async fn copy_to_points() {
|
||||
let points = Subpath::new_rect(DVec2::NEG_ONE * 10., DVec2::ONE * 10.);
|
||||
let instance = Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE);
|
||||
|
||||
let expected_points = VectorData::from_subpath(points.clone()).point_domain.positions().to_vec();
|
||||
|
||||
let copy_to_points = super::copy_to_points(Footprint::default(), vector_node(points), vector_node(instance), 1., 1., 0., 0, 0., 0).await;
|
||||
let flattened_copy_to_points = super::flatten_vector_elements(Footprint::default(), copy_to_points).await;
|
||||
let flattened_copy_to_points = flattened_copy_to_points.one_instance().instance;
|
||||
let flatten_vector_elements = super::flatten_vector_elements(Footprint::default(), copy_to_points).await;
|
||||
let flattened_copy_to_points = flatten_vector_elements.instances().next().unwrap().instance;
|
||||
|
||||
assert_eq!(flattened_copy_to_points.region_bezier_paths().count(), expected_points.len());
|
||||
|
||||
for (index, (_, subpath)) in flattened_copy_to_points.region_bezier_paths().enumerate() {
|
||||
let offset = expected_points[index];
|
||||
assert_eq!(
|
||||
&subpath.anchors()[..4],
|
||||
&subpath.anchors(),
|
||||
&[offset + DVec2::NEG_ONE, offset + DVec2::new(1., -1.), offset + DVec2::ONE, offset + DVec2::new(-1., 1.),]
|
||||
);
|
||||
}
|
||||
@@ -1153,7 +1176,7 @@ mod test {
|
||||
async fn sample_points() {
|
||||
let path = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.));
|
||||
let sample_points = super::sample_points(Footprint::default(), vector_node(path), 30., 0., 0., false, vec![100.]).await;
|
||||
let sample_points = sample_points.one_instance().instance;
|
||||
let sample_points = sample_points.instances().next().unwrap().instance;
|
||||
assert_eq!(sample_points.point_domain.positions().len(), 4);
|
||||
for (pos, expected) in sample_points.point_domain.positions().iter().zip([DVec2::X * 0., DVec2::X * 30., DVec2::X * 60., DVec2::X * 90.]) {
|
||||
assert!(pos.distance(expected) < 1e-3, "Expected {expected} found {pos}");
|
||||
@@ -1163,7 +1186,7 @@ mod test {
|
||||
async fn adaptive_spacing() {
|
||||
let path = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.));
|
||||
let sample_points = super::sample_points(Footprint::default(), vector_node(path), 18., 45., 10., true, vec![100.]).await;
|
||||
let sample_points = sample_points.one_instance().instance;
|
||||
let sample_points = sample_points.instances().next().unwrap().instance;
|
||||
assert_eq!(sample_points.point_domain.positions().len(), 4);
|
||||
for (pos, expected) in sample_points.point_domain.positions().iter().zip([DVec2::X * 45., DVec2::X * 60., DVec2::X * 75., DVec2::X * 90.]) {
|
||||
assert!(pos.distance(expected) < 1e-3, "Expected {expected} found {pos}");
|
||||
@@ -1178,7 +1201,7 @@ mod test {
|
||||
0,
|
||||
)
|
||||
.await;
|
||||
let sample_points = sample_points.one_instance().instance;
|
||||
let sample_points = sample_points.instances().next().unwrap().instance;
|
||||
assert!(
|
||||
(20..=40).contains(&sample_points.point_domain.positions().len()),
|
||||
"actual len {}",
|
||||
@@ -1197,7 +1220,7 @@ mod test {
|
||||
#[tokio::test]
|
||||
async fn spline() {
|
||||
let spline = super::spline(Footprint::default(), vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.))).await;
|
||||
let spline = spline.one_instance().instance;
|
||||
let spline = spline.instances().next().unwrap().instance;
|
||||
assert_eq!(spline.stroke_bezier_paths().count(), 1);
|
||||
assert_eq!(spline.point_domain.positions(), &[DVec2::ZERO, DVec2::new(100., 0.), DVec2::new(100., 100.), DVec2::new(0., 100.)]);
|
||||
}
|
||||
@@ -1206,7 +1229,7 @@ mod test {
|
||||
let source = Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.);
|
||||
let target = Subpath::new_ellipse(DVec2::NEG_ONE * 100., DVec2::ZERO);
|
||||
let sample_points = super::morph(Footprint::default(), vector_node(source), vector_node(target), 0.5, 0).await;
|
||||
let sample_points = sample_points.one_instance().instance;
|
||||
let sample_points = sample_points.instances().next().unwrap().instance;
|
||||
assert_eq!(
|
||||
&sample_points.point_domain.positions()[..4],
|
||||
vec![DVec2::new(-25., -50.), DVec2::new(50., -25.), DVec2::new(25., 50.), DVec2::new(-50., 25.)]
|
||||
@@ -1229,7 +1252,7 @@ mod test {
|
||||
async fn bevel_rect() {
|
||||
let source = Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.);
|
||||
let beveled = super::bevel(Footprint::default(), vector_node(source), 5.);
|
||||
let beveled = beveled.one_instance().instance;
|
||||
let beveled = beveled.instances().next().unwrap().instance;
|
||||
|
||||
assert_eq!(beveled.point_domain.positions().len(), 8);
|
||||
assert_eq!(beveled.segment_domain.ids().len(), 8);
|
||||
@@ -1252,7 +1275,7 @@ mod test {
|
||||
let curve = Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::new(10., 0.), DVec2::new(10., 100.), DVec2::X * 100.);
|
||||
let source = Subpath::from_beziers(&[Bezier::from_linear_dvec2(DVec2::X * -100., DVec2::ZERO), curve], false);
|
||||
let beveled = super::bevel((), vector_node(source), 5.);
|
||||
let beveled = beveled.one_instance().instance;
|
||||
let beveled = beveled.instances().next().unwrap().instance;
|
||||
|
||||
assert_eq!(beveled.point_domain.positions().len(), 4);
|
||||
assert_eq!(beveled.segment_domain.ids().len(), 3);
|
||||
@@ -1273,11 +1296,10 @@ mod test {
|
||||
let vector_data = VectorData::from_subpath(source);
|
||||
let mut vector_data_table = VectorDataTable::new(vector_data.clone());
|
||||
|
||||
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(10.), 1., DVec2::new(99., 77.));
|
||||
*vector_data_table.one_instance_mut().transform_mut() = transform;
|
||||
*vector_data_table.one_instance_mut().transform = DAffine2::from_scale_angle_translation(DVec2::splat(10.), 1., DVec2::new(99., 77.));
|
||||
|
||||
let beveled = super::bevel((), VectorDataTable::new(vector_data), 5.);
|
||||
let beveled = beveled.one_instance().instance;
|
||||
let beveled = beveled.instances().next().unwrap().instance;
|
||||
|
||||
assert_eq!(beveled.point_domain.positions().len(), 4);
|
||||
assert_eq!(beveled.segment_domain.ids().len(), 3);
|
||||
@@ -1295,7 +1317,7 @@ mod test {
|
||||
async fn bevel_too_high() {
|
||||
let source = Subpath::from_anchors([DVec2::ZERO, DVec2::new(100., 0.), DVec2::new(100., 100.), DVec2::new(0., 100.)], false);
|
||||
let beveled = super::bevel(Footprint::default(), vector_node(source), 999.);
|
||||
let beveled = beveled.one_instance().instance;
|
||||
let beveled = beveled.instances().next().unwrap().instance;
|
||||
|
||||
assert_eq!(beveled.point_domain.positions().len(), 6);
|
||||
assert_eq!(beveled.segment_domain.ids().len(), 5);
|
||||
@@ -1316,7 +1338,7 @@ mod test {
|
||||
let point = Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::ZERO, DVec2::ZERO);
|
||||
let source = Subpath::from_beziers(&[Bezier::from_linear_dvec2(DVec2::X * -100., DVec2::ZERO), point, curve], false);
|
||||
let beveled = super::bevel(Footprint::default(), vector_node(source), 5.);
|
||||
let beveled = beveled.one_instance().instance;
|
||||
let beveled = beveled.instances().next().unwrap().instance;
|
||||
|
||||
assert_eq!(beveled.point_domain.positions().len(), 6);
|
||||
assert_eq!(beveled.segment_domain.ids().len(), 5);
|
||||
|
||||
Reference in New Issue
Block a user