From 9524e7395561ecb3e292d0fd3d90d4ff054cf4e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Bra=C3=B1a?= Date: Fri, 4 Apr 2025 04:30:44 +0600 Subject: [PATCH] reposicionado --- PVPlantTreeGenerator.py | 368 -------------------- PVPlantTree.ui => Vegetation/PVPlantTree.ui | 0 Vegetation/PVPlantTreeGenerator.py | 305 ++++++++++++++++ 3 files changed, 305 insertions(+), 368 deletions(-) delete mode 100644 PVPlantTreeGenerator.py rename PVPlantTree.ui => Vegetation/PVPlantTree.ui (100%) create mode 100644 Vegetation/PVPlantTreeGenerator.py diff --git a/PVPlantTreeGenerator.py b/PVPlantTreeGenerator.py deleted file mode 100644 index aed23ab..0000000 --- a/PVPlantTreeGenerator.py +++ /dev/null @@ -1,368 +0,0 @@ - -import math - -import ArchComponent -import FreeCAD - -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 Shadow Tree Obcject """ - - def __init__(self, obj): - # Definición de variables: - ArchComponent.Component.__init__(self, obj) - self.obj = obj - self.setProperties(obj) - - def setProperties(self, obj): - # Definicion de Propiedades: - pl = obj.PropertiesList - - # CANOPY: --------------------------------------------------------- - if not ("CanopyHeight" in pl): - obj.addProperty("App::PropertyLength", - "CanopyHeight", - "Canopy", - QT_TRANSLATE_NOOP("App::Property", "The height of self object") - ).CanopyHeight = 4000 - - if not ("CanopyRadius" in pl): - obj.addProperty("App::PropertyLength", - "CanopyRadius", - "Canopy", - QT_TRANSLATE_NOOP("App::Property", "The height of self object") - ).CanopyRadius = 1500 - - if not ("Spikiness" in pl): - obj.addProperty("App::PropertyFloatConstraint", - "Spikiness", - "Canopy", - QT_TRANSLATE_NOOP("App::Property", "The height of self object") - ).Spikiness = (0.5, 0.0, 1.0, 0.05) # (Default, Start, Finish, Step) - - ''' - if not ("Lumpiness" in pl): - obj.addProperty("App::PropertyFloatConstraint", - "Lumpiness", - "Canopy", - QT_TRANSLATE_NOOP("App::Property", "The height of self object") - ).Lumpiness = (0.0, 0.0, 1.0, 0.05) #(Default, Start, Finish, Step)''' - - if not ("CrownExpansion" in pl): - obj.addProperty("App::PropertyFloatConstraint", - "CrownExpansion", - "Canopy", - QT_TRANSLATE_NOOP("App::Property", "The height of self object") - ).CrownExpansion = (1.0, 0.0, 2.0, 0.05) # (Default, Start, Finish, Step) - - if not ("UmbrellaEffect" in pl): - obj.addProperty("App::PropertyFloatConstraint", - "UmbrellaEffect", - "Canopy", - QT_TRANSLATE_NOOP("App::Property", "The height of self object") - ).UmbrellaEffect = (0.0, 0.0, 1.0, 0.05) # (Default, Start, Finish, Step) - - if not ("LeafCount" in pl): - obj.addProperty("App::PropertyQuantity", - "LeafCount", - "Canopy", - QT_TRANSLATE_NOOP("App::Property", "The height of self object") - ).LeafCount = 20 - - # TRUNK: ------------------------------------------------------------------------------------------------------ - if not ("TrunkHeight" in pl): - obj.addProperty("App::PropertyLength", - "TrunkHeight", - "Trunk", - QT_TRANSLATE_NOOP("App::Property", "The height of self object") - ).TrunkHeight = 2000 - - if not ("TrunkRadius" in pl): - obj.addProperty("App::PropertyLength", - "TrunkRadius", - "Trunk", - QT_TRANSLATE_NOOP("App::Property", "The height of self object") - ).TrunkRadius = 150 - - if not ("TrunkFaces" in pl): - obj.addProperty("App::PropertyQuantity", - "TrunkFaces", - "Trunk", - QT_TRANSLATE_NOOP("App::Property", "The height of self object") - ).TrunkFaces = 6 - - if not ("Type" in pl): - obj.addProperty("App::PropertyString", - "Type", - "Base", - "Type").Type = "Vegetable-Tree" - obj.setEditorMode("Type", 1) - - self.Type = obj.Type - 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): - '''if prop in ["CanopyHeight", "CanopyHeight", "Spikiness", "CrownExpansion", "UmbrellaEffect", - "LeafCount"]: - self.canopy = self.createCanopy(obj) - - if prop in ["TrunkHeight", "TrunkRadius", "TrunkFaces"]: - self.trunk = self.createTrunk(obj)''' - - def createTrunk(self, obj): - import Part - angle = (math.pi * 2) / obj.TrunkFaces.Value - delta = obj.TrunkRadius.Value - pts = [FreeCAD.Vector(delta, 0, 0)] - for i in range(int(obj.TrunkFaces.Value) - 1): - ang = (i + 1) * angle - pts.append(FreeCAD.Vector(delta * math.cos(ang), - delta * math.sin(ang), - 0)) - pts.append(pts[0]) - p1 = Part.makePolygon(pts) - p0 = p1.makeOffset2D(90, 2, False, False, True) - p2 = p1.makeOffset2D(-50, 2, False, False, True) - p0.Placement.Base.z = -150 - p1.Placement.Base.z = 150 - p2.Placement.Base.z = obj.TrunkHeight.Value - 250 - return Part.makeLoft([p0, p1, p2], True, True, False) - - def createCanopy(self, obj): - import Part - import random - import Mesh - import numpy as np - - ncircles = int(obj.LeafCount.Value) - if ncircles % 2 == 0: - ncircles += 1 - half_ncircles = int(ncircles / 2) - ncirclesumbrella = int(half_ncircles/2) - ncirclestop = ncircles - ncirclesumbrella - - # 1. Create circles to define the sphere - circles = [] - dist = 2 * obj.CanopyRadius.Value / (ncircles - 1) - margin = dist * 0.01 - '''for i in range(half_ncircles + 1): - if i > 0: - d = (obj.CanopyRadius.Value - dist * i) - else: - d = obj.CanopyRadius.Value - margin - r = (obj.CanopyRadius.Value ** 2 - d ** 2) ** 0.5 - c = Part.makeCircle(r) - circles.append(c) - - ctmp = [c.copy() for c in circles] - ctmp.pop() - ctmp.reverse() - circles.extend(ctmp)''' - - d = - obj.CanopyRadius.Value - dist - b = (obj.CanopyRadius.Value ** 2 - (dist * (ncirclesumbrella - 1)) ** 2) ** 0.5 - - for i in range(ncircles): - d += dist - r = (obj.CanopyRadius.Value ** 2 - d ** 2) ** 0.5 - if i > ncirclesumbrella: - if obj.CrownExpansion < 1: - r = r * obj.CrownExpansion - if r == 0: - r = obj.CanopyRadius.Value * 0.01 - c = Part.makeCircle(r) - circles.append(c) - - # 2. Place circles - dist = obj.CanopyHeight.Value / ncircles - z = 0 - #zmax = dist * half_ncircles - for idx in range(1, half_ncircles): - z += dist - circles[idx].Placement.Base.z = (1 - obj.UmbrellaEffect) * z - #c.Placement.Base.z = obj.UmbrellaEffect * (zmax - z) + z - - dist1 = (obj.CanopyHeight.Value - z) / (half_ncircles + 1) - for idx in range(half_ncircles, ncircles): - c = circles[idx] - z += dist1 - c.Placement.Base.z = z - - # 3. noise generator - pts = [] - val = (dist / 2 - margin) * obj.Spikiness - for c in circles: - tmppts = c.discretize(ncircles) - for j in range(len(tmppts)): - point = tmppts[j] - point.x += random.uniform(-val, val) - point.y += random.uniform(-val, val) - point.z += random.uniform(-val, val) - pts.append(point) - - # 4. generate the mesh / solid - from scipy import spatial as sp_spatial - pts = np.array(pts) - hull = sp_spatial.ConvexHull(pts) - indices = hull.simplices - faces = pts[indices] - mesh = Mesh.Mesh(faces.tolist()) - if len(mesh.Facets) == 0: - return None - mesh.harmonizeNormals() - '''if mesh.Facets[0].Normal.z < 0: - mesh.flipNormals()''' - shape = Part.Shape() - shape.makeShapeFromMesh(mesh.Topology, 0.1) - return Part.makeSolid(shape) - - def execute(self, obj): - pl = obj.Placement - - trunk = self.createTrunk(obj) - canopy = self.createCanopy(obj) - canopy.Placement.Base.z = obj.TrunkHeight.Value - 250 # - obj.CanopyRadius.Value / 2 - obj.Shape = Part.makeCompound([trunk, canopy]) - obj.Placement = pl - - color = [(0.2510, 0.1255, 0.0)] * len(trunk.Faces) - color.extend([(0.0, 0.3922, 0.0)] * len(canopy.Faces)) - obj.ViewObject.DiffuseColor = color - - -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 - - -if FreeCAD.GuiUp: - FreeCADGui.addCommand('PVPlantTree', _CommandTree()) - diff --git a/PVPlantTree.ui b/Vegetation/PVPlantTree.ui similarity index 100% rename from PVPlantTree.ui rename to Vegetation/PVPlantTree.ui diff --git a/Vegetation/PVPlantTreeGenerator.py b/Vegetation/PVPlantTreeGenerator.py new file mode 100644 index 0000000..b5b0cb3 --- /dev/null +++ b/Vegetation/PVPlantTreeGenerator.py @@ -0,0 +1,305 @@ + +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 +