forked from 1b-agent/opencadstudio-upstream
Merge PR #1279 and complete parametric UI actions
This commit is contained in:
commit
adc8555394
22 files changed
+672
-217
No files matched your search
@@ -203,6 +203,42 @@ impl Scene {
|
||||
self.bump_selection_set();
|
||||
}
|
||||
}
|
||||
|
||||
/// Partition selected entity handles into visual objects for operations
|
||||
/// such as edge alignment. A selectable CAD group is one object and must
|
||||
/// receive one shared transform; treating its member lines independently
|
||||
/// can collapse a rectangle or triangle onto its own edge.
|
||||
pub fn selected_object_units(&self, handles: &[Handle]) -> Vec<Vec<Handle>> {
|
||||
let selected: HashSet<Handle> = handles.iter().copied().collect();
|
||||
let mut claimed = HashSet::default();
|
||||
let mut units = Vec::new();
|
||||
for group in self.groups().filter(|group| group.selectable) {
|
||||
let members: Vec<_> = group
|
||||
.entities
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|handle| selected.contains(handle))
|
||||
.collect();
|
||||
if !members.is_empty()
|
||||
&& members.iter().all(|handle| !claimed.contains(handle))
|
||||
&& group
|
||||
.entities
|
||||
.iter()
|
||||
.all(|handle| selected.contains(handle))
|
||||
{
|
||||
claimed.extend(members.iter().copied());
|
||||
units.push(members);
|
||||
}
|
||||
}
|
||||
units.extend(
|
||||
handles
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|handle| !claimed.contains(handle))
|
||||
.map(|handle| vec![handle]),
|
||||
);
|
||||
units
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -247,4 +283,31 @@ mod group_expansion_tests {
|
||||
assert_eq!(both, [a, b, c, d, e].into_iter().collect::<HashSet<_>>());
|
||||
assert!(!both.contains(&lonely));
|
||||
}
|
||||
|
||||
/// `selected_object_units` is `Scene::selected_object_units`'s
|
||||
/// counterpart for operations like edge alignment (`ALIGNTOP`, etc.):
|
||||
/// a complete, fully-selected group collapses to one unit sharing a
|
||||
/// single transform, so aligning a triangle's edge to another object
|
||||
/// moves the whole triangle rather than collapsing its own lines onto
|
||||
/// each other.
|
||||
#[test]
|
||||
fn selected_object_units_keep_a_complete_group_together() {
|
||||
use acadrust::entities::{EntityType, Line};
|
||||
use acadrust::types::Vector3;
|
||||
|
||||
let mut scene = Scene::new();
|
||||
let mut line = |x: f64| {
|
||||
scene.add_entity(EntityType::Line(Line::from_points(
|
||||
Vector3::new(x, 0.0, 0.0),
|
||||
Vector3::new(x + 1.0, 0.0, 0.0),
|
||||
)))
|
||||
};
|
||||
let (a, b, c, standalone) = (line(0.0), line(1.0), line(2.0), line(10.0));
|
||||
scene.create_group("triangle".into(), vec![a, b, c]);
|
||||
|
||||
let units = scene.selected_object_units(&[a, b, c, standalone]);
|
||||
assert_eq!(units.len(), 2);
|
||||
assert!(units.iter().any(|unit| unit == &vec![a, b, c]));
|
||||
assert!(units.iter().any(|unit| unit == &vec![standalone]));
|
||||
}
|
||||
}
|
||||
@@ -1658,6 +1658,9 @@ pub struct Scene {
|
||||
/// Currently selected entity handles.
|
||||
pub selected: HashSet<Handle>,
|
||||
selected_order: Vec<Handle>,
|
||||
/// The parametric-constraint glyph currently selected in the viewport.
|
||||
/// Entity selection changes clear it so the two selections stay exclusive.
|
||||
pub selected_constraint: Option<crate::scene::parametric_constraints::ConstraintId>,
|
||||
/// Session-only ISOLATEOBJECTS / HIDEOBJECTS state. Never written to DWG/DXF.
|
||||
pub object_isolation: ObjectIsolationState,
|
||||
/// Entity handles temporarily removed from the base render while an
|
||||
@@ -2194,6 +2197,7 @@ impl Scene {
|
||||
lighting_cache: RefCell::new(HashMap::default()),
|
||||
selected: HashSet::default(),
|
||||
selected_order: Vec::new(),
|
||||
selected_constraint: None,
|
||||
object_isolation: ObjectIsolationState::default(),
|
||||
preview_hidden: HashSet::default(),
|
||||
command_preview_hidden: HashSet::default(),
|
||||
|
||||
@@ -790,6 +790,98 @@ impl super::Scene {
|
||||
}
|
||||
}
|
||||
|
||||
/// Screen-projected parametric-constraint glyph placements for `scope`:
|
||||
/// `(id, anchor, outward_screen_direction, label, is_conflicting)` for
|
||||
/// every enabled, visible constraint whose glyph projects on-screen.
|
||||
/// `vp_size` is the full canvas size (as `SelectionState::vp_size`
|
||||
/// reports it), matching what `viewport_edit_frame`/
|
||||
/// `active_model_tile_bounds` expect. Mirrors the projection
|
||||
/// `crate::app::view` builds its own render list with, and is reused by
|
||||
/// [`constraint_glyph_hit`](Self::constraint_glyph_hit) — both feed the
|
||||
/// same `(anchor, outward, label)` triples into
|
||||
/// `crate::ui::overlay::constraint_glyph_box`/`constraint_glyph_offsets`,
|
||||
/// so hit-testing can never drift from what's actually drawn.
|
||||
pub fn constraint_glyph_placements_screen(
|
||||
&self,
|
||||
scope: ParametricScope,
|
||||
vp_size: (f32, f32),
|
||||
show_values: bool,
|
||||
) -> Vec<(ConstraintId, iced::Point, [f32; 2], String, bool)> {
|
||||
let Some(set) = self.parametric_constraint_set(scope) else {
|
||||
return Vec::new();
|
||||
};
|
||||
if set.constraints.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let edit_frame = self.viewport_edit_frame(vp_size);
|
||||
let bounds = match &edit_frame {
|
||||
Some((_, full)) => *full,
|
||||
None => self.active_model_tile_bounds(vp_size.0, vp_size.1),
|
||||
};
|
||||
let (view_rot, eye) = if let Some((cam, _)) = &edit_frame {
|
||||
(cam.view_proj_rte(bounds), cam.eye())
|
||||
} else {
|
||||
let cam = self.camera.borrow();
|
||||
(cam.view_proj_rte(bounds), cam.eye())
|
||||
};
|
||||
set.constraints
|
||||
.iter()
|
||||
.filter(|c| c.enabled)
|
||||
.filter(|c| self.is_parametric_constraint_visible(scope, c.id))
|
||||
.filter_map(|c| {
|
||||
let (anchor, outward) = glyph_placement(&self.document, c)?;
|
||||
let screen = crate::scene::pick::grip::project_rte(
|
||||
glam::DVec3::new(anchor.x, anchor.y, anchor.z),
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
)?;
|
||||
let outward_screen = crate::scene::pick::grip::project_rte(
|
||||
glam::DVec3::new(
|
||||
anchor.x + outward.x,
|
||||
anchor.y + outward.y,
|
||||
anchor.z + outward.z,
|
||||
),
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
)?;
|
||||
let direction = (outward_screen - screen).normalize_or(glam::Vec2::NEG_Y);
|
||||
let point = iced::Point::new(bounds.x + screen.x, bounds.y + screen.y);
|
||||
let is_conflicting = set.conflicts.iter().any(|(id, _)| *id == c.id);
|
||||
let label = if show_values {
|
||||
glyph_label(c)
|
||||
} else {
|
||||
c.kind.glyph_symbol().to_string()
|
||||
};
|
||||
point.x.is_finite().then_some((c.id, point, direction.to_array(), label, is_conflicting))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Hit-tests screen point `p` (same coordinate space as `p_full` in the
|
||||
/// viewport click handler) against the glyph pills from
|
||||
/// [`constraint_glyph_placements_screen`](Self::constraint_glyph_placements_screen),
|
||||
/// via `crate::ui::overlay::constraint_glyph_hit_test`'s shared layout
|
||||
/// math, so a click only registers where the pill is actually drawn.
|
||||
pub fn constraint_glyph_hit(
|
||||
&self,
|
||||
scope: ParametricScope,
|
||||
vp_size: (f32, f32),
|
||||
show_values: bool,
|
||||
p: iced::Point,
|
||||
) -> Option<ConstraintId> {
|
||||
let placements = self.constraint_glyph_placements_screen(scope, vp_size, show_values);
|
||||
let glyphs: Vec<(iced::Point, [f32; 2], String, bool)> = placements
|
||||
.iter()
|
||||
.map(|(_, point, direction, label, is_conflicting)| {
|
||||
(*point, *direction, label.clone(), *is_conflicting)
|
||||
})
|
||||
.collect();
|
||||
let index = crate::ui::overlay::constraint_glyph_hit_test(&glyphs, p)?;
|
||||
Some(placements[index].0)
|
||||
}
|
||||
|
||||
/// Handle remapping lives in each command that duplicates
|
||||
/// entities — `Scene::copy_entities`' `handle_map` (COPY/ARRAY/MIRROR,
|
||||
/// `src/scene/modify.rs`) and `OpenCADStudio::finalize_paste`'s own
|
||||
|
||||
@@ -72,6 +72,7 @@ impl Scene {
|
||||
if handles.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.selected_constraint = None;
|
||||
let expanded = self.expanded_with_leaders(handles);
|
||||
let mut changed = false;
|
||||
for handle in expanded {
|
||||
@@ -111,6 +112,7 @@ impl Scene {
|
||||
expanded
|
||||
}
|
||||
pub fn select_entity(&mut self, handle: Handle, exclusive: bool) {
|
||||
self.selected_constraint = None;
|
||||
let handles = self.handles_expanded_for_leader_annotations(&[handle]);
|
||||
let mut changed = false;
|
||||
|
||||
@@ -133,6 +135,7 @@ impl Scene {
|
||||
}
|
||||
|
||||
pub fn deselect_all(&mut self) {
|
||||
self.selected_constraint = None;
|
||||
if self.selected.is_empty() {
|
||||
return;
|
||||
}
|
||||
@@ -215,6 +218,7 @@ impl Scene {
|
||||
/// only when its contents actually changed. History/file/command paths must
|
||||
/// use this instead of assigning `selected` directly.
|
||||
pub(crate) fn replace_selection(&mut self, selected: HashSet<Handle>) {
|
||||
self.selected_constraint = None;
|
||||
let handles: Vec<Handle> = selected.iter().copied().collect();
|
||||
let selected: HashSet<Handle> = self
|
||||
.handles_expanded_for_leader_annotations(&handles)
|
||||
|
||||
Reference in new issue
Block a user