コード例 #1
0
        def embroider(command, channel, _, angle=None, distance=None, **kwargs):
            elements = context.elements
            channel(_("Embroidery Filling"))
            efill = EulerianFill(float(distance))
            for node in elements.elems(emphasized=True):
                try:
                    path = Path(node.shape)
                except AttributeError:
                    try:
                        path = Path(node.path)
                    except AttributeError:
                        continue
                if angle is not None:
                    path *= Matrix.rotate(angle)
                pts = [abs(path).point(i / 100.0, error=1e-4) for i in range(101)]
                efill += pts

            points = efill.get_fill()

            for s in split(points):
                result = Path(Polyline(s, stroke="black"))
                if angle is not None:
                    result *= Matrix.rotate(-angle)
                node = elements.elem_branch.add(path=result, type="elem path")
                elements.classify([node])
コード例 #2
0
 def validate(self):
     if self.point is None:
         self.point = Point(
             float(self.settings.get("x", 0)), float(self.settings.get("y", 0))
         )
     if self.matrix is None:
         self.matrix = Matrix()
コード例 #3
0
def eulerian_fill(settings, outlines, matrix, limit=None):
    """
    Applies optimized Eulerian fill
    @return:
    """
    if matrix is None:
        matrix = Matrix()

    settings = dict(settings)
    h_dist = settings.get("hatch_distance", "1mm")
    h_angle = settings.get("hatch_angle", "0deg")
    distance_y = float(Length(h_dist))
    if isinstance(h_angle, float):
        angle = Angle.degrees(h_angle)
    else:
        angle = Angle.parse(h_angle)

    rotate = Matrix.rotate(angle)
    counter_rotate = Matrix.rotate(-angle)

    def mx_rotate(pt):
        if pt is None:
            return None
        return (
            pt[0] * rotate.a + pt[1] * rotate.c + 1 * rotate.e,
            pt[0] * rotate.b + pt[1] * rotate.d + 1 * rotate.f,
        )

    def mx_counter(pt):
        if pt is None:
            return None
        return (
            pt[0] * counter_rotate.a + pt[1] * counter_rotate.c + 1 * counter_rotate.e,
            pt[0] * counter_rotate.b + pt[1] * counter_rotate.d + 1 * counter_rotate.f,
        )

    transformed_vector = matrix.transform_vector([0, distance_y])
    distance = abs(complex(transformed_vector[0], transformed_vector[1]))
    efill = EulerianFill(distance)
    for sp in outlines:
        sp = list(map(mx_rotate, sp))
        efill += sp
    if limit and efill.estimate() > limit:
        return []
    points = efill.get_fill()

    points = list(map(mx_counter, points))
    return points
コード例 #4
0
    def test_fill_hatch2(self):
        kernel = bootstrap.bootstrap()
        try:
            kernel.console("operation* delete\n")
            kernel.console("rect 0 0 1in 1in\n")
            kernel.console("rect 3in 0 1in 1in\n")
            kernel.console("hatch\n")
            hatch = list(kernel.elements.ops())[0]
            hatch.hatch_type = "eulerian"
            rect0 = list(kernel.elements.elems())[0]
            hatch.add_node(copy(rect0))
            rect1 = list(kernel.elements.elems())[1]
            hatch.add_node(copy(rect1))
            commands = list()
            # kernel.console("tree list\n")
            hatch.preprocess(kernel.root, Matrix(), commands)
            for command in commands:
                command()
            # kernel.console("tree list\n")
            polyline_node0 = hatch.children[0]
            shape0 = polyline_node0.shape
            self.assertEqual(len(shape0), 77)
            # print(shape0)

            polyline_node1 = hatch.children[1]
            shape1 = polyline_node1.shape
            self.assertEqual(len(shape1), 50)
            # print(shape1)
        finally:
            kernel.shutdown()
コード例 #5
0
 def make_image():
     step_x = self.raster_step_x
     step_y = self.raster_step_y
     bounds = self.bounds
     try:
         image = make_raster(list(self.flat()),
                             bounds=bounds,
                             step_x=step_x,
                             step_y=step_y)
     except AssertionError:
         raise CutPlanningFailedError("Raster too large.")
     if image.width == 1 and image.height == 1:
         # TODO: Solve this is a less kludgy manner. The call to make the image can fail the first time
         #  around because the renderer is what sets the size of the text. If the size hasn't already
         #  been set, the initial bounds are wrong.
         bounds = self.bounds
         try:
             image = make_raster(list(self.flat()),
                                 bounds=bounds,
                                 step_x=step_x,
                                 step_y=step_y)
         except AssertionError:
             raise CutPlanningFailedError("Raster too large.")
     image = image.convert("L")
     matrix = Matrix.scale(step_x, step_y)
     matrix.post_translate(bounds[0], bounds[1])
     image_node = ImageNode(image=image, matrix=matrix)
     self.children.clear()
     self.add_node(image_node)
コード例 #6
0
ファイル: test_numpath.py プロジェクト: meerk40t/meerk40t
 def test_numpath_transform(self):
     numpath = Numpath()
     numpath.polyline(
         (
             complex(0.05, 0.05),
             complex(0.95, 0.05),
             complex(0.95, 0.95),
             complex(0.05, 0.95),
             complex(0.05, 0.05),
         )
     )
     numpath.polyline(
         (
             complex(0.25, 0.25),
             complex(0.75, 0.25),
             complex(0.75, 0.75),
             complex(0.25, 0.75),
             complex(0.25, 0.25),
         )
     )
     numpath.uscale(10000)
     c = copy(numpath)
     numpath.rotate(tau * 0.25)
     c.transform(Matrix("rotate(.25turn)"))
     t = numpath.segments == c.segments
     self.assertTrue(np.all(t))
コード例 #7
0
 def __init__(
     self,
     path=None,
     matrix=None,
     fill=None,
     stroke=None,
     stroke_width=None,
     linecap=Linecap.CAP_BUTT,
     linejoin=Linejoin.JOIN_MITER,
     fillrule=Fillrule.FILLRULE_NONZERO,
     *args,
     **kwargs,
 ):
     super().__init__(*args, **kwargs)
     self._formatter = "{element_type} {id} {stroke}"
     self.settings.update(kwargs)
     self.path = path
     if matrix is None:
         matrix = Matrix()
     self.matrix = matrix
     self.fill = fill
     self.stroke = stroke
     self.stroke_width = stroke_width
     self.linecap = linecap
     self.linejoin = linejoin
     self.fillrule = fillrule
     self.lock = False
コード例 #8
0
    def test_actualize_circle_step3_direct_black(self):
        """
        Test for edge pixel error. Black Empty.

        :return:
        """
        image = Image.new("RGBA", (256, 256), "black")
        draw = ImageDraw.Draw(image)
        draw.ellipse((100, 100, 150, 150), "white")

        for step in range(1, 20):
            transform = Matrix()
            actual, transform = actualize(
                image, transform, step_x=step, step_y=step, crop=False, inverted=True
            )
            self.assertEqual(actual.getpixel((-1, -1)), 0)

        # Note: inverted flag not set. White edge pixel is correct.
        actual, transform = actualize(image, Matrix(), step_x=3, step_y=3, crop=False)
        self.assertEqual(actual.getpixel((-1, -1)), 255)
コード例 #9
0
    def test_plotplanner_walk_raster(self):
        """
        Test plotplanner operation of walking to a raster.

        PLOT_FINISH = 256
        PLOT_RAPID = 4
        PLOT_JOG = 2
        PLOT_SETTING = 128
        PLOT_AXIS = 64
        PLOT_DIRECTION = 32
        PLOT_LEFT_UPPER = 512
        PLOT_RIGHT_LOWER = 1024

        1 means cut.
        0 means move.

        :return:
        """

        rasterop = RasterOpNode()
        image = Image.new("RGBA", (256, 256))
        draw = ImageDraw.Draw(image)
        draw.ellipse((0, 0, 255, 255), "black")
        image = image.convert("L")
        inode = ImageNode(image=image, dpi=1000.0, matrix=Matrix())
        inode.step_x = 1
        inode.step_y = 1
        inode.process_image()
        rasterop.add_node(inode)
        rasterop.raster_step_x = 1
        rasterop.raster_step_y = 1

        vectorop = EngraveOpNode()
        vectorop.add_node(
            PathNode(path=Path(Circle(cx=127, cy=127, r=128)), fill="black"))
        cutcode = CutCode()
        cutcode.extend(vectorop.as_cutobjects())
        cutcode.extend(rasterop.as_cutobjects())
        settings = {"power": 500}
        plan = PlotPlanner(settings)
        for c in cutcode.flat():
            plan.push(c)

        setting_changed = False
        for x, y, on in plan.gen():
            if on > 2:
                if setting_changed:
                    # Settings change happens at vector to raster switch and must repost the axis.
                    self.assertEqual(on, PLOT_AXIS)
                if on == PLOT_SETTING:
                    setting_changed = True
                else:
                    setting_changed = False
コード例 #10
0
ファイル: widget.py プロジェクト: meerk40t/meerk40t
 def __init__(
     self,
     scene,
     left: float = None,
     top: float = None,
     right: float = None,
     bottom: float = None,
     all: bool = False,
 ):
     """
     All produces a widget of infinite space rather than finite space.
     """
     assert scene.__class__.__name__ == "Scene"
     list.__init__(self)
     self.matrix = Matrix()
     self.scene = scene
     self.parent = None
     self.properties = ORIENTATION_RELATIVE
     if all:
         # contains all points
         self.left = -float("inf")
         self.top = -float("inf")
         self.right = float("inf")
         self.bottom = float("inf")
     else:
         # contains no points
         self.left = float("inf")
         self.top = float("inf")
         self.right = -float("inf")
         self.bottom = -float("inf")
     if left is not None:
         self.left = left
     if right is not None:
         self.right = right
     if top is not None:
         self.top = top
     if bottom is not None:
         self.bottom = bottom
