803 lines
27 KiB
Python
803 lines
27 KiB
Python
# Copyright (c) 2022-2024, Manfred Moitzi
|
|
# License: MIT License
|
|
from __future__ import annotations
|
|
from typing import Callable, Type, Any, Sequence, Iterator
|
|
import abc
|
|
|
|
from . import sab, sat, const, hdr
|
|
from .const import Features
|
|
from .abstract import DataLoader, AbstractEntity, DataExporter
|
|
from .type_hints import EncodedData
|
|
from ezdxf.math import Matrix44, Vec3, NULLVEC
|
|
|
|
Factory = Callable[[AbstractEntity], "AcisEntity"]
|
|
|
|
ENTITY_TYPES: dict[str, Type[AcisEntity]] = {}
|
|
INF = float("inf")
|
|
|
|
|
|
def load(data: EncodedData) -> list[Body]:
|
|
"""Returns a list of :class:`Body` entities from :term:`SAT` or :term:`SAB`
|
|
data. Accepts :term:`SAT` data as a single string or a sequence of strings
|
|
and :term:`SAB` data as bytes or bytearray.
|
|
|
|
"""
|
|
if isinstance(data, (bytes, bytearray)):
|
|
return SabLoader.load(data)
|
|
return SatLoader.load(data)
|
|
|
|
|
|
def export_sat(
|
|
bodies: Sequence[Body], version: int = const.DEFAULT_SAT_VERSION
|
|
) -> list[str]:
|
|
"""Export one or more :class:`Body` entities as text based :term:`SAT` data.
|
|
|
|
ACIS version 700 is sufficient for DXF versions R2000, R2004, R2007 and
|
|
R2010, later DXF versions require :term:`SAB` data.
|
|
|
|
Raises:
|
|
ExportError: ACIS structures contain unsupported entities
|
|
InvalidLinkStructure: corrupt link structure
|
|
|
|
"""
|
|
if version < const.MIN_EXPORT_VERSION:
|
|
raise const.ExportError(f"invalid ACIS version: {version}")
|
|
exporter = sat.SatExporter(_setup_export_header(version))
|
|
exporter.header.asm_end_marker = False
|
|
for body in bodies:
|
|
exporter.export(body)
|
|
return exporter.dump_sat()
|
|
|
|
|
|
def export_sab(
|
|
bodies: Sequence[Body], version: int = const.DEFAULT_SAB_VERSION
|
|
) -> bytes:
|
|
"""Export one or more :class:`Body` entities as binary encoded :term:`SAB`
|
|
data.
|
|
|
|
ACIS version 21800 is sufficient for DXF versions R2013 and R2018, earlier
|
|
DXF versions require :term:`SAT` data.
|
|
|
|
Raises:
|
|
ExportError: ACIS structures contain unsupported entities
|
|
InvalidLinkStructure: corrupt link structure
|
|
|
|
"""
|
|
if version < const.MIN_EXPORT_VERSION:
|
|
raise const.ExportError(f"invalid ACIS version: {version}")
|
|
exporter = sab.SabExporter(_setup_export_header(version))
|
|
exporter.header.asm_end_marker = True
|
|
for body in bodies:
|
|
exporter.export(body)
|
|
return exporter.dump_sab()
|
|
|
|
|
|
def _setup_export_header(version) -> hdr.AcisHeader:
|
|
if not const.is_valid_export_version(version):
|
|
raise const.ExportError(f"invalid export version: {version}")
|
|
header = hdr.AcisHeader()
|
|
header.set_version(version)
|
|
return header
|
|
|
|
|
|
def register(cls):
|
|
ENTITY_TYPES[cls.type] = cls
|
|
return cls
|
|
|
|
|
|
class NoneEntity:
|
|
type: str = const.NONE_ENTITY_NAME
|
|
|
|
@property
|
|
def is_none(self) -> bool:
|
|
return self.type == const.NONE_ENTITY_NAME
|
|
|
|
|
|
NONE_REF: Any = NoneEntity()
|
|
|
|
|
|
class AcisEntity(NoneEntity):
|
|
"""Base ACIS entity which also represents unsupported entities.
|
|
|
|
Unsupported entities are entities whose internal structure are not fully
|
|
known or user defined entity types.
|
|
|
|
The content of these unsupported entities is not loaded and lost by
|
|
exporting such entities, therefore exporting unsupported entities raises
|
|
an :class:`ExportError` exception.
|
|
|
|
"""
|
|
|
|
type: str = "unsupported-entity"
|
|
id: int = -1
|
|
attributes: AcisEntity = NONE_REF
|
|
|
|
def __str__(self) -> str:
|
|
return f"{self.type}({self.id})"
|
|
|
|
def load(self, loader: DataLoader, entity_factory: Factory) -> None:
|
|
"""Load the ACIS entity content from `loader`."""
|
|
self.restore_common(loader, entity_factory)
|
|
self.restore_data(loader)
|
|
|
|
def restore_common(self, loader: DataLoader, entity_factory: Factory) -> None:
|
|
"""Load the common part of an ACIS entity."""
|
|
pass
|
|
|
|
def restore_data(self, loader: DataLoader) -> None:
|
|
"""Load the data part of an ACIS entity."""
|
|
pass
|
|
|
|
def export(self, exporter: DataExporter) -> None:
|
|
"""Write the ACIS entity content to `exporter`."""
|
|
self.write_common(exporter)
|
|
self.write_data(exporter)
|
|
|
|
def write_common(self, exporter: DataExporter) -> None:
|
|
"""Write the common part of the ACIS entity.
|
|
|
|
It is not possible to export :class:`Body` entities including
|
|
unsupported entities, doing so would cause data loss or worse data
|
|
corruption!
|
|
|
|
"""
|
|
raise const.ExportError(f"unsupported entity type: {self.type}")
|
|
|
|
def write_data(self, exporter: DataExporter) -> None:
|
|
"""Write the data part of the ACIS entity."""
|
|
pass
|
|
|
|
def entities(self) -> Iterator[AcisEntity]:
|
|
"""Yield all attributes of this entity of type AcisEntity."""
|
|
for e in vars(self).values():
|
|
if isinstance(e, AcisEntity):
|
|
yield e
|
|
|
|
|
|
def restore_entity(
|
|
expected_type: str, loader: DataLoader, entity_factory: Factory
|
|
) -> Any:
|
|
raw_entity = loader.read_ptr()
|
|
if raw_entity.is_null_ptr:
|
|
return NONE_REF
|
|
if raw_entity.name.endswith(expected_type):
|
|
return entity_factory(raw_entity)
|
|
else:
|
|
raise const.ParsingError(
|
|
f"expected entity type '{expected_type}', got '{raw_entity.name}'"
|
|
)
|
|
|
|
|
|
@register
|
|
class Transform(AcisEntity):
|
|
type: str = "transform"
|
|
matrix = Matrix44()
|
|
|
|
def restore_data(self, loader: DataLoader) -> None:
|
|
data = loader.read_transform()
|
|
# insert values of the 4th matrix column (0, 0, 0, 1)
|
|
data.insert(3, 0.0)
|
|
data.insert(7, 0.0)
|
|
data.insert(11, 0.0)
|
|
data.append(1.0)
|
|
self.matrix = Matrix44(data)
|
|
|
|
def write_common(self, exporter: DataExporter) -> None:
|
|
def write_double(value: float):
|
|
data.append(f"{value:g}")
|
|
|
|
data: list[str] = []
|
|
for row in self.matrix.rows():
|
|
write_double(row[0])
|
|
write_double(row[1])
|
|
write_double(row[2])
|
|
test_vector = Vec3(1, 0, 0)
|
|
result = self.matrix.transform_direction(test_vector)
|
|
# A uniform scaling in x- y- and z-axis is assumed:
|
|
write_double(round(result.magnitude, 6)) # scale factor
|
|
is_rotated = not result.normalize().isclose(test_vector)
|
|
data.append("rotate" if is_rotated else "no_rotate")
|
|
data.append("no_reflect")
|
|
data.append("no_shear")
|
|
exporter.write_transform(data)
|
|
|
|
|
|
@register
|
|
class AsmHeader(AcisEntity):
|
|
type: str = "asmheader"
|
|
|
|
def __init__(self, version: str = ""):
|
|
self.version = version
|
|
|
|
def restore_common(self, loader: DataLoader, entity_factory: Factory) -> None:
|
|
self.version = loader.read_str()
|
|
|
|
def write_common(self, exporter: DataExporter) -> None:
|
|
exporter.write_str(self.version)
|
|
|
|
|
|
class SupportsPattern(AcisEntity):
|
|
pattern: Pattern = NONE_REF
|
|
|
|
def restore_common(self, loader: DataLoader, entity_factory: Factory) -> None:
|
|
if loader.version >= Features.PATTERN:
|
|
self.pattern = restore_entity("pattern", loader, entity_factory)
|
|
|
|
def write_common(self, exporter: DataExporter) -> None:
|
|
exporter.write_ptr(self.pattern)
|
|
|
|
|
|
@register
|
|
class Body(SupportsPattern):
|
|
type: str = "body"
|
|
pattern: Pattern = NONE_REF
|
|
lump: Lump = NONE_REF
|
|
wire: Wire = NONE_REF
|
|
transform: Transform = NONE_REF
|
|
|
|
def restore_common(self, loader: DataLoader, entity_factory: Factory) -> None:
|
|
super().restore_common(loader, entity_factory)
|
|
self.lump = restore_entity("lump", loader, entity_factory)
|
|
self.wire = restore_entity("wire", loader, entity_factory)
|
|
self.transform = restore_entity("transform", loader, entity_factory)
|
|
|
|
def write_common(self, exporter: DataExporter) -> None:
|
|
super().write_common(exporter)
|
|
exporter.write_ptr(self.lump)
|
|
exporter.write_ptr(self.wire)
|
|
exporter.write_ptr(self.transform)
|
|
|
|
def append_lump(self, lump: Lump) -> None:
|
|
"""Append a :class:`Lump` entity as last lump."""
|
|
lump.body = self
|
|
if self.lump.is_none:
|
|
self.lump = lump
|
|
else:
|
|
current_lump = self.lump
|
|
while not current_lump.next_lump.is_none:
|
|
current_lump = current_lump.next_lump
|
|
current_lump.next_lump = lump
|
|
|
|
def lumps(self) -> list[Lump]:
|
|
"""Returns all linked :class:`Lump` entities as a list."""
|
|
lumps = []
|
|
current_lump = self.lump
|
|
while not current_lump.is_none:
|
|
lumps.append(current_lump)
|
|
current_lump = current_lump.next_lump
|
|
return lumps
|
|
|
|
|
|
@register
|
|
class Wire(SupportsPattern): # not implemented
|
|
type: str = "wire"
|
|
|
|
|
|
@register
|
|
class Pattern(AcisEntity): # not implemented
|
|
type: str = "pattern"
|
|
|
|
|
|
@register
|
|
class Lump(SupportsPattern):
|
|
type: str = "lump"
|
|
next_lump: Lump = NONE_REF
|
|
shell: Shell = NONE_REF
|
|
body: Body = NONE_REF
|
|
|
|
def restore_common(self, loader: DataLoader, entity_factory: Factory) -> None:
|
|
super().restore_common(loader, entity_factory)
|
|
self.next_lump = restore_entity("lump", loader, entity_factory)
|
|
self.shell = restore_entity("shell", loader, entity_factory)
|
|
self.body = restore_entity("body", loader, entity_factory)
|
|
|
|
def write_common(self, exporter: DataExporter) -> None:
|
|
super().write_common(exporter)
|
|
exporter.write_ptr(self.next_lump)
|
|
exporter.write_ptr(self.shell)
|
|
exporter.write_ptr(self.body)
|
|
|
|
def append_shell(self, shell: Shell) -> None:
|
|
"""Append a :class:`Shell` entity as last shell."""
|
|
shell.lump = self
|
|
if self.shell.is_none:
|
|
self.shell = shell
|
|
else:
|
|
current_shell = self.shell
|
|
while not current_shell.next_shell.is_none:
|
|
current_shell = current_shell.next_shell
|
|
current_shell.next_shell = shell
|
|
|
|
def shells(self) -> list[Shell]:
|
|
"""Returns all linked :class:`Shell` entities as a list."""
|
|
shells = []
|
|
current_shell = self.shell
|
|
while not current_shell.is_none:
|
|
shells.append(current_shell)
|
|
current_shell = current_shell.next_shell
|
|
return shells
|
|
|
|
|
|
@register
|
|
class Shell(SupportsPattern):
|
|
type: str = "shell"
|
|
next_shell: Shell = NONE_REF
|
|
subshell: Subshell = NONE_REF
|
|
face: Face = NONE_REF
|
|
wire: Wire = NONE_REF
|
|
lump: Lump = NONE_REF
|
|
|
|
def restore_common(self, loader: DataLoader, entity_factory: Factory) -> None:
|
|
super().restore_common(loader, entity_factory)
|
|
self.next_shell = restore_entity("next_shell", loader, entity_factory)
|
|
self.subshell = restore_entity("subshell", loader, entity_factory)
|
|
self.face = restore_entity("face", loader, entity_factory)
|
|
self.wire = restore_entity("wire", loader, entity_factory)
|
|
self.lump = restore_entity("lump", loader, entity_factory)
|
|
|
|
def write_common(self, exporter: DataExporter) -> None:
|
|
super().write_common(exporter)
|
|
exporter.write_ptr(self.next_shell)
|
|
exporter.write_ptr(self.subshell)
|
|
exporter.write_ptr(self.face)
|
|
exporter.write_ptr(self.wire)
|
|
exporter.write_ptr(self.lump)
|
|
|
|
def append_face(self, face: Face) -> None:
|
|
"""Append a :class:`Face` entity as last face."""
|
|
face.shell = self
|
|
if self.face.is_none:
|
|
self.face = face
|
|
else:
|
|
current_face = self.face
|
|
while not current_face.next_face.is_none:
|
|
current_face = current_face.next_face
|
|
current_face.next_face = face
|
|
|
|
def faces(self) -> list[Face]:
|
|
"""Returns all linked :class:`Face` entities as a list."""
|
|
faces = []
|
|
current_face = self.face
|
|
while not current_face.is_none:
|
|
faces.append(current_face)
|
|
current_face = current_face.next_face
|
|
return faces
|
|
|
|
|
|
@register
|
|
class Subshell(SupportsPattern): # not implemented
|
|
type: str = "subshell"
|
|
|
|
|
|
@register
|
|
class Face(SupportsPattern):
|
|
type: str = "face"
|
|
next_face: "Face" = NONE_REF
|
|
loop: Loop = NONE_REF
|
|
shell: Shell = NONE_REF
|
|
subshell: Subshell = NONE_REF
|
|
surface: Surface = NONE_REF
|
|
# sense: face normal with respect to the surface
|
|
sense = False # True = reversed; False = forward
|
|
double_sided = False # True = double (hollow body); False = single (solid body)
|
|
containment = False # if double_sided: True = in, False = out
|
|
|
|
def restore_common(self, loader: DataLoader, entity_factory: Factory) -> None:
|
|
super().restore_common(loader, entity_factory)
|
|
self.next_face = restore_entity("face", loader, entity_factory)
|
|
self.loop = restore_entity("loop", loader, entity_factory)
|
|
self.shell = restore_entity("shell", loader, entity_factory)
|
|
self.subshell = restore_entity("subshell", loader, entity_factory)
|
|
self.surface = restore_entity("surface", loader, entity_factory)
|
|
self.sense = loader.read_bool("reversed", "forward")
|
|
self.double_sided = loader.read_bool("double", "single")
|
|
if self.double_sided:
|
|
self.containment = loader.read_bool("in", "out")
|
|
|
|
def write_common(self, exporter: DataExporter) -> None:
|
|
super().write_common(exporter)
|
|
exporter.write_ptr(self.next_face)
|
|
exporter.write_ptr(self.loop)
|
|
exporter.write_ptr(self.shell)
|
|
exporter.write_ptr(self.subshell)
|
|
exporter.write_ptr(self.surface)
|
|
exporter.write_bool(self.sense, "reversed", "forward")
|
|
exporter.write_bool(self.double_sided, "double", "single")
|
|
if self.double_sided:
|
|
exporter.write_bool(self.containment, "in", "out")
|
|
|
|
def append_loop(self, loop: Loop) -> None:
|
|
"""Append a :class:`Loop` entity as last loop."""
|
|
loop.face = self
|
|
if self.loop.is_none:
|
|
self.loop = loop
|
|
else: # order of coedges is important! (right-hand rule)
|
|
current_loop = self.loop
|
|
while not current_loop.next_loop.is_none:
|
|
current_loop = current_loop.next_loop
|
|
current_loop.next_loop = loop
|
|
|
|
def loops(self) -> list[Loop]:
|
|
"""Returns all linked :class:`Loop` entities as a list."""
|
|
loops = []
|
|
current_loop = self.loop
|
|
while not current_loop.is_none:
|
|
loops.append(current_loop)
|
|
current_loop = current_loop.next_loop
|
|
return loops
|
|
|
|
|
|
@register
|
|
class Surface(SupportsPattern):
|
|
type: str = "surface"
|
|
u_bounds = INF, INF
|
|
v_bounds = INF, INF
|
|
|
|
def restore_data(self, loader: DataLoader) -> None:
|
|
self.u_bounds = loader.read_interval(), loader.read_interval()
|
|
self.v_bounds = loader.read_interval(), loader.read_interval()
|
|
|
|
def write_data(self, exporter: DataExporter):
|
|
exporter.write_interval(self.u_bounds[0])
|
|
exporter.write_interval(self.u_bounds[1])
|
|
exporter.write_interval(self.v_bounds[0])
|
|
exporter.write_interval(self.v_bounds[1])
|
|
|
|
@abc.abstractmethod
|
|
def evaluate(self, u: float, v: float) -> Vec3:
|
|
"""Returns the spatial location at the parametric surface for the given
|
|
parameters `u` and `v`.
|
|
|
|
"""
|
|
pass
|
|
|
|
|
|
@register
|
|
class Plane(Surface):
|
|
type: str = "plane-surface"
|
|
origin = Vec3(0, 0, 0)
|
|
normal = Vec3(0, 0, 1) # pointing outside
|
|
u_dir = Vec3(1, 0, 0) # unit vector!
|
|
v_dir = Vec3(0, 1, 0) # unit vector!
|
|
# reverse_v:
|
|
# True: "reverse_v" - the normal vector does not follow the right-hand rule
|
|
# False: "forward_v" - the normal vector follows right-hand rule
|
|
reverse_v = False
|
|
|
|
def restore_common(self, loader: DataLoader, entity_factory: Factory) -> None:
|
|
super().restore_common(loader, entity_factory)
|
|
self.origin = Vec3(loader.read_vec3())
|
|
self.normal = Vec3(loader.read_vec3())
|
|
self.u_dir = Vec3(loader.read_vec3())
|
|
self.reverse_v = loader.read_bool("reverse_v", "forward_v")
|
|
self.update_v_dir()
|
|
|
|
def write_common(self, exporter: DataExporter) -> None:
|
|
super().write_common(exporter)
|
|
exporter.write_loc_vec3(self.origin)
|
|
exporter.write_dir_vec3(self.normal)
|
|
exporter.write_dir_vec3(self.u_dir)
|
|
exporter.write_bool(self.reverse_v, "reverse_v", "forward_v")
|
|
# v_dir is not exported
|
|
|
|
def update_v_dir(self):
|
|
v_dir = self.normal.cross(self.u_dir)
|
|
if self.reverse_v:
|
|
v_dir = -v_dir
|
|
self.v_dir = v_dir
|
|
|
|
def evaluate(self, u: float, v: float) -> Vec3:
|
|
return self.origin + (self.u_dir * u) + (self.v_dir * v)
|
|
|
|
|
|
@register
|
|
class Loop(SupportsPattern):
|
|
type: str = "loop"
|
|
next_loop: Loop = NONE_REF
|
|
coedge: Coedge = NONE_REF
|
|
face: Face = NONE_REF # parent/owner
|
|
|
|
def restore_common(self, loader: DataLoader, entity_factory: Factory) -> None:
|
|
super().restore_common(loader, entity_factory)
|
|
self.next_loop = restore_entity("loop", loader, entity_factory)
|
|
self.coedge = restore_entity("coedge", loader, entity_factory)
|
|
self.face = restore_entity("face", loader, entity_factory)
|
|
|
|
def write_common(self, exporter: DataExporter) -> None:
|
|
super().write_common(exporter)
|
|
exporter.write_ptr(self.next_loop)
|
|
exporter.write_ptr(self.coedge)
|
|
exporter.write_ptr(self.face)
|
|
|
|
def set_coedges(self, coedges: list[Coedge], close=True) -> None:
|
|
"""Set all coedges of a loop at once."""
|
|
assert len(coedges) > 0
|
|
self.coedge = coedges[0]
|
|
next_coedges = coedges[1:]
|
|
prev_coedges = coedges[:-1]
|
|
if close:
|
|
next_coedges.append(coedges[0])
|
|
prev_coedges.insert(0, coedges[-1])
|
|
else:
|
|
next_coedges.append(NONE_REF)
|
|
prev_coedges.insert(0, NONE_REF)
|
|
|
|
for coedge, next, prev in zip(coedges, next_coedges, prev_coedges):
|
|
coedge.loop = self
|
|
coedge.prev_coedge = prev
|
|
coedge.next_coedge = next
|
|
|
|
def coedges(self) -> list[Coedge]:
|
|
"""Returns all linked :class:`Coedge` entities as a list."""
|
|
coedges = []
|
|
|
|
current_coedge = self.coedge
|
|
while not current_coedge.is_none: # open loop if none
|
|
coedges.append(current_coedge)
|
|
current_coedge = current_coedge.next_coedge
|
|
if current_coedge is self.coedge: # circular linked list!
|
|
break # closed loop
|
|
return coedges
|
|
|
|
|
|
@register
|
|
class Coedge(SupportsPattern):
|
|
type: str = "coedge"
|
|
next_coedge: Coedge = NONE_REF
|
|
prev_coedge: Coedge = NONE_REF
|
|
# The partner_coedge points to the coedge of an adjacent face, in a
|
|
# manifold body each coedge has zero (open) or one (closed) partner edge.
|
|
# ACIS supports also non-manifold bodies, so there can be more than one
|
|
# partner coedges which are organized in a circular linked list.
|
|
partner_coedge: Coedge = NONE_REF
|
|
edge: Edge = NONE_REF
|
|
# sense: True = reversed; False = forward;
|
|
# coedge has the same direction as the underlying edge
|
|
sense: bool = True
|
|
loop: Loop = NONE_REF # parent/owner
|
|
unknown: int = 0 # only in SAB file!?
|
|
pcurve: PCurve = NONE_REF
|
|
|
|
def restore_common(self, loader: DataLoader, entity_factory: Factory) -> None:
|
|
super().restore_common(loader, entity_factory)
|
|
self.next_coedge = restore_entity("coedge", loader, entity_factory)
|
|
self.prev_coedge = restore_entity("coedge", loader, entity_factory)
|
|
self.partner_coedge = restore_entity("coedge", loader, entity_factory)
|
|
self.edge = restore_entity("edge", loader, entity_factory)
|
|
self.sense = loader.read_bool("reversed", "forward")
|
|
self.loop = restore_entity("loop", loader, entity_factory)
|
|
self.unknown = loader.read_int(skip_sat=0)
|
|
self.pcurve = restore_entity("pcurve", loader, entity_factory)
|
|
|
|
def write_common(self, exporter: DataExporter) -> None:
|
|
super().write_common(exporter)
|
|
exporter.write_ptr(self.next_coedge)
|
|
exporter.write_ptr(self.prev_coedge)
|
|
exporter.write_ptr(self.partner_coedge)
|
|
exporter.write_ptr(self.edge)
|
|
exporter.write_bool(self.sense, "reversed", "forward")
|
|
exporter.write_ptr(self.loop)
|
|
# TODO: write_int() ?
|
|
exporter.write_int(0, skip_sat=True)
|
|
exporter.write_ptr(self.pcurve)
|
|
|
|
def add_partner_coedge(self, coedge: Coedge) -> None:
|
|
assert coedge.partner_coedge.is_none
|
|
partner_coedge = self.partner_coedge
|
|
if partner_coedge.is_none:
|
|
partner_coedge = self
|
|
# insert new coedge as first partner coedge:
|
|
self.partner_coedge = coedge
|
|
coedge.partner_coedge = partner_coedge
|
|
self.order_partner_coedges()
|
|
|
|
def order_partner_coedges(self) -> None:
|
|
# todo: the referenced faces of non-manifold coedges have to be ordered
|
|
# by the right-hand rule around this edge.
|
|
pass
|
|
|
|
def partner_coedges(self) -> list[Coedge]:
|
|
"""Returns all partner coedges of this coedge without `self`."""
|
|
coedges: list[Coedge] = []
|
|
partner_coedge = self.partner_coedge
|
|
if partner_coedge.is_none:
|
|
return coedges
|
|
while True:
|
|
coedges.append(partner_coedge)
|
|
partner_coedge = partner_coedge.partner_coedge
|
|
if partner_coedge.is_none or partner_coedge is self:
|
|
break
|
|
return coedges
|
|
|
|
|
|
@register
|
|
class Edge(SupportsPattern):
|
|
type: str = "edge"
|
|
|
|
# The parent edge of the start_vertex doesn't have to be this edge!
|
|
start_vertex: Vertex = NONE_REF
|
|
start_param: float = 0.0
|
|
|
|
# The parent edge of the end_vertex doesn't have to be this edge!
|
|
end_vertex: Vertex = NONE_REF
|
|
end_param: float = 0.0
|
|
coedge: Coedge = NONE_REF
|
|
curve: Curve = NONE_REF
|
|
# sense: True = reversed; False = forward;
|
|
# forward: edge has the same direction as the underlying curve
|
|
sense: bool = False
|
|
convexity: str = "unknown"
|
|
|
|
def restore_common(self, loader: DataLoader, entity_factory: Factory) -> None:
|
|
super().restore_common(loader, entity_factory)
|
|
self.start_vertex = restore_entity("vertex", loader, entity_factory)
|
|
if loader.version >= Features.TOL_MODELING:
|
|
self.start_param = loader.read_double()
|
|
self.end_vertex = restore_entity("vertex", loader, entity_factory)
|
|
if loader.version >= Features.TOL_MODELING:
|
|
self.end_param = loader.read_double()
|
|
self.coedge = restore_entity("coedge", loader, entity_factory)
|
|
self.curve = restore_entity("curve", loader, entity_factory)
|
|
self.sense = loader.read_bool("reversed", "forward")
|
|
if loader.version >= Features.TOL_MODELING:
|
|
self.convexity = loader.read_str()
|
|
|
|
def write_common(self, exporter: DataExporter) -> None:
|
|
# write support >= version 700 only
|
|
super().write_common(exporter)
|
|
exporter.write_ptr(self.start_vertex)
|
|
exporter.write_double(self.start_param)
|
|
exporter.write_ptr(self.end_vertex)
|
|
exporter.write_double(self.end_param)
|
|
exporter.write_ptr(self.coedge)
|
|
exporter.write_ptr(self.curve)
|
|
exporter.write_bool(self.sense, "reversed", "forward")
|
|
exporter.write_str(self.convexity)
|
|
|
|
|
|
@register
|
|
class PCurve(SupportsPattern): # not implemented
|
|
type: str = "pcurve"
|
|
|
|
|
|
@register
|
|
class Vertex(SupportsPattern):
|
|
type: str = "vertex"
|
|
edge: Edge = NONE_REF
|
|
ref_count: int = 0 # only in SAB files
|
|
point: Point = NONE_REF
|
|
|
|
def restore_common(self, loader: DataLoader, entity_factory: Factory) -> None:
|
|
super().restore_common(loader, entity_factory)
|
|
self.edge = restore_entity("edge", loader, entity_factory)
|
|
self.ref_count = loader.read_int(skip_sat=0)
|
|
self.point = restore_entity("point", loader, entity_factory)
|
|
|
|
def write_common(self, exporter: DataExporter) -> None:
|
|
super().write_common(exporter)
|
|
exporter.write_ptr(self.edge)
|
|
exporter.write_int(self.ref_count, skip_sat=True)
|
|
exporter.write_ptr(self.point)
|
|
|
|
|
|
@register
|
|
class Curve(SupportsPattern):
|
|
type: str = "curve"
|
|
bounds = INF, INF
|
|
|
|
def restore_data(self, loader: DataLoader) -> None:
|
|
self.bounds = loader.read_interval(), loader.read_interval()
|
|
|
|
def write_data(self, exporter: DataExporter) -> None:
|
|
exporter.write_interval(self.bounds[0])
|
|
exporter.write_interval(self.bounds[1])
|
|
|
|
@abc.abstractmethod
|
|
def evaluate(self, param: float) -> Vec3:
|
|
"""Returns the spatial location at the parametric curve for the given
|
|
parameter.
|
|
|
|
"""
|
|
pass
|
|
|
|
|
|
@register
|
|
class StraightCurve(Curve):
|
|
type: str = "straight-curve"
|
|
origin = Vec3(0, 0, 0)
|
|
direction = Vec3(1, 0, 0)
|
|
|
|
def restore_data(self, loader: DataLoader) -> None:
|
|
self.origin = Vec3(loader.read_vec3())
|
|
self.direction = Vec3(loader.read_vec3())
|
|
super().restore_data(loader)
|
|
|
|
def write_data(self, exporter: DataExporter) -> None:
|
|
exporter.write_loc_vec3(self.origin)
|
|
exporter.write_dir_vec3(self.direction)
|
|
super().write_data(exporter)
|
|
|
|
def evaluate(self, param: float) -> Vec3:
|
|
return self.origin + (self.direction * param)
|
|
|
|
|
|
@register
|
|
class Point(SupportsPattern):
|
|
type: str = "point"
|
|
location: Vec3 = NULLVEC
|
|
|
|
def restore_data(self, loader: DataLoader) -> None:
|
|
self.location = Vec3(loader.read_vec3())
|
|
|
|
def write_data(self, exporter: DataExporter) -> None:
|
|
exporter.write_loc_vec3(self.location)
|
|
|
|
|
|
class FileLoader(abc.ABC):
|
|
records: Sequence[sat.SatEntity | sab.SabEntity]
|
|
|
|
def __init__(self, version: int):
|
|
self.entities: dict[int, AcisEntity] = {}
|
|
self.version: int = version
|
|
|
|
def entity_factory(self, raw_entity: AbstractEntity) -> AcisEntity:
|
|
uid = id(raw_entity)
|
|
try:
|
|
return self.entities[uid]
|
|
except KeyError: # create a new entity
|
|
entity = ENTITY_TYPES.get(raw_entity.name, AcisEntity)()
|
|
self.entities[uid] = entity
|
|
return entity
|
|
|
|
def bodies(self) -> list[Body]:
|
|
# noinspection PyTypeChecker
|
|
return [e for e in self.entities.values() if isinstance(e, Body)]
|
|
|
|
def load_entities(self):
|
|
entity_factory = self.entity_factory
|
|
|
|
for raw_entity in self.records:
|
|
entity = entity_factory(raw_entity)
|
|
entity.id = raw_entity.id
|
|
attributes = raw_entity.attributes
|
|
if not attributes.is_null_ptr:
|
|
entity.attributes = entity_factory(attributes)
|
|
data_loader = self.make_data_loader(raw_entity.data)
|
|
entity.load(data_loader, entity_factory)
|
|
|
|
@abc.abstractmethod
|
|
def make_data_loader(self, data: list[Any]) -> DataLoader:
|
|
pass
|
|
|
|
|
|
class SabLoader(FileLoader):
|
|
def __init__(self, data: bytes | bytearray):
|
|
builder = sab.parse_sab(data)
|
|
super().__init__(builder.header.version)
|
|
self.records = builder.entities
|
|
|
|
def make_data_loader(self, data: list[Any]) -> DataLoader:
|
|
return sab.SabDataLoader(data, self.version)
|
|
|
|
@classmethod
|
|
def load(cls, data: bytes | bytearray) -> list[Body]:
|
|
loader = cls(data)
|
|
loader.load_entities()
|
|
return loader.bodies()
|
|
|
|
|
|
class SatLoader(FileLoader):
|
|
def __init__(self, data: str | Sequence[str]):
|
|
builder = sat.parse_sat(data)
|
|
super().__init__(builder.header.version)
|
|
self.records = builder.entities
|
|
|
|
def make_data_loader(self, data: list[Any]) -> DataLoader:
|
|
return sat.SatDataLoader(data, self.version)
|
|
|
|
@classmethod
|
|
def load(cls, data: str | Sequence[str]) -> list[Body]:
|
|
loader = cls(data)
|
|
loader.load_entities()
|
|
return loader.bodies()
|