Beispiel #1
0
    def calculate_preview_geometry_pocket(self):
        '''
        '''
        if self.geometry is not None:
            offset = self.margin.toMm()

            # 'left' in 'inside', and 'right' is 'outside'
            self.geometry = ShapelyUtils.orientMultiPolygon(self.geometry)
            _, self.preview_geometry = ShapelyUtils.offsetMultiPolygon(
                self.geometry, offset, 'left', ginterior=True)

            ShapelyUtils.MatplotlibDisplay("preview pocket",
                                           self.preview_geometry)

            self.preview_geometry = ShapelyUtils.orientMultiPolygon(
                self.preview_geometry)

            ShapelyUtils.MatplotlibDisplay("preview pocket - oriented",
                                           self.preview_geometry)

            self.geometry_svg_paths = []

            for poly in self.preview_geometry.geoms:
                geometry_svg_path = SvgPath.fromShapelyPolygon(
                    "pycut_geometry_pocket", poly)
                self.geometry_svg_paths.append(geometry_svg_path)
Beispiel #2
0
    def calculate_preview_geometry_inside(self, toolModel: ToolModel):
        '''
        '''
        if self.geometry is not None:
            margin = self.margin.toMm()

            if margin != 0:
                _, geometry = ShapelyUtils.offsetMultiPolygon(
                    self.geometry, margin, 'left')
            else:
                geometry = self.geometry

            toolData = toolModel.getCamData()

            width = self.width.toMm()

            if width < toolData["diameterTool"]:
                width = toolData["diameterTool"]

            _, innergeometry = ShapelyUtils.offsetMultiPolygon(
                geometry, width, 'left')
            self.preview_geometry = geometry.difference(innergeometry)

            if self.preview_geometry.geom_type == 'Polygon':
                self.preview_geometry = shapely.geometry.MultiPolygon(
                    [self.preview_geometry])

        self.geometry_svg_paths = []

        # should have 2 paths, one inner, one outer -> show the "ring"
        for poly in self.preview_geometry.geoms:
            geometry_svg_path = SvgPath.fromShapelyPolygon(
                "pycut_geometry_pocket", poly)
            self.geometry_svg_paths.append(geometry_svg_path)
Beispiel #3
0
    def engrave(cls, geometry: shapely.geometry.MultiPolygon, climb: bool) -> List[CamPath] :
        '''
        Compute paths for engrave operation on Shapely geometry. 
        
        Returns array of CamPath.
        '''
        # use lines, not polygons
        multiline_ext = ShapelyUtils.multiPolyToMultiLine(geometry)
        multiline_int = ShapelyUtils.multiPolyIntToMultiLine(geometry)

        full_line = shapely.ops.linemerge(list(multiline_ext.geoms) + list(multiline_int.geoms))

        if full_line.geom_type == 'LineString':
            camPaths = [ CamPath( full_line, False) ]
            return camPaths

        allPaths = []
        for line in full_line.geoms:
            coords = list(line.coords)  # JSCUT: path = paths.slice(0)
            if not climb:
                coords.reverse()
        
            coords.append(coords[0])
            allPaths.append(shapely.geometry.LineString(coords))
            
        camPaths = [ CamPath(path, False)  for path in allPaths ]
        return camPaths
Beispiel #4
0
    def combine(self):
        '''
        generate the combinaison of the selected paths

        the generated geometry is a MultiPolygon
        '''
        self.shapely_polygons: List[shapely.geometry.Polygon] = []

        for svg_path in self.svg_paths:
            shapely_polygons = svg_path.toShapelyPolygons()
            self.shapely_polygons += shapely_polygons

        if len(self.shapely_polygons) == 0:
            return

        if len(self.shapely_polygons) == 1:
            self.geometry = shapely.geometry.MultiPolygon(
                self.shapely_polygons)
            return

        o = self.shapely_polygons[0]
        #other = shapely.geometry.MultiPolygon(self.shapely_polygons[1:])
        other = ShapelyUtils.union_polygons(self.shapely_polygons[1:])

        if self.combinaison == "Union":
            geometry = o.union(other)
        if self.combinaison == "Intersection":
            geometry = o.intersection(other)
        if self.combinaison == "Difference":
            geometry = o.difference(other)
        if self.combinaison == "Xor":
            geometry = o.symmetric_difference(other)

        self.geometry = geometry

        # what!! result may be not well orienteted!!
        if self.geometry.geom_type == 'Polygon':
            # fix orientation
            self.geometry = ShapelyUtils.reorder_poly_points(self.geometry)

            self.geometry = shapely.geometry.polygon.orient(self.geometry)
            self.geometry = shapely.geometry.MultiPolygon([self.geometry])
        else:
            # fix orientation
            fixed_polys = []
            for poly in self.geometry:
                if not poly.geom_type == 'Polygon':
                    continue
                # fix - do not start a poly from a convex corner
                poly = ShapelyUtils.reorder_poly_points(poly)

                fixed_poly = shapely.geometry.polygon.orient(poly)
                fixed_polys.append(fixed_poly)
            self.geometry = shapely.geometry.MultiPolygon(fixed_polys)
