znlgis 博客

GIS开发与技术分享

第七章:实战案例与最佳实践

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 性能优化建议

  1. 延迟加载:使用动态导入加载非关键模块
  2. 缓存计算结果:对于昂贵的几何计算,使用缓存
  3. 批量操作:合并多个操作减少重渲染次数
  4. Web Workers:将耗时计算移至后台线程
  5. 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二次开发的完整流程:

  1. 参数化建模工具:实现了一个完整的参数化齿轮建模工具,包括几何体定义、参数对话框、实时预览等功能

  2. BIM组件库:构建了一个可扩展的建筑组件库系统,支持分类管理、参数化创建、拖拽放置等功能

  3. 自动化设计工具:实现了设计规则引擎,支持设计检查、问题报告、自动修复等功能

通过这些案例的学习,读者应该能够:


附录:常用资源

A.1 官方资源

A.2 相关技术文档

A.3 社区资源


全书完

感谢您阅读本教程!希望通过这七章的学习,您已经掌握了Chili3D的使用方法和二次开发技能。Chili3D是一个持续发展的开源项目,欢迎您参与社区讨论,贡献代码,共同推动项目的发展。

如有任何问题或建议,欢迎通过GitHub Issues或开发者邮箱与我们联系。祝您在3D CAD开发的道路上取得成功!