feat(commands): add FLATSHOT and CONVTOSURFACE

- FLATSHOT: project the selected solid's edges to 2D Line entities at Z=0
- CONVTOSURFACE: convert the selected solid(s) to Surface entities

Both reuse the cached B-rep's edge wires. COMMANDS.md: 219/262.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-06-29 09:16:44 +03:00
commit c653a3f38e
4 changed files with 108 additions and 4 deletions

View file

@ -230,9 +230,9 @@ Status of every standard CAD command in Open CAD Studio:
| `INTERFERE` | INF | Interference solid from overlap | ✅ | | `INTERFERE` | INF | Interference solid from overlap | ✅ |
| `SECTION` | — | Cross-section outline of a solid | ✅ | | `SECTION` | — | Cross-section outline of a solid | ✅ |
| `CONVTOSOLID` | — | Convert to solid | ❌ | | `CONVTOSOLID` | — | Convert to solid | ❌ |
| `CONVTOSURFACE` | — | Convert to surface | ❌ | | `CONVTOSURFACE` | — | Convert to surface | ✅ |
| `SECTIONPLANE` | — | Section plane object | ❌ | | `SECTIONPLANE` | — | Section plane object | ❌ |
| `FLATSHOT` | — | 2D view from 3D | ❌ | | `FLATSHOT` | — | 2D view from 3D | ✅ |
--- ---
@ -357,12 +357,12 @@ Status of every standard CAD command in Open CAD Studio:
| Text & Table | 10 | 7 | 1 | 2 | | Text & Table | 10 | 7 | 1 | 2 |
| Layer | 19 | 17 | 0 | 2 | | Layer | 19 | 17 | 0 | 2 |
| Block & Reference | 23 | 18 | 2 | 3 | | Block & Reference | 23 | 18 | 2 | 3 |
| 3D Modeling | 27 | 23 | 0 | 4 | | 3D Modeling | 27 | 25 | 0 | 2 |
| View & Navigation | 32 | 15 | 8 | 9 | | View & Navigation | 32 | 15 | 8 | 9 |
| Inquiry | 12 | 12 | 0 | 0 | | Inquiry | 12 | 12 | 0 | 0 |
| File & Plot | 17 | 16 | 0 | 1 | | File & Plot | 17 | 16 | 0 | 1 |
| Manage & Customize | 22 | 13 | 3 | 6 | | Manage & Customize | 22 | 13 | 3 | 6 |
| **Total** | **262** | **217** | **15** | **30** | | **Total** | **262** | **219** | **15** | **28** |
> Counts include commands listed under more than one category (e.g. `SLICE`, `HELIX`, > Counts include commands listed under more than one category (e.g. `SLICE`, `HELIX`,
> `MINSERT`, `SUBTRACT`/`UNION`/`INTERSECT` appear in both their 2D and 3D groups). > `MINSERT`, `SUBTRACT`/`UNION`/`INTERSECT` appear in both their 2D and 3D groups).

View file

