本文档详细解析 cgaprocessor.js 中 roofRidge 函数的实现。该函数与 roofGable 算法核心几乎完全一致,区别在于 roofRidge 允许显式指定屋脊边,而非自动选择最长边。
roofRidge 是 roofGable 的变体/扩展版本。两者都生成人字屋顶(两面坡),但 roofRidge 通过 edgeIndex 参数让用户精确控制哪条边作为屋脊方向。
roofRidge;当只需最自然的屋脊方向时,使用 roofGable。
function func_roofRidge(processor, angle, index) { var args = Array.prototype.slice.call(arguments, 1); var valueType = 'byAngle'; var value, overhangX = 0, overhangY = 0, even = false; var edgeIndex = 0; if (args[0] == 'byAngle' || args[0] == 'byHeight') { valueType = args[0]; value = args[1]; overhangX = args[2] || 0; overhangY = args[3] || 0; even = args[4] || false; edgeIndex = args[5] || 0; } else { value = args[0]; overhangX = args[1] || 0; overhangY = args[2] || 0; even = args[3] || false; edgeIndex = args[4] || 0; }
// ... 2D投影、坐标系建立、顶点排序 ... // Use explicit edge index for ridge direction edgeIndex = Math.floor(edgeIndex) % points.length; if (edgeIndex < 0) edgeIndex += points.length; var a = points[edgeIndex]; var b = points[(edgeIndex+1) % points.length]; var ridge = new THREE.Vector2(b.x-a.x, b.y-a.y); if (!ridge || ridge.length() < 0.0001) { // Fall back to longest edge if specified edge is degenerate var longest = -1; for (var i=0; i<points.length; i++) { var p1 = points[i]; var p2 = points[(i+1) % points.length]; var len = new THREE.Vector2(p2.x-p1.x, p2.y-p1.y).lengthSq(); if (len > longest) { longest = len; ridge = new THREE.Vector2(p2.x-p1.x, p2.y-p1.y); } } } ridge.normalize(); // ... 后续与 roofGable 完全相同:坐标旋转、高度计算、面片构建 ...
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
valueType | String | 'byAngle' | 'byAngle' 或 'byHeight' |
value | Number | 必填 | 坡角度数或屋脊高度 |
overhangX | Number | 0 | 垂直屋脊方向悬挑 |
overhangY | Number | 0 | 平行屋脊方向悬挑 |
even | Boolean | false | 使用平均高度 |
edgeIndex | Number | 0 | 指定哪条边作为屋脊方向(顶点索引) |
| 对比项 | roofGable | roofRidge |
|---|---|---|
| 屋脊方向选择 | 自动选择最长边 | 由 edgeIndex 显式指定 |
| 默认屋脊边 | N/A(自动) | 第 0 条边(edgeIndex=0) |
| 退化处理 | 无退化回退 | 指定边退化时回退到最长边 |
| 参数数量 | 最多 5 个 | 最多 6 个(多一个 edgeIndex) |
| 算法主体 | 完全相同 | 完全相同 |
顶点排序后,边按以下方式编号:
代码中对 edgeIndex 做了规范化处理:
edgeIndex = Math.floor(edgeIndex) % points.length;
if (edgeIndex < 0) edgeIndex += points.length;
图:五边形 footprint 的边编号。顶点按极角逆时针排序后,边按顺序编号。
if (!ridge || ridge.length() < 0.0001) { // Fall back to longest edge if specified edge is degenerate var longest = -1; for (var i=0; i<points.length; i++) { var p1 = points[i]; var p2 = points[(i+1) % points.length]; var len = new THREE.Vector2(p2.x-p1.x, p2.y-p1.y).lengthSq(); if (len > longest) { longest = len; ridge = new THREE.Vector2(p2.x-p1.x, p2.y-p1.y); } } }
如果用户指定的 edgeIndex 对应的边长度几乎为0(退化边),则自动回退到最长边策略,与 roofGable 的行为一致。
// 默认:第 0 条边作为屋脊,30 度坡角 Roof --> roofRidge(30) // 指定第 2 条边作为屋脊方向 Roof --> roofRidge(30, 0.5, 0.3, false, 2) // byHeight 模式,指定第 1 条边 Roof --> roofRidge('byHeight', 2.5, 0.3, 0.2, false, 1) // 负索引:最后一条边作为屋脊 Roof --> roofRidge(45, 0, 0, false, -1)
| 场景 | 推荐函数 | 原因 |
|---|---|---|
| L 形建筑,需要屋脊沿短臂方向 | roofRidge | roofGable 会自动选最长边,可能方向错误 |
| 长方形建筑,屋脊沿长边 | roofGable | 自动选择即可,无需额外指定 |
| 正多边形建筑 | roofPyramid | 各边等长,人字屋顶不适用 |
| 需要动态控制屋脊方向 | roofRidge | 可通过规则参数传入 edgeIndex |
function func_roofRidge(processor, angle, index) { var args = Array.prototype.slice.call(arguments, 1); var valueType = 'byAngle'; var value, overhangX = 0, overhangY = 0, even = false; var edgeIndex = 0; if (args[0] == 'byAngle' || args[0] == 'byHeight') { valueType = args[0]; value = args[1]; overhangX = args[2] || 0; overhangY = args[3] || 0; even = args[4] || false; edgeIndex = args[5] || 0; } else { value = args[0]; overhangX = args[1] || 0; overhangY = args[2] || 0; even = args[3] || false; edgeIndex = args[4] || 0; } if (!isNumeric(value)) throw 'roofRidge requires a numeric angle or height'; if (!isNumeric(overhangX) || !isNumeric(overhangY)) throw 'roofRidge overhangs must be numeric'; 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 'roofRidge needs a non-degenerate face normal'; var center = new THREE.Vector3(); top.vertices.forEach(v => center.add(v)); center.divideScalar(top.vertices.length); 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 points = top.vertices.map((v, i) => { var d = v.clone().sub(center); return { index: i, vertex: v, x: d.dot(xaxis), y: d.dot(yaxis), }; }); var c2 = points.reduce((a, p) => { a.x += p.x; a.y += p.y; return a; }, { x: 0, y: 0 }); c2.x /= points.length; c2.y /= points.length; points.sort((a,b) => Math.atan2(a.y-c2.y, a.x-c2.x) - Math.atan2(b.y-c2.y, b.x-c2.x)); // Use explicit edge index for ridge direction edgeIndex = Math.floor(edgeIndex) % points.length; if (edgeIndex < 0) edgeIndex += points.length; var a = points[edgeIndex]; var b = points[(edgeIndex+1) % points.length]; var ridge = new THREE.Vector2(b.x-a.x, b.y-a.y); if (!ridge || ridge.length() < 0.0001) { var longest = -1; for (var i=0; i<points.length; i++) { var p1 = points[i]; var p2 = points[(i+1) % points.length]; var len = new THREE.Vector2(p2.x-p1.x, p2.y-p1.y).lengthSq(); if (len > longest) { longest = len; ridge = new THREE.Vector2(p2.x-p1.x, p2.y-p1.y); } } } ridge.normalize(); // ... 以下与 roofGable 第 658~718 行完全相同 ... }
cgaprocessor.js 第 1573~1722 行源码分析。生成时间:2026-05-28。