📐 架构总览
CGA.js 引擎是一个分层架构的 CGA 规则解析与三维生成系统,核心流程为:源代码 → 解析 → AST → 求值 → 几何生成。
技术栈
| 层级 | 技术 | 说明 |
|---|---|---|
| Parser | ANTLR4 + TypeScript | LL(*) 解析器,自动生成 Lexer/Parser/Visitor |
| AST | TypeScript Interfaces | 强类型抽象语法树 |
| Evaluator | TypeScript | 递归下降求值 + 函数注册表 |
| Geometry | Three.js (BufferGeometry) | 3D 网格生成与变换 |
| Build | Vite | ESM/CJS 双格式输出 |
⚙️ 解析管线(Pipeline)
从 CGA 源码到三维模型的完整处理流程:
预处理(Preprocess)
normalizeBareStochasticBranches() + preprocessCityEngineSyntax()
处理缩进式随机分支、CityEngine 扩展语法转换
词法分析(Lexical Analysis)
CharStreams → CGAGrammarLexer → CommonTokenStream
将源码转换为 Token 序列(IDENT、FLOAT、STRING、关键字等)
语法分析(Syntax Analysis)
CGAGrammarParser → parse tree
ANTLR4 根据 Grammar 规则生成解析树,同时收集 Ambiguity 警告
AST 构建(AST Building)
ASTBuilder (Visitor) → CGAScript AST
遍历解析树,构建类型化的抽象语法树
求值执行(Evaluation)
evaluate() → Shape[]
递归求值 AST,调用几何函数,生成三维 Shape 树
序列化输出(Serialization)
Shape → JSON / OBJ / GLB / STL
将几何数据导出为前端可渲染的格式
🏗️ 分层设计
🟦 Parser 层 — 语法解析
负责将 CGA 源码转换为结构化 AST。核心文件包括 Grammar 定义、ANTLR 生成器和 AST Builder。
🟩 Evaluator 层 — 规则求值
遍历 AST,执行 operation 链,管理递归深度、变量作用域和随机种子。通过函数注册表分派到具体实现。
🟪 Geometry 层 — 三维计算
纯函数集合,接收 Shape 输入,返回变换后的 Shape。包括基本体创建、布尔运算、UV 投影、屋顶生成等。
🔍 Parser 层详解
Grammar 结构
Grammar 采用 ANTLR4 格式,核心规则包括:
script— 入口:version、import、attr、const、func、style、rule 的集合ruleDef— 规则定义:ident(params)? --> ruleBodyruleBody— 规则体:operationChain / conditional / stochastic / assignmentoperation— 操作:simple / comp / split / pushPop / block / tempShapeexpression— 表达式:算术、关系、逻辑、成员访问、数组、条件表达式
关键设计:通用 Block 解析
blockOperation 是一个 catch-all 规则,匹配所有 ident(args){body} 形式的代码。
这意味着 setback、offset、inline 以及未来的新 block 函数,都不需要修改 Grammar。
// Grammar 中的 blockOperation(通用规则)
blockOperation
: ident ('(' argList? ')')? blockBody (operationSuffix? blockBody)*
;
// 以下都被解析为同一个 BlockOperation AST 节点:
setback(1) { front: A | back: B }
offset(1) { inside: A | border: B }
inline("recompose") { A } { B }
myNewFunc(10) { body } // ← 未来新函数,自动兼容
Parser 回归测试
216 个测试覆盖所有 operation 类型、表达式类型和边缘 case。任何 Grammar 修改都必须通过这些测试。
| 测试组 | 数量 | 覆盖 |
|---|---|---|
| Basic Structures | 11 | 空文件、多规则、声明、注解、注释 |
| Simple Operations | 60+ | 所有内置函数解析 |
| Block / Comp / Split | 35 | 块操作、组件选择、分割轴 |
| Expressions | 40 | 运算符、数组、索引、成员访问 |
| Complex Nesting | 8 | 多层嵌套链 |
| Edge / Error Cases | 17 | 边界值、错误检测 |
🧮 Evaluator 层详解
求值上下文(EvalContext)
每个编译请求创建一个独立的求值上下文,包含:
script— 完整的 CGAScript ASTglobals— 全局变量(attr、const)userFuncs— 用户自定义函数currentDepth / maxDepth— 递归深度控制(防死循环)traceLog— 执行追踪日志(调试用)
Operation 分派
Evaluator 根据 AST 节点类型分派到对应处理器:
evalOperation(op, shape, ctx)
├─ SimpleOperation → evalSimpleOperation(name, args, shape, ctx)
├─ CompOperation → evalCompOperation(selector, branches, shape, ctx)
├─ SplitOperation → evalSplitOperation(axis, branches, shape, ctx)
├─ PushPopOperation→ evalRuleBody(body, shape, ctx)
├─ BlockOperation → evalBlockOperation(op, shape, ctx)
└─ TempShapeOperation → evalRuleBody(body, shape, ctx)
📐 Geometry 层详解
Geometry 层是纯函数集合,按功能分为多个目录:
| 目录 | 函数 | 说明 |
|---|---|---|
create/ | extrude, roof*, primitive*, insert, scatter, taper | 创建新几何体 |
split/ | comp, setback, offset, splitArea, innerRectangle, shapeL/U/O | 面/边分割与退缩 |
transform/ | alignScope*, mirrorScope, rotateScope, setPivot | 坐标系变换 |
material/ | color, texture, setupProjection, projectUV, *UV | 材质与 UV |
boolean/ | union, subtract, intersect | 布尔运算 |
manipulate/ | cleanupGeometry, deleteHoles, softenNormals, rectify | 几何清理 |
scope/ | t, s, r, center, mirror | Scope 变换 |
📋 函数注册表(Function Registry)
为了避免 evaluator.ts 成为"千行怪兽文件",引擎引入了函数注册表机制。
注册 API
// src/runtime/evaluator.ts
export function registerSimpleFunction(
name: string,
handler: (shape, argVals, ctx) => Shape[]
);
export function registerBlockFunction(
name: string,
handler: (op, shape, argVals, ctx) => Shape[]
);
工作原理
现有内置函数注册位置
目前内置函数仍在 evaluator.ts 的 switch-case 中,但注册表优先于 switch-case。未来新函数建议通过注册表添加。
➕ 添加新函数指南
场景 A:普通函数调用 myFunc(args)
- 实现几何逻辑
// src/geometry/operations/create/my-func.ts export function myFunc(shape: Shape, height: number): Shape { // 纯函数:输入 Shape,返回新 Shape return newShape; } - 注册到引擎
import { registerSimpleFunction } from '../runtime/evaluator.js'; import { myFunc } from '../geometry/operations/create/my-func.js'; registerSimpleFunction('myFunc', (shape, args, ctx) => { return [myFunc(shape, args[0] as number)]; }); - 添加测试
// tests/parser/operations.test.ts { name: 'myFunc', code: 'Lot --> myFunc(10)' } // tests/functions/my-func.test.ts // 验证执行结果
场景 B:Block 形式函数 myFunc(args){body}
- 实现几何逻辑(同上)
- 注册 Block Handler
import { registerBlockFunction } from '../runtime/evaluator.js'; registerBlockFunction('myFunc', (op, shape, argVals, ctx) => { // op.bodies: RuleBody[] — block 内的规则体 const result = myFuncGeometry(shape, argVals[0] as number); if (op.bodies.length > 0) { return evalRuleBody(op.bodies[0], result, ctx); } return [result]; }); - 添加测试(同上)
myFunc(args) 解析为 SimpleOperation,将 myFunc(args){body} 解析为 BlockOperation。
📜 Grammar 修改指南
什么时候改 Grammar?
| 场景 | 是否改 Grammar | 替代方案 |
|---|---|---|
| 增加新函数 | 不改 | 函数注册表 |
| Block 形式新函数 | 不改 | BlockOperation 通用解析 + 注册表 |
| 修复语法 bug | 可以改 | 必须跑完全量回归测试 |
| 全新语法结构 | 谨慎评估 | 先考虑 AST 变换层或预处理 |
修改 Grammar 的强制流程
- 备份 —
cp -r dist/ dist-backup-xxx/ - 修改 —
grammar/CGAGrammar.g4 - 重新生成 —
npm run generate-parser - 更新 AST Builder —
src/parser/ast/types.ts+src/parser/visitors/ast-builder.ts - 运行回归测试 —
npm test(216+ 测试必须全过) - 检查 Ambiguity — 观察测试输出中的
[Parser Ambiguity]警告 - 运行函数测试 —
npm run test:functions - 构建 —
npm run build - Beta 部署 —
./scripts/deploy.sh beta - 手动验证 — 在 beta.cgajs.com 测试
- 生产部署 —
./scripts/deploy.sh prod 1.3.0
为什么尽量不改 Grammar?
- First-Set 冲突 — 新规则与现有规则的开头部分重叠
- Ambiguity — Parser 在多个路径间犹豫,可能导致错误选择
- 全局影响 — ANTLR4 的决策表是全局编译的,局部改动可能影响整体
- Visitor 不兼容 — 新规则需要 ASTBuilder 实现新的 visit 方法
真实案例:splitAndSetbackPerimeter 的教训
尝试在 Grammar 中添加 saspOperation 规则以支持 splitAndSetbackPerimeter 的特殊语法:
saspOperation
: 'splitAndSetbackPerimeter' '(' expression ')' '{' saspBranch '}' '{' saspRemainder '}'
该规则的 FIRST 集合与 blockOperation 完全重叠,导致 comp、split、setback、offset、inline 等大量函数的解析出错。
正确做法:利用已有的 blockOperation 通用解析,在 Evaluator 层识别函数名并做特殊处理。
🚀 部署架构
Beta ↔ Prod 隔离
引擎采用双环境隔离部署策略:
| 环境 | 域名 | 引擎目录 | 前端目录 | 用途 |
|---|---|---|---|---|
| Beta | beta.cgajs.com | /www/wwwroot/beta-engine/ | /www/wwwroot/beta.cgajs.com/ | 新功能验证、回归测试 |
| Prod | www.cgajs.com | /www/wwwroot/cgajs-engine/ | /www/wwwroot/www.cgajs.com/ | 正式用户访问 |
API 根据请求的 Host Header 路由到对应引擎:
if host contains "beta":
cli = "/www/wwwroot/beta-engine/cli-wrapper.cjs"
else:
cli = "/www/wwwroot/cgajs-engine/cli-wrapper.cjs"
部署流程(前端+引擎同步)
开发 & 本地测试
npm test + npm run test:functions
Beta 构建
Admin 后台"构建 Beta" 或 ./scripts/deploy.sh beta
自动更新 package.json 版本号,约 20-30 秒完成,后台自动轮询检测
Beta 验证
访问 beta.cgajs.com/ide.html,测试 CGA + 前端交互
晋升生产
Admin 后台"发布到生产" 或 ./scripts/deploy.sh prod 1.3.0
自动同步:引擎(dist) + 前端(ide.html, enhance.js, ku.html, ku/)
紧急回滚
如发现问题,Admin 后台一键"紧急回滚"
同时回滚引擎 + 前端文件到发布前状态
不再需要在 beta 和 www 之间手动复制前端文件。
🧪 测试体系
三层测试金字塔
| 层级 | 文件 | 数量 | 目的 |
|---|---|---|---|
| Parser Unit | tests/parser/operations.test.ts | 216 | 验证所有语法构造能被正确解析 |
| Function | tests/functions/*.test.ts | 53 cases | 验证每个函数的几何输出正确 |
| E2E | tests/e2e/*.test.ts | 10+ | 验证完整 CGA 规则链的执行 |
部署脚本中的强制检查
scripts/deploy.sh 在部署前自动运行 Parser 回归测试和函数测试,任何失败都会阻断部署:
Step 1/4: Running parser regression tests... ✅
Step 2/4: Running function tests... ✅
Step 3/4: Building engine... ✅
Step 4/4: Deploying to beta/prod... ✅
📋 标准工作流速查
添加新函数(不改 Grammar)
1. geometry/operations/[category]/my-func.ts ← 几何实现
2. registerSimpleFunction('myFunc', handler) ← 注册
3. tests/parser/operations.test.ts ← Parser 测试
4. tests/functions/ 或 tests/e2e/ ← 功能测试
5. npm test + npm run test:functions ← 验证
6. ./scripts/deploy.sh beta ← Beta 部署
7. beta.cgajs.com 手动验证 ← 确认
8. ./scripts/deploy.sh prod 1.3.0 ← 生产部署
修复 Parser Bug(需要改 Grammar)
1. 修改 grammar/CGAGrammar.g4
2. npm run generate-parser
3. 更新 src/parser/ast/types.ts
4. 更新 src/parser/visitors/ast-builder.ts
5. npm test(必须 216/216 通过)
6. 检查 Ambiguity 警告
7. npm run test:functions
8. npm run build
9. ./scripts/deploy.sh beta
10. beta.cgajs.com 验证
11. ./scripts/deploy.sh prod 1.3.0
📊 CGA高频函数速查表 — Top 100
本表基于 CityEngine 官方 CGA 函数库与 marketplace.cgajs.com 的实际使用统计,整理了建筑规则生成中最常调用的 100+ 个高频函数。点击函数名可跳转至详细文档。
✅ 已实现 ⚠️ 部分实现 ❌ 未实现
1. 几何操作类(Top 30)
| 函数 | 说明 | 常用参数 | 状态 |
|---|---|---|---|
| extrude | 拉伸,最核心操作 | extrude(10) | ✅ |
| split | 分割,建筑立面必备 | split(x) { ~1: A }* | ✅ |
| comp(f) | 面组件分割 | comp(f) { top: Roof } | ✅ |
| comp(e) | 边组件分割 | comp(e) { all: Edge } | ✅ |
| translate | 平移 | t(0,5,0) | ✅ |
| rotate | 旋转 | r(0,90,0) | ✅ |
| scale | 缩放 | s(2,1,2) | ✅ |
| color | 设置颜色 | color("#ff6600") | ✅ |
| primitiveCube | 立方体图元 | primitiveCube() | ✅ |
| texture | 贴图 | texture("wall.jpg") | ✅ |
| roofGable | 人字屋顶 | roofGable(30) | ✅ |
| roofHip | 四坡屋顶 | roofHip(30) | ✅ |
| setback | 退界 | setback(2) | ✅ |
| offset | 偏移 | offset(-0.3) | ✅ |
| mirror | 镜像 | mirrorScope(x) | ✅ |
| center | 居中 | center(xz) | ✅ |
| taper | 锥化 | taper(0.8) | ✅ |
| envelope | 包络 | envelope(0.5, 0.3) | ✅ |
| insert | 插入资产 | insert("door.obj") | ✅ |
| primitiveSphere | 球体图元 | primitiveSphere() | ✅ |
| primitiveCylinder | 圆柱图元 | primitiveCylinder() | ✅ |
| primitiveCone | 圆锥图元 | primitiveCone() | ✅ |
| roofPyramid | 金字塔屋顶 | roofPyramid(30) | ✅ |
| roofShed | 单坡屋顶 | roofShed(15) | ✅ |
| setbackPerEdge | 按边退界 | setbackPerEdge(1) | ⚠️ |
| innerRectangle | 内接矩形 | innerRectangle() | ✅ |
| shapeL | L形分割 | shapeL(2,2) | ✅ |
| shapeU | U形分割 | shapeU(2,2) | ✅ |
| convexify | 凸化 | convexify() | ✅ |
| deleteHoles | 删除孔洞 | deleteHoles() | ✅ |
// 典型用法:建筑主体 → 立面分割 → 屋顶
Lot -->
extrude(rand(10,20))
comp(f) { top: Roof | side: Facade }
Facade -->
split(y) { ~1: Floor }*
Roof -->
roofGable(30)
texture("roof.jpg")
2. 变换与Scope类(Top 15)
| 函数 | 说明 | 常用参数 | 状态 |
|---|---|---|---|
| alignScopeToAxes | 对齐坐标轴 | alignScopeToAxes(y) | ✅ |
| alignScopeToGeometry | 对齐几何体 | alignScopeToGeometry(zUp,0) | ✅ |
| setPivot | 设置枢轴点 | setPivot(xyz,0,0,0) | ✅ |
| rotateScope | 旋转作用域 | rotateScope(0,45,0) | ✅ |
| mirrorScope | 镜像作用域 | mirrorScope(x) | ✅ |
| scope.sx | 作用域X尺寸 | scope.sx | ✅ |
| scope.sy | 作用域Y尺寸 | scope.sy | ✅ |
| scope.sz | 作用域Z尺寸 | scope.sz | ✅ |
| scope.tx | 作用域X位置 | scope.tx | ✅ |
| scope.ty | 作用域Y位置 | scope.ty | ✅ |
| scope.tz | 作用域Z位置 | scope.tz | ✅ |
// 基于Scope的自适应缩放
Adaptive -->
alignScopeToAxes(y)
s(scope.sx * 0.9, scope.sy, scope.sz * 0.9)
center(xz)
3. 材质与UV类(Top 10)
| 函数 | 说明 | 常用参数 | 状态 |
|---|---|---|---|
| setMaterial | 设置材质 | setMaterial("glass", color, 0.8) | ✅ |
| resetMaterial | 重置材质 | resetMaterial() | ✅ |
| setupProjection | 设置投影 | setupProjection(0, scope.sx, scope.sy) | ✅ |
| projectUV | 投影UV | projectUV(0) | ✅ |
| tileUV | 平铺UV | tileUV(0, 2, 2) | ✅ |
| rotateUV | 旋转UV | rotateUV(0, 45) | ✅ |
| scaleUV | 缩放UV | scaleUV(0, 2, 1) | ✅ |
// 砖墙材质UV设置
BrickWall -->
setupProjection(0, scope.sx, scope.sy)
projectUV(0)
tileUV(0, scope.sx / 0.5, scope.sy / 0.3)
texture("brick.jpg")
4. 数学函数类(Top 15)
| 函数 | 说明 | 常用参数 | 状态 |
|---|---|---|---|
| abs | 绝对值 | abs(-5) | ✅ |
| rand | 随机数 | rand(0,1) | ✅ |
| min | 最小值 | min(a, b) | ✅ |
| max | 最大值 | max(a, b, c) | ✅ |
| clamp | 钳制 | clamp(val, 0, 10) | ✅ |
| sqrt | 平方根 | sqrt(16) | ✅ |
| pow | 幂运算 | pow(2, 3) | ✅ |
| sin | 正弦 | sin(30) | ✅ |
| cos | 余弦 | cos(45) | ✅ |
| tan | 正切 | tan(60) | ✅ |
| floor | 向下取整 | floor(3.7) | ✅ |
| ceil | 向上取整 | ceil(3.2) | ✅ |
| round | 四舍五入 | round(3.5) | ✅ |
| log | 自然对数 | log(100) | ✅ |
| exp | 指数 | exp(1) | ✅ |
// 随机楼层高度
FloorHeight -->
floor(rand(3.0, 4.5) * 10) / 10
5. 字符串与列表类(Top 10)
| 函数 | 说明 | 常用参数 | 状态 |
|---|---|---|---|
| strlen | 字符串长度 | strlen("hello") | ✅ |
| search | 搜索子串 | find("abc", "b") | ✅ |
| replace | 替换子串 | replace("a,b", ",", ";") | ✅ |
| split | 字符串分割 | splitString("a;b;c", ";") | ✅ |
| listSize | 列表大小 | listSize("a;b;") | ✅ |
| listAdd | 列表添加 | listAdd("a;b;", "c") | ✅ |
| listRandom | 随机选择 | listRandom("red;blue;green;") | ✅ |
| listItem | 获取列表项 | listItem("a;b;c;", 1) | ✅ |
| append | 追加元素 | append([1,2], 3) | ✅ |
| reverse | 反转数组 | reverse([1,2,3]) | ✅ |
// 从列表随机选材质
PickMaterial -->
listRandom("brick.jpg;concrete.jpg;wood.jpg;")
texture(listRandom("brick.jpg;concrete.jpg;wood.jpg;"))
6. 几何查询类(Top 15)
| 函数 | 说明 | 常用参数 | 状态 |
|---|---|---|---|
| geometry.area | 面积 | geometry.area | ✅ |
| geometry.height | 高度 | geometry.height | ✅ |
| geometry.volume | 体积 | geometry.volume | ✅ |
| geometry.nFaces | 面数 | geometry.nFaces | ✅ |
| geometry.nVertices | 顶点数 | geometry.nVertices | ✅ |
| geometry.isPlanar | 是否平面 | geometry.isPlanar | ✅ |
| geometry.isClosedSurface | 是否封闭 | geometry.isClosedSurface | ✅ |
| geometry.boundaryLength | 边界长度 | geometry.boundaryLength | ✅ |
| geometry.angle | 角度 | geometry.angle | ⚠️ |
| geometry.bbArea | 包围盒面积 | geometry.bbArea | ❌ |
| geometry.top | 顶部Y坐标 | geometry.top | ❌ |
| geometry.bottom | 底部Y坐标 | geometry.bottom | ❌ |
// 按面积自适应细节
Detail -->
case geometry.area > 50 : HighDetail
else : LowDetail
7. 资产与文件类(Top 10)
| 函数 | 说明 | 常用参数 | 状态 |
|---|---|---|---|
| assetInfo | 资产信息 | assetInfo("tree.obj", "sx") | ✅ |
| assetFitSize | 资产适配尺寸 | assetFitSize(assets, scope.sx) | ✅ |
| assetApproxRatio | 资产近似比例 | assetApproxRatio(assets) | ✅ |
| fileSearch | 文件搜索 | fileSearch("*.jpg") | ✅ |
| fileExists | 文件存在检查 | fileExists("data.txt") | ✅ |
| fileBasename | 文件基名 | fileBasename("a/b.jpg") | ✅ |
// 动态加载适配窗户
Window -->
insert(assetFitSize(assets, scope.sx * 0.8))
8. 布尔与高级操作类(Top 8)
| 函数 | 说明 | 常用参数 | 状态 |
|---|---|---|---|
| union | 并集 | union { A | B } | ⚠️ |
| subtract | 差集 | subtract { A | B } | ⚠️ |
| intersect | 交集 | intersect { A | B } | ⚠️ |
| scatter | 散布 | scatter(surface, 20) | ✅ |
| repeat | 重复 | split(x) { ~1: A }* | ⚠️ |
| copy | 复制 | copy(5) | ❌ |
| instance | 实例化 | instance("ref") | ❌ |
| label | 标签 | label("facade") | ❌ |
| setTag | 设置标签 | setTag("window") | ❌ |
| deleteTags | 删除标签 | deleteTags() | ✅ |
// 散布树木到地面
Ground -->
scatter(surface, 50)
insert("tree.obj")
9. 颜色与转换类(Top 3)
| 函数 | 说明 | 常用参数 | 状态 |
|---|---|---|---|
| colorRamp | 颜色渐变 | colorRamp("height", 0.5) | ✅ |
| rgb | RGB颜色 | rgb(1,0.5,0) | ❌ |
| hsb | HSB颜色 | hsb(120,0.8,0.9) | ❌ |
// 高度驱动的颜色渐变
ColorByHeight -->
color(colorRamp("height", geometry.height / 50))
translate ↔ t、log ↔ ln、round ↔ rint),引擎内部均已支持。⚠️ 标记的函数为简化实现或仅特定上下文可用。