import * as THREE from "three";

const v = (x, y, z) => new THREE.Vector3(x, y, z);

function mk(geo, mat) {
  const m = new THREE.Mesh(geo, mat);
  m.castShadow = true;
  m.receiveShadow = true;
  return m;
}
function box(w, h, d, mat, x = 0, y = 0, z = 0) {
  const m = mk(new THREE.BoxGeometry(w, h, d), mat);
  m.position.set(x, y, z);
  return m;
}
function quadGeo(a, b, c, d) {
  const g = new THREE.BufferGeometry();
  g.setFromPoints([a, b, c, a, c, d]);
  g.computeVertexNormals();
  const uv = new Float32Array([0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1]);
  g.setAttribute("uv", new THREE.BufferAttribute(uv, 2));
  return g;
}
function triGeo(a, b, c) {
  const g = new THREE.BufferGeometry();
  g.setFromPoints([a, b, c]);
  g.computeVertexNormals();
  g.setAttribute("uv", new THREE.BufferAttribute(new Float32Array([0, 0, 1, 0, 0.5, 1]), 2));
  return g;
}
/* thin cap box laid along segment a->b (ridge caps, hip caps, highlights) */
function capAlong(a, b, mat, h = 0.55, w = 1.35) {
  const len = a.distanceTo(b);
  const m = box(len, h, w, mat);
  m.position.copy(a).add(b).multiplyScalar(0.5);
  const dir = v(0, 0, 0).subVectors(b, a).normalize();
  m.quaternion.setFromUnitVectors(v(1, 0, 0), dir);
  return m;
}

/* ---------------- roof builders (geometry == measurement source) ------------- */

function gableRoof({ span, len, pitch, oE = 1.5, oG = 1.2, y0, matFront, matBack, capMat, fasciaMat }) {
  const p = pitch / 12, th = 0.45;
  const theta = Math.atan(p);
  const rise = (span / 2) * p;
  const ridgeY = y0 + rise;
  const halfH = span / 2 + oE;
  const slopeLen = halfH / Math.cos(theta);
  const eaveY = ridgeY - halfH * p;
  const g = new THREE.Group();
  const geo = new THREE.BoxGeometry(len + 2 * oG, th, slopeLen);
  for (const s of [1, -1]) {
    const m = mk(geo, s === 1 ? matFront : matBack);
    m.rotation.x = s * theta;
    m.position.set(0, (ridgeY + eaveY) / 2 + th * 0.5, (s * halfH) / 2);
    g.add(m);
  }
  g.add(box(len + 2 * oG + 0.3, 0.5, 1.5, capMat, 0, ridgeY + 0.45, 0));
  for (const s of [1, -1]) g.add(box(len + 2 * oG, 0.85, 0.4, fasciaMat, 0, eaveY, s * halfH));
  const hl = len / 2 + oG;
  return {
    g,
    area: 2 * (len + 2 * oG) * slopeLen,
    capLf: len,
    ridgeY, eaveY,
    ridgeSeg: [v(-hl, ridgeY + 0.5, 0), v(hl, ridgeY + 0.5, 0)],
    eaveSegs: [
      [v(-hl, eaveY, halfH), v(hl, eaveY, halfH)],
      [v(-hl, eaveY, -halfH), v(hl, eaveY, -halfH)],
    ],
  };
}

function hipRoof({ span, len, pitch, oE = 1.5, y0, mat, capMat, fasciaMat }) {
  const p = pitch / 12;
  const rise = (span / 2) * p;
  const ridgeY = y0 + rise;
  const eaveY = y0 - oE * p;
  const dY = ridgeY - eaveY;
  const halfW = span / 2 + oE, halfL = len / 2 + oE;
  const rl = Math.max(0, len - span) / 2;
  const R1 = v(-rl, ridgeY, 0), R2 = v(rl, ridgeY, 0);
  const E = [v(-halfL, eaveY, halfW), v(halfL, eaveY, halfW), v(halfL, eaveY, -halfW), v(-halfL, eaveY, -halfW)];
  const g = new THREE.Group();
  g.add(mk(quadGeo(E[0], E[1], R2, R1), mat));
  g.add(mk(quadGeo(E[2], E[3], R1, R2), mat));
  g.add(mk(triGeo(E[1], E[2], R2), mat));
  g.add(mk(triGeo(E[3], E[0], R1), mat));
  for (const s of [1, -1]) g.add(box(2 * halfL, 0.85, 0.4, fasciaMat, 0, eaveY, s * halfW));
  for (const s of [1, -1]) {
    const f = box(2 * halfW, 0.85, 0.4, fasciaMat, s * halfL, eaveY, 0);
    f.rotation.y = Math.PI / 2;
    g.add(f);
  }
  const hipSegs = [[E[0], R1], [E[3], R1], [E[1], R2], [E[2], R2]];
  hipSegs.forEach((s) => g.add(capAlong(s[0].clone().add(v(0, 0.35, 0)), s[1].clone().add(v(0, 0.35, 0)), capMat)));
  if (rl > 0.1) g.add(box(rl * 2 + 0.4, 0.5, 1.5, capMat, 0, ridgeY + 0.4, 0));
  const slopeLen = Math.hypot(halfW, dY);
  const endSlope = Math.hypot(halfL - rl, dY);
  const area = 2 * ((2 * halfL + 2 * rl) / 2) * slopeLen + 2 * 0.5 * (2 * halfW) * endSlope;
  const capLf = rl * 2 + hipSegs.reduce((a, s) => a + s[0].distanceTo(s[1]), 0);
  return { g, area, capLf, ridgeY, eaveY, ridgeSeg: [R1.clone().add(v(0, 0.6, 0)), R2.clone().add(v(0, 0.6, 0))], eaveSegs: [[E[0], E[1]], [E[3], E[2]]], hipSegs };
}

