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:
ramox81 committed 2026-09-24 19:38:48 +03:00
commit c1ecb0edaf
77 files changed
+5695 -606

No files matched your search

+1
View File
@@ -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};
+106
View File
@@ -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());
}
}
File diff suppressed because it is too large. Load diff
+237
View File
@@ -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"
));
}
}