2026-04-06 14:16:06 +03:00
|
|
|
|
use acadrust::tables::Ucs;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
|
|
|
|
|
|
// ── Coordinate parsing ─────────────────────────────────────────────────────
|
|
|
|
|
|
|
2026-05-27 11:40:28 +03:00
|
|
|
|
/// How a typed coordinate should be interpreted relative to the last
|
|
|
|
|
|
/// input point.
|
|
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
2026-08-04 23:17:54 +03:00
|
|
|
|
pub(crate) enum CoordKind {
|
2026-05-27 11:40:28 +03:00
|
|
|
|
/// `@x,y` prefix — offset from the last input point.
|
|
|
|
|
|
Relative,
|
|
|
|
|
|
/// `#x,y` prefix — world/UCS absolute, overriding DYN.
|
|
|
|
|
|
Absolute,
|
|
|
|
|
|
/// No prefix — the caller decides (DYN on → relative, off → absolute).
|
|
|
|
|
|
Default,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-04 23:17:54 +03:00
|
|
|
|
/// Parse a typed coordinate string into a local Vec3 plus its interpretation.
|
|
|
|
|
|
/// Accepts Cartesian, polar, cylindrical and spherical forms:
|
|
|
|
|
|
/// `x,y`, `x,y,z`, `distance<angle`, `distance<angle,z`, and
|
|
|
|
|
|
/// `distance<angle<elevation`.
|
2026-05-27 11:40:28 +03:00
|
|
|
|
/// A leading `@` marks the value relative to the last point; a leading
|
|
|
|
|
|
/// `#` forces absolute. Separators: comma or semicolon.
|
feat(units): read back every form the drawing writes
The readout learned to write lengths as 5'-6 1/2" and angles as
N 45d0'0" E, but typed input was still a bare parse::<f64>() — a
coordinate could be displayed and not entered. Give the formatters
their inverses.
parse_length takes feet and inches separated by a dash, a space, or
nothing, with the closing quote optional, and whole-plus-fraction in
any of those forms: 5', 60", 5'-9", 5'9-1/2, 9-1/2, 1/2. A leading
minus is a sign; the one inside 5'-9" is a separator, since after feet
there is nothing left to subtract from.
parse_angle reads the mark that says which convention an angle is in —
g for grads, r for radians, d/'/" for degrees-minutes-seconds, compass
letters for a surveyor's bearing — and reads a bare number in whichever
convention the drawing is set to.
ANGBASE and ANGDIR now apply, but only where they mean something. Which
way a thing points is measured from the drawing's zero and runs the way
it says; how wide an arc opens is the same number whatever zero is
counted from. format_direction and parse_direction are the pair for
directions, and the polar readout and polar input both go through them,
so the bearing shown is the bearing accepted. format_angle stays for
sizes, where applying a base angle would corrupt the value.
The command prompts follow the same rule, each classified rather than
swept: OFFSET distance, FILLET radius, CHAMFER distances, ARRAY and
MINSERT spacing, TRACE width, HELIX, and EXTRUDE height read lengths;
the four dimension text-rotation prompts, ARRAY's total angle and
INSERT's rotation read angles. MLINE's scale and INSERT's scale factor
are left alone — a multiplier has no unit to be written in. INSERT read
both through one parse; they are now separate.
The typed variants also take a decimal comma, which a single-value
prompt has no other use for. parse_length itself does not: inside a
coordinate the comma separates the axes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 23:46:49 +03:00
|
|
|
|
///
|
|
|
|
|
|
/// Each number is read by the same pair that writes it, so anything the
|
|
|
|
|
|
/// drawing displays can be typed straight back: `5'-6 1/2"`, `9 1/2`,
|
|
|
|
|
|
/// `@72'8"<n45d20'6"e`. Angles after `<` are directions, and so are counted
|
|
|
|
|
|
/// from the drawing's zero and run the way it says.
|
2026-08-04 23:17:54 +03:00
|
|
|
|
pub(crate) fn parse_coord(text: &str) -> Option<(glam::DVec3, CoordKind)> {
|
feat(units): read back every form the drawing writes
The readout learned to write lengths as 5'-6 1/2" and angles as
N 45d0'0" E, but typed input was still a bare parse::<f64>() — a
coordinate could be displayed and not entered. Give the formatters
their inverses.
parse_length takes feet and inches separated by a dash, a space, or
nothing, with the closing quote optional, and whole-plus-fraction in
any of those forms: 5', 60", 5'-9", 5'9-1/2, 9-1/2, 1/2. A leading
minus is a sign; the one inside 5'-9" is a separator, since after feet
there is nothing left to subtract from.
parse_angle reads the mark that says which convention an angle is in —
g for grads, r for radians, d/'/" for degrees-minutes-seconds, compass
letters for a surveyor's bearing — and reads a bare number in whichever
convention the drawing is set to.
ANGBASE and ANGDIR now apply, but only where they mean something. Which
way a thing points is measured from the drawing's zero and runs the way
it says; how wide an arc opens is the same number whatever zero is
counted from. format_direction and parse_direction are the pair for
directions, and the polar readout and polar input both go through them,
so the bearing shown is the bearing accepted. format_angle stays for
sizes, where applying a base angle would corrupt the value.
The command prompts follow the same rule, each classified rather than
swept: OFFSET distance, FILLET radius, CHAMFER distances, ARRAY and
MINSERT spacing, TRACE width, HELIX, and EXTRUDE height read lengths;
the four dimension text-rotation prompts, ARRAY's total angle and
INSERT's rotation read angles. MLINE's scale and INSERT's scale factor
are left alone — a multiplier has no unit to be written in. INSERT read
both through one parse; they are now separate.
The typed variants also take a decimal comma, which a single-value
prompt has no other use for. parse_length itself does not: inside a
coordinate the comma separates the axes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 23:46:49 +03:00
|
|
|
|
use crate::entities::common::{parse_direction, parse_length};
|
2026-05-27 11:40:28 +03:00
|
|
|
|
let trimmed = text.trim();
|
|
|
|
|
|
let (kind, rest) = if let Some(r) = trimmed.strip_prefix('@') {
|
|
|
|
|
|
(CoordKind::Relative, r)
|
|
|
|
|
|
} else if let Some(r) = trimmed.strip_prefix('#') {
|
|
|
|
|
|
(CoordKind::Absolute, r)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
(CoordKind::Default, trimmed)
|
|
|
|
|
|
};
|
2026-08-04 23:17:54 +03:00
|
|
|
|
if let Some((distance, angles)) = rest.split_once('<') {
|
feat(units): read back every form the drawing writes
The readout learned to write lengths as 5'-6 1/2" and angles as
N 45d0'0" E, but typed input was still a bare parse::<f64>() — a
coordinate could be displayed and not entered. Give the formatters
their inverses.
parse_length takes feet and inches separated by a dash, a space, or
nothing, with the closing quote optional, and whole-plus-fraction in
any of those forms: 5', 60", 5'-9", 5'9-1/2, 9-1/2, 1/2. A leading
minus is a sign; the one inside 5'-9" is a separator, since after feet
there is nothing left to subtract from.
parse_angle reads the mark that says which convention an angle is in —
g for grads, r for radians, d/'/" for degrees-minutes-seconds, compass
letters for a surveyor's bearing — and reads a bare number in whichever
convention the drawing is set to.
ANGBASE and ANGDIR now apply, but only where they mean something. Which
way a thing points is measured from the drawing's zero and runs the way
it says; how wide an arc opens is the same number whatever zero is
counted from. format_direction and parse_direction are the pair for
directions, and the polar readout and polar input both go through them,
so the bearing shown is the bearing accepted. format_angle stays for
sizes, where applying a base angle would corrupt the value.
The command prompts follow the same rule, each classified rather than
swept: OFFSET distance, FILLET radius, CHAMFER distances, ARRAY and
MINSERT spacing, TRACE width, HELIX, and EXTRUDE height read lengths;
the four dimension text-rotation prompts, ARRAY's total angle and
INSERT's rotation read angles. MLINE's scale and INSERT's scale factor
are left alone — a multiplier has no unit to be written in. INSERT read
both through one parse; they are now separate.
The typed variants also take a decimal comma, which a single-value
prompt has no other use for. parse_length itself does not: inside a
coordinate the comma separates the axes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 23:46:49 +03:00
|
|
|
|
let distance = parse_length(distance)?;
|
2026-08-04 23:17:54 +03:00
|
|
|
|
if let Some((azimuth, elevation)) = angles.split_once('<') {
|
feat(units): read back every form the drawing writes
The readout learned to write lengths as 5'-6 1/2" and angles as
N 45d0'0" E, but typed input was still a bare parse::<f64>() — a
coordinate could be displayed and not entered. Give the formatters
their inverses.
parse_length takes feet and inches separated by a dash, a space, or
nothing, with the closing quote optional, and whole-plus-fraction in
any of those forms: 5', 60", 5'-9", 5'9-1/2, 9-1/2, 1/2. A leading
minus is a sign; the one inside 5'-9" is a separator, since after feet
there is nothing left to subtract from.
parse_angle reads the mark that says which convention an angle is in —
g for grads, r for radians, d/'/" for degrees-minutes-seconds, compass
letters for a surveyor's bearing — and reads a bare number in whichever
convention the drawing is set to.
ANGBASE and ANGDIR now apply, but only where they mean something. Which
way a thing points is measured from the drawing's zero and runs the way
it says; how wide an arc opens is the same number whatever zero is
counted from. format_direction and parse_direction are the pair for
directions, and the polar readout and polar input both go through them,
so the bearing shown is the bearing accepted. format_angle stays for
sizes, where applying a base angle would corrupt the value.
The command prompts follow the same rule, each classified rather than
swept: OFFSET distance, FILLET radius, CHAMFER distances, ARRAY and
MINSERT spacing, TRACE width, HELIX, and EXTRUDE height read lengths;
the four dimension text-rotation prompts, ARRAY's total angle and
INSERT's rotation read angles. MLINE's scale and INSERT's scale factor
are left alone — a multiplier has no unit to be written in. INSERT read
both through one parse; they are now separate.
The typed variants also take a decimal comma, which a single-value
prompt has no other use for. parse_length itself does not: inside a
coordinate the comma separates the axes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 23:46:49 +03:00
|
|
|
|
let azimuth = parse_direction(azimuth)?;
|
|
|
|
|
|
// The rise out of the plane is a size, not a compass direction, so
|
|
|
|
|
|
// it is not counted from the drawing's zero.
|
|
|
|
|
|
let elevation = crate::entities::common::parse_angle(elevation)?;
|
2026-08-04 23:17:54 +03:00
|
|
|
|
let horizontal = distance * elevation.cos();
|
|
|
|
|
|
return Some((
|
|
|
|
|
|
glam::DVec3::new(
|
|
|
|
|
|
horizontal * azimuth.cos(),
|
|
|
|
|
|
horizontal * azimuth.sin(),
|
|
|
|
|
|
distance * elevation.sin(),
|
|
|
|
|
|
),
|
|
|
|
|
|
kind,
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
feat(units): read back every form the drawing writes
The readout learned to write lengths as 5'-6 1/2" and angles as
N 45d0'0" E, but typed input was still a bare parse::<f64>() — a
coordinate could be displayed and not entered. Give the formatters
their inverses.
parse_length takes feet and inches separated by a dash, a space, or
nothing, with the closing quote optional, and whole-plus-fraction in
any of those forms: 5', 60", 5'-9", 5'9-1/2, 9-1/2, 1/2. A leading
minus is a sign; the one inside 5'-9" is a separator, since after feet
there is nothing left to subtract from.
parse_angle reads the mark that says which convention an angle is in —
g for grads, r for radians, d/'/" for degrees-minutes-seconds, compass
letters for a surveyor's bearing — and reads a bare number in whichever
convention the drawing is set to.
ANGBASE and ANGDIR now apply, but only where they mean something. Which
way a thing points is measured from the drawing's zero and runs the way
it says; how wide an arc opens is the same number whatever zero is
counted from. format_direction and parse_direction are the pair for
directions, and the polar readout and polar input both go through them,
so the bearing shown is the bearing accepted. format_angle stays for
sizes, where applying a base angle would corrupt the value.
The command prompts follow the same rule, each classified rather than
swept: OFFSET distance, FILLET radius, CHAMFER distances, ARRAY and
MINSERT spacing, TRACE width, HELIX, and EXTRUDE height read lengths;
the four dimension text-rotation prompts, ARRAY's total angle and
INSERT's rotation read angles. MLINE's scale and INSERT's scale factor
are left alone — a multiplier has no unit to be written in. INSERT read
both through one parse; they are now separate.
The typed variants also take a decimal comma, which a single-value
prompt has no other use for. parse_length itself does not: inside a
coordinate the comma separates the axes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 23:46:49 +03:00
|
|
|
|
// A cylindrical `distance<angle,z` splits on the comma — but a
|
|
|
|
|
|
// surveyor's bearing has none, so nothing here can eat one.
|
|
|
|
|
|
let (angle, z) = match angles.split_once(|c| c == ',' || c == ';') {
|
|
|
|
|
|
Some((angle, z)) => (parse_direction(angle)?, parse_length(z)?),
|
|
|
|
|
|
None => (parse_direction(angles)?, 0.0),
|
2026-08-04 23:17:54 +03:00
|
|
|
|
};
|
|
|
|
|
|
return Some((
|
|
|
|
|
|
glam::DVec3::new(distance * angle.cos(), distance * angle.sin(), z),
|
|
|
|
|
|
kind,
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
let parts: Vec<f64> = rest
|
2026-03-22 22:44:32 +03:00
|
|
|
|
.split(|c| c == ',' || c == ';')
|
feat(units): read back every form the drawing writes
The readout learned to write lengths as 5'-6 1/2" and angles as
N 45d0'0" E, but typed input was still a bare parse::<f64>() — a
coordinate could be displayed and not entered. Give the formatters
their inverses.
parse_length takes feet and inches separated by a dash, a space, or
nothing, with the closing quote optional, and whole-plus-fraction in
any of those forms: 5', 60", 5'-9", 5'9-1/2, 9-1/2, 1/2. A leading
minus is a sign; the one inside 5'-9" is a separator, since after feet
there is nothing left to subtract from.
parse_angle reads the mark that says which convention an angle is in —
g for grads, r for radians, d/'/" for degrees-minutes-seconds, compass
letters for a surveyor's bearing — and reads a bare number in whichever
convention the drawing is set to.
ANGBASE and ANGDIR now apply, but only where they mean something. Which
way a thing points is measured from the drawing's zero and runs the way
it says; how wide an arc opens is the same number whatever zero is
counted from. format_direction and parse_direction are the pair for
directions, and the polar readout and polar input both go through them,
so the bearing shown is the bearing accepted. format_angle stays for
sizes, where applying a base angle would corrupt the value.
The command prompts follow the same rule, each classified rather than
swept: OFFSET distance, FILLET radius, CHAMFER distances, ARRAY and
MINSERT spacing, TRACE width, HELIX, and EXTRUDE height read lengths;
the four dimension text-rotation prompts, ARRAY's total angle and
INSERT's rotation read angles. MLINE's scale and INSERT's scale factor
are left alone — a multiplier has no unit to be written in. INSERT read
both through one parse; they are now separate.
The typed variants also take a decimal comma, which a single-value
prompt has no other use for. parse_length itself does not: inside a
coordinate the comma separates the axes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 23:46:49 +03:00
|
|
|
|
.map(parse_length)
|
|
|
|
|
|
.collect::<Option<Vec<f64>>>()?;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
match parts.as_slice() {
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
[x, y] => Some((glam::DVec3::new(*x, *y, 0.0), kind)),
|
|
|
|
|
|
[x, y, z] => Some((glam::DVec3::new(*x, *y, *z), kind)),
|
2026-03-22 22:44:32 +03:00
|
|
|
|
_ => None,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-04 23:17:54 +03:00
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
mod coordinate_parsing_tests {
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn parses_all_coordinate_forms() {
|
|
|
|
|
|
let close = |a: glam::DVec3, b: glam::DVec3| (a - b).length() < 1e-9;
|
|
|
|
|
|
assert!(close(parse_coord("1,2").unwrap().0, glam::dvec3(1.0, 2.0, 0.0)));
|
|
|
|
|
|
assert!(close(parse_coord("1,2,3").unwrap().0, glam::dvec3(1.0, 2.0, 3.0)));
|
|
|
|
|
|
assert!(close(parse_coord("10<90").unwrap().0, glam::dvec3(0.0, 10.0, 0.0)));
|
|
|
|
|
|
assert!(close(parse_coord("10<90,4").unwrap().0, glam::dvec3(0.0, 10.0, 4.0)));
|
|
|
|
|
|
assert!(close(parse_coord("10<0<30").unwrap().0, glam::dvec3(5.0 * 3.0_f64.sqrt(), 0.0, 5.0)));
|
|
|
|
|
|
assert_eq!(parse_coord("@10<0").unwrap().1, CoordKind::Relative);
|
|
|
|
|
|
assert_eq!(parse_coord("#1,2").unwrap().1, CoordKind::Absolute);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
|
// ── UCS ↔ WCS converter ─────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
/// The single bridge between WCS (how geometry and the file are stored) and the
|
|
|
|
|
|
/// active UCS (the coordinate system the user works in). Build one from the
|
|
|
|
|
|
/// tab's active UCS via [`DocumentTab::ucs_xform`](super::DocumentTab); the
|
|
|
|
|
|
/// `None` UCS yields the identity (plain WCS).
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Every system that has to speak UCS — the coordinate readout, typed input,
|
|
|
|
|
|
/// the UCS icon, snap/ortho, the ViewCube — goes through this one type instead
|
|
|
|
|
|
/// of re-deriving the axis math. Axes are orthonormal, so the inverse rotation
|
|
|
|
|
|
/// is just the transpose (the dot products in `to_ucs`); no matrix inversion.
|
|
|
|
|
|
#[derive(Clone, Copy)]
|
|
|
|
|
|
pub(super) struct UcsXform {
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
origin: glam::DVec3,
|
|
|
|
|
|
x: glam::DVec3,
|
|
|
|
|
|
y: glam::DVec3,
|
|
|
|
|
|
z: glam::DVec3,
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl UcsXform {
|
|
|
|
|
|
/// Plain WCS — no active UCS.
|
|
|
|
|
|
pub(super) fn identity() -> Self {
|
|
|
|
|
|
Self {
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
origin: glam::DVec3::ZERO,
|
|
|
|
|
|
x: glam::DVec3::X,
|
|
|
|
|
|
y: glam::DVec3::Y,
|
|
|
|
|
|
z: glam::DVec3::Z,
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn from_ucs(ucs: &Ucs) -> Self {
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
let v = |a: acadrust::types::Vector3| glam::DVec3::new(a.x, a.y, a.z);
|
|
|
|
|
|
let x = v(ucs.x_axis).normalize_or(glam::DVec3::X);
|
2026-07-30 19:50:33 +03:00
|
|
|
|
let raw_y = v(ucs.y_axis).normalize_or(glam::DVec3::Y);
|
|
|
|
|
|
let fallback_z = if x.dot(glam::DVec3::Z).abs() < 0.999 {
|
|
|
|
|
|
glam::DVec3::Z
|
|
|
|
|
|
} else {
|
|
|
|
|
|
glam::DVec3::Y
|
|
|
|
|
|
};
|
|
|
|
|
|
let z = x.cross(raw_y).normalize_or(x.cross(fallback_z).normalize());
|
|
|
|
|
|
let y = z.cross(x).normalize();
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
|
Self { origin: v(ucs.origin), x, y, z }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn from_active(ucs: Option<&Ucs>) -> Self {
|
|
|
|
|
|
ucs.map(Self::from_ucs).unwrap_or_else(Self::identity)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// True when this is plain WCS — lets callers skip the conversion.
|
|
|
|
|
|
pub(super) fn is_identity(&self) -> bool {
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
self.origin == glam::DVec3::ZERO
|
|
|
|
|
|
&& self.x == glam::DVec3::X
|
|
|
|
|
|
&& self.y == glam::DVec3::Y
|
|
|
|
|
|
&& self.z == glam::DVec3::Z
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// UCS point → WCS.
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
pub(super) fn to_wcs(&self, p: glam::DVec3) -> glam::DVec3 {
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
|
self.origin + self.x * p.x + self.y * p.y + self.z * p.z
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// WCS point → UCS.
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
pub(super) fn to_ucs(&self, p: glam::DVec3) -> glam::DVec3 {
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
|
let d = p - self.origin;
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
glam::DVec3::new(d.dot(self.x), d.dot(self.y), d.dot(self.z))
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// UCS direction → WCS (rotation only, no origin shift).
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
pub(super) fn vec_to_wcs(&self, v: glam::DVec3) -> glam::DVec3 {
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
|
self.x * v.x + self.y * v.y + self.z * v.z
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// WCS direction → UCS (rotation only, no origin shift).
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
pub(super) fn vec_to_ucs(&self, v: glam::DVec3) -> glam::DVec3 {
|
|
|
|
|
|
glam::DVec3::new(v.dot(self.x), v.dot(self.y), v.dot(self.z))
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// `(origin, x, y, z)` axes in WCS — for drawing the UCS icon.
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
pub(super) fn axes(&self) -> (glam::DVec3, glam::DVec3, glam::DVec3, glam::DVec3) {
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
|
(self.origin, self.x, self.y, self.z)
|
|
|
|
|
|
}
|
2026-06-22 11:17:55 +03:00
|
|
|
|
|
2026-08-04 23:17:54 +03:00
|
|
|
|
pub(super) fn working_plane(&self) -> crate::command::WorkingPlane {
|
|
|
|
|
|
crate::command::WorkingPlane::new(self.origin, self.x, self.y)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 11:17:55 +03:00
|
|
|
|
/// UCS→world rotation matrix (columns = UCS axes). For consumers that take
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
/// a `Mat4` rotation directly (ViewCube, OTRACK ray directions). The GPU /
|
|
|
|
|
|
/// screen layer is f32, so the axes downcast here at the boundary.
|
2026-06-22 11:17:55 +03:00
|
|
|
|
pub(super) fn rotation_mat(&self) -> glam::Mat4 {
|
|
|
|
|
|
glam::Mat4::from_cols(
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
self.x.as_vec3().extend(0.0),
|
|
|
|
|
|
self.y.as_vec3().extend(0.0),
|
|
|
|
|
|
self.z.as_vec3().extend(0.0),
|
2026-06-22 11:17:55 +03:00
|
|
|
|
glam::Vec4::W,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2026-07-30 19:50:33 +03:00
|
|
|
|
|
|
|
|
|
|
/// Full UCS-local → WCS transform, using `origin` as local zero while
|
|
|
|
|
|
/// retaining this UCS's orthonormal axes.
|
|
|
|
|
|
pub(super) fn to_wcs_transform_at(
|
|
|
|
|
|
&self,
|
|
|
|
|
|
origin: glam::DVec3,
|
|
|
|
|
|
) -> acadrust::types::Transform {
|
|
|
|
|
|
use acadrust::types::{Matrix4, Transform};
|
|
|
|
|
|
Transform::from_matrix(Matrix4 {
|
|
|
|
|
|
m: [
|
|
|
|
|
|
[self.x.x, self.y.x, self.z.x, origin.x],
|
|
|
|
|
|
[self.x.y, self.y.y, self.z.y, origin.y],
|
|
|
|
|
|
[self.x.z, self.y.z, self.z.z, origin.z],
|
|
|
|
|
|
[0.0, 0.0, 0.0, 1.0],
|
|
|
|
|
|
],
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Full WCS → UCS-local transform, using `origin` as the local zero.
|
|
|
|
|
|
pub(super) fn to_ucs_transform_at(
|
|
|
|
|
|
&self,
|
|
|
|
|
|
origin: glam::DVec3,
|
|
|
|
|
|
) -> acadrust::types::Transform {
|
|
|
|
|
|
use acadrust::types::{Matrix4, Transform};
|
|
|
|
|
|
Transform::from_matrix(Matrix4 {
|
|
|
|
|
|
m: [
|
|
|
|
|
|
[self.x.x, self.x.y, self.x.z, -origin.dot(self.x)],
|
|
|
|
|
|
[self.y.x, self.y.y, self.y.z, -origin.dot(self.y)],
|
|
|
|
|
|
[self.z.x, self.z.y, self.z.z, -origin.dot(self.z)],
|
|
|
|
|
|
[0.0, 0.0, 0.0, 1.0],
|
|
|
|
|
|
],
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Convert from the represented UCS into its canonical local frame.
|
|
|
|
|
|
pub(super) fn to_ucs_transform(&self) -> acadrust::types::Transform {
|
|
|
|
|
|
self.to_ucs_transform_at(self.origin)
|
|
|
|
|
|
}
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── UCS ↔ WCS transforms (thin wrappers over `UcsXform`) ────────────────────
|
|
|
|
|
|
|
2026-05-27 11:40:28 +03:00
|
|
|
|
/// Rotate a UCS-local offset into WCS without applying the origin
|
|
|
|
|
|
/// translation — used for relative coordinate entry, where only the
|
|
|
|
|
|
/// axis orientation matters, not the UCS origin.
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
pub(super) fn ucs_rotate_vec(offset: glam::DVec3, ucs: &Ucs) -> glam::DVec3 {
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
|
UcsXform::from_ucs(ucs).vec_to_wcs(offset)
|
2026-05-27 11:40:28 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-06 14:16:06 +03:00
|
|
|
|
/// Convert a point from UCS local coordinates to WCS.
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
pub(super) fn ucs_to_wcs(pt: glam::DVec3, ucs: &Ucs) -> glam::DVec3 {
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
|
UcsXform::from_ucs(ucs).to_wcs(pt)
|
2026-04-06 14:16:06 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Return the normalised Z axis of a UCS (cross product of X and Y axes).
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
pub(super) fn ucs_z_axis(ucs: &Ucs) -> glam::DVec3 {
|
feat(ucs): central WCS↔UCS converter; adopt file UCS, drive readout + icon
Introduce one bridge between WCS (how geometry and the file are stored)
and the active UCS (the coordinate system the user works in), so every
UCS-aware system routes through it instead of re-deriving axis math.
- UcsXform (app/helpers.rs): the converter — to_wcs/to_ucs (points),
vec_to_wcs/vec_to_ucs (directions), axes(), is_identity(). Orthonormal
axes, so the inverse is the transpose. The old ucs_to_wcs / ucs_rotate_vec
free fns now delegate to it (one implementation).
- DocumentTab::ucs_xform() builds it from the tab's active_ucs (None = WCS).
- DocumentTab::adopt_active_ucs_from_header() loads the document's saved
current UCS (header model-space UCS) into active_ucs on open, so the file's
coordinate system is live immediately; called from both load paths.
- Status-bar coordinate readout reports the cursor in the active UCS.
- UCS icon tripod projects the active UCS axes, so it rotates to the UCS
instead of always showing world X/Y/Z.
First slice of routing the app's coordinate systems through the converter;
input/picking, grid, ViewCube, and the remaining readouts follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 09:57:39 +03:00
|
|
|
|
UcsXform::from_ucs(ucs).axes().3
|
2026-04-06 14:16:06 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Build a UCS with `origin` and axes rotated by `angle_z_rad` around the Z axis.
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
pub(super) fn ucs_rotated_z(origin: glam::DVec3, angle_z: f32) -> Ucs {
|
2026-04-06 14:16:06 +03:00
|
|
|
|
let cos = angle_z.cos() as f64;
|
|
|
|
|
|
let sin = angle_z.sin() as f64;
|
|
|
|
|
|
let mut ucs = Ucs::new("*ACTIVE*");
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
ucs.origin = acadrust::types::Vector3::new(origin.x, origin.y, origin.z);
|
2026-04-06 14:16:06 +03:00
|
|
|
|
ucs.x_axis = acadrust::types::Vector3::new(cos, sin, 0.0);
|
|
|
|
|
|
ucs.y_axis = acadrust::types::Vector3::new(-sin, cos, 0.0);
|
|
|
|
|
|
ucs
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
|
// ── Drawing constraint helpers ─────────────────────────────────────────────
|
|
|
|
|
|
|
2026-08-08 21:32:44 +03:00
|
|
|
|
/// The two live drafting directions in degrees inside the active UCS plane.
|
|
|
|
|
|
pub(super) fn drafting_angles(
|
|
|
|
|
|
isometric: bool,
|
|
|
|
|
|
iso_plane: super::settings::IsoPlane,
|
|
|
|
|
|
snap_angle_deg: f32,
|
|
|
|
|
|
) -> [f64; 2] {
|
|
|
|
|
|
let base = if isometric {
|
|
|
|
|
|
iso_plane.angles()
|
2026-03-22 22:44:32 +03:00
|
|
|
|
} else {
|
2026-08-08 21:32:44 +03:00
|
|
|
|
[0.0, 90.0]
|
2026-06-22 10:00:53 +03:00
|
|
|
|
};
|
2026-08-08 21:32:44 +03:00
|
|
|
|
base.map(|angle| angle + snap_angle_deg as f64)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Convert the live drafting directions into world-space axes.
|
|
|
|
|
|
pub(super) fn drafting_axes(
|
|
|
|
|
|
x: glam::DVec3,
|
|
|
|
|
|
y: glam::DVec3,
|
|
|
|
|
|
z: glam::DVec3,
|
|
|
|
|
|
isometric: bool,
|
|
|
|
|
|
iso_plane: super::settings::IsoPlane,
|
|
|
|
|
|
snap_angle_deg: f32,
|
|
|
|
|
|
) -> (glam::DVec3, glam::DVec3, glam::DVec3) {
|
|
|
|
|
|
let [a, b] = drafting_angles(isometric, iso_plane, snap_angle_deg);
|
|
|
|
|
|
let direction = |degrees: f64| {
|
|
|
|
|
|
let radians = degrees.to_radians();
|
|
|
|
|
|
(x * radians.cos() + y * radians.sin()).normalize_or(x)
|
|
|
|
|
|
};
|
|
|
|
|
|
(direction(a), direction(b), z.normalize_or(x.cross(y)))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Constrain `pt` to the nearest live drafting direction from `base`.
|
|
|
|
|
|
pub(super) fn drafting_constrain(
|
|
|
|
|
|
pt: glam::DVec3,
|
|
|
|
|
|
base: glam::DVec3,
|
|
|
|
|
|
xf: &UcsXform,
|
|
|
|
|
|
isometric: bool,
|
|
|
|
|
|
iso_plane: super::settings::IsoPlane,
|
|
|
|
|
|
snap_angle_deg: f32,
|
|
|
|
|
|
) -> glam::DVec3 {
|
|
|
|
|
|
let p = xf.to_ucs(pt);
|
|
|
|
|
|
let b = xf.to_ucs(base);
|
|
|
|
|
|
let delta = glam::DVec2::new(p.x - b.x, p.y - b.y);
|
|
|
|
|
|
let [a, c] = drafting_angles(isometric, iso_plane, snap_angle_deg).map(|degrees| {
|
|
|
|
|
|
let radians = degrees.to_radians();
|
|
|
|
|
|
glam::DVec2::new(radians.cos(), radians.sin())
|
|
|
|
|
|
});
|
|
|
|
|
|
let direction = if delta.dot(a).abs() >= delta.dot(c).abs() { a } else { c };
|
|
|
|
|
|
let projected = direction * delta.dot(direction);
|
|
|
|
|
|
let c = glam::DVec3::new(b.x + projected.x, b.y + projected.y, p.z);
|
2026-06-22 10:00:53 +03:00
|
|
|
|
xf.to_wcs(c)
|
2026-03-22 22:44:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 10:00:53 +03:00
|
|
|
|
/// Constrain `pt` to the nearest polar angle multiple from `base`, measured in
|
|
|
|
|
|
/// the active UCS plane (identity `xf` = world XY, Z-up).
|
|
|
|
|
|
pub(super) fn polar_constrain(
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
pt: glam::DVec3,
|
|
|
|
|
|
base: glam::DVec3,
|
2026-06-22 10:00:53 +03:00
|
|
|
|
step_deg: f32,
|
|
|
|
|
|
xf: &UcsXform,
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
) -> glam::DVec3 {
|
2026-06-22 10:00:53 +03:00
|
|
|
|
let p = xf.to_ucs(pt);
|
|
|
|
|
|
let b = xf.to_ucs(base);
|
|
|
|
|
|
let dx = p.x - b.x;
|
|
|
|
|
|
let dy = p.y - b.y;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let dist = (dx * dx + dy * dy).sqrt();
|
|
|
|
|
|
if dist < 1e-6 {
|
|
|
|
|
|
return pt;
|
|
|
|
|
|
}
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
let step = (step_deg as f64).to_radians();
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let angle = dy.atan2(dx);
|
|
|
|
|
|
let snapped = (angle / step).round() * step;
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
xf.to_wcs(glam::DVec3::new(
|
2026-06-22 10:00:53 +03:00
|
|
|
|
b.x + dist * snapped.cos(),
|
|
|
|
|
|
b.y + dist * snapped.sin(),
|
|
|
|
|
|
p.z,
|
|
|
|
|
|
))
|
2026-03-22 22:44:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 23:44:31 +03:00
|
|
|
|
/// Polar constraint that only engages when the cursor is within `tol_px`
|
|
|
|
|
|
/// screen pixels of the nearest polar ray; otherwise the cursor is left free
|
|
|
|
|
|
/// so POLAR behaves as if off when pointing away from every angle (issue #70).
|
|
|
|
|
|
pub(super) fn polar_constrain_near(
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
pt: glam::DVec3,
|
|
|
|
|
|
base: glam::DVec3,
|
2026-06-09 23:44:31 +03:00
|
|
|
|
step_deg: f32,
|
2026-06-23 23:33:21 +03:00
|
|
|
|
view_rot: glam::Mat4,
|
|
|
|
|
|
eye: glam::DVec3,
|
2026-06-09 23:44:31 +03:00
|
|
|
|
bounds: iced::Rectangle,
|
|
|
|
|
|
tol_px: f32,
|
2026-06-22 10:00:53 +03:00
|
|
|
|
xf: &UcsXform,
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
) -> glam::DVec3 {
|
2026-06-22 10:00:53 +03:00
|
|
|
|
let snapped = polar_constrain(pt, base, step_deg, xf);
|
fix(precision): keep typed coordinates exact in f64 (#311)
Typed values were quantized to the f32 grid before reaching the
entity (e.g. a length of 2000.8 committed as 2000.8020), worsening
with magnitude. The interaction point layer was all glam::Vec3 (f32).
Two-layer fix:
1. Coordinate backbone -> f64. last_point, last_cursor_world,
dyn_anchor, dyn_ref become DVec3; UcsXform transforms, parse_coord,
the ucs helper fns, dyn_resolve_point, the OTRACK typed-distance
paths, command-line coordinate entry and the coordinate readout all
compute in f64. Screen/GPU/alignment stays f32 (snap engine, render
geometry, projection, UCS-icon/viewcube drawing) with casts only at
those boundaries.
2. Per-command committed storage -> DVec3. on_point already received a
DVec3, but many command impls narrowed it back to f32 (let pt =
pt.as_vec3(); Vec<Vec3> fields) and re-widened at build, quantizing
the committed coordinate. Migrated PLINE, SPLINE, MLINE, LENGTHEN,
INSERT, 3D primitives, every dimension/leader/mleader, ray, revcloud,
wipeout, attdef, table, tolerance, array center, mview, plot_window,
plus the QDIM_PLACE handler and qdim_collect_points. Preview,
hit-test and rubber-band paths keep f32 at the boundary.
Hatch manual boundaries are left for a follow-up: HatchModel.boundary
is structurally Arc<Vec<[f32;2]>> and needs a model-level widen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:26:40 +03:00
|
|
|
|
let to_screen = |w: glam::DVec3| {
|
|
|
|
|
|
let ndc = view_rot.project_point3((w - eye).as_vec3());
|
2026-06-09 23:44:31 +03:00
|
|
|
|
(
|
|
|
|
|
|
(ndc.x + 1.0) * 0.5 * bounds.width,
|
|
|
|
|
|
(1.0 - ndc.y) * 0.5 * bounds.height,
|
|
|
|
|
|
)
|
|
|
|
|
|
};
|
|
|
|
|
|
let (cx, cy) = to_screen(pt);
|
|
|
|
|
|
let (sx, sy) = to_screen(snapped);
|
|
|
|
|
|
if ((cx - sx).powi(2) + (cy - sy).powi(2)).sqrt() <= tol_px {
|
|
|
|
|
|
snapped
|
|
|
|
|
|
} else {
|
|
|
|
|
|
pt
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-22 16:34:47 +03:00
|
|
|
|
/// Hard axis lock (#312): the locked ray's direction — the nearest polar
|
|
|
|
|
|
/// increment (or ortho axis) from `base` toward `cursor`, in the active UCS
|
|
|
|
|
|
/// plane. `None` when the cursor sits on the base point.
|
|
|
|
|
|
pub(super) fn axis_lock_capture(
|
|
|
|
|
|
cursor: glam::DVec3,
|
|
|
|
|
|
base: glam::DVec3,
|
|
|
|
|
|
polar: bool,
|
|
|
|
|
|
step_deg: f32,
|
|
|
|
|
|
xf: &UcsXform,
|
2026-08-08 21:32:44 +03:00
|
|
|
|
isometric: bool,
|
|
|
|
|
|
iso_plane: super::settings::IsoPlane,
|
|
|
|
|
|
snap_angle_deg: f32,
|
2026-07-22 16:34:47 +03:00
|
|
|
|
) -> Option<glam::DVec3> {
|
|
|
|
|
|
let p = xf.to_ucs(cursor);
|
|
|
|
|
|
let b = xf.to_ucs(base);
|
|
|
|
|
|
let dx = p.x - b.x;
|
|
|
|
|
|
let dy = p.y - b.y;
|
|
|
|
|
|
if dx.hypot(dy) < 1e-9 {
|
|
|
|
|
|
return None;
|
|
|
|
|
|
}
|
2026-08-08 21:32:44 +03:00
|
|
|
|
let ang = if polar {
|
|
|
|
|
|
let base = (snap_angle_deg as f64).to_radians();
|
|
|
|
|
|
let step = (step_deg as f64).to_radians();
|
|
|
|
|
|
((dy.atan2(dx) - base) / step).round() * step + base
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let delta = glam::DVec2::new(dx, dy);
|
|
|
|
|
|
let [a, b] = drafting_angles(isometric, iso_plane, snap_angle_deg).map(|degrees| {
|
|
|
|
|
|
let radians = degrees.to_radians();
|
|
|
|
|
|
glam::DVec2::new(radians.cos(), radians.sin())
|
|
|
|
|
|
});
|
|
|
|
|
|
let direction = if delta.dot(a).abs() >= delta.dot(b).abs() { a } else { b };
|
|
|
|
|
|
let direction = if delta.dot(direction) < 0.0 { -direction } else { direction };
|
|
|
|
|
|
direction.y.atan2(direction.x)
|
|
|
|
|
|
};
|
2026-07-22 16:34:47 +03:00
|
|
|
|
let dir_ucs = glam::DVec3::new(ang.cos(), ang.sin(), 0.0);
|
|
|
|
|
|
let dir = xf.to_wcs(b + dir_ucs) - xf.to_wcs(b);
|
|
|
|
|
|
(dir.length_squared() > 1e-12).then(|| dir.normalize())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Project `pt` onto the locked ray through `base` — the hard lock applies to
|
|
|
|
|
|
/// EVERYTHING, including an osnap hit, so a snap far off-axis contributes only
|
|
|
|
|
|
/// its along-axis component (#312).
|
|
|
|
|
|
pub(super) fn axis_lock_apply(
|
|
|
|
|
|
pt: glam::DVec3,
|
|
|
|
|
|
base: glam::DVec3,
|
|
|
|
|
|
dir: glam::DVec3,
|
|
|
|
|
|
) -> glam::DVec3 {
|
|
|
|
|
|
base + dir * (pt - base).dot(dir)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
|
// ── Clipboard / selection helpers ──────────────────────────────────────────
|
|
|
|
|
|
|
2026-07-10 11:16:13 +03:00
|
|
|
|
/// Copy/paste anchor: the lower-left corner of the bounding box that encloses
|
|
|
|
|
|
/// every copied entity. Standard clipboard behaviour puts this corner under the
|
|
|
|
|
|
/// cursor at paste time, so the whole selection drops down-and-right of the pick.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Unions each entity's own bounding box (O(entities)) rather than averaging
|
|
|
|
|
|
/// every tessellated wire vertex, which cost O(total geometry) and stalled a
|
|
|
|
|
|
/// whole-drawing copy. The per-entity `min` corners give the exact enclosing
|
|
|
|
|
|
/// box's lower-left.
|
|
|
|
|
|
pub(super) fn entities_lower_left_by_bbox(
|
2026-07-08 18:57:11 +02:00
|
|
|
|
doc: &acadrust::CadDocument,
|
|
|
|
|
|
handles: &[acadrust::Handle],
|
|
|
|
|
|
) -> glam::DVec3 {
|
2026-07-10 11:16:13 +03:00
|
|
|
|
let mut min = glam::DVec3::splat(f64::INFINITY);
|
|
|
|
|
|
let mut any = false;
|
2026-07-08 18:57:11 +02:00
|
|
|
|
for &h in handles {
|
|
|
|
|
|
let Some(e) = doc.get_entity(h) else { continue };
|
|
|
|
|
|
let bb = e.as_entity().bounding_box();
|
2026-07-10 11:16:13 +03:00
|
|
|
|
let lo = glam::DVec3::new(bb.min.x, bb.min.y, bb.min.z);
|
|
|
|
|
|
if lo.x.is_finite() && lo.y.is_finite() && lo.z.is_finite() {
|
|
|
|
|
|
min = min.min(lo);
|
|
|
|
|
|
any = true;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-10 11:16:13 +03:00
|
|
|
|
if any {
|
|
|
|
|
|
min
|
2026-05-12 10:36:15 +03:00
|
|
|
|
} else {
|
2026-06-24 10:42:58 +03:00
|
|
|
|
glam::DVec3::ZERO
|
2026-05-12 10:36:15 +03:00
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Generate the next available auto group name ("*A1", "*A2", …).
|
|
|
|
|
|
pub(super) fn next_group_auto_name(scene: &crate::scene::Scene) -> String {
|
2026-06-05 00:35:53 +03:00
|
|
|
|
let existing: rustc_hash::FxHashSet<String> =
|
2026-03-22 22:44:32 +03:00
|
|
|
|
scene.groups().map(|g| g.name.clone()).collect();
|
|
|
|
|
|
for n in 1..=9999 {
|
|
|
|
|
|
let name = format!("*A{n}");
|
|
|
|
|
|
if !existing.contains(&name) {
|
|
|
|
|
|
return name;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"*A".to_string()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Entity type labels ─────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn entity_type_label(entity: &acadrust::EntityType) -> String {
|
2026-08-02 22:09:25 +03:00
|
|
|
|
crate::t!(crate::entities::names::ui_name_or_class(entity)).into_owned()
|
2026-03-22 22:44:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn entity_type_key(entity: &acadrust::EntityType) -> String {
|
2026-04-24 01:43:13 +03:00
|
|
|
|
use acadrust::EntityType::*;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
match entity {
|
2026-04-24 01:43:13 +03:00
|
|
|
|
Point(_) => "point",
|
|
|
|
|
|
Line(_) => "line",
|
|
|
|
|
|
Circle(_) => "circle",
|
|
|
|
|
|
Arc(_) => "arc",
|
|
|
|
|
|
Ellipse(_) => "ellipse",
|
|
|
|
|
|
Spline(_) => "spline",
|
2026-07-02 23:28:37 +03:00
|
|
|
|
Helix(_) => "helix",
|
2026-04-24 01:43:13 +03:00
|
|
|
|
LwPolyline(_) | Polyline(_) => "pline",
|
|
|
|
|
|
Polyline2D(_) => "pline2d",
|
|
|
|
|
|
Polyline3D(_) => "pline3d",
|
|
|
|
|
|
PolyfaceMesh(_) => "polyface",
|
|
|
|
|
|
PolygonMesh(_) => "polymesh",
|
|
|
|
|
|
Text(_) => "text",
|
|
|
|
|
|
MText(_) => "mtext",
|
|
|
|
|
|
Dimension(_) => "dimension",
|
|
|
|
|
|
Leader(_) => "leader",
|
|
|
|
|
|
MultiLeader(_) => "multileader",
|
|
|
|
|
|
Tolerance(_) => "tolerance",
|
|
|
|
|
|
Insert(_) => "insert",
|
|
|
|
|
|
Block(_) => "block",
|
|
|
|
|
|
BlockEnd(_) => "blockend",
|
|
|
|
|
|
Hatch(_) => "hatch",
|
|
|
|
|
|
Solid(_) => "solid",
|
|
|
|
|
|
Face3D(_) => "face3d",
|
|
|
|
|
|
Solid3D(_) => "solid3d",
|
|
|
|
|
|
Region(_) => "region",
|
|
|
|
|
|
Body(_) => "body",
|
2026-06-14 20:11:44 +03:00
|
|
|
|
Surface(_) => "surface",
|
2026-04-24 01:43:13 +03:00
|
|
|
|
Mesh(_) => "mesh",
|
|
|
|
|
|
Ray(_) => "ray",
|
|
|
|
|
|
XLine(_) => "xline",
|
|
|
|
|
|
MLine(_) => "mline",
|
|
|
|
|
|
Viewport(_) => "viewport",
|
|
|
|
|
|
RasterImage(_) => "rasterimage",
|
|
|
|
|
|
Wipeout(_) => "wipeout",
|
|
|
|
|
|
Underlay(_) => "underlay",
|
|
|
|
|
|
Shape(_) => "shape",
|
|
|
|
|
|
Table(_) => "table",
|
|
|
|
|
|
AttributeDefinition(_) => "attdef",
|
|
|
|
|
|
AttributeEntity(_) => "attrib",
|
|
|
|
|
|
Ole2Frame(_) => "ole2frame",
|
2026-07-19 22:15:40 +03:00
|
|
|
|
Light(_) => "light",
|
2026-07-21 00:19:51 +03:00
|
|
|
|
SectionSymbol(_) => "sectionsymbol",
|
|
|
|
|
|
ViewBorder(_) => "viewborder",
|
2026-07-26 19:30:43 +03:00
|
|
|
|
Extended(entity) => return entity.class_name().to_ascii_lowercase(),
|
2026-04-24 01:43:13 +03:00
|
|
|
|
Seqend(_) => "seqend",
|
|
|
|
|
|
Unknown(_) => "unknown",
|
2026-03-22 22:44:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
.to_string()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn title_case_word(value: &str) -> String {
|
|
|
|
|
|
let mut chars = value.chars();
|
|
|
|
|
|
match chars.next() {
|
|
|
|
|
|
Some(first) => {
|
|
|
|
|
|
let mut out = first.to_uppercase().collect::<String>();
|
|
|
|
|
|
out.push_str(chars.as_str());
|
|
|
|
|
|
out
|
|
|
|
|
|
}
|
|
|
|
|
|
None => String::new(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Window icon ────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
2026-08-07 17:37:59 +03:00
|
|
|
|
/// The window icon, rasterised from the same file every other build target
|
|
|
|
|
|
/// draws its icon from.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// This used to redraw the mark stroke by stroke in code, with the background
|
|
|
|
|
|
/// colour written out as a literal. The taskbar therefore kept showing whatever
|
|
|
|
|
|
/// the logo used to be, however many times the logo itself was redrawn — the
|
|
|
|
|
|
/// one icon in the application that did not come from `assets/logo.svg`.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Returns 32×32 RGBA. A logo that cannot be rendered leaves the window with
|
|
|
|
|
|
/// the platform default rather than something invented here, which would put
|
|
|
|
|
|
/// this back where it started.
|
2026-06-22 02:03:34 +03:00
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
2026-08-07 17:37:59 +03:00
|
|
|
|
pub(super) fn build_window_icon() -> Option<Vec<u8>> {
|
|
|
|
|
|
const W: u32 = 32;
|
|
|
|
|
|
static LOGO: &[u8] = include_bytes!("../../assets/logo.svg");
|
|
|
|
|
|
|
|
|
|
|
|
let tree = resvg::usvg::Tree::from_data(LOGO, &resvg::usvg::Options::default()).ok()?;
|
|
|
|
|
|
let mut pixmap = resvg::tiny_skia::Pixmap::new(W, W)?;
|
|
|
|
|
|
let size = tree.size();
|
|
|
|
|
|
// Fit the artwork to the square without distorting it, whatever aspect the
|
|
|
|
|
|
// logo happens to have.
|
|
|
|
|
|
let scale = (W as f32 / size.width()).min(W as f32 / size.height());
|
|
|
|
|
|
let transform = resvg::tiny_skia::Transform::from_translate(
|
|
|
|
|
|
(W as f32 - size.width() * scale) / 2.0,
|
|
|
|
|
|
(W as f32 - size.height() * scale) / 2.0,
|
|
|
|
|
|
)
|
|
|
|
|
|
.pre_scale(scale, scale);
|
|
|
|
|
|
resvg::render(&tree, transform, &mut pixmap.as_mut());
|
|
|
|
|
|
Some(pixmap.take())
|
2026-03-22 22:44:32 +03:00
|
|
|
|
}
|