def transform(self, m: Matrix44) -> 'MText': """ Transform MTEXT entity by transformation matrix `m` inplace. .. versionadded:: 0.13 """ 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 text2(self, data: bytes): bs = ByteStream(data) start_point = Vec3(bs.read_vertex()) normal = Vec3(bs.read_vertex()) text_direction = Vec3(bs.read_vertex()) text = bs.read_padded_string() ignore_length_of_string, raw = bs.read_struct('<2l') height, width_factor, oblique_angle, tracking_percentage = bs.read_struct( '<4d') is_backwards, is_upside_down, is_vertical, is_underline, is_overline = bs.read_struct( '<5L') font_filename = bs.read_padded_string() big_font_filename = bs.read_padded_string() attribs = self._build_dxf_attribs() attribs['insert'] = start_point attribs['text'] = text attribs['height'] = height attribs['width'] = width_factor attribs['rotation'] = text_direction.angle_deg attribs['oblique'] = math.degrees(oblique_angle) attribs['style'] = self._get_style(font_filename, big_font_filename) attribs['text_generation_flag'] = 2 * is_backwards + 4 * is_upside_down attribs['extrusion'] = normal return self._factory('TEXT', dxfattribs=attribs)
def test_spatial_arc_from_3p(): start_point_wcs = Vec3(0, 1, 0) end_point_wcs = Vec3(1, 0, 0) def_point_wcs = Vec3(0, 0, 1) ucs = UCS.from_x_axis_and_point_in_xy(origin=def_point_wcs, axis=end_point_wcs - def_point_wcs, point=start_point_wcs) start_point_ucs = ucs.from_wcs(start_point_wcs) end_point_ucs = ucs.from_wcs(end_point_wcs) def_point_ucs = Vec3(0, 0) arc = ConstructionArc.from_3p(start_point_ucs, end_point_ucs, def_point_ucs) dwg = ezdxf.new('R12') msp = dwg.modelspace() dxf_arc = arc.add_to_layout(msp, ucs) assert dxf_arc.dxftype() == 'ARC' assert isclose(dxf_arc.dxf.radius, 0.81649658, abs_tol=1e-9) assert isclose(dxf_arc.dxf.start_angle, 330) assert isclose(dxf_arc.dxf.end_angle, 210) assert dxf_arc.dxf.extrusion.isclose((0.57735027, 0.57735027, 0.57735027), abs_tol=1e-9)
def translate( vertices: Iterable["Vertex"], vec: "Vertex" = (0, 0, 0) ) -> Iterable[Vec3]: """Translate `vertices` along `vec`, faster than a Matrix44 transformation. Args: vertices: iterable of vertices vec: translation vector Returns: yields transformed vertices """ _vec = Vec3(vec) for p in vertices: yield _vec + p
def build(): arc = Arc.new( dxfattribs={ 'start_angle': random.uniform(0, 360), 'end_angle': random.uniform(0, 360), }) vertices = list(arc.vertices(arc.angles(vertex_count))) m = Matrix44.chain( Matrix44.axis_rotate(axis=Vec3.random(), angle=random.uniform(0, math.tau)), Matrix44.translate(dx=random.uniform(-2, 2), dy=random.uniform(-2, 2), dz=random.uniform(-2, 2)), ) return synced_transformation(arc, vertices, m)
def __init__( self, center: "Vertex" = NULLVEC, major_axis: "Vertex" = X_AXIS, extrusion: "Vertex" = Z_AXIS, ratio: float = 1, start_param: float = 0, end_param: float = math.tau, ccw: bool = True, ): self.center = Vec3(center) self.major_axis = Vec3(major_axis) if self.major_axis.isclose(NULLVEC): raise ValueError(f"Invalid major axis (null vector).") self.extrusion = Vec3(extrusion) if self.major_axis.isclose(NULLVEC): raise ValueError(f"Invalid extrusion vector (null vector).") self.ratio = float(ratio) self.start_param = float(start_param) self.end_param = float(end_param) if not ccw: self.start_param, self.end_param = self.end_param, self.start_param self.minor_axis = minor_axis(self.major_axis, self.extrusion, self.ratio)
def set_location(self, insert: 'Vertex', rotation: float = None, attachment_point: int = None) -> 'MText': """ Set attributes :attr:`dxf.insert`, :attr:`dxf.rotation` and :attr:`dxf.attachment_point`, ``None`` for :attr:`dxf.rotation` or :attr:`dxf.attachment_point` preserves the existing value. """ self.dxf.insert = Vec3(insert) if rotation is not None: self.set_rotation(rotation) if attachment_point is not None: self.dxf.attachment_point = attachment_point return self # fluent interface
def move_to(self, location: Vertex) -> None: """Start a new sub-path at `location`. This creates a gap between the current end-point and the start-point of the new sub-path. This converts the instance into a :term:`Multi-Path` object. If the :meth:`move_to` command is the first command, the start point of the path will be reset to `location`. .. versionadded:: 0.17 """ commands = self._commands if not commands: self._vertices[0] = Vec3(location) return self._has_sub_paths = True if commands[-1] == Command.MOVE_TO: # replace last move to command commands.pop() self._vertices.pop() self._start_index.pop() commands.append(Command.MOVE_TO) self._start_index.append(len(self._vertices)) self._vertices.append(Vec3(location))
def test_edge_path_transform_interface(hatch, m44): path = hatch.paths.add_edge_path() path.add_line((0, 0), (10, 0)) path.add_arc((10, 5), radius=5, start_angle=270, end_angle=450, ccw=1) path.add_ellipse((5, 10), major_axis=(5, 0), ratio=0.2, start_angle=0, end_angle=180) spline = path.add_spline([(1, 1), (2, 2), (3, 3), (4, 4)], degree=3, periodic=1) # the following values do not represent a mathematically valid spline spline.control_points = [(1, 1), (2, 2), (3, 3), (4, 4)] spline.knot_values = [1, 2, 3, 4, 5, 6] spline.weights = [4, 3, 2, 1] spline.start_tangent = (10, 1) spline.end_tangent = (2, 20) chk = list( m44.transform_vertices([ Vec3(0, 0), Vec3(10, 0), Vec3(10, 5), Vec3(5, 10), Vec3(1, 1), Vec3(2, 2), Vec3(3, 3), Vec3(4, 4), ])) hatch.transform(m44) line = path.edges[0] assert chk[0].isclose(line.start) assert chk[1].isclose(line.end) arc = path.edges[1] assert chk[2].isclose(arc.center) ellipse = path.edges[2] assert chk[3].isclose(ellipse.center) spline = path.edges[3] for c, v in zip(chk[4:], spline.control_points): assert c.isclose(v) for c, v in zip(chk[4:], spline.fit_points): assert c.isclose(v) assert m44.transform_direction((10, 1, 0)).isclose(spline.start_tangent) assert m44.transform_direction((2, 20, 0)).isclose(spline.end_tangent)
def test_transformation(): axis = Vec3.random() angle = 1.5 ucs = UCS(origin=(3, 4, 5)) m = Matrix44.axis_rotate(axis, angle) expected_origin = m.transform(ucs.origin) expected_ux = m.transform(ucs.ux) expected_uy = m.transform(ucs.uy) expected_uz = m.transform(ucs.uz) new = ucs.transform(m) assert new.origin.isclose(expected_origin) assert new.ux.isclose(expected_ux) assert new.uy.isclose(expected_uy) assert new.uz.isclose(expected_uz)
def translate(self, dx: float, dy: float, dz: float) -> 'Text': """ Optimized TEXT/ATTRIB/ATTDEF translation about `dx` in x-axis, `dy` in y-axis and `dz` in z-axis, returns `self` (floating interface). .. versionadded:: 0.13 """ ocs = self.ocs() dxf = self.dxf vec = Vec3(dx, dy, dz) dxf.insert = ocs.from_wcs(vec + ocs.to_wcs(dxf.insert)) if dxf.hasattr('align_point'): dxf.align_point = ocs.from_wcs(vec + ocs.to_wcs(dxf.align_point)) return self
def test_random_block_reference_transformation(sx, sy, sz, doc1: "Drawing"): def insert(): return ( Insert.new( dxfattribs={ "name": "AXIS", "insert": (0, 0, 0), "xscale": 1, "yscale": 1, "zscale": 1, "rotation": 0, "layer": "insert", }, doc=doc1, ), [Vec3(0, 0, 0), X_AXIS, Y_AXIS, Z_AXIS], ) def check(lines, chk): origin, x, y, z = chk l1, l2, l3 = lines assert origin.isclose(l1.dxf.start) assert x.isclose(l1.dxf.end) assert origin.isclose(l2.dxf.start) assert y.isclose(l2.dxf.end) assert origin.isclose(l3.dxf.start) assert z.isclose(l3.dxf.end) entity0, vertices0 = insert() entity0, vertices0 = synced_scaling(entity0, vertices0, 1, 2, 3) m = Matrix44.chain( # Transformation order is important: scale - rotate - translate # Because scaling after rotation leads to a non orthogonal # coordinate system, which can not represented by the # INSERT entity. Matrix44.scale(sx, sy, sz), Matrix44.axis_rotate(axis=Vec3.random(), angle=random.uniform(0, math.tau)), Matrix44.translate( dx=random.uniform(-2, 2), dy=random.uniform(-2, 2), dz=random.uniform(-2, 2), ), ) entity, vertices = synced_transformation(entity0, vertices0, m) lines = list(entity.virtual_entities()) check(lines, vertices)
def to_lwpolylines(paths: Iterable[Path], *, distance: float = MAX_DISTANCE, segments: int = MIN_SEGMENTS, extrusion: 'Vertex' = Z_AXIS, dxfattribs: Optional[Dict] = None) -> Iterable[LWPolyline]: """ Convert the given `paths` into :class:`~ezdxf.entities.LWPolyline` entities. 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 plane elevation is the distance from the WCS origin to the start point of the first path. Args: paths: iterable of :class:`Path` objects distance: maximum distance, see :meth:`Path.flattening` segments: minimum segment count per Bézier curve extrusion: extrusion vector for all paths dxfattribs: additional DXF attribs Returns: iterable of :class:`~ezdxf.entities.LWPolyline` objects .. versionadded:: 0.16 """ if isinstance(paths, Path): paths = [paths] else: paths = list(paths) if len(paths) == 0: return [] extrusion = Vec3(extrusion) reference_point = paths[0].start dxfattribs = dxfattribs or dict() if not extrusion.isclose(Z_AXIS): ocs, elevation = _get_ocs(extrusion, reference_point) paths = tools.transform_paths_to_ocs(paths, ocs) dxfattribs['elevation'] = elevation dxfattribs['extrusion'] = extrusion elif reference_point.z != 0: dxfattribs['elevation'] = reference_point.z for path in paths: p = LWPolyline.new(dxfattribs=dxfattribs) p.append_points(path.flattening(distance, segments), format='xy') yield p
def ngon( count: int, length: float = None, radius: float = None, rotation: float = 0.0, elevation: float = 0.0, close: bool = False, ) -> Iterable[Vec3]: """Returns the corner vertices of a `regular polygon <https://en.wikipedia.org/wiki/Regular_polygon>`_. The polygon size is determined by the edge `length` or the circum `radius` argument. If both are given `length` has the higher priority. Args: count: count of polygon corners >= 3 length: length of polygon side radius: circum radius rotation: rotation angle in radians elevation: z-axis for all vertices close: yields first vertex also as last vertex if ``True``. Returns: vertices as :class:`~ezdxf.math.Vec3` objects """ if count < 3: raise ValueError("Argument `count` has to be greater than 2.") if length is not None: if length <= 0.0: raise ValueError("Argument `length` has to be greater than 0.") radius = length / 2.0 / sin(pi / count) elif radius is not None: if radius <= 0.0: raise ValueError("Argument `radius` has to be greater than 0.") else: raise ValueError("Argument `length` or `radius` required.") delta = 2.0 * pi / count angle = rotation first = None for _ in range(count): v = Vec3(radius * cos(angle), radius * sin(angle), elevation) if first is None: first = v yield v angle += delta if close: yield first
def subdivide_ngons( faces: Iterable[Sequence[Union[Vec3, Vec2]]]) -> Iterable[List[Vec3]]: """ Yields only triangles or quad faces, subdivides ngons into triangles. Args: faces: iterable of faces as sequence of :class:`Vec2` and :class:`Vec3` objects """ for face in faces: if len(face) < 5: yield face else: mid_pos = Vec3.sum(face) / len(face) for index, vertex in enumerate(face): yield face[index - 1], vertex, mid_pos
def _setup(self) -> None: """ Calc setup values and determines the point order of the dimension line points. """ self.measure_points = [Vec3(point) for point in self.measure_points ] # type: List[Vec3] dimlineray = ConstructionRay(self.dimlinepos, angle=radians( self.angle)) # Type: ConstructionRay self.dimline_points = [ self._get_point_on_dimline(point, dimlineray) for point in self.measure_points ] # type: List[Vec3] self.point_order = self._indices_of_sorted_points( self.dimline_points) # type: List[int] self._build_vectors()
def scene2(filename): doc = ezdxf.new('R2010', setup=True) msp = doc.modelspace() delta = 6 for z in range(-2, 3): for y in range(-2, 3): for x in range(-2, 3): cx = x * delta cy = y * delta cz = z * delta ucs = UCS(origin=(cx, cy, cz)).rotate_local_z(math.radians(45)).rotate_local_x( math.radians(30)) add_excentric_text(msp, ucs, location=Vec3(1, 2, 3), text=f'Hallo\n(x={cx}, y={cy}, z={cz})') doc.set_modelspace_vport(5 * delta) doc.saveas(filename)
def insert(): return ( Insert.new( dxfattribs={ "name": "AXIS", "insert": (0, 0, 0), "xscale": 1, "yscale": 1, "zscale": 1, "rotation": 0, "layer": "insert", }, doc=doc1, ), [Vec3(0, 0, 0), X_AXIS, Y_AXIS, Z_AXIS], )
def __init__( self, control_points: Iterable["Vertex"], degree: int = 2, closed: bool = True, ): """ Args: control_points: B-spline control frame vertices degree: degree of B-spline, only 2 and 3 is supported closed: ``True`` for closed curve """ self.control_points = Vec3.list(control_points) self.degree = degree self.closed = closed
def test_if_nurbs_python_is_reliable(): # Testing for some known values, just for the case # that NURBS-Python is incorrect. expected = [ (0.0, 0.0, 0.0), (11.840000000000003, 13.760000000000002, 16.64), (22.72, 14.079999999999998, 22.719999999999995), (31.759999999999994, 11.2, 24.399999999999995), (39.92, 7.999999999999999, 26.0), (50.0, 0.0, 30.0) ] params = [0, .2, .4, .6, .8, 1.0] curve = BSpline(DEFPOINTS).to_nurbs_python_curve() points = curve.evaluate_list(params) for expect, point in zip(expected, points): assert Vec3(expect).isclose(point)
def point(self, t: float) -> Vec3: """ Get point at distance `t` as :class.`Vec3`. """ def term(length_power, curvature_power, const): return t**length_power / (const * self.curvature_powers[curvature_power]) if t not in self._cache: y = term(3, 2, 6.) - term(7, 6, 336.) + term(11, 10, 42240.) - \ term(15, 14, 9676800.) + term(19, 18, 3530096640.) x = t - term(5, 4, 40.) + term(9, 8, 3456.) - term(13, 12, 599040.) + \ term(17, 16, 175472640.) self._cache[t] = Vec3(x, y) return self._cache[t]
def ucs_direction_from_wcs(self, wcs: Vec3) -> Vec3: """ Returns UCS direction vector from WCS direction. Works only if matrix is used as cartesian UCS without scaling. (internal API) """ m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, *_ = self.matrix x, y, z = wcs return Vec3( x * m0 + y * m1 + z * m2, x * m4 + y * m5 + z * m6, x * m8 + y * m9 + z * m10, )
def spline_insert_knot(): doc = ezdxf.new('R2000', setup=True) msp = doc.modelspace() def add_spline(control_points, color=3, knots=None): msp.add_polyline2d(control_points, dxfattribs={'color': color, 'linetype': 'DASHED'}) msp.add_open_spline(control_points, degree=3, knots=knots, dxfattribs={'color': color}) control_points = Vec3.list([(0, 0), (10, 20), (30, 10), (40, 10), (50, 0), (60, 20), (70, 50), (80, 70)]) add_spline(control_points, color=3, knots=None) bspline = BSpline(control_points, order=4) bspline.insert_knot(bspline.max_t/2) add_spline(bspline.control_points, color=4, knots=bspline.knots()) doc.saveas("Spline_R2000_spline_insert_knot.dxf")
def test_random_ellipse_transformations(sx, sy, sz, start, end): vertex_count = 8 for angle in linspace(0, math.tau, 19): for dx, dy, dz in product([2, 0, -2], repeat=3): axis = Vec3.random() # TODO: fixed rotation axis config = f"CONFIG sx={sx}, sy={sy}, sz={sz}; " \ f"start={start:.4f}, end={end:.4f}; angle={angle};" \ f"dx={dx}, dy={dy}, dz={dz}; axis={str(axis)}" ellipse0, vertices0 = build(angle, dx, dy, dz, axis, start, end, vertex_count) assert check(ellipse0, vertices0, vertex_count) is True, config ellipse1, vertices1 = synced_scaling(ellipse0, vertices0, sx, sy, sz) assert check(ellipse1, vertices1, vertex_count) is True, config
def radius_3d(dxfversion='R2000', delta=10): doc = ezdxf.new(dxfversion, setup=True) msp = doc.modelspace() for x, y in multiple_locations(delta=delta): ucs = UCS(origin=(x, y, 0)).rotate_local_x(math.radians(45)) angle = Vec3(x, y).angle_deg msp.add_circle((0, 0), radius=3).transform(ucs.matrix) dim = msp.add_radius_dim(center=(0, 0), radius=3, angle=angle, dimstyle='EZ_RADIUS') dim.render(discard=BRICSCAD, ucs=ucs) doc.set_modelspace_vport(height=3 * delta) doc.saveas(OUTDIR / f'dim_radius_{dxfversion}_3d.dxf')
def bulge_to(p1: Vec3, p2: Vec3, bulge: float): if p1.isclose(p2): return center, start_angle, end_angle, radius = bulge_to_arc( p1, p2, bulge) ellipse = ConstructionEllipse.from_arc( center, radius, Z_AXIS, math.degrees(start_angle), math.degrees(end_angle), ) curves = list(cubic_bezier_from_ellipse(ellipse)) if curves[0].control_points[0].isclose(p2): curves = _reverse_bezier_curves(curves) self.add_curves(curves)
def test_context_manager_interface(self): xdata = XData() with XDataUserList(xdata) as xlist: xlist.extend(["String", Vec3(1, 2, 3), 3.1415, 256]) tags = xdata.get("EZDXF") assert tags == [ dxftag(1001, "EZDXF"), dxftag(1000, "DefaultList"), dxftag(1002, "{"), dxftag(1000, "String"), dxftag(1010, (1, 2, 3)), dxftag(1040, 3.1415), dxftag(1071, 256), dxftag(1002, "}"), ]
def extend(self, vertices: Iterable["Vertex"]) -> None: """Append multiple vertices to the reference line. It is possible to work with 3D vertices, but all vertices have to be in the same plane and the normal vector of this plan is stored as extrusion vector in the MLINE entity. """ vertices = Vec3.list(vertices) if not vertices: return all_vertices = [] if len(self): all_vertices.extend(self.get_locations()) all_vertices.extend(vertices) self.generate_geometry(all_vertices)
def test_commit_replaces_existing_XDATA(self, list1): xlist = XDataUserList(XData([list1])) xlist.clear() xlist.extend(["String", Vec3(1, 2, 3), 3.1415, 256]) xlist.commit() tags = xlist.xdata.get("EZDXF") assert tags == [ dxftag(1001, "EZDXF"), dxftag(1000, "DefaultList"), dxftag(1002, "{"), dxftag(1000, "String"), dxftag(1010, (1, 2, 3)), dxftag(1040, 3.1415), dxftag(1071, 256), dxftag(1002, "}"), ]
def test_to_edge_path_hatches(self, path): hatches = list(to_hatches(path, edge_path=True)) assert len(hatches) == 1 h0 = hatches[0] assert h0.dxftype() == "HATCH" assert len(h0.paths) == 1 edge_path = h0.paths[0] assert edge_path.type == BoundaryPathType.EDGE line, spline = edge_path.edges assert line.type == EdgeType.LINE assert line.start == (0, 0) assert line.end == (4, 0) assert spline.type == EdgeType.SPLINE assert close_vectors( Vec3.generate(spline.control_points), [(4, 0), (3, 1), (1, 1), (0, 0)], )