/* ---------------- kit parts ------------------------------------------------ */

function windowUnit(M, w = 3.2, h = 4.4) {
  const g = new THREE.Group();
  g.add(box(w + 0.55, h + 0.55, 0.32, M.trim));
  const gl = box(w, h, 0.16, M.glass, 0, 0, 0.12);
  g.add(gl);
  g.add(box(w, 0.14, 0.1, M.trim, 0, 0, 0.22));
  g.add(box(0.14, h, 0.1, M.trim, 0, 0, 0.22));
  return g;
}
function doorUnit(M, mat, w = 3.1, h = 6.9) {
  const g = new THREE.Group();
  g.add(box(w + 0.6, h + 0.4, 0.3, M.trim, 0, 0.1, 0));
  g.add(box(w, h, 0.24, mat, 0, 0, 0.12));
  g.add(box(0.5, 0.12, 0.34, M.trim, w / 2 - 0.5, 0, 0.14));
  return g;
}
function put(parent, obj, x, y, z, rotY = 0) {
  obj.position.set(x, y, z);
  obj.rotation.y = rotY;
  parent.add(obj);
  return obj;
}
function chimney(M, h, x, z, w = 3.4, d = 3) {
  const g = new THREE.Group();
  g.add(box(w, h, d, M.brick, 0, h / 2, 0));
  g.add(box(w + 0.8, 0.6, d + 0.8, M.concrete, 0, h + 0.3, 0));
  g.add(box(1.3, 1, 1.1, M.porchRoof, 0, h + 1, 0));
  g.position.set(x, 0, z);
  return g;
}
function tree(M, s = 1) {
  const g = new THREE.Group();
  const trunk = mk(new THREE.CylinderGeometry(0.45 * s, 0.7 * s, 7 * s, 7), M.trunk);
  trunk.position.y = 3.5 * s;
  g.add(trunk);
  [[0, 9.6, 0, 4.3], [2.1, 7.8, 1.1, 3], [-1.9, 8.2, -0.8, 3.2]].forEach((f, i) => {
    const fol = mk(new THREE.IcosahedronGeometry(f[3] * s, 1), i === 1 ? M.foliage2 : M.foliage);
    fol.position.set(f[0] * s, f[1] * s, f[2] * s);
    g.add(fol);
  });
  return g;
}
function shrub(M, s = 1) {
  const m = mk(new THREE.IcosahedronGeometry(1.15 * s, 1), M.foliage2);
  m.scale.y = 0.75;
  m.position.y = 0.85 * s;
  return m;
}
function mailbox(M) {
  const g = new THREE.Group();
  g.userData.ink = "site";
  g.add(box(0.28, 3.6, 0.28, M.trunk, 0, 1.8, 0));
  g.add(box(1.5, 0.9, 0.8, M.porchRoof, 0.3, 3.9, 0));
  return g;
}
function pediment(M, mat, span, rise, x, y0, alongZ = false) {
  const a = alongZ ? v(x, y0, -span / 2) : v(-span / 2, y0, x);
  const b = alongZ ? v(x, y0, span / 2) : v(span / 2, y0, x);
  const c = alongZ ? v(x, y0 + rise, 0) : v(0, y0 + rise, x);
  const mm = mk(triGeo(a, b, c), mat);
  mm.material = mm.material.clone();
  mm.material.side = THREE.DoubleSide;
  return mm;
}

function hlFromSegs(M, segs, w = 1.7) {
  const g = new THREE.Group();
  segs.forEach((s) => g.add(capAlong(s[0].clone().add(v(0, 0.35, 0)), s[1].clone().add(v(0, 0.35, 0)), M.hlAmber, 0.7, w)));
  g.visible = false;
  return g;
}

/* translucent wash over the main roof slopes, so the "roof" line-item hover reads
   as prominently as the ridge/eave bar highlights instead of just a thin edge tint */
function hlRoofWash(M, roof, hip) {
  const lift = v(0, 0.12, 0);
  const up = (p) => p.clone().add(lift);
  const R = roof.ridgeSeg, E = roof.eaveSegs;
  const faces = [
    quadGeo(up(E[0][0]), up(E[0][1]), up(R[1]), up(R[0])),
    quadGeo(up(E[1][1]), up(E[1][0]), up(R[0]), up(R[1])),
  ];
  if (hip) {
    faces.push(triGeo(up(E[0][1]), up(E[1][1]), up(R[1])));
    faces.push(triGeo(up(E[1][0]), up(E[0][0]), up(R[0])));
  }
  const g = new THREE.Group();
  faces.forEach((geo) => {
    const m = new THREE.Mesh(geo, M.hlFace);
    m.castShadow = false;
    m.receiveShadow = false;
    g.add(m);
  });
  g.visible = false;
  g.userData.ink = "skip";
  return g;
}

/* ---------------- the three houses ---------------------------------------- */

