forked from 1b-agent/opencadstudio-upstream
Complete cylinder workflow and history properties
This commit is contained in:
1 parent
3e45cff72f
commit
e4cfd4c142
9 files changed
+1100
-18
No files matched your search
Generated
+1
-1
@@ -878,7 +878,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=0d48aa453f8689e4d737201b0a79604974427c6f#0d48aa453f8689e4d737201b0a79604974427c6f"
|
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=d37fb1bee9f0fb7432dd9f08ed1a1aeeb56ba40b#d37fb1bee9f0fb7432dd9f08ed1a1aeeb56ba40b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"acadrust",
|
"acadrust",
|
||||||
"cavalier_contours",
|
"cavalier_contours",
|
||||||
|
|||||||
+1
-1
@@ -28,7 +28,7 @@ rfd = "0.17"
|
|||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "45789a7700bd5c006feb4c7ff16c2ff80a7047db", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "45789a7700bd5c006feb4c7ff16c2ff80a7047db", features = ["serde"] }
|
||||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "0d48aa453f8689e4d737201b0a79604974427c6f", features = ["acis", "offset"] }
|
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "d37fb1bee9f0fb7432dd9f08ed1a1aeeb56ba40b", 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"] }
|
||||||
|
|||||||
@@ -957,7 +957,13 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Model commands (3D primitives) ─────────────────────────────
|
// ── Model commands (3D primitives) ─────────────────────────────
|
||||||
"BOX" | "WEDGE" | "CYLINDER" | "CONE" | "SPHERE" | "PYRAMID" | "PYR"
|
"CYLINDER" => {
|
||||||
|
use crate::modules::model::cylinder_cmd::CylinderCommand;
|
||||||
|
let new_cmd = CylinderCommand::new();
|
||||||
|
self.command_line.push_info(&new_cmd.prompt());
|
||||||
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
|
}
|
||||||
|
"BOX" | "WEDGE" | "CONE" | "SPHERE" | "PYRAMID" | "PYR"
|
||||||
| "TORUS" => {
|
| "TORUS" => {
|
||||||
use crate::modules::model::primitive_cmd::PrimitiveCommand;
|
use crate::modules::model::primitive_cmd::PrimitiveCommand;
|
||||||
let new_cmd = PrimitiveCommand::new(cmd);
|
let new_cmd = PrimitiveCommand::new(cmd);
|
||||||
|
|||||||
@@ -441,14 +441,14 @@ impl OpenCADStudio {
|
|||||||
let display_entity = dispatch::entity_in_working_plane(contextual.as_ref(), plane);
|
let display_entity = dispatch::entity_in_working_plane(contextual.as_ref(), plane);
|
||||||
let entity = &display_entity;
|
let entity = &display_entity;
|
||||||
let group_names = self.tabs[i].scene.group_names_for_entity(handle);
|
let group_names = self.tabs[i].scene.group_names_for_entity(handle);
|
||||||
let box_primitive =
|
let specialized_primitive =
|
||||||
crate::scene::model::solid_history::is_box_primitive(
|
crate::scene::model::solid_history::has_specialized_primitive_properties(
|
||||||
&self.tabs[i].scene.document,
|
&self.tabs[i].scene.document,
|
||||||
handle,
|
handle,
|
||||||
);
|
);
|
||||||
let mut sections =
|
let mut sections =
|
||||||
dispatch::properties_sectioned(handle, entity, &text_style_names);
|
dispatch::properties_sectioned(handle, entity, &text_style_names);
|
||||||
if box_primitive {
|
if specialized_primitive {
|
||||||
sections.retain(|section| {
|
sections.retain(|section| {
|
||||||
!section.props.iter().any(|property| {
|
!section.props.iter().any(|property| {
|
||||||
property.field.starts_with("acis_")
|
property.field.starts_with("acis_")
|
||||||
@@ -620,7 +620,7 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !box_primitive && matches!(
|
if !specialized_primitive && matches!(
|
||||||
entity,
|
entity,
|
||||||
acadrust::EntityType::Solid3D(_)
|
acadrust::EntityType::Solid3D(_)
|
||||||
| acadrust::EntityType::Region(_)
|
| acadrust::EntityType::Region(_)
|
||||||
@@ -694,7 +694,7 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !box_primitive {
|
if !specialized_primitive {
|
||||||
sections.extend(crate::entities::object_data::sections(
|
sections.extend(crate::entities::object_data::sections(
|
||||||
&self.tabs[i].scene.document,
|
&self.tabs[i].scene.document,
|
||||||
&self.tabs[i].scene.object_data_cache,
|
&self.tabs[i].scene.object_data_cache,
|
||||||
@@ -2386,7 +2386,7 @@ impl OpenCADStudio {
|
|||||||
annotation_scale_handle,
|
annotation_scale_handle,
|
||||||
);
|
);
|
||||||
let mut entity_grips = dispatch::grips(contextual.as_ref());
|
let mut entity_grips = dispatch::grips(contextual.as_ref());
|
||||||
if crate::scene::model::solid_history::is_box_primitive(
|
if crate::scene::model::solid_history::has_specialized_primitive_properties(
|
||||||
&self.tabs[i].scene.document,
|
&self.tabs[i].scene.document,
|
||||||
handle,
|
handle,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ fn plane_distance(a: DVec3, b: DVec3, plane: WorkingPlane) -> f64 {
|
|||||||
d.x.hypot(d.y)
|
d.x.hypot(d.y)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tangent_object_local(object: TangentObject, plane: WorkingPlane) -> TangentObject {
|
pub(crate) fn tangent_object_local(object: TangentObject, plane: WorkingPlane) -> TangentObject {
|
||||||
match object {
|
match object {
|
||||||
TangentObject::Line { p1, p2 } => TangentObject::Line {
|
TangentObject::Line { p1, p2 } => TangentObject::Line {
|
||||||
p1: plane.to_local(p1),
|
p1: plane.to_local(p1),
|
||||||
@@ -594,7 +594,7 @@ fn solve_quadratic(a: f64, b: f64, c: f64) -> Vec<f64> {
|
|||||||
vec![(-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a)]
|
vec![(-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a)]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn best_of(candidates: &[DVec3], hint: DVec3) -> Option<DVec3> {
|
pub(crate) fn best_of(candidates: &[DVec3], hint: DVec3) -> Option<DVec3> {
|
||||||
candidates.iter().copied().min_by(|a, b| {
|
candidates.iter().copied().min_by(|a, b| {
|
||||||
a.distance(hint)
|
a.distance(hint)
|
||||||
.partial_cmp(&b.distance(hint))
|
.partial_cmp(&b.distance(hint))
|
||||||
@@ -627,7 +627,7 @@ fn tangent_curve(object: TangentObject) -> KernelCurve {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ttr_candidates(first: TangentObject, second: TangentObject, radius: f64) -> Vec<DVec3> {
|
pub(crate) fn ttr_candidates(first: TangentObject, second: TangentObject, radius: f64) -> Vec<DVec3> {
|
||||||
let first = tangent_curve(first);
|
let first = tangent_curve(first);
|
||||||
let second = tangent_curve(second);
|
let second = tangent_curve(second);
|
||||||
fillets_between(&first, &second, radius, Tolerance::default())
|
fillets_between(&first, &second, radius, Tolerance::default())
|
||||||
|
|||||||
@@ -0,0 +1,796 @@
|
|||||||
|
use acadrust::entities::Solid3D;
|
||||||
|
use acadrust::objects::SolidHistoryOperation;
|
||||||
|
use acadrust::EntityType;
|
||||||
|
use cadkernel::brep::Body;
|
||||||
|
use glam::DVec3;
|
||||||
|
|
||||||
|
use crate::command::{
|
||||||
|
CadCommand, CmdOption, CmdResult, DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec,
|
||||||
|
TangentObject, WorkingPlane,
|
||||||
|
};
|
||||||
|
use crate::scene::model::solid_model;
|
||||||
|
use crate::scene::model::wire_model::WireModel;
|
||||||
|
use crate::t;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum Step {
|
||||||
|
BaseCenter,
|
||||||
|
BaseRadius,
|
||||||
|
BaseDiameter,
|
||||||
|
ThreePointFirst,
|
||||||
|
ThreePointSecond(DVec3),
|
||||||
|
ThreePointThird(DVec3, DVec3),
|
||||||
|
TwoPointFirst,
|
||||||
|
TwoPointSecond(DVec3),
|
||||||
|
EllipseFirst,
|
||||||
|
EllipseCenter,
|
||||||
|
EllipseCenterFirstAxis(DVec3),
|
||||||
|
EllipseCenterSecondAxis(DVec3, DVec3),
|
||||||
|
EllipseSecond(DVec3),
|
||||||
|
EllipseThird(DVec3, DVec3),
|
||||||
|
TtrFirst,
|
||||||
|
TtrSecond { object: TangentObject, hit: DVec3 },
|
||||||
|
TtrRadius {
|
||||||
|
first: TangentObject,
|
||||||
|
second: TangentObject,
|
||||||
|
first_hit: DVec3,
|
||||||
|
second_hit: DVec3,
|
||||||
|
},
|
||||||
|
Height,
|
||||||
|
HeightFirstPoint,
|
||||||
|
HeightSecondPoint(DVec3),
|
||||||
|
AxisEndpoint,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct Defaults {
|
||||||
|
major_radius: f64,
|
||||||
|
minor_radius: f64,
|
||||||
|
height: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Defaults {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
major_radius: 1.0,
|
||||||
|
minor_radius: 1.0,
|
||||||
|
height: 1.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn defaults() -> &'static std::sync::Mutex<Defaults> {
|
||||||
|
static DEFAULTS: std::sync::OnceLock<std::sync::Mutex<Defaults>> =
|
||||||
|
std::sync::OnceLock::new();
|
||||||
|
DEFAULTS.get_or_init(|| std::sync::Mutex::new(Defaults::default()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CylinderCommand {
|
||||||
|
step: Step,
|
||||||
|
frame: Option<WorkingPlane>,
|
||||||
|
major_radius: f64,
|
||||||
|
minor_radius: f64,
|
||||||
|
remembered: Defaults,
|
||||||
|
plane: WorkingPlane,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CylinderCommand {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let remembered = defaults()
|
||||||
|
.lock()
|
||||||
|
.map(|value| *value)
|
||||||
|
.unwrap_or_default();
|
||||||
|
Self {
|
||||||
|
step: Step::BaseCenter,
|
||||||
|
frame: None,
|
||||||
|
major_radius: remembered.major_radius,
|
||||||
|
minor_radius: remembered.minor_radius,
|
||||||
|
remembered,
|
||||||
|
plane: WorkingPlane::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn frame_at(&self, center: DVec3) -> WorkingPlane {
|
||||||
|
WorkingPlane {
|
||||||
|
origin: center,
|
||||||
|
x: self.plane.x,
|
||||||
|
y: self.plane.y,
|
||||||
|
z: self.plane.z,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_base(&mut self, mut frame: WorkingPlane, mut x_radius: f64, mut y_radius: f64) -> bool {
|
||||||
|
if !frame.origin.is_finite()
|
||||||
|
|| !frame.x.is_finite()
|
||||||
|
|| !frame.y.is_finite()
|
||||||
|
|| !frame.z.is_finite()
|
||||||
|
|| !x_radius.is_finite()
|
||||||
|
|| !y_radius.is_finite()
|
||||||
|
|| x_radius < 1e-6
|
||||||
|
|| y_radius < 1e-6
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if y_radius > x_radius {
|
||||||
|
std::mem::swap(&mut x_radius, &mut y_radius);
|
||||||
|
frame = WorkingPlane {
|
||||||
|
origin: frame.origin,
|
||||||
|
x: frame.y,
|
||||||
|
y: -frame.x,
|
||||||
|
z: frame.z,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
self.frame = Some(frame);
|
||||||
|
self.major_radius = x_radius;
|
||||||
|
self.minor_radius = y_radius;
|
||||||
|
self.step = Step::Height;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn three_point_frame(a: DVec3, b: DVec3, c: DVec3) -> Option<(WorkingPlane, f64)> {
|
||||||
|
let normal = (b - a).cross(c - a).try_normalize()?;
|
||||||
|
let first_axis = (b - a).try_normalize()?;
|
||||||
|
let second_axis = normal.cross(first_axis).try_normalize()?;
|
||||||
|
let plane = WorkingPlane::new(a, first_axis, second_axis);
|
||||||
|
let local_b = plane.to_local(b);
|
||||||
|
let local_c = plane.to_local(c);
|
||||||
|
let circle = cadkernel::geom2d::arc_through_points(
|
||||||
|
[0.0, 0.0],
|
||||||
|
[local_b.x, local_b.y],
|
||||||
|
[local_c.x, local_c.y],
|
||||||
|
)?;
|
||||||
|
let center = plane.to_world(DVec3::new(circle.centre[0], circle.centre[1], 0.0));
|
||||||
|
let x = (a - center).try_normalize()?;
|
||||||
|
let y = normal.cross(x).try_normalize()?;
|
||||||
|
Some((WorkingPlane::new(center, x, y), circle.radius))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn axis_frame(&self, axis: DVec3) -> Option<(WorkingPlane, f64)> {
|
||||||
|
let current = self.frame?;
|
||||||
|
let height = axis.length();
|
||||||
|
if !height.is_finite() || height < 1e-6 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let z = axis / height;
|
||||||
|
let mut x = current.x - z * current.x.dot(z);
|
||||||
|
if x.length_squared() < 1e-12 {
|
||||||
|
x = current.y - z * current.y.dot(z);
|
||||||
|
}
|
||||||
|
let x = x.try_normalize()?;
|
||||||
|
let y = z.cross(x).try_normalize()?;
|
||||||
|
Some((WorkingPlane::new(current.origin, x, y), height))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn history_transform(frame: WorkingPlane) -> [f64; 16] {
|
||||||
|
glam::DMat4::from_cols(
|
||||||
|
frame.x.extend(0.0),
|
||||||
|
frame.y.extend(0.0),
|
||||||
|
frame.z.extend(0.0),
|
||||||
|
frame.origin.extend(1.0),
|
||||||
|
)
|
||||||
|
.to_cols_array()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(&self, axis: DVec3) -> Option<(Body, SolidHistoryOperation, f64)> {
|
||||||
|
let (frame, height) = self.axis_frame(axis)?;
|
||||||
|
let local = solid_model::elliptical_cylinder_solid(
|
||||||
|
[0.0; 3],
|
||||||
|
self.major_radius,
|
||||||
|
self.minor_radius,
|
||||||
|
height,
|
||||||
|
)?;
|
||||||
|
let placed = solid_model::placed(
|
||||||
|
&local,
|
||||||
|
frame.x.to_array(),
|
||||||
|
frame.y.to_array(),
|
||||||
|
frame.z.to_array(),
|
||||||
|
frame.origin.to_array(),
|
||||||
|
)?;
|
||||||
|
Some((
|
||||||
|
placed,
|
||||||
|
crate::scene::model::solid_history::elliptical_cylinder_op(
|
||||||
|
Self::history_transform(frame),
|
||||||
|
self.major_radius,
|
||||||
|
self.minor_radius,
|
||||||
|
height,
|
||||||
|
),
|
||||||
|
height,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn commit_axis(&self, axis: DVec3) -> CmdResult {
|
||||||
|
let Some((solid, history, height)) = self.build(axis) else {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
};
|
||||||
|
if let Ok(mut remembered) = defaults().lock() {
|
||||||
|
*remembered = Defaults {
|
||||||
|
major_radius: self.major_radius,
|
||||||
|
minor_radius: self.minor_radius,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn commit_height(&self, height: f64) -> CmdResult {
|
||||||
|
let Some(frame) = self.frame else {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
};
|
||||||
|
if !height.is_finite() || height.abs() < 1e-6 {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
}
|
||||||
|
self.commit_axis(frame.z * height)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn height_at(&self, point: DVec3) -> Option<f64> {
|
||||||
|
let frame = self.frame?;
|
||||||
|
Some((point - frame.origin).dot(frame.z))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ttr_center(&self, radius: f64) -> Option<DVec3> {
|
||||||
|
let Step::TtrRadius {
|
||||||
|
first,
|
||||||
|
second,
|
||||||
|
first_hit,
|
||||||
|
second_hit,
|
||||||
|
} = self.step
|
||||||
|
else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let candidates = crate::modules::draw::draw::circle::ttr_candidates(first, second, radius);
|
||||||
|
let local = crate::modules::draw::draw::circle::best_of(
|
||||||
|
&candidates,
|
||||||
|
(first_hit + second_hit) * 0.5,
|
||||||
|
)?;
|
||||||
|
Some(self.plane.to_world(local))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_preview(&self, frame: WorkingPlane, x_radius: f64, y_radius: f64) -> WireModel {
|
||||||
|
let mut points = Vec::new();
|
||||||
|
push_ellipse(
|
||||||
|
&mut points,
|
||||||
|
frame.origin,
|
||||||
|
frame.x,
|
||||||
|
frame.y,
|
||||||
|
x_radius.max(1e-9),
|
||||||
|
y_radius.max(1e-9),
|
||||||
|
);
|
||||||
|
wire("cylinder_base_preview", points)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn solid_preview(&self, axis: DVec3) -> Option<WireModel> {
|
||||||
|
let (frame, height) = self.axis_frame(axis)?;
|
||||||
|
let top = frame.origin + frame.z * height;
|
||||||
|
let mut points = Vec::new();
|
||||||
|
push_ellipse(
|
||||||
|
&mut points,
|
||||||
|
frame.origin,
|
||||||
|
frame.x,
|
||||||
|
frame.y,
|
||||||
|
self.major_radius,
|
||||||
|
self.minor_radius,
|
||||||
|
);
|
||||||
|
push_ellipse(
|
||||||
|
&mut points,
|
||||||
|
top,
|
||||||
|
frame.x,
|
||||||
|
frame.y,
|
||||||
|
self.major_radius,
|
||||||
|
self.minor_radius,
|
||||||
|
);
|
||||||
|
for index in 0..4 {
|
||||||
|
let angle = index as f64 * std::f64::consts::FRAC_PI_2;
|
||||||
|
let base = frame.origin
|
||||||
|
+ frame.x * (self.major_radius * angle.cos())
|
||||||
|
+ frame.y * (self.minor_radius * angle.sin());
|
||||||
|
push_segment(&mut points, base, base + frame.z * height);
|
||||||
|
}
|
||||||
|
Some(wire("cylinder_height_preview", points))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_tangent(&mut self, object: TangentObject, hit: DVec3) -> CmdResult {
|
||||||
|
let object = crate::modules::draw::draw::circle::tangent_object_local(object, self.plane);
|
||||||
|
let hit = self.plane.to_local(hit);
|
||||||
|
match self.step {
|
||||||
|
Step::TtrFirst => self.step = Step::TtrSecond { object, hit },
|
||||||
|
Step::TtrSecond {
|
||||||
|
object: first,
|
||||||
|
hit: first_hit,
|
||||||
|
} => {
|
||||||
|
self.step = Step::TtrRadius {
|
||||||
|
first,
|
||||||
|
second: object,
|
||||||
|
first_hit,
|
||||||
|
second_hit: hit,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_text(&mut self, raw: &str) -> Option<CmdResult> {
|
||||||
|
let token = raw.trim();
|
||||||
|
let upper = token.to_uppercase();
|
||||||
|
match self.step {
|
||||||
|
Step::BaseCenter => {
|
||||||
|
self.step = match upper.as_str() {
|
||||||
|
"3P" => Step::ThreePointFirst,
|
||||||
|
"2P" => Step::TwoPointFirst,
|
||||||
|
"T" | "TTR" => Step::TtrFirst,
|
||||||
|
"E" | "ELLIPTICAL" => Step::EllipseFirst,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
return Some(CmdResult::NeedPoint);
|
||||||
|
}
|
||||||
|
Step::BaseRadius if matches!(upper.as_str(), "D" | "DIAMETER") => {
|
||||||
|
self.step = Step::BaseDiameter;
|
||||||
|
return Some(CmdResult::NeedPoint);
|
||||||
|
}
|
||||||
|
Step::EllipseFirst if matches!(upper.as_str(), "C" | "CENTER") => {
|
||||||
|
self.step = Step::EllipseCenter;
|
||||||
|
return Some(CmdResult::NeedPoint);
|
||||||
|
}
|
||||||
|
Step::Height => {
|
||||||
|
self.step = match upper.as_str() {
|
||||||
|
"2P" | "2POINT" => Step::HeightFirstPoint,
|
||||||
|
"A" | "AXIS" | "AXIS ENDPOINT" => Step::AxisEndpoint,
|
||||||
|
_ => {
|
||||||
|
let height = crate::entities::common::parse_typed_length(token)?;
|
||||||
|
return Some(self.commit_height(height));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return Some(CmdResult::NeedPoint);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
let number = crate::entities::common::parse_typed_length(token)?;
|
||||||
|
match self.step {
|
||||||
|
Step::BaseRadius => {
|
||||||
|
let frame = self.frame?;
|
||||||
|
(number > 0.0).then(|| {
|
||||||
|
self.set_base(frame, number, number);
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Step::BaseDiameter => {
|
||||||
|
let frame = self.frame?;
|
||||||
|
(number > 0.0).then(|| {
|
||||||
|
self.set_base(frame, number * 0.5, number * 0.5);
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Step::EllipseCenterFirstAxis(center) => (number > 0.0).then(|| {
|
||||||
|
self.step = Step::EllipseCenterSecondAxis(
|
||||||
|
center,
|
||||||
|
center + self.plane.x * number,
|
||||||
|
);
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}),
|
||||||
|
Step::TtrRadius { .. } => {
|
||||||
|
let center = self.ttr_center(number)?;
|
||||||
|
(number > 0.0).then(|| {
|
||||||
|
self.set_base(self.frame_at(center), number, number);
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_preview(&self, point: DVec3) -> Option<WireModel> {
|
||||||
|
match self.step {
|
||||||
|
Step::BaseRadius | Step::BaseDiameter => {
|
||||||
|
let frame = self.frame?;
|
||||||
|
let local = frame.to_local(point);
|
||||||
|
let radius = local.x.hypot(local.y);
|
||||||
|
Some(self.base_preview(frame, radius, radius))
|
||||||
|
}
|
||||||
|
Step::ThreePointSecond(first) | Step::TwoPointSecond(first) => {
|
||||||
|
let a = self.plane.to_local(first);
|
||||||
|
let b = self.plane.to_local(point);
|
||||||
|
let center = self.plane.to_world((a + b) * 0.5);
|
||||||
|
let radius = (b - a).truncate().length() * 0.5;
|
||||||
|
Some(self.base_preview(self.frame_at(center), radius, radius))
|
||||||
|
}
|
||||||
|
Step::ThreePointThird(first, second) => {
|
||||||
|
let (frame, radius) = Self::three_point_frame(first, second, point)?;
|
||||||
|
Some(self.base_preview(frame, radius, radius))
|
||||||
|
}
|
||||||
|
Step::EllipseThird(first, second) => {
|
||||||
|
let a = self.plane.to_local(first);
|
||||||
|
let b = self.plane.to_local(second);
|
||||||
|
let center_local = (a + b) * 0.5;
|
||||||
|
let axis = (b - a).truncate();
|
||||||
|
let x2 = axis.try_normalize()?;
|
||||||
|
let x = self.plane.vector_to_world(DVec3::new(x2.x, x2.y, 0.0));
|
||||||
|
let y = self.plane.z.cross(x).normalize_or_zero();
|
||||||
|
let frame = WorkingPlane::new(self.plane.to_world(center_local), x, y);
|
||||||
|
let local = frame.to_local(point);
|
||||||
|
Some(self.base_preview(frame, axis.length() * 0.5, local.y.abs()))
|
||||||
|
}
|
||||||
|
Step::EllipseCenterSecondAxis(center, first_axis_end) => {
|
||||||
|
let axis = first_axis_end - center;
|
||||||
|
let x = axis.try_normalize()?;
|
||||||
|
let y = self.plane.z.cross(x).normalize_or_zero();
|
||||||
|
let frame = WorkingPlane::new(center, x, y);
|
||||||
|
let local = frame.to_local(point);
|
||||||
|
Some(self.base_preview(frame, axis.length(), local.y.abs()))
|
||||||
|
}
|
||||||
|
Step::TtrRadius { second_hit, .. } => {
|
||||||
|
let local = self.plane.to_local(point);
|
||||||
|
let radius = (local - second_hit).truncate().length();
|
||||||
|
let center = self.ttr_center(radius)?;
|
||||||
|
Some(self.base_preview(self.frame_at(center), radius, radius))
|
||||||
|
}
|
||||||
|
Step::Height => {
|
||||||
|
let frame = self.frame?;
|
||||||
|
self.solid_preview(frame.z * self.height_at(point)?)
|
||||||
|
}
|
||||||
|
Step::HeightSecondPoint(first) => {
|
||||||
|
let frame = self.frame?;
|
||||||
|
self.solid_preview(frame.z * first.distance(point))
|
||||||
|
}
|
||||||
|
Step::AxisEndpoint => {
|
||||||
|
let frame = self.frame?;
|
||||||
|
self.solid_preview(point - frame.origin)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CadCommand for CylinderCommand {
|
||||||
|
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||||
|
self.plane = plane;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursor_axis(&self) -> Option<(DVec3, DVec3)> {
|
||||||
|
matches!(self.step, Step::Height).then(|| {
|
||||||
|
let frame = self.frame.unwrap_or(self.plane);
|
||||||
|
(frame.origin, frame.z.normalize_or_zero())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"CYLINDER"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt(&self) -> String {
|
||||||
|
match self.step {
|
||||||
|
Step::BaseCenter =>
|
||||||
|
t!("CYLINDER Specify center point of base or [3P/2P/Ttr/Elliptical]:").into_owned(),
|
||||||
|
Step::BaseRadius => crate::tf!(
|
||||||
|
"CYLINDER Specify base radius or [Diameter] <{:.4}>:",
|
||||||
|
self.remembered.major_radius
|
||||||
|
).into_owned(),
|
||||||
|
Step::BaseDiameter => crate::tf!(
|
||||||
|
"CYLINDER Specify base diameter <{:.4}>:",
|
||||||
|
self.remembered.major_radius * 2.0
|
||||||
|
).into_owned(),
|
||||||
|
Step::ThreePointFirst => t!("CYLINDER Specify first point on base:").into_owned(),
|
||||||
|
Step::ThreePointSecond(_) => t!("CYLINDER Specify second point on base:").into_owned(),
|
||||||
|
Step::ThreePointThird(_, _) => t!("CYLINDER Specify third point on base:").into_owned(),
|
||||||
|
Step::TwoPointFirst => t!("CYLINDER Specify first endpoint of base diameter:").into_owned(),
|
||||||
|
Step::TwoPointSecond(_) => t!("CYLINDER Specify second endpoint of base diameter:").into_owned(),
|
||||||
|
Step::EllipseFirst =>
|
||||||
|
t!("CYLINDER Specify endpoint of first axis or [Center]:").into_owned(),
|
||||||
|
Step::EllipseCenter => t!("CYLINDER Specify center point:").into_owned(),
|
||||||
|
Step::EllipseCenterFirstAxis(_) => crate::tf!(
|
||||||
|
"CYLINDER Specify distance to first axis <{:.4}>:",
|
||||||
|
self.remembered.major_radius
|
||||||
|
).into_owned(),
|
||||||
|
Step::EllipseCenterSecondAxis(_, _) =>
|
||||||
|
t!("CYLINDER Specify endpoint of second axis:").into_owned(),
|
||||||
|
Step::EllipseSecond(_) =>
|
||||||
|
t!("CYLINDER Specify other endpoint of first axis:").into_owned(),
|
||||||
|
Step::EllipseThird(_, _) =>
|
||||||
|
t!("CYLINDER Specify endpoint of second axis:").into_owned(),
|
||||||
|
Step::TtrFirst => t!("CYLINDER Select first tangent object:").into_owned(),
|
||||||
|
Step::TtrSecond { .. } => t!("CYLINDER Select second tangent object:").into_owned(),
|
||||||
|
Step::TtrRadius { .. } => crate::tf!(
|
||||||
|
"CYLINDER Specify base radius <{:.4}>:",
|
||||||
|
self.remembered.major_radius
|
||||||
|
).into_owned(),
|
||||||
|
Step::Height => crate::tf!(
|
||||||
|
"CYLINDER Specify height or [2Point/Axis endpoint] <{:.4}>:",
|
||||||
|
self.remembered.height
|
||||||
|
).into_owned(),
|
||||||
|
Step::HeightFirstPoint =>
|
||||||
|
t!("CYLINDER Specify first point for height:").into_owned(),
|
||||||
|
Step::HeightSecondPoint(_) =>
|
||||||
|
t!("CYLINDER Specify second point for height:").into_owned(),
|
||||||
|
Step::AxisEndpoint => t!("CYLINDER Specify axis endpoint:").into_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
|
match self.step {
|
||||||
|
Step::BaseCenter => vec![
|
||||||
|
CmdOption::new("3P", "3P"),
|
||||||
|
CmdOption::new("2P", "2P"),
|
||||||
|
CmdOption::new("Ttr", "TTR"),
|
||||||
|
CmdOption::new(t!("Elliptical").as_ref(), "E"),
|
||||||
|
],
|
||||||
|
Step::BaseRadius => vec![CmdOption::new(t!("Diameter").as_ref(), "D")],
|
||||||
|
Step::EllipseFirst => vec![CmdOption::new(t!("Center").as_ref(), "C")],
|
||||||
|
Step::Height => vec![
|
||||||
|
CmdOption::new("2Point", "2P"),
|
||||||
|
CmdOption::new(t!("Axis endpoint").as_ref(), "A"),
|
||||||
|
],
|
||||||
|
_ => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||||
|
match self.step {
|
||||||
|
Step::BaseCenter => {
|
||||||
|
self.frame = Some(self.frame_at(point));
|
||||||
|
self.step = Step::BaseRadius;
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
Step::BaseRadius | Step::BaseDiameter => {
|
||||||
|
let Some(frame) = self.frame else {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
};
|
||||||
|
let local = frame.to_local(point);
|
||||||
|
let radius = local.x.hypot(local.y);
|
||||||
|
self.set_base(frame, radius, radius);
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
Step::ThreePointFirst => {
|
||||||
|
self.step = Step::ThreePointSecond(point);
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
Step::ThreePointSecond(first) => {
|
||||||
|
if point.distance_squared(first) > 1e-12 {
|
||||||
|
self.step = Step::ThreePointThird(first, point);
|
||||||
|
}
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
Step::ThreePointThird(first, second) => {
|
||||||
|
if let Some((frame, radius)) = Self::three_point_frame(first, second, point) {
|
||||||
|
self.set_base(frame, radius, radius);
|
||||||
|
}
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
Step::TwoPointFirst => {
|
||||||
|
self.step = Step::TwoPointSecond(point);
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
Step::TwoPointSecond(first) => {
|
||||||
|
let a = self.plane.to_local(first);
|
||||||
|
let b = self.plane.to_local(point);
|
||||||
|
let radius = (b - a).truncate().length() * 0.5;
|
||||||
|
let center = self.plane.to_world((a + b) * 0.5);
|
||||||
|
self.set_base(self.frame_at(center), radius, radius);
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
Step::EllipseFirst => {
|
||||||
|
self.step = Step::EllipseSecond(point);
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
Step::EllipseCenter => {
|
||||||
|
self.step = Step::EllipseCenterFirstAxis(point);
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
Step::EllipseCenterFirstAxis(center) => {
|
||||||
|
if point.distance_squared(center) > 1e-12 {
|
||||||
|
self.step = Step::EllipseCenterSecondAxis(center, point);
|
||||||
|
}
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
Step::EllipseCenterSecondAxis(center, first_axis_end) => {
|
||||||
|
let axis = first_axis_end - center;
|
||||||
|
if let Some(x) = axis.try_normalize() {
|
||||||
|
let y = self.plane.z.cross(x).normalize_or_zero();
|
||||||
|
let frame = WorkingPlane::new(center, x, y);
|
||||||
|
let local = frame.to_local(point);
|
||||||
|
self.set_base(frame, axis.length(), local.y.abs());
|
||||||
|
}
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
Step::EllipseSecond(first) => {
|
||||||
|
if point.distance_squared(first) > 1e-12 {
|
||||||
|
self.step = Step::EllipseThird(first, point);
|
||||||
|
}
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
Step::EllipseThird(first, second) => {
|
||||||
|
let a = self.plane.to_local(first);
|
||||||
|
let b = self.plane.to_local(second);
|
||||||
|
let center_local = (a + b) * 0.5;
|
||||||
|
let axis = (b - a).truncate();
|
||||||
|
let x_radius = axis.length() * 0.5;
|
||||||
|
if let Some(x2) = axis.try_normalize() {
|
||||||
|
let x = self.plane.vector_to_world(DVec3::new(x2.x, x2.y, 0.0));
|
||||||
|
let y = self.plane.z.cross(x).normalize_or_zero();
|
||||||
|
let frame = WorkingPlane::new(self.plane.to_world(center_local), x, y);
|
||||||
|
let local = frame.to_local(point);
|
||||||
|
self.set_base(frame, x_radius, local.y.abs());
|
||||||
|
}
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
Step::TtrRadius { second_hit, .. } => {
|
||||||
|
let local = self.plane.to_local(point);
|
||||||
|
let radius = (local - second_hit).truncate().length();
|
||||||
|
if let Some(center) = self.ttr_center(radius) {
|
||||||
|
self.set_base(self.frame_at(center), radius, radius);
|
||||||
|
}
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
Step::Height => self
|
||||||
|
.height_at(point)
|
||||||
|
.map(|height| self.commit_height(height))
|
||||||
|
.unwrap_or(CmdResult::NeedPoint),
|
||||||
|
Step::HeightFirstPoint => {
|
||||||
|
self.step = Step::HeightSecondPoint(point);
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
Step::HeightSecondPoint(first) => self.commit_height(first.distance(point)),
|
||||||
|
Step::AxisEndpoint => {
|
||||||
|
let Some(frame) = self.frame else {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
};
|
||||||
|
self.commit_axis(point - frame.origin)
|
||||||
|
}
|
||||||
|
Step::TtrFirst | Step::TtrSecond { .. } => CmdResult::NeedPoint,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
|
match self.step {
|
||||||
|
Step::BaseRadius | Step::BaseDiameter => {
|
||||||
|
let Some(frame) = self.frame else {
|
||||||
|
return CmdResult::Cancel;
|
||||||
|
};
|
||||||
|
self.set_base(
|
||||||
|
frame,
|
||||||
|
self.remembered.major_radius,
|
||||||
|
self.remembered.major_radius,
|
||||||
|
);
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
Step::TtrRadius { .. } => {
|
||||||
|
let radius = self.remembered.major_radius;
|
||||||
|
let Some(center) = self.ttr_center(radius) else {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
};
|
||||||
|
self.set_base(self.frame_at(center), radius, radius);
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
Step::EllipseCenterFirstAxis(center) => {
|
||||||
|
self.step = Step::EllipseCenterSecondAxis(
|
||||||
|
center,
|
||||||
|
center + self.plane.x * self.remembered.major_radius,
|
||||||
|
);
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
Step::Height => self.commit_height(self.remembered.height),
|
||||||
|
_ => CmdResult::Cancel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn needs_tangent_pick(&self) -> bool {
|
||||||
|
matches!(self.step, Step::TtrFirst | Step::TtrSecond { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_tangent_point(&mut self, object: TangentObject, hit: DVec3) -> CmdResult {
|
||||||
|
self.on_tangent(object, hit)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wants_text_input(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self.step,
|
||||||
|
Step::BaseCenter
|
||||||
|
| Step::BaseRadius
|
||||||
|
| Step::BaseDiameter
|
||||||
|
| Step::EllipseFirst
|
||||||
|
| Step::EllipseCenterFirstAxis(_)
|
||||||
|
| Step::TtrRadius { .. }
|
||||||
|
| Step::Height
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point_step_accepts_keywords(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self.step,
|
||||||
|
Step::BaseCenter | Step::BaseRadius | Step::EllipseFirst | Step::Height
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_text_input(&mut self, raw: &str) -> Option<CmdResult> {
|
||||||
|
self.on_text(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
|
||||||
|
self.mouse_preview(point)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dyn_spec(&self) -> Option<DynSpec> {
|
||||||
|
let frame = self.frame.unwrap_or(self.plane);
|
||||||
|
let role = match self.step {
|
||||||
|
Step::BaseRadius | Step::TtrRadius { .. } => DynRole::Radius,
|
||||||
|
Step::BaseDiameter => DynRole::Diameter,
|
||||||
|
Step::EllipseCenterFirstAxis(_) => DynRole::Distance,
|
||||||
|
Step::Height | Step::HeightSecondPoint(_) | Step::AxisEndpoint => DynRole::Height,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some(DynSpec {
|
||||||
|
anchor: DynAnchor::Point(frame.origin),
|
||||||
|
fields: vec![DynFieldSpec::new(role)],
|
||||||
|
guide: if matches!(role, DynRole::Radius | DynRole::Diameter) {
|
||||||
|
DynGuide::Radius
|
||||||
|
} else {
|
||||||
|
DynGuide::None
|
||||||
|
},
|
||||||
|
ref_point: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
|
||||||
|
match self.step {
|
||||||
|
Step::BaseRadius => {
|
||||||
|
let local = self.frame?.to_local(cursor);
|
||||||
|
Some(local.x.hypot(local.y))
|
||||||
|
}
|
||||||
|
Step::BaseDiameter => {
|
||||||
|
let local = self.frame?.to_local(cursor);
|
||||||
|
Some(local.x.hypot(local.y) * 2.0)
|
||||||
|
}
|
||||||
|
Step::TtrRadius { second_hit, .. } => {
|
||||||
|
let local = self.plane.to_local(cursor);
|
||||||
|
Some((local - second_hit).truncate().length())
|
||||||
|
}
|
||||||
|
Step::EllipseCenterFirstAxis(center) => Some(center.distance(cursor)),
|
||||||
|
Step::Height => self.height_at(cursor).map(f64::abs),
|
||||||
|
Step::HeightSecondPoint(first) => Some(first.distance(cursor)),
|
||||||
|
Step::AxisEndpoint => self.frame.map(|frame| frame.origin.distance(cursor)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_break(points: &mut Vec<[f32; 3]>) {
|
||||||
|
if !points.is_empty() {
|
||||||
|
points.push([f32::NAN; 3]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_segment(points: &mut Vec<[f32; 3]>, first: DVec3, second: DVec3) {
|
||||||
|
push_break(points);
|
||||||
|
points.extend([first.as_vec3().to_array(), second.as_vec3().to_array()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_ellipse(
|
||||||
|
points: &mut Vec<[f32; 3]>,
|
||||||
|
center: DVec3,
|
||||||
|
x_axis: DVec3,
|
||||||
|
y_axis: DVec3,
|
||||||
|
x_radius: f64,
|
||||||
|
y_radius: f64,
|
||||||
|
) {
|
||||||
|
push_break(points);
|
||||||
|
const SEGMENTS: usize = 64;
|
||||||
|
for index in 0..=SEGMENTS {
|
||||||
|
let angle = index as f64 / SEGMENTS as f64 * std::f64::consts::TAU;
|
||||||
|
let point = center
|
||||||
|
+ x_axis * (x_radius * angle.cos())
|
||||||
|
+ y_axis * (y_radius * angle.sin());
|
||||||
|
points.push(point.as_vec3().to_array());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wire(name: &str, points: Vec<[f32; 3]>) -> WireModel {
|
||||||
|
WireModel::solid(name.to_string(), points, WireModel::CYAN, false)
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
// Solid creation and kernel modelling tools.
|
// Solid creation and kernel modelling tools.
|
||||||
|
|
||||||
pub mod boolean_cmd;
|
pub mod boolean_cmd;
|
||||||
|
pub mod cylinder_cmd;
|
||||||
pub mod edge_cmd;
|
pub mod edge_cmd;
|
||||||
pub mod primitive_cmd;
|
pub mod primitive_cmd;
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ pub const GRIP_RADIUS: usize = 10_004;
|
|||||||
pub const GRIP_OUTER_RADIUS: usize = 10_005;
|
pub const GRIP_OUTER_RADIUS: usize = 10_005;
|
||||||
pub const GRIP_INNER_RADIUS: usize = 10_006;
|
pub const GRIP_INNER_RADIUS: usize = 10_006;
|
||||||
pub const GRIP_SIDES: usize = 10_007;
|
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_BOX_CORNER_FIRST: usize = 10_100;
|
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_MIN: usize = 10_110;
|
||||||
pub const GRIP_BOX_FACE_X_MAX: usize = 10_111;
|
pub const GRIP_BOX_FACE_X_MAX: usize = 10_111;
|
||||||
@@ -31,6 +33,9 @@ pub const PROP_LENGTH: &str = "solid_history_length";
|
|||||||
pub const PROP_WIDTH: &str = "solid_history_width";
|
pub const PROP_WIDTH: &str = "solid_history_width";
|
||||||
pub const PROP_HEIGHT: &str = "solid_history_height";
|
pub const PROP_HEIGHT: &str = "solid_history_height";
|
||||||
pub const PROP_RADIUS: &str = "solid_history_radius";
|
pub const PROP_RADIUS: &str = "solid_history_radius";
|
||||||
|
pub const PROP_MAJOR_RADIUS: &str = "solid_history_major_radius";
|
||||||
|
pub const PROP_MINOR_RADIUS: &str = "solid_history_minor_radius";
|
||||||
|
pub const PROP_ELLIPTICAL: &str = "solid_history_elliptical";
|
||||||
pub const PROP_OUTER_RADIUS: &str = "solid_history_outer_radius";
|
pub const PROP_OUTER_RADIUS: &str = "solid_history_outer_radius";
|
||||||
pub const PROP_INNER_RADIUS: &str = "solid_history_inner_radius";
|
pub const PROP_INNER_RADIUS: &str = "solid_history_inner_radius";
|
||||||
pub const PROP_SIDES: &str = "solid_history_sides";
|
pub const PROP_SIDES: &str = "solid_history_sides";
|
||||||
@@ -63,13 +68,13 @@ fn history_flags(
|
|||||||
Some((history.record_history, history.show_history))
|
Some((history.record_history, history.show_history))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_box_primitive(
|
pub fn has_specialized_primitive_properties(
|
||||||
document: &acadrust::CadDocument,
|
document: &acadrust::CadDocument,
|
||||||
handle: acadrust::Handle,
|
handle: acadrust::Handle,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
document.solid_history_operation(handle),
|
document.solid_history_operation(handle),
|
||||||
Some(SolidHistoryOperation::Box(_))
|
Some(SolidHistoryOperation::Box(_) | SolidHistoryOperation::Cylinder(_))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,10 +84,120 @@ pub fn reference_point(operation: &SolidHistoryOperation) -> Option<glam::DVec3>
|
|||||||
value.base.transform,
|
value.base.transform,
|
||||||
[value.length * 0.5, value.width * 0.5, 0.0],
|
[value.length * 0.5, value.width * 0.5, 0.0],
|
||||||
),
|
),
|
||||||
|
SolidHistoryOperation::Cylinder(value) => world_point(value.base.transform, [0.0; 3]),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cylinder_properties(
|
||||||
|
document: &acadrust::CadDocument,
|
||||||
|
handle: acadrust::Handle,
|
||||||
|
value: &SolidHistoryCylinder,
|
||||||
|
) -> Vec<PropSection> {
|
||||||
|
let Some(position) = world_point(value.base.transform, [0.0; 3]) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let scale = value
|
||||||
|
.major_radius
|
||||||
|
.abs()
|
||||||
|
.max(value.minor_radius.abs())
|
||||||
|
.max(1.0);
|
||||||
|
let elliptical = (value.major_radius - value.minor_radius).abs() > 1e-9 * scale;
|
||||||
|
let rotation = matrix(value.base.transform)
|
||||||
|
.map(|matrix| matrix.x_axis.y.atan2(matrix.x_axis.x))
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
let (record_history, show_history) = history_flags(document, handle).unwrap_or((false, false));
|
||||||
|
let show_value = if show_history { "Yes" } else { "No" };
|
||||||
|
let mut geometry = vec![
|
||||||
|
Property {
|
||||||
|
label: t!("Solid type").into_owned(),
|
||||||
|
field: "solid_history_type",
|
||||||
|
value: PropValue::ReadOnly(t!("Cylinder").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,
|
||||||
|
),
|
||||||
|
Property {
|
||||||
|
label: t!("Elliptical").into_owned(),
|
||||||
|
field: PROP_ELLIPTICAL,
|
||||||
|
value: PropValue::ReadOnly(if elliptical { "Yes" } else { "No" }.to_string()),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
if elliptical {
|
||||||
|
geometry.extend([
|
||||||
|
history_prop(
|
||||||
|
t!("Major radius").as_ref(),
|
||||||
|
PROP_MAJOR_RADIUS,
|
||||||
|
value.major_radius,
|
||||||
|
),
|
||||||
|
history_prop(
|
||||||
|
t!("Minor radius").as_ref(),
|
||||||
|
PROP_MINOR_RADIUS,
|
||||||
|
value.minor_radius,
|
||||||
|
),
|
||||||
|
Property {
|
||||||
|
label: t!("Rotation").into_owned(),
|
||||||
|
field: PROP_ROTATION,
|
||||||
|
value: PropValue::ReadOnly(crate::entities::common::format_direction(rotation)),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
geometry.push(history_prop(
|
||||||
|
t!("Radius").as_ref(),
|
||||||
|
PROP_RADIUS,
|
||||||
|
value.major_radius,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
geometry.push(history_prop(
|
||||||
|
t!("Height").as_ref(),
|
||||||
|
PROP_HEIGHT,
|
||||||
|
value.height,
|
||||||
|
));
|
||||||
|
vec![
|
||||||
|
PropSection {
|
||||||
|
title: t!("Geometry").into_owned(),
|
||||||
|
props: geometry,
|
||||||
|
},
|
||||||
|
PropSection {
|
||||||
|
title: t!("Solid History").into_owned(),
|
||||||
|
props: vec![
|
||||||
|
Property {
|
||||||
|
label: t!("History").into_owned(),
|
||||||
|
field: PROP_HISTORY,
|
||||||
|
value: PropValue::Choice {
|
||||||
|
selected: if record_history { "Record" } else { "None" }.to_string(),
|
||||||
|
options: vec!["None".to_string(), "Record".to_string()],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
label: t!("Show History").into_owned(),
|
||||||
|
field: PROP_SHOW_HISTORY,
|
||||||
|
value: if record_history {
|
||||||
|
PropValue::Choice {
|
||||||
|
selected: show_value.to_string(),
|
||||||
|
options: vec!["No".to_string(), "Yes".to_string()],
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
PropValue::ReadOnly(show_value.to_string())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
fn box_properties(
|
fn box_properties(
|
||||||
document: &acadrust::CadDocument,
|
document: &acadrust::CadDocument,
|
||||||
handle: acadrust::Handle,
|
handle: acadrust::Handle,
|
||||||
@@ -189,7 +304,10 @@ pub fn primitive_properties(
|
|||||||
history_prop(t!("Width").as_ref(), PROP_WIDTH, value.width),
|
history_prop(t!("Width").as_ref(), PROP_WIDTH, value.width),
|
||||||
history_prop(t!("Height").as_ref(), PROP_HEIGHT, value.height),
|
history_prop(t!("Height").as_ref(), PROP_HEIGHT, value.height),
|
||||||
],
|
],
|
||||||
SolidHistoryOperation::Cylinder(value) | SolidHistoryOperation::Cone(value) => vec![
|
SolidHistoryOperation::Cylinder(value) => {
|
||||||
|
return cylinder_properties(document, handle, value)
|
||||||
|
}
|
||||||
|
SolidHistoryOperation::Cone(value) => vec![
|
||||||
history_prop(t!("Radius").as_ref(), PROP_RADIUS, value.major_radius),
|
history_prop(t!("Radius").as_ref(), PROP_RADIUS, value.major_radius),
|
||||||
history_prop(t!("Height").as_ref(), PROP_HEIGHT, value.height),
|
history_prop(t!("Height").as_ref(), PROP_HEIGHT, value.height),
|
||||||
],
|
],
|
||||||
@@ -230,6 +348,8 @@ pub fn is_primitive_property(field: &str) -> bool {
|
|||||||
| PROP_WIDTH
|
| PROP_WIDTH
|
||||||
| PROP_HEIGHT
|
| PROP_HEIGHT
|
||||||
| PROP_RADIUS
|
| PROP_RADIUS
|
||||||
|
| PROP_MAJOR_RADIUS
|
||||||
|
| PROP_MINOR_RADIUS
|
||||||
| PROP_OUTER_RADIUS
|
| PROP_OUTER_RADIUS
|
||||||
| PROP_INNER_RADIUS
|
| PROP_INNER_RADIUS
|
||||||
| PROP_SIDES
|
| PROP_SIDES
|
||||||
@@ -355,6 +475,47 @@ fn apply_box_geometry_property(
|
|||||||
Some(true)
|
Some(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_cylinder_position_property(
|
||||||
|
value: &mut SolidHistoryCylinder,
|
||||||
|
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 origin = current.transform_point3(glam::DVec3::ZERO);
|
||||||
|
if !origin.is_finite() || target == origin[axis] {
|
||||||
|
return Some(false);
|
||||||
|
}
|
||||||
|
let mut delta = glam::DVec3::ZERO;
|
||||||
|
delta[axis] = target - origin[axis];
|
||||||
|
value.base.transform = (glam::DMat4::from_translation(delta) * current).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;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let Some(current) = matrix(value.base.transform) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
std::mem::swap(&mut value.major_radius, &mut value.minor_radius);
|
||||||
|
value.x_radius = value.major_radius;
|
||||||
|
value.base.transform =
|
||||||
|
(current * glam::DMat4::from_rotation_z(std::f64::consts::FRAC_PI_2)).to_cols_array();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
pub fn apply_primitive_property(
|
pub fn apply_primitive_property(
|
||||||
operation: &mut SolidHistoryOperation,
|
operation: &mut SolidHistoryOperation,
|
||||||
field: &str,
|
field: &str,
|
||||||
@@ -365,6 +526,11 @@ pub fn apply_primitive_property(
|
|||||||
return applied;
|
return applied;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let SolidHistoryOperation::Cylinder(cylinder_value) = operation {
|
||||||
|
if let Some(applied) = apply_cylinder_position_property(cylinder_value, field, value) {
|
||||||
|
return applied;
|
||||||
|
}
|
||||||
|
}
|
||||||
let Some(number) = (if field == PROP_SIDES {
|
let Some(number) = (if field == PROP_SIDES {
|
||||||
value.trim().parse::<f64>().ok()
|
value.trim().parse::<f64>().ok()
|
||||||
} else {
|
} else {
|
||||||
@@ -417,7 +583,31 @@ pub fn apply_primitive_property(
|
|||||||
PROP_HEIGHT => value.height = positive().unwrap_or(value.height),
|
PROP_HEIGHT => value.height = positive().unwrap_or(value.height),
|
||||||
_ => return false,
|
_ => return false,
|
||||||
},
|
},
|
||||||
SolidHistoryOperation::Cylinder(value) | SolidHistoryOperation::Cone(value) => match field {
|
SolidHistoryOperation::Cylinder(value) => match field {
|
||||||
|
PROP_RADIUS => {
|
||||||
|
let Some(radius) = positive() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
value.major_radius = radius;
|
||||||
|
value.minor_radius = radius;
|
||||||
|
value.x_radius = radius;
|
||||||
|
}
|
||||||
|
PROP_MAJOR_RADIUS => {
|
||||||
|
value.major_radius = positive().unwrap_or(value.major_radius);
|
||||||
|
if !canonicalize_cylinder_radii(value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PROP_MINOR_RADIUS => {
|
||||||
|
value.minor_radius = positive().unwrap_or(value.minor_radius);
|
||||||
|
if !canonicalize_cylinder_radii(value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PROP_HEIGHT => value.height = positive().unwrap_or(value.height),
|
||||||
|
_ => return false,
|
||||||
|
},
|
||||||
|
SolidHistoryOperation::Cone(value) => match field {
|
||||||
PROP_RADIUS => {
|
PROP_RADIUS => {
|
||||||
let Some(radius) = positive() else {
|
let Some(radius) = positive() else {
|
||||||
return false;
|
return false;
|
||||||
@@ -676,7 +866,45 @@ pub fn primitive_grips(
|
|||||||
Some([0.0, 0.0, 1.0]),
|
Some([0.0, 0.0, 1.0]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
SolidHistoryOperation::Cylinder(value) | SolidHistoryOperation::Cone(value) => {
|
SolidHistoryOperation::Cylinder(value) => {
|
||||||
|
let scale = value
|
||||||
|
.major_radius
|
||||||
|
.abs()
|
||||||
|
.max(value.minor_radius.abs())
|
||||||
|
.max(1.0);
|
||||||
|
if (value.major_radius - value.minor_radius).abs() > 1e-9 * scale {
|
||||||
|
add(
|
||||||
|
GRIP_MAJOR_RADIUS,
|
||||||
|
value.base.transform,
|
||||||
|
[value.major_radius, 0.0, value.height * 0.5],
|
||||||
|
GripShape::Square,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
add(
|
||||||
|
GRIP_MINOR_RADIUS,
|
||||||
|
value.base.transform,
|
||||||
|
[0.0, value.minor_radius, value.height * 0.5],
|
||||||
|
GripShape::Square,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
add(
|
||||||
|
GRIP_RADIUS,
|
||||||
|
value.base.transform,
|
||||||
|
[value.major_radius, 0.0, value.height * 0.5],
|
||||||
|
GripShape::Square,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
add(
|
||||||
|
GRIP_HEIGHT,
|
||||||
|
value.base.transform,
|
||||||
|
[0.0, 0.0, value.height],
|
||||||
|
GripShape::Square,
|
||||||
|
Some([0.0, 0.0, 1.0]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
SolidHistoryOperation::Cone(value) => {
|
||||||
add(
|
add(
|
||||||
GRIP_RADIUS,
|
GRIP_RADIUS,
|
||||||
value.base.transform,
|
value.base.transform,
|
||||||
@@ -825,7 +1053,31 @@ pub fn apply_primitive_grip(
|
|||||||
_ => return false,
|
_ => return false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SolidHistoryOperation::Cylinder(value) | SolidHistoryOperation::Cone(value) => {
|
SolidHistoryOperation::Cylinder(value) => {
|
||||||
|
match grip_id {
|
||||||
|
GRIP_RADIUS => {
|
||||||
|
let radius = local.x.hypot(local.y).max(1e-6);
|
||||||
|
value.major_radius = radius;
|
||||||
|
value.minor_radius = radius;
|
||||||
|
value.x_radius = radius;
|
||||||
|
}
|
||||||
|
GRIP_MAJOR_RADIUS => {
|
||||||
|
value.major_radius = local.x.abs().max(1e-6);
|
||||||
|
if !canonicalize_cylinder_radii(value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GRIP_MINOR_RADIUS => {
|
||||||
|
value.minor_radius = local.y.abs().max(1e-6);
|
||||||
|
if !canonicalize_cylinder_radii(value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GRIP_HEIGHT => value.height = local.z.max(1e-6),
|
||||||
|
_ => return false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SolidHistoryOperation::Cone(value) => {
|
||||||
match grip_id {
|
match grip_id {
|
||||||
GRIP_RADIUS => {
|
GRIP_RADIUS => {
|
||||||
let radius = local.x.hypot(local.y).max(1e-6);
|
let radius = local.x.hypot(local.y).max(1e-6);
|
||||||
@@ -930,6 +1182,23 @@ pub fn cylinder_op(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn elliptical_cylinder_op(
|
||||||
|
transform: [f64; 16],
|
||||||
|
major_radius: f64,
|
||||||
|
minor_radius: f64,
|
||||||
|
height: f64,
|
||||||
|
) -> SolidHistoryOperation {
|
||||||
|
SolidHistoryOperation::Cylinder(SolidHistoryCylinder {
|
||||||
|
base: base(transform),
|
||||||
|
operation_major: 1,
|
||||||
|
height,
|
||||||
|
major_radius,
|
||||||
|
minor_radius,
|
||||||
|
x_radius: major_radius,
|
||||||
|
..SolidHistoryCylinder::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn cone_op(
|
pub fn cone_op(
|
||||||
transform: [f64; 16],
|
transform: [f64; 16],
|
||||||
radius: f64,
|
radius: f64,
|
||||||
|
|||||||
@@ -40,6 +40,16 @@ pub fn cylinder_solid(center: [f64; 3], radius: f64, height: f64) -> Option<Body
|
|||||||
brep::make::cylinder(center, radius, height)
|
brep::make::cylinder(center, radius, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Circular or elliptical cylinder standing on the local z = base plane.
|
||||||
|
pub fn elliptical_cylinder_solid(
|
||||||
|
center: [f64; 3],
|
||||||
|
major_radius: f64,
|
||||||
|
minor_radius: f64,
|
||||||
|
height: f64,
|
||||||
|
) -> Option<Body> {
|
||||||
|
brep::make::elliptical_cylinder(center, major_radius, minor_radius, height)
|
||||||
|
}
|
||||||
|
|
||||||
/// Solid cone standing on the z = base plane, apex `height` above it.
|
/// Solid cone standing on the z = base plane, apex `height` above it.
|
||||||
pub fn cone_solid(center: [f64; 3], radius: f64, height: f64) -> Option<Body> {
|
pub fn cone_solid(center: [f64; 3], radius: f64, height: f64) -> Option<Body> {
|
||||||
brep::make::cone(center, radius, height)
|
brep::make::cone(center, radius, height)
|
||||||
|
|||||||
Reference in new issue
Block a user