forked from 1b-agent/opencadstudio-upstream
feat(model): align polysolid workflow
This commit is contained in:
1 parent
6d6defa0ad
commit
1dd6b115cf
12 files changed
+857
-250
No files matched your search
Generated
+1
-1
@@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
[[package]]
|
||||
name = "cadkernel"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=48c634995fa60d3928c37bae49b12b421c56c886#48c634995fa60d3928c37bae49b12b421c56c886"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=f194a6232066452e3198757f4fe25594fd8b090d#f194a6232066452e3198757f4fe25594fd8b090d"
|
||||
dependencies = [
|
||||
"acadrust",
|
||||
"cavalier_contours",
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ rfd = "0.17"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
env_logger = "0.11"
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "a0f7d444f1607bc4b2c881060cbe7ea1014253cb", features = ["serde"] }
|
||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "48c634995fa60d3928c37bae49b12b421c56c886", features = ["acis", "offset"] }
|
||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "f194a6232066452e3198757f4fe25594fd8b090d", features = ["acis", "offset"] }
|
||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||
flate2 = "1"
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||
|
||||
@@ -1636,11 +1636,24 @@ impl OpenCADStudio {
|
||||
entity,
|
||||
solid,
|
||||
history,
|
||||
erase_source,
|
||||
} => {
|
||||
let label = self.history_label_from_active_cmd(i, "SOLID");
|
||||
let pending = self.begin_undo(i, label, 1, true);
|
||||
let erase_source = erase_source.filter(|handle| {
|
||||
self.tabs[i].scene.document.header.delete_objects
|
||||
&& !self.tabs[i].scene.is_layer_locked(*handle)
|
||||
});
|
||||
let pending = self.begin_undo(
|
||||
i,
|
||||
label,
|
||||
1 + usize::from(erase_source.is_some()),
|
||||
true,
|
||||
);
|
||||
let handle = self.add_solid_model(entity, *solid, history);
|
||||
if !handle.is_null() {
|
||||
if let Some(source) = erase_source {
|
||||
self.tabs[i].scene.erase_entities(&[source]);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
|
||||
+18
-22
@@ -1038,32 +1038,28 @@ impl OpenCADStudio {
|
||||
return Some(self.solid_convtosurface());
|
||||
}
|
||||
|
||||
// POLYSOLID <width> <height> — extrude a selected polyline into a
|
||||
// wall-like solid.
|
||||
"POLYSOLID" => {
|
||||
use crate::command::SelectThenTwoValueCommand;
|
||||
let has_sel = !self.tabs[i].scene.selected_entities().is_empty();
|
||||
let c = SelectThenTwoValueCommand::new(
|
||||
"POLYSOLID",
|
||||
"POLYSOLID wall width:",
|
||||
"POLYSOLID wall height:",
|
||||
has_sel,
|
||||
);
|
||||
use crate::modules::model::polysolid_cmd::PolysolidCommand;
|
||||
let preselected = self.tabs[i]
|
||||
.scene
|
||||
.selected_entities()
|
||||
.into_iter()
|
||||
.find(|(_, entity)| {
|
||||
matches!(
|
||||
entity,
|
||||
acadrust::EntityType::Line(_)
|
||||
| acadrust::EntityType::Arc(_)
|
||||
| acadrust::EntityType::Circle(_)
|
||||
| acadrust::EntityType::Ellipse(_)
|
||||
| acadrust::EntityType::LwPolyline(_)
|
||||
| acadrust::EntityType::Spline(_)
|
||||
)
|
||||
})
|
||||
.map(|(handle, entity)| (handle, entity.clone()));
|
||||
let c = PolysolidCommand::new(preselected);
|
||||
self.command_line.push_info(&c.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(c));
|
||||
}
|
||||
cmd if cmd.starts_with("POLYSOLID ") => {
|
||||
let nums: Vec<f64> = cmd
|
||||
.split_whitespace()
|
||||
.skip(1)
|
||||
.filter_map(|s| s.parse::<f64>().ok())
|
||||
.collect();
|
||||
if nums.len() >= 2 && nums[0] > 0.0 && nums[1] > 0.0 {
|
||||
return Some(self.solid_polysolid(nums[0], nums[1]));
|
||||
}
|
||||
self.command_line
|
||||
.push_info(crate::t!("Usage: POLYSOLID <width> <height> (select a polyline first)").as_ref());
|
||||
}
|
||||
|
||||
// SPLINEFIT — fit a smooth spline through the selected polyline's points.
|
||||
"SPLINEFIT" | "FITSPLINE" => {
|
||||
|
||||
@@ -468,55 +468,6 @@ impl super::OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// POLYSOLID — build a wall solid around an exact polyline offset.
|
||||
pub(super) fn solid_polysolid(&mut self, width: f64, height: f64) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
let found: Option<(Handle, EntityType)> = self.tabs[i]
|
||||
.scene
|
||||
.selected_entities()
|
||||
.iter()
|
||||
.filter(|(h, _)| !self.tabs[i].scene.is_layer_locked(*h))
|
||||
.find_map(|(h, e)| match e {
|
||||
EntityType::LwPolyline(_) => Some((*h, (*e).clone())),
|
||||
_ => None,
|
||||
});
|
||||
let Some((handle, entity)) = found else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("POLYSOLID: select a polyline first.").as_ref());
|
||||
return Task::none();
|
||||
};
|
||||
let Some(result) = crate::scene::model::sweep_model::polysolid(&entity, width, height)
|
||||
else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("POLYSOLID: the kernel could not offset the polyline.").as_ref());
|
||||
return Task::none();
|
||||
};
|
||||
if crate::scene::convert::acis_export::solid_to_sat(&result).is_none() {
|
||||
self.command_line
|
||||
.push_error(crate::t!("POLYSOLID: the result could not be encoded as ACIS.").as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
self.push_undo_snapshot(i, "POLYSOLID");
|
||||
self.tabs[i].scene.erase_entities(&[handle]);
|
||||
let mut s3d = Solid3D::new();
|
||||
s3d.wires = solid_model::edge_wires(&result);
|
||||
let history = solid_history::brep_op(&result);
|
||||
let h = self.add_solid_model(EntityType::Solid3D(s3d), result, history);
|
||||
self.tabs[i].scene.deselect_all();
|
||||
if !h.is_null() {
|
||||
self.tabs[i].scene.select_entity(h, false);
|
||||
}
|
||||
if !h.is_null() {
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
self.command_line.push_output(
|
||||
crate::tf!("POLYSOLID: created a wall solid (width {width}, height {height}).")
|
||||
.as_ref(),
|
||||
);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// 3DMIRROR — add a mirrored copy of the one selected solid across the plane
|
||||
/// perpendicular to the X/Y/Z axis (0/1/2) through its centre, keeping the
|
||||
/// original. A reflection loses handedness, so the kernel reverses every
|
||||
|
||||
+1
-98
@@ -1127,104 +1127,6 @@ impl CadCommand for TCountCommand {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Generic front-end for a two-value command that operates on the current
|
||||
/// selection (POLYSOLID width + height on a selected polyline). Gathers a
|
||||
/// selection first when none is set, prompts for two values, and dispatches
|
||||
/// `<name> <first> <second>` to the inline handler.
|
||||
pub struct SelectThenTwoValueCommand {
|
||||
name: &'static str,
|
||||
prompt1: &'static str,
|
||||
prompt2: &'static str,
|
||||
gathering: bool,
|
||||
selected: Vec<Handle>,
|
||||
first: Option<String>,
|
||||
}
|
||||
|
||||
impl SelectThenTwoValueCommand {
|
||||
pub fn new(
|
||||
name: &'static str,
|
||||
prompt1: &'static str,
|
||||
prompt2: &'static str,
|
||||
has_selection: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
name,
|
||||
prompt1,
|
||||
prompt2,
|
||||
gathering: !has_selection,
|
||||
selected: Vec::new(),
|
||||
first: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for SelectThenTwoValueCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
self.name
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if self.gathering {
|
||||
crate::t!(
|
||||
"%{name} select objects, then press Enter:",
|
||||
name = self.name
|
||||
)
|
||||
.into_owned()
|
||||
} else if self.first.is_none() {
|
||||
crate::t!(self.prompt1).into_owned()
|
||||
} else {
|
||||
crate::t!(self.prompt2).into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
!self.gathering
|
||||
}
|
||||
|
||||
fn is_selection_gathering(&self) -> bool {
|
||||
self.gathering
|
||||
}
|
||||
|
||||
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
|
||||
self.selected = handles;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
if self.gathering {
|
||||
return None;
|
||||
}
|
||||
let t = text.trim();
|
||||
if t.is_empty() {
|
||||
return None;
|
||||
}
|
||||
match &self.first {
|
||||
None => {
|
||||
self.first = Some(t.to_string());
|
||||
// Consumed; `None` would feed the same text back as the second
|
||||
// value and dispatch `<name> <x> <x>` in one step.
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Some(first) => Some(CmdResult::Dispatch(format!("{} {first} {t}", self.name))),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
if self.gathering {
|
||||
if self.selected.is_empty() {
|
||||
return CmdResult::Cancel;
|
||||
}
|
||||
self.gathering = false;
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
|
||||
// ── Result token ──────────────────────────────────────────────────────────
|
||||
|
||||
/// Returned by every `CadCommand` method to tell main.rs what to do.
|
||||
@@ -1278,6 +1180,7 @@ pub enum CmdResult {
|
||||
entity: EntityType,
|
||||
solid: Box<cadkernel::brep::Body>,
|
||||
history: acadrust::objects::SolidHistoryOperation,
|
||||
erase_source: Option<Handle>,
|
||||
},
|
||||
/// Commit an acadrust entity, end the command, and open the in-place text
|
||||
/// editor on it (used by MLEADER to type the annotation after placement).
|
||||
|
||||
@@ -219,6 +219,7 @@ impl CylinderCommand {
|
||||
entity: EntityType::Solid3D(entity),
|
||||
solid: Box::new(solid),
|
||||
history,
|
||||
erase_source: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
pub mod boolean_cmd;
|
||||
pub mod cylinder_cmd;
|
||||
pub mod edge_cmd;
|
||||
pub mod polysolid_cmd;
|
||||
pub mod primitive_cmd;
|
||||
|
||||
use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, ToolDef};
|
||||
|
||||
@@ -0,0 +1,576 @@
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
use acadrust::entities::{LwPolyline, LwVertex, Solid3D};
|
||||
use acadrust::types::Vector2;
|
||||
use acadrust::{EntityType, Handle};
|
||||
use glam::{DVec2, DVec3};
|
||||
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, WorkingPlane};
|
||||
use crate::modules::draw::draw::polyline::{
|
||||
arc_sample_points, compute_bulge, seg_exit_tangent,
|
||||
};
|
||||
use crate::scene::model::solid_model;
|
||||
use crate::scene::model::sweep_model::{self, PolysolidJustification};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use crate::t;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Defaults {
|
||||
height: f64,
|
||||
width: f64,
|
||||
justification: PolysolidJustification,
|
||||
}
|
||||
|
||||
fn defaults() -> &'static Mutex<Defaults> {
|
||||
static DEFAULTS: OnceLock<Mutex<Defaults>> = OnceLock::new();
|
||||
DEFAULTS.get_or_init(|| {
|
||||
Mutex::new(Defaults {
|
||||
height: 80.0,
|
||||
width: 5.0,
|
||||
justification: PolysolidJustification::Center,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Step {
|
||||
Start,
|
||||
Height,
|
||||
Width,
|
||||
Justify,
|
||||
Object,
|
||||
Line,
|
||||
Arc,
|
||||
ArcDirection,
|
||||
ArcSecond,
|
||||
ArcEnd,
|
||||
}
|
||||
|
||||
pub struct PolysolidCommand {
|
||||
step: Step,
|
||||
return_step: Step,
|
||||
plane: WorkingPlane,
|
||||
vertices: Vec<DVec3>,
|
||||
bulges: Vec<f64>,
|
||||
height: f64,
|
||||
width: f64,
|
||||
justification: PolysolidJustification,
|
||||
pending_direction: Option<DVec2>,
|
||||
pending_second: Option<DVec3>,
|
||||
picked: Option<EntityType>,
|
||||
preselected: Option<(Handle, EntityType)>,
|
||||
}
|
||||
|
||||
impl PolysolidCommand {
|
||||
pub fn new(preselected: Option<(Handle, EntityType)>) -> Self {
|
||||
let saved = defaults().lock().map(|value| *value).unwrap_or(Defaults {
|
||||
height: 80.0,
|
||||
width: 5.0,
|
||||
justification: PolysolidJustification::Center,
|
||||
});
|
||||
Self {
|
||||
step: Step::Start,
|
||||
return_step: Step::Start,
|
||||
plane: WorkingPlane::default(),
|
||||
vertices: Vec::new(),
|
||||
bulges: Vec::new(),
|
||||
height: saved.height,
|
||||
width: saved.width,
|
||||
justification: saved.justification,
|
||||
pending_direction: None,
|
||||
pending_second: None,
|
||||
picked: None,
|
||||
preselected,
|
||||
}
|
||||
}
|
||||
|
||||
fn remember(&self) {
|
||||
if let Ok(mut saved) = defaults().lock() {
|
||||
*saved = Defaults {
|
||||
height: self.height,
|
||||
width: self.width,
|
||||
justification: self.justification,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn supported(entity: &EntityType) -> bool {
|
||||
matches!(
|
||||
entity,
|
||||
EntityType::Line(_)
|
||||
| EntityType::Arc(_)
|
||||
| EntityType::Circle(_)
|
||||
| EntityType::Ellipse(_)
|
||||
| EntityType::LwPolyline(_)
|
||||
| EntityType::Spline(_)
|
||||
)
|
||||
}
|
||||
|
||||
fn path_entity(&self, closed: bool) -> Option<EntityType> {
|
||||
if self.vertices.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let local = self
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|point| self.plane.to_local(*point))
|
||||
.collect::<Vec<_>>();
|
||||
let mut path = LwPolyline::new();
|
||||
path.vertices = local
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, point)| {
|
||||
let mut vertex = LwVertex::new(Vector2::new(point.x, point.y));
|
||||
vertex.bulge = self.bulges.get(index).copied().unwrap_or(0.0);
|
||||
vertex
|
||||
})
|
||||
.collect();
|
||||
path.elevation = local[0].z;
|
||||
path.is_closed = closed;
|
||||
Some(self.plane.place_entity(EntityType::LwPolyline(path)))
|
||||
}
|
||||
|
||||
fn commit(&self, path: EntityType, erase_source: Option<Handle>) -> CmdResult {
|
||||
let Some((solid, history)) = sweep_model::polysolid(
|
||||
&path,
|
||||
self.width,
|
||||
self.height,
|
||||
self.justification,
|
||||
) else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
let Some(document) = crate::scene::convert::acis_export::solid_to_sat(&solid) else {
|
||||
return CmdResult::Cancel;
|
||||
};
|
||||
let mut entity = Solid3D::new();
|
||||
entity.set_sat_document(&document);
|
||||
entity.wires = solid_model::edge_wires(&solid);
|
||||
CmdResult::CommitSolid {
|
||||
entity: EntityType::Solid3D(entity),
|
||||
solid: Box::new(solid),
|
||||
history,
|
||||
erase_source,
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(&self, closed: bool) -> CmdResult {
|
||||
self.path_entity(closed)
|
||||
.map(|path| self.commit(path, None))
|
||||
.unwrap_or(CmdResult::Cancel)
|
||||
}
|
||||
|
||||
fn close(&mut self) -> CmdResult {
|
||||
if self.vertices.len() < 3 {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
if self.step == Step::Arc {
|
||||
let last = *self.vertices.last().unwrap();
|
||||
let first = self.vertices[0];
|
||||
let bulge = compute_bulge(
|
||||
self.plane.to_local(last).truncate(),
|
||||
self.pending_direction.unwrap_or_else(|| self.last_tangent()),
|
||||
self.plane.to_local(first).truncate(),
|
||||
);
|
||||
if let Some(value) = self.bulges.last_mut() {
|
||||
*value = bulge;
|
||||
}
|
||||
}
|
||||
self.finish(true)
|
||||
}
|
||||
|
||||
fn undo(&mut self) -> CmdResult {
|
||||
if self.vertices.pop().is_some() {
|
||||
self.bulges.pop();
|
||||
if let Some(last) = self.bulges.last_mut() {
|
||||
*last = 0.0;
|
||||
}
|
||||
}
|
||||
self.pending_direction = None;
|
||||
self.pending_second = None;
|
||||
if self.vertices.is_empty() {
|
||||
self.step = Step::Start;
|
||||
} else if matches!(self.step, Step::Arc | Step::ArcDirection | Step::ArcSecond | Step::ArcEnd)
|
||||
{
|
||||
self.step = Step::Arc;
|
||||
} else {
|
||||
self.step = Step::Line;
|
||||
}
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn last_tangent(&self) -> DVec2 {
|
||||
let count = self.vertices.len();
|
||||
if count < 2 {
|
||||
return DVec2::X;
|
||||
}
|
||||
let a = self.plane.to_local(self.vertices[count - 2]);
|
||||
let b = self.plane.to_local(self.vertices[count - 1]);
|
||||
seg_exit_tangent(a, b, self.bulges[count - 2])
|
||||
.map(|value| value.as_dvec2())
|
||||
.unwrap_or(DVec2::X)
|
||||
}
|
||||
|
||||
fn add_segment(&mut self, point: DVec3, bulge: f64) -> CmdResult {
|
||||
if let Some(last) = self.bulges.last_mut() {
|
||||
*last = bulge;
|
||||
}
|
||||
self.vertices.push(point);
|
||||
self.bulges.push(0.0);
|
||||
self.pending_direction = None;
|
||||
self.pending_second = None;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn three_point_bulge(&self, start: DVec3, middle: DVec3, end: DVec3) -> f64 {
|
||||
let a3 = self.plane.to_local(start);
|
||||
let m3 = self.plane.to_local(middle);
|
||||
let b3 = self.plane.to_local(end);
|
||||
let a = DVec2::new(a3.x, a3.y);
|
||||
let m = DVec2::new(m3.x, m3.y);
|
||||
let b = DVec2::new(b3.x, b3.y);
|
||||
let determinant = 2.0
|
||||
* (a.x * (m.y - b.y) + m.x * (b.y - a.y) + b.x * (a.y - m.y));
|
||||
if determinant.abs() <= 1e-12 {
|
||||
return 0.0;
|
||||
}
|
||||
let aa = a.length_squared();
|
||||
let mm = m.length_squared();
|
||||
let bb = b.length_squared();
|
||||
let center = DVec2::new(
|
||||
(aa * (m.y - b.y) + mm * (b.y - a.y) + bb * (a.y - m.y))
|
||||
/ determinant,
|
||||
(aa * (b.x - m.x) + mm * (a.x - b.x) + bb * (m.x - a.x))
|
||||
/ determinant,
|
||||
);
|
||||
let start_angle = (a - center).y.atan2((a - center).x);
|
||||
let middle_angle = (m - center).y.atan2((m - center).x);
|
||||
let end_angle = (b - center).y.atan2((b - center).x);
|
||||
let ccw = (end_angle - start_angle).rem_euclid(std::f64::consts::TAU);
|
||||
let middle_ccw =
|
||||
(middle_angle - start_angle).rem_euclid(std::f64::consts::TAU);
|
||||
let sweep = if middle_ccw <= ccw + 1e-12 {
|
||||
ccw
|
||||
} else {
|
||||
ccw - std::f64::consts::TAU
|
||||
};
|
||||
(sweep / 4.0).tan()
|
||||
}
|
||||
|
||||
fn value_step(&mut self, step: Step) {
|
||||
self.return_step = if self.vertices.is_empty() {
|
||||
Step::Start
|
||||
} else {
|
||||
self.step
|
||||
};
|
||||
self.step = step;
|
||||
}
|
||||
|
||||
fn path_preview(&self, cursor: DVec3) -> Option<WireModel> {
|
||||
let start = *self.vertices.last()?;
|
||||
let start_local = self.plane.to_local(start);
|
||||
let cursor_local = self.plane.to_local(cursor);
|
||||
let points = match self.step {
|
||||
Step::Arc => {
|
||||
let bulge = compute_bulge(
|
||||
start_local.truncate(),
|
||||
self.last_tangent(),
|
||||
cursor_local.truncate(),
|
||||
);
|
||||
arc_sample_points(start_local.as_vec3(), bulge, cursor_local.as_vec3(), 24)
|
||||
.into_iter()
|
||||
.map(|point| self.plane.to_world(DVec3::new(
|
||||
point[0] as f64,
|
||||
point[1] as f64,
|
||||
point[2] as f64,
|
||||
)).to_array())
|
||||
.collect()
|
||||
}
|
||||
Step::ArcEnd => {
|
||||
let middle = self.pending_second?;
|
||||
let bulge = self.three_point_bulge(start, middle, cursor);
|
||||
arc_sample_points(start_local.as_vec3(), bulge, cursor_local.as_vec3(), 24)
|
||||
.into_iter()
|
||||
.map(|point| self.plane.to_world(DVec3::new(
|
||||
point[0] as f64,
|
||||
point[1] as f64,
|
||||
point[2] as f64,
|
||||
)).to_array())
|
||||
.collect()
|
||||
}
|
||||
Step::ArcDirection => vec![start.to_array(), cursor.to_array()],
|
||||
_ => vec![start.to_array(), cursor.to_array()],
|
||||
};
|
||||
Some(WireModel::solid_f64(
|
||||
"POLYSOLID_PREVIEW".to_string(),
|
||||
points,
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for PolysolidCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"POLYSOLID"
|
||||
}
|
||||
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match self.step {
|
||||
Step::Start => format!(
|
||||
"POLYSOLID Height = {:.4}, Width = {:.4}, Justification = {}\n{}",
|
||||
self.height,
|
||||
self.width,
|
||||
match self.justification {
|
||||
PolysolidJustification::Left => "Left",
|
||||
PolysolidJustification::Center => "Center",
|
||||
PolysolidJustification::Right => "Right",
|
||||
},
|
||||
t!("Specify start point or [Object/Height/Width/Justify] <Object>:")
|
||||
),
|
||||
Step::Height => t!("Specify height:").into_owned(),
|
||||
Step::Width => t!("Specify width:").into_owned(),
|
||||
Step::Justify => format!(
|
||||
"Enter justification [Left/Center/Right] <{}>:",
|
||||
match self.justification {
|
||||
PolysolidJustification::Left => "Left",
|
||||
PolysolidJustification::Center => "Center",
|
||||
PolysolidJustification::Right => "Right",
|
||||
}
|
||||
),
|
||||
Step::Object => t!("Select path object:").into_owned(),
|
||||
Step::Line if self.vertices.len() >= 3 => {
|
||||
t!("Specify next point or [Arc/Close/Undo]:").into_owned()
|
||||
}
|
||||
Step::Line => t!("Specify next point or [Arc/Undo]:").into_owned(),
|
||||
Step::Arc if self.vertices.len() >= 3 => {
|
||||
t!("Specify endpoint of arc or [Close/Direction/Line/Second point/Undo]:")
|
||||
.into_owned()
|
||||
}
|
||||
Step::Arc => {
|
||||
t!("Specify endpoint of arc or [Direction/Line/Second point/Undo]:").into_owned()
|
||||
}
|
||||
Step::ArcDirection => t!("Specify tangent direction for the start point of arc:").into_owned(),
|
||||
Step::ArcSecond => t!("Specify second point on arc:").into_owned(),
|
||||
Step::ArcEnd => t!("Specify end point of arc:").into_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
match self.step {
|
||||
Step::Start => vec![
|
||||
CmdOption::new(t!("Object").as_ref(), "O"),
|
||||
CmdOption::new(t!("Height").as_ref(), "H"),
|
||||
CmdOption::new(t!("Width").as_ref(), "W"),
|
||||
CmdOption::new(t!("Justify").as_ref(), "J"),
|
||||
],
|
||||
Step::Line => {
|
||||
let mut options = vec![CmdOption::new(t!("Arc").as_ref(), "A")];
|
||||
if self.vertices.len() >= 3 {
|
||||
options.push(CmdOption::new(t!("Close").as_ref(), "C"));
|
||||
}
|
||||
options.push(CmdOption::new(t!("Undo").as_ref(), "U"));
|
||||
options
|
||||
}
|
||||
Step::Arc => {
|
||||
let mut options = Vec::new();
|
||||
if self.vertices.len() >= 3 {
|
||||
options.push(CmdOption::new(t!("Close").as_ref(), "C"));
|
||||
}
|
||||
options.extend([
|
||||
CmdOption::new(t!("Direction").as_ref(), "D"),
|
||||
CmdOption::new(t!("Line").as_ref(), "L"),
|
||||
CmdOption::new(t!("Second point").as_ref(), "S"),
|
||||
CmdOption::new(t!("Undo").as_ref(), "U"),
|
||||
]);
|
||||
options
|
||||
}
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||
match self.step {
|
||||
Step::Start => {
|
||||
self.vertices.push(point);
|
||||
self.bulges.push(0.0);
|
||||
self.step = Step::Line;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Line => self.add_segment(point, 0.0),
|
||||
Step::Arc => {
|
||||
let start = *self.vertices.last().unwrap_or(&point);
|
||||
let a = self.plane.to_local(start).truncate();
|
||||
let b = self.plane.to_local(point).truncate();
|
||||
let tangent = self.pending_direction.unwrap_or_else(|| self.last_tangent());
|
||||
let bulge = compute_bulge(a, tangent, b);
|
||||
self.add_segment(point, bulge)
|
||||
}
|
||||
Step::ArcDirection => {
|
||||
let start = *self.vertices.last().unwrap_or(&point);
|
||||
let direction = self.plane.to_local(point) - self.plane.to_local(start);
|
||||
if direction.truncate().length_squared() > 1e-12 {
|
||||
self.pending_direction = Some(direction.truncate().normalize());
|
||||
self.step = Step::Arc;
|
||||
}
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::ArcSecond => {
|
||||
self.pending_second = Some(point);
|
||||
self.step = Step::ArcEnd;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::ArcEnd => {
|
||||
let start = *self.vertices.last().unwrap_or(&point);
|
||||
let middle = self.pending_second.unwrap_or(point);
|
||||
let bulge = self.three_point_bulge(start, middle, point);
|
||||
self.step = Step::Arc;
|
||||
self.add_segment(point, bulge)
|
||||
}
|
||||
_ => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
match self.step {
|
||||
Step::Start => {
|
||||
if let Some((handle, entity)) = self.preselected.take() {
|
||||
return self.commit(entity, Some(handle));
|
||||
}
|
||||
self.step = Step::Object;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Line | Step::Arc if self.vertices.len() >= 2 => self.finish(false),
|
||||
Step::Justify => {
|
||||
self.justification = PolysolidJustification::Center;
|
||||
self.remember();
|
||||
self.step = self.return_step;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => CmdResult::Cancel,
|
||||
}
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
!matches!(self.step, Step::Object | Step::ArcDirection | Step::ArcSecond | Step::ArcEnd)
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
matches!(self.step, Step::Start | Step::Line | Step::Arc)
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let value = text.trim();
|
||||
let keyword = value.to_uppercase();
|
||||
match self.step {
|
||||
Step::Height => {
|
||||
let number = crate::entities::common::parse_length(value)?;
|
||||
if number > 0.0 && number.is_finite() {
|
||||
self.height = number;
|
||||
self.remember();
|
||||
self.step = self.return_step;
|
||||
}
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Step::Width => {
|
||||
let number = crate::entities::common::parse_length(value)?;
|
||||
if number > 0.0 && number.is_finite() {
|
||||
self.width = number;
|
||||
self.remember();
|
||||
self.step = self.return_step;
|
||||
}
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Step::Justify => {
|
||||
self.justification = match keyword.as_str() {
|
||||
"L" | "LEFT" => PolysolidJustification::Left,
|
||||
"C" | "CENTER" => PolysolidJustification::Center,
|
||||
"R" | "RIGHT" => PolysolidJustification::Right,
|
||||
_ => return None,
|
||||
};
|
||||
self.remember();
|
||||
self.step = self.return_step;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Step::Start => match keyword.as_str() {
|
||||
"O" | "OBJECT" => {
|
||||
self.step = Step::Object;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"H" | "HEIGHT" => {
|
||||
self.value_step(Step::Height);
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"W" | "WIDTH" => {
|
||||
self.value_step(Step::Width);
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"J" | "JUSTIFY" => {
|
||||
self.value_step(Step::Justify);
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
Step::Line => match keyword.as_str() {
|
||||
"A" | "ARC" => {
|
||||
self.step = Step::Arc;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"C" | "CLOSE" if self.vertices.len() >= 3 => Some(self.close()),
|
||||
"U" | "UNDO" => Some(self.undo()),
|
||||
_ => None,
|
||||
},
|
||||
Step::Arc => match keyword.as_str() {
|
||||
"D" | "DIRECTION" => {
|
||||
self.step = Step::ArcDirection;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"L" | "LINE" => {
|
||||
self.step = Step::Line;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"S" | "SECOND" | "SECOND POINT" => {
|
||||
self.step = Step::ArcSecond;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"C" | "CLOSE" if self.vertices.len() >= 3 => Some(self.close()),
|
||||
"U" | "UNDO" => Some(self.undo()),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_undo_step(&mut self) -> Option<CmdResult> {
|
||||
(!self.vertices.is_empty()).then(|| self.undo())
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
self.step == Step::Object
|
||||
}
|
||||
|
||||
fn inject_before_entity_pick(&self) -> bool {
|
||||
self.step == Step::Object
|
||||
}
|
||||
|
||||
fn inject_picked_entity(&mut self, entity: EntityType) {
|
||||
self.picked = Some(entity);
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _point: DVec3) -> CmdResult {
|
||||
let Some(entity) = self.picked.take() else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
if !Self::supported(&entity) {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
self.commit(entity, Some(handle))
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
|
||||
self.path_preview(point)
|
||||
}
|
||||
}
|
||||
@@ -472,6 +472,7 @@ impl PrimitiveCommand {
|
||||
entity: EntityType::Solid3D(entity),
|
||||
solid: Box::new(solid),
|
||||
history,
|
||||
erase_source: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1342,6 +1343,7 @@ impl PrimitiveCommand {
|
||||
entity: EntityType::Solid3D(entity),
|
||||
solid: Box::new(placed),
|
||||
history,
|
||||
erase_source: None,
|
||||
}
|
||||
}
|
||||
None => CmdResult::Cancel,
|
||||
|
||||
@@ -2,7 +2,7 @@ use acadrust::entities::Solid3D;
|
||||
use acadrust::objects::{
|
||||
DynamicBlockData, ObjectType, SolidHistoryBox, SolidHistoryBrep, SolidHistoryCone,
|
||||
SolidHistoryCylinder, SolidHistoryNodeBase, SolidHistoryOperation,
|
||||
SolidHistoryPyramid, SolidHistorySphere, SolidHistoryTorus,
|
||||
SolidHistoryPyramid, SolidHistorySphere, SolidHistorySweep, SolidHistoryTorus,
|
||||
};
|
||||
use cadkernel::brep::Body;
|
||||
|
||||
@@ -21,6 +21,7 @@ pub const GRIP_INNER_RADIUS: usize = 10_006;
|
||||
pub const GRIP_SIDES: usize = 10_007;
|
||||
pub const GRIP_MAJOR_RADIUS: usize = 10_008;
|
||||
pub const GRIP_MINOR_RADIUS: usize = 10_009;
|
||||
pub const GRIP_POSITION: usize = 10_010;
|
||||
pub const GRIP_BOX_CORNER_FIRST: usize = 10_100;
|
||||
pub const GRIP_BOX_FACE_X_MIN: usize = 10_110;
|
||||
pub const GRIP_BOX_FACE_X_MAX: usize = 10_111;
|
||||
@@ -99,6 +100,7 @@ pub fn has_specialized_primitive_properties(
|
||||
| SolidHistoryOperation::Sphere(_)
|
||||
| SolidHistoryOperation::Cone(_)
|
||||
| SolidHistoryOperation::Cylinder(_)
|
||||
| SolidHistoryOperation::Sweep(_)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -114,10 +116,77 @@ pub fn reference_point(operation: &SolidHistoryOperation) -> Option<glam::DVec3>
|
||||
}
|
||||
SolidHistoryOperation::Cone(value) => world_point(value.base.transform, [0.0; 3]),
|
||||
SolidHistoryOperation::Cylinder(value) => world_point(value.base.transform, [0.0; 3]),
|
||||
SolidHistoryOperation::Sweep(value) => world_point(
|
||||
value.base.transform,
|
||||
[
|
||||
value.reference_point.x,
|
||||
value.reference_point.y,
|
||||
value.reference_point.z,
|
||||
],
|
||||
),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn sweep_properties(
|
||||
document: &acadrust::CadDocument,
|
||||
handle: acadrust::Handle,
|
||||
value: &SolidHistorySweep,
|
||||
) -> Vec<PropSection> {
|
||||
let Some(position) = reference_point(&SolidHistoryOperation::Sweep(value.clone())) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let (record_history, object_show_history, show_history_mode) =
|
||||
history_flags(document, handle).unwrap_or((false, false, 1));
|
||||
let (show_history, _) =
|
||||
displayed_history_state(object_show_history, show_history_mode);
|
||||
let show_value = if show_history { "Yes" } else { "No" };
|
||||
vec![
|
||||
PropSection {
|
||||
title: t!("Geometry").into_owned(),
|
||||
props: vec![
|
||||
Property {
|
||||
label: t!("Solid type").into_owned(),
|
||||
field: "solid_history_type",
|
||||
value: PropValue::ReadOnly(t!("Sweep").into_owned()),
|
||||
},
|
||||
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,
|
||||
),
|
||||
],
|
||||
},
|
||||
PropSection {
|
||||
title: t!("Solid History").into_owned(),
|
||||
props: vec![
|
||||
Property {
|
||||
label: t!("History").into_owned(),
|
||||
field: PROP_HISTORY,
|
||||
value: PropValue::ReadOnly(
|
||||
if record_history { "Record" } else { "None" }.to_string(),
|
||||
),
|
||||
},
|
||||
Property {
|
||||
label: t!("Show History").into_owned(),
|
||||
field: PROP_SHOW_HISTORY,
|
||||
value: PropValue::ReadOnly(show_value.to_string()),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn cone_properties(
|
||||
document: &acadrust::CadDocument,
|
||||
handle: acadrust::Handle,
|
||||
@@ -558,6 +627,9 @@ pub fn primitive_properties(
|
||||
SolidHistoryOperation::Cylinder(value) => {
|
||||
return cylinder_properties(document, handle, value)
|
||||
}
|
||||
SolidHistoryOperation::Sweep(value) => {
|
||||
return sweep_properties(document, handle, value)
|
||||
}
|
||||
SolidHistoryOperation::Torus(value) => vec![
|
||||
history_prop(
|
||||
t!("Outer Radius").as_ref(),
|
||||
@@ -812,6 +884,40 @@ fn apply_cylinder_position_property(
|
||||
Some(true)
|
||||
}
|
||||
|
||||
fn apply_sweep_position_property(
|
||||
value: &mut SolidHistorySweep,
|
||||
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 target = crate::entities::common::parse_length(text)?;
|
||||
if !target.is_finite() {
|
||||
return Some(false);
|
||||
}
|
||||
let current = matrix(value.base.transform)?;
|
||||
let reference = current.transform_point3(glam::DVec3::new(
|
||||
value.reference_point.x,
|
||||
value.reference_point.y,
|
||||
value.reference_point.z,
|
||||
));
|
||||
if !reference.is_finite() || (target - reference[axis]).abs() <= 1e-12 {
|
||||
return Some(false);
|
||||
}
|
||||
let mut delta = glam::DVec3::ZERO;
|
||||
delta[axis] = target - reference[axis];
|
||||
let updated = glam::DMat4::from_translation(delta) * current;
|
||||
if !updated.is_finite() || updated.determinant().abs() <= 1e-12 {
|
||||
return Some(false);
|
||||
}
|
||||
value.base.transform = updated.to_cols_array();
|
||||
Some(true)
|
||||
}
|
||||
|
||||
fn canonicalize_cylinder_radii(value: &mut SolidHistoryCylinder) -> bool {
|
||||
if value.minor_radius <= value.major_radius {
|
||||
value.x_radius = value.major_radius;
|
||||
@@ -856,6 +962,11 @@ pub fn apply_primitive_property(
|
||||
return applied;
|
||||
}
|
||||
}
|
||||
if let SolidHistoryOperation::Sweep(sweep_value) = operation {
|
||||
if let Some(applied) = apply_sweep_position_property(sweep_value, field, value) {
|
||||
return applied;
|
||||
}
|
||||
}
|
||||
let Some(number) = (if field == PROP_SIDES {
|
||||
value.trim().parse::<f64>().ok()
|
||||
} else {
|
||||
@@ -1334,6 +1445,17 @@ pub fn primitive_grips(
|
||||
None,
|
||||
);
|
||||
}
|
||||
SolidHistoryOperation::Sweep(value) => add(
|
||||
GRIP_POSITION,
|
||||
value.base.transform,
|
||||
[
|
||||
value.reference_point.x,
|
||||
value.reference_point.y,
|
||||
value.reference_point.z,
|
||||
],
|
||||
GripShape::Square,
|
||||
None,
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
grips
|
||||
@@ -1488,6 +1610,22 @@ pub fn apply_primitive_grip(
|
||||
}
|
||||
_ => return false,
|
||||
},
|
||||
SolidHistoryOperation::Sweep(value) if grip_id == GRIP_POSITION => {
|
||||
let Some(current) = matrix(value.base.transform) else {
|
||||
return false;
|
||||
};
|
||||
let reference = current.transform_point3(glam::DVec3::new(
|
||||
value.reference_point.x,
|
||||
value.reference_point.y,
|
||||
value.reference_point.z,
|
||||
));
|
||||
let delta = world - reference;
|
||||
if !delta.is_finite() || delta.length_squared() <= 1e-24 {
|
||||
return false;
|
||||
}
|
||||
value.base.transform =
|
||||
(glam::DMat4::from_translation(delta) * current).to_cols_array();
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
true
|
||||
|
||||
+103
-77
@@ -1,8 +1,13 @@
|
||||
// Exact profile sweeps stored as kernel B-reps and ACIS.
|
||||
|
||||
use cadkernel::brep::{self, Body};
|
||||
use cadkernel::geom2d::{offset_polyline, Arc, Curve, EllipseArc, Line, Polyline};
|
||||
use cadkernel::geom2d::{Arc, Curve, EllipseArc, Line};
|
||||
use cadkernel::space::{PlanarCurve, Plane, Vec3};
|
||||
use acadrust::entities::{EmbeddedEntity, LwPolyline, LwVertex};
|
||||
use acadrust::objects::{
|
||||
SolidHistoryNodeBase, SolidHistoryOperation, SolidHistorySweep,
|
||||
};
|
||||
use acadrust::types::{Vector2, Vector3};
|
||||
use acadrust::EntityType;
|
||||
|
||||
use crate::entities::curve::entity_curve;
|
||||
@@ -162,88 +167,109 @@ pub fn lofted(profiles: &[EntityType]) -> Option<Body> {
|
||||
brep::loft(§ions)
|
||||
}
|
||||
|
||||
/// Builds an exact wall solid around a polyline centreline.
|
||||
pub fn polysolid(entity: &EntityType, width: f64, height: f64) -> Option<Body> {
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum PolysolidJustification {
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
fn embedded_path(entity: &EntityType) -> Option<EmbeddedEntity> {
|
||||
match entity {
|
||||
EntityType::Line(value) => Some(EmbeddedEntity::Line(value.clone())),
|
||||
EntityType::Arc(value) => Some(EmbeddedEntity::Arc(value.clone())),
|
||||
EntityType::Circle(value) => Some(EmbeddedEntity::Circle(value.clone())),
|
||||
EntityType::Ellipse(value) => Some(EmbeddedEntity::Ellipse(value.clone())),
|
||||
EntityType::LwPolyline(value) => Some(EmbeddedEntity::LwPolyline(value.clone())),
|
||||
EntityType::Spline(_) => {
|
||||
let planar = entity_curve(entity)?;
|
||||
let samples = planar.curve.tessellate_within(1e-4);
|
||||
if samples.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let mut path = LwPolyline::new();
|
||||
path.vertices = samples
|
||||
.into_iter()
|
||||
.map(|point| LwVertex::new(Vector2::new(point[0], point[1])))
|
||||
.collect();
|
||||
path.is_closed = planar.curve.is_closed();
|
||||
let plane = crate::command::WorkingPlane::new(
|
||||
glam::DVec3::from_array(planar.plane.origin),
|
||||
glam::DVec3::from_array(planar.plane.x_axis),
|
||||
glam::DVec3::from_array(planar.plane.y_axis),
|
||||
);
|
||||
let EntityType::LwPolyline(path) = plane.place_entity(EntityType::LwPolyline(path))
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
Some(EmbeddedEntity::LwPolyline(path))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a history-backed wall solid along a supported planar curve.
|
||||
pub fn polysolid(
|
||||
entity: &EntityType,
|
||||
width: f64,
|
||||
height: f64,
|
||||
justification: PolysolidJustification,
|
||||
) -> Option<(Body, SolidHistoryOperation)> {
|
||||
if !width.is_finite() || !height.is_finite() || width <= 0.0 || height.abs() <= 1e-12 {
|
||||
return None;
|
||||
}
|
||||
let planar = entity_curve(entity)?;
|
||||
let Curve::Polyline(source) = planar.curve else {
|
||||
let start_local = planar.curve.point_at(0.0);
|
||||
let tangent_local = Vec3::from([
|
||||
planar.curve.tangent_at(0.0)[0],
|
||||
planar.curve.tangent_at(0.0)[1],
|
||||
0.0,
|
||||
])
|
||||
.normalize()?;
|
||||
let start = Vec3::from(planar.plane.point_at(start_local));
|
||||
let tangent = (Vec3::from(planar.plane.x_axis) * tangent_local.x
|
||||
+ Vec3::from(planar.plane.y_axis) * tangent_local.y)
|
||||
.normalize()?;
|
||||
let normal = Vec3::from(planar.plane.normal()?).normalize()?;
|
||||
let side = tangent.cross(normal).normalize()?;
|
||||
let offset = match justification {
|
||||
PolysolidJustification::Left => 0.0,
|
||||
PolysolidJustification::Center => -width * 0.5,
|
||||
PolysolidJustification::Right => -width,
|
||||
};
|
||||
let profile_origin = start + side * offset;
|
||||
let profile_plane = crate::command::WorkingPlane::new(
|
||||
glam::DVec3::from_array(profile_origin.to_array()),
|
||||
glam::DVec3::from_array(side.to_array()),
|
||||
glam::DVec3::from_array(normal.to_array()),
|
||||
);
|
||||
let mut profile = LwPolyline::new();
|
||||
profile.vertices = [[0.0, 0.0], [width, 0.0], [width, height], [0.0, height]]
|
||||
.into_iter()
|
||||
.map(|point| LwVertex::new(Vector2::new(point[0], point[1])))
|
||||
.collect();
|
||||
profile.is_closed = true;
|
||||
let EntityType::LwPolyline(profile) =
|
||||
profile_plane.place_entity(EntityType::LwPolyline(profile))
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
let (left_pick, right_pick) = offset_picks(&source, width)?;
|
||||
let mut left = offset_polyline(&source, width * 0.5, left_pick);
|
||||
let mut right = offset_polyline(&source, width * 0.5, right_pick);
|
||||
if left.len() != 1 || right.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
let left = left.remove(0);
|
||||
let right = right.remove(0);
|
||||
let normal = Vec3::from(planar.plane.normal()?);
|
||||
let direction = (normal * height).to_array();
|
||||
|
||||
if source.closed {
|
||||
if !left.closed || !right.closed {
|
||||
return None;
|
||||
}
|
||||
let mut profiles = [left, right]
|
||||
.into_iter()
|
||||
.map(|polyline| {
|
||||
let area = Curve::Polyline(polyline.clone()).enclosed_area().abs();
|
||||
(area, Curve::Polyline(polyline).segments())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
profiles.sort_by(|first, second| second.0.total_cmp(&first.0));
|
||||
if profiles[0].0 <= profiles[1].0 || profiles[1].0 <= 1e-12 {
|
||||
return None;
|
||||
}
|
||||
return brep::extrude_region(
|
||||
planar.plane,
|
||||
&[profiles.remove(0).1, profiles.remove(0).1],
|
||||
direction,
|
||||
);
|
||||
}
|
||||
|
||||
if left.closed || right.closed {
|
||||
return None;
|
||||
}
|
||||
let left_start = left.vertices.first()?.position;
|
||||
let left_end = left.vertices.last()?.position;
|
||||
let right_start = right.vertices.first()?.position;
|
||||
let right_end = right.vertices.last()?.position;
|
||||
let mut outline = Curve::Polyline(left).segments();
|
||||
outline.push(Curve::Line(Line {
|
||||
start: left_end,
|
||||
end: right_end,
|
||||
}));
|
||||
let mut back = Curve::Polyline(right).segments();
|
||||
back.reverse();
|
||||
outline.extend(back);
|
||||
outline.push(Curve::Line(Line {
|
||||
start: right_start,
|
||||
end: left_start,
|
||||
}));
|
||||
brep::extrude(planar.plane, &outline, direction)
|
||||
}
|
||||
|
||||
fn offset_picks(polyline: &Polyline, width: f64) -> Option<([f64; 2], [f64; 2])> {
|
||||
let start = polyline.vertices.first()?.position;
|
||||
let next = polyline.vertices.get(1)?.position;
|
||||
let along = if let Some(arc) = polyline.segment_arc(0) {
|
||||
let near = arc.sample(1e-6);
|
||||
[near[0] - start[0], near[1] - start[1]]
|
||||
} else {
|
||||
[next[0] - start[0], next[1] - start[1]]
|
||||
};
|
||||
let length = along[0].hypot(along[1]);
|
||||
if length <= 1e-12 {
|
||||
return None;
|
||||
}
|
||||
let side = [-along[1] / length * width, along[0] / length * width];
|
||||
Some((
|
||||
[start[0] + side[0], start[1] + side[1]],
|
||||
[start[0] - side[0], start[1] - side[1]],
|
||||
))
|
||||
let mut base = SolidHistoryNodeBase::new(1);
|
||||
base.transform = glam::DMat4::IDENTITY.to_cols_array();
|
||||
let operation = SolidHistoryOperation::Sweep(SolidHistorySweep {
|
||||
base,
|
||||
operation_major: 1,
|
||||
direction: Vector3::new(normal.x * height, normal.y * height, normal.z * height),
|
||||
sweep_entity: Some(EmbeddedEntity::LwPolyline(profile)),
|
||||
path_entity: Some(embedded_path(entity)?),
|
||||
scale_factor: 1.0,
|
||||
sweep_entity_transform: glam::DMat4::IDENTITY.to_cols_array(),
|
||||
path_entity_transform: glam::DMat4::IDENTITY.to_cols_array(),
|
||||
reference_point: Vector3::new(start.x, start.y, start.z),
|
||||
..SolidHistorySweep::default()
|
||||
});
|
||||
let body = cadkernel::acis::rebuild_body(&operation).ok()?;
|
||||
Some((body, operation))
|
||||
}
|
||||
|
||||
fn circular_loft(profiles: &[EntityType]) -> Option<Body> {
|
||||
|
||||
Reference in new issue
Block a user