add skew to transform tool
This commit is contained in:
parent
258b131f2d
commit
0d8fa7ffbd
|
|
@ -90,6 +90,8 @@ impl Transform {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert to an affine transform matrix
|
/// Convert to an affine transform matrix
|
||||||
|
/// Note: Skew is applied in local space. For proper centering, the shape's
|
||||||
|
/// bounding box center should be used (see renderer.rs for the full implementation).
|
||||||
pub fn to_affine(&self) -> kurbo::Affine {
|
pub fn to_affine(&self) -> kurbo::Affine {
|
||||||
use kurbo::Affine;
|
use kurbo::Affine;
|
||||||
|
|
||||||
|
|
@ -98,7 +100,7 @@ impl Transform {
|
||||||
let rotate = Affine::rotate(self.rotation.to_radians());
|
let rotate = Affine::rotate(self.rotation.to_radians());
|
||||||
let scale = Affine::scale_non_uniform(self.scale_x, self.scale_y);
|
let scale = Affine::scale_non_uniform(self.scale_x, self.scale_y);
|
||||||
|
|
||||||
// Skew transforms
|
// Skew transforms (applied in local space)
|
||||||
let skew_x = if self.skew_x != 0.0 {
|
let skew_x = if self.skew_x != 0.0 {
|
||||||
let tan_skew = self.skew_x.to_radians().tan();
|
let tan_skew = self.skew_x.to_radians().tan();
|
||||||
Affine::new([1.0, 0.0, tan_skew, 1.0, 0.0, 0.0])
|
Affine::new([1.0, 0.0, tan_skew, 1.0, 0.0, 0.0])
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
use crate::animation::TransformProperty;
|
use crate::animation::TransformProperty;
|
||||||
use crate::document::Document;
|
use crate::document::Document;
|
||||||
use crate::layer::{AnyLayer, VectorLayer};
|
use crate::layer::{AnyLayer, VectorLayer};
|
||||||
use kurbo::Affine;
|
use kurbo::{Affine, Shape};
|
||||||
use vello::kurbo::Rect;
|
use vello::kurbo::Rect;
|
||||||
use vello::peniko::Fill;
|
use vello::peniko::Fill;
|
||||||
use vello::Scene;
|
use vello::Scene;
|
||||||
|
|
@ -133,6 +133,28 @@ fn render_vector_layer(document: &Document, layer: &VectorLayer, scene: &mut Sce
|
||||||
time,
|
time,
|
||||||
transform.scale_y,
|
transform.scale_y,
|
||||||
);
|
);
|
||||||
|
let skew_x = layer
|
||||||
|
.layer
|
||||||
|
.animation_data
|
||||||
|
.eval(
|
||||||
|
&crate::animation::AnimationTarget::Object {
|
||||||
|
id: object.id,
|
||||||
|
property: TransformProperty::SkewX,
|
||||||
|
},
|
||||||
|
time,
|
||||||
|
transform.skew_x,
|
||||||
|
);
|
||||||
|
let skew_y = layer
|
||||||
|
.layer
|
||||||
|
.animation_data
|
||||||
|
.eval(
|
||||||
|
&crate::animation::AnimationTarget::Object {
|
||||||
|
id: object.id,
|
||||||
|
property: TransformProperty::SkewY,
|
||||||
|
},
|
||||||
|
time,
|
||||||
|
transform.skew_y,
|
||||||
|
);
|
||||||
let opacity = layer
|
let opacity = layer
|
||||||
.layer
|
.layer
|
||||||
.animation_data
|
.animation_data
|
||||||
|
|
@ -162,9 +184,40 @@ fn render_vector_layer(document: &Document, layer: &VectorLayer, scene: &mut Sce
|
||||||
let path = shape.get_morphed_path(shape_index);
|
let path = shape.get_morphed_path(shape_index);
|
||||||
|
|
||||||
// Build transform matrix (compose with base transform for camera)
|
// Build transform matrix (compose with base transform for camera)
|
||||||
|
// Get shape center for skewing around center
|
||||||
|
let shape_bbox = shape.path().bounding_box();
|
||||||
|
let center_x = (shape_bbox.x0 + shape_bbox.x1) / 2.0;
|
||||||
|
let center_y = (shape_bbox.y0 + shape_bbox.y1) / 2.0;
|
||||||
|
|
||||||
|
// Build skew transforms (applied around shape center)
|
||||||
|
let skew_transform = if skew_x != 0.0 || skew_y != 0.0 {
|
||||||
|
let skew_x_affine = if skew_x != 0.0 {
|
||||||
|
let tan_skew = skew_x.to_radians().tan();
|
||||||
|
Affine::new([1.0, 0.0, tan_skew, 1.0, 0.0, 0.0])
|
||||||
|
} else {
|
||||||
|
Affine::IDENTITY
|
||||||
|
};
|
||||||
|
|
||||||
|
let skew_y_affine = if skew_y != 0.0 {
|
||||||
|
let tan_skew = skew_y.to_radians().tan();
|
||||||
|
Affine::new([1.0, tan_skew, 0.0, 1.0, 0.0, 0.0])
|
||||||
|
} else {
|
||||||
|
Affine::IDENTITY
|
||||||
|
};
|
||||||
|
|
||||||
|
// Skew around center: translate to origin, skew, translate back
|
||||||
|
Affine::translate((center_x, center_y))
|
||||||
|
* skew_x_affine
|
||||||
|
* skew_y_affine
|
||||||
|
* Affine::translate((-center_x, -center_y))
|
||||||
|
} else {
|
||||||
|
Affine::IDENTITY
|
||||||
|
};
|
||||||
|
|
||||||
let object_transform = Affine::translate((x, y))
|
let object_transform = Affine::translate((x, y))
|
||||||
* Affine::rotate(rotation.to_radians())
|
* Affine::rotate(rotation.to_radians())
|
||||||
* Affine::scale_non_uniform(scale_x, scale_y);
|
* Affine::scale_non_uniform(scale_x, scale_y)
|
||||||
|
* skew_transform;
|
||||||
let affine = base_transform * object_transform;
|
let affine = base_transform * object_transform;
|
||||||
|
|
||||||
// Calculate final opacity (layer * object)
|
// Calculate final opacity (layer * object)
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,8 @@ pub enum TransformMode {
|
||||||
ScaleEdge { axis: Axis, origin: Point },
|
ScaleEdge { axis: Axis, origin: Point },
|
||||||
/// Rotate around a pivot
|
/// Rotate around a pivot
|
||||||
Rotate { center: Point },
|
Rotate { center: Point },
|
||||||
|
/// Skew along an edge
|
||||||
|
Skew { axis: Axis, origin: Point },
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Axis for edge scaling
|
/// Axis for edge scaling
|
||||||
|
|
|
||||||
|
|
@ -694,10 +694,38 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
|
||||||
vello::kurbo::Point::new(local_bbox.x0, local_bbox.y1), // Bottom-left
|
vello::kurbo::Point::new(local_bbox.x0, local_bbox.y1), // Bottom-left
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Build skew transforms around shape center
|
||||||
|
let center_x = (local_bbox.x0 + local_bbox.x1) / 2.0;
|
||||||
|
let center_y = (local_bbox.y0 + local_bbox.y1) / 2.0;
|
||||||
|
|
||||||
|
let skew_transform = if object.transform.skew_x != 0.0 || object.transform.skew_y != 0.0 {
|
||||||
|
let skew_x_affine = if object.transform.skew_x != 0.0 {
|
||||||
|
let tan_skew = object.transform.skew_x.to_radians().tan();
|
||||||
|
Affine::new([1.0, 0.0, tan_skew, 1.0, 0.0, 0.0])
|
||||||
|
} else {
|
||||||
|
Affine::IDENTITY
|
||||||
|
};
|
||||||
|
|
||||||
|
let skew_y_affine = if object.transform.skew_y != 0.0 {
|
||||||
|
let tan_skew = object.transform.skew_y.to_radians().tan();
|
||||||
|
Affine::new([1.0, tan_skew, 0.0, 1.0, 0.0, 0.0])
|
||||||
|
} else {
|
||||||
|
Affine::IDENTITY
|
||||||
|
};
|
||||||
|
|
||||||
|
Affine::translate((center_x, center_y))
|
||||||
|
* skew_x_affine
|
||||||
|
* skew_y_affine
|
||||||
|
* Affine::translate((-center_x, -center_y))
|
||||||
|
} else {
|
||||||
|
Affine::IDENTITY
|
||||||
|
};
|
||||||
|
|
||||||
// Transform to world space
|
// Transform to world space
|
||||||
let obj_transform = Affine::translate((object.transform.x, object.transform.y))
|
let obj_transform = Affine::translate((object.transform.x, object.transform.y))
|
||||||
* Affine::rotate(object.transform.rotation.to_radians())
|
* Affine::rotate(object.transform.rotation.to_radians())
|
||||||
* Affine::scale_non_uniform(object.transform.scale_x, object.transform.scale_y);
|
* Affine::scale_non_uniform(object.transform.scale_x, object.transform.scale_y)
|
||||||
|
* skew_transform;
|
||||||
|
|
||||||
let world_corners: Vec<vello::kurbo::Point> = local_corners
|
let world_corners: Vec<vello::kurbo::Point> = local_corners
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -2228,6 +2256,46 @@ impl StagePane {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TransformMode::Skew { axis, origin } => {
|
||||||
|
// Calculate skew amount based on parallel mouse movement (drag along edge)
|
||||||
|
// Convert mouse movement to skew angle in degrees
|
||||||
|
let skew_degrees = match axis {
|
||||||
|
Axis::Horizontal => {
|
||||||
|
// Horizontal edge: drag horizontally to skew
|
||||||
|
let delta_x = current_mouse.x - start_mouse.x;
|
||||||
|
// Calculate skew angle based on movement
|
||||||
|
delta_x / 2.0 // Sensitivity: 2 pixels = 1 degree
|
||||||
|
}
|
||||||
|
Axis::Vertical => {
|
||||||
|
// Vertical edge: drag vertically to skew
|
||||||
|
let delta_y = current_mouse.y - start_mouse.y;
|
||||||
|
delta_y / 2.0 // Sensitivity: 2 pixels = 1 degree
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply skew to all selected objects
|
||||||
|
for (object_id, original_transform) in original_transforms {
|
||||||
|
vector_layer.modify_object_internal(object_id, |obj| {
|
||||||
|
// Set skew based on axis
|
||||||
|
match axis {
|
||||||
|
Axis::Horizontal => {
|
||||||
|
obj.transform.skew_x = original_transform.skew_x + skew_degrees;
|
||||||
|
}
|
||||||
|
Axis::Vertical => {
|
||||||
|
obj.transform.skew_y = original_transform.skew_y + skew_degrees;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep other transform properties unchanged
|
||||||
|
obj.transform.x = original_transform.x;
|
||||||
|
obj.transform.y = original_transform.y;
|
||||||
|
obj.transform.rotation = original_transform.rotation;
|
||||||
|
obj.transform.scale_x = original_transform.scale_x;
|
||||||
|
obj.transform.scale_y = original_transform.scale_y;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2285,6 +2353,52 @@ impl StagePane {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for skew (hovering over edge but not near a handle)
|
||||||
|
// Define edge segments
|
||||||
|
let edge_segments = [
|
||||||
|
// Top edge
|
||||||
|
(Point::new(bbox.x0, bbox.y0), Point::new(bbox.x1, bbox.y0), Axis::Horizontal, bbox.y1),
|
||||||
|
// Right edge
|
||||||
|
(Point::new(bbox.x1, bbox.y0), Point::new(bbox.x1, bbox.y1), Axis::Vertical, bbox.x0),
|
||||||
|
// Bottom edge
|
||||||
|
(Point::new(bbox.x1, bbox.y1), Point::new(bbox.x0, bbox.y1), Axis::Horizontal, bbox.y0),
|
||||||
|
// Left edge
|
||||||
|
(Point::new(bbox.x0, bbox.y1), Point::new(bbox.x0, bbox.y0), Axis::Vertical, bbox.x1),
|
||||||
|
];
|
||||||
|
|
||||||
|
let skew_tolerance = tolerance * 1.5; // Slightly larger tolerance for edge detection
|
||||||
|
for (start, end, axis, origin_coord) in &edge_segments {
|
||||||
|
// Calculate distance from point to line segment
|
||||||
|
let edge_vec = *end - *start;
|
||||||
|
let point_vec = point - *start;
|
||||||
|
let edge_length = edge_vec.hypot();
|
||||||
|
|
||||||
|
if edge_length > 0.0 {
|
||||||
|
// Project point onto line segment
|
||||||
|
let t = (point_vec.x * edge_vec.x + point_vec.y * edge_vec.y) / (edge_length * edge_length);
|
||||||
|
|
||||||
|
// Check if projection is within segment bounds (not at ends where handles are)
|
||||||
|
let handle_exclusion = tolerance / edge_length; // Exclude regions near handles
|
||||||
|
|
||||||
|
if t > handle_exclusion && t < (1.0 - handle_exclusion) {
|
||||||
|
// Calculate perpendicular distance to edge
|
||||||
|
let closest_point = *start + edge_vec * t;
|
||||||
|
let distance = point.distance(closest_point);
|
||||||
|
|
||||||
|
if distance < skew_tolerance {
|
||||||
|
let origin = match axis {
|
||||||
|
Axis::Horizontal => Point::new(point.x, *origin_coord),
|
||||||
|
Axis::Vertical => Point::new(*origin_coord, point.y),
|
||||||
|
};
|
||||||
|
return Some(TransformMode::Skew {
|
||||||
|
axis: *axis,
|
||||||
|
origin,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2365,6 +2479,43 @@ impl StagePane {
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Set cursor based on hovering over handles
|
||||||
|
let tolerance = 10.0;
|
||||||
|
if let Some(mode) = Self::hit_test_transform_handle(point, bbox, tolerance) {
|
||||||
|
use lightningbeam_core::tool::TransformMode;
|
||||||
|
let cursor = match mode {
|
||||||
|
TransformMode::ScaleCorner { origin } => {
|
||||||
|
// Determine which corner based on origin
|
||||||
|
if (origin.x - bbox.x0).abs() < 0.1 && (origin.y - bbox.y0).abs() < 0.1 {
|
||||||
|
egui::CursorIcon::ResizeNwSe // Top-left
|
||||||
|
} else if (origin.x - bbox.x1).abs() < 0.1 && (origin.y - bbox.y0).abs() < 0.1 {
|
||||||
|
egui::CursorIcon::ResizeNeSw // Top-right
|
||||||
|
} else if (origin.x - bbox.x1).abs() < 0.1 && (origin.y - bbox.y1).abs() < 0.1 {
|
||||||
|
egui::CursorIcon::ResizeNwSe // Bottom-right
|
||||||
|
} else {
|
||||||
|
egui::CursorIcon::ResizeNeSw // Bottom-left
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TransformMode::ScaleEdge { axis, .. } => {
|
||||||
|
use lightningbeam_core::tool::Axis;
|
||||||
|
match axis {
|
||||||
|
Axis::Horizontal => egui::CursorIcon::ResizeHorizontal,
|
||||||
|
Axis::Vertical => egui::CursorIcon::ResizeVertical,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TransformMode::Rotate { .. } => egui::CursorIcon::AllScroll,
|
||||||
|
TransformMode::Skew { axis, .. } => {
|
||||||
|
use lightningbeam_core::tool::Axis;
|
||||||
|
// Use Move cursor to indicate skew
|
||||||
|
match axis {
|
||||||
|
Axis::Horizontal => egui::CursorIcon::ResizeHorizontal,
|
||||||
|
Axis::Vertical => egui::CursorIcon::ResizeVertical,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ui.ctx().set_cursor_icon(cursor);
|
||||||
|
}
|
||||||
|
|
||||||
// Mouse down: check if clicking on a handle
|
// Mouse down: check if clicking on a handle
|
||||||
if response.drag_started() || response.clicked() {
|
if response.drag_started() || response.clicked() {
|
||||||
let tolerance = 10.0; // Click tolerance in world space
|
let tolerance = 10.0; // Click tolerance in world space
|
||||||
|
|
@ -2485,9 +2636,37 @@ impl StagePane {
|
||||||
vello::kurbo::Point::new(local_bbox.x0, local_bbox.y1),
|
vello::kurbo::Point::new(local_bbox.x0, local_bbox.y1),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Build skew transforms around shape center
|
||||||
|
let center_x = (local_bbox.x0 + local_bbox.x1) / 2.0;
|
||||||
|
let center_y = (local_bbox.y0 + local_bbox.y1) / 2.0;
|
||||||
|
|
||||||
|
let skew_transform = if object.transform.skew_x != 0.0 || object.transform.skew_y != 0.0 {
|
||||||
|
let skew_x_affine = if object.transform.skew_x != 0.0 {
|
||||||
|
let tan_skew = object.transform.skew_x.to_radians().tan();
|
||||||
|
Affine::new([1.0, 0.0, tan_skew, 1.0, 0.0, 0.0])
|
||||||
|
} else {
|
||||||
|
Affine::IDENTITY
|
||||||
|
};
|
||||||
|
|
||||||
|
let skew_y_affine = if object.transform.skew_y != 0.0 {
|
||||||
|
let tan_skew = object.transform.skew_y.to_radians().tan();
|
||||||
|
Affine::new([1.0, tan_skew, 0.0, 1.0, 0.0, 0.0])
|
||||||
|
} else {
|
||||||
|
Affine::IDENTITY
|
||||||
|
};
|
||||||
|
|
||||||
|
Affine::translate((center_x, center_y))
|
||||||
|
* skew_x_affine
|
||||||
|
* skew_y_affine
|
||||||
|
* Affine::translate((-center_x, -center_y))
|
||||||
|
} else {
|
||||||
|
Affine::IDENTITY
|
||||||
|
};
|
||||||
|
|
||||||
let obj_transform = Affine::translate((object.transform.x, object.transform.y))
|
let obj_transform = Affine::translate((object.transform.x, object.transform.y))
|
||||||
* Affine::rotate(object.transform.rotation.to_radians())
|
* Affine::rotate(object.transform.rotation.to_radians())
|
||||||
* Affine::scale_non_uniform(object.transform.scale_x, object.transform.scale_y);
|
* Affine::scale_non_uniform(object.transform.scale_x, object.transform.scale_y)
|
||||||
|
* skew_transform;
|
||||||
|
|
||||||
let world_corners: Vec<vello::kurbo::Point> = local_corners
|
let world_corners: Vec<vello::kurbo::Point> = local_corners
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -2564,6 +2743,52 @@ impl StagePane {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for skew (hovering over edge but not near handles)
|
||||||
|
if !hovering_handle {
|
||||||
|
let skew_tolerance = tolerance * 1.5;
|
||||||
|
|
||||||
|
// Check each edge
|
||||||
|
for i in 0..4 {
|
||||||
|
let start = world_corners[i];
|
||||||
|
let end = world_corners[(i + 1) % 4];
|
||||||
|
let edge_midpoint = edge_midpoints[i];
|
||||||
|
|
||||||
|
// Calculate distance from point to line segment
|
||||||
|
let edge_vec = end - start;
|
||||||
|
let point_vec = point - start;
|
||||||
|
let edge_length = edge_vec.hypot();
|
||||||
|
|
||||||
|
if edge_length > 0.0 {
|
||||||
|
// Project point onto line segment
|
||||||
|
let t = (point_vec.x * edge_vec.x + point_vec.y * edge_vec.y) / (edge_length * edge_length);
|
||||||
|
|
||||||
|
// Check if projection is within segment bounds
|
||||||
|
if t > 0.0 && t < 1.0 {
|
||||||
|
let closest_point = start + edge_vec * t;
|
||||||
|
let distance = point.distance(closest_point);
|
||||||
|
|
||||||
|
// Check if close to edge but not near corner or midpoint handles
|
||||||
|
if distance < skew_tolerance {
|
||||||
|
let near_corner = point.distance(start) < tolerance || point.distance(end) < tolerance;
|
||||||
|
let near_midpoint = point.distance(edge_midpoint) < tolerance;
|
||||||
|
|
||||||
|
if !near_corner && !near_midpoint {
|
||||||
|
// Show skew cursor
|
||||||
|
let cursor = match i {
|
||||||
|
0 | 2 => egui::CursorIcon::ResizeHorizontal, // Top/Bottom edges
|
||||||
|
1 | 3 => egui::CursorIcon::ResizeVertical, // Right/Left edges
|
||||||
|
_ => egui::CursorIcon::Default,
|
||||||
|
};
|
||||||
|
ui.ctx().set_cursor_icon(cursor);
|
||||||
|
hovering_handle = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Mouse down: hit test handles (using the same handle positions and order as cursor logic) ===
|
// === Mouse down: hit test handles (using the same handle positions and order as cursor logic) ===
|
||||||
|
|
@ -2651,6 +2876,62 @@ impl StagePane {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for skew (same logic as cursor hover)
|
||||||
|
let skew_tolerance = tolerance * 1.5;
|
||||||
|
for i in 0..4 {
|
||||||
|
let start = world_corners[i];
|
||||||
|
let end = world_corners[(i + 1) % 4];
|
||||||
|
let edge_midpoint = edge_midpoints[i];
|
||||||
|
|
||||||
|
let edge_vec = end - start;
|
||||||
|
let point_vec = point - start;
|
||||||
|
let edge_length = edge_vec.hypot();
|
||||||
|
|
||||||
|
if edge_length > 0.0 {
|
||||||
|
let t = (point_vec.x * edge_vec.x + point_vec.y * edge_vec.y) / (edge_length * edge_length);
|
||||||
|
|
||||||
|
if t > 0.0 && t < 1.0 {
|
||||||
|
let closest_point = start + edge_vec * t;
|
||||||
|
let distance = point.distance(closest_point);
|
||||||
|
|
||||||
|
if distance < skew_tolerance {
|
||||||
|
let near_corner = point.distance(start) < tolerance || point.distance(end) < tolerance;
|
||||||
|
let near_midpoint = point.distance(edge_midpoint) < tolerance;
|
||||||
|
|
||||||
|
if !near_corner && !near_midpoint {
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use lightningbeam_core::tool::Axis;
|
||||||
|
|
||||||
|
let mut original_transforms = HashMap::new();
|
||||||
|
original_transforms.insert(object_id, object.transform.clone());
|
||||||
|
|
||||||
|
// Determine skew axis and origin
|
||||||
|
let (axis, opposite_edge) = match i {
|
||||||
|
0 => (Axis::Horizontal, edge_midpoints[2]), // Top edge
|
||||||
|
1 => (Axis::Vertical, edge_midpoints[3]), // Right edge
|
||||||
|
2 => (Axis::Horizontal, edge_midpoints[0]), // Bottom edge
|
||||||
|
3 => (Axis::Vertical, edge_midpoints[1]), // Left edge
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
*shared.tool_state = ToolState::Transforming {
|
||||||
|
mode: lightningbeam_core::tool::TransformMode::Skew {
|
||||||
|
axis,
|
||||||
|
origin: opposite_edge,
|
||||||
|
},
|
||||||
|
original_transforms,
|
||||||
|
pivot: opposite_edge,
|
||||||
|
start_mouse: point,
|
||||||
|
current_mouse: point,
|
||||||
|
original_bbox: vello::kurbo::Rect::new(local_bbox.x0, local_bbox.y0, local_bbox.x1, local_bbox.y1),
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mouse drag: apply transform in local space
|
// Mouse drag: apply transform in local space
|
||||||
|
|
@ -2859,6 +3140,51 @@ impl StagePane {
|
||||||
obj.transform.rotation = original.rotation;
|
obj.transform.rotation = original.rotation;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
lightningbeam_core::tool::TransformMode::Skew { axis, origin } => {
|
||||||
|
// Transform mouse positions to local space to get skew amount
|
||||||
|
let original_transform = Affine::translate((original.x, original.y))
|
||||||
|
* Affine::rotate(original.rotation.to_radians())
|
||||||
|
* Affine::scale_non_uniform(original.scale_x, original.scale_y);
|
||||||
|
let inv_original_transform = original_transform.inverse();
|
||||||
|
|
||||||
|
// Get mouse movement in local space
|
||||||
|
let local_start = inv_original_transform * start_mouse;
|
||||||
|
let local_current = inv_original_transform * point;
|
||||||
|
|
||||||
|
use lightningbeam_core::tool::Axis;
|
||||||
|
// Calculate skew angle in degrees based on mouse movement
|
||||||
|
let skew_degrees = match axis {
|
||||||
|
Axis::Horizontal => {
|
||||||
|
// Horizontal edge: drag horizontally (in local space) to skew
|
||||||
|
let delta_x = local_current.x - local_start.x;
|
||||||
|
delta_x / 2.0 // Sensitivity: 2 pixels = 1 degree
|
||||||
|
}
|
||||||
|
Axis::Vertical => {
|
||||||
|
// Vertical edge: drag vertically (in local space) to skew
|
||||||
|
let delta_y = local_current.y - local_start.y;
|
||||||
|
delta_y / 2.0 // Sensitivity: 2 pixels = 1 degree
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
vector_layer.modify_object_internal(&object_id, |obj| {
|
||||||
|
// Apply skew based on axis
|
||||||
|
match axis {
|
||||||
|
Axis::Horizontal => {
|
||||||
|
obj.transform.skew_x = original.skew_x + skew_degrees;
|
||||||
|
}
|
||||||
|
Axis::Vertical => {
|
||||||
|
obj.transform.skew_y = original.skew_y + skew_degrees;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep other transform properties unchanged
|
||||||
|
obj.transform.x = original.x;
|
||||||
|
obj.transform.y = original.y;
|
||||||
|
obj.transform.rotation = original.rotation;
|
||||||
|
obj.transform.scale_x = original.scale_x;
|
||||||
|
obj.transform.scale_y = original.scale_y;
|
||||||
|
});
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue