Utilisation de l'outil Calepinage sous ARCH

Forum destiné aux questions et discussions en français
Forum rules
Be nice to others! Read the FreeCAD code of conduct!
User avatar
flachyjoe
Veteran
Posts: 1891
Joined: Sat Mar 31, 2012 12:00 pm
Location: Limoges, France

Re: Utilisation de l'outil Calepinage sous ARCH

Post by flachyjoe »

Voila un premier jet d'une macro de calepinage rectangulaire avec traits de coupe traversants :

Code: Select all

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
#  COPanno.FCMacro
#
#  Copyright 2020 Florian Foinant <ffw_at_2f2v_dot_fr>
#
#  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., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.
#
#
import numbers


class Panneau(object):
    """Represente un panneau avec un indice permettant de l'identifier"""

    def __init__(self, indice, largeur, hauteur):
        self.indice = indice
        self.largeur = largeur
        self.hauteur = hauteur

    def __lt__(self, other):
        if estPanneau(other):
            return (
                (self.largeur < other.largeur and self.hauteur < other.hauteur) or
                (self.hauteur < other.largeur and self.largeur < other.hauteur)
            )
        else:
            if isinstance(other, numbers.Number):
                return self.largeur < other or self.hauteur < other
            else:
                raise TypeError

    def __le__(self, other):
        if estPanneau(other):
            return (
                (self.largeur <= other.largeur and self.hauteur <= other.hauteur) or
                (self.hauteur <= other.largeur and self.largeur <= other.hauteur)
            )
        else:
            if isinstance(other, numbers.Number):
                return self.largeur <= other or self.hauteur <= other
            else:
                raise TypeError

    def __gt__(self, other):
        if estPanneau(other):
            return (
                (self.largeur > other.largeur and self.hauteur > other.hauteur) or
                (self.hauteur > other.largeur and self.largeur > other.hauteur)
            )
        else:
            if isinstance(other, numbers.Number):
                return self.largeur > other or self.hauteur > other
            else:
                raise TypeError

    def __ge__(self, other):
        if estPanneau(other):
            return (
                (self.largeur >= other.largeur and self.hauteur >= other.hauteur) or
                (self.hauteur >= other.largeur and self.largeur >= other.hauteur)
            )
        else:
            if isinstance(other, numbers.Number):
                return self.largeur >= other or self.hauteur >= other
            else:
                raise TypeError

    def __eq__(self, other):
        if estPanneau(other):
            return (
                (self.largeur == other.largeur and self.hauteur == other.hauteur) or
                (self.hauteur == other.largeur and self.largeur == other.hauteur)
            )
        else:
            if isinstance(other, numbers.Number):
                return self.largeur == other or self.hauteur == other
            else:
                raise TypeError

    def __ne__(self, other):
        if estPanneau(other):
            return (
                (self.largeur != other.largeur or self.hauteur != other.hauteur) and
                (self.hauteur != other.largeur or self.largeur != other.hauteur)
            )
        else:
            if isinstance(other, numbers.Number):
                return self.largeur != other and self.hauteur != other
            else:
                raise TypeError

    def transpose(self):
        """Inverse largeur et hauteur"""
        self.largeur, self.hauteur = (self.hauteur, self.largeur)

    def __repr__(self):
        return "%s (%i x %i)" % (self.indice, self.largeur, self.hauteur)

    def __mul__(self, i):
        """Renvoi une liste de i panneaux identiques"""
        if isinstance(i, int):
            return [self]*i
        else:
            raise TypeError


class PanneauPositionne(Panneau):
    """Represente un panneau à un emplacement donné"""

    def __init__(self, indice, largeur, hauteur, x, y):
        super().__init__(indice, largeur, hauteur)
        self.x = x
        self.y = y

    def __repr__(self):
        return "%s %ix%i (x=%i, y=%i)" % (self.indice, self.largeur, self.hauteur, self.x, self.y)

    @classmethod
    def place(cls, panneau, x, y):
        """Crée un PanneauPositionne à partir d'un Panneau et des positions x y"""
        if not estPanneau(panneau):
            raise TypeError
        return PanneauPositionne(panneau.indice, panneau.largeur, panneau.hauteur, x, y)


def estPanneau(obj):
    """vrai si l'objet est du type Panneau"""
    return isinstance(obj, Panneau)


def estPanneauPositionne(obj):
    """vrai si l'objet est du type PanneauPositionne"""
    return isinstance(obj, PanneauPositionne)


