diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index fff2dcf990..82e8f2a580 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -151,6 +151,18 @@ fn node_registry() -> HashMap> { .into_iter() .map(|entry| (graphene_std::vector::assign_colors::IDENTIFIER.clone(), entry)), ); + // The mirror's plain vector rows, served under its identifier. + node_types.extend( + graphene_std::graphic::mirror_vector_entries() + .into_iter() + .map(|entry| (graphene_std::graphic::mirror::IDENTIFIER.clone(), entry)), + ); + // The morph's plain vector rows, served under its identifier. + node_types.extend( + graphene_std::vector::morph_vector_entries() + .into_iter() + .map(|entry| (graphene_std::vector::morph::IDENTIFIER.clone(), entry)), + ); // Element-wise coercion into `Graphic` for single-typed leveled inputs, // served by the to_graphic rows. node_types.extend( @@ -167,6 +179,12 @@ fn node_registry() -> HashMap> { .zip(["List", "List", "List>", "List>", "List", "List", "List"]) .map(|(entry, target)| (ProtoNodeIdentifier::with_owned_string(format!("graphene_core::ops::ConvertNode<{target}>")), entry)), ); + // The same bridge into the attribute family's type-erased list. + node_types.extend( + graphene_std::graphic::level_to_list_dyn_entries() + .into_iter() + .map(|entry| (ProtoNodeIdentifier::new("graphene_core::ops::ConvertNode"), entry)), + ); // ============= // CONVERT NODES // ============= diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index 916acde0bd..38bb46bc80 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -129,25 +129,9 @@ fn map( Err(GraphError::past_end().into()) } -#[node_macro::node(category("General"))] -fn mirror( - _: impl Ctx, - #[implementations( - List, - List, - List, - List>, - List, - List, - )] - content: List, - #[default(ReferencePoint::Center)] relative_to_bounds: ReferencePoint, - #[unit(" px")] offset: f64, - #[range] - #[soft(-90..90)] - angle: Angle, - #[default(true)] keep_original: bool, -) -> List +/// The reflection transform the mirror applies, or nothing when the content +/// has no rectangular bounds (the legacy passthrough case). +fn mirror_reflection(legacy: &List, relative_to_bounds: ReferencePoint, offset: f64, angle: f64) -> Option where List: BoundingBox, { @@ -155,8 +139,8 @@ where let normal = DVec2::from_angle(angle.to_radians()); // The mirror reference may be based on the bounding box if an explicit reference point is chosen - let RenderBoundingBox::Rectangle(bounding_box) = content.bounding_box(DAffine2::IDENTITY, false) else { - return content; + let RenderBoundingBox::Rectangle(bounding_box) = legacy.bounding_box(DAffine2::IDENTITY, false) else { + return None; }; let reference_point_location = relative_to_bounds.point_in_bounding_box((bounding_box[0], bounding_box[1]).into()); @@ -172,31 +156,212 @@ where ); // Apply reflection around the reference point - let reflected_transform = if let Some(mirror_reference_point) = mirror_reference_point { + Some(if let Some(mirror_reference_point) = mirror_reference_point { DAffine2::from_translation(mirror_reference_point) * reflection * DAffine2::from_translation(-mirror_reference_point) } else { reflection * DAffine2::from_translation(DVec2::from_angle(angle.to_radians()) * DVec2::splat(-offset)) + }) +} + +/// One output lane of the mirror over its legacy-converted level: the source +/// row's element and standard attributes, the reflection composed onto the +/// mirrored half's transforms. +fn mirror_lane<'e, T: Clone + Default + Send + Sync + 'static>( + arena: &'e core_types::arena::Arena, + legacy: List, + lane: usize, + relative_to_bounds: ReferencePoint, + offset: f64, + angle: f64, + keep_original: bool, +) -> Result< + ( + T, + Attr<'e, TransformAttr>, + Attr<'e, graphic_types::markers::Fill>, + Attr<'e, graphic_types::markers::Stroke>, + Attr<'e, core_types::attribute::BlendMode>, + Attr<'e, core_types::attribute::Opacity>, + Attr<'e, core_types::attribute::OpacityFill>, + Attr<'e, core_types::attribute::ClippingMask>, + Attr<'e, EditorLayerPath>, + ), + Interrupt, +> +where + List: BoundingBox, +{ + let count = legacy.len(); + let reflected_transform = mirror_reflection(&legacy, relative_to_bounds, offset, angle); + let (source, mirrored) = match (reflected_transform.is_some() && keep_original, lane < count) { + (true, true) => (lane, false), + (true, false) => (lane - count, true), + (false, _) => (lane, reflected_transform.is_some()), + }; + if source >= count { + return Err(GraphError::past_end().into()); + } + + let exhausted = || { + Interrupt::from(GraphError { + kind: core_types::gpoll::ErrorKind::ArenaExhausted, + trace: Vec::new(), + }) + }; + let park_paint = |paint: Option>| -> Result>, Interrupt> { + match paint { + Some(paint) => Ok(Some(arena.alloc(paint).ok_or_else(exhausted)?.0)), + None => Ok(None), + } }; - let mut result_list = List::new(); - - // Add original items depending on the keep_original flag - if keep_original { - for item in content.clone().into_iter() { - result_list.push(item); - } + let element = legacy.element(source).cloned().unwrap_or_default(); + let mut transform: DAffine2 = legacy.attribute_cloned_or_default(ATTR_TRANSFORM, source); + if mirrored { + transform = reflected_transform.expect("a mirrored lane exists only under a reflection") * transform; } + let fill = park_paint(legacy.attribute::>(graphic_types::ATTR_FILL, source).cloned())?; + let stroke = park_paint(legacy.attribute::>(graphic_types::ATTR_STROKE, source).cloned())?; + let layer_path: Vec = legacy + .attribute::>(ATTR_EDITOR_LAYER_PATH, source) + .map(|path| path.iter_element_values().copied().collect()) + .unwrap_or_default(); + let layer_path = arena.alloc(layer_path).ok_or_else(exhausted)?.0; - // Create and add mirrored items - for mut row in content.into_iter() { - let current_transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM); - row.set_attribute(ATTR_TRANSFORM, reflected_transform * current_transform); - result_list.push(row); - } - - result_list + Ok(( + element, + Attr(transform), + Attr(fill), + Attr(stroke), + Attr(legacy.attribute_cloned_or_default(core_types::ATTR_BLEND_MODE, source)), + Attr(legacy.attribute_cloned_or(core_types::ATTR_OPACITY, source, 1.)), + Attr(legacy.attribute_cloned_or(core_types::ATTR_OPACITY_FILL, source, 1.)), + Attr(legacy.attribute_cloned_or_default(core_types::ATTR_CLIPPING_MASK, source)), + Attr(layer_path.as_slice()), + )) } +/// The mirrored level: the original lanes (when kept) followed by the +/// reflected lanes. +fn mirror_extent_of(legacy: &List, relative_to_bounds: ReferencePoint, offset: f64, angle: f64, keep_original: bool) -> Extent +where + List: BoundingBox, +{ + match mirror_reflection(legacy, relative_to_bounds, offset, angle) { + Some(_) if keep_original => Extent::Exactly(legacy.len() * 2), + _ => Extent::Exactly(legacy.len()), + } +} + +/// The materialized level as its legacy render list. +fn legacy_render_list_of(content: core_types::node::List<'_, T>) -> List { + // SAFETY: a materialized input's frames are arena-resident. + let item = unsafe { core_types::record::GroupItem::from_resident(content.batch()) }; + graphic_types::graphic::run_to_render_list::(&item).expect("the run holds the row's element type") +} + +#[node_macro::node(category("General"), extent(mirror_extent))] +fn mirror<'e>( + ctx: impl Ctx + core_types::context::ExtractArena<'e> + ExtractIndex + InjectIndex + Copy, + content: IList, + #[default(ReferencePoint::Center)] relative_to_bounds: ReferencePoint, + #[unit(" px")] offset: f64, + #[range] + #[soft(-90..90)] + angle: Angle, + #[default(true)] keep_original: bool, +) -> Result< + IList<( + Graphic, + Attr<'e, TransformAttr>, + Attr<'e, graphic_types::markers::Fill>, + Attr<'e, graphic_types::markers::Stroke>, + Attr<'e, core_types::attribute::BlendMode>, + Attr<'e, core_types::attribute::Opacity>, + Attr<'e, core_types::attribute::OpacityFill>, + Attr<'e, core_types::attribute::ClippingMask>, + Attr<'e, EditorLayerPath>, + )>, + Interrupt, +> { + mirror_lane(ctx.arena(), legacy_render_list_of(content), ctx.innermost_index() as usize, relative_to_bounds, offset, angle, keep_original) +} + +fn mirror_extent( + content: ListIn<'_, Graphic>, + relative_to_bounds: ValueIn<'_, ReferencePoint>, + offset: ValueIn<'_, f64>, + angle: ValueIn<'_, f64>, + keep_original: ValueIn<'_, bool>, + level: LevelIn, +) -> GPoll { + match level.top() { + true => content + .get() + .zip(relative_to_bounds.get()) + .zip(offset.get()) + .zip(angle.get()) + .zip(keep_original.get()) + .map(|((((content, relative_to_bounds), offset), angle), keep_original)| { + mirror_extent_of(&legacy_render_list_of(content), relative_to_bounds, offset, angle, keep_original) + }), + false => GPoll::Final(Extent::Exactly(1)), + } +} + +/// The mirror over a plain vector level, as [`mirror`]. Registered under the +/// mirror identifier. +#[node_macro::node(category(""), extent(mirror_vector_extent))] +fn mirror_vector<'e>( + ctx: impl Ctx + core_types::context::ExtractArena<'e> + ExtractIndex + InjectIndex + Copy, + content: IList, + #[default(ReferencePoint::Center)] relative_to_bounds: ReferencePoint, + #[unit(" px")] offset: f64, + #[range] + #[soft(-90..90)] + angle: Angle, + #[default(true)] keep_original: bool, +) -> Result< + IList<( + Vector, + Attr<'e, TransformAttr>, + Attr<'e, graphic_types::markers::Fill>, + Attr<'e, graphic_types::markers::Stroke>, + Attr<'e, core_types::attribute::BlendMode>, + Attr<'e, core_types::attribute::Opacity>, + Attr<'e, core_types::attribute::OpacityFill>, + Attr<'e, core_types::attribute::ClippingMask>, + Attr<'e, EditorLayerPath>, + )>, + Interrupt, +> { + mirror_lane(ctx.arena(), legacy_render_list_of(content), ctx.innermost_index() as usize, relative_to_bounds, offset, angle, keep_original) +} + +fn mirror_vector_extent( + content: ListIn<'_, Vector>, + relative_to_bounds: ValueIn<'_, ReferencePoint>, + offset: ValueIn<'_, f64>, + angle: ValueIn<'_, f64>, + keep_original: ValueIn<'_, bool>, + level: LevelIn, +) -> GPoll { + match level.top() { + true => content + .get() + .zip(relative_to_bounds.get()) + .zip(offset.get()) + .zip(angle.get()) + .zip(keep_original.get()) + .map(|((((content, relative_to_bounds), offset), angle), keep_original)| { + mirror_extent_of(&legacy_render_list_of(content), relative_to_bounds, offset, angle, keep_original) + }), + false => GPoll::Final(Extent::Exactly(1)), + } +} + +pub use _mirror_vector_mod::mirror_vector_entries; + /// Returns the path identifying the subgraph (network) that contains this proto node — i.e. the input `node_path` /// with its own trailing entry dropped. The terminating element of the returned path is the document node whose /// encapsulated network we live in, so the path doubles as a unique reference to that node at any nesting depth. @@ -632,6 +797,20 @@ pub fn level_to_list( graphic_types::graphic::run_to_render_list::(&item).expect("the run holds the row's element type") } +/// The transitional level bridge into the type-erased list the attribute +/// family consumes, as [`level_to_list`]. +#[node_macro::node(category(""))] +pub fn level_to_list_dyn( + _: impl Ctx + ExtractIndex + InjectIndex + Copy, + #[implementations(Graphic, Vector, Raster, Raster, Color, GradientStops, String)] value: IList, + _converter: (), +) -> ListDyn { + // SAFETY: a materialized input's frames are arena-resident. + let item = unsafe { core_types::record::GroupItem::from_resident(value.batch()) }; + graphic_types::graphic::run_to_render_list::(&item).expect("the run holds the row's element type").into() +} + +pub use _level_to_list_dyn_mod::level_to_list_dyn_entries; pub use _level_to_list_mod::level_to_list_entries; pub use _to_graphic_mod::to_graphic_entries;