From 3627b6365858dd15904c49dba1ba76594c46aad3 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 10 Sep 2020 20:03:01 -0700 Subject: [PATCH] Make records store their associated Property records. Previously, `Property` records were (incorrectly) just associated with the library or cell. This requires a change to how `CellName`s are handled by `OasisLayout`, since they can have associated properties. They now have their own non-record object (like `XName`s did before) which holds the properties. There is also now a `FileModals.property_target` attribute which tracks which record new `Property` rescords should be associated with. --- fatamorgana/main.py | 107 +++++++++++++++++++++++++++++++---------- fatamorgana/records.py | 66 +++++++++++++++++++++---- 2 files changed, 137 insertions(+), 36 deletions(-) diff --git a/fatamorgana/main.py b/fatamorgana/main.py index 56e4e41..4659fd1 100644 --- a/fatamorgana/main.py +++ b/fatamorgana/main.py @@ -30,11 +30,16 @@ class FileModals: textstring_implicit: Optional[bool] = None propstring_implicit: Optional[bool] = None + property_target: List[records.Property] + within_cell: bool = False within_cblock: bool = False - end_has_offset_table: bool + end_has_offset_table: bool = False started: bool = False + def __init__(self, property_target = List[records.Property]): + self.property_target = property_target + class OasisLayout: """ @@ -53,7 +58,7 @@ class OasisLayout: validation (Validation): checksum data (Names) - cellnames (Dict[int, NString]): Cell names + cellnames (Dict[int, CellName]): Cell names propnames (Dict[int, NString]): Property names xnames (Dict[int, XName]): Custom names @@ -73,7 +78,7 @@ class OasisLayout: properties: List[records.Property] cells: List['Cell'] - cellnames: Dict[int, NString] + cellnames: Dict[int, 'CellName'] propnames: Dict[int, NString] xnames: Dict[int, 'XName'] @@ -115,9 +120,9 @@ class OasisLayout: Returns: New `OasisLayout` object. """ - file_state = FileModals() - modals = Modals() layout = OasisLayout(unit=-1) # dummy unit + modals = Modals() + file_state = FileModals(layout.properties) read_magic_bytes(stream) @@ -225,7 +230,10 @@ class OasisLayout: key = record.reference_number if key is None: key = len(self.cellnames) - self.cellnames[key] = record.nstring + + cellname = CellName.from_record(record) + self.cellnames[key] = cellname + file_state.property_target = cellname.properties elif record_id in (5, 6): implicit = record_id == 5 if file_state.textstring_implicit is None: @@ -272,10 +280,7 @@ class OasisLayout: elif record_id in (28, 29): record = records.Property.read(stream, record_id) record.merge_with_modals(modals) - if not file_state.within_cell: - self.properties.append(record) - else: - self.cells[-1].properties.append(record) + file_state.property_target.append(record) elif record_id in (30, 31): implicit = record_id == 30 if file_state.xname_implicit is None: @@ -289,6 +294,7 @@ class OasisLayout: if key is None: key = len(self.xnames) self.xnames[key] = XName.from_record(record) + # TODO: do anything with property target? # # Cell and elements @@ -296,7 +302,9 @@ class OasisLayout: elif record_id in (13, 14): record = records.Cell.read(stream, record_id) record.merge_with_modals(modals) - self.cells.append(Cell(record.name)) + cell = Cell(record.name) + self.cells.append(cell) + file_state.property_target = cell.properties elif record_id in (15, 16): record = records.XYMode.read(stream, record_id) record.merge_with_modals(modals) @@ -304,10 +312,12 @@ class OasisLayout: record = records.Placement.read(stream, record_id) record.merge_with_modals(modals) self.cells[-1].placements.append(record) + file_state.property_target = record.properties elif record_id in _GEOMETRY: record = _GEOMETRY[record_id].read(stream, record_id) record.merge_with_modals(modals) self.cells[-1].geometry.append(record) + file_state.property_target = record.properties else: raise InvalidRecordError('Unknown record id: {}'.format(record_id)) return False @@ -330,10 +340,12 @@ class OasisLayout: size = 0 size += write_magic_bytes(stream) size += records.Start(self.unit, self.version).dedup_write(stream, modals) + size += sum(p.dedup_write(stream, modals) for p in self.properties) cellnames_offset = OffsetEntry(False, size) - size += sum(records.CellName(name, refnum).dedup_write(stream, modals) - for refnum, name in self.cellnames.items()) + for refnum, cn in self.cellnames.items(): + size += records.CellName(cn.nstring, refnum).dedup_write(stream, modals) + size += sum(p.dedup_write(stream, modals) for p in cn.properties) propnames_offset = OffsetEntry(False, size) size += sum(records.PropName(name, refnum).dedup_write(stream, modals) @@ -354,8 +366,6 @@ class OasisLayout: layernames_offset = OffsetEntry(False, size) size += sum(r.dedup_write(stream, modals) for r in self.layers) - size += sum(p.dedup_write(stream, modals) for p in self.properties) - size += sum(c.dedup_write(stream, modals) for c in self.cells) offset_table = OffsetTable( @@ -386,15 +396,17 @@ class Cell: placements: List[records.Placement] geometry: List[records.geometry_t] - def __init__(self, name: Union[NString, str, int]): - """ - Args: - name: `NString` or "CellName reference" number - """ + def __init__(self, + name: Union[NString, str, int], + *, + properties: Optional[List[records.Property]] = None, + placements: Optional[List[records.Placement]] = None, + geometry: Optional[List[records.geometry_t]] = None, + ): self.name = name if isinstance(name, (NString, int)) else NString(name) - self.properties = [] - self.placements = [] - self.geometry = [] + self.properties = [] if properties is None else properties + self.placements = [] if placements is None else placements + self.geometry = [] if geometry is None else geometry def dedup_write(self, stream: io.BufferedIOBase, modals: Modals) -> int: """ @@ -413,11 +425,54 @@ class Cell: """ size = records.Cell(self.name).dedup_write(stream, modals) size += sum(p.dedup_write(stream, modals) for p in self.properties) - size += sum(p.dedup_write(stream, modals) for p in self.placements) - size += sum(g.dedup_write(stream, modals) for g in self.geometry) + for placement in self.placements: + size += placement.dedup_write(stream, modals) + size += sum(p.dedup_write(stream, modals) for p in placement.properties) + for shape in self.geometry: + size += shape.dedup_write(stream, modals) + size += sum(p.dedup_write(stream, modals) for p in shape.properties) return size +class CellName: + """ + Representation of a CellName. + + This class is effectively a simplified form of a `records.CellName`, + with the reference data stripped out. + """ + nstring: NString + properties: List[records.Property] + + def __init__(self, + nstring: Union[NString, str], + properties: Optional[List[records.Property]] = None): + """ + Args: + nstring: The contained string. + properties: Properties which apply to this CellName's cell, but + are placed following the CellName record. + """ + if isinstance(nstring, NString): + self.nstring = nstring + else: + self.nstring = NString(nstring) + self.properties = [] if properties is None else properties + + @staticmethod + def from_record(record: records.CellName) -> 'CellName': + """ + Create an `CellName` object from a `records.CellName` record. + + Args: + record: CellName record to use. + + Returns: + A new `CellName` object. + """ + return CellName(record.nstring) + + class XName: """ Representation of an XName. @@ -446,7 +501,7 @@ class XName: record: XName record to use. Returns: - `XName` object. + a new `XName` object. """ return XName(record.attribute, record.bstring) diff --git a/fatamorgana/records.py b/fatamorgana/records.py index e9c30cb..a261f60 100644 --- a/fatamorgana/records.py +++ b/fatamorgana/records.py @@ -1101,15 +1101,21 @@ class XElement(Record): """ attribute: int bstring: bytes + properties: List['Property'] - def __init__(self, attribute: int, bstring: bytes): + def __init__(self, + attribute: int, + bstring: bytes, + properties: Optional[List['Property']] = 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): pass @@ -1147,6 +1153,7 @@ class XGeometry(Record, GeometryMixin): x (Optional[int]): None means reuse modal y (Optional[int]): None means reuse modal repetition (Optional[repetition_t]): Repetition, if any + properties (List[Property]): List of property records associate with this record. """ attribute: int bstring: bytes @@ -1155,6 +1162,7 @@ class XGeometry(Record, GeometryMixin): x: Optional[int] = None y: Optional[int] = None repetition: Optional[repetition_t] = None + properties: List['Property'] def __init__(self, attribute: int, @@ -1163,7 +1171,8 @@ class XGeometry(Record, GeometryMixin): datatype: Optional[int] = None, x: Optional[int] = None, y: Optional[int] = None, - repetition: Optional[repetition_t] = None): + repetition: Optional[repetition_t] = None, + properties: Optional[List['Property']] = None): """ Args: attribute: Attribute number for this XGeometry. @@ -1173,6 +1182,7 @@ class XGeometry(Record, GeometryMixin): 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 @@ -1181,6 +1191,7 @@ class XGeometry(Record, GeometryMixin): self.x = x self.y = y self.repetition = repetition + self.properties = [] if properties is None else properties def merge_with_modals(self, modals: Modals): adjust_coordinates(self, modals, 'geometry_x', 'geometry_y') @@ -1305,6 +1316,7 @@ class Placement(Record): y (Optional[int]): y-offset, None means reuse modal repetition (repetition_t or None): Repetition, if any flip (bool): Whether to perform reflection about the x-axis. + properties (List[Property]): List of property records associate with this record. """ name: Union[NString, int, None] = None magnification: Optional[real_t] = None @@ -1313,6 +1325,7 @@ class Placement(Record): y: Optional[int] = None repetition: Optional[repetition_t] = None flip: bool + properties: List['Property'] def __init__(self, flip: bool, @@ -1321,7 +1334,8 @@ class Placement(Record): angle: Optional[real_t] = None, x: Optional[int] = None, y: Optional[int] = None, - repetition: Optional[repetition_t] = None): + repetition: Optional[repetition_t] = None, + properties: Optional[List['Property']] = None): """ Args: flip: Whether to perform reflection about the x-axis. @@ -1333,6 +1347,7 @@ class Placement(Record): 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 @@ -1344,6 +1359,7 @@ class Placement(Record): self.name = name else: self.name = NString(name) + self.properties = [] if properties is None else properties def get_name(self) -> Union[NString, int]: return verify_modal(self.name) # type: ignore @@ -1448,6 +1464,7 @@ class Text(Record, GeometryMixin): x (Optional[int]): x-offset, None means reuse modal y (Optional[int]): y-offset, None means reuse modal repetition (Optional[repetition_t]): Repetition, if any + properties (List[Property]): List of property records associate with this record. """ string: Optional[Union[AString, int]] = None layer: Optional[int] = None @@ -1455,6 +1472,7 @@ class Text(Record, GeometryMixin): x: Optional[int] = None y: Optional[int] = None repetition: Optional[repetition_t] = None + properties: List['Property'] def __init__(self, string: Union[AString, str, int, None] = None, @@ -1462,7 +1480,8 @@ class Text(Record, GeometryMixin): datatype: Optional[int] = None, x: Optional[int] = None, y: Optional[int] = None, - repetition: Optional[repetition_t] = None): + repetition: Optional[repetition_t] = None, + properties: Optional[List['Property']] = None): """ Args: string: Text content, or `TextString` reference number. @@ -1472,6 +1491,7 @@ class Text(Record, GeometryMixin): 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 @@ -1482,6 +1502,7 @@ class Text(Record, GeometryMixin): self.string = string else: self.string = AString(string) + self.properties = [] if properties is None else properties def get_string(self) -> Union[AString, int]: return verify_modal(self.string) # type: ignore @@ -1575,6 +1596,7 @@ class Rectangle(Record, GeometryMixin): y (Optional[int]): y-offset of the rectangle's lower-left (min-y) point. None means reuse modal repetition (Optional[repetition_t]): Repetition, if any. + properties (List[Property]): List of property records associate with this record. """ layer: Optional[int] = None datatype: Optional[int] = None @@ -1584,6 +1606,7 @@ class Rectangle(Record, GeometryMixin): y: Optional[int] = None repetition: Optional[repetition_t] = None is_square: bool = False + properties: List['Property'] def __init__(self, is_square: bool = False, @@ -1593,7 +1616,8 @@ class Rectangle(Record, GeometryMixin): height: Optional[int] = None, x: Optional[int] = None, y: Optional[int] = None, - repetition: Optional[repetition_t] = None): + repetition: Optional[repetition_t] = None, + properties: Optional[List['Property']] = None): self.is_square = is_square self.layer = layer self.datatype = datatype @@ -1604,6 +1628,7 @@ class Rectangle(Record, GeometryMixin): 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) @@ -1710,6 +1735,7 @@ class Polygon(Record, GeometryMixin): None means reuse modal repetition (Optional[repetition_t]): Repetition, if any. Default no repetition. + properties (List[Property]): List of property records associate with this record. """ layer: Optional[int] = None datatype: Optional[int] = None @@ -1717,6 +1743,7 @@ class Polygon(Record, GeometryMixin): y: Optional[int] = None repetition: Optional[repetition_t] = None point_list: Optional[point_list_t] = None + properties: List['Property'] def __init__(self, point_list: Optional[point_list_t] = None, @@ -1724,13 +1751,15 @@ class Polygon(Record, GeometryMixin): datatype: Optional[int] = None, x: Optional[int] = None, y: Optional[int] = None, - repetition: Optional[repetition_t] = None): + repetition: Optional[repetition_t] = None, + properties: Optional[List['Property']] = 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: if len(point_list) < 3: @@ -1829,6 +1858,7 @@ class Path(Record, GeometryMixin): x (Optional[int]): x-offset, None means reuse modal y (Optional[int]): y-offset, None means reuse modal repetition (Optional[repetition_t]): Repetition, if any + properties (List[Property]): List of property records associate with this record. """ layer: Optional[int] = None datatype: Optional[int] = None @@ -1839,6 +1869,7 @@ class Path(Record, GeometryMixin): half_width: Optional[int] = None extension_start: Optional[pathextension_t] = None extension_end: Optional[pathextension_t] = None + properties: List['Property'] def __init__(self, point_list: Optional[point_list_t] = None, @@ -1849,7 +1880,8 @@ class Path(Record, GeometryMixin): datatype: Optional[int] = None, x: Optional[int] = None, y: Optional[int] = None, - repetition: Optional[repetition_t] = None): + repetition: Optional[repetition_t] = None, + properties: Optional[List['Property']] = None): self.layer = layer self.datatype = datatype self.x = x @@ -1859,6 +1891,7 @@ class Path(Record, GeometryMixin): 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) @@ -2006,6 +2039,7 @@ class Trapezoid(Record, GeometryMixin): y (Optional[int]): y-offset to lower-left corner of the trapezoid's bounding box. None means reuse modal repetition (Optional[repetition_t]): Repetition, if any + properties (List[Property]): List of property records associate with this record. """ layer: Optional[int] = None datatype: Optional[int] = None @@ -2017,6 +2051,7 @@ class Trapezoid(Record, GeometryMixin): delta_a: int = 0 delta_b: int = 0 is_vertical: bool + properties: List['Property'] def __init__(self, is_vertical: bool, @@ -2028,7 +2063,8 @@ class Trapezoid(Record, GeometryMixin): height: int = None, x: int = None, y: int = None, - repetition: repetition_t = None): + repetition: repetition_t = None, + properties: Optional[List['Property']] = None): """ Raises: InvalidDataError: if dimensions are impossible. @@ -2043,6 +2079,7 @@ class Trapezoid(Record, GeometryMixin): 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: @@ -2206,6 +2243,7 @@ class CTrapezoid(Record, GeometryMixin): y (Optional[int]): y-offset of lower-left (min-y) point of bounding box. None means reuse modal repetition (Optional[repetition_t]): Repetition, if any + properties (List[Property]): List of property records associate with this record. """ ctrapezoid_type: Optional[int] = None layer: Optional[int] = None @@ -2215,6 +2253,7 @@ class CTrapezoid(Record, GeometryMixin): x: Optional[int] = None y: Optional[int] = None repetition: Optional[repetition_t] = None + properties: List['Property'] def __init__(self, ctrapezoid_type: int = None, @@ -2224,7 +2263,8 @@ class CTrapezoid(Record, GeometryMixin): height: int = None, x: int = None, y: int = None, - repetition: repetition_t = None): + repetition: repetition_t = None, + properties: Optional[List['Property']] = None): """ Raises: InvalidDataError: if dimensions are invalid. @@ -2237,6 +2277,7 @@ class CTrapezoid(Record, GeometryMixin): self.x = x self.y = y self.repetition = repetition + self.properties = [] if properties is None else properties self.check_valid() @@ -2405,6 +2446,7 @@ class Circle(Record, GeometryMixin): x (Optional[int]): x-offset, None means reuse modal y (Optional[int]): y-offset, None means reuse modal repetition (Optional[repetition_t]): Repetition, if any + properties (List[Property]): List of property records associate with this record. """ layer: Optional[int] = None datatype: Optional[int] = None @@ -2412,6 +2454,7 @@ class Circle(Record, GeometryMixin): y: Optional[int] = None repetition: Optional[repetition_t] = None radius: Optional[int] = None + properties: List['Property'] def __init__(self, radius: int = None, @@ -2419,7 +2462,8 @@ class Circle(Record, GeometryMixin): datatype: int = None, x: int = None, y: int = None, - repetition: repetition_t = None): + repetition: repetition_t = None, + properties: Optional[List['Property']] = None): """ Args: radius: Radius. Default `None` (reuse modal). @@ -2428,6 +2472,7 @@ class Circle(Record, GeometryMixin): 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. @@ -2438,6 +2483,7 @@ class Circle(Record, GeometryMixin): 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)