znlgis 博客

GIS开发与技术分享

第五章:二次开发入门

5.1 开发环境配置

5.1.1 推荐开发工具

进行Chili3D二次开发,推荐使用以下开发工具:

代码编辑器

推荐VS Code扩展

// .vscode/extensions.json
{
    "recommendations": [
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode",
        "ms-vscode.vscode-typescript-next",
        "bradlc.vscode-tailwindcss",
        "formulahendry.auto-rename-tag",
        "christian-kohler.path-intellisense"
    ]
}

VS Code配置

// .vscode/settings.json
{
    "editor.formatOnSave": true,
    "editor.defaultFormatter": "esbenp.prettier-vscode",
    "typescript.tsdk": "node_modules/typescript/lib",
    "typescript.enablePromptUseWorkspaceTsdk": true,
    "[typescript]": {
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    }
}

5.1.2 项目结构准备

二次开发可以采用以下两种方式:

方式一:Fork项目(推荐)

# Fork项目到自己的GitHub账户
# 然后克隆
git clone https://github.com/your-username/chili3d.git
cd chili3d

# 添加上游仓库,便于同步更新
git remote add upstream https://github.com/xiangechen/chili3d.git

方式二:创建扩展包

在packages目录下创建新的扩展包:

# 创建扩展包目录
mkdir -p packages/chili-extension/src
cd packages/chili-extension

# 初始化package.json
npm init -y

创建package.json:

{
    "name": "chili-extension",
    "version": "0.1.0",
    "description": "Custom extension for Chili3D",
    "main": "src/index.ts",
    "dependencies": {
        "chili-core": "*",
        "chili": "*"
    }
}

5.1.3 调试配置

VS Code调试配置

// .vscode/launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "chrome",
            "request": "launch",
            "name": "Debug in Chrome",
            "url": "http://localhost:8080",
            "webRoot": "${workspaceFolder}",
            "sourceMaps": true,
            "sourceMapPathOverrides": {
                "webpack:///./~/*": "${workspaceFolder}/node_modules/*",
                "webpack:///./*": "${workspaceFolder}/*"
            }
        },
        {
            "type": "node",
            "request": "launch",
            "name": "Debug Tests",
            "program": "${workspaceFolder}/node_modules/jest/bin/jest.js",
            "args": ["--runInBand"],
            "console": "integratedTerminal",
            "internalConsoleOptions": "neverOpen"
        }
    ]
}

5.1.4 TypeScript配置

理解项目的TypeScript配置:

// tsconfig.json
{
    "compilerOptions": {
        "target": "ES2020",
        "module": "ESNext",
        "moduleResolution": "node",
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true,
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true,
        "declaration": true,
        "declarationMap": true,
        "sourceMap": true,
        "baseUrl": ".",
        "paths": {
            "chili-core": ["packages/chili-core/src"],
            "chili": ["packages/chili/src"],
            "chili-wasm": ["packages/chili-wasm/src"],
            "chili-three": ["packages/chili-three/src"],
            "chili-ui": ["packages/chili-ui/src"]
        }
    },
    "include": ["packages/*/src/**/*"],
    "exclude": ["node_modules", "dist"]
}

5.2 自定义命令开发

5.2.1 命令开发基础

命令是Chili3D中最常见的扩展方式。以下是创建自定义命令的基本步骤:

步骤一:创建命令类

// packages/chili-extension/src/commands/myCommand.ts
import { command, ICommand, IDocument } from "chili-core";

@command({
    name: "Custom.MyCommand",
    icon: "icon-custom",
    display: "command.myCommand"
})
export class MyCommand implements ICommand {
    async execute(document: IDocument): Promise<void> {
        console.log("MyCommand executed!");
        // 你的命令逻辑
    }
}

步骤二:注册命令

// packages/chili-extension/src/index.ts
import "./commands/myCommand";

export * from "./commands/myCommand";

步骤三:添加到功能区

// 修改或扩展ribbon配置
import { MyCommand } from "chili-extension";

export function extendRibbon(ribbon: Ribbon): void {
    const customTab = ribbon.addTab("custom", "ribbon.custom");
    
    const toolsGroup = customTab.addGroup("tools", "ribbon.tools");
    toolsGroup.addButton({
        icon: "icon-custom",
        label: "command.myCommand",
        command: "Custom.MyCommand"
    });
}

5.2.2 带参数的命令

