forked from 1b-agent/opencadstudio-upstream
Fix constrained corner and edge grip behavior
This commit is contained in:
1 parent
1450fdeedf
commit
01a23673f1
3 files changed
+315
-29
No files matched your search
+84
-20
@@ -6236,7 +6236,7 @@ mod selection_preview_tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constrained_polyline_vertex_grip_reshapes_instead_of_translating() {
|
||||
fn constrained_rectangle_grips_distinguish_perpendicular_corner_and_free_ends() {
|
||||
use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef, ParametricScope};
|
||||
use acadrust::{entities::LwPolyline, types::Vector2, EntityType};
|
||||
|
||||
@@ -6248,6 +6248,7 @@ mod selection_preview_tests {
|
||||
app.snapper.otrack_enabled = false;
|
||||
app.ortho_mode = false;
|
||||
app.polar_mode = false;
|
||||
app.constraint_solve_mode = true;
|
||||
app.tabs[i].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
|
||||
let mut rectangle = LwPolyline::from_points(vec![
|
||||
Vector2::new(0.0, 0.0),
|
||||
@@ -6262,6 +6263,14 @@ mod selection_preview_tests {
|
||||
set.add(ConstraintKind::Parallel, vec![ParametricRef::segment(handle, 1), ParametricRef::segment(handle, 3)], None);
|
||||
set.add(ConstraintKind::Perpendicular, vec![ParametricRef::segment(handle, 3), ParametricRef::segment(handle, 2)], None);
|
||||
set.add(ConstraintKind::Horizontal, vec![ParametricRef::segment(handle, 2)], None);
|
||||
let handle = if let Ok(path) = std::env::var("OCS_GRIP_TEST_DRAWING") {
|
||||
let result = app.automation_op(&serde_json::json!({"op":"open","path":path}).to_string());
|
||||
assert_eq!(result["ok"], true, "{result}");
|
||||
app.tabs[i].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
|
||||
Handle::new(0x556)
|
||||
} else {
|
||||
handle
|
||||
};
|
||||
let vertices = |app: &OpenCADStudio| {
|
||||
let EntityType::LwPolyline(polyline) =
|
||||
app.tabs[i].scene.document.get_entity(handle).unwrap()
|
||||
@@ -6271,27 +6280,82 @@ mod selection_preview_tests {
|
||||
polyline.vertices.iter().map(|vertex| vertex.location).collect::<Vec<_>>()
|
||||
};
|
||||
let before = vertices(&app);
|
||||
app.tabs[i].active_grip = Some(GripEdit::single(handle, 0, false, glam::DVec3::ZERO));
|
||||
let height = before[3].y - before[0].y;
|
||||
let width = before[1].x - before[0].x;
|
||||
for grip_id in 0..8 {
|
||||
let vertex = if grip_id < 4 { before[grip_id] }
|
||||
else { (before[grip_id - 4] + before[(grip_id - 3) % 4]) * 0.5 };
|
||||
let origin = glam::DVec3::new(vertex.x, vertex.y, 0.0);
|
||||
app.tabs[i].active_grip = Some(GripEdit::single(handle, grip_id, grip_id >= 4, origin));
|
||||
for offset in [[-1.0, height], [-1.0, 3.0], [-2.0, 6.0], [-2.0, 6.0], [1.0, -4.0]] {
|
||||
let target = origin + glam::DVec3::new(offset[0], offset[1], 0.0);
|
||||
let cursor = app.tabs[i].scene.camera.borrow().project(target,
|
||||
iced::Rectangle::with_size(iced::Size::new(800.0, 600.0))).unwrap();
|
||||
let _ = app.on_viewport_move(Point::new(cursor.x, cursor.y));
|
||||
let after = vertices(&app);
|
||||
let (left, right, bottom, top) = match grip_id {
|
||||
0 => (target.x, target.x + width, target.y, before[3].y),
|
||||
1 => (before[0].x, target.x, target.y, before[3].y),
|
||||
2 => (before[0].x, target.x, target.y - height, target.y),
|
||||
3 => (target.x, target.x + width, target.y - height, target.y),
|
||||
4 => (before[0].x + offset[0], before[1].x + offset[0], target.y, before[3].y),
|
||||
5 => (before[0].x, target.x, before[0].y + offset[1], before[3].y + offset[1]),
|
||||
6 => (before[0].x + offset[0], before[1].x + offset[0], before[0].y, target.y),
|
||||
7 => (target.x, before[1].x, before[0].y + offset[1], before[3].y + offset[1]),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let expected = [Vector2::new(left, bottom), Vector2::new(right, bottom),
|
||||
Vector2::new(right, top), Vector2::new(left, top)];
|
||||
for (actual, expected) in after.iter().zip(expected) {
|
||||
assert!((*actual - expected).length_squared() < 1e-10,
|
||||
"grip {grip_id}: rectangle collapsed or drifted: {after:?}, expected {expected:?}");
|
||||
}
|
||||
}
|
||||
let preview = vertices(&app);
|
||||
let _ = app.on_viewport_left_release();
|
||||
assert_eq!(vertices(&app), preview);
|
||||
app.undo_steps(1);
|
||||
assert_eq!(vertices(&app), before);
|
||||
app.redo_steps(1);
|
||||
assert_eq!(vertices(&app), preview);
|
||||
app.undo_steps(1);
|
||||
}
|
||||
}
|
||||
|
||||
let _ = app.on_viewport_move(Point::new(460.0, 220.0));
|
||||
#[test]
|
||||
fn axis_constrained_line_grip_changes_length_and_translates_normal_to_axis() {
|
||||
use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef, ParametricScope};
|
||||
use acadrust::{entities::Line, types::Vector3, EntityType};
|
||||
|
||||
let after = vertices(&app);
|
||||
let dragged = after[0] - before[0];
|
||||
assert!(dragged.length_squared() > 1.0e-12);
|
||||
assert!(after.iter().zip(&before).skip(1).any(|(after, before)|
|
||||
(*after - *before - dragged).length_squared() > 1.0e-12));
|
||||
let segments = [
|
||||
after[1] - after[0],
|
||||
after[2] - after[1],
|
||||
after[3] - after[2],
|
||||
after[0] - after[3],
|
||||
];
|
||||
let cross = |a: Vector2, b: Vector2| a.x * b.y - a.y * b.x;
|
||||
let dot = |a: Vector2, b: Vector2| a.x * b.x + a.y * b.y;
|
||||
assert!(cross(segments[0], segments[2]).abs() < 1.0e-6);
|
||||
assert!(cross(segments[1], segments[3]).abs() < 1.0e-6);
|
||||
assert!(dot(segments[3], segments[2]).abs() < 1.0e-6);
|
||||
assert!(segments[2].y.abs() < 1.0e-6);
|
||||
for vertical in [false, true] {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.automation_op(r#"{"op":"new"}"#);
|
||||
let i = app.active_tab;
|
||||
app.snapper.snap_enabled = false;
|
||||
app.snapper.grid_snap_on = false;
|
||||
app.snapper.otrack_enabled = false;
|
||||
app.ortho_mode = false;
|
||||
app.polar_mode = false;
|
||||
app.constraint_solve_mode = true;
|
||||
app.tabs[i].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
|
||||
let end = if vertical { Vector3::new(0.0, 4.0, 0.0) } else { Vector3::new(4.0, 0.0, 0.0) };
|
||||
let handle = app.tabs[i].scene.add_entity(EntityType::Line(Line::from_points(Vector3::ZERO, end)));
|
||||
app.tabs[i].scene.parametric_constraint_set_mut(ParametricScope::ModelSpace).add(
|
||||
if vertical { ConstraintKind::Vertical } else { ConstraintKind::Horizontal },
|
||||
vec![ParametricRef::whole(handle)], None);
|
||||
app.tabs[i].active_grip = Some(GripEdit::single(handle, 0, false, glam::DVec3::ZERO));
|
||||
for target in [glam::DVec3::new(-1.0, 3.0, 0.0), glam::DVec3::new(2.0, -2.0, 0.0)] {
|
||||
let cursor = app.tabs[i].scene.camera.borrow().project(target,
|
||||
iced::Rectangle::with_size(iced::Size::new(800.0, 600.0))).unwrap();
|
||||
let _ = app.on_viewport_move(Point::new(cursor.x, cursor.y));
|
||||
let EntityType::Line(line) = app.tabs[i].scene.document.get_entity(handle).unwrap()
|
||||
else { panic!("line") };
|
||||
let opposite = if vertical { Vector3::new(target.x, end.y, 0.0) }
|
||||
else { Vector3::new(end.x, target.y, 0.0) };
|
||||
assert!((line.start - Vector3::new(target.x, target.y, 0.0)).length() < 1e-5);
|
||||
assert!((line.end - opposite).length() < 1e-5, "{line:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -83,22 +83,30 @@ impl ParametricRef {
|
||||
}
|
||||
}
|
||||
|
||||
/// Point references temporarily pinned while solving a native grip edit.
|
||||
/// Both endpoints of an edited line are exact inputs: the opposite endpoint
|
||||
/// stays fixed while the grabbed endpoint follows the cursor.
|
||||
/// Grabbed points are exact kernel inputs; the solver anchors the remaining
|
||||
/// endpoint coordinates according to the line's directional constraints.
|
||||
pub(crate) fn grip_solve_anchor_refs(
|
||||
entity: &acadrust::EntityType,
|
||||
handle: Handle,
|
||||
grip_id: usize,
|
||||
) -> Vec<ParametricRef> {
|
||||
match entity {
|
||||
acadrust::EntityType::Line(_) if grip_id <= 1 => vec![
|
||||
ParametricRef::point(handle, 0),
|
||||
ParametricRef::point(handle, 1),
|
||||
],
|
||||
acadrust::EntityType::Line(_) if grip_id <= 1 => {
|
||||
vec![ParametricRef::point(handle, grip_id as i32)]
|
||||
}
|
||||
acadrust::EntityType::LwPolyline(polyline) if grip_id < polyline.vertices.len() => {
|
||||
vec![ParametricRef::point(handle, grip_id as i32)]
|
||||
}
|
||||
acadrust::EntityType::LwPolyline(polyline) => {
|
||||
let segment = grip_id - polyline.vertices.len();
|
||||
if polyline.vertices.get(segment).is_some_and(|vertex| vertex.bulge.abs() < 1e-9)
|
||||
&& (segment + 1 < polyline.vertices.len() || polyline.is_closed)
|
||||
{
|
||||
vec![ParametricRef::segment(handle, segment)]
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
acadrust::EntityType::Polyline2D(polyline) if grip_id < polyline.vertices.len() => {
|
||||
vec![ParametricRef::point(handle, grip_id as i32)]
|
||||
}
|
||||
|
||||
@@ -1766,6 +1766,37 @@ fn retained_radius(entity: &EntityType) -> Option<f64> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Propagate explicit axis constraints through parallel/perpendicular relations.
|
||||
fn constrained_line_axes(set: &ParametricConstraintSet) -> HashMap<ParametricRef, bool> {
|
||||
let mut axes = HashMap::new();
|
||||
for c in set.constraints.iter().filter(|c| c.enabled) {
|
||||
if let [reference] = c.refs.as_slice() {
|
||||
if matches!(c.kind, ConstraintKind::Horizontal | ConstraintKind::Vertical) {
|
||||
axes.insert(*reference, c.kind == ConstraintKind::Vertical);
|
||||
}
|
||||
}
|
||||
}
|
||||
loop {
|
||||
let before = axes.len();
|
||||
for c in set.constraints.iter().filter(|c| c.enabled) {
|
||||
if !matches!(c.kind, ConstraintKind::Parallel | ConstraintKind::Perpendicular | ConstraintKind::Colinear) {
|
||||
continue;
|
||||
}
|
||||
let [a, b] = c.refs.as_slice() else { continue };
|
||||
let perpendicular = c.kind == ConstraintKind::Perpendicular;
|
||||
if let Some(axis) = axes.get(a).copied() {
|
||||
axes.entry(*b).or_insert(axis ^ perpendicular);
|
||||
}
|
||||
if let Some(axis) = axes.get(b).copied() {
|
||||
axes.entry(*a).or_insert(axis ^ perpendicular);
|
||||
}
|
||||
}
|
||||
if before == axes.len() {
|
||||
return axes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rebuilds `set`'s entire `cadkernel_constraints::System` from current document
|
||||
/// geometry, solves it, and returns the resulting entity states for every
|
||||
/// handle whose registered coordinates actually moved beyond floating-point
|
||||
@@ -1793,6 +1824,7 @@ fn solve_scope(
|
||||
};
|
||||
let mut sys = System::new();
|
||||
let mut cache: HashMap<Handle, EntityGeom> = HashMap::new();
|
||||
let line_axes = if retain_lengths { HashMap::new() } else { constrained_line_axes(set) };
|
||||
// Tracks which system-level `cadkernel_constraints` constraint(s) came from which
|
||||
// `ParametricConstraint` — a `SubSystem`'s redundant-row indices (from
|
||||
// `cadkernel_constraints::diagnosis`) are local to that partition's own constraint
|
||||
@@ -1804,6 +1836,23 @@ fn solve_scope(
|
||||
let mut owner: Vec<(Rc<dyn Constraint>, ConstraintId)> = Vec::new();
|
||||
|
||||
for c in &set.constraints {
|
||||
if c.enabled && matches!(c.kind, ConstraintKind::Parallel | ConstraintKind::Perpendicular)
|
||||
&& c.refs.len() == 2 && c.refs.iter().all(|r| line_axes.contains_key(r))
|
||||
&& (line_axes[&c.refs[0]] ^ line_axes[&c.refs[1]]) == (c.kind == ConstraintKind::Perpendicular)
|
||||
&& refs_share_supported_plane(document, &c.refs)
|
||||
&& c.refs.iter().all(|reference| {
|
||||
match resolve_ref(document, &mut sys, &mut cache, *reference) {
|
||||
Some(EntityGeom::Line(_)) => true,
|
||||
Some(geometry @ EntityGeom::Polyline { .. }) => reference.segment_index()
|
||||
.and_then(|index| geometry.line_segment(index)).is_some(),
|
||||
_ => false,
|
||||
}
|
||||
})
|
||||
{
|
||||
// Implied axis equations below replace normalized angle equations,
|
||||
// which become ill-conditioned when a dragged edge crosses zero.
|
||||
continue;
|
||||
}
|
||||
let tangent_side = retained_tangent_side(document, retained_before, c);
|
||||
let tangent_point = (c.kind == ConstraintKind::Tangent && c.refs.len() == 2
|
||||
&& c.refs[0].entity != c.refs[1].entity
|
||||
@@ -1837,6 +1886,126 @@ fn solve_scope(
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut anchors = driven_refs.to_vec();
|
||||
if !retain_lengths {
|
||||
let mut axis_lines: Vec<_> = line_axes.into_iter().filter_map(|(reference, vertical)| {
|
||||
let geometry = cache.get(&reference.entity)?;
|
||||
let line = match geometry {
|
||||
EntityGeom::Line(line) => *line,
|
||||
EntityGeom::Polyline { .. } => geometry.line_segment(reference.segment_index()?)?,
|
||||
_ => return None,
|
||||
};
|
||||
Some((reference, line, vertical))
|
||||
}).collect();
|
||||
axis_lines.sort_by_key(|(reference, _, _)| (reference.entity.value(), reference.marker));
|
||||
let driven_points: Vec<_> = driven_refs.iter().filter_map(|reference|
|
||||
resolve_constraint_point(document, &mut sys, &mut cache, *reference)).collect();
|
||||
let coincident: Vec<_> = set.constraints.iter().filter(|c|
|
||||
c.enabled && c.kind == ConstraintKind::Coincident).filter_map(|c| {
|
||||
let [a, b] = c.refs.as_slice() else { return None };
|
||||
Some((resolve_constraint_point(document, &mut sys, &mut cache, *a)?,
|
||||
resolve_constraint_point(document, &mut sys, &mut cache, *b)?))
|
||||
}).collect();
|
||||
let point_group = |point| {
|
||||
let mut points = vec![point];
|
||||
let mut cursor = 0;
|
||||
while cursor < points.len() {
|
||||
for (a, b) in &coincident {
|
||||
let next = if points[cursor] == *a { Some(*b) }
|
||||
else if points[cursor] == *b { Some(*a) } else { None };
|
||||
if let Some(next) = next.filter(|next| !points.contains(next)) {
|
||||
points.push(next);
|
||||
}
|
||||
}
|
||||
cursor += 1;
|
||||
}
|
||||
points
|
||||
};
|
||||
let perpendicular: Vec<_> = set.constraints.iter().filter(|c|
|
||||
c.enabled && c.kind == ConstraintKind::Perpendicular && c.refs.len() == 2).collect();
|
||||
let mut retained_axes = HashSet::new();
|
||||
for point in &driven_points {
|
||||
let group = point_group(*point);
|
||||
let incident: Vec<_> = axis_lines.iter().filter(|(_, line, _)|
|
||||
group.contains(&line.p1) || group.contains(&line.p2)).collect();
|
||||
if incident.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let common_corner = perpendicular.iter().any(|c| c.refs.iter()
|
||||
.all(|reference| incident.iter().any(|(r, _, _)| r == reference)));
|
||||
if common_corner && retain_size {
|
||||
// The shared right-angle corner carries its two legs together.
|
||||
retained_axes.extend([false, true]);
|
||||
continue;
|
||||
}
|
||||
let free_legs: Vec<_> = incident.iter().copied().filter(|(reference, _, _)|
|
||||
perpendicular.iter().any(|c| c.refs.contains(reference))).collect();
|
||||
let editable = if free_legs.is_empty() { &incident } else { &free_legs };
|
||||
retained_axes.extend([false, true].into_iter().filter(|axis|
|
||||
!editable.iter().any(|(_, _, vertical)| vertical == axis)));
|
||||
for (reference, line, vertical) in editable.iter().copied() {
|
||||
let opposite = if group.contains(&line.p1) { line.p2 } else { line.p1 };
|
||||
if !driven_points.contains(&opposite) {
|
||||
let parameter = if *vertical { opposite.y } else { opposite.x };
|
||||
// Only the along-edge coordinate stays fixed. The normal
|
||||
// coordinate follows the grabbed point through the kernel.
|
||||
if let Some(original) = retained_before.get(&reference.entity) {
|
||||
let points = super::dimension_assoc::source_points(original);
|
||||
let start = reference.segment_index().unwrap_or(0);
|
||||
let index = if opposite == line.p1 { start } else { (start + 1) % points.len().max(1) };
|
||||
if let Some(point) = points.get(index) {
|
||||
sys.store_mut().set(parameter, if *vertical { point.y } else { point.x });
|
||||
}
|
||||
}
|
||||
sys.store_mut().set_driven(parameter, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (reference, line, vertical) in &axis_lines {
|
||||
// These implied linear equations remain defined even while an
|
||||
// edge crosses zero length; cross-product equations do not.
|
||||
let (a, b) = if *vertical { (line.p1.x, line.p2.x) } else { (line.p1.y, line.p2.y) };
|
||||
sys.add_constraint(Rc::new(Equal::new(a, b, 1.0)));
|
||||
let paired_edge_is_dragged = set.constraints.iter().any(|c| {
|
||||
c.enabled && c.kind == ConstraintKind::Parallel && c.refs.contains(reference)
|
||||
&& c.refs.iter().any(|other| other != reference && driven_refs.contains(other))
|
||||
});
|
||||
if paired_edge_is_dragged && !driven_refs.contains(reference) {
|
||||
// Moving a whole edge changes its separation from the opposite
|
||||
// parallel edge, whose normal coordinate remains the reference.
|
||||
if let Some(original) = retained_before.get(&reference.entity) {
|
||||
let points = super::dimension_assoc::source_points(original);
|
||||
if let Some(point) = points.get(reference.segment_index().unwrap_or(0)) {
|
||||
sys.store_mut().set(a, if *vertical { point.x } else { point.y });
|
||||
sys.store_mut().set_driven(a, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
if retain_size && retained_axes.contains(vertical) {
|
||||
let Some(original) = retained_before.get(&reference.entity) else { continue };
|
||||
let points = super::dimension_assoc::source_points(original);
|
||||
let index = reference.segment_index().unwrap_or(0);
|
||||
let (Some(a), Some(b)) = (points.get(index), points.get(index + 1).or_else(|| points.first()))
|
||||
else { continue };
|
||||
let (a_param, b_param, delta) = if *vertical {
|
||||
(line.p1.y, line.p2.y, b.y - a.y)
|
||||
} else {
|
||||
(line.p1.x, line.p2.x, b.x - a.x)
|
||||
};
|
||||
let target = sys.add_param(delta, true);
|
||||
sys.add_constraint(Rc::new(Difference::new(a_param, b_param, target)));
|
||||
}
|
||||
}
|
||||
for reference in driven_refs {
|
||||
if matches!(document.get_entity(reference.entity), Some(EntityType::Line(_)))
|
||||
&& matches!(reference.marker, Some(0 | 1))
|
||||
&& !axis_lines.iter().any(|(line_ref, _, _)| line_ref.entity == reference.entity)
|
||||
{
|
||||
anchors.push(ParametricRef::point(reference.entity, 1 - reference.marker.unwrap()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Grip edits retain radii while connected lines resize to meet their
|
||||
// endpoints. Other edits can retain line lengths as well.
|
||||
if retain_size {
|
||||
@@ -2000,7 +2169,7 @@ fn solve_scope(
|
||||
// geometry parameters as driven keeps them exact and out of the solver's
|
||||
// free-variable list; an approximate equality equation would still allow
|
||||
// visible drift after repeated grip frames.
|
||||
for reference in driven_refs {
|
||||
for reference in &anchors {
|
||||
if !set
|
||||
.constraints
|
||||
.iter()
|
||||
@@ -2008,7 +2177,12 @@ fn solve_scope(
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let params_to_pin = if reference.marker.is_some() && reference.segment_index().is_none() {
|
||||
let params_to_pin = if let Some(index) = reference.segment_index() {
|
||||
resolve_ref(document, &mut sys, &mut cache, *reference)
|
||||
.and_then(|geometry| geometry.line_segment(index))
|
||||
.map(|line| vec![line.p1.x, line.p1.y, line.p2.x, line.p2.y])
|
||||
.unwrap_or_default()
|
||||
} else if reference.marker.is_some() {
|
||||
resolve_constraint_point(document, &mut sys, &mut cache, *reference)
|
||||
.map(|point| vec![point.x, point.y])
|
||||
.unwrap_or_default()
|
||||
@@ -2725,6 +2899,46 @@ mod tests {
|
||||
use acadrust::entities::EntityType;
|
||||
use acadrust::types::Vector3;
|
||||
|
||||
#[test]
|
||||
fn perpendicular_line_grips_distinguish_shared_corner_and_free_ends() {
|
||||
use acadrust::entities::Line;
|
||||
for (vertical, marker) in [(false, 0), (false, 1), (true, 1)] {
|
||||
let mut scene = Scene::new();
|
||||
let a = scene.add_entity(EntityType::Line(Line::from_points(
|
||||
Vector3::ZERO, Vector3::new(4.0, 0.0, 0.0))));
|
||||
let b = scene.add_entity(EntityType::Line(Line::from_points(
|
||||
Vector3::ZERO, Vector3::new(0.0, 2.0, 0.0))));
|
||||
let set = scene.parametric_constraint_set_mut(ParametricScope::ModelSpace);
|
||||
set.add(ConstraintKind::Horizontal, vec![ParametricRef::whole(a)], None);
|
||||
set.add(ConstraintKind::Perpendicular, vec![ParametricRef::whole(a), ParametricRef::whole(b)], None);
|
||||
set.add(ConstraintKind::Coincident, vec![ParametricRef::point(a, 0), ParametricRef::point(b, 0)], None);
|
||||
let originals = [a, b].map(|handle| (handle, scene.document.get_entity(handle).unwrap().clone()));
|
||||
let handle = if vertical { b } else { a };
|
||||
let target = Vector3::new(6.0, 3.0, 0.0);
|
||||
let Some(EntityType::Line(line)) = scene.document.get_entity_mut(handle) else { panic!("line") };
|
||||
if marker == 0 { line.start = target; } else { line.end = target; }
|
||||
let solved = scene.solve_parametric_constraints_preview(
|
||||
&[handle], &[ParametricRef::point(handle, marker)], true, &originals);
|
||||
for (handle, entity) in solved {
|
||||
*scene.document.get_entity_mut(handle).unwrap() = entity;
|
||||
}
|
||||
let expected = if marker == 0 {
|
||||
[[target, Vector3::new(10.0, 3.0, 0.0)], [target, Vector3::new(6.0, 5.0, 0.0)]]
|
||||
} else if vertical {
|
||||
[[Vector3::new(6.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0)],
|
||||
[Vector3::new(6.0, 0.0, 0.0), target]]
|
||||
} else {
|
||||
[[Vector3::new(0.0, 3.0, 0.0), target],
|
||||
[Vector3::new(0.0, 3.0, 0.0), Vector3::new(0.0, 5.0, 0.0)]]
|
||||
};
|
||||
for (handle, points) in [a, b].into_iter().zip(expected) {
|
||||
let Some(EntityType::Line(line)) = scene.document.get_entity(handle) else { panic!("line") };
|
||||
assert!((line.start - points[0]).length() < 1e-7, "{vertical}/{marker}: {line:?}");
|
||||
assert!((line.end - points[1]).length() < 1e-7, "{vertical}/{marker}: {line:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_rejects_unsupported_entity_shapes_and_planes() {
|
||||
let mut scene = Scene::new();
|
||||
|
||||
Reference in new issue
Block a user