BREP_render 0.4.0

BREP Rust rendering engine: kernel-fed scene store + wgpu renderer (headless artifact, desktop window, and wasm canvas shells).
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
//! Assembly-constraint VIEWPORT overlays (build-spec §8.4) — the CONSTRAINT twin
//! of [`crate::feature_dimensions`].
//!
//! Pure builders: the kernel's `assembly_overlay_json` rows (per-constraint world
//! anchors / directions / status / measured value) + the `assembly_state_json`
//! constraint list (for `inputParams` — expression detection + element refs) fold
//! into [`ConstraintOverlay`] records, which bake into the SAME leader/arrow/arc
//! triangle buffers the feature-dimension gizmo draws (via
//! [`crate::feature_dimensions::leaders_buffers`] — nothing re-rolled, per the
//! UI-consistency directive):
//!
//! * **distance** → a LINEAR dimension annotation — the silver rod + orange
//!   cone arrow + orange origin sphere, GRABBABLE (drag edits
//!   `inputParams.distance`, commit auto-solves). Plane-based pairings draw the
//!   TRUE dimension perpendicular to the BASE face (the perpendicular-foot
//!   construction on [`build_distance_annotation`], matching the kernel's
//!   signed `d = (P_other − P_base)·n̂_base` convention); plane-less pairings
//!   keep the plain anchor-to-anchor leader.
//! * **angle** → an ANGULAR annotation (the screen-constant arc + orange sweep-end
//!   handle + red dashed zero reference + green axis), GRABBABLE (drag edits
//!   `inputParams.angle`).
//! * **center** → two plain leaders drawn BY ROLE from the row's `groups`
//!   (the kernel mapper's inferred `[width pair, tab]` element-index groups):
//!   the width span between its two faces, and a leader from that span's
//!   midpoint (which lies on the mid-plane) to the tab's anchor centroid; the
//!   label sits at the width midpoint. Pick order never shapes the drawing.
//! * everything else (coincident / parallel / perpendicular / concentric /
//!   tangent / touch_align / fixed) → a plain anchor-to-anchor leader line +
//!   label anchor only — never a handle.
//!
//! Dragging is DISABLED for a distance/angle whose param is a non-numeric
//! EXPRESSION string (matches how feature dimensions treat expression-driven
//! params); the engine consults [`ConstraintOverlay::draggable`].
//!
//! The interactive state machine (hit regions, drag preview, the
//! `assembly_update_constraint_json` commit) lives in
//! `engine_state/assembly_overlay.rs`; this module stays camera-free and pure so
//! the geometry is unit-testable from canned JSON payloads.

use serde_json::Value;

use crate::feature_dimensions::{
    append_plain_leader, leaders_buffers, FeatureDimAnnotation,
};

/// Which overlay family a constraint renders as.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ConstraintOverlayKind {
    /// A grabbable linear dimension arrow pair (`distance`).
    Distance,
    /// A grabbable angle arc (`angle`).
    Angle,
    /// A non-interactive leader + label (every other type).
    Leader,
}

/// One constraint's overlay record: identity + status for the label, the
/// resolved world geometry, and (for the dimensional kinds) the annotation the
/// shared leader renderer draws + the drag machinery grabs.
#[derive(Clone, Debug)]
pub struct ConstraintOverlay {
    /// The constraint id (`DIST3`, `ANGL2`, …) — the mutation-ABI key.
    pub id: String,
    /// The constraint `type` string (`distance`, `coincident`, …).
    pub constraint_type: String,
    /// The type's icon glyph (`brep_kernel::ConstraintTypeDef::icon`) — what
    /// the viewport chip shows in place of the id. Empty for an unknown type,
    /// in which case the chip falls back to the id.
    pub icon: String,
    /// The solve status (`satisfied` / `adjusted` / `error` / …) — drives the
    /// label color via [`status_color`].
    pub status: String,
    /// The human status/solve message (label tooltip).
    pub message: String,
    /// The overlay family.
    pub kind: ConstraintOverlayKind,
    /// The resolved WORLD anchor points (one per selection; empty when the
    /// kernel could not resolve the selections — label-less, geometry-less row).
    pub anchors: Vec<[f64; 3]>,
    /// The element ROLE groups a multi-element type inferred (`groups` on the
    /// overlay row): index groups into `anchors`, role order — center's
    /// `[width pair, tab]`. Empty for the pairing types, whose two anchors ARE
    /// the drawing.
    pub groups: Vec<Vec<usize>>,
    /// Distance/Angle: the dimension annotation (reusing the feature-dim shape so
    /// `leaders_buffers` renders it verbatim). `field_key` is the `inputParams`
    /// key the drag edits (`distance` / `angle`). `None` for `Leader` rows and
    /// for dimensional rows whose anchors did not resolve.
    pub annotation: Option<FeatureDimAnnotation>,
    /// The measured value the kernel evaluated (`value` in the overlay row), for
    /// the label suffix. `None` for non-dimensional rows.
    pub value: Option<f64>,
    /// The value's unit (`"mm"` / `"deg"`), empty when `value` is `None`.
    pub unit: String,
    /// Whether the dimensional handle may be DRAGGED: true only for
    /// Distance/Angle whose current param is numeric (or absent — a first-solve
    /// initialized target). A non-numeric expression string disables the drag.
    pub draggable: bool,
    /// The constraint's `inputParams` (from the state list) — the drag commit
    /// mutates a clone of this (so `elements` / flags ride along unchanged).
    pub input_params: Value,
    /// The referenced element names (`inputParams.elements`) — label hover
    /// highlights these through the existing emphasis machinery.
    pub elements: Vec<String>,
}

impl ConstraintOverlay {
    /// The `inputParams` key a drag on this overlay edits (`distance` / `angle`).
    pub fn field_key(&self) -> Option<&'static str> {
        match self.kind {
            ConstraintOverlayKind::Distance => Some("distance"),
            ConstraintOverlayKind::Angle => Some("angle"),
            ConstraintOverlayKind::Leader => None,
        }
    }

    /// The world-space label anchor: a dimensional annotation's chip anchor (the
    /// leader midpoint, or the angle arc's mid-sweep at the screen-constant
    /// radius — camera-dependent via `world_per_pixel`); a role-grouped row's
    /// first-group centroid (center: the width span's midpoint, on the
    /// mid-plane); a leader row's anchor midpoint (or its single anchor).
    /// `None` when nothing resolved.
    pub fn label_anchor(&self, world_per_pixel: f64) -> Option<[f64; 3]> {
        if let Some(annotation) = &self.annotation {
            return Some(match self.kind {
                ConstraintOverlayKind::Angle => {
                    crate::feature_dimensions::angular_chip_anchor(annotation, world_per_pixel)
                }
                _ => annotation.midpoint(),
            });
        }
        if let Some((span, _)) = self.role_leaders() {
            return Some(co_mid(span.0, span.1));
        }
        match self.anchors.len() {
            0 => None,
            1 => Some(self.anchors[0]),
            _ => {
                let a = self.anchors[0];
                let b = self.anchors[1];
                Some([(a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5, (a[2] + b[2]) * 0.5])
            }
        }
    }

    /// The role-drawn leaders of a grouped row (center): the WIDTH span
    /// between the first group's two anchors, and the leader from that span's
    /// midpoint to the second group's anchor centroid. `None` unless the row
    /// carries two groups whose indexes all resolve to anchors (a pairing row,
    /// or a stale `groups` against a shorter anchor list, draws the plain
    /// leader instead).
    fn role_leaders(&self) -> Option<(([f64; 3], [f64; 3]), ([f64; 3], [f64; 3]))> {
        let [width, tab] = self.groups.as_slice() else {
            return None;
        };
        let [w0, w1] = width.as_slice() else {
            return None;
        };
        let (a, b) = (*self.anchors.get(*w0)?, *self.anchors.get(*w1)?);
        if tab.is_empty() {
            return None;
        }
        let mut centroid = [0.0f64; 3];
        for &index in tab {
            let p = self.anchors.get(index)?;
            for k in 0..3 {
                centroid[k] += p[k] / tab.len() as f64;
            }
        }
        Some(((a, b), (co_mid(a, b), centroid)))
    }

    /// The label chip text: the type's icon, then `{value}{unit}` for
    /// dimensional rows (`⟺ 5.25 mm`, `∠ 90°`); the bare icon otherwise. The
    /// id no longer rides in the chip — the app's chip hover names it — so a
    /// crowded assembly reads by picture, not by `DIST3`/`COIN9` codes. A row
    /// whose type has no icon keeps the id in the icon's place.
    pub fn label_text(&self) -> String {
        let lead = if self.icon.is_empty() { self.id.as_str() } else { self.icon.as_str() };
        match self.value {
            Some(value) => {
                let n = crate::formatting::compact_decimal(value, 2);
                if self.unit == "deg" {
                    format!("{lead} {n}\u{00b0}")
                } else if self.unit.is_empty() {
                    format!("{lead} {n}")
                } else {
                    format!("{lead} {n} {}", self.unit)
                }
            }
            None => lead.to_string(),
        }
    }
}

