diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 75493c3dd..e147c5245 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -1517,8 +1517,12 @@ impl OpenCADStudio { Some(crate::scene::named_parameters::DrivingValue::Named(_)) ) }) - .filter_map(|c| c.refs.first().copied()) - .filter(|reference| reference.marker.is_some()) + .flat_map(|c| { + crate::scene::parametric_constraints::dimensional_anchor_refs( + &self.tabs[i].scene.document, + &c.refs, + ) + }) .collect(); let pending = self.begin_undo(i, "Apply named parameters", touched.len(), true); self.tabs[i].scene.record_undo_named_parameters_before(); @@ -1580,7 +1584,6 @@ impl OpenCADStudio { &c.refs, ) }) - .filter(|reference| reference.marker.is_some()) .collect(); self.tabs[i].dirty = true; if touched.is_empty() { @@ -4039,6 +4042,7 @@ impl OpenCADStudio { direction, name, expression, + renamed, label, } => { use crate::modules::annotate::{aligned_dim, linear_dim}; @@ -4094,7 +4098,14 @@ impl OpenCADStudio { ); let name = name.trim().to_string(); if !is_valid_name(&name) { - self.reprompt_active_command(i, "Invalid parameter name."); + self.command_line.push_error( + "Only alphanumeric names, starting with alpha characters, are allowed.", + ); + self.reprompt_active_command(i, &format!("Invalid parameter name: {name}.")); + return Task::none(); + } + if renamed && self.tabs[i].scene.named_parameters().contains(&name) { + self.reprompt_active_command(i, "Parameter with this name already exists."); return Task::none(); } let mut table = self.tabs[i].scene.named_parameters().clone(); @@ -4123,6 +4134,7 @@ impl OpenCADStudio { false, Some(&expression), annotational, + None, ); let mut entity = if kind == ConstraintKind::Distance { aligned_dim::aligned_dimension_entity( @@ -4222,6 +4234,206 @@ impl OpenCADStudio { self.commit_undo_delta(i, pd); } } + CmdResult::CancelWithMessage(message) => { + self.command_line.push_error(&message); + return self.apply_cmd_result(CmdResult::Cancel); + } + CmdResult::AddAngularConstraint { + refs, + points, + location, + sector, + name, + expression, + renamed, + } => { + use crate::modules::annotate::angular_dim; + use crate::scene::named_parameters::{is_valid_name, DrivingValue}; + use crate::scene::parametric_constraints::{ + angle_decimals, dynamic_dimension_text, ConstraintKind, ParametricRef, + }; + + let scope = self.tabs[i].current_parametric_scope(); + let kind = if refs.len() == 2 { + ConstraintKind::Angle + } else { + ConstraintKind::Angle3Point + }; + // The same angle between the same sides is refused after the + // value (whichever sector), and the command ends. + let duplicate = self.tabs[i] + .scene + .parametric_constraint_set(scope) + .is_some_and(|set| { + set.constraints.iter().any(|c| { + c.enabled + && c.kind == kind + && c.refs.len() == refs.len() + && if kind == ConstraintKind::Angle { + c.refs.contains(&refs[0]) && c.refs.contains(&refs[1]) + } else { + c.refs == refs + } + }) + }); + if duplicate { + self.command_line + .push_output("The constraint already exists on the selected objects."); + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + return Task::none(); + } + if let Err(message) = self.tabs[i].scene.validate_parametric_constraint( + kind, + &refs, + Some(&DrivingValue::Literal(1.0)), + ) { + self.reprompt_active_command(i, message); + return Task::none(); + } + let name = name.trim().to_string(); + if !is_valid_name(&name) { + self.command_line.push_error( + "Only alphanumeric names, starting with alpha characters, are allowed.", + ); + self.reprompt_active_command(i, &format!("Invalid parameter name: {name}.")); + return Task::none(); + } + if renamed && self.tabs[i].scene.named_parameters().contains(&name) { + self.reprompt_active_command(i, "Parameter with this name already exists."); + return Task::none(); + } + let mut table = self.tabs[i].scene.named_parameters().clone(); + if let Err(error) = table.set(&name, &expression) { + self.reprompt_active_command(i, &error.to_string()); + return Task::none(); + } + let value = match table.resolve(&name) { + Ok(value) if value.is_finite() => value, + _ => { + self.command_line.push_error("Invalid expression."); + self.reprompt_active_command( + i, + "The parameter is used in an expression which results in an invalid value for a dimensional constraint.", + ); + return Task::none(); + } + }; + let annotational = self.constraint_form_annotational; + let decimals = angle_decimals(&self.tabs[i].scene.document, None); + let text = dynamic_dimension_text( + &name, + value, + self.tabs[i].scene.constraint_name_format, + false, + Some(&expression), + annotational, + Some(decimals), + ); + let entity = if kind == ConstraintKind::Angle { + angular_dim::angular_two_line_entity( + points[0], points[1], points[2], points[3], location, Some(text), + ) + } else { + angular_dim::angular_three_point_entity( + points[0], points[1], points[2], location, Some(text), + ) + }; + let Some(mut entity) = entity else { + self.reprompt_active_command(i, "No valid constraint point found."); + return Task::none(); + }; + crate::scene::creation_style::apply_current_creation_styles( + &self.tabs[i].scene.document, + &mut entity, + ); + if annotational { + entity + .as_entity_mut() + .set_layer(self.tabs[i].active_layer.clone()); + } else { + self.tabs[i].scene.ensure_dynamic_dimension_layer(); + entity.as_entity_mut().set_layer( + crate::scene::parametric_constraints::DYNAMIC_DIMENSION_LAYER.to_string(), + ); + entity.common_mut().color = acadrust::types::Color::Rgb { + r: 103, + g: 109, + b: 118, + }; + } + let constraints_before = self.tabs[i] + .scene + .parametric_constraint_set(scope) + .cloned() + .unwrap_or_else(|| { + crate::scene::parametric_constraints::ParametricConstraintSet::new(scope) + }); + let mut touched: Vec = Vec::new(); + for reference in &refs { + if !touched.contains(&reference.entity) { + touched.push(reference.entity); + } + } + // The first side (or the vertex and first point) stays; a + // value other than the measured one turns the other side. + let anchors: Vec = + crate::scene::parametric_constraints::dimensional_anchor_refs( + &self.tabs[i].scene.document, + &refs, + ); + let association = if kind == ConstraintKind::Angle { + vec![ + Some(refs[0].entity), + Some(refs[0].entity), + Some(refs[1].entity), + Some(refs[1].entity), + ] + } else { + vec![ + Some(refs[1].entity), + Some(refs[0].entity), + Some(refs[2].entity), + ] + }; + let pending = self.begin_undo(i, "Angular constraint", touched.len() + 1, false); + self.tabs[i] + .scene + .record_undo_parametric_constraints_before(scope, constraints_before); + self.tabs[i].scene.record_undo_named_parameters_before(); + self.tabs[i].scene.named_parameters = table; + let dimension = self.tabs[i].scene.add_entity(entity); + let set = self.tabs[i].scene.parametric_constraint_set_mut(scope); + let id = set.add(kind, refs, Some(DrivingValue::Named(name))); + if let Some(constraint) = set.constraints.iter_mut().find(|c| c.id == id) { + constraint.angle_sector = sector; + } + set.dimensions.insert(id, dimension); + self.tabs[i].scene.note_parametric_constraint_applied( + scope, + id, + self.constraint_bar_display, + ); + self.tabs[i] + .scene + .attach_dimension_association(dimension, association); + let changes: Vec<(Handle, crate::scene::ChangeKind)> = touched + .into_iter() + .map(|handle| (handle, crate::scene::ChangeKind::Modified)) + .collect(); + self.tabs[i] + .scene + .bump_entities_with_parametric_policy(&changes, &anchors, false); + self.tabs[i].scene.refresh_hidden_dynamic_dimensions(); + self.tabs[i].scene.refresh_dynamic_dimension_scales(true); + self.tabs[i].dirty = true; + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.refresh_properties(); + if let Some(pd) = pending { + self.commit_undo_delta(i, pd); + } + } CmdResult::MakeParallel { first_line, first_ends, diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index c51425fc7..26344dbad 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -1569,10 +1569,8 @@ impl OpenCADStudio { table .iter() .map(|parameter| { - let value = table - .resolve(¶meter.name) - .map(|value| format!("{value:.4}")) - .unwrap_or_else(|_| "**".to_string()); + let value = + self.tabs[i].scene.parameter_value_text(¶meter.name); format!( "Parameter: {:<12} Expression: {:<21} Value: {value}", parameter.name, parameter.source @@ -1597,10 +1595,7 @@ impl OpenCADStudio { let old = { let table = self.tabs[i].scene.named_parameters(); table.get(&name).map(|parameter| { - let value = table - .resolve(&name) - .map(|value| format!("{value:.4}")) - .unwrap_or_else(|_| "**".to_string()); + let value = self.tabs[i].scene.parameter_value_text(&name); format!("Old Expression = {}, Value = {value}", parameter.source) }) }; @@ -1941,41 +1936,19 @@ impl OpenCADStudio { } "ACONSTRAINT" | "DCANGULAR" => { - if cmd == "DCANGULAR" { - self.dim_constraint_last = "ANgular"; - } - let handles = self.tabs[i].scene.selected_handles_in_order(); - if handles.is_empty() { - use crate::modules::draw::select::SelectObjectsCommand; - let sel = SelectObjectsCommand::new(cmd); - self.command_line.push_info(&sel.prompt()); - self.tabs[i].active_cmd = Some(Box::new(sel)); - } else if handles.len() != 2 { - self.command_line.push_output( - "Select exactly two lines (first = reference, second = the one that rotates), then run this constraint again.", - ); - } else { - use crate::modules::parametric::AngleConstraintCommand; - let command_name = if cmd == "DCANGULAR" { - "DCANGULAR" - } else { - "ACONSTRAINT" - }; - match AngleConstraintCommand::with_name( - &self.tabs[i].scene, - handles[0], - handles[1], - command_name, - ) { - Some(new_cmd) => { - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - None => self - .command_line - .push_output("Select two lines for an angle constraint."), - } - } + use crate::modules::parametric::{DimConstraintAxis, DimConstraintCommand}; + use crate::scene::parametric_constraints::{ + angle_decimals, next_angular_parameter_name, + }; + self.dim_constraint_last = "ANgular"; + let name = next_angular_parameter_name(self.tabs[i].scene.named_parameters()); + let decimals = angle_decimals(&self.tabs[i].scene.document, None); + // Both sides are picked inside the command. + self.tabs[i].scene.deselect_all(); + let command = DimConstraintCommand::new(DimConstraintAxis::Angular, name) + .with_angle_decimals(decimals); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); } // ── Model commands (3D primitives) ───────────────────────────── diff --git a/src/command.rs b/src/command.rs index bcd1f968f..199017a5b 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1630,8 +1630,26 @@ pub enum CmdResult { direction: Option, name: String, expression: String, + /// The user named the parameter (`name=expression`); an existing + /// name is then refused. + renamed: bool, label: &'static str, }, + /// Adds an angular constraint with its dynamic angular dimension and + /// the `angN` parameter that drives it. Two lines: `refs` = + /// `[first_line, second_line]`, `points` = both lines' ends; three + /// points: `refs` = `[first, vertex, second]`, `points` = + /// `[vertex, first, second]`. + AddAngularConstraint { + refs: Vec, + points: Vec, + location: DVec3, + /// Which of the four angles the dimension line location picked. + sector: u8, + name: String, + expression: String, + renamed: bool, + }, /// Aligned's 2Lines: makes `second_line` parallel to `first_line` (whose /// ends stay put), then hands the second line's solved ends back through /// `CadCommand::accept_parallel_line`. @@ -1812,6 +1830,8 @@ pub enum CmdResult { ReportMeasurement(String), /// Print an input error and keep the command active. ReportError(String), + /// Reports an error and ends the command (`Lines are parallel.`). + CancelWithMessage(String), /// Print a measurement result, clear the current selection, and keep the command active. ReportMeasurementAndDeselect(String), /// Clear the current selection and keep the command active at its updated step. diff --git a/src/modules/annotate/angular_dim.rs b/src/modules/annotate/angular_dim.rs index 3df4b2d3d..36a4395ab 100644 --- a/src/modules/annotate/angular_dim.rs +++ b/src/modules/annotate/angular_dim.rs @@ -1093,7 +1093,7 @@ fn point_angle_in_frame(vertex: DVec3, point: DVec3, frame: (f64, f64)) -> bool (angle - frame.0).rem_euclid(std::f64::consts::TAU) <= sweep + 1.0e-9 } -fn two_line_frame( +pub(crate) fn two_line_frame( first_start: DVec3, first_end: DVec3, second_start: DVec3, @@ -1186,6 +1186,51 @@ fn angular_preview_with_frame( points } +/// A two-line angular dimension whose arc sits at `arc_point`, for a +/// dimensional constraint; `None` when the lines are parallel or the arc +/// point picks no sector. +pub(crate) fn angular_two_line_entity( + first_start: DVec3, + first_end: DVec3, + second_start: DVec3, + second_end: DVec3, + arc_point: DVec3, + text: Option, +) -> Option { + two_line_frame(first_start, first_end, second_start, second_end, arc_point)?; + let mut dim = DimensionAngular2Ln::default(); + dim.first_point = v3(first_start); + dim.second_point = v3(first_end); + dim.angle_vertex = v3(second_start); + dim.definition_point = v3(second_end); + dim.dimension_arc = v3(arc_point); + dim.base.definition_point = dim.definition_point; + dim.base.text_middle_point = dim.dimension_arc; + dim.base.insertion_point = dim.dimension_arc; + dim.base.actual_measurement = dim.measurement_degrees(); + crate::entities::dimension::set_dimension_text_override(&mut dim.base, text); + Some(EntityType::Dimension(Dimension::Angular2Ln(dim))) +} + +/// A three-point angular dimension (vertex, two points, arc point), for a +/// dimensional constraint. +pub(crate) fn angular_three_point_entity( + vertex: DVec3, + first: DVec3, + second: DVec3, + arc_point: DVec3, + text: Option, +) -> Option { + 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; + dim.base.actual_measurement = dim.measurement_degrees(); + crate::entities::dimension::set_dimension_text_override(&mut dim.base, text); + Some(EntityType::Dimension(Dimension::Angular3Pt(dim))) +} + fn v3(point: DVec3) -> Vector3 { Vector3::new(point.x, point.y, point.z) } diff --git a/src/modules/parametric/dim_constraint.rs b/src/modules/parametric/dim_constraint.rs index 746cff97f..63fc1df6b 100644 --- a/src/modules/parametric/dim_constraint.rs +++ b/src/modules/parametric/dim_constraint.rs @@ -14,6 +14,8 @@ pub enum DimConstraintAxis { Horizontal, Vertical, Aligned, + /// The angle between two lines, or at a vertex between two points. + Angular, } /// A picked line (a line entity or one polyline segment) an Aligned @@ -82,6 +84,28 @@ enum Step { /// The line the distance is measured perpendicular to. direction: Option, }, + /// Angular: `Select first line or arc or [3Point] <3Point>:` + AngularFirst, + /// `Select second line:` + AngularSecond { first: LineTarget }, + /// 3Point: `Specify angle vertex:` + AngularVertex, + /// `Specify first angle constraint point:` + AngularPoint1 { vertex: (ParametricRef, DVec3) }, + /// `Specify second angle constraint point:` + AngularPoint2 { + vertex: (ParametricRef, DVec3), + first: (ParametricRef, DVec3), + }, + /// `Specify dimension line location:` of an angle. + AngularLocation { data: AngularData }, + /// `Enter value or name and value :` + AngularValue { + data: AngularData, + location: DVec3, + sector: u8, + measured: f64, + }, /// `Enter value or name and value :` Value { first: ParametricRef, @@ -96,6 +120,21 @@ enum Step { }, } +/// Angular's picks: two lines, or a vertex and two points (an arc gives +/// its center and its ends). +#[derive(Clone, Copy)] +enum AngularData { + Lines { + first: LineTarget, + second: LineTarget, + }, + Points { + vertex: (ParametricRef, DVec3), + first: (ParametricRef, DVec3), + second: (ParametricRef, DVec3), + }, +} + /// The reference's Linear/Horizontal/Vertical/Aligned dimensional constraint: /// two constraint points (or one object's ends), a dimension line location, /// then the parameter name and expression the dynamic dimension carries. @@ -105,6 +144,8 @@ pub struct DimConstraintCommand { picked_entity: Option, /// The next free `dN` the host reserved for this constraint. default_name: String, + /// Angular precision (DIMADEC) for `Dimension text = 27`. + angle_decimals: usize, } impl DimConstraintCommand { @@ -113,22 +154,163 @@ impl DimConstraintCommand { pub const SAME_POINT: &'static str = "The object or point is already selected. Select a different object or constraint point."; pub const INVALID_LINE: &'static str = "Invalid selection for Aligned. Select a line segment, polyline segment, text, MText, major or minor axis of ellipse or elliptical arc."; + pub const PARALLEL_LINES: &'static str = "Lines are parallel."; pub fn new(axis: DimConstraintAxis, default_name: String) -> Self { Self { axis, - step: Step::First, + step: if axis == DimConstraintAxis::Angular { + Step::AngularFirst + } else { + Step::First + }, picked_entity: None, default_name, + angle_decimals: 0, } } + /// The angular precision the measured angle is reported with. + pub fn with_angle_decimals(mut self, decimals: usize) -> Self { + self.angle_decimals = decimals; + self + } + + fn angle_display(&self, degrees: f64) -> String { + let decimals = self.angle_decimals; + format!( + "{:.decimals$}", + crate::scene::parametric_constraints::normalize_angle_display(degrees) + ) + } + + /// An Angular pick: a line or a polyline segment (text baselines and + /// ellipse axes are not angle sides). + fn angular_line(entity: &EntityType, handle: Handle, point: DVec3) -> Option { + matches!( + entity, + EntityType::Line(_) | EntityType::LwPolyline(_) | EntityType::Polyline2D(_) + ) + .then(|| Self::line_target(entity, handle, point)) + .flatten() + } + + /// The angle (degrees) the dimension line location picks between the + /// directed sides `p1a→p1b` and `p2a→p2b`, and which of the four + /// sectors it is in the solver's terms (which signed sides bound it). + fn angle_frame( + p1a: DVec3, + p1b: DVec3, + p2a: DVec3, + p2b: DVec3, + location: DVec3, + ) -> Option<(f64, u8)> { + use crate::scene::parametric_constraints::angle_sector; + let tau = std::f64::consts::TAU; + let (_, start, end) = + crate::modules::annotate::angular_dim::two_line_frame(p1a, p1b, p2a, p2b, location)?; + let sweep = (end - start).rem_euclid(tau); + let same = |a: f64, b: f64| { + let d = (a - b).rem_euclid(tau); + d < 1.0e-6 || d > tau - 1.0e-6 + }; + let d1 = p1b - p1a; + let d2 = p2b - p2a; + let a1 = d1.y.atan2(d1.x); + let a2 = d2.y.atan2(d2.x); + let pi = std::f64::consts::PI; + let sector = if same(start, a1) || same(start, a1 + pi) { + // Swept from a side of the first line to a side of the second. + if same(start, a1) == same(end, a2) { + angle_sector::PARALLEL_COUNTERCLOCKWISE + } else { + angle_sector::ANTIPARALLEL_COUNTERCLOCKWISE + } + } else if same(start, a2) == same(end, a1) { + angle_sector::PARALLEL_CLOCKWISE + } else { + angle_sector::ANTIPARALLEL_CLOCKWISE + }; + Some((sweep.to_degrees(), sector)) + } + + /// The angle (degrees) the dimension line location picks at `vertex` + /// between the rays to `first` and `second`: the counterclockwise sweep + /// from the first ray when the location lies in it, else the other way + /// round (a semicircle measures 180, a reflex angle is allowed). + fn ray_frame(vertex: DVec3, first: DVec3, second: DVec3, location: DVec3) -> Option<(f64, u8)> { + use crate::scene::parametric_constraints::angle_sector; + let tau = std::f64::consts::TAU; + let r1 = first - vertex; + let r2 = second - vertex; + let at = location - vertex; + if r1.length_squared() < 1.0e-18 || r2.length_squared() < 1.0e-18 || at.length_squared() < 1.0e-18 { + return None; + } + let a1 = r1.y.atan2(r1.x); + let a2 = r2.y.atan2(r2.x); + let t = at.y.atan2(at.x); + let sweep = (a2 - a1).rem_euclid(tau); + if sweep < 1.0e-9 { + return None; + } + if (t - a1).rem_euclid(tau) <= sweep + 1.0e-9 { + Some((sweep.to_degrees(), angle_sector::PARALLEL_COUNTERCLOCKWISE)) + } else { + Some(((tau - sweep).to_degrees(), angle_sector::PARALLEL_CLOCKWISE)) + } + } + + fn build_angular(&self, name: String, expression: String) -> Option { + let Step::AngularValue { + data, + location, + sector, + .. + } = self.step + else { + return None; + }; + if expression.trim().is_empty() { + return None; + } + let (refs, points) = match data { + AngularData::Lines { first, second } => ( + vec![first.line, second.line], + vec![ + first.ends[0].1, + first.ends[1].1, + second.ends[0].1, + second.ends[1].1, + ], + ), + AngularData::Points { + vertex, + first, + second, + } => ( + vec![first.0, vertex.0, second.0], + vec![vertex.1, first.1, second.1], + ), + }; + Some(CmdResult::AddAngularConstraint { + refs, + points, + location, + sector, + renamed: name != self.default_name, + name, + expression, + }) + } + fn command_name(&self) -> &'static str { match self.axis { DimConstraintAxis::Linear => "DCLINEAR", DimConstraintAxis::Horizontal => "DCHORIZONTAL", DimConstraintAxis::Vertical => "DCVERTICAL", DimConstraintAxis::Aligned => "DCALIGNED", + DimConstraintAxis::Angular => "DCANGULAR", } } @@ -138,6 +320,7 @@ impl DimConstraintCommand { DimConstraintAxis::Horizontal => "Horizontal", DimConstraintAxis::Vertical => "Vertical", DimConstraintAxis::Aligned => "Aligned", + DimConstraintAxis::Angular => "Angular", } } @@ -291,7 +474,8 @@ impl DimConstraintCommand { } DimConstraintAxis::Horizontal => (ConstraintKind::DistanceX, DVec3::X), DimConstraintAxis::Vertical => (ConstraintKind::DistanceY, DVec3::Y), - DimConstraintAxis::Aligned => ( + // Angular never measures here; its own steps decide the sector. + DimConstraintAxis::Aligned | DimConstraintAxis::Angular => ( ConstraintKind::Distance, (second - first).normalize_or(DVec3::X), ), @@ -334,13 +518,14 @@ impl DimConstraintCommand { location, axis, direction: direction.map(|line| line.line), + renamed: name != self.default_name, name, expression, label: match self.axis { DimConstraintAxis::Linear => "Linear constraint", DimConstraintAxis::Horizontal => "Horizontal distance constraint", DimConstraintAxis::Vertical => "Vertical distance constraint", - DimConstraintAxis::Aligned => "Aligned constraint", + DimConstraintAxis::Aligned | DimConstraintAxis::Angular => "Aligned constraint", }, }) } @@ -371,7 +556,25 @@ impl CadCommand for DimConstraintCommand { Step::TwoLinesSecond { .. } | Step::TwoLinesParallel { .. } => { format!("{name} Select second line to make parallel:") } - Step::Location { .. } => format!("{name} Specify dimension line location:"), + Step::AngularFirst => { + format!("{name} Select first line or arc or [3Point] <3Point>:") + } + Step::AngularSecond { .. } => format!("{name} Select second line:"), + Step::AngularVertex => format!("{name} Specify angle vertex:"), + Step::AngularPoint1 { .. } => { + format!("{name} Specify first angle constraint point:") + } + Step::AngularPoint2 { .. } => { + format!("{name} Specify second angle constraint point:") + } + Step::AngularLocation { .. } | Step::Location { .. } => { + format!("{name} Specify dimension line location:") + } + Step::AngularValue { measured, .. } => format!( + "{name} Enter value or name and value <{}={}>:", + self.default_name, + measured_expression(measured) + ), Step::Value { measured, .. } => format!( "{name} Enter value or name and value <{}={}>:", self.default_name, @@ -388,6 +591,7 @@ impl CadCommand for DimConstraintCommand { CmdOption::new("2Lines", "2L"), ], Step::First => vec![CmdOption::new("Object", "O")], + Step::AngularFirst => vec![CmdOption::new("3Point", "3P")], Step::PointLinePoint => vec![CmdOption::new("Line", "L")], _ => Vec::new(), } @@ -400,7 +604,11 @@ impl CadCommand for DimConstraintCommand { fn point_step_accepts_keywords(&self) -> bool { matches!( self.step, - Step::First | Step::PointLinePoint | Step::Value { .. } + Step::First + | Step::PointLinePoint + | Step::Value { .. } + | Step::AngularFirst + | Step::AngularValue { .. } ) } @@ -427,13 +635,25 @@ impl CadCommand for DimConstraintCommand { } None } - Step::Value { .. } => { + Step::AngularFirst => { + let keyword = text.trim().trim_start_matches('_').to_ascii_uppercase(); + if matches!(keyword.as_str(), "3P" | "3POINT") { + self.step = Step::AngularVertex; + return Some(CmdResult::NeedPoint); + } + None + } + Step::Value { .. } | Step::AngularValue { .. } => { let text = text.trim(); let (name, expression) = match text.split_once('=') { Some((name, expression)) => (name.trim().to_string(), expression.trim().to_string()), None => (self.default_name.clone(), text.to_string()), }; - self.build(name, expression) + if matches!(self.step, Step::AngularValue { .. }) { + self.build_angular(name, expression) + } else { + self.build(name, expression) + } } _ => None, } @@ -451,6 +671,11 @@ impl CadCommand for DimConstraintCommand { | Step::LinePoint { .. } | Step::TwoLinesFirst | Step::TwoLinesSecond { .. } + | Step::AngularFirst + | Step::AngularSecond { .. } + | Step::AngularVertex + | Step::AngularPoint1 { .. } + | Step::AngularPoint2 { .. } ) } @@ -571,7 +796,81 @@ impl CadCommand for DimConstraintCommand { }; CmdResult::NeedPoint } - Step::Location { .. } | Step::Value { .. } => self.on_point(point), + Step::AngularFirst => { + if handle.is_null() { + return Self::report(Self::NO_OBJECT); + } + let Some(entity) = self.picked_entity.take() else { + return CmdResult::NeedPoint; + }; + if matches!(entity, EntityType::Arc(_)) { + // An arc's included angle: its center and its ends. + let center = ParametricRef::center(handle); + let start = ParametricRef::point(handle, 0); + let end = ParametricRef::point(handle, 1); + let (Some(c), Some(s), Some(e)) = ( + Self::world(&entity, center), + Self::world(&entity, start), + Self::world(&entity, end), + ) else { + return Self::report(Self::NO_POINT); + }; + self.step = Step::AngularLocation { + data: AngularData::Points { + vertex: (center, c), + first: (start, s), + second: (end, e), + }, + }; + return CmdResult::NeedPoint; + } + match Self::angular_line(&entity, handle, point) { + Some(line) => { + self.step = Step::AngularSecond { first: line }; + CmdResult::NeedPoint + } + None => Self::report(&format!( + "Invalid selection for {}. Select a line, polyline segment or arc.", + self.noun() + )), + } + } + Step::AngularSecond { first } => { + if handle.is_null() { + return Self::report(Self::NO_OBJECT); + } + let Some(entity) = self.picked_entity.take() else { + return CmdResult::NeedPoint; + }; + let Some(second) = Self::angular_line(&entity, handle, point) else { + return Self::report(&format!( + "Invalid selection for {}. Select a line, polyline segment or arc.", + self.noun() + )); + }; + if second.line == first.line { + return Self::report(Self::SAME_POINT); + } + let cross = first.dir.x * second.dir.y - first.dir.y * second.dir.x; + if cross.abs() < 1.0e-9 { + return CmdResult::CancelWithMessage(Self::PARALLEL_LINES.to_string()); + } + self.step = Step::AngularLocation { + data: AngularData::Lines { first, second }, + }; + CmdResult::NeedPoint + } + Step::AngularVertex | Step::AngularPoint1 { .. } | Step::AngularPoint2 { .. } => { + CmdResult::CheckConstraintPoint(CoincidentPick { + handle: (!handle.is_null()).then_some(handle), + point, + whole_curve: false, + }) + } + Step::Location { .. } + | Step::Value { .. } + | Step::AngularLocation { .. } + | Step::AngularValue { .. } => self.on_point(point), } } @@ -644,6 +943,35 @@ impl CadCommand for DimConstraintCommand { }; CmdResult::NeedPoint } + Step::AngularVertex => { + self.step = Step::AngularPoint1 { + vertex: (reference, point), + }; + CmdResult::NeedPoint + } + Step::AngularPoint1 { vertex } => { + if reference == vertex.0 { + return Self::report(Self::SAME_POINT); + } + self.step = Step::AngularPoint2 { + vertex, + first: (reference, point), + }; + CmdResult::NeedPoint + } + Step::AngularPoint2 { vertex, first } => { + if reference == vertex.0 || reference == first.0 { + return Self::report(Self::SAME_POINT); + } + self.step = Step::AngularLocation { + data: AngularData::Points { + vertex, + first, + second: (reference, point), + }, + }; + CmdResult::NeedPoint + } _ => CmdResult::NeedPoint, } } @@ -657,11 +985,49 @@ impl CadCommand for DimConstraintCommand { whole_curve: false, }) } + Step::AngularVertex | Step::AngularPoint1 { .. } | Step::AngularPoint2 { .. } => { + CmdResult::CheckConstraintPoint(CoincidentPick { + handle: None, + point, + whole_curve: false, + }) + } Step::Object | Step::PointLineLine { .. } | Step::LineFirst | Step::TwoLinesFirst - | Step::TwoLinesSecond { .. } => Self::report(Self::NO_OBJECT), + | Step::TwoLinesSecond { .. } + | Step::AngularFirst + | Step::AngularSecond { .. } => Self::report(Self::NO_OBJECT), + Step::AngularLocation { data } => { + let frame = match data { + AngularData::Lines { first, second } => Self::angle_frame( + first.ends[0].1, + first.ends[1].1, + second.ends[0].1, + second.ends[1].1, + point, + ), + AngularData::Points { + vertex, + first, + second, + } => Self::ray_frame(vertex.1, first.1, second.1, point), + }; + let Some((measured, sector)) = frame else { + return Self::report(Self::NO_POINT); + }; + self.step = Step::AngularValue { + data, + location: point, + sector, + measured, + }; + CmdResult::ReportMeasurement(format!( + "Dimension text = {}", + self.angle_display(measured) + )) + } Step::Location { first, second, @@ -684,7 +1050,9 @@ impl CadCommand for DimConstraintCommand { }; CmdResult::ReportMeasurement(format!("Dimension text = {measured:.4}")) } - Step::TwoLinesParallel { .. } | Step::Value { .. } => CmdResult::NeedPoint, + Step::TwoLinesParallel { .. } | Step::Value { .. } | Step::AngularValue { .. } => { + CmdResult::NeedPoint + } } } @@ -694,6 +1062,14 @@ impl CadCommand for DimConstraintCommand { self.step = Step::Object; CmdResult::NeedPoint } + // Enter takes the 3Point default. + Step::AngularFirst => { + self.step = Step::AngularVertex; + CmdResult::NeedPoint + } + Step::AngularValue { measured, .. } => self + .build_angular(self.default_name.clone(), measured_expression(measured)) + .unwrap_or(CmdResult::Cancel), Step::PointLinePoint => { self.step = Step::LineFirst; CmdResult::NeedPoint diff --git a/src/modules/parametric/mod.rs b/src/modules/parametric/mod.rs index c064008d9..bd432ed06 100644 --- a/src/modules/parametric/mod.rs +++ b/src/modules/parametric/mod.rs @@ -50,7 +50,7 @@ pub use tools::{ perpendicular, symmetric, tangent, vertical, }; pub use value::{ - angle_tool, dimensional_tools, distance_tool, AngleConstraintCommand, + angle_tool, dimensional_tools, distance_tool, DistanceConstraintCommand, DistanceMode, }; @@ -123,9 +123,9 @@ impl CadModule for ParametricModule { default: "DCLINEAR", }, RibbonItem::LargeTool(dimensional_tools::aligned()), - RibbonItem::LargeTool(dimensional_tools::angular()), - RibbonItem::LargeTool(dimensional_tools::diameter()), RibbonItem::LargeTool(dimensional_tools::radius()), + RibbonItem::LargeTool(dimensional_tools::diameter()), + RibbonItem::LargeTool(dimensional_tools::angular()), RibbonItem::LargeTool(dimensional_tools::convert()), RibbonItem::LabeledDropdown { id: "DCVISIBILITY", label: "Show/Hide", @@ -201,8 +201,8 @@ mod tests { assert_eq!( groups[1].tools.iter().map(item_id).collect::>(), [ - "DC_LINEAR_MENU", "DCALIGNED", "DCANGULAR", "DCDIAMETER", - "DCRADIUS", "DCCONVERT", "DCVISIBILITY", "DCSHOWALL", "DCHIDEALL", + "DC_LINEAR_MENU", "DCALIGNED", "DCRADIUS", "DCDIAMETER", + "DCANGULAR", "DCCONVERT", "DCVISIBILITY", "DCSHOWALL", "DCHIDEALL", ] ); assert_eq!(groups[2].tools.iter().map(item_id).collect::>(), ["DELCONSTRAINT", "PARAMETERS"]); diff --git a/src/modules/parametric/value.rs b/src/modules/parametric/value.rs index 94e304f12..6ef7e8c78 100644 --- a/src/modules/parametric/value.rs +++ b/src/modules/parametric/value.rs @@ -372,99 +372,6 @@ impl CadCommand for DistanceConstraintCommand { } } -/// Constrains the angle (in degrees) from `fixed`'s direction to `moving`'s. -pub struct AngleConstraintCommand { - command_name: &'static str, - fixed_handle: Handle, - moving_handle: Handle, - default_value: f64, - /// Snapshot of `Scene::named_parameters`' names at construction time — - /// see `parse_driving_value`'s doc comment for why a snapshot. - known_param_names: Vec, -} - -impl AngleConstraintCommand { - /// `None` unless both `fixed` and `moving` are lines. - pub fn new(scene: &Scene, fixed: Handle, moving: Handle) -> Option { - Self::with_name(scene, fixed, moving, "ACONSTRAINT") - } - - pub fn with_name( - scene: &Scene, - fixed: Handle, - moving: Handle, - command_name: &'static str, - ) -> Option { - let fixed_entity = scene.document.get_entity(fixed)?; - let moving_entity = scene.document.get_entity(moving)?; - let (EntityType::Line(f), EntityType::Line(m)) = (fixed_entity, moving_entity) else { - return None; - }; - let a1 = (f.end.y - f.start.y).atan2(f.end.x - f.start.x); - let a2 = (m.end.y - m.start.y).atan2(m.end.x - m.start.x); - let default_value = (a2 - a1).to_degrees(); - let known_param_names = scene - .named_parameters() - .iter() - .map(|p| p.name.clone()) - .collect(); - Some(Self { - command_name, - fixed_handle: fixed, - moving_handle: moving, - default_value, - known_param_names, - }) - } - - fn build(&self, target: DrivingValue) -> Option { - if matches!(&target, DrivingValue::Literal(value) if !value.is_finite()) { - return None; - } - Some(CmdResult::AddParametricConstraint { - kind: ConstraintKind::Angle, - refs: vec![ - ParametricRef::whole(self.fixed_handle), - ParametricRef::whole(self.moving_handle), - ], - driving_param: Some(target), - label: "Angle constraint", - }) - } -} - -impl CadCommand for AngleConstraintCommand { - fn name(&self) -> &'static str { - self.command_name - } - - fn prompt(&self) -> String { - format!("Specify angle in degrees <{:.4}>: ", self.default_value) - } - - fn input_kind(&self) -> InputKind { - InputKind::SingleToken - } - - fn on_point(&mut self, _pt: DVec3) -> CmdResult { - CmdResult::NeedPoint - } - - fn on_enter(&mut self) -> CmdResult { - self.build(DrivingValue::Literal(self.default_value)) - .unwrap_or(CmdResult::Cancel) - } - - fn on_text_input(&mut self, text: &str) -> Option { - let value = parse_driving_value(text, &self.known_param_names)?; - self.build(value) - } - - fn on_escape(&mut self) -> CmdResult { - CmdResult::Cancel - } -} - // ── Autocomplete registry ───────────────────────────────── inventory::submit!(crate::command::CommandRegistration { names: &[ diff --git a/src/scene/parametric_constraints.rs b/src/scene/parametric_constraints.rs index 95a39065a..7c20a9c36 100644 --- a/src/scene/parametric_constraints.rs +++ b/src/scene/parametric_constraints.rs @@ -363,6 +363,55 @@ pub(crate) fn next_dimensional_parameter_name( .unwrap_or_else(|| "d1".to_string()) } +/// The first free `ang1`, `ang2`, … name a new angular constraint takes. +pub(crate) fn next_angular_parameter_name( + table: &super::named_parameters::ParameterTable, +) -> String { + (1..) + .map(|n| format!("ang{n}")) + .find(|name| !table.contains(name)) + .unwrap_or_else(|| "ang1".to_string()) +} + +/// An angle as the reference displays it: a negative or over-full value is +/// brought into one turn (`-30` → `330`, `400` → `40`), a full turn stays. +pub(crate) fn normalize_angle_display(degrees: f64) -> f64 { + if !degrees.is_finite() || (degrees.abs() - 360.0).abs() < 1.0e-9 { + degrees + } else { + degrees.rem_euclid(360.0) + } +} + +/// The angular precision (DIMADEC, or DIMDEC when unset) of a dimension +/// style, by name or the drawing's current one. +pub(crate) fn angle_decimals(document: &acadrust::CadDocument, style_name: Option<&str>) -> usize { + let requested = style_name + .map(str::trim) + .filter(|name| !name.is_empty()) + .unwrap_or_else(|| document.header.current_dimstyle_name.trim()); + let style = document + .dim_styles + .iter() + .find(|style| style.name.eq_ignore_ascii_case(requested)) + .or_else(|| { + document + .dim_styles + .iter() + .find(|style| style.name.eq_ignore_ascii_case("Standard")) + }); + style + .map(|style| { + if style.dimadec < 0 { + style.dimdec + } else { + style.dimadec + } + }) + .unwrap_or(0) + .clamp(0, 8) as usize +} + /// The constraint-bar label that draws a dynamic dimension's lock mark. pub const DYNAMIC_DIMENSION_GLYPH: &str = "\u{1F512}"; @@ -385,12 +434,18 @@ pub(crate) fn dynamic_dimension_text( reference: bool, expression: Option<&str>, annotational: bool, + angle_decimals: Option, ) -> String { let formula = expression .map(str::trim) .filter(|source| !source.is_empty() && source.parse::().is_err()); let shown = match formula { Some(source) => source.to_string(), + // An angle shows at angular precision, within one turn (`ang1=27`). + None if angle_decimals.is_some() => { + let decimals = angle_decimals.unwrap_or(0); + format!("{:.decimals$}", normalize_angle_display(value)) + } // An annotational dimension shows the value at dimension precision // (`d1=100.0000`), as the reference does. None if annotational => format!("{value:.4}"), @@ -510,10 +565,107 @@ fn dynamic_dimension_follow_points( true } +/// The end points of a line reference: a line's two ends, or the picked +/// polyline segment's vertices. +fn line_ends(entity: &acadrust::EntityType, reference: ParametricRef) -> Option<(Vector3, Vector3)> { + let (start, end) = match reference.segment_index() { + Some(index) => { + let index = index as i32; + let end = resolve_point(entity, index + 1).map_or(0, |_| index + 1); + (index, end) + } + None => (0, 1), + }; + resolve_point(entity, start).zip(resolve_point(entity, end)) +} + +/// Where an angular constraint's sides are now: both lines' ends for a +/// two-line angle, `[vertex, first, second]` for a three-point one. +fn angular_follow_points( + document: &acadrust::CadDocument, + constraint: &ParametricConstraint, +) -> Option> { + match (constraint.kind, constraint.refs.as_slice()) { + (ConstraintKind::Angle, [first, second]) => { + let (a, b) = line_ends(document.get_entity(first.entity)?, *first)?; + let (c, d) = line_ends(document.get_entity(second.entity)?, *second)?; + Some(vec![a, b, c, d]) + } + (ConstraintKind::Angle3Point, [first, vertex, second]) => { + let world = |reference: &ParametricRef| { + resolve_point(document.get_entity(reference.entity)?, reference.marker?) + }; + Some(vec![world(vertex)?, world(first)?, world(second)?]) + } + _ => None, + } +} + +/// Rebuilds a dynamic angular dimension on its sides' current positions, +/// keeping its arc point; true when anything moved. +fn dynamic_dimension_follow_angle( + dimension: &mut acadrust::entities::Dimension, + points: &[Vector3], +) -> bool { + use acadrust::entities::Dimension; + let same = |a: Vector3, b: Vector3| (a - b).length_squared() < 1.0e-16; + let to_dvec = |p: Vector3| glam::DVec3::new(p.x, p.y, p.z); + let text = dimension.base().user_text.clone(); + let rebuilt = match (&*dimension, points) { + (Dimension::Angular2Ln(d), [a, b, c, e]) => { + if same(d.first_point, *a) + && same(d.second_point, *b) + && same(d.angle_vertex, *c) + && same(d.definition_point, *e) + { + return false; + } + crate::modules::annotate::angular_dim::angular_two_line_entity( + to_dvec(*a), + to_dvec(*b), + to_dvec(*c), + to_dvec(*e), + to_dvec(d.dimension_arc), + text, + ) + } + (Dimension::Angular3Pt(d), [vertex, first, second]) => { + if same(d.angle_vertex, *vertex) + && same(d.first_point, *first) + && same(d.second_point, *second) + { + return false; + } + crate::modules::annotate::angular_dim::angular_three_point_entity( + to_dvec(*vertex), + to_dvec(*first), + to_dvec(*second), + to_dvec(d.definition_point), + text, + ) + } + _ => return false, + }; + let Some(acadrust::EntityType::Dimension(mut rebuilt)) = rebuilt else { + return false; + }; + let base = dimension.base(); + let target = rebuilt.base_mut(); + target.common = base.common.clone(); + target.style_name = base.style_name.clone(); + target.text_rotation = base.text_rotation; + target.attachment_point = base.attachment_point; + target.line_spacing_style = base.line_spacing_style; + target.line_spacing_factor = base.line_spacing_factor; + *dimension = rebuilt; + true +} + /// What stays put when a dimensional constraint's value changes: its first /// point, and — when the distance runs perpendicular to a line that owns /// that point (2Lines, line-first Point & line) — the whole line, so the -/// other object moves, as in the reference. +/// other object moves, as in the reference. An angle keeps its first side +/// (a two-line angle) or its vertex and first point (a three-point one). pub(crate) fn dimensional_anchor_refs( document: &acadrust::CadDocument, refs: &[ParametricRef], @@ -521,6 +673,29 @@ pub(crate) fn dimensional_anchor_refs( let Some(&first) = refs.first() else { return Vec::new(); }; + // Three point references: a three-point angle `[first, vertex, second]`. + if refs.len() == 3 + && refs + .iter() + .all(|reference| reference.marker.is_some() && reference.segment_index().is_none()) + { + return vec![refs[1], first]; + } + // A two-line angle whose first side is a polyline segment: pin its ends. + if refs.len() == 2 && first.marker.is_none() { + return vec![first]; + } + if let (2, Some(index)) = (refs.len(), first.segment_index()) { + let index = index as i32; + let end = document + .get_entity(first.entity) + .and_then(|entity| resolve_point(entity, index + 1)) + .map_or(0, |_| index + 1); + return vec![ + ParametricRef::point(first.entity, index), + ParametricRef::point(first.entity, end), + ]; + } let mut anchors = vec![first]; if let Some(&line) = refs.get(2).filter(|line| line.entity == first.entity) { let ends = match line.segment_index() { @@ -1803,7 +1978,12 @@ impl super::Scene { /// origins to where the solve left the constraint points. pub(crate) fn refresh_dynamic_dimension_texts(&mut self) { let format = self.constraint_name_format; - let mut updates: Vec<(Handle, String, Option<(Vector3, Vector3)>)> = Vec::new(); + let mut updates: Vec<( + Handle, + String, + Option<(Vector3, Vector3)>, + Option>, + )> = Vec::new(); // A reference (driven) constraint's parameter follows the geometry. let mut followed: Vec<(String, f64)> = Vec::new(); for set in &self.parametric_constraints { @@ -1826,7 +2006,11 @@ impl super::Scene { _ => None, }; let reference = !constraint.enabled; - let measured = points.map(|(first, second)| { + let angular = matches!( + constraint.kind, + ConstraintKind::Angle | ConstraintKind::Angle3Point + ); + let measured = points.filter(|_| !angular).map(|(first, second)| { let delta = second - first; let axis = match self.document.get_entity(*dimension) { Some(acadrust::EntityType::Dimension( @@ -1863,6 +2047,16 @@ impl super::Scene { None => continue, }; let annotational = self.dimension_is_annotational(*dimension); + let decimals = angular.then(|| { + let style = match self.document.get_entity(*dimension) { + Some(acadrust::EntityType::Dimension(d)) => d.base().style_name.clone(), + _ => String::new(), + }; + angle_decimals(&self.document, Some(&style)) + }); + let angle_points = angular + .then(|| angular_follow_points(&self.document, constraint)) + .flatten(); updates.push(( *dimension, dynamic_dimension_text( @@ -1872,8 +2066,10 @@ impl super::Scene { reference, source.as_deref(), annotational, + decimals, ), points, + angle_points, )); } } @@ -1883,7 +2079,7 @@ impl super::Scene { let _ = self.named_parameters.set(&name, &source); } } - for (handle, text, points) in updates { + for (handle, text, points, angle_points) in updates { let Some(acadrust::EntityType::Dimension(mut dimension)) = self.document.get_entity(handle).cloned() else { @@ -1895,6 +2091,11 @@ impl super::Scene { changed = true; } } + if let Some(angle_points) = angle_points { + if dynamic_dimension_follow_angle(&mut dimension, &angle_points) { + changed = true; + } + } if dimension.base().user_text.as_deref() != Some(text.as_str()) { crate::entities::dimension::set_dimension_text_override( dimension.base_mut(), @@ -1995,6 +2196,31 @@ impl super::Scene { let _ = self.document.layers.add(layer); } + /// True when an angular constraint drives with the parameter `name`. + pub(crate) fn parameter_is_angular(&self, name: &str) -> bool { + self.parametric_constraints + .iter() + .flat_map(|set| set.constraints.iter()) + .any(|constraint| { + matches!(constraint.kind, ConstraintKind::Angle | ConstraintKind::Angle3Point) + && matches!(&constraint.driving_param, Some(DrivingValue::Named(used)) if used == name) + }) + } + + /// A parameter's value as -PARAMETERS prints it: an angle at angular + /// precision within one turn, anything else with four decimals. + pub(crate) fn parameter_value_text(&self, name: &str) -> String { + let Ok(value) = self.named_parameters.resolve(name) else { + return "**".to_string(); + }; + if self.parameter_is_angular(name) { + let decimals = angle_decimals(&self.document, None); + format!("{:.decimals$}", normalize_angle_display(value)) + } else { + format!("{value:.4}") + } + } + /// True for a dynamic dimension of a dimensional constraint (not an /// annotational one, which is an ordinary plotted dimension). pub(crate) fn is_dynamic_dimension(&self, handle: Handle) -> bool { @@ -2078,8 +2304,15 @@ impl super::Scene { use crate::entities::dim_override as ov; let xdata = &dimension.base().common.extended_data; let current = ov::real(xdata, ov::DIMSCALE); - // The reference draws dynamic dimension text horizontally. - let horizontal = ov::int(xdata, ov::DIMTIH) == Some(1) && ov::int(xdata, ov::DIMTOH) == Some(1); + // The reference draws dynamic distance text horizontally; an + // angle's text keeps its style's alignment. + let angular = matches!( + dimension, + acadrust::entities::Dimension::Angular2Ln(_) + | acadrust::entities::Dimension::Angular3Pt(_) + ); + let horizontal = angular + || (ov::int(xdata, ov::DIMTIH) == Some(1) && ov::int(xdata, ov::DIMTOH) == Some(1)); if current.is_some_and(|value| (value - scale).abs() < 1e-9) && horizontal { continue; }