Impression 3D pseudo 5 axes : Différence entre versions

De Wiki LOGre
Aller à : navigation, rechercher
m (Sphère vers plan)
m (Sphère vers plan)
Ligne 49 : Ligne 49 :
 
* la portion de sphère doit être posée sur le plan Z=0, orientée vers le bas, et centrée sur l'axe Z
 
* la portion de sphère doit être posée sur le plan Z=0, orientée vers le bas, et centrée sur l'axe Z
 
* il faut donner :
 
* il faut donner :
** le rayon intérieur de la sphère intérieur, qui touche le plan XY (RADIUS_INT)
+
** le rayon intérieur de la sphère (RADIUS_INT)
** la position du centre de la sphère sur l'axe Z (SPHERE_ORIGIN_Z)
+
** la position verticale du centre de la sphère sur l'axe Z (SPHERE_ORIGIN_Z)
  
 
<syntaxhighlight lang="python">
 
<syntaxhighlight lang="python">

Version du 8 juillet 2019 à 15:14

Langue : Français  • English

Projet réalisé par Fma38.

En cours.

Présentation

Le but de ce projet est de pouvoir imprimer les panneaux du robot BB-8 de manière optimale.

BB-8 orange-panel.png

Étude

Ces panneaux sont des portions de sphère. Si on les imprime de manière classique, il faudra beaucoup de support. De plus, il y aura un effet d'escalier de plus en plus marqué vers le sommet ; cet effet pourrait être atténué en utilisant une hauteur de couche variable, au détriment du temps d'impression. Dans tous les cas, les couches resteront très visibles.

Une solution est d'utiliser une imprimante 5 axes, et d'imprimer le long de la sphère : les couches ne seraient alors plus visibles, puisque la surface du haut correspondrait à celle d'une pièce plane classique.

Si l'imprimante n'est pas très difficile à réaliser (!), il en va tout autrement du trancheur. À ce jour, seules quelques sociétés ont une solution de ce type, propriétaire, bien sûr.

Mais, au contraire d'une pièce quelconque, ici, on connaît parfaitement la géométrie - simple - de la pièce : une sphère. Il suffirait donc que le trancheur parcours la pièce selon cette sphère, plutôt que le plan XY. Mais même cette modification n'est pas triviale, et bien au delà de mes compétences. Et je doute d'arriver à motiver un développeur de trancheur d'implémenter ça.

Heureusement, il y a une autre solution, beaucoup plus accessible, qui est de mapper la pièce sphérique sur le plan ! Cela se fait très simplement avec un script Python (cf ci-dessous). Une fois la pièce plane, n'importe quel trancheur de base fait l'affaire : il pensera parcourir la pièce suivant un plan, alors qu'il suit en fait une sphère ;o)

Il reste ensuite à transformer les coordonnées X/Y/Z en coordonnées X/Y/Z/A/B (où A et B sont les 2 axes supplémentaires, les rotations de la tête ou du plateau), de telle sorte que la tête se déplace bien sur la sphère plutôt que le plan. Cette transformation peut se faire soit dans le firmware (j'utilise la carte Duet - avec le firmware RRF adapté - pour laquelle il est relativement simple de développer une nouvelle cinématique), soit en modifiant le G-Code. C'est certainement cette seconde solution que j'adopterai dans un premier temps.

Transformation sphère -> plan

TODO.

Cette partie semble bien fonctionner. Voici ce que ça donne sur une petite sphère toute simple :

Sphere.png Sphere mapped.png

Transformation plan -> sphère

Une fois la pièce mappée sur un plan, puis tranchée de manière classique, il faut donc générer les coordonnées X/Y/Z/A/B de la machine 5 axes à partir des coordonnées X/Y/Z.

Code

Sphère vers plan

Voici le code Python permettant de transformer un fichier STL (ascii ou binaire).

Attention :

  • la portion de sphère doit être posée sur le plan Z=0, orientée vers le bas, et centrée sur l'axe Z
  • il faut donner :
    • le rayon intérieur de la sphère (RADIUS_INT)
    • la position verticale du centre de la sphère sur l'axe Z (SPHERE_ORIGIN_Z)
# -*- coding: utf-8 -*-

"""
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
or see:

  http://www.gnu.org/licenses/gpl.html

author: Frédéric Mantegazza
copyright: (C) 2019 Frédéric Mantegazza
"""

import re
import sys
import math
import struct
import os.path

import numpy

RADIUS_INT = 65.
SPHERE_ORIGIN_Z = -51.235


class Vector(list):
    def __str__(self):
        return "%.5f %.5f %.5f" % (self[0], self[1], self[2])


class Vertex(list):
    def __str__(self):
        return "%.5f %.5f %.5f" % (self[0], self[1], self[2])

    def mapToPlan(self, radiusInt, sphereOriginZ):
        """
        """
        L = math.sqrt(self[0] ** 2 + self[1] ** 2 + (self[2] - sphereOriginZ) ** 2)
        alpha = math.atan2(math.sqrt(self[0] ** 2 + self[1] ** 2), self[2] - sphereOriginZ)
        beta = math.atan2(self[1], self[0])

        x = alpha * L * math.cos(beta)
        y = alpha * L * math.sin(beta)
        z = L - radiusInt

        return Vertex([x, y, z])


class Facet(object):
    def __init__(self, normal, vertices):
        if len(vertices) != 3:
            raise ValueError("A facet must have 3 vertices")

        self._normal = normal
        self._vertices = vertices

    @property
    def normal(self):
        return self._normal

    @property
    def vertices(self):
        return self._vertices

    def __str__(self):
        s = "  facet normal %s\n" % self._normal
        s += "    outer loop\n"
        for vertex in self._vertices:
            s += "      vertex %s\n" % vertex
        s += "    endloop\n"
        s += "  endfacet"

        return s


class Solid(object):
    def __init__(self, name="Onshape"):
        self._name = name
        self._facets = []

    def __str__(self):
        s = "solid %s\n" % self._name
        for facet in self._facets:
            s += "%s\n" % facet
        s += "endsolid %s\n" % self._name

        return s

    @property
    def name(self):
        return self._name

    @property
    def facets(self):
        return self._facets

    def addFacet(self, facet):
        self._facets.append(facet)

    def mapToPlan(self):
        newSolid = Solid("%s_mapped" % self._name)

        for i, facet in enumerate(self._facets):
            newVertices = []
            for vertex in facet.vertices:
                newVertex = vertex.mapToPlan(RADIUS_INT, SPHERE_ORIGIN_Z)
                newVertices.append(newVertex)

            # Compute new normal
            vector1 = [b-a for a, b in zip(newVertices[0], newVertices[1])]
            vector2 = [b-a for a, b in zip(newVertices[0], newVertices[2])]
            newNormal  = numpy.cross(vector1, vector2)
            newNormal /= numpy.sqrt(newNormal.dot(newNormal))

            newFacet = Facet(Vector(newNormal), newVertices)
            newSolid.addFacet(newFacet)

        return newSolid


def parseAscii(inputFile):
    inputFile.seek(0)
    inputStr = inputFile.read()

    # Iter over 'solid' (should be only one)
    for solidStr in re.findall(r"solid\s(.*?)endsolid", inputStr, re.S):
        solidName = re.match(r"^(.*)$", solidStr, re.M).group(0)
        print "Processing object '%s'..." % solidName

        solid = Solid(solidName)

        # Iter over 'facet normal'
        for facetNormalStr in re.findall(r"facet\snormal\s(.*?)endfacet", solidStr, re.S):
            coords = [float(coord) for coord in facetNormalStr.split('\n')[0].split()]
            normal = Vector(coords)

            # Iter over 'outer loop' (should be only one)
            for outerLoopStr in re.findall(r"outer\sloop(.*?)endloop", facetNormalStr, re.S):
                vertices = []

                # Iter over 'vertex'
                for vertexStr in re.findall(r"vertex\s(.*)$", outerLoopStr, re.M):
                    coords = [float(coord) for coord in vertexStr.split()]
                    vertex = Vertex(coords)
                    vertices.append(vertex)

            facet = Facet(normal, vertices)
            solid.addFacet(facet)

    return solid


def parseBinary(inputFile):

    # Skip header
    inputFile.seek(80)

    nbFacets = struct.unpack("<I", inputFile.read(4))[0]
    print "found %d facets" % nbFacets

    solid = Solid("Onshape")

    # Iterate over facets
    for i in range(nbFacets):

        # Get normal
        coords = struct.unpack("<fff", inputFile.read(3*4))
        normal = Vector(coords)

        vertices = []

        # Iterate over vertices
        for j in range(3):
            coords = struct.unpack("<fff", inputFile.read(3*4))
            vertex = Vertex(coords)
            vertices.append(vertex)

        facet = Facet(normal, vertices)
        solid.addFacet(facet)

        # Skip byte count
        inputFile.seek(2, 1)

    return solid


def main():
    inputFileName = sys.argv[1]
    inputFile = file(inputFileName)
    if inputFile.read(5) == "solid":
        print "ascii file"
        solid = parseAscii(inputFile)
    else:
        print "binary file"
        solid = parseBinary(inputFile)
    inputFile.close()

    outputFileName = "%s_mapped%s%s" % (os.path.splitext(inputFileName)[0], os.path.extsep, "stl")
    outputFile = file(outputFileName, "w")
    newSolid = solid.mapToPlan()
    outputFile.write(str(newSolid))
    outputFile.close()

    print "%s saved" % outputFileName


if __name__ == "__main__":
    main()

Plan vers sphère

Voici le code Python permettant de convertir le fichier G-Code généré à partir d'une pièce sphérique mappée vers un plan (avec le code ci-dessus), pour être imprimée sur une imprimante 5 axes.

Les 2 axes supplémentaires sont le basculement du plateau suivant X et Y.