roofRidge 函数实现详解

本文档详细解析 cgaprocessor.jsroofRidge 函数的实现。该函数与 roofGable 算法核心几乎完全一致,区别在于 roofRidge 允许显式指定屋脊边,而非自动选择最长边。

一、函数定位

roofRidgeroofGable变体/扩展版本。两者都生成人字屋顶(两面坡),但 roofRidge 通过 edgeIndex 参数让用户精确控制哪条边作为屋脊方向。

当你需要屋顶的屋脊沿特定方向(而非自动选择的最长边)时,使用 roofRidge;当只需最自然的屋脊方向时,使用 roofGable

二、源码实现(第 1573~1722 行)

2.1 参数解析

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;
  }

2.2 核心算法(与 roofGable 完全一致)

  // ... 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 完全相同:坐标旋转、高度计算、面片构建 ...

三、参数说明

参数类型默认值说明
valueTypeString'byAngle''byAngle''byHeight'
valueNumber必填坡角度数或屋脊高度
overhangXNumber0垂直屋脊方向悬挑
overhangYNumber0平行屋脊方向悬挑
evenBooleanfalse使用平均高度
edgeIndexNumber0指定哪条边作为屋脊方向(顶点索引)

四、与 roofGable 的核心差异

对比项roofGableroofRidge
屋脊方向选择自动选择最长边edgeIndex 显式指定
默认屋脊边N/A(自动)第 0 条边(edgeIndex=0)
退化处理无退化回退指定边退化时回退到最长边
参数数量最多 5 个最多 6 个(多一个 edgeIndex)
算法主体完全相同完全相同

五、edgeIndex 的确定方式

顶点排序后,边按以下方式编号:

代码中对 edgeIndex 做了规范化处理:

edgeIndex = Math.floor(edgeIndex) % points.length;
if (edgeIndex < 0) edgeIndex += points.length;
0 1 2 3 4 edge 0 edge 1 edge 2 edge 3 edge 4 若 edgeIndex=2,屋脊沿此方向

图:五边形 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)

八、何时使用 roofRidge 而非 roofGable?

场景推荐函数原因
L 形建筑,需要屋脊沿短臂方向roofRidgeroofGable 会自动选最长边,可能方向错误
长方形建筑,屋脊沿长边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。