Architectural Drawings – e-drafts.com

I need some help with these roofs, they won’t connect right no matter how hard I try to get them right even with an ai model.

The link to my page is: Architectural Drawings – e-drafts.com

I need the CSG for the walls to be set up so the walls have clean corners.

Also, I want to add a roof attic area and perfect the roof’s layouts, make the ceiling joists turned the right way for each layout, cover the roof’s top face with sheathing, and then prep for the final roof, but this is getting very complicated.. and my roof is kinda awkward looking..

Looking for professional help from someone with more experience.

Especially with roofs, and making walls..

If you play this code you will see the issue, which is that the walls do not have clean corners. Each wall should be treated as an individual wall, but appear to be extruded all as one wall, like on a floorplan, so they don’t overlap, or.. for polygonal, I’m thinking what we want to do is join the walls, after they’ve been cut out, using CSG, retaining each wall’s attributes and subtraction, so it keeps what joins and removes what doesn’t…

Also, the roofs need the rafters to run all the way through each ridge, connecting to the valley and hip rafters. If you will notice some of the rafters aren’t cut to the right length or shape, or interpreting the correct ridge height, in tandem with the pitch.

I tried to use AI to push it along as far as I could, and I’ve updated the code a little, now we have multi-story buildings, and I added a brick veneer selection and cleaned up the UI.

However, the brick veneer is crashing as well.

