mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 13:30:48 +00:00
Implement clipping masks, stroke align, and stroke paint order (#2644)
* refactor: opacity + blend_mode -> blend_style * Add code for clipping * Add alt-click masking * Clip to all colors. Fill option * Fix undo not working. Fix strokes not being white * Allow clipped to be grouped or raster * Switch to alpha mode in mask-type * add plumbing to know if clipped in frontend and add fill slider * Attempt at document upgrade code * Fix fill slider * Add clipped styling and Alt-click layer border * Use mask attr judiciously by using clip when possible * Fix breaking documents and upgrade code * Fix fixes * No-op toggle if last child of parent and don't show clip UI if last element * Fix mouse styles by plumbing clippable to frontend * Fix Clip detection by disallowed groups as clipPath according to SVG spec doesn't allow <g> * Add opacity to clippers can_use_clip check * Fix issue with clipping not working nicely with strokes by using masks * Add vello code * cleanup * Add stroke alignment hacks to SVG renderer * svg: Fix mask bounds in vector data * vello: Implement mask hacks to support stroke alignment * Move around alignment and doc upgrade code * rename Line X -> X * An attempt at fixing names not updating * svg: add stroke order with svg * vello: add stroke order with by calling one before the other explicitly * fix merge * fix svg renderer messing up transform det * Code review; reorder and rename parameters (TODO: fix tools) * Fixes to previous * Formatting * fix bug 3 * some moving around (not fixed) * fix issue 1 * fix vello * Final code review --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
8a3f133140
commit
e238753a35
28 changed files with 1025 additions and 311 deletions
|
@ -187,7 +187,8 @@ impl PreferencesDialogMessageHandler {
|
|||
];
|
||||
|
||||
let mut checkbox_id = CheckboxId::default();
|
||||
let vector_mesh_tooltip = "Allow tools to produce vector meshes, where more than two segments can connect to an anchor point.\n\nCurrently this does not properly handle line joins and fills.";
|
||||
let vector_mesh_tooltip =
|
||||
"Allow tools to produce vector meshes, where more than two segments can connect to an anchor point.\n\nCurrently this does not properly handle stroke joins and fills.";
|
||||
let vector_meshes = vec![
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
|
|
|
@ -121,6 +121,9 @@ pub enum DocumentMessage {
|
|||
SelectedLayersReorder {
|
||||
relative_index_offset: isize,
|
||||
},
|
||||
ClipLayer {
|
||||
id: NodeId,
|
||||
},
|
||||
SelectLayer {
|
||||
id: NodeId,
|
||||
ctrl: bool,
|
||||
|
@ -142,6 +145,9 @@ pub enum DocumentMessage {
|
|||
SetOpacityForSelectedLayers {
|
||||
opacity: f64,
|
||||
},
|
||||
SetFillForSelectedLayers {
|
||||
fill: f64,
|
||||
},
|
||||
SetOverlaysVisibility {
|
||||
visible: bool,
|
||||
overlays_type: Option<OverlaysType>,
|
||||
|
|
|
@ -20,7 +20,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::{Flo
|
|||
use crate::messages::portfolio::document::utility_types::nodes::RawBuffer;
|
||||
use crate::messages::portfolio::utility_types::PersistentData;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_blend_mode, get_opacity};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_blend_mode, get_fill, get_opacity};
|
||||
use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys;
|
||||
use crate::messages::tool::tool_messages::tool_prelude::Key;
|
||||
use crate::messages::tool::utility_types::ToolType;
|
||||
|
@ -1083,6 +1083,12 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
|||
DocumentMessage::SelectedLayersReorder { relative_index_offset } => {
|
||||
self.selected_layers_reorder(relative_index_offset, responses);
|
||||
}
|
||||
DocumentMessage::ClipLayer { id } => {
|
||||
let layer = LayerNodeIdentifier::new(id, &self.network_interface, &[]);
|
||||
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
responses.add(GraphOperationMessage::ClipModeToggle { layer });
|
||||
}
|
||||
DocumentMessage::SelectLayer { id, ctrl, shift } => {
|
||||
let layer = LayerNodeIdentifier::new(id, &self.network_interface, &[]);
|
||||
|
||||
|
@ -1177,6 +1183,12 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
|||
responses.add(GraphOperationMessage::OpacitySet { layer, opacity });
|
||||
}
|
||||
}
|
||||
DocumentMessage::SetFillForSelectedLayers { fill } => {
|
||||
let fill = fill.clamp(0., 1.);
|
||||
for layer in self.network_interface.selected_nodes().selected_layers_except_artboards(&self.network_interface) {
|
||||
responses.add(GraphOperationMessage::BlendingFillSet { layer, fill });
|
||||
}
|
||||
}
|
||||
DocumentMessage::SetOverlaysVisibility { visible, overlays_type } => {
|
||||
let visibility_settings = &mut self.overlays_visibility_settings;
|
||||
let overlays_type = match overlays_type {
|
||||
|
@ -2533,38 +2545,47 @@ impl DocumentMessageHandler {
|
|||
let selected_layers_except_artboards = selected_nodes.selected_layers_except_artboards(&self.network_interface);
|
||||
|
||||
// Look up the current opacity and blend mode of the selected layers (if any), and split the iterator into the first tuple and the rest.
|
||||
let mut opacity_and_blend_mode = selected_layers_except_artboards.map(|layer| {
|
||||
let mut blending_options = selected_layers_except_artboards.map(|layer| {
|
||||
(
|
||||
get_opacity(layer, &self.network_interface).unwrap_or(100.),
|
||||
get_fill(layer, &self.network_interface).unwrap_or(100.),
|
||||
get_blend_mode(layer, &self.network_interface).unwrap_or_default(),
|
||||
)
|
||||
});
|
||||
let first_opacity_and_blend_mode = opacity_and_blend_mode.next();
|
||||
let result_opacity_and_blend_mode = opacity_and_blend_mode;
|
||||
let first_blending_options = blending_options.next();
|
||||
let result_blending_options = blending_options;
|
||||
|
||||
// If there are no selected layers, disable the opacity and blend mode widgets.
|
||||
let disabled = first_opacity_and_blend_mode.is_none();
|
||||
let disabled = first_blending_options.is_none();
|
||||
|
||||
// Amongst the selected layers, check if the opacities and blend modes are identical across all layers.
|
||||
// The result is setting `option` and `blend_mode` to Some value if all their values are identical, or None if they are not.
|
||||
// If identical, we display the value in the widget. If not, we display a dash indicating dissimilarity.
|
||||
let (opacity, blend_mode) = first_opacity_and_blend_mode
|
||||
.map(|(first_opacity, first_blend_mode)| {
|
||||
let (opacity, fill, blend_mode) = first_blending_options
|
||||
.map(|(first_opacity, first_fill, first_blend_mode)| {
|
||||
let mut opacity_identical = true;
|
||||
let mut fill_identical = true;
|
||||
let mut blend_mode_identical = true;
|
||||
|
||||
for (opacity, blend_mode) in result_opacity_and_blend_mode {
|
||||
for (opacity, fill, blend_mode) in result_blending_options {
|
||||
if (opacity - first_opacity).abs() > (f64::EPSILON * 100.) {
|
||||
opacity_identical = false;
|
||||
}
|
||||
if (fill - first_fill).abs() > (f64::EPSILON * 100.) {
|
||||
fill_identical = false;
|
||||
}
|
||||
if blend_mode != first_blend_mode {
|
||||
blend_mode_identical = false;
|
||||
}
|
||||
}
|
||||
|
||||
(opacity_identical.then_some(first_opacity), blend_mode_identical.then_some(first_blend_mode))
|
||||
(
|
||||
opacity_identical.then_some(first_opacity),
|
||||
fill_identical.then_some(first_fill),
|
||||
blend_mode_identical.then_some(first_blend_mode),
|
||||
)
|
||||
})
|
||||
.unwrap_or((None, None));
|
||||
.unwrap_or((None, None, None));
|
||||
|
||||
let blend_mode_menu_entries = BlendMode::list_svg_subset()
|
||||
.iter()
|
||||
|
@ -2623,6 +2644,28 @@ impl DocumentMessageHandler {
|
|||
.max_width(100)
|
||||
.tooltip("Opacity")
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
NumberInput::new(fill)
|
||||
.label("Fill")
|
||||
.unit("%")
|
||||
.display_decimal_places(0)
|
||||
.disabled(disabled)
|
||||
.min(0.)
|
||||
.max(100.)
|
||||
.range_min(Some(0.))
|
||||
.range_max(Some(100.))
|
||||
.mode_range()
|
||||
.on_update(|number_input: &NumberInput| {
|
||||
if let Some(value) = number_input.value {
|
||||
DocumentMessage::SetFillForSelectedLayers { fill: value / 100. }.into()
|
||||
} else {
|
||||
Message::NoOp
|
||||
}
|
||||
})
|
||||
.on_commit(|_| DocumentMessage::AddTransaction.into())
|
||||
.max_width(100)
|
||||
.tooltip("Fill")
|
||||
.widget_holder(),
|
||||
];
|
||||
let layers_panel_control_bar_left = WidgetLayout::new(vec![LayoutGroup::Row { widgets }]);
|
||||
|
||||
|
|
|
@ -21,6 +21,10 @@ pub enum GraphOperationMessage {
|
|||
layer: LayerNodeIdentifier,
|
||||
fill: Fill,
|
||||
},
|
||||
BlendingFillSet {
|
||||
layer: LayerNodeIdentifier,
|
||||
fill: f64,
|
||||
},
|
||||
OpacitySet {
|
||||
layer: LayerNodeIdentifier,
|
||||
opacity: f64,
|
||||
|
@ -29,6 +33,9 @@ pub enum GraphOperationMessage {
|
|||
layer: LayerNodeIdentifier,
|
||||
blend_mode: BlendMode,
|
||||
},
|
||||
ClipModeToggle {
|
||||
layer: LayerNodeIdentifier,
|
||||
},
|
||||
StrokeSet {
|
||||
layer: LayerNodeIdentifier,
|
||||
stroke: Stroke,
|
||||
|
|
|
@ -5,12 +5,13 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
|
|||
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface, OutputConnector};
|
||||
use crate::messages::portfolio::document::utility_types::nodes::CollapsedLayers;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::get_clip_mode;
|
||||
use glam::{DAffine2, DVec2, IVec2};
|
||||
use graph_craft::document::{NodeId, NodeInput};
|
||||
use graphene_core::Color;
|
||||
use graphene_core::renderer::Quad;
|
||||
use graphene_core::text::{Font, TypesettingConfig};
|
||||
use graphene_core::vector::style::{Fill, Gradient, GradientStops, GradientType, LineCap, LineJoin, Stroke};
|
||||
use graphene_core::vector::style::{Fill, Gradient, GradientStops, GradientType, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
use graphene_std::vector::convert_usvg_path;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -41,6 +42,11 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
|
|||
modify_inputs.fill_set(fill);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::BlendingFillSet { layer, fill } => {
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||
modify_inputs.blending_fill_set(fill);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::OpacitySet { layer, opacity } => {
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||
modify_inputs.opacity_set(opacity);
|
||||
|
@ -51,6 +57,12 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
|
|||
modify_inputs.blend_mode_set(blend_mode);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::ClipModeToggle { layer } => {
|
||||
let clip_mode = get_clip_mode(layer, network_interface);
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||
modify_inputs.clip_mode_toggle(clip_mode);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::StrokeSet { layer, stroke } => {
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||
modify_inputs.stroke_set(stroke);
|
||||
|
@ -376,18 +388,20 @@ fn apply_usvg_stroke(stroke: &usvg::Stroke, modify_inputs: &mut ModifyInputsCont
|
|||
weight: stroke.width().get() as f64,
|
||||
dash_lengths: stroke.dasharray().as_ref().map(|lengths| lengths.iter().map(|&length| length as f64).collect()).unwrap_or_default(),
|
||||
dash_offset: stroke.dashoffset() as f64,
|
||||
line_cap: match stroke.linecap() {
|
||||
usvg::LineCap::Butt => LineCap::Butt,
|
||||
usvg::LineCap::Round => LineCap::Round,
|
||||
usvg::LineCap::Square => LineCap::Square,
|
||||
cap: match stroke.linecap() {
|
||||
usvg::LineCap::Butt => StrokeCap::Butt,
|
||||
usvg::LineCap::Round => StrokeCap::Round,
|
||||
usvg::LineCap::Square => StrokeCap::Square,
|
||||
},
|
||||
line_join: match stroke.linejoin() {
|
||||
usvg::LineJoin::Miter => LineJoin::Miter,
|
||||
usvg::LineJoin::MiterClip => LineJoin::Miter,
|
||||
usvg::LineJoin::Round => LineJoin::Round,
|
||||
usvg::LineJoin::Bevel => LineJoin::Bevel,
|
||||
join: match stroke.linejoin() {
|
||||
usvg::LineJoin::Miter => StrokeJoin::Miter,
|
||||
usvg::LineJoin::MiterClip => StrokeJoin::Miter,
|
||||
usvg::LineJoin::Round => StrokeJoin::Round,
|
||||
usvg::LineJoin::Bevel => StrokeJoin::Bevel,
|
||||
},
|
||||
line_join_miter_limit: stroke.miterlimit().get() as f64,
|
||||
join_miter_limit: stroke.miterlimit().get() as f64,
|
||||
align: StrokeAlign::Center,
|
||||
paint_order: PaintOrder::StrokeAbove,
|
||||
transform,
|
||||
non_scaling: false,
|
||||
})
|
||||
|
|
|
@ -15,8 +15,8 @@ use graphene_core::text::{Font, TypesettingConfig};
|
|||
use graphene_core::vector::brush_stroke::BrushStroke;
|
||||
use graphene_core::vector::style::{Fill, Stroke};
|
||||
use graphene_core::vector::{PointId, VectorModificationType};
|
||||
use graphene_std::GraphicGroupTable;
|
||||
use graphene_std::vector::{VectorData, VectorDataTable};
|
||||
use graphene_std::{GraphicGroupTable, NodeInputDecleration};
|
||||
|
||||
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum TransformIn {
|
||||
|
@ -58,13 +58,13 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
/// Non layer nodes directly upstream of a layer are treated as part of that layer. See insert_index == 2 in the diagram
|
||||
/// -----> Post node
|
||||
/// | if insert_index == 0, return (Post node, Some(Layer1))
|
||||
/// -> Layer1
|
||||
/// -> Layer1
|
||||
/// ↑ if insert_index == 1, return (Layer1, Some(Layer2))
|
||||
/// -> Layer2
|
||||
/// -> Layer2
|
||||
/// ↑
|
||||
/// -> NonLayerNode
|
||||
/// ↑ if insert_index == 2, return (NonLayerNode, Some(Layer3))
|
||||
/// -> Layer3
|
||||
/// -> Layer3
|
||||
/// if insert_index == 3, return (Layer3, None)
|
||||
pub fn get_post_node_with_index(network_interface: &NodeNetworkInterface, parent: LayerNodeIdentifier, insert_index: usize) -> InputConnector {
|
||||
let mut post_node_input_connector = if parent == LayerNodeIdentifier::ROOT_PARENT {
|
||||
|
@ -333,37 +333,52 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Fill(fill), false), false);
|
||||
}
|
||||
|
||||
pub fn blend_mode_set(&mut self, blend_mode: BlendMode) {
|
||||
let Some(blend_node_id) = self.existing_node_id("Blending", true) else { return };
|
||||
let input_connector = InputConnector::node(blend_node_id, 1);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::BlendMode(blend_mode), false), false);
|
||||
}
|
||||
|
||||
pub fn opacity_set(&mut self, opacity: f64) {
|
||||
let Some(opacity_node_id) = self.existing_node_id("Opacity", true) else { return };
|
||||
let input_connector = InputConnector::node(opacity_node_id, 1);
|
||||
let Some(blend_node_id) = self.existing_node_id("Blending", true) else { return };
|
||||
let input_connector = InputConnector::node(blend_node_id, 2);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(opacity * 100.), false), false);
|
||||
}
|
||||
|
||||
pub fn blend_mode_set(&mut self, blend_mode: BlendMode) {
|
||||
let Some(blend_mode_node_id) = self.existing_node_id("Blend Mode", true) else {
|
||||
return;
|
||||
};
|
||||
let input_connector = InputConnector::node(blend_mode_node_id, 1);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::BlendMode(blend_mode), false), false);
|
||||
pub fn blending_fill_set(&mut self, fill: f64) {
|
||||
let Some(blend_node_id) = self.existing_node_id("Blending", true) else { return };
|
||||
let input_connector = InputConnector::node(blend_node_id, 3);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(fill * 100.), false), false);
|
||||
}
|
||||
|
||||
pub fn clip_mode_toggle(&mut self, clip_mode: Option<bool>) {
|
||||
let clip = !clip_mode.unwrap_or(false);
|
||||
let Some(clip_node_id) = self.existing_node_id("Blending", true) else { return };
|
||||
let input_connector = InputConnector::node(clip_node_id, 4);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Bool(clip), false), false);
|
||||
}
|
||||
|
||||
pub fn stroke_set(&mut self, stroke: Stroke) {
|
||||
let Some(stroke_node_id) = self.existing_node_id("Stroke", true) else { return };
|
||||
|
||||
let input_connector = InputConnector::node(stroke_node_id, 1);
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::ColorInput::<Option<graphene_std::Color>>::INDEX);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::OptionalColor(stroke.color), false), true);
|
||||
let input_connector = InputConnector::node(stroke_node_id, 2);
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::WeightInput::INDEX);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.weight), false), true);
|
||||
let input_connector = InputConnector::node(stroke_node_id, 3);
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::AlignInput::INDEX);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::StrokeAlign(stroke.align), false), false);
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::CapInput::INDEX);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::StrokeCap(stroke.cap), false), true);
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::JoinInput::INDEX);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::StrokeJoin(stroke.join), false), true);
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::MiterLimitInput::INDEX);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.join_miter_limit), false), false);
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::PaintOrderInput::INDEX);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::PaintOrder(stroke.paint_order), false), false);
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashLengthsInput::INDEX);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::VecF64(stroke.dash_lengths), false), true);
|
||||
let input_connector = InputConnector::node(stroke_node_id, 4);
|
||||
let input_connector = InputConnector::node(stroke_node_id, graphene_std::vector::stroke::DashOffsetInput::INDEX);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.dash_offset), false), true);
|
||||
let input_connector = InputConnector::node(stroke_node_id, 5);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::LineCap(stroke.line_cap), false), true);
|
||||
let input_connector = InputConnector::node(stroke_node_id, 6);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::LineJoin(stroke.line_join), false), true);
|
||||
let input_connector = InputConnector::node(stroke_node_id, 7);
|
||||
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(stroke.line_join_miter_limit), false), false);
|
||||
}
|
||||
|
||||
/// Update the transform value of the upstream Transform node based a change to its existing value and the given parent transform.
|
||||
|
|
|
@ -15,6 +15,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::{
|
|||
use crate::messages::portfolio::document::utility_types::nodes::{CollapsedLayers, LayerPanelEntry};
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::get_clip_mode;
|
||||
use crate::messages::tool::tool_messages::tool_prelude::{Key, MouseMotion};
|
||||
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
|
||||
use glam::{DAffine2, DVec2, IVec2};
|
||||
|
@ -2442,6 +2443,7 @@ impl NodeGraphMessageHandler {
|
|||
}
|
||||
});
|
||||
|
||||
let clippable = layer.can_be_clipped(network_interface.document_metadata());
|
||||
let data = LayerPanelEntry {
|
||||
id: node_id,
|
||||
alias: network_interface.display_name(&node_id, &[]),
|
||||
|
@ -2461,6 +2463,8 @@ impl NodeGraphMessageHandler {
|
|||
selected: selected_layers.contains(&node_id),
|
||||
ancestor_of_selected: ancestors_of_selected.contains(&node_id),
|
||||
descendant_of_selected: descendants_of_selected.contains(&node_id),
|
||||
clipped: get_clip_mode(layer, network_interface).unwrap_or(false) && clippable,
|
||||
clippable,
|
||||
};
|
||||
responses.add(FrontendMessage::UpdateDocumentLayerDetails { data });
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ use graphene_core::raster_types::{CPU, GPU, RasterDataTable};
|
|||
use graphene_core::text::Font;
|
||||
use graphene_core::vector::generator_nodes::grid;
|
||||
use graphene_core::vector::misc::CentroidType;
|
||||
use graphene_core::vector::style::{GradientType, LineCap, LineJoin};
|
||||
use graphene_core::vector::style::{GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
use graphene_std::animation::RealTimeMode;
|
||||
use graphene_std::ops::XY;
|
||||
use graphene_std::transform::{Footprint, ReferencePoint};
|
||||
|
@ -233,8 +233,10 @@ pub(crate) fn property_from_type(
|
|||
Some(x) if x == TypeId::of::<DomainWarpType>() => enum_choice::<DomainWarpType>().for_socket(default_info).disabled(false).property_row(),
|
||||
Some(x) if x == TypeId::of::<RelativeAbsolute>() => enum_choice::<RelativeAbsolute>().for_socket(default_info).disabled(false).property_row(),
|
||||
Some(x) if x == TypeId::of::<GridType>() => enum_choice::<GridType>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<LineCap>() => enum_choice::<LineCap>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<LineJoin>() => enum_choice::<LineJoin>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<StrokeCap>() => enum_choice::<StrokeCap>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<StrokeJoin>() => enum_choice::<StrokeJoin>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<StrokeAlign>() => enum_choice::<StrokeAlign>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<PaintOrder>() => enum_choice::<PaintOrder>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<ArcType>() => enum_choice::<ArcType>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<BooleanOperation>() => enum_choice::<BooleanOperation>().for_socket(default_info).property_row(),
|
||||
Some(x) if x == TypeId::of::<CentroidType>() => enum_choice::<CentroidType>().for_socket(default_info).property_row(),
|
||||
|
@ -1679,20 +1681,43 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
|
|||
return Vec::new();
|
||||
}
|
||||
};
|
||||
let color_index = 1;
|
||||
let weight_index = 2;
|
||||
let dash_lengths_index = 3;
|
||||
let dash_offset_index = 4;
|
||||
let line_cap_index = 5;
|
||||
let line_join_index = 6;
|
||||
let miter_limit_index = 7;
|
||||
let color_index = graphene_std::vector::stroke::ColorInput::<Option<Color>>::INDEX;
|
||||
let weight_index = graphene_std::vector::stroke::WeightInput::INDEX;
|
||||
let align_index = graphene_std::vector::stroke::AlignInput::INDEX;
|
||||
let cap_index = graphene_std::vector::stroke::CapInput::INDEX;
|
||||
let join_index = graphene_std::vector::stroke::JoinInput::INDEX;
|
||||
let miter_limit_index = graphene_std::vector::stroke::MiterLimitInput::INDEX;
|
||||
let paint_order_index = graphene_std::vector::stroke::PaintOrderInput::INDEX;
|
||||
let dash_lengths_index = graphene_std::vector::stroke::DashLengthsInput::INDEX;
|
||||
let dash_offset_index = graphene_std::vector::stroke::DashOffsetInput::INDEX;
|
||||
|
||||
let color = color_widget(ParameterWidgetsInfo::from_index(document_node, node_id, color_index, true, context), ColorInput::default());
|
||||
let weight = number_widget(
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, weight_index, true, context),
|
||||
NumberInput::default().unit(" px").min(0.),
|
||||
);
|
||||
|
||||
let align = enum_choice::<StrokeAlign>()
|
||||
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, align_index, true, context))
|
||||
.property_row();
|
||||
let cap = enum_choice::<StrokeCap>()
|
||||
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, cap_index, true, context))
|
||||
.property_row();
|
||||
let join = enum_choice::<StrokeJoin>()
|
||||
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, join_index, true, context))
|
||||
.property_row();
|
||||
let miter_limit = number_widget(
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, miter_limit_index, true, context),
|
||||
NumberInput::default().min(0.).disabled({
|
||||
let join_value = match &document_node.inputs[join_index].as_value() {
|
||||
Some(TaggedValue::StrokeJoin(x)) => x,
|
||||
_ => &StrokeJoin::Miter,
|
||||
};
|
||||
join_value != &StrokeJoin::Miter
|
||||
}),
|
||||
);
|
||||
let paint_order = enum_choice::<PaintOrder>()
|
||||
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, paint_order_index, true, context))
|
||||
.property_row();
|
||||
let dash_lengths_val = match &document_node.inputs[dash_lengths_index].as_value() {
|
||||
Some(TaggedValue::VecF64(x)) => x,
|
||||
_ => &vec![],
|
||||
|
@ -1703,29 +1728,17 @@ pub fn stroke_properties(node_id: NodeId, context: &mut NodePropertiesContext) -
|
|||
);
|
||||
let number_input = NumberInput::default().unit(" px").disabled(dash_lengths_val.is_empty());
|
||||
let dash_offset = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, dash_offset_index, true, context), number_input);
|
||||
let line_cap = enum_choice::<LineCap>()
|
||||
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, line_cap_index, true, context))
|
||||
.property_row();
|
||||
let line_join = enum_choice::<LineJoin>()
|
||||
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, line_join_index, true, context))
|
||||
.property_row();
|
||||
let line_join_val = match &document_node.inputs[line_join_index].as_value() {
|
||||
Some(TaggedValue::LineJoin(x)) => x,
|
||||
_ => &LineJoin::Miter,
|
||||
};
|
||||
let miter_limit = number_widget(
|
||||
ParameterWidgetsInfo::from_index(document_node, node_id, miter_limit_index, true, context),
|
||||
NumberInput::default().min(0.).disabled(line_join_val != &LineJoin::Miter),
|
||||
);
|
||||
|
||||
vec![
|
||||
color,
|
||||
LayoutGroup::Row { widgets: weight },
|
||||
align,
|
||||
cap,
|
||||
join,
|
||||
LayoutGroup::Row { widgets: miter_limit },
|
||||
paint_order,
|
||||
LayoutGroup::Row { widgets: dash_lengths },
|
||||
LayoutGroup::Row { widgets: dash_offset },
|
||||
line_cap,
|
||||
line_join,
|
||||
LayoutGroup::Row { widgets: miter_limit },
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -1737,25 +1750,27 @@ pub fn offset_path_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
|||
return Vec::new();
|
||||
}
|
||||
};
|
||||
let distance_index = 1;
|
||||
let line_join_index = 2;
|
||||
let miter_limit_index = 3;
|
||||
let distance_index = graphene_std::vector::offset_path::DistanceInput::INDEX;
|
||||
let join_index = graphene_std::vector::offset_path::JoinInput::INDEX;
|
||||
let miter_limit_index = graphene_std::vector::offset_path::MiterLimitInput::INDEX;
|
||||
|
||||
let number_input = NumberInput::default().unit(" px");
|
||||
let distance = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, distance_index, true, context), number_input);
|
||||
|
||||
let line_join = enum_choice::<LineJoin>()
|
||||
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, line_join_index, true, context))
|
||||
let join = enum_choice::<StrokeJoin>()
|
||||
.for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, join_index, true, context))
|
||||
.property_row();
|
||||
let line_join_val = match &document_node.inputs[line_join_index].as_value() {
|
||||
Some(TaggedValue::LineJoin(x)) => x,
|
||||
_ => &LineJoin::Miter,
|
||||
};
|
||||
|
||||
let number_input = NumberInput::default().min(0.).disabled(line_join_val != &LineJoin::Miter);
|
||||
let number_input = NumberInput::default().min(0.).disabled({
|
||||
let join_val = match &document_node.inputs[join_index].as_value() {
|
||||
Some(TaggedValue::StrokeJoin(x)) => x,
|
||||
_ => &StrokeJoin::Miter,
|
||||
};
|
||||
join_val != &StrokeJoin::Miter
|
||||
});
|
||||
let miter_limit = number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, miter_limit_index, true, context), number_input);
|
||||
|
||||
vec![LayoutGroup::Row { widgets: distance }, line_join, LayoutGroup::Row { widgets: miter_limit }]
|
||||
vec![LayoutGroup::Row { widgets: distance }, join, LayoutGroup::Row { widgets: miter_limit }]
|
||||
}
|
||||
|
||||
pub fn math_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
|
|
|
@ -287,6 +287,12 @@ impl LayerNodeIdentifier {
|
|||
child.ancestors(metadata).any(|ancestor| ancestor == self)
|
||||
}
|
||||
|
||||
/// Is the layer last child of parent group? Used for clipping
|
||||
pub fn can_be_clipped(self, metadata: &DocumentMetadata) -> bool {
|
||||
self.parent(metadata)
|
||||
.map_or(false, |layer| layer.last_child(metadata).expect("Parent accessed via child should have children") != self)
|
||||
}
|
||||
|
||||
/// Iterator over all direct children (excluding self and recursive children)
|
||||
pub fn children(self, metadata: &DocumentMetadata) -> AxisIter {
|
||||
AxisIter {
|
||||
|
|
|
@ -1160,6 +1160,13 @@ impl NodeNetworkInterface {
|
|||
.and_then(|node_metadata| node_metadata.persistent_metadata.input_properties.get(index))
|
||||
}
|
||||
|
||||
pub fn insert_input_properties_row(&mut self, node_id: &NodeId, index: usize, network_path: &[NodeId]) {
|
||||
let row = ("", "TODO").into();
|
||||
let _ = self
|
||||
.node_metadata_mut(node_id, network_path)
|
||||
.map(|node_metadata| node_metadata.persistent_metadata.input_properties.insert(index - 1, row));
|
||||
}
|
||||
|
||||
pub fn input_metadata(&self, node_id: &NodeId, index: usize, field: &str, network_path: &[NodeId]) -> Option<&Value> {
|
||||
let Some(input_row) = self.input_properties_row(node_id, index, network_path) else {
|
||||
log::error!("Could not get input_row in get_input_metadata");
|
||||
|
|
|
@ -55,6 +55,8 @@ pub struct LayerPanelEntry {
|
|||
pub ancestor_of_selected: bool,
|
||||
#[serde(rename = "descendantOfSelected")]
|
||||
pub descendant_of_selected: bool,
|
||||
pub clipped: bool,
|
||||
pub clippable: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, PartialEq, Eq, specta::Type)]
|
||||
|
|
|
@ -24,7 +24,7 @@ use graph_craft::document::value::TaggedValue;
|
|||
use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput};
|
||||
use graphene_core::renderer::Quad;
|
||||
use graphene_core::text::{Font, TypesettingConfig};
|
||||
use graphene_std::vector::style::{Fill, FillType, Gradient};
|
||||
use graphene_std::vector::style::{Fill, FillType, Gradient, PaintOrder, StrokeAlign};
|
||||
use graphene_std::vector::{VectorData, VectorDataTable};
|
||||
use std::vec;
|
||||
|
||||
|
@ -678,6 +678,30 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
|
|||
}
|
||||
}
|
||||
|
||||
// Upgrade Stroke node to reorder parameters and add "Align" and "Paint Order" (#2644)
|
||||
if reference == "Stroke" && inputs_count == 8 {
|
||||
let node_definition = resolve_document_node_type(reference).unwrap();
|
||||
let document_node = node_definition.default_node_template().document_node;
|
||||
document.network_interface.replace_implementation(node_id, network_path, document_node.implementation.clone());
|
||||
document.network_interface.insert_input_properties_row(node_id, 8, network_path);
|
||||
document.network_interface.insert_input_properties_row(node_id, 9, network_path);
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, document_node.inputs.clone(), network_path);
|
||||
let align_input = NodeInput::value(TaggedValue::StrokeAlign(StrokeAlign::Center), false);
|
||||
let paint_order_input = NodeInput::value(TaggedValue::PaintOrder(PaintOrder::StrokeAbove), false);
|
||||
|
||||
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), old_inputs[1].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[2].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 3), align_input, network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[5].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 5), old_inputs[6].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 6), old_inputs[7].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 7), paint_order_input, network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 8), old_inputs[3].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node(*node_id, 9), old_inputs[4].clone(), network_path);
|
||||
}
|
||||
|
||||
// Rename the old "Splines from Points" node to "Spline" and upgrade it to the new "Spline" node
|
||||
if reference == "Splines from Points" {
|
||||
document.network_interface.set_reference(node_id, network_path, Some("Spline".to_string()));
|
||||
|
|
|
@ -13,6 +13,7 @@ use graphene_core::raster::BlendMode;
|
|||
use graphene_core::raster_types::{CPU, GPU, RasterDataTable};
|
||||
use graphene_core::text::{Font, TypesettingConfig};
|
||||
use graphene_core::vector::style::Gradient;
|
||||
use graphene_std::NodeInputDecleration;
|
||||
use graphene_std::vector::{ManipulatorPointId, PointId, SegmentId, VectorModificationType};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
|
@ -258,7 +259,7 @@ pub fn get_viewport_pivot(layer: LayerNodeIdentifier, network_interface: &NodeNe
|
|||
network_interface.document_metadata().transform_to_viewport(layer).transform_point2(min + (max - min) * pivot)
|
||||
}
|
||||
|
||||
/// Get the current gradient of a layer from the closest Fill node
|
||||
/// Get the current gradient of a layer from the closest "Fill" node.
|
||||
pub fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Gradient> {
|
||||
let fill_index = 1;
|
||||
|
||||
|
@ -269,7 +270,7 @@ pub fn get_gradient(layer: LayerNodeIdentifier, network_interface: &NodeNetworkI
|
|||
Some(gradient.clone())
|
||||
}
|
||||
|
||||
/// Get the current fill of a layer from the closest Fill node
|
||||
/// Get the current fill of a layer from the closest "Fill" node.
|
||||
pub fn get_fill_color(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<Color> {
|
||||
let fill_index = 1;
|
||||
|
||||
|
@ -280,16 +281,16 @@ pub fn get_fill_color(layer: LayerNodeIdentifier, network_interface: &NodeNetwor
|
|||
Some(color.to_linear_srgb())
|
||||
}
|
||||
|
||||
/// Get the current blend mode of a layer from the closest Blend Mode node
|
||||
/// Get the current blend mode of a layer from the closest "Blending" node.
|
||||
pub fn get_blend_mode(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<BlendMode> {
|
||||
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blend Mode")?;
|
||||
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blending")?;
|
||||
let TaggedValue::BlendMode(blend_mode) = inputs.get(1)?.as_value()? else {
|
||||
return None;
|
||||
};
|
||||
Some(*blend_mode)
|
||||
}
|
||||
|
||||
/// Get the current opacity of a layer from the closest Opacity node.
|
||||
/// Get the current opacity of a layer from the closest "Blending" node.
|
||||
/// This may differ from the actual opacity contained within the data type reaching this layer, because that actual opacity may be:
|
||||
/// - Multiplied with additional opacity nodes earlier in the chain
|
||||
/// - Set by an Opacity node with an exposed input value driven by another node
|
||||
|
@ -298,13 +299,29 @@ pub fn get_blend_mode(layer: LayerNodeIdentifier, network_interface: &NodeNetwor
|
|||
///
|
||||
/// With those limitations in mind, the intention of this function is to show just the value already present in an upstream Opacity node so that value can be directly edited.
|
||||
pub fn get_opacity(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<f64> {
|
||||
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Opacity")?;
|
||||
let TaggedValue::F64(opacity) = inputs.get(1)?.as_value()? else {
|
||||
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blending")?;
|
||||
let TaggedValue::F64(opacity) = inputs.get(2)?.as_value()? else {
|
||||
return None;
|
||||
};
|
||||
Some(*opacity)
|
||||
}
|
||||
|
||||
pub fn get_clip_mode(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<bool> {
|
||||
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blending")?;
|
||||
let TaggedValue::Bool(clip) = inputs.get(4)?.as_value()? else {
|
||||
return None;
|
||||
};
|
||||
Some(*clip)
|
||||
}
|
||||
|
||||
pub fn get_fill(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<f64> {
|
||||
let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blending")?;
|
||||
let TaggedValue::F64(fill) = inputs.get(3)?.as_value()? else {
|
||||
return None;
|
||||
};
|
||||
Some(*fill)
|
||||
}
|
||||
|
||||
pub fn get_fill_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
|
||||
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Fill")
|
||||
}
|
||||
|
@ -356,7 +373,7 @@ pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInter
|
|||
}
|
||||
|
||||
pub fn get_stroke_width(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<f64> {
|
||||
let weight_node_input_index = 2;
|
||||
let weight_node_input_index = graphene_std::vector::stroke::WeightInput::INDEX;
|
||||
if let TaggedValue::F64(width) = NodeGraphLayer::new(layer, network_interface).find_input("Stroke", weight_node_input_index)? {
|
||||
Some(*width)
|
||||
} else {
|
||||
|
|
|
@ -338,7 +338,15 @@ impl NodeGraphExecutor {
|
|||
fn debug_render(render_object: impl GraphicElementRendered, transform: DAffine2, responses: &mut VecDeque<Message>) {
|
||||
// Setup rendering
|
||||
let mut render = SvgRender::new();
|
||||
let render_params = RenderParams::new(ViewMode::Normal, None, false, false, false);
|
||||
let render_params = RenderParams {
|
||||
view_mode: ViewMode::Normal,
|
||||
culling_bounds: None,
|
||||
thumbnail: false,
|
||||
hide_artboards: false,
|
||||
for_export: false,
|
||||
for_mask: false,
|
||||
alignment_parent_transform: None,
|
||||
};
|
||||
|
||||
// Render SVG
|
||||
render_object.render_svg(&mut render, &render_params);
|
||||
|
|
|
@ -323,7 +323,15 @@ impl NodeRuntime {
|
|||
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);
|
||||
let render_params = RenderParams {
|
||||
view_mode: ViewMode::Normal,
|
||||
culling_bounds: bounds,
|
||||
thumbnail: true,
|
||||
hide_artboards: false,
|
||||
for_export: false,
|
||||
for_mask: false,
|
||||
alignment_parent_transform: None,
|
||||
};
|
||||
let mut render = SvgRender::new();
|
||||
graphic_element.render_svg(&mut render, &render_params);
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { getContext, onMount, tick } from "svelte";
|
||||
import { getContext, onMount, onDestroy, tick } from "svelte";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import { beginDraggingElement } from "@graphite/io-managers/drag";
|
||||
|
@ -55,6 +55,10 @@
|
|||
let fakeHighlightOfNotYetSelectedLayerBeingDragged: undefined | bigint = undefined;
|
||||
let dragInPanel = false;
|
||||
|
||||
// Interactive clipping
|
||||
let layerToClipUponClick: LayerListingInfo | undefined = undefined;
|
||||
let layerToClipAltKeyPressed = false;
|
||||
|
||||
// Layouts
|
||||
let layersPanelControlBarLeftLayout = defaultWidgetLayout();
|
||||
let layersPanelControlBarRightLayout = defaultWidgetLayout();
|
||||
|
@ -87,6 +91,16 @@
|
|||
|
||||
updateLayerInTree(targetId, targetLayer);
|
||||
});
|
||||
|
||||
addEventListener("pointermove", clippingHover);
|
||||
addEventListener("keydown", clippingKeyPress);
|
||||
addEventListener("keyup", clippingKeyPress);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
removeEventListener("pointermove", clippingHover);
|
||||
removeEventListener("keydown", clippingKeyPress);
|
||||
removeEventListener("keyup", clippingKeyPress);
|
||||
});
|
||||
|
||||
type DocumentLayerStructure = {
|
||||
|
@ -208,12 +222,58 @@
|
|||
// Get the state of the platform's accel key and its opposite platform's accel key
|
||||
const [accel, oppositeAccel] = platformIsMac() ? [meta, ctrl] : [ctrl, meta];
|
||||
|
||||
// Alt-clicking to make a clipping mask
|
||||
if (layerToClipAltKeyPressed && layerToClipUponClick && layerToClipUponClick.entry.clippable) clipLayer(layerToClipUponClick);
|
||||
// Select the layer only if the accel and/or shift keys are pressed
|
||||
if (!oppositeAccel && !alt) selectLayer(listing, accel, shift);
|
||||
else if (!oppositeAccel && !alt) selectLayer(listing, accel, shift);
|
||||
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
function clipLayer(listing: LayerListingInfo) {
|
||||
editor.handle.clipLayer(listing.entry.id);
|
||||
}
|
||||
|
||||
function clippingKeyPress(e: KeyboardEvent) {
|
||||
layerToClipAltKeyPressed = e.altKey;
|
||||
}
|
||||
|
||||
function clippingHover(e: PointerEvent) {
|
||||
// Don't do anything if the user is dragging to rearrange layers
|
||||
if (dragInPanel) return;
|
||||
|
||||
// Get the layer below the cursor
|
||||
const target = (e.target instanceof HTMLElement && e.target.closest("[data-layer]")) || undefined;
|
||||
if (!target) {
|
||||
layerToClipUponClick = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the cursor is near the border btween two layers
|
||||
const DISTANCE = 6;
|
||||
const distanceFromTop = e.clientY - target.getBoundingClientRect().top;
|
||||
const distanceFromBottom = target.getBoundingClientRect().bottom - e.clientY;
|
||||
|
||||
const nearTop = distanceFromTop < DISTANCE;
|
||||
const nearBottom = distanceFromBottom < DISTANCE;
|
||||
|
||||
// If we are not near the border, we don't want to clip
|
||||
if (!nearTop && !nearBottom) {
|
||||
layerToClipUponClick = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
// If we are near the border, we want to clip the layer above the border
|
||||
const indexAttribute = target?.getAttribute("data-index") ?? undefined;
|
||||
const index = indexAttribute ? Number(indexAttribute) : undefined;
|
||||
const layer = index !== undefined && layers[nearTop ? index - 1 : index];
|
||||
if (!layer) return;
|
||||
|
||||
// Update the state used to show the clipping action
|
||||
layerToClipUponClick = layer;
|
||||
layerToClipAltKeyPressed = e.altKey;
|
||||
}
|
||||
|
||||
function selectLayer(listing: LayerListingInfo, accel: boolean, shift: boolean) {
|
||||
// Don't select while we are entering text to rename the layer
|
||||
if (listing.editingName) return;
|
||||
|
@ -433,7 +493,16 @@
|
|||
<WidgetLayout layout={layersPanelControlBarRightLayout} />
|
||||
</LayoutRow>
|
||||
<LayoutRow class="list-area" scrollableY={true}>
|
||||
<LayoutCol class="list" data-layer-panel bind:this={list} on:click={() => deselectAllLayers()} on:dragover={updateInsertLine} on:dragend={drop} on:drop={drop}>
|
||||
<LayoutCol
|
||||
class="list"
|
||||
styles={{ cursor: layerToClipUponClick && layerToClipAltKeyPressed && layerToClipUponClick.entry.clippable ? "alias" : "auto" }}
|
||||
data-layer-panel
|
||||
bind:this={list}
|
||||
on:click={() => deselectAllLayers()}
|
||||
on:dragover={updateInsertLine}
|
||||
on:dragend={drop}
|
||||
on:drop={drop}
|
||||
>
|
||||
{#each layers as listing, index}
|
||||
{@const selected = fakeHighlightOfNotYetSelectedLayerBeingDragged !== undefined ? fakeHighlightOfNotYetSelectedLayerBeingDragged === listing.entry.id : listing.entry.selected}
|
||||
<LayoutRow
|
||||
|
@ -464,6 +533,11 @@
|
|||
on:click={(e) => handleExpandArrowClickWithModifiers(e, listing.entry.id)}
|
||||
tabindex="0"
|
||||
></button>
|
||||
{:else}
|
||||
<div class="expand-arrow-none"></div>
|
||||
{/if}
|
||||
{#if listing.entry.clipped}
|
||||
<IconLabel icon="Clipped" class="clipped-arrow" tooltip={"Clipping mask is active (Alt-click border to release)"} />
|
||||
{/if}
|
||||
<div class="thumbnail">
|
||||
{#if $nodeGraph.thumbnails.has(listing.entry.id)}
|
||||
|
@ -589,6 +663,7 @@
|
|||
.expand-arrow {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin-right: 4px;
|
||||
width: 16px;
|
||||
height: 100%;
|
||||
border: none;
|
||||
|
@ -625,10 +700,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
.expand-arrow-none {
|
||||
flex: 0 0 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.clipped-arrow {
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
width: 36px;
|
||||
height: 24px;
|
||||
margin-left: 4px;
|
||||
border-radius: 2px;
|
||||
flex: 0 0 auto;
|
||||
background-image: var(--color-transparent-checkered-background);
|
||||
|
@ -636,10 +720,6 @@
|
|||
background-position: var(--color-transparent-checkered-background-position-mini);
|
||||
background-repeat: var(--color-transparent-checkered-background-repeat);
|
||||
|
||||
&:first-child {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: calc(100% - 4px);
|
||||
height: calc(100% - 4px);
|
||||
|
|
|
@ -907,6 +907,10 @@ export class LayerPanelEntry {
|
|||
ancestorOfSelected!: boolean;
|
||||
|
||||
descendantOfSelected!: boolean;
|
||||
|
||||
clipped!: boolean;
|
||||
|
||||
clippable!: boolean;
|
||||
}
|
||||
|
||||
export class DisplayDialogDismiss extends JsMessage {}
|
||||
|
|
|
@ -504,6 +504,13 @@ impl EditorHandle {
|
|||
self.dispatch(message);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = clipLayer)]
|
||||
pub fn clip_layer(&self, id: u64) {
|
||||
let id = NodeId(id);
|
||||
let message = DocumentMessage::ClipLayer { id };
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
/// Modify the layer selection based on the layer which is clicked while holding down the <kbd>Ctrl</kbd> and/or <kbd>Shift</kbd> modifier keys used for range selection behavior
|
||||
#[wasm_bindgen(js_name = selectLayer)]
|
||||
pub fn select_layer(&self, id: u64, ctrl: bool, shift: bool) {
|
||||
|
|
|
@ -14,9 +14,12 @@ pub mod renderer;
|
|||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, DynAny, specta::Type)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[serde(default)]
|
||||
pub struct AlphaBlending {
|
||||
pub opacity: f32,
|
||||
pub blend_mode: BlendMode,
|
||||
pub opacity: f32,
|
||||
pub fill: f32,
|
||||
pub clip: bool,
|
||||
}
|
||||
impl Default for AlphaBlending {
|
||||
fn default() -> Self {
|
||||
|
@ -26,13 +29,22 @@ impl Default for AlphaBlending {
|
|||
impl core::hash::Hash for AlphaBlending {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
self.opacity.to_bits().hash(state);
|
||||
self.fill.to_bits().hash(state);
|
||||
self.blend_mode.hash(state);
|
||||
self.clip.hash(state);
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for AlphaBlending {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let round = |x: f32| (x * 1e3).round() / 1e3;
|
||||
write!(f, "Opacity: {}% — Blend Mode: {}", round(self.opacity * 100.), self.blend_mode)
|
||||
write!(
|
||||
f,
|
||||
"Blend Mode: {} — Opacity: {}% — Fill: {}% — Clip: {}",
|
||||
self.blend_mode,
|
||||
round(self.opacity * 100.),
|
||||
round(self.fill * 100.),
|
||||
if self.clip { "Yes" } else { "No" }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,7 +52,9 @@ impl AlphaBlending {
|
|||
pub const fn new() -> Self {
|
||||
Self {
|
||||
opacity: 1.,
|
||||
fill: 1.,
|
||||
blend_mode: BlendMode::Normal,
|
||||
clip: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,7 +63,9 @@ impl AlphaBlending {
|
|||
|
||||
AlphaBlending {
|
||||
opacity: lerp(self.opacity, other.opacity, t),
|
||||
fill: lerp(self.fill, other.fill, t),
|
||||
blend_mode: if t < 0.5 { self.blend_mode } else { other.blend_mode },
|
||||
clip: if t < 0.5 { self.clip } else { other.clip },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -205,6 +221,26 @@ impl GraphicElement {
|
|||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn had_clip_enabled(&self) -> bool {
|
||||
match self {
|
||||
GraphicElement::VectorData(data) => data.instance_ref_iter().all(|instance| instance.alpha_blending.clip),
|
||||
GraphicElement::GraphicGroup(data) => data.instance_ref_iter().all(|instance| instance.alpha_blending.clip),
|
||||
GraphicElement::RasterDataCPU(data) => data.instance_ref_iter().all(|instance| instance.alpha_blending.clip),
|
||||
GraphicElement::RasterDataGPU(data) => data.instance_ref_iter().all(|instance| instance.alpha_blending.clip),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_reduce_to_clip_path(&self) -> bool {
|
||||
match self {
|
||||
GraphicElement::VectorData(vector_data_table) => vector_data_table.instance_ref_iter().all(|instance_data| {
|
||||
let style = &instance_data.instance.style;
|
||||
let alpha_blending = &instance_data.alpha_blending;
|
||||
(alpha_blending.opacity > 1. - f32::EPSILON) && style.fill().is_opaque() && style.stroke().is_none_or(|stroke| !stroke.has_renderable_stroke())
|
||||
}),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// // TODO: Rename to Raster
|
||||
|
@ -448,8 +484,10 @@ async fn flatten_vector(_: impl Ctx, group: GraphicGroupTable) -> VectorDataTabl
|
|||
instance: current_element.instance.clone(),
|
||||
transform: *current_instance.transform * *current_element.transform,
|
||||
alpha_blending: AlphaBlending {
|
||||
opacity: current_instance.alpha_blending.opacity * current_element.alpha_blending.opacity,
|
||||
blend_mode: current_element.alpha_blending.blend_mode,
|
||||
opacity: current_instance.alpha_blending.opacity * current_element.alpha_blending.opacity,
|
||||
fill: current_element.alpha_blending.fill,
|
||||
clip: current_element.alpha_blending.clip,
|
||||
},
|
||||
source_node_id: reference,
|
||||
});
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
mod quad;
|
||||
mod rect;
|
||||
|
||||
use crate::instances::Instance;
|
||||
use crate::raster::{BlendMode, Image};
|
||||
use crate::raster_types::{CPU, GPU, RasterDataTable};
|
||||
use crate::transform::{Footprint, Transform};
|
||||
use crate::uuid::{NodeId, generate_uuid};
|
||||
use crate::vector::style::{Fill, Stroke, ViewMode};
|
||||
use crate::vector::style::{Fill, Stroke, StrokeAlign, ViewMode};
|
||||
use crate::vector::{PointId, VectorDataTable};
|
||||
use crate::{Artboard, ArtboardGroupTable, Color, GraphicElement, GraphicGroupTable};
|
||||
use base64::Engine;
|
||||
|
@ -50,6 +51,29 @@ pub struct ClickTarget {
|
|||
bounding_box: Option<[DVec2; 2]>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
enum MaskType {
|
||||
Clip,
|
||||
Mask,
|
||||
}
|
||||
|
||||
impl MaskType {
|
||||
fn to_attribute(self) -> String {
|
||||
match self {
|
||||
Self::Mask => "mask".to_string(),
|
||||
Self::Clip => "clip-path".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_to_defs(self, svg_defs: &mut String, uuid: u64, svg_string: String) {
|
||||
let id = format!("mask-{}", uuid);
|
||||
match self {
|
||||
Self::Clip => write!(svg_defs, r##"<clipPath id="{id}">{}</clipPath>"##, svg_string).unwrap(),
|
||||
Self::Mask => write!(svg_defs, r##"<mask id="{id}" mask-type="alpha">{}</mask>"##, svg_string).unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ClickTarget {
|
||||
pub fn new_with_subpath(subpath: bezier_rs::Subpath<PointId>, stroke_width: f64) -> Self {
|
||||
let bounding_box = subpath.loose_bounding_box();
|
||||
|
@ -289,17 +313,20 @@ pub struct RenderParams {
|
|||
pub hide_artboards: bool,
|
||||
/// Are we exporting? Causes the text above an artboard to be hidden.
|
||||
pub for_export: bool,
|
||||
/// Are we generating a mask in this render pass? Used to see if fill should be multiplied with alpha.
|
||||
pub for_mask: bool,
|
||||
/// Are we generating a mask for alignment? Used to prevent unnecesary transforms in masks
|
||||
pub alignment_parent_transform: Option<DAffine2>,
|
||||
}
|
||||
|
||||
impl RenderParams {
|
||||
pub fn new(view_mode: ViewMode, culling_bounds: Option<[DVec2; 2]>, thumbnail: bool, hide_artboards: bool, for_export: bool) -> Self {
|
||||
Self {
|
||||
view_mode,
|
||||
culling_bounds,
|
||||
thumbnail,
|
||||
hide_artboards,
|
||||
for_export,
|
||||
}
|
||||
pub fn for_clipper(&self) -> Self {
|
||||
Self { for_mask: true, ..*self }
|
||||
}
|
||||
|
||||
pub fn for_alignment(&self, transform: DAffine2) -> Self {
|
||||
let alignment_parent_transform = Some(transform);
|
||||
Self { alignment_parent_transform, ..*self }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -362,7 +389,10 @@ pub trait GraphicElementRendered {
|
|||
|
||||
impl GraphicElementRendered for GraphicGroupTable {
|
||||
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
||||
for instance in self.instance_ref_iter() {
|
||||
let mut iter = self.instance_ref_iter().peekable();
|
||||
let mut mask_state = None;
|
||||
|
||||
while let Some(instance) = iter.next() {
|
||||
render.parent_tag(
|
||||
"g",
|
||||
|attributes| {
|
||||
|
@ -371,13 +401,37 @@ impl GraphicElementRendered for GraphicGroupTable {
|
|||
attributes.push("transform", matrix);
|
||||
}
|
||||
|
||||
if instance.alpha_blending.opacity < 1. {
|
||||
attributes.push("opacity", instance.alpha_blending.opacity.to_string());
|
||||
let factor = if render_params.for_mask { 1. } else { instance.alpha_blending.fill };
|
||||
let opacity = instance.alpha_blending.opacity * factor;
|
||||
if opacity < 1. {
|
||||
attributes.push("opacity", opacity.to_string());
|
||||
}
|
||||
|
||||
if instance.alpha_blending.blend_mode != BlendMode::default() {
|
||||
attributes.push("style", instance.alpha_blending.blend_mode.render());
|
||||
}
|
||||
|
||||
let next_clips = iter.peek().is_some_and(|next_instance| next_instance.instance.had_clip_enabled());
|
||||
|
||||
if next_clips && mask_state.is_none() {
|
||||
let uuid = generate_uuid();
|
||||
let mask_type = if instance.instance.can_reduce_to_clip_path() { MaskType::Clip } else { MaskType::Mask };
|
||||
mask_state = Some((uuid, mask_type));
|
||||
let mut svg = SvgRender::new();
|
||||
instance.instance.render_svg(&mut svg, &render_params.for_clipper());
|
||||
|
||||
write!(&mut attributes.0.svg_defs, r##"{}"##, svg.svg_defs).unwrap();
|
||||
mask_type.write_to_defs(&mut attributes.0.svg_defs, uuid, svg.svg.to_svg_string());
|
||||
} else if let Some((uuid, mask_type)) = mask_state {
|
||||
if !next_clips {
|
||||
mask_state = None;
|
||||
}
|
||||
|
||||
let id = format!("mask-{}", uuid);
|
||||
let selector = format!("url(#{id})");
|
||||
|
||||
attributes.push(mask_type.to_attribute(), selector);
|
||||
}
|
||||
},
|
||||
|render| {
|
||||
instance.instance.render_svg(render, render_params);
|
||||
|
@ -388,25 +442,31 @@ impl GraphicElementRendered for GraphicGroupTable {
|
|||
|
||||
#[cfg(feature = "vello")]
|
||||
fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) {
|
||||
for instance in self.instance_ref_iter() {
|
||||
let mut iter = self.instance_ref_iter().peekable();
|
||||
let mut mask_instance_state = None;
|
||||
|
||||
while let Some(instance) = iter.next() {
|
||||
let transform = transform * *instance.transform;
|
||||
let alpha_blending = *instance.alpha_blending;
|
||||
|
||||
let mut layer = false;
|
||||
if let Some(bounds) = self
|
||||
|
||||
let bounds = self
|
||||
.instance_ref_iter()
|
||||
.filter_map(|element| element.instance.bounding_box(transform, true))
|
||||
.reduce(Quad::combine_bounds)
|
||||
{
|
||||
.reduce(Quad::combine_bounds);
|
||||
if let Some(bounds) = bounds {
|
||||
let blend_mode = match render_params.view_mode {
|
||||
ViewMode::Outline => peniko::Mix::Normal,
|
||||
_ => alpha_blending.blend_mode.into(),
|
||||
};
|
||||
|
||||
if alpha_blending.opacity < 1. || (render_params.view_mode != ViewMode::Outline && alpha_blending.blend_mode != BlendMode::default()) {
|
||||
let factor = if render_params.for_mask { 1. } else { alpha_blending.fill };
|
||||
let opacity = alpha_blending.opacity * factor;
|
||||
if opacity < 1. || (render_params.view_mode != ViewMode::Outline && alpha_blending.blend_mode != BlendMode::default()) {
|
||||
scene.push_layer(
|
||||
peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver),
|
||||
alpha_blending.opacity,
|
||||
opacity,
|
||||
kurbo::Affine::IDENTITY,
|
||||
&vello::kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y),
|
||||
);
|
||||
|
@ -414,7 +474,33 @@ impl GraphicElementRendered for GraphicGroupTable {
|
|||
}
|
||||
}
|
||||
|
||||
instance.instance.render_to_vello(scene, transform, context, render_params);
|
||||
let next_clips = iter.peek().is_some_and(|next_instance| next_instance.instance.had_clip_enabled());
|
||||
if next_clips && mask_instance_state.is_none() {
|
||||
mask_instance_state = Some((instance.instance, transform));
|
||||
|
||||
instance.instance.render_to_vello(scene, transform, context, render_params);
|
||||
} else if let Some((instance_mask, transform_mask)) = mask_instance_state {
|
||||
if !next_clips {
|
||||
mask_instance_state = None;
|
||||
}
|
||||
|
||||
if let Some(bounds) = bounds {
|
||||
let rect = vello::kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y);
|
||||
|
||||
scene.push_layer(peniko::Mix::Normal, 1., kurbo::Affine::IDENTITY, &rect);
|
||||
instance_mask.render_to_vello(scene, transform_mask, context, &render_params.for_clipper());
|
||||
scene.push_layer(peniko::BlendMode::new(peniko::Mix::Clip, peniko::Compose::SrcIn), 1., kurbo::Affine::IDENTITY, &rect);
|
||||
}
|
||||
|
||||
instance.instance.render_to_vello(scene, transform, context, render_params);
|
||||
|
||||
if bounds.is_some() {
|
||||
scene.pop_layer();
|
||||
scene.pop_layer();
|
||||
}
|
||||
} else {
|
||||
instance.instance.render_to_vello(scene, transform, context, render_params);
|
||||
}
|
||||
|
||||
if layer {
|
||||
scene.pop_layer();
|
||||
|
@ -488,21 +574,54 @@ impl GraphicElementRendered for GraphicGroupTable {
|
|||
impl GraphicElementRendered for VectorDataTable {
|
||||
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
||||
for instance in self.instance_ref_iter() {
|
||||
let multiplied_transform = render.transform * *instance.transform;
|
||||
let multiplied_transform = *instance.transform;
|
||||
let vector_data = &instance.instance;
|
||||
// Only consider strokes with non-zero weight, since default strokes with zero weight would prevent assigning the correct stroke transform
|
||||
let has_real_stroke = instance.instance.style.stroke().filter(|stroke| stroke.weight() > 0.);
|
||||
let has_real_stroke = vector_data.style.stroke().filter(|stroke| stroke.weight() > 0.);
|
||||
let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform.matrix2.determinant() != 0.);
|
||||
let applied_stroke_transform = set_stroke_transform.unwrap_or(*instance.transform);
|
||||
let applied_stroke_transform = render_params.alignment_parent_transform.unwrap_or(applied_stroke_transform);
|
||||
let element_transform = set_stroke_transform.map(|stroke_transform| multiplied_transform * stroke_transform.inverse());
|
||||
let element_transform = element_transform.unwrap_or(DAffine2::IDENTITY);
|
||||
let layer_bounds = instance.instance.bounding_box().unwrap_or_default();
|
||||
let transformed_bounds = instance.instance.bounding_box_with_transform(applied_stroke_transform).unwrap_or_default();
|
||||
let layer_bounds = vector_data.bounding_box().unwrap_or_default();
|
||||
let transformed_bounds = vector_data.bounding_box_with_transform(applied_stroke_transform).unwrap_or_default();
|
||||
|
||||
let mut path = String::new();
|
||||
|
||||
for subpath in instance.instance.stroke_bezier_paths() {
|
||||
let _ = subpath.subpath_to_svg(&mut path, applied_stroke_transform);
|
||||
}
|
||||
|
||||
let connected = vector_data.stroke_bezier_paths().all(|path| path.closed());
|
||||
let can_draw_aligned_stroke = vector_data.style.stroke().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered()) && connected;
|
||||
let mut push_id = None;
|
||||
|
||||
if can_draw_aligned_stroke {
|
||||
let mask_type = if vector_data.style.stroke().unwrap().align == StrokeAlign::Inside {
|
||||
MaskType::Clip
|
||||
} else {
|
||||
MaskType::Mask
|
||||
};
|
||||
|
||||
let can_use_order = !instance.instance.style.fill().is_none() && mask_type == MaskType::Mask;
|
||||
if !can_use_order {
|
||||
let id = format!("alignment-{}", generate_uuid());
|
||||
let mut vector_row = VectorDataTable::default();
|
||||
let mut fill_instance = instance.instance.clone();
|
||||
|
||||
fill_instance.style.clear_stroke();
|
||||
fill_instance.style.set_fill(Fill::solid(Color::BLACK));
|
||||
|
||||
vector_row.push(Instance {
|
||||
instance: fill_instance,
|
||||
alpha_blending: *instance.alpha_blending,
|
||||
transform: *instance.transform,
|
||||
source_node_id: None,
|
||||
});
|
||||
push_id = Some((id, mask_type, vector_row));
|
||||
}
|
||||
}
|
||||
|
||||
render.leaf_tag("path", |attributes| {
|
||||
attributes.push("d", path);
|
||||
let matrix = format_transform_matrix(element_transform);
|
||||
|
@ -511,15 +630,43 @@ impl GraphicElementRendered for VectorDataTable {
|
|||
}
|
||||
|
||||
let defs = &mut attributes.0.svg_defs;
|
||||
if let Some((ref id, mask_type, ref vector_row)) = push_id {
|
||||
let mut svg = SvgRender::new();
|
||||
vector_row.render_svg(&mut svg, &render_params.for_alignment(applied_stroke_transform));
|
||||
|
||||
let fill_and_stroke = instance
|
||||
.instance
|
||||
.style
|
||||
.render(render_params.view_mode, defs, element_transform, applied_stroke_transform, layer_bounds, transformed_bounds);
|
||||
let weight = instance.instance.style.stroke().unwrap().weight * instance.transform.matrix2.determinant();
|
||||
let quad = Quad::from_box(transformed_bounds).inflate(weight);
|
||||
let (x, y) = quad.top_left().into();
|
||||
let (width, height) = (quad.bottom_right() - quad.top_left()).into();
|
||||
write!(defs, r##"{}"##, svg.svg_defs).unwrap();
|
||||
let rect = format!(r##"<rect x="{}" y="{}" width="{width}" height="{height}" fill="white" />"##, x, y);
|
||||
match mask_type {
|
||||
MaskType::Clip => write!(defs, r##"<clipPath id="{id}">{}</clipPath>"##, svg.svg.to_svg_string()).unwrap(),
|
||||
MaskType::Mask => write!(defs, r##"<mask id="{id}">{}{}</mask>"##, rect, svg.svg.to_svg_string()).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
let fill_and_stroke = instance.instance.style.render(
|
||||
defs,
|
||||
element_transform,
|
||||
applied_stroke_transform,
|
||||
layer_bounds,
|
||||
transformed_bounds,
|
||||
can_draw_aligned_stroke,
|
||||
can_draw_aligned_stroke && push_id.is_none(),
|
||||
render_params,
|
||||
);
|
||||
|
||||
if let Some((id, mask_type, _)) = push_id {
|
||||
let selector = format!("url(#{id})");
|
||||
attributes.push(mask_type.to_attribute(), selector);
|
||||
}
|
||||
attributes.push_val(fill_and_stroke);
|
||||
|
||||
if instance.alpha_blending.opacity < 1. {
|
||||
attributes.push("opacity", instance.alpha_blending.opacity.to_string());
|
||||
let factor = if render_params.for_mask { 1. } else { instance.alpha_blending.fill };
|
||||
let opacity = instance.alpha_blending.opacity * factor;
|
||||
if opacity < 1. {
|
||||
attributes.push("opacity", opacity.to_string());
|
||||
}
|
||||
|
||||
if instance.alpha_blending.blend_mode != BlendMode::default() {
|
||||
|
@ -530,9 +677,9 @@ impl GraphicElementRendered for VectorDataTable {
|
|||
}
|
||||
|
||||
#[cfg(feature = "vello")]
|
||||
fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, _: &mut RenderContext, render_params: &RenderParams) {
|
||||
fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) {
|
||||
use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT};
|
||||
use crate::vector::style::{GradientType, LineCap, LineJoin};
|
||||
use crate::vector::style::{GradientType, StrokeCap, StrokeJoin};
|
||||
use vello::kurbo::{Cap, Join};
|
||||
use vello::peniko;
|
||||
|
||||
|
@ -541,6 +688,7 @@ impl GraphicElementRendered for VectorDataTable {
|
|||
let has_real_stroke = instance.instance.style.stroke().filter(|stroke| stroke.weight() > 0.);
|
||||
let set_stroke_transform = has_real_stroke.map(|stroke| stroke.transform).filter(|transform| transform.matrix2.determinant() != 0.);
|
||||
let applied_stroke_transform = set_stroke_transform.unwrap_or(multiplied_transform);
|
||||
let applied_stroke_transform = render_params.alignment_parent_transform.unwrap_or(applied_stroke_transform);
|
||||
let element_transform = set_stroke_transform.map(|stroke_transform| multiplied_transform * stroke_transform.inverse());
|
||||
let element_transform = element_transform.unwrap_or(DAffine2::IDENTITY);
|
||||
let layer_bounds = instance.instance.bounding_box().unwrap_or_default();
|
||||
|
@ -557,16 +705,51 @@ impl GraphicElementRendered for VectorDataTable {
|
|||
_ => instance.alpha_blending.blend_mode.into(),
|
||||
};
|
||||
let mut layer = false;
|
||||
if instance.alpha_blending.opacity < 1. || instance.alpha_blending.blend_mode != BlendMode::default() {
|
||||
let factor = if render_params.for_mask { 1. } else { instance.alpha_blending.fill };
|
||||
let opacity = instance.alpha_blending.opacity * factor;
|
||||
if opacity < 1. || instance.alpha_blending.blend_mode != BlendMode::default() {
|
||||
layer = true;
|
||||
scene.push_layer(
|
||||
peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver),
|
||||
instance.alpha_blending.opacity,
|
||||
opacity,
|
||||
kurbo::Affine::new(multiplied_transform.to_cols_array()),
|
||||
&kurbo::Rect::new(layer_bounds[0].x, layer_bounds[0].y, layer_bounds[1].x, layer_bounds[1].y),
|
||||
);
|
||||
}
|
||||
|
||||
let can_draw_aligned_stroke = instance.instance.style.stroke().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered())
|
||||
&& instance.instance.stroke_bezier_paths().all(|path| path.closed());
|
||||
|
||||
let reorder_for_outside = instance
|
||||
.instance
|
||||
.style
|
||||
.stroke()
|
||||
.is_some_and(|stroke| stroke.align == StrokeAlign::Outside && !instance.instance.style.fill().is_none());
|
||||
if can_draw_aligned_stroke && !reorder_for_outside {
|
||||
let mut vector_data = VectorDataTable::default();
|
||||
|
||||
let mut fill_instance = instance.instance.clone();
|
||||
fill_instance.style.clear_stroke();
|
||||
fill_instance.style.set_fill(Fill::solid(Color::BLACK));
|
||||
|
||||
vector_data.push(Instance {
|
||||
instance: fill_instance,
|
||||
alpha_blending: *instance.alpha_blending,
|
||||
transform: *instance.transform,
|
||||
source_node_id: None,
|
||||
});
|
||||
|
||||
let weight = instance.instance.style.stroke().unwrap().weight;
|
||||
let quad = Quad::from_box(layer_bounds).inflate(weight * element_transform.matrix2.determinant());
|
||||
let rect = vello::kurbo::Rect::new(quad.top_left().x, quad.top_left().y, quad.bottom_right().x, quad.bottom_right().y);
|
||||
|
||||
let inside = instance.instance.style.stroke().unwrap().align == StrokeAlign::Inside;
|
||||
let compose = if inside { peniko::Compose::SrcIn } else { peniko::Compose::SrcOut };
|
||||
scene.push_layer(peniko::Mix::Normal, 1., kurbo::Affine::IDENTITY, &rect);
|
||||
vector_data.render_to_vello(scene, parent_transform, _context, &render_params.for_alignment(applied_stroke_transform));
|
||||
scene.push_layer(peniko::BlendMode::new(peniko::Mix::Clip, compose), 1., kurbo::Affine::IDENTITY, &rect);
|
||||
}
|
||||
|
||||
// Render the path
|
||||
match render_params.view_mode {
|
||||
ViewMode::Outline => {
|
||||
|
@ -589,90 +772,111 @@ impl GraphicElementRendered for VectorDataTable {
|
|||
scene.stroke(&outline_stroke, kurbo::Affine::new(element_transform.to_cols_array()), outline_color, None, &path);
|
||||
}
|
||||
_ => {
|
||||
match instance.instance.style.fill() {
|
||||
Fill::Solid(color) => {
|
||||
let fill = peniko::Brush::Solid(peniko::Color::new([color.r(), color.g(), color.b(), color.a()]));
|
||||
scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, &path);
|
||||
}
|
||||
Fill::Gradient(gradient) => {
|
||||
let mut stops = peniko::ColorStops::new();
|
||||
for &(offset, color) in &gradient.stops {
|
||||
stops.push(peniko::ColorStop {
|
||||
offset: offset as f32,
|
||||
color: peniko::color::DynamicColor::from_alpha_color(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])),
|
||||
});
|
||||
}
|
||||
// Compute bounding box of the shape to determine the gradient start and end points
|
||||
let bounds = instance.instance.nonzero_bounding_box();
|
||||
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
enum Op {
|
||||
Fill,
|
||||
Stroke,
|
||||
}
|
||||
|
||||
let inverse_parent_transform = (parent_transform.matrix2.determinant() != 0.).then(|| parent_transform.inverse()).unwrap_or_default();
|
||||
let mod_points = inverse_parent_transform * multiplied_transform * bound_transform;
|
||||
|
||||
let start = mod_points.transform_point2(gradient.start);
|
||||
let end = mod_points.transform_point2(gradient.end);
|
||||
|
||||
let fill = peniko::Brush::Gradient(peniko::Gradient {
|
||||
kind: match gradient.gradient_type {
|
||||
GradientType::Linear => peniko::GradientKind::Linear {
|
||||
start: to_point(start),
|
||||
end: to_point(end),
|
||||
},
|
||||
GradientType::Radial => {
|
||||
let radius = start.distance(end);
|
||||
peniko::GradientKind::Radial {
|
||||
start_center: to_point(start),
|
||||
start_radius: 0.,
|
||||
end_center: to_point(start),
|
||||
end_radius: radius as f32,
|
||||
}
|
||||
}
|
||||
},
|
||||
stops,
|
||||
..Default::default()
|
||||
});
|
||||
// Vello does `element_transform * brush_transform` internally. We don't want element_transform to have any impact so we need to left multiply by the inverse.
|
||||
// This makes the final internal brush transform equal to `parent_transform`, allowing you to stretch a gradient by transforming the parent folder.
|
||||
let inverse_element_transform = (element_transform.matrix2.determinant() != 0.).then(|| element_transform.inverse()).unwrap_or_default();
|
||||
let brush_transform = kurbo::Affine::new((inverse_element_transform * parent_transform).to_cols_array());
|
||||
scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, Some(brush_transform), &path);
|
||||
}
|
||||
Fill::None => {}
|
||||
let order = match instance.instance.style.stroke().is_some_and(|stroke| !stroke.paint_order.is_default()) || reorder_for_outside {
|
||||
true => [Op::Stroke, Op::Fill],
|
||||
false => [Op::Fill, Op::Stroke], // Default
|
||||
};
|
||||
for operation in order {
|
||||
match operation {
|
||||
Op::Fill => {
|
||||
match instance.instance.style.fill() {
|
||||
Fill::Solid(color) => {
|
||||
let fill = peniko::Brush::Solid(peniko::Color::new([color.r(), color.g(), color.b(), color.a()]));
|
||||
scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, None, &path);
|
||||
}
|
||||
Fill::Gradient(gradient) => {
|
||||
let mut stops = peniko::ColorStops::new();
|
||||
for &(offset, color) in &gradient.stops {
|
||||
stops.push(peniko::ColorStop {
|
||||
offset: offset as f32,
|
||||
color: peniko::color::DynamicColor::from_alpha_color(peniko::Color::new([color.r(), color.g(), color.b(), color.a()])),
|
||||
});
|
||||
}
|
||||
// Compute bounding box of the shape to determine the gradient start and end points
|
||||
let bounds = instance.instance.nonzero_bounding_box();
|
||||
let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]);
|
||||
|
||||
if let Some(stroke) = instance.instance.style.stroke() {
|
||||
let color = match stroke.color {
|
||||
Some(color) => peniko::Color::new([color.r(), color.g(), color.b(), color.a()]),
|
||||
None => peniko::Color::TRANSPARENT,
|
||||
};
|
||||
let cap = match stroke.line_cap {
|
||||
LineCap::Butt => Cap::Butt,
|
||||
LineCap::Round => Cap::Round,
|
||||
LineCap::Square => Cap::Square,
|
||||
};
|
||||
let join = match stroke.line_join {
|
||||
LineJoin::Miter => Join::Miter,
|
||||
LineJoin::Bevel => Join::Bevel,
|
||||
LineJoin::Round => Join::Round,
|
||||
};
|
||||
let stroke = kurbo::Stroke {
|
||||
width: stroke.weight,
|
||||
miter_limit: stroke.line_join_miter_limit,
|
||||
join,
|
||||
start_cap: cap,
|
||||
end_cap: cap,
|
||||
dash_pattern: stroke.dash_lengths.into(),
|
||||
dash_offset: stroke.dash_offset,
|
||||
};
|
||||
let inverse_parent_transform = (parent_transform.matrix2.determinant() != 0.).then(|| parent_transform.inverse()).unwrap_or_default();
|
||||
let mod_points = inverse_parent_transform * multiplied_transform * bound_transform;
|
||||
|
||||
// Draw the stroke if it's visible
|
||||
if stroke.width > 0. {
|
||||
scene.stroke(&stroke, kurbo::Affine::new(element_transform.to_cols_array()), color, None, &path);
|
||||
let start = mod_points.transform_point2(gradient.start);
|
||||
let end = mod_points.transform_point2(gradient.end);
|
||||
|
||||
let fill = peniko::Brush::Gradient(peniko::Gradient {
|
||||
kind: match gradient.gradient_type {
|
||||
GradientType::Linear => peniko::GradientKind::Linear {
|
||||
start: to_point(start),
|
||||
end: to_point(end),
|
||||
},
|
||||
GradientType::Radial => {
|
||||
let radius = start.distance(end);
|
||||
peniko::GradientKind::Radial {
|
||||
start_center: to_point(start),
|
||||
start_radius: 0.,
|
||||
end_center: to_point(start),
|
||||
end_radius: radius as f32,
|
||||
}
|
||||
}
|
||||
},
|
||||
stops,
|
||||
..Default::default()
|
||||
});
|
||||
// Vello does `element_transform * brush_transform` internally. We don't want element_transform to have any impact so we need to left multiply by the inverse.
|
||||
// This makes the final internal brush transform equal to `parent_transform`, allowing you to stretch a gradient by transforming the parent folder.
|
||||
let inverse_element_transform = (element_transform.matrix2.determinant() != 0.).then(|| element_transform.inverse()).unwrap_or_default();
|
||||
let brush_transform = kurbo::Affine::new((inverse_element_transform * parent_transform).to_cols_array());
|
||||
scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(element_transform.to_cols_array()), &fill, Some(brush_transform), &path);
|
||||
}
|
||||
Fill::None => {}
|
||||
};
|
||||
}
|
||||
Op::Stroke => {
|
||||
if let Some(stroke) = instance.instance.style.stroke() {
|
||||
let color = match stroke.color {
|
||||
Some(color) => peniko::Color::new([color.r(), color.g(), color.b(), color.a()]),
|
||||
None => peniko::Color::TRANSPARENT,
|
||||
};
|
||||
let cap = match stroke.cap {
|
||||
StrokeCap::Butt => Cap::Butt,
|
||||
StrokeCap::Round => Cap::Round,
|
||||
StrokeCap::Square => Cap::Square,
|
||||
};
|
||||
let join = match stroke.join {
|
||||
StrokeJoin::Miter => Join::Miter,
|
||||
StrokeJoin::Bevel => Join::Bevel,
|
||||
StrokeJoin::Round => Join::Round,
|
||||
};
|
||||
let stroke = kurbo::Stroke {
|
||||
width: stroke.weight * if can_draw_aligned_stroke { 2. } else { 1. },
|
||||
miter_limit: stroke.join_miter_limit,
|
||||
join,
|
||||
start_cap: cap,
|
||||
end_cap: cap,
|
||||
dash_pattern: stroke.dash_lengths.into(),
|
||||
dash_offset: stroke.dash_offset,
|
||||
};
|
||||
|
||||
// Draw the stroke if it's visible
|
||||
if stroke.width > 0. {
|
||||
scene.stroke(&stroke, kurbo::Affine::new(element_transform.to_cols_array()), color, None, &path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if can_draw_aligned_stroke {
|
||||
scene.pop_layer();
|
||||
scene.pop_layer();
|
||||
}
|
||||
|
||||
// If we pushed a layer for opacity or a blend mode, we need to pop it
|
||||
if layer {
|
||||
scene.pop_layer();
|
||||
|
@ -689,11 +893,11 @@ impl GraphicElementRendered for VectorDataTable {
|
|||
|
||||
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.);
|
||||
let miter_limit = instance.instance.style.stroke().map(|s| s.join_miter_limit).unwrap_or(1.);
|
||||
|
||||
let scale = transform.decompose_scale();
|
||||
|
||||
// We use the full line width here to account for different styles of line caps
|
||||
// We use the full line width here to account for different styles of stroke caps
|
||||
let offset = DVec2::splat(stroke_width * scale.x.max(scale.y) * miter_limit);
|
||||
|
||||
instance.instance.bounding_box_with_transform(transform * *instance.transform).map(|[a, b]| [a - offset, b + offset])
|
||||
|
@ -844,13 +1048,13 @@ impl GraphicElementRendered for Artboard {
|
|||
let color = peniko::Color::new([self.background.r(), self.background.g(), self.background.b(), self.background.a()]);
|
||||
let [a, b] = [self.location.as_dvec2(), self.location.as_dvec2() + self.dimensions.as_dvec2()];
|
||||
let rect = kurbo::Rect::new(a.x.min(b.x), a.y.min(b.y), a.x.max(b.x), a.y.max(b.y));
|
||||
let blend_mode = peniko::BlendMode::new(peniko::Mix::Clip, peniko::Compose::SrcOver);
|
||||
|
||||
scene.push_layer(peniko::Mix::Normal, 1., kurbo::Affine::new(transform.to_cols_array()), &rect);
|
||||
scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(transform.to_cols_array()), color, None, &rect);
|
||||
scene.pop_layer();
|
||||
|
||||
if self.clip {
|
||||
let blend_mode = peniko::BlendMode::new(peniko::Mix::Clip, peniko::Compose::SrcOver);
|
||||
scene.push_layer(blend_mode, 1., kurbo::Affine::new(transform.to_cols_array()), &rect);
|
||||
}
|
||||
// Since the graphic group's transform is right multiplied in when rendering the graphic group, we just need to right multiply by the offset here.
|
||||
|
@ -935,9 +1139,9 @@ impl GraphicElementRendered for ArtboardGroupTable {
|
|||
}
|
||||
|
||||
impl GraphicElementRendered for RasterDataTable<CPU> {
|
||||
fn render_svg(&self, render: &mut SvgRender, _render_params: &RenderParams) {
|
||||
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
||||
for instance in self.instance_ref_iter() {
|
||||
let transform = *instance.transform * render.transform;
|
||||
let transform = *instance.transform;
|
||||
|
||||
let image = &instance.instance;
|
||||
if image.data.is_empty() {
|
||||
|
@ -961,8 +1165,10 @@ impl GraphicElementRendered for RasterDataTable<CPU> {
|
|||
if !matrix.is_empty() {
|
||||
attributes.push("transform", matrix);
|
||||
}
|
||||
if instance.alpha_blending.opacity < 1. {
|
||||
attributes.push("opacity", instance.alpha_blending.opacity.to_string());
|
||||
let factor = if render_params.for_mask { 1. } else { instance.alpha_blending.fill };
|
||||
let opacity = instance.alpha_blending.opacity * factor;
|
||||
if opacity < 1. {
|
||||
attributes.push("opacity", opacity.to_string());
|
||||
}
|
||||
if instance.alpha_blending.blend_mode != BlendMode::default() {
|
||||
attributes.push("style", instance.alpha_blending.blend_mode.render());
|
||||
|
|
|
@ -318,6 +318,32 @@ impl SetBlendMode for RasterDataTable<CPU> {
|
|||
}
|
||||
}
|
||||
|
||||
trait SetClip {
|
||||
fn set_clip(&mut self, clip: bool);
|
||||
}
|
||||
|
||||
impl SetClip for VectorDataTable {
|
||||
fn set_clip(&mut self, clip: bool) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.clip = clip;
|
||||
}
|
||||
}
|
||||
}
|
||||
impl SetClip for GraphicGroupTable {
|
||||
fn set_clip(&mut self, clip: bool) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.clip = clip;
|
||||
}
|
||||
}
|
||||
}
|
||||
impl SetClip for RasterDataTable<CPU> {
|
||||
fn set_clip(&mut self, clip: bool) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.clip = clip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Style"))]
|
||||
fn blend_mode<T: SetBlendMode>(
|
||||
_: impl Ctx,
|
||||
|
@ -343,9 +369,31 @@ fn opacity<T: MultiplyAlpha>(
|
|||
RasterDataTable<CPU>,
|
||||
)]
|
||||
mut value: T,
|
||||
#[default(100.)] factor: Percentage,
|
||||
#[default(100.)] opacity: Percentage,
|
||||
) -> T {
|
||||
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
|
||||
value.multiply_alpha(factor / 100.);
|
||||
value.multiply_alpha(opacity / 100.);
|
||||
value
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Style"))]
|
||||
fn blending<T: SetBlendMode + MultiplyAlpha + MultiplyFill + SetClip>(
|
||||
_: impl Ctx,
|
||||
#[implementations(
|
||||
GraphicGroupTable,
|
||||
VectorDataTable,
|
||||
RasterDataTable<CPU>,
|
||||
)]
|
||||
mut value: T,
|
||||
blend_mode: BlendMode,
|
||||
#[default(100.)] opacity: Percentage,
|
||||
#[default(100.)] fill: Percentage,
|
||||
#[default(false)] clip: bool,
|
||||
) -> T {
|
||||
// TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance<T>) rather than applying to each row in its own table, which produces the undesired result
|
||||
value.set_blend_mode(blend_mode);
|
||||
value.multiply_alpha(opacity / 100.);
|
||||
value.multiply_fill(fill / 100.);
|
||||
value.set_clip(clip);
|
||||
value
|
||||
}
|
||||
|
|
|
@ -1321,6 +1321,36 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) trait MultiplyFill {
|
||||
fn multiply_fill(&mut self, factor: f64);
|
||||
}
|
||||
impl MultiplyFill for Color {
|
||||
fn multiply_fill(&mut self, factor: f64) {
|
||||
*self = Color::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), (self.a() * factor as f32).clamp(0., 1.))
|
||||
}
|
||||
}
|
||||
impl MultiplyFill for VectorDataTable {
|
||||
fn multiply_fill(&mut self, factor: f64) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.fill *= factor as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
impl MultiplyFill for GraphicGroupTable {
|
||||
fn multiply_fill(&mut self, factor: f64) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.fill *= factor as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
impl MultiplyFill for RasterDataTable<CPU> {
|
||||
fn multiply_fill(&mut self, factor: f64) {
|
||||
for instance in self.instance_mut_iter() {
|
||||
instance.alpha_blending.fill *= factor as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Aims for interoperable compatibility with:
|
||||
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=nvrt%27%20%3D%20Invert-,%27post%27%20%3D%20Posterize,-%27thrs%27%20%3D%20Threshold
|
||||
//
|
||||
|
|
|
@ -514,6 +514,11 @@ impl Color {
|
|||
self.alpha
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_opaque(&self) -> bool {
|
||||
self.alpha > 1. - f32::EPSILON
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn average_rgb_channels(&self) -> f32 {
|
||||
(self.red + self.green + self.blue) / 3.
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
use crate::Color;
|
||||
use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT};
|
||||
use crate::renderer::format_transform_matrix;
|
||||
use crate::renderer::{RenderParams, format_transform_matrix};
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use std::fmt::Write;
|
||||
|
@ -214,7 +214,7 @@ 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 {
|
||||
fn render_defs(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2], _render_params: &RenderParams) -> 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]);
|
||||
|
@ -381,7 +381,7 @@ impl Fill {
|
|||
}
|
||||
|
||||
/// Renders the fill, adding necessary defs through mutating the first argument.
|
||||
pub fn render(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> String {
|
||||
pub fn render(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2], render_params: &RenderParams) -> String {
|
||||
match self {
|
||||
Self::None => r#" fill="none""#.to_string(),
|
||||
Self::Solid(color) => {
|
||||
|
@ -392,7 +392,7 @@ impl Fill {
|
|||
result
|
||||
}
|
||||
Self::Gradient(gradient) => {
|
||||
let gradient_id = gradient.render_defs(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds);
|
||||
let gradient_id = gradient.render_defs(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds, render_params);
|
||||
format!(r##" fill="url('#{gradient_id}')""##)
|
||||
}
|
||||
}
|
||||
|
@ -413,6 +413,20 @@ impl Fill {
|
|||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Find if fill can be represented with only opaque colors
|
||||
pub fn is_opaque(&self) -> bool {
|
||||
match self {
|
||||
Fill::Solid(color) => color.is_opaque(),
|
||||
Fill::Gradient(gradient) => gradient.stops.iter().all(|(_, color)| color.is_opaque()),
|
||||
Fill::None => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns if fill is none
|
||||
pub fn is_none(&self) -> bool {
|
||||
*self == Self::None
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for Fill {
|
||||
|
@ -499,19 +513,19 @@ pub enum FillType {
|
|||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum LineCap {
|
||||
pub enum StrokeCap {
|
||||
#[default]
|
||||
Butt,
|
||||
Round,
|
||||
Square,
|
||||
}
|
||||
|
||||
impl LineCap {
|
||||
impl StrokeCap {
|
||||
fn svg_name(&self) -> &'static str {
|
||||
match self {
|
||||
LineCap::Butt => "butt",
|
||||
LineCap::Round => "round",
|
||||
LineCap::Square => "square",
|
||||
StrokeCap::Butt => "butt",
|
||||
StrokeCap::Round => "round",
|
||||
StrokeCap::Square => "square",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -519,29 +533,61 @@ impl LineCap {
|
|||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum LineJoin {
|
||||
pub enum StrokeJoin {
|
||||
#[default]
|
||||
Miter,
|
||||
Bevel,
|
||||
Round,
|
||||
}
|
||||
|
||||
impl LineJoin {
|
||||
impl StrokeJoin {
|
||||
fn svg_name(&self) -> &'static str {
|
||||
match self {
|
||||
LineJoin::Bevel => "bevel",
|
||||
LineJoin::Miter => "miter",
|
||||
LineJoin::Round => "round",
|
||||
StrokeJoin::Bevel => "bevel",
|
||||
StrokeJoin::Miter => "miter",
|
||||
StrokeJoin::Round => "round",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum StrokeAlign {
|
||||
#[default]
|
||||
Center,
|
||||
Inside,
|
||||
Outside,
|
||||
}
|
||||
|
||||
impl StrokeAlign {
|
||||
pub fn is_not_centered(self) -> bool {
|
||||
self != Self::Center
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||
#[widget(Radio)]
|
||||
pub enum PaintOrder {
|
||||
#[default]
|
||||
StrokeAbove,
|
||||
StrokeBelow,
|
||||
}
|
||||
|
||||
impl PaintOrder {
|
||||
pub fn is_default(self) -> bool {
|
||||
self == Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn daffine2_identity() -> DAffine2 {
|
||||
DAffine2::IDENTITY
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
|
||||
#[serde(default)]
|
||||
pub struct Stroke {
|
||||
/// Stroke color
|
||||
pub color: Option<Color>,
|
||||
|
@ -549,26 +595,38 @@ pub struct Stroke {
|
|||
pub weight: f64,
|
||||
pub dash_lengths: Vec<f64>,
|
||||
pub dash_offset: f64,
|
||||
pub line_cap: LineCap,
|
||||
pub line_join: LineJoin,
|
||||
pub line_join_miter_limit: f64,
|
||||
#[serde(alias = "line_cap")]
|
||||
pub cap: StrokeCap,
|
||||
#[serde(alias = "line_join")]
|
||||
pub join: StrokeJoin,
|
||||
#[serde(alias = "line_join_miter_limit")]
|
||||
pub join_miter_limit: f64,
|
||||
#[serde(default)]
|
||||
pub align: StrokeAlign,
|
||||
#[serde(default = "daffine2_identity")]
|
||||
pub transform: DAffine2,
|
||||
#[serde(default)]
|
||||
pub non_scaling: bool,
|
||||
#[serde(default)]
|
||||
pub paint_order: PaintOrder,
|
||||
}
|
||||
|
||||
impl core::hash::Hash for Stroke {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
self.color.hash(state);
|
||||
self.weight.to_bits().hash(state);
|
||||
self.dash_lengths.len().hash(state);
|
||||
self.dash_lengths.iter().for_each(|length| length.to_bits().hash(state));
|
||||
{
|
||||
self.dash_lengths.len().hash(state);
|
||||
self.dash_lengths.iter().for_each(|length| length.to_bits().hash(state));
|
||||
}
|
||||
self.dash_offset.to_bits().hash(state);
|
||||
self.line_cap.hash(state);
|
||||
self.line_join.hash(state);
|
||||
self.line_join_miter_limit.to_bits().hash(state);
|
||||
self.cap.hash(state);
|
||||
self.join.hash(state);
|
||||
self.join_miter_limit.to_bits().hash(state);
|
||||
self.align.hash(state);
|
||||
self.transform.to_cols_array().iter().for_each(|x| x.to_bits().hash(state));
|
||||
self.non_scaling.hash(state);
|
||||
self.paint_order.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -590,11 +648,13 @@ impl Stroke {
|
|||
weight,
|
||||
dash_lengths: Vec::new(),
|
||||
dash_offset: 0.,
|
||||
line_cap: LineCap::Butt,
|
||||
line_join: LineJoin::Miter,
|
||||
line_join_miter_limit: 4.,
|
||||
cap: StrokeCap::Butt,
|
||||
join: StrokeJoin::Miter,
|
||||
join_miter_limit: 4.,
|
||||
align: StrokeAlign::Center,
|
||||
transform: DAffine2::IDENTITY,
|
||||
non_scaling: false,
|
||||
paint_order: PaintOrder::StrokeAbove,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -604,14 +664,16 @@ impl Stroke {
|
|||
weight: self.weight + (other.weight - self.weight) * time,
|
||||
dash_lengths: self.dash_lengths.iter().zip(other.dash_lengths.iter()).map(|(a, b)| a + (b - a) * time).collect(),
|
||||
dash_offset: self.dash_offset + (other.dash_offset - self.dash_offset) * time,
|
||||
line_cap: if time < 0.5 { self.line_cap } else { other.line_cap },
|
||||
line_join: if time < 0.5 { self.line_join } else { other.line_join },
|
||||
line_join_miter_limit: self.line_join_miter_limit + (other.line_join_miter_limit - self.line_join_miter_limit) * time,
|
||||
cap: if time < 0.5 { self.cap } else { other.cap },
|
||||
join: if time < 0.5 { self.join } else { other.join },
|
||||
join_miter_limit: self.join_miter_limit + (other.join_miter_limit - self.join_miter_limit) * time,
|
||||
align: if time < 0.5 { self.align } else { other.align },
|
||||
transform: DAffine2::from_mat2_translation(
|
||||
time * self.transform.matrix2 + (1. - time) * other.transform.matrix2,
|
||||
self.transform.translation * time + other.transform.translation * (1. - time),
|
||||
),
|
||||
non_scaling: if time < 0.5 { self.non_scaling } else { other.non_scaling },
|
||||
paint_order: if time < 0.5 { self.paint_order } else { other.paint_order },
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -637,23 +699,23 @@ impl Stroke {
|
|||
self.dash_offset
|
||||
}
|
||||
|
||||
pub fn line_cap_index(&self) -> u32 {
|
||||
self.line_cap as u32
|
||||
pub fn cap_index(&self) -> u32 {
|
||||
self.cap as u32
|
||||
}
|
||||
|
||||
pub fn line_join_index(&self) -> u32 {
|
||||
self.line_join as u32
|
||||
pub fn join_index(&self) -> u32 {
|
||||
self.join as u32
|
||||
}
|
||||
|
||||
pub fn line_join_miter_limit(&self) -> f32 {
|
||||
self.line_join_miter_limit as f32
|
||||
pub fn join_miter_limit(&self) -> f32 {
|
||||
self.join_miter_limit as f32
|
||||
}
|
||||
|
||||
/// Provide the SVG attributes for the stroke.
|
||||
pub fn render(&self) -> String {
|
||||
pub fn render(&self, aligned_strokes: bool, override_paint_order: bool, _render_params: &RenderParams) -> String {
|
||||
// Don't render a stroke at all if it would be invisible
|
||||
let Some(color) = self.color else { return String::new() };
|
||||
if self.weight <= 0. || color.a() == 0. {
|
||||
if !self.has_renderable_stroke() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
|
@ -661,16 +723,21 @@ impl Stroke {
|
|||
let weight = (self.weight != 1.).then_some(self.weight);
|
||||
let dash_array = (!self.dash_lengths.is_empty()).then_some(self.dash_lengths());
|
||||
let dash_offset = (self.dash_offset != 0.).then_some(self.dash_offset);
|
||||
let line_cap = (self.line_cap != LineCap::Butt).then_some(self.line_cap);
|
||||
let line_join = (self.line_join != LineJoin::Miter).then_some(self.line_join);
|
||||
let line_join_miter_limit = (self.line_join_miter_limit != 4.).then_some(self.line_join_miter_limit);
|
||||
let stroke_cap = (self.cap != StrokeCap::Butt).then_some(self.cap);
|
||||
let stroke_join = (self.join != StrokeJoin::Miter).then_some(self.join);
|
||||
let stroke_join_miter_limit = (self.join_miter_limit != 4.).then_some(self.join_miter_limit);
|
||||
let stroke_align = (self.align != StrokeAlign::Center).then_some(self.align);
|
||||
let paint_order = (self.paint_order != PaintOrder::StrokeAbove || override_paint_order).then_some(PaintOrder::StrokeBelow);
|
||||
|
||||
// Render the needed stroke attributes
|
||||
let mut attributes = format!(r##" stroke="#{}""##, color.to_rgb_hex_srgb_from_gamma());
|
||||
if color.a() < 1. {
|
||||
let _ = write!(&mut attributes, r#" stroke-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
|
||||
}
|
||||
if let Some(weight) = weight {
|
||||
if let Some(mut weight) = weight {
|
||||
if stroke_align.is_some() && aligned_strokes {
|
||||
weight *= 2.;
|
||||
}
|
||||
let _ = write!(&mut attributes, r#" stroke-width="{}""#, weight);
|
||||
}
|
||||
if let Some(dash_array) = dash_array {
|
||||
|
@ -679,19 +746,22 @@ impl Stroke {
|
|||
if let Some(dash_offset) = dash_offset {
|
||||
let _ = write!(&mut attributes, r#" stroke-dashoffset="{}""#, dash_offset);
|
||||
}
|
||||
if let Some(line_cap) = line_cap {
|
||||
let _ = write!(&mut attributes, r#" stroke-linecap="{}""#, line_cap.svg_name());
|
||||
if let Some(stroke_cap) = stroke_cap {
|
||||
let _ = write!(&mut attributes, r#" stroke-linecap="{}""#, stroke_cap.svg_name());
|
||||
}
|
||||
if let Some(line_join) = line_join {
|
||||
let _ = write!(&mut attributes, r#" stroke-linejoin="{}""#, line_join.svg_name());
|
||||
if let Some(stroke_join) = stroke_join {
|
||||
let _ = write!(&mut attributes, r#" stroke-linejoin="{}""#, stroke_join.svg_name());
|
||||
}
|
||||
if let Some(line_join_miter_limit) = line_join_miter_limit {
|
||||
let _ = write!(&mut attributes, r#" stroke-miterlimit="{}""#, line_join_miter_limit);
|
||||
if let Some(stroke_join_miter_limit) = stroke_join_miter_limit {
|
||||
let _ = write!(&mut attributes, r#" stroke-miterlimit="{}""#, stroke_join_miter_limit);
|
||||
}
|
||||
// Add vector-effect attribute to make strokes non-scaling
|
||||
if self.non_scaling {
|
||||
let _ = write!(&mut attributes, r#" vector-effect="non-scaling-stroke""#);
|
||||
}
|
||||
if paint_order.is_some() {
|
||||
let _ = write!(&mut attributes, r#" style="paint-order: stroke;" "#);
|
||||
}
|
||||
attributes
|
||||
}
|
||||
|
||||
|
@ -724,18 +794,23 @@ impl Stroke {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn with_line_cap(mut self, line_cap: LineCap) -> Self {
|
||||
self.line_cap = line_cap;
|
||||
pub fn with_stroke_cap(mut self, stroke_cap: StrokeCap) -> Self {
|
||||
self.cap = stroke_cap;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_line_join(mut self, line_join: LineJoin) -> Self {
|
||||
self.line_join = line_join;
|
||||
pub fn with_stroke_join(mut self, stroke_join: StrokeJoin) -> Self {
|
||||
self.join = stroke_join;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_line_join_miter_limit(mut self, limit: f64) -> Self {
|
||||
self.line_join_miter_limit = limit;
|
||||
pub fn with_stroke_join_miter_limit(mut self, limit: f64) -> Self {
|
||||
self.join_miter_limit = limit;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_stroke_align(mut self, stroke_align: StrokeAlign) -> Self {
|
||||
self.align = stroke_align;
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -743,6 +818,10 @@ impl Stroke {
|
|||
self.non_scaling = non_scaling;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn has_renderable_stroke(&self) -> bool {
|
||||
self.weight > 0. && self.color.is_some_and(|color| color.a() != 0.)
|
||||
}
|
||||
}
|
||||
|
||||
// Having an alpha of 1 to start with leads to a better experience with the properties panel
|
||||
|
@ -753,11 +832,13 @@ impl Default for Stroke {
|
|||
color: Some(Color::from_rgba8_srgb(0, 0, 0, 255)),
|
||||
dash_lengths: Vec::new(),
|
||||
dash_offset: 0.,
|
||||
line_cap: LineCap::Butt,
|
||||
line_join: LineJoin::Miter,
|
||||
line_join_miter_limit: 4.,
|
||||
cap: StrokeCap::Butt,
|
||||
join: StrokeJoin::Miter,
|
||||
join_miter_limit: 4.,
|
||||
align: StrokeAlign::Center,
|
||||
transform: DAffine2::IDENTITY,
|
||||
non_scaling: false,
|
||||
paint_order: PaintOrder::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -929,19 +1010,35 @@ impl PathStyle {
|
|||
}
|
||||
|
||||
/// Renders the shape's fill and stroke attributes as a string with them concatenated together.
|
||||
pub fn render(&self, view_mode: ViewMode, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> String {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn render(
|
||||
&self,
|
||||
svg_defs: &mut String,
|
||||
element_transform: DAffine2,
|
||||
stroke_transform: DAffine2,
|
||||
bounds: [DVec2; 2],
|
||||
transformed_bounds: [DVec2; 2],
|
||||
aligned_strokes: bool,
|
||||
override_paint_order: bool,
|
||||
render_params: &RenderParams,
|
||||
) -> String {
|
||||
let view_mode = render_params.view_mode;
|
||||
match view_mode {
|
||||
ViewMode::Outline => {
|
||||
let fill_attribute = Fill::None.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds);
|
||||
let fill_attribute = Fill::None.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds, render_params);
|
||||
let mut outline_stroke = Stroke::new(Some(LAYER_OUTLINE_STROKE_COLOR), LAYER_OUTLINE_STROKE_WEIGHT);
|
||||
// Outline strokes should be non-scaling by default
|
||||
outline_stroke.non_scaling = true;
|
||||
let stroke_attribute = outline_stroke.render();
|
||||
let stroke_attribute = outline_stroke.render(aligned_strokes, override_paint_order, render_params);
|
||||
format!("{fill_attribute}{stroke_attribute}")
|
||||
}
|
||||
_ => {
|
||||
let fill_attribute = self.fill.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds);
|
||||
let stroke_attribute = self.stroke.as_ref().map(|stroke| stroke.render()).unwrap_or_default();
|
||||
let fill_attribute = self.fill.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds, render_params);
|
||||
let stroke_attribute = self
|
||||
.stroke
|
||||
.as_ref()
|
||||
.map(|stroke| stroke.render(aligned_strokes, override_paint_order, render_params))
|
||||
.unwrap_or_default();
|
||||
format!("{fill_attribute}{stroke_attribute}")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ use crate::registry::types::{Angle, Fraction, IntegerCount, Length, Multiplier,
|
|||
use crate::renderer::GraphicElementRendered;
|
||||
use crate::transform::{Footprint, ReferencePoint, Transform};
|
||||
use crate::vector::misc::dvec2_to_point;
|
||||
use crate::vector::style::{LineCap, LineJoin};
|
||||
use crate::vector::style::{PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
||||
use crate::vector::{FillId, PointDomain, RegionId};
|
||||
use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroupTable, OwnedContextImpl};
|
||||
use bezier_rs::{Join, ManipulatorGroup, Subpath};
|
||||
|
@ -167,17 +167,22 @@ async fn stroke<C: Into<Option<Color>> + 'n + Send, V>(
|
|||
#[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 alignment of stroke to the path's centerline or (for closed shapes) the inside or outside of the shape.
|
||||
align: StrokeAlign,
|
||||
/// The shape of the stroke at open endpoints.
|
||||
line_cap: crate::vector::style::LineCap,
|
||||
cap: StrokeCap,
|
||||
/// The curvature of the bent stroke at sharp corners.
|
||||
line_join: LineJoin,
|
||||
join: StrokeJoin,
|
||||
#[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,
|
||||
/// The order to paint the stroke on top of the fill, or the fill on top of the stroke.
|
||||
/// <https://svgwg.org/svg2-draft/painting.html#PaintOrderProperty>
|
||||
paint_order: PaintOrder,
|
||||
/// 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 phase offset distance from the starting point of the dash pattern.
|
||||
dash_offset: f64,
|
||||
) -> Instances<V>
|
||||
where
|
||||
Instances<V>: VectorDataTableIterMut + 'n + Send,
|
||||
|
@ -187,12 +192,15 @@ where
|
|||
weight,
|
||||
dash_lengths,
|
||||
dash_offset,
|
||||
line_cap,
|
||||
line_join,
|
||||
line_join_miter_limit: miter_limit,
|
||||
cap,
|
||||
join,
|
||||
join_miter_limit: miter_limit,
|
||||
align,
|
||||
transform: DAffine2::IDENTITY,
|
||||
non_scaling: false,
|
||||
paint_order,
|
||||
};
|
||||
|
||||
for vector in vector_data.vector_iter_mut() {
|
||||
let mut stroke = stroke.clone();
|
||||
stroke.transform *= *vector.transform;
|
||||
|
@ -1084,7 +1092,7 @@ async fn points_to_polyline(_: impl Ctx, mut points: VectorDataTable, #[default(
|
|||
}
|
||||
|
||||
#[node_macro::node(category("Vector"), path(graphene_core::vector), properties("offset_path_properties"))]
|
||||
async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, line_join: LineJoin, #[default(4.)] miter_limit: f64) -> VectorDataTable {
|
||||
async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, join: StrokeJoin, #[default(4.)] miter_limit: f64) -> VectorDataTable {
|
||||
let mut result_table = VectorDataTable::default();
|
||||
|
||||
for mut vector_data_instance in vector_data.instance_iter() {
|
||||
|
@ -1106,10 +1114,10 @@ async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, l
|
|||
let mut subpath_out = offset_subpath(
|
||||
&subpath,
|
||||
-distance,
|
||||
match line_join {
|
||||
LineJoin::Miter => Join::Miter(Some(miter_limit)),
|
||||
LineJoin::Bevel => Join::Bevel,
|
||||
LineJoin::Round => Join::Round,
|
||||
match join {
|
||||
StrokeJoin::Miter => Join::Miter(Some(miter_limit)),
|
||||
StrokeJoin::Bevel => Join::Bevel,
|
||||
StrokeJoin::Round => Join::Round,
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -1139,19 +1147,19 @@ async fn solidify_stroke(_: impl Ctx, vector_data: VectorDataTable) -> VectorDat
|
|||
let mut result = VectorData::default();
|
||||
|
||||
// Taking the existing stroke data and passing it to kurbo::stroke to generate new fill paths.
|
||||
let join = match stroke.line_join {
|
||||
LineJoin::Miter => kurbo::Join::Miter,
|
||||
LineJoin::Bevel => kurbo::Join::Bevel,
|
||||
LineJoin::Round => kurbo::Join::Round,
|
||||
let join = match stroke.join {
|
||||
StrokeJoin::Miter => kurbo::Join::Miter,
|
||||
StrokeJoin::Bevel => kurbo::Join::Bevel,
|
||||
StrokeJoin::Round => kurbo::Join::Round,
|
||||
};
|
||||
let cap = match stroke.line_cap {
|
||||
LineCap::Butt => kurbo::Cap::Butt,
|
||||
LineCap::Round => kurbo::Cap::Round,
|
||||
LineCap::Square => kurbo::Cap::Square,
|
||||
let cap = match stroke.cap {
|
||||
StrokeCap::Butt => kurbo::Cap::Butt,
|
||||
StrokeCap::Round => kurbo::Cap::Round,
|
||||
StrokeCap::Square => kurbo::Cap::Square,
|
||||
};
|
||||
let dash_offset = stroke.dash_offset;
|
||||
let dash_pattern = stroke.dash_lengths;
|
||||
let miter_limit = stroke.line_join_miter_limit;
|
||||
let miter_limit = stroke.join_miter_limit;
|
||||
|
||||
let stroke_style = kurbo::Stroke::new(stroke.weight)
|
||||
.with_caps(cap)
|
||||
|
|
|
@ -234,8 +234,12 @@ tagged_value! {
|
|||
SelectiveColorChoice(graphene_core::raster::SelectiveColorChoice),
|
||||
GridType(graphene_core::vector::misc::GridType),
|
||||
ArcType(graphene_core::vector::misc::ArcType),
|
||||
LineCap(graphene_core::vector::style::LineCap),
|
||||
LineJoin(graphene_core::vector::style::LineJoin),
|
||||
#[serde(alias = "LineCap")]
|
||||
StrokeCap(graphene_core::vector::style::StrokeCap),
|
||||
#[serde(alias = "LineJoin")]
|
||||
StrokeJoin(graphene_core::vector::style::StrokeJoin),
|
||||
StrokeAlign(graphene_core::vector::style::StrokeAlign),
|
||||
PaintOrder(graphene_core::vector::style::PaintOrder),
|
||||
FillType(graphene_core::vector::style::FillType),
|
||||
FillChoice(graphene_core::vector::style::FillChoice),
|
||||
GradientType(graphene_core::vector::style::GradientType),
|
||||
|
|
|
@ -259,7 +259,15 @@ async fn render<'a: 'n, T: 'n + GraphicElementRendered + WasmNotSend>(
|
|||
ctx.footprint();
|
||||
|
||||
let RenderConfig { hide_artboards, for_export, .. } = render_config;
|
||||
let render_params = RenderParams::new(render_config.view_mode, None, false, hide_artboards, for_export);
|
||||
let render_params = RenderParams {
|
||||
view_mode: render_config.view_mode,
|
||||
culling_bounds: None,
|
||||
thumbnail: false,
|
||||
hide_artboards,
|
||||
for_export,
|
||||
for_mask: false,
|
||||
alignment_parent_transform: None,
|
||||
};
|
||||
|
||||
let data = data.eval(ctx.clone()).await;
|
||||
let editor_api = editor_api.eval(None).await;
|
||||
|
|
|
@ -61,8 +61,10 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
|||
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]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::LineCap]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::LineJoin]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::StrokeCap]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::StrokeJoin]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::PaintOrder]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::StrokeAlign]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::Stroke]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::Gradient]),
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::style::GradientStops]),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue