From 22d91f8946f4b53acb15291c7b35991f48cb69c9 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:50:49 +0300 Subject: [PATCH] Add 3D mesh creation workflow --- src/app/commands/draw.rs | 7 ++ src/modules/draw/draw/mesh3d.rs | 185 ++++++++++++++++++++++++++++++++ src/modules/draw/draw/mod.rs | 1 + 3 files changed, 193 insertions(+) create mode 100644 src/modules/draw/draw/mesh3d.rs diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index d9edbc862..9c888b04a 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -454,6 +454,13 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } + "3DMESH" => { + use crate::modules::draw::draw::mesh3d::Mesh3dCommand; + let new_cmd = Mesh3dCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + // 2D filled solid. Reached via SO / SOLID2D — the bare SOLID verb is // currently the shaded-display toggle (token collision tracked). "SOLID" | "SOLID2D" => { diff --git a/src/modules/draw/draw/mesh3d.rs b/src/modules/draw/draw/mesh3d.rs new file mode 100644 index 000000000..066f0b6e8 --- /dev/null +++ b/src/modules/draw/draw/mesh3d.rs @@ -0,0 +1,185 @@ +use acadrust::entities::polygon_mesh::{PolygonMesh, PolygonMeshVertex}; +use acadrust::types::Vector3; +use acadrust::EntityType; +use glam::DVec3; + +use crate::command::{CadCommand, CmdResult, DynField}; +use crate::scene::model::wire_model::WireModel; +use crate::t; + +const MIN_SIZE: usize = 2; +const MAX_SIZE: usize = 256; + +#[derive(Clone, Copy, PartialEq, Eq)] +enum Step { + MSize, + NSize, + Vertices, +} + +/// Creates a legacy M by N polygon mesh from row-major 3D vertex input. +pub struct Mesh3dCommand { + step: Step, + m: usize, + n: usize, + points: Vec, +} + +impl Mesh3dCommand { + pub fn new() -> Self { + Self { + step: Step::MSize, + m: 0, + n: 0, + points: Vec::new(), + } + } + + fn parse_size(text: &str) -> Option { + text.trim() + .parse::() + .ok() + .filter(|value| (MIN_SIZE..=MAX_SIZE).contains(value)) + } + + fn total_vertices(&self) -> usize { + self.m.saturating_mul(self.n) + } + + fn current_indices(&self) -> (usize, usize) { + let index = self.points.len().min(self.total_vertices().saturating_sub(1)); + (index / self.n.max(1), index % self.n.max(1)) + } + + fn build(&self) -> Option { + if self.m < MIN_SIZE + || self.n < MIN_SIZE + || self.points.len() != self.total_vertices() + { + return None; + } + let mut mesh = PolygonMesh::new(); + mesh.m_vertex_count = self.m as i16; + mesh.n_vertex_count = self.n as i16; + mesh.vertices = self + .points + .iter() + .map(|point| PolygonMeshVertex::at(Vector3::new(point.x, point.y, point.z))) + .collect(); + Some(EntityType::PolygonMesh(mesh)) + } + + fn preview(&self, cursor: DVec3) -> Option { + if self.step != Step::Vertices { + return None; + } + let mut points = self.points.clone(); + if points.len() < self.total_vertices() { + points.push(cursor); + } + let mut lines = Vec::new(); + let mut add_segment = |a: DVec3, b: DVec3| { + if !lines.is_empty() { + lines.push([f64::NAN; 3]); + } + lines.push([a.x, a.y, a.z]); + lines.push([b.x, b.y, b.z]); + }; + for index in 0..points.len() { + let row = index / self.n; + let column = index % self.n; + if column > 0 { + add_segment(points[index - 1], points[index]); + } + if row > 0 { + add_segment(points[index - self.n], points[index]); + } + } + (!lines.is_empty()).then(|| { + WireModel::solid_f64("mesh3d_preview".to_string(), lines, WireModel::CYAN, false) + }) + } +} + +impl CadCommand for Mesh3dCommand { + fn name(&self) -> &'static str { + "3DMESH" + } + + fn prompt(&self) -> String { + match self.step { + Step::MSize => t!("3DMESH Enter size of mesh in M direction (2-256):").into_owned(), + Step::NSize => t!("3DMESH Enter size of mesh in N direction (2-256):").into_owned(), + Step::Vertices => { + let (m, n) = self.current_indices(); + t!( + "3DMESH Specify location for vertex (%{m}, %{n}):", + m = m, + n = n + ) + .into_owned() + } + } + } + + fn wants_text_input(&self) -> bool { + self.step != Step::Vertices + } + + fn dyn_field(&self) -> DynField { + if self.step == Step::Vertices { + DynField::Point + } else { + DynField::Scalar + } + } + + fn on_text_input(&mut self, text: &str) -> Option { + match self.step { + Step::MSize => { + if let Some(value) = Self::parse_size(text) { + self.m = value; + self.step = Step::NSize; + } + Some(CmdResult::NeedPoint) + } + Step::NSize => { + if let Some(value) = Self::parse_size(text) { + self.n = value; + self.points.reserve(self.total_vertices()); + self.step = Step::Vertices; + } + Some(CmdResult::NeedPoint) + } + Step::Vertices => None, + } + } + + fn on_point(&mut self, point: DVec3) -> CmdResult { + if self.step != Step::Vertices { + return CmdResult::NeedPoint; + } + self.points.push(point); + if self.points.len() == self.total_vertices() { + self.build() + .map(CmdResult::CommitAndExit) + .unwrap_or(CmdResult::Cancel) + } else { + CmdResult::NeedPoint + } + } + + fn on_enter(&mut self) -> CmdResult { + CmdResult::NeedPoint + } + + fn on_escape(&mut self) -> CmdResult { + CmdResult::Cancel + } + + fn on_mouse_move(&mut self, point: DVec3) -> Option { + self.preview(point) + } +} + +inventory::submit!(crate::command::CommandRegistration { names: &["3DMESH"] }); diff --git a/src/modules/draw/draw/mod.rs b/src/modules/draw/draw/mod.rs index f475484c2..0b5f9709e 100644 --- a/src/modules/draw/draw/mod.rs +++ b/src/modules/draw/draw/mod.rs @@ -10,6 +10,7 @@ pub mod hatchedit; pub mod helix; pub mod line; pub mod mline; +pub mod mesh3d; pub mod point; pub mod poly3d; pub mod polyline;