Beispiel #5
0
    def calculate_toolpaths(self, svgModel: SvgModel, toolModel: ToolModel,
                            materialModel: MaterialModel):
        '''
        '''
        toolData = toolModel.getCamData()

        name = self.name
        ramp = self.ramp
        cam_op = self.cam_op
        direction = self.direction
        cutDepth = self.cutDepth
        margin = self.margin
        width = self.width

        geometry = self.geometry

        offset = margin.toMm()

        #if cam_op == "Outside" :
        #    #direction = "outer2inner"
        #    direction = "inner2outer"
        #    if direction == "outer2inner":
        #        # start from the outer ring and cam in the inside dir
        #        width = self.width.toMm()
        #        if width < toolData["diameterTool"]:
        #            width = toolData["diameterTool"]
        #        offset += width
        #    else:
        #        # start from the inner ring and cam in the outside dir
        #        pass

        if cam_op != "Engrave":
            # 'left' for Inside OR pocket, 'right' for Outside
            _, geometry = ShapelyUtils.offsetMultiPolygon(
                geometry,
                offset,
                'right' if cam_op == 'Outside' else 'left',
                ginterior=True)

        if cam_op == "Pocket":
            self.cam_paths = cam.pocket(geometry, toolData["diameterTool"],
                                        1 - toolData["stepover"],
                                        direction == "Climb")
        elif cam_op == "Inside" or cam_op == "Outside":
            width = width.toMm()
            if width < toolData["diameterTool"]:
                width = toolData["diameterTool"]
            self.cam_paths = cam.outline(geometry, toolData["diameterTool"],
                                         cam_op == "Inside", width,
                                         1 - toolData["stepover"],
                                         direction == "Climb")
        elif cam_op == "Engrave":
            self.cam_paths = cam.engrave(geometry, direction == "Climb")

        for cam_path in self.cam_paths:
            svg_path = SvgPath.fromShapelyLineString("pycut_toolpath",
                                                     cam_path.path,
                                                     cam_path.safeToClose)
            self.cam_paths_svg_paths.append(svg_path)
Beispiel #6
0
    def calculate_preview_geometry_outside(self, toolModel: ToolModel):
        '''
        '''
        # shapely: first the outer, then the inner hole
        toolData = toolModel.getCamData()

        if self.geometry is not None:
            width = self.width.toMm()
            if width < toolData["diameterTool"]:
                width = toolData["diameterTool"]

            margin = self.margin.toMm()
            margin_plus_width = margin + width

            # 'right' in 'inside', and 'left' is 'outside'  hopefully
            _, geometry_outer = ShapelyUtils.offsetMultiPolygon(
                self.geometry,
                margin_plus_width,
                'right',
                resolution=16,
                join_style=1,
                mitre_limit=5)
            _, geometry_inner = ShapelyUtils.offsetMultiPolygon(self.geometry,
                                                                margin,
                                                                'right',
                                                                resolution=16,
                                                                join_style=1,
                                                                mitre_limit=5)

            self.preview_geometry = geometry_outer.difference(geometry_inner)
            self.preview_geometry = shapely.geometry.polygon.orient(
                self.preview_geometry)  # check if necessary!

            if self.preview_geometry.geom_type == 'Polygon':
                self.preview_geometry = shapely.geometry.MultiPolygon(
                    [self.preview_geometry])

        self.geometry_svg_paths = []

        # should have 2 paths, one inner, one outer -> show the "ring"
        for poly in self.preview_geometry.geoms:
            geometry_svg_path = SvgPath.fromShapelyPolygon(
                "pycut_geometry_pocket", poly)
            self.geometry_svg_paths.append(geometry_svg_path)
