def _virtual_polyline_entities(points, elevation: float, extrusion: Vec3, dxfattribs: dict, doc) -> Iterable[Union["Line", "Arc"]]: ocs = OCS(extrusion) if extrusion else OCS() prev_point = None prev_bulge = None for x, y, bulge in points: point = Vec3(x, y, elevation) if prev_point is None: prev_point = point prev_bulge = bulge continue attribs = dict(dxfattribs) if prev_bulge != 0: center, start_angle, end_angle, radius = bulge_to_arc( prev_point, point, prev_bulge) if radius > 0: attribs["center"] = Vec3(center.x, center.y, elevation) attribs["radius"] = radius attribs["start_angle"] = math.degrees(start_angle) attribs["end_angle"] = math.degrees(end_angle) if extrusion: attribs["extrusion"] = extrusion yield factory.new(dxftype="ARC", dxfattribs=attribs, doc=doc) else: attribs["start"] = ocs.to_wcs(prev_point) attribs["end"] = ocs.to_wcs(point) yield factory.new(dxftype="LINE", dxfattribs=attribs, doc=doc) prev_point = point prev_bulge = bulge
def _virtual_polyline_entities(points, elevation: float, extrusion: Vector, dxfattribs: dict, doc) -> Iterable[Union['Line', 'Arc']]: ocs = OCS(extrusion) if extrusion else OCS() prev_point = None prev_bulge = None for x, y, bulge in points: point = Vector(x, y, elevation) if prev_point is None: prev_point = point prev_bulge = bulge continue attribs = dict(dxfattribs) if prev_bulge != 0: center, start_angle, end_angle, radius = bulge_to_arc( prev_point, point, prev_bulge) attribs['center'] = Vector(center.x, center.y, elevation) attribs['radius'] = radius attribs['start_angle'] = math.degrees(start_angle) attribs['end_angle'] = math.degrees(end_angle) if extrusion: attribs['extrusion'] = extrusion yield factory.new(dxftype='ARC', dxfattribs=attribs, doc=doc) else: attribs['start'] = ocs.to_wcs(prev_point) attribs['end'] = ocs.to_wcs(point) yield factory.new(dxftype='LINE', dxfattribs=attribs, doc=doc) prev_point = point prev_bulge = bulge
def test_flip_deg_angle(angle): t = OCSTransform.from_ocs( OCS(-Z_AXIS), OCS(Z_AXIS), Matrix44(), ) control_value = t.transform_deg_angle(angle) assert _flip_deg_angle(angle) == pytest.approx(control_value)
def __init__(self, extrusion: Vec3 = None, m: Matrix44 = None): self.m = m if extrusion is None: self.old_ocs = None self.scale_uniform = False self.new_ocs = None else: self.old_ocs = OCS(extrusion) new_extrusion, self.scale_uniform = transform_extrusion(extrusion, m) self.new_ocs = OCS(new_extrusion)
def __init__(self, extrusion: Vec3 = None, m: Matrix44 = None): if m is None: self.m = Matrix44() else: self.m = m self.scale_uniform: bool = True if extrusion is None: # fill in dummy values self._reset_ocs(_PLACEHOLDER_OCS, _PLACEHOLDER_OCS, True) else: new_extrusion, scale_uniform = transform_extrusion(extrusion, m) self._reset_ocs(OCS(extrusion), OCS(new_extrusion), scale_uniform)
def ocs(self) -> OCS: """Returns object coordinate system (:ref:`ocs`) for 2D entities like :class:`Text` or :class:`Circle`, returns a pass-through OCS for entities without OCS support. """ # extrusion is only defined for 2D entities like Text, Circle, ... if self.dxf.is_supported("extrusion"): extrusion = self.dxf.get("extrusion", default=(0, 0, 1)) return OCS(extrusion) else: return OCS()
def transform(self, m: 'Matrix44') -> 'Insert': """ Transform INSERT entity by transformation matrix `m` inplace. Unlike the transformation matrix `m`, the INSERT entity can not represent a non orthogonal target coordinate system, for this case an :class:`InsertTransformationError` will be raised. .. versionadded:: 0.13 """ dxf = self.dxf m1 = self.matrix44() # Transform scaled source axis into target coordinate system ux, uy, uz = m.transform_directions((m1.ux, m1.uy, m1.uz)) # Get new scaling factors, all are positive: # z-axis is the real new z-axis, no reflection required # x-axis is the real new x-axis, no reflection required # y-axis - reflection is detected below z_scale = uz.magnitude x_scale = ux.magnitude y_scale = uy.magnitude # check for orthogonal x-, y- and z-axis ux = ux.normalize() uy = uy.normalize() uz = uz.normalize() if not (math.isclose(ux.dot(uz), 0.0, abs_tol=1e-9) and math.isclose(ux.dot(uy), 0.0, abs_tol=1e-9) and math.isclose(uz.dot(uy), 0.0, abs_tol=1e-9)): raise InsertTransformationError(NON_ORTHO_MSG) # expected y-axis for an orthogonal right handed coordinate system expected_uy = uz.cross(ux) if expected_uy.isclose(-uy, abs_tol=1e-9): # transformed y-axis points into opposite direction of the expected # y-axis: y_scale = -y_scale ocs = OCSTransform.from_ocs(OCS(dxf.extrusion), OCS(uz), m) dxf.insert = ocs.transform_vertex(dxf.insert) dxf.rotation = ocs.transform_deg_angle(dxf.rotation) dxf.extrusion = uz dxf.xscale = x_scale dxf.yscale = y_scale dxf.zscale = z_scale for attrib in self.attribs: attrib.transform(m) return self
def transform_extrusion(extrusion: 'Vertex', m: Matrix44) -> Tuple[Vec3, bool]: """ Transforms the old `extrusion` vector into a new extrusion vector. Returns the new extrusion vector and a boolean value: ``True`` if the new OCS established by the new extrusion vector has a uniform scaled xy-plane, else ``False``. The new extrusion vector is perpendicular to plane defined by the transformed x- and y-axis. Args: extrusion: extrusion vector of the old OCS m: transformation matrix Returns: """ ocs = OCS(extrusion) ocs_x_axis_in_wcs = ocs.to_wcs(X_AXIS) ocs_y_axis_in_wcs = ocs.to_wcs(Y_AXIS) x_axis, y_axis = m.transform_directions( (ocs_x_axis_in_wcs, ocs_y_axis_in_wcs)) # Not sure if this is the correct test for a uniform scaled xy-plane is_uniform = math.isclose(x_axis.magnitude_square, y_axis.magnitude_square, abs_tol=1e-9) new_extrusion = x_axis.cross(y_axis).normalize() return new_extrusion, is_uniform
def test_from_complex_edge_path(self, edge_path): path = converter.from_hatch_edge_path(edge_path, ocs=OCS((0, 0, -1)), elevation=4) assert path.has_sub_paths is False assert len(path) == 19 assert all(math.isclose(v.z, -4) for v in path.control_vertices())
def transform(self, m: Matrix44) -> 'MText': """ Transform the MTEXT entity by transformation matrix `m` inplace. """ dxf = self.dxf old_extrusion = Vec3(dxf.extrusion) new_extrusion, _ = transform_extrusion(old_extrusion, m) if dxf.hasattr('rotation') and not dxf.hasattr('text_direction'): # MTEXT is not an OCS entity, but I don't know how else to convert # a rotation angle for an entity just defined by an extrusion vector. # It's correct for the most common case: extrusion=(0, 0, 1) ocs = OCS(old_extrusion) dxf.text_direction = ocs.to_wcs(Vec3.from_deg_angle(dxf.rotation)) dxf.discard('rotation') old_text_direction = Vec3(dxf.text_direction) new_text_direction = m.transform_direction(old_text_direction) old_char_height_vec = old_extrusion.cross( old_text_direction).normalize(dxf.char_height) new_char_height_vec = m.transform_direction(old_char_height_vec) oblique = new_text_direction.angle_between(new_char_height_vec) dxf.char_height = new_char_height_vec.magnitude * math.sin(oblique) if dxf.hasattr('width'): width_vec = old_text_direction.normalize(dxf.width) dxf.width = m.transform_direction(width_vec).magnitude dxf.insert = m.transform(dxf.insert) dxf.text_direction = new_text_direction dxf.extrusion = new_extrusion return self
def create_block_references( layout: 'BaseLayout', block_name: str, layer: str = "LAYER", grid=(10, 10), extrusions=((0, 0, 1), (0, 0, -1)), scales=((1, 1, 1), (-1, 1, 1), (1, -1, 1), (1, 1, -1)), angles=(0, 45, 90, 135, 180, 225, 270, 315), ): y = 0 grid_x, grid_y = grid for extrusion in extrusions: ocs = OCS(extrusion) for sx, sy, sz in scales: for index, angle in enumerate(angles): x = index * grid_x insert = ocs.from_wcs((x, y)) blk_ref = layout.add_blockref(block_name, insert, dxfattribs={ 'layer': layer, 'rotation': angle, 'xscale': sx, 'yscale': sy, 'zscale': sz, 'extrusion': extrusion, }) show_config(blk_ref) y += grid_y
def transform(self, m: "Matrix44") -> "Insert": """Transform INSERT entity by transformation matrix `m` inplace. Unlike the transformation matrix `m`, the INSERT entity can not represent a non orthogonal target coordinate system, for this case an :class:`InsertTransformationError` will be raised. """ dxf = self.dxf ocs = self.ocs() # Transform source OCS axis into the target coordinate system: ux, uy, uz = m.transform_directions((ocs.ux, ocs.uy, ocs.uz)) # Calculate new axis scaling factors: x_scale = ux.magnitude * dxf.xscale y_scale = uy.magnitude * dxf.yscale z_scale = uz.magnitude * dxf.zscale ux = ux.normalize() uy = uy.normalize() uz = uz.normalize() # check for orthogonal x-, y- and z-axis if (abs(ux.dot(uz)) > ABS_TOL or abs(ux.dot(uy)) > ABS_TOL or abs(uz.dot(uy)) > ABS_TOL): raise InsertTransformationError(NON_ORTHO_MSG) # expected y-axis for an orthogonal right handed coordinate system: expected_uy = uz.cross(ux) if not expected_uy.isclose(uy, abs_tol=ABS_TOL): # new y-axis points into opposite direction: y_scale = -y_scale ocs_transform = OCSTransform.from_ocs(OCS(dxf.extrusion), OCS(uz), m) dxf.insert = ocs_transform.transform_vertex(dxf.insert) dxf.rotation = ocs_transform.transform_deg_angle(dxf.rotation) dxf.extrusion = uz dxf.xscale = x_scale dxf.yscale = y_scale dxf.zscale = z_scale for attrib in self.attribs: attrib.transform(m) self.post_transform(m) return self
def test_to_ocs(self): p = Path((0, 1, 1)) p.line_to((0, 1, 3)) ocs = OCS((1, 0, 0)) # x-Axis result = list(transform_paths_to_ocs([p], ocs)) p0 = result[0] assert ocs.from_wcs((0, 1, 1)) == p0.start assert ocs.from_wcs((0, 1, 3)) == p0[0].end
def test_matrix44_to_ocs(): ocs = OCS(EXTRUSION) matrix = Matrix44.ucs(ocs.ux, ocs.uy, ocs.uz) assert is_close_points( matrix.ocs_from_wcs(Vector(-9.56460754, 8.44764172, 9.97894327)), (9.41378764657076, 13.15481838975576, 0.8689258932616031), places=6, )
def get_draw_angles(start: float, end: float, extrusion: Vector): if extrusion.isclose(Z_AXIS): return start, end else: ocs = OCS(extrusion) s = ocs.to_wcs(Vector.from_angle(start)) e = ocs.to_wcs(Vector.from_angle(end)) return normalize_angle(e.angle), normalize_angle(s.angle)
def draw(points, extrusion=None): dxfattribs = {'color': 1} if extrusion is not None: ocs = OCS(extrusion) points = ocs.points_from_wcs(points) dxfattribs['extrusion'] = extrusion for point in points: msp.add_circle(radius=0.1, center=point, dxfattribs=dxfattribs)
def test_spline_edge(self): ep = EdgePath() ep.add_spline(fit_points=[(10, 5), (8, 5), (6, 8), (5, 10)]) ep.add_line((5, 10), (10, 5)) path = converter.from_hatch_edge_path(ep, ocs=OCS((0, 0, -1)), elevation=4) assert len(path) > 2 assert all(math.isclose(v.z, -4) for v in path.control_vertices())
def test_matrix44_to_wcs(): ocs = OCS(EXTRUSION) matrix = Matrix44.ucs(ocs.ux, ocs.uy, ocs.uz) matrix.transpose() assert is_close_points( matrix.transform( (9.41378764657076, 13.15481838975576, 0.8689258932616031)), (-9.56460754, 8.44764172, 9.97894327), places=6, )
def ocs(self) -> Optional[OCS]: """ Returns object coordinate system (:ref:`ocs`) for 2D entities like :class:`Text` or :class:`Circle`, returns ``None`` for entities without OCS support. """ # extrusion is only defined for 2D entities like Text, Circle, ... if self.dxf.is_supported('extrusion'): extrusion = self.dxf.get('extrusion', default=(0, 0, 1)) return OCS(extrusion) else: return None
def flatten_to_polyline_path( path: AbstractBoundaryPath, distance: float, segments: int = 16 ) -> "PolylinePath": import ezdxf.path # avoid cyclic imports # keep path in original OCS! ez_path = ezdxf.path.from_hatch_boundary_path(path, ocs=OCS(), elevation=0) vertices = ((v.x, v.y) for v in ez_path.flattening(distance, segments)) return PolylinePath.from_vertices( vertices, flags=path.path_type_flags, )
def from_hatch_polyline_path(cls, polyline: 'PolylinePath', ocs: OCS = None, elevation: float = 0) -> 'Path': """ Returns a :class:`Path` from a :class:`~ezdxf.entities.Hatch` polyline path. """ path = cls() path._setup_polyline_2d( polyline.vertices, # List[(x, y, bulge)] close=polyline.is_closed, ocs=ocs or OCS(), elevation=elevation, ) return path
def test_line_edge(self): ep = EdgePath() ep.add_line(A, B) ep.add_line(B, C) ep.add_line(C, D) ep.add_line(D, A) path = converter.from_hatch_edge_path(ep, ocs=OCS((0, 0, -1)), elevation=4) assert len(list(path.sub_paths())) == 1, "expected one closed loop" assert len(list(path.control_vertices())) == 5 assert all(math.isclose(v.z, -4) for v in path.control_vertices()) assert path.is_closed is True, "expected a closed loop"
def to_matplotlib_path(paths: Iterable[Path], extrusion: "Vertex" = Z_AXIS): """Convert the given `paths` into a single :class:`matplotlib.path.Path` object. The `extrusion` vector is applied to all paths, all vertices are projected onto the plane normal to this extrusion vector.The default extrusion vector is the WCS z-axis. The Matplotlib :class:`Path` is a 2D object with :ref:`OCS` coordinates and the z-elevation is lost. (requires Matplotlib) Args: paths: iterable of :class:`Path` objects extrusion: extrusion vector for all paths Returns: matplotlib `Path`_ in OCS! .. versionadded:: 0.16 """ from matplotlib.path import Path as MatplotlibPath if not Z_AXIS.isclose(extrusion): paths = tools.transform_paths_to_ocs(paths, OCS(extrusion)) else: paths = list(paths) if len(paths) == 0: raise ValueError("one or more paths required") def add_command(code: MplCmd, point: Vec3): codes.append(code) vertices.append((point.x, point.y)) vertices: List[Tuple[float, float]] = [] codes: List[MplCmd] = [] for path in paths: add_command(MplCmd.MOVETO, path.start) for cmd in path.commands(): if cmd.type == Command.LINE_TO: add_command(MplCmd.LINETO, cmd.end) elif cmd.type == Command.MOVE_TO: add_command(MplCmd.MOVETO, cmd.end) elif cmd.type == Command.CURVE3_TO: add_command(MplCmd.CURVE3, cmd.ctrl) # type: ignore add_command(MplCmd.CURVE3, cmd.end) elif cmd.type == Command.CURVE4_TO: add_command(MplCmd.CURVE4, cmd.ctrl1) # type: ignore add_command(MplCmd.CURVE4, cmd.ctrl2) # type: ignore add_command(MplCmd.CURVE4, cmd.end) # STOP command is currently not required assert len(vertices) == len(codes) return MatplotlibPath(vertices, codes)
def to_ocs(self) -> "ConstructionEllipse": """Returns ellipse parameters as OCS representation. OCS elevation is stored in :attr:`center.z`. """ ocs = OCS(self.extrusion) return self.__class__( center=ocs.from_wcs(self.center), major_axis=ocs.from_wcs( self.major_axis).replace(z=0), # type: ignore ratio=self.ratio, start_param=self.start_param, end_param=self.end_param, )
def test_arc_edge(self): ep = EdgePath() ep.add_arc( center=(5.0, 5.0), radius=5.0, start_angle=0, end_angle=90, ccw=True, ) ep.add_line((5, 10), (10, 5)) path = converter.from_hatch_edge_path(ep, ocs=OCS((0, 0, -1)), elevation=4) assert len(path) == 2 assert all(math.isclose(v.z, -4) for v in path.control_vertices())
def from_hatch_polyline_path(polyline: 'PolylinePath', ocs: OCS = None, elevation: float = 0) -> 'Path': """ Returns a :class:`Path` object from a :class:`~ezdxf.entities.Hatch` polyline path. """ path = Path() tools.add_2d_polyline( path, polyline.vertices, # List[(x, y, bulge)] close=polyline.is_closed, ocs=ocs or OCS(), elevation=elevation, ) return path
def get_text_direction(self) -> Vec3: """Returns the horizontal text direction as :class:`~ezdxf.math.Vec3` object, even if only the text rotation is defined. """ dxf = self.dxf # "text_direction" has higher priority than "rotation" if dxf.hasattr("text_direction"): return dxf.text_direction if dxf.hasattr("rotation"): # MTEXT is not an OCS entity, but I don't know how else to convert # a rotation angle for an entity just defined by an extrusion vector. # It's correct for the most common case: extrusion=(0, 0, 1) return OCS(dxf.extrusion).to_wcs(Vec3.from_deg_angle(dxf.rotation)) return X_AXIS
def _update_location_from_mtext(text: Text, mtext: MText) -> None: # TEXT is an OCS entity, MTEXT is a WCS entity dxf = text.dxf insert = Vec3(mtext.dxf.insert) extrusion = Vec3(mtext.dxf.extrusion) text_direction = mtext.get_text_direction() if extrusion.isclose(Z_AXIS): # most common case dxf.rotation = text_direction.angle_deg else: ocs = OCS(extrusion) insert = ocs.from_wcs(insert) dxf.extrusion = extrusion.normalize() dxf.rotation = ocs.from_wcs(text_direction).angle_deg # type: ignore dxf.insert = insert dxf.align_point = insert # the same point for all MTEXT alignments! dxf.halign, dxf.valign = MAP_MTEXT_ALIGN_TO_FLAGS.get( mtext.dxf.attachment_point, (TextHAlign.LEFT, TextVAlign.TOP))
def test_circle_user_ocs(): center = (2, 3, 4) extrusion = (0, 1, 0) circle = Circle.new( dxfattribs={'center': center, 'extrusion': extrusion, 'thickness': 2}) ocs = OCS(extrusion) v = ocs.to_wcs(center) # (-2, 4, 3) v = Vector(v.x * 2, v.y * 4, v.z * 2) v += (1, 1, 1) # and back to OCS, extrusion is unchanged result = ocs.from_wcs(v) m = Matrix44.chain(Matrix44.scale(2, 4, 2), Matrix44.translate(1, 1, 1)) circle.transform(m) assert circle.dxf.center == result assert circle.dxf.extrusion == (0, 1, 0) assert circle.dxf.thickness == 8 # in WCS y-axis
def to_qpainter_path(paths: Iterable[Path], extrusion: "Vertex" = Z_AXIS): """Convert the given `paths` into a :class:`QtGui.QPainterPath` object. The `extrusion` vector is applied to all paths, all vertices are projected onto the plane normal to this extrusion vector. The default extrusion vector is the WCS z-axis. The :class:`QPainterPath` is a 2D object with :ref:`OCS` coordinates and the z-elevation is lost. (requires Qt bindings) Args: paths: iterable of :class:`Path` objects extrusion: extrusion vector for all paths Returns: `QPainterPath`_ in OCS! .. versionadded:: 0.16 """ from ezdxf.addons.xqt import QPainterPath, QPointF if not Z_AXIS.isclose(extrusion): paths = tools.transform_paths_to_ocs(paths, OCS(extrusion)) else: paths = list(paths) if len(paths) == 0: raise ValueError("one or more paths required") def qpnt(v: Vec3): return QPointF(v.x, v.y) qpath = QPainterPath() for path in paths: qpath.moveTo(qpnt(path.start)) for cmd in path.commands(): if cmd.type == Command.LINE_TO: qpath.lineTo(qpnt(cmd.end)) elif cmd.type == Command.MOVE_TO: qpath.moveTo(qpnt(cmd.end)) elif cmd.type == Command.CURVE3_TO: qpath.quadTo(qpnt(cmd.ctrl), qpnt(cmd.end)) # type: ignore elif cmd.type == Command.CURVE4_TO: qpath.cubicTo(qpnt(cmd.ctrl1), qpnt(cmd.ctrl2), qpnt(cmd.end)) # type: ignore return qpath