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]]
|
||||
name = "cadkernel"
|
||||
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 = [
|
||||
"acadrust",
|
||||
"cavalier_contours",
|
||||
@@ -932,7 +932,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "cadkernel-constraints"
|
||||
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 = [
|
||||
"nalgebra",
|
||||
]
|
||||
|
||||
+2
-2
@@ -34,8 +34,8 @@ clap = { version = "4", features = ["derive", "string"] }
|
||||
env_logger = "0.11"
|
||||
log = "0.4"
|
||||
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-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "7e43598" }
|
||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "4815518", features = ["acis", "offset"] }
|
||||
cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "4815518" }
|
||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||
flate2 = "1"
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||
|
||||
@@ -3372,6 +3372,88 @@ impl OpenCADStudio {
|
||||
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 {
|
||||
first,
|
||||
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();
|
||||
if handles.is_empty() {
|
||||
use crate::modules::draw::select::SelectObjectsCommand;
|
||||
let sel = SelectObjectsCommand::new(cmd);
|
||||
self.command_line.push_info(&sel.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(sel));
|
||||
} else if handles.len() != 2 {
|
||||
self.command_line.push_output(
|
||||
"Select exactly two circles/arcs, then run this constraint again.",
|
||||
);
|
||||
let command = ConcentricConstraintCommand::new();
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
} else {
|
||||
use crate::command::CmdResult;
|
||||
use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef};
|
||||
return Some(self.apply_cmd_result(CmdResult::AddParametricConstraint {
|
||||
kind: ConstraintKind::Concentric,
|
||||
refs: vec![
|
||||
ParametricRef::center(handles[0]),
|
||||
ParametricRef::center(handles[1]),
|
||||
],
|
||||
driving_param: None,
|
||||
label: "Concentric constraint",
|
||||
}));
|
||||
let refs = handles
|
||||
.iter()
|
||||
.filter_map(|handle| {
|
||||
let entity = self.tabs[i].scene.document.get_entity(*handle)?;
|
||||
ConcentricConstraintCommand::preselected_reference(entity, *handle)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if handles.len() == 2 && refs.len() == 2 && refs[0] != refs[1] {
|
||||
return Some(self.apply_cmd_result(CmdResult::AddConcentricConstraint {
|
||||
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
|
||||
.as_ref()
|
||||
.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<(
|
||||
iced::Point,
|
||||
[f32; 2],
|
||||
@@ -912,6 +929,7 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
constraint_glyphs,
|
||||
self.constraint_glyph_tooltip
|
||||
.map(|kind| crate::t!(kind.label()).into_owned()),
|
||||
constraint_cursor_badge,
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
@@ -1512,6 +1512,13 @@ pub enum CmdResult {
|
||||
second: crate::scene::parametric_constraints::ParametricRef,
|
||||
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.
|
||||
OpenAutoConstrainSettings,
|
||||
/// Adds an ordered Coincident relation. Point/point selections create a
|
||||
@@ -2778,6 +2785,7 @@ mod constraint_registry_tests {
|
||||
"ECONSTRAINT",
|
||||
"TCONSTRAINT",
|
||||
"NCONSTRAINT",
|
||||
"GCCONCENTRIC",
|
||||
"NRCONSTRAINT",
|
||||
"LCONSTRAINT",
|
||||
"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!("Arc length").as_ref(), "arc_length", format_length(arc_length)),
|
||||
ro(t!("Area").as_ref(), "area", format_area(area)),
|
||||
ro(t!("Normal X").as_ref(), "normal_x", format!("{:.4}", arc.normal.x)),
|
||||
ro(t!("Normal Y").as_ref(), "normal_y", format!("{:.4}", arc.normal.y)),
|
||||
ro(t!("Normal Z").as_ref(), "normal_z", format!("{:.4}", arc.normal.z)),
|
||||
edit(t!("Normal X").as_ref(), "normal_x", arc.normal.x),
|
||||
edit(t!("Normal Y").as_ref(), "normal_y", arc.normal.y),
|
||||
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,
|
||||
"start_angle" => arc.start_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::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::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!("Circumference").as_ref(), "circumference", 2.0 * PI * r),
|
||||
edit(t!("Area").as_ref(), "area", PI * r * r),
|
||||
ro(t!("Normal X").as_ref(), "normal_x", format!("{:.4}", circle.normal.x)),
|
||||
ro(t!("Normal Y").as_ref(), "normal_y", format!("{:.4}", circle.normal.y)),
|
||||
ro(t!("Normal Z").as_ref(), "normal_z", format!("{:.4}", circle.normal.z)),
|
||||
edit(t!("Normal X").as_ref(), "normal_x", circle.normal.x),
|
||||
edit(t!("Normal Y").as_ref(), "normal_y", circle.normal.y),
|
||||
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,
|
||||
"circumference" if v > 0.0 => circle.radius = v / (2.0 * PI),
|
||||
"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!("End parameter").as_ref(), "end_param", format!("{:.4}", ell.end_parameter)),
|
||||
ro(t!("Length").as_ref(), "length", format_length(props.perimeter)),
|
||||
ro(t!("Normal X").as_ref(), "normal_x", format!("{:.4}", ell.normal.x)),
|
||||
ro(t!("Normal Y").as_ref(), "normal_y", format!("{:.4}", ell.normal.y)),
|
||||
ro(t!("Normal Z").as_ref(), "normal_z", format!("{:.4}", ell.normal.z)),
|
||||
edit(t!("Normal X").as_ref(), "normal_x", ell.normal.x),
|
||||
edit(t!("Normal Y").as_ref(), "normal_y", ell.normal.y),
|
||||
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,
|
||||
"start_angle" => ell.start_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",
|
||||
"SM" | "SMOOTH" => "GCSMOOTH",
|
||||
"C" | "COINCIDENT" => "GCCOINCIDENT",
|
||||
"CON" | "CONCENTRIC" => "NCONSTRAINT",
|
||||
"CON" | "CONCENTRIC" => "GCCONCENTRIC",
|
||||
"COL" | "COLLINEAR" | "COLINEAR" => "LCONSTRAINT",
|
||||
"SY" | "SYMMETRIC" => "SYCONSTRAINT",
|
||||
"E" | "EQUAL" => "ECONSTRAINT",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//! resulting constraints through the geometry kernel.
|
||||
|
||||
mod coincident;
|
||||
mod concentric;
|
||||
mod equal_distance;
|
||||
mod geom_constraint;
|
||||
#[path = "perpendicular.rs"]
|
||||
@@ -14,6 +15,7 @@ mod tangent_command;
|
||||
mod tools;
|
||||
mod value;
|
||||
pub use coincident::{coincident_tool, CoincidentConstraintCommand};
|
||||
pub use concentric::ConcentricConstraintCommand;
|
||||
pub use equal_distance::{equal_distance_tool, EqualDistanceConstraintCommand};
|
||||
pub use geom_constraint::GeomConstraintCommand;
|
||||
pub use perpendicular_command::{PerpendicularConstraintCommand, PerpendicularPick};
|
||||
@@ -23,8 +25,8 @@ pub use point_on_entity::{
|
||||
pub use smooth::SmoothConstraintCommand;
|
||||
pub use tangent_command::TangentConstraintCommand;
|
||||
pub use tools::{
|
||||
colinear, concentric, equal, fixed, horizontal, normal, parallel, perpendicular, symmetric,
|
||||
tangent, vertical,
|
||||
colinear, concentric as concentric_tool, equal, fixed, horizontal, normal, parallel,
|
||||
perpendicular, symmetric, tangent, vertical,
|
||||
};
|
||||
pub use value::{
|
||||
angle_tool, dimensional_tools, distance_tool, AngleConstraintCommand,
|
||||
@@ -67,7 +69,7 @@ impl CadModule for ParametricModule {
|
||||
"GCSMOOTH", "Smooth",
|
||||
include_bytes!("../../../assets/icons/constrain/smooth.svg"),
|
||||
)),
|
||||
RibbonItem::LargeTool(concentric::tool()),
|
||||
RibbonItem::LargeTool(concentric_tool::tool()),
|
||||
RibbonItem::LargeTool(horizontal::tool()),
|
||||
RibbonItem::LargeTool(symmetric::tool()),
|
||||
RibbonItem::LargeTool(fixed::tool()),
|
||||
@@ -170,7 +172,7 @@ mod tests {
|
||||
groups[0].tools.iter().map(item_id).collect::<Vec<_>>(),
|
||||
[
|
||||
"AUTOCONSTRAIN", "CCONSTRAINT", "PCONSTRAINT", "TCONSTRAINT",
|
||||
"LCONSTRAINT", "QCONSTRAINT", "GCSMOOTH", "NCONSTRAINT",
|
||||
"LCONSTRAINT", "QCONSTRAINT", "GCSMOOTH", "GCCONCENTRIC",
|
||||
"HCONSTRAINT", "SYCONSTRAINT", "FXCONSTRAINT", "VCONSTRAINT",
|
||||
"ECONSTRAINT", "GCVISIBILITY", "GCSHOWALL", "GCHIDEALL",
|
||||
]
|
||||
|
||||
@@ -88,12 +88,12 @@ pub mod concentric {
|
||||
use super::*;
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "NCONSTRAINT",
|
||||
id: "GCCONCENTRIC",
|
||||
label: "Concentric",
|
||||
icon: IconKind::Svg(include_bytes!(
|
||||
"../../../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",
|
||||
"TCONSTRAINT",
|
||||
"NCONSTRAINT",
|
||||
"GCCONCENTRIC",
|
||||
"NRCONSTRAINT",
|
||||
"LCONSTRAINT",
|
||||
"FXCONSTRAINT",
|
||||
|
||||
@@ -454,11 +454,17 @@ impl<'a> GroupBuilder<'a> {
|
||||
return Some(*existing);
|
||||
}
|
||||
let segment_midpoint = ParametricRef::point(handle, marker).segment_midpoint_index();
|
||||
let segment_center = ParametricRef::point(handle, marker).segment_center_index();
|
||||
let polyline = matches!(
|
||||
self.document.get_entity(handle),
|
||||
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)?,
|
||||
implicit_point_type::MID,
|
||||
@@ -551,6 +557,9 @@ impl<'a> GroupBuilder<'a> {
|
||||
if r.segment_midpoint_index().is_some() {
|
||||
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() {
|
||||
return self.segment_node(r.entity, segment);
|
||||
}
|
||||
@@ -2079,6 +2088,9 @@ pub(super) fn native_constraint_set(
|
||||
implicit_point_type::MID => {
|
||||
ParametricRef::segment_midpoint(curve.entity, segment)
|
||||
}
|
||||
implicit_point_type::CENTER => {
|
||||
ParametricRef::segment_center(curve.entity, segment)
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -14,8 +14,8 @@ use acadrust::types::{Handle, Vector3};
|
||||
/// 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
|
||||
/// circle/arc's center; -2 = a bounded curve's midpoint). Polyline segment
|
||||
/// and segment-midpoint references use private negative ranges so they stay
|
||||
/// distinct from vertex markers.
|
||||
/// segment-midpoint, and curved-segment-center references use private
|
||||
/// negative ranges so they stay distinct from vertex markers.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ParametricRef {
|
||||
pub entity: Handle,
|
||||
@@ -28,6 +28,7 @@ pub struct ParametricRef {
|
||||
|
||||
const POLYLINE_SEGMENT_MARKER_BASE: i32 = -1_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_MINOR_AXIS_MARKER: i32 = -5;
|
||||
const TEXT_BASELINE_MARKER: i32 = -6;
|
||||
@@ -88,10 +89,25 @@ impl ParametricRef {
|
||||
|
||||
pub fn segment_midpoint_index(self) -> Option<usize> {
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn text_baseline(entity: Handle) -> Self {
|
||||
Self {
|
||||
@@ -503,7 +519,7 @@ impl ParametricConstraintSet {
|
||||
/// endpoint's equivalent of `dimension_assoc::resolve_reference`, restricted
|
||||
/// to the marker conventions constraint endpoints actually use (whole-entity
|
||||
/// `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
|
||||
/// 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);
|
||||
return Some(Vector3::new(point[0], point[1], point[2]));
|
||||
}
|
||||
let segment = ParametricRef {
|
||||
let reference = ParametricRef {
|
||||
entity: Handle::NULL,
|
||||
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) = segment {
|
||||
if let Some(segment) = reference.segment_midpoint_index() {
|
||||
let planar = crate::entities::curve::entity_curve(entity)?;
|
||||
let curve = planar.curve.segments().into_iter().nth(segment)?;
|
||||
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;
|
||||
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() {
|
||||
let anchor = resolve_point(
|
||||
entity,
|
||||
|
||||
@@ -513,6 +513,12 @@ fn resolve_constraint_point(
|
||||
if let Some(point) = geometry.point_for_marker(marker) {
|
||||
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
|
||||
.segment_midpoint_index()
|
||||
@@ -794,6 +800,175 @@ fn refs_share_supported_plane(document: &acadrust::CadDocument, refs: &[Parametr
|
||||
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> {
|
||||
let value = constraint.driving_param.as_ref()?.resolve(params).ok()?;
|
||||
if !value.is_finite() {
|
||||
@@ -1982,6 +2157,7 @@ fn polyline_ref_order(
|
||||
}
|
||||
let position = if let Some(index) = reference.segment_index()
|
||||
.or_else(|| reference.segment_midpoint_index())
|
||||
.or_else(|| reference.segment_center_index())
|
||||
{
|
||||
2 * index + 1
|
||||
} else {
|
||||
@@ -2133,7 +2309,13 @@ fn solve_scope(
|
||||
|
||||
let has_smooth = constraints.iter()
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -3013,6 +3195,13 @@ fn solve_scope(
|
||||
}
|
||||
}
|
||||
apply_smooth_constraints(document, set, &mut results);
|
||||
apply_spatial_concentric_constraints(
|
||||
document,
|
||||
set,
|
||||
driven_refs,
|
||||
initial_fixed_refs,
|
||||
&mut results,
|
||||
);
|
||||
Some((results, dof, conflicts))
|
||||
}
|
||||
|
||||
@@ -3064,6 +3253,21 @@ impl Scene {
|
||||
}
|
||||
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) {
|
||||
return Err(
|
||||
"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);
|
||||
}
|
||||
|
||||
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(
|
||||
anchor: Point,
|
||||
outward: [f32; 2],
|
||||
@@ -855,6 +865,7 @@ pub fn selection_overlay<'a>(
|
||||
selection_visual: SelectionVisualOptions,
|
||||
constraint_glyphs: Vec<(Point, [f32; 2], String, bool, bool, Vec<Point>)>,
|
||||
constraint_glyph_tooltip: Option<String>,
|
||||
constraint_cursor_badge: Option<String>,
|
||||
) -> Element<'a, Message> {
|
||||
canvas(SelectionCanvas {
|
||||
selection,
|
||||
@@ -880,6 +891,7 @@ pub fn selection_overlay<'a>(
|
||||
selection_visual,
|
||||
constraint_glyphs,
|
||||
constraint_glyph_tooltip,
|
||||
constraint_cursor_badge,
|
||||
})
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
@@ -944,6 +956,9 @@ struct SelectionCanvas {
|
||||
constraint_glyphs: Vec<(Point, [f32; 2], String, bool, bool, Vec<Point>)>,
|
||||
/// Localized kind name made visible after the app-level hover dwell.
|
||||
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(
|
||||
@@ -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);
|
||||
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
|
||||
|
||||
@@ -1929,8 +1966,8 @@ impl canvas::Program<Message> for SelectionCanvas {
|
||||
let hovered = cursor
|
||||
.position_in(bounds)
|
||||
.and_then(|point| constraint_glyph_hit_test(&glyphs_for_offsets, point));
|
||||
let normal_bg = theme.palette().primary.base.color;
|
||||
let normal_fg = theme.palette().primary.base.text;
|
||||
let normal_bg = Color::from_rgb8(103, 109, 118);
|
||||
let normal_fg = Color::WHITE;
|
||||
// A redundant or conflicting constraint gets the danger palette
|
||||
// instead of the
|
||||
// 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);
|
||||
} else if label == "G²" {
|
||||
draw_smooth_constraint_glyph(&mut frame, glyph_center, fg);
|
||||
} else if label == "◎" {
|
||||
draw_concentric_constraint_glyph(&mut frame, glyph_center, fg);
|
||||
} else {
|
||||
frame.fill_text(canvas::Text {
|
||||
content: label.clone(),
|
||||
|
||||
Reference in new issue
Block a user