Phase 3.5a: place imported images on the canvas (image-filled rect)
Replaces the DCEL "not yet supported" stubs so importing/dropping an image actually puts it in the vector scene. - AddShapeAction gains an `image_fill` + `AddShapeAction::image_rect(...)` constructor: a borderless rectangle (invisible edges) whose enclosed region is paint-bucketed and tagged with an image asset id. The renderer already prioritises `image_fill`. - Direct import (auto_place_asset): an imported image is placed centered on the canvas at native size on a vector layer. - Drag from the asset library onto the stage: image-filled rect at the drop point (centered), native size, using the asset's dimensions. Next: SetImageFillAction + an Info-Panel image-fill picker for existing shapes; then 3.5b container persistence.
This commit is contained in:
parent
aea26f6192
commit
6c9fcb1921
|
|
@ -23,6 +23,9 @@ pub struct AddShapeAction {
|
||||||
stroke_color: Option<ShapeColor>,
|
stroke_color: Option<ShapeColor>,
|
||||||
fill_color: Option<ShapeColor>,
|
fill_color: Option<ShapeColor>,
|
||||||
is_closed: bool,
|
is_closed: bool,
|
||||||
|
/// When set, the enclosed region is filled with this image asset (instead of a
|
||||||
|
/// solid colour). The renderer prioritises `image_fill` over colour/gradient.
|
||||||
|
image_fill: Option<Uuid>,
|
||||||
description_text: String,
|
description_text: String,
|
||||||
/// Snapshot of the graph before insertion (for undo).
|
/// Snapshot of the graph before insertion (for undo).
|
||||||
graph_before: Option<VectorGraph>,
|
graph_before: Option<VectorGraph>,
|
||||||
|
|
@ -46,15 +49,53 @@ impl AddShapeAction {
|
||||||
stroke_color,
|
stroke_color,
|
||||||
fill_color,
|
fill_color,
|
||||||
is_closed,
|
is_closed,
|
||||||
|
image_fill: None,
|
||||||
description_text: "Add shape".to_string(),
|
description_text: "Add shape".to_string(),
|
||||||
graph_before: None,
|
graph_before: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A borderless, axis-aligned rectangle filled with an image asset — the result
|
||||||
|
/// of importing/dropping an image onto a vector layer.
|
||||||
|
pub fn image_rect(
|
||||||
|
layer_id: Uuid,
|
||||||
|
time: f64,
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
w: f64,
|
||||||
|
h: f64,
|
||||||
|
asset_id: Uuid,
|
||||||
|
) -> Self {
|
||||||
|
let mut path = BezPath::new();
|
||||||
|
path.move_to((x, y));
|
||||||
|
path.line_to((x + w, y));
|
||||||
|
path.line_to((x + w, y + h));
|
||||||
|
path.line_to((x, y + h));
|
||||||
|
path.close_path();
|
||||||
|
Self {
|
||||||
|
layer_id,
|
||||||
|
time,
|
||||||
|
path,
|
||||||
|
stroke_style: None, // invisible edges — just the image
|
||||||
|
stroke_color: None,
|
||||||
|
fill_color: None,
|
||||||
|
is_closed: true,
|
||||||
|
image_fill: Some(asset_id),
|
||||||
|
description_text: "Add image".to_string(),
|
||||||
|
graph_before: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
|
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
|
||||||
self.description_text = desc.into();
|
self.description_text = desc.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fill the created region with an image asset (image takes render priority).
|
||||||
|
pub fn with_image_fill(mut self, asset_id: Uuid) -> Self {
|
||||||
|
self.image_fill = Some(asset_id);
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Action for AddShapeAction {
|
impl Action for AddShapeAction {
|
||||||
|
|
@ -87,16 +128,25 @@ impl Action for AddShapeAction {
|
||||||
DEFAULT_SNAP_EPSILON,
|
DEFAULT_SNAP_EPSILON,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Apply fill if this is a closed shape with fill
|
// Apply fill if this is a closed shape with a colour and/or image fill.
|
||||||
if self.is_closed {
|
if self.is_closed && (self.fill_color.is_some() || self.image_fill.is_some()) {
|
||||||
if let Some(ref fill) = self.fill_color {
|
// Compute centroid of the path's bounding box and paint-bucket fill.
|
||||||
// Compute centroid of the path's bounding box and paint-bucket fill
|
let bbox = self.path.bounding_box();
|
||||||
let bbox = self.path.bounding_box();
|
let centroid = kurbo::Point::new(
|
||||||
let centroid = kurbo::Point::new(
|
(bbox.x0 + bbox.x1) / 2.0,
|
||||||
(bbox.x0 + bbox.x1) / 2.0,
|
(bbox.y0 + bbox.y1) / 2.0,
|
||||||
(bbox.y0 + bbox.y1) / 2.0,
|
);
|
||||||
);
|
// paint_bucket needs a colour; an image-only fill uses a placeholder
|
||||||
graph.paint_bucket(centroid, fill.clone(), FillRule::NonZero, 0.0);
|
// that the image overrides (cleared below).
|
||||||
|
let color = self.fill_color.clone().unwrap_or_else(|| ShapeColor::rgba(255, 255, 255, 255));
|
||||||
|
if let Some(fid) = graph.paint_bucket(centroid, color, FillRule::NonZero, 0.0) {
|
||||||
|
if let Some(asset_id) = self.image_fill {
|
||||||
|
let fill = graph.fill_mut(fid);
|
||||||
|
fill.image_fill = Some(asset_id);
|
||||||
|
if self.fill_color.is_none() {
|
||||||
|
fill.color = None; // image-only: don't double-paint a colour
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4921,15 +4921,22 @@ impl EditorApp {
|
||||||
|
|
||||||
// Add clip instance or shape to the target layer
|
// Add clip instance or shape to the target layer
|
||||||
if let Some(layer_id) = target_layer_id {
|
if let Some(layer_id) = target_layer_id {
|
||||||
// For images, create a shape with image fill instead of a clip instance
|
// For images, create an image-filled rectangle on the (vector) layer,
|
||||||
|
// centered on the canvas at native size.
|
||||||
if asset_info.clip_type == panes::DragClipType::Image {
|
if asset_info.clip_type == panes::DragClipType::Image {
|
||||||
// Get image dimensions
|
let (img_w, img_h) = asset_info.dimensions.unwrap_or((100.0, 100.0));
|
||||||
let (width, height) = asset_info.dimensions.unwrap_or((100.0, 100.0));
|
let (doc_w, doc_h) = {
|
||||||
|
let d = self.action_executor.document();
|
||||||
// TODO: Image fills on DCEL faces are a separate feature.
|
(d.width as f64, d.height as f64)
|
||||||
// For now, just log a message.
|
};
|
||||||
let _ = (layer_id, width, height);
|
let x = (doc_w - img_w) / 2.0;
|
||||||
eprintln!("Image drop to canvas not yet supported with DCEL backend");
|
let y = (doc_h - img_h) / 2.0;
|
||||||
|
let action = lightningbeam_core::actions::AddShapeAction::image_rect(
|
||||||
|
layer_id, self.playback_time, x, y, img_w, img_h, asset_info.clip_id,
|
||||||
|
);
|
||||||
|
if let Err(e) = self.action_executor.execute(Box::new(action)) {
|
||||||
|
eprintln!("Failed to place image: {}", e);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// For clips, create a clip instance
|
// For clips, create a clip instance
|
||||||
let mut clip_instance = ClipInstance::new(asset_info.clip_id)
|
let mut clip_instance = ClipInstance::new(asset_info.clip_id)
|
||||||
|
|
|
||||||
|
|
@ -11780,9 +11780,18 @@ impl PaneRenderer for StagePane {
|
||||||
if let Some(layer_id) = target_layer_id {
|
if let Some(layer_id) = target_layer_id {
|
||||||
// For images, create a shape with image fill instead of a clip instance
|
// For images, create a shape with image fill instead of a clip instance
|
||||||
if dragging.clip_type == DragClipType::Image {
|
if dragging.clip_type == DragClipType::Image {
|
||||||
// TODO: Image fills on DCEL faces are a separate feature.
|
// Image-filled rectangle at the drop point (centered), native size.
|
||||||
let _ = (layer_id, world_pos);
|
let dims = shared.action_executor.document()
|
||||||
eprintln!("Image drag to stage not yet supported with DCEL backend");
|
.get_image_asset(&dragging.clip_id)
|
||||||
|
.map(|a| (a.width as f64, a.height as f64));
|
||||||
|
if let Some((img_w, img_h)) = dims {
|
||||||
|
let x = world_pos.x as f64 - img_w / 2.0;
|
||||||
|
let y = world_pos.y as f64 - img_h / 2.0;
|
||||||
|
let action = lightningbeam_core::actions::AddShapeAction::image_rect(
|
||||||
|
layer_id, drop_time, x, y, img_w, img_h, dragging.clip_id,
|
||||||
|
);
|
||||||
|
let _ = shared.action_executor.execute(Box::new(action));
|
||||||
|
}
|
||||||
} else if dragging.clip_type == DragClipType::Effect {
|
} else if dragging.clip_type == DragClipType::Effect {
|
||||||
// Handle effect drops specially
|
// Handle effect drops specially
|
||||||
// Get effect definition from registry or document
|
// Get effect definition from registry or document
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue