feat(ucs): integrate coordinate systems

Route coordinate input, geometry creation, transforms, properties,\nviews, and viewport persistence through a shared working plane.\n\nRefs #484
This commit is contained in:
Hakan Seven committed 2026-08-04 23:17:54 +03:00
1 parent b1d55c6a7e
commit 657d023fb7
61 files changed
+2218 -747

No files matched your search

+143 -11
View File
@@ -11,6 +11,132 @@ use crate::scene::Scene;
use acadrust::{EntityType, Handle};
use glam::DVec3;
// ── Working plane ─────────────────────────────────────────────────────────
/// Full-precision coordinate frame used by interactive commands.
///
/// Points delivered to commands remain WCS points. Commands that construct
/// planar geometry use this frame for their local calculations, then convert
/// the result back to WCS for storage and preview.
#[derive(Clone, Copy, Debug)]
pub struct WorkingPlane {
pub origin: DVec3,
pub x: DVec3,
pub y: DVec3,
pub z: DVec3,
}
impl Default for WorkingPlane {
fn default() -> Self {
Self {
origin: DVec3::ZERO,
x: DVec3::X,
y: DVec3::Y,
z: DVec3::Z,
}
}
}
impl WorkingPlane {
pub fn is_identity(self) -> bool {
self.origin.abs_diff_eq(DVec3::ZERO, 1e-12)
&& self.x.abs_diff_eq(DVec3::X, 1e-12)
&& self.y.abs_diff_eq(DVec3::Y, 1e-12)
&& self.z.abs_diff_eq(DVec3::Z, 1e-12)
}
pub fn new(origin: DVec3, x: DVec3, y: DVec3) -> Self {
let x = x.normalize_or(DVec3::X);
let raw_y = y.normalize_or(DVec3::Y);
let fallback = if x.dot(DVec3::Z).abs() < 0.999 {
DVec3::Z
} else {
DVec3::Y
};
let z = x.cross(raw_y).normalize_or(x.cross(fallback).normalize());
let y = z.cross(x).normalize();
Self { origin, x, y, z }
}
pub fn to_world(self, point: DVec3) -> DVec3 {
self.origin + self.x * point.x + self.y * point.y + self.z * point.z
}
pub fn to_local(self, point: DVec3) -> DVec3 {
let delta = point - self.origin;
DVec3::new(delta.dot(self.x), delta.dot(self.y), delta.dot(self.z))
}
pub fn vector_to_world(self, vector: DVec3) -> DVec3 {
self.x * vector.x + self.y * vector.y + self.z * vector.z
}
pub fn vector_to_local(self, vector: DVec3) -> DVec3 {
DVec3::new(vector.dot(self.x), vector.dot(self.y), vector.dot(self.z))
}
pub fn angle(self, from: DVec3, to: DVec3) -> Option<f64> {
let direction = self.vector_to_local(to - from);
(direction.x.hypot(direction.y) > f64::EPSILON)
.then(|| direction.y.atan2(direction.x))
}
pub fn to_world_transform(self) -> acadrust::types::Transform {
use acadrust::types::{Matrix4, Transform};
Transform::from_matrix(Matrix4 {
m: [
[self.x.x, self.y.x, self.z.x, self.origin.x],
[self.x.y, self.y.y, self.z.y, self.origin.y],
[self.x.z, self.y.z, self.z.z, self.origin.z],
[0.0, 0.0, 0.0, 1.0],
],
})
}
pub fn to_local_transform(self) -> acadrust::types::Transform {
use acadrust::types::{Matrix4, Transform};
Transform::from_matrix(Matrix4 {
m: [
[self.x.x, self.x.y, self.x.z, -self.origin.dot(self.x)],
[self.y.x, self.y.y, self.y.z, -self.origin.dot(self.y)],
[self.z.x, self.z.y, self.z.z, -self.origin.dot(self.z)],
[0.0, 0.0, 0.0, 1.0],
],
})
}
pub fn place_entity(self, mut entity: EntityType) -> EntityType {
crate::scene::view::dispatch::apply_transform(
&mut entity,
&EntityTransform::Affine(self.to_world_transform()),
);
entity
}
}
#[cfg(test)]
mod working_plane_tests {
use super::*;
#[test]
fn translated_and_rotated_frame_round_trips_points_and_vectors() {
let plane = WorkingPlane::new(
DVec3::new(125_000.25, -42_000.5, 810.75),
DVec3::new(0.0, 1.0, 0.0),
DVec3::new(0.0, 0.0, 1.0),
);
let local = DVec3::new(12.5, -3.25, 7.75);
let vector = DVec3::new(-2.0, 5.0, 1.5);
assert!(plane
.to_local(plane.to_world(local))
.abs_diff_eq(local, 1e-9));
assert!(plane
.vector_to_local(plane.vector_to_world(vector))
.abs_diff_eq(vector, 1e-12));
}
}
/// Domain object resolved under the cursor for ObjectPick snapping.
#[derive(Clone, Copy, Debug)]
pub struct ObjectPickHit {
@@ -44,14 +170,22 @@ pub struct AreaPreviewRegion {
/// A geometric transformation applied to existing entities.
#[derive(Clone)]
pub enum EntityTransform {
/// Move every point by the given world-space delta (world XY plane).
/// Move every point by the given world-space delta.
Translate(DVec3),
/// Rotate around `center` by `angle_rad` in the world XY plane.
Rotate { center: DVec3, angle_rad: f64 },
/// Rotate around the axis through `center`.
Rotate {
center: DVec3,
axis: DVec3,
angle_rad: f64,
},
/// Uniform scale from `center` by `factor`.
Scale { center: DVec3, factor: f64 },
/// Mirror across the line through `p1`→`p2` in the world XY plane.
Mirror { p1: DVec3, p2: DVec3 },
/// Mirror through the plane containing `p1`→`p2` and the working normal.
Mirror {
p1: DVec3,
p2: DVec3,
working_normal: DVec3,
},
/// General affine transform. Used when a complete UCS basis must be baked
/// into block-local geometry instead of stored as drawing UCS state.
Affine(acadrust::types::Transform),
@@ -1296,12 +1430,10 @@ pub trait CadCommand: Send {
Vec::new()
}
/// Push the active UCS into the command as a UCS→render(wire)-space affine
/// (identity = plain WCS). Commands that build axis-aligned geometry (RECT,
/// rectangular ARRAY, …) override this to store it and rotate their implicit
/// axes into the UCS; most commands work purely from picked points and
/// ignore it. Called before each point / preview dispatch.
fn set_ucs(&mut self, _ucs: glam::Mat4) {}
/// Push the active coordinate frame in full precision. Geometry commands
/// use it for plane-local construction; inquiry and modify commands use it
/// for local deltas, angles and transformation axes.
fn set_working_plane(&mut self, _plane: WorkingPlane) {}
/// Push the live Ctrl-key state into the command before each preview/commit
/// dispatch. Commands that offer a Ctrl toggle (e.g. arc-direction flip on