feat(ucs): build axis-aligned shapes in the active UCS

Commands that construct geometry with implicit axis directions ignored
the UCS, so a rectangle drawn under a rotated UCS came out world-aligned.
Give commands the UCS and build square to it:

- CadCommand::set_ucs(Mat4) (UCS→wire affine, default no-op); the host
  pushes the current UCS via App::push_ucs_to_cmd before each point /
  preview / text dispatch (DocumentTab::ucs_wire_affine, identity outside
  model space). New axis-aligned commands just override set_ucs.
- RECT (two-corner) and RECT_CEN (center) place their derived corners on
  the UCS axes (ucs_box_corners / ucs_box_around_center).
- ARRAYRECT row/column offsets run along the UCS axes.

RECT_ROT, POLYGON, and ELLIPSE already take their orientation from picked
points, so they need no change.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-22 17:30:42 +03:00
1 parent d63602161f
commit 0c2a97e17e
5 files changed
+128 -35

No files matched your search

+22
View File
@@ -195,6 +195,28 @@ impl DocumentTab {
self.scene.viewcube_ucs = self.ucs_xform().rotation_mat();
}
/// UCS→render(wire)-space affine for commands that build axis-aligned
/// geometry. Columns are the UCS axes; translation is the UCS origin in wire
/// space. Identity outside model space (no UCS there).
pub(super) fn ucs_wire_affine(&self) -> glam::Mat4 {
if self.scene.current_layout != "Model" {
return glam::Mat4::IDENTITY;
}
let (o, x, y, z) = self.ucs_xform().axes();
let wo = self.scene.world_offset;
let origin = glam::Vec3::new(
o.x - wo[0] as f32,
o.y - wo[1] as f32,
o.z - wo[2] as f32,
);
glam::Mat4::from_cols(
x.extend(0.0),
y.extend(0.0),
z.extend(0.0),
origin.extend(1.0),
)
}
/// Grid origin (render/wire space) and UCS→world rotation for grid snap and
/// the grid overlay. Identity / origin-at-zero outside model space.
pub(super) fn ucs_grid_basis(&self) -> (glam::Vec3, glam::Mat4) {
+19
View File
@@ -1660,6 +1660,7 @@ impl OpenCADStudio {
.map(|c| c.wants_text_input())
.unwrap_or(false)
{
self.push_ucs_to_cmd(i);
if let Some(result) = self.tabs[i]
.active_cmd
.as_mut()
@@ -1687,6 +1688,7 @@ impl OpenCADStudio {
self.dyn_user_reshaped = false;
self.sync_dyn_fields();
self.reset_tracking_after_point();
self.push_ucs_to_cmd(i);
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt));
if let Some(r) = result {
let task = self.apply_cmd_result(r);
@@ -1728,6 +1730,7 @@ impl OpenCADStudio {
self.dyn_user_reshaped = false;
self.sync_dyn_fields();
self.reset_tracking_after_point();
self.push_ucs_to_cmd(i);
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(wcs_pt));
if let Some(r) = result {
let task = self.apply_cmd_result(r);
@@ -1744,6 +1747,7 @@ impl OpenCADStudio {
return Task::none();
}
self.push_ucs_to_cmd(i);
if let Some(result) = self.tabs[i]
.active_cmd
.as_mut()
@@ -2971,6 +2975,9 @@ impl OpenCADStudio {
s.screen.y += tile_b.y;
}
// Give the command the current UCS before it builds its
// rubber-band preview (and, by persistence, its commit).
self.push_ucs_to_cmd(i);
let mut previews = if needs_structure {
let mut p = self.tabs[i]
.active_cmd
@@ -8347,6 +8354,16 @@ impl OpenCADStudio {
/// values. Locked fields use their typed buffer; the rest fall back to
/// the live cursor-derived value. Returns `None` when the field set
/// isn't one we know how to turn into a point.
/// Hand the active command the current UCS (as a UCS→wire affine) so
/// axis-aligned constructions build square to the user's coordinate system.
/// No-op for commands that don't override `set_ucs`.
fn push_ucs_to_cmd(&mut self, i: usize) {
let ucs = self.tabs[i].ucs_wire_affine();
if let Some(c) = self.tabs[i].active_cmd.as_mut() {
c.set_ucs(ucs);
}
}
fn dyn_resolve_point(&self) -> Option<glam::Vec3> {
use super::document::DynComponent;
let i = self.active_tab;
@@ -8606,6 +8623,7 @@ impl OpenCADStudio {
self.dyn_user_reshaped = false;
self.sync_dyn_fields();
self.reset_tracking_after_point();
self.push_ucs_to_cmd(i);
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt));
let task = result.map(|r| self.apply_cmd_result(r))?;
self.refresh_active_cmd_preview(i);
@@ -8664,6 +8682,7 @@ impl OpenCADStudio {
self.dyn_user_reshaped = false;
self.sync_dyn_fields();
self.reset_tracking_after_point();
self.push_ucs_to_cmd(i);
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt));
for f in self.tabs[i].dyn_fields.iter_mut() {
f.buffer = None;