fix(mleader): repair creation and editing

This commit is contained in:
Hakan Seven committed 2026-08-28 19:36:48 +03:00
commit ff1f873633
40 files changed
+4504 -1754

No files matched your search

+2
View File
@@ -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,
}
}
}
+2
View File
@@ -234,6 +234,8 @@ impl AngularDimensionCommand {
CmdResult::CommitDimension {
entity,
association: DimensionAssociationInput::Explicit(self.source_refs.clone()),
preserve_base_style: false,
continue_command: false,
}
}
+2
View File
@@ -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,
}
}
}
+2
View File
@@ -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
+791 -163
View File
@@ -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,
}
}
}
+2
View File
@@ -187,6 +187,8 @@ impl CadCommand for LinearDimensionCommand {
CmdResult::CommitDimension {
entity,
association: DimensionAssociationInput::Infer(self.source_handle),
preserve_base_style: false,
continue_command: false,
}
}
}
+16 -4
View File
@@ -236,19 +236,31 @@ impl MLeaderCommand {
acadrust::entities::multileader::FlowDirectionType::ByStyle
}
};
ml.context.text_attachment_point = match source.attachment_point {
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::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::multileader::TextAttachmentPointType::Right,
acadrust::entities::TextAlignmentType::Right,
)
}
_ => acadrust::entities::multileader::TextAttachmentPointType::Left,
_ => (
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()
+56 -65
View File
@@ -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,12 +125,16 @@ impl CadCommand for MLeaderAddCommand {
return CmdResult::Cancel;
}
let h = *handle;
if let EntityType::MultiLeader(ref mut ml) = entity {
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;
@@ -141,12 +149,22 @@ impl CadCommand for MLeaderAddCommand {
root.text_attachment_direction = template.text_attachment_direction;
}
let line = root.create_line(points);
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;
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]);
@@ -252,10 +270,10 @@ impl CadCommand for MLeaderRemoveCommand {
let mut distance = line
.points
.windows(2)
.map(|segment| point_segment_distance_sq_xy(pick, segment[0], segment[1]))
.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_sq_xy(
distance = distance.min(point_segment_distance_xy(
pick,
last,
root.connection_point,
@@ -265,16 +283,19 @@ impl CadCommand for MLeaderRemoveCommand {
})
})
.min_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal));
if let Some((root_index, line_index, _)) = best {
let total_lines = ml.total_leader_line_count();
if total_lines > 1 {
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);
}
}
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]);
}
@@ -370,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.y,
pt.x,
pt.y
);
// 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,
}
@@ -463,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.y
);
if let Some(&first) = h.first() {
return CmdResult::ReplaceEntity(first, vec![EntityType::XLine(xl)]);
}
return CmdResult::CollectMLeaders {
handles: handles.clone(),
point: pt,
};
}
CmdResult::NeedPoint
}
@@ -528,20 +524,15 @@ fn preview_wire(pts: &[Vec3]) -> WireModel {
}
}
fn point_segment_distance_sq_xy(point: Vector3, start: Vector3, end: Vector3) -> f64 {
let dx = end.x - start.x;
let dy = end.y - start.y;
let len_sq = dx * dx + dy * dy;
if len_sq <= 1.0e-18 {
let px = point.x - start.x;
let py = point.y - start.y;
return px * px + py * py;
}
let t = (((point.x - start.x) * dx + (point.y - start.y) * dy) / len_sq)
.clamp(0.0, 1.0);
let px = point.x - (start.x + t * dx);
let py = point.y - (start.y + t * dy);
px * px + py * py
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
+2
View File
@@ -116,6 +116,8 @@ impl CadCommand for OrdinateDimCommand {
Dimension::Ordinate(dim),
)),
association: DimensionAssociationInput::Infer(None),
preserve_base_style: false,
continue_command: false,
}
}
}
+362 -105
View File
@@ -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"] });
+2
View File
@@ -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,
}
}
}