很多命令需要用户输入参数:

// packages/chili-extension/src/commands/scaleCommand.ts
import { command, ICommand, IDocument, Property, Serializable } from "chili-core";

interface ScaleOptions {
    scaleFactor: number;
    uniformScale: boolean;
}

@command({
    name: "Custom.Scale",
    icon: "icon-scale",
    display: "command.scale"
})
export class ScaleCommand implements ICommand {
    // 命令属性(可在属性面板中编辑)
    @Property({ display: "param.scaleFactor", min: 0.01, max: 100, step: 0.1 })
    scaleFactor: number = 1.0;
    
    @Property({ display: "param.uniformScale" })
    uniformScale: boolean = true;
    
    async execute(document: IDocument): Promise<void> {
        // 获取选中的对象
        const selectedNodes = document.selection.selectedNodes;
        
        if (selectedNodes.length === 0) {
            Toast.show(t("error.noSelection"));
            return;
        }
        
        // 显示参数对话框
        const dialog = new ParameterDialog(this);
        const confirmed = await dialog.show();
        
        if (!confirmed) return;
        
        // 执行缩放
        for (const node of selectedNodes) {
            if (node instanceof GeometryNode) {
                this.scaleNode(node);
            }
        }
    }
    
    private scaleNode(node: GeometryNode): void {
        const center = node.shape?.getCenter();
        if (!center) return;
        
        const scaleMatrix = Matrix4.makeScale(
            this.scaleFactor,
            this.uniformScale ? this.scaleFactor : 1,
            this.uniformScale ? this.scaleFactor : 1,
            center
        );
        
        node.matrix = node.matrix.multiply(scaleMatrix);
    }
}

5.2.3 多步骤命令

对于需要多次用户交互的复杂命令:

// packages/chili-extension/src/commands/createTubeCommand.ts
import {
    command,
    MultistepCommand,
    IStep,
    PointStep,
    LengthStep,
    GeometryNode
} from "chili";

@command({
    name: "Create.Tube",
    icon: "icon-tube",
    display: "command.tube"
})
export class CreateTubeCommand extends MultistepCommand {
    protected getSteps(): IStep[] {
        return [
            new PointStep("center", "prompt.selectCenter"),
            new LengthStep("innerRadius", "prompt.inputInnerRadius"),
            new LengthStep("outerRadius", "prompt.inputOuterRadius"),
            new LengthStep("height", "prompt.inputHeight")
        ];
    }
    
    protected complete(): void {
        const center = this.stepDatas.get("center") as XYZ;
        const innerRadius = this.stepDatas.get("innerRadius") as number;
        const outerRadius = this.stepDatas.get("outerRadius") as number;
        const height = this.stepDatas.get("height") as number;
        
        // 验证参数
        if (innerRadius >= outerRadius) {
            Toast.show(t("error.invalidRadius"));
            return;
        }
        
        // 创建管道体
        const body = new TubeBody(
            this.document,
            center,
            innerRadius,
            outerRadius,
            height
        );
        
        // 添加到文档
        const node = new GeometryNode(this.document, "Tube", body);
        this.document.addNode(node);
    }
}

// 管道几何体
@Serializable("TubeBody")
export class TubeBody extends Body {
    @Property()
    private _center: XYZ;
    
    @Property()
    private _innerRadius: number;
    
    @Property()
    private _outerRadius: number;
    
    @Property()
    private _height: number;
    
    constructor(
        document: IDocument,
        center: XYZ,
        innerRadius: number,
        outerRadius: number,
        height: number
    ) {
        super(document);
        this._center = center;
        this._innerRadius = innerRadius;
        this._outerRadius = outerRadius;
        this._height = height;
    }
    
    protected generateShape(): Result<IShape> {
        const factory = this.document.application.shapeFactory;
        
        // 创建外圆柱
        const outerCylinder = factory.cylinder(
            new Ray(this._center, XYZ.unitZ),
            this._outerRadius,
            this._height
        );
        
        if (!outerCylinder.isOk) {
            return outerCylinder;
        }
        
        // 创建内圆柱
        const innerCylinder = factory.cylinder(
            new Ray(this._center, XYZ.unitZ),
            this._innerRadius,
            this._height
        );
        
        if (!innerCylinder.isOk) {
            return innerCylinder;
        }
        
        // 布尔差集
        return factory.booleanCut(outerCylinder.value, innerCylinder.value);
    }
}