export function buildGable(M) {
  const g = new THREE.Group();
  const MW = 42, MD = 28, WH = 19, PITCH = 6;
  const rise = (MD / 2) * (PITCH / 12);
  const roofF = M.shingle.clone(), roofB = M.shingle.clone();
  const roofG1 = M.shingle.clone(), roofG2 = M.shingle.clone();

  g.add(box(MW + 1, 1.4, MD + 1, M.concrete, 0, 0.7, 0));            /* foundation plinth */
  g.add(box(MW, WH, MD, M.sidingWhite, 0, WH / 2 + 1, 0));
  g.add(pediment(M, M.sidingWhite, MD, rise, MW / 2, WH + 1, true));
  g.add(pediment(M, M.sidingWhite, MD, rise, -MW / 2, WH + 1, true));
  const roof = gableRoof({ span: MD, len: MW, pitch: PITCH, y0: WH + 1, matFront: roofF, matBack: roofB, capMat: M.fascia, fasciaMat: M.fascia });
  g.add(roof.g);
  const roofWash = hlRoofWash(M, roof, false);
  g.add(roofWash);

  /* attached garage, gable facing the street */
  const GW = 20, GD = 22, GH = 10;
  const gx = MW / 2 + GW / 2 - 0.2, gz = MD / 2 - GD / 2;
  g.add(box(GW + 0.8, 1.2, GD + 0.8, M.concrete, gx, 0.6, gz));
  g.add(box(GW, GH, GD, M.sidingWhite, gx, GH / 2 + 1, gz));
  const grise = (GW / 2) * (PITCH / 12);
  const gRoofGrp = new THREE.Group();
  const gRoof = gableRoof({ span: GW, len: GD, pitch: PITCH, y0: GH + 1, matFront: roofG1, matBack: roofG2, capMat: M.fascia, fasciaMat: M.fascia });
  gRoofGrp.add(gRoof.g);
  gRoofGrp.rotation.y = Math.PI / 2;
  gRoofGrp.position.set(gx, 0, gz);
  g.add(gRoofGrp);
  const pedF = pediment(M, M.sidingWhite, GW, grise, 0, GH + 1, false);
  pedF.position.set(gx, 0, MD / 2 - 0.01);
  g.add(pedF);
  put(g, doorUnit(M, M.garage, 16, 7.6), gx, 4.9, MD / 2 + 0.1);

  put(g, chimney(M, WH + rise + 5, -MW / 2 - 1.7, -4));

  /* porch over the front door */
  const px = -8;
  g.add(box(9.5, 0.9, 5.5, M.concrete, px, 0.45, MD / 2 + 2.6));
  [[-4, 2.2], [4, 2.2], [-4, 4.9], [4, 4.9]].forEach((pp) =>
    g.add(box(0.55, 8.6, 0.55, M.trim, px + pp[0], 5.2, MD / 2 + pp[1])));
  const pr = box(11, 0.4, 7, M.porchRoof, px, 10.2, MD / 2 + 2.8);
  pr.rotation.x = 0.12;
  g.add(pr);
  put(g, doorUnit(M, M.doorRed), px, 4.45, MD / 2 + 0.12);

  /* windows */
  const F = MD / 2 + 0.1;
  [-16, -8, 0, 8, 16].forEach((x) => put(g, windowUnit(M), x, 15.6, F));
  [-16, 0, 8, 16].forEach((x) => put(g, windowUnit(M, 3.2, 5), x, 6.4, F));
  [-14, -4, 6, 16].forEach((x) => put(g, windowUnit(M), x, 15.6, -F));
  [-14, 4, 14].forEach((x) => put(g, windowUnit(M, 3.2, 5), x, 6.4, -F));
  [-6, 6].forEach((z) => put(g, windowUnit(M), -MW / 2 - 0.1, 15.6, z, -Math.PI / 2));
  put(g, windowUnit(M, 2.6, 3.4), gx + GW / 2 + 0.1, 6, gz, Math.PI / 2);

  /* grounds */
  g.add(box(17, 0.14, 30, M.asphalt, gx, 0.08, MD / 2 + 15));
  g.add(box(3.4, 0.12, 26, M.concrete, px, 0.07, MD / 2 + 13));
  put(g, mailbox(M), px + 5, 0, 41);
  put(g, tree(M, 1.15), -33, 0, -15);
  put(g, tree(M, 0.9), 29, 0, -24);
  put(g, tree(M, 0.75), -31, 0, 14);
  [-18, -13, -3, 3, 13, 18].forEach((x, i) => put(g, shrub(M, 1 + (i % 3) * 0.18), x, 0, MD / 2 + 2.2));

  const area = Math.round(roof.area + gRoof.area);
  return {
    group: g,
    measure: { roofSqFt: area, ridgeLf: Math.round(roof.capLf + gRoof.capLf), facets: 5 },
    anchors: {
      spanA: v(-MW / 2, 0.4, MD / 2), spanB: v(MW / 2, 0.4, MD / 2),
      peakTop: v(-MW / 2, WH + 1 + rise + 0.6, 0), peakBase: v(-MW / 2, 0, 0),
      pitchAt: v(-MW / 2 - 1.2, roof.eaveY, MD / 2 + 1.5),
    },
    hl: {
      ridge: hlFromSegs(M, [roof.ridgeSeg, [v(gx, gRoof.ridgeY + 0.5, gz - GD / 2 - 1.2), v(gx, gRoof.ridgeY + 0.5, gz + GD / 2 + 1.2)]]),
      eave: hlFromSegs(M, roof.eaveSegs),
      roof: roofWash,
      roofMats: [roofF, roofB, roofG1, roofG2],
      damage: null,
    },
  };
}