def calepiner(listPanneaux, feuille):
    """
        Optimise le placement de panneaux rectangulaires dans une feuille rectangulaire

        :param listPanneaux: liste des panneaux à calepiner
        :type listPanneaux: [Panneau]
        :param feuille: panneau dans lequel calepiner
        :type feuille: Panneau
        :return: renvoi un 3-uple (emplacements, restes, chutes)
        :rtype: ([PanneauPositionne], [Panneau], [PanneauPositionne])
    """
    reste = []
    chutes = []
    cas = []
    result = ([], [], [])

    if not estPanneau(feuille):
        raise ValueError('feuille doit être un Panneau')
    if not estPanneauPositionne(feuille):
        feuille = PanneauPositionne.place(feuille, 0, 0)

    if not listPanneaux:
        return(([], [], [feuille]))

    if feuille <= 0:
        return(([], listPanneaux, []))

    aPlacer = listPanneaux[:]

    for panneau in sorted(listPanneaux, reverse=True):
        if (panneau.largeur <= feuille.largeur) and (panneau.hauteur <= feuille.hauteur):
            # le panneau entre à l'horizontale
            aPlacerC = aPlacer[:]
            aPlacerC.remove(panneau)
            place = PanneauPositionne.place(panneau, feuille.x, feuille.y)
            # cas 1 : trait de coupe horizontal
            chute1 = PanneauPositionne('', feuille.largeur - panneau.largeur,
                                       panneau.hauteur, feuille.x + panneau.largeur, feuille.y)
            chute2 = PanneauPositionne('', feuille.largeur, feuille.hauteur - panneau.hauteur,
                                       feuille.x, feuille.y + panneau.hauteur)
            place_cas11, rest_cas11, chut_cas11 = calepiner(aPlacerC, chute1)
            place_cas12, rest_cas1, chut_cas12 = calepiner(rest_cas11, chute2)

            cas.append((place_cas11 + place_cas12 + [place], rest_cas1, chut_cas11 + chut_cas12))

            # cas 2 : trait de coupe vertical
            chute1 = PanneauPositionne('', panneau.largeur, feuille.hauteur - panneau.hauteur,
                                       feuille.x, feuille.y + panneau.hauteur)
            chute2 = PanneauPositionne('', feuille.largeur - panneau.largeur, feuille.hauteur,
                                       feuille.x + panneau.largeur, feuille.y)
            place_cas21, rest_cas21, chut_cas21 = calepiner(aPlacerC, chute1)
            place_cas22, rest_cas2, chut_cas22 = calepiner(rest_cas21, chute2)

            cas.append((place_cas21 + place_cas22 + [place], rest_cas2, chut_cas21 + chut_cas22))

        elif (panneau.hauteur <= feuille.largeur) and (panneau.largeur <= feuille.hauteur):
            # le panneau entre à la vertical
            aPlacerC = aPlacer[:]
            aPlacerC.remove(panneau)
            panneau.transpose()
            place = PanneauPositionne.place(panneau, feuille.x, feuille.y)
            # cas 3 : trait de coupe horizontal
            chute1 = PanneauPositionne('', feuille.largeur - panneau.largeur,
                                       panneau.hauteur, feuille.x + panneau.largeur, feuille.y)
            chute2 = PanneauPositionne('', feuille.largeur, feuille.hauteur - panneau.hauteur,
                                       feuille.x, feuille.y + panneau.hauteur)
            place_cas31, rest_cas31, chut_cas31 = calepiner(aPlacerC, chute1)
            place_cas32, rest_cas3, chut_cas32 = calepiner(rest_cas31, chute2)

            cas.append((place_cas31 + place_cas32 + [place], rest_cas3, chut_cas31 + chut_cas32))

            # cas 4 : trait de coupe vertical
            chute1 = PanneauPositionne('', panneau.largeur, feuille.hauteur - panneau.hauteur,
                                       feuille.x, feuille.y + panneau.hauteur)
            chute2 = PanneauPositionne('', feuille.largeur - panneau.largeur, feuille.hauteur,
                                       feuille.x + panneau.largeur, feuille.y)
            place_cas41, rest_cas41, chut_cas41 = calepiner(aPlacerC, chute1)
            place_cas42, rest_cas4, chut_cas42 = calepiner(rest_cas41, chute2)

            cas.append((place_cas41 + place_cas42 + [place], rest_cas4, chut_cas41 + chut_cas42))
        else:
            # le panneau n'entre pas
            reste.append(panneau)
            aPlacer.remove(panneau)
            continue

        # CHOIX du meilleur cas
        # minimum de reste
        nbr_reste = [len(c[1]) for c in cas]
        reste_min = [i for i, x in enumerate(nbr_reste) if x == min(nbr_reste)]
        # ordonnée par dim max des chutes
        chut_max = [(i, max(c[2])) for i, c in enumerate(cas) if i in reste_min]
        chut_max.sort(key=lambda t: t[1], reverse=True)
        # minimum de chutes
        # tuples (indice du cas, nb de chute) pour les cas ayant le moins de reste
        nbr_chute = [(i, len(c[2])) for i, c in enumerate(cas) if i in [t[0] for t in chut_max]]
        meilleur = min(nbr_chute, key=lambda t: t[1])[0]

        result = cas[meilleur]

        break  # on ne teste qu'avec le premier panneau qui passe

    # aucun panneau n'entre
    if not cas:
        chutes.append(feuille)

    result[1].extend(reste)
    result[2].extend(chutes)
    return result

# _____________________
# INTERFACE FREECAD
# Place les objets dans le dernier selectionné

App.ActiveDocument.openTransaction() # gestion undo

sel = Gui.Selection.getSelection()

# Les objets à placer
pan=[]
for s in sel[0:-1]:
	# reinitialise la rotation de l'objet
	s.Placement.Rotation=App.Rotation(App.Vector(0, 0, 1), 0)
	# ajoute à la liste
	pan.append(Panneau(s.Name, s.Shape.BoundBox.XLength, s.Shape.BoundBox.YLength))

# la feuille dans laquelle placer
der_sel = sel[-1]
bb=der_sel.Shape.BoundBox
feuil = PanneauPositionne(der_sel.Name, bb.XLength, bb.YLength, bb.XMin, bb.YMin)

# calepinage
(pos, rest, chut) = calepiner(pan, feuil)

#déplace les objets aux emplacements trouvés
for p in pos:
	obj = App.ActiveDocument.getObject(p.indice)
	# tourne de 90° si nécessaire
	if obj.Shape.BoundBox.XLength != p.largeur:
		obj.Placement.Rotation=App.Rotation(App.Vector(0, 0, 1), 90)
		p.x = p.x + p.largeur
	obj.Placement.Base.x = p.x
	obj.Placement.Base.y = p.y
	
