第08章-二次开发基础

第08章:二次开发基础

8.1 CadQuery架构深入

8.1.1 CadQuery代码结构

CadQuery的代码结构清晰,主要包含以下模块:

cadquery/
├── __init__.py          # 主入口,导出公共API
├── cq.py                # 核心Workplane类
├── selectors.py         # 选择器系统
├── exporters/           # 导出模块
│   ├── __init__.py
│   ├── assembly.py
│   └── utils.py
├── importers/           # 导入模块
│   ├── __init__.py
│   └── ...
├── occ_impl/            # OCCT包装层
│   ├── shapes.py        # 形状类
│   ├── geom.py          # 几何类
│   └── ...
├── sketch.py            # Sketch API
└── assembly.py          # 装配体模块

8.1.2 核心类关系

# CadQuery核心类层次

# 1. Workplane - 主要的用户接口
class Workplane:
    """工作平面类,链式调用的核心"""
    plane: Plane           # 当前工作平面
    objects: List[Shape]   # 对象栈
    ctx: BuildContext      # 构建上下文
    
# 2. Shape - 几何形状基类
class Shape:
    """所有几何形状的基类"""
    wrapped: TopoDS_Shape  # OCCT底层对象
    
# 3. Compound, Solid, Face, Edge, Vertex - 具体形状类
class Solid(Shape):
    """实体形状"""
    pass

# 4. Selector - 选择器基类
class Selector:
    """选择器基类"""
    def filter(self, objectList): ...

8.1.3 OCP绑定层

CadQuery使用OCP(Open CASCADE Python)作为与OCCT内核的桥梁:

# OCP提供的主要模块
from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeCylinder
from OCP.BRepBuilderAPI import BRepBuilderAPI_Transform
from OCP.BRepAlgoAPI import BRepAlgoAPI_Fuse, BRepAlgoAPI_Cut
from OCP.gp import gp_Pnt, gp_Vec, gp_Ax1

# 示例:直接使用OCP创建盒子
from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox

box_maker = BRepPrimAPI_MakeBox(10, 20, 30)
box_shape = box_maker.Shape()

8.2 扩展CadQuery

8.2.1 添加自定义方法

CadQuery允许通过猴子补丁方式添加自定义方法:

import cadquery as cq

def custom_rounded_box(self, length, width, height, radius):
    """
    创建圆角盒子的自定义方法
    
    Args:
        length: 长度
        width: 宽度
        height: 高度
        radius: 圆角半径
    """
    return (
        self
        .rect(length, width)
        .extrude(height)
        .edges("|Z")
        .fillet(radius)
    )

# 将方法添加到Workplane类
cq.Workplane.roundedBox = custom_rounded_box

# 使用自定义方法
result = cq.Workplane("XY").roundedBox(30, 20, 10, 3)
cq.exporters.export(result, "rounded_box.step")

8.2.2 创建自定义选择器

import cadquery as cq
from cadquery import Selector

class AreaSelector(Selector):
    """按面积选择面"""
    
    def __init__(self, min_area=0, max_area=float('inf')):
        self.min_area = min_area
        self.max_area = max_area
    
    def filter(self, objectList):
        result = []
        for obj in objectList:
            if hasattr(obj, 'Area'):
                area = obj.Area()
                if self.min_area <= area <= self.max_area:
                    result.append(obj)
        return result

# 使用自定义选择器
model = cq.Workplane("XY").box(50, 30, 20)
large_faces = model.faces(AreaSelector(min_area=1000))
print(f"大面积面数: {len(large_faces.vals())}")

8.2.3 扩展导出格式

import cadquery as cq
import json

def export_to_custom_format(shape, filename):
    """导出为自定义JSON格式"""
    bb = shape.val().BoundingBox()
    vol = shape.val().Volume()
    
    data = {
        "type": "CadQueryModel",
        "version": "1.0",
        "bounding_box": {
            "min": [bb.xmin, bb.ymin, bb.zmin],
            "max": [bb.xmax, bb.ymax, bb.zmax]
        },
        "volume": vol,
        "faces_count": len(shape.faces().vals()),
        "edges_count": len(shape.edges().vals())
    }
    
    with open(filename, 'w') as f:
        json.dump(data, f, indent=2)

# 添加到exporters模块(可选)
# cq.exporters.export_custom = export_to_custom_format

# 使用
model = cq.Workplane("XY").box(30, 20, 10)
export_to_custom_format(model, "model_info.json")

8.3 使用OCP直接操作

8.3.1 OCP基础操作

import cadquery as cq
from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeCylinder
from OCP.gp import gp_Pnt, gp_Dir, gp_Ax2

