forked from 1b-agent/opencadstudio-upstream
Merge pull request #1443 from ramox81:codex/radius-constraint-parity
This commit is contained in:
commit
6605a71fdd
11 files changed
+516
-87
No files matched your search
@@ -4533,6 +4533,172 @@ impl OpenCADStudio {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
CmdResult::AddRadialConstraint {
|
||||
circle,
|
||||
location,
|
||||
name,
|
||||
expression,
|
||||
renamed,
|
||||
} => {
|
||||
use crate::scene::named_parameters::{is_valid_name, DrivingValue};
|
||||
use crate::scene::parametric_constraints::{dynamic_dimension_text, ConstraintKind};
|
||||
|
||||
let scope = self.tabs[i].current_parametric_scope();
|
||||
let kind = ConstraintKind::Radius;
|
||||
let refs = vec![circle];
|
||||
// A second radius on the same circle is refused after the
|
||||
// value, 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 == 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();
|
||||
}
|
||||
// A negative radius is its magnitude (`rad1=-20` is a radius
|
||||
// of 20); a zero radius has no circle.
|
||||
let value = match table.resolve(&name) {
|
||||
Ok(value) if value.is_finite() && value != 0.0 => 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 text = dynamic_dimension_text(
|
||||
&name,
|
||||
value,
|
||||
self.tabs[i].scene.constraint_name_format,
|
||||
false,
|
||||
Some(&expression),
|
||||
annotational,
|
||||
None,
|
||||
);
|
||||
let source = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(circle.entity)
|
||||
.and_then(|entity| {
|
||||
crate::scene::dimension_assoc::radial_source_at(
|
||||
entity,
|
||||
acadrust::types::Vector3::new(location.x, location.y, location.z),
|
||||
)
|
||||
});
|
||||
let Some(source) = source else {
|
||||
self.reprompt_active_command(
|
||||
i,
|
||||
crate::modules::parametric::DimConstraintCommand::NO_OBJECT,
|
||||
);
|
||||
return Task::none();
|
||||
};
|
||||
let mut entity = crate::modules::annotate::radius_dim::radial_dimension_entity(
|
||||
source,
|
||||
location,
|
||||
Some(text),
|
||||
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)
|
||||
});
|
||||
// The center stays; a value other than the measured radius
|
||||
// resizes the circle about it.
|
||||
let anchors = crate::scene::parametric_constraints::dimensional_anchor_refs(
|
||||
&self.tabs[i].scene.document,
|
||||
&refs,
|
||||
);
|
||||
let pending = self.begin_undo(i, "Radius constraint", 2, 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)));
|
||||
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, vec![Some(circle.entity)]);
|
||||
self.tabs[i].scene.bump_entities_with_parametric_policy(
|
||||
&[(circle.entity, crate::scene::ChangeKind::Modified)],
|
||||
&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,
|
||||
@@ -5977,6 +6143,16 @@ impl OpenCADStudio {
|
||||
CmdResult::Dispatch(cmd) => {
|
||||
// End this interactive front-end, then run the assembled command
|
||||
// through the normal dispatcher. Selection is left untouched.
|
||||
// DIMCONSTRAINT's Radius option becomes its next default; the
|
||||
// DCRADIUS command run on its own leaves the default alone.
|
||||
if cmd == "DCRADIUS"
|
||||
&& self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.is_some_and(|command| command.name() == "DIMCONSTRAINT")
|
||||
{
|
||||
self.dim_constraint_last = "Radius";
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
|
||||
@@ -1891,10 +1891,20 @@ impl OpenCADStudio {
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
|
||||
"DCONSTRAINT" | "DCRADIUS" | "DCDIAMETER" => {
|
||||
if cmd == "DCRADIUS" {
|
||||
self.dim_constraint_last = "Radius";
|
||||
} else if cmd == "DCDIAMETER" {
|
||||
"DCRADIUS" => {
|
||||
use crate::modules::parametric::{DimConstraintAxis, DimConstraintCommand};
|
||||
use crate::scene::parametric_constraints::next_radial_parameter_name;
|
||||
// Run on its own it leaves DIMCONSTRAINT's default alone.
|
||||
let name = next_radial_parameter_name(self.tabs[i].scene.named_parameters());
|
||||
// The circle or arc is picked inside the command.
|
||||
self.tabs[i].scene.deselect_all();
|
||||
let command = DimConstraintCommand::new(DimConstraintAxis::Radius, name);
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
|
||||
"DCONSTRAINT" | "DCDIAMETER" => {
|
||||
if cmd == "DCDIAMETER" {
|
||||
self.dim_constraint_last = "Diameter";
|
||||
}
|
||||
let handles = self.tabs[i].scene.selected_handles_in_order();
|
||||
|
||||
@@ -2368,6 +2368,9 @@ impl OpenCADStudio {
|
||||
acadrust::EntityType::Dimension(
|
||||
Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_),
|
||||
) => t!("Angular Dimensional Constraint"),
|
||||
acadrust::EntityType::Dimension(Dimension::Radius(_)) => {
|
||||
t!("Radial Dimension (Dynamic)")
|
||||
}
|
||||
_ => t!("Linear Dimensional Constraint"),
|
||||
}
|
||||
.into_owned()
|
||||
@@ -2850,6 +2853,13 @@ handles={handles_ms:.1} panel={:.1} ribbon={ribbon_ms:.1} tail={:.1} selected={}
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if matches!(
|
||||
contextual.as_ref(),
|
||||
acadrust::EntityType::Dimension(acadrust::entities::Dimension::Radius(_))
|
||||
) && self.tabs[i].scene.is_dynamic_dimension(handle)
|
||||
{
|
||||
// A dynamic radius shows only its text square.
|
||||
entity_grips.retain(|grip| grip.id == 2);
|
||||
} else if self.tabs[i].scene.is_dynamic_dimension(handle) {
|
||||
// A dynamic dimension shows the reference's grips: a
|
||||
// triangle at each constraint point aimed at the other
|
||||
|
||||
@@ -1650,6 +1650,15 @@ pub enum CmdResult {
|
||||
expression: String,
|
||||
renamed: bool,
|
||||
},
|
||||
/// Adds a radius constraint on a circle or arc with its dynamic radial
|
||||
/// dimension (text at `location`) and the `radN` parameter that drives it.
|
||||
AddRadialConstraint {
|
||||
circle: crate::scene::parametric_constraints::ParametricRef,
|
||||
location: DVec3,
|
||||
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`.
|
||||
|
||||
@@ -1134,7 +1134,11 @@ impl Grippable for Dimension {
|
||||
// of the world origin, so it stays on the visible text and grabbable.
|
||||
let text = {
|
||||
let p = self.base().text_middle_point;
|
||||
if p.x * p.x + p.y * p.y + p.z * p.z > 1e-16 {
|
||||
// A dynamic radius constraint draws its text at mid-radius, not
|
||||
// at the stored point; its grip sits on the drawn text.
|
||||
let drawn_elsewhere =
|
||||
matches!(self, Dimension::Radius(_)) && dynamic_constraint_dimension(self);
|
||||
if p.x * p.x + p.y * p.y + p.z * p.z > 1e-16 && !drawn_elsewhere {
|
||||
dv3(&p)
|
||||
} else {
|
||||
dv3(&dimension_text_pos_f64(self, None, 2.5, 1.0))
|
||||
@@ -4780,7 +4784,12 @@ fn dimension_geometry(
|
||||
// to the centre. Text outside: only a leader from the arc point to
|
||||
// the text, unless DIMTOFL asks for the inside line as well.
|
||||
if !jogged && !suppress.dim2 && (!text_is_outside || params.dimtofl) {
|
||||
add_segment(&mut g.dim_lines, center, point);
|
||||
if dynamic_constraint_dimension(dim) {
|
||||
// The line passes behind the mid-radius text.
|
||||
add_segment_with_text_break(&mut g.dim_lines, center, point, params.text_break);
|
||||
} else {
|
||||
add_segment(&mut g.dim_lines, center, point);
|
||||
}
|
||||
}
|
||||
if text_is_outside && !suppress.dim2 {
|
||||
append_radial_leader(&mut g, point, text, ¶ms);
|
||||
@@ -7542,6 +7551,15 @@ fn stored_text_point(dim: &Dimension) -> Option<Vector3> {
|
||||
(p.x * p.x + p.y * p.y + p.z * p.z > 1e-16).then_some(p)
|
||||
}
|
||||
|
||||
/// A dimensional constraint's dynamic dimension: the reference keeps it on
|
||||
/// its own hidden layer and draws it by its own rules.
|
||||
fn dynamic_constraint_dimension(dim: &Dimension) -> bool {
|
||||
dim.base()
|
||||
.common
|
||||
.layer
|
||||
.eq_ignore_ascii_case(crate::scene::parametric_constraints::DYNAMIC_DIMENSION_LAYER)
|
||||
}
|
||||
|
||||
/// The point on the circle a radial leader leaves from: the arc point of a
|
||||
/// radius, or whichever end of a diameter's chord is nearer the text.
|
||||
fn radial_leader_tip(dim: &Dimension, text: Vector3) -> Vector3 {
|
||||
@@ -7598,6 +7616,12 @@ fn dimension_text_pos_f64(
|
||||
|
||||
if let Some(point) = stored_text_point(dim) {
|
||||
return match dim {
|
||||
// A dynamic radius constraint draws its text at mid-radius on the
|
||||
// dimension line, whatever text position the file stores (the
|
||||
// picked location), as the reference does.
|
||||
Dimension::Radius(d) if dynamic_constraint_dimension(dim) => {
|
||||
(d.angle_vertex + d.definition_point) * 0.5
|
||||
}
|
||||
// DIMTMOVE 1 stores where the leader's hook starts: one arrow of
|
||||
// hook, a gap, then the text, all running away from the arc.
|
||||
// Measured on 39 such dimensions the stored point sits 2.4 text
|
||||
|
||||
@@ -62,32 +62,40 @@ impl RadiusDimensionCommand {
|
||||
source: crate::scene::dimension_assoc::RadialSourceGeometry,
|
||||
world: DVec3,
|
||||
) -> EntityType {
|
||||
let plane = WorkingPlane::new(
|
||||
DVec3::from_array(source.plane.origin),
|
||||
DVec3::from_array(source.plane.x_axis),
|
||||
DVec3::from_array(source.plane.y_axis),
|
||||
);
|
||||
let center = plane.to_local(dvec(source.center_world()));
|
||||
let chord = plane.to_local(dvec(source.chord_at(world.to_array())));
|
||||
let point = plane.to_local(world);
|
||||
let mut dim = DimensionRadius::new(v3(center), v3(chord));
|
||||
dim.base.definition_point = v3(chord);
|
||||
dim.base.text_middle_point = v3(point);
|
||||
dim.base.insertion_point = v3(point);
|
||||
dim.base.text_user_positioned = true;
|
||||
dim.leader_length = chord.distance(point);
|
||||
dim.base.actual_measurement = dim.measurement();
|
||||
crate::entities::dimension::set_dimension_text_override(
|
||||
&mut dim.base,
|
||||
self.text_override.clone(),
|
||||
);
|
||||
if let Some(angle) = self.text_angle {
|
||||
dim.base.text_rotation = angle;
|
||||
}
|
||||
plane.place_entity(EntityType::Dimension(Dimension::Radius(dim)))
|
||||
radial_dimension_entity(source, world, self.text_override.clone(), self.text_angle)
|
||||
}
|
||||
}
|
||||
|
||||
/// A radius dimension of `source` with its text at `world`: the dimension
|
||||
/// line runs from the center through the text (DIMUPT).
|
||||
pub(crate) fn radial_dimension_entity(
|
||||
source: crate::scene::dimension_assoc::RadialSourceGeometry,
|
||||
world: DVec3,
|
||||
text_override: Option<String>,
|
||||
text_angle: Option<f64>,
|
||||
) -> EntityType {
|
||||
let plane = WorkingPlane::new(
|
||||
DVec3::from_array(source.plane.origin),
|
||||
DVec3::from_array(source.plane.x_axis),
|
||||
DVec3::from_array(source.plane.y_axis),
|
||||
);
|
||||
let center = plane.to_local(dvec(source.center_world()));
|
||||
let chord = plane.to_local(dvec(source.chord_at(world.to_array())));
|
||||
let point = plane.to_local(world);
|
||||
let mut dim = DimensionRadius::new(v3(center), v3(chord));
|
||||
dim.base.definition_point = v3(chord);
|
||||
dim.base.text_middle_point = v3(point);
|
||||
dim.base.insertion_point = v3(point);
|
||||
dim.base.text_user_positioned = true;
|
||||
dim.leader_length = chord.distance(point);
|
||||
dim.base.actual_measurement = dim.measurement();
|
||||
crate::entities::dimension::set_dimension_text_override(&mut dim.base, text_override);
|
||||
if let Some(angle) = text_angle {
|
||||
dim.base.text_rotation = angle;
|
||||
}
|
||||
plane.place_entity(EntityType::Dimension(Dimension::Radius(dim)))
|
||||
}
|
||||
|
||||
impl CadCommand for RadiusDimensionCommand {
|
||||
fn set_working_plane(&mut self, _plane: WorkingPlane) {
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ pub enum DimConstraintAxis {
|
||||
Aligned,
|
||||
/// The angle between two lines, or at a vertex between two points.
|
||||
Angular,
|
||||
/// The radius of a circle or arc.
|
||||
Radius,
|
||||
}
|
||||
|
||||
/// A picked line (a line entity or one polyline segment) an Aligned
|
||||
@@ -106,6 +108,16 @@ enum Step {
|
||||
sector: u8,
|
||||
measured: f64,
|
||||
},
|
||||
/// Radius: `Select arc or circle:`
|
||||
RadialSelect,
|
||||
/// `Specify dimension line location:` of a radius.
|
||||
RadialLocation { circle: ParametricRef, radius: f64 },
|
||||
/// `Enter value or name and value <rad1=30>:`
|
||||
RadialValue {
|
||||
circle: ParametricRef,
|
||||
radius: f64,
|
||||
location: DVec3,
|
||||
},
|
||||
/// `Enter value or name and value <d1=100>:`
|
||||
Value {
|
||||
first: ParametricRef,
|
||||
@@ -155,14 +167,16 @@ impl DimConstraintCommand {
|
||||
"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 const INVALID_RADIAL: &'static str =
|
||||
"Invalid selection for Radial. Select a circle or arc.";
|
||||
|
||||
pub fn new(axis: DimConstraintAxis, default_name: String) -> Self {
|
||||
Self {
|
||||
axis,
|
||||
step: if axis == DimConstraintAxis::Angular {
|
||||
Step::AngularFirst
|
||||
} else {
|
||||
Step::First
|
||||
step: match axis {
|
||||
DimConstraintAxis::Angular => Step::AngularFirst,
|
||||
DimConstraintAxis::Radius => Step::RadialSelect,
|
||||
_ => Step::First,
|
||||
},
|
||||
picked_entity: None,
|
||||
default_name,
|
||||
@@ -304,6 +318,25 @@ impl DimConstraintCommand {
|
||||
})
|
||||
}
|
||||
|
||||
fn build_radial(&self, name: String, expression: String) -> Option<CmdResult> {
|
||||
let Step::RadialValue {
|
||||
circle, location, ..
|
||||
} = self.step
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
if expression.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(CmdResult::AddRadialConstraint {
|
||||
circle,
|
||||
location,
|
||||
renamed: name != self.default_name,
|
||||
name,
|
||||
expression,
|
||||
})
|
||||
}
|
||||
|
||||
fn command_name(&self) -> &'static str {
|
||||
match self.axis {
|
||||
DimConstraintAxis::Linear => "DCLINEAR",
|
||||
@@ -311,6 +344,7 @@ impl DimConstraintCommand {
|
||||
DimConstraintAxis::Vertical => "DCVERTICAL",
|
||||
DimConstraintAxis::Aligned => "DCALIGNED",
|
||||
DimConstraintAxis::Angular => "DCANGULAR",
|
||||
DimConstraintAxis::Radius => "DCRADIUS",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,6 +355,7 @@ impl DimConstraintCommand {
|
||||
DimConstraintAxis::Vertical => "Vertical",
|
||||
DimConstraintAxis::Aligned => "Aligned",
|
||||
DimConstraintAxis::Angular => "Angular",
|
||||
DimConstraintAxis::Radius => "Radial",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,8 +509,8 @@ impl DimConstraintCommand {
|
||||
}
|
||||
DimConstraintAxis::Horizontal => (ConstraintKind::DistanceX, DVec3::X),
|
||||
DimConstraintAxis::Vertical => (ConstraintKind::DistanceY, DVec3::Y),
|
||||
// Angular never measures here; its own steps decide the sector.
|
||||
DimConstraintAxis::Aligned | DimConstraintAxis::Angular => (
|
||||
// Angular and Radius never measure here; their own steps decide.
|
||||
DimConstraintAxis::Aligned | DimConstraintAxis::Angular | DimConstraintAxis::Radius => (
|
||||
ConstraintKind::Distance,
|
||||
(second - first).normalize_or(DVec3::X),
|
||||
),
|
||||
@@ -525,7 +560,9 @@ impl DimConstraintCommand {
|
||||
DimConstraintAxis::Linear => "Linear constraint",
|
||||
DimConstraintAxis::Horizontal => "Horizontal distance constraint",
|
||||
DimConstraintAxis::Vertical => "Vertical distance constraint",
|
||||
DimConstraintAxis::Aligned | DimConstraintAxis::Angular => "Aligned constraint",
|
||||
DimConstraintAxis::Aligned | DimConstraintAxis::Angular | DimConstraintAxis::Radius => {
|
||||
"Aligned constraint"
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -567,14 +604,17 @@ impl CadCommand for DimConstraintCommand {
|
||||
Step::AngularPoint2 { .. } => {
|
||||
format!("{name} Specify second angle constraint point:")
|
||||
}
|
||||
Step::AngularLocation { .. } | Step::Location { .. } => {
|
||||
Step::RadialSelect => format!("{name} Select arc or circle:"),
|
||||
Step::AngularLocation { .. } | Step::Location { .. } | Step::RadialLocation { .. } => {
|
||||
format!("{name} Specify dimension line location:")
|
||||
}
|
||||
Step::AngularValue { measured, .. } => format!(
|
||||
"{name} Enter value or name and value <{}={}>:",
|
||||
self.default_name,
|
||||
measured_expression(measured)
|
||||
),
|
||||
Step::AngularValue { measured, .. } | Step::RadialValue { radius: 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,
|
||||
@@ -609,6 +649,7 @@ impl CadCommand for DimConstraintCommand {
|
||||
| Step::Value { .. }
|
||||
| Step::AngularFirst
|
||||
| Step::AngularValue { .. }
|
||||
| Step::RadialValue { .. }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -643,16 +684,16 @@ impl CadCommand for DimConstraintCommand {
|
||||
}
|
||||
None
|
||||
}
|
||||
Step::Value { .. } | Step::AngularValue { .. } => {
|
||||
Step::Value { .. } | Step::AngularValue { .. } | Step::RadialValue { .. } => {
|
||||
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()),
|
||||
};
|
||||
if matches!(self.step, Step::AngularValue { .. }) {
|
||||
self.build_angular(name, expression)
|
||||
} else {
|
||||
self.build(name, expression)
|
||||
match self.step {
|
||||
Step::AngularValue { .. } => self.build_angular(name, expression),
|
||||
Step::RadialValue { .. } => self.build_radial(name, expression),
|
||||
_ => self.build(name, expression),
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
@@ -676,6 +717,7 @@ impl CadCommand for DimConstraintCommand {
|
||||
| Step::AngularVertex
|
||||
| Step::AngularPoint1 { .. }
|
||||
| Step::AngularPoint2 { .. }
|
||||
| Step::RadialSelect
|
||||
)
|
||||
}
|
||||
|
||||
@@ -867,10 +909,35 @@ impl CadCommand for DimConstraintCommand {
|
||||
whole_curve: false,
|
||||
})
|
||||
}
|
||||
Step::RadialSelect => {
|
||||
if handle.is_null() {
|
||||
return Self::report(Self::NO_OBJECT);
|
||||
}
|
||||
let Some(entity) = self.picked_entity.take() else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
let radius = match &entity {
|
||||
EntityType::Circle(circle) => circle.radius,
|
||||
EntityType::Arc(arc) => arc.radius,
|
||||
_ => return Self::report(Self::INVALID_RADIAL),
|
||||
};
|
||||
self.step = Step::RadialLocation {
|
||||
circle: ParametricRef::whole(handle),
|
||||
radius,
|
||||
};
|
||||
// The reference reports the radius before asking where the
|
||||
// dimension goes.
|
||||
CmdResult::ReportMeasurement(format!(
|
||||
"Dimension text = {}",
|
||||
measured_expression(radius)
|
||||
))
|
||||
}
|
||||
Step::Location { .. }
|
||||
| Step::Value { .. }
|
||||
| Step::AngularLocation { .. }
|
||||
| Step::AngularValue { .. } => self.on_point(point),
|
||||
| Step::AngularValue { .. }
|
||||
| Step::RadialLocation { .. }
|
||||
| Step::RadialValue { .. } => self.on_point(point),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -998,7 +1065,16 @@ impl CadCommand for DimConstraintCommand {
|
||||
| Step::TwoLinesFirst
|
||||
| Step::TwoLinesSecond { .. }
|
||||
| Step::AngularFirst
|
||||
| Step::AngularSecond { .. } => Self::report(Self::NO_OBJECT),
|
||||
| Step::AngularSecond { .. }
|
||||
| Step::RadialSelect => Self::report(Self::NO_OBJECT),
|
||||
Step::RadialLocation { circle, radius } => {
|
||||
self.step = Step::RadialValue {
|
||||
circle,
|
||||
radius,
|
||||
location: point,
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::AngularLocation { data } => {
|
||||
let frame = match data {
|
||||
AngularData::Lines { first, second } => Self::angle_frame(
|
||||
@@ -1050,9 +1126,10 @@ impl CadCommand for DimConstraintCommand {
|
||||
};
|
||||
CmdResult::ReportMeasurement(format!("Dimension text = {measured:.4}"))
|
||||
}
|
||||
Step::TwoLinesParallel { .. } | Step::Value { .. } | Step::AngularValue { .. } => {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::TwoLinesParallel { .. }
|
||||
| Step::Value { .. }
|
||||
| Step::AngularValue { .. }
|
||||
| Step::RadialValue { .. } => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1070,6 +1147,9 @@ impl CadCommand for DimConstraintCommand {
|
||||
Step::AngularValue { measured, .. } => self
|
||||
.build_angular(self.default_name.clone(), measured_expression(measured))
|
||||
.unwrap_or(CmdResult::Cancel),
|
||||
Step::RadialValue { radius, .. } => self
|
||||
.build_radial(self.default_name.clone(), measured_expression(radius))
|
||||
.unwrap_or(CmdResult::Cancel),
|
||||
Step::PointLinePoint => {
|
||||
self.step = Step::LineFirst;
|
||||
CmdResult::NeedPoint
|
||||
|
||||
@@ -123,10 +123,14 @@ impl CadModule for ParametricModule {
|
||||
default: "DCLINEAR",
|
||||
},
|
||||
RibbonItem::LargeTool(dimensional_tools::aligned()),
|
||||
RibbonItem::LargeTool(dimensional_tools::radius()),
|
||||
RibbonItem::LargeTool(dimensional_tools::diameter()),
|
||||
RibbonItem::LargeTool(dimensional_tools::angular()),
|
||||
RibbonItem::LargeTool(dimensional_tools::convert()),
|
||||
// Two rows of small icon-only buttons, as in the
|
||||
// reference: Radius, Diameter / Angular, Convert.
|
||||
RibbonItem::ToolGrid {
|
||||
columns: vec![
|
||||
vec![dimensional_tools::radius(), dimensional_tools::angular()],
|
||||
vec![dimensional_tools::diameter(), dimensional_tools::convert()],
|
||||
],
|
||||
},
|
||||
RibbonItem::LabeledDropdown {
|
||||
id: "DCVISIBILITY", label: "Show/Hide",
|
||||
icon: IconKind::Svg(include_bytes!("../../../assets/icons/constrain/show.svg")),
|
||||
@@ -200,10 +204,17 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
groups[1].tools.iter().map(item_id).collect::<Vec<_>>(),
|
||||
[
|
||||
"DC_LINEAR_MENU", "DCALIGNED", "DCRADIUS", "DCDIAMETER",
|
||||
"DCANGULAR", "DCCONVERT", "DCVISIBILITY", "DCSHOWALL", "DCHIDEALL",
|
||||
]
|
||||
["DC_LINEAR_MENU", "DCALIGNED", "GRID", "DCVISIBILITY", "DCSHOWALL", "DCHIDEALL"]
|
||||
);
|
||||
let RibbonItem::ToolGrid { columns } = &groups[1].tools[2] else {
|
||||
panic!("radius, diameter, angular and convert must be a small-button grid");
|
||||
};
|
||||
assert_eq!(
|
||||
columns
|
||||
.iter()
|
||||
.map(|column| column.iter().map(|tool| tool.id).collect::<Vec<_>>())
|
||||
.collect::<Vec<_>>(),
|
||||
[["DCRADIUS", "DCANGULAR"], ["DCDIAMETER", "DCCONVERT"]]
|
||||
);
|
||||
assert_eq!(groups[2].tools.iter().map(item_id).collect::<Vec<_>>(), ["DELCONSTRAINT", "PARAMETERS"]);
|
||||
|
||||
@@ -218,8 +229,6 @@ mod tests {
|
||||
assert!(groups[0].tools[1..13]
|
||||
.iter()
|
||||
.all(|item| matches!(item, RibbonItem::LargeTool(_))));
|
||||
assert!(groups[1].tools[2..6]
|
||||
.iter()
|
||||
.all(|item| matches!(item, RibbonItem::LargeTool(_))));
|
||||
assert!(matches!(groups[1].tools[1], RibbonItem::LargeTool(_)));
|
||||
}
|
||||
}
|
||||
@@ -353,24 +353,36 @@ pub(crate) fn equal_size_follower(
|
||||
Some(entity)
|
||||
}
|
||||
|
||||
/// The first free `<prefix>1`, `<prefix>2`, … name a new constraint takes.
|
||||
fn next_prefixed_parameter_name(
|
||||
table: &super::named_parameters::ParameterTable,
|
||||
prefix: &str,
|
||||
) -> String {
|
||||
(1..)
|
||||
.map(|n| format!("{prefix}{n}"))
|
||||
.find(|name| !table.contains(name))
|
||||
.unwrap_or_else(|| format!("{prefix}1"))
|
||||
}
|
||||
|
||||
/// The first free `d1`, `d2`, … name a new dimensional constraint takes.
|
||||
pub(crate) fn next_dimensional_parameter_name(
|
||||
table: &super::named_parameters::ParameterTable,
|
||||
) -> String {
|
||||
(1..)
|
||||
.map(|n| format!("d{n}"))
|
||||
.find(|name| !table.contains(name))
|
||||
.unwrap_or_else(|| "d1".to_string())
|
||||
next_prefixed_parameter_name(table, "d")
|
||||
}
|
||||
|
||||
/// 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())
|
||||
next_prefixed_parameter_name(table, "ang")
|
||||
}
|
||||
|
||||
/// The first free `rad1`, `rad2`, … name a new radius constraint takes.
|
||||
pub(crate) fn next_radial_parameter_name(
|
||||
table: &super::named_parameters::ParameterTable,
|
||||
) -> String {
|
||||
next_prefixed_parameter_name(table, "rad")
|
||||
}
|
||||
|
||||
/// An angle as the reference displays it: a negative or over-full value is
|
||||
@@ -565,6 +577,52 @@ fn dynamic_dimension_follow_points(
|
||||
true
|
||||
}
|
||||
|
||||
/// A circle's or arc's center and radius.
|
||||
fn radial_geometry(entity: &acadrust::EntityType) -> Option<(Vector3, f64)> {
|
||||
match entity {
|
||||
acadrust::EntityType::Circle(circle) => Some((circle.center_wcs(), circle.radius)),
|
||||
acadrust::EntityType::Arc(arc) => Some((arc.center_wcs(), arc.radius)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves a dynamic radial dimension onto its circle's current center and
|
||||
/// radius: the dimension line keeps its direction and the text keeps its
|
||||
/// distance beyond the circle, as in the reference; true when it moved.
|
||||
fn dynamic_dimension_follow_radius(
|
||||
dimension: &mut acadrust::entities::Dimension,
|
||||
center: Vector3,
|
||||
radius: f64,
|
||||
) -> bool {
|
||||
let acadrust::entities::Dimension::Radius(radial) = dimension else {
|
||||
return false;
|
||||
};
|
||||
let to_dvec = |p: Vector3| glam::DVec3::new(p.x, p.y, p.z);
|
||||
let to_vec = |p: glam::DVec3| Vector3::new(p.x, p.y, p.z);
|
||||
let old_center = to_dvec(radial.angle_vertex);
|
||||
let old_radius = (to_dvec(radial.definition_point) - old_center).length();
|
||||
let center = to_dvec(center);
|
||||
if (old_center - center).length_squared() < 1.0e-16 && (old_radius - radius).abs() < 1.0e-9 {
|
||||
return false;
|
||||
}
|
||||
let toward_text = to_dvec(radial.base.text_middle_point) - old_center;
|
||||
let direction = toward_text
|
||||
.try_normalize()
|
||||
.or_else(|| (to_dvec(radial.definition_point) - old_center).try_normalize())
|
||||
.unwrap_or(glam::DVec3::X);
|
||||
let beyond = toward_text.length() - old_radius;
|
||||
let chord = center + direction * radius;
|
||||
let text = center + direction * (radius + beyond);
|
||||
radial.angle_vertex = to_vec(center);
|
||||
radial.definition_point = to_vec(chord);
|
||||
radial.base.definition_point = to_vec(chord);
|
||||
radial.base.text_middle_point = to_vec(text);
|
||||
radial.base.insertion_point = to_vec(text);
|
||||
radial.leader_length = (text - chord).length();
|
||||
radial.base.actual_measurement = radius;
|
||||
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)> {
|
||||
@@ -673,6 +731,11 @@ pub(crate) fn dimensional_anchor_refs(
|
||||
let Some(&first) = refs.first() else {
|
||||
return Vec::new();
|
||||
};
|
||||
// A whole circle or arc (a radius or diameter): its center stays and the
|
||||
// value resizes it.
|
||||
if refs.len() == 1 && first.marker.is_none() {
|
||||
return vec![ParametricRef::center(first.entity)];
|
||||
}
|
||||
// Three point references: a three-point angle `[first, vertex, second]`.
|
||||
if refs.len() == 3
|
||||
&& refs
|
||||
@@ -1983,6 +2046,7 @@ impl super::Scene {
|
||||
String,
|
||||
Option<(Vector3, Vector3)>,
|
||||
Option<Vec<Vector3>>,
|
||||
Option<(Vector3, f64)>,
|
||||
)> = Vec::new();
|
||||
// A reference (driven) constraint's parameter follows the geometry.
|
||||
let mut followed: Vec<(String, f64)> = Vec::new();
|
||||
@@ -2010,7 +2074,15 @@ impl super::Scene {
|
||||
constraint.kind,
|
||||
ConstraintKind::Angle | ConstraintKind::Angle3Point
|
||||
);
|
||||
let measured = points.filter(|_| !angular).map(|(first, second)| {
|
||||
// A radius: the circle's or arc's center and radius now.
|
||||
let radial = (constraint.kind == ConstraintKind::Radius)
|
||||
.then(|| {
|
||||
let reference = constraint.refs.first()?;
|
||||
radial_geometry(self.document.get_entity(reference.entity)?)
|
||||
})
|
||||
.flatten();
|
||||
let measured = radial.map(|(_, radius)| radius);
|
||||
let measured = measured.or(points.filter(|_| !angular).map(|(first, second)| {
|
||||
let delta = second - first;
|
||||
let axis = match self.document.get_entity(*dimension) {
|
||||
Some(acadrust::EntityType::Dimension(
|
||||
@@ -2026,7 +2098,7 @@ impl super::Scene {
|
||||
}
|
||||
_ => delta.length(),
|
||||
}
|
||||
});
|
||||
}));
|
||||
let (name, value, source) = match &constraint.driving_param {
|
||||
Some(DrivingValue::Named(name)) => {
|
||||
let value = match (reference, measured) {
|
||||
@@ -2070,6 +2142,7 @@ impl super::Scene {
|
||||
),
|
||||
points,
|
||||
angle_points,
|
||||
radial,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -2079,7 +2152,7 @@ impl super::Scene {
|
||||
let _ = self.named_parameters.set(&name, &source);
|
||||
}
|
||||
}
|
||||
for (handle, text, points, angle_points) in updates {
|
||||
for (handle, text, points, angle_points, radial) in updates {
|
||||
let Some(acadrust::EntityType::Dimension(mut dimension)) =
|
||||
self.document.get_entity(handle).cloned()
|
||||
else {
|
||||
@@ -2096,6 +2169,11 @@ impl super::Scene {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if let Some((center, radius)) = radial {
|
||||
if dynamic_dimension_follow_radius(&mut dimension, center, radius) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if dimension.base().user_text.as_deref() != Some(text.as_str()) {
|
||||
crate::entities::dimension::set_dimension_text_override(
|
||||
dimension.base_mut(),
|
||||
|
||||
@@ -996,14 +996,15 @@ fn resolved_target(params: &ParameterTable, constraint: &ParametricConstraint) -
|
||||
// A dimensional distance keeps its sign in the parameter but measures
|
||||
// its magnitude, as the reference does (`d1=-50` shortens the line to
|
||||
// 50; `0` folds the points together).
|
||||
if constraint.kind == ConstraintKind::Distance {
|
||||
return Some(value.abs());
|
||||
// A radius or diameter likewise (`rad1=-20` is a radius of 20); zero is
|
||||
// no circle at all.
|
||||
match constraint.kind {
|
||||
ConstraintKind::Distance => Some(value.abs()),
|
||||
ConstraintKind::Radius | ConstraintKind::Diameter => {
|
||||
(value != 0.0).then(|| value.abs())
|
||||
}
|
||||
_ => Some(value),
|
||||
}
|
||||
let must_be_positive = matches!(
|
||||
constraint.kind,
|
||||
ConstraintKind::Radius | ConstraintKind::Diameter
|
||||
);
|
||||
(!must_be_positive || value > 0.0).then_some(value)
|
||||
}
|
||||
|
||||
fn oriented_distance(target: f64, current_projection: f64) -> f64 {
|
||||
|
||||
@@ -480,6 +480,30 @@ pub(super) fn make_tip(tip: String) -> Element<'static, Message> {
|
||||
text(tip).size(11).into()
|
||||
}
|
||||
|
||||
/// The sentence the reference shows under a tool's name, where known.
|
||||
fn tool_description(id: &str) -> Option<&'static str> {
|
||||
Some(match id {
|
||||
"DCLINEAR" => "Constrains the horizontal or vertical distance between points",
|
||||
"DCHORIZONTAL" => "Constrains the X-distance between points on an object, or between two points on different objects",
|
||||
"DCVERTICAL" => "Constrains the Y-distance between points on an object, or between two points on different objects",
|
||||
"DCALIGNED" => "Constrains the distance between two points on an object or between two points on different objects",
|
||||
"DCRADIUS" => "Constrains the radius of a circle or arc",
|
||||
"DCDIAMETER" => "Constrains the diameter of a circle or arc",
|
||||
"DCANGULAR" => "Constrains the angle between line or polyline segments, the angle swept out by an arc or a polyline arc segment, or the angle between three points on objects",
|
||||
"DCCONVERT" => "Convert dimensions to dimensional constraints",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// A tool's tooltip: its name, its description when one is known, and the
|
||||
/// command it runs.
|
||||
fn tool_tip_text(label: &str, id: &str) -> String {
|
||||
match tool_description(id) {
|
||||
Some(description) => format!("{label}\n{}\n{} {id}", t!(description), t!("Command:")),
|
||||
None => format!("{label}\n{} {id}", t!("Command:")),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn tip_style(theme: &Theme) -> container::Style {
|
||||
let palette = theme.palette();
|
||||
container::Style {
|
||||
@@ -511,7 +535,7 @@ pub(super) fn render_small<'a>(
|
||||
let active = is_active_tool(t.id, active_tool, &state);
|
||||
let event = t.event.clone();
|
||||
let tool_id = t.id.to_string();
|
||||
let tip_text = format!("{}\n{} {}", t!(t.label), t!("Command:"), t.id);
|
||||
let tip_text = tool_tip_text(&t!(t.label), t.id);
|
||||
let btn = button(make_icon(t.icon, SMALL_ICON))
|
||||
.on_press(Message::RibbonToolClick { tool_id, event })
|
||||
.style(move |theme: &Theme, status| tool_btn_style(theme, active, status))
|
||||
@@ -530,7 +554,7 @@ pub(super) fn render_small<'a>(
|
||||
let event = t.event.clone();
|
||||
let tool_id = t.id.to_string();
|
||||
let label = t!(t.label).into_owned();
|
||||
let tip_text = format!("{}\n{} {}", label, t!("Command:"), t.id);
|
||||
let tip_text = tool_tip_text(&label, t.id);
|
||||
let content = row![
|
||||
container(make_icon(t.icon, SMALL_ICON)).width(Length::Fixed(SMALL_W)),
|
||||
text(label).size(10).wrapping(advanced_text::Wrapping::None),
|
||||
@@ -588,7 +612,7 @@ pub(super) fn render_small<'a>(
|
||||
})
|
||||
.or_else(|| items.first().map(|(_, lbl, _)| *lbl))
|
||||
.unwrap_or(*id);
|
||||
let tip_text = format!("{}\n{} {}", t!(cur_label), t!("Command:"), last);
|
||||
let tip_text = tool_tip_text(&t!(cur_label), last);
|
||||
|
||||
let icon_btn = button(make_icon(cur_icon, SMALL_ICON))
|
||||
.on_press(Message::RibbonToolClick {
|
||||
@@ -658,7 +682,7 @@ pub(super) fn render_small<'a>(
|
||||
.on_press(Message::ToggleRibbonDropdown(id.to_string()))
|
||||
.style(move |theme: &Theme, status| tool_btn_style(theme, dd_open, status))
|
||||
.width(Length::Fixed(ARROW_W)).height(ROW_H).padding(0);
|
||||
let face_tip = format!("{}\n{} {}", localized_label, t!("Command:"), last);
|
||||
let face_tip = tool_tip_text(&localized_label, last);
|
||||
let arrow_tip = format!("{} {}", localized_label, t!("options"));
|
||||
PosReport::new(*id, row![
|
||||
tooltip(face_btn, make_tip(face_tip), TipPos::Right)
|
||||
@@ -727,7 +751,7 @@ pub(super) fn render_large_dropdown<'a>(
|
||||
.or_else(|| items.first().map(|(_, lbl, _)| *lbl))
|
||||
.unwrap_or(id);
|
||||
let label = t!(explicit_label.unwrap_or(cur_label)).into_owned();
|
||||
let tip_text = format!("{}\n{} {}", t!(cur_label), t!("Command:"), last);
|
||||
let tip_text = tool_tip_text(&t!(cur_label), last);
|
||||
let arr_tip = format!("{} {}", label, t!("options"));
|
||||
|
||||
// The label owns the bottom of the face. The icon's Fill container centers
|
||||
@@ -864,7 +888,7 @@ pub(super) fn render_large<'a>(
|
||||
let event = t.event.clone();
|
||||
let tool_id = t.id.to_string();
|
||||
let label = t!(t.label).into_owned();
|
||||
let tip_text = format!("{}\n{} {}", label, t!("Command:"), t.id);
|
||||
let tip_text = tool_tip_text(&label, t.id);
|
||||
let btn = button(
|
||||
column![
|
||||
container(make_icon(t.icon, LARGE_ICON))
|
||||
|
||||
Reference in new issue
Block a user