// ---------------------------------------------------------------------------
// Status → color
// ---------------------------------------------------------------------------

/// The requirements-doc §5 status → color vocabulary as display-sRGB `[r,g,b]`
/// (0..1, hex/255 like the leader palette — the overlay shader writes ~directly).
///
/// Overlay-shader color (0..1 floats) for a constraint status — a thin view
/// over the ONE canonical map in [`crate::assembly_status`] (the panel's row
/// labels and the tree rollup consume the same table; unified at Wave-3
/// integration so the vocabulary can never drift).
pub fn status_color(status: &str) -> [f32; 3] {
    let [r, g, b] = crate::assembly_status::status_color_rgb(status);
    [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0]
}

// ---------------------------------------------------------------------------
// Builder
// ---------------------------------------------------------------------------

/// Build the overlay records from the kernel payloads: `overlay_rows` is the
/// parsed `assembly_overlay_json` array; `state_constraints` is the parsed
/// `assembly_state_json`'s `constraints` array (may be `Null` — the builder then
/// has no `inputParams`, so dimensional rows fall back to draggable-with-empty
/// params). Rows without resolved anchors yield status-only records (no
/// geometry, no label anchor); a `fixed` constraint's single anchor yields a
/// label anchor but no leader.
pub fn build_constraint_overlays(
    overlay_rows: &Value,
    state_constraints: &Value,
) -> Vec<ConstraintOverlay> {
    let Some(rows) = overlay_rows.as_array() else {
        return Vec::new();
    };
    rows.iter()
        .filter_map(|row| build_row(row, state_constraints))
        .collect()
}