#crée les chutes
for c in chut:
	obj=Draft.makeRectangle(c.largeur, c.hauteur, App.Placement(App.Vector(c.x,c.y,0),App.Vector(0,0,1),0))
	obj.Label="Chute"

# fin de la macro
App.ActiveDocument.commitTransaction() # gestion undo
App.activeDocument().recompute(None,True,True)
Pour l'utiliser :
  • Sélectionner les objets à placer (les panneaux)
  • Ajouter l'objet dans lequel placer (la feuille) en cliquant dessus avec la touche Ctrl appuyée (dernier objet de la sélection)
  • Lancer la macro
NB :
  • L'épaisseur de coupe n'est pas prise en compte, il convient donc d'augmenter les dimensions de tous les objet d'une épaisseur de lame, de même que la feuille.
  • Les objets sont réalignés avec les axes avant d'être calepinés
  • Les objets sont calepinés sur le plan (OXY)
  • Ce sont les boites englobantes qui détermine la dimension des objets donc on peut calepiner autre chose que des rectangles mais c'est leur encombrement global qui sera pris en compte et pas leur forme
  • Ça ne fonctionne sans doute pas avec des groupes, des pièces ou autre ayant des Placement imbriqués (pas testé).
  • J'ai pris le parti de créer des rectangles représentant les chutes, il faut commenter le paragraphe correspondant si c'est gênant.
  • L'orientation des panneaux est optimisée donc ce n'est pas possible de définir le sens du fil
- Flachy Joe -
Image
User avatar
gege81100
Posts: 47
Joined: Sat Nov 09, 2019 1:17 pm

Re: Utilisation de l'outil Calepinage sous ARCH

Post by gege81100 »

flachyjoe wrote: Mon Feb 03, 2020 12:41 pm Voila un premier jet d'une macro de calepinage rectangulaire avec traits de coupe traversants :
...
Super je vais tester cela dès que possible.
User avatar
gege81100
Posts: 47
Joined: Sat Nov 09, 2019 1:17 pm

Re: Utilisation de l'outil Calepinage sous ARCH

Post by gege81100 »

flachyjoe wrote: Mon Feb 03, 2020 12:41 pm Voila un premier jet d'une macro de calepinage rectangulaire avec traits de coupe traversants :
...

Pour l'utiliser :
  • Sélectionner les objets à placer (les panneaux)
  • Ajouter l'objet dans lequel placer (la feuille) en cliquant dessus avec la touche Ctrl appuyée (dernier objet de la sélection)
  • Lancer la macro
...
Bien comme promis j'ai fais un test simple ce matin avec ma surface plafond et la surface placo symbolisé par panel et panel001.
Le résultat ne semble pas être celui attendu. :oops:

Par contre il semble y avoir une erreur dans le code à la ligne 278 et/ou 302, voici le rapport :

Code: Select all

Loading Arch module... done
Arch workbench activated
Traceback (most recent call last):
  File "/home/gf06041953/.FreeCAD/Macros/testcalepinage.FCMacro", line 278, in <module>
    der_sel = sel[-1]
<class 'IndexError'>: list index out of range
Traceback (most recent call last):
  File "/home/gf06041953/.FreeCAD/Macros/testcalepinage.FCMacro", line 278, in <module>
    der_sel = sel[-1]
<class 'IndexError'>: list index out of range
Traceback (most recent call last):
  File "/home/gf06041953/.FreeCAD/Macros/testcalepinage.FCMacro", line 302, in <module>
    App.activeDocument().recompute(None,True,True)
<class 'TypeError'>: function takes exactly 0 arguments (3 given)
Traceback (most recent call last):
  File "/home/gf06041953/.FreeCAD/Macros/testcalepinage.FCMacro", line 302, in <module>
    App.activeDocument().recompute(None,True,True)
<class 'TypeError'>: function takes exactly 0 arguments (3 given)
et voici comment j'ai procédé :
  • Recopie du code puis inséré dans un nouveau fichier macro sous FC
    Sélection de mes panneaux le premier étant le plus petit le dernier le plus grand en utilisant la touche "Ctrl" comme préconisé.
    enfin sélection de la macro dans ma liste et "Lancer"
Le résultat, 2 objets créés chute1 et chute2
Voir fichier joint

Bon je ne sais si j'ai fais avancer l'affaire.
Attachments
TestCalepinage.FCStd
(17.46 KiB) Downloaded 27 times
User avatar
flachyjoe
Veteran
Posts: 1891
Joined: Sat Mar 31, 2012 12:00 pm
Location: Limoges, France

Re: Utilisation de l'outil Calepinage sous ARCH

Post by flachyjoe »

Salut,
l'erreur
<class 'IndexError'>: list index out of range
a lieu quand la sélection est vide.
Tes objets Panel sont basés sur des esquisses, on est dans le cas des Placement imbriqués : il faut bouger l'esquisse pour bouger le panel, ce que ne sait pas faire ma macro. Donc les chutes s'affichent bien à la bonne place mais le Panneau n'est pas déplacé. Essaye avec des bêtes rectangles Draft.

MAJ pour ne pas avoir de message quand la macro est lancée sans rien sélectionné et pour charger Draft si nécessaire :

Code: Select all

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
#  COPanno.FCMacro
#
#  Copyright 2020 Florian Foinant <ffw_at_2f2v_dot_fr>
#
#  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., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.
#
#
import numbers