コード例 #11
0
ファイル: rotary.py プロジェクト: meerk40t/meerk40t
 def apply_rotary_scale(*args, **kwargs):
     sx = self.scale_x
     sy = self.scale_y
     x, y = self.device.current
     matrix = Matrix("scale(%f, %f, %f, %f)" % (sx, sy, x, y))
     for node in self.elements.elems():
         if hasattr(node, "rotary_scale"):
             # This element is already scaled
             return
         try:
             node.rotary_scale = sx, sy
             node.matrix *= matrix
             node.modified()
         except AttributeError:
             pass
コード例 #12
0
    def test_actualize_circle_step3_direct_white(self):
        """
        Test for edge pixel error. White empty.

        :return:
        """
        image = Image.new("RGBA", (256, 256), "white")
        draw = ImageDraw.Draw(image)
        draw.ellipse((100, 100, 150, 150), "black")

        for step in range(1, 20):
            transform = Matrix()
            actual, transform = actualize(
                image, transform, step_x=step, step_y=step, crop=False
            )
            self.assertEqual(actual.getpixel((-1, -1)), 255)
コード例 #13
0
ファイル: vectrace.py プロジェクト: meerk40t/meerk40t
 def vectrace(data, **kwargs):
     elements = kernel.root.elements
     path = Path(fill="black", stroke="blue")
     paths = []
     for node in data:
         matrix = node.matrix
         image = node.image
         width, height = node.image.size
         if image.mode != "L":
             image = image.convert("L")
         image = image.point(lambda e: int(e > 127) * 255)
         for points in _vectrace(image.load(), width, height):
             path += Polygon(*points)
         paths.append(
             elements.elem_branch.add(path=path,
                                      matrix=Matrix(matrix),
                                      type="elem path"))
     return "elements", paths
コード例 #14
0
ファイル: hershey.py プロジェクト: meerk40t/meerk40t
        def linetext(command,
                     channel,
                     _,
                     font=None,
                     font_size=None,
                     remainder=None,
                     **kwargs):
            context.setting(str, "shx_preferred", None)
            if font is not None:
                context.shx_preferred = font
            font = context.shx_preferred

            safe_dir = realpath(get_safe_path(context.kernel.name))
            if font is None:
                channel(_("SHX fonts in {path}:").format(path=safe_dir))
                for p in glob(join(safe_dir, "*.shx")):
                    channel(p)
                for p in glob(join(safe_dir, "*.SHX")):
                    channel(p)
                return
            font_path = join(safe_dir, font)
            if not os.path.exists(font_path):
                channel(
                    _("Font was not found at {path}").format(path=font_path))
                for p in glob(join(safe_dir, "*.shx")):
                    channel(p)
                for p in glob(join(safe_dir, "*.SHX")):
                    channel(p)
                return
            if remainder is None:
                channel(_("No text to make a path with."))
                return
            font = ShxFont(font_path)
            path = ShxPath()
            font.render(path, remainder, True, float(font_size))
            path_node = PathNode(
                path=path.path,
                matrix=Matrix.translate(0, float(font_size)),
                stroke=Color("black"),
            )
            context.elements.elem_branch.add_node(path_node)
            context.signal("element_added", path_node)
コード例 #15
0
ファイル: toolpoint.py プロジェクト: meerk40t/meerk40t
 def event(self,
           window_pos=None,
           space_pos=None,
           event_type=None,
           nearest_snap=None):
     response = RESPONSE_CHAIN
     if event_type == "leftclick":
         if nearest_snap is None:
             point = Point(space_pos[0], space_pos[1])
         else:
             point = Point(nearest_snap[0], nearest_snap[1])
         elements = self.scene.context.elements
         node = elements.elem_branch.add(point=point,
                                         matrix=Matrix(),
                                         type="elem point")
         if self.scene.context.elements.default_stroke is not None:
             node.stroke = self.scene.context.elements.default_stroke
         if self.scene.context.elements.default_fill is not None:
             node.fill = self.scene.context.elements.default_fill
         if elements.classify_new:
             elements.classify([node])
         self.notify_created(node)
         response = RESPONSE_CONSUME
     return response
コード例 #16
0
 def test_fill_euler_scale(self):
     w = 1000
     h = 1000
     paths = (
         (
             (w * 0.05, h * 0.05),
             (w * 0.95, h * 0.05),
             (w * 0.95, h * 0.95),
             (w * 0.05, h * 0.95),
             (w * 0.05, h * 0.05),
         ),
         (
             (w * 0.25, h * 0.25),
             (w * 0.75, h * 0.25),
             (w * 0.75, h * 0.75),
             (w * 0.25, h * 0.75),
             (w * 0.25, h * 0.25),
         ),
     )
     matrix = Matrix.scale(0.005)
     fill = list(eulerian_fill(settings={}, outlines=paths, matrix=matrix))
     self.assertEqual(len(fill), 327)
     for x, y in fill:
         self.assertIn(x, (50, 250, 750, 950))
コード例 #17
0
    def test_actualize_pureblack(self):
        """
        Test that a pure black image does not crash.

        :return:
        """
        kernel = bootstrap.bootstrap()
        try:
            kernel_root = kernel.get_context("/")
            # kernel_root("channel print console\n")
            image = Image.new("RGBA", (256, 256), "black")
            elements = kernel_root.elements
            node = elements.elem_branch.add(
                image=image, dpi=1000.0, matrix=Matrix(), type="elem image"
            )
            node.emphasized = True
            kernel_root("image resample\n")
            for element in kernel_root.elements.elems():
                if node.type == "elem image":
                    self.assertEqual(element.image.size, (256, 256))
                    self.assertEqual(element.matrix.value_trans_x(), 0)
                    self.assertEqual(element.matrix.value_trans_y(), 0)
        finally:
            kernel.shutdown()
コード例 #18
0
ファイル: scene.py プロジェクト: meerk40t/meerk40t
    def rebuild_hit_chain(self, current_widget, current_matrix=None):
        """
        Iterates through the hit chain to find elements which respond to their hit() function that they are HITCHAIN_HIT
        and registers this within the hittable_elements list if they are able to hit at the current time. Given the
        dimensions of the widget and the current matrix within the widget tree.

        HITCHAIN_HIT means that this is a hit value and should the termination of this branch of the widget tree.
        HITCHAIN_DELEGATE means that this is not a hittable widget and should not receive mouse events.
        HITCHAIN_HIT_AND_DELEGATE means that this is a hittable widget, but other widgets within it might also matter.
        HITCHAIN_DELEGATE_AND_HIT means that other widgets in the tree should be checked first, but after those this
        widget should be checked.

        The hitchain is the current matrix and current widget in the order of depth.

        """
        # If there is a matrix for the widget concatenate it.
        if current_widget.matrix is not None:
            matrix_within_scene = Matrix(current_widget.matrix)
            matrix_within_scene.post_cat(current_matrix)
        else:
            matrix_within_scene = Matrix(current_matrix)

        # Add to list and recurse for children based on response.
        response = current_widget.hit()
        if response == HITCHAIN_HIT:
            self.hittable_elements.append(
                (current_widget, matrix_within_scene))
        # elif response == HITCHAIN_HIT_WITH_PRIORITY:
        #    self.hittable_elements.insert(0, (current_widget, matrix_within_scene))
        elif response == HITCHAIN_DELEGATE:
            for w in current_widget:
                self.rebuild_hit_chain(w, matrix_within_scene)
        elif response == HITCHAIN_HIT_AND_DELEGATE:
            self.hittable_elements.append(
                (current_widget, matrix_within_scene))
            for w in current_widget:
                self.rebuild_hit_chain(w, matrix_within_scene)
        elif response == HITCHAIN_DELEGATE_AND_HIT:
            for w in current_widget:
                self.rebuild_hit_chain(w, matrix_within_scene)
            self.hittable_elements.append(
                (current_widget, matrix_within_scene))
