///|
pub type Vec2 = (Double, Double)

///|
pub type Vec3 = (Double, Double, Double)

///|
#derive(Debug)
pub struct Rgb {
  r : Double
  g : Double
  b : Double
}

///|
pub fn Rgb::new(r : Double, g : Double, b : Double) -> Rgb {
  { r, g, b, }
}

///|
pub fn Rgb::orange() -> Rgb {
  { r: 1.0, g: 0.6, b: 0.0, }
}

///|
pub fn Rgb::blue() -> Rgb {
  { r: 0.2, g: 0.6, b: 1.0, }
}

///|
pub fn Rgb::grey() -> Rgb {
  { r: 0.2, g: 0.2, b: 0.2, }
}

///|
pub fn Rgb::purple() -> Rgb {
  { r: 0.5, g: 0.0, b: 0.5, }
}

///|
pub fn Rgb::magenta() -> Rgb {
  { r: 1.0, g: 0.0, b: 1.0, }
}

///|
pub fn Rgb::white() -> Rgb {
  { r: 1.0, g: 1.0, b: 1.0, }
}

///|
pub fn Rgb::black() -> Rgb {
  { r: 0.0, g: 0.0, b: 0.0, }
}

///|
pub fn Rgb::green() -> Rgb {
  { r: 0.0, g: 0.8, b: 0.0, }
}

///|
pub fn Rgb::red() -> Rgb {
  { r: 0.8, g: 0.0, b: 0.0, }
}

///|
pub fn Rgb::cyan() -> Rgb {
  { r: 0.0, g: 0.8, b: 0.8, }
}

///|
pub struct Profile2D {
  outer : Array[(Double, Double)]
  holes : Array[Array[(Double, Double)]]
}

///|
pub struct Cuboid {
  sx : Double
  sy : Double
  sz : Double
}

///|
/// Cylinder is a solid right circular cylinder whose base lies on the local
/// Z = 0 plane and whose axis follows +Z through the origin.
pub struct Cylinder {
  diameter : Double
  height : Double
}

///|
pub struct CalibrationChamferBlock {
  length_mm : Double
  width_mm : Double
  height_mm : Double
  chamfer_size_mm : Double
  fillet_radius_mm : Double
  fillet_segments : Int32
}

///|
pub struct ExtrudedProfile {
  profile : Profile2D
  height_mm : Double
  /// Optional analytic circular through-bore `(center, diameter)`. It compiles
  /// to a real `CYLINDRICAL_SURFACE` with `CIRCLE` bounds, so a gear bore is
  /// never a sampled polygon. Only one round bore is supported; polygonal holes
  /// in `profile.holes` remain available for other features.
  round_bore : (Vec2, Double)?
}

///|
/// ProfileSegment is one boundary segment of a planar profile: a straight line
/// or a counter-clockwise minor circular arc (sweep at most 180 degrees).
pub(all) enum ProfileSegment {
  SegmentLine(Vec2, Vec2)
  SegmentArc(Vec2, Vec2, Vec2)
}

///|
/// ExtrudedArcProfile is an extruded closed profile whose boundary contains
/// circular arcs, so a genuinely circular side becomes an analytic
/// `CYLINDRICAL_SURFACE` instead of a polygon of planar facets.
///
/// `segments` is one closed counter-clockwise outer loop. `round_bore` is an
/// optional analytic circular through-bore. This is the representation for a
/// keyed shaft (a circular body with a real keyway notch) and for other
/// line-plus-arc sections.
pub struct ExtrudedArcProfile {
  segments : Array[ProfileSegment]
  height_mm : Double
  round_bore : (Vec2, Double)?
  /// Closed counter-clockwise inner loops of lines and minor arcs, each a
  /// through-hole with analytic cylindrical faces on its arcs.
  holes : Array[Array[ProfileSegment]]
}

///|
pub struct SweptProfile {
  path : SweepPath
  profile : SweepProfile
  make_start_cap : Bool
  make_end_cap : Bool
  straight_holes : Bool
  straight_outline : Bool
  /// Emit a smooth helical B-rep for a rectangular profile on a helical path:
  /// four B-spline side surfaces and two planar caps (six faces). The mesh,
  /// section, and mass-property analysis keep using the sampled path.
  smooth_helix : Bool
  /// Optional start/end corner positions (inner-top, inner-bottom, outer-top,
  /// outer-bottom) that override the sampled corner endpoints. The corner
  /// curves are blended to meet them, which reproduces a reference's split-cap
  /// vertices while keeping smooth B-spline sides.
  corner_override : (Array[Vec3], Array[Vec3])?
}

