forked from 1b-agent/opencadstudio-upstream
3088 lines
115 KiB
Rust
3088 lines
115 KiB
Rust
// Interactive kernel primitives stored as ACIS Solid3D entities.
|
|
|
|
use acadrust::entities::Solid3D;
|
|
use acadrust::objects::SolidHistoryOperation;
|
|
use acadrust::EntityType;
|
|
use cadkernel::brep::Body;
|
|
use cadkernel::geom2d::{
|
|
fillets_between, Circle as KernelCircle, Curve as KernelCurve, Line as KernelLine,
|
|
Tolerance,
|
|
};
|
|
use glam::DVec3;
|
|
use std::sync::{Mutex, OnceLock};
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
|
|
use crate::command::{CadCommand, CmdOption, CmdResult, TangentObject, WorkingPlane};
|
|
use crate::scene::model::solid_model;
|
|
use crate::scene::model::wire_model::WireModel;
|
|
use crate::t;
|
|
|
|
static LAST_WEDGE_HEIGHT: AtomicU64 = AtomicU64::new(0);
|
|
|
|
/// Which primitive a `PrimitiveCommand` builds.
|
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
pub enum Shape {
|
|
Box,
|
|
Wedge,
|
|
Cylinder,
|
|
Cone,
|
|
Sphere,
|
|
Pyramid,
|
|
Torus,
|
|
}
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
enum BoxStep {
|
|
FirstCorner,
|
|
CenterPoint,
|
|
OppositeCorner,
|
|
CubeSize,
|
|
Length,
|
|
Width,
|
|
Height,
|
|
HeightFirstPoint,
|
|
HeightSecondPoint,
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
enum SphereStep {
|
|
Center,
|
|
Radius(DVec3),
|
|
Diameter(DVec3),
|
|
TwoPointFirst,
|
|
TwoPointSecond(DVec3),
|
|
ThreePointFirst,
|
|
ThreePointSecond(DVec3),
|
|
ThreePointThird(DVec3, DVec3),
|
|
TtrFirst,
|
|
TtrSecond {
|
|
object: TangentObject,
|
|
hit: DVec3,
|
|
},
|
|
TtrRadius {
|
|
first: TangentObject,
|
|
second: TangentObject,
|
|
first_hit: DVec3,
|
|
second_hit: DVec3,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
enum ConeStep {
|
|
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,
|
|
HeightAfterTopRadius,
|
|
HeightFirstPoint,
|
|
HeightSecondPoint(DVec3),
|
|
AxisEndpoint,
|
|
TopRadius,
|
|
}
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
enum PyramidType {
|
|
Circumscribed,
|
|
Inscribed,
|
|
}
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
enum PyramidStep {
|
|
BaseCenter,
|
|
Sides,
|
|
EdgeFirst,
|
|
EdgeSecond,
|
|
BaseRadius,
|
|
Height,
|
|
HeightAfterTopRadius,
|
|
HeightFirstPoint,
|
|
HeightSecondPoint,
|
|
AxisEndpoint,
|
|
TopRadius,
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
struct PyramidDefaults {
|
|
displayed_base_radius: f64,
|
|
displayed_top_radius: f64,
|
|
height: f64,
|
|
sides: usize,
|
|
kind: PyramidType,
|
|
}
|
|
|
|
impl Default for PyramidDefaults {
|
|
fn default() -> Self {
|
|
Self {
|
|
displayed_base_radius: 1.0,
|
|
displayed_top_radius: 0.0,
|
|
height: 1.0,
|
|
sides: 4,
|
|
kind: PyramidType::Circumscribed,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
struct ConeDefaults {
|
|
base_x_radius: f64,
|
|
base_y_radius: f64,
|
|
top_radius: f64,
|
|
height: f64,
|
|
}
|
|
|
|
impl Default for ConeDefaults {
|
|
fn default() -> Self {
|
|
Self {
|
|
base_x_radius: 1.0,
|
|
base_y_radius: 1.0,
|
|
top_radius: 0.0,
|
|
height: 1.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn sphere_radius_store() -> &'static Mutex<f64> {
|
|
static VALUE: OnceLock<Mutex<f64>> = OnceLock::new();
|
|
VALUE.get_or_init(|| Mutex::new(1.0))
|
|
}
|
|
|
|
fn sphere_radius_default() -> f64 {
|
|
sphere_radius_store()
|
|
.lock()
|
|
.map(|value| *value)
|
|
.unwrap_or(1.0)
|
|
}
|
|
|
|
fn remember_sphere_radius(radius: f64) {
|
|
if radius.is_finite() && radius > 1e-6 {
|
|
if let Ok(mut value) = sphere_radius_store().lock() {
|
|
*value = radius;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn sphere_tangent_local(object: TangentObject, plane: WorkingPlane) -> TangentObject {
|
|
match object {
|
|
TangentObject::Line { p1, p2 } => TangentObject::Line {
|
|
p1: plane.to_local(p1),
|
|
p2: plane.to_local(p2),
|
|
},
|
|
TangentObject::Circle { center, radius } => TangentObject::Circle {
|
|
center: plane.to_local(center),
|
|
radius,
|
|
},
|
|
}
|
|
}
|
|
|
|
fn sphere_tangent_curve(object: TangentObject) -> KernelCurve {
|
|
match object {
|
|
TangentObject::Line { p1, p2 } => KernelCurve::Line(KernelLine {
|
|
start: [p1.x, p1.y],
|
|
end: [p2.x, p2.y],
|
|
}),
|
|
TangentObject::Circle { center, radius } => KernelCurve::Circle(KernelCircle {
|
|
centre: [center.x, center.y],
|
|
radius,
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn sphere_ttr_centers(
|
|
first: TangentObject,
|
|
second: TangentObject,
|
|
radius: f64,
|
|
) -> Vec<DVec3> {
|
|
fillets_between(
|
|
&sphere_tangent_curve(first),
|
|
&sphere_tangent_curve(second),
|
|
radius,
|
|
Tolerance::default(),
|
|
)
|
|
.into_iter()
|
|
.map(|fillet| DVec3::new(fillet.centre[0], fillet.centre[1], 0.0))
|
|
.collect()
|
|
}
|
|
|
|
fn closest_sphere_center(candidates: &[DVec3], hint: DVec3) -> Option<DVec3> {
|
|
candidates.iter().copied().min_by(|first, second| {
|
|
first
|
|
.distance(hint)
|
|
.partial_cmp(&second.distance(hint))
|
|
.unwrap_or(std::cmp::Ordering::Equal)
|
|
})
|
|
}
|
|
|
|
fn sphere_through_three_points(
|
|
first: DVec3,
|
|
second: DVec3,
|
|
third: DVec3,
|
|
) -> Option<(DVec3, f64)> {
|
|
let circle = cadkernel::geom2d::arc_through_points(
|
|
[first.x, first.y],
|
|
[second.x, second.y],
|
|
[third.x, third.y],
|
|
)?;
|
|
Some((
|
|
DVec3::new(circle.centre[0], circle.centre[1], first.z),
|
|
circle.radius,
|
|
))
|
|
}
|
|
|
|
fn cone_defaults() -> &'static std::sync::Mutex<ConeDefaults> {
|
|
static DEFAULTS: std::sync::OnceLock<std::sync::Mutex<ConeDefaults>> =
|
|
std::sync::OnceLock::new();
|
|
DEFAULTS.get_or_init(|| std::sync::Mutex::new(ConeDefaults::default()))
|
|
}
|
|
|
|
fn pyramid_defaults() -> &'static Mutex<PyramidDefaults> {
|
|
static DEFAULTS: OnceLock<Mutex<PyramidDefaults>> = OnceLock::new();
|
|
DEFAULTS.get_or_init(|| Mutex::new(PyramidDefaults::default()))
|
|
}
|
|
|
|
fn pyramid_apothem_factor(sides: usize) -> f64 {
|
|
(std::f64::consts::PI / sides.max(3) as f64).cos()
|
|
}
|
|
|
|
fn pyramid_display_to_circumradius(value: f64, kind: PyramidType, sides: usize) -> f64 {
|
|
match kind {
|
|
PyramidType::Circumscribed => value / pyramid_apothem_factor(sides),
|
|
PyramidType::Inscribed => value,
|
|
}
|
|
}
|
|
|
|
fn pyramid_circumradius_to_display(value: f64, kind: PyramidType, sides: usize) -> f64 {
|
|
match kind {
|
|
PyramidType::Circumscribed => value * pyramid_apothem_factor(sides),
|
|
PyramidType::Inscribed => value,
|
|
}
|
|
}
|
|
|
|
impl Shape {
|
|
fn from_id(id: &str) -> Option<Shape> {
|
|
Some(match id {
|
|
"BOX" => Shape::Box,
|
|
"WEDGE" => Shape::Wedge,
|
|
"CYLINDER" => Shape::Cylinder,
|
|
"CONE" => Shape::Cone,
|
|
"SPHERE" => Shape::Sphere,
|
|
"PYRAMID" | "PYR" => Shape::Pyramid,
|
|
"TORUS" => Shape::Torus,
|
|
_ => return None,
|
|
})
|
|
}
|
|
fn name(self) -> &'static str {
|
|
match self {
|
|
Shape::Box => "BOX",
|
|
Shape::Wedge => "WEDGE",
|
|
Shape::Cylinder => "CYLINDER",
|
|
Shape::Cone => "CONE",
|
|
Shape::Sphere => "SPHERE",
|
|
Shape::Pyramid => "PYRAMID",
|
|
Shape::Torus => "TORUS",
|
|
}
|
|
}
|
|
/// True for footprints picked as a centre + radius (round shapes); false
|
|
/// for corner-to-corner footprints (box/wedge).
|
|
fn radial(self) -> bool {
|
|
!self.rectangular()
|
|
}
|
|
fn rectangular(self) -> bool {
|
|
matches!(self, Shape::Box | Shape::Wedge)
|
|
}
|
|
/// Whether a height value is collected after the footprint.
|
|
fn needs_height(self) -> bool {
|
|
!matches!(self, Shape::Sphere | Shape::Torus)
|
|
}
|
|
}
|
|
|
|
pub struct PrimitiveCommand {
|
|
shape: Shape,
|
|
/// Footprint points collected so far (local/world XY, z = 0).
|
|
pts: Vec<DVec3>,
|
|
/// True once the footprint is set and we are collecting the height.
|
|
height_step: bool,
|
|
box_step: BoxStep,
|
|
box_centered: bool,
|
|
box_origin: Option<DVec3>,
|
|
box_length: Option<f64>,
|
|
box_width: Option<f64>,
|
|
box_angle: f64,
|
|
box_width_sign: f64,
|
|
box_height_anchor: Option<DVec3>,
|
|
sphere_step: SphereStep,
|
|
sphere_default_radius: f64,
|
|
cone_step: ConeStep,
|
|
cone_frame: Option<WorkingPlane>,
|
|
cone_base_x_radius: f64,
|
|
cone_base_y_radius: f64,
|
|
cone_top_radius: f64,
|
|
cone_defaults: ConeDefaults,
|
|
pyramid_step: PyramidStep,
|
|
pyramid_frame: Option<WorkingPlane>,
|
|
pyramid_edge_first: Option<DVec3>,
|
|
pyramid_height_first: Option<DVec3>,
|
|
pyramid_base_radius: f64,
|
|
pyramid_top_radius: f64,
|
|
pyramid_sides: usize,
|
|
pyramid_type: PyramidType,
|
|
pyramid_defaults: PyramidDefaults,
|
|
plane: WorkingPlane,
|
|
}
|
|
|
|
impl PrimitiveCommand {
|
|
pub fn new(id: &str) -> Self {
|
|
let remembered = cone_defaults()
|
|
.lock()
|
|
.map(|value| *value)
|
|
.unwrap_or_default();
|
|
let pyramid_remembered = pyramid_defaults()
|
|
.lock()
|
|
.map(|value| *value)
|
|
.unwrap_or_default();
|
|
Self {
|
|
shape: Shape::from_id(id).unwrap_or(Shape::Box),
|
|
pts: Vec::new(),
|
|
height_step: false,
|
|
box_step: BoxStep::FirstCorner,
|
|
box_centered: false,
|
|
box_origin: None,
|
|
box_length: None,
|
|
box_width: None,
|
|
box_angle: 0.0,
|
|
box_width_sign: 1.0,
|
|
box_height_anchor: None,
|
|
sphere_step: SphereStep::Center,
|
|
sphere_default_radius: sphere_radius_default(),
|
|
cone_step: ConeStep::BaseCenter,
|
|
cone_frame: None,
|
|
cone_base_x_radius: remembered.base_x_radius,
|
|
cone_base_y_radius: remembered.base_y_radius,
|
|
cone_top_radius: remembered.top_radius,
|
|
cone_defaults: remembered,
|
|
pyramid_step: PyramidStep::BaseCenter,
|
|
pyramid_frame: None,
|
|
pyramid_edge_first: None,
|
|
pyramid_height_first: None,
|
|
pyramid_base_radius: pyramid_display_to_circumradius(
|
|
pyramid_remembered.displayed_base_radius,
|
|
pyramid_remembered.kind,
|
|
pyramid_remembered.sides,
|
|
),
|
|
pyramid_top_radius: pyramid_display_to_circumradius(
|
|
pyramid_remembered.displayed_top_radius,
|
|
pyramid_remembered.kind,
|
|
pyramid_remembered.sides,
|
|
),
|
|
pyramid_sides: pyramid_remembered.sides,
|
|
pyramid_type: pyramid_remembered.kind,
|
|
pyramid_defaults: pyramid_remembered,
|
|
plane: WorkingPlane::default(),
|
|
}
|
|
}
|
|
|
|
fn commit_sphere(&mut self, center: DVec3, radius: f64) -> CmdResult {
|
|
if !center.is_finite() || !radius.is_finite() || radius <= 1e-6 {
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
self.pts = vec![center, center + DVec3::X * radius];
|
|
self.sphere_default_radius = radius;
|
|
remember_sphere_radius(radius);
|
|
self.commit(0.0)
|
|
}
|
|
|
|
fn sphere_ttr_result(&mut self, radius: f64) -> CmdResult {
|
|
let SphereStep::TtrRadius {
|
|
first,
|
|
second,
|
|
first_hit,
|
|
second_hit,
|
|
} = self.sphere_step
|
|
else {
|
|
return CmdResult::NeedPoint;
|
|
};
|
|
let hint = (first_hit + second_hit) * 0.5;
|
|
let Some(center) = closest_sphere_center(
|
|
&sphere_ttr_centers(first, second, radius),
|
|
hint,
|
|
) else {
|
|
return CmdResult::NeedPoint;
|
|
};
|
|
self.commit_sphere(center, radius)
|
|
}
|
|
|
|
fn sphere_preview(&self, center: DVec3, radius: f64) -> Option<WireModel> {
|
|
if !center.is_finite() || !radius.is_finite() || radius <= 1e-6 {
|
|
return None;
|
|
}
|
|
Some(self.place_preview(footprint_wire(
|
|
Shape::Sphere,
|
|
&[center, center + DVec3::X * radius],
|
|
)))
|
|
}
|
|
|
|
fn cone_frame_at(&self, center: DVec3) -> WorkingPlane {
|
|
WorkingPlane {
|
|
origin: center,
|
|
x: self.plane.x,
|
|
y: self.plane.y,
|
|
z: self.plane.z,
|
|
}
|
|
}
|
|
|
|
fn set_cone_base(
|
|
&mut self,
|
|
frame: WorkingPlane,
|
|
base_x_radius: f64,
|
|
base_y_radius: f64,
|
|
) -> bool {
|
|
if !frame.origin.is_finite()
|
|
|| !frame.x.is_finite()
|
|
|| !frame.y.is_finite()
|
|
|| !frame.z.is_finite()
|
|
|| !base_x_radius.is_finite()
|
|
|| !base_y_radius.is_finite()
|
|
|| base_x_radius < 1e-6
|
|
|| base_y_radius < 1e-6
|
|
{
|
|
return false;
|
|
}
|
|
self.cone_frame = Some(frame);
|
|
self.cone_base_x_radius = base_x_radius;
|
|
self.cone_base_y_radius = base_y_radius;
|
|
self.cone_step = ConeStep::Height;
|
|
true
|
|
}
|
|
|
|
fn cone_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 cone_axis_frame(&self, axis: DVec3) -> Option<(WorkingPlane, f64)> {
|
|
let current = self.cone_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 cone_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_cone(&self, axis: DVec3) -> Option<(Body, SolidHistoryOperation, f64)> {
|
|
let (frame, height) = self.cone_axis_frame(axis)?;
|
|
let local = solid_model::cone_frustum_solid(
|
|
[0.0; 3],
|
|
self.cone_base_x_radius,
|
|
self.cone_base_y_radius,
|
|
self.cone_top_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::cone_op(
|
|
Self::cone_history_transform(frame),
|
|
self.cone_base_x_radius,
|
|
self.cone_base_y_radius,
|
|
self.cone_top_radius,
|
|
height,
|
|
),
|
|
height,
|
|
))
|
|
}
|
|
|
|
fn commit_cone_axis(&self, axis: DVec3) -> CmdResult {
|
|
let Some((solid, history, height)) = self.build_cone(axis) else {
|
|
return CmdResult::NeedPoint;
|
|
};
|
|
if let Ok(mut defaults) = cone_defaults().lock() {
|
|
*defaults = ConeDefaults {
|
|
base_x_radius: self.cone_base_x_radius,
|
|
base_y_radius: self.cone_base_y_radius,
|
|
top_radius: self.cone_top_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_cone_height(&self, height: f64) -> CmdResult {
|
|
let Some(frame) = self.cone_frame else {
|
|
return CmdResult::NeedPoint;
|
|
};
|
|
if !height.is_finite() || height.abs() < 1e-6 {
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
self.commit_cone_axis(frame.z * height)
|
|
}
|
|
|
|
fn cone_height_at(&self, point: DVec3) -> Option<f64> {
|
|
let frame = self.cone_frame?;
|
|
Some((point - frame.origin).dot(frame.z))
|
|
}
|
|
|
|
fn cone_ttr_center(&self, radius: f64) -> Option<DVec3> {
|
|
let ConeStep::TtrRadius {
|
|
first,
|
|
second,
|
|
first_hit,
|
|
second_hit,
|
|
} = self.cone_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 cone_preview(&self, axis: DVec3) -> Option<WireModel> {
|
|
let (frame, height) = self.cone_axis_frame(axis)?;
|
|
let top = frame.origin + frame.z * height;
|
|
let top_y = if self.cone_top_radius > 0.0 {
|
|
self.cone_top_radius * self.cone_base_y_radius / self.cone_base_x_radius
|
|
} else {
|
|
0.0
|
|
};
|
|
let mut points = Vec::new();
|
|
push_ellipse_world(
|
|
&mut points,
|
|
frame.origin,
|
|
frame.x,
|
|
frame.y,
|
|
self.cone_base_x_radius,
|
|
self.cone_base_y_radius,
|
|
);
|
|
if self.cone_top_radius > 1e-9 {
|
|
push_ellipse_world(
|
|
&mut points,
|
|
top,
|
|
frame.x,
|
|
frame.y,
|
|
self.cone_top_radius,
|
|
top_y,
|
|
);
|
|
}
|
|
for index in 0..4 {
|
|
let angle = index as f64 * std::f64::consts::FRAC_PI_2;
|
|
let base = frame.origin
|
|
+ frame.x * (self.cone_base_x_radius * angle.cos())
|
|
+ frame.y * (self.cone_base_y_radius * angle.sin());
|
|
let upper = if self.cone_top_radius > 1e-9 {
|
|
top + frame.x * (self.cone_top_radius * angle.cos())
|
|
+ frame.y * (top_y * angle.sin())
|
|
} else {
|
|
top
|
|
};
|
|
push_segment(&mut points, base, upper);
|
|
}
|
|
Some(wire("primitive_height_preview", points))
|
|
}
|
|
|
|
fn cone_base_preview(&self, frame: WorkingPlane, x_radius: f64, y_radius: f64) -> WireModel {
|
|
let mut points = Vec::new();
|
|
push_ellipse_world(
|
|
&mut points,
|
|
frame.origin,
|
|
frame.x,
|
|
frame.y,
|
|
x_radius.max(1e-9),
|
|
y_radius.max(1e-9),
|
|
);
|
|
wire("primitive_preview", points)
|
|
}
|
|
|
|
fn cone_prompt(&self) -> String {
|
|
match self.cone_step {
|
|
ConeStep::BaseCenter =>
|
|
t!("CONE Specify center point of base or [3P/2P/Ttr/Elliptical]:").into_owned(),
|
|
ConeStep::BaseRadius => crate::tf!(
|
|
"CONE Specify base radius or [Diameter] <{:.4}>:",
|
|
self.cone_defaults.base_x_radius
|
|
).into_owned(),
|
|
ConeStep::BaseDiameter => crate::tf!(
|
|
"CONE Specify base diameter <{:.4}>:",
|
|
self.cone_defaults.base_x_radius * 2.0
|
|
).into_owned(),
|
|
ConeStep::ThreePointFirst => t!("CONE Specify first point on base:").into_owned(),
|
|
ConeStep::ThreePointSecond(_) => t!("CONE Specify second point on base:").into_owned(),
|
|
ConeStep::ThreePointThird(_, _) => t!("CONE Specify third point on base:").into_owned(),
|
|
ConeStep::TwoPointFirst => t!("CONE Specify first endpoint of base diameter:").into_owned(),
|
|
ConeStep::TwoPointSecond(_) => t!("CONE Specify second endpoint of base diameter:").into_owned(),
|
|
ConeStep::EllipseFirst =>
|
|
t!("CONE Specify endpoint of first axis or [Center]:").into_owned(),
|
|
ConeStep::EllipseCenter => t!("CONE Specify center point:").into_owned(),
|
|
ConeStep::EllipseCenterFirstAxis(_) => crate::tf!(
|
|
"CONE Specify distance to first axis <{:.4}>:",
|
|
self.cone_defaults.base_x_radius
|
|
)
|
|
.into_owned(),
|
|
ConeStep::EllipseCenterSecondAxis(_, _) =>
|
|
t!("CONE Specify endpoint of second axis:").into_owned(),
|
|
ConeStep::EllipseSecond(_) => t!("CONE Specify second endpoint of ellipse axis:").into_owned(),
|
|
ConeStep::EllipseThird(_, _) => t!("CONE Specify distance to other ellipse axis:").into_owned(),
|
|
ConeStep::TtrFirst => t!("CONE Select first tangent object:").into_owned(),
|
|
ConeStep::TtrSecond { .. } => t!("CONE Select second tangent object:").into_owned(),
|
|
ConeStep::TtrRadius { .. } => crate::tf!(
|
|
"CONE Specify base radius <{:.4}>:",
|
|
self.cone_defaults.base_x_radius
|
|
).into_owned(),
|
|
ConeStep::Height => crate::tf!(
|
|
"CONE Specify height or [2Point/Axis endpoint/Top radius] <{:.4}>:",
|
|
self.cone_defaults.height
|
|
).into_owned(),
|
|
ConeStep::HeightAfterTopRadius => crate::tf!(
|
|
"CONE Specify height or [2Point/Axis endpoint] <{:.4}>:",
|
|
self.cone_defaults.height
|
|
)
|
|
.into_owned(),
|
|
ConeStep::HeightFirstPoint => t!("CONE Specify first point for height:").into_owned(),
|
|
ConeStep::HeightSecondPoint(_) => t!("CONE Specify second point for height:").into_owned(),
|
|
ConeStep::AxisEndpoint => t!("CONE Specify axis endpoint:").into_owned(),
|
|
ConeStep::TopRadius => crate::tf!(
|
|
"CONE Specify top radius <{:.4}>:",
|
|
self.cone_defaults.top_radius
|
|
).into_owned(),
|
|
}
|
|
}
|
|
|
|
fn cone_options(&self) -> Vec<CmdOption> {
|
|
match self.cone_step {
|
|
ConeStep::BaseCenter => vec![
|
|
CmdOption::new("3P", "3P"),
|
|
CmdOption::new("2P", "2P"),
|
|
CmdOption::new("Ttr", "TTR"),
|
|
CmdOption::new(t!("Elliptical").as_ref(), "E"),
|
|
],
|
|
ConeStep::BaseRadius => vec![CmdOption::new(t!("Diameter").as_ref(), "D")],
|
|
ConeStep::EllipseFirst => vec![CmdOption::new(t!("Center").as_ref(), "C")],
|
|
ConeStep::Height => vec![
|
|
CmdOption::new("2Point", "2P"),
|
|
CmdOption::new(t!("Axis endpoint").as_ref(), "A"),
|
|
CmdOption::new(t!("Top radius").as_ref(), "T"),
|
|
],
|
|
ConeStep::HeightAfterTopRadius => vec![
|
|
CmdOption::new("2Point", "2P"),
|
|
CmdOption::new(t!("Axis endpoint").as_ref(), "A"),
|
|
],
|
|
_ => Vec::new(),
|
|
}
|
|
}
|
|
|
|
fn on_cone_point(&mut self, point: DVec3) -> CmdResult {
|
|
match self.cone_step {
|
|
ConeStep::BaseCenter => {
|
|
self.cone_frame = Some(self.cone_frame_at(point));
|
|
self.cone_step = ConeStep::BaseRadius;
|
|
CmdResult::NeedPoint
|
|
}
|
|
ConeStep::BaseRadius => {
|
|
let Some(frame) = self.cone_frame else {
|
|
return CmdResult::NeedPoint;
|
|
};
|
|
let local = frame.to_local(point);
|
|
let radius = local.x.hypot(local.y);
|
|
self.set_cone_base(frame, radius, radius);
|
|
CmdResult::NeedPoint
|
|
}
|
|
ConeStep::BaseDiameter => {
|
|
let Some(frame) = self.cone_frame else {
|
|
return CmdResult::NeedPoint;
|
|
};
|
|
let local = frame.to_local(point);
|
|
let radius = local.x.hypot(local.y) * 0.5;
|
|
self.set_cone_base(frame, radius, radius);
|
|
CmdResult::NeedPoint
|
|
}
|
|
ConeStep::ThreePointFirst => {
|
|
self.cone_step = ConeStep::ThreePointSecond(point);
|
|
CmdResult::NeedPoint
|
|
}
|
|
ConeStep::ThreePointSecond(first) => {
|
|
if point.distance_squared(first) > 1e-12 {
|
|
self.cone_step = ConeStep::ThreePointThird(first, point);
|
|
}
|
|
CmdResult::NeedPoint
|
|
}
|
|
ConeStep::ThreePointThird(first, second) => {
|
|
if let Some((frame, radius)) = Self::cone_three_point_frame(first, second, point) {
|
|
self.set_cone_base(frame, radius, radius);
|
|
}
|
|
CmdResult::NeedPoint
|
|
}
|
|
ConeStep::TwoPointFirst => {
|
|
self.cone_step = ConeStep::TwoPointSecond(point);
|
|
CmdResult::NeedPoint
|
|
}
|
|
ConeStep::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_cone_base(self.cone_frame_at(center), radius, radius);
|
|
CmdResult::NeedPoint
|
|
}
|
|
ConeStep::EllipseFirst => {
|
|
self.cone_step = ConeStep::EllipseSecond(point);
|
|
CmdResult::NeedPoint
|
|
}
|
|
ConeStep::EllipseCenter => {
|
|
self.cone_step = ConeStep::EllipseCenterFirstAxis(point);
|
|
CmdResult::NeedPoint
|
|
}
|
|
ConeStep::EllipseCenterFirstAxis(center) => {
|
|
if point.distance_squared(center) > 1e-12 {
|
|
self.cone_step = ConeStep::EllipseCenterSecondAxis(center, point);
|
|
}
|
|
CmdResult::NeedPoint
|
|
}
|
|
ConeStep::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_cone_base(frame, axis.length(), local.y.abs());
|
|
}
|
|
CmdResult::NeedPoint
|
|
}
|
|
ConeStep::EllipseSecond(first) => {
|
|
if point.distance_squared(first) > 1e-12 {
|
|
self.cone_step = ConeStep::EllipseThird(first, point);
|
|
}
|
|
CmdResult::NeedPoint
|
|
}
|
|
ConeStep::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_cone_base(frame, x_radius, local.y.abs());
|
|
}
|
|
CmdResult::NeedPoint
|
|
}
|
|
ConeStep::TtrRadius { second_hit, .. } => {
|
|
let local = self.plane.to_local(point);
|
|
let radius = (local - second_hit).truncate().length();
|
|
if let Some(center) = self.cone_ttr_center(radius) {
|
|
self.set_cone_base(self.cone_frame_at(center), radius, radius);
|
|
}
|
|
CmdResult::NeedPoint
|
|
}
|
|
ConeStep::Height | ConeStep::HeightAfterTopRadius => self
|
|
.cone_height_at(point)
|
|
.map(|height| self.commit_cone_height(height))
|
|
.unwrap_or(CmdResult::NeedPoint),
|
|
ConeStep::HeightFirstPoint => {
|
|
self.cone_step = ConeStep::HeightSecondPoint(point);
|
|
CmdResult::NeedPoint
|
|
}
|
|
ConeStep::HeightSecondPoint(first) => self.commit_cone_height(first.distance(point)),
|
|
ConeStep::AxisEndpoint => {
|
|
let Some(frame) = self.cone_frame else {
|
|
return CmdResult::NeedPoint;
|
|
};
|
|
self.commit_cone_axis(point - frame.origin)
|
|
}
|
|
ConeStep::TopRadius => {
|
|
let Some(frame) = self.cone_frame else {
|
|
return CmdResult::NeedPoint;
|
|
};
|
|
let local = frame.to_local(point);
|
|
let radius = local.x.hypot(local.y);
|
|
if radius.is_finite() {
|
|
self.cone_top_radius = radius;
|
|
self.cone_step = ConeStep::HeightAfterTopRadius;
|
|
}
|
|
CmdResult::NeedPoint
|
|
}
|
|
ConeStep::TtrFirst | ConeStep::TtrSecond { .. } => CmdResult::NeedPoint,
|
|
}
|
|
}
|
|
|
|
fn on_cone_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.cone_step {
|
|
ConeStep::TtrFirst => {
|
|
self.cone_step = ConeStep::TtrSecond { object, hit };
|
|
}
|
|
ConeStep::TtrSecond {
|
|
object: first,
|
|
hit: first_hit,
|
|
} => {
|
|
self.cone_step = ConeStep::TtrRadius {
|
|
first,
|
|
second: object,
|
|
first_hit,
|
|
second_hit: hit,
|
|
};
|
|
}
|
|
_ => {}
|
|
}
|
|
CmdResult::NeedPoint
|
|
}
|
|
|
|
fn on_cone_text(&mut self, raw: &str) -> Option<CmdResult> {
|
|
let token = raw.trim();
|
|
let upper = token.to_uppercase();
|
|
match self.cone_step {
|
|
ConeStep::BaseCenter => {
|
|
self.cone_step = match upper.as_str() {
|
|
"3P" => ConeStep::ThreePointFirst,
|
|
"2P" => ConeStep::TwoPointFirst,
|
|
"T" | "TTR" => ConeStep::TtrFirst,
|
|
"E" | "ELLIPTICAL" => ConeStep::EllipseFirst,
|
|
_ => return None,
|
|
};
|
|
return Some(CmdResult::NeedPoint);
|
|
}
|
|
ConeStep::BaseRadius if matches!(upper.as_str(), "D" | "DIAMETER") => {
|
|
self.cone_step = ConeStep::BaseDiameter;
|
|
return Some(CmdResult::NeedPoint);
|
|
}
|
|
ConeStep::EllipseFirst if matches!(upper.as_str(), "C" | "CENTER") => {
|
|
self.cone_step = ConeStep::EllipseCenter;
|
|
return Some(CmdResult::NeedPoint);
|
|
}
|
|
ConeStep::Height => {
|
|
self.cone_step = match upper.as_str() {
|
|
"2P" | "2POINT" => ConeStep::HeightFirstPoint,
|
|
"A" | "AXIS" | "AXIS ENDPOINT" => ConeStep::AxisEndpoint,
|
|
"T" | "TOP" | "TOP RADIUS" => ConeStep::TopRadius,
|
|
_ => {
|
|
let height = crate::entities::common::parse_typed_length(token)?;
|
|
return Some(self.commit_cone_height(height));
|
|
}
|
|
};
|
|
return Some(CmdResult::NeedPoint);
|
|
}
|
|
ConeStep::HeightAfterTopRadius => {
|
|
self.cone_step = match upper.as_str() {
|
|
"2P" | "2POINT" => ConeStep::HeightFirstPoint,
|
|
"A" | "AXIS" | "AXIS ENDPOINT" => ConeStep::AxisEndpoint,
|
|
_ => {
|
|
let height = crate::entities::common::parse_typed_length(token)?;
|
|
return Some(self.commit_cone_height(height));
|
|
}
|
|
};
|
|
return Some(CmdResult::NeedPoint);
|
|
}
|
|
_ => {}
|
|
}
|
|
let number = crate::entities::common::parse_typed_length(token)?;
|
|
match self.cone_step {
|
|
ConeStep::BaseRadius => {
|
|
let frame = self.cone_frame?;
|
|
(number > 0.0).then(|| {
|
|
self.set_cone_base(frame, number, number);
|
|
CmdResult::NeedPoint
|
|
})
|
|
}
|
|
ConeStep::BaseDiameter => {
|
|
let frame = self.cone_frame?;
|
|
(number > 0.0).then(|| {
|
|
self.set_cone_base(frame, number * 0.5, number * 0.5);
|
|
CmdResult::NeedPoint
|
|
})
|
|
}
|
|
ConeStep::EllipseCenterFirstAxis(center) => (number > 0.0).then(|| {
|
|
self.cone_step = ConeStep::EllipseCenterSecondAxis(
|
|
center,
|
|
center + self.plane.x * number,
|
|
);
|
|
CmdResult::NeedPoint
|
|
}),
|
|
ConeStep::TtrRadius { .. } => {
|
|
let center = self.cone_ttr_center(number)?;
|
|
(number > 0.0).then(|| {
|
|
self.set_cone_base(self.cone_frame_at(center), number, number);
|
|
CmdResult::NeedPoint
|
|
})
|
|
}
|
|
ConeStep::TopRadius if number >= 0.0 => {
|
|
self.cone_top_radius = number;
|
|
self.cone_step = ConeStep::HeightAfterTopRadius;
|
|
Some(CmdResult::NeedPoint)
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn cone_mouse_move(&self, point: DVec3) -> Option<WireModel> {
|
|
match self.cone_step {
|
|
ConeStep::BaseRadius => {
|
|
let frame = self.cone_frame?;
|
|
let local = frame.to_local(point);
|
|
let radius = local.x.hypot(local.y);
|
|
Some(self.cone_base_preview(frame, radius, radius))
|
|
}
|
|
ConeStep::BaseDiameter => {
|
|
let frame = self.cone_frame?;
|
|
let local = frame.to_local(point);
|
|
let radius = local.x.hypot(local.y) * 0.5;
|
|
Some(self.cone_base_preview(frame, radius, radius))
|
|
}
|
|
ConeStep::ThreePointSecond(first) | ConeStep::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.cone_base_preview(self.cone_frame_at(center), radius, radius))
|
|
}
|
|
ConeStep::ThreePointThird(first, second) => {
|
|
let (frame, radius) = Self::cone_three_point_frame(first, second, point)?;
|
|
Some(self.cone_base_preview(frame, radius, radius))
|
|
}
|
|
ConeStep::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.cone_base_preview(frame, axis.length() * 0.5, local.y.abs()))
|
|
}
|
|
ConeStep::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.cone_base_preview(frame, axis.length(), local.y.abs()))
|
|
}
|
|
ConeStep::TtrRadius { second_hit, .. } => {
|
|
let local = self.plane.to_local(point);
|
|
let radius = (local - second_hit).truncate().length();
|
|
let center = self.cone_ttr_center(radius)?;
|
|
Some(self.cone_base_preview(self.cone_frame_at(center), radius, radius))
|
|
}
|
|
ConeStep::Height | ConeStep::HeightAfterTopRadius => {
|
|
let frame = self.cone_frame?;
|
|
self.cone_preview(frame.z * self.cone_height_at(point)?)
|
|
}
|
|
ConeStep::HeightSecondPoint(first) => {
|
|
let frame = self.cone_frame?;
|
|
self.cone_preview(frame.z * first.distance(point))
|
|
}
|
|
ConeStep::AxisEndpoint => {
|
|
let frame = self.cone_frame?;
|
|
self.cone_preview(point - frame.origin)
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn pyramid_frame_at(&self, center: DVec3) -> WorkingPlane {
|
|
WorkingPlane {
|
|
origin: center,
|
|
x: self.plane.x,
|
|
y: self.plane.y,
|
|
z: self.plane.z,
|
|
}
|
|
}
|
|
|
|
fn set_pyramid_type(&mut self, kind: PyramidType) {
|
|
let displayed_top = pyramid_circumradius_to_display(
|
|
self.pyramid_top_radius,
|
|
self.pyramid_type,
|
|
self.pyramid_sides,
|
|
);
|
|
self.pyramid_type = kind;
|
|
self.pyramid_top_radius =
|
|
pyramid_display_to_circumradius(displayed_top, kind, self.pyramid_sides);
|
|
}
|
|
|
|
fn pyramid_set_base_from_point(&mut self, point: DVec3) -> bool {
|
|
let Some(frame) = self.pyramid_frame else {
|
|
return false;
|
|
};
|
|
let delta = point - frame.origin;
|
|
let planar = delta - frame.z * delta.dot(frame.z);
|
|
let displayed_radius = planar.length();
|
|
let Some(direction) = planar.try_normalize() else {
|
|
return false;
|
|
};
|
|
if !displayed_radius.is_finite() || displayed_radius < 1e-6 {
|
|
return false;
|
|
}
|
|
let x = match self.pyramid_type {
|
|
PyramidType::Inscribed => direction,
|
|
PyramidType::Circumscribed => {
|
|
let half = std::f64::consts::PI / self.pyramid_sides as f64;
|
|
direction * half.cos() - frame.z.cross(direction) * half.sin()
|
|
}
|
|
};
|
|
let Some(x) = x.try_normalize() else {
|
|
return false;
|
|
};
|
|
let Some(y) = frame.z.cross(x).try_normalize() else {
|
|
return false;
|
|
};
|
|
self.pyramid_frame = Some(WorkingPlane::new(frame.origin, x, y));
|
|
self.pyramid_base_radius = pyramid_display_to_circumradius(
|
|
displayed_radius,
|
|
self.pyramid_type,
|
|
self.pyramid_sides,
|
|
);
|
|
self.pyramid_step = PyramidStep::Height;
|
|
true
|
|
}
|
|
|
|
fn pyramid_set_base_from_edge(&mut self, first: DVec3, second: DVec3) -> bool {
|
|
let delta = second - first;
|
|
let planar = delta - self.plane.z * delta.dot(self.plane.z);
|
|
let length = planar.length();
|
|
let Some(edge) = planar.try_normalize() else {
|
|
return false;
|
|
};
|
|
if !length.is_finite() || length < 1e-6 {
|
|
return false;
|
|
}
|
|
let radius = length
|
|
/ (2.0 * (std::f64::consts::PI / self.pyramid_sides as f64).sin());
|
|
let apothem = radius * pyramid_apothem_factor(self.pyramid_sides);
|
|
let center = (first + second) * 0.5 + self.plane.z.cross(edge) * apothem;
|
|
let Some(x) = (first - center).try_normalize() else {
|
|
return false;
|
|
};
|
|
let Some(y) = self.plane.z.cross(x).try_normalize() else {
|
|
return false;
|
|
};
|
|
self.pyramid_frame = Some(WorkingPlane::new(center, x, y));
|
|
self.pyramid_base_radius = radius;
|
|
self.pyramid_step = PyramidStep::Height;
|
|
true
|
|
}
|
|
|
|
fn pyramid_axis_frame(&self, axis: DVec3) -> Option<(WorkingPlane, f64)> {
|
|
let current = self.pyramid_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 pyramid_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_pyramid(&self, axis: DVec3) -> Option<(Body, SolidHistoryOperation, f64)> {
|
|
let (frame, height) = self.pyramid_axis_frame(axis)?;
|
|
let local = solid_model::pyramid_frustum_solid(
|
|
[0.0; 3],
|
|
self.pyramid_base_radius,
|
|
self.pyramid_top_radius,
|
|
height,
|
|
self.pyramid_sides,
|
|
)?;
|
|
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::pyramid_op(
|
|
Self::pyramid_history_transform(frame),
|
|
self.pyramid_base_radius,
|
|
self.pyramid_top_radius,
|
|
height,
|
|
self.pyramid_sides,
|
|
self.pyramid_type == PyramidType::Inscribed,
|
|
),
|
|
height,
|
|
))
|
|
}
|
|
|
|
fn commit_pyramid_axis(&self, axis: DVec3) -> CmdResult {
|
|
let Some((solid, history, height)) = self.build_pyramid(axis) else {
|
|
return CmdResult::NeedPoint;
|
|
};
|
|
if let Ok(mut defaults) = pyramid_defaults().lock() {
|
|
*defaults = PyramidDefaults {
|
|
displayed_base_radius: pyramid_circumradius_to_display(
|
|
self.pyramid_base_radius,
|
|
self.pyramid_type,
|
|
self.pyramid_sides,
|
|
),
|
|
displayed_top_radius: pyramid_circumradius_to_display(
|
|
self.pyramid_top_radius,
|
|
self.pyramid_type,
|
|
self.pyramid_sides,
|
|
),
|
|
height,
|
|
sides: self.pyramid_sides,
|
|
kind: self.pyramid_type,
|
|
};
|
|
}
|
|
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_pyramid_height(&self, height: f64) -> CmdResult {
|
|
let Some(frame) = self.pyramid_frame else {
|
|
return CmdResult::NeedPoint;
|
|
};
|
|
if !height.is_finite() || height.abs() < 1e-6 {
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
self.commit_pyramid_axis(frame.z * height)
|
|
}
|
|
|
|
fn pyramid_height_at(&self, point: DVec3) -> Option<f64> {
|
|
let frame = self.pyramid_frame?;
|
|
Some((point - frame.origin).dot(frame.z))
|
|
}
|
|
|
|
fn pyramid_polygon(frame: WorkingPlane, radius: f64, sides: usize, z: f64) -> Vec<DVec3> {
|
|
(0..sides)
|
|
.map(|index| {
|
|
let angle = std::f64::consts::TAU * index as f64 / sides as f64;
|
|
frame.origin
|
|
+ frame.x * (radius * angle.cos())
|
|
+ frame.y * (radius * angle.sin())
|
|
+ frame.z * z
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn pyramid_preview(&self, axis: DVec3) -> Option<WireModel> {
|
|
let (frame, height) = self.pyramid_axis_frame(axis)?;
|
|
let base = Self::pyramid_polygon(frame, self.pyramid_base_radius, self.pyramid_sides, 0.0);
|
|
let top = Self::pyramid_polygon(frame, self.pyramid_top_radius, self.pyramid_sides, height);
|
|
let apex = frame.origin + frame.z * height;
|
|
let mut points = Vec::new();
|
|
push_path_loop(&mut points, &base);
|
|
if self.pyramid_top_radius > 1e-9 {
|
|
push_path_loop(&mut points, &top);
|
|
}
|
|
for index in 0..self.pyramid_sides {
|
|
push_segment(
|
|
&mut points,
|
|
base[index],
|
|
if self.pyramid_top_radius > 1e-9 { top[index] } else { apex },
|
|
);
|
|
}
|
|
Some(wire("primitive_height_preview", points))
|
|
}
|
|
|
|
fn pyramid_base_preview(&self, frame: WorkingPlane, displayed_radius: f64) -> Option<WireModel> {
|
|
if !displayed_radius.is_finite() || displayed_radius < 1e-9 {
|
|
return None;
|
|
}
|
|
let radius = pyramid_display_to_circumradius(
|
|
displayed_radius,
|
|
self.pyramid_type,
|
|
self.pyramid_sides,
|
|
);
|
|
let points = Self::pyramid_polygon(frame, radius, self.pyramid_sides, 0.0);
|
|
let mut wire_points = Vec::new();
|
|
push_path_loop(&mut wire_points, &points);
|
|
Some(wire("primitive_preview", wire_points))
|
|
}
|
|
|
|
fn pyramid_prompt(&self) -> String {
|
|
let base_default = self.pyramid_defaults.displayed_base_radius;
|
|
let top_default = self.pyramid_defaults.displayed_top_radius;
|
|
match self.pyramid_step {
|
|
PyramidStep::BaseCenter => crate::tf!(
|
|
"PYRAMID Specify center point of base or [Edge/Sides]:"
|
|
).into_owned(),
|
|
PyramidStep::Sides => crate::tf!(
|
|
"PYRAMID Enter number of sides <{}>:",
|
|
self.pyramid_sides
|
|
).into_owned(),
|
|
PyramidStep::EdgeFirst => t!("PYRAMID Specify first endpoint of edge:").into_owned(),
|
|
PyramidStep::EdgeSecond => t!("PYRAMID Specify second endpoint of edge:").into_owned(),
|
|
PyramidStep::BaseRadius => match self.pyramid_type {
|
|
PyramidType::Circumscribed => crate::tf!(
|
|
"PYRAMID Specify base radius or [Inscribed] <{:.4}>:",
|
|
base_default
|
|
).into_owned(),
|
|
PyramidType::Inscribed => crate::tf!(
|
|
"PYRAMID Specify base radius or [Circumscribed] <{:.4}>:",
|
|
base_default
|
|
).into_owned(),
|
|
},
|
|
PyramidStep::Height => crate::tf!(
|
|
"PYRAMID Specify height or [2Point/Axis endpoint/Top radius] <{:.4}>:",
|
|
self.pyramid_defaults.height
|
|
).into_owned(),
|
|
PyramidStep::HeightAfterTopRadius => crate::tf!(
|
|
"PYRAMID Specify height or [2Point/Axis endpoint] <{:.4}>:",
|
|
self.pyramid_defaults.height
|
|
).into_owned(),
|
|
PyramidStep::HeightFirstPoint => t!("PYRAMID Specify first point for height:").into_owned(),
|
|
PyramidStep::HeightSecondPoint => t!("PYRAMID Specify second point for height:").into_owned(),
|
|
PyramidStep::AxisEndpoint => t!("PYRAMID Specify axis endpoint:").into_owned(),
|
|
PyramidStep::TopRadius => crate::tf!(
|
|
"PYRAMID Specify top radius <{:.4}>:",
|
|
top_default
|
|
).into_owned(),
|
|
}
|
|
}
|
|
|
|
fn pyramid_options(&self) -> Vec<CmdOption> {
|
|
match self.pyramid_step {
|
|
PyramidStep::BaseCenter => vec![
|
|
CmdOption::new(t!("Edge").as_ref(), "E"),
|
|
CmdOption::new(t!("Sides").as_ref(), "S"),
|
|
],
|
|
PyramidStep::BaseRadius => vec![match self.pyramid_type {
|
|
PyramidType::Circumscribed => CmdOption::new(t!("Inscribed").as_ref(), "I"),
|
|
PyramidType::Inscribed => CmdOption::new(t!("Circumscribed").as_ref(), "C"),
|
|
}],
|
|
PyramidStep::Height => vec![
|
|
CmdOption::new("2Point", "2P"),
|
|
CmdOption::new(t!("Axis endpoint").as_ref(), "A"),
|
|
CmdOption::new(t!("Top radius").as_ref(), "T"),
|
|
],
|
|
PyramidStep::HeightAfterTopRadius => vec![
|
|
CmdOption::new("2Point", "2P"),
|
|
CmdOption::new(t!("Axis endpoint").as_ref(), "A"),
|
|
],
|
|
_ => Vec::new(),
|
|
}
|
|
}
|
|
|
|
fn on_pyramid_point(&mut self, point: DVec3) -> CmdResult {
|
|
match self.pyramid_step {
|
|
PyramidStep::BaseCenter => {
|
|
self.pyramid_frame = Some(self.pyramid_frame_at(point));
|
|
self.pyramid_step = PyramidStep::BaseRadius;
|
|
CmdResult::NeedPoint
|
|
}
|
|
PyramidStep::EdgeFirst => {
|
|
self.pyramid_edge_first = Some(point);
|
|
self.pyramid_step = PyramidStep::EdgeSecond;
|
|
CmdResult::NeedPoint
|
|
}
|
|
PyramidStep::EdgeSecond => {
|
|
if let Some(first) = self.pyramid_edge_first {
|
|
self.pyramid_set_base_from_edge(first, point);
|
|
}
|
|
CmdResult::NeedPoint
|
|
}
|
|
PyramidStep::BaseRadius => {
|
|
self.pyramid_set_base_from_point(point);
|
|
CmdResult::NeedPoint
|
|
}
|
|
PyramidStep::Height | PyramidStep::HeightAfterTopRadius => self
|
|
.pyramid_height_at(point)
|
|
.map(|height| self.commit_pyramid_height(height))
|
|
.unwrap_or(CmdResult::NeedPoint),
|
|
PyramidStep::HeightFirstPoint => {
|
|
self.pyramid_height_first = Some(point);
|
|
self.pyramid_step = PyramidStep::HeightSecondPoint;
|
|
CmdResult::NeedPoint
|
|
}
|
|
PyramidStep::HeightSecondPoint => self
|
|
.pyramid_height_first
|
|
.map(|first| self.commit_pyramid_height(first.distance(point)))
|
|
.unwrap_or(CmdResult::NeedPoint),
|
|
PyramidStep::AxisEndpoint => {
|
|
let Some(frame) = self.pyramid_frame else {
|
|
return CmdResult::NeedPoint;
|
|
};
|
|
self.commit_pyramid_axis(point - frame.origin)
|
|
}
|
|
PyramidStep::TopRadius => {
|
|
let Some(frame) = self.pyramid_frame else {
|
|
return CmdResult::NeedPoint;
|
|
};
|
|
let delta = point - frame.origin;
|
|
let displayed = (delta - frame.z * delta.dot(frame.z)).length();
|
|
if displayed.is_finite() {
|
|
self.pyramid_top_radius = pyramid_display_to_circumradius(
|
|
displayed,
|
|
self.pyramid_type,
|
|
self.pyramid_sides,
|
|
);
|
|
self.pyramid_step = PyramidStep::HeightAfterTopRadius;
|
|
}
|
|
CmdResult::NeedPoint
|
|
}
|
|
PyramidStep::Sides => CmdResult::NeedPoint,
|
|
}
|
|
}
|
|
|
|
fn on_pyramid_text(&mut self, raw: &str) -> Option<CmdResult> {
|
|
let token = raw.trim();
|
|
let upper = token.to_uppercase();
|
|
match self.pyramid_step {
|
|
PyramidStep::BaseCenter => {
|
|
self.pyramid_step = match upper.as_str() {
|
|
"E" | "EDGE" => PyramidStep::EdgeFirst,
|
|
"S" | "SIDES" => PyramidStep::Sides,
|
|
_ => return None,
|
|
};
|
|
return Some(CmdResult::NeedPoint);
|
|
}
|
|
PyramidStep::BaseRadius if matches!(upper.as_str(), "I" | "INSCRIBED") => {
|
|
self.set_pyramid_type(PyramidType::Inscribed);
|
|
return Some(CmdResult::NeedPoint);
|
|
}
|
|
PyramidStep::BaseRadius if matches!(upper.as_str(), "C" | "CIRCUMSCRIBED") => {
|
|
self.set_pyramid_type(PyramidType::Circumscribed);
|
|
return Some(CmdResult::NeedPoint);
|
|
}
|
|
PyramidStep::Height => {
|
|
self.pyramid_step = match upper.as_str() {
|
|
"2P" | "2POINT" => PyramidStep::HeightFirstPoint,
|
|
"A" | "AXIS" | "AXIS ENDPOINT" => PyramidStep::AxisEndpoint,
|
|
"T" | "TOP" | "TOP RADIUS" => PyramidStep::TopRadius,
|
|
_ => {
|
|
let height = crate::entities::common::parse_typed_length(token)?;
|
|
return Some(self.commit_pyramid_height(height));
|
|
}
|
|
};
|
|
return Some(CmdResult::NeedPoint);
|
|
}
|
|
PyramidStep::HeightAfterTopRadius => {
|
|
self.pyramid_step = match upper.as_str() {
|
|
"2P" | "2POINT" => PyramidStep::HeightFirstPoint,
|
|
"A" | "AXIS" | "AXIS ENDPOINT" => PyramidStep::AxisEndpoint,
|
|
_ => {
|
|
let height = crate::entities::common::parse_typed_length(token)?;
|
|
return Some(self.commit_pyramid_height(height));
|
|
}
|
|
};
|
|
return Some(CmdResult::NeedPoint);
|
|
}
|
|
PyramidStep::Sides => {
|
|
let sides = token.parse::<usize>().ok()?;
|
|
if !(3..=32).contains(&sides) {
|
|
return None;
|
|
}
|
|
let displayed_top = pyramid_circumradius_to_display(
|
|
self.pyramid_top_radius,
|
|
self.pyramid_type,
|
|
self.pyramid_sides,
|
|
);
|
|
self.pyramid_sides = sides;
|
|
self.pyramid_top_radius = pyramid_display_to_circumradius(
|
|
displayed_top,
|
|
self.pyramid_type,
|
|
self.pyramid_sides,
|
|
);
|
|
self.pyramid_defaults.sides = sides;
|
|
self.pyramid_step = PyramidStep::BaseCenter;
|
|
return Some(CmdResult::NeedPoint);
|
|
}
|
|
_ => {}
|
|
}
|
|
let number = crate::entities::common::parse_typed_length(token)?;
|
|
match self.pyramid_step {
|
|
PyramidStep::BaseRadius if number > 0.0 => {
|
|
let frame = self.pyramid_frame?;
|
|
self.pyramid_set_base_from_point(frame.origin + frame.x * number)
|
|
.then_some(CmdResult::NeedPoint)
|
|
}
|
|
PyramidStep::TopRadius if number >= 0.0 => {
|
|
self.pyramid_top_radius = pyramid_display_to_circumradius(
|
|
number,
|
|
self.pyramid_type,
|
|
self.pyramid_sides,
|
|
);
|
|
self.pyramid_step = PyramidStep::HeightAfterTopRadius;
|
|
Some(CmdResult::NeedPoint)
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn pyramid_mouse_move(&self, point: DVec3) -> Option<WireModel> {
|
|
match self.pyramid_step {
|
|
PyramidStep::EdgeSecond => {
|
|
let first = self.pyramid_edge_first?;
|
|
let mut preview = PrimitiveCommand::new("PYRAMID");
|
|
preview.plane = self.plane;
|
|
preview.pyramid_sides = self.pyramid_sides;
|
|
preview.pyramid_type = self.pyramid_type;
|
|
preview.pyramid_set_base_from_edge(first, point);
|
|
let frame = preview.pyramid_frame?;
|
|
preview.pyramid_base_preview(
|
|
frame,
|
|
pyramid_circumradius_to_display(
|
|
preview.pyramid_base_radius,
|
|
preview.pyramid_type,
|
|
preview.pyramid_sides,
|
|
),
|
|
)
|
|
}
|
|
PyramidStep::BaseRadius => {
|
|
let frame = self.pyramid_frame?;
|
|
let delta = point - frame.origin;
|
|
let planar = delta - frame.z * delta.dot(frame.z);
|
|
let displayed = planar.length();
|
|
let direction = planar.try_normalize()?;
|
|
let x = match self.pyramid_type {
|
|
PyramidType::Inscribed => direction,
|
|
PyramidType::Circumscribed => {
|
|
let half = std::f64::consts::PI / self.pyramid_sides as f64;
|
|
direction * half.cos() - frame.z.cross(direction) * half.sin()
|
|
}
|
|
}
|
|
.try_normalize()?;
|
|
let y = frame.z.cross(x).try_normalize()?;
|
|
self.pyramid_base_preview(WorkingPlane::new(frame.origin, x, y), displayed)
|
|
}
|
|
PyramidStep::Height | PyramidStep::HeightAfterTopRadius => {
|
|
let frame = self.pyramid_frame?;
|
|
self.pyramid_preview(frame.z * self.pyramid_height_at(point)?)
|
|
}
|
|
PyramidStep::HeightSecondPoint => {
|
|
let frame = self.pyramid_frame?;
|
|
self.pyramid_preview(frame.z * self.pyramid_height_first?.distance(point))
|
|
}
|
|
PyramidStep::AxisEndpoint => {
|
|
let frame = self.pyramid_frame?;
|
|
self.pyramid_preview(point - frame.origin)
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Number of footprint points the shape needs before the height step.
|
|
fn footprint_pts(&self) -> usize {
|
|
match self.shape {
|
|
Shape::Torus => 3, // centre, major-radius, minor-radius
|
|
_ => 2, // corner/corner or centre/radius
|
|
}
|
|
}
|
|
|
|
/// A reasonable default height when the user just presses Enter.
|
|
fn default_height(&self) -> f64 {
|
|
if self.shape == Shape::Wedge {
|
|
let bits = LAST_WEDGE_HEIGHT.load(Ordering::Relaxed);
|
|
if bits != 0 {
|
|
let height = f64::from_bits(bits);
|
|
if height.is_finite() && height >= 1e-6 {
|
|
return height;
|
|
}
|
|
}
|
|
}
|
|
let height = match self.shape {
|
|
Shape::Box | Shape::Wedge => match (self.box_length, self.box_width) {
|
|
(Some(length), Some(width)) => length.max(width),
|
|
_ if self.pts.len() >= 2 => {
|
|
let d = self.pts[1] - self.pts[0];
|
|
d.x.abs().max(d.y.abs())
|
|
}
|
|
_ => 1.0,
|
|
},
|
|
_ if self.pts.len() >= 2 => (self.pts[1] - self.pts[0]).length(),
|
|
_ => 1.0,
|
|
};
|
|
height.max(1.0)
|
|
}
|
|
|
|
fn cursor_height(&self, point: DVec3) -> f64 {
|
|
let height = self.plane.to_local(point).z - self.pts[0].z;
|
|
if self.shape.rectangular() {
|
|
height
|
|
} else {
|
|
height.max(1e-6)
|
|
}
|
|
}
|
|
|
|
fn place_preview(&self, mut preview: WireModel) -> WireModel {
|
|
for point in &mut preview.points {
|
|
if !point[0].is_nan() {
|
|
*point = self
|
|
.plane
|
|
.to_world(glam::Vec3::from_array(*point).as_dvec3())
|
|
.as_vec3()
|
|
.to_array();
|
|
}
|
|
}
|
|
preview
|
|
}
|
|
|
|
fn history_transform(&self, origin: DVec3) -> [f64; 16] {
|
|
glam::DMat4::from_cols(
|
|
self.plane.x.extend(0.0),
|
|
self.plane.y.extend(0.0),
|
|
self.plane.z.extend(0.0),
|
|
self.plane.to_world(origin).extend(1.0),
|
|
)
|
|
.to_cols_array()
|
|
}
|
|
|
|
fn history_transform_axes(
|
|
&self,
|
|
origin: DVec3,
|
|
x_axis: DVec3,
|
|
y_axis: DVec3,
|
|
) -> [f64; 16] {
|
|
let to_world_vector = |value: DVec3| {
|
|
self.plane.x * value.x + self.plane.y * value.y + self.plane.z * value.z
|
|
};
|
|
glam::DMat4::from_cols(
|
|
to_world_vector(x_axis).extend(0.0),
|
|
to_world_vector(y_axis).extend(0.0),
|
|
self.plane.z.extend(0.0),
|
|
self.plane.to_world(origin).extend(1.0),
|
|
)
|
|
.to_cols_array()
|
|
}
|
|
|
|
fn box_spec(&self, height: f64) -> Option<(DVec3, DVec3, DVec3, f64, f64, f64)> {
|
|
let (mut origin, x_axis, y_axis, length, width) =
|
|
if let (Some(origin), Some(length), Some(width)) =
|
|
(self.box_origin, self.box_length, self.box_width)
|
|
{
|
|
let (sin, cos) = self.box_angle.sin_cos();
|
|
(
|
|
origin,
|
|
DVec3::new(cos, sin, 0.0),
|
|
DVec3::new(-sin, cos, 0.0) * self.box_width_sign,
|
|
length,
|
|
width,
|
|
)
|
|
} else {
|
|
let (a, b) = (*self.pts.first()?, *self.pts.get(1)?);
|
|
(
|
|
DVec3::new(a.x.min(b.x), a.y.min(b.y), a.z),
|
|
DVec3::X,
|
|
DVec3::Y,
|
|
(b.x - a.x).abs(),
|
|
(b.y - a.y).abs(),
|
|
)
|
|
};
|
|
let height_abs = height.abs();
|
|
if !origin.is_finite()
|
|
|| !x_axis.is_finite()
|
|
|| !y_axis.is_finite()
|
|
|| !length.is_finite()
|
|
|| !width.is_finite()
|
|
|| !height_abs.is_finite()
|
|
|| length < 1e-6
|
|
|| width < 1e-6
|
|
|| height_abs < 1e-6
|
|
{
|
|
return None;
|
|
}
|
|
if height < 0.0 {
|
|
origin.z += height;
|
|
}
|
|
if !origin.is_finite() {
|
|
return None;
|
|
}
|
|
Some((origin, x_axis, y_axis, length, width, height_abs))
|
|
}
|
|
|
|
fn build_box(&self, height: f64) -> Option<(Body, SolidHistoryOperation)> {
|
|
use crate::scene::model::solid_history;
|
|
|
|
let (origin, x_axis, y_axis, length, width, height) = self.box_spec(height)?;
|
|
let base = solid_model::box_solid(
|
|
[length * 0.5, width * 0.5, height * 0.5],
|
|
length,
|
|
width,
|
|
height,
|
|
)?;
|
|
let placed = solid_model::placed(
|
|
&base,
|
|
x_axis.to_array(),
|
|
y_axis.to_array(),
|
|
DVec3::Z.to_array(),
|
|
origin.to_array(),
|
|
)?;
|
|
Some((
|
|
placed,
|
|
solid_history::box_op(
|
|
self.history_transform_axes(origin, x_axis, y_axis),
|
|
length,
|
|
width,
|
|
height,
|
|
),
|
|
))
|
|
}
|
|
|
|
fn set_box_corner_footprint(&mut self, point: DVec3) -> bool {
|
|
let first = self.pts[0];
|
|
let delta = point - first;
|
|
if delta.x.abs() < 1e-6 || delta.y.abs() < 1e-6 {
|
|
return false;
|
|
}
|
|
if self.box_centered {
|
|
self.pts = vec![
|
|
DVec3::new(first.x - delta.x.abs(), first.y - delta.y.abs(), first.z),
|
|
DVec3::new(first.x + delta.x.abs(), first.y + delta.y.abs(), first.z),
|
|
];
|
|
} else {
|
|
self.pts.push(point);
|
|
}
|
|
self.box_step = BoxStep::Height;
|
|
self.height_step = true;
|
|
true
|
|
}
|
|
|
|
fn set_box_cube(&mut self, size: f64, angle: f64) -> Option<CmdResult> {
|
|
let size = size.abs();
|
|
if !size.is_finite() || !angle.is_finite() || size < 1e-6 {
|
|
return None;
|
|
}
|
|
let (sin, cos) = angle.sin_cos();
|
|
let x_axis = DVec3::new(cos, sin, 0.0);
|
|
let y_axis = DVec3::new(-sin, cos, 0.0);
|
|
let origin = if self.box_centered {
|
|
self.pts[0] - x_axis * size * 0.5 - y_axis * size * 0.5
|
|
} else {
|
|
self.pts[0]
|
|
};
|
|
self.box_origin = Some(origin);
|
|
self.box_length = Some(size);
|
|
self.box_width = Some(size);
|
|
self.box_angle = angle;
|
|
self.box_width_sign = 1.0;
|
|
Some(self.commit(size))
|
|
}
|
|
|
|
fn set_box_length(&mut self, length: f64, angle: f64) -> bool {
|
|
let length = length.abs();
|
|
if !length.is_finite() || !angle.is_finite() || length < 1e-6 {
|
|
return false;
|
|
}
|
|
self.box_origin = self.pts.first().copied();
|
|
self.box_length = Some(length);
|
|
self.box_angle = angle;
|
|
self.box_step = BoxStep::Width;
|
|
true
|
|
}
|
|
|
|
fn set_box_width(&mut self, width: f64, sign: f64) -> bool {
|
|
let width = width.abs();
|
|
if !width.is_finite() || !sign.is_finite() || width < 1e-6 {
|
|
return false;
|
|
}
|
|
self.box_width = Some(width);
|
|
self.box_width_sign = if sign < 0.0 { -1.0 } else { 1.0 };
|
|
if self.box_centered {
|
|
let center = self.pts[0];
|
|
let (sin, cos) = self.box_angle.sin_cos();
|
|
let x_axis = DVec3::new(cos, sin, 0.0);
|
|
let y_axis = DVec3::new(-sin, cos, 0.0) * self.box_width_sign;
|
|
self.box_origin = Some(
|
|
center
|
|
- x_axis * self.box_length.unwrap_or_default() * 0.5
|
|
- y_axis * width * 0.5,
|
|
);
|
|
}
|
|
self.box_step = BoxStep::Height;
|
|
self.height_step = true;
|
|
true
|
|
}
|
|
|
|
fn rectangular_preview(&self, height: f64) -> Option<WireModel> {
|
|
let (origin, x_axis, y_axis, length, width, height) = self.box_spec(height)?;
|
|
let mut points = Vec::new();
|
|
if self.shape == Shape::Wedge {
|
|
let near = [
|
|
origin,
|
|
origin + x_axis * length,
|
|
origin + DVec3::Z * height,
|
|
];
|
|
let far = near.map(|point| point + y_axis * width);
|
|
push_loop(&mut points, &near);
|
|
push_loop(&mut points, &far);
|
|
for index in 0..3 {
|
|
push_segment(&mut points, near[index], far[index]);
|
|
}
|
|
} else {
|
|
let base = [
|
|
origin,
|
|
origin + x_axis * length,
|
|
origin + x_axis * length + y_axis * width,
|
|
origin + y_axis * width,
|
|
];
|
|
let top = base.map(|point| point + DVec3::Z * height);
|
|
push_loop(&mut points, &base);
|
|
push_loop(&mut points, &top);
|
|
for index in 0..4 {
|
|
push_segment(&mut points, base[index], top[index]);
|
|
}
|
|
}
|
|
Some(self.place_preview(wire("primitive_height_preview", points)))
|
|
}
|
|
|
|
fn build_wedge(&self, height: f64) -> Option<(Body, SolidHistoryOperation)> {
|
|
use crate::scene::model::solid_history;
|
|
|
|
let (origin, x_axis, y_axis, length, width, height) = self.box_spec(height)?;
|
|
let base = solid_model::wedge_solid([0.0; 3], length, width, height)?;
|
|
let placed = solid_model::placed(
|
|
&base,
|
|
x_axis.to_array(),
|
|
y_axis.to_array(),
|
|
DVec3::Z.to_array(),
|
|
origin.to_array(),
|
|
)?;
|
|
Some((
|
|
placed,
|
|
solid_history::wedge_op(
|
|
self.history_transform_axes(origin, x_axis, y_axis),
|
|
length,
|
|
width,
|
|
height,
|
|
),
|
|
))
|
|
}
|
|
|
|
/// Build the persistent kernel body and its history node.
|
|
fn build(&self, height: f64) -> Option<(Body, SolidHistoryOperation)> {
|
|
use crate::scene::model::solid_history;
|
|
|
|
let (solid, history) = match self.shape {
|
|
Shape::Box => return self.build_box(height),
|
|
Shape::Wedge => return self.build_wedge(height),
|
|
Shape::Cylinder => {
|
|
let c = self.pts[0];
|
|
let r = (self.pts[1] - c).length();
|
|
if r < 1e-6 || height < 1e-6 {
|
|
return None;
|
|
}
|
|
let center = [c.x, c.y, c.z];
|
|
(
|
|
solid_model::cylinder_solid(center, r, height),
|
|
solid_history::cylinder_op(self.history_transform(c), r, height),
|
|
)
|
|
}
|
|
Shape::Cone => return None,
|
|
Shape::Sphere => {
|
|
let c = self.pts[0];
|
|
let r = (self.pts[1] - c).length();
|
|
if r < 1e-6 {
|
|
return None;
|
|
}
|
|
let center = [c.x, c.y, c.z];
|
|
(
|
|
solid_model::sphere_solid(center, r),
|
|
solid_history::sphere_op(self.history_transform(c), r),
|
|
)
|
|
}
|
|
Shape::Pyramid => return None,
|
|
Shape::Torus => {
|
|
let c = self.pts[0];
|
|
let first = (self.pts[1] - c).length();
|
|
let second = (self.pts[2] - c).length();
|
|
let outer = first.max(second);
|
|
let inner = first.min(second);
|
|
if inner < 1e-6 || outer - inner < 1e-6 {
|
|
return None;
|
|
}
|
|
let major = (outer + inner) * 0.5;
|
|
let minor = (outer - inner) * 0.5;
|
|
let center = [c.x, c.y, c.z];
|
|
(
|
|
solid_model::torus_solid(center, major, minor),
|
|
solid_history::torus_op(
|
|
self.history_transform(c),
|
|
major,
|
|
minor,
|
|
),
|
|
)
|
|
}
|
|
};
|
|
let solid = solid?;
|
|
Some((solid, history))
|
|
}
|
|
|
|
fn commit(&self, height: f64) -> CmdResult {
|
|
match self.build(height) {
|
|
Some((solid, history)) => {
|
|
if self.shape == Shape::Wedge {
|
|
LAST_WEDGE_HEIGHT.store(height.abs().to_bits(), Ordering::Relaxed);
|
|
}
|
|
// Place the local body on the working plane.
|
|
let placed = solid_model::placed(
|
|
&solid,
|
|
[self.plane.x.x, self.plane.x.y, self.plane.x.z],
|
|
[self.plane.y.x, self.plane.y.y, self.plane.y.z],
|
|
[self.plane.z.x, self.plane.z.y, self.plane.z.z],
|
|
[
|
|
self.plane.origin.x,
|
|
self.plane.origin.y,
|
|
self.plane.origin.z,
|
|
],
|
|
);
|
|
match placed {
|
|
Some(placed) => {
|
|
let Some(document) =
|
|
crate::scene::convert::acis_export::solid_to_sat(&placed)
|
|
else {
|
|
return CmdResult::Cancel;
|
|
};
|
|
let mut entity = Solid3D::new();
|
|
entity.set_sat_document(&document);
|
|
entity.wires = solid_model::edge_wires(&placed);
|
|
CmdResult::CommitSolid {
|
|
entity: EntityType::Solid3D(entity),
|
|
solid: Box::new(placed),
|
|
history,
|
|
}
|
|
}
|
|
None => CmdResult::Cancel,
|
|
}
|
|
}
|
|
None => CmdResult::Cancel,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CadCommand for PrimitiveCommand {
|
|
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
|
self.plane = plane;
|
|
}
|
|
|
|
fn cursor_axis(&self) -> Option<(DVec3, DVec3)> {
|
|
if self.shape == Shape::Cone {
|
|
return matches!(self.cone_step, ConeStep::Height | ConeStep::HeightAfterTopRadius)
|
|
.then(|| {
|
|
let frame = self.cone_frame.unwrap_or(self.plane);
|
|
(frame.origin, frame.z.normalize_or_zero())
|
|
});
|
|
}
|
|
|
|
if self.shape == Shape::Pyramid {
|
|
return matches!(
|
|
self.pyramid_step,
|
|
PyramidStep::Height | PyramidStep::HeightAfterTopRadius
|
|
)
|
|
.then(|| {
|
|
let frame = self.pyramid_frame.unwrap_or(self.plane);
|
|
(frame.origin, frame.z.normalize_or_zero())
|
|
});
|
|
}
|
|
let constrained = if self.shape.rectangular() {
|
|
self.box_step == BoxStep::Height
|
|
} else {
|
|
self.height_step
|
|
};
|
|
constrained.then(|| {
|
|
(
|
|
self.plane.to_world(self.pts[0]),
|
|
self.plane.z.normalize_or_zero(),
|
|
)
|
|
})
|
|
}
|
|
|
|
fn name(&self) -> &'static str {
|
|
self.shape.name()
|
|
}
|
|
|
|
fn needs_tangent_pick(&self) -> bool {
|
|
(self.shape == Shape::Sphere
|
|
&& matches!(
|
|
self.sphere_step,
|
|
SphereStep::TtrFirst | SphereStep::TtrSecond { .. }
|
|
))
|
|
|| (self.shape == Shape::Cone
|
|
&& matches!(
|
|
self.cone_step,
|
|
ConeStep::TtrFirst | ConeStep::TtrSecond { .. }
|
|
))
|
|
}
|
|
|
|
fn on_tangent_point(&mut self, object: TangentObject, hit: DVec3) -> CmdResult {
|
|
if self.shape == Shape::Cone {
|
|
return self.on_cone_tangent(object, hit);
|
|
}
|
|
if self.shape != Shape::Sphere {
|
|
return self.on_point(hit);
|
|
}
|
|
let object = sphere_tangent_local(object, self.plane);
|
|
let hit = self.plane.to_local(hit);
|
|
match self.sphere_step {
|
|
SphereStep::TtrFirst => {
|
|
self.sphere_step = SphereStep::TtrSecond { object, hit };
|
|
CmdResult::NeedPoint
|
|
}
|
|
SphereStep::TtrSecond {
|
|
object: first,
|
|
hit: first_hit,
|
|
} => {
|
|
self.sphere_step = SphereStep::TtrRadius {
|
|
first,
|
|
second: object,
|
|
first_hit,
|
|
second_hit: hit,
|
|
};
|
|
CmdResult::NeedPoint
|
|
}
|
|
_ => CmdResult::NeedPoint,
|
|
}
|
|
}
|
|
|
|
fn prompt(&self) -> String {
|
|
let n = self.shape.name();
|
|
if self.shape == Shape::Cone {
|
|
return self.cone_prompt();
|
|
}
|
|
if self.shape == Shape::Pyramid {
|
|
return self.pyramid_prompt();
|
|
}
|
|
if self.shape.rectangular() {
|
|
return match self.box_step {
|
|
BoxStep::FirstCorner => t!("%{n} Specify first corner or [Center]:", n = n),
|
|
BoxStep::CenterPoint => t!("%{n} Specify center point:", n = n),
|
|
BoxStep::OppositeCorner => {
|
|
t!("%{n} Specify other corner or [Cube/Length]:", n = n)
|
|
}
|
|
BoxStep::CubeSize => t!("%{n} Specify cube length:", n = n),
|
|
BoxStep::Length => t!("%{n} Specify length:", n = n),
|
|
BoxStep::Width => t!("%{n} Specify width:", n = n),
|
|
BoxStep::Height if self.shape == Shape::Wedge => t!(
|
|
"%{n} Specify height or [2Point] <%{height}>:",
|
|
n = n,
|
|
height = format!("{:.4}", self.default_height())
|
|
),
|
|
BoxStep::Height => t!("%{n} Specify height or [2Point]:", n = n),
|
|
BoxStep::HeightFirstPoint => {
|
|
t!("%{n} Specify first point for height:", n = n)
|
|
}
|
|
BoxStep::HeightSecondPoint => {
|
|
t!("%{n} Specify second point for height:", n = n)
|
|
}
|
|
}
|
|
.into_owned();
|
|
}
|
|
if self.shape == Shape::Sphere {
|
|
return match self.sphere_step {
|
|
SphereStep::Center => {
|
|
t!("SPHERE Specify center point or [3P/2P/Ttr]:").into_owned()
|
|
}
|
|
SphereStep::Radius(_) => crate::tf!(
|
|
"SPHERE Specify radius or [Diameter] <{:.4}>:",
|
|
self.sphere_default_radius
|
|
)
|
|
.into_owned(),
|
|
SphereStep::Diameter(_) => crate::tf!(
|
|
"SPHERE Specify diameter <{:.4}>:",
|
|
self.sphere_default_radius * 2.0
|
|
)
|
|
.into_owned(),
|
|
SphereStep::TwoPointFirst => {
|
|
t!("SPHERE Specify first end point of diameter:").into_owned()
|
|
}
|
|
SphereStep::TwoPointSecond(_) => {
|
|
t!("SPHERE Specify second end point of diameter:").into_owned()
|
|
}
|
|
SphereStep::ThreePointFirst => {
|
|
t!("SPHERE Specify first point on sphere:").into_owned()
|
|
}
|
|
SphereStep::ThreePointSecond(_) => {
|
|
t!("SPHERE Specify second point on sphere:").into_owned()
|
|
}
|
|
SphereStep::ThreePointThird(_, _) => {
|
|
t!("SPHERE Specify third point on sphere:").into_owned()
|
|
}
|
|
SphereStep::TtrFirst => {
|
|
t!("SPHERE Select first tangent object:").into_owned()
|
|
}
|
|
SphereStep::TtrSecond { .. } => {
|
|
t!("SPHERE Select second tangent object:").into_owned()
|
|
}
|
|
SphereStep::TtrRadius { .. } => crate::tf!(
|
|
"SPHERE Specify radius <{:.4}>:",
|
|
self.sphere_default_radius
|
|
)
|
|
.into_owned(),
|
|
};
|
|
}
|
|
if self.height_step {
|
|
return t!("%{n} Specify height <Enter for default>:", n = n).into_owned();
|
|
}
|
|
match (self.shape, self.pts.len()) {
|
|
(Shape::Torus, 0) => t!("%{n} Specify center point:", n = n).into_owned(),
|
|
(Shape::Torus, 1) => t!("%{n} Specify outer radius:", n = n).into_owned(),
|
|
(Shape::Torus, _) => t!("%{n} Specify inner radius:", n = n).into_owned(),
|
|
(shape, 0) if shape.radial() => {
|
|
t!("%{n} Specify center point:", n = n).into_owned()
|
|
}
|
|
(shape, _) if shape.radial() => {
|
|
t!("%{n} Specify radius:", n = n).into_owned()
|
|
}
|
|
(_, 0) => t!("%{n} Specify first corner:", n = n).into_owned(),
|
|
(_, _) => t!("%{n} Specify opposite corner:", n = n).into_owned(),
|
|
}
|
|
}
|
|
|
|
fn options(&self) -> Vec<CmdOption> {
|
|
if self.shape == Shape::Sphere {
|
|
return match self.sphere_step {
|
|
SphereStep::Center => vec![
|
|
CmdOption::new("3P", "3P"),
|
|
CmdOption::new("2P", "2P"),
|
|
CmdOption::new("Ttr", "T"),
|
|
],
|
|
SphereStep::Radius(_) => vec![CmdOption::new(t!("Diameter").as_ref(), "D")],
|
|
_ => Vec::new(),
|
|
};
|
|
}
|
|
if self.shape == Shape::Cone {
|
|
return self.cone_options();
|
|
}
|
|
if self.shape == Shape::Pyramid {
|
|
return self.pyramid_options();
|
|
}
|
|
if !self.shape.rectangular() {
|
|
return Vec::new();
|
|
}
|
|
match self.box_step {
|
|
BoxStep::FirstCorner => vec![CmdOption::new(t!("Center").as_ref(), "C")],
|
|
BoxStep::OppositeCorner => vec![
|
|
CmdOption::new(t!("Cube").as_ref(), "C"),
|
|
CmdOption::new(t!("Length").as_ref(), "L"),
|
|
],
|
|
BoxStep::Height => vec![CmdOption::new(t!("2Point").as_ref(), "2P")],
|
|
_ => Vec::new(),
|
|
}
|
|
}
|
|
|
|
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
|
if self.shape == Shape::Sphere {
|
|
let local = self.plane.to_local(pt);
|
|
if !local.is_finite() {
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
return match self.sphere_step {
|
|
SphereStep::Center => {
|
|
self.sphere_step = SphereStep::Radius(local);
|
|
CmdResult::NeedPoint
|
|
}
|
|
SphereStep::Radius(center) => self.commit_sphere(center, center.distance(local)),
|
|
SphereStep::Diameter(center) => {
|
|
self.commit_sphere(center, center.distance(local) * 0.5)
|
|
}
|
|
SphereStep::TwoPointFirst => {
|
|
self.sphere_step = SphereStep::TwoPointSecond(local);
|
|
CmdResult::NeedPoint
|
|
}
|
|
SphereStep::TwoPointSecond(first) => {
|
|
self.commit_sphere((first + local) * 0.5, first.distance(local) * 0.5)
|
|
}
|
|
SphereStep::ThreePointFirst => {
|
|
self.sphere_step = SphereStep::ThreePointSecond(local);
|
|
CmdResult::NeedPoint
|
|
}
|
|
SphereStep::ThreePointSecond(first) => {
|
|
self.sphere_step = SphereStep::ThreePointThird(first, local);
|
|
CmdResult::NeedPoint
|
|
}
|
|
SphereStep::ThreePointThird(first, second) => {
|
|
let Some((center, radius)) =
|
|
sphere_through_three_points(first, second, local)
|
|
else {
|
|
return CmdResult::NeedPoint;
|
|
};
|
|
self.commit_sphere(center, radius)
|
|
}
|
|
SphereStep::TtrRadius { second_hit, .. } => {
|
|
self.sphere_ttr_result(second_hit.distance(local))
|
|
}
|
|
SphereStep::TtrFirst | SphereStep::TtrSecond { .. } => CmdResult::NeedPoint,
|
|
};
|
|
}
|
|
if self.shape == Shape::Cone {
|
|
return self.on_cone_point(pt);
|
|
}
|
|
if self.shape == Shape::Pyramid {
|
|
return self.on_pyramid_point(pt);
|
|
}
|
|
if self.shape.rectangular() {
|
|
let local = self.plane.to_local(pt);
|
|
if !local.is_finite() {
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
return match self.box_step {
|
|
BoxStep::FirstCorner | BoxStep::CenterPoint => {
|
|
self.pts = vec![local];
|
|
self.box_step = BoxStep::OppositeCorner;
|
|
CmdResult::NeedPoint
|
|
}
|
|
BoxStep::OppositeCorner => {
|
|
let height = local.z - self.pts[0].z;
|
|
if !self.set_box_corner_footprint(local) {
|
|
CmdResult::NeedPoint
|
|
} else if self.shape == Shape::Wedge && height.abs() >= 1e-6 {
|
|
self.commit(height)
|
|
} else {
|
|
CmdResult::NeedPoint
|
|
}
|
|
}
|
|
BoxStep::CubeSize => {
|
|
let delta = local - self.pts[0];
|
|
let (size, angle) = if self.shape == Shape::Wedge {
|
|
(delta.x.hypot(delta.y), delta.y.atan2(delta.x))
|
|
} else {
|
|
(delta.length(), 0.0)
|
|
};
|
|
self.set_box_cube(size, angle)
|
|
.unwrap_or(CmdResult::NeedPoint)
|
|
}
|
|
BoxStep::Length => {
|
|
let delta = local - self.pts[0];
|
|
if !self.set_box_length(delta.x.hypot(delta.y), delta.y.atan2(delta.x)) {
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
CmdResult::NeedPoint
|
|
}
|
|
BoxStep::Width => {
|
|
let delta = local - self.pts[0];
|
|
let normal = DVec3::new(-self.box_angle.sin(), self.box_angle.cos(), 0.0);
|
|
let signed = delta.dot(normal);
|
|
if !self.set_box_width(signed, signed) {
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
CmdResult::NeedPoint
|
|
}
|
|
BoxStep::Height => {
|
|
let height = self.cursor_height(pt);
|
|
if height.abs() < 1e-6 {
|
|
CmdResult::NeedPoint
|
|
} else {
|
|
self.commit(height)
|
|
}
|
|
}
|
|
BoxStep::HeightFirstPoint => {
|
|
self.box_height_anchor = Some(local);
|
|
self.box_step = BoxStep::HeightSecondPoint;
|
|
CmdResult::NeedPoint
|
|
}
|
|
BoxStep::HeightSecondPoint => {
|
|
let Some(first) = self.box_height_anchor else {
|
|
return CmdResult::NeedPoint;
|
|
};
|
|
let height = (local - first).length();
|
|
if height < 1e-6 {
|
|
CmdResult::NeedPoint
|
|
} else {
|
|
self.commit(height)
|
|
}
|
|
}
|
|
};
|
|
}
|
|
if self.height_step {
|
|
return self.commit(self.cursor_height(pt));
|
|
}
|
|
self.pts.push(self.plane.to_local(pt));
|
|
if self.pts.len() < self.footprint_pts() {
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
// Footprint complete.
|
|
if self.shape.needs_height() {
|
|
self.height_step = true;
|
|
CmdResult::NeedPoint
|
|
} else {
|
|
self.commit(0.0)
|
|
}
|
|
}
|
|
|
|
fn on_enter(&mut self) -> CmdResult {
|
|
if self.shape == Shape::Sphere {
|
|
return match self.sphere_step {
|
|
SphereStep::Radius(center) => {
|
|
self.commit_sphere(center, self.sphere_default_radius)
|
|
}
|
|
SphereStep::Diameter(center) => {
|
|
self.commit_sphere(center, self.sphere_default_radius)
|
|
}
|
|
SphereStep::TtrRadius { .. } => {
|
|
self.sphere_ttr_result(self.sphere_default_radius)
|
|
}
|
|
_ => CmdResult::Cancel,
|
|
};
|
|
}
|
|
if self.shape == Shape::Cone {
|
|
return match self.cone_step {
|
|
ConeStep::BaseRadius | ConeStep::BaseDiameter => {
|
|
let Some(frame) = self.cone_frame else {
|
|
return CmdResult::Cancel;
|
|
};
|
|
self.set_cone_base(
|
|
frame,
|
|
self.cone_defaults.base_x_radius,
|
|
self.cone_defaults.base_y_radius,
|
|
);
|
|
CmdResult::NeedPoint
|
|
}
|
|
ConeStep::TtrRadius { .. } => {
|
|
let radius = self.cone_defaults.base_x_radius;
|
|
let Some(center) = self.cone_ttr_center(radius) else {
|
|
return CmdResult::NeedPoint;
|
|
};
|
|
self.set_cone_base(self.cone_frame_at(center), radius, radius);
|
|
CmdResult::NeedPoint
|
|
}
|
|
ConeStep::EllipseCenterFirstAxis(center) => {
|
|
self.cone_step = ConeStep::EllipseCenterSecondAxis(
|
|
center,
|
|
center + self.plane.x * self.cone_defaults.base_x_radius,
|
|
);
|
|
CmdResult::NeedPoint
|
|
}
|
|
ConeStep::Height | ConeStep::HeightAfterTopRadius => {
|
|
self.commit_cone_height(self.cone_defaults.height)
|
|
}
|
|
ConeStep::TopRadius => {
|
|
self.cone_top_radius = self.cone_defaults.top_radius;
|
|
self.cone_step = ConeStep::HeightAfterTopRadius;
|
|
CmdResult::NeedPoint
|
|
}
|
|
_ => CmdResult::Cancel,
|
|
};
|
|
}
|
|
if self.shape == Shape::Pyramid {
|
|
return match self.pyramid_step {
|
|
PyramidStep::Sides => {
|
|
self.pyramid_step = PyramidStep::BaseCenter;
|
|
CmdResult::NeedPoint
|
|
}
|
|
PyramidStep::BaseRadius => {
|
|
let displayed = self.pyramid_defaults.displayed_base_radius;
|
|
let Some(frame) = self.pyramid_frame else {
|
|
return CmdResult::Cancel;
|
|
};
|
|
self.pyramid_set_base_from_point(frame.origin + frame.x * displayed);
|
|
CmdResult::NeedPoint
|
|
}
|
|
PyramidStep::Height | PyramidStep::HeightAfterTopRadius => {
|
|
self.commit_pyramid_height(self.pyramid_defaults.height)
|
|
}
|
|
PyramidStep::TopRadius => {
|
|
self.pyramid_top_radius = pyramid_display_to_circumradius(
|
|
self.pyramid_defaults.displayed_top_radius,
|
|
self.pyramid_type,
|
|
self.pyramid_sides,
|
|
);
|
|
self.pyramid_step = PyramidStep::HeightAfterTopRadius;
|
|
CmdResult::NeedPoint
|
|
}
|
|
_ => CmdResult::Cancel,
|
|
};
|
|
}
|
|
if self.shape.rectangular() {
|
|
return if self.shape == Shape::Wedge && self.box_step == BoxStep::Height {
|
|
self.commit(self.default_height())
|
|
} else {
|
|
CmdResult::Cancel
|
|
};
|
|
}
|
|
if self.height_step {
|
|
let h = self.default_height();
|
|
return self.commit(h);
|
|
}
|
|
CmdResult::Cancel
|
|
}
|
|
|
|
fn on_escape(&mut self) -> CmdResult {
|
|
CmdResult::Cancel
|
|
}
|
|
|
|
fn wants_text_input(&self) -> bool {
|
|
if self.shape == Shape::Sphere {
|
|
return matches!(
|
|
self.sphere_step,
|
|
SphereStep::Center
|
|
| SphereStep::Radius(_)
|
|
| SphereStep::Diameter(_)
|
|
| SphereStep::TtrRadius { .. }
|
|
);
|
|
}
|
|
if self.shape == Shape::Cone {
|
|
return matches!(
|
|
self.cone_step,
|
|
ConeStep::BaseCenter
|
|
| ConeStep::BaseRadius
|
|
| ConeStep::BaseDiameter
|
|
| ConeStep::EllipseFirst
|
|
| ConeStep::EllipseCenterFirstAxis(_)
|
|
| ConeStep::TtrRadius { .. }
|
|
| ConeStep::Height
|
|
| ConeStep::HeightAfterTopRadius
|
|
| ConeStep::TopRadius
|
|
);
|
|
}
|
|
if self.shape == Shape::Pyramid {
|
|
return matches!(
|
|
self.pyramid_step,
|
|
PyramidStep::BaseCenter
|
|
| PyramidStep::Sides
|
|
| PyramidStep::BaseRadius
|
|
| PyramidStep::Height
|
|
| PyramidStep::HeightAfterTopRadius
|
|
| PyramidStep::TopRadius
|
|
);
|
|
}
|
|
if self.shape.rectangular() {
|
|
matches!(
|
|
self.box_step,
|
|
BoxStep::FirstCorner
|
|
| BoxStep::OppositeCorner
|
|
| BoxStep::CubeSize
|
|
| BoxStep::Length
|
|
| BoxStep::Width
|
|
| BoxStep::Height
|
|
)
|
|
} else {
|
|
self.height_step
|
|
}
|
|
}
|
|
|
|
fn point_step_accepts_keywords(&self) -> bool {
|
|
if self.shape == Shape::Sphere {
|
|
return matches!(self.sphere_step, SphereStep::Center | SphereStep::Radius(_));
|
|
}
|
|
if self.shape == Shape::Cone {
|
|
return matches!(
|
|
self.cone_step,
|
|
ConeStep::BaseCenter
|
|
| ConeStep::BaseRadius
|
|
| ConeStep::EllipseFirst
|
|
| ConeStep::Height
|
|
| ConeStep::HeightAfterTopRadius
|
|
);
|
|
}
|
|
if self.shape == Shape::Pyramid {
|
|
return matches!(
|
|
self.pyramid_step,
|
|
PyramidStep::BaseCenter
|
|
| PyramidStep::BaseRadius
|
|
| PyramidStep::Height
|
|
| PyramidStep::HeightAfterTopRadius
|
|
);
|
|
}
|
|
self.shape.rectangular()
|
|
&& matches!(
|
|
self.box_step,
|
|
BoxStep::FirstCorner | BoxStep::OppositeCorner | BoxStep::Height
|
|
)
|
|
}
|
|
|
|
fn on_text_input(&mut self, raw: &str) -> Option<CmdResult> {
|
|
if self.shape == Shape::Sphere {
|
|
let token = raw.trim();
|
|
let upper = token.to_uppercase();
|
|
return match self.sphere_step {
|
|
SphereStep::Center if upper == "3P" => {
|
|
self.sphere_step = SphereStep::ThreePointFirst;
|
|
Some(CmdResult::NeedPoint)
|
|
}
|
|
SphereStep::Center if upper == "2P" => {
|
|
self.sphere_step = SphereStep::TwoPointFirst;
|
|
Some(CmdResult::NeedPoint)
|
|
}
|
|
SphereStep::Center if matches!(upper.as_str(), "T" | "TTR") => {
|
|
self.sphere_step = SphereStep::TtrFirst;
|
|
Some(CmdResult::NeedPoint)
|
|
}
|
|
SphereStep::Radius(_) if matches!(upper.as_str(), "D" | "DIAMETER") => {
|
|
let SphereStep::Radius(center) = self.sphere_step else {
|
|
unreachable!();
|
|
};
|
|
self.sphere_step = SphereStep::Diameter(center);
|
|
Some(CmdResult::NeedPoint)
|
|
}
|
|
SphereStep::Radius(center) => {
|
|
let radius = crate::entities::common::parse_typed_length(token)?;
|
|
Some(self.commit_sphere(center, radius))
|
|
}
|
|
SphereStep::Diameter(center) => {
|
|
let diameter = crate::entities::common::parse_typed_length(token)?;
|
|
Some(self.commit_sphere(center, diameter * 0.5))
|
|
}
|
|
SphereStep::TtrRadius { .. } => {
|
|
let radius = crate::entities::common::parse_typed_length(token)?;
|
|
Some(self.sphere_ttr_result(radius))
|
|
}
|
|
_ => None,
|
|
};
|
|
}
|
|
if self.shape == Shape::Cone {
|
|
return self.on_cone_text(raw);
|
|
}
|
|
if self.shape == Shape::Pyramid {
|
|
return self.on_pyramid_text(raw);
|
|
}
|
|
if self.shape.rectangular() {
|
|
let token = raw.trim();
|
|
let upper = token.to_uppercase();
|
|
return match self.box_step {
|
|
BoxStep::FirstCorner if matches!(upper.as_str(), "C" | "CENTER") => {
|
|
self.box_centered = true;
|
|
self.box_step = BoxStep::CenterPoint;
|
|
Some(CmdResult::NeedPoint)
|
|
}
|
|
BoxStep::OppositeCorner if matches!(upper.as_str(), "C" | "CUBE") => {
|
|
self.box_step = BoxStep::CubeSize;
|
|
Some(CmdResult::NeedPoint)
|
|
}
|
|
BoxStep::OppositeCorner if matches!(upper.as_str(), "L" | "LENGTH") => {
|
|
self.box_step = BoxStep::Length;
|
|
Some(CmdResult::NeedPoint)
|
|
}
|
|
BoxStep::Height if matches!(upper.as_str(), "2P" | "2POINT") => {
|
|
self.box_step = BoxStep::HeightFirstPoint;
|
|
Some(CmdResult::NeedPoint)
|
|
}
|
|
BoxStep::CubeSize => crate::entities::common::parse_typed_length(token)
|
|
.and_then(|value| self.set_box_cube(value, 0.0)),
|
|
BoxStep::Length => {
|
|
let value = crate::entities::common::parse_typed_length(token)?;
|
|
self.set_box_length(value, 0.0)
|
|
.then_some(CmdResult::NeedPoint)
|
|
}
|
|
BoxStep::Width => {
|
|
let value = crate::entities::common::parse_typed_length(token)?;
|
|
self.set_box_width(value, value)
|
|
.then_some(CmdResult::NeedPoint)
|
|
}
|
|
BoxStep::Height => {
|
|
let value = crate::entities::common::parse_typed_length(token)?;
|
|
(value.is_finite() && value.abs() >= 1e-6).then(|| self.commit(value))
|
|
}
|
|
_ => None,
|
|
};
|
|
}
|
|
if !self.height_step {
|
|
return None;
|
|
}
|
|
let h: f64 = raw.trim().parse().ok().filter(|v| *v > 0.0)?;
|
|
Some(self.commit(h))
|
|
}
|
|
|
|
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
|
if self.shape == Shape::Sphere {
|
|
let local = self.plane.to_local(pt);
|
|
if !local.is_finite() {
|
|
return None;
|
|
}
|
|
return match self.sphere_step {
|
|
SphereStep::Radius(center) => self.sphere_preview(center, center.distance(local)),
|
|
SphereStep::Diameter(center) => {
|
|
self.sphere_preview(center, center.distance(local) * 0.5)
|
|
}
|
|
SphereStep::TwoPointSecond(first) => {
|
|
self.sphere_preview((first + local) * 0.5, first.distance(local) * 0.5)
|
|
}
|
|
SphereStep::ThreePointSecond(first) => {
|
|
self.sphere_preview((first + local) * 0.5, first.distance(local) * 0.5)
|
|
}
|
|
SphereStep::ThreePointThird(first, second) => {
|
|
let (center, radius) = sphere_through_three_points(first, second, local)?;
|
|
self.sphere_preview(center, radius)
|
|
}
|
|
SphereStep::TtrRadius {
|
|
first,
|
|
second,
|
|
first_hit,
|
|
second_hit,
|
|
} => {
|
|
let radius = second_hit.distance(local);
|
|
let hint = (first_hit + second_hit) * 0.5;
|
|
let center = closest_sphere_center(
|
|
&sphere_ttr_centers(first, second, radius),
|
|
hint,
|
|
)?;
|
|
self.sphere_preview(center, radius)
|
|
}
|
|
_ => None,
|
|
};
|
|
}
|
|
if self.shape == Shape::Cone {
|
|
return self.cone_mouse_move(pt);
|
|
}
|
|
if self.shape == Shape::Pyramid {
|
|
return self.pyramid_mouse_move(pt);
|
|
}
|
|
if self.pts.is_empty() {
|
|
return None;
|
|
}
|
|
if self.shape.rectangular() {
|
|
let local = self.plane.to_local(pt);
|
|
if !local.is_finite() {
|
|
return None;
|
|
}
|
|
return match self.box_step {
|
|
BoxStep::OppositeCorner => {
|
|
let first = self.pts[0];
|
|
let points = if self.box_centered {
|
|
let delta = local - first;
|
|
vec![
|
|
DVec3::new(
|
|
first.x - delta.x.abs(),
|
|
first.y - delta.y.abs(),
|
|
first.z,
|
|
),
|
|
DVec3::new(
|
|
first.x + delta.x.abs(),
|
|
first.y + delta.y.abs(),
|
|
first.z,
|
|
),
|
|
]
|
|
} else {
|
|
vec![first, local]
|
|
};
|
|
Some(self.place_preview(footprint_wire(Shape::Box, &points)))
|
|
}
|
|
BoxStep::CubeSize => {
|
|
let delta = local - self.pts[0];
|
|
let size = if self.shape == Shape::Wedge {
|
|
delta.x.hypot(delta.y)
|
|
} else {
|
|
delta.length()
|
|
}
|
|
.max(1e-6);
|
|
let mut preview = PrimitiveCommand::new(self.shape.name());
|
|
preview.plane = self.plane;
|
|
preview.pts = self.pts.clone();
|
|
preview.box_length = Some(size);
|
|
preview.box_width = Some(size);
|
|
preview.box_width_sign = 1.0;
|
|
if self.shape == Shape::Wedge {
|
|
preview.box_origin = Some(self.pts[0]);
|
|
preview.box_angle = delta.y.atan2(delta.x);
|
|
} else {
|
|
preview.box_origin = Some(if self.box_centered {
|
|
self.pts[0] - DVec3::new(size * 0.5, size * 0.5, 0.0)
|
|
} else {
|
|
self.pts[0]
|
|
});
|
|
}
|
|
if self.shape == Shape::Wedge && self.box_centered {
|
|
let (sin, cos) = preview.box_angle.sin_cos();
|
|
let x_axis = DVec3::new(cos, sin, 0.0);
|
|
let y_axis = DVec3::new(-sin, cos, 0.0);
|
|
preview.box_origin = Some(
|
|
self.pts[0] - x_axis * size * 0.5 - y_axis * size * 0.5,
|
|
);
|
|
}
|
|
preview.rectangular_preview(size)
|
|
}
|
|
BoxStep::Length => Some(self.place_preview(wire(
|
|
"primitive_preview",
|
|
vec![self.pts[0].as_vec3().to_array(), local.as_vec3().to_array()],
|
|
))),
|
|
BoxStep::Width => {
|
|
let delta = local - self.pts[0];
|
|
let normal = DVec3::new(-self.box_angle.sin(), self.box_angle.cos(), 0.0);
|
|
let signed = delta.dot(normal);
|
|
let mut preview = PrimitiveCommand::new(self.shape.name());
|
|
preview.plane = self.plane;
|
|
preview.pts = self.pts.clone();
|
|
preview.box_origin = self.box_origin;
|
|
preview.box_length = self.box_length;
|
|
preview.box_width = Some(signed.abs().max(1e-6));
|
|
preview.box_angle = self.box_angle;
|
|
preview.box_width_sign = if signed < 0.0 { -1.0 } else { 1.0 };
|
|
preview.rectangular_preview(1e-6)
|
|
}
|
|
BoxStep::Height => self.rectangular_preview(self.cursor_height(pt)),
|
|
BoxStep::HeightSecondPoint => self.box_height_anchor.map(|first| {
|
|
self.place_preview(wire(
|
|
"primitive_preview",
|
|
vec![first.as_vec3().to_array(), local.as_vec3().to_array()],
|
|
))
|
|
}),
|
|
_ => None,
|
|
};
|
|
}
|
|
if self.height_step {
|
|
return Some(self.place_preview(height_wire(
|
|
self.shape,
|
|
&self.pts,
|
|
self.cursor_height(pt),
|
|
)));
|
|
}
|
|
let mut foot = self.pts.clone();
|
|
foot.push(self.plane.to_local(pt));
|
|
Some(self.place_preview(footprint_wire(self.shape, &foot)))
|
|
}
|
|
|
|
fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
|
|
use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec};
|
|
|
|
if self.shape == Shape::Sphere {
|
|
let (anchor, role) = match self.sphere_step {
|
|
SphereStep::Radius(center) => (center, DynRole::Radius),
|
|
SphereStep::Diameter(center) => (center, DynRole::Diameter),
|
|
SphereStep::TtrRadius { second_hit, .. } => {
|
|
(second_hit, DynRole::Radius)
|
|
}
|
|
_ => return None,
|
|
};
|
|
return Some(DynSpec {
|
|
anchor: DynAnchor::Point(self.plane.to_world(anchor)),
|
|
fields: vec![DynFieldSpec::new(role)],
|
|
guide: DynGuide::Radius,
|
|
ref_point: None,
|
|
});
|
|
}
|
|
|
|
if self.shape == Shape::Cone {
|
|
let frame = self.cone_frame.unwrap_or(self.plane);
|
|
let (anchor, role) = match self.cone_step {
|
|
ConeStep::BaseRadius | ConeStep::TopRadius => {
|
|
(frame.origin, DynRole::Radius)
|
|
}
|
|
ConeStep::BaseDiameter => (frame.origin, DynRole::Diameter),
|
|
ConeStep::TtrRadius { second_hit, .. } => {
|
|
(self.plane.to_world(second_hit), DynRole::Radius)
|
|
}
|
|
ConeStep::EllipseCenterFirstAxis(center) => (center, DynRole::Distance),
|
|
ConeStep::Height | ConeStep::HeightAfterTopRadius | ConeStep::AxisEndpoint => {
|
|
(frame.origin, DynRole::Height)
|
|
}
|
|
ConeStep::HeightSecondPoint(first) => (first, DynRole::Height),
|
|
_ => return None,
|
|
};
|
|
return Some(DynSpec {
|
|
anchor: DynAnchor::Point(anchor),
|
|
fields: vec![DynFieldSpec::new(role)],
|
|
guide: if matches!(role, DynRole::Radius | DynRole::Diameter) {
|
|
DynGuide::Radius
|
|
} else {
|
|
DynGuide::None
|
|
},
|
|
ref_point: None,
|
|
});
|
|
}
|
|
|
|
if self.shape == Shape::Pyramid {
|
|
let frame = self.pyramid_frame.unwrap_or(self.plane);
|
|
let (anchor, role) = match self.pyramid_step {
|
|
PyramidStep::BaseRadius | PyramidStep::TopRadius => {
|
|
(frame.origin, DynRole::Radius)
|
|
}
|
|
PyramidStep::Height
|
|
| PyramidStep::HeightAfterTopRadius
|
|
| PyramidStep::AxisEndpoint => (frame.origin, DynRole::Height),
|
|
PyramidStep::HeightSecondPoint => {
|
|
(self.pyramid_height_first?, DynRole::Height)
|
|
}
|
|
_ => return None,
|
|
};
|
|
return Some(DynSpec {
|
|
anchor: DynAnchor::Point(anchor),
|
|
fields: vec![DynFieldSpec::new(role)],
|
|
guide: if role == DynRole::Radius {
|
|
DynGuide::Radius
|
|
} else {
|
|
DynGuide::None
|
|
},
|
|
ref_point: None,
|
|
});
|
|
}
|
|
|
|
let role = if self.shape.rectangular() {
|
|
match self.box_step {
|
|
BoxStep::CubeSize | BoxStep::Length => DynRole::Distance,
|
|
BoxStep::Width => DynRole::Width,
|
|
BoxStep::Height => DynRole::Height,
|
|
_ => return None,
|
|
}
|
|
} else if self.height_step {
|
|
DynRole::Height
|
|
} else {
|
|
return None;
|
|
};
|
|
Some(DynSpec {
|
|
anchor: DynAnchor::Point(self.plane.to_world(self.pts[0])),
|
|
fields: vec![DynFieldSpec::new(role)],
|
|
guide: DynGuide::None,
|
|
ref_point: None,
|
|
})
|
|
}
|
|
|
|
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
|
|
if self.shape == Shape::Sphere {
|
|
let local = self.plane.to_local(cursor);
|
|
return match self.sphere_step {
|
|
SphereStep::Radius(center) => Some(center.distance(local)),
|
|
SphereStep::Diameter(center) => Some(center.distance(local)),
|
|
SphereStep::TtrRadius { second_hit, .. } => {
|
|
Some(second_hit.distance(local))
|
|
}
|
|
_ => None,
|
|
};
|
|
}
|
|
if self.shape == Shape::Cone {
|
|
return match self.cone_step {
|
|
ConeStep::BaseRadius | ConeStep::TopRadius => {
|
|
let frame = self.cone_frame?;
|
|
let local = frame.to_local(cursor);
|
|
Some(local.x.hypot(local.y))
|
|
}
|
|
ConeStep::BaseDiameter => {
|
|
let frame = self.cone_frame?;
|
|
let local = frame.to_local(cursor);
|
|
Some(local.x.hypot(local.y))
|
|
}
|
|
ConeStep::TtrRadius { second_hit, .. } => {
|
|
let local = self.plane.to_local(cursor);
|
|
Some((local - second_hit).truncate().length())
|
|
}
|
|
ConeStep::EllipseCenterFirstAxis(center) => Some(center.distance(cursor)),
|
|
ConeStep::Height | ConeStep::HeightAfterTopRadius => {
|
|
self.cone_height_at(cursor).map(f64::abs)
|
|
}
|
|
ConeStep::HeightSecondPoint(first) => Some(first.distance(cursor)),
|
|
ConeStep::AxisEndpoint => self.cone_frame.map(|frame| frame.origin.distance(cursor)),
|
|
_ => None,
|
|
};
|
|
}
|
|
if self.shape == Shape::Pyramid {
|
|
return match self.pyramid_step {
|
|
PyramidStep::BaseRadius | PyramidStep::TopRadius => {
|
|
let frame = self.pyramid_frame?;
|
|
let delta = cursor - frame.origin;
|
|
Some((delta - frame.z * delta.dot(frame.z)).length())
|
|
}
|
|
PyramidStep::Height | PyramidStep::HeightAfterTopRadius => {
|
|
self.pyramid_height_at(cursor).map(f64::abs)
|
|
}
|
|
PyramidStep::HeightSecondPoint => {
|
|
Some(self.pyramid_height_first?.distance(cursor))
|
|
}
|
|
PyramidStep::AxisEndpoint => {
|
|
self.pyramid_frame.map(|frame| frame.origin.distance(cursor))
|
|
}
|
|
_ => None,
|
|
};
|
|
}
|
|
if self.shape.rectangular() {
|
|
let local = self.plane.to_local(cursor);
|
|
if !local.is_finite() {
|
|
return None;
|
|
}
|
|
return match self.box_step {
|
|
BoxStep::CubeSize | BoxStep::Length => {
|
|
let delta = local - self.pts[0];
|
|
Some(if self.shape == Shape::Wedge {
|
|
delta.x.hypot(delta.y)
|
|
} else {
|
|
delta.length()
|
|
})
|
|
}
|
|
BoxStep::Width => {
|
|
let normal = DVec3::new(-self.box_angle.sin(), self.box_angle.cos(), 0.0);
|
|
Some((local - self.pts[0]).dot(normal).abs())
|
|
}
|
|
BoxStep::Height => Some(self.cursor_height(cursor)),
|
|
_ => None,
|
|
};
|
|
}
|
|
self.height_step.then(|| self.cursor_height(cursor))
|
|
}
|
|
}
|
|
|
|
// ── Footprint preview ───────────────────────────────────────────────────────
|
|
|
|
fn footprint_wire(shape: Shape, pts: &[DVec3]) -> WireModel {
|
|
let mut points: Vec<[f32; 3]> = Vec::new();
|
|
if shape == Shape::Pyramid {
|
|
let center = pts[0];
|
|
let radius = (pts[1] - center).length();
|
|
let corners: [DVec3; 4] = std::array::from_fn(|index| {
|
|
let angle = index as f64 * std::f64::consts::FRAC_PI_2;
|
|
center + DVec3::new(radius * angle.cos(), radius * angle.sin(), 0.0)
|
|
});
|
|
push_loop(&mut points, &corners);
|
|
} else if shape.radial() {
|
|
let c = pts[0];
|
|
let r = (pts[1] - c).length();
|
|
circle_points(&mut points, c, r);
|
|
if shape == Shape::Torus && pts.len() >= 3 {
|
|
let inner = (pts[2] - c).length();
|
|
points.push([f32::NAN; 3]);
|
|
circle_points(&mut points, c, inner);
|
|
}
|
|
} else {
|
|
let (a, b) = (pts[0], pts[1]);
|
|
points.extend_from_slice(&[
|
|
[a.x as f32, a.y as f32, a.z as f32],
|
|
[b.x as f32, a.y as f32, a.z as f32],
|
|
[b.x as f32, b.y as f32, a.z as f32],
|
|
[a.x as f32, b.y as f32, a.z as f32],
|
|
[a.x as f32, a.y as f32, a.z as f32],
|
|
]);
|
|
}
|
|
wire("primitive_preview", points)
|
|
}
|
|
|
|
fn height_wire(shape: Shape, pts: &[DVec3], height: f64) -> WireModel {
|
|
let mut points = Vec::new();
|
|
match shape {
|
|
Shape::Box => {
|
|
let (a, b) = (pts[0], pts[1]);
|
|
let base = [
|
|
DVec3::new(a.x, a.y, a.z),
|
|
DVec3::new(b.x, a.y, a.z),
|
|
DVec3::new(b.x, b.y, a.z),
|
|
DVec3::new(a.x, b.y, a.z),
|
|
];
|
|
let top = base.map(|point| point + DVec3::Z * height);
|
|
push_loop(&mut points, &base);
|
|
push_loop(&mut points, &top);
|
|
for i in 0..4 {
|
|
push_segment(&mut points, base[i], top[i]);
|
|
}
|
|
}
|
|
Shape::Wedge => {
|
|
let (a, b) = (pts[0], pts[1]);
|
|
let (x0, x1) = (a.x.min(b.x), a.x.max(b.x));
|
|
let (y0, y1) = (a.y.min(b.y), a.y.max(b.y));
|
|
let low = [
|
|
DVec3::new(x0, y0, a.z),
|
|
DVec3::new(x1, y0, a.z),
|
|
DVec3::new(x0, y0, a.z + height),
|
|
];
|
|
let high = low.map(|point| DVec3::new(point.x, y1, point.z));
|
|
push_loop(&mut points, &low);
|
|
push_loop(&mut points, &high);
|
|
for i in 0..3 {
|
|
push_segment(&mut points, low[i], high[i]);
|
|
}
|
|
}
|
|
Shape::Cylinder | Shape::Cone | Shape::Pyramid => {
|
|
let center = pts[0];
|
|
let radius = (pts[1] - center).length();
|
|
if shape == Shape::Pyramid {
|
|
let base: [DVec3; 4] = std::array::from_fn(|index| {
|
|
let angle = index as f64 * std::f64::consts::FRAC_PI_2;
|
|
center + DVec3::new(radius * angle.cos(), radius * angle.sin(), 0.0)
|
|
});
|
|
push_loop(&mut points, &base);
|
|
let apex = center + DVec3::Z * height;
|
|
for corner in base {
|
|
push_segment(&mut points, corner, apex);
|
|
}
|
|
return wire("primitive_height_preview", points);
|
|
}
|
|
push_circle(&mut points, center, radius);
|
|
if shape == Shape::Cylinder {
|
|
push_circle(&mut points, center + DVec3::Z * height, radius);
|
|
for i in 0..4 {
|
|
let angle = i as f64 * std::f64::consts::FRAC_PI_2;
|
|
let base = center + DVec3::new(angle.cos() * radius, angle.sin() * radius, 0.0);
|
|
push_segment(&mut points, base, base + DVec3::Z * height);
|
|
}
|
|
} else {
|
|
let apex = center + DVec3::Z * height;
|
|
for i in 0..4 {
|
|
let angle = i as f64 * std::f64::consts::FRAC_PI_2;
|
|
let base = center + DVec3::new(angle.cos() * radius, angle.sin() * radius, 0.0);
|
|
push_segment(&mut points, base, apex);
|
|
}
|
|
}
|
|
}
|
|
Shape::Sphere | Shape::Torus => {}
|
|
}
|
|
wire("primitive_height_preview", points)
|
|
}
|
|
|
|
fn push_break(points: &mut Vec<[f32; 3]>) {
|
|
if !points.is_empty() {
|
|
points.push([f32::NAN; 3]);
|
|
}
|
|
}
|
|
|
|
fn push_loop<const N: usize>(points: &mut Vec<[f32; 3]>, path: &[DVec3; N]) {
|
|
push_break(points);
|
|
points.extend(path.iter().chain(path.first()).map(|point| point.as_vec3().to_array()));
|
|
}
|
|
|
|
fn push_path_loop(points: &mut Vec<[f32; 3]>, path: &[DVec3]) {
|
|
if path.is_empty() {
|
|
return;
|
|
}
|
|
push_break(points);
|
|
points.extend(path.iter().chain(path.first()).map(|point| point.as_vec3().to_array()));
|
|
}
|
|
|
|
fn push_segment(points: &mut Vec<[f32; 3]>, a: DVec3, b: DVec3) {
|
|
push_break(points);
|
|
points.extend([a.as_vec3().to_array(), b.as_vec3().to_array()]);
|
|
}
|
|
|
|
fn push_circle(points: &mut Vec<[f32; 3]>, center: DVec3, radius: f64) {
|
|
push_break(points);
|
|
circle_points(points, center, radius);
|
|
}
|
|
|
|
fn push_ellipse_world(
|
|
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 circle_points(out: &mut Vec<[f32; 3]>, c: DVec3, r: f64) {
|
|
const SEG: usize = 48;
|
|
for i in 0..=SEG {
|
|
let t = i as f64 / SEG as f64 * std::f64::consts::TAU;
|
|
out.push([
|
|
(c.x + r * t.cos()) as f32,
|
|
(c.y + r * t.sin()) as f32,
|
|
c.z as f32,
|
|
]);
|
|
}
|
|
}
|
|
|
|
// ── Autocomplete registry ─────────────────────────────────
|
|
inventory::submit!(crate::command::CommandRegistration {
|
|
names: &["BOX", "WEDGE", "CYLINDER", "CONE", "SPHERE", "PYRAMID", "PYR", "TORUS"]
|
|
});
|
|
|
|
fn wire(name: &str, points: Vec<[f32; 3]>) -> WireModel {
|
|
WireModel {
|
|
point_marker: None,
|
|
taper_widths: Vec::new(),
|
|
pattern_stations: Vec::new(),
|
|
world_width: 0.0,
|
|
depth_override: None,
|
|
display_visible: true,
|
|
plot_visible: true,
|
|
fill_is_3d: false,
|
|
fill_is_2d_solid: false,
|
|
render_instance: None,
|
|
pick_tris: Vec::new(),
|
|
pick_tris_low: Vec::new(),
|
|
dash_from_start: false,
|
|
dash_align_end: None,
|
|
text_verts: Vec::new(),
|
|
name: name.into(),
|
|
points,
|
|
points_low: Vec::new(),
|
|
color: WireModel::CYAN,
|
|
selected: false,
|
|
pattern_length: 0.0,
|
|
pattern: [0.0; 8],
|
|
line_weight_px: 1.0,
|
|
snap_pts: vec![],
|
|
tangent_geoms: vec![],
|
|
aci: 0,
|
|
key_vertices: vec![],
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
|
plinegen: true,
|
|
fill_tris: vec![],
|
|
fill_tris_low: Vec::new(),
|
|
}
|
|
}
|