mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-25 08:28:11 +08:00
Retire the Vector stroke field into the appearance coverage
Stroke parameters live only on the appearance's stroke coverage now. Vector loses its stroke field, transform normalization, and concat stroke adoption; solidify, morph, combine, and the recolor read the coverage instead, with morph gaining the cover-paired appearance lerp. The stroke-inclusive bounds take the stroke as a parameter, the plain vector bounds ignore include_stroke, the editor's metadata channel carries one resolved appearance snapshot per layer, and the data panel's Vector table drops its stroke properties tab for a handles tab. Legacy vector payloads parse their stroke solely to validate the shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d278fd666d
commit
ff074b2a1c
@@ -37,8 +37,8 @@ pub enum PathStep {
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum VectorTableTab {
|
||||
#[default]
|
||||
Properties,
|
||||
Points,
|
||||
Segments,
|
||||
Regions,
|
||||
Handles,
|
||||
}
|
||||
|
||||
@@ -376,7 +376,7 @@ impl TableItemLayout for Vector {
|
||||
)
|
||||
}
|
||||
fn value_page(&self, data: &mut LayoutData) -> Vec<LayoutGroup> {
|
||||
let table_tab_entries = [VectorTableTab::Properties, VectorTableTab::Points, VectorTableTab::Segments, VectorTableTab::Regions]
|
||||
let table_tab_entries = [VectorTableTab::Points, VectorTableTab::Segments, VectorTableTab::Regions, VectorTableTab::Handles]
|
||||
.into_iter()
|
||||
.map(|tab| {
|
||||
RadioEntryData::new(format!("{tab:?}"))
|
||||
@@ -388,57 +388,6 @@ impl TableItemLayout for Vector {
|
||||
|
||||
let mut table_rows = Vec::new();
|
||||
match data.vector_table_tab {
|
||||
VectorTableTab::Properties => {
|
||||
table_rows.push(column_headings(&["property", "value"]));
|
||||
|
||||
if let Some(stroke) = self.stroke.as_ref() {
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Stroke Weight").narrow(true).widget_instance(),
|
||||
TextLabel::new(format!("{} px", stroke.weight)).narrow(true).widget_instance(),
|
||||
]);
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Stroke Dash Lengths").narrow(true).widget_instance(),
|
||||
TextLabel::new(if stroke.dash_lengths.is_empty() {
|
||||
"-".to_string()
|
||||
} else {
|
||||
format!("[{}]", stroke.dash_lengths.iter().map(|x| format!("{x} px")).collect::<Vec<_>>().join(", "))
|
||||
})
|
||||
.narrow(true)
|
||||
.widget_instance(),
|
||||
]);
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Stroke Dash Offset").narrow(true).widget_instance(),
|
||||
TextLabel::new(format!("{}", stroke.dash_offset)).narrow(true).widget_instance(),
|
||||
]);
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Stroke Cap").narrow(true).widget_instance(),
|
||||
TextLabel::new(stroke.cap.to_string()).narrow(true).widget_instance(),
|
||||
]);
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Stroke Join").narrow(true).widget_instance(),
|
||||
TextLabel::new(stroke.join.to_string()).narrow(true).widget_instance(),
|
||||
]);
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Stroke Join Miter Limit").narrow(true).widget_instance(),
|
||||
TextLabel::new(format!("{}", stroke.join_miter_limit)).narrow(true).widget_instance(),
|
||||
]);
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Stroke Align").narrow(true).widget_instance(),
|
||||
TextLabel::new(stroke.align.to_string()).narrow(true).widget_instance(),
|
||||
]);
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Stroke Transform").narrow(true).widget_instance(),
|
||||
TextLabel::new(format_transform_matrix(stroke.transform)).narrow(true).widget_instance(),
|
||||
]);
|
||||
}
|
||||
|
||||
let colinear = self.colinear_manipulators.iter().map(|[a, b]| format!("[{a} / {b}]")).collect::<Vec<_>>().join(", ");
|
||||
let colinear = if colinear.is_empty() { "-".to_string() } else { colinear };
|
||||
table_rows.push(vec![
|
||||
TextLabel::new("Colinear Handle IDs").narrow(true).widget_instance(),
|
||||
TextLabel::new(colinear).narrow(true).widget_instance(),
|
||||
]);
|
||||
}
|
||||
VectorTableTab::Points => {
|
||||
table_rows.push(column_headings(&["", "position"]));
|
||||
table_rows.extend(self.point_domain.iter().map(|(id, position)| {
|
||||
@@ -469,6 +418,16 @@ impl TableItemLayout for Vector {
|
||||
]
|
||||
}));
|
||||
}
|
||||
VectorTableTab::Handles => {
|
||||
table_rows.push(column_headings(&["", "colinear_manipulators[0]", "colinear_manipulators[1]"]));
|
||||
table_rows.extend(self.colinear_manipulators.iter().enumerate().map(|(index, [a, b])| {
|
||||
vec![
|
||||
TextLabel::new(format!("{index}")).narrow(true).widget_instance(),
|
||||
TextLabel::new(format!("{a}")).narrow(true).widget_instance(),
|
||||
TextLabel::new(format!("{b}")).narrow(true).widget_instance(),
|
||||
]
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
vec![LayoutGroup::row(table_tabs), LayoutGroup::table(table_rows, false)]
|
||||
|
||||
@@ -11,9 +11,8 @@ use crate::messages::portfolio::utility_types::PanelType;
|
||||
use crate::messages::prelude::*;
|
||||
use glam::{DAffine2, IVec2};
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_std::Appearance;
|
||||
use graphene_std::Color;
|
||||
use graphene_std::Graphic;
|
||||
use graphene_std::list::List;
|
||||
use graphene_std::raster::BlendMode;
|
||||
use graphene_std::raster::Image;
|
||||
use graphene_std::transform::Footprint;
|
||||
@@ -245,14 +244,10 @@ pub enum DocumentMessage {
|
||||
vector_data: HashMap<NodeId, Arc<Vector>>,
|
||||
},
|
||||
// `Message` is only serialized at `editor_wrapper.rs`, and only inputs from JS pass through it.
|
||||
// `UpdateFillAttributes` and `UpdateStrokeAttributes` are produced inside `editor.handle_message` by `node_graph_executor.rs` and consumed in the same dispatch loop, so it never reaches that serialization point.
|
||||
// `UpdateAppearanceAttributes` is produced inside `editor.handle_message` by `node_graph_executor.rs` and consumed in the same dispatch loop, so it never reaches that serialization point.
|
||||
#[serde(skip)]
|
||||
UpdateFillAttributes {
|
||||
fill_attributes: HashMap<NodeId, Arc<List<Graphic<'static>>>>,
|
||||
},
|
||||
#[serde(skip)]
|
||||
UpdateStrokeAttributes {
|
||||
stroke_attributes: HashMap<NodeId, Arc<List<Graphic<'static>>>>,
|
||||
UpdateAppearanceAttributes {
|
||||
appearance_attributes: HashMap<NodeId, Arc<Appearance>>,
|
||||
},
|
||||
Undo,
|
||||
UngroupSelectedLayers,
|
||||
|
||||
@@ -37,7 +37,7 @@ use graph_craft::application_io::wgpu_available;
|
||||
use graph_craft::descriptor;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork};
|
||||
use graphene_std::graphic::is_paint_present;
|
||||
use graphene_std::Cover;
|
||||
use graphene_std::math::quad::Quad;
|
||||
use graphene_std::path_bool_nodes::boolean_intersect;
|
||||
use graphene_std::raster::BlendMode;
|
||||
@@ -1502,9 +1502,9 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
.collect();
|
||||
self.network_interface.update_vector_data(layer_vector_data);
|
||||
}
|
||||
DocumentMessage::UpdateFillAttributes { fill_attributes } => {
|
||||
DocumentMessage::UpdateAppearanceAttributes { appearance_attributes } => {
|
||||
// Convert NodeId keys to LayerNodeIdentifier keys, filtering to only layers
|
||||
let layer_fill_attributes = fill_attributes
|
||||
let layer_appearance_attributes = appearance_attributes
|
||||
.into_iter()
|
||||
.filter(|(node_id, _)| self.network_interface.document_network().nodes.contains_key(node_id))
|
||||
.filter_map(|(node_id, attrs)| {
|
||||
@@ -1514,21 +1514,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
self.network_interface.update_fill_attributes(layer_fill_attributes);
|
||||
}
|
||||
DocumentMessage::UpdateStrokeAttributes { stroke_attributes } => {
|
||||
// Convert NodeId keys to LayerNodeIdentifier keys, filtering to only layers
|
||||
let layer_stroke_attributes = stroke_attributes
|
||||
.into_iter()
|
||||
.filter(|(node_id, _)| self.network_interface.document_network().nodes.contains_key(node_id))
|
||||
.filter_map(|(node_id, attrs)| {
|
||||
self.network_interface.is_layer(&node_id, &[]).then(|| {
|
||||
let layer = LayerNodeIdentifier::new(node_id, &self.network_interface);
|
||||
(layer, attrs)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
self.network_interface.update_stroke_attributes(layer_stroke_attributes);
|
||||
self.network_interface.update_appearance_attributes(layer_appearance_attributes);
|
||||
}
|
||||
DocumentMessage::Undo => {
|
||||
if self.network_interface.transaction_status() != TransactionStatus::Finished {
|
||||
@@ -2763,20 +2749,19 @@ impl DocumentMessageHandler {
|
||||
let mut resulting_layers: Vec<NodeId> = Vec::new();
|
||||
|
||||
for layer in selected_layers {
|
||||
let Some(vector_data) = self.network_interface.document_metadata().layer_vector_data.get(&layer) else {
|
||||
if !self.network_interface.document_metadata().layer_vector_data.contains_key(&layer) {
|
||||
resulting_layers.push(layer.to_node());
|
||||
continue;
|
||||
};
|
||||
let stroke = vector_data.stroke.as_ref();
|
||||
}
|
||||
|
||||
let fill_graphic_list = self.network_interface.document_metadata().layer_fill_attributes.get(&layer);
|
||||
let stroke_graphic_list = self.network_interface.document_metadata().layer_stroke_attributes.get(&layer);
|
||||
let appearance = self.network_interface.document_metadata().layer_appearance_attributes.get(&layer);
|
||||
|
||||
let has_fill = fill_graphic_list.is_some_and(|list| is_paint_present(list));
|
||||
// `Vector.stroke` captures stroke geometry, even with weight 0 or transparent paint.
|
||||
// So stroke visibility must be checked from the stroke paint snapshot, the paint source of truth.
|
||||
let stroke_visible = stroke_graphic_list.is_some_and(|list| list.element(0).is_some_and(|g| !g.is_fully_transparent()));
|
||||
let has_stroke = stroke.as_ref().is_some_and(|s| s.has_renderable_stroke()) && stroke_visible;
|
||||
let has_fill = appearance.is_some_and(|appearance| appearance.has_painted_cover(Cover::Fill));
|
||||
// A visible stroke needs both renderable geometry (non-zero weight) and paint that draws something
|
||||
let has_stroke = appearance.is_some_and(|appearance| {
|
||||
appearance.first_coverage_of(Cover::Stroke).is_some_and(|coverage| coverage.stroke_params().has_renderable_stroke())
|
||||
&& appearance.first_paint_of(Cover::Stroke).is_some_and(|paint| !paint.is_fully_transparent())
|
||||
});
|
||||
|
||||
// No stroke means there's nothing to solidify. Fill-only layers are already in the desired form, so skip.
|
||||
if !has_stroke {
|
||||
|
||||
@@ -6,8 +6,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::Flow
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_std::Graphic;
|
||||
use graphene_std::list::List;
|
||||
use graphene_std::Appearance;
|
||||
use graphene_std::math::quad::Quad;
|
||||
use graphene_std::subpath;
|
||||
use graphene_std::transform::Footprint;
|
||||
@@ -41,12 +40,9 @@ pub struct DocumentMetadata {
|
||||
/// Vector data keyed by layer ID, used as fallback when no Path node exists.
|
||||
/// This provides accurate SegmentIds for layers without explicit Path nodes.
|
||||
pub layer_vector_data: HashMap<LayerNodeIdentifier, Arc<Vector>>,
|
||||
/// Per-layer fill paint snapshot, exposed so message handlers can read paint
|
||||
/// information that lives on the list.
|
||||
pub layer_fill_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic<'static>>>>,
|
||||
/// Per-layer stroke paint snapshot, exposed so message handlers can read
|
||||
/// stroke paint information that lives on the list.
|
||||
pub layer_stroke_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic<'static>>>>,
|
||||
/// Per-layer resolved appearance snapshot, exposed so message handlers can
|
||||
/// read the paint and stroke parameters that live on the coverage rows.
|
||||
pub layer_appearance_attributes: HashMap<LayerNodeIdentifier, Arc<Appearance>>,
|
||||
/// Transform from document space to viewport space.
|
||||
pub document_to_viewport: DAffine2,
|
||||
}
|
||||
@@ -233,10 +229,15 @@ impl DocumentMetadata {
|
||||
/// stroke geometry when the layer is a vector with a stroke style. Falls back to the click-target-based
|
||||
/// bounds for non-vector layers (groups, raster, text, color, gradient).
|
||||
pub fn bounding_box_document_with_stroke(&self, layer: LayerNodeIdentifier) -> Option<[DVec2; 2]> {
|
||||
if let Some(vector) = self.layer_vector_data.get(&layer)
|
||||
&& let Some(bounds) = vector.stroke_inclusive_bounding_box_with_transform(self.transform_to_document(layer))
|
||||
{
|
||||
return Some(bounds);
|
||||
if let Some(vector) = self.layer_vector_data.get(&layer) {
|
||||
let stroke = self
|
||||
.layer_appearance_attributes
|
||||
.get(&layer)
|
||||
.and_then(|appearance| appearance.first_coverage_of(graphene_std::Cover::Stroke))
|
||||
.map(graphene_std::Coverage::stroke_params);
|
||||
if let Some(bounds) = vector.stroke_inclusive_bounding_box_with_transform(self.transform_to_document(layer), stroke.as_ref()) {
|
||||
return Some(bounds);
|
||||
}
|
||||
}
|
||||
self.bounding_box_document(layer)
|
||||
}
|
||||
|
||||
@@ -24,8 +24,7 @@ use graph_craft::Type;
|
||||
use graph_craft::application_io::resource::ResourceId;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork};
|
||||
use graphene_std::Graphic;
|
||||
use graphene_std::list::List;
|
||||
use graphene_std::Appearance;
|
||||
use graphene_std::math::quad::Quad;
|
||||
use graphene_std::subpath::Subpath;
|
||||
use graphene_std::transform::Footprint;
|
||||
@@ -3439,14 +3438,9 @@ impl NodeNetworkInterface {
|
||||
self.document_metadata.layer_vector_data = new_layer_vector_data;
|
||||
}
|
||||
|
||||
/// Update the per-layer fill paint snapshot.
|
||||
pub fn update_fill_attributes(&mut self, new_layer_fill_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic<'static>>>>) {
|
||||
self.document_metadata.layer_fill_attributes = new_layer_fill_attributes;
|
||||
}
|
||||
|
||||
/// Update the per-layer stroke paint snapshot.
|
||||
pub fn update_stroke_attributes(&mut self, new_layer_stroke_attributes: HashMap<LayerNodeIdentifier, Arc<List<Graphic<'static>>>>) {
|
||||
self.document_metadata.layer_stroke_attributes = new_layer_stroke_attributes;
|
||||
/// Update the per-layer resolved appearance snapshot.
|
||||
pub fn update_appearance_attributes(&mut self, new_layer_appearance_attributes: HashMap<LayerNodeIdentifier, Arc<Appearance>>) {
|
||||
self.document_metadata.layer_appearance_attributes = new_layer_appearance_attributes;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -715,8 +715,7 @@ impl NodeGraphExecutor {
|
||||
text_frames,
|
||||
clip_targets,
|
||||
vector_data,
|
||||
fill_attributes,
|
||||
stroke_attributes,
|
||||
appearance_attributes,
|
||||
backgrounds: _,
|
||||
} = render_output.metadata;
|
||||
|
||||
@@ -731,8 +730,7 @@ impl NodeGraphExecutor {
|
||||
responses.add(DocumentMessage::UpdateTextFrames { text_frames });
|
||||
responses.add(DocumentMessage::UpdateClipTargets { clip_targets });
|
||||
responses.add(DocumentMessage::UpdateVectorData { vector_data });
|
||||
responses.add(DocumentMessage::UpdateFillAttributes { fill_attributes });
|
||||
responses.add(DocumentMessage::UpdateStrokeAttributes { stroke_attributes });
|
||||
responses.add(DocumentMessage::UpdateAppearanceAttributes { appearance_attributes });
|
||||
responses.add(DocumentMessage::RenderScrollbars);
|
||||
responses.add(DocumentMessage::RenderRulers);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
Reference in New Issue
Block a user