export function buildHip(M) {
  const g = new THREE.Group();
  const RW = 46, RD = 26, WH = 10.5, PITCH = 6;
  const roofM = M.shingleWarm.clone(), roofG = M.shingleWarm.clone(), roofS = M.shingleWarm.clone();

  g.add(box(RW + 1, 1.2, RD + 1, M.concrete, 0, 0.6, 0));
  g.add(box(RW, WH, RD, M.sidingBeige, 0, WH / 2 + 1, 0));
  const roof = hipRoof({ span: RD, len: RW, pitch: PITCH, y0: WH + 1, mat: roofM, capMat: M.fascia, fasciaMat: M.fascia });
  g.add(roof.g);
  const roofWash = hlRoofWash(M, roof, true);
  g.add(roofWash);

  /* attached hip garage, protruding toward the street */
  const GW = 20, GH = 9;
  const gx = 14, gz = RD / 2 + GW / 2 - 1;
  g.add(box(GW + 0.8, 1.1, GW + 0.8, M.concrete, gx, 0.55, gz));
  g.add(box(GW, GH, GW, M.sidingBeige, gx, GH / 2 + 1, gz));
  const gRoof = hipRoof({ span: GW, len: GW, pitch: PITCH, y0: GH + 1, mat: roofG, capMat: M.fascia, fasciaMat: M.fascia });
  gRoof.g.position.set(gx, 0, gz);
  g.add(gRoof.g);
  put(g, doorUnit(M, M.garage, 15, 7.2), gx, 4.7, gz + GW / 2 + 0.1);

  /* windows + door on the clear side of the facade */
  const F = RD / 2 + 0.1;
  put(g, doorUnit(M, M.doorBlue), -2, 4.45, F);
  [-14.5, -11.2, -7.9].forEach((x) => put(g, windowUnit(M, 2.9, 4.2), x, 6, F));
  put(g, windowUnit(M, 3.2, 4.2), -19.5, 6, F);
  [-12, 0, 12].forEach((x) => put(g, windowUnit(M, 3.2, 4.2), x, 6, -F));
  [-4, 5].forEach((z) => put(g, windowUnit(M, 3, 4), -RW / 2 - 0.1, 6, z, -Math.PI / 2));

  /* backyard shed */
  const shed = new THREE.Group();
  shed.add(box(10, 6.5, 8, M.sidingGray, 0, 3.25, 0));
  const sr = gableRoof({ span: 8, len: 10, pitch: 6, oE: 0.8, oG: 0.7, y0: 6.5, matFront: roofS, matBack: roofS, capMat: M.fascia, fasciaMat: M.fascia });
  shed.add(sr.g);
  shed.add(pediment(M, M.sidingGray, 8, 2, 5, 6.5, true));
  shed.add(pediment(M, M.sidingGray, 8, 2, -5, 6.5, true));
  put(shed, doorUnit(M, M.doorBlue, 2.6, 5.4), 0, 3.7, 4.1);
  shed.position.set(-26, 0, -19);
  shed.rotation.y = 0.35;
  g.add(shed);

  /* rear terrace with pergola */
  g.add(box(18, 0.3, 12, M.concrete, 4, 0.15, -RD / 2 - 6));
  [[-8, -1.5], [8, -1.5], [-8, -10.5], [8, -10.5]].forEach((pp) =>
    g.add(box(0.6, 8.4, 0.6, M.trim, 4 + pp[0], 4.2, -RD / 2 + pp[1])));
  for (let i = 0; i < 5; i++) g.add(box(18.5, 0.22, 0.9, M.trim, 4, 8.5, -RD / 2 - 2 - i * 2.1));

  /* grounds */
  g.add(box(17, 0.14, 13, M.asphalt, gx, 0.08, gz + GW / 2 + 6.5));
  g.add(box(3, 0.12, 29, M.concrete, -2, 0.07, F + 14.5));
  put(g, mailbox(M), 3, 0, 42);
  put(g, tree(M, 1.3), -31, 0, 8);
  put(g, tree(M, 1), 30, 0, -14);
  [-16, -6, 2].forEach((x, i) => put(g, shrub(M, 1 + i * 0.15), x, 0, F + 1.8));

  return {
    group: g,
    measure: { roofSqFt: Math.round(roof.area + gRoof.area + sr.area), ridgeLf: Math.round(roof.capLf + gRoof.capLf + sr.capLf), facets: 10 },
    anchors: {
      spanA: v(-RW / 2, 0.4, RD / 2), spanB: v(RW / 2, 0.4, RD / 2),
      peakTop: v(-(RW - RD) / 2, roof.ridgeY + 0.8, 0), peakBase: v(-(RW - RD) / 2, 0, 0),
      pitchAt: v(-RW / 2 - 1.5, roof.eaveY, RD / 2 + 1.5),
    },
    hl: {
      ridge: hlFromSegs(M, [roof.ridgeSeg].concat(roof.hipSegs)),
      eave: hlFromSegs(M, roof.eaveSegs),
      roof: roofWash,
      roofMats: [roofM, roofG, roofS],
      damage: null,
    },
  };
}

