def __init__(self, p1: "Vertex", p2: "Vertex" = None, angle: float = None): self._location = Vec2(p1) self._angle: Optional[float] self._slope: Optional[float] self._yof0: Optional[float] self._direction: Vec2 self._is_vertical: bool self._is_horizontal: bool if p2 is not None: p2 = Vec2(p2) if self._location.x < p2.x: self._direction = (p2 - self._location).normalize() else: self._direction = (self._location - p2).normalize() self._angle = self._direction.angle elif angle is not None: self._angle = angle self._direction = Vec2.from_angle(angle) else: raise ValueError("p2 or angle required.") if abs(self._direction.x) <= ABS_TOL: self._slope = None self._yof0 = None else: self._slope = self._direction.y / self._direction.x self._yof0 = self._location.y - self._slope * self._location.x self._is_vertical = self._slope is None self._is_horizontal = abs(self._direction.y) <= ABS_TOL
def random_2d_path(steps: int = 100, max_step_size: float = 1.0, max_heading: float = math.pi / 2, retarget: int = 20) -> Iterable[Vec2]: """ Returns a random 2D path as iterable of :class:`~ezdxf.math.Vec2` objects. Args: steps: count of vertices to generate max_step_size: max step size max_heading: limit heading angle change per step to ± max_heading/2 in radians retarget: specifies steps before changing global walking target """ max_ = max_step_size * steps def next_global_target(): return Vec2((rnd(max_), rnd(max_))) walker = Vec2(0, 0) target = next_global_target() for i in range(steps): if i % retarget == 0: target = target + next_global_target() angle = (target - walker).angle heading = angle + rnd_perlin(max_heading, walker) length = max_step_size * random.random() walker = walker + Vec2.from_angle(heading, length) yield walker
def __init__(self, p1: 'Vertex', p2: 'Vertex' = None, angle: float = None): self._location = Vec2(p1) if p2 is not None: p2 = Vec2(p2) if self._location.x < p2.x: self._direction = (p2 - self._location).normalize() else: self._direction = (self._location - p2).normalize() self._angle = self._direction.angle elif angle is not None: self._angle = angle self._direction = Vec2.from_angle(angle) else: raise ValueError('p2 or angle required.') if math.isclose(self._direction.x, 0., abs_tol=1e-12): self._slope = None self._yof0 = None else: self._slope = self._direction.y / self._direction.x self._yof0 = self._location.y - self._slope * self._location.x self._is_vertical = self._slope is None self._is_horizontal = math.isclose(self._direction.y, 0., abs_tol=1e-12)
def point_at(self, angle: float) -> Vec2: """ Returns point on circle at `angle` as :class:`Vec2` object. Args: angle: angle in radians """ return self.center + Vec2.from_angle(angle, self.radius)
def main_axis_points(self): center = self.center radius = self.radius start = math.radians(self.start_angle) end = math.radians(self.end_angle) for angle in QUARTER_ANGLES: if enclosing_angles(angle, start, end): yield center + Vec2.from_angle(angle, radius)
def point_at(self, angle: float) -> Vec2: """Returns point on circle at `angle` as :class:`Vec2` object. Args: angle: angle in radians, angle goes counter clockwise around the z-axis, x-axis = 0 deg. """ return self.center + Vec2.from_angle(angle, self.radius)
def polar(p: Any, angle: float, distance: float) -> Vec2: """ Returns the point at a specified `angle` and `distance` from point `p`. Args: p: point as :class:`Vec2` compatible object angle: angle in radians distance: distance """ return Vec2(p) + Vec2.from_angle(angle, distance)
def vertices(self, angles: Iterable[float]) -> Iterable[Vec2]: """Yields vertices of the circle for iterable `angles`. Args: angles: iterable of angles as radians, angle goes counter clockwise around the z-axis, x-axis = 0 deg. .. versionadded:: 0.17.1 """ center = self.center radius = self.radius for angle in angles: yield center + Vec2.from_angle(angle, radius)
def bulge_center(start_point: 'Vertex', end_point: 'Vertex', bulge: float) -> 'Vec2': """ Returns center of arc described by the given bulge parameters. Based on Bulge Center by `Lee Mac`_. Args: start_point: start point as :class:`Vec2` compatible object end_point: end point as :class:`Vec2` compatible object bulge: bulge value as float """ start_point = Vec2(start_point) a = angle(start_point, end_point) + (math.pi / 2. - math.atan(bulge) * 2.) return start_point + Vec2.from_angle( a, signed_bulge_radius(start_point, end_point, bulge))
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 __init__( self, dimension: Dimension, ucs: "UCS" = None, override: DimStyleOverride = None, ): # The local coordinate system is defined by origin and the # horizontal_direction in OCS: self.origin_ocs: Vec2 = get_required_defpoint(dimension, "defpoint") self.feature_location_ocs: Vec2 = get_required_defpoint( dimension, "defpoint2") self.end_of_leader_ocs: Vec2 = get_required_defpoint( dimension, "defpoint3") # Horizontal direction in clockwise orientation, see DXF reference # for group code 51: self.horizontal_dir = -dimension.dxf.get("horizontal_direction", 0.0) self.rotation = math.radians(self.horizontal_dir) self.local_x_axis = Vec2.from_angle(self.rotation) self.local_y_axis = self.local_x_axis.orthogonal() self.x_type = bool( # x-type is set! dimension.dxf.get("dimtype", 0) & const.DIM_ORDINATE_TYPE) super().__init__(dimension, ucs, override) # Measurement directions can be opposite to local x- or y-axis self.leader_vec_ocs = self.end_of_leader_ocs - self.feature_location_ocs leader_x_vec = self.local_x_axis.project(self.leader_vec_ocs) leader_y_vec = self.local_y_axis.project(self.leader_vec_ocs) try: self.measurement_direction: Vec2 = leader_x_vec.normalize() except ZeroDivisionError: self.measurement_direction = Vec2(1, 0) try: self.measurement_orthogonal: Vec2 = leader_y_vec.normalize() except ZeroDivisionError: self.measurement_orthogonal = Vec2(0, 1) if not self.x_type: self.measurement_direction, self.measurement_orthogonal = ( self.measurement_orthogonal, self.measurement_direction, ) self.update_measurement() if self.tol.has_limits: self.tol.update_limits(self.measurement.value) # Text width and -height is required first, text location and -rotation # are not valid yet: self.text_box = self.init_text_box() # Set text location and rotation: self.measurement.text_location = self.get_default_text_location() self.measurement.text_rotation = self.get_default_text_rotation() # Update text box location and -rotation: self.text_box.center = self.measurement.text_location self.text_box.angle = self.measurement.text_rotation self.geometry.set_text_box(self.text_box) # Update final text location in the DIMENSION entity: self.dimension.dxf.text_midpoint = self.measurement.text_location
def setup_text_location(self) -> None: """Setup geometric text properties (location, rotation) and the TextBox object. """ # dimtix: measurement.force_text_inside is ignored # dimtih: measurement.text_inside_horizontal is ignored # dimtoh: measurement.text_outside_horizontal is ignored # text radial direction = center -> text text_radial_dir: Vec2 # text "vertical" direction measurement = self.measurement # determine text location: at_default_location: bool = measurement.user_location is None has_text_shifting: bool = bool(measurement.text_shift_h or measurement.text_shift_v) if at_default_location: # place text in the "horizontal" center of the dimension line at the # default location defined by measurement.text_valign (dimtad): text_radial_dir = Vec2.from_angle(self.center_angle_rad) shift_text_upwards: float = 0.0 if measurement.text_is_outside: # reset vertical alignment to "above" measurement.text_valign = 1 if measurement.is_wide_text: # move measurement text "above" the extension line endings: shift_text_upwards = self.extension_lines.extension_above measurement.text_location = self.default_location( shift=shift_text_upwards) if (measurement.text_valign > 0 and not has_text_shifting ): # not in the center and no text shifting is applied # disable expensive hidden line calculation self.remove_hidden_lines_of_dimline = False else: # apply dimtmove: measurement.text_movement_rule user_location = measurement.user_location assert isinstance(user_location, Vec2) if measurement.relative_user_location: user_location += self.dim_midpoint measurement.text_location = user_location if measurement.text_movement_rule == 0: # Moves the dimension line with dimension text and # aligns the text direction perpendicular to the connection # line from the arc center to the text center: self.dim_line_radius = (self.center_of_arc - user_location).magnitude # Attributes about the text and arrow fitting have to be # updated now: self.setup_text_and_arrow_fitting() elif measurement.text_movement_rule == 1: # Adds a leader when dimension text, text direction is # "horizontal" or user text rotation if given. # Leader location is defined by dimtad (text_valign): # "center" - connects to the left or right center of the text # "below" - add a line below the text if measurement.user_text_rotation is None: # override text rotation measurement.user_text_rotation = 0.0 measurement.text_is_outside = True # by definition elif measurement.text_movement_rule == 2: # Allows text to be moved freely without a leader and # aligns the text direction perpendicular to the connection # line from the arc center to the text center: measurement.text_is_outside = True # by definition text_radial_dir = (measurement.text_location - self.center_of_arc).normalize() # set text "horizontal": text_tangential_dir = text_radial_dir.orthogonal(ccw=False) if at_default_location and has_text_shifting: # Apply text relative shift (ezdxf only feature) if measurement.text_shift_h: measurement.text_location += (text_tangential_dir * measurement.text_shift_h) if measurement.text_shift_v: measurement.text_location += (text_radial_dir * measurement.text_shift_v) # apply user text rotation; rotation in degrees: if measurement.user_text_rotation is None: rotation = text_tangential_dir.angle_deg else: rotation = measurement.user_text_rotation if not self.geometry.requires_extrusion: # todo: extrusion vector (0, 0, -1)? # Practically all DIMENSION entities are 2D entities, # where OCS == WCS, check WCS text orientation: wcs_angle = self.geometry.ucs.to_ocs_angle_deg(rotation) if is_upside_down_text_angle(wcs_angle): measurement.has_upside_down_correction = True rotation += 180.0 # apply to UCS rotation! measurement.text_rotation = rotation
def default_location(self, shift: float = 0.0) -> Vec2: radius = (self.dim_line_radius + self.measurement.text_vertical_distance() + shift) text_radial_dir = Vec2.from_angle(self.center_angle_rad) return self.center_of_arc + text_radial_dir * radius
def dim_midpoint(self) -> Vec2: """Return the midpoint of the dimension line.""" return self.center_of_arc + Vec2.from_angle(self.center_angle_rad, self.dim_line_radius)
def __init__(self, dimension: 'Dimension', ucs: 'UCS' = None, override: 'DimStyleOverride' = None): super().__init__(dimension, ucs, override) if self.text_movement_rule == 0: # moves the dimension line with dimension text, this makes no sense for ezdxf (just set `base` argument) self.text_movement_rule = 2 self.oblique_angle = self.dimension.get_dxf_attrib( 'oblique_angle', 90) # type: float self.dim_line_angle = self.dimension.get_dxf_attrib('angle', 0) # type: float self.dim_line_angle_rad = math.radians( self.dim_line_angle) # type: float self.ext_line_angle = self.dim_line_angle + self.oblique_angle # type: float self.ext_line_angle_rad = math.radians( self.ext_line_angle) # type: float # text is aligned to dimension line self.text_rotation = self.dim_line_angle # type: float if self.text_halign in ( 3, 4 ): # text above extension line, is always aligned with extension lines self.text_rotation = self.ext_line_angle self.ext1_line_start = Vec2(self.dimension.dxf.defpoint2) self.ext2_line_start = Vec2(self.dimension.dxf.defpoint3) ext1_ray = ConstructionRay(self.ext1_line_start, angle=self.ext_line_angle_rad) ext2_ray = ConstructionRay(self.ext2_line_start, angle=self.ext_line_angle_rad) dim_line_ray = ConstructionRay(self.dimension.dxf.defpoint, angle=self.dim_line_angle_rad) self.dim_line_start = dim_line_ray.intersect(ext1_ray) # type: Vec2 self.dim_line_end = dim_line_ray.intersect(ext2_ray) # type: Vec2 self.dim_line_center = self.dim_line_start.lerp( self.dim_line_end) # type: Vec2 if self.dim_line_start == self.dim_line_end: self.dim_line_vec = Vec2.from_angle(self.dim_line_angle_rad) else: self.dim_line_vec = (self.dim_line_end - self.dim_line_start).normalize() # type: Vec2 # set dimension defpoint to expected location - 3D vertex required! self.dimension.dxf.defpoint = Vector(self.dim_line_start) self.measurement = (self.dim_line_end - self.dim_line_start).magnitude # type: float self.text = self.text_override( self.measurement * self.dim_measurement_factor) # type: str # only for linear dimension in multi point mode self.multi_point_mode = override.pop('multi_point_mode', False) # 1 .. move wide text up # 2 .. move wide text down # None .. ignore self.move_wide_text = override.pop('move_wide_text', None) # type: bool # actual text width in drawing units self.dim_text_width = 0 # type: float # arrows self.required_arrows_space = 2 * self.arrow_size + self.text_gap # type: float self.arrows_outside = self.required_arrows_space > self.measurement # type: bool # text location and rotation if self.text: # text width and required space self.dim_text_width = self.text_width(self.text) # type: float if self.dim_tolerance: self.dim_text_width += self.tol_text_width elif self.dim_limits: # limits show the upper and lower limit of the measurement as stacked values # and with the size of tolerances measurement = self.measurement * self.dim_measurement_factor self.measurement_upper_limit = measurement + self.tol_maximum self.measurement_lower_limit = measurement - self.tol_minimum self.tol_text_upper = self.format_tolerance_text( self.measurement_upper_limit) self.tol_text_lower = self.format_tolerance_text( self.measurement_lower_limit) self.tol_text_width = self.tolerance_text_width( max(len(self.tol_text_upper), len(self.tol_text_lower))) # only limits are displayed so: self.dim_text_width = self.tol_text_width if self.multi_point_mode: # ezdxf has total control about vertical text position in multi point mode self.text_vertical_position = 0. if self.text_valign == 0 and abs( self.text_vertical_position) < 0.7: # vertical centered text needs also space for arrows required_space = self.dim_text_width + 2 * self.arrow_size else: required_space = self.dim_text_width self.is_wide_text = required_space > self.measurement if not self.force_text_inside: # place text outside if wide text and not forced inside self.text_outside = self.is_wide_text elif self.is_wide_text and self.text_halign < 3: # center wide text horizontal self.text_halign = 0 # use relative text shift to move wide text up or down in multi point mode if self.multi_point_mode and self.is_wide_text and self.move_wide_text: shift_value = self.text_height + self.text_gap if self.move_wide_text == 1: # move text up self.text_shift_v = shift_value if self.vertical_placement == -1: # text below dimension line # shift again self.text_shift_v += shift_value elif self.move_wide_text == 2: # move text down self.text_shift_v = -shift_value if self.vertical_placement == 1: # text above dimension line # shift again self.text_shift_v -= shift_value # get final text location - no altering after this line self.text_location = self.get_text_location() # type: Vec2 # text rotation override rotation = self.text_rotation # type: float if self.user_text_rotation is not None: rotation = self.user_text_rotation elif self.text_outside and self.text_outside_horizontal: rotation = 0 elif self.text_inside and self.text_inside_horizontal: rotation = 0 self.text_rotation = rotation self.text_box = TextBox(center=self.text_location, width=self.dim_text_width, height=self.text_height, angle=self.text_rotation, gap=self.text_gap * .75) if self.text_has_leader: p1, p2, *_ = self.text_box.corners self.leader1, self.leader2 = order_leader_points( self.dim_line_center, p1, p2) # not exact what BricsCAD (AutoCAD) expect, but close enough self.dimension.dxf.text_midpoint = self.leader1 else: # write final text location into DIMENSION entity self.dimension.dxf.text_midpoint = self.text_location
def __init__( self, dimension: "Dimension", ucs: "UCS" = None, override: "DimStyleOverride" = None, ): super().__init__(dimension, ucs, override) measurement = self.measurement if measurement.text_movement_rule == 0: # moves the dimension line with dimension text, this makes no sense # for ezdxf (just set `base` argument) measurement.text_movement_rule = 2 self.oblique_angle: float = self.dimension.get_dxf_attrib( "oblique_angle", 90) self.dim_line_angle: float = self.dimension.get_dxf_attrib("angle", 0) self.dim_line_angle_rad: float = math.radians(self.dim_line_angle) self.ext_line_angle: float = self.dim_line_angle + self.oblique_angle self.ext_line_angle_rad: float = math.radians(self.ext_line_angle) # text is aligned to dimension line measurement.text_rotation = self.dim_line_angle # text above extension line, is always aligned with extension lines if measurement.text_halign in (3, 4): measurement.text_rotation = self.ext_line_angle self.ext1_line_start = Vec2(self.dimension.dxf.defpoint2) self.ext2_line_start = Vec2(self.dimension.dxf.defpoint3) ext1_ray = ConstructionRay(self.ext1_line_start, angle=self.ext_line_angle_rad) ext2_ray = ConstructionRay(self.ext2_line_start, angle=self.ext_line_angle_rad) dim_line_ray = ConstructionRay(self.dimension.dxf.defpoint, angle=self.dim_line_angle_rad) self.dim_line_start: Vec2 = dim_line_ray.intersect(ext1_ray) self.dim_line_end: Vec2 = dim_line_ray.intersect(ext2_ray) self.dim_line_center: Vec2 = self.dim_line_start.lerp( self.dim_line_end) if self.dim_line_start == self.dim_line_end: self.dim_line_vec = Vec2.from_angle(self.dim_line_angle_rad) else: self.dim_line_vec = (self.dim_line_end - self.dim_line_start).normalize() # set dimension defpoint to expected location - 3D vertex required! self.dimension.dxf.defpoint = Vec3(self.dim_line_start) raw_measurement = (self.dim_line_end - self.dim_line_start).magnitude measurement.update(raw_measurement) # only for linear dimension in multi point mode self.multi_point_mode = self.dim_style.pop("multi_point_mode", False) # 1 .. move wide text up # 2 .. move wide text down # None .. ignore self.move_wide_text: Optional[bool] = self.dim_style.pop( "move_wide_text", None) # actual text width in drawing units self._total_text_width: float = 0 # arrows self.required_arrows_space: float = (2 * self.arrows.arrow_size + measurement.text_gap) self.arrows_outside: bool = (self.required_arrows_space > raw_measurement) # text location and rotation if measurement.text: # text width and required space self._total_text_width = self.total_text_width() if self.tol.has_limits: # limits show the upper and lower limit of the measurement as # stacked values and with the size of tolerances self.tol.update_limits(self.measurement.value) if self.multi_point_mode: # ezdxf has total control about vertical text position in multi # point mode measurement.text_vertical_position = 0.0 if (measurement.text_valign == 0 and abs(measurement.text_vertical_position) < 0.7): # vertical centered text needs also space for arrows required_space = (self._total_text_width + 2 * self.arrows.arrow_size) else: required_space = self._total_text_width measurement.is_wide_text = required_space > raw_measurement if not measurement.force_text_inside: # place text outside if wide text and not forced inside measurement.text_is_outside = measurement.is_wide_text elif measurement.is_wide_text and measurement.text_halign < 3: # center wide text horizontal measurement.text_halign = 0 # use relative text shift to move wide text up or down in multi # point mode if (self.multi_point_mode and measurement.is_wide_text and self.move_wide_text): shift_value = measurement.text_height + measurement.text_gap if self.move_wide_text == 1: # move text up measurement.text_shift_v = shift_value if (measurement.vertical_placement == -1 ): # text below dimension line # shift again measurement.text_shift_v += shift_value elif self.move_wide_text == 2: # move text down measurement.text_shift_v = -shift_value if (measurement.vertical_placement == 1 ): # text above dimension line # shift again measurement.text_shift_v -= shift_value # get final text location - no altering after this line measurement.text_location = self.get_text_location() # text rotation override rotation: float = measurement.text_rotation if measurement.user_text_rotation is not None: rotation = measurement.user_text_rotation elif (measurement.text_is_outside and measurement.text_outside_horizontal): rotation = 0.0 elif (measurement.text_is_inside and measurement.text_inside_horizontal): rotation = 0.0 measurement.text_rotation = rotation text_box = self.init_text_box() self.geometry.set_text_box(text_box) if measurement.has_leader: p1, p2, *_ = text_box.corners self.leader1, self.leader2 = order_leader_points( self.dim_line_center, p1, p2) # not exact what BricsCAD (AutoCAD) expect, but close enough self.dimension.dxf.text_midpoint = self.leader1 else: # write final text location into DIMENSION entity self.dimension.dxf.text_midpoint = measurement.text_location