import math import ArchComponent import FreeCAD import Part import random from FreeCAD import Qt from PySide.QtCore import QT_TRANSLATE_NOOP try: from scipy import spatial has_scipy = True except ImportError: has_scipy = False if FreeCAD.GuiUp: import FreeCADGui from PySide import QtCore, QtGui from DraftTools import translate from PySide.QtCore import QT_TRANSLATE_NOOP import Part import os else: # \cond def translate(ctxt, txt): return txt def QT_TRANSLATE_NOOP(ctxt, txt): return txt # \endcond __title__ = "FreeCAD Fixed Rack" __author__ = "Javier Braña" __url__ = "http://www.sogos-solar.com" __dir__ = os.path.join(FreeCAD.getUserAppDataDir(), "Mod", "PVPlant") DirResources = os.path.join(__dir__, "../Resources") DirIcons = os.path.join(DirResources, "Icons") DirImages = os.path.join(DirResources, "Images") def makeTree(): obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "Tree") Tree(obj) ViewProviderTree(obj.ViewObject) FreeCAD.ActiveDocument.recompute() try: folder = FreeCAD.ActiveDocument.Vegetation except: folder = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", 'Vegetation') folder.Label = "Vegetation" folder.addObject(obj) return obj class Tree(ArchComponent.Component): """A parametric tree object for architectural design""" def __init__(self, obj): ArchComponent.Component.__init__(self, obj) self.obj = obj self.setProperties(obj) random.seed(42) # Semilla para resultados consistentes def setProperties(self, obj): """Define y configura las propiedades del objeto""" pl = obj.PropertiesList # Propiedades de la copa canopy_props = [ ("CanopyHeight", "App::PropertyLength", "Altura total de la copa"), ("CanopyRadius", "App::PropertyLength", "Radio máximo de la copa"), ("Spikiness", "App::PropertyFloatConstraint", "Irregularidad de la superficie", (0.5, 0.0, 1.0, 0.05)), ( "CrownExpansion", "App::PropertyFloatConstraint", "Expansión de la corona superior", (1.0, 0.0, 2.0, 0.05)), ("UmbrellaEffect", "App::PropertyFloatConstraint", "Efecto de dosel/paraguas", (0.0, 0.0, 1.0, 0.05)), ("LeafCount", "App::PropertyInteger", "Densidad de follaje (número de segmentos)") ] for prop in canopy_props: name, ptype, doc, *args = prop if name not in pl: if ptype == "App::PropertyFloatConstraint": obj.addProperty(ptype, name, "Canopy", doc).__setattr__(name, args[0]) else: obj.addProperty(ptype, name, "Canopy", doc) # Valores por defecto if name == "LeafCount": setattr(obj, name, 20) elif name in ["CanopyHeight", "CanopyRadius"]: setattr(obj, name, 4000 if "Height" in name else 1500) # Propiedades del tronco trunk_props = [ ("TrunkHeight", "App::PropertyLength", "Altura del tronco", 2000), ("TrunkRadius", "App::PropertyLength", "Radio base del tronco", 150), ("TrunkFaces", "App::PropertyInteger", "Caras del tronco", 6) ] for prop in trunk_props: name, ptype, doc, default = prop if name not in pl: obj.addProperty(ptype, name, "Trunk", doc) setattr(obj, name, default) # Propiedades base if "Type" not in pl: obj.addProperty("App::PropertyString", "Type", "Base", "Tipo de objeto").Type = "Vegetable-Tree" obj.setEditorMode("Type", 1) # Hacerla de solo lectura obj.Proxy = self obj.IfcType = "Shading Device" obj.setEditorMode("IfcType", 1) def onDocumentRestored(self, obj): ArchComponent.Component.onDocumentRestored(self, obj) self.setProperties(obj) def onChanged(self, obj, prop): """Actualiza la forma cuando cambian propiedades""" if prop in ["CanopyHeight", "CanopyRadius", "Spikiness", "CrownExpansion", "UmbrellaEffect", "LeafCount", "TrunkHeight", "TrunkRadius", "TrunkFaces"]: self.execute(obj) def createTrunk(self, obj): """Crea la geometría del tronco usando un loft""" try: # Calcula dimensiones proporcionales base_radius = obj.TrunkRadius.Value top_radius = base_radius * 0.8 height = obj.TrunkHeight.Value # Crea tres perfiles circulares profiles = [] for z, radius in [(0, base_radius), (height / 3, base_radius), (height, top_radius)]: circle = Part.makeCircle(radius, FreeCAD.Vector(0, 0, z)) profiles.append(Part.Wire([circle])) return Part.makeLoft(profiles, True, True) except Exception as e: FreeCAD.Console.PrintError(f"Error creando tronco: {str(e)}\n") return None def createCanopy(self, obj): """Genera la forma de la copa usando una envoltura convexa""" if not has_scipy: FreeCAD.Console.PrintError("Scipy no está instalado. No se puede generar la copa.\n") return None try: # Configuración inicial n_segments = max(3, obj.LeafCount) # Mínimo 3 segmentos radius = obj.CanopyRadius.Value height = obj.CanopyHeight.Value # Genera puntos distribuidos esféricamente con ruido points = [] for _ in range(n_segments * 10): # 10 puntos por segmento theta = random.uniform(0, 2 * math.pi) phi = math.acos(random.uniform(-1, 1)) # Aplica parámetros de forma r = radius * (1 - obj.Spikiness * random.random()) x = r * math.sin(phi) * math.cos(theta) y = r * math.sin(phi) * math.sin(theta) z = height * (0.5 + 0.5 * math.cos(phi)) # Distribución vertical # Aplica efectos de forma z *= (1 - obj.UmbrellaEffect) if z > height / 2: x *= obj.CrownExpansion y *= obj.CrownExpansion points.append(FreeCAD.Vector(x, y, z)) # Crea la envoltura convexa hull = spatial.ConvexHull([(p.x, p.y, p.z) for p in points]) faces = [] for simplex in hull.simplices: triangle = [points[i] for i in simplex] faces.append(Part.Face(Part.makePolygon(triangle + [triangle[0]]))) return Part.Compound(faces) except Exception as e: FreeCAD.Console.PrintError(f"Error creando copa: {str(e)}\n") return None def execute(self, obj): """Ensambla el objeto final""" try: # Crea componentes trunk = self.createTrunk(obj) canopy = self.createCanopy(obj) # Verifica componentes válidos if not trunk or not canopy: raise ValueError("Error en la generación de componentes") # Posiciona la copa sobre el tronco canopy_placement = FreeCAD.Placement() canopy_placement.Base.z = obj.TrunkHeight.Value canopy.Placement = canopy_placement # Combina las formas compound = Part.Compound([trunk, canopy]) obj.Shape = compound # Configura apariencia if obj.ViewObject: obj.ViewObject.DiffuseColor = ([(0.35, 0.2, 0.05)] * len(trunk.Faces) + [(0.1, 0.6, 0.2)] * len(canopy.Faces)) # Color copa except Exception as e: FreeCAD.Console.PrintError(f"Error al ejecutar: {str(e)}\n") class ViewProviderTree(ArchComponent.ViewProviderComponent): "A View Provider for the Pipe object" def __init__(self, vobj): ArchComponent.ViewProviderComponent.__init__(self, vobj) def getIcon(self): return str(os.path.join(DirIcons, "tree(1).svg")) class TreeTaskPanel(QtGui.QWidget): def __init__(self, obj=None): QtGui.QWidget.__init__(self) self.obj = obj if self.obj is None: self.obj = makeTree() self.form = FreeCADGui.PySideUic.loadUi(__dir__ + "/PVPlantTree.ui") self.layout = QtGui.QHBoxLayout(self) self.layout.setContentsMargins(4, 4, 4, 4) self.layout.addWidget(self.form) self.form.editCanopyHeight.valueChanged.connect(self.Canopy) self.form.editCanopyRadius.valueChanged.connect(self.Canopy) self.form.editSpikiness.valueChanged.connect(self.Canopy) self.form.editCrownExpansion.valueChanged.connect(self.Canopy) self.form.editLeftUmbrellaEffect.valueChanged.connect(self.Canopy) self.form.editLeafCount.valueChanged.connect(self.Canopy) def Canopy(self): self.obj.CanopyHeight = FreeCAD.Units.Quantity(self.form.editCanopyHeight.text()).Value self.obj.CanopyRadius = FreeCAD.Units.Quantity(self.form.editCanopyRadius.text()).Value self.obj.Spikiness = self.form.editSpikiness.value() self.obj.CrownExpansion = self.form.editCrownExpansion.value() self.obj.UmbrellaEffect = self.form.editLeftUmbrellaEffect.value() self.obj.LeafCount = self.form.editLeafCount.value() FreeCAD.ActiveDocument.recompute() def accept(self): FreeCADGui.Control.closeDialog() return True def reject(self): FreeCAD.ActiveDocument.removeObject(self.obj.Name) FreeCADGui.Control.closeDialog() return True class CommandTree: "the PVPlant Tree command definition" def GetResources(self): return {'Pixmap': str(os.path.join(DirIcons, "tree(1).svg")), 'MenuText': QtCore.QT_TRANSLATE_NOOP("PVPlantTree", "Tree"), 'Accel': "S, T", 'ToolTip': QtCore.QT_TRANSLATE_NOOP("PVPlanTree", "Creates a Tree object from setup dialog.")} def IsActive(self): return not FreeCAD.ActiveDocument is None def Activated(self): import draftguitools.gui_trackers as DraftTrackers self.tree = makeTree() FreeCADGui.Snapper.getPoint(callback=self.getPoint, movecallback=self.mousemove, extradlg=self.taskbox(), title="Position of the tree:") def getPoint(self, point=None, obj=None): self.tree.Placement.Base = point FreeCAD.ActiveDocument.commitTransaction() FreeCAD.ActiveDocument.recompute() self.tracker.finalize() def mousemove(self, pt, snapInfo): self.tree.Placement.Base = pt def taskbox(self): self.form = TreeTaskPanel(self.tree) return self.form