class Panneau(object):
    """Represente un panneau avec un indice permettant de l'identifier"""

    def __init__(self, indice, largeur, hauteur):
        self.indice = indice
        self.largeur = largeur
        self.hauteur = hauteur

    def __lt__(self, other):
        if estPanneau(other):
            return (
                (self.largeur < other.largeur and self.hauteur < other.hauteur) or
                (self.hauteur < other.largeur and self.largeur < other.hauteur)
            )
        else:
            if isinstance(other, numbers.Number):
                return self.largeur < other or self.hauteur < other
            else:
                raise TypeError

    def __le__(self, other):
        if estPanneau(other):
            return (
                (self.largeur <= other.largeur and self.hauteur <= other.hauteur) or
                (self.hauteur <= other.largeur and self.largeur <= other.hauteur)
            )
        else:
            if isinstance(other, numbers.Number):
                return self.largeur <= other or self.hauteur <= other
            else:
                raise TypeError

    def __gt__(self, other):
        if estPanneau(other):
            return (
                (self.largeur > other.largeur and self.hauteur > other.hauteur) or
                (self.hauteur > other.largeur and self.largeur > other.hauteur)
            )
        else:
            if isinstance(other, numbers.Number):
                return self.largeur > other or self.hauteur > other
            else:
                raise TypeError

    def __ge__(self, other):
        if estPanneau(other):
            return (
                (self.largeur >= other.largeur and self.hauteur >= other.hauteur) or
                (self.hauteur >= other.largeur and self.largeur >= other.hauteur)
            )
        else:
            if isinstance(other, numbers.Number):
                return self.largeur >= other or self.hauteur >= other
            else:
                raise TypeError

    def __eq__(self, other):
        if estPanneau(other):
            return (
                (self.largeur == other.largeur and self.hauteur == other.hauteur) or
                (self.hauteur == other.largeur and self.largeur == other.hauteur)
            )
        else:
            if isinstance(other, numbers.Number):
                return self.largeur == other or self.hauteur == other
            else:
                raise TypeError

    def __ne__(self, other):
        if estPanneau(other):
            return (
                (self.largeur != other.largeur or self.hauteur != other.hauteur) and
                (self.hauteur != other.largeur or self.largeur != other.hauteur)
            )
        else:
            if isinstance(other, numbers.Number):
                return self.largeur != other and self.hauteur != other
            else:
                raise TypeError

    def transpose(self):
        """Inverse largeur et hauteur"""
        self.largeur, self.hauteur = (self.hauteur, self.largeur)

    def __repr__(self):
        return "%s (%i x %i)" % (self.indice, self.largeur, self.hauteur)

    def __mul__(self, i):
        """Renvoi une liste de i panneaux identiques"""
        if isinstance(i, int):
            return [self]*i
        else:
            raise TypeError


class PanneauPositionne(Panneau):
    """Represente un panneau à un emplacement donné"""

    def __init__(self, indice, largeur, hauteur, x, y):
        super().__init__(indice, largeur, hauteur)
        self.x = x
        self.y = y

    def __repr__(self):
        return "%s %ix%i (x=%i, y=%i)" % (self.indice, self.largeur, self.hauteur, self.x, self.y)

    @classmethod
    def place(cls, panneau, x, y):
        """Crée un PanneauPositionne à partir d'un Panneau est des positions x y"""
        if not estPanneau(panneau):
            raise TypeError
        return PanneauPositionne(panneau.indice, panneau.largeur, panneau.hauteur, x, y)


def estPanneau(obj):
    """vrai si l'objet est du type Panneau"""
    return isinstance(obj, Panneau)


def estPanneauPositionne(obj):
    """vrai si l'objet est du type PanneauPositionne"""
    return isinstance(obj, PanneauPositionne)


