Skip to content

Commit 3dfaf03

Browse files
Merge branch 'master' into add-arrow-shape-feature-cleaned
2 parents b26885c + 33e3a88 commit 3dfaf03

7 files changed

Lines changed: 173 additions & 24 deletions

File tree

editor/src/messages/dialog/export_dialog/export_dialog_message_handler.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,21 @@ impl MessageHandler<ExportDialogMessage, ExportDialogMessageContext<'_>> for Exp
4343
ExportDialogMessage::TransparentBackground { transparent } => self.transparent_background = transparent,
4444
ExportDialogMessage::ExportBounds { bounds } => self.bounds = bounds,
4545

46-
ExportDialogMessage::Submit => responses.add_front(PortfolioMessage::SubmitDocumentExport {
47-
name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(),
48-
file_type: self.file_type,
49-
scale_factor: self.scale_factor,
50-
bounds: self.bounds,
51-
transparent_background: self.file_type != FileType::Jpg && self.transparent_background,
52-
}),
46+
ExportDialogMessage::Submit => {
47+
let artboard_name = match self.bounds {
48+
ExportBounds::Artboard(layer) => self.artboards.get(&layer).cloned(),
49+
_ => None,
50+
};
51+
responses.add_front(PortfolioMessage::SubmitDocumentExport {
52+
name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(),
53+
file_type: self.file_type,
54+
scale_factor: self.scale_factor,
55+
bounds: self.bounds,
56+
transparent_background: self.file_type != FileType::Jpg && self.transparent_background,
57+
artboard_name,
58+
artboard_count: self.artboards.len(),
59+
})
60+
}
5361
}
5462

5563
self.send_dialog_to_frontend(responses);

