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)
- 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
- 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