Beispiel #7
0
    def toShapelyPolygons(self) -> List[shapely.geometry.Polygon]:
        '''
        The main method to transform a svg path into a polygon or a list of polygons
        - if only 1 [Mm] inside the svg path, then it is a simple [closed] line ie a polygon without holes
        - if more than 1 [Mm] inside the svg path, then it is a polygon with holes
        -> split them, the one with the largest area is the exterior, the others are the holes

        -> Wow! not automatically! for example for 'i' and 'j' there are 2 [Mm] but no interior,
        indeed 'i' is composed of 2 distincts paths, as 'j' also is.

        -> so we have infact to check if the simple paths are included in the larger one, or not
        '''
        is_simple_path = self._is_simple_path()

        polys = []

        if is_simple_path == True:
            line = self._toShapelyLineString()
            poly = shapely.geometry.Polygon(line)

            # set the right orientation for this polygon
            poly = shapely.geometry.polygon.orient(poly)
            polys.append(poly)
        else:
            # generate temporary svgpath objects with simple paths
            # to build polygon(s) with holes
            svgpaths = self._generate_simple_svgpaths()

            # from these simple path, build list of polygons
            # some of them will have holes, other not...
            data = {}
            for svgpath in svgpaths:
                linestring = svgpath._toShapelyLineString()
                s_polygon = svgpath._toShapelySimplePolygon()

                data[svgpath.p_id] = {
                    "id": svgpath.p_id,
                    "svgpath": svgpath,
                    "linestring": linestring,
                    "s_polygon": s_polygon,
                    "area": s_polygon.area,
                    "exterior": None,
                    "interiors": []
                }

            data_sorted = []
            for p_id in data:
                data_sorted.append(data[p_id])

            # sort from the largest area first
            data_sorted.sort(key=lambda x: -x.get('area'))

            # build the polygons one after the other, starting from the biggest one
            # NOTE: polygons SHOULD note overlap themselves

            # the first is the largest one
            xpoly = data_sorted[0]
            xpoly["exterior"] = xpoly["linestring"]

            xpolys = [xpoly]

            # the others
            for data in data_sorted[1:]:
                line = data["linestring"]

                is_hole = False
                for xpoly in xpolys:  # the existing polys
                    if xpoly["s_polygon"].covers(line):
                        is_hole = True
                        xpoly["interiors"].append(line)
                        break

                if is_hole is False:
                    # is a separate polygon
                    data["exterior"] = line
                    xpolys.append(data)  # append to the list of existing polys

                # sort the list - the smallest are the first to be searched
                xpolys.sort(key=lambda x: x.get('area'))

            # ok, time to build the "real" polygons
            all_polys = []
            for xpoly in xpolys:
                if xpoly["interiors"]:
                    poly = shapely.geometry.Polygon(xpoly["exterior"],
                                                    holes=xpoly["interiors"])
                else:
                    poly = shapely.geometry.Polygon(xpoly["exterior"])

                all_polys.append(poly)

            # wow, for some letters (D, P) there is a problem (TO INQUIRE)
            # D: exterior/interior is wrong
            # P: interior in wrong
            polys = []
            for poly in all_polys:
                poly = ShapelyUtils.fixGenericPolygon(poly)
                polys.append(poly)

        return polys
