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
@@ -245,23 +245,40 @@ impl WireModel {
|
||||
|
||||
/// Return a clone with every point rotated around `center` by `angle_rad`.
|
||||
pub fn rotated(&self, center: glam::Vec3, angle_rad: f32) -> Self {
|
||||
self.rotated_about_axis(center, glam::Vec3::Z, angle_rad)
|
||||
}
|
||||
|
||||
/// Return a clone rotated about an arbitrary world-space axis.
|
||||
pub fn rotated_about_axis(
|
||||
&self,
|
||||
center: glam::Vec3,
|
||||
axis: glam::Vec3,
|
||||
angle_rad: f32,
|
||||
) -> Self {
|
||||
let rotation = glam::Quat::from_axis_angle(axis.normalize_or_zero(), angle_rad);
|
||||
let (s, c) = angle_rad.sin_cos();
|
||||
let mut out = self.clone();
|
||||
out.name = format!("preview_{}", self.name);
|
||||
out.color = Self::CYAN;
|
||||
out.selected = false;
|
||||
for p in &mut out.points {
|
||||
let dx = p[0] - center.x;
|
||||
let dy = p[1] - center.y;
|
||||
p[0] = center.x + dx * c - dy * s;
|
||||
p[1] = center.y + dx * s + dy * c;
|
||||
let mapped = center + rotation * (glam::Vec3::from_array(*p) - center);
|
||||
*p = mapped.to_array();
|
||||
}
|
||||
for p in &mut out.points_low {
|
||||
*p = (rotation * glam::Vec3::from_array(*p)).to_array();
|
||||
}
|
||||
if !out.text_verts.is_empty() {
|
||||
let (cx, cy) = (center.x as f64, center.y as f64);
|
||||
let center = center.as_dvec3();
|
||||
let axis = axis.as_dvec3().normalize_or_zero();
|
||||
let (s, c) = (s as f64, c as f64);
|
||||
out.text_verts = map_text_verts(&self.text_verts, |x, y, z| {
|
||||
let (dx, dy) = (x - cx, y - cy);
|
||||
(cx + dx * c - dy * s, cy + dx * s + dy * c, z)
|
||||
let v = glam::DVec3::new(x, y, z) - center;
|
||||
let mapped = center
|
||||
+ v * c
|
||||
+ axis.cross(v) * s
|
||||
+ axis * axis.dot(v) * (1.0 - c);
|
||||
(mapped.x, mapped.y, mapped.z)
|
||||
});
|
||||
}
|
||||
out
|
||||
@@ -322,41 +339,48 @@ impl WireModel {
|
||||
|
||||
/// Return a clone mirrored across the line through `p1`→`p2`.
|
||||
pub fn mirrored(&self, p1: glam::Vec3, p2: glam::Vec3) -> Self {
|
||||
let ax = p2.x - p1.x;
|
||||
let ay = p2.y - p1.y;
|
||||
let len2 = ax * ax + ay * ay;
|
||||
self.mirrored_in_plane(p1, p2, glam::Vec3::Z)
|
||||
}
|
||||
|
||||
/// Return a clone reflected through the plane containing the picked line
|
||||
/// and the supplied working-plane normal.
|
||||
pub fn mirrored_in_plane(
|
||||
&self,
|
||||
p1: glam::Vec3,
|
||||
p2: glam::Vec3,
|
||||
working_normal: glam::Vec3,
|
||||
) -> Self {
|
||||
let plane_normal = (p2 - p1)
|
||||
.cross(working_normal.normalize_or_zero())
|
||||
.normalize_or_zero();
|
||||
let mut out = self.clone();
|
||||
out.name = format!("preview_{}", self.name);
|
||||
out.color = Self::CYAN;
|
||||
out.selected = false;
|
||||
if len2 < 1e-12 {
|
||||
if plane_normal.length_squared() < 1e-12 {
|
||||
return out;
|
||||
}
|
||||
for p in &mut out.points {
|
||||
let dx = p[0] - p1.x;
|
||||
let dy = p[1] - p1.y;
|
||||
let t = (dx * ax + dy * ay) / len2;
|
||||
p[0] = p1.x + 2.0 * t * ax - dx;
|
||||
p[1] = p1.y + 2.0 * t * ay - dy;
|
||||
let point = glam::Vec3::from_array(*p);
|
||||
*p = (point - 2.0 * plane_normal.dot(point - p1) * plane_normal).to_array();
|
||||
}
|
||||
// World position is the double-single sum `points + points_low` (text /
|
||||
// UTM wires split it), so the residual must reflect too — as a direction
|
||||
// (linear reflection about the axis, no `p1` offset).
|
||||
for p in &mut out.points_low {
|
||||
let t = (p[0] * ax + p[1] * ay) / len2;
|
||||
p[0] = 2.0 * t * ax - p[0];
|
||||
p[1] = 2.0 * t * ay - p[1];
|
||||
let point = glam::Vec3::from_array(*p);
|
||||
*p = (point - 2.0 * plane_normal.dot(point) * plane_normal).to_array();
|
||||
}
|
||||
// Glyph quads reflect wholesale (true mirror) — the caller only routes
|
||||
// text through here for MIRRTEXT-on; MIRRTEXT-off relocates via
|
||||
// `translated` so glyphs stay readable.
|
||||
if !out.text_verts.is_empty() {
|
||||
let (ax, ay, len2) = (ax as f64, ay as f64, len2 as f64);
|
||||
let (p1x, p1y) = (p1.x as f64, p1.y as f64);
|
||||
let p1 = p1.as_dvec3();
|
||||
let normal = plane_normal.as_dvec3();
|
||||
out.text_verts = map_text_verts(&self.text_verts, |x, y, z| {
|
||||
let (dx, dy) = (x - p1x, y - p1y);
|
||||
let t = (dx * ax + dy * ay) / len2;
|
||||
(p1x + 2.0 * t * ax - dx, p1y + 2.0 * t * ay - dy, z)
|
||||
let point = glam::DVec3::new(x, y, z);
|
||||
let mapped = point - 2.0 * normal.dot(point - p1) * normal;
|
||||
(mapped.x, mapped.y, mapped.z)
|
||||
});
|
||||
}
|
||||
out
|
||||
|
||||
@@ -433,6 +433,15 @@ impl Scene {
|
||||
self.camera.borrow().rotation * glam::Vec3::Z
|
||||
}
|
||||
|
||||
pub fn active_camera_rotation(&self) -> glam::Quat {
|
||||
if let Some(handle) = self.active_viewport {
|
||||
if let Some(camera) = self.camera_for_viewport(handle) {
|
||||
return camera.rotation;
|
||||
}
|
||||
}
|
||||
self.camera.borrow().rotation
|
||||
}
|
||||
|
||||
/// View-rotation matrix for the active viewport (MSPACE), or the
|
||||
/// paper-space camera's matrix when not in MSPACE.
|
||||
/// Used by ViewCube hit-testing so clicks map to the correct camera.
|
||||
|
||||
@@ -83,6 +83,39 @@ pub fn properties_sectioned(
|
||||
sections
|
||||
}
|
||||
|
||||
pub fn entity_in_working_plane(
|
||||
entity: &EntityType,
|
||||
plane: crate::command::WorkingPlane,
|
||||
) -> EntityType {
|
||||
let mut local = entity.clone();
|
||||
if !plane.is_identity() {
|
||||
apply_transform(
|
||||
&mut local,
|
||||
&EntityTransform::Affine(plane.to_local_transform()),
|
||||
);
|
||||
}
|
||||
local
|
||||
}
|
||||
|
||||
pub fn apply_geom_prop_in_working_plane(
|
||||
entity: &mut EntityType,
|
||||
field: &str,
|
||||
value: &str,
|
||||
plane: crate::command::WorkingPlane,
|
||||
) {
|
||||
if plane.is_identity() {
|
||||
apply_geom_prop(entity, field, value);
|
||||
return;
|
||||
}
|
||||
let mut local = entity_in_working_plane(entity, plane);
|
||||
apply_geom_prop(&mut local, field, value);
|
||||
apply_transform(
|
||||
&mut local,
|
||||
&EntityTransform::Affine(plane.to_world_transform()),
|
||||
);
|
||||
*entity = local;
|
||||
}
|
||||
|
||||
pub fn apply_common_prop(entity: &mut EntityType, field: &str, value: &str) {
|
||||
let e = entity.as_entity_mut();
|
||||
match field {
|
||||
|
||||
+57
-21
@@ -8,13 +8,12 @@ fn to_v3(v: DVec3) -> Vector3 {
|
||||
Vector3::new(v.x, v.y, v.z)
|
||||
}
|
||||
|
||||
pub fn apply_standard_transform<T>(entity: &mut T, center: DVec3, angle_rad: f64)
|
||||
pub fn apply_standard_transform<T>(entity: &mut T, center: DVec3, axis: DVec3, angle_rad: f64)
|
||||
where
|
||||
T: acadrust::Entity,
|
||||
{
|
||||
let z_axis = Vector3::new(0.0, 0.0, 1.0);
|
||||
let t = Transform::from_translation(to_v3(-center))
|
||||
.then(&Transform::from_rotation(z_axis, angle_rad))
|
||||
.then(&Transform::from_rotation(to_v3(axis.normalize_or(DVec3::Z)), angle_rad))
|
||||
.then(&Transform::from_translation(to_v3(center)));
|
||||
entity.apply_transform(&t);
|
||||
}
|
||||
@@ -35,11 +34,25 @@ where
|
||||
{
|
||||
match t {
|
||||
EntityTransform::Translate(d) => entity.translate(to_v3(*d)),
|
||||
EntityTransform::Rotate { center, angle_rad } => {
|
||||
apply_standard_transform(entity, *center, *angle_rad)
|
||||
EntityTransform::Rotate { center, axis, angle_rad } => {
|
||||
apply_standard_transform(entity, *center, *axis, *angle_rad)
|
||||
}
|
||||
EntityTransform::Scale { center, factor } => apply_standard_scale(entity, *center, *factor),
|
||||
EntityTransform::Mirror { p1, p2 } => mirror(entity, *p1, *p2),
|
||||
EntityTransform::Mirror {
|
||||
p1,
|
||||
p2,
|
||||
working_normal,
|
||||
} => {
|
||||
if working_normal.normalize_or(DVec3::Z).abs_diff_eq(DVec3::Z, 1e-10) {
|
||||
mirror(entity, *p1, *p2)
|
||||
} else {
|
||||
entity.apply_transform(&reflection_about_working_line(
|
||||
*p1,
|
||||
*p2,
|
||||
*working_normal,
|
||||
));
|
||||
}
|
||||
}
|
||||
EntityTransform::Affine(transform) => entity.apply_transform(transform),
|
||||
}
|
||||
}
|
||||
@@ -49,19 +62,46 @@ where
|
||||
/// `apply_transform` paths (which handle direction flags, stored-angle
|
||||
/// conventions and bulges themselves). Degenerate line → identity.
|
||||
pub fn reflection_about_xy_line(p1: DVec3, p2: DVec3) -> acadrust::types::Transform {
|
||||
reflection_about_working_line(p1, p2, DVec3::Z)
|
||||
}
|
||||
|
||||
/// Reflection through the plane that contains the picked line and is
|
||||
/// perpendicular to the active working plane.
|
||||
pub fn reflection_about_working_line(
|
||||
p1: DVec3,
|
||||
p2: DVec3,
|
||||
working_normal: DVec3,
|
||||
) -> acadrust::types::Transform {
|
||||
use acadrust::types::{Matrix4, Transform};
|
||||
let dx = (p2.x - p1.x) as f64;
|
||||
let dy = (p2.y - p1.y) as f64;
|
||||
if dx * dx + dy * dy < 1e-12 {
|
||||
let line = p2 - p1;
|
||||
let plane_normal = line
|
||||
.cross(working_normal.normalize_or(DVec3::Z))
|
||||
.normalize_or(DVec3::ZERO);
|
||||
if plane_normal.length_squared() < 1e-12 {
|
||||
return Transform::identity();
|
||||
}
|
||||
let ang = dy.atan2(dx);
|
||||
let m = Matrix4::translation(p1.x as f64, p1.y as f64, 0.0)
|
||||
* Matrix4::rotation_z(ang)
|
||||
* Matrix4::scaling(1.0, -1.0, 1.0)
|
||||
* Matrix4::rotation_z(-ang)
|
||||
* Matrix4::translation(-(p1.x as f64), -(p1.y as f64), 0.0);
|
||||
Transform::from_matrix(m)
|
||||
let n = plane_normal;
|
||||
let d = 2.0 * n.dot(p1);
|
||||
Transform::from_matrix(Matrix4 {
|
||||
m: [
|
||||
[1.0 - 2.0 * n.x * n.x, -2.0 * n.x * n.y, -2.0 * n.x * n.z, d * n.x],
|
||||
[-2.0 * n.y * n.x, 1.0 - 2.0 * n.y * n.y, -2.0 * n.y * n.z, d * n.y],
|
||||
[-2.0 * n.z * n.x, -2.0 * n.z * n.y, 1.0 - 2.0 * n.z * n.z, d * n.z],
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
pub fn reflected_point(p: DVec3, p1: DVec3, p2: DVec3, working_normal: DVec3) -> DVec3 {
|
||||
let line = p2 - p1;
|
||||
let normal = line
|
||||
.cross(working_normal.normalize_or(DVec3::Z))
|
||||
.normalize_or(DVec3::ZERO);
|
||||
if normal.length_squared() < 1e-12 {
|
||||
p
|
||||
} else {
|
||||
p - 2.0 * normal.dot(p - p1) * normal
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reflect_xy_point(x: &mut f64, y: &mut f64, p1: DVec3, p2: DVec3) {
|
||||
@@ -80,11 +120,6 @@ pub fn reflect_xy_point(x: &mut f64, y: &mut f64, p1: DVec3, p2: DVec3) {
|
||||
*y = p1.y as f64 + my;
|
||||
}
|
||||
|
||||
pub fn mirror_xy_line(line: &mut acadrust::entities::Line, p1: DVec3, p2: DVec3) {
|
||||
reflect_xy_point(&mut line.start.x, &mut line.start.y, p1, p2);
|
||||
reflect_xy_point(&mut line.end.x, &mut line.end.y, p1, p2);
|
||||
}
|
||||
|
||||
/// DXF arbitrary-axis algorithm — returns the OCS X and Y basis vectors in WCS
|
||||
/// for a given entity normal vector.
|
||||
///
|
||||
@@ -190,6 +225,7 @@ mod mirror_delegation_tests {
|
||||
h.apply_transform(&EntityTransform::Mirror {
|
||||
p1: DVec3::new(5.0, 0.0, 0.0),
|
||||
p2: DVec3::new(5.0, 1.0, 0.0),
|
||||
working_normal: DVec3::Z,
|
||||
});
|
||||
|
||||
let edges = &h.paths[0].edges;
|
||||
|
||||
Reference in new issue
Block a user