fn build_row(row: &Value, state_constraints: &Value) -> Option<ConstraintOverlay> {
    let id = row.get("id")?.as_str()?.to_string();
    let constraint_type = row
        .get("type")
        .and_then(Value::as_str)
        .unwrap_or("")
        .to_string();
    let icon = brep_kernel::constraint_type(&constraint_type)
        .map(|def| def.icon.to_string())
        .unwrap_or_default();
    let status = row
        .get("status")
        .and_then(Value::as_str)
        .unwrap_or("")
        .to_string();
    let message = row
        .get("message")
        .and_then(Value::as_str)
        .unwrap_or("")
        .to_string();
    let anchors = read_points(row.get("anchors"));
    let directions = read_dirs(row.get("directions"));
    let geoms = read_strings(row.get("geoms"));
    let groups = read_groups(row.get("groups"));
    let value = row.get("value").and_then(Value::as_f64);
    let unit = row
        .get("unit")
        .and_then(Value::as_str)
        .unwrap_or("")
        .to_string();

    // The matching state entry's inputParams (expression check + elements).
    let input_params = state_constraints
        .as_array()
        .and_then(|list| {
            list.iter().find(|entry| {
                entry
                    .get("inputParams")
                    .and_then(|p| p.get("id"))
                    .and_then(Value::as_str)
                    == Some(id.as_str())
            })
        })
        .and_then(|entry| entry.get("inputParams"))
        .cloned()
        .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
    let elements = crate::json_support::string_values(input_params.get("elements"))
        .map(str::to_string)
        .collect();

    let kind = match constraint_type.as_str() {
        "distance" => ConstraintOverlayKind::Distance,
        "angle" => ConstraintOverlayKind::Angle,
        _ => ConstraintOverlayKind::Leader,
    };

    // Dragging: only the dimensional kinds, and only while the param is NOT a
    // non-numeric expression string (absent / number / plain numeric string are
    // all draggable — matching the feature-dimension expression rule).
    let draggable = match kind {
        ConstraintOverlayKind::Leader => false,
        ConstraintOverlayKind::Distance => param_allows_drag(&input_params, "distance"),
        ConstraintOverlayKind::Angle => param_allows_drag(&input_params, "angle"),
    };

    let annotation = match kind {
        ConstraintOverlayKind::Distance => {
            build_distance_annotation(&anchors, &directions, &geoms, value)
        }
        ConstraintOverlayKind::Angle => build_angle_annotation(&anchors, &directions, value),
        ConstraintOverlayKind::Leader => None,
    };

    Some(ConstraintOverlay {
        id,
        constraint_type,
        icon,
        status,
        message,
        kind,
        anchors,
        groups,
        annotation,
        value: match kind {
            ConstraintOverlayKind::Leader => None,
            _ => value,
        },
        unit,
        draggable,
        input_params,
        elements,
    })
}

/// Whether `params[key]` permits a value drag: absent (first-solve initialized),
/// a JSON number, or a PLAIN numeric string — but NOT a non-numeric expression
/// string (`"a + b"`), which stays authoritative and disables the handle.
fn param_allows_drag(params: &Value, key: &str) -> bool {
    match params.get(key) {
        None | Some(Value::Null) => true,
        Some(Value::Number(_)) => true,
        Some(Value::String(text)) => text.trim().parse::<f64>().is_ok(),
        _ => false,
    }
}

/// The distance constraint's LINEAR annotation.
///
/// PLANE-BASED pairings (at least one element is tagged `"plane"` in the row's
/// `geoms` — the BASE face, element 0 preferred, matching the kernel mapper's
/// base choice) draw the TRUE dimension: the other element's anchor `P` is
/// projected onto the base plane along its outward normal `n̂` — the
/// perpendicular foot `F = P − s·n̂` with `s = (P − Q)·n̂` the SIGNED offset —
/// and the arrow runs `F → P`. That segment is normal to the base face by
/// construction and its length `|s|` IS the constrained distance, so the arrow
/// shows the kernel's signed `d = (P_other − P_base)·n̂_base` convention
/// verbatim (a negative `s` points behind the face). The base normal rides in
/// the annotation's (linear-unused) `axis` field so the drag can measure
/// signed offsets along it — including through the face into negatives, and
/// even when `s = 0` collapses the segment to a point.
///
/// Plane-less pairings (lines/points — no side to be on) keep the plain
/// `anchors[0] → anchors[1]` leader with the unsigned measured value and a
/// zero `axis`. `None` unless both anchors resolved.
fn build_distance_annotation(
    anchors: &[[f64; 3]],
    directions: &[Option<[f64; 3]>],
    geoms: &[String],
    value: Option<f64>,
) -> Option<FeatureDimAnnotation> {
    if anchors.len() < 2 {
        return None;
    }
    // The base plane: the first element tagged "plane" with a usable normal.
    let base = (0..2).find(|&i| {
        geoms.get(i).map(String::as_str) == Some("plane")
            && directions
                .get(i)
                .copied()
                .flatten()
                .is_some_and(|n| co_norm(n) > 1e-9)
    });
    if let Some(base) = base {
        let n = directions[base].expect("base index checked above");
        let len = co_norm(n);
        let n = [n[0] / len, n[1] / len, n[2] / len];
        let q = anchors[base];
        let p = anchors[1 - base];
        let s = co_dot(co_sub(p, q), n);
        let foot = [p[0] - n[0] * s, p[1] - n[1] * s, p[2] - n[2] * s];
        // The kernel's measured `value` equals `s` for both plane arms (same
        // formula over the same anchors); prefer it for label consistency.
        let mut annotation =
            FeatureDimAnnotation::linear("distance", foot, p, value.unwrap_or(s), "D");
        annotation.axis = n;
        return Some(annotation);
    }
    let a = anchors[0];
    let b = anchors[1];
    let value = value.unwrap_or_else(|| co_norm(co_sub(b, a)));
    Some(FeatureDimAnnotation::linear("distance", a, b, value, "D"))
}

