forked from 1b-agent/opencadstudio-upstream
Merge remote-tracking branch 'origin/main' into codex/pdf-attach-parity
# Conflicts: # locales/ar-SA/opencadstudio.ftl # locales/bg-BG/opencadstudio.ftl # locales/cs-CZ/opencadstudio.ftl # locales/de-DE/opencadstudio.ftl # locales/el-GR/opencadstudio.ftl # locales/en-US/opencadstudio.ftl # locales/es-ES/opencadstudio.ftl # locales/fi-FI/opencadstudio.ftl # locales/fr-FR/opencadstudio.ftl # locales/hi-IN/opencadstudio.ftl # locales/hu-HU/opencadstudio.ftl # locales/it-IT/opencadstudio.ftl # locales/ja-JP/opencadstudio.ftl # locales/ko-KR/opencadstudio.ftl # locales/nl-NL/opencadstudio.ftl # locales/pl-PL/opencadstudio.ftl # locales/pt-BR/opencadstudio.ftl # locales/ru-RU/opencadstudio.ftl # locales/tr-TR/opencadstudio.ftl # locales/zh-CN/opencadstudio.ftl # locales/zh-TW/opencadstudio.ftl # src/app/control/actions.rs # src/app/mod.rs # src/app/update/mod.rs # src/app/view/modal.rs # src/io/xref.rs # src/ui/window/mod.rs
This commit is contained in:
commit
c1ecb0edaf
77 files changed
+5695
-606
No files matched your search
@@ -25,6 +25,7 @@ mod underlay_layers;
|
||||
pub(crate) mod wblock;
|
||||
mod xadjust;
|
||||
pub(crate) mod xattach;
|
||||
pub(crate) mod xref_cmd;
|
||||
mod xclip;
|
||||
|
||||
use crate::modules::{CadModule, IconKind, RibbonGroup, RibbonItem};
|
||||
|
||||
@@ -157,3 +157,109 @@ pub fn normalize_to_origin(out: &mut CadDocument) {
|
||||
crate::scene::view::dispatch::apply_transform(entity, &shift);
|
||||
}
|
||||
}
|
||||
|
||||
/// One-shot point picker for the Write Block dialog's "Pick point" button.
|
||||
pub struct WblockPickBasePointCommand;
|
||||
|
||||
impl crate::command::CadCommand for WblockPickBasePointCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"WBLOCK"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
crate::t!("WBLOCK Specify insertion base point:").into_owned()
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: glam::DVec3) -> crate::command::CmdResult {
|
||||
crate::command::CmdResult::Dispatch(format!("WBLOCK_POINT_PICKED {} {} {}", pt.x, pt.y, pt.z))
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> crate::command::CmdResult {
|
||||
crate::command::CmdResult::Dispatch("WBLOCK_POINT_CANCELLED".to_string())
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> crate::command::CmdResult {
|
||||
crate::command::CmdResult::Dispatch("WBLOCK_POINT_CANCELLED".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a standalone `CadDocument` from an explicit list of entity handles,
|
||||
/// applying `base_point` translation and `unit` insertion units.
|
||||
pub fn extract_entities_to_doc_with_base(
|
||||
src: &CadDocument,
|
||||
handles: &[acadrust::Handle],
|
||||
base_point: glam::DVec3,
|
||||
unit: i16,
|
||||
) -> Result<CadDocument, String> {
|
||||
let mut out = CadDocument::new();
|
||||
out.header.insertion_units = unit;
|
||||
extract_entities_into(src, handles, &mut out)?;
|
||||
if base_point != glam::DVec3::ZERO {
|
||||
let shift = crate::command::EntityTransform::Affine(
|
||||
acadrust::types::Transform::from_translation(acadrust::types::Vector3::new(
|
||||
-base_point.x,
|
||||
-base_point.y,
|
||||
-base_point.z,
|
||||
)),
|
||||
);
|
||||
for entity in out.entities_mut() {
|
||||
crate::scene::view::dispatch::apply_transform(entity, &shift);
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use acadrust::entities::Line;
|
||||
use acadrust::types::Vector3;
|
||||
|
||||
#[test]
|
||||
fn test_extract_entities_with_base_point_and_units() {
|
||||
let mut doc = CadDocument::new();
|
||||
let mut line = Line::new();
|
||||
line.start = Vector3::new(10.0, 20.0, 0.0);
|
||||
line.end = Vector3::new(30.0, 40.0, 0.0);
|
||||
let h = doc.add_entity(EntityType::Line(line)).unwrap();
|
||||
|
||||
let out = extract_entities_to_doc_with_base(
|
||||
&doc,
|
||||
&[h],
|
||||
glam::DVec3::new(10.0, 20.0, 0.0),
|
||||
4, // Millimeters
|
||||
)
|
||||
.expect("extract should succeed");
|
||||
|
||||
assert_eq!(out.header.insertion_units, 4);
|
||||
assert_eq!(out.entities().count(), 1);
|
||||
let e = out.entities().next().unwrap();
|
||||
if let EntityType::Line(l) = e {
|
||||
assert!((l.start.x - 0.0).abs() < 1e-6);
|
||||
assert!((l.start.y - 0.0).abs() < 1e-6);
|
||||
assert!((l.end.x - 20.0).abs() < 1e-6);
|
||||
assert!((l.end.y - 20.0).abs() < 1e-6);
|
||||
} else {
|
||||
panic!("Expected Line entity");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_block_to_doc() {
|
||||
let mut doc = CadDocument::new();
|
||||
let mut line = Line::new();
|
||||
line.start = Vector3::new(1.0, 2.0, 0.0);
|
||||
line.end = Vector3::new(3.0, 4.0, 0.0);
|
||||
let h = doc.add_entity(EntityType::Line(line)).unwrap();
|
||||
|
||||
let mut block_record = acadrust::tables::BlockRecord::new("MY_BLOCK".to_string());
|
||||
block_record.entity_handles.push(h);
|
||||
let _ = doc.block_records.add(block_record);
|
||||
|
||||
let out = extract_block_to_doc(&doc, "MY_BLOCK").expect("block extract should succeed");
|
||||
assert_eq!(out.entities().count(), 1);
|
||||
|
||||
assert!(extract_block_to_doc(&doc, "NON_EXISTENT").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
+592
-251
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,237 @@
|
||||
// -XREF — the command-line form of the reference manager.
|
||||
//
|
||||
// Enter an option [?/Bind/Detach/Path/pathType/Unload/Reload/Overlay/Attach/Show] <Attach>:
|
||||
//
|
||||
// Each option collects its answers with the reference prompts and hands them
|
||||
// to the existing reference operations through `CmdResult::Dispatch`.
|
||||
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum Step {
|
||||
Option,
|
||||
List,
|
||||
Names(&'static str),
|
||||
File { overlay: bool },
|
||||
PathNames,
|
||||
NewPath { name: String },
|
||||
PathTypeNames,
|
||||
PathTypeKind { names: String },
|
||||
ShowNames,
|
||||
}
|
||||
|
||||
pub struct XrefCommand {
|
||||
step: Step,
|
||||
/// Saved paths by reference name, for the Path option's "Old path:" line.
|
||||
saved_paths: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl XrefCommand {
|
||||
pub fn new(saved_paths: Vec<(String, String)>) -> Self {
|
||||
Self {
|
||||
step: Step::Option,
|
||||
saved_paths,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether any reference name matches `pattern` (`*` / `?` wildcards).
|
||||
fn matches(&self, pattern: &str) -> bool {
|
||||
self.saved_paths
|
||||
.iter()
|
||||
.any(|(name, _)| crate::io::xref_model::wildcard_match(name, pattern))
|
||||
}
|
||||
|
||||
fn choose(&mut self, token: &str) -> CmdResult {
|
||||
self.step = match token {
|
||||
"?" => Step::List,
|
||||
"B" | "BIND" => Step::Names("Bind"),
|
||||
"D" | "DETACH" => Step::Names("Detach"),
|
||||
"P" | "PATH" => Step::PathNames,
|
||||
"T" | "PATHTYPE" => Step::PathTypeNames,
|
||||
"U" | "UNLOAD" => Step::Names("Unload"),
|
||||
"R" | "RELOAD" => Step::Names("Reload"),
|
||||
"O" | "OVERLAY" => Step::File { overlay: true },
|
||||
"A" | "ATTACH" => Step::File { overlay: false },
|
||||
"S" | "SHOW" => Step::ShowNames,
|
||||
_ => {
|
||||
return CmdResult::ReportError("Invalid option keyword.".to_string());
|
||||
}
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for XrefCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"-XREF"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
let text = match &self.step {
|
||||
Step::Option => "Enter an option [?/Bind/Detach/Path/pathType/Unload/Reload/Overlay/Attach/Show] <Attach>:",
|
||||
Step::List => "Enter xref name(s) to list <*>:",
|
||||
Step::Names("Bind") => "Enter xref name(s) to bind:",
|
||||
Step::Names("Detach") => "Enter xref name(s) to detach:",
|
||||
Step::Names("Unload") => "Enter xref name(s) to unload:",
|
||||
Step::Names(_) => "Enter xref name(s) to reload:",
|
||||
Step::File { overlay: false } => "Enter name of file to attach:",
|
||||
Step::File { overlay: true } => "Enter name of file to overlay:",
|
||||
Step::PathNames => "Edit xref name(s) to edit path:",
|
||||
Step::NewPath { .. } => "Enter new path:",
|
||||
Step::PathTypeNames => "Enter xref name(s) to edit path type:",
|
||||
Step::PathTypeKind { .. } => "Enter new path type [Full/Relative/None]:",
|
||||
Step::ShowNames => "Enter xref name(s) to show:",
|
||||
};
|
||||
format!("-XREF {text}")
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
match self.step {
|
||||
Step::Option => vec![
|
||||
CmdOption::new("?", "?"),
|
||||
CmdOption::new("Bind", "B"),
|
||||
CmdOption::new("Detach", "D"),
|
||||
CmdOption::new("Path", "P"),
|
||||
CmdOption::new("pathType", "T"),
|
||||
CmdOption::new("Unload", "U"),
|
||||
CmdOption::new("Reload", "R"),
|
||||
CmdOption::new("Overlay", "O"),
|
||||
CmdOption::new("Attach", "A"),
|
||||
CmdOption::new("Show", "S"),
|
||||
],
|
||||
Step::PathTypeKind { .. } => vec![
|
||||
CmdOption::new("Full", "F"),
|
||||
CmdOption::new("Relative", "R"),
|
||||
CmdOption::new("None", "N"),
|
||||
],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn input_kind(&self) -> InputKind {
|
||||
match self.step {
|
||||
// Paths and names can hold spaces.
|
||||
Step::File { .. } | Step::NewPath { .. } => InputKind::FreeText,
|
||||
_ => InputKind::SingleToken,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let value = text.trim().trim_matches('"').to_string();
|
||||
let token = value.to_uppercase();
|
||||
let names_step = matches!(
|
||||
self.step,
|
||||
Step::Names(_) | Step::PathTypeNames | Step::ShowNames
|
||||
);
|
||||
if names_step && !self.matches(&value) {
|
||||
return Some(CmdResult::Measurement(
|
||||
"No matching xref names found.".to_string(),
|
||||
));
|
||||
}
|
||||
Some(match self.step.clone() {
|
||||
Step::Option => self.choose(&token),
|
||||
Step::List => CmdResult::Dispatch(format!("XREFLIST {value}")),
|
||||
Step::Names(op) => CmdResult::Dispatch(format!("XREF {op} {value}")),
|
||||
Step::File { overlay } => {
|
||||
let verb = if overlay { "XREFOVERLAYFILE" } else { "XREFATTACHFILE" };
|
||||
CmdResult::Dispatch(format!("{verb} {value}"))
|
||||
}
|
||||
Step::PathNames => {
|
||||
let Some((name, old)) = self
|
||||
.saved_paths
|
||||
.iter()
|
||||
.find(|(name, _)| crate::io::xref_model::wildcard_match(name, &value))
|
||||
.cloned()
|
||||
else {
|
||||
return Some(CmdResult::Measurement(
|
||||
"No matching xref names found.".to_string(),
|
||||
));
|
||||
};
|
||||
self.step = Step::NewPath { name: name.clone() };
|
||||
CmdResult::ReportMeasurement(format!(
|
||||
"{}\n{}",
|
||||
format!("xref name: \"{}\"", name),
|
||||
format!("Old path: {}", display_path(&old))
|
||||
))
|
||||
}
|
||||
Step::NewPath { name } => CmdResult::Dispatch(format!("XREF Path {name} {value}")),
|
||||
Step::PathTypeNames => {
|
||||
self.step = Step::PathTypeKind { names: value };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::PathTypeKind { names } => {
|
||||
let kind = match token.as_str() {
|
||||
"F" | "FULL" => "Full",
|
||||
"R" | "RELATIVE" => "Relative",
|
||||
"N" | "NONE" => "None",
|
||||
_ => {
|
||||
return Some(CmdResult::ReportError(
|
||||
"Invalid option keyword.".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
CmdResult::Dispatch(format!("XREF Pathtype {kind} {names}"))
|
||||
}
|
||||
Step::ShowNames => CmdResult::Dispatch("XREFSHOW".to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
match self.step.clone() {
|
||||
Step::Option => self.choose("A"),
|
||||
Step::List => CmdResult::Dispatch("XREFLIST *".to_string()),
|
||||
Step::NewPath { .. } => {
|
||||
CmdResult::Measurement("Path unchanged.".to_string())
|
||||
}
|
||||
Step::PathTypeKind { .. } => CmdResult::NeedPoint,
|
||||
_ => CmdResult::Cancel,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A stored path shown the way the platform writes it.
|
||||
pub fn display_path(path: &str) -> String {
|
||||
if cfg!(windows) {
|
||||
path.replace('/', "\\")
|
||||
} else {
|
||||
path.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::XrefCommand;
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
|
||||
#[test]
|
||||
fn enter_chooses_attach() {
|
||||
let mut cmd = XrefCommand::new(Vec::new());
|
||||
assert!(matches!(cmd.on_enter(), CmdResult::NeedPoint));
|
||||
assert!(cmd.prompt().contains("Enter name of file to attach:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_defaults_to_every_reference() {
|
||||
let mut cmd = XrefCommand::new(Vec::new());
|
||||
cmd.on_text_input("?");
|
||||
assert!(cmd.prompt().contains("Enter xref name(s) to list <*>:"));
|
||||
assert!(matches!(cmd.on_enter(), CmdResult::Dispatch(ref s) if s == "XREFLIST *"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlay_file_is_dispatched() {
|
||||
let mut cmd = XrefCommand::new(Vec::new());
|
||||
cmd.on_text_input("O");
|
||||
assert!(cmd.prompt().contains("Enter name of file to overlay:"));
|
||||
assert!(matches!(
|
||||
cmd.on_text_input("C:/refs/site plan.dwg"),
|
||||
Some(CmdResult::Dispatch(ref s)) if s == "XREFOVERLAYFILE C:/refs/site plan.dwg"
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user