This commit is contained in:
Christian Anetzberger
2026-01-22 20:23:51 +01:00
commit a197de9456
4327 changed files with 1235205 additions and 0 deletions

View File

@@ -0,0 +1,106 @@
# Copyright (C) 2011-2023, Manfred Moitzi
# License: MIT License
"""Ezdxf is an interface library for the DXF file format.
The package is designed to facilitate the creation and manipulation of DXF
documents, with compatibility across various DXF versions. It empowers users to
seamlessly load and edit DXF files while preserving all content, except for comments.
Any unfamiliar DXF tags encountered in the document are gracefully ignored but retained
for future modifications. This feature enables the processing of DXF documents
containing data from third-party applications without any loss of valuable information.
"""
from typing import TextIO, Optional
import sys
import os
from .version import version, __version__
VERSION = __version__
__author__ = "mozman <me@mozman.at>"
TRUE_STATE = {"True", "true", "On", "on", "1"}
PYPY = hasattr(sys, "pypy_version_info")
PYPY_ON_WINDOWS = sys.platform.startswith("win") and PYPY
# name space imports - do not remove
from ezdxf._options import options, config_files
from ezdxf.colors import (
int2rgb,
rgb2int,
transparency2float,
float2transparency,
)
from ezdxf.enums import InsertUnits
from ezdxf.lldxf import const
from ezdxf.lldxf.validator import is_dxf_file, is_dxf_stream
from ezdxf.filemanagement import readzip, new, read, readfile, decode_base64
from ezdxf.tools.standards import (
setup_linetypes,
setup_styles,
setup_dimstyles,
setup_dimstyle,
)
from ezdxf.tools import pattern
from ezdxf.render.arrows import ARROWS
from ezdxf.lldxf.const import (
DXFError,
DXFStructureError,
DXFVersionError,
DXFTableEntryError,
DXFAppDataError,
DXFXDataError,
DXFAttributeError,
DXFValueError,
DXFKeyError,
DXFIndexError,
DXFTypeError,
DXFBlockInUseError,
InvalidGeoDataException,
DXF12,
DXF2000,
DXF2004,
DXF2007,
DXF2010,
DXF2013,
DXF2018,
)
# name space imports - do not remove
import codecs
from ezdxf.lldxf.encoding import (
dxf_backslash_replace,
has_dxf_unicode,
decode_dxf_unicode,
)
# setup DXF unicode encoder -> '\U+nnnn'
codecs.register_error("dxfreplace", dxf_backslash_replace)
EZDXF_TEST_FILES = options.test_files
YES_NO = {True: "yes", False: "no"}
def print_config(verbose: bool = False, stream: Optional[TextIO] = None) -> None:
from pathlib import Path
if stream is None:
stream = sys.stdout
stream.writelines(
[
f"ezdxf {__version__} from {Path(__file__).parent}\n",
f"Python version: {sys.version}\n",
f"using C-extensions: {YES_NO[options.use_c_ext]}\n",
]
)
if verbose:
stream.write("\nConfiguration:\n")
options.write(stream)
stream.write("\nEnvironment Variables:\n")
for v in options.CONFIG_VARS:
stream.write(f"{v}={os.environ.get(v, '')}\n")
stream.write("\nLoaded Config Files:\n")
for path in options.loaded_config_files:
stream.write(str(path.absolute()) + "\n")

View File

@@ -0,0 +1,120 @@
# Copyright (c) 2021-2023, Manfred Moitzi
# License: MIT License
import sys
import argparse
from pathlib import Path
from ezdxf import options, print_config
from ezdxf import commands
from ezdxf.fonts import fonts
YES_NO = {True: "yes", False: "no"}
options.set(options.CORE, "LOAD_PROXY_GRAPHICS", "true")
def add_common_arguments(parser):
parser.add_argument(
"-V",
"--version",
action="store_true",
help="show version and exit",
)
parser.add_argument(
"-f",
"--fonts",
action="store_true",
help="rebuild system font cache and print all fonts found",
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="give more output",
)
parser.add_argument(
"--config",
action="store",
help="path to a config file",
)
parser.add_argument(
"--log",
action="store",
help='path to a verbose appending log, "stderr" logs to the '
"standard error stream",
)
def print_version(verbose=False):
print_config(verbose=verbose, stream=sys.stdout)
def print_available_fonts(verbose=False):
from ezdxf.fonts import fonts
print("Rebuilding system font cache.")
fonts.build_system_font_cache()
fonts.font_manager.print_available_fonts(verbose)
def setup_log(args):
import logging
from datetime import datetime
from io import StringIO
level = "DEBUG" if args.verbose else "INFO"
if args.log.lower() == "stderr":
logging.basicConfig(stream=sys.stderr, level=level)
else:
logging.basicConfig(filename=args.log, level=level)
print(f'Appending logs to file "{args.log}", logging level: {level}\n')
logger = logging.getLogger("ezdxf")
logger.info("***** Launch time: " + datetime.now().isoformat() + " *****")
if args.verbose:
s = StringIO()
print_config(verbose=True, stream=s)
logger.info("configuration\n" + s.getvalue())
DESCRIPTION = """
Command launcher for the Python package "ezdxf": https://pypi.org/project/ezdxf/
"""
def main():
parser = argparse.ArgumentParser(
"ezdxf",
description=DESCRIPTION,
)
add_common_arguments(parser)
subparsers = parser.add_subparsers(dest="command")
commands.add_parsers(subparsers)
args = parser.parse_args(sys.argv[1:])
help_ = True
if args.config:
config = Path(args.config)
if config.exists():
options.read_file(args.config)
if args.verbose:
print(f'using config file: "{config}"')
else:
print(f'config file "{config}" not found')
if args.log:
setup_log(args)
if args.version:
print_version(verbose=args.verbose)
help_ = False
if args.fonts:
print_available_fonts(args.verbose)
help_ = False
run = commands.get(args.command)
if run:
# For the case automatic font loading is disabled:
fonts.load()
run(args)
elif help_:
parser.print_help()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,335 @@
# Copyright (c) 2011-2023, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TextIO, Sequence
import os
import sys
from pathlib import Path
from configparser import ConfigParser
# Recommended uses of the global object "options":
# import ezdxf
# value = ezdxf.options.<attribute>
#
# alternative:
# from ezdxf._options import options
TRUE_STATE = {"True", "true", "On", "on", "1"}
CORE = "core"
BROWSE_COMMAND = "browse-command"
VIEW_COMMAND = "view-command"
DRAW_COMMAND = "draw-command"
EZDXF_INI = "ezdxf.ini"
EZDXF = "ezdxf"
XDG_CONFIG_HOME = "XDG_CONFIG_HOME"
CONFIG_DIRECTORY = ".config"
ODAFC_ADDON = "odafc-addon"
OPENSCAD_ADDON = "openscad-addon"
DRAWING_ADDON = "drawing-addon"
DIR_SEPARATOR = "\n"
def xdg_path(xdg_var: str, directory: str) -> Path:
xdg_home = os.environ.get(xdg_var)
if xdg_home:
# should default to $HOME/<directory> e.g. $HOME/.config
home = Path(xdg_home).expanduser()
else:
# replicate structure
home = Path("~").expanduser() / directory
return home / EZDXF
def config_home_path() -> Path:
return xdg_path(XDG_CONFIG_HOME, CONFIG_DIRECTORY)
def default_config_files() -> list[Path]:
config_paths = [
config_home_path() / EZDXF_INI,
Path(f"./{EZDXF_INI}"),
]
return config_paths
def default_config() -> ConfigParser:
config = ConfigParser()
config[CORE] = {
"DEFAULT_DIMENSION_TEXT_STYLE": "OpenSansCondensed-Light",
"TEST_FILES": "",
"SUPPORT_DIRS": "",
"LOAD_PROXY_GRAPHICS": "true",
"STORE_PROXY_GRAPHICS": "true",
"LOG_UNPROCESSED_TAGS": "false",
"FILTER_INVALID_XDATA_GROUP_CODES": "true",
"WRITE_FIXED_META_DATA_FOR_TESTING": "false",
"DISABLE_C_EXT": "false",
}
config[BROWSE_COMMAND] = {
"TEXT_EDITOR": r'"C:\Program Files\Notepad++\notepad++.exe" '
r'"{filename}" -n{num}',
"ICON_SIZE": "32",
}
config[ODAFC_ADDON] = {
"WIN_EXEC_PATH": r'"C:\Program Files\ODA\ODAFileConverter\ODAFileConverter.exe"',
"UNIX_EXEC_PATH": "",
}
config[OPENSCAD_ADDON] = {
"WIN_EXEC_PATH": r'"C:\Program Files\OpenSCAD\openscad.exe"'
}
config[DRAWING_ADDON] = {
# These options are just for testing scenarios!
"TRY_PYSIDE6": "true",
"TRY_PYQT5": "true",
# Order for resolving SHX fonts: 1. "t"=TrueType; 2. "s"=SHX; 3. "l"=LFF
"SHX_RESOLVE_ORDER": "tsl",
}
return config
def config_files() -> list[Path]:
# Loading order for config files:
# 1. user home directory:
# "$XDG_CONFIG_HOME/ezdxf/ezdxf.ini" or
# "~/.config/ezdxf/ezdxf.ini"
# 2. current working directory "./ezdxf.ini"
# 3. config file specified by EZDXF_CONFIG_FILE
paths = default_config_files()
env_cfg = os.getenv("EZDXF_CONFIG_FILE", "")
if env_cfg:
paths.append(Path(env_cfg))
return paths
def load_config_files(paths: list[Path]) -> ConfigParser:
config = default_config()
try:
config.read(paths, encoding="utf8")
except UnicodeDecodeError as e:
print(str(e))
print(f"Paths: {paths}")
print("Maybe a file with UTF16 LE-BOM encoding. (Powershell!!!)")
exit(1)
# environment variables override config files
for name, env_name in [
("TEST_FILES", "EZDXF_TEST_FILES"),
("DISABLE_C_EXT", "EZDXF_DISABLE_C_EXT"),
]:
value = os.environ.get(env_name, "")
if value:
config[CORE][name] = value
return config
def boolstr(value: bool) -> str:
return str(value).lower()
class Options:
CORE = CORE
BROWSE_COMMAND = BROWSE_COMMAND
VIEW_COMMAND = VIEW_COMMAND
DRAW_COMMAND = DRAW_COMMAND
CONFIG_VARS = [
"EZDXF_DISABLE_C_EXT",
"EZDXF_TEST_FILES",
"EZDXF_CONFIG_FILE",
]
def __init__(self) -> None:
paths = config_files()
self._loaded_paths: list[Path] = [p for p in paths if p.exists()]
self._config = load_config_files(paths)
# needs fast access:
self.log_unprocessed_tags = True
# Activate/deactivate Matplotlib support (e.g. for testing)
self._use_c_ext = False # set ezdxf.acc.__init__!
self.debug = False
self.update_cached_options()
def set(self, section: str, key: str, value: str) -> None:
self._config.set(section, key, value)
def get(self, section: str, key: str, default: str = "") -> str:
return self._config.get(section, key, fallback=default)
def get_bool(self, section: str, key: str, default: bool = False) -> bool:
return self._config.getboolean(section, key, fallback=default)
def get_int(self, section: str, key: str, default: int = 0) -> int:
return self._config.getint(section, key, fallback=default)
def get_float(self, section: str, key: str, default: float = 0.0) -> float:
return self._config.getfloat(section, key, fallback=default)
def update_cached_options(self) -> None:
self.log_unprocessed_tags = self.get_bool(
Options.CORE, "LOG_UNPROCESSED_TAGS", default=True
)
def rewrite_cached_options(self):
# rewrite cached options
self._config.set(
Options.CORE,
"LOG_UNPROCESSED_TAGS",
boolstr(self.log_unprocessed_tags),
)
@property
def loaded_config_files(self) -> tuple[Path, ...]:
return tuple(self._loaded_paths)
def read_file(self, filename: str) -> None:
"""Append content from config file `filename`, but does not reset the
configuration.
"""
try:
self._config.read(filename)
except IOError as e:
print(str(e))
else:
self._loaded_paths.append(Path(filename))
self.update_cached_options()
def write(self, fp: TextIO) -> None:
"""Write current configuration into given file object, the file object
must be a writeable text file with 'utf8' encoding.
"""
self.rewrite_cached_options()
try:
self._config.write(fp)
except IOError as e:
print(str(e))
def write_file(self, filename: str = EZDXF_INI) -> None:
"""Write current configuration into file `filename`."""
with open(os.path.expanduser(filename), "wt", encoding="utf8") as fp:
self.write(fp)
@property
def filter_invalid_xdata_group_codes(self) -> bool:
return self.get_bool(CORE, "FILTER_INVALID_XDATA_GROUP_CODES", default=True)
@property
def default_dimension_text_style(self) -> str:
return self.get(
CORE,
"DEFAULT_DIMENSION_TEXT_STYLE",
default="OpenSansCondensed-Light",
)
@default_dimension_text_style.setter
def default_dimension_text_style(self, style: str) -> None:
self.set(
CORE,
"DEFAULT_DIMENSION_TEXT_STYLE",
style,
)
@property
def support_dirs(self) -> list[str]:
return [d for d in self.get(CORE, "SUPPORT_DIRS", "").split(DIR_SEPARATOR) if d]
@support_dirs.setter
def support_dirs(self, support_dirs: Sequence[str]) -> None:
self.set(CORE, "SUPPORT_DIRS", DIR_SEPARATOR.join(support_dirs))
@property
def test_files(self) -> str:
return os.path.expanduser(self.get(CORE, "TEST_FILES"))
@property
def test_files_path(self) -> Path:
return Path(self.test_files)
@property
def load_proxy_graphics(self) -> bool:
return self.get_bool(CORE, "LOAD_PROXY_GRAPHICS", default=True)
@load_proxy_graphics.setter
def load_proxy_graphics(self, value: bool) -> None:
self.set(CORE, "LOAD_PROXY_GRAPHICS", boolstr(value))
@property
def store_proxy_graphics(self) -> bool:
return self.get_bool(CORE, "STORE_PROXY_GRAPHICS", default=True)
@store_proxy_graphics.setter
def store_proxy_graphics(self, value: bool) -> None:
self.set(CORE, "STORE_PROXY_GRAPHICS", boolstr(value))
@property
def write_fixed_meta_data_for_testing(self) -> bool:
# Enable this option to always create same meta data for testing
# scenarios, e.g. to use a diff like tool to compare DXF documents.
return self.get_bool(CORE, "WRITE_FIXED_META_DATA_FOR_TESTING", default=False)
@write_fixed_meta_data_for_testing.setter
def write_fixed_meta_data_for_testing(self, state: bool) -> None:
self.set(CORE, "write_fixed_meta_data_for_testing", boolstr(state))
@property
def disable_c_ext(self) -> bool:
"""Disable C-extensions if ``True``."""
return self.get_bool(CORE, "DISABLE_C_EXT", default=False)
@property
def use_c_ext(self) -> bool:
"""Returns ``True`` if the C-extensions are in use."""
return self._use_c_ext
def preserve_proxy_graphics(self, state: bool = True) -> None:
"""Enable/disable proxy graphic load/store support."""
value = boolstr(state)
self.set(CORE, "LOAD_PROXY_GRAPHICS", value)
self.set(CORE, "STORE_PROXY_GRAPHICS", value)
def print(self):
"""Print current configuration to `stdout`."""
self._config.write(sys.stdout)
def write_home_config(self):
"""Write current configuration into file "~/.config/ezdxf/ezdxf.ini" or
"XDG_CONFIG_HOME/ezdxf/ezdxf.ini".
"""
home_path = config_home_path()
if not home_path.exists():
try:
home_path.mkdir(parents=True)
except IOError as e:
print(str(e))
return
filename = str(home_path / EZDXF_INI)
try:
self.write_file(filename)
except IOError as e:
print(str(e))
else:
print(f"created config file: '{filename}'")
def reset(self):
self._loaded_paths = []
self._config = default_config()
self.update_cached_options()
@staticmethod
def delete_default_config_files():
for file in default_config_files():
if file.exists():
try:
file.unlink()
print(f"deleted config file: '{file}'")
except IOError as e:
print(str(e))
@staticmethod
def xdg_path(xdg_var: str, directory: str) -> Path:
return xdg_path(xdg_var, directory)
# Global Options
options = Options()

View File