5.2.4 可撤销命令

实现可撤销/重做的命令:

// packages/chili-extension/src/commands/setColorCommand.ts
import { IReversibleCommand, IDocument, INode, GeometryNode } from "chili-core";

export class SetColorCommand implements IReversibleCommand {
    private _previousColors: Map<string, number> = new Map();
    
    constructor(
        private document: IDocument,
        private nodes: INode[],
        private newColor: number
    ) {}
    
    async execute(document: IDocument): Promise<void> {
        // 保存原始颜色
        for (const node of this.nodes) {
            if (node instanceof GeometryNode) {
                this._previousColors.set(node.id, node.color);
                node.color = this.newColor;
            }
        }
    }
    
    undo(): void {
        // 恢复原始颜色
        for (const node of this.nodes) {
            if (node instanceof GeometryNode) {
                const previousColor = this._previousColors.get(node.id);
                if (previousColor !== undefined) {
                    node.color = previousColor;
                }
            }
        }
    }
    
    redo(): void {
        // 重新应用新颜色
        for (const node of this.nodes) {
            if (node instanceof GeometryNode) {
                node.color = this.newColor;
            }
        }
    }
}

5.3 自定义几何体

5.3.1 创建新几何体类

// packages/chili-extension/src/bodys/helixBody.ts
import { Body, Property, Serializable, Result, IShape, IDocument, XYZ } from "chili-core";

@Serializable("HelixBody")
export class HelixBody extends Body {
    @Property({ display: "param.center" })
    private _center: XYZ;
    
    @Property({ display: "param.radius", min: 0.1 })
    private _radius: number;
    
    @Property({ display: "param.pitch", min: 0.1 })
    private _pitch: number;
    
    @Property({ display: "param.turns", min: 0.1 })
    private _turns: number;
    
    constructor(
        document: IDocument,
        center: XYZ,
        radius: number,
        pitch: number,
        turns: number
    ) {
        super(document);
        this._center = center;
        this._radius = radius;
        this._pitch = pitch;
        this._turns = turns;
    }
    
    // Getter/Setter with invalidation
    get center(): XYZ { return this._center; }
    set center(value: XYZ) {
        if (!this._center.equals(value)) {
            this._center = value;
            this.invalidate();
        }
    }
    
    get radius(): number { return this._radius; }
    set radius(value: number) {
        if (this._radius !== value && value > 0) {
            this._radius = value;
            this.invalidate();
        }
    }
    
    get pitch(): number { return this._pitch; }
    set pitch(value: number) {
        if (this._pitch !== value && value > 0) {
            this._pitch = value;
            this.invalidate();
        }
    }
    
    get turns(): number { return this._turns; }
    set turns(value: number) {
        if (this._turns !== value && value > 0) {
            this._turns = value;
            this.invalidate();
        }
    }
    
    protected generateShape(): Result<IShape> {
        // 生成螺旋线点
        const points: XYZ[] = [];
        const segments = Math.ceil(this._turns * 36); // 每圈36个点
        
        for (let i = 0; i <= segments; i++) {
            const t = i / segments;
            const angle = t * this._turns * 2 * Math.PI;
            const z = t * this._turns * this._pitch;
            
            points.push(new XYZ(
                this._center.x + this._radius * Math.cos(angle),
                this._center.y + this._radius * Math.sin(angle),
                this._center.z + z
            ));
        }
        
        // 创建B样条曲线
        const factory = this.document.application.shapeFactory;
        return factory.bspline(points);
    }
}

5.3.2 参数化几何体

创建支持参数约束的几何体:

// packages/chili-extension/src/bodys/parametricBoxBody.ts
import { Body, Property, Serializable, Result, IShape, IDocument, XYZ } from "chili-core";

@Serializable("ParametricBoxBody")
export class ParametricBoxBody extends Body {
    @Property({ display: "param.width", min: 0.1 })
    private _width: number;
    
    @Property({ display: "param.aspectRatio", min: 0.1, max: 10 })
    private _aspectRatio: number;
    
    @Property({ display: "param.heightRatio", min: 0.1, max: 10 })
    private _heightRatio: number;
    
    constructor(
        document: IDocument,
        width: number,
        aspectRatio: number = 1,
        heightRatio: number = 1
    ) {
        super(document);
        this._width = width;
        this._aspectRatio = aspectRatio;
        this._heightRatio = heightRatio;
    }
    
