192 lines
7.3 KiB
Rust
192 lines
7.3 KiB
Rust
//! Bidirectional vector shape ↔ tool state synchronization.
|
|
//!
|
|
//! Purpose: Keep the iced GUI's tool settings (stroke, fill, opacity, colors)
|
|
//! and the selected VectorShape in the document engine in sync. When the user
|
|
//! changes tool properties while a shape is selected, push them to the shape.
|
|
//! When a shape is selected, pull its properties back into the tool state.
|
|
//!
|
|
//! Logic & Workflow:
|
|
//! - `sync_shape_from_tool`: Reads tool settings + fg/bg colors from the app,
|
|
//! writes all matching properties into the engine's selected vector shape.
|
|
//! - `sync_tool_from_shape`: Reads the engine's selected shape properties,
|
|
//! writes them back into tool settings and app color state.
|
|
//!
|
|
//! Side Effects: Mutates both the engine (via `mark_composite_dirty`) and
|
|
//! the app's `settings.tool_settings` / `fg_color` / `bg_color`.
|
|
|
|
use crate::app::HcieIcedApp;
|
|
use hcie_engine_api::{Tool, VectorShape};
|
|
|
|
/// Push tool state properties (stroke, fill, color, opacity, hardness)
|
|
/// into the currently selected vector shape.
|
|
///
|
|
/// Called when the user changes a tool property or picks a color while
|
|
/// a shape is selected in VectorSelect mode.
|
|
pub fn sync_shape_from_tool(app: &mut HcieIcedApp) {
|
|
if app.tool_state.active_tool != Tool::VectorSelect {
|
|
return;
|
|
}
|
|
let shape_idx = match app.documents[app.active_doc].selected_vector_shape {
|
|
Some(i) => i,
|
|
None => return,
|
|
};
|
|
if !app.documents[app.active_doc]
|
|
.engine
|
|
.active_vector_shapes()
|
|
.is_some_and(|shapes| shape_idx < shapes.len())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Snapshot for undo (before first modification in this edit session)
|
|
if app.vector_shapes_snapshot.is_none() {
|
|
let doc = &app.documents[app.active_doc];
|
|
if let Some(shapes) = doc.engine.active_vector_shapes() {
|
|
app.vector_shapes_snapshot = Some(crate::vector_edit::VectorEditSnapshot {
|
|
layer_id: doc.engine.active_layer_id(),
|
|
shapes,
|
|
});
|
|
}
|
|
}
|
|
|
|
let layer_id = app.documents[app.active_doc].engine.active_layer_id();
|
|
let ts = &app.settings.tool_settings;
|
|
|
|
// Stroke width
|
|
app.documents[app.active_doc]
|
|
.engine
|
|
.set_vector_shape_stroke(layer_id, shape_idx, ts.vector_stroke_width);
|
|
|
|
// Fill toggle — only push if shape has a fill or the value changed
|
|
if let Some(shapes) = app.documents[app.active_doc].engine.active_vector_shapes() {
|
|
if let Some(shape) = shapes.get(shape_idx) {
|
|
if shape.fill_color().is_some() || shape.is_filled() != ts.vector_fill {
|
|
app.documents[app.active_doc].engine.set_vector_shape_fill(
|
|
layer_id,
|
|
shape_idx,
|
|
ts.vector_fill,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
app.documents[app.active_doc].modified = true;
|
|
app.documents[app.active_doc].engine.mark_composite_dirty();
|
|
|
|
// Opacity
|
|
app.documents[app.active_doc]
|
|
.engine
|
|
.set_vector_shape_opacity(layer_id, shape_idx, ts.vector_opacity);
|
|
|
|
// Hardness — use brush_hardness as the shared hardness value
|
|
app.documents[app.active_doc]
|
|
.engine
|
|
.set_vector_shape_hardness(layer_id, shape_idx, app.tool_state.brush_hardness);
|
|
|
|
// Rotation angle (radians in engine, degrees in GUI)
|
|
app.documents[app.active_doc].engine.set_vector_shape_angle(
|
|
layer_id,
|
|
shape_idx,
|
|
ts.vector_angle.to_radians(),
|
|
);
|
|
|
|
// Primary color (stroke color)
|
|
app.documents[app.active_doc]
|
|
.engine
|
|
.set_vector_shape_color(layer_id, shape_idx, app.fg_color);
|
|
|
|
// Fill color — only push if shape already has a fill color
|
|
if let Some(shapes) = app.documents[app.active_doc].engine.active_vector_shapes() {
|
|
if let Some(shape) = shapes.get(shape_idx) {
|
|
if shape.fill_color().is_some() {
|
|
app.documents[app.active_doc]
|
|
.engine
|
|
.set_vector_shape_fill_color(layer_id, shape_idx, app.bg_color);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Shape-specific properties (radius, points, sides)
|
|
if let Some(shapes) = app.documents[app.active_doc].engine.active_vector_shapes() {
|
|
if let Some(shape) = shapes.get(shape_idx) {
|
|
match shape {
|
|
VectorShape::Rect { .. } => {
|
|
// round_rect_radius is set directly via engine if needed
|
|
}
|
|
VectorShape::Star { .. } => {
|
|
// star_points / inner_radius via engine when available
|
|
}
|
|
VectorShape::Polygon { .. } => {
|
|
// polygon_sides via engine when available
|
|
}
|
|
VectorShape::SvgShape { .. } => {
|
|
// SVG shapes have no extra per-shape properties
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Pull properties from the currently selected vector shape back into
|
|
/// tool settings and app color state.
|
|
///
|
|
/// Called when the user clicks/selects a shape in VectorSelect mode
|
|
/// so the properties panel and tool state reflect the shape's actual values.
|
|
pub fn sync_tool_from_shape(app: &mut HcieIcedApp) {
|
|
let shape_idx = match app.documents[app.active_doc].selected_vector_shape {
|
|
Some(i) => i,
|
|
None => return,
|
|
};
|
|
|
|
if let Some(shapes) = app.documents[app.active_doc].engine.active_vector_shapes() {
|
|
if let Some(shape) = shapes.get(shape_idx) {
|
|
let ts = &mut app.settings.tool_settings;
|
|
|
|
// Common properties — note: angle is intentionally NOT synced here
|
|
// so that the "default angle for new shapes" stays independent from
|
|
// the selected shape's rotation. The angle slider in the Properties
|
|
// panel writes directly to `tool_settings.vector_angle` via
|
|
// VectorAngleChanged and is pushed to the shape via
|
|
// sync_shape_from_tool.
|
|
ts.vector_stroke_width = shape.stroke();
|
|
ts.vector_opacity = shape.shape_opacity();
|
|
app.tool_state.brush_hardness = shape.hardness();
|
|
ts.vector_fill = shape.is_filled();
|
|
|
|
// Shape-specific properties
|
|
match shape {
|
|
VectorShape::Rect { radius, .. } => {
|
|
ts.vector_radius = *radius;
|
|
}
|
|
VectorShape::Star {
|
|
points,
|
|
inner_radius: _,
|
|
..
|
|
} => {
|
|
ts.vector_points = *points;
|
|
}
|
|
VectorShape::Polygon { sides, .. } => {
|
|
ts.vector_sides = *sides;
|
|
}
|
|
VectorShape::SvgShape { .. } => {
|
|
// No shape-specific properties for SVG shapes
|
|
}
|
|
VectorShape::Line {
|
|
cap_start,
|
|
cap_end: _,
|
|
..
|
|
} => {
|
|
// Store line caps in settings if available
|
|
let _ = cap_start;
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
// Colors are intentionally NOT synced from the shape into the
|
|
// active palette. The geometry / properties panels read the
|
|
// shape's colors directly from the engine for display, and the
|
|
// user picks new colors via the color palette or panel pickers.
|
|
}
|
|
}
|
|
}
|