FreePacking

在总平面图排布中运用角度场和矩形代理

作者 胡潜
时间
类目 技术分享

本文供建筑原型智能设计项目[ProtoMass]()算法脚本开发人员参考:如何用向量场让底面矩形构成符合建筑设计要求的总平面布局。阅读本文档前需要了解自研计算几何库ArchiGeoSharp和建筑体量原型的核心定义ProtoMass.Core。

目录

1 总述

1.1 它解决什么问题

场地边界往往是斜边、折线,不是正交网格。体块如果全部朝北,边会对不齐红线;如果各自乱转,整体又会显得散。

AngleGrid 是一张铺在场地上的 90° 周期方向场(交叉场 / 张量场):

它不是普通向量场:0° 与 90° 表示同一组正交轴(只是把哪条边当「宽」对调了)。所以格点写入时会自动 Mod90。

红线边方向 ──约束──► 格点打标 ──MathematicalSolver 平滑──► AngleGrid
                                              │
                    ┌─────────────────────────┴─────────────────────────┐
                    │  GetAngle(点)          流线 / 十字可视化            │
                    │  RectAgent 随场转向     BSP 沿场剖分                │
                    └───────────────────────────────────────────────────┘

1.2 分层:ags → utils → syntax → 脚本

脚本开发要同时看两层,不要只抄 syntax 里的两行,也不要绕过 utils 自己重写推挤。

┌─────────────────────────────────────────────────────────────────┐
│ ArchiGeoSharp                                                     │
│   ArchiGeo.Basic.Geometry.AngleGrid                               │
│   ArchiGeo.Basic.Geometry.AngleGridExtension   约束打标           │
│   ArchiGeo.MathematicalSolver.AngleGridExtension2  平滑 / 一键生成  │
└───────────────────────────────┬─────────────────────────────────┘
                                │ PolySmoothGrid / GetAngle / SmoothGrid
┌───────────────────────────────▼─────────────────────────────────┐
│ ProtoMass.Library · utils                                        │
│   RectAgent1(质点推挤 + UpdateDir 随场转向)                      │
│   DistributionScheme.FieldDir_SeparatePos_AnySize_RectsIn         │
│   职责:矩形代理如何「跟着场转 + 互不重叠 + 不越界」               │
└───────────────────────────────┬─────────────────────────────────┘
                                │ 先造初始矩形,再交给场驱动松弛
┌───────────────────────────────▼─────────────────────────────────┐
│ ProtoMass.Library · syntax / volume                               │
│   SyntaxFreeRepeats / SyntaxFreeSimilar / SyntaxMixedTemplate     │
│   VolumeSubdivideOffset(场 + BSP 剖分)                          │
│   职责:场地指标 → 数量/尺寸 → 调用 utils → 得到底面矩形 / VBB    │
└───────────────────────────────┬─────────────────────────────────┘
                                │ 脚本照同一条链写,只是类型带 Geo. 前缀