/// The angle constraint's ANGULAR annotation. Geometry: the arc sweeps from
/// direction `d0` toward `d1` about `axis = normalize(d0 × d1)` — the axis that
/// makes rotating `d0` by the measured interior angle land exactly on `d1` — and
/// is centered at the closest-approach midpoint of the two carrier lines
/// (`anchors[i] + t·dᵢ`), the natural angle vertex; parallel/degenerate carriers
/// fall back to the anchor midpoint, and a parallel/antiparallel pair (the 0°/
/// 180° satisfied states — `d0 × d1 ≈ 0`) falls back to an arbitrary
/// perpendicular axis via the `angular` ctor, which still renders and keeps the
/// drag well-defined once the value moves off zero. `None` unless both anchors
/// AND both directions resolved.
fn build_angle_annotation(
    anchors: &[[f64; 3]],
    directions: &[Option<[f64; 3]>],
    value: Option<f64>,
) -> Option<FeatureDimAnnotation> {
    if anchors.len() < 2 || directions.len() < 2 {
        return None;
    }
    let d0 = directions[0]?;
    let d1 = directions[1]?;
    let axis = co_cross(d0, d1);
    let center = carrier_closest_midpoint(anchors[0], d0, anchors[1], d1);
    let value = value.unwrap_or(0.0).clamp(-360.0, 360.0);
    Some(FeatureDimAnnotation::angular(
        "angle", center, axis, d0, value, "A",
    ))
}

/// The midpoint of the closest-approach segment between carrier lines
/// `a + t·da` and `b + s·db`; the anchor midpoint when (near) parallel.
fn carrier_closest_midpoint(a: [f64; 3], da: [f64; 3], b: [f64; 3], db: [f64; 3]) -> [f64; 3] {
    let mid = |p: [f64; 3], q: [f64; 3]| {
        [(p[0] + q[0]) * 0.5, (p[1] + q[1]) * 0.5, (p[2] + q[2]) * 0.5]
    };
    let da_n = co_norm(da);
    let db_n = co_norm(db);
    if da_n < 1e-9 || db_n < 1e-9 {
        return mid(a, b);
    }
    let u = [da[0] / da_n, da[1] / da_n, da[2] / da_n];
    let v = [db[0] / db_n, db[1] / db_n, db[2] / db_n];
    let w0 = co_sub(a, b);
    let b_uv = co_dot(u, v);
    let denom = 1.0 - b_uv * b_uv;
    if denom.abs() < 1e-9 {
        return mid(a, b); // parallel carriers — no unique vertex
    }
    let d = co_dot(u, w0);
    let e = co_dot(v, w0);
    let t = (b_uv * e - d) / denom;
    let s = (e - b_uv * d) / denom;
    let p = [a[0] + u[0] * t, a[1] + u[1] * t, a[2] + u[2] * t];
    let q = [b[0] + v[0] * s, b[1] + v[1] * s, b[2] + v[2] * s];
    mid(p, q)
}

// ---------------------------------------------------------------------------
// Buffers
// ---------------------------------------------------------------------------