export function buildStorm(M) {
  const g = new THREE.Group();
  const FW = 58, FD = 28, WH = 18, PITCH = 5;
  const rise = (FD / 2) * (PITCH / 12);
  const roofF = M.shingleStorm.clone(), roofB = M.shingle.clone(), roofW = M.shingle.clone();

  g.add(box(FW + 1, 1.4, FD + 1, M.concrete, 0, 0.7, 0));
  g.add(box(FW, WH, FD, M.sidingGray, 0, WH / 2 + 1, 0));
  g.add(pediment(M, M.sidingGray, FD, rise, FW / 2, WH + 1, true));
  g.add(pediment(M, M.sidingGray, FD, rise, -FW / 2, WH + 1, true));
  const roof = gableRoof({ span: FD, len: FW, pitch: PITCH, y0: WH + 1, matFront: roofF, matBack: roofB, capMat: M.fascia, fasciaMat: M.fascia });
  g.add(roof.g);
  const roofWash = hlRoofWash(M, roof, false);
  g.add(roofWash);
  put(g, chimney(M, WH + rise + 6, 7, 0));

  /* rear single-storey wing (the L that makes this a 30-square claim) */
  const WGW = 24, WGL = 20, WGH = 10;
  const wx = -8, wz = -FD / 2 - WGL / 2 + 0.5;
  g.add(box(WGW + 0.8, 1.2, WGL + 0.8, M.concrete, wx, 0.6, wz));
  g.add(box(WGW, WGH, WGL, M.sidingGray, wx, WGH / 2 + 1, wz));
  const wrise = (WGW / 2) * (PITCH / 12);
  const wRoofGrp = new THREE.Group();
  const wRoof = gableRoof({ span: WGW, len: WGL, pitch: PITCH, y0: WGH + 1, matFront: roofW, matBack: roofW, capMat: M.fascia, fasciaMat: M.fascia });
  wRoofGrp.add(wRoof.g);
  wRoofGrp.rotation.y = Math.PI / 2;
  wRoofGrp.position.set(wx, 0, wz);
  g.add(wRoofGrp);
  const wPed = pediment(M, M.sidingGray, WGW, wrise, 0, WGH + 1, false);
  wPed.position.set(wx, 0, wz - WGL / 2 - 0.01);
  wPed.rotation.y = Math.PI;
  g.add(wPed);
  [-6, 6].forEach((dx) => put(g, windowUnit(M, 3, 4), wx + dx, 6, wz - WGL / 2 - 0.1, Math.PI));

  /* full-width front porch */
  const F = FD / 2;
  g.add(box(FW - 2, 0.9, 7, M.concrete, 0, 0.45, F + 3.4));
  [-26, -13, 0, 13, 26].forEach((x) => g.add(box(0.6, 9.3, 0.6, M.trim, x, 5.55, F + 6.2)));
  const pr = box(FW, 0.4, 8.6, M.porchRoof, 0, 11, F + 3.6);
  pr.rotation.x = 0.1;
  g.add(pr);

  put(g, doorUnit(M, M.doorBlue, 3.4, 7), 0, 4.5, F + 0.12);
  [-23, -11.5, 11.5, 23].forEach((x) => put(g, windowUnit(M, 3.2, 4.6), x, 6.4, F + 0.1));
  [-23, -11.5, 0, 11.5, 23].forEach((x) => put(g, windowUnit(M), x, 15.4, F + 0.1));
  [-25, 9, 17, 25].forEach((x) => put(g, windowUnit(M), x, 15.4, -F - 0.1));
  [10, 18, 26].forEach((x) => put(g, windowUnit(M, 3.2, 4.6), x, 6.4, -F - 0.1));
  [-5, 5].forEach((z) => put(g, windowUnit(M, 3, 4.2), -FW / 2 - 0.1, 14, z, -Math.PI / 2));

  /* tarped section on the damaged plane */
  const p = PITCH / 12;
  const ry = WH + 1 + rise;
  const tarp = mk(quadGeo(
    v(9, ry + 0.55, 0.4), v(21, ry + 0.55, 0.4),
    v(21.6, ry - 8.2 * p + 0.55, 8.6), v(8.4, ry - 8.2 * p + 0.55, 8.6)
  ), M.tarp);
  g.add(tarp);

  /* 41 hail impacts clustered on the front plane (facet F2) */
  const impactMat = M.impact.clone();
  const imp = new THREE.InstancedMesh(new THREE.SphereGeometry(0.36, 8, 6), impactMat, 41);
  const dm = new THREE.Matrix4();
  let sd = 4021;
  const rr = () => ((sd = (sd * 16807) % 2147483647) / 2147483647);
  for (let i = 0; i < 41; i++) {
    const cl = rr() < 0.62;
    const x = cl ? -18 + rr() * 20 : -28 + rr() * 56;
    const t = 0.12 + rr() * 0.8;
    const z = t * (FD / 2 + 1.2);
    const y = ry - z * p + 0.45;
    dm.makeScale(1, 0.4, 1);
    dm.setPosition(x, y, z);
    imp.setMatrixAt(i, dm);
  }
  imp.castShadow = false;
  g.add(imp);

  /* storm-yard: debris, fallen limb */
  let s2 = 99;
  const r2 = () => ((s2 = (s2 * 48271) % 2147483647) / 2147483647);
  for (let i = 0; i < 6; i++) {
    const d = box(1.6 + r2() * 2, 0.18, 1 + r2(), M.porchRoof, -20 + r2() * 44, 0.1, F + 8 + r2() * 14);
    d.rotation.y = r2() * 3;
    g.add(d);
  }
  const limb = mk(new THREE.CylinderGeometry(0.35, 0.5, 9, 6), M.trunk);
  limb.rotation.z = Math.PI / 2 - 0.06;
  limb.rotation.y = 0.5;
  limb.position.set(27, 0.5, 16);
  g.add(limb);

  g.add(box(13, 0.14, 30, M.asphalt, 30, 0.08, F + 16));
  g.add(box(3.2, 0.12, 22, M.concrete, 0, 0.07, F + 18));
  put(g, mailbox(M), -5, 0, 42);
  put(g, tree(M, 1.2), -34, 0, -10);
  put(g, tree(M, 1.05), 34, 0, -18);

  return {
    group: g,
    measure: { roofSqFt: Math.round(roof.area + wRoof.area), ridgeLf: Math.round(roof.capLf + wRoof.capLf), facets: 4 },
    anchors: {
      spanA: v(-FW / 2, 0.4, FD / 2), spanB: v(FW / 2, 0.4, FD / 2),
      peakTop: v(-FW / 2, ry + 0.7, 0), peakBase: v(-FW / 2, 0, 0),
      pitchAt: v(-FW / 2 - 1.5, roof.eaveY, FD / 2 + 1.5),
      damageAt: v(-8, ry - 4 * p, 4.5),
    },
    hl: {
      ridge: hlFromSegs(M, [roof.ridgeSeg]),
      eave: hlFromSegs(M, roof.eaveSegs),
      roof: roofWash,
      roofMats: [roofF, roofB, roofW],
      damage: { mesh: imp, mat: impactMat, base: 0.9 },
    },
  };
}

