forked from 1b-agent/opencadstudio-upstream
Merge pull request #1332 from ramox81/codex/concentric-constraint-parity
# Conflicts: # src/entities/arc.rs # src/entities/ellipse.rs # src/modules/parametric/mod.rs # src/scene/parametric_solve.rs # src/ui/overlay.rs
This commit is contained in:
commit
16c417f81d
17 files changed
+692
-52
No files matched your search
Generated
+2
-2
@@ -921,7 +921,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "cadkernel"
|
name = "cadkernel"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=7e43598#7e435984d9ba32999d2471930c18449a2ab58d67"
|
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=4815518#481551836494e581279fdbfbf8114921170ee75b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"acadrust",
|
"acadrust",
|
||||||
"cavalier_contours",
|
"cavalier_contours",
|
||||||
@@ -932,7 +932,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "cadkernel-constraints"
|
name = "cadkernel-constraints"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=7e43598#7e435984d9ba32999d2471930c18449a2ab58d67"
|
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=4815518#481551836494e581279fdbfbf8114921170ee75b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nalgebra",
|
"nalgebra",
|
||||||
]
|
]
|
||||||
|
|||||||
+2
-2
@@ -34,8 +34,8 @@ clap = { version = "4", features = ["derive", "string"] }
|
|||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "8a28c21", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "8a28c21", features = ["serde"] }
|
||||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "7e43598", features = ["acis", "offset"] }
|
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "4815518", features = ["acis", "offset"] }
|
||||||
cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "7e43598" }
|
cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "4815518" }
|
||||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||||
|
|||||||
@@ -3372,6 +3372,88 @@ impl OpenCADStudio {
|
|||||||
self.commit_undo_delta(i, pd);
|
self.commit_undo_delta(i, pd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
CmdResult::AddConcentricConstraint {
|
||||||
|
first,
|
||||||
|
second,
|
||||||
|
label,
|
||||||
|
} => {
|
||||||
|
use crate::scene::parametric_constraints::ConstraintKind;
|
||||||
|
|
||||||
|
let refs = vec![first, second];
|
||||||
|
if let Err(message) = self.tabs[i].scene.validate_parametric_constraint(
|
||||||
|
ConstraintKind::Concentric,
|
||||||
|
&refs,
|
||||||
|
None,
|
||||||
|
) {
|
||||||
|
self.tabs[i].active_cmd = None;
|
||||||
|
self.tabs[i].snap_result = None;
|
||||||
|
self.command_line.push_error(message);
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
let scope = self.tabs[i].current_parametric_scope();
|
||||||
|
let duplicate = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.parametric_constraint_set(scope)
|
||||||
|
.is_some_and(|set| {
|
||||||
|
set.constraints.iter().any(|constraint| {
|
||||||
|
constraint.enabled
|
||||||
|
&& constraint.kind == ConstraintKind::Concentric
|
||||||
|
&& matches!(constraint.refs.as_slice(), [a, b]
|
||||||
|
if (*a == first && *b == second)
|
||||||
|
|| (*a == second && *b == first))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if duplicate {
|
||||||
|
self.tabs[i].active_cmd = None;
|
||||||
|
self.tabs[i].snap_result = None;
|
||||||
|
self.command_line
|
||||||
|
.push_output("The Concentric constraint already exists.");
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
let constraints_before = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.parametric_constraint_set(scope)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
crate::scene::parametric_constraints::ParametricConstraintSet::new(scope)
|
||||||
|
});
|
||||||
|
let touched = if first.entity == second.entity {
|
||||||
|
vec![first.entity]
|
||||||
|
} else {
|
||||||
|
vec![first.entity, second.entity]
|
||||||
|
};
|
||||||
|
let pending = self.begin_undo(i, label, touched.len(), true);
|
||||||
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.record_undo_parametric_constraints_before(scope, constraints_before);
|
||||||
|
let id = self.tabs[i].scene.parametric_constraint_set_mut(scope).add(
|
||||||
|
ConstraintKind::Concentric,
|
||||||
|
refs,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
self.tabs[i].scene.note_parametric_constraint_applied(
|
||||||
|
scope,
|
||||||
|
id,
|
||||||
|
self.constraint_bar_display,
|
||||||
|
);
|
||||||
|
let changes = touched
|
||||||
|
.into_iter()
|
||||||
|
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
self.tabs[i].scene.bump_entities_with_initial_parametric_policy(
|
||||||
|
&changes,
|
||||||
|
&[first],
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
self.tabs[i].active_cmd = None;
|
||||||
|
self.tabs[i].snap_result = None;
|
||||||
|
self.command_line.push_output("Constraint applied.");
|
||||||
|
self.refresh_properties();
|
||||||
|
if let Some(pd) = pending {
|
||||||
|
self.commit_undo_delta(i, pd);
|
||||||
|
}
|
||||||
|
}
|
||||||
CmdResult::AddCoincidentConstraint {
|
CmdResult::AddCoincidentConstraint {
|
||||||
first,
|
first,
|
||||||
second,
|
second,
|
||||||
|
|||||||
+32
-20
@@ -1496,29 +1496,41 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"NCONSTRAINT" => {
|
"NCONSTRAINT" | "GCCONCENTRIC" => {
|
||||||
|
use crate::command::CmdResult;
|
||||||
|
use crate::modules::parametric::ConcentricConstraintCommand;
|
||||||
|
|
||||||
let handles = self.tabs[i].scene.selected_handles_in_order();
|
let handles = self.tabs[i].scene.selected_handles_in_order();
|
||||||
if handles.is_empty() {
|
if handles.is_empty() {
|
||||||
use crate::modules::draw::select::SelectObjectsCommand;
|
let command = ConcentricConstraintCommand::new();
|
||||||
let sel = SelectObjectsCommand::new(cmd);
|
self.command_line.push_info(&command.prompt());
|
||||||
self.command_line.push_info(&sel.prompt());
|
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||||
self.tabs[i].active_cmd = Some(Box::new(sel));
|
|
||||||
} else if handles.len() != 2 {
|
|
||||||
self.command_line.push_output(
|
|
||||||
"Select exactly two circles/arcs, then run this constraint again.",
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
use crate::command::CmdResult;
|
let refs = handles
|
||||||
use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef};
|
.iter()
|
||||||
return Some(self.apply_cmd_result(CmdResult::AddParametricConstraint {
|
.filter_map(|handle| {
|
||||||
kind: ConstraintKind::Concentric,
|
let entity = self.tabs[i].scene.document.get_entity(*handle)?;
|
||||||
refs: vec![
|
ConcentricConstraintCommand::preselected_reference(entity, *handle)
|
||||||
ParametricRef::center(handles[0]),
|
})
|
||||||
ParametricRef::center(handles[1]),
|
.collect::<Vec<_>>();
|
||||||
],
|
if handles.len() == 2 && refs.len() == 2 && refs[0] != refs[1] {
|
||||||
driving_param: None,
|
return Some(self.apply_cmd_result(CmdResult::AddConcentricConstraint {
|
||||||
label: "Concentric constraint",
|
first: refs[0],
|
||||||
}));
|
second: refs[1],
|
||||||
|
label: "Concentric constraint",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
self.tabs[i].scene.deselect_all();
|
||||||
|
self.command_line.push_error(
|
||||||
|
"Invalid selection for Concentric. Select a circle, arc, ellipse or polyline arc segment.",
|
||||||
|
);
|
||||||
|
let command = refs
|
||||||
|
.first()
|
||||||
|
.copied()
|
||||||
|
.map(ConcentricConstraintCommand::with_first)
|
||||||
|
.unwrap_or_else(ConcentricConstraintCommand::new);
|
||||||
|
self.command_line.push_info(&command.prompt());
|
||||||
|
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -834,6 +834,23 @@ bg={bg_ms:.1}ms n={view_count}"
|
|||||||
.active_cmd
|
.active_cmd
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|cmd| !cmd.needs_entity_pick() && !cmd.is_selection_gathering());
|
.is_some_and(|cmd| !cmd.needs_entity_pick() && !cmd.is_selection_gathering());
|
||||||
|
let constraint_cursor_badge = if is_paper {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
tab.scene.hover_highlight.and_then(|handle| {
|
||||||
|
tab.scene
|
||||||
|
.parametric_constraint_set(tab.current_parametric_scope())
|
||||||
|
.and_then(|set| {
|
||||||
|
set.constraints_touching(handle)
|
||||||
|
.find(|constraint| {
|
||||||
|
constraint.kind
|
||||||
|
== crate::scene::parametric_constraints::ConstraintKind::Concentric
|
||||||
|
})
|
||||||
|
.or_else(|| set.constraints_touching(handle).next())
|
||||||
|
})
|
||||||
|
.map(|constraint| constraint.kind.glyph_symbol().to_string())
|
||||||
|
})
|
||||||
|
};
|
||||||
let constraint_glyphs: Vec<(
|
let constraint_glyphs: Vec<(
|
||||||
iced::Point,
|
iced::Point,
|
||||||
[f32; 2],
|
[f32; 2],
|
||||||
@@ -912,6 +929,7 @@ bg={bg_ms:.1}ms n={view_count}"
|
|||||||
constraint_glyphs,
|
constraint_glyphs,
|
||||||
self.constraint_glyph_tooltip
|
self.constraint_glyph_tooltip
|
||||||
.map(|kind| crate::t!(kind.label()).into_owned()),
|
.map(|kind| crate::t!(kind.label()).into_owned()),
|
||||||
|
constraint_cursor_badge,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1512,6 +1512,13 @@ pub enum CmdResult {
|
|||||||
second: crate::scene::parametric_constraints::ParametricRef,
|
second: crate::scene::parametric_constraints::ParametricRef,
|
||||||
label: &'static str,
|
label: &'static str,
|
||||||
},
|
},
|
||||||
|
/// Adds an ordered concentric relation. The first picked center remains
|
||||||
|
/// fixed during the initial solve while the second curve moves rigidly.
|
||||||
|
AddConcentricConstraint {
|
||||||
|
first: crate::scene::parametric_constraints::ParametricRef,
|
||||||
|
second: crate::scene::parametric_constraints::ParametricRef,
|
||||||
|
label: &'static str,
|
||||||
|
},
|
||||||
/// Opens the Auto Constrain settings dialog from the selection prompt.
|
/// Opens the Auto Constrain settings dialog from the selection prompt.
|
||||||
OpenAutoConstrainSettings,
|
OpenAutoConstrainSettings,
|
||||||
/// Adds an ordered Coincident relation. Point/point selections create a
|
/// Adds an ordered Coincident relation. Point/point selections create a
|
||||||
@@ -2778,6 +2785,7 @@ mod constraint_registry_tests {
|
|||||||
"ECONSTRAINT",
|
"ECONSTRAINT",
|
||||||
"TCONSTRAINT",
|
"TCONSTRAINT",
|
||||||
"NCONSTRAINT",
|
"NCONSTRAINT",
|
||||||
|
"GCCONCENTRIC",
|
||||||
"NRCONSTRAINT",
|
"NRCONSTRAINT",
|
||||||
"LCONSTRAINT",
|
"LCONSTRAINT",
|
||||||
"FXCONSTRAINT",
|
"FXCONSTRAINT",
|
||||||
|
|||||||
+21
-3
@@ -212,9 +212,9 @@ fn properties(arc: &Arc) -> Vec<PropSection> {
|
|||||||
ro(t!("Total angle").as_ref(), "total_angle", format_angle(total_angle.to_radians())),
|
ro(t!("Total angle").as_ref(), "total_angle", format_angle(total_angle.to_radians())),
|
||||||
ro(t!("Arc length").as_ref(), "arc_length", format_length(arc_length)),
|
ro(t!("Arc length").as_ref(), "arc_length", format_length(arc_length)),
|
||||||
ro(t!("Area").as_ref(), "area", format_area(area)),
|
ro(t!("Area").as_ref(), "area", format_area(area)),
|
||||||
ro(t!("Normal X").as_ref(), "normal_x", format!("{:.4}", arc.normal.x)),
|
edit(t!("Normal X").as_ref(), "normal_x", arc.normal.x),
|
||||||
ro(t!("Normal Y").as_ref(), "normal_y", format!("{:.4}", arc.normal.y)),
|
edit(t!("Normal Y").as_ref(), "normal_y", arc.normal.y),
|
||||||
ro(t!("Normal Z").as_ref(), "normal_z", format!("{:.4}", arc.normal.z)),
|
edit(t!("Normal Z").as_ref(), "normal_z", arc.normal.z),
|
||||||
],
|
],
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
@@ -244,6 +244,24 @@ fn apply_geom_prop(arc: &mut Arc, field: &str, value: &str) {
|
|||||||
"radius" if v > 0.0 => arc.radius = v,
|
"radius" if v > 0.0 => arc.radius = v,
|
||||||
"start_angle" => arc.start_angle = v.to_radians(),
|
"start_angle" => arc.start_angle = v.to_radians(),
|
||||||
"end_angle" => arc.end_angle = v.to_radians(),
|
"end_angle" => arc.end_angle = v.to_radians(),
|
||||||
|
"normal_x" | "normal_y" | "normal_z" => {
|
||||||
|
let center = arc.center_wcs();
|
||||||
|
let mut normal = glam::DVec3::new(arc.normal.x, arc.normal.y, arc.normal.z);
|
||||||
|
match field {
|
||||||
|
"normal_x" => normal.x = v,
|
||||||
|
"normal_y" => normal.y = v,
|
||||||
|
"normal_z" => normal.z = v,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
if let Some(normal) = normal.try_normalize() {
|
||||||
|
arc.normal = acadrust::types::Vector3::new(normal.x, normal.y, normal.z);
|
||||||
|
let (x, y, z) = crate::scene::view::transform::wcs_point_to_ocs(
|
||||||
|
(center.x, center.y, center.z),
|
||||||
|
(normal.x, normal.y, normal.z),
|
||||||
|
);
|
||||||
|
arc.center = acadrust::types::Vector3::new(x, y, z);
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-4
@@ -3,7 +3,7 @@ use crate::t;
|
|||||||
|
|
||||||
use crate::command::EntityTransform;
|
use crate::command::EntityTransform;
|
||||||
use crate::entities::common::{
|
use crate::entities::common::{
|
||||||
center_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip,
|
center_grip, edit_prop as edit, parse_f64, square_grip,
|
||||||
};
|
};
|
||||||
use crate::entities::traits::RenderConvertible;
|
use crate::entities::traits::RenderConvertible;
|
||||||
use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, RenderObject};
|
use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, RenderObject};
|
||||||
@@ -169,9 +169,9 @@ fn properties(circle: &Circle) -> Vec<PropSection> {
|
|||||||
edit(t!("Diameter").as_ref(), "diameter", r * 2.0),
|
edit(t!("Diameter").as_ref(), "diameter", r * 2.0),
|
||||||
edit(t!("Circumference").as_ref(), "circumference", 2.0 * PI * r),
|
edit(t!("Circumference").as_ref(), "circumference", 2.0 * PI * r),
|
||||||
edit(t!("Area").as_ref(), "area", PI * r * r),
|
edit(t!("Area").as_ref(), "area", PI * r * r),
|
||||||
ro(t!("Normal X").as_ref(), "normal_x", format!("{:.4}", circle.normal.x)),
|
edit(t!("Normal X").as_ref(), "normal_x", circle.normal.x),
|
||||||
ro(t!("Normal Y").as_ref(), "normal_y", format!("{:.4}", circle.normal.y)),
|
edit(t!("Normal Y").as_ref(), "normal_y", circle.normal.y),
|
||||||
ro(t!("Normal Z").as_ref(), "normal_z", format!("{:.4}", circle.normal.z)),
|
edit(t!("Normal Z").as_ref(), "normal_z", circle.normal.z),
|
||||||
],
|
],
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
@@ -202,6 +202,24 @@ fn apply_geom_prop(circle: &mut Circle, field: &str, value: &str) {
|
|||||||
"diameter" if v > 0.0 => circle.radius = v / 2.0,
|
"diameter" if v > 0.0 => circle.radius = v / 2.0,
|
||||||
"circumference" if v > 0.0 => circle.radius = v / (2.0 * PI),
|
"circumference" if v > 0.0 => circle.radius = v / (2.0 * PI),
|
||||||
"area" if v > 0.0 => circle.radius = (v / PI).sqrt(),
|
"area" if v > 0.0 => circle.radius = (v / PI).sqrt(),
|
||||||
|
"normal_x" | "normal_y" | "normal_z" => {
|
||||||
|
let center = circle.center_wcs();
|
||||||
|
let mut normal = glam::DVec3::new(circle.normal.x, circle.normal.y, circle.normal.z);
|
||||||
|
match field {
|
||||||
|
"normal_x" => normal.x = v,
|
||||||
|
"normal_y" => normal.y = v,
|
||||||
|
"normal_z" => normal.z = v,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
if let Some(normal) = normal.try_normalize() {
|
||||||
|
circle.normal = acadrust::types::Vector3::new(normal.x, normal.y, normal.z);
|
||||||
|
let (x, y, z) = crate::scene::view::transform::wcs_point_to_ocs(
|
||||||
|
(center.x, center.y, center.z),
|
||||||
|
(normal.x, normal.y, normal.z),
|
||||||
|
);
|
||||||
|
circle.center = acadrust::types::Vector3::new(x, y, z);
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-3
@@ -187,9 +187,9 @@ fn properties(ell: &Ellipse) -> Vec<PropSection> {
|
|||||||
ro(t!("Start parameter").as_ref(), "start_param", format!("{:.4}", ell.start_parameter)),
|
ro(t!("Start parameter").as_ref(), "start_param", format!("{:.4}", ell.start_parameter)),
|
||||||
ro(t!("End parameter").as_ref(), "end_param", format!("{:.4}", ell.end_parameter)),
|
ro(t!("End parameter").as_ref(), "end_param", format!("{:.4}", ell.end_parameter)),
|
||||||
ro(t!("Length").as_ref(), "length", format_length(props.perimeter)),
|
ro(t!("Length").as_ref(), "length", format_length(props.perimeter)),
|
||||||
ro(t!("Normal X").as_ref(), "normal_x", format!("{:.4}", ell.normal.x)),
|
edit(t!("Normal X").as_ref(), "normal_x", ell.normal.x),
|
||||||
ro(t!("Normal Y").as_ref(), "normal_y", format!("{:.4}", ell.normal.y)),
|
edit(t!("Normal Y").as_ref(), "normal_y", ell.normal.y),
|
||||||
ro(t!("Normal Z").as_ref(), "normal_z", format!("{:.4}", ell.normal.z)),
|
edit(t!("Normal Z").as_ref(), "normal_z", ell.normal.z),
|
||||||
],
|
],
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
@@ -220,6 +220,38 @@ fn apply_geom_prop(ell: &mut Ellipse, field: &str, value: &str) {
|
|||||||
"ratio" if v > 0.0 => ell.minor_axis_ratio = v,
|
"ratio" if v > 0.0 => ell.minor_axis_ratio = v,
|
||||||
"start_angle" => ell.start_parameter = v.to_radians(),
|
"start_angle" => ell.start_parameter = v.to_radians(),
|
||||||
"end_angle" => ell.end_parameter = v.to_radians(),
|
"end_angle" => ell.end_parameter = v.to_radians(),
|
||||||
|
"normal_x" | "normal_y" | "normal_z" => {
|
||||||
|
let mut normal = glam::DVec3::new(ell.normal.x, ell.normal.y, ell.normal.z);
|
||||||
|
match field {
|
||||||
|
"normal_x" => normal.x = v,
|
||||||
|
"normal_y" => normal.y = v,
|
||||||
|
"normal_z" => normal.z = v,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
if let Some(normal) = normal.try_normalize() {
|
||||||
|
let current = glam::DVec3::new(
|
||||||
|
ell.major_axis.x,
|
||||||
|
ell.major_axis.y,
|
||||||
|
ell.major_axis.z,
|
||||||
|
);
|
||||||
|
let length = current.length();
|
||||||
|
let projected = current - normal * current.dot(normal);
|
||||||
|
let major = projected.try_normalize().or_else(|| {
|
||||||
|
let helper = if normal.x.abs() < 0.8 {
|
||||||
|
glam::DVec3::X
|
||||||
|
} else {
|
||||||
|
glam::DVec3::Y
|
||||||
|
};
|
||||||
|
normal.cross(helper).try_normalize()
|
||||||
|
});
|
||||||
|
if let Some(major) = major {
|
||||||
|
ell.normal = acadrust::types::Vector3::new(normal.x, normal.y, normal.z);
|
||||||
|
let major = major * length;
|
||||||
|
ell.major_axis =
|
||||||
|
acadrust::types::Vector3::new(major.x, major.y, major.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
use acadrust::{EntityType, Handle};
|
||||||
|
use glam::DVec3;
|
||||||
|
|
||||||
|
use crate::command::{CadCommand, CmdResult};
|
||||||
|
use crate::scene::parametric_constraints::ParametricRef;
|
||||||
|
use crate::t;
|
||||||
|
|
||||||
|
pub struct ConcentricConstraintCommand {
|
||||||
|
first: Option<ParametricRef>,
|
||||||
|
picked_entity: Option<EntityType>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConcentricConstraintCommand {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
first: None,
|
||||||
|
picked_entity: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_first(first: ParametricRef) -> Self {
|
||||||
|
Self {
|
||||||
|
first: Some(first),
|
||||||
|
picked_entity: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalid_selection() -> CmdResult {
|
||||||
|
CmdResult::ReportError(
|
||||||
|
t!("Invalid selection for Concentric. Select a circle, arc, ellipse or polyline arc segment.")
|
||||||
|
.into_owned(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn polyline_segment_is_arc(entity: &EntityType, index: usize) -> bool {
|
||||||
|
match entity {
|
||||||
|
EntityType::LwPolyline(polyline) => polyline
|
||||||
|
.vertices
|
||||||
|
.get(index)
|
||||||
|
.is_some_and(|vertex| vertex.bulge.abs() > 1.0e-12),
|
||||||
|
EntityType::Polyline2D(polyline) => polyline
|
||||||
|
.vertices
|
||||||
|
.get(index)
|
||||||
|
.is_some_and(|vertex| vertex.bulge.abs() > 1.0e-12),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn picked_reference(entity: &EntityType, handle: Handle, point: DVec3) -> Option<ParametricRef> {
|
||||||
|
match entity {
|
||||||
|
EntityType::Circle(_) | EntityType::Arc(_) | EntityType::Ellipse(_) => {
|
||||||
|
Some(ParametricRef::center(handle))
|
||||||
|
}
|
||||||
|
EntityType::LwPolyline(_) | EntityType::Polyline2D(_) => {
|
||||||
|
let planar = crate::entities::curve::entity_curve(entity)?;
|
||||||
|
let local = planar.plane.project(point.to_array())?;
|
||||||
|
let segments = planar.curve.segments();
|
||||||
|
let (index, _) = cadkernel::geom2d::nearest_of(segments.iter(), local)?;
|
||||||
|
matches!(segments.get(index), Some(cadkernel::geom2d::Curve::Arc(_)))
|
||||||
|
.then(|| ParametricRef::segment_center(handle, index))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn preselected_reference(entity: &EntityType, handle: Handle) -> Option<ParametricRef> {
|
||||||
|
match entity {
|
||||||
|
EntityType::Circle(_) | EntityType::Arc(_) | EntityType::Ellipse(_) => {
|
||||||
|
Some(ParametricRef::center(handle))
|
||||||
|
}
|
||||||
|
EntityType::LwPolyline(polyline) => {
|
||||||
|
let segment_count = if polyline.is_closed {
|
||||||
|
polyline.vertices.len()
|
||||||
|
} else {
|
||||||
|
polyline.vertices.len().saturating_sub(1)
|
||||||
|
};
|
||||||
|
(segment_count == 1 && Self::polyline_segment_is_arc(entity, 0))
|
||||||
|
.then(|| ParametricRef::segment_center(handle, 0))
|
||||||
|
}
|
||||||
|
EntityType::Polyline2D(polyline) => {
|
||||||
|
let segment_count = if polyline.is_closed() {
|
||||||
|
polyline.vertices.len()
|
||||||
|
} else {
|
||||||
|
polyline.vertices.len().saturating_sub(1)
|
||||||
|
};
|
||||||
|
(segment_count == 1 && Self::polyline_segment_is_arc(entity, 0))
|
||||||
|
.then(|| ParametricRef::segment_center(handle, 0))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CadCommand for ConcentricConstraintCommand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"GCCONCENTRIC"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt(&self) -> String {
|
||||||
|
if self.first.is_some() {
|
||||||
|
t!("GCCONCENTRIC Select second object:").into_owned()
|
||||||
|
} else {
|
||||||
|
t!("GCCONCENTRIC Select first object:").into_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn needs_entity_pick(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entity_pick_highlights_hover(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inject_before_entity_pick(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inject_picked_entity(&mut self, entity: EntityType) {
|
||||||
|
self.picked_entity = Some(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult {
|
||||||
|
if handle.is_null() {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
}
|
||||||
|
let Some(entity) = self.picked_entity.take() else {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
};
|
||||||
|
let Some(reference) = Self::picked_reference(&entity, handle, point) else {
|
||||||
|
return Self::invalid_selection();
|
||||||
|
};
|
||||||
|
let Some(first) = self.first else {
|
||||||
|
self.first = Some(reference);
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
};
|
||||||
|
if first == reference {
|
||||||
|
return Self::invalid_selection();
|
||||||
|
}
|
||||||
|
CmdResult::AddConcentricConstraint {
|
||||||
|
first,
|
||||||
|
second: reference,
|
||||||
|
label: "Concentric constraint",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_point(&mut self, _point: DVec3) -> CmdResult {
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
|
CmdResult::Cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_escape(&mut self) -> CmdResult {
|
||||||
|
CmdResult::Cancel
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ impl GeomConstraintCommand {
|
|||||||
"T" | "TANGENT" => "TCONSTRAINT",
|
"T" | "TANGENT" => "TCONSTRAINT",
|
||||||
"SM" | "SMOOTH" => "GCSMOOTH",
|
"SM" | "SMOOTH" => "GCSMOOTH",
|
||||||
"C" | "COINCIDENT" => "GCCOINCIDENT",
|
"C" | "COINCIDENT" => "GCCOINCIDENT",
|
||||||
"CON" | "CONCENTRIC" => "NCONSTRAINT",
|
"CON" | "CONCENTRIC" => "GCCONCENTRIC",
|
||||||
"COL" | "COLLINEAR" | "COLINEAR" => "LCONSTRAINT",
|
"COL" | "COLLINEAR" | "COLINEAR" => "LCONSTRAINT",
|
||||||
"SY" | "SYMMETRIC" => "SYCONSTRAINT",
|
"SY" | "SYMMETRIC" => "SYCONSTRAINT",
|
||||||
"E" | "EQUAL" => "ECONSTRAINT",
|
"E" | "EQUAL" => "ECONSTRAINT",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
//! resulting constraints through the geometry kernel.
|
//! resulting constraints through the geometry kernel.
|
||||||
|
|
||||||
mod coincident;
|
mod coincident;
|
||||||
|
mod concentric;
|
||||||
mod equal_distance;
|
mod equal_distance;
|
||||||
mod geom_constraint;
|
mod geom_constraint;
|
||||||
#[path = "perpendicular.rs"]
|
#[path = "perpendicular.rs"]
|
||||||
@@ -14,6 +15,7 @@ mod tangent_command;
|
|||||||
mod tools;
|
mod tools;
|
||||||
mod value;
|
mod value;
|
||||||
pub use coincident::{coincident_tool, CoincidentConstraintCommand};
|
pub use coincident::{coincident_tool, CoincidentConstraintCommand};
|
||||||
|
pub use concentric::ConcentricConstraintCommand;
|
||||||
pub use equal_distance::{equal_distance_tool, EqualDistanceConstraintCommand};
|
pub use equal_distance::{equal_distance_tool, EqualDistanceConstraintCommand};
|
||||||
pub use geom_constraint::GeomConstraintCommand;
|
pub use geom_constraint::GeomConstraintCommand;
|
||||||
pub use perpendicular_command::{PerpendicularConstraintCommand, PerpendicularPick};
|
pub use perpendicular_command::{PerpendicularConstraintCommand, PerpendicularPick};
|
||||||
@@ -23,8 +25,8 @@ pub use point_on_entity::{
|
|||||||
pub use smooth::SmoothConstraintCommand;
|
pub use smooth::SmoothConstraintCommand;
|
||||||
pub use tangent_command::TangentConstraintCommand;
|
pub use tangent_command::TangentConstraintCommand;
|
||||||
pub use tools::{
|
pub use tools::{
|
||||||
colinear, concentric, equal, fixed, horizontal, normal, parallel, perpendicular, symmetric,
|
colinear, concentric as concentric_tool, equal, fixed, horizontal, normal, parallel,
|
||||||
tangent, vertical,
|
perpendicular, symmetric, tangent, vertical,
|
||||||
};
|
};
|
||||||
pub use value::{
|
pub use value::{
|
||||||
angle_tool, dimensional_tools, distance_tool, AngleConstraintCommand,
|
angle_tool, dimensional_tools, distance_tool, AngleConstraintCommand,
|
||||||
@@ -67,7 +69,7 @@ impl CadModule for ParametricModule {
|
|||||||
"GCSMOOTH", "Smooth",
|
"GCSMOOTH", "Smooth",
|
||||||
include_bytes!("../../../assets/icons/constrain/smooth.svg"),
|
include_bytes!("../../../assets/icons/constrain/smooth.svg"),
|
||||||
)),
|
)),
|
||||||
RibbonItem::LargeTool(concentric::tool()),
|
RibbonItem::LargeTool(concentric_tool::tool()),
|
||||||
RibbonItem::LargeTool(horizontal::tool()),
|
RibbonItem::LargeTool(horizontal::tool()),
|
||||||
RibbonItem::LargeTool(symmetric::tool()),
|
RibbonItem::LargeTool(symmetric::tool()),
|
||||||
RibbonItem::LargeTool(fixed::tool()),
|
RibbonItem::LargeTool(fixed::tool()),
|
||||||
@@ -170,7 +172,7 @@ mod tests {
|
|||||||
groups[0].tools.iter().map(item_id).collect::<Vec<_>>(),
|
groups[0].tools.iter().map(item_id).collect::<Vec<_>>(),
|
||||||
[
|
[
|
||||||
"AUTOCONSTRAIN", "CCONSTRAINT", "PCONSTRAINT", "TCONSTRAINT",
|
"AUTOCONSTRAIN", "CCONSTRAINT", "PCONSTRAINT", "TCONSTRAINT",
|
||||||
"LCONSTRAINT", "QCONSTRAINT", "GCSMOOTH", "NCONSTRAINT",
|
"LCONSTRAINT", "QCONSTRAINT", "GCSMOOTH", "GCCONCENTRIC",
|
||||||
"HCONSTRAINT", "SYCONSTRAINT", "FXCONSTRAINT", "VCONSTRAINT",
|
"HCONSTRAINT", "SYCONSTRAINT", "FXCONSTRAINT", "VCONSTRAINT",
|
||||||
"ECONSTRAINT", "GCVISIBILITY", "GCSHOWALL", "GCHIDEALL",
|
"ECONSTRAINT", "GCVISIBILITY", "GCSHOWALL", "GCHIDEALL",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -88,12 +88,12 @@ pub mod concentric {
|
|||||||
use super::*;
|
use super::*;
|
||||||
pub fn tool() -> ToolDef {
|
pub fn tool() -> ToolDef {
|
||||||
ToolDef {
|
ToolDef {
|
||||||
id: "NCONSTRAINT",
|
id: "GCCONCENTRIC",
|
||||||
label: "Concentric",
|
label: "Concentric",
|
||||||
icon: IconKind::Svg(include_bytes!(
|
icon: IconKind::Svg(include_bytes!(
|
||||||
"../../../assets/icons/constrain/concentric.svg"
|
"../../../assets/icons/constrain/concentric.svg"
|
||||||
)),
|
)),
|
||||||
event: ModuleEvent::Command("NCONSTRAINT".to_string()),
|
event: ModuleEvent::Command("GCCONCENTRIC".to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,6 +168,7 @@ inventory::submit!(crate::command::CommandRegistration {
|
|||||||
"ECONSTRAINT",
|
"ECONSTRAINT",
|
||||||
"TCONSTRAINT",
|
"TCONSTRAINT",
|
||||||
"NCONSTRAINT",
|
"NCONSTRAINT",
|
||||||
|
"GCCONCENTRIC",
|
||||||
"NRCONSTRAINT",
|
"NRCONSTRAINT",
|
||||||
"LCONSTRAINT",
|
"LCONSTRAINT",
|
||||||
"FXCONSTRAINT",
|
"FXCONSTRAINT",
|
||||||
|
|||||||
@@ -454,11 +454,17 @@ impl<'a> GroupBuilder<'a> {
|
|||||||
return Some(*existing);
|
return Some(*existing);
|
||||||
}
|
}
|
||||||
let segment_midpoint = ParametricRef::point(handle, marker).segment_midpoint_index();
|
let segment_midpoint = ParametricRef::point(handle, marker).segment_midpoint_index();
|
||||||
|
let segment_center = ParametricRef::point(handle, marker).segment_center_index();
|
||||||
let polyline = matches!(
|
let polyline = matches!(
|
||||||
self.document.get_entity(handle),
|
self.document.get_entity(handle),
|
||||||
Some(EntityType::LwPolyline(_) | EntityType::Polyline2D(_))
|
Some(EntityType::LwPolyline(_) | EntityType::Polyline2D(_))
|
||||||
);
|
);
|
||||||
let (curve_id, point_type) = if let Some(segment) = segment_midpoint {
|
let (curve_id, point_type) = if let Some(segment) = segment_center {
|
||||||
|
(
|
||||||
|
self.segment_node(handle, segment)?,
|
||||||
|
implicit_point_type::CENTER,
|
||||||
|
)
|
||||||
|
} else if let Some(segment) = segment_midpoint {
|
||||||
(
|
(
|
||||||
self.segment_node(handle, segment)?,
|
self.segment_node(handle, segment)?,
|
||||||
implicit_point_type::MID,
|
implicit_point_type::MID,
|
||||||
@@ -551,6 +557,9 @@ impl<'a> GroupBuilder<'a> {
|
|||||||
if r.segment_midpoint_index().is_some() {
|
if r.segment_midpoint_index().is_some() {
|
||||||
return self.point_node(r.entity, r.marker?);
|
return self.point_node(r.entity, r.marker?);
|
||||||
}
|
}
|
||||||
|
if r.segment_center_index().is_some() {
|
||||||
|
return self.point_node(r.entity, r.marker?);
|
||||||
|
}
|
||||||
if let Some(segment) = r.segment_index() {
|
if let Some(segment) = r.segment_index() {
|
||||||
return self.segment_node(r.entity, segment);
|
return self.segment_node(r.entity, segment);
|
||||||
}
|
}
|
||||||
@@ -2079,6 +2088,9 @@ pub(super) fn native_constraint_set(
|
|||||||
implicit_point_type::MID => {
|
implicit_point_type::MID => {
|
||||||
ParametricRef::segment_midpoint(curve.entity, segment)
|
ParametricRef::segment_midpoint(curve.entity, segment)
|
||||||
}
|
}
|
||||||
|
implicit_point_type::CENTER => {
|
||||||
|
ParametricRef::segment_center(curve.entity, segment)
|
||||||
|
}
|
||||||
_ => continue,
|
_ => continue,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ use acadrust::types::{Handle, Vector3};
|
|||||||
/// ordered per-entity-type point list when non-negative (0/1 = a line's
|
/// ordered per-entity-type point list when non-negative (0/1 = a line's
|
||||||
/// start/end, ...), or names a special case when negative (-3 = a
|
/// start/end, ...), or names a special case when negative (-3 = a
|
||||||
/// circle/arc's center; -2 = a bounded curve's midpoint). Polyline segment
|
/// circle/arc's center; -2 = a bounded curve's midpoint). Polyline segment
|
||||||
/// and segment-midpoint references use private negative ranges so they stay
|
/// segment-midpoint, and curved-segment-center references use private
|
||||||
/// distinct from vertex markers.
|
/// negative ranges so they stay distinct from vertex markers.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct ParametricRef {
|
pub struct ParametricRef {
|
||||||
pub entity: Handle,
|
pub entity: Handle,
|
||||||
@@ -28,6 +28,7 @@ pub struct ParametricRef {
|
|||||||
|
|
||||||
const POLYLINE_SEGMENT_MARKER_BASE: i32 = -1_000_000;
|
const POLYLINE_SEGMENT_MARKER_BASE: i32 = -1_000_000;
|
||||||
const POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE: i32 = -2_000_000;
|
const POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE: i32 = -2_000_000;
|
||||||
|
const POLYLINE_SEGMENT_CENTER_MARKER_BASE: i32 = -3_000_000;
|
||||||
const ELLIPSE_MAJOR_AXIS_MARKER: i32 = -4;
|
const ELLIPSE_MAJOR_AXIS_MARKER: i32 = -4;
|
||||||
const ELLIPSE_MINOR_AXIS_MARKER: i32 = -5;
|
const ELLIPSE_MINOR_AXIS_MARKER: i32 = -5;
|
||||||
const TEXT_BASELINE_MARKER: i32 = -6;
|
const TEXT_BASELINE_MARKER: i32 = -6;
|
||||||
@@ -88,10 +89,25 @@ impl ParametricRef {
|
|||||||
|
|
||||||
pub fn segment_midpoint_index(self) -> Option<usize> {
|
pub fn segment_midpoint_index(self) -> Option<usize> {
|
||||||
let marker = self.marker?;
|
let marker = self.marker?;
|
||||||
(marker <= POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE)
|
(marker <= POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE
|
||||||
|
&& marker > POLYLINE_SEGMENT_CENTER_MARKER_BASE)
|
||||||
.then(|| (POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE - marker) as usize)
|
.then(|| (POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE - marker) as usize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Select the center of one curved polyline segment.
|
||||||
|
pub fn segment_center(entity: Handle, index: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
entity,
|
||||||
|
marker: Some(POLYLINE_SEGMENT_CENTER_MARKER_BASE - index as i32),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn segment_center_index(self) -> Option<usize> {
|
||||||
|
let marker = self.marker?;
|
||||||
|
(marker <= POLYLINE_SEGMENT_CENTER_MARKER_BASE)
|
||||||
|
.then(|| (POLYLINE_SEGMENT_CENTER_MARKER_BASE - marker) as usize)
|
||||||
|
}
|
||||||
|
|
||||||
/// Select the displayed baseline of a Text or MText entity.
|
/// Select the displayed baseline of a Text or MText entity.
|
||||||
pub fn text_baseline(entity: Handle) -> Self {
|
pub fn text_baseline(entity: Handle) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -503,7 +519,7 @@ impl ParametricConstraintSet {
|
|||||||
/// endpoint's equivalent of `dimension_assoc::resolve_reference`, restricted
|
/// endpoint's equivalent of `dimension_assoc::resolve_reference`, restricted
|
||||||
/// to the marker conventions constraint endpoints actually use (whole-entity
|
/// to the marker conventions constraint endpoints actually use (whole-entity
|
||||||
/// `None`, an ordinary `source_points()` index, the `-3` center case, or a
|
/// `None`, an ordinary `source_points()` index, the `-3` center case, or a
|
||||||
/// bounded curve/segment midpoint).
|
/// bounded curve/segment midpoint, or a curved polyline-segment center).
|
||||||
///
|
///
|
||||||
/// Solver-side registration reads raw entity fields directly. This helper is
|
/// Solver-side registration reads raw entity fields directly. This helper is
|
||||||
/// for UI-side consumers that need the current world-space position.
|
/// for UI-side consumers that need the current world-space position.
|
||||||
@@ -524,12 +540,20 @@ pub(crate) fn resolve_point(entity: &acadrust::EntityType, marker: i32) -> Optio
|
|||||||
let point = curve.point_at(0.5);
|
let point = curve.point_at(0.5);
|
||||||
return Some(Vector3::new(point[0], point[1], point[2]));
|
return Some(Vector3::new(point[0], point[1], point[2]));
|
||||||
}
|
}
|
||||||
let segment = ParametricRef {
|
let reference = ParametricRef {
|
||||||
entity: Handle::NULL,
|
entity: Handle::NULL,
|
||||||
marker: Some(marker),
|
marker: Some(marker),
|
||||||
|
};
|
||||||
|
if let Some(segment) = reference.segment_center_index() {
|
||||||
|
let planar = crate::entities::curve::entity_curve(entity)?;
|
||||||
|
let curve = planar.curve.segments().into_iter().nth(segment)?;
|
||||||
|
let cadkernel::geom2d::Curve::Arc(arc) = curve else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let point = planar.plane.point_at(arc.centre);
|
||||||
|
return Some(Vector3::new(point[0], point[1], point[2]));
|
||||||
}
|
}
|
||||||
.segment_midpoint_index();
|
if let Some(segment) = reference.segment_midpoint_index() {
|
||||||
if let Some(segment) = segment {
|
|
||||||
let planar = crate::entities::curve::entity_curve(entity)?;
|
let planar = crate::entities::curve::entity_curve(entity)?;
|
||||||
let curve = planar.curve.segments().into_iter().nth(segment)?;
|
let curve = planar.curve.segments().into_iter().nth(segment)?;
|
||||||
let point = planar.plane.point_at(curve.point_at(0.5));
|
let point = planar.plane.point_at(curve.point_at(0.5));
|
||||||
@@ -831,6 +855,18 @@ fn glyph_placement_for_reference(
|
|||||||
let anchor = (start + end) * 0.5;
|
let anchor = (start + end) * 0.5;
|
||||||
return Some((anchor, segment_normal(start, end)));
|
return Some((anchor, segment_normal(start, end)));
|
||||||
}
|
}
|
||||||
|
if let Some(segment) = r.segment_center_index() {
|
||||||
|
let planar = crate::entities::curve::entity_curve(entity)?;
|
||||||
|
let curve = planar.curve.segments().into_iter().nth(segment)?;
|
||||||
|
let on_arc = planar.plane.point_at(curve.point_at(0.5));
|
||||||
|
let cadkernel::geom2d::Curve::Arc(arc) = curve else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let center = planar.plane.point_at(arc.centre);
|
||||||
|
let center = Vector3::new(center[0], center[1], center[2]);
|
||||||
|
let on_arc = Vector3::new(on_arc[0], on_arc[1], on_arc[2]);
|
||||||
|
return Some((on_arc, on_arc - center));
|
||||||
|
}
|
||||||
if let Some(segment) = r.segment_index() {
|
if let Some(segment) = r.segment_index() {
|
||||||
let anchor = resolve_point(
|
let anchor = resolve_point(
|
||||||
entity,
|
entity,
|
||||||
|
|||||||
@@ -513,6 +513,12 @@ fn resolve_constraint_point(
|
|||||||
if let Some(point) = geometry.point_for_marker(marker) {
|
if let Some(point) = geometry.point_for_marker(marker) {
|
||||||
return Some(point);
|
return Some(point);
|
||||||
}
|
}
|
||||||
|
if let Some(segment) = reference
|
||||||
|
.segment_center_index()
|
||||||
|
.and_then(|index| geometry.arc_segment(index))
|
||||||
|
{
|
||||||
|
return Some(segment.arc.circle.center);
|
||||||
|
}
|
||||||
|
|
||||||
let midpoint_line = reference
|
let midpoint_line = reference
|
||||||
.segment_midpoint_index()
|
.segment_midpoint_index()
|
||||||
@@ -794,6 +800,175 @@ fn refs_share_supported_plane(document: &acadrust::CadDocument, refs: &[Parametr
|
|||||||
plane_z.is_some()
|
plane_z.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn concentric_reference_geometry(
|
||||||
|
document: &acadrust::CadDocument,
|
||||||
|
reference: ParametricRef,
|
||||||
|
) -> Option<(Vector3, cadkernel::space::Plane)> {
|
||||||
|
let marker = reference.marker?;
|
||||||
|
if marker != -3 && reference.segment_center_index().is_none() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let entity = document.get_entity(reference.entity)?;
|
||||||
|
let center = super::parametric_constraints::resolve_point(entity, marker)?;
|
||||||
|
let plane = crate::entities::curve::entity_curve(entity)?.plane;
|
||||||
|
Some((center, plane))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn concentric_refs_share_plane(
|
||||||
|
document: &acadrust::CadDocument,
|
||||||
|
refs: &[ParametricRef],
|
||||||
|
) -> bool {
|
||||||
|
let [first, second] = refs else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if first == second {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let Some((first_center, first_plane)) = concentric_reference_geometry(document, *first) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Some((second_center, second_plane)) = concentric_reference_geometry(document, *second)
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let (Some(first_normal), Some(second_normal)) = (first_plane.normal(), second_plane.normal())
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let parallel = (first_normal[0] * second_normal[0]
|
||||||
|
+ first_normal[1] * second_normal[1]
|
||||||
|
+ first_normal[2] * second_normal[2])
|
||||||
|
.abs()
|
||||||
|
>= 1.0 - 1.0e-9;
|
||||||
|
let points = [
|
||||||
|
first_plane.origin,
|
||||||
|
second_plane.origin,
|
||||||
|
[first_center.x, first_center.y, first_center.z],
|
||||||
|
[second_center.x, second_center.y, second_center.z],
|
||||||
|
];
|
||||||
|
let tolerance = cadkernel::space::coplanarity_tolerance(&points);
|
||||||
|
parallel
|
||||||
|
&& first_plane
|
||||||
|
.distance_to(second_plane.origin)
|
||||||
|
.is_some_and(|distance| distance.abs() <= tolerance)
|
||||||
|
&& first_plane
|
||||||
|
.distance_to([second_center.x, second_center.y, second_center.z])
|
||||||
|
.is_some_and(|distance| distance.abs() <= tolerance)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ref_is_driven(driven: &[ParametricRef], reference: ParametricRef) -> bool {
|
||||||
|
driven.iter().any(|candidate| {
|
||||||
|
candidate.entity == reference.entity
|
||||||
|
&& (candidate.marker == reference.marker || candidate.marker.is_none())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_spatial_concentric_constraints(
|
||||||
|
document: &acadrust::CadDocument,
|
||||||
|
set: &ParametricConstraintSet,
|
||||||
|
driven_refs: &[ParametricRef],
|
||||||
|
initial_fixed_refs: &[ParametricRef],
|
||||||
|
results: &mut Vec<(Handle, EntityType)>,
|
||||||
|
) {
|
||||||
|
let current_entity = |handle: Handle, results: &[(Handle, EntityType)]| {
|
||||||
|
results
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.find(|(candidate, _)| *candidate == handle)
|
||||||
|
.map(|(_, entity)| entity.clone())
|
||||||
|
.or_else(|| document.get_entity(handle).cloned())
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut handles = Vec::<Handle>::new();
|
||||||
|
let mut relations = Vec::new();
|
||||||
|
let mut fixed = Vec::new();
|
||||||
|
let mut points = Vec::new();
|
||||||
|
for constraint in set.constraints.iter().filter(|constraint| {
|
||||||
|
constraint.enabled
|
||||||
|
&& constraint.kind == ConstraintKind::Concentric
|
||||||
|
&& !refs_share_supported_plane(document, &constraint.refs)
|
||||||
|
&& concentric_refs_share_plane(document, &constraint.refs)
|
||||||
|
}) {
|
||||||
|
let [first, second] = constraint.refs.as_slice() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if first.entity == second.entity {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (Some(first_entity), Some(second_entity)) = (
|
||||||
|
current_entity(first.entity, results),
|
||||||
|
current_entity(second.entity, results),
|
||||||
|
) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let (Some(first_marker), Some(second_marker)) = (first.marker, second.marker) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let (Some(first_center), Some(second_center)) = (
|
||||||
|
super::parametric_constraints::resolve_point(&first_entity, first_marker),
|
||||||
|
super::parametric_constraints::resolve_point(&second_entity, second_marker),
|
||||||
|
) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let first_index = handles
|
||||||
|
.iter()
|
||||||
|
.position(|handle| *handle == first.entity)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
handles.push(first.entity);
|
||||||
|
handles.len() - 1
|
||||||
|
});
|
||||||
|
let second_index = handles
|
||||||
|
.iter()
|
||||||
|
.position(|handle| *handle == second.entity)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
handles.push(second.entity);
|
||||||
|
handles.len() - 1
|
||||||
|
});
|
||||||
|
let first_point = [first_center.x, first_center.y, first_center.z];
|
||||||
|
let second_point = [second_center.x, second_center.y, second_center.z];
|
||||||
|
relations.push((first_index, first_point, second_index, second_point));
|
||||||
|
points.extend([first_point, second_point]);
|
||||||
|
if ref_is_driven(driven_refs, *first) || ref_is_driven(initial_fixed_refs, *first) {
|
||||||
|
fixed.push(first_index);
|
||||||
|
}
|
||||||
|
if ref_is_driven(driven_refs, *second) || ref_is_driven(initial_fixed_refs, *second) {
|
||||||
|
fixed.push(second_index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if relations.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fixed.sort_unstable();
|
||||||
|
fixed.dedup();
|
||||||
|
let tolerance = cadkernel::space::coplanarity_tolerance(&points).max(MOVE_EPS);
|
||||||
|
let Some(translations) = cadkernel::space::solve_rigid_point_coincidence(
|
||||||
|
handles.len(),
|
||||||
|
&relations,
|
||||||
|
&fixed,
|
||||||
|
tolerance,
|
||||||
|
) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
for (handle, translation) in handles.into_iter().zip(translations) {
|
||||||
|
let translation = glam::DVec3::from_array(translation);
|
||||||
|
if translation.length() <= tolerance {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(mut entity) = current_entity(handle, results) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
crate::scene::view::dispatch::apply_transform(
|
||||||
|
&mut entity,
|
||||||
|
&crate::command::EntityTransform::Translate(translation),
|
||||||
|
);
|
||||||
|
if let Some((_, current)) = results.iter_mut().find(|(candidate, _)| *candidate == handle) {
|
||||||
|
*current = entity;
|
||||||
|
} else {
|
||||||
|
results.push((handle, entity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn resolved_target(params: &ParameterTable, constraint: &ParametricConstraint) -> Option<f64> {
|
fn resolved_target(params: &ParameterTable, constraint: &ParametricConstraint) -> Option<f64> {
|
||||||
let value = constraint.driving_param.as_ref()?.resolve(params).ok()?;
|
let value = constraint.driving_param.as_ref()?.resolve(params).ok()?;
|
||||||
if !value.is_finite() {
|
if !value.is_finite() {
|
||||||
@@ -1982,6 +2157,7 @@ fn polyline_ref_order(
|
|||||||
}
|
}
|
||||||
let position = if let Some(index) = reference.segment_index()
|
let position = if let Some(index) = reference.segment_index()
|
||||||
.or_else(|| reference.segment_midpoint_index())
|
.or_else(|| reference.segment_midpoint_index())
|
||||||
|
.or_else(|| reference.segment_center_index())
|
||||||
{
|
{
|
||||||
2 * index + 1
|
2 * index + 1
|
||||||
} else {
|
} else {
|
||||||
@@ -2133,7 +2309,13 @@ fn solve_scope(
|
|||||||
|
|
||||||
let has_smooth = constraints.iter()
|
let has_smooth = constraints.iter()
|
||||||
.any(|constraint| constraint.enabled && constraint.kind == ConstraintKind::Smooth);
|
.any(|constraint| constraint.enabled && constraint.kind == ConstraintKind::Smooth);
|
||||||
if cache.is_empty() && !has_smooth {
|
let has_spatial_concentric = constraints.iter().any(|constraint| {
|
||||||
|
constraint.enabled
|
||||||
|
&& constraint.kind == ConstraintKind::Concentric
|
||||||
|
&& !refs_share_supported_plane(document, &constraint.refs)
|
||||||
|
&& concentric_refs_share_plane(document, &constraint.refs)
|
||||||
|
});
|
||||||
|
if cache.is_empty() && !has_smooth && !has_spatial_concentric {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3013,6 +3195,13 @@ fn solve_scope(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
apply_smooth_constraints(document, set, &mut results);
|
apply_smooth_constraints(document, set, &mut results);
|
||||||
|
apply_spatial_concentric_constraints(
|
||||||
|
document,
|
||||||
|
set,
|
||||||
|
driven_refs,
|
||||||
|
initial_fixed_refs,
|
||||||
|
&mut results,
|
||||||
|
);
|
||||||
Some((results, dof, conflicts))
|
Some((results, dof, conflicts))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3064,6 +3253,21 @@ impl Scene {
|
|||||||
}
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
if kind == ConstraintKind::Concentric {
|
||||||
|
if !concentric_refs_share_plane(&self.document, refs) {
|
||||||
|
return Err(
|
||||||
|
"Concentric requires two supported circular curves on the same plane.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !refs_share_supported_plane(&self.document, refs) {
|
||||||
|
if refs[0].entity == refs[1].entity {
|
||||||
|
return Err(
|
||||||
|
"Concentric cannot move two curved segments of the same object on this plane.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
if !refs_share_supported_plane(&self.document, refs) {
|
if !refs_share_supported_plane(&self.document, refs) {
|
||||||
return Err(
|
return Err(
|
||||||
"Constraint references must be supported entities on the same world-XY plane.",
|
"Constraint references must be supported entities on the same world-XY plane.",
|
||||||
|
|||||||
+41
-2
@@ -84,6 +84,16 @@ fn draw_tangent_constraint_glyph(
|
|||||||
frame.stroke(&canvas::Path::line(contact, tangent_end), stroke);
|
frame.stroke(&canvas::Path::line(contact, tangent_end), stroke);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn draw_concentric_constraint_glyph(
|
||||||
|
frame: &mut canvas::Frame,
|
||||||
|
center: Point,
|
||||||
|
color: Color,
|
||||||
|
) {
|
||||||
|
let stroke = canvas::Stroke::default().with_color(color).with_width(1.25);
|
||||||
|
frame.stroke(&canvas::Path::circle(center, 4.7), stroke.clone());
|
||||||
|
frame.stroke(&canvas::Path::circle(center, 2.15), stroke);
|
||||||
|
}
|
||||||
|
|
||||||
fn constraint_glyph_box(
|
fn constraint_glyph_box(
|
||||||
anchor: Point,
|
anchor: Point,
|
||||||
outward: [f32; 2],
|
outward: [f32; 2],
|
||||||
@@ -855,6 +865,7 @@ pub fn selection_overlay<'a>(
|
|||||||
selection_visual: SelectionVisualOptions,
|
selection_visual: SelectionVisualOptions,
|
||||||
constraint_glyphs: Vec<(Point, [f32; 2], String, bool, bool, Vec<Point>)>,
|
constraint_glyphs: Vec<(Point, [f32; 2], String, bool, bool, Vec<Point>)>,
|
||||||
constraint_glyph_tooltip: Option<String>,
|
constraint_glyph_tooltip: Option<String>,
|
||||||
|
constraint_cursor_badge: Option<String>,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
canvas(SelectionCanvas {
|
canvas(SelectionCanvas {
|
||||||
selection,
|
selection,
|
||||||
@@ -880,6 +891,7 @@ pub fn selection_overlay<'a>(
|
|||||||
selection_visual,
|
selection_visual,
|
||||||
constraint_glyphs,
|
constraint_glyphs,
|
||||||
constraint_glyph_tooltip,
|
constraint_glyph_tooltip,
|
||||||
|
constraint_cursor_badge,
|
||||||
})
|
})
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
@@ -944,6 +956,9 @@ struct SelectionCanvas {
|
|||||||
constraint_glyphs: Vec<(Point, [f32; 2], String, bool, bool, Vec<Point>)>,
|
constraint_glyphs: Vec<(Point, [f32; 2], String, bool, bool, Vec<Point>)>,
|
||||||
/// Localized kind name made visible after the app-level hover dwell.
|
/// Localized kind name made visible after the app-level hover dwell.
|
||||||
constraint_glyph_tooltip: Option<String>,
|
constraint_glyph_tooltip: Option<String>,
|
||||||
|
/// Symbol shown beside the cursor while it targets an entity that already
|
||||||
|
/// participates in an enabled geometric constraint.
|
||||||
|
constraint_cursor_badge: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_grip_marker(
|
fn draw_grip_marker(
|
||||||
@@ -1842,6 +1857,28 @@ impl canvas::Program<Message> for SelectionCanvas {
|
|||||||
let hole = canvas::Path::circle(Point::new(bx + 6.0, by + 10.5), 1.4);
|
let hole = canvas::Path::circle(Point::new(bx + 6.0, by + 10.5), 1.4);
|
||||||
frame.fill(&hole, dark);
|
frame.fill(&hole, dark);
|
||||||
}
|
}
|
||||||
|
if let Some(label) = &self.constraint_cursor_badge {
|
||||||
|
let center = Point::new(cp.x + sq + if self.hover_locked { 30.0 } else { 12.0 }, cp.y - sq - 7.0);
|
||||||
|
let blue = Color::from_rgb8(35, 145, 230);
|
||||||
|
let badge = canvas::Path::circle(center, 8.5);
|
||||||
|
frame.fill(&badge, blue);
|
||||||
|
if label == "◎" {
|
||||||
|
draw_concentric_constraint_glyph(&mut frame, center, Color::WHITE);
|
||||||
|
} else if label == "T" {
|
||||||
|
draw_tangent_constraint_glyph(&mut frame, center, Color::WHITE);
|
||||||
|
} else {
|
||||||
|
frame.fill_text(canvas::Text {
|
||||||
|
content: label.clone(),
|
||||||
|
position: center,
|
||||||
|
color: Color::WHITE,
|
||||||
|
size: iced::Pixels(11.0),
|
||||||
|
align_x: iced::alignment::Horizontal::Center.into(),
|
||||||
|
align_y: iced::alignment::Vertical::Center,
|
||||||
|
shaping: iced::advanced::text::Shaping::Advanced,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} // end !over_viewcube
|
} // end !over_viewcube
|
||||||
|
|
||||||
@@ -1929,8 +1966,8 @@ impl canvas::Program<Message> for SelectionCanvas {
|
|||||||
let hovered = cursor
|
let hovered = cursor
|
||||||
.position_in(bounds)
|
.position_in(bounds)
|
||||||
.and_then(|point| constraint_glyph_hit_test(&glyphs_for_offsets, point));
|
.and_then(|point| constraint_glyph_hit_test(&glyphs_for_offsets, point));
|
||||||
let normal_bg = theme.palette().primary.base.color;
|
let normal_bg = Color::from_rgb8(103, 109, 118);
|
||||||
let normal_fg = theme.palette().primary.base.text;
|
let normal_fg = Color::WHITE;
|
||||||
// A redundant or conflicting constraint gets the danger palette
|
// A redundant or conflicting constraint gets the danger palette
|
||||||
// instead of the
|
// instead of the
|
||||||
// ordinary primary one — same information a resolver panel
|
// ordinary primary one — same information a resolver panel
|
||||||
@@ -1981,6 +2018,8 @@ impl canvas::Program<Message> for SelectionCanvas {
|
|||||||
draw_tangent_constraint_glyph(&mut frame, glyph_center, fg);
|
draw_tangent_constraint_glyph(&mut frame, glyph_center, fg);
|
||||||
} else if label == "G²" {
|
} else if label == "G²" {
|
||||||
draw_smooth_constraint_glyph(&mut frame, glyph_center, fg);
|
draw_smooth_constraint_glyph(&mut frame, glyph_center, fg);
|
||||||
|
} else if label == "◎" {
|
||||||
|
draw_concentric_constraint_glyph(&mut frame, glyph_center, fg);
|
||||||
} else {
|
} else {
|
||||||
frame.fill_text(canvas::Text {
|
frame.fill_text(canvas::Text {
|
||||||
content: label.clone(),
|
content: label.clone(),
|
||||||
|
|||||||
Reference in new issue
Block a user