@ -704,6 +704,16 @@ impl OpenCADStudio {
return Some(self.solid_interfere()); return Some(self.solid_interfere());
} }
// FLATSHOT — flatten the selected solid's edges to 2D lines at Z=0.
"FLATSHOT" => {
return Some(self.solid_flatshot());
}
// CONVTOSURFACE — convert the selected solid(s) to surface entities.
"CONVTOSURFACE" => {
return Some(self.solid_convtosurface());
}
// POLYSOLID <width> <height> — extrude a selected polyline into a // POLYSOLID <width> <height> — extrude a selected polyline into a
// wall-like solid. // wall-like solid.
cmd if cmd == "POLYSOLID" || cmd.starts_with("POLYSOLID ") => { cmd if cmd == "POLYSOLID" || cmd.starts_with("POLYSOLID ") => {

View file

@ -346,6 +346,8 @@ inventory::submit!(crate::command::CommandRegistration {
"REG", "REG",
"ARCHIVE", "ARCHIVE",
"ETRANSMIT", "ETRANSMIT",
"FLATSHOT",
"CONVTOSURFACE",
// Slice/section + interference + press-pull/thicken + 3D transforms + wall/pyramid. // Slice/section + interference + press-pull/thicken + 3D transforms + wall/pyramid.
"SLICE", "SLICE",
"SL", "SL",

View file

@ -748,4 +748,96 @@ impl super::OpenCADStudio {
.push_output(&format!("SPLINEFIT: fit a spline through {n} points.")); .push_output(&format!("SPLINEFIT: fit a spline through {n} points."));
Task::none() Task::none()
} }
/// FLATSHOT — project the selected solid's edges onto the XY plane (Z=0) as
/// Line entities, giving a flattened 2D shot of the model. Reuses the cached
/// solid's edge wires (the same source SECTION uses).
pub(super) fn solid_flatshot(&mut self) -> Task<Message> {
use acadrust::types::Vector3;
use acadrust::Line;
let i = self.active_tab;
let handles: Vec<Handle> = self.tabs[i]
.scene
.selected
.iter()
.copied()
.filter(|h| self.tabs[i].scene.solid_models.contains_key(h))
.collect();
if handles.is_empty() {
self.command_line
.push_error("FLATSHOT: select a solid created this session.");
return Task::none();
}
self.push_undo_snapshot(i, "FLATSHOT");
let mut n = 0usize;
for h in &handles {
let solid = self.tabs[i].scene.solid_models[h].clone();
for w in solid_model::edge_wires(&solid) {
for seg in w.points.windows(2) {
let line = Line::from_points(
Vector3::new(seg[0].x, seg[0].y, 0.0),
Vector3::new(seg[1].x, seg[1].y, 0.0),
);
self.tabs[i].scene.add_entity(EntityType::Line(line));
n += 1;
}
}
}
self.tabs[i].dirty = true;
self.refresh_properties();
self.command_line
.push_output(&format!("FLATSHOT: created {n} projected edge(s) at Z=0."));
Task::none()
}
/// CONVTOSURFACE — convert the selected solid(s) into Surface entities,
/// carrying the solid's edge wires (reuses the cached B-rep's edge wires).
pub(super) fn solid_convtosurface(&mut self) -> Task<Message> {
use acadrust::entities::{Surface, SurfaceKind, Wire as AWire};
use acadrust::types::Vector3;
let i = self.active_tab;
let handles: Vec<Handle> = self.tabs[i]
.scene
.selected
.iter()
.copied()
.filter(|h| self.tabs[i].scene.solid_models.contains_key(h))
.collect();
if handles.is_empty() {
self.command_line
.push_error("CONVTOSURFACE: select a solid created this session.");
return Task::none();
}
let mut surfaces: Vec<Surface> = Vec::new();
for h in &handles {
let solid = self.tabs[i].scene.solid_models[h].clone();
let awires: Vec<AWire> = solid_model::edge_wires(&solid)
.into_iter()
.map(|w| {
let mut aw = AWire::new();
aw.points = w
.points
.iter()
.map(|p| Vector3::new(p.x, p.y, p.z))
.collect();
aw
})
.collect();
let mut surf = Surface::new(SurfaceKind::Generic);
surf.wires = awires;
surf.common.layer = self.tabs[i].active_layer.clone();
surfaces.push(surf);
}
self.push_undo_snapshot(i, "CONVTOSURFACE");
self.tabs[i].scene.erase_entities(&handles);
let n = surfaces.len();
for surf in surfaces {
self.tabs[i].scene.add_entity(EntityType::Surface(surf));
}
self.tabs[i].dirty = true;
self.refresh_properties();
self.command_line
.push_output(&format!("CONVTOSURFACE: converted {n} solid(s) to surface(s)."));
Task::none()
}
} }