/* multi-wing estate: two perpendicular gable wings plus a rear nub, wrapping a
   courtyard pool — modelled after a client-supplied aerial survey of a complex
   multi-facet roof (5705 Bent Oak Place, Dallas TX). */
export function buildEstate(M) {
  const g = new THREE.Group();
  const MW = 42, MD = 28, WH = 19, PITCH = 6;
  const rise = (MD / 2) * (PITCH / 12);
  const roofF = M.shingle.clone(), roofB = M.shingle.clone();
  const roofW1 = M.shingle.clone(), roofW2 = M.shingle.clone();
  const roofN1 = M.shingle.clone(), roofN2 = M.shingle.clone();

  g.add(box(MW + 1, 1.4, MD + 1, M.concrete, 0, 0.7, 0));            /* foundation plinth */
  g.add(box(MW, WH, MD, M.sidingWhite, 0, WH / 2 + 1, 0));
  g.add(pediment(M, M.sidingWhite, MD, rise, MW / 2, WH + 1, true));
  g.add(pediment(M, M.sidingWhite, MD, rise, -MW / 2, WH + 1, true));
  const roof = gableRoof({ span: MD, len: MW, pitch: PITCH, y0: WH + 1, matFront: roofF, matBack: roofB, capMat: M.fascia, fasciaMat: M.fascia });
  g.add(roof.g);
  const roofWash = hlRoofWash(M, roof, false);
  g.add(roofWash);
  put(g, chimney(M, WH + rise + 5, -MW / 2 - 1.7, -4));

  /* wing A: perpendicular guest wing, front-flush, closes one side of the courtyard */
  const GW = 20, GD = 22, GH = 10;
  const gx = MW / 2 + GW / 2 - 0.2, gz = MD / 2 - GD / 2;
  g.add(box(GW + 0.8, 1.2, GD + 0.8, M.concrete, gx, 0.6, gz));
  g.add(box(GW, GH, GD, M.sidingWhite, gx, GH / 2 + 1, gz));
  const grise = (GW / 2) * (PITCH / 12);
  const gRoofGrp = new THREE.Group();
  const gRoof = gableRoof({ span: GW, len: GD, pitch: PITCH, y0: GH + 1, matFront: roofW1, matBack: roofW2, capMat: M.fascia, fasciaMat: M.fascia });
  gRoofGrp.add(gRoof.g);
  gRoofGrp.rotation.y = Math.PI / 2;
  gRoofGrp.position.set(gx, 0, gz);
  g.add(gRoofGrp);
  const pedF = pediment(M, M.sidingWhite, GW, grise, 0, GH + 1, false);
  pedF.position.set(gx, 0, MD / 2 - 0.01);
  g.add(pedF);
  put(g, doorUnit(M, M.doorBlue, 3, 6.6), gx, 4.4, MD / 2 + 0.1);
  [-6, 6].forEach((z) => put(g, windowUnit(M, 2.8, 4), gx + GW / 2 + 0.1, 6, gz + z, Math.PI / 2));

  /* wing B: small rear nub off the main ridge, the extra facet visible in the survey */
  const NW = 14, ND = 12, NH = 8;
  const nx = -MW / 2 + NW / 2 + 3, nz = -MD / 2 - ND / 2 + 1;
  g.add(box(NW + 0.6, 1, ND + 0.6, M.concrete, nx, 0.5, nz));
  g.add(box(NW, NH, ND, M.sidingWhite, nx, NH / 2 + 1, nz));
  const nRoofGrp = new THREE.Group();
  const nRoof = gableRoof({ span: ND, len: NW, pitch: PITCH, oE: 1, oG: 1, y0: NH + 1, matFront: roofN1, matBack: roofN2, capMat: M.fascia, fasciaMat: M.fascia });
  nRoofGrp.add(nRoof.g);
  nRoofGrp.position.set(nx, 0, nz);
  g.add(nRoofGrp);
  put(g, windowUnit(M, 2.4, 3.4), nx, 5.6, nz - ND / 2 - 0.1, Math.PI);

  /* pool, nestled in the courtyard the wing creates */
  g.add(box(14, 0.3, 7, M.concrete, gx - 2, -0.05, -10));

  /* porch over the front door */
  const px = -8;
  g.add(box(9.5, 0.9, 5.5, M.concrete, px, 0.45, MD / 2 + 2.6));
  [[-4, 2.2], [4, 2.2], [-4, 4.9], [4, 4.9]].forEach((pp) =>
    g.add(box(0.55, 8.6, 0.55, M.trim, px + pp[0], 5.2, MD / 2 + pp[1])));
  const pr = box(11, 0.4, 7, M.porchRoof, px, 10.2, MD / 2 + 2.8);
  pr.rotation.x = 0.12;
  g.add(pr);
  put(g, doorUnit(M, M.doorRed), px, 4.45, MD / 2 + 0.12);

  /* windows */
  const F = MD / 2 + 0.1;
  [-16, -8, 0, 8, 16].forEach((x) => put(g, windowUnit(M), x, 15.6, F));
  [-16, 0, 8, 16].forEach((x) => put(g, windowUnit(M, 3.2, 5), x, 6.4, F));
  [-14, -4, 6, 16].forEach((x) => put(g, windowUnit(M), x, 15.6, -F));
  [-14, 4, 14].forEach((x) => put(g, windowUnit(M, 3.2, 5), x, 6.4, -F));
  [-6, 6].forEach((z) => put(g, windowUnit(M), -MW / 2 - 0.1, 15.6, z, -Math.PI / 2));

  /* grounds */
  g.add(box(17, 0.14, 30, M.asphalt, gx, 0.08, MD / 2 + 15));
  g.add(box(3.4, 0.12, 26, M.concrete, px, 0.07, MD / 2 + 13));
  put(g, mailbox(M), px + 5, 0, 41);
  put(g, tree(M, 1.15), -33, 0, -15);
  put(g, tree(M, 0.9), 29, 0, -24);
  put(g, tree(M, 0.75), -31, 0, 14);
  [-18, -13, -3, 3, 13, 18].forEach((x, i) => put(g, shrub(M, 1 + (i % 3) * 0.18), x, 0, MD / 2 + 2.2));

  const area = Math.round(roof.area + gRoof.area + nRoof.area);
  return {
    group: g,
    measure: { roofSqFt: area, ridgeLf: Math.round(roof.capLf + gRoof.capLf + nRoof.capLf), facets: 6 },
    anchors: {
      spanA: v(-MW / 2, 0.4, MD / 2), spanB: v(MW / 2, 0.4, MD / 2),
      peakTop: v(-MW / 2, WH + 1 + rise + 0.6, 0), peakBase: v(-MW / 2, 0, 0),
      pitchAt: v(-MW / 2 - 1.2, roof.eaveY, MD / 2 + 1.5),
    },
    hl: {
      ridge: hlFromSegs(M, [roof.ridgeSeg, [v(gx, gRoof.ridgeY + 0.5, gz - GD / 2 - 1.2), v(gx, gRoof.ridgeY + 0.5, gz + GD / 2 + 1.2)]]),
      eave: hlFromSegs(M, roof.eaveSegs),
      roof: roofWash,
      roofMats: [roofF, roofB, roofW1, roofW2, roofN1, roofN2],
      damage: null,
    },
  };
}

