def virtual_block_reference_entities( block_ref: 'Insert', skipped_entity_callback: Optional[Callable[['DXFGraphic', str], None]] = None ) -> Iterable['DXFGraphic']: """ Yields 'virtual' parts of block reference `block_ref`. This method is meant to examine the the block reference entities without the need to explode the block reference. The `skipped_entity_callback()` will be called for all entities which are not processed, signature: :code:`skipped_entity_callback(entity: DXFGraphic, reason: str)`, `entity` is the original (untransformed) DXF entity of the block definition, the `reason` string is an explanation why the entity was skipped. This entities are located at the 'exploded' positions, but are not stored in the entity database, have no handle and are not assigned to any layout. Args: block_ref: Block reference entity (INSERT) skipped_entity_callback: called whenever the transformation of an entity is not supported and so was skipped. .. warning:: **Non uniform scaling** may lead to incorrect results for text entities (TEXT, MTEXT, ATTRIB) and maybe some other entities. (internal API) """ assert block_ref.dxftype() == 'INSERT' Ellipse = cast('Ellipse', factory.cls('ELLIPSE')) skipped_entity_callback = skipped_entity_callback or default_logging_callback def disassemble(layout) -> Iterable['DXFGraphic']: for entity in layout: # Do not explode ATTDEF entities. Already available in Insert.attribs if entity.dxftype() == 'ATTDEF': continue try: copy = entity.copy() except DXFTypeError: skipped_entity_callback(entity, 'non copyable') else: if hasattr(copy, 'remove_association'): copy.remove_association() yield copy def transform(entities): for entity in entities: try: entity.transform(m) except NotImplementedError: skipped_entity_callback(entity, 'non transformable') except NonUniformScalingError: dxftype = entity.dxftype() if dxftype in {'ARC', 'CIRCLE'}: if not math.isclose(entity.dxf.radius, 0.0): # radius < 0 is ok. yield Ellipse.from_arc(entity).transform(m) else: skipped_entity_callback( entity, f'Invalid radius in entity {str(entity)}.') elif dxftype in {'LWPOLYLINE', 'POLYLINE'}: # has arcs yield from transform(entity.virtual_entities()) else: skipped_entity_callback(entity, 'unsupported non-uniform scaling') except InsertTransformationError: # INSERT entity can not represented in the target coordinate # system defined by transformation matrix `m`. # Yield transformed sub-entities of the INSERT entity: yield from transform( virtual_block_reference_entities(entity, skipped_entity_callback)) else: yield entity m = block_ref.matrix44() block_layout = block_ref.block() if block_layout is None: raise DXFStructureError( f'Required block definition for "{block_ref.dxf.name}" does not exist.' ) yield from transform(disassemble(block_layout))
def virtual_block_reference_entities( block_ref: 'Insert', uniform_scaling_factor: float = None, skipped_entity_callback: Optional[Callable[['DXFGraphic', str], None]] = None ) -> Iterable['DXFGraphic']: """ Yields 'virtual' parts of block reference `block_ref`. This method is meant to examine the the block reference entities without the need to explode the block reference. The `skipped_entity_callback()` will be called for all entities which are not processed, signature: :code:`skipped_entity_callback(entity: DXFEntity, reason: str)`, `entity` is the original (untransformed) DXF entity of the block definition, the `reason` string is an explanation why the entity was skipped. This entities are located at the 'exploded' positions, but are not stored in the entity database, have no handle and are not assigned to any layout. Args: block_ref: Block reference entity (INSERT) uniform_scaling_factor: override uniform scaling factor for text entities (TEXT, ATTRIB, MTEXT) and HATCH pattern, default is ``max(abs(xscale), abs(yscale), abs(zscale))`` skipped_entity_callback: called whenever the transformation of an entity is not supported and so was skipped. .. warning:: **Non uniform scaling** returns incorrect results for text entities (TEXT, MTEXT, ATTRIB) and some other entities like ELLIPSE, SHAPE, HATCH with arc or ellipse path segments and POLYLINE/LWPOLYLINE with arc segments. (internal API) """ assert block_ref.dxftype() == 'INSERT' Ellipse = cast('Ellipse', factory.cls('ELLIPSE')) if skipped_entity_callback is None: def skipped_entity_callback(entity, reason): logger.debug( f'(Virtual Block Reference Entities) Ignoring {str(entity)}: "{reason}"' ) def disassemble(layout) -> Generator['DXFGraphic', None, None]: for entity in layout: dxftype = entity.dxftype() if dxftype == 'ATTDEF': # do not explode ATTDEF entities. Already available in Insert.attribs continue if has_non_uniform_scaling: if dxftype in {'ARC', 'CIRCLE'}: # convert ARC to ELLIPSE yield Ellipse.from_arc(entity) continue if dxftype in {'LWPOLYLINE', 'POLYLINE'} and entity.has_arc: # disassemble (LW)POLYLINE into LINE and ARC segments for segment in entity.virtual_entities(): # convert ARC to ELLIPSE if segment.dxftype() == 'ARC': yield Ellipse.from_arc(segment) else: yield segment continue # Copy entity with all DXF attributes try: copy = entity.copy() except DXFTypeError: skipped_entity_callback(entity, 'non copyable') continue # non copyable entities will be ignored if copy.dxftype() == 'HATCH': if copy.dxf.associative: # remove associations copy.dxf.associative = 0 for path in copy.paths: path.source_boundary_objects = [] if has_non_uniform_scaling and copy.paths.has_critical_elements( ): # None uniform scaling produces incorrect results for the arc and ellipse transformations. # This causes an DXF structure error for AutoCAD. # todo: requires testing skipped_entity_callback(entity, 'unsupported non-uniform scaling') continue # For the case that arc and ellipse transformation works correct someday: # copy.paths.arc_edges_to_ellipse_edges() yield copy brcs = block_ref.brcs() block_layout = block_ref.block() if block_layout is None: raise DXFStructureError( f'Required block definition for "{block_ref.dxf.name}" does not exist.' ) has_scaling = block_ref.has_scaling if has_scaling: # Non uniform scaling will produce incorrect results for some entities! # Mirroring about an axis is handled like non uniform scaling! (-1, 1, 1) # (-1, -1, -1) is uniform scaling! has_non_uniform_scaling = not block_ref.has_uniform_scaling xscale = block_ref.dxf.xscale yscale = block_ref.dxf.yscale zscale = block_ref.dxf.zscale if block_ref.has_uniform_scaling and xscale < 0: # handle reflection about all three axis -x, -y, -z explicit as non uniform scaling has_non_uniform_scaling = True if uniform_scaling_factor is not None: uniform_scaling_factor = float(uniform_scaling_factor) else: uniform_scaling_factor = block_ref.text_scaling else: xscale, yscale, zscale = (1, 1, 1) uniform_scaling_factor = 1 has_non_uniform_scaling = False for entity in disassemble(block_layout): dxftype = entity.dxftype() original_ellipse: Optional[Ellipse] = None if has_non_uniform_scaling and dxftype == 'ELLIPSE': original_ellipse = entity.copy() # Basic transformation from BRCS to WCS try: entity.transform_to_wcs(brcs) except NotImplementedError: # entities without 'transform_to_wcs' support will be ignored skipped_entity_callback(entity, 'non transformable') continue if has_scaling: # Apply DXF attribute scaling: # Simple entities without properties to scale if dxftype in { 'LINE', 'POINT', 'LWPOLYLINE', 'POLYLINE', 'MESH', 'SPLINE', 'SOLID', '3DFACE', 'TRACE', 'IMAGE', 'WIPEOUT', 'XLINE', 'RAY', 'LIGHT', 'HELIX' }: pass # nothing else to do elif dxftype in {'CIRCLE', 'ARC'}: # Non uniform scaling: ARC and CIRCLE converted to ELLIPSE # TODO: since non uniform scale => ellipse, scaling by (-s, -s, -s) is the only possible reflection here # TODO: handle reflections about z entity.dxf.radius = entity.dxf.radius * uniform_scaling_factor elif dxftype == 'ELLIPSE': # TODO: handle reflections about z if has_non_uniform_scaling: open_ellipse = not math.isclose( normalize_angle(original_ellipse.dxf.start_param), normalize_angle(original_ellipse.dxf.end_param), ) minor_axis = brcs.direction_to_wcs( original_ellipse.minor_axis) ellipse = cast('Ellipse', entity) # Transform axis major_axis = ellipse.dxf.major_axis if not math.isclose( major_axis.dot(minor_axis), 0, abs_tol=1e-9): try: major_axis, _, ratio = rytz_axis_construction( major_axis, minor_axis) except ArithmeticError: # axis construction error - skip entity skipped_entity_callback( entity, 'axis construction error - please send a bug report.' ) continue else: ratio = minor_axis.magnitude / major_axis.magnitude ellipse.dxf.major_axis = major_axis # AutoCAD does not accept a ratio < 1e-6 -> invalid DXF file ellipse.dxf.ratio = max(ratio, 1e-6) if open_ellipse: original_start_param = original_ellipse.dxf.start_param original_end_param = original_ellipse.dxf.end_param start_point, end_point = brcs.points_to_wcs( original_ellipse.vertices( (original_start_param, original_end_param))) # adjusting start- and end parameter center = ellipse.dxf.center # transformed center point extrusion = ellipse.dxf.extrusion # transformed extrusion vector, default is (0, 0, 1) start_angle = extrusion.angle_about( major_axis, start_point - center) end_angle = extrusion.angle_about( major_axis, end_point - center) start_param = angle_to_param(ratio, start_angle) end_param = angle_to_param(ratio, end_angle) # if drawing the wrong side of the ellipse if (start_param > end_param) != (original_start_param > original_end_param): start_param, end_param = end_param, start_param ellipse.dxf.start_param = start_param ellipse.dxf.end_param = end_param if ellipse.dxf.ratio > 1: ellipse.swap_axis() elif dxftype == 'MTEXT': # TODO: handle reflections. Note that the entity does store enough information to represent being # reflected. This can be seen by reflecting then exploding in Autocad. # The text will no longer be reflected. # Scale MTEXT height/width just by uniform_scaling. entity.dxf.char_height *= uniform_scaling_factor entity.dxf.width *= uniform_scaling_factor elif dxftype in {'TEXT', 'ATTRIB'}: # TODO: handle reflections. Note that the entity does store enough information to represent being # reflected. This can be seen by reflecting then exploding in Autocad. # The text will no longer be reflected. # Scale TEXT height just by uniform_scaling. entity.dxf.height *= uniform_scaling_factor elif dxftype == 'INSERT': # Set scaling of child INSERT to scaling of parent INSERT entity.dxf.xscale *= xscale entity.dxf.yscale *= yscale entity.dxf.zscale *= zscale # Scale attached ATTRIB entities: for attrib in entity.attribs: attrib.dxf.height *= uniform_scaling_factor elif dxftype == 'SHAPE': # Scale SHAPE size just by uniform_scaling. entity.dxf.size *= uniform_scaling_factor elif dxftype == 'HATCH': # Non uniform scaling produces incorrect results for boundary paths containing ARC or ELLIPSE segments. # Scale HATCH pattern: hatch = cast('Hatch', entity) if uniform_scaling_factor != 1 and hatch.has_pattern_fill and hatch.pattern is not None: hatch.dxf.pattern_scale *= uniform_scaling_factor # hatch.pattern is already scaled by the stored pattern_scale value hatch.set_pattern_definition(hatch.pattern.as_list(), uniform_scaling_factor) else: # unsupported entity will be ignored skipped_entity_callback(entity, 'unsupported entity') continue yield entity
def virtual_block_reference_entities( block_ref: 'Insert', uniform_scaling_factor: float = None) -> Iterable['DXFGraphic']: """ Yields 'virtual' parts of block reference `block_ref`. This method is meant to examine the the block reference entities without the need to explode the block reference. This entities are located at the 'exploded' positions, but are not stored in the entity database, have no handle and are not assigned to any layout. Args: block_ref: Block reference entity (INSERT) uniform_scaling_factor: override uniform scaling factor for text entities (TEXT, ATTRIB, MTEXT) and HATCH pattern, default is ``max(abs(xscale), abs(yscale), abs(zscale))`` .. warning:: **Non uniform scaling** returns incorrect results for text entities (TEXT, MTEXT, ATTRIB) and some other entities like ELLIPSE, SHAPE, HATCH with arc or ellipse path segments and POLYLINE/LWPOLYLINE with arc segments. (internal API) """ assert block_ref.dxftype() == 'INSERT' Ellipse = cast('Ellipse', factory.cls('ELLIPSE')) def disassemble(layout): for entity in layout: dxftype = entity.dxftype() if dxftype == 'ATTDEF': # do not explode ATTDEF entities continue if has_non_uniform_scaling: if dxftype in {'ARC', 'CIRCLE'}: # convert ARC to ELLIPSE yield Ellipse.from_arc(entity) continue if dxftype in {'LWPOLYLINE', 'POLYLINE'} and entity.has_arc: # disassemble (LW)POLYLINE into LINE and ARC segments for segment in entity.virtual_entities(): # convert ARC to ELLIPSE if segment.dxftype() == 'ARC': yield Ellipse.from_arc(segment) else: yield segment continue # Copy entity with all DXF attributes try: copy = entity.copy() except DXFTypeError: logger.debug( f'(Virtual Block Reference Entities) Ignoring non copyable entity {str(entity)}' ) continue # non copyable entities will be ignored if copy.dxftype() == 'HATCH': if copy.dxf.associative: # remove associations copy.dxf.associative = 0 for path in copy.paths: path.source_boundary_objects = [] if has_non_uniform_scaling and copy.paths.has_critical_elements( ): # None uniform scaling produces incorrect results for the arc and ellipse transformations. # This causes an DXF structure error for AutoCAD. # todo: requires testing logger.debug( f'(Virtual Block Reference Entities) Ignoring {str(entity)} for non uniform scaling.' ) continue # For the case that arc and ellipse transformation works correct someday: # copy.paths.arc_edges_to_ellipse_edges() yield copy brcs = block_ref.brcs() block_layout = block_ref.block() if block_layout is None: raise DXFStructureError( f'Required block definition for "{block_ref.dxf.name}" does not exist.' ) has_scaling = block_ref.has_scaling if has_scaling: xscale = block_ref.dxf.xscale yscale = block_ref.dxf.yscale zscale = block_ref.dxf.zscale if uniform_scaling_factor is not None: uniform_scaling_factor = float(uniform_scaling_factor) else: uniform_scaling_factor = block_ref.text_scaling # Non uniform scaling will produce incorrect results for some entities! if xscale == yscale == zscale: has_non_uniform_scaling = False if xscale == 1: # yscale == 1, zscale == 1 has_scaling = False else: has_non_uniform_scaling = True else: xscale, yscale, zscale = (1, 1, 1) uniform_scaling_factor = 1 has_non_uniform_scaling = False for entity in disassemble(block_layout): dxftype = entity.dxftype() if has_non_uniform_scaling and dxftype == 'ELLIPSE': # transform start- and end location before main transformation ellipse = cast('Ellipse', entity) open_ellipse = not math.isclose( normalize_angle(ellipse.dxf.start_param), normalize_angle(ellipse.dxf.end_param), ) if open_ellipse: # transformed start- and end point start_param = ellipse.dxf.start_param end_param = ellipse.dxf.end_param start_point, end_point = brcs.points_to_wcs( ellipse.vertices((start_param, end_param))) minor_axis = brcs.direction_to_wcs(ellipse.minor_axis) # Basic transformation from BRCS to WCS try: entity.transform_to_wcs(brcs) except NotImplementedError: # entities without 'transform_to_wcs' support will be ignored logger.debug( f'(Virtual Block Reference Entities) Ignoring non transformable entity {str(entity)}' ) continue if has_scaling: # Apply DXF attribute scaling: # Simple entities without properties to scale if dxftype in { 'LINE', 'POINT', 'LWPOLYLINE', 'POLYLINE', 'MESH', 'SPLINE', 'SOLID', '3DFACE', 'TRACE', 'IMAGE', 'WIPEOUT', 'XLINE', 'RAY', 'LIGHT', 'HELIX' }: pass # nothing else to do elif dxftype in {'CIRCLE', 'ARC'}: # Non uniform scaling: ARC and CIRCLE converted to ELLIPSE entity.dxf.radius = entity.dxf.radius * uniform_scaling_factor elif dxftype == 'ELLIPSE' and has_non_uniform_scaling: ellipse = cast('Ellipse', entity) # Transform axis major_axis = ellipse.dxf.major_axis if not math.isclose(major_axis.dot(minor_axis), 0): major_axis, _, ratio = rytz_axis_construction( major_axis, minor_axis) else: ratio = minor_axis.magnitude / major_axis.magnitude ellipse.dxf.major_axis = major_axis ellipse.dxf.ratio = max(ratio, 1e-6) if open_ellipse: # adjusting start- and end parameter center = ellipse.dxf.center # transformed center point start_angle = major_axis.angle_between(start_point - center) end_angle = major_axis.angle_between(end_point - center) # todo: quadrant detection may fail if the rytz's axis construction algorithm is applied ellipse.dxf.start_param = angle_to_param( ratio, start_angle, quadrant(start_param)) ellipse.dxf.end_param = angle_to_param( ratio, end_angle, quadrant(end_param)) if ellipse.dxf.ratio > 1: ellipse.swap_axis() elif dxftype == 'MTEXT': # Scale MTEXT height/width just by uniform_scaling. entity.dxf.char_height *= uniform_scaling_factor entity.dxf.width *= uniform_scaling_factor elif dxftype in {'TEXT', 'ATTRIB'}: # Scale TEXT height just by uniform_scaling. entity.dxf.height *= uniform_scaling_factor elif dxftype == 'INSERT': # Set scaling of child INSERT to scaling of parent INSERT entity.dxf.xscale *= xscale entity.dxf.yscale *= yscale entity.dxf.zscale *= zscale # Scale attached ATTRIB entities: for attrib in entity.attribs: attrib.dxf.height *= uniform_scaling_factor elif dxftype == 'SHAPE': # Scale SHAPE size just by uniform_scaling. entity.dxf.size *= uniform_scaling_factor elif dxftype == 'HATCH': # Non uniform scaling produces incorrect results for boundary paths containing ARC or ELLIPSE segments. # Scale HATCH pattern: hatch = cast('Hatch', entity) if uniform_scaling_factor != 1 and hatch.has_pattern_fill and hatch.pattern is not None: hatch.dxf.pattern_scale *= uniform_scaling_factor # hatch.pattern is already scaled by the stored pattern_scale value hatch.set_pattern_definition(hatch.pattern.as_list(), uniform_scaling_factor) else: # unsupported entity will be ignored continue yield entity