diff --git a/src/app/commands/dim.rs b/src/app/commands/dim.rs index 0138c9fc2..dce8a510c 100644 --- a/src/app/commands/dim.rs +++ b/src/app/commands/dim.rs @@ -55,6 +55,13 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } + "DIMARC" => { + use crate::modules::annotate::arc_length_dim::ArcLengthDimensionCommand; + let new_cmd = ArcLengthDimensionCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + "DIMORDINATE" => { use crate::modules::annotate::ordinate_dim::OrdinateDimCommand; let new_cmd = OrdinateDimCommand::new(); diff --git a/src/app/properties.rs b/src/app/properties.rs index bd7daa3c8..22534279f 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -1725,7 +1725,16 @@ impl OpenCADStudio { _ => None, }; if let Some((anno_field, insert_after)) = anno { - let is_anno = crate::scene::annotative::is_annotative(doc, entity); + let is_anno = crate::scene::annotative::is_annotative(doc, entity) + || match entity { + acadrust::EntityType::Dimension(dimension) => { + crate::scene::annotative::dim_style_is_annotative( + doc, + &dimension.base().style_name, + ) + } + _ => false, + }; // Dimensions/tables carry no Annotative row yet — add one // right after their style row. if let Some(anchor) = insert_after { @@ -1754,6 +1763,13 @@ impl OpenCADStudio { value: t.is_annotative, }, ), + acadrust::EntityType::Dimension( + acadrust::entities::Dimension::Arc(_), + ) => set_row( + &mut sections, + "annotative", + if is_anno { "Yes" } else { "No" }.to_string(), + ), acadrust::EntityType::Text(_) | acadrust::EntityType::Insert(_) | acadrust::EntityType::Hatch(_) diff --git a/src/entities/dim_override.rs b/src/entities/dim_override.rs index 9aad267e4..893c6c42f 100644 --- a/src/entities/dim_override.rs +++ b/src/entities/dim_override.rs @@ -286,6 +286,7 @@ pub fn property_int_code(field: &str) -> Option { "dim_tolerance_precision" => DIMTDEC, "dim_tolerance_pos_vert" => DIMTOLJ, "dim_tolerance_alignment" => DIMTALN, + "dim_arc_symbol" => DIMARCSYM, _ => return None, }) } @@ -337,6 +338,7 @@ fn inherited_int(doc: &CadDocument, handle: Handle, code: i16) -> i16 { DIMADEC => style.dimadec, DIMTFILL => style.dimtfill, DIMTALN => 0, + DIMARCSYM => style.dimarcsym, _ => 0, } } @@ -703,6 +705,12 @@ pub fn set_property( "move text, no leader" => Some(2), _ => trimmed.parse().ok(), }, + "dim_arc_symbol" => match trimmed.to_ascii_lowercase().as_str() { + "preceding dimension text" => Some(0), + "above dimension text" => Some(1), + "none" => Some(2), + _ => trimmed.parse().ok(), + }, "dim_alt_format" => match trimmed.to_ascii_lowercase().as_str() { "scientific" => Some(1), "decimal" => Some(2), diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs index ac62e24b2..18bf5b981 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -175,6 +175,16 @@ fn properties(dim: &Dimension) -> Vec { ], }]; } + if let Dimension::Arc(d) = dim { + return vec![PropSection { + title: t!("Misc").into_owned(), + props: vec![Property { + label: t!("Dimension style").into_owned(), + field: "style_name", + value: PropValue::PlainText(d.base.style_name.clone()), + }], + }]; + } let mut props = base_props(dim.base()); match dim { Dimension::Aligned(d) => { @@ -2598,6 +2608,33 @@ pub fn style_sections( } } + if matches!(dimension, Dimension::Arc(_)) { + if let Some(lines) = sections + .iter_mut() + .find(|section| section.title == t!("Lines & Arrows").as_ref()) + { + let symbol = match int(ov::DIMARCSYM, s.dimarcsym) { + 1 => "Above dimension text", + 2 => "None", + _ => "Preceding dimension text", + }; + lines.props.insert( + 3, + choice( + t!("Arc length symbol").as_ref(), + "dim_arc_symbol", + symbol, + &[ + "Preceding dimension text", + "Above dimension text", + "None", + ], + true, + ), + ); + } + } + if matches!(dimension, Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_)) { if let Some(text_section) = sections .iter_mut() @@ -3405,10 +3442,8 @@ fn tessellate_dimension_inner( // DIMUPT governs interactive creation-time text placement; saved // geometry already carries the resulting position. let _ = s.dimupt; - // DIMARCSYM only applies to arc-length dims; DIMJOGANG only to - // jogged-radius dims. We don't ship those Dimension variants yet, - // so the values are read for round-trip but not drawn. - let _ = (s.dimarcsym, s.dimjogang); + // DIMJOGANG is consumed by the jogged-radius path. + let _ = s.dimjogang; // DIMUNIT is the obsolete pre-R2000 linear unit format; DIMLUNIT // supersedes it. Read but not honoured. let _ = s.dimunit; @@ -3634,6 +3669,46 @@ fn tessellate_dimension_inner( fill_tris_low: Vec::new(), }); + if let Some(symbol) = style.and_then(|style| { + arc_length_symbol_points(dim, Some(style), dim_txt, dim_scale, style.dimarcsym) + }) { + let mut points = Vec::new(); + add_polyline(&mut points, &symbol); + wires.push(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.clone(), + points, + points_low: Vec::new(), + color: if selected { WireModel::SELECTED } else { text_color }, + selected, + aci: 0, + pattern_length: 0.0, + pattern: [0.0; 8], + line_weight_px: 1.0, + snap_pts: vec![], + tangent_geoms: vec![], + key_vertices: vec![], + aabb: WireModel::UNBOUNDED_AABB, + plinegen: true, + fill_tris: vec![], + fill_tris_low: Vec::new(), + }); + } + // DIMTFILL: 0=none, 1=drawing background (mask), 2=DIMTFILLCLR. if let Some(s) = style { if s.dimtfill == 1 || s.dimtfill == 2 { @@ -3935,6 +4010,57 @@ fn text_fill_rect( let p4 = corner(-hx, hy); Some(vec![p1, p2, p3, p1, p3, p4]) } + +fn arc_length_symbol_points( + dim: &Dimension, + style: Option<&DimStyle>, + text_height: f64, + dim_scale: f64, + symbol_position: i16, +) -> Option> { + if !matches!(dim, Dimension::Arc(_)) || symbol_position == 2 { + return None; + } + let value = dimension_text_value(dim, style)?; + if value.is_empty() || text_height <= 1.0e-12 { + return None; + } + + let position = dimension_text_pos_f64(dim, style, text_height, dim_scale); + let rotation = dimension_text_rotation(dim, style); + let (sin_rotation, cos_rotation) = rotation.sin_cos(); + let text_width = value.chars().count() as f64 * text_height * 0.6; + let symbol_width = text_height * 0.62; + let symbol_height = text_height * 0.20; + let (center_x, center_y) = if symbol_position == 1 { + (0.0, text_height * 0.72) + } else { + (-(text_width * 0.5 + symbol_width * 0.70), text_height * 0.02) + }; + let transform = |x: f64, y: f64| { + let local_x = center_x + x; + let local_y = center_y + y; + Vec3::new( + (position.x + local_x * cos_rotation - local_y * sin_rotation) as f32, + (position.y + local_x * sin_rotation + local_y * cos_rotation) as f32, + position.z as f32, + ) + }; + + let steps = 8usize; + Some( + (0..=steps) + .map(|index| { + let t = index as f64 / steps as f64; + let x = (t - 0.5) * symbol_width; + let normalized = x / (symbol_width * 0.5); + let y = symbol_height * (1.0 - normalized * normalized); + transform(x, y) + }) + .collect(), + ) +} + struct SuppressFlags { ext1: bool, ext2: bool, @@ -4170,6 +4296,7 @@ fn dimension_geometry( } } Dimension::Arc(d) => { + let explicit_sweep = arc_dimension_angles(d); append_angular_dimension( &mut g, lv(d.center_point), @@ -4178,10 +4305,7 @@ fn dimension_geometry( lv(d.definition_point), arrow1, arrow2, - d.is_partial.then_some(( - d.arc_start_parameter as f32, - d.arc_end_parameter as f32, - )), + explicit_sweep, params, suppress, ); @@ -4585,6 +4709,25 @@ fn two_line_angle_frame( Some((vertex, start, end)) } +pub(crate) fn arc_dimension_angles(dimension: &DimensionArc) -> Option<(f32, f32)> { + let raw = dimension.arc_end_parameter - dimension.arc_start_parameter; + let mut sweep = raw.rem_euclid(std::f64::consts::TAU); + if sweep <= 1.0e-12 && raw.abs() > 1.0e-12 { + sweep = std::f64::consts::TAU; + } + if sweep > 1.0e-12 { + let start = dimension.arc_start_parameter as f32; + return Some((start, start + sweep as f32)); + } + + let start = (dimension.first_extension_point.y - dimension.center_point.y) + .atan2(dimension.first_extension_point.x - dimension.center_point.x); + let end = (dimension.second_extension_point.y - dimension.center_point.y) + .atan2(dimension.second_extension_point.x - dimension.center_point.x); + let sweep = (end - start).rem_euclid(std::f64::consts::TAU); + (sweep > 1.0e-12).then_some((start as f32, (start + sweep) as f32)) +} + fn angular_dimension_frame(dim: &Dimension) -> Option<(Vec3, f32, f32, f32)> { let (vertex, start, end, arc_point) = match dim { Dimension::Angular2Ln(value) => { @@ -4611,6 +4754,12 @@ fn angular_dimension_frame(dim: &Dimension) -> Option<(Vec3, f32, f32, f32)> { two_line_angle_frame(vertex, first, vertex, second, arc_point)?; (vertex, start, end, arc_point) } + Dimension::Arc(value) => { + let vertex = vec3_local(value.center_point); + let arc_point = vec3_local(value.definition_point); + let (start, end) = arc_dimension_angles(value)?; + (vertex, start, end, arc_point) + } _ => return None, }; let radius = vertex.distance(arc_point); @@ -5201,7 +5350,7 @@ fn dimension_text_natural_rotation(dim: &Dimension) -> f64 { let dy = d.second_point.y - d.first_point.y; dy.atan2(dx) } - Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_) => angular_dimension_frame(dim) + Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_) | Dimension::Arc(_) => angular_dimension_frame(dim) .map(|(_, start, end, _)| { ((start + end) * 0.5 + std::f32::consts::FRAC_PI_2) as f64 }) @@ -6135,6 +6284,45 @@ pub(crate) fn baked_dimension_text_entity( Some(ent) } +pub(crate) fn baked_arc_length_symbol_points( + dim: &Dimension, + document: &CadDocument, + anno_scale: f64, +) -> Vec { + if !matches!(dim, Dimension::Arc(_)) { + return Vec::new(); + } + let style_name = dim.base().style_name.as_str(); + let style = document.dim_styles.iter().find(|style| { + style.name.eq_ignore_ascii_case(style_name) + || (style_name.trim().is_empty() && style.name.eq_ignore_ascii_case("Standard")) + }); + let dim_scale = style + .map(|style| { + if style.dimscale > 1.0e-6 { + style.dimscale + } else { + anno_scale + } + }) + .unwrap_or(1.0); + let text_height = style + .map(|style| style.dimtxt * dim_scale) + .unwrap_or(2.5 * dim_scale); + let symbol_position = crate::entities::dim_override::int( + &dim.base().common.extended_data, + crate::entities::dim_override::DIMARCSYM, + ) + .or_else(|| style.map(|style| style.dimarcsym)) + .unwrap_or(0); + + arc_length_symbol_points(dim, style, text_height, dim_scale, symbol_position) + .unwrap_or_default() + .into_iter() + .map(|point| Vector3::new(point.x as f64, point.y as f64, point.z as f64)) + .collect() +} + pub(crate) fn dimension_text_grip_position( dim: &Dimension, document: &CadDocument, diff --git a/src/modules/annotate/arc_length_dim.rs b/src/modules/annotate/arc_length_dim.rs new file mode 100644 index 000000000..28ab7b8a4 --- /dev/null +++ b/src/modules/annotate/arc_length_dim.rs @@ -0,0 +1,638 @@ +use acadrust::entities::{Dimension, DimensionArc}; +use acadrust::types::{Handle, Vector3}; +use acadrust::EntityType; +use cadkernel::geom2d::tessellate::{arc, DEFAULT_SEGMENTS_PER_RADIAN}; +use glam::DVec3; + +use crate::command::{ + CadCommand, CmdOption, CmdResult, DimensionAssociationInput, + DimensionAssociationSource, WorkingPlane, +}; +use crate::modules::{IconKind, ModuleEvent, ToolDef}; +use crate::scene::dimension_assoc::{ + polyline_arc_point_marker, RadialSourceGeometry, + POLYLINE_ARC_CENTER_MARKER, +}; +use crate::scene::model::wire_model::WireModel; + +pub const ICON: IconKind = + IconKind::Svg(include_bytes!("../../../assets/icons/dim_angular.svg")); + +pub fn tool() -> ToolDef { + ToolDef { + id: "DIMARC", + label: "Arc Length", + icon: ICON, + event: ModuleEvent::Command("DIMARC".to_string()), + } +} + +#[derive(Clone, Copy)] +enum SourceBinding { + Arc, + PolylineSegment(i32), +} + +#[derive(Clone, Copy)] +struct ArcSelection { + source: RadialSourceGeometry, + binding: SourceBinding, + start_angle: f64, + end_angle: f64, + is_partial: bool, +} + +impl ArcSelection { + fn new(source: RadialSourceGeometry, binding: SourceBinding) -> Option { + let sweep = positive_sweep(source.start_angle, source.end_angle); + (source.limited && source.radius > 1.0e-12 && sweep > 1.0e-12).then_some(Self { + source, + binding, + start_angle: source.start_angle, + end_angle: source.start_angle + sweep, + is_partial: false, + }) + } + + fn sweep(self) -> f64 { + positive_sweep(self.start_angle, self.end_angle) + } + + fn point_at(self, angle: f64) -> DVec3 { + dvec(self.source.point_at_angle(angle)) + } + + fn center(self) -> DVec3 { + dvec(self.source.center_world()) + } + + fn plane(self) -> WorkingPlane { + WorkingPlane::new( + DVec3::from_array(self.source.plane.origin), + DVec3::from_array(self.source.plane.x_axis), + DVec3::from_array(self.source.plane.y_axis), + ) + } + + fn clamped_angle(self, point: DVec3) -> f64 { + let raw = self.source.angle_at(point.to_array()); + let relative = (raw - self.start_angle).rem_euclid(std::f64::consts::TAU); + let sweep = self.sweep(); + if relative <= sweep + 1.0e-10 { + return self.start_angle + relative.min(sweep); + } + let start_distance = relative.min(std::f64::consts::TAU - relative); + let end_relative = (raw - self.end_angle).rem_euclid(std::f64::consts::TAU); + let end_distance = end_relative.min(std::f64::consts::TAU - end_relative); + if start_distance <= end_distance { + self.start_angle + } else { + self.end_angle + } + } + + fn with_partial(self, first: f64, second: f64) -> Option { + let first = (first - self.start_angle) + .rem_euclid(std::f64::consts::TAU) + .clamp(0.0, self.sweep()); + let second = (second - self.start_angle) + .rem_euclid(std::f64::consts::TAU) + .clamp(0.0, self.sweep()); + let (start, end) = if first <= second { + (first, second) + } else { + (second, first) + }; + (end - start > 1.0e-12).then_some(Self { + start_angle: self.start_angle + start, + end_angle: self.start_angle + end, + is_partial: true, + ..self + }) + } + + fn association_sources(self, handle: Handle) -> Vec> { + match self.binding { + SourceBinding::Arc => vec![ + Some(DimensionAssociationSource::explicit(handle, -3, 0.0)), + Some(DimensionAssociationSource::explicit( + handle, + -2, + self.start_angle, + )), + Some(DimensionAssociationSource::explicit( + handle, + -2, + self.end_angle, + )), + ], + SourceBinding::PolylineSegment(segment) => { + let point_marker = polyline_arc_point_marker(segment); + vec![ + Some(DimensionAssociationSource::explicit( + handle, + POLYLINE_ARC_CENTER_MARKER, + segment as f64, + )), + Some(DimensionAssociationSource::explicit( + handle, + point_marker, + self.start_angle, + )), + Some(DimensionAssociationSource::explicit( + handle, + point_marker, + self.end_angle, + )), + ] + } + } + } +} + +#[derive(Clone, Copy)] +enum Step { + SelectObject, + DimLine(ArcSelection), + PartialFirst(ArcSelection), + PartialSecond { + selection: ArcSelection, + first_angle: f64, + }, +} + +pub struct ArcLengthDimensionCommand { + step: Step, + picked_entity: Option, + source_handle: Option, + text_override: Option, + awaiting_text: bool, + text_angle: Option, + awaiting_angle: bool, + leader_enabled: bool, +} + +impl ArcLengthDimensionCommand { + pub fn new() -> Self { + Self { + step: Step::SelectObject, + picked_entity: None, + source_handle: None, + text_override: None, + awaiting_text: false, + text_angle: None, + awaiting_angle: false, + leader_enabled: false, + } + } + + fn editor_anchor(&self) -> DVec3 { + match self.step { + Step::SelectObject => DVec3::ZERO, + Step::DimLine(selection) + | Step::PartialFirst(selection) + | Step::PartialSecond { selection, .. } => { + selection.point_at(selection.start_angle + selection.sweep() * 0.5) + } + } + } + + fn commit_dimension(&self, selection: ArcSelection, point: DVec3) -> CmdResult { + let plane = selection.plane(); + 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 picked = plane.to_local(point); + let picked_radius = (picked - center).truncate().length(); + if !picked_radius.is_finite() || picked_radius <= 1.0e-12 { + return CmdResult::NeedPoint; + } + + let mut dimension = DimensionArc::default(); + dimension.center_point = v3(center); + dimension.first_extension_point = v3(first); + dimension.second_extension_point = v3(second); + dimension.definition_point = v3(picked); + dimension.is_partial = selection.is_partial; + dimension.arc_start_parameter = selection.start_angle; + dimension.arc_end_parameter = selection.end_angle; + dimension.base.definition_point = dimension.definition_point; + dimension.base.text_middle_point = dimension.definition_point; + dimension.base.insertion_point = dimension.definition_point; + dimension.has_leader = self.leader_enabled && selection.sweep() > std::f64::consts::FRAC_PI_2; + if dimension.has_leader { + let middle = selection.start_angle + selection.sweep() * 0.5; + let anchor = DVec3::new( + center.x + picked_radius * middle.cos(), + center.y + picked_radius * middle.sin(), + picked.z, + ); + dimension.first_leader_point = v3(anchor); + dimension.second_leader_point = v3(picked); + dimension.definition_point = v3(anchor); + dimension.base.definition_point = dimension.definition_point; + dimension.base.text_user_positioned = true; + } + dimension.base.actual_measurement = dimension.measurement(); + crate::entities::dimension::set_dimension_text_override( + &mut dimension.base, + self.text_override.clone(), + ); + if let Some(angle) = self.text_angle { + dimension.base.text_rotation = angle; + } + + let association = self.source_handle.map_or_else( + || DimensionAssociationInput::Explicit(Vec::new()), + |handle| { + DimensionAssociationInput::Explicit( + selection.association_sources(handle), + ) + }, + ); + CmdResult::CommitDimension { + entity: plane.place_entity(EntityType::Dimension(Dimension::Arc(dimension))), + association, + } + } +} + +impl CadCommand for ArcLengthDimensionCommand { + fn set_working_plane(&mut self, _plane: WorkingPlane) {} + + fn name(&self) -> &'static str { + "DIMARC" + } + + fn prompt(&self) -> String { + if self.awaiting_text { + return "DIMARC Enter dimension text (blank = measured value):".to_string(); + } + if self.awaiting_angle { + return "DIMARC Specify text angle (degrees):".to_string(); + } + match self.step { + Step::SelectObject => { + "DIMARC Select arc or polyline arc segment:".to_string() + } + Step::DimLine(selection) => { + let leader_option = if selection.sweep() > std::f64::consts::FRAC_PI_2 { + if self.leader_enabled { "/No Leader" } else { "/Leader" } + } else { + "" + }; + format!( + "DIMARC Specify arc length dimension location [Mtext/Text/Angle/Partial{leader_option}]:" + ) + } + Step::PartialFirst(_) => { + "DIMARC Specify first point of partial arc:".to_string() + } + Step::PartialSecond { .. } => { + "DIMARC Specify second point of partial arc:".to_string() + } + } + } + + fn on_point(&mut self, point: DVec3) -> CmdResult { + match self.step { + Step::SelectObject => CmdResult::NeedPoint, + Step::DimLine(selection) => self.commit_dimension(selection, point), + Step::PartialFirst(selection) => { + self.step = Step::PartialSecond { + selection, + first_angle: selection.clamped_angle(point), + }; + CmdResult::NeedPoint + } + Step::PartialSecond { + selection, + first_angle, + } => { + let second_angle = selection.clamped_angle(point); + let Some(partial) = selection.with_partial(first_angle, second_angle) else { + return CmdResult::NeedPoint; + }; + self.leader_enabled = false; + self.step = Step::DimLine(partial); + CmdResult::NeedPoint + } + } + } + + fn on_enter(&mut self) -> CmdResult { + if self.awaiting_text { + self.awaiting_text = false; + return CmdResult::NeedPoint; + } + if self.awaiting_angle { + self.awaiting_angle = false; + return CmdResult::NeedPoint; + } + CmdResult::Cancel + } + + fn on_escape(&mut self) -> CmdResult { + CmdResult::Cancel + } + + fn wants_text_input(&self) -> bool { + true + } + + fn point_step_accepts_keywords(&self) -> bool { + !self.awaiting_text && !self.awaiting_angle + } + + fn wants_text_with_spaces(&self) -> bool { + self.awaiting_text + } + + fn options(&self) -> Vec { + let Step::DimLine(selection) = self.step else { + return Vec::new(); + }; + if self.awaiting_text || self.awaiting_angle { + return Vec::new(); + } + let mut options = vec![ + CmdOption::new("MText", "MTEXT"), + CmdOption::new("Text", "TEXT"), + CmdOption::new("Angle", "ANGLE"), + CmdOption::new("Partial", "PARTIAL"), + ]; + if selection.sweep() > std::f64::consts::FRAC_PI_2 { + options.push(if self.leader_enabled { + CmdOption::new("No Leader", "NOLEADER") + } else { + CmdOption::new("Leader", "LEADER") + }); + } + options + } + + fn on_text_input(&mut self, text: &str) -> Option { + if self.awaiting_text { + let value = text.trim(); + self.text_override = if value.is_empty() || value == "<>" { + None + } else { + Some(value.to_string()) + }; + self.awaiting_text = false; + return Some(CmdResult::NeedPoint); + } + if self.awaiting_angle { + let value = text.trim(); + self.text_angle = if value.is_empty() { + None + } else { + crate::entities::common::parse_typed_angle(value) + }; + self.awaiting_angle = false; + return Some(CmdResult::NeedPoint); + } + let Step::DimLine(selection) = self.step else { + return None; + }; + match text.trim().to_ascii_uppercase().as_str() { + "M" | "MTEXT" => Some(CmdResult::SuspendForMTextInput { + pos: self.editor_anchor(), + initial: self.text_override.clone().unwrap_or_default(), + height: 2.5, + }), + "T" | "TEXT" => { + self.awaiting_text = true; + Some(CmdResult::NeedPoint) + } + "A" | "ANGLE" => { + self.awaiting_angle = true; + Some(CmdResult::NeedPoint) + } + "P" | "PARTIAL" => { + self.step = Step::PartialFirst(selection); + Some(CmdResult::NeedPoint) + } + "L" | "LEADER" if selection.sweep() > std::f64::consts::FRAC_PI_2 => { + self.leader_enabled = true; + Some(CmdResult::NeedPoint) + } + "N" | "NOLEADER" => { + self.leader_enabled = false; + Some(CmdResult::NeedPoint) + } + _ => None, + } + } + + fn on_editor_text(&mut self, value: String) { + let value = value.trim(); + self.text_override = if value.is_empty() || value == "<>" { + None + } else { + Some(value.to_string()) + }; + } + + fn on_editor_closed(&mut self, _committed: bool) -> CmdResult { + CmdResult::NeedPoint + } + + fn needs_entity_pick(&self) -> bool { + matches!(self.step, Step::SelectObject) + } + + fn entity_pick_highlights_hover(&self) -> bool { + true + } + + fn inject_before_entity_pick(&self) -> bool { + true + } + + fn inject_picked_entity(&mut self, entity: EntityType) { + self.picked_entity = Some(entity); + } + + fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult { + let Some(entity) = self.picked_entity.take() else { + return CmdResult::NeedPoint; + }; + let Some(source) = crate::scene::dimension_assoc::radial_source_at( + &entity, + Vector3::new(point.x, point.y, point.z), + ) else { + return CmdResult::NeedPoint; + }; + if !source.limited { + return CmdResult::NeedPoint; + } + let binding = match entity { + EntityType::Arc(_) => SourceBinding::Arc, + EntityType::LwPolyline(_) | EntityType::Polyline2D(_) => { + SourceBinding::PolylineSegment(source.marker) + } + _ => return CmdResult::NeedPoint, + }; + let Some(selection) = ArcSelection::new(source, binding) else { + return CmdResult::NeedPoint; + }; + self.source_handle = Some(handle); + self.step = Step::DimLine(selection); + CmdResult::NeedPoint + } + + fn on_mouse_move(&mut self, point: DVec3) -> Option { + match self.step { + Step::SelectObject | Step::PartialFirst(_) => None, + Step::DimLine(selection) => Some(dimension_preview( + selection, + point, + self.leader_enabled, + )), + Step::PartialSecond { + selection, + first_angle, + } => { + let second_angle = selection.clamped_angle(point); + let partial = selection.with_partial(first_angle, second_angle)?; + Some(partial_preview(partial)) + } + } + } +} + +fn positive_sweep(start: f64, end: f64) -> f64 { + let raw = end - start; + let mut sweep = raw.rem_euclid(std::f64::consts::TAU); + if sweep <= 1.0e-12 && raw.abs() > 1.0e-12 { + sweep = std::f64::consts::TAU; + } + sweep +} + +fn dimension_preview( + selection: ArcSelection, + point: DVec3, + leader: bool, +) -> WireModel { + let plane = selection.plane(); + 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 point = plane.to_local(point); + let radius = (point - center).truncate().length(); + if radius <= 1.0e-12 { + return preview_wire(Vec::new(), "dimarc_preview"); + } + let start_land = DVec3::new( + center.x + radius * selection.start_angle.cos(), + center.y + radius * selection.start_angle.sin(), + point.z, + ); + let end_land = DVec3::new( + center.x + radius * selection.end_angle.cos(), + center.y + radius * selection.end_angle.sin(), + point.z, + ); + let mut points = vec![first, start_land, nan(), second, end_land, nan()]; + points.extend( + arc( + [center.x, center.y], + radius, + selection.start_angle, + selection.end_angle, + point.z, + DEFAULT_SEGMENTS_PER_RADIAN, + ) + .into_iter() + .map(DVec3::from_array), + ); + if leader && selection.sweep() > std::f64::consts::FRAC_PI_2 { + let middle = selection.start_angle + selection.sweep() * 0.5; + points.extend([ + nan(), + DVec3::new( + center.x + radius * middle.cos(), + center.y + radius * middle.sin(), + point.z, + ), + point, + ]); + } + preview_wire( + points + .into_iter() + .map(|value| if value.is_nan() { value } else { plane.to_world(value) }) + .collect(), + "dimarc_preview", + ) +} + +fn partial_preview(selection: ArcSelection) -> WireModel { + let points = arc( + selection.source.center, + selection.source.radius, + selection.start_angle, + selection.end_angle, + 0.0, + DEFAULT_SEGMENTS_PER_RADIAN, + ) + .into_iter() + .map(|point| DVec3::from_array(selection.source.plane.point_at([point[0], point[1]]))) + .collect(); + preview_wire(points, "dimarc_partial_preview") +} + +fn v3(point: DVec3) -> Vector3 { + Vector3::new(point.x, point.y, point.z) +} + +fn dvec(point: Vector3) -> DVec3 { + DVec3::new(point.x, point.y, point.z) +} + +fn nan() -> DVec3 { + DVec3::splat(f64::NAN) +} + +fn preview_wire(points: Vec, 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(), + } +} + +inventory::submit!(crate::command::CommandRegistration { names: &["DIMARC"] }); diff --git a/src/modules/annotate/mod.rs b/src/modules/annotate/mod.rs index cb6d65f12..bb0f2c1f3 100644 --- a/src/modules/annotate/mod.rs +++ b/src/modules/annotate/mod.rs @@ -1,6 +1,7 @@ // Annotate module — dimension, text, leader, table, and markup tools. pub mod aligned_dim; +pub mod arc_length_dim; pub mod angular_dim; pub mod data_extract; pub mod data_link; @@ -98,6 +99,11 @@ impl CadModule for AnnotateModule { angular_dim::tool().label, angular_dim::tool().icon, ), + ( + arc_length_dim::tool().id, + arc_length_dim::tool().label, + arc_length_dim::tool().icon, + ), ( radius_dim::tool().id, radius_dim::tool().label, diff --git a/src/modules/draw/modify/explode.rs b/src/modules/draw/modify/explode.rs index 9dff1b97e..2e5562d82 100644 --- a/src/modules/draw/modify/explode.rs +++ b/src/modules/draw/modify/explode.rs @@ -1190,6 +1190,8 @@ fn explode_dimension(dim: &Dimension, doc: &CadDocument) -> Vec { } } Dimension::Arc(d) => { + let explicit_sweep = crate::entities::dimension::arc_dimension_angles(d) + .map(|(start, end)| (start as f64, end as f64)); result.extend(angular_block_segs( d.center_point, d.first_extension_point, @@ -1198,10 +1200,7 @@ fn explode_dimension(dim: &Dimension, doc: &CadDocument) -> Vec { &met, &ext_c, &dim_c, - d.is_partial.then_some(( - d.arc_start_parameter, - d.arc_end_parameter, - )), + explicit_sweep, )); if d.has_leader { result.push(make_seg( @@ -1235,6 +1234,15 @@ fn explode_dimension(dim: &Dimension, doc: &CadDocument) -> Vec { } } + let symbol_points = + crate::entities::dimension::baked_arc_length_symbol_points(dim, doc, 1.0); + if symbol_points.len() > 1 { + let text_c = dim_common(&base.common, met.dimclrt, -2); + for pair in symbol_points.windows(2) { + result.push(make_seg(&pair[0], &pair[1], &text_c)); + } + } + // Measurement text is the live render's own Text/MText entity (value, // position, height, rotation, alignment, text style, MText handling all // shared), so the baked block matches the on-screen dimension and the label diff --git a/src/scene/dimension_assoc.rs b/src/scene/dimension_assoc.rs index 43901eea9..a9fee8b1c 100644 --- a/src/scene/dimension_assoc.rs +++ b/src/scene/dimension_assoc.rs @@ -16,6 +16,18 @@ use crate::command::DimensionAssociationSource; use super::{ChangeKind, Scene}; +pub(crate) const POLYLINE_ARC_CENTER_MARKER: i32 = -4; +const POLYLINE_ARC_POINT_MARKER_BASE: i32 = -5; + +pub(crate) fn polyline_arc_point_marker(segment: i32) -> i32 { + POLYLINE_ARC_POINT_MARKER_BASE - segment.max(0) +} + +fn polyline_arc_segment_from_point_marker(marker: i32) -> Option { + (marker <= POLYLINE_ARC_POINT_MARKER_BASE) + .then_some(POLYLINE_ARC_POINT_MARKER_BASE - marker) +} + #[derive(Clone, Copy, Debug)] pub(crate) struct RadialSourceGeometry { pub plane: Plane, @@ -329,7 +341,13 @@ fn source_marker(entity: &EntityType, point: Vector3) -> Option { fn resolve_reference(scene: &Scene, reference: &AssocDimensionReference) -> Option { let source = *reference.xrefs.first()?; let entity = scene.document.get_entity(source)?; - if reference.main_gs_marker == -4 { + if let Some(segment) = + polyline_arc_segment_from_point_marker(reference.main_gs_marker) + { + let radial = radial_source_for_marker(entity, segment)?; + return Some(radial.point_at_angle(reference.osnap_distance)); + } + if reference.main_gs_marker == POLYLINE_ARC_CENTER_MARKER { let segment = reference.osnap_distance.round().max(0.0) as usize; return polyline_arc_center(entity, segment); } @@ -422,10 +440,55 @@ fn dimension_reference_points(dimension: &Dimension) -> Vec { angular.definition_point, ], Dimension::Ordinate(ordinate) => vec![ordinate.feature_location], + Dimension::Arc(arc) => vec![ + arc.center_point, + arc.first_extension_point, + arc.second_extension_point, + ], _ => Vec::new(), } } +fn positive_sweep(start: f64, end: f64) -> f64 { + let raw = end - start; + let mut sweep = raw.rem_euclid(TAU); + if sweep <= 1.0e-12 && raw.abs() > 1.0e-12 { + sweep = TAU; + } + sweep +} + +fn signed_angle_delta(value: f64) -> f64 { + (value + std::f64::consts::PI).rem_euclid(TAU) - std::f64::consts::PI +} + +fn angle_about_plane( + plane: Plane, + center: Vector3, + point: Vector3, +) -> f64 { + let delta = [ + point.x - center.x, + point.y - center.y, + point.z - center.z, + ]; + let dot = |axis: [f64; 3]| { + delta[0] * axis[0] + delta[1] * axis[1] + delta[2] * axis[2] + }; + dot(plane.y_axis).atan2(dot(plane.x_axis)) +} + +fn point_on_radial_circle( + radial: RadialSourceGeometry, + radius: f64, + angle: f64, +) -> Vector3 { + vector3(radial.plane.point_at([ + radial.center[0] + radius * angle.cos(), + radial.center[1] + radius * angle.sin(), + ])) +} + pub(crate) fn dimension_is_associative( document: &acadrust::CadDocument, dimension: Handle, @@ -796,6 +859,18 @@ impl Scene { let radial = radial_source_for_marker(entity, reference.main_gs_marker)?; Some((radial, reference.osnap_distance)) }); + let arc_source = association.references[0].first().and_then(|reference| { + let source = *reference.xrefs.first()?; + let entity = self.document.get_entity(source)?; + let segment = match reference.main_gs_marker { + -3 => 0, + POLYLINE_ARC_CENTER_MARKER => { + reference.osnap_distance.round().max(0.0) as i32 + } + _ => return None, + }; + radial_source_for_marker(entity, segment) + }); if radial_source.is_none() && resolved.iter().all(Option::is_none) { continue; } @@ -898,6 +973,71 @@ impl Scene { } ordinate.refresh_measurement(); } + Dimension::Arc(arc) => { + let Some(radial) = arc_source else { + continue; + }; + let center = resolved[0].unwrap_or_else(|| radial.center_world()); + let Some(first) = resolved[1] else { + continue; + }; + let Some(second) = resolved[2] else { + continue; + }; + + let old_center = arc.center_point; + let old_definition = arc.definition_point; + let old_source_radius = old_center.distance(&arc.first_extension_point); + let old_dim_radius = old_center.distance(&old_definition); + let radial_offset = old_dim_radius - old_source_radius; + let old_mid = arc.arc_start_parameter + + positive_sweep( + arc.arc_start_parameter, + arc.arc_end_parameter, + ) * 0.5; + let old_definition_angle = + angle_about_plane(radial.plane, old_center, old_definition); + let definition_angle_offset = + signed_angle_delta(old_definition_angle - old_mid); + + let start = radial.angle_at(dpoint(first)); + let end_at = radial.angle_at(dpoint(second)); + let sweep = positive_sweep(start, end_at); + let end = start + sweep; + let new_radius = center.distance(&first); + if !new_radius.is_finite() || new_radius <= 1.0e-12 { + continue; + } + let dim_radius = (new_radius + radial_offset).max(1.0e-9); + let middle = start + sweep * 0.5; + let definition = point_on_radial_circle( + radial, + dim_radius, + middle + definition_angle_offset, + ); + let delta = definition - old_definition; + + arc.center_point = center; + arc.first_extension_point = first; + arc.second_extension_point = second; + arc.arc_start_parameter = start; + arc.arc_end_parameter = end; + arc.definition_point = definition; + arc.base.definition_point = definition; + if arc.base.text_user_positioned { + arc.base.text_middle_point = arc.base.text_middle_point + delta; + arc.base.insertion_point = arc.base.insertion_point + delta; + } else { + arc.base.text_middle_point = definition; + arc.base.insertion_point = definition; + } + if arc.has_leader { + arc.first_leader_point = + point_on_radial_circle(radial, dim_radius, middle); + arc.second_leader_point = arc.second_leader_point + delta; + } + arc.base.actual_measurement = arc.measurement(); + } _ => continue, } dimension.base_mut().actual_measurement = dimension.measurement();