mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 13:07:41 +00:00
A projection now counts as invertible when its determinant, or its adjugate's determinant, is more than 1e-15 times the largest of the six products that determinant sums. The adjugate of the adjugate is the map times a number, so the inverse of a projection that inverts also inverts, is stored and reads back. The adjugate's cofactors are formed with exact products, so the inverse of that inverse gives back the original map up to scale, within 1e-6 of each row's largest entry, and a map whose third row is the sum of its first two is still refused. Homography::conjugated and Node::transform_projection judged the affine map by an absolute determinant of 1e-15, so a scale by 1e-8 left type and symbol instances in perspective with their old projection. Both now use Homography::affine_is_invertible, which compares the determinant with the two products it sums at any scale of the map. An affine map whose determinant or inverse is not finite, such as a scale by 1e200 or 1e-200, is inverted through Homography::inverse. When the direct product underflows to a map that is not invertible, as for a projection stored at a subnormal scale, conjugated forms it again with its powers of two kept apart. - crates/geom/src/projective.rs: the_inverse_of_an_invertible_map_is_invertible, conjugating_does_not_depend_on_the_scale_of_the_affine, conjugating_a_map_at_a_tiny_scale_gives_an_invertible_map - crates/doc/src/node/tests.rs: a_projection_follows_the_transform_or_is_removed covers an invertible scale by 1e-8 and a singular map at that size
72 lines
3.0 KiB
Rust
72 lines
3.0 KiB
Rust
//! 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};
|
||
use vectorcraft_geom::{Homography, Rect};
|
||
|
||
use crate::{Node, NodeKind};
|
||
|
||
/// 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,
|
||
/// Type and symbol instances: the projective map (row-major 3 × 3) that draws their flat art
|
||
/// in perspective. They stay type and symbol instances; the renderer and exporters project
|
||
/// their outlines.
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub projection: Option<[f64; 9]>,
|
||
/// Object › Perspective › Edit Text in progress: the type is drawn flat to be edited (not
|
||
/// saved).
|
||
#[serde(skip)]
|
||
pub editing: bool,
|
||
}
|
||
|
||
impl PerspectiveAttachment {
|
||
pub fn new(plane: &str, depth: f64) -> Self {
|
||
Self { plane: plane.to_string(), depth: if depth.is_finite() { depth } else { 0.0 }, ..Self::default() }
|
||
}
|
||
|
||
/// The projection, when it is a valid one.
|
||
pub fn homography(&self) -> Option<Homography> {
|
||
self.projection.and_then(Homography::from_array)
|
||
}
|
||
}
|
||
|
||
impl Node {
|
||
/// The projective map type or a symbol instance in perspective is drawn through (not while
|
||
/// Edit Text shows it flat).
|
||
pub fn projection(&self) -> Option<Homography> {
|
||
let a = self.extras().perspective.as_deref().filter(|a| !a.editing)?;
|
||
matches!(self.kind, NodeKind::Text(_) | NodeKind::SymbolInstance { .. }).then(|| a.homography()).flatten()
|
||
}
|
||
|
||
/// `b` (flat bounds) as drawn: the box of its projection for type and symbols in perspective.
|
||
pub(crate) fn projected(&self, b: Option<Rect>) -> Option<Rect> {
|
||
match (self.projection(), b) {
|
||
(Some(h), Some(b)) => h.map_rect_bbox(b).or(Some(b)),
|
||
(_, b) => b,
|
||
}
|
||
}
|
||
|
||
/// Keep a projection drawing the same picture moved by `a` (the flat art moves by `a` too). An
|
||
/// invertible `a` that the projection cannot follow removes it, and the art is drawn flat.
|
||
pub(crate) fn transform_projection(&mut self, a: vectorcraft_geom::Affine) {
|
||
if let Some(p) = self.extras_if_any().and_then(|e| e.perspective.as_deref_mut())
|
||
&& let Some(h) = p.homography()
|
||
&& Homography::affine_is_invertible(a)
|
||
{
|
||
p.projection = h.conjugated(a).map(|h| h.to_array());
|
||
}
|
||
}
|
||
}
|