# 使用OCP创建盒子
def make_box_ocp(length, width, height):
    """使用OCP直接创建盒子"""
    builder = BRepPrimAPI_MakeBox(length, width, height)
    shape = builder.Shape()
    
    # 转换为CadQuery Shape对象
    return cq.Shape.cast(shape)

# 使用OCP创建圆柱
def make_cylinder_ocp(radius, height):
    """使用OCP直接创建圆柱"""
    builder = BRepPrimAPI_MakeCylinder(radius, height)
    shape = builder.Shape()
    return cq.Shape.cast(shape)

# 测试
box = make_box_ocp(30, 20, 10)
print(f"体积: {box.Volume()}")

8.3.2 复杂几何操作

import cadquery as cq
from OCP.BRepFilletAPI import BRepFilletAPI_MakeFillet
from OCP.TopExp import TopExp_Explorer
from OCP.TopAbs import TopAbs_EDGE

def advanced_fillet(shape, radius):
    """使用OCP实现高级圆角"""
    # 获取底层OCCT形状
    occ_shape = shape.val().wrapped
    
    # 创建圆角构造器
    fillet_builder = BRepFilletAPI_MakeFillet(occ_shape)
    
    # 遍历所有边并添加圆角
    explorer = TopExp_Explorer(occ_shape, TopAbs_EDGE)
    while explorer.More():
        edge = explorer.Current()
        fillet_builder.Add(radius, edge)
        explorer.Next()
    
    # 构建结果
    result_shape = fillet_builder.Shape()
    
    # 转换回CadQuery
    return cq.Workplane("XY").add(cq.Shape.cast(result_shape))

# 使用
model = cq.Workplane("XY").box(30, 20, 10)
filleted = advanced_fillet(model, 2)
cq.exporters.export(filleted, "advanced_fillet.step")

8.3.3 访问底层几何

import cadquery as cq
from OCP.BRep import BRep_Tool
from OCP.TopoDS import TopoDS
from OCP.Geom import Geom_Surface

def analyze_faces(model):
    """分析模型的面信息"""
    faces = model.faces().vals()
    
    for i, face in enumerate(faces):
        # 获取底层OCCT面
        occ_face = face.wrapped
        
        # 获取几何曲面
        surface = BRep_Tool.Surface_s(occ_face)
        
        # 获取面积
        area = face.Area()
        
        # 获取中心点
        center = face.Center()
        
        print(f"面 {i+1}:")
        print(f"  面积: {area:.2f} mm²")
        print(f"  中心: ({center.x:.2f}, {center.y:.2f}, {center.z:.2f})")
        print(f"  类型: {type(surface).__name__}")

# 使用
model = cq.Workplane("XY").box(30, 20, 10)
analyze_faces(model)

8.4 创建CadQuery插件

8.4.1 插件结构

标准CadQuery插件结构:

my_cq_plugin/
├── __init__.py
├── operations.py
├── selectors.py
└── utils.py

8.4.2 基本插件示例

# my_cq_plugin/__init__.py
"""
我的CadQuery插件

提供额外的建模功能
"""

import cadquery as cq
from .operations import *
from .selectors import *

__version__ = "0.1.0"

def register_all():
    """注册所有扩展"""
    register_operations()
    register_selectors()
# my_cq_plugin/operations.py
"""自定义操作"""

import cadquery as cq

def hexagonal_pattern(self, radius, count, feature_func):
    """
    创建六边形图案
    
    Args:
        radius: 图案半径
        count: 每圈数量
        feature_func: 在每个位置执行的函数
    """
    import math
    
    result = self
    for i in range(count):
        angle = 2 * math.pi * i / count
        x = radius * math.cos(angle)
        y = radius * math.sin(angle)
        result = result.center(x, y)
        result = feature_func(result)
        result = result.center(-x, -y)
    
    return result

def register_operations():
    """注册自定义操作到Workplane"""
    cq.Workplane.hexPattern = hexagonal_pattern
# my_cq_plugin/selectors.py
"""自定义选择器"""

import cadquery as cq
from cadquery import Selector

class TypeSelector(Selector):
    """按几何类型选择"""
    
    def __init__(self, geom_type):
        self.geom_type = geom_type
    
    def filter(self, objectList):
        return [obj for obj in objectList 
                if self.geom_type.lower() in type(obj).__name__.lower()]

def register_selectors():
    """注册选择器(如果需要)"""
    pass

8.4.3 使用插件

import cadquery as cq
import my_cq_plugin

# 注册插件功能
my_cq_plugin.register_all()

# 使用插件提供的功能
result = (
    cq.Workplane("XY")
    .box(100, 100, 10)
    .faces(">Z")
    .workplane()
    .hexPattern(30, 6, lambda wp: wp.circle(5).cutBlind(-3))
)

8.5 集成外部工具