///|
pub struct BooleanUnion {
  nodes : Array[SceneNode]
}

///|
pub struct BooleanDifference {
  base : SceneNode
  cutters : Array[SceneNode]
}

///|
/// BezierAnchor represents a single anchor point in a Bezier curve.
/// The handles are absolute positions (not relative offsets).
pub struct BezierAnchor {
  position : Vec2
  handle_in : Vec2 // Absolute position of incoming handle
  handle_out : Vec2 // Absolute position of outgoing handle
}

///|
/// BezierPath represents a 2D path made of cubic Bezier curves.
pub struct BezierPath {
  anchors : Array[BezierAnchor]
  closed : Bool
  clear : Bool // True for holes (cut-outs)
}

///|
/// ExtrudedBezierProfile represents an extruded shape from Bezier paths.
pub struct ExtrudedBezierProfile {
  paths : Array[BezierPath] // First non-clear path is outer, clear paths are holes
  height_mm : Double
}

///|
/// EdgeSelector chooses which edges of a base solid an edge fillet or chamfer
/// applies to.
///
/// The selector is intent, not geometry: the flat STEP writer cannot evaluate
/// it. Only the optional OCCT-backed evaluated export (`Design::to_step_evaluated`)
/// or the Blender backend can apply the operation. Selectors are evaluated
/// against the base solid's straight/curved edge bounds:
///
/// - `AllEdges` selects every edge.
/// - `EdgesByDirection` selects edges whose bounding-box diagonal is parallel
///   to `dir` within `tolerance_deg`.
/// - `EdgesNearPoint` selects edges whose bounding box is within `radius_mm`
///   of `point`.
/// - `EdgeIndices` selects 1-based edge indices in the solid's edge order.
pub(all) enum EdgeSelector {
  AllEdges
  EdgesByDirection(Vec3, Double)
  EdgesNearPoint(Vec3, Double)
  EdgeIndices(Array[Int32])
}

///|
/// EdgeFeature is the intent to fillet or chamfer the selected edges of a base
/// solid.
///
/// `size_mm` is the fillet radius or chamfer distance. `angle_deg` is an
/// optional chamfer angle; `None` means an equal-distance chamfer.
pub(all) struct EdgeFeature {
  base : SceneNode
  selector : EdgeSelector
  size_mm : Double
  angle_deg : Double?
}

///|
/// SocketedSolid is a solid of revolution with a blind vertical hexagonal
/// pocket cut into its flat top face.
///
/// `body` is a closed `RevolvedProfile` whose final segment is the radial flat
/// top cap from `(top_radius, top_z)` to `(0, top_z)`. The compiler suppresses
/// that cap and replaces it with a true single-shell pocket boundary: an
/// annular planar top face, six planar socket walls, an optional ruled 45-degree
/// lead-in, and a planar hexagonal floor. The result is one closed solid, not a
/// stack of layered bodies, and every surface stays analytic (no polygon
/// substitutes for the body's cylinders, cones, spheres, or tori).
///
/// The socket mouth lies on the body's flat top, so `top_radius` must exceed the
/// socket circumradius. `lead_in_mm` is a parallel 45-degree break on the six
/// socket flats; it is a ruled approximation of the conical rim and is recorded
/// as such by the caller.
pub struct SocketedSolid {
  body : RevolvedProfile
  across_flats_mm : Double
  socket_floor_z_mm : Double
  lead_in_mm : Double
  rotation_deg : Double
  /// When true the socket rim and floor are true cones (`rim_angle_deg`,
  /// `floor_angle_deg`) trimmed by the hexagon flats, and
  /// `socket_floor_z_mm` is the drill-point apex rather than a flat floor.
  conical : Bool
  rim_angle_deg : Double
  floor_angle_deg : Double
}

///|
/// SquareHoleWasher is a plain circular washer with a centered square through
/// hole. The outer surface is one analytic cylinder; the hole walls are four
/// planes and the two end faces are planes with a square hole.
pub struct SquareHoleWasher {
  outer_diameter_mm : Double
  square_side_mm : Double
  thickness_mm : Double
}