コード例 #19
0
def scanline_fill(settings, outlines, matrix, limit=None):
    """
    Applies optimized scanline fill
    @return:
    """
    if matrix is None:
        matrix = Matrix()

    settings = dict(settings)
    h_dist = settings.get("hatch_distance", "1mm")
    h_angle = settings.get("hatch_angle", "0deg")
    distance_y = float(Length(h_dist))
    if isinstance(h_angle, float):
        angle = Angle.degrees(h_angle)
    else:
        angle = Angle.parse(h_angle)

    rotate = Matrix.rotate(angle)
    counter_rotate = Matrix.rotate(-angle)

    def mx_rotate(pt):
        if pt is None:
            return None
        return (
            pt[0] * rotate.a + pt[1] * rotate.c + 1 * rotate.e,
            pt[0] * rotate.b + pt[1] * rotate.d + 1 * rotate.f,
        )

    def mx_counter(pt):
        if pt is None:
            return None
        return (
            pt[0] * counter_rotate.a + pt[1] * counter_rotate.c + 1 * counter_rotate.e,
            pt[0] * counter_rotate.b + pt[1] * counter_rotate.d + 1 * counter_rotate.f,
        )

    transformed_vector = matrix.transform_vector([0, distance_y])
    distance = abs(complex(transformed_vector[0], transformed_vector[1]))

    vm = VectorMontonizer()
    for outline in outlines:
        pts = list(map(Point, map(mx_rotate, outline)))
        vm.add_cluster(pts)
    vm.sort_clusters()
    y_max = vm.clusters[-1][0]
    y_min = vm.clusters[0][0]
    height = y_max - y_min
    try:
        count = height / distance
    except ZeroDivisionError:
        return []
    if limit and count > limit:
        return []
    vm.valid_low_value = y_min - distance
    vm.valid_high_value = y_max + distance
    vm.scanline(y_min - distance)
    points = list()
    forward = True
    while vm.valid_range():
        vm.next_intercept(distance)
        vm.sort_actives()
        y = vm.current
        for i in (
            range(1, len(vm.actives), 2)
            if forward
            else range(len(vm.actives) - 1, 0, -2)
        ):
            left_segment = vm.actives[i - 1]
            right_segment = vm.actives[i]
            left_segment_x = vm.intercept(left_segment, y)
            right_segment_x = vm.intercept(right_segment, y)
            if forward:
                points.append((left_segment_x, y))
                points.append((right_segment_x, y))
            else:
                points.append((right_segment_x, y))
                points.append((left_segment_x, y))
            points.append(None)
        forward = not forward
    points = list(map(mx_counter, points))
    return points
コード例 #20
0
    def draw_cutcode(self,
                     cutcode: CutCode,
                     gc: wx.GraphicsContext,
                     x: int = 0,
                     y: int = 0):
        """
        Draw cutcode object into wxPython graphics code.

        This code accepts x,y offset values. The cutcode laser offset can be set with a
        command with the rest of the cutcode remaining the same. So drawing the cutcode
        requires knowing what, if any offset is currently being applied.

        @param cutcode: flat cutcode object to draw.
        @param gc: wx.graphics context
        @param x: offset in x direction
        @param y: offset in y direction
        @return:
        """
        p = None
        last_point = None
        color = None
        for cut in cutcode:
            c = cut.line_color
            if c is not color:
                color = c
                last_point = None
                if p is not None:
                    gc.StrokePath(p)
                    del p
                p = gc.CreatePath()
                self.set_pen(gc, c, width=7.0, alpha=127)
            start = cut.start
            end = cut.end
            if p is None:
                p = gc.CreatePath()
            if last_point != start:
                p.MoveToPoint(start[0] + x, start[1] + y)
            if isinstance(cut, LineCut):
                # Standard line cut. Applies to path object.
                p.AddLineToPoint(end[0] + x, end[1] + y)
            elif isinstance(cut, QuadCut):
                # Standard quadratic bezier cut
                p.AddQuadCurveToPoint(cut.c()[0] + x,
                                      cut.c()[1] + y, end[0] + x, end[1] + y)
            elif isinstance(cut, CubicCut):
                # Standard cubic bezier cut
                p.AddCurveToPoint(
                    cut.c1()[0] + x,
                    cut.c1()[1] + y,
                    cut.c2()[0] + x,
                    cut.c2()[1] + y,
                    end[0] + x,
                    end[1] + y,
                )
            elif isinstance(cut, RasterCut):
                # Rastercut object.
                image = cut.image
                gc.PushState()
                matrix = Matrix.scale(cut.step_x, cut.step_y)
                matrix.post_translate(cut.offset_x + x,
                                      cut.offset_y + y)  # Adjust image xy
                gc.ConcatTransform(
                    wx.GraphicsContext.CreateMatrix(gc, ZMatrix(matrix)))
                try:
                    cache = cut.cache
                    cache_id = cut.cache_id
                except AttributeError:
                    cache = None
                    cache_id = -1
                if cache_id != id(image):
                    # Cached image is invalid.
                    cache = None
                if cache is None:
                    # No valid cache. Generate.
                    cut.c_width, cut.c_height = image.size
                    try:
                        cut.cache = self.make_thumbnail(image, maximum=5000)
                    except (MemoryError, RuntimeError):
                        cut.cache = None
                    cut.cache_id = id(image)
                if cut.cache is not None:
                    # Cache exists and is valid.
                    gc.DrawBitmap(cut.cache, 0, 0, cut.c_width, cut.c_height)
                else:
                    # Image was too large to cache, draw a red rectangle instead.
                    gc.SetBrush(wx.RED_BRUSH)
                    gc.DrawRectangle(0, 0, cut.c_width, cut.c_height)
                    gc.DrawBitmap(icons8_image_50.GetBitmap(), 0, 0,
                                  cut.c_width, cut.c_height)
                gc.PopState()
            elif isinstance(cut, RawCut):
                pass
            elif isinstance(cut, PlotCut):
                p.MoveToPoint(start[0] + x, start[1] + y)
                for px, py, pon in cut.plot:
                    if pon == 0:
                        p.MoveToPoint(px + x, py + y)
                    else:
                        p.AddLineToPoint(px + x, py + y)
            elif isinstance(cut, DwellCut):
                pass
            elif isinstance(cut, WaitCut):
                pass
            elif isinstance(cut, InputCut):
                pass
            elif isinstance(cut, OutputCut):
                pass
            last_point = end
        if p is not None:
            gc.StrokePath(p)
            del p