8.5.1 与NumPy集成

import cadquery as cq
import numpy as np

def make_surface_from_array(z_array, x_size, y_size, z_scale=1.0):
    """
    从NumPy数组创建曲面
    
    Args:
        z_array: 2D NumPy数组,包含Z值
        x_size: X方向尺寸
        y_size: Y方向尺寸
        z_scale: Z方向缩放
    """
    rows, cols = z_array.shape
    x_step = x_size / (cols - 1)
    y_step = y_size / (rows - 1)
    
    # 生成点列表
    points = []
    for i in range(rows):
        row_points = []
        for j in range(cols):
            x = j * x_step - x_size / 2
            y = i * y_step - y_size / 2
            z = z_array[i, j] * z_scale
            row_points.append((x, y, z))
        points.append(row_points)
    
    return points

# 创建一个正弦波表面
x = np.linspace(0, 2*np.pi, 20)
y = np.linspace(0, 2*np.pi, 20)
X, Y = np.meshgrid(x, y)
Z = np.sin(X) * np.cos(Y)

surface_points = make_surface_from_array(Z, 50, 50, z_scale=10)
print(f"生成了 {len(surface_points)} x {len(surface_points[0])} 个点")

8.5.2 与Pandas集成

import cadquery as cq
import pandas as pd

def generate_parts_from_csv(csv_file):
    """从CSV文件批量生成零件"""
    df = pd.read_csv(csv_file)
    
    parts = {}
    for _, row in df.iterrows():
        name = row['name']
        length = row['length']
        width = row['width']
        height = row['height']
        
        part = (
            cq.Workplane("XY")
            .box(length, width, height)
            .edges()
            .fillet(min(length, width, height) * 0.05)
        )
        
        parts[name] = part
        cq.exporters.export(part, f"{name}.step")
    
    return parts

# CSV文件格式:
# name,length,width,height
# part_a,30,20,10
# part_b,50,30,15
# part_c,40,40,20

8.5.3 与matplotlib集成

import cadquery as cq
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
import numpy as np

def visualize_model(model, filename="model_preview.png"):
    """使用matplotlib可视化模型"""
    # 获取模型的三角网格
    # 这需要先导出为STL然后解析,或使用其他方法
    
    # 简化:只绘制边界框
    bb = model.val().BoundingBox()
    
    fig = plt.figure(figsize=(10, 8))
    ax = fig.add_subplot(111, projection='3d')
    
    # 绘制边界框
    vertices = [
        [bb.xmin, bb.ymin, bb.zmin],
        [bb.xmax, bb.ymin, bb.zmin],
        [bb.xmax, bb.ymax, bb.zmin],
        [bb.xmin, bb.ymax, bb.zmin],
        [bb.xmin, bb.ymin, bb.zmax],
        [bb.xmax, bb.ymin, bb.zmax],
        [bb.xmax, bb.ymax, bb.zmax],
        [bb.xmin, bb.ymax, bb.zmax]
    ]
    
    # 定义边界框的12条边
    edges = [
        [0, 1], [1, 2], [2, 3], [3, 0],  # 底面
        [4, 5], [5, 6], [6, 7], [7, 4],  # 顶面
        [0, 4], [1, 5], [2, 6], [3, 7]   # 竖直边
    ]
    
    for edge in edges:
        points = [vertices[edge[0]], vertices[edge[1]]]
        ax.plot3D(*zip(*points), 'b-')
    
    ax.set_xlabel('X')
    ax.set_ylabel('Y')
    ax.set_zlabel('Z')
    ax.set_title('模型预览')
    
    plt.savefig(filename)
    plt.close()
    
    print(f"预览图已保存: {filename}")

# 使用
model = cq.Workplane("XY").box(30, 20, 10)
visualize_model(model)

8.6 错误处理与调试

8.6.1 常见错误类型

import cadquery as cq

# 1. 几何构建错误
try:
    # 错误:圆角半径太大
    result = cq.Workplane("XY").box(10, 10, 10).edges().fillet(10)
except Exception as e:
    print(f"几何错误: {e}")

# 2. 选择器错误
try:
    # 错误:无效的选择器语法
    result = cq.Workplane("XY").box(10, 10, 10).faces(">>Z")
except Exception as e:
    print(f"选择器错误: {e}")

# 3. 布尔运算错误
try:
    # 错误:不相交的布尔运算
    box1 = cq.Workplane("XY").box(10, 10, 10)
    box2 = cq.Workplane("XY").center(100, 100).box(5, 5, 5)
    result = box1.intersect(box2)  # 交集为空
except Exception as e:
    print(f"布尔运算错误: {e}")

8.6.2 调试装饰器

import cadquery as cq
import functools
import traceback