@@ -0,0 +1,39 @@
# Copyright (c) 2020-2024, Manfred Moitzi
# License: MIT License
import sys
from ezdxf._options import options
# Set environment variable EZDXF_DISABLE_C_EXT to '1' or 'True' to disable
# the usage of C extensions implemented by Cython.
#
# Important: If you change the EZDXF_DISABLE_C_EXT state, you have to restart
# the Python interpreter, because C extension integration is done at the
# ezdxf import!
#
# Config files:
# Section: core
# Key: disable_c_ext = 1
#
# Direct imports from the C extension modules can not be disabled,
# just the usage by the ezdxf core package.
# For an example see ezdxf.math.__init__, if you import Vec3 from ezdxf.math
# the implementation depends on DISABLE_C_EXT and the existence of the C
# extension, but if you import Vec3 from ezdxf.math.vectors, you always get
# the Python implementation.
USE_C_EXT = not options.disable_c_ext
# C-extensions are always disabled for pypy because JIT compiled Python code is
# much faster!
PYPY = hasattr(sys, 'pypy_version_info')
if PYPY:
USE_C_EXT = False
if USE_C_EXT:
try:
from ezdxf.acc import vector
except ImportError:
USE_C_EXT = False
# set actual state of C-extension usage
options._use_c_ext = USE_C_EXT

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,222 @@
# cython: language_level=3
# Copyright (c) 2021-2024 Manfred Moitzi
# License: MIT License
from typing import TYPE_CHECKING, Sequence
from .vector cimport Vec3, isclose, v3_dist, v3_lerp, v3_add, Vec2
from .matrix44 cimport Matrix44
import warnings
if TYPE_CHECKING:
from ezdxf.math import UVec
__all__ = ['Bezier3P']
cdef extern from "constants.h":
const double ABS_TOL
const double REL_TOL
cdef double RECURSION_LIMIT = 1000
cdef class Bezier3P:
cdef:
FastQuadCurve curve # pyright: ignore
readonly Vec3 start_point
Vec3 cp1
readonly Vec3 end_point
def __cinit__(self, defpoints: Sequence[UVec]):
if not isinstance(defpoints[0], (Vec2, Vec3)):
warnings.warn(
DeprecationWarning,
"Bezier3P requires defpoints of type Vec2 or Vec3 in the future",
)
if len(defpoints) == 3:
self.start_point = Vec3(defpoints[0])
self.cp1 = Vec3(defpoints[1])
self.end_point = Vec3(defpoints[2])
self.curve = FastQuadCurve(
self.start_point,
self.cp1,
self.end_point
)
else:
raise ValueError("Three control points required.")
@property
def control_points(self) -> tuple[Vec3, Vec3, Vec3]:
return self.start_point, self.cp1, self.end_point
def __reduce__(self):
return Bezier3P, (self.control_points,)
def point(self, double t) -> Vec3:
if 0.0 <= t <= 1.0:
return self.curve.point(t)
else:
raise ValueError("t not in range [0 to 1]")
def tangent(self, double t) -> Vec3:
if 0.0 <= t <= 1.0:
return self.curve.tangent(t)
else:
raise ValueError("t not in range [0 to 1]")
def approximate(self, int segments) -> list[Vec3]:
cdef double delta_t
cdef int segment
cdef list points = [self.start_point]
if segments < 1:
raise ValueError(segments)
delta_t = 1.0 / segments
for segment in range(1, segments):
points.append(self.point(delta_t * segment))
points.append(self.end_point)
return points
def flattening(self, double distance, int segments = 4) -> list[Vec3]:
cdef double dt = 1.0 / segments
cdef double t0 = 0.0, t1
cdef _Flattening f = _Flattening(self, distance)
cdef Vec3 start_point = self.start_point
cdef Vec3 end_point
while t0 < 1.0:
t1 = t0 + dt
if isclose(t1, 1.0, REL_TOL, ABS_TOL):
end_point = self.end_point
t1 = 1.0
else:
end_point = self.curve.point(t1)
f.reset_recursion_check()
f.flatten(start_point, end_point, t0, t1)
if f.has_recursion_error():
raise RecursionError(
"Bezier3P flattening error, check for very large coordinates"
)
t0 = t1
start_point = end_point
return f.points
def approximated_length(self, segments: int = 128) -> float:
cdef double length = 0.0
cdef bint start_flag = 0
cdef Vec3 prev_point, point
for point in self.approximate(segments):
if start_flag:
length += v3_dist(prev_point, point)
else:
start_flag = 1
prev_point = point
return length
def reverse(self) -> Bezier3P:
return Bezier3P((self.end_point, self.cp1, self.start_point))
def transform(self, Matrix44 m) -> Bezier3P:
return Bezier3P(tuple(m.transform_vertices(self.control_points)))
cdef class _Flattening:
cdef FastQuadCurve curve # pyright: ignore
cdef double distance
cdef list points
cdef int _recursion_level
cdef int _recursion_error
def __cinit__(self, Bezier3P curve, double distance):
self.curve = curve.curve
self.distance = distance
self.points = [curve.start_point]
self._recursion_level = 0
self._recursion_error = 0
cdef has_recursion_error(self):
return self._recursion_error
cdef reset_recursion_check(self):
self._recursion_level = 0
self._recursion_error = 0
cdef flatten(
self,
Vec3 start_point,
Vec3 end_point,
double start_t,
double end_t
):
if self._recursion_level > RECURSION_LIMIT:
self._recursion_error = 1
return
self._recursion_level += 1
cdef double mid_t = (start_t + end_t) * 0.5
cdef Vec3 mid_point = self.curve.point(mid_t)
cdef double d = v3_dist(mid_point, v3_lerp(start_point, end_point, 0.5))
if d < self.distance:
self.points.append(end_point)
else:
self.flatten(start_point, mid_point, start_t, mid_t)
self.flatten(mid_point, end_point, mid_t, end_t)
self._recursion_level -= 1
cdef class FastQuadCurve:
cdef:
double[3] offset
double[3] p1
double[3] p2
def __cinit__(self, Vec3 p0, Vec3 p1, Vec3 p2):
self.offset[0] = p0.x
self.offset[1] = p0.y
self.offset[2] = p0.z
# 1st control point (p0) is always (0, 0, 0)
self.p1[0] = p1.x - p0.x
self.p1[1] = p1.y - p0.y
self.p1[2] = p1.z - p0.z
self.p2[0] = p2.x - p0.x
self.p2[1] = p2.y - p0.y
self.p2[2] = p2.z - p0.z
cdef Vec3 point(self, double t):
# 1st control point (p0) is always (0, 0, 0)
# => p0 * a is always (0, 0, 0)
cdef:
Vec3 result = Vec3()
# double a = (1 - t) ** 2
double b = 2.0 * t * (1.0 - t)
double c = t * t
iadd_mul(result, self.p1, b)
iadd_mul(result, self.p2, c)
# add offset at last - it is maybe very large
result.x += self.offset[0]
result.y += self.offset[1]
result.z += self.offset[2]
return result
cdef Vec3 tangent(self, double t):
# tangent vector is independent from offset location!
cdef:
Vec3 result = Vec3()
# double a = -2 * (1 - t)
double b = 2.0 - 4.0 * t
double c = 2.0 * t
iadd_mul(result, self.p1, b)
iadd_mul(result, self.p2, c)
return result
cdef void iadd_mul(Vec3 a, double[3] b, double c):
a.x += b[0] * c
a.y += b[1] * c
a.z += b[2] * c

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,371 @@
# cython: language_level=3
# Copyright (c) 2020-2024 Manfred Moitzi
# License: MIT License
from typing import TYPE_CHECKING, Sequence, Iterable, Iterator
import cython
import warnings
from .vector cimport (
Vec3,
Vec2,
isclose,
v3_add,
v3_mul,
v3_dist,
v3_lerp,
v3_from_angle,
normalize_rad_angle,
)
from .matrix44 cimport Matrix44
from libc.math cimport ceil, tan, M_PI
from .construct import arc_angle_span_deg
if TYPE_CHECKING:
from ezdxf.math import UVec
from ezdxf.math.ellipse import ConstructionEllipse
__all__ = [
'Bezier4P', 'cubic_bezier_arc_parameters',
'cubic_bezier_from_arc', 'cubic_bezier_from_ellipse',
]
cdef extern from "constants.h":
const double ABS_TOL
const double REL_TOL
const double M_TAU
cdef double DEG2RAD = M_PI / 180.0
cdef double RECURSION_LIMIT = 1000
cdef class Bezier4P:
cdef:
FastCubicCurve curve # pyright: ignore
readonly Vec3 start_point
Vec3 cp1
Vec3 cp2
readonly Vec3 end_point
def __cinit__(self, defpoints: Sequence[UVec]):
if not isinstance(defpoints[0], (Vec2, Vec3)):
warnings.warn(
DeprecationWarning,
"Bezier4P requires defpoints of type Vec2 or Vec3 in the future",
)
if len(defpoints) == 4:
self.start_point = Vec3(defpoints[0])
self.cp1 = Vec3(defpoints[1])
self.cp2 = Vec3(defpoints[2])
self.end_point = Vec3(defpoints[3])
self.curve = FastCubicCurve(
self.start_point,
self.cp1,
self.cp2,
self.end_point
)
else:
raise ValueError("Four control points required.")
@property
def control_points(self) -> tuple[Vec3, Vec3, Vec3, Vec3]:
return self.start_point, self.cp1, self.cp2, self.end_point
def __reduce__(self):
return Bezier4P, (self.control_points,)
def point(self, double t) -> Vec3:
if 0.0 <= t <= 1.0:
return self.curve.point(t)
else:
raise ValueError("t not in range [0 to 1]")
def tangent(self, double t) -> Vec3:
if 0.0 <= t <= 1.0:
return self.curve.tangent(t)
else:
raise ValueError("t not in range [0 to 1]")
def approximate(self, int segments) -> list[Vec3]:
cdef double delta_t
cdef int segment
cdef list points = [self.start_point]
if segments < 1:
raise ValueError(segments)
delta_t = 1.0 / segments
for segment in range(1, segments):
points.append(self.curve.point(delta_t * segment))
points.append(self.end_point)
return points
def flattening(self, double distance, int segments = 4) -> list[Vec3]:
cdef double dt = 1.0 / segments
cdef double t0 = 0.0, t1
cdef _Flattening f = _Flattening(self, distance)
cdef Vec3 start_point = self.start_point
cdef Vec3 end_point
while t0 < 1.0:
t1 = t0 + dt
if isclose(t1, 1.0, REL_TOL, ABS_TOL):
end_point = self.end_point
t1 = 1.0
else:
end_point = self.curve.point(t1)
f.reset_recursion_check()
f.flatten(start_point, end_point, t0, t1)
if f.has_recursion_error():
raise RecursionError(
"Bezier4P flattening error, check for very large coordinates"
)
t0 = t1
start_point = end_point
return f.points
def approximated_length(self, segments: int = 128) -> float:
cdef double length = 0.0
cdef bint start_flag = 0
cdef Vec3 prev_point, point
for point in self.approximate(segments):
if start_flag:
length += v3_dist(prev_point, point)
else:
start_flag = 1
prev_point = point
return length
def reverse(self) -> Bezier4P:
return Bezier4P((self.end_point, self.cp2, self.cp1, self.start_point))
def transform(self, Matrix44 m) -> Bezier4P:
return Bezier4P(tuple(m.transform_vertices(self.control_points)))
cdef class _Flattening:
cdef FastCubicCurve curve # pyright: ignore
cdef double distance
cdef list points
cdef int _recursion_level
cdef int _recursion_error
def __cinit__(self, Bezier4P curve, double distance):
self.curve = curve.curve
self.distance = distance
self.points = [curve.start_point]
self._recursion_level = 0
self._recursion_error = 0
cdef has_recursion_error(self):
return self._recursion_error
cdef reset_recursion_check(self):
self._recursion_level = 0
self._recursion_error = 0
cdef flatten(
self,
Vec3 start_point,
Vec3 end_point,
double start_t,
double end_t
):
# Keep in sync with CPython implementation: ezdxf/math/_bezier4p.py
# Test suite: 630a
if self._recursion_level > RECURSION_LIMIT:
self._recursion_error = 1
return
self._recursion_level += 1
cdef double mid_t = (start_t + end_t) * 0.5
cdef Vec3 mid_point = self.curve.point(mid_t)
cdef double d = v3_dist(mid_point, v3_lerp(start_point, end_point, 0.5))
if d < self.distance:
self.points.append(end_point)
else:
self.flatten(start_point, mid_point, start_t, mid_t)
self.flatten(mid_point, end_point, mid_t, end_t)
self._recursion_level -= 1
cdef double DEFAULT_TANGENT_FACTOR = 4.0 / 3.0 # 1.333333333333333333
cdef double OPTIMIZED_TANGENT_FACTOR = 1.3324407374108935
cdef double TANGENT_FACTOR = DEFAULT_TANGENT_FACTOR
@cython.cdivision(True)
def cubic_bezier_arc_parameters(
double start_angle,
double end_angle,
int segments = 1,
) -> Iterator[tuple[Vec3, Vec3, Vec3, Vec3]]:
if segments < 1:
raise ValueError('Invalid argument segments (>= 1).')
cdef double delta_angle = end_angle - start_angle
cdef int arc_count
if delta_angle > 0:
arc_count = <int> ceil(delta_angle / M_PI * 2.0)
if segments > arc_count:
arc_count = segments
else:
raise ValueError('Delta angle from start- to end angle has to be > 0.')
cdef double segment_angle = delta_angle / arc_count
cdef double tangent_length = TANGENT_FACTOR * tan(segment_angle / 4.0)
cdef double angle = start_angle
cdef Vec3 start_point, end_point, cp1, cp2
end_point = v3_from_angle(angle, 1.0)
for _ in range(arc_count):
start_point = end_point
angle += segment_angle
end_point = v3_from_angle(angle, 1.0)
cp1 = Vec3()
cp1.x = start_point.x - start_point.y * tangent_length
cp1.y = start_point.y + start_point.x * tangent_length
cp2 = Vec3()
cp2.x = end_point.x + end_point.y * tangent_length
cp2.y = end_point.y - end_point.x * tangent_length
yield start_point, cp1, cp2, end_point
def cubic_bezier_from_arc(
center = (0, 0),
double radius = 1.0,
double start_angle = 0.0,
double end_angle = 360.0,
int segments = 1
) -> Iterable[Bezier4P]:
cdef Vec3 center_ = Vec3(center)
cdef Vec3 tmp
cdef list res
cdef int i
cdef double angle_span = arc_angle_span_deg(start_angle, end_angle)
if abs(angle_span) < 1e-9:
return
cdef double s = start_angle
start_angle = (s * DEG2RAD) % M_TAU
end_angle = (s + angle_span) * DEG2RAD
while start_angle > end_angle:
end_angle += M_TAU
for control_points in cubic_bezier_arc_parameters(start_angle, end_angle, segments):
res = []
for i in range(4):
tmp = <Vec3> control_points[i]
res.append(v3_add(center_, v3_mul(tmp, radius)))
yield Bezier4P(res)
def cubic_bezier_from_ellipse(
ellipse: ConstructionEllipse,
int segments = 1
) -> Iterator[Bezier4P]:
cdef double param_span = ellipse.param_span
if abs(param_span) < 1e-9:
return
cdef double start_angle = normalize_rad_angle(ellipse.start_param)
cdef double end_angle = start_angle + param_span
while start_angle > end_angle:
end_angle += M_TAU
cdef Vec3 center = Vec3(ellipse.center)
cdef Vec3 x_axis = Vec3(ellipse.major_axis)
cdef Vec3 y_axis = Vec3(ellipse.minor_axis)
cdef Vec3 cp
cdef Vec3 c_res
cdef list res
for control_points in cubic_bezier_arc_parameters(start_angle, end_angle, segments):
res = list()
for i in range(4):
cp = <Vec3> control_points[i]
c_res = v3_add_3(center, v3_mul(x_axis, cp.x), v3_mul(y_axis, cp.y))
res.append(c_res)
yield Bezier4P(res)
cdef Vec3 v3_add_3(Vec3 a, Vec3 b, Vec3 c):
cdef Vec3 result = Vec3()
result.x = a.x + b.x + c.x
result.y = a.y + b.y + c.y
result.z = a.z + b.z + c.z
return result
cdef class FastCubicCurve:
cdef:
double[3] offset
double[3] p1
double[3] p2
double[3] p3
def __cinit__(self, Vec3 p0, Vec3 p1, Vec3 p2, Vec3 p3):
cdef:
double x = p0.x
double y = p0.y
double z = p0.z
self.offset[0] = x
self.offset[1] = y
self.offset[2] = z
# 1st control point (p0) is always (0, 0, 0)
self.p1[0] = p1.x - x
self.p1[1] = p1.y - y
self.p1[2] = p1.z - z
self.p2[0] = p2.x - x
self.p2[1] = p2.y - y
self.p2[2] = p2.z - z
self.p3[0] = p3.x - x
self.p3[1] = p3.y - y
self.p3[2] = p3.z - z
cdef Vec3 point(self, double t):
# 1st control point (p0) is always (0, 0, 0)
# => p0 * a is always (0, 0, 0)
cdef:
Vec3 result = Vec3()
double t2 = t * t
double _1_minus_t = 1.0 - t
# a = (1 - t) ** 3
double b = 3.0 * _1_minus_t * _1_minus_t * t
double c = 3.0 * _1_minus_t * t2
double d = t2 * t
iadd_mul(result, self.p1, b)
iadd_mul(result, self.p2, c)
iadd_mul(result, self.p3, d)
# add offset at last - it is maybe very large
result.x += self.offset[0]
result.y += self.offset[1]
result.z += self.offset[2]
return result
cdef Vec3 tangent(self, double t):
# tangent vector is independent from offset location!
cdef:
Vec3 result = Vec3()
double t2 = t * t
# a = -3 * (1 - t) ** 2
double b = 3.0 * (1.0 - 4.0 * t + 3.0 * t2)
double c = 3.0 * t * (2.0 - 3.0 * t)
double d = 3.0 * t2
iadd_mul(result, self.p1, b)
iadd_mul(result, self.p2, c)
iadd_mul(result, self.p3, d)
return result
cdef void iadd_mul(Vec3 a, double[3] b, double c):
a.x += b[0] * c
a.y += b[1] * c
a.z += b[2] * c

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,400 @@
# cython: language_level=3
# Copyright (c) 2021-2024, Manfred Moitzi
# License: MIT License
from typing import Iterable, Sequence, Iterator
import cython
from cpython.mem cimport PyMem_Malloc, PyMem_Free
from .vector cimport Vec3, isclose, v3_mul, v3_sub
__all__ = ['Basis', 'Evaluator']
cdef extern from "constants.h":
const double ABS_TOL
const double REL_TOL
const int MAX_SPLINE_ORDER
# factorial from 0 to 18
cdef double[19] FACTORIAL = [
1., 1., 2., 6., 24., 120., 720., 5040., 40320., 362880., 3628800.,
39916800., 479001600., 6227020800., 87178291200., 1307674368000.,
20922789888000., 355687428096000., 6402373705728000.
]
NULL_LIST = [0.0]
ONE_LIST = [1.0]
cdef Vec3 NULLVEC = Vec3()
@cython.cdivision(True)
cdef double binomial_coefficient(int k, int i):
cdef double k_fact = FACTORIAL[k]
cdef double i_fact = FACTORIAL[i]
cdef double k_i_fact
if i > k:
return 0.0
k_i_fact = FACTORIAL[k - i]
return k_fact / (k_i_fact * i_fact)
@cython.boundscheck(False)
cdef int bisect_right(double *a, double x, int lo, int hi):
cdef int mid
while lo < hi:
mid = (lo + hi) // 2
if x < a[mid]:
hi = mid
else:
lo = mid + 1
return lo
cdef reset_double_array(double *a, int count, double value):
cdef int i
for i in range(count):
a[i] = value
cdef class Basis:
""" Immutable Basis function class. """
# public:
cdef readonly int order
cdef readonly int count
cdef readonly double max_t
cdef tuple weights_ # public attribute for Cython Evaluator
# private:
cdef double *_knots
cdef int knot_count
def __cinit__(
self, knots: Iterable[float],
int order,
int count,
weights: Sequence[float] = None
):
if order < 2 or order >= MAX_SPLINE_ORDER:
raise ValueError('invalid order')
self.order = order
if count < 2:
raise ValueError('invalid count')
self.count = count
self.knot_count = self.order + self.count
self.weights_ = tuple(float(x) for x in weights) if weights else tuple()
cdef Py_ssize_t i = len(self.weights_)
if i != 0 and i != self.count:
raise ValueError('invalid weight count')
knots = [float(x) for x in knots]
if len(knots) != self.knot_count:
raise ValueError('invalid knot count')
self._knots = <double *> PyMem_Malloc(self.knot_count * sizeof(double))
for i in range(self.knot_count):
self._knots[i] = knots[i]
self.max_t = self._knots[self.knot_count - 1]
def __dealloc__(self):
PyMem_Free(self._knots)
@property
def degree(self) -> int:
return self.order - 1
@property
def knots(self) -> tuple[float, ...]:
return tuple(x for x in self._knots[:self.knot_count])
@property
def weights(self) -> tuple[float, ...]:
return self.weights_
@property
def is_rational(self) -> bool:
""" Returns ``True`` if curve is a rational B-spline. (has weights) """
return bool(self.weights_)
cpdef list basis_vector(self, double t):
""" Returns the expanded basis vector. """
cdef int span = self.find_span(t)
cdef int p = self.order - 1
cdef int front = span - p
cdef int back = self.count - span - 1
cdef list result
if front > 0:
result = NULL_LIST * front
result.extend(self.basis_funcs(span, t))
else:
result = self.basis_funcs(span, t)
if back > 0:
result.extend(NULL_LIST * back)
return result
cpdef int find_span(self, double u):
""" Determine the knot span index. """
# Linear search is more reliable than binary search of the Algorithm A2.1
# from The NURBS Book by Piegl & Tiller.
cdef double *knots = self._knots
cdef int count = self.count # text book: n+1
cdef int p = self.order - 1
cdef int span
if u >= knots[count]: # special case
return count - 1
# common clamped spline:
if knots[p] == 0.0: # use binary search
# This is fast and works most of the time,
# but Test 621 : test_weired_closed_spline()
# goes into an infinity loop, because of
# a weird knot configuration.
return bisect_right(knots, u, p, count) - 1
else: # use linear search
span = 0
while knots[span] <= u and span < count:
span += 1
return span - 1
cpdef list basis_funcs(self, int span, double u):
# Source: The NURBS Book: Algorithm A2.2
cdef int order = self.order
cdef double *knots = self._knots
cdef double[MAX_SPLINE_ORDER] N, left, right
cdef list result
reset_double_array(N, order, 0.0)
reset_double_array(left, order, 0.0)
reset_double_array(right, order, 0.0)
cdef int j, r, i1
cdef double temp, saved, temp_r, temp_l
N[0] = 1.0
for j in range(1, order):
i1 = span + 1 - j
if i1 < 0:
i1 = 0
left[j] = u - knots[i1]
right[j] = knots[span + j] - u
saved = 0.0
for r in range(j):
temp_r = right[r + 1]
temp_l = left[j - r]
temp = N[r] / (temp_r + temp_l)
N[r] = saved + temp_r * temp
saved = temp_l * temp
N[j] = saved
result = [x for x in N[:order]]
if self.is_rational:
return self.span_weighting(result, span)
else:
return result
cpdef list span_weighting(self, nbasis: list[float], int span):
cdef list products = [
nb * w for nb, w in zip(
nbasis,
self.weights_[span - self.order + 1: span + 1]
)
]
s = sum(products)
if s != 0:
return [p / s for p in products]
else:
return NULL_LIST * len(nbasis)
cpdef list basis_funcs_derivatives(self, int span, double u, int n = 1):
# pyright: reportUndefinedVariable=false
# pyright flags Cython multi-arrays incorrect:
# cdef double[4][4] a # this is a valid array definition in Cython!
# https://cython.readthedocs.io/en/latest/src/userguide/language_basics.html#c-arrays
# Source: The NURBS Book: Algorithm A2.3
cdef int order = self.order
cdef int p = order - 1
if n > p:
n = p
cdef double *knots = self._knots
cdef double[MAX_SPLINE_ORDER] left, right
reset_double_array(left, order, 1.0)
reset_double_array(right, order, 1.0)
cdef double[MAX_SPLINE_ORDER][MAX_SPLINE_ORDER] ndu # pyright: ignore
reset_double_array(<double *> ndu, MAX_SPLINE_ORDER*MAX_SPLINE_ORDER, 1.0)
cdef int j, r, i1
cdef double temp, saved, tmp_r, tmp_l
for j in range(1, order):
i1 = span + 1 - j
if i1 < 0:
i1 = 0
left[j] = u - knots[i1]
right[j] = knots[span + j] - u
saved = 0.0
for r in range(j):
# lower triangle
tmp_r = right[r + 1]
tmp_l = left[j - r]
ndu[j][r] = tmp_r + tmp_l
temp = ndu[r][j - 1] / ndu[j][r]
# upper triangle
ndu[r][j] = saved + (tmp_r * temp)
saved = tmp_l * temp
ndu[j][j] = saved
# load the basis_vector functions
cdef double[MAX_SPLINE_ORDER][MAX_SPLINE_ORDER] derivatives # pyright: ignore
reset_double_array(
<double *> derivatives, MAX_SPLINE_ORDER*MAX_SPLINE_ORDER, 0.0
)
for j in range(order):
derivatives[0][j] = ndu[j][p]
# loop over function index
cdef double[2][MAX_SPLINE_ORDER] a # pyright: ignore
reset_double_array(<double *> a, 2*MAX_SPLINE_ORDER, 1.0)
cdef int s1, s2, k, rk, pk, j1, j2, t
cdef double d
for r in range(order):
s1 = 0
s2 = 1
# alternate rows in array a
a[0][0] = 1.0
# loop to compute kth derivative
for k in range(1, n + 1):
d = 0.0
rk = r - k
pk = p - k
if r >= k:
a[s2][0] = a[s1][0] / ndu[pk + 1][rk]
d = a[s2][0] * ndu[rk][pk]
if rk >= -1:
j1 = 1
else:
j1 = -rk
if (r - 1) <= pk:
j2 = k - 1
else:
j2 = p - r
for j in range(j1, j2 + 1):
a[s2][j] = (a[s1][j] - a[s1][j - 1]) / ndu[pk + 1][rk + j]
d += (a[s2][j] * ndu[rk + j][pk])
if r <= pk:
a[s2][k] = -a[s1][k - 1] / ndu[pk + 1][r]
d += (a[s2][k] * ndu[r][pk])
derivatives[k][r] = d
# Switch rows
t = s1
s1 = s2
s2 = t
# Multiply through by the correct factors
cdef double rr = p
for k in range(1, n + 1):
for j in range(order):
derivatives[k][j] *= rr
rr *= (p - k)
# return result as Python lists
cdef list result = [], row
for k in range(0, n + 1):
row = []
result.append(row)
for j in range(order):
row.append(derivatives[k][j])
return result
cdef class Evaluator:
""" B-spline curve point and curve derivative evaluator. """
cdef Basis _basis
cdef tuple _control_points
def __cinit__(self, basis: Basis, control_points: Sequence[Vec3]):
self._basis = basis
self._control_points = Vec3.tuple(control_points)
cpdef Vec3 point(self, double u):
# Source: The NURBS Book: Algorithm A3.1
cdef Basis basis = self._basis
if isclose(u, basis.max_t, REL_TOL, ABS_TOL):
u = basis.max_t
cdef:
int p = basis.order - 1
int span = basis.find_span(u)
list N = basis.basis_funcs(span, u)
int i
Vec3 cpoint, v3_sum = Vec3()
tuple control_points = self._control_points
double factor
for i in range(p + 1):
factor = <double> N[i]
cpoint = <Vec3> control_points[span - p + i]
v3_sum.x += cpoint.x * factor
v3_sum.y += cpoint.y * factor
v3_sum.z += cpoint.z * factor
return v3_sum
def points(self, t: Iterable[float]) -> Iterator[Vec3]:
cdef double u
for u in t:
yield self.point(u)
cpdef list derivative(self, double u, int n = 1):
""" Return point and derivatives up to n <= degree for parameter u. """
# Source: The NURBS Book: Algorithm A3.2
cdef Basis basis = self._basis
if isclose(u, basis.max_t, REL_TOL, ABS_TOL):
u = basis.max_t
cdef:
list CK = [], CKw = [], wders = []
tuple control_points = self._control_points
tuple weights
Vec3 cpoint, v3_sum
double wder, bas_func_weight, bas_func
int k, j, i, p = basis.degree
int span = basis.find_span(u)
list basis_funcs_ders = basis.basis_funcs_derivatives(span, u, n)
if basis.is_rational:
# Homogeneous point representation required:
# (x*w, y*w, z*w, w)
weights = basis.weights_
for k in range(n + 1):
v3_sum = Vec3()
wder = 0.0
for j in range(p + 1):
i = span - p + j
bas_func_weight = basis_funcs_ders[k][j] * weights[i]
# control_point * weight * bas_func_der = (x*w, y*w, z*w) * bas_func_der
cpoint = <Vec3> control_points[i]
v3_sum.x += cpoint.x * bas_func_weight
v3_sum.y += cpoint.y * bas_func_weight
v3_sum.z += cpoint.z * bas_func_weight
wder += bas_func_weight
CKw.append(v3_sum)
wders.append(wder)
# Source: The NURBS Book: Algorithm A4.2
for k in range(n + 1):
v3_sum = CKw[k]
for j in range(1, k + 1):
bas_func_weight = binomial_coefficient(k, j) * wders[j]
v3_sum = v3_sub(
v3_sum,
v3_mul(CK[k - j], bas_func_weight)
)
CK.append(v3_sum / wders[0])
else:
for k in range(n + 1):
v3_sum = Vec3()
for j in range(p + 1):
bas_func = basis_funcs_ders[k][j]
cpoint = <Vec3> control_points[span - p + j]
v3_sum.x += cpoint.x * bas_func
v3_sum.y += cpoint.y * bas_func
v3_sum.z += cpoint.z * bas_func
CK.append(v3_sum)
return CK
def derivatives(self, t: Iterable[float], int n = 1) -> Iterator[list[Vec3]]:
cdef double u
for u in t:
yield self.derivative(u, n)

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2023, Manfred Moitzi
// License: MIT License
// global constants
#define ABS_TOL 1e-12
#define REL_TOL 1e-9
#define M_TAU 6.283185307179586
// AutoCAD limits the degree of SPLINE to 11 or order = 12
#define MAX_SPLINE_ORDER 12

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,359 @@
# cython: language_level=3
# Copyright (c) 2020-2024, Manfred Moitzi
# License: MIT License
from typing import Iterable, TYPE_CHECKING, Sequence, Optional
from libc.math cimport fabs, M_PI, M_PI_2, M_PI_4, M_E, sin, tan, pow, atan, log
from .vector cimport (
isclose,
Vec2,
v2_isclose,
Vec3,
v3_sub,
v3_add,
v3_mul,
v3_normalize,
v3_cross,
v3_magnitude_sqr,
v3_isclose,
)
import cython
if TYPE_CHECKING:
from ezdxf.math import UVec
cdef extern from "constants.h":
const double ABS_TOL
const double REL_TOL
const double M_TAU
cdef double RAD_ABS_TOL = 1e-15
cdef double DEG_ABS_TOL = 1e-13
cdef double TOLERANCE = 1e-10
def has_clockwise_orientation(vertices: Iterable[UVec]) -> bool:
""" Returns True if 2D `vertices` have clockwise orientation. Ignores
z-axis of all vertices.
Args:
vertices: iterable of :class:`Vec2` compatible objects
Raises:
ValueError: less than 3 vertices
"""
cdef list _vertices = [Vec2(v) for v in vertices]
if len(_vertices) < 3:
raise ValueError('At least 3 vertices required.')
cdef Vec2 p1 = <Vec2> _vertices[0]
cdef Vec2 p2 = <Vec2> _vertices[-1]
cdef double s = 0.0
cdef Py_ssize_t index
# Using the same tolerance as the Python implementation:
if not v2_isclose(p1, p2, REL_TOL, ABS_TOL):
_vertices.append(p1)
for index in range(1, len(_vertices)):
p2 = <Vec2> _vertices[index]
s += (p2.x - p1.x) * (p2.y + p1.y)
p1 = p2
return s > 0.0
def intersection_line_line_2d(
line1: Sequence[Vec2],
line2: Sequence[Vec2],
bint virtual=True,
double abs_tol=TOLERANCE) -> Optional[Vec2]:
"""
Compute the intersection of two lines in the xy-plane.
Args:
line1: start- and end point of first line to test
e.g. ((x1, y1), (x2, y2)).
line2: start- and end point of second line to test
e.g. ((x3, y3), (x4, y4)).
virtual: ``True`` returns any intersection point, ``False`` returns
only real intersection points.
abs_tol: tolerance for intersection test.
Returns:
``None`` if there is no intersection point (parallel lines) or
intersection point as :class:`Vec2`
"""
# Algorithm based on: http://paulbourke.net/geometry/pointlineplane/
# chapter: Intersection point of two line segments in 2 dimensions
cdef Vec2 s1, s2, c1, c2, res
cdef double s1x, s1y, s2x, s2y, c1x, c1y, c2x, c2y, den, us, uc
cdef double lwr = 0.0, upr = 1.0
s1 = line1[0]
s2 = line1[1]
c1 = line2[0]
c2 = line2[1]
s1x = s1.x
s1y = s1.y
s2x = s2.x
s2y = s2.y
c1x = c1.x
c1y = c1.y
c2x = c2.x
c2y = c2.y
den = (c2y - c1y) * (s2x - s1x) - (c2x - c1x) * (s2y - s1y)
if fabs(den) <= abs_tol:
return None
# den near zero is checked by if-statement above:
with cython.cdivision(True):
us = ((c2x - c1x) * (s1y - c1y) - (c2y - c1y) * (s1x - c1x)) / den
res = Vec2(s1x + us * (s2x - s1x), s1y + us * (s2y - s1y))
if virtual:
return res
# 0 = intersection point is the start point of the line
# 1 = intersection point is the end point of the line
# otherwise: linear interpolation
if lwr <= us <= upr: # intersection point is on the subject line
with cython.cdivision(True):
uc = ((s2x - s1x) * (s1y - c1y) - (s2y - s1y) * (s1x - c1x)) / den
if lwr <= uc <= upr: # intersection point is on the clipping line
return res
return None
cdef double _determinant(Vec3 v1, Vec3 v2, Vec3 v3):
return v1.x * v2.y * v3.z + v1.y * v2.z * v3.x + \
v1.z * v2.x * v3.y - v1.z * v2.y * v3.x - \
v1.x * v2.z * v3.y - v1.y * v2.x * v3.z
def intersection_ray_ray_3d(
ray1: tuple[Vec3, Vec3],
ray2: tuple[Vec3, Vec3],
double abs_tol=TOLERANCE
) -> Sequence[Vec3]:
"""
Calculate intersection of two 3D rays, returns a 0-tuple for parallel rays,
a 1-tuple for intersecting rays and a 2-tuple for not intersecting and not
parallel rays with points of closest approach on each ray.
Args:
ray1: first ray as tuple of two points as Vec3() objects
ray2: second ray as tuple of two points as Vec3() objects
abs_tol: absolute tolerance for comparisons
"""
# source: http://www.realtimerendering.com/intersections.html#I304
cdef:
Vec3 o2_o1
double det1, det2
Vec3 o1 = Vec3(ray1[0])
Vec3 p1 = Vec3(ray1[1])
Vec3 o2 = Vec3(ray2[0])
Vec3 p2 = Vec3(ray2[1])
Vec3 d1 = v3_normalize(v3_sub(p1, o1), 1.0)
Vec3 d2 = v3_normalize(v3_sub(p2, o2), 1.0)
Vec3 d1xd2 = v3_cross(d1, d2)
double denominator = v3_magnitude_sqr(d1xd2)
if denominator <= abs_tol:
# ray1 is parallel to ray2
return tuple()
else:
o2_o1 = v3_sub(o2, o1)
det1 = _determinant(o2_o1, d2, d1xd2)
det2 = _determinant(o2_o1, d1, d1xd2)
with cython.cdivision(True): # denominator check is already done
p1 = v3_add(o1, v3_mul(d1, (det1 / denominator)))
p2 = v3_add(o2, v3_mul(d2, (det2 / denominator)))
if v3_isclose(p1, p2, abs_tol, abs_tol):
# ray1 and ray2 have an intersection point
return p1,
else:
# ray1 and ray2 do not have an intersection point,
# p1 and p2 are the points of closest approach on each ray
return p1, p2
def arc_angle_span_deg(double start, double end) -> float:
if isclose(start, end, REL_TOL, DEG_ABS_TOL):
return 0.0
start %= 360.0
if isclose(start, end % 360.0, REL_TOL, DEG_ABS_TOL):
return 360.0
if not isclose(end, 360.0, REL_TOL, DEG_ABS_TOL):
end %= 360.0
if end < start:
end += 360.0
return end - start
def arc_angle_span_rad(double start, double end) -> float:
if isclose(start, end, REL_TOL, RAD_ABS_TOL):
return 0.0
start %= M_TAU
if isclose(start, end % M_TAU, REL_TOL, RAD_ABS_TOL):
return M_TAU
if not isclose(end, M_TAU, REL_TOL, RAD_ABS_TOL):
end %= M_TAU
if end < start:
end += M_TAU
return end - start
def is_point_in_polygon_2d(
point: Vec2, polygon: list[Vec2], double abs_tol=TOLERANCE
) -> int:
"""
Test if `point` is inside `polygon`. Returns +1 for inside, 0 for on the
boundary and -1 for outside.
Supports convex and concave polygons with clockwise or counter-clockwise oriented
polygon vertices. Does not raise an exception for degenerated polygons.
Args:
point: 2D point to test as :class:`Vec2`
polygon: list of 2D points as :class:`Vec2`
abs_tol: tolerance for distance check
Returns:
+1 for inside, 0 for on the boundary, -1 for outside
"""
# Source: http://www.faqs.org/faqs/graphics/algorithms-faq/
# Subject 2.03: How do I find if a point lies within a polygon?
# Numpy version was just 10x faster, this version is 23x faster than the Python
# version!
cdef double a, b, c, d, x, y, x1, y1, x2, y2
cdef list vertices = polygon
cdef Vec2 p1, p2
cdef int size, last, i
cdef bint inside = 0
size = len(vertices)
if size < 3: # empty polygon
return -1
last = size - 1
p1 = <Vec2> vertices[0]
p2 = <Vec2> vertices[last]
if v2_isclose(p1, p2, REL_TOL, ABS_TOL): # open polygon
size -= 1
last -= 1
if size < 3:
return -1
x = point.x
y = point.y
p1 = <Vec2> vertices[last]
x1 = p1.x
y1 = p1.y
for i in range(size):
p2 = <Vec2> vertices[i]
x2 = p2.x
y2 = p2.y
# is point on polygon boundary line:
# is point in x-range of line
a, b = (x2, x1) if x2 < x1 else (x1, x2)
if a <= x <= b:
# is point in y-range of line
c, d = (y2, y1) if y2 < y1 else (y1, y2)
if (c <= y <= d) and fabs(
(y2 - y1) * x - (x2 - x1) * y + (x2 * y1 - y2 * x1)
) <= abs_tol:
return 0 # on boundary line
if ((y1 <= y < y2) or (y2 <= y < y1)) and (
x < (x2 - x1) * (y - y1) / (y2 - y1) + x1
):
inside = not inside
x1 = x2
y1 = y2
if inside:
return 1 # inside polygon
else:
return -1 # outside polygon
cdef double WGS84_SEMI_MAJOR_AXIS = 6378137
cdef double WGS84_SEMI_MINOR_AXIS = 6356752.3142
cdef double WGS84_ELLIPSOID_ECCENTRIC = 0.08181919092890624
cdef double RADIANS = M_PI / 180.0
cdef double DEGREES = 180.0 / M_PI
def gps_to_world_mercator(double longitude, double latitude) -> tuple[float, float]:
"""Transform GPS (long/lat) to World Mercator.
Transform WGS84 `EPSG:4326 <https://epsg.io/4326>`_ location given as
latitude and longitude in decimal degrees as used by GPS into World Mercator
cartesian 2D coordinates in meters `EPSG:3395 <https://epsg.io/3395>`_.
Args:
longitude: represents the longitude value (East-West) in decimal degrees
latitude: represents the latitude value (North-South) in decimal degrees.
"""
# From: https://epsg.io/4326
# EPSG:4326 WGS84 - World Geodetic System 1984, used in GPS
# To: https://epsg.io/3395
# EPSG:3395 - World Mercator
# Source: https://gis.stackexchange.com/questions/259121/transformation-functions-for-epsg3395-projection-vs-epsg3857
longitude = longitude * RADIANS # east
latitude = latitude * RADIANS # north
cdef double e_sin_lat = sin(latitude) * WGS84_ELLIPSOID_ECCENTRIC
cdef double c = pow(
(1.0 - e_sin_lat) / (1.0 + e_sin_lat), WGS84_ELLIPSOID_ECCENTRIC / 2.0
) # 7-7 p.44
y = WGS84_SEMI_MAJOR_AXIS * log(tan(M_PI_4 + latitude / 2.0) * c) # 7-7 p.44
x = WGS84_SEMI_MAJOR_AXIS * longitude
return x, y
def world_mercator_to_gps(double x, double y, double tol = 1e-6) -> tuple[float, float]:
"""Transform World Mercator to GPS.
Transform WGS84 World Mercator `EPSG:3395 <https://epsg.io/3395>`_
location given as cartesian 2D coordinates x, y in meters into WGS84 decimal
degrees as longitude and latitude `EPSG:4326 <https://epsg.io/4326>`_ as
used by GPS.
Args:
x: coordinate WGS84 World Mercator
y: coordinate WGS84 World Mercator
tol: accuracy for latitude calculation
"""
# From: https://epsg.io/3395
# EPSG:3395 - World Mercator
# To: https://epsg.io/4326
# EPSG:4326 WGS84 - World Geodetic System 1984, used in GPS
# Source: Map Projections - A Working Manual
# https://pubs.usgs.gov/pp/1395/report.pdf
cdef double eccentric_2 = WGS84_ELLIPSOID_ECCENTRIC / 2.0
cdef double t = pow(M_E, (-y / WGS84_SEMI_MAJOR_AXIS)) # 7-10 p.44
cdef double e_sin_lat, latitude, latitude_prev
latitude_prev = M_PI_2 - 2.0 * atan(t) # 7-11 p.45
while True:
e_sin_lat = sin(latitude_prev) * WGS84_ELLIPSOID_ECCENTRIC
latitude = M_PI_2 - 2.0 * atan(
t * pow(((1.0 - e_sin_lat) / (1.0 + e_sin_lat)), eccentric_2)
) # 7-9 p.44
if fabs(latitude - latitude_prev) < tol:
break
latitude_prev = latitude
longitude = x / WGS84_SEMI_MAJOR_AXIS # 7-12 p.45
return longitude * DEGREES, latitude * DEGREES

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,91 @@
# cython: language_level=3
# Copyright (c) 2022-2024, Manfred Moitzi
# License: MIT License
from typing import Iterator, TYPE_CHECKING, Sequence
import cython
from cpython.mem cimport PyMem_Malloc, PyMem_Free
from .vector cimport Vec3, v3_isclose, v3_sub, v3_add, v3_mul, v3_magnitude
if TYPE_CHECKING:
from ezdxf.math import UVec
__all__ = ["_LineTypeRenderer"]
LineSegment = tuple[Vec3, Vec3]
cdef extern from "constants.h":
const double ABS_TOL
const double REL_TOL
cdef class _LineTypeRenderer:
cdef double *dashes
cdef int dash_count
cdef readonly bint is_solid
cdef bint is_dash
cdef int current_dash
cdef double current_dash_length
def __init__(self, dashes: Sequence[float]):
cdef list _dashes = list(dashes)
cdef int i
self.dash_count = len(_dashes)
self.dashes = <double *> PyMem_Malloc(self.dash_count * sizeof(double))
for i in range(self.dash_count):
self.dashes[i] = _dashes[i]
self.is_solid = True
self.is_dash = False
self.current_dash = 0
self.current_dash_length = 0.0
if self.dash_count > 1:
self.is_solid = False
self.current_dash_length = self.dashes[0]
self.is_dash = True
def __dealloc__(self):
PyMem_Free(self.dashes)
def line_segment(self, start: UVec, end: UVec) -> Iterator[LineSegment]:
cdef Vec3 v3_start = Vec3(start)
cdef Vec3 v3_end = Vec3(end)
cdef Vec3 segment_vec, segment_dir
cdef double segment_length, dash_length
cdef list dashes = []
if self.is_solid or v3_isclose(v3_start, v3_end, REL_TOL, ABS_TOL):
yield v3_start, v3_end
return
segment_vec = v3_sub(v3_end, v3_start)
segment_length = v3_magnitude(segment_vec)
with cython.cdivision:
segment_dir = v3_mul(segment_vec, 1.0 / segment_length) # normalize
self._render_dashes(segment_length, dashes)
for dash_length in dashes:
v3_end = v3_add(v3_start, v3_mul(segment_dir, abs(dash_length)))
if dash_length > 0:
yield v3_start, v3_end
v3_start = v3_end
cdef _render_dashes(self, double length, list dashes):
if length <= self.current_dash_length:
self.current_dash_length -= length
dashes.append(length if self.is_dash else -length)
if self.current_dash_length < ABS_TOL:
self._cycle_dashes()
else:
# Avoid deep recursions!
while length > self.current_dash_length:
length -= self.current_dash_length
self._render_dashes(self.current_dash_length, dashes)
if length > 0.0:
self._render_dashes(length, dashes)
cdef _cycle_dashes(self):
with cython.cdivision:
self.current_dash = (self.current_dash + 1) % self.dash_count
self.current_dash_length = self.dashes[self.current_dash]
self.is_dash = not self.is_dash

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,835 @@
# cython: language_level=3
# Source: https://github.com/mapbox/earcut
# License: ISC License (MIT compatible)
#
# Copyright (c) 2016, Mapbox
#
# Permission to use, copy, modify, and/or distribute this software for any purpose
# with or without fee is hereby granted, provided that the above copyright notice
# and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
# FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
# THIS SOFTWARE.
#
# Cython implementation of module ezdxf.math._mapbox_earcut.py
# Copyright (c) 2022, Manfred Moitzi
# License: MIT License
# type: ignore
from libc.math cimport fmin, fmax, fabs, INFINITY
cdef class Node:
cdef:
int i
double x
double y
int z
bint steiner
object point
Node prev
Node next
Node prev_z
Node next_z
def __cinit__(self, int i, object point):
self.i = i
self.point = point
self.x = point.x
self.y = point.y
self.z = 0
self.steiner = False
self.prev = None
self.next = None
self.prev_z = None
self.next_z = None
cdef bint equals(self, Node other):
return self.x == other.x and self.y == other.y
def node_key(Node node):
return node.x, node.y
def earcut(list exterior, list holes):
"""Implements a modified ear slicing algorithm, optimized by z-order
curve hashing and extended to handle holes, twisted polygons, degeneracies
and self-intersections in a way that doesn't guarantee correctness of
triangulation, but attempts to always produce acceptable results for
practical data.
Source: https://github.com/mapbox/earcut
Args:
exterior: outer path as list of points as objects which provide a
`x`- and a `y`-attribute
holes: list of holes, each hole is list of points, a hole with
a single points is a Steiner point
Returns:
Returns a list of triangles, each triangle is a tuple of three points,
the output points are the same objects as the input points.
"""
cdef:
Node outer_node
list triangles = []
double max_x, max_y, x, y
double min_x = 0.0
double min_y = 0.0
double inv_size = 0.0
if not exterior:
return triangles
outer_node = linked_list(exterior, 0, ccw=True)
if outer_node is None or outer_node.next is outer_node.prev:
return triangles
if holes:
outer_node = eliminate_holes(holes, len(exterior), outer_node)
# if the shape is not too simple, we'll use z-order curve hash later
# calculate polygon bbox
if len(exterior) > 80:
min_x = max_x = exterior[0].x
min_y = max_y = exterior[0].y
for point in exterior:
x = point.x
y = point.y
min_x = fmin(min_x, x)
min_y = fmin(min_y, y)
max_x = fmax(max_x, x)
max_y = fmax(max_y, y)
# min_x, min_y and inv_size are later used to transform coords into
# integers for z-order calculation
inv_size = fmax(max_x - min_x, max_y - min_y)
inv_size = 32767 / inv_size if inv_size != 0 else 0
earcut_linked(outer_node, triangles, min_x, min_y, inv_size, 0)
return triangles
cdef Node linked_list(list points, int start, bint ccw):
"""Create a circular doubly linked list from polygon points in the specified
winding order
"""
cdef:
Node last = None
int end
if ccw is (signed_area(points) < 0):
for point in points:
last = insert_node(start, point, last)
start += 1
else:
end = start + len(points)
for point in reversed(points):
last = insert_node(end, point, last)
end -= 1
# open polygon: where the 1st vertex is not coincident with the last vertex
if last and last.equals(last.next):
remove_node(last)
last = last.next
return last
cdef double signed_area(list points):
cdef:
double s = 0.0
double point_x, prev_x, point_y, prev_y
if not len(points):
return s
prev = points[-1]
prev_x = prev.x
prev_y = prev.y
for point in points:
point_x = point.x
point_y = point.y
s += (point_x - prev_x) * (point_y + prev_y)
prev_x = point_x
prev_y = point_y
# s < 0 is counter-clockwise
# s > 0 is clockwise
return s
cdef double area(Node p, Node q, Node r):
"""Returns signed area of a triangle"""
return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y)
cdef bint is_valid_diagonal(Node a, Node b):
"""Check if a diagonal between two polygon nodes is valid (lies in polygon
interior)
"""
return (
a.next.i != b.i
and a.prev.i != b.i
and not intersects_polygon(a, b) # doesn't intersect other edges
and (
locally_inside(a, b)
and locally_inside(b, a)
and middle_inside(a, b)
and (
area(a.prev, a, b.prev) or area(a, b.prev, b)
) # does not create opposite-facing sectors
or a.equals(b)
and area(a.prev, a, a.next) > 0
and area(b.prev, b, b.next) > 0
) # special zero-length case
)
cdef bint intersects_polygon(Node a, Node b):
"""Check if a polygon diagonal intersects any polygon segments"""
cdef Node p = a
while True:
if (
p.i != a.i
and p.next.i != a.i
and p.i != b.i
and p.next.i != b.i
and intersects(p, p.next, a, b)
):
return True
p = p.next
if p is a:
break
return False
cdef int sign(double num):
if num < 0.0:
return -1
if num > 0.0:
return 1
return 0
cdef bint on_segment(p: Node, q: Node, r: Node):
return fmax(p.x, r.x) >= q.x >= fmin(p.x, r.x) and fmax(
p.y, r.y
) >= q.y >= fmin(p.y, r.y)
cdef bint intersects(Node p1, Node q1, Node p2, Node q2):
"""check if two segments intersect"""
cdef:
int o1 = sign(area(p1, q1, p2))
int o2 = sign(area(p1, q1, q2))
int o3 = sign(area(p2, q2, p1))
int o4 = sign(area(p2, q2, q1))
if o1 != o2 and o3 != o4:
return True # general case
if o1 == 0 and on_segment(p1, p2, q1):
return True # p1, q1 and p2 are collinear and p2 lies on p1q1
if o2 == 0 and on_segment(p1, q2, q1):
return True # p1, q1 and q2 are collinear and q2 lies on p1q1
if o3 == 0 and on_segment(p2, p1, q2):
return True # p2, q2 and p1 are collinear and p1 lies on p2q2
if o4 == 0 and on_segment(p2, q1, q2):
return True # p2, q2 and q1 are collinear and q1 lies on p2q2
return False
cdef Node insert_node(int i, point, Node last):
"""create a node and optionally link it with previous one (in a circular
doubly linked list)
"""
cdef Node p = Node(i, point)
if last is None:
p.prev = p
p.next = p
else:
p.next = last.next
p.prev = last
last.next.prev = p
last.next = p
return p
cdef remove_node(Node p):
p.next.prev = p.prev
p.prev.next = p.next
if p.prev_z is not None:
p.prev_z.next_z = p.next_z
if p.next_z is not None:
p.next_z.prev_z = p.prev_z
cdef Node eliminate_holes(list holes, int start, Node outer_node):
"""link every hole into the outer loop, producing a single-ring polygon
without holes
"""
cdef:
list queue = []
list hole
for hole in holes:
if len(hole) < 1: # skip empty holes
continue
# hole vertices in clockwise order
_list = linked_list(hole, start, ccw=False)
if _list is _list.next:
_list.steiner = True
start += len(hole)
queue.append(get_leftmost(_list))
queue.sort(key=node_key)
# process holes from left to right
for hole_ in queue:
outer_node = eliminate_hole(hole_, outer_node)
return outer_node
cdef Node eliminate_hole(Node hole, Node outer_node):
"""Find a bridge between vertices that connects hole with an outer ring and
link it
"""
cdef:
Node bridge = find_hole_bridge(hole, outer_node)
Node bridge_reverse
if bridge is None:
return outer_node
bridge_reverse = split_polygon(bridge, hole)
# filter collinear points around the cuts
filter_points(bridge_reverse, bridge_reverse.next)
return filter_points(bridge, bridge.next)
cdef Node filter_points(Node start, Node end = None):
"""eliminate colinear or duplicate points"""
cdef:
Node p
bint again
if start is None:
return start
if end is None:
end = start
p = start
while True:
again = False
if not p.steiner and (
p.equals(p.next) or area(p.prev, p, p.next) == 0
):
remove_node(p)
p = end = p.prev
if p is p.next:
break
again = True
else:
p = p.next
if not (again or p is not end):
break
return end
# main ear slicing loop which triangulates a polygon (given as a linked list)
cdef earcut_linked(
Node ear,
list triangles,
double min_x,
double min_y,
double inv_size,
int pass_,
):
cdef:
Node stop, prev, next
bint _is_ear
if ear is None:
return
# interlink polygon nodes in z-order
if not pass_ and inv_size:
index_curve(ear, min_x, min_y, inv_size)
stop = ear
# iterate through ears, slicing them one by one
while ear.prev is not ear.next:
prev = ear.prev
next = ear.next
_is_ear = (
is_ear_hashed(ear, min_x, min_y, inv_size)
if inv_size
else is_ear(ear)
)
if _is_ear:
# cut off the triangle
triangles.append((prev.point, ear.point, next.point))
remove_node(ear)
# skipping the next vertex leads to less sliver triangles
ear = next.next
stop = next.next
continue
ear = next
# if we looped through the whole remaining polygon and can't find any more ears
if ear is stop:
# try filtering points and slicing again
if not pass_:
earcut_linked(
filter_points(ear),
triangles,
min_x,
min_y,
inv_size,
1,
)
# if this didn't work, try curing all small self-intersections locally
elif pass_ == 1:
ear = cure_local_intersections(filter_points(ear), triangles)
earcut_linked(ear, triangles, min_x, min_y, inv_size, 2)
# as a last resort, try splitting the remaining polygon into two
elif pass_ == 2:
split_ear_cut(ear, triangles, min_x, min_y, inv_size)
break
cdef bint is_ear(Node ear):
"""check whether a polygon node forms a valid ear with adjacent nodes"""
cdef:
Node a = ear.prev
Node b = ear
Node c = ear.next
Node p
double x0, x1, y0, y1
if area(a, b, c) >= 0:
return False # reflex, can't be an ear
# now make sure we don't have other points inside the potential ear
# triangle bbox
x0 = fmin(a.x, fmin(b.x, c.x))
x1 = fmax(a.x, fmax(b.x, c.x))
y0 = fmin(a.y, fmin(b.y, c.y))
y1 = fmax(a.y, fmax(b.y, c.y))
p = c.next
while p is not a:
if (
x0 <= p.x <= x1
and y0 <= p.y <= y1
and point_in_triangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y)
and area(p.prev, p, p.next) >= 0
):
return False
p = p.next
return True
cdef bint is_ear_hashed(Node ear, double min_x, double min_y, double inv_size):
cdef:
Node a = ear.prev
Node b = ear
Node c = ear.next
double x0, x1, y0, y1, min_z, max_z
Node p, n
if area(a, b, c) >= 0:
return False # reflex, can't be an ear
# triangle bbox
x0 = fmin(a.x, fmin(b.x, c.x))
x1 = fmax(a.x, fmax(b.x, c.x))
y0 = fmin(a.y, fmin(b.y, c.y))
y1 = fmax(a.y, fmax(b.y, c.y))
# z-order range for the current triangle bbox;
min_z = z_order(x0, y0, min_x, min_y, inv_size)
max_z = z_order(x1, y1, min_x, min_y, inv_size)
p = ear.prev_z
n = ear.next_z
# look for points inside the triangle in both directions
while p and p.z >= min_z and n and n.z <= max_z:
if (
x0 <= p.x <= x1
and y0 <= p.y <= y1
and p is not a
and p is not c
and point_in_triangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y)
and area(p.prev, p, p.next) >= 0
):
return False
p = p.prev_z
if (
x0 <= n.x <= x1
and y0 <= n.y <= y1
and n is not a
and n is not c
and point_in_triangle(a.x, a.y, b.x, b.y, c.x, c.y, n.x, n.y)
and area(n.prev, n, n.next) >= 0
):
return False
n = n.next_z
# look for remaining points in decreasing z-order
while p and p.z >= min_z:
if (
x0 <= p.x <= x1
and y0 <= p.y <= y1
and p is not a
and p is not c
and point_in_triangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y)
and area(p.prev, p, p.next) >= 0
):
return False
p = p.prev_z
# look for remaining points in increasing z-order
while n and n.z <= max_z:
if (
x0 <= n.x <= x1
and y0 <= n.y <= y1
and n is not a
and n is not c
and point_in_triangle(a.x, a.y, b.x, b.y, c.x, c.y, n.x, n.y)
and area(n.prev, n, n.next) >= 0
):
return False
n = n.next_z
return True
cdef Node get_leftmost(Node start):
"""Find the leftmost node of a polygon ring"""
cdef:
Node p = start
Node leftmost = start
while True:
if p.x < leftmost.x or (p.x == leftmost.x and p.y < leftmost.y):
leftmost = p
p = p.next
if p is start:
break
return leftmost
cdef bint point_in_triangle(
double ax,
double ay,
double bx,
double by_,
double cx,
double cy,
double px,
double py,
):
"""Check if a point lies within a convex triangle"""
return (
(cx - px) * (ay - py) >= (ax - px) * (cy - py)
and (ax - px) * (by_ - py) >= (bx - px) * (ay - py)
and (bx - px) * (cy - py) >= (cx - px) * (by_ - py)
)
cdef bint sector_contains_sector(Node m, Node p):
"""Whether sector in vertex m contains sector in vertex p in the same
coordinates.
"""
return area(m.prev, m, p.prev) < 0 and area(p.next, m, m.next) < 0
cdef index_curve(Node start, double min_x, double min_y, double inv_size):
"""Interlink polygon nodes in z-order"""
cdef Node p = start
while True:
if p.z == 0:
p.z = z_order(p.x, p.y, min_x, min_y, inv_size)
p.prev_z = p.prev
p.next_z = p.next
p = p.next
if p is start:
break
p.prev_z.next_z = None
p.prev_z = None
sort_linked(p)
cdef int z_order(
double x0, double y0, double min_x, double min_y, double inv_size
):
"""Z-order of a point given coords and inverse of the longer side of data
bbox.
"""
# coords are transformed into non-negative 15-bit integer range
cdef:
int x = int((x0 - min_x) * inv_size)
int y = int ((y0 - min_y) * inv_size)
x = (x | (x << 8)) & 0x00FF00FF
x = (x | (x << 4)) & 0x0F0F0F0F
x = (x | (x << 2)) & 0x33333333
x = (x | (x << 1)) & 0x55555555
y = (y | (y << 8)) & 0x00FF00FF
y = (y | (y << 4)) & 0x0F0F0F0F
y = (y | (y << 2)) & 0x33333333
y = (y | (y << 1)) & 0x55555555
return x | (y << 1)
# Simon Tatham's linked list merge sort algorithm
# http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html
cdef Node sort_linked(Node head):
cdef:
int in_size = 1
int num_merges, p_size, q_size, i
Node tail, p, q, e
while True:
p = head
head = None
tail = None
num_merges = 0
while p:
num_merges += 1
q = p
p_size = 0
for i in range(in_size):
p_size += 1
q = q.next_z
if not q:
break
q_size = in_size
while p_size > 0 or (q_size > 0 and q):
if p_size != 0 and (q_size == 0 or not q or p.z <= q.z):
e = p
p = p.next_z
p_size -= 1
else:
e = q
q = q.next_z
q_size -= 1
if tail:
tail.next_z = e
else:
head = e
e.prev_z = tail
tail = e
p = q
tail.next_z = None
in_size *= 2
if num_merges <= 1:
break
return head
cdef Node split_polygon(Node a, Node b):
"""Link two polygon vertices with a bridge.
If the vertices belong to the same ring, it splits polygon into two.
If one belongs to the outer ring and another to a hole, it merges it into a
single ring.
"""
cdef :
Node a2 = Node(a.i, a.point)
Node b2 = Node(b.i, b.point)
Node an = a.next
Node bp = b.prev
a.next = b
b.prev = a
a2.next = an
an.prev = a2
b2.next = a2
a2.prev = b2
bp.next = b2
b2.prev = bp
return b2
# go through all polygon nodes and cure small local self-intersections
cdef Node cure_local_intersections(Node start, list triangles):
cdef:
Node p = start
Node a, b
while True:
a = p.prev
b = p.next.next
if (
not a.equals(b)
and intersects(a, p, p.next, b)
and locally_inside(a, b)
and locally_inside(b, a)
):
triangles.append((a.point, p.point, b.point))
# remove two nodes involved
remove_node(p)
remove_node(p.next)
p = start = b
p = p.next
if p is start:
break
return filter_points(p)
cdef split_ear_cut(
Node start,
list triangles,
double min_x,
double min_y,
double inv_size,
):
"""Try splitting polygon into two and triangulate them independently"""
# look for a valid diagonal that divides the polygon into two
cdef:
Node a = start
Node b, c
while True:
b = a.next.next
while b is not a.prev:
if a.i != b.i and is_valid_diagonal(a, b):
# split the polygon in two by the diagonal
c = split_polygon(a, b)
# filter colinear points around the cuts
a = filter_points(a, a.next)
c = filter_points(c, c.next)
# run earcut on each half
earcut_linked(a, triangles, min_x, min_y, inv_size, 0)
earcut_linked(c, triangles, min_x, min_y, inv_size, 0)
return
b = b.next
a = a.next
if a is start:
break
# David Eberly's algorithm for finding a bridge between hole and outer polygon
cdef Node find_hole_bridge(Node hole, Node outer_node):
cdef:
Node p = outer_node
Node m = None
Node stop
double hx = hole.x
double hy = hole.y
double qx = -INFINITY
double mx, my, tan_min, tan
# find a segment intersected by a ray from the hole's leftmost point to the left;
# segment's endpoint with lesser x will be potential connection point
while True:
if p.y >= hy >= p.next.y != p.y:
x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y)
if hx >= x > qx:
qx = x
m = p if p.x < p.next.x else p.next
if x == hx:
# hole touches outer segment; pick leftmost endpoint
return m
p = p.next
if p is outer_node:
break
if m is None:
return None
# look for points inside the triangle of hole point, segment intersection and endpoint;
# if there are no points found, we have a valid connection;
# otherwise choose the point of the minimum angle with the ray as connection point
stop = m
mx = m.x
my = m.y
tan_min = INFINITY
p = m
while True:
if (
hx >= p.x >= mx
and hx != p.x
and point_in_triangle(
hx if hy < my else qx,
hy,
mx,
my,
qx if hy < my else hx,
hy,
p.x,
p.y,
)
):
tan = fabs(hy - p.y) / (hx - p.x) # tangential
if locally_inside(p, hole) and (
tan < tan_min
or (
tan == tan_min
and (
p.x > m.x
or (p.x == m.x and sector_contains_sector(m, p))
)
)
):
m = p
tan_min = tan
p = p.next
if p is stop:
break
return m
cdef bint locally_inside(Node a, Node b):
"""Check if a polygon diagonal is locally inside the polygon"""
return (
area(a, b, a.next) >= 0 and area(a, a.prev, b) >= 0
if area(a.prev, a, a.next) < 0
else area(a, b, a.prev) < 0 or area(a, a.next, b) < 0
)
cdef bint middle_inside(Node a, Node b):
"""Check if the middle point of a polygon diagonal is inside the polygon"""
cdef:
Node p = a
bint inside = False
double px = (a.x + b.x) / 2
double py = (a.y + b.y) / 2
while True:
if (
((p.y > py) != (p.next.y > py))
and p.next.y != p.y
and (px < (p.next.x - p.x) * (py - p.y) / (p.next.y - p.y) + p.x)
):
inside = not inside
p = p.next
if p is a:
break
return inside

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
# cython: language_level=3
# Copyright (c) 2020-2023, Manfred Moitzi
# License: MIT License
from .vector cimport Vec3
cdef class Matrix44:
cdef double m[16]
cdef Vec3 get_ux(self: Matrix44)
cdef Vec3 get_uy(self: Matrix44)
cdef Vec3 get_uz(self: Matrix44)
cdef inline swap(double *a, double *b):
cdef double tmp = a[0]
a[0] = b[0]
b[0] = tmp

