|
|
|
@@ -1,20 +1,16 @@
|
|
|
|
|
// 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.
|
|
|
|
|
//! Repeating continued dimensions from the latest session dimension or a picked base.
|
|
|
|
|
|
|
|
|
|
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 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 +23,262 @@ 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: DVec3,
|
|
|
|
|
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: DVec3,
|
|
|
|
|
line_coordinate: f64,
|
|
|
|
|
},
|
|
|
|
|
Angular2Ln {
|
|
|
|
|
source: DimensionAngular2Ln,
|
|
|
|
|
vertex: DVec3,
|
|
|
|
|
current: DVec3,
|
|
|
|
|
radius: f64,
|
|
|
|
|
},
|
|
|
|
|
Angular3Pt {
|
|
|
|
|
source: DimensionAngular3Pt,
|
|
|
|
|
vertex: DVec3,
|
|
|
|
|
current: DVec3,
|
|
|
|
|
radius: f64,
|
|
|
|
|
},
|
|
|
|
|
Ordinate {
|
|
|
|
|
source: DimensionOrdinate,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
struct ContinueState {
|
|
|
|
|
kind: ContinueKind,
|
|
|
|
|
style: SourceStyle,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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: Option<DVec3>) -> bool {
|
|
|
|
|
let EntityType::Dimension(dimension) = entity else {
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
let style = SourceStyle::from_base(dimension.base());
|
|
|
|
|
let kind = match dimension {
|
|
|
|
|
Dimension::Linear(source) => {
|
|
|
|
|
let rotation = source.rotation;
|
|
|
|
|
let perpendicular = DVec3::new(-rotation.sin(), rotation.cos(), 0.0);
|
|
|
|
|
let current = selected_linear_end(
|
|
|
|
|
dv(source.first_point),
|
|
|
|
|
dv(source.second_point),
|
|
|
|
|
pick,
|
|
|
|
|
);
|
|
|
|
|
ContinueKind::Linear {
|
|
|
|
|
current,
|
|
|
|
|
rotation,
|
|
|
|
|
perpendicular,
|
|
|
|
|
line_coordinate: dv(source.base.definition_point).dot(perpendicular),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Dimension::Aligned(source) => {
|
|
|
|
|
let first = dv(source.first_point);
|
|
|
|
|
let second = dv(source.second_point);
|
|
|
|
|
let delta = second - first;
|
|
|
|
|
if delta.length_squared() <= 1.0e-18 {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
let rotation = delta.y.atan2(delta.x);
|
|
|
|
|
let perpendicular = DVec3::new(-rotation.sin(), rotation.cos(), 0.0);
|
|
|
|
|
ContinueKind::Linear {
|
|
|
|
|
current: selected_linear_end(first, second, pick),
|
|
|
|
|
rotation,
|
|
|
|
|
perpendicular,
|
|
|
|
|
line_coordinate: dv(source.base.definition_point).dot(perpendicular),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Dimension::Angular2Ln(source) => {
|
|
|
|
|
let vertex = angular2_vertex(source);
|
|
|
|
|
let first = dv(source.second_point);
|
|
|
|
|
let second = dv(source.definition_point);
|
|
|
|
|
ContinueKind::Angular2Ln {
|
|
|
|
|
source: source.clone(),
|
|
|
|
|
vertex,
|
|
|
|
|
current: selected_angular_end(first, second, pick),
|
|
|
|
|
radius: (dv(source.dimension_arc) - vertex).length().max(1.0e-9),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Dimension::Angular3Pt(source) => {
|
|
|
|
|
let vertex = dv(source.angle_vertex);
|
|
|
|
|
ContinueKind::Angular3Pt {
|
|
|
|
|
source: source.clone(),
|
|
|
|
|
vertex,
|
|
|
|
|
current: selected_angular_end(
|
|
|
|
|
dv(source.first_point),
|
|
|
|
|
dv(source.second_point),
|
|
|
|
|
pick,
|
|
|
|
|
),
|
|
|
|
|
radius: (dv(source.definition_point) - vertex).length().max(1.0e-9),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Dimension::Ordinate(source) => ContinueKind::Ordinate {
|
|
|
|
|
source: source.clone(),
|
|
|
|
|
},
|
|
|
|
|
_ => return false,
|
|
|
|
|
};
|
|
|
|
|
self.base = Some(ContinueState { kind, style });
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn build_dimension(&self, point: DVec3) -> Option<Dimension> {
|
|
|
|
|
let state = self.base.as_ref()?;
|
|
|
|
|
let mut dimension = match &state.kind {
|
|
|
|
|
ContinueKind::Linear {
|
|
|
|
|
current,
|
|
|
|
|
rotation,
|
|
|
|
|
perpendicular,
|
|
|
|
|
line_coordinate,
|
|
|
|
|
} => build_linear(
|
|
|
|
|
*current,
|
|
|
|
|
point,
|
|
|
|
|
*rotation,
|
|
|
|
|
*perpendicular,
|
|
|
|
|
*line_coordinate,
|
|
|
|
|
),
|
|
|
|
|
ContinueKind::Angular2Ln {
|
|
|
|
|
source,
|
|
|
|
|
vertex,
|
|
|
|
|
current,
|
|
|
|
|
radius,
|
|
|
|
|
} => {
|
|
|
|
|
let moving = point - *vertex;
|
|
|
|
|
let previous = *current - *vertex;
|
|
|
|
|
if moving.length_squared() <= 1.0e-18 || previous.length_squared() <= 1.0e-18 {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
let mut result = source.clone();
|
|
|
|
|
result.first_point = v3(*vertex);
|
|
|
|
|
result.second_point = v3(point);
|
|
|
|
|
result.angle_vertex = v3(*vertex);
|
|
|
|
|
result.definition_point = v3(*current);
|
|
|
|
|
let (definition, text) =
|
|
|
|
|
angular_definition_and_text(*vertex, previous, moving, *radius);
|
|
|
|
|
result.dimension_arc = v3(definition);
|
|
|
|
|
result.base.definition_point = result.dimension_arc;
|
|
|
|
|
result.base.text_middle_point = v3(text);
|
|
|
|
|
result.base.insertion_point = result.base.text_middle_point;
|
|
|
|
|
result.base.actual_measurement = result.measurement_degrees();
|
|
|
|
|
Dimension::Angular2Ln(result)
|
|
|
|
|
}
|
|
|
|
|
ContinueKind::Angular3Pt {
|
|
|
|
|
source,
|
|
|
|
|
vertex,
|
|
|
|
|
current,
|
|
|
|
|
radius,
|
|
|
|
|
} => {
|
|
|
|
|
let moving = point - *vertex;
|
|
|
|
|
let previous = *current - *vertex;
|
|
|
|
|
if moving.length_squared() <= 1.0e-18 || previous.length_squared() <= 1.0e-18 {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
let mut result = source.clone();
|
|
|
|
|
result.angle_vertex = v3(*vertex);
|
|
|
|
|
result.first_point = v3(point);
|
|
|
|
|
result.second_point = v3(*current);
|
|
|
|
|
let (definition, text) =
|
|
|
|
|
angular_definition_and_text(*vertex, previous, moving, *radius);
|
|
|
|
|
result.definition_point = v3(definition);
|
|
|
|
|
result.base.definition_point = result.definition_point;
|
|
|
|
|
result.base.text_middle_point = v3(text);
|
|
|
|
|
result.base.insertion_point = result.base.text_middle_point;
|
|
|
|
|
result.base.actual_measurement = result.measurement_degrees();
|
|
|
|
|
Dimension::Angular3Pt(result)
|
|
|
|
|
}
|
|
|
|
|
ContinueKind::Ordinate { source } => {
|
|
|
|
|
let source_leader = dv(source.leader_endpoint);
|
|
|
|
|
let leader = if source.is_ordinate_type_x {
|
|
|
|
|
DVec3::new(point.x, source_leader.y, source_leader.z)
|
|
|
|
|
} else {
|
|
|
|
|
DVec3::new(source_leader.x, point.y, source_leader.z)
|
|
|
|
|
};
|
|
|
|
|
let mut result =
|
|
|
|
|
DimensionOrdinate::new(v3(point), v3(leader), source.is_ordinate_type_x);
|
|
|
|
|
result.definition_point = source.definition_point;
|
|
|
|
|
result.base.definition_point = source.base.definition_point;
|
|
|
|
|
result.base.text_middle_point = v3(leader);
|
|
|
|
|
result.base.insertion_point = result.base.text_middle_point;
|
|
|
|
|
result.base.actual_measurement = result.measurement();
|
|
|
|
|
Dimension::Ordinate(result)
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
state.style.apply(dimension.base_mut());
|
|
|
|
|
dimension.base_mut().common.handle = Handle::NULL;
|
|
|
|
|
dimension.base_mut().block_name.clear();
|
|
|
|
|
Some(dimension)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn advance(&mut self, point: DVec3) {
|
|
|
|
|
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 +288,373 @@ 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,
|
|
|
|
|
keep_active: 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 selected_linear_end(first: DVec3, second: DVec3, pick: Option<DVec3>) -> DVec3 {
|
|
|
|
|
pick.map_or(second, |point| {
|
|
|
|
|
if point.distance_squared(first) <= point.distance_squared(second) {
|
|
|
|
|
first
|
|
|
|
|
} else {
|
|
|
|
|
second
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn selected_angular_end(first: DVec3, second: DVec3, pick: Option<DVec3>) -> DVec3 {
|
|
|
|
|
pick.map_or(first, |point| {
|
|
|
|
|
if point.distance_squared(first) <= point.distance_squared(second) {
|
|
|
|
|
first
|
|
|
|
|
} else {
|
|
|
|
|
second
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Autocomplete registry ─────────────────────────────────
|
|
|
|
|
inventory::submit!(crate::command::CommandRegistration { names: &["DIMCONTINUE"] }); // DimContinueCommand
|
|
|
|
|
fn build_linear(
|
|
|
|
|
first: DVec3,
|
|
|
|
|
second: DVec3,
|
|
|
|
|
rotation: f64,
|
|
|
|
|
perpendicular: DVec3,
|
|
|
|
|
line_coordinate: f64,
|
|
|
|
|
) -> Dimension {
|
|
|
|
|
let first_line = first + perpendicular * (line_coordinate - first.dot(perpendicular));
|
|
|
|
|
let second_line = second + perpendicular * (line_coordinate - second.dot(perpendicular));
|
|
|
|
|
let mut result = DimensionLinear::new(v3(first), v3(second));
|
|
|
|
|
result.rotation = rotation;
|
|
|
|
|
result.definition_point = v3(second_line);
|
|
|
|
|
result.base.definition_point = v3(second_line);
|
|
|
|
|
result.base.text_middle_point = v3((first_line + second_line) * 0.5);
|
|
|
|
|
result.base.insertion_point = result.base.text_middle_point;
|
|
|
|
|
result.base.actual_measurement = result.measurement();
|
|
|
|
|
Dimension::Linear(result)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn angular2_vertex(source: &DimensionAngular2Ln) -> DVec3 {
|
|
|
|
|
let first = dv(source.first_point);
|
|
|
|
|
let first_direction = dv(source.second_point) - first;
|
|
|
|
|
let second = dv(source.angle_vertex);
|
|
|
|
|
let second_direction = dv(source.definition_point) - second;
|
|
|
|
|
line_intersection(first, first_direction, second, second_direction).unwrap_or(second)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn line_intersection(a: DVec3, b: DVec3, c: DVec3, d: DVec3) -> Option<DVec3> {
|
|
|
|
|
let cross = b.x * d.y - b.y * d.x;
|
|
|
|
|
if cross.abs() <= 1.0e-12 {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
let delta = c - a;
|
|
|
|
|
Some(a + b * ((delta.x * d.y - delta.y * d.x) / cross))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn angular_definition_and_text(
|
|
|
|
|
vertex: DVec3,
|
|
|
|
|
previous: DVec3,
|
|
|
|
|
moving: DVec3,
|
|
|
|
|
radius: f64,
|
|
|
|
|
) -> (DVec3, DVec3) {
|
|
|
|
|
let start = previous.y.atan2(previous.x);
|
|
|
|
|
let mut sweep = moving.y.atan2(moving.x) - start;
|
|
|
|
|
while sweep <= -std::f64::consts::PI {
|
|
|
|
|
sweep += std::f64::consts::TAU;
|
|
|
|
|
}
|
|
|
|
|
while sweep > std::f64::consts::PI {
|
|
|
|
|
sweep -= std::f64::consts::TAU;
|
|
|
|
|
}
|
|
|
|
|
let point_at = |fraction: f64| {
|
|
|
|
|
let angle = start + sweep * fraction;
|
|
|
|
|
vertex + DVec3::new(angle.cos(), angle.sin(), 0.0) * radius
|
|
|
|
|
};
|
|
|
|
|
(point_at(2.0 / 3.0), point_at(0.5))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn preview_for_dimension(dimension: &Dimension) -> WireModel {
|
|
|
|
|
let mut points = Vec::<[f64; 3]>::new();
|
|
|
|
|
match dimension {
|
|
|
|
|
Dimension::Linear(dimension) => linear_preview(
|
|
|
|
|
&mut points,
|
|
|
|
|
dv(dimension.first_point),
|
|
|
|
|
dv(dimension.second_point),
|
|
|
|
|
dimension.rotation,
|
|
|
|
|
dv(dimension.definition_point),
|
|
|
|
|
dv(dimension.base.text_middle_point),
|
|
|
|
|
dimension.base.actual_measurement,
|
|
|
|
|
),
|
|
|
|
|
Dimension::Angular2Ln(dimension) => {
|
|
|
|
|
let vertex = angular2_vertex(dimension);
|
|
|
|
|
angular_preview(
|
|
|
|
|
&mut points,
|
|
|
|
|
vertex,
|
|
|
|
|
dv(dimension.second_point) - vertex,
|
|
|
|
|
dv(dimension.definition_point) - vertex,
|
|
|
|
|
dv(dimension.dimension_arc),
|
|
|
|
|
dv(dimension.base.text_middle_point),
|
|
|
|
|
dimension.base.actual_measurement,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
Dimension::Angular3Pt(dimension) => angular_preview(
|
|
|
|
|
&mut points,
|
|
|
|
|
dv(dimension.angle_vertex),
|
|
|
|
|
dv(dimension.first_point) - dv(dimension.angle_vertex),
|
|
|
|
|
dv(dimension.second_point) - dv(dimension.angle_vertex),
|
|
|
|
|
dv(dimension.definition_point),
|
|
|
|
|
dv(dimension.base.text_middle_point),
|
|
|
|
|
dimension.base.actual_measurement,
|
|
|
|
|
),
|
|
|
|
|
Dimension::Ordinate(dimension) => ordinate_preview(
|
|
|
|
|
&mut points,
|
|
|
|
|
dv(dimension.feature_location),
|
|
|
|
|
dv(dimension.leader_endpoint),
|
|
|
|
|
dimension.base.actual_measurement,
|
|
|
|
|
),
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
WireModel::solid_f64(
|
|
|
|
|
"dimcontinue_preview".to_string(),
|
|
|
|
|
points,
|
|
|
|
|
WireModel::CYAN,
|
|
|
|
|
false,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn linear_preview(
|
|
|
|
|
points: &mut Vec<[f64; 3]>,
|
|
|
|
|
first: DVec3,
|
|
|
|
|
second: DVec3,
|
|
|
|
|
rotation: f64,
|
|
|
|
|
definition: DVec3,
|
|
|
|
|
text: DVec3,
|
|
|
|
|
measurement: f64,
|
|
|
|
|
) {
|
|
|
|
|
let axis = DVec3::new(rotation.cos(), rotation.sin(), 0.0);
|
|
|
|
|
let perpendicular = DVec3::new(-axis.y, axis.x, 0.0);
|
|
|
|
|
let coordinate = definition.dot(perpendicular);
|
|
|
|
|
let first_line = first + perpendicular * (coordinate - first.dot(perpendicular));
|
|
|
|
|
let second_line = second + perpendicular * (coordinate - second.dot(perpendicular));
|
|
|
|
|
segment(points, first, first_line);
|
|
|
|
|
segment(points, second, second_line);
|
|
|
|
|
segment(points, first_line, second_line);
|
|
|
|
|
arrow(points, first_line, axis, perpendicular, first_line.distance(second_line));
|
|
|
|
|
arrow(points, second_line, -axis, perpendicular, first_line.distance(second_line));
|
|
|
|
|
text_box(points, text, axis, perpendicular, measurement);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn angular_preview(
|
|
|
|
|
points: &mut Vec<[f64; 3]>,
|
|
|
|
|
vertex: DVec3,
|
|
|
|
|
first: DVec3,
|
|
|
|
|
second: DVec3,
|
|
|
|
|
arc_point: DVec3,
|
|
|
|
|
text: DVec3,
|
|
|
|
|
measurement: f64,
|
|
|
|
|
) {
|
|
|
|
|
let radius = (arc_point - vertex).length().max(1.0e-9);
|
|
|
|
|
let start = first.y.atan2(first.x);
|
|
|
|
|
let mut sweep = second.y.atan2(second.x) - start;
|
|
|
|
|
while sweep <= -std::f64::consts::PI {
|
|
|
|
|
sweep += std::f64::consts::TAU;
|
|
|
|
|
}
|
|
|
|
|
while sweep > std::f64::consts::PI {
|
|
|
|
|
sweep -= std::f64::consts::TAU;
|
|
|
|
|
}
|
|
|
|
|
let first_end = vertex + normalized_or(first, DVec3::X) * radius;
|
|
|
|
|
let second_end = vertex + normalized_or(second, DVec3::Y) * radius;
|
|
|
|
|
segment(points, vertex, first_end);
|
|
|
|
|
segment(points, vertex, second_end);
|
|
|
|
|
let arc = cadkernel::geom2d::tessellate::arc(
|
|
|
|
|
[vertex.x, vertex.y],
|
|
|
|
|
radius,
|
|
|
|
|
start,
|
|
|
|
|
start + sweep,
|
|
|
|
|
vertex.z,
|
|
|
|
|
cadkernel::geom2d::tessellate::DEFAULT_SEGMENTS_PER_RADIAN,
|
|
|
|
|
);
|
|
|
|
|
points.extend(arc);
|
|
|
|
|
points.push([f64::NAN, f64::NAN, f64::NAN]);
|
|
|
|
|
let sign = sweep.signum();
|
|
|
|
|
let start_tangent = DVec3::new(-start.sin(), start.cos(), 0.0) * sign;
|
|
|
|
|
let end_angle = start + sweep;
|
|
|
|
|
let end_tangent = DVec3::new(end_angle.sin(), -end_angle.cos(), 0.0) * sign;
|
|
|
|
|
arrow(points, first_end, start_tangent, normalized_or(first, DVec3::X), radius);
|
|
|
|
|
arrow(points, second_end, end_tangent, normalized_or(second, DVec3::Y), radius);
|
|
|
|
|
let middle = start + sweep * 0.5;
|
|
|
|
|
let text_axis = normalized_or(DVec3::new(-middle.sin(), middle.cos(), 0.0), DVec3::X);
|
|
|
|
|
let text_perpendicular = DVec3::new(-text_axis.y, text_axis.x, 0.0);
|
|
|
|
|
text_box(points, text, text_axis, text_perpendicular, measurement);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn ordinate_preview(
|
|
|
|
|
points: &mut Vec<[f64; 3]>,
|
|
|
|
|
feature: DVec3,
|
|
|
|
|
leader: DVec3,
|
|
|
|
|
measurement: f64,
|
|
|
|
|
) {
|
|
|
|
|
segment(points, feature, leader);
|
|
|
|
|
let axis = normalized_or(leader - feature, DVec3::Y);
|
|
|
|
|
let perpendicular = DVec3::new(-axis.y, axis.x, 0.0);
|
|
|
|
|
arrow(points, feature, axis, perpendicular, feature.distance(leader));
|
|
|
|
|
text_box(points, leader, perpendicular, axis, measurement);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn arrow(
|
|
|
|
|
points: &mut Vec<[f64; 3]>,
|
|
|
|
|
tip: DVec3,
|
|
|
|
|
inward: DVec3,
|
|
|
|
|
perpendicular: DVec3,
|
|
|
|
|
span: f64,
|
|
|
|
|
) {
|
|
|
|
|
let inward = normalized_or(inward, DVec3::X);
|
|
|
|
|
let perpendicular = normalized_or(perpendicular, DVec3::Y);
|
|
|
|
|
let size = span.clamp(1.0, 100.0) * 0.035;
|
|
|
|
|
let back = tip + inward * size;
|
|
|
|
|
segment(points, tip, back + perpendicular * size * 0.4);
|
|
|
|
|
segment(points, tip, back - perpendicular * size * 0.4);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn text_box(
|
|
|
|
|
points: &mut Vec<[f64; 3]>,
|
|
|
|
|
center: DVec3,
|
|
|
|
|
axis: DVec3,
|
|
|
|
|
perpendicular: DVec3,
|
|
|
|
|
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;
|
|
|
|
|
points.extend([a.to_array(), b.to_array(), c.to_array(), d.to_array(), a.to_array()]);
|
|
|
|
|
points.push([f64::NAN, f64::NAN, f64::NAN]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn segment(points: &mut Vec<[f64; 3]>, first: DVec3, second: DVec3) {
|
|
|
|
|
points.push(first.to_array());
|
|
|
|
|
points.push(second.to_array());
|
|
|
|
|
points.push([f64::NAN, f64::NAN, f64::NAN]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn normalized_or(value: DVec3, fallback: DVec3) -> DVec3 {
|
|
|
|
|
if value.length_squared() > 1.0e-18 {
|
|
|
|
|
value.normalize()
|
|
|
|
|
} else {
|
|
|
|
|
fallback
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn dv(point: Vector3) -> DVec3 {
|
|
|
|
|
DVec3::new(point.x, point.y, point.z)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn v3(point: DVec3) -> Vector3 {
|
|
|
|
|
Vector3::new(point.x, point.y, point.z)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
inventory::submit!(crate::command::CommandRegistration { names: &["DIMCONTINUE"] });
|