forked from 1b-agent/opencadstudio-upstream
feat(commands): expand standard CAD command coverage
Integrate a large batch of standard CAD commands and fix several name/behaviour mismatches, with no product references in code. 3D solids (reusing the truck B-rep pipeline): - SLICE, INTERFERE, PRESSPULL, POLYSOLID, PYRAMID, SECTION - 3DROTATE, 3DMIRROR, 3DALIGN (cached-solid transforms) Annotation / text: - ARCTEXT, SPLINEFIT (Catmull-Rom to Bezier), DIMJOGGED, OBJECTSCALE (XData markers honoured by the existing geometry-gen / tessellator) Blocks / misc: - NCOPY (block-content extraction via explode_entity) - OPTIONS, SYNCPVIEWPORTS, THICKEN, plus earlier toggles, aliases, system variables, and layer/style/view commands POINTCLOUD/RECAP and UNDERLAYLAYERS/UOSNAP are intentionally left out: they require a proprietary binary / PDF-layer parser, a GPU point renderer, and real test files. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
76ef544cda
commit
c19c18c2f2
24 files changed
+4405
-216
No files matched your search
+135
-12
@@ -39,10 +39,27 @@ impl OpenCADStudio {
|
||||
self.refresh_properties();
|
||||
}
|
||||
|
||||
"QSELECT" | "QS" => {
|
||||
// QSELECT builds a selection set by object type / property; FILTER is
|
||||
// the same criteria-based selection.
|
||||
"QSELECT" | "QS" | "FILTER" | "FI" => {
|
||||
return Some(Task::done(Message::QSelectOpen));
|
||||
}
|
||||
|
||||
// CAL <expression> — evaluate an arithmetic expression
|
||||
// (+ - * /, parentheses, unary minus, decimals).
|
||||
cmd if cmd == "CAL" || cmd.starts_with("CAL ") => {
|
||||
let expr = cmd.trim_start_matches("CAL").trim();
|
||||
if expr.is_empty() {
|
||||
self.command_line
|
||||
.push_info("Usage: CAL <expression> e.g. CAL (2+3)*4");
|
||||
} else {
|
||||
match arith_eval(expr) {
|
||||
Ok(v) => self.command_line.push_output(&format!("= {v}")),
|
||||
Err(e) => self.command_line.push_error(&format!("CAL: {e}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── LIST — entity info ────────────────────────────────────────
|
||||
"LIST" | "LI" => {
|
||||
let selected: Vec<_> = self.tabs[i].scene.selected_entities();
|
||||
@@ -101,12 +118,12 @@ impl OpenCADStudio {
|
||||
.index()
|
||||
.map(|c| c.to_string())
|
||||
.unwrap_or_else(|| "ByLayer".to_string());
|
||||
let linetype =
|
||||
if common.linetype.is_empty() || common.linetype == "ByLayer" {
|
||||
"ByLayer".to_string()
|
||||
} else {
|
||||
common.linetype.clone()
|
||||
};
|
||||
let linetype = if common.linetype.is_empty() || common.linetype == "ByLayer"
|
||||
{
|
||||
"ByLayer".to_string()
|
||||
} else {
|
||||
common.linetype.clone()
|
||||
};
|
||||
let details = entity_list_details(entity);
|
||||
format!(
|
||||
"{type_name} Handle:{:X} Layer:{} Color:{} LT:{}{}",
|
||||
@@ -123,7 +140,8 @@ impl OpenCADStudio {
|
||||
})
|
||||
.collect();
|
||||
if lines.is_empty() {
|
||||
self.command_line.push_info("DBLIST: drawing has no entities.");
|
||||
self.command_line
|
||||
.push_info("DBLIST: drawing has no entities.");
|
||||
} else {
|
||||
let count = lines.len();
|
||||
for l in lines {
|
||||
@@ -668,7 +686,8 @@ impl OpenCADStudio {
|
||||
.document
|
||||
.entities()
|
||||
.filter_map(|e| {
|
||||
use crate::entities::traits::EntityTypeOps; let txt = e.text_content()?;
|
||||
use crate::entities::traits::EntityTypeOps;
|
||||
let txt = e.text_content()?;
|
||||
if txt.to_lowercase().contains(&search_lc) {
|
||||
Some(e.common().handle)
|
||||
} else {
|
||||
@@ -691,7 +710,9 @@ impl OpenCADStudio {
|
||||
self.push_undo_snapshot(i, "FIND/REPLACE");
|
||||
for h in &targets {
|
||||
if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) {
|
||||
crate::entities::traits::EntityTypeOps::replace_text(e, search, rep);
|
||||
crate::entities::traits::EntityTypeOps::replace_text(
|
||||
e, search, rep,
|
||||
);
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
@@ -710,7 +731,8 @@ impl OpenCADStudio {
|
||||
} else {
|
||||
for h in &handles {
|
||||
if let Some(e) = self.tabs[i].scene.document.get_entity(*h) {
|
||||
use crate::entities::traits::EntityTypeOps; let txt = e.text_content().unwrap_or_default();
|
||||
use crate::entities::traits::EntityTypeOps;
|
||||
let txt = e.text_content().unwrap_or_default();
|
||||
self.command_line.push_output(&format!(
|
||||
" Handle {:X}: \"{}\"",
|
||||
h.value(),
|
||||
@@ -885,7 +907,6 @@ fn build_data_extraction_csv(doc: &acadrust::CadDocument) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
|
||||
/// Return a short geometry summary for CSV ExtraInfo column.
|
||||
fn entity_extra_info(entity: &acadrust::EntityType) -> String {
|
||||
use acadrust::EntityType;
|
||||
@@ -930,3 +951,105 @@ fn csv_escape(s: &str) -> String {
|
||||
s.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
// ── CAL — arithmetic expression evaluator ──────────────────────────────────
|
||||
// A small recursive-descent parser for `+ - * /`, parentheses, unary signs and
|
||||
// decimal numbers. Self-contained (no external dependency).
|
||||
fn arith_eval(expr: &str) -> Result<f64, String> {
|
||||
let mut p = ArithParser {
|
||||
chars: expr.chars().filter(|c| !c.is_whitespace()).collect(),
|
||||
pos: 0,
|
||||
};
|
||||
let v = p.expr()?;
|
||||
if p.pos != p.chars.len() {
|
||||
return Err(format!("unexpected '{}'", p.chars[p.pos]));
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
struct ArithParser {
|
||||
chars: Vec<char>,
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl ArithParser {
|
||||
fn peek(&self) -> Option<char> {
|
||||
self.chars.get(self.pos).copied()
|
||||
}
|
||||
|
||||
// expr = term (('+' | '-') term)*
|
||||
fn expr(&mut self) -> Result<f64, String> {
|
||||
let mut v = self.term()?;
|
||||
while let Some(op) = self.peek() {
|
||||
if op == '+' || op == '-' {
|
||||
self.pos += 1;
|
||||
let rhs = self.term()?;
|
||||
v = if op == '+' { v + rhs } else { v - rhs };
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
// term = factor (('*' | '/') factor)*
|
||||
fn term(&mut self) -> Result<f64, String> {
|
||||
let mut v = self.factor()?;
|
||||
while let Some(op) = self.peek() {
|
||||
if op == '*' || op == '/' {
|
||||
self.pos += 1;
|
||||
let rhs = self.factor()?;
|
||||
if op == '/' {
|
||||
if rhs == 0.0 {
|
||||
return Err("division by zero".into());
|
||||
}
|
||||
v /= rhs;
|
||||
} else {
|
||||
v *= rhs;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
// factor = ('+' | '-') factor | '(' expr ')' | number
|
||||
fn factor(&mut self) -> Result<f64, String> {
|
||||
match self.peek() {
|
||||
Some('+') => {
|
||||
self.pos += 1;
|
||||
self.factor()
|
||||
}
|
||||
Some('-') => {
|
||||
self.pos += 1;
|
||||
Ok(-self.factor()?)
|
||||
}
|
||||
Some('(') => {
|
||||
self.pos += 1;
|
||||
let v = self.expr()?;
|
||||
if self.peek() != Some(')') {
|
||||
return Err("missing ')'".into());
|
||||
}
|
||||
self.pos += 1;
|
||||
Ok(v)
|
||||
}
|
||||
Some(c) if c.is_ascii_digit() || c == '.' => self.number(),
|
||||
Some(c) => Err(format!("unexpected '{c}'")),
|
||||
None => Err("unexpected end of expression".into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn number(&mut self) -> Result<f64, String> {
|
||||
let start = self.pos;
|
||||
while let Some(c) = self.peek() {
|
||||
if c.is_ascii_digit() || c == '.' {
|
||||
self.pos += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let s: String = self.chars[start..self.pos].iter().collect();
|
||||
s.parse::<f64>().map_err(|_| format!("bad number '{s}'"))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user