View File

@@ -0,0 +1,676 @@
# cython: language_level=3
# Copyright (c) 2020-2024, Manfred Moitzi
# License: MIT License
from typing import Sequence, Iterable, Tuple, TYPE_CHECKING, Iterator
from itertools import chain
import math
import numpy as np
import cython
from .vector cimport (
Vec2, Vec3, v3_normalize, v3_isclose, v3_cross, v3_dot,
)
from .vector import X_AXIS, Y_AXIS, Z_AXIS, NULLVEC
from libc.math cimport fabs, sin, cos, tan
if TYPE_CHECKING:
from ezdxf.math import UVec
cdef extern from "constants.h":
const double ABS_TOL
const double REL_TOL
cdef double[16] IDENTITY = [
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0
]
cdef void set_floats(double *m, object values) except *:
cdef int i = 0
for v in values:
if i < 16: # Do not write beyond array bounds
m[i] = v
i += 1
if i != 16:
raise ValueError("invalid argument count")
cdef class Matrix44:
def __cinit__(self, *args):
cdef int nargs = len(args)
if nargs == 0: # default constructor Matrix44(): fastest setup
self.m = IDENTITY # memcopy!
elif nargs == 1: # 16 numbers: slow setup
set_floats(self.m, args[0])
elif nargs == 4: # 4 rows of 4 numbers: slowest setup
set_floats(self.m, chain(*args))
else:
raise ValueError("invalid argument count: 4 row vectors or "
"iterable of 16 numbers")
def __reduce__(self):
return Matrix44, (tuple(self),)
def __getitem__(self, tuple index) -> float:
cdef int row = index[0]
cdef int col = index[1]
cdef int i = row * 4 + col
if 0 <= i < 16 and 0 <= col < 4:
return self.m[i]
else:
raise IndexError(f'index out of range: {index}')
def __setitem__(self, tuple index, double value):
cdef int row = index[0]
cdef int col = index[1]
cdef int i = row * 4 + col
if 0 <= i < 16 and 0 <= col < 4:
self.m[i] = value
else:
raise IndexError(f'index out of range: {index}')
def __iter__(self):
cdef int i
for i in range(16):
yield self.m[i]
def __repr__(self) -> str:
def format_row(row):
return "(%s)" % ", ".join(str(value) for value in row)
return "Matrix44(%s)" % \
", ".join(format_row(row) for row in self.rows())
def get_2d_transformation(self) -> Tuple[float, ...]:
cdef double *m = self.m
return m[0], m[1], 0.0, m[4], m[5], 0.0, m[12], m[13], 1.0
@staticmethod
def from_2d_transformation(components: Sequence[float]) -> Matrix44:
if len(components) != 6:
raise ValueError(
"First 2 columns of a 3x3 matrix required: m11, m12, m21, m22, m31, m32"
)
m44 = Matrix44()
m44.m[0] = components[0]
m44.m[1] = components[1]
m44.m[4] = components[2]
m44.m[5] = components[3]
m44.m[12] = components[4]
m44.m[13] = components[5]
return m44
def get_row(self, int row) -> Tuple[float, ...]:
cdef int index = row * 4
if 0 <= index < 13:
return self.m[index], self.m[index + 1], self.m[index + 2], self.m[
index + 3]
else:
raise IndexError(f'invalid row index: {row}')
def set_row(self, int row, values: Sequence[float]) -> None:
cdef Py_ssize_t count = len(values)
cdef Py_ssize_t start = row * 4
cdef Py_ssize_t i
if 0 <= row < 4:
if count > 4:
count = 4
for i in range(count):
self.m[start + i] = values[i]
else:
raise IndexError(f'invalid row index: {row}')
def get_col(self, int col) -> Tuple[float, ...]:
if 0 <= col < 4:
return self.m[col], self.m[col + 4], \
self.m[col + 8], self.m[col + 12]
else:
raise IndexError(f'invalid col index: {col}')
def set_col(self, int col, values: Sequence[float]):
cdef Py_ssize_t count = len(values)
cdef Py_ssize_t i
if 0 <= col < 4:
if count > 4:
count = 4
for i in range(count):
self.m[col + i * 4] = values[i]
else:
raise IndexError(f'invalid col index: {col}')
def rows(self) -> Iterator[Tuple[float, ...]]:
return (self.get_row(index) for index in (0, 1, 2, 3))
def columns(self) -> Iterator[Tuple[float, ...]]:
return (self.get_col(index) for index in (0, 1, 2, 3))
def copy(self) -> Matrix44:
cdef Matrix44 _copy = Matrix44()
_copy.m = self.m
return _copy
__copy__ = copy
@property
def origin(self) -> Vec3:
cdef Vec3 v = Vec3()
v.x = self.m[12]
v.y = self.m[13]
v.z = self.m[14]
return v
@origin.setter
def origin(self, v: UVec) -> None:
cdef Vec3 origin = Vec3(v)
self.m[12] = origin.x
self.m[13] = origin.y
self.m[14] = origin.z
@property
def ux(self) -> Vec3:
return self.get_ux()
cdef Vec3 get_ux(self):
cdef Vec3 v = Vec3()
v.x = self.m[0]
v.y = self.m[1]
v.z = self.m[2]
return v
@property
def uy(self) -> Vec3:
return self.get_uy()
cdef Vec3 get_uy(self):
cdef Vec3 v = Vec3()
v.x = self.m[4]
v.y = self.m[5]
v.z = self.m[6]
return v
@property
def uz(self) -> Vec3:
return self.get_uz()
cdef Vec3 get_uz(self):
cdef Vec3 v = Vec3()
v.x = self.m[8]
v.y = self.m[9]
v.z = self.m[10]
return v
@property
def is_cartesian(self) -> bool:
cdef Vec3 x_axis = v3_cross(self.get_uy(), self.get_uz())
return v3_isclose(x_axis, self.get_ux(), REL_TOL, ABS_TOL)
@property
def is_orthogonal(self) -> bool:
cdef Vec3 ux = v3_normalize(self.get_ux(), 1.0)
cdef Vec3 uy = v3_normalize(self.get_uy(), 1.0)
cdef Vec3 uz = v3_normalize(self.get_uz(), 1.0)
return fabs(v3_dot(ux, uy)) < 1e-9 and \
fabs(v3_dot(ux, uz)) < 1e-9 and \
fabs(v3_dot(uy, uz)) < 1e-9
@staticmethod
def scale(double sx, sy = None, sz = None) -> Matrix44:
cdef Matrix44 mat = Matrix44()
mat.m[0] = sx
mat.m[5] = sx if sy is None else sy
mat.m[10] = sx if sz is None else sz
return mat
@staticmethod
def translate(double dx, double dy, double dz) -> Matrix44:
cdef Matrix44 mat = Matrix44()
mat.m[12] = dx
mat.m[13] = dy
mat.m[14] = dz
return mat
@staticmethod
def x_rotate(double angle) -> Matrix44:
cdef Matrix44 mat = Matrix44()
cdef double cos_a = cos(angle)
cdef double sin_a = sin(angle)
mat.m[5] = cos_a
mat.m[6] = sin_a
mat.m[9] = -sin_a
mat.m[10] = cos_a
return mat
@staticmethod
def y_rotate(double angle) -> Matrix44:
cdef Matrix44 mat = Matrix44()
cdef double cos_a = cos(angle)
cdef double sin_a = sin(angle)
mat.m[0] = cos_a
mat.m[2] = -sin_a
mat.m[8] = sin_a
mat.m[10] = cos_a
return mat
@staticmethod
def z_rotate(double angle) -> Matrix44:
cdef Matrix44 mat = Matrix44()
cdef double cos_a = cos(angle)
cdef double sin_a = sin(angle)
mat.m[0] = cos_a
mat.m[1] = sin_a
mat.m[4] = -sin_a
mat.m[5] = cos_a
return mat
@staticmethod
def axis_rotate(axis: UVec, double angle) -> Matrix44:
cdef Matrix44 mat = Matrix44()
cdef double cos_a = cos(angle)
cdef double sin_a = sin(angle)
cdef double one_m_cos = 1.0 - cos_a
cdef Vec3 _axis = Vec3(axis).normalize()
cdef double x = _axis.x
cdef double y = _axis.y
cdef double z = _axis.z
mat.m[0] = x * x * one_m_cos + cos_a
mat.m[1] = y * x * one_m_cos + z * sin_a
mat.m[2] = x * z * one_m_cos - y * sin_a
mat.m[4] = x * y * one_m_cos - z * sin_a
mat.m[5] = y * y * one_m_cos + cos_a
mat.m[6] = y * z * one_m_cos + x * sin_a
mat.m[8] = x * z * one_m_cos + y * sin_a
mat.m[9] = y * z * one_m_cos - x * sin_a
mat.m[10] = z * z * one_m_cos + cos_a
return mat
@staticmethod
def xyz_rotate(double angle_x, double angle_y,
double angle_z) -> Matrix44:
cdef Matrix44 mat = Matrix44()
cdef double cx = cos(angle_x)
cdef double sx = sin(angle_x)
cdef double cy = cos(angle_y)
cdef double sy = sin(angle_y)
cdef double cz = cos(angle_z)
cdef double sz = sin(angle_z)
cdef double sxsy = sx * sy
cdef double cxsy = cx * sy
mat.m[0] = cy * cz
mat.m[1] = sxsy * cz + cx * sz
mat.m[2] = -cxsy * cz + sx * sz
mat.m[4] = -cy * sz
mat.m[5] = -sxsy * sz + cx * cz
mat.m[6] = cxsy * sz + sx * cz
mat.m[8] = sy
mat.m[9] = -sx * cy
mat.m[10] = cx * cy
return mat
@staticmethod
def shear_xy(double angle_x = 0, double angle_y = 0) -> Matrix44:
cdef Matrix44 mat = Matrix44()
cdef double tx = tan(angle_x)
cdef double ty = tan(angle_y)
mat.m[1] = ty
mat.m[4] = tx
return mat
@staticmethod
def perspective_projection(double left, double right, double top,
double bottom, double near,
double far) -> Matrix44:
cdef Matrix44 mat = Matrix44()
mat.m[0] = (2. * near) / (right - left)
mat.m[5] = (2. * near) / (top - bottom)
mat.m[8] = (right + left) / (right - left)
mat.m[9] = (top + bottom) / (top - bottom)
mat.m[10] = -((far + near) / (far - near))
mat.m[11] = -1
mat.m[14] = -((2. * far * near) / (far - near))
return mat
@staticmethod
def perspective_projection_fov(fov: float, aspect: float, near: float,
far: float) -> Matrix44:
vrange = near * math.tan(fov / 2.)
left = -vrange * aspect
right = vrange * aspect
bottom = -vrange
top = vrange
return Matrix44.perspective_projection(left, right, bottom, top, near,
far)
@staticmethod
def chain(*matrices: Matrix44) -> Matrix44:
cdef Matrix44 transformation = Matrix44()
for matrix in matrices:
transformation *= matrix
return transformation
def __imul__(self, Matrix44 other) -> Matrix44:
cdef double[16] m1 = self.m
cdef double *m2 = other.m
self.m[0] = m1[0] * m2[0] + m1[1] * m2[4] + m1[2] * m2[8] + \
m1[3] * m2[12]
self.m[1] = m1[0] * m2[1] + m1[1] * m2[5] + m1[2] * m2[9] + \
m1[3] * m2[13]
self.m[2] = m1[0] * m2[2] + m1[1] * m2[6] + m1[2] * m2[10] + \
m1[3] * m2[14]
self.m[3] = m1[0] * m2[3] + m1[1] * m2[7] + m1[2] * m2[11] + \
m1[3] * m2[15]
self.m[4] = m1[4] * m2[0] + m1[5] * m2[4] + m1[6] * m2[8] + \
m1[7] * m2[12]
self.m[5] = m1[4] * m2[1] + m1[5] * m2[5] + m1[6] * m2[9] + \
m1[7] * m2[13]
self.m[6] = m1[4] * m2[2] + m1[5] * m2[6] + m1[6] * m2[10] + \
m1[7] * m2[14]
self.m[7] = m1[4] * m2[3] + m1[5] * m2[7] + m1[6] * m2[11] + \
m1[7] * m2[15]
self.m[8] = m1[8] * m2[0] + m1[9] * m2[4] + m1[10] * m2[8] + \
m1[11] * m2[12]
self.m[9] = m1[8] * m2[1] + m1[9] * m2[5] + m1[10] * m2[9] + \
m1[11] * m2[13]
self.m[10] = m1[8] * m2[2] + m1[9] * m2[6] + m1[10] * m2[10] + \
m1[11] * m2[14]
self.m[11] = m1[8] * m2[3] + m1[9] * m2[7] + m1[10] * m2[11] + \
m1[11] * m2[15]
self.m[12] = m1[12] * m2[0] + m1[13] * m2[4] + m1[14] * m2[8] + \
m1[15] * m2[12]
self.m[13] = m1[12] * m2[1] + m1[13] * m2[5] + m1[14] * m2[9] + \
m1[15] * m2[13]
self.m[14] = m1[12] * m2[2] + m1[13] * m2[6] + m1[14] * m2[10] + \
m1[15] * m2[14]
self.m[15] = m1[12] * m2[3] + m1[13] * m2[7] + m1[14] * m2[11] + \
m1[15] * m2[15]
return self
def __mul__(self, Matrix44 other) -> Matrix44:
cdef Matrix44 res_matrix = self.copy()
return res_matrix.__imul__(other)
# __matmul__ = __mul__ does not work!
def __matmul__(self, Matrix44 other) -> Matrix44:
cdef Matrix44 res_matrix = self.copy()
return res_matrix.__imul__(other)
def transpose(self) -> None:
swap(&self.m[1], &self.m[4])
swap(&self.m[2], &self.m[8])
swap(&self.m[3], &self.m[12])
swap(&self.m[6], &self.m[9])
swap(&self.m[7], &self.m[13])
swap(&self.m[11], &self.m[14])
def determinant(self) -> float:
cdef double *m = self.m
return m[0] * m[5] * m[10] * m[15] - m[0] * m[5] * m[11] * m[14] + \
m[0] * m[6] * m[11] * m[13] - m[0] * m[6] * m[9] * m[15] + \
m[0] * m[7] * m[9] * m[14] - m[0] * m[7] * m[10] * m[13] - \
m[1] * m[6] * m[11] * m[12] + m[1] * m[6] * m[8] * m[15] - \
m[1] * m[7] * m[8] * m[14] + m[1] * m[7] * m[10] * m[12] - \
m[1] * m[4] * m[10] * m[15] + m[1] * m[4] * m[11] * m[14] + \
m[2] * m[7] * m[8] * m[13] - m[2] * m[7] * m[9] * m[12] + \
m[2] * m[4] * m[9] * m[15] - m[2] * m[4] * m[11] * m[13] + \
m[2] * m[5] * m[11] * m[12] - m[2] * m[5] * m[8] * m[15] - \
m[3] * m[4] * m[9] * m[14] + m[3] * m[4] * m[10] * m[13] - \
m[3] * m[5] * m[10] * m[12] + m[3] * m[5] * m[8] * m[14] - \
m[3] * m[6] * m[8] * m[13] + m[3] * m[6] * m[9] * m[12]
def inverse(self) -> None:
cdef double[16] m = self.m # memcopy
cdef double det = self.determinant()
cdef double f = 1. / det
self.m[0] = (m[6] * m[11] * m[13] - m[7] * m[10] * m[13] + m[7] * m[9] *
m[14] - m[5] * m[11] * m[14] - m[6] * m[9] * m[15] + m[5] *
m[10] * m[15]) * f
self.m[1] = (m[3] * m[10] * m[13] - m[2] * m[11] * m[13] - m[3] * m[9] *
m[14] + m[1] * m[11] * m[14] + m[2] * m[9] * m[15] -
m[1] * m[10] * m[15]) * f
self.m[2] = (m[2] * m[7] * m[13] - m[3] * m[6] * m[13] + m[3] * m[5] *
m[14] - m[1] * m[7] * m[14] - m[2] * m[5] * m[15] +
m[1] * m[6] * m[15]) * f
self.m[3] = (m[3] * m[6] * m[9] - m[2] * m[7] * m[9] - m[3] * m[5] *
m[10] + m[1] * m[7] * m[10] + m[2] * m[5] * m[11] - m[1] *
m[6] * m[11]) * f
self.m[4] = (m[7] * m[10] * m[12] - m[6] * m[11] * m[12] - m[7] * m[8] *
m[14] + m[4] * m[11] * m[14] + m[6] * m[8] * m[15] -
m[4] * m[10] * m[15]) * f
self.m[5] = (m[2] * m[11] * m[12] - m[3] * m[10] * m[12] + m[3] * m[8] *
m[14] - m[0] * m[11] * m[14] - m[2] * m[8] * m[15] +
m[0] * m[10] * m[15]) * f
self.m[6] = (m[3] * m[6] * m[12] - m[2] * m[7] * m[12] - m[3] * m[4] *
m[14] + m[0] * m[7] * m[14] + m[2] * m[4] * m[15] -
m[0] * m[6] * m[15]) * f
self.m[7] = (m[2] * m[7] * m[8] - m[3] * m[6] * m[8] + m[3] * m[4] *
m[10] - m[0] * m[7] * m[10] - m[2] * m[4] * m[11] +
m[0] * m[6] * m[11]) * f
self.m[8] = (m[5] * m[11] * m[12] - m[7] * m[9] * m[12] + m[7] * m[8] *
m[13] - m[4] * m[11] * m[13] - m[5] * m[8] * m[15] +
m[4] * m[9] * m[15]) * f
self.m[9] = (m[3] * m[9] * m[12] - m[1] * m[11] * m[12] - m[3] *
m[8] * m[13] + m[0] * m[11] * m[13] + m[1] * m[8] *
m[15] - m[0] * m[9] * m[15]) * f
self.m[10] = (m[1] * m[7] * m[12] - m[3] * m[5] * m[12] + m[3] *
m[4] * m[13] - m[0] * m[7] * m[13] - m[1] * m[4] *
m[15] + m[0] * m[5] * m[15]) * f
self.m[11] = (m[3] * m[5] * m[8] - m[1] * m[7] * m[8] - m[3] * m[4] *
m[9] + m[0] * m[7] * m[9] + m[1] * m[4] * m[11] -
m[0] * m[5] * m[11]) * f
self.m[12] = (m[6] * m[9] * m[12] - m[5] * m[10] * m[12] - m[6] *
m[8] * m[13] + m[4] * m[10] * m[13] + m[5] * m[8] *
m[14] - m[4] * m[9] * m[14]) * f
self.m[13] = (m[1] * m[10] * m[12] - m[2] * m[9] * m[12] + m[2] *
m[8] * m[13] - m[0] * m[10] * m[13] - m[1] * m[8] *
m[14] + m[0] * m[9] * m[14]) * f
self.m[14] = (m[2] * m[5] * m[12] - m[1] * m[6] * m[12] - m[2] *
m[4] * m[13] + m[0] * m[6] * m[13] + m[1] * m[4] *
m[14] - m[0] * m[5] * m[14]) * f
self.m[15] = (m[1] * m[6] * m[8] - m[2] * m[5] * m[8] + m[2] * m[4] *
m[9] - m[0] * m[6] * m[9] - m[1] * m[4] * m[10] +
m[0] * m[5] * m[10]) * f
@staticmethod
def ucs(ux=X_AXIS, uy=Y_AXIS, uz=Z_AXIS, origin=NULLVEC) -> Matrix44:
cdef Matrix44 mat = Matrix44()
cdef Vec3 _ux = Vec3(ux)
cdef Vec3 _uy = Vec3(uy)
cdef Vec3 _uz = Vec3(uz)
cdef Vec3 _origin = Vec3(origin)
mat.m[0] = _ux.x
mat.m[1] = _ux.y
mat.m[2] = _ux.z
mat.m[4] = _uy.x
mat.m[5] = _uy.y
mat.m[6] = _uy.z
mat.m[8] = _uz.x
mat.m[9] = _uz.y
mat.m[10] = _uz.z
mat.m[12] = _origin.x
mat.m[13] = _origin.y
mat.m[14] = _origin.z
return mat
def transform(self, vector: UVec) -> Vec3:
cdef Vec3 res = Vec3(vector)
cdef double x = res.x
cdef double y = res.y
cdef double z = res.z
cdef double *m = self.m
res.x = x * m[0] + y * m[4] + z * m[8] + m[12]
res.y = x * m[1] + y * m[5] + z * m[9] + m[13]
res.z = x * m[2] + y * m[6] + z * m[10] + m[14]
return res
def transform_direction(self, vector: UVec, normalize=False) -> Vec3:
cdef Vec3 res = Vec3(vector)
cdef double x = res.x
cdef double y = res.y
cdef double z = res.z
cdef double *m = self.m
res.x = x * m[0] + y * m[4] + z * m[8]
res.y = x * m[1] + y * m[5] + z * m[9]
res.z = x * m[2] + y * m[6] + z * m[10]
if normalize:
return v3_normalize(res, 1.0)
else:
return res
ocs_to_wcs = transform_direction
def transform_vertices(self, vectors: Iterable[UVec]) -> Iterator[Vec3]:
cdef double *m = self.m
cdef Vec3 res
cdef double x, y, z
for vector in vectors:
res = Vec3(vector)
x = res.x
y = res.y
z = res.z
res.x = x * m[0] + y * m[4] + z * m[8] + m[12]
res.y = x * m[1] + y * m[5] + z * m[9] + m[13]
res.z = x * m[2] + y * m[6] + z * m[10] + m[14]
yield res
def fast_2d_transform(self, points: Iterable[UVec]) -> Iterator[Vec2]:
cdef double m0 = self.m[0]
cdef double m1 = self.m[1]
cdef double m4 = self.m[4]
cdef double m5 = self.m[5]
cdef double m12 = self.m[12]
cdef double m13 = self.m[13]
cdef double x, y
cdef Vec2 res
for pnt in points:
res = Vec2(pnt)
x = res.x
y = res.y
res.x = x * m0 + y * m4 + m12
res.y = x * m1 + y * m5 + m13
yield res
def transform_array_inplace(self, array: np.ndarray, ndim: int) -> None:
"""Transforms a numpy array inplace, the argument `ndim` defines the dimensions
to transform, this allows 2D/3D transformation on arrays with more columns
e.g. a polyline array which stores points as (x, y, start_width, end_width,
bulge) values.
"""
cdef int _ndim = ndim
if _ndim == 2:
assert array.shape[1] > 1
transform_2d_array_inplace(self.m, array, array.shape[0])
elif _ndim == 3:
assert array.shape[1] > 2
transform_3d_array_inplace(self.m, array, array.shape[0])
else:
raise ValueError("ndim has to be 2 or 3")
def transform_directions(
self, vectors: Iterable[UVec], normalize=False
) -> Iterator[Vec3]:
cdef double *m = self.m
cdef Vec3 res
cdef double x, y, z
cdef bint _normalize = normalize
for vector in vectors:
res = Vec3(vector)
x = res.x
y = res.y
z = res.z
res.x = x * m[0] + y * m[4] + z * m[8]
res.y = x * m[1] + y * m[5] + z * m[9]
res.z = x * m[2] + y * m[6] + z * m[10]
yield v3_normalize(res, 1.0) if _normalize else res
def ucs_vertex_from_wcs(self, wcs: Vec3) -> Vec3:
return self.ucs_direction_from_wcs(wcs - self.origin)
def ucs_direction_from_wcs(self, wcs: UVec) -> Vec3:
cdef double *m = self.m
cdef Vec3 res = Vec3(wcs)
cdef double x = res.x
cdef double y = res.y
cdef double z = res.z
res.x = x * m[0] + y * m[1] + z * m[2]
res.y = x * m[4] + y * m[5] + z * m[6]
res.z = x * m[8] + y * m[9] + z * m[10]
return res
ocs_from_wcs = ucs_direction_from_wcs
@cython.boundscheck(False)
@cython.wraparound(False)
cdef void transform_2d_array_inplace(double *m, double [:, ::1] array, Py_ssize_t size):
cdef double m0 = m[0]
cdef double m1 = m[1]
cdef double m4 = m[4]
cdef double m5 = m[5]
cdef double m12 = m[12]
cdef double m13 = m[13]
cdef double x, y
cdef Py_ssize_t i
for i in range(size):
x = array[i, 0]
y = array[i, 1]
array[i, 0] = x * m0 + y * m4 + m12
array[i, 1] = x * m1 + y * m5 + m13
@cython.boundscheck(False)
@cython.wraparound(False)
cdef void transform_3d_array_inplace(double *m, double [:, ::1] array, Py_ssize_t size):
cdef double x, y, z
cdef Py_ssize_t i
for i in range(size):
x = array[i, 0]
y = array[i, 1]
z = array[i, 2]
array[i, 0] = x * m[0] + y * m[4] + z * m[8] + m[12]
array[i, 1] = x * m[1] + y * m[5] + z * m[9] + m[13]
array[i, 2] = x * m[2] + y * m[6] + z * m[10] + m[14]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,177 @@
# cython: language_level=3
# Copyright (c) 2023-2024, Manfred Moitzi
# License: MIT License
from typing_extensions import TypeAlias
import numpy as np
import numpy.typing as npt
import cython
from .vector cimport isclose
cdef extern from "constants.h":
const double ABS_TOL
const double REL_TOL
NDArray: TypeAlias = npt.NDArray[np.float64]
def has_clockwise_orientation(vertices: np.ndarray) -> bool:
""" Returns True if 2D `vertices` have clockwise orientation. Ignores
z-axis of all vertices.
Args:
vertices: numpy array
Raises:
ValueError: less than 3 vertices
"""
if len(vertices) < 3:
raise ValueError('At least 3 vertices required.')
return _has_clockwise_orientation(vertices, vertices.shape[0])
@cython.boundscheck(False)
@cython.wraparound(False)
cdef bint _has_clockwise_orientation(double [:, ::1] vertices, Py_ssize_t size):
cdef Py_ssize_t index
cdef Py_ssize_t start
cdef Py_ssize_t last = size - 1
cdef double s = 0.0
cdef double p1x = vertices[0][0]
cdef double p1y = vertices[0][1]
cdef double p2x = vertices[last][0]
cdef double p2y = vertices[last][1]
# Using the same tolerance as the Python implementation:
cdef bint x_is_close = isclose(p1x, p2x, REL_TOL, ABS_TOL)
cdef bint y_is_close = isclose(p1y, p2y, REL_TOL, ABS_TOL)
if x_is_close and y_is_close:
p1x = vertices[0][0]
p1y = vertices[0][1]
start = 1
else:
p1x = vertices[last][0]
p1y = vertices[last][1]
start = 0
for index in range(start, size):
p2x = vertices[index][0]
p2y = vertices[index][1]
s += (p2x - p1x) * (p2y + p1y)
p1x = p2x
p1y = p2y
return s > 0.0
def lu_decompose(A: NDArray, m1: int, m2: int) -> tuple[NDArray, NDArray, NDArray]:
upper: np.ndarray = np.array(A, dtype=np.float64)
n: int = upper.shape[0]
lower: np.ndarray = np.zeros((n, m1), dtype=np.float64)
# Is <np.int_> better to match <int> on all platforms?
index: np.ndarray = np.zeros((n,), dtype=np.int32)
_lu_decompose_cext(upper, lower, index, n, m1, m2)
return upper, lower, index
@cython.boundscheck(False)
@cython.wraparound(False)
cdef _lu_decompose_cext(
double [:, ::1] upper,
double [:, ::1] lower,
int [::1] index,
int n,
int m1,
int m2
):
cdef int mm = m1 + m2 + 1
cdef int l = m1
cdef int i, j, k
cdef double dum
for i in range(m1):
for j in range(m1 - i, mm):
upper[i][j - l] = upper[i][j]
l -= 1
for j in range(mm - l - 1, mm):
upper[i][j] = 0.0
l = m1
for k in range(n):
dum = upper[k][0]
i = k
if l < n:
l += 1
for j in range(k + 1, l):
if abs(upper[j][0]) > abs(dum):
dum = upper[j][0]
i = j
index[k] = i + 1
if i != k:
for j in range(mm):
upper[k][j], upper[i][j] = upper[i][j], upper[k][j]
for i in range(k + 1, l):
dum = upper[i][0] / upper[k][0]
lower[k][i - k - 1] = dum
for j in range(1, mm):
upper[i][j - 1] = upper[i][j] - dum * upper[k][j]
upper[i][mm - 1] = 0.0
def solve_vector_banded_matrix(
x: NDArray,
upper: NDArray,
lower: NDArray,
index: NDArray,
m1: int,
m2: int,
) -> NDArray:
n: int = upper.shape[0]
x = np.array(x) # copy x because array x gets modified
if x.shape[0] != n:
raise ValueError(
"Item count of vector <x> has to match the row count of matrix <upper>."
)
_solve_vector_banded_matrix_cext(x, upper, lower, index, n, m1, m2)
return x
@cython.boundscheck(False)
@cython.wraparound(False)
cdef _solve_vector_banded_matrix_cext(
double [::1] x,
double [:, ::1] au,
double [:, ::1] al,
int [::1] index,
int n,
int m1,
int m2,
):
cdef int mm = m1 + m2 + 1
cdef int l = m1
cdef int i, j, k
cdef double dum
for k in range(n):
j = index[k] - 1
if j != k:
x[k], x[j] = x[j], x[k]
if l < n:
l += 1
for j in range(k + 1, l):
x[j] -= al[k][j - k - 1] * x[k]
l = 1
for i in range(n - 1, -1, -1):
dum = x[i]
for k in range(1, l):
dum -= au[i][k] * x[k + i]
x[i] = dum / au[i][0]
if l < mm:
l += 1
return x

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,49 @@
# cython: language_level=3
# Copyright (c) 2020-2024, Manfred Moitzi
# License: MIT License
cdef bint isclose(double a, double b, double rel_tol, double abs_tol)
cdef double normalize_rad_angle(double a)
cdef double normalize_deg_angle(double a)
cdef class Vec2:
cdef readonly double x, y
# Vec2 C-functions:
cdef Vec2 v2_add(Vec2 a, Vec2 b)
cdef Vec2 v2_sub(Vec2 a, Vec2 b)
cdef Vec2 v2_mul(Vec2 a, double factor)
cdef Vec2 v2_normalize(Vec2 a, double length)
cdef double v2_dot(Vec2 a, Vec2 b)
cdef double v2_det(Vec2 a, Vec2 b)
cdef double v2_dist(Vec2 a, Vec2 b)
cdef Vec2 v2_from_angle(double angle, double length)
cdef double v2_angle_between(Vec2 a, Vec2 b) except -1000
cdef Vec2 v2_lerp(Vec2 a, Vec2 b, double factor)
cdef Vec2 v2_ortho(Vec2 a, bint ccw)
cdef Vec2 v2_project(Vec2 a, Vec2 b)
cdef bint v2_isclose(Vec2 a, Vec2 b, double rel_tol, double abs_tol)
cdef class Vec3:
cdef readonly double x, y, z
# Vec3 C-functions:
cdef Vec3 v3_add(Vec3 a, Vec3 b)
cdef Vec3 v3_sub(Vec3 a, Vec3 b)
cdef Vec3 v3_mul(Vec3 a, double factor)
cdef Vec3 v3_reverse(Vec3 a)
cdef double v3_dot(Vec3 a, Vec3 b)
cdef Vec3 v3_cross(Vec3 a, Vec3 b)
cdef double v3_magnitude_sqr(Vec3 a)
cdef double v3_magnitude(Vec3 a)
cdef double v3_dist(Vec3 a, Vec3 b)
cdef Vec3 v3_from_angle(double angle, double length)
cdef double v3_angle_between(Vec3 a, Vec3 b) except -1000
cdef double v3_angle_about(Vec3 a, Vec3 base, Vec3 target)
cdef Vec3 v3_normalize(Vec3 a, double length)
cdef Vec3 v3_lerp(Vec3 a, Vec3 b, double factor)
cdef Vec3 v3_ortho(Vec3 a, bint ccw)
cdef Vec3 v3_project(Vec3 a, Vec3 b)
cdef bint v3_isclose(Vec3 a, Vec3 b, double rel_tol, double abs_tol)

