Example #1
0
    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
Example #2
0
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
Example #3
0
    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)
Example #4
0
    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)
Example #5
0
 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)
Example #6
0
    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)
Example #7
0
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)
Example #8
0
    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)
Example #9
0
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))
Example #10
0
        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
Example #11
0
    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
Example #12
0
    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
Example #13
0
 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
Example #14
0
 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)
Example #15
0
    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
Example #16
0
    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