第五章:二次开发入门
5.1 开发环境配置
5.1.1 推荐开发工具
进行Chili3D二次开发,推荐使用以下开发工具:
代码编辑器:
- Visual Studio Code(推荐):免费、开源、功能强大
- JetBrains WebStorm:专业的Web开发IDE
推荐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二次开发的入门知识,包括:
- 开发环境配置:工具推荐、项目结构、调试配置
- 自定义命令开发:基础命令、带参数命令、多步骤命令、可撤销命令
- 自定义几何体:创建新几何体类、参数化几何体
- 自定义UI组件:面板、工具栏、对话框
- 事件处理与扩展点:文档事件、视图事件、应用扩展
- 数据持久化扩展:云存储、自定义序列化
- 测试与调试:单元测试、调试技巧
通过本章的学习,读者应该能够开始进行基本的Chili3D扩展开发。在下一章中,我们将深入探讨更高级的二次开发主题。
下一章预告:第六章将介绍Chili3D二次开发的进阶内容,包括自定义渲染器、高级几何算法、性能优化、插件系统等高级主题。