Merge remote-tracking branch 'origin/main' into merge-test/main-into-constraints

# Conflicts:
#	src/app/command_driver.rs
#	src/command.rs
#	src/ui/ribbon/mod.rs
#	src/ui/window/options.rs
This commit is contained in:
Felix Riestra committed 2026-09-10 23:34:50 +02:00
commit ad718def3f
124 files changed
+9866 -692

No files matched your search

+31 -7
View File
@@ -4,10 +4,10 @@
// Produces a Ray entity; repeats until Enter/Esc.
// XLINE — infinite construction line: same two-click pattern, yields XLine.
use crate::t;
use acadrust::entities::{Ray as RayEnt, XLine as XLineEnt};
use acadrust::types::Vector3;
use acadrust::EntityType;
use crate::t;
use crate::command::{CadCommand, CmdResult};
use crate::scene::model::wire_model::WireModel;
@@ -61,9 +61,7 @@ impl CadCommand for RayCommand {
Vector3::new(base.x, base.y, base.z),
Vector3::new(dir_n.x, dir_n.y, dir_n.z),
);
// Stay active: new base = same base (can keep clicking through points)
// Actually AutoCAD prompts for new start after each ray — reset base.
self.base = None;
// Repeated through points share the original start point.
CmdResult::CommitEntity(EntityType::Ray(ray))
} else {
self.base = Some(pt);
@@ -167,7 +165,6 @@ impl CadCommand for XLineCommand {
Vector3::new(base.x, base.y, base.z),
Vector3::new(dir_n.x, dir_n.y, dir_n.z),
);
self.base = None;
CmdResult::CommitEntity(EntityType::XLine(xline))
} else {
self.base = Some(pt);
@@ -229,7 +226,34 @@ impl CadCommand for XLineCommand {
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn repeated_rays_and_xlines_keep_their_first_point() {
let base = DVec3::new(2.0, 3.0, 4.0);
let mut ray = RayCommand::new();
let mut xline = XLineCommand::new();
assert!(matches!(ray.on_point(base), CmdResult::NeedPoint));
assert!(matches!(xline.on_point(base), CmdResult::NeedPoint));
for through in [DVec3::new(3.0, 3.0, 4.0), DVec3::new(2.0, 5.0, 4.0)] {
let CmdResult::CommitEntity(EntityType::Ray(entity)) = ray.on_point(through) else {
panic!("ray was not committed");
};
assert_eq!(entity.base_point, Vector3::new(base.x, base.y, base.z));
let CmdResult::CommitEntity(EntityType::XLine(entity)) = xline.on_point(through) else {
panic!("construction line was not committed");
};
assert_eq!(entity.base_point, Vector3::new(base.x, base.y, base.z));
}
}
}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["RAY"] }); // RayCommand
inventory::submit!(crate::command::CommandRegistration { names: &["CONSTRUCTIONLINE", "XLINE"] }); // XLineCommand
inventory::submit!(crate::command::CommandRegistration { names: &["RAY"] }); // RayCommand
inventory::submit!(crate::command::CommandRegistration {
names: &["CONSTRUCTIONLINE", "XLINE"]
}); // XLineCommand
+3 -1
View File
@@ -933,4 +933,6 @@ fn style_from_entity(entity: &EntityType) -> Option<CloudStyle> {
})
}
inventory::submit!(crate::command::CommandRegistration { names: &["REVCLOUD"] });
inventory::submit!(crate::command::CommandRegistration {
names: &["REVCLOUD", "REVCLOUD_RECTANGULAR", "REVCLOUD_POLYGONAL", "REVCLOUD_FREEHAND"]
});
+134 -38
View File
@@ -3,9 +3,9 @@
// Command: SPLINE (SPL)
// Click to add fit points. Enter (≥2 pts) → commits EntityType::Spline.
use crate::t;
use acadrust::types::Vector3;
use acadrust::{EntityType, Spline};
use crate::t;
use crate::command::{CadCommand, CmdResult};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
@@ -24,49 +24,87 @@ pub fn tool() -> ToolDef {
pub struct SplineCommand {
pts: Vec<DVec3>,
control_vertices: bool,
choosing_method: bool,
}
impl SplineCommand {
pub fn new() -> Self {
Self { pts: Vec::new() }
Self {
pts: Vec::new(),
control_vertices: false,
choosing_method: false,
}
}
pub fn control_vertices() -> Self {
Self {
control_vertices: true,
..Self::new()
}
}
fn build(&self, closed: bool) -> Option<EntityType> {
if self.pts.len() < 2 {
return None;
}
let fit_points = self
.pts
.iter()
.map(|p| Vector3::new(p.x, p.y, p.z))
.collect();
let mut spline = Spline {
degree: 3,
fit_points,
..Default::default()
};
spline.flags.closed = closed;
Some(EntityType::Spline(spline))
Some(EntityType::Spline(make_spline(
&self.pts,
closed,
self.control_vertices,
)))
}
}
/// Preview through the same fit-point representation that `build()` commits.
fn sample_curve(pts: &[DVec3], closed: bool) -> Vec<[f32; 3]> {
/// Store the chosen construction method directly in the persistent spline.
fn make_spline(pts: &[DVec3], closed: bool, control_vertices: bool) -> Spline {
let mut points: Vec<Vector3> = pts.iter().map(|p| Vector3::new(p.x, p.y, p.z)).collect();
let mut spline = if control_vertices {
if closed && pts.first() != pts.last() {
points.push(points[0]);
}
let count = points.len();
let degree = 3.min(count.saturating_sub(1));
let spans = count - degree;
// Open uniform knot vector: degree + 1 equal knots at each endpoint.
let knots = (0..count + degree + 1)
.map(|index| {
if index <= degree {
0.0
} else if index >= count {
1.0
} else {
(index - degree) as f64 / spans as f64
}
})
.collect();
Spline {
degree: degree as i32,
control_points: points,
knots,
weights: vec![1.0; count],
..Default::default()
}
} else {
Spline {
degree: 3,
fit_points: points,
..Default::default()
}
};
spline.flags.closed = closed;
spline
}
/// Preview the exact representation that `build()` commits.
fn sample_curve(pts: &[DVec3], closed: bool, control_vertices: bool) -> Vec<[f32; 3]> {
if pts.len() < 2 {
return pts
.iter()
.map(|p| [p.x as f32, p.y as f32, p.z as f32])
.collect();
}
let mut spline = Spline {
degree: 3,
fit_points: pts
.iter()
.map(|p| Vector3::new(p.x, p.y, p.z))
.collect(),
..Default::default()
};
spline.flags.closed = closed;
let spline = make_spline(pts, closed, control_vertices);
crate::entities::curve::spline_curve(&spline)
.map(|curve| crate::entities::curve::curve_points(&curve))
.unwrap_or_else(|| crate::entities::spline::measurement_polyline(&spline))
@@ -77,11 +115,19 @@ fn sample_curve(pts: &[DVec3], closed: bool) -> Vec<[f32; 3]> {
impl CadCommand for SplineCommand {
fn name(&self) -> &'static str {
"SPLINE"
if self.control_vertices {
"SPLINECV"
} else {
"SPLINE"
}
}
fn prompt(&self) -> String {
if self.pts.is_empty() {
if self.choosing_method {
"SPLINE Choose creation method [Fit/Control vertices]:".into()
} else if self.pts.is_empty() && self.control_vertices {
t!("SPLINE Specify first control point:").into_owned()
} else if self.pts.is_empty() {
t!("SPLINE Specify first point:").into_owned()
} else {
let n = self.pts.len();
@@ -91,8 +137,14 @@ impl CadCommand for SplineCommand {
fn options(&self) -> Vec<crate::command::CmdOption> {
use crate::command::CmdOption;
if self.choosing_method {
return vec![
CmdOption::new("Fit", "FIT"),
CmdOption::new("Control vertices", "CV"),
];
}
if self.pts.is_empty() {
return Vec::new();
return vec![CmdOption::new(t!("Method").as_ref(), "M")];
}
let mut opts = vec![CmdOption::new(t!("Close").as_ref(), "C")];
// Undo only makes sense once a control point exists.
@@ -102,11 +154,18 @@ impl CadCommand for SplineCommand {
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
if self.choosing_method || !pt.is_finite() {
return CmdResult::NeedPoint;
}
self.pts.push(pt);
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
if self.choosing_method {
self.choosing_method = false;
return CmdResult::NeedPoint;
}
match self.build(false) {
Some(e) => CmdResult::CommitAndExit(e),
None => CmdResult::Cancel,
@@ -114,26 +173,37 @@ impl CadCommand for SplineCommand {
}
fn enter_accepts_default_start(&self) -> bool {
self.pts.is_empty()
self.pts.is_empty() && !self.choosing_method
}
fn on_escape(&mut self) -> CmdResult {
match self.build(false) {
Some(e) => CmdResult::CommitAndExit(e),
None => CmdResult::Cancel,
}
CmdResult::Cancel
}
fn wants_text_input(&self) -> bool {
!self.pts.is_empty()
true
}
fn point_step_accepts_keywords(&self) -> bool {
!self.pts.is_empty()
true
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
match text.trim().to_uppercase().as_str() {
"M" | "METHOD" if self.pts.is_empty() => {
self.choosing_method = true;
Some(CmdResult::NeedPoint)
}
"F" | "FIT" if self.pts.is_empty() => {
self.control_vertices = false;
self.choosing_method = false;
Some(CmdResult::NeedPoint)
}
"CV" | "CONTROL" | "CONTROLVERTICES" if self.pts.is_empty() => {
self.control_vertices = true;
self.choosing_method = false;
Some(CmdResult::NeedPoint)
}
"C" | "CLOSE" => match self.build(true) {
Some(e) => Some(CmdResult::CommitAndExit(e)),
None => Some(CmdResult::NeedPoint),
@@ -150,18 +220,44 @@ impl CadCommand for SplineCommand {
if self.pts.is_empty() {
return None;
}
// Preview the committed fit-point representation.
// Preview the committed construction method.
let mut ctrl = self.pts.clone();
ctrl.push(pt);
Some(WireModel::solid(
"rubber_band".into(),
sample_curve(&ctrl, false),
sample_curve(&ctrl, false, self.control_vertices),
WireModel::CYAN,
false,
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn control_vertices_make_a_clamped_finite_spline() {
let points = [
DVec3::new(0.0, 0.0, 0.0),
DVec3::new(1.0, 2.0, 0.0),
DVec3::new(2.0, 2.0, 0.0),
DVec3::new(3.0, 0.0, 0.0),
];
let spline = make_spline(&points, false, true);
assert_eq!(spline.degree, 3);
assert_eq!(spline.control_points.len(), 4);
assert_eq!(spline.knots, vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]);
assert_eq!(spline.weights, vec![1.0; 4]);
assert!(sample_curve(&points, false, true)
.iter()
.flatten()
.all(|value| value.is_finite()));
}
}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["SPLINE"] }); // SplineCommand
inventory::submit!(crate::command::CommandRegistration {
names: &["SPLINE", "SPLINECV"]
}); // SplineCommand
+2
View File
@@ -2778,6 +2778,8 @@ pub fn empty_extruded_surface(direction: DVec3, taper_angle: f64) -> EntityType
use acadrust::types::Vector3;
let mut surface = Surface::new(SurfaceKind::Extruded);
surface.u_isolines = 6;
surface.v_isolines = 6;
if let SurfaceData::Extruded {
options,
sweep_vector,
+7 -1
View File
@@ -79,7 +79,13 @@ impl CadModule for ManageModule {
// ── Application ───────────────────────────────────────────────────
RibbonGroup {
title: "Application",
tools: vec![RibbonItem::LargeTool(about::tool())],
tools: vec![
// The tool and its icon already existed but were never
// added to a group, so the dialog could only be reached
// by typing OPTIONS or from the start page.
RibbonItem::LargeTool(options::tool()),
RibbonItem::LargeTool(about::tool()),
],
},
]
})
-1
View File
@@ -1,5 +1,4 @@
use crate::modules::{IconKind, ModuleEvent, ToolDef};
#[allow(dead_code)]
pub fn tool() -> ToolDef {
ToolDef {
id: "OPTIONS",
+6 -1
View File
@@ -210,7 +210,12 @@ impl SolidEdgeCommand {
)
.ok()?,
};
let mut wires = crate::scene::model::solid_model::grip_preview_wires(&result, handle);
let mut wires = crate::scene::model::solid_model::grip_preview_wires(
&result,
handle,
[0; 2],
false,
);
for wire in &mut wires {
wire.color = self.preview_color;
wire.name = format!("{}-{}-PREVIEW", handle.value(), self.name());