def _append(self, point: Vec2, normal: Vec2, width: float) -> None: """ Add a curve trace station (like a vertex) at location `point`. Args: point: 2D curve location (vertex), z-axis of 3D vertices is ignored. normal: curve normal width: width of station """ if _NULLVEC2.isclose(normal): normal = _NULLVEC2 else: normal = normal.normalize(width / 2) self._stations.append(CurveStation(point + normal, point - normal))
def __init__(self, center: 'Vertex' = (0, 0), radius: float = 1, start_angle: float = 0, end_angle: float = 360, is_counter_clockwise: bool = True): self.center = Vec2(center) self.radius = radius if is_counter_clockwise: self.start_angle = start_angle self.end_angle = end_angle else: self.start_angle = end_angle self.end_angle = start_angle
def apply_text_shift(self, location: Vec2, text_rotation: float) -> Vec2: """ Add `self.text_shift_h` and `sel.text_shift_v` to point `location`, shifting along and perpendicular to text orientation defined by `text_rotation` Args: location: location point text_rotation: text rotation in degrees Returns: new location """ shift_vec = Vec2((self.text_shift_h, self.text_shift_v)) location += shift_vec.rotate(text_rotation) return location
def render(self, layout: 'GenericLayoutType', dxfattribs: dict = None): dxfattribs['closed'] = True center = self.shape[0] d = Vec2((self.radius / 2, 0)) p1 = center - d p2 = center + d if layout.dxfversion > 'AC1009': dxfattribs['const_width'] = self.radius layout.add_lwpolyline([(p1, 1), (p2, 1)], format='vb', dxfattribs=dxfattribs) else: dxfattribs['default_start_width'] = self.radius dxfattribs['default_end_width'] = self.radius polyline = layout.add_polyline2d(points=[p1, p2], dxfattribs=dxfattribs) polyline[0].dxf.bulge = 1 polyline[1].dxf.bulge = 1
def add_horiz_ext_line_default(self, start: "Vertex") -> None: """Add horizontal outside extension line from start for default locations. """ attribs = self.dimension_line.dxfattribs() self.add_line(start, self.outside_default_defpoint, dxfattribs=attribs) if self.measurement.vertical_placement == 0: hdist = self.arrows.arrow_size else: hdist = self._total_text_width angle = self.dim_line_angle % 360.0 # normalize 0 .. 360 if 90 < angle <= 270: hdist = -hdist end = self.outside_default_defpoint + Vec2((hdist, 0)) self.add_line(self.outside_default_defpoint, end, dxfattribs=attribs)
def test_intersecting_zig_zag_lines(self): pline1 = Vec2.list([(0, 0), (2, 2), (4, 0), (6, 2), (8, 0)]) pline2 = Vec2.list([(0, 2), (2, 0), (4, 2), (6, 0), (8, 2)]) res = intersect_polylines_2d(pline1, pline2) assert len(res) == 4 res.sort() # do not rely on any order assert res[0].isclose(Vec2(1, 1)) assert res[1].isclose(Vec2(3, 1)) assert res[2].isclose(Vec2(5, 1)) assert res[3].isclose(Vec2(7, 1))
def _better(self, pk1, pk2, center: float, tar_ang: float): """ 求解center至边线夹角更接近tar_ang的位置 Args: pk1 (float): 桩号1 pk2 (float): 桩号2 center (float): 中心点桩号 tar_ang: 弧度,目标夹角,左侧为逆时针小于pi,右侧大于pi Returns: """ norm = Vec2(*self.get_direction(center)) ccpt = Vec2(*self.get_coordinate(center)) pkm = (pk1 + pk2) * 0.5 wl1, wr1 = self.get_width(pk1) wlm, wrm = self.get_width(pkm) wl2, wr2 = self.get_width(pk2) if tar_ang <= pi: # 左侧目标角度 find_dir = 0.5 * pi w1 = wl1 wm = wlm w2 = wl2 else: find_dir = -0.5 * pi w1 = wr1 wm = wrm w2 = wr2 pt1 = Vec2(*self.get_coordinate(pk1)) + Vec2(*self.get_direction(pk1)).rotate(find_dir) * w1 ptm = Vec2(*self.get_coordinate(pkm)) + Vec2(*self.get_direction(pkm)).rotate(find_dir) * wm pt2 = Vec2(*self.get_coordinate(pk2)) + Vec2(*self.get_direction(pk2)).rotate(find_dir) * w2 val1 = signed_angle_between(norm, pt1 - ccpt) valm = signed_angle_between(norm, ptm - ccpt) val2 = signed_angle_between(norm, pt2 - ccpt) if (val1 - tar_ang) * (val2 - tar_ang) < 0: if (val1 - tar_ang) * (valm - tar_ang) < 0: return pk1, pkm else: return pkm, pk2 else: return None
def test_tangents(): angles = [0, 45, 90, 135, -45, -90, -135, 180] sin45 = math.sin(math.pi / 4) result = [ (0, 1), (-sin45, sin45), (-1, 0), (-sin45, -sin45), (sin45, sin45), (1, 0), (sin45, -sin45), (0, -1), ] arc = ConstructionArc(center=(1, 1)) vertices = list(arc.tangents(angles)) for v, r in zip(vertices, result): assert v.isclose(Vec2(r))
def __init__(self, insert: Vertex, size: float = 1.0, angle: float = 0): # shape = [lower_left, lower_right, upper_right, upper_left, connection point] s2 = size / 2 super().__init__([ Vec2((-s2, -s2)), Vec2((+s2, -s2)), Vec2((+s2, +s2)), Vec2((-s2, +s2)), Vec2((-s2, 0)), Vec2((-size, 0)), ]) self.place(insert, angle)
def test_two_angled_faces(): t = LinearTrace() t.add_station((0, 0), 1, 0.5) t.add_station((2, 0), 1, 1) t.add_station((4, 2), 1, 1) face1, face2 = list(t.faces()) assert face1[0].isclose(Vec2(0, +0.5)) assert face1[1].isclose(Vec2(0, -0.5)) assert face1[2].isclose(Vec2(2.5224077499274835, -0.18469903125906456)) assert face1[3].isclose(Vec2(1.5936828611675133, 0.3007896423540608)) assert face2[2].isclose(Vec2(4.353553390593274, 1.6464466094067263)) assert face2[3].isclose(Vec2(3.646446609406726, 2.353553390593274))
def default_text_location(self) -> Vec2: """Calculate default text location in UCS based on `self.text_halign`, `self.text_valign` and `self.text_outside` """ start = self.dim_line_start end = self.dim_line_end measurement = self.measurement halign = measurement.text_halign # positions the text above and aligned with the first/second extension line ext_lines = self.extension_lines if halign in (3, 4): # horizontal location hdist = measurement.text_gap + measurement.text_height / 2.0 hvec = self.dim_line_vec * hdist location = (start if halign == 3 else end) - hvec # vertical location vdist = ext_lines.extension_above + self._total_text_width / 2.0 location += Vec2.from_deg_angle( self.ext_line_angle).normalize(vdist) else: # relocate outside text to center location if measurement.text_is_outside: halign = 0 if halign == 0: location = self.dim_line_center # center of dimension line else: hdist = (self._total_text_width / 2.0 + self.arrows.arrow_size + measurement.text_gap) if (halign == 1 ): # positions the text next to the first extension line location = start + (self.dim_line_vec * hdist) else: # positions the text next to the second extension line location = end - (self.dim_line_vec * hdist) if measurement.text_is_outside: # move text up vdist = (ext_lines.extension_above + measurement.text_gap + measurement.text_height / 2.0) else: # distance from extension line to text midpoint vdist = measurement.text_vertical_distance() location += self.dim_line_vec.orthogonal().normalize(vdist) return location
def add_arc( self, center: "Vertex", radius: float = 1.0, start_angle: float = 0.0, end_angle: float = 360.0, ccw: bool = True, ) -> "ArcEdge": """Add an :class:`ArcEdge`. **Adding Clockwise Oriented Arcs:** Clockwise oriented :class:`ArcEdge` objects are sometimes necessary to build closed loops, but the :class:`ArcEdge` objects are always represented in counter-clockwise orientation. To add a clockwise oriented :class:`ArcEdge` you have to swap the start- and end angle and set the `ccw` flag to ``False``, e.g. to add a clockwise oriented :class:`ArcEdge` from 180 to 90 degree, add the :class:`ArcEdge` in counter-clockwise orientation with swapped angles:: edge_path.add_arc(center, radius, start_angle=90, end_angle=180, ccw=False) Args: center: center point of arc, (x, y)-tuple radius: radius of circle start_angle: start angle of arc in degrees (`end_angle` for a clockwise oriented arc) end_angle: end angle of arc in degrees (`start_angle` for a clockwise oriented arc) ccw: ``True`` for counter clockwise ``False`` for clockwise orientation """ arc = ArcEdge() arc.center = Vec2(center) arc.radius = radius # Start- and end angles always for counter-clockwise oriented arcs! arc.start_angle = start_angle arc.end_angle = end_angle # Flag to export the counter-clockwise oriented arc in # correct clockwise orientation: arc.ccw = bool(ccw) self.edges.append(arc) return arc
def add_text(self, text: str, pos: Vector, rotation: float, dxfattribs: dict = None) -> None: """ Add TEXT (DXF R12) or MTEXT (DXF R2000+) entity to the dimension BLOCK. Args: text: text as string pos: insertion location in UCS rotation: rotation angle in degrees in UCS (x-axis is 0 degrees) dxfattribs: additional or overridden DXF attributes """ attribs = self.default_attributes() attribs['style'] = self.text_style_name attribs['color'] = self.text_color if self.requires_extrusion: attribs['extrusion'] = self.ucs.uz if self.supports_dxf_r2000: text_direction = self.ucs.to_wcs( Vec2.from_deg_angle(rotation)) - self.ucs.origin attribs['text_direction'] = text_direction attribs['char_height'] = self.text_height attribs['insert'] = self.wcs(pos) attribs['attachment_point'] = self.text_attachment_point if self.supports_dxf_r2007: if self.text_fill: attribs['box_fill_scale'] = self.text_box_fill_scale attribs['bg_fill_color'] = self.text_fill_color attribs['bg_fill'] = 3 if self.text_fill == 1 else 1 if dxfattribs: attribs.update(dxfattribs) self.block.add_mtext(text, dxfattribs=attribs) else: attribs['rotation'] = self.ucs.to_ocs_angle_deg(rotation) attribs['height'] = self.text_height if dxfattribs: attribs.update(dxfattribs) dxftext = self.block.add_text(text, dxfattribs=attribs) dxftext.set_pos(self.ocs(pos), align='MIDDLE_CENTER')
def test_two_single_paths(self): p1 = path.Path() p1.line_to((4, 5, 6)) p2 = path.Path() p2.line_to((7, 8, 6)) mpath = path.to_matplotlib_path([p1, p2]) assert tuple(mpath.codes) == ( MC.MOVETO, MC.LINETO, MC.MOVETO, MC.LINETO, ) assert Vec2.list(mpath.vertices) == [ (0, 0), (4, 5), (0, 0), (7, 8), ]
def location_override(self, location: 'Vertex', leader=False, relative=False) -> None: """ Set user defined dimension text location. ezdxf defines a user defined location per definition as 'outside'. Args: location: text midpoint leader: use leader or not (movement rules) relative: is location absolute (in UCS) or relative to dimension line center. """ self.dim_style.set_location(location, leader, relative) self.user_location = Vec2(location) self.text_movement_rule = 1 if leader else 2 self.relative_user_location = relative self.text_outside = True
def get_default_text_location(self) -> Vec2: """Returns default text midpoint based on `text_valign` and `text_outside`. """ measurement = self.measurement if measurement.text_is_outside and measurement.text_outside_horizontal: return super().get_default_text_location() text_direction = Vec2.from_deg_angle(measurement.text_rotation) vertical_direction = text_direction.orthogonal(ccw=True) vertical_distance = measurement.text_vertical_distance() if measurement.text_is_inside: text_midpoint = self.center else: hdist = (self._total_text_width / 2.0 + self.arrows.arrow_size + measurement.text_gap) text_midpoint = self.point_on_circle + (self.dim_line_vec * hdist) return text_midpoint + (vertical_direction * vertical_distance)
def set_boundary_path(self, vertices: Iterable["Vertex"]) -> None: """Set boundary path to `vertices`. Two vertices describe a rectangle (lower left and upper right corner), more than two vertices is a polygon as clipping path. """ _vertices = Vec2.list(vertices) if len(_vertices): if len(_vertices) > 2 and not _vertices[-1].isclose(_vertices[0]): # Close path, otherwise AutoCAD crashes _vertices.append(_vertices[0]) self._boundary_path = _vertices self.set_flag_state(self.USE_CLIPPING_BOUNDARY, state=True) self.dxf.clipping = 1 self.dxf.clipping_boundary_type = 1 if len(_vertices) < 3 else 2 self.dxf.count_boundary_points = len(self._boundary_path) else: self.reset_boundary_path()
def set_masking_area(self, vertices: Iterable["Vertex"]) -> None: """Set a new masking area, the area is placed in the layout xy-plane.""" self.update_dxf_attribs(self.DEFAULT_ATTRIBS) vertices = Vec2.list(vertices) bounds = BoundingBox2d(vertices) x_size, y_size = bounds.size dxf = self.dxf dxf.insert = Vec3(bounds.extmin) dxf.u_pixel = Vec3(x_size, 0, 0) dxf.v_pixel = Vec3(0, y_size, 0) def boundary_path(): extmin = bounds.extmin for vertex in vertices: v = vertex - extmin yield Vec2(v.x / x_size - 0.5, 0.5 - v.y / y_size) self.set_boundary_path(boundary_path())
def test_closed_linear_path(): t = LinearTrace() t.add_station((0, 0), 1, 1) t.add_station((1, 0), 1, 1) t.add_station((1, 1), 1, 1) t.add_station((0, 1), 1, 1) t.add_station((0, 0), 1, 1) faces = list(t.faces()) assert len(faces) == 4 assert faces[0] == ( Vec2(0.5, 0.5), Vec2(-0.5, -0.5), Vec2(1.5, -0.5), Vec2(0.5, 0.5), ) assert faces[3] == ( Vec2(0.5, 0.5), Vec2(-0.5, 1.5), Vec2(-0.5, -0.5), Vec2(0.5, 0.5), )
def test_one_multi_path(self): p = path.Path() p.line_to((4, 5, 6)) p.move_to((0, 0, 0)) p.line_to((7, 8, 9)) mpath = path.to_matplotlib_path([p]) assert tuple(mpath.codes) == ( MC.MOVETO, MC.LINETO, MC.MOVETO, MC.LINETO, ) assert Vec2.list(mpath.vertices) == [ (0, 0), (4, 5), (0, 0), (7, 8), ]
def is_inside(self, point: "Vertex") -> bool: """Returns ``True`` if `point` is inside of box.""" point = Vec2(point) delta = self.center - point if abs(self.angle) < ABS_TOL: # fast path for horizontal rectangles return abs(delta.x) <= (self._width / 2.0) and abs( delta.y) <= (self._height / 2.0) else: distance = delta.magnitude if distance > self.circumcircle_radius: return False elif distance <= self.incircle_radius: return True else: # inside if point is "left of line" of all border lines. p1, p2, p3, p4 = self.corners return all( (point_to_line_relation(point, a, b) < 1 for a, b in [(p1, p2), (p2, p3), (p3, p4), (p4, p1)]))
def build_edge_path(hatch: Hatch, path: Path, flags: int): if path.has_curves: # Edge path with LINE and SPLINE edges edge_path = hatch.paths.add_edge_path(flags) for edge in to_bsplines_and_vertices(path, g1_tol=g1_tol): if isinstance(edge, BSpline): edge_path.add_spline( control_points=edge.control_points, degree=edge.degree, knot_values=edge.knots(), ) else: # add LINE edges prev = edge[0] for p in edge[1:]: edge_path.add_line(prev, p) prev = p else: # Polyline boundary path hatch.paths.add_polyline_path(Vec2.generate( path.flattening(distance, segments)), flags=flags)
def measure_fixed_angle(msp, angle: float): x_dist = 15 radius = 3 distance = 1 delta = angle / 2.0 for dimtad, y_dist in [[0, 0], [1, 20], [4, 40]]: for count in range(8): center = Vec2(x_dist * count, y_dist) main_angle = 45.0 * count start_angle = main_angle - delta end_angle = main_angle + delta yield msp.add_angular_dim_cra( center, radius, start_angle, end_angle, distance, override={"dimtad": dimtad}, )
def test_dimension_line_divided_by_measurement_text(doc: Drawing, s, e): """Vertical centered measurement text should hide the part of the dimension line beneath the text. This creates two arcs instead of one. """ msp = doc.modelspace() dim = msp.add_angular_dim_cra( center=Vec2(), radius=5, start_angle=s, end_angle=e, distance=2, override={"dimtad": 0}, # vertical centered text ) dim.render() arcs = dim.dimension.get_geometry_block().query("ARC") assert len(arcs) == 2 assert sum( arc_angle_span_deg(arc.dxf.start_angle, arc.dxf.end_angle) for arc in arcs) < arc_angle_span_deg( s, e), "sum of visual arcs should be smaller than the full arc"
def to_spline_edge(e: EllipseEdge) -> SplineEdge: # No OCS transformation needed, source ellipse and target spline # reside in the same OCS. ellipse = ConstructionEllipse( center=e.center, major_axis=e.major_axis, ratio=e.ratio, start_param=e.start_param, end_param=e.end_param, ) count = max(int(float(num) * ellipse.param_span / math.tau), 3) tool = BSpline.ellipse_approximation(ellipse, count) spline = SplineEdge() spline.degree = tool.degree if not e.ccw: tool = tool.reverse() spline.control_points = Vec2.list(tool.control_points) spline.knot_values = tool.knots() # type: ignore spline.weights = tool.weights() # type: ignore return spline
def add_dimension_line(self, start: Vec2, end: Vec2) -> None: """Add dimension line to dimension BLOCK, adds extension DIMDLE if required, and uses DIMSD1 or DIMSD2 to suppress first or second part of dimension line. Removes line parts hidden by dimension text. Args: start: dimension line start end: dimension line end """ dim_line = self.dimension_line arrows = self.arrows extension = self.dim_line_vec * dim_line.extension ticks = arrows.has_ticks if ticks or ARROWS.has_extension_line(arrows.arrow1_name): start = start - extension if ticks or ARROWS.has_extension_line(arrows.arrow2_name): end = end + extension attribs = dim_line.dxfattribs() if dim_line.suppress1 or dim_line.suppress2: # TODO: results not as expected, but good enough # center should take into account text location center = start.lerp(end) if not dim_line.suppress1: self.add_line(start, center, dxfattribs=attribs, remove_hidden_lines=True) if not dim_line.suppress2: self.add_line(center, end, dxfattribs=attribs, remove_hidden_lines=True) else: self.add_line(start, end, dxfattribs=attribs, remove_hidden_lines=True)
def _edges(points) -> Iterable[Union[LineEdge, ArcEdge]]: prev_point = None prev_bulge = None for x, y, bulge in points: point = Vec3(x, y) if prev_point is None: prev_point = point prev_bulge = bulge continue if prev_bulge != 0: arc = ArcEdge() # bulge_to_arc returns always counter-clockwise oriented # start- and end angles: ( arc.center, start_angle, end_angle, arc.radius, ) = bulge_to_arc(prev_point, point, prev_bulge) chk_point = arc.center + Vec2.from_angle( start_angle, arc.radius ) arc.ccw = chk_point.isclose(prev_point, abs_tol=1e-9) arc.start_angle = math.degrees(start_angle) % 360.0 arc.end_angle = math.degrees(end_angle) % 360.0 if math.isclose( arc.start_angle, arc.end_angle ) and math.isclose(arc.start_angle, 0): arc.end_angle = 360.0 yield arc else: line = LineEdge() line.start = (prev_point.x, prev_point.y) line.end = (point.x, point.y) yield line prev_point = point prev_bulge = bulge
def render(self, layout: "GenericLayoutType", dxfattribs: dict = None): center = self.shape[0] d = Vec2((self.radius / 2, 0)) p1 = center - d p2 = center + d dxfattribs = dxfattribs or {} if layout.dxfversion > "AC1009": dxfattribs["const_width"] = self.radius layout.add_lwpolyline( [(p1, 1), (p2, 1)], format="vb", close=True, dxfattribs=dxfattribs, ) else: dxfattribs["default_start_width"] = self.radius dxfattribs["default_end_width"] = self.radius polyline = layout.add_polyline2d(points=[p1, p2], close=True, dxfattribs=dxfattribs) polyline[0].dxf.bulge = 1 polyline[1].dxf.bulge = 1
def convex_hull_2d(points: Iterable['Vertex']) -> List['Vertex']: """ Returns 2D convex hull for `points`. Args: points: iterable of points as :class:`Vec3` compatible objects, z-axis is ignored """ def _convexhull(hull): while len(hull) > 2: # the last three points start_point, check_point, destination_point = hull[-3:] # curve not turns right if not is_point_left_of_line(check_point, start_point, destination_point): # remove the penultimate point del hull[-2] else: break return hull points = sorted(set(Vec2.generate(points))) # remove duplicate points if len(points) < 3: raise ValueError( "Convex hull calculation requires 3 or more unique points.") upper_hull = points[:2] # first two points for next_point in points[2:]: upper_hull.append(next_point) upper_hull = _convexhull(upper_hull) lower_hull = [points[-1], points[-2]] # last two points for next_point in reversed(points[:-2]): lower_hull.append(next_point) lower_hull = _convexhull(lower_hull) upper_hull.extend(lower_hull[1:-1]) return upper_hull
def intersect(self, other: "ConstructionRay") -> Vec2: """Returns the intersection point as ``(x, y)`` tuple of `self` and `other`. Raises: ParallelRaysError: if rays are parallel """ ray1 = self ray2 = other if ray1.is_parallel(ray2): raise ParallelRaysError("Rays are parallel") if ray1._is_vertical: x = ray1._location.x if ray2.is_horizontal: y = ray2._location.y else: y = ray2.yof(x) elif ray2._is_vertical: x = ray2._location.x if ray1.is_horizontal: y = ray1._location.y else: y = ray1.yof(x) elif ray1._is_horizontal: y = ray1._location.y x = ray2.xof(y) elif ray2._is_horizontal: y = ray2._location.y x = ray1.xof(y) else: # calc intersection with the 'straight-line-equation' # based on y(x) = y0 + x*slope # guards above guarantee that no slope is None x = (ray1._yof0 - ray2._yof0) / (ray2._slope - ray1._slope) # type: ignore y = ray1.yof(x) return Vec2((x, y))