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 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 _to_wcs(self, ocs: OCS, elevation: float): self._start = ocs.to_wcs(self._start.replace(z=elevation)) for i, cmd in enumerate(self._commands): new_cmd = [cmd[0]] new_cmd.extend( ocs.points_to_wcs(p.replace(z=elevation) for p in cmd[1:])) self._commands[i] = tuple(new_cmd)
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 _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 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 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 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 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 __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 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 from_hatch_boundary_path( boundary: AbstractBoundaryPath, ocs: OCS = None, elevation: float = 0, offset: Vec3 = NULLVEC, # ocs offset! ) -> "Path": """Returns a :class:`Path` object from a :class:`~ezdxf.entities.Hatch` polyline- or edge path. .. versionchanged:: 17.1 Attaches the boundary state to each path as :class:`ezdxf.lldxf.const.BoundaryPathState`. """ if isinstance(boundary, EdgePath): p = from_hatch_edge_path(boundary, ocs, elevation) elif isinstance(boundary, PolylinePath): p = from_hatch_polyline_path(boundary, ocs, elevation) else: raise TypeError(type(boundary)) if offset and ocs is not None: # only for MPOLYGON # assume offset is in OCS offset = ocs.to_wcs(offset.replace(z=elevation)) p = p.transform(Matrix44.translate(offset.x, offset.y, offset.z)) # attach path type information p.user_data = const.BoundaryPathState.from_flags(boundary.path_type_flags) return p
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") -> "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 faces_wcs(self, ocs: OCS, elevation: float) -> Iterable[Sequence[Vec3]]: """Yields all faces as 4-tuples of :class:`~ezdxf.math.Vec3` objects in :ref:`WCS`. """ for face in self.faces(): yield tuple( ocs.points_to_wcs(Vec3(v.x, v.y, elevation) for v in face))
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 _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 polygons_wcs(self, ocs: OCS, elevation: float) -> Iterable[Sequence[Vec3]]: """Yields for each sub-trace a single polygon as sequence of :class:`~ezdxf.math.Vec3` objects in :ref:`WCS`. """ for trace in self._traces: yield tuple( ocs.points_to_wcs( Vec3(v.x, v.y, elevation) for v in trace.polygon()))
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, )
class OCSTransform: 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) @property def old_extrusion(self) -> Vec3: return self.old_ocs.uz @property def new_extrusion(self) -> Vec3: return self.new_ocs.uz @classmethod def from_ocs(cls, old: OCS, new: OCS, m: Matrix44) -> 'OCSTransform': ocs = cls() ocs.m = m ocs.old_ocs = old ocs.new_ocs = new return ocs def transform_length(self, length: 'Vertex', reflection=1.0) -> float: """ Returns magnitude of `length` direction vector transformed from old OCS into new OCS including `reflection` correction applied. """ return self.m.transform_direction(self.old_ocs.to_wcs(length)).magnitude * sign(reflection) transform_scale_factor = transform_length def transform_vertex(self, vertex: 'Vertex') -> Vec3: """ Returns vertex transformed from old OCS into new OCS. """ return self.new_ocs.from_wcs(self.m.transform(self.old_ocs.to_wcs(vertex))) def transform_2d_vertex(self, vertex: 'Vertex', elevation: float) -> Vec2: """ Returns 2D vertex transformed from old OCS into new OCS. """ v = Vec3(vertex).replace(z=elevation) return self.new_ocs.from_wcs(self.m.transform(self.old_ocs.to_wcs(v))).vec2 def transform_direction(self, direction: 'Vertex') -> Vec3: """ Returns direction transformed from old OCS into new OCS. """ return self.new_ocs.from_wcs(self.m.transform_direction(self.old_ocs.to_wcs(direction))) def transform_angle(self, angle: float) -> float: """ Returns angle (in radians) from old OCS transformed into new OCS. """ return self.transform_direction(Vec3.from_angle(angle)).angle def transform_deg_angle(self, angle: float) -> float: """ Returns angle (in degrees) from old OCS transformed into new OCS. """ return math.degrees(self.transform_angle(math.radians(angle)))
def main(filename): doc = ezdxf.new('R2010') msp = doc.modelspace() origin = (3, 3, 3) axis = (1, 0, -1) def_point = (3, 10, 4) ucs = UCS.from_z_axis_and_point_in_yz(origin, axis=axis, point=def_point) ucs.render_axis(msp, length=5) msp.add_point(location=def_point, dxfattribs={'color': 2}) ocs = OCS(ucs.uz) msp.add_circle(center=ocs.from_wcs(origin), radius=1, dxfattribs={ 'color': 2, 'extrusion': ucs.uz, }) doc.saveas(filename)
def from_arc( cls, center: "Vertex" = NULLVEC, radius: float = 1, extrusion: "Vertex" = Z_AXIS, start_angle: float = 0, end_angle: float = 360, ccw: bool = True, ) -> "ConstructionEllipse": """Returns :class:`ConstructionEllipse` from arc or circle. Arc and Circle parameters defined in OCS. Args: center: center in OCS radius: arc or circle radius extrusion: OCS extrusion vector start_angle: start angle in degrees end_angle: end angle in degrees ccw: arc curve goes counter clockwise from start to end if ``True`` """ radius = abs(radius) if NULLVEC.isclose(extrusion): raise ValueError(f"Invalid extrusion: {str(extrusion)}") ratio = 1.0 ocs = OCS(extrusion) center = ocs.to_wcs(center) # Major axis along the OCS x-axis. major_axis = ocs.to_wcs(Vec3(radius, 0, 0)) # No further adjustment of start- and end angle required. start_param = math.radians(start_angle) end_param = math.radians(end_angle) return cls( center, major_axis, extrusion, ratio, start_param, end_param, bool(ccw), )
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, )