View File

@@ -0,0 +1,838 @@
# cython: language_level=3
# cython: c_api_binop_methods=True
# Copyright (c) 2020-2024, Manfred Moitzi
# License: MIT License
from typing import Iterable, List, Sequence, TYPE_CHECKING, Tuple, Iterator
from libc.math cimport fabs, sin, cos, M_PI, hypot, atan2, acos, sqrt, fmod
import random
cdef extern from "constants.h":
const double ABS_TOL
const double REL_TOL
const double M_TAU
cdef double RAD2DEG = 180.0 / M_PI
cdef double DEG2RAD = M_PI / 180.0
if TYPE_CHECKING:
from ezdxf.math import AnyVec, UVec
cdef bint isclose(double a, double b, double rel_tol, double abs_tol):
# Has to match the Python implementation!
cdef double diff = fabs(b - a)
return diff <= fabs(rel_tol * b) or \
diff <= fabs(rel_tol * a) or \
diff <= abs_tol
cdef double normalize_rad_angle(double a):
# Emulate the Python behavior of (a % math.tau)
cdef double res = fmod(a, M_TAU)
if res < 0.0:
res += M_TAU
return res
def _normalize_rad_angle(double angle):
# just for testing
return normalize_rad_angle(angle)
cdef double normalize_deg_angle(double a):
# Emulate the Python behavior of (a % 360)
cdef double res = fmod(a, 360.0)
if res < 0.0:
res += 360.0
return res
def _normalize_deg_angle(double angle):
# just for testing
return normalize_deg_angle(angle)
cdef class Vec2:
""" Immutable 2D vector.
Init:
- Vec2(vec2)
- Vec2(vec3)
- Vec2((x, y))
- Vec2((x, y, z)), ignore z-axis
- Vec2(x, y)
- Vec2(x, y, z), ignore z-axis
"""
def __cinit__(self, *args):
cdef Py_ssize_t count = len(<tuple> args)
if count == 0: # fastest init - default constructor
self.x = 0
self.y = 0
return
if count == 1:
arg = args[0]
if isinstance(arg, Vec2):
# fast init by Vec2()
self.x = (<Vec2> arg).x
self.y = (<Vec2> arg).y
return
if isinstance(arg, Vec3):
# fast init by Vec3()
self.x = (<Vec3> arg).x
self.y = (<Vec3> arg).y
return
args = arg
count = len(args)
# ignore z-axis but raise error for 4 or more arguments
if count > 3:
raise TypeError('invalid argument count')
# slow init by sequence
self.x = args[0]
self.y = args[1]
def __reduce__(self):
return Vec2, (self.x, self.y)
@property
def vec3(self) -> Vec3:
return Vec3(self)
def round(self, ndigits=None) -> Vec2:
# only used for testing
return Vec2(round(self.x, ndigits), round(self.y, ndigits))
@staticmethod
def list(items: Iterable[UVec]) -> List[Vec2]:
return list(Vec2.generate(items))
@staticmethod
def tuple(items: Iterable[UVec]) -> Sequence[Vec2]:
return tuple(Vec2.generate(items))
@staticmethod
def generate(items: Iterable[UVec]) -> Iterator[Vec2]:
return (Vec2(item) for item in items)
@staticmethod
def from_angle(double angle, double length = 1.0) -> Vec2:
return v2_from_angle(angle, length)
@staticmethod
def from_deg_angle(double angle, double length = 1.0) -> Vec2:
return v2_from_angle(angle * DEG2RAD, length)
def __str__(self) -> str:
return f'({self.x}, {self.y})'
def __repr__(self)-> str:
return "Vec2" + self.__str__()
def __len__(self) -> int:
return 2
def __hash__(self) -> int:
return hash((self.x, self.y))
def copy(self) -> Vec2:
return self # immutable
def __copy__(self) -> Vec2:
return self # immutable
def __deepcopy__(self, memodict: dict) -> Vec2:
return self # immutable
def __getitem__(self, int index) -> float:
if index == 0:
return self.x
elif index == 1:
return self.y
else:
raise IndexError(f'invalid index {index}')
def __iter__(self) -> Iterator[float]:
yield self.x
yield self.y
def __abs__(self) -> float:
return hypot(self.x, self.y)
@property
def magnitude(self) -> float:
return hypot(self.x, self.y)
@property
def is_null(self) -> bool:
return fabs(self.x) <= ABS_TOL and fabs(self.y) <= ABS_TOL
@property
def angle(self) -> float:
return atan2(self.y, self.x)
@property
def angle_deg(self) -> float:
return atan2(self.y, self.x) * RAD2DEG
def orthogonal(self, ccw: bool = True) -> Vec2:
return v2_ortho(self, ccw)
def lerp(self, other: "AnyVec", double factor = 0.5) -> Vec2:
cdef Vec2 o = Vec2(other)
return v2_lerp(self, o, factor)
def normalize(self, double length = 1.) -> Vec2:
return v2_normalize(self, length)
def project(self, other: AnyVec) -> Vec2:
cdef Vec2 o = Vec2(other)
return v2_project(self, o)
def __neg__(self) -> Vec2:
cdef Vec2 res = Vec2()
res.x = -self.x
res.y = -self.y
return res
reversed = __neg__
def __bool__(self) -> bool:
return self.x != 0 or self.y != 0
def isclose(self, other: UVec, *, double rel_tol=REL_TOL,
double abs_tol = ABS_TOL) -> bool:
cdef Vec2 o = Vec2(other)
return isclose(self.x, o.x, rel_tol, abs_tol) and \
isclose(self.y, o.y, rel_tol, abs_tol)
def __eq__(self, other: UVec) -> bool:
if not isinstance(other, Vec2):
other = Vec2(other)
return self.x == other.x and self.y == other.y
def __lt__(self, other) -> bool:
cdef Vec2 o = Vec2(other)
if self.x == o.x:
return self.y < o.y
else:
return self.x < o.x
def __add__(self, other: AnyVec) -> Vec2:
if not isinstance(other, Vec2):
other = Vec2(other)
return v2_add(self, <Vec2> other)
# __radd__ not supported for Vec2
__iadd__ = __add__ # immutable
def __sub__(self, other: AnyVec) -> Vec2:
if not isinstance(other, Vec2):
other = Vec2(other)
return v2_sub(self, <Vec2> other)
# __rsub__ not supported for Vec2
__isub__ = __sub__ # immutable
def __mul__(self, factor) -> Vec2:
if isinstance(self, Vec2):
return v2_mul(self, factor)
elif isinstance(factor, Vec2):
return v2_mul(<Vec2> factor, <double> self)
else:
return NotImplemented
# Special Cython <(3.0) feature: __rmul__ == __mul__(factor, self)
def __rmul__(self, double factor) -> Vec2:
# for Cython >= 3.0
return v2_mul(self, factor)
__imul__ = __mul__ # immutable
def __truediv__(self, double factor) -> Vec2:
return v2_mul(self, 1.0 / factor)
# __rtruediv__ not supported -> TypeError
def dot(self, other: AnyVec) -> float:
cdef Vec2 o = Vec2(other)
return v2_dot(self, o)
def det(self, other: AnyVec) -> float:
cdef Vec2 o = Vec2(other)
return v2_det(self, o)
def distance(self, other: AnyVec) -> float:
cdef Vec2 o = Vec2(other)
return v2_dist(self, o)
def angle_between(self, other: AnyVec) -> float:
cdef Vec2 o = Vec2(other)
return v2_angle_between(self, o)
def rotate(self, double angle) -> Vec2:
cdef double self_angle = atan2(self.y, self.x)
cdef double magnitude = hypot(self.x, self.y)
return v2_from_angle(self_angle + angle, magnitude)
def rotate_deg(self, double angle) -> Vec2:
return self.rotate(angle * DEG2RAD)
@staticmethod
def sum(items: Iterable[Vec2]) -> Vec2:
cdef Vec2 res = Vec2()
cdef Vec2 tmp
res.x = 0.0
res.y = 0.0
for v in items:
tmp = v
res.x += tmp.x
res.y += tmp.y
return res
cdef Vec2 v2_add(Vec2 a, Vec2 b):
res = Vec2()
res.x = a.x + b.x
res.y = a.y + b.y
return res
cdef Vec2 v2_sub(Vec2 a, Vec2 b):
res = Vec2()
res.x = a.x - b.x
res.y = a.y - b.y
return res
cdef Vec2 v2_mul(Vec2 a, double factor):
res = Vec2()
res.x = a.x * factor
res.y = a.y * factor
return res
cdef double v2_dot(Vec2 a, Vec2 b):
return a.x * b.x + a.y * b.y
cdef double v2_det(Vec2 a, Vec2 b):
return a.x * b.y - a.y * b.x
cdef double v2_dist(Vec2 a, Vec2 b):
return hypot(a.x - b.x, a.y - b.y)
cdef Vec2 v2_from_angle(double angle, double length):
cdef Vec2 res = Vec2()
res.x = cos(angle) * length
res.y = sin(angle) * length
return res
cdef double v2_angle_between(Vec2 a, Vec2 b) except -1000:
cdef double cos_theta = v2_dot(v2_normalize(a, 1.0), v2_normalize(b, 1.0))
# avoid domain errors caused by floating point imprecision:
if cos_theta < -1.0:
cos_theta = -1.0
elif cos_theta > 1.0:
cos_theta = 1.0
return acos(cos_theta)
cdef Vec2 v2_normalize(Vec2 a, double length):
cdef double factor = length / hypot(a.x, a.y)
cdef Vec2 res = Vec2()
res.x = a.x * factor
res.y = a.y * factor
return res
cdef Vec2 v2_lerp(Vec2 a, Vec2 b, double factor):
cdef Vec2 res = Vec2()
res.x = a.x + (b.x - a.x) * factor
res.y = a.y + (b.y - a.y) * factor
return res
cdef Vec2 v2_ortho(Vec2 a, bint ccw):
cdef Vec2 res = Vec2()
if ccw:
res.x = -a.y
res.y = a.x
else:
res.x = a.y
res.y = -a.x
return res
cdef Vec2 v2_project(Vec2 a, Vec2 b):
cdef Vec2 uv = v2_normalize(a, 1.0)
return v2_mul(uv, v2_dot(uv, b))
cdef bint v2_isclose(Vec2 a, Vec2 b, double rel_tol, double abs_tol):
return isclose(a.x, b.x, rel_tol, abs_tol) and \
isclose(a.y, b.y, rel_tol, abs_tol)
cdef class Vec3:
""" Immutable 3D vector.
Init:
- Vec3()
- Vec3(vec3)
- Vec3(vec2)
- Vec3((x, y))
- Vec3((x, y, z))
- Vec3(x, y)
- Vec3(x, y, z)
"""
def __cinit__(self, *args):
cdef Py_ssize_t count = len(<tuple> args)
if count == 0: # fastest init - default constructor
self.x = 0
self.y = 0
self.z = 0
return
if count == 1:
arg0 = args[0]
if isinstance(arg0, Vec3):
# fast init by Vec3()
self.x = (<Vec3> arg0).x
self.y = (<Vec3> arg0).y
self.z = (<Vec3> arg0).z
return
if isinstance(arg0, Vec2):
# fast init by Vec2()
self.x = (<Vec2> arg0).x
self.y = (<Vec2> arg0).y
self.z = 0
return
args = arg0
count = len(args)
if count > 3 or count < 2:
raise TypeError('invalid argument count')
# slow init by sequence
self.x = args[0]
self.y = args[1]
if count > 2:
self.z = args[2]
else:
self.z = 0.0
def __reduce__(self):
return Vec3, self.xyz
@property
def xy(self) -> Vec3:
cdef Vec3 res = Vec3()
res.x = self.x
res.y = self.y
return res
@property
def xyz(self) -> Tuple[float, float, float]:
return self.x, self.y, self.z
@property
def vec2(self) -> Vec2:
cdef Vec2 res = Vec2()
res.x = self.x
res.y = self.y
return res
def replace(self, x: float = None, y: float = None,
z: float = None) -> Vec3:
cdef Vec3 res = Vec3()
res.x = self.x if x is None else x
res.y = self.y if y is None else y
res.z = self.z if z is None else z
return res
def round(self, ndigits: int | None = None) -> Vec3:
return Vec3(
round(self.x, ndigits),
round(self.y, ndigits),
round(self.z, ndigits),
)
@staticmethod
def list(items: Iterable[UVec]) -> List[Vec3]:
return list(Vec3.generate(items))
@staticmethod
def tuple(items: Iterable[UVec]) -> Sequence[Vec3]:
return tuple(Vec3.generate(items))
@staticmethod
def generate(items: Iterable[UVec]) -> Iterator[Vec3]:
return (Vec3(item) for item in items)
@staticmethod
def from_angle(double angle, double length = 1.0) -> Vec3:
return v3_from_angle(angle, length)
@staticmethod
def from_deg_angle(double angle, double length = 1.0) -> Vec3:
return v3_from_angle(angle * DEG2RAD, length)
@staticmethod
def random(double length = 1.0) -> Vec3:
cdef Vec3 res = Vec3()
uniform = random.uniform
res.x = uniform(-1, 1)
res.y = uniform(-1, 1)
res.z = uniform(-1, 1)
return v3_normalize(res, length)
def __str__(self) -> str:
return f'({self.x}, {self.y}, {self.z})'
def __repr__(self)-> str:
return "Vec3" + self.__str__()
def __len__(self) -> int:
return 3
def __hash__(self) -> int:
return hash(self.xyz)
def copy(self) -> Vec3:
return self # immutable
__copy__ = copy
def __deepcopy__(self, memodict: dict) -> Vec3:
return self # immutable!
def __getitem__(self, int index) -> float:
if index == 0:
return self.x
elif index == 1:
return self.y
elif index == 2:
return self.z
else:
raise IndexError(f'invalid index {index}')
def __iter__(self) -> Iterator[float]:
yield self.x
yield self.y
yield self.z
def __abs__(self) -> float:
return v3_magnitude(self)
@property
def magnitude(self) -> float:
return v3_magnitude(self)
@property
def magnitude_xy(self) -> float:
return hypot(self.x, self.y)
@property
def magnitude_square(self) -> float:
return v3_magnitude_sqr(self)
@property
def is_null(self) -> bool:
return fabs(self.x) <= ABS_TOL and fabs(self.y) <= ABS_TOL and \
fabs(self.z) <= ABS_TOL
def is_parallel(self, other: UVec, *, double rel_tol=REL_TOL,
double abs_tol = ABS_TOL) -> bool:
cdef Vec3 o = Vec3(other)
cdef Vec3 v1 = v3_normalize(self, 1.0)
cdef Vec3 v2 = v3_normalize(o, 1.0)
cdef Vec3 neg_v2 = v3_reverse(v2)
return v3_isclose(v1, v2, rel_tol, abs_tol) or \
v3_isclose(v1, neg_v2, rel_tol, abs_tol)
@property
def spatial_angle(self) -> float:
return acos(v3_dot(<Vec3> X_AXIS, v3_normalize(self, 1.0)))
@property
def spatial_angle_deg(self) -> float:
return self.spatial_angle * RAD2DEG
@property
def angle(self) -> float:
return atan2(self.y, self.x)
@property
def angle_deg(self) -> float:
return atan2(self.y, self.x) * RAD2DEG
def orthogonal(self, ccw: bool = True) -> Vec3:
return v3_ortho(self, ccw)
def lerp(self, other: UVec, double factor=0.5) -> Vec3:
if not isinstance(other, Vec3):
other = Vec3(other)
return v3_lerp(self, <Vec3> other, factor)
def project(self, other: UVec) -> Vec3:
if not isinstance(other, Vec3):
other = Vec3(other)
return v3_project(self, <Vec3> other)
def normalize(self, double length = 1.) -> Vec3:
return v3_normalize(self, length)
def reversed(self) -> Vec3:
return v3_reverse(self)
def __neg__(self) -> Vec3:
return v3_reverse(self)
def __bool__(self) -> bool:
return not self.is_null
def isclose(self, other: UVec, *, double rel_tol = REL_TOL,
double abs_tol = ABS_TOL) -> bool:
if not isinstance(other, Vec3):
other = Vec3(other)
return v3_isclose(self, <Vec3> other, rel_tol, abs_tol)
def __eq__(self, other: UVec) -> bool:
if not isinstance(other, Vec3):
other = Vec3(other)
return self.x == other.x and self.y == other.y and self.z == other.z
def __lt__(self, other: UVec) -> bool:
if not isinstance(other, Vec3):
other = Vec3(other)
if self.x == (<Vec3> other).x:
return self.y < (<Vec3> other).y
else:
return self.x < (<Vec3> other).x
# Special Cython (<3.0) feature: __radd__ == __add__(other, self)
def __add__(self, other) -> Vec3:
if not isinstance(self, Vec3):
# other is the real self
return v3_add(Vec3(self), <Vec3> other)
if not isinstance(other, Vec3):
other = Vec3(other)
return v3_add(<Vec3> self, <Vec3> other)
__radd__ = __add__ # Cython >= 3.0
__iadd__ = __add__ # immutable
# Special Cython (<3.0) feature: __rsub__ == __sub__(other, self)
def __sub__(self, other) -> Vec3:
if not isinstance(self, Vec3):
# other is the real self
return v3_sub(Vec3(self), <Vec3> other)
if not isinstance(other, Vec3):
other = Vec3(other)
return v3_sub(<Vec3> self, <Vec3> other)
def __rsub__(self, other) -> Vec3:
# for Cython >= 3.0
return v3_sub(Vec3(other), <Vec3> self)
__isub__ = __sub__ # immutable
# Special Cython <(3.0) feature: __rmul__ == __mul__(factor, self)
def __mul__(self, factor) -> Vec3:
if isinstance(factor, Vec3):
return v3_mul(<Vec3> factor, self)
return v3_mul(<Vec3> self, factor)
def __rmul__(self, double factor) -> Vec3:
# for Cython >= 3.0
return v3_mul(self, factor)
__imul__ = __mul__ # immutable
def __truediv__(self, double factor) -> Vec3:
return v3_mul(self, 1.0 / factor)
# __rtruediv__ not supported -> TypeError
@staticmethod
def sum(items: Iterable[UVec]) -> Vec3:
cdef Vec3 res = Vec3()
cdef Vec3 tmp
for v in items:
tmp = Vec3(v)
res.x += tmp.x
res.y += tmp.y
res.z += tmp.z
return res
def dot(self, other: UVec) -> float:
cdef Vec3 o = Vec3(other)
return v3_dot(self, o)
def cross(self, other: UVec) -> Vec3:
cdef Vec3 o = Vec3(other)
return v3_cross(self, o)
def distance(self, other: UVec) -> float:
cdef Vec3 o = Vec3(other)
return v3_dist(self, o)
def angle_between(self, other: UVec) -> float:
cdef Vec3 o = Vec3(other)
return v3_angle_between(self, o)
def angle_about(self, base: UVec, target: UVec) -> float:
cdef Vec3 b = Vec3(base)
cdef Vec3 t = Vec3(target)
return v3_angle_about(self, b, t)
def rotate(self, double angle) -> Vec3:
cdef double angle_ = atan2(self.y, self.x) + angle
cdef double magnitude_ = hypot(self.x, self.y)
cdef Vec3 res = Vec3.from_angle(angle_, magnitude_)
res.z = self.z
return res
def rotate_deg(self, double angle) -> Vec3:
return self.rotate(angle * DEG2RAD)
X_AXIS = Vec3(1, 0, 0)
Y_AXIS = Vec3(0, 1, 0)
Z_AXIS = Vec3(0, 0, 1)
NULLVEC = Vec3(0, 0, 0)
cdef Vec3 v3_add(Vec3 a, Vec3 b):
res = Vec3()
res.x = a.x + b.x
res.y = a.y + b.y
res.z = a.z + b.z
return res
cdef Vec3 v3_sub(Vec3 a, Vec3 b):
res = Vec3()
res.x = a.x - b.x
res.y = a.y - b.y
res.z = a.z - b.z
return res
cdef Vec3 v3_mul(Vec3 a, double factor):
res = Vec3()
res.x = a.x * factor
res.y = a.y * factor
res.z = a.z * factor
return res
cdef Vec3 v3_reverse(Vec3 a):
cdef Vec3 res = Vec3()
res.x = -a.x
res.y = -a.y
res.z = -a.z
return res
cdef double v3_dot(Vec3 a, Vec3 b):
return a.x * b.x + a.y * b.y + a.z * b.z
cdef Vec3 v3_cross(Vec3 a, Vec3 b):
res = Vec3()
res.x = a.y * b.z - a.z * b.y
res.y = a.z * b.x - a.x * b.z
res.z = a.x * b.y - a.y * b.x
return res
cdef inline double v3_magnitude_sqr(Vec3 a):
return a.x * a.x + a.y * a.y + a.z * a.z
cdef inline double v3_magnitude(Vec3 a):
return sqrt(v3_magnitude_sqr(a))
cdef double v3_dist(Vec3 a, Vec3 b):
cdef double dx = a.x - b.x
cdef double dy = a.y - b.y
cdef double dz = a.z - b.z
return sqrt(dx * dx + dy * dy + dz * dz)
cdef Vec3 v3_from_angle(double angle, double length):
cdef Vec3 res = Vec3()
res.x = cos(angle) * length
res.y = sin(angle) * length
return res
cdef double v3_angle_between(Vec3 a, Vec3 b) except -1000:
cdef double cos_theta = v3_dot(v3_normalize(a, 1.0), v3_normalize(b, 1.0))
# avoid domain errors caused by floating point imprecision:
if cos_theta < -1.0:
cos_theta = -1.0
elif cos_theta > 1.0:
cos_theta = 1.0
return acos(cos_theta)
cdef double v3_angle_about(Vec3 a, Vec3 base, Vec3 target):
cdef Vec3 x_axis = v3_normalize(v3_sub(base, v3_project(a, base)), 1.0)
cdef Vec3 y_axis = v3_normalize(v3_cross(a, x_axis), 1.0)
cdef double target_projected_x = v3_dot(x_axis, target)
cdef double target_projected_y = v3_dot(y_axis, target)
return normalize_rad_angle(atan2(target_projected_y, target_projected_x))
cdef Vec3 v3_normalize(Vec3 a, double length):
cdef double factor = length / v3_magnitude(a)
cdef Vec3 res = Vec3()
res.x = a.x * factor
res.y = a.y * factor
res.z = a.z * factor
return res
cdef Vec3 v3_lerp(Vec3 a, Vec3 b, double factor):
cdef Vec3 res = Vec3()
res.x = a.x + (b.x - a.x) * factor
res.y = a.y + (b.y - a.y) * factor
res.z = a.z + (b.z - a.z) * factor
return res
cdef Vec3 v3_ortho(Vec3 a, bint ccw):
cdef Vec3 res = Vec3()
res.z = a.z
if ccw:
res.x = -a.y
res.y = a.x
else:
res.x = a.y
res.y = -a.x
return res
cdef Vec3 v3_project(Vec3 a, Vec3 b):
cdef Vec3 uv = v3_normalize(a, 1.0)
return v3_mul(uv, v3_dot(uv, b))
cdef bint v3_isclose(Vec3 a, Vec3 b, double rel_tol, double abs_tol):
return isclose(a.x, b.x, rel_tol, abs_tol) and \
isclose(a.y, b.y, rel_tol, abs_tol) and \
isclose(a.z, b.z, rel_tol, abs_tol)
def distance(p1: UVec, p2: UVec) -> float:
cdef Vec3 a = Vec3(p1)
cdef Vec3 b = Vec3(p2)
return v3_dist(a, b)
def lerp(p1: UVec, p2: UVec, double factor = 0.5) -> Vec3:
cdef Vec3 a = Vec3(p1)
cdef Vec3 b = Vec3(p2)
return v3_lerp(a, b, factor)