def calepiner(listPanneaux, feuille):
    """
        Optimise le placement de panneaux rectangulaires dans une feuille rectangulaire

        :param listPanneaux: liste des panneaux à calepiner
        :type listPanneaux: [Panneau]
        :param feuille: panneau dans lequel calepiner
        :type feuille: Panneau
        :return: renvoi un 3-uple (emplacements, restes, chutes)
        :rtype: ([PanneauPositionne], [Panneau], [PanneauPositionne])
    """
    reste = []
    chutes = []
    cas = []
    result = ([], [], [])

    if not estPanneau(feuille):
        raise ValueError('feuille doit être un Panneau')
    if not estPanneauPositionne(feuille):
        feuille = PanneauPositionne.place(feuille, 0, 0)

    if not listPanneaux:
        return(([], [], [feuille]))

    if feuille <= 0:
        return(([], listPanneaux, []))

    aPlacer = listPanneaux[:]

    for panneau in sorted(listPanneaux, reverse=True):
        if (panneau.largeur <= feuille.largeur) and (panneau.hauteur <= feuille.hauteur):
            # le panneau entre à l'horizontale
            aPlacerC = aPlacer[:]
            aPlacerC.remove(panneau)
            place = PanneauPositionne.place(panneau, feuille.x, feuille.y)
            # cas 1 : trait de coupe horizontal
            chute1 = PanneauPositionne('', feuille.largeur - panneau.largeur,
                                       panneau.hauteur, feuille.x + panneau.largeur, feuille.y)
            chute2 = PanneauPositionne('', feuille.largeur, feuille.hauteur - panneau.hauteur,
                                       feuille.x, feuille.y + panneau.hauteur)
            place_cas11, rest_cas11, chut_cas11 = calepiner(aPlacerC, chute1)
            place_cas12, rest_cas1, chut_cas12 = calepiner(rest_cas11, chute2)

            cas.append((place_cas11 + place_cas12 + [place], rest_cas1, chut_cas11 + chut_cas12))

            # cas 2 : trait de coupe vertical
            chute1 = PanneauPositionne('', panneau.largeur, feuille.hauteur - panneau.hauteur,
                                       feuille.x, feuille.y + panneau.hauteur)
            chute2 = PanneauPositionne('', feuille.largeur - panneau.largeur, feuille.hauteur,
                                       feuille.x + panneau.largeur, feuille.y)
            place_cas21, rest_cas21, chut_cas21 = calepiner(aPlacerC, chute1)
            place_cas22, rest_cas2, chut_cas22 = calepiner(rest_cas21, chute2)

            cas.append((place_cas21 + place_cas22 + [place], rest_cas2, chut_cas21 + chut_cas22))

        elif (panneau.hauteur <= feuille.largeur) and (panneau.largeur <= feuille.hauteur):
            # le panneau entre à la vertical
            aPlacerC = aPlacer[:]
            aPlacerC.remove(panneau)
            panneau.transpose()
            place = PanneauPositionne.place(panneau, feuille.x, feuille.y)
            # cas 3 : trait de coupe horizontal
            chute1 = PanneauPositionne('', feuille.largeur - panneau.largeur,
                                       panneau.hauteur, feuille.x + panneau.largeur, feuille.y)
            chute2 = PanneauPositionne('', feuille.largeur, feuille.hauteur - panneau.hauteur,
                                       feuille.x, feuille.y + panneau.hauteur)
            place_cas31, rest_cas31, chut_cas31 = calepiner(aPlacerC, chute1)
            place_cas32, rest_cas3, chut_cas32 = calepiner(rest_cas31, chute2)

            cas.append((place_cas31 + place_cas32 + [place], rest_cas3, chut_cas31 + chut_cas32))

            # cas 4 : trait de coupe vertical
            chute1 = PanneauPositionne('', panneau.largeur, feuille.hauteur - panneau.hauteur,
                                       feuille.x, feuille.y + panneau.hauteur)
            chute2 = PanneauPositionne('', feuille.largeur - panneau.largeur, feuille.hauteur,
                                       feuille.x + panneau.largeur, feuille.y)
            place_cas41, rest_cas41, chut_cas41 = calepiner(aPlacerC, chute1)
            place_cas42, rest_cas4, chut_cas42 = calepiner(rest_cas41, chute2)

            cas.append((place_cas41 + place_cas42 + [place], rest_cas4, chut_cas41 + chut_cas42))
        else:
            # le panneau n'entre pas
            reste.append(panneau)
            aPlacer.remove(panneau)
            continue

        # CHOIX du meilleur cas
        # minimum de reste
        nbr_reste = [len(c[1]) for c in cas]
        reste_min = [i for i, x in enumerate(nbr_reste) if x == min(nbr_reste)]
        # ordonnée par dim max des chutes
        chut_max = [(i, max(c[2])) for i, c in enumerate(cas) if i in reste_min]
        chut_max.sort(key=lambda t: t[1], reverse=True)
        # minimum de chutes
        # tuples (indice du cas, nb de chute) pour les cas ayant le moins de reste
        nbr_chute = [(i, len(c[2])) for i, c in enumerate(cas) if i in [t[0] for t in chut_max]]
        meilleur = min(nbr_chute, key=lambda t: t[1])[0]

        result = cas[meilleur]

        break  # on ne teste qu'avec le premier panneau qui passe

    # aucun panneau n'entre
    if not cas:
        chutes.append(feuille)

    result[1].extend(reste)
    result[2].extend(chutes)
    return result

# _____________________
# INTERFACE FREECAD
# Place les objets dans le dernier selectionné

import Draft

sel = Gui.Selection.getSelection()
if sel:
	App.ActiveDocument.openTransaction() # gestion undo
	# Les objets à placer
	pan=[]
	for s in sel[0:-1]:
		# reinitialise la rotation de l'objet
		s.Placement.Rotation=App.Rotation(App.Vector(0, 0, 1), 0)
		# ajoute à la liste
		pan.append(Panneau(s.Name, s.Shape.BoundBox.XLength, s.Shape.BoundBox.YLength))
	
	# la feuille dans laquelle placer
	der_sel = sel[-1]
	bb=der_sel.Shape.BoundBox
	feuil = PanneauPositionne(der_sel.Name, bb.XLength, bb.YLength, bb.XMin, bb.YMin)
	
	# calepinage
	(pos, rest, chut) = calepiner(pan, feuil)
	
	#déplace les objets aux emplacements trouvés
	for p in pos:
		obj = App.ActiveDocument.getObject(p.indice)
		# tourne de 90° si nécessaire
		if obj.Shape.BoundBox.XLength != p.largeur:
			obj.Placement.Rotation=App.Rotation(App.Vector(0, 0, 1), 90)
			p.x = p.x + p.largeur
		obj.Placement.Base.x = p.x
		obj.Placement.Base.y = p.y
		
	#crée les chutes
	for c in chut:
		obj=Draft.makeRectangle(c.largeur, c.hauteur, App.Placement(App.Vector(c.x,c.y,0),App.Vector(0,0,1),0))
		obj.Label="Chute"
	
	# fin de la macro
	App.ActiveDocument.commitTransaction() # gestion undo
	App.activeDocument().recompute(None,True,True)
- Flachy Joe -
Image
User avatar
gege81100
Posts: 47
Joined: Sat Nov 09, 2019 1:17 pm

Re: Utilisation de l'outil Calepinage sous ARCH

Post by gege81100 »

