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(command): ADDSELECTED draws a new object like the selected one (#239)
Take the first selected entity as a template, adopt its general
properties (layer, colour, linetype, lineweight, linetype scale) as the
current defaults, then start the matching draw command so the object
drawn next inherits them.
Supports Point, Line, Circle, Arc, Ellipse, all polylines (PLINE), Text,
MText, Spline, Hatch, Solid, Ray and XLine; other types report that
creation is unsupported. Registered for command-line autocomplete.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 18:42:26 +03:00
|
|
|
// ADDSELECTED — draw a new object of the same type as the selected
|
|
|
|
|
// one, inheriting its general properties. (#239)
|
|
|
|
|
"ADDSELECTED" => {
|
|
|
|
|
return Some(self.cmd_add_selected(i));
|
|
|
|
|
}
|
|
|
|
|
|
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))
|
|
|
|
|
}
|
feat(command): ADDSELECTED draws a new object like the selected one (#239)
Take the first selected entity as a template, adopt its general
properties (layer, colour, linetype, lineweight, linetype scale) as the
current defaults, then start the matching draw command so the object
drawn next inherits them.
Supports Point, Line, Circle, Arc, Ellipse, all polylines (PLINE), Text,
MText, Spline, Hatch, Solid, Ray and XLine; other types report that
creation is unsupported. Registered for command-line autocomplete.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 18:42:26 +03:00
|
|
|
|
|
|
|
|
/// ADDSELECTED — start the draw command that creates the same kind of
|
|
|
|
|
/// object as the currently-selected one, adopting its general properties
|
|
|
|
|
/// (layer, colour, linetype, lineweight, linetype scale) as the current
|
|
|
|
|
/// defaults so the new object is drawn to match. Issue #239.
|
|
|
|
|
pub(super) fn cmd_add_selected(&mut self, i: usize) -> Task<Message> {
|
|
|
|
|
// Use the first selected object as the template.
|
|
|
|
|
let Some(handle) = self.tabs[i].scene.selected.iter().next().copied() else {
|
|
|
|
|
self.command_line.push_info(
|
|
|
|
|
"ADDSELECTED: select an object first, then run ADDSELECTED to draw a new one like it.",
|
|
|
|
|
);
|
|
|
|
|
return Task::none();
|
|
|
|
|
};
|
|
|
|
|
// Pull the template's type + general properties into owned values, then
|
|
|
|
|
// drop the borrow so the document can be mutated below.
|
|
|
|
|
let info = self.tabs[i].scene.document.get_entity(handle).map(|e| {
|
|
|
|
|
let c = e.common();
|
|
|
|
|
(
|
|
|
|
|
add_selected_verb(e),
|
|
|
|
|
crate::entities::names::dxf_name(e).to_string(),
|
|
|
|
|
c.layer.clone(),
|
|
|
|
|
c.color,
|
|
|
|
|
c.linetype.clone(),
|
|
|
|
|
c.linetype_scale,
|
|
|
|
|
c.line_weight,
|
|
|
|
|
)
|
|
|
|
|
});
|
|
|
|
|
let Some((verb, kind, layer, color, linetype, lt_scale, lw)) = info else {
|
|
|
|
|
self.command_line
|
|
|
|
|
.push_error("ADDSELECTED: selected object not found.");
|
|
|
|
|
return Task::none();
|
|
|
|
|
};
|
|
|
|
|
let Some(verb) = verb else {
|
|
|
|
|
self.command_line
|
|
|
|
|
.push_error(&format!("ADDSELECTED: creating a new {kind} is not supported."));
|
|
|
|
|
return Task::none();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Clear the selection so adopting the properties as defaults doesn't
|
|
|
|
|
// rewrite the template, and so the draw starts on a clean slate.
|
|
|
|
|
self.tabs[i].scene.deselect_all();
|
|
|
|
|
|
|
|
|
|
// Adopt the template's general properties as the current defaults. The
|
|
|
|
|
// entity-creation path stamps new objects from the tab's active layer
|
|
|
|
|
// and the ribbon's active colour / linetype / lineweight, mirrored into
|
|
|
|
|
// the header so they persist (CLAYER / CECOLOR / CELTYPE / CELWEIGHT /
|
|
|
|
|
// CELTSCALE).
|
|
|
|
|
let layer_handle = self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.layers
|
|
|
|
|
.get(&layer)
|
|
|
|
|
.map(|l| l.handle)
|
|
|
|
|
.unwrap_or(acadrust::types::Handle::NULL);
|
|
|
|
|
let lt_handle = self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.line_types
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|x| x.name.eq_ignore_ascii_case(&linetype))
|
|
|
|
|
.map(|x| x.handle)
|
|
|
|
|
.unwrap_or(acadrust::types::Handle::NULL);
|
|
|
|
|
{
|
|
|
|
|
let header = &mut self.tabs[i].scene.document.header;
|
|
|
|
|
header.current_layer_name = layer.clone();
|
|
|
|
|
header.current_layer_handle = layer_handle;
|
|
|
|
|
header.current_entity_color = color;
|
|
|
|
|
header.current_linetype_name = linetype.clone();
|
|
|
|
|
header.current_linetype_handle = lt_handle;
|
|
|
|
|
header.current_line_weight = lw.value();
|
|
|
|
|
header.current_entity_linetype_scale = lt_scale;
|
|
|
|
|
}
|
|
|
|
|
self.tabs[i].active_layer = layer.clone();
|
|
|
|
|
self.tabs[i].layers.current_layer = layer.clone();
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
self.ribbon.active_layer = layer.clone();
|
|
|
|
|
self.ribbon.active_color = color;
|
|
|
|
|
self.ribbon.active_linetype = linetype;
|
|
|
|
|
self.ribbon.active_lineweight = lw;
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
"Add Selected: drawing a new {kind} on layer \"{layer}\"."
|
|
|
|
|
));
|
|
|
|
|
// Launch the matching draw command (installs its interactive step).
|
|
|
|
|
self.dispatch_command(verb)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Map a template entity to the draw-command verb that creates the same kind of
|
|
|
|
|
/// object, or `None` when there is no interactive creator for it. Used by
|
|
|
|
|
/// ADDSELECTED (issue #239).
|
|
|
|
|
fn add_selected_verb(entity: &acadrust::EntityType) -> Option<&'static str> {
|
|
|
|
|
use acadrust::EntityType;
|
|
|
|
|
Some(match entity {
|
|
|
|
|
EntityType::Point(_) => "POINT",
|
|
|
|
|
EntityType::Line(_) => "LINE",
|
|
|
|
|
EntityType::Circle(_) => "CIRCLE",
|
|
|
|
|
EntityType::Arc(_) => "ARC",
|
|
|
|
|
EntityType::Ellipse(_) => "ELLIPSE",
|
|
|
|
|
EntityType::LwPolyline(_)
|
|
|
|
|
| EntityType::Polyline(_)
|
|
|
|
|
| EntityType::Polyline2D(_)
|
|
|
|
|
| EntityType::Polyline3D(_) => "PLINE",
|
|
|
|
|
EntityType::Text(_) => "TEXT",
|
|
|
|
|
EntityType::MText(_) => "MTEXT",
|
|
|
|
|
EntityType::Spline(_) => "SPLINE",
|
|
|
|
|
EntityType::Hatch(_) => "HATCH",
|
|
|
|
|
EntityType::Solid(_) => "SOLID",
|
|
|
|
|
EntityType::Ray(_) => "RAY",
|
|
|
|
|
EntityType::XLine(_) => "XLINE",
|
|
|
|
|
_ => return None,
|
|
|
|
|
})
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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}'"))
|
|
|
|
|
}
|
|
|
|
|
}
|