コード例 #21
0
ファイル: units.py プロジェクト: meerk40t/meerk40t
class ViewPort:
    """
    The width and height are of the viewport are stored in MK native units (nm).

    Origin_x and origin_y are the location of the home position in unit square values.
    This is to say 1,1 is the bottom left, and 0.5 0.5 is the middle of the bed.

    user_scale is a scale factor for applied by the user rather than the driver.

    native_scale is the scale factor of the driver units to MK native units

    flip_x, flip_y, and swap_xy are used to apply whatever flips and swaps are needed.
    """
    def __init__(
        self,
        width,
        height,
        origin_x=0.0,
        origin_y=0.0,
        user_scale_x=1.0,
        user_scale_y=1.0,
        native_scale_x=1.0,
        native_scale_y=1.0,
        flip_x=False,
        flip_y=False,
        swap_xy=False,
        show_origin_x=None,
        show_origin_y=None,
    ):
        self._matrix = None
        self._imatrix = None
        self.width = width
        self.height = height
        self.origin_x = origin_x
        self.origin_y = origin_y
        self.user_scale_x = user_scale_x
        self.user_scale_y = user_scale_y
        self.native_scale_x = native_scale_x
        self.native_scale_y = native_scale_y
        self.flip_x = flip_x
        self.flip_y = flip_y
        self.swap_xy = swap_xy
        if show_origin_x is None:
            show_origin_x = origin_x
        if show_origin_y is None:
            show_origin_y = origin_y
        self.show_origin_x = show_origin_x
        self.show_origin_y = show_origin_y

        self._width = None
        self._height = None
        self._offset_x = None
        self._offset_y = None
        self._scale_x = None
        self._scale_y = None
        self.realize()

    def realize(self):
        self._imatrix = None
        self._matrix = None
        self._width = Length(self.width, unitless=1.0).units
        self._height = Length(self.height, unitless=1.0).units
        self._offset_x = self._width * self.origin_x
        self._offset_y = self._height * self.origin_y
        self._scale_x = self.user_scale_x * self.native_scale_x
        self._scale_y = self.user_scale_y * self.native_scale_y

    def physical_to_scene_position(self, x, y, unitless=UNITS_PER_PIXEL):
        """
        Converts an X,Y position into viewport units.

        @param x:
        @param y:
        @param as_float:
        @param unitless:
        @return:
        """
        unit_x = Length(x, relative_length=self._width,
                        unitless=unitless).units
        unit_y = Length(y, relative_length=self._height,
                        unitless=unitless).units
        return unit_x, unit_y

    def physical_to_device_position(self, x, y, unitless=UNITS_PER_PIXEL):
        """
        Converts an X,Y position into viewport units.
        @param x:
        @param y:
        @param unitless:
        @return:
        """
        x, y = self.physical_to_scene_position(x, y, unitless)
        return self.scene_to_device_position(x, y)

    def physical_to_device_length(self, x, y, unitless=UNITS_PER_PIXEL):
        """
        Converts an X,Y position into dx, dy.
        @param x:
        @param y:
        @param unitless:
        @return:
        """
        x, y = self.physical_to_scene_position(x, y, unitless)
        return self.scene_to_device_position(x, y, vector=True)

    def device_to_scene_position(self, x, y):
        if self._imatrix is None:
            self.calculate_matrices()
        point = self._imatrix.point_in_matrix_space((x, y))
        return point.x, point.y

    def scene_to_device_position(self, x, y, vector=False):
        if self._matrix is None:
            self.calculate_matrices()
        if vector:
            point = self._matrix.transform_vector([x, y])
            return point[0], point[1]
        else:
            point = self._matrix.point_in_matrix_space((x, y))
            return point.x, point.y

    def calculate_matrices(self):
        self._matrix = Matrix(self.scene_to_device_matrix())
        self._imatrix = Matrix(self._matrix)
        self._imatrix.inverse()

    def device_to_scene_matrix(self):
        if self._matrix is None:
            self.calculate_matrices()
        return self._imatrix

    def scene_to_device_matrix(self):
        ops = []
        if self._scale_x != 1.0 or self._scale_y != 1.0:
            ops.append("scale({sx:.13f}, {sy:.13f})".format(
                sx=1.0 / self._scale_x, sy=1.0 / self._scale_y))
        if self._offset_x != 0 or self._offset_y != 0:
            ops.append("translate({dx:.13f}, {dy:.13f})".format(
                dx=self._offset_x, dy=self._offset_y))
        if self.flip_y:
            ops.append("scale(1.0, -1.0)")
        if self.flip_x:
            ops.append("scale(-1.0, 1.0)")
        if self.swap_xy:
            ops.append("scale(-1.0, 1.0) rotate(90deg)")
        return " ".join(ops)

    def length(
        self,
        value,
        axis=None,
        new_units=None,
        relative_length=None,
        as_float=False,
        unitless=UNITS_PER_PIXEL,
        digits=None,
        scale=None,
    ):
        """
        Axis 0 is X
        Axis 1 is Y

        Axis -1 is 1D in x, y space. e.g. a line width.

        Convert a length of distance {value} to new native values.

        @param value:
        @param axis:
        @param new_units:
        @param relative_length:
        @param as_float:
        @param unitless: factor for units with no units sets.
        @param scale: scale length by given factor.
        @return:
        """
        if axis == 0:
            if relative_length is None:
                relative_length = self.width
        else:
            if relative_length is None:
                relative_length = self.height
        length = Length(value,
                        relative_length=relative_length,
                        unitless=unitless,
                        digits=digits)
        if scale is not None:
            length *= scale

        if new_units == "mm":
            if as_float:
                return length.mm
            else:
                return length.length_mm
        elif new_units == "inch":
            if as_float:
                return length.inches
            else:
                return length.length_inches
        elif new_units == "cm":
            if as_float:
                return length.cm
            else:
                return length.length_cm
        elif new_units == "px":
            if as_float:
                return length.pixels
            else:
                return length.length_pixels
        elif new_units == "mil":
            if as_float:
                return length.mil
            else:
                return length.length_mil
        else:
            return length.units

    def contains(self, x, y):
        x = self.length(x, 0)
        y = self.length(y, 1)
        if x >= self._width:
            return False
        if y >= self._height:
            return False
        if x < 0:
            return False
        if y < 0:
            return False
        return True

    def bbox(self):
        return (
            0,
            0,
            self.unit_width,
            self.unit_height,
        )

    def dpi_to_steps(self, dpi, matrix=None):
        """
        Converts a DPI to a given step amount within the device length values. So M2 Nano will have 1 step per mil,
        the DPI of 500 therefore is step_x 2, step_y 2. A Galvo laser with a 200mm lens will have steps equal to
        200mm/65536 ~= 0.12 mils. So a DPI of 500 needs a step size of ~16.65 for x and y. Since 500 DPI is one dot
        per 2 mils.

        Note, steps size can be negative if our driver is x or y flipped.

        @param dpi:
        @return:
        """
        # We require vectors so any positional offsets are non-contributing.
        unit_x = UNITS_PER_INCH
        unit_y = UNITS_PER_INCH
        if matrix is None:
            if self._matrix is None:
                self.calculate_matrices()
            matrix = self._matrix
        oneinch_x = matrix.transform_vector([unit_x, 0])[0]
        oneinch_y = matrix.transform_vector([0, unit_y])[1]
        step_x = float(oneinch_x / dpi)
        step_y = float(oneinch_y / dpi)
        return step_x, step_y

    @property
    def length_width(self):
        return Length(self.width)

    @property
    def length_height(self):
        return Length(self.height)

    @property
    def unit_width(self):
        return float(Length(self.width))

    @property
    def unit_height(self):
        return float(Length(self.height))

    @staticmethod
    def viewbox_transform(e_x, e_y, e_width, e_height, vb_x, vb_y, vb_width,
                          vb_height, aspect):
        """
        SVG 1.1 7.2, SVG 2.0 8.2 equivalent transform of an SVG viewport.
        With regards to https://github.com/w3c/svgwg/issues/215 use 8.2 version.

        It creates transform commands equal to that viewport expected.

        Let e-x, e-y, e-width, e-height be the position and size of the element respectively.
        Let vb-x, vb-y, vb-width, vb-height be the min-x, min-y, width and height values of the viewBox attribute
        respectively.

        Let align be align value of preserveAspectRatio, or 'xMidYMid' if preserveAspectRatio is not defined.
        Let meetOrSlice be the meetOrSlice value of preserveAspectRatio, or 'meet' if preserveAspectRatio is not defined
        or if meetOrSlice is missing from this value.

        @param e_x: element_x value
        @param e_y: element_y value
        @param e_width: element_width value
        @param e_height: element_height value
        @param vb_x: viewbox_x value
        @param vb_y: viewbox_y value
        @param vb_width: viewbox_width value
        @param vb_height: viewbox_height value
        @param aspect: preserve aspect ratio value
        @return: string of the SVG transform commands to account for the viewbox.
        """
        if (e_x is None or e_y is None or e_width is None or e_height is None
                or vb_x is None or vb_y is None or vb_width is None
                or vb_height is None):
            return ""
        if aspect is not None:
            aspect_slice = aspect.split(" ")
            try:
                align = aspect_slice[0]
            except IndexError:
                align = "xMidyMid"
            try:
                meet_or_slice = aspect_slice[1]
            except IndexError:
                meet_or_slice = "meet"
        else:
            align = "xMidyMid"
            meet_or_slice = "meet"
        # Initialize scale-x to e-width/vb-width.
        scale_x = e_width / vb_width
        # Initialize scale-y to e-height/vb-height.
        scale_y = e_height / vb_height

        # If align is not 'none' and meetOrSlice is 'meet', set the larger of scale-x and scale-y to the smaller.
        if align != "none" and meet_or_slice == "meet":
            scale_x = scale_y = min(scale_x, scale_y)
        # Otherwise, if align is not 'none' and meetOrSlice is 'slice', set the smaller of scale-x and scale-y to the larger
        elif align != "none" and meet_or_slice == "slice":
            scale_x = scale_y = max(scale_x, scale_y)
        # Initialize translate-x to e-x - (vb-x * scale-x).
        translate_x = e_x - (vb_x * scale_x)
        # Initialize translate-y to e-y - (vb-y * scale-y)
        translate_y = e_y - (vb_y * scale_y)
        # If align contains 'xMid', add (e-width - vb-width * scale-x) / 2 to translate-x.
        align = align.lower()
        if "xmid" in align:
            translate_x += (e_width - vb_width * scale_x) / 2.0
        # If align contains 'xMax', add (e-width - vb-width * scale-x) to translate-x.
        if "xmax" in align:
            translate_x += e_width - vb_width * scale_x
        # If align contains 'yMid', add (e-height - vb-height * scale-y) / 2 to translate-y.
        if "ymid" in align:
            translate_y += (e_height - vb_height * scale_y) / 2.0
        # If align contains 'yMax', add (e-height - vb-height * scale-y) to translate-y.
        if "ymax" in align:
            translate_y += e_height - vb_height * scale_y
        # The transform applied to content contained by the element is given by:
        # translate(translate-x, translate-y) scale(scale-x, scale-y)
        if isinstance(scale_x, Length) or isinstance(scale_y, Length):
            raise ValueError
        if translate_x == 0 and translate_y == 0:
            if scale_x == 1 and scale_y == 1:
                return ""  # Nothing happens.
            else:
                return "scale(%s, %s)" % (Length.str(scale_x),
                                          Length.str(scale_y))
        else:
            if scale_x == 1 and scale_y == 1:
                return "translate(%s, %s)" % (
                    Length.str(translate_x),
                    Length.str(translate_y),
                )
            else:
                return "translate(%s, %s) scale(%s, %s)" % (
                    Length.str(translate_x),
                    Length.str(translate_y),
                    Length.str(scale_x),
                    Length.str(scale_y),
                )

    @staticmethod
    def conversion(units, amount=1):
        return Length("{amount}{units}".format(units=units,
                                               amount=amount)).preferred
コード例 #22
0
ファイル: units.py プロジェクト: meerk40t/meerk40t
 def calculate_matrices(self):
     self._matrix = Matrix(self.scene_to_device_matrix())
     self._imatrix = Matrix(self._matrix)
     self._imatrix.inverse()
コード例 #23
0
ファイル: scenespacewidget.py プロジェクト: meerk40t/meerk40t
 def aspect_matrix(self):
     """
     Specifically view the scene with the given Viewbox.
     """
     if self._frame and self._view and self.aspect:
         self.scene_widget.matrix = Matrix(self._view.transform(self._frame))
