forked from 1b-agent/opencadstudio-upstream
fix(script): support AutoCAD .scr scripts with multi-line, single-line, and prefix handling
This commit is contained in:
1 parent
ab0682153d
commit
f32d0422d2
6 files changed
+222
-34
No files matched your search
@@ -1097,4 +1097,62 @@ impl OpenCADStudio {
|
|||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Feed one line from a command script (.scr) into the command pipeline.
|
||||||
|
///
|
||||||
|
/// If an interactive command is active, the line (or its tokens) is routed to
|
||||||
|
/// the active command's prompts (e.g. coordinates or options). If no command
|
||||||
|
/// is active, the line starts a new command (supporting both bare verbs and
|
||||||
|
/// inline single-line commands).
|
||||||
|
pub(in crate::app) fn feed_script_line(&mut self, line: &str) -> Task<Message> {
|
||||||
|
let i = self.active_tab;
|
||||||
|
let trimmed = line.trim();
|
||||||
|
|
||||||
|
// 1. Text editor currently open (e.g. TEXT command content step).
|
||||||
|
if self.text_inline.is_some() {
|
||||||
|
if let Some(editor) = self.text_inline.as_mut() {
|
||||||
|
editor.value = line.to_string();
|
||||||
|
}
|
||||||
|
let committed = self.text_inline_commit();
|
||||||
|
let task = self.post_editor_closed(committed);
|
||||||
|
return if committed {
|
||||||
|
Task::batch([task, Task::done(Message::CommandEscape)])
|
||||||
|
} else {
|
||||||
|
task
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Blank line submits Enter to the active command (or repeats last).
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
return self.feed_command(StepInput::Enter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. A command is currently active: route to its prompt handlers.
|
||||||
|
if self.tabs[i].active_cmd.is_some() {
|
||||||
|
if self.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_ref()
|
||||||
|
.map(|c| c.input_kind().is_free_text())
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
return self.feed_command(StepInput::Text(trimmed.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let tokens: Vec<&str> = trimmed.split_whitespace().collect();
|
||||||
|
let mut tasks = Vec::new();
|
||||||
|
for tok in tokens {
|
||||||
|
if self.tabs[i].active_cmd.is_none() {
|
||||||
|
// Preceding token finished the active command; remaining tokens start a new one.
|
||||||
|
tasks.push(self.run_command_line(tok));
|
||||||
|
} else {
|
||||||
|
tasks.push(self.feed_active_cmd(tok));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Task::batch(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. No command is active: run as top-level command.
|
||||||
|
self.run_command_line(trimmed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+137
-22
@@ -339,33 +339,55 @@ impl OpenCADStudio {
|
|||||||
// line is fed through the same command path the `--script` startup
|
// line is fed through the same command path the `--script` startup
|
||||||
// flag uses. Blank rows submit Enter to the active command.
|
// flag uses. Blank rows submit Enter to the active command.
|
||||||
"SCRIPT" | "SCR" => {
|
"SCRIPT" | "SCR" => {
|
||||||
use crate::command::ValuePromptCommand;
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let c = ValuePromptCommand::new("SCRIPT", "SCRIPT path to the .scr file:");
|
{
|
||||||
self.command_line.push_info(&c.prompt());
|
return Some(Task::perform(
|
||||||
self.tabs[i].active_cmd = Some(Box::new(c));
|
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 ") => {
|
cmd if cmd.starts_with("SCRIPT ") || cmd.starts_with("SCR ") => {
|
||||||
let path = cmd.split_once(' ').map(|(_, r)| r.trim().to_string());
|
let path = cmd.split_once(' ').map(|(_, r)| r.trim().to_string());
|
||||||
match path {
|
match path {
|
||||||
Some(p) if !p.is_empty() => match std::fs::read_to_string(&p) {
|
Some(p) if !p.is_empty() => {
|
||||||
Ok(text) => {
|
let clean_path = p.trim().trim_matches(|c| c == '"' || c == '\'');
|
||||||
let cmds: Vec<Task<Message>> = text
|
match std::fs::read_to_string(clean_path) {
|
||||||
.lines()
|
Ok(text) => {
|
||||||
.map(str::trim)
|
let cmds: Vec<Task<Message>> = text
|
||||||
.filter(|l| !l.starts_with('#') && !l.starts_with(';'))
|
.lines()
|
||||||
.map(|l| Task::done(Message::ScriptLine(l.to_string())))
|
.map(str::trim)
|
||||||
.collect();
|
.filter(|l| !l.starts_with('#') && !l.starts_with(';'))
|
||||||
self.command_line.push_output(crate::tf!(
|
.map(|l| Task::done(Message::ScriptLine(l.to_string())))
|
||||||
"SCRIPT: running {} command(s) from {p}.",
|
.collect();
|
||||||
cmds.len()
|
self.command_line.push_output(crate::tf!(
|
||||||
).as_ref());
|
"SCRIPT: running {} command(s) from {clean_path}.",
|
||||||
return Some(Task::batch(cmds));
|
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
|
self.command_line
|
||||||
.push_info(crate::t!("Usage: SCRIPT <path to .scr file>").as_ref());
|
.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
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -113,6 +113,11 @@ impl OpenCADStudio {
|
|||||||
Some(rest) => (rest.trim(), true),
|
Some(rest) => (rest.trim(), true),
|
||||||
None => (cmd, false),
|
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 resolved = self.resolve_alias(cmd);
|
||||||
let cmd = resolved.as_deref().unwrap_or(cmd);
|
let cmd = resolved.as_deref().unwrap_or(cmd);
|
||||||
if is_spacemouse_command(cmd) {
|
if is_spacemouse_command(cmd) {
|
||||||
|
|||||||
+11
-4
@@ -1294,6 +1294,7 @@ pub(super) struct OpenCADStudio {
|
|||||||
/// drawings in a file manager produces exactly that (one process per file,
|
/// drawings in a file manager produces exactly that (one process per file,
|
||||||
/// all arriving at once), which makes this queue load-bearing, not polish.
|
/// all arriving at once), which makes this queue load-bearing, not polish.
|
||||||
pub(super) pending_opens: std::collections::VecDeque<PathBuf>,
|
pub(super) pending_opens: std::collections::VecDeque<PathBuf>,
|
||||||
|
pub(super) pending_startup_script_lines: Vec<String>,
|
||||||
/// One global interaction-index build at a time. Large drawings can each
|
/// One global interaction-index build at a time. Large drawings can each
|
||||||
/// hold millions of entries, so file-open bursts must not multiply peak
|
/// hold millions of entries, so file-open bursts must not multiply peak
|
||||||
/// CPU and memory by the number of tabs.
|
/// CPU and memory by the number of tabs.
|
||||||
@@ -4278,6 +4279,7 @@ impl OpenCADStudio {
|
|||||||
missing_fonts_downloading: false,
|
missing_fonts_downloading: false,
|
||||||
suppressed_missing_fonts: rustc_hash::FxHashSet::default(),
|
suppressed_missing_fonts: rustc_hash::FxHashSet::default(),
|
||||||
pending_opens: std::collections::VecDeque::new(),
|
pending_opens: std::collections::VecDeque::new(),
|
||||||
|
pending_startup_script_lines: Vec::new(),
|
||||||
active_interaction_index: None,
|
active_interaction_index: None,
|
||||||
queued_interaction_indices: std::collections::VecDeque::new(),
|
queued_interaction_indices: std::collections::VecDeque::new(),
|
||||||
pending_close: None,
|
pending_close: None,
|
||||||
@@ -4624,21 +4626,26 @@ impl OpenCADStudio {
|
|||||||
s.command_line.push_warning(¬ice);
|
s.command_line.push_warning(¬ice);
|
||||||
crate::scene::pipeline::report_gpu_line(&format!("[gpu] {notice}"));
|
crate::scene::pipeline::report_gpu_line(&format!("[gpu] {notice}"));
|
||||||
}
|
}
|
||||||
let cli_open: Task<Message> = if !cfg.files.is_empty() {
|
let has_files = !cfg.files.is_empty();
|
||||||
|
let cli_open: Task<Message> = if has_files {
|
||||||
Task::batch(
|
Task::batch(
|
||||||
cfg.files
|
cfg.files
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|p| Task::done(Message::OpenExternal(p))),
|
.map(|p| Task::done(Message::OpenExternal(p))),
|
||||||
)
|
)
|
||||||
} else if cfg.new {
|
} else if cfg.new || !cfg.script_lines.is_empty() {
|
||||||
Task::done(Message::TabNew)
|
Task::done(Message::TabNew)
|
||||||
} else {
|
} else {
|
||||||
Task::none()
|
Task::none()
|
||||||
};
|
};
|
||||||
// Startup command script: each line dispatched as if typed at the
|
// Startup command script: if files were passed to open, defer the
|
||||||
// command line, in order, after any file open is requested.
|
// script lines until the document finishes opening. Otherwise, dispatch
|
||||||
|
// them on the newly created drawing tab.
|
||||||
let script: Task<Message> = if cfg.script_lines.is_empty() {
|
let script: Task<Message> = if cfg.script_lines.is_empty() {
|
||||||
Task::none()
|
Task::none()
|
||||||
|
} else if has_files {
|
||||||
|
s.pending_startup_script_lines = cfg.script_lines;
|
||||||
|
Task::none()
|
||||||
} else {
|
} else {
|
||||||
Task::batch(
|
Task::batch(
|
||||||
cfg.script_lines
|
cfg.script_lines
|
||||||
|
|||||||
@@ -1976,7 +1976,13 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
self.drain_pending_open()
|
self.drain_pending_open()
|
||||||
};
|
};
|
||||||
Task::batch([thumbs_task, pending_open_task, interaction_task])
|
let startup_script_task = if !self.pending_startup_script_lines.is_empty() {
|
||||||
|
let lines = std::mem::take(&mut self.pending_startup_script_lines);
|
||||||
|
Task::batch(lines.into_iter().map(|l| Task::done(Message::ScriptLine(l))))
|
||||||
|
} else {
|
||||||
|
Task::none()
|
||||||
|
};
|
||||||
|
Task::batch([thumbs_task, pending_open_task, interaction_task, startup_script_task])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn on_wblock_save_result_some(
|
pub(super) fn on_wblock_save_result_some(
|
||||||
|
|||||||
@@ -982,6 +982,9 @@ impl OpenCADStudio {
|
|||||||
.extension()
|
.extension()
|
||||||
.map(|e| e.to_string_lossy().to_lowercase())
|
.map(|e| e.to_string_lossy().to_lowercase())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
if ext == "scr" {
|
||||||
|
return self.dispatch_command(&format!("SCRIPT \"{}\"", path.display()));
|
||||||
|
}
|
||||||
if !matches!(ext.as_str(), "dwg" | "dxf" | "bak" | "sv$") {
|
if !matches!(ext.as_str(), "dwg" | "dxf" | "bak" | "sv$") {
|
||||||
self.command_line.push_error(
|
self.command_line.push_error(
|
||||||
crate::tf!("Unsupported file type: {}", path.display()).as_ref(),
|
crate::tf!("Unsupported file type: {}", path.display()).as_ref(),
|
||||||
@@ -2506,13 +2509,7 @@ impl OpenCADStudio {
|
|||||||
self.dispatch_command(&cmd)
|
self.dispatch_command(&cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::ScriptLine(line) => {
|
Message::ScriptLine(line) => self.feed_script_line(&line),
|
||||||
if line.trim().is_empty() {
|
|
||||||
self.feed_command(crate::command::StepInput::Enter)
|
|
||||||
} else {
|
|
||||||
self.run_command_line(&line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Message::ToggleLayers => {
|
Message::ToggleLayers => {
|
||||||
if self.active_modal == Some(super::ModalKind::Layers) {
|
if self.active_modal == Some(super::ModalKind::Layers) {
|
||||||
|
|||||||
Reference in new issue
Block a user