Skip to main content

brep_render/sketch/
external_ref.rs

1//! Project solid edges into a sketch as fixed construction references.
2//!
3//! Projected polylines become lines, circles, arcs, or line-chain fallbacks.
4//! Ground constraints pin the generated points. Edge names identify persisted
5//! references, so picking an edge again updates its coordinates in place.
6//!
7//! The sketch feature's `projectedEdges` list is the ONE record of which edges
8//! a sketch links: an edge picked inside the editor is written into that list
9//! on Finish, and an edge put into the list from the feature dialog is
10//! materialized here on the next enter, through the same classifier. The
11//! `persistentData.externalRefs` entries only remember which points and
12//! geometry each listed edge became. When a listed edge stops resolving (its
13//! feature was deleted, or a regeneration lost the edge) the link is RELEASED:
14//! the geometry stays in the sketch as ordinary fixed construction geometry
15//! ([`release_ref`]), the entry and the list item go.
16
17use crate::geometry2d::{distance as dist, point_segment_distance};
18
19use std::collections::HashSet;
20
21use serde::{Deserialize, Serialize};
22use serde_json::{Map, Value};
23
24use super::doc::{id_key, SketchConstraint, SketchDoc, SketchGeometry, SketchPoint};
25use super::PlaneFrame;
26
27/// The classification of a projected edge polyline, in plane `(u, v)` coordinates.
28#[derive(Clone, Debug, PartialEq)]
29pub enum EdgeLink {
30    /// A straight edge — link as a `line` through the two endpoints.
31    Line { a: (f64, f64), b: (f64, f64) },
32    /// A closed circular edge — link as a `circle` (center + a rim point).
33    Circle { center: (f64, f64), rim: (f64, f64) },
34    /// A circular ARC — link as an `arc` (center, start, end; CCW start→end).
35    /// The endpoints are NORMALIZED to that convention: a polyline sampled
36    /// clockwise about its center is published with its ends swapped, so the
37    /// arc covers the sampled points and never their complement.
38    Arc {
39        center: (f64, f64),
40        start: (f64, f64),
41        end: (f64, f64),
42    },
43    /// A faithful fallback for anything else — a chain of `line` segments through the
44    /// projected polyline samples.
45    Polyline { pts: Vec<(f64, f64)> },
46}
47
48impl EdgeLink {
49    /// The classified shape type; `polyline` materializes as a chain of lines.
50    pub fn kind(&self) -> &'static str {
51        match self {
52            EdgeLink::Line { .. } => "line",
53            EdgeLink::Circle { .. } => "circle",
54            EdgeLink::Arc { .. } => "arc",
55            EdgeLink::Polyline { .. } => "polyline",
56        }
57    }
58
59    /// The ordered `(u, v)` coordinates of the points this link materializes (its
60    /// geometry references them in order).
61    pub fn point_uvs(&self) -> Vec<(f64, f64)> {
62        match self {
63            EdgeLink::Line { a, b } => vec![*a, *b],
64            EdgeLink::Circle { center, rim } => vec![*center, *rim],
65            EdgeLink::Arc { center, start, end } => vec![*center, *start, *end],
66            EdgeLink::Polyline { pts } => pts.clone(),
67        }
68    }
69}
70
71/// Project a world-space polyline into the plane's `(u, v)` frame (orthogonal
72/// projection; the off-plane component is dropped). Mirrors the previous
73/// world→UV projection applied per vertex.
74pub fn project_polyline(plane: &PlaneFrame, world: &[[f64; 3]]) -> Vec<(f64, f64)> {
75    world.iter().map(|&w| plane.to_uv(w)).collect()
76}
77
78/// Classify a projected polyline (in plane `(u, v)`) as a straight line, a circle /
79/// arc, or a polyline fallback. Tolerances are RELATIVE to the polyline's extent so
80/// the same thresholds work at any sketch scale:
81///
82/// - **STRAIGHT** when every interior sample lies within `1e-4·extent` of the chord
83///   between the endpoints (a 2-sample polyline is trivially straight).
84/// - **CIRCULAR** when a circle fit through 3 well-spaced samples has a max radial
85///   residual under `1e-3·extent` (and a non-degenerate radius). Coincident
86///   endpoints → a closed `Circle`; else an `Arc`.
87/// - else the **Polyline** fallback.
88pub fn classify_uv(uv: &[(f64, f64)]) -> EdgeLink {
89    let n = uv.len();
90    if n < 2 {
91        // Degenerate — surface it as a (possibly zero-length) polyline; callers guard
92        // against < 2 samples before linking.
93        return EdgeLink::Polyline { pts: uv.to_vec() };
94    }
95    let a = uv[0];
96    let b = uv[n - 1];
97    let extent = polyline_extent(uv).max(1e-9);
98    let straight_tol = 1e-4 * extent;
99    let closed = dist(a, b) <= straight_tol;
100
101    // Two samples (or all-interior-on-chord and open) → a straight line.
102    if !closed {
103        let max_dev = uv[1..n - 1]
104            .iter()
105            .map(|&p| point_segment_distance(p, a, b).0)
106            .fold(0.0_f64, f64::max);
107        if n == 2 || max_dev <= straight_tol {
108            return EdgeLink::Line { a, b };
109        }
110    }
111
112    // Circle fit through three well-spaced samples. Sampling at 0 / n/3 / 2n/3 (NOT
113    // the last index) keeps the three distinct even for a CLOSED loop, where the
114    // first and last samples coincide.
115    if n >= 3 {
116        if let Some((cx, cy, r)) = fit_circle(uv[0], uv[n / 3], uv[(2 * n) / 3]) {
117            let circle_tol = 1e-3 * extent;
118            let residual = uv
119                .iter()
120                .map(|&p| (dist(p, (cx, cy)) - r).abs())
121                .fold(0.0_f64, f64::max);
122            if r.is_finite() && r > straight_tol && residual <= circle_tol {
123                if closed {
124                    return EdgeLink::Circle {
125                        center: (cx, cy),
126                        rim: (cx + r, cy),
127                    };
128                }
129                // The sketch convention sweeps CCW start→end (the kernel's
130                // `(end − start).rem_euclid(TAU)`, the overlay's `arc_poly`): a
131                // polyline that runs CLOCKWISE about the center must publish its
132                // ends swapped, or the arc covers the complement of the edge.
133                let (start, end) = if winding_ccw(uv, (cx, cy)) { (a, b) } else { (b, a) };
134                return EdgeLink::Arc {
135                    center: (cx, cy),
136                    start,
137                    end,
138                };
139            }
140        }
141    }
142
143    EdgeLink::Polyline { pts: uv.to_vec() }
144}
145
146/// A per-session external-reference mapping: the linked scene edge (by name + owning
147/// solid) and the sketch entities materialized for it. Persisted to / loaded from
148/// `persistentData.externalRefs` so a linked edge round-trips a commit + re-enter.
149#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
150pub struct ExternalRef {
151    /// Kernel edge name (the dedup key).
152    #[serde(rename = "edgeName")]
153    pub edge_name: String,
154    /// Owning solid's scene name (metadata; may be empty).
155    #[serde(rename = "solidName", default)]
156    pub solid_name: String,
157    /// The materialized point ids (order matches the link's geometry).
158    #[serde(rename = "pointIds", default)]
159    pub point_ids: Vec<Value>,
160    /// The materialized geometry ids (one for line/circle/arc; N-1 for a polyline
161    /// chain).
162    #[serde(rename = "geomIds", default)]
163    pub geom_ids: Vec<Value>,
164    /// The link classification (`"line"|"circle"|"arc"|"polyline"`).
165    #[serde(default)]
166    pub kind: String,
167}
168
169/// Link (or update) a picked scene edge into the sketch as an external reference.
170///
171/// Projects `world_poly` into `plane`, classifies it, and materializes external-ref
172/// points + `⏚` grounds + construction geometry, recording an [`ExternalRef`] in
173/// `refs`. Dedup: when `refs` already holds an entry for `edge_name` whose structure
174/// matches the new classification, the existing points are UPDATED in place (no
175/// duplication); when the structure differs, the old entities are removed and fresh
176/// ones created; otherwise a new ref is appended.
177///
178/// Returns whether the doc actually changed (a new/rebuilt ref, or moved coordinates)
179/// — `false` on a redundant re-link of an unchanged edge, so the caller can drop a
180/// dead undo step. Never solves; the caller re-solves.
181pub fn link_or_update(
182    doc: &mut SketchDoc,
183    refs: &mut Vec<ExternalRef>,
184    edge_name: &str,
185    solid_name: &str,
186    world_poly: &[[f64; 3]],
187    plane: &PlaneFrame,
188) -> bool {
189    if world_poly.len() < 2 {
190        return false;
191    }
192    let uv = project_polyline(plane, world_poly);
193    let link = classify_uv(&uv);
194    let new_uvs = link.point_uvs();
195
196    if let Some(pos) = refs.iter().position(|r| r.edge_name == edge_name) {
197        let structure_matches =
198            refs[pos].kind == link.kind() && refs[pos].point_ids.len() == new_uvs.len();
199        if structure_matches {
200            // Update the existing points' coordinates in place (keeps ids + geometry).
201            let mut moved = false;
202            let point_ids = refs[pos].point_ids.clone();
203            for (id, (u, v)) in point_ids.iter().zip(new_uvs.iter()) {
204                if let Some(p) = doc.point_mut(id) {
205                    if (p.x - u).abs() > 1e-12 || (p.y - v).abs() > 1e-12 {
206                        moved = true;
207                    }
208                    p.x = *u;
209                    p.y = *v;
210                    p.fixed = true;
211                    p.construction = true;
212                    p.external_reference = true;
213                }
214            }
215            if refs[pos].solid_name != solid_name {
216                refs[pos].solid_name = solid_name.to_string();
217            }
218            return moved;
219        }
220        // Structure changed (e.g. a straight edge became curved after a model edit) —
221        // drop the stale entities and rebuild the ref fresh.
222        remove_ref_entities(doc, &refs[pos].clone());
223        let (point_ids, geom_ids) = add_external_ref(doc, &link);
224        refs[pos] = ExternalRef {
225            edge_name: edge_name.to_string(),
226            solid_name: solid_name.to_string(),
227            point_ids,
228            geom_ids,
229            kind: link.kind().to_string(),
230        };
231        return true;
232    }
233
234    // A brand-new reference for this edge.
235    let (point_ids, geom_ids) = add_external_ref(doc, &link);
236    refs.push(ExternalRef {
237        edge_name: edge_name.to_string(),
238        solid_name: solid_name.to_string(),
239        point_ids,
240        geom_ids,
241        kind: link.kind().to_string(),
242    });
243    true
244}
245
246/// Materialize an [`EdgeLink`] into the doc: push external-ref points (`fixed`,
247/// `construction`, `externalReference`) with a `⏚` ground each, then the construction
248/// geometry referencing them. Returns `(point_ids, geom_ids)`.
249pub fn add_external_ref(doc: &mut SketchDoc, link: &EdgeLink) -> (Vec<Value>, Vec<Value>) {
250    let mut point_ids = Vec::new();
251    for (u, v) in link.point_uvs() {
252        let id = doc.next_point_id();
253        doc.points.push(SketchPoint {
254            id: id.clone(),
255            x: u,
256            y: v,
257            fixed: true,
258            construction: true,
259            external_reference: true,
260        });
261        push_ground(doc, &id);
262        point_ids.push(id);
263    }
264    let geom_ids = match link {
265        EdgeLink::Line { .. } => vec![push_construction_geometry(
266            doc,
267            "line",
268            vec![point_ids[0].clone(), point_ids[1].clone()],
269        )],
270        EdgeLink::Circle { .. } => vec![push_construction_geometry(
271            doc,
272            "circle",
273            vec![point_ids[0].clone(), point_ids[1].clone()],
274        )],
275        EdgeLink::Arc { .. } => vec![push_construction_geometry(
276            doc,
277            "arc",
278            vec![
279                point_ids[0].clone(),
280                point_ids[1].clone(),
281                point_ids[2].clone(),
282            ],
283        )],
284        EdgeLink::Polyline { .. } => point_ids
285            .windows(2)
286            .map(|w| push_construction_geometry(doc, "line", vec![w[0].clone(), w[1].clone()]))
287            .collect(),
288    };
289    (point_ids, geom_ids)
290}
291
292/// Materialize an [`EdgeLink`] as PLAIN sketch geometry, a copy that nothing
293/// keeps up to date: free points (not fixed, construction or external), no
294/// grounds, and each geometry carrying `note`. A copied end that lands on a
295/// plain point already in the sketch (within `1e-6·extent`) reuses it, so the
296/// edges of one loop, copied one by one, join into a closed profile. Returns
297/// the geometry ids.
298pub fn add_copied_edge(doc: &mut SketchDoc, link: &EdgeLink, note: &str) -> Vec<Value> {
299    let uvs = link.point_uvs();
300    let near = 1e-6 * polyline_extent(&uvs).max(1.0);
301    let mut point_ids = Vec::new();
302    for (u, v) in uvs {
303        let reuse = doc
304            .points
305            .iter()
306            .find(|p| !p.construction && !p.external_reference && dist((p.x, p.y), (u, v)) <= near)
307            .map(|p| p.id.clone());
308        let id = reuse.unwrap_or_else(|| {
309            let id = doc.next_point_id();
310            doc.points.push(SketchPoint { id: id.clone(), x: u, y: v, fixed: false, construction: false, external_reference: false });
311            id
312        });
313        point_ids.push(id);
314    }
315    let push = |doc: &mut SketchDoc, geom_type: &str, points: Vec<Value>| {
316        let id = doc.next_geometry_id();
317        let mut extra = Map::new();
318        extra.insert("note".to_string(), Value::String(note.to_string()));
319        doc.geometries.push(SketchGeometry { id: id.clone(), geom_type: geom_type.to_string(), points, extra });
320        id
321    };
322    match link {
323        EdgeLink::Line { .. } => vec![push(doc, "line", point_ids[..2].to_vec())],
324        EdgeLink::Circle { .. } => vec![push(doc, "circle", point_ids[..2].to_vec())],
325        EdgeLink::Arc { .. } => vec![push(doc, "arc", point_ids[..3].to_vec())],
326        EdgeLink::Polyline { .. } => point_ids
327            .windows(2)
328            .filter(|w| id_key(&w[0]) != id_key(&w[1]))
329            .map(|w| push(doc, "line", w.to_vec()))
330            .collect(),
331    }
332}
333
334/// Remove every entity an [`ExternalRef`] materialized: its geometries, its points,
335/// and any constraint referencing one of its points (the `⏚` grounds).
336fn remove_ref_entities(doc: &mut SketchDoc, r: &ExternalRef) {
337    let pt_keys: HashSet<String> = r.point_ids.iter().map(id_key).collect();
338    let geo_keys: HashSet<String> = r.geom_ids.iter().map(id_key).collect();
339    doc.geometries.retain(|g| !geo_keys.contains(&id_key(&g.id)));
340    doc.points.retain(|p| !pt_keys.contains(&id_key(&p.id)));
341    doc.constraints
342        .retain(|c| !c.points().iter().any(|p| pt_keys.contains(&id_key(p))));
343}
344
345/// Drop every external-reference entry whose materialized entities no longer resolve
346/// in `doc` — i.e. the linked edge's points / geometry were deleted. A dropped entry
347/// frees its `edge_name` so the SAME edge can be RE-LINKED: without this, the stale
348/// entry (keyed by `edge_name`, holding now-dangling `point_ids`) makes
349/// [`link_or_update`]'s dedup take the "structure matches" branch, find every point
350/// missing (`doc.point_mut` → `None`), and return `false` — permanently blocking the
351/// re-link. Called both after a delete AND on sketch-enter (to heal docs already
352/// poisoned by that pre-fix delete). Returns whether any entry was pruned.
353pub fn prune_dead_refs(doc: &SketchDoc, refs: &mut Vec<ExternalRef>) -> bool {
354    let before = refs.len();
355    refs.retain(|r| {
356        r.point_ids.iter().all(|id| doc.point(id).is_some())
357            && r.geom_ids.iter().all(|id| doc.geometry(id).is_some())
358    });
359    refs.len() != before
360}
361
362/// The `persistentData` key a unified commit stamps: once it is set, the
363/// `projectedEdges` list and the `externalRefs` entries describe the SAME set
364/// of edges, so an entry whose edge the dialog no longer lists was removed by
365/// the user. A document without it predates the unification: its entries were
366/// never listed, so they are adopted into the list, never released.
367pub const LINKS_LISTED_KEY: &str = "linksInProjectedEdges";
368
369/// Release a linked edge whose source edge is gone: its materialized points stay
370/// where they are, `fixed` and grounded (`⏚`), but stop being external
371/// references, so the line / arc / circle becomes an ordinary sketch entity the
372/// user owns. It stays CONSTRUCTION: flipping that on a feature deletion would
373/// add profile edges and change what a downstream extrude builds. Returns
374/// whether any point changed (`false` when the entry's points are all gone).
375pub fn release_ref(doc: &mut SketchDoc, r: &ExternalRef) -> bool {
376    let mut changed = false;
377    for id in &r.point_ids {
378        if let Some(p) = doc.point_mut(id) {
379            if p.external_reference || !p.fixed {
380                changed = true;
381            }
382            p.external_reference = false;
383            p.fixed = true;
384        }
385    }
386    changed
387}
388
389/// [`release_ref`] on the persisted JSON doc (`persistentData.sketch`), for the
390/// regenerate-time fold that cannot go through the editor: every point the entry
391/// materialized loses `externalReference` and keeps (or regains) `fixed`; nothing
392/// else in the doc is touched, so the fold is lossless. Returns whether any
393/// point changed.
394pub fn release_ref_json(sketch: &mut Value, r: &ExternalRef) -> bool {
395    let keys: HashSet<String> = r.point_ids.iter().map(id_key).collect();
396    let Some(points) = sketch.get_mut("points").and_then(Value::as_array_mut) else {
397        return false;
398    };
399    let mut changed = false;
400    for point in points.iter_mut() {
401        if !point.get("id").is_some_and(|id| keys.contains(&id_key(id))) {
402            continue;
403        }
404        if point.get("externalReference").and_then(Value::as_bool) != Some(false) {
405            changed = true;
406        }
407        if point.get("fixed").and_then(Value::as_bool) != Some(true) {
408            changed = true;
409        }
410        point["externalReference"] = Value::Bool(false);
411        point["fixed"] = Value::Bool(true);
412    }
413    changed
414}
415
416/// The `projectedEdges` list a Finish writes: every name the feature listed
417/// before, in its order, minus the ones whose link the session dropped (deleted
418/// in the editor, or released), plus every live link the list did not name yet.
419/// `kept_unlinked` are listed names that had nothing to materialize at enter
420/// (the edge did not resolve in the backdrop) — they stay listed for the kernel
421/// to report.
422pub fn listed_edge_names(
423    listed: &[String],
424    refs: &[ExternalRef],
425    kept_unlinked: &[String],
426) -> Vec<String> {
427    let mut out: Vec<String> = listed
428        .iter()
429        .filter(|name| {
430            refs.iter().any(|r| &r.edge_name == *name) || kept_unlinked.contains(name)
431        })
432        .cloned()
433        .collect();
434    for r in refs {
435        if !out.contains(&r.edge_name) {
436            out.push(r.edge_name.clone());
437        }
438    }
439    out
440}
441
442/// Ensure the sketch carries the PART ORIGIN reference point: world `(0, 0, 0)`
443/// projected onto `plane`, under the reserved id
444/// [`brep_kernel::SKETCH_ORIGIN_POINT_ID`], flagged fixed + construction +
445/// external reference, with one `⏚` ground (reserved id too). Adds it when
446/// missing and re-projects it when the plane moved. It is something to constrain
447/// to inside the sketch only — the kernel never publishes it. Returns whether the
448/// doc changed.
449pub fn ensure_origin_point(doc: &mut SketchDoc, plane: &PlaneFrame) -> bool {
450    let id = Value::String(brep_kernel::SKETCH_ORIGIN_POINT_ID.to_string());
451    let (u, v) = plane.to_uv([0.0, 0.0, 0.0]);
452    let mut changed = false;
453    match doc.point_mut(&id) {
454        Some(p) => {
455            if (p.x - u).abs() > 1e-12 || (p.y - v).abs() > 1e-12 {
456                changed = true;
457            }
458            if !(p.fixed && p.construction && p.external_reference) {
459                changed = true;
460            }
461            p.x = u;
462            p.y = v;
463            p.fixed = true;
464            p.construction = true;
465            p.external_reference = true;
466        }
467        None => {
468            doc.points.push(SketchPoint {
469                id: id.clone(),
470                x: u,
471                y: v,
472                fixed: true,
473                construction: true,
474                external_reference: true,
475            });
476            changed = true;
477        }
478    }
479    let grounded = doc.constraints.iter().any(|c| {
480        c.ctype() == Some("⏚") && c.points().first().map(id_key) == Some(id_key(&id))
481    });
482    if !grounded {
483        let mut raw = Map::new();
484        raw.insert("id".to_string(), id.clone());
485        raw.insert("type".to_string(), Value::String("⏚".to_string()));
486        raw.insert("points".to_string(), Value::Array(vec![id]));
487        doc.constraints.push(SketchConstraint { raw });
488        changed = true;
489    }
490    changed
491}
492
493/// Whether `id` names the reserved part-origin point (or its ground).
494pub fn is_origin_id(id: &Value) -> bool {
495    brep_kernel::is_sketch_origin_point_id(id)
496}
497
498/// Push a `⏚` GROUND constraint pinning point `pid` (mirrors the S4 toggle-ground
499/// factory).
500fn push_ground(doc: &mut SketchDoc, pid: &Value) {
501    let cid = doc.next_constraint_id();
502    let mut raw = Map::new();
503    raw.insert("id".to_string(), cid);
504    raw.insert("type".to_string(), Value::String("⏚".to_string()));
505    raw.insert("points".to_string(), Value::Array(vec![pid.clone()]));
506    doc.constraints.push(SketchConstraint { raw });
507}
508
509/// Push a construction geometry (`construction: true` — dashed, non-modeling) with a
510/// freshly minted id, returning that id.
511fn push_construction_geometry(doc: &mut SketchDoc, geom_type: &str, points: Vec<Value>) -> Value {
512    let id = doc.next_geometry_id();
513    let mut extra = Map::new();
514    extra.insert("construction".to_string(), Value::Bool(true));
515    doc.geometries.push(SketchGeometry {
516        id: id.clone(),
517        geom_type: geom_type.to_string(),
518        points,
519        extra,
520    });
521    id
522}
523
524// --- geometry helpers ---------------------------------------------------------
525
526/// The bounding-box diagonal of a `(u, v)` polyline — the relative-tolerance scale.
527fn polyline_extent(uv: &[(f64, f64)]) -> f64 {
528    let (mut minx, mut miny) = (f64::INFINITY, f64::INFINITY);
529    let (mut maxx, mut maxy) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
530    for &(x, y) in uv {
531        minx = minx.min(x);
532        miny = miny.min(y);
533        maxx = maxx.max(x);
534        maxy = maxy.max(y);
535    }
536    ((maxx - minx).powi(2) + (maxy - miny).powi(2)).sqrt()
537}
538
539/// Whether `uv` travels counter-clockwise about `center`: the sum of the WRAPPED
540/// signed angle steps between consecutive samples (each in `(−π, π]`) is positive.
541/// Summing the per-step turns, rather than comparing one midpoint's side of the
542/// chord, stays correct for sweeps beyond 180°.
543fn winding_ccw(uv: &[(f64, f64)], center: (f64, f64)) -> bool {
544    use std::f64::consts::{PI, TAU};
545    let angle = |p: (f64, f64)| (p.1 - center.1).atan2(p.0 - center.0);
546    let total: f64 = uv
547        .windows(2)
548        .map(|w| {
549            let step = angle(w[1]) - angle(w[0]);
550            // Wrap into (−π, π] so a sample straddling the ±π cut does not read
551            // as an almost-full turn the other way.
552            step - TAU * ((step + PI) / TAU).floor()
553        })
554        .sum();
555    total >= 0.0
556}
557
558/// Fit a circle through three points (circumcenter + radius), or `None` when the
559/// points are (near) collinear.
560fn fit_circle(p1: (f64, f64), p2: (f64, f64), p3: (f64, f64)) -> Option<(f64, f64, f64)> {
561    let (ax, ay) = p1;
562    let (bx, by) = p2;
563    let (cx, cy) = p3;
564    // 2·(signed area of the triangle) — zero when collinear.
565    let d = 2.0 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
566    if d.abs() < 1e-12 {
567        return None;
568    }
569    let a2 = ax * ax + ay * ay;
570    let b2 = bx * bx + by * by;
571    let c2 = cx * cx + cy * cy;
572    let ux = (a2 * (by - cy) + b2 * (cy - ay) + c2 * (ay - by)) / d;
573    let uy = (a2 * (cx - bx) + b2 * (ax - cx) + c2 * (bx - ax)) / d;
574    let r = dist((ux, uy), p1);
575    if !ux.is_finite() || !uy.is_finite() || !r.is_finite() {
576        return None;
577    }
578    Some((ux, uy, r))
579}
580