///|
/// SquareHoleWasher::new builds a square-hole washer intent.
pub fn SquareHoleWasher::new(
  outer_diameter_mm~ : Double,
  square_side_mm~ : Double,
  thickness_mm~ : Double,
) -> SquareHoleWasher {
  { outer_diameter_mm, square_side_mm, thickness_mm, }
}

///|
/// SquareHoleWasher::as_shape wraps the washer as a Shape.
pub fn SquareHoleWasher::as_shape(self : SquareHoleWasher) -> Shape {
  Shape::SquareHoleWasher(self)
}

///|
/// TabWasher is a plain circular washer with a rectangular external tab. The
/// tab blends into the washer with two concave analytic cylinders; the bore is
/// a cylinder and the tab sides/end and the two faces are planes.
pub struct TabWasher {
  outer_diameter_mm : Double
  bore_diameter_mm : Double
  tab_width_mm : Double
  tab_length_mm : Double
  fillet_radius_mm : Double
  thickness_mm : Double
}

///|
/// TabWasher::new builds a tab-washer intent.
pub fn TabWasher::new(
  outer_diameter_mm~ : Double,
  bore_diameter_mm~ : Double,
  tab_width_mm~ : Double,
  tab_length_mm~ : Double,
  fillet_radius_mm~ : Double,
  thickness_mm~ : Double,
) -> TabWasher {
  {
    outer_diameter_mm,
    bore_diameter_mm,
    tab_width_mm,
    tab_length_mm,
    fillet_radius_mm,
    thickness_mm,
  }
}

///|
/// TabWasher::as_shape wraps the tab washer as a Shape.
pub fn TabWasher::as_shape(self : TabWasher) -> Shape {
  Shape::TabWasher(self)
}

///|
/// HexHeadWasherFace is a hexagon head trimmed by a conical washer face: the
/// top is a circle of radius `across_flats / 2`, the chamfer is a 75-degree
/// cone, and the flat/cone intersections are hyperbolas.
pub struct HexHeadWasherFace {
  across_flats_mm : Double
  head_height_mm : Double
  /// Conical washer-face semi-angle (75 degrees for the simple hex heads, 65
  /// for the detailed ISO 4017 references).
  face_angle_deg : Double
  /// Optional shank below the head. `shank_diameter_mm <= 0` keeps the
  /// head-only solid. When positive the head, under-head washer face and shank
  /// are emitted as one closed shell.
  shank_diameter_mm : Double
  shank_length_mm : Double
  /// Optional under-head washer face `(diameter, height)`. Diameter 0 uses the
  /// full hexagon underside.
  washer_face_diameter_mm : Double
  washer_face_height_mm : Double
  tip_chamfer_mm : Double
}

///|
/// HexHeadWasherFace::new builds a conical-washer-face hex head.
pub fn HexHeadWasherFace::new(
  across_flats_mm~ : Double,
  head_height_mm~ : Double,
  face_angle_deg~ : Double = 75.0,
  shank_diameter_mm~ : Double = 0.0,
  shank_length_mm~ : Double = 0.0,
  washer_face_diameter_mm~ : Double = 0.0,
  washer_face_height_mm~ : Double = 0.0,
  tip_chamfer_mm~ : Double = 0.0,
) -> HexHeadWasherFace {
  {
    across_flats_mm,
    head_height_mm,
    face_angle_deg,
    shank_diameter_mm,
    shank_length_mm,
    washer_face_diameter_mm,
    washer_face_height_mm,
    tip_chamfer_mm,
  }
}

///|
/// HexHeadWasherFace::as_shape wraps the head as a Shape.
pub fn HexHeadWasherFace::as_shape(self : HexHeadWasherFace) -> Shape {
  Shape::HexHeadWasherFace(self)
}

///|

///|
/// ConicalNut is a hexagon nut with a 75/60-degree conical washer face on each
/// bearing face and a circular through bore. The face chamfer is a true
/// `CONICAL_SURFACE` trimmed by the six flats (hyperbola edges with pcurves),
/// not a parallel-inset ring stack.
pub struct ConicalNut {
  across_flats_mm : Double
  height_mm : Double
  bore_diameter_mm : Double
  face_angle_deg : Double
  /// 45-degree bore lead-in depth at each face; 0 for a straight bore.
  lead_in_mm : Double
  /// Corner clocking in degrees (vertices at rotation + 60 i; flats at
  /// rotation + 30 + 60 i).
  rotation_deg : Double
}

