forked from 1b-agent/opencadstudio-upstream
Merge origin/main
This commit is contained in:
commit
0f02a74eb1
3 files changed
+172
-92
No files matched your search
@@ -6772,6 +6772,48 @@ mod selection_preview_tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn xline_direction_grip_drag_changes_direction() {
|
||||||
|
use acadrust::{types::Vector3, EntityType};
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
let i = app.active_tab;
|
||||||
|
app.tabs[i].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
|
||||||
|
// Base away from the model origin so the UCS icon cannot swallow
|
||||||
|
// the press (it sits at the origin in a fresh drawing).
|
||||||
|
let handle = app.tabs[i].scene.add_entity(EntityType::XLine(
|
||||||
|
acadrust::entities::XLine::new(
|
||||||
|
Vector3::new(100.0, 50.0, 0.0),
|
||||||
|
Vector3::new(0.0, 1.0, 0.0),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
app.tabs[i].scene.selected.insert(handle);
|
||||||
|
app.refresh_selected_grips();
|
||||||
|
assert_eq!(app.tabs[i].selected_grips.len(), 2);
|
||||||
|
// Press on grip 1 (direction handle) and drag it to point along +X.
|
||||||
|
let bounds = iced::Rectangle::with_size(iced::Size::new(800.0, 600.0));
|
||||||
|
let g1world = app.tabs[i].selected_grips[1].world;
|
||||||
|
let g1cursor = app.tabs[i].scene.camera.borrow().project(g1world, bounds).unwrap();
|
||||||
|
app.tabs[i].scene.selection.borrow_mut().last_move_pos =
|
||||||
|
Some(iced::Point::new(g1cursor.x, g1cursor.y));
|
||||||
|
let _ = app.on_viewport_left_press();
|
||||||
|
assert_eq!(
|
||||||
|
app.tabs[i].active_grip.as_ref().map(|g| g.grip_id),
|
||||||
|
Some(1),
|
||||||
|
"press on the direction grip must engage grip 1"
|
||||||
|
);
|
||||||
|
let dir_target = glam::DVec3::new(200.0, 50.0, 0.0);
|
||||||
|
let dir_cursor = app.tabs[i].scene.camera.borrow().project(dir_target, bounds).unwrap();
|
||||||
|
let _ = app.on_viewport_move(iced::Point::new(dir_cursor.x, dir_cursor.y));
|
||||||
|
let EntityType::XLine(moved) = app.tabs[i].scene.document.get_entity(handle).unwrap()
|
||||||
|
else { panic!("expected xline") };
|
||||||
|
assert!(
|
||||||
|
(moved.direction.x - 1.0).abs() < 1e-6 && moved.direction.y.abs() < 1e-6,
|
||||||
|
"direction grip drag must point the xline along +X, got {:?}",
|
||||||
|
moved.direction
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn constraint_glyph_tooltip_appears_after_hover_dwell() {
|
fn constraint_glyph_tooltip_appears_after_hover_dwell() {
|
||||||
use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef, ParametricScope};
|
use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef, ParametricScope};
|
||||||
|
|||||||
+8
-4
@@ -3,7 +3,7 @@ use crate::t;
|
|||||||
|
|
||||||
use crate::command::EntityTransform;
|
use crate::command::EntityTransform;
|
||||||
use crate::entities::common::{
|
use crate::entities::common::{
|
||||||
center_grip, edit_prop as edit, format_length, ro_prop as ro, square_grip,
|
edit_prop as edit, format_length, ro_prop as ro, square_grip,
|
||||||
};
|
};
|
||||||
use crate::entities::curve::{point_along, unit_direction};
|
use crate::entities::curve::{point_along, unit_direction};
|
||||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
|
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
|
||||||
@@ -48,11 +48,14 @@ impl Grippable for Ray {
|
|||||||
let bp = &self.base_point;
|
let bp = &self.base_point;
|
||||||
let dir = &self.direction;
|
let dir = &self.direction;
|
||||||
// Grip 0: base point (movable)
|
// Grip 0: base point (movable)
|
||||||
// Grip 1: a point along the direction (changes direction)
|
// Grip 1: a point along the direction (changes direction). A stretch
|
||||||
|
// grip, not a move handle: `center_grip` would flag it `is_midpoint`
|
||||||
|
// and the drag would arrive as `Translate`, which the direction arm
|
||||||
|
// ignores — the grip would silently do nothing.
|
||||||
let guide_dist = 10.0_f64;
|
let guide_dist = 10.0_f64;
|
||||||
vec![
|
vec![
|
||||||
square_grip(0, glam::DVec3::new(bp.x, bp.y, bp.z)),
|
square_grip(0, glam::DVec3::new(bp.x, bp.y, bp.z)),
|
||||||
center_grip(
|
square_grip(
|
||||||
1,
|
1,
|
||||||
glam::DVec3::new(
|
glam::DVec3::new(
|
||||||
bp.x + dir.x * guide_dist,
|
bp.x + dir.x * guide_dist,
|
||||||
@@ -191,7 +194,8 @@ impl Grippable for XLine {
|
|||||||
let guide_dist = 10.0_f64;
|
let guide_dist = 10.0_f64;
|
||||||
vec![
|
vec![
|
||||||
square_grip(0, glam::DVec3::new(bp.x, bp.y, bp.z)),
|
square_grip(0, glam::DVec3::new(bp.x, bp.y, bp.z)),
|
||||||
center_grip(
|
// Direction handle: a stretch grip, not a move handle (see Ray).
|
||||||
|
square_grip(
|
||||||
1,
|
1,
|
||||||
glam::DVec3::new(
|
glam::DVec3::new(
|
||||||
bp.x + dir.x * guide_dist,
|
bp.x + dir.x * guide_dist,
|
||||||
|
|||||||
+122
-88
@@ -88,6 +88,7 @@ fn entity_boundary_source_on_plane(
|
|||||||
entity: &EntityType,
|
entity: &EntityType,
|
||||||
plane: WorkingPlane,
|
plane: WorkingPlane,
|
||||||
tolerance: f64,
|
tolerance: f64,
|
||||||
|
clip_bounds: Option<([f64; 2], [f64; 2])>,
|
||||||
) -> Option<BoundarySource> {
|
) -> Option<BoundarySource> {
|
||||||
let curves = entity_curves_on_plane(entity, plane, tolerance);
|
let curves = entity_curves_on_plane(entity, plane, tolerance);
|
||||||
|
|
||||||
@@ -95,27 +96,92 @@ fn entity_boundary_source_on_plane(
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let segments: Vec<Line> = curves
|
let segments = live_segments_for_curves(&curves, clip_bounds, tolerance);
|
||||||
.iter()
|
|
||||||
.flat_map(|curve| {
|
|
||||||
curve
|
|
||||||
.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE)
|
|
||||||
.windows(2)
|
|
||||||
.filter_map(|pair| {
|
|
||||||
let start = pair[0];
|
|
||||||
let end = pair[1];
|
|
||||||
|
|
||||||
(start.iter().chain(&end).all(|value| value.is_finite())
|
|
||||||
&& start != end)
|
|
||||||
.then_some(Line { start, end })
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
(!segments.is_empty()).then_some(BoundarySource { segments, curves })
|
(!segments.is_empty()).then_some(BoundarySource { segments, curves })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Finite extents of boundary segments, expanded by `tolerance` so clipped
|
||||||
|
/// endpoints weld with the segments that produced the box. `None` when
|
||||||
|
/// there is nothing finite to clip against.
|
||||||
|
fn segments_bounds(segments: &[Line], tolerance: f64) -> Option<([f64; 2], [f64; 2])> {
|
||||||
|
let mut bounds: Option<([f64; 2], [f64; 2])> = None;
|
||||||
|
for segment in segments {
|
||||||
|
for point in [segment.start, segment.end] {
|
||||||
|
bounds = Some(match bounds {
|
||||||
|
None => ([point[0], point[1]], [point[0], point[1]]),
|
||||||
|
Some((min, max)) => (
|
||||||
|
[min[0].min(point[0]), min[1].min(point[1])],
|
||||||
|
[max[0].max(point[0]), max[1].max(point[1])],
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bounds.map(|(min, max)| {
|
||||||
|
(
|
||||||
|
[min[0] - tolerance, min[1] - tolerance],
|
||||||
|
[max[0] + tolerance, max[1] + tolerance],
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tessellate live curves to finite segments. Bounded curves tessellate as
|
||||||
|
/// always; rays and infinite lines are clipped to `clip_bounds` instead —
|
||||||
|
/// tessellating them raw yields a unit stub that can never close a face.
|
||||||
|
/// With no bounds there is nothing to clip against, so unbounded curves
|
||||||
|
/// contribute nothing.
|
||||||
|
fn live_segments_for_curves(
|
||||||
|
curves: &[Curve],
|
||||||
|
clip_bounds: Option<([f64; 2], [f64; 2])>,
|
||||||
|
tolerance: f64,
|
||||||
|
) -> Vec<Line> {
|
||||||
|
curves
|
||||||
|
.iter()
|
||||||
|
.flat_map(|curve| {
|
||||||
|
match curve {
|
||||||
|
Curve::Ray(ray) => clip_bounds
|
||||||
|
.and_then(|(min, max)| {
|
||||||
|
clip_unbounded_to_bounds(
|
||||||
|
ray.origin,
|
||||||
|
ray.direction,
|
||||||
|
true,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
tolerance,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
Curve::XLine(line) => clip_bounds
|
||||||
|
.and_then(|(min, max)| {
|
||||||
|
clip_unbounded_to_bounds(
|
||||||
|
line.base,
|
||||||
|
line.direction,
|
||||||
|
false,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
tolerance,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
_ => curve
|
||||||
|
.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE)
|
||||||
|
.windows(2)
|
||||||
|
.filter_map(|pair| {
|
||||||
|
let start = pair[0];
|
||||||
|
let end = pair[1];
|
||||||
|
|
||||||
|
(start.iter().chain(&end).all(|value| value.is_finite())
|
||||||
|
&& start != end)
|
||||||
|
.then_some(Line { start, end })
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn hatch_path_seed(path: &acadrust::entities::BoundaryPath) -> Option<[f64; 2]> {
|
fn hatch_path_seed(path: &acadrust::entities::BoundaryPath) -> Option<[f64; 2]> {
|
||||||
let mut ring = Vec::new();
|
let mut ring = Vec::new();
|
||||||
for edge in &path.edges {
|
for edge in &path.edges {
|
||||||
@@ -794,6 +860,14 @@ impl Scene {
|
|||||||
}
|
}
|
||||||
association_changed |= path.boundary_handles.len() != old_count;
|
association_changed |= path.boundary_handles.len() != old_count;
|
||||||
modified |= association_changed;
|
modified |= association_changed;
|
||||||
|
// Finite extents for clipping unbounded live sources (rays /
|
||||||
|
// construction lines) below. `all_sources` already went
|
||||||
|
// through the same clipping in `boundary_sources_on_plane`.
|
||||||
|
let all_segments: Vec<Line> = all_sources
|
||||||
|
.values()
|
||||||
|
.flat_map(|source| source.segments.iter().copied())
|
||||||
|
.collect();
|
||||||
|
let clip_bounds = segments_bounds(&all_segments, WELD_TOLERANCE);
|
||||||
let sources: rustc_hash::FxHashMap<_, _> = path
|
let sources: rustc_hash::FxHashMap<_, _> = path
|
||||||
.boundary_handles
|
.boundary_handles
|
||||||
.iter()
|
.iter()
|
||||||
@@ -809,6 +883,7 @@ impl Scene {
|
|||||||
entity,
|
entity,
|
||||||
plane,
|
plane,
|
||||||
WELD_TOLERANCE,
|
WELD_TOLERANCE,
|
||||||
|
clip_bounds,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.or_else(|| all_sources.get(source).cloned());
|
.or_else(|| all_sources.get(source).cloned());
|
||||||
@@ -895,32 +970,14 @@ impl Scene {
|
|||||||
.segments
|
.segments
|
||||||
.extend(segments);
|
.extend(segments);
|
||||||
}
|
}
|
||||||
// Finite extents of the wire geometry. Rays and infinite
|
// Finite extents of the wire geometry, so rays and infinite
|
||||||
// construction lines are clipped to this box below: tessellating
|
// construction lines clip to something real instead of tessellating
|
||||||
// them raw yields a single unit stub at the base point, which can
|
// to a unit stub (see `live_segments_for_curves`).
|
||||||
// never close a face (and would overwrite the wire segments with
|
let all_wire_segments: Vec<Line> = sources
|
||||||
// it). Expanded by the weld tolerance so clipped endpoints meet
|
.values()
|
||||||
// the segments that produced the box.
|
.flat_map(|source| source.segments.iter().copied())
|
||||||
let mut bounds: Option<([f64; 2], [f64; 2])> = None;
|
.collect();
|
||||||
for source in sources.values() {
|
let bounds = segments_bounds(&all_wire_segments, tolerance);
|
||||||
for segment in &source.segments {
|
|
||||||
for point in [segment.start, segment.end] {
|
|
||||||
bounds = Some(match bounds {
|
|
||||||
None => ([point[0], point[1]], [point[0], point[1]]),
|
|
||||||
Some((min, max)) => (
|
|
||||||
[min[0].min(point[0]), min[1].min(point[1])],
|
|
||||||
[max[0].max(point[0]), max[1].max(point[1])],
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let bounds = bounds.map(|(min, max)| {
|
|
||||||
(
|
|
||||||
[min[0] - tolerance, min[1] - tolerance],
|
|
||||||
[max[0] + tolerance, max[1] + tolerance],
|
|
||||||
)
|
|
||||||
});
|
|
||||||
for (&handle, source) in &mut sources {
|
for (&handle, source) in &mut sources {
|
||||||
let curves = self
|
let curves = self
|
||||||
.document
|
.document
|
||||||
@@ -931,51 +988,7 @@ impl Scene {
|
|||||||
// Associative hatch regeneration must use the entity's live geometry,
|
// Associative hatch regeneration must use the entity's live geometry,
|
||||||
// not a potentially one-frame-old display wire. Grip edits and STRETCH
|
// not a potentially one-frame-old display wire. Grip edits and STRETCH
|
||||||
// mutate the document before the resident wire cache catches up.
|
// mutate the document before the resident wire cache catches up.
|
||||||
let live_segments: Vec<Line> = curves
|
let live_segments = live_segments_for_curves(&curves, bounds, tolerance);
|
||||||
.iter()
|
|
||||||
.flat_map(|curve| {
|
|
||||||
match curve {
|
|
||||||
Curve::Ray(ray) => bounds
|
|
||||||
.and_then(|(min, max)| {
|
|
||||||
clip_unbounded_to_bounds(
|
|
||||||
ray.origin,
|
|
||||||
ray.direction,
|
|
||||||
true,
|
|
||||||
min,
|
|
||||||
max,
|
|
||||||
tolerance,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.into_iter()
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
Curve::XLine(line) => bounds
|
|
||||||
.and_then(|(min, max)| {
|
|
||||||
clip_unbounded_to_bounds(
|
|
||||||
line.base,
|
|
||||||
line.direction,
|
|
||||||
false,
|
|
||||||
min,
|
|
||||||
max,
|
|
||||||
tolerance,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.into_iter()
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
_ => curve
|
|
||||||
.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE)
|
|
||||||
.windows(2)
|
|
||||||
.filter_map(|pair| {
|
|
||||||
let start = pair[0];
|
|
||||||
let end = pair[1];
|
|
||||||
|
|
||||||
(start.iter().chain(&end).all(|value| value.is_finite())
|
|
||||||
&& start != end)
|
|
||||||
.then_some(Line { start, end })
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if !live_segments.is_empty() {
|
if !live_segments.is_empty() {
|
||||||
source.segments = live_segments;
|
source.segments = live_segments;
|
||||||
@@ -1196,6 +1209,27 @@ mod boundary_xline_tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn xline_source_clips_to_given_bounds() {
|
||||||
|
let xline = EntityType::XLine(acadrust::entities::XLine::new(
|
||||||
|
acadrust::types::Vector3::new(5.0, 0.0, 0.0),
|
||||||
|
acadrust::types::Vector3::new(0.0, 1.0, 0.0),
|
||||||
|
));
|
||||||
|
let source = entity_boundary_source_on_plane(
|
||||||
|
&xline,
|
||||||
|
crate::command::WorkingPlane::default(),
|
||||||
|
1.0e-6,
|
||||||
|
Some(([0.0, 0.0], [10.0, 10.0])),
|
||||||
|
)
|
||||||
|
.expect("xline crossing the box must produce a source");
|
||||||
|
assert_eq!(source.segments.len(), 1);
|
||||||
|
let segment = &source.segments[0];
|
||||||
|
assert!((segment.start[0] - 5.0).abs() < 1e-6, "got {:?}", segment.start);
|
||||||
|
assert!((segment.start[1] - 0.0).abs() < 1e-6, "got {:?}", segment.start);
|
||||||
|
assert!((segment.end[0] - 5.0).abs() < 1e-6, "got {:?}", segment.end);
|
||||||
|
assert!((segment.end[1] - 10.0).abs() < 1e-6, "got {:?}", segment.end);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn xline_missing_the_geometry_contributes_no_face() {
|
fn xline_missing_the_geometry_contributes_no_face() {
|
||||||
let mut scene = Scene::new();
|
let mut scene = Scene::new();
|
||||||
|
|||||||
Reference in new issue
Block a user