first commit

This commit is contained in:
2026-07-22 13:48:46 +08:00
commit c87751c3dc
2820 changed files with 726976 additions and 0 deletions
+106
View File
@@ -0,0 +1,106 @@
# SimpleCAD API Core Classes Documentation
This directory documents the core public object model for SimpleCADAPI.
SimpleCADAPI is OCP-native at runtime: public geometry objects are thin Python wrappers around OpenCascade/OCP shapes exposed through the `.wrapped` attribute. The package provides functional modeling operations, expression parameters, QL selectors, and replayable model JSON.
## Core Classes Overview
### Coordinate and tagging utilities
#### [CoordinateSystem](coordinate_system.md)
A right-handed 3D coordinate system for local modeling contexts and point/vector transformation.
#### [SimpleWorkplane](simple_workplane.md)
A context manager for temporarily modeling in a local coordinate system.
#### [TaggedMixin](tagged_mixin.md)
Shared tag and metadata behavior for geometry wrappers.
### Geometry wrappers
#### [Vertex](vertex.md)
A 0D topology wrapper with coordinate queries.
#### [Edge](edge.md)
A 1D topology wrapper for lines, arcs, circles, splines, and other curve edges.
#### [Wire](wire.md)
A connected path made from edges. Wires may be open or closed.
#### [Face](face.md)
A bounded surface with an outer wire and optional inner wires.
#### [Solid](solid.md)
A closed 3D body with volume, faces, edges, tags, and metadata.
#### [Compound](compound.md)
A collection wrapper for multiple geometry objects.
### Product semantics roadmap
#### [Part and Assembly Development Plan](part_assembly_development_plan.md)
Planned single-body `Part`, material assignment, component placement, and `Assembly` semantics layered above the current topology/geometry and operation graph model.
## Relationship Diagram
```text
TaggedMixin
├── Vertex (0D)
├── Edge (1D)
├── Wire (1D) ← composed of edges
├── Face (2D) ← bounded by wires
├── Solid (3D) ← bounded by faces
└── Compound ← collection of shapes
CoordinateSystem ← independent utility
SimpleWorkplane ← local modeling context
```
## Design Principles
- **Shape-first API**: users work with `Vertex`, `Edge`, `Wire`, `Face`, and `Solid`, not graph nodes.
- **Functional modeling style**: public operations return new geometry values, e.g. `make_box_rsolid(...)`, `cut_rsolid(...)`, `fillet_rsolid(...)`.
- **OCP-native runtime**: geometry construction, topology traversal, properties, booleans, transforms, and export use OCP/OpenCascade helpers.
- **Replayable graph workflows**: `GraphSession` can record a canonical low-level operation graph and `export_model_json()` can serialize it for `replay_model_json()`.
- **Tags and metadata**: tags are useful for lightweight semantics; structured numeric facts should be stored in metadata such as `metadata["geo"]`.
- **Indexed topology access**: use plural methods such as `get_edges()` and `get_faces()` for enumeration, and pass an index to the same getter, such as `get_edges(index)` or `get_faces(index)`, for intentional indexed picks that should become graph selection nodes.
## Basic Usage
```python
import simplecadapi as scad
with scad.SimpleWorkplane(origin=(0, 0, 0)):
box = scad.make_box_rsolid(width=5, height=3, depth=2)
scad.apply_tag(box, "role.bracket")
box.set_metadata("material", "6061-T6")
box.auto_tag_faces("box")
top_faces = [face for face in box.get_faces() if "face.top" in scad.list_tags(face)]
print(len(top_faces))
```
## Replayable Model JSON
```python
import simplecadapi as scad
with scad.GraphSession() as session:
body = scad.make_box_rsolid(10, 10, 4)
hole = scad.make_cylinder_rsolid(1.5, 8, bottom_face_center=(0, 0, -2))
part = scad.cut_rsolid(body, hole)
payload = scad.export_model_json(session)
rebuilt = scad.replay_model_json(payload)
print(len(rebuilt))
```
## More Resources
- [API Reference Documentation](../api/)
- [Examples](../../examples/)
- [User Guide](../../README.md)
- [JSON Operation Graph Spec](operation_graph_json_spec.md)
- [Serialization and Replay Operation Guides](serialization/)
+22
View File
@@ -0,0 +1,22 @@
# Compound
`Compound` is an explicit multi-shape projection wrapper.
SimpleCADAPI 2.0 beta keeps integrated modeling centered on single `Solid`
results, but product-level `Assembly` workflows can be projected into a flattened
`Compound` with `make_compound_from_assembly_rcompound(...)`.
The stable public geometry wrapper surface is:
- `Vertex`
- `Edge`
- `Wire`
- `Face`
- `Solid`
- `Compound`
Use `Compound` when a workflow intentionally needs a flattened geometry
projection. Do not use it as a substitute for `Assembly` product structure.
For normal part modeling, keep using `Solid` as the body-level geometry. A
single-body `Part` wraps exactly one `Solid`.
@@ -0,0 +1,30 @@
# CoordinateSystem
`CoordinateSystem` represents a right-handed local coordinate frame used by `SimpleWorkplane` and public modeling operations.
## Public constructor
```python
CoordinateSystem(
origin=(0, 0, 0),
x_axis=(1, 0, 0),
y_axis=(0, 1, 0),
z_axis=(0, 0, 1),
)
```
## Main capabilities
- Store an origin and orthonormal axes.
- Transform local points to global coordinates.
- Transform local vectors to global vectors.
- Format readable frame diagnostics.
## Example
```python
import simplecadapi as scad
cs = scad.CoordinateSystem(origin=(10, 0, 0))
print(cs.transform_point((1, 2, 3)))
```
@@ -0,0 +1,65 @@
# Declarative Constraint Status
Sketch constraints are supported through the isomorphic sketch API surface:
- `make_sketch_rsketch(...)`
- `add_point_rsketch(...)`
- `add_line_rsketch(...)`
- `add_circle_rsketch(...)`
- `constrain_*_rsketch(...)`
- `inspect_sketch_rsketchresult(...)` for non-recording diagnostics
- `make_wire_from_sketch_rwire(...)`
- `make_face_from_sketch_rface(...)`
When the modeling intent is a sketch/profile, use these sketch APIs as the only recommended construction path. Concrete geometry APIs such as `make_line_redge(...)` and `make_wire_from_edges_rwire(...)` remain for paths, pure geometry, and internal lowering targets.
Sketch document construction is functional: `add_*_rsketch(...)` and `constrain_*_rsketch(...)` return an updated `Sketch` instead of mutating the input document. Each sketch entity must have a stable explicit id, and constraints may use those ids directly:
```python
sketch = make_sketch_rsketch("plate_profile")
sketch = add_point_rsketch(sketch, "p0", 0.0, 0.0)
sketch = add_point_rsketch(sketch, "p1", 72.0, 0.0)
sketch = add_line_rsketch(sketch, "bottom", "p0", "p1")
sketch = constrain_horizontal_rsketch(sketch, "bottom")
sketch = constrain_distance_rsketch(sketch, "p0", "p1", 72.0)
```
`solve` is not a first-class modeling graph leaf. `make_wire_from_sketch_rwire(...)` and `make_face_from_sketch_rface(...)` run the sketch solver internally during `Sketch -> Wire/Face` promotion. Promotion graph nodes record `solve_snapshot` and `promotion_map` evidence, and promoted geometry receives `source_sketch`, `sketch_solve`, and sketch entity metadata/tags. `inspect_sketch_rsketchresult(...)` is diagnostic-only and does not record graph nodes.
In `.FCStd` translation, sketch promotion is represented by a visible `Sketcher::SketchObject`. `make_face_from_sketch_rface(...)` does not create a separate face bridge object in the graph path; downstream FreeCAD operations such as `Part::Extrusion` use the promoted Sketcher object as their base. FreeCAD Sketcher constraints are validated as they are added: constraints that are unsupported, crash-risky, or rejected as redundant by FreeCAD are recorded in `SimpleCADSketchConstraints.skipped` instead of being emitted in a way that would make the sketch unsolvable or force a synthetic base object.
# Assembly Constraint Status
Assembly containers, explicit part transforms, and declarative assembly constraints are temporarily removed from the public/support surface while the assembly system is redesigned.
Removed public APIs include:
- `Assembly`
- `PartHandle`
- `PointAnchor`
- `AxisAnchor`
- `AssemblyResult`
- `SolveReport`
- `make_assembly_rassembly`
- `clone_assembly_rassembly`
- `add_part_rassembly`
- `translate_part_rassembly`
- `rotate_part_rassembly`
- `solve_assembly_rresult`
- `constrain_coincident_rassembly`
- `constrain_concentric_rassembly`
- `constrain_offset_rassembly`
- `constrain_distance_rassembly`
- `clear_constraints_rassembly`
- `stack_rassembly`
- `stack`
Current supported workflows should model final parts as ordinary geometry:
- Use `translate_shape(...)`, `rotate_shape(...)`, and `mirror_shape(...)` for explicit placement.
- Use Python sequences of `Solid` objects plus `export_step([...], path)` / `export_stl(...)` for multi-body exports.
- Use `union_rsolid(...)`, `cut_rsolid(...)`, and `intersect_rsolid(...)` when a single merged solid is required.
`export_model_json(...)` no longer accepts `assembly=...`, and newly exported model JSON does not include `assembly`, `assembly_registry`, or `constraint_registry` fields.
The next assembly implementation should define a new assembly graph / constraint graph contract before reintroducing public APIs.
+386
View File
@@ -0,0 +1,386 @@
# Edge
## Overview
`Edge` is the edge class in SimpleCAD API, representing a 1D geometric element connecting two vertices. Edges can be lines, arcs, splines, and other types of curves. It wraps OCP's Edge object and adds tag functionality.
## Class Definition
```python
class Edge(TaggedMixin):
"""边类,包装OCP的Edge,添加标签功能"""
```
## Inheritance Relationships
- Inherits from `TaggedMixin`, with tag and metadata functionality
## Usage
- Represent connections between two points
- Fundamental elements composing Wires and Faces
- Provide geometric information (length, vertices, etc.)
- Support tag management and queries
## Constructor
### `__init__(wrapped)`
Initialize an edge object.
**Parameters:**
- `wrapped` (OCP TopoDS_Edge): OCP edge object
**Exceptions:**
- `ValueError`: Raised when the input edge object is invalid
**Example:**
```python
from simplecadapi import make_line_redge, make_circle_redge
# 通过 SimpleCAD 函数创建边
line_edge = make_line_redge(start=(0, 0, 0), end=(1, 1, 0))
circle_edge = make_circle_redge(center=(0, 0, 0), radius=1.0)
```
## Main Properties
- `wrapped`: Underlying OCP edge object
- `_tags`: Tag set (inherited from TaggedMixin)
- `_metadata`: Metadata dictionary (inherited from TaggedMixin)
## Common Methods
### `get_length()`
Get the length of the edge.
**Returns:**
- `float`: Edge length
**Exceptions:**
- `ValueError`: Raised when length retrieval fails
**Example:**
```python
from simplecadapi import make_line_redge, make_circle_redge
import math
# 直线边
line = make_line_redge(start=(0, 0, 0), end=(3, 4, 0))
line_length = line.get_length()
print(f"直线长度: {line_length}") # 5.0
# 圆形边
circle = make_circle_redge(center=(0, 0, 0), radius=2.0)
circle_length = circle.get_length()
print(f"圆形周长: {circle_length}") # 约 12.566 (2π * 2)
```
### `get_start_vertex()`
Get the start vertex of the edge.
**Returns:**
- `Vertex`: Start vertex object
**Exceptions:**
- `ValueError`: Raised when vertex retrieval fails
**Example:**
```python
from simplecadapi import make_line_redge
line = make_line_redge(start=(1, 2, 3), end=(4, 5, 6))
start_vertex = line.get_start_vertex()
start_coords = start_vertex.get_coordinates()
print(f"起始点坐标: {start_coords}") # (1.0, 2.0, 3.0)
```
### `get_end_vertex()`
Get the end vertex of the edge.
**Returns:**
- `Vertex`: End vertex object
**Exceptions:**
- `ValueError`: Raised when vertex retrieval fails
**Example:**
```python
from simplecadapi import make_line_redge
line = make_line_redge(start=(1, 2, 3), end=(4, 5, 6))
end_vertex = line.get_end_vertex()
end_coords = end_vertex.get_coordinates()
print(f"结束点坐标: {end_coords}") # (4.0, 5.0, 6.0)
```
### Tagging and Metadata
Use the functional public API `apply_tag(shape, tag)` and `list_tags(shape)` for tags. Use `set_metadata(key, value)` and `get_metadata(key, default=None)` for structured metadata.
## Usage Examples
### Creating Different Types of Edges
```python
from simplecadapi import (
make_line_redge,
make_circle_redge,
make_three_point_arc_redge,
make_spline_redge
)
# 直线边
line = make_line_redge(start=(0, 0, 0), end=(5, 0, 0))
apply_tag(line, "base_line")
# 圆形边
circle = make_circle_redge(center=(0, 0, 0), radius=2.0)
apply_tag(circle, "full_circle")
# 三点圆弧边
arc = make_three_point_arc_redge(
start=(0, 0, 0),
mid=(1, 1, 0),
end=(2, 0, 0)
)
apply_tag(arc, "arc_segment")
# 样条边:control_points 是 B-spline poles,不是采样点
spline = make_spline_redge(
control_points=[(0, 0, 0), (1, 1, 0), (2, 1, 0), (3, 0, 0)]
)
apply_tag(spline, "smooth_curve")
# 打印边的信息
edges = [line, circle, arc, spline]
for edge in edges:
print(f"边标签: {list_tags(edge)}, 长度: {edge.get_length():.3f}")
```
### Edge Analysis and Classification
```python
from simplecadapi import make_line_redge
import math
def analyze_edge_collection():
"""分析边的集合"""
# 创建多条边
edges = [
make_line_redge(start=(0, 0, 0), end=(1, 0, 0)), # 水平线
make_line_redge(start=(0, 0, 0), end=(0, 1, 0)), # 垂直线
make_line_redge(start=(0, 0, 0), end=(1, 1, 0)), # 对角线
make_line_redge(start=(0, 0, 0), end=(2, 0, 0)), # 长水平线
make_line_redge(start=(0, 0, 0), end=(0, 2, 0)), # 长垂直线
]
# 分析每条边
for i, edge in enumerate(edges):
length = edge.get_length()
start_coords = edge.get_start_vertex().get_coordinates()
end_coords = edge.get_end_vertex().get_coordinates()
# 计算方向向量
direction = (
end_coords[0] - start_coords[0],
end_coords[1] - start_coords[1],
end_coords[2] - start_coords[2]
)
# 分类边
if abs(direction[0]) > 0 and abs(direction[1]) == 0:
apply_tag(edge, "horizontal")
elif abs(direction[0]) == 0 and abs(direction[1]) > 0:
apply_tag(edge, "vertical")
elif abs(direction[0]) > 0 and abs(direction[1]) > 0:
apply_tag(edge, "diagonal")
# 根据长度分类
if length < 1.5:
apply_tag(edge, "short")
else:
apply_tag(edge, "long")
# 添加元数据
edge.set_metadata("length", length)
edge.set_metadata("direction", direction)
edge.set_metadata("index", i)
print(f"{i}: 长度={length:.3f}, 标签={list_tags(edge)}")
analyze_edge_collection()
```
### Building Edge Networks
```python
from simplecadapi import make_line_redge
def create_edge_network():
"""创建边的网络结构"""
# 定义节点
nodes = [
(0, 0, 0), # A
(2, 0, 0), # B
(2, 2, 0), # C
(0, 2, 0), # D
(1, 1, 0), # E (中心点)
]
# 定义连接关系
connections = [
(0, 1), # A-B
(1, 2), # B-C
(2, 3), # C-D
(3, 0), # D-A
(0, 4), # A-E
(1, 4), # B-E
(2, 4), # C-E
(3, 4), # D-E
]
edges = []
for i, (start_idx, end_idx) in enumerate(connections):
start_point = nodes[start_idx]
end_point = nodes[end_idx]
edge = make_line_redge(start=start_point, end=end_point)
# 添加连接信息
apply_tag(edge, f"connection_{chr(65+start_idx)}{chr(65+end_idx)}")
# 分类边
if start_idx < 4 and end_idx < 4:
apply_tag(edge, "perimeter")
else:
apply_tag(edge, "internal")
# 添加元数据
edge.set_metadata("start_node", chr(65+start_idx))
edge.set_metadata("end_node", chr(65+end_idx))
edge.set_metadata("connection_index", i)
edges.append(edge)
return edges
# 创建网络
network_edges = create_edge_network()
# 分析网络
perimeter_edges = [e for e in network_edges if "perimeter" in list_tags(e)]
internal_edges = [e for e in network_edges if "internal" in list_tags(e)]
print(f"周边边数: {len(perimeter_edges)}")
print(f"内部边数: {len(internal_edges)}")
# 计算总长度
total_length = sum(edge.get_length() for edge in network_edges)
print(f"网络总长度: {total_length:.3f}")
```
### Edge Geometric Calculations
```python
from simplecadapi import make_line_redge, make_circle_redge
import math
def calculate_edge_properties():
"""计算边的几何属性"""
# 创建不同类型的边
line = make_line_redge(start=(0, 0, 0), end=(3, 4, 0))
circle = make_circle_redge(center=(0, 0, 0), radius=5.0)
# 直线属性
line_length = line.get_length()
line_start = line.get_start_vertex().get_coordinates()
line_end = line.get_end_vertex().get_coordinates()
# 计算直线的中点
line_midpoint = (
(line_start[0] + line_end[0]) / 2,
(line_start[1] + line_end[1]) / 2,
(line_start[2] + line_end[2]) / 2
)
# 计算直线的方向向量
line_direction = (
line_end[0] - line_start[0],
line_end[1] - line_start[1],
line_end[2] - line_start[2]
)
# 归一化方向向量
line_dir_length = math.sqrt(sum(x*x for x in line_direction))
line_unit_direction = tuple(x / line_dir_length for x in line_direction)
# 圆形属性
circle_length = circle.get_length() # 周长
circle_radius = circle_length / (2 * math.pi)
# 存储计算结果
line.set_metadata("midpoint", line_midpoint)
line.set_metadata("direction", line_direction)
line.set_metadata("unit_direction", line_unit_direction)
apply_tag(line, "calculated")
circle.set_metadata("radius", circle_radius)
circle.set_metadata("circumference", circle_length)
apply_tag(circle, "calculated")
print(f"直线长度: {line_length:.3f}")
print(f"直线中点: {line_midpoint}")
print(f"直线单位方向: {line_unit_direction}")
print(f"圆形周长: {circle_length:.3f}")
print(f"圆形半径: {circle_radius:.3f}")
calculate_edge_properties()
```
## String Representation
```python
from simplecadapi import make_line_redge
edge = make_line_redge(start=(0, 0, 0), end=(3, 4, 0))
apply_tag(edge, "example_edge")
edge.set_metadata("type", "line")
print(edge)
```
Output:
```
Edge:
length: 5.000
vertices:
start: (0.0, 0.0, 0.0)
end: (3.0, 4.0, 0.0)
tags: [example_edge]
metadata:
type: line
```
## Relationships with Other Geometries
- **Vertex**: Endpoints of edges
- **Wire**: Composed of multiple connected edges
- **Face**: Boundary defined by edges (via wires)
- **Solid**: Ultimately composed of faces formed by edges
## Notes
- Edge length is determined by its geometry and cannot be directly modified
- Circular edges are complete circles with identical start and end vertices
- Spline edge lengths are approximate values and may have precision errors
- Edge directionality may affect certain operations
- Tags and metadata do not affect edge geometry properties
- When retrieving vertices, for closed edges like circular edges, start and end vertices may be identical
+545
View File
@@ -0,0 +1,545 @@
# Face
## Overview
`Face` is the face class in the SimpleCAD API, representing 2D surface geometry. A face is bounded by one or more wires, including an outer boundary and possibly inner boundaries (holes). It wraps the OCP Face object and adds tagging functionality.
## Class Definition
```python
class Face(TaggedMixin):
"""面类,包装OCP的Face,添加标签功能"""
```
## Inheritance
- Inherits from `TaggedMixin`, providing tag and metadata functionality
## Usage
- Represent 2D surface areas
- Form the boundary of solids (Solid)
- Define cross-sections for sweep, extrude, and other operations
- Calculate geometric properties such as area and normal vectors
## Constructor
### `__init__(wrapped)`
Initializes a face object.
**Parameters:**
- `wrapped` (OCP TopoDS_Face): A OCP face object
**Raises:**
- `ValueError`: When the input face object is invalid
**Example:**
```python
from simplecadapi import (
make_rectangle_rface,
make_circle_rface,
make_face_from_wire_rface,
make_rectangle_rwire
)
# 通过 SimpleCAD 函数创建面
rectangle = make_rectangle_rface(width=5, height=3)
circle = make_circle_rface(center=(0, 0, 0), radius=2.0)
# 从线创建面
wire = make_rectangle_rwire(width=4, height=4)
face_from_wire = make_face_from_wire_rface(wire)
```
## Main Properties
- `wrapped`: The underlying OCP face object
- `_tags`: Tag set (inherited from TaggedMixin)
- `_metadata`: Metadata dictionary (inherited from TaggedMixin)
## Common Methods
### `get_area()`
Get the area of the face.
**Returns:**
- `float`: The area of the face
**Raises:**
- `ValueError`: When area retrieval fails
**Example:**
```python
from simplecadapi import make_rectangle_rface, make_circle_rface
import math
# 矩形面
rectangle = make_rectangle_rface(width=5, height=3)
rect_area = rectangle.get_area()
print(f"矩形面积: {rect_area}") # 15.0
# 圆形面
circle = make_circle_rface(center=(0, 0, 0), radius=2.0)
circle_area = circle.get_area()
expected_area = math.pi * 2.0 * 2.0
print(f"圆形面积: {circle_area:.3f}, 期望: {expected_area:.3f}")
```
### `get_normal_at(u, v)`
Get the normal vector of the face at the specified parameter position.
**Parameters:**
- `u` (float, optional): U parameter, default 0.5
- `v` (float, optional): V parameter, default 0.5
**Returns:**
- `simplecadapi.core.Vec3`: Normal vector
**Raises:**
- `ValueError`: When normal vector retrieval fails
**Example:**
```python
from simplecadapi import make_rectangle_rface
rectangle = make_rectangle_rface(width=5, height=3)
normal = rectangle.get_normal_at()
print(f"法向量: ({normal.x:.3f}, {normal.y:.3f}, {normal.z:.3f})")
```
### `get_outer_wire()`
Get the outer boundary wire of the face.
**Returns:**
- `Wire`: Outer boundary wire object
**Raises:**
- `ValueError`: When outer boundary wire retrieval fails
**Example:**
```python
from simplecadapi import make_rectangle_rface
rectangle = make_rectangle_rface(width=5, height=3)
outer_wire = rectangle.get_outer_wire()
edges = outer_wire.get_edges()
print(f"外边界由 {len(edges)} 条边组成")
```
### Tagging and Metadata
Use the functional public API `apply_tag(shape, tag)` and `list_tags(shape)` for tags. Use `set_metadata(key, value)` and `get_metadata(key, default=None)` for structured metadata.
## Usage Examples
### Creating Different Types of Faces
```python
from simplecadapi import (
make_rectangle_rface,
make_circle_rface,
make_face_from_wire_rface,
make_polyline_rwire
)
# 矩形面
rectangle = make_rectangle_rface(width=10, height=6)
apply_tag(rectangle, "rectangle")
apply_tag(rectangle, "quadrilateral")
# 圆形面
circle = make_circle_rface(center=(0, 0, 0), radius=3.0)
apply_tag(circle, "circle")
apply_tag(circle, "curved")
# 复杂多边形面
points = [
(0, 0, 0), (4, 0, 0), (4, 3, 0), (2, 5, 0), (0, 3, 0), (0, 0, 0)
]
polygon_wire = make_polyline_rwire(points=points)
polygon = make_face_from_wire_rface(polygon_wire)
apply_tag(polygon, "polygon")
apply_tag(polygon, "complex")
# 分析面的属性
faces = [rectangle, circle, polygon]
for face in faces:
area = face.get_area()
normal = face.get_normal_at()
outer_wire = face.get_outer_wire()
edges = outer_wire.get_edges()
tags = list_tags(face)
print(f"面类型: {tags}")
print(f" 面积: {area:.3f}")
print(f" 法向量: ({normal.x:.3f}, {normal.y:.3f}, {normal.z:.3f})")
print(f" 边数: {len(edges)}")
print()
```
### Geometric Analysis of Faces
```python
from simplecadapi import make_rectangle_rface, make_circle_rface
import math
def analyze_face_geometry():
"""分析面的几何属性"""
# 创建不同尺寸的矩形
rectangles = [
make_rectangle_rface(width=2, height=3),
make_rectangle_rface(width=4, height=4),
make_rectangle_rface(width=6, height=2)
]
# 创建不同半径的圆
circles = [
make_circle_rface(center=(0, 0, 0), radius=1.0),
make_circle_rface(center=(0, 0, 0), radius=2.0),
make_circle_rface(center=(0, 0, 0), radius=3.0)
]
# 分析矩形
for i, rect in enumerate(rectangles):
area = rect.get_area()
outer_wire = rect.get_outer_wire()
edges = outer_wire.get_edges()
# 计算周长
perimeter = sum(edge.get_length() for edge in edges)
# 计算长宽比
lengths = [edge.get_length() for edge in edges]
lengths.sort()
aspect_ratio = lengths[1] / lengths[0] if lengths[0] > 0 else 1.0
apply_tag(rect, f"rectangle_{i}")
rect.set_metadata("area", area)
rect.set_metadata("perimeter", perimeter)
rect.set_metadata("aspect_ratio", aspect_ratio)
if aspect_ratio == 1.0:
apply_tag(rect, "square")
elif aspect_ratio > 2.0:
apply_tag(rect, "elongated")
print(f"矩形 {i}: 面积={area:.3f}, 周长={perimeter:.3f}, 长宽比={aspect_ratio:.3f}")
# 分析圆形
for i, circle in enumerate(circles):
area = circle.get_area()
outer_wire = circle.get_outer_wire()
edges = outer_wire.get_edges()
# 计算周长(圆周长)
perimeter = sum(edge.get_length() for edge in edges)
# 从面积计算半径
radius_from_area = math.sqrt(area / math.pi)
# 从周长计算半径
radius_from_perimeter = perimeter / (2 * math.pi)
apply_tag(circle, f"circle_{i}")
circle.set_metadata("area", area)
circle.set_metadata("perimeter", perimeter)
circle.set_metadata("radius_from_area", radius_from_area)
circle.set_metadata("radius_from_perimeter", radius_from_perimeter)
if radius_from_area < 1.5:
apply_tag(circle, "small")
elif radius_from_area > 2.5:
apply_tag(circle, "large")
else:
apply_tag(circle, "medium")
print(f"圆形 {i}: 面积={area:.3f}, 周长={perimeter:.3f}, 半径={radius_from_area:.3f}")
analyze_face_geometry()
```
### Faces with Holes
```python
from simplecadapi import (
make_rectangle_rface,
make_circle_rface,
make_face_from_wire_rface,
make_rectangle_rwire,
make_circle_rwire
)
def create_face_with_holes():
"""创建带孔的面(概念示例)"""
# 创建外边界
outer_boundary = make_rectangle_rwire(width=10, height=8)
# 创建内边界(孔)
hole1 = make_circle_rwire(center=(3, 2, 0), radius=1.0)
hole2 = make_circle_rwire(center=(7, 6, 0), radius=1.5)
# 注意:SimpleCAD 当前版本可能不直接支持多边界面
# 这里展示概念和标签使用
# 主面
main_face = make_rectangle_rface(width=10, height=8)
apply_tag(main_face, "main_surface")
apply_tag(main_face, "with_holes")
# 孔面(用于布尔运算)
hole_face1 = make_circle_rface(center=(3, 2, 0), radius=1.0)
apply_tag(hole_face1, "hole")
apply_tag(hole_face1, "circular")
hole_face1.set_metadata("hole_id", 1)
hole_face1.set_metadata("center", (3, 2, 0))
hole_face1.set_metadata("radius", 1.0)
hole_face2 = make_circle_rface(center=(7, 6, 0), radius=1.5)
apply_tag(hole_face2, "hole")
apply_tag(hole_face2, "circular")
hole_face2.set_metadata("hole_id", 2)
hole_face2.set_metadata("center", (7, 6, 0))
hole_face2.set_metadata("radius", 1.5)
# 计算有效面积
main_area = main_face.get_area()
hole1_area = hole_face1.get_area()
hole2_area = hole_face2.get_area()
effective_area = main_area - hole1_area - hole2_area
main_face.set_metadata("total_area", main_area)
main_face.set_metadata("hole_area", hole1_area + hole2_area)
main_face.set_metadata("effective_area", effective_area)
print(f"主面面积: {main_area:.3f}")
print(f"孔面积总和: {hole1_area + hole2_area:.3f}")
print(f"有效面积: {effective_area:.3f}")
return main_face, [hole_face1, hole_face2]
main_face, holes = create_face_with_holes()
```
### Face Transformation and Operations
```python
from simplecadapi import (
make_rectangle_rface,
translate_shape,
rotate_shape
)
def transform_faces():
"""变换面的操作"""
# 创建基础面
base_face = make_rectangle_rface(width=4, height=3)
apply_tag(base_face, "base")
apply_tag(base_face, "original")
# 应用变换
translated_face = translate_shape(base_face, offset=(6, 0, 0))
apply_tag(translated_face, "translated")
rotated_face = rotate_shape(base_face, axis=(0, 0, 1), angle=45)
apply_tag(rotated_face, "rotated")
elevated_face = translate_shape(base_face, offset=(0, 0, 2))
apply_tag(elevated_face, "elevated")
# 收集所有面
all_faces = [base_face, translated_face, rotated_face, elevated_face]
# 分析变换结果
for face in all_faces:
area = face.get_area()
normal = face.get_normal_at()
outer_wire = face.get_outer_wire()
edges = outer_wire.get_edges()
# 计算边界框
all_coords = []
for edge in edges:
start_coords = edge.get_start_vertex().get_coordinates()
end_coords = edge.get_end_vertex().get_coordinates()
all_coords.extend([start_coords, end_coords])
if all_coords:
min_x = min(coord[0] for coord in all_coords)
max_x = max(coord[0] for coord in all_coords)
min_y = min(coord[1] for coord in all_coords)
max_y = max(coord[1] for coord in all_coords)
min_z = min(coord[2] for coord in all_coords)
max_z = max(coord[2] for coord in all_coords)
face.set_metadata("bbox_min", (min_x, min_y, min_z))
face.set_metadata("bbox_max", (max_x, max_y, max_z))
face.set_metadata("area", area)
face.set_metadata("normal", (normal.x, normal.y, normal.z))
print(f"面标签: {list_tags(face)}")
print(f" 面积: {area:.3f}")
print(f" 法向量: ({normal.x:.3f}, {normal.y:.3f}, {normal.z:.3f})")
if face.get_metadata("bbox_min"):
print(f" 边界框: {face.get_metadata('bbox_min')}{face.get_metadata('bbox_max')}")
print()
transform_faces()
```
### Face Classification and Filtering
```python
from simplecadapi import make_rectangle_rface, make_circle_rface
def classify_faces():
"""分类和筛选面"""
# 创建不同类型的面
faces = []
# 小矩形
small_rects = [
make_rectangle_rface(width=1, height=1),
make_rectangle_rface(width=2, height=1),
make_rectangle_rface(width=1, height=2)
]
# 大矩形
large_rects = [
make_rectangle_rface(width=5, height=4),
make_rectangle_rface(width=6, height=3),
make_rectangle_rface(width=4, height=6)
]
# 圆形
circles = [
make_circle_rface(center=(0, 0, 0), radius=1.0),
make_circle_rface(center=(0, 0, 0), radius=2.0),
make_circle_rface(center=(0, 0, 0), radius=3.0)
]
# 标记面
for i, face in enumerate(small_rects):
apply_tag(face, "rectangle")
apply_tag(face, "small")
face.set_metadata("size_category", "small")
face.set_metadata("shape_type", "rectangle")
faces.append(face)
for i, face in enumerate(large_rects):
apply_tag(face, "rectangle")
apply_tag(face, "large")
face.set_metadata("size_category", "large")
face.set_metadata("shape_type", "rectangle")
faces.append(face)
for i, face in enumerate(circles):
apply_tag(face, "circle")
area = face.get_area()
if area < 10:
apply_tag(face, "small")
face.set_metadata("size_category", "small")
elif area > 20:
apply_tag(face, "large")
face.set_metadata("size_category", "large")
else:
apply_tag(face, "medium")
face.set_metadata("size_category", "medium")
face.set_metadata("shape_type", "circle")
faces.append(face)
# 分类统计
rectangles = [f for f in faces if "rectangle" in list_tags(f)]
circles = [f for f in faces if "circle" in list_tags(f)]
small_faces = [f for f in faces if "small" in list_tags(f)]
large_faces = [f for f in faces if "large" in list_tags(f)]
print(f"总面数: {len(faces)}")
print(f"矩形面: {len(rectangles)}")
print(f"圆形面: {len(circles)}")
print(f"小面: {len(small_faces)}")
print(f"大面: {len(large_faces)}")
# 计算统计信息
total_area = sum(f.get_area() for f in faces)
avg_area = total_area / len(faces)
print(f"总面积: {total_area:.3f}")
print(f"平均面积: {avg_area:.3f}")
return faces
classified_faces = classify_faces()
```
## String Representation
```python
from simplecadapi import make_rectangle_rface
face = make_rectangle_rface(width=5, height=3)
apply_tag(face, "example_face")
face.set_metadata("material", "steel")
print(face)
```
Output:
```
Face:
area: 15.000
normal: [0.000, 0.000, 1.000]
outer_wire:
Wire:
edge_count: 4
closed: True
edges:
edge_0:
length: 5.000
vertices:
start: (0.0, 0.0, 0.0)
end: (5.0, 0.0, 0.0)
edge_1:
length: 3.000
vertices:
start: (5.0, 0.0, 0.0)
end: (5.0, 3.0, 0.0)
edge_2:
length: 5.000
vertices:
start: (5.0, 3.0, 0.0)
end: (0.0, 3.0, 0.0)
edge_3:
length: 3.000
vertices:
start: (0.0, 3.0, 0.0)
end: (0.0, 0.0, 0.0)
tags: [example_face]
metadata:
material: steel
```
## Relationships with Other Geometry
- **Wire (Wire)**: Boundary of the face
- **Edge (Edge)**: Indirectly associated through wires
- **Solid (Solid)**: Faces form the surfaces of a solid
- **Shell (Shell)**: A collection of surfaces composed of multiple faces
## Notes
- Faces must be closed, bounded by closed wires
- The face normal direction follows the right-hand rule
- Area calculation includes all regions bounded by the boundary
- Faces with holes require special treatment (outer boundary + inner boundary)
- Face orientation affects subsequent solid operations
- Complex faces may have self-intersection or degenerate cases
- The u, v parameter range is typically [0, 1]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,448 @@
# Part and Assembly Development Plan
This document records the SimpleCADAPI Part and Assembly direction after
reviewing the current topology/geometry model, operation graph philosophy,
sketch constraint implementation, QL surface, and the Boiling Lake completeness
principle.
The MVP APIs described in the Part, Material, Placement, Assembly, and projection
sections are implemented as public APIs. Connector and mate-solver concepts are
future work and remain explicitly marked as out of scope.
## Completeness Principle - Boil The Lake
The Part/Assembly MVP is treated as a boilable lake, not an ocean. When choosing
between a complete implementation and a shortcut that saves modest effort, the
complete implementation is the recommended path.
This principle applies to:
- API validation.
- Error messages and error paths.
- Graph recording.
- Replay and strict replay behavior.
- Model JSON serialization.
- Translator behavior.
- Examples.
- Generated API docs.
- Negative and edge-case tests.
Do not defer tests, docs, validation, or replay support to save a small amount of
code. If the remaining work is inside the Part/Assembly MVP lake, boil it in the
same implementation pass.
The ocean boundary is different: full assembly mate solving, full physical
simulation, multi-body part authoring, editable feature trees, or rewriting the
kernel around XCAF/OCAF are out of scope for this MVP and should be handled as
separate future lakes.
## Current Design Baseline
SimpleCADAPI is currently centered on two stable ideas:
- Topology/geometry values: `Vertex`, `Edge`, `Wire`, `Face`, `Solid`, and `Compound`.
- Typed functional operations: `make_*`, `extrude_rsolid`, `cut_rsolid`, `fillet_rsolid`, `loft_rsolid`, transforms, selectors, serialization, and translators.
Sketches now form a symbolic construction layer:
- A `Sketch` contains symbolic entities and declarative constraints.
- A `Sketch` is not BREP geometry by itself.
- `make_wire_from_sketch_rwire(...)` and `make_face_from_sketch_rface(...)` promote solved sketch geometry into the topology/geometry layer.
- Solve evidence belongs to promotion metadata and replay validation, not to a standalone modeling object.
This same philosophy should guide Part and Assembly support:
- A `Feature` is not a public object.
- Feature-like behavior remains a typed operation graph node.
- A `Body` is not a new public abstraction for the MVP.
- In the MVP, body-level geometry is exactly a `Solid`.
## Layer Model
```text
Sketch
symbolic constrained construction
becomes topology/geometry only through explicit promotion
Shape / TopoGeo
Vertex / Edge / Wire / Face / Solid / Compound
Solid is the MVP body-level geometry
Operation
typed functional graph node
examples: extrude, cut, union, fillet, chamfer, loft, transform
Part
semantic wrapper over exactly one Solid
owns part-local coordinates, product identity, and assigned material
Assembly
product structure over Part or subassembly component instances
owns instance placement and product tree identity
```
## Non-Goals For The MVP
The following remain intentionally out of the MVP lake:
- Multi-body parts.
- Public `Body` objects.
- Public `Feature` objects.
- Physical properties APIs.
- Generic part reference APIs.
- Assembly mate/constraint solver.
- Connector/datums for assembly constraints.
- Automatically treating arbitrary `Solid` values as `Part` values inside assemblies.
These are not all permanently rejected. They are deferred because the first
boilable lake is explicit single-body parts, materials, component instances,
placement, serialization, and export/projection behavior.
## Coordinate Model
For the MVP, a `Part` has one correct coordinate rule:
```text
Part-local coordinates = the wrapped Solid's modeling coordinates.
```
If a user wants a different part origin, the correct approach is to build the
`Solid` in that coordinate system before wrapping it as a `Part`.
Assembly component placement maps child-local coordinates into parent assembly
coordinates:
```text
p_assembly = T_component * p_part
```
Nested assembly placement composes transforms:
```text
p_root = T_parent_component * T_child_component * p_part
```
Moving a part inside an assembly must not transform the part's internal solid.
Only the component placement changes.
## Canonical Placement Representation
Only one public placement representation should be exposed in the MVP:
```python
make_placement_rplacement(
origin: tuple[float, float, float],
*,
x_axis: tuple[float, float, float] = (1.0, 0.0, 0.0),
y_axis: tuple[float, float, float] = (0.0, 1.0, 0.0),
) -> Placement
```
Rules:
- `origin` is the child origin expressed in parent coordinates.
- `x_axis` and `y_axis` define the child basis expressed in parent coordinates.
- Axes are normalized by the implementation.
- Axes must be non-zero and orthogonal within a documented tolerance.
- The frame must be right-handed.
- `z_axis = x_axis cross y_axis`.
- Public placement APIs do not also accept Euler angles, quaternions, or axis-angle forms.
Identity placement:
```python
identity_placement_rplacement() -> Placement
```
## Material API
Material has a single correct entry point and is not passed to
`make_part_rpart(...)`.
```python
make_material_rmaterial(
material_id: str,
*,
name: str | None = None,
density: float | None = None,
density_unit: str | None = None,
color: tuple[float, float, float] | None = None,
) -> Material
```
Validation requirements:
- `material_id` must be a stable non-empty identifier.
- `density`, when provided, must be finite and positive.
- `density_unit`, when provided, must be explicit.
- `color`, when provided, must be a 3-tuple of finite values in `[0.0, 1.0]`.
- Unknown physical semantics do not get hidden in generic physical property APIs.
Material assignment:
```python
assign_material_rpart(part: Part, material: Material) -> Part
```
This keeps one correct workflow:
```text
make material -> make part -> assign material
```
## Part API MVP
The implemented MVP supports single-body parts only.
```python
make_part_rpart(
part_id: str,
body: Solid,
*,
name: str | None = None,
) -> Part
```
Semantics:
- `part_id` is stable product identity.
- `part_id` replaces ambiguous names such as `part_number`.
- `body` must be a `Solid`.
- `Compound` is not accepted in the single-body MVP.
- Material is assigned only through `assign_material_rpart(...)`.
- Physical properties are not part of the MVP.
- The part-local coordinate system is the body's modeling coordinate system.
Validation requirements:
- Reject empty `part_id`.
- Reject duplicate part IDs inside one model context where uniqueness is required.
- Reject non-`Solid` bodies.
- Preserve graph identity and replay behavior.
- Preserve part identity in model JSON.
## Assembly API MVP
An `Assembly` contains component instances. A component references a `Part` or a
subassembly and owns an instance placement.
```python
make_assembly_rassembly(
assembly_id: str,
*,
name: str | None = None,
) -> Assembly
```
```python
add_component_rassembly(
assembly: Assembly,
item: Part | Assembly,
*,
component_id: str,
placement: Placement,
name: str | None = None,
) -> Assembly
```
```python
place_component_rassembly(
assembly: Assembly,
component_id: str,
placement: Placement,
) -> Assembly
```
Semantics:
- `component_id` is stable only within its parent assembly.
- The same `Part` can be instantiated multiple times.
- Moving a component changes placement only; it does not transform the referenced `Part` body.
- Subassemblies are allowed as component items after the base Part path is complete.
- Assembly is not a `Compound`.
Validation requirements:
- Reject empty `assembly_id`.
- Reject empty `component_id`.
- Reject duplicate `component_id` in the same assembly.
- Reject cycles in subassembly references.
- Reject invalid placements.
- Reject arbitrary `Solid` items. Users must explicitly wrap solids as parts.
## Assembly Geometry Projection
Assembly semantic structure must not be flattened implicitly.
For preview, export fallback, bounding boxes, and geometry-only workflows, expose
an explicit projection operation:
```python
make_compound_from_assembly_rcompound(assembly: Assembly) -> Compound
```
Semantics:
- Applies component placements to referenced part bodies.
- Produces a flattened geometry projection.
- Does not replace the assembly product tree.
- Records enough graph evidence to replay the projection deterministically.
## FreeCAD Translation
The FreeCAD translator preserves the product tree instead of exporting only a
fixed geometry result.
Current FCStd mapping:
- `Part` is emitted as an `App::Part` containing the wrapped body object.
- `Assembly` is emitted as a native `Assembly::AssemblyObject`.
- Part components are emitted as `App::Link` objects under the owning assembly.
- Subassembly components are emitted as `Assembly::AssemblyLink` objects under the owning assembly.
- Component placements are written to the link placement.
- Material assignment is stored on the part container as `SimpleCADMaterial`.
- `make_compound_from_assembly_rcompound(...)` still emits an explicit flattened projection for geometry workflows, but the projection is hidden when it is the result leaf so the visible FCStd result remains the editable assembly tree.
Assembly constraints, mates, solving, and connector/datum references are still
out of scope for the MVP. The FreeCAD output is therefore a placed product
structure, not a solved constraint model.
## Part References And Connectors
The MVP does not include a generic part reference API.
The rejected MVP shape is:
```python
add_part_reference_rpart(part, name, selection)
```
Reasons:
- It mixes product semantics with raw topology selection.
- It does not clarify whether the selected entity is a face, edge, axis, point, datum, or connector.
- Topology selections alone are not the right abstraction for future assembly constraints.
- Assembly constraints are not in the MVP, so generic references would be premature.
When assembly constraints are introduced later, the preferred direction is an
explicit connector/datum interface, not a generic reference bag:
```python
add_connector_rpart(
part: Part,
connector_id: str,
placement: Placement,
) -> Part
```
Connectors are part-local coordinate frames. Future constraints can refer to
`(component_id, connector_id)` instead of raw topology.
This is a separate lake and should not be mixed into the single-body
Part/Assembly MVP.
## Current QL Surface Used In Examples
The current QL APIs that are safe to use in examples include:
```python
ql.faces()
ql.edges()
ql.tag("face.top")
ql.prop("geom.normal.z", ">", 0.9)
ql.key("geom.center.z")
ql.center_axis("z")
selector.take(1).exactly(1).resolve(shape)
selector.boundary("wire").boundary("edge")
```
Do not document fake selectors such as `select_cylinder_axis(...)` unless those
APIs are actually implemented.
## Example: Hydraulic Rod Assembly
`examples/10_part_assembly.py` builds a hydraulic rod/cylinder assembly that uses
implemented geometry, QL, Part, Material, Placement, Assembly, projection, STEP,
and FCStd translation APIs.
The example intentionally keeps product structure separate from geometry
projection:
- The outer sleeve is one single-body `Part` with a barrel, gland flange, bolt-hole details, rear eye, and pin hole.
- The inner piston rod is another single-body `Part` with piston lands, a seal groove, chrome rod, rod-eye neck, and rod-eye pin hole.
- The final `Assembly` instantiates both parts with component placement.
- `make_compound_from_assembly_rcompound(...)` produces the flattened preview/STEP projection.
- `translate_model_json_to_fcstd(...)` writes a native FreeCAD Assembly Workbench document where the assembly tree remains visible and editable.
Run it from the source checkout:
```bash
uv run python examples/10_part_assembly.py
```
## Boiling Lake Implementation Plan
The MVP is a boilable lake. It is not an ocean-scale rewrite.
Complete means:
- Public dataclasses/types for `Material`, `Placement`, `Part`, `Component`, and `Assembly`.
- Complete validation and error paths.
- Functional APIs with typed return suffixes.
- Graph recording and replay.
- Model JSON serialization.
- Strict replay behavior.
- Deterministic component ordering.
- Explicit assembly-to-compound projection.
- STEP/FCStd translator behavior that preserves identity when supported and clearly documents fallback projection behavior.
- API docs generated and reviewed.
- Core design docs updated.
- Examples using only implemented public APIs.
- Unit tests for each API and edge case.
- Translator/regression tests for identity and placement.
- Negative tests for invalid IDs, duplicate components, invalid placements, non-solid part bodies, and assembly cycles.
Suggested implementation order:
1. Add core immutable data types and validation helpers.
2. Add material and placement APIs with exhaustive tests.
3. Add single-body Part APIs with graph/model JSON tests.
4. Add Assembly and Component APIs with placement and duplicate/cycle validation.
5. Add `make_compound_from_assembly_rcompound(...)` projection with replay tests.
6. Add translator support for Part/Assembly identity and projection fallback.
7. Add example script and docs.
8. Run full tests, compile, whitespace checks, examples, and FreeCAD translator regressions.
## Effort Estimate
Both human-team and Codex-scale estimates are shown per the Boiling Lake rule.
| Task | Human team | Codex | Compression |
| --- | --- | --- | --- |
| Core dataclasses and validation | 2 days | 15 min | ~100x |
| Material and placement APIs/tests | 1 day | 15 min | ~50x |
| Part APIs, graph, replay, model JSON | 3 days | 30 min | ~30x |
| Assembly/component APIs and validation | 3 days | 30 min | ~30x |
| Projection to Compound and tests | 1 day | 15 min | ~50x |
| FreeCAD/STEP translator support | 1 week | 30-60 min | ~20-30x |
| Docs, examples, generated API refresh | 1 day | 15 min | ~50x |
| Architecture review and edge-case pass | 2 days | 4 hours | ~5x |
Recommended scope is the complete MVP lake, not a shortcut that skips tests,
validation, docs, or translator behavior.
## Out Of Scope Oceans
The following are ocean-scale for this phase and should not be smuggled into the
MVP:
- Full assembly mate solver.
- Constraint-driven connector/datum system.
- Full physical simulation or mass-property subsystem.
- Multi-body Part authoring environment.
- Editable feature tree or feature suppression/reordering.
- Rewriting the kernel around XCAF/OCAF directly.
- Adding features to OCP/OCCT itself.
These can become future lakes once the single-body Part and explicit-placement
Assembly foundation is complete.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,423 @@
# SimpleCADAPI 2.0 需求与验收文档
## 文档目的
本文档是 `docs/core/rearchitecture_2_0.md` 的配套执行文档,用于把 2.0 的架构方向压缩成:
- 明确的功能需求
- 明确的非目标
- 可验证的验收标准
- 可执行的测试映射
本文档面向实现与测试,不替代架构文档。
## 适用范围
本文档覆盖 `SimpleCADAPI 2.0` 的第一阶段和第二阶段建设要求,重点包括:
- API 连续性
- 表达式图
- OCP 内核迁移方向
- 几何值模型
- 历史/拓扑变化模型
- 装配约束整合
- 模型序列化方向
## 硬约束
以下要求已经确定,后续实现不得违反:
1. 保留纯函数风格 API。
2. 保留 `Vertex -> Edge -> Wire -> Face -> Solid` 作为公开几何对象主线。
3. 保留 `*_rvertex``*_redge``*_rwire``*_rface``*_rsolid` 命名风格。
4. 尽量保证已有脚本只需小改动即可迁移。
5. 2.0 第一阶段全部按无单位标量处理。
6. 2.0 核心实现以 `OCP` 为主,不再以 `OCP` 作为内核前提。
## 非目标
以下内容不属于 2.0 第一阶段必做项:
1. 单位系统。
2. 通用符号代数系统。
3. 完整 2D sketch constraint solver。
4. 所有 evolve/macro 函数进入稳定 IR。
5. 跨所有外部 CAD 软件的原生参数树兼容导出。
## 需求列表
### A. API 连续性
#### REQ-API-001
2.0 必须继续公开以下几何主类型:
- `Vertex`
- `Edge`
- `Wire`
- `Face`
- `Solid`
#### REQ-API-002
2.0 必须继续公开核心建模函数的 `r` 风格命名。例如:
- `make_point_rvertex`
- `make_line_redge`
- `make_rectangle_rface`
- `make_box_rsolid`
- `extrude_rsolid`
- `revolve_rsolid`
- `fillet_rsolid`
- `chamfer_rsolid`
- `shell_rsolid`
- `cut_rsolid`
- `union_rsolid`
- `intersect_rsolid`
#### REQ-API-003
2.0 必须保留“常量参数直接建模”的调用体验,不允许把表达式系统变成基本建模的前置条件。
允许:
```python
box = scad.make_box_rsolid(10, 20, 30)
```
不允许要求用户必须写成:
```python
box = scad.make_box_rsolid(scad.const(10), scad.const(20), scad.const(30))
```
#### REQ-API-004
2.0 的变换与特征操作必须继续保持闭包式返回:
- `translate_shape(solid) -> Solid`
- `rotate_shape(solid) -> Solid`
- `extrude_rsolid(face) -> Solid`
- `fillet_rsolid(solid) -> Solid`
### B. 表达式图
#### REQ-EXPR-001
2.0 必须提供显式变量创建入口:
```python
r = scad.var("r", 10.0)
```
变量名必须由用户显式提供。
#### REQ-EXPR-002
2.0 必须支持在变量上使用标准算术运算构造表达式图,包括:
- `+`
- `-`
- `*`
- `/`
- `**`
- unary `-`
#### REQ-EXPR-003
2.0 必须允许表达式对象直接作为公开建模 API 的参数输入,而不要求额外包装。
允许:
```python
r = scad.var("r", 10.0)
face = scad.make_circle_rface((0, 0, 0), r)
solid = scad.extrude_rsolid(face, (0, 0, 1), r * 2)
```
#### REQ-EXPR-004
所有公开建模 API 在入口处必须统一做参数 canonicalization
- `int/float -> Const`
- `Var -> Var`
- `Expr -> Expr`
- 向量/点参数逐项 lift
#### REQ-EXPR-005
表达式图必须独立于操作图存在,不能简单把表达式树反复内嵌在每个操作节点中。
最小要求:
- 存在 `ExpressionGraph`
- `OperationNode` 通过引用关系绑定参数表达式
#### REQ-EXPR-006
2.0 第一阶段全部按无单位标量处理。表达式系统不引入单位类型。
### C. 几何值模型
#### REQ-GEO-001
2.0 公开几何对象必须继续保持值对象风格,而不是对外暴露 scene graph 节点风格。
#### REQ-GEO-002
2.0 应引入 `Sketch` 作为新增一等对象,但 `Sketch` 不能替代 `Vertex/Edge/Wire/Face/Solid` 主线。
#### REQ-GEO-003
每个公开几何对象都必须可查询空间信息,但对象空间信息的内部来源允许不同:
- 直接拥有 placement/frame
- 从上游 owner/instance 推导
这属于内部实现约束,不作为用户侧公开类型分裂。
### D. OCP 内核
#### REQ-KERNEL-001
2.0 的核心 evaluator 必须以 `OCP` 为主实现,而不是继续依赖 `cadquery` 作为核心构造层。
#### REQ-KERNEL-002
2.0 的 `Vertex/Edge/Wire/Face/Solid` 内部实现应逐步迁移为 OCP thin wrapper。
#### REQ-KERNEL-003
如保留 `OCP`,只能作为过渡期兼容工具或开发辅助工具,而不能作为 2.0 核心前提。
### E. 基础操作集
#### REQ-OPS-001
2.0 第一阶段稳定基础操作集为:
- `line`
- `arc`
- `bspline`
- `sketch`
- `extrude`
- `revolve`
- `fillet`
- `chamfer`
- `shell`
- `cut`
- `union`
- `intersection`
#### REQ-OPS-002
第一阶段不要求把复杂宏、evolve case 和特殊建模脚本纳入核心稳定 IR。
### F. 历史与变化记录
#### REQ-HIST-001
2.0 不能只记录最终几何结果,必须记录可 replay 的操作图。
#### REQ-HIST-002
2.0 必须区分两层变化:
- `SemanticDelta`
- `TopoDelta`
#### REQ-HIST-003
`TopoDelta` 至少必须能表达:
- `created`
- `preserved`
- `modified`
- `deleted`
#### REQ-HIST-004
以下基础操作应具备可靠的历史/变化记录能力:
- `extrude`
- `revolve`
- `fillet`
- `chamfer`
- `shell`
- `cut`
- `union`
- `intersection`
#### REQ-HIST-005
对象引用体系必须至少同时支持:
- `SemanticRef`
- `TopoRef`
### G. 装配与约束
#### REQ-ASM-001
装配系统必须整合进统一模型 IR,而不是继续作为完全独立的旁路系统。
#### REQ-ASM-002
装配约束参数必须复用同一套表达式系统。
#### REQ-ASM-003
2.0 第一阶段装配重点仍是 rigid pose solving,不要求在 assembly solve 中直接改 part 内部拓扑。
### H. 序列化与导出
#### REQ-IO-001
2.0 必须以模型 IR 作为 canonical export,而不是 STEP/STL。
#### REQ-IO-002
canonical export 至少应包含:
- expression graph
- operation graph
- geometry registry
- optional assembly/constraint data
- semantic/topology deltas
#### REQ-IO-003
STEP/STL/BRep 继续保留,但只作为最终几何导出,不作为参数化事实来源。
## 验收标准
### 阶段 1 验收标准
满足以下条件即认为 Phase 1 完成:
1. `var()` API 已存在。
2. `Var` 可参与算术表达式。
3. 至少 2 个公开建模 API 能直接接受表达式参数。
4. 常量参数建模脚本仍可不加 wrapper 正常运行。
5. `ExpressionGraph` 可序列化和反序列化。
6. 至少有一组契约测试覆盖 API 连续性与表达式入口。
### 阶段 2 验收标准
满足以下条件即认为 Phase 2 完成:
1. `Vertex/Edge/Wire/Face/Solid` 至少一部分已切换到 OCP thin wrapper。
2. `extrude``revolve` 具备表达式驱动与历史记录。
3. `TopoDelta` 对基础 feature 可用。
4. replay 可以从模型 IR 重新生成结果。
### 阶段 3 验收标准
满足以下条件即认为 Phase 3 完成:
1. `cut/union/intersection` 具备稳定变化记录。
2. `fillet/chamfer/shell` 具备稳定变化记录。
3. assembly / constraint public surface 暂时移除,避免在重做前冻结旧契约。
4. canonical JSON export 初步稳定。
### 最终状态验收标准
当以下条件全部满足时,认为当前 2.0 主线已经达到本轮冻结的“最终状态”:
1. `model.json` 是 canonical export,且可以不依赖 `.graph.json` 独立 replay。
2. `model.json` 顶层必须同时包含:
- `graph`
- `leaf_ids`
- `expression_graph`
- `frame_graph`
- `geometry_registry`
- `semantic_entity_registry`
- `sketch_profile_registry`
- `semantic_delta_log`
- `topology_delta_log`
- `canonical_contract`
3. `canonical_contract` 必须明确声明:
- `graph` 的角色是唯一真相源的 canonical low-level graph
- `leaf_ids` 是多输出场景的显式结果集
- replay 默认直接使用 `graph`
4. `graph` 必须限制在冻结的 canonical op set 内,不能泄漏 convenience 或 macro-only op;至少不允许出现:
- `make_box`
- `make_cylinder`
- `make_sphere`
- `make_cone`
- `make_circle_face`
- `make_rectangle_face`
- `make_circle_wire`
- `make_rectangle_wire`
- `make_polyline_wire`
- `make_segment_wire`
- `make_three_point_arc_wire`
- `make_angle_arc_wire`
- `make_spline_wire`
- `make_helix_wire`
- `linear_pattern`
- `radial_pattern`
- `helical_sweep`
5. `helical_sweep` 必须被视为组合宏,而不是基础 canonical 节点;最终 `graph` 中只能出现它 lower 后的基础链条。
6. `linear_pattern` / `radial_pattern` 必须 lower 为显式 transform 实例链,并通过 `leaf_ids` 明确哪些实例是最终结果集。
7. detail feature 的 canonical 参数必须保留 explicit selected refs
- `fillet/chamfer` 使用 `selected_edges`
- `shell` 使用 `selected_faces`
8. selection-ref schema 必须冻结,并至少要求每个 explicit topo ref 具备:
- `graph_id`
- `node_id`
- `output_slot`
- `kind`
- `topo_id`
- 可选 `selector_hint`
9. `replay_model_json()` 对 detail feature 的选择解析顺序必须固定为:
- explicit topo refs
- stable indices
- selection query
- selector hint
10. 上述最终状态必须由自动化测试锁定,并在 `uv run python -m unittest discover -s test` 下全量通过。
## 测试映射
### 现阶段已落地的契约测试
以下 requirement 将由新建的契约测试文件覆盖:
- `REQ-API-001`
- `REQ-API-002`
- `REQ-API-003`
- `REQ-API-004`
- `REQ-EXPR-001` 的入口占位检查
- `REQ-EXPR-003` 的未来占位检查
### 推荐测试文件
- `test/test_public_api_surface.py`
- `test/test_original_api_integration.py`
- `test/test_rearchitecture_2_0_contract.py`
### 推荐执行命令
项目使用 `uv`,因此测试执行统一建议使用:
```bash
uv run python -m unittest test/test_public_api_surface.py test/test_rearchitecture_2_0_contract.py
```
后续如果表达式系统开始落地,可追加:
```bash
uv run python -m unittest test/test_rearchitecture_2_0_contract.py
```
## 分支策略
2.0 重构建议在独立分支上推进,不在旧实现上做原地无序叠加。当前建议分支策略为:
- 使用独立重构分支承载 2.0 需求、测试和新内核工作
- 逐步让兼容层指向新内核
- 不把临时过渡性旧逻辑继续扩散到新模块中
## 结论
本文件将 2.0 的方向收束成了可以逐条验收的要求。后续实现应以 requirement ID 为基准推进,测试也应优先围绕 requirement ID 来补齐,而不是围绕临时实现细节写脆弱测试。
@@ -0,0 +1,62 @@
# Serialization and Replay Operation Guides
This directory documents how SimpleCADAPI serializes replayable modeling operations into the canonical low-level `model.json` operation graph.
The long-form schema reference remains [`../operation_graph_json_spec.md`](../operation_graph_json_spec.md). These files are more practical, operation-by-operation guides intended for people comparing source code with exported JSON.
## Recommended workflow
```python
import json
import simplecadapi as scad
with scad.GraphSession() as session:
body = scad.make_box_rsolid(10, 6, 2)
hole = scad.make_cylinder_rsolid(1, 4, bottom_face_center=(0, 0, -1))
result = scad.cut_rsolid(body, hole)
model_json = scad.export_model_json(session)
payload = json.loads(model_json)
rebuilt = scad.replay_model_json(model_json)
```
Inspect these fields:
- `payload["graph"]["nodes"]`: canonical operation nodes in topological order.
- `node["op"]`: stable replay operation name.
- `node["params"]`: numeric / JSON-compatible parameter snapshot.
- `node["param_exprs"]`: optional expression links into `expression_graph`.
- `node["inputs"]`: upstream node ids used by replay.
- `payload["leaf_ids"]`: explicit final result node ids.
- `payload["expression_graph"]`: expression DAG used by expression-backed parameters.
## Important rule: source API is not always graph API
Many user-facing functions are convenience APIs. During an active `GraphSession`, they lower to canonical low-level nodes:
| Source call | Serialized graph result |
| --- | --- |
| `make_box_rsolid(...)` | rectangle profile + `make_extrude_rsolid` |
| `make_cylinder_rsolid(...)` | circle face + `make_extrude_rsolid` |
| `make_sphere_rsolid(...)` | profile + `make_revolve_rsolid` |
| `make_cone_rsolid(...)` | profile + `make_revolve_rsolid` |
| `make_rectangle_rwire(...)` | line edges + `make_wire_from_edges_rwire` |
| `make_circle_rface(...)` | circle edge + wire + face |
| `make_polyline_rwire(...)` | line edges + wire |
| `linear_pattern_rsolidlist(...)` | explicit `make_translate_rshape` nodes |
| `radial_pattern_rsolidlist(...)` | explicit `make_rotate_rshape` nodes |
| `helical_sweep_rsolid(...)` | helix wire + profile face + `make_sweep_rsolid` |
## Guides
- [Primitive and profile operations](primitives-and-profiles.md)
- [Features, booleans, transforms, patterns, and selectors](features-booleans-transforms.md)
- [Expressions and replay behavior](expressions-and-replay.md)
## Example
See [`../../../examples/07_serialization_operation_tree.py`](../../../examples/07_serialization_operation_tree.py). It intentionally exercises every canonical core operation and writes:
- `examples/out/serialization_operation_tree.model.json`
- `examples/out/serialization_operation_tree.summary.md`
- `examples/out/serialization_operation_tree.step`
@@ -0,0 +1,145 @@
# Expressions and Replay Behavior
SimpleCADAPI stores expression-backed parameters in two places:
1. `node.params`: numeric / JSON-compatible snapshot used by simple replay
2. `node.param_exprs`: references into the top-level `expression_graph`
This lets consumers choose between:
- pure geometric replay using only the numeric snapshots
- parameter-aware import using `param_exprs + expression_graph`
## Source example
```python
import simplecadapi as scad
width = scad.var("width", 24.0, comment="plate width")
height = scad.var("height", 12.0, comment="plate height")
thickness = scad.var("thickness", 4.0, comment="plate thickness")
with scad.GraphSession() as session:
plate = scad.make_box_rsolid(width, height, thickness)
rib = scad.make_box_rsolid(width / 4.0, height, thickness * 2.0)
part = scad.union_rsolid(plate, rib)
model_json = scad.export_model_json(session)
```
Because `make_box_rsolid(...)` lowers to profile + extrude nodes, the expressions appear on the lowered line/profile/extrude nodes rather than on a `make_box` node.
## Node-level JSON shape
A node with expression-backed params may look like:
```json
{
"op": "make_extrude_rsolid",
"params": {
"direction": [0.0, 0.0, 1.0],
"distance": 4.0
},
"param_exprs": {
"distance": {"expr_id": "var_thickness"}
},
"inputs": ["node_for_profile"],
"output_count": 1
}
```
`params.distance` is the evaluated snapshot. `param_exprs.distance` says the value came from expression node `var_thickness`.
For tuple/list params, `param_exprs` mirrors the shape of the parameter and uses `null` where no expression is present:
```json
{
"params": {
"start": [-12.0, -6.0, 0.0],
"end": [12.0, -6.0, 0.0]
},
"param_exprs": {
"start": [{"expr_id": "expr_a"}, {"expr_id": "expr_b"}, null],
"end": [{"expr_id": "expr_c"}, {"expr_id": "expr_b"}, null]
}
}
```
## Top-level expression graph
`payload["expression_graph"]` contains expression nodes for variables, constants, and arithmetic operations. The exact ids are stable within one exported payload but should not be treated as human-authored names.
Consumers that want parameterization should:
1. Build an expression table from `expression_graph.nodes`.
2. For each operation node, inspect `param_exprs`.
3. Replace or annotate corresponding numeric `params` entries with expression references.
4. Keep numeric `params` as fallback evaluated values.
Consumers that only want geometry can ignore `param_exprs` and `expression_graph`.
## Replay policy in current implementation
`replay_model_json(model_json)` currently uses the canonical low-level `graph` and the numeric values in `node.params`.
That means replay is deterministic with respect to the exported snapshot. It does not currently re-solve expressions with changed variable values.
In practical terms:
```python
width = scad.var("width", 24.0)
with scad.GraphSession() as session:
box = scad.make_box_rsolid(width, 10, 2)
payload = scad.export_model_json(session)
rebuilt = scad.replay_model_json(payload)
```
Replay rebuilds using width `24.0`, because that is the value stored in `params`.
## Expression metadata is still important
Even though replay uses snapshots today, `param_exprs` and `expression_graph` are important for external tools:
- FreeCAD or CAD translators can reconstruct spreadsheet bindings.
- UI tools can display which dimensions are driven by variables.
- Future parametric replay can use the same expression references.
- Diffs can distinguish numeric constants from expression-derived values.
## Leaf ids and replayed outputs
The top-level `leaf_ids` field determines which node outputs are returned by replay:
```json
{
"leaf_ids": ["node_final", "node_auxiliary"]
}
```
Replay behavior:
1. Execute every graph node in topological order.
2. Store each node output by `node_id`.
3. Return outputs for `leaf_ids` in order.
If an example creates many independent showcase shapes, `leaf_ids` may contain many node ids. This is expected: the graph is not required to have a single final part.
## Unsupported / lossy expression cases
- Python callables are not serialized as expressions.
- Some discrete selector data, topology refs, and counts are intentionally treated as JSON data rather than scalar expressions.
## Practical inspection snippet
```python
import json
payload = json.loads(model_json)
for node in payload["graph"]["nodes"]:
if node.get("param_exprs"):
print(node["node_id"], node["op"])
print(" params:", node["params"])
print(" param_exprs:", node["param_exprs"])
```
Use this to show the source-to-JSON relationship for expression-backed dimensions.
@@ -0,0 +1,468 @@
# Features, Booleans, Transforms, Patterns, and Selection Serialization
This guide covers replayable feature operations, boolean operations, transforms, macro pattern lowering, and detail-feature selectors.
## Extrude
Source:
```python
profile = scad.make_rectangle_rface(4.0, 2.0)
solid = scad.extrude_rsolid(profile, (0, 0, 1), 3.0)
```
Serialized node:
```json
{
"op": "make_extrude_rsolid",
"params": {
"direction": [0.0, 0.0, 1.0],
"distance": 3.0
},
"inputs": ["node_for_profile"],
"output_count": 1
}
```
Replay effect:
1. Replay the input profile node, which must output a `Wire` or `Face`.
2. Call `extrude_rsolid(profile, direction, distance)`.
## Revolve
Source:
```python
profile = scad.make_polyline_rwire(
[(0.5, 0, 0), (1.2, 0, 0), (1.0, 0, 1.6), (0.5, 0, 1.6)],
closed=True,
)
solid = scad.revolve_rsolid(
profile,
axis=(0, 0, 1),
angle=360.0,
origin=(0, 0, 0),
)
```
Serialized node:
```json
{
"op": "make_revolve_rsolid",
"params": {
"axis": [0.0, 0.0, 1.0],
"angle": 360.0,
"origin": [0.0, 0.0, 0.0]
},
"inputs": ["node_for_profile"],
"output_count": 1
}
```
Replay effect: replays the profile and calls `revolve_rsolid(profile, axis, angle, origin)`.
## Loft
Source:
```python
a = scad.make_rectangle_rwire(2.0, 1.0, center=(0, 0, 0))
b = scad.make_rectangle_rwire(1.0, 0.5, center=(0, 0, 3))
solid = scad.loft_rsolid([a, b], ruled=True)
```
Serialized node:
```json
{
"op": "make_loft_rsolid",
"params": {
"profile_count": 2,
"ruled": true
},
"inputs": ["node_for_a", "node_for_b"],
"output_count": 1
}
```
Replay effect:
1. Replay all profile input nodes.
2. Call `loft_rsolid(profiles, ruled=...)`.
Profile geometry is recovered from `inputs`; only count/options are stored in `params`.
## Sweep
Source:
```python
profile = scad.make_circle_rface((0, 0, 0), 0.3, normal=(1, 0, 0))
path = scad.make_polyline_rwire([(0, 0, 0), (2, 0, 1), (4, 1, 1)])
solid = scad.sweep_rsolid(profile, path, is_frenet=False)
```
Serialized node:
```json
{
"op": "make_sweep_rsolid",
"params": {"is_frenet": false},
"inputs": ["node_for_profile_face", "node_for_path_wire"],
"output_count": 1
}
```
Replay effect:
1. Replay profile face from input 0.
2. Replay path wire from input 1.
3. Call `sweep_rsolid(profile, path, is_frenet=...)`.
## Helical sweep macro lowering
Source:
```python
profile = scad.make_rectangle_rwire(0.25, 0.18)
solid = scad.helical_sweep_rsolid(
profile,
pitch=0.7,
height=2.2,
radius=0.9,
)
```
Lowered serialized graph:
```text
profile wire
-> make_face_from_wire_rface
make_helix_redge
-> make_wire_from_edges_rwire
profile face + helix wire
-> make_sweep_rsolid(is_frenet=true)
```
There is no canonical `helical_sweep` node. Replay rebuilds the helix and sweeps along it.
## Translate
Source:
```python
moved = scad.translate_shape(shape, (1.0, 2.0, 0.0))
```
Serialized node:
```json
{
"op": "make_translate_rshape",
"params": {"vector": [1.0, 2.0, 0.0]},
"inputs": ["node_for_shape"],
"output_count": 1
}
```
Replay effect: replays input shape and calls `translate_shape(shape, vector)`.
## Rotate
Source:
```python
rotated = scad.rotate_shape(shape, 90.0, axis=(0, 0, 1), origin=(0, 0, 0))
```
Serialized node:
```json
{
"op": "make_rotate_rshape",
"params": {
"angle": 90.0,
"axis": [0.0, 0.0, 1.0],
"origin": [0.0, 0.0, 0.0]
},
"inputs": ["node_for_shape"],
"output_count": 1
}
```
Replay effect: replays input shape and calls `rotate_shape(shape, angle, axis, origin)`.
Note: `rotate_shape(shape, 0.0)` returns the original shape and does not record a node.
## Mirror
Source:
```python
mirrored = scad.mirror_shape(
shape,
plane_origin=(0, 0, 0),
plane_normal=(1, 0, 0),
)
```
Serialized node:
```json
{
"op": "make_mirror_rshape",
"params": {
"plane_origin": [0.0, 0.0, 0.0],
"plane_normal": [1.0, 0.0, 0.0]
},
"inputs": ["node_for_shape"],
"output_count": 1
}
```
Replay effect: replays input shape and calls `mirror_shape(shape, plane_origin, plane_normal)`.
## Boolean union
Source:
```python
a = scad.make_box_rsolid(3, 2, 1)
b = scad.make_box_rsolid(3, 2, 1, bottom_face_center=(1.5, 0, 0))
result = scad.union_rsolid(a, b)
```
Serialized node:
```json
{
"op": "make_union_rsolid",
"params": {
"input_count": 2,
"clean": true,
"glue": true,
"tol": 1e-7
},
"inputs": ["node_for_a", "node_for_b"],
"output_count": 1
}
```
Replay effect:
1. Replay all input solids.
2. Call `union_rsolid(all_solids)`.
Important: `union_rsolid` expects one connected solid result. If inputs remain disconnected, runtime and replay both raise an error instead of returning a compound.
## Boolean cut
Source:
```python
body = scad.make_box_rsolid(4, 4, 2)
tool = scad.make_cylinder_rsolid(0.8, 4, bottom_face_center=(0, 0, -1))
result = scad.cut_rsolid(body, tool)
```
Serialized node:
```json
{
"op": "make_cut_rsolid",
"params": {
"tool_count": 1,
"input_count": 2
},
"inputs": ["node_for_body", "node_for_tool"],
"output_count": 1
}
```
Replay effect:
1. Replay first input as the body.
2. Replay remaining inputs as tools.
3. Call `cut_rsolid(body, tools)`.
## Boolean intersection
Source:
```python
a = scad.make_box_rsolid(2, 2, 2)
b = scad.make_box_rsolid(2, 2, 2, bottom_face_center=(1, 0, 0))
result = scad.intersect_rsolid(a, b)
```
Serialized node:
```json
{
"op": "make_intersect_rsolid",
"params": {
"input_count": 2
},
"inputs": ["node_for_a", "node_for_b"],
"output_count": 1
}
```
Replay effect: replays inputs and calls `intersect_rsolid(first, rest)`.
## Fillet
Source with serializable QL selector:
```python
from simplecadapi import ql as Q
selector = Q.edges().where(Q.curve_type("line")).take(4)
result = scad.fillet_rsolid(solid, selector, 0.25)
```
Serialized node:
```json
{
"op": "make_fillet_rsolid",
"params": {
"radius": 0.25,
"edge_count": 4,
"selected_edges": [
{
"graph_id": "graph_xxx",
"node_id": "node_xxx",
"output_slot": 0,
"kind": "EDGE",
"topo_id": "edge_...",
"selector_hint": {...}
}
],
"selected_edge_node_ids": ["node_select_edge_0", "node_select_edge_1", "node_select_edge_2", "node_select_edge_3"]
},
"inputs": ["node_for_solid", "node_select_edge_0", "node_select_edge_1", "node_select_edge_2", "node_select_edge_3"],
"output_count": 1
}
```
Each QL-selected or indexed getter-selected edge is serialized as its own `make_select_redge` node whose `geo_selector` is fixed to the runtime-selected edge geometry. `geo_selector` does not contain tags or source indices; it uses geometry facts such as `geom_type`, `length`, `center`, endpoints, bbox, and `metadata_geo`.
Replay edge resolution order:
1. Geo select nodes from `selected_edge_node_ids`
2. Legacy/fallback `selection_query`, when present
3. Explicit topo refs in `selected_edges`
4. Legacy indices in `selected_edge_indices`, when select nodes are unavailable
5. `selector_hint` fallback
Then replay calls `fillet_rsolid(solid, resolved_edges, radius)`.
## Chamfer
Source:
```python
selector = Q.edges().order_by(Q.center_axis("z"), desc=True).take(4)
result = scad.chamfer_rsolid(solid, selector, 0.15)
```
Serialized node shape is the same as fillet, except:
```json
{
"op": "make_chamfer_rsolid",
"params": {
"distance": 0.15,
"edge_count": 4,
"selected_edges": [...],
"selected_edge_node_ids": [...]
},
"inputs": ["node_for_solid", "node_select_edge_0", "..."]
}
```
Replay resolves edges using the same order and calls `chamfer_rsolid(solid, resolved_edges, distance)`.
## Shell
Source:
```python
selector = Q.faces().order_by(Q.center_axis("z"), desc=True).take(1).exactly(1)
result = scad.shell_rsolid(solid, selector, 0.25)
```
Serialized node:
```json
{
"op": "make_shell_rsolid",
"params": {
"thickness": 0.25,
"removed_face_count": 1,
"selected_faces": [...],
"selected_face_node_ids": ["node_select_face_0"]
},
"inputs": ["node_for_solid", "node_select_face_0"],
"output_count": 1
}
```
The face select node uses `make_select_rface` with a tag-free `geo_selector` fixed to the runtime-selected face geometry.
Replay face resolution order:
1. Geo select nodes from `selected_face_node_ids`
2. Legacy/fallback `selection_query`, when present
3. Explicit topo refs in `selected_faces`
4. Legacy indices in `selected_face_indices`, when select nodes are unavailable
5. `selector_hint` fallback
Then replay calls `shell_rsolid(solid, resolved_faces, thickness)`.
## Linear pattern macro lowering
Source:
```python
copies = scad.linear_pattern_rsolidlist(seed, (1, 0, 0), count=3, spacing=2.0)
```
When recording is active, this does not emit a `linear_pattern` node. It emits one translate node per generated copy:
```text
seed -> make_translate_rshape(vector=[0, 0, 0])
seed -> make_translate_rshape(vector=[2, 0, 0])
seed -> make_translate_rshape(vector=[4, 0, 0])
```
Replay effect: each generated copy is replayed as an ordinary translated shape.
## Radial pattern macro lowering
Source:
```python
copies = scad.radial_pattern_rsolidlist(
seed,
center=(0, 0, 0),
axis=(0, 0, 1),
count=4,
total_rotation_angle=360.0,
)
```
When recording is active, this emits explicit rotate nodes for non-zero rotations. The zero-angle first copy is the original shape and does not create a rotate node.
```text
seed retained as first copy
seed -> make_rotate_rshape(angle=90)
seed -> make_rotate_rshape(angle=180)
seed -> make_rotate_rshape(angle=270)
```
Replay effect: copies are ordinary rotate operations, not a pattern macro.
@@ -0,0 +1,465 @@
# Primitive and Profile Operation Serialization
This guide covers replayable primitive/profile operations in the canonical operation graph.
All examples assume:
```python
import json
import simplecadapi as scad
with scad.GraphSession() as session:
...
payload = json.loads(scad.export_model_json(session))
```
In exported JSON, each operation appears in `payload["graph"]["nodes"]` as:
```json
{
"node_id": "node_xxxxxxxx",
"op": "make_line_redge",
"params": {...},
"inputs": [],
"output_count": 1,
"tags": [...],
"display": {...},
"param_exprs": {...},
"context": {...}
}
```
`display`, `tags`, `context`, `semantic_delta`, and `topo_delta` are useful metadata. Replay primarily depends on `op`, `params`, and `inputs`.
## Point
Source:
```python
p = scad.make_point_rvertex(1.0, 2.0, 3.0)
```
Serialized node:
```json
{
"op": "make_point_rvertex",
"params": {"x": 1.0, "y": 2.0, "z": 3.0},
"inputs": [],
"output_count": 1
}
```
Replay effect: calls `make_point_rvertex(x, y, z)` and returns a `Vertex`.
## Line edge
Source:
```python
edge = scad.make_line_redge((0, 0, 0), (5, 0, 0))
```
Serialized node:
```json
{
"op": "make_line_redge",
"params": {"start": [0.0, 0.0, 0.0], "end": [5.0, 0.0, 0.0]},
"inputs": [],
"output_count": 1
}
```
Replay effect: calls `make_line_redge(start, end)` and returns an `Edge`.
### Segment aliases
`make_segment_redge(start, end)` is an alias of `make_line_redge(...)` and records the same `make_line_redge` node.
`make_segment_rwire(start, end)` lowers to:
1. `make_line_redge`
2. `make_wire_from_edges_rwire`
There is no canonical `make_segment_wire` node in model JSON.
## Circle edge, wire, and face
Source edge:
```python
edge = scad.make_circle_redge((0, 0, 0), 2.0, normal=(0, 0, 1))
```
Serialized node:
```json
{
"op": "make_circle_redge",
"params": {
"center": [0.0, 0.0, 0.0],
"radius": 2.0,
"normal": [0.0, 0.0, 1.0]
},
"inputs": [],
"output_count": 1
}
```
Replay effect: calls `make_circle_redge(center, radius, normal)`.
Source wire:
```python
wire = scad.make_circle_rwire((0, 0, 0), 2.0)
```
Lowered serialized graph:
```text
make_circle_redge -> make_wire_from_edges_rwire
```
Source face:
```python
face = scad.make_circle_rface((0, 0, 0), 2.0)
```
Lowered serialized graph:
```text
make_circle_redge -> make_wire_from_edges_rwire -> make_face_from_wire_rface
```
There is no canonical `make_circle_wire` or `make_circle_face` node.
## Three-point arc edge and wire
Source edge:
```python
arc = scad.make_three_point_arc_redge(
(0, 0, 0),
(1, 1, 0),
(2, 0, 0),
)
```
Serialized node:
```json
{
"op": "make_three_point_arc_redge",
"params": {
"start": [0.0, 0.0, 0.0],
"middle": [1.0, 1.0, 0.0],
"end": [2.0, 0.0, 0.0]
},
"inputs": [],
"output_count": 1
}
```
Replay effect: calls `make_three_point_arc_redge(start, middle, end)`.
`make_three_point_arc_rwire(...)` lowers to:
```text
make_three_point_arc_redge -> make_wire_from_edges_rwire
```
## Angle arc edge and wire
Source edge:
```python
arc = scad.make_angle_arc_redge(
center=(0, 0, 0),
radius=1.0,
start_angle=0.0,
end_angle=1.57,
normal=(0, 0, 1),
)
```
Serialized node:
```json
{
"op": "make_angle_arc_redge",
"params": {
"center": [0.0, 0.0, 0.0],
"radius": 1.0,
"start_angle": 0.0,
"end_angle": 1.57,
"normal": [0.0, 0.0, 1.0]
},
"inputs": [],
"output_count": 1
}
```
Replay effect: calls `make_angle_arc_redge(center, radius, start_angle, end_angle, normal)`.
`make_angle_arc_rwire(...)` lowers to:
```text
make_angle_arc_redge -> make_wire_from_edges_rwire
```
## Spline edge and wire
Source edge:
```python
fit = scad.fit_cubic_bspline_control_points(
[(0, 0, 0), (1, 1, 0), (2, 0, 0)],
tolerance=0.01,
)
spline = scad.make_spline_redge(
control_points=fit.control_points,
knots=fit.unique_knots,
multiplicities=fit.multiplicities,
)
```
Serialized node:
```json
{
"op": "make_spline_redge",
"params": {
"control_points": [[0.0, 0.0, 0.0], [0.6, 1.0, 0.0], [1.4, 1.0, 0.0], [2.0, 0.0, 0.0]],
"degree": 3,
"knots": [0.0, 1.0],
"multiplicities": [4, 4],
"weights": null,
"periodic": false
},
"inputs": [],
"output_count": 1
}
```
Replay effect: calls `make_spline_redge(control_points=..., degree=..., knots=..., multiplicities=..., weights=..., periodic=...)`.
`make_spline_rwire(control_points=..., ...)` lowers to:
```text
make_spline_redge -> make_wire_from_edges_rwire
```
`make_spline_redge` now stores an exact B-spline definition. It does not accept sampled/interpolated curve points directly; use `fit_cubic_bspline_control_points(...)` first when human/LLM-authored code starts from samples.
## Helix edge and wire
Source edge:
```python
helix = scad.make_helix_redge(
pitch=0.7,
height=2.2,
radius=0.9,
center=(0, 0, 0),
dir=(0, 0, 1),
)
```
Serialized node:
```json
{
"op": "make_helix_redge",
"params": {
"pitch": 0.7,
"height": 2.2,
"radius": 0.9,
"center": [0.0, 0.0, 0.0],
"dir": [0.0, 0.0, 1.0]
},
"inputs": [],
"output_count": 1
}
```
Replay effect: calls `make_helix_redge(pitch, height, radius, center, dir)`.
`make_helix_rwire(...)` lowers to:
```text
make_helix_redge -> make_wire_from_edges_rwire
```
## Wire from edges
Source:
```python
a = scad.make_line_redge((0, 0, 0), (1, 0, 0))
b = scad.make_line_redge((1, 0, 0), (1, 1, 0))
wire = scad.make_wire_from_edges_rwire([a, b])
```
Serialized node:
```json
{
"op": "make_wire_from_edges_rwire",
"params": {"edge_count": 2},
"inputs": ["node_for_a", "node_for_b"],
"output_count": 1
}
```
Replay effect:
1. Replay each input edge node.
2. Collect input edge outputs in input order.
3. Call `make_wire_from_edges_rwire(edges)`.
The actual edge geometry is not duplicated inside this node; it is recovered through `inputs`.
## Face from wire
Source:
```python
face = scad.make_face_from_wire_rface(wire, normal=(0, 0, 1))
```
Serialized node:
```json
{
"op": "make_face_from_wire_rface",
"params": {"normal": [0.0, 0.0, 1.0]},
"inputs": ["node_for_wire"],
"output_count": 1
}
```
Replay effect:
1. Replay the input wire node.
2. Call `make_face_from_wire_rface(wire, normal=...)`.
## Rectangle wire and face lowering
Source:
```python
wire = scad.make_rectangle_rwire(4.0, 2.0, center=(0, 0, 0))
face = scad.make_rectangle_rface(4.0, 2.0, center=(0, 0, 0))
```
Lowered serialized graph:
```text
make_rectangle_rwire:
make_line_redge x4 -> make_wire_from_edges_rwire
make_rectangle_rface:
make_line_redge x4 -> make_wire_from_edges_rwire -> make_face_from_wire_rface
```
There is no canonical `make_rectangle_wire` or `make_rectangle_face` node.
## Polyline wire lowering
Source:
```python
wire = scad.make_polyline_rwire(
[(0, 0, 0), (1, 0, 0), (1, 1, 0)],
closed=False,
)
```
Lowered serialized graph:
```text
make_line_redge x(number_of_segments) -> make_wire_from_edges_rwire
```
If `closed=True`, one additional closing line edge is emitted.
There is no canonical `make_polyline_wire` node.
## Box, cylinder, sphere, and cone lowering
These user-facing primitive solids are intentionally lowered to canonical profile/feature operations.
### Box
Source:
```python
box = scad.make_box_rsolid(4.0, 2.0, 1.0)
```
Lowered serialized graph:
```text
make_line_redge x4
-> make_wire_from_edges_rwire
-> make_face_from_wire_rface
-> make_extrude_rsolid
```
Replay effect: rebuilds the rectangular face, then extrudes it.
There is no canonical `make_box` node.
### Cylinder
Source:
```python
cyl = scad.make_cylinder_rsolid(1.0, 3.0)
```
Lowered serialized graph:
```text
make_circle_redge
-> make_wire_from_edges_rwire
-> make_face_from_wire_rface
-> make_extrude_rsolid
```
There is no canonical `make_cylinder` node.
### Sphere
Source:
```python
sphere = scad.make_sphere_rsolid(1.5, center=(0, 0, 0))
```
Lowered serialized graph:
```text
profile edges/wire/face -> make_revolve_rsolid
```
There is no canonical `make_sphere` node.
### Cone / truncated cone
Source:
```python
cone = scad.make_cone_rsolid(1.2, 2.0, top_radius=0.4)
```
Lowered serialized graph:
```text
profile edges/wire/face -> make_revolve_rsolid
```
There is no canonical `make_cone` node.
@@ -0,0 +1,13 @@
# Scalar Fields / SDF Status
SDF and scalar field modeling are temporarily removed from the supported SimpleCADAPI surface.
Current contract:
- `simplecadapi.field` is not exported.
- `make_field_surface_rsolid` is not exported.
- `*_rscalarfield` APIs are not generated in public API docs.
- `make_field_surface_rsolid` is not a canonical graph op.
- Model JSON replay does not rebuild scalar field surfaces.
Historical payloads or examples that rely on scalar field trees should be treated as unsupported until a new SDF contract is designed.
+11
View File
@@ -0,0 +1,11 @@
# Shell
`Shell` is not part of the stable SimpleCADAPI 2.0 beta public wrapper surface.
Use `Face` for bounded surfaces and `Solid` for closed 3D bodies. For thin-walled parts, use the public feature operation:
```python
shelled = shell_rsolid(solid, faces_to_remove, thickness)
```
See [`shell_rsolid`](../api/shell_rsolid.md) for the stable public operation.
@@ -0,0 +1,26 @@
# SimpleWorkplane
`SimpleWorkplane` is a context manager for modeling in a temporary local coordinate frame.
## Public constructor
```python
SimpleWorkplane(origin=(0, 0, 0), normal=(0, 0, 1))
```
## Main capabilities
- Push a local coordinate frame for nested modeling operations.
- Automatically restore the previous frame when the context exits.
- Keep the public API shape-first: functions still return `Vertex`, `Edge`, `Wire`, `Face`, and `Solid` objects.
## Example
```python
import simplecadapi as scad
with scad.SimpleWorkplane((0, 0, 10), normal=(0, 0, 1)):
box = scad.make_box_rsolid(2, 2, 2)
print(box.get_volume())
```
+726
View File
@@ -0,0 +1,726 @@
# Solid
## Overview
`Solid` is the solid class in the SimpleCAD API, representing a 3D closed geometry. Solids have volume and are one of the most important geometry types in CAD modeling. It wraps the OCP Solid object and adds tagging functionality and automatic face tagging capability.
## Class Definition
```python
class Solid(TaggedMixin):
"""实体类,包装OCP的Solid,添加标签功能"""
```
## Inheritance
- Inherits from `TaggedMixin`, providing tag and metadata functionality
## Usage
- Represent 3D solid objects
- Perform boolean operations (union, intersection, difference)
- Apply feature operations (fillets, chamfers, etc.)
- Calculate physical properties such as volume and surface area
- Generate manufacturing data
## Constructor
### `__init__(wrapped)`
Initializes a solid object.
**Parameters:**
- `wrapped` (Union[OCP TopoDS_Solid, Any]): A OCP solid object or other Shape object
**Raises:**
- `ValueError`: When the input solid object is invalid
**Example:**
```python
from simplecadapi import (
make_box_rsolid,
make_cylinder_rsolid,
make_sphere_rsolid
)
# 通过 SimpleCAD 函数创建实体
box = make_box_rsolid(width=5, height=3, depth=2)
cylinder = make_cylinder_rsolid(center=(0, 0, 0), radius=2, height=4)
sphere = make_sphere_rsolid(center=(0, 0, 0), radius=1.5)
```
## Main Properties
- `wrapped`: The underlying OCP solid object
- `_tags`: Tag set (inherited from TaggedMixin)
- `_metadata`: Metadata dictionary (inherited from TaggedMixin)
- `_face_tags`: Face tag dictionary (internal use)
## Common Methods
### `get_volume()`
Get the volume of the solid.
**Returns:**
- `float`: The volume of the solid
**Raises:**
- `ValueError`: When volume retrieval fails
**Example:**
```python
from simplecadapi import make_box_rsolid, make_cylinder_rsolid, make_sphere_rsolid
import math
# 立方体体积
box = make_box_rsolid(width=2, height=3, depth=4)
box_volume = box.get_volume()
print(f"立方体体积: {box_volume}") # 24.0
# 圆柱体体积
cylinder = make_cylinder_rsolid(center=(0, 0, 0), radius=2, height=5)
cylinder_volume = cylinder.get_volume()
expected_volume = math.pi * 2**2 * 5
print(f"圆柱体体积: {cylinder_volume:.3f}, 期望: {expected_volume:.3f}")
# 球体体积
sphere = make_sphere_rsolid(center=(0, 0, 0), radius=1.5)
sphere_volume = sphere.get_volume()
expected_volume = (4/3) * math.pi * 1.5**3
print(f"球体体积: {sphere_volume:.3f}, 期望: {expected_volume:.3f}")
```
### `get_faces()`
Get all faces that make up the solid.
**Returns:**
- `List[Face]`: List of face objects
**Raises:**
- `ValueError`: When face list retrieval fails
**Example:**
```python
from simplecadapi import make_box_rsolid
box = make_box_rsolid(width=4, height=3, depth=2)
faces = box.get_faces()
print(f"立方体有 {len(faces)} 个面")
for i, face in enumerate(faces):
area = face.get_area()
print(f"{i}: 面积 {area:.3f}")
```
### `get_faces(index)`
Get one face by explicit index. In an active `GraphSession`, this intentional
indexed pick is preserved as a graph geo select node.
**Returns:**
- `Face`: The selected face object
**Example:**
```python
from simplecadapi import make_box_rsolid
box = make_box_rsolid(width=4, height=3, depth=2)
first_face = box.get_faces(0)
print(first_face.get_area())
```
### `get_edges()`
Get all edges that make up the solid.
**Returns:**
- `List[Edge]`: List of edge objects
**Raises:**
- `ValueError`: When edge list retrieval fails
**Example:**
```python
from simplecadapi import make_box_rsolid
box = make_box_rsolid(width=4, height=3, depth=2)
edges = box.get_edges()
print(f"立方体有 {len(edges)} 条边")
for i, edge in enumerate(edges):
length = edge.get_length()
print(f"{i}: 长度 {length:.3f}")
```
### `get_edges(index)`
Get one edge by explicit index. In an active `GraphSession`, this intentional
indexed pick is preserved as a graph geo select node.
**Returns:**
- `Edge`: The selected edge object
**Example:**
```python
from simplecadapi import make_box_rsolid
box = make_box_rsolid(width=4, height=3, depth=2)
first_edge = box.get_edges(0)
print(first_edge.get_length())
```
### `auto_tag_faces(geometry_type)`
Automatically add tags to faces.
**Parameters:**
- `geometry_type` (str): Geometry type ("box", "cylinder", "sphere", "unknown")
**Example:**
```python
from simplecadapi import make_box_rsolid, make_cylinder_rsolid
# 立方体面标记
box = make_box_rsolid(width=4, height=3, depth=2)
box.auto_tag_faces("box")
faces = box.get_faces()
for face in faces:
print(f"面标签: {list_tags(face)}")
# 圆柱体面标记
cylinder = make_cylinder_rsolid(center=(0, 0, 0), radius=2, height=4)
cylinder.auto_tag_faces("cylinder")
faces = cylinder.get_faces()
for face in faces:
print(f"面标签: {list_tags(face)}")
```
### Tagging and Metadata
Use the functional public API `apply_tag(shape, tag)` and `list_tags(shape)` for tags. Use `set_metadata(key, value)` and `get_metadata(key, default=None)` for structured metadata.
## Usage Examples
### Creating and Analyzing Basic Solids
```python
from simplecadapi import (
make_box_rsolid,
make_cylinder_rsolid,
make_sphere_rsolid
)
def create_basic_solids():
"""创建和分析基础实体"""
# 创建不同类型的实体
solids = [
("box", make_box_rsolid(width=4, height=3, depth=2)),
("cylinder", make_cylinder_rsolid(center=(0, 0, 0), radius=2, height=4)),
("sphere", make_sphere_rsolid(center=(0, 0, 0), radius=1.5))
]
for name, solid in solids:
# 添加基本标签
apply_tag(solid, name)
apply_tag(solid, "basic_geometry")
# 自动标记面
solid.auto_tag_faces(name)
# 获取几何属性
volume = solid.get_volume()
faces = solid.get_faces()
edges = solid.get_edges()
# 计算表面积
total_surface_area = sum(face.get_area() for face in faces)
# 分析面的分布
face_areas = [face.get_area() for face in faces]
min_face_area = min(face_areas)
max_face_area = max(face_areas)
avg_face_area = sum(face_areas) / len(face_areas)
# 存储元数据
solid.set_metadata("volume", volume)
solid.set_metadata("surface_area", total_surface_area)
solid.set_metadata("face_count", len(faces))
solid.set_metadata("edge_count", len(edges))
solid.set_metadata("min_face_area", min_face_area)
solid.set_metadata("max_face_area", max_face_area)
solid.set_metadata("avg_face_area", avg_face_area)
# 计算体积效率(体积/表面积比)
volume_efficiency = volume / total_surface_area if total_surface_area > 0 else 0
solid.set_metadata("volume_efficiency", volume_efficiency)
print(f"{name.upper()} 实体分析:")
print(f" 体积: {volume:.3f}")
print(f" 表面积: {total_surface_area:.3f}")
print(f" 面数: {len(faces)}")
print(f" 边数: {len(edges)}")
print(f" 体积效率: {volume_efficiency:.3f}")
print(f" 面积范围: {min_face_area:.3f} - {max_face_area:.3f}")
print()
create_basic_solids()
```
### Boolean Operations Example
```python
from simplecadapi import (
make_box_rsolid,
make_cylinder_rsolid,
union_rsolid,
cut_rsolid,
intersect_rsolid
)
def boolean_operations_example():
"""布尔运算示例"""
# 创建基础几何体
box = make_box_rsolid(width=6, height=4, depth=3)
apply_tag(box, "base_box")
cylinder = make_cylinder_rsolid(center=(3, 2, 0), radius=1, height=5)
apply_tag(cylinder, "cutting_cylinder")
# 并集运算
union_result = union_rsolid(box, cylinder)
apply_tag(union_result, "union_result")
apply_tag(union_result, "combined_geometry")
# 差集运算(从盒子中减去圆柱)
cut_result = cut_rsolid(box, cylinder)[0]
apply_tag(cut_result, "cut_result")
apply_tag(cut_result, "with_hole")
# 交集运算
intersect_result = intersect_rsolid(box, cylinder)[0]
apply_tag(intersect_result, "intersect_result")
apply_tag(intersect_result, "common_volume")
# 分析结果
operations = [
("原始盒子", box),
("原始圆柱", cylinder),
("并集", union_result),
("差集", cut_result),
("交集", intersect_result)
]
for name, solid in operations:
volume = solid.get_volume()
faces = solid.get_faces()
surface_area = sum(face.get_area() for face in faces)
solid.set_metadata("operation_type", name)
solid.set_metadata("volume", volume)
solid.set_metadata("surface_area", surface_area)
print(f"{name}:")
print(f" 体积: {volume:.3f}")
print(f" 表面积: {surface_area:.3f}")
print(f" 面数: {len(faces)}")
print(f" 标签: {list_tags(solid)}")
print()
boolean_operations_example()
```
### Feature Operations Example
```python
from simplecadapi import (
make_box_rsolid,
fillet_rsolid,
chamfer_rsolid,
select_edges_by_tag
)
def feature_operations_example():
"""特征操作示例"""
# 创建基础盒子
base_box = make_box_rsolid(width=8, height=6, depth=4)
apply_tag(base_box, "base_geometry")
base_box.auto_tag_faces("box")
# 分析原始几何体
original_volume = base_box.get_volume()
original_faces = base_box.get_faces()
original_edges = base_box.get_edges()
print(f"原始几何体:")
print(f" 体积: {original_volume:.3f}")
print(f" 面数: {len(original_faces)}")
print(f" 边数: {len(original_edges)}")
print()
# 圆角操作
try:
filleted_box = fillet_rsolid(base_box, radius=0.5)
apply_tag(filleted_box, "filleted")
apply_tag(filleted_box, "rounded_edges")
filleted_volume = filleted_box.get_volume()
filleted_faces = filleted_box.get_faces()
filleted_edges = filleted_box.get_edges()
filleted_box.set_metadata("original_volume", original_volume)
filleted_box.set_metadata("volume_change", filleted_volume - original_volume)
filleted_box.set_metadata("volume_ratio", filleted_volume / original_volume)
print(f"圆角后几何体:")
print(f" 体积: {filleted_volume:.3f}")
print(f" 体积变化: {filleted_volume - original_volume:.3f}")
print(f" 面数: {len(filleted_faces)}")
print(f" 边数: {len(filleted_edges)}")
print()
except Exception as e:
print(f"圆角操作失败: {e}")
filleted_box = None
# 倒角操作
try:
chamfered_box = chamfer_rsolid(base_box, distance=0.3)
apply_tag(chamfered_box, "chamfered")
apply_tag(chamfered_box, "beveled_edges")
chamfered_volume = chamfered_box.get_volume()
chamfered_faces = chamfered_box.get_faces()
chamfered_edges = chamfered_box.get_edges()
chamfered_box.set_metadata("original_volume", original_volume)
chamfered_box.set_metadata("volume_change", chamfered_volume - original_volume)
chamfered_box.set_metadata("volume_ratio", chamfered_volume / original_volume)
print(f"倒角后几何体:")
print(f" 体积: {chamfered_volume:.3f}")
print(f" 体积变化: {chamfered_volume - original_volume:.3f}")
print(f" 面数: {len(chamfered_faces)}")
print(f" 边数: {len(chamfered_edges)}")
print()
except Exception as e:
print(f"倒角操作失败: {e}")
chamfered_box = None
# 比较结果
results = [("原始", base_box)]
if filleted_box:
results.append(("圆角", filleted_box))
if chamfered_box:
results.append(("倒角", chamfered_box))
print("特征操作比较:")
for name, solid in results:
volume = solid.get_volume()
faces = solid.get_faces()
tags = list_tags(solid)
print(f" {name}: 体积={volume:.3f}, 面数={len(faces)}, 标签={tags}")
feature_operations_example()
```
### Creating Complex Geometry
```python
from simplecadapi import (
make_box_rsolid,
make_cylinder_rsolid,
make_sphere_rsolid,
union_rsolid,
cut_rsolid,
translate_shape,
rotate_shape
)
def create_complex_geometry():
"""创建复杂几何体"""
# 创建主体
main_body = make_box_rsolid(width=12, height=8, depth=6)
apply_tag(main_body, "main_body")
apply_tag(main_body, "base_structure")
# 创建圆柱形孔
holes = []
hole_positions = [(3, 2, 0), (9, 2, 0), (3, 6, 0), (9, 6, 0)]
for i, (x, y, z) in enumerate(hole_positions):
hole = make_cylinder_rsolid(center=(x, y, z), radius=0.8, height=8)
apply_tag(hole, f"hole_{i}")
apply_tag(hole, "mounting_hole")
holes.append(hole)
# 创建球形特征
sphere_feature = make_sphere_rsolid(center=(6, 4, 6), radius=2)
apply_tag(sphere_feature, "sphere_feature")
apply_tag(sphere_feature, "decorative")
# 创建圆柱形支柱
support_cylinder = make_cylinder_rsolid(center=(6, 4, 0), radius=1, height=4)
apply_tag(support_cylinder, "support_cylinder")
apply_tag(support_cylinder, "structural")
# 组合几何体
complex_solid = union_rsolid(main_body, sphere_feature)
complex_solid = union_rsolid(complex_solid, support_cylinder)
# 减去孔
for hole in holes:
complex_solid = cut_rsolid(complex_solid, hole)[0]
# 标记复杂几何体
apply_tag(complex_solid, "complex_geometry")
apply_tag(complex_solid, "multi_feature")
apply_tag(complex_solid, "machined_part")
# 分析复杂几何体
volume = complex_solid.get_volume()
faces = complex_solid.get_faces()
edges = complex_solid.get_edges()
# 计算几何复杂度
face_count = len(faces)
edge_count = len(edges)
complexity_ratio = edge_count / face_count if face_count > 0 else 0
# 分析面的分布
face_areas = [face.get_area() for face in faces]
total_surface_area = sum(face_areas)
# 分类面
small_faces = [f for f in faces if f.get_area() < 1.0]
large_faces = [f for f in faces if f.get_area() > 10.0]
medium_faces = [f for f in faces if 1.0 <= f.get_area() <= 10.0]
# 存储分析结果
complex_solid.set_metadata("volume", volume)
complex_solid.set_metadata("surface_area", total_surface_area)
complex_solid.set_metadata("face_count", face_count)
complex_solid.set_metadata("edge_count", edge_count)
complex_solid.set_metadata("complexity_ratio", complexity_ratio)
complex_solid.set_metadata("small_face_count", len(small_faces))
complex_solid.set_metadata("medium_face_count", len(medium_faces))
complex_solid.set_metadata("large_face_count", len(large_faces))
# 根据复杂度添加标签
if complexity_ratio < 5:
apply_tag(complex_solid, "simple_topology")
elif complexity_ratio < 10:
apply_tag(complex_solid, "moderate_topology")
else:
apply_tag(complex_solid, "complex_topology")
print(f"复杂几何体分析:")
print(f" 体积: {volume:.3f}")
print(f" 表面积: {total_surface_area:.3f}")
print(f" 面数: {face_count}")
print(f" 边数: {edge_count}")
print(f" 复杂度比: {complexity_ratio:.2f}")
print(f" 面分布 - 小:{len(small_faces)}, 中:{len(medium_faces)}, 大:{len(large_faces)}")
print(f" 标签: {list_tags(complex_solid)}")
return complex_solid
complex_geometry = create_complex_geometry()
```
### Solid Quality Analysis
```python
from simplecadapi import make_box_rsolid, make_cylinder_rsolid, make_sphere_rsolid
def analyze_solid_quality():
"""分析实体质量"""
# 创建测试实体
test_solids = [
("small_box", make_box_rsolid(width=1, height=1, depth=1)),
("large_box", make_box_rsolid(width=10, height=10, depth=10)),
("thin_box", make_box_rsolid(width=10, height=10, depth=0.1)),
("cylinder", make_cylinder_rsolid(center=(0, 0, 0), radius=2, height=5)),
("sphere", make_sphere_rsolid(center=(0, 0, 0), radius=2))
]
for name, solid in test_solids:
apply_tag(solid, name)
apply_tag(solid, "test_geometry")
# 基本几何属性
volume = solid.get_volume()
faces = solid.get_faces()
edges = solid.get_edges()
# 计算质量指标
surface_area = sum(face.get_area() for face in faces)
volume_to_surface_ratio = volume / surface_area if surface_area > 0 else 0
# 分析拓扑复杂度
face_count = len(faces)
edge_count = len(edges)
euler_characteristic = None # 简化版本不计算欧拉特征数
# 面积分布分析
face_areas = [face.get_area() for face in faces]
if face_areas:
min_area = min(face_areas)
max_area = max(face_areas)
area_ratio = max_area / min_area if min_area > 0 else float('inf')
else:
min_area = max_area = area_ratio = 0
# 质量评估
quality_score = 0
quality_issues = []
# 体积检查
if volume > 1e-6:
quality_score += 20
else:
quality_issues.append("极小体积")
# 面数检查
if 4 <= face_count <= 100:
quality_score += 20
elif face_count > 100:
quality_issues.append("面数过多")
else:
quality_issues.append("面数异常")
# 面积比检查
if area_ratio < 1000:
quality_score += 20
else:
quality_issues.append("面积差异过大")
# 体积效率检查
if volume_to_surface_ratio > 0.1:
quality_score += 20
else:
quality_issues.append("体积效率低")
# 边数合理性检查
if edge_count < face_count * 10:
quality_score += 20
else:
quality_issues.append("边数过多")
# 存储质量数据
solid.set_metadata("volume", volume)
solid.set_metadata("surface_area", surface_area)
solid.set_metadata("face_count", face_count)
solid.set_metadata("edge_count", edge_count)
solid.set_metadata("volume_to_surface_ratio", volume_to_surface_ratio)
solid.set_metadata("area_ratio", area_ratio)
solid.set_metadata("quality_score", quality_score)
solid.set_metadata("quality_issues", quality_issues)
# 质量标签
if quality_score >= 80:
apply_tag(solid, "high_quality")
elif quality_score >= 60:
apply_tag(solid, "good_quality")
elif quality_score >= 40:
apply_tag(solid, "acceptable_quality")
else:
apply_tag(solid, "poor_quality")
print(f"{name.upper()} 质量分析:")
print(f" 体积: {volume:.6f}")
print(f" 表面积: {surface_area:.3f}")
print(f" 面数: {face_count}")
print(f" 边数: {edge_count}")
print(f" 体积效率: {volume_to_surface_ratio:.3f}")
print(f" 面积比: {area_ratio:.2f}")
print(f" 质量分数: {quality_score}/100")
if quality_issues:
print(f" 质量问题: {', '.join(quality_issues)}")
print(f" 质量等级: {[tag for tag in list_tags(solid) if 'quality' in tag]}")
print()
analyze_solid_quality()
```
## String Representation
```python
from simplecadapi import make_box_rsolid
box = make_box_rsolid(width=5, height=3, depth=2)
apply_tag(box, "example_box")
box.auto_tag_faces("box")
box.set_metadata("material", "aluminum")
print(box)
```
Output:
```
Solid:
volume: 30.000
face_count: 6
edge_count: 12
faces:
face_0:
area: 15.000
normal: [0.000, 0.000, 1.000]
tags: [top]
face_1:
area: 15.000
normal: [0.000, 0.000, -1.000]
tags: [bottom]
face_2:
area: 10.000
normal: [0.000, 1.000, 0.000]
tags: [front]
face_3:
area: 10.000
normal: [0.000, -1.000, 0.000]
tags: [back]
face_4:
area: 6.000
normal: [1.000, 0.000, 0.000]
tags: [right]
face_5:
area: 6.000
normal: [-1.000, 0.000, 0.000]
tags: [left]
tags: [example_box]
metadata:
material: aluminum
```
## Relationships with Other Geometry
- **Face (Face)**: Boundary surfaces of a solid
- **Edge (Edge)**: Boundaries of faces
- **Shell (Shell)**: Solids can be decomposed into shells
- **Compound (Compound)**: Multiple solids can form a compound
## Application Scenarios
- **Mechanical design**: Part modeling
- **Architectural design**: Building components
- **Product design**: Industrial products
- **3D printing**: Prototype manufacturing
- **Simulation analysis**: Finite element analysis
## Notes
- Solids must be closed, valid geometry
- Boolean operations may change the solid's topology
- Complex solids may contain many faces and edges
- Feature operations may fail and require appropriate error handling
- Automatic face tagging depends on geometry regularity
- Solid quality directly affects the success rate and performance of subsequent operations
@@ -0,0 +1,90 @@
# TaggedMixin
## Overview
`TaggedMixin` is the internal tag and metadata storage mixin used by `Vertex`, `Edge`, `Wire`, `Face`, and `Solid`. It owns the shared `_tags`, `_metadata`, and `_runtime` stores for topology wrappers.
User code should not call member tag mutators. The public tag API is functional:
- `apply_tag(shape, tag)` attaches one normalized tag.
- `list_tags(shape)` returns tags in deterministic sorted order.
- `select_faces_by_tag(...)`, `select_edges_by_tag(...)`, and QL predicates such as `ql.tag("role.*")` provide selection/query helpers.
## Tagging Mental Model
- Tags are normalized lowercase dot-separated semantic tokens.
- Examples: `role.mounting_surface`, `anchor.datum.primary`, `group.fasteners`, `face.top`, `edge.boundary`, `wire.outer`, `solid.boolean.cut`.
- `apply_tag(shape, tag)` does not expose propagation controls.
- The standard policy propagates `role.*`, `anchor.*`, `group.*`, and a few legacy bare semantic tags downward.
- Topology-specific tags such as `face.*`, `edge.*`, `wire.*`, `vertex.*`, and `solid.*` stay local.
- Numeric dimensions, measurements, and rich descriptive payloads belong in metadata, not tags.
- Geometry builders store structured geometry facts under `metadata["geo"]`.
## Public Tag Usage
```python
import simplecadapi as scad
box = scad.make_box_rsolid(width=5, height=3, depth=2)
scad.apply_tag(box, "role.bracket")
box.auto_tag_faces("box")
print(scad.list_tags(box))
top_faces = [face for face in box.get_faces() if "face.top" in scad.list_tags(face)]
print(len(top_faces))
```
## Propagation Example
```python
import simplecadapi as scad
body = scad.make_box_rsolid(10, 10, 2)
scad.apply_tag(body, "role.mounting_plate")
face_hits = [face for face in body.get_faces() if "role.mounting_plate" in scad.list_tags(face)]
edge_hits = [edge for edge in body.get_edges() if "role.mounting_plate" in scad.list_tags(edge)]
print(len(face_hits), len(edge_hits))
```
## Auto Tags
Primitives and modeling operations may attach normalized tags automatically:
- Primitive tags such as `geom.primitive.box`, `geom.primitive.cylinder`, and `geom.primitive.sphere`.
- Face tags from `auto_tag_faces(...)`, such as `face.top`, `face.bottom`, `face.side`, and `face.surface`.
- Wire tags such as `wire.outer` and `wire.inner`.
- Operation/tracking tags such as `solid.boolean.cut`, `op.cut.modified`, or `op.extrude.generated`.
## Metadata Methods
`set_metadata(key, value)` and `get_metadata(key, default=None)` remain shape member methods for structured data.
```python
import simplecadapi as scad
part = scad.make_box_rsolid(10, 8, 5)
scad.apply_tag(part, "role.housing")
part.set_metadata("material", "6061-T6")
part.set_metadata("part_number", "mp-001-a")
print(part.get_metadata("material"))
print(part.get_metadata("geo"))
```
## QL Queries
```python
import simplecadapi as scad
from simplecadapi import ql as Q
body = scad.make_box_rsolid(10, 10, 2)
scad.apply_tag(body, "role.mounting_plate")
body.auto_tag_faces("box")
top_faces = Q.select(body.get_faces()).where(Q.tag("face.top")).all()
role_faces = Q.select(body.get_faces()).where(Q.tag("role.*")).all()
print(len(top_faces), len(role_faces))
```
+287
View File
@@ -0,0 +1,287 @@
# Vertex
## Overview
`Vertex` is the vertex class in SimpleCAD API, representing a point in 3D space. It wraps OCP's Vertex object and adds tag functionality for identifying and managing specific vertices in geometries.
## Class Definition
```python
class Vertex(TaggedMixin):
"""顶点类,包装OCP的Vertex,添加标签功能"""
```
## Inheritance Relationships
- Inherits from `TaggedMixin`, with tag and metadata functionality
## Usage
- Represent points in 3D space
- Serve as building elements for edges, wires, faces, and other geometries
- Provide vertex coordinate information
- Support tag management and queries
## Constructor
### `__init__(wrapped)`
Initialize a vertex object.
**Parameters:**
- `wrapped` (OCP TopoDS_Vertex): OCP vertex object
**Exceptions:**
- `ValueError`: Raised when the input vertex object is invalid
**Example:**
```python
from simplecadapi import make_point_rvertex
# 通过 SimpleCAD 函数创建顶点
vertex = make_point_rvertex(1.0, 2.0, 3.0)
```
## Main Properties
- `wrapped`: Underlying OCP vertex object
- `_tags`: Tag set (inherited from TaggedMixin)
- `_metadata`: Metadata dictionary (inherited from TaggedMixin)
## Common Methods
### `get_coordinates()`
Get the coordinates of the vertex.
**Returns:**
- `Tuple[float, float, float]`: Vertex coordinates (x, y, z)
**Exceptions:**
- `ValueError`: Raised when coordinate retrieval fails
**Example:**
```python
from simplecadapi import make_point_rvertex
vertex = make_point_rvertex(1.0, 2.0, 3.0)
coords = vertex.get_coordinates()
print(coords) # (1.0, 2.0, 3.0)
```
### Tagging and Metadata
Use the functional public API `apply_tag(shape, tag)` and `list_tags(shape)` for tags. Use `set_metadata(key, value)` and `get_metadata(key, default=None)` for structured metadata.
**Example:**
```python
from simplecadapi import apply_tag, list_tags, make_point_rvertex
vertex = make_point_rvertex(0, 0, 0)
apply_tag(vertex, "role.origin")
apply_tag(vertex, "anchor.reference_point")
if "role.origin" in list_tags(vertex):
print("这是原点")
```
### Metadata Management Methods
#### `set_metadata(key, value)`
Set metadata.
**Example:**
```python
vertex = make_point_rvertex(0, 0, 0)
vertex.set_metadata("created_by", "user_input")
vertex.set_metadata("importance", "high")
```
#### `get_metadata(key, default=None)`
Get metadata.
**Example:**
```python
vertex = make_point_rvertex(0, 0, 0)
vertex.set_metadata("created_by", "user_input")
creator = vertex.get_metadata("created_by")
print(creator) # "user_input"
unknown = vertex.get_metadata("unknown_key", "default_value")
print(unknown) # "default_value"
```
## Usage Examples
### Creating and Using Vertices
```python
from simplecadapi import make_point_rvertex
# 创建顶点
vertex1 = make_point_rvertex(0, 0, 0)
vertex2 = make_point_rvertex(1, 1, 1)
# 获取坐标
coords1 = vertex1.get_coordinates()
coords2 = vertex2.get_coordinates()
print(f"顶点1坐标: {coords1}") # 顶点1坐标: (0.0, 0.0, 0.0)
print(f"顶点2坐标: {coords2}") # 顶点2坐标: (1.0, 1.0, 1.0)
```
### Vertex Tag Management
```python
from simplecadapi import make_point_rvertex
# 创建关键点
origin = make_point_rvertex(0, 0, 0)
corner1 = make_point_rvertex(10, 0, 0)
corner2 = make_point_rvertex(10, 10, 0)
corner3 = make_point_rvertex(0, 10, 0)
# 添加标签
apply_tag(origin, "origin")
apply_tag(origin, "reference")
apply_tag(corner1, "corner")
apply_tag(corner1, "x_axis")
apply_tag(corner2, "corner")
apply_tag(corner2, "diagonal")
apply_tag(corner3, "corner")
apply_tag(corner3, "y_axis")
# 查找所有角点
vertices = [origin, corner1, corner2, corner3]
corners = [v for v in vertices if "corner" in list_tags(v)]
print(f"找到 {len(corners)} 个角点")
```
### Vertex Classification and Management
```python
from simplecadapi import make_point_rvertex
def create_grid_vertices(width, height, spacing):
"""创建网格顶点"""
vertices = []
for i in range(width + 1):
for j in range(height + 1):
x = i * spacing
y = j * spacing
z = 0
vertex = make_point_rvertex(x, y, z)
# 添加位置标签
if i == 0 and j == 0:
apply_tag(vertex, "origin")
elif i == 0:
apply_tag(vertex, "left_edge")
elif i == width:
apply_tag(vertex, "right_edge")
if j == 0:
apply_tag(vertex, "bottom_edge")
elif j == height:
apply_tag(vertex, "top_edge")
# 添加角点标签
if (i == 0 or i == width) and (j == 0 or j == height):
apply_tag(vertex, "corner")
# 添加元数据
vertex.set_metadata("grid_position", (i, j))
vertex.set_metadata("distance_from_origin", (x*x + y*y)**0.5)
vertices.append(vertex)
return vertices
# 创建 5x3 网格
vertices = create_grid_vertices(5, 3, 1.0)
# 查找特定顶点
corners = [v for v in vertices if "corner" in list_tags(v)]
origin = [v for v in vertices if "origin" in list_tags(v)][0]
print(f"网格顶点总数: {len(vertices)}")
print(f"角点数量: {len(corners)}")
print(f"原点坐标: {origin.get_coordinates()}")
```
### Vertex Distance Calculation
```python
import math
from simplecadapi import make_point_rvertex
def calculate_distance(vertex1, vertex2):
"""计算两个顶点之间的距离"""
coords1 = vertex1.get_coordinates()
coords2 = vertex2.get_coordinates()
dx = coords2[0] - coords1[0]
dy = coords2[1] - coords1[1]
dz = coords2[2] - coords1[2]
return math.sqrt(dx*dx + dy*dy + dz*dz)
# 创建顶点
v1 = make_point_rvertex(0, 0, 0)
v2 = make_point_rvertex(3, 4, 0)
v3 = make_point_rvertex(0, 0, 5)
# 计算距离
dist12 = calculate_distance(v1, v2)
dist13 = calculate_distance(v1, v3)
dist23 = calculate_distance(v2, v3)
print(f"v1 到 v2 的距离: {dist12}") # 5.0
print(f"v1 到 v3 的距离: {dist13}") # 5.0
print(f"v2 到 v3 的距离: {dist23}") # 约 7.07
```
## String Representation
```python
from simplecadapi import make_point_rvertex
vertex = make_point_rvertex(1.234, 5.678, 9.012)
apply_tag(vertex, "test_point")
vertex.set_metadata("created_by", "example")
print(vertex)
```
Output:
```
Vertex:
coordinates: [1.234, 5.678, 9.012]
tags: [test_point]
metadata:
created_by: example
```
## Relationships with Other Geometries
Vertices are the fundamental elements that compose more complex geometries:
- **Edge**: Defined by two vertices
- **Wire**: Composed of multiple connected edges, containing multiple vertices
- **Face**: Boundary defined by vertices
- **Solid**: Ultimately composed of vertices
## Notes
- Vertex objects wrap OCP's underlying vertices; do not modify coordinates directly
- Tags are of string type and are case-sensitive
- Metadata can store values of any type
- Vertex coordinates are read-only; to modify positions, create new vertices
- Floating-point coordinates may have precision issues; consider tolerance when comparing
+447
View File
@@ -0,0 +1,447 @@
# Wire
## Overview
`Wire` is the wire class in the SimpleCAD API, representing a 1D geometric path formed by connecting multiple edges. A wire can be open (different start and end points) or closed (forming a closed path). It wraps the OCP Wire object and adds tagging functionality.
## Class Definition
```python
class Wire(TaggedMixin):
"""线类,包装OCP的Wire,添加标签功能"""
```
## Inheritance
- Inherits from `TaggedMixin`, providing tag and metadata functionality
## Usage
- Represent continuous paths or contours
- Form the boundary of faces (Face)
- Define paths for sweep, extrude, and other operations
- Create complex geometric contours
## Constructor
### `__init__(wrapped)`
Initializes a wire object.
**Parameters:**
- `wrapped` (OCP TopoDS_Wire): A OCP wire object
**Raises:**
- `ValueError`: When the input wire object is invalid
**Example:**
```python
from simplecadapi import (
make_rectangle_rwire,
make_circle_rwire,
make_polyline_rwire
)
# 通过 SimpleCAD 函数创建线
rectangle = make_rectangle_rwire(width=5, height=3)
circle = make_circle_rwire(center=(0, 0, 0), radius=2.0)
polyline = make_polyline_rwire(points=[(0, 0, 0), (1, 1, 0), (2, 0, 0)])
```
## Main Properties
- `wrapped`: The underlying OCP wire object
- `_tags`: Tag set (inherited from TaggedMixin)
- `_metadata`: Metadata dictionary (inherited from TaggedMixin)
## Common Methods
### `get_edges()`
Get all edges that make up the wire.
**Returns:**
- `List[Edge]`: List of edge objects
**Raises:**
- `ValueError`: When edge list retrieval fails
**Example:**
```python
from simplecadapi import make_rectangle_rwire
rectangle = make_rectangle_rwire(width=4, height=3)
edges = rectangle.get_edges()
print(f"矩形由 {len(edges)} 条边组成")
for i, edge in enumerate(edges):
print(f"{i}: 长度 {edge.get_length():.3f}")
```
### `is_closed()`
Check if the wire is closed.
**Returns:**
- `bool`: Returns True if the wire is closed, False otherwise
**Raises:**
- `ValueError`: When closure check fails
**Example:**
```python
from simplecadapi import make_rectangle_rwire, make_polyline_rwire
# 闭合线
rectangle = make_rectangle_rwire(width=5, height=3)
print(f"矩形是否闭合: {rectangle.is_closed()}") # True
# 开放线
polyline = make_polyline_rwire(points=[(0, 0, 0), (1, 1, 0), (2, 0, 0)])
print(f"折线是否闭合: {polyline.is_closed()}") # False
```
### Tagging and Metadata
Use the functional public API `apply_tag(shape, tag)` and `list_tags(shape)` for tags. Use `set_metadata(key, value)` and `get_metadata(key, default=None)` for structured metadata.
## Usage Examples
### Creating Different Types of Wires
```python
from simplecadapi import (
make_rectangle_rwire,
make_circle_rwire,
make_polyline_rwire,
make_spline_rwire
)
# 矩形线
rectangle = make_rectangle_rwire(width=10, height=6)
apply_tag(rectangle, "rectangle")
apply_tag(rectangle, "closed")
# 圆形线
circle = make_circle_rwire(center=(0, 0, 0), radius=3.0)
apply_tag(circle, "circle")
apply_tag(circle, "closed")
# 折线
polyline = make_polyline_rwire(points=[
(0, 0, 0), (2, 0, 0), (2, 2, 0), (1, 3, 0), (0, 2, 0)
])
apply_tag(polyline, "polyline")
apply_tag(polyline, "open")
# 样条线:control_points 是 B-spline poles,不是采样点
spline = make_spline_rwire(
control_points=[(0, 0, 0), (1, 2, 0), (3, 2, 0), (4, 0, 0)]
)
apply_tag(spline, "spline")
apply_tag(spline, "smooth")
# 分析线的属性
wires = [rectangle, circle, polyline, spline]
for wire in wires:
edges = wire.get_edges()
closed = wire.is_closed()
tags = list_tags(wire)
print(f"线类型: {tags}, 边数: {len(edges)}, 闭合: {closed}")
```
### Creating Complex Contours
```python
from simplecadapi import make_polyline_rwire
def create_complex_profile():
"""创建复杂的轮廓线"""
# 定义轮廓点
points = [
(0, 0, 0), # 起点
(10, 0, 0), # 底边
(10, 2, 0), # 右下
(8, 2, 0), # 内凹1
(8, 4, 0), #
(10, 4, 0), # 右上
(10, 6, 0), # 顶边右
(0, 6, 0), # 顶边左
(0, 4, 0), # 左上
(2, 4, 0), # 内凹2
(2, 2, 0), #
(0, 2, 0), # 左下
(0, 0, 0) # 闭合回起点
]
profile = make_polyline_rwire(points=points)
apply_tag(profile, "complex_profile")
apply_tag(profile, "symmetric")
# 添加几何信息
edges = profile.get_edges()
total_length = sum(edge.get_length() for edge in edges)
profile.set_metadata("total_length", total_length)
profile.set_metadata("point_count", len(points))
profile.set_metadata("edge_count", len(edges))
return profile
profile = create_complex_profile()
print(f"复杂轮廓: {list_tags(profile)}")
print(f"总长度: {profile.get_metadata('total_length'):.3f}")
print(f"边数: {profile.get_metadata('edge_count')}")
```
### Wire Analysis and Processing
```python
from simplecadapi import make_rectangle_rwire, make_circle_rwire
def analyze_wire_properties():
"""分析线的属性"""
# 创建不同的线
rectangle = make_rectangle_rwire(width=6, height=4)
circle = make_circle_rwire(center=(0, 0, 0), radius=2.0)
wires = [rectangle, circle]
for i, wire in enumerate(wires):
# 基本属性
edges = wire.get_edges()
is_closed = wire.is_closed()
# 计算总长度
total_length = sum(edge.get_length() for edge in edges)
# 分析边
edge_lengths = [edge.get_length() for edge in edges]
min_edge_length = min(edge_lengths)
max_edge_length = max(edge_lengths)
avg_edge_length = sum(edge_lengths) / len(edge_lengths)
# 添加标签和元数据
apply_tag(wire, f"wire_{i}")
apply_tag(wire, "analyzed")
if is_closed:
apply_tag(wire, "closed")
else:
apply_tag(wire, "open")
wire.set_metadata("total_length", total_length)
wire.set_metadata("edge_count", len(edges))
wire.set_metadata("min_edge_length", min_edge_length)
wire.set_metadata("max_edge_length", max_edge_length)
wire.set_metadata("avg_edge_length", avg_edge_length)
# 分类边
for j, edge in enumerate(edges):
apply_tag(edge, f"wire_{i}_edge_{j}")
edge.set_metadata("parent_wire", i)
edge.set_metadata("position_in_wire", j)
print(f"线 {i}:")
print(f" 总长度: {total_length:.3f}")
print(f" 边数: {len(edges)}")
print(f" 闭合: {is_closed}")
print(f" 最短边: {min_edge_length:.3f}")
print(f" 最长边: {max_edge_length:.3f}")
print(f" 平均边长: {avg_edge_length:.3f}")
print()
analyze_wire_properties()
```
### Wire Transformation and Operations
```python
from simplecadapi import make_rectangle_rwire, translate_shape, rotate_shape
def transform_wires():
"""变换线的操作"""
# 创建基础矩形
base_rect = make_rectangle_rwire(width=4, height=2)
apply_tag(base_rect, "base")
apply_tag(base_rect, "original")
# 创建变换后的线
translated_rect = translate_shape(base_rect, offset=(5, 0, 0))
apply_tag(translated_rect, "translated")
rotated_rect = rotate_shape(base_rect, axis=(0, 0, 1), angle=45)
apply_tag(rotated_rect, "rotated")
# 收集所有线
all_wires = [base_rect, translated_rect, rotated_rect]
# 分析变换结果
for wire in all_wires:
edges = wire.get_edges()
total_length = sum(edge.get_length() for edge in edges)
# 计算边界框(简化版)
all_coords = []
for edge in edges:
start_coords = edge.get_start_vertex().get_coordinates()
end_coords = edge.get_end_vertex().get_coordinates()
all_coords.extend([start_coords, end_coords])
if all_coords:
min_x = min(coord[0] for coord in all_coords)
max_x = max(coord[0] for coord in all_coords)
min_y = min(coord[1] for coord in all_coords)
max_y = max(coord[1] for coord in all_coords)
wire.set_metadata("bbox_min", (min_x, min_y))
wire.set_metadata("bbox_max", (max_x, max_y))
wire.set_metadata("bbox_width", max_x - min_x)
wire.set_metadata("bbox_height", max_y - min_y)
wire.set_metadata("total_length", total_length)
print(f"线标签: {list_tags(wire)}")
print(f" 总长度: {total_length:.3f}")
if wire.get_metadata("bbox_min"):
print(f" 边界框: {wire.get_metadata('bbox_min')}{wire.get_metadata('bbox_max')}")
print()
transform_wires()
```
### Building Wire Sequences
```python
from simplecadapi import make_segment_rwire
def create_wire_sequence():
"""创建线的序列"""
# 创建连续的线段
segments = []
# 定义路径点
waypoints = [
(0, 0, 0),
(2, 0, 0),
(2, 2, 0),
(0, 2, 0),
(0, 4, 0),
(4, 4, 0),
(4, 0, 0),
(6, 0, 0)
]
# 创建连续的线段
for i in range(len(waypoints) - 1):
start = waypoints[i]
end = waypoints[i + 1]
segment = make_segment_rwire(start=start, end=end)
apply_tag(segment, f"segment_{i}")
apply_tag(segment, "path_segment")
# 添加方向信息
direction = (
end[0] - start[0],
end[1] - start[1],
end[2] - start[2]
)
if direction[0] > 0:
apply_tag(segment, "eastward")
elif direction[0] < 0:
apply_tag(segment, "westward")
if direction[1] > 0:
apply_tag(segment, "northward")
elif direction[1] < 0:
apply_tag(segment, "southward")
segment.set_metadata("start_point", start)
segment.set_metadata("end_point", end)
segment.set_metadata("direction", direction)
segment.set_metadata("sequence_index", i)
segments.append(segment)
# 分析序列
total_path_length = sum(seg.get_edges(0).get_length() for seg in segments)
print(f"路径段数: {len(segments)}")
print(f"总路径长度: {total_path_length:.3f}")
# 按方向分类
eastward = [s for s in segments if "eastward" in list_tags(s)]
northward = [s for s in segments if "northward" in list_tags(s)]
print(f"向东段数: {len(eastward)}")
print(f"向北段数: {len(northward)}")
return segments
sequence = create_wire_sequence()
```
## String Representation
```python
from simplecadapi import make_rectangle_rwire
wire = make_rectangle_rwire(width=5, height=3)
apply_tag(wire, "example_rectangle")
wire.set_metadata("area", 15.0)
print(wire)
```
Output:
```
Wire:
edge_count: 4
closed: True
edges:
edge_0:
length: 5.000
vertices:
start: (0.0, 0.0, 0.0)
end: (5.0, 0.0, 0.0)
edge_1:
length: 3.000
vertices:
start: (5.0, 0.0, 0.0)
end: (5.0, 3.0, 0.0)
edge_2:
length: 5.000
vertices:
start: (5.0, 3.0, 0.0)
end: (0.0, 3.0, 0.0)
edge_3:
length: 3.000
vertices:
start: (0.0, 3.0, 0.0)
end: (0.0, 0.0, 0.0)
tags: [example_rectangle]
metadata:
area: 15.0
```
## Relationships with Other Geometry
- **Edge (Edge)**: Components of a wire
- **Face (Face)**: Closed wires can define face boundaries
- **Solid (Solid)**: Can be created by sweeping or extruding wires
## Notes
- Wire edges must be continuous; endpoints of adjacent edges must coincide
- The start and end points of a closed wire must coincide
- Wire orientation affects certain operations (such as face normal direction)
- Complex wires may contain self-intersections and require special handling
- Wire length equals the sum of all edge lengths
- When creating faces, outer boundary wires should be counterclockwise; inner boundary wires should be clockwise