View File

@@ -0,0 +1,3 @@
# Copyright (c) 2022-2024, Manfred Moitzi
# License: MIT License
# Users should always import from ezdxf.acis.api!

View File

@@ -0,0 +1,224 @@
# Copyright (c) 2022-2024, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TypeVar, Generic, TYPE_CHECKING, Optional
from abc import ABC, abstractmethod
from .const import NULL_PTR_NAME, MIN_EXPORT_VERSION
from .hdr import AcisHeader
if TYPE_CHECKING:
from .entities import AcisEntity
from ezdxf.math import Vec3
T = TypeVar("T", bound="AbstractEntity")
class AbstractEntity(ABC):
"""Unified query interface for SAT and SAB data."""
name: str
id: int = -1
def __str__(self):
return f"{self.name}"
@property
def is_null_ptr(self) -> bool:
"""Returns ``True`` if this entity is the ``NULL_PTR`` entity."""
return self.name == NULL_PTR_NAME
class DataLoader(ABC):
"""Data loading interface to create high level AcisEntity data from low
level AbstractEntity representation.
"""
version: int = MIN_EXPORT_VERSION
@abstractmethod
def has_data(self) -> bool:
pass
@abstractmethod
def read_int(self, skip_sat: Optional[int] = None) -> int:
"""There are sometimes additional int values in SAB files which are
not present in SAT files, maybe reference counters e.g. vertex, coedge.
"""
pass
@abstractmethod
def read_double(self) -> float:
pass
@abstractmethod
def read_interval(self) -> float:
pass
@abstractmethod
def read_vec3(self) -> tuple[float, float, float]:
pass
@abstractmethod
def read_bool(self, true: str, false: str) -> bool:
pass
@abstractmethod
def read_str(self) -> str:
pass
@abstractmethod
def read_ptr(self) -> AbstractEntity:
pass
@abstractmethod
def read_transform(self) -> list[float]:
pass
class DataExporter(ABC):
version: int = MIN_EXPORT_VERSION
@abstractmethod
def write_int(self, value: int, skip_sat=False) -> None:
"""There are sometimes additional int values in SAB files which are
not present in SAT files, maybe reference counters e.g. vertex, coedge.
"""
pass
@abstractmethod
def write_double(self, value: float) -> None:
pass
@abstractmethod
def write_interval(self, value: float) -> None:
pass
@abstractmethod
def write_loc_vec3(self, value: Vec3) -> None:
pass
@abstractmethod
def write_dir_vec3(self, value: Vec3) -> None:
pass
@abstractmethod
def write_bool(self, value: bool, true: str, false: str) -> None:
pass
@abstractmethod
def write_str(self, value: str) -> None:
pass
@abstractmethod
def write_literal_str(self, value: str) -> None:
pass
@abstractmethod
def write_ptr(self, entity: AcisEntity) -> None:
pass
@abstractmethod
def write_transform(self, data: list[str]) -> None:
pass
class AbstractBuilder(Generic[T]):
header: AcisHeader
bodies: list[T]
entities: list[T]
def reorder_records(self) -> None:
if len(self.entities) == 0:
return
header: list[T] = []
entities: list[T] = []
for e in self.entities:
if e.name == "body":
header.append(e)
elif e.name == "asmheader":
header.insert(0, e) # has to be the first record
else:
entities.append(e)
self.entities = header + entities
def reset_ids(self, start: int = 0) -> None:
for num, entity in enumerate(self.entities, start=start):
entity.id = num
def clear_ids(self) -> None:
for entity in self.entities:
entity.id = -1
class EntityExporter(Generic[T]):
def __init__(self, header: AcisHeader):
self.header = header
self.version = header.version
self._exported_entities: dict[int, T] = {}
if self.header.has_asm_header:
self.export(self.header.asm_header())
def export_records(self) -> list[T]:
return list(self._exported_entities.values())
@abstractmethod
def make_record(self, entity: AcisEntity) -> T:
pass
@abstractmethod
def make_data_exporter(self, record: T) -> DataExporter:
pass
def get_record(self, entity: AcisEntity) -> T:
assert not entity.is_none
return self._exported_entities[id(entity)]
def export(self, entity: AcisEntity):
if entity.is_none:
raise TypeError("invalid NONE_REF entity given")
self._make_all_records(entity)
self._export_data(entity)
def _has_record(self, entity: AcisEntity) -> bool:
return id(entity) in self._exported_entities
def _add_record(self, entity: AcisEntity, record: T) -> None:
assert not entity.is_none
self._exported_entities[id(entity)] = record
def _make_all_records(self, entity: AcisEntity):
def add(e: AcisEntity) -> bool:
if not e.is_none and not self._has_record(e):
self._add_record(e, self.make_record(e))
return True
return False
entities = [entity]
while entities:
next_entity = entities.pop(0)
add(next_entity)
for sub_entity in next_entity.entities():
if add(sub_entity):
entities.append(sub_entity)
def _export_data(self, entity: AcisEntity):
def _export_record(e: AcisEntity):
if id(e) not in done:
done.add(id(e))
record = self.get_record(e)
if not e.attributes.is_none:
record.attributes = self.get_record(e.attributes) # type: ignore
e.export(self.make_data_exporter(record))
return True
return False
entities = [entity]
done: set[int] = set()
while entities:
next_entity = entities.pop(0)
_export_record(next_entity)
for sub_entity in next_entity.entities():
if _export_record(sub_entity):
entities.append(sub_entity)

