fix(dimarc): complete merge integration

Preserve source-plane geometry and associative arc parameters while combining the latest properties and tolerance workflows.
This commit is contained in:
Hakan Seven committed 2026-08-27 01:00:23 +03:00
commit 6ef2d75e84
67 files changed
+4773 -883

No files matched your search

+28 -43
View File
@@ -11,7 +11,7 @@ use crate::command::{
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::dimension_assoc::{
polyline_arc_point_marker, RadialSourceGeometry,
POLYLINE_ARC_CENTER_MARKER,
ARC_DIMENSION_POINT_MARKER, POLYLINE_ARC_CENTER_MARKER,
};
use crate::scene::model::wire_model::WireModel;
@@ -66,6 +66,11 @@ impl ArcSelection {
dvec(self.source.center_world())
}
fn project(self, point: DVec3) -> Option<DVec3> {
let point = self.source.plane.project(point.to_array())?;
Some(DVec3::from_array(self.source.plane.point_at(point)))
}
fn plane(self) -> WorkingPlane {
WorkingPlane::new(
DVec3::from_array(self.source.plane.origin),
@@ -112,18 +117,22 @@ impl ArcSelection {
}
fn association_sources(self, handle: Handle) -> Vec<Option<DimensionAssociationSource>> {
let source_sweep = positive_sweep(self.source.start_angle, self.source.end_angle);
let parameter = |angle: f64| {
((angle - self.source.start_angle) / source_sweep).clamp(0.0, 1.0)
};
match self.binding {
SourceBinding::Arc => vec![
Some(DimensionAssociationSource::explicit(handle, -3, 0.0)),
Some(DimensionAssociationSource::explicit(
handle,
-2,
self.start_angle,
ARC_DIMENSION_POINT_MARKER,
parameter(self.start_angle),
)),
Some(DimensionAssociationSource::explicit(
handle,
-2,
self.end_angle,
ARC_DIMENSION_POINT_MARKER,
parameter(self.end_angle),
)),
],
SourceBinding::PolylineSegment(segment) => {
@@ -137,12 +146,12 @@ impl ArcSelection {
Some(DimensionAssociationSource::explicit(
handle,
point_marker,
self.start_angle,
parameter(self.start_angle),
)),
Some(DimensionAssociationSource::explicit(
handle,
point_marker,
self.end_angle,
parameter(self.end_angle),
)),
]
}
@@ -202,6 +211,9 @@ impl ArcLengthDimensionCommand {
let center = plane.to_local(selection.center());
let first = plane.to_local(selection.point_at(selection.start_angle));
let second = plane.to_local(selection.point_at(selection.end_angle));
let Some(point) = selection.project(point) else {
return CmdResult::NeedPoint;
};
let picked = plane.to_local(point);
let picked_radius = (picked - center).truncate().length();
if !picked_radius.is_finite() || picked_radius <= 1.0e-12 {
@@ -520,6 +532,9 @@ fn dimension_preview(
let center = plane.to_local(selection.center());
let first = plane.to_local(selection.point_at(selection.start_angle));
let second = plane.to_local(selection.point_at(selection.end_angle));
let Some(point) = selection.project(point) else {
return preview_wire(Vec::new(), "dimarc_preview");
};
let point = plane.to_local(point);
let radius = (point - center).truncate().length();
if radius <= 1.0e-12 {
@@ -597,42 +612,12 @@ fn nan() -> DVec3 {
}
fn preview_wire(points: Vec<DVec3>, name: &str) -> WireModel {
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: name.to_string(),
points: points
.into_iter()
.map(|point| [point.x as f32, point.y as f32, point.z as f32])
.collect(),
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(),
}
WireModel::solid_f64(
name.to_string(),
points.into_iter().map(|point| point.to_array()).collect(),
WireModel::CYAN,
false,
)
}
inventory::submit!(crate::command::CommandRegistration { names: &["DIMARC"] });
+358 -20
View File
@@ -1,7 +1,11 @@
use crate::command::{CadCommand, CmdResult};
use acadrust::entities::mtext::AttachmentPoint;
use acadrust::types::Vector3;
use acadrust::MText;
use glam::DVec3;
use crate::command::{CadCommand, CmdOption, CmdResult, DynField, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
use glam::DVec3;
use crate::t;
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/mtext.svg"));
@@ -15,20 +19,180 @@ pub fn tool() -> ToolDef {
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Step {
InsertPoint,
FirstCorner,
OppositeCorner,
Height,
Justify,
LineSpacing,
Rotation,
Style,
Width,
ColumnMode,
ColumnCount,
ColumnWidth,
ColumnGutter,
}
pub struct MTextCommand {
step: Step,
first: Option<DVec3>,
plane: WorkingPlane,
height: f64,
style: String,
attachment: AttachmentPoint,
line_spacing: f64,
rotation: f64,
width: Option<f64>,
column_type: i16,
column_count: i32,
column_width: f64,
column_gutter: f64,
}
impl MTextCommand {
pub fn with_height(height: f64) -> Self {
pub fn with_defaults(height: f64, style: String) -> Self {
Self {
step: Step::InsertPoint,
height,
step: Step::FirstCorner,
first: None,
plane: WorkingPlane::default(),
height: height.max(1e-6),
style,
attachment: AttachmentPoint::TopLeft,
line_spacing: 1.0,
rotation: 0.0,
width: None,
column_type: 0,
column_count: 2,
column_width: 0.0,
column_gutter: 0.0,
}
}
fn resume_point_step(&mut self) {
self.step = if self.first.is_some() {
Step::OppositeCorner
} else {
Step::FirstCorner
};
}
fn point_options() -> Vec<CmdOption> {
vec![
CmdOption::new("Height", "HEIGHT"),
CmdOption::new("Justify", "JUSTIFY"),
CmdOption::new("Line spacing", "LINESPACING"),
CmdOption::new("Rotation", "ROTATION"),
CmdOption::new("Style", "STYLE"),
CmdOption::new("Width", "WIDTH"),
CmdOption::new("Columns", "COLUMNS"),
]
}
fn begin_option(&mut self, keyword: &str) -> bool {
self.step = match keyword {
"H" | "HEIGHT" => Step::Height,
"J" | "JUSTIFY" => Step::Justify,
"L" | "LINESPACING" | "LINE SPACING" => Step::LineSpacing,
"R" | "ROTATION" => Step::Rotation,
"S" | "STYLE" => Step::Style,
"W" | "WIDTH" => Step::Width,
"C" | "COLUMNS" => Step::ColumnMode,
_ => return false,
};
true
}
fn boundary_geometry(&self, opposite: DVec3) -> (DVec3, f64, f64) {
let first = self.first.unwrap_or(opposite);
let local = self.plane.vector_to_local(opposite - first);
let (sin, cos) = self.rotation.sin_cos();
let picked_horizontal = local.x * cos + local.y * sin;
let vertical = -local.x * sin + local.y * cos;
let width = self.width.unwrap_or(picked_horizontal.abs());
let horizontal = if self.width.is_some() {
width.copysign(if picked_horizontal.abs() > 1e-12 {
picked_horizontal
} else {
1.0
})
} else {
picked_horizontal
};
let (xmin, xmax) = (horizontal.min(0.0), horizontal.max(0.0));
let (ymin, ymax) = (vertical.min(0.0), vertical.max(0.0));
let horizontal_anchor = match self.attachment {
AttachmentPoint::TopLeft
| AttachmentPoint::MiddleLeft
| AttachmentPoint::BottomLeft => 0.0,
AttachmentPoint::TopCenter
| AttachmentPoint::MiddleCenter
| AttachmentPoint::BottomCenter => 0.5,
AttachmentPoint::TopRight
| AttachmentPoint::MiddleRight
| AttachmentPoint::BottomRight => 1.0,
};
let vertical_anchor = match self.attachment {
AttachmentPoint::TopLeft
| AttachmentPoint::TopCenter
| AttachmentPoint::TopRight => 1.0,
AttachmentPoint::MiddleLeft
| AttachmentPoint::MiddleCenter
| AttachmentPoint::MiddleRight => 0.5,
AttachmentPoint::BottomLeft
| AttachmentPoint::BottomCenter
| AttachmentPoint::BottomRight => 0.0,
};
let anchor_x = xmin + (xmax - xmin) * horizontal_anchor;
let anchor_y = ymin + (ymax - ymin) * vertical_anchor;
let offset = DVec3::new(
anchor_x * cos - anchor_y * sin,
anchor_x * sin + anchor_y * cos,
0.0,
);
(
first + self.plane.vector_to_world(offset),
width,
vertical.abs(),
)
}
fn open_editor(&self, opposite: DVec3) -> CmdResult {
let (insertion, width, boundary_height) = self.boundary_geometry(opposite);
let mut template = MText::default();
template.insertion_point = Vector3::new(insertion.x, insertion.y, insertion.z);
template.height = self.height;
template.rectangle_width = width.max(0.0);
template.rectangle_height = (boundary_height > 1e-9).then_some(boundary_height);
template.rotation = self.rotation;
template.style = self.style.clone();
template.attachment_point = self.attachment;
template.line_spacing_factor = self.line_spacing;
template.column_data.column_type = self.column_type;
if self.column_type != 0 {
template.column_data.column_count =
crate::entities::text_support::clamp_mtext_column_count(self.column_count);
template.column_data.auto_height = self.column_type == 2;
template.column_data.width = if self.column_width > 0.0 {
self.column_width
} else if width > 0.0 {
width
} else {
self.height * 10.0
};
template.column_data.gutter = if self.column_gutter > 0.0 {
self.column_gutter
} else {
self.height
};
}
CmdResult::OpenMTextEditor {
pos: insertion,
handle: None,
initial: String::new(),
height: self.height,
template: Some(Box::new(template)),
}
}
}
@@ -38,34 +202,208 @@ impl CadCommand for MTextCommand {
"MTEXT"
}
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn prompt(&self) -> String {
match &self.step {
Step::InsertPoint => t!("MTEXT Specify insertion point:").into_owned(),
match self.step {
Step::FirstCorner => t!("MTEXT Specify first corner:").into_owned(),
Step::OppositeCorner => t!("MTEXT Specify opposite corner:").into_owned(),
Step::Height => crate::tf!("MTEXT Specify text height <{}>:", self.height).into_owned(),
Step::Justify => t!("MTEXT Enter justification [TL / TC / TR / ML / MC / MR / BL / BC / BR] <TL>:").into_owned(),
Step::LineSpacing => crate::tf!("MTEXT Enter line spacing factor (0.25-4.00) <{}>:", self.line_spacing).into_owned(),
Step::Rotation => crate::tf!("MTEXT Specify rotation angle <{}>:", self.rotation.to_degrees()).into_owned(),
Step::Style => crate::tf!("MTEXT Enter text style <{}>:", self.style).into_owned(),
Step::Width => crate::tf!("MTEXT Specify boundary width <{}>:", self.width.unwrap_or(0.0)).into_owned(),
Step::ColumnMode => t!("MTEXT Columns [None / Static / Dynamic] <None>:").into_owned(),
Step::ColumnCount => crate::tf!("MTEXT Enter column count <{}>:", self.column_count).into_owned(),
Step::ColumnWidth => crate::tf!("MTEXT Enter column width <{}>:", self.column_width).into_owned(),
Step::ColumnGutter => crate::tf!("MTEXT Enter column gutter <{}>:", self.column_gutter).into_owned(),
}
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
// Hand off to the in-place editor (toolbar + text area + live preview).
CmdResult::OpenMTextEditor {
pos: pt,
handle: None,
initial: String::new(),
height: self.height,
fn options(&self) -> Vec<CmdOption> {
match self.step {
Step::FirstCorner | Step::OppositeCorner => Self::point_options(),
Step::Justify => ["TL", "TC", "TR", "ML", "MC", "MR", "BL", "BC", "BR"]
.into_iter()
.map(|value| CmdOption::new(value, value))
.collect(),
Step::ColumnMode => vec![
CmdOption::new("None", "NONE"),
CmdOption::new("Static", "STATIC"),
CmdOption::new("Dynamic", "DYNAMIC"),
],
_ => Vec::new(),
}
}
fn point_step_accepts_keywords(&self) -> bool {
matches!(self.step, Step::FirstCorner | Step::OppositeCorner)
}
fn wants_text_input(&self) -> bool {
true
}
fn wants_text_with_spaces(&self) -> bool {
matches!(self.step, Step::Style)
}
fn dyn_field(&self) -> DynField {
match self.step {
Step::Rotation => DynField::Angle,
Step::Height
| Step::LineSpacing
| Step::Width
| Step::ColumnCount
| Step::ColumnWidth
| Step::ColumnGutter => DynField::Scalar,
_ => DynField::Point,
}
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let trimmed = text.trim();
let upper = trimmed.to_ascii_uppercase();
if matches!(self.step, Step::FirstCorner | Step::OppositeCorner)
&& self.begin_option(&upper)
{
return Some(CmdResult::NeedPoint);
}
match self.step {
Step::Height => {
let value = crate::entities::common::parse_typed_length(trimmed)?;
if value <= 0.0 {
return None;
}
self.height = value;
self.resume_point_step();
}
Step::Justify => {
self.attachment = match upper.as_str() {
"TL" => AttachmentPoint::TopLeft,
"TC" => AttachmentPoint::TopCenter,
"TR" => AttachmentPoint::TopRight,
"ML" => AttachmentPoint::MiddleLeft,
"MC" => AttachmentPoint::MiddleCenter,
"MR" => AttachmentPoint::MiddleRight,
"BL" => AttachmentPoint::BottomLeft,
"BC" => AttachmentPoint::BottomCenter,
"BR" => AttachmentPoint::BottomRight,
_ => return None,
};
self.resume_point_step();
}
Step::LineSpacing => {
let value = trimmed.replace(',', ".").parse::<f64>().ok()?;
if !(0.25..=4.0).contains(&value) {
return None;
}
self.line_spacing = value;
self.resume_point_step();
}
Step::Rotation => {
self.rotation = crate::entities::common::parse_typed_direction(trimmed)?;
self.resume_point_step();
}
Step::Style => {
if trimmed.is_empty() {
return None;
}
self.style = trimmed.to_string();
self.resume_point_step();
}
Step::Width => {
let value = crate::entities::common::parse_typed_length(trimmed)?;
if value < 0.0 {
return None;
}
self.width = Some(value);
self.resume_point_step();
}
Step::ColumnMode => {
self.column_type = match upper.as_str() {
"N" | "NONE" => 0,
"S" | "STATIC" => 1,
"D" | "DYNAMIC" => 2,
_ => return None,
};
if self.column_type == 0 {
self.resume_point_step();
} else {
self.step = Step::ColumnCount;
}
}
Step::ColumnCount => {
let value = trimmed.parse::<i32>().ok()?;
if !(1..=crate::entities::text_support::MAX_MTEXT_COLUMNS).contains(&value) {
return None;
}
self.column_count = value;
self.step = Step::ColumnWidth;
}
Step::ColumnWidth => {
let value = crate::entities::common::parse_typed_length(trimmed)?;
if value <= 0.0 {
return None;
}
self.column_width = value;
self.step = Step::ColumnGutter;
}
Step::ColumnGutter => {
let value = crate::entities::common::parse_typed_length(trimmed)?;
if value < 0.0 {
return None;
}
self.column_gutter = value;
self.resume_point_step();
}
Step::FirstCorner | Step::OppositeCorner => return None,
}
Some(CmdResult::NeedPoint)
}
fn on_point(&mut self, point: DVec3) -> CmdResult {
match self.step {
Step::FirstCorner => {
self.first = Some(point);
self.step = Step::OppositeCorner;
CmdResult::NeedPoint
}
Step::OppositeCorner => self.open_editor(point),
_ => CmdResult::NeedPoint,
}
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
match self.step {
Step::Height
| Step::Justify
| Step::LineSpacing
| Step::Rotation
| Step::Style
| Step::Width
| Step::ColumnMode
| Step::ColumnCount
| Step::ColumnWidth
| Step::ColumnGutter => {
self.resume_point_step();
CmdResult::NeedPoint
}
_ => CmdResult::Cancel,
}
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_mouse_move(&mut self, _pt: DVec3) -> Option<WireModel> {
fn on_mouse_move(&mut self, _point: DVec3) -> Option<WireModel> {
None
}
}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["MTEXT"] }); // MTextCommand
inventory::submit!(crate::command::CommandRegistration { names: &["MTEXT"] });
+406 -22
View File
@@ -1,7 +1,14 @@
use crate::command::{CadCommand, CmdResult};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
use acadrust::entities::{
Text, TextHorizontalAlignment as HA, TextVerticalAlignment as VA,
};
use acadrust::tables::TextStyle;
use acadrust::types::Vector3;
use glam::DVec3;
use crate::command::{CadCommand, CmdOption, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::creation_style::TextCreationDefaults;
use crate::scene::model::wire_model::WireModel;
use crate::t;
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/text.svg"));
@@ -15,57 +22,434 @@ pub fn tool() -> ToolDef {
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Step {
InsertPoint,
Start,
Justification,
Style,
Height,
Rotation,
SecondPoint,
}
pub struct TextCommand {
step: Step,
plane: WorkingPlane,
first_point: Option<DVec3>,
second_point: Option<DVec3>,
horizontal: HA,
vertical: VA,
style_name: String,
styles: Vec<TextStyle>,
height: f64,
rotation: f64,
width_factor: f64,
oblique_angle: f64,
fixed_height: bool,
annotative: bool,
annotation_multiplier: f64,
last_display_height: Option<f64>,
last_entity: Option<Text>,
}
impl TextCommand {
pub fn with_height(height: f64) -> Self {
Self {
step: Step::InsertPoint,
height,
pub fn with_defaults(
defaults: TextCreationDefaults,
styles: Vec<TextStyle>,
annotation_multiplier: f64,
) -> Self {
let current_height = defaults.height;
let mut command = Self {
step: Step::Start,
plane: WorkingPlane::default(),
first_point: None,
second_point: None,
horizontal: HA::Left,
vertical: VA::Baseline,
style_name: defaults.style_name,
styles,
height: defaults.height,
rotation: 0.0,
width_factor: defaults.width_factor,
oblique_angle: defaults.oblique_angle,
fixed_height: false,
annotative: false,
annotation_multiplier: annotation_multiplier.max(1.0e-9),
last_display_height: None,
last_entity: None,
};
let style = command.style_name.clone();
command.select_style(&style);
if !command.fixed_height {
command.height = current_height;
}
command
}
fn select_style(&mut self, name: &str) -> bool {
let Some(style) = self
.styles
.iter()
.find(|style| style.name.eq_ignore_ascii_case(name))
.cloned()
else {
return false;
};
self.style_name = style.name;
self.fixed_height = style.height > 1.0e-9;
if self.fixed_height {
self.height = style.height;
} else if style.last_height > 1.0e-9 {
self.height = style.last_height;
}
self.width_factor = style.width_factor.max(0.01);
self.oblique_angle = style.oblique_angle.clamp(
-85.0_f64.to_radians(),
85.0_f64.to_radians(),
);
self.annotative = style.annotative;
true
}
fn set_justification(&mut self, value: &str) -> bool {
let normalized = value.trim().to_ascii_uppercase().replace([' ', '-'], "");
let alignment = match normalized.as_str() {
"L" | "LEFT" => (HA::Left, VA::Baseline),
"C" | "CENTER" => (HA::Center, VA::Baseline),
"R" | "RIGHT" => (HA::Right, VA::Baseline),
"A" | "ALIGNED" | "ALIGN" => (HA::Aligned, VA::Baseline),
"M" | "MIDDLE" => (HA::Middle, VA::Baseline),
"F" | "FIT" => (HA::Fit, VA::Baseline),
"TL" | "TOPLEFT" => (HA::Left, VA::Top),
"TC" | "TOPCENTER" => (HA::Center, VA::Top),
"TR" | "TOPRIGHT" => (HA::Right, VA::Top),
"ML" | "MIDDLELEFT" => (HA::Left, VA::Middle),
"MC" | "MIDDLECENTER" => (HA::Center, VA::Middle),
"MR" | "MIDDLERIGHT" => (HA::Right, VA::Middle),
"BL" | "BOTTOMLEFT" => (HA::Left, VA::Bottom),
"BC" | "BOTTOMCENTER" => (HA::Center, VA::Bottom),
"BR" | "BOTTOMRIGHT" => (HA::Right, VA::Bottom),
_ => return false,
};
self.horizontal = alignment.0;
self.vertical = alignment.1;
true
}
fn is_two_point(&self) -> bool {
matches!(self.horizontal, HA::Aligned | HA::Fit)
}
fn after_first_point(&mut self) -> CmdResult {
if self.is_two_point() {
self.step = Step::SecondPoint;
} else if self.fixed_height {
self.step = Step::Rotation;
} else {
self.step = Step::Height;
}
CmdResult::NeedPoint
}
fn after_second_point(&mut self) -> CmdResult {
if matches!(self.horizontal, HA::Fit) && !self.fixed_height {
self.step = Step::Height;
CmdResult::NeedPoint
} else {
self.open_editor()
}
}
fn make_entity(&self) -> Option<Text> {
let first = self.plane.to_local(self.first_point?);
let mut text = Text::with_value("", Vector3::new(first.x, first.y, first.z))
.with_height(self.height.max(1.0e-9));
text.style = self.style_name.clone();
text.width_factor = self.width_factor.max(0.01);
text.oblique_angle = self.oblique_angle;
text.rotation = self.rotation;
text.horizontal_alignment = self.horizontal;
text.vertical_alignment = self.vertical;
text.alignment_point = if self.is_two_point() {
let second = self.plane.to_local(self.second_point?);
Some(Vector3::new(second.x, second.y, second.z))
} else if matches!((self.horizontal, self.vertical), (HA::Left, VA::Baseline)) {
None
} else {
Some(Vector3::new(first.x, first.y, first.z))
};
Some(text)
}
fn open_editor(&mut self) -> CmdResult {
let Some(entity) = self.make_entity() else {
return CmdResult::NeedPoint;
};
let pos = self.first_point.unwrap_or(DVec3::ZERO);
self.last_display_height = None;
self.last_entity = Some(entity.clone());
CmdResult::SuspendForTextInput { pos, entity }
}
fn open_next_line(&mut self) -> CmdResult {
let Some(mut entity) = self.last_entity.take() else {
return CmdResult::Cancel;
};
let angle = if matches!(entity.horizontal_alignment, HA::Aligned | HA::Fit) {
entity.alignment_point.map_or(entity.rotation, |point| {
(point.y - entity.insertion_point.y)
.atan2(point.x - entity.insertion_point.x)
})
} else {
entity.rotation
};
let fallback_height = entity.height.max(1.0e-9)
* if self.annotative {
self.annotation_multiplier
} else {
1.0
};
let spacing = self
.last_display_height
.take()
.unwrap_or(fallback_height)
.max(1.0e-9)
* 1.666_666_666_7;
let delta = Vector3::new(angle.sin() * spacing, -angle.cos() * spacing, 0.0);
entity.insertion_point = entity.insertion_point + delta;
if let Some(point) = entity.alignment_point.as_mut() {
*point = *point + delta;
}
entity.value.clear();
let local = DVec3::new(
entity.insertion_point.x,
entity.insertion_point.y,
entity.insertion_point.z,
);
let pos = self.plane.to_world(local);
self.last_entity = Some(entity.clone());
CmdResult::SuspendForTextInput { pos, entity }
}
}
impl CadCommand for TextCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str {
"TEXT"
}
fn prompt(&self) -> String {
match &self.step {
Step::InsertPoint => t!("TEXT Specify insertion point:").into_owned(),
match self.step {
Step::Start => format!(
"{}\n{}",
crate::tf!(
"TEXT Current style: {}, Height: {}, Annotative: {}",
self.style_name,
self.height,
if self.annotative { "Yes" } else { "No" }
),
t!("TEXT Specify start point or [Justify/Style]:")
),
Step::Justification => t!(
"TEXT Enter justification [Left/Center/Right/Aligned/Middle/Fit/TL/TC/TR/ML/MC/MR/BL/BC/BR]:"
)
.into_owned(),
Step::Style => crate::tf!("TEXT Enter style name <{}>:", self.style_name).into_owned(),
Step::Height if self.annotative => {
crate::tf!("TEXT Specify paper text height <{}>:", self.height).into_owned()
}
Step::Height => crate::tf!("TEXT Specify height <{}>:", self.height).into_owned(),
Step::Rotation => crate::tf!(
"TEXT Specify rotation angle <{}>:",
self.rotation.to_degrees()
)
.into_owned(),
Step::SecondPoint => t!("TEXT Specify second endpoint:").into_owned(),
}
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
// Hand off to the in-place plain-text editor anchored at the click.
CmdResult::OpenTextEditor {
pos: pt,
handle: None,
initial: String::new(),
height: self.height,
fn options(&self) -> Vec<CmdOption> {
match self.step {
Step::Start => vec![
CmdOption::new(t!("Justify").as_ref(), "J"),
CmdOption::new(t!("Style").as_ref(), "ST"),
],
Step::Justification => [
("Left", "L"), ("Center", "C"), ("Right", "R"),
("Aligned", "A"), ("Middle", "M"), ("Fit", "F"),
("TL", "TL"), ("TC", "TC"), ("TR", "TR"),
("ML", "ML"), ("MC", "MC"), ("MR", "MR"),
("BL", "BL"), ("BC", "BC"), ("BR", "BR"),
]
.into_iter()
.map(|(label, keyword)| CmdOption::new(label, keyword))
.collect(),
Step::Style => self
.styles
.iter()
.map(|style| CmdOption::new(&style.name, &style.name))
.collect(),
_ => Vec::new(),
}
}
fn wants_text_input(&self) -> bool {
matches!(
self.step,
Step::Justification | Step::Style | Step::Height | Step::Rotation
)
}
fn point_step_accepts_keywords(&self) -> bool {
matches!(self.step, Step::Start)
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let token = text.trim();
let upper = token.to_ascii_uppercase();
match self.step {
Step::Start => match upper.as_str() {
"J" | "JUSTIFY" | "JUSTIFICATION" => self.step = Step::Justification,
"S" | "ST" | "STYLE" => self.step = Step::Style,
_ => return None,
},
Step::Justification => {
if !self.set_justification(token) {
return None;
}
self.step = Step::Start;
}
Step::Style => {
if !self.select_style(token) {
return None;
}
self.step = Step::Start;
}
Step::Height => {
let value = token.replace(',', ".").parse::<f64>().ok()?;
if !value.is_finite() || value <= 1.0e-9 {
return None;
}
self.height = value;
if self.is_two_point() {
return Some(self.open_editor());
}
self.step = Step::Rotation;
}
Step::Rotation => {
let value = token.replace(',', ".").parse::<f64>().ok()?;
if !value.is_finite() {
return None;
}
self.rotation = value.to_radians();
return Some(self.open_editor());
}
Step::SecondPoint => return None,
}
Some(CmdResult::NeedPoint)
}
fn on_point(&mut self, point: DVec3) -> CmdResult {
match self.step {
Step::Start => {
self.first_point = Some(point);
self.second_point = None;
self.after_first_point()
}
Step::Height => {
let Some(first) = self.first_point else {
return CmdResult::NeedPoint;
};
let value = self
.plane
.vector_to_local(point - first)
.truncate()
.length();
if value <= 1.0e-9 {
return CmdResult::NeedPoint;
}
self.height = value;
if self.is_two_point() {
self.open_editor()
} else {
self.step = Step::Rotation;
CmdResult::NeedPoint
}
}
Step::Rotation => {
let Some(first) = self.first_point else {
return CmdResult::NeedPoint;
};
let Some(angle) = self.plane.angle(first, point) else {
return CmdResult::NeedPoint;
};
self.rotation = angle;
self.open_editor()
}
Step::SecondPoint => {
let Some(first) = self.first_point else {
return CmdResult::NeedPoint;
};
if self
.plane
.vector_to_local(point - first)
.truncate()
.length()
<= 1.0e-9
{
return CmdResult::NeedPoint;
}
self.second_point = Some(point);
self.after_second_point()
}
Step::Justification | Step::Style => CmdResult::NeedPoint,
}
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
match self.step {
Step::Start => CmdResult::Cancel,
Step::Justification | Step::Style => {
self.step = Step::Start;
CmdResult::NeedPoint
}
Step::Height => {
if self.is_two_point() {
self.open_editor()
} else {
self.step = Step::Rotation;
CmdResult::NeedPoint
}
}
Step::Rotation => self.open_editor(),
Step::SecondPoint => CmdResult::NeedPoint,
}
}
fn on_editor_closed(&mut self, committed: bool) -> CmdResult {
if committed {
self.open_next_line()
} else {
CmdResult::Cancel
}
}
fn on_editor_display_height(&mut self, height: f64) {
if height.is_finite() && height > 1.0e-9 {
self.last_display_height = Some(height);
}
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_mouse_move(&mut self, _pt: DVec3) -> Option<WireModel> {
fn on_mouse_move(&mut self, _point: DVec3) -> Option<WireModel> {
None
}
}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["TEXT"] }); // TextCommand
inventory::submit!(crate::command::CommandRegistration { names: &["TEXT"] });
+24 -49
View File
@@ -1,8 +1,6 @@
// TOLERANCE command — place a GD&T (geometric dimensioning & tolerancing) frame.
//
// Workflow:
// 1. Text: Enter tolerance string (e.g. "%%v0.05|A" or plain text)
// 2. Point: Click insertion point
// The structured editor prepares the frame; this command places it.
use acadrust::entities::Tolerance;
use acadrust::types::Vector3;
@@ -25,20 +23,17 @@ pub fn tool() -> ToolDef {
}
}
enum Step {
Text,
Insertion { text: String },
}
pub struct ToleranceCommand {
step: Step,
text: String,
preview_strokes: Vec<Vec<[f32; 2]>>,
plane: WorkingPlane,
}
impl ToleranceCommand {
pub fn new() -> Self {
pub fn with_text(text: String, preview_strokes: Vec<Vec<[f32; 2]>>) -> Self {
Self {
step: Step::Text,
text,
preview_strokes,
plane: WorkingPlane::default(),
}
}
@@ -54,36 +49,18 @@ impl CadCommand for ToleranceCommand {
}
fn prompt(&self) -> String {
match &self.step {
Step::Text => t!("TOLERANCE Enter tolerance text:").into_owned(),
Step::Insertion { text } => {
t!("TOLERANCE Specify insertion point [%{text}]:", text = text).into_owned()
}
}
t!("TOLERANCE Specify insertion point:").into_owned()
}
fn wants_text_input(&self) -> bool {
matches!(self.step, Step::Text)
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let t = text.trim().to_string();
if t.is_empty() {
return Some(CmdResult::Cancel);
}
self.step = Step::Insertion { text: t };
Some(CmdResult::NeedPoint)
false
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
if let Step::Insertion { text } = &self.step {
let point = self.plane.to_local(pt);
let ins = Vector3::new(point.x, point.y, point.z);
let tol = Tolerance::with_text(ins, text.clone());
CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Tolerance(tol)))
} else {
CmdResult::NeedPoint
}
let point = self.plane.to_local(pt);
let ins = Vector3::new(point.x, point.y, point.z);
let tol = Tolerance::with_text(ins, self.text.clone());
CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Tolerance(tol)))
}
fn on_enter(&mut self) -> CmdResult {
@@ -91,17 +68,18 @@ impl CadCommand for ToleranceCommand {
}
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
if !matches!(self.step, Step::Insertion { .. }) {
return None;
let mut points = Vec::new();
for stroke in &self.preview_strokes {
if stroke.len() < 2 {
continue;
}
if !points.is_empty() {
points.push(DVec3::splat(f64::NAN));
}
points.extend(stroke.iter().map(|[x, y]| {
pt + self.plane.x * *x as f64 + self.plane.y * *y as f64
}));
}
let d = 0.15;
let points = [
pt - self.plane.x * d,
pt + self.plane.x * d,
DVec3::splat(f64::NAN),
pt - self.plane.y * d,
pt + self.plane.y * d,
];
Some(WireModel {
point_marker: None,
taper_widths: Vec::new(),
@@ -119,10 +97,7 @@ impl CadCommand for ToleranceCommand {
dash_align_end: None,
text_verts: Vec::new(),
name: "tolerance_preview".into(),
points: points
.iter()
.map(|point| point.as_vec3().to_array())
.collect(),
points: points.iter().map(|point| point.as_vec3().to_array()).collect(),
points_low: Vec::new(),
color: WireModel::CYAN,
selected: false,