┌───────────────────────────────▼─────────────────────────────────┐
│ scripts/template/*.cs                                             │
│   你实现 ISyntaxBox / ISyntaxMixed / IVolume*                      │
└─────────────────────────────────────────────────────────────────┘

一句话:

本文后4章对应:2 = ags 场;3 = 矩形代理;4 = utils 方案 + syntax 如何串起来;5 = BSP、FAQ、速查。

1.3 脚本最小引用

PolySmoothGrid / SmoothGrid 在 MathematicalSolver 求解器程序集里,不是 AngleGrid 所在的 Basic 程序集。缺任一引用,扩展方法会「找不到」。

#r "ref/ProtoMass.Public.dll"
#r "ref/ProtoMass.Library.dll"
#r "ref/ArchiGeoSharp.Basic.dll"
#r "ref/ArchiGeoSharp.MathematicalSolver.dll"
#r "ref/ArchiGeoSharp.OpenNurbs.dll"
#r "ref/ArchiGeoSharp.Depute.dll"

using ArchiGeo.MathematicalSolver;        // PolySmoothGrid / SmoothGrid
using Geo = ArchiGeo.Basic.Geometry;      // AngleGrid / Polyline / Rect
using ProtoMass.Utils;                    // DistributionScheme / RectAgent1

脚本里 Rhino 的 Polyline 与 ArchiGeo 的 Polyline 重名,所以几何类型一律写 Geo.Polyline、Geo.AngleGrid、Geo.Rect。

库内等价写法是直接 using ArchiGeo.Basic.Geometry;,因此没有 Geo. 前缀。逻辑相同。

本机需能加载 MathematicalSolver。SmoothGrid 默认时限 10 秒;场地很大时第一次调用会明显停顿。


2 AngleGrid 相关

网格覆盖一个轴对齐矩形:原点 BasePt,格距 Unit,X 向 XUnitCount 格、Y 向 YUnitCount 格。格点矩阵尺寸是 (XUnitCount+1) × (YUnitCount+1)。

2.1 AngleGrid API

命名空间:ArchiGeo.Basic.Geometry。

构造

new Geo.AngleGrid(int xUnitCount, int yUnitCount, double unit)
new Geo.AngleGrid(int xUnitCount, int yUnitCount, double unit, Geo.Point basePt)
new Geo.AngleGrid(Geo.Angle[,] valueMatrix, double unit)   // BasePt = 原点

新建的网格角度全是默认 0°。不打约束、不 SmoothGrid,全场都是正交世界轴,没有「贴边」效果。

属性

成员 类型 含义
AngleMatrix Angle[,] 格点角度矩阵
this[int i, int j] Angle 读写格点;写入自动 Mod90
Unit double 格距
ColCount / RowCount int 格点数(= 格数 + 1)
XUnitCount / YUnitCount int 格数
Width / Height double 覆盖宽高 = 格数 × Unit
BasePt Point 网格原点(通常取红线 AABB 角点)

索引 i 沿 X,j 沿 Y。越界读写会抛异常。

查询

Geo.Point  GetPoint(int i, int j)
Geo.Angle  GetAngle(int i, int j)
Geo.Angle  GetAngle(int i, int j, double rx, double ry)  // 格内双线性;rx,ry ∈ [0,1]
Geo.Angle  GetAngle(Geo.Point pt)                        // 世界坐标 → 插值角度
bool       PtInRange(Geo.Point pt)

脚本里几乎只用 GetAngle(pt):给矩形中心,得到该处十字方向。点在网格外时返回默认角 0°。

插值在「四倍角」空间里做,所以 0° 与 89° 会被当成接近的方向(它们对应几乎同一组十字),而不是差了 89 度的两个向量。

取到的角度仍在 [0, 90)。矩形真正朝向常再加 k * 90(k = 0,1,2,3),表示把哪条边当宽。

流线与可视化

Geo.Polyline GenerateSinglePath(
    Geo.Point startPt,
    double stepDist = 1,
    Geo.Angle minAngle = default,   // 起点限制在 [minAngle, minAngle+90)
    int countLim = 100)

Geo.LineSeg[] GenerateCrossRep(
    double repUnit = 0,             // 采样间距;≤0 则用 Unit
    double crossLength = 0)         // 十字臂长;≤0 则为 repUnit/5

日常排布 不必 自己走流线;Library 的 BSP 剖分在内部沿场割缝。需要看场长什么样时,用十字表示即可:

Geo.LineSeg[] crosses = ag.GenerateCrossRep(repUnit: 8, crossLength: 2);
var doc = RhinoDoc.ActiveDoc;
for (int i = 0; i < crosses.Length; i++)
{
    var a = crosses[i].StartPt;
    var b = crosses[i].EndPt;
    doc.Objects.AddLine(new Rg.Point3d(a[0], a[1], 0), new Rg.Point3d(b[0], b[1], 0));
}

2.2 AngleGridExtension API(约束打标)

命名空间:ArchiGeo.Basic.Geometry
作用:在平滑之前,把「这些格点必须朝哪」写进网格,并打标。

打标用的是 静态 MarkMatrix。同一时刻只能服务一张正在编辑的网格;每次新场必须先 InitMarkMatrix()。

方法 签名 作用
InitMarkMatrix void InitMarkMatrix(this AngleGrid ag) 分配全 false 的标记矩阵,尺寸与 ag 一致
ReadMark bool ReadMark(this AngleGrid ag, int i, int j) 该格是否被约束
AddDirectNomination void AddDirectNomination(this AngleGrid ag, int i, int j, Angle angle) 指定格点角度(Mod90)并打标
AddLineSegConstraint void AddLineSegConstraint(this AngleGrid ag, LineSeg ls, double bufferDist) 线段缓冲带内格点对齐线段方向
AddPolyConstraint void AddPolyConstraint(this AngleGrid ag, IPoly poly, double bufferDist) 折线每一段都做线段约束(红线贴边)
AddRadialConstraint void AddRadialConstraint(this AngleGrid ag, Point center, double bufferDist) 圆心缓冲区内格点朝向径向(再 Mod90)
AddRadialConstraint void AddRadialConstraint(this AngleGrid ag, ICircleExpr c) 同上,半径取圆半径

bufferDist 是半带宽:线段约束内部会构造「长 = 线段长、宽 = 2 * bufferDist」的定向矩形,落在里面的格点被钉死。

只打标、不平滑:被钉死的格点有方向,其余仍是 0°,场是硬切的。要内部过渡,必须再调用下一节的 SmoothGrid。

2.3 AngleGridExtension2 API(平滑)

命名空间:ArchiGeo.MathematicalSolver
作用:把已打标的网格交给 MathematicalSolver,最小化相邻格点方向差,得到平滑场。

SmoothGrid

Geo.AngleGrid SmoothGrid(this Geo.AngleGrid input, double timeLim = 10)

标准四步(扩展方法都挂在 AngleGrid / Polyline 上,不必写类名):

Geo.AngleGrid input = new Geo.AngleGrid(20, 20, 30, basePt);
input.InitMarkMatrix();
input.AddPolyConstraint(boundary, bufferDist);
input.AddRadialConstraint(center, radialBuffer);   // 可选
Geo.AngleGrid ag = input.SmoothGrid();             // 可传 timeLim

返回 新网格,不改 input。未打标的格点由求解器填;已打标的格点保持约束方向。

PolySmoothGrid(脚本默认入口)

Geo.AngleGrid PolySmoothGrid(this Geo.Polyline poly)

内部自动:

  1. 取 poly 的轴对齐包围盒(AABB)
  2. unit = (int)(AABB.W / 20),大约 20 格宽
  3. 网格原点 = AABB 角点
  4. InitMarkMatrix + AddPolyConstraint(poly, unit * 2) + SmoothGrid()

红线贴边、内部平滑,这一条就够大多数 Syntax。

场地 AABB 宽度小于 20 时 unit 会变成 0,随后会除零。控制线过小或单位不是米时不要用这一键方法,改用手造网格(见 2.5)。

PolyAxisSmoothGrid

在红线约束之外,再钉一批轴线:

Geo.AngleGrid PolyAxisSmoothGrid(this Geo.Polyline poly, Geo.LineSeg[] axes, double[] axesbuffer = null)
Geo.AngleGrid PolyAxisSmoothGrid(this Geo.Polyline poly, ILineExpr[] axes, double[] axesbuffer = null)

有明确道路、景观轴、建筑主轴时用这个,比纯 PolySmoothGrid 更能「顺着轴排」。

2.4 相关 Angle 行为

读场、写约束时会碰到这些:

成员 含义
Mod90 收到 [0, 90)。格点写入、约束写入都走它
limitTo(min, cycle) 收到 [min, min+cycle)。UpdateDir 用 limitTo(当前-45°, 90°) 防止跳轴
UnitVec 单位方向向量,画十字、走流线用
angle + k * 90 在同一组十字上改「哪条边当宽」

矩形初始朝向用 GetAngle(pt) + k*90 是刻意的:场只决定正交框架,不决定宽边朝哪一侧。

2.5 进阶:自定义约束后再平滑

PolySmoothGrid 不够时(要径向、要轴线、要改格距),手走四步。

Geo.Rect aabb = siteBoundary.AxisAlignedBoundingBox();
double unit = Math.Max(4, aabb.W / 20);            // 避免 unit==0
Geo.Point basePt = aabb.BasePt;
int nx = (int)(aabb.W / unit) + 1;
int ny = (int)(aabb.D / unit) + 1;

Geo.AngleGrid input = new Geo.AngleGrid(nx, ny, unit, basePt);
input.InitMarkMatrix();
input.AddPolyConstraint(siteBoundary, unit * 2);

// 可选:景观轴
input.AddLineSegConstraint(axisSeg, unit * 2);

// 可选:圆心辐射(庭院、塔楼)
input.AddRadialConstraint(courtCenter, courtRadius);

Geo.AngleGrid ag = input.SmoothGrid(timeLim: 10);

有一批轴线时优先 PolyAxisSmoothGrid,少写样板代码。

InitMarkMatrix 必须在任何 Add*Constraint 之前调用。标记矩阵是静态的:连续造两张场时,第二张也要重新 InitMarkMatrix,否则会读到上一张的打标。

造好的 ag 再交给第4章的 FieldDir_*(或自己用 RectAgent1)。


3 RectAgent 相关

场造好后,矩形如何跟着走,由 ProtoMass.Utils.RectAgent1 负责。日常排布优先走第4章的 DistributionScheme.FieldDir_*(内部就是它);只有要改力、改停止条件、或可视化推挤过程时,才直接调本节 API。

命名空间:ProtoMass.Utils。脚本里已有 using ProtoMass.Utils; 即可。

3.1 设计约定

RectAgent1 继承 Geo.Rect,在矩形上加了质点动力学。约定:

做 不做
平移(质心运动) 伸缩(W / D 不变)
推挤产生力,沿中心连线分开 推挤产生转矩(不会被撞转)
方向只跟位置有关:随 AngleGrid 转,或始终不变 方向跟邻居碰撞有关

需要「被撞会转」时用 RectAgent2——该类目前只有构造函数,尚未实现,不要用。

3.2 构造、继承、状态

public RectAgent1(Geo.Point center, double width, double depth, Geo.Angle dir)

继承 Rect,脚本里可直接用这些几何成员:

成员 含义
Center 中心点;UpdatePos 会改它
W / D 宽、深,过程中不变
Angle 朝向;UpdateDir 会改它
Area W * D
RelateSimp(...) 与另一矩形 / 折线的内含·相交·相离

自身状态:

成员 类型 含义
Velocity Vector(只读) 当前速度
Acceleration Vector(可读写) 当前加速度;一步结束时 UpdatePos 会清零
ToRect Rect 拷成普通矩形(丢掉速度)

初始速度、加速度都是 (0,0)。

3.3 积分:UpdatePos / UpdateDir

每步必须先把力累进 Acceleration,再积分。顺序固定为 先动后转:

void UpdatePos()                 // v += a; Center += v; a = 0
void UpdateDir(Geo.AngleGrid ag) // 随场转向,可省略(方向锁定时不调)

UpdatePos:把当前加速度积到速度,再把速度积到中心,然后把加速度清零。下一步的力必须重新累加。

UpdateDir:ag.GetAngle(Center),再 limitTo(当前角 - 45°, 90°)。跟着场转,但不会突然跳 90°。场本身不产生力,只改朝向。

方向锁定(例如全部平行于 OBB)时只调 UpdatePos,不要调 UpdateDir。AlignedDir_* 方案就是这样做的。

3.4 力:Acc_*(直接累加)

把力直接加到自己的 Acceleration 上。成对方法会对 两个 代理施加大小相等、方向相反的力,每对只调一次。

void Acc_VelocityDecay(double decayRate)
void Acc_KeepDistcance(RectAgent1 other, double accVecMag, double minDist = 0)
void Acc_NoOverlap(RectAgent1 other, double accVecMag)
void Acc_InsideBoundary(IPoly ipoly, double accVecMag, double minDistToBoundary = 0)

方法名里的 KeepDistcance 是库内既有拼写,脚本必须照写。

方法 何时有力 方向 成对?
Acc_VelocityDecay 始终(速度的反向) -sign(decayRate) * Velocity 否
Acc_KeepDistcance 相交,或净距 < minDist 沿两中心连线分开 是:this += acc,other -= acc
Acc_NoOverlap 非 OUTSIDE(相交 / 接触) 同上 是
Acc_InsideBoundary 未完全在边界内,或到边距离 < minDistToBoundary 指向场内(沿中心到边界最近点) 否

accVecMag 是力的模长,不是物理单位加速度。场地变大时要跟着放大,否则代理几乎不动。库内经验值见 3.6。

Acc_KeepDistcance 比 Acc_NoOverlap 多一层净距,排布里优先用前者。

源码注释里的推荐套用法(直接累加版):

for (int i = 0; i < agents.Count; i++)
{
    agents[i].Acc_InsideBoundary(boundary, accVecMag, boundDist);
    agents[i].Acc_VelocityDecay(0.5);
    for (int j = i + 1; j < agents.Count; j++)
        agents[i].Acc_KeepDistcance(agents[j], accVecMag, minDist);
}

j 从 i + 1 起,避免同一对加两次(第二次会把力抵消或加倍)。

3.5 力:GetAcc_*(返回向量)

与上一节一一对应,只返回向量,不改状态。库内 FieldDir_* 走这条,便于按场地尺度乘 step、自己决定加到哪一侧。

Geo.Vector GetAcc_VelocityDecay(double decayRate)
Geo.Vector GetAcc_KeepDistcance(RectAgent1 other, double accVecMag, double minDist = 0)
Geo.Vector GetAcc_NoOverlap(RectAgent1 other, double accVecMag)
Geo.Vector GetAcc_InsideBoundary(IPoly ipoly, double accVecMag, double minDistToBoundary = 0)

成对力必须两侧都写:

Geo.Vector acc = agents[i].GetAcc_KeepDistcance(agents[j], mag, minDist);
agents[i].Acceleration += acc;
agents[j].Acceleration -= acc;

GetAcc_VelocityDecay(0.5) 返回 -0.5 * Velocity。紧接着 UpdatePos 会做 v += a,等效于每步速度减半。decayRate 取绝对值,符号无意义。

两中心重合时,分开方向用随机单位向量,避免除零。

3.6 标准迭代循环

手写推挤时按这个骨架。FieldDir_* 就是它再加 UpdateDir。

int n = agents.Count;
double area = Math.Abs(boundary.Area);
int step = (int)(Math.Sqrt(area) / 100) + 1;   // 场地越大,力越大
int maxIter = 10000;

for (int iter = 0; iter < maxIter; iter++)
{
    for (int i = 0; i < n; i++)
    {
        agents[i].Acceleration += agents[i].GetAcc_InsideBoundary(boundary, 1.0 * step, boundDist);
        agents[i].Acceleration += agents[i].GetAcc_VelocityDecay(0.5);
        for (int j = i + 1; j < n; j++)
        {
            Geo.Vector acc = agents[i].GetAcc_KeepDistcance(agents[j], 0.6 * step, rectsDist);
            agents[i].Acceleration += acc;
            agents[j].Acceleration -= acc;
        }
    }
    for (int i = 0; i < n; i++)
    {
        agents[i].UpdatePos();
        agents[i].UpdateDir(ag);   // 方向锁定则删掉这行
    }
}
Geo.Rect[] baseRects = agents.ToRects();

库内在 iter > 100 之后,若所有代理加速度模长都 ≤ 0.5 会提前结束。手写时同样建议加停止条件,不要死跑 maxIter。

典型系数(与 FieldDir_* 一致):

项 值
边界力模长 1.0 * step
间距力模长 0.6 * step
速度衰减 0.5(每步速度减半)
step (int)(√场地面积 / 100) + 1

3.7 RectAgentExtension / RectAgent2

Geo.Rect[] ToRects(this RectAgent1[] agents)
Geo.Rect[] ToRects(this IEnumerable<RectAgent1> agents)

交给 HeightScheme 之前转成普通 Rect[]。ToRect 是单个;ToRects 是一批。

RectAgent2:预留给「推挤产生转矩」的代理,目前不能用。


4 Util 和 Syntax 的具体实现

库内 SyntaxFreeRepeats(模块复制)与 SyntaxFreeSimilar(基座散置)是同一条链,只在「初始矩形」上不同。脚本应复制这条链,而不是只调用 PolySmoothGrid。

4.1 为什么必须两层一起看

只做 AngleGrid 只做 RectAgent
有场,矩形不会自己排开 FieldDir_* 没有 ag 就无法转向
GetAngle 得到方向,但重叠、越界还在 推挤循环已经写好,不必在 syntax 里复制

syntax 的职责只是:定 n 和尺寸 → 抽样初始矩形 → 建场 → 交给 FieldDir_* → 赋高。

FieldDir_* 封装了第3章的循环;syntax / 脚本不要再写一遍,除非要改力。

4.2 DistributionScheme.FieldDir_*

大多数脚本停在这一层,不必自己管 RectAgent1。

public static Geo.Rect[] FieldDir_SeparatePos_AnySize_RectsIn(
    Geo.Polyline boundary,
    Geo.AngleGrid ag,
    Geo.Rect[] rects,
    double rectsDist = 4,     // 矩形之间最小净距
    double boundDist = 2,     // 离红线最小距离
    int maxIter = 1000)

输入 rects 真正用到的是 个数 / 长 / 宽;初始方向和位置会被场和推挤改掉。

内部循环:

  1. 每个矩形做成 RectAgent1,初始角 = ag.GetAngle(中心) + k*90(k 随机 0–3)
  2. 每步用 GetAcc_* 累加:不越界、速度衰减、两两保持间距(系数见 3.6)
  3. UpdatePos() 再 UpdateDir(ag)
  4. 迭代够久且加速度足够小则提前结束
  5. ToRects() 返回

造好的 ag 若来自 2.5 的自定义场,同样传进本方法。

4.3 造「初始矩形」

场驱动松弛需要先有一批矩形。Syntax 先抽样、再交给上一节。

// 固定宽深、随机位置、随机朝向(模块复制)
Geo.Rect[] RandDir_RandPos_SameSize_RectsIn(
    Geo.Polyline boundary, int rectNum, double rectW, double rectD)

// 面积在 averageArea 附近波动、长宽比可配、随机位置(基座散置)
Geo.Rect[] RandDir_RandPos_RandSize_RectsIn(
    Geo.Polyline boundary, int rectNum, double averageArea,
    double aspectRatioMin = 0.5, double aspectRatioMax = 2.0)

RandSize 版本实际个数可能少于 rectNum(AABB 内随机点多次仍放不进红线时会放弃)。脚本里应对空数组做回退。

尺寸方案(间距、单体宽深)用:

SizeNumScheme sns = SizeNumScheme.SetNumAverageRect(siteArea, site.BuildingDensity, n);
// sns.RectSize1 / RectSize2 / SpacingSize / UnitNum

4.4 Syntax:模块复制

对应 ISyntaxBox.GetVolumeBoundingBoxes 的底面排布段(同尺寸 + 场方向):

Geo.Polyline siteBoundary = new Geo.Polyline(site.Boundary, true) ^ 2;
siteBoundary.AntiClockwise();
double siteArea = Math.Abs(siteBoundary.Area);

SizeNumScheme sns = SizeNumScheme.SetNumAverageRect(siteArea, site.BuildingDensity, n);
double unitWidth = sns.RectSize1;
double unitDepth = sns.RectSize2;

Geo.Rect[] initialRects = DistributionScheme.RandDir_RandPos_SameSize_RectsIn(
    siteBoundary, n, unitWidth, unitDepth);

Geo.AngleGrid ag = siteBoundary.PolySmoothGrid();   // ags:红线 → 平滑场

Geo.Rect[] baseRects = DistributionScheme.FieldDir_SeparatePos_AnySize_RectsIn(
    siteBoundary, ag, initialRects, sns.SpacingSize, boundDist: 2, maxIter: 10000);

^ 2 把点投到 2D。库内 site.ControlBoundaryPoly2d() 就是这两行;脚本里扩展方法不一定导出,请手写。

完整可运行文件:scripts/template/ScriptSyntaxB1cFreeRepeats.cs。

4.5 Syntax:基座散置

唯一差别:初始矩形用面积抽样(变尺寸 + 场方向)。

double unitArea = unitWidth * unitDepth;
Geo.Rect[] initialRects = DistributionScheme.RandDir_RandPos_RandSize_RectsIn(
    siteBoundary, n, unitArea);

Geo.AngleGrid ag = siteBoundary.PolySmoothGrid();
Geo.Rect[] baseRects = DistributionScheme.FieldDir_SeparatePos_AnySize_RectsIn(
    siteBoundary, ag, initialRects, sns.SpacingSize, 2, 10000);

完整可运行文件:scripts/template/ScriptSyntaxB1cFreeSimilar.cs。

4.6 脚本完整片段

下面可直接放进 ISyntaxBox 实现。前置 #r / using 见 1.3。

public VolumeBoundingBox[] GetVolumeBoundingBoxes(
    SiteSlice site, double totalAreaOccupyRate, double bottomAreaOccupyRate)
{
    site = site.PreTreat(totalAreaOccupyRate, bottomAreaOccupyRate);

    int n = site.VolumeNum != null && site.VolumeNum.Length == 1 && site.VolumeNum[0] > 0
        ? site.VolumeNum[0]
        : 6;

    Geo.Polyline siteBoundary = new Geo.Polyline(site.Boundary, true) ^ 2;
    siteBoundary.AntiClockwise();
    double siteArea = Math.Abs(siteBoundary.Area);

    SizeNumScheme sns = SizeNumScheme.SetNumAverageRect(siteArea, site.BuildingDensity, n);
    double unitWidth = sns.RectSize1;
    double unitDepth = sns.RectSize2;

    Geo.Rect[] initialRects = DistributionScheme.RandDir_RandPos_SameSize_RectsIn(
        siteBoundary, n, unitWidth, unitDepth);
    if (initialRects == null || initialRects.Length == 0)
        return Array.Empty<VolumeBoundingBox>();

    Geo.AngleGrid ag = siteBoundary.PolySmoothGrid();

    Geo.Rect[] baseRects = DistributionScheme.FieldDir_SeparatePos_AnySize_RectsIn(
        siteBoundary, ag, initialRects, sns.SpacingSize, 2, 10000);

    double coverage = 0;
    for (int i = 0; i < baseRects.Length; i++)
        coverage += baseRects[i].Area;
    if (coverage < 1e-6)
        coverage = n * unitWidth * unitDepth;

    double averageHeight = site.FloorAreaRatio * siteArea / coverage * HeightScheme.FLOOR_HEIGHT;
    return HeightScheme.AllSame(baseRects, site.HeightLim, averageHeight);
}

5 其他附加内容

5.1 Volume 侧:场 + BSP 剖分

VolumeSubdivideOffset 不拿场去转矩形,而是拿场去 沿流线剖分场地:

Geo.AngleGrid field = siteInside.PolySmoothGrid();
var adapt = new Geo.BspAdaptiveOptions
{
    MaxDepth = 3,
    MinLeafArea = areaInside * 0.08,
    MinElongation = 0.28,
    Ratio = 0.5,
};
Geo.Polyline[] blocks = siteInside.SubdivideBspAdaptive(field, adapt).Blocks();

SubdivideBspAdaptive / SubdivideBsp 是 Polyline 的扩展方法(Basic)。场决定割缝方向;叶多边形再各自做成体量。这是「场 → 地块」,与第4章「场 → 矩形朝向」并列的第二条产品路径。

5.2 常见问题

PolySmoothGrid 编译不过 / 运行时找不到扩展方法
缺 using ArchiGeo.MathematicalSolver;,或没引用 ArchiGeoSharp.MathematicalSolver.dll。见 1.3。

场全是横平竖直,完全不贴红线
只 new AngleGrid(...) 就 GetAngle,没有 AddPolyConstraint + SmoothGrid。用 PolySmoothGrid,或走 2.5 四步。

体块重叠或跑出红线
场只负责朝向。位置必须经 FieldDir_SeparatePos_AnySize_RectsIn(或自己调 RectAgent1 的边界力 / 间距力)。不要只对每个随机点 GetAngle 后直接 new Rect。

手写 RectAgent1 时矩形对穿、力抵消或飞出
成对力(Acc_KeepDistcance / GetAcc_KeepDistcance)每对只处理一次(j = i + 1)。GetAcc_* 不会改状态,忘了 Acceleration += 等于没受力。UpdatePos 之后加速度已被清零,下一步必须重新累加。accVecMag 过小则几乎不动,过大则飞出红线;按 √面积 / 100 取 step。

KeepDistcance 编译不过
库内拼写是 KeepDistcance(少一个 a),不是 KeepDistance。

想改力却去改 FieldDir_* 的参数不够用
rectsDist / boundDist / maxIter 只覆盖间距、退界、迭代上限。要加新力、改衰减、画力箭头,直接用 RectAgent1 按 3.6 写循环。

GetAngle 总是 0
点落在网格外(PtInRange 为 false)。PolySmoothGrid 的网格是红线 AABB;AABB 外的点没有定义。先保证矩形中心在红线内。

第一次排布很慢
MathematicalSolver 在解整张格网。可加大 unit 减格点,或把 SmoothGrid(timeLim) 调小(质量下降)。FieldDir_* 的 maxIter 也会耗时间,与求解器是两段。

场地很小就崩溃
PolySmoothGrid 用 (int)(AABB.W / 20) 当地板格距。宽度 < 20 时为 0。改用手造网格并 Math.Max 格距。

想「全部平行于某一边」而不是平滑过渡
不要用场。用 DistributionScheme.AlignedDir_* 一类方法,方向取 OBB 或指定 Angle。场是给非正交、需要随边旋转的原型用的。

5.3 API 速查

ags · AngleGrid

构造  (xUnitCount, yUnitCount, unit[, basePt]) | (Angle[,], unit)
读写  this[i,j]  Unit  ColCount  RowCount  XUnitCount  YUnitCount
      Width  Height  BasePt  AngleMatrix
查询  GetPoint(i,j)  GetAngle(i,j)  GetAngle(i,j,rx,ry)  GetAngle(pt)  PtInRange(pt)
可视化 GenerateSinglePath(start, stepDist=1, minAngle=0, countLim=100)
      GenerateCrossRep(repUnit=0, crossLength=0)

ags · AngleGridExtension

InitMarkMatrix()  ReadMark(i,j)  AddDirectNomination(i,j,angle)
AddLineSegConstraint(ls, bufferDist)
AddPolyConstraint(poly, bufferDist)
AddRadialConstraint(center, bufferDist) | AddRadialConstraint(circle)

ags · AngleGridExtension2(需 using ArchiGeo.MathematicalSolver)

SmoothGrid(timeLim=10)
Polyline.PolySmoothGrid()
Polyline.PolyAxisSmoothGrid(LineSeg[] axes, double[] axesbuffer=null)
Polyline.PolyAxisSmoothGrid(ILineExpr[] axes, double[] axesbuffer=null)

Library · RectAgent1

构造  (center, width, depth, dir)          继承 Rect:Center / W / D / Angle
状态  Velocity  Acceleration  ToRect
积分  UpdatePos()  UpdateDir(ag)
Acc_  Acc_VelocityDecay(decayRate)
      Acc_KeepDistcance(other, mag, minDist=0)     // 成对,每对一次
      Acc_NoOverlap(other, mag)
      Acc_InsideBoundary(poly, mag, minDist=0)
GetAcc_  同上四个,返回 Vector,不改状态
扩展  agents.ToRects()
勿用  RectAgent2(未实现)

Library · utils 方案

DistributionScheme.FieldDir_SeparatePos_AnySize_RectsIn(boundary, ag, rects, rectsDist=4, boundDist=2, maxIter=1000)
DistributionScheme.RandDir_RandPos_SameSize_RectsIn(boundary, n, w, d)
DistributionScheme.RandDir_RandPos_RandSize_RectsIn(boundary, n, averageArea, ...)
SizeNumScheme.SetNumAverageRect(siteArea, density, n)

Library · syntax 调用顺序

site.PreTreat
→ 控制线 2D(脚本手写 Polyline ^ 2 + AntiClockwise)
→ SizeNumScheme 定 n / 宽深 / 间距
→ RandDir_* 初始矩形
→ PolySmoothGrid(或自定义约束 + SmoothGrid)
→ FieldDir_SeparatePos_AnySize_RectsIn
→ HeightScheme 赋高 → VolumeBoundingBox[]