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 = Vector(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_spatial_arc_from_3p(): start_point_wcs = Vector(0, 1, 0) end_point_wcs = Vector(1, 0, 0) def_point_wcs = Vector(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 = Vector(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 vertices(self, angles: Iterable[float]) -> Iterable[Vector]: """ Yields vertices of the circle for iterable `angles` in WCS. This method takes into account a local OCS. Args: angles: iterable of angles in OCS as degrees, angle goes counter clockwise around the extrusion vector, ocs x-axis = 0 deg. .. versionadded:: 0.11 """ ocs = self.ocs() for angle in angles: v = Vector.from_deg_angle(angle, self.dxf.radius) + self.dxf.center yield ocs.to_wcs(v)
def draw_viewport_entity(self, entity: DXFGraphic) -> None: assert entity.dxftype() == 'VIEWPORT' dxf = entity.dxf view_vector: Vector = dxf.view_direction_vector mag = view_vector.magnitude if math.isclose(mag, 0.0): print('warning: viewport with null view vector') return view_vector /= mag if not math.isclose(view_vector.dot(Vector(0, 0, 1)), 1.0): print( f'cannot render viewport with non-perpendicular view direction: {dxf.view_direction_vector}' ) return cx, cy = dxf.center.x, dxf.center.y dx = dxf.width / 2 dy = dxf.height / 2 minx, miny = cx - dx, cy - dy maxx, maxy = cx + dx, cy + dy points = [(minx, miny), (maxx, miny), (maxx, maxy), (minx, maxy), (minx, miny)] self.out.draw_filled_polygon([Vector(x, y, 0) for x, y in points], VIEWPORT_COLOR)
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([ Vector(0, 0), Vector(10, 0), Vector(10, 5), Vector(5, 10), Vector(1, 1), Vector(2, 2), Vector(3, 3), Vector(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 cubic_bezier_arc_parameters(start_angle: float, end_angle: float, segments: int = 1) -> Sequence[Vector]: """ Yields cubic Bézier-curve parameters for a circular 2D arc with center at (0, 0) and a radius of 1 in the form of [start point, 1. control point, 2. control point, end point]. Args: start_angle: start angle in radians end_angle: end angle in radians (end_angle > start_angle!) segments: count of Bèzier-curve segments, at least one segment for each quarter (pi/2) """ if segments < 1: raise ValueError('Invalid argument segments (>= 1).') delta_angle = end_angle - start_angle if delta_angle > 0: arc_count = max(math.ceil(delta_angle / math.pi * 2.0), segments) else: raise ValueError('Delta angle from start- to end angle has to be > 0.') segment_angle = delta_angle / arc_count tangent_length = TANGENT_FACTOR * math.tan(segment_angle / 4.0) angle = start_angle end_point = None for _ in range(arc_count): start_point = Vector.from_angle( angle) if end_point is None else end_point angle += segment_angle end_point = Vector.from_angle(angle) control_point_1 = start_point + (-start_point.y * tangent_length, start_point.x * tangent_length) control_point_2 = end_point + (end_point.y * tangent_length, -end_point.x * tangent_length) yield start_point, control_point_1, control_point_2, end_point
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=Vector.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 draw_line_entity(self, entity: DXFGraphic) -> None: d, dxftype = entity.dxf, entity.dxftype() properties = self._resolve_properties(entity) if dxftype == 'LINE': self.out.draw_line(d.start, d.end, properties) elif dxftype in ('XLINE', 'RAY'): start = d.start delta = Vector(d.unit_vector.x, d.unit_vector.y, 0) * INFINITE_LINE_LENGTH if dxftype == 'XLINE': self.out.draw_line(start - delta / 2, start + delta / 2, properties) elif dxftype == 'RAY': self.out.draw_line(start, start + delta, properties) else: raise TypeError(dxftype)
def text2(self, data: bytes): bs = ByteStream(data) start_point = Vector(bs.read_vertex()) normal = Vector(bs.read_vertex()) text_direction = Vector(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_get_start_and_end_vertex(): ellipse = Ellipse.new(handle='ABBA', owner='0', dxfattribs={ 'center': (1, 2, 3), 'major_axis': (4, 3, 0), 'ratio': .7, 'start_param': math.pi / 2, 'end_param': math.pi, 'extrusion': (0, 0, -1), }) start, end = list( ellipse.vertices([ ellipse.dxf.start_param, ellipse.dxf.end_param, ])) # test values from BricsCAD assert start.isclose(Vector(3.1, -0.8, 3), abs_tol=1e-6) assert end.isclose(Vector(-3, -1, 3), abs_tol=1e-6) # for convenience, but Ellipse.vertices is much more efficient: assert ellipse.start_point.isclose(Vector(3.1, -0.8, 3), abs_tol=1e-6) assert ellipse.end_point.isclose(Vector(-3, -1, 3), abs_tol=1e-6)
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 = Vector(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 test_transformation(): axis = Vector.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 transform(self, m: Matrix44) -> 'Bezier4P': """ General transformation interface, returns a new :class:`Bezier4p` curve and it is always a 3D curve. Args: m: 4x4 transformation matrix (:class:`ezdxf.math.Matrix44`) .. versionadded:: 0.14 """ if len(self._control_points[0]) == 2: defpoints = Vector.generate(self._control_points) else: defpoints = self._control_points defpoints = tuple(m.transform_vertices(defpoints)) return Bezier4P(defpoints)
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 = Vector.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 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 = Vector.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 translate( vertices: Iterable['Vertex'], vec: 'Vertex' = (0, 0, 1)) -> Iterable[Vector]: """ Translate `vertices` along `vec`, faster than a Matrix44 transformation. Args: vertices: iterable of vertices vec: translation vector Returns: yields transformed vertices """ vec = Vector(vec) for p in vertices: yield vec + p
def diameter_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 = Vector(x, y).angle_deg msp.add_circle((0, 0), radius=3).transform(ucs.matrix) dim = msp.add_diameter_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_diameter_{dxfversion}_3d.dxf')
def point(self, t: float) -> Vector: """ Get point at distance `t` as Vector(). """ 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] = Vector(x, y) return self._cache[t]
def bulge_to(p1: Vector, p2: Vector, 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 ngon(count: int, length: float = None, radius: float = None, rotation: float = 0., elevation: float = 0., close: bool = False) -> Iterable[Vector]: """ 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 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.Vector` objects """ if count < 3: raise ValueError('Argument `count` has to be greater than 2.') if length is not None: if length <= 0.: raise ValueError('Argument `length` has to be greater than 0.') radius = length / 2. / sin(pi / count) elif radius is not None: if radius <= 0.: raise ValueError('Argument `radius` has to be greater than 0.') else: raise ValueError('Argument `length` or `radius` required.') delta = 2. * pi / count angle = rotation first = None for _ in range(count): v = Vector(radius * cos(angle), radius * sin(angle), elevation) if first is None: first = v yield v angle += delta if close: yield first
def radius_default_inside_horizontal(dxfversion='R2000', delta=10, dimtmove=0): doc = ezdxf.new(dxfversion, setup=True) style = doc.dimstyles.get('EZ_RADIUS_INSIDE') style.dxf.dimtmove = dimtmove msp = doc.modelspace() for x, y in multiple_locations(delta=delta): angle = Vector(x, y).angle_deg msp.add_circle((x, y), radius=3) dim = msp.add_radius_dim(center=(x, y), radius=3, angle=angle, dimstyle='EZ_RADIUS_INSIDE', override={ 'dimtih': 1, # force text inside horizontal }) dim.render(discard=BRICSCAD) doc.set_modelspace_vport(height=3 * delta) doc.saveas(OUTDIR / f'dim_radius_{dxfversion}_default_inside_horizontal_dimtmove_{dimtmove}.dxf')
def test_from_ezdxf_bspline_to_nurbs_python_curve_rational(): bspline = rational_spline_from_arc(center=Vector(0, 0), radius=2, start_angle=0, end_angle=90) # to NURBS-Python curve = bspline.to_nurbs_python_curve() assert curve.degree == 2 assert len(curve.ctrlpts) == 3 assert len(curve.knotvector) == 6 # count + order assert curve.rational is True assert curve.weights == [1.0, 0.7071067811865476, 1.0] # and back to ezdxf spline = BSpline.from_nurbs_python_curve(curve) assert spline.degree == 2 assert len(spline.control_points) == 3 assert len(spline.knots()) == 6 # count + order assert spline.weights() == [1.0, 0.7071067811865476, 1.0]
def user_text_free(dimstyle, x=0, y=0, leader=False): """ User defined dimension text placing. Args: dimstyle: dimstyle to use x: start point x y: start point y leader: use leader line if True """ override = { 'dimdle': 0., 'dimexe': .5, # length of extension line above dimension line 'dimexo': .5, # extension line offset 'dimtfill': 2, # custom text fill 'dimtfillclr': 4 # cyan } base = (x, y + 2) dim = msp.add_linear_dim(base=base, p1=(x, y), p2=(x + 3, y), dimstyle=dimstyle, override=override) # type: DimStyleOverride location = Vector(x + 3, y + 3, 0) dim.set_location(location, leader=leader) dim.render(ucs=ucs, discard=BRICSCAD) add_text([f'usr absolute={location}', f'leader={leader}'], insert=Vector(x, y)) x += 4 dim = msp.add_linear_dim(base=base, p1=(x, y), p2=(x + 3, y), dimstyle=dimstyle, override=override) # type: DimStyleOverride relative = Vector(-1, +1) # relative to dimline center dim.set_location(relative, leader=leader, relative=True) dim.render(ucs=ucs, discard=BRICSCAD) add_text([f'usr relative={relative}', f'leader={leader}'], insert=Vector(x, y)) x += 4 dim = msp.add_linear_dim(base=base, p1=(x, y), p2=(x + 3, y), dimstyle=dimstyle, override=override) # type: DimStyleOverride dh = -.7 dv = 1.5 dim.shift_text(dh, dv) dim.render(ucs=ucs, discard=BRICSCAD) add_text([f'shift text=({dh}, {dv})', ], insert=Vector(x, y)) override['dimtix'] = 1 # force text inside x += 4 dim = msp.add_linear_dim(base=base, p1=(x, y), p2=(x + .3, y), dimstyle=dimstyle, override=override) # type: DimStyleOverride dh = 0 dv = 1 dim.shift_text(dh, dv) dim.render(ucs=ucs, discard=BRICSCAD) add_text([f'shift text=({dh}, {dv})', ], insert=Vector(x, y))
def test_intersection_ray_ray_3d(): ray1 = (Vector(0, 0, 0), Vector(1, 0, 0)) ray2 = (Vector(0, 0, 0), Vector(0, 0, 1)) # parallel rays return a 0-tuple result = intersection_ray_ray_3d(ray1, ray1) assert len(result) == 0 assert bool(result) is False # intersecting rays return a 1-tuple result = intersection_ray_ray_3d(ray1, ray2) assert len(result) == 1 assert bool(result) is True assert result == (Vector(0, 0, 0), ) # not intersecting and not parallel rays return a 2-tuple line3 = (Vector(0, 0, 1), Vector(0, 1, 1)) result = intersection_ray_ray_3d(ray1, line3) assert len(result) == 2 assert bool(result) is True # returns points of closest approach on each ray assert Vector(0, 0, 1) in result assert Vector(0, 0, 0) in result
def test_basis_vector_N_ip(): degree = 3 fit_points = Vector.list(POINTS2) # data points D n = len(fit_points) - 1 t_vector = list(uniform_t_vector(fit_points)) knots = list(control_frame_knots(n, degree, t_vector)) should_count = len(fit_points) - 2 # target control point count h = should_count - 1 spline = Basis(knots, order=degree + 1, count=len(fit_points)) matrix_N = [spline.basis(t) for t in t_vector] for k in range(1, n): basis_vector = bspline_basis_vector(u=t_vector[k], count=len(fit_points), degree=degree, knots=knots) for i in range(1, h): assert isclose(matrix_N[k][i], basis_vector[i])
def ellipse(major_axis=(1, 0), ratio: float = 0.5, start: float = 0, end: float = math.tau, count: int = 8): major_axis = Vector(major_axis).replace(z=0) ellipse_ = Ellipse.new(dxfattribs={ 'center': (0, 0, 0), 'major_axis': major_axis, 'ratio': min(max(ratio, 1e-6), 1), 'start_param': start, 'end_param': end }, doc=doc) control_vertices = list(ellipse_.vertices(ellipse_.params(count))) axis_vertices = list( ellipse_.vertices([0, math.pi / 2, math.pi, math.pi * 1.5])) return ellipse_, control_vertices, axis_vertices
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 add_vertices(self, vertices: Iterable['Vertex']) -> Sequence[int]: """ Add new vertices to the mesh, each vertex is a ``(x, y, z)`` tuple or a :class:`~ezdxf.math.Vector` object, returns the indices of the `vertices` added to the :attr:`vertices` list. e.g. adding 4 vertices to an empty mesh, returns the indices ``(0, 1, 2, 3)``, adding additional 4 vertices returns the indices ``(4, 5, 6, 7)``. Args: vertices: list of vertices, vertex as ``(x, y, z)`` tuple or :class:`~ezdxf.math.Vector` objects Returns: tuple: indices of the `vertices` added to the :attr:`vertices` list """ start_index = len(self.vertices) self.vertices.extend(Vector.generate(vertices)) return tuple(range(start_index, len(self.vertices)))
def test_arc_from_2p_radius(): p1 = (2, 1) p2 = (0, 3) radius = 2 arc = ConstructionArc.from_2p_radius(start_point=p1, end_point=p2, radius=radius) assert arc.center == (0, 1) assert isclose(arc.radius, radius) assert isclose(arc.start_angle, 0) assert isclose(arc.end_angle, 90) arc = ConstructionArc.from_2p_radius(start_point=p2, end_point=p1, radius=radius) assert arc.center == Vector(2, 3) assert isclose(arc.radius, radius) assert isclose(arc.start_angle, 180) assert isclose(arc.end_angle, -90)
def add_measurement_text(self, dim_text: str, pos: Vec2, rotation: float) -> None: """ Add measurement text to dimension BLOCK. Args: dim_text: dimension text pos: text location rotation: text rotation in degrees """ attribs = { 'color': self.text_color, } self.add_text(dim_text, pos=Vector(pos), rotation=rotation, dxfattribs=attribs)