def debug_operation(func):
    """调试装饰器"""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"调用: {func.__name__}")
        print(f"  参数: {args[1:]}")  # 跳过self
        print(f"  关键字参数: {kwargs}")
        try:
            result = func(*args, **kwargs)
            print(f"  成功!")
            return result
        except Exception as e:
            print(f"  失败: {e}")
            traceback.print_exc()
            raise
    return wrapper

# 使用装饰器
class DebugWorkplane(cq.Workplane):
    @debug_operation
    def box(self, *args, **kwargs):
        return super().box(*args, **kwargs)
    
    @debug_operation  
    def hole(self, *args, **kwargs):
        return super().hole(*args, **kwargs)

8.6.3 日志记录

import cadquery as cq
import logging

# 配置日志
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(levelname)s - %(message)s',
    filename='cadquery_debug.log'
)

logger = logging.getLogger('CadQueryDebug')

def logged_export(model, filename):
    """带日志的导出函数"""
    logger.info(f"开始导出: {filename}")
    
    try:
        # 记录模型信息
        vol = model.val().Volume()
        faces = len(model.faces().vals())
        logger.debug(f"模型体积: {vol:.2f}")
        logger.debug(f"面数: {faces}")
        
        # 执行导出
        cq.exporters.export(model, filename)
        logger.info(f"导出成功: {filename}")
        
    except Exception as e:
        logger.error(f"导出失败: {e}")
        raise

# 使用
model = cq.Workplane("XY").box(30, 20, 10)
logged_export(model, "logged_model.step")

8.7 测试框架

8.7.1 单元测试基础

import unittest
import cadquery as cq

class TestCadQueryOperations(unittest.TestCase):
    """CadQuery操作测试"""
    
    def test_box_creation(self):
        """测试盒子创建"""
        box = cq.Workplane("XY").box(30, 20, 10)
        vol = box.val().Volume()
        self.assertAlmostEqual(vol, 30 * 20 * 10, places=1)
    
    def test_cylinder_creation(self):
        """测试圆柱创建"""
        import math
        cyl = cq.Workplane("XY").cylinder(20, 10)
        vol = cyl.val().Volume()
        expected = math.pi * 10**2 * 20
        self.assertAlmostEqual(vol, expected, places=0)
    
    def test_hole_creation(self):
        """测试孔创建"""
        result = (
            cq.Workplane("XY")
            .box(30, 30, 10)
            .faces(">Z")
            .workplane()
            .hole(10)
        )
        # 验证体积减少
        box_vol = 30 * 30 * 10
        result_vol = result.val().Volume()
        self.assertLess(result_vol, box_vol)
    
    def test_fillet_edges(self):
        """测试圆角"""
        result = (
            cq.Workplane("XY")
            .box(30, 20, 10)
            .edges("|Z")
            .fillet(2)
        )
        # 验证模型有效
        self.assertIsNotNone(result.val())

if __name__ == '__main__':
    unittest.main()

8.7.2 参数化测试

import unittest
import cadquery as cq

class TestParametricBox(unittest.TestCase):
    """参数化盒子测试"""
    
    test_cases = [
        (10, 10, 10),
        (30, 20, 10),
        (50, 50, 50),
        (100, 60, 20),
    ]
    
    def test_box_volumes(self):
        """测试不同尺寸盒子的体积"""
        for length, width, height in self.test_cases:
            with self.subTest(l=length, w=width, h=height):
                box = cq.Workplane("XY").box(length, width, height)
                vol = box.val().Volume()
                expected = length * width * height
                self.assertAlmostEqual(vol, expected, places=1,
                    msg=f"盒子 {length}x{width}x{height} 体积错误")

if __name__ == '__main__':
    unittest.main()

8.8 本章小结

本章介绍了CadQuery二次开发的基础知识:

  1. 架构理解

    • CadQuery代码结构
    • 核心类关系
    • OCP绑定层
  2. 扩展方法

    • 添加自定义方法
    • 创建自定义选择器
    • 扩展导出格式
  3. OCP操作

    • 直接使用OCCT API
    • 复杂几何操作
    • 访问底层几何
  4. 插件开发

    • 插件结构
    • 注册机制
    • 插件使用
  5. 外部集成

    • NumPy、Pandas集成
    • 可视化
  6. 调试与测试

    • 错误处理
    • 日志记录
    • 单元测试

通过本章学习,您应该能够:

  • 理解CadQuery的内部架构
  • 扩展CadQuery功能
  • 使用OCP进行高级操作
  • 创建CadQuery插件
  • 有效调试和测试代码

下一章我们将学习更高级的二次开发技巧和实际项目案例。


posted @ 2026-01-10 13:15  我才是银古  阅读(9)  评论(0)    收藏  举报