    // 计算属性
    get length(): number {
        return this._width * this._aspectRatio;
    }
    
    get height(): number {
        return this._width * this._heightRatio;
    }
    
    protected generateShape(): Result<IShape> {
        return this.document.application.shapeFactory.box(
            XYZ.zero,
            this._width,
            this.length,
            this.height
        );
    }
}

5.4 自定义UI组件

5.4.1 创建自定义面板

// packages/chili-extension/src/ui/customPanel.ts
export class CustomPanel extends HTMLElement {
    private _header: HTMLElement;
    private _content: HTMLElement;
    private _collapsed: boolean = false;
    
    constructor(title: string) {
        super();
        this.className = styles.customPanel;
        
        // 创建头部
        this._header = document.createElement("div");
        this._header.className = styles.header;
        this._header.innerHTML = `
            <span class="${styles.collapseIcon}">▼</span>
            <span class="${styles.title}">${t(title)}</span>
        `;
        this._header.onclick = () => this.toggle();
        this.appendChild(this._header);
        
        // 创建内容区
        this._content = document.createElement("div");
        this._content.className = styles.content;
        this.appendChild(this._content);
    }
    
    setContent(content: HTMLElement | HTMLElement[]): void {
        this._content.innerHTML = "";
        
        if (Array.isArray(content)) {
            content.forEach(el => this._content.appendChild(el));
        } else {
            this._content.appendChild(content);
        }
    }
    
    toggle(): void {
        this._collapsed = !this._collapsed;
        this._content.style.display = this._collapsed ? "none" : "block";
        this._header.querySelector(`.${styles.collapseIcon}`)!.textContent = 
            this._collapsed ? "" : "";
    }
    
    expand(): void {
        if (this._collapsed) this.toggle();
    }
    
    collapse(): void {
        if (!this._collapsed) this.toggle();
    }
}

customElements.define("custom-panel", CustomPanel);

5.4.2 创建自定义工具栏

// packages/chili-extension/src/ui/customToolbar.ts
export class CustomToolbar extends HTMLElement {
    private _tools: Map<string, ToolButton> = new Map();
    
    constructor() {
        super();
        this.className = styles.toolbar;
    }
    
    addTool(options: ToolOptions): ToolButton {
        const button = new ToolButton(options);
        this._tools.set(options.id, button);
        this.appendChild(button);
        return button;
    }
    
    addSeparator(): void {
        const separator = document.createElement("div");
        separator.className = styles.separator;
        this.appendChild(separator);
    }
    
    setToolEnabled(id: string, enabled: boolean): void {
        const tool = this._tools.get(id);
        if (tool) {
            tool.enabled = enabled;
        }
    }
    
    setToolActive(id: string, active: boolean): void {
        const tool = this._tools.get(id);
        if (tool) {
            tool.active = active;
        }
    }
}

export class ToolButton extends HTMLElement {
    private _enabled: boolean = true;
    private _active: boolean = false;
    
    constructor(private options: ToolOptions) {
        super();
        this.className = styles.toolButton;
        this.title = t(options.tooltip || options.label);
        
        // 图标
        const icon = document.createElement("span");
        icon.className = `${styles.icon} ${options.icon}`;
        this.appendChild(icon);
        
        // 标签(可选)
        if (options.showLabel) {
            const label = document.createElement("span");
            label.className = styles.label;
            label.textContent = t(options.label);
            this.appendChild(label);
        }
        
        this.onclick = () => {
            if (this._enabled) {
                options.onClick?.();
            }
        };
    }
    
    get enabled(): boolean { return this._enabled; }
    set enabled(value: boolean) {
        this._enabled = value;
        this.classList.toggle(styles.disabled, !value);
    }
    
    get active(): boolean { return this._active; }
    set active(value: boolean) {
        this._active = value;
        this.classList.toggle(styles.active, value);
    }
}

customElements.define("custom-toolbar", CustomToolbar);
customElements.define("tool-button", ToolButton);

5.4.3 自定义对话框

// packages/chili-extension/src/ui/inputDialog.ts
export class InputDialog extends Dialog {
    private _inputs: Map<string, HTMLInputElement> = new Map();
    private _values: Record<string, any> = {};
    