Beispiel #8
0
    def pocket(cls, geometry: shapely.geometry.MultiPolygon, cutterDia: float, overlap: float, climb: bool) -> List[CamPath] :
        '''
        Compute paths for pocket operation on Shapely geometry. 
        
        Returns array of CamPath.
        
        cutterDia is in "UserUnit" units. 
        overlap is in the range [0, 1).
        '''
        # use polygons exteriors lines - offset them and and diff with the interiors if any
        geometry = ShapelyUtils.orientMultiPolygon(geometry)

        ShapelyUtils.MatplotlibDisplay("geom pocket init", geometry)

        # the exterior
        multi_offset, current = ShapelyUtils.offsetMultiPolygon(geometry, cutterDia / 2, 'left', ginterior=True)
        
        current = ShapelyUtils.simplifyMultiPoly(current, 0.001)
        current = ShapelyUtils.orientMultiPolygon(current)

        for geom in current.geoms:
            ShapelyUtils.MatplotlibDisplay("geom pocket init after simplify", geom)

        if not current:
            return []
            
        if len(current.geoms) == 0:
            # cannot offset ! maybe geometry too narrow for the cutter
            return []

        # bound must be the exterior enveloppe + the interiors polygons
        #bounds = geometry 

        # no! the bounds are from the first offset with width cutterDia / 2
        bounds = shapely.geometry.MultiPolygon(current)

        allPaths : List[shapely.geometry.LineString] = []

        # -------------------------------------------------------------
        def collect_paths(multi_offset, allPaths):
            lines_ok = []
            
            for offset in multi_offset:
                if offset.geom_type == 'LineString':
                    if len(list(offset.coords)) > 0:
                        lines_ok.append(offset)

                    #print("---- SIMPLIFY #nb pts = ", len(list(offset.coords)))
                    print("---- SIMPLIFY len = ", offset.length)

                if offset.geom_type == 'MultiLineString':
                    for geom in offset.geoms:
                        if geom.geom_type == 'LineString':
                            if len(list(geom.coords)) > 0:
                                lines_ok.append(geom)

                            print("---- SIMPLIFY #nb pts = ", len(list(geom.coords)))
                            print("---- SIMPLIFY len = ", geom.length)

            allPaths = lines_ok + allPaths

            return allPaths
        # -------------------------------------------------------------

        while True:
            #if climb:
            #    for line in current:
            #        line.reverse()

            allPaths = collect_paths(multi_offset, allPaths)

            multi_offset, current = ShapelyUtils.offsetMultiPolygon(current, cutterDia * (1 - overlap), 'left')
            
            if not current:
                allPaths = collect_paths(multi_offset, allPaths)
                break
            current = ShapelyUtils.simplifyMultiPoly(current, 0.001)
            if not current:
                allPaths = collect_paths(multi_offset, allPaths)
                break
            current = ShapelyUtils.orientMultiPolygon(current)
           
            if not multi_offset:
                break

        # last: make beautiful interiors, only 1 step
        interior_multi_offset, _ = ShapelyUtils.offsetMultiPolygonInteriors(geometry, cutterDia / 2, 'left', gexterior=True)
        allPaths = collect_paths(interior_multi_offset, allPaths)
        # - done !

        return cls.mergePaths(bounds, allPaths)
Beispiel #9
0
    def mergePaths(cls, _bounds: shapely.geometry.MultiPolygon, paths: List[shapely.geometry.LineString]) -> List[CamPath] :
        '''
        Try to merge paths. A merged path doesn't cross outside of bounds AND the interior polygons
        '''
        #ShapelyUtils.MatplotlibDisplay("mergePath", shapely.geometry.MultiLineString(paths), force=True)

        if _bounds and len(_bounds.geoms) > 0:
            bounds = _bounds
        else: 
            bounds = shapely.geometry.MultiPolygon()
 
 
        ext_lines = ShapelyUtils.multiPolyToMultiLine(bounds)
        int_polys = []
        for poly in bounds.geoms:
            if poly.interiors:
                for interior in poly.interiors:
                    int_poly = shapely.geometry.Polygon(interior)
                    int_polys.append(int_poly)
        if int_polys:
            int_multipoly = shapely.geometry.MultiPolygon(int_polys)
        else:
            int_multipoly = None

        # std list
        thepaths = [ list(path.coords) for path in paths ]
        paths = thepaths

        currentPath = paths[0]
        
        pathEndPoint = currentPath[-1]
        pathStartPoint = currentPath[0]

        # close if start/end point not equal - why ? I could have simple lines!
        if pathEndPoint[0] != pathStartPoint[0] or pathEndPoint[1] != pathStartPoint[1]:
            currentPath = currentPath + [pathStartPoint]
        
        currentPoint = currentPath[-1]
        paths[0] = [] # empty

        mergedPaths : List[shapely.geometry.LineString] = [] 
        numLeft = len(paths) - 1

        while numLeft > 0 :
            closestPathIndex = None
            closestPointIndex = None
            closestPointDist = sys.maxsize
            for pathIndex, path in enumerate(paths):
                for pointIndex, point in enumerate(path):
                    dist = cam.distP(currentPoint, point)
                    if dist < closestPointDist:
                        closestPathIndex = pathIndex
                        closestPointIndex = pointIndex
                        closestPointDist = dist

            path = paths[closestPathIndex]
            paths[closestPathIndex] = [] # empty
            numLeft -= 1
            needNew = ShapelyUtils.crosses(ext_lines, currentPoint, path[closestPointIndex])
            if (not needNew) and int_multipoly:
                needNew = ShapelyUtils.crosses(int_multipoly, currentPoint, path[closestPointIndex])

            # JSCUT path = path.slice(closestPointIndex, len(path)).concat(path.slice(0, closestPointIndex))
            path = path[closestPointIndex:] + path[:closestPointIndex]
            path.append(path[0])

            if needNew:
                mergedPaths.append(currentPath)
                currentPath = path
                currentPoint = currentPath[-1]
            else:
                currentPath = currentPath + path
                currentPoint = currentPath[-1]

        mergedPaths.append(currentPath)

        camPaths : List[CamPath] = []
        for path in mergedPaths:
            safeToClose = not ShapelyUtils.crosses(bounds, path[0], path[-1])
            camPaths.append( CamPath( shapely.geometry.LineString(path), safeToClose) )

        return camPaths