コード例 #24
0
    def make_raster(
        self,
        nodes,
        bounds,
        width=None,
        height=None,
        bitmap=False,
        step_x=1,
        step_y=1,
        keep_ratio=False,
        recursion=0,
    ):
        """
        Make Raster turns an iterable of elements and a bounds into an image of the designated size, taking into account
        the step size. The physical pixels in the image is reduced by the step size then the matrix for the element is
        scaled up by the same amount. This makes step size work like inverse dpi and correctly sets the image scale to
        the step scale for 1:1 sizes independent of the scale.

        This function requires both wxPython and Pillow.

        @param nodes: elements to render.
        @param bounds: bounds of those elements for the viewport.
        @param width: desired width of the resulting raster
        @param height: desired height of the resulting raster
        @param bitmap: bitmap to use rather than provisioning
        @param step: raster step rate, int scale rate of the image.
        @param keepratio: get a picture with the same height / width
               ratio as the original
        @return:
        """
        if bounds is None:
            return None
        xxmin = float("inf")
        yymin = float("inf")
        xxmax = -float("inf")
        yymax = -float("inf")
        # print ("Recursion=%d" % recursion)
        if not isinstance(nodes, (tuple, list)):
            mynodes = [nodes]
        else:
            mynodes = nodes
        if recursion == 0:
            # Do it only once...
            textnodes = []
            for item in mynodes:
                if item.type == "elem text":
                    if item.text.width == 0 or item.text.height == 0:
                        textnodes.append(item)
            if len(textnodes) > 0:
                # print ("Invalid textnodes found, call me again...")
                self.make_raster(
                    nodes=textnodes,
                    bounds=bounds,
                    width=width,
                    height=height,
                    bitmap=bitmap,
                    step_x=step_x,
                    step_y=step_y,
                    keep_ratio=keep_ratio,
                    recursion=1,
                )

        for item in mynodes:
            bb = item.bounds
            # if item.type == "elem text":
            #     print ("Bounds for text: %.1f, %.1f, %.1f, %.1f, w=%.1f, h=%.1f)" % (bb[0], bb[1], bb[2], bb[3], item.text.width, item.text.height))
            if bb[0] < xxmin:
                xxmin = bb[0]
            if bb[1] < yymin:
                yymin = bb[1]
            if bb[2] > xxmax:
                xxmax = bb[2]
            if bb[3] > yymax:
                yymax = bb[3]

        xmin = xxmin
        ymin = yymin
        xmax = xxmax
        ymax = yymax
        xmax = ceil(xmax)
        ymax = ceil(ymax)
        xmin = floor(xmin)
        ymin = floor(ymin)
        # print ("Bounds: %.1f, %.1f, %.1f, %.1f, Mine: %.1f, %.1f, %.1f, %.1f)" % (xmin, ymin, xmax, ymax, xxmin, yymin, xxmax, yymax))

        image_width = int(xmax - xmin)
        if image_width == 0:
            image_width = 1

        image_height = int(ymax - ymin)
        if image_height == 0:
            image_height = 1

        if width is None:
            width = image_width
        if height is None:
            height = image_height
        # Scale physical image down by step amount.
        width /= float(step_x)
        height /= float(step_y)
        width = int(ceil(abs(width)))
        height = int(ceil(abs(height)))
        if width <= 0:
            width = 1
        if height <= 0:
            height = 1
        bmp = wx.Bitmap(width, height, 32)
        dc = wx.MemoryDC()
        dc.SelectObject(bmp)
        dc.SetBackground(wx.WHITE_BRUSH)
        dc.Clear()

        matrix = Matrix()
        matrix.post_translate(-xmin, -ymin)

        # Scale affine matrix up by step amount scaled down.
        scale_x = width / float(image_width)
        scale_y = height / float(image_height)
        if keep_ratio:
            scale_x = min(scale_x, scale_y)
            scale_y = scale_x
        matrix.post_scale(scale_x, scale_y)

        gc = wx.GraphicsContext.Create(dc)
        gc.SetInterpolationQuality(wx.INTERPOLATION_BEST)
        gc.PushState()
        if not matrix.is_identity():
            gc.ConcatTransform(
                wx.GraphicsContext.CreateMatrix(gc, ZMatrix(matrix)))
        if not isinstance(nodes, (list, tuple)):
            nodes = [nodes]
        gc.SetBrush(wx.WHITE_BRUSH)
        gc.DrawRectangle(xmin - 1, ymin - 1, xmax + 1, ymax + 1)
        self.render(nodes, gc, draw_mode=DRAW_MODE_CACHE | DRAW_MODE_VARIABLES)
        img = bmp.ConvertToImage()
        buf = img.GetData()
        image = Image.frombuffer("RGB", tuple(bmp.GetSize()), bytes(buf),
                                 "raw", "RGB", 0, 1)
        gc.PopState()
        dc.SelectObject(wx.NullBitmap)
        gc.Destroy()
        del dc
        if bitmap:
            return bmp

        # for item in mynodes:
        #     bb = item.bounds
        #     if item.type == "elem text":
        #         print ("Afterwards Bounds for text: %.1f, %.1f, %.1f, %.1f, w=%.1f, h=%.1f)" % (bb[0], bb[1], bb[2], bb[3], item.text.width, item.text.height))

        return image
コード例 #25
0
class PointNode(Node):
    """
    PointNode is the bootstrapped node type for the 'elem path' type.
    """

    def __init__(
        self,
        point=None,
        matrix=None,
        fill=None,
        stroke=None,
        stroke_width=None,
        **kwargs,
    ):
        super(PointNode, self).__init__(type="elem point", **kwargs)
        self._formatter = "{element_type} {id} {stroke}"
        self.point = point
        self.matrix = matrix
        self.settings = kwargs
        self.fill = fill
        self.stroke = stroke
        self.stroke_width = stroke_width
        self.lock = False

    def __copy__(self):
        return PointNode(
            point=copy(self.point),
            matrix=copy(self.matrix),
            fill=copy(self.fill),
            stroke=copy(self.stroke),
            stroke_width=self.stroke_width,
            **self.settings,
        )

    def validate(self):
        if self.point is None:
            self.point = Point(
                float(self.settings.get("x", 0)), float(self.settings.get("y", 0))
            )
        if self.matrix is None:
            self.matrix = Matrix()

    def preprocess(self, context, matrix, commands):
        self.matrix *= matrix
        self._bounds_dirty = True

    @property
    def bounds(self):
        if self._bounds_dirty:
            p = self.matrix.transform_point(self.point)
            self._bounds = (
                p[0],
                p[1],
                p[0],
                p[1],
            )
        return self._bounds

    def default_map(self, default_map=None):
        default_map = super(PointNode, self).default_map(default_map=default_map)
        default_map["element_type"] = "Point"
        if self.point is not None:
            default_map["x"] = self.point[0]
            default_map["y"] = self.point[1]
        else:
            default_map["x"] = 0
            default_map["y"] = 0
        default_map.update(self.settings)
        default_map["stroke"] = self.stroke
        default_map["fill"] = self.fill
        default_map["stroke-width"] = self.stroke_width
        default_map["matrix"] = self.matrix
        return default_map

    def drop(self, drag_node, modify=True):
        # Dragging element into element.
        if drag_node.type.startswith("elem"):
            if modify:
                self.insert_sibling(drag_node)
            return True
        return False

    def revalidate_points(self):
        bounds = self.bounds
        if bounds is None:
            return
        if len(self._points) < 9:
            self._points.extend([None] * (9 - len(self._points)))
        self._points[0] = [bounds[0], bounds[1], "bounds top_left"]
        self._points[1] = [bounds[2], bounds[1], "bounds top_right"]
        self._points[2] = [bounds[0], bounds[3], "bounds bottom_left"]
        self._points[3] = [bounds[2], bounds[3], "bounds bottom_right"]
        cx = (bounds[0] + bounds[2]) / 2
        cy = (bounds[1] + bounds[3]) / 2
        self._points[4] = [cx, cy, "bounds center_center"]
        self._points[5] = [cx, bounds[1], "bounds top_center"]
        self._points[6] = [cx, bounds[3], "bounds bottom_center"]
        self._points[7] = [bounds[0], cy, "bounds center_left"]
        self._points[8] = [bounds[2], cy, "bounds center_right"]
        self._points.append([self.point.x, self.point.y, "point"])

    def update_point(self, index, point):
        return False

    def add_point(self, point, index=None):
        return False