    constructor(title: string, fields: InputField[]) {
        super({ title: t(title) });
        
        const form = document.createElement("form");
        form.className = styles.form;
        
        for (const field of fields) {
            const row = this.createFieldRow(field);
            form.appendChild(row);
        }
        
        this.setContent(form);
        
        this.addButton("button.cancel", () => this.close(null));
        this.addButton("button.ok", () => this.submit(), true);
    }
    
    private createFieldRow(field: InputField): HTMLElement {
        const row = document.createElement("div");
        row.className = styles.fieldRow;
        
        // 标签
        const label = document.createElement("label");
        label.textContent = t(field.label);
        row.appendChild(label);
        
        // 输入框
        const input = document.createElement("input");
        input.type = field.type || "text";
        input.value = field.defaultValue?.toString() || "";
        input.placeholder = field.placeholder ? t(field.placeholder) : "";
        
        if (field.type === "number") {
            if (field.min !== undefined) input.min = field.min.toString();
            if (field.max !== undefined) input.max = field.max.toString();
            if (field.step !== undefined) input.step = field.step.toString();
        }
        
        this._inputs.set(field.key, input);
        row.appendChild(input);
        
        return row;
    }
    
    private submit(): void {
        const values: Record<string, any> = {};
        
        for (const [key, input] of this._inputs) {
            const value = input.type === "number" 
                ? parseFloat(input.value) 
                : input.value;
            values[key] = value;
        }
        
        this.close(values);
    }
    
    static async prompt<T = Record<string, any>>(
        title: string,
        fields: InputField[]
    ): Promise<T | null> {
        const dialog = new InputDialog(title, fields);
        return dialog.show() as Promise<T | null>;
    }
}

interface InputField {
    key: string;
    label: string;
    type?: "text" | "number" | "password" | "email";
    defaultValue?: string | number;
    placeholder?: string;
    min?: number;
    max?: number;
    step?: number;
}

5.5 事件处理与扩展点

5.5.1 订阅文档事件

// packages/chili-extension/src/eventHandlers.ts
export class DocumentEventHandler {
    constructor(private document: IDocument) {
        this.bindEvents();
    }
    
    private bindEvents(): void {
        // 选择变化事件
        this.document.selection.addObserver("selectionChanged", (nodes: INode[]) => {
            console.log("Selection changed:", nodes);
            this.onSelectionChanged(nodes);
        });
        
        // 节点添加事件
        this.document.addObserver("nodeAdded", (node: INode) => {
            console.log("Node added:", node);
            this.onNodeAdded(node);
        });
        
        // 节点删除事件
        this.document.addObserver("nodeRemoved", (node: INode) => {
            console.log("Node removed:", node);
            this.onNodeRemoved(node);
        });
        
        // 文档保存事件
        this.document.addObserver("documentSaved", () => {
            console.log("Document saved");
            this.onDocumentSaved();
        });
    }
    
    private onSelectionChanged(nodes: INode[]): void {
        // 处理选择变化
        if (nodes.length === 1) {
            // 单选时显示详细信息
            this.showNodeDetails(nodes[0]);
        }
    }
    
    private onNodeAdded(node: INode): void {
        // 处理节点添加
        if (node instanceof GeometryNode) {
            // 自动应用默认材质
            node.material = this.document.materials[0];
        }
    }
    
    private onNodeRemoved(node: INode): void {
        // 处理节点删除
        // 可以进行清理操作
    }
    
    private onDocumentSaved(): void {
        // 处理文档保存
        Toast.show(t("message.documentSaved"));
    }
    
    private showNodeDetails(node: INode): void {
        // 显示节点详情
    }
}

5.5.2 自定义事件处理器

// packages/chili-extension/src/handlers/customEventHandler.ts
export class CustomViewEventHandler implements IEventHandler {
    private _active: boolean = false;
    private _mode: "select" | "measure" | "annotate" = "select";
    
    constructor(private document: IDocument) {}
    
    setMode(mode: "select" | "measure" | "annotate"): void {
        this._mode = mode;
    }
    
    onMouseMove(e: MouseEvent, view: IView): void {
        switch (this._mode) {
            case "select":
                this.handleSelectMove(e, view);
                break;
            case "measure":
                this.handleMeasureMove(e, view);
                break;
            case "annotate":
                this.handleAnnotateMove(e, view);
                break;
        }
    }
    
