forked from 1b-agent/opencadstudio-upstream
Merge pull request #1272 from kevin9327/fix/linear-dimension-off-world-z
This commit is contained in:
commit
48a734ba9b
2 files changed
+204
No files matched your search
@@ -3431,6 +3431,26 @@ fn tessellate_dimension_inner(
|
|||||||
return wires;
|
return wires;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some((local, normal)) = linear_dimension_in_ocs(dim) {
|
||||||
|
let mut wires = tessellate_dimension_inner(
|
||||||
|
document,
|
||||||
|
handle,
|
||||||
|
&local,
|
||||||
|
selected,
|
||||||
|
entity_color,
|
||||||
|
line_weight_px,
|
||||||
|
anno_scale,
|
||||||
|
selected_set,
|
||||||
|
active_viewport,
|
||||||
|
bg_color,
|
||||||
|
None,
|
||||||
|
world_per_pixel,
|
||||||
|
);
|
||||||
|
for wire in &mut wires {
|
||||||
|
map_wire_ocs_to_wcs(wire, normal);
|
||||||
|
}
|
||||||
|
return wires;
|
||||||
|
}
|
||||||
let name = handle.value().to_string();
|
let name = handle.value().to_string();
|
||||||
// (Baked-block fast path moved up into scene::tessellate_entity so the
|
// (Baked-block fast path moved up into scene::tessellate_entity so the
|
||||||
// recursive call goes through the LOD ladder, not the kernel path.)
|
// recursive call goes through the LOD ladder, not the kernel path.)
|
||||||
@@ -5226,6 +5246,50 @@ pub(crate) fn large_radial_dimension_in_ocs(
|
|||||||
Some((local, normal))
|
Some((local, normal))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A linear or aligned dimension in a plane off world Z, moved into its own
|
||||||
|
/// OCS. Their builders read the offset and extension directions in XY, so
|
||||||
|
/// they draw it there and the result is mapped back onto the plane. `None`
|
||||||
|
/// for a dimension in world XY or of another kind.
|
||||||
|
pub(crate) fn linear_dimension_in_ocs(dimension: &Dimension) -> Option<(Dimension, Vector3)> {
|
||||||
|
let length = dimension.base().normal.length();
|
||||||
|
if !length.is_finite() || length <= 1.0e-12 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let normal = dimension.base().normal / length;
|
||||||
|
if (normal - Vector3::UNIT_Z).length() <= 1.0e-12 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let normal_tuple = (normal.x, normal.y, normal.z);
|
||||||
|
let to_ocs = |point: Vector3| {
|
||||||
|
let point = crate::scene::view::transform::wcs_point_to_ocs(
|
||||||
|
(point.x, point.y, point.z),
|
||||||
|
normal_tuple,
|
||||||
|
);
|
||||||
|
Vector3::new(point.0, point.1, point.2)
|
||||||
|
};
|
||||||
|
let mut local = dimension.clone();
|
||||||
|
match &mut local {
|
||||||
|
Dimension::Linear(d) => {
|
||||||
|
d.first_point = to_ocs(d.first_point);
|
||||||
|
d.second_point = to_ocs(d.second_point);
|
||||||
|
d.definition_point = to_ocs(d.definition_point);
|
||||||
|
d.base.definition_point = d.definition_point;
|
||||||
|
d.base.text_middle_point = to_ocs(d.base.text_middle_point);
|
||||||
|
d.base.normal = Vector3::UNIT_Z;
|
||||||
|
}
|
||||||
|
Dimension::Aligned(d) => {
|
||||||
|
d.first_point = to_ocs(d.first_point);
|
||||||
|
d.second_point = to_ocs(d.second_point);
|
||||||
|
d.definition_point = to_ocs(d.definition_point);
|
||||||
|
d.base.definition_point = d.definition_point;
|
||||||
|
d.base.text_middle_point = to_ocs(d.base.text_middle_point);
|
||||||
|
d.base.normal = Vector3::UNIT_Z;
|
||||||
|
}
|
||||||
|
_ => return None,
|
||||||
|
}
|
||||||
|
Some((local, normal))
|
||||||
|
}
|
||||||
|
|
||||||
fn map_wire_ocs_to_wcs(wire: &mut WireModel, normal: Vector3) {
|
fn map_wire_ocs_to_wcs(wire: &mut WireModel, normal: Vector3) {
|
||||||
let normal_tuple = (normal.x, normal.y, normal.z);
|
let normal_tuple = (normal.x, normal.y, normal.z);
|
||||||
let map = |x: f64, y: f64, z: f64| {
|
let map = |x: f64, y: f64, z: f64| {
|
||||||
@@ -7687,4 +7751,79 @@ mod linear_transform_tests {
|
|||||||
);
|
);
|
||||||
assert_measures_ten(&dim);
|
assert_measures_ten(&dim);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every stroke point the dimension draws, with its low residual added.
|
||||||
|
fn stroke_points(dim: &Dimension) -> Vec<DVec3> {
|
||||||
|
let document = CadDocument::new();
|
||||||
|
let wires = dim.tessellate(
|
||||||
|
&document,
|
||||||
|
Handle::new(0x40),
|
||||||
|
false,
|
||||||
|
[1.0; 4],
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
&rustc_hash::FxHashSet::default(),
|
||||||
|
None,
|
||||||
|
[0.0, 0.0, 0.0, 1.0],
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let mut points = Vec::new();
|
||||||
|
for wire in &wires {
|
||||||
|
for (index, high) in wire.points.iter().enumerate() {
|
||||||
|
if !high[0].is_finite() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let low = wire.points_low.get(index).copied().unwrap_or([0.0; 3]);
|
||||||
|
points.push(DVec3::new(
|
||||||
|
high[0] as f64 + low[0] as f64,
|
||||||
|
high[1] as f64 + low[1] as f64,
|
||||||
|
high[2] as f64 + low[2] as f64,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
points
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turned 90° about X into the XZ plane, the dimension has to draw the
|
||||||
|
/// same strokes turned with it.
|
||||||
|
fn assert_draws_turned_with_its_plane(flat: Dimension) {
|
||||||
|
let turn = glam::DQuat::from_axis_angle(DVec3::X, FRAC_PI_2);
|
||||||
|
let expected: Vec<DVec3> = stroke_points(&flat).into_iter().map(|p| turn * p).collect();
|
||||||
|
assert!(!expected.is_empty(), "the dimension draws no strokes");
|
||||||
|
let mut turned = flat;
|
||||||
|
apply_transform(
|
||||||
|
&mut turned,
|
||||||
|
&EntityTransform::Rotate {
|
||||||
|
center: DVec3::ZERO,
|
||||||
|
axis: DVec3::X,
|
||||||
|
angle_rad: FRAC_PI_2,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let drawn = stroke_points(&turned);
|
||||||
|
assert_eq!(drawn.len(), expected.len(), "stroke point count");
|
||||||
|
for (index, (got, want)) in drawn.iter().zip(&expected).enumerate() {
|
||||||
|
assert!(
|
||||||
|
got.abs_diff_eq(*want, 1e-3),
|
||||||
|
"point {index}: expected {want:?}, got {got:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A linear dimension in a working plane off world Z draws in that plane.
|
||||||
|
/// Its offset used to be read in world XY only, so once it ran along Z the
|
||||||
|
/// dimension line fell onto the measured points and the extension lines
|
||||||
|
/// stuck out along world Y.
|
||||||
|
#[test]
|
||||||
|
fn a_linear_dimension_off_world_z_draws_in_its_own_plane() {
|
||||||
|
assert_draws_turned_with_its_plane(horizontal());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An aligned dimension draws through the same builder and plane.
|
||||||
|
#[test]
|
||||||
|
fn an_aligned_dimension_off_world_z_draws_in_its_own_plane() {
|
||||||
|
let mut d = DimensionAligned::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(6.0, 8.0, 0.0));
|
||||||
|
d.definition_point = Vector3::new(2.0, 11.0, 0.0);
|
||||||
|
assert_draws_turned_with_its_plane(Dimension::Aligned(d));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -927,6 +927,19 @@ fn explode_dimension(dim: &Dimension, doc: &CadDocument) -> Vec<EntityType> {
|
|||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some((local, normal)) = crate::entities::dimension::linear_dimension_in_ocs(dim) {
|
||||||
|
let (x_axis, y_axis) =
|
||||||
|
crate::scene::view::transform::ocs_axes((normal.x, normal.y, normal.z));
|
||||||
|
let plane = WorkingPlane::new(
|
||||||
|
DVec3::ZERO,
|
||||||
|
DVec3::new(x_axis.0, x_axis.1, x_axis.2),
|
||||||
|
DVec3::new(y_axis.0, y_axis.1, y_axis.2),
|
||||||
|
);
|
||||||
|
return explode_dimension(&local, doc)
|
||||||
|
.into_iter()
|
||||||
|
.map(|entity| plane.place_entity(entity))
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
let base = dim.base();
|
let base = dim.base();
|
||||||
let met = dim_metrics(dim, doc);
|
let met = dim_metrics(dim, doc);
|
||||||
@@ -1659,6 +1672,58 @@ mod tests {
|
|||||||
assert_eq!(solids, 2, "expected 2 filled (SOLID) arrowheads, got {solids}");
|
assert_eq!(solids, 2, "expected 2 filled (SOLID) arrowheads, got {solids}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The line segments EXPLODE turns the dimension into.
|
||||||
|
fn exploded_segments(dim: &Dimension, doc: &CadDocument) -> Vec<(DVec3, DVec3)> {
|
||||||
|
let point = |v: Vector3| DVec3::new(v.x, v.y, v.z);
|
||||||
|
explode_dimension(dim, doc)
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|entity| match entity {
|
||||||
|
EntityType::Line(line) => Some((point(line.start), point(line.end))),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// EXPLODE of a linear dimension in a working plane off world Z leaves
|
||||||
|
/// its lines in that plane. Turned 90° about X, the dimension has to
|
||||||
|
/// explode into the same lines turned with it; its offset, now along Z,
|
||||||
|
/// used to be read in world XY only.
|
||||||
|
#[test]
|
||||||
|
fn exploding_a_linear_dimension_off_world_z_keeps_its_plane() {
|
||||||
|
use crate::command::EntityTransform;
|
||||||
|
let doc = CadDocument::new();
|
||||||
|
let mut d = DimensionLinear::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0));
|
||||||
|
d.definition_point = Vector3::new(0.0, 5.0, 0.0);
|
||||||
|
let flat = Dimension::Linear(d);
|
||||||
|
let turn = glam::DQuat::from_axis_angle(DVec3::X, std::f64::consts::FRAC_PI_2);
|
||||||
|
let expected: Vec<(DVec3, DVec3)> = exploded_segments(&flat, &doc)
|
||||||
|
.into_iter()
|
||||||
|
.map(|(a, b)| (turn * a, turn * b))
|
||||||
|
.collect();
|
||||||
|
assert!(!expected.is_empty(), "the dimension explodes into no lines");
|
||||||
|
|
||||||
|
let mut turned = EntityType::Dimension(flat);
|
||||||
|
crate::scene::view::dispatch::apply_transform(
|
||||||
|
&mut turned,
|
||||||
|
&EntityTransform::Rotate {
|
||||||
|
center: DVec3::ZERO,
|
||||||
|
axis: DVec3::X,
|
||||||
|
angle_rad: std::f64::consts::FRAC_PI_2,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let EntityType::Dimension(turned) = turned else {
|
||||||
|
panic!("dimension expected");
|
||||||
|
};
|
||||||
|
let lines = exploded_segments(&turned, &doc);
|
||||||
|
assert_eq!(lines.len(), expected.len(), "line count");
|
||||||
|
for (index, (got, want)) in lines.iter().zip(&expected).enumerate() {
|
||||||
|
assert!(
|
||||||
|
got.0.abs_diff_eq(want.0, 1e-9) && got.1.abs_diff_eq(want.1, 1e-9),
|
||||||
|
"line {index}: expected {want:?}, got {got:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// An angular dimension must bake its swept ARC (not just two rays), else a
|
// An angular dimension must bake its swept ARC (not just two rays), else a
|
||||||
// saved+reloaded angular dim collapses to a V. The block should carry the
|
// saved+reloaded angular dim collapses to a V. The block should carry the
|
||||||
// two extension lines plus many arc chords.
|
// two extension lines plus many arc chords.
|
||||||
|
|||||||
Reference in new issue
Block a user