コード例 #26
0
ファイル: elem_image.py プロジェクト: meerk40t/meerk40t
    def process_image(self):
        if self.step_x is None:
            step = UNITS_PER_INCH / self.dpi
            self.step_x = step
            self.step_y = step

        from PIL import Image, ImageEnhance, ImageFilter, ImageOps

        from meerk40t.image.actualize import actualize
        from meerk40t.image.imagetools import dither

        image = self.image
        main_matrix = self.matrix

        r = self.red * 0.299
        g = self.green * 0.587
        b = self.blue * 0.114
        v = self.lightness
        c = r + g + b
        try:
            c /= v
            r = r / c
            g = g / c
            b = b / c
        except ZeroDivisionError:
            pass
        if image.mode != "L":
            image = image.convert("RGB")
            image = image.convert("L", matrix=[r, g, b, 1.0])
        if self.invert:
            image = image.point(lambda e: 255 - e)

        # Calculate device real step.
        step_x, step_y = self.step_x, self.step_y
        if (
            main_matrix.a != step_x
            or main_matrix.b != 0.0
            or main_matrix.c != 0.0
            or main_matrix.d != step_y
        ):
            try:
                image, actualized_matrix = actualize(
                    image,
                    main_matrix,
                    step_x=step_x,
                    step_y=step_y,
                    inverted=self.invert,
                )
            except (MemoryError, DecompressionBombError):
                self.process_image_failed = True
                return
        else:
            actualized_matrix = Matrix(main_matrix)

        if self.invert:
            empty_mask = image.convert("L").point(lambda e: 0 if e == 0 else 255)
        else:
            empty_mask = image.convert("L").point(lambda e: 0 if e == 255 else 255)
        # Process operations.

        for op in self.operations:
            name = op["name"]
            if name == "crop":
                try:
                    if op["enable"] and op["bounds"] is not None:
                        crop = op["bounds"]
                        left = int(crop[0])
                        upper = int(crop[1])
                        right = int(crop[2])
                        lower = int(crop[3])
                        image = image.crop((left, upper, right, lower))
                except KeyError:
                    pass
            elif name == "edge_enhance":
                try:
                    if op["enable"]:
                        if image.mode == "P":
                            image = image.convert("L")
                        image = image.filter(filter=ImageFilter.EDGE_ENHANCE)
                except KeyError:
                    pass
            elif name == "auto_contrast":
                try:
                    if op["enable"]:
                        if image.mode not in ("RGB", "L"):
                            # Auto-contrast raises NotImplementedError if P
                            # Auto-contrast raises OSError if not RGB, L.
                            image = image.convert("L")
                        image = ImageOps.autocontrast(image, cutoff=op["cutoff"])
                except KeyError:
                    pass
            elif name == "tone":
                try:
                    if op["enable"] and op["values"] is not None:
                        if image.mode == "L":
                            image = image.convert("P")
                            tone_values = op["values"]
                            if op["type"] == "spline":
                                spline = ImageNode.spline(tone_values)
                            else:
                                tone_values = [q for q in tone_values if q is not None]
                                spline = ImageNode.line(tone_values)
                            if len(spline) < 256:
                                spline.extend([255] * (256 - len(spline)))
                            if len(spline) > 256:
                                spline = spline[:256]
                            image = image.point(spline)
                            if image.mode != "L":
                                image = image.convert("L")
                except KeyError:
                    pass
            elif name == "contrast":
                try:
                    if op["enable"]:
                        if op["contrast"] is not None and op["brightness"] is not None:
                            contrast = ImageEnhance.Contrast(image)
                            c = (op["contrast"] + 128.0) / 128.0
                            image = contrast.enhance(c)

                            brightness = ImageEnhance.Brightness(image)
                            b = (op["brightness"] + 128.0) / 128.0
                            image = brightness.enhance(b)
                except KeyError:
                    pass
            elif name == "gamma":
                try:
                    if op["enable"] and op["factor"] is not None:
                        if image.mode == "L":
                            gamma_factor = float(op["factor"])

                            def crimp(px):
                                px = int(round(px))
                                if px < 0:
                                    return 0
                                if px > 255:
                                    return 255
                                return px

                            if gamma_factor == 0:
                                gamma_lut = [0] * 256
                            else:
                                gamma_lut = [
                                    crimp(pow(i / 255, (1.0 / gamma_factor)) * 255)
                                    for i in range(256)
                                ]
                            image = image.point(gamma_lut)
                            if image.mode != "L":
                                image = image.convert("L")
                except KeyError:
                    pass
            elif name == "unsharp_mask":
                try:
                    if (
                        op["enable"]
                        and op["percent"] is not None
                        and op["radius"] is not None
                        and op["threshold"] is not None
                    ):
                        unsharp = ImageFilter.UnsharpMask(
                            radius=op["radius"],
                            percent=op["percent"],
                            threshold=op["threshold"],
                        )
                        image = image.filter(unsharp)
                except (KeyError, ValueError):  # Value error if wrong type of image.
                    pass
            elif name == "halftone":
                try:
                    if op["enable"]:
                        image = RasterScripts.halftone(
                            image,
                            sample=op["sample"],
                            angle=op["angle"],
                            oversample=op["oversample"],
                            black=op["black"],
                        )
                except KeyError:
                    pass

        if empty_mask is not None:
            background = Image.new(image.mode, image.size, "white")
            background.paste(image, mask=empty_mask)
            image = background  # Mask exists use it to remove any pixels that were pure reject.

        if self.dither and self.dither_type is not None:
            if self.dither_type != "Floyd-Steinberg":
                image = dither(image, self.dither_type)
            image = image.convert("1")
        inverted_main_matrix = Matrix(main_matrix).inverse()
        self.processed_matrix = actualized_matrix * inverted_main_matrix
        self.processed_image = image
        # self.matrix = actualized_matrix
        self.altered()
        self.process_image_failed = False
コード例 #27
0
ファイル: elem_image.py プロジェクト: meerk40t/meerk40t
    def __init__(
        self,
        image=None,
        matrix=None,
        overscan=None,
        direction=None,
        dpi=500,
        operations=None,
        **kwargs,
    ):
        super(ImageNode, self).__init__(type="elem image", **kwargs)
        self.__formatter = "{element_type} {width}x{height}"
        if "href" in kwargs:
            self.matrix = Matrix()
            try:
                from PIL import Image as PILImage

                self.image = PILImage.open(kwargs["href"])
                if "x" in kwargs:
                    self.matrix.post_translate_x(kwargs["x"])
                if "y" in kwargs:
                    self.matrix.post_translate_x(kwargs["y"])
                real_width, real_height = self.image.size
                declared_width, declared_height = real_width, real_height
                if "width" in kwargs:
                    declared_width = kwargs["width"]
                if "height" in kwargs:
                    declared_height = kwargs["height"]
                try:
                    sx = declared_width / real_width
                    sy = declared_height / real_height
                    self.matrix.post_scale(sx, sy)
                except ZeroDivisionError:
                    pass
            except ImportError:
                self.image = None
        else:
            self.image = image
            self.matrix = matrix
        self.processed_image = None
        self.processed_matrix = None
        self.process_image_failed = False
        self.text = None

        self._needs_update = False
        self._update_thread = None
        self._update_lock = threading.Lock()

        self.settings = kwargs
        self.overscan = overscan
        self.direction = direction
        self.dpi = dpi
        self.step_x = None
        self.step_y = None
        self.lock = False

        self.invert = False
        self.red = 1.0
        self.green = 1.0
        self.blue = 1.0
        self.lightness = 1.0
        self.view_invert = False
        self.dither = True
        self.dither_type = "Floyd-Steinberg"

        if operations is None:
            operations = list()
        self.operations = operations
