forked from 1b-agent/opencadstudio-upstream
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:
commit
6ef2d75e84
67 files changed
+4773
-883
No files matched your search
@@ -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
@@ -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
@@ -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"] });
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user