forked from 1b-agent/opencadstudio-upstream
Complete Smooth G2 constraint workflow
This commit is contained in:
1 parent
a27545c379
commit
7c6665153c
16 files changed
+608
-100
No files matched your search
@@ -771,7 +771,13 @@ fn constraint_node(
|
||||
) -> Option<i32> {
|
||||
let refs: &[ParametricRef] = &constraint.refs;
|
||||
if constraint.kind == ConstraintKind::Smooth {
|
||||
let [first, second] = refs else { return None };
|
||||
let (first, second, second_curve_ref) = match refs {
|
||||
[first, second] => (*first, *second, ParametricRef::whole(second.entity)),
|
||||
[first, second, second_curve] if second.entity == second_curve.entity => {
|
||||
(*first, *second, *second_curve)
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
let endpoint_parameter = |reference: ParametricRef| {
|
||||
let entity = document.get_entity(reference.entity)?;
|
||||
let EntityType::Spline(spline) = entity else {
|
||||
@@ -791,15 +797,15 @@ fn constraint_node(
|
||||
}?;
|
||||
Some(Some(value))
|
||||
};
|
||||
let first_parameter = endpoint_parameter(*first)?;
|
||||
let second_parameter = endpoint_parameter(*second)?;
|
||||
let first_parameter = endpoint_parameter(first)?;
|
||||
let second_parameter = endpoint_parameter(second)?;
|
||||
if first_parameter.is_none() && second_parameter.is_none() {
|
||||
return None;
|
||||
}
|
||||
let first_curve = builder.geometry_node(first.entity)?;
|
||||
let second_curve = builder.geometry_node(second.entity)?;
|
||||
let first_point = builder.ref_node(*first)?;
|
||||
let second_point = builder.ref_node(*second)?;
|
||||
let second_curve = builder.ref_node(second_curve_ref)?;
|
||||
let first_point = builder.ref_node(first)?;
|
||||
let second_point = builder.ref_node(second)?;
|
||||
|
||||
let coincidence = builder.alloc_node_id();
|
||||
builder.push_node(
|
||||
@@ -2130,7 +2136,7 @@ pub(super) fn native_constraint_set(
|
||||
owned_constraint_ids,
|
||||
..
|
||||
} if kind == ConstraintKind::Smooth => {
|
||||
let endpoints = owned_constraint_ids
|
||||
let mut endpoints: Vec<ParametricRef> = owned_constraint_ids
|
||||
.iter()
|
||||
.filter_map(|id| group.nodes.iter().find(|child| child.node_id == *id))
|
||||
.find(|child| {
|
||||
@@ -2146,6 +2152,23 @@ pub(super) fn native_constraint_set(
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
if let Some(target) = endpoints.get(1).copied() {
|
||||
let segment = owned_constraint_ids
|
||||
.iter()
|
||||
.filter_map(|id| group.nodes.iter().find(|child| child.node_id == *id))
|
||||
.filter(|child| {
|
||||
child.class_name.eq_ignore_ascii_case("AcTangentConstraint")
|
||||
})
|
||||
.flat_map(|child| child.connections.iter())
|
||||
.filter_map(|id| refs.get(id).copied())
|
||||
.find(|reference| {
|
||||
reference.entity == target.entity
|
||||
&& reference.segment_index().is_some()
|
||||
});
|
||||
if let Some(segment) = segment {
|
||||
endpoints.push(segment);
|
||||
}
|
||||
}
|
||||
(endpoints, Vec::new())
|
||||
}
|
||||
_ => (
|
||||
@@ -2192,6 +2215,7 @@ pub(super) fn native_constraint_set(
|
||||
ConstraintKind::DistanceDirected => matches!(targets.len(), 2 | 3),
|
||||
ConstraintKind::EqualDistance => targets.len() == 4,
|
||||
ConstraintKind::RigidSet => !targets.is_empty() && rigid_points.len() >= 2,
|
||||
ConstraintKind::Smooth => matches!(targets.len(), 2 | 3),
|
||||
_ => targets.len() == 2,
|
||||
};
|
||||
if !valid_target_count {
|
||||
|
||||
@@ -687,6 +687,27 @@ pub(crate) fn parametric_curve_ref_for_pick(
|
||||
}
|
||||
|
||||
impl ConstraintKind {
|
||||
/// Per-kind visibility bit used by CONSTRAINTBARMODE for the standard
|
||||
/// geometric-constraint family. Helper relations which have no standard
|
||||
/// bit stay visible under the normal display policy.
|
||||
pub const fn bar_mode_bit(self) -> Option<i16> {
|
||||
match self {
|
||||
Self::Horizontal => Some(1),
|
||||
Self::Vertical => Some(2),
|
||||
Self::Perpendicular => Some(4),
|
||||
Self::Parallel => Some(8),
|
||||
Self::Tangent => Some(16),
|
||||
Self::Smooth => Some(32),
|
||||
Self::Coincident => Some(64),
|
||||
Self::Concentric => Some(128),
|
||||
Self::Colinear => Some(256),
|
||||
Self::Symmetric => Some(512),
|
||||
Self::Equal => Some(1024),
|
||||
Self::Fixed => Some(2048),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Coincident => "Coincident",
|
||||
@@ -1083,10 +1104,15 @@ impl super::Scene {
|
||||
&self,
|
||||
scope: ParametricScope,
|
||||
id: ConstraintId,
|
||||
kind: ConstraintKind,
|
||||
related_entity_selected: bool,
|
||||
display_mode: i16,
|
||||
bar_mode: i16,
|
||||
) -> bool {
|
||||
self.is_parametric_constraint_visible(scope, id)
|
||||
&& kind
|
||||
.bar_mode_bit()
|
||||
.is_none_or(|bit| bar_mode & bit != 0)
|
||||
&& (self.shown_parametric_constraints.contains(&(scope, id))
|
||||
|| (display_mode & 2 != 0 && related_entity_selected))
|
||||
}
|
||||
@@ -1437,40 +1463,6 @@ impl super::Scene {
|
||||
inferred
|
||||
}
|
||||
|
||||
pub fn smooth_constraint_refs(&self, handles: &[Handle]) -> Option<Vec<ParametricRef>> {
|
||||
let [first, second] = handles else {
|
||||
return None;
|
||||
};
|
||||
let first_entity = self.document.get_entity(*first)?;
|
||||
let second_entity = self.document.get_entity(*second)?;
|
||||
let (spline_handle, spline, target_handle, target) = match (first_entity, second_entity) {
|
||||
(acadrust::EntityType::Spline(spline), target) => (*first, spline, *second, target),
|
||||
(target, acadrust::EntityType::Spline(spline)) => (*second, spline, *first, target),
|
||||
_ => return None,
|
||||
};
|
||||
if spline.flags.closed || spline.flags.periodic {
|
||||
return None;
|
||||
}
|
||||
let spline_points =
|
||||
super::dimension_assoc::source_points(&acadrust::EntityType::Spline(spline.clone()));
|
||||
let target_points = super::dimension_assoc::source_points(target);
|
||||
let spline_ends = [*spline_points.first()?, *spline_points.last()?];
|
||||
let target_ends = [*target_points.first()?, *target_points.last()?];
|
||||
let mut best = (f64::INFINITY, 0, 0);
|
||||
for (source_marker, source) in spline_ends.iter().enumerate() {
|
||||
for (target_marker, target) in target_ends.iter().enumerate() {
|
||||
let distance = (*source - *target).length_squared();
|
||||
if distance < best.0 {
|
||||
best = (distance, source_marker, target_marker);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(vec![
|
||||
ParametricRef::point(spline_handle, best.1 as i32),
|
||||
ParametricRef::point(target_handle, best.2 as i32),
|
||||
])
|
||||
}
|
||||
|
||||
/// The constraint set for `scope`, if one has been created.
|
||||
pub fn parametric_constraint_set(
|
||||
&self,
|
||||
@@ -1521,6 +1513,7 @@ impl super::Scene {
|
||||
vp_size: (f32, f32),
|
||||
show_values: bool,
|
||||
display_mode: i16,
|
||||
bar_mode: i16,
|
||||
) -> Vec<(
|
||||
ConstraintId,
|
||||
iced::Point,
|
||||
@@ -1555,7 +1548,14 @@ impl super::Scene {
|
||||
.iter()
|
||||
.any(|reference| self.selected.contains(&reference.entity)
|
||||
|| self.preview_hidden.contains(&reference.entity));
|
||||
self.should_display_parametric_constraint(scope, c.id, selected, display_mode)
|
||||
self.should_display_parametric_constraint(
|
||||
scope,
|
||||
c.id,
|
||||
c.kind,
|
||||
selected,
|
||||
display_mode,
|
||||
bar_mode,
|
||||
)
|
||||
})
|
||||
.flat_map(|c| {
|
||||
let is_conflicting = set.conflicts.iter().any(|(id, _)| *id == c.id);
|
||||
@@ -1627,6 +1627,7 @@ impl super::Scene {
|
||||
vp_size: (f32, f32),
|
||||
show_values: bool,
|
||||
display_mode: i16,
|
||||
bar_mode: i16,
|
||||
p: iced::Point,
|
||||
) -> Option<ConstraintId> {
|
||||
let placements = self.constraint_glyph_placements_screen(
|
||||
@@ -1634,6 +1635,7 @@ impl super::Scene {
|
||||
vp_size,
|
||||
show_values,
|
||||
display_mode,
|
||||
bar_mode,
|
||||
);
|
||||
let glyphs: Vec<(iced::Point, [f32; 2], String, bool)> = placements
|
||||
.iter()
|
||||
@@ -2213,18 +2215,34 @@ mod tests {
|
||||
vec![ParametricRef::whole(h(1))],
|
||||
None,
|
||||
);
|
||||
assert!(!scene.should_display_parametric_constraint(scope, id, false, 3));
|
||||
assert!(scene.should_display_parametric_constraint(scope, id, true, 2));
|
||||
assert!(!scene.should_display_parametric_constraint(scope, id, true, 1));
|
||||
assert!(!scene.should_display_parametric_constraint(
|
||||
scope, id, ConstraintKind::Horizontal, false, 3, 4095
|
||||
));
|
||||
assert!(scene.should_display_parametric_constraint(
|
||||
scope, id, ConstraintKind::Horizontal, true, 2, 4095
|
||||
));
|
||||
assert!(!scene.should_display_parametric_constraint(
|
||||
scope, id, ConstraintKind::Horizontal, true, 1, 4095
|
||||
));
|
||||
scene.note_parametric_constraint_applied(scope, id, 1);
|
||||
assert!(scene.should_display_parametric_constraint(scope, id, false, 0));
|
||||
assert!(scene.should_display_parametric_constraint(
|
||||
scope, id, ConstraintKind::Horizontal, false, 0, 4095
|
||||
));
|
||||
scene.set_parametric_constraint_visibility(scope, None, false, false);
|
||||
assert!(!scene.should_display_parametric_constraint(scope, id, true, 3));
|
||||
assert!(!scene.should_display_parametric_constraint(
|
||||
scope, id, ConstraintKind::Horizontal, true, 3, 4095
|
||||
));
|
||||
scene.set_parametric_constraint_visibility(scope, None, false, true);
|
||||
assert!(scene.should_display_parametric_constraint(scope, id, false, 0));
|
||||
assert!(scene.should_display_parametric_constraint(
|
||||
scope, id, ConstraintKind::Horizontal, false, 0, 4095
|
||||
));
|
||||
scene.note_parametric_constraint_applied(scope, id, 0);
|
||||
assert!(!scene.should_display_parametric_constraint(scope, id, false, 3));
|
||||
assert!(scene.should_display_parametric_constraint(scope, id, true, 2));
|
||||
assert!(!scene.should_display_parametric_constraint(
|
||||
scope, id, ConstraintKind::Horizontal, false, 3, 4095
|
||||
));
|
||||
assert!(scene.should_display_parametric_constraint(
|
||||
scope, id, ConstraintKind::Horizontal, true, 2, 4095
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2238,7 +2256,9 @@ mod tests {
|
||||
);
|
||||
scene.hidden_parametric_constraints.insert((scope, id));
|
||||
|
||||
assert!(!scene.should_display_parametric_constraint(scope, id, true, 2));
|
||||
assert!(!scene.should_display_parametric_constraint(
|
||||
scope, id, ConstraintKind::Horizontal, true, 2, 4095
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+173
-30
@@ -1697,26 +1697,167 @@ fn build_constraint(
|
||||
}
|
||||
}
|
||||
|
||||
fn smooth_target_jet(entity: &EntityType, marker: i32) -> Option<cadkernel::space::CurveJet> {
|
||||
let parameter = match marker {
|
||||
0 => 0.0,
|
||||
1 => 1.0,
|
||||
_ => return None,
|
||||
};
|
||||
let curve = crate::entities::curve::entity_curve(entity)?;
|
||||
if curve.is_closed() {
|
||||
return None;
|
||||
fn smooth_refs(refs: &[ParametricRef]) -> Option<(ParametricRef, ParametricRef, ParametricRef)> {
|
||||
match refs {
|
||||
[source, target_endpoint] => Some((
|
||||
*source,
|
||||
*target_endpoint,
|
||||
ParametricRef::whole(target_endpoint.entity),
|
||||
)),
|
||||
[source, target_endpoint, target_curve]
|
||||
if target_endpoint.entity == target_curve.entity =>
|
||||
{
|
||||
Some((*source, *target_endpoint, *target_curve))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
let step = if marker == 0 { 1.0e-4 } else { -1.0e-4 };
|
||||
}
|
||||
|
||||
fn smooth_target_planar_curve(
|
||||
entity: &EntityType,
|
||||
curve_reference: ParametricRef,
|
||||
) -> Option<cadkernel::space::PlanarCurve> {
|
||||
let planar = crate::entities::curve::entity_curve(entity)?;
|
||||
let Some(segment) = curve_reference.segment_index() else {
|
||||
return (!planar.is_closed()).then_some(planar);
|
||||
};
|
||||
let curve = planar.curve.segments().into_iter().nth(segment)?;
|
||||
Some(cadkernel::space::PlanarCurve::new(planar.plane, curve))
|
||||
}
|
||||
|
||||
fn smooth_endpoint_parameter(
|
||||
entity: &EntityType,
|
||||
endpoint_reference: ParametricRef,
|
||||
curve: &cadkernel::space::PlanarCurve,
|
||||
) -> Option<f64> {
|
||||
let marker = endpoint_reference.marker?;
|
||||
let endpoint = super::parametric_constraints::resolve_point(entity, marker)?;
|
||||
let endpoint = cadkernel::space::Vec3::new(endpoint.x, endpoint.y, endpoint.z);
|
||||
let start = cadkernel::space::Vec3::from(curve.point_at(0.0));
|
||||
let end = cadkernel::space::Vec3::from(curve.point_at(1.0));
|
||||
Some(if endpoint.distance(start) <= endpoint.distance(end) {
|
||||
0.0
|
||||
} else {
|
||||
1.0
|
||||
})
|
||||
}
|
||||
|
||||
fn smooth_target_jet(
|
||||
entity: &EntityType,
|
||||
endpoint_reference: ParametricRef,
|
||||
curve_reference: ParametricRef,
|
||||
) -> Option<cadkernel::space::CurveJet> {
|
||||
if curve_reference.segment_index().is_none() {
|
||||
if let EntityType::Spline(spline) = entity {
|
||||
let endpoint = match endpoint_reference.marker {
|
||||
Some(0) => cadkernel::space::SplineEnd::Start,
|
||||
Some(1) => cadkernel::space::SplineEnd::End,
|
||||
_ => return None,
|
||||
};
|
||||
let nurbs = crate::entities::spline::nurbs3(spline)?;
|
||||
return cadkernel::space::CurveJet::from_nurbs(&nurbs, endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
let curve = smooth_target_planar_curve(entity, curve_reference)?;
|
||||
let parameter = smooth_endpoint_parameter(entity, endpoint_reference, &curve)?;
|
||||
|
||||
let point = curve.point_at(parameter);
|
||||
let tangent = curve.tangent_at(parameter);
|
||||
let next = curve.point_at(parameter + step);
|
||||
let third = curve.point_at(parameter + 2.0 * step);
|
||||
Some(cadkernel::space::CurveJet {
|
||||
point,
|
||||
tangent,
|
||||
curvature: cadkernel::space::curve::curvature_through(point, next, third),
|
||||
})
|
||||
match &curve.curve {
|
||||
cadkernel::geom2d::Curve::Line(_) => {
|
||||
cadkernel::space::CurveJet::linear(point, tangent)
|
||||
}
|
||||
cadkernel::geom2d::Curve::Arc(arc) => cadkernel::space::CurveJet::circular(
|
||||
point,
|
||||
tangent,
|
||||
curve.plane.point_at(arc.centre),
|
||||
),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn smooth_spline_plane(spline: &acadrust::entities::Spline) -> Option<cadkernel::space::Plane> {
|
||||
let source = if crate::entities::spline::uses_fit_method(spline) {
|
||||
&spline.fit_points
|
||||
} else {
|
||||
&spline.control_points
|
||||
};
|
||||
let origin = source.first()?;
|
||||
let origin_vector = cadkernel::space::Vec3::new(origin.x, origin.y, origin.z);
|
||||
let along = source
|
||||
.iter()
|
||||
.skip(1)
|
||||
.map(|point| cadkernel::space::Vec3::new(point.x, point.y, point.z) - origin_vector)
|
||||
.find(|vector| vector.length_squared() > 1.0e-24)?;
|
||||
let normal = source
|
||||
.iter()
|
||||
.skip(1)
|
||||
.map(|point| cadkernel::space::Vec3::new(point.x, point.y, point.z) - origin_vector)
|
||||
.map(|vector| along.cross(vector))
|
||||
.find(|vector| vector.length_squared() > 1.0e-24)?;
|
||||
let plane = cadkernel::space::Plane::orthonormal(
|
||||
origin_vector.to_array(),
|
||||
along.to_array(),
|
||||
normal.to_array(),
|
||||
)?;
|
||||
let points: Vec<_> = source.iter().map(|point| [point.x, point.y, point.z]).collect();
|
||||
let tolerance = cadkernel::space::coplanarity_tolerance(&points);
|
||||
source
|
||||
.iter()
|
||||
.all(|point| plane.contains([point.x, point.y, point.z], tolerance))
|
||||
.then_some(plane)
|
||||
}
|
||||
|
||||
fn smooth_entity_plane(
|
||||
entity: &EntityType,
|
||||
curve_reference: ParametricRef,
|
||||
) -> Option<cadkernel::space::Plane> {
|
||||
if let EntityType::Spline(spline) = entity {
|
||||
return crate::entities::curve::entity_curve(entity)
|
||||
.map(|curve| curve.plane)
|
||||
.or_else(|| smooth_spline_plane(spline));
|
||||
}
|
||||
smooth_target_planar_curve(entity, curve_reference).map(|curve| curve.plane)
|
||||
}
|
||||
|
||||
fn smooth_refs_share_plane(
|
||||
document: &acadrust::CadDocument,
|
||||
source_ref: ParametricRef,
|
||||
target_curve_ref: ParametricRef,
|
||||
) -> bool {
|
||||
let Some(source_entity) = document.get_entity(source_ref.entity) else {
|
||||
return false;
|
||||
};
|
||||
let Some(source_plane) = smooth_entity_plane(source_entity, ParametricRef::whole(source_ref.entity))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let Some(target_entity) = document.get_entity(target_curve_ref.entity) else {
|
||||
return false;
|
||||
};
|
||||
let Some(target_plane) = smooth_entity_plane(target_entity, target_curve_ref) else {
|
||||
return false;
|
||||
};
|
||||
let points = [source_plane.origin, target_plane.origin];
|
||||
let tolerance = cadkernel::space::coplanarity_tolerance(&points);
|
||||
if let EntityType::Line(line) = target_entity {
|
||||
return source_plane
|
||||
.contains([line.start.x, line.start.y, line.start.z], tolerance)
|
||||
&& source_plane
|
||||
.contains([line.end.x, line.end.y, line.end.z], tolerance);
|
||||
}
|
||||
let (Some(source_normal), Some(target_normal)) =
|
||||
(source_plane.normal(), target_plane.normal())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
cadkernel::space::Vec3::from(source_normal)
|
||||
.cross(cadkernel::space::Vec3::from(target_normal))
|
||||
.length()
|
||||
<= 1.0e-9
|
||||
&& source_plane.contains(target_plane.origin, tolerance)
|
||||
&& target_plane.contains(source_plane.origin, tolerance)
|
||||
}
|
||||
|
||||
fn apply_smooth_constraints(
|
||||
@@ -1729,7 +1870,9 @@ fn apply_smooth_constraints(
|
||||
.iter()
|
||||
.filter(|constraint| constraint.enabled && constraint.kind == ConstraintKind::Smooth)
|
||||
{
|
||||
let [source_ref, target_ref] = constraint.refs.as_slice() else {
|
||||
let Some((source_ref, target_ref, target_curve_ref)) =
|
||||
smooth_refs(&constraint.refs)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let source = results
|
||||
@@ -1750,10 +1893,7 @@ fn apply_smooth_constraints(
|
||||
Some(1) => cadkernel::space::SplineEnd::End,
|
||||
_ => continue,
|
||||
};
|
||||
let Some(target_marker) = target_ref.marker else {
|
||||
continue;
|
||||
};
|
||||
let Some(target_jet) = smooth_target_jet(&target, target_marker) else {
|
||||
let Some(target_jet) = smooth_target_jet(&target, target_ref, target_curve_ref) else {
|
||||
continue;
|
||||
};
|
||||
let Some(curve) = crate::entities::spline::nurbs3(&spline) else {
|
||||
@@ -2871,13 +3011,8 @@ impl Scene {
|
||||
refs: &[ParametricRef],
|
||||
driving_param: Option<&super::named_parameters::DrivingValue>,
|
||||
) -> Result<(), &'static str> {
|
||||
if !refs_share_supported_plane(&self.document, refs) {
|
||||
return Err(
|
||||
"Constraint references must be supported entities on the same world-XY plane.",
|
||||
);
|
||||
}
|
||||
if kind == ConstraintKind::Smooth {
|
||||
let [source_ref, target_ref] = refs else {
|
||||
let Some((source_ref, target_ref, target_curve_ref)) = smooth_refs(refs) else {
|
||||
return Err("Smooth requires one spline endpoint and one target endpoint.");
|
||||
};
|
||||
if source_ref.entity == target_ref.entity {
|
||||
@@ -2895,13 +3030,16 @@ impl Scene {
|
||||
Some(1) => cadkernel::space::SplineEnd::End,
|
||||
_ => return Err("Smooth requires a spline endpoint."),
|
||||
};
|
||||
let Some(target_marker) = target_ref.marker else {
|
||||
let Some(_) = target_ref.marker else {
|
||||
return Err("Smooth requires a target endpoint.");
|
||||
};
|
||||
let Some(target) = self.document.get_entity(target_ref.entity) else {
|
||||
return Err("Smooth target does not exist.");
|
||||
};
|
||||
let Some(target_jet) = smooth_target_jet(target, target_marker) else {
|
||||
if !smooth_refs_share_plane(&self.document, source_ref, target_curve_ref) {
|
||||
return Err("Smooth requires both curves to lie on the same plane.");
|
||||
}
|
||||
let Some(target_jet) = smooth_target_jet(target, target_ref, target_curve_ref) else {
|
||||
return Err("The selected target does not support endpoint smoothing.");
|
||||
};
|
||||
let Some(curve) = crate::entities::spline::nurbs3(spline) else {
|
||||
@@ -2912,6 +3050,11 @@ impl Scene {
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
if !refs_share_supported_plane(&self.document, refs) {
|
||||
return Err(
|
||||
"Constraint references must be supported entities on the same world-XY plane.",
|
||||
);
|
||||
}
|
||||
let validation_target = match driving_param {
|
||||
Some(super::named_parameters::DrivingValue::Named(name))
|
||||
if !self.named_parameters.contains(name) =>
|
||||
|
||||
Reference in new issue
Block a user