コード例 #28
0
ファイル: elem_image.py プロジェクト: meerk40t/meerk40t
class ImageNode(Node):
    """
    ImageNode is the bootstrapped node type for the 'elem image' type.

    ImageNode contains a main matrix, main image. A processed image and a processed matrix.
    The processed matrix must be concated with the main matrix to be accurate.
    """

    def __init__(
        self,
        image=None,
        matrix=None,
        overscan=None,
        direction=None,
        dpi=500,
        operations=None,
        **kwargs,
    ):
        super(ImageNode, self).__init__(type="elem image", **kwargs)
        self.__formatter = "{element_type} {width}x{height}"
        if "href" in kwargs:
            self.matrix = Matrix()
            try:
                from PIL import Image as PILImage

                self.image = PILImage.open(kwargs["href"])
                if "x" in kwargs:
                    self.matrix.post_translate_x(kwargs["x"])
                if "y" in kwargs:
                    self.matrix.post_translate_x(kwargs["y"])
                real_width, real_height = self.image.size
                declared_width, declared_height = real_width, real_height
                if "width" in kwargs:
                    declared_width = kwargs["width"]
                if "height" in kwargs:
                    declared_height = kwargs["height"]
                try:
                    sx = declared_width / real_width
                    sy = declared_height / real_height
                    self.matrix.post_scale(sx, sy)
                except ZeroDivisionError:
                    pass
            except ImportError:
                self.image = None
        else:
            self.image = image
            self.matrix = matrix
        self.processed_image = None
        self.processed_matrix = None
        self.process_image_failed = False
        self.text = None

        self._needs_update = False
        self._update_thread = None
        self._update_lock = threading.Lock()

        self.settings = kwargs
        self.overscan = overscan
        self.direction = direction
        self.dpi = dpi
        self.step_x = None
        self.step_y = None
        self.lock = False

        self.invert = False
        self.red = 1.0
        self.green = 1.0
        self.blue = 1.0
        self.lightness = 1.0
        self.view_invert = False
        self.dither = True
        self.dither_type = "Floyd-Steinberg"

        if operations is None:
            operations = list()
        self.operations = operations

    def __copy__(self):
        return ImageNode(
            image=self.image,
            matrix=copy(self.matrix),
            overscan=self.overscan,
            direction=self.direction,
            dpi=self.dpi,
            operations=self.operations,
            **self.settings,
        )

    def __repr__(self):
        return "%s('%s', %s, %s)" % (
            self.__class__.__name__,
            self.type,
            str(self.image),
            str(self._parent),
        )

    @property
    def active_image(self):
        if self.processed_image is not None:
            return self.processed_image
        else:
            return self.image

    @property
    def active_matrix(self):
        if self.processed_matrix is None:
            return self.matrix
        return self.processed_matrix * self.matrix

    def preprocess(self, context, matrix, commands):
        """
        Preprocess step during the cut planning stages.

        We require a context to calculate the correct step values relative to the device
        """
        self.step_x, self.step_y = context.device.dpi_to_steps(self.dpi)
        self.matrix *= matrix
        self._bounds_dirty = True
        self.process_image()

    @property
    def bounds(self):
        if self._bounds_dirty:
            image_width, image_height = self.active_image.size
            matrix = self.active_matrix
            x0, y0 = matrix.point_in_matrix_space((0, 0))
            x1, y1 = matrix.point_in_matrix_space((image_width, image_height))
            x2, y2 = matrix.point_in_matrix_space((0, image_height))
            x3, y3 = matrix.point_in_matrix_space((image_width, 0))
            self._bounds_dirty = False
            self._bounds = (
                min(x0, x1, x2, x3),
                min(y0, y1, y2, y3),
                max(x0, x1, x2, x3),
                max(y0, y1, y2, y3),
            )
        return self._bounds

    def default_map(self, default_map=None):
        default_map = super(ImageNode, self).default_map(default_map=default_map)
        default_map.update(self.settings)
        image = self.active_image
        default_map["width"] = image.width
        default_map["height"] = image.height
        default_map["element_type"] = "Image"
        default_map["matrix"] = self.matrix
        default_map["dpi"] = self.dpi
        default_map["overscan"] = self.overscan
        default_map["direction"] = self.direction
        return default_map

    def drop(self, drag_node, modify=True):
        # Dragging element into element.
        if drag_node.type.startswith("elem"):
            if modify:
                self.insert_sibling(drag_node)
            return True
        return False

    def revalidate_points(self):
        bounds = self.bounds
        if bounds is None:
            return
        if len(self._points) < 9:
            self._points.extend([None] * (9 - len(self._points)))
        self._points[0] = [bounds[0], bounds[1], "bounds top_left"]
        self._points[1] = [bounds[2], bounds[1], "bounds top_right"]
        self._points[2] = [bounds[0], bounds[3], "bounds bottom_left"]
        self._points[3] = [bounds[2], bounds[3], "bounds bottom_right"]
        cx = (bounds[0] + bounds[2]) / 2
        cy = (bounds[1] + bounds[3]) / 2
        self._points[4] = [cx, cy, "bounds center_center"]
        self._points[5] = [cx, bounds[1], "bounds top_center"]
        self._points[6] = [cx, bounds[3], "bounds bottom_center"]
        self._points[7] = [bounds[0], cy, "bounds center_left"]
        self._points[8] = [bounds[2], cy, "bounds center_right"]

    def update_point(self, index, point):
        return False

    def add_point(self, point, index=None):
        return False

    def update(self, context):
        self._needs_update = True
        self.text = "Processing..."
        context.signal("refresh_scene", "Scene")
        if self._update_thread is None:

            def clear(result):
                if self.process_image_failed:
                    self.text = "Process image could not exist in memory."
                else:
                    self.text = None
                self._needs_update = False
                self._update_thread = None
                context.signal("refresh_scene", "Scene")
                context.signal("image updated", self)

            self.processed_image = None
            self.processed_matrix = None
            self._update_thread = context.threaded(
                self.process_image_thread, result=clear, daemon=True
            )

    def process_image_thread(self):
        while self._needs_update:
            self._needs_update = False
            self.process_image()
            # Unset cache.
            self.wx_bitmap_image = None
            self.cache = None

    def process_image(self):
        if self.step_x is None:
            step = UNITS_PER_INCH / self.dpi
            self.step_x = step
            self.step_y = step

        from PIL import Image, ImageEnhance, ImageFilter, ImageOps

        from meerk40t.image.actualize import actualize
        from meerk40t.image.imagetools import dither

        image = self.image
        main_matrix = self.matrix

        r = self.red * 0.299
        g = self.green * 0.587
        b = self.blue * 0.114
        v = self.lightness
        c = r + g + b
        try:
            c /= v
            r = r / c
            g = g / c
            b = b / c
        except ZeroDivisionError:
            pass
        if image.mode != "L":
            image = image.convert("RGB")
            image = image.convert("L", matrix=[r, g, b, 1.0])
        if self.invert:
            image = image.point(lambda e: 255 - e)

        # Calculate device real step.
        step_x, step_y = self.step_x, self.step_y
        if (
            main_matrix.a != step_x
            or main_matrix.b != 0.0
            or main_matrix.c != 0.0
            or main_matrix.d != step_y
        ):
            try:
                image, actualized_matrix = actualize(
                    image,
                    main_matrix,
                    step_x=step_x,
                    step_y=step_y,
                    inverted=self.invert,
                )
            except (MemoryError, DecompressionBombError):
                self.process_image_failed = True
                return
        else:
            actualized_matrix = Matrix(main_matrix)

        if self.invert:
            empty_mask = image.convert("L").point(lambda e: 0 if e == 0 else 255)
        else:
            empty_mask = image.convert("L").point(lambda e: 0 if e == 255 else 255)
        # Process operations.

        for op in self.operations:
            name = op["name"]
            if name == "crop":
                try:
                    if op["enable"] and op["bounds"] is not None:
                        crop = op["bounds"]
                        left = int(crop[0])
                        upper = int(crop[1])
                        right = int(crop[2])
                        lower = int(crop[3])
                        image = image.crop((left, upper, right, lower))
                except KeyError:
                    pass
            elif name == "edge_enhance":
                try:
                    if op["enable"]:
                        if image.mode == "P":
                            image = image.convert("L")
                        image = image.filter(filter=ImageFilter.EDGE_ENHANCE)
                except KeyError:
                    pass
            elif name == "auto_contrast":
                try:
                    if op["enable"]:
                        if image.mode not in ("RGB", "L"):
                            # Auto-contrast raises NotImplementedError if P
                            # Auto-contrast raises OSError if not RGB, L.
                            image = image.convert("L")
                        image = ImageOps.autocontrast(image, cutoff=op["cutoff"])
                except KeyError:
                    pass
            elif name == "tone":
                try:
                    if op["enable"] and op["values"] is not None:
                        if image.mode == "L":
                            image = image.convert("P")
                            tone_values = op["values"]
                            if op["type"] == "spline":
                                spline = ImageNode.spline(tone_values)
                            else:
                                tone_values = [q for q in tone_values if q is not None]
                                spline = ImageNode.line(tone_values)
                            if len(spline) < 256:
                                spline.extend([255] * (256 - len(spline)))
                            if len(spline) > 256:
                                spline = spline[:256]
                            image = image.point(spline)
                            if image.mode != "L":
                                image = image.convert("L")
                except KeyError:
                    pass
            elif name == "contrast":
                try:
                    if op["enable"]:
                        if op["contrast"] is not None and op["brightness"] is not None:
                            contrast = ImageEnhance.Contrast(image)
                            c = (op["contrast"] + 128.0) / 128.0
                            image = contrast.enhance(c)

                            brightness = ImageEnhance.Brightness(image)
                            b = (op["brightness"] + 128.0) / 128.0
                            image = brightness.enhance(b)
                except KeyError:
                    pass
            elif name == "gamma":
                try:
                    if op["enable"] and op["factor"] is not None:
                        if image.mode == "L":
                            gamma_factor = float(op["factor"])

                            def crimp(px):
                                px = int(round(px))
                                if px < 0:
                                    return 0
                                if px > 255:
                                    return 255
                                return px

                            if gamma_factor == 0:
                                gamma_lut = [0] * 256
                            else:
                                gamma_lut = [
                                    crimp(pow(i / 255, (1.0 / gamma_factor)) * 255)
                                    for i in range(256)
                                ]
                            image = image.point(gamma_lut)
                            if image.mode != "L":
                                image = image.convert("L")
                except KeyError:
                    pass
            elif name == "unsharp_mask":
                try:
                    if (
                        op["enable"]
                        and op["percent"] is not None
                        and op["radius"] is not None
                        and op["threshold"] is not None
                    ):
                        unsharp = ImageFilter.UnsharpMask(
                            radius=op["radius"],
                            percent=op["percent"],
                            threshold=op["threshold"],
                        )
                        image = image.filter(unsharp)
                except (KeyError, ValueError):  # Value error if wrong type of image.
                    pass
            elif name == "halftone":
                try:
                    if op["enable"]:
                        image = RasterScripts.halftone(
                            image,
                            sample=op["sample"],
                            angle=op["angle"],
                            oversample=op["oversample"],
                            black=op["black"],
                        )
                except KeyError:
                    pass

        if empty_mask is not None:
            background = Image.new(image.mode, image.size, "white")
            background.paste(image, mask=empty_mask)
            image = background  # Mask exists use it to remove any pixels that were pure reject.

        if self.dither and self.dither_type is not None:
            if self.dither_type != "Floyd-Steinberg":
                image = dither(image, self.dither_type)
            image = image.convert("1")
        inverted_main_matrix = Matrix(main_matrix).inverse()
        self.processed_matrix = actualized_matrix * inverted_main_matrix
        self.processed_image = image
        # self.matrix = actualized_matrix
        self.altered()
        self.process_image_failed = False

    @staticmethod
    def line(p):
        N = len(p) - 1
        try:
            m = [(p[i + 1][1] - p[i][1]) / (p[i + 1][0] - p[i][0]) for i in range(0, N)]
        except ZeroDivisionError:
            m = [1] * N
        # b = y - mx
        b = [p[i][1] - (m[i] * p[i][0]) for i in range(0, N)]
        r = list()
        for i in range(0, p[0][0]):
            r.append(0)
        for i in range(len(p) - 1):
            x0 = p[i][0]
            x1 = p[i + 1][0]
            range_list = [int(round((m[i] * x) + b[i])) for x in range(x0, x1)]
            r.extend(range_list)
        for i in range(p[-1][0], 256):
            r.append(255)
        r.append(round(int(p[-1][1])))
        return r

    @staticmethod
    def spline(p):
        """
        Spline interpreter.

        Returns all integer locations between different spline interpolation values
        @param p: points to be quad spline interpolated.
        @return: integer y values for given spline points.
        """
        try:
            N = len(p) - 1
            w = [(p[i + 1][0] - p[i][0]) for i in range(0, N)]
            h = [(p[i + 1][1] - p[i][1]) / w[i] for i in range(0, N)]
            ftt = (
                [0]
                + [3 * (h[i + 1] - h[i]) / (w[i + 1] + w[i]) for i in range(0, N - 1)]
                + [0]
            )
            A = [(ftt[i + 1] - ftt[i]) / (6 * w[i]) for i in range(0, N)]
            B = [ftt[i] / 2 for i in range(0, N)]
            C = [h[i] - w[i] * (ftt[i + 1] + 2 * ftt[i]) / 6 for i in range(0, N)]
            D = [p[i][1] for i in range(0, N)]
        except ZeroDivisionError:
            return list(range(256))
        r = list()
        for i in range(0, p[0][0]):
            r.append(0)
        for i in range(len(p) - 1):
            a = p[i][0]
            b = p[i + 1][0]
            r.extend(
                int(
                    round(
                        A[i] * (x - a) ** 3
                        + B[i] * (x - a) ** 2
                        + C[i] * (x - a)
                        + D[i]
                    )
                )
                for x in range(a, b)
            )
        for i in range(p[-1][0], 256):
            r.append(255)
        r.append(round(int(p[-1][1])))
        return r

    def as_path(self):
        image_width, image_height = self.active_image.size
        matrix = self.active_matrix
        x0, y0 = matrix.point_in_matrix_space((0, 0))
        x1, y1 = matrix.point_in_matrix_space((0, image_height))
        x2, y2 = matrix.point_in_matrix_space((image_width, image_height))
        x3, y3 = matrix.point_in_matrix_space((image_width, 0))
        return abs(Path(Polygon((x0,y0), (x1,y1), (x2,y2), (x3,y3), (x0,y0))))