///|
/// SlitTube is a straight tube with a full-length parallel-sided slot cut
/// through one side (an ISO 8752 slotted spring pin). The outer and inner
/// surfaces are analytic cylinders, the slot walls are planes, and the two end
/// faces are C-shaped planes. The slot is centred on +X with parallel walls at
/// `y = +/- slot_width/2`.
pub struct SlitTube {
  outer_diameter_mm : Double
  inner_diameter_mm : Double
  length_mm : Double
  slot_width_mm : Double
}

///|
/// AcornNut is a DIN 1587 hexagon domed cap nut: the ISO 4032/DIN 934 hexagon
/// body with conical washer faces on both ends, a through bore, and a domed
/// cap. The cap is a spherical cap that starts at the hexagon top rim and
/// reaches `total_height_mm`; inside it, a concentric spherical cavity leaves
/// `wall_mm` of cap wall above the bore.
pub struct AcornNut {
  across_flats_mm : Double
  /// Height of the hexagon body (DIN 1587 `m`), excluding the dome.
  body_height_mm : Double
  bore_diameter_mm : Double
  /// Overall height including the dome (DIN 1587 `h`).
  total_height_mm : Double
  /// Diameter of the dome where it meets the hexagon top (DIN 1587 `dk`).
  dome_diameter_mm : Double
  /// Radial thickness of the domed cap above the bore.
  wall_mm : Double
  face_angle_deg : Double
  /// Corner clocking in degrees (vertices at rotation + 60 i; flats at
  /// rotation + 30 + 60 i).
  rotation_deg : Double
}

///|
/// SquareNut is a DIN 557 square nut: a square prism with a conical washer
/// face on the top bearing face and a circular through bore. The bottom face
/// is flat, as DIN 557 specifies a chamfer on one face only.
pub struct SquareNut {
  across_flats_mm : Double
  height_mm : Double
  bore_diameter_mm : Double
  face_angle_deg : Double
  /// Corner clocking in degrees (vertices at rotation + 90 i; flats at
  /// rotation + 45 + 90 i).
  rotation_deg : Double
}

///|
pub enum Shape {
  Cuboid(Cuboid)
  Cylinder(Cylinder)
  RevolvedProfile(RevolvedProfile)
  ChamferedPrism(ChamferedPrism)
  SocketedSolid(SocketedSolid)
  ConicalNut(ConicalNut)
  SlitTube(SlitTube)
  AcornNut(AcornNut)
  SquareNut(SquareNut)
  CalibrationChamferBlock(CalibrationChamferBlock)
  ExtrudedProfile(ExtrudedProfile)
  ExtrudedArcProfile(ExtrudedArcProfile)
  SquareHoleWasher(SquareHoleWasher)
  TabWasher(TabWasher)
  HexHeadWasherFace(HexHeadWasherFace)
  ExtrudedBezierProfile(ExtrudedBezierProfile)
  SweptProfile(SweptProfile)
  Fillet(EdgeFeature)
  Chamfer(EdgeFeature)
  Union(BooleanUnion)
  Difference(BooleanDifference)
}

///|
pub struct SceneNode {
  shape : Shape
  name : String
  color : Rgb
  tx : Double
  ty : Double
  tz : Double
  rx : Double // rotation X in degrees
  ry : Double // rotation Y in degrees
  rz : Double // rotation Z in degrees
}

///|
pub(all) enum Face {
  Top
  Bottom
  Front
  Back
  Left
  Right
}

///|
/// SweepPath represents the information needed to sweep a profile along a path.
pub(all) struct SweepPath {
  points : Array[Vec3]
  normals : Array[Vec3]
  tangents : Array[Vec3]
}

///|
/// SweepProfile represents a 2D profile to be swept along a path.
///
/// `vertices` is the outer boundary of the profile; `holes` holds optional
/// inner boundaries (e.g. the bore of a ring gear). All loops are given in the
/// same coordinate system as `vertices`. Loops must be wound so that the outer
/// boundary and every hole have opposite orientation; with the default
/// `normal_dir`/`right_dir` the swept side faces then point out of the solid.
pub(all) struct SweepProfile {
  center : Vec3
  vertices : Array[Vec3]
  holes : Array[Array[Vec3]]
  // normal_dir points in the direction of extrusion
  normal_dir : Vec3
  // right_dir points to the "right" side of the profile when looking along normal_dir
  right_dir : Vec3
}