Thanks to anyone who fixes this.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Architectural CSG Wall Engine - Framing & Layouts</title>
<link href="https://fonts.googleapis.com/css2?family=Cabin+Sketch:wght@700&display=swap" rel="stylesheet">
<style>
    body { margin:0; background:#222; display:flex; flex-direction:column; align-items:center; min-height:100vh; font-family:sans-serif; overflow-x:hidden; }
    .controls { margin-top:10px; color:white; display:flex; flex-wrap:wrap; gap:8px; align-items:center; background:#111; padding:10px 15px; border-radius:6px; z-index: 1000; font-size: 12px; max-width: 98vw; justify-content: center; }
    .controls select, .controls input { background:#333; color:white; border:1px solid #555; padding:4px; border-radius:4px; font-size:11px; outline:none; }
    .control-group { display: flex; gap: 6px; align-items: center; border-left: 2px solid #444; padding-left: 10px; }
    .control-group:first-child { border-left: none; padding-left: 0; }
    
    .btn-action { background: #3b5998; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 11px; transition: 0.2s; }
    .btn-action:hover { background: #4b6aab; }
    .btn-danger { background: #dc3545; }
    .btn-danger:hover { background: #c82333; }
    .btn-edit { background: #ffc107; color: #000; }
    .btn-edit:hover { background: #e0a800; }

    .svg-container { width: 95vw; max-width: 1200px; aspect-ratio: 36 / 24; background:white; position: relative; box-shadow: 0 0 20px rgba(0,0,0,0.5); margin-top: 10px; overflow: hidden; }
    .sheet-page { display:none; width:100%; height:100%; position:relative; }
    .sheet-page.active { display:block; }
    
    .vp-single { width: 100%; height: 100%; position: absolute; inset: 0; z-index: 10; }
    
    .label-layer { position: absolute; inset: 0; pointer-events: none; z-index: 20; }
    .sheet-border { position: absolute; inset: 0.5in; border: 2px solid #000; pointer-events: none; z-index: 50; }
    
    .title-block { position: absolute; bottom: 0.58in; right: 0.58in; width: 300px; border: 2px solid #000; background: white; z-index: 100; font-family: Arial, sans-serif;}
    .title-row { display: flex; border-bottom: 1px solid #000; }
    .title-row:last-child { border-bottom: none; }
    .title-cell { padding: 4px; font-size: 9px; flex: 1; border-right: 1px solid #000; color:#000; }
    .title-cell:last-child { border-right: none; }
    .cell-bold { font-weight: bold; font-size: 12px; margin-top: 1px; text-transform: uppercase; }
    .text-welcome-brand { font-family: 'Cabin Sketch', cursive; font-size: 22px; text-align: center; background: #000; color: #fff; padding: 4px; border-bottom: 2px solid #000; }
    
    .view-tag { position: absolute; left: 0.8in; bottom: 0.8in; display: flex; align-items: center; z-index: 20; color: #000; font-family: sans-serif; pointer-events: none; }
    .vt-circle { width: 34px; height: 34px; border: 2px solid #000; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 16px; background: white; z-index: 2; margin-right: -2px; }
    .vt-lines { display: flex; flex-direction: column; justify-content: center; }
    .vt-name { font-size: 15px; font-weight: bold; letter-spacing: 1px; padding-left: 10px; margin-bottom: 2px; text-transform: uppercase; }
    .vt-line { height: 2px; background: #000; width: 220px; }
    .vt-scale { font-size: 11px; padding-left: 10px; margin-top: 3px; text-transform: uppercase; font-weight: bold; }

    .side-nav-btn { position: fixed; top: 50%; transform: translateY(-50%); background: #111; color: white; width: 45px; height: 80px; cursor: pointer; border-radius: 6px; z-index: 999; border: 1px solid #555; font-size: 20px; transition: all 0.2s;}
    .side-nav-left { left: 15px; } .side-nav-right { right: 15px; }
    .side-nav-btn:hover { background: #fff; color: #000; border-color: #fff; }
    .wall-label { position: absolute; transform: translate(-50%, -50%); background: #000; color: #fff; font-size: 11px; font-weight: bold; padding: 2px 6px; border-radius: 10px; border: 1px solid #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.5); font-family: sans-serif;}

    .modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.8); z-index: 2000; justify-content: center; align-items: center; }
    .modal-content { background: #222; padding: 20px; border-radius: 8px; border: 1px solid #555; width: 720px; color: white; display: flex; flex-direction: column; gap: 10px; font-family: sans-serif;}
    .modal-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #444; padding-bottom: 10px; margin-bottom: 5px;}
    .modal-header h3 { margin: 0; font-size: 18px; }
    
    .tabs { display: flex; gap: 8px; border-bottom: 2px solid #444; padding-bottom: 10px; flex-wrap: wrap; }
    .tab-btn { background: #333; color: #aaa; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-weight: bold; transition: 0.2s; font-size: 12px; }
    .tab-btn:hover { background: #444; color: #fff;}
    .tab-btn.active { background: #3b5998; color: white; }
    .tab-content { display: none; }
    .tab-content.active { display: block; }

    .modal-form { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; background: #111; padding: 15px; border-radius: 6px; }
    .modal-form label { display: flex; justify-content: space-between; align-items: center; font-size: 12px; }
    .modal-form input[type="number"], .modal-form select { width: 110px; }
    .modal-form input[type="color"] { width: 30px; height: 24px; padding: 0; cursor: pointer; }
    .modal-form button { grid-column: span 2; padding: 8px; background: #28a745; color: white; border: none; cursor: pointer; border-radius: 4px; margin-top: 5px; font-weight: bold; }
    .modal-form button:hover { background: #34ce57; }
    .dynamic-list { max-height: 140px; overflow-y: auto; background: #111; padding: 10px; border-radius: 6px; display: flex; flex-direction: column; gap: 5px; margin-top: 5px; }
    .dynamic-item { display: flex; justify-content: space-between; align-items: center; background: #333; padding: 6px 10px; border-radius: 4px; font-size: 11px; }
    .dynamic-actions { display: flex; gap: 5px; }
    .close-btn { background: #555; padding: 10px; border: none; color: white; cursor: pointer; border-radius: 4px; margin-top: 10px;}

    /* Modern Toggle Switch for Brick Veneer */
    .switch { position: relative; display: inline-block; width: 36px; height: 20px; margin: 0; }
    .switch input { opacity: 0; width: 0; height: 0; }
    .slider { position: absolute; cursor: pointer; inset: 0; background-color: #555; transition: .3s; border-radius: 20px; }
    .slider:before { position: absolute; content: ""; height: 14px; width: 14px; left: 3px; bottom: 3px; background-color: white; transition: .3s; border-radius: 50%; }
    .switch input:checked + .slider { background-color: #28a745; }
    .switch input:checked + .slider:before { transform: translateX(16px); }
</style>
</head>
<body>

<button class="side-nav-btn side-nav-left" onclick="window.changePage(-1)">&#10094;</button>
<button class="side-nav-btn side-nav-right" onclick="window.changePage(1)">&#10095;</button>

<div class="controls">
  <div class="control-group">
    Shape: <select id="bldgShape" onchange="window.updateShapeUI()">
        <option value="rectangular">Rectangular</option>
        <option value="l_shape">L Shape</option>
        <option value="u_shape">U Shape</option>
        <option value="h_shape">H Shape</option>
        <option value="left_wing">Left Wing</option>
        <option value="right_wing">Right Wing</option>
        <option value="dual_wing">Dual Wing</option>
        <option value="polygon_reg">Polygonal (Regular)</option>
        <option value="polygon_irreg">Polygonal (Irregular)</option>
    </select>
  </div>
  <div class="control-group poly-ui" style="display:none; background:#222; padding: 4px; border-radius: 4px;">
    Poly: 
    Rad <input type="number" id="polyRadius" value="240" style="width:40px" onchange="window.updateModel()">
    Sides <input type="number" id="polySides" value="6" style="width:35px" onchange="window.updateModel()">
    Ridge Sc. <input type="number" id="polyRidgeScale" value="20" style="width:35px" onchange="window.updateModel()">%
    <button id="btnIrreg" class="btn-action btn-edit" style="display:none; margin-left: 5px;" onclick="window.openIrregModal()">Customize Pts</button>
  </div>
  <div class="control-group">
    Size: 
    <input type="number" id="bldgW" value="480" step="12" style="width:45px" onchange="window.updateModel()"> x 
    <input type="number" id="bldgL" value="360" step="12" style="width:45px" onchange="window.updateModel()">
  </div>
  
  <div class="control-group" style="padding-left: 10px; gap: 8px;">
    <button class="btn-action" style="background: #6c757d;" onclick="window.openFoundModal()">Foundation Settings</button>
    <button class="btn-action" style="background: #17a2b8;" onclick="window.openWallModal()">Wall Settings</button>
    <button class="btn-action" style="background: #fd7e14;" onclick="window.openFloorModal()">Floor Settings</button>
    <button class="btn-action" style="background: #28a745;" onclick="window.openRoofModal()">Roof Settings</button>
    <button class="btn-action" onclick="window.openCustomizeModal()">Advanced Editor</button>
  </div>
</div>

<div id="modalFound" class="modal-overlay">
  <div class="modal-content" style="width: 380px;">
      <div class="modal-header"><h3>Foundation Settings</h3></div>
      <div class="modal-form" style="grid-template-columns: 1fr; gap: 12px;">
          <label>FF to Grade (in): <input type="number" id="ffToGrade" value="24" style="width:60px" onchange="window.updateModel()"></label>
          <label>Found Wall W (in): <input type="number" id="foundW" value="10" style="width:60px" onchange="window.updateModel()"></label>
          <label>Footing W (in): <input type="number" id="footW" value="16" style="width:60px" onchange="window.updateModel()"></label>
          <label><span style="display:flex; align-items:center; gap:4px;"><input type="checkbox" id="showPlanes" onchange="window.updateModel()"> Show Planes</span></label>
          <button class="close-btn" onclick="window.closeFoundModal()">Apply & Rebuild Model</button>
      </div>
  </div>
</div>

<div id="modalWall" class="modal-overlay">
  <div class="modal-content" style="width: 380px;">
      <div class="modal-header"><h3>Wall & Story Settings</h3></div>
      <div class="modal-form" style="grid-template-columns: 1fr; gap: 12px;">
          <label>Number of Stories: <input type="number" id="numStories" value="1" min="1" max="10" onchange="window.updateFloorUI(); window.updateModel();"></label>
          <label>Wall Height (in): <input type="number" id="wallH" value="120" style="width:60px" onchange="window.updateModel()"></label>
          <label>Wall Thickness (in): <input type="number" id="wallT" value="8" style="width:60px" onchange="window.updateModel()"></label>
          
          <div style="display:flex; flex-direction:column; gap:8px; background:#1a1a1a; padding:10px; border-radius:6px; border:1px solid #333; grid-column: 1 / -1;">
              <label style="display:flex; justify-content:space-between; align-items:center; cursor:pointer; margin:0;">
                  <strong>Brick Veneer</strong>
                  <label class="switch">
                      <input type="checkbox" id="brickVeneerToggle" onchange="document.getElementById('veneerSettings').style.display = this.checked ? 'flex' : 'none'; window.updateModel && window.updateModel();">
                      <span class="slider"></span>
                  </label>
              </label>
              <div id="veneerSettings" style="display: none; flex-direction: column; gap: 10px; margin-top: 8px; padding-top: 10px; border-top: 1px dashed #444;">
                  <label>Veneer Width (in): <input type="number" id="veneerW" value="3.25" step="0.25" style="width:60px" onchange="window.updateModel && window.updateModel()"></label>
                  <label>Gap (in): <input type="number" id="veneerGap" value="1" step="0.25" style="width:60px" onchange="window.updateModel && window.updateModel()"></label>
              </div>
          </div>

          <button class="close-btn" onclick="window.closeWallModal()">Apply & Rebuild Model</button>
      </div>
  </div>
</div>

<div id="modalFloor" class="modal-overlay">
  <div class="modal-content" style="width: 380px;">
      <div class="modal-header"><h3>Floor Settings</h3></div>
      <div class="modal-form" style="grid-template-columns: 1fr; gap: 12px;">
          <label>Subfloor Thickness (in): <input type="number" id="sfThk" value="1.5" step="0.25" style="width:60px" onchange="window.updateModel()"></label>
          <div id="floorJoistContainer" style="display:flex; flex-direction:column; gap:12px; margin-top:10px;">
              </div>
          <button class="close-btn" onclick="window.closeFloorModal()">Apply & Rebuild Model</button>
      </div>
  </div>
</div>

<div id="modalRoof" class="modal-overlay">
  <div class="modal-content" style="width: 380px;">
      <div class="modal-header"><h3>Roof & Joist Settings</h3></div>
      <div class="modal-form" style="grid-template-columns: 1fr; gap: 12px;">
          <label>Roof Type: 
              <div style="display:flex; gap:10px;">
                  <label style="display:flex; align-items:center; gap:2px;"><input type="radio" name="roofType" value="hip" checked onchange="window.updateRoofUI()"> Hip</label>
                  <label style="display:flex; align-items:center; gap:2px;"><input type="radio" name="roofType" value="gable" onchange="window.updateRoofUI()"> Gable</label>
                  <label style="display:flex; align-items:center; gap:2px;"><input type="radio" name="roofType" value="flat" onchange="window.updateRoofUI()"> Flat</label>
              </div>
          </label>
          <label>Ceil Joist Size:
              <select id="cjSize" onchange="window.updateModel()">
                  <option value="9.25">2x10 (9.25")</option>
                  <option value="11.25" selected>2x12 (11.25")</option>
                  <option value="13.25">14" I-Joist (13.25")</option>
                  <option value="15.25">16" I-Joist (15.25")</option>
              </select>
          </label>
          <label>Overhang (in): <input type="number" id="roofOverhang" value="24" onchange="window.updateModel()"></label>
          
          <div id="roofPitchUI" style="display:flex; justify-content: space-between; align-items: center; width: 100%;">
              <label>Pitch (/12): <input type="number" id="roofRise" value="6" min="0" style="width:50px" onchange="window.updateModel()"></label>
              <label>Ridge Hgt: <input type="text" id="ridgeHgt" readonly value="-" style="width:60px; background:#222; border:1px solid #444; color:#888; text-align:center; cursor:not-allowed;" title="Height above top of wall"></label>
          </div>
          
          <div id="roofThickUI" style="display:flex; justify-content: space-between; align-items: center; width: 100%;">
              <label><span id="lblRidgeThick">Ridge Thk</span> <input type="number" id="roofRidgeThick" value="1.5" step="0.5" style="width:50px" onchange="window.updateModel()"></label>
              <label><span id="lblFlatThick" style="display:none;">Flat Perim</span> <input type="number" id="roofFlatThick" value="1.5" step="0.5" style="width:50px; display:none;" onchange="window.updateModel()"></label>
          </div>
          <button class="close-btn" onclick="window.closeRoofModal()">Apply & Rebuild Model</button>
      </div>
  </div>
</div>

<div id="modalIrreg" class="modal-overlay">
  <div class="modal-content" style="width:400px;">
      <div class="modal-header"><h3>Irregular Polygon Points</h3></div>
      <div class="modal-form">
          <label>X: <input type="number" id="irregX" value="0"></label>
          <label>Z: <input type="number" id="irregZ" value="0"></label>
          <button onclick="window.addIrregPt()">+ Add Point</button>
      </div>
      <div id="irregList" class="dynamic-list"></div>
      <button class="close-btn" onclick="window.closeIrregModal()">Apply & Rebuild</button>
  </div>
</div>

<div id="modalCustomize" class="modal-overlay">
  <div class="modal-content">
    <div class="modal-header"><h3>Building Customizer</h3></div>
    <div class="tabs">
        <button class="tab-btn active" onclick="window.switchTab('tab-openings', event)">Doors & Windows</button>
        <button class="tab-btn" onclick="window.switchTab('tab-hvac', event)">HVAC / Fireplace</button>
        <button class="tab-btn" onclick="window.switchTab('tab-interior', event)">Interior Walls (Polylines)</button>
        <button class="tab-btn" onclick="window.switchTab('tab-electrical', event)">Electrical & Batteries</button>
        <button class="tab-btn" onclick="window.switchTab('tab-columns', event)">Columns</button>
    </div>

    <div id="tab-openings" class="tab-content active">
        <div class="modal-form">
          <label>Category: <select id="opCat" onchange="window.toggleOpUI()"><option value="door">Door</option><option value="window">Window</option></select></label>
          <label>Type: <select id="opType"></select></label>
          <label>Parent Wall: <select id="opWall"></select></label>
          <label>Pos (0-100%): <input type="number" id="opPos" value="50" step="1"></label>
          <label>Size (W x H): <div style="display:flex; gap:4px"><input type="number" id="opW" value="36" style="width:45px"> x <input type="number" id="opH" value="80" style="width:45px"></div></label>
          <label>Finish/Wood: <select id="opFinish"><option value="pine">Pine</option><option value="oak">Oak</option><option value="mahogany">Mahogany</option><option value="cedar">Cedar</option><option value="cherry">Cherry</option></select></label>
          <label><span style="display:flex; align-items:center; gap:5px;"><input type="checkbox" id="opIsMetal"> Metal Material?</span></label>
          <label>Paint Color: <input type="color" id="opColor" value="#ffffff"></label>
          <label id="sillWrapper" style="display:none;">Sill Hgt: <input type="number" id="opSill" value="36"></label>
          <label id="deadboltWrapper" style="display:flex;"><span style="display:flex; align-items:center; gap:5px;"><input type="checkbox" id="opDeadbolt"> Int. Deadbolt?</span></label>
          <button onclick="window.addOpening()">+ Save Feature</button>
        </div>
        <div id="openingList" class="dynamic-list"></div>
    </div>
    <div id="tab-hvac" class="tab-content">
        <div class="modal-form">
          <label>Type: <select id="hvacType">
              <option value="fireplace">Fireplace (Interior)</option>
              <option value="ac">AC Unit (Exterior)</option>
              <option value="central">Central HVAC (Interior)</option>
              <option value="thermostat">Thermostat (Interior)</option>
              <option value="duct">Ducts (Interior Floor)</option>
          </select></label>
          <label>Parent Wall: <select id="hvacWall"></select></label>
          <label>Pos (0-100%): <input type="number" id="hvacPos" value="50" step="1"></label>
          <label>Width: <input type="number" id="hvacW" value="60"></label>
          <label>Depth: <input type="number" id="hvacD" value="24"></label>
          <label>Height: <input type="number" id="hvacH" value="60"></label>
          <button onclick="window.addHVAC()">+ Save Unit</button>
        </div>
        <div id="hvacList" class="dynamic-list"></div>
    </div>
    <div id="tab-interior" class="tab-content">
        <div class="modal-form">
          <label>Anchor Wall: <select id="iwWall"></select></label>
          <label>Pos (0-100%): <input type="number" id="iwPos" value="50"></label>
          <label>Length (in): <input type="number" id="iwLen" value="120"></label>
          <label>Angle (deg): <input type="number" id="iwAngle" value="0"></label>
          <label>Thickness: <input type="number" id="iwThick" value="4.5" step="0.5"></label>
          <label>Snap To Wall: <select id="iwSnap"><option value="none">None</option></select></label>
          <label>Custom Color: <input type="color" id="iwColor" value="#dddddd"></label>
          <label><span style="display:flex; align-items:center; gap:5px;"><input type="checkbox" id="iwCopyUp"> Copy to upper floors?</span></label>
          <button onclick="window.addIntWall()">+ Add Wall Segment</button>
        </div>
        <div id="intWallList" class="dynamic-list"></div>
    </div>
    <div id="tab-electrical" class="tab-content">
        <div class="modal-form">
          <label>Type: <select id="elType" onchange="window.toggleElUI()">
              <option value="outlet">Outlet</option>
              <option value="switch">Switch</option>
              <option value="light">Wall Light</option>
              <option value="battery_wall">Battery Pack (Wall)</option>
              <option value="battery_grade">Battery Pack (Grade)</option>
          </select></label>
          <label>Parent Wall: <select id="elWall"></select></label>
          <label>Pos (0-100%): <input type="number" id="elPos" value="50"></label>
          <label class="el-dim" style="display:none;">Length (W): <input type="number" id="elW" value="36"></label>
          <label class="el-dim" style="display:none;">Height: <input type="number" id="elH" value="48"></label>
          <label class="el-dim" style="display:none;">Depth (W): <input type="number" id="elD" value="12"></label>
          <button onclick="window.addElectrical()">+ Save Electrical</button>
        </div>
        <div id="electricalList" class="dynamic-list"></div>
    </div>
    <div id="tab-columns" class="tab-content">
        <div class="modal-form">
          <label>Profile Shape: <select id="colShape"><option value="circular">Circular (Lathe)</option><option value="tapered">Tapered (Lathe)</option><option value="square">Square</option><option value="hss_round">HSS Round</option><option value="hss_sq">HSS Square</option><option value="hss_rect">HSS Rectangular</option><option value="wf">Wide Flange (I Shape)</option></select></label>
          <label>Plate Shapes: <select id="colPlateShape"><option value="square">Square</option><option value="rect">Rectangular</option><option value="circular">Circular</option><option value="poly">Polygonal</option></select></label>
          <label>Floor: <input type="number" id="colFloor" value="1" min="1"></label>
          <label>Top Plate Thk: <input type="number" id="colTopThk" value="2"></label>
          <label>Array Count: <input type="number" id="colCount" value="1"></label>
          <label>Start X | Z: <div style="display:flex; gap:4px"><input type="number" id="colX" value="0" style="width:45px"> <input type="number" id="colZ" value="0" style="width:45px"></div></label>
          <label>Space X | Z: <div style="display:flex; gap:4px"><input type="number" id="colSpacingX" value="120" style="width:45px"> <input type="number" id="colSpacingZ" value="0" style="width:45px"></div></label>
          <button onclick="window.addColumn()">+ Save Columns</button>
        </div>
        <div id="columnList" class="dynamic-list"></div>
    </div>
    <button class="close-btn" onclick="window.closeCustomizeModal()">Close & Rebuild Model</button>
  </div>
</div>

<div class="svg-container">
  <div id="sheet1" class="sheet-page active">
    <div class="vp-single" id="vp1"></div><div class="label-layer" id="lbl1"></div><div class="sheet-border"></div>
    <div class="view-tag"><div class="vt-circle">1</div><div class="vt-lines"><div class="vt-name">FLOOR PLAN</div><div class="vt-line"></div><div class="vt-scale">SCALE: 1/4" = 1'-0"</div></div></div>
    <div class="title-block">
        <div class="text-welcome-brand">e-drafts.com</div>
        <div class="title-row"><div class="title-cell">TITLE<div class="cell-bold">CUSTOM BUILDING</div></div></div>
        <div class="title-row"><div class="title-cell">PROJECT<div class="cell-bold">ARCHITECTURAL PLAN</div></div></div>
        <div class="title-row"><div class="title-cell">VIEW<div class="cell-bold">FLOOR PLAN</div></div></div>
        <div class="title-row"><div class="title-cell">SCALE<div class="cell-bold scale-label">1/4" = 1'-0"</div></div><div class="title-cell">SHEET<div class="cell-bold">1 OF 8</div></div></div>
    </div>
  </div>
  
  <div id="sheet2" class="sheet-page">
    <div class="vp-single" id="vp2"></div><div class="sheet-border"></div>
    <div class="view-tag"><div class="vt-circle">1</div><div class="vt-lines"><div class="vt-name">NORTH ELEVATION</div><div class="vt-line"></div><div class="vt-scale">SCALE: 1/4" = 1'-0"</div></div></div>
    <div class="title-block">
        <div class="text-welcome-brand">e-drafts.com</div>
        <div class="title-row"><div class="title-cell">TITLE<div class="cell-bold">CUSTOM BUILDING</div></div></div>
        <div class="title-row"><div class="title-cell">PROJECT<div class="cell-bold">ARCHITECTURAL PLAN</div></div></div>
        <div class="title-row"><div class="title-cell">VIEW<div class="cell-bold">NORTH ELEVATION</div></div></div>
        <div class="title-row"><div class="title-cell">SCALE<div class="cell-bold scale-label">1/4" = 1'-0"</div></div><div class="title-cell">SHEET<div class="cell-bold">2 OF 8</div></div></div>
    </div>
  </div>
  
  <div id="sheet3" class="sheet-page">
    <div class="vp-single" id="vp3"></div><div class="sheet-border"></div>
    <div class="view-tag"><div class="vt-circle">1</div><div class="vt-lines"><div class="vt-name">EAST ELEVATION</div><div class="vt-line"></div><div class="vt-scale">SCALE: 1/4" = 1'-0"</div></div></div>
    <div class="title-block">
        <div class="text-welcome-brand">e-drafts.com</div>
        <div class="title-row"><div class="title-cell">TITLE<div class="cell-bold">CUSTOM BUILDING</div></div></div>
        <div class="title-row"><div class="title-cell">PROJECT<div class="cell-bold">ARCHITECTURAL PLAN</div></div></div>
        <div class="title-row"><div class="title-cell">VIEW<div class="cell-bold">EAST ELEVATION</div></div></div>
        <div class="title-row"><div class="title-cell">SCALE<div class="cell-bold scale-label">1/4" = 1'-0"</div></div><div class="title-cell">SHEET<div class="cell-bold">3 OF 8</div></div></div>
    </div>
  </div>
  
  <div id="sheet4" class="sheet-page">
    <div class="vp-single" id="vp4"></div><div class="sheet-border"></div>
    <div class="view-tag"><div class="vt-circle">1</div><div class="vt-lines"><div class="vt-name">SOUTH ELEVATION</div><div class="vt-line"></div><div class="vt-scale">SCALE: 1/4" = 1'-0"</div></div></div>
    <div class="title-block">
        <div class="text-welcome-brand">e-drafts.com</div>
        <div class="title-row"><div class="title-cell">TITLE<div class="cell-bold">CUSTOM BUILDING</div></div></div>
        <div class="title-row"><div class="title-cell">PROJECT<div class="cell-bold">ARCHITECTURAL PLAN</div></div></div>
        <div class="title-row"><div class="title-cell">VIEW<div class="cell-bold">SOUTH ELEVATION</div></div></div>
        <div class="title-row"><div class="title-cell">SCALE<div class="cell-bold scale-label">1/4" = 1'-0"</div></div><div class="title-cell">SHEET<div class="cell-bold">4 OF 8</div></div></div>
    </div>
  </div>

  <div id="sheet5" class="sheet-page">
    <div class="vp-single" id="vp5"></div><div class="sheet-border"></div>
    <div class="view-tag"><div class="vt-circle">1</div><div class="vt-lines"><div class="vt-name">WEST ELEVATION</div><div class="vt-line"></div><div class="vt-scale">SCALE: 1/4" = 1'-0"</div></div></div>
    <div class="title-block">
        <div class="text-welcome-brand">e-drafts.com</div>
        <div class="title-row"><div class="title-cell">TITLE<div class="cell-bold">CUSTOM BUILDING</div></div></div>
        <div class="title-row"><div class="title-cell">PROJECT<div class="cell-bold">ARCHITECTURAL PLAN</div></div></div>
        <div class="title-row"><div class="title-cell">VIEW<div class="cell-bold">WEST ELEVATION</div></div></div>
        <div class="title-row"><div class="title-cell">SCALE<div class="cell-bold scale-label">1/4" = 1'-0"</div></div><div class="title-cell">SHEET<div class="cell-bold">5 OF 8</div></div></div>
    </div>
  </div>

  <div id="sheet6" class="sheet-page">
    <div class="vp-single" id="vp6"></div><div class="sheet-border"></div>
    <div class="view-tag"><div class="vt-circle">1</div><div class="vt-lines"><div class="vt-name">3D ISOMETRIC</div><div class="vt-line"></div><div class="vt-scale">SCALE: NTS</div></div></div>
    <div class="title-block">
        <div class="text-welcome-brand">e-drafts.com</div>
        <div class="title-row"><div class="title-cell">TITLE<div class="cell-bold">CUSTOM BUILDING</div></div></div>
        <div class="title-row"><div class="title-cell">PROJECT<div class="cell-bold">ARCHITECTURAL PLAN</div></div></div>
        <div class="title-row"><div class="title-cell">VIEW<div class="cell-bold">3D ISOMETRIC</div></div></div>
        <div class="title-row"><div class="title-cell">SCALE<div class="cell-bold scale-label">NTS</div></div><div class="title-cell">SHEET<div class="cell-bold">6 OF 8</div></div></div>
    </div>
  </div>

  <div id="sheet7" class="sheet-page">
    <div class="vp-single" id="vp7"></div><div class="sheet-border"></div>
    <div class="view-tag"><div class="vt-circle">1</div><div class="vt-lines"><div class="vt-name">FRAMING PLAN</div><div class="vt-line"></div><div class="vt-scale">SCALE: 1/4" = 1'-0"</div></div></div>
    <div class="title-block">
        <div class="text-welcome-brand">e-drafts.com</div>
        <div class="title-row"><div class="title-cell">TITLE<div class="cell-bold">CUSTOM BUILDING</div></div></div>
        <div class="title-row"><div class="title-cell">PROJECT<div class="cell-bold">ARCHITECTURAL PLAN</div></div></div>
        <div class="title-row"><div class="title-cell">VIEW<div class="cell-bold">FRAMING PLAN</div></div></div>
        <div class="title-row"><div class="title-cell">SCALE<div class="cell-bold scale-label">1/4" = 1'-0"</div></div><div class="title-cell">SHEET<div class="cell-bold">7 OF 8</div></div></div>
    </div>
  </div>

  <div id="sheet8" class="sheet-page">
    <div class="vp-single" id="vp8"></div><div class="label-layer" id="lbl8"></div><div class="sheet-border"></div>
    <div class="view-tag"><div class="vt-circle">1</div><div class="vt-lines"><div class="vt-name">ROOF PLAN</div><div class="vt-line"></div><div class="vt-scale">SCALE: 1/4" = 1'-0"</div></div></div>
    <div class="title-block">
        <div class="text-welcome-brand">e-drafts.com</div>
        <div class="title-row"><div class="title-cell">TITLE<div class="cell-bold">CUSTOM BUILDING</div></div></div>
        <div class="title-row"><div class="title-cell">PROJECT<div class="cell-bold">ARCHITECTURAL PLAN</div></div></div>
        <div class="title-row"><div class="title-cell">VIEW<div class="cell-bold">ROOF PLAN</div></div></div>
        <div class="title-row"><div class="title-cell">SCALE<div class="cell-bold scale-label">1/4" = 1'-0"</div></div><div class="title-cell">SHEET<div class="cell-bold">8 OF 8</div></div></div>
    </div>
  </div>
</div>

Attached script:

<script type="importmap">
{
    "imports": {
        "three": "https://cdn.jsdelivr.net/npm/three@0.128/build/three.module.js",
        "csg": "https://cdn.jsdelivr.net/gh/manthrax/THREE-CSGMesh/three-csg.js"
    }
}
</script>

<script type="module">
import * as THREE from "three";
import CSG from "csg";

const viewList = ['1', '2', '3', '4', '5', '6', '7', '8'];
let scenes = {}, cameras = {}, renderers = {};
let currentPage = 1, maxPages = 8, activeWallCount = 4, activeWallCenters = [];
let extraGeoms = [], extraMats = [];

// DATA STORES
let featuresData = [
    { id: 'F1', cat: 'door', type: 'Standard', wallId: '1.1', w: 36, h: 80, pos: 50, sill: 0, finish: 'pine', isMetal: false, color: '#ffffff', deadbolt: true },
    { id: 'F2', cat: 'window', type: 'Single Hung', wallId: '1.2', w: 48, h: 48, pos: 50, sill: 36, finish: 'pine', isMetal: false, color: '#ffffff', deadbolt: false }
];
let hvacData = [{ id: 'H1', type: 'fireplace', wallId: '1.3', w: 60, d: 24, h: 60, pos: 50 }];
let interiorWallsData = [{ id: 'A1.1', anchorId: '1.4', pos: 50, length: 144, thick: 4.5, snapId: 'none', color: '#dddddd', angle: 0, copyUp: false }];
let electricalData = [];
let columnsData = [];

window.polyIrregPts = [
    {x: -150, y: -150}, {x: 150, y: -150}, {x: 250, y: 50}, {x: 0, y: 200}, {x: -200, y: 50}
];
window.allWallsContext = [];

window.renumberAll = () => {
    featuresData.forEach((f, i) => f.id = 'F' + (i+1));
    hvacData.forEach((h, i) => h.id = 'H' + (i+1));
    electricalData.forEach((e, i) => e.id = 'E' + (i+1));
    columnsData.forEach((c, i) => c.id = 'C' + (i+1));

    let floorMap = {};
    let iwNewIdMap = {};
    let floorCounters = {};

    let unresolved = [...interiorWallsData];
    let loops = 0;
    while(unresolved.length > 0 && loops < 1000) {
        loops++;
        let iw = unresolved.shift();
        let anchorFloor = null;
        let oldAnchor = iw.anchorId;

        let matchExt = oldAnchor.match(/^(\d+)\./);
        if (matchExt) {
            anchorFloor = parseInt(matchExt[1]);
        } else if (oldAnchor.includes('_F')) {
            anchorFloor = parseInt(oldAnchor.split('_F')[1]);
        } else {
            let matchInt = oldAnchor.match(/^A(\d+)\./);
            if (matchInt) {
                anchorFloor = parseInt(matchInt[1]);
            } else {
                for (let oldId in iwNewIdMap) {
                    if (iwNewIdMap[oldId] === oldAnchor || oldId === oldAnchor) {
                        anchorFloor = floorMap[oldId];
                        break;
                    }
                }
            }
        }

        if (anchorFloor !== null) {
            floorMap[iw.id] = anchorFloor;
            if (!floorCounters[anchorFloor]) floorCounters[anchorFloor] = 1;
            let newId = 'A' + anchorFloor + '.' + (floorCounters[anchorFloor]++);
            iwNewIdMap[iw.id] = newId;
            iw.id = newId;
        } else {
            unresolved.push(iw);
        }
    }

    interiorWallsData.forEach(iw => {
        if (iwNewIdMap[iw.anchorId]) iw.anchorId = iwNewIdMap[iw.anchorId];
        if (iw.snapId && iw.snapId !== 'none' && iwNewIdMap[iw.snapId]) iw.snapId = iwNewIdMap[iw.snapId];
    });

    [featuresData, hvacData, electricalData].forEach(arr => {
        arr.forEach(item => {
            if (iwNewIdMap[item.wallId]) item.wallId = iwNewIdMap[item.wallId];
        });
    });
};

// UI LOGIC
window.updateShapeUI = () => {
    let shape = document.getElementById('bldgShape').value;
    let polyUI = document.querySelector('.poly-ui');
    let btnIrreg = document.getElementById('btnIrreg');
    if (shape.startsWith('polygon')) {
        polyUI.style.display = 'flex';
        btnIrreg.style.display = shape === 'polygon_irreg' ? 'block' : 'none';
    } else {
        polyUI.style.display = 'none';
    }
    window.updateModel();
};

window.updateRoofUI = () => {
    let type = document.querySelector('input[name="roofType"]:checked').value;
    let pitchUI = document.getElementById('roofPitchUI');
    
    if (type === 'flat') {
        pitchUI.style.display = 'none';
        document.getElementById('lblRidgeThick').style.display = 'none';
        document.getElementById('roofRidgeThick').style.display = 'none';
        document.getElementById('lblFlatThick').style.display = 'inline';
        document.getElementById('roofFlatThick').style.display = 'inline';
    } else {
        pitchUI.style.display = 'flex';
        document.getElementById('lblRidgeThick').style.display = 'inline';
        document.getElementById('roofRidgeThick').style.display = 'inline';
        document.getElementById('lblFlatThick').style.display = 'none';
        document.getElementById('roofFlatThick').style.display = 'none';
    }
    window.updateModel();
};

window.updateFloorUI = () => {
    let ns = parseInt(document.getElementById('numStories').value) || 1;
    let container = document.getElementById('floorJoistContainer');
    let existingVals = [];
    for(let i=1; i<=10; i++) {
        let el = document.getElementById('fjSize' + i);
        if(el) existingVals[i] = el.value;
    }
    container.innerHTML = '';
    for(let i=1; i<=ns; i++) {
        let val = existingVals[i] || "11.25";
        container.innerHTML += `<label style="display:flex; justify-content:space-between; align-items:center;">Story ${i} Joist Size:
            <select id="fjSize${i}" onchange="window.updateModel()" style="width:140px;">
                <option value="9.25" ${val==="9.25"?"selected":""}>2x10 (9.25")</option>
                <option value="11.25" ${val==="11.25"?"selected":""}>2x12 (11.25")</option>
                <option value="13.25" ${val==="13.25"?"selected":""}>14" I-Joist (13.25")</option>
                <option value="15.25" ${val==="15.25"?"selected":""}>16" I-Joist (15.25")</option>
            </select>
        </label>`;
    }
};

window.switchTab = (tabId, e) => {
    document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
    document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active'));
    document.getElementById(tabId).classList.add('active');
    e.currentTarget.classList.add('active');
};
window.toggleOpUI = () => {
    const cat = document.getElementById('opCat').value;
    const typeSel = document.getElementById('opType');
    typeSel.innerHTML = '';
    
    if(cat === 'door') {
        ['Standard', 'Standard w/ Panes', 'Sliding', 'Bifold'].forEach(t => typeSel.add(new Option(t, t)));
        document.getElementById('sillWrapper').style.display = 'none';
        document.getElementById('deadboltWrapper').style.display = 'flex';
        document.getElementById('opSill').value = 0;
    } else {
        ['Single Hung', 'Double Hung', 'Sliding', 'Casement'].forEach(t => typeSel.add(new Option(t, t)));
        document.getElementById('sillWrapper').style.display = 'flex';
        document.getElementById('deadboltWrapper').style.display = 'none';
    }
};
window.toggleElUI = () => {
    const type = document.getElementById('elType').value;
    const dims = document.querySelectorAll('.el-dim');
    if (type === 'battery_wall' || type === 'battery_grade') {
        dims.forEach(el => el.style.display = 'flex');
    } else {
        dims.forEach(el => el.style.display = 'none');
    }
};

window.populateWallSelects = () => {
    const selects = ['opWall', 'iwWall', 'elWall', 'hvacWall', 'iwSnap'];
    let opts = window.allWallsContext ? window.allWallsContext.map(w => w.fullId) : [];
    
    selects.forEach(id => {
        const el = document.getElementById(id);
        if(!el) return;
        const cur = el.value; el.innerHTML = '';
        if(id === 'iwSnap') el.add(new Option('None', 'none'));
        opts.forEach(o => el.add(new Option('Wall ' + o, o)));
        if(opts.includes(cur)) el.value = cur;
    });
};

window.openIrregModal = () => { document.getElementById('modalIrreg').style.display = 'flex'; window.renderIrregList(); };
window.closeIrregModal = () => { document.getElementById('modalIrreg').style.display = 'none'; window.updateModel(); };
window.openFoundModal = () => document.getElementById('modalFound').style.display = 'flex';
window.closeFoundModal = () => { document.getElementById('modalFound').style.display = 'none'; window.updateModel(); };
window.openWallModal = () => document.getElementById('modalWall').style.display = 'flex';
window.closeWallModal = () => { document.getElementById('modalWall').style.display = 'none'; window.updateModel(); };
window.openFloorModal = () => document.getElementById('modalFloor').style.display = 'flex';
window.closeFloorModal = () => { document.getElementById('modalFloor').style.display = 'none'; window.updateModel(); };
window.openRoofModal = () => { document.getElementById('modalRoof').style.display = 'flex'; };
window.closeRoofModal = () => { document.getElementById('modalRoof').style.display = 'none'; window.updateModel(); };

window.addIrregPt = () => {
    let x = parseFloat(document.getElementById('irregX').value)||0;
    let z = parseFloat(document.getElementById('irregZ').value)||0;
    window.polyIrregPts.push({x, y: z});
    window.renderIrregList();
};
window.removeIrregPt = (i) => {
    if(window.polyIrregPts.length <= 3) return alert("Minimum 3 points required.");
    window.polyIrregPts.splice(i, 1);
    window.renderIrregList();
};
window.renderIrregList = () => {
    let list = document.getElementById('irregList');
    list.innerHTML = '';
    window.polyIrregPts.forEach((p, i) => {
        list.innerHTML += `<div class="dynamic-item"><span>Pt ${i+1}: X:${p.x}, Z:${p.y}</span><button class="btn-action btn-danger" onclick="window.removeIrregPt(${i})">X</button></div>`;
    });
};

window.openCustomizeModal = () => { window.populateWallSelects(); window.toggleOpUI(); window.toggleElUI(); document.getElementById('modalCustomize').style.display = 'flex'; renderAllLists(); };
window.closeCustomizeModal = () => { document.getElementById('modalCustomize').style.display = 'none'; window.updateModel(); };
window.removeFeature = (i) => { featuresData.splice(i,1); window.renumberAll(); renderAllLists(); };
window.removeHVAC = (i) => { hvacData.splice(i,1); window.renumberAll(); renderAllLists(); };
window.removeIntWall = (i) => { interiorWallsData.splice(i,1); window.renumberAll(); window.populateWallSelects(); renderAllLists(); };
window.removeElectrical = (i) => { electricalData.splice(i,1); window.renumberAll(); renderAllLists(); };
window.removeColumn = (i) => { columnsData.splice(i,1); window.renumberAll(); renderAllLists(); };

window.editFeature = (i) => {
    let f = featuresData[i];
    document.getElementById('opCat').value = f.cat; window.toggleOpUI();
    document.getElementById('opType').value = f.type; document.getElementById('opWall').value = f.wallId;
    document.getElementById('opPos').value = f.pos; document.getElementById('opW').value = f.w;
    document.getElementById('opH').value = f.h; document.getElementById('opFinish').value = f.finish;
    document.getElementById('opIsMetal').checked = f.isMetal; document.getElementById('opColor').value = f.color;
    if(f.cat === 'window') document.getElementById('opSill').value = f.sill; else document.getElementById('opDeadbolt').checked = f.deadbolt;
    window.removeFeature(i);
};
window.editHVAC = (i) => {
    let h = hvacData[i];
    document.getElementById('hvacType').value = h.type; document.getElementById('hvacWall').value = h.wallId;
    document.getElementById('hvacPos').value = h.pos; document.getElementById('hvacW').value = h.w;
    document.getElementById('hvacD').value = h.d || 24; document.getElementById('hvacH').value = h.h || 60;
    window.removeHVAC(i);
};
window.editIntWall = (i) => {
    let iw = interiorWallsData[i];
    document.getElementById('iwWall').value = iw.anchorId; 
    document.getElementById('iwPos').value = iw.pos;
    document.getElementById('iwLen').value = iw.length; 
    document.getElementById('iwThick').value = iw.thick;
    document.getElementById('iwSnap').value = iw.snapId || 'none';
    document.getElementById('iwColor').value = iw.color || '#dddddd';
    document.getElementById('iwAngle').value = iw.angle || 0;
    document.getElementById('iwCopyUp').checked = iw.copyUp || false;
    window.removeIntWall(i);
};
window.editElectrical = (i) => {
    let e = electricalData[i];
    document.getElementById('elType').value = e.type; window.toggleElUI(); 
    document.getElementById('elWall').value = e.wallId;
    document.getElementById('elPos').value = e.pos;
    if (e.w) document.getElementById('elW').value = e.w;
    if (e.h) document.getElementById('elH').value = e.h;
    if (e.d) document.getElementById('elD').value = e.d;
    window.removeElectrical(i);
};
window.editColumn = (i) => {
    let c = columnsData[i];
    document.getElementById('colShape').value = c.shape; document.getElementById('colPlateShape').value = c.plateShape;
    document.getElementById('colFloor').value = c.floor || 1;
    document.getElementById('colTopThk').value = c.topThick; document.getElementById('colCount').value = c.count;
    document.getElementById('colX').value = c.startX; document.getElementById('colZ').value = c.startZ;
    document.getElementById('colSpacingX').value = c.spacingX; document.getElementById('colSpacingZ').value = c.spacingZ;
    window.removeColumn(i);
};

function renderAllLists() {
    let opL = document.getElementById('openingList'); opL.innerHTML = '';
    featuresData.forEach((f, i) => { opL.innerHTML += `<div class="dynamic-item"><span><strong>${f.wallId}</strong> | ${f.type} ${f.cat}: ${f.w}"x${f.h}" @ ${f.pos}%</span><div class="dynamic-actions"><button class="btn-action btn-edit" onclick="window.editFeature(${i})">Edit</button><button class="btn-action btn-danger" onclick="window.removeFeature(${i})">X</button></div></div>`; });
    let hvL = document.getElementById('hvacList'); hvL.innerHTML = '';
    hvacData.forEach((f, i) => { hvL.innerHTML += `<div class="dynamic-item"><span><strong>${f.wallId}</strong> | ${f.type} @ ${f.pos}%</span><div class="dynamic-actions"><button class="btn-action btn-edit" onclick="window.editHVAC(${i})">Edit</button><button class="btn-action btn-danger" onclick="window.removeHVAC(${i})">X</button></div></div>`; });
    let iwL = document.getElementById('intWallList'); iwL.innerHTML = '';
    
    interiorWallsData.forEach((f, i) => { 
        let snapTxt = (f.snapId && f.snapId !== 'none') ? ` (Snaps to ${f.snapId})` : '';
        let details = f.isSnapSegment ? `[AUTO-SNAP]` : `L:${f.length}" @ ${f.angle}°`;
        let copyTxt = f.copyUp ? ` [Copied Up]` : '';
        iwL.innerHTML += `<div class="dynamic-item"><span><strong>${f.id}</strong> on ${f.anchorId} | ${details} T:${f.thick}"${snapTxt}${copyTxt}</span><div class="dynamic-actions"><button class="btn-action btn-edit" onclick="window.editIntWall(${i})">Edit</button><button class="btn-action btn-danger" onclick="window.removeIntWall(${i})">X</button></div></div>`; 
    });
    
    let elL = document.getElementById('electricalList'); elL.innerHTML = '';
    electricalData.forEach((f, i) => { 
        let dims = (f.type.startsWith('battery')) ? ` (${f.w}x${f.h}x${f.d})` : '';
        elL.innerHTML += `<div class="dynamic-item"><span><strong>${f.wallId}</strong> | ${f.type}${dims} @ ${f.pos}%</span><div class="dynamic-actions"><button class="btn-action btn-edit" onclick="window.editElectrical(${i})">Edit</button><button class="btn-action btn-danger" onclick="window.removeElectrical(${i})">X</button></div></div>`; 
    });
    let colL = document.getElementById('columnList'); colL.innerHTML = '';
    columnsData.forEach((c, i) => { colL.innerHTML += `<div class="dynamic-item"><span><strong>${c.id}</strong> | ${c.shape} | Flr: ${c.floor||1} | Count: ${c.count} @ X:${c.startX}, Z:${c.startZ}</span><div class="dynamic-actions"><button class="btn-action btn-edit" onclick="window.editColumn(${i})">Edit</button><button class="btn-action btn-danger" onclick="window.removeColumn(${i})">X</button></div></div>`; });
}

window.addOpening = () => { featuresData.push({ id: 'TEMP', cat: document.getElementById('opCat').value, type: document.getElementById('opType').value, wallId: document.getElementById('opWall').value, w: parseFloat(document.getElementById('opW').value)||36, h: parseFloat(document.getElementById('opH').value)||80, pos: parseFloat(document.getElementById('opPos').value)||50, sill: document.getElementById('opCat').value === 'window' ? (parseFloat(document.getElementById('opSill').value)||36) : 0, finish: document.getElementById('opFinish').value, isMetal: document.getElementById('opIsMetal').checked, color: document.getElementById('opColor').value, deadbolt: document.getElementById('opDeadbolt').checked }); window.renumberAll(); renderAllLists(); };
window.addHVAC = () => { hvacData.push({ id: 'TEMP', type: document.getElementById('hvacType').value, wallId: document.getElementById('hvacWall').value, pos: parseFloat(document.getElementById('hvacPos').value)||50, w: parseFloat(document.getElementById('hvacW').value)||60, d: parseFloat(document.getElementById('hvacD').value)||24, h: parseFloat(document.getElementById('hvacH').value)||60 }); window.renumberAll(); renderAllLists(); };

window.addIntWall = () => { 
    let snapId = document.getElementById('iwSnap').value;
    let tempId1 = 'TEMP_' + Math.random();

    interiorWallsData.push({ 
        id: tempId1, 
        anchorId: document.getElementById('iwWall').value, 
        pos: parseFloat(document.getElementById('iwPos').value)||50, 
        length: parseFloat(document.getElementById('iwLen').value)||120, 
        thick: parseFloat(document.getElementById('iwThick').value)||4.5, 
        snapId: 'none',
        color: document.getElementById('iwColor').value,
        angle: parseFloat(document.getElementById('iwAngle').value)||0,
        copyUp: document.getElementById('iwCopyUp').checked
    });

    if (snapId !== 'none') {
        interiorWallsData.push({
            id: 'TEMP_' + Math.random(),
            anchorId: tempId1,
            pos: 100,
            length: 0,
            thick: parseFloat(document.getElementById('iwThick').value)||4.5,
            snapId: snapId,
            isSnapSegment: true,
            color: document.getElementById('iwColor').value,
            angle: 0,
            copyUp: document.getElementById('iwCopyUp').checked
        });
    }

    window.renumberAll(); 
    window.populateWallSelects(); 
    renderAllLists(); 
};

window.addElectrical = () => { 
    let t = document.getElementById('elType').value;
    let eObj = { id: 'TEMP', type: t, wallId: document.getElementById('elWall').value, pos: parseFloat(document.getElementById('elPos').value)||50 };
    if (t === 'battery_wall' || t === 'battery_grade') {
        eObj.w = parseFloat(document.getElementById('elW').value)||36;
        eObj.h = parseFloat(document.getElementById('elH').value)||48;
        eObj.d = parseFloat(document.getElementById('elD').value)||12;
    }
    electricalData.push(eObj); 
    window.renumberAll(); 
    renderAllLists(); 
};
window.addColumn = () => { columnsData.push({ id: 'TEMP', shape: document.getElementById('colShape').value, plateShape: document.getElementById('colPlateShape').value, floor: parseInt(document.getElementById('colFloor').value)||1, topThick: parseFloat(document.getElementById('colTopThk').value)||2, count: parseInt(document.getElementById('colCount').value)||1, startX: parseFloat(document.getElementById('colX').value)||0, startZ: parseFloat(document.getElementById('colZ').value)||0, spacingX: parseFloat(document.getElementById('colSpacingX').value)||120, spacingZ: parseFloat(document.getElementById('colSpacingZ').value)||0 }); window.renumberAll(); renderAllLists(); }

// MATERIALS DICT
const MATS = {
    glass: new THREE.MeshPhysicalMaterial({ color: 0x88ccff, transparent: true, opacity: 0.4, roughness: 0.1, metalness: 0.1, side: THREE.DoubleSide }),
    metal: new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.4, metalness: 0.8, side: THREE.DoubleSide }),
    knob: new THREE.MeshStandardMaterial({ color: 0xd4af37, roughness: 0.2, metalness: 0.9, side: THREE.DoubleSide }),
    concrete: new THREE.MeshStandardMaterial({ color: 0x777777, roughness: 0.9, side: THREE.DoubleSide }),
    wall: new THREE.MeshStandardMaterial({ color: 0xdddddd, roughness: 0.8, side: THREE.DoubleSide }),
    wood: new THREE.MeshStandardMaterial({ color: 0xb58957, roughness: 0.9, side: THREE.DoubleSide }),
    subfloor: new THREE.MeshStandardMaterial({ color: 0xd2b48c, roughness: 1.0, side: THREE.DoubleSide }),
    brick: new THREE.MeshStandardMaterial({ color: 0x9c4a36, roughness: 0.9, side: THREE.DoubleSide })
};

function getFrameMat(op) {
    if(op.isMetal) return MATS.metal;
    let baseHex = parseInt(op.color.replace('#',''), 16);
    let m = new THREE.MeshStandardMaterial({ color: baseHex, roughness: 0.7, side: THREE.DoubleSide });
    extraMats.push(m); return m;
}

// MILLWORK BUILDERS
function buildCasementTrim(w, h, thick) {
    const grp = new THREE.Group();
    const trimW = 3; const trimD = 0.75;
    const left = new THREE.Mesh(new THREE.BoxGeometry(trimW, h + trimW, trimD), MATS.wall); left.position.set(-w/2 - trimW/2, trimW/2, thick/2 + trimD/2);
    const right = new THREE.Mesh(new THREE.BoxGeometry(trimW, h + trimW, trimD), MATS.wall); right.position.set(w/2 + trimW/2, trimW/2, thick/2 + trimD/2);
    const top = new THREE.Mesh(new THREE.BoxGeometry(w + trimW*2, trimW, trimD), MATS.wall); top.position.set(0, h/2 + trimW/2, thick/2 + trimD/2);
    grp.add(left, right, top);
    let intGrp = grp.clone(); intGrp.position.z = -thick - trimD; grp.add(intGrp);
    return grp;
}

function buildStandardDoor(w, h, op, isExterior) {
    const grp = new THREE.Group(); const fMat = getFrameMat(op);
    const sW = 5.5, sT = 1.5;
    const leftStile = new THREE.Mesh(new THREE.BoxGeometry(sW, h, sT), fMat); leftStile.position.set(-w/2 + sW/2, 0, 0);
    
    let rCSG = CSG.fromMesh(new THREE.Mesh(new THREE.BoxGeometry(sW, h, sT)), 0);
    const knobHole = new THREE.Mesh(new THREE.CylinderGeometry(1.2, 1.2, sT*2, 16)); knobHole.rotation.x = Math.PI/2; knobHole.position.set(0, -h/2 + 36, 0); 
    rCSG = rCSG.subtract(CSG.fromMesh(knobHole, 1));
    if(isExterior || op.deadbolt) {
        const dbHole = new THREE.Mesh(new THREE.CylinderGeometry(1, 1, sT*2, 16)); dbHole.rotation.x = Math.PI/2; dbHole.position.set(0, -h/2 + 42, 0); 
        rCSG = rCSG.subtract(CSG.fromMesh(dbHole, 2));
    }
    const rightStile = CSG.toMesh(rCSG, new THREE.Matrix4(), fMat); rightStile.position.set(w/2 - sW/2, 0, 0);
    
    const rW = w - (sW * 2);
    const topRail = new THREE.Mesh(new THREE.BoxGeometry(rW, sW, sT), fMat); topRail.position.set(0, h/2 - sW/2, 0);
    const botRail = new THREE.Mesh(new THREE.BoxGeometry(rW, sW, sT), fMat); botRail.position.set(0, -h/2 + sW/2, 0);
    const midRail = new THREE.Mesh(new THREE.BoxGeometry(rW, 7.5, sT), fMat); midRail.position.set(0, 0, 0);

    const knob = new THREE.Mesh(new THREE.SphereGeometry(1.3, 16, 16), MATS.knob); knob.position.set(w/2 - sW/2, -h/2 + 36, sT);
    const knob2 = knob.clone(); knob2.position.z = -sT;
    
    const hg = new THREE.BoxGeometry(0.2, 4, 1.6);
    for(let y of [-h/2 + 10, h/2 - 10]) { let hng = new THREE.Mesh(hg, MATS.metal); hng.position.set(-w/2 + 0.1, y, 0); grp.add(hng); }
    grp.add(leftStile, rightStile, topRail, botRail, midRail, knob, knob2);
    
    const panH = (h - (sW*2) - 7.5)/2;
    if(op.type.includes('Panes')) {
        const pane = new THREE.Mesh(new THREE.BoxGeometry(rW - 2, panH - 2, 0.25), MATS.glass); pane.position.set(0, h/2 - sW - panH/2, 0); grp.add(pane);
    }
    return grp;
}

function buildSliding(w, h, op) {
    const grp = new THREE.Group(); const fMat = getFrameMat(op);
    const pW = w/2 + 1, pD = 2; 
    for(let i=0; i<2; i++) {
        let pGrp = new THREE.Group();
        let frame = new THREE.Mesh(new THREE.BoxGeometry(pW, h, pD), fMat);
        let hole = new THREE.Mesh(new THREE.BoxGeometry(pW - 2, h - 2, pD*2));
        let fMesh = CSG.toMesh(CSG.fromMesh(frame, 0).subtract(CSG.fromMesh(hole, 1)), new THREE.Matrix4(), fMat);
        let glass = new THREE.Mesh(new THREE.BoxGeometry(pW-2, h-2, 0.25), MATS.glass); pGrp.add(fMesh, glass);
        let handle = new THREE.Mesh(new THREE.BoxGeometry(1, 8, 0.5), MATS.metal); handle.position.set(i===0 ? -pW/2 + 2 : pW/2 - 2, 0, pD/2 + 0.25); pGrp.add(handle);
        pGrp.position.set(i===0 ? -w/4 : w/4, 0, i===0 ? -1 : 1); grp.add(pGrp);
    }
    return grp;
}

function buildWindowSashes(w, h, op) {
    const grp = new THREE.Group(); const fMat = getFrameMat(op); const sT = 1.5;
    if(op.type === 'Sliding') return buildSliding(w, h, op);
    const sH = h/2 + 0.5;
    for(let i=0; i<2; i++) {
        let frame = new THREE.Mesh(new THREE.BoxGeometry(w, sH, sT), fMat);
        let hole = new THREE.Mesh(new THREE.BoxGeometry(w - sT*2, sH - sT*2, sT*2));
        let fMesh = CSG.toMesh(CSG.fromMesh(frame, 0).subtract(CSG.fromMesh(hole, 1)), new THREE.Matrix4(), fMat);
        let glass = new THREE.Mesh(new THREE.BoxGeometry(w - sT*2, sH - sT*2, 0.25), MATS.glass); fMesh.add(glass);
        fMesh.position.set(0, i===0 ? -h/4 : h/4, i===0 ? -0.5 : 0.5); grp.add(fMesh);
    }
    return grp;
}

function generateShapePoints(shapeType, W, L) {
    let w2 = W/2, l2 = L/2, tw = W * 0.35, tl = L * 0.35; let pts = [];
    switch(shapeType) {
        case 'rectangular': pts = [new THREE.Vector2(-w2, -l2), new THREE.Vector2(w2, -l2), new THREE.Vector2(w2, l2), new THREE.Vector2(-w2, l2)]; break;
        case 'l_shape': pts = [new THREE.Vector2(-w2, -l2), new THREE.Vector2(w2, -l2), new THREE.Vector2(w2, -l2+tl), new THREE.Vector2(-w2+tw, -l2+tl), new THREE.Vector2(-w2+tw, l2), new THREE.Vector2(-w2, l2)]; break;
        case 'u_shape': pts = [new THREE.Vector2(-w2, -l2), new THREE.Vector2(w2, -l2), new THREE.Vector2(w2, l2), new THREE.Vector2(w2-tw, l2), new THREE.Vector2(w2-tw, -l2+tl), new THREE.Vector2(-w2+tw, -l2+tl), new THREE.Vector2(-w2+tw, l2), new THREE.Vector2(-w2, l2)]; break;
        case 'h_shape': pts = [new THREE.Vector2(-w2, -l2), new THREE.Vector2(-w2+tw, -l2), new THREE.Vector2(-w2+tw, -tl/2), new THREE.Vector2(w2-tw, -tl/2), new THREE.Vector2(w2-tw, -l2), new THREE.Vector2(w2, -l2), new THREE.Vector2(w2, l2), new THREE.Vector2(w2-tw, l2), new THREE.Vector2(w2-tw, tl/2), new THREE.Vector2(-w2+tw, tl/2), new THREE.Vector2(-w2+tw, l2), new THREE.Vector2(-w2, l2)]; break;
        case 'right_wing': pts = [ new THREE.Vector2(-W*0.5, -L*0.5), new THREE.Vector2(0, -L*0.5), new THREE.Vector2(0, -L*0.15), new THREE.Vector2(W*0.2, -L*0.15), new THREE.Vector2(W*0.2, -L*0.35), new THREE.Vector2(W*0.5, -L*0.35), new THREE.Vector2(W*0.5, L*0.35), new THREE.Vector2(W*0.2, L*0.35), new THREE.Vector2(W*0.2, L*0.15), new THREE.Vector2(0, L*0.15), new THREE.Vector2(0, L*0.5), new THREE.Vector2(-W*0.5, L*0.5) ]; break;
        case 'left_wing': pts = [ new THREE.Vector2(0, -L*0.5), new THREE.Vector2(W*0.5, -L*0.5), new THREE.Vector2(W*0.5, L*0.5), new THREE.Vector2(0, L*0.5), new THREE.Vector2(0, L*0.15), new THREE.Vector2(-W*0.2, L*0.15), new THREE.Vector2(-W*0.2, L*0.35), new THREE.Vector2(-W*0.5, L*0.35), new THREE.Vector2(-W*0.5, -L*0.35), new THREE.Vector2(-W*0.2, -L*0.35), new THREE.Vector2(-W*0.2, -L*0.15), new THREE.Vector2(0, -L*0.15) ]; break;
        case 'dual_wing': pts = [ new THREE.Vector2(-W*0.2, -L*0.5), new THREE.Vector2(W*0.2, -L*0.5), new THREE.Vector2(W*0.2, -L*0.15), new THREE.Vector2(W*0.35, -L*0.15), new THREE.Vector2(W*0.35, -L*0.35), new THREE.Vector2(W*0.5, -L*0.35), new THREE.Vector2(W*0.5, L*0.35), new THREE.Vector2(W*0.35, L*0.35), new THREE.Vector2(W*0.35, L*0.15), new THREE.Vector2(W*0.2, L*0.15), new THREE.Vector2(W*0.2, L*0.5), new THREE.Vector2(-W*0.2, L*0.5), new THREE.Vector2(-W*0.2, L*0.15), new THREE.Vector2(-W*0.35, L*0.15), new THREE.Vector2(-W*0.35, L*0.35), new THREE.Vector2(-W*0.5, L*0.35), new THREE.Vector2(-W*0.5, -L*0.35), new THREE.Vector2(-W*0.35, -L*0.35), new THREE.Vector2(-W*0.35, -L*0.15), new THREE.Vector2(-W*0.2, -L*0.15) ]; break;
        case 'polygon_reg':
            let sides = parseInt(document.getElementById('polySides').value) || 6;
            let radius = parseFloat(document.getElementById('polyRadius').value) || 240;
            for(let i=0; i<sides; i++) {
                let ang = (i / sides) * Math.PI * 2;
                pts.push(new THREE.Vector2(Math.cos(ang)*radius, Math.sin(ang)*radius));
            }
            break;
        case 'polygon_irreg':
            pts = window.polyIrregPts.map(p => new THREE.Vector2(p.x, p.y));
            let area = 0;
            for(let i=0; i<pts.length; i++) {
                let p1 = pts[i], p2 = pts[(i+1)%pts.length];
                area += (p2.x - p1.x) * (p2.y + p1.y);
            }
            if(area > 0) pts.reverse(); 
            break;
    }
    return pts;
}

function getFloorRectangles(shape, W, L, wT) {
    let rects = []; let w2 = W/2, l2 = L/2, tw = W*0.35, tl = L*0.35, wT2 = wT/2;
    const addR = (x1, x2, z1, z2) => { let rw = Math.abs(x2 - x1), rl = Math.abs(z2 - z1), cx = (x1 + x2) / 2, cz = (z1 + z2) / 2; rects.push({cx, cz, rw, rl}); };
    if(shape.startsWith('polygon')) {
        let pts = generateShapePoints(shape, W, L);
        let minX = Infinity, maxX = -Infinity, minZ = Infinity, maxZ = -Infinity;
        pts.forEach(p => { minX = Math.min(minX, p.x); maxX = Math.max(maxX, p.x); minZ = Math.min(minZ, p.y); maxZ = Math.max(maxZ, p.y); });
        addR(minX-wT2, maxX+wT2, minZ-wT2, maxZ+wT2);
        return rects;
    }
    if(shape === 'rectangular') { addR(-w2-wT2, w2+wT2, -l2-wT2, l2+wT2); } 
    else if(shape === 'l_shape') { addR(-w2-wT2, w2+wT2, -l2-wT2, -l2+tl+wT2); addR(-w2-wT2, -w2+tw+wT2, -l2+tl+wT2, l2+wT2); } 
    else if(shape === 'u_shape') { addR(-w2-wT2, -w2+tw+wT2, -l2-wT2, l2+wT2); addR(w2-tw-wT2, w2+wT2, -l2-wT2, l2+wT2); addR(-w2+tw+wT2, w2-tw-wT2, -l2-wT2, -l2+tl+wT2); } 
    else if(shape === 'h_shape') { addR(-w2-wT2, -w2+tw+wT2, -l2-wT2, l2+wT2); addR(w2-tw-wT2, w2+wT2, -l2-wT2, l2+wT2); addR(-w2+tw+wT2, w2-tw-wT2, -tl/2-wT2, tl/2+wT2); } 
    else if(shape === 'right_wing') { addR(-W*0.5-wT2, 0+wT2, -L*0.5-wT2, L*0.5+wT2); addR(W*0.2-wT2, W*0.5+wT2, -L*0.35-wT2, L*0.35+wT2); addR(0+wT2, W*0.2-wT2, -L*0.15-wT2, L*0.15+wT2); } 
    else if(shape === 'left_wing') { addR(0-wT2, W*0.5+wT2, -L*0.5-wT2, L*0.5+wT2); addR(-W*0.5-wT2, -W*0.2+wT2, -L*0.35-wT2, L*0.35+wT2); addR(-W*0.2+wT2, 0-wT2, -L*0.15-wT2, L*0.15+wT2); } 
    else if(shape === 'dual_wing') { addR(-W*0.2-wT2, W*0.2+wT2, -L*0.5-wT2, L*0.5+wT2); addR(W*0.35-wT2, W*0.5+wT2, -L*0.35-wT2, L*0.35+wT2); addR(-W*0.5-wT2, -W*0.35+wT2, -L*0.35-wT2, L*0.35+wT2); addR(W*0.2+wT2, W*0.35-wT2, -L*0.15-wT2, L*0.15+wT2); addR(-W*0.35+wT2, -W*0.2-wT2, -L*0.15-wT2, L*0.15+wT2); }
    return rects;
}

function buildRoofPlan(shape, W, L, O, py, ry, type, pThick, rThick, cjSize) {
    let group = new THREE.Group();
    
    const addThickLine = (x1, z1, y1, x2, z2, y2, thk, depth, mat) => {
        let dx = x2 - x1, dy = y2 - y1, dz = z2 - z1;
        let len = Math.sqrt(dx*dx + dy*dy + dz*dz);
        if (len < 0.001) return;
        let cx = (x1 + x2)/2, cy = (y1 + y2)/2, cz = (z1 + z2)/2;
        let mesh = new THREE.Mesh(new THREE.BoxGeometry(thk, depth, len), mat);
        mesh.position.set(cx, cy, cz);
        mesh.lookAt(new THREE.Vector3(x2, y2, z2));
        group.add(mesh);
    };

    let basePts = generateShapePoints(shape, W, L);
    let edges = [];
    for(let i=0; i<basePts.length; i++) {
        let p1 = basePts[i], p2 = basePts[(i+1)%basePts.length];
        let dx = p2.x - p1.x, dy = p2.y - p1.y, len = Math.sqrt(dx*dx + dy*dy);
        let nx = dy/len, ny = -dx/len; 
        edges.push({ nx, ny });
    }
    
    let oPts = [];
    for(let i=0; i<basePts.length; i++) {
        let ePrev = edges[(i-1+edges.length)%edges.length];
        let eCurr = edges[i];
        let n1x = ePrev.nx, n1y = ePrev.ny;
        let n2x = eCurr.nx, n2y = eCurr.ny;
        let nx = n1x + n2x, ny = n1y + n2y;
        let nLen = Math.sqrt(nx*nx + ny*ny);
        if(nLen < 0.001) {
            oPts.push(new THREE.Vector2(basePts[i].x + n1x*O, basePts[i].y + n1y*O));
        } else {
            nx /= nLen; ny /= nLen;
            let dot = n1x * nx + n1y * ny;
            let dist = dot !== 0 ? O / dot : O;
            oPts.push(new THREE.Vector2(basePts[i].x + nx*dist, basePts[i].y + ny*dist));
        }
    }

    if (type === 'flat') {
        let minX = Math.min(...oPts.map(p=>p.x)) - 10;
        let maxX = Math.max(...oPts.map(p=>p.x)) + 10;
        let minZ = Math.min(...oPts.map(p=>p.y)) - 10;
        let maxZ = Math.max(...oPts.map(p=>p.y)) + 10;

        let roofShape = new THREE.Shape();
        oPts.forEach((p, i) => { if(i===0) roofShape.moveTo(p.x, p.y); else roofShape.lineTo(p.x, p.y); });
        let roofExtGeo = new THREE.ExtrudeGeometry(roofShape, { depth: cjSize, bevelEnabled: false });
        roofExtGeo.rotateX(Math.PI/2);
        let roofMesh = new THREE.Mesh(roofExtGeo);
        roofMesh.position.y = py + cjSize/2;
        roofMesh.updateMatrix();
        let roofCSG = CSG.fromMesh(roofMesh, 0);

        let jGeo = new THREE.BoxGeometry(2, cjSize, maxZ - minZ + 20);
        let jCSG = null;
        for(let x = minX; x <= maxX; x += 16) {
            let jm = new THREE.Mesh(jGeo);
            jm.position.set(x, py, (minZ+maxZ)/2);
            jm.updateMatrix();
            let c = CSG.fromMesh(jm, 1);
            jCSG = jCSG ? jCSG.union(c) : c;
        }
        if (jCSG) {
            try {
                let finalJoists = CSG.toMesh(jCSG.intersect(roofCSG), new THREE.Matrix4(), MATS.wood);
                finalJoists.userData.layer = 'roof';
                group.add(finalJoists);
            } catch(e) { console.warn('CSG Joist Error:', e); }
        }

        for(let i=0; i<oPts.length; i++) {
            let p1 = oPts[i], p2 = oPts[(i+1)%oPts.length];
            addThickLine(p1.x, p1.y, py, p2.x, p2.y, py, pThick, cjSize, MATS.wood);
        }
        
        group.userData.layer = 'roof';
        return group;
    }

    let w2 = W/2, l2 = L/2, tw = W*0.35, tl = L*0.35;
    let ridges = [], rafters = [];
    
    if (shape.startsWith('polygon')) {
        let cx = 0, cz = 0;
        basePts.forEach(p => { cx += p.x; cz += p.y; });
        cx /= basePts.length; cz /= basePts.length;

        let scale = parseFloat(document.getElementById('polyRidgeScale').value) / 100 || 0.2;
        let rPts = basePts.map(p => ({ x: cx + (p.x - cx)*scale, z: cz + (p.y - cz)*scale }));

        for(let i=0; i<oPts.length; i++) {
            let next = (i+1)%oPts.length;
            ridges.push([rPts[i], rPts[next]]);
            rafters.push([{x: oPts[i].x, z: oPts[i].y}, rPts[i]]);
        }
    }
    else if (shape === 'rectangular') {
        let rx1 = -w2-O + (l2+O), rx2 = w2+O - (l2+O);
        if(rx1 > rx2) rx1 = rx2 = 0; 
        
        if (type === 'gable') { rx1 = -w2-O; rx2 = w2+O; }
        
        let rP1 = {x: rx1, z: 0}, rP2 = {x: rx2, z: 0};
        ridges.push([rP1, rP2]);
        
        rafters.push([{x: -w2-O, z: -l2-O}, rP1], [{x: -w2-O, z: l2+O}, rP1]);
        rafters.push([{x: w2+O, z: -l2-O}, rP2], [{x: w2+O, z: l2+O}, rP2]);
    }
    else if (shape === 'l_shape') {
        let splitX = -w2+tw+O, splitZ = -l2+tl+O;
        let cxCorner = -w2 + tw/2, czCorner = -l2 + tl/2;
        let rxEnd = w2+O - (tl/2+O), rzEnd = l2+O - (tw/2+O);
        
        if (type === 'gable') { rxEnd = w2+O; rzEnd = l2+O; }
        
        let pCorner = {x: cxCorner, z: czCorner}, pTop = {x: cxCorner, z: rzEnd}, pRight = {x: rxEnd, z: czCorner};
        ridges.push([pCorner, pTop], [pCorner, pRight]);
        
        rafters.push([{x: -w2-O, z: l2+O}, pTop], [{x: splitX, z: l2+O}, pTop]);
        rafters.push([{x: w2+O, z: -l2-O}, pRight], [{x: w2+O, z: splitZ}, pRight]);
        rafters.push([{x: -w2-O, z: -l2-O}, pCorner], [{x: splitX, z: splitZ}, pCorner]);
    }
    else if (shape === 'u_shape') {
        let sL = -w2+tw+O, sR = w2-tw-O, sZ = -l2+tl+O;
        let cxL = -w2 + tw/2, cxR = w2 - tw/2, czCorner = -l2 + tl/2, rzEnd = l2+O - (tw/2+O);
        
        if (type === 'gable') rzEnd = l2+O;
        
        let pCL = {x: cxL, z: czCorner}, pCR = {x: cxR, z: czCorner};
        let pTL = {x: cxL, z: rzEnd}, pTR = {x: cxR, z: rzEnd};
        ridges.push([pCL, pTL], [pCL, pCR], [pCR, pTR]);
        
        rafters.push([{x: -w2-O, z: l2+O}, pTL], [{x: sL, z: l2+O}, pTL]);
        rafters.push([{x: w2+O, z: l2+O}, pTR], [{x: sR, z: l2+O}, pTR]);
        rafters.push([{x: -w2-O, z: -l2-O}, pCL], [{x: sL, z: sZ}, pCL]);
        rafters.push([{x: w2+O, z: -l2-O}, pCR], [{x: sR, z: sZ}, pCR]);
    }
    else if (shape === 'h_shape') {
        let sL = -w2+tw+O, sR = w2-tw-O, sT = -tl/2-O, sB = tl/2+O;
        let cxL = -w2 + tw/2, cxR = w2 - tw/2, rzT = -l2+O + (tw/2+O), rzB = l2+O - (tw/2+O);
        
        if (type === 'gable') { rzT = -l2-O; rzB = l2+O; }
        
        let pTL = {x: cxL, z: rzT}, pBL = {x: cxL, z: rzB}, pTR = {x: cxR, z: rzT}, pBR = {x: cxR, z: rzB};
        let pML = {x: cxL, z: 0}, pMR = {x: cxR, z: 0};
        ridges.push([pTL, pBL], [pTR, pBR], [pML, pMR]);
        
        rafters.push([{x: -w2-O, z: -l2-O}, pTL], [{x: sL, z: -l2-O}, pTL]);
        rafters.push([{x: -w2-O, z: l2+O}, pBL], [{x: sL, z: l2+O}, pBL]);
        rafters.push([{x: w2+O, z: -l2-O}, pTR], [{x: sR, z: -l2-O}, pTR]);
        rafters.push([{x: w2+O, z: l2+O}, pBR], [{x: sR, z: l2+O}, pBR]);
        rafters.push([{x: sL, z: sT}, pML], [{x: sL, z: sB}, pML]);
        rafters.push([{x: sR, z: sT}, pMR], [{x: sR, z: sB}, pMR]);
    }
    else if (shape === 'right_wing') {
        let cR = W*0.2;
        let sL = O, sR = cR - O, sT = -L*0.15 - O, sB = L*0.15 + O;
        let cxM = -w2/4, rzMT = -l2 + W/4, rzMB = l2 - W/4;
        let cxW = cR + (w2-cR)/2, rzWT = -L*0.35 + (w2-cR)/2, rzWB = L*0.35 - (w2-cR)/2;
        
        if (type === 'gable') {
            rzMT = -l2-O; rzMB = l2+O;
            rzWT = -L*0.35-O; rzWB = L*0.35+O;
        }

        let pMT = {x: cxM, z: rzMT}, pMB = {x: cxM, z: rzMB};
        let pWT = {x: cxW, z: rzWT}, pWB = {x: cxW, z: rzWB};
        let pMC = {x: cxM, z: 0}, pWC = {x: cxW, z: 0};
        ridges.push([pMT, pMB], [pWT, pWB], [pMC, pWC]);
        
        rafters.push([{x: -w2-O, z: -l2-O}, pMT], [{x: sL, z: -l2-O}, pMT]);
        rafters.push([{x: -w2-O, z: l2+O}, pMB], [{x: sL, z: l2+O}, pMB]);
        rafters.push([{x: sR, z: -L*0.35-O}, pWT], [{x: w2+O, z: -L*0.35-O}, pWT]);
        rafters.push([{x: sR, z: L*0.35+O}, pWB], [{x: w2+O, z: L*0.35+O}, pWB]);
        rafters.push([{x: sL, z: sT}, pMC], [{x: sL, z: sB}, pMC]);
        rafters.push([{x: sR, z: sT}, pWC], [{x: sR, z: sB}, pWC]);
    }
    else if (shape === 'left_wing') {
        let cL = -W*0.2;
        let sL = cL + O, sR = -O, sT = -L*0.15 - O, sB = L*0.15 + O;
        let cxM = w2/4, rzMT = -l2 + W/4, rzMB = l2 - W/4;
        let cxW = (-w2 + cL)/2, rzWT = -L*0.35 + (w2+cL)/2, rzWB = L*0.35 - (w2+cL)/2;
        
        if (type === 'gable') {
            rzMT = -l2-O; rzMB = l2+O;
            rzWT = -L*0.35-O; rzWB = L*0.35+O;
        }

        let pMT = {x: cxM, z: rzMT}, pMB = {x: cxM, z: rzMB};
        let pWT = {x: cxW, z: rzWT}, pWB = {x: cxW, z: rzWB};
        let pMC = {x: cxM, z: 0}, pWC = {x: cxW, z: 0};
        ridges.push([pMT, pMB], [pWT, pWB], [pMC, pWC]);
        
        rafters.push([{x: sR, z: -l2-O}, pMT], [{x: w2+O, z: -l2-O}, pMT]);
        rafters.push([{x: sR, z: l2+O}, pMB], [{x: w2+O, z: l2+O}, pMB]);
        rafters.push([{x: -w2-O, z: -L*0.35-O}, pWT], [{x: sL, z: -L*0.35-O}, pWT]);
        rafters.push([{x: -w2-O, z: L*0.35+O}, pWB], [{x: sL, z: L*0.35+O}, pWB]);
        rafters.push([{x: sL, z: sT}, pWC], [{x: sL, z: sB}, pWC]);
        rafters.push([{x: sR, z: sT}, pMC], [{x: sR, z: sB}, pMC]);
    }
    else if (shape === 'dual_wing') {
        let sML = -W*0.2 - O, sMR = W*0.2 + O;
        let sLL = -w2 - O, sLR = -W*0.35 + O;
        let sRL = W*0.35 - O, sRR = w2 + O;
        let sT = -L*0.15 - O, sB = L*0.15 + O;
        let cxM = 0, rzMT = -l2 + W*0.2, rzMB = l2 - W*0.2;
        let cxLW = (-w2 - W*0.35)/2, lwHW = (w2 - W*0.35)/2; 
        let rzLWT = -L*0.35 + lwHW, rzLWB = L*0.35 - lwHW;
        let cxRW = (w2 + W*0.35)/2, rwHW = (w2 - W*0.35)/2;
        let rzRWT = -L*0.35 + rwHW, rzRWB = L*0.35 - rwHW;
        
        if (type === 'gable') {
            rzMT = -l2-O; rzMB = l2+O;
            rzLWT = -L*0.35-O; rzLWB = L*0.35+O;
            rzRWT = -L*0.35-O; rzRWB = L*0.35+O;
        }

        let pMT = {x: cxM, z: rzMT}, pMB = {x: cxM, z: rzMB};
        let pLWT = {x: cxLW, z: rzLWT}, pLWB = {x: cxLW, z: rzLWB};
        let pRWT = {x: cxRW, z: rzRWT}, pRWB = {x: cxRW, z: rzRWB};
        let pLMC = {x: cxM, z: 0}, pLWC = {x: cxLW, z: 0};
        let pRMC = {x: cxM, z: 0}, pRWC = {x: cxRW, z: 0};

        ridges.push([pMT, pMB], [pLWT, pLWB], [pRWT, pRWB], [pLMC, pLWC], [pRMC, pRWC]);
        
        rafters.push([{x: sML, z: -l2-O}, pMT], [{x: sMR, z: -l2-O}, pMT]);
        rafters.push([{x: sML, z: l2+O}, pMB], [{x: sMR, z: l2+O}, pMB]);
        rafters.push([{x: sLL, z: -L*0.35-O}, pLWT], [{x: sLR, z: -L*0.35-O}, pLWT]);
        rafters.push([{x: sLL, z: L*0.35+O}, pLWB], [{x: sLR, z: L*0.35+O}, pLWB]);
        rafters.push([{x: sRL, z: -L*0.35-O}, pRWT], [{x: sRR, z: -L*0.35-O}, pRWT]);
        rafters.push([{x: sRL, z: L*0.35+O}, pRWB], [{x: sRR, z: L*0.35+O}, pRWB]);
        
        rafters.push([{x: sLR, z: sT}, pLWC], [{x: sLR, z: sB}, pLWC]);
        rafters.push([{x: sML, z: sT}, pLMC], [{x: sML, z: sB}, pLMC]);
        rafters.push([{x: sMR, z: sT}, pRMC], [{x: sMR, z: sB}, pRMC]);
        rafters.push([{x: sRL, z: sT}, pRWC], [{x: sRL, z: sB}, pRWC]);
    }

    // Draw main skeleton ridges and hips/valleys
    ridges.forEach(r => addThickLine(r[0].x, r[0].z, ry, r[1].x, r[1].z, ry, rThick, cjSize, MATS.wood));
    rafters.forEach(r => addThickLine(r[0].x, r[0].z, py, r[1].x, r[1].z, ry, rThick, cjSize, MATS.wood));

    let gableEnds = new Array(oPts.length).fill(false);
    if (type === 'gable') {
        for(let i=0; i<oPts.length; i++) {
            let p1 = oPts[i], p2 = oPts[(i+1)%oPts.length];
            let isGable = false;
            ridges.forEach(r => {
                [r[0], r[1]].forEach(pt => {
                    let d1 = Math.hypot(pt.x - p1.x, pt.z - p1.y);
                    let d2 = Math.hypot(pt.x - p2.x, pt.z - p2.y);
                    let segLen = Math.hypot(p2.x - p1.x, p2.y - p1.y);
                    if (Math.abs(d1 + d2 - segLen) < 1.0) isGable = true;
                });
            });
            gableEnds[i] = isGable;
        }
    }

    // --- NEW: Universal Rafter Generation (Commons, Hip Jacks, Valley Jacks) ---
    let rafterSpacing = 24;
    let pitchRatio = (parseFloat(document.getElementById('roofRise').value) || 6) / 12;
    
    let boundaries = [];
    for(let i=0; i<oPts.length; i++) boundaries.push({p1: oPts[i], p2: oPts[(i+1)%oPts.length]});
    ridges.forEach(r => boundaries.push({p1: {x: r[0].x, y: r[0].z}, p2: {x: r[1].x, y: r[1].z}}));
    rafters.forEach(r => boundaries.push({p1: {x: r[0].x, y: r[0].z}, p2: {x: r[1].x, y: r[1].z}}));

    function getRoofZ(X, Z) {
        let minH = Infinity;
        for(let i=0; i<oPts.length; i++) {
            if (gableEnds[i]) continue;
            let p1 = oPts[i], p2 = oPts[(i+1)%oPts.length];
            let dx = p2.x - p1.x, dy = p2.y - p1.y;
            let len = Math.hypot(dx, dy);
            let nx = -dy/len, ny = dx/len; 
            let dist = (X - p1.x)*nx + (Z - p1.y)*ny;
            if (dist > -0.1) {
                let h = py + dist * pitchRatio;
                if (h < minH) minH = h;
            }
        }
        return Math.min(minH, ry);
    }

    function raycast(S, N) {
        let minT = Infinity, closestPt = null;
        boundaries.forEach(b => {
            let dx = b.p2.x - b.p1.x, dy = b.p2.y - b.p1.y;
            let cross = N.x * dy - N.y * dx;
            if (Math.abs(cross) < 0.001) return;
            let t = ((b.p1.x - S.x) * dy - (b.p1.y - S.y) * dx) / cross;
            let u = ((b.p1.x - S.x) * N.y - (b.p1.y - S.y) * N.x) / cross;
            if (t > 0.1 && u >= -0.001 && u <= 1.001) {
                if (t < minT) { minT = t; closestPt = { x: S.x + t * N.x, y: S.y + t * N.y }; }
            }
        });
        return closestPt;
    }

    let rDict = {};
    function addRaft(pA, pB) {
        if (Math.hypot(pA.x-pB.x, pA.y-pB.y) < 1.0) return;
        let cx = (pA.x+pB.x)/2, cy = (pA.y+pB.y)/2;
        let key = Math.round(cx)+","+Math.round(cy);
        if(rDict[key]) return;
        rDict[key] = true;
        let yA = getRoofZ(pA.x, pA.y);
        let yB = getRoofZ(pB.x, pB.y);
        addThickLine(pA.x, pA.y, yA, pB.x, pB.y, yB, rThick, cjSize, MATS.wood);
    }

    let isPoly = shape.startsWith('polygon');
    function stepAlongLine(p1, p2, isDiag, cb) {
        let dx = p2.x - p1.x, dy = p2.y - p1.y;
        let len = Math.hypot(dx, dy);
        if (len < 0.1) return;
        if (isDiag) {
            for(let d = rafterSpacing/2; d < len; d += rafterSpacing) {
                cb({ x: p1.x + (dx/len)*d, y: p1.y + (dy/len)*d });
            }
        } else if (Math.abs(dx) > Math.abs(dy)) {
            let minB = Math.min(p1.x, p2.x), maxB = Math.max(p1.x, p2.x);
            for(let x = Math.ceil(minB/rafterSpacing)*rafterSpacing; x < maxB; x += rafterSpacing) {
                if(Math.abs(x - minB)<1 || Math.abs(x - maxB)<1) continue;
                cb({ x: x, y: p1.y + ((x - p1.x)/dx)*dy });
            }
        } else {
            let minB = Math.min(p1.y, p2.y), maxB = Math.max(p1.y, p2.y);
            for(let y = Math.ceil(minB/rafterSpacing)*rafterSpacing; y < maxB; y += rafterSpacing) {
                if(Math.abs(y - minB)<1 || Math.abs(y - maxB)<1) continue;
                cb({ x: p1.x + ((y - p1.y)/dy)*dx, y: y });
            }
        }
    }

    // Pass 1: Run rafters upwards from the Eaves (Catches Common & Hip Jack rafters)
    for(let i=0; i<oPts.length; i++) {
        let p1 = oPts[i], p2 = oPts[(i+1)%oPts.length];
        addThickLine(p1.x, p1.y, py, p2.x, p2.y, py, pThick, cjSize, MATS.wood); 
        
        if (gableEnds[i]) continue;
        
        let dx = p2.x - p1.x, dy = p2.y - p1.y;
        let len = Math.hypot(dx, dy);
        let normX = -dy/len, normY = dx/len; 
        
        stepAlongLine(p1, p2, isPoly, (S) => {
            let hit = raycast(S, {x: normX, y: normY});
            if (hit) addRaft(S, hit);
        });
    }

    // Pass 2: Run rafters downwards from the Ridges (Catches Common & Valley Jack rafters)
    ridges.forEach(r => {
        let p1 = {x: r[0].x, y: r[0].z}, p2 = {x: r[1].x, y: r[1].z};
        let dx = p2.x - p1.x, dy = p2.y - p1.y;
        if (Math.hypot(dx, dy) < 0.1) return;
        let n1, n2;
        if (isPoly) {
            let len = Math.hypot(dx, dy);
            n1 = {x: -dy/len, y: dx/len};
            n2 = {x: dy/len, y: -dx/len};
        } else {
            if (Math.abs(dx) > Math.abs(dy)) { n1 = {x:0, y:1}; n2 = {x:0, y:-1}; }
            else { n1 = {x:1, y:0}; n2 = {x:-1, y:0}; }
        }
        
        stepAlongLine(p1, p2, isPoly, (S) => {
            let hit1 = raycast(S, n1);
            if (hit1) addRaft(S, hit1);
            let hit2 = raycast(S, n2);
            if (hit2) addRaft(S, hit2);
        });
    });

    // --- CEILING JOISTS ---
    let minX = Math.min(...oPts.map(p=>p.x)) - 10;
    let maxX = Math.max(...oPts.map(p=>p.x)) + 10;
    let minZ = Math.min(...oPts.map(p=>p.y)) - 10;
    let maxZ = Math.max(...oPts.map(p=>p.y)) + 10;

    let ceilingShape = new THREE.Shape();
    oPts.forEach((p, i) => { if(i===0) ceilingShape.moveTo(p.x, p.y); else ceilingShape.lineTo(p.x, p.y); });
    let ceilingExtGeo = new THREE.ExtrudeGeometry(ceilingShape, { depth: cjSize, bevelEnabled: false });
    ceilingExtGeo.rotateX(Math.PI/2);
    let ceilingMesh = new THREE.Mesh(ceilingExtGeo);
    ceilingMesh.position.y = py + cjSize/2;
    ceilingMesh.updateMatrix();
    let ceilingCSG = CSG.fromMesh(ceilingMesh, 0);

    let ceilJGeo = new THREE.BoxGeometry(2, cjSize, maxZ - minZ + 20);
    let ceilJCSG = null;
    
    for(let x = minX; x <= maxX; x += 16) {
        let jm = new THREE.Mesh(ceilJGeo);
        jm.position.set(x, py, (minZ+maxZ)/2);
        jm.updateMatrix();
        let c = CSG.fromMesh(jm, 1);
        ceilJCSG = ceilJCSG ? ceilJCSG.union(c) : c;
    }
    
    if (ceilJCSG) {
        try {
            let finalCeilJoists = CSG.toMesh(ceilJCSG.intersect(ceilingCSG), new THREE.Matrix4(), MATS.wood);
            finalCeilJoists.userData.layer = 'roof';
            group.add(finalCeilJoists);
        } catch(e) { console.warn('CSG Ceiling Joist Error:', e); }
    }

    group.userData.layer = 'roof';
    return group;
}

function init() {
    viewList.forEach(v => {
        scenes[v] = new THREE.Scene(); scenes[v].background = new THREE.Color(0xf4f4f4);
        cameras[v] = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 10000);
        let cont = document.getElementById('vp'+v);
        renderers[v] = new THREE.WebGLRenderer({ antialias: true, alpha: false });
        renderers[v].setSize(cont.clientWidth||800, cont.clientHeight||600);
        cont.appendChild(renderers[v].domElement);
        scenes[v].add(new THREE.AmbientLight(0xffffff, 0.7));
        let dl = new THREE.DirectionalLight(0xffffff, 0.6); dl.position.set(1000, 2000, 1500); scenes[v].add(dl);
    });
    window.updateRoofUI();
    window.updateFloorUI();
    window.addEventListener('resize', window.resizeActiveViewport);
    setTimeout(() => { window.updateModel(); }, 100);
    animate();
}

window.updateModel = () => {
    // --- VENEER & UI INPUTS ---
    const showVeneer = document.getElementById('brickVeneerToggle')?.checked || false;
    const veneerW = parseFloat(document.getElementById('veneerW')?.value) || 3.25;
    const veneerGap = parseFloat(document.getElementById('veneerGap')?.value) || 1.0;

    extraGeoms.forEach(g => g.dispose()); extraMats.forEach(m => m.dispose());
    extraGeoms = []; extraMats = [];
    viewList.forEach(v => { let g = scenes[v].children.filter(c => c.name==='bldg'); g.forEach(x => scenes[v].remove(x)); });
    
    activeWallCenters = [];
    const bldg = new THREE.Group(); bldg.name = 'bldg';
    
    const mW = parseFloat(document.getElementById('bldgW').value)||480; 
    const mL = parseFloat(document.getElementById('bldgL').value)||360; 
    const wH = parseFloat(document.getElementById('wallH').value)||120;
    const wT = parseFloat(document.getElementById('wallT').value)||8;
    const ff2G = parseFloat(document.getElementById('ffToGrade').value)||24;
    const foundW = parseFloat(document.getElementById('foundW').value)||10;
    const footW = parseFloat(document.getElementById('footW').value)||16;
    const overhang = parseFloat(document.getElementById('roofOverhang').value)||24;
    const footD = 12; 
    
    const ns = parseInt(document.getElementById('numStories').value) || 1;
    const sfThk = parseFloat(document.getElementById('sfThk').value) || 1.5;
    const cjSize = parseFloat(document.getElementById('cjSize').value) || 11.25;
    const showPlanes = document.getElementById('showPlanes').checked;
    
    let fjSizes = [];
    for(let i=1; i<=ns; i++) {
        fjSizes.push(parseFloat(document.getElementById('fjSize'+i)?.value || 11.25));
    }
    
    let storyBases = [ff2G];
    for(let i=1; i<ns; i++) {
        storyBases.push(storyBases[i-1] + wH + fjSizes[i] + sfThk);
    }

    const roofType = document.querySelector('input[name="roofType"]:checked').value;
    const pitch = parseFloat(document.getElementById('roofRise').value) || 6;
    const rThick = parseFloat(document.getElementById('roofRidgeThick').value) || 1.5;
    const fThick = parseFloat(document.getElementById('roofFlatThick').value) || 1.5;

    let shape = document.getElementById('bldgShape').value;
    let isPoly = shape.startsWith('polygon');
    let pts = generateShapePoints(shape, mW, mL);
    activeWallCount = pts.length;
    
    // 1. BASE EXTERIOR WALLS
    let baseExtWalls = [];
    for(let i=0; i<activeWallCount; i++) {
        let p1 = pts[i], p2 = pts[(i+1)%activeWallCount];
        let dx = p2.x - p1.x, dz = p2.y - p1.y, len = Math.sqrt(dx*dx + dz*dz);
        baseExtWalls.push({ id: (i+1).toString(), len: len, angle: Math.atan2(dz, dx), midX: (p1.x+p2.x)/2, midZ: (p1.y+p2.y)/2, nX: -dz/len, nZ: dx/len, thick: wT, isInt: false });
    }

    // 2. BASE INTERIOR WALLS
    let baseIntWalls = [];
    let unresolvedInt = [...interiorWallsData];
    let loopCount = 0;
    while(unresolvedInt.length > 0 && loopCount < 1000) {
        let iw = unresolvedInt.shift();
        let anchorFloor = null;
        let anchor = null;

        let matchExt = iw.anchorId.match(/^(\d+)\./);
        let matchInt = iw.anchorId.match(/^A(\d+)\./);

        if (matchExt) {
            anchorFloor = parseInt(matchExt[1]);
            let extId = iw.anchorId.split('.')[1];
            anchor = baseExtWalls.find(w => w.id === extId);
        } else if (matchInt) {
            let baseId = iw.anchorId.split('_F')[0];
            anchor = baseIntWalls.find(w => w.id === baseId);
            if (anchor) {
                if (iw.anchorId.includes('_F')) {
                    anchorFloor = parseInt(iw.anchorId.split('_F')[1]);
                } else {
                    anchorFloor = anchor.floor;
                }
            }
        }
        
        if(anchor) {
            let localX = -anchor.len/2 + anchor.len*(iw.pos/100);
            let startX = anchor.midX + localX*Math.cos(anchor.angle) + anchor.nX*(anchor.thick/2);
            let startZ = anchor.midZ + localX*Math.sin(anchor.angle) + anchor.nZ*(anchor.thick/2);
            
            let iAng = Math.atan2(anchor.nZ, anchor.nX) + ((iw.angle||0) * Math.PI / 180);
            let length = iw.length;
            
            if (iw.isSnapSegment && iw.snapId && iw.snapId !== 'none') {
                let target = null;
                let snapMatchExt = iw.snapId.match(/^(\d+)\./);
                if (snapMatchExt) {
                    let snapExtId = iw.snapId.split('.')[1];
                    target = baseExtWalls.find(w => w.id === snapExtId);
                } else {
                    let baseSnapId = iw.snapId.split('_F')[0];
                    target = baseIntWalls.find(w => w.id === baseSnapId);
                }

                if (target) {
                    let dx = startX - target.midX;
                    let dz = startZ - target.midZ;
                    let dist = dx * target.nX + dz * target.nZ;
                    let absDist = Math.abs(dist) - (target.thick / 2) + (iw.thick / 2);
                    length = Math.max(1, absDist); 
                    let sign = dist >= 0 ? -1 : 1;
                    let dirX = sign * target.nX;
                    let dirZ = sign * target.nZ;
                    iAng = Math.atan2(dirZ, dirX);
                } else {
                    unresolvedInt.push(iw); 
                    loopCount++;
                    continue;
                }
            }
            let endX = startX + Math.cos(iAng)*length;
            let endZ = startZ + Math.sin(iAng)*length;
            
            baseIntWalls.push({ 
                id: iw.id, floor: anchorFloor, copyUp: iw.copyUp,
                len: length, angle: iAng, midX: (startX+endX)/2, midZ: (startZ+endZ)/2, 
                nX: Math.cos(iAng+Math.PI/2), nZ: Math.sin(iAng+Math.PI/2), 
                thick: iw.thick, isInt: true, eX: endX, eZ: endZ, color: iw.color 
            });
        } else {
            unresolvedInt.push(iw); 
        }
        loopCount++;
    }

    // 3. FLATTEN TO ALL WALLS BY STORY
    let allWalls = [];
    for(let s=1; s<=ns; s++) {
        let sBase = storyBases[s-1];
        let jY = storyBases[s-1] - sfThk - fjSizes[s-1]; 

        baseExtWalls.forEach(bw => {
            allWalls.push({ ...bw, fullId: s + '.' + bw.id, story: s, sBase, jY });
        });
        baseIntWalls.forEach(bw => {
            if (s === bw.floor) {
                allWalls.push({ ...bw, fullId: bw.id, story: s, sBase, jY });
            } else if (bw.copyUp && s > bw.floor) {
                let copiedId = bw.id + '_F' + s;
                allWalls.push({ ...bw, fullId: copiedId, story: s, sBase, jY });
            }
        });
    }
    window.allWallsContext = allWalls;

    let floorRects = getFloorRectangles(shape, mW, mL, wT);
    let topOfFound = Math.max(1, ff2G - sfThk - fjSizes[0]); 

    let floorBoundCSG = null;
    if(isPoly) {
        let floorShape = new THREE.Shape();
        pts.forEach((p, i) => { if(i===0) floorShape.moveTo(p.x, p.y); else floorShape.lineTo(p.x, p.y); });
        let floorBoundGeo = new THREE.ExtrudeGeometry(floorShape, { depth: 100, bevelEnabled: false });
        floorBoundGeo.rotateX(Math.PI/2);
        let floorBoundMesh = new THREE.Mesh(floorBoundGeo);
        floorBoundMesh.position.y = 100;
        floorBoundMesh.updateMatrix();
        floorBoundCSG = CSG.fromMesh(floorBoundMesh, 0);
        extraGeoms.push(floorBoundGeo);
    }
    
    for(let s=0; s<ns; s++) {
        let curJDepth = fjSizes[s];
        let sfY = storyBases[s] - sfThk/2;
        let jY = storyBases[s] - sfThk - curJDepth/2;

        floorRects.forEach(rect => {
            let sfGeo = new THREE.BoxGeometry(rect.rw, sfThk, rect.rl);
            let sfMesh = new THREE.Mesh(sfGeo, MATS.subfloor); sfMesh.position.set(rect.cx, sfY, rect.cz);
            if(isPoly) { sfMesh.updateMatrix(); sfMesh = CSG.toMesh(CSG.fromMesh(sfMesh, 0).intersect(floorBoundCSG), sfMesh.matrix, MATS.subfloor); }
            sfMesh.userData.layer = 'subfloor'; bldg.add(sfMesh);
            
            let gGeo = new THREE.BoxGeometry(rect.rw, curJDepth, 5.5);
            let gMesh = new THREE.Mesh(gGeo, MATS.wood); gMesh.position.set(rect.cx, jY, rect.cz);
            if(isPoly) { gMesh.updateMatrix(); gMesh = CSG.toMesh(CSG.fromMesh(gMesh, 0).intersect(floorBoundCSG), gMesh.matrix, MATS.wood); }
            gMesh.userData.layer = 'joist'; bldg.add(gMesh);

            let jSpacing = 16, numJoists = Math.floor(rect.rw / jSpacing), startX = rect.cx - rect.rw/2 + jSpacing/2;
            let jGeo = new THREE.BoxGeometry(2, curJDepth, rect.rl);
            let jCSG = null;
            for(let i=0; i<=numJoists; i++) {
                let jx = startX + i*jSpacing; if (jx > rect.cx + rect.rw/2) break;
                let jMesh = new THREE.Mesh(jGeo, MATS.wood); jMesh.position.set(jx, jY, rect.cz);
                if(isPoly) { jMesh.updateMatrix(); let csg = CSG.fromMesh(jMesh, i); jCSG = jCSG ? jCSG.union(csg) : csg; } 
                else { jMesh.userData.layer = 'joist'; bldg.add(jMesh); }
            }
            if(isPoly && jCSG) {
                let finalJ = CSG.toMesh(jCSG.intersect(floorBoundCSG), new THREE.Matrix4(), MATS.wood);
                finalJ.userData.layer = 'joist'; bldg.add(finalJ);
                extraGeoms.push(finalJ.geometry);
            }
            extraGeoms.push(sfGeo, gGeo, jGeo);
        });
    }

    let rH = 0;
    if (roofType !== 'flat') {
        let span = mW;
        if (shape.startsWith('polygon')) span = (parseFloat(document.getElementById('polyRadius').value)||240) * 2;
        else if (shape === 'rectangular') span = Math.min(mW, mL);
        else span = mW * 0.35; 
        
        rH = (span / 2 + overhang) * (pitch / 12);
        document.getElementById('ridgeHgt').value = rH.toFixed(1) + '"';
    } else {
        document.getElementById('ridgeHgt').value = '-';
    }
    
    let py = storyBases[ns-1] + wH;
    let ry = py + rH;
    
    let customRoofPlan = buildRoofPlan(shape, mW, mL, overhang, py, ry, roofType, fThick, rThick, cjSize);
    bldg.add(customRoofPlan);

    let masterWallCSG = null;
    let masterVeneerCSG = null;
    let masterFoundCSG = null;

    allWalls.forEach((w, idx) => {
        activeWallCenters.push({ pos: new THREE.Vector3(w.midX, w.sBase + wH + 10, w.midZ), label: w.fullId });
        
        // Foundation logic is strictly for the first story
        if(w.story === 1) {
            if(!w.isInt) {
                let fShift = (foundW/2) - (w.thick/2), cx = w.midX + w.nX*fShift, cz = w.midZ + w.nZ*fShift;
                let fwGeo = new THREE.BoxGeometry(w.len + foundW, topOfFound, foundW); let fwMesh = new THREE.Mesh(fwGeo, MATS.concrete);
                fwMesh.position.set(cx, topOfFound/2, cz); fwMesh.rotation.y = -w.angle; fwMesh.updateMatrix();

                let ftGeo = new THREE.BoxGeometry(w.len + footW, footD, footW); let ftMesh = new THREE.Mesh(ftGeo, MATS.concrete);
                ftMesh.position.set(cx, -footD/2, cz); ftMesh.rotation.y = -w.angle; ftMesh.updateMatrix();
                
                let foundBlockCSG = CSG.fromMesh(fwMesh, 0).union(CSG.fromMesh(ftMesh, 0));
                masterFoundCSG = masterFoundCSG ? masterFoundCSG.union(foundBlockCSG) : foundBlockCSG;
                extraGeoms.push(fwGeo, ftGeo);
            } else {
                let pGeo = new THREE.BoxGeometry(foundW, topOfFound, foundW); let pMesh = new THREE.Mesh(pGeo, MATS.concrete); pMesh.position.set(w.eX, topOfFound/2, w.eZ); pMesh.updateMatrix();
                let pfGeo = new THREE.BoxGeometry(footW, footD, footW); let pfMesh = new THREE.Mesh(pfGeo, MATS.concrete); pfMesh.position.set(w.eX, -footD/2, w.eZ); pfMesh.updateMatrix();
                
                let fIntBlockCSG = CSG.fromMesh(pMesh, 0).union(CSG.fromMesh(pfMesh, 0));
                masterFoundCSG = masterFoundCSG ? masterFoundCSG.union(fIntBlockCSG) : fIntBlockCSG;
                extraGeoms.push(pGeo, pfGeo);
            }
        }

        if(!w.isInt) {
            let rjShift = 1 - (w.thick/2); let rjGeo = new THREE.BoxGeometry(w.len, fjSizes[w.story-1], 2); let rjMesh = new THREE.Mesh(rjGeo, MATS.wood);
            rjMesh.position.set(w.midX + w.nX * rjShift, w.jY, w.midZ + w.nZ * rjShift); rjMesh.rotation.y = -w.angle; rjMesh.userData.layer = 'joist'; bldg.add(rjMesh);
            extraGeoms.push(rjGeo);
        } else {
            let djGeo = new THREE.BoxGeometry(w.len, fjSizes[w.story-1], 3.0); 
            let djMesh = new THREE.Mesh(djGeo, MATS.wood);
            djMesh.position.set(w.midX, w.jY, w.midZ);
            djMesh.rotation.y = -w.angle;
            djMesh.userData.layer = 'joist';
            bldg.add(djMesh);
            extraGeoms.push(djGeo);
        }

        let wallGeo = new THREE.BoxGeometry(w.len + (w.isInt ? w.thick : w.thick), wH, w.thick);
        let wallMesh = new THREE.Mesh(wallGeo, MATS.wall); 
        wallMesh.position.set(w.midX, w.sBase + wH/2, w.midZ); 
        wallMesh.rotation.y = -w.angle; 
        wallMesh.updateMatrix();
        let wallCSG = CSG.fromMesh(wallMesh, idx);
        extraGeoms.push(wallGeo);
        
        // --- BRICK VENEER CSG ---
        let vCSG = null;
        if (showVeneer && !w.isInt) {
            // For story 1, base is perfectly at Y = 0 (top of footing). For higher stories, it covers the joist gap down to the top of the wall below.
            let vBottom = (w.story === 1) ? 0 : (storyBases[w.story-2] + wH);
            let vTop = w.sBase + wH;
            let vH = vTop - vBottom;
            let vY = vBottom + vH / 2;
            
            // Calculate outward shift (nX, nZ point inwardly, so subtract to push outward)
            let vOffset = (w.thick / 2) + veneerGap + (veneerW / 2);
            let vX = w.midX - w.nX * vOffset;
            let vZ = w.midZ - w.nZ * vOffset;
            
            // Expand length slightly to naturally intersect and close outside corners cleanly
            let vLen = w.len + w.thick + 2 * (veneerGap + veneerW);
            
            let vGeo = new THREE.BoxGeometry(vLen, vH, veneerW);
            let vMesh = new THREE.Mesh(vGeo, MATS.brick);
            vMesh.position.set(vX, vY, vZ);
            vMesh.rotation.y = -w.angle;
            vMesh.updateMatrix();
            
            vCSG = CSG.fromMesh(vMesh, idx + 10000);
            extraGeoms.push(vGeo);
        }

        let cuts = featuresData.filter(op => op.wallId === w.fullId);
        cuts.forEach(op => {
            let localX = -w.len/2 + w.len*(op.pos/100);
            let cX = w.midX + localX*Math.cos(w.angle);
            let cZ = w.midZ + localX*Math.sin(w.angle);

            let headHeight = 80; 
            let cy = op.cat === 'window' ? (w.sBase + headHeight - (op.h/2)) : (w.sBase + (op.h/2));
            
            // Make cut deep enough to seamlessly pierce both inner frame and outer veneer
            let cutDepth = Math.max(40, w.thick * 4 + (showVeneer ? veneerGap * 2 + veneerW * 2 : 0));
            let cutMesh = new THREE.Mesh(new THREE.BoxGeometry(op.w, op.h, cutDepth)); 
            cutMesh.position.set(cX, cy, cZ); 
            cutMesh.rotation.y = -w.angle; 
            cutMesh.updateMatrix();
            
            let csgCut = CSG.fromMesh(cutMesh, 1);
            wallCSG = wallCSG.subtract(csgCut);
            if (vCSG) vCSG = vCSG.subtract(csgCut);

            let assm = op.cat === 'door' ? (op.type === 'Sliding' ? buildSliding(op.w, op.h, op) : buildStandardDoor(op.w, op.h, op, !w.isInt)) : buildWindowSashes(op.w, op.h, op);
            assm.add(buildCasementTrim(op.w, op.h, w.thick)); assm.position.set(cX, cy, cZ); assm.rotation.y = -w.angle; assm.userData.layer = 'detail';
            
            assm.traverse(c => { c.userData.layer = 'detail'; });
            bldg.add(assm);
        });

        // HVAC Fireplace Cutouts applied to the wall CSG (and veneer if present)
        let hvacCuts = hvacData.filter(hv => hv.wallId === w.fullId && hv.type === 'fireplace');
        hvacCuts.forEach(hv => {
            let localX = -w.len/2 + w.len*(hv.pos/100);
            let cX = w.midX + localX*Math.cos(w.angle);
            let cZ = w.midZ + localX*Math.sin(w.angle);
            let hvH = hv.h || 60;
            let hvD = hv.d || 24;
            let taperH = 30;
            let chimH = 144;
            
            let insideX = w.nX, insideZ = w.nZ;
            let insideFace = w.thick / 2;
            let interiorProtrusion = 4;
            let centerOffset = insideFace + interiorProtrusion - (hvD / 2);
            let baseX = cX + insideX * centerOffset;
            let baseZ = cZ + insideZ * centerOffset;
            
            let cutBase = new THREE.Mesh(new THREE.BoxGeometry(hv.w, hvH, hvD));
            cutBase.position.set(baseX, w.sBase + hvH/2, baseZ);
            cutBase.rotation.y = -w.angle; cutBase.updateMatrix();
            
            let taperGeo = new THREE.CylinderGeometry(0.5 * Math.SQRT2, 1.0 * Math.SQRT2, taperH, 4);
            taperGeo.rotateY(Math.PI/4);
            taperGeo.scale(hv.w / 2, 1, hvD / 2);
            let cutTaper = new THREE.Mesh(taperGeo);
            cutTaper.position.set(baseX, w.sBase + hvH + taperH/2, baseZ);
            cutTaper.rotation.y = -w.angle; cutTaper.updateMatrix();
            
            let cutChim = new THREE.Mesh(new THREE.BoxGeometry(hv.w/2, chimH, hvD/2));
            cutChim.position.set(baseX, w.sBase + hvH + taperH + chimH/2, baseZ);
            cutChim.rotation.y = -w.angle; cutChim.updateMatrix();
            
            let csgBase = CSG.fromMesh(cutBase, 2);
            let csgTaper = CSG.fromMesh(cutTaper, 2);
            let csgChim = CSG.fromMesh(cutChim, 2);

            wallCSG = wallCSG.subtract(csgBase).subtract(csgTaper).subtract(csgChim);
            if (vCSG) vCSG = vCSG.subtract(csgBase).subtract(csgTaper).subtract(csgChim);
        });
        
        masterWallCSG = masterWallCSG ? masterWallCSG.union(wallCSG) : wallCSG;
        if (vCSG) masterVeneerCSG = masterVeneerCSG ? masterVeneerCSG.union(vCSG) : vCSG;
    });

    if(masterWallCSG) {
        let finalMasterWall = CSG.toMesh(masterWallCSG, new THREE.Matrix4(), MATS.wall);
        finalMasterWall.userData.layer = 'wall';
        bldg.add(finalMasterWall);
        extraGeoms.push(finalMasterWall.geometry);
    }
    
    // Add unified brick veneer to the scene
    if(masterVeneerCSG) {
        let finalMasterVeneer = CSG.toMesh(masterVeneerCSG, new THREE.Matrix4(), MATS.brick);
        finalMasterVeneer.userData.layer = 'wall';
        bldg.add(finalMasterVeneer);
        extraGeoms.push(finalMasterVeneer.geometry);
    }
    
    if(masterFoundCSG) {
        let finalMasterFound = CSG.toMesh(masterFoundCSG, new THREE.Matrix4(), MATS.concrete);
        finalMasterFound.userData.layer = 'foundation';
        bldg.add(finalMasterFound);
        extraGeoms.push(finalMasterFound.geometry);
    }
// HVAC Renderings
    hvacData.forEach(hv => {
        let w = allWalls.find(x => x.fullId === hv.wallId); if(!w) return;
        let localX = -w.len/2 + w.len*(hv.pos/100);
        let cx = w.midX + localX*Math.cos(w.angle);
        let cz = w.midZ + localX*Math.sin(w.angle);
        let mesh;
        let hvD = hv.d || 24;
        let hvH = hv.h || 60;
        
        // nX/nZ point inside, -nX/-nZ point outside
        let insideX = w.nX, insideZ = w.nZ;
        let outsideX = -w.nX, outsideZ = -w.nZ;

        // Safely fetch veneer settings in case they are missing from scope
        const showVeneer = document.getElementById('brickVeneerToggle')?.checked || false;
        const veneerW = parseFloat(document.getElementById('veneerW')?.value) || 3.25;
        const veneerGap = parseFloat(document.getElementById('veneerGap')?.value) || 1.0;

if (hv.type === 'fireplace') {
            let fpGroup = new THREE.Group();
            let taperH = 30;
            let chimH = 144;
            let fpBaseH = w.sBase + hvH; 
            
            let insideFace = w.thick / 2;
            let interiorProtrusion = 0; // INSET 4 INCHES INTO THE ROOM
            let centerOffset = insideFace + interiorProtrusion - (hvD / 2); 
            
            let baseX = cx + insideX * centerOffset;
            let baseZ = cz + insideZ * centerOffset;
            
            let baseMesh = new THREE.Mesh(new THREE.BoxGeometry(hv.w, fpBaseH, hvD), MATS.wall); 
            baseMesh.position.set(0, fpBaseH/2, 0);
            baseMesh.userData.layer = 'foundation'; 
            
            // --- TAPER GEOMETRY ---
            // Scaled to match the new chimney footprint (50% W, 75% D)
            let taperGeo = new THREE.CylinderGeometry(0.5 * Math.SQRT2, 1.0 * Math.SQRT2, taperH, 4);
            taperGeo.rotateY(Math.PI/4);
            taperGeo.scale(hv.w / 2, 1, hvD / 2);
            
            // Shift top vertices to keep interior face flush
            // Offset is half the depth difference (1.0 - 0.75) / 2 = 0.125
            let posAttr = taperGeo.attributes.position;
            for (let i = 0; i < posAttr.count; i++) {
                if (posAttr.getY(i) > 0) { 
                    posAttr.setZ(i, posAttr.getZ(i) + (hvD * 0.125)); 
                }
            }
            taperGeo.computeVertexNormals(); 

            let taperMesh = new THREE.Mesh(taperGeo, MATS.wall);
            taperMesh.position.set(0, fpBaseH + taperH/2, 0);
            taperMesh.userData.layer = 'foundation'; 
            
            // --- CHIMNEY (50% W, 75% D) ---
            // Offset Z by 12.5% of depth to maintain flush interior face
            let chimMesh = new THREE.Mesh(new THREE.BoxGeometry(hv.w * 0.5, chimH, hvD * 0.75), MATS.wall);
            chimMesh.position.set(0, fpBaseH + taperH + chimH/2, hvD * 0.125);
            chimMesh.userData.layer = 'foundation'; 

            // HEARTH & RIM JOISTS (Remain same)
            let hearthD = 18;
            let hearthH = w.sBase + 12;
            let hearthMesh = new THREE.Mesh(new THREE.BoxGeometry(hv.w, hearthH, hearthD), MATS.wall);
            hearthMesh.position.set(0, hearthH/2, (hvD/2) + (hearthD/2));
            hearthMesh.userData.layer = 'foundation'; 

            let curJDepth = fjSizes[(w.story || 1) - 1] || 11.25;
            let rjLeft = new THREE.Mesh(new THREE.BoxGeometry(2, curJDepth, hearthD), MATS.wood);
            rjLeft.position.set(-(hv.w/2 + 1), w.jY, (hvD/2) + (hearthD/2));
            rjLeft.userData.layer = 'joist';

            let rjRight = new THREE.Mesh(new THREE.BoxGeometry(2, curJDepth, hearthD), MATS.wood);
            rjRight.position.set((hv.w/2 + 1), w.jY, (hvD/2) + (hearthD/2));
            rjRight.userData.layer = 'joist';

            let rjFront = new THREE.Mesh(new THREE.BoxGeometry(hv.w + 4, curJDepth, 2), MATS.wood);
            rjFront.position.set(0, w.jY, (hvD/2) + hearthD + 1);
            rjFront.userData.layer = 'joist';

            let slabThick = 12;
            let slabW = hv.w + 16;
            let slabD = hvD + hearthD + 2 + 12;
            let fpSlab = new THREE.Mesh(new THREE.BoxGeometry(slabW, slabThick, slabD), MATS.concrete);
            fpSlab.position.set(0, -slabThick/2, (hearthD + 2)/2); 
            fpSlab.userData.layer = 'foundation'; 

            // --- FIREPLACE FACE FRAMING ---
            let frameW = 3.5;
            let frameD = 5.5;
            let frameZ = (hvD / 2) + (frameD / 2); 
            
            let whiteMat = new THREE.MeshStandardMaterial({ color: 0xffffff });

            let topFrameY = fpBaseH - (frameW / 2);
            let fpFrameTop = new THREE.Mesh(new THREE.BoxGeometry(hv.w, frameW, frameD), whiteMat);
            fpFrameTop.position.set(0, topFrameY, frameZ);
            fpFrameTop.userData.layer = 'wall';

            let vertStartY = hearthH; 
            let vertEndY = fpBaseH - frameW; 
            let vertH = vertEndY - vertStartY;
            let vertY = vertStartY + (vertH / 2);

            let fpFrameLeft = new THREE.Mesh(new THREE.BoxGeometry(frameW, vertH, frameD), whiteMat);
            fpFrameLeft.position.set(-(hv.w / 2) + (frameW / 2), vertY, frameZ); 
            fpFrameLeft.userData.layer = 'wall';

            let fpFrameRight = new THREE.Mesh(new THREE.BoxGeometry(frameW, vertH, frameD), whiteMat);
            fpFrameRight.position.set((hv.w / 2) - (frameW / 2), vertY, frameZ);
            fpFrameRight.userData.layer = 'wall';
            
            fpGroup.add(
                baseMesh, taperMesh, chimMesh, hearthMesh, 
                rjLeft, rjRight, rjFront, fpSlab,
                fpFrameTop, fpFrameLeft, fpFrameRight
            );
            
            fpGroup.traverse(c => { 
                if(c.isMesh && !c.userData.layer) c.userData.layer = 'detail'; 
            });
            
            fpGroup.position.set(baseX, 0, baseZ);
            fpGroup.rotation.y = -w.angle;
            bldg.add(fpGroup);

            // --- LOCAL CSG POST-PROCESSING TRIM ---
            // (Note: CSG logic remains robust as long as the base and hearth geometry are consistent)
            let cutMinY = -14; 
            let cutMaxY = fpBaseH + 2; 
            let cutH = cutMaxY - cutMinY;
            let cutY = cutMinY + cutH / 2;

            let cutBase = new THREE.Mesh(new THREE.BoxGeometry(hv.w + 0.1, cutH, hvD + 0.1));
            cutBase.position.set(baseX, cutY, baseZ);
            cutBase.rotation.y = -w.angle;
            cutBase.updateMatrix();

            let hearthCutMaxY = hearthH + 2;
            let hearthCutH = hearthCutMaxY - cutMinY;
            let hearthCutY = cutMinY + hearthCutH / 2;
            
            let cutHearthW = hv.w + 4;
            let cutHearthD = hearthD + 2;
            let hearthCutCX = baseX + insideX * ((hvD/2) + cutHearthD/2);
            let hearthCutCZ = baseZ + insideZ * ((hvD/2) + cutHearthD/2);
            
            let cutHearth = new THREE.Mesh(new THREE.BoxGeometry(cutHearthW + 0.1, hearthCutH, cutHearthD + 0.1));
            cutHearth.position.set(hearthCutCX, hearthCutY, hearthCutCZ);
            cutHearth.rotation.y = -w.angle;
            cutHearth.updateMatrix();

            let csgBase = CSG.fromMesh(cutBase, 0);
            let csgHearth = CSG.fromMesh(cutHearth, 0);
            let combinedVoid = csgBase.union(csgHearth);

            let voidGrp = new THREE.Group();
            voidGrp.add(cutBase.clone(), cutHearth.clone());
            voidGrp.updateMatrixWorld(true);
            let voidBox = new THREE.Box3().setFromObject(voidGrp);

            for (let i = bldg.children.length - 1; i >= 0; i--) {
                let child = bldg.children[i];
                if (['joist', 'subfloor', 'foundation', 'wall'].includes(child.userData.layer)) {
                    child.updateMatrixWorld(true);
                    let childBox = new THREE.Box3().setFromObject(child);
                    if (voidBox.intersectsBox(childBox)) {
                        try {
                            let childCSG = CSG.fromMesh(child, 0);
                            let resultCSG = childCSG.subtract(combinedVoid);
                            if (resultCSG.polygons.length === 0) {
                                bldg.remove(child);
                            } else {
                                let newMesh = CSG.toMesh(resultCSG, new THREE.Matrix4(), child.material);
                                newMesh.userData.layer = child.userData.layer;
                                bldg.remove(child);
                                bldg.add(newMesh);
                            }
                        } catch(e) { console.warn('CSG Trim Skip:', e); }
                    }
                }
            }
        }else if (hv.type === 'ac') {
            let acGroup = new THREE.Group();
            
            // Main chassis
            let acBody = new THREE.Mesh(new THREE.BoxGeometry(hv.w, hvH, hvD), MATS.metal);
            
            // Dark interior fan area (top)
            let fanRadius = Math.min(hv.w, hvD) * 0.4;
            let fanHole = new THREE.Mesh(new THREE.CylinderGeometry(fanRadius, fanRadius, 0.5, 16), new THREE.MeshStandardMaterial({color: 0x111111, roughness: 0.9}));
            fanHole.position.y = hvH/2;
            
            // Fan blades
            let bladeMat = new THREE.MeshStandardMaterial({color: 0x222222});
            let fanBlade1 = new THREE.Mesh(new THREE.BoxGeometry(fanRadius * 1.8, 0.2, 3), bladeMat);
            fanBlade1.position.y = hvH/2 + 0.1;
            fanBlade1.rotation.y = Math.PI/4;
            let fanBlade2 = new THREE.Mesh(new THREE.BoxGeometry(3, 0.2, fanRadius * 1.8), bladeMat);
            fanBlade2.position.y = hvH/2 + 0.1;
            fanBlade2.rotation.y = Math.PI/4;
            
            // Top protective grill ring
            let grill = new THREE.Mesh(new THREE.TorusGeometry(fanRadius, 0.5, 4, 16), MATS.metal);
            grill.rotation.x = Math.PI/2;
            grill.position.y = hvH/2 + 0.5;
            
            // Side electrical control box
            let controlBox = new THREE.Mesh(new THREE.BoxGeometry(6, 14, 4), MATS.wall);
            controlBox.position.set(hv.w/2 + 1, 0, hvD/4);
            
            acGroup.add(acBody, fanHole, fanBlade1, fanBlade2, grill, controlBox);
            
            let outsideFace = w.thick / 2;
            let acClearance = 2; // 2 inches from exterior face
            let extraOffset = (showVeneer && !w.isInt) ? (veneerGap + veneerW) : 0;
            let centerOffset = outsideFace + acClearance + extraOffset + (hvD / 2);
            
            // Assembly center
            acGroup.position.set(cx + outsideX * centerOffset, 4 + hvH/2, cz + outsideZ * centerOffset);
            mesh = acGroup;
            
            // Draw 4" thick slab underneath AC
            let slab = new THREE.Mesh(new THREE.BoxGeometry(hv.w + 4, 4, hvD + 4), MATS.concrete);
            slab.position.set(cx + outsideX * centerOffset, 2, cz + outsideZ * centerOffset);
            slab.rotation.y = -w.angle; slab.userData.layer = 'detail'; bldg.add(slab);
            
        } else if (hv.type === 'central') {
            let centralGroup = new THREE.Group();
            
            // Main Air Handler Cabinet
            let cabH = hvH * 0.7;
            let cab = new THREE.Mesh(new THREE.BoxGeometry(hv.w, cabH, hvD), MATS.metal);
            cab.position.y = -hvH/2 + cabH/2; 
            
            // Upper Supply Air Plenum
            let plenH = hvH * 0.3;
            let plenum = new THREE.Mesh(new THREE.BoxGeometry(hv.w * 0.85, plenH, hvD * 0.85), MATS.metal);
            plenum.position.y = -hvH/2 + cabH + plenH/2;
            
            // Front Access Panel
            let panelGeo = new THREE.BoxGeometry(hv.w * 0.7, cabH * 0.5, 1);
            let panelMat = new THREE.MeshStandardMaterial({color: 0x999999, roughness: 0.5});
            let panel = new THREE.Mesh(panelGeo, panelMat);
            panel.position.set(0, -hvH/2 + cabH * 0.6, hvD/2 + 0.2); 
            
            // Side piping (Insulated Refrigerant line)
            let pipeGeo = new THREE.CylinderGeometry(0.8, 0.8, 6, 8);
            pipeGeo.rotateZ(Math.PI/2);
            let lineSetMat = new THREE.MeshStandardMaterial({color: 0x111111}); 
            let lineSet = new THREE.Mesh(pipeGeo, lineSetMat);
            lineSet.position.set(hv.w/2 + 2, -hvH/2 + cabH * 0.3, 0);

            // PVC Exhaust/Condensate pipe
            let pvcGeo = new THREE.CylinderGeometry(0.6, 0.6, 6, 8);
            pvcGeo.rotateZ(Math.PI/2);
            let pvcMat = new THREE.MeshStandardMaterial({color: 0xffffff}); 
            let pvc = new THREE.Mesh(pvcGeo, pvcMat);
            pvc.position.set(hv.w/2 + 2, -hvH/2 + cabH * 0.2, hvD/4);

            centralGroup.add(cab, plenum, panel, lineSet, pvc);

            let insideFace = w.thick / 2;
            let centerOffset = insideFace + (hvD / 2);
            
            // Rests properly on the subfloor base instead of floating
            centralGroup.position.set(cx + insideX * centerOffset, w.sBase + hvH/2 + 1, cz + insideZ * centerOffset);
            mesh = centralGroup;
            
        } else if (hv.type === 'thermostat') {
            mesh = new THREE.Mesh(new THREE.BoxGeometry(4, 3, 1), MATS.metal);
            let insideFace = w.thick / 2;
            let centerOffset = insideFace + 0.5; // d=1, so 0.5 is flush
            mesh.position.set(cx + insideX * centerOffset, w.sBase + 60, cz + insideZ * centerOffset);
        } else if (hv.type === 'duct') {
            mesh = new THREE.Mesh(new THREE.BoxGeometry(hv.w, 8, 12), MATS.metal);
            let insideFace = w.thick / 2;
            let centerOffset = insideFace + 6; // default depth 12 / 2
            mesh.position.set(cx + insideX * centerOffset, w.sBase + 4, cz + insideZ * centerOffset);
        }
        
        if(mesh) {
            mesh.rotation.y = -w.angle;
            mesh.traverse(c => { if(c.isMesh && !c.userData.layer) c.userData.layer = 'detail'; });
            bldg.add(mesh);
        }
    });
    electricalData.forEach(el => {
        let w = allWalls.find(x => x.fullId === el.wallId); if(!w) return;
        let localX = -w.len/2 + w.len*(el.pos/100);
        let cx = w.midX + localX*Math.cos(w.angle), cz = w.midZ + localX*Math.sin(w.angle);
        
        // nX/nZ point inside, -nX/-nZ point outside
        let insideX = w.nX, insideZ = w.nZ;
        let outsideX = -w.nX, outsideZ = -w.nZ;

        if (el.type === 'battery_wall' || el.type === 'battery_grade') {
            let elW = el.w || 36, elH = el.h || 48, elD = el.d || 12;
            if(el.type === 'battery_wall') elD = 6; // Wall mounted strictly 6" depth
            
            let centerOffset, cy;
            let isGrade = el.type === 'battery_grade';
            let extraOffset = (showVeneer && !w.isInt) ? (veneerGap + veneerW) : 0;
            
            if (isGrade) {
                let clearance = 2; // 2 inches off the wall
                centerOffset = w.thick / 2 + clearance + extraOffset + elD / 2;
                cy = 4 + elH / 2; // Rests on a 4" thick slab
                
                // Draw 4" thick slab underneath battery pack
                let slab = new THREE.Mesh(new THREE.BoxGeometry(elW + 4, 4, elD + 4), MATS.concrete);
                slab.position.set(cx + outsideX * centerOffset, 2, cz + outsideZ * centerOffset);
                slab.rotation.y = -w.angle; slab.userData.layer = 'detail'; bldg.add(slab);
            } else {
                centerOffset = w.thick / 2 + extraOffset + elD / 2; // Flush against outside wall
                cy = w.sBase + 36 + elH / 2; // Wall mounted height
            }
            
            // Build an array of batteries within the case bounds
            let bGrp = new THREE.Group();
            
            // Slightly transparent case
            let caseGeo = new THREE.BoxGeometry(elW, elH, elD);
            let caseMat = new THREE.MeshStandardMaterial({ color: 0x999999, transparent: true, opacity: 0.5, side: THREE.DoubleSide });
            let caseMesh = new THREE.Mesh(caseGeo, caseMat);
            bGrp.add(caseMesh);
            
            // Inner cells grid
            let cols = Math.max(1, Math.floor(elW / 8));
            let rows = Math.max(1, Math.floor(elH / 16));
            let cellW = (elW - 4) / cols - 1;
            let cellH = (elH - 4) / rows - 1;
            let cellD = elD - 4;
            
            let cellGeo = new THREE.BoxGeometry(cellW, cellH, cellD);
            let cellMat = new THREE.MeshStandardMaterial({ color: 0x333333, metalness: 0.8, roughness: 0.2 });
            
            for(let c = 0; c < cols; c++) {
                for(let r = 0; r < rows; r++) {
                    let cell = new THREE.Mesh(cellGeo, cellMat);
                    let px = -elW/2 + 2 + cellW/2 + c*(cellW+1);
                    let py = -elH/2 + 2 + cellH/2 + r*(cellH+1);
                    cell.position.set(px, py, 0);
                    bGrp.add(cell);
                }
            }
            
            bGrp.position.set(cx + outsideX * centerOffset, cy, cz + outsideZ * centerOffset);
            bGrp.rotation.y = -w.angle; 
            bGrp.userData.layer = 'detail'; 
            bldg.add(bGrp);

        } else {
            // Standard electrical items (inside wall)
            let h = el.type === 'switch' ? 6 : el.type === 'outlet' ? 4 : 8, wd = 3, d = 1;
            let cy = w.sBase + (el.type === 'switch' ? 48 : el.type === 'outlet' ? 18 : 84);
            let elMesh = new THREE.Mesh(new THREE.BoxGeometry(wd, h, d), MATS.metal);
            
            // Properly calculated inside normals for interior flush mounting
            let centerOffset = w.thick / 2 + d / 2;
            elMesh.position.set(cx + insideX * centerOffset, cy, cz + insideZ * centerOffset);
            elMesh.rotation.y = -w.angle; elMesh.userData.layer = 'detail'; bldg.add(elMesh);
        }
    });

    if(showPlanes) {
        const fGeo = new THREE.PlaneGeometry(mW*2, mL*2); fGeo.rotateX(-Math.PI/2); extraGeoms.push(fGeo);
        const grade = new THREE.Mesh(fGeo, new THREE.MeshStandardMaterial({ color: 0x446644, side: THREE.DoubleSide })); grade.position.y = 0; grade.userData.layer = 'plane'; bldg.add(grade);
    }

    columnsData.forEach(col => {
        let floorIdx = Math.min((col.floor || 1) - 1, ns - 1);
        let colSBase = storyBases[floorIdx] || storyBases[0];
        let colHeight = wH;
        
        let colGeo = new THREE.CylinderGeometry(4, 4, colHeight, 16); 
        let plateThk = col.topThick||2;
        let plateGeo = new THREE.BoxGeometry(12, plateThk, 12);
        let pierGeo = new THREE.BoxGeometry(16, ff2G + footD, 16);
        extraGeoms.push(colGeo, plateGeo, pierGeo);
        
        for(let i=0; i<col.count; i++) {
            let px = col.startX + (i*col.spacingX), pz = col.startZ + (i*col.spacingZ);
            
            let m = new THREE.Mesh(colGeo, MATS.metal); 
            m.position.set(px, colSBase + colHeight/2, pz); 
            m.userData.layer = 'detail'; 
            bldg.add(m);
            
            let bPlate = new THREE.Mesh(plateGeo, MATS.metal); 
            bPlate.position.set(px, colSBase + plateThk/2, pz); 
            bPlate.userData.layer = 'detail'; 
            bldg.add(bPlate);
            
            let tPlate = new THREE.Mesh(plateGeo, MATS.metal); 
            tPlate.position.set(px, colSBase + colHeight - plateThk/2, pz); 
            tPlate.userData.layer = 'detail'; 
            bldg.add(tPlate);
            
            if (col.floor === 1 || !col.floor) {
                let pier = new THREE.Mesh(pierGeo, MATS.concrete); 
                pier.position.set(px, (ff2G+footD)/2 - footD, pz); 
                pier.userData.layer = 'foundation'; 
                bldg.add(pier);
            }
        }
    });

    viewList.forEach(v => {
        let clone = bldg.clone();
        
        clone.traverse(child => {
            if(!child.userData || !child.userData.layer) return;
            
            if(v === '1') {
                if(['roof', 'plane'].includes(child.userData.layer)) {
                    child.visible = false;
                } else if (child.isMesh) {
                    child.material = child.material;
                    child.material.transparent = true;
                    child.material.opacity = 0.8; 
                    let edgesGeo = new THREE.EdgesGeometry(child.geometry, 1);
                    let edgesMat = new THREE.LineBasicMaterial({color: 0x000000});
                    let edges = new THREE.LineSegments(edgesGeo, edgesMat);
                    child.add(edges);
                }
            }
            else if (v === '7') {
                if(!['joist', 'foundation'].includes(child.userData.layer)) child.visible = false;
                else if (child.isMesh) {
                    let edgesGeo = new THREE.EdgesGeometry(child.geometry, 1);
                    let edgesMat = new THREE.LineBasicMaterial({color: 0x000000});
                    let edges = new THREE.LineSegments(edgesGeo, edgesMat);
                    extraMats.push(edgesMat); extraGeoms.push(edgesGeo);
                    child.add(edges);
                }
            }
            else if (v === '8') {
                if(child.userData.layer !== 'roof') child.visible = false;
            }
        });
        scenes[v].add(clone);
    });

    let max = Math.max(mW, mL, wH * ns) * 1.5, cyPos = ff2G + (wH * ns)/2;
    cameras['1'].position.set(0, max, 0); cameras['1'].lookAt(0, 0, 0); 
    cameras['2'].position.set(0, cyPos, max); cameras['2'].lookAt(0, cyPos, 0); 
    cameras['3'].position.set(max, cyPos, 0); cameras['3'].lookAt(0, cyPos, 0); 
    cameras['4'].position.set(0, cyPos, -max); cameras['4'].lookAt(0, cyPos, 0); 
    cameras['5'].position.set(-max, cyPos, 0); cameras['5'].lookAt(0, cyPos, 0); 
    cameras['6'].position.set(max*0.8, max*0.8, max*0.8); cameras['6'].lookAt(0, 0, 0); 
    cameras['7'].position.set(0, max, 0); cameras['7'].lookAt(0, 0, 0); 
    cameras['8'].position.set(0, max, 0); cameras['8'].lookAt(0, 0, 0); 
    
    window.resizeActiveViewport();
};

window.resizeActiveViewport = () => {
    let activeVps = [currentPage.toString()];
    
    let max = Math.max(parseFloat(document.getElementById('bldgW').value), parseFloat(document.getElementById('bldgL').value));
    let s = Math.max(max * 0.7, 50);

    activeVps.forEach(v => {
        let vp = document.getElementById('vp' + v);
        if(!vp || vp.clientWidth === 0) return;
        let aspect = vp.clientWidth / vp.clientHeight;
        renderers[v].setSize(vp.clientWidth, vp.clientHeight);
        let cam = cameras[v]; cam.left = -s * aspect; cam.right = s * aspect; cam.top = s; cam.bottom = -s; cam.updateProjectionMatrix();
    });

    let lblLayer = document.getElementById('lbl'+currentPage); 
    if(lblLayer) lblLayer.innerHTML = '';
    
    if(currentPage === 1 && lblLayer) {
        let cam = cameras['1'], vp = document.getElementById('vp1');
        activeWallCenters.forEach(d => {
            let sp = d.pos.clone().project(cam);
            if(sp.z >= -1 && sp.z <= 1) {
                let div = document.createElement('div'); div.className = 'wall-label';
                div.style.left = `${(sp.x*0.5 + 0.5)*vp.clientWidth}px`; div.style.top = `${(-sp.y*0.5 + 0.5)*vp.clientHeight}px`;
                div.textContent = d.label; lblLayer.appendChild(div);
            }
        });
    }
};

window.changePage = (dir) => {
    document.getElementById('sheet'+currentPage).classList.remove('active');
    currentPage += dir; if(currentPage < 1) currentPage = maxPages; if(currentPage > maxPages) currentPage = 1;
    document.getElementById('sheet'+currentPage).classList.add('active');
    setTimeout(window.resizeActiveViewport, 10);
};

function animate() {
    requestAnimationFrame(animate);
    let activeVps = [currentPage.toString()];
    
    activeVps.forEach(v => {
        let vp = document.getElementById('vp'+v);
        if(vp && vp.clientWidth > 0) renderers[v].render(scenes[v], cameras[v]);
    });
}
window.onload = init;
</script>

Just sharing three ideas for making beams easier to stitch without CSG:

  • use shearing instead of only rotating
  • use clipping planes as alternative to CSG
  • use custom beams defined by 4x4 vertices

I managed to get the cuts you’re pointing out with this code here:



function buildRoofPlan(shape, W, L, O, py, ry, type, pThick, rThick, cjSize) {
    let group = new THREE.Group();
    
    const addThickLine = (x1, z1, y1, x2, z2, y2, thk, depth, mat) => {
        let dx = x2 - x1, dy = y2 - y1, dz = z2 - z1;
        let len = Math.sqrt(dx*dx + dy*dy + dz*dz);
        if (len < 0.001) return;
        let cx = (x1 + x2)/2, cy = (y1 + y2)/2, cz = (z1 + z2)/2;
        let mesh = new THREE.Mesh(new THREE.BoxGeometry(thk, depth, len), mat);
        mesh.position.set(cx, cy, cz);
        mesh.lookAt(new THREE.Vector3(x2, y2, z2));
        group.add(mesh);
    };

    let basePts = generateShapePoints(shape, W, L);
    let edges = [];
    for(let i=0; i<basePts.length; i++) {
        let p1 = basePts[i], p2 = basePts[(i+1)%basePts.length];
        let dx = p2.x - p1.x, dy = p2.y - p1.y, len = Math.sqrt(dx*dx + dy*dy);
        let nx = dy/len, ny = -dx/len; 
        edges.push({ nx, ny });
    }
    
    let oPts = [];
    for(let i=0; i<basePts.length; i++) {
        let ePrev = edges[(i-1+edges.length)%edges.length];
        let eCurr = edges[i];
        let n1x = ePrev.nx, n1y = ePrev.ny;
        let n2x = eCurr.nx, n2y = eCurr.ny;
        let nx = n1x + n2x, ny = n1y + n2y;
        let nLen = Math.sqrt(nx*nx + ny*ny);
        if(nLen < 0.001) {
            oPts.push(new THREE.Vector2(basePts[i].x + n1x*O, basePts[i].y + n1y*O));
        } else {
            nx /= nLen; ny /= nLen;
            let dot = n1x * nx + n1y * ny;
            let dist = dot !== 0 ? O / dot : O;
            oPts.push(new THREE.Vector2(basePts[i].x + nx*dist, basePts[i].y + ny*dist));
        }
    }

    let w2 = W/2, l2 = L/2, tw = W*0.35, tl = L*0.35;
    let ceilSections = [];

    if (shape === 'rectangular') {
        ceilSections.push({ minX: -w2, maxX: w2, minZ: -l2, maxZ: l2 });
    } else if (shape === 'l_shape') {
        ceilSections.push({ minX: -w2, maxX: w2, minZ: -l2, maxZ: -l2+tl });
        ceilSections.push({ minX: -w2, maxX: -w2+tw, minZ: -l2+tl, maxZ: l2 });
    } else if (shape === 'u_shape') {
        ceilSections.push({ minX: -w2, maxX: w2, minZ: -l2, maxZ: -l2+tl });
        ceilSections.push({ minX: -w2, maxX: -w2+tw, minZ: -l2+tl, maxZ: l2 });
        ceilSections.push({ minX: w2-tw, maxX: w2, minZ: -l2+tl, maxZ: l2 });
    } else if (shape === 'h_shape') {
        ceilSections.push({ minX: -w2, maxX: -w2+tw, minZ: -l2, maxZ: l2 });
        ceilSections.push({ minX: w2-tw, maxX: w2, minZ: -l2, maxZ: l2 });
        ceilSections.push({ minX: -w2+tw, maxX: w2-tw, minZ: -tl/2, maxZ: tl/2 });
    } else if (shape === 'right_wing') {
        ceilSections.push({ minX: -w2, maxX: W*0.2, minZ: -l2, maxZ: l2 });
        ceilSections.push({ minX: W*0.2, maxX: w2, minZ: -L*0.35, maxZ: L*0.35 });
    } else if (shape === 'left_wing') {
        ceilSections.push({ minX: -W*0.2, maxX: w2, minZ: -l2, maxZ: l2 });
        ceilSections.push({ minX: -w2, maxX: -W*0.2, minZ: -L*0.35, maxZ: L*0.35 });
    } else if (shape === 'dual_wing') {
        ceilSections.push({ minX: -W*0.2, maxX: W*0.2, minZ: -l2, maxZ: l2 });
        ceilSections.push({ minX: -w2, maxX: -W*0.2, minZ: -L*0.35, maxZ: L*0.35 });
        ceilSections.push({ minX: W*0.2, maxX: w2, minZ: -L*0.35, maxZ: L*0.35 });
    } else {
        let globalMinX = Math.min(...basePts.map(p=>p.x)) - 2;
        let globalMaxX = Math.max(...basePts.map(p=>p.x)) + 2;
        let globalMinZ = Math.min(...basePts.map(p=>p.y)) - 2;
        let globalMaxZ = Math.max(...basePts.map(p=>p.y)) + 2;
        ceilSections.push({ minX: globalMinX, maxX: globalMaxX, minZ: globalMinZ, maxZ: globalMaxZ });
    }

    function buildCeilingJoistsCSG() {
        let jCSG = null;
        let joistSpacing = 16;
        ceilSections.forEach(sec => {
            let secW = sec.maxX - sec.minX;
            let secL = sec.maxZ - sec.minZ;
            let ext = 10; 
            
            if (secW < secL) {
                let jGeo = new THREE.BoxGeometry(secW + ext, cjSize, 2);
                for (let z = Math.floor(sec.minZ/joistSpacing)*joistSpacing; z <= sec.maxZ; z += joistSpacing) {
                    let jm = new THREE.Mesh(jGeo);
                    jm.position.set((sec.minX + sec.maxX)/2, py, z);
                    jm.updateMatrix();
                    let c = CSG.fromMesh(jm, 1);
                    jCSG = jCSG ? jCSG.union(c) : c;
                }
            } else {
                let jGeo = new THREE.BoxGeometry(2, cjSize, secL + ext);
                for (let x = Math.floor(sec.minX/joistSpacing)*joistSpacing; x <= sec.maxX; x += joistSpacing) {
                    let jm = new THREE.Mesh(jGeo);
                    jm.position.set(x, py, (sec.minZ + sec.maxZ)/2);
                    jm.updateMatrix();
                    let c = CSG.fromMesh(jm, 1);
                    jCSG = jCSG ? jCSG.union(c) : c;
                }
            }
        });
        return jCSG;
    }

    if (type === 'flat') {
        let roofShape = new THREE.Shape();
        oPts.forEach((p, i) => { if(i===0) roofShape.moveTo(p.x, p.y); else roofShape.lineTo(p.x, p.y); });
        let roofExtGeo = new THREE.ExtrudeGeometry(roofShape, { depth: cjSize, bevelEnabled: false });
        roofExtGeo.rotateX(Math.PI/2);
        let roofMesh = new THREE.Mesh(roofExtGeo);
        roofMesh.position.y = py + cjSize/2;
        roofMesh.updateMatrix();
        let roofCSG = CSG.fromMesh(roofMesh, 0);

        let jCSG = buildCeilingJoistsCSG();
        if (jCSG) {
            let innerShape = new THREE.Shape();
            basePts.forEach((p, i) => { if(i===0) innerShape.moveTo(p.x, p.y); else innerShape.lineTo(p.x, p.y); });
            let innerExtGeo = new THREE.ExtrudeGeometry(innerShape, { depth: cjSize * 2, bevelEnabled: false });
            innerExtGeo.rotateX(Math.PI/2);
            let innerMesh = new THREE.Mesh(innerExtGeo);
            innerMesh.position.y = py + cjSize;
            innerMesh.updateMatrix();
            let innerCSG = CSG.fromMesh(innerMesh, 0);

            try {
                let finalJoists = CSG.toMesh(jCSG.intersect(innerCSG), new THREE.Matrix4(), MATS.wood);
                finalJoists.userData.layer = 'roof';
                group.add(finalJoists);
            } catch(e) {}
        }

        for(let i=0; i<oPts.length; i++) {
            let p1 = oPts[i], p2 = oPts[(i+1)%oPts.length];
            addThickLine(p1.x, p1.y, py, p2.x, p2.y, py, pThick, cjSize, MATS.wood);
        }
        
        group.userData.layer = 'roof';
        return group;
    }

    let ridges = [], rafters = [];
    
    if (shape.startsWith('polygon')) {
        let cx = 0, cz = 0;
        basePts.forEach(p => { cx += p.x; cz += p.y; });
        cx /= basePts.length; cz /= basePts.length;

        let scale = parseFloat(document.getElementById('polyRidgeScale').value) / 100 || 0.2;
        let rPts = basePts.map(p => ({ x: cx + (p.x - cx)*scale, z: cz + (p.y - cz)*scale }));

        for(let i=0; i<oPts.length; i++) {
            let next = (i+1)%oPts.length;
            ridges.push([rPts[i], rPts[next]]);
            rafters.push([{x: oPts[i].x, z: oPts[i].y}, rPts[i]]);
        }
    }
    else if (shape === 'rectangular') {
        let rx1 = -w2-O + (l2+O), rx2 = w2+O - (l2+O);
        if(rx1 > rx2) rx1 = rx2 = 0; 
        
        if (type === 'gable') { rx1 = -w2-O; rx2 = w2+O; }
        
        let rP1 = {x: rx1, z: 0}, rP2 = {x: rx2, z: 0};
        ridges.push([rP1, rP2]);
        
        rafters.push([{x: -w2-O, z: -l2-O}, rP1], [{x: -w2-O, z: l2+O}, rP1]);
        rafters.push([{x: w2+O, z: -l2-O}, rP2], [{x: w2+O, z: l2+O}, rP2]);
    }
    else if (shape === 'l_shape') {
        let splitX = -w2+tw+O, splitZ = -l2+tl+O;
        let cxCorner = -w2 + tw/2, czCorner = -l2 + tl/2;
        let rxEnd = w2+O - (tl/2+O), rzEnd = l2+O - (tw/2+O);
        
        if (type === 'gable') { rxEnd = w2+O; rzEnd = l2+O; }
        
        let pCorner = {x: cxCorner, z: czCorner}, pTop = {x: cxCorner, z: rzEnd}, pRight = {x: rxEnd, z: czCorner};
        ridges.push([pCorner, pTop], [pCorner, pRight]);
        
        rafters.push([{x: -w2-O, z: l2+O}, pTop], [{x: splitX, z: l2+O}, pTop]);
        rafters.push([{x: w2+O, z: -l2-O}, pRight], [{x: w2+O, z: splitZ}, pRight]);
        rafters.push([{x: -w2-O, z: -l2-O}, pCorner], [{x: splitX, z: splitZ}, pCorner]);
    }
    else if (shape === 'u_shape') {
        let sL = -w2+tw+O, sR = w2-tw-O, sZ = -l2+tl+O;
        let cxL = -w2 + tw/2, cxR = w2 - tw/2, czCorner = -l2 + tl/2, rzEnd = l2+O - (tw/2+O);
        
        if (type === 'gable') rzEnd = l2+O;
        
        let pCL = {x: cxL, z: czCorner}, pCR = {x: cxR, z: czCorner};
        let pTL = {x: cxL, z: rzEnd}, pTR = {x: cxR, z: rzEnd};
        ridges.push([pCL, pTL], [pCL, pCR], [pCR, pTR]);
        
        rafters.push([{x: -w2-O, z: l2+O}, pTL], [{x: sL, z: l2+O}, pTL]);
        rafters.push([{x: w2+O, z: l2+O}, pTR], [{x: sR, z: l2+O}, pTR]);
        rafters.push([{x: -w2-O, z: -l2-O}, pCL], [{x: sL, z: sZ}, pCL]);
        rafters.push([{x: w2+O, z: -l2-O}, pCR], [{x: sR, z: sZ}, pCR]);
    }
    else if (shape === 'h_shape') {
        let sL = -w2+tw+O, sR = w2-tw-O, sT = -tl/2-O, sB = tl/2+O;
        let cxL = -w2 + tw/2, cxR = w2 - tw/2, rzT = -l2+O + (tw/2+O), rzB = l2+O - (tw/2+O);
        
        if (type === 'gable') { rzT = -l2-O; rzB = l2+O; }
        
        let pTL = {x: cxL, z: rzT}, pBL = {x: cxL, z: rzB}, pTR = {x: cxR, z: rzT}, pBR = {x: cxR, z: rzB};
        let pML = {x: cxL, z: 0}, pMR = {x: cxR, z: 0};
        ridges.push([pTL, pBL], [pTR, pBR], [pML, pMR]);
        
        rafters.push([{x: -w2-O, z: -l2-O}, pTL], [{x: sL, z: -l2-O}, pTL]);
        rafters.push([{x: -w2-O, z: l2+O}, pBL], [{x: sL, z: l2+O}, pBL]);
        rafters.push([{x: w2+O, z: -l2-O}, pTR], [{x: sR, z: -l2-O}, pTR]);
        rafters.push([{x: w2+O, z: l2+O}, pBR], [{x: sR, z: l2+O}, pBR]);
        rafters.push([{x: sL, z: sT}, pML], [{x: sL, z: sB}, pML]);
        rafters.push([{x: sR, z: sT}, pMR], [{x: sR, z: sB}, pMR]);
    }
    else if (shape === 'right_wing') {
        let cR = W*0.2;
        let sL = O, sR = cR - O, sT = -L*0.15 - O, sB = L*0.15 + O;
        let cxM = -w2/2, rzMT = -l2 + W/4, rzMB = l2 - W/4; 
        let cxW = cR + (w2-cR)/2, rzWT = -L*0.35 + (w2-cR)/2, rzWB = L*0.35 - (w2-cR)/2;
        
        if (type === 'gable') {
            rzMT = -l2-O; rzMB = l2+O;
            rzWT = -L*0.35-O; rzWB = L*0.35+O;
        }

        let pMT = {x: cxM, z: rzMT}, pMB = {x: cxM, z: rzMB};
        let pWT = {x: cxW, z: rzWT}, pWB = {x: cxW, z: rzWB};
        let pMC = {x: cxM, z: 0}, pWC = {x: cxW, z: 0};
        ridges.push([pMT, pMB], [pWT, pWB], [pMC, pWC]);
        
        rafters.push([{x: -w2-O, z: -l2-O}, pMT], [{x: sL, z: -l2-O}, pMT]);
        rafters.push([{x: -w2-O, z: l2+O}, pMB], [{x: sL, z: l2+O}, pMB]);
        rafters.push([{x: sR, z: -L*0.35-O}, pWT], [{x: w2+O, z: -L*0.35-O}, pWT]);
        rafters.push([{x: sR, z: L*0.35+O}, pWB], [{x: w2+O, z: L*0.35+O}, pWB]);
        rafters.push([{x: sL, z: sT}, pMC], [{x: sL, z: sB}, pMC]);
        rafters.push([{x: sR, z: sT}, pWC], [{x: sR, z: sB}, pWC]);
    }
    else if (shape === 'left_wing') {
        let cL = -W*0.2;
        let sL = cL + O, sR = -O, sT = -L*0.15 - O, sB = L*0.15 + O;
        let cxM = w2/2, rzMT = -l2 + W/4, rzMB = l2 - W/4; 
        let cxW = (-w2 + cL)/2, rzWT = -L*0.35 + (w2+cL)/2, rzWB = L*0.35 - (w2+cL)/2;
        
        if (type === 'gable') {
            rzMT = -l2-O; rzMB = l2+O;
            rzWT = -L*0.35-O; rzWB = L*0.35+O;
        }

        let pMT = {x: cxM, z: rzMT}, pMB = {x: cxM, z: rzMB};
        let pWT = {x: cxW, z: rzWT}, pWB = {x: cxW, z: rzWB};
        let pMC = {x: cxM, z: 0}, pWC = {x: cxW, z: 0};
        ridges.push([pMT, pMB], [pWT, pWB], [pMC, pWC]);
        
        rafters.push([{x: sR, z: -l2-O}, pMT], [{x: w2+O, z: -l2-O}, pMT]);
        rafters.push([{x: sR, z: l2+O}, pMB], [{x: w2+O, z: l2+O}, pMB]);
        rafters.push([{x: -w2-O, z: -L*0.35-O}, pWT], [{x: sL, z: -L*0.35-O}, pWT]);
        rafters.push([{x: -w2-O, z: L*0.35+O}, pWB], [{x: sL, z: L*0.35+O}, pWB]);
        rafters.push([{x: sL, z: sT}, pWC], [{x: sL, z: sB}, pWC]);
        rafters.push([{x: sR, z: sT}, pMC], [{x: sR, z: sB}, pMC]);
    }
    else if (shape === 'dual_wing') {
        let sML = -W*0.2 - O, sMR = W*0.2 + O;
        let sLL = -w2 - O, sLR = -W*0.35 + O;
        let sRL = W*0.35 - O, sRR = w2 + O;
        let sT = -L*0.15 - O, sB = L*0.15 + O;
        let cxM = 0, rzMT = -l2 + W*0.2, rzMB = l2 - W*0.2;
        let cxLW = (-w2 - W*0.35)/2, lwHW = (w2 - W*0.35)/2; 
        let rzLWT = -L*0.35 + lwHW, rzLWB = L*0.35 - lwHW;
        let cxRW = (w2 + W*0.35)/2, rwHW = (w2 - W*0.35)/2;
        let rzRWT = -L*0.35 + rwHW, rzRWB = L*0.35 - rwHW;
        
        if (type === 'gable') {
            rzMT = -l2-O; rzMB = l2+O;
            rzLWT = -L*0.35-O; rzLWB = L*0.35+O;
            rzRWT = -L*0.35-O; rzRWB = L*0.35+O;
        }

        let pMT = {x: cxM, z: rzMT}, pMB = {x: cxM, z: rzMB};
        let pLWT = {x: cxLW, z: rzLWT}, pLWB = {x: cxLW, z: rzLWB};
        let pRWT = {x: cxRW, z: rzRWT}, pRWB = {x: cxRW, z: rzRWB};
        let pLMC = {x: cxM, z: 0}, pLWC = {x: cxLW, z: 0};
        let pRMC = {x: cxM, z: 0}, pRWC = {x: cxRW, z: 0};

        ridges.push([pMT, pMB], [pLWT, pLWB], [pRWT, pRWB], [pLMC, pLWC], [pRMC, pRWC]);
        
        rafters.push([{x: sML, z: -l2-O}, pMT], [{x: sMR, z: -l2-O}, pMT]);
        rafters.push([{x: sML, z: l2+O}, pMB], [{x: sMR, z: l2+O}, pMB]);
        rafters.push([{x: sLL, z: -L*0.35-O}, pLWT], [{x: sLR, z: -L*0.35-O}, pLWT]);
        rafters.push([{x: sLL, z: L*0.35+O}, pLWB], [{x: sLR, z: L*0.35+O}, pLWB]);
        rafters.push([{x: sRL, z: -L*0.35-O}, pRWT], [{x: sRR, z: -L*0.35-O}, pRWT]);
        rafters.push([{x: sRL, z: L*0.35+O}, pRWB], [{x: sRR, z: L*0.35+O}, pRWB]);
        
        rafters.push([{x: sLR, z: sT}, pLWC], [{x: sLR, z: sB}, pLWC]);
        rafters.push([{x: sML, z: sT}, pLMC], [{x: sML, z: sB}, pLMC]);
        rafters.push([{x: sMR, z: sT}, pRMC], [{x: sMR, z: sB}, pRMC]);
        rafters.push([{x: sRL, z: sT}, pRWC], [{x: sRL, z: sB}, pRWC]);
    }

    // Helper to check if a ridge is a division beam based on coordinates
    const isDivision = (r) => {
        let midX = (r[0].x + r[1].x) / 2;
        let midZ = (r[0].z + r[1].z) / 2;
        
        // Define specific split coordinates for each shape
        if (shape === 'l_shape') {
            return (Math.abs(midX - (-w2+tw+O)) < 0.5) || (Math.abs(midZ - (-l2+tl+O)) < 0.5);
        }
        if (shape === 'u_shape') {
            return (Math.abs(midX - (-w2+tw+O)) < 0.5) || (Math.abs(midX - (w2-tw-O)) < 0.5) || (Math.abs(midZ - (-l2+tl+O)) < 0.5);
        }
        if (shape === 'h_shape') {
            return (Math.abs(midX - (-w2+tw+O)) < 0.5) || (Math.abs(midX - (w2-tw-O)) < 0.5);
        }
        return false;
    };

    // Filter and add ridges
    ridges.forEach(r => {
        if (!isDivision(r)) {
            addThickLine(r[0].x, r[0].z, ry, r[1].x, r[1].z, ry, rThick, cjSize, MATS.wood);
        }
    });

    let gableEnds = new Array(oPts.length).fill(false);
    if (type === 'gable') {
        for(let i=0; i<oPts.length; i++) {
            let p1 = oPts[i], p2 = oPts[(i+1)%oPts.length];
            let isGable = false;
            ridges.forEach(r => {
                [r[0], r[1]].forEach(pt => {
                    let d1 = Math.hypot(pt.x - p1.x, pt.z - p1.y);
                    let d2 = Math.hypot(pt.x - p2.x, pt.z - p2.y);
                    let segLen = Math.hypot(p2.x - p1.x, p2.y - p1.y);
                    if (Math.abs(d1 + d2 - segLen) < 1.0) isGable = true;
                });
            });
            gableEnds[i] = isGable;
        }
    }

    let rafterSpacing = 24;
    let pitchRatio = (parseFloat(document.getElementById('roofRise').value) || 6) / 12;
    
    let boundaries = [];
    for(let i=0; i<oPts.length; i++) boundaries.push({p1: oPts[i], p2: oPts[(i+1)%oPts.length], isPerimeter: true});
    ridges.forEach(r => boundaries.push({p1: {x: r[0].x, y: r[0].z}, p2: {x: r[1].x, y: r[1].z}, isPerimeter: false, isRidge: true}));
    rafters.forEach(r => boundaries.push({p1: {x: r[0].x, y: r[0].z}, p2: {x: r[1].x, y: r[1].z}, isPerimeter: false, isRafter: true}));

    function getRoofZ(X, Z) {
        let minH = Infinity;
        for(let i=0; i<oPts.length; i++) {
            if (gableEnds[i]) continue;
            let p1 = oPts[i], p2 = oPts[(i+1)%oPts.length];
            let dx = p2.x - p1.x, dy = p2.y - p1.y;
            let len = Math.hypot(dx, dy);
            let nx = -dy/len, ny = dx/len; 
            let dist = (X - p1.x)*nx + (Z - p1.y)*ny;
            if (dist > -0.1) {
                let h = py + dist * pitchRatio;
                if (h < minH) minH = h;
            }
        }
        return Math.min(minH, ry);
    }

    function raycast(S, N) {
        let minT = Infinity, closestPt = null, hitB = null;
        boundaries.forEach(b => {
            let dx = b.p2.x - b.p1.x, dy = b.p2.y - b.p1.y;
            let cross = N.x * dy - N.y * dx;
            if (Math.abs(cross) < 0.001) return;
            let t = ((b.p1.x - S.x) * dy - (b.p1.y - S.y) * dx) / cross;
            let u = ((b.p1.x - S.x) * N.y - (b.p1.y - S.y) * N.x) / cross;
            if (t > 0.1 && u >= -0.001 && u <= 1.001) {
                if (t < minT) { 
                    minT = t; 
                    closestPt = { x: S.x + t * N.x, y: S.y + t * N.y };
                    hitB = b;
                }
            }
        });
        return closestPt ? { pt: closestPt, b: hitB } : null;
    }

    let rDict = {};
    function addTrimmedRafter(pA, pB, bA, bB) {
        if (Math.hypot(pA.x - pB.x, pA.y - pB.y) < 1.0) return;
        
        let midX = (pA.x + pB.x) / 2, midY = (pA.y + pB.y) / 2;
        let key = Math.round(midX) + "," + Math.round(midY);
        if (rDict[key]) return;
        rDict[key] = true;

        let yA = getRoofZ(pA.x, pA.y);
        let yB = getRoofZ(pB.x, pB.y);

        const isHittingRidge = Array.isArray(bB) ? bB.some(b => b.isRidge) : (bB && bB.isRidge);
        if (isHittingRidge) {
            yB = ry;
        }

        let pA_3D = new THREE.Vector3(pA.x, yA, pA.y);
        let pB_3D = new THREE.Vector3(pB.x, yB, pB.y);

        let dir = new THREE.Vector3().subVectors(pB_3D, pA_3D);
        let len = dir.length();
        if (len < 0.001) return;
        dir.normalize();

        let ext = rThick * 2;
        let pStart = new THREE.Vector3().copy(pA_3D).addScaledVector(dir, -ext);
        let pEnd = new THREE.Vector3().copy(pB_3D).addScaledVector(dir, ext);
        let extLen = len + ext * 2;

        let mesh = new THREE.Mesh(new THREE.BoxGeometry(rThick, cjSize, extLen));
        mesh.position.copy(pStart).lerp(pEnd, 0.5);
        mesh.lookAt(pEnd);
        mesh.updateMatrix();

        let rafterCSG = null;
        try {
            rafterCSG = CSG.fromMesh(mesh, 0);
        } catch(e) {
            addThickLine(pA.x, pA.y, yA, pB.x, pB.y, yB, rThick, cjSize, MATS.wood);
            return;
        }

        const applyCut = (boundary, keepPt) => {
            if (!boundary) return;
            let dx = boundary.p2.x - boundary.p1.x;
            let dz = boundary.p2.y - boundary.p1.y;
            let bLen = Math.hypot(dx, dz);
            if (bLen < 0.001) return;
            let nx = -dz/bLen, nz = dx/bLen;
            
            if (nx * (keepPt.x - boundary.p1.x) + nz * (keepPt.y - boundary.p1.y) < 0) {
                nx = -nx; nz = -nz;
            }

            let offset = boundary.isPerimeter ? 0 : (boundary.isRidge ? 0 : rThick / 2);
            let cBox = new THREE.Mesh(new THREE.BoxGeometry(4000, 2000, 2000));
            let cx = boundary.p1.x + nx * (1000 + offset);
            let cz = boundary.p1.y + nz * (1000 + offset);
            
            cBox.position.set(cx, py, cz);
            cBox.lookAt(new THREE.Vector3(cx + nx, py, cz + nz));
            cBox.updateMatrix();

            try {
                let cutCSG = CSG.fromMesh(cBox, 1);
                rafterCSG = rafterCSG.intersect(cutCSG);
            } catch(e) {}
        };

        let boundsA = Array.isArray(bA) ? bA : (bA ? [bA] : []);
        let boundsB = Array.isArray(bB) ? bB : (bB ? [bB] : []);

        boundsA.forEach(b => applyCut(b, pB));
        boundsB.forEach(b => applyCut(b, pA));

        try {
            let finalMesh = CSG.toMesh(rafterCSG, new THREE.Matrix4(), MATS.wood);
            group.add(finalMesh);
        } catch(e) {
            addThickLine(pA.x, pA.y, yA, pB.x, pB.y, yB, rThick, cjSize, MATS.wood);
        }
    }
    
    rafters.forEach(r => {
        let pA = {x: r[0].x, y: r[0].z};
        let pB = {x: r[1].x, y: r[1].z};
        
        let dirX = pB.x - pA.x;
        let dirY = pB.y - pA.y;
        let len = Math.hypot(dirX, dirY);
        if(len < 0.001) return;
        dirX /= len; dirY /= len;

        let cutsA = [];
        let cutsB = [];

        boundaries.forEach(b => {
            if (b.isRafter) return; 
            
            let bDx = b.p2.x - b.p1.x, bDy = b.p2.y - b.p1.y;
            let bLen = Math.hypot(bDx, bDy);
            if(bLen < 0.001) return;
            
            let cross = Math.abs(dirX * (bDy/bLen) - dirY * (bDx/bLen));
            if (cross > 0.05) {
                if (Math.hypot(b.p1.x - pA.x, b.p1.y - pA.y) < 0.1 || Math.hypot(b.p2.x - pA.x, b.p2.y - pA.y) < 0.1) {
                    cutsA.push(b);
                }
                if (Math.hypot(b.p1.x - pB.x, b.p1.y - pB.y) < 0.1 || Math.hypot(b.p2.x - pB.x, b.p2.y - pB.y) < 0.1) {
                    cutsB.push(b);
                }
            }
        });

        addTrimmedRafter(pA, pB, cutsA, cutsB);
    });

    let isPoly = shape.startsWith('polygon');
    function stepAlongLine(p1, p2, isDiag, cb) {
        let dx = p2.x - p1.x, dy = p2.y - p1.y;
        let len = Math.hypot(dx, dy);
        if (len < 0.1) return;
        if (isDiag) {
            for(let d = rafterSpacing/2; d < len; d += rafterSpacing) {
                cb({ x: p1.x + (dx/len)*d, y: p1.y + (dy/len)*d });
            }
        } else if (Math.abs(dx) > Math.abs(dy)) {
            let minB = Math.min(p1.x, p2.x), maxB = Math.max(p1.x, p2.x);
            for(let x = Math.ceil(minB/rafterSpacing)*rafterSpacing; x <= maxB; x += rafterSpacing) {
                cb({ x: x, y: p1.y + ((x - p1.x)/dx)*dy });
            }
        } else {
            let minB = Math.min(p1.y, p2.y), maxB = Math.max(p1.y, p2.y);
            for(let y = Math.ceil(minB/rafterSpacing)*rafterSpacing; y <= maxB; y += rafterSpacing) {
                cb({ x: p1.x + ((y - p1.y)/dy)*dx, y: y });
            }
        }
    }

    for(let i=0; i<oPts.length; i++) {
        let p1 = oPts[i], p2 = oPts[(i+1)%oPts.length];
        
        if (gableEnds[i]) continue;
        
        addThickLine(p1.x, p1.y, py, p2.x, p2.y, py, pThick, cjSize, MATS.wood); 
        
        let dx = p2.x - p1.x, dy = p2.y - p1.y;
        let len = Math.hypot(dx, dy);
        let normX = -dy/len, normY = dx/len; 
        let startBound = {p1: p1, p2: p2, isPerimeter: true};
        
        stepAlongLine(p1, p2, isPoly, (S) => {
            let hit = raycast(S, {x: normX, y: normY});
            if (hit) addTrimmedRafter(S, hit.pt, startBound, hit.b);
            
            let soffitY = py - (cjSize / 2) + 2.25; 
            let wallEndX = S.x + normX * O;
            let wallEndZ = S.y + normY * O;
            
            addThickLine(S.x, S.y, soffitY, wallEndX, wallEndZ, soffitY, 1.5, 3.5, MATS.wood);
        });
    }

    ridges.forEach(r => {
        let p1 = {x: r[0].x, y: r[0].z}, p2 = {x: r[1].x, y: r[1].z};
        let dx = p2.x - p1.x, dy = p2.y - p1.y;
        if (Math.hypot(dx, dy) < 0.1) return;
        let n1, n2;
        if (isPoly) {
            let len = Math.hypot(dx, dy);
            n1 = {x: -dy/len, y: dx/len};
            n2 = {x: dy/len, y: -dx/len};
        } else {
            if (Math.abs(dx) > Math.abs(dy)) { n1 = {x:0, y:1}; n2 = {x:0, y:-1}; }
            else { n1 = {x:1, y:0}; n2 = {x:-1, y:0}; }
        }
        
        let startBound = {p1: p1, p2: p2, isPerimeter: false};

        stepAlongLine(p1, p2, isPoly, (S) => {
            let hit1 = raycast(S, n1);
            if (hit1) addTrimmedRafter(S, hit1.pt, startBound, hit1.b);
            let hit2 = raycast(S, n2);
            if (hit2) addTrimmedRafter(S, hit2.pt, startBound, hit2.b);
        });
    });

    let ceilingShape = new THREE.Shape();
    basePts.forEach((p, i) => { if(i===0) ceilingShape.moveTo(p.x, p.y); else ceilingShape.lineTo(p.x, p.y); });
    
    let ceilingExtGeo = new THREE.ExtrudeGeometry(ceilingShape, { depth: cjSize * 2, bevelEnabled: false });
    ceilingExtGeo.rotateX(Math.PI/2);
    let ceilingMesh = new THREE.Mesh(ceilingExtGeo);
    ceilingMesh.position.y = py + cjSize;
    ceilingMesh.updateMatrix();
    let ceilingCSG = CSG.fromMesh(ceilingMesh, 0);

    let ceilJCSG = buildCeilingJoistsCSG();
    if (ceilJCSG) {
        try {
            let finalCeilJoists = CSG.toMesh(ceilJCSG.intersect(ceilingCSG), new THREE.Matrix4(), MATS.wood);
            finalCeilJoists.userData.layer = 'roof';
            group.add(finalCeilJoists);
        } catch(e) {}
    }

    group.userData.layer = 'roof';
    return group;
}

I still need to implement the CSG subtraction for the bird’s mouth cut and add a user-adjustable parameter to control the seat cut depth. This will literally raise and lower the entire roof assembly minus the ceiling joists.

However, my issue is not necessarily the cutting of the rafter now, it is the connection point for the rafters that connect from the ridge to a hip or valley rafter at the corners, notice in this photo that some of the rafters are not connecting to the ridge but are connecting to the valley rafter:

Also same scenario here:

I’ve attached the roof generation logic above, anyone who can pinpoint this issue, is much appreciated.

I’m sorry, I’m not aware of the specific terminology, so it is hard to guess where to look. I’ve marked in red the things that do not look connected. Are they the issue?

Here is my attempt at making a valley:

https://codepen.io/boytchev/full/MYJEmMP

image

Your code is too long. Someone with enough spare time might have the chance to look at it. It would help if you prepare a minimal example that manifests the problem.

Edit:

While running your older code, my suspicion is that getRoofZ(X, Z) is not correct. I modified it and added a red sphere and black spike to see whether roof’s Y is calculated well. Here is my modification and a snapshot with the result. Wrong spheres are underlined.

function getRoofZ(X, Z) {
   :
   // your code is here
   :
   // added this:
   var spike = new THREE.Mesh(...);
   var ball = new THREE.Mesh(...);
   ball.position.set(X,Math.min(minH, ry),Z);		
   ball.add( spike );
   scenes[6].add(ball);
		
   return Math.min(minH, ry);
}

Next edit:

When you calculate the minimum, most likely one section of the roof leaks into the other. See the slope of wrong points. The blue lines are in the left section, but they adhere to the slope of the right section. And vice versa:

Next edit: I’m not sure what does gableEnds[i] mean, but it is never true for l-shape (so maybe getRoofZ is innocent and the problem comes from gableEnds?).

function getRoofZ(X, Z) {
	let minH = Infinity;
	for(let i=0; i<oPts.length; i++) {
		if (gableEnds[i]) {console.log('boom'); continue;} // never happens
		let p1 = oPts[i], p2 = oPts[(i+1)%oPts.length];
		let dx = p2.x - p1.x, dy = p2.y - p1.y;
		let len = Math.hypot(dx, dy);
		let nx = -dy/len, ny = dx/len; 
		let dist = (X - p1.x)*nx + (Z - p1.y)*ny;
		if (dist > -0.1) {
			let h = py + dist * pitchRatio;
			if (h < minH) minH = h;
		}
	}

	return Math.min(minH, ry);
}

Thanks that helped trick the ai model I was using into picking them up right..
this is correct code:

function buildRoofPlan(shape, W, L, O, py, ry, type, pThick, rThick, cjSize) {
    let group = new THREE.Group();

    // PRO ENHANCEMENT: Universal mesh setup for shadows and rendering quality
    const createProMesh = (geometry, material) => {
        let mesh = new THREE.Mesh(geometry, material);
        mesh.castShadow = true;
        mesh.receiveShadow = true;
        return mesh;
    };
    
    const addThickLine = (x1, z1, y1, x2, z2, y2, thk, depth, mat) => {
        let dx = x2 - x1, dy = y2 - y1, dz = z2 - z1;
        let len = Math.sqrt(dx*dx + dy*dy + dz*dz);
        if (len < 0.001) return;
        let cx = (x1 + x2)/2, cy = (y1 + y2)/2, cz = (z1 + z2)/2;
        let mesh = createProMesh(new THREE.BoxGeometry(thk, depth, len), mat);
        mesh.position.set(cx, cy, cz);
        mesh.lookAt(new THREE.Vector3(x2, y2, z2));
        group.add(mesh);
    };

    let basePts = generateShapePoints(shape, W, L);
    let edges = [];
    for(let i=0; i<basePts.length; i++) {
        let p1 = basePts[i], p2 = basePts[(i+1)%basePts.length];
        let dx = p2.x - p1.x, dy = p2.y - p1.y, len = Math.sqrt(dx*dx + dy*dy);
        let nx = dy/len, ny = -dx/len; 
        edges.push({ nx, ny });
    }
    
    let oPts = [];
    for(let i=0; i<basePts.length; i++) {
        let ePrev = edges[(i-1+edges.length)%edges.length];
        let eCurr = edges[i];
        let n1x = ePrev.nx, n1y = ePrev.ny;
        let n2x = eCurr.nx, n2y = eCurr.ny;
        let nx = n1x + n2x, ny = n1y + n2y;
        let nLen = Math.sqrt(nx*nx + ny*ny);
        if(nLen < 0.001) {
            oPts.push(new THREE.Vector2(basePts[i].x + n1x*O, basePts[i].y + n1y*O));
        } else {
            nx /= nLen; ny /= nLen;
            let dot = n1x * nx + n1y * ny;
            let dist = dot !== 0 ? O / dot : O;
            oPts.push(new THREE.Vector2(basePts[i].x + nx*dist, basePts[i].y + ny*dist));
        }
    }

    let w2 = W/2, l2 = L/2, tw = W*0.35, tl = L*0.35;
    let ceilSections = [];

    if (shape === 'rectangular') {
        ceilSections.push({ minX: -w2, maxX: w2, minZ: -l2, maxZ: l2 });
    } else if (shape === 'l_shape') {
        ceilSections.push({ minX: -w2, maxX: w2, minZ: -l2, maxZ: -l2+tl });
        ceilSections.push({ minX: -w2, maxX: -w2+tw, minZ: -l2+tl, maxZ: l2 });
    } else if (shape === 'u_shape') {
        ceilSections.push({ minX: -w2, maxX: w2, minZ: -l2, maxZ: -l2+tl });
        ceilSections.push({ minX: -w2, maxX: -w2+tw, minZ: -l2+tl, maxZ: l2 });
        ceilSections.push({ minX: w2-tw, maxX: w2, minZ: -l2+tl, maxZ: l2 });
    } else if (shape === 'h_shape') {
        ceilSections.push({ minX: -w2, maxX: -w2+tw, minZ: -l2, maxZ: l2 });
        ceilSections.push({ minX: w2-tw, maxX: w2, minZ: -l2, maxZ: l2 });
        ceilSections.push({ minX: -w2+tw, maxX: w2-tw, minZ: -tl/2, maxZ: tl/2 });
    } else if (shape === 'right_wing') {
        ceilSections.push({ minX: -w2, maxX: W*0.2, minZ: -l2, maxZ: l2 });
        ceilSections.push({ minX: W*0.2, maxX: w2, minZ: -L*0.35, maxZ: L*0.35 });
    } else if (shape === 'left_wing') {
        ceilSections.push({ minX: -W*0.2, maxX: w2, minZ: -l2, maxZ: l2 });
        ceilSections.push({ minX: -w2, maxX: -W*0.2, minZ: -L*0.35, maxZ: L*0.35 });
    } else if (shape === 'dual_wing') {
        ceilSections.push({ minX: -W*0.2, maxX: W*0.2, minZ: -l2, maxZ: l2 });
        ceilSections.push({ minX: -w2, maxX: -W*0.2, minZ: -L*0.35, maxZ: L*0.35 });
        ceilSections.push({ minX: W*0.2, maxX: w2, minZ: -L*0.35, maxZ: L*0.35 });
    } else {
        let globalMinX = Math.min(...basePts.map(p=>p.x)) - 2;
        let globalMaxX = Math.max(...basePts.map(p=>p.x)) + 2;
        let globalMinZ = Math.min(...basePts.map(p=>p.y)) - 2;
        let globalMaxZ = Math.max(...basePts.map(p=>p.y)) + 2;
        ceilSections.push({ minX: globalMinX, maxX: globalMaxX, minZ: globalMinZ, maxZ: globalMaxZ });
    }

    function buildCeilingJoistsCSG() {
        let jCSG = null;
        let joistSpacing = 16;
        ceilSections.forEach(sec => {
            let secW = sec.maxX - sec.minX;
            let secL = sec.maxZ - sec.minZ;
            let ext = 10; 
            
            if (secW < secL) {
                let jGeo = new THREE.BoxGeometry(secW + ext, cjSize, 2);
                for (let z = Math.floor(sec.minZ/joistSpacing)*joistSpacing; z <= sec.maxZ; z += joistSpacing) {
                    let jm = new THREE.Mesh(jGeo);
                    jm.position.set((sec.minX + sec.maxX)/2, py, z);
                    jm.updateMatrix();
                    let c = CSG.fromMesh(jm, 1);
                    jCSG = jCSG ? jCSG.union(c) : c;
                }
            } else {
                let jGeo = new THREE.BoxGeometry(2, cjSize, secL + ext);
                for (let x = Math.floor(sec.minX/joistSpacing)*joistSpacing; x <= sec.maxX; x += joistSpacing) {
                    let jm = new THREE.Mesh(jGeo);
                    jm.position.set(x, py, (sec.minZ + sec.maxZ)/2);
                    jm.updateMatrix();
                    let c = CSG.fromMesh(jm, 1);
                    jCSG = jCSG ? jCSG.union(c) : c;
                }
            }
        });
        return jCSG;
    }

    if (type === 'flat') {
        let roofShape = new THREE.Shape();
        oPts.forEach((p, i) => { if(i===0) roofShape.moveTo(p.x, p.y); else roofShape.lineTo(p.x, p.y); });
        let roofExtGeo = new THREE.ExtrudeGeometry(roofShape, { depth: cjSize, bevelEnabled: false });
        roofExtGeo.rotateX(Math.PI/2);
        let roofMesh = createProMesh(roofExtGeo, MATS.wood);
        roofMesh.position.y = py + cjSize/2;
        roofMesh.updateMatrix();
        let roofCSG = CSG.fromMesh(roofMesh, 0);

        let jCSG = buildCeilingJoistsCSG();
        if (jCSG) {
            let innerShape = new THREE.Shape();
            basePts.forEach((p, i) => { if(i===0) innerShape.moveTo(p.x, p.y); else innerShape.lineTo(p.x, p.y); });
            let innerExtGeo = new THREE.ExtrudeGeometry(innerShape, { depth: cjSize * 2, bevelEnabled: false });
            innerExtGeo.rotateX(Math.PI/2);
            let innerMesh = new THREE.Mesh(innerExtGeo);
            innerMesh.position.y = py + cjSize;
            innerMesh.updateMatrix();
            let innerCSG = CSG.fromMesh(innerMesh, 0);

            try {
                let finalJoists = CSG.toMesh(jCSG.intersect(innerCSG), new THREE.Matrix4(), MATS.wood);
                finalJoists.castShadow = true;
                finalJoists.receiveShadow = true;
                finalJoists.userData.layer = 'roof';
                group.add(finalJoists);
            } catch(e) {}
        }

        for(let i=0; i<oPts.length; i++) {
            let p1 = oPts[i], p2 = oPts[(i+1)%oPts.length];
            addThickLine(p1.x, p1.y, py, p2.x, p2.y, py, pThick, cjSize, MATS.wood);
        }
        
        group.userData.layer = 'roof';
        return group;
    }

    let ridges = [], rafters = [];
    
    if (shape.startsWith('polygon')) {
        let cx = 0, cz = 0;
        basePts.forEach(p => { cx += p.x; cz += p.y; });
        cx /= basePts.length; cz /= basePts.length;

        let scale = parseFloat(document.getElementById('polyRidgeScale').value) / 100 || 0.2;
        let rPts = basePts.map(p => ({ x: cx + (p.x - cx)*scale, z: cz + (p.y - cz)*scale }));

        for(let i=0; i<oPts.length; i++) {
            let next = (i+1)%oPts.length;
            ridges.push([rPts[i], rPts[next]]);
            rafters.push([{x: oPts[i].x, z: oPts[i].y}, rPts[i]]);
        }
    }
    else if (shape === 'rectangular') {
        let rx1 = -w2-O + (l2+O), rx2 = w2+O - (l2+O);
        if(rx1 > rx2) rx1 = rx2 = 0; 
        if (type === 'gable') { rx1 = -w2-O; rx2 = w2+O; }
        
        let rP1 = {x: rx1, z: 0}, rP2 = {x: rx2, z: 0};
        ridges.push([rP1, rP2]);
        rafters.push([{x: -w2-O, z: -l2-O}, rP1], [{x: -w2-O, z: l2+O}, rP1]);
        rafters.push([{x: w2+O, z: -l2-O}, rP2], [{x: w2+O, z: l2+O}, rP2]);
    }
    else if (shape === 'l_shape') {
        let splitX = -w2+tw+O, splitZ = -l2+tl+O;
        let cxCorner = -w2 + tw/2, czCorner = -l2 + tl/2;
        let rxEnd = w2+O - (tl/2+O), rzEnd = l2+O - (tw/2+O);
        
        if (type === 'gable') { rxEnd = w2+O; rzEnd = l2+O; }
        
        let pCorner = {x: cxCorner, z: czCorner}, pTop = {x: cxCorner, z: rzEnd}, pRight = {x: rxEnd, z: czCorner};
        ridges.push([pCorner, pTop], [pCorner, pRight]);
        rafters.push([{x: -w2-O, z: l2+O}, pTop], [{x: splitX, z: l2+O}, pTop]);
        rafters.push([{x: w2+O, z: -l2-O}, pRight], [{x: w2+O, z: splitZ}, pRight]);
        rafters.push([{x: -w2-O, z: -l2-O}, pCorner], [{x: splitX, z: splitZ}, pCorner]);
    }
    else if (shape === 'u_shape') {
        let sL = -w2+tw+O, sR = w2-tw-O, sZ = -l2+tl+O;
        let cxL = -w2 + tw/2, cxR = w2 - tw/2, czCorner = -l2 + tl/2, rzEnd = l2+O - (tw/2+O);
        
        if (type === 'gable') rzEnd = l2+O;
        
        let pCL = {x: cxL, z: czCorner}, pCR = {x: cxR, z: czCorner};
        let pTL = {x: cxL, z: rzEnd}, pTR = {x: cxR, z: rzEnd};
        ridges.push([pCL, pTL], [pCL, pCR], [pCR, pTR]);
        rafters.push([{x: -w2-O, z: l2+O}, pTL], [{x: sL, z: l2+O}, pTL]);
        rafters.push([{x: w2+O, z: l2+O}, pTR], [{x: sR, z: l2+O}, pTR]);
        rafters.push([{x: -w2-O, z: -l2-O}, pCL], [{x: sL, z: sZ}, pCL]);
        rafters.push([{x: w2+O, z: -l2-O}, pCR], [{x: sR, z: sZ}, pCR]);
    }
    else if (shape === 'h_shape') {
        let sL = -w2+tw+O, sR = w2-tw-O, sT = -tl/2-O, sB = tl/2+O;
        let cxL = -w2 + tw/2, cxR = w2 - tw/2, rzT = -l2+O + (tw/2+O), rzB = l2+O - (tw/2+O);
        
        if (type === 'gable') { rzT = -l2-O; rzB = l2+O; }
        
        let pTL = {x: cxL, z: rzT}, pBL = {x: cxL, z: rzB}, pTR = {x: cxR, z: rzT}, pBR = {x: cxR, z: rzB};
        let pML = {x: cxL, z: 0}, pMR = {x: cxR, z: 0};
        ridges.push([pTL, pBL], [pTR, pBR], [pML, pMR]);
        rafters.push([{x: -w2-O, z: -l2-O}, pTL], [{x: sL, z: -l2-O}, pTL]);
        rafters.push([{x: -w2-O, z: l2+O}, pBL], [{x: sL, z: l2+O}, pBL]);
        rafters.push([{x: w2+O, z: -l2-O}, pTR], [{x: sR, z: -l2-O}, pTR]);
        rafters.push([{x: w2+O, z: l2+O}, pBR], [{x: sR, z: l2+O}, pBR]);
        rafters.push([{x: sL, z: sT}, pML], [{x: sL, z: sB}, pML]);
        rafters.push([{x: sR, z: sT}, pMR], [{x: sR, z: sB}, pMR]);
    }
    else if (shape === 'right_wing') {
        let cR = W*0.2;
        let sL = O, sR = cR - O, sT = -L*0.15 - O, sB = L*0.15 + O;
        let cxM = -w2/2, rzMT = -l2 + W/4, rzMB = l2 - W/4; 
        let cxW = cR + (w2-cR)/2, rzWT = -L*0.35 + (w2-cR)/2, rzWB = L*0.35 - (w2-cR)/2;
        
        if (type === 'gable') {
            rzMT = -l2-O; rzMB = l2+O;
            rzWT = -L*0.35-O; rzWB = L*0.35+O;
        }

        let pMT = {x: cxM, z: rzMT}, pMB = {x: cxM, z: rzMB};
        let pWT = {x: cxW, z: rzWT}, pWB = {x: cxW, z: rzWB};
        let pMC = {x: cxM, z: 0}, pWC = {x: cxW, z: 0};
        ridges.push([pMT, pMB], [pWT, pWB], [pMC, pWC]);
        rafters.push([{x: -w2-O, z: -l2-O}, pMT], [{x: sL, z: -l2-O}, pMT]);
        rafters.push([{x: -w2-O, z: l2+O}, pMB], [{x: sL, z: l2+O}, pMB]);
        rafters.push([{x: sR, z: -L*0.35-O}, pWT], [{x: w2+O, z: -L*0.35-O}, pWT]);
        rafters.push([{x: sR, z: L*0.35+O}, pWB], [{x: w2+O, z: L*0.35+O}, pWB]);
        rafters.push([{x: sL, z: sT}, pMC], [{x: sL, z: sB}, pMC]);
        rafters.push([{x: sR, z: sT}, pWC], [{x: sR, z: sB}, pWC]);
    }
    else if (shape === 'left_wing') {
        let cL = -W*0.2;
        let sL = cL + O, sR = -O, sT = -L*0.15 - O, sB = L*0.15 + O;
        let cxM = w2/2, rzMT = -l2 + W/4, rzMB = l2 - W/4; 
        let cxW = (-w2 + cL)/2, rzWT = -L*0.35 + (w2+cL)/2, rzWB = L*0.35 - (w2+cL)/2;
        
        if (type === 'gable') {
            rzMT = -l2-O; rzMB = l2+O;
            rzWT = -L*0.35-O; rzWB = L*0.35+O;
        }

        let pMT = {x: cxM, z: rzMT}, pMB = {x: cxM, z: rzMB};
        let pWT = {x: cxW, z: rzWT}, pWB = {x: cxW, z: rzWB};
        let pMC = {x: cxM, z: 0}, pWC = {x: cxW, z: 0};
        ridges.push([pMT, pMB], [pWT, pWB], [pMC, pWC]);
        rafters.push([{x: sR, z: -l2-O}, pMT], [{x: w2+O, z: -l2-O}, pMT]);
        rafters.push([{x: sR, z: l2+O}, pMB], [{x: w2+O, z: l2+O}, pMB]);
        rafters.push([{x: -w2-O, z: -L*0.35-O}, pWT], [{x: sL, z: -L*0.35-O}, pWT]);
        rafters.push([{x: -w2-O, z: L*0.35+O}, pWB], [{x: sL, z: L*0.35+O}, pWB]);
        rafters.push([{x: sL, z: sT}, pWC], [{x: sL, z: sB}, pWC]);
        rafters.push([{x: sR, z: sT}, pMC], [{x: sR, z: sB}, pMC]);
    }
    else if (shape === 'dual_wing') {
        let sML = -W*0.2 - O, sMR = W*0.2 + O;
        let sLL = -w2 - O, sLR = -W*0.35 + O;
        let sRL = W*0.35 - O, sRR = w2 + O;
        let sT = -L*0.15 - O, sB = L*0.15 + O;
        let cxM = 0, rzMT = -l2 + W*0.2, rzMB = l2 - W*0.2;
        let cxLW = (-w2 - W*0.35)/2, lwHW = (w2 - W*0.35)/2; 
        let rzLWT = -L*0.35 + lwHW, rzLWB = L*0.35 - lwHW;
        let cxRW = (w2 + W*0.35)/2, rwHW = (w2 - W*0.35)/2;
        let rzRWT = -L*0.35 + rwHW, rzRWB = L*0.35 - rwHW;
        
        if (type === 'gable') {
            rzMT = -l2-O; rzMB = l2+O;
            rzLWT = -L*0.35-O; rzLWB = L*0.35+O;
            rzRWT = -L*0.35-O; rzRWB = L*0.35+O;
        }

        let pMT = {x: cxM, z: rzMT}, pMB = {x: cxM, z: rzMB};
        let pLWT = {x: cxLW, z: rzLWT}, pLWB = {x: cxLW, z: rzLWB};
        let pRWT = {x: cxRW, z: rzRWT}, pRWB = {x: cxRW, z: rzRWB};
        let pLMC = {x: cxM, z: 0}, pLWC = {x: cxLW, z: 0};
        let pRMC = {x: cxM, z: 0}, pRWC = {x: cxRW, z: 0};

        ridges.push([pMT, pMB], [pLWT, pLWB], [pRWT, pRWB], [pLMC, pLWC], [pRMC, pRWC]);
        rafters.push([{x: sML, z: -l2-O}, pMT], [{x: sMR, z: -l2-O}, pMT]);
        rafters.push([{x: sML, z: l2+O}, pMB], [{x: sMR, z: l2+O}, pMB]);
        rafters.push([{x: sLL, z: -L*0.35-O}, pLWT], [{x: sLR, z: -L*0.35-O}, pLWT]);
        rafters.push([{x: sLL, z: L*0.35+O}, pLWB], [{x: sLR, z: L*0.35+O}, pLWB]);
        rafters.push([{x: sRL, z: -L*0.35-O}, pRWT], [{x: sRR, z: -L*0.35-O}, pRWT]);
        rafters.push([{x: sRL, z: L*0.35+O}, pRWB], [{x: sRR, z: L*0.35+O}, pRWB]);
        rafters.push([{x: sLR, z: sT}, pLWC], [{x: sLR, z: sB}, pLWC]);
        rafters.push([{x: sML, z: sT}, pLMC], [{x: sML, z: sB}, pLMC]);
        rafters.push([{x: sMR, z: sT}, pRMC], [{x: sMR, z: sB}, pRMC]);
        rafters.push([{x: sRL, z: sT}, pRWC], [{x: sRL, z: sB}, pRWC]);
    }

    // Helper to evaluate if a coordinate perfectly intersects the primary ridges
    const isPointOnRidge = (x, z) => {
        for (let r of ridges) {
            let d1 = Math.hypot(x - r[0].x, z - r[0].z);
            let d2 = Math.hypot(x - r[1].x, z - r[1].z);
            let len = Math.hypot(r[1].x - r[0].x, r[1].z - r[0].z);
            if (Math.abs(d1 + d2 - len) < 0.1) return true;
        }
        return false;
    };

    const isDivision = (r) => {
        let midX = (r[0].x + r[1].x) / 2;
        let midZ = (r[0].z + r[1].z) / 2;
        if (shape === 'l_shape') {
            return (Math.abs(midX - (-w2+tw+O)) < 0.5) || (Math.abs(midZ - (-l2+tl+O)) < 0.5);
        }
        if (shape === 'u_shape') {
            return (Math.abs(midX - (-w2+tw+O)) < 0.5) || (Math.abs(midX - (w2-tw-O)) < 0.5) || (Math.abs(midZ - (-l2+tl+O)) < 0.5);
        }
        if (shape === 'h_shape') {
            return (Math.abs(midX - (-w2+tw+O)) < 0.5) || (Math.abs(midX - (w2-tw-O)) < 0.5);
        }
        return false;
    };

    // Filter and add ridges
    ridges.forEach(r => {
        if (!isDivision(r)) {
            addThickLine(r[0].x, r[0].z, ry, r[1].x, r[1].z, ry, rThick, cjSize, MATS.wood);
        }
    });

    let gableEnds = new Array(oPts.length).fill(false);
    if (type === 'gable') {
        for(let i=0; i<oPts.length; i++) {
            let p1 = oPts[i], p2 = oPts[(i+1)%oPts.length];
            let isGable = false;
            ridges.forEach(r => {
                [r[0], r[1]].forEach(pt => {
                    let d1 = Math.hypot(pt.x - p1.x, pt.z - p1.y);
                    let d2 = Math.hypot(pt.x - p2.x, pt.z - p2.y);
                    let segLen = Math.hypot(p2.x - p1.x, p2.y - p1.y);
                    if (Math.abs(d1 + d2 - segLen) < 1.0) isGable = true;
                });
            });
            gableEnds[i] = isGable;
        }
    }

    let rafterSpacing = 24;
    let pitchRatio = (parseFloat(document.getElementById('roofRise').value) || 6) / 12;
    
    let boundaries = [];
    for(let i=0; i<oPts.length; i++) boundaries.push({p1: oPts[i], p2: oPts[(i+1)%oPts.length], isPerimeter: true});
    ridges.forEach(r => boundaries.push({p1: {x: r[0].x, y: r[0].z}, p2: {x: r[1].x, y: r[1].z}, isPerimeter: false, isRidge: true}));
    rafters.forEach(r => boundaries.push({p1: {x: r[0].x, y: r[0].z}, p2: {x: r[1].x, y: r[1].z}, isPerimeter: false, isRafter: true}));

    function getRoofZ(X, Z) {
        let minH = Infinity;
        for(let i=0; i<oPts.length; i++) {
            if (gableEnds[i]) continue;
            let p1 = oPts[i], p2 = oPts[(i+1)%oPts.length];
            let dx = p2.x - p1.x, dy = p2.y - p1.y;
            let len = Math.hypot(dx, dy);
            let nx = -dy/len, ny = dx/len; 
            let dist = (X - p1.x)*nx + (Z - p1.y)*ny;
            if (dist > -0.1) {
                let h = py + dist * pitchRatio;
                if (h < minH) minH = h;
            }
        }
        return Math.min(minH, ry);
    }

    function raycast(S, N) {
        let minT = Infinity, closestPt = null, hitB = null;
        boundaries.forEach(b => {
            let dx = b.p2.x - b.p1.x, dy = b.p2.y - b.p1.y;
            let cross = N.x * dy - N.y * dx;
            if (Math.abs(cross) < 0.001) return;
            let t = ((b.p1.x - S.x) * dy - (b.p1.y - S.y) * dx) / cross;
            let u = ((b.p1.x - S.x) * N.y - (b.p1.y - S.y) * N.x) / cross;
            if (t > 0.1 && u >= -0.001 && u <= 1.001) {
                if (t < minT) { 
                    minT = t; 
                    closestPt = { x: S.x + t * N.x, y: S.y + t * N.y };
                    hitB = b;
                }
            }
        });
        return closestPt ? { pt: closestPt, b: hitB } : null;
    }

    let rDict = {};
    function addTrimmedRafter(pA, pB, bA, bB) {
        if (Math.hypot(pA.x - pB.x, pA.y - pB.y) < 1.0) return;
        
        let midX = (pA.x + pB.x) / 2, midY = (pA.y + pB.y) / 2;
        let key = Math.round(midX) + "," + Math.round(midY);
        if (rDict[key]) return;
        rDict[key] = true;

        // Force correct height alignment along the valley/hip planes
        const getTrueHeight = (pt, bounds, defaultY) => {
            if (!bounds) return defaultY;
            let bArr = Array.isArray(bounds) ? bounds : [bounds];
            for (let b of bArr) {
                if (b.isRidge) return ry;
                if (b.isRafter) { // Hip or Valley intersection
                    let isP1Ridge = isPointOnRidge(b.p1.x, b.p1.y);
                    let isP2Ridge = isPointOnRidge(b.p2.x, b.p2.y);
                    let h1 = isP1Ridge ? ry : py;
                    let h2 = isP2Ridge ? ry : py;
                    let dTot = Math.hypot(b.p2.x - b.p1.x, b.p2.y - b.p1.y);
                    let dPt = Math.hypot(pt.x - b.p1.x, pt.y - b.p1.y);
                    if (dTot > 0.001) return h1 + (h2 - h1) * (dPt / dTot);
                }
            }
            return defaultY;
        };

        let yA = getRoofZ(pA.x, pA.y);
        let yB = getRoofZ(pB.x, pB.y);

        if (isPointOnRidge(pA.x, pA.y)) yA = ry;
        if (isPointOnRidge(pB.x, pB.y)) yB = ry;

        yA = getTrueHeight(pA, bA, yA);
        yB = getTrueHeight(pB, bB, yB);

        let pA_3D = new THREE.Vector3(pA.x, yA, pA.y);
        let pB_3D = new THREE.Vector3(pB.x, yB, pB.y);

        let dir = new THREE.Vector3().subVectors(pB_3D, pA_3D);
        let len = dir.length();
        if (len < 0.001) return;
        dir.normalize();

        let ext = rThick * 2;
        let pStart = new THREE.Vector3().copy(pA_3D).addScaledVector(dir, -ext);
        let pEnd = new THREE.Vector3().copy(pB_3D).addScaledVector(dir, ext);
        let extLen = len + ext * 2;

        let mesh = new THREE.Mesh(new THREE.BoxGeometry(rThick, cjSize, extLen));
        mesh.position.copy(pStart).lerp(pEnd, 0.5);
        mesh.lookAt(pEnd);
        mesh.updateMatrix();

        let rafterCSG = null;
        try {
            rafterCSG = CSG.fromMesh(mesh, 0);
        } catch(e) {
            addThickLine(pA.x, pA.y, yA, pB.x, pB.y, yB, rThick, cjSize, MATS.wood);
            return;
        }

        const applyCut = (boundary, keepPt) => {
            if (!boundary) return;
            let dx = boundary.p2.x - boundary.p1.x;
            let dz = boundary.p2.y - boundary.p1.y;
            let bLen = Math.hypot(dx, dz);
            if (bLen < 0.001) return;
            let nx = -dz/bLen, nz = dx/bLen;
            
            if (nx * (keepPt.x - boundary.p1.x) + nz * (keepPt.y - boundary.p1.y) < 0) {
                nx = -nx; nz = -nz;
            }

            let offset = boundary.isPerimeter ? 0 : (boundary.isRidge ? 0 : rThick / 2);
            let cBox = new THREE.Mesh(new THREE.BoxGeometry(4000, 2000, 2000));
            let cx = boundary.p1.x + nx * (1000 + offset);
            let cz = boundary.p1.y + nz * (1000 + offset);
            
            cBox.position.set(cx, py, cz);
            cBox.lookAt(new THREE.Vector3(cx + nx, py, cz + nz));
            cBox.updateMatrix();

            try {
                let cutCSG = CSG.fromMesh(cBox, 1);
                rafterCSG = rafterCSG.intersect(cutCSG);
            } catch(e) {}
        };

        let boundsA = Array.isArray(bA) ? bA : (bA ? [bA] : []);
        let boundsB = Array.isArray(bB) ? bB : (bB ? [bB] : []);

        boundsA.forEach(b => applyCut(b, pB));
        boundsB.forEach(b => applyCut(b, pA));

        try {
            let finalMesh = CSG.toMesh(rafterCSG, new THREE.Matrix4(), MATS.wood);
            finalMesh.castShadow = true;
            finalMesh.receiveShadow = true;
            group.add(finalMesh);
        } catch(e) {
            addThickLine(pA.x, pA.y, yA, pB.x, pB.y, yB, rThick, cjSize, MATS.wood);
        }
    }
    
    rafters.forEach(r => {
        let pA = {x: r[0].x, y: r[0].z};
        let pB = {x: r[1].x, y: r[1].z};
        
        let dirX = pB.x - pA.x;
        let dirY = pB.y - pA.y;
        let len = Math.hypot(dirX, dirY);
        if(len < 0.001) return;
        dirX /= len; dirY /= len;

        let cutsA = [];
        let cutsB = [];

        boundaries.forEach(b => {
            if (b.isRafter) return; 
            
            let bDx = b.p2.x - b.p1.x, bDy = b.p2.y - b.p1.y;
            let bLen = Math.hypot(bDx, bDy);
            if(bLen < 0.001) return;
            
            let cross = Math.abs(dirX * (bDy/bLen) - dirY * (bDx/bLen));
            if (cross > 0.05) {
                if (Math.hypot(b.p1.x - pA.x, b.p1.y - pA.y) < 0.1 || Math.hypot(b.p2.x - pA.x, b.p2.y - pA.y) < 0.1) {
                    cutsA.push(b);
                }
                if (Math.hypot(b.p1.x - pB.x, b.p1.y - pB.y) < 0.1 || Math.hypot(b.p2.x - pB.x, b.p2.y - pB.y) < 0.1) {
                    cutsB.push(b);
                }
            }
        });

        addTrimmedRafter(pA, pB, cutsA, cutsB);
    });

    let isPoly = shape.startsWith('polygon');
    function stepAlongLine(p1, p2, isDiag, cb) {
        let dx = p2.x - p1.x, dy = p2.y - p1.y;
        let len = Math.hypot(dx, dy);
        if (len < 0.1) return;
        if (isDiag) {
            for(let d = rafterSpacing/2; d < len; d += rafterSpacing) {
                cb({ x: p1.x + (dx/len)*d, y: p1.y + (dy/len)*d });
            }
        } else if (Math.abs(dx) > Math.abs(dy)) {
            let minB = Math.min(p1.x, p2.x), maxB = Math.max(p1.x, p2.x);
            for(let x = Math.ceil(minB/rafterSpacing)*rafterSpacing; x <= maxB; x += rafterSpacing) {
                cb({ x: x, y: p1.y + ((x - p1.x)/dx)*dy });
            }
        } else {
            let minB = Math.min(p1.y, p2.y), maxB = Math.max(p1.y, p2.y);
            for(let y = Math.ceil(minB/rafterSpacing)*rafterSpacing; y <= maxB; y += rafterSpacing) {
                cb({ x: p1.x + ((y - p1.y)/dy)*dx, y: y });
            }
        }
    }

    for(let i=0; i<oPts.length; i++) {
        let p1 = oPts[i], p2 = oPts[(i+1)%oPts.length];
        if (gableEnds[i]) continue;
        
        addThickLine(p1.x, p1.y, py, p2.x, p2.y, py, pThick, cjSize, MATS.wood); 
        
        let dx = p2.x - p1.x, dy = p2.y - p1.y;
        let len = Math.hypot(dx, dy);
        let normX = -dy/len, normY = dx/len; 
        let startBound = {p1: p1, p2: p2, isPerimeter: true};
        
        stepAlongLine(p1, p2, isPoly, (S) => {
            let hit = raycast(S, {x: normX, y: normY});
            if (hit) addTrimmedRafter(S, hit.pt, startBound, hit.b);
            
            let soffitY = py - (cjSize / 2) + 2.25; 
            let wallEndX = S.x + normX * O;
            let wallEndZ = S.y + normY * O;
            
            addThickLine(S.x, S.y, soffitY, wallEndX, wallEndZ, soffitY, 1.5, 3.5, MATS.wood);
        });
    }

    ridges.forEach(r => {
        let p1 = {x: r[0].x, y: r[0].z}, p2 = {x: r[1].x, y: r[1].z};
        let dx = p2.x - p1.x, dy = p2.y - p1.y;
        if (Math.hypot(dx, dy) < 0.1) return;
        let n1, n2;
        if (isPoly) {
            let len = Math.hypot(dx, dy);
            n1 = {x: -dy/len, y: dx/len};
            n2 = {x: dy/len, y: -dx/len};
        } else {
            if (Math.abs(dx) > Math.abs(dy)) { n1 = {x:0, y:1}; n2 = {x:0, y:-1}; }
            else { n1 = {x:1, y:0}; n2 = {x:-1, y:0}; }
        }
        
        let startBound = {p1: p1, p2: p2, isPerimeter: false};

        stepAlongLine(p1, p2, isPoly, (S) => {
            let hit1 = raycast(S, n1);
            if (hit1) addTrimmedRafter(S, hit1.pt, startBound, hit1.b);
            let hit2 = raycast(S, n2);
            if (hit2) addTrimmedRafter(S, hit2.pt, startBound, hit2.b);
        });
    });

    let ceilingShape = new THREE.Shape();
    basePts.forEach((p, i) => { if(i===0) ceilingShape.moveTo(p.x, p.y); else ceilingShape.lineTo(p.x, p.y); });
    
    let ceilingExtGeo = new THREE.ExtrudeGeometry(ceilingShape, { depth: cjSize * 2, bevelEnabled: false });
    ceilingExtGeo.rotateX(Math.PI/2);
    let ceilingMesh = createProMesh(ceilingExtGeo, MATS.wood);
    ceilingMesh.position.y = py + cjSize;
    ceilingMesh.updateMatrix();
    let ceilingCSG = CSG.fromMesh(ceilingMesh, 0);

    let ceilJCSG = buildCeilingJoistsCSG();
    if (ceilJCSG) {
        try {
            let finalCeilJoists = CSG.toMesh(ceilJCSG.intersect(ceilingCSG), new THREE.Matrix4(), MATS.wood);
            finalCeilJoists.castShadow = true;
            finalCeilJoists.receiveShadow = true;
            finalCeilJoists.userData.layer = 'roof';
            group.add(finalCeilJoists);
        } catch(e) {}
    }

    group.userData.layer = 'roof';
    return group;
}

I managed to fix the brick veneer so it’s now functioning and visible. I see an extruded wall for the brick veneer, but I have no CSG subtraction applied to it, I’m looking for proper methods for applying CSG subtraction to a brick veneer wall.

Also, I have created an algorithm that will automatically create girders at a 15’-0" span for the floors, so now I have an automatic foundation generator (this needs a little work for user to be able to customize but we’ll leave it alone for now)

Note in this image the new brick veneer wall:

Brick veneer on L shape Layout - ISO View:

I have isolated the brick veneer to just this snippet, however other snippets for cutting are beneath it, we want to find a way to apply the cuts to the brick veneer without breaking it

  // --- BRICK VENEER (NO CSG YET) ---

        if (showVeneer && !w.isInt && w.story === 1) {

            let vOffset = (w.thick / 2) + veneerGap + (veneerW / 2);

            let vX = w.midX - w.nX * vOffset;

            let vZ = w.midZ - w.nZ * vOffset;

            let vLen = w.len + w.thick + 2 * (veneerGap + veneerW);

            

            // Calculate height up to the bottom of the fascia (soffit height)

            let soffitY = py - (cjSize / 2) + 2.25; 

            let vH = soffitY;

            

            let vGeo = new THREE.BoxGeometry(vLen, vH, veneerW);

            let vMesh = new THREE.Mesh(vGeo, MATS.brick);

            vMesh.position.set(vX, vH / 2, vZ);

            vMesh.rotation.y = -w.angle;

            vMesh.userData.layer = 'wall';

            bldg.add(vMesh);

            extraGeoms.push(vGeo);

        }

window.updateModel = () => {
    // --- VENEER & UI INPUTS ---
    const showVeneer = document.getElementById('brickVeneerToggle')?.checked || false;
    const veneerW = parseFloat(document.getElementById('veneerW')?.value) || 3.25;
    const veneerGap = parseFloat(document.getElementById('veneerGap')?.value) || 1.0;

    extraGeoms.forEach(g => g.dispose()); extraMats.forEach(m => m.dispose());
    extraGeoms = []; extraMats = [];
    viewList.forEach(v => { let g = scenes[v].children.filter(c => c.name==='bldg'); g.forEach(x => scenes[v].remove(x)); });
    
    activeWallCenters = [];
    const bldg = new THREE.Group(); bldg.name = 'bldg';
    
    const mW = parseFloat(document.getElementById('bldgW')?.value)||480; 
    const mL = parseFloat(document.getElementById('bldgL')?.value)||360; 
    const wH = parseFloat(document.getElementById('wallH')?.value)||120;
    const wT = parseFloat(document.getElementById('wallT')?.value)||8;
    const ff2G = parseFloat(document.getElementById('ffToGrade')?.value)||24;
    const foundW = parseFloat(document.getElementById('foundW')?.value)||10;
    const footW = parseFloat(document.getElementById('footW')?.value)||16;
    const overhang = parseFloat(document.getElementById('roofOverhang')?.value)||24;
    const footD = 12; 
    
    const ns = parseInt(document.getElementById('numStories')?.value) || 1;
    const sfThk = parseFloat(document.getElementById('sfThk')?.value) || 1.5;
    const cjSize = parseFloat(document.getElementById('cjSize')?.value) || 11.25;
    const showPlanes = document.getElementById('showPlanes')?.checked;
    
    const sillThk = parseFloat(document.getElementById('sillThk')?.value) || 1.5;
    const sillW = parseFloat(document.getElementById('sillW')?.value) || 5.5;

    let fjSizes = [];
    for(let i=1; i<=ns; i++) {
        fjSizes.push(parseFloat(document.getElementById('fjSize'+i)?.value || 11.25));
    }
    
    let storyBases = [ff2G];
    for(let i=1; i<ns; i++) {
        storyBases.push(storyBases[i-1] + wH + fjSizes[i] + sfThk);
    }

    const roofTypeInput = document.querySelector('input[name="roofType"]:checked');
    const roofType = roofTypeInput ? roofTypeInput.value : 'gable';
    const pitch = parseFloat(document.getElementById('roofRise')?.value) || 6;
    const rThick = parseFloat(document.getElementById('roofRidgeThick')?.value) || 1.5;
    const fThick = parseFloat(document.getElementById('roofFlatThick')?.value) || 1.5;

    let shape = document.getElementById('bldgShape')?.value || 'rectangular';
    let isPoly = shape.startsWith('polygon');
    let pts = generateShapePoints(shape, mW, mL);
    activeWallCount = pts.length;
    
    // 1. BASE EXTERIOR WALLS
    let baseExtWalls = [];
    for(let i=0; i<activeWallCount; i++) {
        let p1 = pts[i], p2 = pts[(i+1)%activeWallCount];
        let dx = p2.x - p1.x, dz = p2.y - p1.y, len = Math.sqrt(dx*dx + dz*dz);
        baseExtWalls.push({ id: (i+1).toString(), len: len, angle: Math.atan2(dz, dx), midX: (p1.x+p2.x)/2, midZ: (p1.y+p2.y)/2, nX: -dz/len, nZ: dx/len, thick: wT, isInt: false });
    }

    // 2. BASE INTERIOR WALLS
    let baseIntWalls = [];
    let unresolvedInt = [...interiorWallsData];
    let loopCount = 0;
    while(unresolvedInt.length > 0 && loopCount < 1000) {
        let iw = unresolvedInt.shift();
        let anchorFloor = null;
        let anchor = null;

        let matchExt = iw.anchorId.match(/^(\d+)\./);
        let matchInt = iw.anchorId.match(/^A(\d+)\./);

        if (matchExt) {
            anchorFloor = parseInt(matchExt[1]);
            let extId = iw.anchorId.split('.')[1];
            anchor = baseExtWalls.find(w => w.id === extId);
        } else if (matchInt) {
            let baseId = iw.anchorId.split('_F')[0];
            anchor = baseIntWalls.find(w => w.id === baseId);
            if (anchor) {
                if (iw.anchorId.includes('_F')) {
                    anchorFloor = parseInt(iw.anchorId.split('_F')[1]);
                } else {
                    anchorFloor = anchor.floor;
                }
            }
        }
        
        if(anchor) {
            let localX = -anchor.len/2 + anchor.len*(iw.pos/100);
            let startX = anchor.midX + localX*Math.cos(anchor.angle) + anchor.nX*(anchor.thick/2);
            let startZ = anchor.midZ + localX*Math.sin(anchor.angle) + anchor.nZ*(anchor.thick/2);
            
            let iAng = Math.atan2(anchor.nZ, anchor.nX) + ((iw.angle||0) * Math.PI / 180);
            let length = iw.length;
            
            if (iw.isSnapSegment && iw.snapId && iw.snapId !== 'none') {
                let target = null;
                let snapMatchExt = iw.snapId.match(/^(\d+)\./);
                if (snapMatchExt) {
                    let snapExtId = iw.snapId.split('.')[1];
                    target = baseExtWalls.find(w => w.id === snapExtId);
                } else {
                    let baseSnapId = iw.snapId.split('_F')[0];
                    target = baseIntWalls.find(w => w.id === baseSnapId);
                }

                if (target) {
                    let dx = startX - target.midX;
                    let dz = startZ - target.midZ;
                    let dist = dx * target.nX + dz * target.nZ;
                    let absDist = Math.abs(dist) - (target.thick / 2) + (iw.thick / 2);
                    length = Math.max(1, absDist); 
                    let sign = dist >= 0 ? -1 : 1;
                    let dirX = sign * target.nX;
                    let dirZ = sign * target.nZ;
                    iAng = Math.atan2(dirZ, dirX);
                } else {
                    unresolvedInt.push(iw); 
                    loopCount++;
                    continue;
                }
            }
            let endX = startX + Math.cos(iAng)*length;
            let endZ = startZ + Math.sin(iAng)*length;
            
            baseIntWalls.push({ 
                id: iw.id, floor: anchorFloor, copyUp: iw.copyUp,
                len: length, angle: iAng, midX: (startX+endX)/2, midZ: (startZ+endZ)/2, 
                nX: Math.cos(iAng+Math.PI/2), nZ: Math.sin(iAng+Math.PI/2), 
                thick: iw.thick, isInt: true, eX: endX, eZ: endZ, color: iw.color 
            });
        } else {
            unresolvedInt.push(iw); 
        }
        loopCount++;
    }

    // 3. FLATTEN TO ALL WALLS BY STORY
    let allWalls = [];
    for(let s=1; s<=ns; s++) {
        let sBase = storyBases[s-1];
        let jY = storyBases[s-1] - sfThk - fjSizes[s-1]; 

        baseExtWalls.forEach(bw => {
            allWalls.push({ ...bw, fullId: s + '.' + bw.id, story: s, sBase, jY });
        });
        baseIntWalls.forEach(bw => {
            if (s === bw.floor) {
                allWalls.push({ ...bw, fullId: bw.id, story: s, sBase, jY });
            } else if (bw.copyUp && s > bw.floor) {
                let copiedId = bw.id + '_F' + s;
                allWalls.push({ ...bw, fullId: copiedId, story: s, sBase, jY });
            }
        });
    }
    window.allWallsContext = allWalls;

    let floorData = getFloorRectangles(shape, mW, mL, wT);
    let floorRects = floorData.rects;
    let baseGirders = floorData.girders;
    let topOfFound = Math.max(1, ff2G - sfThk - fjSizes[0] - sillThk); 

    let floorBoundCSG = null;
    if(isPoly) {
        let floorShape = new THREE.Shape();
        pts.forEach((p, i) => { if(i===0) floorShape.moveTo(p.x, p.y); else floorShape.lineTo(p.x, p.y); });
        // Depth increased heavily to cover 2-story buildings passing CSG intersection flawlessly
        let floorBoundGeo = new THREE.ExtrudeGeometry(floorShape, { depth: 1000, bevelEnabled: false });
        floorBoundGeo.rotateX(Math.PI/2);
        let floorBoundMesh = new THREE.Mesh(floorBoundGeo);
        floorBoundMesh.position.y = 500;
        floorBoundMesh.updateMatrix();
        floorBoundCSG = CSG.fromMesh(floorBoundMesh, 0);
        extraGeoms.push(floorBoundGeo);
    }
    
    for(let s=0; s<ns; s++) {
        let curJDepth = fjSizes[s];
        let sfY = storyBases[s] - sfThk/2;
        let jY = storyBases[s] - sfThk - curJDepth/2;

        floorRects.forEach(rect => {
            let sfGeo = new THREE.BoxGeometry(rect.rw, sfThk, rect.rl);
            let sfMesh = new THREE.Mesh(sfGeo, MATS.subfloor); sfMesh.position.set(rect.cx, sfY, rect.cz);
            if(isPoly) { sfMesh.updateMatrix(); sfMesh = CSG.toMesh(CSG.fromMesh(sfMesh, 0).intersect(floorBoundCSG), sfMesh.matrix, MATS.subfloor); }
            sfMesh.userData.layer = 'subfloor'; bldg.add(sfMesh);
            
            let isXSpan = rect.rw < rect.rl;
            let jSpacing = 16;
            let jCSG = null;
            
            if (isXSpan) {
                let numJoists = Math.floor(rect.rl / jSpacing);
                let startZ = rect.cz - rect.rl/2 + jSpacing/2;
                let jGeo = new THREE.BoxGeometry(rect.rw, curJDepth, 2);
                for(let i=0; i<=numJoists; i++) {
                    let jz = startZ + i*jSpacing; if (jz > rect.cz + rect.rl/2) break;
                    let jMesh = new THREE.Mesh(jGeo, MATS.wood); jMesh.position.set(rect.cx, jY, jz);
                    if(isPoly) { jMesh.updateMatrix(); let csg = CSG.fromMesh(jMesh, i); jCSG = jCSG ? jCSG.union(csg) : csg; } 
                    else { jMesh.userData.layer = 'joist'; bldg.add(jMesh); }
                }
            } else {
                let numJoists = Math.floor(rect.rw / jSpacing);
                let startX = rect.cx - rect.rw/2 + jSpacing/2;
                let jGeo = new THREE.BoxGeometry(2, curJDepth, rect.rl);
                for(let i=0; i<=numJoists; i++) {
                    let jx = startX + i*jSpacing; if (jx > rect.cx + rect.rw/2) break;
                    let jMesh = new THREE.Mesh(jGeo, MATS.wood); jMesh.position.set(jx, jY, rect.cz);
                    if(isPoly) { jMesh.updateMatrix(); let csg = CSG.fromMesh(jMesh, i); jCSG = jCSG ? jCSG.union(csg) : csg; } 
                    else { jMesh.userData.layer = 'joist'; bldg.add(jMesh); }
                }
            }
            
            if(isPoly && jCSG) {
                let finalJ = CSG.toMesh(jCSG.intersect(floorBoundCSG), new THREE.Matrix4(), MATS.wood);
                finalJ.userData.layer = 'joist'; bldg.add(finalJ);
            }
        });

        baseGirders.forEach(g => {
            let dx = g.x2 - g.x1;
            let dz = g.z2 - g.z1;
            let len = Math.hypot(dx, dz);
            if (len < 0.1) return;
            let cx = (g.x1 + g.x2)/2;
            let cz = (g.z1 + g.z2)/2;
            let ang = Math.atan2(dz, dx);
            
            let gW = 5.5; 
            let gY = (s === 0) ? jY - curJDepth : jY; 
            
            let gGeo = new THREE.BoxGeometry(len, curJDepth, gW);
            let gMesh = new THREE.Mesh(gGeo, MATS.wood);
            gMesh.position.set(cx, gY, cz);
            gMesh.rotation.y = -ang;
            
            if (isPoly) {
                gMesh.updateMatrix();
                let gCSG = CSG.fromMesh(gMesh, 0).intersect(floorBoundCSG);
                gMesh = CSG.toMesh(gCSG, gMesh.matrix, MATS.wood);
            }
            gMesh.userData.layer = 'joist';
            bldg.add(gMesh);

            // Set piers directly underneath girders at no more than 8' OC for the first floor
            if (s === 0) {
                let numPiers = Math.ceil(len / 96);
                if (numPiers > 0) {
                    let spacing = len / numPiers;
                    for(let p=1; p<numPiers; p++) { 
                        let px = g.x1 + (dx/len) * (p*spacing);
                        let pz = g.z1 + (dz/len) * (p*spacing);
                        
                        let pierTop = gY - curJDepth/2;
                        let pierH = pierTop - (-footD);
                        let pierGeo = new THREE.BoxGeometry(16, pierH, 16);
                        let pierMesh = new THREE.Mesh(pierGeo, MATS.concrete);
                        // Shift half-height to accurately bridge distance from base of girder to bottom of footing (-12")
                        pierMesh.position.set(px, -footD + pierH/2, pz);
                        pierMesh.userData.layer = 'foundation';
                        bldg.add(pierMesh);
                    }
                }
            }
        });
    }

    let rH = 0;
    if (roofType !== 'flat') {
        let span = mW;
        if (shape.startsWith('polygon')) span = (parseFloat(document.getElementById('polyRadius')?.value)||240) * 2;
        else if (shape === 'rectangular') span = Math.min(mW, mL);
        else span = mW * 0.35; 
        
        rH = (span / 2 + overhang) * (pitch / 12);
        let ridgeHgtInput = document.getElementById('ridgeHgt');
        if(ridgeHgtInput) ridgeHgtInput.value = rH.toFixed(1) + '"';
    } else {
        let ridgeHgtInput = document.getElementById('ridgeHgt');
        if(ridgeHgtInput) ridgeHgtInput.value = '-';
    }
    
    // Top of wall height for the uppermost story
    let py = storyBases[ns-1] + wH;
    let ry = py + rH;
    
    let customRoofPlan = buildRoofPlan(shape, mW, mL, overhang, py, ry, roofType, fThick, rThick, cjSize);
    bldg.add(customRoofPlan);

    let masterWallCSG = null;
    let masterFoundCSG = null;

    allWalls.forEach((w, idx) => {
        activeWallCenters.push({ pos: new THREE.Vector3(w.midX, w.sBase + wH + 10, w.midZ), label: w.fullId });
        
        // Foundation logic is strictly for the first story
        if(w.story === 1) {
            if(!w.isInt) {
                let fShift = (foundW/2) - (w.thick/2), cx = w.midX + w.nX*fShift, cz = w.midZ + w.nZ*fShift;
                let fwGeo = new THREE.BoxGeometry(w.len + foundW, topOfFound, foundW); let fwMesh = new THREE.Mesh(fwGeo, MATS.concrete);
                fwMesh.position.set(cx, topOfFound/2, cz); fwMesh.rotation.y = -w.angle; fwMesh.updateMatrix();

                let ftGeo = new THREE.BoxGeometry(w.len + footW, footD, footW); let ftMesh = new THREE.Mesh(ftGeo, MATS.concrete);
                ftMesh.position.set(cx, -footD/2, cz); ftMesh.rotation.y = -w.angle; ftMesh.updateMatrix();
                
                let foundBlockCSG = CSG.fromMesh(fwMesh, 0).union(CSG.fromMesh(ftMesh, 0));
                masterFoundCSG = masterFoundCSG ? masterFoundCSG.union(foundBlockCSG) : foundBlockCSG;
                extraGeoms.push(fwGeo, ftGeo);
            } else {
                let pGeo = new THREE.BoxGeometry(foundW, topOfFound, foundW); let pMesh = new THREE.Mesh(pGeo, MATS.concrete); pMesh.position.set(w.eX, topOfFound/2, w.eZ); pMesh.updateMatrix();
                let pfGeo = new THREE.BoxGeometry(footW, footD, footW); let pfMesh = new THREE.Mesh(pfGeo, MATS.concrete); pfMesh.position.set(w.eX, -footD/2, w.eZ); pfMesh.updateMatrix();
                
                let fIntBlockCSG = CSG.fromMesh(pMesh, 0).union(CSG.fromMesh(pfMesh, 0));
                masterFoundCSG = masterFoundCSG ? masterFoundCSG.union(fIntBlockCSG) : fIntBlockCSG;
                extraGeoms.push(pGeo, pfGeo);
            }
        }

        if(!w.isInt) {
            let rjShift = 1 - (w.thick/2); 
            let rjLen = w.len + w.thick; // flush at corners
            let rjGeo = new THREE.BoxGeometry(rjLen, fjSizes[w.story-1], 2); 
            let rjMesh = new THREE.Mesh(rjGeo, MATS.wood);
            let rjY = w.jY + fjSizes[w.story-1]/2; // Picked up by half their size
            rjMesh.position.set(w.midX + w.nX * rjShift, rjY, w.midZ + w.nZ * rjShift); 
            rjMesh.rotation.y = -w.angle; 
            rjMesh.userData.layer = 'joist'; 
            bldg.add(rjMesh);
            extraGeoms.push(rjGeo);

            if (w.story === 1) {
                let spShift = (sillW/2) - (w.thick/2); 
                let spLen = w.len + w.thick; // flush at corners
                let spGeo = new THREE.BoxGeometry(spLen, sillThk, sillW);
                let spMesh = new THREE.Mesh(spGeo, MATS.wood);
                
                let spY = topOfFound + sillThk/2; 
                spMesh.position.set(w.midX + w.nX * spShift, spY, w.midZ + w.nZ * spShift);
                spMesh.rotation.y = -w.angle;
                spMesh.userData.layer = 'joist';
                bldg.add(spMesh);
                extraGeoms.push(spGeo);
            }
        } else {
            let djGeo = new THREE.BoxGeometry(w.len, fjSizes[w.story-1], 3.0); 
            let djMesh = new THREE.Mesh(djGeo, MATS.wood);
            djMesh.position.set(w.midX, w.jY, w.midZ);
            djMesh.rotation.y = -w.angle;
            djMesh.userData.layer = 'joist';
            bldg.add(djMesh);
            extraGeoms.push(djGeo);
        }

        let wallGeo = new THREE.BoxGeometry(w.len + (w.isInt ? w.thick : w.thick), wH, w.thick);
        let wallMesh = new THREE.Mesh(wallGeo, MATS.wall); 
        wallMesh.position.set(w.midX, w.sBase + wH/2, w.midZ); 
        wallMesh.rotation.y = -w.angle; 
        wallMesh.updateMatrix();
        let wallCSG = CSG.fromMesh(wallMesh, idx);
        extraGeoms.push(wallGeo);

  // --- BRICK VENEER (NO CSG YET) ---

        if (showVeneer && !w.isInt && w.story === 1) {

            let vOffset = (w.thick / 2) + veneerGap + (veneerW / 2);

            let vX = w.midX - w.nX * vOffset;

            let vZ = w.midZ - w.nZ * vOffset;

            let vLen = w.len + w.thick + 2 * (veneerGap + veneerW);

            

            // Calculate height up to the bottom of the fascia (soffit height)

            let soffitY = py - (cjSize / 2) + 2.25; 

            let vH = soffitY;

            

            let vGeo = new THREE.BoxGeometry(vLen, vH, veneerW);

            let vMesh = new THREE.Mesh(vGeo, MATS.brick);

            vMesh.position.set(vX, vH / 2, vZ);

            vMesh.rotation.y = -w.angle;

            vMesh.userData.layer = 'wall';

            bldg.add(vMesh);

            extraGeoms.push(vGeo);

        }
        let cuts = featuresData.filter(op => op.wallId === w.fullId);
        cuts.forEach(op => {
            let localX = -w.len/2 + w.len*(op.pos/100);
            let cX = w.midX + localX*Math.cos(w.angle);
            let cZ = w.midZ + localX*Math.sin(w.angle);

            let headHeight = 80; 
            let cy = op.cat === 'window' ? (w.sBase + headHeight - (op.h/2)) : (w.sBase + (op.h/2));
            
            // Make cut deep enough to seamlessly pierce the inner frame
            let cutDepth = Math.max(40, w.thick * 4);
            let cutMesh = new THREE.Mesh(new THREE.BoxGeometry(op.w, op.h, cutDepth)); 
            cutMesh.position.set(cX, cy, cZ); 
            cutMesh.rotation.y = -w.angle; 
            cutMesh.updateMatrix();
            
            let csgCut = CSG.fromMesh(cutMesh, 1);
            wallCSG = wallCSG.subtract(csgCut);

            let assm = op.cat === 'door' ? (op.type === 'Sliding' ? buildSliding(op.w, op.h, op) : buildStandardDoor(op.w, op.h, op, !w.isInt)) : buildWindowSashes(op.w, op.h, op);
            assm.add(buildCasementTrim(op.w, op.h, w.thick)); assm.position.set(cX, cy, cZ); assm.rotation.y = -w.angle; assm.userData.layer = 'detail';
            
            assm.traverse(c => { c.userData.layer = 'detail'; });
            bldg.add(assm);
        });

        // HVAC Fireplace Cutouts applied to the wall CSG
        let hvacCuts = hvacData.filter(hv => hv.wallId === w.fullId && hv.type === 'fireplace');
        hvacCuts.forEach(hv => {
            let localX = -w.len/2 + w.len*(hv.pos/100);
            let cX = w.midX + localX*Math.cos(w.angle);
            let cZ = w.midZ + localX*Math.sin(w.angle);
            let hvH = hv.h || 60;
            let hvD = hv.d || 24;
            let taperH = 30;
            let chimH = 144;
            
            let insideX = w.nX, insideZ = w.nZ;
            let insideFace = w.thick / 2;
            let interiorProtrusion = 4;
            let centerOffset = insideFace + interiorProtrusion - (hvD / 2);
            let baseX = cX + insideX * centerOffset;
            let baseZ = cZ + insideZ * centerOffset;
            
            let cutBase = new THREE.Mesh(new THREE.BoxGeometry(hv.w, hvH, hvD));
            cutBase.position.set(baseX, w.sBase + hvH/2, baseZ);
            cutBase.rotation.y = -w.angle; cutBase.updateMatrix();
            
            let taperGeo = new THREE.CylinderGeometry(0.5 * Math.SQRT2, 1.0 * Math.SQRT2, taperH, 4);
            taperGeo.rotateY(Math.PI/4);
            taperGeo.scale(hv.w / 2, 1, hvD / 2);
            let cutTaper = new THREE.Mesh(taperGeo);
            cutTaper.position.set(baseX, w.sBase + hvH + taperH/2, baseZ);
            cutTaper.rotation.y = -w.angle; cutTaper.updateMatrix();
            
            let cutChim = new THREE.Mesh(new THREE.BoxGeometry(hv.w/2, chimH, hvD/2));
            cutChim.position.set(baseX, w.sBase + hvH + taperH + chimH/2, baseZ);
            cutChim.rotation.y = -w.angle; cutChim.updateMatrix();
            
            let csgBase = CSG.fromMesh(cutBase, 2);
            let csgTaper = CSG.fromMesh(cutTaper, 2);
            let csgChim = CSG.fromMesh(cutChim, 2);

            wallCSG = wallCSG.subtract(csgBase).subtract(csgTaper).subtract(csgChim);
        });
        
        masterWallCSG = masterWallCSG ? masterWallCSG.union(wallCSG) : wallCSG;
    });

    if(masterWallCSG) {
        let finalMasterWall = CSG.toMesh(masterWallCSG, new THREE.Matrix4(), MATS.wall);
        finalMasterWall.userData.layer = 'wall';
        bldg.add(finalMasterWall);
        extraGeoms.push(finalMasterWall.geometry);
    }
    
    if(masterFoundCSG) {
        let finalMasterFound = CSG.toMesh(masterFoundCSG, new THREE.Matrix4(), MATS.concrete);
        finalMasterFound.userData.layer = 'foundation';
        bldg.add(finalMasterFound);
        extraGeoms.push(finalMasterFound.geometry);
    }

    // HVAC Renderings
    hvacData.forEach(hv => {
        let w = allWalls.find(x => x.fullId === hv.wallId); if(!w) return;
        let localX = -w.len/2 + w.len*(hv.pos/100);
        let cx = w.midX + localX*Math.cos(w.angle);
        let cz = w.midZ + localX*Math.sin(w.angle);
        let mesh;
        let hvD = hv.d || 24;
        let hvH = hv.h || 60;
        
        // nX/nZ point inside, -nX/-nZ point outside
        let insideX = w.nX, insideZ = w.nZ;
        let outsideX = -w.nX, outsideZ = -w.nZ;

        // Safely fetch veneer settings in case they are missing from scope
        const showVeneer = document.getElementById('brickVeneerToggle')?.checked || false;
        const veneerW = parseFloat(document.getElementById('veneerW')?.value) || 3.25;
        const veneerGap = parseFloat(document.getElementById('veneerGap')?.value) || 1.0;

        if (hv.type === 'fireplace') {
            let fpGroup = new THREE.Group();
            let taperH = 30;
            let chimH = 144;
            let fpBaseH = w.sBase + hvH; 
            
            let insideFace = w.thick / 2;
            let interiorProtrusion = 0; // INSET 4 INCHES INTO THE ROOM
            let centerOffset = insideFace + interiorProtrusion - (hvD / 2); 
            
            let baseX = cx + insideX * centerOffset;
            let baseZ = cz + insideZ * centerOffset;
            
            let baseMesh = new THREE.Mesh(new THREE.BoxGeometry(hv.w, fpBaseH, hvD), MATS.wall); 
            baseMesh.position.set(0, fpBaseH/2, 0);
            baseMesh.userData.layer = 'foundation'; 
            
            // --- TAPER GEOMETRY ---
            // Scaled to match the new chimney footprint (50% W, 75% D)
            let taperGeo = new THREE.CylinderGeometry(0.5 * Math.SQRT2, 1.0 * Math.SQRT2, taperH, 4);
            taperGeo.rotateY(Math.PI/4);
            taperGeo.scale(hv.w / 2, 1, hvD / 2);
            
            // Shift top vertices to keep interior face flush
            // Offset is half the depth difference (1.0 - 0.75) / 2 = 0.125
            let posAttr = taperGeo.attributes.position;
            for (let i = 0; i < posAttr.count; i++) {
                if (posAttr.getY(i) > 0) { 
                    posAttr.setZ(i, posAttr.getZ(i) + (hvD * 0.125)); 
                }
            }
            taperGeo.computeVertexNormals(); 

            let taperMesh = new THREE.Mesh(taperGeo, MATS.wall);
            taperMesh.position.set(0, fpBaseH + taperH/2, 0);
            taperMesh.userData.layer = 'foundation'; 
            
            // --- CHIMNEY (50% W, 75% D) ---
            // Offset Z by 12.5% of depth to maintain flush interior face
            let chimMesh = new THREE.Mesh(new THREE.BoxGeometry(hv.w * 0.5, chimH, hvD * 0.75), MATS.wall);
            chimMesh.position.set(0, fpBaseH + taperH + chimH/2, hvD * 0.125);
            chimMesh.userData.layer = 'foundation'; 

            // HEARTH & RIM JOISTS (Remain same)
            let hearthD = 18;
            let hearthH = w.sBase + 12;
            let hearthMesh = new THREE.Mesh(new THREE.BoxGeometry(hv.w, hearthH, hearthD), MATS.wall);
            hearthMesh.position.set(0, hearthH/2, (hvD/2) + (hearthD/2));
            hearthMesh.userData.layer = 'foundation'; 

            let curJDepth = fjSizes[(w.story || 1) - 1] || 11.25;
            let fpRjY = w.jY + curJDepth/2;

            let rjLeft = new THREE.Mesh(new THREE.BoxGeometry(2, curJDepth, hearthD), MATS.wood);
            rjLeft.position.set(-(hv.w/2 + 1), fpRjY, (hvD/2) + (hearthD/2));
            rjLeft.userData.layer = 'joist';

            let rjRight = new THREE.Mesh(new THREE.BoxGeometry(2, curJDepth, hearthD), MATS.wood);
            rjRight.position.set((hv.w/2 + 1), fpRjY, (hvD/2) + (hearthD/2));
            rjRight.userData.layer = 'joist';

            let rjFront = new THREE.Mesh(new THREE.BoxGeometry(hv.w + 4, curJDepth, 2), MATS.wood);
            rjFront.position.set(0, fpRjY, (hvD/2) + hearthD + 1);
            rjFront.userData.layer = 'joist';

            let slabThick = 12;
            let slabW = hv.w + 16;
            let slabD = hvD + hearthD + 2 + 12;
            let fpSlab = new THREE.Mesh(new THREE.BoxGeometry(slabW, slabThick, slabD), MATS.concrete);
            fpSlab.position.set(0, -slabThick/2, (hearthD + 2)/2); 
            fpSlab.userData.layer = 'foundation'; 

            // --- FIREPLACE FACE FRAMING ---
            let frameW = 3.5;
            let frameD = 5.5;
            let frameZ = (hvD / 2) + (frameD / 2); 
            
            let whiteMat = new THREE.MeshStandardMaterial({ color: 0xffffff });

            let topFrameY = fpBaseH - (frameW / 2);
            let fpFrameTop = new THREE.Mesh(new THREE.BoxGeometry(hv.w, frameW, frameD), whiteMat);
            fpFrameTop.position.set(0, topFrameY, frameZ);
            fpFrameTop.userData.layer = 'wall';

            let vertStartY = hearthH; 
            let vertEndY = fpBaseH - frameW; 
            let vertH = vertEndY - vertStartY;
            let vertY = vertStartY + (vertH / 2);

            let fpFrameLeft = new THREE.Mesh(new THREE.BoxGeometry(frameW, vertH, frameD), whiteMat);
            fpFrameLeft.position.set(-(hv.w / 2) + (frameW / 2), vertY, frameZ); 
            fpFrameLeft.userData.layer = 'wall';

            let fpFrameRight = new THREE.Mesh(new THREE.BoxGeometry(frameW, vertH, frameD), whiteMat);
            fpFrameRight.position.set((hv.w / 2) - (frameW / 2), vertY, frameZ);
            fpFrameRight.userData.layer = 'wall';
            
            fpGroup.add(
                baseMesh, taperMesh, chimMesh, hearthMesh, 
                rjLeft, rjRight, rjFront, fpSlab,
                fpFrameTop, fpFrameLeft, fpFrameRight
            );
            
            fpGroup.traverse(c => { 
                if(c.isMesh && !c.userData.layer) c.userData.layer = 'detail'; 
            });
            
            fpGroup.position.set(baseX, 0, baseZ);
            fpGroup.rotation.y = -w.angle;
            bldg.add(fpGroup);

            // --- LOCAL CSG POST-PROCESSING TRIM ---
            // (Note: CSG logic remains robust as long as the base and hearth geometry are consistent)
            let cutMinY = -14; 
            let cutMaxY = fpBaseH + 2; 
            let cutH = cutMaxY - cutMinY;
            let cutY = cutMinY + cutH / 2;

            let cutBase = new THREE.Mesh(new THREE.BoxGeometry(hv.w + 0.1, cutH, hvD + 0.1));
            cutBase.position.set(baseX, cutY, baseZ);
            cutBase.rotation.y = -w.angle;
            cutBase.updateMatrix();

            let hearthCutMaxY = hearthH + 2;
            let hearthCutH = hearthCutMaxY - cutMinY;
            let hearthCutY = cutMinY + hearthCutH / 2;
            
            let cutHearthW = hv.w + 4;
            let cutHearthD = hearthD + 2;
            let hearthCutCX = baseX + insideX * ((hvD/2) + cutHearthD/2);
            let hearthCutCZ = baseZ + insideZ * ((hvD/2) + cutHearthD/2);
            
            let cutHearth = new THREE.Mesh(new THREE.BoxGeometry(cutHearthW + 0.1, hearthCutH, cutHearthD + 0.1));
            cutHearth.position.set(hearthCutCX, hearthCutY, hearthCutCZ);
            cutHearth.rotation.y = -w.angle;
            cutHearth.updateMatrix();

            let csgBase = CSG.fromMesh(cutBase, 0);
            let csgHearth = CSG.fromMesh(cutHearth, 0);
            let combinedVoid = csgBase.union(csgHearth);

            let voidGrp = new THREE.Group();
            voidGrp.add(cutBase.clone(), cutHearth.clone());
            voidGrp.updateMatrixWorld(true);
            let voidBox = new THREE.Box3().setFromObject(voidGrp);

            for (let i = bldg.children.length - 1; i >= 0; i--) {
                let child = bldg.children[i];
                if (['joist', 'subfloor', 'foundation', 'wall'].includes(child.userData.layer)) {
                    child.updateMatrixWorld(true);
                    let childBox = new THREE.Box3().setFromObject(child);
                    if (voidBox.intersectsBox(childBox)) {
                        try {
                            let childCSG = CSG.fromMesh(child, 0);
                            let resultCSG = childCSG.subtract(combinedVoid);
                            if (resultCSG.polygons.length === 0) {
                                bldg.remove(child);
                            } else {
                                let newMesh = CSG.toMesh(resultCSG, new THREE.Matrix4(), child.material);
                                newMesh.userData.layer = child.userData.layer;
                                bldg.remove(child);
                                bldg.add(newMesh);
                            }
                        } catch(e) { console.warn('CSG Trim Skip:', e); }
                    }
                }
            }
        }else if (hv.type === 'ac') {
            let acGroup = new THREE.Group();
            
            // Main chassis
            let acBody = new THREE.Mesh(new THREE.BoxGeometry(hv.w, hvH, hvD), MATS.metal);
            
            // Dark interior fan area (top)
            let fanRadius = Math.min(hv.w, hvD) * 0.4;
            let fanHole = new THREE.Mesh(new THREE.CylinderGeometry(fanRadius, fanRadius, 0.5, 16), new THREE.MeshStandardMaterial({color: 0x111111, roughness: 0.9}));
            fanHole.position.y = hvH/2;
            
            // Fan blades
            let bladeMat = new THREE.MeshStandardMaterial({color: 0x222222});
            let fanBlade1 = new THREE.Mesh(new THREE.BoxGeometry(fanRadius * 1.8, 0.2, 3), bladeMat);
            fanBlade1.position.y = hvH/2 + 0.1;
            fanBlade1.rotation.y = Math.PI/4;
            let fanBlade2 = new THREE.Mesh(new THREE.BoxGeometry(3, 0.2, fanRadius * 1.8), bladeMat);
            fanBlade2.position.y = hvH/2 + 0.1;
            fanBlade2.rotation.y = Math.PI/4;
            
            // Top protective grill ring
            let grill = new THREE.Mesh(new THREE.TorusGeometry(fanRadius, 0.5, 4, 16), MATS.metal);
            grill.rotation.x = Math.PI/2;
            grill.position.y = hvH/2 + 0.5;
            
            // Side electrical control box
            let controlBox = new THREE.Mesh(new THREE.BoxGeometry(6, 14, 4), MATS.wall);
            controlBox.position.set(hv.w/2 + 1, 0, hvD/4);
            
            acGroup.add(acBody, fanHole, fanBlade1, fanBlade2, grill, controlBox);
            
            let outsideFace = w.thick / 2;
            let acClearance = 2; // 2 inches from exterior face
            let extraOffset = (showVeneer && !w.isInt) ? (veneerGap + veneerW) : 0;
            let centerOffset = outsideFace + acClearance + extraOffset + (hvD / 2);
            
            // Assembly center
            acGroup.position.set(cx + outsideX * centerOffset, 4 + hvH/2, cz + outsideZ * centerOffset);
            mesh = acGroup;
            
            // Draw 4" thick slab underneath AC
            let slab = new THREE.Mesh(new THREE.BoxGeometry(hv.w + 4, 4, hvD + 4), MATS.concrete);
            slab.position.set(cx + outsideX * centerOffset, 2, cz + outsideZ * centerOffset);
            slab.rotation.y = -w.angle; slab.userData.layer = 'detail'; bldg.add(slab);
            
        } else if (hv.type === 'central') {
            let centralGroup = new THREE.Group();
            
            // Main Air Handler Cabinet
            let cabH = hvH * 0.7;
            let cab = new THREE.Mesh(new THREE.BoxGeometry(hv.w, cabH, hvD), MATS.metal);
            cab.position.y = -hvH/2 + cabH/2; 
            
            // Upper Supply Air Plenum
            let plenH = hvH * 0.3;
            let plenum = new THREE.Mesh(new THREE.BoxGeometry(hv.w * 0.85, plenH, hvD * 0.85), MATS.metal);
            plenum.position.y = -hvH/2 + cabH + plenH/2;
            
            // Front Access Panel
            let panelGeo = new THREE.BoxGeometry(hv.w * 0.7, cabH * 0.5, 1);
            let panelMat = new THREE.MeshStandardMaterial({color: 0x999999, roughness: 0.5});
            let panel = new THREE.Mesh(panelGeo, panelMat);
            panel.position.set(0, -hvH/2 + cabH * 0.6, hvD/2 + 0.2); 
            
            // Side piping (Insulated Refrigerant line)
            let pipeGeo = new THREE.CylinderGeometry(0.8, 0.8, 6, 8);
            pipeGeo.rotateZ(Math.PI/2);
            let lineSetMat = new THREE.MeshStandardMaterial({color: 0x111111}); 
            let lineSet = new THREE.Mesh(pipeGeo, lineSetMat);
            lineSet.position.set(hv.w/2 + 2, -hvH/2 + cabH * 0.3, 0);

            // PVC Exhaust/Condensate pipe
            let pvcGeo = new THREE.CylinderGeometry(0.6, 0.6, 6, 8);
            pvcGeo.rotateZ(Math.PI/2);
            let pvcMat = new THREE.MeshStandardMaterial({color: 0xffffff}); 
            let pvc = new THREE.Mesh(pvcGeo, pvcMat);
            pvc.position.set(hv.w/2 + 2, -hvH/2 + cabH * 0.2, hvD/4);

            centralGroup.add(cab, plenum, panel, lineSet, pvc);

            let insideFace = w.thick / 2;
            let centerOffset = insideFace + (hvD / 2);
            
            // Rests properly on the subfloor base instead of floating
            centralGroup.position.set(cx + insideX * centerOffset, w.sBase + hvH/2 + 1, cz + insideZ * centerOffset);
            mesh = centralGroup;
            
        } else if (hv.type === 'thermostat') {
            mesh = new THREE.Mesh(new THREE.BoxGeometry(4, 3, 1), MATS.metal);
            let insideFace = w.thick / 2;
            let centerOffset = insideFace + 0.5; // d=1, so 0.5 is flush
            mesh.position.set(cx + insideX * centerOffset, w.sBase + 60, cz + insideZ * centerOffset);
        } else if (hv.type === 'duct') {
            mesh = new THREE.Mesh(new THREE.BoxGeometry(hv.w, 8, 12), MATS.metal);
            let insideFace = w.thick / 2;
            let centerOffset = insideFace + 6; // default depth 12 / 2
            mesh.position.set(cx + insideX * centerOffset, w.sBase + 4, cz + insideZ * centerOffset);
        }
        
        if(mesh) {
            mesh.rotation.y = -w.angle;
            mesh.traverse(c => { if(c.isMesh && !c.userData.layer) c.userData.layer = 'detail'; });
            bldg.add(mesh);
        }
    });
    electricalData.forEach(el => {
        let w = allWalls.find(x => x.fullId === el.wallId); if(!w) return;
        let localX = -w.len/2 + w.len*(el.pos/100);
        let cx = w.midX + localX*Math.cos(w.angle), cz = w.midZ + localX*Math.sin(w.angle);
        
        // nX/nZ point inside, -nX/-nZ point outside
        let insideX = w.nX, insideZ = w.nZ;
        let outsideX = -w.nX, outsideZ = -w.nZ;

        if (el.type === 'battery_wall' || el.type === 'battery_grade') {
            let elW = el.w || 36, elH = el.h || 48, elD = el.d || 12;
            if(el.type === 'battery_wall') elD = 6; // Wall mounted strictly 6" depth
            
            let centerOffset, cy;
            let isGrade = el.type === 'battery_grade';
            let extraOffset = (showVeneer && !w.isInt) ? (veneerGap + veneerW) : 0;
            
            if (isGrade) {
                let clearance = 2; // 2 inches off the wall
                centerOffset = w.thick / 2 + clearance + extraOffset + elD / 2;
                cy = 4 + elH / 2; // Rests on a 4" thick slab
                
                // Draw 4" thick slab underneath battery pack
                let slab = new THREE.Mesh(new THREE.BoxGeometry(elW + 4, 4, elD + 4), MATS.concrete);
                slab.position.set(cx + outsideX * centerOffset, 2, cz + outsideZ * centerOffset);
                slab.rotation.y = -w.angle; slab.userData.layer = 'detail'; bldg.add(slab);
            } else {
                centerOffset = w.thick / 2 + extraOffset + elD / 2; // Flush against outside wall
                cy = w.sBase + 36 + elH / 2; // Wall mounted height
            }
            
            // Build an array of batteries within the case bounds
            let bGrp = new THREE.Group();
            
            // Slightly transparent case
            let caseGeo = new THREE.BoxGeometry(elW, elH, elD);
            let caseMat = new THREE.MeshStandardMaterial({ color: 0x999999, transparent: true, opacity: 0.5, side: THREE.DoubleSide });
            let caseMesh = new THREE.Mesh(caseGeo, caseMat);
            bGrp.add(caseMesh);
            
            // Inner cells grid
            let cols = Math.max(1, Math.floor(elW / 8));
            let rows = Math.max(1, Math.floor(elH / 16));
            let cellW = (elW - 4) / cols - 1;
            let cellH = (elH - 4) / rows - 1;
            let cellD = elD - 4;
            
            let cellGeo = new THREE.BoxGeometry(cellW, cellH, cellD);
            let cellMat = new THREE.MeshStandardMaterial({ color: 0x333333, metalness: 0.8, roughness: 0.2 });
            
            for(let c = 0; c < cols; c++) {
                for(let r = 0; r < rows; r++) {
                    let cell = new THREE.Mesh(cellGeo, cellMat);
                    let px = -elW/2 + 2 + cellW/2 + c*(cellW+1);
                    let py = -elH/2 + 2 + cellH/2 + r*(cellH+1);
                    cell.position.set(px, py, 0);
                    bGrp.add(cell);
                }
            }
            
            bGrp.position.set(cx + outsideX * centerOffset, cy, cz + outsideZ * centerOffset);
            bGrp.rotation.y = -w.angle; 
            bGrp.userData.layer = 'detail'; 
            bldg.add(bGrp);

        } else {
            // Standard electrical items (inside wall)
            let h = el.type === 'switch' ? 6 : el.type === 'outlet' ? 4 : 8, wd = 3, d = 1;
            let cy = w.sBase + (el.type === 'switch' ? 48 : el.type === 'outlet' ? 18 : 84);
            let elMesh = new THREE.Mesh(new THREE.BoxGeometry(wd, h, d), MATS.metal);
            
            // Properly calculated inside normals for interior flush mounting
            let centerOffset = w.thick / 2 + d / 2;
            elMesh.position.set(cx + insideX * centerOffset, cy, cz + insideZ * centerOffset);
            elMesh.rotation.y = -w.angle; elMesh.userData.layer = 'detail'; bldg.add(elMesh);
        }
    });

    if(showPlanes) {
        const fGeo = new THREE.PlaneGeometry(mW*2, mL*2); fGeo.rotateX(-Math.PI/2); extraGeoms.push(fGeo);
        const grade = new THREE.Mesh(fGeo, new THREE.MeshStandardMaterial({ color: 0x446644, side: THREE.DoubleSide })); grade.position.y = 0; grade.userData.layer = 'plane'; bldg.add(grade);
    }

    columnsData.forEach(col => {
        let floorIdx = Math.min((col.floor || 1) - 1, ns - 1);
        let colSBase = storyBases[floorIdx] || storyBases[0];
        let colHeight = wH;
        
        let colGeo = new THREE.CylinderGeometry(4, 4, colHeight, 16); 
        let plateThk = col.topThick||2;
        let plateGeo = new THREE.BoxGeometry(12, plateThk, 12);
        let pierGeo = new THREE.BoxGeometry(16, ff2G + footD, 16);
        extraGeoms.push(colGeo, plateGeo, pierGeo);
        
        for(let i=0; i<col.count; i++) {
            let px = col.startX + (i*col.spacingX), pz = col.startZ + (i*col.spacingZ);
            
            let m = new THREE.Mesh(colGeo, MATS.metal); 
            m.position.set(px, colSBase + colHeight/2, pz); 
            m.userData.layer = 'detail'; 
            bldg.add(m);
            
            let bPlate = new THREE.Mesh(plateGeo, MATS.metal); 
            bPlate.position.set(px, colSBase + plateThk/2, pz); 
            bPlate.userData.layer = 'detail'; 
            bldg.add(bPlate);
            
            let tPlate = new THREE.Mesh(plateGeo, MATS.metal); 
            tPlate.position.set(px, colSBase + colHeight - plateThk/2, pz); 
            tPlate.userData.layer = 'detail'; 
            bldg.add(tPlate);
            
            if (col.floor === 1 || !col.floor) {
                let pier = new THREE.Mesh(pierGeo, MATS.concrete); 
                pier.position.set(px, (ff2G+footD)/2 - footD, pz); 
                pier.userData.layer = 'foundation'; 
                bldg.add(pier);
            }
        }
    });

    viewList.forEach(v => {
        let clone = bldg.clone();
        
        clone.traverse(child => {
            if(!child.userData || !child.userData.layer) return;
            
            if(v === '1') {
                if(['roof', 'plane'].includes(child.userData.layer)) {
                    child.visible = false;
                } else if (child.isMesh) {
                    child.material = child.material;
                    child.material.transparent = true;
                    child.material.opacity = 0.8; 
                    let edgesGeo = new THREE.EdgesGeometry(child.geometry, 1);
                    let edgesMat = new THREE.LineBasicMaterial({color: 0x000000});
                    let edges = new THREE.LineSegments(edgesGeo, edgesMat);
                    child.add(edges);
                }
            }
            else if (v === '7') {
                if(!['joist', 'foundation'].includes(child.userData.layer)) child.visible = false;
                else if (child.isMesh) {
                    let edgesGeo = new THREE.EdgesGeometry(child.geometry, 1);
                    let edgesMat = new THREE.LineBasicMaterial({color: 0x000000});
                    let edges = new THREE.LineSegments(edgesGeo, edgesMat);
                    extraMats.push(edgesMat); extraGeoms.push(edgesGeo);
                    child.add(edges);
                }
            }
            else if (v === '8') {
                if(child.userData.layer !== 'roof') child.visible = false;
            }
        });
        scenes[v].add(clone);
    });

    let max = Math.max(mW, mL, wH * ns) * 1.5, cyPos = ff2G + (wH * ns)/2;
    cameras['1'].position.set(0, max, 0); cameras['1'].lookAt(0, 0, 0); 
    cameras['2'].position.set(0, cyPos, max); cameras['2'].lookAt(0, cyPos, 0); 
    cameras['3'].position.set(max, cyPos, 0); cameras['3'].lookAt(0, cyPos, 0); 
    cameras['4'].position.set(0, cyPos, -max); cameras['4'].lookAt(0, cyPos, 0); 
    cameras['5'].position.set(-max, cyPos, 0); cameras['5'].lookAt(0, cyPos, 0); 
    cameras['6'].position.set(max*0.8, max*0.8, max*0.8); cameras['6'].lookAt(0, 0, 0); 
    cameras['7'].position.set(0, max, 0); cameras['7'].lookAt(0, 0, 0); 
    cameras['8'].position.set(0, max, 0); cameras['8'].lookAt(0, 0, 0); 
    
    window.resizeActiveViewport();
};

This is my ai model’s code to generate cuts into the brick veneer, it took a few tries to get it to stop crashing, but I finally isolated the issue and fixed the problems over the last hour.

Attached snippet for cutting brick veneer:

// --- 1. GLOBAL UI INPUTS & MASTER CSG ---
const showVeneer = document.getElementById('brickVeneerToggle')?.checked || false;
const veneerW = parseFloat(document.getElementById('veneerW')?.value) || 3.25;
const veneerGap = parseFloat(document.getElementById('veneerGap')?.value) || 1.0;

let masterVeneerCSG = null;


// --- INSIDE THE allWalls.forEach((w, idx) => { ... }) LOOP ---
allWalls.forEach((w, idx) => {

    // --- 2. BASE VENEER GENERATION ---
    let veneerCSG = null;
    if (showVeneer && !w.isInt && w.story === 1) {
        let vOffset = (w.thick / 2) + veneerGap + (veneerW / 2);
        let vX = w.midX - w.nX * vOffset;
        let vZ = w.midZ - w.nZ * vOffset;
        let vLen = w.len + w.thick + 2 * (veneerGap + veneerW);
        
        let soffitY = py - (cjSize / 2) + 2.25; 
        let vH = soffitY;
        
        let vGeo = new THREE.BoxGeometry(vLen, vH, veneerW);
        let vMesh = new THREE.Mesh(vGeo, MATS.brick);
        vMesh.position.set(vX, vH / 2, vZ);
        vMesh.rotation.y = -w.angle;
        vMesh.updateMatrix();
        
        // Strictly assign material index 0 to avoid massive array allocation crash
        veneerCSG = CSG.fromMesh(vMesh, 0); 
        extraGeoms.push(vGeo);
    }

    // --- 3. WINDOW/DOOR VENEER CUTS ---
    let cuts = featuresData.filter(op => op.wallId === w.fullId);
    cuts.forEach(op => {
        let localX = -w.len/2 + w.len*(op.pos/100);
        let cX = w.midX + localX*Math.cos(w.angle);
        let cZ = w.midZ + localX*Math.sin(w.angle);

        let headHeight = 80; 
        let cy = op.cat === 'window' ? (w.sBase + headHeight - (op.h/2)) : (w.sBase + (op.h/2));
        let cutDepth = Math.max(40, w.thick * 4);

        if (veneerCSG) {
            // Strictly decoupled fresh copy of cut geometry
            let vCutMesh = new THREE.Mesh(new THREE.BoxGeometry(op.w, op.h, cutDepth)); 
            vCutMesh.position.set(cX, cy, cZ); 
            vCutMesh.rotation.y = -w.angle; 
            vCutMesh.updateMatrix();
            
            let vCsgCut = CSG.fromMesh(vCutMesh, 0);
            veneerCSG = veneerCSG.subtract(vCsgCut);
        }
    });

    // --- 4. HVAC FIREPLACE VENEER CUTS ---
    let hvacCuts = hvacData.filter(hv => hv.wallId === w.fullId && hv.type === 'fireplace');
    hvacCuts.forEach(hv => {
        let localX = -w.len/2 + w.len*(hv.pos/100);
        let cX = w.midX + localX*Math.cos(w.angle);
        let cZ = w.midZ + localX*Math.sin(w.angle);
        let hvH = hv.h || 60;
        let hvD = hv.d || 24;
        let taperH = 30;
        let chimH = 144;
        
        let insideX = w.nX, insideZ = w.nZ;
        let insideFace = w.thick / 2;
        let interiorProtrusion = 4;
        let centerOffset = insideFace + interiorProtrusion - (hvD / 2);
        let baseX = cX + insideX * centerOffset;
        let baseZ = cZ + insideZ * centerOffset;

        if (veneerCSG) {
            let vCutBase = new THREE.Mesh(new THREE.BoxGeometry(hv.w, hvH, hvD));
            vCutBase.position.set(baseX, w.sBase + hvH/2, baseZ);
            vCutBase.rotation.y = -w.angle; vCutBase.updateMatrix();

            // Generate fresh taper geometry to separate memory space
            let vTaperGeo = new THREE.CylinderGeometry(0.5 * Math.SQRT2, 1.0 * Math.SQRT2, taperH, 4);
            vTaperGeo.rotateY(Math.PI/4);
            vTaperGeo.scale(hv.w / 2, 1, hvD / 2);
            let vCutTaper = new THREE.Mesh(vTaperGeo); 
            vCutTaper.position.set(baseX, w.sBase + hvH + taperH/2, baseZ);
            vCutTaper.rotation.y = -w.angle; vCutTaper.updateMatrix();

            let vCutChim = new THREE.Mesh(new THREE.BoxGeometry(hv.w/2, chimH, hvD/2));
            vCutChim.position.set(baseX, w.sBase + hvH + taperH + chimH/2, baseZ);
            vCutChim.rotation.y = -w.angle; vCutChim.updateMatrix();

            let vCsgBase = CSG.fromMesh(vCutBase, 0);
            let vCsgTaper = CSG.fromMesh(vCutTaper, 0);
            let vCsgChim = CSG.fromMesh(vCutChim, 0);

            veneerCSG = veneerCSG.subtract(vCsgBase).subtract(vCsgTaper).subtract(vCsgChim);
        }
    });
    
    // --- 5. ACCUMULATE INTO MASTER ---
    if (veneerCSG) {
        masterVeneerCSG = masterVeneerCSG ? masterVeneerCSG.union(veneerCSG) : veneerCSG;
    }
});


// --- 6. RENDER FINAL VENEER (OUTSIDE LOOP) ---
if (masterVeneerCSG) {
    let finalMasterVeneer = CSG.toMesh(masterVeneerCSG, new THREE.Matrix4(), MATS.brick);
    finalMasterVeneer.userData.layer = 'wall';
    bldg.add(finalMasterVeneer);
    extraGeoms.push(finalMasterVeneer.geometry);
}

Attached brick veneer with cuts:

Now my issue is the final wall’s geometry, I need a way to make the csg cutout the same way but the corners aren’t right, each wall is being extruded individually.

You can see the final wall’s geometry error in this photo, polygonal really shows it: