feat: 拆分 STEP 编辑器并完善最小系统
将原来的 main.py/step_model.py 拆分为 step_editor 包,补充测量、同域高亮、模型修复、历史导出、槽宽/凸台/边长等 MVP 编辑能力,并更新 README 和忽略规则。
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Transform
|
||||
from OCC.Core.IFSelect import IFSelect_RetDone
|
||||
from OCC.Core.Interface import Interface_Static
|
||||
from OCC.Core.STEPCAFControl import STEPCAFControl_Reader
|
||||
from OCC.Core.STEPControl import STEPControl_AsIs, STEPControl_Reader, STEPControl_Writer
|
||||
from OCC.Core.TDF import TDF_Label, TDF_LabelSequence
|
||||
from OCC.Core.TDocStd import TDocStd_Document
|
||||
from OCC.Core.TopLoc import TopLoc_Location
|
||||
from OCC.Core.TopoDS import TopoDS_Shape
|
||||
from OCC.Core.XCAFDoc import XCAFDoc_DocumentTool
|
||||
|
||||
from .geometry_utils import _compound_from_shapes, _repair_shape, _unify_same_domain_shape
|
||||
from .model_types import PartNode
|
||||
|
||||
|
||||
def _load_with_xcaf(path: Path, product_names: list[str]) -> tuple[list[PartNode], TopoDS_Shape]:
|
||||
doc = TDocStd_Document("pythonocc-step-document")
|
||||
shape_tool = XCAFDoc_DocumentTool.ShapeTool(doc.Main())
|
||||
|
||||
reader = STEPCAFControl_Reader()
|
||||
reader.SetColorMode(True)
|
||||
reader.SetLayerMode(True)
|
||||
reader.SetNameMode(True)
|
||||
reader.SetMatMode(True)
|
||||
reader.SetGDTMode(True)
|
||||
status = reader.ReadFile(str(path))
|
||||
if status != IFSelect_RetDone:
|
||||
raise ValueError(f"Could not read STEP file: {path}")
|
||||
if not reader.Transfer(doc):
|
||||
raise ValueError(f"Could not transfer STEP document: {path}")
|
||||
|
||||
parts: list[PartNode] = []
|
||||
free_shapes = TDF_LabelSequence()
|
||||
shape_tool.GetFreeShapes(free_shapes)
|
||||
|
||||
def next_name(label: TDF_Label, index: int) -> str:
|
||||
label_name = str(label.GetLabelName()).strip()
|
||||
if label_name:
|
||||
return label_name
|
||||
if index - 1 < len(product_names):
|
||||
return product_names[index - 1]
|
||||
return f"Part {index}"
|
||||
|
||||
def add_node(
|
||||
name: str,
|
||||
kind: str,
|
||||
shape: TopoDS_Shape,
|
||||
parent_id: int | None,
|
||||
depth: int,
|
||||
path_text: str,
|
||||
) -> PartNode:
|
||||
node = PartNode(len(parts) + 1, name, kind, shape, parent_id, depth, path_text)
|
||||
parts.append(node)
|
||||
return node
|
||||
|
||||
def transformed_shape(label: TDF_Label, locations: list[TopLoc_Location]) -> TopoDS_Shape:
|
||||
shape = shape_tool.GetShape(label)
|
||||
if shape.IsNull() or not locations:
|
||||
return shape
|
||||
location = TopLoc_Location()
|
||||
for loc in locations:
|
||||
location = location.Multiplied(loc)
|
||||
return BRepBuilderAPI_Transform(shape, location.Transformation()).Shape()
|
||||
|
||||
def walk(label: TDF_Label, parent_id: int | None, depth: int, locations: list[TopLoc_Location], path_names: list[str]):
|
||||
name = next_name(label, len(parts) + 1)
|
||||
label_path = " / ".join(path_names + [name])
|
||||
|
||||
if shape_tool.IsAssembly(label):
|
||||
node = add_node(name, "assembly", transformed_shape(label, locations), parent_id, depth, label_path)
|
||||
components = TDF_LabelSequence()
|
||||
shape_tool.GetComponents(label, components)
|
||||
for i in range(1, components.Length() + 1):
|
||||
component = components.Value(i)
|
||||
if shape_tool.IsReference(component):
|
||||
referred = TDF_Label()
|
||||
shape_tool.GetReferredShape(component, referred)
|
||||
loc = shape_tool.GetLocation(component)
|
||||
walk(referred, node.id, depth + 1, locations + [loc], path_names + [name])
|
||||
else:
|
||||
walk(component, node.id, depth + 1, locations, path_names + [name])
|
||||
return
|
||||
|
||||
if shape_tool.IsSimpleShape(label) or shape_tool.IsShape(label):
|
||||
add_node(name, "part", transformed_shape(label, locations), parent_id, depth, label_path)
|
||||
|
||||
for i in range(1, free_shapes.Length() + 1):
|
||||
walk(free_shapes.Value(i), None, 0, [], [])
|
||||
|
||||
display_shapes = [p.shape for p in parts if p.kind == "part" and not p.shape.IsNull()]
|
||||
if not display_shapes:
|
||||
display_shapes = [p.shape for p in parts if not p.shape.IsNull()]
|
||||
return parts, _compound_from_shapes(display_shapes)
|
||||
|
||||
|
||||
def _load_plain_step(path: Path) -> TopoDS_Shape:
|
||||
reader = STEPControl_Reader()
|
||||
status = reader.ReadFile(str(path))
|
||||
if status != IFSelect_RetDone:
|
||||
raise ValueError(f"Could not read STEP file: {path}")
|
||||
if not reader.TransferRoots():
|
||||
raise ValueError(f"Could not transfer STEP roots: {path}")
|
||||
return reader.Shape()
|
||||
|
||||
|
||||
def _parse_product_names(path: Path) -> list[str]:
|
||||
text = path.read_text(errors="ignore")
|
||||
names = re.findall(r"PRODUCT\('((?:''|[^'])*)'", text)
|
||||
return [name.replace("''", "'") for name in names if name.strip()]
|
||||
|
||||
|
||||
def _write_step(shape: TopoDS_Shape, filename: Path) -> None:
|
||||
if shape.IsNull():
|
||||
raise ValueError("Cannot export a null shape.")
|
||||
filename.parent.mkdir(parents=True, exist_ok=True)
|
||||
Interface_Static.SetCVal("write.step.schema", "AP214IS")
|
||||
writer = STEPControl_Writer()
|
||||
writer.Transfer(shape, STEPControl_AsIs)
|
||||
status = writer.Write(str(filename))
|
||||
if status != IFSelect_RetDone:
|
||||
raise IOError(f"Could not write STEP file: {filename}")
|
||||
|
||||
|
||||
def _prepare_shape_for_step_export(shape: TopoDS_Shape) -> TopoDS_Shape:
|
||||
if shape.IsNull():
|
||||
return shape
|
||||
try:
|
||||
repaired = _repair_shape(shape)
|
||||
unified = _unify_same_domain_shape(repaired)
|
||||
repaired_unified = _repair_shape(unified)
|
||||
if repaired_unified.IsNull():
|
||||
return shape
|
||||
return repaired_unified
|
||||
except Exception:
|
||||
return shape
|
||||
Reference in New Issue
Block a user