forked from 1b-agent/opencadstudio-upstream
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:
1 parent
b1d55c6a7e
commit
657d023fb7
61 files changed
+2218
-747
No files matched your search
+143
-11
@@ -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
|
||||
|
||||
Reference in new issue
Block a user