///|
/// SweepProfile::new returns a new SweepProfile.
pub fn SweepProfile::new(
  vertices : Array[Vec3],
  center~ : Vec3 = (0.0, 0.0, 0.0),
  holes~ : Array[Array[Vec3]] = [],
  normal_dir~ : Vec3 = (0.0, 1.0, 0.0),
  right_dir~ : Vec3 = (1.0, 0.0, 0.0),
) -> SweepProfile {
  { center, vertices, holes, normal_dir, right_dir, }
}

///|
pub fn Shape::bounding_box(
  self : Shape,
) -> (Double, Double, Double, Double, Double, Double) {
  match self {
    Cuboid(c) => (0.0, c.sx, 0.0, c.sy, 0.0, c.sz)
    Cylinder(c) => {
      let r = c.diameter / 2.0
      (-r, r, -r, r, 0.0, c.height)
    }
    RevolvedProfile(rev) => {
      let samples : Array[Vec2] = []
      for seg in rev.segments {
        match seg {
          RevLine(a, b) => {
            samples.push(a)
            samples.push(b)
          }
          RevArc(a, b, c) => {
            // Sample the arc so its extreme radius and height are captured.
            let rho = rev_dist2(a, c)
            let a0 = @math.atan2(a.1 - c.1, a.0 - c.0)
            let a1 = @math.atan2(b.1 - c.1, b.0 - c.0)
            let delta = rev_norm_angle(a1 - a0)
            let count = 64
            for i in 0..<=count {
              let t = Double::from_int(i) / Double::from_int(count)
              let ang = a0 + delta * t
              samples.push(
                (c.0 + rho * @math.cos(ang), c.1 + rho * @math.sin(ang)),
              )
            }
          }
        }
      }
      let mut r_max = 0.0
      let mut z0 = @double.max_value
      let mut z1 = @double.min_value
      for p in samples {
        if p.0 > r_max {
          r_max = p.0
        }
        if p.1 < z0 {
          z0 = p.1
        }
        if p.1 > z1 {
          z1 = p.1
        }
      }
      if r_max <= 0.0 {
        (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
      } else {
        (-r_max, r_max, -r_max, r_max, z0, z1)
      }
    }
    ChamferedPrism(p) => {
      let mut x0 = @double.max_value
      let mut x1 = @double.min_value
      let mut y0 = @double.max_value
      let mut y1 = @double.min_value
      let mut z0 = @double.max_value
      let mut z1 = @double.min_value
      for level in p.levels {
        let (z, inset) = level
        let ring = match chamfer_inset(p.base, inset) {
          Some(r) => r
          None => p.base
        }
        for pt in ring {
          if pt.0 < x0 {
            x0 = pt.0
          }
          if pt.0 > x1 {
            x1 = pt.0
          }
          if pt.1 < y0 {
            y0 = pt.1
          }
          if pt.1 > y1 {
            y1 = pt.1
          }
        }
        if z < z0 {
          z0 = z
        }
        if z > z1 {
          z1 = z
        }
      }
      (x0, x1, y0, y1, z0, z1)
    }
    SocketedSolid(s) => Shape::RevolvedProfile(s.body).bounding_box()
    ConicalNut(n) => conical_nut_bounding_box(n)
    SlitTube(t) => {
      let r = t.outer_diameter_mm / 2.0
      (-r, r, -r, r, 0.0, t.length_mm)
    }
    AcornNut(n) => acorn_nut_bounding_box(n)
    SquareNut(n) => square_nut_bounding_box(n)
    CalibrationChamferBlock(b) =>
      (0.0, b.length_mm, 0.0, b.width_mm, 0.0, b.height_mm)
    ExtrudedProfile(e) => {
      let mut x0 = @double.max_value
      let mut x1 = @double.min_value
      let mut y0 = @double.max_value
      let mut y1 = @double.min_value
      for pt in e.profile.outer {
        if pt.0 < x0 {
          x0 = pt.0
        }
        if pt.0 > x1 {
          x1 = pt.0
        }
        if pt.1 < y0 {
          y0 = pt.1
        }
        if pt.1 > y1 {
          y1 = pt.1
        }
      }
      (x0, x1, y0, y1, 0.0, e.height_mm)
    }
    ExtrudedArcProfile(e) => {
      let mut x0 = @double.max_value
      let mut x1 = @double.min_value
      let mut y0 = @double.max_value
      let mut y1 = @double.min_value
      for seg in e.segments {
        match seg {
          SegmentLine(a, b) =>
            for pt in [a, b] {
              if pt.0 < x0 {
                x0 = pt.0
              }
              if pt.0 > x1 {
                x1 = pt.0
              }
              if pt.1 < y0 {
                y0 = pt.1
              }
              if pt.1 > y1 {
                y1 = pt.1
              }
            }
          SegmentArc(a, b, c) => {
            let rho = arc_dist2(a, c)
            let a0 = @math.atan2(a.1 - c.1, a.0 - c.0)
            let delta = arc_norm_sweep(@math.atan2(b.1 - c.1, b.0 - c.0) - a0)
            for k in 0..<=64 {
              let t = Double::from_int(k) / 64.0
              let ang = a0 + delta * t
              let px = c.0 + rho * @math.cos(ang)
              let py = c.1 + rho * @math.sin(ang)
              if px < x0 {
                x0 = px
              }
              if px > x1 {
                x1 = px
              }
              if py < y0 {
                y0 = py
              }
              if py > y1 {
                y1 = py
              }
            }
            // Axis extrema occur at the cardinal angles; include them exactly
            // rather than relying on the uniform samples to land on them.
            for base in [0.0, @math.PI / 2.0, @math.PI, 3.0 * @math.PI / 2.0] {
              for shift in -1..<=2 {
                let ang = base + 2.0 * @math.PI * Double::from_int(shift)
                if ang >= a0 - CAD_ANGLE_EPS &&
                  ang <= a0 + delta + CAD_ANGLE_EPS {
                  let px = c.0 + rho * @math.cos(ang)
                  let py = c.1 + rho * @math.sin(ang)
                  if px < x0 {
                    x0 = px
                  }
                  if px > x1 {
                    x1 = px
                  }
                  if py < y0 {
                    y0 = py
                  }
                  if py > y1 {
                    y1 = py
                  }
                }
              }
            }
          }
        }
      }
      (x0, x1, y0, y1, 0.0, e.height_mm)
    }
    SquareHoleWasher(w) => {
      let r = w.outer_diameter_mm / 2.0
      (-r, r, -r, r, 0.0, w.thickness_mm)
    }
    TabWasher(w) => {
      let r = w.outer_diameter_mm / 2.0
      (-w.tab_length_mm, r, -r, r, 0.0, w.thickness_mm)
    }
    HexHeadWasherFace(h) => {
      let r = h.across_flats_mm / Double::sqrt(3.0)
      let z0 = if h.shank_diameter_mm > 0.0 && h.shank_length_mm > 0.0 {
        -h.shank_length_mm
      } else {
        0.0
      }
      (-r, r, -r, r, z0, h.head_height_mm)
    }
    ExtrudedBezierProfile(e) => {
      let mut x0 = @double.max_value
      let mut x1 = @double.min_value
      let mut y0 = @double.max_value
      let mut y1 = @double.min_value
      for path in e.paths {
        for anchor in path.anchors {
          // Check position and handles for bounding box
          for pt in [anchor.position, anchor.handle_in, anchor.handle_out] {
            if pt.0 < x0 {
              x0 = pt.0
            }
            if pt.0 > x1 {
              x1 = pt.0
            }
            if pt.1 < y0 {
              y0 = pt.1
            }
            if pt.1 > y1 {
              y1 = pt.1
            }
          }
        }
      }
      (x0, x1, y0, y1, 0.0, e.height_mm)
    }
    Difference(d) => d.base.shape.bounding_box()
    Fillet(f) => f.base.bounding_box()
    Chamfer(c) => c.base.bounding_box()
    SweptProfile(s) => {
      // Sweep the actual profile vertices through the same frames the STEP
      // compiler uses. The old implementation expanded the path bounding box by
      // a fixed 5 mm, which made collision checks and component sizes
      // needlessly conservative (a thread or spring appeared 5 mm larger in
      // every direction).
      let path = s.path
      let profile = s.profile
      let n_path = path.points.length()
      if n_path == 0 || profile.vertices.length() < 3 {
        (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
      } else {
        // Profile-local frame: X = right_dir, Z = normal_dir, Y = Z x X.
        let prof_z = v_norm(profile.normal_dir)
        let prof_x = v_norm(profile.right_dir)
        let prof_y = v_cross(prof_z, prof_x)
        let m_global_to_prof = Matrix3x3::{
          m00: prof_x.0,
          m01: prof_x.1,
          m02: prof_x.2,
          m10: prof_y.0,
          m11: prof_y.1,
          m12: prof_y.2,
          m20: prof_z.0,
          m21: prof_z.1,
          m22: prof_z.2,
        }
        let loops : Array[Array[Vec3]] = [
          profile.vertices,
          ..profile.holes.filter(fn(h) { h.length() >= 3 }),
        ]
        let local_verts : Array[Vec3] = []
        let is_hole : Array[Bool] = []
        let mut loop_index = 0
        for lp in loops {
          for v in lp {
            local_verts.push(
              apply_matrix(m_global_to_prof, v_sub(v, profile.center)),
            )
            is_hole.push(loop_index > 0)
          }
          loop_index = loop_index + 1
        }
        // First path frame, used by straight outlines and straight holes.
        let t0 = v_norm(path.tangents[0])
        let n0 = v_norm(path.normals[0])
        let r0 = v_norm(v_cross(t0, n0))
        let m_start_to_global = Matrix3x3::{
          m00: r0.0,
          m01: n0.0,
          m02: t0.0,
          m10: r0.1,
          m11: n0.1,
          m12: t0.1,
          m20: r0.2,
          m21: n0.2,
          m22: t0.2,
        }
        let mut x0 = @double.max_value
        let mut x1 = @double.min_value
        let mut y0 = @double.max_value
        let mut y1 = @double.min_value
        let mut z0 = @double.max_value
        let mut z1 = @double.min_value
        for i in 0.. x1 {
              x1 = px
            }
            if py < y0 {
              y0 = py
            }
            if py > y1 {
              y1 = py
            }
            if pz < z0 {
              z0 = pz
            }
            if pz > z1 {
              z1 = pz
            }
          }
        }
        (x0, x1, y0, y1, z0, z1)
      }
    }
    Union(u) => {
      let mut x0 = @double.max_value
      let mut x1 = @double.min_value
      let mut y0 = @double.max_value
      let mut y1 = @double.min_value
      let mut z0 = @double.max_value
      let mut z1 = @double.min_value
      for child in u.nodes {
        let (cx0, cx1, cy0, cy1, cz0, cz1) = child.bounding_box()
        if cx0 < x0 {
          x0 = cx0
        }
        if cx1 > x1 {
          x1 = cx1
        }
        if cy0 < y0 {
          y0 = cy0
        }
        if cy1 > y1 {
          y1 = cy1
        }
        if cz0 < z0 {
          z0 = cz0
        }
        if cz1 > z1 {
          z1 = cz1
        }
      }
      (x0, x1, y0, y1, z0, z1)
    }
  }
}

