Complete angular dimension interaction and associations
This commit is contained in:
parent
a8f05716cf
commit
24b344b2b2
7 changed files with 777 additions and 75 deletions
|
|
@ -581,6 +581,36 @@ impl OpenCADStudio {
|
|||
let _ = self.apply_cmd_result(r);
|
||||
return;
|
||||
}
|
||||
let accepts_points = self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.is_some_and(|command| command.entity_pick_accepts_points());
|
||||
if accepts_points {
|
||||
if let Some((coord, kind)) = super::helpers::parse_coord(token) {
|
||||
let ucs = self.tabs[i].active_ucs.clone();
|
||||
let wcs = match (
|
||||
matches!(kind, super::helpers::CoordKind::Relative),
|
||||
self.last_point,
|
||||
) {
|
||||
(true, Some(base)) => {
|
||||
base + match &ucs {
|
||||
Some(value) => super::helpers::ucs_rotate_vec(coord, value),
|
||||
None => coord,
|
||||
}
|
||||
}
|
||||
_ => match &ucs {
|
||||
Some(value) => super::helpers::ucs_to_wcs(coord, value),
|
||||
None => coord,
|
||||
},
|
||||
};
|
||||
if self.command_point_allowed(i, wcs) {
|
||||
self.last_point = Some(wcs);
|
||||
self.push_ucs_to_cmd(i);
|
||||
let _ = self.feed_command(StepInput::Point(wcs));
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if let Ok(v) = u64::from_str_radix(token.trim_start_matches("0x"), 16) {
|
||||
let handle = Handle::new(v);
|
||||
let pt = self.tabs[i]
|
||||
|
|
@ -1257,6 +1287,30 @@ impl OpenCADStudio {
|
|||
self.commit_undo_delta(i, pending);
|
||||
}
|
||||
}
|
||||
CmdResult::CommitAssociativeDimensionDetailed { entity, sources } => {
|
||||
let label = self.history_label_from_active_cmd(i, "DIMENSION");
|
||||
let pending = self.begin_undo(i, label, 1, false);
|
||||
if let Some(handle) = self.commit_entity_handle(entity) {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.attach_dimension_association_sources(handle, sources.clone());
|
||||
let mut changed = vec![(handle, crate::scene::ChangeKind::Modified)];
|
||||
changed.extend(sources.iter().flatten().map(|source| {
|
||||
(source.handle, crate::scene::ChangeKind::Modified)
|
||||
}));
|
||||
changed.sort_by_key(|(handle, _)| handle.value());
|
||||
changed.dedup_by_key(|(handle, _)| handle.value());
|
||||
self.tabs[i].scene.bump_entities(&changed);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
if let Some(pending) = pending {
|
||||
self.commit_undo_delta(i, pending);
|
||||
}
|
||||
}
|
||||
CmdResult::CommitSolid {
|
||||
entity,
|
||||
solid,
|
||||
|
|
@ -3572,6 +3626,19 @@ impl OpenCADStudio {
|
|||
self.tabs[i].snap_result = None;
|
||||
self.open_mtext_editor(pos, handle, &initial, height);
|
||||
}
|
||||
CmdResult::SuspendForMTextInput {
|
||||
pos,
|
||||
initial,
|
||||
height,
|
||||
} => {
|
||||
self.tabs[i].suspended_cmd = self.tabs[i].active_cmd.take();
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.command_mtext_input = true;
|
||||
self.pending_command_editor_text = None;
|
||||
self.open_mtext_editor(pos, None, &initial, height);
|
||||
}
|
||||
CmdResult::OpenTextEditor {
|
||||
pos,
|
||||
handle,
|
||||
|
|
|
|||
|
|
@ -764,6 +764,10 @@ pub(super) struct OpenCADStudio {
|
|||
ctrl_down: bool,
|
||||
/// Open in-place MText editor (toolbar + text area + live preview), if any.
|
||||
mtext_editor: Option<mtext_editor::MTextEditorState>,
|
||||
/// The current rich-text editor belongs to a suspended drawing command and
|
||||
/// must return text instead of creating a standalone MText entity.
|
||||
command_mtext_input: bool,
|
||||
pending_command_editor_text: Option<String>,
|
||||
/// Open in-place single-line TEXT editor (plain text-entry box), if any.
|
||||
text_inline: Option<text_inline::TextInlineState>,
|
||||
/// Cursor-anchored one-shot snap override menu (Shift+RMB): the canvas
|
||||
|
|
@ -3252,6 +3256,8 @@ impl OpenCADStudio {
|
|||
ctrl_down: false,
|
||||
cont_anchor: None,
|
||||
mtext_editor: None,
|
||||
command_mtext_input: false,
|
||||
pending_command_editor_text: None,
|
||||
text_inline: None,
|
||||
snap_override_popup: None,
|
||||
axis_lock_dir: None,
|
||||
|
|
|
|||
|
|
@ -1209,6 +1209,12 @@ impl super::OpenCADStudio {
|
|||
pub(super) fn mtext_commit(&mut self) -> bool {
|
||||
let i = self.active_tab;
|
||||
let Some(ed) = self.mtext_editor.take() else { return false };
|
||||
if self.command_mtext_input {
|
||||
self.pending_command_editor_text = Some(ed.build_mtext().value);
|
||||
self.command_mtext_input = false;
|
||||
self.refresh_properties();
|
||||
return true;
|
||||
}
|
||||
let body_empty = ed.content.text().trim().is_empty();
|
||||
let mut mt = ed.build_mtext();
|
||||
let annotative = ed.editing.is_none()
|
||||
|
|
@ -1302,6 +1308,10 @@ impl super::OpenCADStudio {
|
|||
/// then updated in place on later applies, so repeated Apply never leaves
|
||||
/// duplicate entities behind.
|
||||
pub(super) fn mtext_apply(&mut self) {
|
||||
if self.command_mtext_input {
|
||||
self.rebuild_mtext_preview();
|
||||
return;
|
||||
}
|
||||
let i = self.active_tab;
|
||||
let (mut mt, editing, body_empty) = match self.mtext_editor.as_ref() {
|
||||
Some(ed) => (
|
||||
|
|
@ -1398,6 +1408,8 @@ impl super::OpenCADStudio {
|
|||
/// Discard the editor without changing the drawing.
|
||||
pub(super) fn mtext_cancel(&mut self) {
|
||||
self.mtext_editor = None;
|
||||
self.command_mtext_input = false;
|
||||
self.pending_command_editor_text = None;
|
||||
self.reset_modal_geometry();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -492,6 +492,13 @@ impl OpenCADStudio {
|
|||
self.reset_modal_geometry();
|
||||
let i = self.active_tab;
|
||||
if let Some(mut cmd) = self.tabs[i].suspended_cmd.take() {
|
||||
if committed {
|
||||
if let Some(value) = self.pending_command_editor_text.take() {
|
||||
cmd.on_editor_text(value);
|
||||
}
|
||||
} else {
|
||||
self.pending_command_editor_text = None;
|
||||
}
|
||||
let res = cmd.on_editor_closed(committed);
|
||||
self.tabs[i].active_cmd = Some(cmd);
|
||||
self.apply_cmd_result(res)
|
||||
|
|
|
|||
|
|
@ -169,6 +169,34 @@ pub struct SelectionEntity {
|
|||
pub surface_area: Option<f64>,
|
||||
}
|
||||
|
||||
/// One source point used by an associative dimension. A missing marker asks
|
||||
/// the scene to infer the closest stable sub-entity; an explicit marker keeps
|
||||
/// segment-level references intact.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct DimensionAssociationSource {
|
||||
pub handle: Handle,
|
||||
pub marker: Option<i32>,
|
||||
pub parameter: f64,
|
||||
}
|
||||
|
||||
impl DimensionAssociationSource {
|
||||
pub const fn inferred(handle: Handle) -> Self {
|
||||
Self {
|
||||
handle,
|
||||
marker: None,
|
||||
parameter: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn explicit(handle: Handle, marker: i32, parameter: f64) -> Self {
|
||||
Self {
|
||||
handle,
|
||||
marker: Some(marker),
|
||||
parameter,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum AreaPreviewSource {
|
||||
Handles(Vec<Handle>),
|
||||
|
|
@ -1217,6 +1245,12 @@ pub enum CmdResult {
|
|||
entity: EntityType,
|
||||
sources: Vec<Handle>,
|
||||
},
|
||||
/// Commit a dimension whose definition points carry explicit sub-entity
|
||||
/// references. `None` leaves that definition point independent.
|
||||
CommitAssociativeDimensionDetailed {
|
||||
entity: EntityType,
|
||||
sources: Vec<Option<DimensionAssociationSource>>,
|
||||
},
|
||||
/// Commit a Model-tab 3D solid: the acadrust entity (for selection /
|
||||
/// persistence) plus its B-rep (cached for boolean ops + shaded
|
||||
/// rendering). Ends the command.
|
||||
|
|
@ -1396,6 +1430,13 @@ pub enum CmdResult {
|
|||
initial: String,
|
||||
height: f64,
|
||||
},
|
||||
/// Temporarily suspend the active command and collect formatted text in the
|
||||
/// rich editor without creating a standalone MText entity.
|
||||
SuspendForMTextInput {
|
||||
pos: DVec3,
|
||||
initial: String,
|
||||
height: f64,
|
||||
},
|
||||
/// Open the in-place single-line TEXT editor (a plain text-entry box, no
|
||||
/// formatting toolbar). `handle` is `Some` when editing an existing Text,
|
||||
/// `None` when creating a new one at `pos`.
|
||||
|
|
@ -1777,6 +1818,12 @@ pub trait CadCommand: Send {
|
|||
false
|
||||
}
|
||||
|
||||
/// Allow typed coordinates while object picking is active. Commands whose
|
||||
/// first prompt accepts either an object or a point opt in explicitly.
|
||||
fn entity_pick_accepts_points(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Include filled hatch / DXF SOLID regions in the entity hit-test.
|
||||
///
|
||||
/// Most entity-pick commands operate on curve geometry and intentionally
|
||||
|
|
@ -1805,6 +1852,10 @@ pub trait CadCommand: Send {
|
|||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
/// Supplies formatted text collected by `SuspendForMTextInput` before the
|
||||
/// command is resumed through `on_editor_closed`.
|
||||
fn on_editor_text(&mut self, _value: String) {}
|
||||
|
||||
/// Called when the user clicks and `needs_entity_pick()` is true.
|
||||
/// `handle` is the nearest wire's entity handle (Handle::NULL if nothing found).
|
||||
fn on_entity_pick(&mut self, _handle: Handle, _pt: DVec3) -> CmdResult {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ use acadrust::entities::{Dimension, DimensionAngular2Ln, DimensionAngular3Pt};
|
|||
use acadrust::types::{Handle, Vector3};
|
||||
use acadrust::EntityType;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
||||
use crate::command::{
|
||||
CadCommand, CmdOption, CmdResult, DimensionAssociationSource, WorkingPlane,
|
||||
};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use crate::t;
|
||||
|
|
@ -26,13 +28,13 @@ enum Step {
|
|||
CircleSecondRay {
|
||||
vertex: DVec3,
|
||||
first: DVec3,
|
||||
radius: f64,
|
||||
source: Handle,
|
||||
},
|
||||
SecondLine {
|
||||
first_start: DVec3,
|
||||
first_end: DVec3,
|
||||
first_source: Handle,
|
||||
first_refs: [SourceLocator; 2],
|
||||
},
|
||||
ArcPoint3 {
|
||||
vertex: DVec3,
|
||||
|
|
@ -47,16 +49,44 @@ enum Step {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
struct SourceLocator {
|
||||
marker: Option<i32>,
|
||||
parameter: f64,
|
||||
}
|
||||
|
||||
impl SourceLocator {
|
||||
const INFERRED: Self = Self { marker: None, parameter: 0.0 };
|
||||
|
||||
const fn explicit(marker: i32, parameter: f64) -> Self {
|
||||
Self { marker: Some(marker), parameter }
|
||||
}
|
||||
|
||||
fn bind(self, handle: Handle) -> DimensionAssociationSource {
|
||||
match self.marker {
|
||||
Some(marker) => DimensionAssociationSource::explicit(handle, marker, self.parameter),
|
||||
None => DimensionAssociationSource::inferred(handle),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum PickedCurve {
|
||||
Line(DVec3, DVec3),
|
||||
Line {
|
||||
start: DVec3,
|
||||
end: DVec3,
|
||||
refs: [SourceLocator; 2],
|
||||
normal: Option<DVec3>,
|
||||
},
|
||||
Arc {
|
||||
center: DVec3,
|
||||
first: DVec3,
|
||||
second: DVec3,
|
||||
refs: [SourceLocator; 3],
|
||||
normal: DVec3,
|
||||
},
|
||||
Circle {
|
||||
center: DVec3,
|
||||
radius: f64,
|
||||
normal: DVec3,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -65,12 +95,15 @@ pub struct AngularDimensionCommand {
|
|||
plane: WorkingPlane,
|
||||
selecting_object: bool,
|
||||
picked_entity: Option<EntityType>,
|
||||
source_handles: Vec<Handle>,
|
||||
source_refs: Vec<Option<DimensionAssociationSource>>,
|
||||
text_override: Option<String>,
|
||||
awaiting_text: bool,
|
||||
mtext_override: bool,
|
||||
text_angle: Option<f64>,
|
||||
awaiting_angle: bool,
|
||||
angle_origin: Option<DVec3>,
|
||||
awaiting_quadrant: bool,
|
||||
quadrant_lock: Option<(f64, f64)>,
|
||||
}
|
||||
|
||||
impl AngularDimensionCommand {
|
||||
|
|
@ -78,14 +111,17 @@ impl AngularDimensionCommand {
|
|||
Self {
|
||||
step: Step::Vertex,
|
||||
plane: WorkingPlane::default(),
|
||||
selecting_object: false,
|
||||
selecting_object: true,
|
||||
picked_entity: None,
|
||||
source_handles: Vec::new(),
|
||||
source_refs: Vec::new(),
|
||||
text_override: None,
|
||||
awaiting_text: false,
|
||||
mtext_override: false,
|
||||
text_angle: None,
|
||||
awaiting_angle: false,
|
||||
angle_origin: None,
|
||||
awaiting_quadrant: false,
|
||||
quadrant_lock: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -99,12 +135,23 @@ impl AngularDimensionCommand {
|
|||
let vertex = self.plane.to_local(vertex);
|
||||
let first = self.plane.to_local(first);
|
||||
let second = self.plane.to_local(second);
|
||||
let arc_point = self.plane.to_local(arc_point);
|
||||
let picked_text = self.plane.to_local(arc_point);
|
||||
let arc_point = locked_arc_point(vertex, picked_text, self.quadrant_lock);
|
||||
if two_line_frame(vertex, first, vertex, second, arc_point).is_none() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
let mut dim = DimensionAngular3Pt::new(v3(vertex), v3(first), v3(second));
|
||||
dim.definition_point = v3(arc_point);
|
||||
dim.base.definition_point = dim.definition_point;
|
||||
dim.base.text_middle_point = dim.definition_point;
|
||||
dim.base.insertion_point = dim.definition_point;
|
||||
if self.quadrant_lock.is_some_and(|frame| {
|
||||
!point_angle_in_frame(vertex, picked_text, frame)
|
||||
}) {
|
||||
dim.base.text_middle_point = v3(picked_text);
|
||||
dim.base.insertion_point = dim.base.text_middle_point;
|
||||
dim.base.text_user_positioned = true;
|
||||
}
|
||||
dim.base.actual_measurement = dim.measurement_degrees();
|
||||
crate::entities::dimension::set_dimension_text_override(
|
||||
&mut dim.base,
|
||||
|
|
@ -128,7 +175,28 @@ impl AngularDimensionCommand {
|
|||
let first_end = self.plane.to_local(first_end);
|
||||
let second_start = self.plane.to_local(second_start);
|
||||
let second_end = self.plane.to_local(second_end);
|
||||
let arc_point = self.plane.to_local(arc_point);
|
||||
let picked_text = self.plane.to_local(arc_point);
|
||||
let Some((vertex, _, _)) = two_line_frame(
|
||||
first_start,
|
||||
first_end,
|
||||
second_start,
|
||||
second_end,
|
||||
picked_text,
|
||||
) else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
let arc_point = locked_arc_point(vertex, picked_text, self.quadrant_lock);
|
||||
if two_line_frame(
|
||||
first_start,
|
||||
first_end,
|
||||
second_start,
|
||||
second_end,
|
||||
arc_point,
|
||||
)
|
||||
.is_none()
|
||||
{
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
let mut dim = DimensionAngular2Ln::default();
|
||||
dim.first_point = v3(first_start);
|
||||
dim.second_point = v3(first_end);
|
||||
|
|
@ -138,6 +206,13 @@ impl AngularDimensionCommand {
|
|||
dim.base.definition_point = dim.dimension_arc;
|
||||
dim.base.text_middle_point = dim.dimension_arc;
|
||||
dim.base.insertion_point = dim.dimension_arc;
|
||||
if self.quadrant_lock.is_some_and(|frame| {
|
||||
!point_angle_in_frame(vertex, picked_text, frame)
|
||||
}) {
|
||||
dim.base.text_middle_point = v3(picked_text);
|
||||
dim.base.insertion_point = dim.base.text_middle_point;
|
||||
dim.base.text_user_positioned = true;
|
||||
}
|
||||
dim.base.actual_measurement = dim.measurement_degrees();
|
||||
crate::entities::dimension::set_dimension_text_override(
|
||||
&mut dim.base,
|
||||
|
|
@ -151,12 +226,12 @@ impl AngularDimensionCommand {
|
|||
|
||||
fn commit(&self, dimension: Dimension) -> CmdResult {
|
||||
let entity = self.plane.place_entity(EntityType::Dimension(dimension));
|
||||
if self.source_handles.is_empty() {
|
||||
if self.source_refs.iter().all(Option::is_none) {
|
||||
CmdResult::CommitAndExit(entity)
|
||||
} else {
|
||||
CmdResult::CommitAssociativeDimension {
|
||||
CmdResult::CommitAssociativeDimensionDetailed {
|
||||
entity,
|
||||
sources: self.source_handles.clone(),
|
||||
sources: self.source_refs.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -164,6 +239,81 @@ impl AngularDimensionCommand {
|
|||
fn placement_step(&self) -> bool {
|
||||
matches!(self.step, Step::ArcPoint3 { .. } | Step::ArcPoint2 { .. })
|
||||
}
|
||||
|
||||
fn editor_anchor(&self) -> DVec3 {
|
||||
match self.step {
|
||||
Step::ArcPoint3 { vertex, first, second } => {
|
||||
let radius = vertex.distance(first).max(vertex.distance(second)).max(1.0);
|
||||
let first_angle = self.plane.angle(vertex, first).unwrap_or(0.0);
|
||||
let second_angle = self.plane.angle(vertex, second).unwrap_or(first_angle);
|
||||
let sweep = (second_angle - first_angle).rem_euclid(std::f64::consts::TAU);
|
||||
let angle = first_angle + sweep * 0.5;
|
||||
self.plane.to_world(
|
||||
self.plane.to_local(vertex)
|
||||
+ DVec3::new(angle.cos() * radius, angle.sin() * radius, 0.0),
|
||||
)
|
||||
}
|
||||
Step::ArcPoint2 {
|
||||
first_start,
|
||||
first_end,
|
||||
second_start,
|
||||
second_end,
|
||||
} => {
|
||||
let first_start = self.plane.to_local(first_start);
|
||||
let first_end = self.plane.to_local(first_end);
|
||||
let second_start = self.plane.to_local(second_start);
|
||||
let second_end = self.plane.to_local(second_end);
|
||||
two_line_frame(
|
||||
first_start,
|
||||
first_end,
|
||||
second_start,
|
||||
second_end,
|
||||
(first_start + second_start) * 0.5,
|
||||
)
|
||||
.map(|(vertex, start, end)| {
|
||||
self.plane.to_world(vertex + DVec3::new(
|
||||
((start + end) * 0.5).cos(),
|
||||
((start + end) * 0.5).sin(),
|
||||
0.0,
|
||||
))
|
||||
})
|
||||
.unwrap_or_else(|| self.plane.to_world(first_start))
|
||||
}
|
||||
_ => self.plane.origin,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_quadrant_from_point(&mut self, point: DVec3) -> bool {
|
||||
let point = self.plane.to_local(point);
|
||||
let frame = match self.step {
|
||||
Step::ArcPoint3 { vertex, first, second } => two_line_frame(
|
||||
self.plane.to_local(vertex),
|
||||
self.plane.to_local(first),
|
||||
self.plane.to_local(vertex),
|
||||
self.plane.to_local(second),
|
||||
point,
|
||||
),
|
||||
Step::ArcPoint2 {
|
||||
first_start,
|
||||
first_end,
|
||||
second_start,
|
||||
second_end,
|
||||
} => two_line_frame(
|
||||
self.plane.to_local(first_start),
|
||||
self.plane.to_local(first_end),
|
||||
self.plane.to_local(second_start),
|
||||
self.plane.to_local(second_end),
|
||||
point,
|
||||
),
|
||||
_ => None,
|
||||
};
|
||||
if let Some((_, start, end)) = frame {
|
||||
self.quadrant_lock = Some((start, end));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for AngularDimensionCommand {
|
||||
|
|
@ -176,6 +326,9 @@ impl CadCommand for AngularDimensionCommand {
|
|||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if self.awaiting_quadrant {
|
||||
return t!("DIMANGULAR Specify quadrant:").into_owned();
|
||||
}
|
||||
if self.awaiting_text {
|
||||
return if self.mtext_override {
|
||||
t!("DIMANGULAR Enter formatted dimension text (blank = measured value):")
|
||||
|
|
@ -185,17 +338,24 @@ impl CadCommand for AngularDimensionCommand {
|
|||
};
|
||||
}
|
||||
if self.awaiting_angle {
|
||||
return t!("DIMANGULAR Specify text angle (degrees):").into_owned();
|
||||
return if self.angle_origin.is_some() {
|
||||
t!("DIMANGULAR Specify second point for text angle:").into_owned()
|
||||
} else {
|
||||
t!("DIMANGULAR Specify text angle or first point:").into_owned()
|
||||
};
|
||||
}
|
||||
if self.selecting_object {
|
||||
return match self.step {
|
||||
Step::SecondLine { .. } => t!("DIMANGULAR Select second line:").into_owned(),
|
||||
_ => t!("DIMANGULAR Select arc, circle, line, or polyline arc:").into_owned(),
|
||||
_ => t!(
|
||||
"DIMANGULAR Select arc, circle, line, or specify an angle vertex:"
|
||||
)
|
||||
.into_owned(),
|
||||
};
|
||||
}
|
||||
match self.step {
|
||||
Step::Vertex => t!(
|
||||
"DIMANGULAR Specify angle vertex or press Enter to select an object:"
|
||||
"DIMANGULAR Specify angle vertex:"
|
||||
)
|
||||
.into_owned(),
|
||||
Step::FirstRay(_) => {
|
||||
|
|
@ -213,6 +373,29 @@ impl CadCommand for AngularDimensionCommand {
|
|||
}
|
||||
|
||||
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||
if self.awaiting_quadrant {
|
||||
if self.set_quadrant_from_point(point) {
|
||||
self.awaiting_quadrant = false;
|
||||
}
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
if self.awaiting_angle {
|
||||
if let Some(origin) = self.angle_origin {
|
||||
if let Some(angle) = self.plane.angle(origin, point) {
|
||||
self.text_angle = Some(angle);
|
||||
self.awaiting_angle = false;
|
||||
self.angle_origin = None;
|
||||
}
|
||||
} else {
|
||||
self.angle_origin = Some(point);
|
||||
}
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
if self.selecting_object && matches!(self.step, Step::Vertex) {
|
||||
self.selecting_object = false;
|
||||
self.step = Step::FirstRay(point);
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
match self.step {
|
||||
Step::Vertex => {
|
||||
self.step = Step::FirstRay(point);
|
||||
|
|
@ -229,15 +412,26 @@ impl CadCommand for AngularDimensionCommand {
|
|||
if point.distance_squared(vertex) <= 1.0e-24 {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
let first_local = self.plane.vector_to_local(first - vertex);
|
||||
let second_local = self.plane.vector_to_local(point - vertex);
|
||||
if (first_local.x * second_local.y - first_local.y * second_local.x).abs()
|
||||
<= 1.0e-12 * first_local.length().max(second_local.length()).max(1.0)
|
||||
{
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
self.step = Step::ArcPoint3 { vertex, first, second: point };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::CircleSecondRay { vertex, first, radius, source } => {
|
||||
let Some(second) = project_to_radius(vertex, point, radius) else {
|
||||
Step::CircleSecondRay { vertex, first, source } => {
|
||||
if point.distance_squared(vertex) <= 1.0e-24 {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
self.source_handles = vec![source, source, source];
|
||||
self.step = Step::ArcPoint3 { vertex, first, second };
|
||||
}
|
||||
self.source_refs = vec![
|
||||
Some(DimensionAssociationSource::inferred(source)),
|
||||
Some(DimensionAssociationSource::inferred(source)),
|
||||
None,
|
||||
];
|
||||
self.step = Step::ArcPoint3 { vertex, first, second: point };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::SecondLine { .. } => CmdResult::NeedPoint,
|
||||
|
|
@ -260,16 +454,21 @@ impl CadCommand for AngularDimensionCommand {
|
|||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
if self.awaiting_quadrant {
|
||||
self.awaiting_quadrant = false;
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
if self.awaiting_text {
|
||||
self.awaiting_text = false;
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
if self.awaiting_angle {
|
||||
self.awaiting_angle = false;
|
||||
self.angle_origin = None;
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
if matches!(self.step, Step::Vertex) {
|
||||
self.selecting_object = true;
|
||||
self.selecting_object = !self.selecting_object;
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
CmdResult::Cancel
|
||||
|
|
@ -287,8 +486,24 @@ impl CadCommand for AngularDimensionCommand {
|
|||
self.awaiting_text
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
if self.placement_step() && !self.awaiting_text && !self.awaiting_angle && !self.awaiting_quadrant {
|
||||
vec![
|
||||
CmdOption::new("Mtext", "MTEXT"),
|
||||
CmdOption::new("Text", "TEXT"),
|
||||
CmdOption::new("Angle", "ANGLE"),
|
||||
CmdOption::new("Quadrant", "QUADRANT"),
|
||||
]
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
self.placement_step() && !self.awaiting_text && !self.awaiting_angle
|
||||
self.placement_step()
|
||||
&& !self.awaiting_text
|
||||
&& !self.awaiting_angle
|
||||
&& !self.awaiting_quadrant
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
|
|
@ -309,6 +524,7 @@ impl CadCommand for AngularDimensionCommand {
|
|||
crate::entities::common::parse_typed_angle(text.trim())
|
||||
};
|
||||
self.awaiting_angle = false;
|
||||
self.angle_origin = None;
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
if !self.placement_step() {
|
||||
|
|
@ -322,22 +538,46 @@ impl CadCommand for AngularDimensionCommand {
|
|||
}
|
||||
"M" | "MTEXT" => {
|
||||
self.mtext_override = true;
|
||||
self.awaiting_text = true;
|
||||
Some(CmdResult::NeedPoint)
|
||||
Some(CmdResult::SuspendForMTextInput {
|
||||
pos: self.editor_anchor(),
|
||||
initial: self.text_override.clone().unwrap_or_default(),
|
||||
height: 2.5,
|
||||
})
|
||||
}
|
||||
"A" | "ANGLE" => {
|
||||
self.awaiting_angle = true;
|
||||
self.angle_origin = None;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"Q" | "QUADRANT" => {
|
||||
self.awaiting_quadrant = true;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"Q" | "QUADRANT" => 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 {
|
||||
self.selecting_object
|
||||
}
|
||||
|
||||
fn entity_pick_accepts_points(&self) -> bool {
|
||||
self.selecting_object && matches!(self.step, Step::Vertex)
|
||||
}
|
||||
|
||||
fn entity_pick_highlights_hover(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
|
@ -352,6 +592,10 @@ impl CadCommand for AngularDimensionCommand {
|
|||
|
||||
fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult {
|
||||
if handle.is_null() {
|
||||
if matches!(self.step, Step::Vertex) {
|
||||
self.selecting_object = false;
|
||||
self.step = Step::FirstRay(point);
|
||||
}
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
let Some(entity) = self.picked_entity.take() else {
|
||||
|
|
@ -360,13 +604,42 @@ impl CadCommand for AngularDimensionCommand {
|
|||
let Some(curve) = picked_curve(&entity, point) else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
if !curve_is_coplanar(&curve, self.plane) {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
|
||||
match (&self.step, curve) {
|
||||
(
|
||||
Step::SecondLine { first_start, first_end, first_source },
|
||||
PickedCurve::Line(second_start, second_end),
|
||||
) if *first_source != handle => {
|
||||
self.source_handles = vec![*first_source, *first_source, handle, handle];
|
||||
Step::SecondLine {
|
||||
first_start,
|
||||
first_end,
|
||||
first_source,
|
||||
first_refs,
|
||||
},
|
||||
PickedCurve::Line {
|
||||
start: second_start,
|
||||
end: second_end,
|
||||
refs: second_refs,
|
||||
..
|
||||
},
|
||||
) => {
|
||||
if (*first_source == handle && *first_refs == second_refs)
|
||||
|| lines_parallel_in_plane(
|
||||
self.plane,
|
||||
*first_start,
|
||||
*first_end,
|
||||
second_start,
|
||||
second_end,
|
||||
)
|
||||
{
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
self.source_refs = vec![
|
||||
Some(first_refs[0].bind(*first_source)),
|
||||
Some(first_refs[1].bind(*first_source)),
|
||||
Some(second_refs[0].bind(handle)),
|
||||
Some(second_refs[1].bind(handle)),
|
||||
];
|
||||
self.selecting_object = false;
|
||||
self.step = Step::ArcPoint2 {
|
||||
first_start: *first_start,
|
||||
|
|
@ -376,29 +649,49 @@ impl CadCommand for AngularDimensionCommand {
|
|||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
(Step::Vertex, PickedCurve::Line(first_start, first_end)) => {
|
||||
(
|
||||
Step::Vertex,
|
||||
PickedCurve::Line {
|
||||
start: first_start,
|
||||
end: first_end,
|
||||
refs,
|
||||
..
|
||||
},
|
||||
) => {
|
||||
self.step = Step::SecondLine {
|
||||
first_start,
|
||||
first_end,
|
||||
first_source: handle,
|
||||
first_refs: refs,
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
(Step::Vertex, PickedCurve::Arc { center, first, second }) => {
|
||||
self.source_handles = vec![handle, handle, handle];
|
||||
(
|
||||
Step::Vertex,
|
||||
PickedCurve::Arc {
|
||||
center,
|
||||
first,
|
||||
second,
|
||||
refs,
|
||||
..
|
||||
},
|
||||
) => {
|
||||
self.source_refs = refs
|
||||
.into_iter()
|
||||
.map(|reference| Some(reference.bind(handle)))
|
||||
.collect();
|
||||
self.selecting_object = false;
|
||||
self.step = Step::ArcPoint3 { vertex: center, first, second };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
(Step::Vertex, PickedCurve::Circle { center, radius }) => {
|
||||
let Some(first) = project_to_radius(center, point, radius) else {
|
||||
(Step::Vertex, PickedCurve::Circle { center, .. }) => {
|
||||
if point.distance_squared(center) <= 1.0e-24 {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
}
|
||||
self.selecting_object = false;
|
||||
self.step = Step::CircleSecondRay {
|
||||
vertex: center,
|
||||
first,
|
||||
radius,
|
||||
first: point,
|
||||
source: handle,
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
|
|
@ -408,29 +701,63 @@ impl CadCommand for AngularDimensionCommand {
|
|||
}
|
||||
|
||||
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
|
||||
if self.awaiting_angle {
|
||||
return self
|
||||
.angle_origin
|
||||
.map(|origin| preview_wire(vec![origin, point]));
|
||||
}
|
||||
let points = match self.step {
|
||||
Step::Vertex | Step::SecondLine { .. } => return None,
|
||||
Step::FirstRay(vertex) => vec![vertex, point],
|
||||
Step::SecondRay { vertex, first } => vec![vertex, first, nan(), vertex, point],
|
||||
Step::CircleSecondRay { vertex, first, radius, .. } => {
|
||||
let second = project_to_radius(vertex, point, radius).unwrap_or(point);
|
||||
vec![vertex, first, nan(), vertex, second]
|
||||
Step::CircleSecondRay { vertex, first, .. } => {
|
||||
vec![vertex, first, nan(), vertex, point]
|
||||
}
|
||||
Step::ArcPoint3 { vertex, first, second } => {
|
||||
let vertex = self.plane.to_local(vertex);
|
||||
let first = self.plane.to_local(first);
|
||||
let second = self.plane.to_local(second);
|
||||
let point = locked_arc_point(
|
||||
vertex,
|
||||
self.plane.to_local(point),
|
||||
self.quadrant_lock,
|
||||
);
|
||||
angular_preview(vertex, first, second, point)
|
||||
.into_iter()
|
||||
.map(|value| if value.is_nan() { value } else { self.plane.to_world(value) })
|
||||
.collect()
|
||||
}
|
||||
Step::ArcPoint2 {
|
||||
first_start,
|
||||
first_end,
|
||||
second_start,
|
||||
second_end,
|
||||
} => two_line_preview(
|
||||
first_start,
|
||||
first_end,
|
||||
second_start,
|
||||
second_end,
|
||||
point,
|
||||
),
|
||||
} => {
|
||||
let first_start = self.plane.to_local(first_start);
|
||||
let first_end = self.plane.to_local(first_end);
|
||||
let second_start = self.plane.to_local(second_start);
|
||||
let second_end = self.plane.to_local(second_end);
|
||||
let point = self.plane.to_local(point);
|
||||
let point = two_line_frame(
|
||||
first_start,
|
||||
first_end,
|
||||
second_start,
|
||||
second_end,
|
||||
point,
|
||||
)
|
||||
.map(|(vertex, _, _)| locked_arc_point(vertex, point, self.quadrant_lock))
|
||||
.unwrap_or(point);
|
||||
two_line_preview(
|
||||
first_start,
|
||||
first_end,
|
||||
second_start,
|
||||
second_end,
|
||||
point,
|
||||
)
|
||||
.into_iter()
|
||||
.map(|value| if value.is_nan() { value } else { self.plane.to_world(value) })
|
||||
.collect()
|
||||
}
|
||||
};
|
||||
Some(preview_wire(points))
|
||||
}
|
||||
|
|
@ -438,24 +765,43 @@ impl CadCommand for AngularDimensionCommand {
|
|||
|
||||
fn picked_curve(entity: &EntityType, click: DVec3) -> Option<PickedCurve> {
|
||||
let point = |value: Vector3| DVec3::new(value.x, value.y, value.z);
|
||||
let normal = |value: Vector3| DVec3::new(value.x, value.y, value.z);
|
||||
match entity {
|
||||
EntityType::Line(line) => Some(PickedCurve::Line(point(line.start), point(line.end))),
|
||||
EntityType::Line(line) => {
|
||||
let start = point(line.start);
|
||||
let end = point(line.end);
|
||||
(start.distance_squared(end) > 1.0e-24).then_some(PickedCurve::Line {
|
||||
start,
|
||||
end,
|
||||
refs: [SourceLocator::INFERRED; 2],
|
||||
normal: None,
|
||||
})
|
||||
}
|
||||
EntityType::Arc(arc) => Some(PickedCurve::Arc {
|
||||
center: point(arc.center_wcs()),
|
||||
first: point(arc.start_point_wcs()),
|
||||
second: point(arc.end_point_wcs()),
|
||||
refs: [SourceLocator::INFERRED; 3],
|
||||
normal: normal(arc.normal),
|
||||
}),
|
||||
EntityType::Circle(circle) => Some(PickedCurve::Circle {
|
||||
center: point(circle.center_wcs()),
|
||||
radius: circle.radius,
|
||||
normal: normal(circle.normal),
|
||||
}),
|
||||
EntityType::LwPolyline(polyline) => {
|
||||
let click = crate::scene::view::transform::wcs_point_to_ocs(
|
||||
(click.x, click.y, click.z),
|
||||
(polyline.normal.x, polyline.normal.y, polyline.normal.z),
|
||||
);
|
||||
let points: Vec<_> = polyline
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|vertex| [vertex.location.x, vertex.location.y])
|
||||
.collect();
|
||||
let bulges: Vec<_> = polyline.vertices.iter().map(|vertex| vertex.bulge).collect();
|
||||
let segment = nearest_segment_index(
|
||||
polyline.vertices.iter().map(|vertex| [vertex.location.x, vertex.location.y]).collect(),
|
||||
&points,
|
||||
&bulges,
|
||||
polyline.is_closed,
|
||||
[click.0, click.1],
|
||||
)?;
|
||||
|
|
@ -467,17 +813,24 @@ fn picked_curve(entity: &EntityType, click: DVec3) -> Option<PickedCurve> {
|
|||
first.bulge,
|
||||
polyline.elevation,
|
||||
polyline.normal,
|
||||
segment,
|
||||
polyline.vertices.len(),
|
||||
)
|
||||
}
|
||||
EntityType::Polyline2D(polyline) => {
|
||||
let vertices = crate::entities::polyline::drawn_vertices2d(polyline)
|
||||
.unwrap_or_else(|| polyline.vertices.clone());
|
||||
let vertices = &polyline.vertices;
|
||||
let click = crate::scene::view::transform::wcs_point_to_ocs(
|
||||
(click.x, click.y, click.z),
|
||||
(polyline.normal.x, polyline.normal.y, polyline.normal.z),
|
||||
);
|
||||
let points: Vec<_> = vertices
|
||||
.iter()
|
||||
.map(|vertex| [vertex.location.x, vertex.location.y])
|
||||
.collect();
|
||||
let bulges: Vec<_> = vertices.iter().map(|vertex| vertex.bulge).collect();
|
||||
let segment = nearest_segment_index(
|
||||
vertices.iter().map(|vertex| [vertex.location.x, vertex.location.y]).collect(),
|
||||
&points,
|
||||
&bulges,
|
||||
polyline.is_closed(),
|
||||
[click.0, click.1],
|
||||
)?;
|
||||
|
|
@ -489,27 +842,86 @@ fn picked_curve(entity: &EntityType, click: DVec3) -> Option<PickedCurve> {
|
|||
first.bulge,
|
||||
polyline.elevation,
|
||||
polyline.normal,
|
||||
segment,
|
||||
vertices.len(),
|
||||
)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn nearest_segment_index(points: Vec<[f64; 2]>, closed: bool, click: [f64; 2]) -> Option<usize> {
|
||||
fn nearest_segment_index(
|
||||
points: &[[f64; 2]],
|
||||
bulges: &[f64],
|
||||
closed: bool,
|
||||
click: [f64; 2],
|
||||
) -> Option<usize> {
|
||||
if points.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let count = if closed { points.len() } else { points.len() - 1 };
|
||||
(0..count).min_by(|first, second| {
|
||||
segment_distance_squared(points[*first], points[(*first + 1) % points.len()], click)
|
||||
.total_cmp(&segment_distance_squared(
|
||||
polyline_segment_distance_squared(
|
||||
points[*first],
|
||||
points[(*first + 1) % points.len()],
|
||||
bulges.get(*first).copied().unwrap_or(0.0),
|
||||
click,
|
||||
)
|
||||
.total_cmp(&polyline_segment_distance_squared(
|
||||
points[*second],
|
||||
points[(*second + 1) % points.len()],
|
||||
bulges.get(*second).copied().unwrap_or(0.0),
|
||||
click,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn bulge_center(first: [f64; 2], second: [f64; 2], bulge: f64) -> Option<[f64; 2]> {
|
||||
if bulge.abs() <= 1.0e-12 {
|
||||
return None;
|
||||
}
|
||||
let dx = second[0] - first[0];
|
||||
let dy = second[1] - first[1];
|
||||
let chord = dx.hypot(dy);
|
||||
if chord <= 1.0e-12 {
|
||||
return None;
|
||||
}
|
||||
let midpoint = [(first[0] + second[0]) * 0.5, (first[1] + second[1]) * 0.5];
|
||||
let offset = chord * (1.0 - bulge * bulge) / (4.0 * bulge);
|
||||
Some([
|
||||
midpoint[0] - dy / chord * offset,
|
||||
midpoint[1] + dx / chord * offset,
|
||||
])
|
||||
}
|
||||
|
||||
fn polyline_segment_distance_squared(
|
||||
first: [f64; 2],
|
||||
second: [f64; 2],
|
||||
bulge: f64,
|
||||
point: [f64; 2],
|
||||
) -> f64 {
|
||||
let Some(center) = bulge_center(first, second, bulge) else {
|
||||
return segment_distance_squared(first, second, point);
|
||||
};
|
||||
let radius = (first[0] - center[0]).hypot(first[1] - center[1]);
|
||||
let start = (first[1] - center[1]).atan2(first[0] - center[0]);
|
||||
let target = (point[1] - center[1]).atan2(point[0] - center[0]);
|
||||
let sweep = 4.0 * bulge.atan();
|
||||
let into = if sweep >= 0.0 {
|
||||
(target - start).rem_euclid(std::f64::consts::TAU)
|
||||
} else {
|
||||
(start - target).rem_euclid(std::f64::consts::TAU)
|
||||
};
|
||||
if into <= sweep.abs() + 1.0e-12 {
|
||||
let radial = (point[0] - center[0]).hypot(point[1] - center[1]);
|
||||
(radial - radius).powi(2)
|
||||
} else {
|
||||
((point[0] - first[0]).powi(2) + (point[1] - first[1]).powi(2)).min(
|
||||
(point[0] - second[0]).powi(2) + (point[1] - second[1]).powi(2),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn segment_distance_squared(first: [f64; 2], second: [f64; 2], point: [f64; 2]) -> f64 {
|
||||
let dx = second[0] - first[0];
|
||||
let dy = second[1] - first[1];
|
||||
|
|
@ -530,6 +942,8 @@ fn polyline_segment(
|
|||
bulge: f64,
|
||||
elevation: f64,
|
||||
normal: Vector3,
|
||||
segment: usize,
|
||||
vertex_count: usize,
|
||||
) -> Option<PickedCurve> {
|
||||
let to_world = |point: [f64; 2]| {
|
||||
let value = crate::scene::view::transform::ocs_point_to_wcs(
|
||||
|
|
@ -540,31 +954,95 @@ fn polyline_segment(
|
|||
};
|
||||
let first_world = to_world(first);
|
||||
let second_world = to_world(second);
|
||||
if bulge.abs() <= 1.0e-12 {
|
||||
return Some(PickedCurve::Line(first_world, second_world));
|
||||
}
|
||||
let dx = second[0] - first[0];
|
||||
let dy = second[1] - first[1];
|
||||
let chord = (dx * dx + dy * dy).sqrt();
|
||||
if chord <= 1.0e-12 {
|
||||
if first_world.distance_squared(second_world) <= 1.0e-24 {
|
||||
return None;
|
||||
}
|
||||
let midpoint = [(first[0] + second[0]) * 0.5, (first[1] + second[1]) * 0.5];
|
||||
let center_offset = chord * (1.0 - bulge * bulge) / (4.0 * bulge);
|
||||
let center = [
|
||||
midpoint[0] - dy / chord * center_offset,
|
||||
midpoint[1] + dx / chord * center_offset,
|
||||
let refs = [
|
||||
SourceLocator::explicit(segment as i32, 0.0),
|
||||
SourceLocator::explicit(((segment + 1) % vertex_count) as i32, 0.0),
|
||||
];
|
||||
let normal_world = DVec3::new(normal.x, normal.y, normal.z);
|
||||
if bulge.abs() <= 1.0e-12 {
|
||||
return Some(PickedCurve::Line {
|
||||
start: first_world,
|
||||
end: second_world,
|
||||
refs,
|
||||
normal: Some(normal_world),
|
||||
});
|
||||
}
|
||||
let center = bulge_center(first, second, bulge)?;
|
||||
Some(PickedCurve::Arc {
|
||||
center: to_world(center),
|
||||
first: first_world,
|
||||
second: second_world,
|
||||
refs: [
|
||||
SourceLocator::explicit(-4, segment as f64),
|
||||
refs[0],
|
||||
refs[1],
|
||||
],
|
||||
normal: normal_world,
|
||||
})
|
||||
}
|
||||
|
||||
fn project_to_radius(center: DVec3, point: DVec3, radius: f64) -> Option<DVec3> {
|
||||
let direction = (point - center).normalize_or_zero();
|
||||
(direction.length_squared() > 0.0).then_some(center + direction * radius)
|
||||
fn curve_is_coplanar(curve: &PickedCurve, plane: WorkingPlane) -> bool {
|
||||
let (points, normal): (Vec<DVec3>, Option<DVec3>) = match curve {
|
||||
PickedCurve::Line { start, end, normal, .. } => (vec![*start, *end], *normal),
|
||||
PickedCurve::Arc { center, first, second, normal, .. } => {
|
||||
(vec![*center, *first, *second], Some(*normal))
|
||||
}
|
||||
PickedCurve::Circle { center, normal } => (vec![*center], Some(*normal)),
|
||||
};
|
||||
if let Some(normal) = normal {
|
||||
let alignment = normal.normalize_or_zero().dot(plane.z).abs();
|
||||
if alignment < 1.0 - 1.0e-7 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
let local: Vec<_> = points.into_iter().map(|point| plane.to_local(point)).collect();
|
||||
let min_z = local.iter().map(|point| point.z).fold(f64::INFINITY, f64::min);
|
||||
let max_z = local.iter().map(|point| point.z).fold(f64::NEG_INFINITY, f64::max);
|
||||
let scale = local
|
||||
.iter()
|
||||
.map(|point| point.x.abs().max(point.y.abs()).max(1.0))
|
||||
.fold(1.0, f64::max);
|
||||
max_z - min_z <= 1.0e-9 * scale
|
||||
}
|
||||
|
||||
fn lines_parallel_in_plane(
|
||||
plane: WorkingPlane,
|
||||
first_start: DVec3,
|
||||
first_end: DVec3,
|
||||
second_start: DVec3,
|
||||
second_end: DVec3,
|
||||
) -> bool {
|
||||
let first = plane.vector_to_local(first_end - first_start);
|
||||
let second = plane.vector_to_local(second_end - second_start);
|
||||
let denominator = first.x * second.y - first.y * second.x;
|
||||
denominator.abs() <= 1.0e-12 * first.length().max(second.length()).max(1.0)
|
||||
}
|
||||
|
||||
fn locked_arc_point(
|
||||
vertex: DVec3,
|
||||
point: DVec3,
|
||||
frame: Option<(f64, f64)>,
|
||||
) -> DVec3 {
|
||||
let Some((start, end)) = frame else { return point };
|
||||
let radius = ((point.x - vertex.x).powi(2) + (point.y - vertex.y).powi(2)).sqrt();
|
||||
if radius <= 1.0e-12 {
|
||||
return point;
|
||||
}
|
||||
let angle = (start + end) * 0.5;
|
||||
DVec3::new(
|
||||
vertex.x + angle.cos() * radius,
|
||||
vertex.y + angle.sin() * radius,
|
||||
vertex.z,
|
||||
)
|
||||
}
|
||||
|
||||
fn point_angle_in_frame(vertex: DVec3, point: DVec3, frame: (f64, f64)) -> bool {
|
||||
let angle = (point.y - vertex.y).atan2(point.x - vertex.x);
|
||||
let sweep = frame.1 - frame.0;
|
||||
(angle - frame.0).rem_euclid(std::f64::consts::TAU) <= sweep + 1.0e-9
|
||||
}
|
||||
|
||||
fn two_line_frame(
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ use acadrust::objects::{
|
|||
use acadrust::types::{Handle, Vector3};
|
||||
use acadrust::EntityType;
|
||||
|
||||
use crate::command::DimensionAssociationSource;
|
||||
|
||||
use super::{ChangeKind, Scene};
|
||||
|
||||
fn point_distance_squared(first: Vector3, second: Vector3) -> f64 {
|
||||
|
|
@ -23,6 +25,62 @@ fn ocs_point(x: f64, y: f64, elevation: f64, normal: Vector3) -> Vector3 {
|
|||
Vector3::new(point.0, point.1, point.2)
|
||||
}
|
||||
|
||||
fn bulge_center_world(
|
||||
first: [f64; 2],
|
||||
second: [f64; 2],
|
||||
bulge: f64,
|
||||
elevation: f64,
|
||||
normal: Vector3,
|
||||
) -> Option<Vector3> {
|
||||
if bulge.abs() <= 1.0e-12 {
|
||||
return None;
|
||||
}
|
||||
let dx = second[0] - first[0];
|
||||
let dy = second[1] - first[1];
|
||||
let chord = dx.hypot(dy);
|
||||
if chord <= 1.0e-12 {
|
||||
return None;
|
||||
}
|
||||
let midpoint = [(first[0] + second[0]) * 0.5, (first[1] + second[1]) * 0.5];
|
||||
let offset = chord * (1.0 - bulge * bulge) / (4.0 * bulge);
|
||||
Some(ocs_point(
|
||||
midpoint[0] - dy / chord * offset,
|
||||
midpoint[1] + dx / chord * offset,
|
||||
elevation,
|
||||
normal,
|
||||
))
|
||||
}
|
||||
|
||||
fn polyline_arc_center(entity: &EntityType, segment: usize) -> Option<Vector3> {
|
||||
match entity {
|
||||
EntityType::LwPolyline(polyline) => {
|
||||
let count = polyline.vertices.len();
|
||||
let first = *polyline.vertices.get(segment)?;
|
||||
let second = *polyline.vertices.get((segment + 1) % count)?;
|
||||
bulge_center_world(
|
||||
[first.location.x, first.location.y],
|
||||
[second.location.x, second.location.y],
|
||||
first.bulge,
|
||||
polyline.elevation,
|
||||
polyline.normal,
|
||||
)
|
||||
}
|
||||
EntityType::Polyline2D(polyline) => {
|
||||
let count = polyline.vertices.len();
|
||||
let first = polyline.vertices.get(segment)?;
|
||||
let second = polyline.vertices.get((segment + 1) % count)?;
|
||||
bulge_center_world(
|
||||
[first.location.x, first.location.y],
|
||||
[second.location.x, second.location.y],
|
||||
first.bulge,
|
||||
polyline.elevation,
|
||||
polyline.normal,
|
||||
)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn source_points(entity: &EntityType) -> Vec<Vector3> {
|
||||
match entity {
|
||||
EntityType::Line(line) => vec![line.start, line.end],
|
||||
|
|
@ -101,6 +159,10 @@ fn source_marker(entity: &EntityType, point: Vector3) -> Option<i32> {
|
|||
fn resolve_reference(scene: &Scene, reference: &AssocDimensionReference) -> Option<Vector3> {
|
||||
let source = *reference.xrefs.first()?;
|
||||
let entity = scene.document.get_entity(source)?;
|
||||
if reference.main_gs_marker == -4 {
|
||||
let segment = reference.osnap_distance.round().max(0.0) as usize;
|
||||
return polyline_arc_center(entity, segment);
|
||||
}
|
||||
if reference.main_gs_marker == -3 {
|
||||
return match entity {
|
||||
EntityType::Circle(circle) => Some(circle.center_wcs()),
|
||||
|
|
@ -167,6 +229,20 @@ impl Scene {
|
|||
&mut self,
|
||||
dimension: Handle,
|
||||
sources: Vec<Option<Handle>>,
|
||||
) {
|
||||
self.attach_dimension_association_sources(
|
||||
dimension,
|
||||
sources
|
||||
.into_iter()
|
||||
.map(|source| source.map(DimensionAssociationSource::inferred))
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn attach_dimension_association_sources(
|
||||
&mut self,
|
||||
dimension: Handle,
|
||||
sources: Vec<Option<DimensionAssociationSource>>,
|
||||
) {
|
||||
let Some(EntityType::Dimension(dimension_entity)) =
|
||||
self.document.get_entity(dimension)
|
||||
|
|
@ -182,9 +258,14 @@ impl Scene {
|
|||
.enumerate()
|
||||
.map(|(index, point)| {
|
||||
let source = sources.get(index).copied().flatten()?;
|
||||
let entity = self.document.get_entity(source)?;
|
||||
source_reference(entity, *point)
|
||||
.map(|(marker, parameter)| (source, marker, parameter))
|
||||
let (marker, parameter) = match source.marker {
|
||||
Some(marker) => (marker, source.parameter),
|
||||
None => {
|
||||
let entity = self.document.get_entity(source.handle)?;
|
||||
source_reference(entity, *point)?
|
||||
}
|
||||
};
|
||||
Some((source.handle, marker, parameter))
|
||||
})
|
||||
.collect();
|
||||
if resolved.iter().all(Option::is_none) {
|
||||
|
|
@ -229,7 +310,7 @@ impl Scene {
|
|||
.insert(association_handle, ObjectType::Associative(object));
|
||||
|
||||
let mut reactor_targets = vec![dimension];
|
||||
reactor_targets.extend(sources.into_iter().flatten());
|
||||
reactor_targets.extend(sources.into_iter().flatten().map(|source| source.handle));
|
||||
reactor_targets.sort_by_key(|handle| handle.value());
|
||||
reactor_targets.dedup();
|
||||
for handle in reactor_targets {
|
||||
|
|
|
|||
Loading…
Reference in a new issue