/* steep 12/12 multi-wing estate with a detached outbuilding, modelled after a client-supplied
   EagleView survey (12552 Mustang Circle, Forney TX): 6,490 sq ft across 37 facets on the real
   roof, simplified here to a cross-wing main house plus a separate garage while keeping the
   signature 12/12 pitch and split-structure footprint. */
export function buildMustang(M) {
  const g = new THREE.Group();
  const MW = 32, MD = 24, WH = 13, PITCH = 12;
  const rise = (MD / 2) * (PITCH / 12);
  const roofF = M.shingle.clone(), roofB = M.shingle.clone();
  const roofW1 = M.shingle.clone(), roofW2 = M.shingle.clone();
  const roofX1 = M.shingle.clone(), roofX2 = M.shingle.clone();
  const roofN1 = M.shingle.clone(), roofN2 = M.shingle.clone();
  const roofG1 = M.shingle.clone(), roofG2 = M.shingle.clone();

  g.add(box(MW + 1, 1.4, MD + 1, M.concrete, 0, 0.7, 0));
  g.add(box(MW, WH, MD, M.sidingWhite, 0, WH / 2 + 1, 0));
  g.add(pediment(M, M.sidingWhite, MD, rise, MW / 2, WH + 1, true));
  g.add(pediment(M, M.sidingWhite, MD, rise, -MW / 2, WH + 1, true));
  const roof = gableRoof({ span: MD, len: MW, pitch: PITCH, y0: WH + 1, matFront: roofF, matBack: roofB, capMat: M.fascia, fasciaMat: M.fascia });
  g.add(roof.g);
  const roofWash = hlRoofWash(M, roof, false);
  g.add(roofWash);
  put(g, chimney(M, WH + rise + 6, 10, -MD / 2 - 2));

  /* wing A: front-right, its own steep gable, offset like the real survey's radiating wings */
  const AW = 18, AD = 20, AH = 11;
  const ax = MW / 2 + AW / 2 - 0.2, az = MD / 2 - AD / 2 - 2;
  g.add(box(AW + 0.8, 1.2, AD + 0.8, M.concrete, ax, 0.6, az));
  g.add(box(AW, AH, AD, M.sidingWhite, ax, AH / 2 + 1, az));
  const arise = (AW / 2) * (PITCH / 12);
  const aRoofGrp = new THREE.Group();
  const aRoof = gableRoof({ span: AW, len: AD, pitch: PITCH, y0: AH + 1, matFront: roofW1, matBack: roofW2, capMat: M.fascia, fasciaMat: M.fascia });
  aRoofGrp.add(aRoof.g);
  aRoofGrp.rotation.y = Math.PI / 2;
  aRoofGrp.position.set(ax, 0, az);
  g.add(aRoofGrp);
  const pedA = pediment(M, M.sidingWhite, AW, arise, 0, AH + 1, false);
  pedA.position.set(ax, 0, az + AD / 2 - 0.01);
  g.add(pedA);
  [-4, 4].forEach((z) => put(g, windowUnit(M, 2.8, 4), ax + AW / 2 + 0.1, 6, az + z, Math.PI / 2));

  /* wing B: rear-left, mirrored offset, so the roofline radiates like the real survey's cross plan */
  const BW = 16, BD = 18, BH = 10;
  const bx = -MW / 2 - BW / 2 + 0.2, bz = -MD / 2 + BD / 2 + 2;
  g.add(box(BW + 0.8, 1.2, BD + 0.8, M.concrete, bx, 0.6, bz));
  g.add(box(BW, BH, BD, M.sidingWhite, bx, BH / 2 + 1, bz));
  const brise = (BW / 2) * (PITCH / 12);
  const bRoofGrp = new THREE.Group();
  const bRoof = gableRoof({ span: BW, len: BD, pitch: PITCH, y0: BH + 1, matFront: roofX1, matBack: roofX2, capMat: M.fascia, fasciaMat: M.fascia });
  bRoofGrp.add(bRoof.g);
  bRoofGrp.rotation.y = Math.PI / 2;
  bRoofGrp.position.set(bx, 0, bz);
  g.add(bRoofGrp);
  const pedB = pediment(M, M.sidingWhite, BW, brise, 0, BH + 1, false);
  pedB.position.set(bx, 0, bz - BD / 2 + 0.01);
  pedB.rotation.y = Math.PI;
  g.add(pedB);
  [-4, 4].forEach((z) => put(g, windowUnit(M, 2.6, 3.6), bx - BW / 2 - 0.1, 5.8, bz + z, -Math.PI / 2));

  /* rear nub: the extra facet visible in the survey's back roofline */
  const NW = 12, ND = 11, NH = 8;
  const nx = 2, nz = -MD / 2 - ND / 2 + 1;
  g.add(box(NW + 0.6, 1, ND + 0.6, M.concrete, nx, 0.5, nz));
  g.add(box(NW, NH, ND, M.sidingWhite, nx, NH / 2 + 1, nz));
  const nRoofGrp = new THREE.Group();
  const nRoof = gableRoof({ span: ND, len: NW, pitch: PITCH, oE: 1, oG: 1, y0: NH + 1, matFront: roofN1, matBack: roofN2, capMat: M.fascia, fasciaMat: M.fascia });
  nRoofGrp.add(nRoof.g);
  nRoofGrp.position.set(nx, 0, nz);
  g.add(nRoofGrp);
  put(g, windowUnit(M, 2.4, 3.2), nx, 5.4, nz - ND / 2 - 0.1, Math.PI);

  /* front door + small stoop; a full porch roof would clip a pitch this steep */
  g.add(box(6, 0.7, 4, M.concrete, -6, 0.35, MD / 2 + 2));
  put(g, doorUnit(M, M.doorRed), -6, 4.45, MD / 2 + 0.12);

  /* windows */
  const F = MD / 2 + 0.1;
  [-12, -4, 4, 12].forEach((x) => put(g, windowUnit(M), x, 15.2, F));
  [-12, 12].forEach((x) => put(g, windowUnit(M, 3.2, 5), x, 6.4, F));
  [-10, 0, 10].forEach((x) => put(g, windowUnit(M), x, 15.2, -F));

  /* detached garage: a separate outbuilding on a shallow 4/12 pitch, matching the survey's Structure #2 */
  const GW = 20, GD = 18, GH = 8, GP = 4;
  const gx = -MW / 2 - 26, gz = 6;
  const garage = new THREE.Group();
  garage.add(box(GW + 0.8, 1, GD + 0.8, M.concrete, 0, 0.5, 0));
  garage.add(box(GW, GH, GD, M.sidingGray, 0, GH / 2 + 0.5, 0));
  const gRoof = gableRoof({ span: GD, len: GW, pitch: GP, oE: 1.3, oG: 1, y0: GH + 0.5, matFront: roofG1, matBack: roofG2, capMat: M.fascia, fasciaMat: M.fascia });
  garage.add(gRoof.g);
  put(garage, doorUnit(M, M.garage, 14, 7), 0, 4.4, GD / 2 + 0.1);
  garage.position.set(gx, 0, gz);
  g.add(garage);

  /* grounds */
  g.add(box(16, 0.14, 34, M.asphalt, gx + 4, 0.08, gz + 20));
  g.add(box(3, 0.12, 20, M.concrete, -6, 0.07, MD / 2 + 12));
  put(g, mailbox(M), -3, 0, 38);
  put(g, tree(M, 1.3), -40, 0, -18);
  put(g, tree(M, 1), 26, 0, -22);
  put(g, tree(M, 0.85), -18, 0, 30);
  [-14, -6, 6, 14].forEach((x, i) => put(g, shrub(M, 1 + (i % 3) * 0.15), x, 0, MD / 2 + 2));

  const area = Math.round(roof.area + aRoof.area + bRoof.area + nRoof.area + gRoof.area);
  return {
    group: g,
    measure: { roofSqFt: area, ridgeLf: Math.round(roof.capLf + aRoof.capLf + bRoof.capLf + nRoof.capLf + gRoof.capLf), facets: 12 },
    anchors: {
      spanA: v(-MW / 2, 0.4, MD / 2), spanB: v(MW / 2, 0.4, MD / 2),
      peakTop: v(-MW / 2, WH + 1 + rise + 0.6, 0), peakBase: v(-MW / 2, 0, 0),
      pitchAt: v(-MW / 2 - 1.2, roof.eaveY, MD / 2 + 1.5),
    },
    hl: {
      ridge: hlFromSegs(M, [
        roof.ridgeSeg,
        [v(ax, aRoof.ridgeY + 0.5, az - AD / 2 - 1.2), v(ax, aRoof.ridgeY + 0.5, az + AD / 2 + 1.2)],
        [v(bx, bRoof.ridgeY + 0.5, bz - BD / 2 - 1.2), v(bx, bRoof.ridgeY + 0.5, bz + BD / 2 + 1.2)],
      ]),
      eave: hlFromSegs(M, roof.eaveSegs),
      roof: roofWash,
      roofMats: [roofF, roofB, roofW1, roofW2, roofX1, roofX2, roofN1, roofN2, roofG1, roofG2],
      damage: null,
    },
  };
}
