forked from 1b-agent/opencadstudio-upstream
fix(box): preserve construction semantics
This commit is contained in:
commit
fc93efeed7
52 files changed
+6700
-1754
No files matched your search
@@ -135,6 +135,8 @@ impl CadCommand for AlignedDimensionCommand {
|
||||
CmdResult::CommitDimension {
|
||||
entity: self.plane.place_entity(EntityType::Dimension(Dimension::Aligned(dim))),
|
||||
association: DimensionAssociationInput::Infer(self.source_handle),
|
||||
preserve_base_style: false,
|
||||
continue_command: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,6 +234,8 @@ impl AngularDimensionCommand {
|
||||
CmdResult::CommitDimension {
|
||||
entity,
|
||||
association: DimensionAssociationInput::Explicit(self.source_refs.clone()),
|
||||
preserve_base_style: false,
|
||||
continue_command: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -265,6 +265,8 @@ impl ArcLengthDimensionCommand {
|
||||
CmdResult::CommitDimension {
|
||||
entity: plane.place_entity(EntityType::Dimension(Dimension::Arc(dimension))),
|
||||
association,
|
||||
preserve_base_style: false,
|
||||
continue_command: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +113,8 @@ impl CadCommand for DiameterDimensionCommand {
|
||||
entity: source_plane
|
||||
.place_entity(EntityType::Dimension(Dimension::Diameter(dim))),
|
||||
association: DimensionAssociationInput::Infer(self.source_handle),
|
||||
preserve_base_style: false,
|
||||
continue_command: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,20 +1,19 @@
|
||||
// DIMCONTINUE command — chain linear/aligned dimensions end-to-end.
|
||||
//
|
||||
// Each new point becomes the second extension line origin of a new dimension,
|
||||
// whose first extension line origin is the second extension line of the previous dim.
|
||||
// The dimension line stays at the same perpendicular offset as the base dimension.
|
||||
//
|
||||
// Constructed from commands.rs after finding the last placed linear/aligned dimension.
|
||||
|
||||
use acadrust::entities::{Dimension, DimensionLinear};
|
||||
use acadrust::entities::{
|
||||
Dimension, DimensionAngular2Ln, DimensionAngular3Pt, DimensionBase, DimensionLinear,
|
||||
DimensionOrdinate,
|
||||
};
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::EntityType;
|
||||
use glam::{DVec3, Vec3};
|
||||
use acadrust::{EntityType, Handle};
|
||||
use cadkernel::geom2d::{
|
||||
arc_span, closest_point, intersect, nearest_of, Arc, Curve, Ray, Tolerance, Transform, Vec2,
|
||||
XLine,
|
||||
};
|
||||
use cadkernel::space::Plane;
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, DimensionAssociationInput};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use crate::t;
|
||||
|
||||
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/dim_continue.svg"));
|
||||
|
||||
@@ -27,74 +26,292 @@ pub fn tool() -> ToolDef {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DimContinueCommand {
|
||||
/// Fixed first-extension-line origin for the current step (moves each iteration).
|
||||
chain_p1: DVec3,
|
||||
/// Direction along the dimension axis (0.0 = horizontal, PI/2 = vertical).
|
||||
rotation: f64,
|
||||
/// Text reading rotation inherited from the base dim so a UCS-aligned chain
|
||||
/// keeps its text consistent with the originating DIMLINEAR. 0 = natural.
|
||||
#[derive(Clone)]
|
||||
struct SourceStyle {
|
||||
layer: String,
|
||||
style_name: String,
|
||||
normal: Vector3,
|
||||
text_rotation: f64,
|
||||
/// Absolute perpendicular coordinate (dot with `perp`) of the base
|
||||
/// dimension line. Each continued dim line is projected onto this exact
|
||||
/// coordinate so the whole chain stays collinear — even when the extension
|
||||
/// origins sit at different perpendicular positions (extension lines of
|
||||
/// different lengths). (#151)
|
||||
dim_line_perp: f64,
|
||||
/// Direction of "up" perpendicular to the dim axis (points toward the dim line).
|
||||
perp: DVec3,
|
||||
/// True once we have a base dimension loaded.
|
||||
ready: bool,
|
||||
horizontal_direction: f64,
|
||||
}
|
||||
|
||||
impl DimContinueCommand {
|
||||
/// No base dim found — will show an error prompt and cancel immediately.
|
||||
pub fn new() -> Self {
|
||||
impl SourceStyle {
|
||||
fn from_base(base: &DimensionBase) -> Self {
|
||||
Self {
|
||||
chain_p1: DVec3::ZERO,
|
||||
rotation: 0.0,
|
||||
text_rotation: 0.0,
|
||||
dim_line_perp: 0.0,
|
||||
perp: DVec3::Y,
|
||||
ready: false,
|
||||
layer: base.common.layer.clone(),
|
||||
style_name: base.style_name.clone(),
|
||||
normal: base.normal,
|
||||
text_rotation: base.text_rotation,
|
||||
horizontal_direction: base.horizontal_direction,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build from the last placed dimension.
|
||||
///
|
||||
/// `p2` — second extension line origin of the base dim (the chain starts
|
||||
/// here). `p1` is unused — the dim line position comes from
|
||||
/// `definition_point` alone — but kept for signature parity with the
|
||||
/// shared `find_last_linear_dim` tuple (DIMBASELINE uses p1).
|
||||
/// `definition_point` — where the dim line was placed; its perpendicular
|
||||
/// coordinate is the line the whole chain stays collinear with.
|
||||
/// `rotation` — 0.0 = horizontal dim, PI/2 = vertical dim.
|
||||
pub fn from_base(
|
||||
_p1: Vec3,
|
||||
p2: Vec3,
|
||||
definition_point: Vec3,
|
||||
fn apply(&self, base: &mut DimensionBase) {
|
||||
base.common.layer = self.layer.clone();
|
||||
base.style_name = self.style_name.clone();
|
||||
base.normal = self.normal;
|
||||
base.text_rotation = self.text_rotation;
|
||||
base.horizontal_direction = self.horizontal_direction;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum ContinueKind {
|
||||
Linear {
|
||||
current: Vec2,
|
||||
rotation: f64,
|
||||
text_rotation: f64,
|
||||
) -> Self {
|
||||
// Widen the base-dim points to f64 so the committed chain math runs in
|
||||
// full precision. (The base points arrive as f32 from the entity, but
|
||||
// every derived committed coordinate must stay f64 from here on.)
|
||||
let p2 = p2.as_dvec3();
|
||||
let definition_point = definition_point.as_dvec3();
|
||||
// Axis unit vector along the measurement direction — the base dim's
|
||||
// rotation angle (any angle, incl. a UCS-aligned one), not a world H/V.
|
||||
let axis = DVec3::new(rotation.cos(), rotation.sin(), 0.0);
|
||||
// Perpendicular unit vector toward the dim line.
|
||||
let perp = DVec3::new(-axis.y, axis.x, 0.0);
|
||||
let dim_line_perp = definition_point.dot(perp);
|
||||
Self {
|
||||
chain_p1: p2,
|
||||
rotation,
|
||||
text_rotation,
|
||||
dim_line_perp,
|
||||
perp,
|
||||
ready: true,
|
||||
perpendicular: Vec2,
|
||||
line_coordinate: f64,
|
||||
},
|
||||
Angular2Ln {
|
||||
vertex: Vec2,
|
||||
current: Vec2,
|
||||
radius: f64,
|
||||
},
|
||||
Angular3Pt {
|
||||
vertex: Vec2,
|
||||
current: Vec2,
|
||||
radius: f64,
|
||||
},
|
||||
Ordinate {
|
||||
definition: Vec2,
|
||||
leader: Vec2,
|
||||
is_x: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ContinueState {
|
||||
kind: ContinueKind,
|
||||
style: SourceStyle,
|
||||
plane: Plane,
|
||||
}
|
||||
|
||||
pub struct DimContinueCommand {
|
||||
base: Option<ContinueState>,
|
||||
injected: Option<EntityType>,
|
||||
history: Vec<ContinueState>,
|
||||
preserve_base_style: bool,
|
||||
}
|
||||
|
||||
impl DimContinueCommand {
|
||||
pub fn new(recent: Option<EntityType>, preserve_base_style: bool) -> Self {
|
||||
let mut command = Self {
|
||||
base: None,
|
||||
injected: None,
|
||||
history: Vec::new(),
|
||||
preserve_base_style,
|
||||
};
|
||||
if let Some(entity) = recent {
|
||||
let _ = command.install_base(&entity, None);
|
||||
}
|
||||
command
|
||||
}
|
||||
|
||||
fn install_base(&mut self, entity: &EntityType, pick_world: Option<DVec3>) -> bool {
|
||||
let EntityType::Dimension(dimension) = entity else {
|
||||
return false;
|
||||
};
|
||||
let style = SourceStyle::from_base(dimension.base());
|
||||
let plane = plane_from_normal(dimension.base().definition_point, style.normal);
|
||||
let pick = pick_world.and_then(|point| plane.project(point.to_array()).map(Vec2::from));
|
||||
let kind = match dimension {
|
||||
Dimension::Linear(source) => {
|
||||
let rotation = source.rotation;
|
||||
let perpendicular = Vec2::new(-rotation.sin(), rotation.cos());
|
||||
ContinueKind::Linear {
|
||||
current: selected_linear_end(
|
||||
project(&plane, source.first_point),
|
||||
project(&plane, source.second_point),
|
||||
pick,
|
||||
),
|
||||
rotation,
|
||||
perpendicular,
|
||||
line_coordinate: project(&plane, source.base.definition_point)
|
||||
.dot(perpendicular),
|
||||
}
|
||||
}
|
||||
Dimension::Aligned(source) => {
|
||||
let first = project(&plane, source.first_point);
|
||||
let second = project(&plane, source.second_point);
|
||||
let Some(direction) = (second - first).normalize() else {
|
||||
return false;
|
||||
};
|
||||
let rotation = direction.angle();
|
||||
let perpendicular = direction.perpendicular();
|
||||
ContinueKind::Linear {
|
||||
current: selected_linear_end(first, second, pick),
|
||||
rotation,
|
||||
perpendicular,
|
||||
line_coordinate: project(&plane, source.base.definition_point)
|
||||
.dot(perpendicular),
|
||||
}
|
||||
}
|
||||
Dimension::Angular2Ln(source) => {
|
||||
let lines = [
|
||||
[
|
||||
project(&plane, source.first_point),
|
||||
project(&plane, source.second_point),
|
||||
],
|
||||
[
|
||||
project(&plane, source.angle_vertex),
|
||||
project(&plane, source.definition_point),
|
||||
],
|
||||
];
|
||||
let vertex = angular2_vertex(lines).unwrap_or(lines[1][0]);
|
||||
ContinueKind::Angular2Ln {
|
||||
vertex,
|
||||
current: selected_angular_end(vertex, [lines[0][1], lines[1][1]], pick),
|
||||
radius: project(&plane, source.dimension_arc)
|
||||
.distance(vertex)
|
||||
.max(1.0e-9),
|
||||
}
|
||||
}
|
||||
Dimension::Angular3Pt(source) => {
|
||||
let vertex = project(&plane, source.angle_vertex);
|
||||
ContinueKind::Angular3Pt {
|
||||
vertex,
|
||||
current: selected_angular_end(
|
||||
vertex,
|
||||
[
|
||||
project(&plane, source.first_point),
|
||||
project(&plane, source.second_point),
|
||||
],
|
||||
pick,
|
||||
),
|
||||
radius: project(&plane, source.definition_point)
|
||||
.distance(vertex)
|
||||
.max(1.0e-9),
|
||||
}
|
||||
}
|
||||
Dimension::Ordinate(source) => ContinueKind::Ordinate {
|
||||
definition: project(&plane, source.definition_point),
|
||||
leader: project(&plane, source.leader_endpoint),
|
||||
is_x: source.is_ordinate_type_x,
|
||||
},
|
||||
_ => return false,
|
||||
};
|
||||
self.base = Some(ContinueState { kind, style, plane });
|
||||
true
|
||||
}
|
||||
|
||||
fn build_dimension(&self, point_world: DVec3) -> Option<Dimension> {
|
||||
let state = self.base.as_ref()?;
|
||||
let point = state.plane.project(point_world.to_array()).map(Vec2::from)?;
|
||||
let mut dimension = match &state.kind {
|
||||
ContinueKind::Linear {
|
||||
current,
|
||||
rotation,
|
||||
perpendicular,
|
||||
line_coordinate,
|
||||
} => build_linear(
|
||||
state.plane,
|
||||
*current,
|
||||
point,
|
||||
*rotation,
|
||||
*perpendicular,
|
||||
*line_coordinate,
|
||||
),
|
||||
ContinueKind::Angular2Ln {
|
||||
vertex,
|
||||
current,
|
||||
radius,
|
||||
} => {
|
||||
let previous = *current - *vertex;
|
||||
let moving = point - *vertex;
|
||||
let (definition, text) =
|
||||
angular_definition_and_text(*vertex, previous, moving, *radius)?;
|
||||
let mut result = DimensionAngular2Ln::default();
|
||||
result.first_point = world(state.plane, *vertex);
|
||||
result.second_point = world(state.plane, *current);
|
||||
result.angle_vertex = world(state.plane, *vertex);
|
||||
result.definition_point = world(state.plane, point);
|
||||
result.dimension_arc = world(state.plane, definition);
|
||||
result.base.definition_point = result.dimension_arc;
|
||||
result.base.text_middle_point = world(state.plane, text);
|
||||
result.base.insertion_point = result.base.text_middle_point;
|
||||
Dimension::Angular2Ln(result)
|
||||
}
|
||||
ContinueKind::Angular3Pt {
|
||||
vertex,
|
||||
current,
|
||||
radius,
|
||||
} => {
|
||||
let previous = *current - *vertex;
|
||||
let moving = point - *vertex;
|
||||
let (definition, text) =
|
||||
angular_definition_and_text(*vertex, previous, moving, *radius)?;
|
||||
let mut result = DimensionAngular3Pt::new(
|
||||
world(state.plane, *vertex),
|
||||
world(state.plane, *current),
|
||||
world(state.plane, point),
|
||||
);
|
||||
result.definition_point = world(state.plane, definition);
|
||||
result.base.definition_point = result.definition_point;
|
||||
result.base.text_middle_point = world(state.plane, text);
|
||||
result.base.insertion_point = result.base.text_middle_point;
|
||||
Dimension::Angular3Pt(result)
|
||||
}
|
||||
ContinueKind::Ordinate {
|
||||
definition,
|
||||
leader,
|
||||
is_x,
|
||||
} => {
|
||||
let to_dimension = Transform::rotation(-state.style.horizontal_direction);
|
||||
let from_dimension = Transform::rotation(state.style.horizontal_direction);
|
||||
let feature = Vec2::from(to_dimension.apply_point(point.into()));
|
||||
let source_leader = Vec2::from(to_dimension.apply_point((*leader).into()));
|
||||
let leader = if *is_x {
|
||||
Vec2::new(feature.x, source_leader.y)
|
||||
} else {
|
||||
Vec2::new(source_leader.x, feature.y)
|
||||
};
|
||||
let leader = Vec2::from(from_dimension.apply_point(leader.into()));
|
||||
let mut result = DimensionOrdinate::new(
|
||||
world(state.plane, point),
|
||||
world(state.plane, leader),
|
||||
*is_x,
|
||||
);
|
||||
result.definition_point = world(state.plane, *definition);
|
||||
result.base.definition_point = result.definition_point;
|
||||
result.base.text_middle_point = result.leader_endpoint;
|
||||
result.base.insertion_point = result.base.text_middle_point;
|
||||
Dimension::Ordinate(result)
|
||||
}
|
||||
};
|
||||
state.style.apply(dimension.base_mut());
|
||||
refresh_measurement(&mut dimension, state.plane);
|
||||
dimension.base_mut().common.handle = Handle::NULL;
|
||||
dimension.base_mut().block_name.clear();
|
||||
Some(dimension)
|
||||
}
|
||||
|
||||
fn advance(&mut self, point_world: DVec3) {
|
||||
let Some(point) = self
|
||||
.base
|
||||
.as_ref()
|
||||
.and_then(|state| state.plane.project(point_world.to_array()))
|
||||
.map(Vec2::from)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(state) = self.base.as_mut() else {
|
||||
return;
|
||||
};
|
||||
match &mut state.kind {
|
||||
ContinueKind::Linear { current, .. }
|
||||
| ContinueKind::Angular2Ln { current, .. }
|
||||
| ContinueKind::Angular3Pt { current, .. } => *current = point,
|
||||
ContinueKind::Ordinate { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn undo_one(&mut self) -> CmdResult {
|
||||
let Some(previous) = self.history.pop() else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
self.base = Some(previous);
|
||||
CmdResult::UndoDocument
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,110 +321,521 @@ impl CadCommand for DimContinueCommand {
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if !self.ready {
|
||||
t!("DIMCONTINUE No base dimension found. Place a dimension first.").into_owned()
|
||||
if self.base.is_none() {
|
||||
"DIMCONTINUE Select continued dimension:".to_string()
|
||||
} else if self
|
||||
.base
|
||||
.as_ref()
|
||||
.is_some_and(|base| matches!(&base.kind, ContinueKind::Ordinate { .. }))
|
||||
{
|
||||
"DIMCONTINUE Specify feature location [Undo/Select] <Select>:".to_string()
|
||||
} else {
|
||||
t!("DIMCONTINUE Specify a second extension line origin (Enter to exit):").into_owned()
|
||||
"DIMCONTINUE Specify second extension line origin [Select/Undo] <Select>:"
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
if !self.ready {
|
||||
return CmdResult::Cancel;
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
self.base.as_ref().map_or_else(Vec::new, |base| {
|
||||
if matches!(&base.kind, ContinueKind::Ordinate { .. }) {
|
||||
vec![CmdOption::new("Undo", "U"), CmdOption::new("Select", "S")]
|
||||
} else {
|
||||
vec![CmdOption::new("Select", "S"), CmdOption::new("Undo", "U")]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
self.base.is_none()
|
||||
}
|
||||
|
||||
fn entity_pick_highlights_hover(&self) -> bool {
|
||||
self.base.is_none()
|
||||
}
|
||||
|
||||
fn inject_before_entity_pick(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn inject_picked_entity(&mut self, entity: EntityType) {
|
||||
self.injected = Some(entity);
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, _handle: Handle, point: DVec3) -> CmdResult {
|
||||
let Some(entity) = self.injected.take() else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
let _ = self.install_base(&entity, Some(point));
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||
let Some(dimension) = self.build_dimension(point) else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
if let Some(base) = self.base.clone() {
|
||||
self.history.push(base);
|
||||
}
|
||||
let p1 = self.chain_p1;
|
||||
let p2 = pt;
|
||||
|
||||
// Build a new linear dimension.
|
||||
let mut dim = DimensionLinear::new(v3(p1), v3(p2));
|
||||
dim.rotation = self.rotation;
|
||||
if self.text_rotation.abs() > 1e-9 {
|
||||
dim.base.text_rotation = self.text_rotation;
|
||||
self.advance(point);
|
||||
CmdResult::CommitDimension {
|
||||
entity: EntityType::Dimension(dimension),
|
||||
association: DimensionAssociationInput::Infer(None),
|
||||
preserve_base_style: self.preserve_base_style,
|
||||
continue_command: true,
|
||||
}
|
||||
|
||||
// Dim line stays collinear with the base: project both extension
|
||||
// origins onto the base dim line's absolute perpendicular coordinate.
|
||||
// (A fixed offset from p1 drifts off the base line whenever p1 and p2
|
||||
// sit at different perpendicular positions.) (#151)
|
||||
let d1 = p1 + self.perp * (self.dim_line_perp - p1.dot(self.perp));
|
||||
let d2 = p2 + self.perp * (self.dim_line_perp - p2.dot(self.perp));
|
||||
dim.definition_point = v3(d1);
|
||||
dim.base.definition_point = v3(d1);
|
||||
dim.base.text_middle_point = v3((d1 + d2) * 0.5);
|
||||
dim.base.insertion_point = dim.base.text_middle_point;
|
||||
dim.base.actual_measurement = dim.measurement();
|
||||
|
||||
// Advance chain: next dim's P1 = this dim's P2.
|
||||
self.chain_p1 = p2;
|
||||
|
||||
CmdResult::CommitEntity(EntityType::Dimension(Dimension::Linear(dim)))
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||
if !self.ready {
|
||||
return None;
|
||||
if self.base.take().is_some() {
|
||||
CmdResult::NeedPoint
|
||||
} else {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
// Preview / rubber-band geometry feeds an f32 GPU buffer, so drop to f32
|
||||
// at this screen-only boundary.
|
||||
let pt = pt.as_vec3();
|
||||
let p1 = self.chain_p1.as_vec3();
|
||||
let perp = self.perp.as_vec3();
|
||||
let dim_line_perp = self.dim_line_perp as f32;
|
||||
let dim_line_pt = p1 + perp * (dim_line_perp - p1.dot(perp));
|
||||
let dim_line_pt2 = pt + perp * (dim_line_perp - pt.dot(perp));
|
||||
Some(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: "dimcont_preview".into(),
|
||||
points: vec![
|
||||
[p1.x, p1.y, p1.z],
|
||||
[dim_line_pt.x, dim_line_pt.y, dim_line_pt.z],
|
||||
[f32::NAN, 0.0, 0.0],
|
||||
[pt.x, pt.y, pt.z],
|
||||
[dim_line_pt2.x, dim_line_pt2.y, dim_line_pt2.z],
|
||||
[f32::NAN, 0.0, 0.0],
|
||||
[dim_line_pt.x, dim_line_pt.y, dim_line_pt.z],
|
||||
[dim_line_pt2.x, dim_line_pt2.y, dim_line_pt2.z],
|
||||
],
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
self.base.is_some()
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
self.base.is_some()
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
match text.trim().to_ascii_uppercase().as_str() {
|
||||
"S" | "SELECT" => {
|
||||
self.base = None;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"U" | "UNDO" => Some(self.undo_one()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_undo_step(&mut self) -> Option<CmdResult> {
|
||||
if self.history.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.undo_one())
|
||||
}
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
|
||||
self.build_dimension(point)
|
||||
.map(|dimension| preview_for_dimension(&dimension))
|
||||
}
|
||||
}
|
||||
|
||||
fn v3(p: DVec3) -> Vector3 {
|
||||
Vector3::new(p.x, p.y, p.z)
|
||||
fn build_linear(
|
||||
plane: Plane,
|
||||
first: Vec2,
|
||||
second: Vec2,
|
||||
rotation: f64,
|
||||
perpendicular: Vec2,
|
||||
line_coordinate: f64,
|
||||
) -> Dimension {
|
||||
let first_line = project_to_line(first, perpendicular, line_coordinate);
|
||||
let second_line = project_to_line(second, perpendicular, line_coordinate);
|
||||
let mut result = DimensionLinear::new(world(plane, first), world(plane, second));
|
||||
result.rotation = rotation;
|
||||
result.definition_point = world(plane, second_line);
|
||||
result.base.definition_point = result.definition_point;
|
||||
result.base.text_middle_point = world(plane, first_line.lerp(second_line, 0.5));
|
||||
result.base.insertion_point = result.base.text_middle_point;
|
||||
Dimension::Linear(result)
|
||||
}
|
||||
|
||||
fn selected_linear_end(first: Vec2, second: Vec2, pick: Option<Vec2>) -> Vec2 {
|
||||
pick.map_or(second, |point| {
|
||||
if point.distance_squared(first) <= point.distance_squared(second) {
|
||||
first
|
||||
} else {
|
||||
second
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["DIMCONTINUE"] }); // DimContinueCommand
|
||||
fn selected_angular_end(vertex: Vec2, ends: [Vec2; 2], pick: Option<Vec2>) -> Vec2 {
|
||||
let Some(pick) = pick else {
|
||||
return ends[0];
|
||||
};
|
||||
let rays = ends.map(|end| {
|
||||
Curve::Ray(Ray {
|
||||
origin: vertex.into(),
|
||||
direction: (end - vertex).into(),
|
||||
})
|
||||
});
|
||||
nearest_of(rays.iter(), pick.into())
|
||||
.map(|(index, _)| ends[index])
|
||||
.unwrap_or(ends[0])
|
||||
}
|
||||
|
||||
fn angular2_vertex(lines: [[Vec2; 2]; 2]) -> Option<Vec2> {
|
||||
let first = Curve::XLine(XLine {
|
||||
base: lines[0][0].into(),
|
||||
direction: (lines[0][1] - lines[0][0]).into(),
|
||||
});
|
||||
let second = Curve::XLine(XLine {
|
||||
base: lines[1][0].into(),
|
||||
direction: (lines[1][1] - lines[1][0]).into(),
|
||||
});
|
||||
let scale = (lines[0][1] - lines[0][0])
|
||||
.length()
|
||||
.max((lines[1][1] - lines[1][0]).length())
|
||||
.max(1.0);
|
||||
intersect(&first, &second, Tolerance::new(scale * 1.0e-9))
|
||||
.first()
|
||||
.map(|crossing| Vec2::from(crossing.point))
|
||||
}
|
||||
|
||||
fn angular_definition_and_text(
|
||||
vertex: Vec2,
|
||||
previous: Vec2,
|
||||
moving: Vec2,
|
||||
radius: f64,
|
||||
) -> Option<(Vec2, Vec2)> {
|
||||
let arc = angular_arc(vertex, previous, moving, radius)?;
|
||||
Some((
|
||||
Vec2::from(arc.point_at(2.0 / 3.0)),
|
||||
Vec2::from(arc.point_at(0.5)),
|
||||
))
|
||||
}
|
||||
|
||||
fn angular_arc(vertex: Vec2, first: Vec2, second: Vec2, radius: f64) -> Option<Curve> {
|
||||
let first = first.normalize()?;
|
||||
let second = second.normalize()?;
|
||||
if first.distance_squared(second) <= 1.0e-18 {
|
||||
return None;
|
||||
}
|
||||
let first_angle = first.angle();
|
||||
let second_angle = second.angle();
|
||||
let (start_angle, end_angle) =
|
||||
if arc_span(first_angle, second_angle) <= std::f64::consts::PI {
|
||||
(first_angle, second_angle)
|
||||
} else {
|
||||
(second_angle, first_angle)
|
||||
};
|
||||
Some(Curve::Arc(Arc {
|
||||
centre: vertex.into(),
|
||||
radius,
|
||||
start_angle,
|
||||
end_angle,
|
||||
}))
|
||||
}
|
||||
|
||||
fn refresh_measurement(dimension: &mut Dimension, plane: Plane) {
|
||||
match dimension {
|
||||
Dimension::Linear(value) => {
|
||||
let first = project(&plane, value.first_point);
|
||||
let second = project(&plane, value.second_point);
|
||||
let axis = Vec2::new(value.rotation.cos(), value.rotation.sin());
|
||||
value.base.actual_measurement = (second - first).dot(axis).abs();
|
||||
}
|
||||
Dimension::Angular2Ln(value) => {
|
||||
value.base.actual_measurement = value.measurement_degrees()
|
||||
}
|
||||
Dimension::Angular3Pt(value) => {
|
||||
value.base.actual_measurement = value.measurement_degrees()
|
||||
}
|
||||
Dimension::Ordinate(value) => value.refresh_measurement(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn preview_for_dimension(dimension: &Dimension) -> WireModel {
|
||||
let mut points = Vec::<[f64; 3]>::new();
|
||||
match dimension {
|
||||
Dimension::Linear(value) => {
|
||||
let plane = plane_from_normal(value.first_point, value.base.normal);
|
||||
linear_preview(
|
||||
&mut points,
|
||||
plane,
|
||||
project(&plane, value.first_point),
|
||||
project(&plane, value.second_point),
|
||||
value.rotation,
|
||||
project(&plane, value.definition_point),
|
||||
project(&plane, value.base.text_middle_point),
|
||||
value.base.actual_measurement,
|
||||
);
|
||||
}
|
||||
Dimension::Angular2Ln(value) => {
|
||||
let plane = plane_from_normal(value.first_point, value.base.normal);
|
||||
let lines = [
|
||||
[
|
||||
project(&plane, value.first_point),
|
||||
project(&plane, value.second_point),
|
||||
],
|
||||
[
|
||||
project(&plane, value.angle_vertex),
|
||||
project(&plane, value.definition_point),
|
||||
],
|
||||
];
|
||||
let vertex = angular2_vertex(lines).unwrap_or(lines[1][0]);
|
||||
angular_preview(
|
||||
&mut points,
|
||||
plane,
|
||||
vertex,
|
||||
lines[0][1] - vertex,
|
||||
lines[1][1] - vertex,
|
||||
project(&plane, value.dimension_arc),
|
||||
project(&plane, value.base.text_middle_point),
|
||||
value.base.actual_measurement,
|
||||
);
|
||||
}
|
||||
Dimension::Angular3Pt(value) => {
|
||||
let plane = plane_from_normal(value.angle_vertex, value.base.normal);
|
||||
let vertex = project(&plane, value.angle_vertex);
|
||||
angular_preview(
|
||||
&mut points,
|
||||
plane,
|
||||
vertex,
|
||||
project(&plane, value.first_point) - vertex,
|
||||
project(&plane, value.second_point) - vertex,
|
||||
project(&plane, value.definition_point),
|
||||
project(&plane, value.base.text_middle_point),
|
||||
value.base.actual_measurement,
|
||||
);
|
||||
}
|
||||
Dimension::Ordinate(value) => {
|
||||
let plane = plane_from_normal(value.definition_point, value.base.normal);
|
||||
ordinate_preview(
|
||||
&mut points,
|
||||
plane,
|
||||
project(&plane, value.feature_location),
|
||||
project(&plane, value.leader_endpoint),
|
||||
value.base.actual_measurement,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
WireModel::solid_f64(
|
||||
"dimcontinue_preview".to_string(),
|
||||
points,
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
fn linear_preview(
|
||||
points: &mut Vec<[f64; 3]>,
|
||||
plane: Plane,
|
||||
first: Vec2,
|
||||
second: Vec2,
|
||||
rotation: f64,
|
||||
definition: Vec2,
|
||||
text: Vec2,
|
||||
measurement: f64,
|
||||
) {
|
||||
let axis = Vec2::new(rotation.cos(), rotation.sin());
|
||||
let perpendicular = axis.perpendicular();
|
||||
let coordinate = definition.dot(perpendicular);
|
||||
let first_line = project_to_line(first, perpendicular, coordinate);
|
||||
let second_line = project_to_line(second, perpendicular, coordinate);
|
||||
segment(points, plane, first, first_line);
|
||||
segment(points, plane, second, second_line);
|
||||
segment(points, plane, first_line, second_line);
|
||||
arrow(
|
||||
points,
|
||||
plane,
|
||||
first_line,
|
||||
axis,
|
||||
perpendicular,
|
||||
first_line.distance(second_line),
|
||||
);
|
||||
arrow(
|
||||
points,
|
||||
plane,
|
||||
second_line,
|
||||
-axis,
|
||||
perpendicular,
|
||||
first_line.distance(second_line),
|
||||
);
|
||||
text_box(points, plane, text, axis, perpendicular, measurement);
|
||||
}
|
||||
|
||||
fn angular_preview(
|
||||
points: &mut Vec<[f64; 3]>,
|
||||
plane: Plane,
|
||||
vertex: Vec2,
|
||||
first: Vec2,
|
||||
second: Vec2,
|
||||
arc_point: Vec2,
|
||||
text: Vec2,
|
||||
measurement: f64,
|
||||
) {
|
||||
let radius = arc_point.distance(vertex).max(1.0e-9);
|
||||
let Some(first_direction) = first.normalize() else {
|
||||
return;
|
||||
};
|
||||
let Some(second_direction) = second.normalize() else {
|
||||
return;
|
||||
};
|
||||
let Some(arc) = angular_arc(vertex, first, second, radius) else {
|
||||
return;
|
||||
};
|
||||
segment(points, plane, vertex, vertex + first_direction * radius);
|
||||
segment(points, plane, vertex, vertex + second_direction * radius);
|
||||
for index in 0..=24 {
|
||||
push_local(points, plane, Vec2::from(arc.point_at(index as f64 / 24.0)));
|
||||
}
|
||||
points.push([f64::NAN; 3]);
|
||||
let start = Vec2::from(arc.point_at(0.0));
|
||||
let start_next = Vec2::from(arc.point_at(1.0 / 1024.0));
|
||||
let end = Vec2::from(arc.point_at(1.0));
|
||||
let end_previous = Vec2::from(arc.point_at(1.0 - 1.0 / 1024.0));
|
||||
arrow(
|
||||
points,
|
||||
plane,
|
||||
start,
|
||||
(start_next - start).normalize().unwrap_or(first_direction),
|
||||
first_direction,
|
||||
radius,
|
||||
);
|
||||
arrow(
|
||||
points,
|
||||
plane,
|
||||
end,
|
||||
(end_previous - end).normalize().unwrap_or(-second_direction),
|
||||
second_direction,
|
||||
radius,
|
||||
);
|
||||
let before_middle = Vec2::from(arc.point_at(0.5 - 1.0 / 1024.0));
|
||||
let after_middle = Vec2::from(arc.point_at(0.5 + 1.0 / 1024.0));
|
||||
let text_axis = (after_middle - before_middle)
|
||||
.normalize()
|
||||
.unwrap_or(Vec2::new(1.0, 0.0));
|
||||
text_box(
|
||||
points,
|
||||
plane,
|
||||
text,
|
||||
text_axis,
|
||||
text_axis.perpendicular(),
|
||||
measurement,
|
||||
);
|
||||
}
|
||||
|
||||
fn ordinate_preview(
|
||||
points: &mut Vec<[f64; 3]>,
|
||||
plane: Plane,
|
||||
feature: Vec2,
|
||||
leader: Vec2,
|
||||
measurement: f64,
|
||||
) {
|
||||
segment(points, plane, feature, leader);
|
||||
let axis = (leader - feature)
|
||||
.normalize()
|
||||
.unwrap_or(Vec2::new(0.0, 1.0));
|
||||
let perpendicular = axis.perpendicular();
|
||||
arrow(
|
||||
points,
|
||||
plane,
|
||||
feature,
|
||||
axis,
|
||||
perpendicular,
|
||||
feature.distance(leader),
|
||||
);
|
||||
text_box(
|
||||
points,
|
||||
plane,
|
||||
leader,
|
||||
perpendicular,
|
||||
axis,
|
||||
measurement,
|
||||
);
|
||||
}
|
||||
|
||||
fn arrow(
|
||||
points: &mut Vec<[f64; 3]>,
|
||||
plane: Plane,
|
||||
tip: Vec2,
|
||||
inward: Vec2,
|
||||
perpendicular: Vec2,
|
||||
span: f64,
|
||||
) {
|
||||
let inward = inward.normalize().unwrap_or(Vec2::new(1.0, 0.0));
|
||||
let perpendicular = perpendicular
|
||||
.normalize()
|
||||
.unwrap_or(Vec2::new(0.0, 1.0));
|
||||
let size = span.clamp(1.0, 100.0) * 0.035;
|
||||
let back = tip + inward * size;
|
||||
segment(
|
||||
points,
|
||||
plane,
|
||||
tip,
|
||||
back + perpendicular * size * 0.4,
|
||||
);
|
||||
segment(
|
||||
points,
|
||||
plane,
|
||||
tip,
|
||||
back - perpendicular * size * 0.4,
|
||||
);
|
||||
}
|
||||
|
||||
fn text_box(
|
||||
points: &mut Vec<[f64; 3]>,
|
||||
plane: Plane,
|
||||
center: Vec2,
|
||||
axis: Vec2,
|
||||
perpendicular: Vec2,
|
||||
measurement: f64,
|
||||
) {
|
||||
let digits = if measurement.abs() < 1.0 {
|
||||
1.0
|
||||
} else {
|
||||
measurement.abs().log10().floor() + 1.0
|
||||
};
|
||||
let half_width = (digits + 2.0) * 0.12;
|
||||
let half_height = 0.16;
|
||||
let a = center - axis * half_width - perpendicular * half_height;
|
||||
let b = center + axis * half_width - perpendicular * half_height;
|
||||
let c = center + axis * half_width + perpendicular * half_height;
|
||||
let d = center - axis * half_width + perpendicular * half_height;
|
||||
for point in [a, b, c, d, a] {
|
||||
push_local(points, plane, point);
|
||||
}
|
||||
points.push([f64::NAN; 3]);
|
||||
}
|
||||
|
||||
fn project_to_line(point: Vec2, perpendicular: Vec2, coordinate: f64) -> Vec2 {
|
||||
let line = Curve::XLine(XLine {
|
||||
base: (perpendicular * coordinate).into(),
|
||||
direction: perpendicular.perpendicular().into(),
|
||||
});
|
||||
Vec2::from(closest_point(&line, point.into()).point)
|
||||
}
|
||||
|
||||
fn plane_from_normal(origin: Vector3, normal: Vector3) -> Plane {
|
||||
let (x_axis, y_axis) =
|
||||
crate::scene::view::transform::ocs_axes((normal.x, normal.y, normal.z));
|
||||
Plane::from_axes(
|
||||
point(origin),
|
||||
[x_axis.0, x_axis.1, x_axis.2],
|
||||
[y_axis.0, y_axis.1, y_axis.2],
|
||||
)
|
||||
}
|
||||
|
||||
fn project(plane: &Plane, value: Vector3) -> Vec2 {
|
||||
Vec2::from(plane.project(point(value)).unwrap_or([0.0; 2]))
|
||||
}
|
||||
|
||||
fn world(plane: Plane, value: Vec2) -> Vector3 {
|
||||
let value = plane.point_at(value.into());
|
||||
Vector3::new(value[0], value[1], value[2])
|
||||
}
|
||||
|
||||
fn point(value: Vector3) -> [f64; 3] {
|
||||
[value.x, value.y, value.z]
|
||||
}
|
||||
|
||||
fn segment(points: &mut Vec<[f64; 3]>, plane: Plane, first: Vec2, second: Vec2) {
|
||||
push_local(points, plane, first);
|
||||
push_local(points, plane, second);
|
||||
points.push([f64::NAN; 3]);
|
||||
}
|
||||
|
||||
fn push_local(points: &mut Vec<[f64; 3]>, plane: Plane, point: Vec2) {
|
||||
points.push(plane.point_at(point.into()));
|
||||
}
|
||||
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["DIMCONTINUE"] });
|
||||
@@ -139,6 +139,8 @@ impl JoggedRadiusDimensionCommand {
|
||||
dimension,
|
||||
))),
|
||||
association,
|
||||
preserve_base_style: false,
|
||||
continue_command: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +187,8 @@ impl CadCommand for LinearDimensionCommand {
|
||||
CmdResult::CommitDimension {
|
||||
entity,
|
||||
association: DimensionAssociationInput::Infer(self.source_handle),
|
||||
preserve_base_style: false,
|
||||
continue_command: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// MLEADER: pick the arrow and elbow, then edit the text.
|
||||
|
||||
use acadrust::entities::MultiLeader;
|
||||
use acadrust::entities::{LeaderContentType, MultiLeader, MultiLeaderPathType};
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::EntityType;
|
||||
use glam::{DVec3, Mat4, Vec3};
|
||||
|
||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, WorkingPlane};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use crate::t;
|
||||
@@ -26,6 +26,47 @@ pub struct MLeaderCommand {
|
||||
plane: WorkingPlane,
|
||||
style: Option<acadrust::objects::MultiLeaderStyle>,
|
||||
display_scale: f64,
|
||||
order: CreationOrder,
|
||||
step: Step,
|
||||
content_type: LeaderContentType,
|
||||
path_type: MultiLeaderPathType,
|
||||
enable_landing: bool,
|
||||
landing_length: f64,
|
||||
max_points: usize,
|
||||
first_angle: Option<f64>,
|
||||
second_angle: Option<f64>,
|
||||
layer: String,
|
||||
text: String,
|
||||
picked_entity: Option<EntityType>,
|
||||
selected_mtext: Option<acadrust::entities::MText>,
|
||||
block_sources: Vec<(String, acadrust::Handle)>,
|
||||
block_handle: Option<acadrust::Handle>,
|
||||
layers: Vec<String>,
|
||||
text_styles: Vec<(String, acadrust::Handle)>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum CreationOrder {
|
||||
ArrowFirst,
|
||||
LandingFirst,
|
||||
ContentFirst,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Step {
|
||||
PickPoints,
|
||||
Options,
|
||||
LeaderType,
|
||||
Landing,
|
||||
LandingLength,
|
||||
ContentType,
|
||||
MaxPoints,
|
||||
FirstAngle,
|
||||
SecondAngle,
|
||||
Layer,
|
||||
PreEnterText,
|
||||
SelectMText,
|
||||
BlockSource,
|
||||
}
|
||||
|
||||
impl MLeaderCommand {
|
||||
@@ -35,6 +76,23 @@ impl MLeaderCommand {
|
||||
plane: WorkingPlane::default(),
|
||||
style: None,
|
||||
display_scale: 1.0,
|
||||
order: CreationOrder::ArrowFirst,
|
||||
step: Step::PickPoints,
|
||||
content_type: LeaderContentType::MText,
|
||||
path_type: MultiLeaderPathType::StraightLineSegments,
|
||||
enable_landing: true,
|
||||
landing_length: 2.5,
|
||||
max_points: 2,
|
||||
first_angle: None,
|
||||
second_angle: None,
|
||||
layer: String::new(),
|
||||
text: String::new(),
|
||||
picked_entity: None,
|
||||
selected_mtext: None,
|
||||
block_sources: Vec::new(),
|
||||
block_handle: None,
|
||||
layers: Vec::new(),
|
||||
text_styles: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,10 +108,214 @@ impl MLeaderCommand {
|
||||
Self {
|
||||
verts: Vec::new(),
|
||||
plane: WorkingPlane::default(),
|
||||
style: Some(style),
|
||||
style: Some(style.clone()),
|
||||
display_scale,
|
||||
order: CreationOrder::ArrowFirst,
|
||||
step: Step::PickPoints,
|
||||
content_type: (style.content_type as i16).into(),
|
||||
path_type: (style.path_type as i16).into(),
|
||||
enable_landing: style.enable_landing,
|
||||
landing_length: style.landing_distance,
|
||||
max_points: style.max_leader_points.max(2) as usize,
|
||||
first_angle: (style.first_segment_angle.abs() > 1.0e-12)
|
||||
.then_some(style.first_segment_angle),
|
||||
second_angle: (style.second_segment_angle.abs() > 1.0e-12)
|
||||
.then_some(style.second_segment_angle),
|
||||
layer: String::new(),
|
||||
text: style.default_text.clone(),
|
||||
picked_entity: None,
|
||||
selected_mtext: None,
|
||||
block_sources: Vec::new(),
|
||||
block_handle: style.block_content_handle,
|
||||
layers: Vec::new(),
|
||||
text_styles: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_drawing_resources(
|
||||
mut self,
|
||||
block_sources: Vec<(String, acadrust::Handle)>,
|
||||
layers: Vec<String>,
|
||||
text_styles: Vec<(String, acadrust::Handle)>,
|
||||
) -> Self {
|
||||
self.block_sources = block_sources;
|
||||
self.layers = layers;
|
||||
self.text_styles = text_styles;
|
||||
self
|
||||
}
|
||||
|
||||
fn point_limit(&self) -> usize {
|
||||
if self.order == CreationOrder::ContentFirst {
|
||||
2
|
||||
} else {
|
||||
self.max_points.max(2)
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(&self) -> CmdResult {
|
||||
if self.verts.len() < 2 {
|
||||
return CmdResult::Cancel;
|
||||
}
|
||||
let local: Vec<DVec3> = self
|
||||
.verts
|
||||
.iter()
|
||||
.map(|point| self.plane.to_local(*point))
|
||||
.collect();
|
||||
let (leader_points, content_point) = match self.order {
|
||||
CreationOrder::ArrowFirst => (local, None),
|
||||
CreationOrder::LandingFirst => {
|
||||
let mut points = local;
|
||||
points.reverse();
|
||||
(points, None)
|
||||
}
|
||||
CreationOrder::ContentFirst => {
|
||||
let content = local[0];
|
||||
let arrow = local[1];
|
||||
let sign = if content.x >= arrow.x { 1.0 } else { -1.0 };
|
||||
let elbow = DVec3::new(
|
||||
content.x - sign * self.landing_length * self.display_scale,
|
||||
content.y,
|
||||
content.z,
|
||||
);
|
||||
(vec![arrow, elbow], Some(content))
|
||||
}
|
||||
};
|
||||
let mut ml = build_mleader(
|
||||
&self.text,
|
||||
&leader_points,
|
||||
content_point,
|
||||
Mat4::IDENTITY,
|
||||
self.style.as_ref(),
|
||||
self.display_scale,
|
||||
);
|
||||
ml.content_type = self.content_type;
|
||||
ml.path_type = self.path_type;
|
||||
ml.enable_landing = self.enable_landing;
|
||||
ml.enable_dogleg = self.enable_landing;
|
||||
ml.dogleg_length = self.landing_length;
|
||||
if self.content_type == LeaderContentType::Block {
|
||||
ml.block_content_handle = self.block_handle;
|
||||
}
|
||||
if !self.layer.trim().is_empty() {
|
||||
ml.common.layer = format!("__MLEADER_LAYER__{}", self.layer.trim());
|
||||
}
|
||||
for root in &mut ml.context.leader_roots {
|
||||
root.landing_distance = self.landing_length * self.display_scale;
|
||||
for line in &mut root.lines {
|
||||
line.path_type = self.path_type;
|
||||
}
|
||||
}
|
||||
normalize_content_context(&mut ml);
|
||||
if let Some(source) = &self.selected_mtext {
|
||||
let scale = self.display_scale.max(1.0e-12);
|
||||
ml.text_height = source.height / scale;
|
||||
ml.context.text_height = source.height;
|
||||
ml.context.text_width = source.rectangle_width;
|
||||
ml.context.text_rotation = source.rotation;
|
||||
ml.context.text_direction = source.dwg_x_direction.unwrap_or_else(|| {
|
||||
Vector3::new(source.rotation.cos(), source.rotation.sin(), 0.0)
|
||||
});
|
||||
ml.context.line_spacing_factor = source.line_spacing_factor;
|
||||
ml.context.line_spacing_style = source.line_spacing_style;
|
||||
ml.text_color = source.common.color;
|
||||
ml.context.text_color = source.common.color;
|
||||
ml.text_frame = source.background_fill_flags & 0x10 != 0;
|
||||
ml.context.background_fill_enabled = source.background_fill_flags & 0x01 != 0;
|
||||
ml.context.background_mask_fill_on = source.background_fill_flags & 0x02 != 0;
|
||||
ml.context.background_scale_factor = source.background_scale;
|
||||
ml.context.background_fill_color = source.background_color;
|
||||
ml.context.background_transparency = source.background_transparency;
|
||||
ml.context.text_flow_direction = match source.drawing_direction {
|
||||
acadrust::entities::mtext::DrawingDirection::LeftToRight => {
|
||||
acadrust::entities::multileader::FlowDirectionType::Horizontal
|
||||
}
|
||||
acadrust::entities::mtext::DrawingDirection::TopToBottom => {
|
||||
acadrust::entities::multileader::FlowDirectionType::Vertical
|
||||
}
|
||||
acadrust::entities::mtext::DrawingDirection::ByStyle => {
|
||||
acadrust::entities::multileader::FlowDirectionType::ByStyle
|
||||
}
|
||||
};
|
||||
let (attachment, alignment) = match source.attachment_point {
|
||||
acadrust::entities::mtext::AttachmentPoint::TopCenter
|
||||
| acadrust::entities::mtext::AttachmentPoint::MiddleCenter
|
||||
| acadrust::entities::mtext::AttachmentPoint::BottomCenter => {
|
||||
(
|
||||
acadrust::entities::multileader::TextAttachmentPointType::Center,
|
||||
acadrust::entities::TextAlignmentType::Center,
|
||||
)
|
||||
}
|
||||
acadrust::entities::mtext::AttachmentPoint::TopRight
|
||||
| acadrust::entities::mtext::AttachmentPoint::MiddleRight
|
||||
| acadrust::entities::mtext::AttachmentPoint::BottomRight => {
|
||||
(
|
||||
acadrust::entities::multileader::TextAttachmentPointType::Right,
|
||||
acadrust::entities::TextAlignmentType::Right,
|
||||
)
|
||||
}
|
||||
_ => (
|
||||
acadrust::entities::multileader::TextAttachmentPointType::Left,
|
||||
acadrust::entities::TextAlignmentType::Left,
|
||||
),
|
||||
};
|
||||
ml.context.text_attachment_point = attachment;
|
||||
ml.text_alignment = alignment;
|
||||
ml.context.text_alignment = alignment;
|
||||
let style_handle = self
|
||||
.text_styles
|
||||
.iter()
|
||||
.find(|(name, _)| name.eq_ignore_ascii_case(&source.style))
|
||||
.map(|(_, handle)| *handle);
|
||||
ml.text_style_handle = style_handle;
|
||||
ml.context.text_style_handle = style_handle;
|
||||
}
|
||||
let entity = self.plane.place_entity(EntityType::MultiLeader(ml));
|
||||
if self.content_type == LeaderContentType::MText && self.text.is_empty() {
|
||||
CmdResult::CommitAndEditText(entity)
|
||||
} else {
|
||||
CmdResult::CommitAndExit(entity)
|
||||
}
|
||||
}
|
||||
|
||||
fn constrained_point(&self, point: DVec3) -> DVec3 {
|
||||
let Some(previous) = self.verts.last().copied() else {
|
||||
return point;
|
||||
};
|
||||
let segment_index = self.verts.len() - 1;
|
||||
let constraint = match (self.order, segment_index) {
|
||||
(CreationOrder::LandingFirst, 0) => self.second_angle,
|
||||
(CreationOrder::LandingFirst, 1) => self.first_angle,
|
||||
(_, 0) => self.first_angle,
|
||||
(_, 1) => self.second_angle,
|
||||
_ => None,
|
||||
};
|
||||
let Some(angle) = constraint else {
|
||||
return point;
|
||||
};
|
||||
let previous_local = self.plane.to_local(previous);
|
||||
let point_local = self.plane.to_local(point);
|
||||
let delta = point_local - previous_local;
|
||||
let length = delta.truncate().length();
|
||||
if length <= 1.0e-12 {
|
||||
return point;
|
||||
}
|
||||
let candidates = [angle, -angle, std::f64::consts::PI - angle, angle - std::f64::consts::PI];
|
||||
let current = delta.y.atan2(delta.x);
|
||||
let chosen = candidates
|
||||
.into_iter()
|
||||
.min_by(|a, b| {
|
||||
angle_delta(current, *a)
|
||||
.partial_cmp(&angle_delta(current, *b))
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
})
|
||||
.unwrap_or(angle);
|
||||
let local = DVec3::new(
|
||||
previous_local.x + length * chosen.cos(),
|
||||
previous_local.y + length * chosen.sin(),
|
||||
point_local.z,
|
||||
);
|
||||
self.plane.to_world(local)
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for MLeaderCommand {
|
||||
@@ -66,60 +328,86 @@ impl CadCommand for MLeaderCommand {
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if self.verts.is_empty() {
|
||||
t!("MLEADER Specify arrowhead point:").into_owned()
|
||||
} else {
|
||||
t!("MLEADER Specify landing point:").into_owned()
|
||||
match self.step {
|
||||
Step::Options => t!("MLEADER Options [Leader type/Landing/Content type/Max points/First angle/Second angle/Layer/Exit]:").into_owned(),
|
||||
Step::LeaderType => t!("MLEADER Leader type [Straight/Spline/None]:").into_owned(),
|
||||
Step::Landing => t!("MLEADER Leader landing [Yes/No]:").into_owned(),
|
||||
Step::LandingLength => t!("MLEADER Specify landing distance:").into_owned(),
|
||||
Step::ContentType => t!("MLEADER Content type [MText/Block/None]:").into_owned(),
|
||||
Step::MaxPoints => t!("MLEADER Enter maximum leader points:").into_owned(),
|
||||
Step::FirstAngle => t!("MLEADER First segment angle [Any/15/30/45/60/90]:").into_owned(),
|
||||
Step::SecondAngle => t!("MLEADER Second segment angle [Any/15/30/45/60/90]:").into_owned(),
|
||||
Step::Layer => t!("MLEADER Enter layer name:").into_owned(),
|
||||
Step::PreEnterText => t!("MLEADER Enter text:").into_owned(),
|
||||
Step::SelectMText => t!("MLEADER Select an MText object:").into_owned(),
|
||||
Step::BlockSource => t!("MLEADER Enter source block name:").into_owned(),
|
||||
Step::PickPoints if self.verts.is_empty() => match self.order {
|
||||
CreationOrder::ArrowFirst => t!("MLEADER Specify arrowhead point or [Landing first/Content first/Text/Select MText/Options]:").into_owned(),
|
||||
CreationOrder::LandingFirst => t!("MLEADER Specify landing point or [Arrowhead first/Content first/Text/Select MText/Options]:").into_owned(),
|
||||
CreationOrder::ContentFirst => t!("MLEADER Specify content location or [Arrowhead first/Landing first/Text/Select MText/Options]:").into_owned(),
|
||||
},
|
||||
Step::PickPoints if self.order == CreationOrder::ContentFirst => {
|
||||
t!("MLEADER Specify arrowhead point:").into_owned()
|
||||
}
|
||||
Step::PickPoints => t!("MLEADER Specify next leader point or press Enter to finish:").into_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
match self.step {
|
||||
Step::PickPoints if self.verts.is_empty() => vec![
|
||||
CmdOption::new("Arrowhead first", "A"),
|
||||
CmdOption::new("Landing first", "L"),
|
||||
CmdOption::new("Content first", "C"),
|
||||
CmdOption::new("Text", "T"),
|
||||
CmdOption::new("Select MText", "S"),
|
||||
CmdOption::new("Options", "O"),
|
||||
],
|
||||
Step::Options => vec![
|
||||
CmdOption::new("Leader type", "LT"),
|
||||
CmdOption::new("Landing", "LD"),
|
||||
CmdOption::new("Content type", "CT"),
|
||||
CmdOption::new("Max points", "M"),
|
||||
CmdOption::new("First angle", "F"),
|
||||
CmdOption::new("Second angle", "S"),
|
||||
CmdOption::new("Layer", "LA"),
|
||||
CmdOption::new("Exit", "X"),
|
||||
],
|
||||
Step::LeaderType => ["Straight", "Spline", "None"].into_iter().map(|v| CmdOption::new(v, v)).collect(),
|
||||
Step::Landing => ["Yes", "No"].into_iter().map(|v| CmdOption::new(v, v)).collect(),
|
||||
Step::ContentType => ["MText", "Block", "None"].into_iter().map(|v| CmdOption::new(v, v)).collect(),
|
||||
Step::BlockSource => self
|
||||
.block_sources
|
||||
.iter()
|
||||
.map(|(name, _)| CmdOption::new(name, name))
|
||||
.collect(),
|
||||
Step::Layer => self
|
||||
.layers
|
||||
.iter()
|
||||
.map(|name| CmdOption::new(name, name))
|
||||
.collect(),
|
||||
Step::FirstAngle | Step::SecondAngle => ["Any", "15", "30", "45", "60", "90"].into_iter().map(|v| CmdOption::new(v, v)).collect(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
self.verts.push(pt);
|
||||
|
||||
if self.verts.len() < 2 {
|
||||
if self.step != Step::PickPoints {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
self.verts.push(self.constrained_point(pt));
|
||||
|
||||
let local: Vec<DVec3> = self
|
||||
.verts
|
||||
.iter()
|
||||
.map(|point| self.plane.to_local(*point))
|
||||
.collect();
|
||||
|
||||
let ml = build_mleader(
|
||||
"",
|
||||
&local,
|
||||
Mat4::IDENTITY,
|
||||
self.style.as_ref(),
|
||||
self.display_scale,
|
||||
);
|
||||
|
||||
CmdResult::CommitAndEditText(
|
||||
self.plane.place_entity(EntityType::MultiLeader(ml)),
|
||||
)
|
||||
if self.verts.len() < self.point_limit() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
self.finish()
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
if self.verts.len() < 2 {
|
||||
if self.step != Step::PickPoints || self.verts.len() < 2 {
|
||||
return CmdResult::Cancel;
|
||||
}
|
||||
// Place the leader with empty text, then open the in-place MText editor
|
||||
// so the user types the annotation into the rich editor.
|
||||
let local: Vec<DVec3> = self
|
||||
.verts
|
||||
.iter()
|
||||
.map(|point| self.plane.to_local(*point))
|
||||
.collect();
|
||||
let ml = build_mleader(
|
||||
"",
|
||||
&local,
|
||||
Mat4::IDENTITY,
|
||||
self.style.as_ref(),
|
||||
self.display_scale,
|
||||
);
|
||||
CmdResult::CommitAndEditText(
|
||||
self.plane.place_entity(EntityType::MultiLeader(ml)),
|
||||
)
|
||||
self.finish()
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
@@ -127,16 +415,37 @@ impl CadCommand for MLeaderCommand {
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||
if self.verts.is_empty() {
|
||||
if self.step != Step::PickPoints || self.verts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
// Preview / rubber-band is GPU screen-space: downcast to f32 here.
|
||||
let mut pts: Vec<Vec3> = self
|
||||
let mut local_points: Vec<DVec3> = self
|
||||
.verts
|
||||
.iter()
|
||||
.map(|point| self.plane.to_local(*point).as_vec3())
|
||||
.map(|point| self.plane.to_local(*point))
|
||||
.collect();
|
||||
pts.push(self.plane.to_local(pt).as_vec3());
|
||||
local_points.push(self.plane.to_local(self.constrained_point(pt)));
|
||||
let pts: Vec<Vec3> = match self.order {
|
||||
CreationOrder::ArrowFirst => local_points,
|
||||
CreationOrder::LandingFirst => {
|
||||
local_points.reverse();
|
||||
local_points
|
||||
}
|
||||
CreationOrder::ContentFirst => {
|
||||
let content = local_points[0];
|
||||
let arrow = *local_points.last().unwrap_or(&content);
|
||||
let sign = if content.x >= arrow.x { 1.0 } else { -1.0 };
|
||||
let elbow = DVec3::new(
|
||||
content.x - sign * self.landing_length * self.display_scale,
|
||||
content.y,
|
||||
content.z,
|
||||
);
|
||||
vec![arrow, elbow]
|
||||
}
|
||||
}
|
||||
.into_iter()
|
||||
.map(DVec3::as_vec3)
|
||||
.collect();
|
||||
let arrow_size = self
|
||||
.style
|
||||
.as_ref()
|
||||
@@ -159,6 +468,178 @@ impl CadCommand for MLeaderCommand {
|
||||
.collect();
|
||||
Some(preview)
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn wants_text_with_spaces(&self) -> bool {
|
||||
matches!(self.step, Step::Layer | Step::PreEnterText)
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
self.step == Step::PickPoints
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, input: &str) -> Option<CmdResult> {
|
||||
let text = input.trim();
|
||||
let upper = text.to_ascii_uppercase();
|
||||
match self.step {
|
||||
Step::PickPoints if self.verts.is_empty() => match upper.as_str() {
|
||||
"A" | "ARROWHEAD" | "ARROWHEAD FIRST" => self.order = CreationOrder::ArrowFirst,
|
||||
"L" | "LANDING" | "LANDING FIRST" => self.order = CreationOrder::LandingFirst,
|
||||
"C" | "CONTENT" | "CONTENT FIRST" => self.order = CreationOrder::ContentFirst,
|
||||
"T" | "TEXT" => self.step = Step::PreEnterText,
|
||||
"S" | "SELECT" | "SELECT MTEXT" => self.step = Step::SelectMText,
|
||||
"O" | "OPTIONS" => self.step = Step::Options,
|
||||
_ => return None,
|
||||
},
|
||||
Step::Options => match upper.as_str() {
|
||||
"LT" | "LEADER TYPE" => self.step = Step::LeaderType,
|
||||
"LD" | "LANDING" => self.step = Step::Landing,
|
||||
"CT" | "CONTENT TYPE" => self.step = Step::ContentType,
|
||||
"M" | "MAX" | "MAX POINTS" => self.step = Step::MaxPoints,
|
||||
"F" | "FIRST" | "FIRST ANGLE" => self.step = Step::FirstAngle,
|
||||
"S" | "SECOND" | "SECOND ANGLE" => self.step = Step::SecondAngle,
|
||||
"LA" | "LAYER" => self.step = Step::Layer,
|
||||
"X" | "EXIT" => self.step = Step::PickPoints,
|
||||
_ => return None,
|
||||
},
|
||||
Step::LeaderType => {
|
||||
self.path_type = match upper.as_str() {
|
||||
"SPLINE" => MultiLeaderPathType::Spline,
|
||||
"NONE" => MultiLeaderPathType::Invisible,
|
||||
"STRAIGHT" => MultiLeaderPathType::StraightLineSegments,
|
||||
_ => return None,
|
||||
};
|
||||
self.step = Step::Options;
|
||||
}
|
||||
Step::Landing => match upper.as_str() {
|
||||
"Y" | "YES" => {
|
||||
self.enable_landing = true;
|
||||
self.step = Step::LandingLength;
|
||||
}
|
||||
"N" | "NO" => {
|
||||
self.enable_landing = false;
|
||||
self.step = Step::Options;
|
||||
}
|
||||
_ => return None,
|
||||
},
|
||||
Step::LandingLength => {
|
||||
let value = crate::entities::common::parse_typed_length(text)?;
|
||||
if value <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
self.landing_length = value;
|
||||
self.step = Step::Options;
|
||||
}
|
||||
Step::ContentType => {
|
||||
self.content_type = match upper.as_str() {
|
||||
"MTEXT" => LeaderContentType::MText,
|
||||
"BLOCK" => LeaderContentType::Block,
|
||||
"NONE" => LeaderContentType::None,
|
||||
_ => return None,
|
||||
};
|
||||
self.step = if self.content_type == LeaderContentType::Block
|
||||
&& self.block_handle.is_none()
|
||||
{
|
||||
Step::BlockSource
|
||||
} else {
|
||||
Step::Options
|
||||
};
|
||||
}
|
||||
Step::MaxPoints => {
|
||||
let value = text.parse::<usize>().ok()?;
|
||||
if value < 2 {
|
||||
return None;
|
||||
}
|
||||
self.max_points = value;
|
||||
self.step = Step::Options;
|
||||
}
|
||||
Step::FirstAngle | Step::SecondAngle => {
|
||||
let angle = if upper == "ANY" {
|
||||
None
|
||||
} else {
|
||||
let degrees = text.parse::<f64>().ok()?;
|
||||
if !matches!(degrees as i32, 15 | 30 | 45 | 60 | 90) {
|
||||
return None;
|
||||
}
|
||||
Some(degrees.to_radians())
|
||||
};
|
||||
if self.step == Step::FirstAngle {
|
||||
self.first_angle = angle;
|
||||
} else {
|
||||
self.second_angle = angle;
|
||||
}
|
||||
self.step = Step::Options;
|
||||
}
|
||||
Step::Layer => {
|
||||
if text.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let Some(layer) = self
|
||||
.layers
|
||||
.iter()
|
||||
.find(|layer| layer.eq_ignore_ascii_case(text))
|
||||
.cloned()
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
self.layer = layer;
|
||||
self.step = Step::Options;
|
||||
}
|
||||
Step::PreEnterText => {
|
||||
self.text = input.to_string();
|
||||
self.content_type = LeaderContentType::MText;
|
||||
self.step = Step::PickPoints;
|
||||
}
|
||||
Step::SelectMText => return None,
|
||||
Step::BlockSource => {
|
||||
let Some((_, handle)) = self
|
||||
.block_sources
|
||||
.iter()
|
||||
.find(|(name, _)| name.eq_ignore_ascii_case(text))
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
self.block_handle = Some(*handle);
|
||||
self.content_type = LeaderContentType::Block;
|
||||
self.step = Step::Options;
|
||||
}
|
||||
Step::PickPoints => return None,
|
||||
}
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
self.step == Step::SelectMText
|
||||
}
|
||||
|
||||
fn inject_before_entity_pick(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn inject_picked_entity(&mut self, entity: EntityType) {
|
||||
self.picked_entity = Some(entity);
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, _handle: acadrust::Handle, _point: DVec3) -> CmdResult {
|
||||
let Some(EntityType::MText(text)) = self.picked_entity.take() else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
self.verts.clear();
|
||||
self.verts.push(DVec3::new(
|
||||
text.insertion_point.x,
|
||||
text.insertion_point.y,
|
||||
text.insertion_point.z,
|
||||
));
|
||||
self.text = text.value.clone();
|
||||
self.selected_mtext = Some(text);
|
||||
self.content_type = LeaderContentType::MText;
|
||||
self.order = CreationOrder::ContentFirst;
|
||||
self.step = Step::PickPoints;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────────────
|
||||
@@ -170,18 +651,24 @@ fn v3(p: DVec3) -> Vector3 {
|
||||
fn build_mleader(
|
||||
text: &str,
|
||||
verts: &[DVec3],
|
||||
content_point: Option<DVec3>,
|
||||
ucs: Mat4,
|
||||
style: Option<&acadrust::objects::MultiLeaderStyle>,
|
||||
display_scale: f64,
|
||||
) -> MultiLeader {
|
||||
// First click is the arrow; second click is the elbow.
|
||||
// First point is the arrow, last point is the elbow. Intermediate points
|
||||
// are leader vertices governed by the configured maximum-point count.
|
||||
let arrow_pt = verts[0];
|
||||
let elbow_pt = verts[1];
|
||||
let elbow_pt = *verts.last().unwrap_or(&verts[0]);
|
||||
|
||||
let arrow_v3 = v3(arrow_pt);
|
||||
let elbow_v3 = v3(elbow_pt);
|
||||
|
||||
let mut ml = MultiLeader::with_text(text, elbow_v3, vec![arrow_v3]);
|
||||
let leader_vertices = verts[..verts.len().saturating_sub(1)]
|
||||
.iter()
|
||||
.copied()
|
||||
.map(v3)
|
||||
.collect();
|
||||
let mut ml = MultiLeader::with_text(text, elbow_v3, leader_vertices);
|
||||
if let Some(style) = style {
|
||||
crate::scene::annotative::apply_mleader_style(&mut ml, style);
|
||||
} else {
|
||||
@@ -225,11 +712,13 @@ fn build_mleader(
|
||||
// Place text beyond the landing and gap.
|
||||
let off = landing * (landing_distance + landing_gap) as f32;
|
||||
|
||||
let text_location = Vector3::new(
|
||||
elbow_v3.x + off.x as f64,
|
||||
elbow_v3.y + off.y as f64,
|
||||
elbow_v3.z,
|
||||
);
|
||||
let text_location = content_point.map(v3).unwrap_or_else(|| {
|
||||
Vector3::new(
|
||||
elbow_v3.x + off.x as f64,
|
||||
elbow_v3.y + off.y as f64,
|
||||
elbow_v3.z,
|
||||
)
|
||||
});
|
||||
|
||||
ml.context.text_location = text_location;
|
||||
ml.context.content_base_point = text_location;
|
||||
@@ -237,6 +726,34 @@ fn build_mleader(
|
||||
ml
|
||||
}
|
||||
|
||||
fn normalize_content_context(ml: &mut MultiLeader) {
|
||||
match ml.content_type {
|
||||
LeaderContentType::MText => {
|
||||
ml.context.has_text_contents = true;
|
||||
ml.context.has_block_contents = false;
|
||||
}
|
||||
LeaderContentType::Block => {
|
||||
ml.context.has_text_contents = false;
|
||||
ml.context.has_block_contents = ml.block_content_handle.is_some();
|
||||
ml.context.block_content_handle = ml.block_content_handle;
|
||||
ml.context.block_content_location = ml.context.content_base_point;
|
||||
ml.context.block_content_scale = ml.block_scale;
|
||||
ml.context.block_rotation = ml.block_rotation;
|
||||
ml.context.block_content_color = ml.block_content_color;
|
||||
ml.context.block_connection_type = ml.block_connection_type;
|
||||
}
|
||||
_ => {
|
||||
ml.context.has_text_contents = false;
|
||||
ml.context.has_block_contents = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn angle_delta(a: f64, b: f64) -> f64 {
|
||||
let two_pi = std::f64::consts::TAU;
|
||||
((a - b + std::f64::consts::PI).rem_euclid(two_pi) - std::f64::consts::PI).abs()
|
||||
}
|
||||
|
||||
fn preview_wire(pts: &[Vec3], arrow_size: f32) -> WireModel {
|
||||
let mut points: Vec<[f32; 3]> = pts.iter().map(|p| [p.x, p.y, p.z]).collect();
|
||||
if pts.len() >= 2 {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
use acadrust::entities::{LeaderLine, MultiLeader};
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::{EntityType, Handle};
|
||||
use cadkernel::geom2d::{closest_point, Curve, Line};
|
||||
use glam::{DVec3, Vec3};
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
@@ -92,6 +93,9 @@ impl CadCommand for MLeaderAddCommand {
|
||||
match &mut self.step {
|
||||
AddStep::PickArrowhead { handle, entity } => {
|
||||
if let Some(ent) = entity.take() {
|
||||
if !matches!(ent, EntityType::MultiLeader(_)) {
|
||||
return CmdResult::Cancel;
|
||||
}
|
||||
let h = *handle;
|
||||
self.step = AddStep::CollectPoints {
|
||||
handle: h,
|
||||
@@ -121,14 +125,46 @@ impl CadCommand for MLeaderAddCommand {
|
||||
return CmdResult::Cancel;
|
||||
}
|
||||
let h = *handle;
|
||||
if let EntityType::MultiLeader(ref mut ml) = entity {
|
||||
// Add a new leader root with the collected points
|
||||
if let EntityType::MultiLeader(ref mut ml) = entity {
|
||||
let points: Vec<Vector3> = pts
|
||||
.iter()
|
||||
.map(|p| Vector3::new(p.x, p.y, p.z))
|
||||
.collect();
|
||||
let template_root = ml.context.leader_roots.first().cloned();
|
||||
let template_line = template_root
|
||||
.as_ref()
|
||||
.and_then(|root| root.lines.first())
|
||||
.cloned();
|
||||
let path_type = ml.path_type;
|
||||
let line_color = ml.line_color;
|
||||
let line_type_handle = ml.line_type_handle;
|
||||
let line_weight = ml.line_weight;
|
||||
let arrowhead_handle = ml.arrowhead_handle;
|
||||
let arrowhead_size = ml.arrowhead_size;
|
||||
let root = ml.context.add_leader_root();
|
||||
root.create_line(points);
|
||||
if let Some(template) = template_root {
|
||||
root.connection_point = template.connection_point;
|
||||
root.direction = template.direction;
|
||||
root.landing_distance = template.landing_distance;
|
||||
root.text_attachment_direction = template.text_attachment_direction;
|
||||
}
|
||||
let line = root.create_line(points);
|
||||
if let Some(template) = template_line {
|
||||
line.path_type = template.path_type;
|
||||
line.line_color = template.line_color;
|
||||
line.line_type_handle = template.line_type_handle;
|
||||
line.line_weight = template.line_weight;
|
||||
line.arrowhead_handle = template.arrowhead_handle;
|
||||
line.arrowhead_size = template.arrowhead_size;
|
||||
line.override_flags = template.override_flags;
|
||||
} else {
|
||||
line.path_type = path_type;
|
||||
line.line_color = line_color;
|
||||
line.line_type_handle = line_type_handle;
|
||||
line.line_weight = line_weight;
|
||||
line.arrowhead_handle = arrowhead_handle;
|
||||
line.arrowhead_size = arrowhead_size;
|
||||
}
|
||||
}
|
||||
let updated = std::mem::replace(entity, EntityType::XLine(Default::default()));
|
||||
return CmdResult::ReplaceEntity(h, vec![updated]);
|
||||
@@ -223,26 +259,43 @@ impl CadCommand for MLeaderRemoveCommand {
|
||||
if let Some(mut ent) = entity.take() {
|
||||
let h = *handle;
|
||||
if let EntityType::MultiLeader(ref mut ml) = ent {
|
||||
// Remove the leader root whose first point is closest to `pt`
|
||||
let pick = Vector3::new(pt.x as f64, pt.y as f64, pt.z as f64);
|
||||
let best = ml
|
||||
.context
|
||||
.leader_roots
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, root)| {
|
||||
root.lines.first().and_then(|l| l.points.first()).map(|p| {
|
||||
let dx = p.x - pick.x;
|
||||
let dz = p.z - pick.z;
|
||||
(i, dx * dx + dz * dz)
|
||||
.flat_map(|(root_index, root)| {
|
||||
root.lines.iter().enumerate().map(move |(line_index, line)| {
|
||||
let mut distance = line
|
||||
.points
|
||||
.windows(2)
|
||||
.map(|segment| point_segment_distance_xy(pick, segment[0], segment[1]))
|
||||
.fold(f64::INFINITY, f64::min);
|
||||
if let Some(last) = line.points.last().copied() {
|
||||
distance = distance.min(point_segment_distance_xy(
|
||||
pick,
|
||||
last,
|
||||
root.connection_point,
|
||||
));
|
||||
}
|
||||
(root_index, line_index, distance)
|
||||
})
|
||||
})
|
||||
.min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||
if let Some((idx, _)) = best {
|
||||
if ml.context.leader_roots.len() > 1 {
|
||||
ml.context.leader_roots.remove(idx);
|
||||
}
|
||||
.min_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal));
|
||||
let Some((root_index, line_index, _)) = best else {
|
||||
return CmdResult::Cancel;
|
||||
};
|
||||
if ml.total_leader_line_count() <= 1 {
|
||||
return CmdResult::Cancel;
|
||||
}
|
||||
let root = &mut ml.context.leader_roots[root_index];
|
||||
root.lines.remove(line_index);
|
||||
if root.lines.is_empty() {
|
||||
ml.context.leader_roots.remove(root_index);
|
||||
}
|
||||
} else {
|
||||
return CmdResult::Cancel;
|
||||
}
|
||||
return CmdResult::ReplaceEntity(h, vec![ent]);
|
||||
}
|
||||
@@ -338,25 +391,11 @@ impl CadCommand for MLeaderAlignCommand {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
AlignStep::PickEndDir { handles, from } => {
|
||||
let h = handles.clone();
|
||||
let f = *from;
|
||||
// Emit sentinel for commands.rs
|
||||
use acadrust::entities::XLine;
|
||||
let mut xl = XLine::default();
|
||||
let hstr: Vec<String> = h.iter().map(|hh| hh.value().to_string()).collect();
|
||||
xl.common.layer = format!(
|
||||
"__MLEADERALIGN__{};{:.4},{:.4};{:.4},{:.4}",
|
||||
hstr.join(","),
|
||||
f.x,
|
||||
f.z,
|
||||
pt.x,
|
||||
pt.z
|
||||
);
|
||||
// Use first handle as the "replaced" entity (sentinel)
|
||||
if let Some(&first) = h.first() {
|
||||
return CmdResult::ReplaceEntity(first, vec![EntityType::XLine(xl)]);
|
||||
CmdResult::AlignMLeaders {
|
||||
handles: handles.clone(),
|
||||
from: *from,
|
||||
to: pt,
|
||||
}
|
||||
CmdResult::Cancel
|
||||
}
|
||||
_ => CmdResult::NeedPoint,
|
||||
}
|
||||
@@ -431,21 +470,10 @@ impl CadCommand for MLeaderCollectCommand {
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
if let CollectStep::PickLocation { handles } = &self.step {
|
||||
let h = handles.clone();
|
||||
// Build a new combined multileader at the collection point, delete originals.
|
||||
// The first handle is the "base" — the rest are erased and their leaders merged.
|
||||
use acadrust::entities::XLine;
|
||||
let mut xl = XLine::default();
|
||||
let hstr: Vec<String> = h.iter().map(|hh| hh.value().to_string()).collect();
|
||||
xl.common.layer = format!(
|
||||
"__MLEADERCOLLECT__{};{:.4},{:.4}",
|
||||
hstr.join(","),
|
||||
pt.x,
|
||||
pt.z
|
||||
);
|
||||
if let Some(&first) = h.first() {
|
||||
return CmdResult::ReplaceEntity(first, vec![EntityType::XLine(xl)]);
|
||||
}
|
||||
return CmdResult::CollectMLeaders {
|
||||
handles: handles.clone(),
|
||||
point: pt,
|
||||
};
|
||||
}
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
@@ -496,6 +524,17 @@ fn preview_wire(pts: &[Vec3]) -> WireModel {
|
||||
}
|
||||
}
|
||||
|
||||
fn point_segment_distance_xy(point: Vector3, start: Vector3, end: Vector3) -> f64 {
|
||||
closest_point(
|
||||
&Curve::Line(Line {
|
||||
start: [start.x, start.y],
|
||||
end: [end.x, end.y],
|
||||
}),
|
||||
[point.x, point.y],
|
||||
)
|
||||
.distance
|
||||
}
|
||||
|
||||
// Silence unused-import warning for MultiLeader and LeaderLine if not used in all paths
|
||||
fn _uses_ml_types(_ml: &MultiLeader, _ll: &LeaderLine) {}
|
||||
|
||||
|
||||
@@ -116,6 +116,8 @@ impl CadCommand for OrdinateDimCommand {
|
||||
Dimension::Ordinate(dim),
|
||||
)),
|
||||
association: DimensionAssociationInput::Infer(None),
|
||||
preserve_base_style: false,
|
||||
continue_command: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+362
-105
@@ -1,17 +1,15 @@
|
||||
// QDIM command — quick dimension: select objects then pick placement line.
|
||||
//
|
||||
// Workflow:
|
||||
// 1. Select objects (window/click, Enter to finish selection)
|
||||
// 2. Pick a point that defines the dimension-line position
|
||||
//
|
||||
// Creates one linear dimension per detected endpoint pair on the selected objects.
|
||||
//! Quick dimension creation from a selected set of drawing entities.
|
||||
|
||||
use acadrust::entities::{Dimension, DimensionLinear};
|
||||
use acadrust::entities::{Dimension, DimensionDiameter, DimensionLinear, DimensionOrdinate, DimensionRadius};
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::{EntityType, Handle};
|
||||
use cadkernel::geom2d::{characteristic_points, intersect, Curve, SnapKind, Tolerance};
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{
|
||||
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, DimensionAssociationSource,
|
||||
EntityTransform, SelectionEntity, WorkingPlane,
|
||||
};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use crate::t;
|
||||
@@ -27,129 +25,388 @@ pub fn tool() -> ToolDef {
|
||||
}
|
||||
}
|
||||
|
||||
enum Step {
|
||||
Gathering,
|
||||
PlaceLine { handles: Vec<Handle> },
|
||||
}
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Step { Gathering, Place, Datum, Edit, Settings }
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Mode { Continuous, Staggered, Baseline, Ordinate, Radius, Diameter }
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum SnapPriority { Endpoints, Intersections }
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SourceCurve { handle: Handle, curve: Curve }
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Candidate { point: DVec3, sources: Vec<DimensionAssociationSource> }
|
||||
|
||||
pub struct QdimCommand {
|
||||
step: Step,
|
||||
mode: Mode,
|
||||
snap_priority: SnapPriority,
|
||||
plane: WorkingPlane,
|
||||
selection: Vec<SelectionEntity>,
|
||||
datum: Option<DVec3>,
|
||||
excluded: Vec<DVec3>,
|
||||
dim_spacing: f64,
|
||||
}
|
||||
|
||||
impl QdimCommand {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(selection: Vec<SelectionEntity>, dim_spacing: f64, snap_priority: u8) -> Self {
|
||||
let step = if selection.is_empty() { Step::Gathering } else { Step::Place };
|
||||
Self {
|
||||
step: Step::Gathering,
|
||||
step,
|
||||
mode: Mode::Continuous,
|
||||
snap_priority: if snap_priority == 1 { SnapPriority::Intersections } else { SnapPriority::Endpoints },
|
||||
plane: WorkingPlane::default(),
|
||||
selection,
|
||||
datum: None,
|
||||
excluded: Vec::new(),
|
||||
dim_spacing: dim_spacing.abs().max(1e-9),
|
||||
}
|
||||
}
|
||||
|
||||
fn local_sources(&self) -> Vec<SourceCurve> {
|
||||
let transform = EntityTransform::Affine(self.plane.to_local_transform());
|
||||
self.selection.iter().filter_map(|selected| {
|
||||
let mut entity = selected.entity.clone();
|
||||
crate::scene::view::dispatch::apply_transform(&mut entity, &transform);
|
||||
crate::entities::curve::entity_curve_xy(&entity)
|
||||
.map(|curve| SourceCurve { handle: selected.handle, curve })
|
||||
}).collect()
|
||||
}
|
||||
|
||||
fn candidates(&self) -> Vec<Candidate> {
|
||||
let curves = self.local_sources();
|
||||
let mut candidates = if self.snap_priority == SnapPriority::Intersections {
|
||||
let mut hits = Vec::new();
|
||||
for first in 0..curves.len() {
|
||||
for second in first + 1..curves.len() {
|
||||
for crossing in intersect(&curves[first].curve, &curves[second].curve, Tolerance::default()) {
|
||||
hits.push(Candidate {
|
||||
point: DVec3::new(crossing.point[0], crossing.point[1], 0.0),
|
||||
// Intersections depend on two curves, but each
|
||||
// extension origin stores one source.
|
||||
sources: Vec::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
hits
|
||||
} else { Vec::new() };
|
||||
|
||||
if candidates.is_empty() {
|
||||
for source in &curves {
|
||||
for point in characteristic_points(&source.curve).into_iter().filter(|point| point.kind == SnapKind::Endpoint) {
|
||||
candidates.push(Candidate {
|
||||
point: DVec3::new(point.point[0], point.point[1], 0.0),
|
||||
sources: vec![DimensionAssociationSource::inferred(source.handle)],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for selected in &self.selection {
|
||||
if let EntityType::Dimension(dimension) = &selected.entity {
|
||||
for point in dimension_reference_points(dimension) {
|
||||
candidates.push(Candidate {
|
||||
point: self.plane.to_local(point),
|
||||
sources: vec![DimensionAssociationSource::inferred(selected.handle)],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tolerance = candidate_tolerance(candidates.iter().map(|candidate| candidate.point));
|
||||
let mut unique: Vec<Candidate> = Vec::new();
|
||||
for candidate in candidates {
|
||||
if let Some(existing) = unique.iter_mut().find(|existing| existing.point.distance(candidate.point) <= tolerance) {
|
||||
for source in candidate.sources {
|
||||
if !existing.sources.iter().any(|current| current.handle == source.handle) {
|
||||
existing.sources.push(source);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unique.push(candidate);
|
||||
}
|
||||
}
|
||||
unique.retain(|candidate| !self.excluded.iter().any(|point| point.distance(candidate.point) <= tolerance));
|
||||
unique
|
||||
}
|
||||
|
||||
fn radial_sources(&self) -> Vec<(Handle, Curve)> {
|
||||
self.local_sources().into_iter()
|
||||
.filter(|source| matches!(source.curve, Curve::Circle(_) | Curve::Arc(_)))
|
||||
.map(|source| (source.handle, source.curve)).collect()
|
||||
}
|
||||
|
||||
fn orientation(&self, points: &[Candidate], place: DVec3) -> bool {
|
||||
let min_x = points.iter().map(|point| point.point.x).fold(f64::INFINITY, f64::min);
|
||||
let max_x = points.iter().map(|point| point.point.x).fold(f64::NEG_INFINITY, f64::max);
|
||||
let min_y = points.iter().map(|point| point.point.y).fold(f64::INFINITY, f64::min);
|
||||
let max_y = points.iter().map(|point| point.point.y).fold(f64::NEG_INFINITY, f64::max);
|
||||
let outside_y = (place.y - max_y).max(min_y - place.y).max(0.0);
|
||||
let outside_x = (place.x - max_x).max(min_x - place.x).max(0.0);
|
||||
if (outside_x - outside_y).abs() > 1e-9 { outside_y >= outside_x } else { max_x - min_x >= max_y - min_y }
|
||||
}
|
||||
|
||||
fn build_dimensions(&self, place_world: DVec3) -> Vec<(EntityType, DimensionAssociationInput)> {
|
||||
let place = self.plane.to_local(place_world);
|
||||
if matches!(self.mode, Mode::Radius | Mode::Diameter) {
|
||||
return self.build_radial_dimensions(place);
|
||||
}
|
||||
let mut points = self.candidates();
|
||||
if points.is_empty() { return Vec::new(); }
|
||||
let horizontal = self.orientation(&points, place);
|
||||
points.sort_by(|first, second| if horizontal { first.point.x.total_cmp(&second.point.x) } else { first.point.y.total_cmp(&second.point.y) });
|
||||
let tolerance = candidate_tolerance(points.iter().map(|point| point.point));
|
||||
points.dedup_by(|first, second| {
|
||||
let delta = if horizontal { first.point.x - second.point.x } else { first.point.y - second.point.y };
|
||||
delta.abs() <= tolerance
|
||||
});
|
||||
|
||||
match self.mode {
|
||||
Mode::Continuous | Mode::Staggered => points.windows(2).enumerate().map(|(index, pair)| {
|
||||
let offset = if self.mode == Mode::Staggered { index as f64 * self.dim_spacing } else { 0.0 };
|
||||
let direction = if horizontal {
|
||||
if place.y >= (pair[0].point.y + pair[1].point.y) * 0.5 { 1.0 } else { -1.0 }
|
||||
} else if place.x >= (pair[0].point.x + pair[1].point.x) * 0.5 { 1.0 } else { -1.0 };
|
||||
let shifted = if horizontal { DVec3::new(place.x, place.y + direction * offset, 0.0) } else { DVec3::new(place.x + direction * offset, place.y, 0.0) };
|
||||
self.linear_dimension(&pair[0], &pair[1], shifted, horizontal)
|
||||
}).collect(),
|
||||
Mode::Baseline => {
|
||||
let datum = self.datum.unwrap_or(points[0].point);
|
||||
let datum_candidate = Candidate {
|
||||
point: datum,
|
||||
sources: points
|
||||
.iter()
|
||||
.find(|point| point.point.distance(datum) <= tolerance)
|
||||
.map(|point| point.sources.clone())
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
points.iter().filter(|point| point.point.distance(datum) > tolerance)
|
||||
.map(|point| self.linear_dimension(&datum_candidate, point, place, horizontal)).collect()
|
||||
}
|
||||
Mode::Ordinate => points.iter().map(|point| self.ordinate_dimension(point, place, horizontal)).collect(),
|
||||
Mode::Radius | Mode::Diameter => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn linear_dimension(&self, first: &Candidate, second: &Candidate, place: DVec3, horizontal: bool) -> (EntityType, DimensionAssociationInput) {
|
||||
let mut dimension = DimensionLinear::new(v3(first.point), v3(second.point));
|
||||
dimension.rotation = if horizontal { 0.0 } else { std::f64::consts::FRAC_PI_2 };
|
||||
let definition = if horizontal { DVec3::new(second.point.x, place.y, 0.0) } else { DVec3::new(place.x, second.point.y, 0.0) };
|
||||
let text = if horizontal { DVec3::new((first.point.x + second.point.x) * 0.5, place.y, 0.0) } else { DVec3::new(place.x, (first.point.y + second.point.y) * 0.5, 0.0) };
|
||||
dimension.definition_point = v3(definition);
|
||||
dimension.base.definition_point = v3(definition);
|
||||
dimension.base.text_middle_point = v3(text);
|
||||
dimension.base.insertion_point = v3(text);
|
||||
dimension.base.actual_measurement = dimension.measurement();
|
||||
let association = DimensionAssociationInput::Explicit(vec![first.sources.first().copied(), second.sources.first().copied()]);
|
||||
(self.plane.place_entity(EntityType::Dimension(Dimension::Linear(dimension))), association)
|
||||
}
|
||||
|
||||
fn ordinate_dimension(&self, point: &Candidate, place: DVec3, horizontal: bool) -> (EntityType, DimensionAssociationInput) {
|
||||
let datum = self.datum.unwrap_or(DVec3::ZERO);
|
||||
let leader = if horizontal { DVec3::new(point.point.x, place.y, 0.0) } else { DVec3::new(place.x, point.point.y, 0.0) };
|
||||
let mut dimension = DimensionOrdinate::new(v3(point.point), v3(leader), horizontal);
|
||||
dimension.definition_point = v3(datum);
|
||||
dimension.base.definition_point = v3(datum);
|
||||
dimension.base.text_middle_point = v3(leader);
|
||||
dimension.base.insertion_point = v3(leader);
|
||||
dimension.refresh_measurement();
|
||||
(self.plane.place_entity(EntityType::Dimension(Dimension::Ordinate(dimension))), DimensionAssociationInput::Explicit(vec![point.sources.first().copied()]))
|
||||
}
|
||||
|
||||
fn build_radial_dimensions(&self, place: DVec3) -> Vec<(EntityType, DimensionAssociationInput)> {
|
||||
self.radial_sources().into_iter().enumerate().map(|(index, (handle, curve))| {
|
||||
let (center, radius) = match curve { Curve::Circle(circle) => (circle.centre, circle.radius), Curve::Arc(arc) => (arc.centre, arc.radius), _ => unreachable!() };
|
||||
let center = DVec3::new(center[0], center[1], 0.0);
|
||||
let mut direction = place - center;
|
||||
direction.z = 0.0;
|
||||
direction = if direction.length_squared() <= f64::EPSILON { DVec3::X } else { direction.normalize() };
|
||||
let chord = center + direction * radius;
|
||||
let text = place + DVec3::new(0.0, index as f64 * self.dim_spacing, 0.0);
|
||||
let association = DimensionAssociationInput::Explicit(vec![Some(DimensionAssociationSource::inferred(handle))]);
|
||||
let entity = if self.mode == Mode::Radius {
|
||||
let mut dimension = DimensionRadius::new(v3(center), v3(chord));
|
||||
dimension.base.definition_point = v3(chord);
|
||||
dimension.base.text_middle_point = v3(text);
|
||||
dimension.base.insertion_point = v3(text);
|
||||
dimension.base.text_user_positioned = true;
|
||||
dimension.leader_length = chord.distance(text);
|
||||
dimension.base.actual_measurement = dimension.measurement();
|
||||
EntityType::Dimension(Dimension::Radius(dimension))
|
||||
} else {
|
||||
let opposite = center - direction * radius;
|
||||
let mut dimension = DimensionDiameter::new(v3(chord), v3(opposite));
|
||||
dimension.base.definition_point = v3(opposite);
|
||||
dimension.base.text_middle_point = v3(text);
|
||||
dimension.base.insertion_point = v3(text);
|
||||
dimension.base.text_user_positioned = true;
|
||||
dimension.leader_length = chord.distance(text);
|
||||
dimension.base.actual_measurement = dimension.measurement();
|
||||
EntityType::Dimension(Dimension::Diameter(dimension))
|
||||
};
|
||||
(self.plane.place_entity(entity), association)
|
||||
}).collect()
|
||||
}
|
||||
|
||||
fn remove_candidate(&mut self, point_world: DVec3) {
|
||||
let point = self.plane.to_local(point_world);
|
||||
let all = self.candidates();
|
||||
let Some(nearest) = all.iter().min_by(|first, second| first.point.distance(point).total_cmp(&second.point.distance(point))) else { return; };
|
||||
self.excluded.push(nearest.point);
|
||||
}
|
||||
|
||||
fn preview_dimensions(&self, point: DVec3) -> Vec<WireModel> {
|
||||
self.build_dimensions(point).into_iter().enumerate().flat_map(|(index, (entity, _))| preview_entity(index, &entity)).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for QdimCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"QDIM"
|
||||
}
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) { self.plane = plane; }
|
||||
fn name(&self) -> &'static str { "QDIM" }
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::Gathering => {
|
||||
t!("QDIM Select geometry to dimension (Enter when done):").into_owned()
|
||||
match self.step {
|
||||
Step::Gathering => t!("QDIM Select geometry to dimension (Enter when done):").into_owned(),
|
||||
Step::Place => t!("QDIM Specify dimension line position or choose an option:").into_owned(),
|
||||
Step::Datum => t!("QDIM Specify datum point:").into_owned(),
|
||||
Step::Edit => t!("QDIM Select a point to remove (Enter when done):").into_owned(),
|
||||
Step::Settings => t!("QDIM Choose extension origin priority [Endpoints/Intersections]:").into_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
match self.step {
|
||||
Step::Place => vec![
|
||||
CmdOption::new("Continuous", "CONTINUOUS"), CmdOption::new("Staggered", "STAGGERED"),
|
||||
CmdOption::new("Baseline", "BASELINE"), CmdOption::new("Ordinate", "ORDINATE"),
|
||||
CmdOption::new("Radius", "RADIUS"), CmdOption::new("Diameter", "DIAMETER"),
|
||||
CmdOption::new("Datum point", "DATUM"), CmdOption::new("Edit", "EDIT"),
|
||||
CmdOption::new("Settings", "SETTINGS"),
|
||||
],
|
||||
Step::Settings => vec![CmdOption::new("Endpoints", "ENDPOINTS"), CmdOption::new("Intersections", "INTERSECTIONS")],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_selection_gathering(&self) -> bool { self.step == Step::Gathering }
|
||||
fn selection_forces_add(&self) -> bool { self.step == Step::Gathering }
|
||||
fn on_selection_complete(&mut self, _handles: Vec<Handle>) -> CmdResult { CmdResult::NeedPoint }
|
||||
fn inject_selection_entities(&mut self, entities: Vec<SelectionEntity>) { self.selection = entities; }
|
||||
fn wants_text_input(&self) -> bool { matches!(self.step, Step::Place | Step::Settings) }
|
||||
fn point_step_accepts_keywords(&self) -> bool { self.step == Step::Place }
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let keyword = text.trim().to_ascii_uppercase();
|
||||
if self.step == Step::Settings {
|
||||
match keyword.as_str() {
|
||||
"E" | "ENDPOINT" | "ENDPOINTS" => { self.snap_priority = SnapPriority::Endpoints; self.step = Step::Place; return Some(CmdResult::SetQuickDimensionSnapPriority(0)); }
|
||||
"I" | "INTERSECTION" | "INTERSECTIONS" => { self.snap_priority = SnapPriority::Intersections; self.step = Step::Place; return Some(CmdResult::SetQuickDimensionSnapPriority(1)); }
|
||||
_ => return Some(CmdResult::NeedPoint),
|
||||
}
|
||||
Step::PlaceLine { .. } => t!("QDIM Specify dimension line position:").into_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_selection_gathering(&self) -> bool {
|
||||
matches!(self.step, Step::Gathering)
|
||||
}
|
||||
|
||||
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
|
||||
if handles.is_empty() {
|
||||
return CmdResult::Cancel;
|
||||
match keyword.as_str() {
|
||||
"C" | "CONTINUOUS" => self.mode = Mode::Continuous,
|
||||
"S" | "STAGGERED" => self.mode = Mode::Staggered,
|
||||
"B" | "BASELINE" => self.mode = Mode::Baseline,
|
||||
"O" | "ORDINATE" => self.mode = Mode::Ordinate,
|
||||
"R" | "RADIUS" => self.mode = Mode::Radius,
|
||||
"DI" | "DIAMETER" => self.mode = Mode::Diameter,
|
||||
"DA" | "DATUM" | "DATUMPOINT" => { self.step = Step::Datum; return Some(CmdResult::NeedPoint); }
|
||||
"E" | "EDIT" => { self.step = Step::Edit; return Some(CmdResult::NeedPoint); }
|
||||
"SE" | "SETTINGS" => { self.step = Step::Settings; return Some(CmdResult::NeedPoint); }
|
||||
_ => return Some(CmdResult::NeedPoint),
|
||||
}
|
||||
self.step = Step::PlaceLine { handles };
|
||||
CmdResult::NeedPoint
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
if let Step::PlaceLine { handles } = &self.step {
|
||||
CmdResult::Relaunch(
|
||||
// Keep the picked placement point in full f64 precision so the
|
||||
// committed dimension coordinates are not quantized to f32.
|
||||
format!("QDIM_PLACE {:.6} {:.6} {:.6}", pt.x, pt.y, pt.z),
|
||||
handles.clone(),
|
||||
)
|
||||
} else {
|
||||
CmdResult::NeedPoint
|
||||
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||
match self.step {
|
||||
Step::Gathering | Step::Settings => CmdResult::NeedPoint,
|
||||
Step::Datum => { self.datum = Some(self.plane.to_local(point)); self.step = Step::Place; CmdResult::NeedPoint }
|
||||
Step::Edit => { self.remove_candidate(point); CmdResult::NeedPoint }
|
||||
Step::Place => {
|
||||
let dimensions = self.build_dimensions(point);
|
||||
if dimensions.is_empty() { CmdResult::NeedPoint } else { CmdResult::CommitDimensionsAndExit(dimensions) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { let pt = pt.as_vec3();
|
||||
if !matches!(self.step, Step::PlaceLine { .. }) {
|
||||
return None;
|
||||
match self.step {
|
||||
Step::Gathering if !self.selection.is_empty() => { self.step = Step::Place; CmdResult::NeedPoint }
|
||||
Step::Edit | Step::Datum | Step::Settings => { self.step = Step::Place; CmdResult::NeedPoint }
|
||||
_ => CmdResult::Cancel,
|
||||
}
|
||||
let d = 0.5_f32;
|
||||
Some(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: "qdim_preview".into(),
|
||||
points: vec![[pt.x - d * 3.0, pt.y, pt.z], [pt.x + d * 3.0, pt.y, pt.z]],
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
fn on_preview_wires(&mut self, point: DVec3) -> Vec<WireModel> {
|
||||
if self.step == Step::Place { return self.preview_dimensions(point); }
|
||||
if self.step == Step::Edit {
|
||||
let size = self.dim_spacing * 0.12;
|
||||
return self.candidates().into_iter().enumerate().flat_map(|(index, candidate)| {
|
||||
let x_start = self.plane.to_world(candidate.point - DVec3::X * size);
|
||||
let x_end = self.plane.to_world(candidate.point + DVec3::X * size);
|
||||
let y_start = self.plane.to_world(candidate.point - DVec3::Y * size);
|
||||
let y_end = self.plane.to_world(candidate.point + DVec3::Y * size);
|
||||
[
|
||||
WireModel::solid_f64(format!("qdim_edit_{index}_x"), vec![
|
||||
x_start.to_array(), x_end.to_array(),
|
||||
], WireModel::CYAN, false),
|
||||
WireModel::solid_f64(format!("qdim_edit_{index}_y"), vec![
|
||||
y_start.to_array(), y_end.to_array(),
|
||||
], WireModel::CYAN, false),
|
||||
]
|
||||
}).collect();
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a linear dimension between two points at the given dimension-line position.
|
||||
#[allow(dead_code)]
|
||||
pub fn make_linear_dim(p1: DVec3, p2: DVec3, dim_pt: DVec3) -> EntityType {
|
||||
let v = |p: DVec3| Vector3::new(p.x, p.y, p.z);
|
||||
let mut dim = DimensionLinear::new(v(p1), v(p2));
|
||||
// Determine axis: horizontal if Δx > Δz, else vertical
|
||||
let dx = (p2.x - p1.x).abs();
|
||||
let dz = (p2.z - p1.z).abs();
|
||||
dim.rotation = if dz > dx {
|
||||
std::f64::consts::FRAC_PI_2
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
dim.definition_point = v(dim_pt);
|
||||
dim.base.definition_point = v(dim_pt);
|
||||
let text_pos = (p1 + p2) * 0.5;
|
||||
dim.base.text_middle_point = v(text_pos);
|
||||
dim.base.insertion_point = v(text_pos);
|
||||
dim.base.actual_measurement = dim.measurement();
|
||||
EntityType::Dimension(Dimension::Linear(dim))
|
||||
fn v3(point: DVec3) -> Vector3 { Vector3::new(point.x, point.y, point.z) }
|
||||
fn d3(point: Vector3) -> DVec3 { DVec3::new(point.x, point.y, point.z) }
|
||||
|
||||
fn candidate_tolerance(points: impl Iterator<Item = DVec3>) -> f64 {
|
||||
let magnitude = points.map(|point| point.x.abs().max(point.y.abs()).max(point.z.abs())).fold(1.0_f64, f64::max);
|
||||
Tolerance::default().linear().max(magnitude * f64::EPSILON * 64.0)
|
||||
}
|
||||
|
||||
fn dimension_reference_points(dimension: &Dimension) -> Vec<DVec3> {
|
||||
match dimension {
|
||||
Dimension::Linear(value) => vec![d3(value.first_point), d3(value.second_point)],
|
||||
Dimension::Aligned(value) => vec![d3(value.first_point), d3(value.second_point)],
|
||||
Dimension::Radius(value) => vec![d3(value.angle_vertex), d3(value.definition_point)],
|
||||
Dimension::Diameter(value) => vec![d3(value.angle_vertex), d3(value.definition_point)],
|
||||
Dimension::Ordinate(value) => vec![d3(value.feature_location)],
|
||||
Dimension::Angular2Ln(value) => vec![d3(value.first_point), d3(value.second_point), d3(value.angle_vertex), d3(value.definition_point)],
|
||||
Dimension::Angular3Pt(value) => vec![d3(value.angle_vertex), d3(value.first_point), d3(value.second_point)],
|
||||
Dimension::Arc(value) => vec![d3(value.center_point), d3(value.first_extension_point), d3(value.second_extension_point)],
|
||||
Dimension::LargeRadial(value) => vec![d3(value.definition_point), d3(value.chord_point)],
|
||||
}
|
||||
}
|
||||
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["QDIM"] }); // QdimCommand
|
||||
fn preview_entity(index: usize, entity: &EntityType) -> Vec<WireModel> {
|
||||
let EntityType::Dimension(dimension) = entity else { return Vec::new(); };
|
||||
let mut lines: Vec<Vec<[f64; 3]>> = Vec::new();
|
||||
match dimension {
|
||||
Dimension::Linear(value) => {
|
||||
let first = d3(value.first_point); let second = d3(value.second_point); let definition = d3(value.definition_point);
|
||||
let horizontal = value.rotation.cos().abs() >= value.rotation.sin().abs();
|
||||
let first_line = if horizontal { DVec3::new(first.x, definition.y, definition.z) } else { DVec3::new(definition.x, first.y, definition.z) };
|
||||
let second_line = if horizontal { DVec3::new(second.x, definition.y, definition.z) } else { DVec3::new(definition.x, second.y, definition.z) };
|
||||
lines.push(vec![first.to_array(), first_line.to_array()]);
|
||||
lines.push(vec![second.to_array(), second_line.to_array()]);
|
||||
lines.push(vec![first_line.to_array(), second_line.to_array()]);
|
||||
}
|
||||
Dimension::Ordinate(value) => lines.push(vec![d3(value.feature_location).to_array(), d3(value.leader_endpoint).to_array()]),
|
||||
Dimension::Radius(value) => lines.push(vec![d3(value.angle_vertex).to_array(), d3(value.definition_point).to_array(), d3(value.base.text_middle_point).to_array()]),
|
||||
Dimension::Diameter(value) => lines.push(vec![d3(value.definition_point).to_array(), d3(value.angle_vertex).to_array(), d3(value.base.text_middle_point).to_array()]),
|
||||
_ => {}
|
||||
}
|
||||
lines.into_iter().enumerate().map(|(part, points)| WireModel::solid_f64(format!("qdim_preview_{index}_{part}"), points, WireModel::CYAN, false)).collect()
|
||||
}
|
||||
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["QDIM"] });
|
||||
@@ -111,6 +111,8 @@ impl CadCommand for RadiusDimensionCommand {
|
||||
CmdResult::CommitDimension {
|
||||
entity: source_plane.place_entity(EntityType::Dimension(Dimension::Radius(dim))),
|
||||
association: DimensionAssociationInput::Infer(self.source_handle),
|
||||
preserve_base_style: false,
|
||||
continue_command: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// Persistent command defaults — last-used values survive across command runs
|
||||
// within the same session. Uses thread_local! so no synchronisation needed.
|
||||
// Last-used command values for this session.
|
||||
|
||||
use std::cell::Cell;
|
||||
|
||||
@@ -28,6 +27,10 @@ thread_local! {
|
||||
static RECT_FILLET: Cell<f64> = Cell::new(0.0);
|
||||
static DONUT_INNER_DIAMETER: Cell<f64> = Cell::new(0.5);
|
||||
static DONUT_OUTER_DIAMETER: Cell<f64> = Cell::new(1.0);
|
||||
static HELIX_BASE_RADIUS: Cell<f64> = Cell::new(1.0);
|
||||
static HELIX_HEIGHT: Cell<f64> = Cell::new(1.0);
|
||||
static HELIX_TURNS: Cell<f64> = Cell::new(3.0);
|
||||
static HELIX_COUNTER_CLOCKWISE: Cell<bool> = Cell::new(true);
|
||||
}
|
||||
|
||||
macro_rules! accessors {
|
||||
@@ -70,6 +73,16 @@ accessors!(get_rect_rotation, set_rect_rotation, RECT_ROTATION);
|
||||
accessors!(get_rect_chamfer1, set_rect_chamfer1, RECT_CHAMFER1);
|
||||
accessors!(get_rect_chamfer2, set_rect_chamfer2, RECT_CHAMFER2);
|
||||
accessors!(get_rect_fillet, set_rect_fillet, RECT_FILLET);
|
||||
accessors!(get_helix_base_radius, set_helix_base_radius, HELIX_BASE_RADIUS);
|
||||
accessors!(get_helix_height, set_helix_height, HELIX_HEIGHT);
|
||||
accessors!(get_helix_turns, set_helix_turns, HELIX_TURNS);
|
||||
pub fn get_helix_counter_clockwise() -> bool {
|
||||
HELIX_COUNTER_CLOCKWISE.with(|value| value.get())
|
||||
}
|
||||
|
||||
pub fn set_helix_counter_clockwise(value: bool) {
|
||||
HELIX_COUNTER_CLOCKWISE.with(|current| current.set(value));
|
||||
}
|
||||
accessors!(
|
||||
get_donut_inner_diameter,
|
||||
set_donut_inner_diameter,
|
||||
|
||||
@@ -0,0 +1,500 @@
|
||||
use acadrust::entities::{face3d::InvisibleEdgeFlags, Face3D};
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::{EntityType, Handle};
|
||||
use cadkernel::geom2d::Tolerance;
|
||||
use cadkernel::space::{curve::segments_overlap_collinearly, Vec3 as KernelVec3};
|
||||
use glam::DVec3;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use crate::t;
|
||||
|
||||
fn vector(point: DVec3) -> Vector3 {
|
||||
Vector3::new(point.x, point.y, point.z)
|
||||
}
|
||||
|
||||
fn point(value: Vector3) -> DVec3 {
|
||||
DVec3::new(value.x, value.y, value.z)
|
||||
}
|
||||
|
||||
fn set_edge_invisible(flags: &mut InvisibleEdgeFlags, edge: usize, invisible: bool) {
|
||||
match edge {
|
||||
0 => flags.set_first_invisible(invisible),
|
||||
1 => flags.set_second_invisible(invisible),
|
||||
2 => flags.set_third_invisible(invisible),
|
||||
3 => flags.set_fourth_invisible(invisible),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn edge_is_invisible(flags: InvisibleEdgeFlags, edge: usize) -> bool {
|
||||
match edge {
|
||||
0 => flags.is_first_invisible(),
|
||||
1 => flags.is_second_invisible(),
|
||||
2 => flags.is_third_invisible(),
|
||||
3 => flags.is_fourth_invisible(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn append_segment(points: &mut Vec<[f64; 3]>, start: [f64; 3], end: [f64; 3]) {
|
||||
if !points.is_empty() {
|
||||
points.push([f64::NAN; 3]);
|
||||
}
|
||||
points.push(start);
|
||||
points.push(end);
|
||||
}
|
||||
|
||||
pub struct Face3dCommand {
|
||||
points: Vec<DVec3>,
|
||||
invisible: [bool; 4],
|
||||
pending_invisible: bool,
|
||||
}
|
||||
|
||||
impl Face3dCommand {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
points: Vec::new(),
|
||||
invisible: [false; 4],
|
||||
pending_invisible: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn build(&self, fourth: DVec3) -> Option<EntityType> {
|
||||
if self.points.len() != 3 {
|
||||
return None;
|
||||
}
|
||||
let mut face = Face3D::new(
|
||||
vector(self.points[0]),
|
||||
vector(self.points[1]),
|
||||
vector(self.points[2]),
|
||||
vector(fourth),
|
||||
);
|
||||
for edge in 0..4 {
|
||||
set_edge_invisible(&mut face.invisible_edges, edge, self.invisible[edge]);
|
||||
}
|
||||
Some(EntityType::Face3D(face))
|
||||
}
|
||||
|
||||
fn continue_from_last_edge(&mut self, fourth: DVec3) {
|
||||
let third = self.points[2];
|
||||
let shared_edge_invisible = self.invisible[2];
|
||||
self.points.clear();
|
||||
self.points.push(third);
|
||||
self.points.push(fourth);
|
||||
self.invisible = [shared_edge_invisible, false, false, false];
|
||||
self.pending_invisible = false;
|
||||
}
|
||||
|
||||
fn continue_from_triangle(&mut self) {
|
||||
let second = self.points[1];
|
||||
let third = self.points[2];
|
||||
let shared_edge_invisible = self.invisible[1];
|
||||
self.points.clear();
|
||||
self.points.push(second);
|
||||
self.points.push(third);
|
||||
self.invisible = [shared_edge_invisible, false, false, false];
|
||||
self.pending_invisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for Face3dCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"3DFACE"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match self.points.len() {
|
||||
0 => t!("3DFACE Specify first point or [Invisible]:").into_owned(),
|
||||
1 => t!("3DFACE Specify second point or [Invisible]:").into_owned(),
|
||||
2 => t!("3DFACE Specify third point or [Invisible] <exit>:").into_owned(),
|
||||
_ => t!("3DFACE Specify fourth point or [Invisible] <create three-sided face>:")
|
||||
.into_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
vec![CmdOption::new(t!("Invisible").as_ref(), "I")]
|
||||
}
|
||||
|
||||
fn on_point(&mut self, picked: DVec3) -> CmdResult {
|
||||
let point_index = self.points.len();
|
||||
if self.pending_invisible && point_index < 4 {
|
||||
self.invisible[point_index] = true;
|
||||
}
|
||||
self.pending_invisible = false;
|
||||
if self.points.len() == 3 {
|
||||
let entity = self.build(picked);
|
||||
self.continue_from_last_edge(picked);
|
||||
return entity.map_or(CmdResult::Cancel, CmdResult::CommitEntity);
|
||||
}
|
||||
self.points.push(picked);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
if self.points.len() != 3 {
|
||||
return CmdResult::Cancel;
|
||||
}
|
||||
if self.pending_invisible {
|
||||
self.invisible[3] = true;
|
||||
}
|
||||
let third = self.points[2];
|
||||
let entity = self.build(third);
|
||||
self.continue_from_triangle();
|
||||
entity.map_or(CmdResult::Cancel, CmdResult::CommitEntity)
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, value: &str) -> Option<CmdResult> {
|
||||
match value.trim().to_ascii_uppercase().as_str() {
|
||||
"I" | "INVISIBLE" => {
|
||||
self.pending_invisible = true;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, cursor: DVec3) -> Option<WireModel> {
|
||||
if self.points.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut preview = Vec::new();
|
||||
for edge in 0..self.points.len().saturating_sub(1) {
|
||||
if !self.invisible[edge] {
|
||||
append_segment(
|
||||
&mut preview,
|
||||
self.points[edge].to_array(),
|
||||
self.points[edge + 1].to_array(),
|
||||
);
|
||||
}
|
||||
}
|
||||
let pending_edge = self.points.len() - 1;
|
||||
if !self.invisible[pending_edge] {
|
||||
append_segment(
|
||||
&mut preview,
|
||||
self.points.last()?.to_array(),
|
||||
cursor.to_array(),
|
||||
);
|
||||
}
|
||||
if self.points.len() == 3 && !(self.invisible[3] || self.pending_invisible) {
|
||||
append_segment(&mut preview, cursor.to_array(), self.points[0].to_array());
|
||||
}
|
||||
Some(WireModel::solid_f64(
|
||||
"face3d_preview".to_string(),
|
||||
preview,
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum EdgeMode {
|
||||
Toggle,
|
||||
Display,
|
||||
DisplaySelect,
|
||||
}
|
||||
|
||||
pub struct FaceEdgeCommand {
|
||||
faces: Vec<(Handle, Face3D)>,
|
||||
mode: EdgeMode,
|
||||
show_all_hidden: bool,
|
||||
displayed_faces: HashSet<Handle>,
|
||||
pending_replacements: HashMap<Handle, Face3D>,
|
||||
}
|
||||
|
||||
impl FaceEdgeCommand {
|
||||
pub fn new(faces: Vec<(Handle, Face3D)>) -> Self {
|
||||
Self {
|
||||
faces,
|
||||
mode: EdgeMode::Toggle,
|
||||
show_all_hidden: false,
|
||||
displayed_faces: HashSet::new(),
|
||||
pending_replacements: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn corners(face: &Face3D) -> [KernelVec3; 4] {
|
||||
[
|
||||
KernelVec3::from(point(face.first_corner).to_array()),
|
||||
KernelVec3::from(point(face.second_corner).to_array()),
|
||||
KernelVec3::from(point(face.third_corner).to_array()),
|
||||
KernelVec3::from(point(face.fourth_corner).to_array()),
|
||||
]
|
||||
}
|
||||
|
||||
fn nearest_edge(face: &Face3D, picked: KernelVec3) -> Option<usize> {
|
||||
let corners = Self::corners(face);
|
||||
let tolerance = Tolerance::default().linear();
|
||||
let mut best = None;
|
||||
for edge in 0..4 {
|
||||
let start = corners[edge];
|
||||
let end = corners[(edge + 1) % 4];
|
||||
if start.distance(end) <= tolerance {
|
||||
continue;
|
||||
}
|
||||
let distance = picked.distance_to_segment(start, end);
|
||||
if best.is_none_or(|(_, best_distance)| distance < best_distance) {
|
||||
best = Some((edge, distance));
|
||||
}
|
||||
}
|
||||
best.map(|(edge, _)| edge)
|
||||
}
|
||||
|
||||
fn edge_wire(face: &Face3D, edge: usize, name: &str) -> WireModel {
|
||||
let corners = Self::corners(face);
|
||||
WireModel::solid_f64(
|
||||
name.to_string(),
|
||||
vec![corners[edge].to_array(), corners[(edge + 1) % 4].to_array()],
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
fn collinear_replacements(
|
||||
&mut self,
|
||||
selected_face: usize,
|
||||
selected_edge: usize,
|
||||
) -> Vec<(Handle, Vec<EntityType>)> {
|
||||
let selected_corners = Self::corners(&self.faces[selected_face].1);
|
||||
let selected_start = selected_corners[selected_edge];
|
||||
let selected_end = selected_corners[(selected_edge + 1) % 4];
|
||||
let make_invisible = !edge_is_invisible(
|
||||
self.faces[selected_face].1.invisible_edges,
|
||||
selected_edge,
|
||||
);
|
||||
let mut replacements = Vec::new();
|
||||
self.pending_replacements.clear();
|
||||
for (handle, face) in &self.faces {
|
||||
let corners = Self::corners(face);
|
||||
let mut updated = face.clone();
|
||||
let mut changed = false;
|
||||
for edge in 0..4 {
|
||||
if segments_overlap_collinearly(
|
||||
selected_start.to_array(),
|
||||
selected_end.to_array(),
|
||||
corners[edge].to_array(),
|
||||
corners[(edge + 1) % 4].to_array(),
|
||||
Tolerance::default().linear(),
|
||||
) && edge_is_invisible(updated.invisible_edges, edge) != make_invisible
|
||||
{
|
||||
set_edge_invisible(&mut updated.invisible_edges, edge, make_invisible);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
self.pending_replacements.insert(*handle, updated.clone());
|
||||
replacements.push((*handle, vec![EntityType::Face3D(updated)]));
|
||||
}
|
||||
}
|
||||
replacements
|
||||
}
|
||||
|
||||
fn hidden_edges_wire(&self) -> WireModel {
|
||||
let mut points = Vec::new();
|
||||
for (handle, face) in &self.faces {
|
||||
if !self.show_all_hidden && !self.displayed_faces.contains(handle) {
|
||||
continue;
|
||||
}
|
||||
let corners = Self::corners(face);
|
||||
for edge in 0..4 {
|
||||
if edge_is_invisible(face.invisible_edges, edge)
|
||||
&& corners[edge].distance(corners[(edge + 1) % 4])
|
||||
> Tolerance::default().linear()
|
||||
{
|
||||
append_segment(
|
||||
&mut points,
|
||||
corners[edge].to_array(),
|
||||
corners[(edge + 1) % 4].to_array(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
WireModel::solid_f64(
|
||||
"face3d_hidden_edges".to_string(),
|
||||
points,
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for FaceEdgeCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"EDGE"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match self.mode {
|
||||
EdgeMode::Toggle => {
|
||||
t!("EDGE Select edge of 3D face to toggle visibility or [Display]:").into_owned()
|
||||
}
|
||||
EdgeMode::Display => {
|
||||
t!("EDGE Display invisible edges [All/Select] <Select>:").into_owned()
|
||||
}
|
||||
EdgeMode::DisplaySelect => {
|
||||
t!("EDGE Select 3D faces to display invisible edges <done>:").into_owned()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
match self.mode {
|
||||
EdgeMode::Toggle => vec![CmdOption::new(t!("Display").as_ref(), "D")],
|
||||
EdgeMode::Display => vec![
|
||||
CmdOption::new(t!("All").as_ref(), "A"),
|
||||
CmdOption::new(t!("Select").as_ref(), "S"),
|
||||
],
|
||||
EdgeMode::DisplaySelect => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
matches!(self.mode, EdgeMode::Toggle | EdgeMode::DisplaySelect)
|
||||
}
|
||||
|
||||
fn entity_pick_includes_fills(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn entity_pick_uses_surface_point(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn entity_pick_highlights_hover(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, picked: DVec3) -> CmdResult {
|
||||
let Some(index) = self.faces.iter().position(|(candidate, _)| *candidate == handle) else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
if self.mode == EdgeMode::DisplaySelect {
|
||||
self.displayed_faces.insert(handle);
|
||||
return CmdResult::Preview(self.hidden_edges_wire());
|
||||
}
|
||||
let picked = KernelVec3::from(picked.to_array());
|
||||
let Some(edge) = Self::nearest_edge(&self.faces[index].1, picked) else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
let replacements = self.collinear_replacements(index, edge);
|
||||
if replacements.is_empty() {
|
||||
CmdResult::NeedPoint
|
||||
} else {
|
||||
CmdResult::ReplaceManyContinue(replacements)
|
||||
}
|
||||
}
|
||||
|
||||
fn on_hover_entity(&mut self, handle: Handle, picked: DVec3) -> Vec<WireModel> {
|
||||
let Some((_, face)) = self.faces.iter().find(|(candidate, _)| *candidate == handle) else {
|
||||
return Vec::new();
|
||||
};
|
||||
if self.mode == EdgeMode::DisplaySelect
|
||||
|| self.show_all_hidden
|
||||
|| self.displayed_faces.contains(&handle)
|
||||
{
|
||||
let mut points = Vec::new();
|
||||
let corners = Self::corners(face);
|
||||
for edge in 0..4 {
|
||||
if corners[edge].distance(corners[(edge + 1) % 4])
|
||||
> Tolerance::default().linear()
|
||||
{
|
||||
append_segment(
|
||||
&mut points,
|
||||
corners[edge].to_array(),
|
||||
corners[(edge + 1) % 4].to_array(),
|
||||
);
|
||||
}
|
||||
}
|
||||
return vec![WireModel::solid_f64(
|
||||
"face3d_edges".to_string(),
|
||||
points,
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
)];
|
||||
}
|
||||
Self::nearest_edge(face, KernelVec3::from(picked.to_array()))
|
||||
.map(|edge| vec![Self::edge_wire(face, edge, "face3d_edge")])
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, value: &str) -> Option<CmdResult> {
|
||||
let keyword = value.trim().to_ascii_uppercase();
|
||||
match self.mode {
|
||||
EdgeMode::Toggle if matches!(keyword.as_str(), "D" | "DISPLAY") => {
|
||||
self.mode = EdgeMode::Display;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
EdgeMode::Display if matches!(keyword.as_str(), "A" | "ALL") => {
|
||||
self.mode = EdgeMode::Toggle;
|
||||
self.show_all_hidden = true;
|
||||
Some(CmdResult::Preview(self.hidden_edges_wire()))
|
||||
}
|
||||
EdgeMode::Display if matches!(keyword.as_str(), "S" | "SELECT") => {
|
||||
self.mode = EdgeMode::DisplaySelect;
|
||||
self.show_all_hidden = false;
|
||||
self.displayed_faces.clear();
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
_ => Some(CmdResult::NeedPoint),
|
||||
}
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _point: DVec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
match self.mode {
|
||||
EdgeMode::Display => {
|
||||
self.mode = EdgeMode::DisplaySelect;
|
||||
self.show_all_hidden = false;
|
||||
self.displayed_faces.clear();
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
EdgeMode::DisplaySelect => {
|
||||
self.mode = EdgeMode::Toggle;
|
||||
CmdResult::Preview(self.hidden_edges_wire())
|
||||
}
|
||||
EdgeMode::Toggle => CmdResult::Cancel,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_entity_replaced(&mut self, old: Handle, new_handles: &[Handle]) {
|
||||
if let Some(new_handle) = new_handles.first().copied() {
|
||||
if let Some((handle, face)) =
|
||||
self.faces.iter_mut().find(|(handle, _)| *handle == old)
|
||||
{
|
||||
if let Some(updated) = self.pending_replacements.remove(&old) {
|
||||
*face = updated;
|
||||
}
|
||||
*handle = new_handle;
|
||||
}
|
||||
if self.displayed_faces.remove(&old) {
|
||||
self.displayed_faces.insert(new_handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inventory::submit!(crate::command::CommandRegistration {
|
||||
names: &["3DFACE", "EDGE"]
|
||||
});
|
||||
+368
-185
@@ -1,35 +1,18 @@
|
||||
// Parametric helix tool — interactive command.
|
||||
//
|
||||
// Command: HELIX — build a parametric 3D helix and commit it as a single 3D
|
||||
// polyline whose straight segments approximate the curve. The user picks a
|
||||
// base centre point, then types the base radius, the top radius (Enter keeps
|
||||
// it equal to the base radius for a cylindrical helix), the height, and the
|
||||
// number of turns (Enter accepts a default of three turns).
|
||||
//
|
||||
// The curve is sampled at a fixed angular resolution: with `SEG_PER_TURN`
|
||||
// samples per turn the helix has `turns * SEG_PER_TURN` segments. For each
|
||||
// sample the radius interpolates linearly from base to top, the elevation
|
||||
// interpolates linearly from zero to the height, and the planar position is
|
||||
// `(radius*cos(angle), radius*sin(angle))` offset from the centre.
|
||||
|
||||
use acadrust::entities::Polyline3D;
|
||||
use acadrust::entities::{Helix, HelixConstraint, Spline};
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::EntityType;
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult, DynField, WorkingPlane};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use cadkernel::space::{HelixCurve, HelixDirection, Vec3};
|
||||
use glam::DVec3;
|
||||
|
||||
/// Angular samples per full turn of the helix.
|
||||
const SEG_PER_TURN: usize = 36;
|
||||
/// Default number of turns when the user accepts the prompt with Enter.
|
||||
const DEFAULT_TURNS: f64 = 3.0;
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, DynField, WorkingPlane};
|
||||
use crate::modules::draw::defaults;
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use crate::t;
|
||||
|
||||
// ── Ribbon definition ─────────────────────────────────────────────────────
|
||||
const EPSILON: f64 = 1.0e-9;
|
||||
|
||||
#[allow(dead_code)] // ribbon definition ready for wiring; command works via the command line
|
||||
#[allow(dead_code)]
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "HELIX",
|
||||
@@ -39,16 +22,18 @@ pub fn tool() -> ToolDef {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Command implementation ────────────────────────────────────────────────
|
||||
|
||||
/// Which parameter the command is currently collecting.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Step {
|
||||
Center,
|
||||
BaseRadius,
|
||||
BaseDiameter,
|
||||
TopRadius,
|
||||
Height,
|
||||
TopDiameter,
|
||||
Final,
|
||||
AxisEndpoint,
|
||||
Turns,
|
||||
TurnHeight,
|
||||
Twist,
|
||||
}
|
||||
|
||||
pub struct HelixCommand {
|
||||
@@ -58,58 +43,130 @@ pub struct HelixCommand {
|
||||
top_radius: f64,
|
||||
height: f64,
|
||||
turns: f64,
|
||||
requested_turn_height: Option<f64>,
|
||||
counter_clockwise: bool,
|
||||
constraint: HelixConstraint,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl HelixCommand {
|
||||
pub fn new() -> Self {
|
||||
let base_radius = defaults::get_helix_base_radius();
|
||||
Self {
|
||||
step: Step::Center,
|
||||
center: DVec3::ZERO,
|
||||
base_radius: 0.0,
|
||||
top_radius: 0.0,
|
||||
height: 0.0,
|
||||
turns: DEFAULT_TURNS,
|
||||
base_radius,
|
||||
top_radius: base_radius,
|
||||
height: defaults::get_helix_height(),
|
||||
turns: defaults::get_helix_turns(),
|
||||
requested_turn_height: None,
|
||||
counter_clockwise: defaults::get_helix_counter_clockwise(),
|
||||
constraint: HelixConstraint::Turns,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sample the helix into a list of world points.
|
||||
fn sample_points(&self, base_radius: f64, top_radius: f64, height: f64, turns: f64) -> Vec<DVec3> {
|
||||
let turns = if turns <= 0.0 { DEFAULT_TURNS } else { turns };
|
||||
let segments = ((turns * SEG_PER_TURN as f64).round() as usize).max(1);
|
||||
let mut pts = Vec::with_capacity(segments + 1);
|
||||
for t in 0..=segments {
|
||||
let frac = t as f64 / segments as f64;
|
||||
let angle = turns * std::f64::consts::TAU * frac;
|
||||
let radius = base_radius + (top_radius - base_radius) * frac;
|
||||
let z = height * frac;
|
||||
pts.push(
|
||||
self.center
|
||||
+ self.plane.x * (radius * angle.cos())
|
||||
+ self.plane.y * (radius * angle.sin())
|
||||
+ self.plane.z * z,
|
||||
);
|
||||
}
|
||||
pts
|
||||
}
|
||||
|
||||
/// Build the committed helix entity from the collected parameters.
|
||||
fn build(&self) -> Option<EntityType> {
|
||||
let pts = self.sample_points(self.base_radius, self.top_radius, self.height, self.turns);
|
||||
if pts.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let verts: Vec<Vector3> = pts.iter().map(|p| Vector3::new(p.x, p.y, p.z)).collect();
|
||||
let mut pl = Polyline3D::from_points(verts);
|
||||
pl.flags.closed = false;
|
||||
Some(EntityType::Polyline3D(pl))
|
||||
}
|
||||
|
||||
/// Parse a positive distance from typed text; returns `None` on failure.
|
||||
fn parse_value(text: &str) -> Option<f64> {
|
||||
crate::entities::common::parse_typed_length(text)
|
||||
}
|
||||
|
||||
fn parse_turns(text: &str) -> Option<f64> {
|
||||
text.trim().replace(',', ".").parse().ok().filter(|value: &f64| value.is_finite())
|
||||
}
|
||||
|
||||
fn axis_start_direction(&self, axis: Vec3) -> Option<Vec3> {
|
||||
let project = |direction: DVec3| {
|
||||
let direction = Vec3::from(direction.to_array());
|
||||
(direction - axis * direction.dot(axis)).normalize()
|
||||
};
|
||||
project(self.plane.x).or_else(|| project(self.plane.y))
|
||||
}
|
||||
|
||||
fn resolved_turns(&self, height: f64) -> f64 {
|
||||
match self.requested_turn_height {
|
||||
Some(turn_height) if turn_height > EPSILON => (height.abs() / turn_height).max(EPSILON),
|
||||
_ => self.turns,
|
||||
}
|
||||
}
|
||||
|
||||
fn kernel_curve(&self, height: f64, axis: DVec3) -> Option<HelixCurve> {
|
||||
let axis = Vec3::from(axis.to_array())
|
||||
.normalize()
|
||||
.or_else(|| Vec3::from(self.plane.z.to_array()).normalize())?;
|
||||
Some(HelixCurve {
|
||||
base_center: self.center.to_array(),
|
||||
axis_direction: axis.to_array(),
|
||||
start_direction: self.axis_start_direction(axis)?.to_array(),
|
||||
base_radius: self.base_radius,
|
||||
top_radius: self.top_radius,
|
||||
height,
|
||||
turns: self.resolved_turns(height),
|
||||
direction: if self.counter_clockwise {
|
||||
HelixDirection::CounterClockwise
|
||||
} else {
|
||||
HelixDirection::Clockwise
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn build(&self, height: f64, axis: DVec3) -> Option<EntityType> {
|
||||
let curve = self.kernel_curve(height, axis)?;
|
||||
let nurbs = curve.nurbs()?;
|
||||
let axis = DVec3::from_array(curve.axis_direction).normalize_or(self.plane.z);
|
||||
let start_direction = DVec3::from_array(curve.start_direction).normalize_or(self.plane.x);
|
||||
let mut spline = Spline::new();
|
||||
spline.degree = nurbs.degree() as i32;
|
||||
spline.knots = nurbs.knots().to_vec();
|
||||
spline.control_points = nurbs
|
||||
.control_points()
|
||||
.iter()
|
||||
.map(|point| Vector3::new(point[0], point[1], point[2]))
|
||||
.collect();
|
||||
spline.flags.planar = false;
|
||||
spline.flags.rational = nurbs.is_rational();
|
||||
if spline.flags.rational {
|
||||
spline.weights = nurbs.weights().to_vec();
|
||||
}
|
||||
|
||||
let mut helix = Helix::new();
|
||||
helix.spline = spline;
|
||||
helix.axis_base_point = Vector3::new(self.center.x, self.center.y, self.center.z);
|
||||
let start = self.center + start_direction * curve.base_radius;
|
||||
helix.start_point = Vector3::new(start.x, start.y, start.z);
|
||||
helix.axis_vector = Vector3::new(axis.x, axis.y, axis.z);
|
||||
helix.radius = curve.base_radius;
|
||||
helix.turns = curve.turns;
|
||||
helix.turn_height = height / curve.turns;
|
||||
helix.handedness = self.counter_clockwise;
|
||||
helix.constraint = self.constraint;
|
||||
Some(EntityType::Helix(helix))
|
||||
}
|
||||
|
||||
fn commit(&mut self, height: f64, axis: DVec3) -> CmdResult {
|
||||
let Some(entity) = self.build(height, axis) else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
self.height = height;
|
||||
self.turns = self.resolved_turns(height);
|
||||
defaults::set_helix_base_radius(self.base_radius);
|
||||
defaults::set_helix_height(height);
|
||||
defaults::set_helix_turns(self.turns);
|
||||
defaults::set_helix_counter_clockwise(self.counter_clockwise);
|
||||
CmdResult::CommitAndExit(entity)
|
||||
}
|
||||
|
||||
fn preview(&self, height: f64, axis: DVec3) -> Option<WireModel> {
|
||||
let points = self
|
||||
.kernel_curve(height, axis)?
|
||||
.nurbs()?
|
||||
.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE);
|
||||
Some(WireModel::solid_f64(
|
||||
"helix_preview".to_string(),
|
||||
points,
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for HelixCommand {
|
||||
@@ -123,65 +180,158 @@ impl CadCommand for HelixCommand {
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match self.step {
|
||||
Step::Center => t!("HELIX Specify centre point of base:").into_owned(),
|
||||
Step::BaseRadius => t!("HELIX Specify base radius:").into_owned(),
|
||||
Step::TopRadius => t!("HELIX Specify top radius <same as base>:").into_owned(),
|
||||
Step::Height => t!("HELIX Specify helix height:").into_owned(),
|
||||
Step::Turns => t!("HELIX Enter number of turns <3>:").into_owned(),
|
||||
Step::Center => crate::tf!(
|
||||
"HELIX Number of turns = {:.4} Twist = {}\nSpecify center point of base:",
|
||||
self.turns,
|
||||
if self.counter_clockwise { "CCW" } else { "CW" }
|
||||
)
|
||||
.into_owned(),
|
||||
Step::BaseRadius => crate::tf!(
|
||||
"HELIX Specify base radius or [Diameter] <{:.4}>:",
|
||||
self.base_radius
|
||||
)
|
||||
.into_owned(),
|
||||
Step::BaseDiameter => crate::tf!(
|
||||
"HELIX Specify base diameter <{:.4}>:",
|
||||
self.base_radius * 2.0
|
||||
)
|
||||
.into_owned(),
|
||||
Step::TopRadius => crate::tf!(
|
||||
"HELIX Specify top radius or [Diameter] <{:.4}>:",
|
||||
self.base_radius
|
||||
)
|
||||
.into_owned(),
|
||||
Step::TopDiameter => crate::tf!(
|
||||
"HELIX Specify top diameter <{:.4}>:",
|
||||
self.base_radius * 2.0
|
||||
)
|
||||
.into_owned(),
|
||||
Step::Final => crate::tf!(
|
||||
"HELIX Specify helix height or [Axis endpoint/Turns/turn Height/tWist] <{:.4}>:",
|
||||
self.height
|
||||
)
|
||||
.into_owned(),
|
||||
Step::AxisEndpoint => t!("HELIX Specify axis endpoint:").into_owned(),
|
||||
Step::Turns => crate::tf!("HELIX Enter number of turns <{:.4}>:", self.turns).into_owned(),
|
||||
Step::TurnHeight => crate::tf!(
|
||||
"HELIX Specify distance between turns <{:.4}>:",
|
||||
self.height.abs() / self.turns.max(EPSILON)
|
||||
)
|
||||
.into_owned(),
|
||||
Step::Twist => crate::tf!(
|
||||
"HELIX Enter twist direction [Clockwise/Counterclockwise] <{}>:",
|
||||
if self.counter_clockwise { "CCW" } else { "CW" }
|
||||
)
|
||||
.into_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
match self.step {
|
||||
Step::BaseRadius | Step::TopRadius => vec![CmdOption::new(t!("Diameter").as_ref(), "D")],
|
||||
Step::Final => vec![
|
||||
CmdOption::new(t!("Axis endpoint").as_ref(), "A"),
|
||||
CmdOption::new(t!("Turns").as_ref(), "T"),
|
||||
CmdOption::new(t!("Turn Height").as_ref(), "H"),
|
||||
CmdOption::new(t!("Twist").as_ref(), "W"),
|
||||
],
|
||||
Step::Twist => vec![
|
||||
CmdOption::new(t!("Clockwise").as_ref(), "CW"),
|
||||
CmdOption::new(t!("Counterclockwise").as_ref(), "CCW"),
|
||||
],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
matches!(self.step, Step::BaseRadius | Step::TopRadius | Step::Final | Step::Twist)
|
||||
}
|
||||
|
||||
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||
match self.step {
|
||||
Step::Center => {
|
||||
self.center = pt;
|
||||
self.center = point;
|
||||
self.step = Step::BaseRadius;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::BaseRadius => {
|
||||
// A point click resolves the base radius as the planar distance
|
||||
// from the centre to the picked point.
|
||||
let delta = self.plane.vector_to_local(pt - self.center);
|
||||
let r = delta.x.hypot(delta.y);
|
||||
if r > 0.0 {
|
||||
self.base_radius = r;
|
||||
Step::BaseRadius | Step::BaseDiameter => {
|
||||
let local = self.plane.vector_to_local(point - self.center);
|
||||
let distance = local.x.hypot(local.y);
|
||||
let radius = if self.step == Step::BaseDiameter {
|
||||
distance * 0.5
|
||||
} else {
|
||||
distance
|
||||
};
|
||||
if radius > EPSILON {
|
||||
self.base_radius = radius;
|
||||
self.top_radius = radius;
|
||||
self.step = Step::TopRadius;
|
||||
}
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::TopRadius => {
|
||||
let delta = self.plane.vector_to_local(pt - self.center);
|
||||
let r = delta.x.hypot(delta.y);
|
||||
self.top_radius = if r > 0.0 { r } else { self.base_radius };
|
||||
self.step = Step::Height;
|
||||
Step::TopRadius | Step::TopDiameter => {
|
||||
let local = self.plane.vector_to_local(point - self.center);
|
||||
let distance = local.x.hypot(local.y);
|
||||
self.top_radius = if self.step == Step::TopDiameter {
|
||||
distance * 0.5
|
||||
} else {
|
||||
distance
|
||||
};
|
||||
self.step = Step::Final;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Height => {
|
||||
// Use the elevation of the picked point relative to the centre.
|
||||
let h = (pt - self.center).dot(self.plane.z);
|
||||
if h.abs() > 0.0 {
|
||||
self.height = h;
|
||||
self.step = Step::Turns;
|
||||
Step::Final => {
|
||||
let height = (point - self.center).dot(self.plane.z);
|
||||
if height.abs() <= EPSILON {
|
||||
CmdResult::NeedPoint
|
||||
} else {
|
||||
self.commit(height, self.plane.z)
|
||||
}
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Turns => CmdResult::NeedPoint,
|
||||
Step::AxisEndpoint => {
|
||||
let vector = point - self.center;
|
||||
let height = vector.length();
|
||||
if height <= EPSILON {
|
||||
CmdResult::NeedPoint
|
||||
} else {
|
||||
self.commit(height, vector / height)
|
||||
}
|
||||
}
|
||||
Step::Turns | Step::TurnHeight | Step::Twist => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
match self.step {
|
||||
Step::TopRadius => {
|
||||
// Enter keeps the top radius equal to the base radius.
|
||||
Step::Center => CmdResult::Cancel,
|
||||
Step::BaseRadius | Step::BaseDiameter => {
|
||||
self.top_radius = self.base_radius;
|
||||
self.step = Step::Height;
|
||||
self.step = Step::TopRadius;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::TopRadius | Step::TopDiameter => {
|
||||
self.top_radius = self.base_radius;
|
||||
self.step = Step::Final;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Final => self.commit(self.height, self.plane.z),
|
||||
Step::AxisEndpoint => CmdResult::NeedPoint,
|
||||
Step::Turns => {
|
||||
self.constraint = HelixConstraint::Turns;
|
||||
self.requested_turn_height = None;
|
||||
self.step = Step::Final;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::TurnHeight => {
|
||||
self.constraint = HelixConstraint::TurnHeight;
|
||||
self.requested_turn_height = Some(self.height.abs() / self.turns.max(EPSILON));
|
||||
self.step = Step::Final;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Twist => {
|
||||
self.step = Step::Final;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Turns => match self.build() {
|
||||
Some(e) => CmdResult::CommitAndExit(e),
|
||||
None => CmdResult::Cancel,
|
||||
},
|
||||
_ => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,123 +340,156 @@ impl CadCommand for HelixCommand {
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
// Every step past the centre pick reads a typed numeric value.
|
||||
self.step != Step::Center
|
||||
}
|
||||
|
||||
fn dyn_field(&self) -> DynField {
|
||||
match self.step {
|
||||
Step::Center => DynField::Point,
|
||||
Step::BaseRadius | Step::TopRadius => DynField::Distance,
|
||||
Step::Height => DynField::Distance,
|
||||
Step::Turns => DynField::Scalar,
|
||||
Step::Center | Step::AxisEndpoint => DynField::Point,
|
||||
Step::BaseRadius | Step::BaseDiameter | Step::TopRadius | Step::TopDiameter | Step::Final | Step::TurnHeight => DynField::Distance,
|
||||
Step::Turns | Step::Twist => DynField::Scalar,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let t = text.trim();
|
||||
let value = text.trim();
|
||||
let keyword = value.to_ascii_uppercase();
|
||||
match self.step {
|
||||
Step::Center => None,
|
||||
Step::BaseRadius if matches!(keyword.as_str(), "D" | "DIAMETER") => {
|
||||
self.step = Step::BaseDiameter;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Step::TopRadius if matches!(keyword.as_str(), "D" | "DIAMETER") => {
|
||||
self.step = Step::TopDiameter;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Step::Final if matches!(keyword.as_str(), "A" | "AXIS" | "AXIS ENDPOINT") => {
|
||||
self.step = Step::AxisEndpoint;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Step::Final if matches!(keyword.as_str(), "T" | "TURNS") => {
|
||||
self.step = Step::Turns;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Step::Final if matches!(keyword.as_str(), "H" | "HEIGHT" | "TURN HEIGHT") => {
|
||||
self.step = Step::TurnHeight;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Step::Final if matches!(keyword.as_str(), "W" | "TWIST") => {
|
||||
self.step = Step::Twist;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Step::BaseRadius => {
|
||||
if let Some(v) = Self::parse_value(t) {
|
||||
if v > 0.0 {
|
||||
self.base_radius = v;
|
||||
self.step = Step::TopRadius;
|
||||
}
|
||||
let radius = Self::parse_value(value)?;
|
||||
if radius > EPSILON {
|
||||
self.base_radius = radius;
|
||||
self.top_radius = radius;
|
||||
self.step = Step::TopRadius;
|
||||
}
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Step::BaseDiameter => {
|
||||
let diameter = Self::parse_value(value)?;
|
||||
if diameter > EPSILON {
|
||||
self.base_radius = diameter * 0.5;
|
||||
self.top_radius = self.base_radius;
|
||||
self.step = Step::TopRadius;
|
||||
}
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Step::TopRadius => {
|
||||
if t.is_empty() {
|
||||
// Empty entry keeps the top radius equal to the base radius.
|
||||
self.top_radius = self.base_radius;
|
||||
self.step = Step::Height;
|
||||
} else if let Some(v) = Self::parse_value(t) {
|
||||
self.top_radius = if v > 0.0 { v } else { self.base_radius };
|
||||
self.step = Step::Height;
|
||||
let radius = Self::parse_value(value)?;
|
||||
if radius >= 0.0 {
|
||||
self.top_radius = radius;
|
||||
self.step = Step::Final;
|
||||
}
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Step::Height => {
|
||||
if let Some(v) = Self::parse_value(t) {
|
||||
if v != 0.0 {
|
||||
self.height = v;
|
||||
self.step = Step::Turns;
|
||||
}
|
||||
Step::TopDiameter => {
|
||||
let diameter = Self::parse_value(value)?;
|
||||
if diameter >= 0.0 {
|
||||
self.top_radius = diameter * 0.5;
|
||||
self.step = Step::Final;
|
||||
}
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Step::Final => {
|
||||
let height = Self::parse_value(value)?;
|
||||
(height.abs() > EPSILON).then(|| self.commit(height, self.plane.z))
|
||||
}
|
||||
Step::AxisEndpoint => None,
|
||||
Step::Turns => {
|
||||
self.turns = if t.is_empty() {
|
||||
DEFAULT_TURNS
|
||||
} else {
|
||||
match Self::parse_value(t) {
|
||||
Some(v) if v > 0.0 => v,
|
||||
_ => return Some(CmdResult::NeedPoint),
|
||||
}
|
||||
};
|
||||
match self.build() {
|
||||
Some(e) => Some(CmdResult::CommitAndExit(e)),
|
||||
None => Some(CmdResult::Cancel),
|
||||
let turns = Self::parse_turns(value)?;
|
||||
if turns > EPSILON {
|
||||
self.turns = turns;
|
||||
self.requested_turn_height = None;
|
||||
self.constraint = HelixConstraint::Turns;
|
||||
self.step = Step::Final;
|
||||
}
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Step::TurnHeight => {
|
||||
let turn_height = Self::parse_value(value)?;
|
||||
if turn_height > EPSILON {
|
||||
self.requested_turn_height = Some(turn_height);
|
||||
self.constraint = HelixConstraint::TurnHeight;
|
||||
self.step = Step::Final;
|
||||
}
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Step::Twist => {
|
||||
match keyword.as_str() {
|
||||
"C" | "CW" | "CLOCKWISE" => self.counter_clockwise = false,
|
||||
"CC" | "CCW" | "COUNTERCLOCKWISE" => self.counter_clockwise = true,
|
||||
_ => return Some(CmdResult::NeedPoint),
|
||||
}
|
||||
self.step = Step::Final;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||
// Preview the helix once enough parameters are known. Before a base
|
||||
// radius is fixed, track the cursor distance to size a live ring.
|
||||
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
|
||||
match self.step {
|
||||
Step::Center => None,
|
||||
Step::BaseRadius => {
|
||||
let delta = self.plane.vector_to_local(pt - self.center);
|
||||
let r = delta.x.hypot(delta.y);
|
||||
if r <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let pts = self.sample_points(r, r, 0.0, 1.0);
|
||||
let raw: Vec<[f64; 3]> = pts.iter().map(|p| [p.x, p.y, p.z]).collect();
|
||||
Some(WireModel::solid_f64(
|
||||
"helix_preview".to_string(),
|
||||
raw,
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
))
|
||||
}
|
||||
_ => {
|
||||
// Use the parameters fixed so far; for the height step let the
|
||||
// cursor elevation drive the live height.
|
||||
let height = if self.step == Step::Height {
|
||||
(pt - self.center).dot(self.plane.z)
|
||||
Step::BaseRadius | Step::BaseDiameter => {
|
||||
let local = self.plane.vector_to_local(point - self.center);
|
||||
let old = (self.base_radius, self.top_radius);
|
||||
let distance = local.x.hypot(local.y);
|
||||
self.base_radius = if self.step == Step::BaseDiameter {
|
||||
distance * 0.5
|
||||
} else {
|
||||
self.height
|
||||
};
|
||||
let top = if self.step == Step::TopRadius {
|
||||
let delta = self.plane.vector_to_local(pt - self.center);
|
||||
let r = delta.x.hypot(delta.y);
|
||||
if r > 0.0 {
|
||||
r
|
||||
} else {
|
||||
self.base_radius
|
||||
}
|
||||
} else {
|
||||
self.top_radius
|
||||
};
|
||||
let pts = self.sample_points(self.base_radius, top, height, self.turns);
|
||||
if pts.len() < 2 {
|
||||
return None;
|
||||
distance
|
||||
}
|
||||
let raw: Vec<[f64; 3]> = pts.iter().map(|p| [p.x, p.y, p.z]).collect();
|
||||
Some(WireModel::solid_f64(
|
||||
"helix_preview".to_string(),
|
||||
raw,
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
))
|
||||
.max(EPSILON);
|
||||
self.top_radius = self.base_radius;
|
||||
let preview = self.preview(0.0, self.plane.z);
|
||||
(self.base_radius, self.top_radius) = old;
|
||||
preview
|
||||
}
|
||||
Step::TopRadius | Step::TopDiameter => {
|
||||
let local = self.plane.vector_to_local(point - self.center);
|
||||
let old = self.top_radius;
|
||||
let distance = local.x.hypot(local.y);
|
||||
self.top_radius = if self.step == Step::TopDiameter {
|
||||
distance * 0.5
|
||||
} else {
|
||||
distance
|
||||
};
|
||||
let preview = self.preview(0.0, self.plane.z);
|
||||
self.top_radius = old;
|
||||
preview
|
||||
}
|
||||
Step::Final => self.preview((point - self.center).dot(self.plane.z), self.plane.z),
|
||||
Step::AxisEndpoint => {
|
||||
let vector = point - self.center;
|
||||
let height = vector.length();
|
||||
(height > EPSILON).then(|| self.preview(height, vector / height)).flatten()
|
||||
}
|
||||
Step::Turns | Step::TurnHeight | Step::Twist => self.preview(self.height, self.plane.z),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["HELIX"] }); // HelixCommand
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["HELIX"] });
|
||||
@@ -0,0 +1,188 @@
|
||||
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 an M by N polygon mesh from row-major 3D input.
|
||||
pub struct Mesh3dCommand {
|
||||
step: Step,
|
||||
m: usize,
|
||||
n: usize,
|
||||
points: Vec<DVec3>,
|
||||
}
|
||||
|
||||
impl Mesh3dCommand {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: Step::MSize,
|
||||
m: 0,
|
||||
n: 0,
|
||||
points: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_size(text: &str) -> Option<usize> {
|
||||
text.trim()
|
||||
.parse::<usize>()
|
||||
.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<EntityType> {
|
||||
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<WireModel> {
|
||||
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<CmdResult> {
|
||||
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;
|
||||
}
|
||||
if !point.is_finite() {
|
||||
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<WireModel> {
|
||||
self.preview(point)
|
||||
}
|
||||
}
|
||||
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["3DMESH"] });
|
||||
@@ -5,11 +5,13 @@ pub mod circle;
|
||||
pub mod dimcenter;
|
||||
pub mod donut;
|
||||
pub mod ellipse;
|
||||
pub mod face3d;
|
||||
pub mod hatch;
|
||||
pub mod hatchedit;
|
||||
pub mod helix;
|
||||
pub mod line;
|
||||
pub mod mline;
|
||||
pub mod mesh3d;
|
||||
pub mod point;
|
||||
pub mod poly3d;
|
||||
pub mod polyline;
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
// 3D polyline tool — interactive command.
|
||||
//
|
||||
// Command: 3DPOLY — pick a sequence of points whose Z ordinates may differ and
|
||||
// commit them as a single 3D polyline of straight segments. Unlike PLINE (a
|
||||
// planar lightweight polyline with bulge arcs), every vertex keeps its own Z,
|
||||
// so the result is a true non-planar path. Close (C) adds a closing segment
|
||||
// back to the first point; Undo (U) drops the last picked vertex. Enter / Esc
|
||||
// finishes the open polyline.
|
||||
// 3DPOLY creates open or closed non-planar paths.
|
||||
|
||||
use acadrust::entities::Polyline3D;
|
||||
use acadrust::types::Vector3;
|
||||
@@ -67,7 +60,7 @@ impl CadCommand for Poly3dCommand {
|
||||
fn prompt(&self) -> String {
|
||||
match self.points.len() {
|
||||
0 => t!("3DPOLY Specify start point of polyline:").into_owned(),
|
||||
1 => t!("3DPOLY Specify next point [Undo]:").into_owned(),
|
||||
1 | 2 => t!("3DPOLY Specify next point [Undo]:").into_owned(),
|
||||
_ => t!("3DPOLY Specify next point [Close/Undo]:").into_owned(),
|
||||
}
|
||||
}
|
||||
@@ -98,10 +91,10 @@ impl CadCommand for Poly3dCommand {
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
match text.trim().to_uppercase().as_str() {
|
||||
"C" | "CLOSE" => match self.build(true) {
|
||||
Some(e) => Some(CmdResult::CommitAndExit(e)),
|
||||
None => Some(CmdResult::NeedPoint),
|
||||
},
|
||||
"C" | "CLOSE" if self.points.len() >= 3 => self
|
||||
.build(true)
|
||||
.map(CmdResult::CommitAndExit),
|
||||
"C" | "CLOSE" => Some(CmdResult::NeedPoint),
|
||||
"U" | "UNDO" => {
|
||||
self.points.pop();
|
||||
Some(CmdResult::NeedPoint)
|
||||
|
||||
@@ -1,26 +1,15 @@
|
||||
// PEDIT command — edit a polyline entity (#263).
|
||||
//
|
||||
// Flow:
|
||||
// Select polyline — picking a LINE or ARC instead asks "Turn it into one?"
|
||||
// (Yes converts in place and continues editing the result).
|
||||
// Options: Close / Open / Join / Width / Fit / Spline / Decurve / eXit.
|
||||
// Join gathers more segments (lines / arcs / polylines) and merges every
|
||||
// contiguous run through the JOIN machinery. (Vertex editing lives on the
|
||||
// polyline's grips, not in PEDIT.)
|
||||
// Fit smooths the segments into tangent-blended arcs, Spline replaces the
|
||||
// shape with a sampled cubic B-spline of the vertex frame, Decurve
|
||||
// straightens every segment.
|
||||
// PEDIT edits polylines and polygon meshes.
|
||||
|
||||
use acadrust::entities::LwVertex;
|
||||
use cadkernel::geom2d::nurbs::clamped_uniform_knots;
|
||||
use cadkernel::geom2d::NurbsCurve;
|
||||
use acadrust::types::Vector2;
|
||||
use acadrust::{EntityType, Handle};
|
||||
use cadkernel::geom2d::nurbs::clamped_uniform_knots;
|
||||
use cadkernel::geom2d::NurbsCurve;
|
||||
use glam::{DVec2, DVec3};
|
||||
use crate::t;
|
||||
use rustc_hash::FxHashMap as HashMap;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::t;
|
||||
|
||||
const TAU: f64 = std::f64::consts::TAU;
|
||||
|
||||
@@ -31,6 +20,8 @@ pub struct PeditTarget {
|
||||
pub is_poly: bool,
|
||||
/// Line / Arc — offered for conversion on pick.
|
||||
pub convertible: bool,
|
||||
/// M/N size for a legacy polygon mesh; absent for ordinary polylines.
|
||||
pub mesh_size: Option<(usize, usize)>,
|
||||
}
|
||||
|
||||
enum Mode {
|
||||
@@ -41,12 +32,17 @@ enum Mode {
|
||||
AwaitWidth,
|
||||
/// Join: gathering additional segments; Enter merges.
|
||||
JoinGather(Vec<Handle>),
|
||||
/// Polygon-mesh vertex navigation uses the mesh's row-major control net.
|
||||
MeshVertex(usize),
|
||||
/// Waiting for the replacement location of the selected mesh vertex.
|
||||
MeshVertexMove(usize),
|
||||
}
|
||||
|
||||
pub struct PeditCommand {
|
||||
target: Option<Handle>,
|
||||
info: HashMap<u64, PeditTarget>,
|
||||
mode: Mode,
|
||||
undo_count: usize,
|
||||
}
|
||||
|
||||
impl PeditCommand {
|
||||
@@ -55,6 +51,7 @@ impl PeditCommand {
|
||||
target: None,
|
||||
info,
|
||||
mode: Mode::PickTarget,
|
||||
undo_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +75,10 @@ impl PeditCommand {
|
||||
self
|
||||
}
|
||||
|
||||
fn mesh_size(&self) -> Option<(usize, usize)> {
|
||||
let handle = self.target?;
|
||||
self.info.get(&handle.value())?.mesh_size
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for PeditCommand {
|
||||
@@ -100,6 +101,16 @@ impl CadCommand for PeditCommand {
|
||||
count = list.len().saturating_sub(1)
|
||||
)
|
||||
.into_owned(),
|
||||
Mode::MeshVertex(index) => t!(
|
||||
"PEDIT Edit mesh vertex %{vertex} Enter option:",
|
||||
vertex = index + 1
|
||||
)
|
||||
.into_owned(),
|
||||
Mode::MeshVertexMove(index) => t!(
|
||||
"PEDIT Specify new location for mesh vertex %{vertex}:",
|
||||
vertex = index + 1
|
||||
)
|
||||
.into_owned(),
|
||||
Mode::Options => t!("PEDIT Enter option:").into_owned(),
|
||||
}
|
||||
}
|
||||
@@ -107,6 +118,17 @@ impl CadCommand for PeditCommand {
|
||||
fn options(&self) -> Vec<crate::command::CmdOption> {
|
||||
use crate::command::CmdOption;
|
||||
match &self.mode {
|
||||
Mode::Options if self.mesh_size().is_some() => vec![
|
||||
CmdOption::new(t!("Edit vertex").as_ref(), "E"),
|
||||
CmdOption::new(t!("Smooth surface").as_ref(), "S"),
|
||||
CmdOption::new(t!("Desmooth").as_ref(), "D"),
|
||||
CmdOption::new(t!("M close").as_ref(), "MC"),
|
||||
CmdOption::new(t!("M open").as_ref(), "MO"),
|
||||
CmdOption::new(t!("N close").as_ref(), "NC"),
|
||||
CmdOption::new(t!("N open").as_ref(), "NO"),
|
||||
CmdOption::new(t!("Undo").as_ref(), "U"),
|
||||
CmdOption::new(t!("eXit").as_ref(), "X"),
|
||||
],
|
||||
Mode::Options => vec![
|
||||
CmdOption::new(t!("Close").as_ref(), "C"),
|
||||
CmdOption::new(t!("Open").as_ref(), "O"),
|
||||
@@ -117,10 +139,22 @@ impl CadCommand for PeditCommand {
|
||||
CmdOption::new(t!("Decurve").as_ref(), "D"),
|
||||
CmdOption::new(t!("eXit").as_ref(), "X"),
|
||||
],
|
||||
Mode::MeshVertex(_) => vec![
|
||||
CmdOption::new(t!("Next").as_ref(), "N"),
|
||||
CmdOption::new(t!("Previous").as_ref(), "P"),
|
||||
CmdOption::new(t!("Left").as_ref(), "L"),
|
||||
CmdOption::new(t!("Right").as_ref(), "R"),
|
||||
CmdOption::new(t!("Up").as_ref(), "U"),
|
||||
CmdOption::new(t!("Down").as_ref(), "D"),
|
||||
CmdOption::new(t!("Move").as_ref(), "M"),
|
||||
CmdOption::new(t!("Regen").as_ref(), "G"),
|
||||
CmdOption::new(t!("eXit").as_ref(), "X"),
|
||||
],
|
||||
Mode::ConvertPrompt(_) => {
|
||||
vec![CmdOption::new(t!("Yes").as_ref(), "Y"), CmdOption::new(t!("No").as_ref(), "N")]
|
||||
}
|
||||
Mode::JoinGather(_) => vec![CmdOption::enter(t!("Join").as_ref())],
|
||||
Mode::MeshVertexMove(_) => vec![],
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
@@ -179,6 +213,7 @@ impl CadCommand for PeditCommand {
|
||||
PeditTarget {
|
||||
is_poly: true,
|
||||
convertible: false,
|
||||
mesh_size: None,
|
||||
},
|
||||
);
|
||||
self.target = Some(nh);
|
||||
@@ -186,12 +221,17 @@ impl CadCommand for PeditCommand {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_pedit_applied(&mut self) {
|
||||
self.undo_count = self.undo_count.saturating_add(1);
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
!matches!(self.mode, Mode::PickTarget)
|
||||
!matches!(self.mode, Mode::PickTarget | Mode::MeshVertexMove(_))
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let up = text.trim().to_uppercase();
|
||||
let mesh_size = self.mesh_size();
|
||||
match &mut self.mode {
|
||||
Mode::PickTarget => None,
|
||||
Mode::ConvertPrompt(handle) => {
|
||||
@@ -222,8 +262,79 @@ impl CadCommand for PeditCommand {
|
||||
})
|
||||
}
|
||||
Mode::JoinGather(_) => None,
|
||||
Mode::MeshVertexMove(_) => None,
|
||||
Mode::MeshVertex(index) => {
|
||||
let (m, n) = mesh_size?;
|
||||
let count = m.saturating_mul(n);
|
||||
if count == 0 {
|
||||
self.mode = Mode::Options;
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
let row = *index / n;
|
||||
let column = *index % n;
|
||||
match up.as_str() {
|
||||
"N" | "NEXT" => *index = (*index + 1) % count,
|
||||
"P" | "PREVIOUS" => *index = (*index + count - 1) % count,
|
||||
"L" | "LEFT" => *index = row * n + (column + n - 1) % n,
|
||||
"R" | "RIGHT" => *index = row * n + (column + 1) % n,
|
||||
"U" | "UP" => *index = ((row + m - 1) % m) * n + column,
|
||||
"D" | "DOWN" => *index = ((row + 1) % m) * n + column,
|
||||
"M" | "MOVE" => self.mode = Mode::MeshVertexMove(*index),
|
||||
"G" | "REGEN" => {
|
||||
// Mesh display is regenerated after every edit; keep the
|
||||
// command active without creating a false undo record.
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
"X" | "EXIT" => self.mode = Mode::Options,
|
||||
_ => return None,
|
||||
}
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
Mode::Options => {
|
||||
let handle = self.target?;
|
||||
if mesh_size.is_some() {
|
||||
return match up.as_str() {
|
||||
"E" | "EDIT" | "EDIT VERTEX" => {
|
||||
self.mode = Mode::MeshVertex(0);
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"S" | "SMOOTH" | "SMOOTH SURFACE" => Some(CmdResult::PeditOp {
|
||||
handle,
|
||||
op: PeditOp::SetMeshSmooth(
|
||||
acadrust::entities::polygon_mesh::SurfaceSmoothType::Quadratic,
|
||||
),
|
||||
}),
|
||||
"D" | "DESMOOTH" => Some(CmdResult::PeditOp {
|
||||
handle,
|
||||
op: PeditOp::SetMeshSmooth(
|
||||
acadrust::entities::polygon_mesh::SurfaceSmoothType::NoSmooth,
|
||||
),
|
||||
}),
|
||||
"MC" | "MCLOSE" | "M CLOSE" => Some(CmdResult::PeditOp {
|
||||
handle,
|
||||
op: PeditOp::SetMeshClosedM(true),
|
||||
}),
|
||||
"MO" | "MOPEN" | "M OPEN" => Some(CmdResult::PeditOp {
|
||||
handle,
|
||||
op: PeditOp::SetMeshClosedM(false),
|
||||
}),
|
||||
"NC" | "NCLOSE" | "N CLOSE" => Some(CmdResult::PeditOp {
|
||||
handle,
|
||||
op: PeditOp::SetMeshClosedN(true),
|
||||
}),
|
||||
"NO" | "NOPEN" | "N OPEN" => Some(CmdResult::PeditOp {
|
||||
handle,
|
||||
op: PeditOp::SetMeshClosedN(false),
|
||||
}),
|
||||
"U" | "UNDO" if self.undo_count > 0 => {
|
||||
self.undo_count -= 1;
|
||||
Some(CmdResult::UndoDocument)
|
||||
}
|
||||
"U" | "UNDO" => Some(CmdResult::NeedPoint),
|
||||
"X" | "EXIT" => Some(CmdResult::Cancel),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
match up.as_str() {
|
||||
"X" | "EXIT" => Some(CmdResult::Cancel),
|
||||
"C" | "CLOSE" => Some(CmdResult::PeditOp {
|
||||
@@ -272,7 +383,17 @@ impl CadCommand for PeditCommand {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
|
||||
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||
if let Mode::MeshVertexMove(index) = self.mode {
|
||||
let Some(handle) = self.target else {
|
||||
return CmdResult::Cancel;
|
||||
};
|
||||
self.mode = Mode::MeshVertex(index);
|
||||
return CmdResult::PeditOp {
|
||||
handle,
|
||||
op: PeditOp::MoveMeshVertex { index, point },
|
||||
};
|
||||
}
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
@@ -303,6 +424,10 @@ pub enum PeditOp {
|
||||
Fit,
|
||||
Spline,
|
||||
Decurve,
|
||||
SetMeshClosedM(bool),
|
||||
SetMeshClosedN(bool),
|
||||
SetMeshSmooth(acadrust::entities::polygon_mesh::SurfaceSmoothType),
|
||||
MoveMeshVertex { index: usize, point: DVec3 },
|
||||
}
|
||||
|
||||
// ── Apply logic (pure entity edits; driver handles convert/break/marker) ──
|
||||
@@ -352,6 +477,71 @@ pub fn apply_pedit(entity: &mut EntityType, op: &PeditOp) -> bool {
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
PeditOp::SetMeshClosedM(closed) => match entity {
|
||||
EntityType::PolygonMesh(mesh) => {
|
||||
if mesh.is_closed_m() == *closed {
|
||||
return false;
|
||||
}
|
||||
mesh.flags.set(
|
||||
acadrust::entities::polygon_mesh::PolygonMeshFlags::CLOSED_M,
|
||||
*closed,
|
||||
);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
PeditOp::SetMeshClosedN(closed) => match entity {
|
||||
EntityType::PolygonMesh(mesh) => {
|
||||
if mesh.is_closed_n() == *closed {
|
||||
return false;
|
||||
}
|
||||
mesh.flags.set(
|
||||
acadrust::entities::polygon_mesh::PolygonMeshFlags::CLOSED_N,
|
||||
*closed,
|
||||
);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
PeditOp::SetMeshSmooth(smooth) => match entity {
|
||||
EntityType::PolygonMesh(mesh) => {
|
||||
let mut changed = mesh.smooth_type != *smooth;
|
||||
mesh.smooth_type = *smooth;
|
||||
if mesh.smooth_type
|
||||
!= acadrust::entities::polygon_mesh::SurfaceSmoothType::NoSmooth
|
||||
{
|
||||
if !(2..=256).contains(&mesh.m_smooth_density) {
|
||||
mesh.m_smooth_density = 6;
|
||||
changed = true;
|
||||
}
|
||||
if !(2..=256).contains(&mesh.n_smooth_density) {
|
||||
mesh.n_smooth_density = 6;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
changed
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
PeditOp::MoveMeshVertex { index, point } => match entity {
|
||||
EntityType::PolygonMesh(mesh) => {
|
||||
if !point.is_finite() {
|
||||
return false;
|
||||
}
|
||||
let Some(vertex) = mesh.vertices.get_mut(*index) else {
|
||||
return false;
|
||||
};
|
||||
if vertex.location.x == point.x
|
||||
&& vertex.location.y == point.y
|
||||
&& vertex.location.z == point.z
|
||||
{
|
||||
return false;
|
||||
}
|
||||
vertex.location = acadrust::types::Vector3::new(point.x, point.y, point.z);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
// Handled by the driver (it replaces the entity, not edits in place).
|
||||
PeditOp::ConvertToPolyline => false,
|
||||
}
|
||||
|
||||
@@ -200,12 +200,24 @@ impl PrimitiveCommand {
|
||||
)
|
||||
};
|
||||
let height_abs = height.abs();
|
||||
if length < 1e-6 || width < 1e-6 || height_abs < 1e-6 {
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -237,10 +249,13 @@ impl PrimitiveCommand {
|
||||
))
|
||||
}
|
||||
|
||||
fn set_box_corner_footprint(&mut self, point: DVec3) {
|
||||
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 {
|
||||
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),
|
||||
@@ -250,11 +265,12 @@ impl PrimitiveCommand {
|
||||
}
|
||||
self.box_step = BoxStep::Height;
|
||||
self.height_step = true;
|
||||
true
|
||||
}
|
||||
|
||||
fn set_box_cube(&mut self, size: f64) -> Option<CmdResult> {
|
||||
let size = size.abs();
|
||||
if size < 1e-6 {
|
||||
if !size.is_finite() || size < 1e-6 {
|
||||
return None;
|
||||
}
|
||||
let origin = if self.box_centered {
|
||||
@@ -270,7 +286,7 @@ impl PrimitiveCommand {
|
||||
|
||||
fn set_box_length(&mut self, length: f64, angle: f64) -> bool {
|
||||
let length = length.abs();
|
||||
if length < 1e-6 {
|
||||
if !length.is_finite() || !angle.is_finite() || length < 1e-6 {
|
||||
return false;
|
||||
}
|
||||
self.box_origin = self.pts.first().copied();
|
||||
@@ -282,7 +298,7 @@ impl PrimitiveCommand {
|
||||
|
||||
fn set_box_width(&mut self, width: f64, sign: f64) -> bool {
|
||||
let width = width.abs();
|
||||
if width < 1e-6 {
|
||||
if !width.is_finite() || !sign.is_finite() || width < 1e-6 {
|
||||
return false;
|
||||
}
|
||||
self.box_width = Some(width);
|
||||
@@ -424,9 +440,7 @@ impl PrimitiveCommand {
|
||||
fn commit(&self, height: f64) -> CmdResult {
|
||||
match self.build(height) {
|
||||
Some((solid, history)) => {
|
||||
// Built upright in its own frame, then put on the working
|
||||
// plane — the same move `place_entity` makes for the ACIS
|
||||
// copy, so the two stay on top of each other.
|
||||
// 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],
|
||||
@@ -468,7 +482,12 @@ impl CadCommand for PrimitiveCommand {
|
||||
}
|
||||
|
||||
fn cursor_axis(&self) -> Option<(DVec3, DVec3)> {
|
||||
self.height_step.then(|| {
|
||||
let constrained = if self.shape == Shape::Box {
|
||||
self.box_step == BoxStep::Height
|
||||
} else {
|
||||
self.height_step
|
||||
};
|
||||
constrained.then(|| {
|
||||
(
|
||||
self.plane.to_world(self.pts[0]),
|
||||
self.plane.z.normalize_or_zero(),
|
||||
@@ -538,6 +557,9 @@ impl CadCommand for PrimitiveCommand {
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
if self.shape == Shape::Box {
|
||||
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];
|
||||
@@ -625,7 +647,19 @@ impl CadCommand for PrimitiveCommand {
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
self.shape == Shape::Box || self.height_step
|
||||
if self.shape == Shape::Box {
|
||||
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 {
|
||||
@@ -658,24 +692,21 @@ impl CadCommand for PrimitiveCommand {
|
||||
self.box_step = BoxStep::HeightFirstPoint;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
BoxStep::CubeSize => token
|
||||
.replace(',', ".")
|
||||
.parse::<f64>()
|
||||
.ok()
|
||||
BoxStep::CubeSize => crate::entities::common::parse_typed_length(token)
|
||||
.and_then(|value| self.set_box_cube(value)),
|
||||
BoxStep::Length => {
|
||||
let value = token.replace(',', ".").parse::<f64>().ok()?;
|
||||
let value = crate::entities::common::parse_typed_length(token)?;
|
||||
self.set_box_length(value, 0.0)
|
||||
.then_some(CmdResult::NeedPoint)
|
||||
}
|
||||
BoxStep::Width => {
|
||||
let value = token.replace(',', ".").parse::<f64>().ok()?;
|
||||
let value = crate::entities::common::parse_typed_length(token)?;
|
||||
self.set_box_width(value, value)
|
||||
.then_some(CmdResult::NeedPoint)
|
||||
}
|
||||
BoxStep::Height => {
|
||||
let value = token.replace(',', ".").parse::<f64>().ok()?;
|
||||
(value.abs() >= 1e-6).then(|| self.commit(value))
|
||||
let value = crate::entities::common::parse_typed_length(token)?;
|
||||
(value.is_finite() && value.abs() >= 1e-6).then(|| self.commit(value))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
@@ -693,6 +724,9 @@ impl CadCommand for PrimitiveCommand {
|
||||
}
|
||||
if self.shape == Shape::Box {
|
||||
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];
|
||||
@@ -720,7 +754,11 @@ impl CadCommand for PrimitiveCommand {
|
||||
let mut preview = PrimitiveCommand::new("BOX");
|
||||
preview.plane = self.plane;
|
||||
preview.pts = self.pts.clone();
|
||||
preview.box_origin = Some(self.pts[0]);
|
||||
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]
|
||||
});
|
||||
preview.box_length = Some(size);
|
||||
preview.box_width = Some(size);
|
||||
preview.box_preview(size)
|
||||
@@ -768,15 +806,21 @@ impl CadCommand for PrimitiveCommand {
|
||||
fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
|
||||
use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, 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 {
|
||||
let role = if self.shape == Shape::Box {
|
||||
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(DynRole::Height)],
|
||||
fields: vec![DynFieldSpec::new(role)],
|
||||
guide: DynGuide::None,
|
||||
ref_point: None,
|
||||
})
|
||||
@@ -785,6 +829,9 @@ impl CadCommand for PrimitiveCommand {
|
||||
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
|
||||
if self.shape == Shape::Box {
|
||||
let local = self.plane.to_local(cursor);
|
||||
if !local.is_finite() {
|
||||
return None;
|
||||
}
|
||||
return match self.box_step {
|
||||
BoxStep::CubeSize | BoxStep::Length => Some((local - self.pts[0]).length()),
|
||||
BoxStep::Width => {
|
||||
|
||||
Reference in new issue
Block a user