本文档详细解析 cgaprocessor.js 中 roofHip 函数的实现。该函数用于生成四坡屋顶(Hip Roof / 庑殿顶)——四面都有坡度的屋顶形式,顶部有一条屋脊线,四角有斜脊(hip line)。
roofHip 是屋顶函数中最复杂的一个。它采用矩形近似算法:将 footprint 投影到2D平面后,用其包围盒的长宽比来近似生成四坡屋顶。当 footprint 接近正方形时会自动退化为金字塔顶。
function func_roofHip(processor, angle) { angle = angle || 30; var top = processor.top; if (!top.faces.length) return; top.mergeVertices(); top.computeFaceNormals(); var normal = top.faces[0].normal.clone().normalize(); if (!normal.length()) throw 'roofHip needs a non-degenerate face normal'; var center = new THREE.Vector3(); top.vertices.forEach(v => center.add(v)); center.divideScalar(top.vertices.length); // Project vertices to 2D plane var projected = top.vertices.map(v => v.clone().sub(center)); var xaxis = projected[1] ? projected[1].clone().sub(projected[0]) : new THREE.Vector3(1,0,0); xaxis.sub(normal.clone().multiplyScalar(xaxis.dot(normal))); if (xaxis.length() < 0.0001) xaxis = new THREE.Vector3(1,0,0).cross(normal); if (xaxis.length() < 0.0001) xaxis = new THREE.Vector3(0,1,0).cross(normal); xaxis.normalize(); var yaxis = new THREE.Vector3().crossVectors(normal, xaxis).normalize(); var pts2d = top.vertices.map(v => { var d = v.clone().sub(center); return { x: d.dot(xaxis), y: d.dot(yaxis), vertex: v }; }); // Sort CCW var c2 = pts2d.reduce((a, p) => { a.x += p.x; a.y += p.y; return a; }, {x:0,y:0}); c2.x /= pts2d.length; c2.y /= pts2d.length; pts2d.sort((a,b) => Math.atan2(a.y-c2.y, a.x-c2.x) - Math.atan2(b.y-c2.y, b.x-c2.x)); // Compute bounding box to determine orientation var minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; pts2d.forEach(p => { minX = Math.min(minX, p.x); maxX = Math.max(maxX, p.x); minY = Math.min(minY, p.y); maxY = Math.max(maxY, p.y); }); var W = maxX - minX, D = maxY - minY;
// Near-square → pyramid (correct degeneracy) if (Math.min(W,D) / Math.max(W,D) > 0.85) { func_roofPyramid(processor, angle); return; } // ... var h = Math.tan(THREE.Math.degToRad(angle)) * shortSize / 2; var hipRun = h / Math.tan(THREE.Math.degToRad(angle)); if (hipRun > shortSize / 2 + 0.0001) { func_roofPyramid(processor, angle); return; }
var ridgeHalfShort = shortSize / 2 - hipRun; var ridgeHalfLong = longSize / 2; // ... corners and roofBase definitions ... var ridgeLeft = center.clone().add(longAxis.clone().multiplyScalar(-ridgeHalfW)) .add(shortAxis.clone().multiplyScalar(0)).add(normal.clone().multiplyScalar(h)); var ridgeRight = center.clone().add(longAxis.clone().multiplyScalar( ridgeHalfW)) .add(shortAxis.clone().multiplyScalar(0)).add(normal.clone().multiplyScalar(h)); // Footprint corners (ground level) var fl = center.clone().add(longAxis.clone().multiplyScalar(-halfW)) .add(shortAxis.clone().multiplyScalar(-halfD)); var fr = center.clone().add(longAxis.clone().multiplyScalar( halfW)) .add(shortAxis.clone().multiplyScalar(-halfD)); var br = center.clone().add(longAxis.clone().multiplyScalar( halfW)) .add(shortAxis.clone().multiplyScalar( halfD)); var bl = center.clone().add(longAxis.clone().multiplyScalar(-halfW)) .add(shortAxis.clone().multiplyScalar( halfD)); // Roof base corners (at height h) var flh = center.clone().add(longAxis.clone().multiplyScalar(-ridgeHalfW)) .add(shortAxis.clone().multiplyScalar(-halfD)).add(normal.clone().multiplyScalar(h)); var frh = center.clone().add(longAxis.clone().multiplyScalar( ridgeHalfW)) .add(shortAxis.clone().multiplyScalar(-halfD)).add(normal.clone().multiplyScalar(h)); var brh = center.clone().add(longAxis.clone().multiplyScalar( ridgeHalfW)) .add(shortAxis.clone().multiplyScalar( halfD)).add(normal.clone().multiplyScalar(h)); var blh = center.clone().add(longAxis.clone().multiplyScalar(-ridgeHalfW)) .add(shortAxis.clone().multiplyScalar( halfD)).add(normal.clone().multiplyScalar(h)); function addFace(a, b, c) { var l = geometry.vertices.length; geometry.vertices.push(a, b, c); geometry.faces.push(new THREE.Face3(l, l+1, l+2)); } // Hip faces (triangles from ridge end to corner) addFace(ridgeLeft, flh, fl); addFace(ridgeRight, fr, frh); addFace(ridgeRight, brh, br); addFace(ridgeLeft, bl, blh); // Gable faces (trapezoids from ridge to eave) addFace(ridgeLeft, ridgeRight, frh); addFace(ridgeLeft, frh, flh); addFace(ridgeRight, ridgeLeft, blh); addFace(ridgeRight, blh, brh); geometry.mergeVertices(); geometry.computeFaceNormals(); geometry.computeVertexNormals(); processor.update(geometry); }
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
angle | Number | 30 | 屋顶坡度角(度) |
与 roofGable 相同,先建立局部坐标系,将 footprint 顶点投影到2D平面,按极角排序。然后计算投影点的包围盒:
W = maxX - minX:长轴方向跨度D = maxY - minY:短轴方向跨度if (Math.min(W,D) / Math.max(W,D) > 0.85) { func_roofPyramid(processor, angle); return; }
当长短边比例超过 0.85 时(接近正方形),四坡屋顶退化为金字塔顶。
var h = Math.tan(THREE.Math.degToRad(angle)) * shortSize / 2; var hipRun = h / Math.tan(THREE.Math.degToRad(angle)); if (hipRun > shortSize / 2 + 0.0001) { func_roofPyramid(processor, angle); return; }
计算关键参数:
hipRun > shortSize / 2 的判断实际上永远不会触发。这行代码可能是作者预留的安全检查,或复制自其他实现。
var isWider = W >= D; var longAxis = isWider ? xaxis : yaxis; var shortAxis = isWider ? yaxis : xaxis; var longSize = isWider ? W : D; var shortSize = isWider ? D : W;
将坐标轴与 footprint 的长短边对齐,确保屋脊始终沿长边方向。
代码生成了三类顶点:
| 顶点类型 | 变量名 | 位置 | 高度 |
|---|---|---|---|
| 屋脊左端点 | ridgeLeft | 长轴 -ridgeHalfW,短轴 0 | h |
| 屋脊右端点 | ridgeRight | 长轴 +ridgeHalfW,短轴 0 | h |
| 前左地面角点 | fl | 长轴 -halfW,短轴 -halfD | 0 |
| 前右地面角点 | fr | 长轴 +halfW,短轴 -halfD | 0 |
| 后右地面角点 | br | 长轴 +halfW,短轴 +halfD | 0 |
| 后左地面角点 | bl | 长轴 -halfW,短轴 +halfD | 0 |
| 前左 roofBase | flh | 长轴 -ridgeHalfW,短轴 -halfD | h |
| 前右 roofBase | frh | 长轴 +ridgeHalfW,短轴 -halfD | h |
| 后右 roofBase | brh | 长轴 +ridgeHalfW,短轴 +halfD | h |
| 后左 roofBase | blh | 长轴 -ridgeHalfW,短轴 +halfD | h |
其中 ridgeHalfW = halfW - hipRun = halfW - shortSize/2 = (W - D)/2(当 W ≥ D 时)。
图:俯视示意图。中间 roofBase 矩形和 ridge 都在高度 h,外圈 footprint 在高度 0。
| 面片 | 顶点 | 覆盖区域 |
|---|---|---|
| Front-left hip | ridgeLeft → flh → fl | 前左角,从屋脊到地面 |
| Front-right hip | ridgeRight → fr → frh | 前右角,从屋脊到地面 |
| Back-right hip | ridgeRight → brh → br | 后右角,从屋脊到地面 |
| Back-left hip | ridgeLeft → bl → blh | 后左角,从屋脊到地面 |
| 面片 | 顶点 | 说明 |
|---|---|---|
| Front gable (1) | ridgeLeft → ridgeRight → frh | 前侧上方水平三角形 |
| Front gable (2) | ridgeLeft → frh → flh | 前侧上方水平三角形 |
| Back gable (1) | ridgeRight → ridgeLeft → blh | 后侧上方水平三角形 |
| Back gable (2) | ridgeRight → blh → brh | 后侧上方水平三角形 |
| 参数 | 公式 | 说明 |
|---|---|---|
| 屋顶高度 h | h = tan(θ) × shortSize / 2 | 由坡角和短边决定 |
| hipRun | hipRun = h / tan(θ) = shortSize / 2 | hip 线水平投影长度 |
| ridgeHalfW | (W - D) / 2 | 屋脊半长(当 W ≥ D) |
| 屋脊长度 | W - D | 等于 longSize - shortSize |
// 默认 30 度四坡屋顶 Roof --> roofHip() // 45 度陡坡四坡屋顶 Roof --> roofHip(45) // 在方形 footprint 上会自动退化为金字塔顶 Square --> primitiveQuad(4, 4) roofHip(30)
| 函数 | 坡面数 | 屋脊 | 适用 footprint |
|---|---|---|---|
| roofGable | 2 | 有(沿长边) | 任意多边形(最长边为屋脊) |
| roofHip | 4(近似) | 有(沿长边) | 矩形近似(长短比 < 0.85) |
| roofPyramid | N | 无(汇聚为一点) | 近正方形 |
| roofShed | 1 | 无 | 任意(沿 Z 轴坡面) |
cgaprocessor.js 第 1185~1420 行源码分析。生成时间:2026-05-28。