Beispiel #10
0
    def outline(cls, geometry: shapely.geometry.MultiPolygon, cutterDia: float, isInside: bool, width: float, overlap: float, climb: bool) -> List[CamPath] :
        '''
        Compute paths for outline operation on Shapely geometry. 
        
        Returns array of CamPath.
        
        cutterDia and width are in Shapely units. 
        overlap is in the  range [0, 1).
        '''
        # use lines, not polygons
        multiline = ShapelyUtils.multiPolyToMultiLine(geometry)

        currentWidth = cutterDia
        allPaths  : List[shapely.geometry.LineString] = []
        eachWidth = cutterDia * (1 - overlap)

        if isInside :
            # because we always start from the outer ring -> we go "inside"
            current = ShapelyUtils.offsetMultiLine(multiline, cutterDia /2, 'left')
            offset = ShapelyUtils.offsetMultiLine(multiline, width - cutterDia / 2, 'left')
            #bounds = ShapelyUtils.diff(current, offset)
            bounds = current
            eachOffset = eachWidth
            needReverse = climb
        else :
            direction = "inner2outer"
            #direction = "outer2inner"

            if direction == "inner2outer":
                # because we always start from the inner ring -> we go "outside"
                current = ShapelyUtils.offsetMultiLine(multiline, cutterDia /2, 'right')
                offset = ShapelyUtils.offsetMultiLine(multiline, width - cutterDia / 2, 'right')
                #bounds = ShapelyUtils.diff(current, offset)
                bounds = current
            else:
                # because we always start from the outer ring -> we go "inside"
                current = ShapelyUtils.offsetMultiLine(multiline, cutterDia /2, 'left')
                offset = ShapelyUtils.offsetMultiLine(multiline, width - cutterDia / 2, 'left')
                #bounds = ShapelyUtils.diff(current, offset)
                bounds = current

            eachOffset = eachWidth
            needReverse = not climb

            # TEST
            #allPaths = [p for p in current.geoms] 

        while True and currentWidth <= width :
            if needReverse:
                reversed = []
                for path in current.geoms:
                    coords = list(path.coords)  # is a tuple!  JSCUT current reversed in place
                    coords.reverse()
                    reversed.append(shapely.geometry.LineString(coords))
                allPaths = reversed + allPaths  # JSCUT: allPaths = current.concat(allPaths)
            else:
                allPaths = [p for p in current.geoms] + allPaths  # JSCUT: allPaths = current.concat(allPaths)

            nextWidth = currentWidth + eachWidth
            if nextWidth > width and (width - currentWidth) > 0 :
                # >>> XAM fix
                last_delta = width - currentWidth
                # <<< XAM fix
                current = ShapelyUtils.offsetMultiLine(current, last_delta, 'left')
                if current :
                    current = ShapelyUtils.simplifyMultiLine(current, 0.01)
                
                if current:
                    if needReverse:
                        reversed = []
                        for path in current.geoms:
                            coords = list(path.coords)  # is a tuple!  JSCUT current reversed in place
                            coords.reverse()
                            reversed.append(shapely.geometry.LineString(coords))
                        allPaths = reversed + allPaths # JSCUT: allPaths = current.concat(allPaths)
                    else:
                        allPaths = [p for p in current.geoms] + allPaths # JSCUT: allPaths = current.concat(allPaths)
                    break
            
            currentWidth = nextWidth

            if not current:
                break

            current = ShapelyUtils.offsetMultiLine(current, eachOffset, 'left', resolution=16)
            if current:
                current = ShapelyUtils.simplifyMultiLine(current, 0.01)
                print("--- next toolpath")
            else:
                break

        if len(allPaths) == 0: 
            # no possible paths! TODO . inform user
            return []

        # mergePaths need MultiPolygon
        bounds = ShapelyUtils.multiLineToMultiPoly(bounds)

        return cls.mergePaths(bounds, allPaths)