    onClick(e: MouseEvent, view: IView): void {
        switch (this._mode) {
            case "select":
                this.handleSelectClick(e, view);
                break;
            case "measure":
                this.handleMeasureClick(e, view);
                break;
            case "annotate":
                this.handleAnnotateClick(e, view);
                break;
        }
    }
    
    onKeyDown(e: KeyboardEvent): void {
        if (e.key === "Escape") {
            this.cancel();
        }
    }
    
    private handleSelectMove(e: MouseEvent, view: IView): void {
        // 高亮悬停对象
        const hit = view.hitTest(e.clientX, e.clientY);
        if (hit) {
            view.highlighter.highlight(hit.shape);
        } else {
            view.highlighter.clear();
        }
    }
    
    private handleSelectClick(e: MouseEvent, view: IView): void {
        const hit = view.hitTest(e.clientX, e.clientY);
        if (hit) {
            this.document.selection.select([hit.node], e.ctrlKey);
        } else if (!e.ctrlKey) {
            this.document.selection.clear();
        }
    }
    
    private handleMeasureMove(e: MouseEvent, view: IView): void {
        // 测量模式下的鼠标移动处理
    }
    
    private handleMeasureClick(e: MouseEvent, view: IView): void {
        // 测量模式下的点击处理
    }
    
    private handleAnnotateMove(e: MouseEvent, view: IView): void {
        // 标注模式下的鼠标移动处理
    }
    
    private handleAnnotateClick(e: MouseEvent, view: IView): void {
        // 标注模式下的点击处理
    }
    
    private cancel(): void {
        this._mode = "select";
        this.document.visual.setEventHandler(undefined);
    }
}

5.5.3 扩展应用程序

// packages/chili-extension/src/appExtension.ts
export class AppExtension {
    private _document: IDocument | undefined;
    private _eventHandler: DocumentEventHandler | undefined;
    
    constructor(private app: IApplication) {
        this.init();
    }
    
    private init(): void {
        // 监听文档打开事件
        this.app.addObserver("documentOpened", (doc: IDocument) => {
            this.onDocumentOpened(doc);
        });
        
        // 监听文档关闭事件
        this.app.addObserver("documentClosed", (doc: IDocument) => {
            this.onDocumentClosed(doc);
        });
        
        // 注册自定义服务
        this.registerServices();
        
        // 扩展UI
        this.extendUI();
    }
    
    private onDocumentOpened(document: IDocument): void {
        this._document = document;
        this._eventHandler = new DocumentEventHandler(document);
        
        // 初始化文档特定功能
        this.initDocumentFeatures(document);
    }
    
    private onDocumentClosed(document: IDocument): void {
        if (this._document === document) {
            this._document = undefined;
            this._eventHandler = undefined;
        }
    }
    
    private registerServices(): void {
        // 注册自定义服务
        Services.register("customAnalyzer", new CustomAnalyzer());
        Services.register("exportService", new ExportService());
    }
    
    private extendUI(): void {
        // 添加自定义面板
        const customPanel = new CustomPanel("panel.custom");
        // ... 配置面板
    }
    
    private initDocumentFeatures(document: IDocument): void {
        // 初始化文档特定功能
    }
}

// 使用示例
const extension = new AppExtension(Application.instance);

5.6 数据持久化扩展

5.6.1 自定义存储

// packages/chili-extension/src/storage/cloudStorage.ts
export class CloudStorage implements IStorage {
    private _apiEndpoint: string;
    private _authToken: string;
    
    constructor(apiEndpoint: string, authToken: string) {
        this._apiEndpoint = apiEndpoint;
        this._authToken = authToken;
    }
    
    async save(key: string, data: any): Promise<void> {
        const response = await fetch(`${this._apiEndpoint}/documents/${key}`, {
            method: "PUT",
            headers: {
                "Content-Type": "application/json",
                "Authorization": `Bearer ${this._authToken}`
            },
            body: JSON.stringify(data)
        });
        
        if (!response.ok) {
            throw new Error(`Failed to save: ${response.statusText}`);
        }
    }
    
    async load(key: string): Promise<any> {
        const response = await fetch(`${this._apiEndpoint}/documents/${key}`, {
            headers: {
                "Authorization": `Bearer ${this._authToken}`
            }
        });
        
        if (!response.ok) {
            throw new Error(`Failed to load: ${response.statusText}`);
        }
        
        return response.json();
    }
    
