mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-09 03:50:04 -06:00
* Add -ARRAY, the classic command-line array, as AutoCAD lays it out Rectangular arrays move the copy in row r and column c by (c * column distance, r * row distance), asking for a distance only when there is more than one row or column; polar arrays space n items 360/n apart around a full circle and fill/(n - 1) otherwise, turning the copies or moving them with the group 10 point of the object selected last. The copies are separate objects; the type asked defaults to the one used last. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * -ARRAY: refuse an unknown type and oversized arrays before making transforms The JSON form errors on a type other than rectangular or polar, and checks MAXARRAY before the copies' transforms are made; the command line bounds the columns by the rows (CodeRabbit). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
172 lines
7.9 KiB
Rust
172 lines
7.9 KiB
Rust
use super::{Bounds2, Drawing, Entity, Handle, Input, PendingWindow, Result, Session, Space, Vec2, cmd, select};
|
|
|
|
impl Session {
|
|
// ---------------- picking without a command ----------------
|
|
|
|
/// Open a selection window with one corner at `corner`, for press-and-drag selection
|
|
/// (AutoCAD's PICKDRAG = 2: a drag opens the window wherever it starts, even over an object;
|
|
/// the next point, where the drag ends, closes it). Only while objects are being selected: no
|
|
/// command running, or a command asking for objects. Returns whether a window was opened.
|
|
pub fn begin_window(&mut self, corner: Vec2) -> bool {
|
|
let during_command = self.running.is_some();
|
|
if !self.settings.pickdrag || !corner.is_finite() || (during_command && !self.current_prompt().is_some_and(|p| p.accept.select)) {
|
|
return false;
|
|
}
|
|
self.pending_window = Some(PendingWindow { corner, during_command });
|
|
true
|
|
}
|
|
|
|
/// A click with no command running: pick/toggle objects or start a selection window.
|
|
///
|
|
/// PICKADD on (the default): picks add to the selection and Shift removes. PICKADD off: each pick
|
|
/// replaces the selection and Shift adds (Shift+pick on a selected object removes it).
|
|
pub fn idle_click(&mut self, p: Vec2, shift: bool) -> Result<()> {
|
|
let space = self.space();
|
|
let pickadd = self.settings.pickadd;
|
|
if let Some(pw) = self.pending_window.take() {
|
|
let crossing = p.x < pw.corner.x;
|
|
let hs = select::select_window(self.doc()?, &space, Bounds2::new(pw.corner, p), crossing);
|
|
let hs = cmd::groups::expand(self, hs);
|
|
let mut sel = if pickadd || shift { self.selection() } else { Vec::new() };
|
|
if shift && pickadd {
|
|
sel.retain(|h| !hs.contains(h));
|
|
} else {
|
|
sel.extend(hs);
|
|
}
|
|
self.set_selection(sel);
|
|
return Ok(());
|
|
}
|
|
let ap = self.pixel_size() * self.settings.pickbox.max(1.0) * 1.5;
|
|
match select::pick(self.doc()?, &space, p, ap) {
|
|
Some(h) => self.idle_pick(h, shift),
|
|
None => self.pending_window = Some(PendingWindow { corner: p, during_command: false }),
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// An idle click that picked `h`, with [`Self::idle_click`]'s PICKADD and Shift rules.
|
|
fn idle_pick(&mut self, h: Handle, shift: bool) {
|
|
let pickadd = self.settings.pickadd;
|
|
// With group selection the object's selectable groups come (or go) with it.
|
|
let hs = cmd::groups::expand(self, vec![h]);
|
|
let mut sel = if pickadd || shift { self.selection() } else { Vec::new() };
|
|
let had = sel.contains(&h);
|
|
if shift && (pickadd || had) {
|
|
sel.retain(|x| !hs.contains(x));
|
|
} else if !had {
|
|
sel.extend(hs);
|
|
}
|
|
self.set_selection(sel);
|
|
}
|
|
|
|
/// Whether a click at `p` selects objects by picking: no command running, or a command at
|
|
/// "Select objects" with no selection window or typed mode under way.
|
|
pub fn click_picks(&self) -> bool {
|
|
self.pending_window.is_none()
|
|
&& match self.running.as_ref() {
|
|
None => self.lisp_prompt().is_none(),
|
|
Some(r) => self.select_mode.is_none() && r.machine.prompt(self).accept.select,
|
|
}
|
|
}
|
|
|
|
/// The objects under the pick box at `p` when selection cycling is on and more than one is
|
|
/// there: what a click at `p` would choose between.
|
|
fn cycling_hits(&self, p: Vec2) -> Vec<Handle> {
|
|
if !self.settings.selection_cycling {
|
|
return Vec::new();
|
|
}
|
|
let hs = self.pick_candidates(p);
|
|
if hs.len() > 1 { hs } else { Vec::new() }
|
|
}
|
|
|
|
/// Every object under the pick box at `p`, nearest first, when a click there picks objects
|
|
/// (empty otherwise): what Shift+Space steps the highlight through.
|
|
pub fn pick_candidates(&self, p: Vec2) -> Vec<Handle> {
|
|
if !self.click_picks() {
|
|
return Vec::new();
|
|
}
|
|
let ap = self.pixel_size() * self.settings.pickbox.max(1.0) * 1.5;
|
|
self.doc().map(|d| select::pick_all(d, &self.space(), p, ap)).unwrap_or_default()
|
|
}
|
|
|
|
/// Whether selection cycling marks `p` with its badge (SELECTIONCYCLING 1 or 2): overlapping
|
|
/// objects a click there would choose between.
|
|
pub fn cycling_badge(&self, p: Vec2) -> bool {
|
|
self.settings.selection_cycling_display >= 1 && !self.cycling_hits(p).is_empty()
|
|
}
|
|
|
|
/// The objects a click at `p` lists to choose from (SELECTIONCYCLING 2), nearest first; empty
|
|
/// when the click should pick as usual.
|
|
pub fn cycling_candidates(&self, p: Vec2) -> Vec<Handle> {
|
|
if self.settings.selection_cycling_display < 2 {
|
|
return Vec::new();
|
|
}
|
|
self.cycling_hits(p)
|
|
}
|
|
|
|
/// Pick `h` from the selection cycling list, as a click on it alone would have: into the
|
|
/// command at "Select objects", else into the selection (Shift as for [`Self::idle_click`]).
|
|
pub fn pick_cycled(&mut self, h: Handle, shift: bool) -> Result<()> {
|
|
// The list may be older than an undo or an erase.
|
|
if self.doc()?.entity(h).is_none() {
|
|
return Ok(());
|
|
}
|
|
if self.running.is_some() {
|
|
let hs = cmd::groups::expand(self, vec![h]);
|
|
return self.input(Input::Pick(hs));
|
|
}
|
|
self.idle_pick(h, shift);
|
|
Ok(())
|
|
}
|
|
|
|
/// Remember the selection used by a command as "Previous".
|
|
pub fn remember_selection(&mut self, sel: &[Handle]) {
|
|
if let Ok(st) = self.state_mut() {
|
|
st.previous_selection = sel.to_vec();
|
|
}
|
|
}
|
|
|
|
// ---------------- one object by name ----------------
|
|
|
|
/// Give the running command an object by name, as AutoLISP's `command` does with an entity
|
|
/// name or an `entsel` list (name and point). A prompt picking one object with a point
|
|
/// (BREAK's "Select object", MEASURE's "Select object to measure"…) takes the point, and a
|
|
/// pick there finds that object ([`Session::pick_object`]) wherever it lies. A bare name
|
|
/// stands at the object's DXF group 10 point (a line's start, a circle's centre, a
|
|
/// polyline's first vertex), the point AutoCAD picks it at. The name holds for that input
|
|
/// only: a later pick at the same point finds what lies there. Other prompts take the object
|
|
/// as a pick.
|
|
pub fn input_object(&mut self, h: Handle, at: Option<Vec2>) -> Result<()> {
|
|
let one = self.current_prompt().is_some_and(|p| p.accept.point && !p.accept.select && p.picks_objects());
|
|
let p = if one { at.or_else(|| self.doc().ok().and_then(|d| group_10(d, d.entity(h)?))) } else { None };
|
|
let Some(p) = p.filter(|p| p.is_finite()) else { return self.input(Input::Pick(vec![h])) };
|
|
self.pick_hint = Some((h, p));
|
|
let r = self.input(Input::Point(p));
|
|
self.pick_hint = None;
|
|
r
|
|
}
|
|
|
|
/// The object a prompt picking one object finds at `p` in `space`: the one named for that
|
|
/// point ([`Session::input_object`]), which is none when it lies in another space, else the
|
|
/// object under `p` within `aperture`.
|
|
pub fn pick_object(&self, space: &Space, p: Vec2, aperture: f64) -> Result<Option<Handle>> {
|
|
let d = self.doc()?;
|
|
if let Some((h, q)) = self.pick_hint
|
|
&& q == p
|
|
{
|
|
return Ok((d.space_of(h).as_ref() == Some(space)).then_some(h));
|
|
}
|
|
Ok(select::pick(d, space, p, aperture))
|
|
}
|
|
}
|
|
|
|
/// An object's DXF group 10 point: the first point of its entity list, or its first grip where
|
|
/// entity lists are not built in.
|
|
pub(crate) fn group_10(d: &Drawing, e: &Entity) -> Option<Vec2> {
|
|
let Some(codec) = cmd::file::entity_codec() else { return e.kind.grips().first().copied() };
|
|
let (_, tags) = (codec.groups)(d, e);
|
|
let i = tags.iter().position(|t| t.code == 10)?;
|
|
let y = tags.get(i + 1).filter(|t| t.code == 20)?;
|
|
Some(Vec2::new(tags[i].value.as_f64(), y.value.as_f64()))
|
|
}
|