M8.12: Perspective Selection tool scales, copies, moves perpendicular and nudges in perspective

The Perspective Selection tool shows the selection's bounding box in perspective; its handles
scale in plane space (perspective.transform; Shift proportional, Alt from the centre). Alt-drag
moves copies that stay attached, pressing 5 while dragging moves along the plane's normal
(ToolKey::Digit, delivered by the canvas and MCP press_key), and the arrow keys nudge in
perspective (perspective.nudge). Transform Again repeats the last perspective move or scale in
plane space. Attachments (plane and depth) now live on the objects (Node.perspective), so copies,
duplicates and pastes keep them; grids from older files hand theirs over. Homography moves to
vectorcraft-geom. Hint-bar text for the tool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic
2026-10-06 11:00:15 +07:00
co-authored by Claude Opus 5.5
parent 7df7cf568a
commit a0847e8a25
18 changed files with 1250 additions and 140 deletions
+2
View File
@@ -19,6 +19,7 @@ pub mod node;
pub mod orient;
pub mod overprint;
pub mod pattern;
pub mod perspective;
mod pixels;
pub mod profiles;
pub mod range;
@@ -65,6 +66,7 @@ pub use node::{ImageMap, ObjectAttributes};
pub use node::{ImageObject, LAYER_COLORS, LayerColor, LiveShape, Node, NodeId, NodeKind, OpacityMask};
pub use orient::OrientedBox;
pub use pattern::{Overlap, PatternDef, PatternEdit, RepeatKind, RepeatSpec, TileType};
pub use perspective::PerspectiveAttachment;
pub use profiles::ColorProfiles;
pub use rastersettings::{RasterColorModel, RasterEffectsSettings};
pub use selection::{AnchorRef, Selection};
+4
View File
@@ -343,6 +343,9 @@ pub struct Node {
/// [`crate::orient`]); Reset Bounding Box sets it back to 0.
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
pub bbox_angle: f64,
/// Object › Perspective: the perspective grid plane the object is attached to.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub perspective: Option<Box<crate::PerspectiveAttachment>>,
}
/// Opacity mask: the luminance of the mask art sets the object's opacity (white = opaque).
@@ -392,6 +395,7 @@ impl Node {
attrs: None,
slice: None,
bbox_angle: 0.0,
perspective: None,
}
}
pub fn path(id: NodeId, path: PathData, appearance: Appearance) -> Self {
+26
View File
@@ -0,0 +1,26 @@
//! Perspective Grid attachments (Object › Perspective).
//!
//! The grid itself is document data (`Document.unknown["perspectiveGrid"]`, modelled by the tools
//! crate). Which plane an object lies on is kept on the object, so copies, duplicates and pastes
//! stay attached and deleted objects leave nothing stale behind.
use serde::{Deserialize, Serialize};
/// The plane an object in perspective lies on.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PerspectiveAttachment {
/// `left`, `right` or `ground`.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub plane: String,
/// Where along the plane's normal (points) the parallel plane the object lies on is: 0 is the
/// grid plane in its original place.
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
pub depth: f64,
}
impl PerspectiveAttachment {
pub fn new(plane: &str, depth: f64) -> Self {
Self { plane: plane.to_string(), depth: if depth.is_finite() { depth } else { 0.0 } }
}
}