577 lines
19 KiB
Python
577 lines
19 KiB
Python
# Copyright (c) 2021-2022, Manfred Moitzi
|
|
# License: MIT License
|
|
# Debugging tools to analyze DXF entities.
|
|
from __future__ import annotations
|
|
from typing import Iterable, Sequence, Optional
|
|
import textwrap
|
|
|
|
import ezdxf
|
|
from ezdxf import colors
|
|
from ezdxf.math import Vec2
|
|
from ezdxf.lldxf import const
|
|
from ezdxf.enums import TextEntityAlignment
|
|
from ezdxf.tools.debug import print_bitmask
|
|
from ezdxf.render.mleader import MLeaderStyleOverride, OVERRIDE_FLAG
|
|
from ezdxf.entities import (
|
|
EdgePath,
|
|
PolylinePath,
|
|
LineEdge,
|
|
ArcEdge,
|
|
EllipseEdge,
|
|
SplineEdge,
|
|
mleader,
|
|
)
|
|
from ezdxf.entities.polygon import DXFPolygon
|
|
|
|
EDGE_START_MARKER = "EDGE_START_MARKER"
|
|
EDGE_END_MARKER = "EDGE_END_MARKER"
|
|
HATCH_LAYER = "HATCH"
|
|
POLYLINE_LAYER = "POLYLINE_MARKER"
|
|
LINE_LAYER = "LINE_MARKER"
|
|
ARC_LAYER = "ARC_MARKER"
|
|
ELLIPSE_LAYER = "ELLIPSE_MARKER"
|
|
SPLINE_LAYER = "SPLINE_MARKER"
|
|
|
|
|
|
class HatchAnalyzer:
|
|
def __init__(
|
|
self,
|
|
*,
|
|
marker_size: float = 1.0,
|
|
angle: float = 45,
|
|
):
|
|
self.marker_size = marker_size
|
|
self.angle = angle
|
|
self.doc = ezdxf.new()
|
|
self.msp = self.doc.modelspace()
|
|
self.init_layers()
|
|
self.init_markers()
|
|
|
|
def init_layers(self):
|
|
self.doc.layers.add(POLYLINE_LAYER, color=colors.YELLOW)
|
|
self.doc.layers.add(LINE_LAYER, color=colors.RED)
|
|
self.doc.layers.add(ARC_LAYER, color=colors.GREEN)
|
|
self.doc.layers.add(ELLIPSE_LAYER, color=colors.MAGENTA)
|
|
self.doc.layers.add(SPLINE_LAYER, color=colors.CYAN)
|
|
self.doc.layers.add(HATCH_LAYER)
|
|
|
|
def init_markers(self):
|
|
blk = self.doc.blocks.new(EDGE_START_MARKER)
|
|
attribs = {"layer": "0"} # from INSERT
|
|
radius = self.marker_size / 2.0
|
|
height = radius
|
|
|
|
# start marker: 0-- name
|
|
blk.add_circle(
|
|
center=(0, 0),
|
|
radius=radius,
|
|
dxfattribs=attribs,
|
|
)
|
|
text_start = radius * 4
|
|
blk.add_line(
|
|
start=(radius, 0),
|
|
end=(text_start - radius / 2.0, 0),
|
|
dxfattribs=attribs,
|
|
)
|
|
text = blk.add_attdef(
|
|
tag="NAME",
|
|
dxfattribs=attribs,
|
|
)
|
|
text.dxf.height = height
|
|
text.set_placement(
|
|
(text_start, 0), align=TextEntityAlignment.MIDDLE_LEFT
|
|
)
|
|
|
|
# end marker: name --X
|
|
blk = self.doc.blocks.new(EDGE_END_MARKER)
|
|
attribs = {"layer": "0"} # from INSERT
|
|
blk.add_line(
|
|
start=(-radius, -radius),
|
|
end=(radius, radius),
|
|
dxfattribs=attribs,
|
|
)
|
|
blk.add_line(
|
|
start=(-radius, radius),
|
|
end=(radius, -radius),
|
|
dxfattribs=attribs,
|
|
)
|
|
text_start = -radius * 4
|
|
blk.add_line(
|
|
start=(-radius, 0),
|
|
end=(text_start + radius / 2.0, 0),
|
|
dxfattribs=attribs,
|
|
)
|
|
text = blk.add_attdef(
|
|
tag="NAME",
|
|
dxfattribs=attribs,
|
|
)
|
|
text.dxf.height = height
|
|
text.set_placement(
|
|
(text_start, 0), align=TextEntityAlignment.MIDDLE_RIGHT
|
|
)
|
|
|
|
def export(self, name: str) -> None:
|
|
self.doc.saveas(name)
|
|
|
|
def add_hatch(self, hatch: DXFPolygon) -> None:
|
|
hatch.dxf.discard("extrusion")
|
|
hatch.dxf.layer = HATCH_LAYER
|
|
self.msp.add_foreign_entity(hatch)
|
|
|
|
def add_boundary_markers(self, hatch: DXFPolygon) -> None:
|
|
hatch.dxf.discard("extrusion")
|
|
path_num: int = 0
|
|
|
|
for p in hatch.paths:
|
|
path_num += 1
|
|
if isinstance(p, PolylinePath):
|
|
self.add_polyline_markers(p, path_num)
|
|
elif isinstance(p, EdgePath):
|
|
self.add_edge_markers(p, path_num)
|
|
else:
|
|
raise TypeError(f"unknown boundary path type: {type(p)}")
|
|
|
|
def add_start_marker(self, location: Vec2, name: str, layer: str) -> None:
|
|
self.add_marker(EDGE_START_MARKER, location, name, layer)
|
|
|
|
def add_end_marker(self, location: Vec2, name: str, layer: str) -> None:
|
|
self.add_marker(EDGE_END_MARKER, location, name, layer)
|
|
|
|
def add_marker(
|
|
self, blk_name: str, location: Vec2, name: str, layer: str
|
|
) -> None:
|
|
blkref = self.msp.add_blockref(
|
|
name=blk_name,
|
|
insert=location,
|
|
dxfattribs={
|
|
"layer": layer,
|
|
"rotation": self.angle,
|
|
},
|
|
)
|
|
blkref.add_auto_attribs({"NAME": name})
|
|
|
|
def add_polyline_markers(self, p: PolylinePath, num: int) -> None:
|
|
self.add_start_marker(
|
|
Vec2(p.vertices[0]), f"Poly-S({num})", POLYLINE_LAYER
|
|
)
|
|
self.add_end_marker(
|
|
Vec2(p.vertices[0]), f"Poly-E({num})", POLYLINE_LAYER
|
|
)
|
|
|
|
def add_edge_markers(self, p: EdgePath, num: int) -> None:
|
|
edge_num: int = 0
|
|
for edge in p.edges:
|
|
edge_num += 1
|
|
name = f"({num}.{edge_num})"
|
|
if isinstance(
|
|
edge,
|
|
LineEdge,
|
|
):
|
|
self.add_line_edge_markers(edge, name)
|
|
elif isinstance(edge, ArcEdge):
|
|
self.add_arc_edge_markers(edge, name)
|
|
elif isinstance(edge, EllipseEdge):
|
|
self.add_ellipse_edge_markers(edge, name)
|
|
elif isinstance(edge, SplineEdge):
|
|
self.add_spline_edge_markers(edge, name)
|
|
else:
|
|
raise TypeError(f"unknown edge type: {type(edge)}")
|
|
|
|
def add_line_edge_markers(self, line: LineEdge, name: str) -> None:
|
|
self.add_start_marker(line.start, "Line-S" + name, LINE_LAYER)
|
|
self.add_end_marker(line.end, "Line-E" + name, LINE_LAYER)
|
|
|
|
def add_arc_edge_markers(self, arc: ArcEdge, name: str) -> None:
|
|
self.add_start_marker(arc.start_point, "Arc-S" + name, ARC_LAYER)
|
|
self.add_end_marker(arc.end_point, "Arc-E" + name, ARC_LAYER)
|
|
|
|
def add_ellipse_edge_markers(self, ellipse: EllipseEdge, name: str) -> None:
|
|
self.add_start_marker(
|
|
ellipse.start_point, "Ellipse-S" + name, ELLIPSE_LAYER
|
|
)
|
|
self.add_end_marker(
|
|
ellipse.end_point, "Ellipse-E" + name, ELLIPSE_LAYER
|
|
)
|
|
|
|
def add_spline_edge_markers(self, spline: SplineEdge, name: str) -> None:
|
|
if len(spline.control_points):
|
|
# Assuming a clamped B-spline, because this is the only practical
|
|
# usable B-spline for edges.
|
|
self.add_start_marker(
|
|
spline.start_point, "SplineS" + name, SPLINE_LAYER
|
|
)
|
|
self.add_end_marker(
|
|
spline.end_point, "SplineE" + name, SPLINE_LAYER
|
|
)
|
|
|
|
@staticmethod
|
|
def report(hatch: DXFPolygon) -> list[str]:
|
|
return hatch_report(hatch)
|
|
|
|
@staticmethod
|
|
def print_report(hatch: DXFPolygon) -> None:
|
|
print("\n".join(hatch_report(hatch)))
|
|
|
|
|
|
def hatch_report(hatch: DXFPolygon) -> list[str]:
|
|
dxf = hatch.dxf
|
|
style = const.ISLAND_DETECTION[dxf.hatch_style]
|
|
pattern_type = const.HATCH_PATTERN_TYPE[dxf.pattern_type]
|
|
text = [
|
|
f"{str(hatch)}",
|
|
f" solid fill: {bool(dxf.solid_fill)}",
|
|
f" pattern type: {pattern_type}",
|
|
f" pattern name: {dxf.pattern_name}",
|
|
f" associative: {bool(dxf.associative)}",
|
|
f" island detection: {style}",
|
|
f" has pattern data: {hatch.pattern is not None}",
|
|
f" has gradient data: {hatch.gradient is not None}",
|
|
f" seed value count: {len(hatch.seeds)}",
|
|
f" boundary path count: {len(hatch.paths)}",
|
|
]
|
|
num = 0
|
|
for path in hatch.paths:
|
|
num += 1
|
|
if isinstance(path, PolylinePath):
|
|
text.extend(polyline_path_report(path, num))
|
|
elif isinstance(path, EdgePath):
|
|
text.extend(edge_path_report(path, num))
|
|
return text
|
|
|
|
|
|
def polyline_path_report(p: PolylinePath, num: int) -> list[str]:
|
|
path_type = ", ".join(const.boundary_path_flag_names(p.path_type_flags))
|
|
return [
|
|
f"{num}. Polyline Path, vertex count: {len(p.vertices)}",
|
|
f" path type: {path_type}",
|
|
]
|
|
|
|
|
|
def edge_path_report(p: EdgePath, num: int) -> list[str]:
|
|
closed = False
|
|
connected = False
|
|
path_type = ", ".join(const.boundary_path_flag_names(p.path_type_flags))
|
|
edges = p.edges
|
|
if len(edges):
|
|
closed = edges[0].start_point.isclose(edges[-1].end_point)
|
|
connected = all(
|
|
e1.end_point.isclose(e2.start_point)
|
|
for e1, e2 in zip(edges, edges[1:])
|
|
)
|
|
|
|
return [
|
|
f"{num}. Edge Path, edge count: {len(p.edges)}",
|
|
f" path type: {path_type}",
|
|
f" continuously connected edges: {connected}",
|
|
f" closed edge loop: {closed}",
|
|
]
|
|
|
|
|
|
MULTILEADER = "MULTILEADER_MARKER"
|
|
POINT_MARKER = "POINT_MARKER"
|
|
|
|
|
|
class MultileaderAnalyzer:
|
|
"""Multileader can not be added as foreign entity to a new document.
|
|
Annotations have to be added to the source document.
|
|
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
multileader: mleader.MultiLeader,
|
|
*,
|
|
marker_size: float = 1.0,
|
|
report_width: int = 79,
|
|
):
|
|
self.marker_size = marker_size
|
|
self.report_width = report_width
|
|
self.multileader = multileader
|
|
assert self.multileader.doc is not None, "valid DXF document required"
|
|
self.doc = self.multileader.doc
|
|
self.msp = self.doc.modelspace()
|
|
self.init_layers()
|
|
self.init_markers()
|
|
|
|
def init_layers(self):
|
|
if self.doc.layers.has_entry(MULTILEADER):
|
|
return
|
|
self.doc.layers.add(MULTILEADER, color=colors.RED)
|
|
|
|
def init_markers(self):
|
|
if POINT_MARKER not in self.doc.blocks:
|
|
blk = self.doc.blocks.new(POINT_MARKER)
|
|
attribs = {"layer": "0"} # from INSERT
|
|
size = self.marker_size
|
|
size_2 = size / 2.0
|
|
radius = size / 4.0
|
|
blk.add_circle(
|
|
center=(0, 0),
|
|
radius=radius,
|
|
dxfattribs=attribs,
|
|
)
|
|
blk.add_line((-size_2, 0), (size_2, 0), dxfattribs=attribs)
|
|
blk.add_line((0, -size_2), (0, size_2), dxfattribs=attribs)
|
|
|
|
@property
|
|
def context(self) -> mleader.MLeaderContext:
|
|
return self.multileader.context
|
|
|
|
@property
|
|
def mleaderstyle(self) -> Optional[mleader.MLeaderStyle]:
|
|
handle = self.multileader.dxf.get("style_handle")
|
|
return self.doc.entitydb.get(handle) # type: ignore
|
|
|
|
def divider_line(self, symbol="-") -> str:
|
|
return symbol * self.report_width
|
|
|
|
def shorten_lines(self, lines: Iterable[str]) -> list[str]:
|
|
return [
|
|
textwrap.shorten(line, width=self.report_width) for line in lines
|
|
]
|
|
|
|
def report(self) -> list[str]:
|
|
report = [
|
|
str(self.multileader),
|
|
self.divider_line(),
|
|
"Existing DXF attributes:",
|
|
self.divider_line(),
|
|
]
|
|
report.extend(self.multileader_attributes())
|
|
report.extend(self.context_attributes())
|
|
if self.context.mtext is not None:
|
|
report.extend(self.mtext_attributes())
|
|
if self.context.block is not None:
|
|
report.extend(self.block_attributes())
|
|
if self.multileader.block_attribs:
|
|
report.extend(self.block_reference_attribs())
|
|
if self.multileader.context.leaders:
|
|
report.extend(self.leader_attributes())
|
|
if self.multileader.arrow_heads:
|
|
report.extend(self.arrow_heads())
|
|
return self.shorten_lines(report)
|
|
|
|
def print_report(self) -> None:
|
|
width = self.report_width
|
|
print()
|
|
print("=" * width)
|
|
print("\n".join(self.report()))
|
|
print("=" * width)
|
|
|
|
def print_overridden_properties(self):
|
|
print("\n".join(self.overridden_attributes()))
|
|
|
|
def overridden_attributes(self) -> list[str]:
|
|
multileader = self.multileader
|
|
style = self.mleaderstyle
|
|
if style is not None:
|
|
report = [
|
|
self.divider_line(),
|
|
f"Override attributes of {str(style)}: '{style.dxf.name}'",
|
|
self.divider_line(),
|
|
]
|
|
|
|
override = MLeaderStyleOverride(style, multileader)
|
|
for name in OVERRIDE_FLAG.keys():
|
|
if override.is_overridden(name):
|
|
report.append(f"{name}: {override.get(name)}")
|
|
if override.use_mtext_default_content:
|
|
report.append("use_mtext_default_content: 1")
|
|
else:
|
|
handle = self.multileader.dxf.get("style_handle")
|
|
report = [
|
|
self.divider_line(),
|
|
f"MLEADERSTYLE(#{handle}) not found",
|
|
]
|
|
return report
|
|
|
|
def multileader_attributes(self) -> list[str]:
|
|
attribs = self.multileader.dxf.all_existing_dxf_attribs()
|
|
keys = sorted(attribs.keys())
|
|
return [f"{key}: {attribs[key]}" for key in keys]
|
|
|
|
def print_override_state(self):
|
|
flags = self.multileader.dxf.property_override_flags
|
|
print(f"\nproperty_override_flags:")
|
|
print(f"dec: {flags}")
|
|
print(f"hex: {hex(flags)}")
|
|
print_bitmask(flags)
|
|
|
|
def print_context_attributes(self):
|
|
print("\n".join(self.context_attributes()))
|
|
|
|
def context_attributes(self) -> list[str]:
|
|
context = self.context
|
|
if context is None:
|
|
return []
|
|
report = [
|
|
self.divider_line(),
|
|
"CONTEXT object attributes:",
|
|
self.divider_line(),
|
|
f"has MTEXT content: {yes_or_no(context.mtext)}",
|
|
f"has BLOCK content: {yes_or_no(context.block)}",
|
|
self.divider_line(),
|
|
]
|
|
keys = [
|
|
key
|
|
for key in context.__dict__.keys()
|
|
if key not in ("mtext", "block", "leaders")
|
|
]
|
|
attributes = [f"{name}: {getattr(context, name)}" for name in keys]
|
|
attributes.sort()
|
|
report.extend(attributes)
|
|
return self.shorten_lines(report)
|
|
|
|
def print_mtext_attributes(self):
|
|
print("\n".join(self.mtext_attributes()))
|
|
|
|
def mtext_attributes(self) -> list[str]:
|
|
report = [
|
|
self.divider_line(),
|
|
"MTEXT content attributes:",
|
|
self.divider_line(),
|
|
]
|
|
mtext = self.context.mtext
|
|
if mtext is not None:
|
|
report.extend(_content_attributes(mtext))
|
|
return self.shorten_lines(report)
|
|
|
|
def print_block_attributes(self):
|
|
print("\n".join(self.block_attributes()))
|
|
|
|
def block_attributes(self) -> list[str]:
|
|
report = [
|
|
self.divider_line(),
|
|
"BLOCK content attributes:",
|
|
self.divider_line(),
|
|
]
|
|
block = self.context.block
|
|
if block is not None:
|
|
report.extend(_content_attributes(block))
|
|
return self.shorten_lines(report)
|
|
|
|
def print_leader_attributes(self):
|
|
print("\n".join(self.leader_attributes()))
|
|
|
|
def leader_attributes(self) -> list[str]:
|
|
report = []
|
|
leaders = self.context.leaders
|
|
if leaders is not None:
|
|
for index, leader in enumerate(leaders):
|
|
report.extend(self._leader_attributes(index, leader))
|
|
return self.shorten_lines(report)
|
|
|
|
def _leader_attributes(
|
|
self, index: int, leader: mleader.LeaderData
|
|
) -> list[str]:
|
|
report = [
|
|
self.divider_line(),
|
|
f"{index+1}. LEADER attributes:",
|
|
self.divider_line(),
|
|
]
|
|
report.extend(_content_attributes(leader, exclude=("lines", "breaks")))
|
|
s = ", ".join(map(str, leader.breaks))
|
|
report.append(f"breaks: [{s}]")
|
|
if leader.lines:
|
|
report.extend(self._leader_lines(leader.lines))
|
|
return report
|
|
|
|
def _leader_lines(self, lines) -> list[str]:
|
|
report = []
|
|
for num, line in enumerate(lines):
|
|
report.extend(
|
|
[
|
|
self.divider_line(),
|
|
f"{num + 1}. LEADER LINE attributes:",
|
|
self.divider_line(),
|
|
]
|
|
)
|
|
for name, value in line.__dict__.items():
|
|
if name in ("vertices", "breaks"):
|
|
vstr = ""
|
|
if value is not None:
|
|
vstr = ", ".join(map(str, value))
|
|
vstr = f"[{vstr}]"
|
|
else:
|
|
vstr = str(value)
|
|
report.append(f"{name}: {vstr}")
|
|
return report
|
|
|
|
def print_block_attribs(self):
|
|
print("\n".join(self.block_reference_attribs()))
|
|
|
|
def block_reference_attribs(self) -> list[str]:
|
|
report = [
|
|
self.divider_line(),
|
|
f"BLOCK reference attributes:",
|
|
self.divider_line(),
|
|
]
|
|
for index, attr in enumerate(self.multileader.block_attribs):
|
|
report.extend(
|
|
[
|
|
f"{index+1}. Attributes",
|
|
self.divider_line(),
|
|
]
|
|
)
|
|
report.append(f"handle: {attr.handle}")
|
|
report.append(f"index: {attr.index}")
|
|
report.append(f"width: {attr.width}")
|
|
report.append(f"text: {attr.text}")
|
|
return report
|
|
|
|
def arrow_heads(self) -> list[str]:
|
|
report = [
|
|
self.divider_line(),
|
|
f"ARROW HEAD attributes:",
|
|
self.divider_line(),
|
|
]
|
|
for index, attr in enumerate(self.multileader.arrow_heads):
|
|
report.extend(
|
|
[
|
|
f"{index+1}. Arrow Head",
|
|
self.divider_line(),
|
|
]
|
|
)
|
|
report.append(f"handle: {attr.handle}")
|
|
report.append(f"index: {attr.index}")
|
|
return report
|
|
|
|
def print_mleaderstyle(self):
|
|
print("\n".join(self.mleaderstyle_attributes()))
|
|
|
|
def mleaderstyle_attributes(self) -> list[str]:
|
|
report = []
|
|
style = self.mleaderstyle
|
|
if style is not None:
|
|
report.extend(
|
|
[
|
|
self.divider_line("="),
|
|
str(style),
|
|
self.divider_line("="),
|
|
]
|
|
)
|
|
attribs = style.dxf.all_existing_dxf_attribs()
|
|
keys = sorted(attribs.keys())
|
|
report.extend([f"{name}: {attribs[name]}" for name in keys])
|
|
else:
|
|
handle = self.multileader.dxf.get("style_handle")
|
|
report.append(f"MLEADERSTYLE(#{handle}) not found")
|
|
return self.shorten_lines(report)
|
|
|
|
|
|
def _content_attributes(
|
|
entity, exclude: Optional[Sequence[str]] = None
|
|
) -> list[str]:
|
|
exclude = exclude or []
|
|
if entity is not None:
|
|
return [
|
|
f"{name}: {value}"
|
|
for name, value in entity.__dict__.items()
|
|
if name not in exclude
|
|
]
|
|
return []
|
|
|
|
|
|
def yes_or_no(data) -> str:
|
|
return "yes" if data else "no"
|