diff --git a/src/modules/model/primitive_cmd.rs b/src/modules/model/primitive_cmd.rs index b7dc9d25e..fa1550270 100644 --- a/src/modules/model/primitive_cmd.rs +++ b/src/modules/model/primitive_cmd.rs @@ -6,7 +6,7 @@ use acadrust::EntityType; use cadkernel::brep::Body; use glam::DVec3; -use crate::command::{CadCommand, CmdResult, WorkingPlane}; +use crate::command::{CadCommand, CmdOption, CmdResult, WorkingPlane}; use crate::scene::model::solid_model; use crate::scene::model::wire_model::WireModel; use crate::t; @@ -23,6 +23,19 @@ pub enum Shape { Torus, } +#[derive(Clone, Copy, PartialEq, Eq)] +enum BoxStep { + FirstCorner, + CenterPoint, + OppositeCorner, + CubeSize, + Length, + Width, + Height, + HeightFirstPoint, + HeightSecondPoint, +} + impl Shape { fn from_id(id: &str) -> Option { Some(match id { @@ -64,6 +77,14 @@ pub struct PrimitiveCommand { pts: Vec, /// True once the footprint is set and we are collecting the height. height_step: bool, + box_step: BoxStep, + box_centered: bool, + box_origin: Option, + box_length: Option, + box_width: Option, + box_angle: f64, + box_width_sign: f64, + box_height_anchor: Option, plane: WorkingPlane, } @@ -73,6 +94,14 @@ impl PrimitiveCommand { 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, plane: WorkingPlane::default(), } } @@ -98,8 +127,12 @@ impl PrimitiveCommand { } fn cursor_height(&self, point: DVec3) -> f64 { - (self.plane.to_local(point).z - self.pts[0].z) - .max(1e-6) + let height = self.plane.to_local(point).z - self.pts[0].z; + if self.shape == Shape::Box { + height + } else { + height.max(1e-6) + } } fn place_preview(&self, mut preview: WireModel) -> WireModel { @@ -125,46 +158,192 @@ impl PrimitiveCommand { .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 length < 1e-6 || width < 1e-6 || height_abs < 1e-6 { + return None; + } + if height < 0.0 { + origin.z += height; + } + 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) { + let first = self.pts[0]; + if self.box_centered { + let delta = point - first; + 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; + } + + fn set_box_cube(&mut self, size: f64) -> Option { + let size = size.abs(); + if size < 1e-6 { + return None; + } + let origin = if self.box_centered { + self.pts[0] - DVec3::new(size * 0.5, size * 0.5, 0.0) + } else { + self.pts[0] + }; + self.box_origin = Some(origin); + self.box_length = Some(size); + self.box_width = Some(size); + Some(self.commit(size)) + } + + fn set_box_length(&mut self, length: f64, angle: f64) -> bool { + let length = length.abs(); + if 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 < 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 box_preview(&self, height: f64) -> Option { + let (origin, x_axis, y_axis, length, width, height) = self.box_spec(height)?; + 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); + let mut points = Vec::new(); + 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))) + } + /// 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 | Shape::Wedge => { + Shape::Box => return self.build_box(height), + Shape::Wedge => { let (a, b) = (self.pts[0], self.pts[1]); let length = (b.x - a.x).abs(); let width = (b.y - a.y).abs(); if length < 1e-6 || width < 1e-6 || height < 1e-6 { return None; } - if self.shape == Shape::Box { - let origin = DVec3::new(a.x.min(b.x), a.y.min(b.y), a.z); - let center = [ - (a.x + b.x) / 2.0, - (a.y + b.y) / 2.0, - a.z + height / 2.0, - ]; - ( - solid_model::box_solid(center, length, width, height), - solid_history::box_op( - self.history_transform(origin), - length, - width, - height, - ), - ) - } else { - let origin = [a.x.min(b.x), a.y.min(b.y), a.z]; - ( - solid_model::wedge_solid(origin, length, width, height), - solid_history::wedge_op( - self.history_transform(DVec3::from_array(origin)), - length, - width, - height, - ), - ) - } + let origin = [a.x.min(b.x), a.y.min(b.y), a.z]; + ( + solid_model::wedge_solid(origin, length, width, height), + solid_history::wedge_op( + self.history_transform(DVec3::from_array(origin)), + length, + width, + height, + ), + ) } Shape::Cylinder | Shape::Cone => { let c = self.pts[0]; @@ -303,6 +482,26 @@ impl CadCommand for PrimitiveCommand { fn prompt(&self) -> String { let n = self.shape.name(); + if self.shape == Shape::Box { + 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 => 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.height_step { return t!("%{n} Specify height :", n = n).into_owned(); } @@ -321,7 +520,79 @@ impl CadCommand for PrimitiveCommand { } } + fn options(&self) -> Vec { + if self.shape != Shape::Box { + 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::Box { + let local = self.plane.to_local(pt); + return match self.box_step { + BoxStep::FirstCorner | BoxStep::CenterPoint => { + self.pts = vec![local]; + self.box_step = BoxStep::OppositeCorner; + CmdResult::NeedPoint + } + BoxStep::OppositeCorner => { + self.set_box_corner_footprint(local); + CmdResult::NeedPoint + } + BoxStep::CubeSize => self + .set_box_cube((local - self.pts[0]).length()) + .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)); } @@ -339,6 +610,9 @@ impl CadCommand for PrimitiveCommand { } fn on_enter(&mut self) -> CmdResult { + if self.shape == Shape::Box { + return CmdResult::Cancel; + } if self.height_step { let h = self.default_height(); return self.commit(h); @@ -351,10 +625,61 @@ impl CadCommand for PrimitiveCommand { } fn wants_text_input(&self) -> bool { - self.height_step + self.shape == Shape::Box || self.height_step + } + + fn point_step_accepts_keywords(&self) -> bool { + self.shape == Shape::Box + && matches!( + self.box_step, + BoxStep::FirstCorner | BoxStep::OppositeCorner | BoxStep::Height + ) } fn on_text_input(&mut self, raw: &str) -> Option { + if self.shape == Shape::Box { + 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 => token + .replace(',', ".") + .parse::() + .ok() + .and_then(|value| self.set_box_cube(value)), + BoxStep::Length => { + let value = token.replace(',', ".").parse::().ok()?; + self.set_box_length(value, 0.0) + .then_some(CmdResult::NeedPoint) + } + BoxStep::Width => { + let value = token.replace(',', ".").parse::().ok()?; + self.set_box_width(value, value) + .then_some(CmdResult::NeedPoint) + } + BoxStep::Height => { + let value = token.replace(',', ".").parse::().ok()?; + (value.abs() >= 1e-6).then(|| self.commit(value)) + } + _ => None, + }; + } if !self.height_step { return None; } @@ -366,6 +691,68 @@ impl CadCommand for PrimitiveCommand { if self.pts.is_empty() { return None; } + if self.shape == Shape::Box { + let local = self.plane.to_local(pt); + 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 size = (local - self.pts[0]).length().max(1e-6); + let mut preview = PrimitiveCommand::new("BOX"); + preview.plane = self.plane; + preview.pts = self.pts.clone(); + preview.box_origin = Some(self.pts[0]); + preview.box_length = Some(size); + preview.box_width = Some(size); + preview.box_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("BOX"); + 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.box_preview(1e-6) + } + BoxStep::Height => self.box_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, @@ -381,7 +768,13 @@ impl CadCommand for PrimitiveCommand { fn dyn_spec(&self) -> Option { use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec}; - self.height_step.then(|| DynSpec { + (self.height_step && self.shape != Shape::Box + || self.shape == Shape::Box + && matches!( + self.box_step, + BoxStep::CubeSize | BoxStep::Length | BoxStep::Width | BoxStep::Height + )) + .then(|| DynSpec { anchor: DynAnchor::Point(self.plane.to_world(self.pts[0])), fields: vec![DynFieldSpec::new(DynRole::Height)], guide: DynGuide::None, @@ -390,6 +783,18 @@ impl CadCommand for PrimitiveCommand { } fn dyn_live_value(&self, cursor: DVec3) -> Option { + if self.shape == Shape::Box { + let local = self.plane.to_local(cursor); + return match self.box_step { + BoxStep::CubeSize | BoxStep::Length => Some((local - self.pts[0]).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)) } }