forked from 1b-agent/opencadstudio-upstream
Merge PR #1083: implement solid shell workflow
This commit is contained in:
commit
c5503e4758
19 files changed
+851
-115
No files matched your search
Generated
+2
-2
@@ -79,7 +79,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "acadrust"
|
name = "acadrust"
|
||||||
version = "0.5.4"
|
version = "0.5.4"
|
||||||
source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=44cde65#44cde6532ea89a5b3169e5eb829cff44e51a3120"
|
source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=db2eae87f759f724115d5d7a7b3e7c44f9759b64#db2eae87f759f724115d5d7a7b3e7c44f9759b64"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.8.12",
|
"ahash 0.8.12",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -885,7 +885,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "cadkernel"
|
name = "cadkernel"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=256b7c829b6c1d64cd762568902c035eb6af7c31#256b7c829b6c1d64cd762568902c035eb6af7c31"
|
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=20a4dfa75f5a00a4ce28f07a797134810f4006b0#20a4dfa75f5a00a4ce28f07a797134810f4006b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"acadrust",
|
"acadrust",
|
||||||
"cavalier_contours",
|
"cavalier_contours",
|
||||||
|
|||||||
+2
-2
@@ -31,8 +31,8 @@ glam = { version = "0.33", features = ["bytemuck"] }
|
|||||||
rfd = "0.17"
|
rfd = "0.17"
|
||||||
clap = { version = "4", features = ["derive", "string"] }
|
clap = { version = "4", features = ["derive", "string"] }
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "44cde65", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "db2eae87f759f724115d5d7a7b3e7c44f9759b64", features = ["serde"] }
|
||||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "256b7c829b6c1d64cd762568902c035eb6af7c31", features = ["acis", "offset"] }
|
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "20a4dfa75f5a00a4ce28f07a797134810f4006b0", features = ["acis", "offset"] }
|
||||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ bincode = "1.3"
|
|||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
|
|
||||||
# Host adapters convert ObjectId to acadrust handles; kernel handles stay local.
|
# Host adapters convert ObjectId to acadrust handles; kernel handles stay local.
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "44cde65", features = ["serde"], optional = true }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "db2eae87f759f724115d5d7a7b3e7c44f9759b64", features = ["serde"], optional = true }
|
||||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "256b7c829b6c1d64cd762568902c035eb6af7c31", features = ["acis", "offset"], optional = true }
|
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "20a4dfa75f5a00a4ce28f07a797134810f4006b0", features = ["acis", "offset"], optional = true }
|
||||||
ocs_plugin_api = { path = "../ocs_plugin_api", optional = true, features = ["host"] }
|
ocs_plugin_api = { path = "../ocs_plugin_api", optional = true, features = ["host"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ serde = { version = "1", features = ["derive"] }
|
|||||||
# Pulled in only by the `host` feature, which adds the `acadrust`-typed
|
# Pulled in only by the `host` feature, which adds the `acadrust`-typed
|
||||||
# `HostApi` runtime surface. The default crate stays dependency-free so engine
|
# `HostApi` runtime surface. The default crate stays dependency-free so engine
|
||||||
# crates and external tooling can depend on the manifest/ribbon contract cheaply.
|
# crates and external tooling can depend on the manifest/ribbon contract cheaply.
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "44cde65", optional = true, features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "db2eae87f759f724115d5d7a7b3e7c44f9759b64", optional = true, features = ["serde"] }
|
||||||
|
|
||||||
# Runtime IPC and serialization (host feature only).
|
# Runtime IPC and serialization (host feature only).
|
||||||
interprocess = { version = "2", optional = true }
|
interprocess = { version = "2", optional = true }
|
||||||
@@ -38,7 +38,7 @@ serde_json = "1"
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
cargo-lock = "11"
|
cargo-lock = "11"
|
||||||
# acadrust is scanned at build time to generate the embedded type registry.
|
# acadrust is scanned at build time to generate the embedded type registry.
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "44cde65", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "db2eae87f759f724115d5d7a7b3e7c44f9759b64", features = ["serde"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
@@ -8,7 +8,7 @@ publish = false
|
|||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "44cde65", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "db2eae87f759f724115d5d7a7b3e7c44f9759b64", features = ["serde"] }
|
||||||
bincode = "1.3"
|
bincode = "1.3"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
console_error_panic_hook = "0.1"
|
console_error_panic_hook = "0.1"
|
||||||
|
|||||||
@@ -10,4 +10,4 @@ crate-type = ["cdylib"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ocs_plugin_api = { path = "../ocs_plugin_api", features = ["host"] }
|
ocs_plugin_api = { path = "../ocs_plugin_api", features = ["host"] }
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "44cde65", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "db2eae87f759f724115d5d7a7b3e7c44f9759b64", features = ["serde"] }
|
||||||
@@ -2929,6 +2929,14 @@ impl OpenCADStudio {
|
|||||||
self.command_line.push_info(&prompt);
|
self.command_line.push_info(&prompt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
CmdResult::ReportError(msg) => {
|
||||||
|
self.tabs[i].snap_result = None;
|
||||||
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
|
self.command_line.push_error(&msg);
|
||||||
|
if let Some(prompt) = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()) {
|
||||||
|
self.command_line.push_info(&prompt);
|
||||||
|
}
|
||||||
|
}
|
||||||
CmdResult::ReportMeasurementAndDeselect(msg) => {
|
CmdResult::ReportMeasurementAndDeselect(msg) => {
|
||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
self.tabs[i].scene.deselect_all();
|
self.tabs[i].scene.deselect_all();
|
||||||
@@ -4127,6 +4135,19 @@ impl OpenCADStudio {
|
|||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CmdResult::SolidShell {
|
||||||
|
handle,
|
||||||
|
actions,
|
||||||
|
distance,
|
||||||
|
} => {
|
||||||
|
let task = self.solid_shell(handle, &actions, distance);
|
||||||
|
self.tabs[i].active_cmd = None;
|
||||||
|
self.tabs[i].snap_result = None;
|
||||||
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
|
self.restore_pre_cmd_tangent();
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
CmdResult::SolidSubtract {
|
CmdResult::SolidSubtract {
|
||||||
bases,
|
bases,
|
||||||
cutters,
|
cutters,
|
||||||
|
|||||||
@@ -971,6 +971,30 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"SHELL" | "SOLIDEDIT" => {
|
||||||
|
use crate::modules::model::shell_cmd::ShellCommand;
|
||||||
|
let selected = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.selected_handles_in_order()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
|
||||||
|
.filter(|handle| {
|
||||||
|
matches!(
|
||||||
|
self.tabs[i].scene.document.get_entity(*handle),
|
||||||
|
Some(acadrust::EntityType::Solid3D(_))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let target = (selected.len() == 1).then_some(selected[0]);
|
||||||
|
let new_cmd = if cmd == "SHELL" {
|
||||||
|
ShellCommand::direct(target)
|
||||||
|
} else {
|
||||||
|
ShellCommand::solid_edit(target)
|
||||||
|
};
|
||||||
|
self.command_line.push_info(&new_cmd.prompt());
|
||||||
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
|
}
|
||||||
|
|
||||||
// ── Solid booleans ─────────────────────────────────────────────
|
// ── Solid booleans ─────────────────────────────────────────────
|
||||||
"UNION" => {
|
"UNION" => {
|
||||||
use crate::modules::model::boolean_cmd::BoolOp;
|
use crate::modules::model::boolean_cmd::BoolOp;
|
||||||
|
|||||||
+87
-1
@@ -982,7 +982,7 @@ impl OpenCADStudio {
|
|||||||
"area":planar.curve.is_closed().then(|| planar.curve.enclosed_area().abs())
|
"area":planar.curve.is_closed().then(|| planar.curve.enclosed_area().abs())
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
out.push(json!({"handle":format!("{:X}",handle.value()),"type":crate::entities::names::ui_name(entity),"bounds":{"min":[bb.min.x,bb.min.y,bb.min.z],"max":[bb.max.x,bb.max.y,bb.max.z]},"curve":curve,"mesh":metrics.map(|m|json!({"vertices":m.metrics.vertices,"triangles":m.metrics.triangles,"surface_area":m.metrics.surface_area,"volume":m.metrics.volume,"centroid":m.metrics.centroid}))}));
|
out.push(json!({"handle":format!("{:X}",handle.value()),"type":crate::entities::names::ui_name(entity),"bounds":{"min":[bb.min.x,bb.min.y,bb.min.z],"max":[bb.max.x,bb.max.y,bb.max.z]},"curve":curve,"mesh":metrics.map(|m|json!({"vertices":m.metrics.vertices,"triangles":m.metrics.triangles,"surface_area":m.metrics.surface_area,"volume":m.metrics.volume,"centroid":m.metrics.centroid,"moment_of_inertia":m.metrics.moment_of_inertia,"principal_directions":m.metrics.principal_directions,"principal_moments":m.metrics.principal_moments,"product_of_inertia":m.metrics.product_of_inertia,"radii_of_gyration":m.metrics.radii_of_gyration}))}));
|
||||||
}
|
}
|
||||||
json!({"ok":true,"document_id":tab.id,"geometry_revision":tab.scene.geometry_epoch,"measurements":out})
|
json!({"ok":true,"document_id":tab.id,"geometry_revision":tab.scene.geometry_epoch,"measurements":out})
|
||||||
}
|
}
|
||||||
@@ -1222,6 +1222,92 @@ mod tests {
|
|||||||
assert!((curve["length"].as_f64().unwrap() - std::f64::consts::TAU * 2.0).abs() < 1e-9);
|
assert!((curve["length"].as_f64().unwrap() - std::f64::consts::TAU * 2.0).abs() < 1e-9);
|
||||||
assert!((curve["area"].as_f64().unwrap() - std::f64::consts::PI * 4.0).abs() < 1e-9);
|
assert!((curve["area"].as_f64().unwrap() - std::f64::consts::PI * 4.0).abs() < 1e-9);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn control_shell_roundtrips_with_kernel_mass_properties() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
request(&mut app, json!({"op":"new"}));
|
||||||
|
request(&mut app, json!({"op":"start","cmd":"BOX"}));
|
||||||
|
request(
|
||||||
|
&mut app,
|
||||||
|
json!({"op":"input","kind":"point","point":[0.0,0.0,0.0]}),
|
||||||
|
);
|
||||||
|
request(
|
||||||
|
&mut app,
|
||||||
|
json!({"op":"input","kind":"point","point":[6.0,5.0,0.0]}),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
request(&mut app, json!({"op":"input","kind":"token","text":"4"}))["status"],
|
||||||
|
"completed"
|
||||||
|
);
|
||||||
|
let handle = app.tabs[app.active_tab]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.entities()
|
||||||
|
.find_map(|entity| {
|
||||||
|
matches!(entity, acadrust::EntityType::Solid3D(_)).then(|| entity.common().handle)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let handle_text = format!("{:X}", handle.value());
|
||||||
|
|
||||||
|
request(&mut app, json!({"op":"select","handles":[handle_text]}));
|
||||||
|
request(&mut app, json!({"op":"start","cmd":"SHELL"}));
|
||||||
|
request(
|
||||||
|
&mut app,
|
||||||
|
json!({"op":"input","kind":"entity","handle":handle_text,"point":[3.0,2.5,4.0]}),
|
||||||
|
);
|
||||||
|
request(&mut app, json!({"op":"input","kind":"enter"}));
|
||||||
|
assert_eq!(
|
||||||
|
request(&mut app, json!({"op":"input","kind":"token","text":"0.5"}))["status"],
|
||||||
|
"completed"
|
||||||
|
);
|
||||||
|
|
||||||
|
let measured = app.control_measure(&json!({"handles":[handle_text]}));
|
||||||
|
let mesh = &measured["measurements"][0]["mesh"];
|
||||||
|
assert!((mesh["volume"].as_f64().unwrap() - 50.0).abs() < 1e-9);
|
||||||
|
assert_eq!(mesh["principal_moments"].as_array().unwrap().len(), 3);
|
||||||
|
assert!(matches!(
|
||||||
|
app.tabs[app.active_tab]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.solid_history_operation(handle),
|
||||||
|
Some(acadrust::objects::SolidHistoryOperation::Brep(_))
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(request(&mut app, json!({"op":"undo"}))["ok"], true);
|
||||||
|
let restored = app.control_measure(&json!({"handles":[handle_text]}));
|
||||||
|
assert!(
|
||||||
|
(restored["measurements"][0]["mesh"]["volume"]
|
||||||
|
.as_f64()
|
||||||
|
.unwrap()
|
||||||
|
- 120.0)
|
||||||
|
.abs()
|
||||||
|
< 1e-9
|
||||||
|
);
|
||||||
|
assert_eq!(request(&mut app, json!({"op":"redo"}))["ok"], true);
|
||||||
|
|
||||||
|
let path = std::env::temp_dir().join(format!("ocs-shell-{}.dwg", session_id()));
|
||||||
|
assert_eq!(
|
||||||
|
request(&mut app, json!({"op":"save","path":path}))["ok"],
|
||||||
|
true
|
||||||
|
);
|
||||||
|
request(&mut app, json!({"op":"new"}));
|
||||||
|
assert_eq!(
|
||||||
|
request(&mut app, json!({"op":"open","path":path}))["status"],
|
||||||
|
"completed"
|
||||||
|
);
|
||||||
|
let reopened = app.control_measure(&json!({"handles":[handle_text]}));
|
||||||
|
assert!(
|
||||||
|
(reopened["measurements"][0]["mesh"]["volume"]
|
||||||
|
.as_f64()
|
||||||
|
.unwrap()
|
||||||
|
- 50.0)
|
||||||
|
.abs()
|
||||||
|
< 1e-9
|
||||||
|
);
|
||||||
|
let _ = std::fs::remove_file(path);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn control_retry_is_idempotent_and_stale_state_rejected() {
|
fn control_retry_is_idempotent_and_stale_state_rejected() {
|
||||||
let mut app = OpenCADStudio::new_for_test();
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
|||||||
+138
-1
@@ -9,7 +9,7 @@ use acadrust::{
|
|||||||
};
|
};
|
||||||
use cadkernel::brep::{Body, EdgeKey, FaceKey};
|
use cadkernel::brep::{Body, EdgeKey, FaceKey};
|
||||||
use iced::Task;
|
use iced::Task;
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use super::Message;
|
use super::Message;
|
||||||
use crate::modules::model::boolean_cmd::BoolOp;
|
use crate::modules::model::boolean_cmd::BoolOp;
|
||||||
@@ -861,6 +861,143 @@ impl super::OpenCADStudio {
|
|||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn solid_shell(
|
||||||
|
&mut self,
|
||||||
|
handle: Handle,
|
||||||
|
actions: &[crate::command::ShellFaceAction],
|
||||||
|
distance: f64,
|
||||||
|
) -> Task<Message> {
|
||||||
|
use crate::command::ShellFaceAction;
|
||||||
|
|
||||||
|
let i = self.active_tab;
|
||||||
|
if self.reject_locked_edit(i, handle) {
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
if !matches!(
|
||||||
|
self.tabs[i].scene.document.get_entity(handle),
|
||||||
|
Some(EntityType::Solid3D(_))
|
||||||
|
) {
|
||||||
|
self.command_line
|
||||||
|
.push_error(crate::t!("Select a 3D solid.").as_ref());
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
if !distance.is_finite() || distance.abs() <= f64::EPSILON {
|
||||||
|
self.command_line
|
||||||
|
.push_error(crate::t!("The shell offset distance must be nonzero.").as_ref());
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tabs[i].scene.restore_solid_models(&[handle]);
|
||||||
|
let Some(body) = self.tabs[i].scene.solid_models.get(&handle).cloned() else {
|
||||||
|
self.command_line
|
||||||
|
.push_error(crate::t!("The solid geometry could not be restored.").as_ref());
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
|
||||||
|
let all_faces = body.face_keys().collect::<Vec<_>>();
|
||||||
|
let mut removed = HashSet::new();
|
||||||
|
for action in actions {
|
||||||
|
match *action {
|
||||||
|
ShellFaceAction::Remove(point) => {
|
||||||
|
let Some(face) = cadkernel::brep::shell_face_at_point(&body, point.to_array())
|
||||||
|
else {
|
||||||
|
self.command_line.push_error(
|
||||||
|
crate::t!("A selected face could not be resolved.").as_ref(),
|
||||||
|
);
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
removed.insert(face);
|
||||||
|
}
|
||||||
|
ShellFaceAction::Add(point) => {
|
||||||
|
let Some(face) = cadkernel::brep::shell_face_at_point(&body, point.to_array())
|
||||||
|
else {
|
||||||
|
self.command_line.push_error(
|
||||||
|
crate::t!("A selected face could not be resolved.").as_ref(),
|
||||||
|
);
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
removed.remove(&face);
|
||||||
|
}
|
||||||
|
ShellFaceAction::RemoveAll => removed.extend(all_faces.iter().copied()),
|
||||||
|
ShellFaceAction::AddAll => removed.clear(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let removed = removed.into_iter().collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let result = match cadkernel::brep::shell(&body, &removed, distance) {
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(cadkernel::brep::ShellError::InvalidDistance) => {
|
||||||
|
self.command_line
|
||||||
|
.push_error(crate::t!("The shell offset distance must be nonzero.").as_ref());
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
Err(cadkernel::brep::ShellError::UnsupportedSolid) => {
|
||||||
|
self.command_line.push_error(
|
||||||
|
crate::t!("SHELL supports rectangular boxes, circular cylinders, and spheres.")
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
Err(cadkernel::brep::ShellError::UnknownFace) => {
|
||||||
|
self.command_line
|
||||||
|
.push_error(crate::t!("A selected face does not belong to the solid.").as_ref());
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
Err(cadkernel::brep::ShellError::NoMaterial) => {
|
||||||
|
self.command_line.push_error(
|
||||||
|
crate::t!("The offset removes all material. Use a smaller distance or add a face back.")
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
Err(cadkernel::brep::ShellError::Kernel(_)) => {
|
||||||
|
self.command_line.push_error(
|
||||||
|
crate::t!("The shell could not be constructed from the selected solid and distance.")
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(source) = self.tabs[i].scene.document.get_entity(handle).cloned() else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
let Some(entity) = entity_with_boolean_body(source, &result) else {
|
||||||
|
self.command_line
|
||||||
|
.push_error(crate::t!("The result could not be encoded as ACIS.").as_ref());
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
let Some(display) = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.prepare_solid_model_display(handle, &result)
|
||||||
|
.filter(|display| display.0.complete)
|
||||||
|
else {
|
||||||
|
self.command_line.push_error(crate::t!("The result could not be displayed completely. The original solid was retained.").as_ref());
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
|
||||||
|
self.push_undo_snapshot(i, "SHELL");
|
||||||
|
self.tabs[i].scene.delete_solid_history(handle);
|
||||||
|
if !self.tabs[i].scene.update_entity(entity) {
|
||||||
|
self.command_line
|
||||||
|
.push_error(crate::t!("The solid could not be updated.").as_ref());
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.create_solid_history(handle, solid_history::brep_op(&result));
|
||||||
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.register_prepared_solid_model(handle, result, display);
|
||||||
|
self.tabs[i].scene.deselect_all();
|
||||||
|
self.tabs[i].scene.select_entity(handle, false);
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
self.refresh_properties();
|
||||||
|
self.command_line
|
||||||
|
.push_output(crate::t!("SHELL: solid updated.").as_ref());
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
/// Intersect compatible selected solids, Regions, and coplanar Surfaces.
|
/// Intersect compatible selected solids, Regions, and coplanar Surfaces.
|
||||||
pub(super) fn solid_intersect(&mut self) -> Task<Message> {
|
pub(super) fn solid_intersect(&mut self) -> Task<Message> {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
|
|||||||
+77
-34
@@ -636,7 +636,7 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !compact_solid && matches!(
|
if matches!(
|
||||||
entity,
|
entity,
|
||||||
acadrust::EntityType::Solid3D(_)
|
acadrust::EntityType::Solid3D(_)
|
||||||
| acadrust::EntityType::Region(_)
|
| acadrust::EntityType::Region(_)
|
||||||
@@ -654,38 +654,44 @@ impl OpenCADStudio {
|
|||||||
{
|
{
|
||||||
use crate::entities::common::ro_prop;
|
use crate::entities::common::ro_prop;
|
||||||
let metrics = mesh.metrics;
|
let metrics = mesh.metrics;
|
||||||
let mut props = vec![
|
let triple = |values: [f64; 3]| {
|
||||||
ro_prop(
|
format!("{:.6}, {:.6}, {:.6}", values[0], values[1], values[2])
|
||||||
t!("Vertices").as_ref(),
|
};
|
||||||
"mesh_vertices",
|
let mut props = if compact_solid {
|
||||||
metrics.vertices.to_string(),
|
vec![ro_prop(
|
||||||
),
|
|
||||||
ro_prop(
|
|
||||||
t!("Triangles").as_ref(),
|
|
||||||
"mesh_triangles",
|
|
||||||
metrics.triangles.to_string(),
|
|
||||||
),
|
|
||||||
ro_prop(
|
|
||||||
t!("Surface Area").as_ref(),
|
|
||||||
"mesh_surface_area",
|
|
||||||
format!("{:.6}", metrics.surface_area),
|
|
||||||
),
|
|
||||||
ro_prop(
|
|
||||||
t!("Centroid").as_ref(),
|
t!("Centroid").as_ref(),
|
||||||
"mesh_centroid",
|
"mesh_centroid",
|
||||||
format!(
|
triple(metrics.centroid),
|
||||||
"{:.6}, {:.6}, {:.6}",
|
)]
|
||||||
metrics.centroid[0],
|
} else {
|
||||||
metrics.centroid[1],
|
vec![
|
||||||
metrics.centroid[2]
|
ro_prop(
|
||||||
|
t!("Vertices").as_ref(),
|
||||||
|
"mesh_vertices",
|
||||||
|
metrics.vertices.to_string(),
|
||||||
),
|
),
|
||||||
),
|
ro_prop(
|
||||||
ro_prop(
|
t!("Triangles").as_ref(),
|
||||||
t!("Tessellation").as_ref(),
|
"mesh_triangles",
|
||||||
"mesh_complete",
|
metrics.triangles.to_string(),
|
||||||
if mesh.complete { "Complete" } else { "Partial" },
|
),
|
||||||
),
|
ro_prop(
|
||||||
];
|
t!("Surface Area").as_ref(),
|
||||||
|
"mesh_surface_area",
|
||||||
|
format!("{:.6}", metrics.surface_area),
|
||||||
|
),
|
||||||
|
ro_prop(
|
||||||
|
t!("Centroid").as_ref(),
|
||||||
|
"mesh_centroid",
|
||||||
|
triple(metrics.centroid),
|
||||||
|
),
|
||||||
|
ro_prop(
|
||||||
|
t!("Tessellation").as_ref(),
|
||||||
|
"mesh_complete",
|
||||||
|
if mesh.complete { "Complete" } else { "Partial" },
|
||||||
|
),
|
||||||
|
]
|
||||||
|
};
|
||||||
if mesh.complete
|
if mesh.complete
|
||||||
&& matches!(
|
&& matches!(
|
||||||
entity,
|
entity,
|
||||||
@@ -694,14 +700,50 @@ impl OpenCADStudio {
|
|||||||
| acadrust::EntityType::Body(_)
|
| acadrust::EntityType::Body(_)
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
props.insert(
|
let closed_props = vec![
|
||||||
3,
|
ro_prop(
|
||||||
|
t!("Moment of inertia").as_ref(),
|
||||||
|
"mesh_moment_of_inertia",
|
||||||
|
triple(metrics.moment_of_inertia),
|
||||||
|
),
|
||||||
|
ro_prop(
|
||||||
|
t!("Principal directions").as_ref(),
|
||||||
|
"mesh_principal_directions",
|
||||||
|
metrics
|
||||||
|
.principal_directions
|
||||||
|
.chunks_exact(3)
|
||||||
|
.map(|axis| format!("({:.6}, {:.6}, {:.6})", axis[0], axis[1], axis[2]))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", "),
|
||||||
|
),
|
||||||
|
ro_prop(
|
||||||
|
t!("Principal moments").as_ref(),
|
||||||
|
"mesh_principal_moments",
|
||||||
|
triple(metrics.principal_moments),
|
||||||
|
),
|
||||||
|
ro_prop(
|
||||||
|
t!("Product of inertia").as_ref(),
|
||||||
|
"mesh_product_of_inertia",
|
||||||
|
triple(metrics.product_of_inertia),
|
||||||
|
),
|
||||||
|
ro_prop(
|
||||||
|
t!("Radii of gyration").as_ref(),
|
||||||
|
"mesh_radii_of_gyration",
|
||||||
|
triple(metrics.radii_of_gyration),
|
||||||
|
),
|
||||||
ro_prop(
|
ro_prop(
|
||||||
t!("Volume").as_ref(),
|
t!("Volume").as_ref(),
|
||||||
"mesh_volume",
|
"mesh_volume",
|
||||||
format!("{:.6}", metrics.volume),
|
format!("{:.6}", metrics.volume),
|
||||||
),
|
),
|
||||||
);
|
];
|
||||||
|
if compact_solid {
|
||||||
|
props.extend(closed_props);
|
||||||
|
} else {
|
||||||
|
let volume = closed_props.last().cloned().unwrap();
|
||||||
|
props.insert(3, volume);
|
||||||
|
props.extend(closed_props.into_iter().take(5));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sections.push(crate::scene::model::object::PropSection {
|
sections.push(crate::scene::model::object::PropSection {
|
||||||
title: t!("Mass Properties").into_owned(),
|
title: t!("Mass Properties").into_owned(),
|
||||||
@@ -3093,7 +3135,8 @@ fn retain_compact_solid_sections(
|
|||||||
| "transparency"
|
| "transparency"
|
||||||
| "hyperlink"
|
| "hyperlink"
|
||||||
| "material"
|
| "material"
|
||||||
) || crate::scene::model::solid_history::is_specialized_property(property.field)
|
) || property.field.starts_with("mesh_")
|
||||||
|
|| crate::scene::model::solid_history::is_specialized_property(property.field)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
sections.retain(|section| !section.props.is_empty());
|
sections.retain(|section| !section.props.is_empty());
|
||||||
|
|||||||
@@ -1195,6 +1195,15 @@ pub struct LoftOptions {
|
|||||||
pub align_direction: bool,
|
pub align_direction: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One face-selection edit made while collecting a solid shell operation.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum ShellFaceAction {
|
||||||
|
Remove(DVec3),
|
||||||
|
Add(DVec3),
|
||||||
|
RemoveAll,
|
||||||
|
AddAll,
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for LoftOptions {
|
impl Default for LoftOptions {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -1374,6 +1383,8 @@ pub enum CmdResult {
|
|||||||
Measurement(String),
|
Measurement(String),
|
||||||
/// Print a measurement result and keep the command active.
|
/// Print a measurement result and keep the command active.
|
||||||
ReportMeasurement(String),
|
ReportMeasurement(String),
|
||||||
|
/// Print an input error and keep the command active.
|
||||||
|
ReportError(String),
|
||||||
/// Print a measurement result, clear the current selection, and keep the command active.
|
/// Print a measurement result, clear the current selection, and keep the command active.
|
||||||
ReportMeasurementAndDeselect(String),
|
ReportMeasurementAndDeselect(String),
|
||||||
/// Clear the current selection and keep the command active at its updated step.
|
/// Clear the current selection and keep the command active at its updated step.
|
||||||
@@ -1556,6 +1567,12 @@ pub enum CmdResult {
|
|||||||
other_value: f64,
|
other_value: f64,
|
||||||
fillet: bool,
|
fillet: bool,
|
||||||
},
|
},
|
||||||
|
/// Offset a solid and optionally open selected faces.
|
||||||
|
SolidShell {
|
||||||
|
handle: Handle,
|
||||||
|
actions: Vec<ShellFaceAction>,
|
||||||
|
distance: f64,
|
||||||
|
},
|
||||||
SolidSubtract {
|
SolidSubtract {
|
||||||
bases: Vec<Handle>,
|
bases: Vec<Handle>,
|
||||||
cutters: Vec<Handle>,
|
cutters: Vec<Handle>,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ pub mod cylinder_cmd;
|
|||||||
pub mod edge_cmd;
|
pub mod edge_cmd;
|
||||||
pub mod polysolid_cmd;
|
pub mod polysolid_cmd;
|
||||||
pub mod primitive_cmd;
|
pub mod primitive_cmd;
|
||||||
|
pub mod shell_cmd;
|
||||||
pub mod slice_cmd;
|
pub mod slice_cmd;
|
||||||
|
|
||||||
use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, ToolDef};
|
use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, ToolDef};
|
||||||
@@ -91,6 +92,7 @@ impl CadModule for ModelModule {
|
|||||||
tools: vec![
|
tools: vec![
|
||||||
RibbonItem::LargeTool(tool("FILLETEDGE", "Fillet Edge", FILLET_ICON)),
|
RibbonItem::LargeTool(tool("FILLETEDGE", "Fillet Edge", FILLET_ICON)),
|
||||||
RibbonItem::LargeTool(tool("CHAMFEREDGE", "Chamfer", CHAMFER_ICON)),
|
RibbonItem::LargeTool(tool("CHAMFEREDGE", "Chamfer", CHAMFER_ICON)),
|
||||||
|
RibbonItem::LargeTool(tool("SHELL", "Shell", PRESSPULL_ICON)),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,308 @@
|
|||||||
|
use acadrust::Handle;
|
||||||
|
use glam::DVec3;
|
||||||
|
|
||||||
|
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind, ShellFaceAction};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
enum ShellStep {
|
||||||
|
SolidEditOption,
|
||||||
|
BodyOption,
|
||||||
|
SelectSolid,
|
||||||
|
RemoveFaces,
|
||||||
|
AddFaces,
|
||||||
|
Distance,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ShellCommand {
|
||||||
|
entry_name: &'static str,
|
||||||
|
step: ShellStep,
|
||||||
|
target: Option<Handle>,
|
||||||
|
actions: Vec<ShellFaceAction>,
|
||||||
|
default_distance: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShellCommand {
|
||||||
|
pub fn direct(target: Option<Handle>) -> Self {
|
||||||
|
Self {
|
||||||
|
entry_name: "SHELL",
|
||||||
|
step: if target.is_some() {
|
||||||
|
ShellStep::RemoveFaces
|
||||||
|
} else {
|
||||||
|
ShellStep::SelectSolid
|
||||||
|
},
|
||||||
|
target,
|
||||||
|
actions: Vec::new(),
|
||||||
|
default_distance: 1.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn solid_edit(target: Option<Handle>) -> Self {
|
||||||
|
Self {
|
||||||
|
entry_name: "SOLIDEDIT",
|
||||||
|
step: ShellStep::SolidEditOption,
|
||||||
|
target,
|
||||||
|
actions: Vec::new(),
|
||||||
|
default_distance: 1.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(&mut self, distance: f64) -> CmdResult {
|
||||||
|
let Some(handle) = self.target else {
|
||||||
|
return CmdResult::Cancel;
|
||||||
|
};
|
||||||
|
CmdResult::SolidShell {
|
||||||
|
handle,
|
||||||
|
actions: std::mem::take(&mut self.actions),
|
||||||
|
distance,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn undo_face_action(&mut self) -> CmdResult {
|
||||||
|
self.actions.pop();
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
fn face_options(add: bool) -> Vec<CmdOption> {
|
||||||
|
if add {
|
||||||
|
vec![
|
||||||
|
CmdOption::new("Undo", "UNDO"),
|
||||||
|
CmdOption::new("Remove", "REMOVE"),
|
||||||
|
CmdOption::new("ALL", "ALL"),
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
vec![
|
||||||
|
CmdOption::new("Undo", "UNDO"),
|
||||||
|
CmdOption::new("Add", "ADD"),
|
||||||
|
CmdOption::new("ALL", "ALL"),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CadCommand for ShellCommand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
self.entry_name
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt(&self) -> String {
|
||||||
|
match self.step {
|
||||||
|
ShellStep::SolidEditOption => {
|
||||||
|
crate::t!("Enter a solid editing option [Body/eXit]:").into_owned()
|
||||||
|
}
|
||||||
|
ShellStep::BodyOption => {
|
||||||
|
crate::t!("Enter a body editing option [Shell/eXit]:").into_owned()
|
||||||
|
}
|
||||||
|
ShellStep::SelectSolid => crate::t!("Select a 3D solid:").into_owned(),
|
||||||
|
ShellStep::RemoveFaces => {
|
||||||
|
crate::t!("Select faces to remove [Undo/Add/ALL] <done>:").into_owned()
|
||||||
|
}
|
||||||
|
ShellStep::AddFaces => {
|
||||||
|
crate::t!("Select faces to add [Undo/Remove/ALL] <done>:").into_owned()
|
||||||
|
}
|
||||||
|
ShellStep::Distance => crate::tf!(
|
||||||
|
"Specify the shell offset distance <{:.3}>:",
|
||||||
|
self.default_distance
|
||||||
|
)
|
||||||
|
.into_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
|
match self.step {
|
||||||
|
ShellStep::SolidEditOption => vec![
|
||||||
|
CmdOption::new("Body", "BODY"),
|
||||||
|
CmdOption::new("eXit", "EXIT"),
|
||||||
|
],
|
||||||
|
ShellStep::BodyOption => vec![
|
||||||
|
CmdOption::new("Shell", "SHELL"),
|
||||||
|
CmdOption::new("eXit", "EXIT"),
|
||||||
|
],
|
||||||
|
ShellStep::RemoveFaces => Self::face_options(false),
|
||||||
|
ShellStep::AddFaces => Self::face_options(true),
|
||||||
|
_ => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn needs_entity_pick(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self.step,
|
||||||
|
ShellStep::SelectSolid | ShellStep::RemoveFaces | ShellStep::AddFaces
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entity_pick_includes_fills(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entity_pick_uses_surface_point(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entity_pick_highlights_hover(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult {
|
||||||
|
if handle.is_null() {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
}
|
||||||
|
match self.step {
|
||||||
|
ShellStep::SelectSolid => {
|
||||||
|
self.target = Some(handle);
|
||||||
|
self.step = ShellStep::RemoveFaces;
|
||||||
|
}
|
||||||
|
ShellStep::RemoveFaces if self.target == Some(handle) => {
|
||||||
|
self.actions.push(ShellFaceAction::Remove(point));
|
||||||
|
}
|
||||||
|
ShellStep::AddFaces if self.target == Some(handle) => {
|
||||||
|
self.actions.push(ShellFaceAction::Add(point));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_point(&mut self, _point: DVec3) -> CmdResult {
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
|
let value = text.trim();
|
||||||
|
let keyword = value.to_ascii_uppercase();
|
||||||
|
let result = match self.step {
|
||||||
|
ShellStep::SolidEditOption => match keyword.as_str() {
|
||||||
|
"B" | "BODY" => {
|
||||||
|
self.step = ShellStep::BodyOption;
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
"X" | "EXIT" => CmdResult::Cancel,
|
||||||
|
_ => return Some(CmdResult::NeedPoint),
|
||||||
|
},
|
||||||
|
ShellStep::BodyOption => match keyword.as_str() {
|
||||||
|
"S" | "SHELL" => {
|
||||||
|
self.step = if self.target.is_some() {
|
||||||
|
ShellStep::RemoveFaces
|
||||||
|
} else {
|
||||||
|
ShellStep::SelectSolid
|
||||||
|
};
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
"X" | "EXIT" => CmdResult::Cancel,
|
||||||
|
_ => return Some(CmdResult::NeedPoint),
|
||||||
|
},
|
||||||
|
ShellStep::RemoveFaces => match keyword.as_str() {
|
||||||
|
"U" | "UNDO" => return Some(self.undo_face_action()),
|
||||||
|
"A" | "ADD" => {
|
||||||
|
self.step = ShellStep::AddFaces;
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
"ALL" => {
|
||||||
|
self.actions.push(ShellFaceAction::RemoveAll);
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
_ => return Some(CmdResult::NeedPoint),
|
||||||
|
},
|
||||||
|
ShellStep::AddFaces => match keyword.as_str() {
|
||||||
|
"U" | "UNDO" => return Some(self.undo_face_action()),
|
||||||
|
"R" | "REMOVE" => {
|
||||||
|
self.step = ShellStep::RemoveFaces;
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
"ALL" => {
|
||||||
|
self.actions.push(ShellFaceAction::AddAll);
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
_ => return Some(CmdResult::NeedPoint),
|
||||||
|
},
|
||||||
|
ShellStep::Distance => {
|
||||||
|
let Ok(distance) = value.parse::<f64>() else {
|
||||||
|
return Some(CmdResult::ReportError(
|
||||||
|
crate::t!("A numeric shell offset distance is required.").into_owned(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
if !distance.is_finite() || distance.abs() <= f64::EPSILON {
|
||||||
|
return Some(CmdResult::ReportError(
|
||||||
|
crate::t!("Value must be nonzero.").into_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.default_distance = distance;
|
||||||
|
self.finish(distance)
|
||||||
|
}
|
||||||
|
ShellStep::SelectSolid => return None,
|
||||||
|
};
|
||||||
|
Some(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
|
match self.step {
|
||||||
|
ShellStep::RemoveFaces | ShellStep::AddFaces => {
|
||||||
|
self.step = ShellStep::Distance;
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
ShellStep::Distance => self.finish(self.default_distance),
|
||||||
|
_ => CmdResult::Cancel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_undo_step(&mut self) -> Option<CmdResult> {
|
||||||
|
matches!(self.step, ShellStep::RemoveFaces | ShellStep::AddFaces)
|
||||||
|
.then(|| self.undo_face_action())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn input_kind(&self) -> InputKind {
|
||||||
|
if matches!(self.step, ShellStep::SelectSolid) {
|
||||||
|
InputKind::Point
|
||||||
|
} else {
|
||||||
|
InputKind::SingleToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inventory::submit!(crate::command::CommandRegistration {
|
||||||
|
names: &["SHELL", "SOLIDEDIT"]
|
||||||
|
});
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn direct_shell_collects_face_edits_and_signed_distance() {
|
||||||
|
let handle = Handle::new(42);
|
||||||
|
let first = DVec3::new(1.0, 2.0, 3.0);
|
||||||
|
let second = DVec3::new(4.0, 5.0, 6.0);
|
||||||
|
let mut command = ShellCommand::direct(Some(handle));
|
||||||
|
|
||||||
|
command.on_entity_pick(handle, first);
|
||||||
|
command.on_text_input("ADD");
|
||||||
|
command.on_entity_pick(handle, second);
|
||||||
|
command.on_text_input("UNDO");
|
||||||
|
command.on_text_input("ALL");
|
||||||
|
assert!(matches!(command.on_enter(), CmdResult::NeedPoint));
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
command.on_text_input("-0.5"),
|
||||||
|
Some(CmdResult::SolidShell {
|
||||||
|
handle: result_handle,
|
||||||
|
actions,
|
||||||
|
distance: -0.5,
|
||||||
|
}) if result_handle == handle
|
||||||
|
&& actions == vec![ShellFaceAction::Remove(first), ShellFaceAction::AddAll]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_distance_keeps_the_distance_step_active() {
|
||||||
|
let mut command = ShellCommand::direct(Some(Handle::new(7)));
|
||||||
|
command.on_enter();
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
command.on_text_input("zero"),
|
||||||
|
Some(CmdResult::ReportError(_))
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
command.on_text_input("0.25"),
|
||||||
|
Some(CmdResult::SolidShell { distance: 0.25, .. })
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -177,6 +177,11 @@ pub fn tessellate_sat(
|
|||||||
color,
|
color,
|
||||||
None,
|
None,
|
||||||
));
|
));
|
||||||
|
if let [single] = bodies.as_slice() {
|
||||||
|
if let Some(properties) = brep::analytic_mass_properties(&single.0) {
|
||||||
|
set.apply_mass_properties(properties);
|
||||||
|
}
|
||||||
|
}
|
||||||
set.curved_gens = curved_gens;
|
set.curved_gens = curved_gens;
|
||||||
for point in edges {
|
for point in edges {
|
||||||
let high = [point[0] as f32, point[1] as f32, point[2] as f32];
|
let high = [point[0] as f32, point[1] as f32, point[2] as f32];
|
||||||
|
|||||||
@@ -55,6 +55,44 @@ pub struct MeshMetrics {
|
|||||||
pub surface_area: f64,
|
pub surface_area: f64,
|
||||||
pub volume: f64,
|
pub volume: f64,
|
||||||
pub centroid: [f64; 3],
|
pub centroid: [f64; 3],
|
||||||
|
pub moment_of_inertia: [f64; 3],
|
||||||
|
pub principal_directions: [f64; 9],
|
||||||
|
pub principal_moments: [f64; 3],
|
||||||
|
pub product_of_inertia: [f64; 3],
|
||||||
|
pub radii_of_gyration: [f64; 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MeshMetrics {
|
||||||
|
fn apply_mass_properties(&mut self, properties: cadkernel::brep::MassProperties) {
|
||||||
|
self.volume = properties.volume;
|
||||||
|
self.centroid = properties.centroid;
|
||||||
|
self.moment_of_inertia = properties.moment_of_inertia;
|
||||||
|
self.principal_directions = properties.principal_directions;
|
||||||
|
self.principal_moments = properties.principal_moments;
|
||||||
|
self.product_of_inertia = properties.product_of_inertia;
|
||||||
|
self.radii_of_gyration = properties.radii_of_gyration;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn translate(&mut self, delta: [f64; 3]) {
|
||||||
|
if self.volume > 1e-18 {
|
||||||
|
self.apply_mass_properties(
|
||||||
|
cadkernel::brep::MassProperties {
|
||||||
|
volume: self.volume,
|
||||||
|
centroid: self.centroid,
|
||||||
|
moment_of_inertia: self.moment_of_inertia,
|
||||||
|
principal_directions: self.principal_directions,
|
||||||
|
principal_moments: self.principal_moments,
|
||||||
|
product_of_inertia: self.product_of_inertia,
|
||||||
|
radii_of_gyration: self.radii_of_gyration,
|
||||||
|
}
|
||||||
|
.translated(delta),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
for axis in 0..3 {
|
||||||
|
self.centroid[axis] += delta[axis];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -146,74 +184,52 @@ fn compute_mesh_metrics(lods: &[MeshModel]) -> MeshMetrics {
|
|||||||
let Some(mesh) = lods.iter().find(|mesh| !mesh.indices.is_empty()) else {
|
let Some(mesh) = lods.iter().find(|mesh| !mesh.indices.is_empty()) else {
|
||||||
return MeshMetrics::default();
|
return MeshMetrics::default();
|
||||||
};
|
};
|
||||||
let point = |index: u32| {
|
let kernel_mesh = cadkernel::brep::Mesh {
|
||||||
let index = index as usize;
|
positions: mesh
|
||||||
let high = mesh.verts.get(index).copied().unwrap_or([0.0; 3]);
|
.verts
|
||||||
let low = mesh.verts_low.get(index).copied().unwrap_or([0.0; 3]);
|
.iter()
|
||||||
[
|
.enumerate()
|
||||||
high[0] as f64 + low[0] as f64,
|
.map(|(index, high)| {
|
||||||
high[1] as f64 + low[1] as f64,
|
let low = mesh.verts_low.get(index).copied().unwrap_or([0.0; 3]);
|
||||||
high[2] as f64 + low[2] as f64,
|
[
|
||||||
]
|
high[0] as f64 + low[0] as f64,
|
||||||
|
high[1] as f64 + low[1] as f64,
|
||||||
|
high[2] as f64 + low[2] as f64,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
normals: Vec::new(),
|
||||||
|
triangles: mesh
|
||||||
|
.indices
|
||||||
|
.chunks_exact(3)
|
||||||
|
.map(|triangle| {
|
||||||
|
[
|
||||||
|
triangle[0] as usize,
|
||||||
|
triangle[1] as usize,
|
||||||
|
triangle[2] as usize,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
};
|
};
|
||||||
let mut area = 0.0;
|
let surface = kernel_mesh.surface_properties();
|
||||||
let mut area_centroid_numerator = [0.0; 3];
|
let mut metrics = MeshMetrics {
|
||||||
let mut signed_volume = 0.0;
|
|
||||||
let mut centroid_numerator = [0.0; 3];
|
|
||||||
for triangle in mesh.indices.chunks_exact(3) {
|
|
||||||
let a = point(triangle[0]);
|
|
||||||
let b = point(triangle[1]);
|
|
||||||
let c = point(triangle[2]);
|
|
||||||
let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
|
||||||
let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
|
|
||||||
let cross = [
|
|
||||||
ab[1] * ac[2] - ab[2] * ac[1],
|
|
||||||
ab[2] * ac[0] - ab[0] * ac[2],
|
|
||||||
ab[0] * ac[1] - ab[1] * ac[0],
|
|
||||||
];
|
|
||||||
let triangle_area = 0.5
|
|
||||||
* (cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2])
|
|
||||||
.sqrt();
|
|
||||||
area += triangle_area;
|
|
||||||
for axis in 0..3 {
|
|
||||||
area_centroid_numerator[axis] +=
|
|
||||||
triangle_area * (a[axis] + b[axis] + c[axis]) / 3.0;
|
|
||||||
}
|
|
||||||
let tetra = (
|
|
||||||
a[0] * (b[1] * c[2] - b[2] * c[1])
|
|
||||||
- a[1] * (b[0] * c[2] - b[2] * c[0])
|
|
||||||
+ a[2] * (b[0] * c[1] - b[1] * c[0])
|
|
||||||
) / 6.0;
|
|
||||||
signed_volume += tetra;
|
|
||||||
for axis in 0..3 {
|
|
||||||
centroid_numerator[axis] += tetra * (a[axis] + b[axis] + c[axis]) / 4.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let centroid = if signed_volume.abs() > 1e-18 {
|
|
||||||
[
|
|
||||||
centroid_numerator[0] / signed_volume,
|
|
||||||
centroid_numerator[1] / signed_volume,
|
|
||||||
centroid_numerator[2] / signed_volume,
|
|
||||||
]
|
|
||||||
} else if area > 1e-18 {
|
|
||||||
[
|
|
||||||
area_centroid_numerator[0] / area,
|
|
||||||
area_centroid_numerator[1] / area,
|
|
||||||
area_centroid_numerator[2] / area,
|
|
||||||
]
|
|
||||||
} else {
|
|
||||||
[0.0; 3]
|
|
||||||
};
|
|
||||||
MeshMetrics {
|
|
||||||
vertices: mesh.verts.len(),
|
vertices: mesh.verts.len(),
|
||||||
triangles: mesh.indices.len() / 3,
|
triangles: mesh.indices.len() / 3,
|
||||||
surface_area: area,
|
surface_area: surface.map_or(0.0, |value| value.0),
|
||||||
volume: signed_volume.abs(),
|
centroid: surface.map_or([0.0; 3], |value| value.1),
|
||||||
centroid,
|
..MeshMetrics::default()
|
||||||
|
};
|
||||||
|
if let Some(properties) = kernel_mesh.inertial_properties() {
|
||||||
|
metrics.apply_mass_properties(properties);
|
||||||
}
|
}
|
||||||
|
metrics
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MeshLodSet {
|
impl MeshLodSet {
|
||||||
|
pub fn apply_mass_properties(&mut self, properties: cadkernel::brep::MassProperties) {
|
||||||
|
self.metrics.apply_mass_properties(properties);
|
||||||
|
}
|
||||||
|
|
||||||
/// Build a set from its LODs, computing the 3D AABB.
|
/// Build a set from its LODs, computing the 3D AABB.
|
||||||
pub fn from_lods(lods: Vec<MeshModel>) -> Self {
|
pub fn from_lods(lods: Vec<MeshModel>) -> Self {
|
||||||
let (world_aabb, z_aabb) = compute_mesh_aabb(&lods);
|
let (world_aabb, z_aabb) = compute_mesh_aabb(&lods);
|
||||||
|
|||||||
@@ -1013,9 +1013,44 @@ fn brep_properties(
|
|||||||
history_flags(document, handle).unwrap_or((false, false, 1));
|
history_flags(document, handle).unwrap_or((false, false, 1));
|
||||||
let (show_history, show_history_editable) =
|
let (show_history, show_history_editable) =
|
||||||
displayed_history_state(object_show_history, show_history_mode);
|
displayed_history_state(object_show_history, show_history_mode);
|
||||||
vec![PropSection {
|
let mut geometry = vec![Property {
|
||||||
title: t!("Solid History").into_owned(),
|
label: t!("Solid type").into_owned(),
|
||||||
props: vec![
|
field: "solid_history_type",
|
||||||
|
value: PropValue::ReadOnly(t!("3D Solid").into_owned()),
|
||||||
|
}];
|
||||||
|
if let Some(position) = document
|
||||||
|
.solid_history_operation(handle)
|
||||||
|
.and_then(|operation| match operation {
|
||||||
|
SolidHistoryOperation::Brep(value) => world_point(value.base.transform, [0.0; 3]),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
{
|
||||||
|
geometry.extend([
|
||||||
|
crate::entities::common::edit_prop(
|
||||||
|
t!("Position X").as_ref(),
|
||||||
|
PROP_POSITION_X,
|
||||||
|
position.x,
|
||||||
|
),
|
||||||
|
crate::entities::common::edit_prop(
|
||||||
|
t!("Position Y").as_ref(),
|
||||||
|
PROP_POSITION_Y,
|
||||||
|
position.y,
|
||||||
|
),
|
||||||
|
crate::entities::common::edit_prop(
|
||||||
|
t!("Position Z").as_ref(),
|
||||||
|
PROP_POSITION_Z,
|
||||||
|
position.z,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
vec![
|
||||||
|
PropSection {
|
||||||
|
title: t!("Geometry").into_owned(),
|
||||||
|
props: geometry,
|
||||||
|
},
|
||||||
|
PropSection {
|
||||||
|
title: t!("Solid History").into_owned(),
|
||||||
|
props: vec![
|
||||||
Property {
|
Property {
|
||||||
label: t!("History").into_owned(),
|
label: t!("History").into_owned(),
|
||||||
field: PROP_HISTORY,
|
field: PROP_HISTORY,
|
||||||
@@ -1037,7 +1072,8 @@ fn brep_properties(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}]
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cylinder_properties(
|
fn cylinder_properties(
|
||||||
@@ -2203,11 +2239,50 @@ fn set_pyramid_sides(value: &mut SolidHistoryPyramid, sides: i32) -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_brep_position_property(
|
||||||
|
value: &mut SolidHistoryBrep,
|
||||||
|
field: &str,
|
||||||
|
text: &str,
|
||||||
|
) -> Option<bool> {
|
||||||
|
let axis = match field {
|
||||||
|
PROP_POSITION_X => 0,
|
||||||
|
PROP_POSITION_Y => 1,
|
||||||
|
PROP_POSITION_Z => 2,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
let Some(target) = crate::entities::common::parse_length(text) else {
|
||||||
|
return Some(false);
|
||||||
|
};
|
||||||
|
if !target.is_finite() {
|
||||||
|
return Some(false);
|
||||||
|
}
|
||||||
|
let Some(current) = matrix(value.base.transform) else {
|
||||||
|
return Some(false);
|
||||||
|
};
|
||||||
|
let position = current.transform_point3(glam::DVec3::ZERO);
|
||||||
|
let mut next = position;
|
||||||
|
next[axis] = target;
|
||||||
|
if next == position {
|
||||||
|
return Some(true);
|
||||||
|
}
|
||||||
|
let updated = glam::DMat4::from_translation(next - position) * current;
|
||||||
|
if !updated.is_finite() || updated.determinant().abs() <= 1e-12 {
|
||||||
|
return Some(false);
|
||||||
|
}
|
||||||
|
value.base.transform = updated.to_cols_array();
|
||||||
|
Some(true)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn apply_primitive_property(
|
pub fn apply_primitive_property(
|
||||||
operation: &mut SolidHistoryOperation,
|
operation: &mut SolidHistoryOperation,
|
||||||
field: &str,
|
field: &str,
|
||||||
value: &str,
|
value: &str,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
|
if let SolidHistoryOperation::Brep(brep_value) = operation {
|
||||||
|
if let Some(applied) = apply_brep_position_property(brep_value, field, value) {
|
||||||
|
return applied;
|
||||||
|
}
|
||||||
|
}
|
||||||
if let SolidHistoryOperation::Box(rectangular_value)
|
if let SolidHistoryOperation::Box(rectangular_value)
|
||||||
| SolidHistoryOperation::Wedge(rectangular_value) = operation
|
| SolidHistoryOperation::Wedge(rectangular_value) = operation
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -475,7 +475,11 @@ pub fn display_from_solid(
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
Some((mesh_from_tessellation(tessellation, color)?, wires, center))
|
let mut mesh = mesh_from_tessellation(tessellation, color)?;
|
||||||
|
if let Some(properties) = cadkernel::brep::analytic_mass_properties(body) {
|
||||||
|
mesh.apply_mass_properties(properties);
|
||||||
|
}
|
||||||
|
Some((mesh, wires, center))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The middle of a body, for a caller needing a point to turn or scale about.
|
/// The middle of a body, for a caller needing a point to turn or scale about.
|
||||||
|
|||||||
+1
-3
@@ -1364,9 +1364,7 @@ impl Scene {
|
|||||||
generator.source = source;
|
generator.source = source;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
set.metrics.centroid[0] += delta[0];
|
set.metrics.translate(delta);
|
||||||
set.metrics.centroid[1] += delta[1];
|
|
||||||
set.metrics.centroid[2] += delta[2];
|
|
||||||
set.recompute_aabb();
|
set.recompute_aabb();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user