/// Bake the whole overlay set into flat world-space triangle `(positions,
/// colors)` buffers (the `tris` shape the overlay group consumes): the
/// dimensional annotations through [`leaders_buffers`] (identical arrow/arc
/// styling), the non-dimensional rows as plain silver leaders
/// ([`append_plain_leader`]). Rows without geometry contribute nothing.
pub fn constraint_overlay_buffers(
    overlays: &[ConstraintOverlay],
    world_per_pixel: f64,
) -> (Vec<f32>, Vec<f32>) {
    let annotations: Vec<FeatureDimAnnotation> = overlays
        .iter()
        .filter_map(|overlay| overlay.annotation.clone())
        .collect();
    let (mut positions, mut colors) = leaders_buffers(&annotations, world_per_pixel);
    for overlay in overlays {
        if overlay.kind != ConstraintOverlayKind::Leader {
            continue;
        }
        if let Some((span, tab)) = overlay.role_leaders() {
            // Role-drawn (center): the width span, then mid-plane → tab.
            append_plain_leader(&mut positions, &mut colors, span.0, span.1, world_per_pixel);
            append_plain_leader(&mut positions, &mut colors, tab.0, tab.1, world_per_pixel);
        } else if overlay.anchors.len() >= 2 {
            append_plain_leader(
                &mut positions,
                &mut colors,
                overlay.anchors[0],
                overlay.anchors[1],
                world_per_pixel,
            );
        }
    }
    (positions, colors)
}

// ---------------------------------------------------------------------------
// JSON + vec helpers (self-contained; `co_` prefixed like the fd_ family)
// ---------------------------------------------------------------------------

fn read_points(value: Option<&Value>) -> Vec<[f64; 3]> {
    value
        .and_then(Value::as_array)
        .map(|list| list.iter().filter_map(read_point3).collect())
        .unwrap_or_default()
}

/// Directions align index-wise with anchors; a JSON `null` (a point-like
/// selection has no direction) stays `None`.
fn read_dirs(value: Option<&Value>) -> Vec<Option<[f64; 3]>> {
    value
        .and_then(Value::as_array)
        .map(|list| list.iter().map(read_point3).collect())
        .unwrap_or_default()
}

/// The row's per-element `geoms` tags (aligned index-wise with anchors);
/// empty when absent — a distance row then has no identifiable base plane and
/// falls back to the plain anchor-to-anchor leader.
fn read_strings(value: Option<&Value>) -> Vec<String> {
    value
        .and_then(Value::as_array)
        .map(|list| {
            list.iter()
                .map(|v| v.as_str().unwrap_or("").to_string())
                .collect()
        })
        .unwrap_or_default()
}

/// The row's role `groups` (index groups into `anchors`); empty when absent.
fn read_groups(value: Option<&Value>) -> Vec<Vec<usize>> {
    value
        .and_then(Value::as_array)
        .map(|groups| {
            groups
                .iter()
                .map(|group| {
                    group
                        .as_array()
                        .map(|list| {
                            list.iter()
                                .filter_map(Value::as_u64)
                                .map(|index| index as usize)
                                .collect()
                        })
                        .unwrap_or_default()
                })
                .collect()
        })
        .unwrap_or_default()
}

fn read_point3(value: &Value) -> Option<[f64; 3]> {
    let list = value.as_array()?;
    Some([
        list.first()?.as_f64()?,
        list.get(1)?.as_f64()?,
        list.get(2)?.as_f64()?,
    ])
}

fn co_mid(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
    [(a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5, (a[2] + b[2]) * 0.5]
}

fn co_sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}

fn co_dot(a: [f64; 3], b: [f64; 3]) -> f64 {
    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}

fn co_norm(v: [f64; 3]) -> f64 {
    co_dot(v, v).sqrt()
}

fn co_cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
    [
        a[1] * b[2] - a[2] * b[1],
        a[2] * b[0] - a[0] * b[2],
        a[0] * b[1] - a[1] * b[0],
    ]
}

// BREP private tests: 03f60e4537c81872