diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 27566f98..03332480 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -1864,6 +1864,12 @@ annotate = .dimjogged-created-a-jogged-radius-dimension = DIMJOGGED: created a jogged radius dimension. .dimjogged-invalid-radius = DIMJOGGED: invalid radius. .dimjogged-select-an-arc-or-circle-first = DIMJOGGED: select an arc or circle first. + .dimjogged-enter-dimension-text-blank-measured = DIMJOGGED Enter dimension text (blank = measured value): + .dimjogged-select-arc-circle-or-polyline-arc = DIMJOGGED Select arc, circle, or polyline arc: + .dimjogged-specify-center-location-override = DIMJOGGED Specify center location override: + .dimjogged-specify-dimension-line-location-text = DIMJOGGED Specify dimension line location [Mtext/Text/Angle]: + .dimjogged-specify-jog-location = DIMJOGGED Specify jog location: + .dimjogged-specify-text-angle-degrees = DIMJOGGED Specify text angle (degrees): .dimjogline-select-linear-or-aligned-dimension = DIMJOGLINE Select linear or aligned dimension: .dimjogline-specify-jog-location = DIMJOGLINE Specify jog location: .dimlinear-enter-dimension-text-blank-measured = DIMLINEAR Enter dimension text (blank = measured value): @@ -2667,6 +2673,7 @@ properties = .island-detection-style = Island detection style .italic = Italic .jog-angle = Jog Angle + .jogged-radius = Jogged Radius .jog-x = Jog X .jog-y = Jog Y .jog-z = Jog Z diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 62a06be4..32ac287e 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -1844,6 +1844,12 @@ annotate = .dimjogged-created-a-jogged-radius-dimension = DIMJOGGED: kırıklı yarıçap ölçüsü oluşturuldu. .dimjogged-invalid-radius = DIMJOGGED: yarıçap geçersiz. .dimjogged-select-an-arc-or-circle-first = DIMJOGGED: önce bir yay veya daire seçin. + .dimjogged-enter-dimension-text-blank-measured = DIMJOGGED Ölçü metnini girin (boş = ölçülen değer): + .dimjogged-select-arc-circle-or-polyline-arc = DIMJOGGED Yay, daire veya çoklu çizgi yayı seçin: + .dimjogged-specify-center-location-override = DIMJOGGED Geçersiz kılınmış merkez konumunu belirtin: + .dimjogged-specify-dimension-line-location-text = DIMJOGGED Ölçü çizgisinin konumunu belirtin [Çok satırlı metin/Metin/Açı]: + .dimjogged-specify-jog-location = DIMJOGGED Kırılma konumunu belirtin: + .dimjogged-specify-text-angle-degrees = DIMJOGGED Metin açısını belirtin (derece): .dimjogline-select-linear-or-aligned-dimension = DIMJOGLINE Doğrusal veya hizalı ölçüyü seçin: .dimjogline-specify-jog-location = DIMJOGLINE Kırılma konumunu belirtin: .dimlinear-enter-dimension-text-blank-measured = DIMLINEAR Ölçü metnini girin (boş = ölçülen değer): @@ -2606,6 +2612,7 @@ properties = .island-detection-style = Ada algılama stili .italic = İtalik .jog-angle = Kırılma Açısı + .jogged-radius = Kırıklı Yarıçap .jog-x = Kırılma X .jog-y = Kırılma Y .jog-z = Kırılma Z diff --git a/src/app/commands/dim.rs b/src/app/commands/dim.rs index bab14ea1..22ee0d96 100644 --- a/src/app/commands/dim.rs +++ b/src/app/commands/dim.rs @@ -48,6 +48,17 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } + "DIMJOGGED" | "DIMJOG" => { + use crate::modules::annotate::jogged_radius_dim::JoggedRadiusDimensionCommand; + let defaults = crate::scene::creation_style::current_dimension_defaults( + &self.tabs[i].scene.document, + ); + let multiplier = self.tabs[i].scene.creation_annotation_multiplier(); + let new_cmd = JoggedRadiusDimensionCommand::new(defaults, multiplier); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + "DIMANGULAR" => { use crate::modules::annotate::angular_dim::AngularDimensionCommand; let new_cmd = AngularDimensionCommand::new(); @@ -1293,49 +1304,6 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } - // DIMJOGGED — create a jogged radius dimension on the selected - // arc/circle: a standard radius dim carrying an OCS_JOGGED XData marker - // that the geometry generator renders with a foreshortened zig-zag. - "DIMJOGGED" | "DIMJOG" => { - use acadrust::entities::{Dimension, DimensionRadius}; - use acadrust::types::Vector3; - use acadrust::xdata::{ExtendedDataRecord, XDataValue}; - let found = - self.tabs[i] - .scene - .selected_entities() - .iter() - .find_map(|(_, e)| match e { - acadrust::EntityType::Arc(a) => Some((a.center, a.radius)), - acadrust::EntityType::Circle(c) => Some((c.center, c.radius)), - _ => None, - }); - let Some((center, radius)) = found else { - self.command_line - .push_error(crate::t!("DIMJOGGED: select an arc or circle first.").as_ref()); - return None; - }; - if radius <= 0.0 { - self.command_line.push_error(crate::t!("DIMJOGGED: invalid radius.").as_ref()); - return None; - } - let k = std::f64::consts::FRAC_1_SQRT_2; - let arc_point = - Vector3::new(center.x + radius * k, center.y + radius * k, center.z); - let mut dim = DimensionRadius::new(center, arc_point); - dim.base.common.layer = self.tabs[i].active_layer.clone(); - let mut rec = ExtendedDataRecord::new("OCS_JOGGED"); - rec.add_value(XDataValue::String("1".to_string())); - dim.base.common.extended_data.add_record(rec); - self.push_undo_snapshot(i, "DIMJOGGED"); - self.tabs[i] - .scene - .add_entity(acadrust::EntityType::Dimension(Dimension::Radius(dim))); - self.tabs[i].dirty = true; - self.command_line - .push_output(crate::t!("DIMJOGGED: created a jogged radius dimension.").as_ref()); - } - // ARCTEXT — lay the text out as one Text entity per character // along the selected arc, each rotated to follow the tangent. "ARCTEXT" => { diff --git a/src/app/properties.rs b/src/app/properties.rs index 73c80170..006877cc 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -1031,22 +1031,19 @@ impl OpenCADStudio { if !dim_style_names.is_empty() { // Current style is already shown as text in the geom section; // replace/upgrade it to a Choice if we have a list. - if let Some(geom) = sections.last_mut() { - // Replace the style name with a choice. - if let Some(prop) = - geom.props.iter_mut().find(|p| p.field == "style_name") - { - let current = match &prop.value { - crate::scene::model::object::PropValue::PlainText(s) => { - s.clone() - } - _ => String::new(), - }; - prop.value = crate::scene::model::object::PropValue::Choice { - selected: current, - options: dim_style_names, - }; - } + if let Some(prop) = sections + .iter_mut() + .flat_map(|section| section.props.iter_mut()) + .find(|property| property.field == "style_name") + { + let current = match &prop.value { + crate::scene::model::object::PropValue::PlainText(s) => s.clone(), + _ => String::new(), + }; + prop.value = crate::scene::model::object::PropValue::Choice { + selected: current, + options: dim_style_names, + }; } } @@ -1066,6 +1063,16 @@ impl OpenCADStudio { &self.tabs[i].scene.document, )); + if matches!(d, acadrust::entities::Dimension::LargeRadial(_)) { + if let Some(index) = sections + .iter() + .position(|section| section.title == t!("Misc").as_ref()) + { + let misc = sections.remove(index); + sections.push(misc); + } + } + // Prefer entity-level dimension-variable overrides; // fall back to the assigned style values. use crate::entities::dim_override as dov; diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs index 2af67cbe..061dcaa7 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -108,6 +108,52 @@ fn properties(dim: &Dimension) -> Vec { ], }]; } + if let Dimension::LargeRadial(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()), + }, + edit(t!("Center X").as_ref(), "definition_x", d.definition_point.x), + edit(t!("Center Y").as_ref(), "definition_y", d.definition_point.y), + edit(t!("Center Z").as_ref(), "definition_z", d.definition_point.z), + edit(t!("Chord X").as_ref(), "chord_x", d.chord_point.x), + edit(t!("Chord Y").as_ref(), "chord_y", d.chord_point.y), + edit(t!("Chord Z").as_ref(), "chord_z", d.chord_point.z), + edit( + t!("Override Center X").as_ref(), + "override_x", + d.override_center.x, + ), + edit( + t!("Override Center Y").as_ref(), + "override_y", + d.override_center.y, + ), + edit( + t!("Override Center Z").as_ref(), + "override_z", + d.override_center.z, + ), + edit(t!("Jog X").as_ref(), "jog_x", d.jog_point.x), + edit(t!("Jog Y").as_ref(), "jog_y", d.jog_point.y), + edit(t!("Jog Z").as_ref(), "jog_z", d.jog_point.z), + edit_angle( + t!("Jog Angle").as_ref(), + "jog_angle", + d.jog_angle.to_degrees(), + ), + ro( + t!("Measurement").as_ref(), + "measurement", + format!("{:.4}", d.measurement()), + ), + ], + }]; + } let compact_linear = match dim { Dimension::Linear(d) => Some(( &d.base, @@ -281,21 +327,7 @@ fn properties(dim: &Dimension) -> Vec { props.push(edit(t!("Leader 2 Y").as_ref(), "leader2_y", d.second_leader_point.y)); props.push(edit(t!("Leader 2 Z").as_ref(), "leader2_z", d.second_leader_point.z)); } - Dimension::LargeRadial(d) => { - props.push(edit(t!("Center X").as_ref(), "definition_x", d.definition_point.x)); - props.push(edit(t!("Center Y").as_ref(), "definition_y", d.definition_point.y)); - props.push(edit(t!("Center Z").as_ref(), "definition_z", d.definition_point.z)); - props.push(edit(t!("Chord X").as_ref(), "chord_x", d.chord_point.x)); - props.push(edit(t!("Chord Y").as_ref(), "chord_y", d.chord_point.y)); - props.push(edit(t!("Chord Z").as_ref(), "chord_z", d.chord_point.z)); - props.push(edit(t!("Override Center X").as_ref(), "override_x", d.override_center.x)); - props.push(edit(t!("Override Center Y").as_ref(), "override_y", d.override_center.y)); - props.push(edit(t!("Override Center Z").as_ref(), "override_z", d.override_center.z)); - props.push(edit(t!("Jog X").as_ref(), "jog_x", d.jog_point.x)); - props.push(edit(t!("Jog Y").as_ref(), "jog_y", d.jog_point.y)); - props.push(edit(t!("Jog Z").as_ref(), "jog_z", d.jog_point.z)); - props.push(edit_angle(t!("Jog Angle").as_ref(), "jog_angle", d.jog_angle.to_degrees())); - } + Dimension::LargeRadial(_) => unreachable!("handled by compact large-radial properties"), } vec![PropSection { title: t!("Geometry").into_owned(), @@ -1828,7 +1860,7 @@ pub fn style_sections( let linetype = |code, inherited| { linetype_name(document, ov::handle(data, code).unwrap_or(inherited)) }; - let arrow_1 = if matches!(dimension, Dimension::Radius(_)) { + let arrow_1 = if matches!(dimension, Dimension::Radius(_) | Dimension::LargeRadial(_)) { let inherited = if s.dimblk1.is_null() { s.dimblk } else { s.dimblk1 }; let inherited_name = if s.dimblk1.is_null() { &s.dimblk_name @@ -2530,7 +2562,10 @@ pub fn style_sections( ], }, ]; - if matches!(dimension, Dimension::Radius(_) | Dimension::Diameter(_)) { + if matches!( + dimension, + Dimension::Radius(_) | Dimension::Diameter(_) | Dimension::LargeRadial(_) + ) { let dimcen = real(ov::DIMCEN, s.dimcen); let center_type = if dimcen > 1e-12 { "Center marks" @@ -2573,8 +2608,18 @@ pub fn style_sections( "dim_ext_line_ext", "dim_ext_line_offset", ]; + const LARGE_RADIAL_LINE_FIELDS: &[&str] = &[ + "dim_arrowhead_1", + "dim_arrow_size", + "dim_line_lineweight", + "dim_line_1", + "dim_line_color", + "dim_linetype", + ]; let retained = if matches!(dimension, Dimension::Diameter(_)) { DIAMETER_LINE_FIELDS + } else if matches!(dimension, Dimension::LargeRadial(_)) { + LARGE_RADIAL_LINE_FIELDS } else { RADIUS_LINE_FIELDS }; @@ -3097,6 +3142,28 @@ fn tessellate_dimension_inner( return wires; } } + if let Dimension::LargeRadial(radial) = dim { + if let Some((local_radial, normal)) = large_radial_dimension_in_ocs(radial) { + let mut wires = tessellate_dimension_inner( + document, + handle, + &Dimension::LargeRadial(local_radial), + selected, + entity_color, + line_weight_px, + anno_scale, + selected_set, + active_viewport, + bg_color, + None, + world_per_pixel, + ); + for wire in &mut wires { + map_wire_ocs_to_wcs(wire, normal); + } + return wires; + } + } let name = handle.value().to_string(); // (Baked-block fast path moved up into scene::tessellate_entity so the // recursive call goes through the LOD ladder, not the kernel path.) @@ -3464,8 +3531,7 @@ fn tessellate_dimension_inner( // DIMUPT governs interactive creation-time text placement; saved // geometry already carries the resulting position. let _ = s.dimupt; - // DIMJOGANG is consumed by the jogged-radius path. - let _ = s.dimjogang; + let _ = (s.dimarcsym, s.dimjogang); // DIMUNIT is the obsolete pre-R2000 linear unit format; DIMLUNIT // supersedes it. Read but not honoured. let _ = s.dimunit; @@ -4345,15 +4411,23 @@ fn dimension_geometry( let override_center = lv(d.override_center); let (near, far) = jogged_radial_break(chord, jog, override_center, d.jog_angle as f32); - add_segment(&mut g.dim_lines, chord, near); - add_segment(&mut g.dim_lines, near, far); - add_segment(&mut g.dim_lines, far, override_center); + if !suppress.dim1 { + add_segment(&mut g.dim_lines, chord, near); + add_segment(&mut g.dim_lines, near, far); + add_segment(&mut g.dim_lines, far, override_center); + } append_arrow( &mut g, chord, normalized_or(near - chord, Vec3::X), arrow1, ); + append_center_mark( + &mut g, + lv(d.definition_point), + params.dimcen, + (chord - lv(d.definition_point)).length(), + ); } } g @@ -4789,6 +4863,43 @@ pub(crate) fn arc_dimension_in_ocs( Some((local, normal)) } +pub(crate) fn large_radial_dimension_in_ocs( + dimension: &DimensionLargeRadial, +) -> Option<(DimensionLargeRadial, Vector3)> { + let length = dimension.base.normal.length(); + if !length.is_finite() || length <= 1.0e-12 { + return None; + } + let normal = dimension.base.normal / length; + if (normal - Vector3::UNIT_Z).length() <= 1.0e-12 { + return None; + } + let normal_tuple = (normal.x, normal.y, normal.z); + let to_ocs = |point: Vector3| { + let point = crate::scene::view::transform::wcs_point_to_ocs( + (point.x, point.y, point.z), + normal_tuple, + ); + Vector3::new(point.0, point.1, point.2) + }; + let mut local = dimension.clone(); + local.definition_point = to_ocs(dimension.definition_point); + let elevation = local.definition_point.z; + let on_plane = |point: Vector3| { + let mut point = to_ocs(point); + point.z = elevation; + point + }; + local.chord_point = on_plane(dimension.chord_point); + local.override_center = on_plane(dimension.override_center); + local.jog_point = on_plane(dimension.jog_point); + local.base.definition_point = local.definition_point; + local.base.text_middle_point = on_plane(dimension.base.text_middle_point); + local.base.insertion_point = on_plane(dimension.base.insertion_point); + local.base.normal = Vector3::UNIT_Z; + Some((local, normal)) +} + fn map_wire_ocs_to_wcs(wire: &mut WireModel, normal: Vector3) { let normal_tuple = (normal.x, normal.y, normal.z); let map = |x: f64, y: f64, z: f64| { diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 62568e3d..92147a70 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -767,6 +767,12 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "DIMJOGGED: created a jogged radius dimension." => Some(("annotate", "dimjogged-created-a-jogged-radius-dimension")), "DIMJOGGED: invalid radius." => Some(("annotate", "dimjogged-invalid-radius")), "DIMJOGGED: select an arc or circle first." => Some(("annotate", "dimjogged-select-an-arc-or-circle-first")), + "DIMJOGGED Enter dimension text (blank = measured value):" => Some(("annotate", "dimjogged-enter-dimension-text-blank-measured")), + "DIMJOGGED Select arc, circle, or polyline arc:" => Some(("annotate", "dimjogged-select-arc-circle-or-polyline-arc")), + "DIMJOGGED Specify center location override:" => Some(("annotate", "dimjogged-specify-center-location-override")), + "DIMJOGGED Specify dimension line location [Mtext/Text/Angle]:" => Some(("annotate", "dimjogged-specify-dimension-line-location-text")), + "DIMJOGGED Specify jog location:" => Some(("annotate", "dimjogged-specify-jog-location")), + "DIMJOGGED Specify text angle (degrees):" => Some(("annotate", "dimjogged-specify-text-angle-degrees")), "DIMJOGLINE Select linear or aligned dimension:" => Some(("annotate", "dimjogline-select-linear-or-aligned-dimension")), "DIMJOGLINE Specify jog location:" => Some(("annotate", "dimjogline-specify-jog-location")), "DIMJOGLINE: not yet implemented — nothing changed." => Some(("common", "dimjogline-not-yet-implemented-nothing-changed")), @@ -1298,6 +1304,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "JUSTIFYTEXT: updated {n} text object(s)." => Some(("annotate", "justifytext-updated-n-text-object-s")), "Jog Angle" => Some(("properties", "jog-angle")), "Jog Line" => Some(("annotate", "jog-line")), + "Jogged Radius" => Some(("common", "jogged-radius")), "Jog X" => Some(("properties", "jog-x")), "Jog Y" => Some(("properties", "jog-y")), "Jog Z" => Some(("properties", "jog-z")), diff --git a/src/modules/annotate/jogged_radius_dim.rs b/src/modules/annotate/jogged_radius_dim.rs new file mode 100644 index 00000000..fdfc6097 --- /dev/null +++ b/src/modules/annotate/jogged_radius_dim.rs @@ -0,0 +1,479 @@ +use acadrust::entities::{Dimension, DimensionLargeRadial}; +use acadrust::types::{Handle, Vector3}; +use acadrust::EntityType; +use glam::{DVec3, Vec3}; + +use crate::command::{ + CadCommand, CmdOption, CmdResult, DimensionAssociationInput, DimensionAssociationSource, + WorkingPlane, +}; +use crate::modules::{IconKind, ModuleEvent, ToolDef}; +use crate::scene::creation_style::DimensionCreationDefaults; +use crate::scene::dimension_assoc::RadialSourceGeometry; +use crate::scene::model::wire_model::WireModel; +use crate::t; + +pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/dim_jog.svg")); + +pub fn tool() -> ToolDef { + ToolDef { + id: "DIMJOGGED", + label: "Jogged Radius", + icon: ICON, + event: ModuleEvent::Command("DIMJOGGED".to_string()), + } +} + +#[derive(Clone, Copy)] +enum Step { + SelectObject, + OverrideCenter(RadialSourceGeometry), + DimLine { + source: RadialSourceGeometry, + override_center: DVec3, + }, + Jog { + source: RadialSourceGeometry, + override_center: DVec3, + chord: DVec3, + text_position: DVec3, + }, +} + +pub struct JoggedRadiusDimensionCommand { + step: Step, + defaults: DimensionCreationDefaults, + editor_height: f64, + text_override: Option, + text_angle: Option, + awaiting_text: bool, + awaiting_angle: bool, + picked_entity: Option, + source_handle: Option, +} + +impl JoggedRadiusDimensionCommand { + pub fn new(defaults: DimensionCreationDefaults, annotation_multiplier: f64) -> Self { + let display_scale = if defaults.annotative { + annotation_multiplier.max(f64::EPSILON) + } else { + defaults.scale + }; + let editor_height = defaults.text_height * display_scale; + Self { + step: Step::SelectObject, + defaults, + editor_height, + text_override: None, + text_angle: None, + awaiting_text: false, + awaiting_angle: false, + picked_entity: None, + source_handle: None, + } + } + + fn editor_anchor(&self) -> DVec3 { + match self.step { + Step::SelectObject => DVec3::ZERO, + Step::OverrideCenter(source) => dvec(source.point_at_angle(source.start_angle)), + Step::DimLine { + override_center, .. + } + | Step::Jog { + override_center, .. + } => override_center, + } + } + + fn commit_dimension( + &self, + source: RadialSourceGeometry, + override_center_world: DVec3, + chord_world: DVec3, + text_world: DVec3, + jog_world: DVec3, + ) -> CmdResult { + let plane = source_plane(source); + let center = plane.to_local(dvec(source.center_world())); + let chord = plane.to_local(chord_world); + let override_center = plane.to_local(override_center_world); + let jog = project_jog(override_center, chord, plane.to_local(jog_world)); + let text_position = plane.to_local(text_world); + + let mut dimension = DimensionLargeRadial::default(); + dimension.definition_point = v3(center); + dimension.base.definition_point = v3(center); + dimension.chord_point = v3(chord); + dimension.override_center = v3(override_center); + dimension.jog_point = v3(jog); + dimension.jog_angle = self.defaults.jog_angle; + dimension.base.style_name.clone_from(&self.defaults.style_name); + dimension.base.text_middle_point = v3(text_position); + dimension.base.insertion_point = v3(text_position); + 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(vec![Some( + DimensionAssociationSource::explicit( + handle, + source.marker, + source.angle_at(chord_world.to_array()), + ), + )]) + }, + ); + + CmdResult::CommitDimension { + entity: plane.place_entity(EntityType::Dimension(Dimension::LargeRadial( + dimension, + ))), + association, + } + } +} + +impl CadCommand for JoggedRadiusDimensionCommand { + fn set_working_plane(&mut self, _plane: WorkingPlane) {} + + fn name(&self) -> &'static str { + "DIMJOGGED" + } + + fn prompt(&self) -> String { + if self.awaiting_text { + return t!("DIMJOGGED Enter dimension text (blank = measured value):").into_owned(); + } + if self.awaiting_angle { + return t!("DIMJOGGED Specify text angle (degrees):").into_owned(); + } + match self.step { + Step::SelectObject => { + t!("DIMJOGGED Select arc, circle, or polyline arc:").into_owned() + } + Step::OverrideCenter(_) => { + t!("DIMJOGGED Specify center location override:").into_owned() + } + Step::DimLine { .. } => t!( + "DIMJOGGED Specify dimension line location [Mtext/Text/Angle]:" + ) + .into_owned(), + Step::Jog { .. } => t!("DIMJOGGED Specify jog location:").into_owned(), + } + } + + fn on_point(&mut self, point: DVec3) -> CmdResult { + match self.step { + Step::SelectObject => CmdResult::NeedPoint, + Step::OverrideCenter(source) => { + self.step = Step::DimLine { + source, + override_center: point, + }; + CmdResult::NeedPoint + } + Step::DimLine { + source, + override_center, + } => { + self.step = Step::Jog { + source, + override_center, + chord: dvec(source.chord_at(point.to_array())), + text_position: point, + }; + CmdResult::NeedPoint + } + Step::Jog { + source, + override_center, + chord, + text_position, + } => self.commit_dimension( + source, + override_center, + chord, + text_position, + point, + ), + } + } + + 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 { + if matches!(self.step, Step::DimLine { .. }) + && !self.awaiting_text + && !self.awaiting_angle + { + vec![ + CmdOption::new("MText", "MTEXT"), + CmdOption::new("Text", "TEXT"), + CmdOption::new("Angle", "ANGLE"), + ] + } else { + Vec::new() + } + } + + 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); + } + if !matches!(self.step, Step::DimLine { .. }) { + return None; + } + match text.trim().to_ascii_uppercase().as_str() { + "T" | "TEXT" => { + self.awaiting_text = true; + Some(CmdResult::NeedPoint) + } + "M" | "MTEXT" => Some(CmdResult::SuspendForMTextInput { + pos: self.editor_anchor(), + initial: self.text_override.clone().unwrap_or_default(), + height: self.editor_height, + }), + "A" | "ANGLE" => { + self.awaiting_angle = true; + 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.radius.is_finite() || source.radius <= 1e-12 { + return CmdResult::NeedPoint; + } + self.source_handle = Some(handle); + self.step = Step::OverrideCenter(source); + CmdResult::NeedPoint + } + + fn on_mouse_move(&mut self, point: DVec3) -> Option { + match self.step { + Step::SelectObject => None, + Step::OverrideCenter(source) => Some(preview_wire(vec![ + dvec(source.center_world()).as_vec3(), + point.as_vec3(), + ])), + Step::DimLine { + source, + override_center, + } => { + let chord = dvec(source.chord_at(point.to_array())); + Some(preview_wire(vec![ + override_center.as_vec3(), + chord.as_vec3(), + Vec3::splat(f32::NAN), + chord.as_vec3(), + point.as_vec3(), + ])) + } + Step::Jog { + source, + override_center, + chord, + .. + } => { + let plane = source_plane(source); + let chord_local = plane.to_local(chord); + let override_local = plane.to_local(override_center); + let jog_local = project_jog(override_local, chord_local, plane.to_local(point)); + let (near, far) = jog_break( + chord_local, + jog_local, + override_local, + self.defaults.jog_angle, + ); + Some(preview_wire( + [chord_local, near, far, override_local] + .into_iter() + .map(|position| plane.to_world(position).as_vec3()) + .collect(), + )) + } + } + } +} + +fn source_plane(source: RadialSourceGeometry) -> WorkingPlane { + WorkingPlane::new( + DVec3::from_array(source.plane.origin), + DVec3::from_array(source.plane.x_axis), + DVec3::from_array(source.plane.y_axis), + ) +} + +fn project_jog(override_center: DVec3, chord: DVec3, point: DVec3) -> DVec3 { + let line = chord - override_center; + let length_squared = line.length_squared(); + if length_squared <= 1e-18 { + return override_center; + } + let factor = ((point - override_center).dot(line) / length_squared).clamp(0.0, 1.0); + override_center + line * factor +} + +fn jog_break( + chord: DVec3, + jog: DVec3, + override_center: DVec3, + jog_angle: f64, +) -> (DVec3, DVec3) { + let radial = (chord - override_center).normalize_or_zero(); + let (sin, cos) = jog_angle.sin_cos(); + let transverse = DVec3::new( + radial.x * cos - radial.y * sin, + radial.x * sin + radial.y * cos, + 0.0, + ) + .normalize_or_zero(); + let half = ((chord - override_center).length() * 0.04).max(1e-3); + let first = jog - transverse * half; + let second = jog + transverse * half; + if chord.distance_squared(first) <= chord.distance_squared(second) { + (first, second) + } else { + (second, first) + } +} + +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 preview_wire(points: Vec) -> 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: "dimjogged_preview".to_string(), + points: points.into_iter().map(|point| point.to_array()).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::new(), + tangent_geoms: Vec::new(), + aci: 0, + key_vertices: Vec::new(), + aabb: WireModel::UNBOUNDED_AABB, + plinegen: true, + fill_tris: Vec::new(), + fill_tris_low: Vec::new(), + } +} + +inventory::submit!(crate::command::CommandRegistration { + names: &["DIMJOGGED", "DIMJOG"] +}); diff --git a/src/modules/annotate/mod.rs b/src/modules/annotate/mod.rs index bb0f2c1f..8c48397f 100644 --- a/src/modules/annotate/mod.rs +++ b/src/modules/annotate/mod.rs @@ -16,6 +16,7 @@ pub mod dimspace; pub mod dimtedit; pub mod leader_cmd; pub mod linear_dim; +pub mod jogged_radius_dim; pub mod mleader_cmd; pub mod mleader_edit; pub mod mtext; @@ -109,6 +110,11 @@ impl CadModule for AnnotateModule { radius_dim::tool().label, radius_dim::tool().icon, ), + ( + jogged_radius_dim::tool().id, + jogged_radius_dim::tool().label, + jogged_radius_dim::tool().icon, + ), ( diameter_dim::tool().id, diameter_dim::tool().label, diff --git a/src/modules/draw/modify/explode.rs b/src/modules/draw/modify/explode.rs index 1624a8c4..f8626854 100644 --- a/src/modules/draw/modify/explode.rs +++ b/src/modules/draw/modify/explode.rs @@ -927,6 +927,24 @@ fn explode_dimension(dim: &Dimension, doc: &CadDocument) -> Vec { .collect(); } } + if let Dimension::LargeRadial(radial) = dim { + if let Some((local_radial, normal)) = + crate::entities::dimension::large_radial_dimension_in_ocs(radial) + { + let (x_axis, y_axis) = crate::scene::view::transform::ocs_axes(( + normal.x, normal.y, normal.z, + )); + let plane = WorkingPlane::new( + DVec3::ZERO, + DVec3::new(x_axis.0, x_axis.1, x_axis.2), + DVec3::new(y_axis.0, y_axis.1, y_axis.2), + ); + return explode_dimension(&Dimension::LargeRadial(local_radial), doc) + .into_iter() + .map(|entity| plane.place_entity(entity)) + .collect(); + } + } let base = dim.base(); let met = dim_metrics(dim, doc); @@ -1235,9 +1253,11 @@ fn explode_dimension(dim: &Dimension, doc: &CadDocument) -> Vec { d.override_center, d.jog_angle, ); - result.push(make_seg(&d.chord_point, &near, &dim_c)); - result.push(make_seg(&near, &far, &dim_c)); - result.push(make_seg(&far, &d.override_center, &dim_c)); + if !met.dimsd1 { + result.push(make_seg(&d.chord_point, &near, &dim_c)); + result.push(make_seg(&near, &far, &dim_c)); + result.push(make_seg(&far, &d.override_center, &dim_c)); + } let len = ((near.x - d.chord_point.x).powi(2) + (near.y - d.chord_point.y).powi(2)) .sqrt() @@ -1249,6 +1269,15 @@ fn explode_dimension(dim: &Dimension, doc: &CadDocument) -> Vec { &met.arrow1, &dim_c, )); + let radius = ((d.definition_point.x - d.chord_point.x).powi(2) + + (d.definition_point.y - d.chord_point.y).powi(2)) + .sqrt(); + result.extend(dim_center_mark( + d.definition_point, + met.dimcen, + radius, + &dim_c, + )); } } diff --git a/src/scene/creation_style.rs b/src/scene/creation_style.rs index bfc4ac5c..442d6118 100644 --- a/src/scene/creation_style.rs +++ b/src/scene/creation_style.rs @@ -18,6 +18,7 @@ pub struct DimensionCreationDefaults { pub text_height: f64, pub gap: f64, pub arrow_size: f64, + pub jog_angle: f64, pub scale: f64, pub annotative: bool, } @@ -92,6 +93,9 @@ pub fn current_dimension_defaults(doc: &CadDocument) -> DimensionCreationDefault text_height: style.as_ref().map_or(2.5, |style| style.dimtxt), gap: style.as_ref().map_or(0.625, |style| style.dimgap), arrow_size: style.as_ref().map_or(2.5, |style| style.dimasz), + jog_angle: style + .as_ref() + .map_or(std::f64::consts::FRAC_PI_4, |style| style.dimjogang), scale: style.as_ref().map_or(1.0, |style| { if style.annotative { 1.0 diff --git a/src/scene/dimension_assoc.rs b/src/scene/dimension_assoc.rs index 0cb179fa..9e752c68 100644 --- a/src/scene/dimension_assoc.rs +++ b/src/scene/dimension_assoc.rs @@ -559,6 +559,7 @@ pub(crate) fn radial_extension_points( let target_point = match dimension_entity { Dimension::Radius(radius) => radius.definition_point, Dimension::Diameter(diameter) => diameter.angle_vertex, + Dimension::LargeRadial(radial) => radial.chord_point, _ => return None, }; let association = document.objects.values().find_map(|object| { @@ -655,6 +656,11 @@ impl Scene { diameter.measurement() * 0.5, diameter.angle_vertex, )), + Dimension::LargeRadial(radial) => Some(( + radial.definition_point, + radial.measurement(), + radial.chord_point, + )), _ => None, }; if let Some((center, measured_radius, chord)) = radial_data { @@ -810,6 +816,11 @@ impl Scene { diameter.measurement() * 0.5, diameter.angle_vertex, )), + Dimension::LargeRadial(radial) => Some(( + radial.definition_point, + radial.measurement(), + radial.chord_point, + )), _ => None, }; if let Some((center, radius, chord)) = radial_data { @@ -1003,6 +1014,33 @@ impl Scene { } diameter.base.actual_measurement = diameter.measurement(); } + Dimension::LargeRadial(radial) => { + let Some((source, angle)) = radial_source else { + continue; + }; + let old_center = radial.definition_point; + let new_center = source.center_world(); + let new_chord = source.point_at_angle(angle); + let center_delta = Vector3::new( + new_center.x - old_center.x, + new_center.y - old_center.y, + new_center.z - old_center.z, + ); + radial.definition_point = new_center; + radial.base.definition_point = new_center; + radial.chord_point = new_chord; + if point_distance_squared(center_delta, Vector3::default()) > 1e-18 { + radial.override_center = radial.override_center + center_delta; + radial.jog_point = radial.jog_point + center_delta; + if radial.base.text_user_positioned { + radial.base.text_middle_point = + radial.base.text_middle_point + center_delta; + radial.base.insertion_point = + radial.base.insertion_point + center_delta; + } + } + radial.base.actual_measurement = radial.measurement(); + } Dimension::Ordinate(ordinate) => { if let Some(feature) = resolved[0] { ordinate.feature_location = feature; @@ -1098,7 +1136,6 @@ impl Scene { } arc.base.actual_measurement = arc.measurement(); } - _ => continue, } dimension.base_mut().actual_measurement = dimension.measurement(); refreshed.push((association.dimension, ChangeKind::Modified));