feat(area): support multi-object selection
This commit is contained in:
parent
48332ee807
commit
5662e07303
8 changed files with 426 additions and 99 deletions
|
|
@ -40,15 +40,16 @@ command-line-hint = Type a command or use the ribbon. Open OBJ from the Insert t
|
|||
command-line-label = Command:
|
||||
command-line-literal-spaces = Literal spaces: Space stays in the line instead of running the command. Stays on until toggled off.
|
||||
|
||||
area-option-object = Object
|
||||
area-option-objects = Objects
|
||||
area-option-back = Back
|
||||
area-option-add = Add area
|
||||
area-option-subtract = Subtract area
|
||||
area-prompt-first = AREA Specify first corner point or [Object/Add area/Subtract area] <Object>:
|
||||
area-prompt-add = AREA ADD Specify first corner point or [Object/Subtract area] <Object>:
|
||||
area-prompt-subtract = AREA SUBTRACT Specify first corner point or [Object/Add area] <Object>:
|
||||
area-prompt-first = AREA Specify first corner point or [Objects/Add area/Subtract area] <Objects>:
|
||||
area-prompt-add = AREA ADD Specify first corner point or [Objects/Subtract area] <Objects>:
|
||||
area-prompt-subtract = AREA SUBTRACT Specify first corner point or [Objects/Add area] <Objects>:
|
||||
area-prompt-next = AREA Specify next point ({ $count } picked, Enter to calculate):
|
||||
area-prompt-object = AREA Select object:
|
||||
area-object-not-measurable = AREA: the selected object has no measurable area.
|
||||
area-prompt-objects = AREA Select objects ({ $count } selected, Enter to calculate):
|
||||
area-objects-not-measurable = AREA: none of the selected objects has a measurable area.
|
||||
area-result = Area = { $area }, Perimeter = { $perimeter }
|
||||
area-result-area-only = Area = { $area }
|
||||
area-running-result =
|
||||
|
|
|
|||
|
|
@ -40,15 +40,16 @@ command-line-hint = Bir komut yazın veya şeridi kullanın. OBJ dosyasını Ekl
|
|||
command-line-label = Komut:
|
||||
command-line-literal-spaces = Değişmez boşluklar: Boşluk, komutu çalıştırmak yerine satırda kalır. Yeniden kapatılana kadar etkin kalır.
|
||||
|
||||
area-option-object = Nesne
|
||||
area-option-objects = Nesneler
|
||||
area-option-back = Geri
|
||||
area-option-add = Alan ekle
|
||||
area-option-subtract = Alan çıkar
|
||||
area-prompt-first = AREA İlk köşe noktasını belirtin veya [Nesne/Alan ekle/Alan çıkar] <Nesne>:
|
||||
area-prompt-add = AREA EKLE İlk köşe noktasını belirtin veya [Nesne/Alan çıkar] <Nesne>:
|
||||
area-prompt-subtract = AREA ÇIKAR İlk köşe noktasını belirtin veya [Nesne/Alan ekle] <Nesne>:
|
||||
area-prompt-first = AREA İlk köşe noktasını belirtin veya [Nesneler/Alan ekle/Alan çıkar] <Nesneler>:
|
||||
area-prompt-add = AREA EKLE İlk köşe noktasını belirtin veya [Nesneler/Alan çıkar] <Nesneler>:
|
||||
area-prompt-subtract = AREA ÇIKAR İlk köşe noktasını belirtin veya [Nesneler/Alan ekle] <Nesneler>:
|
||||
area-prompt-next = AREA Sonraki noktayı belirtin ({ $count } seçildi, hesaplamak için Enter):
|
||||
area-prompt-object = AREA Nesne seçin:
|
||||
area-object-not-measurable = AREA: Seçilen nesnenin ölçülebilir alanı yok.
|
||||
area-prompt-objects = AREA Nesneleri seçin ({ $count } seçildi, hesaplamak için Enter):
|
||||
area-objects-not-measurable = AREA: Seçilen nesnelerin hiçbirinde ölçülebilir alan yok.
|
||||
area-result = Alan = { $area }, Çevre = { $perimeter }
|
||||
area-result-area-only = Alan = { $area }
|
||||
area-running-result =
|
||||
|
|
|
|||
|
|
@ -1,9 +1,19 @@
|
|||
use super::{Message, OpenCADStudio};
|
||||
use crate::command::{CmdResult, StepInput};
|
||||
use crate::command::{CmdResult, SelectionEntity, StepInput};
|
||||
use acadrust::Handle;
|
||||
use iced::Task;
|
||||
|
||||
impl OpenCADStudio {
|
||||
fn refresh_area_preview(&mut self, i: usize) {
|
||||
let regions = self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.and_then(|command| command.area_preview_regions());
|
||||
if let Some(regions) = regions {
|
||||
self.tabs[i].scene.set_area_preview_regions(®ions);
|
||||
}
|
||||
}
|
||||
|
||||
/// Point supplied by a bare Enter before LINE/PLINE's first click. Prefer
|
||||
/// the current endpoint of the most recently created path drawable in the
|
||||
/// active space. A loaded drawing has no runtime anchor, so recover its
|
||||
|
|
@ -258,6 +268,31 @@ impl OpenCADStudio {
|
|||
self.reset_tracking_after_point();
|
||||
self.push_ucs_to_cmd(i);
|
||||
}
|
||||
if let StepInput::SelectionComplete(handles) = &input {
|
||||
let entities = {
|
||||
let scene = &self.tabs[i].scene;
|
||||
handles
|
||||
.iter()
|
||||
.filter_map(|handle| {
|
||||
scene.document.get_entity(*handle).cloned().map(|entity| {
|
||||
let surface_area = scene
|
||||
.meshes
|
||||
.get(handle)
|
||||
.or_else(|| scene.block_meshes.get(handle))
|
||||
.map(|mesh| mesh.metrics.surface_area);
|
||||
SelectionEntity {
|
||||
handle: *handle,
|
||||
entity,
|
||||
surface_area,
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
|
||||
command.inject_selection_entities(entities);
|
||||
}
|
||||
}
|
||||
let ctrl = self.ctrl_down;
|
||||
let shift = self.shift_down;
|
||||
let result: Option<CmdResult> = {
|
||||
|
|
@ -399,14 +434,7 @@ impl OpenCADStudio {
|
|||
.into_iter()
|
||||
.map(|(h, _)| h)
|
||||
.collect();
|
||||
let result = self.tabs[i]
|
||||
.active_cmd
|
||||
.as_mut()
|
||||
.map(|cmd| cmd.on_selection_complete(handles));
|
||||
Some(match result {
|
||||
Some(r) => self.apply_cmd_result(r),
|
||||
None => Task::none(),
|
||||
})
|
||||
Some(self.feed_command(StepInput::SelectionComplete(handles)))
|
||||
}
|
||||
|
||||
pub(super) fn feed_active_cmd(&mut self, token: &str) {
|
||||
|
|
@ -557,6 +585,7 @@ impl OpenCADStudio {
|
|||
// and typed digits land in it rather than the command line,
|
||||
// instead of waiting for the next cursor move to resync.
|
||||
self.sync_dyn_fields();
|
||||
self.refresh_area_preview(i);
|
||||
}
|
||||
CmdResult::Preview(wire) => {
|
||||
self.tabs[i].scene.set_preview_wires(vec![wire]);
|
||||
|
|
@ -1792,11 +1821,33 @@ impl OpenCADStudio {
|
|||
CmdResult::ReportMeasurement(msg) => {
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.refresh_area_preview(i);
|
||||
self.command_line.push_output(&msg);
|
||||
if let Some(prompt) = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()) {
|
||||
self.command_line.push_info(&prompt);
|
||||
}
|
||||
}
|
||||
CmdResult::ReportMeasurementAndDeselect(msg) => {
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.refresh_area_preview(i);
|
||||
self.refresh_properties();
|
||||
self.command_line.push_output(&msg);
|
||||
if let Some(prompt) = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()) {
|
||||
self.command_line.push_info(&prompt);
|
||||
}
|
||||
}
|
||||
CmdResult::DeselectAndContinue => {
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.refresh_area_preview(i);
|
||||
self.refresh_properties();
|
||||
if let Some(prompt) = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()) {
|
||||
self.command_line.push_info(&prompt);
|
||||
}
|
||||
}
|
||||
CmdResult::AlignSelected {
|
||||
handles,
|
||||
src1,
|
||||
|
|
|
|||
|
|
@ -2417,6 +2417,11 @@ impl OpenCADStudio {
|
|||
.as_ref()
|
||||
.map(|c| c.is_selection_gathering())
|
||||
.unwrap_or(false);
|
||||
let selection_pick_add = self.pick_add
|
||||
|| self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.is_some_and(|command| command.selection_forces_add());
|
||||
// A committed window corner must stay free of the Ortho/Polar
|
||||
// lock too, so the picked rectangle isn't flattened (#291).
|
||||
let is_window_corner = self.tabs[i]
|
||||
|
|
@ -3011,7 +3016,7 @@ impl OpenCADStudio {
|
|||
self.tabs[i].scene.deselect_entity(*h);
|
||||
}
|
||||
} else {
|
||||
if !self.pick_add && !handles.is_empty() {
|
||||
if !selection_pick_add && !handles.is_empty() {
|
||||
self.tabs[i].scene.deselect_all();
|
||||
}
|
||||
for h in &handles {
|
||||
|
|
@ -3142,7 +3147,7 @@ impl OpenCADStudio {
|
|||
// PICKADD 0 (#226): a plain marquee REPLACES
|
||||
// the selection (empty results still leave it
|
||||
// alone, matching the box rule).
|
||||
if !self.pick_add && !handles.is_empty() {
|
||||
if !selection_pick_add && !handles.is_empty() {
|
||||
self.tabs[i].scene.deselect_all();
|
||||
}
|
||||
for h in &handles {
|
||||
|
|
@ -3271,7 +3276,7 @@ impl OpenCADStudio {
|
|||
// REPLACES the selection instead and
|
||||
// Shift+click toggles membership.
|
||||
if self.shift_down {
|
||||
if !self.pick_add
|
||||
if !selection_pick_add
|
||||
&& !self.tabs[i].scene.selected.contains(&handle)
|
||||
{
|
||||
self.tabs[i].scene.select_entity(handle, false);
|
||||
|
|
@ -3280,7 +3285,9 @@ impl OpenCADStudio {
|
|||
self.tabs[i].scene.deselect_entity(handle);
|
||||
}
|
||||
} else {
|
||||
self.tabs[i].scene.select_entity(handle, !self.pick_add);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.select_entity(handle, !selection_pick_add);
|
||||
self.tabs[i].scene.expand_selection_for_groups(&[handle]);
|
||||
}
|
||||
self.refresh_properties();
|
||||
|
|
@ -3294,7 +3301,7 @@ impl OpenCADStudio {
|
|||
// selection.
|
||||
// PICKADD 0 (#226): OS convention — the
|
||||
// empty click also drops the selection.
|
||||
if !self.pick_add && !self.shift_down {
|
||||
if !selection_pick_add && !self.shift_down {
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.refresh_properties();
|
||||
}
|
||||
|
|
@ -3413,7 +3420,7 @@ impl OpenCADStudio {
|
|||
self.tabs[i].scene.deselect_entity(*h);
|
||||
}
|
||||
} else {
|
||||
if !self.pick_add && !handles.is_empty() {
|
||||
if !selection_pick_add && !handles.is_empty() {
|
||||
self.tabs[i].scene.deselect_all();
|
||||
}
|
||||
for h in &handles {
|
||||
|
|
@ -3447,10 +3454,7 @@ impl OpenCADStudio {
|
|||
.into_iter()
|
||||
.map(|(h, _)| h)
|
||||
.collect();
|
||||
if let Some(cmd) = self.tabs[i].active_cmd.as_mut() {
|
||||
let result = cmd.on_selection_complete(handles);
|
||||
return self.apply_cmd_result(result);
|
||||
}
|
||||
return self.feed_command(crate::command::StepInput::SelectionComplete(handles));
|
||||
}
|
||||
|
||||
// ── Double-click in Model Space: DDEDIT for Text/MText ────
|
||||
|
|
|
|||
|
|
@ -20,6 +20,25 @@ pub struct ObjectPickHit {
|
|||
pub label: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SelectionEntity {
|
||||
pub handle: Handle,
|
||||
pub entity: EntityType,
|
||||
pub surface_area: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum AreaPreviewSource {
|
||||
Handles(Vec<Handle>),
|
||||
Boundary(Vec<[f64; 2]>),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AreaPreviewRegion {
|
||||
pub source: AreaPreviewSource,
|
||||
pub subtract: bool,
|
||||
}
|
||||
|
||||
// ── Transform ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// A geometric transformation applied to existing entities.
|
||||
|
|
@ -908,6 +927,10 @@ pub enum CmdResult {
|
|||
Measurement(String),
|
||||
/// Print a measurement result and keep the command active.
|
||||
ReportMeasurement(String),
|
||||
/// Print a measurement result, clear the current selection, and keep the command active.
|
||||
ReportMeasurementAndDeselect(String),
|
||||
/// Clear the current selection and keep the command active at its updated step.
|
||||
DeselectAndContinue,
|
||||
/// Break `handle` at points `p1` and `p2`; replace with computed fragments.
|
||||
BreakEntity { handle: Handle, p1: DVec3, p2: DVec3 },
|
||||
/// Attempt to join the given entities into fewer merged entities.
|
||||
|
|
@ -1473,6 +1496,10 @@ pub trait CadCommand: Send {
|
|||
false
|
||||
}
|
||||
|
||||
fn selection_forces_add(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Called after a selection action completes while `is_selection_gathering` is true.
|
||||
/// `handles` is the full set of currently selected entities.
|
||||
/// Return `Relaunch` to fire the pending command, or `NeedPoint` to keep gathering.
|
||||
|
|
@ -1480,6 +1507,12 @@ pub trait CadCommand: Send {
|
|||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn inject_selection_entities(&mut self, _entities: Vec<SelectionEntity>) {}
|
||||
|
||||
fn area_preview_regions(&self) -> Option<Vec<AreaPreviewRegion>> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns `true` when the current step picks a corner of a selection
|
||||
/// *window* by point (e.g. STRETCH's crossing window). Such a pick must be a
|
||||
/// free point: applying the Ortho/Polar lock would pin the opposite corner to
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
use acadrust::{EntityType, Handle};
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult};
|
||||
use crate::command::{
|
||||
AreaPreviewRegion, AreaPreviewSource, CadCommand, CmdOption, CmdResult, SelectionEntity,
|
||||
};
|
||||
use crate::entities::traits::EntityTypeOps;
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
|
||||
|
|
@ -21,9 +23,9 @@ struct AreaMeasurement {
|
|||
pub struct AreaCommand {
|
||||
mode: AreaMode,
|
||||
points: Vec<DVec3>,
|
||||
object_pick: bool,
|
||||
picked_entity: Option<EntityType>,
|
||||
picked_surface_area: Option<f64>,
|
||||
objects_gathering: bool,
|
||||
selected_entities: Vec<SelectionEntity>,
|
||||
preview_regions: Vec<AreaPreviewRegion>,
|
||||
total_area: f64,
|
||||
total_perimeter: f64,
|
||||
}
|
||||
|
|
@ -33,9 +35,9 @@ impl AreaCommand {
|
|||
Self {
|
||||
mode: AreaMode::Single,
|
||||
points: Vec::new(),
|
||||
object_pick: false,
|
||||
picked_entity: None,
|
||||
picked_surface_area: None,
|
||||
objects_gathering: false,
|
||||
selected_entities: Vec::new(),
|
||||
preview_regions: Vec::new(),
|
||||
total_area: 0.0,
|
||||
total_perimeter: 0.0,
|
||||
}
|
||||
|
|
@ -176,6 +178,38 @@ impl AreaCommand {
|
|||
}
|
||||
}
|
||||
|
||||
fn selection_entity_measurement(entity: &SelectionEntity) -> Option<AreaMeasurement> {
|
||||
Self::entity_measurement(&entity.entity).or_else(|| {
|
||||
entity.surface_area.map(|area| AreaMeasurement {
|
||||
area,
|
||||
perimeter: None,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn selection_measurement(&self) -> Option<(AreaMeasurement, Vec<Handle>)> {
|
||||
let measured = self
|
||||
.selected_entities
|
||||
.iter()
|
||||
.filter_map(|entity| {
|
||||
Self::selection_entity_measurement(entity)
|
||||
.map(|measurement| (entity.handle, measurement))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if measured.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let area = measured.iter().map(|(_, measurement)| measurement.area).sum();
|
||||
let perimeter = measured
|
||||
.iter()
|
||||
.map(|(_, measurement)| measurement.perimeter)
|
||||
.collect::<Option<Vec<_>>>()
|
||||
.map(|values| values.into_iter().sum());
|
||||
let handles = measured.into_iter().map(|(handle, _)| handle).collect();
|
||||
Some((AreaMeasurement { area, perimeter }, handles))
|
||||
}
|
||||
|
||||
fn result_message(measurement: AreaMeasurement) -> String {
|
||||
match measurement.perimeter {
|
||||
Some(perimeter) => crate::tr!(
|
||||
|
|
@ -204,7 +238,11 @@ impl AreaCommand {
|
|||
}
|
||||
}
|
||||
|
||||
fn finish_measurement(&mut self, measurement: AreaMeasurement) -> CmdResult {
|
||||
fn finish_measurement(
|
||||
&mut self,
|
||||
measurement: AreaMeasurement,
|
||||
deselect: bool,
|
||||
) -> CmdResult {
|
||||
if self.mode == AreaMode::Single {
|
||||
return CmdResult::Measurement(Self::result_message(measurement));
|
||||
}
|
||||
|
|
@ -215,8 +253,14 @@ impl AreaCommand {
|
|||
self.total_perimeter += sign * perimeter;
|
||||
}
|
||||
self.points.clear();
|
||||
self.object_pick = false;
|
||||
CmdResult::ReportMeasurement(self.running_result_message(measurement))
|
||||
self.objects_gathering = false;
|
||||
self.selected_entities.clear();
|
||||
let message = self.running_result_message(measurement);
|
||||
if deselect {
|
||||
CmdResult::ReportMeasurementAndDeselect(message)
|
||||
} else {
|
||||
CmdResult::ReportMeasurement(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -226,8 +270,11 @@ impl CadCommand for AreaCommand {
|
|||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if self.object_pick {
|
||||
return crate::tr!("area-prompt-object");
|
||||
if self.objects_gathering {
|
||||
return crate::tr!(
|
||||
"area-prompt-objects",
|
||||
count = self.selected_entities.len()
|
||||
);
|
||||
}
|
||||
if !self.points.is_empty() {
|
||||
return crate::tr!("area-prompt-next", count = self.points.len());
|
||||
|
|
@ -240,33 +287,54 @@ impl CadCommand for AreaCommand {
|
|||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
if self.object_pick || !self.points.is_empty() {
|
||||
if self.objects_gathering {
|
||||
return vec![Self::option(crate::tr!("area-option-back"), "BACK")];
|
||||
}
|
||||
if !self.points.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let object = || Self::option(crate::tr!("area-option-object"), "OBJECT");
|
||||
let objects = || Self::option(crate::tr!("area-option-objects"), "OBJECTS");
|
||||
let add = || Self::option(crate::tr!("area-option-add"), "ADD");
|
||||
let subtract = || Self::option(crate::tr!("area-option-subtract"), "SUBTRACT");
|
||||
match self.mode {
|
||||
AreaMode::Single => vec![object(), add(), subtract()],
|
||||
AreaMode::Add => vec![object(), subtract()],
|
||||
AreaMode::Subtract => vec![object(), add()],
|
||||
AreaMode::Single => vec![objects(), add(), subtract()],
|
||||
AreaMode::Add => vec![objects(), subtract()],
|
||||
AreaMode::Subtract => vec![objects(), add()],
|
||||
}
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
!self.object_pick
|
||||
!self.objects_gathering
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
!self.object_pick
|
||||
!self.objects_gathering
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
if self.object_pick || !self.points.is_empty() {
|
||||
let input = text.trim().to_ascii_uppercase();
|
||||
if self.objects_gathering {
|
||||
if matches!(input.as_str(), "B" | "BACK") {
|
||||
if let Some((measurement, handles)) = self.selection_measurement() {
|
||||
if self.mode != AreaMode::Single {
|
||||
self.preview_regions.push(AreaPreviewRegion {
|
||||
source: AreaPreviewSource::Handles(handles),
|
||||
subtract: self.mode == AreaMode::Subtract,
|
||||
});
|
||||
}
|
||||
return Some(self.finish_measurement(measurement, true));
|
||||
}
|
||||
self.objects_gathering = false;
|
||||
self.selected_entities.clear();
|
||||
return Some(CmdResult::DeselectAndContinue);
|
||||
}
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
match text.trim().to_ascii_uppercase().as_str() {
|
||||
"O" | "OBJECT" => self.object_pick = true,
|
||||
if !self.points.is_empty() {
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
match input.as_str() {
|
||||
"O" | "OBJECT" | "OBJECTS" => self.objects_gathering = true,
|
||||
"A" | "ADD" => self.mode = AreaMode::Add,
|
||||
"S" | "SUBTRACT" => self.mode = AreaMode::Subtract,
|
||||
_ => return Some(CmdResult::NeedPoint),
|
||||
|
|
@ -274,73 +342,67 @@ impl CadCommand for AreaCommand {
|
|||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
self.object_pick
|
||||
fn is_selection_gathering(&self) -> bool {
|
||||
self.objects_gathering
|
||||
}
|
||||
|
||||
fn entity_pick_includes_fills(&self) -> bool {
|
||||
true
|
||||
fn selection_forces_add(&self) -> bool {
|
||||
self.objects_gathering
|
||||
}
|
||||
|
||||
fn entity_pick_highlights_hover(&self) -> bool {
|
||||
true
|
||||
fn inject_selection_entities(&mut self, entities: Vec<SelectionEntity>) {
|
||||
self.selected_entities = entities;
|
||||
}
|
||||
|
||||
fn inject_before_entity_pick(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn inject_picked_entity(&mut self, entity: EntityType) {
|
||||
self.picked_entity = Some(entity);
|
||||
self.picked_surface_area = None;
|
||||
}
|
||||
|
||||
fn inject_picked_surface_area(&mut self, area: f64) {
|
||||
self.picked_surface_area = Some(area);
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _point: DVec3) -> CmdResult {
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
let measurement = self
|
||||
.picked_entity
|
||||
.take()
|
||||
.and_then(|entity| Self::entity_measurement(&entity))
|
||||
.or_else(|| {
|
||||
self.picked_surface_area.take().map(|area| AreaMeasurement {
|
||||
area,
|
||||
perimeter: None,
|
||||
})
|
||||
});
|
||||
match measurement {
|
||||
Some(measurement) => self.finish_measurement(measurement),
|
||||
None => CmdResult::ReportMeasurement(crate::tr!("area-object-not-measurable")),
|
||||
}
|
||||
fn on_selection_complete(&mut self, _handles: Vec<Handle>) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||
if self.objects_gathering {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
self.points.push(point);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
if self.object_pick {
|
||||
return CmdResult::Cancel;
|
||||
if self.objects_gathering {
|
||||
let Some((measurement, handles)) = self.selection_measurement() else {
|
||||
self.selected_entities.clear();
|
||||
return CmdResult::ReportMeasurementAndDeselect(crate::tr!(
|
||||
"area-objects-not-measurable"
|
||||
));
|
||||
};
|
||||
if self.mode != AreaMode::Single {
|
||||
self.preview_regions.push(AreaPreviewRegion {
|
||||
source: AreaPreviewSource::Handles(handles),
|
||||
subtract: self.mode == AreaMode::Subtract,
|
||||
});
|
||||
}
|
||||
return self.finish_measurement(measurement, true);
|
||||
}
|
||||
if self.points.is_empty() {
|
||||
self.object_pick = true;
|
||||
self.objects_gathering = true;
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
if self.points.len() < 3 {
|
||||
return CmdResult::Cancel;
|
||||
}
|
||||
let measurement = Self::point_measurement(&self.points, true);
|
||||
self.finish_measurement(measurement)
|
||||
if self.mode != AreaMode::Single {
|
||||
self.preview_regions.push(AreaPreviewRegion {
|
||||
source: AreaPreviewSource::Boundary(
|
||||
self.points.iter().map(|point| [point.x, point.y]).collect(),
|
||||
),
|
||||
subtract: self.mode == AreaMode::Subtract,
|
||||
});
|
||||
}
|
||||
self.finish_measurement(measurement, false)
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
|
||||
if self.object_pick || self.points.is_empty() {
|
||||
if self.objects_gathering || self.points.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let to_render = |p: DVec3| [p.x as f32, p.y as f32, p.z as f32];
|
||||
|
|
@ -375,6 +437,31 @@ impl CadCommand for AreaCommand {
|
|||
fill_tris_low: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn area_preview_regions(&self) -> Option<Vec<AreaPreviewRegion>> {
|
||||
let mut regions = self.preview_regions.clone();
|
||||
if self.objects_gathering {
|
||||
let handles = self
|
||||
.selected_entities
|
||||
.iter()
|
||||
.map(|entity| entity.handle)
|
||||
.collect::<Vec<_>>();
|
||||
if !handles.is_empty() {
|
||||
regions.push(AreaPreviewRegion {
|
||||
source: AreaPreviewSource::Handles(handles),
|
||||
subtract: self.mode == AreaMode::Subtract,
|
||||
});
|
||||
}
|
||||
} else if self.points.len() >= 3 {
|
||||
regions.push(AreaPreviewRegion {
|
||||
source: AreaPreviewSource::Boundary(
|
||||
self.points.iter().map(|point| [point.x, point.y]).collect(),
|
||||
),
|
||||
subtract: self.mode == AreaMode::Subtract,
|
||||
});
|
||||
}
|
||||
Some(regions)
|
||||
}
|
||||
}
|
||||
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["AREA"] });
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
// Auto-split from scene/mod.rs. Pure text-move; behaviour unchanged.
|
||||
use super::*;
|
||||
use crate::command::{AreaPreviewRegion, AreaPreviewSource};
|
||||
use crate::scene::model::hatch_model::HatchPattern;
|
||||
|
||||
impl Scene {
|
||||
// ── Preview wire ──────────────────────────────────────────────────────
|
||||
|
|
@ -27,6 +29,158 @@ impl Scene {
|
|||
self.preview_hatches = std::sync::Arc::new(models);
|
||||
}
|
||||
|
||||
pub fn set_area_preview_regions(&mut self, regions: &[AreaPreviewRegion]) {
|
||||
let mut models = Vec::new();
|
||||
for region in regions {
|
||||
match ®ion.source {
|
||||
AreaPreviewSource::Handles(handles) => {
|
||||
for &handle in handles {
|
||||
self.append_area_preview_handle(handle, region.subtract, &mut models);
|
||||
}
|
||||
}
|
||||
AreaPreviewSource::Boundary(boundary) => {
|
||||
if let Some(model) = Self::area_preview_hatch(
|
||||
std::slice::from_ref(boundary),
|
||||
region.subtract,
|
||||
) {
|
||||
models.push(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.preview_hatches = std::sync::Arc::new(models);
|
||||
}
|
||||
|
||||
fn append_area_preview_handle(
|
||||
&self,
|
||||
handle: Handle,
|
||||
subtract: bool,
|
||||
models: &mut Vec<HatchModel>,
|
||||
) {
|
||||
if let Some(mut model) = self.hatches.get(&handle).cloned() {
|
||||
Self::style_area_preview_hatch(&mut model, subtract);
|
||||
models.push(model);
|
||||
return;
|
||||
}
|
||||
|
||||
let direct_boundary = crate::scene::project::clip_boundary_polygon_for_document(
|
||||
&self.document,
|
||||
handle,
|
||||
0.0,
|
||||
);
|
||||
let mut rings = if direct_boundary.len() >= 3 {
|
||||
vec![direct_boundary
|
||||
.into_iter()
|
||||
.map(|point| [point[0] as f64, point[1] as f64])
|
||||
.collect()]
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
if !rings.is_empty() {
|
||||
if let Some(model) = Self::area_preview_hatch(&rings, subtract) {
|
||||
models.push(model);
|
||||
}
|
||||
return;
|
||||
}
|
||||
for wire in self.wire_models_for(&[handle]) {
|
||||
let mut ring = Vec::new();
|
||||
for (index, point) in wire.points.iter().enumerate() {
|
||||
let low = wire.points_low.get(index).copied().unwrap_or([0.0; 3]);
|
||||
let x = point[0] as f64 + low[0] as f64;
|
||||
let y = point[1] as f64 + low[1] as f64;
|
||||
if x.is_finite() && y.is_finite() {
|
||||
let candidate = [x, y];
|
||||
if ring.last().is_none_or(|last| *last != candidate) {
|
||||
ring.push(candidate);
|
||||
}
|
||||
} else {
|
||||
Self::push_area_preview_ring(&mut rings, &mut ring);
|
||||
}
|
||||
}
|
||||
Self::push_area_preview_ring(&mut rings, &mut ring);
|
||||
}
|
||||
if let Some(model) = Self::area_preview_hatch(&rings, subtract) {
|
||||
models.push(model);
|
||||
}
|
||||
}
|
||||
|
||||
fn push_area_preview_ring(rings: &mut Vec<Vec<[f64; 2]>>, ring: &mut Vec<[f64; 2]>) {
|
||||
if ring.len() >= 3 {
|
||||
let min_x = ring.iter().map(|point| point[0]).fold(f64::INFINITY, f64::min);
|
||||
let max_x = ring
|
||||
.iter()
|
||||
.map(|point| point[0])
|
||||
.fold(f64::NEG_INFINITY, f64::max);
|
||||
let min_y = ring.iter().map(|point| point[1]).fold(f64::INFINITY, f64::min);
|
||||
let max_y = ring
|
||||
.iter()
|
||||
.map(|point| point[1])
|
||||
.fold(f64::NEG_INFINITY, f64::max);
|
||||
let diagonal_sq = (max_x - min_x).powi(2) + (max_y - min_y).powi(2);
|
||||
let twice_area = ring
|
||||
.iter()
|
||||
.zip(ring.iter().cycle().skip(1))
|
||||
.take(ring.len())
|
||||
.map(|(a, b)| a[0] * b[1] - b[0] * a[1])
|
||||
.sum::<f64>()
|
||||
.abs();
|
||||
if twice_area > diagonal_sq * 1e-12 {
|
||||
rings.push(std::mem::take(ring));
|
||||
return;
|
||||
}
|
||||
}
|
||||
ring.clear();
|
||||
}
|
||||
|
||||
fn area_preview_hatch(rings: &[Vec<[f64; 2]>], subtract: bool) -> Option<HatchModel> {
|
||||
let origin = rings.iter().find_map(|ring| ring.first()).copied()?;
|
||||
let mut boundary = Vec::new();
|
||||
let mut first = true;
|
||||
for ring in rings.iter().filter(|ring| ring.len() >= 3) {
|
||||
if !first {
|
||||
boundary.push([f32::NAN, f32::NAN]);
|
||||
}
|
||||
first = false;
|
||||
boundary.extend(
|
||||
ring.iter()
|
||||
.map(|point| [(point[0] - origin[0]) as f32, (point[1] - origin[1]) as f32]),
|
||||
);
|
||||
}
|
||||
if boundary.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
let mut model = HatchModel {
|
||||
world_origin: origin,
|
||||
boundary: std::sync::Arc::new(boundary),
|
||||
boundary_wcs: None,
|
||||
pattern: HatchPattern::Solid,
|
||||
name: "AREA_PREVIEW".into(),
|
||||
color: [0.0; 4],
|
||||
aci: 0,
|
||||
line_weight_px: 1.0,
|
||||
angle_offset: 0.0,
|
||||
scale: 1.0,
|
||||
draw_depth: 0.0,
|
||||
};
|
||||
Self::style_area_preview_hatch(&mut model, subtract);
|
||||
Some(model)
|
||||
}
|
||||
|
||||
fn style_area_preview_hatch(model: &mut HatchModel, subtract: bool) {
|
||||
model.pattern = HatchPattern::Solid;
|
||||
model.name = "AREA_PREVIEW".into();
|
||||
model.color = if subtract {
|
||||
[1.0, 0.28, 0.18, 0.16]
|
||||
} else {
|
||||
[0.15, 0.55, 1.0, 0.12]
|
||||
};
|
||||
model.aci = 0;
|
||||
model.line_weight_px = 1.0;
|
||||
model.angle_offset = 0.0;
|
||||
model.scale = 1.0;
|
||||
model.draw_depth = 0.0;
|
||||
}
|
||||
|
||||
fn append_preview_hatch(&self, handle: Handle, models: &mut Vec<HatchModel>) {
|
||||
let Some(mut model) = self.hatches.get(&handle).cloned() else {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -384,11 +384,7 @@ impl shader::Primitive for Primitive {
|
|||
inner.upload_preview_hatches(
|
||||
device,
|
||||
queue,
|
||||
if fill_mode {
|
||||
&vp.preview_hatches[..]
|
||||
} else {
|
||||
&[]
|
||||
},
|
||||
&vp.preview_hatches[..],
|
||||
);
|
||||
inner.cached_preview_hatch_source =
|
||||
Some(Arc::clone(&vp.preview_hatches));
|
||||
|
|
|
|||
Loading…
Reference in a new issue