flachyjoe wrote: Tue Feb 04, 2020 12:00 pm Salut,
l'erreur
<class 'IndexError'>: list index out of range
a lieu quand la sélection est vide.
Tes objets Panel sont basés sur des esquisses, on est dans le cas des Placement imbriqués : il faut bouger l'esquisse pour bouger le panel, ce que ne sait pas faire ma macro. Donc les chutes s'affichent bien à la bonne place mais le Panneau n'est pas déplacé. Essaye avec des bêtes rectangles Draft.

MAJ pour ne pas avoir de message quand la macro est lancée sans rien sélectionné et pour charger Draft si nécessaire :
...
Ok pour les panels, je les ai remplacés par de bêtes rectangles avec draft, mais j'ai l'impression d'obtenir la même choe avec un autre message d'erreur
Traceback (most recent call last):
File "/home/gf06041953/.FreeCAD/Macros/testcalepinage.FCMacro", line 303, in <module>
App.activeDocument().recompute(None,True,True)
<class 'TypeError'>: function takes exactly 0 arguments (3 given)
Donc j'ai remplacé le premier code par celui de ton dernier post, je l'ai sauvegardé,
j'ai créé en suite mes deux rectangles
puis j'ai sélectionné le plus petit et avec Ctrl le plus grand pour enfin lancer la macro.
Macro qui m'a généré "chute" et "chute003"

Je joins le fichier
Attachments
TestCalepinage.FCStd
(22.21 KiB) Downloaded 25 times
User avatar
flachyjoe
Veteran
Posts: 1891
Joined: Sat Mar 31, 2012 12:00 pm
Location: Limoges, France

Re: Utilisation de l'outil Calepinage sous ARCH

Post by flachyjoe »

Tu obtiens sensiblement la même chose mais le rectangle s'est déplacé à l'emplacement voulu. Si tu veux paver tout ton grand rectangle avec des petits, il faut faire un nombre suffisant de petits rectangles.
Exemple :
Capture d’écran_2020-02-05_09-31-06.png
Capture d’écran_2020-02-05_09-31-06.png (24.39 KiB) Viewed 1046 times
Les objets Face sont issu de l'explosion d'une Array (icone flèche vers le bas de Draft).
Résultat, avec les chutes en rouge :
Capture d’écran_2020-02-05_09-34-33.png
Capture d’écran_2020-02-05_09-34-33.png (24.49 KiB) Viewed 1046 times
Et avec plein de petits carrés :
Capture d’écran_2020-02-05_09-39-41.png
Capture d’écran_2020-02-05_09-39-41.png (2.52 KiB) Viewed 1044 times
Comme l'algo favorise les plus grandes chutes, le grand carré est passé au milieu et la chute horizontale est maximisée.

EDIT : pour l'erreur sur Recompute, quelle version de FC utilises-tu ? Je suis sur la version du PPA Daily.
OS: Ubuntu 19.10 (XFCE/xubuntu)
Word size of OS: 64-bit
Word size of FreeCAD: 64-bit
Version: 0.19.
Build type: Release
Python version: 3.7.5
Qt version: 5.12.4
Coin version: 4.0.0a
OCC version: 7.3.0
Locale: French/France (fr_FR)
- Flachy Joe -
Image
User avatar
flachyjoe
Veteran
Posts: 1891
Joined: Sat Mar 31, 2012 12:00 pm
Location: Limoges, France

Re: Utilisation de l'outil Calepinage sous ARCH

Post by flachyjoe »

MAJ qui corrige l'optimisation des chutes et qui accélère le calepinage d'un grand nombre de petits panneaux.

Code: Select all

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
#  COPanno.FCMacro
#
#  Copyright 2020 Florian Foinant <ffw_at_2f2v_dot_fr>
#
#  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., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.
#
#
import numbers


class Panneau(object):
    """Represente un panneau avec un indice permettant de l'identifier"""

    def __init__(self, indice, largeur, hauteur):
        self.indice = indice
        self.largeur = largeur
        self.hauteur = hauteur

    def aire(self):
        return self.largeur * self.hauteur

    def __lt__(self, other):
        if estPanneau(other):
            return self.aire() < other.aire()
        else:
            if isinstance(other, numbers.Number):
                return self.largeur < other or self.hauteur < other
            else:
                raise TypeError

    def __le__(self, other):
        if estPanneau(other):
            return self.aire() <= other.aire()
        else:
            if isinstance(other, numbers.Number):
                return self.largeur <= other or self.hauteur <= other
            else:
                raise TypeError

    def __gt__(self, other):
        if estPanneau(other):
            return self.aire() > other.aire()
        else:
            if isinstance(other, numbers.Number):
                return self.largeur > other or self.hauteur > other
            else:
                raise TypeError

    def __ge__(self, other):
        if estPanneau(other):
            self.aire() >= other.aire()
        else:
            if isinstance(other, numbers.Number):
                return self.largeur >= other or self.hauteur >= other
            else:
                raise TypeError

    def __eq__(self, other):
        if estPanneau(other):
            return (
                (self.largeur == other.largeur and self.hauteur == other.hauteur) or
                (self.hauteur == other.largeur and self.largeur == other.hauteur)
            )
        else:
            if isinstance(other, numbers.Number):
                return self.largeur == other or self.hauteur == other
            else:
                raise TypeError

    def __ne__(self, other):
        if estPanneau(other):
            return (
                (self.largeur != other.largeur or self.hauteur != other.hauteur) and
                (self.hauteur != other.largeur or self.largeur != other.hauteur)
            )
        else:
            if isinstance(other, numbers.Number):
                return self.largeur != other and self.hauteur != other
            else:
                raise TypeError

    def transpose(self):
        """Inverse largeur et hauteur"""
        self.largeur, self.hauteur = (self.hauteur, self.largeur)

    def __repr__(self):
        return "%s (%i x %i)" % (self.indice, self.largeur, self.hauteur)

    def __mul__(self, i):
        """Renvoi une liste de i panneaux identiques"""
        if isinstance(i, int):
            return [self]*i
        else:
            raise TypeError


