fix(script): support AutoCAD .scr scripts with multi-line, single-line, and prefix handling

This commit is contained in:
yztink committed 2026-10-01 23:34:15 -07:00
1 parent ab0682153d
commit f32d0422d2
6 files changed
+222 -34

No files matched your search

+137 -22
View File
@@ -339,33 +339,55 @@ impl OpenCADStudio {
// line is fed through the same command path the `--script` startup
// flag uses. Blank rows submit Enter to the active command.
"SCRIPT" | "SCR" => {
use crate::command::ValuePromptCommand;
let c = ValuePromptCommand::new("SCRIPT", "SCRIPT path to the .scr file:");
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
#[cfg(not(target_arch = "wasm32"))]
{
return Some(Task::perform(
async move {
crate::sys::file_dialog()
.add_filter("Script files (*.scr)", &["scr"])
.pick_file()
.await
.map(|h| crate::sys::handle_path(&h))
},
|path| match path {
Some(p) => Message::Command(format!("SCRIPT \"{}\"", p.display())),
None => Message::Noop,
},
));
}
#[cfg(target_arch = "wasm32")]
{
use crate::command::ValuePromptCommand;
let c = ValuePromptCommand::new("SCRIPT", "SCRIPT path to the .scr file:");
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
}
}
cmd if cmd.starts_with("SCRIPT ") || cmd.starts_with("SCR ") => {
let path = cmd.split_once(' ').map(|(_, r)| r.trim().to_string());
match path {
Some(p) if !p.is_empty() => match std::fs::read_to_string(&p) {
Ok(text) => {
let cmds: Vec<Task<Message>> = text
.lines()
.map(str::trim)
.filter(|l| !l.starts_with('#') && !l.starts_with(';'))
.map(|l| Task::done(Message::ScriptLine(l.to_string())))
.collect();
self.command_line.push_output(crate::tf!(
"SCRIPT: running {} command(s) from {p}.",
cmds.len()
).as_ref());
return Some(Task::batch(cmds));
Some(p) if !p.is_empty() => {
let clean_path = p.trim().trim_matches(|c| c == '"' || c == '\'');
match std::fs::read_to_string(clean_path) {
Ok(text) => {
let cmds: Vec<Task<Message>> = text
.lines()
.map(str::trim)
.filter(|l| !l.starts_with('#') && !l.starts_with(';'))
.map(|l| Task::done(Message::ScriptLine(l.to_string())))
.collect();
self.command_line.push_output(crate::tf!(
"SCRIPT: running {} command(s) from {clean_path}.",
cmds.len()
).as_ref());
return Some(Task::batch(cmds));
}
Err(e) => {
self.command_line
.push_error(crate::tf!("SCRIPT: cannot read {clean_path}: {e}").as_ref());
}
}
Err(e) => {
self.command_line
.push_error(crate::tf!("SCRIPT: cannot read {p}: {e}").as_ref());
}
},
}
_ => {
self.command_line
.push_info(crate::t!("Usage: SCRIPT <path to .scr file>").as_ref());
@@ -418,3 +440,96 @@ fn parse_background_color(args: &[&str]) -> Option<[f32; 4]> {
}
None
}
#[cfg(test)]
mod tests {
use crate::app::{Message, OpenCADStudio};
fn fresh_app() -> OpenCADStudio {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
app
}
#[test]
fn multiline_script_draws_line() {
let mut app = fresh_app();
let i = app.active_tab;
let before_count = app.tabs[i].scene.document.entities().count();
// Feed standard multi-line CAD script sequence
let _ = app.update(Message::ScriptLine("LINE".into()));
assert!(app.tabs[i].active_cmd.is_some(), "LINE command should be active");
let _ = app.update(Message::ScriptLine("0,0".into()));
assert!(app.tabs[i].active_cmd.is_some(), "LINE command should stay active after first point");
let _ = app.update(Message::ScriptLine("10,10".into()));
let _ = app.update(Message::ScriptLine("".into())); // Enter to finish
let after_count = app.tabs[i].scene.document.entities().count();
assert_eq!(after_count, before_count + 1, "Line entity should have been created");
assert!(app.tabs[i].active_cmd.is_none(), "LINE command should be finished");
}
#[test]
fn single_line_script_still_works() {
let mut app = fresh_app();
let i = app.active_tab;
let before_count = app.tabs[i].scene.document.entities().count();
let _ = app.update(Message::ScriptLine("CIRCLE 5,5 2".into()));
let after_count = app.tabs[i].scene.document.entities().count();
assert_eq!(after_count, before_count + 1, "Circle entity should have been created");
assert!(app.tabs[i].active_cmd.is_none(), "CIRCLE command should be finished");
}
#[test]
fn autodesk_command_prefixes_strip_underscore_and_dot() {
let mut app = fresh_app();
let i = app.active_tab;
let before_count = app.tabs[i].scene.document.entities().count();
let _ = app.update(Message::ScriptLine("_CIRCLE 10,10 3".into()));
let after_count = app.tabs[i].scene.document.entities().count();
assert_eq!(after_count, before_count + 1, "_CIRCLE should resolve to CIRCLE and create entity");
}
#[test]
fn test_script_scr_draws_all_entities() {
let mut app = fresh_app();
let i = app.active_tab;
const SCRIPT: &str = r#"
_LINE
0,0
100,0
100,100
0,100
C
_LINE
0,0
100,100
_LINE
0,100
100,0
CIRCLE
50,50
35
CIRCLE 50,50 15
ZOOM EXTENTS
"#;
for line in SCRIPT.lines().map(str::trim).filter(|l| !l.starts_with('#') && !l.starts_with(';')) {
let _ = app.update(Message::ScriptLine(line.to_string()));
}
// 4 square edges + 2 diagonal lines + 2 circles = 8 entities
assert_eq!(app.tabs[i].scene.document.entities().count(), 8);
}
}
+5
View File
@@ -113,6 +113,11 @@ impl OpenCADStudio {
Some(rest) => (rest.trim(), true),
None => (cmd, false),
};
let cmd = cmd.trim();
// AutoCAD command prefixes: '_' (international name), '.' (built-in name bypass)
let cmd = cmd.strip_prefix('.').unwrap_or(cmd);
let cmd = cmd.strip_prefix('_').unwrap_or(cmd);
let cmd = cmd.strip_prefix('.').unwrap_or(cmd);
let resolved = self.resolve_alias(cmd);
let cmd = resolved.as_deref().unwrap_or(cmd);
if is_spacemouse_command(cmd) {