fix(dimension): correct aligned/ordinate placement and preview plane (#150)

DIMALIGNED's drag preview never matched where the dimension landed. The
preview offset the dimension line in the XZ plane while the committed
entity's renderer offsets in the XY plane, and the commit path passed the
straight-line p2→cursor *distance* to `set_offset`, which re-applies it
along the perpendicular — wrong distance, always on the +perp side. Both
now store the cursor as the definition point and project it onto the
XY-plane perpendicular, matching the renderer and DIMLINEAR.

The same XZ-vs-XY plane confusion affected other annotate commands:

- DIMORDINATE worked entirely in the XZ plane (dropping the Y coordinate)
  and left `definition_point` at the origin, so the leader kinked through
  (0,0,0). Reworked onto the XY plane with a proper orthogonal elbow and a
  live preview that matches the placed entity.
- DIMTEDIT / DIMJOGLINE cursor markers were built in the XZ plane, so in
  the top-down view they collapsed to a flat line instead of a box /
  zigzag. Rebuilt in the XY plane.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-06-25 17:55:39 +03:00
commit 9d5bed2727
4 changed files with 99 additions and 34 deletions

View file

@ -61,17 +61,19 @@ impl CadCommand for AlignedDimensionCommand {
} }
Step::DimLine { p1, p2 } => { Step::DimLine { p1, p2 } => {
let mut dim = DimensionAligned::new(v3(p1), v3(p2)); let mut dim = DimensionAligned::new(v3(p1), v3(p2));
// Set offset: distance from p2 to the dim line location // The dimension line runs through the cursor: store it as the
let dx = pt.x as f64 - p2.x as f64; // definition point and let the renderer project it onto the
let dy = pt.z as f64 - p2.z as f64; // line perpendicular (same as the preview and DIMLINEAR).
let offset = (dx * dx + dy * dy).sqrt(); //
dim.set_offset(offset); // The old path called `set_offset` with the straight-line
// p2→cursor *distance*, which `set_offset` then re-applies along
// the line perpendicular — placing the line at the wrong
// perpendicular distance and always on the +perp side, so it
// never matched the preview the user was dragging. (#150)
dim.definition_point = v3(pt);
dim.base.definition_point = v3(pt); dim.base.definition_point = v3(pt);
dim.base.text_middle_point = v3(Vec3::new( let (d1, d2) = dim_line_endpoints(p1, p2, pt);
(p1.x + p2.x) * 0.5, dim.base.text_middle_point = v3((d1 + d2) * 0.5);
(p1.y + p2.y) * 0.5,
(p1.z + p2.z) * 0.5,
));
dim.base.insertion_point = dim.base.text_middle_point; dim.base.insertion_point = dim.base.text_middle_point;
dim.base.actual_measurement = dim.measurement(); dim.base.actual_measurement = dim.measurement();
CmdResult::CommitAndExit(EntityType::Dimension(Dimension::Aligned(dim))) CmdResult::CommitAndExit(EntityType::Dimension(Dimension::Aligned(dim)))
@ -117,13 +119,21 @@ fn v3(p: Vec3) -> Vector3 {
Vector3::new(p.x as f64, p.y as f64, p.z as f64) Vector3::new(p.x as f64, p.y as f64, p.z as f64)
} }
/// Dimension-line endpoints: the baseline `p1`–`p2` shifted to pass through
/// the cursor's perpendicular projection. Uses the XY-plane perpendicular so
/// it matches the committed entity's renderer (and DIMLINEAR). The old
/// preview used an XZ-plane perpendicular, drawing the offset in the wrong
/// spatial direction. (#150)
fn dim_line_endpoints(p1: Vec3, p2: Vec3, dim_pt: Vec3) -> (Vec3, Vec3) {
let axis = (p2 - p1).normalize_or_zero();
let perp = Vec3::new(-axis.y, axis.x, 0.0);
let offset = (dim_pt - p1).dot(perp);
(p1 + perp * offset, p2 + perp * offset)
}
fn preview_aligned(p1: Vec3, p2: Vec3, dim_pt: Vec3) -> WireModel { fn preview_aligned(p1: Vec3, p2: Vec3, dim_pt: Vec3) -> WireModel {
// Show ext lines + dim line // Show ext lines + dim line.
let dir = (p2 - p1).normalize_or_zero(); let (d1, d2) = dim_line_endpoints(p1, p2, dim_pt);
let perp = Vec3::new(-dir.z, dir.y, dir.x).normalize_or_zero();
let offset = (dim_pt - p2).dot(perp);
let d1 = p1 + perp * offset;
let d2 = p2 + perp * offset;
WireModel { WireModel {
name: "dimaligned_preview".into(), name: "dimaligned_preview".into(),
points: vec![ points: vec![

View file

@ -86,10 +86,13 @@ impl CadCommand for DimJogLineCommand {
let d = 0.3_f32; let d = 0.3_f32;
Some(WireModel { Some(WireModel {
name: "dimjog_preview".into(), name: "dimjog_preview".into(),
// Jog zigzag in the XY drawing plane (Z is elevation, ~0). The old
// marker varied Z, so in the top-down view it collapsed to a flat
// line instead of a zigzag. (#150)
points: vec![ points: vec![
[pt.x - d, pt.y, pt.z], [pt.x - d, pt.y, pt.z],
[pt.x - d * 0.3, pt.y, pt.z + d], [pt.x - d * 0.3, pt.y + d, pt.z],
[pt.x + d * 0.3, pt.y, pt.z - d], [pt.x + d * 0.3, pt.y - d, pt.z],
[pt.x + d, pt.y, pt.z], [pt.x + d, pt.y, pt.z],
], ],
points_low: Vec::new(), points_low: Vec::new(),

View file

@ -99,12 +99,15 @@ impl CadCommand for DimTeditCommand {
let d = 0.2_f32; let d = 0.2_f32;
Some(WireModel { Some(WireModel {
name: "dimtedit_preview".into(), name: "dimtedit_preview".into(),
// Marker box in the XY drawing plane (Z is elevation, ~0). The old
// box varied Z, so in the top-down view it collapsed to a flat line
// instead of a square. (#150)
points: vec![ points: vec![
[pt.x - d, pt.y, pt.z - d], [pt.x - d, pt.y - d, pt.z],
[pt.x + d, pt.y, pt.z - d], [pt.x + d, pt.y - d, pt.z],
[pt.x + d, pt.y, pt.z + d], [pt.x + d, pt.y + d, pt.z],
[pt.x - d, pt.y, pt.z + d], [pt.x - d, pt.y + d, pt.z],
[pt.x - d, pt.y, pt.z - d], [pt.x - d, pt.y - d, pt.z],
], ],
points_low: Vec::new(), points_low: Vec::new(),
color: WireModel::CYAN, color: WireModel::CYAN,

View file

@ -62,14 +62,16 @@ impl CadCommand for OrdinateDimCommand {
CmdResult::NeedPoint CmdResult::NeedPoint
} }
Step::LeaderEndpoint { feature } => { Step::LeaderEndpoint { feature } => {
let dx = (pt.x - feature.x).abs(); let is_x = is_x_type(feature, pt);
let dy = (pt.z - feature.z).abs(); let elbow = ordinate_elbow(feature, pt, is_x);
// If leader is more vertical (Y-screen = Z-world moves more) → X ordinate. let mut dim = DimensionOrdinate::new(v3(feature), v3(pt), is_x);
// If leader is more horizontal → Y ordinate. // The leader is an orthogonal L from the feature to the
let is_x = dy >= dx; // endpoint; store its elbow as the definition point. Without it
let feat_v3 = v3(feature); // the renderer draws feature → (0,0,0) → endpoint, kinking the
let lead_v3 = v3(pt); // leader through the world origin. The old code also worked in
let dim = DimensionOrdinate::new(feat_v3, lead_v3, is_x); // the wrong (XZ) plane, dropping the Y coordinate. (#150)
dim.definition_point = v3(elbow);
dim.base.definition_point = v3(elbow);
CmdResult::CommitAndExit(EntityType::Dimension(Dimension::Ordinate(dim))) CmdResult::CommitAndExit(EntityType::Dimension(Dimension::Ordinate(dim)))
} }
} }
@ -78,13 +80,60 @@ impl CadCommand for OrdinateDimCommand {
fn on_enter(&mut self) -> CmdResult { fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel CmdResult::Cancel
} }
fn on_preview_wires(&mut self, _pt: DVec3) -> Vec<WireModel> { fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { let pt = pt.as_vec3();
vec![] let feature = match self.step {
Step::LeaderEndpoint { feature } => feature,
_ => return None,
};
let is_x = is_x_type(feature, pt);
let elbow = ordinate_elbow(feature, pt, is_x);
Some(preview_wire(vec![feature, elbow, pt]))
} }
} }
fn v3(p: Vec3) -> Vector3 { fn v3(p: Vec3) -> Vector3 {
Vector3::new(p.x as f64, 0.0, p.z as f64) Vector3::new(p.x as f64, p.y as f64, p.z as f64)
}
/// X-datum (labels the feature's X coordinate) when the leader runs more
/// vertically than horizontally; Y-datum otherwise. Mirrors the placement
/// decision so the preview and the committed entity agree.
fn is_x_type(feature: Vec3, leader: Vec3) -> bool {
let dx = (leader.x - feature.x).abs();
let dy = (leader.y - feature.y).abs();
dy >= dx
}
/// Orthogonal elbow of the ordinate leader: an X-datum runs along Y from the
/// feature then jogs across in X; a Y-datum runs along X then jogs in Y.
fn ordinate_elbow(feature: Vec3, leader: Vec3, is_x: bool) -> Vec3 {
if is_x {
Vec3::new(feature.x, leader.y, feature.z)
} else {
Vec3::new(leader.x, feature.y, feature.z)
}
}
fn preview_wire(points: Vec<Vec3>) -> WireModel {
WireModel {
name: "dimordinate_preview".into(),
points: points.into_iter().map(|p| [p.x, p.y, p.z]).collect(),
points_low: Vec::new(),
color: WireModel::CYAN,
selected: false,
pattern_length: 0.0,
pattern: [0.0; 8],
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
aci: 0,
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
}
} }