class PanneauPositionne(Panneau):
    """Represente un panneau à un emplacement donné"""

    def __init__(self, indice, largeur, hauteur, x, y):
        super().__init__(indice, largeur, hauteur)
        self.x = x
        self.y = y

    def __repr__(self):
        return "%s %ix%i (x=%i, y=%i)" % (self.indice, self.largeur, self.hauteur, self.x, self.y)

    @classmethod
    def place(cls, panneau, x, y):
        """Crée un PanneauPositionne à partir d'un Panneau est des positions x y"""
        if not estPanneau(panneau):
            raise TypeError
        return PanneauPositionne(panneau.indice, panneau.largeur, panneau.hauteur, x, y)


def estPanneau(obj):
    """vrai si l'objet est du type Panneau"""
    return isinstance(obj, Panneau)


def estPanneauPositionne(obj):
    """vrai si l'objet est du type PanneauPositionne"""
    return isinstance(obj, PanneauPositionne)


def calepiner(listPanneaux, feuille):
    """
        Optimise le placement de panneaux rectangulaires dans une feuille rectangulaire

        :param listPanneaux: liste des panneaux à calepiner
        :type listPanneaux: [Panneau]
        :param feuille: panneau dans lequel calepiner
        :type feuille: Panneau
        :return: renvoi un 3-uple (emplacements, restes, chutes)
        :rtype: ([PanneauPositionne], [Panneau], [PanneauPositionne])
    """
    reste = []
    chutes = []
    cas = []
    place = False
    result = False

    if not estPanneau(feuille):
        raise ValueError('feuille doit être un Panneau')
    if not estPanneauPositionne(feuille):
        feuille = PanneauPositionne.place(feuille, 0, 0)

    if feuille <= 0:
        return(([], listPanneaux, []))

    if not listPanneaux:
        return(([], [], [feuille]))

    aPlacer = listPanneaux[:]

    for panneau in sorted(listPanneaux, reverse=True):
        if place and panneau==place:
            # identique au panneau juste testé
            continue

        if (panneau.largeur <= feuille.largeur) and (panneau.hauteur <= feuille.hauteur):
            # le panneau entre à l'horizontale
            aPlacerC = aPlacer[:]
            aPlacerC.remove(panneau)
            place = PanneauPositionne.place(panneau, feuille.x, feuille.y)
            # cas 1 : trait de coupe horizontal
            chute1 = PanneauPositionne('', feuille.largeur - panneau.largeur,
                                       panneau.hauteur, feuille.x + panneau.largeur, feuille.y)
            chute2 = PanneauPositionne('', feuille.largeur, feuille.hauteur - panneau.hauteur,
                                       feuille.x, feuille.y + panneau.hauteur)
            place_cas11, rest_cas11, chut_cas11 = calepiner(aPlacerC, chute1)
            place_cas12, rest_cas1, chut_cas12 = calepiner(rest_cas11, chute2)

            cas.append((place_cas11 + place_cas12 + [place], rest_cas1, chut_cas11 + chut_cas12))

            # cas 2 : trait de coupe vertical
            chute1 = PanneauPositionne('', panneau.largeur, feuille.hauteur - panneau.hauteur,
                                       feuille.x, feuille.y + panneau.hauteur)
            chute2 = PanneauPositionne('', feuille.largeur - panneau.largeur, feuille.hauteur,
                                       feuille.x + panneau.largeur, feuille.y)
            place_cas21, rest_cas21, chut_cas21 = calepiner(aPlacerC, chute1)
            place_cas22, rest_cas2, chut_cas22 = calepiner(rest_cas21, chute2)

            cas.append((place_cas21 + place_cas22 + [place], rest_cas2, chut_cas21 + chut_cas22))

        elif (panneau.hauteur <= feuille.largeur) and (panneau.largeur <= feuille.hauteur):
            # le panneau entre à la vertical
            aPlacerC = aPlacer[:]
            aPlacerC.remove(panneau)
            panneau.transpose()
            place = PanneauPositionne.place(panneau, feuille.x, feuille.y)
            # cas 3 : trait de coupe horizontal
            chute1 = PanneauPositionne('', feuille.largeur - panneau.largeur,
                                       panneau.hauteur, feuille.x + panneau.largeur, feuille.y)
            chute2 = PanneauPositionne('', feuille.largeur, feuille.hauteur - panneau.hauteur,
                                       feuille.x, feuille.y + panneau.hauteur)
            place_cas31, rest_cas31, chut_cas31 = calepiner(aPlacerC, chute1)
            place_cas32, rest_cas3, chut_cas32 = calepiner(rest_cas31, chute2)

            cas.append((place_cas31 + place_cas32 + [place], rest_cas3, chut_cas31 + chut_cas32))

            # cas 4 : trait de coupe vertical
            chute1 = PanneauPositionne('', panneau.largeur, feuille.hauteur - panneau.hauteur,
                                       feuille.x, feuille.y + panneau.hauteur)
            chute2 = PanneauPositionne('', feuille.largeur - panneau.largeur, feuille.hauteur,
                                       feuille.x + panneau.largeur, feuille.y)
            place_cas41, rest_cas41, chut_cas41 = calepiner(aPlacerC, chute1)
            place_cas42, rest_cas4, chut_cas42 = calepiner(rest_cas41, chute2)

            cas.append((place_cas41 + place_cas42 + [place], rest_cas4, chut_cas41 + chut_cas42))
        else:
            # le panneau n'entre pas
            reste.append(panneau)
            aPlacer.remove(panneau)
            continue

    if not cas:
        # aucun panneau n'entre
        return ([], reste, [feuille])
    else:
        # CHOIX du meilleur cas
        # minimum de reste
        nbr_reste = [len(c[1]) for c in cas]
        reste_min = [i for i, x in enumerate(nbr_reste) if x == min(nbr_reste)]
        # ordonnée par dim max des chutes
        # tuples (indice du cas, taille chute max, cas) pour les cas ayant le moins de reste
        try:
            chut_max = [(i, max(c[2]), c) for i, c in enumerate(cas) if i in reste_min]
            chut_max.sort(key=lambda t: t[1], reverse=True)
        except ValueError:
            # il y a au moins un cas sans chute
            chut_max = [(i, 0, c) for i, c in enumerate(cas) if i in reste_min]
        # minimum de chutes
        # tuples (indice du cas, nb de chute) pour les cas ayant le moins de reste
        nbr_chute = [(i, len(c[2])) for i, n, c in chut_max]
        meilleur = min(nbr_chute, key=lambda t: t[1])[0]
        result = cas[meilleur]
        result[1].extend(reste)
        result[2].extend(chutes)
        return result


