Complete revolve workflow and properties

This commit is contained in:
ramox81 committed 2026-09-03 13:40:23 +03:00
1 parent e6429d5712
commit 2e112f9aeb
9 files changed
+913 -94

No files matched your search

Generated
+1 -1
View File
@@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cadkernel"
version = "0.1.0"
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=ac2e69070278bf56ae44829f31a69f6a1006ac41#ac2e69070278bf56ae44829f31a69f6a1006ac41"
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=284df21acdbc778e2f0fde7cbf47a35dae328920#284df21acdbc778e2f0fde7cbf47a35dae328920"
dependencies = [
"acadrust",
"cavalier_contours",
+1 -1
View File
@@ -28,7 +28,7 @@ rfd = "0.17"
clap = { version = "4", features = ["derive"] }
env_logger = "0.11"
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "393d6c0857447f9ca678bed97a612707f0a842c6", features = ["serde"] }
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "ac2e69070278bf56ae44829f31a69f6a1006ac41", features = ["acis", "offset"] }
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "284df21acdbc778e2f0fde7cbf47a35dae328920", features = ["acis", "offset"] }
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
flate2 = "1"
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
+104 -37
View File
@@ -3822,66 +3822,133 @@ impl OpenCADStudio {
}
// ── REVOLVE ────────────────────────────────────────────────────
CmdResult::RevolveEntity {
handle,
CmdResult::RevolveEntities {
handles,
axis_start,
axis_end,
angle_deg,
angle,
start_angle,
mode,
color: _,
} => {
if self.reject_locked_edit(i, handle) {
if handles.is_empty()
|| handles
.iter()
.any(|handle| self.reject_locked_edit(i, *handle))
{
self.tabs[i].active_cmd = None;
return Task::none();
}
use crate::modules::insert::solid3d_cmds::empty_solid3d;
let delete_sources = self.tabs[i].scene.document.header.delete_objects;
use crate::command::ExtrudeMode;
use crate::modules::insert::solid3d_cmds::{
empty_revolved_surface, empty_solid3d,
};
use crate::scene::model::sweep_model;
let entity_opt = self.tabs[i].scene.document.get_entity(handle).cloned();
if let Some(entity) = entity_opt {
let from = [
axis_start.x as f64,
axis_start.y as f64,
axis_start.z as f64,
];
let to = [axis_end.x as f64, axis_end.y as f64, axis_end.z as f64];
let angle = (angle_deg as f64).to_radians();
let result = sweep_model::revolve_history(&entity, from, to, angle)
let from = axis_start.to_array();
let to = axis_end.to_array();
let pending = self.begin_undo(i, "REVOLVE", handles.len(), true);
let mut created_handles = Vec::new();
let mut consumed = Vec::new();
let mut failed = 0usize;
for handle in &handles {
let Some(entity) = self.tabs[i].scene.document.get_entity(*handle).cloned()
else {
failed += 1;
continue;
};
let Some((_, closed)) = sweep_model::extrusion_profile_of(&entity) else {
failed += 1;
continue;
};
let creates_surface = mode == ExtrudeMode::Surface || !closed;
let created = if creates_surface {
let Some(body) =
sweep_model::revolved_surface(&entity, from, to, angle, start_angle)
else {
failed += 1;
continue;
};
self.add_surface_model(
empty_revolved_surface(
&entity,
axis_start,
axis_end,
angle,
start_angle,
),
body,
)
} else {
let result = sweep_model::revolve_history(
&entity,
from,
to,
angle,
start_angle,
)
.and_then(|history| {
cadkernel::acis::rebuild_body(&history)
.ok()
.map(|solid| (solid, history))
})
.or_else(|| {
sweep_model::revolved(&entity, from, to, angle).map(|solid| {
let history =
crate::scene::model::solid_history::brep_op(&solid);
(solid, history)
})
sweep_model::revolved(&entity, from, to, angle, start_angle).map(
|solid| {
let history =
crate::scene::model::solid_history::brep_op(&solid);
(solid, history)
},
)
});
if let Some((solid, history)) = result {
let pending = self.begin_undo(i, "REVOLVE", 1, true);
let created = self.add_solid_model(empty_solid3d(), solid, history);
if !created.is_null() {
self.tabs[i].dirty = true;
self.command_line.push_output(
crate::tf!("REVOLVE: solid created ({:.0}°).", angle_deg).as_ref(),
);
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
}
let Some((solid, history)) = result else {
failed += 1;
continue;
};
self.add_solid_model(empty_solid3d(), solid, history)
};
if created.is_null() {
failed += 1;
} else {
self.command_line
.push_error(crate::t!("REVOLVE: could not revolve profile.").as_ref());
created_handles.push(created);
if delete_sources {
consumed.push(*handle);
}
}
}
if delete_sources && !created_handles.is_empty() {
consumed.sort_unstable_by_key(|handle| handle.value());
consumed.dedup();
self.tabs[i].scene.erase_entities(&consumed);
}
if !created_handles.is_empty() {
self.tabs[i].scene.deselect_all();
for handle in &created_handles {
self.tabs[i].scene.select_entity(*handle, true);
}
self.tabs[i].dirty = true;
self.command_line.push_output(
crate::tf!(
"REVOLVE: created %{created} object(s); %{failed} source(s) could not be revolved.",
created = created_handles.len(),
failed = failed
)
.as_ref(),
);
} else {
self.command_line
.push_error(crate::t!("REVOLVE: entity not found.").as_ref());
self.command_line.push_error(
crate::t!("REVOLVE: no selected object could be revolved with the requested options.")
.as_ref(),
);
}
if let Some(pending) = pending {
self.commit_undo_delta(i, pending);
}
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.restore_pre_cmd_tangent();
self.refresh_properties();
}
// ── SWEEP ─────────────────────────────────────────────────────
CmdResult::SweepEntity {
+10 -1
View File
@@ -794,8 +794,17 @@ impl OpenCADStudio {
// ── REVOLVE ────────────────────────────────────────────────────
"REVOLVE" => {
use crate::modules::insert::solid3d_cmds::RevolveCommand;
let selected: Vec<_> = self.tabs[i].scene.selected_entities().into_iter().collect();
let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
let cmd = RevolveCommand::new(color);
let mut cmd = RevolveCommand::new(color);
if !selected.is_empty() {
cmd.set_preselection(
selected
.iter()
.map(|(handle, entity)| (*handle, (*entity).clone()))
.collect(),
);
}
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
}
+10 -1
View File
@@ -660,7 +660,16 @@ impl OpenCADStudio {
// the sign of the cursor's side relative to the reference, so the
// committed direction matches the drag (the box shows magnitude
// only). Commands receive an already-signed string.
let text = self.dyn_sign_angle_text(i, text);
let auto_sign = self.tabs[i]
.active_cmd
.as_ref()
.map(|command| command.dyn_auto_sign_angle())
.unwrap_or(true);
let text = if auto_sign {
self.dyn_sign_angle_text(i, text)
} else {
text
};
let result = self.tabs[i]
.active_cmd
.as_mut()
+12 -4
View File
@@ -1443,12 +1443,14 @@ pub enum CmdResult {
drag: Option<DVec3>,
color: [f32; 4],
},
/// Revolve the profile entity `handle` around the given axis by `angle_deg`.
RevolveEntity {
handle: Handle,
/// Revolve the profile entities around the given axis.
RevolveEntities {
handles: Vec<Handle>,
axis_start: glam::DVec3,
axis_end: glam::DVec3,
angle_deg: f32,
angle: f64,
start_angle: f64,
mode: ExtrudeMode,
color: [f32; 4],
},
/// Sweep the profile entity `profile_handle` along `path_handle`.
@@ -2161,6 +2163,12 @@ pub trait CadCommand: Send {
false
}
/// Whether a bare dynamic angle inherits its sign from the world-XY
/// cursor side. Commands with their own 3D axis semantics opt out.
fn dyn_auto_sign_angle(&self) -> bool {
true
}
/// Explicit per-step dynamic-input description. `Some(spec)` takes full
/// control of the boxes, guide geometry and anchor for this step; `None`
/// (the default) falls back to the legacy `dyn_field()` behaviour so
+481 -37
View File
@@ -6,7 +6,7 @@ use acadrust::{entities::Solid3D, EntityType, Handle};
use glam::DVec3;
use crate::command::{
CadCommand, CmdOption, CmdResult, ExtrudeExtent, ExtrudeMode, SelectionEntity,
CadCommand, CmdOption, CmdResult, ExtrudeExtent, ExtrudeMode, SelectionEntity, WorkingPlane,
};
use crate::scene::WireModel;
use crate::t;
@@ -615,30 +615,221 @@ impl CadCommand for PresspullCommand {
pub struct RevolveCommand {
step: RevolveStep,
target_handle: acadrust::Handle,
handles: Vec<Handle>,
preview_profiles: Vec<(Handle, EntityType)>,
injected_axis: Option<EntityType>,
axis_start: DVec3,
axis_end: DVec3,
working_plane: WorkingPlane,
mode: ExtrudeMode,
angle: f64,
start_angle: f64,
reverse: bool,
color: [f32; 4],
}
#[derive(PartialEq)]
#[derive(Clone, Copy)]
struct RevolveDefaults {
mode: ExtrudeMode,
angle: f64,
start_angle: f64,
}
fn revolve_defaults() -> &'static Mutex<RevolveDefaults> {
static DEFAULTS: OnceLock<Mutex<RevolveDefaults>> = OnceLock::new();
DEFAULTS.get_or_init(|| {
Mutex::new(RevolveDefaults {
mode: ExtrudeMode::Solid,
angle: std::f64::consts::TAU,
start_angle: 0.0,
})
})
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum RevolveStep {
Pick,
Mode,
AxisStart,
AxisEnd,
AxisObject,
Angle,
StartAngle,
Expression,
}
impl RevolveCommand {
pub fn new(color: [f32; 4]) -> Self {
let defaults = *revolve_defaults().lock().unwrap_or_else(|error| error.into_inner());
Self {
step: RevolveStep::Pick,
target_handle: acadrust::Handle::NULL,
handles: Vec::new(),
preview_profiles: Vec::new(),
injected_axis: None,
axis_start: DVec3::ZERO,
axis_end: DVec3::new(0.0, 0.0, 1.0),
working_plane: WorkingPlane::default(),
mode: defaults.mode,
angle: defaults.angle,
start_angle: defaults.start_angle,
reverse: false,
color,
}
}
pub fn set_preselection(&mut self, profiles: Vec<(Handle, EntityType)>) {
self.handles = profiles.iter().map(|(handle, _)| *handle).collect();
self.preview_profiles = profiles;
if !self.handles.is_empty() {
self.step = RevolveStep::AxisStart;
}
}
fn profile_reference(&self) -> Option<DVec3> {
let axis = (self.axis_end - self.axis_start).try_normalize()?;
self.preview_profiles
.iter()
.filter_map(|(_, entity)| {
crate::scene::model::sweep_model::extrusion_profile_of(entity)
})
.flat_map(|(profile, _)| {
profile.pieces.into_iter().flat_map(move |piece| {
[0.0, 0.5, 1.0].into_iter().map(move |parameter| {
DVec3::from_array(profile.plane.point_at(piece.point_at(parameter)))
})
})
})
.filter(|point| point.is_finite())
.max_by(|first, second| {
let radial_length = |point: DVec3| {
let delta = point - self.axis_start;
(delta - axis * delta.dot(axis)).length_squared()
};
radial_length(*first).total_cmp(&radial_length(*second))
})
}
fn angle_anchor(&self) -> Option<DVec3> {
let axis = (self.axis_end - self.axis_start).try_normalize()?;
let reference = self.profile_reference()?;
Some(self.axis_start + axis * (reference - self.axis_start).dot(axis))
}
fn cursor_angle(&self, cursor: DVec3, full_at_zero: bool) -> Option<f64> {
let axis = (self.axis_end - self.axis_start).try_normalize()?;
let anchor = self.angle_anchor()?;
let reference = (self.profile_reference()? - anchor).try_normalize()?;
let radial = (cursor - (self.axis_start
+ axis * (cursor - self.axis_start).dot(axis)))
.try_normalize()?;
let mut angle = axis.dot(reference.cross(radial)).atan2(reference.dot(radial));
if angle < 0.0 {
angle += std::f64::consts::TAU;
}
if full_at_zero && angle.abs() <= 1e-9 {
angle = std::f64::consts::TAU;
}
angle.is_finite().then_some(angle)
}
fn signed_angle(&self, angle: f64) -> f64 {
angle * if self.reverse { -1.0 } else { 1.0 }
}
fn preview_wires(&self, cursor: DVec3) -> Vec<WireModel> {
let (angle, start_angle) = match self.step {
RevolveStep::Angle => (
self.cursor_angle(cursor, true)
.map(|angle| self.signed_angle(angle)),
Some(self.start_angle),
),
RevolveStep::StartAngle => (
Some(self.signed_angle(self.angle)),
self.cursor_angle(cursor, false),
),
_ => return Vec::new(),
};
let (Some(angle), Some(start_angle)) = (angle, start_angle) else {
return Vec::new();
};
let from = self.axis_start.to_array();
let to = self.axis_end.to_array();
self.preview_profiles
.iter()
.flat_map(|(_, entity)| {
let Some((_, closed)) =
crate::scene::model::sweep_model::extrusion_profile_of(entity)
else {
return Vec::new();
};
let creates_surface = self.mode == ExtrudeMode::Surface || !closed;
let body = if creates_surface {
crate::scene::model::sweep_model::revolved_surface(
entity,
from,
to,
angle,
start_angle,
)
} else {
crate::scene::model::sweep_model::revolved(
entity,
from,
to,
angle,
start_angle,
)
};
body.map(|body| preview_body_wires(&body, self.color))
.unwrap_or_default()
})
.collect()
}
fn set_axis(&mut self, direction: DVec3) -> CmdResult {
let Some(direction) = direction.try_normalize() else {
return CmdResult::NeedPoint;
};
self.axis_start = self.working_plane.origin;
self.axis_end = self.axis_start + direction;
self.step = RevolveStep::Angle;
CmdResult::NeedPoint
}
fn finish(&self, angle: f64) -> CmdResult {
let angle = self.signed_angle(angle);
if angle.is_finite() && angle.abs() > 1e-9 && angle.abs() <= std::f64::consts::TAU + 1e-9 {
let mut defaults = revolve_defaults().lock().unwrap_or_else(|error| error.into_inner());
defaults.mode = self.mode;
defaults.angle = angle.abs();
defaults.start_angle = self.start_angle;
}
CmdResult::RevolveEntities {
handles: self.handles.clone(),
axis_start: self.axis_start,
axis_end: self.axis_end,
angle,
start_angle: self.start_angle,
mode: self.mode,
color: self.color,
}
}
}
fn revolve_axis(entity: &EntityType) -> Option<(DVec3, DVec3)> {
let point = |value: &acadrust::types::Vector3| DVec3::new(value.x, value.y, value.z);
let (start, direction) = match entity {
EntityType::Line(line) => (point(&line.start), point(&line.end) - point(&line.start)),
EntityType::Ray(ray) => (point(&ray.base_point), point(&ray.direction)),
EntityType::XLine(line) => (point(&line.base_point), point(&line.direction)),
EntityType::Polyline3D(line) if line.vertices.len() == 2 => {
let start = point(&line.vertices[0].position);
(start, point(&line.vertices[1].position) - start)
}
_ => return None,
};
let direction = direction.try_normalize()?;
Some((start, start + direction))
}
impl CadCommand for RevolveCommand {
@@ -647,22 +838,69 @@ impl CadCommand for RevolveCommand {
}
fn prompt(&self) -> String {
match self.step {
RevolveStep::Pick => t!("REVOLVE Select profile:").into_owned(),
RevolveStep::AxisStart => t!("REVOLVE Axis start point:").into_owned(),
RevolveStep::Pick => t!("REVOLVE Select objects to revolve or [Mode] (Enter to finish):").into_owned(),
RevolveStep::Mode => format!(
"{} <{}>:",
t!("REVOLVE Creation mode [Solid/Surface]"),
if self.mode == ExtrudeMode::Solid { "Solid" } else { "Surface" }
),
RevolveStep::AxisStart => t!("REVOLVE Specify axis start point or [Object/X/Y/Z]:").into_owned(),
RevolveStep::AxisEnd => t!("REVOLVE Axis end point:").into_owned(),
RevolveStep::Angle => t!("REVOLVE Angle of revolution <360>:").into_owned(),
RevolveStep::AxisObject => t!("REVOLVE Select line, ray, or construction line for axis:").into_owned(),
RevolveStep::Angle => format!(
"{} <{}>:",
t!("REVOLVE Specify angle of revolution or [Start angle/Reverse/Expression]"),
crate::entities::common::format_angle(self.angle)
),
RevolveStep::StartAngle => format!(
"{} <{}>:",
t!("REVOLVE Specify start angle"),
crate::entities::common::format_angle(self.start_angle)
),
RevolveStep::Expression => t!("REVOLVE Enter angle expression:").into_owned(),
}
}
fn options(&self) -> Vec<CmdOption> {
match self.step {
RevolveStep::Pick => vec![CmdOption::new("Mode", "MODE"), CmdOption::enter("Done")],
RevolveStep::Mode => vec![
CmdOption::new("Solid", "SOLID"),
CmdOption::new("Surface", "SURFACE"),
],
RevolveStep::AxisStart => vec![
CmdOption::new("Object", "OBJECT"),
CmdOption::new("X", "X"),
CmdOption::new("Y", "Y"),
CmdOption::new("Z", "Z"),
],
RevolveStep::Angle => vec![
CmdOption::new("Start angle", "START"),
CmdOption::new("Reverse", "REVERSE"),
CmdOption::new("Expression", "EXPRESSION"),
],
_ => Vec::new(),
}
}
fn needs_entity_pick(&self) -> bool {
self.step == RevolveStep::Pick
self.step == RevolveStep::AxisObject
}
fn on_entity_pick(&mut self, handle: acadrust::Handle, _pt: DVec3) -> CmdResult {
if handle.is_null() {
return CmdResult::NeedPoint;
}
self.target_handle = handle;
self.step = RevolveStep::AxisStart;
CmdResult::NeedPoint
match self.step {
RevolveStep::AxisObject => {
let Some((start, end)) = self.injected_axis.take().as_ref().and_then(revolve_axis)
else {
return CmdResult::NeedPoint;
};
self.axis_start = start;
self.axis_end = end;
self.step = RevolveStep::Angle;
CmdResult::NeedPoint
}
_ => CmdResult::NeedPoint,
}
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.step {
@@ -672,46 +910,213 @@ impl CadCommand for RevolveCommand {
CmdResult::NeedPoint
}
RevolveStep::AxisEnd => {
self.axis_end = pt;
if (pt - self.axis_start).length_squared() <= 1e-12 {
CmdResult::NeedPoint
} else {
self.axis_end = pt;
self.step = RevolveStep::Angle;
CmdResult::NeedPoint
}
}
RevolveStep::Angle => self
.cursor_angle(pt, true)
.map(|angle| self.finish(angle))
.unwrap_or(CmdResult::NeedPoint),
RevolveStep::StartAngle => {
let Some(angle) = self.cursor_angle(pt, false) else {
return CmdResult::NeedPoint;
};
self.start_angle = angle;
self.step = RevolveStep::Angle;
CmdResult::NeedPoint
}
RevolveStep::Angle => self.make_revolve(360.0),
_ => CmdResult::NeedPoint,
}
}
fn wants_text_input(&self) -> bool {
self.step == RevolveStep::Angle
matches!(
self.step,
RevolveStep::Pick
| RevolveStep::Mode
| RevolveStep::AxisStart
| RevolveStep::Angle
| RevolveStep::StartAngle
| RevolveStep::Expression
)
}
fn point_step_accepts_keywords(&self) -> bool {
matches!(self.step, RevolveStep::AxisStart | RevolveStep::Angle)
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let angle = if text.trim().is_empty() {
360.0f32
} else {
text.trim()
.parse::<f32>()
.ok()
.filter(|&a| a.abs() > 1e-3)?
};
Some(self.make_revolve(angle))
let value = text.trim();
if value.is_empty() {
return None;
}
let upper = value.to_ascii_uppercase();
match self.step {
RevolveStep::Pick if "MODE".starts_with(&upper) => {
self.step = RevolveStep::Mode;
Some(CmdResult::NeedPoint)
}
RevolveStep::Mode => {
if "SOLID".starts_with(&upper) {
self.mode = ExtrudeMode::Solid;
} else if "SURFACE".starts_with(&upper) {
self.mode = ExtrudeMode::Surface;
} else {
return Some(CmdResult::NeedPoint);
}
revolve_defaults()
.lock()
.unwrap_or_else(|error| error.into_inner())
.mode = self.mode;
self.step = RevolveStep::Pick;
Some(CmdResult::NeedPoint)
}
RevolveStep::AxisStart => {
if "OBJECT".starts_with(&upper) {
self.step = RevolveStep::AxisObject;
Some(CmdResult::NeedPoint)
} else if upper == "X" {
Some(self.set_axis(self.working_plane.x))
} else if upper == "Y" {
Some(self.set_axis(self.working_plane.y))
} else if upper == "Z" {
Some(self.set_axis(self.working_plane.z))
} else {
None
}
}
RevolveStep::Angle => {
if "START ANGLE".starts_with(&upper) || "START".starts_with(&upper) {
self.step = RevolveStep::StartAngle;
return Some(CmdResult::NeedPoint);
}
if "REVERSE".starts_with(&upper) {
self.reverse = !self.reverse;
return Some(CmdResult::NeedPoint);
}
if "EXPRESSION".starts_with(&upper) {
self.step = RevolveStep::Expression;
return Some(CmdResult::NeedPoint);
}
crate::entities::common::parse_angle(value)
.filter(|angle| {
angle.is_finite()
&& angle.abs() > 1e-9
&& angle.abs() <= std::f64::consts::TAU + 1e-9
})
.map(|angle| self.finish(angle))
}
RevolveStep::StartAngle => {
let angle = crate::entities::common::parse_angle(value)?;
if !angle.is_finite() {
return Some(CmdResult::NeedPoint);
}
self.start_angle = angle;
self.step = RevolveStep::Angle;
Some(CmdResult::NeedPoint)
}
RevolveStep::Expression => {
let angle = crate::app::expr_eval::eval_number(value)
.and_then(|value| crate::entities::common::parse_angle(&value.to_string()))?;
if !angle.is_finite()
|| angle.abs() <= 1e-9
|| angle.abs() > std::f64::consts::TAU + 1e-9
{
return Some(CmdResult::NeedPoint);
}
Some(self.finish(angle))
}
_ => None,
}
}
fn on_enter(&mut self) -> CmdResult {
if self.step == RevolveStep::Angle {
self.make_revolve(360.0)
} else {
CmdResult::Cancel
match self.step {
RevolveStep::Pick if !self.handles.is_empty() => {
self.step = RevolveStep::AxisStart;
CmdResult::NeedPoint
}
RevolveStep::Mode => {
self.step = RevolveStep::Pick;
CmdResult::NeedPoint
}
RevolveStep::Angle => self.finish(self.angle),
RevolveStep::StartAngle => {
self.step = RevolveStep::Angle;
CmdResult::NeedPoint
}
_ => CmdResult::Cancel,
}
}
}
impl RevolveCommand {
fn make_revolve(&self, angle_deg: f32) -> CmdResult {
CmdResult::RevolveEntity {
handle: self.target_handle,
axis_start: self.axis_start,
axis_end: self.axis_end,
angle_deg,
color: self.color,
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.working_plane = plane;
}
fn is_selection_gathering(&self) -> bool {
self.step == RevolveStep::Pick
}
fn selection_forces_add(&self) -> bool {
self.step == RevolveStep::Pick
}
fn inject_selection_entities(&mut self, entities: Vec<SelectionEntity>) {
if self.step != RevolveStep::Pick {
return;
}
self.handles = entities.iter().map(|entry| entry.handle).collect();
self.preview_profiles = entities
.into_iter()
.map(|entry| (entry.handle, entry.entity))
.collect();
}
fn on_selection_complete(&mut self, _handles: Vec<Handle>) -> CmdResult {
CmdResult::NeedPoint
}
fn inject_before_entity_pick(&self) -> bool {
self.step == RevolveStep::AxisObject
}
fn inject_picked_entity(&mut self, entity: EntityType) {
if self.step == RevolveStep::AxisObject {
self.injected_axis = Some(entity);
}
}
fn on_preview_wires(&mut self, cursor: DVec3) -> Vec<WireModel> {
self.preview_wires(cursor)
}
fn dyn_field(&self) -> crate::command::DynField {
if matches!(self.step, RevolveStep::Angle | RevolveStep::StartAngle) {
crate::command::DynField::Angle
} else {
crate::command::DynField::Point
}
}
fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
matches!(self.step, RevolveStep::Angle | RevolveStep::StartAngle).then(|| {
crate::command::DynSpec {
anchor: crate::command::DynAnchor::Point(
self.angle_anchor().unwrap_or(self.axis_start),
),
fields: vec![crate::command::DynFieldSpec::new(crate::command::DynRole::Angle)],
guide: crate::command::DynGuide::None,
ref_point: self.profile_reference(),
}
})
}
fn dyn_commit_as_text(&self) -> bool {
matches!(self.step, RevolveStep::Angle | RevolveStep::StartAngle)
}
fn dyn_auto_sign_angle(&self) -> bool {
false
}
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
let full_at_zero = self.step == RevolveStep::Angle;
self.cursor_angle(cursor, full_at_zero).map(|angle| {
let angle = if self.step == RevolveStep::Angle {
self.signed_angle(angle)
} else {
angle
};
crate::command::dyn_display_angle_deg(angle as f32) as f64
})
}
}
@@ -868,6 +1273,45 @@ pub fn empty_extruded_surface(direction: DVec3, taper_angle: f64) -> EntityType
EntityType::Surface(surface)
}
pub fn empty_revolved_surface(
profile: &EntityType,
axis_start: DVec3,
axis_end: DVec3,
angle: f64,
start_angle: f64,
) -> EntityType {
use acadrust::entities::{Surface, SurfaceData, SurfaceKind};
use acadrust::types::Vector3;
let mut surface = Surface::new(SurfaceKind::Revolved);
let axis = (axis_end - axis_start).normalize_or(DVec3::Z);
surface.point_of_reference = Vector3::new(axis_start.x, axis_start.y, axis_start.z);
if let SurfaceData::Revolved {
revolve_entity,
axis_point,
axis_vector,
revolve_angle,
start_angle: stored_start,
entity_transform,
solid,
..
} = &mut surface.surface_data
{
if let Some((embedded, transform)) =
crate::scene::model::sweep_model::embedded_revolve_profile(profile)
{
*revolve_entity = Some(embedded);
*entity_transform = transform;
}
*axis_point = Vector3::new(axis_start.x, axis_start.y, axis_start.z);
*axis_vector = Vector3::new(axis.x, axis.y, axis.z);
*revolve_angle = angle;
*stored_start = start_angle;
*solid = false;
}
EntityType::Surface(surface)
}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration {
names: &["EXTRUDE", "THICKEN", "PRESSPULL"]
+210
View File
@@ -68,6 +68,13 @@ pub const PROP_SCALE_ALONG_PATH: &str = "solid_history_scale_along_path";
pub const PROP_SWEEP_LENGTH: &str = "solid_history_sweep_length";
pub const PROP_EXTRUSION_HEIGHT: &str = "solid_history_extrusion_height";
pub const PROP_TAPER_ANGLE: &str = "solid_history_taper_angle";
pub const PROP_REVOLVE_ANGLE: &str = "solid_history_revolve_angle";
pub const PROP_AXIS_POSITION_X: &str = "solid_history_axis_position_x";
pub const PROP_AXIS_POSITION_Y: &str = "solid_history_axis_position_y";
pub const PROP_AXIS_POSITION_Z: &str = "solid_history_axis_position_z";
pub const PROP_AXIS_DIRECTION_X: &str = "solid_history_axis_direction_x";
pub const PROP_AXIS_DIRECTION_Y: &str = "solid_history_axis_direction_y";
pub const PROP_AXIS_DIRECTION_Z: &str = "solid_history_axis_direction_z";
pub const PROP_PYRAMID_TYPE: &str = "solid_history_pyramid_type";
pub const PROP_HISTORY: &str = "solid_history_record";
pub const PROP_SHOW_HISTORY: &str = "solid_history_show";
@@ -122,6 +129,7 @@ pub fn has_specialized_primitive_properties(
| SolidHistoryOperation::Pyramid(_)
| SolidHistoryOperation::Sweep(_)
| SolidHistoryOperation::Extrusion(_)
| SolidHistoryOperation::Revolve(_)
)
)
}
@@ -147,10 +155,112 @@ pub fn reference_point(operation: &SolidHistoryOperation) -> Option<glam::DVec3>
),
SolidHistoryOperation::Torus(value) => world_point(value.base.transform, [0.0; 3]),
SolidHistoryOperation::Pyramid(value) => world_point(value.base.transform, [0.0; 3]),
SolidHistoryOperation::Revolve(value) => world_point(
value.base.transform,
[value.axis_point.x, value.axis_point.y, value.axis_point.z],
),
_ => None,
}
}
fn revolve_properties(
document: &acadrust::CadDocument,
handle: acadrust::Handle,
value: &SolidHistoryRevolve,
) -> Vec<PropSection> {
let Some(axis_position) = world_point(
value.base.transform,
[value.axis_point.x, value.axis_point.y, value.axis_point.z],
) else {
return Vec::new();
};
let Some(axis_direction) = world_vector(
value.base.transform,
[value.direction.x, value.direction.y, value.direction.z],
) else {
return Vec::new();
};
let (record_history, object_show_history, show_history_mode) =
history_flags(document, handle).unwrap_or((false, false, 1));
let (show_history, show_history_editable) =
displayed_history_state(object_show_history, show_history_mode);
let show_value = if show_history { "Yes" } else { "No" };
vec![
PropSection {
title: t!("Geometry").into_owned(),
props: vec![
Property {
label: t!("Solid type").into_owned(),
field: "solid_history_type",
value: PropValue::ReadOnly(t!("Revolve").into_owned()),
},
Property {
label: t!("Angle of revolution").into_owned(),
field: PROP_REVOLVE_ANGLE,
value: PropValue::EditText(crate::entities::common::format_angle(
value.revolve_angle,
)),
},
crate::entities::common::edit_prop(
t!("Axis position X").as_ref(),
PROP_AXIS_POSITION_X,
axis_position.x,
),
crate::entities::common::edit_prop(
t!("Axis position Y").as_ref(),
PROP_AXIS_POSITION_Y,
axis_position.y,
),
crate::entities::common::edit_prop(
t!("Axis position Z").as_ref(),
PROP_AXIS_POSITION_Z,
axis_position.z,
),
crate::entities::common::edit_scalar_prop(
t!("Axis direction X").as_ref(),
PROP_AXIS_DIRECTION_X,
axis_direction.x,
),
crate::entities::common::edit_scalar_prop(
t!("Axis direction Y").as_ref(),
PROP_AXIS_DIRECTION_Y,
axis_direction.y,
),
crate::entities::common::edit_scalar_prop(
t!("Axis direction Z").as_ref(),
PROP_AXIS_DIRECTION_Z,
axis_direction.z,
),
],
},
PropSection {
title: t!("Solid History").into_owned(),
props: vec![
Property {
label: t!("History").into_owned(),
field: PROP_HISTORY,
value: PropValue::Choice {
selected: if record_history { "Record" } else { "None" }.to_string(),
options: vec!["None".to_string(), "Record".to_string()],
},
},
Property {
label: t!("Show History").into_owned(),
field: PROP_SHOW_HISTORY,
value: if show_history_editable {
PropValue::Choice {
selected: show_value.to_string(),
options: vec!["No".to_string(), "Yes".to_string()],
}
} else {
PropValue::ReadOnly(show_value.to_string())
},
},
],
},
]
}
fn extrusion_properties(
document: &acadrust::CadDocument,
handle: acadrust::Handle,
@@ -1021,6 +1131,7 @@ pub fn primitive_properties(
SolidHistoryOperation::Extrusion(value) => {
extrusion_properties(document, handle, value)
}
SolidHistoryOperation::Revolve(value) => revolve_properties(document, handle, value),
_ => Vec::new(),
}
}
@@ -1053,6 +1164,13 @@ pub fn is_primitive_property(field: &str) -> bool {
| PROP_SCALE_ALONG_PATH
| PROP_EXTRUSION_HEIGHT
| PROP_TAPER_ANGLE
| PROP_REVOLVE_ANGLE
| PROP_AXIS_POSITION_X
| PROP_AXIS_POSITION_Y
| PROP_AXIS_POSITION_Z
| PROP_AXIS_DIRECTION_X
| PROP_AXIS_DIRECTION_Y
| PROP_AXIS_DIRECTION_Z
| PROP_PYRAMID_TYPE
)
}
@@ -1142,6 +1260,93 @@ fn apply_extrusion_geometry_property(
}
}
fn apply_revolve_geometry_property(
value: &mut SolidHistoryRevolve,
field: &str,
text: &str,
) -> Option<bool> {
if field == PROP_REVOLVE_ANGLE {
let angle = crate::entities::common::parse_angle(text)?;
if !angle.is_finite()
|| angle.abs() <= 1e-12
|| angle.abs() > std::f64::consts::TAU + 1e-12
|| (angle - value.revolve_angle).abs() <= 1e-12
{
return Some(false);
}
value.revolve_angle = angle;
return Some(true);
}
if matches!(
field,
PROP_AXIS_POSITION_X | PROP_AXIS_POSITION_Y | PROP_AXIS_POSITION_Z
) {
let axis = match field {
PROP_AXIS_POSITION_X => 0,
PROP_AXIS_POSITION_Y => 1,
_ => 2,
};
let target = crate::entities::common::parse_length(text)?;
if !target.is_finite() {
return Some(false);
}
let mut world = world_point(
value.base.transform,
[value.axis_point.x, value.axis_point.y, value.axis_point.z],
)?;
if (world[axis] - target).abs() <= 1e-12 {
return Some(false);
}
world[axis] = target;
let local = local_point(value.base.transform, world)?;
if !local.is_finite() {
return Some(false);
}
value.axis_point = acadrust::types::Vector3::new(local.x, local.y, local.z);
return Some(true);
}
if matches!(
field,
PROP_AXIS_DIRECTION_X | PROP_AXIS_DIRECTION_Y | PROP_AXIS_DIRECTION_Z
) {
let axis = match field {
PROP_AXIS_DIRECTION_X => 0,
PROP_AXIS_DIRECTION_Y => 1,
_ => 2,
};
let target = text.trim().replace(',', ".").parse::<f64>().ok()?;
if !target.is_finite() {
return Some(false);
}
let mut world = world_vector(
value.base.transform,
[value.direction.x, value.direction.y, value.direction.z],
)?;
world[axis] = target;
let Some(world) = world.try_normalize() else {
return Some(false);
};
let Some(local) = matrix(value.base.transform)?
.inverse()
.transform_vector3(world)
.try_normalize()
else {
return Some(false);
};
let current = glam::DVec3::new(value.direction.x, value.direction.y, value.direction.z)
.normalize_or_zero();
if (local - current).length_squared() <= 1e-24 {
return Some(false);
}
value.direction = acadrust::types::Vector3::new(local.x, local.y, local.z);
return Some(true);
}
None
}
pub fn apply_history_choice(
document: &mut acadrust::CadDocument,
handle: acadrust::Handle,
@@ -1578,6 +1783,11 @@ pub fn apply_primitive_property(
return applied;
}
}
if let SolidHistoryOperation::Revolve(revolve_value) = operation {
if let Some(applied) = apply_revolve_geometry_property(revolve_value, field, value) {
return applied;
}
}
if let SolidHistoryOperation::Torus(torus_value) = operation {
if let Some(applied) = apply_torus_position_property(torus_value, field, value) {
return applied;
+84 -12
View File
@@ -386,15 +386,65 @@ pub fn revolved(
from: [f64; 3],
to: [f64; 3],
angle: f64,
start_angle: f64,
) -> Option<Body> {
let profile = profile_of(entity)?;
brep::revolve(
profile.plane,
&profile.pieces,
from,
[to[0] - from[0], to[1] - from[1], to[2] - from[2]],
angle,
)
let axis = [to[0] - from[0], to[1] - from[1], to[2] - from[2]];
let body = if let Some((plane, profiles)) =
region_profiles(entity).filter(|(_, profiles)| profiles.len() > 1)
{
brep::revolve_region(plane, &profiles, from, axis, angle)?
} else {
let profile = profile_of(entity)?;
brep::revolve(profile.plane, &profile.pieces, from, axis, angle)?
};
turn_revolved_body(body, from, axis, start_angle)
}
/// REVOLVE sheet mode for an open or closed planar profile.
pub fn revolved_surface(
entity: &EntityType,
from: [f64; 3],
to: [f64; 3],
angle: f64,
start_angle: f64,
) -> Option<Body> {
let axis = [to[0] - from[0], to[1] - from[1], to[2] - from[2]];
let body = if let Some((plane, profiles)) =
region_profiles(entity).filter(|(_, profiles)| profiles.len() > 1)
{
brep::revolve_surface_region(plane, &profiles, from, axis, angle)?
} else {
let (profile, _) = extrusion_profile_of(entity)?;
brep::revolve_surface(profile.plane, &profile.pieces, from, axis, angle)?
};
turn_revolved_body(body, from, axis, start_angle)
}
fn turn_revolved_body(
body: Body,
pivot: [f64; 3],
axis: [f64; 3],
angle: f64,
) -> Option<Body> {
if !angle.is_finite() {
return None;
}
if angle.abs() <= 1e-12 {
return Some(body);
}
let axis = Vec3::from(axis).normalize()?;
let rotate = |vector: Vec3| {
let (sin, cos) = angle.sin_cos();
vector * cos + axis.cross(vector) * sin + axis * axis.dot(vector) * (1.0 - cos)
};
let pivot = Vec3::from(pivot);
let placement = brep::Placement {
x_axis: rotate(Vec3::X).to_array(),
y_axis: rotate(Vec3::Y).to_array(),
z_axis: rotate(Vec3::Z).to_array(),
origin: (pivot - rotate(pivot)).to_array(),
};
brep::transform(&body, &placement)
}
fn profile_center(profile: &Profile) -> Option<Vec3> {
@@ -413,8 +463,13 @@ pub fn revolve_history(
from: [f64; 3],
to: [f64; 3],
angle: f64,
start_angle: f64,
) -> Option<SolidHistoryOperation> {
if !angle.is_finite() || angle.abs() <= 1e-12 {
if !angle.is_finite()
|| angle.abs() <= 1e-12
|| !start_angle.is_finite()
|| matches!(entity, EntityType::Region(region) if region.wires.len() > 1)
{
return None;
}
let profile = profile_of(entity)?;
@@ -422,16 +477,27 @@ pub fn revolve_history(
let direction = (Vec3::from(to) - from).normalize()?;
let center = profile_center(&profile)?;
let axis_point = from + direction * (center - from).dot(direction);
let (sweep_entity, transform) = embedded_planar_entity(entity)?;
let transform = glam::DMat4::from_cols_array(&transform);
let inverse = transform.inverse();
if !transform.is_finite() || !inverse.is_finite() || transform.determinant().abs() <= 1e-12 {
return None;
}
let axis_point = inverse.transform_point3(glam::DVec3::from_array(axis_point.to_array()));
let direction = inverse
.transform_vector3(glam::DVec3::from_array(direction.to_array()))
.try_normalize()?;
let mut base = SolidHistoryNodeBase::new(1);
base.transform = glam::DMat4::IDENTITY.to_cols_array();
base.transform = transform.to_cols_array();
Some(SolidHistoryOperation::Revolve(SolidHistoryRevolve {
base,
operation_major: 1,
axis_point: Vector3::new(axis_point.x, axis_point.y, axis_point.z),
direction: Vector3::new(direction.x, direction.y, direction.z),
revolve_angle: angle,
start_angle,
flag_290: true,
sweep_entity: Some(embedded_path(entity)?),
sweep_entity: Some(sweep_entity),
..SolidHistoryRevolve::default()
}))
}
@@ -597,6 +663,10 @@ fn embedded_planar_entity(entity: &EntityType) -> Option<(EmbeddedEntity, [f64;
Some((EmbeddedEntity::LwPolyline(polyline), transform))
}
pub fn embedded_revolve_profile(entity: &EntityType) -> Option<(EmbeddedEntity, [f64; 16])> {
embedded_planar_entity(entity)
}
pub fn extrusion_history(
profile: &EntityType,
path: Option<&EntityType>,
@@ -850,6 +920,7 @@ mod tests {
[0.0, 0.0, 0.0],
[0.0, 10.0, 0.0],
std::f64::consts::TAU,
0.0,
);
// The profile is in the XY plane and the axis lies in it, so this is
// a revolution about the y axis: radius ten, height ten.
@@ -875,7 +946,8 @@ mod tests {
&square(10.0),
[0.0, 0.0, 0.0],
[0.0, 1.0, 1.0],
std::f64::consts::TAU
std::f64::consts::TAU,
0.0,
)
.is_none());
}