Sketch Offset on two sides, or symmetric about the source (#807)
Sketch Offset can place the copy on both sides of the source, or symmetric about it.
This commit is contained in:
1 parent
5d661fa2af
commit
53238fba5f
6 files changed
+567
-144
No files matched your search
@@ -42,7 +42,12 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.icon("offset")
|
||||
.key("O")
|
||||
.enabled(in_sketch)
|
||||
.params(concat!("curves: [ids], distance: number (positive = left of the chain, or toward `side`), side?: [x,y], chain?: bool (default true: the connected chain)", created_doc!())),
|
||||
.params(concat!(
|
||||
"curves: [ids], distance: number (positive = left of the chain, or toward `side`), side?: [x,y], chain?: bool (default true: the connected chain); ",
|
||||
"direction?: one_side|symmetric (default one_side) or symmetric?: bool (the same distance each way, as for `solid.extrude`; no `side`, whose job it takes over), ",
|
||||
"distance2?: positive number (Two Sides: an independent distance the other way, as for `solid.extrude`). Each side is its own offset association, listed in `sides`",
|
||||
created_doc!()
|
||||
)),
|
||||
CommandSpec::new("sketch.offset.break", "Break Offset Association", break_offset)
|
||||
.enabled(in_sketch)
|
||||
.params("curve: derived offset curve id (retains geometry and makes its points editable)"),
|
||||
@@ -919,6 +924,148 @@ fn piece_dist(p: &Piece, q: Vec2) -> f64 {
|
||||
}
|
||||
}
|
||||
|
||||
/// One side of an offset. The curves created at the signed distance, the parallel constraints
|
||||
/// added with them, and the association when the boundary carries an offset topology.
|
||||
struct Side {
|
||||
distance: f64,
|
||||
curves: Vec<String>,
|
||||
constraints: Vec<String>,
|
||||
association: Option<String>,
|
||||
}
|
||||
|
||||
/// The signed distances an offset runs at. One side is the distance as given. `symmetric` is the
|
||||
/// same distance each way, as `direction: symmetric` means for `solid.extrude`. `distance2` is an
|
||||
/// independent distance the other way, as `distance2` means for `solid.extrude`. The primary side
|
||||
/// comes first, so a one-sided caller reads the same first entry it always did.
|
||||
fn offset_runs(d: f64, symmetric: bool, distance2: Option<f64>) -> Vec<f64> {
|
||||
match (symmetric, distance2) {
|
||||
(true, _) => vec![d.abs(), -d.abs()],
|
||||
(_, Some(second)) => vec![d, -second * d.signum()],
|
||||
_ => vec![d],
|
||||
}
|
||||
}
|
||||
|
||||
/// Build one side of a chain offset at the signed distance `d`. `order` and `sources` are the
|
||||
/// walked chain, computed once before anything is created so a second side walks the same
|
||||
/// curves rather than a pool the first side has already grown.
|
||||
fn offset_side(sk: &mut Sketch, cmd: &str, order: &[(usize, bool)], sources: &[(String, bool)], closed: bool, convex: bool, d: f64) -> Result<Side> {
|
||||
let mut made = Vec::new();
|
||||
let mut cons = Vec::new();
|
||||
if convex {
|
||||
let created = sk.add_convex_offset(sources.to_vec(), d)?;
|
||||
let roles = sk
|
||||
.offsets
|
||||
.iter()
|
||||
.filter_map(|offset| offset.topology.as_ref())
|
||||
.find(|topology| topology.id == created.association)
|
||||
.map(|topology| topology.roles.clone())
|
||||
.ok_or_else(|| bad(cmd, "new offset association is missing"))?;
|
||||
for (source, role) in sources.iter().zip(&roles) {
|
||||
if role.kind == solidcraft_sketch::OffsetRoleKind::Line {
|
||||
let a = sk.curve_index(&source.0).ok_or_else(|| bad(cmd, "offset source line vanished"))?;
|
||||
let b = sk.curve_index(&role.output).ok_or_else(|| bad(cmd, "offset output line vanished"))?;
|
||||
cons.push(add_c(sk, ConstraintKind::Parallel { a, b })?);
|
||||
}
|
||||
}
|
||||
return Ok(Side { distance: d, curves: created.curves, constraints: cons, association: Some(created.association) });
|
||||
}
|
||||
let pieces: Vec<Piece> =
|
||||
order.iter().map(|(c, f)| offset_piece(sk, *c, *f, d)).collect::<Option<_>>().ok_or_else(|| bad(cmd, "the offset collapses an arc"))?;
|
||||
let n = pieces.len();
|
||||
let start_of = |p: &Piece| match p {
|
||||
Piece::Line(a, _) => *a,
|
||||
Piece::Arc(_, _, _, s, _, _) => *s,
|
||||
Piece::Free(v) => v.first().copied().unwrap_or_default(),
|
||||
};
|
||||
let end_of = |p: &Piece| match p {
|
||||
Piece::Line(_, b) => *b,
|
||||
Piece::Arc(_, _, _, _, e, _) => *e,
|
||||
Piece::Free(v) => v.last().copied().unwrap_or_default(),
|
||||
};
|
||||
// Junction points between consecutive pieces.
|
||||
let joint = |x: &Piece, y: &Piece| -> Vec2 {
|
||||
let (e, s0) = (end_of(x), start_of(y));
|
||||
if e.dist(s0) < TOL * 10.0 {
|
||||
return e;
|
||||
}
|
||||
let guess = (e + s0) * 0.5;
|
||||
if matches!(x, Piece::Free(_)) || matches!(y, Piece::Free(_)) {
|
||||
return guess;
|
||||
}
|
||||
intersections(&piece_shape(x), &piece_shape(y)).into_iter().min_by(|a, b| a.dist(guess).total_cmp(&b.dist(guess))).unwrap_or(guess)
|
||||
};
|
||||
let (Some(head), Some(tail)) = (pieces.first(), pieces.last()) else {
|
||||
return Err(bad(cmd, "no curves"));
|
||||
};
|
||||
let mut joints: Vec<Vec2> = Vec::new();
|
||||
joints.push(if closed { joint(tail, head) } else { start_of(head) });
|
||||
for k in 0..n.saturating_sub(1) {
|
||||
let (Some(x), Some(y)) = (pieces.get(k), pieces.get(k + 1)) else {
|
||||
return Err(bad(cmd, "the offset chain changed while it was built"));
|
||||
};
|
||||
joints.push(joint(x, y));
|
||||
}
|
||||
if !closed {
|
||||
joints.push(end_of(tail));
|
||||
}
|
||||
// A straight piece whose joints come out reversed (or meet) was overrun by its
|
||||
// neighbours: the offset is larger than the shape allows there.
|
||||
for (k, pc) in pieces.iter().enumerate() {
|
||||
if let Piece::Line(a, b) = pc {
|
||||
let (ja, jb) = (joints.get(k).copied(), joints.get(if closed { (k + 1) % n } else { k + 1 }).copied());
|
||||
if let (Some(ja), Some(jb)) = (ja, jb)
|
||||
&& (jb - ja).dot(*b - *a) <= TOL * a.dist(*b)
|
||||
{
|
||||
return Err(bad(cmd, "the offset is larger than the shape allows (the offset curves would cross)"));
|
||||
}
|
||||
}
|
||||
}
|
||||
let pts: Vec<usize> = joints.iter().map(|q| sk.add_point(*q, None)).collect::<std::result::Result<_, _>>()?;
|
||||
for (k, pc) in pieces.iter().enumerate() {
|
||||
let (Some(pa), Some(pb)) = (pts.get(k).copied(), pts.get(if closed { (k + 1) % n } else { k + 1 }).copied()) else {
|
||||
return Err(bad(cmd, "the offset chain changed while it was built"));
|
||||
};
|
||||
let orig = order.get(k).map(|(c, _)| *c).ok_or_else(|| bad(cmd, "the offset chain changed while it was built"))?;
|
||||
match pc.clone() {
|
||||
Piece::Free(v) => {
|
||||
// A fit spline through a few of the samples, ending at the joints.
|
||||
let step = (v.len() / 24).max(1);
|
||||
let mut idx = vec![pa];
|
||||
for q in v.iter().skip(step).step_by(step) {
|
||||
if q.dist(*v.last().unwrap_or(q)) > 1e-6 {
|
||||
idx.push(sk.add_point(*q, None)?);
|
||||
}
|
||||
}
|
||||
idx.push(pb);
|
||||
idx.dedup();
|
||||
if idx.len() >= 2 {
|
||||
made.push(sk.add_curve(CurveKind::Spline { pts: idx, control: false, degree: 3, periodic: false }, None)?);
|
||||
}
|
||||
}
|
||||
Piece::Line(..) => {
|
||||
let l = sk.add_line_pts(pa, pb, None)?;
|
||||
cons.push(add_c(sk, ConstraintKind::Parallel { a: orig, b: l })?);
|
||||
made.push(l);
|
||||
}
|
||||
Piece::Arc(ci, cc, _, _, _, ccw) => {
|
||||
let (qa, qb) = (sk.point(pa).unwrap_or_default(), sk.point(pb).unwrap_or_default());
|
||||
let a = if ccw {
|
||||
sk.add_arc(cc, qa, qb, [Some(ci), Some(pa), Some(pb)], None)?
|
||||
} else {
|
||||
sk.add_arc(cc, qb, qa, [Some(ci), Some(pb), Some(pa)], None)?
|
||||
};
|
||||
made.push(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
let outputs = ids_of(sk, &made);
|
||||
if pieces.iter().all(|p| !matches!(p, Piece::Free(_))) {
|
||||
sk.offsets.push(solidcraft_sketch::OffsetChain { sources: sources.to_vec(), outputs: outputs.clone(), distance: d, closed, topology: None });
|
||||
sk.refresh_offsets()?;
|
||||
}
|
||||
Ok(Side { distance: d, curves: outputs, constraints: cons, association: None })
|
||||
}
|
||||
|
||||
fn offset(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "sketch.offset";
|
||||
let ids = string_list(p, "curves");
|
||||
@@ -928,7 +1075,32 @@ fn offset(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let d0 = num(p, "distance").filter(|d| d.abs() > 1e-9).ok_or_else(|| bad(cmd, "`distance` must be a non-zero number"))?;
|
||||
let side = p.get("side").and_then(vec2);
|
||||
let use_chain = bool_(p, "chain").unwrap_or(true);
|
||||
let (out, info) = edit(s, p, cmd, true, |sk, doc| {
|
||||
// Both sides at once. The words keep the meaning `solid.extrude` gives them: `symmetric` is
|
||||
// one distance applied each way, `distance2` is an independent distance the other way.
|
||||
let symmetric = match str_(p, "direction").map(str::to_ascii_lowercase).as_deref() {
|
||||
None | Some("positive") | Some("one_side") => bool_(p, "symmetric").unwrap_or(false),
|
||||
Some("symmetric") => true,
|
||||
Some("negative" | "reverse" | "flip") => {
|
||||
return Err(bad(cmd, "an offset has no negative direction: give a negative `distance`, or a `side` to offset toward"));
|
||||
}
|
||||
Some(other) => return Err(bad(cmd, format!("unknown direction `{other}` (one_side, symmetric)"))),
|
||||
};
|
||||
let distance2 = match p.get("distance2") {
|
||||
None => None,
|
||||
Some(_) => Some(num(p, "distance2").filter(|d| *d > 1e-9).ok_or_else(|| {
|
||||
bad(
|
||||
cmd,
|
||||
"`distance2` must be a positive number: the other side's distance (the sign of `distance`, or `side`, picks which side is which)",
|
||||
)
|
||||
})?),
|
||||
};
|
||||
if symmetric && distance2.is_some() {
|
||||
return Err(bad(cmd, "give `distance2` or `symmetric`, not both"));
|
||||
}
|
||||
if symmetric && side.is_some() {
|
||||
return Err(bad(cmd, "`symmetric` offsets both sides, so `side` has no side left to pick: drop one of them"));
|
||||
}
|
||||
let (sides, info) = edit(s, p, cmd, true, |sk, doc| {
|
||||
sk.refresh_offsets()?;
|
||||
let rep = solve_current(sk, doc)?;
|
||||
if !rep.keeps_orientation() {
|
||||
@@ -945,30 +1117,42 @@ fn offset(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"offsets of a boundary with retired corner arcs are not supported yet; break its offset association to use the current fixed topology",
|
||||
));
|
||||
}
|
||||
let mut made = Vec::new();
|
||||
let mut cons = Vec::new();
|
||||
// Circles offset on their own.
|
||||
let mut sides: Vec<Side> = Vec::new();
|
||||
// Circles offset on their own, concentrically. Both sides are then a smaller circle and
|
||||
// a larger one; the one toward the centre still has to have a size.
|
||||
if let Some(CurveKind::Circle { c, r }) = sk.curves.get(first).map(|c| c.kind.clone()) {
|
||||
let cc = sk.point(c).unwrap_or_default();
|
||||
let source = sk.curves.get(first).map(|c| c.id.clone()).ok_or_else(|| bad(cmd, "offset source circle vanished"))?;
|
||||
let grow = match side {
|
||||
Some(q) => q.dist(cc) > r,
|
||||
None => d0 < 0.0,
|
||||
};
|
||||
let nr = if grow { r + d0.abs() } else { r - d0.abs() };
|
||||
if nr <= 1e-9 {
|
||||
return Err(bad(cmd, "the offset circle would have no size"));
|
||||
let radii: Vec<f64> = match (symmetric, distance2) {
|
||||
(true, _) => vec![r - d0.abs(), r + d0.abs()],
|
||||
(_, Some(second)) if grow => vec![r + d0.abs(), r - second],
|
||||
(_, Some(second)) => vec![r - d0.abs(), r + second],
|
||||
_ if grow => vec![r + d0.abs()],
|
||||
_ => vec![r - d0.abs()],
|
||||
};
|
||||
for nr in radii {
|
||||
if nr <= 1e-9 {
|
||||
return Err(bad(cmd, "the offset circle would have no size"));
|
||||
}
|
||||
let ci = sk.add_circle(cc, nr, Some(c), None)?;
|
||||
let outputs = ids_of(sk, &[ci]);
|
||||
sk.offsets.push(solidcraft_sketch::OffsetChain {
|
||||
sources: vec![(source.clone(), true)],
|
||||
outputs: outputs.clone(),
|
||||
distance: r - nr,
|
||||
closed: true,
|
||||
topology: None,
|
||||
});
|
||||
sides.push(Side { distance: r - nr, curves: outputs, constraints: Vec::new(), association: None });
|
||||
}
|
||||
let ci = sk.add_circle(cc, nr, Some(c), None)?;
|
||||
made.push(ci);
|
||||
sk.offsets.push(solidcraft_sketch::OffsetChain {
|
||||
sources: vec![(ids[0].clone(), true)],
|
||||
outputs: ids_of(sk, &made),
|
||||
distance: r - nr,
|
||||
closed: true,
|
||||
topology: None,
|
||||
});
|
||||
return Ok((ids_of(sk, &made), cons, None));
|
||||
return Ok(sides);
|
||||
}
|
||||
// The chain is walked once, before anything is created: a second side has to follow the
|
||||
// same source curves, not a pool the first side has already added its output to.
|
||||
let pool: Vec<usize> = if use_chain {
|
||||
(0..sk.curves.len())
|
||||
.filter(|i| sk.curves.get(*i).is_some_and(|c| c.construction == sk.curves.get(first).is_some_and(|f| f.construction)))
|
||||
@@ -989,6 +1173,7 @@ fn offset(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
None => d0,
|
||||
};
|
||||
let runs = offset_runs(d, symmetric, distance2);
|
||||
if order.iter().any(|(i, _)| sk.shape(*i).is_none()) {
|
||||
// A smooth free-form entity is offset from exact pieces. Junctions that
|
||||
// would require trimming/intersecting independent normal offsets are
|
||||
@@ -996,9 +1181,12 @@ fn offset(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if order.len() != 1 {
|
||||
return Err(bad(cmd, "offset smooth free-form curves individually; mixed chain junctions are ambiguous"));
|
||||
}
|
||||
let (source, forward) = order[0];
|
||||
let output = sk.add_freeform_offset(source, forward, d)?;
|
||||
return Ok((ids_of(sk, &[output]), cons, None));
|
||||
let (source, forward) = *order.first().ok_or_else(|| bad(cmd, "no curves"))?;
|
||||
for dd in runs {
|
||||
let output = sk.add_freeform_offset(source, forward, dd)?;
|
||||
sides.push(Side { distance: dd, curves: ids_of(sk, &[output]), constraints: Vec::new(), association: None });
|
||||
}
|
||||
return Ok(sides);
|
||||
}
|
||||
let sources = order
|
||||
.iter()
|
||||
@@ -1006,115 +1194,30 @@ fn offset(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
sk.curves.get(*index).map(|curve| (curve.id.clone(), *forward)).ok_or_else(|| bad(cmd, "offset chain source vanished"))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
if closed && sk.convex_offset_supported(&sources)? {
|
||||
let created = sk.add_convex_offset(sources.clone(), d)?;
|
||||
let roles = sk
|
||||
.offsets
|
||||
.iter()
|
||||
.filter_map(|offset| offset.topology.as_ref())
|
||||
.find(|topology| topology.id == created.association)
|
||||
.map(|topology| topology.roles.clone())
|
||||
.ok_or_else(|| bad(cmd, "new offset association is missing"))?;
|
||||
for (source, role) in sources.iter().zip(&roles) {
|
||||
if role.kind == solidcraft_sketch::OffsetRoleKind::Line {
|
||||
let a = sk.curve_index(&source.0).ok_or_else(|| bad(cmd, "offset source line vanished"))?;
|
||||
let b = sk.curve_index(&role.output).ok_or_else(|| bad(cmd, "offset output line vanished"))?;
|
||||
cons.push(add_c(sk, ConstraintKind::Parallel { a, b })?);
|
||||
}
|
||||
}
|
||||
return Ok((created.curves, cons, Some(created.association)));
|
||||
let convex = closed && sk.convex_offset_supported(&sources)?;
|
||||
for dd in runs {
|
||||
sides.push(offset_side(sk, cmd, &order, &sources, closed, convex, dd)?);
|
||||
}
|
||||
let pieces: Vec<Piece> =
|
||||
order.iter().map(|(c, f)| offset_piece(sk, *c, *f, d)).collect::<Option<_>>().ok_or_else(|| bad(cmd, "the offset collapses an arc"))?;
|
||||
let n = pieces.len();
|
||||
let start_of = |p: &Piece| match p {
|
||||
Piece::Line(a, _) => *a,
|
||||
Piece::Arc(_, _, _, s, _, _) => *s,
|
||||
Piece::Free(v) => v.first().copied().unwrap_or_default(),
|
||||
};
|
||||
let end_of = |p: &Piece| match p {
|
||||
Piece::Line(_, b) => *b,
|
||||
Piece::Arc(_, _, _, _, e, _) => *e,
|
||||
Piece::Free(v) => v.last().copied().unwrap_or_default(),
|
||||
};
|
||||
// Junction points between consecutive pieces.
|
||||
let joint = |x: &Piece, y: &Piece| -> Vec2 {
|
||||
let (e, s0) = (end_of(x), start_of(y));
|
||||
if e.dist(s0) < TOL * 10.0 {
|
||||
return e;
|
||||
}
|
||||
let guess = (e + s0) * 0.5;
|
||||
if matches!(x, Piece::Free(_)) || matches!(y, Piece::Free(_)) {
|
||||
return guess;
|
||||
}
|
||||
intersections(&piece_shape(x), &piece_shape(y)).into_iter().min_by(|a, b| a.dist(guess).total_cmp(&b.dist(guess))).unwrap_or(guess)
|
||||
};
|
||||
let mut joints: Vec<Vec2> = Vec::new();
|
||||
joints.push(if closed { joint(&pieces[n - 1], &pieces[0]) } else { start_of(&pieces[0]) });
|
||||
for k in 0..n.saturating_sub(1) {
|
||||
joints.push(joint(&pieces[k], &pieces[k + 1]));
|
||||
}
|
||||
if !closed {
|
||||
joints.push(end_of(&pieces[n - 1]));
|
||||
}
|
||||
// A straight piece whose joints come out reversed (or meet) was overrun by its
|
||||
// neighbours: the offset is larger than the shape allows there.
|
||||
for (k, pc) in pieces.iter().enumerate() {
|
||||
if let Piece::Line(a, b) = pc {
|
||||
let (ja, jb) = (joints.get(k).copied(), joints.get(if closed { (k + 1) % n } else { k + 1 }).copied());
|
||||
if let (Some(ja), Some(jb)) = (ja, jb)
|
||||
&& (jb - ja).dot(*b - *a) <= TOL * a.dist(*b)
|
||||
{
|
||||
return Err(bad(cmd, "the offset is larger than the shape allows (the offset curves would cross)"));
|
||||
}
|
||||
}
|
||||
}
|
||||
let pts: Vec<usize> = joints.iter().map(|q| sk.add_point(*q, None)).collect::<std::result::Result<_, _>>()?;
|
||||
for (k, pc) in pieces.iter().enumerate() {
|
||||
let (pa, pb) = (pts[k], if closed { pts[(k + 1) % n] } else { pts[k + 1] });
|
||||
let (orig, _) = order[k];
|
||||
match pc.clone() {
|
||||
Piece::Free(v) => {
|
||||
// A fit spline through a few of the samples, ending at the joints.
|
||||
let step = (v.len() / 24).max(1);
|
||||
let mut idx = vec![pa];
|
||||
for q in v.iter().skip(step).step_by(step) {
|
||||
if q.dist(*v.last().unwrap_or(q)) > 1e-6 {
|
||||
idx.push(sk.add_point(*q, None)?);
|
||||
}
|
||||
}
|
||||
idx.push(pb);
|
||||
idx.dedup();
|
||||
if idx.len() >= 2 {
|
||||
made.push(sk.add_curve(CurveKind::Spline { pts: idx, control: false, degree: 3, periodic: false }, None)?);
|
||||
}
|
||||
}
|
||||
Piece::Line(..) => {
|
||||
let l = sk.add_line_pts(pa, pb, None)?;
|
||||
cons.push(add_c(sk, ConstraintKind::Parallel { a: orig, b: l })?);
|
||||
made.push(l);
|
||||
}
|
||||
Piece::Arc(ci, cc, _, _, _, ccw) => {
|
||||
let (qa, qb) = (sk.point(pa).unwrap_or_default(), sk.point(pb).unwrap_or_default());
|
||||
let a = if ccw {
|
||||
sk.add_arc(cc, qa, qb, [Some(ci), Some(pa), Some(pb)], None)?
|
||||
} else {
|
||||
sk.add_arc(cc, qb, qa, [Some(ci), Some(pb), Some(pa)], None)?
|
||||
};
|
||||
made.push(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
let outputs = ids_of(sk, &made);
|
||||
if pieces.iter().all(|p| !matches!(p, Piece::Free(_))) {
|
||||
let sources = order.iter().filter_map(|(i, forward)| sk.curves.get(*i).map(|c| (c.id.clone(), *forward))).collect();
|
||||
sk.offsets.push(solidcraft_sketch::OffsetChain { sources, outputs: outputs.clone(), distance: d, closed, topology: None });
|
||||
sk.refresh_offsets()?;
|
||||
}
|
||||
Ok((outputs, cons, None))
|
||||
Ok(sides)
|
||||
})?;
|
||||
let mut response = json!({"curves":out.0,"constraints":out.1,"sketch":info});
|
||||
if let Some(association) = out.2 {
|
||||
let curves: Vec<&String> = sides.iter().flat_map(|side| side.curves.iter()).collect();
|
||||
let constraints: Vec<&String> = sides.iter().flat_map(|side| side.constraints.iter()).collect();
|
||||
let each: Vec<Value> = sides
|
||||
.iter()
|
||||
.map(|side| {
|
||||
let mut entry = json!({"distance": side.distance, "curves": side.curves});
|
||||
if let Some(association) = &side.association {
|
||||
entry["association"] = json!(association);
|
||||
}
|
||||
entry
|
||||
})
|
||||
.collect();
|
||||
let mut response = json!({"curves":curves,"constraints":constraints,"sketch":info,"sides":each});
|
||||
// One side keeps the single `association` it always returned. Two sides have one each, in
|
||||
// `sides`, so there is no single association left to name.
|
||||
if let [only] = sides.as_slice()
|
||||
&& let Some(association) = &only.association
|
||||
{
|
||||
response["association"] = json!(association);
|
||||
}
|
||||
Ok(response)
|
||||
|
||||
Reference in new issue
Block a user