editor/src/messages/portfolio/portfolio_message.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ pub enum PortfolioMessage {
124124
scale_factor: f64,
125125
bounds: ExportBounds,
126126
transparent_background: bool,
127+
artboard_name: Option<String>,
128+
artboard_count: usize,
127129
},
128130
SubmitActiveGraphRender,
129131
SubmitGraphRender {

editor/src/messages/portfolio/portfolio_message_handler.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,6 +1036,8 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
10361036
scale_factor,
10371037
bounds,
10381038
transparent_background,
1039+
artboard_name,
1040+
artboard_count,
10391041
} => {
10401042
let document = self.active_document_id.and_then(|id| self.documents.get_mut(&id)).expect("Tried to render non-existent document");
10411043
let export_config = ExportConfig {
@@ -1044,6 +1046,8 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
10441046
scale_factor,
10451047
bounds,
10461048
transparent_background,
1049+
artboard_name,
1050+
artboard_count,
10471051
..Default::default()
10481052
};
10491053
let result = self.executor.submit_document_export(document, self.active_document_id.unwrap(), export_config);

editor/src/messages/tool/common_functionality/shape_editor.rs

Lines changed: 84 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,68 @@ impl ShapeState {
421421
(point.as_handle().is_some() && self.ignore_handles) || (point.as_anchor().is_some() && self.ignore_anchors)
422422
}
423423

424+
/// Applies a dummy vector modification to the layer. In the case where a group containing some vector data is selected, this triggers the creation of a «Flatten Path» node.
425+
fn add_dummy_modification_to_trigger_graph_reorganization(layer: LayerNodeIdentifier, start_point: PointId, _end_point: PointId, responses: &mut VecDeque<Message>) {
426+
// Apply a zero-delta to one of the points to trigger reorganization
427+
let dummy_modification = VectorModificationType::ApplyPointDelta {
428+
point: start_point,
429+
delta: DVec2::ZERO,
430+
};
431+
responses.add(GraphOperationMessage::Vector {
432+
layer,
433+
modification_type: dummy_modification,
434+
});
435+
responses.add(NodeGraphMessage::RunDocumentGraph);
436+
}
437+
438+
fn defer_connect_points_by_position(
439+
document: &DocumentMessageHandler,
440+
layer1: LayerNodeIdentifier,
441+
start_point: PointId,
442+
layer2: LayerNodeIdentifier,
443+
end_point: PointId,
444+
target_layer: LayerNodeIdentifier,
445+
responses: &mut VecDeque<Message>,
446+
) {
447+
// Get the local positions of the selected points
448+
let start_local_pos = document.network_interface.compute_modified_vector(layer1).and_then(|v| v.point_domain.position_from_id(start_point));
449+
let end_local_pos = document.network_interface.compute_modified_vector(layer2).and_then(|v| v.point_domain.position_from_id(end_point));
450+
451+
// Transform to document/world space
452+
let start_transform = document.metadata().transform_to_document(layer1);
453+
let end_transform = document.metadata().transform_to_document(layer2);
454+
455+
let (Some(start_local), Some(end_local)) = (start_local_pos, end_local_pos) else {
456+
warn!("Unable to resolve point ids for joining");
457+
return;
458+
};
459+
// Transform positions to document/world space
460+
// These positions are stable (won't change during reorganization)
461+
let start_pos = start_transform.transform_point2(start_local);
462+
let end_pos = end_transform.transform_point2(end_local);
463+
464+
// Defer position-based connection to run after reorganization completes
465+
// By then, PointIds will be stable with their new remapped values
466+
responses.add(DeferMessage::AfterGraphRun {
467+
messages: vec![
468+
ToolMessage::Path(PathToolMessage::ConnectPointsByPosition {
469+
layer: target_layer,
470+
start_position: start_pos,
471+
end_position: end_pos,
472+
})
473+
.into(),
474+
],
475+
});
476+
}
477+
478+
fn handle_grouped_transform_close_path(document: &DocumentMessageHandler, layer: LayerNodeIdentifier, start_point: PointId, end_point: PointId, responses: &mut VecDeque<Message>) {
479+
// This zero-delta modification triggers point domain reorganization
480+
Self::add_dummy_modification_to_trigger_graph_reorganization(layer, start_point, end_point, responses);
481+
482+
// Use the helper to defer the connection until after reorganization
483+
Self::defer_connect_points_by_position(document, layer, start_point, layer, end_point, layer, responses);
484+
}
485+
424486
pub fn close_selected_path(&self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
425487
// First collect all selected anchor points across all layers
426488
let all_selected_points: Vec<(LayerNodeIdentifier, PointId)> = self
@@ -447,28 +509,34 @@ impl ShapeState {
447509
let (layer2, end_point) = all_selected_points[1];
448510

449511
if layer1 == layer2 {
512+
// Same layer case
450513
if start_point == end_point {
451514
return;
452515
}
453516

454-
let segment_id = SegmentId::generate();
455-
let modification_type = VectorModificationType::InsertSegment {
456-
id: segment_id,
457-
points: [end_point, start_point],
458-
handles: [None, None],
459-
};
460-
responses.add(GraphOperationMessage::Vector { layer: layer1, modification_type });
517+
// Check if this layer has multiple children (is a merged/grouped layer created with Cmd+G)
518+
let num_children = layer1.children(document.metadata()).count();
519+
let is_grouped = num_children > 1;
520+
521+
if is_grouped {
522+
// Grouped/merged layer: use helper function to handle reorganization
523+
Self::handle_grouped_transform_close_path(document, layer1, start_point, end_point, responses);
524+
} else {
525+
// Single segment: PointIDs are stable, use immediate insertion
526+
let segment_id = SegmentId::generate();
527+
let modification_type = VectorModificationType::InsertSegment {
528+
id: segment_id,
529+
points: [end_point, start_point],
530+
handles: [None, None],
531+
};
532+
responses.add(GraphOperationMessage::Vector { layer: layer1, modification_type });
533+
}
461534
} else {
462-
// Merge the layers
535+
// Different layers: merge first, then create segment
463536
merge_layers(document, layer1, layer2, responses);
464-
// Create segment between the two points
465-
let segment_id = SegmentId::generate();
466-
let modification_type = VectorModificationType::InsertSegment {
467-
id: segment_id,
468-
points: [end_point, start_point],
469-
handles: [None, None],
470-
};
471-
responses.add(GraphOperationMessage::Vector { layer: layer1, modification_type });
537+
538+
// Use the helper to defer the connection until after reorganization
539+
Self::defer_connect_points_by_position(document, layer1, start_point, layer2, end_point, layer1, responses);
472540
}
473541
return;
474542
}

editor/src/messages/tool/tool_messages/path_tool.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ pub enum PathToolMessage {
7373
},
7474
Escape,
7575
ClosePath,
76+
ConnectPointsByPosition {
77+
layer: LayerNodeIdentifier,
78+
start_position: DVec2,
79+
end_position: DVec2,
80+
},
7681
DoubleClick {
7782
extend_selection: Key,
7883
shrink_selection: Key,
@@ -2669,6 +2674,60 @@ impl Fsm for PathToolFsmState {
26692674

26702675
self
26712676
}
2677+
(_, PathToolMessage::ConnectPointsByPosition { layer, start_position, end_position }) => {
2678+
// Get the merged vector
2679+
let Some(vector) = document.network_interface.compute_modified_vector(layer) else {
2680+
return self;
2681+
};
2682+
2683+
// Find points by their positions (with small tolerance for floating point comparison)
2684+
const POSITION_TOLERANCE: f64 = 1e-6;
2685+
2686+
let positions = vector.point_domain.positions();
2687+
let point_ids = vector.point_domain.ids();
2688+
2689+
let mut start_point_id = None;
2690+
let mut end_point_id = None;
2691+
2692+
// Get the merged layer's transform to convert local positions to document space
2693+
let layer_transform = document.metadata().transform_to_document(layer);
2694+
2695+
for (i, &local_pos) in positions.iter().enumerate() {
2696+
// Transform the local position to document space for comparison
2697+
let doc_pos = layer_transform.transform_point2(local_pos);
2698+
2699+
let start_distance = (doc_pos - start_position).length();
2700+
let end_distance = (doc_pos - end_position).length();
2701+
2702+
if start_point_id.is_none() && start_distance < POSITION_TOLERANCE {
2703+
start_point_id = Some(point_ids[i]);
2704+
}
2705+
if end_point_id.is_none() && end_distance < POSITION_TOLERANCE {
2706+
end_point_id = Some(point_ids[i]);
2707+
}
2708+
if start_point_id.is_some() && end_point_id.is_some() {
2709+
break;
2710+
}
2711+
}
2712+
2713+
if let (Some(start_id), Some(end_id)) = (start_point_id, end_point_id) {
2714+
// Create segment directly
2715+
responses.add(DocumentMessage::StartTransaction);
2716+
2717+
let segment_id = SegmentId::generate();
2718+
let modification_type = VectorModificationType::InsertSegment {
2719+
id: segment_id,
2720+
points: [end_id, start_id],
2721+
handles: [None, None],
2722+
};
2723+
2724+
responses.add(GraphOperationMessage::Vector { layer, modification_type });
2725+
responses.add(DocumentMessage::EndTransaction);
2726+
responses.add(OverlaysMessage::Draw);
2727+
}
2728+
2729+
self
2730+
}
26722731
(_, PathToolMessage::StartSlidingPoint) => {
26732732
responses.add(DocumentMessage::StartTransaction);
26742733
if tool_data.start_sliding_point(shape_editor, document) {

editor/src/node_graph_executor.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,8 @@ impl NodeGraphExecutor {
242242
scale_factor,
243243
#[cfg(feature = "gpu")]
244244
transparent_background,
245+
artboard_name,
246+
artboard_count,
245247
..
246248
} = export_config;
247249

@@ -250,7 +252,11 @@ impl NodeGraphExecutor {
250252
FileType::Png => "png",
251253
FileType::Jpg => "jpg",
252254
};
253-
let name = format!("{name}.{file_extension}");
255+
let base_name = match (artboard_name, artboard_count) {
256+
(Some(artboard_name), count) if count > 1 => format!("{name} - {artboard_name}"),
257+
_ => name,
258+
};
259+
let name = format!("{base_name}.{file_extension}");
254260

255261
match node_graph_output {
256262
TaggedValue::RenderOutput(RenderOutput {

editor/src/node_graph_executor/runtime.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ pub struct ExportConfig {
8585
pub bounds: ExportBounds,
8686
pub transparent_background: bool,
8787
pub size: DVec2,
88+
pub artboard_name: Option<String>,
89+
pub artboard_count: usize,
8890
}
8991

9092
#[derive(Clone)]

0 commit comments

Comments
 (0)