View File

@@ -0,0 +1,31 @@
# Copyright (c) 2022-2024, Manfred Moitzi
# License: MIT License
# Public API module (interface)
"""
The main goals of this ACIS support library is:
1. load and parse simple and known ACIS data structures
2. create and export simple and known ACIS data structures
It is NOT a goal to edit and export arbitrary existing ACIS structures.
Don't even try it!
This modules do not implement an ACIS kernel!!!
So tasks beyond stitching some flat polygonal faces to a polyhedron or creating
simple curves is not possible.
To all beginners: GO AWAY!
"""
from .const import (
AcisException,
ParsingError,
InvalidLinkStructure,
ExportError,
)
from .mesh import mesh_from_body, body_from_mesh, vertices_from_body
from .entities import load, export_sat, export_sab, Body
from .dbg import AcisDebugger, dump_sab_as_text
from .dxf import export_dxf, load_dxf
from .cache import AcisCache

View File

@@ -0,0 +1,59 @@
# Copyright (c) 2024, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Sequence
from .entities import Body, load
from .type_hints import EncodedData
if TYPE_CHECKING:
from ezdxf.entities import DXFEntity
__all__ = ["AcisCache"]
NO_BODIES: Sequence[Body] = tuple()
class AcisCache:
"""This cache manages ACIS bodies created from SAT or SAB data stored in DXF
entities.
Each entry is a list of ACIS bodies and is indexed by a hash calculated from the
source content of the SAT or SAB data.
"""
def __init__(self) -> None:
self._entries: dict[int, Sequence[Body]] = {}
self.hits: int = 0
self.misses: int = 0
@staticmethod
def hash_data(data: EncodedData) -> int:
if isinstance(data, list):
return hash(tuple(data))
elif isinstance(data, bytearray):
return hash(bytes(data))
return hash(data)
def __len__(self) -> int:
return len(self._entries)
def get_bodies(self, data: EncodedData) -> Sequence[Body]:
if not data:
return NO_BODIES
hash_value = AcisCache.hash_data(data)
bodies = self._entries.get(hash_value, NO_BODIES)
if bodies is not NO_BODIES:
self.hits += 1
return bodies
self.misses += 1
bodies = tuple(load(data))
self._entries[hash_value] = bodies
return bodies
def from_dxf_entity(self, entity: DXFEntity) -> Sequence[Body]:
from ezdxf.entities import Body as DxfBody
if not isinstance(entity, DxfBody):
return NO_BODIES
return self.get_bodies(entity.acis_data)