///|
pub fn SceneNode::bounding_box(
  self : SceneNode,
) -> (Double, Double, Double, Double, Double, Double) {
  let (lx0, lx1, ly0, ly1, lz0, lz1) = self.shape.bounding_box()
  let corners = [
    (lx0, ly0, lz0),
    (lx1, ly0, lz0),
    (lx1, ly1, lz0),
    (lx0, ly1, lz0),
    (lx0, ly0, lz1),
    (lx1, ly0, lz1),
    (lx1, ly1, lz1),
    (lx0, ly1, lz1),
  ]
  let mut x0 = @double.max_value
  let mut x1 = @double.min_value
  let mut y0 = @double.max_value
  let mut y1 = @double.min_value
  let mut z0 = @double.max_value
  let mut z1 = @double.min_value
  for p in corners {
    let (tx, ty, tz) = rotate_point(p, self.rx, self.ry, self.rz)
    let gx = tx + self.tx
    let gy = ty + self.ty
    let gz = tz + self.tz
    if gx < x0 {
      x0 = gx
    }
    if gx > x1 {
      x1 = gx
    }
    if gy < y0 {
      y0 = gy
    }
    if gy > y1 {
      y1 = gy
    }
    if gz < z0 {
      z0 = gz
    }
    if gz > z1 {
      z1 = gz
    }
  }
  (x0, x1, y0, y1, z0, z1)
}