initial
This commit is contained in:
106
.venv/lib/python3.12/site-packages/ezdxf/__init__.py
Normal file
106
.venv/lib/python3.12/site-packages/ezdxf/__init__.py
Normal 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")
|
||||
120
.venv/lib/python3.12/site-packages/ezdxf/__main__.py
Normal file
120
.venv/lib/python3.12/site-packages/ezdxf/__main__.py
Normal 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()
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
335
.venv/lib/python3.12/site-packages/ezdxf/_options.py
Normal file
335
.venv/lib/python3.12/site-packages/ezdxf/_options.py
Normal 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()
|
||||
39
.venv/lib/python3.12/site-packages/ezdxf/acc/__init__.py
Normal file
39
.venv/lib/python3.12/site-packages/ezdxf/acc/__init__.py
Normal 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
|
||||
Binary file not shown.
13516
.venv/lib/python3.12/site-packages/ezdxf/acc/bezier3p.c
Normal file
13516
.venv/lib/python3.12/site-packages/ezdxf/acc/bezier3p.c
Normal file
File diff suppressed because it is too large
Load Diff
BIN
.venv/lib/python3.12/site-packages/ezdxf/acc/bezier3p.cpython-312-darwin.so
Executable file
BIN
.venv/lib/python3.12/site-packages/ezdxf/acc/bezier3p.cpython-312-darwin.so
Executable file
Binary file not shown.
222
.venv/lib/python3.12/site-packages/ezdxf/acc/bezier3p.pyx
Normal file
222
.venv/lib/python3.12/site-packages/ezdxf/acc/bezier3p.pyx
Normal 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
|
||||
18390
.venv/lib/python3.12/site-packages/ezdxf/acc/bezier4p.c
Normal file
18390
.venv/lib/python3.12/site-packages/ezdxf/acc/bezier4p.c
Normal file
File diff suppressed because it is too large
Load Diff
BIN
.venv/lib/python3.12/site-packages/ezdxf/acc/bezier4p.cpython-312-darwin.so
Executable file
BIN
.venv/lib/python3.12/site-packages/ezdxf/acc/bezier4p.cpython-312-darwin.so
Executable file
Binary file not shown.
371
.venv/lib/python3.12/site-packages/ezdxf/acc/bezier4p.pyx
Normal file
371
.venv/lib/python3.12/site-packages/ezdxf/acc/bezier4p.pyx
Normal 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
|
||||
|
||||
19393
.venv/lib/python3.12/site-packages/ezdxf/acc/bspline.c
Normal file
19393
.venv/lib/python3.12/site-packages/ezdxf/acc/bspline.c
Normal file
File diff suppressed because it is too large
Load Diff
BIN
.venv/lib/python3.12/site-packages/ezdxf/acc/bspline.cpython-312-darwin.so
Executable file
BIN
.venv/lib/python3.12/site-packages/ezdxf/acc/bspline.cpython-312-darwin.so
Executable file
Binary file not shown.
400
.venv/lib/python3.12/site-packages/ezdxf/acc/bspline.pyx
Normal file
400
.venv/lib/python3.12/site-packages/ezdxf/acc/bspline.pyx
Normal 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)
|
||||
8
.venv/lib/python3.12/site-packages/ezdxf/acc/constants.h
Normal file
8
.venv/lib/python3.12/site-packages/ezdxf/acc/constants.h
Normal 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
|
||||
11803
.venv/lib/python3.12/site-packages/ezdxf/acc/construct.c
Normal file
11803
.venv/lib/python3.12/site-packages/ezdxf/acc/construct.c
Normal file
File diff suppressed because it is too large
Load Diff
BIN
.venv/lib/python3.12/site-packages/ezdxf/acc/construct.cpython-312-darwin.so
Executable file
BIN
.venv/lib/python3.12/site-packages/ezdxf/acc/construct.cpython-312-darwin.so
Executable file
Binary file not shown.
359
.venv/lib/python3.12/site-packages/ezdxf/acc/construct.pyx
Normal file
359
.venv/lib/python3.12/site-packages/ezdxf/acc/construct.pyx
Normal 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
|
||||
11553
.venv/lib/python3.12/site-packages/ezdxf/acc/linetypes.c
Normal file
11553
.venv/lib/python3.12/site-packages/ezdxf/acc/linetypes.c
Normal file
File diff suppressed because it is too large
Load Diff
BIN
.venv/lib/python3.12/site-packages/ezdxf/acc/linetypes.cpython-312-darwin.so
Executable file
BIN
.venv/lib/python3.12/site-packages/ezdxf/acc/linetypes.cpython-312-darwin.so
Executable file
Binary file not shown.
91
.venv/lib/python3.12/site-packages/ezdxf/acc/linetypes.pyx
Normal file
91
.venv/lib/python3.12/site-packages/ezdxf/acc/linetypes.pyx
Normal 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
|
||||
16206
.venv/lib/python3.12/site-packages/ezdxf/acc/mapbox_earcut.c
Normal file
16206
.venv/lib/python3.12/site-packages/ezdxf/acc/mapbox_earcut.c
Normal file
File diff suppressed because it is too large
Load Diff
BIN
.venv/lib/python3.12/site-packages/ezdxf/acc/mapbox_earcut.cpython-312-darwin.so
Executable file
BIN
.venv/lib/python3.12/site-packages/ezdxf/acc/mapbox_earcut.cpython-312-darwin.so
Executable file
Binary file not shown.
835
.venv/lib/python3.12/site-packages/ezdxf/acc/mapbox_earcut.pyx
Normal file
835
.venv/lib/python3.12/site-packages/ezdxf/acc/mapbox_earcut.pyx
Normal 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
|
||||
45728
.venv/lib/python3.12/site-packages/ezdxf/acc/matrix44.c
Normal file
45728
.venv/lib/python3.12/site-packages/ezdxf/acc/matrix44.c
Normal file
File diff suppressed because it is too large
Load Diff
BIN
.venv/lib/python3.12/site-packages/ezdxf/acc/matrix44.cpython-312-darwin.so
Executable file
BIN
.venv/lib/python3.12/site-packages/ezdxf/acc/matrix44.cpython-312-darwin.so
Executable file
Binary file not shown.
15
.venv/lib/python3.12/site-packages/ezdxf/acc/matrix44.pxd
Normal file
15
.venv/lib/python3.12/site-packages/ezdxf/acc/matrix44.pxd
Normal 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
|
||||
676
.venv/lib/python3.12/site-packages/ezdxf/acc/matrix44.pyx
Normal file
676
.venv/lib/python3.12/site-packages/ezdxf/acc/matrix44.pyx
Normal 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]
|
||||
29254
.venv/lib/python3.12/site-packages/ezdxf/acc/np_support.c
Normal file
29254
.venv/lib/python3.12/site-packages/ezdxf/acc/np_support.c
Normal file
File diff suppressed because it is too large
Load Diff
BIN
.venv/lib/python3.12/site-packages/ezdxf/acc/np_support.cpython-312-darwin.so
Executable file
BIN
.venv/lib/python3.12/site-packages/ezdxf/acc/np_support.cpython-312-darwin.so
Executable file
Binary file not shown.
177
.venv/lib/python3.12/site-packages/ezdxf/acc/np_support.pyx
Normal file
177
.venv/lib/python3.12/site-packages/ezdxf/acc/np_support.pyx
Normal 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
|
||||
32048
.venv/lib/python3.12/site-packages/ezdxf/acc/vector.c
Normal file
32048
.venv/lib/python3.12/site-packages/ezdxf/acc/vector.c
Normal file
File diff suppressed because it is too large
Load Diff
BIN
.venv/lib/python3.12/site-packages/ezdxf/acc/vector.cpython-312-darwin.so
Executable file
BIN
.venv/lib/python3.12/site-packages/ezdxf/acc/vector.cpython-312-darwin.so
Executable file
Binary file not shown.
49
.venv/lib/python3.12/site-packages/ezdxf/acc/vector.pxd
Normal file
49
.venv/lib/python3.12/site-packages/ezdxf/acc/vector.pxd
Normal 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)
|
||||
|
||||
838
.venv/lib/python3.12/site-packages/ezdxf/acc/vector.pyx
Normal file
838
.venv/lib/python3.12/site-packages/ezdxf/acc/vector.pyx
Normal 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)
|
||||
@@ -0,0 +1,3 @@
|
||||
# Copyright (c) 2022-2024, Manfred Moitzi
|
||||
# License: MIT License
|
||||
# Users should always import from ezdxf.acis.api!
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
224
.venv/lib/python3.12/site-packages/ezdxf/acis/abstract.py
Normal file
224
.venv/lib/python3.12/site-packages/ezdxf/acis/abstract.py
Normal 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)
|
||||
31
.venv/lib/python3.12/site-packages/ezdxf/acis/api.py
Normal file
31
.venv/lib/python3.12/site-packages/ezdxf/acis/api.py
Normal 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
|
||||
59
.venv/lib/python3.12/site-packages/ezdxf/acis/cache.py
Normal file
59
.venv/lib/python3.12/site-packages/ezdxf/acis/cache.py
Normal 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)
|
||||
183
.venv/lib/python3.12/site-packages/ezdxf/acis/const.py
Normal file
183
.venv/lib/python3.12/site-packages/ezdxf/acis/const.py
Normal 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
|
||||
193
.venv/lib/python3.12/site-packages/ezdxf/acis/dbg.py
Normal file
193
.venv/lib/python3.12/site-packages/ezdxf/acis/dbg.py
Normal 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)
|
||||
82
.venv/lib/python3.12/site-packages/ezdxf/acis/dxf.py
Normal file
82
.venv/lib/python3.12/site-packages/ezdxf/acis/dxf.py
Normal 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 []
|
||||
802
.venv/lib/python3.12/site-packages/ezdxf/acis/entities.py
Normal file
802
.venv/lib/python3.12/site-packages/ezdxf/acis/entities.py
Normal 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
Reference in New Issue
Block a user