forked from 1b-agent/opencadstudio-upstream
Fix connected constraint grip previews and preserve anchored endpoints
This commit is contained in:
1 parent
bfd0c30329
commit
cdf8bfe3ad
7 files changed
+367
-163
No files matched your search
@@ -118,8 +118,45 @@ impl OpenCADStudio {
|
|||||||
let _ = self.on_viewport_exit();
|
let _ = self.on_viewport_exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Roll a hot grip back to its pre-drag image and remove every grip-owned
|
pub(super) fn solve_grip_constraints(
|
||||||
/// overlay. Shared by Escape and drawing-space transitions.
|
&mut self,
|
||||||
|
i: usize,
|
||||||
|
grip: &crate::scene::pick::grip::GripEdit,
|
||||||
|
) {
|
||||||
|
let touched: Vec<_> = grip.targets.iter().map(|target| target.handle).collect();
|
||||||
|
let connected = self.tabs[i].scene.parametric_connected_handles(
|
||||||
|
self.tabs[i].current_parametric_scope(), &touched, true,
|
||||||
|
);
|
||||||
|
for handle in connected {
|
||||||
|
if !self.grip_originals.iter().any(|(original, _)| *original == handle) {
|
||||||
|
if let Some(entity) = self.tabs[i].scene.document.get_entity(handle).cloned() {
|
||||||
|
self.grip_originals.push((handle, entity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !self.grip_preview_handles.contains(&handle) {
|
||||||
|
self.grip_preview_handles.push(handle);
|
||||||
|
if !self.tabs[i].scene.meshes.contains_key(&handle) {
|
||||||
|
self.tabs[i].scene.preview_hidden.insert(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let driven_refs: Vec<_> = grip.targets.iter().flat_map(|target| {
|
||||||
|
self.tabs[i].scene.document.get_entity(target.handle)
|
||||||
|
.map(|entity| crate::scene::parametric_constraints::grip_solve_anchor_refs(
|
||||||
|
entity, target.handle, target.grip_id,
|
||||||
|
)).unwrap_or_default()
|
||||||
|
}).collect();
|
||||||
|
let solved = self.tabs[i].scene.solve_parametric_constraints_preview(
|
||||||
|
&touched, &driven_refs,
|
||||||
|
self.constraint_solve_mode && !driven_refs.is_empty(), &self.grip_originals,
|
||||||
|
);
|
||||||
|
for (handle, entity) in solved {
|
||||||
|
if let Some(slot) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
||||||
|
*slot = entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn capture_grip_history_originals(&mut self, i: usize, handles: &[Handle]) {
|
pub(super) fn capture_grip_history_originals(&mut self, i: usize, handles: &[Handle]) {
|
||||||
if !self.grip_history_originals.is_empty() {
|
if !self.grip_history_originals.is_empty() {
|
||||||
return;
|
return;
|
||||||
@@ -220,7 +257,7 @@ impl OpenCADStudio {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||||
.collect();
|
.collect();
|
||||||
self.tabs[i].scene.bump_entities(&changes);
|
self.tabs[i].scene.bump_entities_after_parametric_solve(&changes);
|
||||||
if let Some(dirty_before) = self.grip_dirty_before.take() {
|
if let Some(dirty_before) = self.grip_dirty_before.take() {
|
||||||
self.tabs[i].dirty = dirty_before;
|
self.tabs[i].dirty = dirty_before;
|
||||||
}
|
}
|
||||||
@@ -2190,7 +2227,7 @@ impl OpenCADStudio {
|
|||||||
let scope = self.tabs[i].current_parametric_scope();
|
let scope = self.tabs[i].current_parametric_scope();
|
||||||
handles = self.tabs[i]
|
handles = self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
.parametric_connected_handles(scope, &handles);
|
.parametric_connected_handles(scope, &handles, false);
|
||||||
handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
|
handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
|
||||||
}
|
}
|
||||||
let label = self.history_label_from_active_cmd(i, "MOVE");
|
let label = self.history_label_from_active_cmd(i, "MOVE");
|
||||||
|
|||||||
+1
-1
@@ -1072,7 +1072,7 @@ impl OpenCADStudio {
|
|||||||
// references visually stale after BEDIT closes.
|
// references visually stale after BEDIT closes.
|
||||||
scene.bump_geometry();
|
scene.bump_geometry();
|
||||||
} else {
|
} else {
|
||||||
scene.bump_entities(&final_changes);
|
scene.bump_entities_after_parametric_solve(&final_changes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -573,6 +573,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
.scene
|
.scene
|
||||||
.apply_grip(handle, grip_id, apply);
|
.apply_grip(handle, grip_id, apply);
|
||||||
}
|
}
|
||||||
|
self.solve_grip_constraints(i, &grip);
|
||||||
|
|
||||||
// Keep GripEdit synchronized so the normal commit
|
// Keep GripEdit synchronized so the normal commit
|
||||||
// path records the exact final position.
|
// path records the exact final position.
|
||||||
|
|||||||
+141
-61
@@ -1244,7 +1244,7 @@ impl OpenCADStudio {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut seen_handles = rustc_hash::FxHashSet::default();
|
let mut seen_handles = rustc_hash::FxHashSet::default();
|
||||||
let mut edited_handles: Vec<_> = grip
|
let edited_handles: Vec<_> = grip
|
||||||
.targets
|
.targets
|
||||||
.iter()
|
.iter()
|
||||||
.map(|target| target.handle)
|
.map(|target| target.handle)
|
||||||
@@ -1254,6 +1254,9 @@ impl OpenCADStudio {
|
|||||||
// Initialize once: constraint solves may add neighbors to this gesture.
|
// Initialize once: constraint solves may add neighbors to this gesture.
|
||||||
// Wire entities use the overlay; solid meshes stay visible and move live.
|
// Wire entities use the overlay; solid meshes stay visible and move live.
|
||||||
if self.grip_preview_handles.is_empty() {
|
if self.grip_preview_handles.is_empty() {
|
||||||
|
let preview_handles = self.tabs[i].scene.parametric_connected_handles(
|
||||||
|
self.tabs[i].current_parametric_scope(), &edited_handles, true,
|
||||||
|
);
|
||||||
if self.grip_dirty_before.is_none() {
|
if self.grip_dirty_before.is_none() {
|
||||||
self.grip_dirty_before = Some(self.tabs[i].dirty);
|
self.grip_dirty_before = Some(self.tabs[i].dirty);
|
||||||
}
|
}
|
||||||
@@ -1261,7 +1264,7 @@ impl OpenCADStudio {
|
|||||||
// before insertion so append + placement is one undo step.
|
// before insertion so append + placement is one undo step.
|
||||||
// Normal grip drags still snapshot their current entities here.
|
// Normal grip drags still snapshot their current entities here.
|
||||||
if self.grip_originals.is_empty() {
|
if self.grip_originals.is_empty() {
|
||||||
self.grip_originals = edited_handles
|
self.grip_originals = preview_handles
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|&handle| {
|
.filter_map(|&handle| {
|
||||||
self.tabs[i]
|
self.tabs[i]
|
||||||
@@ -1274,17 +1277,17 @@ impl OpenCADStudio {
|
|||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
self.capture_grip_history_originals(i, &edited_handles);
|
self.capture_grip_history_originals(i, &edited_handles);
|
||||||
for &handle in &edited_handles {
|
for &handle in &preview_handles {
|
||||||
if !self.tabs[i].scene.meshes.contains_key(&handle) {
|
if !self.tabs[i].scene.meshes.contains_key(&handle) {
|
||||||
self.tabs[i].scene.preview_hidden.insert(handle);
|
self.tabs[i].scene.preview_hidden.insert(handle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let changes: Vec<_> = edited_handles
|
let changes: Vec<_> = preview_handles
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&handle| (handle, crate::scene::ChangeKind::Modified))
|
.map(|&handle| (handle, crate::scene::ChangeKind::Modified))
|
||||||
.collect();
|
.collect();
|
||||||
self.tabs[i].scene.bump_entities(&changes);
|
self.tabs[i].scene.bump_entities_after_parametric_solve(&changes);
|
||||||
self.grip_preview_handles = edited_handles.clone();
|
self.grip_preview_handles = preview_handles;
|
||||||
// Snapshot the entity's glyph quads once so each move can
|
// Snapshot the entity's glyph quads once so each move can
|
||||||
// slide the already-shaped text rather than re-shaping it
|
// slide the already-shaped text rather than re-shaping it
|
||||||
// (issue #316). The fast slide path only runs for a rigid
|
// (issue #316). The fast slide path only runs for a rigid
|
||||||
@@ -1292,7 +1295,7 @@ impl OpenCADStudio {
|
|||||||
// dimension re-tessellates) and a Square insertion grip (so
|
// dimension re-tessellates) and a Square insertion grip (so
|
||||||
// an MTEXT width handle, a Triangle, still re-tessellates so
|
// an MTEXT width handle, a Triangle, still re-tessellates so
|
||||||
// the re-wrap is exact).
|
// the re-wrap is exact).
|
||||||
let snap = self.tabs[i].scene.wire_models_for(&edited_handles);
|
let snap = self.tabs[i].scene.wire_models_for(&self.grip_preview_handles);
|
||||||
|
|
||||||
// Preserve the exact geometry from the instant the grip drag began.
|
// Preserve the exact geometry from the instant the grip drag began.
|
||||||
// This snapshot is visual/reference-only: do NOT append it to
|
// This snapshot is visual/reference-only: do NOT append it to
|
||||||
@@ -1317,7 +1320,7 @@ impl OpenCADStudio {
|
|||||||
grip_def.shape == crate::scene::model::object::GripShape::Square
|
grip_def.shape == crate::scene::model::object::GripShape::Square
|
||||||
})
|
})
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
self.grip_text_slide = edited_handles.len() == 1
|
self.grip_text_slide = self.grip_preview_handles.len() == 1
|
||||||
&& grip.targets.len() == 1
|
&& grip.targets.len() == 1
|
||||||
&& !self.grip_text_verts.is_empty()
|
&& !self.grip_text_verts.is_empty()
|
||||||
&& snap.iter().all(|w| w.points.is_empty())
|
&& snap.iter().all(|w| w.points.is_empty())
|
||||||
@@ -1745,62 +1748,17 @@ impl OpenCADStudio {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let driven_refs: Vec<_> = grip
|
self.solve_grip_constraints(i, &grip);
|
||||||
.targets
|
let mesh_changes: Vec<_> = self.grip_preview_handles
|
||||||
.iter()
|
|
||||||
.filter_map(|target| {
|
|
||||||
let entity = self.tabs[i]
|
|
||||||
.scene
|
|
||||||
.document
|
|
||||||
.get_entity(target.handle)?;
|
|
||||||
crate::scene::parametric_constraints::grip_solve_anchor_ref(
|
|
||||||
entity,
|
|
||||||
target.handle,
|
|
||||||
target.grip_id,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
// Re-solve constrained neighbors on each drag frame and include
|
|
||||||
// their original state in the gesture's undo record.
|
|
||||||
let solved_by_constraints = self.tabs[i]
|
|
||||||
.scene
|
|
||||||
.solve_parametric_constraints_preview(
|
|
||||||
&edited_handles,
|
|
||||||
&driven_refs,
|
|
||||||
false,
|
|
||||||
&self.grip_originals,
|
|
||||||
);
|
|
||||||
for (handle, _) in &solved_by_constraints {
|
|
||||||
let handle = *handle;
|
|
||||||
if !self.grip_preview_handles.contains(&handle) {
|
|
||||||
if let Some(original) = self.tabs[i].scene.document.get_entity(handle).cloned()
|
|
||||||
{
|
|
||||||
self.grip_originals.push((handle, original));
|
|
||||||
}
|
|
||||||
self.grip_preview_handles.push(handle);
|
|
||||||
if !self.tabs[i].scene.meshes.contains_key(&handle) {
|
|
||||||
self.tabs[i].scene.preview_hidden.insert(handle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !edited_handles.contains(&handle) {
|
|
||||||
edited_handles.push(handle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (handle, new_entity) in solved_by_constraints {
|
|
||||||
if let Some(slot) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
|
||||||
*slot = new_entity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mesh_changes: Vec<_> = edited_handles
|
|
||||||
.iter()
|
.iter()
|
||||||
.copied()
|
.copied()
|
||||||
.filter(|handle| self.tabs[i].scene.meshes.contains_key(handle))
|
.filter(|handle| self.tabs[i].scene.meshes.contains_key(handle))
|
||||||
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||||
.collect();
|
.collect();
|
||||||
if !mesh_changes.is_empty() {
|
if !mesh_changes.is_empty() {
|
||||||
self.tabs[i].scene.bump_entities(&mesh_changes);
|
self.tabs[i].scene.bump_entities_after_parametric_solve(&mesh_changes);
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.set_preview_hatches(&edited_handles);
|
self.tabs[i].scene.set_preview_hatches(&self.grip_preview_handles);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
if let Some(active) = self.tabs[i].active_grip.as_mut() {
|
if let Some(active) = self.tabs[i].active_grip.as_mut() {
|
||||||
active.last_world = snapped;
|
active.last_world = snapped;
|
||||||
@@ -1826,7 +1784,7 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].scene.set_preview_wires(Vec::new());
|
self.tabs[i].scene.set_preview_wires(Vec::new());
|
||||||
} else {
|
} else {
|
||||||
// Current deformed geometry.
|
// Current deformed geometry.
|
||||||
let mut preview = self.tabs[i].scene.grip_wire_models_for(&edited_handles);
|
let mut preview = self.tabs[i].scene.grip_wire_models_for(&self.grip_preview_handles);
|
||||||
|
|
||||||
// Also show the drag-start geometry as a faint ghost.
|
// Also show the drag-start geometry as a faint ghost.
|
||||||
//
|
//
|
||||||
@@ -3480,7 +3438,7 @@ impl OpenCADStudio {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||||
.collect();
|
.collect();
|
||||||
self.tabs[i].scene.bump_entities(&changes);
|
self.tabs[i].scene.bump_entities_after_parametric_solve(&changes);
|
||||||
}
|
}
|
||||||
// Placement confirmed — keep the just-added leader.
|
// Placement confirmed — keep the just-added leader.
|
||||||
self.grip_add_provisional = None;
|
self.grip_add_provisional = None;
|
||||||
@@ -6075,6 +6033,128 @@ mod selection_preview_tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::app::{HoverDwell, OpenCADStudio, HOVER_DWELL_MS};
|
use crate::app::{HoverDwell, OpenCADStudio, HOVER_DWELL_MS};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lower_endpoint_drag_previews_connected_tangent_profile_and_keeps_top_fixed() {
|
||||||
|
use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef, ParametricScope};
|
||||||
|
use acadrust::{entities::{Arc, Line}, types::Vector3, EntityType};
|
||||||
|
|
||||||
|
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 scene = &mut app.tabs[i].scene;
|
||||||
|
let [outer, inner, upper, lower] = [
|
||||||
|
([0.0, 0.0], [0.0, 5.0]),
|
||||||
|
([-1.0, -1.0], [-1.0, 5.0]),
|
||||||
|
([4.0, 0.0], [0.0, 0.0]),
|
||||||
|
([4.0, -1.0], [-1.0, -1.0]),
|
||||||
|
].map(|(a, b)| scene.add_entity(EntityType::Line(Line::from_points(
|
||||||
|
Vector3::new(a[0], a[1], 0.0), Vector3::new(b[0], b[1], 0.0),
|
||||||
|
))));
|
||||||
|
let top = scene.add_entity(EntityType::Arc(Arc::from_coords(
|
||||||
|
-0.5, 5.0, 0.0, 0.5, 0.0, std::f64::consts::PI,
|
||||||
|
)));
|
||||||
|
let right = scene.add_entity(EntityType::Arc(Arc::from_coords(
|
||||||
|
4.0, -0.5, 0.0, 0.5, -std::f64::consts::FRAC_PI_2,
|
||||||
|
std::f64::consts::FRAC_PI_2,
|
||||||
|
)));
|
||||||
|
let set = scene.parametric_constraint_set_mut(ParametricScope::ModelSpace);
|
||||||
|
for (a, ai, b, bi) in [
|
||||||
|
(outer, 1, top, 0), (top, 1, inner, 1),
|
||||||
|
(outer, 0, upper, 1), (upper, 0, right, 1),
|
||||||
|
(inner, 0, lower, 1), (right, 0, lower, 0),
|
||||||
|
] {
|
||||||
|
set.add(ConstraintKind::Coincident,
|
||||||
|
vec![ParametricRef::point(a, ai), ParametricRef::point(b, bi)], None);
|
||||||
|
}
|
||||||
|
for (kind, a, b) in [
|
||||||
|
(ConstraintKind::Parallel, outer, inner),
|
||||||
|
(ConstraintKind::Parallel, upper, lower),
|
||||||
|
(ConstraintKind::Tangent, outer, top),
|
||||||
|
(ConstraintKind::Tangent, inner, top),
|
||||||
|
(ConstraintKind::Tangent, upper, right),
|
||||||
|
(ConstraintKind::Tangent, lower, right),
|
||||||
|
(ConstraintKind::Equal, top, right),
|
||||||
|
] {
|
||||||
|
set.add(kind, vec![ParametricRef::whole(a), ParametricRef::whole(b)], None);
|
||||||
|
}
|
||||||
|
set.add(ConstraintKind::Horizontal, vec![ParametricRef::whole(lower)], None);
|
||||||
|
let handles = 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);
|
||||||
|
[0x57F, 0x57D, 0x57C, 0x57A, 0x57E, 0x57B].map(Handle::new)
|
||||||
|
} else {
|
||||||
|
[outer, inner, upper, lower, top, right]
|
||||||
|
};
|
||||||
|
let inner = handles[1];
|
||||||
|
let geometry = |app: &OpenCADStudio| handles.map(|handle|
|
||||||
|
app.tabs[i].scene.document.get_entity(handle).unwrap().clone());
|
||||||
|
let before = geometry(&app);
|
||||||
|
let EntityType::Line(original_line) = &before[1] else { panic!("line") };
|
||||||
|
let lower_endpoint = original_line.start;
|
||||||
|
let upper_endpoint = original_line.end;
|
||||||
|
let EntityType::Arc(original_arc) = &before[4] else { panic!("arc") };
|
||||||
|
let radius = original_arc.radius;
|
||||||
|
app.tabs[i].scene.selected.insert(inner);
|
||||||
|
app.refresh_selected_grips();
|
||||||
|
app.tabs[i].active_grip = Some(GripEdit::single(
|
||||||
|
inner, 0, false, glam::DVec3::new(lower_endpoint.x, lower_endpoint.y, 0.0),
|
||||||
|
));
|
||||||
|
let check_profile = |app: &OpenCADStudio| {
|
||||||
|
let entities = geometry(app);
|
||||||
|
let EntityType::Line(line) = &entities[1] else { panic!("line") };
|
||||||
|
assert_eq!(line.end, upper_endpoint, "top endpoint drifted");
|
||||||
|
let EntityType::Arc(arc) = &entities[4] else { panic!("arc") };
|
||||||
|
assert!((arc.end_point() - line.end).length() < 1e-7, "top arc detached");
|
||||||
|
for index in [4, 5] {
|
||||||
|
let EntityType::Arc(arc) = &entities[index] else { panic!("arc") };
|
||||||
|
assert!((arc.radius - radius).abs() < 1e-7, "parallel gap changed");
|
||||||
|
}
|
||||||
|
for (line_index, arc_index, marker) in [(0, 4, 0), (1, 4, 1), (2, 5, 1), (3, 5, 0)] {
|
||||||
|
let EntityType::Line(line) = &entities[line_index] else { panic!("line") };
|
||||||
|
let EntityType::Arc(arc) = &entities[arc_index] else { panic!("arc") };
|
||||||
|
let contact = if marker == 0 { arc.start_point() } else { arc.end_point() };
|
||||||
|
let endpoint = if line_index < 2 { line.end } else { line.start };
|
||||||
|
assert!((contact - endpoint).length() < 1e-7, "tangent contact detached");
|
||||||
|
let direction = line.end - line.start;
|
||||||
|
assert!(direction.dot(&(contact - arc.center)).abs() < 1e-7,
|
||||||
|
"tangency lost: line={line:?}, arc={arc:?}, error={}", direction.dot(&(contact - arc.center)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for offset in [[-0.4, -0.5], [-1.0, -1.0], [-1.0, -1.0], [0.3, 0.4]] {
|
||||||
|
let target = [lower_endpoint.x + offset[0], lower_endpoint.y + offset[1]];
|
||||||
|
let cursor = app.tabs[i].scene.camera.borrow().project(
|
||||||
|
glam::DVec3::new(target[0], target[1], 0.0),
|
||||||
|
iced::Rectangle::with_size(iced::Size::new(800.0, 600.0)),
|
||||||
|
).unwrap();
|
||||||
|
let _ = app.on_viewport_move(Point::new(cursor.x, cursor.y));
|
||||||
|
check_profile(&app);
|
||||||
|
let EntityType::Line(line) = app.tabs[i].scene.document.get_entity(inner).unwrap()
|
||||||
|
else { panic!("line") };
|
||||||
|
assert!((line.start - Vector3::new(target[0], target[1], 0.0)).length() < 1e-5);
|
||||||
|
for handle in handles {
|
||||||
|
assert!(app.tabs[i].scene.preview_wires.iter().any(|wire|
|
||||||
|
wire.name == handle.value().to_string() && wire.color[3] > 0.9),
|
||||||
|
"connected entity {handle:?} missing from preview");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let preview = geometry(&app);
|
||||||
|
let _ = app.on_viewport_left_release();
|
||||||
|
check_profile(&app);
|
||||||
|
assert_eq!(geometry(&app), preview, "release changed the preview solution");
|
||||||
|
app.undo_steps(1);
|
||||||
|
assert_eq!(geometry(&app), before);
|
||||||
|
app.redo_steps(1);
|
||||||
|
assert_eq!(geometry(&app), preview);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn grip_moves_keep_perpendicular_constraints_live_and_undoable() {
|
fn grip_moves_keep_perpendicular_constraints_live_and_undoable() {
|
||||||
use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef};
|
use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef};
|
||||||
@@ -6137,9 +6217,9 @@ mod selection_preview_tests {
|
|||||||
.normalize()
|
.normalize()
|
||||||
};
|
};
|
||||||
assert!(direction(current[0]).dot(direction(current[1])).abs() < 1e-8);
|
assert!(direction(current[0]).dot(direction(current[1])).abs() < 1e-8);
|
||||||
assert_eq!(
|
assert_ne!(
|
||||||
current[1], before[1],
|
current[1], before[1],
|
||||||
"geometry outside the grip edit must stay fixed"
|
"the constrained neighbor must follow the edited line"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
current[0][0], before[0][0],
|
current[0][0], before[0][0],
|
||||||
|
|||||||
+17
-1
@@ -2879,6 +2879,22 @@ impl Scene {
|
|||||||
driven_refs: &[parametric_constraints::ParametricRef],
|
driven_refs: &[parametric_constraints::ParametricRef],
|
||||||
retain_size: bool,
|
retain_size: bool,
|
||||||
retained_originals: &[(Handle, EntityType)],
|
retained_originals: &[(Handle, EntityType)],
|
||||||
|
) {
|
||||||
|
self.bump_entities_with_solve_policy(changes, driven_refs, retain_size, retained_originals, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refresh display and associations after a solved grip or exact history restore.
|
||||||
|
pub(crate) fn bump_entities_after_parametric_solve(&mut self, changes: &[(Handle, ChangeKind)]) {
|
||||||
|
self.bump_entities_with_solve_policy(changes, &[], false, &[], false);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bump_entities_with_solve_policy(
|
||||||
|
&mut self,
|
||||||
|
changes: &[(Handle, ChangeKind)],
|
||||||
|
driven_refs: &[parametric_constraints::ParametricRef],
|
||||||
|
retain_size: bool,
|
||||||
|
retained_originals: &[(Handle, EntityType)],
|
||||||
|
solve_parametric: bool,
|
||||||
) {
|
) {
|
||||||
if changes.iter().any(|(handle, kind)| {
|
if changes.iter().any(|(handle, kind)| {
|
||||||
matches!(kind, ChangeKind::Removed)
|
matches!(kind, ChangeKind::Removed)
|
||||||
@@ -2912,7 +2928,7 @@ impl Scene {
|
|||||||
changes.push(change);
|
changes.push(change);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !self.parametric_constraints.is_empty() {
|
if solve_parametric && !self.parametric_constraints.is_empty() {
|
||||||
for change in self.refresh_parametric_constraints_with_originals(
|
for change in self.refresh_parametric_constraints_with_originals(
|
||||||
&changes,
|
&changes,
|
||||||
driven_refs,
|
driven_refs,
|
||||||
|
|||||||
@@ -83,32 +83,33 @@ impl ParametricRef {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Point reference temporarily pinned while solving a native grip edit.
|
/// Point references temporarily pinned while solving a native grip edit.
|
||||||
/// A line endpoint anchors its opposite endpoint so the grabbed endpoint is
|
/// Both endpoints of an edited line are exact inputs: the opposite endpoint
|
||||||
/// projected onto the constraint solution instead of moving the whole line.
|
/// stays fixed while the grabbed endpoint follows the cursor.
|
||||||
pub(crate) fn grip_solve_anchor_ref(
|
pub(crate) fn grip_solve_anchor_refs(
|
||||||
entity: &acadrust::EntityType,
|
entity: &acadrust::EntityType,
|
||||||
handle: Handle,
|
handle: Handle,
|
||||||
grip_id: usize,
|
grip_id: usize,
|
||||||
) -> Option<ParametricRef> {
|
) -> Vec<ParametricRef> {
|
||||||
match entity {
|
match entity {
|
||||||
acadrust::EntityType::Line(_) if grip_id <= 1 => {
|
acadrust::EntityType::Line(_) if grip_id <= 1 => vec![
|
||||||
Some(ParametricRef::point(handle, 1 - grip_id as i32))
|
ParametricRef::point(handle, 0),
|
||||||
}
|
ParametricRef::point(handle, 1),
|
||||||
|
],
|
||||||
acadrust::EntityType::LwPolyline(polyline) if grip_id < polyline.vertices.len() => {
|
acadrust::EntityType::LwPolyline(polyline) if grip_id < polyline.vertices.len() => {
|
||||||
Some(ParametricRef::point(handle, grip_id as i32))
|
vec![ParametricRef::point(handle, grip_id as i32)]
|
||||||
}
|
}
|
||||||
acadrust::EntityType::Polyline2D(polyline) if grip_id < polyline.vertices.len() => {
|
acadrust::EntityType::Polyline2D(polyline) if grip_id < polyline.vertices.len() => {
|
||||||
Some(ParametricRef::point(handle, grip_id as i32))
|
vec![ParametricRef::point(handle, grip_id as i32)]
|
||||||
}
|
}
|
||||||
acadrust::EntityType::Arc(_) => match grip_id {
|
acadrust::EntityType::Arc(_) => match grip_id {
|
||||||
0 => Some(ParametricRef::center(handle)),
|
0 => vec![ParametricRef::center(handle)],
|
||||||
1 => Some(ParametricRef::point(handle, 0)),
|
1 => vec![ParametricRef::point(handle, 0)],
|
||||||
2 => Some(ParametricRef::point(handle, 1)),
|
2 => vec![ParametricRef::point(handle, 1)],
|
||||||
_ => None,
|
_ => Vec::new(),
|
||||||
},
|
},
|
||||||
acadrust::EntityType::Circle(_) if grip_id == 0 => {
|
acadrust::EntityType::Circle(_) if grip_id == 0 => {
|
||||||
Some(ParametricRef::center(handle))
|
vec![ParametricRef::center(handle)]
|
||||||
}
|
}
|
||||||
acadrust::EntityType::Point(_)
|
acadrust::EntityType::Point(_)
|
||||||
| acadrust::EntityType::Insert(_)
|
| acadrust::EntityType::Insert(_)
|
||||||
@@ -116,9 +117,9 @@ pub(crate) fn grip_solve_anchor_ref(
|
|||||||
| acadrust::EntityType::MText(_)
|
| acadrust::EntityType::MText(_)
|
||||||
if grip_id == 0 =>
|
if grip_id == 0 =>
|
||||||
{
|
{
|
||||||
Some(ParametricRef::point(handle, 0))
|
vec![ParametricRef::point(handle, 0)]
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -903,13 +904,13 @@ pub(crate) fn constraint_hover_points(
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl super::Scene {
|
impl super::Scene {
|
||||||
/// Expand a set of entities to the full enabled constraint component in
|
/// Expand through enabled constraints. Whole-object translations follow
|
||||||
/// one scope. Whole-object translations use this to keep every connected
|
/// point connections; grip previews include curve relations as well.
|
||||||
/// entity's own shape while moving the assembly.
|
|
||||||
pub(crate) fn parametric_connected_handles(
|
pub(crate) fn parametric_connected_handles(
|
||||||
&self,
|
&self,
|
||||||
scope: ParametricScope,
|
scope: ParametricScope,
|
||||||
seeds: &[Handle],
|
seeds: &[Handle],
|
||||||
|
include_curve_relations: bool,
|
||||||
) -> Vec<Handle> {
|
) -> Vec<Handle> {
|
||||||
let mut ordered = seeds.to_vec();
|
let mut ordered = seeds.to_vec();
|
||||||
let mut found: std::collections::HashSet<_> = seeds.iter().copied().collect();
|
let mut found: std::collections::HashSet<_> = seeds.iter().copied().collect();
|
||||||
@@ -920,10 +921,10 @@ impl super::Scene {
|
|||||||
let mut added = false;
|
let mut added = false;
|
||||||
for constraint in set.constraints.iter().filter(|constraint| {
|
for constraint in set.constraints.iter().filter(|constraint| {
|
||||||
constraint.enabled
|
constraint.enabled
|
||||||
&& matches!(
|
&& (include_curve_relations || matches!(
|
||||||
constraint.kind,
|
constraint.kind,
|
||||||
ConstraintKind::Coincident | ConstraintKind::PointOnCurve
|
ConstraintKind::Coincident | ConstraintKind::PointOnCurve
|
||||||
)
|
))
|
||||||
}) {
|
}) {
|
||||||
if !constraint
|
if !constraint
|
||||||
.refs
|
.refs
|
||||||
@@ -1414,7 +1415,8 @@ impl super::Scene {
|
|||||||
let selected = c
|
let selected = c
|
||||||
.refs
|
.refs
|
||||||
.iter()
|
.iter()
|
||||||
.any(|reference| self.selected.contains(&reference.entity));
|
.any(|reference| self.selected.contains(&reference.entity)
|
||||||
|
|| self.preview_hidden.contains(&reference.entity));
|
||||||
self.should_display_parametric_constraint(scope, c.id, selected, display_mode)
|
self.should_display_parametric_constraint(scope, c.id, selected, display_mode)
|
||||||
})
|
})
|
||||||
.flat_map(|c| {
|
.flat_map(|c| {
|
||||||
@@ -1888,18 +1890,18 @@ mod tests {
|
|||||||
));
|
));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
grip_solve_anchor_ref(&arc, handle, 0),
|
grip_solve_anchor_refs(&arc, handle, 0),
|
||||||
Some(ParametricRef::center(handle))
|
vec![ParametricRef::center(handle)]
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
grip_solve_anchor_ref(&arc, handle, 1),
|
grip_solve_anchor_refs(&arc, handle, 1),
|
||||||
Some(ParametricRef::point(handle, 0))
|
vec![ParametricRef::point(handle, 0)]
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
grip_solve_anchor_ref(&arc, handle, 2),
|
grip_solve_anchor_refs(&arc, handle, 2),
|
||||||
Some(ParametricRef::point(handle, 1))
|
vec![ParametricRef::point(handle, 1)]
|
||||||
);
|
);
|
||||||
assert_eq!(grip_solve_anchor_ref(&arc, handle, 3), None);
|
assert_eq!(grip_solve_anchor_refs(&arc, handle, 3), Vec::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2146,7 +2148,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
scene.parametric_connected_handles(ParametricScope::ModelSpace, &[h(1)]),
|
scene.parametric_connected_handles(ParametricScope::ModelSpace, &[h(1)], false),
|
||||||
vec![h(1), h(2), h(3)]
|
vec![h(1), h(2), h(3)]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+133
-65
@@ -800,27 +800,54 @@ fn oriented_distance(target: f64, current_projection: f64) -> f64 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One [`ParametricConstraint`]'s `cadkernel_constraints` construction: every system-level
|
fn retained_tangent_side(
|
||||||
/// constraint it contributes (most kinds contribute exactly one; a few —
|
document: &acadrust::CadDocument,
|
||||||
/// `Coincident`/`Concentric`/`CenterPoint`'s X+Y halves, `Colinear`'s two
|
retained_before: &HashMap<Handle, std::sync::Arc<EntityType>>,
|
||||||
/// endpoints, `Midpoint`'s X+Y, `Symmetric`'s midpoint+perpendicular pair,
|
constraint: &ParametricConstraint,
|
||||||
/// `Fixed`'s per-coordinate pins — contribute more). Empty means it can't be
|
) -> Option<bool> {
|
||||||
/// built (an unsupported/not-yet-mapped kind, a dangling ref, a ref shape
|
let [a, b] = constraint.refs.as_slice() else {
|
||||||
/// the kind doesn't expect — e.g. `Parallel` needs two whole-line refs — or,
|
return None;
|
||||||
/// for a dimensional kind, a `driving_param` that fails to resolve). A constraint
|
};
|
||||||
/// that can't be built is simply skipped for this solve, not an error:
|
if constraint.kind != ConstraintKind::Tangent
|
||||||
/// geometry it would have constrained is left alone, matching how a
|
|| !retained_before.contains_key(&a.entity) && !retained_before.contains_key(&b.entity)
|
||||||
/// dangling associative-dimension reference degrades today rather than
|
{
|
||||||
/// aborting the whole recompute. (A named-parameter *cycle* should be
|
return None;
|
||||||
/// unreachable here — `ParameterTable::set` already refuses to create one —
|
}
|
||||||
/// so it gets the same treatment as any other resolve failure rather than a
|
let curve = |reference: ParametricRef| {
|
||||||
/// special case.)
|
let entity = retained_before
|
||||||
|
.get(&reference.entity)
|
||||||
|
.map(|entity| entity.as_ref())
|
||||||
|
.or_else(|| document.get_entity(reference.entity))?;
|
||||||
|
let curve = crate::entities::curve::entity_curve_xy(entity)?;
|
||||||
|
reference
|
||||||
|
.segment_index()
|
||||||
|
.map(|index| curve.segments().into_iter().nth(index))
|
||||||
|
.unwrap_or(Some(curve))
|
||||||
|
};
|
||||||
|
let circle_center = |curve: &cadkernel::geom2d::Curve| match curve {
|
||||||
|
cadkernel::geom2d::Curve::Circle(circle) => Some(circle.centre),
|
||||||
|
cadkernel::geom2d::Curve::Arc(arc) => Some(arc.centre),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let (a, b) = (curve(*a)?, curve(*b)?);
|
||||||
|
let (line, center) = a
|
||||||
|
.as_ray()
|
||||||
|
.zip(circle_center(&b))
|
||||||
|
.or_else(|| b.as_ray().zip(circle_center(&a)))?;
|
||||||
|
let ([x1, y1], [dx, dy]) = line;
|
||||||
|
Some(dx * (center[1] - y1) - dy * (center[0] - x1) >= 0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the kernel equations for one constraint. Unsupported references or
|
||||||
|
/// invalid dimensional targets produce no equations.
|
||||||
fn build_constraint(
|
fn build_constraint(
|
||||||
document: &acadrust::CadDocument,
|
document: &acadrust::CadDocument,
|
||||||
sys: &mut System,
|
sys: &mut System,
|
||||||
cache: &mut HashMap<Handle, EntityGeom>,
|
cache: &mut HashMap<Handle, EntityGeom>,
|
||||||
params: &ParameterTable,
|
params: &ParameterTable,
|
||||||
c: &ParametricConstraint,
|
c: &ParametricConstraint,
|
||||||
|
tangent_side: Option<bool>,
|
||||||
|
tangent_point: Option<ParametricRef>,
|
||||||
) -> Vec<Rc<dyn Constraint>> {
|
) -> Vec<Rc<dyn Constraint>> {
|
||||||
if !c.enabled || !refs_share_supported_plane(document, &c.refs) {
|
if !c.enabled || !refs_share_supported_plane(document, &c.refs) {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
@@ -1517,6 +1544,14 @@ fn build_constraint(
|
|||||||
}
|
}
|
||||||
(CircleOrLine::Circle(circ), CircleOrLine::Line(line))
|
(CircleOrLine::Circle(circ), CircleOrLine::Line(line))
|
||||||
| (CircleOrLine::Line(line), CircleOrLine::Circle(circ)) => {
|
| (CircleOrLine::Line(line), CircleOrLine::Circle(circ)) => {
|
||||||
|
if let Some(point) = tangent_point.and_then(|reference|
|
||||||
|
resolve_constraint_point(document, sys, cache, reference))
|
||||||
|
{
|
||||||
|
// At a coincident endpoint, constrain the tangent direction
|
||||||
|
// directly; a circle-to-line distance has a singular derivative.
|
||||||
|
let radius = GLine { p1: circ.center, p2: point };
|
||||||
|
return vec![Rc::new(PerpendicularConstraint::new(sys.store(), line, radius))];
|
||||||
|
}
|
||||||
let store = sys.store();
|
let store = sys.store();
|
||||||
let (cx, cy) = (store.get(circ.center.x), store.get(circ.center.y));
|
let (cx, cy) = (store.get(circ.center.x), store.get(circ.center.y));
|
||||||
let (x1, y1) = (store.get(line.p1.x), store.get(line.p1.y));
|
let (x1, y1) = (store.get(line.p1.x), store.get(line.p1.y));
|
||||||
@@ -1525,7 +1560,7 @@ fn build_constraint(
|
|||||||
// `C2LDistance::signed_value` itself uses — so `ccw`'s
|
// `C2LDistance::signed_value` itself uses — so `ccw`'s
|
||||||
// sign matches whichever side the circle already sits on.
|
// sign matches whichever side the circle already sits on.
|
||||||
let area = (x2 - x1) * (cy - y1) - (y2 - y1) * (cx - x1);
|
let area = (x2 - x1) * (cy - y1) - (y2 - y1) * (cx - x1);
|
||||||
let ccw = area >= 0.0;
|
let ccw = tangent_side.unwrap_or(area >= 0.0);
|
||||||
// A driven, fixed zero: with `internal: false` this makes
|
// A driven, fixed zero: with `internal: false` this makes
|
||||||
// `C2LDistance`'s target exactly the circle's own radius
|
// `C2LDistance`'s target exactly the circle's own radius
|
||||||
// (see its `error_grad`), i.e. plain tangency rather than
|
// (see its `error_grad`), i.e. plain tangency rather than
|
||||||
@@ -1748,6 +1783,7 @@ fn solve_scope(
|
|||||||
set: &ParametricConstraintSet,
|
set: &ParametricConstraintSet,
|
||||||
driven_refs: &[ParametricRef],
|
driven_refs: &[ParametricRef],
|
||||||
retain_size: bool,
|
retain_size: bool,
|
||||||
|
retain_lengths: bool,
|
||||||
retained_before: &HashMap<Handle, std::sync::Arc<EntityType>>,
|
retained_before: &HashMap<Handle, std::sync::Arc<EntityType>>,
|
||||||
) -> Option<SolveResult> {
|
) -> Option<SolveResult> {
|
||||||
let params = if set.local_parameters.is_empty() {
|
let params = if set.local_parameters.is_empty() {
|
||||||
@@ -1768,7 +1804,26 @@ fn solve_scope(
|
|||||||
let mut owner: Vec<(Rc<dyn Constraint>, ConstraintId)> = Vec::new();
|
let mut owner: Vec<(Rc<dyn Constraint>, ConstraintId)> = Vec::new();
|
||||||
|
|
||||||
for c in &set.constraints {
|
for c in &set.constraints {
|
||||||
for constraint in build_constraint(document, &mut sys, &mut cache, params, c) {
|
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
|
||||||
|
&& c.refs.iter().all(|r| r.marker.is_none()))
|
||||||
|
.then(|| set.constraints.iter().find_map(|connection| {
|
||||||
|
(connection.enabled && connection.kind == ConstraintKind::Coincident
|
||||||
|
&& connection.refs.len() == 2
|
||||||
|
&& connection.refs.iter().all(|r| matches!(r.marker, Some(0 | 1)))
|
||||||
|
&& c.refs.iter().all(|r| connection.refs.iter().any(|p| p.entity == r.entity)))
|
||||||
|
.then(|| connection.refs[0])
|
||||||
|
})).flatten();
|
||||||
|
for constraint in build_constraint(
|
||||||
|
document,
|
||||||
|
&mut sys,
|
||||||
|
&mut cache,
|
||||||
|
params,
|
||||||
|
c,
|
||||||
|
tangent_side,
|
||||||
|
tangent_point,
|
||||||
|
) {
|
||||||
owner.push((constraint.clone(), c.id));
|
owner.push((constraint.clone(), c.id));
|
||||||
sys.add_constraint(constraint);
|
sys.add_constraint(constraint);
|
||||||
}
|
}
|
||||||
@@ -1782,13 +1837,12 @@ fn solve_scope(
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preserve current line lengths and radii for this solve when
|
// Grip edits retain radii while connected lines resize to meet their
|
||||||
// CONSTRAINTSOLVEMODE requests it. These temporary equations guide the
|
// endpoints. Other edits can retain line lengths as well.
|
||||||
// solution without becoming persistent dimensional constraints.
|
|
||||||
if retain_size {
|
if retain_size {
|
||||||
for (handle, geom) in &cache {
|
for (handle, geom) in &cache {
|
||||||
match geom {
|
match geom {
|
||||||
EntityGeom::Line(line) => {
|
EntityGeom::Line(line) if retain_lengths => {
|
||||||
let current_length = {
|
let current_length = {
|
||||||
let store = sys.store();
|
let store = sys.store();
|
||||||
let dx = store.get(line.p2.x) - store.get(line.p1.x);
|
let dx = store.get(line.p2.x) - store.get(line.p1.x);
|
||||||
@@ -1806,7 +1860,7 @@ fn solve_scope(
|
|||||||
points,
|
points,
|
||||||
closed,
|
closed,
|
||||||
..
|
..
|
||||||
} => {
|
} if retain_lengths => {
|
||||||
for index in 0..points.len() {
|
for index in 0..points.len() {
|
||||||
let Some(a) = points.get(index).copied() else {
|
let Some(a) = points.get(index).copied() else {
|
||||||
continue;
|
continue;
|
||||||
@@ -2476,6 +2530,8 @@ impl Scene {
|
|||||||
&mut cache,
|
&mut cache,
|
||||||
&self.named_parameters,
|
&self.named_parameters,
|
||||||
&candidate,
|
&candidate,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.is_empty()
|
.is_empty()
|
||||||
{
|
{
|
||||||
@@ -2592,6 +2648,7 @@ impl Scene {
|
|||||||
&self.parametric_constraints[i],
|
&self.parametric_constraints[i],
|
||||||
driven_refs,
|
driven_refs,
|
||||||
retain_size,
|
retain_size,
|
||||||
|
true,
|
||||||
&retained_before,
|
&retained_before,
|
||||||
) else {
|
) else {
|
||||||
continue;
|
continue;
|
||||||
@@ -2636,25 +2693,17 @@ impl Scene {
|
|||||||
if !is_touched {
|
if !is_touched {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let mut anchors = driven_refs.to_vec();
|
let connected = self.parametric_connected_handles(set.scope, touched, true);
|
||||||
for reference in set
|
let mut component = set.clone();
|
||||||
.constraints
|
component.constraints.retain(|constraint| constraint.refs.iter()
|
||||||
.iter()
|
.any(|reference| connected.contains(&reference.entity)));
|
||||||
.filter(|constraint| constraint.enabled)
|
|
||||||
.flat_map(|constraint| &constraint.refs)
|
|
||||||
{
|
|
||||||
if !touched.contains(&reference.entity)
|
|
||||||
&& !anchors.iter().any(|anchor| anchor.entity == reference.entity)
|
|
||||||
{
|
|
||||||
anchors.push(ParametricRef::whole(reference.entity));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let Some((solved, _dof, _conflicts)) = solve_scope(
|
let Some((solved, _dof, _conflicts)) = solve_scope(
|
||||||
&self.document,
|
&self.document,
|
||||||
&self.named_parameters,
|
&self.named_parameters,
|
||||||
set,
|
&component,
|
||||||
&anchors,
|
driven_refs,
|
||||||
retain_size,
|
retain_size,
|
||||||
|
false,
|
||||||
&retained_before,
|
&retained_before,
|
||||||
)
|
)
|
||||||
else {
|
else {
|
||||||
@@ -2925,68 +2974,87 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A grip preview treats geometry outside the edit as a fixed reference,
|
/// A grip preview lets the paired line and tangent circle follow the
|
||||||
/// so a parallel constraint projects the dragged endpoint without moving
|
/// edited line while preserving their sizes and parallel separation.
|
||||||
/// the other line or changing their separation.
|
|
||||||
#[test]
|
#[test]
|
||||||
fn preview_solve_keeps_untouched_parallel_geometry_fixed() {
|
fn preview_solve_preserves_parallel_spacing_through_tangent_radius() {
|
||||||
let mut scene = Scene::new();
|
let mut scene = Scene::new();
|
||||||
let a = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points(
|
let a = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points(
|
||||||
Vector3::new(0.0, 0.0, 0.0),
|
Vector3::new(0.0, 0.0, 0.0),
|
||||||
Vector3::new(10.0, 0.0, 0.0),
|
Vector3::new(0.0, 10.0, 0.0),
|
||||||
)));
|
)));
|
||||||
let b = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points(
|
let b = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points(
|
||||||
Vector3::new(0.0, 5.0, 0.0),
|
Vector3::new(2.0, 0.0, 0.0),
|
||||||
Vector3::new(10.0, 5.0, 0.0),
|
Vector3::new(2.0, 10.0, 0.0),
|
||||||
)));
|
)));
|
||||||
|
let circle = scene.add_entity(EntityType::Circle(
|
||||||
|
acadrust::entities::Circle::from_center_radius(Vector3::new(1.0, 5.0, 0.0), 1.0),
|
||||||
|
));
|
||||||
let mut set = ParametricConstraintSet::new(ParametricScope::ModelSpace);
|
let mut set = ParametricConstraintSet::new(ParametricScope::ModelSpace);
|
||||||
set.add(
|
set.add(
|
||||||
ConstraintKind::Parallel,
|
ConstraintKind::Parallel,
|
||||||
vec![ParametricRef::whole(a), ParametricRef::whole(b)],
|
vec![ParametricRef::whole(a), ParametricRef::whole(b)],
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
set.add(
|
||||||
|
ConstraintKind::Tangent,
|
||||||
|
vec![ParametricRef::whole(a), ParametricRef::whole(circle)],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
set.add(
|
||||||
|
ConstraintKind::Tangent,
|
||||||
|
vec![ParametricRef::whole(b), ParametricRef::whole(circle)],
|
||||||
|
None,
|
||||||
|
);
|
||||||
scene.parametric_constraints.push(set);
|
scene.parametric_constraints.push(set);
|
||||||
|
|
||||||
// Simulate a live grip drag: mutate `a` directly (as `apply_grip`
|
let a_before = scene.document.get_entity(a).unwrap().clone();
|
||||||
// would each frame), without going through `bump_entities` at all.
|
|
||||||
if let Some(EntityType::Line(l)) = scene.document.get_entity_mut(a) {
|
if let Some(EntityType::Line(l)) = scene.document.get_entity_mut(a) {
|
||||||
l.end = Vector3::new(10.0, 6.0, 0.0);
|
l.end = Vector3::new(2.0, 10.0, 0.0);
|
||||||
}
|
}
|
||||||
let b_before = scene.document.get_entity(b).cloned();
|
let b_before = scene.document.get_entity(b).cloned();
|
||||||
|
|
||||||
let solved = scene.solve_parametric_constraints_preview(
|
let solved = scene.solve_parametric_constraints_preview(
|
||||||
&[a],
|
&[a],
|
||||||
&[ParametricRef::point(a, 0)],
|
&[ParametricRef::point(a, 0), ParametricRef::point(a, 1)],
|
||||||
false,
|
true,
|
||||||
&[],
|
&[(a, a_before)],
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
scene.document.get_entity(b),
|
scene.document.get_entity(b),
|
||||||
b_before.as_ref(),
|
b_before.as_ref(),
|
||||||
"preview must not mutate the document"
|
"preview must not mutate the document"
|
||||||
);
|
);
|
||||||
let moved_a = solved
|
let solved_entity = |handle| {
|
||||||
.iter()
|
solved
|
||||||
.find(|(h, _)| *h == a)
|
.iter()
|
||||||
.map(|(_, entity)| entity)
|
.find(|(candidate, _)| *candidate == handle)
|
||||||
.expect("the dragged line should be projected");
|
.map(|(_, entity)| entity)
|
||||||
assert!(solved.iter().all(|(handle, _)| *handle != b));
|
.unwrap_or_else(|| scene.document.get_entity(handle).unwrap())
|
||||||
let EntityType::Line(moved_a_line) = moved_a else {
|
};
|
||||||
|
let EntityType::Line(moved_a) = solved_entity(a) else {
|
||||||
panic!("expected a Line")
|
panic!("expected a Line")
|
||||||
};
|
};
|
||||||
assert_eq!(moved_a_line.start, Vector3::new(0.0, 0.0, 0.0));
|
let EntityType::Line(moved_b) = solved_entity(b) else {
|
||||||
let EntityType::Line(fixed_b) = b_before.unwrap() else {
|
|
||||||
panic!("expected a Line")
|
panic!("expected a Line")
|
||||||
};
|
};
|
||||||
let dir_a = moved_a_line.end - moved_a_line.start;
|
let EntityType::Circle(moved_circle) = solved_entity(circle) else {
|
||||||
let dir_b = fixed_b.end - fixed_b.start;
|
panic!("expected a Circle")
|
||||||
|
};
|
||||||
|
assert_eq!(moved_a.start, Vector3::new(0.0, 0.0, 0.0));
|
||||||
|
assert_eq!(moved_a.end, Vector3::new(2.0, 10.0, 0.0));
|
||||||
|
assert_ne!(solved_entity(b), b_before.as_ref().unwrap());
|
||||||
|
let dir_a = moved_a.end - moved_a.start;
|
||||||
|
let dir_b = moved_b.end - moved_b.start;
|
||||||
let cross = dir_a.x * dir_b.y - dir_a.y * dir_b.x;
|
let cross = dir_a.x * dir_b.y - dir_a.y * dir_b.x;
|
||||||
|
assert!(cross.abs() < 1.0e-6);
|
||||||
|
let offset = moved_b.start - moved_a.start;
|
||||||
|
let separation = (dir_a.x * offset.y - dir_a.y * offset.x).abs() / dir_a.length();
|
||||||
assert!(
|
assert!(
|
||||||
cross.abs() < 1e-6,
|
(separation - 2.0).abs() < 1.0e-6,
|
||||||
"the previewed positions should be mutually parallel: dir_a={dir_a:?} dir_b={dir_b:?}"
|
"separation={separation}, a={moved_a:?}, b={moved_b:?}, circle={moved_circle:?}"
|
||||||
);
|
);
|
||||||
assert_eq!(fixed_b.start.y - moved_a_line.start.y, 5.0);
|
assert!((moved_circle.radius - 1.0).abs() < 1.0e-9);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in new issue
Block a user