refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
impl OpenCADStudio {
|
|
|
|
|
pub(super) fn dispatch_inquiry(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
|
|
|
|
match cmd {
|
|
|
|
|
"3DORBIT" | "3O" => {
|
|
|
|
|
self.command_line
|
|
|
|
|
.push_info("3D Orbit: drag with right mouse button.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Selection utilities ───────────────────────────────────────
|
|
|
|
|
"SELECTALL" | "SA" => {
|
|
|
|
|
use crate::scene::Scene;
|
|
|
|
|
let handles: Vec<acadrust::Handle> = self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.entity_wires()
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|w| Scene::handle_from_wire_name(&w.name))
|
2026-07-01 13:10:39 +03:00
|
|
|
// Objects on a locked layer aren't selectable.
|
|
|
|
|
.filter(|&h| !self.tabs[i].scene.is_layer_locked(h))
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
.collect();
|
|
|
|
|
let count = handles.len();
|
|
|
|
|
for h in handles {
|
|
|
|
|
self.tabs[i].scene.select_entity(h, false);
|
|
|
|
|
}
|
|
|
|
|
self.command_line
|
|
|
|
|
.push_output(&format!("SELECTALL: {} object(s) selected.", count));
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
"DESELECT" | "DE" | "DESELALL" => {
|
|
|
|
|
self.tabs[i].scene.deselect_all();
|
|
|
|
|
self.command_line.push_output("Deselected.");
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
"SELECTSIMILAR" | "SELSIM" => {
|
|
|
|
|
let added = self.tabs[i].scene.select_similar();
|
|
|
|
|
self.command_line
|
|
|
|
|
.push_output(&format!("Select Similar: {} added.", added));
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-06-29 08:23:58 +03:00
|
|
|
// QSELECT builds a selection set by object type / property; FILTER is
|
|
|
|
|
// the same criteria-based selection.
|
|
|
|
|
"QSELECT" | "QS" | "FILTER" | "FI" => {
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
return Some(Task::done(Message::QSelectOpen));
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-29 09:04:18 +03:00
|
|
|
// CAL / QUICKCALC <expression> — evaluate an arithmetic expression
|
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>
2026-06-29 08:23:58 +03:00
|
|
|
// (+ - * /, parentheses, unary minus, decimals).
|
2026-06-29 09:04:18 +03:00
|
|
|
cmd if cmd == "CAL"
|
|
|
|
|
|| cmd == "QUICKCALC"
|
|
|
|
|
|| cmd == "QC"
|
|
|
|
|
|| cmd.starts_with("CAL ")
|
|
|
|
|
|| cmd.starts_with("QUICKCALC ")
|
|
|
|
|
|| cmd.starts_with("QC ") =>
|
|
|
|
|
{
|
|
|
|
|
let expr = cmd.splitn(2, char::is_whitespace).nth(1).unwrap_or("").trim();
|
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>
2026-06-29 08:23:58 +03:00
|
|
|
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}")),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
// ── LIST — entity info ────────────────────────────────────────
|
|
|
|
|
"LIST" | "LI" => {
|
|
|
|
|
let selected: Vec<_> = self.tabs[i].scene.selected_entities();
|
|
|
|
|
if selected.is_empty() {
|
|
|
|
|
self.command_line
|
|
|
|
|
.push_error("LIST: no entities selected. Select entities first.");
|
|
|
|
|
} else {
|
|
|
|
|
for (handle, _) in &selected {
|
|
|
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity(*handle) {
|
|
|
|
|
let type_name = crate::entities::names::dxf_name(entity);
|
|
|
|
|
let common = entity.common();
|
|
|
|
|
let color_str = common
|
|
|
|
|
.color
|
|
|
|
|
.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()
|
|
|
|
|
};
|
|
|
|
|
// Entity-specific details
|
|
|
|
|
let details = entity_list_details(entity);
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
"{type_name} Handle:{:X} Layer:{} Color:{} LT:{}{}",
|
|
|
|
|
handle.value(),
|
|
|
|
|
common.layer,
|
|
|
|
|
color_str,
|
|
|
|
|
linetype,
|
|
|
|
|
if details.is_empty() {
|
|
|
|
|
String::new()
|
|
|
|
|
} else {
|
|
|
|
|
format!("\n {details}")
|
|
|
|
|
}
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat(commands): integrate standard CAD commands and fix behavior gaps
Broad pass over the command set: integrate missing commands, wire
previously-unreachable ribbon/menu buttons, and correct name/behaviour
mismatches. Every UI command token now resolves (real handler where the
infrastructure exists, an honest "not yet implemented" status otherwise).
New commands:
- Draw: 3DPOLY, 2D solid (SO/SOLID2D), HELIX, TRACE, SKETCH, DIMCENTER
(DCE/CENTERMARK)
- Modify/inquiry: REVERSE, MEASUREGEOM (MEA), DBLIST
- Data/manage: BASE, OVERKILL (exact-duplicate removal), AUDIT (read-only
integrity report), SETVAR (system-variable get/set), SCRIPT (.scr runner),
FINDNONPURGEABLE
Behaviour fixes:
- LINE and SPLINE gain Close/Undo; AREA computes area/perimeter in f64
(correct at large coordinates)
- ATTEDIT/ATE unified so the alias and the ribbon button open the
interactive editor; -ATTEDIT is the command-line form
- Linear/aligned/radius/diameter dimensions gain a text override (Text) and
text angle (Angle) option
- INSERT gains Scale/Rotate options; COPY gains an Array option
- LINETYPE SET sets the current entity linetype; SETBYLAYER resets overrides
- UNDO accepts a step count
Integrations: standard dimension/block aliases (DLI/DRA/DAN, BMAKE,
EXPORTPDF, DDIM), drafting-aid and screen toggles (GRID/SNAP/POLAR/OSNAP/
DSETTINGS/CLEANSCREEN/QUICKPROPERTIES), viewport tiling
(CASCADE/HORIZONTAL/VERTICAL/VPJOIN), and customization entry points
(CUI/ALIASEDIT).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 19:27:35 +03:00
|
|
|
// DBLIST — list data for every entity in the drawing (LIST over the
|
|
|
|
|
// whole database rather than the current selection).
|
|
|
|
|
"DBLIST" => {
|
|
|
|
|
// Format every entity first so the immutable document borrow is
|
|
|
|
|
// released before writing to the command line.
|
|
|
|
|
let lines: Vec<String> = self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.entities()
|
|
|
|
|
.map(|entity| {
|
|
|
|
|
let type_name = crate::entities::names::dxf_name(entity);
|
|
|
|
|
let common = entity.common();
|
|
|
|
|
let color_str = common
|
|
|
|
|
.color
|
|
|
|
|
.index()
|
|
|
|
|
.map(|c| c.to_string())
|
|
|
|
|
.unwrap_or_else(|| "ByLayer".to_string());
|
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>
2026-06-29 08:23:58 +03:00
|
|
|
let linetype = if common.linetype.is_empty() || common.linetype == "ByLayer"
|
|
|
|
|
{
|
|
|
|
|
"ByLayer".to_string()
|
|
|
|
|
} else {
|
|
|
|
|
common.linetype.clone()
|
|
|
|
|
};
|
feat(commands): integrate standard CAD commands and fix behavior gaps
Broad pass over the command set: integrate missing commands, wire
previously-unreachable ribbon/menu buttons, and correct name/behaviour
mismatches. Every UI command token now resolves (real handler where the
infrastructure exists, an honest "not yet implemented" status otherwise).
New commands:
- Draw: 3DPOLY, 2D solid (SO/SOLID2D), HELIX, TRACE, SKETCH, DIMCENTER
(DCE/CENTERMARK)
- Modify/inquiry: REVERSE, MEASUREGEOM (MEA), DBLIST
- Data/manage: BASE, OVERKILL (exact-duplicate removal), AUDIT (read-only
integrity report), SETVAR (system-variable get/set), SCRIPT (.scr runner),
FINDNONPURGEABLE
Behaviour fixes:
- LINE and SPLINE gain Close/Undo; AREA computes area/perimeter in f64
(correct at large coordinates)
- ATTEDIT/ATE unified so the alias and the ribbon button open the
interactive editor; -ATTEDIT is the command-line form
- Linear/aligned/radius/diameter dimensions gain a text override (Text) and
text angle (Angle) option
- INSERT gains Scale/Rotate options; COPY gains an Array option
- LINETYPE SET sets the current entity linetype; SETBYLAYER resets overrides
- UNDO accepts a step count
Integrations: standard dimension/block aliases (DLI/DRA/DAN, BMAKE,
EXPORTPDF, DDIM), drafting-aid and screen toggles (GRID/SNAP/POLAR/OSNAP/
DSETTINGS/CLEANSCREEN/QUICKPROPERTIES), viewport tiling
(CASCADE/HORIZONTAL/VERTICAL/VPJOIN), and customization entry points
(CUI/ALIASEDIT).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 19:27:35 +03:00
|
|
|
let details = entity_list_details(entity);
|
|
|
|
|
format!(
|
|
|
|
|
"{type_name} Handle:{:X} Layer:{} Color:{} LT:{}{}",
|
|
|
|
|
common.handle.value(),
|
|
|
|
|
common.layer,
|
|
|
|
|
color_str,
|
|
|
|
|
linetype,
|
|
|
|
|
if details.is_empty() {
|
|
|
|
|
String::new()
|
|
|
|
|
} else {
|
|
|
|
|
format!("\n {details}")
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
if lines.is_empty() {
|
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>
2026-06-29 08:23:58 +03:00
|
|
|
self.command_line
|
|
|
|
|
.push_info("DBLIST: drawing has no entities.");
|
feat(commands): integrate standard CAD commands and fix behavior gaps
Broad pass over the command set: integrate missing commands, wire
previously-unreachable ribbon/menu buttons, and correct name/behaviour
mismatches. Every UI command token now resolves (real handler where the
infrastructure exists, an honest "not yet implemented" status otherwise).
New commands:
- Draw: 3DPOLY, 2D solid (SO/SOLID2D), HELIX, TRACE, SKETCH, DIMCENTER
(DCE/CENTERMARK)
- Modify/inquiry: REVERSE, MEASUREGEOM (MEA), DBLIST
- Data/manage: BASE, OVERKILL (exact-duplicate removal), AUDIT (read-only
integrity report), SETVAR (system-variable get/set), SCRIPT (.scr runner),
FINDNONPURGEABLE
Behaviour fixes:
- LINE and SPLINE gain Close/Undo; AREA computes area/perimeter in f64
(correct at large coordinates)
- ATTEDIT/ATE unified so the alias and the ribbon button open the
interactive editor; -ATTEDIT is the command-line form
- Linear/aligned/radius/diameter dimensions gain a text override (Text) and
text angle (Angle) option
- INSERT gains Scale/Rotate options; COPY gains an Array option
- LINETYPE SET sets the current entity linetype; SETBYLAYER resets overrides
- UNDO accepts a step count
Integrations: standard dimension/block aliases (DLI/DRA/DAN, BMAKE,
EXPORTPDF, DDIM), drafting-aid and screen toggles (GRID/SNAP/POLAR/OSNAP/
DSETTINGS/CLEANSCREEN/QUICKPROPERTIES), viewport tiling
(CASCADE/HORIZONTAL/VERTICAL/VPJOIN), and customization entry points
(CUI/ALIASEDIT).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 19:27:35 +03:00
|
|
|
} else {
|
|
|
|
|
let count = lines.len();
|
|
|
|
|
for l in lines {
|
|
|
|
|
self.command_line.push_output(&l);
|
|
|
|
|
}
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
"DBLIST: {count} entit{}.",
|
|
|
|
|
if count == 1 { "y" } else { "ies" }
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
// ── Break / Join ─────────────────────────────────────────────────
|
|
|
|
|
"JOIN" | "J" => {
|
|
|
|
|
use crate::modules::draw::modify::join::JoinCommand;
|
|
|
|
|
let cmd = JoinCommand::new();
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
"BREAK" | "BR" => {
|
|
|
|
|
use crate::modules::draw::modify::break_cmd::BreakInteractiveCommand;
|
|
|
|
|
let cmd = BreakInteractiveCommand::new();
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
"BREAKATPOINT" | "BAP" => {
|
|
|
|
|
use crate::modules::draw::modify::break_cmd::BreakAtPointCommand;
|
|
|
|
|
let cmd = BreakAtPointCommand::new();
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
"PEDIT" | "PE" => {
|
|
|
|
|
use crate::modules::draw::modify::pedit::PeditCommand;
|
|
|
|
|
let cmd_obj = PeditCommand::new();
|
|
|
|
|
self.command_line.push_info(&cmd_obj.prompt());
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
"SPLINEDIT" | "SPE" => {
|
|
|
|
|
use crate::modules::draw::modify::splinedit::SplineditCommand;
|
|
|
|
|
let cmd_obj = SplineditCommand::new();
|
|
|
|
|
self.command_line.push_info(&cmd_obj.prompt());
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-01 08:05:25 +03:00
|
|
|
// Bare ATTEDIT (and the ATE alias) open the attribute editor dialog.
|
|
|
|
|
// If a single block with attributes is already selected it opens on
|
|
|
|
|
// that block; otherwise the pick command runs and the editor opens
|
|
|
|
|
// once a block is chosen (see `command_driver`).
|
feat(commands): integrate standard CAD commands and fix behavior gaps
Broad pass over the command set: integrate missing commands, wire
previously-unreachable ribbon/menu buttons, and correct name/behaviour
mismatches. Every UI command token now resolves (real handler where the
infrastructure exists, an honest "not yet implemented" status otherwise).
New commands:
- Draw: 3DPOLY, 2D solid (SO/SOLID2D), HELIX, TRACE, SKETCH, DIMCENTER
(DCE/CENTERMARK)
- Modify/inquiry: REVERSE, MEASUREGEOM (MEA), DBLIST
- Data/manage: BASE, OVERKILL (exact-duplicate removal), AUDIT (read-only
integrity report), SETVAR (system-variable get/set), SCRIPT (.scr runner),
FINDNONPURGEABLE
Behaviour fixes:
- LINE and SPLINE gain Close/Undo; AREA computes area/perimeter in f64
(correct at large coordinates)
- ATTEDIT/ATE unified so the alias and the ribbon button open the
interactive editor; -ATTEDIT is the command-line form
- Linear/aligned/radius/diameter dimensions gain a text override (Text) and
text angle (Angle) option
- INSERT gains Scale/Rotate options; COPY gains an Array option
- LINETYPE SET sets the current entity linetype; SETBYLAYER resets overrides
- UNDO accepts a step count
Integrations: standard dimension/block aliases (DLI/DRA/DAN, BMAKE,
EXPORTPDF, DDIM), drafting-aid and screen toggles (GRID/SNAP/POLAR/OSNAP/
DSETTINGS/CLEANSCREEN/QUICKPROPERTIES), viewport tiling
(CASCADE/HORIZONTAL/VERTICAL/VPJOIN), and customization entry points
(CUI/ALIASEDIT).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 19:27:35 +03:00
|
|
|
"ATTEDIT" | "ATE" => {
|
2026-07-01 08:05:25 +03:00
|
|
|
let selected_attr_insert = {
|
|
|
|
|
let sel = self.tabs[i].scene.selected_entities();
|
|
|
|
|
if sel.len() == 1 {
|
|
|
|
|
let (h, e) = sel[0];
|
|
|
|
|
match e {
|
|
|
|
|
acadrust::EntityType::Insert(ins) if !ins.attributes.is_empty() => {
|
|
|
|
|
Some(h)
|
|
|
|
|
}
|
|
|
|
|
_ => None,
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
if let Some(handle) = selected_attr_insert {
|
|
|
|
|
self.open_attribute_editor(handle);
|
|
|
|
|
} else {
|
|
|
|
|
use crate::modules::draw::modify::attedit::AtteditCommand;
|
|
|
|
|
let cmd_obj = AtteditCommand::new();
|
|
|
|
|
self.command_line.push_info(&cmd_obj.prompt());
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
|
|
|
|
|
}
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── REFEDIT — in-place block editing ─────────────────────────────
|
|
|
|
|
"REFEDIT" => {
|
|
|
|
|
use crate::modules::draw::modify::refedit::RefEditPickCommand;
|
|
|
|
|
// If a session is already active, tell the user.
|
|
|
|
|
if self.tabs[i].refedit_session.is_some() {
|
|
|
|
|
self.command_line
|
|
|
|
|
.push_error("REFEDIT: a session is already active. Use REFCLOSE first.");
|
|
|
|
|
} else {
|
|
|
|
|
// Check if a single INSERT is already selected.
|
|
|
|
|
let selected: Vec<_> =
|
|
|
|
|
self.tabs[i].scene.selected_entities().into_iter().collect();
|
|
|
|
|
if selected.len() == 1 {
|
|
|
|
|
if let Some(acadrust::EntityType::Insert(_)) =
|
|
|
|
|
selected.first().map(|(_, e)| e)
|
|
|
|
|
{
|
|
|
|
|
let handle = selected[0].0;
|
|
|
|
|
// Skip pick phase — jump straight to begin.
|
|
|
|
|
let _ =
|
|
|
|
|
self.dispatch_command(&format!("REFEDIT_BEGIN:{}", handle.value()));
|
|
|
|
|
return Some(Task::none());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let cmd_obj = RefEditPickCommand::new();
|
|
|
|
|
self.command_line.push_info(&cmd_obj.prompt());
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd if cmd.starts_with("REFEDIT_BEGIN:") => {
|
|
|
|
|
use crate::modules::draw::modify::refedit::{
|
|
|
|
|
apply_insert_transform, RefEditSession,
|
|
|
|
|
};
|
|
|
|
|
use acadrust::Handle;
|
|
|
|
|
|
|
|
|
|
let handle_u64: u64 = cmd["REFEDIT_BEGIN:".len()..].parse().unwrap_or(0);
|
|
|
|
|
let insert_handle = Handle::new(handle_u64);
|
|
|
|
|
|
|
|
|
|
// Get INSERT entity.
|
|
|
|
|
let insert = match self.tabs[i].scene.document.get_entity(insert_handle) {
|
|
|
|
|
Some(acadrust::EntityType::Insert(ins)) => ins.clone(),
|
|
|
|
|
_ => {
|
|
|
|
|
self.command_line
|
|
|
|
|
.push_error("REFEDIT: selected object is not an INSERT.");
|
|
|
|
|
return Some(Task::none());
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Build the INSERT's full placement transform (OCS + rotation +
|
|
|
|
|
// scale, including non-uniform / mirrored) and its inverse, so
|
|
|
|
|
// edits round-trip back to block-local coordinates on SAVE.
|
|
|
|
|
let sx = insert.x_scale();
|
|
|
|
|
let sy = insert.y_scale();
|
|
|
|
|
let sz = insert.z_scale();
|
|
|
|
|
let forward = insert.get_transform();
|
|
|
|
|
let inverse = {
|
|
|
|
|
use acadrust::types::{Matrix3, Matrix4, Transform};
|
|
|
|
|
let ocs_t =
|
|
|
|
|
Matrix4::from_matrix3(Matrix3::arbitrary_axis(insert.normal).transpose());
|
|
|
|
|
let t_inv = Matrix4::translation(
|
|
|
|
|
-insert.insert_point.x,
|
|
|
|
|
-insert.insert_point.y,
|
|
|
|
|
-insert.insert_point.z,
|
|
|
|
|
);
|
|
|
|
|
let r_inv = Matrix4::rotation_z(-insert.rotation);
|
|
|
|
|
let s_inv = Matrix4::scaling(1.0 / sx, 1.0 / sy, 1.0 / sz);
|
|
|
|
|
// inverse(OCS·T·R·S) = S⁻¹·R⁻¹·T⁻¹·OCSᵀ
|
|
|
|
|
Transform::from_matrix(s_inv * r_inv * t_inv * ocs_t)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Find the block record.
|
|
|
|
|
let br_handle = match self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.block_records
|
|
|
|
|
.get(&insert.block_name)
|
|
|
|
|
{
|
|
|
|
|
Some(br) => br.handle,
|
|
|
|
|
None => {
|
|
|
|
|
self.command_line.push_error(&format!(
|
|
|
|
|
"REFEDIT: block \"{}\" not found.",
|
|
|
|
|
insert.block_name
|
|
|
|
|
));
|
|
|
|
|
return Some(Task::none());
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Collect block-local entities (skip structural Block/BlockEnd/AttDef).
|
|
|
|
|
let block_entities: Vec<_> = {
|
|
|
|
|
let br = self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.block_records
|
|
|
|
|
.get(&insert.block_name)
|
|
|
|
|
.unwrap();
|
|
|
|
|
br.entity_handles
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned())
|
|
|
|
|
.filter(|e| {
|
|
|
|
|
!matches!(
|
|
|
|
|
e,
|
|
|
|
|
acadrust::EntityType::Block(_)
|
|
|
|
|
| acadrust::EntityType::BlockEnd(_)
|
|
|
|
|
| acadrust::EntityType::AttributeDefinition(_)
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
.collect()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if block_entities.is_empty() {
|
|
|
|
|
self.command_line.push_error("REFEDIT: block is empty.");
|
|
|
|
|
return Some(Task::none());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let session = RefEditSession {
|
|
|
|
|
block_name: insert.block_name.clone(),
|
|
|
|
|
br_handle,
|
|
|
|
|
temp_handles: vec![],
|
|
|
|
|
forward,
|
|
|
|
|
inverse,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
self.push_undo_snapshot(i, "REFEDIT");
|
|
|
|
|
self.tabs[i].refedit_session = Some(session.clone());
|
|
|
|
|
|
|
|
|
|
// Add block entities to model space with INSERT transform applied.
|
|
|
|
|
let mut temp_handles = Vec::new();
|
|
|
|
|
for mut entity in block_entities {
|
|
|
|
|
apply_insert_transform(&mut entity, &session);
|
|
|
|
|
entity.common_mut().handle = acadrust::Handle::NULL;
|
|
|
|
|
entity.common_mut().owner_handle = acadrust::Handle::NULL;
|
|
|
|
|
let h = self.tabs[i].scene.add_entity(entity);
|
|
|
|
|
temp_handles.push(h);
|
|
|
|
|
}
|
|
|
|
|
self.tabs[i].refedit_session.as_mut().unwrap().temp_handles = temp_handles.clone();
|
|
|
|
|
|
|
|
|
|
// Fade everything except the entities being edited, so the
|
|
|
|
|
// surrounding drawing stays visible for context but the block's
|
|
|
|
|
// geometry stands out. (#136)
|
|
|
|
|
self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.set_refedit_keep(Some(temp_handles.iter().copied().collect()));
|
|
|
|
|
|
|
|
|
|
// Select the temp entities so user can see what they're editing.
|
|
|
|
|
self.tabs[i].scene.deselect_all();
|
|
|
|
|
for h in &temp_handles {
|
|
|
|
|
self.tabs[i].scene.select_entity(*h, false);
|
|
|
|
|
}
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
|
|
|
|
// No active command — the user edits the block's geometry freely
|
|
|
|
|
// (move, grips, draw, erase…) and runs REFCLOSE when done. (#136)
|
|
|
|
|
self.tabs[i].active_cmd = None;
|
|
|
|
|
self.command_line.push_info(&format!(
|
|
|
|
|
"REFEDIT: Editing block \"{}\". Run REFCLOSE to save, REFCLOSE_DISCARD to cancel.",
|
|
|
|
|
insert.block_name
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
"REFCLOSE" => {
|
|
|
|
|
if self.tabs[i].refedit_session.is_some() {
|
|
|
|
|
use crate::modules::draw::modify::refedit::RefCloseCommand;
|
|
|
|
|
let cmd_obj = RefCloseCommand::new();
|
|
|
|
|
self.command_line.push_info(&cmd_obj.prompt());
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
|
|
|
|
|
} else {
|
|
|
|
|
self.command_line
|
|
|
|
|
.push_error("REFCLOSE: no REFEDIT session active.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
"REFCLOSE_SAVE" => {
|
|
|
|
|
use crate::modules::draw::modify::explode::normalize_entity_for_block;
|
|
|
|
|
use crate::modules::draw::modify::refedit::apply_insert_inverse_transform;
|
|
|
|
|
|
|
|
|
|
let session = match self.tabs[i].refedit_session.take() {
|
|
|
|
|
Some(s) => s,
|
|
|
|
|
None => {
|
|
|
|
|
self.command_line
|
|
|
|
|
.push_error("REFCLOSE: no REFEDIT session active.");
|
|
|
|
|
return Some(Task::none());
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
self.push_undo_snapshot(i, "REFCLOSE");
|
|
|
|
|
|
|
|
|
|
// Collect the edited temp entities.
|
|
|
|
|
let new_entities: Vec<acadrust::EntityType> = session
|
|
|
|
|
.temp_handles
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned())
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
// Remove temp entities from model space.
|
|
|
|
|
self.tabs[i].scene.erase_entities(&session.temp_handles);
|
|
|
|
|
|
|
|
|
|
// Apply inverse INSERT transform → block-local coordinates.
|
|
|
|
|
let new_entities: Vec<_> = new_entities
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|mut entity| {
|
|
|
|
|
apply_insert_inverse_transform(&mut entity, &session);
|
|
|
|
|
let mut entity = normalize_entity_for_block(entity);
|
|
|
|
|
entity.common_mut().handle = acadrust::Handle::NULL;
|
|
|
|
|
entity.common_mut().owner_handle = session.br_handle;
|
|
|
|
|
entity
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
// Remove old block entities from the document.
|
|
|
|
|
let old_handles: Vec<_> = match self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.block_records
|
|
|
|
|
.get(&session.block_name)
|
|
|
|
|
{
|
|
|
|
|
Some(br) => br.entity_handles.clone(),
|
|
|
|
|
None => vec![],
|
|
|
|
|
};
|
|
|
|
|
for h in &old_handles {
|
|
|
|
|
self.tabs[i].scene.document.remove_entity(*h);
|
|
|
|
|
}
|
|
|
|
|
// Flush the entity_handles list from the block record.
|
|
|
|
|
if let Some(br) = self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.block_records
|
|
|
|
|
.get_mut(&session.block_name)
|
|
|
|
|
{
|
|
|
|
|
br.entity_handles.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add the new block entities.
|
|
|
|
|
for entity in new_entities {
|
|
|
|
|
let _ = self.tabs[i].scene.document.add_entity(entity);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
"REFCLOSE: Block \"{}\" saved. All references updated.",
|
|
|
|
|
session.block_name
|
|
|
|
|
));
|
|
|
|
|
// End the edit fade before rebuilding, so the restored geometry
|
|
|
|
|
// recolours bright. (#136)
|
|
|
|
|
self.tabs[i].scene.set_refedit_keep(None);
|
|
|
|
|
// Rebuild hatch/image/mesh caches since block content changed.
|
|
|
|
|
self.tabs[i].scene.rebuild_derived_caches();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
"REFCLOSE_DISCARD" => {
|
|
|
|
|
let session = match self.tabs[i].refedit_session.take() {
|
|
|
|
|
Some(s) => s,
|
|
|
|
|
None => {
|
|
|
|
|
self.command_line
|
|
|
|
|
.push_error("REFCLOSE: no REFEDIT session active.");
|
|
|
|
|
return Some(Task::none());
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
// Remove temp entities without modifying the block.
|
|
|
|
|
self.tabs[i].scene.erase_entities(&session.temp_handles);
|
|
|
|
|
self.tabs[i].scene.deselect_all();
|
|
|
|
|
// End the edit fade — restore the drawing to full brightness.
|
|
|
|
|
self.tabs[i].scene.set_refedit_keep(None);
|
|
|
|
|
self.command_line
|
|
|
|
|
.push_output("REFCLOSE: Changes discarded.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
"ALIGN" | "AL" => {
|
|
|
|
|
use crate::modules::draw::modify::align::AlignCommand;
|
|
|
|
|
let cmd = AlignCommand::new();
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
"LENGTHEN" | "LEN" => {
|
|
|
|
|
use crate::modules::draw::modify::lengthen::LengthenCommand;
|
|
|
|
|
let cmd = LengthenCommand::new();
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
"DIVIDE" | "DIV" => {
|
|
|
|
|
use crate::modules::draw::inquiry::divide::DivideCommand;
|
|
|
|
|
let cmd = DivideCommand::new();
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
"MEASURE" | "ME" => {
|
|
|
|
|
use crate::modules::draw::inquiry::divide::MeasureCommand;
|
|
|
|
|
let cmd = MeasureCommand::new();
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Inquiry ──────────────────────────────────────────────────────
|
|
|
|
|
"DIST" | "DI" => {
|
|
|
|
|
use crate::modules::draw::inquiry::dist::DistCommand;
|
|
|
|
|
let cmd = DistCommand::new();
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
"ID" => {
|
|
|
|
|
use crate::modules::draw::inquiry::id::IdCommand;
|
|
|
|
|
let cmd = IdCommand::new();
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
"AREA" => {
|
|
|
|
|
use crate::modules::draw::inquiry::area::AreaCommand;
|
|
|
|
|
let cmd = AreaCommand::new();
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── MASSPROP — area, perimeter, centroid of selected entities ────
|
|
|
|
|
"MASSPROP" => {
|
|
|
|
|
let selected = self.tabs[i].scene.selected_entities();
|
|
|
|
|
if selected.is_empty() {
|
|
|
|
|
self.command_line
|
|
|
|
|
.push_error("MASSPROP: no entities selected. Select entities first.");
|
|
|
|
|
} else {
|
|
|
|
|
for (handle, _) in &selected {
|
|
|
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity(*handle) {
|
|
|
|
|
use crate::entities::traits::EntityTypeOps;
|
|
|
|
|
if let Some(props) = entity.mass_props() {
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
"{} Area={:.4} Perimeter={:.4} Centroid=({:.4},{:.4})",
|
|
|
|
|
crate::entities::names::dxf_name(entity),
|
|
|
|
|
props.area,
|
|
|
|
|
props.perimeter,
|
|
|
|
|
props.cx,
|
|
|
|
|
props.cy,
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── FLATTEN — move selected (or all) entities to Z=0 ─────────────
|
|
|
|
|
"FLATTEN" => {
|
|
|
|
|
let handles: Vec<acadrust::Handle> = {
|
|
|
|
|
let sel = self.tabs[i].scene.selected_entities();
|
|
|
|
|
if sel.is_empty() {
|
|
|
|
|
// Flatten all entities
|
|
|
|
|
self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.entities()
|
|
|
|
|
.map(|e| e.common().handle)
|
|
|
|
|
.collect()
|
|
|
|
|
} else {
|
|
|
|
|
sel.into_iter().map(|(h, _)| h).collect()
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
if handles.is_empty() {
|
|
|
|
|
self.command_line.push_error("FLATTEN: no entities.");
|
|
|
|
|
} else {
|
|
|
|
|
self.push_undo_snapshot(i, "FLATTEN");
|
|
|
|
|
for h in &handles {
|
|
|
|
|
if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) {
|
|
|
|
|
flatten_entity_z(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
"FLATTEN: {} entity(ies) moved to Z=0.",
|
|
|
|
|
handles.len()
|
|
|
|
|
));
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── QSELECT — quick-select entities by property ───────────────────
|
|
|
|
|
// QSELECT TYPE <type> — select all entities of given type
|
|
|
|
|
// QSELECT LAYER <name> — select all entities on layer
|
|
|
|
|
// QSELECT COLOR <n> — select all entities with color index n
|
|
|
|
|
// QSELECT LINETYPE <name> — select all entities with linetype
|
|
|
|
|
cmd if cmd == "QSELECT" || cmd.starts_with("QSELECT ") => {
|
|
|
|
|
let rest = cmd.split_once(' ').map(|(_, r)| r.trim()).unwrap_or("");
|
|
|
|
|
let parts: Vec<&str> = rest.splitn(2, ' ').collect();
|
|
|
|
|
let prop = parts.first().map(|s| s.to_uppercase()).unwrap_or_default();
|
|
|
|
|
let val = parts.get(1).map(|s| s.trim()).unwrap_or("").to_uppercase();
|
|
|
|
|
|
|
|
|
|
let matched: Vec<acadrust::Handle> = self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.entities()
|
|
|
|
|
.filter(|e| {
|
|
|
|
|
let c = e.common();
|
|
|
|
|
match prop.as_str() {
|
|
|
|
|
"TYPE" => crate::entities::names::dxf_name(e).to_uppercase() == val,
|
|
|
|
|
"LAYER" => c.layer.to_uppercase() == val,
|
|
|
|
|
"COLOR" => c
|
|
|
|
|
.color
|
|
|
|
|
.index()
|
|
|
|
|
.map(|n| n.to_string() == val)
|
|
|
|
|
.unwrap_or(val == "BYLAYER"),
|
|
|
|
|
"LINETYPE" => c.linetype.to_uppercase() == val,
|
|
|
|
|
_ => false,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.map(|e| e.common().handle)
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
if prop.is_empty() {
|
|
|
|
|
self.command_line
|
|
|
|
|
.push_info("Usage: QSELECT TYPE|LAYER|COLOR|LINETYPE <value>");
|
|
|
|
|
} else if matched.is_empty() {
|
|
|
|
|
self.command_line
|
|
|
|
|
.push_output("QSELECT: no matching entities.");
|
|
|
|
|
} else {
|
|
|
|
|
self.tabs[i].scene.deselect_all();
|
|
|
|
|
for h in &matched {
|
|
|
|
|
self.tabs[i].scene.select_entity(*h, false);
|
|
|
|
|
}
|
|
|
|
|
self.command_line
|
|
|
|
|
.push_output(&format!("QSELECT: {} entity(ies) selected.", matched.len()));
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── COUNT — entity statistics ─────────────────────────────────────
|
|
|
|
|
cmd if cmd == "COUNT" || cmd.starts_with("COUNT ") => {
|
|
|
|
|
let filter = cmd.split_once(' ').map(|(_, r)| r.trim().to_uppercase());
|
|
|
|
|
let mut counts: std::collections::BTreeMap<String, usize> = Default::default();
|
|
|
|
|
for e in self.tabs[i].scene.document.entities() {
|
|
|
|
|
let layer = &e.common().layer;
|
|
|
|
|
let type_name = crate::entities::names::dxf_name(e);
|
|
|
|
|
let key = match &filter {
|
|
|
|
|
Some(f) if f == "LAYER" => layer.clone(),
|
|
|
|
|
Some(f) if f == "TYPE" => type_name.to_string(),
|
|
|
|
|
Some(f) => {
|
|
|
|
|
// Filter by layer name
|
|
|
|
|
if layer.to_uppercase() != *f {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
type_name.to_string()
|
|
|
|
|
}
|
|
|
|
|
None => type_name.to_string(),
|
|
|
|
|
};
|
|
|
|
|
*counts.entry(key).or_default() += 1;
|
|
|
|
|
}
|
|
|
|
|
let total: usize = counts.values().sum();
|
|
|
|
|
for (k, n) in &counts {
|
|
|
|
|
self.command_line.push_output(&format!(" {k}: {n}"));
|
|
|
|
|
}
|
|
|
|
|
self.command_line
|
|
|
|
|
.push_output(&format!("COUNT: {total} entity(ies) total."));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
"DATAEXTRACTION" | "EATTEXT" | "ATTEXT" => {
|
|
|
|
|
let csv = build_data_extraction_csv(&self.tabs[i].scene.document);
|
|
|
|
|
return Some(Task::done(Message::DataExtractionSave(csv)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Find / Replace ────────────────────────────────────────────────
|
|
|
|
|
// FIND <search> — list all Text/MText/Dimension containing <search>
|
|
|
|
|
// FIND <search> REPLACE <rep> — replace first occurrence (case-insensitive)
|
|
|
|
|
// FINDALL <search> REPLACE <rep> — replace all occurrences
|
|
|
|
|
cmd if cmd == "FIND"
|
|
|
|
|
|| cmd.starts_with("FIND ")
|
|
|
|
|
|| cmd == "FINDALL"
|
|
|
|
|
|| cmd.starts_with("FINDALL ") =>
|
|
|
|
|
{
|
|
|
|
|
let all_mode = cmd.starts_with("FINDALL");
|
|
|
|
|
let rest = cmd.split_once(' ').map(|(_, r)| r.trim()).unwrap_or("");
|
|
|
|
|
|
|
|
|
|
// Split at " REPLACE " keyword (case-insensitive)
|
|
|
|
|
let (search, replacement) = if let Some(pos) = rest.to_uppercase().find(" REPLACE ")
|
|
|
|
|
{
|
|
|
|
|
(&rest[..pos], Some(rest[pos + 9..].trim()))
|
|
|
|
|
} else {
|
|
|
|
|
(rest, None)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if search.is_empty() {
|
|
|
|
|
self.command_line.push_error("FIND: specify search text.");
|
|
|
|
|
} else {
|
|
|
|
|
let search_lc = search.to_lowercase();
|
|
|
|
|
let mut count = 0usize;
|
|
|
|
|
let handles: Vec<acadrust::Handle> = self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.entities()
|
|
|
|
|
.filter_map(|e| {
|
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>
2026-06-29 08:23:58 +03:00
|
|
|
use crate::entities::traits::EntityTypeOps;
|
|
|
|
|
let txt = e.text_content()?;
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
if txt.to_lowercase().contains(&search_lc) {
|
|
|
|
|
Some(e.common().handle)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
if let Some(rep) = replacement {
|
|
|
|
|
// Replace mode
|
|
|
|
|
let targets: Vec<_> = if all_mode {
|
|
|
|
|
handles.clone()
|
|
|
|
|
} else {
|
|
|
|
|
handles.iter().copied().take(1).collect()
|
|
|
|
|
};
|
|
|
|
|
if targets.is_empty() {
|
|
|
|
|
self.command_line
|
|
|
|
|
.push_output(&format!("FIND: \"{}\" not found.", search));
|
|
|
|
|
} else {
|
|
|
|
|
self.push_undo_snapshot(i, "FIND/REPLACE");
|
|
|
|
|
for h in &targets {
|
|
|
|
|
if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) {
|
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>
2026-06-29 08:23:58 +03:00
|
|
|
crate::entities::traits::EntityTypeOps::replace_text(
|
|
|
|
|
e, search, rep,
|
|
|
|
|
);
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
count += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
"FIND/REPLACE: replaced {} occurrence(s) of \"{}\" → \"{}\".",
|
|
|
|
|
count, search, rep
|
|
|
|
|
));
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// List mode
|
|
|
|
|
if handles.is_empty() {
|
|
|
|
|
self.command_line
|
|
|
|
|
.push_output(&format!("FIND: \"{}\" not found.", search));
|
|
|
|
|
} else {
|
|
|
|
|
for h in &handles {
|
|
|
|
|
if let Some(e) = self.tabs[i].scene.document.get_entity(*h) {
|
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>
2026-06-29 08:23:58 +03:00
|
|
|
use crate::entities::traits::EntityTypeOps;
|
|
|
|
|
let txt = e.text_content().unwrap_or_default();
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
" Handle {:X}: \"{}\"",
|
|
|
|
|
h.value(),
|
|
|
|
|
txt
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
"FIND: {} match(es) for \"{}\".",
|
|
|
|
|
handles.len(),
|
|
|
|
|
search
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_ => return None,
|
|
|
|
|
}
|
|
|
|
|
Some(self.finish_dispatch(cmd))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn entity_list_details(entity: &acadrust::EntityType) -> String {
|
|
|
|
|
use std::f64::consts::PI;
|
|
|
|
|
match entity {
|
|
|
|
|
acadrust::EntityType::Line(l) => format!(
|
|
|
|
|
"from ({:.4},{:.4},{:.4}) to ({:.4},{:.4},{:.4}) len={:.4}",
|
|
|
|
|
l.start.x,
|
|
|
|
|
l.start.y,
|
|
|
|
|
l.start.z,
|
|
|
|
|
l.end.x,
|
|
|
|
|
l.end.y,
|
|
|
|
|
l.end.z,
|
|
|
|
|
((l.end.x - l.start.x).powi(2)
|
|
|
|
|
+ (l.end.y - l.start.y).powi(2)
|
|
|
|
|
+ (l.end.z - l.start.z).powi(2))
|
|
|
|
|
.sqrt()
|
|
|
|
|
),
|
|
|
|
|
acadrust::EntityType::Circle(c) => format!(
|
|
|
|
|
"center ({:.4},{:.4},{:.4}) r={:.4} area={:.4}",
|
|
|
|
|
c.center.x,
|
|
|
|
|
c.center.y,
|
|
|
|
|
c.center.z,
|
|
|
|
|
c.radius,
|
|
|
|
|
PI * c.radius * c.radius
|
|
|
|
|
),
|
|
|
|
|
acadrust::EntityType::Arc(a) => format!(
|
|
|
|
|
"center ({:.4},{:.4},{:.4}) r={:.4} start={:.2}° end={:.2}°",
|
|
|
|
|
a.center.x,
|
|
|
|
|
a.center.y,
|
|
|
|
|
a.center.z,
|
|
|
|
|
a.radius,
|
|
|
|
|
a.start_angle.to_degrees(),
|
|
|
|
|
a.end_angle.to_degrees()
|
|
|
|
|
),
|
|
|
|
|
acadrust::EntityType::LwPolyline(p) => format!(
|
|
|
|
|
"{} vertices closed={} elevation={:.4}",
|
|
|
|
|
p.vertices.len(),
|
|
|
|
|
p.is_closed,
|
|
|
|
|
p.elevation
|
|
|
|
|
),
|
|
|
|
|
acadrust::EntityType::Text(t) => format!(
|
|
|
|
|
"\"{}\" h={:.4} at ({:.4},{:.4})",
|
|
|
|
|
t.value, t.height, t.insertion_point.x, t.insertion_point.y
|
|
|
|
|
),
|
|
|
|
|
acadrust::EntityType::MText(t) => format!(
|
|
|
|
|
"\"{}\" h={:.4} at ({:.4},{:.4})",
|
|
|
|
|
t.value.chars().take(40).collect::<String>(),
|
|
|
|
|
t.height,
|
|
|
|
|
t.insertion_point.x,
|
|
|
|
|
t.insertion_point.y
|
|
|
|
|
),
|
|
|
|
|
acadrust::EntityType::Insert(ins) => format!(
|
|
|
|
|
"block=\"{}\" at ({:.4},{:.4},{:.4}) scale=({:.4},{:.4},{:.4}) rot={:.2}°",
|
|
|
|
|
ins.block_name,
|
|
|
|
|
ins.insert_point.x,
|
|
|
|
|
ins.insert_point.y,
|
|
|
|
|
ins.insert_point.z,
|
|
|
|
|
ins.x_scale(),
|
|
|
|
|
ins.y_scale(),
|
|
|
|
|
ins.z_scale(),
|
|
|
|
|
ins.rotation.to_degrees()
|
|
|
|
|
),
|
|
|
|
|
acadrust::EntityType::Spline(s) => format!(
|
|
|
|
|
"{} ctrl pts degree={} closed={}",
|
|
|
|
|
s.control_points.len(),
|
|
|
|
|
s.degree,
|
|
|
|
|
s.flags.closed
|
|
|
|
|
),
|
|
|
|
|
acadrust::EntityType::Ellipse(e) => format!(
|
|
|
|
|
"center ({:.4},{:.4}) major_len={:.4} ratio={:.4}",
|
|
|
|
|
e.center.x,
|
|
|
|
|
e.center.y,
|
|
|
|
|
e.major_axis_length(),
|
|
|
|
|
e.minor_axis_ratio
|
|
|
|
|
),
|
|
|
|
|
_ => String::new(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn flatten_entity_z(entity: &mut acadrust::EntityType) {
|
|
|
|
|
match entity {
|
|
|
|
|
acadrust::EntityType::Line(l) => {
|
|
|
|
|
l.start.z = 0.0;
|
|
|
|
|
l.end.z = 0.0;
|
|
|
|
|
}
|
|
|
|
|
acadrust::EntityType::Circle(c) => {
|
|
|
|
|
c.center.z = 0.0;
|
|
|
|
|
}
|
|
|
|
|
acadrust::EntityType::Arc(a) => {
|
|
|
|
|
a.center.z = 0.0;
|
|
|
|
|
}
|
|
|
|
|
acadrust::EntityType::LwPolyline(p) => {
|
|
|
|
|
p.elevation = 0.0;
|
|
|
|
|
}
|
|
|
|
|
acadrust::EntityType::Text(t) => {
|
|
|
|
|
t.insertion_point.z = 0.0;
|
|
|
|
|
}
|
|
|
|
|
acadrust::EntityType::MText(t) => {
|
|
|
|
|
t.insertion_point.z = 0.0;
|
|
|
|
|
}
|
|
|
|
|
acadrust::EntityType::Insert(ins) => {
|
|
|
|
|
ins.insert_point.z = 0.0;
|
|
|
|
|
}
|
|
|
|
|
acadrust::EntityType::Point(p) => {
|
|
|
|
|
p.location.z = 0.0;
|
|
|
|
|
}
|
|
|
|
|
acadrust::EntityType::Spline(s) => {
|
|
|
|
|
for cp in &mut s.control_points {
|
|
|
|
|
cp.z = 0.0;
|
|
|
|
|
}
|
|
|
|
|
for fp in &mut s.fit_points {
|
|
|
|
|
fp.z = 0.0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
acadrust::EntityType::Ellipse(e) => {
|
|
|
|
|
e.center.z = 0.0;
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── DATAEXTRACTION ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/// Build a CSV string with one row per entity in model space.
|
|
|
|
|
/// Columns: Type, Handle, Layer, Color, Linetype, ExtraInfo
|
|
|
|
|
fn build_data_extraction_csv(doc: &acadrust::CadDocument) -> String {
|
|
|
|
|
use acadrust::EntityType;
|
|
|
|
|
|
|
|
|
|
let mut out = String::from("Type,Handle,Layer,Color,Linetype,ExtraInfo\n");
|
|
|
|
|
|
|
|
|
|
let ms_handle = doc.header.model_space_block_handle;
|
|
|
|
|
for e in doc.entities() {
|
|
|
|
|
// Skip Block/EndBlock sentinels and paper-space entities.
|
|
|
|
|
if matches!(e, EntityType::Block(_) | EntityType::BlockEnd(_)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if !ms_handle.is_null() && e.common().owner_handle != ms_handle {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let type_name = crate::entities::names::dxf_name(e);
|
|
|
|
|
let handle = format!("{:X}", e.common().handle.value());
|
|
|
|
|
let layer = csv_escape(&e.common().layer);
|
|
|
|
|
let color = format!("{}", e.common().color);
|
|
|
|
|
let lt = csv_escape(&e.common().linetype);
|
|
|
|
|
let extra = csv_escape(&entity_extra_info(e));
|
|
|
|
|
out.push_str(&format!(
|
|
|
|
|
"{type_name},{handle},{layer},{color},{lt},{extra}\n"
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Return a short geometry summary for CSV ExtraInfo column.
|
|
|
|
|
fn entity_extra_info(entity: &acadrust::EntityType) -> String {
|
|
|
|
|
use acadrust::EntityType;
|
|
|
|
|
match entity {
|
|
|
|
|
EntityType::Line(e) => format!(
|
|
|
|
|
"({:.3},{:.3})-({:.3},{:.3})",
|
|
|
|
|
e.start.x, e.start.y, e.end.x, e.end.y
|
|
|
|
|
),
|
|
|
|
|
EntityType::Circle(e) => {
|
|
|
|
|
format!("C({:.3},{:.3}) R={:.3}", e.center.x, e.center.y, e.radius)
|
|
|
|
|
}
|
|
|
|
|
EntityType::Arc(e) => format!(
|
|
|
|
|
"C({:.3},{:.3}) R={:.3} {:.1}°-{:.1}°",
|
|
|
|
|
e.center.x,
|
|
|
|
|
e.center.y,
|
|
|
|
|
e.radius,
|
|
|
|
|
e.start_angle.to_degrees(),
|
|
|
|
|
e.end_angle.to_degrees()
|
|
|
|
|
),
|
|
|
|
|
EntityType::Text(e) => e.value.clone(),
|
|
|
|
|
EntityType::MText(e) => e.value.chars().take(60).collect(),
|
|
|
|
|
EntityType::Insert(e) => format!(
|
|
|
|
|
"BLK={} @({:.3},{:.3})",
|
|
|
|
|
e.block_name, e.insert_point.x, e.insert_point.y
|
|
|
|
|
),
|
|
|
|
|
EntityType::LwPolyline(e) => format!("{} vertices", e.vertices.len()),
|
|
|
|
|
EntityType::Polyline(e) => format!("{} vertices", e.vertices.len()),
|
|
|
|
|
EntityType::Polyline2D(e) => format!("{} vertices", e.vertices.len()),
|
|
|
|
|
EntityType::Polyline3D(e) => format!("{} vertices", e.vertices.len()),
|
|
|
|
|
EntityType::Hatch(e) => format!("PAT={}", e.pattern.name),
|
|
|
|
|
EntityType::Dimension(e) => format!("{:.3}", e.base().actual_measurement),
|
|
|
|
|
EntityType::Spline(e) => format!("{} ctrl pts", e.control_points.len()),
|
|
|
|
|
_ => String::new(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Escape a string for a CSV field (wrap in quotes if it contains comma/quote/newline).
|
|
|
|
|
fn csv_escape(s: &str) -> String {
|
|
|
|
|
if s.contains(',') || s.contains('"') || s.contains('\n') {
|
|
|
|
|
format!("\"{}\"", s.replace('"', "\"\""))
|
|
|
|
|
} else {
|
|
|
|
|
s.to_string()
|
|
|
|
|
}
|
|
|
|
|
}
|
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>
2026-06-29 08:23:58 +03:00
|
|
|
|
|
|
|
|
// ── 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}'"))
|
|
|
|
|
}
|
|
|
|
|
}
|