    async delete(key: string): Promise<void> {
        const response = await fetch(`${this._apiEndpoint}/documents/${key}`, {
            method: "DELETE",
            headers: {
                "Authorization": `Bearer ${this._authToken}`
            }
        });
        
        if (!response.ok) {
            throw new Error(`Failed to delete: ${response.statusText}`);
        }
    }
    
    async list(): Promise<string[]> {
        const response = await fetch(`${this._apiEndpoint}/documents`, {
            headers: {
                "Authorization": `Bearer ${this._authToken}`
            }
        });
        
        if (!response.ok) {
            throw new Error(`Failed to list: ${response.statusText}`);
        }
        
        const data = await response.json();
        return data.documents.map((d: any) => d.id);
    }
}

5.6.2 自定义序列化

// packages/chili-extension/src/serialize/customSerializer.ts
export class CustomSerializer {
    static registerCustomTypes(): void {
        // 注册自定义类型的序列化器
        SerializerRegistry.register("HelixBody", {
            serialize: (obj: HelixBody) => ({
                classKey: "HelixBody",
                center: obj.center.toArray(),
                radius: obj.radius,
                pitch: obj.pitch,
                turns: obj.turns
            }),
            
            deserialize: (data: any, document: IDocument) => {
                return new HelixBody(
                    document,
                    XYZ.fromArray(data.center),
                    data.radius,
                    data.pitch,
                    data.turns
                );
            }
        });
    }
}

5.7 测试与调试

5.7.1 单元测试

// packages/chili-extension/test/helixBody.test.ts
import { HelixBody } from "../src/bodys/helixBody";
import { XYZ } from "chili-core";

describe("HelixBody", () => {
    let mockDocument: any;
    let helixBody: HelixBody;
    
    beforeEach(() => {
        mockDocument = {
            application: {
                shapeFactory: {
                    bspline: jest.fn().mockReturnValue({ isOk: true, value: {} })
                }
            }
        };
        
        helixBody = new HelixBody(
            mockDocument,
            XYZ.zero,
            10,  // radius
            5,   // pitch
            3    // turns
        );
    });
    
    test("should create helix with correct parameters", () => {
        expect(helixBody.radius).toBe(10);
        expect(helixBody.pitch).toBe(5);
        expect(helixBody.turns).toBe(3);
    });
    
    test("should regenerate shape when radius changes", () => {
        const oldShape = helixBody.shape;
        helixBody.radius = 20;
        const newShape = helixBody.shape;
        
        // 形状应该重新生成
        expect(mockDocument.application.shapeFactory.bspline).toHaveBeenCalledTimes(2);
    });
    
    test("should not accept invalid radius", () => {
        helixBody.radius = -5;
        expect(helixBody.radius).toBe(10); // 保持原值
    });
});

5.7.2 调试技巧

// 添加调试日志
export class DebugHelper {
    static enabled: boolean = true;
    
    static log(category: string, message: string, data?: any): void {
        if (!this.enabled) return;
        
        console.log(`[${category}] ${message}`, data || "");
    }
    
    static time(label: string): void {
        if (this.enabled) console.time(label);
    }
    
    static timeEnd(label: string): void {
        if (this.enabled) console.timeEnd(label);
    }
    
    static group(label: string): void {
        if (this.enabled) console.group(label);
    }
    
    static groupEnd(): void {
        if (this.enabled) console.groupEnd();
    }
}

// 使用示例
DebugHelper.time("generateShape");
const shape = this.generateShape();
DebugHelper.timeEnd("generateShape");

DebugHelper.log("Shape", "Generated shape", { type: shape.shapeType });

5.8 本章小结

本章介绍了Chili3D二次开发的入门知识,包括:

  1. 开发环境配置:工具推荐、项目结构、调试配置
  2. 自定义命令开发:基础命令、带参数命令、多步骤命令、可撤销命令
  3. 自定义几何体:创建新几何体类、参数化几何体
  4. 自定义UI组件:面板、工具栏、对话框
  5. 事件处理与扩展点:文档事件、视图事件、应用扩展
  6. 数据持久化扩展:云存储、自定义序列化
  7. 测试与调试:单元测试、调试技巧

通过本章的学习,读者应该能够开始进行基本的Chili3D扩展开发。在下一章中,我们将深入探讨更高级的二次开发主题。


下一章预告:第六章将介绍Chili3D二次开发的进阶内容,包括自定义渲染器、高级几何算法、性能优化、插件系统等高级主题。