# _____________________
# INTERFACE FREECAD
# Place les objets dans le dernier selectionné

import Draft

sel = Gui.Selection.getSelection()
if sel:
	App.ActiveDocument.openTransaction() # gestion undo
	# Les objets à placer
	pan=[]
	for s in sel[0:-1]:
		# reinitialise la rotation de l'objet
		s.Placement.Rotation=App.Rotation(App.Vector(0, 0, 1), 0)
		# ajoute à la liste
		pan.append(Panneau(s.Name, s.Shape.BoundBox.XLength, s.Shape.BoundBox.YLength))
	
	# la feuille dans laquelle placer
	der_sel = sel[-1]
	bb=der_sel.Shape.BoundBox
	feuil = PanneauPositionne(der_sel.Name, bb.XLength, bb.YLength, bb.XMin, bb.YMin)
	
	# calepinage
	(pos, rest, chut) = calepiner(pan, feuil)
	
	#déplace les objets aux emplacements trouvés
	for p in pos:
		obj = App.ActiveDocument.getObject(p.indice)
		# tourne de 90° si nécessaire
		if obj.Shape.BoundBox.XLength != p.largeur:
			obj.Placement.Rotation=App.Rotation(App.Vector(0, 0, 1), 90)
			p.x = p.x + p.largeur
		obj.Placement.Base.x = p.x
		obj.Placement.Base.y = p.y
		
	#crée les chutes
	for c in chut:
		obj=Draft.makeRectangle(c.largeur, c.hauteur, App.Placement(App.Vector(c.x,c.y,0),App.Vector(0,0,1),0))
		obj.Label="Chute"
	
	# fin de la macro
	App.ActiveDocument.commitTransaction() # gestion undo
	App.activeDocument().recompute()
- Flachy Joe -
Image
User avatar
gege81100
Posts: 47
Joined: Sat Nov 09, 2019 1:17 pm

Re: Utilisation de l'outil Calepinage sous ARCH

Post by gege81100 »

flachyjoe wrote: Wed Feb 05, 2020 8:36 am Tu obtiens sensiblement la même chose mais le rectangle s'est déplacé à l'emplacement voulu. Si tu veux paver tout ton grand rectangle avec des petits, il faut faire un nombre suffisant de petits rectangles. ...
Bonjour "flachyjoe" et merci pour le boulot que tu fais.

Bon je viens de remplacer le code mais je n'ai toujours pas le résultat escompté.

A savoir, que je cherche à déterminer avec la surface d'une plaque de placo ou d'une forme de carrelage le nombre d'élément qu'il me faudra pour couvrir ma surface globale en ayant le moins de perte possible ce qui me semble être la définition du calepinage.

Hors si j'analyse ce dont tu me parles il me faut faire un certain nombre de rectangle pour les voir s'afficher sur la surface globale, donc je fais le boulot à la place de la macro, non ?
User avatar
flachyjoe
Veteran
Posts: 1891
Joined: Sat Mar 31, 2012 12:00 pm
Location: Limoges, France

Re: Utilisation de l'outil Calepinage sous ARCH

Post by flachyjoe »

Pour moi le calepinage c'est faire entrer tous les morceaux à couper dans une feuille de façon à économiser la matière ;)
Mais la macro peut quand même t'être utile :
  • tu fais un gros tas de carrelage avec un array de ton petit rectangle, sans te soucier d'en avoir trop.
  • Tu lances la macro
  • Tu comptes le nombre de panneau qui sont utilisés (sélection avec une boite Maj+B et la quantité s'affiche en haut à droite du panneau "Afficher la sélection")

Il te restera encore des bouts à couvrir, tu peux faire des demi-carreaux et lancer la macro sur les chutes...
- Flachy Joe -
Image
User avatar
papyblaise
Veteran
Posts: 8002
Joined: Thu Jun 13, 2019 4:28 pm
Location: France

Re: Utilisation de l'outil Calepinage sous ARCH

Post by papyblaise »

du coup , si t'as des carreaux qui ne sont pas carrés ou rectangle (tommette) n'est-il pas mieux de faire le paquet de carreaux plus grand que le panneau , et de faire un commun :?:
Post Reply