View File

@@ -0,0 +1,183 @@
# Copyright (c) 2022-2024, Manfred Moitzi
# License: MIT License
import enum
from ezdxf.version import __version__
# SAT Export Requirements for Autodesk Products
# ---------------------------------------------
# Script to create test files:
# examples/acistools/create_3dsolid_cube.py
# DXF R2000, R2004, R2007, R2010: OK, tested with TrueView 2022
# ACIS version 700
# ACIS version string: "ACIS 32.0 NT"
# record count: 0, not required
# body count: 1, required
# ASM header: no
# end-marker: "End-of-ACIS-data"
# DXF R2004, R2007, R2010: OK, tested with TrueView 2022
# ACIS version 20800
# ACIS version string: "ACIS 208.00 NT"
# record count: 0, not required
# body count: n + 1 (asm-header), required
# ASM header: "208.0.4.7009"
# end-marker: "End-of-ACIS-data"
# SAB Export Requirements for Autodesk Products
# ---------------------------------------------
# DXF R2013, R2018: OK, tested with TrueView 2022
# ACIS version 21800
# ACIS version string: "ACIS 208.00 NT"
# record count: 0, not required
# body count: n + 1 (asm-header), required
# ASM header: "208.0.4.7009"
# end-marker: "End-of-ASM-data"
ACIS_VERSION = {
400: "ACIS 4.00 NT", # DXF R2000, no asm header - only R2000
700: "ACIS 32.0 NT", # DXF R2000-R2010, no asm header
20800: "ACIS 208.00 NT", # DXF R2013 with asm-header, asm-end-marker
21800: "ACIS 218.00 NT", # DXF R2013 with asm-header, asm-end-marker
22300: "ACIS 223.00 NT", # DXF R2018 with asm-header, asm-end-marker
}
ASM_VERSION = {
20800: "208.0.4.7009", # DXF R2004, R2007, R2010
21800: "208.0.4.7009", # DXF R2013, default version for R2013 and R2018
22300: "222.0.0.1700", # DXF R2018
}
EZDXF_BUILDER_ID = f"ezdxf v{__version__} ACIS Builder"
MIN_EXPORT_VERSION = 700
# ACIS version 700 is the default version for DXF R2000, R2004, R2007 and R2010 (SAT)
# ACIS version 21800 is the default version for DXF R2013 and R2018 (SAB)
DEFAULT_SAT_VERSION = 700
DEFAULT_SAB_VERSION = 21800
DATE_FMT = "%a %b %d %H:%M:%S %Y"
END_OF_ACIS_DATA_SAT = "End-of-ACIS-data"
END_OF_ACIS_DATA_SAB = b"\x0e\x03End\x0e\x02of\x0e\x04ACIS\x0d\x04data"
END_OF_ASM_DATA_SAT = "End-of-ASM-data"
END_OF_ASM_DATA_SAB = b"\x0e\x03End\x0e\x02of\x0e\x03ASM\x0d\x04data"
BEGIN_OF_ACIS_HISTORY_DATA = "Begin-of-ACIS-History-data"
END_OF_ACIS_HISTORY_DATA = "End-of-ACIS-History-data"
DATA_END_MARKERS = (
END_OF_ACIS_DATA_SAT,
BEGIN_OF_ACIS_HISTORY_DATA,
END_OF_ASM_DATA_SAT,
)
NULL_PTR_NAME = "null-ptr"
NONE_ENTITY_NAME = "none-entity"
NOR_TOL = 1e-10
RES_TOL = 9.9999999999999995e-7
BOOL_SPECIFIER = {
"forward": True,
"forward_v": True,
"reversed": False,
"reversed_v": False,
"single": True,
"double": False,
}
ACIS_SIGNATURE = b"ACIS BinaryFile" # DXF R2013/R2018
ASM_SIGNATURE = b"ASM BinaryFile4" # DXF R2018
SIGNATURES = [ACIS_SIGNATURE, ASM_SIGNATURE]
def is_valid_export_version(version: int):
return version >= MIN_EXPORT_VERSION and version in ACIS_VERSION
class Tags(enum.IntEnum):
NO_TYPE = 0x00
BYTE = 0x01 # not used in files!
CHAR = 0x02 # not used in files!
SHORT = 0x03 # not used in files!
INT = 0x04 # 32-bit signed integer
FLOAT = 0x05 # not used in files!
DOUBLE = 0x06 # 64-bit double precision floating point value
STR = 0x07 # count is the following 8-bit uchar
STR2 = 0x08 # not used in files!
STR3 = 0x09 # not used in files!
# bool value for reversed, double, I - depends on context
BOOL_TRUE = 0x0A
# bool value forward, single, forward_v - depends on context
BOOL_FALSE = 0x0B
POINTER = 0x0C
ENTITY_TYPE = 0x0D
ENTITY_TYPE_EX = 0x0E
SUBTYPE_START = 0x0F
SUBTYPE_END = 0x10
RECORD_END = 0x11
LITERAL_STR = 0x12 # count ia a 32-bit uint, see transform entity
LOCATION_VEC = 0x13 # vector (3 doubles)
DIRECTION_VEC = 0x14 # vector (3 doubles)
# Enumeration are stored as strings in SAT and ints in SAB.
# It's not possible to translate SAT enums (strings) to SAB enums (int) and
# vice versa without knowing the implementation details. Each enumeration
# is specific to the class where it is used.
ENUM = 0x15
# 0x16: ???
UNKNOWN_0x17 = 0x17 # double
# entity type structure:
# 0x0D 0x04 (char count of) "body" = SAT "body"
# 0x0E 0x05 "plane" 0x0D 0x07 "surface" = SAT "plane-surface"
# 0x0E 0x06 "ref_vt" 0x0E 0x03 "eye" 0x0D 0x06 "attrib" = SAT "ref_vt-eye-attrib"
class Flags(enum.IntFlag):
HAS_HISTORY = 1
class AcisException(Exception):
pass
class InvalidLinkStructure(AcisException):
pass
class ParsingError(AcisException):
pass
class ExportError(AcisException):
pass
class EndOfAcisData(AcisException):
pass
class Features:
LAW_SPL = 400
CONE_SCALING = 400
LOFT_LAW = 400
REF_MIN_UV_GRID = 400
VBLEND_AUTO = 400
BL_ENV_SF = 400
ELLIPSE_OFFSET = 500
TOL_MODELING = 500
APPROX_SUMMARY = 500
TAPER_SCALING = 500
LAZY_B_SPLINE = 500
DM_MULTI_SURF = 500
GA_COPY_ACTION = 600
DM_MULTI_SURF_COLOR = 600
RECAL_SKIN_ERROR = 520
TAPER_U_RULED = 600
DM_60 = 600
LOFT_PCURVE = 600
EELIST_OWNER = 600
ANNO_HOOKED = 700
PATTERN = 700
ENTITY_TAGS = 700
AT = 700
NET_LAW = 700
STRINGLESS_HISTORY = 700