コード例 #29
0
    def test_cutcode_image(self):
        """
        Convert CutCode from Image operation
        Test image-based crosshatched setting

        :return:
        """
        laserop = ImageOpNode()

        # Add Path
        initial = "M 0,0 L 100,100 L 0,0 M 50,-50 L 100,-100 M 0,0 Q 100,100 200,0"
        path = Path(initial)
        laserop.add_node(PathNode(path))

        # Add SVG Image1
        image = Image.new("RGBA", (256, 256), (255, 255, 255, 0))
        draw = ImageDraw.Draw(image)
        draw.ellipse((50, 50, 150, 150), "white")
        draw.ellipse((100, 100, 105, 105), "black")
        inode1 = ImageNode(image=image, matrix=Matrix(), dpi=1000.0 / 3.0)
        inode1.step_x = 3
        inode1.step_y = 3
        inode1.process_image()
        laserop.add_node(inode1)

        # Add SVG Image2
        image2 = Image.new("RGBA", (256, 256), (255, 255, 255, 0))
        draw = ImageDraw.Draw(image2)
        draw.ellipse((50, 50, 150, 150), "white")
        draw.ellipse((80, 80, 120, 120), "black")
        inode2 = ImageNode(image=image2, matrix=Matrix(), dpi=500, direction=4)
        inode2.step_x = 2
        inode2.step_y = 2
        inode2.process_image()
        laserop.add_node(inode2)  # crosshatch
        for i in range(2):  # Check for knockon
            cutcode = CutCode(laserop.as_cutobjects())
            self.assertEqual(len(cutcode), 3)

            rastercut = cutcode[0]
            self.assertTrue(isinstance(rastercut, RasterCut))
            self.assertEqual(rastercut.offset_x, 100)
            self.assertEqual(rastercut.offset_y, 100)
            image = rastercut.image
            self.assertTrue(isinstance(image, Image.Image))
            self.assertIn(image.mode, ("L", "1"))
            self.assertEqual(image.size, (2, 2))  # step value 2, 6/2
            self.assertEqual(
                rastercut.path, "M 100,100 L 100,106 L 106,106 L 106,100 Z"
            )

            rastercut1 = cutcode[1]
            self.assertTrue(isinstance(rastercut1, RasterCut))
            self.assertEqual(rastercut1.offset_x, 80)
            self.assertEqual(rastercut1.offset_y, 80)
            image1 = rastercut1.image
            self.assertTrue(isinstance(image1, Image.Image))
            self.assertIn(image1.mode, ("L", "1"))
            self.assertEqual(image1.size, (21, 21))  # default step value 2, 40/2 + 1
            self.assertEqual(rastercut1.path, "M 80,80 L 80,122 L 122,122 L 122,80 Z")

            rastercut2 = cutcode[2]
            self.assertTrue(isinstance(rastercut2, RasterCut))
            self.assertEqual(rastercut2.offset_x, 80)
            self.assertEqual(rastercut2.offset_y, 80)
            image2 = rastercut2.image
            self.assertTrue(isinstance(image2, Image.Image))
            self.assertIn(image2.mode, ("L", "1"))
            self.assertEqual(image2.size, (21, 21))  # default step value 2, 40/2 + 1
            self.assertEqual(rastercut2.path, "M 80,80 L 80,122 L 122,122 L 122,80 Z")
コード例 #30
0
    def test_cutcode_image_crosshatch(self):
        """
        Convert CutCode from Image Operation.
        Test ImageOp Crosshatch Setting

        :return:
        """
        laserop = ImageOpNode(raster_direction=4)

        # Add Path
        initial = "M 0,0 L 100,100 L 0,0 M 50,-50 L 100,-100 M 0,0 Q 100,100 200,0"
        path = Path(initial)
        laserop.add_node(PathNode(path))

        # Add SVG Image1
        image1 = Image.new("RGBA", (256, 256), (255, 255, 255, 0))
        draw = ImageDraw.Draw(image1)
        draw.ellipse((50, 50, 150, 150), "white")
        draw.ellipse((100, 100, 105, 105), "black")
        inode = ImageNode(image=image1, matrix=Matrix(), dpi=1000.0 / 3.0)
        inode.step_x = 3
        inode.step_y = 3
        inode.process_image()
        laserop.add_node(inode)

        # Add SVG Image2
        image2 = Image.new("RGBA", (256, 256), (255, 255, 255, 0))
        draw = ImageDraw.Draw(image2)
        draw.ellipse((50, 50, 150, 150), "white")
        draw.ellipse((80, 80, 120, 120), "black")
        inode = ImageNode(image=image2, matrix=Matrix(), dpi=500.0)
        inode.step_x = 2
        inode.step_y = 2
        inode.process_image()
        laserop.add_node(inode)

        # Add SVG Image3
        inode = ImageNode(image=image2, matrix=Matrix(), dpi=1000.0 / 3.0)
        inode.step_x = 3
        inode.step_y = 3
        inode.process_image()
        laserop.add_node(inode)

        cutcode = CutCode(laserop.as_cutobjects())
        self.assertEqual(len(cutcode), 6)

        rastercut1_0 = cutcode[0]
        self.assertTrue(isinstance(rastercut1_0, RasterCut))
        self.assertEqual(rastercut1_0.offset_x, 100)
        self.assertEqual(rastercut1_0.offset_y, 100)
        image = rastercut1_0.image
        self.assertTrue(isinstance(image, Image.Image))
        self.assertIn(image.mode, ("L", "1"))
        self.assertEqual(image.size, (2, 2))  # step value 2, 6/2
        self.assertEqual(rastercut1_0.path, "M 100,100 L 100,106 L 106,106 L 106,100 Z")

        rastercut1_1 = cutcode[1]
        self.assertTrue(isinstance(rastercut1_1, RasterCut))
        self.assertEqual(rastercut1_1.offset_x, 100)
        self.assertEqual(rastercut1_1.offset_y, 100)
        image = rastercut1_1.image
        self.assertTrue(isinstance(image, Image.Image))
        self.assertIn(image.mode, ("L", "1"))
        self.assertEqual(image.size, (2, 2))  # step value 2, 6/2
        self.assertEqual(rastercut1_1.path, "M 100,100 L 100,106 L 106,106 L 106,100 Z")

        rastercut2_0 = cutcode[2]
        self.assertTrue(isinstance(rastercut2_0, RasterCut))
        self.assertEqual(rastercut2_0.offset_x, 80)
        self.assertEqual(rastercut2_0.offset_y, 80)
        image1 = rastercut2_0.image
        self.assertTrue(isinstance(image1, Image.Image))
        self.assertIn(image1.mode, ("L", "1"))
        self.assertEqual(image1.size, (21, 21))  # default step value 2, 40/2 + 1
        self.assertEqual(rastercut2_0.path, "M 80,80 L 80,122 L 122,122 L 122,80 Z")

        rastercut2_1 = cutcode[3]
        self.assertTrue(isinstance(rastercut2_1, RasterCut))
        self.assertEqual(rastercut2_1.offset_x, 80)
        self.assertEqual(rastercut2_1.offset_y, 80)
        image2 = rastercut2_1.image
        self.assertTrue(isinstance(image2, Image.Image))
        self.assertIn(image2.mode, ("L", "1"))
        self.assertEqual(image2.size, (21, 21))  # default step value 2, 40/2 + 1
        self.assertEqual(rastercut2_0.path, "M 80,80 L 80,122 L 122,122 L 122,80 Z")

        rastercut3_0 = cutcode[4]
        self.assertTrue(isinstance(rastercut3_0, RasterCut))
        self.assertEqual(rastercut3_0.offset_x, 80)
        self.assertEqual(rastercut3_0.offset_y, 80)
        image3 = rastercut3_0.image
        self.assertTrue(isinstance(image3, Image.Image))
        self.assertIn(image3.mode, ("L", "1"))
        self.assertEqual(image3.size, (14, 14))  # default step value 3, ceil(40/3) + 1
        self.assertEqual(rastercut3_0.path, "M 80,80 L 80,122 L 122,122 L 122,80 Z")

        rastercut3_1 = cutcode[5]
        self.assertTrue(isinstance(rastercut3_1, RasterCut))
        self.assertEqual(rastercut3_1.offset_x, 80)
        self.assertEqual(rastercut3_1.offset_y, 80)
        image4 = rastercut3_1.image
        self.assertTrue(isinstance(image4, Image.Image))
        self.assertIn(image4.mode, ("L", "1"))
        self.assertEqual(image4.size, (14, 14))  # default step value 3, ceil(40/3) + 1
        self.assertEqual(rastercut2_0.path, "M 80,80 L 80,122 L 122,122 L 122,80 Z")