feat: add 增加了几个较为简单的几何操作 #8
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|||||||
import math
|
import math
|
||||||
from typing import Any, Iterable
|
from typing import Any, Iterable
|
||||||
|
|
||||||
from build123d import Axis, Compound, Edge, Face, Plane, ShapeList, Solid, Vector, Wire, export_step
|
from build123d import Axis, Compound, Edge, Face, Helix, Location, Plane, ShapeList, Solid, Vector, Wire, export_step
|
||||||
|
|
||||||
from .runtime_types import AxisSpec, HoleSpec, PlaneSpec, TopologyRecord, Vector3, canonical_plane_signature
|
from .runtime_types import AxisSpec, HoleSpec, PlaneSpec, TopologyRecord, Vector3, canonical_plane_signature
|
||||||
|
|
||||||
@@ -146,6 +146,25 @@ class Build123dGeometryAdapter:
|
|||||||
plane = PlaneSpec.from_mapping(sketch.get("workplane") or {})
|
plane = PlaneSpec.from_mapping(sketch.get("workplane") or {})
|
||||||
return self._faces_from_circles(sketch.get("entities") or [], plane)
|
return self._faces_from_circles(sketch.get("entities") or [], plane)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _coerce_single_or_compound(result: Any, *, empty_error: str | None = None) -> Any:
|
||||||
|
"""规整一次布尔结果:None/空视为失败(可选报错),多成员合并为 Compound。"""
|
||||||
|
# build123d 的布尔方法有时返回 None(无结果)、ShapeList(多/单成员)
|
||||||
|
# 或直接返回 Solid/Compound,这里统一为单实体或 Compound。
|
||||||
|
if result is None:
|
||||||
|
if empty_error is not None:
|
||||||
|
raise ValueError(empty_error)
|
||||||
|
return None
|
||||||
|
members = list(result) if isinstance(result, ShapeList) else [result]
|
||||||
|
if not members:
|
||||||
|
if empty_error is not None:
|
||||||
|
raise ValueError(empty_error)
|
||||||
|
return None
|
||||||
|
if len(members) == 1:
|
||||||
|
return members[0]
|
||||||
|
# build123d 类型桩未声明 make_compound,但运行时存在(宽泛类型桩噪音)。
|
||||||
|
return Compound.make_compound(members) # pyright: ignore[reportAttributeAccessIssue]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def extrude(face: Face, direction: Vector3) -> Solid:
|
def extrude(face: Face, direction: Vector3) -> Solid:
|
||||||
# 沿给定方向向量拉伸一个面,生成实体。
|
# 沿给定方向向量拉伸一个面,生成实体。
|
||||||
@@ -180,13 +199,9 @@ class Build123dGeometryAdapter:
|
|||||||
pierced = Solid.extrude(face, unit * margin)
|
pierced = Solid.extrude(face, unit * margin)
|
||||||
slab = Solid.extrude(target, -unit * margin)
|
slab = Solid.extrude(target, -unit * margin)
|
||||||
trimmed = pierced.intersect(slab)
|
trimmed = pierced.intersect(slab)
|
||||||
if isinstance(trimmed, ShapeList):
|
return Build123dGeometryAdapter._coerce_single_or_compound(
|
||||||
members = list(trimmed)
|
trimmed, empty_error="extent target produced an empty trimmed solid",
|
||||||
# build123d 类型桩未声明 make_compound,但运行时存在(宽泛类型桩噪音)。
|
)
|
||||||
trimmed = members[0] if len(members) == 1 else Compound.make_compound(members) # pyright: ignore[reportAttributeAccessIssue]
|
|
||||||
if trimmed is None or (hasattr(trimmed, "is_empty") and trimmed.is_empty()):
|
|
||||||
raise ValueError("extent target produced an empty trimmed solid")
|
|
||||||
return trimmed
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def body_center(body: Any) -> Vector3:
|
def body_center(body: Any) -> Vector3:
|
||||||
@@ -288,6 +303,32 @@ class Build123dGeometryAdapter:
|
|||||||
# 以给定球心与半径生成球体实体。
|
# 以给定球心与半径生成球体实体。
|
||||||
return Solid.make_sphere(radius_mm, Plane(origin=_vector(center_mm)))
|
return Solid.make_sphere(radius_mm, Plane(origin=_vector(center_mm)))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def box(length_mm: float, width_mm: float, height_mm: float, plane: PlaneSpec | None = None) -> Solid:
|
||||||
|
# 原生立方体图元。plane 缺省为世界 XY;plane 的原点是长方体最小角点,
|
||||||
|
# 长/宽/高分别沿 plane 的 x/y/z 方向生长(build123d Solid.make_box 原生语义)。
|
||||||
|
build_plane = Build123dGeometryAdapter.plane(plane) if plane is not None else Plane.XY
|
||||||
|
return Solid.make_box(length_mm, width_mm, height_mm, build_plane)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def cylinder(radius_mm: float, height_mm: float, axis: AxisSpec | None = None) -> Solid:
|
||||||
|
# 原生圆柱图元。axis 缺省为世界 +Z;axis 的原点是底面圆心,
|
||||||
|
# 轴向由 axis 的方向决定,沿该方向生长高度。
|
||||||
|
build_plane = (
|
||||||
|
Plane(origin=_vector(axis.origin_mm), z_dir=_vector(axis.direction))
|
||||||
|
if axis is not None
|
||||||
|
else Plane.XY
|
||||||
|
)
|
||||||
|
return Solid.make_cylinder(radius_mm, height_mm, build_plane)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def intersect(left: Any, right: Any) -> Any:
|
||||||
|
# 布尔交:取两实体公共部分。结果可能为空(不相交或仅边界接触),
|
||||||
|
# 此时规整 helper 会抛出明确的空交集错误。
|
||||||
|
return Build123dGeometryAdapter._coerce_single_or_compound(
|
||||||
|
left.intersect(right), empty_error="boolean intersection produced no solid",
|
||||||
|
)
|
||||||
|
|
||||||
def hole_tool(self, spec: HoleSpec, starts: Iterable[Vector3], inward: Vector3, through_depth_mm: float) -> Solid:
|
def hole_tool(self, spec: HoleSpec, starts: Iterable[Vector3], inward: Vector3, through_depth_mm: float) -> Solid:
|
||||||
"""Build a neutral ``HoleSpec`` into one OCC cutting tool."""
|
"""Build a neutral ``HoleSpec`` into one OCC cutting tool."""
|
||||||
# 将孔规格 HoleSpec 转成一个可直接切除的 OCC 工具体。
|
# 将孔规格 HoleSpec 转成一个可直接切除的 OCC 工具体。
|
||||||
@@ -375,6 +416,73 @@ class Build123dGeometryAdapter:
|
|||||||
# 对指定边做倒角;distance_2_mm 提供时形成非对称倒角。
|
# 对指定边做倒角;distance_2_mm 提供时形成非对称倒角。
|
||||||
return body.chamfer(distance_mm, distance_2_mm, list(edges), face=face)
|
return body.chamfer(distance_mm, distance_2_mm, list(edges), face=face)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def sweep(section: Face, spine: Edge | Wire) -> Solid:
|
||||||
|
# 沿路径线扫掠截面生成实体(build123d 原生扫掠,路径可为直线/曲线/螺旋边)。
|
||||||
|
return Solid.sweep(section, spine)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def sweep_path(points: Iterable[Vector3]) -> Wire:
|
||||||
|
# 把三维点列连成折线 Wire,作为扫掠路径的通用构造入口。
|
||||||
|
vertices = [_vector(point) for point in points]
|
||||||
|
if len(vertices) < 2:
|
||||||
|
raise ValueError("sweep path needs at least two points")
|
||||||
|
return Wire([Edge.make_line(vertices[index], vertices[index + 1]) for index in range(len(vertices) - 1)])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def helix_path(radius_mm: float, pitch_mm: float, turns: float, *, lefthand: bool = False) -> Edge:
|
||||||
|
# 构造螺旋线路径(单段 Edge),供扫掠/后续螺纹、斜齿等特征使用。
|
||||||
|
# 螺旋从 (radius, 0, 0) 处沿 +Z 方向上升(lefthand=True 时反向缠绕)。
|
||||||
|
if pitch_mm <= 0:
|
||||||
|
raise ValueError("helix pitch must be positive")
|
||||||
|
if turns <= 0:
|
||||||
|
raise ValueError("helix turns must be positive")
|
||||||
|
helix = Helix(pitch=pitch_mm, height=turns * pitch_mm, radius=radius_mm, lefthand=lefthand)
|
||||||
|
return helix.edges()[0]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def pattern_linear(body: Any, count: int, direction: Vector3, spacing_mm: float) -> Any:
|
||||||
|
# 内核直接阵列:把 body 沿 direction 方向以 spacing 间距复制 count 份并合并。
|
||||||
|
# 与 runtime 的“源特征重放”pattern 不同:这里直接复制实体几何本身。
|
||||||
|
if count < 1:
|
||||||
|
raise ValueError("pattern count must be at least 1")
|
||||||
|
if count == 1 or spacing_mm == 0:
|
||||||
|
return body
|
||||||
|
vector = _vector(direction)
|
||||||
|
if vector.length <= 1e-12:
|
||||||
|
raise ValueError("pattern direction must be non-zero")
|
||||||
|
unit = vector.normalized()
|
||||||
|
result = body
|
||||||
|
for index in range(1, count):
|
||||||
|
offset = unit * (index * spacing_mm)
|
||||||
|
result = result.fuse(body.moved(Location((offset.X, offset.Y, offset.Z))))
|
||||||
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def pattern_circular(body: Any, count: int, axis: AxisSpec, sweep_angle_deg: float) -> Any:
|
||||||
|
# 内核直接阵列:把 body 绕 axis(过 axis.origin_mm、沿 axis.direction)
|
||||||
|
# 旋转 sweep_angle_deg 均布 count 份并合并。
|
||||||
|
if count < 1:
|
||||||
|
raise ValueError("pattern count must be at least 1")
|
||||||
|
if count == 1 or sweep_angle_deg == 0:
|
||||||
|
return body
|
||||||
|
origin = _vector(axis.origin_mm)
|
||||||
|
direction = _vector(axis.direction)
|
||||||
|
if direction.length <= 1e-12:
|
||||||
|
raise ValueError("pattern rotation axis must be non-zero")
|
||||||
|
unit = direction.normalized()
|
||||||
|
step_angle = sweep_angle_deg / count
|
||||||
|
# 注意:Location(pos, axis_vec, angle) 的语义是“绕世界原点旋转 + 平移 pos”,
|
||||||
|
# 因此绕任意轴点旋转需要分解为 T(-O) → R(绕原点) → T(+O) 三步合成。
|
||||||
|
to_origin = Location((-float(origin.X), -float(origin.Y), -float(origin.Z)))
|
||||||
|
back = Location((float(origin.X), float(origin.Y), float(origin.Z)))
|
||||||
|
result = body
|
||||||
|
for index in range(1, count):
|
||||||
|
rotation = Location((0.0, 0.0, 0.0), (float(unit.X), float(unit.Y), float(unit.Z)), index * step_angle)
|
||||||
|
instance = body.moved(to_origin).moved(rotation).moved(back)
|
||||||
|
result = result.fuse(instance)
|
||||||
|
return result
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def mirror(body: Any, plane: PlaneSpec) -> Any:
|
def mirror(body: Any, plane: PlaneSpec) -> Any:
|
||||||
# 沿给定平面镜像主体。
|
# 沿给定平面镜像主体。
|
||||||
|
|||||||
Reference in New Issue
Block a user