第七章:实战案例与最佳实践
7.1 案例一:参数化建模工具
7.1.1 需求分析
参数化建模是现代CAD系统的核心功能之一。本案例将实现一个参数化齿轮建模工具,用户可以通过调整参数动态生成齿轮模型。
功能需求:
- 支持设置齿数、模数、压力角等参数
- 实时预览齿轮形状
- 支持导出为标准格式
- 参数验证和约束
7.1.2 齿轮几何体实现
// packages/chili-extension/src/examples/gear/gearBody.ts
import {
Body, Property, Serializable, Result, IShape, IDocument, XYZ,
Ray, Matrix4
} from "chili-core";
@Serializable("GearBody")
export class GearBody extends Body {
@Property({ display: "param.teeth", min: 6, max: 200, step: 1 })
private _teeth: number;
@Property({ display: "param.module", min: 0.5, max: 10, step: 0.1 })
private _module: number;
@Property({ display: "param.pressureAngle", min: 14.5, max: 25, step: 0.5 })
private _pressureAngle: number;
@Property({ display: "param.faceWidth", min: 1, max: 100, step: 1 })
private _faceWidth: number;
@Property({ display: "param.boreRadius", min: 0, max: 50, step: 0.5 })
private _boreRadius: number;
@Property({ display: "param.center" })
private _center: XYZ;
constructor(
document: IDocument,
center: XYZ = XYZ.zero,
teeth: number = 20,
module: number = 2,
pressureAngle: number = 20,
faceWidth: number = 10,
boreRadius: number = 5
) {
super(document);
this._center = center;
this._teeth = teeth;
this._module = module;
this._pressureAngle = pressureAngle;
this._faceWidth = faceWidth;
this._boreRadius = boreRadius;
}
// 计算属性
get pitchDiameter(): number {
return this._teeth * this._module;
}
get baseDiameter(): number {
const pressureRadians = this._pressureAngle * Math.PI / 180;
return this.pitchDiameter * Math.cos(pressureRadians);
}
get addendumDiameter(): number {
return this.pitchDiameter + 2 * this._module;
}
get dedendumDiameter(): number {
return this.pitchDiameter - 2.5 * this._module;
}
// Getter/Setter
get teeth(): number { return this._teeth; }
set teeth(value: number) {
if (value >= 6 && value !== this._teeth) {
this._teeth = Math.round(value);
this.invalidate();
}
}
get module(): number { return this._module; }
set module(value: number) {
if (value > 0 && value !== this._module) {
this._module = value;
this.invalidate();
}
}
protected generateShape(): Result<IShape> {
const factory = this.document.application.shapeFactory;
try {
// 1. 生成齿廓曲线
const toothProfile = this.generateToothProfile();
// 2. 创建单个齿的面
const toothFace = factory.face(toothProfile);
if (!toothFace.isOk) {
return Result.error("Failed to create tooth face");
}
// 3. 阵列齿
const angleStep = 360 / this._teeth;
let gearWire = toothProfile;
for (let i = 1; i < this._teeth; i++) {
const angle = i * angleStep * Math.PI / 180;
const rotatedTooth = this.rotateWire(toothProfile, angle);
gearWire = this.mergeWires(gearWire, rotatedTooth);
}
// 4. 创建齿轮面
const gearFace = factory.face(gearWire);
if (!gearFace.isOk) {
return Result.error("Failed to create gear face");
}
// 5. 拉伸成实体
const gearSolid = factory.extrude(
gearFace.value,
XYZ.unitZ,
this._faceWidth
);
if (!gearSolid.isOk) {
return gearSolid;
}
// 6. 如果有中心孔,进行布尔差集
if (this._boreRadius > 0) {
const bore = factory.cylinder(
new Ray(this._center, XYZ.unitZ),
this._boreRadius,
this._faceWidth * 1.1
);
if (bore.isOk) {
return factory.booleanCut(gearSolid.value, bore.value);
}
}
// 7. 移动到中心位置
const finalShape = gearSolid.value.transform(
Matrix4.makeTranslation(this._center)
);
return Result.ok(finalShape);
} catch (e) {
return Result.error(`Gear generation failed: ${e}`);
}
}
private generateToothProfile(): IWire {
const factory = this.document.application.shapeFactory;
const edges: IEdge[] = [];
const pitchRadius = this.pitchDiameter / 2;
const baseRadius = this.baseDiameter / 2;
const addendumRadius = this.addendumDiameter / 2;
const dedendumRadius = this.dedendumDiameter / 2;
const toothAngle = Math.PI / this._teeth;
// 生成渐开线齿廓点
const involutePoints = this.generateInvolutePoints(
baseRadius,
addendumRadius,
20
);
// 创建齿廓曲线
const rightFlank = factory.bspline(involutePoints);
if (rightFlank.isOk) {
edges.push(rightFlank.value);
}
// 镜像创建左侧齿廓
const leftPoints = involutePoints.map(p =>
new XYZ(p.x, -p.y, p.z)
).reverse();
const leftFlank = factory.bspline(leftPoints);
if (leftFlank.isOk) {
edges.push(leftFlank.value);
}
// 添加齿顶圆弧
const topArc = factory.arc(
XYZ.zero,
involutePoints[involutePoints.length - 1],
leftPoints[0]
);
if (topArc.isOk) {
edges.push(topArc.value);
}
// 添加齿根圆弧
const rootArc = factory.arc(
XYZ.zero,
leftPoints[leftPoints.length - 1],
involutePoints[0]
);
if (rootArc.isOk) {
edges.push(rootArc.value);
}
return factory.wire(edges).value!;
}
private generateInvolutePoints(
baseRadius: number,
outerRadius: number,
segments: number
): XYZ[] {
const points: XYZ[] = [];
for (let i = 0; i <= segments; i++) {
const t = i / segments;
const r = baseRadius + t * (outerRadius - baseRadius);
// 渐开线参数方程
const theta = Math.sqrt((r * r) / (baseRadius * baseRadius) - 1);
const alpha = theta - Math.atan(theta);
const x = r * Math.cos(alpha);
const y = r * Math.sin(alpha);
points.push(new XYZ(x, y, 0));
}
return points;
}
private rotateWire(wire: IWire, angle: number): IWire {
const matrix = Matrix4.makeRotation(XYZ.unitZ, angle);
return wire.transform(matrix) as IWire;
}
private mergeWires(wire1: IWire, wire2: IWire): IWire {
// 合并两个线框
const factory = this.document.application.shapeFactory;
const edges1 = wire1.findSubShapes(ShapeType.Edge) as IEdge[];
const edges2 = wire2.findSubShapes(ShapeType.Edge) as IEdge[];
return factory.wire([...edges1, ...edges2]).value!;
}
}
7.1.3 齿轮命令
// packages/chili-extension/src/examples/gear/createGearCommand.ts
import {
command, MultistepCommand, IStep, PointStep, GeometryNode
} from "chili";
import { GearBody } from "./gearBody";
@command({
name: "Create.Gear",
icon: "icon-gear",
display: "command.gear"
})
export class CreateGearCommand extends MultistepCommand {
// 命令参数
@Property({ display: "param.teeth", min: 6, max: 200 })
teeth: number = 20;
@Property({ display: "param.module", min: 0.5, max: 10 })
module: number = 2;
@Property({ display: "param.pressureAngle", min: 14.5, max: 25 })
pressureAngle: number = 20;
@Property({ display: "param.faceWidth", min: 1, max: 100 })
faceWidth: number = 10;
@Property({ display: "param.boreRadius", min: 0, max: 50 })
boreRadius: number = 5;
protected getSteps(): IStep[] {
return [
new PointStep("center", "prompt.selectCenter"),
new ParameterStep("params", this)
];
}
protected complete(): void {
const center = this.stepDatas.get("center") as XYZ;
// 创建齿轮体
const body = new GearBody(
this.document,
center,
this.teeth,
this.module,
this.pressureAngle,
this.faceWidth,
this.boreRadius
);
// 添加到文档
const node = new GeometryNode(
this.document,
`Gear_Z${this.teeth}_M${this.module}`,
body
);
this.document.addNode(node);
}
}
// 参数输入步骤
class ParameterStep implements IStep {
readonly name = "params";
constructor(private command: CreateGearCommand) {}
async execute(): Promise<StepResult> {
const dialog = new GearParameterDialog(this.command);
const result = await dialog.show();
return {
success: result !== null,
data: result
};
}
}
7.1.4 参数对话框
// packages/chili-extension/src/examples/gear/gearParameterDialog.ts
import { Dialog, t } from "chili-ui";
export class GearParameterDialog extends Dialog {
private _command: CreateGearCommand;
private _preview: GearPreview;
constructor(command: CreateGearCommand) {
super({ title: t("dialog.gearParameters") });
this._command = command;
this.createContent();
}
private createContent(): void {
const container = document.createElement("div");
container.className = styles.container;
// 左侧:参数输入
const paramsPanel = this.createParametersPanel();
container.appendChild(paramsPanel);
// 右侧:预览
this._preview = new GearPreview();
container.appendChild(this._preview);
this.setContent(container);
// 添加按钮
this.addButton("button.cancel", () => this.close(null));
this.addButton("button.ok", () => this.confirm(), true);
// 初始预览
this.updatePreview();
}
private createParametersPanel(): HTMLElement {
const panel = document.createElement("div");
panel.className = styles.paramsPanel;
// 齿数
panel.appendChild(this.createNumberInput(
"param.teeth",
this._command.teeth,
6, 200, 1,
(v) => {
this._command.teeth = v;
this.updatePreview();
}
));
// 模数
panel.appendChild(this.createNumberInput(
"param.module",
this._command.module,
0.5, 10, 0.1,
(v) => {
this._command.module = v;
this.updatePreview();
}
));
// 压力角
panel.appendChild(this.createNumberInput(
"param.pressureAngle",
this._command.pressureAngle,
14.5, 25, 0.5,
(v) => {
this._command.pressureAngle = v;
this.updatePreview();
}
));
// 齿宽
panel.appendChild(this.createNumberInput(
"param.faceWidth",
this._command.faceWidth,
1, 100, 1,
(v) => {
this._command.faceWidth = v;
this.updatePreview();
}
));
// 孔径
panel.appendChild(this.createNumberInput(
"param.boreRadius",
this._command.boreRadius,
0, 50, 0.5,
(v) => {
this._command.boreRadius = v;
this.updatePreview();
}
));
// 计算值显示
const infoPanel = document.createElement("div");
infoPanel.className = styles.infoPanel;
infoPanel.innerHTML = `
<h4>${t("info.calculatedValues")}</h4>
<div id="pitchDiameter"></div>
<div id="addendumDiameter"></div>
<div id="dedendumDiameter"></div>
`;
panel.appendChild(infoPanel);
return panel;
}
private createNumberInput(
label: string,
value: number,
min: number,
max: number,
step: number,
onChange: (value: number) => void
): HTMLElement {
const row = document.createElement("div");
row.className = styles.inputRow;
const labelEl = document.createElement("label");
labelEl.textContent = t(label);
row.appendChild(labelEl);
const input = document.createElement("input");
input.type = "number";
input.value = value.toString();
input.min = min.toString();
input.max = max.toString();
input.step = step.toString();
input.onchange = () => {
const v = parseFloat(input.value);
if (!isNaN(v) && v >= min && v <= max) {
onChange(v);
}
};
row.appendChild(input);
return row;
}
private updatePreview(): void {
// 更新计算值显示
const pitchDiameter = this._command.teeth * this._command.module;
const addendumDiameter = pitchDiameter + 2 * this._command.module;
const dedendumDiameter = pitchDiameter - 2.5 * this._command.module;
const pitchEl = this.querySelector("#pitchDiameter");
if (pitchEl) {
pitchEl.textContent = `${t("info.pitchDiameter")}: ${pitchDiameter.toFixed(2)}`;
}
const addendumEl = this.querySelector("#addendumDiameter");
if (addendumEl) {
addendumEl.textContent = `${t("info.addendumDiameter")}: ${addendumDiameter.toFixed(2)}`;
}
const dedendumEl = this.querySelector("#dedendumDiameter");
if (dedendumEl) {
dedendumEl.textContent = `${t("info.dedendumDiameter")}: ${dedendumDiameter.toFixed(2)}`;
}
// 更新3D预览
this._preview.update(this._command);
}
private confirm(): void {
this.close(this._command);
}
}
7.2 案例二:BIM组件库
7.2.1 组件库架构
// packages/chili-extension/src/examples/bim/componentLibrary.ts
export class ComponentLibrary {
private _components: Map<string, ComponentDefinition> = new Map();
private _categories: Map<string, Category> = new Map();
constructor() {
this.initializeBuiltinComponents();
}
private initializeBuiltinComponents(): void {
// 结构组件
this.addCategory({
id: "structural",
name: "category.structural",
icon: "icon-structure"
});
this.addComponent({
id: "column",
name: "component.column",
category: "structural",
factory: ColumnComponentFactory,
parameters: [
{ key: "width", type: "number", default: 400, unit: "mm" },
{ key: "depth", type: "number", default: 400, unit: "mm" },
{ key: "height", type: "number", default: 3000, unit: "mm" },
{ key: "material", type: "enum", options: ["concrete", "steel"] }
]
});
this.addComponent({
id: "beam",
name: "component.beam",
category: "structural",
factory: BeamComponentFactory,
parameters: [
{ key: "width", type: "number", default: 200, unit: "mm" },
{ key: "height", type: "number", default: 500, unit: "mm" },
{ key: "length", type: "number", default: 6000, unit: "mm" },
{ key: "profile", type: "enum", options: ["rectangular", "i-beam", "h-beam"] }
]
});
// 门窗组件
this.addCategory({
id: "openings",
name: "category.openings",
icon: "icon-door"
});
this.addComponent({
id: "door",
name: "component.door",
category: "openings",
factory: DoorComponentFactory,
parameters: [
{ key: "width", type: "number", default: 900, unit: "mm" },
{ key: "height", type: "number", default: 2100, unit: "mm" },
{ key: "type", type: "enum", options: ["single", "double", "sliding"] }
]
});
this.addComponent({
id: "window",
name: "component.window",
category: "openings",
factory: WindowComponentFactory,
parameters: [
{ key: "width", type: "number", default: 1200, unit: "mm" },
{ key: "height", type: "number", default: 1500, unit: "mm" },
{ key: "sillHeight", type: "number", default: 900, unit: "mm" },
{ key: "type", type: "enum", options: ["fixed", "casement", "sliding"] }
]
});
}
addCategory(category: Category): void {
this._categories.set(category.id, category);
}
addComponent(component: ComponentDefinition): void {
this._components.set(component.id, component);
}
getCategories(): Category[] {
return Array.from(this._categories.values());
}
getComponentsByCategory(categoryId: string): ComponentDefinition[] {
return Array.from(this._components.values())
.filter(c => c.category === categoryId);
}
getComponent(id: string): ComponentDefinition | undefined {
return this._components.get(id);
}
createComponent(id: string, params: Record<string, any>): Result<IShape> {
const def = this._components.get(id);
if (!def) {
return Result.error(`Component not found: ${id}`);
}
return def.factory.create(params);
}
}
interface Category {
id: string;
name: string;
icon: string;
}
interface ComponentDefinition {
id: string;
name: string;
category: string;
factory: IComponentFactory;
parameters: ParameterDefinition[];
}
interface ParameterDefinition {
key: string;
type: "number" | "string" | "boolean" | "enum";
default?: any;
unit?: string;
options?: string[];
min?: number;
max?: number;
}
interface IComponentFactory {
create(params: Record<string, any>): Result<IShape>;
}
7.2.2 组件工厂实现
// packages/chili-extension/src/examples/bim/factories/columnFactory.ts
export class ColumnComponentFactory implements IComponentFactory {
create(params: Record<string, any>): Result<IShape> {
const width = params.width || 400;
const depth = params.depth || 400;
const height = params.height || 3000;
const material = params.material || "concrete";
const factory = Services.get<IShapeFactory>("shapeFactory");
// 创建柱子基本形状
const columnResult = factory.box(
XYZ.zero,
width,
depth,
height
);
if (!columnResult.isOk) {
return columnResult;
}
let shape = columnResult.value;
// 如果是钢柱,添加边缘倒角
if (material === "steel") {
const edges = shape.findSubShapes(ShapeType.Edge) as IEdge[];
const verticalEdges = edges.filter(e => this.isVerticalEdge(e));
const filletResult = factory.fillet(shape, verticalEdges, 10);
if (filletResult.isOk) {
shape = filletResult.value;
}
}
return Result.ok(shape);
}
private isVerticalEdge(edge: IEdge): boolean {
const curve = edge.curve;
if (curve.curveType !== CurveType.Line) return false;
const direction = (curve as LineCurve).direction;
return Math.abs(direction.z) > 0.99;
}
}
// packages/chili-extension/src/examples/bim/factories/doorFactory.ts
export class DoorComponentFactory implements IComponentFactory {
create(params: Record<string, any>): Result<IShape> {
const width = params.width || 900;
const height = params.height || 2100;
const type = params.type || "single";
const frameThickness = 50;
const doorThickness = 40;
const factory = Services.get<IShapeFactory>("shapeFactory");
const shapes: IShape[] = [];
// 创建门框
const frameResult = this.createFrame(factory, width, height, frameThickness);
if (!frameResult.isOk) return frameResult;
shapes.push(frameResult.value);
// 创建门扇
const doorPanelResult = this.createDoorPanel(
factory, width, height, frameThickness, doorThickness, type
);
if (!doorPanelResult.isOk) return doorPanelResult;
shapes.push(doorPanelResult.value);
// 添加把手
const handleResult = this.createHandle(factory, width, height, doorThickness, type);
if (handleResult.isOk) {
shapes.push(handleResult.value);
}
// 合并所有形状
return this.fuseShapes(factory, shapes);
}
private createFrame(
factory: IShapeFactory,
width: number,
height: number,
thickness: number
): Result<IShape> {
// 创建门框外轮廓
const outerBox = factory.box(XYZ.zero, width, thickness, height);
if (!outerBox.isOk) return outerBox;
// 创建门框内轮廓(用于减去)
const innerBox = factory.box(
new XYZ(thickness / 2, -1, 0),
width - thickness,
thickness + 2,
height - thickness / 2
);
if (!innerBox.isOk) return innerBox;
return factory.booleanCut(outerBox.value, innerBox.value);
}
private createDoorPanel(
factory: IShapeFactory,
width: number,
height: number,
frameThickness: number,
doorThickness: number,
type: string
): Result<IShape> {
const panelWidth = type === "double"
? (width - frameThickness) / 2 - 5
: width - frameThickness - 10;
const panelHeight = height - frameThickness - 10;
const panel = factory.box(
new XYZ(frameThickness / 2 + 5, (frameThickness - doorThickness) / 2, 5),
panelWidth,
doorThickness,
panelHeight
);
if (!panel.isOk) return panel;
// 双开门时添加第二扇
if (type === "double") {
const panel2 = factory.box(
new XYZ(width / 2 + 5, (frameThickness - doorThickness) / 2, 5),
panelWidth,
doorThickness,
panelHeight
);
if (panel2.isOk) {
return factory.booleanUnion(panel.value, panel2.value);
}
}
return panel;
}
private createHandle(
factory: IShapeFactory,
width: number,
height: number,
doorThickness: number,
type: string
): Result<IShape> {
const handleHeight = height / 2;
const handleLength = 120;
const handleRadius = 10;
const handleX = type === "double" ? width / 2 - 100 : width - 100;
return factory.cylinder(
new Ray(
new XYZ(handleX, doorThickness / 2, handleHeight),
XYZ.unitY
),
handleRadius,
handleLength
);
}
private fuseShapes(factory: IShapeFactory, shapes: IShape[]): Result<IShape> {
if (shapes.length === 0) {
return Result.error("No shapes to fuse");
}
let result = shapes[0];
for (let i = 1; i < shapes.length; i++) {
const fuseResult = factory.booleanUnion(result, shapes[i]);
if (!fuseResult.isOk) return fuseResult;
result = fuseResult.value;
}
return Result.ok(result);
}
}
7.2.3 组件库面板
// packages/chili-extension/src/examples/bim/componentLibraryPanel.ts
export class ComponentLibraryPanel extends HTMLElement {
private _library: ComponentLibrary;
private _categoryList: HTMLElement;
private _componentGrid: HTMLElement;
private _selectedCategory: string | null = null;
constructor(library: ComponentLibrary) {
super();
this._library = library;
this.className = styles.panel;
this.render();
}
private render(): void {
// 标题
const header = document.createElement("div");
header.className = styles.header;
header.innerHTML = `<h3>${t("panel.componentLibrary")}</h3>`;
this.appendChild(header);
// 搜索框
const searchBox = this.createSearchBox();
this.appendChild(searchBox);
// 分类列表
this._categoryList = document.createElement("div");
this._categoryList.className = styles.categoryList;
this.renderCategories();
this.appendChild(this._categoryList);
// 组件网格
this._componentGrid = document.createElement("div");
this._componentGrid.className = styles.componentGrid;
this.appendChild(this._componentGrid);
}
private createSearchBox(): HTMLElement {
const container = document.createElement("div");
container.className = styles.searchBox;
const input = document.createElement("input");
input.type = "text";
input.placeholder = t("placeholder.search");
input.oninput = () => this.filterComponents(input.value);
container.appendChild(input);
return container;
}
private renderCategories(): void {
this._categoryList.innerHTML = "";
for (const category of this._library.getCategories()) {
const button = document.createElement("button");
button.className = styles.categoryButton;
button.innerHTML = `
<span class="${styles.icon} ${category.icon}"></span>
<span>${t(category.name)}</span>
`;
button.onclick = () => this.selectCategory(category.id);
this._categoryList.appendChild(button);
}
}
private selectCategory(categoryId: string): void {
this._selectedCategory = categoryId;
this.renderComponents();
// 更新选中状态
const buttons = this._categoryList.querySelectorAll("button");
buttons.forEach((btn, index) => {
const categories = this._library.getCategories();
btn.classList.toggle(
styles.selected,
categories[index].id === categoryId
);
});
}
private renderComponents(): void {
this._componentGrid.innerHTML = "";
if (!this._selectedCategory) return;
const components = this._library.getComponentsByCategory(this._selectedCategory);
for (const component of components) {
const card = this.createComponentCard(component);
this._componentGrid.appendChild(card);
}
}
private createComponentCard(component: ComponentDefinition): HTMLElement {
const card = document.createElement("div");
card.className = styles.componentCard;
card.innerHTML = `
<div class="${styles.thumbnail}">
<canvas id="thumb-${component.id}"></canvas>
</div>
<div class="${styles.name}">${t(component.name)}</div>
`;
// 双击插入组件
card.ondblclick = () => this.insertComponent(component);
// 拖拽支持
card.draggable = true;
card.ondragstart = (e) => {
e.dataTransfer?.setData("componentId", component.id);
};
// 生成缩略图
requestAnimationFrame(() => {
this.generateThumbnail(component, card.querySelector("canvas")!);
});
return card;
}
private async insertComponent(component: ComponentDefinition): Promise<void> {
// 显示参数对话框
const dialog = new ComponentParameterDialog(component);
const params = await dialog.show();
if (!params) return;
// 创建组件
const result = this._library.createComponent(component.id, params);
if (!result.isOk) {
Toast.show(result.error);
return;
}
// 让用户选择放置位置
const document = Application.instance.activeDocument;
if (!document) return;
const pointResult = await document.selection.pickPoint({
prompt: t("prompt.selectInsertPoint")
});
if (!pointResult.success) return;
// 创建节点并添加到文档
const body = new ImportedBody(document, result.value);
const node = new GeometryNode(
document,
t(component.name),
body
);
node.matrix = Matrix4.makeTranslation(pointResult.data);
document.addNode(node);
}
private generateThumbnail(component: ComponentDefinition, canvas: HTMLCanvasElement): void {
// 使用默认参数生成预览
const defaultParams: Record<string, any> = {};
for (const param of component.parameters) {
defaultParams[param.key] = param.default;
}
const result = this._library.createComponent(component.id, defaultParams);
if (!result.isOk) return;
// 渲染到canvas
const renderer = new ThumbnailRenderer(canvas, 100, 100);
renderer.render(result.value);
}
private filterComponents(searchText: string): void {
// 实现搜索过滤逻辑
}
}
customElements.define("component-library-panel", ComponentLibraryPanel);
7.3 案例三:自动化设计工具
7.3.1 规则引擎
// packages/chili-extension/src/examples/automation/ruleEngine.ts
export class RuleEngine {
private _rules: Map<string, Rule> = new Map();
addRule(rule: Rule): void {
this._rules.set(rule.id, rule);
}
removeRule(id: string): void {
this._rules.delete(id);
}
async evaluate(context: DesignContext): Promise<EvaluationResult[]> {
const results: EvaluationResult[] = [];
for (const rule of this._rules.values()) {
if (!rule.enabled) continue;
try {
const result = await rule.evaluate(context);
results.push(result);
} catch (e) {
results.push({
ruleId: rule.id,
passed: false,
message: `Evaluation error: ${e}`,
severity: "error"
});
}
}
return results;
}
async autoFix(
context: DesignContext,
results: EvaluationResult[]
): Promise<FixResult[]> {
const fixResults: FixResult[] = [];
for (const result of results) {
if (result.passed) continue;
const rule = this._rules.get(result.ruleId);
if (!rule || !rule.autoFix) continue;
try {
const fixed = await rule.autoFix(context, result);
fixResults.push({
ruleId: rule.id,
success: fixed,
message: fixed ? "Auto-fixed" : "Auto-fix failed"
});
} catch (e) {
fixResults.push({
ruleId: rule.id,
success: false,
message: `Auto-fix error: ${e}`
});
}
}
return fixResults;
}
}
interface Rule {
id: string;
name: string;
description: string;
enabled: boolean;
evaluate(context: DesignContext): Promise<EvaluationResult>;
autoFix?(context: DesignContext, result: EvaluationResult): Promise<boolean>;
}
interface DesignContext {
document: IDocument;
selectedNodes: INode[];
parameters: Record<string, any>;
}
interface EvaluationResult {
ruleId: string;
passed: boolean;
message: string;
severity: "error" | "warning" | "info";
affectedNodes?: INode[];
suggestion?: string;
}
interface FixResult {
ruleId: string;
success: boolean;
message: string;
}
7.3.2 设计规则实现
// packages/chili-extension/src/examples/automation/rules/minimumWallThicknessRule.ts
export class MinimumWallThicknessRule implements Rule {
readonly id = "minimum-wall-thickness";
readonly name = "rule.minimumWallThickness";
readonly description = "rule.minimumWallThicknessDesc";
enabled = true;
private _minThickness: number = 10;
constructor(minThickness?: number) {
if (minThickness !== undefined) {
this._minThickness = minThickness;
}
}
async evaluate(context: DesignContext): Promise<EvaluationResult> {
const thinWalls: INode[] = [];
for (const node of context.document.getAllNodes()) {
if (!(node instanceof GeometryNode)) continue;
const shape = node.body.shape;
if (!shape) continue;
const thickness = this.measureMinThickness(shape);
if (thickness < this._minThickness) {
thinWalls.push(node);
}
}
return {
ruleId: this.id,
passed: thinWalls.length === 0,
message: thinWalls.length > 0
? `Found ${thinWalls.length} walls with thickness < ${this._minThickness}mm`
: "All walls meet minimum thickness requirement",
severity: thinWalls.length > 0 ? "warning" : "info",
affectedNodes: thinWalls,
suggestion: "Consider increasing wall thickness for structural integrity"
};
}
private measureMinThickness(shape: IShape): number {
// 获取所有面
const faces = shape.findSubShapes(ShapeType.Face) as IFace[];
let minThickness = Infinity;
for (let i = 0; i < faces.length; i++) {
for (let j = i + 1; j < faces.length; j++) {
// 计算相对面之间的距离
const distance = this.measureFaceDistance(faces[i], faces[j]);
if (distance > 0 && distance < minThickness) {
minThickness = distance;
}
}
}
return minThickness === Infinity ? 0 : minThickness;
}
private measureFaceDistance(face1: IFace, face2: IFace): number {
// 检查是否是平行面
const normal1 = face1.surface.normal(0.5, 0.5);
const normal2 = face2.surface.normal(0.5, 0.5);
if (Math.abs(normal1.dot(normal2) + 1) < 0.01) {
// 相对的平面
const point1 = face1.surface.point(0.5, 0.5);
const point2 = face2.surface.point(0.5, 0.5);
return point1.distanceTo(point2);
}
return -1; // 不是平行面
}
}
// packages/chili-extension/src/examples/automation/rules/interferenceCheckRule.ts
export class InterferenceCheckRule implements Rule {
readonly id = "interference-check";
readonly name = "rule.interferenceCheck";
readonly description = "rule.interferenceCheckDesc";
enabled = true;
async evaluate(context: DesignContext): Promise<EvaluationResult> {
const nodes = context.document.getAllNodes()
.filter(n => n instanceof GeometryNode) as GeometryNode[];
const interferences: { node1: INode; node2: INode; volume: number }[] = [];
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
const shape1 = nodes[i].body.shape;
const shape2 = nodes[j].body.shape;
if (!shape1 || !shape2) continue;
const result = ShapeAnalyzer.checkInterference(shape1, shape2);
if (result.hasInterference) {
interferences.push({
node1: nodes[i],
node2: nodes[j],
volume: result.interferenceVolume
});
}
}
}
return {
ruleId: this.id,
passed: interferences.length === 0,
message: interferences.length > 0
? `Found ${interferences.length} interference(s)`
: "No interferences detected",
severity: interferences.length > 0 ? "error" : "info",
affectedNodes: interferences.flatMap(i => [i.node1, i.node2]),
suggestion: "Review and resolve geometric interferences"
};
}
async autoFix(context: DesignContext, result: EvaluationResult): Promise<boolean> {
// 自动修复干涉:尝试移动冲突的对象
// 这里是简化实现,实际应用中需要更复杂的算法
return false;
}
}
7.3.3 自动化设计面板
// packages/chili-extension/src/examples/automation/automationPanel.ts
export class AutomationPanel extends HTMLElement {
private _engine: RuleEngine;
private _rulesList: HTMLElement;
private _resultsList: HTMLElement;
private _lastResults: EvaluationResult[] = [];
constructor() {
super();
this._engine = new RuleEngine();
this.className = styles.panel;
this.initializeDefaultRules();
this.render();
}
private initializeDefaultRules(): void {
this._engine.addRule(new MinimumWallThicknessRule(10));
this._engine.addRule(new InterferenceCheckRule());
this._engine.addRule(new MaterialAssignmentRule());
this._engine.addRule(new NamingConventionRule());
}
private render(): void {
// 标题
const header = document.createElement("div");
header.className = styles.header;
header.innerHTML = `
<h3>${t("panel.automation")}</h3>
<button class="${styles.runButton}" id="runCheck">
${t("button.runCheck")}
</button>
`;
this.appendChild(header);
// 规则列表
const rulesSection = document.createElement("div");
rulesSection.className = styles.section;
rulesSection.innerHTML = `<h4>${t("section.rules")}</h4>`;
this._rulesList = document.createElement("div");
this._rulesList.className = styles.rulesList;
rulesSection.appendChild(this._rulesList);
this.appendChild(rulesSection);
this.renderRules();
// 检查结果
const resultsSection = document.createElement("div");
resultsSection.className = styles.section;
resultsSection.innerHTML = `<h4>${t("section.results")}</h4>`;
this._resultsList = document.createElement("div");
this._resultsList.className = styles.resultsList;
resultsSection.appendChild(this._resultsList);
this.appendChild(resultsSection);
// 操作按钮
const actions = document.createElement("div");
actions.className = styles.actions;
actions.innerHTML = `
<button id="autoFix">${t("button.autoFix")}</button>
<button id="exportReport">${t("button.exportReport")}</button>
`;
this.appendChild(actions);
// 绑定事件
this.querySelector("#runCheck")!.addEventListener("click", () => this.runCheck());
this.querySelector("#autoFix")!.addEventListener("click", () => this.autoFix());
this.querySelector("#exportReport")!.addEventListener("click", () => this.exportReport());
}
private renderRules(): void {
// 渲染规则列表
}
private async runCheck(): Promise<void> {
const document = Application.instance.activeDocument;
if (!document) {
Toast.show(t("error.noDocument"));
return;
}
const context: DesignContext = {
document,
selectedNodes: document.selection.selectedNodes,
parameters: {}
};
// 显示进度
const progressDialog = new ProgressDialog(t("dialog.checkingDesign"));
progressDialog.show();
try {
this._lastResults = await this._engine.evaluate(context);
this.renderResults();
} finally {
progressDialog.close();
}
}
private renderResults(): void {
this._resultsList.innerHTML = "";
for (const result of this._lastResults) {
const item = document.createElement("div");
item.className = `${styles.resultItem} ${styles[result.severity]}`;
item.innerHTML = `
<span class="${styles.icon}">
${result.passed ? "✓" : result.severity === "error" ? "✗" : "⚠"}
</span>
<div class="${styles.content}">
<div class="${styles.message}">${result.message}</div>
${result.suggestion ? `<div class="${styles.suggestion}">${result.suggestion}</div>` : ""}
</div>
`;
// 点击高亮受影响的节点
if (result.affectedNodes && result.affectedNodes.length > 0) {
item.style.cursor = "pointer";
item.onclick = () => this.highlightNodes(result.affectedNodes!);
}
this._resultsList.appendChild(item);
}
}
private highlightNodes(nodes: INode[]): void {
const document = Application.instance.activeDocument;
if (!document) return;
document.selection.select(nodes);
document.visual.zoomToFit(nodes);
}
private async autoFix(): Promise<void> {
const document = Application.instance.activeDocument;
if (!document) return;
const context: DesignContext = {
document,
selectedNodes: document.selection.selectedNodes,
parameters: {}
};
const fixResults = await this._engine.autoFix(context, this._lastResults);
// 显示修复结果
const fixed = fixResults.filter(r => r.success).length;
Toast.show(t("message.autoFixComplete", { fixed }));
// 重新检查
await this.runCheck();
}
private exportReport(): void {
const report = this.generateReport();
const blob = new Blob([report], { type: "text/html" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `design-check-report-${Date.now()}.html`;
a.click();
URL.revokeObjectURL(url);
}
private generateReport(): string {
return `
<!DOCTYPE html>
<html>
<head>
<title>Design Check Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.error { color: red; }
.warning { color: orange; }
.info { color: green; }
</style>
</head>
<body>
<h1>Design Check Report</h1>
<p>Generated: ${new Date().toLocaleString()}</p>
<h2>Results</h2>
<ul>
${this._lastResults.map(r => `
<li class="${r.severity}">
${r.passed ? "✓" : "✗"} ${r.message}
${r.suggestion ? `<br><small>${r.suggestion}</small>` : ""}
</li>
`).join("")}
</ul>
</body>
</html>
`;
}
}
customElements.define("automation-panel", AutomationPanel);
7.4 最佳实践总结
7.4.1 代码组织
项目结构建议:
packages/chili-extension/
├── src/
│ ├── commands/ # 命令实现
│ │ ├── create/ # 创建命令
│ │ ├── modify/ # 修改命令
│ │ └── index.ts
│ ├── bodys/ # 几何体实现
│ ├── ui/ # UI组件
│ │ ├── panels/
│ │ ├── dialogs/
│ │ └── components/
│ ├── services/ # 服务层
│ ├── utils/ # 工具函数
│ ├── types/ # 类型定义
│ └── index.ts # 导出入口
├── test/ # 测试文件
├── assets/ # 静态资源
└── package.json
7.4.2 性能优化建议
- 延迟加载:使用动态导入加载非关键模块
- 缓存计算结果:对于昂贵的几何计算,使用缓存
- 批量操作:合并多个操作减少重渲染次数
- Web Workers:将耗时计算移至后台线程
- LOD策略:根据视距使用不同精度的模型
7.4.3 测试策略
// 单元测试示例
describe("GearBody", () => {
it("should calculate pitch diameter correctly", () => {
const gear = new GearBody(mockDocument, XYZ.zero, 20, 2);
expect(gear.pitchDiameter).toBe(40);
});
it("should generate valid shape", () => {
const gear = new GearBody(mockDocument, XYZ.zero, 20, 2, 20, 10, 5);
const shape = gear.shape;
expect(shape).toBeDefined();
expect(shape!.isValid).toBe(true);
});
});
// 集成测试示例
describe("ComponentLibrary Integration", () => {
it("should create column component", async () => {
const library = new ComponentLibrary();
const result = library.createComponent("column", {
width: 400,
depth: 400,
height: 3000
});
expect(result.isOk).toBe(true);
expect(result.value.shapeType).toBe(ShapeType.Solid);
});
});
7.4.4 文档规范
/**
* 齿轮几何体
*
* 根据齿轮参数生成渐开线齿轮的3D模型。
*
* @example
* ```typescript
* const gear = new GearBody(document, XYZ.zero, 20, 2, 20, 10, 5);
* const shape = gear.shape; // 获取生成的形状
* ```
*
* @see https://en.wikipedia.org/wiki/Involute_gear
*/
@Serializable("GearBody")
export class GearBody extends Body {
/**
* 创建齿轮几何体
*
* @param document - 所属文档
* @param center - 齿轮中心点
* @param teeth - 齿数(最小6)
* @param module - 模数(齿轮大小的标准化参数)
* @param pressureAngle - 压力角(通常为20度)
* @param faceWidth - 齿宽
* @param boreRadius - 中心孔半径(0表示无孔)
*/
constructor(
document: IDocument,
center: XYZ,
teeth: number,
module: number,
pressureAngle: number,
faceWidth: number,
boreRadius: number
) {
// ...
}
}
7.5 本章小结
本章通过三个实战案例展示了Chili3D二次开发的完整流程:
-
参数化建模工具:实现了一个完整的参数化齿轮建模工具,包括几何体定义、参数对话框、实时预览等功能
-
BIM组件库:构建了一个可扩展的建筑组件库系统,支持分类管理、参数化创建、拖拽放置等功能
-
自动化设计工具:实现了设计规则引擎,支持设计检查、问题报告、自动修复等功能
通过这些案例的学习,读者应该能够:
- 理解完整的扩展开发流程
- 掌握参数化建模的实现方法
- 学会构建可复用的组件库
- 了解如何实现自动化设计检查
附录:常用资源
A.1 官方资源
- 项目主页:https://github.com/xiangechen/chili3d
- 在线演示:https://chili3d.com
- B站教程:https://space.bilibili.com/539380032
A.2 相关技术文档
- OpenCascade文档:https://dev.opencascade.org/doc/overview/html/
- Three.js文档:https://threejs.org/docs/
- TypeScript文档:https://www.typescriptlang.org/docs/
A.3 社区资源
- GitHub Discussions:https://github.com/xiangechen/chili3d/discussions
- 开发者邮箱:xiangetg@msn.cn
全书完
感谢您阅读本教程!希望通过这七章的学习,您已经掌握了Chili3D的使用方法和二次开发技能。Chili3D是一个持续发展的开源项目,欢迎您参与社区讨论,贡献代码,共同推动项目的发展。
如有任何问题或建议,欢迎通过GitHub Issues或开发者邮箱与我们联系。祝您在3D CAD开发的道路上取得成功!