View File

@@ -0,0 +1,193 @@
# Copyright (c) 2022-2024, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import Iterator, Callable, Any
from .entities import (
AcisEntity,
NONE_REF,
Face,
Coedge,
Loop,
Vertex,
)
from . import sab
class AcisDebugger:
def __init__(self, root: AcisEntity = NONE_REF, start_id: int = 1):
self._next_id = start_id - 1
self._root: AcisEntity = root
self.entities: dict[int, AcisEntity] = dict()
if not root.is_none:
self._store_entities(root)
@property
def root(self) -> AcisEntity:
return self._root
def _get_id(self) -> int:
self._next_id += 1
return self._next_id
def _store_entities(self, entity: AcisEntity) -> None:
if not entity.is_none and entity.id == -1:
entity.id = self._get_id()
self.entities[entity.id] = entity
for e in vars(entity).values():
if isinstance(e, AcisEntity) and e.id == -1:
self._store_entities(e)
def set_entities(self, entity: AcisEntity) -> None:
self.entities.clear()
self._root = entity
self._store_entities(entity)
def walk(self, root: AcisEntity = NONE_REF) -> Iterator[AcisEntity]:
def _walk(entity: AcisEntity):
if entity.is_none:
return
yield entity
done.add(entity.id)
for e in vars(entity).values():
if isinstance(e, AcisEntity) and e.id not in done:
yield from _walk(e)
if root.is_none:
root = self._root
done: set[int] = set()
yield from _walk(root)
def filter(
self, func: Callable[[AcisEntity], bool], entity: AcisEntity = NONE_REF
) -> Iterator[Any]:
if entity.is_none:
entity = self._root
yield from filter(func, self.walk(entity))
def filter_type(
self, name: str, entity: AcisEntity = NONE_REF
) -> Iterator[Any]:
if entity.is_none:
entity = self._root
yield from filter(lambda x: x.type == name, self.walk(entity))
@staticmethod
def entity_attributes(entity: AcisEntity, indent: int = 0) -> Iterator[str]:
indent_str = " " * indent
for name, data in vars(entity).items():
if name == "id":
continue
yield f"{indent_str}{name}: {data}"
def face_link_structure(self, face: Face, indent: int = 0) -> Iterator[str]:
indent_str = " " * indent
while not face.is_none:
partner_faces = list(self.partner_faces(face))
error = ""
linked_partner_faces = []
unlinked_partner_faces = []
for pface_id in partner_faces:
pface = self.entities.get(pface_id)
if pface is None:
error += f" face {pface_id} does not exist;"
if isinstance(pface, Face):
reverse_faces = self.partner_faces(pface)
if face.id in reverse_faces:
linked_partner_faces.append(pface_id)
else:
unlinked_partner_faces.append(pface_id)
else:
error += f" entity {pface_id} is not a face;"
if unlinked_partner_faces:
error = f"unlinked partner faces: {unlinked_partner_faces} {error}"
yield f"{indent_str}{str(face)} >> {partner_faces} {error}"
face = face.next_face
@staticmethod
def partner_faces(face: Face) -> Iterator[int]:
coedges: list[Coedge] = []
loop = face.loop
while not loop.is_none:
coedges.extend(co for co in loop.coedges())
loop = loop.next_loop
for coedge in coedges:
for partner_coedge in coedge.partner_coedges():
yield partner_coedge.loop.face.id
@staticmethod
def coedge_structure(face: Face, ident: int = 4) -> list[str]:
lines: list[str] = []
coedges: list[Coedge] = []
loop = face.loop
while not loop.is_none:
coedges.extend(co for co in loop.coedges())
loop = loop.next_loop
for coedge in coedges:
edge1 = coedge.edge
sense1 = coedge.sense
lines.append(f"Coedge={coedge.id} edge={edge1.id} sense={sense1}")
for partner_coedge in coedge.partner_coedges():
edge2 = partner_coedge.edge
sense2 = partner_coedge.sense
lines.append(
f" Partner Coedge={partner_coedge.id} edge={edge2.id} sense={sense2}"
)
ident_str = " " * ident
return [ident_str + line for line in lines]
@staticmethod
def loop_vertices(loop: Loop, indent: int = 0) -> str:
indent_str = " " * indent
return f"{indent_str}{loop} >> {list(AcisDebugger.loop_edges(loop))}"
@staticmethod
def loop_edges(loop: Loop) -> Iterator[list[int]]:
coedge = loop.coedge
first = coedge
while not coedge.is_none:
edge = coedge.edge
sv = edge.start_vertex
ev = edge.end_vertex
if coedge.sense:
yield [ev.id, sv.id]
else:
yield [sv.id, ev.id]
coedge = coedge.next_coedge
if coedge is first:
break
def vertex_to_edge_relation(self) -> Iterator[str]:
for vertex in (
e for e in self.entities.values() if isinstance(e, Vertex)
):
edge = vertex.edge
sv = edge.start_vertex
ev = edge.end_vertex
yield f"{vertex}: parent edge is {edge.id}; {sv.id} => {ev.id}; {edge.curve}"
def is_manifold(self) -> bool:
for coedge in self.filter_type("coedge"):
if len(coedge.partner_coedges()) > 1:
return False
return True
def dump_sab_as_text(data: bytes) -> Iterator[str]:
def entity_data(e):
for tag, value in e:
name = sab.Tags(tag).name
yield f"{name} = {value}"
decoder = sab.Decoder(data)
header = decoder.read_header()
yield from header.dumps()
index = 0
try:
for record in decoder.read_records():
yield f"--------------------- record: {index}"
yield from entity_data(record)
index += 1
except sab.ParsingError as e:
yield str(e)

View File

@@ -0,0 +1,82 @@
# Copyright (c) 2022-2024, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import cast, Sequence
from ezdxf.entities import DXFEntity, Body as DXFBody
from ezdxf.lldxf import const
from .entities import Body, export_sat, export_sab, load
def export_dxf(entity: DXFEntity, bodies: Sequence[Body]):
"""Store the :term:`ACIS` bodies in the given DXF entity. This is the
recommended way to set ACIS data of DXF entities.
The DXF entity has to be an ACIS based entity and inherit from
:class:`ezdxf.entities.Body`. The entity has to be bound to a valid DXF
document and the DXF version of the document has to be DXF R2000 or newer.
Raises:
DXFTypeError: invalid DXF entity type
DXFValueError: invalid DXF document
DXFVersionError: invalid DXF version
"""
if not isinstance(entity, DXFBody):
raise const.DXFTypeError(f"invalid DXF entity {entity.dxftype()}")
body = cast(DXFBody, entity)
doc = entity.doc
if doc is None:
raise const.DXFValueError("a valid DXF document is required")
dxfversion = doc.dxfversion
if dxfversion < const.DXF2000:
raise const.DXFVersionError(f"invalid DXF version {dxfversion}")
if dxfversion < const.DXF2013:
body.sat = export_sat(bodies)
else:
body.sab = export_sab(bodies)
def load_dxf(entity: DXFEntity) -> list[Body]:
"""Load the :term:`ACIS` bodies from the given DXF entity. This is the
recommended way to load ACIS data.
The DXF entity has to be an ACIS based entity and inherit from
:class:`ezdxf.entities.Body`. The entity has to be bound to a valid DXF
document and the DXF version of the document has to be DXF R2000 or newer.
Raises:
DXFTypeError: invalid DXF entity type
DXFValueError: invalid DXF document
DXFVersionError: invalid DXF version
.. warning::
Only a limited count of :term:`ACIS` entities is supported, all
unsupported entities are loaded as ``NONE_ENTITY`` and their data is
lost. Exporting such ``NONE_ENTITIES`` will raise an :class:`ExportError`
exception.
To emphasize that again: **It is not possible to load and re-export
arbitrary ACIS data!**
"""
if not isinstance(entity, DXFBody):
raise const.DXFTypeError(f"invalid DXF entity {entity.dxftype()}")
body = cast(DXFBody, entity)
doc = entity.doc
if doc is None:
raise const.DXFValueError("a valid DXF document is required")
dxfversion = doc.dxfversion
if dxfversion < const.DXF2000:
raise const.DXFVersionError(f"invalid DXF version {dxfversion}")
if body.has_binary_data:
binary_data = body.sab
if binary_data:
return load(binary_data)
else:
text = body.sat
if text:
return load(text)
return []

View File

@@ -0,0 +1,802 @@
# 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()

Some files were not shown because too many files have changed in this diff Show More