Merge pull request #1289 from ramox81/fix/coincident-standard-command

# Conflicts:
#	src/command.rs
#	src/scene/mod.rs
#	src/scene/parametric_solve.rs
This commit is contained in:
Hakan Seven committed 2026-09-15 15:08:35 +03:00
commit acde01cfc3
12 files changed
+1273 -228

No files matched your search

+1 -10
View File
@@ -32,7 +32,7 @@ pub fn general_section(entity: &EntityType) -> PropSection {
let hyperlink = crate::scene::pe_url_of(entity).unwrap_or_default().to_owned();
let mut section = PropSection {
let section = PropSection {
title: t!("General").into_owned(),
props: vec![
Property {
@@ -88,15 +88,6 @@ pub fn general_section(entity: &EntityType) -> PropSection {
],
};
// Thickness (DXF 39) is a General-group property, but only the entity
// types that carry an extrusion thickness expose it (line, circle, arc,
// polyline, text, 2D solid, …). Show it right after Hyperlink for those.
if let Some(t) = crate::scene::view::dispatch::entity_thickness(entity) {
section
.props
.push(crate::entities::common::edit_prop(t!("Thickness").as_ref(), "thickness", t));
}
section
}
+270 -25
View File
@@ -400,6 +400,7 @@ pub(crate) fn resolve_point(entity: &acadrust::EntityType, marker: i32) -> Optio
return match entity {
acadrust::EntityType::Circle(circle) => Some(circle.center_wcs()),
acadrust::EntityType::Arc(arc) => Some(arc.center_wcs()),
acadrust::EntityType::Ellipse(ellipse) => Some(ellipse.center),
_ => None,
};
}
@@ -417,21 +418,10 @@ pub(crate) fn resolve_point(entity: &acadrust::EntityType, marker: i32) -> Optio
}
.segment_midpoint_index();
if let Some(segment) = segment {
let points = super::dimension_assoc::source_points(entity);
let closed = match entity {
acadrust::EntityType::LwPolyline(polyline) => polyline.is_closed,
acadrust::EntityType::Polyline2D(polyline) => polyline.is_closed(),
_ => return None,
};
let a = *points.get(segment)?;
let b = if segment + 1 < points.len() {
points[segment + 1]
} else if closed {
*points.first()?
} else {
return None;
};
return Some((a + b) * 0.5);
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));
return Some(Vector3::new(point[0], point[1], point[2]));
}
if marker < 0 {
return None;
@@ -445,6 +435,63 @@ pub(crate) fn resolve_point(entity: &acadrust::EntityType, marker: i32) -> Optio
/// already coincident for [`nearest_parametric_point`]'s purposes.
const COINCIDENT_EPSILON_SQ: f64 = 1.0e-12;
/// Addressable constraint points for one entity, in the same marker space
/// used by persistent constraint references.
pub(crate) fn parametric_point_candidates(
entity: &acadrust::EntityType,
) -> Vec<(i32, Vector3)> {
let mut points: Vec<_> = super::dimension_assoc::source_points(entity)
.into_iter()
.enumerate()
.map(|(marker, point)| (marker as i32, point))
.collect();
match entity {
acadrust::EntityType::Circle(circle) => points.push((-3, circle.center_wcs())),
acadrust::EntityType::Arc(arc) => points.push((-3, arc.center_wcs())),
acadrust::EntityType::Ellipse(ellipse) => points.push((-3, ellipse.center)),
_ => {}
}
if matches!(
entity,
acadrust::EntityType::Line(_)
| acadrust::EntityType::Arc(_)
| acadrust::EntityType::Spline(_)
| acadrust::EntityType::Ellipse(_)
) {
if let Some(curve) = crate::entities::curve::entity_curve(entity) {
if !curve.is_closed() {
let point = curve.point_at(0.5);
points.push((-2, Vector3::new(point[0], point[1], point[2])));
}
}
}
if matches!(
entity,
acadrust::EntityType::LwPolyline(_) | acadrust::EntityType::Polyline2D(_)
) {
if let Some(planar) = crate::entities::curve::entity_curve(entity) {
points.extend(planar.curve.segments().into_iter().enumerate().map(
|(index, curve)| {
let point = planar.plane.point_at(curve.point_at(0.5));
(
POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE - index as i32,
Vector3::new(point[0], point[1], point[2]),
)
},
));
}
}
points
}
pub(crate) fn is_parametric_point_near(entity: &acadrust::EntityType, point: Vector3) -> bool {
parametric_point_candidates(entity)
.into_iter()
.any(|(_, candidate)| (candidate - point).length_squared() <= COINCIDENT_EPSILON_SQ)
}
/// Finds the addressable entity point nearest a snapped world position.
/// Returns `None` when no point in the scope is within the coincidence
/// tolerance.
@@ -470,21 +517,63 @@ pub(crate) fn nearest_parametric_point(
if common.owner_handle != owner || Some(common.handle) == exclude {
continue;
}
for (marker, point) in super::dimension_assoc::source_points(candidate)
.into_iter()
.enumerate()
{
consider(common.handle, marker as i32, point);
}
match candidate {
acadrust::EntityType::Circle(c) => consider(common.handle, -3, c.center_wcs()),
acadrust::EntityType::Arc(a) => consider(common.handle, -3, a.center_wcs()),
_ => {}
for (marker, point) in parametric_point_candidates(candidate) {
consider(common.handle, marker, point);
}
}
best.map(|(_, r)| r)
}
/// Resolve a point pick within one explicitly selected entity. This keeps two
/// different endpoints at the same world coordinate distinguishable.
pub(crate) fn nearest_parametric_point_on_entity(
document: &acadrust::CadDocument,
scope: ParametricScope,
handle: Handle,
world_point: Vector3,
) -> Option<ParametricRef> {
let entity = document.get_entity(handle)?;
if entity.common().owner_handle != scope.owner_handle(document) {
return None;
}
parametric_point_candidates(entity)
.into_iter()
.filter_map(|(marker, point)| {
let distance = (point - world_point).length_squared();
(distance <= COINCIDENT_EPSILON_SQ)
.then_some((distance, ParametricRef::point(handle, marker)))
})
.min_by(|(a, _), (b, _)| a.total_cmp(b))
.map(|(_, reference)| reference)
}
/// Resolve the whole curve or the picked polyline segment used by a
/// point-to-curve Coincident relation.
pub(crate) fn parametric_curve_ref_for_pick(
document: &acadrust::CadDocument,
scope: ParametricScope,
handle: Handle,
world_point: Vector3,
) -> Option<ParametricRef> {
let entity = document.get_entity(handle)?;
if entity.common().owner_handle != scope.owner_handle(document) {
return None;
}
match entity {
acadrust::EntityType::Line(_)
| acadrust::EntityType::Circle(_)
| acadrust::EntityType::Arc(_)
| acadrust::EntityType::Ellipse(_)
| acadrust::EntityType::Spline(_) => Some(ParametricRef::whole(handle)),
acadrust::EntityType::LwPolyline(_) | acadrust::EntityType::Polyline2D(_) => {
let segments = crate::entities::curve::entity_curve_xy(entity)?.segments();
cadkernel::geom2d::nearest_of(segments.iter(), [world_point.x, world_point.y])
.map(|(index, _)| ParametricRef::segment(handle, index))
}
_ => None,
}
}
impl ConstraintKind {
/// The short symbol a constraint glyph shows — matches the existing
/// ribbon icons (`crate::modules::parametric::{tools,value}`) for
@@ -722,6 +811,49 @@ pub(crate) fn constraint_hover_points(
}
impl super::Scene {
/// Expand a set of entities to the full enabled constraint component in
/// one scope. Whole-object translations use this to keep every connected
/// entity's own shape while moving the assembly.
pub(crate) fn parametric_connected_handles(
&self,
scope: ParametricScope,
seeds: &[Handle],
) -> Vec<Handle> {
let mut ordered = seeds.to_vec();
let mut found: std::collections::HashSet<_> = seeds.iter().copied().collect();
let Some(set) = self.parametric_constraint_set(scope) else {
return ordered;
};
loop {
let mut added = false;
for constraint in set.constraints.iter().filter(|constraint| {
constraint.enabled
&& matches!(
constraint.kind,
ConstraintKind::Coincident | ConstraintKind::PointOnCurve
)
}) {
if !constraint
.refs
.iter()
.any(|reference| found.contains(&reference.entity))
{
continue;
}
for reference in &constraint.refs {
if found.insert(reference.entity) {
ordered.push(reference.entity);
added = true;
}
}
}
if !added {
break;
}
}
ordered
}
pub fn is_parametric_constraint_visible(
&self,
scope: ParametricScope,
@@ -1012,6 +1144,68 @@ impl super::Scene {
mapped
}
/// Infer only endpoint/vertex coincidences from the selected objects.
/// Unlike the broader automatic constraint pass, this includes spline,
/// ellipse, and polyline vertices while leaving centers and midpoints to
/// their dedicated constraint kinds.
pub fn inferred_coincident_constraints(
&self,
scope: ParametricScope,
handles: &[Handle],
) -> Vec<Vec<ParametricRef>> {
let owner = scope.owner_handle(&self.document);
let mut sources = Vec::new();
let mut seen_handles = std::collections::HashSet::new();
for handle in handles.iter().copied() {
if !seen_handles.insert(handle) {
continue;
}
let Some(entity) = self.document.get_entity(handle) else {
continue;
};
if entity.common().owner_handle != owner {
continue;
}
for (marker, point) in super::dimension_assoc::source_points(entity)
.into_iter()
.enumerate()
{
sources.push((ParametricRef::point(handle, marker as i32), point));
}
}
let existing = self.parametric_constraint_set(scope);
let mut inferred = Vec::new();
for first in 0..sources.len() {
for second in first + 1..sources.len() {
if sources[first].0.entity == sources[second].0.entity
|| (sources[first].1 - sources[second].1).length_squared()
> COINCIDENT_EPSILON_SQ
{
continue;
}
let refs = vec![sources[first].0, sources[second].0];
let already_exists = existing.is_some_and(|set| {
set.constraints.iter().any(|constraint| {
constraint.kind == ConstraintKind::Coincident
&& constraint.refs.len() == 2
&& (constraint.refs == refs
|| (constraint.refs[0] == refs[1]
&& constraint.refs[1] == refs[0]))
})
});
if !already_exists
&& self
.validate_parametric_constraint(ConstraintKind::Coincident, &refs, None)
.is_ok()
{
inferred.push(refs);
}
}
}
inferred
}
pub fn smooth_constraint_refs(&self, handles: &[Handle]) -> Option<Vec<ParametricRef>> {
let [first, second] = handles else {
return None;
@@ -1714,4 +1908,55 @@ mod tests {
Some(cadkernel::geom2d::Curve::Arc(_))
));
}
#[test]
fn curve_pick_uses_the_bulged_segment_instead_of_its_chord() {
let mut scene = super::super::Scene::new();
let mut polyline = acadrust::entities::LwPolyline::new();
polyline.vertices = vec![
acadrust::entities::LwVertex::with_bulge(
acadrust::types::Vector2::new(0.0, 0.0),
1.0,
),
acadrust::entities::LwVertex::from_coords(10.0, 0.0),
acadrust::entities::LwVertex::from_coords(0.0, -4.0),
];
let handle = scene.add_entity(acadrust::EntityType::LwPolyline(polyline));
assert_eq!(
parametric_curve_ref_for_pick(
&scene.document,
ParametricScope::ModelSpace,
handle,
Vector3::new(5.0, -5.0, 0.0),
),
Some(ParametricRef::segment(handle, 0))
);
}
#[test]
fn move_expansion_follows_only_coincident_relations() {
let mut scene = super::super::Scene::new();
let set = scene.parametric_constraint_set_mut(ParametricScope::ModelSpace);
set.add(
ConstraintKind::Coincident,
vec![ParametricRef::point(h(1), 0), ParametricRef::point(h(2), 0)],
None,
);
set.add(
ConstraintKind::PointOnCurve,
vec![ParametricRef::point(h(2), 0), ParametricRef::whole(h(3))],
None,
);
set.add(
ConstraintKind::Parallel,
vec![ParametricRef::whole(h(3)), ParametricRef::whole(h(4))],
None,
);
assert_eq!(
scene.parametric_connected_handles(ParametricScope::ModelSpace, &[h(1)]),
vec![h(1), h(2), h(3)]
);
}
}
+287 -47
View File
@@ -45,6 +45,7 @@ enum EntityGeom {
Polyline {
points: Rc<Vec<GPoint>>,
straight: Rc<Vec<bool>>,
arcs: Rc<Vec<Option<PolylineArc>>>,
closed: bool,
},
Circle(GCircle),
@@ -61,6 +62,12 @@ enum EntityGeom {
},
}
#[derive(Clone, Copy)]
struct PolylineArc {
arc: GArc,
sweep: f64,
}
impl EntityGeom {
/// The `cadkernel_constraints::geo::Point` for a marker on this entity — `None` for
/// a marker this entity type/value doesn't support (see
@@ -91,6 +98,7 @@ impl EntityGeom {
points,
straight,
closed,
..
} = self
else {
return None;
@@ -108,6 +116,13 @@ impl EntityGeom {
};
Some(GLine { p1, p2 })
}
fn arc_segment(&self, index: usize) -> Option<PolylineArc> {
let EntityGeom::Polyline { arcs, .. } = self else {
return None;
};
arcs.get(index).copied().flatten()
}
}
/// `Tangent`/`Normal` only ever care about "a whole circle-shaped curve" or
@@ -131,8 +146,14 @@ fn as_circle_or_line(g: EntityGeom, reference: ParametricRef) -> CircleOrLine {
EntityGeom::Spline { .. } => CircleOrLine::Other,
EntityGeom::Polyline { .. } => reference
.segment_index()
.and_then(|index| g.line_segment(index))
.map(CircleOrLine::Line)
.and_then(|index| {
g.line_segment(index)
.map(CircleOrLine::Line)
.or_else(|| {
g.arc_segment(index)
.map(|segment| CircleOrLine::Circle(segment.arc.circle))
})
})
.unwrap_or(CircleOrLine::Other),
}
}
@@ -182,42 +203,114 @@ fn register_entity(
sys.add_param(table.insertion_point.x, false),
sys.add_param(table.insertion_point.y, false),
))),
EntityType::LwPolyline(polyline) => Some(EntityGeom::Polyline {
points: Rc::new(
super::dimension_assoc::source_points(entity)
.into_iter()
.map(|point| {
GPoint::new(sys.add_param(point.x, false), sys.add_param(point.y, false))
EntityType::LwPolyline(polyline) => {
let source = super::dimension_assoc::source_points(entity);
let points: Vec<_> = source
.iter()
.map(|point| {
GPoint::new(sys.add_param(point.x, false), sys.add_param(point.y, false))
})
.collect();
let arcs = (0..polyline.vertices.len())
.map(|index| {
let next = if index + 1 < source.len() {
index + 1
} else if polyline.is_closed {
0
} else {
return None;
};
let bulge = polyline.vertices[index].bulge;
let shape = cadkernel::geom2d::BulgeArc::from_bulge(
[source[index].x, source[index].y],
[source[next].x, source[next].y],
bulge,
)?;
Some(PolylineArc {
arc: GArc {
circle: GCircle {
center: GPoint::new(
sys.add_param(shape.center[0], false),
sys.add_param(shape.center[1], false),
),
rad: sys.add_param(shape.radius, false),
},
start: points[index],
end: points[next],
start_angle: sys.add_param(shape.start_angle, false),
end_angle: sys.add_param(shape.start_angle + shape.sweep, false),
},
sweep: shape.sweep,
})
.collect(),
),
straight: Rc::new(
polyline
.vertices
.iter()
.map(|vertex| vertex.bulge.abs() <= 1e-9)
.collect(),
),
closed: polyline.is_closed,
}),
EntityType::Polyline2D(polyline) => Some(EntityGeom::Polyline {
points: Rc::new(
super::dimension_assoc::source_points(entity)
.into_iter()
.map(|point| {
GPoint::new(sys.add_param(point.x, false), sys.add_param(point.y, false))
})
.collect();
Some(EntityGeom::Polyline {
points: Rc::new(points),
straight: Rc::new(
polyline
.vertices
.iter()
.map(|vertex| vertex.bulge.abs() <= 1e-9)
.collect(),
),
arcs: Rc::new(arcs),
closed: polyline.is_closed,
})
}
EntityType::Polyline2D(polyline) => {
let source = super::dimension_assoc::source_points(entity);
let points: Vec<_> = source
.iter()
.map(|point| {
GPoint::new(sys.add_param(point.x, false), sys.add_param(point.y, false))
})
.collect();
let arcs = (0..polyline.vertices.len())
.map(|index| {
let next = if index + 1 < source.len() {
index + 1
} else if polyline.is_closed() {
0
} else {
return None;
};
let bulge = polyline.vertices[index].bulge;
let shape = cadkernel::geom2d::BulgeArc::from_bulge(
[source[index].x, source[index].y],
[source[next].x, source[next].y],
bulge,
)?;
Some(PolylineArc {
arc: GArc {
circle: GCircle {
center: GPoint::new(
sys.add_param(shape.center[0], false),
sys.add_param(shape.center[1], false),
),
rad: sys.add_param(shape.radius, false),
},
start: points[index],
end: points[next],
start_angle: sys.add_param(shape.start_angle, false),
end_angle: sys.add_param(shape.start_angle + shape.sweep, false),
},
sweep: shape.sweep,
})
.collect(),
),
straight: Rc::new(
polyline
.vertices
.iter()
.map(|vertex| vertex.bulge.abs() <= 1e-9)
.collect(),
),
closed: polyline.is_closed(),
}),
})
.collect();
Some(EntityGeom::Polyline {
points: Rc::new(points),
straight: Rc::new(
polyline
.vertices
.iter()
.map(|vertex| vertex.bulge.abs() <= 1e-9)
.collect(),
),
arcs: Rc::new(arcs),
closed: polyline.is_closed(),
})
}
EntityType::Line(l) => {
let p1 = GPoint::new(
sys.add_param(l.start.x, false),
@@ -439,6 +532,42 @@ fn resolve_constraint_point(
)));
return Some(point);
}
if let Some(segment) = reference
.segment_midpoint_index()
.and_then(|index| geometry.arc_segment(index))
{
let (start, end) = {
let store = sys.store();
(
store.get(segment.arc.start_angle),
store.get(segment.arc.end_angle),
)
};
let parameter_value = (start + end) * 0.5;
let parameter = sys.add_param(parameter_value, false);
sys.add_constraint(Rc::new(CenterOfGravity::new(
parameter,
vec![segment.arc.start_angle, segment.arc.end_angle],
vec![0.5, 0.5],
)));
let value = cadkernel_constraints::geo::Curve::value(
&segment.arc,
sys.store(),
parameter_value,
0.0,
None,
);
let point = GPoint::new(sys.add_param(value.x, false), sys.add_param(value.y, false));
let curve = Rc::new(segment.arc);
sys.add_constraint(Rc::new(CurveValue::new(
point,
point.x,
curve.clone(),
parameter,
)));
sys.add_constraint(Rc::new(CurveValue::new(point, point.y, curve, parameter)));
return Some(point);
}
if marker != -2 {
return None;
}
@@ -935,8 +1064,22 @@ fn build_constraint(
}
EntityGeom::Polyline { .. } => b
.segment_index()
.and_then(|index| geom.line_segment(index))
.map(|line| vec![Rc::new(PointOnLine::new(p, line)) as Rc<dyn Constraint>])
.and_then(|index| {
geom.line_segment(index)
.map(|line| {
vec![Rc::new(PointOnLine::new(p, line)) as Rc<dyn Constraint>]
})
.or_else(|| {
geom.arc_segment(index).map(|segment| {
let zero = sys.add_param(0.0, true);
vec![Rc::new(P2CDistance::new(
segment.arc.circle,
p,
zero,
)) as Rc<dyn Constraint>]
})
})
})
.unwrap_or_default(),
EntityGeom::Point(_) => Vec::new(),
}
@@ -1660,13 +1803,10 @@ fn solve_scope(
}
EntityGeom::Polyline {
points,
straight,
closed,
..
} => {
for index in 0..straight.len() {
if !straight[index] {
continue;
}
for index in 0..points.len() {
let Some(a) = points.get(index).copied() else {
continue;
};
@@ -1720,9 +1860,8 @@ fn solve_scope(
// its endpoint. Not tracked in `owner`: these are solver-internal
// bookkeeping, never redundant with anything a user-facing constraint
// could name, so there's no `ConstraintId` for them to report against.
for geom in cache.values() {
let EntityGeom::Arc(arc) = geom else { continue };
let curve = Rc::new(*arc);
let add_arc_rules = |sys: &mut System, arc: GArc, sweep: Option<f64>| {
let curve = Rc::new(arc);
sys.add_constraint(Rc::new(CurveValue::new(
arc.start,
arc.start.x,
@@ -1747,6 +1886,25 @@ fn solve_scope(
curve,
arc.end_angle,
)));
if let Some(sweep) = sweep {
let target = sys.add_param(sweep, true);
sys.add_constraint(Rc::new(Difference::new(
arc.start_angle,
arc.end_angle,
target,
)));
}
};
for geom in cache.values() {
match geom {
EntityGeom::Arc(arc) => add_arc_rules(&mut sys, *arc, None),
EntityGeom::Polyline { arcs, .. } => {
for segment in arcs.iter().flatten() {
add_arc_rules(&mut sys, segment.arc, Some(segment.sweep));
}
}
_ => {}
}
}
// A point explicitly edited by a grip, STRETCH, or Properties is the
@@ -1810,6 +1968,66 @@ fn solve_scope(
sys.add_constraint(Rc::new(Difference::new(el.center.y, el.focus1.y, dy)));
}
// The first ordered Coincident selection is the anchor for this solve.
// Pin either its addressed point or every defining parameter of its whole
// curve temporarily; the relation remains persistent, while these pins do
// not enter the drawing's constraint graph.
for reference in driven_refs {
if !set
.constraints
.iter()
.any(|constraint| constraint.refs.iter().any(|item| item.entity == reference.entity))
{
continue;
}
let params_to_pin = if reference.marker.is_some() && reference.segment_index().is_none() {
resolve_constraint_point(document, &mut sys, &mut cache, *reference)
.map(|point| vec![point.x, point.y])
.unwrap_or_default()
} else {
match resolve_ref(document, &mut sys, &mut cache, *reference) {
Some(EntityGeom::Point(point)) => vec![point.x, point.y],
Some(EntityGeom::Line(line))
| Some(EntityGeom::Ray(line))
| Some(EntityGeom::XLine(line)) => {
vec![line.p1.x, line.p1.y, line.p2.x, line.p2.y]
}
Some(EntityGeom::Polyline { points, .. }) => points
.iter()
.flat_map(|point| [point.x, point.y])
.collect(),
Some(EntityGeom::Circle(circle)) => {
vec![circle.center.x, circle.center.y, circle.rad]
}
Some(EntityGeom::Arc(arc)) => vec![
arc.circle.center.x,
arc.circle.center.y,
arc.circle.rad,
arc.start_angle,
arc.end_angle,
],
Some(EntityGeom::Ellipse(ellipse)) => vec![
ellipse.center.x,
ellipse.center.y,
ellipse.focus1.x,
ellipse.focus1.y,
ellipse.radmin,
],
Some(EntityGeom::Spline { curve, .. }) => curve
.poles
.iter()
.flat_map(|point| [point.x, point.y])
.collect(),
None => Vec::new(),
}
};
for parameter in params_to_pin {
let value = sys.store().get(parameter);
let target = sys.add_param(value, true);
sys.add_constraint(Rc::new(Equal::new(parameter, target, 1.0)));
}
}
let partitions = sys.partition();
for sub in &partitions {
solve_dl(sub, sys.store_mut());
@@ -1826,7 +2044,9 @@ fn solve_scope(
.map(|g| match g {
EntityGeom::Point(_) => 2,
EntityGeom::Line(_) | EntityGeom::Ray(_) | EntityGeom::XLine(_) => 4,
EntityGeom::Polyline { points, .. } => points.len() * 2,
EntityGeom::Polyline { points, arcs, .. } => {
points.len() * 2 + arcs.iter().flatten().count() * 5
}
EntityGeom::Circle(_) => 3,
// Raw param count (center×2, rad, start×2, end×2, both
// angles) — same "raw, not netted against its own
@@ -2531,6 +2751,26 @@ mod tests {
.is_err());
}
#[test]
fn bulged_polyline_segment_registers_as_a_kernel_arc() {
let mut scene = Scene::new();
let mut polyline = acadrust::entities::LwPolyline::new();
polyline.vertices = vec![
acadrust::entities::LwVertex::with_bulge(
acadrust::types::Vector2::new(0.0, 0.0),
1.0,
),
acadrust::entities::LwVertex::from_coords(10.0, 0.0),
];
let handle = scene.add_entity(EntityType::LwPolyline(polyline));
let mut system = cadkernel_constraints::system::System::new();
let geometry = super::register_entity(&scene.document, &mut system, handle).unwrap();
let segment = geometry.arc_segment(0).expect("expected an arc segment");
assert!((system.store().get(segment.arc.circle.rad) - 5.0).abs() < 1.0e-9);
assert!((segment.sweep - std::f64::consts::PI).abs() < 1.0e-9);
}
#[test]
fn ray_and_construction_line_constraints_solve_through_the_kernel() {
let mut scene = Scene::new();
+44 -3
View File
@@ -80,6 +80,25 @@ pub fn properties_sectioned(
} else {
sections.extend(groups);
}
if let Some(thickness) = entity_thickness(entity) {
let already_present = sections.iter().flat_map(|section| &section.props).any(|property| {
property.field == "thickness"
|| property.field == "sl_thickness"
|| property.label == crate::t!("Thickness").as_ref()
});
if !already_present {
if let Some(geometry) = sections
.iter_mut()
.find(|section| section.title == crate::t!("Geometry").as_ref())
{
geometry.props.push(crate::entities::common::edit_prop(
crate::t!("Thickness").as_ref(),
"thickness",
thickness,
));
}
}
}
sections
}
@@ -252,9 +271,8 @@ pub fn set_entity_xdata(
}
/// The extrusion thickness (DXF 39) of the entities that carry one, or `None`
/// for entity types that have none. Thickness is a per-entity field but is
/// surfaced in the General group (as in a standard properties palette), so
/// this bridges the two.
/// for entity types that have none. The Properties panel uses this to append
/// the shared field to each entity's Geometry group.
pub fn entity_thickness(entity: &EntityType) -> Option<f64> {
Some(match entity {
EntityType::Arc(e) => e.thickness,
@@ -382,3 +400,26 @@ pub fn grip_menu_point_value(
pub fn apply_transform(entity: &mut EntityType, t: &EntityTransform) {
EntityTypeOps::apply_transform(entity, t);
}
#[cfg(test)]
mod tests {
use super::*;
use acadrust::types::Vector3;
#[test]
fn line_thickness_follows_the_entity_geometry_rows() {
let mut line = acadrust::entities::Line::from_points(Vector3::ZERO, Vector3::UNIT_X);
line.thickness = 2.5;
let sections = properties_sectioned(Handle::NULL, &EntityType::Line(line), &[]);
assert!(!sections[0]
.props
.iter()
.any(|property| property.field == "thickness"));
let geometry = sections
.iter()
.find(|section| section.props.iter().any(|property| property.field == "start_x"))
.unwrap();
assert_eq!(geometry.props.last().unwrap().field, "thickness");
}
}