forked from jan/fatamorgana
2701 lines
91 KiB
Python
2701 lines
91 KiB
Python
"""
|
|
This module contains all 'record' or 'block'-level datastructures and their
|
|
associated writing and parsing code, as well as a few helper functions.
|
|
|
|
Additionally, this module contains definitions for the record-level modal
|
|
variables (stored in the Modals class).
|
|
|
|
Higher-level code (e.g. monitoring for combinations of records with
|
|
implicit and explicit references, code for deciding which record type to
|
|
parse, or code for dealing with nested records in a CBlock) should live
|
|
in main.py instead.
|
|
"""
|
|
from typing import Any, TypeVar, IO, Union, Protocol
|
|
from collections.abc import Sequence
|
|
from abc import ABCMeta, abstractmethod
|
|
import copy
|
|
import math
|
|
import zlib
|
|
import io
|
|
import logging
|
|
import pprint
|
|
from warnings import warn
|
|
from .basic import (
|
|
AString, NString, repetition_t, property_value_t, real_t,
|
|
ReuseRepetition, OffsetTable, Validation, read_point_list, read_property_value,
|
|
read_bstring, read_uint, read_sint, read_real, read_repetition, read_interval,
|
|
write_bstring, write_uint, write_sint, write_real, write_interval, write_point_list,
|
|
write_property_value, read_bool_byte, write_bool_byte, read_byte, write_byte,
|
|
InvalidDataError, UnfilledModalError, PathExtensionScheme, _USE_NUMPY,
|
|
)
|
|
|
|
if _USE_NUMPY:
|
|
import numpy
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
'''
|
|
Type definitions
|
|
'''
|
|
geometry_t = Union['Text', 'Rectangle', 'Polygon', 'Path', 'Trapezoid',
|
|
'CTrapezoid', 'Circle', 'XElement', 'XGeometry']
|
|
pathextension_t = tuple['PathExtensionScheme', int | None]
|
|
point_list_t = Sequence[Sequence[int]]
|
|
|
|
|
|
class Modals:
|
|
"""
|
|
Modal variables, used to store data about previously-written or -read records.
|
|
"""
|
|
repetition: repetition_t | None = None
|
|
placement_x: int = 0
|
|
placement_y: int = 0
|
|
placement_cell: NString | None = None
|
|
layer: int | None = None
|
|
datatype: int | None = None
|
|
text_layer: int | None = None
|
|
text_datatype: int | None = None
|
|
text_x: int = 0
|
|
text_y: int = 0
|
|
text_string: AString | int | None = None
|
|
geometry_x: int = 0
|
|
geometry_y: int = 0
|
|
xy_relative: bool = False
|
|
geometry_w: int | None = None
|
|
geometry_h: int | None = None
|
|
polygon_point_list: point_list_t | None = None
|
|
path_half_width: int | None = None
|
|
path_point_list: point_list_t | None = None
|
|
path_extension_start: pathextension_t | None = None
|
|
path_extension_end: pathextension_t | None = None
|
|
ctrapezoid_type: int | None = None
|
|
circle_radius: int | None = None
|
|
property_value_list: Sequence[property_value_t] | None = None
|
|
property_name: int | NString | None = None
|
|
property_is_standard: bool | None = None
|
|
|
|
def __init__(self) -> None:
|
|
self.reset()
|
|
|
|
def reset(self) -> None:
|
|
"""
|
|
Resets all modal variables to their default values.
|
|
Default values are:
|
|
`0` for placement_{x,y}, text_{x,y}, geometry_{x,y}
|
|
`False` for xy_relative
|
|
Undefined (`None`) for all others
|
|
"""
|
|
self.repetition = None
|
|
self.placement_x = 0
|
|
self.placement_y = 0
|
|
self.placement_cell = None
|
|
self.layer = None
|
|
self.datatype = None
|
|
self.text_layer = None
|
|
self.text_datatype = None
|
|
self.text_x = 0
|
|
self.text_y = 0
|
|
self.text_string = None
|
|
self.geometry_x = 0
|
|
self.geometry_y = 0
|
|
self.xy_relative = False
|
|
self.geometry_w = None
|
|
self.geometry_h = None
|
|
self.polygon_point_list = None
|
|
self.path_half_width = None
|
|
self.path_point_list = None
|
|
self.path_extension_start = None
|
|
self.path_extension_end = None
|
|
self.ctrapezoid_type = None
|
|
self.circle_radius = None
|
|
self.property_value_list = None
|
|
self.property_name = None
|
|
self.property_is_standard = None
|
|
|
|
|
|
T = TypeVar('T')
|
|
def verify_modal(var: T | None) -> T:
|
|
if var is None:
|
|
raise UnfilledModalError
|
|
return var
|
|
|
|
|
|
#
|
|
#
|
|
# Records
|
|
#
|
|
#
|
|
|
|
class Record(metaclass=ABCMeta):
|
|
"""
|
|
Common interface for records.
|
|
"""
|
|
@abstractmethod
|
|
def merge_with_modals(self, modals: Modals) -> None:
|
|
"""
|
|
Copy all defined values from this record into the modal variables.
|
|
Fill all undefined values in this record from the modal variables.
|
|
|
|
Args:
|
|
modals: Modal variables to merge with.
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def deduplicate_with_modals(self, modals: Modals) -> None:
|
|
"""
|
|
Check all defined values in this record against those in the
|
|
modal variables. If any values are equal, remove them from
|
|
the record and indicate that the modal variables should be
|
|
used instead. Update the modal variables using the remaining
|
|
(unequal) values.
|
|
|
|
Args:
|
|
modals: Modal variables to deduplicate with.
|
|
"""
|
|
pass
|
|
|
|
@staticmethod
|
|
@abstractmethod
|
|
def read(stream: IO[bytes], record_id: int) -> 'Record':
|
|
"""
|
|
Read a record of this type from a stream.
|
|
This function does not merge with modal variables.
|
|
|
|
Args:
|
|
stream: Stream to read from.
|
|
record_id: Record id of the record to read. The
|
|
record id is often used to specify which variant
|
|
of the record is stored.
|
|
|
|
Returns:
|
|
The record that was read.
|
|
|
|
Raises:
|
|
InvalidDataError: if the record is malformed.
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def write(self, stream: IO[bytes]) -> int:
|
|
"""
|
|
Write this record to a stream as-is.
|
|
This function does not merge or deduplicate with modal variables.
|
|
|
|
Args:
|
|
stream: Stream to write to.
|
|
|
|
Returns:
|
|
Number of bytes written.
|
|
|
|
Raises:
|
|
InvalidDataError: if the record contains invalid data.
|
|
"""
|
|
pass
|
|
|
|
def dedup_write(self, stream: IO[bytes], modals: Modals) -> int:
|
|
"""
|
|
Run `.deduplicate_with_modals()` and then `.write()` to the stream.
|
|
|
|
Args:
|
|
stream: Stream to write to.
|
|
modals: Modal variables to merge with.
|
|
|
|
Returns:
|
|
Number of bytes written.
|
|
|
|
Raises:
|
|
InvalidDataError: if the record contains invalid data.
|
|
"""
|
|
# TODO logging
|
|
#print(type(self), stream.tell())
|
|
self.deduplicate_with_modals(modals)
|
|
return self.write(stream)
|
|
|
|
def copy(self) -> 'Record':
|
|
"""
|
|
Perform a deep copy of this record.
|
|
|
|
Returns:
|
|
A deep copy of this record.
|
|
"""
|
|
return copy.deepcopy(self)
|
|
|
|
def __repr__(self) -> str:
|
|
return f'{self.__class__}: ' + pprint.pformat(self.__dict__)
|
|
|
|
|
|
class HasRepetition(Protocol):
|
|
repetition: repetition_t | None
|
|
|
|
|
|
class HasXY(Protocol):
|
|
x: int | None
|
|
y: int | None
|
|
|
|
|
|
class GeometryMixin(metaclass=ABCMeta):
|
|
"""
|
|
Mixin defining common functions for geometry records
|
|
"""
|
|
x: int | None
|
|
y: int | None
|
|
layer: int | None
|
|
datatype: int | None
|
|
|
|
def get_x(self) -> int:
|
|
return verify_modal(self.x)
|
|
|
|
def get_y(self) -> int:
|
|
return verify_modal(self.y)
|
|
|
|
def get_xy(self) -> tuple[int, int]:
|
|
return (self.get_x(), self.get_y())
|
|
|
|
def get_layer(self) -> int:
|
|
return verify_modal(self.layer)
|
|
|
|
def get_datatype(self) -> int:
|
|
return verify_modal(self.datatype)
|
|
|
|
def get_layer_tuple(self) -> tuple[int, int]:
|
|
return (self.get_layer(), self.get_datatype())
|
|
|
|
|
|
def read_refname(
|
|
stream: IO[bytes],
|
|
is_present: bool | int,
|
|
is_reference: bool | int,
|
|
) -> int | NString | None:
|
|
"""
|
|
Helper function for reading a possibly-absent, possibly-referenced NString.
|
|
|
|
Args:
|
|
stream: Stream to read from.
|
|
is_present: If `False`, read nothing and return `None`
|
|
is_reference: If `True`, read a uint (reference id),
|
|
otherwise read an `NString`.
|
|
|
|
Returns:
|
|
`None`, reference id, or `NString`
|
|
"""
|
|
if not is_present:
|
|
return None
|
|
if is_reference:
|
|
return read_uint(stream)
|
|
return NString.read(stream)
|
|
|
|
|
|
def read_refstring(
|
|
stream: IO[bytes],
|
|
is_present: bool | int,
|
|
is_reference: bool | int,
|
|
) -> int | AString | None:
|
|
"""
|
|
Helper function for reading a possibly-absent, possibly-referenced `AString`.
|
|
|
|
Args:
|
|
stream: Stream to read from.
|
|
is_present: If `False`, read nothing and return `None`
|
|
is_reference: If `True`, read a uint (reference id),
|
|
otherwise read an `AString`.
|
|
|
|
Returns:
|
|
`None`, reference id, or `AString`
|
|
"""
|
|
if not is_present:
|
|
return None
|
|
if is_reference:
|
|
return read_uint(stream)
|
|
return AString.read(stream)
|
|
|
|
|
|
class Pad(Record):
|
|
"""
|
|
Pad record (ID 0)
|
|
"""
|
|
def merge_with_modals(self, modals: Modals) -> None:
|
|
pass
|
|
|
|
def deduplicate_with_modals(self, modals: Modals) -> None:
|
|
pass
|
|
|
|
@staticmethod
|
|
def read(stream: IO[bytes], record_id: int) -> 'Pad':
|
|
if record_id != 0:
|
|
raise InvalidDataError(f'Invalid record id for Pad {record_id}')
|
|
record = Pad()
|
|
logger.debug(f'Record ending at 0x{stream.tell():x}:\n {record}')
|
|
return record
|
|
|
|
def write(self, stream: IO[bytes]) -> int:
|
|
return write_uint(stream, 0)
|
|
|
|
|
|
class XYMode(Record):
|
|
"""
|
|
XYMode record (ID 15, 16)
|
|
"""
|
|
relative: bool
|
|
|
|
@property
|
|
def absolute(self) -> bool:
|
|
return not self.relative
|
|
|
|
@absolute.setter
|
|
def absolute(self, b: bool) -> None:
|
|
self.relative = not b
|
|
|
|
def __init__(self, relative: bool) -> None:
|
|
"""
|
|
Args:
|
|
relative: `True` if the mode is 'relative', `False` if 'absolute'.
|
|
"""
|
|
self.relative = relative
|
|
|
|
def merge_with_modals(self, modals: Modals) -> None:
|
|
modals.xy_relative = self.relative
|
|
|
|
def deduplicate_with_modals(self, modals: Modals) -> None:
|
|
pass
|
|
|
|
@staticmethod
|
|
def read(stream: IO[bytes], record_id: int) -> 'XYMode':
|
|
if record_id not in (15, 16):
|
|
raise InvalidDataError('Invalid record id for XYMode')
|
|
record = XYMode(record_id == 16)
|
|
logger.debug(f'Record ending at 0x{stream.tell():x}:\n {record}')
|
|
return record
|
|
|
|
def write(self, stream: IO[bytes]) -> int:
|
|
return write_uint(stream, 15 + self.relative)
|
|
|
|
|
|
class Start(Record):
|
|
"""
|
|
Start Record (ID 1)
|
|
"""
|
|
version: AString
|
|
"""File format version string"""
|
|
|
|
unit: real_t
|
|
"""positive real number, grid steps per micron"""
|
|
|
|
offset_table: OffsetTable | None
|
|
"""If `None` then table must be placed in the `End` record"""
|
|
|
|
def __init__(
|
|
self,
|
|
unit: real_t,
|
|
version: AString | str = "1.0",
|
|
offset_table: OffsetTable | None = None,
|
|
) -> None:
|
|
"""
|
|
Args
|
|
unit: Grid steps per micron (positive real number)
|
|
version: Version string, default "1.0"
|
|
offset_table: `OffsetTable` for the file, or `None` to place
|
|
it in the `End` record instead.
|
|
"""
|
|
if unit <= 0:
|
|
raise InvalidDataError(f'Non-positive unit: {unit}')
|
|
if math.isnan(unit):
|
|
raise InvalidDataError('NaN unit')
|
|
if math.isinf(unit):
|
|
raise InvalidDataError('Non-finite unit')
|
|
self.unit = unit
|
|
|
|
if isinstance(version, AString):
|
|
self.version = version
|
|
else:
|
|
self.version = AString(version)
|
|
|
|
if self.version.string != '1.0':
|
|
raise InvalidDataError(f'Invalid version string, only "1.0" is allowed: "{self.version.string}"')
|
|
self.offset_table = offset_table
|
|
|
|
def merge_with_modals(self, modals: Modals) -> None:
|
|
modals.reset()
|
|
|
|
def deduplicate_with_modals(self, modals: Modals) -> None:
|
|
modals.reset()
|
|
|
|
@staticmethod
|
|
def read(stream: IO[bytes], record_id: int) -> 'Start':
|
|
if record_id != 1:
|
|
raise InvalidDataError(f'Invalid record id for Start: {record_id}')
|
|
version = AString.read(stream)
|
|
unit = read_real(stream)
|
|
has_offset_table = read_uint(stream) == 0
|
|
offset_table: OffsetTable | None
|
|
if has_offset_table:
|
|
offset_table = OffsetTable.read(stream)
|
|
else:
|
|
offset_table = None
|
|
record = Start(unit, version, offset_table)
|
|
logger.debug(f'Record ending at 0x{stream.tell():x}:\n {record}')
|
|
return record
|
|
|
|
def write(self, stream: IO[bytes]) -> int:
|
|
size = write_uint(stream, 1)
|
|
size += self.version.write(stream)
|
|
size += write_real(stream, self.unit)
|
|
size += write_uint(stream, self.offset_table is None)
|
|
if self.offset_table is not None:
|
|
size += self.offset_table.write(stream)
|
|
return size
|
|
|
|
|
|
class End(Record):
|
|
"""
|
|
End record (ID 2)
|
|
|
|
The end record is always padded to a total length of 256 bytes.
|
|
"""
|
|
offset_table: OffsetTable | None
|
|
"""`None` if offset table was written into the `Start` record instead"""
|
|
|
|
validation: Validation
|
|
"""object containing checksum"""
|
|
|
|
def __init__(
|
|
self,
|
|
validation: Validation,
|
|
offset_table: OffsetTable | None = None,
|
|
) -> None:
|
|
"""
|
|
Args:
|
|
validation: `Validation` object for this file.
|
|
offset_table: `OffsetTable`, or `None` if the `Start` record
|
|
contained an `OffsetTable`. Default `None`.
|
|
"""
|
|
self.validation = validation
|
|
self.offset_table = offset_table
|
|
|
|
def merge_with_modals(self, modals: Modals) -> None:
|
|
pass
|
|
|
|
def deduplicate_with_modals(self, modals: Modals) -> None:
|
|
pass
|
|
|
|
@staticmethod
|
|
def read(
|
|
stream: IO[bytes],
|
|
record_id: int,
|
|
has_offset_table: bool
|
|
) -> 'End':
|
|
if record_id != 2:
|
|
raise InvalidDataError(f'Invalid record id for End {record_id}')
|
|
if has_offset_table:
|
|
offset_table: OffsetTable | None = OffsetTable.read(stream)
|
|
else:
|
|
offset_table = None
|
|
_padding_string = read_bstring(stream) # noqa
|
|
validation = Validation.read(stream)
|
|
record = End(validation, offset_table)
|
|
logger.debug(f'Record ending at 0x{stream.tell():x}:\n {record}')
|
|
return record
|
|
|
|
def write(self, stream: IO[bytes]) -> int:
|
|
size = write_uint(stream, 2)
|
|
if self.offset_table is not None:
|
|
size += self.offset_table.write(stream)
|
|
|
|
buf = io.BytesIO()
|
|
self.validation.write(buf)
|
|
validation_bytes = buf.getvalue()
|
|
|
|
pad_len = 256 - size - len(validation_bytes)
|
|
if pad_len > 0:
|
|
pad = [0x80] * (pad_len - 1) + [0x00]
|
|
stream.write(bytes(pad))
|
|
stream.write(validation_bytes)
|
|
return 256
|
|
|
|
|
|
class CBlock(Record):
|
|
"""
|
|
CBlock (Compressed Block) record (ID 34)
|
|
"""
|
|
compression_type: int
|
|
""" `0` for zlib"""
|
|
|
|
decompressed_byte_count: int
|
|
"""size after decompressing"""
|
|
|
|
compressed_bytes: bytes
|
|
"""compressed data"""
|
|
|
|
def __init__(
|
|
self,
|
|
compression_type: int,
|
|
decompressed_byte_count: int,
|
|
compressed_bytes: bytes,
|
|
) -> None:
|
|
"""
|
|
Args:
|
|
compression_type: `0` (zlib)
|
|
decompressed_byte_count: Number of bytes in the decompressed data.
|
|
compressed_bytes: The compressed data.
|
|
"""
|
|
if compression_type != 0:
|
|
raise InvalidDataError(f'CBlock: Invalid compression scheme {compression_type}')
|
|
|
|
self.compression_type = compression_type
|
|
self.decompressed_byte_count = decompressed_byte_count
|
|
self.compressed_bytes = compressed_bytes
|
|
|
|
def merge_with_modals(self, modals: Modals) -> None:
|
|
pass
|
|
|
|
def deduplicate_with_modals(self, modals: Modals) -> None:
|
|
pass
|
|
|
|
@staticmethod
|
|
def read(stream: IO[bytes], record_id: int) -> 'CBlock':
|
|
if record_id != 34:
|
|
raise InvalidDataError(f'Invalid record id for CBlock: {record_id}')
|
|
compression_type = read_uint(stream)
|
|
decompressed_count = read_uint(stream)
|
|
compressed_bytes = read_bstring(stream)
|
|
record = CBlock(compression_type, decompressed_count, compressed_bytes)
|
|
logger.debug(f'CBlock ending at 0x{stream.tell():x} was read successfully')
|
|
return record
|
|
|
|
def write(self, stream: IO[bytes]) -> int:
|
|
size = write_uint(stream, 34)
|
|
size += write_uint(stream, self.compression_type)
|
|
size += write_uint(stream, self.decompressed_byte_count)
|
|
size += write_bstring(stream, self.compressed_bytes)
|
|
return size
|
|
|
|
@staticmethod
|
|
def from_decompressed(
|
|
decompressed_bytes: bytes,
|
|
compression_type: int = 0,
|
|
compression_args: dict[str, Any] | None = None,
|
|
) -> 'CBlock':
|
|
"""
|
|
Create a CBlock record from uncompressed data.
|
|
|
|
Args:
|
|
decompressed_bytes: Uncompressed data (one or more non-CBlock records)
|
|
compression_type: Compression type (0: zlib). Default `0`
|
|
compression_args: Passed as kwargs to `zlib.compressobj()`. Default `{}`.
|
|
|
|
Returns:
|
|
CBlock object constructed from the data.
|
|
|
|
Raises:
|
|
InvalidDataError: if invalid `compression_type`.
|
|
"""
|
|
if compression_args is None:
|
|
compression_args = {}
|
|
|
|
if compression_type == 0:
|
|
count = len(decompressed_bytes)
|
|
compressor = zlib.compressobj(wbits=-zlib.MAX_WBITS, **compression_args)
|
|
compressed_bytes = (compressor.compress(decompressed_bytes)
|
|
+ compressor.flush())
|
|
else:
|
|
raise InvalidDataError(f'Unknown compression type: {compression_type}')
|
|
|
|
return CBlock(compression_type, count, compressed_bytes)
|
|
|
|
def decompress(self, decompression_args: dict[str, Any] | None = None) -> bytes:
|
|
"""
|
|
Decompress the contents of this CBlock.
|
|
|
|
Args:
|
|
decompression_args: Passed as kwargs to `zlib.decompressobj()`.
|
|
|
|
Returns:
|
|
Decompressed `bytes` object.
|
|
|
|
Raises:
|
|
InvalidDataError: if data is malformed or compression type is
|
|
unknonwn.
|
|
"""
|
|
if decompression_args is None:
|
|
decompression_args = {}
|
|
if self.compression_type == 0:
|
|
decompressor = zlib.decompressobj(wbits=-zlib.MAX_WBITS, **decompression_args)
|
|
decompressed_bytes = (decompressor.decompress(self.compressed_bytes)
|
|
+ decompressor.flush())
|
|
if len(decompressed_bytes) != self.decompressed_byte_count:
|
|
raise InvalidDataError('Decompressed data length does not match!')
|
|
else:
|
|
raise InvalidDataError(f'Unknown compression type: {self.compression_type}')
|
|
return decompressed_bytes
|
|
|
|
|
|
class CellName(Record):
|
|
"""
|
|
CellName record (ID 3, 4)
|
|
"""
|
|
nstring: NString
|
|
"""name string"""
|
|
|
|
reference_number: int | None
|
|
"""`None` results in implicit assignment"""
|
|
|
|
def __init__(
|
|
self,
|
|
nstring: str | NString,
|
|
reference_number: int | None = None,
|
|
) -> None:
|
|
"""
|
|
Args:
|
|
nstring: The contained string.
|
|
reference_number: Reference id number for the string.
|
|
Default is to use an implicitly-assigned number.
|
|
"""
|
|
if isinstance(nstring, NString):
|
|
self.nstring = nstring
|
|
else:
|
|
self.nstring = NString(nstring)
|
|
self.reference_number = reference_number
|
|
|
|
def merge_with_modals(self, modals: Modals) -> None:
|
|
modals.reset()
|
|
|
|
def deduplicate_with_modals(self, modals: Modals) -> None:
|
|
modals.reset()
|
|
|
|
@staticmethod
|
|
def read(stream: IO[bytes], record_id: int) -> 'CellName':
|
|
if record_id not in (3, 4):
|
|
raise InvalidDataError(f'Invalid record id for CellName {record_id}')
|
|
nstring = NString.read(stream)
|
|
if record_id == 4:
|
|
reference_number: int | None = read_uint(stream)
|
|
else:
|
|
reference_number = None
|
|
record = CellName(nstring, reference_number)
|
|
logger.debug(f'Record ending at 0x{stream.tell():x}:\n {record}')
|
|
return record
|
|
|
|
def write(self, stream: IO[bytes]) -> int:
|
|
record_id = 3 + (self.reference_number is not None)
|
|
size = write_uint(stream, record_id)
|
|
size += self.nstring.write(stream)
|
|
if self.reference_number is not None:
|
|
size += write_uint(stream, self.reference_number)
|
|
return size
|
|
|
|
class PropName(Record):
|
|
"""
|
|
PropName record (ID 7, 8)
|
|
"""
|
|
nstring: NString
|
|
"""name string"""
|
|
|
|
reference_number: int | None = None
|
|
"""`None` results in implicit assignment"""
|
|
|
|
def __init__(
|
|
self,
|
|
nstring: str | NString,
|
|
reference_number: int | None = None,
|
|
) -> None:
|
|
"""
|
|
Args:
|
|
nstring: The contained string.
|
|
reference_number: Reference id number for the string.
|
|
Default is to use an implicitly-assigned number.
|
|
"""
|
|
if isinstance(nstring, NString):
|
|
self.nstring = nstring
|
|
else:
|
|
self.nstring = NString(nstring)
|
|
self.reference_number = reference_number
|
|
|
|
def merge_with_modals(self, modals: Modals) -> None:
|
|
modals.reset()
|
|
|
|
def deduplicate_with_modals(self, modals: Modals) -> None:
|
|
modals.reset()
|
|
|
|
@staticmethod
|
|
def read(stream: IO[bytes], record_id: int) -> 'PropName':
|
|
if record_id not in (7, 8):
|
|
raise InvalidDataError(f'Invalid record id for PropName {record_id}')
|
|
nstring = NString.read(stream)
|
|
if record_id == 8:
|
|
reference_number: int | None = read_uint(stream)
|
|
else:
|
|
reference_number = None
|
|
record = PropName(nstring, reference_number)
|
|
logger.debug(f'Record ending at 0x{stream.tell():x}:\n {record}')
|
|
return record
|
|
|
|
def write(self, stream: IO[bytes]) -> int:
|
|
record_id = 7 + (self.reference_number is not None)
|
|
size = write_uint(stream, record_id)
|
|
size += self.nstring.write(stream)
|
|
if self.reference_number is not None:
|
|
size += write_uint(stream, self.reference_number)
|
|
return size
|
|
|
|
|
|
class TextString(Record):
|
|
"""
|
|
TextString record (ID 5, 6)
|
|
"""
|
|
astring: AString
|
|
"""string contents"""
|
|
|
|
reference_number: int | None = None
|
|
"""`None` results in implicit assignment"""
|
|
|
|
def __init__(
|
|
self,
|
|
string: AString | str,
|
|
reference_number: int | None = None,
|
|
) -> None:
|
|
"""
|
|
Args:
|
|
string: The contained string.
|
|
reference_number: Reference id number for the string.
|
|
Default is to use an implicitly-assigned number.
|
|
"""
|
|
if isinstance(string, AString):
|
|
self.astring = string
|
|
else:
|
|
self.astring = AString(string)
|
|
self.reference_number = reference_number
|
|
|
|
def merge_with_modals(self, modals: Modals) -> None:
|
|
modals.reset()
|
|
|
|
def deduplicate_with_modals(self, modals: Modals) -> None:
|
|
modals.reset()
|
|
|
|
@staticmethod
|
|
def read(stream: IO[bytes], record_id: int) -> 'TextString':
|
|
if record_id not in (5, 6):
|
|
raise InvalidDataError(f'Invalid record id for TextString: {record_id}')
|
|
astring = AString.read(stream)
|
|
if record_id == 6:
|
|
reference_number: int | None = read_uint(stream)
|
|
else:
|
|
reference_number = None
|
|
record = TextString(astring, reference_number)
|
|
logger.debug(f'Record ending at 0x{stream.tell():x}:\n {record}')
|
|
return record
|
|
|
|
def write(self, stream: IO[bytes]) -> int:
|
|
record_id = 5 + (self.reference_number is not None)
|
|
size = write_uint(stream, record_id)
|
|
size += self.astring.write(stream)
|
|
if self.reference_number is not None:
|
|
size += write_uint(stream, self.reference_number)
|
|
return size
|
|
|
|
|
|
class PropString(Record):
|
|
"""
|
|
PropString record (ID 9, 10)
|
|
"""
|
|
astring: AString
|
|
"""string contents"""
|
|
|
|
reference_number: int | None
|
|
"""`None` results in implicit assignment"""
|
|
|
|
def __init__(
|
|
self,
|
|
string: AString | str,
|
|
reference_number: int | None = None,
|
|
) -> None:
|
|
"""
|
|
Args:
|
|
string: The contained string.
|
|
reference_number: Reference id number for the string.
|
|
Default is to use an implicitly-assigned number.
|
|
"""
|
|
if isinstance(string, AString):
|
|
self.astring = string
|
|
else:
|
|
self.astring = AString(string)
|
|
self.reference_number = reference_number
|
|
|
|
def merge_with_modals(self, modals: Modals) -> None:
|
|
modals.reset()
|
|
|
|
def deduplicate_with_modals(self, modals: Modals) -> None:
|
|
modals.reset()
|
|
|
|
@staticmethod
|
|
def read(stream: IO[bytes], record_id: int) -> 'PropString':
|
|
if record_id not in (9, 10):
|
|
raise InvalidDataError(f'Invalid record id for PropString: {record_id}')
|
|
astring = AString.read(stream)
|
|
if record_id == 10:
|
|
reference_number: int | None = read_uint(stream)
|
|
else:
|
|
reference_number = None
|
|
record = PropString(astring, reference_number)
|
|
logger.debug(f'Record ending at 0x{stream.tell():x}:\n {record}')
|
|
return record
|
|
|
|
def write(self, stream: IO[bytes]) -> int:
|
|
record_id = 9 + (self.reference_number is not None)
|
|
size = write_uint(stream, record_id)
|
|
size += self.astring.write(stream)
|
|
if self.reference_number is not None:
|
|
size += write_uint(stream, self.reference_number)
|
|
return size
|
|
|
|
|
|
class LayerName(Record):
|
|
"""
|
|
LayerName record (ID 11, 12)
|
|
"""
|
|
nstring: NString
|
|
"""name string"""
|
|
|
|
layer_interval: tuple[int | None, int | None]
|
|
"""bounds on the interval"""
|
|
|
|
type_interval: tuple[int | None, int | None]
|
|
"""bounds on the interval"""
|
|
|
|
is_textlayer: bool
|
|
"""Is this a text layer?"""
|
|
|
|
def __init__(
|
|
self,
|
|
nstring: str | NString,
|
|
layer_interval: tuple[int | None, int | None],
|
|
type_interval: tuple[int | None, int | None],
|
|
is_textlayer: bool,
|
|
) -> None:
|
|
"""
|
|
Args:
|
|
nstring: The layer name.
|
|
layer_interval: Tuple giving bounds (or lack of thereof) on the layer number.
|
|
type_interval: Tuple giving bounds (or lack of thereof) on the type number.
|
|
is_textlayer: `True` if the layer is a text layer.
|
|
"""
|
|
if isinstance(nstring, NString):
|
|
self.nstring = nstring
|
|
else:
|
|
self.nstring = NString(nstring)
|
|
self.layer_interval = layer_interval
|
|
self.type_interval = type_interval
|
|
self.is_textlayer = is_textlayer
|
|
|
|
def merge_with_modals(self, modals: Modals) -> None:
|
|
modals.reset()
|
|
|
|
def deduplicate_with_modals(self, modals: Modals) -> None:
|
|
modals.reset()
|
|
|
|
@staticmethod
|
|
def read(stream: IO[bytes], record_id: int) -> 'LayerName':
|
|
if record_id not in (11, 12):
|
|
raise InvalidDataError(f'Invalid record id for LayerName: {record_id}')
|
|
is_textlayer = (record_id == 12)
|
|
nstring = NString.read(stream)
|
|
layer_interval = read_interval(stream)
|
|
type_interval = read_interval(stream)
|
|
record = LayerName(nstring, layer_interval, type_interval, is_textlayer)
|
|
logger.debug(f'Record ending at 0x{stream.tell():x}:\n {record}')
|
|
return record
|
|
|
|
def write(self, stream: IO[bytes]) -> int:
|
|
record_id = 11 + self.is_textlayer
|
|
size = write_uint(stream, record_id)
|
|
size += self.nstring.write(stream)
|
|
size += write_interval(stream, *self.layer_interval)
|
|
size += write_interval(stream, *self.type_interval)
|
|
return size
|
|
|
|
|
|
class Property(Record):
|
|
"""
|
|
LayerName record (ID 28, 29)
|
|
"""
|
|
name: NString | int | None
|
|
"""`int` is an explicit reference, `None` is a flag to use Modal"""
|
|
values: list[property_value_t] | None
|
|
is_standard: bool | None
|
|
"""Whether this is a standard property."""
|
|
|
|
def __init__(
|
|
self,
|
|
name: NString | str | int | None = None,
|
|
values: list[property_value_t] | None= None,
|
|
is_standard: bool | None = None,
|
|
) -> None:
|
|
"""
|
|
Args:
|
|
name: Property name, reference number, or `None` (i.e. use modal)
|
|
Default `None.
|
|
values: List of property values, or `None` (i.e. use modal)
|
|
Default `None`.
|
|
is_standard: `True` if this is a standard property. `None` to use modal.
|
|
Default `None`.
|
|
"""
|
|
if isinstance(name, NString | int) or name is None:
|
|
self.name = name
|
|
else:
|
|
self.name = NString(name)
|
|
self.values = values
|
|
self.is_standard = is_standard
|
|
|
|
def get_name(self) -> NString | int:
|
|
return verify_modal(self.name) # type: ignore
|
|
|
|
def get_values(self) -> list[property_value_t]:
|
|
return verify_modal(self.values)
|
|
|
|
def get_is_standard(self) -> bool:
|
|
return verify_modal(self.is_standard)
|
|
|
|
def merge_with_modals(self, modals: Modals) -> None:
|
|
adjust_field(self, 'name', modals, 'property_name')
|
|
adjust_field(self, 'values', modals, 'property_value_list')
|
|
adjust_field(self, 'is_standard', modals, 'property_is_standard')
|
|
|
|
def deduplicate_with_modals(self, modals: Modals) -> None:
|
|
dedup_field(self, 'name', modals, 'property_name')
|
|
dedup_field(self, 'values', modals, 'property_value_list')
|
|
if self.values is None and self.name is None:
|
|
dedup_field(self, 'is_standard', modals, 'property_is_standard')
|
|
|
|
@staticmethod
|
|
def read(stream: IO[bytes], record_id: int) -> 'Property':
|
|
if record_id not in (28, 29):
|
|
raise InvalidDataError(f'Invalid record id for PropertyValue: {record_id}')
|
|
if record_id == 29:
|
|
record = Property()
|
|
else:
|
|
byte = read_byte(stream) # UUUUVCNS
|
|
uu = 0x0f & (byte >> 4)
|
|
vv = 0x01 & (byte >> 3)
|
|
cc = 0x01 & (byte >> 2)
|
|
nn = 0x01 & (byte >> 1)
|
|
ss = 0x01 & (byte >> 0)
|
|
|
|
name = read_refname(stream, cc, nn)
|
|
if vv == 0:
|
|
if uu < 0x0f:
|
|
value_count = uu
|
|
else:
|
|
value_count = read_uint(stream)
|
|
values: list[property_value_t] | None = [read_property_value(stream)
|
|
for _ in range(value_count)]
|
|
else:
|
|
values = None
|
|
# if uu != 0:
|
|
# logger.warning('Malformed property record header; requested modal'
|
|
# ' values but had nonzero count. Ignoring count.')
|
|
record = Property(name, values, bool(ss))
|
|
logger.debug(f'Record ending at 0x{stream.tell():x}:\n {record}')
|
|
return record
|
|
|
|
def write(self, stream: IO[bytes]) -> int:
|
|
if self.is_standard is None and self.values is None and self.name is None:
|
|
return write_uint(stream, 29)
|
|
|
|
if self.is_standard is None:
|
|
raise InvalidDataError('Property has value or name, but no is_standard flag!')
|
|
|
|
if self.values is not None:
|
|
value_count = len(self.values)
|
|
vv = 0
|
|
uu = 0x0f if value_count >= 0x0f else value_count
|
|
else:
|
|
vv = 1
|
|
uu = 0
|
|
|
|
cc = self.name is not None
|
|
nn = cc and isinstance(self.name, int)
|
|
ss = self.is_standard
|
|
|
|
size = write_uint(stream, 28)
|
|
size += write_byte(stream, (uu << 4) | (vv << 3) | (cc << 2) | (nn << 1) | ss)
|
|
if cc:
|
|
if nn:
|
|
size += write_uint(stream, self.name) # type: ignore
|
|
else:
|
|
size += self.name.write(stream) # type: ignore
|
|
if not vv:
|
|
if uu == 0x0f:
|
|
size += write_uint(stream, len(self.values)) # type: ignore
|
|
size += sum(write_property_value(stream, pp) for pp in self.values) # type: ignore
|
|
return size
|
|
|
|
|
|
class XName(Record):
|
|
"""
|
|
XName record (ID 30, 31)
|
|
"""
|
|
attribute: int
|
|
"""Attribute number"""
|
|
|
|
bstring: bytes
|
|
"""XName data"""
|
|
|
|
reference_number: int | None
|
|
"""None means to use implicit numbering"""
|
|
|
|
def __init__(
|
|
self,
|
|
attribute: int,
|
|
bstring: bytes,
|
|
reference_number: int | None = None,
|
|
) -> None:
|
|
"""
|
|
Args:
|
|
attribute: Attribute number.
|
|
bstring: Binary XName data.
|
|
reference_number: Reference number for this `XName`.
|
|
Default `None` (implicit).
|
|
"""
|
|
self.attribute = attribute
|
|
self.bstring = bstring
|
|
self.reference_number = reference_number
|
|
|
|
def merge_with_modals(self, modals: Modals) -> None:
|
|
modals.reset()
|
|
|
|
def deduplicate_with_modals(self, modals: Modals) -> None:
|
|
modals.reset()
|
|
|
|
@staticmethod
|
|
def read(stream: IO[bytes], record_id: int) -> 'XName':
|
|
if record_id not in (30, 31):
|
|
raise InvalidDataError(f'Invalid record id for XName: {record_id}')
|
|
attribute = read_uint(stream)
|
|
bstring = read_bstring(stream)
|
|
if record_id == 31:
|
|
reference_number: int | None = read_uint(stream)
|
|
else:
|
|
reference_number = None
|
|
record = XName(attribute, bstring, reference_number)
|
|
logger.debug(f'Record ending at 0x{stream.tell():x}:\n {record}')
|
|
return record
|
|
|
|
def write(self, stream: IO[bytes]) -> int:
|
|
record_id = 30 + (self.reference_number is not None)
|
|
size = write_uint(stream, record_id)
|
|
size += write_uint(stream, self.attribute)
|
|
size += write_bstring(stream, self.bstring)
|
|
if self.reference_number is not None:
|
|
size += write_uint(stream, self.reference_number)
|
|
return size
|
|
|
|
|
|
class XElement(Record):
|
|
"""
|
|
XElement record (ID 32)
|
|
"""
|
|
attribute: int
|
|
"""Attribute number"""
|
|
|
|
bstring: bytes
|
|
"""XElement data"""
|
|
|
|
properties: list['Property']
|
|
|
|
def __init__(
|
|
self,
|
|
attribute: int,
|
|
bstring: bytes,
|
|
properties: list['Property'] | None = None,
|
|
) -> None:
|
|
"""
|
|
Args:
|
|
attribute: Attribute number.
|
|
bstring: Binary data for this XElement.
|
|
properties: List of property records associated with this record.
|
|
"""
|
|
self.attribute = attribute
|
|
self.bstring = bstring
|
|
self.properties = [] if properties is None else properties
|
|
|
|
def merge_with_modals(self, modals: Modals) -> None:
|
|
pass
|
|
|
|
def deduplicate_with_modals(self, modals: Modals) -> None:
|
|
pass
|
|
|
|
@staticmethod
|
|
def read(stream: IO[bytes], record_id: int) -> 'XElement':
|
|
if record_id != 32:
|
|
raise InvalidDataError(f'Invalid record id for XElement: {record_id}')
|
|
attribute = read_uint(stream)
|
|
bstring = read_bstring(stream)
|
|
record = XElement(attribute, bstring)
|
|
logger.debug(f'Record ending at 0x{stream.tell():x}:\n {record}')
|
|
return record
|
|
|
|
def write(self, stream: IO[bytes]) -> int:
|
|
size = write_uint(stream, 32)
|
|
size += write_uint(stream, self.attribute)
|
|
size += write_bstring(stream, self.bstring)
|
|
return size
|
|
|
|
|
|
class XGeometry(Record, GeometryMixin):
|
|
"""
|
|
XGeometry record (ID 33)
|
|
"""
|
|
attribute: int
|
|
"""Attribute number"""
|
|
|
|
bstring: bytes
|
|
"""XGeometry data"""
|
|
|
|
layer: int | None = None
|
|
datatype: int | None = None
|
|
x: int | None = None
|
|
y: int | None = None
|
|
repetition: repetition_t | None = None
|
|
properties: list['Property']
|
|
|
|
def __init__(
|
|
self,
|
|
attribute: int,
|
|
bstring: bytes,
|
|
layer: int | None = None,
|
|
datatype: int | None = None,
|
|
x: int | None = None,
|
|
y: int | None = None,
|
|
repetition: repetition_t | None = None,
|
|
properties: list['Property'] | None = None,
|
|
) -> None:
|
|
"""
|
|
Args:
|
|
attribute: Attribute number for this XGeometry.
|
|
bstring: Binary data for this XGeometry.
|
|
layer: Layer number. Default `None` (reuse modal).
|
|
datatype: Datatype number. Default `None` (reuse modal).
|
|
x: X-offset. Default `None` (use modal).
|
|
y: Y-offset. Default `None` (use modal).
|
|
repetition: Repetition. Default `None` (no repetition).
|
|
properties: List of property records associated with this record.
|
|
"""
|
|
self.attribute = attribute
|
|
self.bstring = bstring
|
|
self.layer = layer
|
|
self.datatype = datatype
|
|
self.x = x
|
|
self.y = y
|
|
self.repetition = repetition
|
|
self.properties = [] if properties is None else properties
|
|
|
|
def merge_with_modals(self, modals: Modals) -> None:
|
|
adjust_coordinates(self, modals, 'geometry_x', 'geometry_y')
|
|
adjust_repetition(self, modals)
|
|
adjust_field(self, 'layer', modals, 'layer')
|
|
adjust_field(self, 'datatype', modals, 'datatype')
|
|
|
|
def deduplicate_with_modals(self, modals: Modals) -> None:
|
|
dedup_coordinates(self, modals, 'geometry_x', 'geometry_y')
|
|
dedup_repetition(self, modals)
|
|
dedup_field(self, 'layer', modals, 'layer')
|
|
dedup_field(self, 'datatype', modals, 'datatype')
|
|
|
|
@staticmethod
|
|
def read(stream: IO[bytes], record_id: int) -> 'XGeometry':
|
|
if record_id != 33:
|
|
raise InvalidDataError(f'Invalid record id for XGeometry: {record_id}')
|
|
|
|
z0, z1, z2, xx, yy, rr, dd, ll = read_bool_byte(stream)
|
|
if z0 or z1 or z2:
|
|
raise InvalidDataError('Malformed XGeometry header')
|
|
attribute = read_uint(stream)
|
|
optional: dict[str, Any] = {}
|
|
if ll:
|
|
optional['layer'] = read_uint(stream)
|
|
if dd:
|
|
optional['datatype'] = read_uint(stream)
|
|
bstring = read_bstring(stream)
|
|
if xx:
|
|
optional['x'] = read_sint(stream)
|
|
if yy:
|
|
optional['y'] = read_sint(stream)
|
|
if rr:
|
|
optional['repetition'] = read_repetition(stream)
|
|
|
|
record = XGeometry(attribute, bstring, **optional)
|
|
logger.debug(f'Record ending at 0x{stream.tell():x}:\n {record}')
|
|
return record
|
|
|
|
def write(self, stream: IO[bytes]) -> int:
|
|
xx = self.x is not None
|
|
yy = self.y is not None
|
|
rr = self.repetition is not None
|
|
dd = self.datatype is not None
|
|
ll = self.layer is not None
|
|
|
|
size = write_uint(stream, 33)
|
|
size += write_bool_byte(stream, (0, 0, 0, xx, yy, rr, dd, ll))
|
|
size += write_uint(stream, self.attribute)
|
|
if ll:
|
|
size += write_uint(stream, self.layer) # type: ignore
|
|
if dd:
|
|
size += write_uint(stream, self.datatype) # type: ignore
|
|
size += write_bstring(stream, self.bstring)
|
|
if xx:
|
|
size += write_sint(stream, self.x) # type: ignore
|
|
if yy:
|
|
size += write_sint(stream, self.y) # type: ignore
|
|
if rr:
|
|
size += self.repetition.write(stream) # type: ignore
|
|
return size
|
|
|
|
|
|
class Cell(Record):
|
|
"""
|
|
Cell record (ID 13, 14)
|
|
"""
|
|
name: int | NString
|
|
"""int specifies "CellName reference" number"""
|
|
|
|
def __init__(self, name: int | str | NString) -> None:
|
|
"""
|
|
Args:
|
|
name: `NString`, or an int specifying a `CellName` reference number.
|
|
"""
|
|
self.name = name if isinstance(name, int | NString) else NString(name)
|
|
|
|
def merge_with_modals(self, modals: Modals) -> None:
|
|
modals.reset()
|
|
|
|
def deduplicate_with_modals(self, modals: Modals) -> None:
|
|
modals.reset()
|
|
|
|
@staticmethod
|
|
def read(stream: IO[bytes], record_id: int) -> 'Cell':
|
|
name: int | NString
|
|
if record_id == 13:
|
|
name = read_uint(stream)
|
|
elif record_id == 14:
|
|
name = NString.read(stream)
|
|
else:
|
|
raise InvalidDataError(f'Invalid record id for Cell: {record_id}')
|
|
record = Cell(name)
|
|
logger.debug(f'Record ending at 0x{stream.tell():x}:\n {record}')
|
|
return record
|
|
|
|
def write(self, stream: IO[bytes]) -> int:
|
|
size = 0
|
|
if isinstance(self.name, int):
|
|
size += write_uint(stream, 13)
|
|
size += write_uint(stream, self.name)
|
|
else:
|
|
size += write_uint(stream, 14)
|
|
size += self.name.write(stream)
|
|
return size
|
|
|
|
|
|
class Placement(Record):
|
|
"""
|
|
Placement record (ID 17, 18)
|
|
"""
|
|
name: int | NString | None = None
|
|
"""name, "CellName reference" number, or reuse modal"""
|
|
|
|
magnification: real_t | None = None
|
|
"""magnification factor"""
|
|
|
|
angle: real_t | None = None
|
|
"""Rotation, degrees counterclockwise"""
|
|
|
|
x: int | None = None
|
|
y: int | None = None
|
|
repetition: repetition_t | None = None
|
|
flip: bool
|
|
"""Whether to perform reflection about the x-axis"""
|
|
|
|
properties: list['Property']
|
|
|
|
def __init__(
|
|
self,
|
|
flip: bool,
|
|
name: NString | str | int | None = None,
|
|
magnification: real_t | None = None,
|
|
angle: real_t | None = None,
|
|
x: int | None = None,
|
|
y: int | None = None,
|
|
repetition: repetition_t | None = None,
|
|
properties: list['Property'] | None = None,
|
|
) -> None:
|
|
"""
|
|
Args:
|
|
flip: Whether to perform reflection about the x-axis.
|
|
name: `NString`, an int specifying a `CellName` reference number,
|
|
or `None` (reuse modal).
|
|
magnification: Magnification factor. Default `None` (use modal).
|
|
angle: Rotation angle in degrees, counterclockwise.
|
|
Default `None` (reuse modal).
|
|
x: X-offset. Default `None` (use modal).
|
|
y: Y-offset. Default `None` (use modal).
|
|
repetition: Repetition. Default `None` (no repetition).
|
|
properties: List of property records associated with this record.
|
|
"""
|
|
self.x = x
|
|
self.y = y
|
|
self.repetition = repetition
|
|
self.flip = flip
|
|
self.magnification = magnification
|
|
self.angle = angle
|
|
if isinstance(name, int | NString) or name is None:
|
|
self.name = name
|
|
else:
|
|
self.name = NString(name)
|
|
self.properties = [] if properties is None else properties
|
|
|
|
def get_name(self) -> NString | int:
|
|
return verify_modal(self.name) # type: ignore
|
|
|
|
def get_x(self) -> int:
|
|
return verify_modal(self.x)
|
|
|
|
def get_y(self) -> int:
|
|
return verify_modal(self.y)
|
|
|
|
def merge_with_modals(self, modals: Modals) -> None:
|
|
adjust_coordinates(self, modals, 'placement_x', 'placement_y')
|
|
adjust_repetition(self, modals)
|
|
adjust_field(self, 'name', modals, 'placement_cell')
|
|
|
|
def deduplicate_with_modals(self, modals: Modals) -> None:
|
|
dedup_coordinates(self, modals, 'placement_x', 'placement_y')
|
|
dedup_repetition(self, modals)
|
|
dedup_field(self, 'name', modals, 'placement_cell')
|
|
|
|
@staticmethod
|
|
def read(stream: IO[bytes], record_id: int) -> 'Placement':
|
|
if record_id not in (17, 18):
|
|
raise InvalidDataError(f'Invalid record id for Placement: {record_id}')
|
|
|
|
#CNXYRAAF (17) or CNXYRMAF (18)
|
|
cc, nn, xx, yy, rr, ma0, ma1, flip = read_bool_byte(stream)
|
|
|
|
optional: dict[str, Any] = {}
|
|
name = read_refname(stream, cc, nn)
|
|
if record_id == 17:
|
|
aa = int((ma0 << 1) | ma1)
|
|
optional['angle'] = aa * 90
|
|
elif record_id == 18:
|
|
mm = ma0
|
|
aa1 = ma1
|
|
if mm:
|
|
optional['magnification'] = read_real(stream)
|
|
if aa1:
|
|
optional['angle'] = read_real(stream)
|
|
if xx:
|
|
optional['x'] = read_sint(stream)
|
|
if yy:
|
|
optional['y'] = read_sint(stream)
|
|
if rr:
|
|
optional['repetition'] = read_repetition(stream)
|
|
|
|
record = Placement(flip, name, **optional)
|
|
logger.debug(f'Record ending at 0x{stream.tell():x}:\n {record}')
|
|
return record
|
|
|
|
def write(self, stream: IO[bytes]) -> int:
|
|
cc = self.name is not None
|
|
nn = cc and isinstance(self.name, int)
|
|
xx = self.x is not None
|
|
yy = self.y is not None
|
|
rr = self.repetition is not None
|
|
ff = self.flip
|
|
|
|
if (self.magnification == 1
|
|
and self.angle is not None
|
|
and abs(self.angle % 90.0) < 1e-14):
|
|
aa = int((self.angle / 90) % 4.0)
|
|
bools = (cc, nn, xx, yy, rr, aa & 0b10, aa & 0b01, ff)
|
|
mm = False
|
|
aq = False
|
|
record_id = 17
|
|
else:
|
|
mm = self.magnification is not None
|
|
aq = self.angle is not None
|
|
bools = (cc, nn, xx, yy, rr, mm, aq, ff)
|
|
record_id = 18
|
|
|
|
size = write_uint(stream, record_id)
|
|
size += write_bool_byte(stream, bools)
|
|
if cc:
|
|
if nn:
|
|
size += write_uint(stream, self.name) # type: ignore
|
|
else:
|
|
size += self.name.write(stream) # type: ignore
|
|
if mm:
|
|
size += write_real(stream, self.magnification) # type: ignore
|
|
if aa:
|
|
size += write_real(stream, self.angle) # type: ignore
|
|
if xx:
|
|
size += write_sint(stream, self.x) # type: ignore
|
|
if yy:
|
|
size += write_sint(stream, self.y) # type: ignore
|
|
if rr:
|
|
size += self.repetition.write(stream) # type: ignore
|
|
return size
|
|
|
|
|
|
class Text(Record, GeometryMixin):
|
|
"""
|
|
Text record (ID 19)
|
|
"""
|
|
string: AString | int | None = None
|
|
layer: int | None = None
|
|
datatype: int | None = None
|
|
x: int | None = None
|
|
y: int | None = None
|
|
repetition: repetition_t | None = None
|
|
properties: list['Property']
|
|
|
|
def __init__(
|
|
self,
|
|
string: AString | str | int | None = None,
|
|
layer: int | None = None,
|
|
datatype: int | None = None,
|
|
x: int | None = None,
|
|
y: int | None = None,
|
|
repetition: repetition_t | None = None,
|
|
properties: list['Property'] | None = None,
|
|
) -> None:
|
|
"""
|
|
Args:
|
|
string: Text content, or `TextString` reference number.
|
|
Default `None` (use modal).
|
|
layer: Layer number. Default `None` (reuse modal).
|
|
datatype: Datatype number. Default `None` (reuse modal).
|
|
x: X-offset. Default `None` (use modal).
|
|
y: Y-offset. Default `None` (use modal).
|
|
repetition: Repetition. Default `None` (no repetition).
|
|
properties: List of property records associated with this record.
|
|
"""
|
|
self.layer = layer
|
|
self.datatype = datatype
|
|
self.x = x
|
|
self.y = y
|
|
self.repetition = repetition
|
|
if isinstance(string, int | AString) or string is None:
|
|
self.string = string
|
|
else:
|
|
self.string = AString(string)
|
|
self.properties = [] if properties is None else properties
|
|
|
|
def get_string(self) -> AString | int:
|
|
return verify_modal(self.string) # type: ignore
|
|
|
|
def merge_with_modals(self, modals: Modals) -> None:
|
|
adjust_coordinates(self, modals, 'text_x', 'text_y')
|
|
adjust_repetition(self, modals)
|
|
adjust_field(self, 'string', modals, 'text_string')
|
|
adjust_field(self, 'layer', modals, 'text_layer')
|
|
adjust_field(self, 'datatype', modals, 'text_datatype')
|
|
|
|
def deduplicate_with_modals(self, modals: Modals) -> None:
|
|
dedup_coordinates(self, modals, 'text_x', 'text_y')
|
|
dedup_repetition(self, modals)
|
|
dedup_field(self, 'string', modals, 'text_string')
|
|
dedup_field(self, 'layer', modals, 'text_layer')
|
|
dedup_field(self, 'datatype', modals, 'text_datatype')
|
|
|
|
@staticmethod
|
|
def read(stream: IO[bytes], record_id: int) -> 'Text':
|
|
if record_id != 19:
|
|
raise InvalidDataError(f'Invalid record id for Text: {record_id}')
|
|
|
|
z0, cc, nn, xx, yy, rr, dd, ll = read_bool_byte(stream)
|
|
if z0:
|
|
raise InvalidDataError('Malformed Text header')
|
|
|
|
optional: dict[str, Any] = {}
|
|
string = read_refstring(stream, cc, nn)
|
|
if ll:
|
|
optional['layer'] = read_uint(stream)
|
|
if dd:
|
|
optional['datatype'] = read_uint(stream)
|
|
if xx:
|
|
optional['x'] = read_sint(stream)
|
|
if yy:
|
|
optional['y'] = read_sint(stream)
|
|
if rr:
|
|
optional['repetition'] = read_repetition(stream)
|
|
|
|
record = Text(string, **optional)
|
|
logger.debug(f'Record ending at 0x{stream.tell():x}:\n {record}')
|
|
return record
|
|
|
|
def write(self, stream: IO[bytes]) -> int:
|
|
cc = self.string is not None
|
|
nn = cc and isinstance(self.string, int)
|
|
xx = self.x is not None
|
|
yy = self.y is not None
|
|
rr = self.repetition is not None
|
|
dd = self.datatype is not None
|
|
ll = self.layer is not None
|
|
|
|
size = write_uint(stream, 19)
|
|
size += write_bool_byte(stream, (0, cc, nn, xx, yy, rr, dd, ll))
|
|
if cc:
|
|
if nn:
|
|
size += write_uint(stream, self.string) # type: ignore
|
|
else:
|
|
size += self.string.write(stream) # type: ignore
|
|
if ll:
|
|
size += write_uint(stream, self.layer) # type: ignore
|
|
if dd:
|
|
size += write_uint(stream, self.datatype) # type: ignore
|
|
if xx:
|
|
size += write_sint(stream, self.x) # type: ignore
|
|
if yy:
|
|
size += write_sint(stream, self.y) # type: ignore
|
|
if rr:
|
|
size += self.repetition.write(stream) # type: ignore
|
|
return size
|
|
|
|
|
|
class Rectangle(Record, GeometryMixin):
|
|
"""
|
|
Rectangle record (ID 20)
|
|
|
|
(x, y) denotes the lower-left (min-x, min-y) corner of the rectangle.
|
|
"""
|
|
layer: int | None
|
|
datatype: int | None
|
|
width: int | None
|
|
"""X-width. `None` means reuse modal"""
|
|
|
|
height: int | None
|
|
"""Y-height. Must be `None` if `is_square` is `True`.
|
|
If `is_square` is `False`, `None` means reuse modal
|
|
"""
|
|
|
|
x: int | None
|
|
"""x-offset of the rectangle's lower-left (min-x) point.
|
|
None means reuse modal.
|
|
"""
|
|
y: int | None
|
|
"""y-offset of the rectangle's lower-left (min-y) point.
|
|
None means reuse modal
|
|
"""
|
|
|
|
repetition: repetition_t | None
|
|
is_square: bool
|
|
"""If `True`, `height` must be `None`"""
|
|
|
|
properties: list['Property']
|
|
|
|
def __init__(
|
|
self,
|
|
is_square: bool = False,
|
|
layer: int | None = None,
|
|
datatype: int | None = None,
|
|
width: int | None = None,
|
|
height: int | None = None,
|
|
x: int | None = None,
|
|
y: int | None = None,
|
|
repetition: repetition_t | None = None,
|
|
properties: list['Property'] | None = None,
|
|
) -> None:
|
|
self.is_square = is_square
|
|
self.layer = layer
|
|
self.datatype = datatype
|
|
self.width = width
|
|
self.height = height
|
|
self.x = x
|
|
self.y = y
|
|
self.repetition = repetition
|
|
if is_square and self.height is not None:
|
|
raise InvalidDataError('Rectangle is square and also has height')
|
|
self.properties = [] if properties is None else properties
|
|
|
|
def get_width(self) -> int:
|
|
return verify_modal(self.width)
|
|
|
|
def get_height(self) -> int:
|
|
if self.is_square:
|
|
return verify_modal(self.width)
|
|
return verify_modal(self.height)
|
|
|
|
def merge_with_modals(self, modals: Modals) -> None:
|
|
adjust_coordinates(self, modals, 'geometry_x', 'geometry_y')
|
|
adjust_repetition(self, modals)
|
|
adjust_field(self, 'layer', modals, 'layer')
|
|
adjust_field(self, 'datatype', modals, 'datatype')
|
|
adjust_field(self, 'width', modals, 'geometry_w')
|
|
if self.is_square:
|
|
adjust_field(self, 'width', modals, 'geometry_h')
|
|
else:
|
|
adjust_field(self, 'height', modals, 'geometry_h')
|
|
|
|
def deduplicate_with_modals(self, modals: Modals) -> None:
|
|
dedup_coordinates(self, modals, 'geometry_x', 'geometry_y')
|
|
dedup_repetition(self, modals)
|
|
dedup_field(self, 'layer', modals, 'layer')
|
|
dedup_field(self, 'datatype', modals, 'datatype')
|
|
dedup_field(self, 'width', modals, 'geometry_w')
|
|
if self.is_square:
|
|
dedup_field(self, 'width', modals, 'geometry_h')
|
|
else:
|
|
dedup_field(self, 'height', modals, 'geometry_h')
|
|
|
|
@staticmethod
|
|
def read(stream: IO[bytes], record_id: int) -> 'Rectangle':
|
|
if record_id != 20:
|
|
raise InvalidDataError(f'Invalid record id for Rectangle: {record_id}')
|
|
|
|
is_square, ww, hh, xx, yy, rr, dd, ll = read_bool_byte(stream)
|
|
optional: dict[str, Any] = {}
|
|
if ll:
|
|
optional['layer'] = read_uint(stream)
|
|
if dd:
|
|
optional['datatype'] = read_uint(stream)
|
|
if ww:
|
|
optional['width'] = read_uint(stream)
|
|
if hh:
|
|
optional['height'] = read_uint(stream)
|
|
if xx:
|
|
optional['x'] = read_sint(stream)
|
|
if yy:
|
|
optional['y'] = read_sint(stream)
|
|
if rr:
|
|
optional['repetition'] = read_repetition(stream)
|
|
record = Rectangle(is_square, **optional)
|
|
logger.debug(f'Record ending at 0x{stream.tell():x}:\n {record}')
|
|
return record
|
|
|
|
def write(self, stream: IO[bytes]) -> int:
|
|
ss = self.is_square
|
|
ww = self.width is not None
|
|
hh = self.height is not None
|
|
xx = self.x is not None
|
|
yy = self.y is not None
|
|
rr = self.repetition is not None
|
|
dd = self.datatype is not None
|
|
ll = self.layer is not None
|
|
|
|
size = write_uint(stream, 20)
|
|
size += write_bool_byte(stream, (ss, ww, hh, xx, yy, rr, dd, ll))
|
|
if ll:
|
|
size += write_uint(stream, self.layer) # type: ignore
|
|
if dd:
|
|
size += write_uint(stream, self.datatype) # type: ignore
|
|
if ww:
|
|
size += write_uint(stream, self.width) # type: ignore
|
|
if hh:
|
|
size += write_uint(stream, self.height) # type: ignore
|
|
if xx:
|
|
size += write_sint(stream, self.x) # type: ignore
|
|
if yy:
|
|
size += write_sint(stream, self.y) # type: ignore
|
|
if rr:
|
|
size += self.repetition.write(stream) # type: ignore
|
|
return size
|
|
|
|
|
|
class Polygon(Record, GeometryMixin):
|
|
"""
|
|
Polygon record (ID 21)
|
|
"""
|
|
layer: int | None
|
|
datatype: int | None
|
|
x: int | None
|
|
"""x-offset of the polygon's first point.
|
|
None means reuse modal
|
|
"""
|
|
y: int | None
|
|
"""y-offset of the polygon's first point.
|
|
None means reuse modal
|
|
"""
|
|
repetition: repetition_t | None
|
|
point_list: point_list_t | None
|
|
"""
|
|
List of offsets from the initial vertex (x, y) to the remaining
|
|
vertices, `[[dx0, dy0], [dx1, dy1], ...]`.
|
|
The list is an implicitly closed path, vertices are [int, int].
|
|
The initial vertex is located at (x, y) and is not represented in `point_list`.
|
|
`None` means reuse modal.
|
|
"""
|
|
|
|
properties: list['Property']
|
|
|
|
def __init__(
|
|
self,
|
|
point_list: point_list_t | None = None,
|
|
layer: int | None = None,
|
|
datatype: int | None = None,
|
|
x: int | None = None,
|
|
y: int | None = None,
|
|
repetition: repetition_t | None = None,
|
|
properties: list['Property'] | None = None,
|
|
) -> None:
|
|
self.layer = layer
|
|
self.datatype = datatype
|
|
self.x = x
|
|
self.y = y
|
|
self.repetition = repetition
|
|
self.point_list = point_list
|
|
self.properties = [] if properties is None else properties
|
|
|
|
if point_list is not None and len(point_list) < 3:
|
|
warn('Polygon with < 3 points', stacklevel=2)
|
|
|
|
def get_point_list(self) -> point_list_t:
|
|
return verify_modal(self.point_list)
|
|
|
|
def merge_with_modals(self, modals: Modals) -> None:
|
|
adjust_coordinates(self, modals, 'geometry_x', 'geometry_y')
|
|
adjust_repetition(self, modals)
|
|
adjust_field(self, 'layer', modals, 'layer')
|
|
adjust_field(self, 'datatype', modals, 'datatype')
|
|
adjust_field(self, 'point_list', modals, 'polygon_point_list')
|
|
|
|
def deduplicate_with_modals(self, modals: Modals) -> None:
|
|
dedup_coordinates(self, modals, 'geometry_x', 'geometry_y')
|
|
dedup_repetition(self, modals)
|
|
dedup_field(self, 'layer', modals, 'layer')
|
|
dedup_field(self, 'datatype', modals, 'datatype')
|
|
dedup_field(self, 'point_list', modals, 'polygon_point_list')
|
|
|
|
@staticmethod
|
|
def read(stream: IO[bytes], record_id: int) -> 'Polygon':
|
|
if record_id != 21:
|
|
raise InvalidDataError(f'Invalid record id for Polygon: {record_id}')
|
|
|
|
z0, z1, pp, xx, yy, rr, dd, ll = read_bool_byte(stream)
|
|
if z0 or z1:
|
|
raise InvalidDataError('Invalid polygon header')
|
|
|
|
optional: dict[str, Any] = {}
|
|
if ll:
|
|
optional['layer'] = read_uint(stream)
|
|
if dd:
|
|
optional['datatype'] = read_uint(stream)
|
|
if pp:
|
|
optional['point_list'] = read_point_list(stream, implicit_closed=True)
|
|
if xx:
|
|
optional['x'] = read_sint(stream)
|
|
if yy:
|
|
optional['y'] = read_sint(stream)
|
|
if rr:
|
|
optional['repetition'] = read_repetition(stream)
|
|
record = Polygon(**optional)
|
|
logger.debug('Record ending at 0x{stream.tell():x}:\n {record}')
|
|
return record
|
|
|
|
def write(self, stream: IO[bytes], fast: bool = False) -> int:
|
|
pp = self.point_list is not None
|
|
xx = self.x is not None
|
|
yy = self.y is not None
|
|
rr = self.repetition is not None
|
|
dd = self.datatype is not None
|
|
ll = self.layer is not None
|
|
|
|
size = write_uint(stream, 21)
|
|
size += write_bool_byte(stream, (0, 0, pp, xx, yy, rr, dd, ll))
|
|
if ll:
|
|
size += write_uint(stream, self.layer) # type: ignore
|
|
if dd:
|
|
size += write_uint(stream, self.datatype) # type: ignore
|
|
if pp:
|
|
size += write_point_list(stream, self.point_list, # type: ignore
|
|
implicit_closed=True, fast=fast)
|
|
if xx:
|
|
size += write_sint(stream, self.x) # type: ignore
|
|
if yy:
|
|
size += write_sint(stream, self.y) # type: ignore
|
|
if rr:
|
|
size += self.repetition.write(stream) # type: ignore
|
|
return size
|
|
|
|
|
|
class Path(Record, GeometryMixin):
|
|
"""
|
|
Polygon record (ID 22)
|
|
"""
|
|
layer: int | None = None
|
|
datatype: int | None = None
|
|
x: int | None = None
|
|
y: int | None = None
|
|
repetition: repetition_t | None = None
|
|
point_list: point_list_t | None = None
|
|
"""
|
|
List of offsets from the initial vertex (x, y) to the remaining vertices,
|
|
`[[dx0, dy0], [dx1, dy1], ...]`.
|
|
The initial vertex is located at (x, y) and is not represented in `point_list`.
|
|
Offsets are [int, int]; `None` means reuse modal.
|
|
"""
|
|
|
|
half_width: int | None = None
|
|
"""None means reuse modal"""
|
|
|
|
extension_start: pathextension_t | None = None
|
|
"""
|
|
`None` means reuse modal.
|
|
Tuple is of the form (`PathExtensionScheme`, int | None)
|
|
Second value is None unless using `PathExtensionScheme.Arbitrary`
|
|
Value determines extension past start point.
|
|
"""
|
|
|
|
extension_end: pathextension_t | None = None
|
|
"""
|
|
Same form as `extension_end`. Value determines extension past end point.
|
|
"""
|
|
|
|
properties: list['Property']
|
|
|
|
def __init__(
|
|
self,
|
|
point_list: point_list_t | None = None,
|
|
half_width: int | None = None,
|
|
extension_start: pathextension_t | None = None,
|
|
extension_end: pathextension_t | None = None,
|
|
layer: int | None = None,
|
|
datatype: int | None = None,
|
|
x: int | None = None,
|
|
y: int | None = None,
|
|
repetition: repetition_t | None = None,
|
|
properties: list['Property'] | None = None,
|
|
) -> None:
|
|
self.layer = layer
|
|
self.datatype = datatype
|
|
self.x = x
|
|
self.y = y
|
|
self.repetition = repetition
|
|
self.point_list = point_list
|
|
self.half_width = half_width
|
|
self.extension_start = extension_start
|
|
self.extension_end = extension_end
|
|
self.properties = [] if properties is None else properties
|
|
|
|
def get_point_list(self) -> point_list_t:
|
|
return verify_modal(self.point_list)
|
|
|
|
def get_half_width(self) -> int:
|
|
return verify_modal(self.half_width)
|
|
|
|
def get_extension_start(self) -> pathextension_t:
|
|
return verify_modal(self.extension_start)
|
|
|
|
def get_extension_end(self) -> pathextension_t:
|
|
return verify_modal(self.extension_end)
|
|
|
|
def merge_with_modals(self, modals: Modals) -> None:
|
|
adjust_coordinates(self, modals, 'geometry_x', 'geometry_y')
|
|
adjust_repetition(self, modals)
|
|
adjust_field(self, 'layer', modals, 'layer')
|
|
adjust_field(self, 'datatype', modals, 'datatype')
|
|
adjust_field(self, 'point_list', modals, 'path_point_list')
|
|
adjust_field(self, 'half_width', modals, 'path_half_width')
|
|
adjust_field(self, 'extension_start', modals, 'path_extension_start')
|
|
adjust_field(self, 'extension_end', modals, 'path_extension_end')
|
|
|
|
def deduplicate_with_modals(self, modals: Modals) -> None:
|
|
dedup_coordinates(self, modals, 'geometry_x', 'geometry_y')
|
|
dedup_repetition(self, modals)
|
|
dedup_field(self, 'layer', modals, 'layer')
|
|
dedup_field(self, 'datatype', modals, 'datatype')
|
|
dedup_field(self, 'point_list', modals, 'path_point_list')
|
|
dedup_field(self, 'half_width', modals, 'path_half_width')
|
|
dedup_field(self, 'extension_start', modals, 'path_extension_start')
|
|
dedup_field(self, 'extension_end', modals, 'path_extension_end')
|
|
|
|
@staticmethod
|
|
def read(stream: IO[bytes], record_id: int) -> 'Path':
|
|
if record_id != 22:
|
|
raise InvalidDataError(f'Invalid record id for Path: {record_id}')
|
|
|
|
ee, ww, pp, xx, yy, rr, dd, ll = read_bool_byte(stream)
|
|
optional: dict[str, Any] = {}
|
|
if ll:
|
|
optional['layer'] = read_uint(stream)
|
|
if dd:
|
|
optional['datatype'] = read_uint(stream)
|
|
if ww:
|
|
optional['half_width'] = read_uint(stream)
|
|
if ee:
|
|
scheme = read_uint(stream)
|
|
scheme_end = scheme & 0b11
|
|
scheme_start = (scheme >> 2) & 0b11
|
|
|
|
def get_pathext(ext_scheme: int) -> pathextension_t | None:
|
|
if ext_scheme == 0:
|
|
return None
|
|
if ext_scheme == 1:
|
|
return PathExtensionScheme.Flush, None
|
|
if ext_scheme == 2:
|
|
return PathExtensionScheme.HalfWidth, None
|
|
if ext_scheme == 3:
|
|
return PathExtensionScheme.Arbitrary, read_sint(stream)
|
|
raise InvalidDataError(f'Invalid ext_scheme: {ext_scheme}')
|
|
|
|
optional['extension_start'] = get_pathext(scheme_start)
|
|
optional['extension_end'] = get_pathext(scheme_end)
|
|
if pp:
|
|
optional['point_list'] = read_point_list(stream, implicit_closed=False)
|
|
if xx:
|
|
optional['x'] = read_sint(stream)
|
|
if yy:
|
|
optional['y'] = read_sint(stream)
|
|
if rr:
|
|
optional['repetition'] = read_repetition(stream)
|
|
record = Path(**optional)
|
|
logger.debug(f'Record ending at 0x{stream.tell():x}:\n {record}')
|
|
return record
|
|
|
|
def write(self, stream: IO[bytes], fast: bool = False) -> int:
|
|
ee = self.extension_start is not None or self.extension_end is not None
|
|
ww = self.half_width is not None
|
|
pp = self.point_list is not None
|
|
xx = self.x is not None
|
|
yy = self.y is not None
|
|
rr = self.repetition is not None
|
|
dd = self.datatype is not None
|
|
ll = self.layer is not None
|
|
|
|
size = write_uint(stream, 21)
|
|
size += write_bool_byte(stream, (ee, ww, pp, xx, yy, rr, dd, ll))
|
|
if ll:
|
|
size += write_uint(stream, self.layer) # type: ignore
|
|
if dd:
|
|
size += write_uint(stream, self.datatype) # type: ignore
|
|
if ww:
|
|
size += write_uint(stream, self.half_width) # type: ignore
|
|
if ee:
|
|
scheme = 0
|
|
if self.extension_start is not None:
|
|
scheme += self.extension_start[0].value << 2
|
|
if self.extension_end is not None:
|
|
scheme += self.extension_end[0].value
|
|
size += write_uint(stream, scheme)
|
|
if scheme & 0b1100 == 0b1100:
|
|
size += write_sint(stream, self.extension_start[1]) # type: ignore
|
|
if scheme & 0b0011 == 0b0011:
|
|
size += write_sint(stream, self.extension_end[1]) # type: ignore
|
|
if pp:
|
|
size += write_point_list(stream, self.point_list, # type: ignore
|
|
implicit_closed=False, fast=fast)
|
|
if xx:
|
|
size += write_sint(stream, self.x) # type: ignore
|
|
if yy:
|
|
size += write_sint(stream, self.y) # type: ignore
|
|
if rr:
|
|
size += self.repetition.write(stream) # type: ignore
|
|
return size
|
|
|
|
|
|
class Trapezoid(Record, GeometryMixin):
|
|
"""
|
|
Trapezoid record (ID 23, 24, 25)
|
|
|
|
Trapezoid with at least two sides parallel to the x- or y-axis.
|
|
(x, y) denotes the lower-left (min-x, min-y) corner of the trapezoid's bounding box.
|
|
"""
|
|
layer: int | None = None
|
|
datatype: int | None = None
|
|
width: int | None = None
|
|
"""Bounding box x-width, None means reuse modal."""
|
|
|
|
height: int | None = None
|
|
"""Bounding box y-height, None means reuse modal."""
|
|
|
|
x: int | None = None
|
|
"""x-offset to lower-left corner of the trapezoid's bounding box.
|
|
None means reuse modal
|
|
"""
|
|
|
|
y: int | None = None
|
|
"""y-offset to lower-left corner of the trapezoid's bounding box.
|
|
None means reuse modal
|
|
"""
|
|
|
|
repetition: repetition_t | None = None
|
|
delta_a: int = 0
|
|
"""
|
|
If horizontal, signed x-distance from top left vertex to bottom left vertex.
|
|
If vertical, signed y-distance from bottom left vertex to bottom right vertex.
|
|
None means reuse modal.
|
|
"""
|
|
|
|
delta_b: int = 0
|
|
"""
|
|
If horizontal, signed x-distance from bottom right vertex to top right vertex.
|
|
If vertical, signed y-distance from top right vertex to top left vertex.
|
|
None means reuse modal.
|
|
"""
|
|
|
|
is_vertical: bool
|
|
"""
|
|
`True` if the left and right sides are aligned to the y-axis.
|
|
If the trapezoid is a rectangle, either `True` or `False` can be used.
|
|
"""
|
|
|
|
properties: list['Property']
|
|
|
|
def __init__(
|
|
self,
|
|
is_vertical: bool,
|
|
delta_a: int = 0,
|
|
delta_b: int = 0,
|
|
layer: int | None = None,
|
|
datatype: int | None = None,
|
|
width: int | None = None,
|
|
height: int | None = None,
|
|
x: int | None = None,
|
|
y: int | None = None,
|
|
repetition: repetition_t | None = None,
|
|
properties: list['Property'] | None = None,
|
|
) -> None:
|
|
"""
|
|
Raises:
|
|
InvalidDataError: if dimensions are impossible.
|
|
"""
|
|
self.is_vertical = bool(is_vertical)
|
|
self.delta_a = delta_a
|
|
self.delta_b = delta_b
|
|
self.layer = layer
|
|
self.datatype = datatype
|
|
self.width = width
|
|
self.height = height
|
|
self.x = x
|
|
self.y = y
|
|
self.repetition = repetition
|
|
self.properties = [] if properties is None else properties
|
|
|
|
if self.is_vertical:
|
|
if height is not None and delta_b - delta_a > height:
|
|
raise InvalidDataError(f'Trapezoid: h < delta_b - delta_a ({height} < {delta_b} - {delta_a})')
|
|
elif width is not None and delta_b - delta_a > width:
|
|
raise InvalidDataError(f'Trapezoid: w < delta_b - delta_a ({width} < {delta_b} - {delta_a})')
|
|
|
|
def get_is_vertical(self) -> bool:
|
|
return verify_modal(self.is_vertical)
|
|
|
|
def get_delta_a(self) -> int:
|
|
return verify_modal(self.delta_a)
|
|
|
|
def get_delta_b(self) -> int:
|
|
return verify_modal(self.delta_b)
|
|
|
|
def get_width(self) -> int:
|
|
return verify_modal(self.width)
|
|
|
|
def get_height(self) -> int:
|
|
return verify_modal(self.height)
|
|
|
|
def merge_with_modals(self, modals: Modals) -> None:
|
|
adjust_coordinates(self, modals, 'geometry_x', 'geometry_y')
|
|
adjust_repetition(self, modals)
|
|
adjust_field(self, 'layer', modals, 'layer')
|
|
adjust_field(self, 'datatype', modals, 'datatype')
|
|
adjust_field(self, 'width', modals, 'geometry_w')
|
|
adjust_field(self, 'height', modals, 'geometry_h')
|
|
|
|
def deduplicate_with_modals(self, modals: Modals) -> None:
|
|
dedup_coordinates(self, modals, 'geometry_x', 'geometry_y')
|
|
dedup_repetition(self, modals)
|
|
dedup_field(self, 'layer', modals, 'layer')
|
|
dedup_field(self, 'datatype', modals, 'datatype')
|
|
dedup_field(self, 'width', modals, 'geometry_w')
|
|
dedup_field(self, 'height', modals, 'geometry_h')
|
|
|
|
@staticmethod
|
|
def read(stream: IO[bytes], record_id: int) -> 'Trapezoid':
|
|
if record_id not in (23, 24, 25):
|
|
raise InvalidDataError(f'Invalid record id for Trapezoid: {record_id}')
|
|
|
|
is_vertical, ww, hh, xx, yy, rr, dd, ll = read_bool_byte(stream)
|
|
optional: dict[str, Any] = {}
|
|
if ll:
|
|
optional['layer'] = read_uint(stream)
|
|
if dd:
|
|
optional['datatype'] = read_uint(stream)
|
|
if ww:
|
|
optional['width'] = read_uint(stream)
|
|
if hh:
|
|
optional['height'] = read_uint(stream)
|
|
if record_id != 25:
|
|
optional['delta_a'] = read_sint(stream)
|
|
if record_id != 24:
|
|
optional['delta_b'] = read_sint(stream)
|
|
if xx:
|
|
optional['x'] = read_sint(stream)
|
|
if yy:
|
|
optional['y'] = read_sint(stream)
|
|
if rr:
|
|
optional['repetition'] = read_repetition(stream)
|
|
record = Trapezoid(bool(is_vertical), **optional)
|
|
logger.debug(f'Record ending at 0x{stream.tell():x}:\n {record}')
|
|
return record
|
|
|
|
def write(self, stream: IO[bytes]) -> int:
|
|
vv = self.is_vertical
|
|
ww = self.width is not None
|
|
hh = self.height is not None
|
|
xx = self.x is not None
|
|
yy = self.y is not None
|
|
rr = self.repetition is not None
|
|
dd = self.datatype is not None
|
|
ll = self.layer is not None
|
|
|
|
if self.delta_b == 0:
|
|
record_id = 24
|
|
elif self.delta_a == 0:
|
|
record_id = 25
|
|
else:
|
|
record_id = 23
|
|
size = write_uint(stream, record_id)
|
|
size += write_bool_byte(stream, (vv, ww, hh, xx, yy, rr, dd, ll))
|
|
if ll:
|
|
size += write_uint(stream, self.layer) # type: ignore
|
|
if dd:
|
|
size += write_uint(stream, self.datatype) # type: ignore
|
|
if ww:
|
|
size += write_uint(stream, self.width) # type: ignore
|
|
if hh:
|
|
size += write_uint(stream, self.height) # type: ignore
|
|
if record_id != 25:
|
|
size += write_sint(stream, self.delta_a) # type: ignore
|
|
if record_id != 24:
|
|
size += write_sint(stream, self.delta_b) # type: ignore
|
|
if xx:
|
|
size += write_sint(stream, self.x) # type: ignore
|
|
if yy:
|
|
size += write_sint(stream, self.y) # type: ignore
|
|
if rr:
|
|
size += self.repetition.write(stream) # type: ignore
|
|
return size
|
|
|
|
|
|
class CTrapezoid(Record, GeometryMixin):
|
|
r"""
|
|
CTrapezoid record (ID 26)
|
|
|
|
Compact trapezoid formats.
|
|
Two sides are assumed to be parallel to the x- or y-axis, and the remaining
|
|
sides form 45 or 90 degree angles with them.
|
|
|
|
`ctrapezoid_type` is in `range(0, 26)`, with the following shapes:
|
|
____ ____ _____ ______
|
|
| 0 \ / 2 | / 4 \ / 6 /
|
|
|_____\ /_____| /_______\ /_____/
|
|
______ ______ _________ ______
|
|
| 1 / \ 3 | \ 5 / \ 7 \
|
|
|____/ \____| \_____/ \_____\
|
|
w >= h w >= 2h
|
|
|
|
___ ___ |\ /| /| |\
|
|
|\ /| | | | | | \ / | / | | \
|
|
| \ / | |10 | | 11| |12| |13| |14| |15|
|
|
| \ / | | / \ | | | | | | | | |
|
|
| 8 | | 9 | | / \ | | / \ | | / \ |
|
|
|___| |___| |/ \| |/ \| |/ \|
|
|
h >= w h >= w h >= 2w h >= 2w
|
|
|
|
__________
|
|
|\ /| /\ |\ /| | 24 | (rect)
|
|
| \ / | / \ | \ / | |__________|
|
|
|16\ /18| / 20 \ |22\ /23|
|
|
|___\ /___| /______\ | / \ |
|
|
____ ____ ______ | / \ |
|
|
| / \ | \ / |/ \| _____
|
|
|17/ \19| \ 21 / h = 2w | | (sqr)
|
|
| / \ | \ / set h = None | 25 |
|
|
|/ \| \/ |_____|
|
|
w = h w = 2h w = h
|
|
set h = None set w = None set h = None
|
|
|
|
"""
|
|
ctrapezoid_type: int | None = None
|
|
"""See class docstring for details. None means reuse modal."""
|
|
|
|
layer: int | None = None
|
|
datatype: int | None = None
|
|
width: int | None = None
|
|
"""width: Bounding box x-width
|
|
None means unnecessary, or reuse modal if necessary.
|
|
"""
|
|
|
|
height: int | None = None
|
|
"""Bounding box y-height.
|
|
None means unnecessary, or reuse modal if necessary.
|
|
"""
|
|
|
|
x: int | None = None
|
|
"""x-offset of lower-left (min-x) point of bounding box.
|
|
None means reuse modal
|
|
"""
|
|
y: int | None = None
|
|
"""y-offset of lower-left (min-y) point of bounding box.
|
|
None means reuse modal
|
|
"""
|
|
|
|
repetition: repetition_t | None = None
|
|
properties: list['Property']
|
|
|
|
def __init__(
|
|
self,
|
|
ctrapezoid_type: int | None = None,
|
|
layer: int | None = None,
|
|
datatype: int | None = None,
|
|
width: int | None = None,
|
|
height: int | None = None,
|
|
x: int | None = None,
|
|
y: int | None = None,
|
|
repetition: repetition_t | None = None,
|
|
properties: list['Property'] | None = None,
|
|
) -> None:
|
|
"""
|
|
Raises:
|
|
InvalidDataError: if dimensions are invalid.
|
|
"""
|
|
self.ctrapezoid_type = ctrapezoid_type
|
|
self.layer = layer
|
|
self.datatype = datatype
|
|
self.width = width
|
|
self.height = height
|
|
self.x = x
|
|
self.y = y
|
|
self.repetition = repetition
|
|
self.properties = [] if properties is None else properties
|
|
|
|
self.check_valid()
|
|
|
|
def get_ctrapezoid_type(self) -> int:
|
|
return verify_modal(self.ctrapezoid_type)
|
|
|
|
def get_height(self) -> int:
|
|
if self.ctrapezoid_type is None:
|
|
return verify_modal(self.height)
|
|
if self.ctrapezoid_type in (16, 17, 18, 19, 22, 23, 25):
|
|
return verify_modal(self.width)
|
|
return verify_modal(self.height)
|
|
|
|
def get_width(self) -> int:
|
|
if self.ctrapezoid_type is None:
|
|
return verify_modal(self.width)
|
|
if self.ctrapezoid_type in (20, 21):
|
|
return verify_modal(self.height)
|
|
return verify_modal(self.width)
|
|
|
|
def merge_with_modals(self, modals: Modals) -> None:
|
|
adjust_coordinates(self, modals, 'geometry_x', 'geometry_y')
|
|
adjust_repetition(self, modals)
|
|
adjust_field(self, 'layer', modals, 'layer')
|
|
adjust_field(self, 'datatype', modals, 'datatype')
|
|
adjust_field(self, 'ctrapezoid_type', modals, 'ctrapezoid_type')
|
|
|
|
if self.ctrapezoid_type in (20, 21):
|
|
if self.width is not None:
|
|
raise InvalidDataError(f'CTrapezoid has spurious width entry: {self.width}')
|
|
else:
|
|
adjust_field(self, 'width', modals, 'geometry_w')
|
|
|
|
if self.ctrapezoid_type in (16, 17, 18, 19, 22, 23, 25):
|
|
if self.height is not None:
|
|
raise InvalidDataError(f'CTrapezoid has spurious height entry: {self.height}')
|
|
else:
|
|
adjust_field(self, 'height', modals, 'geometry_h')
|
|
|
|
self.check_valid()
|
|
|
|
def deduplicate_with_modals(self, modals: Modals) -> None:
|
|
dedup_coordinates(self, modals, 'geometry_x', 'geometry_y')
|
|
dedup_repetition(self, modals)
|
|
dedup_field(self, 'layer', modals, 'layer')
|
|
dedup_field(self, 'datatype', modals, 'datatype')
|
|
dedup_field(self, 'width', modals, 'geometry_w')
|
|
dedup_field(self, 'height', modals, 'geometry_h')
|
|
dedup_field(self, 'ctrapezoid_type', modals, 'ctrapezoid_type')
|
|
|
|
if self.ctrapezoid_type in (20, 21):
|
|
if self.width is not None:
|
|
raise InvalidDataError(f'CTrapezoid has spurious width entry: {self.width}')
|
|
else:
|
|
dedup_field(self, 'width', modals, 'geometry_w')
|
|
|
|
if self.ctrapezoid_type in (16, 17, 18, 19, 22, 23, 25):
|
|
if self.height is not None:
|
|
raise InvalidDataError(f'CTrapezoid has spurious height entry: {self.height}')
|
|
else:
|
|
dedup_field(self, 'height', modals, 'geometry_h')
|
|
|
|
self.check_valid()
|
|
|
|
@staticmethod
|
|
def read(stream: IO[bytes], record_id: int) -> 'CTrapezoid':
|
|
if record_id != 26:
|
|
raise InvalidDataError(f'Invalid record id for CTrapezoid: {record_id}')
|
|
|
|
tt, ww, hh, xx, yy, rr, dd, ll = read_bool_byte(stream)
|
|
optional: dict[str, Any] = {}
|
|
if ll:
|
|
optional['layer'] = read_uint(stream)
|
|
if dd:
|
|
optional['datatype'] = read_uint(stream)
|
|
if tt:
|
|
optional['ctrapezoid_type'] = read_uint(stream)
|
|
if ww:
|
|
optional['width'] = read_uint(stream)
|
|
if hh:
|
|
optional['height'] = read_uint(stream)
|
|
if xx:
|
|
optional['x'] = read_sint(stream)
|
|
if yy:
|
|
optional['y'] = read_sint(stream)
|
|
if rr:
|
|
optional['repetition'] = read_repetition(stream)
|
|
record = CTrapezoid(**optional)
|
|
logger.debug(f'Record ending at 0x{stream.tell():x}:\n {record}')
|
|
return record
|
|
|
|
def write(self, stream: IO[bytes]) -> int:
|
|
tt = self.ctrapezoid_type is not None
|
|
ww = self.width is not None
|
|
hh = self.height is not None
|
|
xx = self.x is not None
|
|
yy = self.y is not None
|
|
rr = self.repetition is not None
|
|
dd = self.datatype is not None
|
|
ll = self.layer is not None
|
|
|
|
size = write_uint(stream, 26)
|
|
size += write_bool_byte(stream, (tt, ww, hh, xx, yy, rr, dd, ll))
|
|
if ll:
|
|
size += write_uint(stream, self.layer) # type: ignore
|
|
if dd:
|
|
size += write_uint(stream, self.datatype) # type: ignore
|
|
if tt:
|
|
size += write_uint(stream, self.ctrapezoid_type) # type: ignore
|
|
if ww:
|
|
size += write_uint(stream, self.width) # type: ignore
|
|
if hh:
|
|
size += write_uint(stream, self.height) # type: ignore
|
|
if xx:
|
|
size += write_sint(stream, self.x) # type: ignore
|
|
if yy:
|
|
size += write_sint(stream, self.y) # type: ignore
|
|
if rr:
|
|
size += self.repetition.write(stream) # type: ignore
|
|
return size
|
|
|
|
def check_valid(self) -> None:
|
|
ctrapezoid_type = self.ctrapezoid_type
|
|
width = self.width
|
|
height = self.height
|
|
|
|
if ctrapezoid_type in (20, 21) and width is not None:
|
|
raise InvalidDataError(f'CTrapezoid has spurious width entry: {width}')
|
|
if ctrapezoid_type in (16, 17, 18, 19, 22, 23, 25) and height is not None:
|
|
raise InvalidDataError(f'CTrapezoid has spurious height entry: {height}')
|
|
|
|
if width is not None and height is not None:
|
|
if ctrapezoid_type in range(0, 4) and width < height: # noqa: PIE808
|
|
raise InvalidDataError(f'CTrapezoid has width < height ({width} < {height})')
|
|
if ctrapezoid_type in range(4, 8) and width < 2 * height:
|
|
raise InvalidDataError(f'CTrapezoid has width < 2*height ({width} < 2 * {height})')
|
|
if ctrapezoid_type in range(8, 12) and width > height:
|
|
raise InvalidDataError(f'CTrapezoid has width > height ({width} > {height})')
|
|
if ctrapezoid_type in range(12, 16) and 2 * width > height:
|
|
raise InvalidDataError(f'CTrapezoid has 2*width > height ({width} > 2 * {height})')
|
|
|
|
if ctrapezoid_type is not None and ctrapezoid_type not in range(0, 26): # noqa: PIE808
|
|
raise InvalidDataError(f'CTrapezoid has invalid type: {ctrapezoid_type}')
|
|
|
|
|
|
class Circle(Record, GeometryMixin):
|
|
"""
|
|
Circle record (ID 27)
|
|
"""
|
|
layer: int | None
|
|
datatype: int | None
|
|
x: int | None
|
|
y: int | None
|
|
repetition: repetition_t | None
|
|
radius: int | None
|
|
properties: list['Property']
|
|
|
|
def __init__(
|
|
self,
|
|
radius: int | None = None,
|
|
layer: int | None = None,
|
|
datatype: int | None = None,
|
|
x: int | None = None,
|
|
y: int | None = None,
|
|
repetition: repetition_t | None = None,
|
|
properties: list['Property'] | None = None,
|
|
) -> None:
|
|
"""
|
|
Args:
|
|
radius: Radius. Default `None` (reuse modal).
|
|
layer: Layer number. Default `None` (reuse modal).
|
|
datatype: Datatype number. Default `None` (reuse modal).
|
|
x: X-offset. Default `None` (use modal).
|
|
y: Y-offset. Default `None` (use modal).
|
|
repetition: Repetition. Default `None` (no repetition).
|
|
properties: List of property records associated with this record.
|
|
|
|
Raises:
|
|
InvalidDataError: if dimensions are invalid.
|
|
"""
|
|
self.radius = radius
|
|
self.layer = layer
|
|
self.datatype = datatype
|
|
self.x = x
|
|
self.y = y
|
|
self.repetition = repetition
|
|
self.properties = [] if properties is None else properties
|
|
|
|
def get_radius(self) -> int:
|
|
return verify_modal(self.radius)
|
|
|
|
def merge_with_modals(self, modals: Modals) -> None:
|
|
adjust_coordinates(self, modals, 'geometry_x', 'geometry_y')
|
|
adjust_repetition(self, modals)
|
|
adjust_field(self, 'layer', modals, 'layer')
|
|
adjust_field(self, 'datatype', modals, 'datatype')
|
|
adjust_field(self, 'radius', modals, 'circle_radius')
|
|
|
|
def deduplicate_with_modals(self, modals: Modals) -> None:
|
|
dedup_coordinates(self, modals, 'geometry_x', 'geometry_y')
|
|
dedup_repetition(self, modals)
|
|
dedup_field(self, 'layer', modals, 'layer')
|
|
dedup_field(self, 'datatype', modals, 'datatype')
|
|
dedup_field(self, 'radius', modals, 'circle_radius')
|
|
|
|
@staticmethod
|
|
def read(stream: IO[bytes], record_id: int) -> 'Circle':
|
|
if record_id != 27:
|
|
raise InvalidDataError(f'Invalid record id for Circle: {record_id}')
|
|
|
|
z0, z1, has_radius, xx, yy, rr, dd, ll = read_bool_byte(stream)
|
|
if z0 or z1:
|
|
raise InvalidDataError('Malformed circle header')
|
|
|
|
optional: dict[str, Any] = {}
|
|
if ll:
|
|
optional['layer'] = read_uint(stream)
|
|
if dd:
|
|
optional['datatype'] = read_uint(stream)
|
|
if has_radius:
|
|
optional['radius'] = read_uint(stream)
|
|
if xx:
|
|
optional['x'] = read_sint(stream)
|
|
if yy:
|
|
optional['y'] = read_sint(stream)
|
|
if rr:
|
|
optional['repetition'] = read_repetition(stream)
|
|
record = Circle(**optional)
|
|
logger.debug(f'Record ending at 0x{stream.tell():x}:\n {record}')
|
|
return record
|
|
|
|
def write(self, stream: IO[bytes]) -> int:
|
|
ss = self.radius is not None
|
|
xx = self.x is not None
|
|
yy = self.y is not None
|
|
rr = self.repetition is not None
|
|
dd = self.datatype is not None
|
|
ll = self.layer is not None
|
|
|
|
size = write_uint(stream, 27)
|
|
size += write_bool_byte(stream, (0, 0, ss, xx, yy, rr, dd, ll))
|
|
if ll:
|
|
size += write_uint(stream, self.layer) # type: ignore
|
|
if dd:
|
|
size += write_uint(stream, self.datatype) # type: ignore
|
|
if ss:
|
|
size += write_uint(stream, self.radius) # type: ignore
|
|
if xx:
|
|
size += write_sint(stream, self.x) # type: ignore
|
|
if yy:
|
|
size += write_sint(stream, self.y) # type: ignore
|
|
if rr:
|
|
size += self.repetition.write(stream) # type: ignore
|
|
return size
|
|
|
|
|
|
def adjust_repetition(record: HasRepetition, modals: Modals) -> None:
|
|
"""
|
|
Merge the record's repetition entry with the one in the modals
|
|
|
|
Args:
|
|
record: Record to read or modify.
|
|
modals: Modals to read or modify.
|
|
|
|
Raises:
|
|
InvalidDataError: if a `ReuseRepetition` can't be filled
|
|
from the modals.
|
|
"""
|
|
if record.repetition is not None:
|
|
if isinstance(record.repetition, ReuseRepetition):
|
|
if modals.repetition is None:
|
|
raise InvalidDataError('Unfillable repetition')
|
|
record.repetition = copy.copy(modals.repetition)
|
|
else:
|
|
modals.repetition = copy.copy(record.repetition)
|
|
|
|
|
|
def adjust_field(record: Record, r_field: str, modals: Modals, m_field: str) -> None:
|
|
"""
|
|
Merge `record.r_field` with `modals.m_field`
|
|
|
|
Args:
|
|
record: `Record` to read or modify.
|
|
r_field: Attr of record to access.
|
|
modals: `Modals` to read or modify.
|
|
m_field: Attr of modals to access.
|
|
|
|
Raises:
|
|
InvalidDataError: if both fields are `None`
|
|
"""
|
|
r = getattr(record, r_field)
|
|
if r is not None:
|
|
setattr(modals, m_field, r)
|
|
else:
|
|
m = getattr(modals, m_field)
|
|
if m is not None:
|
|
setattr(record, r_field, copy.copy(m))
|
|
else:
|
|
raise InvalidDataError(f'Unfillable field: {m_field}')
|
|
|
|
|
|
def adjust_coordinates(record: HasXY, modals: Modals, mx_field: str, my_field: str) -> None:
|
|
"""
|
|
Merge `record.x` and `record.y` with `modals.mx_field` and `modals.my_field`,
|
|
taking into account the value of `modals.xy_relative`.
|
|
|
|
If `modals.xy_relative` is `True` and the record has non-`None` coordinates,
|
|
the modal values are added to the record's coordinates. If `modals.xy_relative`
|
|
is `False`, the coordinates are treated the same way as other fields.
|
|
|
|
Args:
|
|
record: `Record` to read or modify.
|
|
modals: `Modals` to read or modify.
|
|
mx_field: Attr of modals corresponding to `record.x`
|
|
my_field: Attr of modals corresponding to `record.y`
|
|
|
|
Raises:
|
|
InvalidDataError: if both fields are `None`
|
|
"""
|
|
if record.x is not None:
|
|
if modals.xy_relative:
|
|
record.x += getattr(modals, mx_field)
|
|
setattr(modals, mx_field, record.x)
|
|
else:
|
|
record.x = getattr(modals, mx_field)
|
|
|
|
if record.y is not None:
|
|
if modals.xy_relative:
|
|
record.y += getattr(modals, my_field)
|
|
setattr(modals, my_field, record.y)
|
|
else:
|
|
record.y = getattr(modals, my_field)
|
|
|
|
|
|
# TODO: Clarify the docs on the dedup_* functions
|
|
def dedup_repetition(record: HasRepetition, modals: Modals) -> None:
|
|
"""
|
|
Deduplicate the record's repetition entry with the one in the modals.
|
|
Update the one in the modals if they are different.
|
|
|
|
Args:
|
|
record: `Record` to read or modify.
|
|
modals: `Modals` to read or modify.
|
|
|
|
Raises:
|
|
InvalidDataError: if a `ReuseRepetition` can't be filled
|
|
from the modals.
|
|
"""
|
|
if record.repetition is None:
|
|
return
|
|
|
|
if isinstance(record.repetition, ReuseRepetition):
|
|
if modals.repetition is None:
|
|
raise InvalidDataError('Unfillable repetition')
|
|
return
|
|
|
|
if record.repetition == modals.repetition:
|
|
record.repetition = ReuseRepetition()
|
|
else:
|
|
modals.repetition = record.repetition
|
|
|
|
|
|
def dedup_field(record: Record, r_field: str, modals: Modals, m_field: str) -> None:
|
|
"""
|
|
Deduplicate `record.r_field` using `modals.m_field`
|
|
Update the `modals.m_field` if they are different.
|
|
|
|
Args:
|
|
record: `Record` to read or modify.
|
|
r_field: Attr of record to access.
|
|
modals: `Modals` to read or modify.
|
|
m_field: Attr of modals to access.
|
|
|
|
Args:
|
|
InvalidDataError: if both fields are `None`
|
|
"""
|
|
rr = getattr(record, r_field)
|
|
mm = getattr(modals, m_field)
|
|
if rr is not None:
|
|
if m_field in ('polygon_point_list', 'path_point_list'):
|
|
if _USE_NUMPY:
|
|
equal = numpy.array_equal(mm, rr)
|
|
else:
|
|
equal = (mm is not None) and all(tuple(mmm) == tuple(rrr) for mmm, rrr in zip(mm, rr, strict=True))
|
|
else:
|
|
equal = (mm is not None) and mm == rr
|
|
|
|
if equal:
|
|
setattr(record, r_field, None)
|
|
else:
|
|
setattr(modals, m_field, rr)
|
|
elif mm is None:
|
|
raise InvalidDataError('Unfillable field')
|
|
|
|
|
|
def dedup_coordinates(record: HasXY, modals: Modals, mx_field: str, my_field: str) -> None:
|
|
"""
|
|
Deduplicate `record.x` and `record.y` using `modals.mx_field` and `modals.my_field`,
|
|
taking into account the value of `modals.xy_relative`.
|
|
|
|
If `modals.xy_relative` is `True` and the record has non-`None` coordinates,
|
|
the modal values are subtracted from the record's coordinates. If `modals.xy_relative`
|
|
is `False`, the coordinates are treated the same way as other fields.
|
|
|
|
Args:
|
|
record: `Record` to read or modify.
|
|
modals: `Modals` to read or modify.
|
|
mx_field: Attr of modals corresponding to `record.x`
|
|
my_field: Attr of modals corresponding to `record.y`
|
|
|
|
Raises:
|
|
InvalidDataError: if both fields are `None`
|
|
"""
|
|
if record.x is not None:
|
|
mx = getattr(modals, mx_field)
|
|
if modals.xy_relative:
|
|
record.x -= mx
|
|
setattr(modals, mx_field, record.x)
|
|
elif record.x == mx:
|
|
record.x = None
|
|
else:
|
|
setattr(modals, mx_field, record.x)
|
|
|
|
if record.y is not None:
|
|
my = getattr(modals, my_field)
|
|
if modals.xy_relative:
|
|
record.y -= my
|
|
setattr(modals, my_field, record.y)
|
|
elif record.y == my:
|
|
record.y = None
|
|
else:
|
|
setattr(modals, my_field, record.y)
|
|
|