664 lines
26 KiB
Python
664 lines
26 KiB
Python
# /**********************************************************************
|
|
# * *
|
|
# * Copyright (c) 2021-2026 Javier Braña <javier.branagutierrez@gmail.com>*
|
|
# * *
|
|
# * PVPlantPlacement - TaskPanels y comandos de placement de trackers *
|
|
# * *
|
|
# * La lógica de cálculo está en Civil/PVPlantPlacementCalc.py *
|
|
# * *
|
|
# ***********************************************************************
|
|
|
|
import FreeCAD
|
|
import Part
|
|
|
|
if FreeCAD.GuiUp:
|
|
import FreeCADGui, os
|
|
from PySide import QtCore, QtGui
|
|
from PySide.QtGui import QListWidgetItem
|
|
from PySide.QtCore import QT_TRANSLATE_NOOP
|
|
else:
|
|
def translate(ctxt, txt): return txt
|
|
def QT_TRANSLATE_NOOP(ctxt, txt): return txt
|
|
|
|
import PVPlantResources
|
|
import PVPlantSite
|
|
from Civil.PVPlantPlacementCalc import (
|
|
selectionFilter, getRows, getCols, get_trend, getTrend,
|
|
adjustToTerrain, optimized_cut, getHeadsAndSoil, moveFrameHead, ConvertObjectsTo
|
|
)
|
|
|
|
version = "0.2.0"
|
|
class _PVPlantPlacementTaskPanel:
|
|
'''The editmode TaskPanel for Schedules'''
|
|
|
|
def __init__(self, obj=None):
|
|
self.site = PVPlantSite.get()
|
|
self.Terrain = self.site.Terrain
|
|
self.FrameSetups = None
|
|
self.PVArea = None
|
|
self.Area = None
|
|
self.gap_col = .0
|
|
self.gap_row = .0
|
|
self.offsetX = .0
|
|
self.offsetY = .0
|
|
self.Dir = FreeCAD.Vector(0, -1, 0)
|
|
self._terrain_interpolator = None
|
|
self._frame_footprints_cache = {}
|
|
self._isinside_cache = {} # LRU: (frame_name, x, y) -> bool
|
|
self._area_polygon = None # Caché shapely del área
|
|
|
|
# UI setup
|
|
self.form = FreeCADGui.PySideUic.loadUi(os.path.join(PVPlantResources.__dir__, "PVPlantPlacement.ui"))
|
|
self.form.setWindowIcon(QtGui.QIcon(os.path.join(PVPlantResources.DirIcons, "way.svg")))
|
|
|
|
self.addFrames()
|
|
self.maxWidth = max([frame.Width.Value for frame in self.site.Frames])
|
|
|
|
self.form.buttonPVArea.clicked.connect(self.addPVArea)
|
|
self.form.editGapCols.valueChanged.connect(self.update_inner_spacing)
|
|
self.update_inner_spacing()
|
|
|
|
def addPVArea(self):
|
|
sel = FreeCADGui.Selection.getSelection()
|
|
if sel:
|
|
self.PVArea = sel[0]
|
|
self.form.editPVArea.setText(self.PVArea.Label)
|
|
|
|
def addFrames(self):
|
|
for frame_setup in self.site.Frames:
|
|
list_item = QListWidgetItem(frame_setup.Name, self.form.listFrameSetups)
|
|
list_item.setCheckState(QtCore.Qt.Checked)
|
|
|
|
def update_inner_spacing(self):
|
|
self.form.editInnerSpacing.setText(f"{self.form.editGapCols.value() - self.maxWidth / 1000} m")
|
|
|
|
def _get_or_create_frame_group(self):
|
|
"""Gestión optimizada de grupos de frames"""
|
|
doc = FreeCAD.ActiveDocument
|
|
main_group = doc.getObject("Frames")
|
|
if not main_group:
|
|
main_group = doc.addObject("App::DocumentObjectGroup", "Frames")
|
|
main_group.Label = "Frames"
|
|
|
|
mg = doc.getObject('MechanicalGroup')
|
|
if mg and main_group not in mg.Group:
|
|
mg.addObject(main_group)
|
|
|
|
if self.form.cbSubfolders.isChecked() and self.PVArea:
|
|
sn = f"Frames-{self.PVArea.Label}"
|
|
sg = next((o for o in main_group.Group if o.Name == sn), None)
|
|
if not sg:
|
|
sg = doc.addObject("App::DocumentObjectGroup", sn)
|
|
sg.Label = sn
|
|
main_group.addObject(sg)
|
|
return sg
|
|
return main_group
|
|
|
|
def createFrameFromPoints(self, dataframe):
|
|
from Mechanical.Frame import PVPlantFrame
|
|
doc = FreeCAD.ActiveDocument
|
|
|
|
group = self._get_or_create_frame_group()
|
|
|
|
frames = []
|
|
placements_key = "placement" if "placement" in dataframe.columns else 0
|
|
|
|
if placements_key == "placement":
|
|
placements = dataframe["placement"].tolist()
|
|
types = dataframe["type"].tolist()
|
|
|
|
for idx, (placement, frame_type) in enumerate(zip(placements, types)):
|
|
newrack = PVPlantFrame.makeTracker(setup=frame_type)
|
|
newrack.Label = "Tracker"
|
|
newrack.Visibility = False
|
|
newrack.Placement = placement
|
|
group.addObject(newrack)
|
|
frames.append(newrack)
|
|
|
|
if self.PVArea and self.PVArea.Name.startswith("FrameArea"):
|
|
self.PVArea.Frames = frames
|
|
|
|
def getProjected(self, shape):
|
|
"""Optimized projection calculation"""
|
|
if shape.BoundBox.ZLength == 0:
|
|
return Part.Face(Part.Wire(shape.Edges))
|
|
|
|
from Utils import PVPlantUtils as utils
|
|
wire = utils.simplifyWire(utils.getProjected(shape))
|
|
return Part.Face(wire.removeSplitter()) if wire and wire.isClosed() else Part.Face(wire)
|
|
|
|
def calculateWorkingArea(self):
|
|
"""Optimized working area calculation"""
|
|
self.Area = self.getProjected(self.PVArea.Shape)
|
|
exclusion_areas = FreeCAD.ActiveDocument.findObjects(Name="ExclusionArea")
|
|
|
|
if exclusion_areas:
|
|
prohibited_faces = []
|
|
for obj in exclusion_areas:
|
|
face = self.getProjected(obj.Shape.SubShapes[1])
|
|
if face and face.isValid():
|
|
prohibited_faces.append(face)
|
|
|
|
if prohibited_faces:
|
|
self.Area = self.Area.cut(prohibited_faces)
|
|
|
|
# Clear caches when area changes
|
|
self._terrain_interpolator = None
|
|
self._area_polygon = None
|
|
self._isinside_cache.clear()
|
|
|
|
def _setup_terrain_interpolator(self):
|
|
"""Cached terrain interpolator"""
|
|
if self._terrain_interpolator is not None:
|
|
return self._terrain_interpolator
|
|
|
|
mesh = self.Terrain.Mesh
|
|
points = np.array([v.Vector for v in mesh.Points])
|
|
bbox = self.Area.BoundBox
|
|
|
|
# Filter points within working area efficiently
|
|
mask = ((points[:, 0] >= bbox.XMin) & (points[:, 0] <= bbox.XMax) &
|
|
(points[:, 1] >= bbox.YMin) & (points[:, 1] <= bbox.YMax))
|
|
filtered_points = points[mask]
|
|
|
|
if len(filtered_points) == 0:
|
|
self._terrain_interpolator = None
|
|
return None
|
|
|
|
try:
|
|
self._terrain_interpolator = LinearNDInterpolator(
|
|
filtered_points[:, :2], filtered_points[:, 2]
|
|
)
|
|
except:
|
|
self._terrain_interpolator = None
|
|
|
|
return self._terrain_interpolator
|
|
|
|
def _get_frame_footprint(self, frame):
|
|
"""Cached footprint calculation"""
|
|
frame_key = (frame.Length.Value, frame.Width.Value)
|
|
if frame_key not in self._frame_footprints_cache:
|
|
l, w = frame.Length.Value, frame.Width.Value
|
|
l_med, w_med = l / 2, w / 2
|
|
|
|
footprint = Part.makePolygon([
|
|
FreeCAD.Vector(-l_med, -w_med, 0),
|
|
FreeCAD.Vector(l_med, -w_med, 0),
|
|
FreeCAD.Vector(l_med, w_med, 0),
|
|
FreeCAD.Vector(-l_med, w_med, 0),
|
|
FreeCAD.Vector(-l_med, -w_med, 0)
|
|
])
|
|
footprint.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), FreeCAD.Vector(0, 1, 0))
|
|
|
|
self._frame_footprints_cache[frame_key] = footprint
|
|
|
|
return self._frame_footprints_cache[frame_key]
|
|
|
|
def _calculate_terrain_adjustment_batch(self, points_data):
|
|
"""Process terrain adjustments in batches for better performance"""
|
|
terrain_interp = self._setup_terrain_interpolator()
|
|
results = []
|
|
|
|
for frame_type, base_point in points_data:
|
|
yl = frame_type.Length.Value / 2
|
|
top_point = FreeCAD.Vector(base_point.x, base_point.y + yl, 0)
|
|
bot_point = FreeCAD.Vector(base_point.x, base_point.y - yl, 0)
|
|
|
|
if terrain_interp:
|
|
# Use interpolator for faster elevation calculation
|
|
yy = np.linspace(bot_point.y, top_point.y, 6) # Reduced points for speed
|
|
xx = np.full_like(yy, base_point.x)
|
|
try:
|
|
zz = terrain_interp(xx, yy)
|
|
if not np.isnan(zz).all():
|
|
slope, intercept, *_ = stats.linregress(yy, zz)
|
|
z_top = slope * top_point.y + intercept
|
|
z_bot = slope * bot_point.y + intercept
|
|
else:
|
|
z_top = z_bot = 0
|
|
except:
|
|
z_top = z_bot = 0
|
|
else:
|
|
# Fallback to direct projection (slower)
|
|
line = Part.LineSegment(bot_point, top_point).toShape()
|
|
try:
|
|
import MeshPart
|
|
projected = MeshPart.projectShapeOnMesh(line, self.Terrain.Mesh, FreeCAD.Vector(0, 0, 1))[0]
|
|
if len(projected) >= 2:
|
|
yy = [p.y for p in projected]
|
|
zz = [p.z for p in projected]
|
|
slope, intercept, *_ = stats.linregress(yy, zz)
|
|
z_top = slope * top_point.y + intercept
|
|
z_bot = slope * bot_point.y + intercept
|
|
else:
|
|
z_top = z_bot = 0
|
|
except:
|
|
z_top = z_bot = 0
|
|
|
|
new_top = FreeCAD.Vector(top_point.x, top_point.y, z_top)
|
|
new_bot = FreeCAD.Vector(bot_point.x, bot_point.y, z_bot)
|
|
|
|
new_pl = FreeCAD.Placement()
|
|
new_pl.Base = (new_top + new_bot) / 2
|
|
new_pl.Rotation = FreeCAD.Rotation(
|
|
FreeCAD.Vector(-1, 0, 0), new_top - new_bot
|
|
)
|
|
|
|
results.append((frame_type, new_pl))
|
|
|
|
return results
|
|
|
|
def adjustToTerrain(self, coordinates):
|
|
"""Unified terrain adjustment function for both aligned and non-aligned arrays"""
|
|
# Create binary array efficiently
|
|
arr = np.array([[int(obj != 0) for obj in col] for col in coordinates], dtype=np.uint8)
|
|
labeled_array, num_features = sclabel(arr)
|
|
|
|
# Build DataFrame efficiently
|
|
data = []
|
|
for label in range(1, num_features + 1):
|
|
cols, rows = np.where(labeled_array == label)
|
|
for col, row in zip(cols, rows):
|
|
frame_type, placement = coordinates[col][row]
|
|
data.append({
|
|
'ID': len(data) + 1,
|
|
'region': label,
|
|
'type': frame_type,
|
|
'column': col,
|
|
'row': row,
|
|
'placement': placement
|
|
})
|
|
|
|
if not data:
|
|
return pd.DataFrame(columns=['ID', 'region', 'type', 'column', 'row', 'placement'])
|
|
|
|
df = pd.DataFrame(data)
|
|
|
|
# Process terrain adjustments in batches
|
|
points_data = [(row['type'], row['placement']) for _, row in df.iterrows()]
|
|
adjusted_results = self._calculate_terrain_adjustment_batch(points_data)
|
|
|
|
# Update placements in DataFrame
|
|
for idx, (frame_type, new_placement) in enumerate(adjusted_results):
|
|
df.at[idx, 'placement'] = new_placement
|
|
|
|
return df
|
|
|
|
def _get_area_polygon(self):
|
|
"""Convierte self.Area a shapely Polygon para comprobaciones rápidas"""
|
|
if self._area_polygon is None and self.Area:
|
|
from shapely.geometry import Polygon
|
|
verts = self.Area.Vertexes
|
|
if len(verts) >= 3:
|
|
self._area_polygon = Polygon([(v.x, v.y) for v in verts])
|
|
return self._area_polygon
|
|
|
|
def isInside(self, frame, point):
|
|
"""
|
|
Comprueba si un frame cabe en el área en un punto dado.
|
|
Usa shapely para la comprobación 2D (mucho más rápido que Part.cut).
|
|
"""
|
|
# Caché LRU: mismo frame + misma posición
|
|
key = (frame.Name, round(point.x, 0), round(point.y, 0))
|
|
if key in self._isinside_cache:
|
|
return self._isinside_cache[key]
|
|
|
|
# Prefiltro rápido por BoundBox
|
|
fw, fl = frame.Width.Value / 2, frame.Length.Value / 2
|
|
if (point.x - fw < self.Area.BoundBox.XMin or
|
|
point.x + fw > self.Area.BoundBox.XMax or
|
|
point.y - fl < self.Area.BoundBox.YMin or
|
|
point.y + fl > self.Area.BoundBox.YMax):
|
|
self._isinside_cache[key] = False
|
|
return False
|
|
|
|
# Comprobación precisa con shapely
|
|
ap = self._get_area_polygon()
|
|
if ap is not None:
|
|
from shapely.geometry import box
|
|
fp = box(point.x - fw, point.y - fl, point.x + fw, point.y + fl)
|
|
result = ap.contains(fp)
|
|
self._isinside_cache[key] = result
|
|
return result
|
|
|
|
# Fallback OCC (si shapely falla)
|
|
try:
|
|
frame_footprint = self._get_frame_footprint(frame)
|
|
frame_footprint.Placement.Base = point
|
|
cut = frame_footprint.cut([self.Area])
|
|
result = len(cut.Vertexes) == 0
|
|
self._isinside_cache[key] = result
|
|
return result
|
|
except Part.OCCError:
|
|
self._isinside_cache[key] = False
|
|
return False
|
|
|
|
def getAligments(self):
|
|
"""
|
|
Calcula las alineaciones X (columnas) y opcionalmente Y (filas)
|
|
en función de las referencias seleccionadas.
|
|
Retorna (x_range, y_range). y_range vacío si no hay referencia vertical.
|
|
"""
|
|
sel = FreeCADGui.Selection.getSelectionEx()
|
|
if not sel or not sel[0].SubObjects:
|
|
return np.array([], dtype=np.float64), np.array([], dtype=np.float64)
|
|
|
|
sub_objects = sel[0].SubObjects
|
|
|
|
if len(sub_objects) == 1:
|
|
# Una sola referencia: usar BoundBox completo
|
|
bb = sub_objects[0].BoundBox
|
|
area_bb = self.Area.BoundBox
|
|
n_cols = max(1, int((area_bb.XMax - area_bb.XMin) / self.gap_col))
|
|
n_rows = max(1, int((area_bb.YMax - area_bb.YMin) / self.gap_row))
|
|
x_range = np.linspace(area_bb.XMin + self.offsetX, area_bb.XMax, n_cols, dtype=np.float64)
|
|
y_range = np.linspace(area_bb.YMax - self.offsetY - self.gap_row, area_bb.YMin, n_rows, dtype=np.float64)
|
|
else:
|
|
refh = max(sub_objects[:2], key=lambda x: x.BoundBox.XLength)
|
|
refv = max(sub_objects[:2], key=lambda x: x.BoundBox.YLength)
|
|
|
|
# Alinear grid con referencias
|
|
area_xmin, area_xmax = self.Area.BoundBox.XMin, self.Area.BoundBox.XMax
|
|
area_ymin, area_ymax = self.Area.BoundBox.YMin, self.Area.BoundBox.YMax
|
|
|
|
n_cols = max(1, int((area_xmax - area_xmin) / self.gap_col))
|
|
n_rows = max(1, int((area_ymax - area_ymin) / self.gap_row))
|
|
x_range = np.linspace(
|
|
refv.BoundBox.XMin + self.offsetX,
|
|
min(refv.BoundBox.XMax + self.offsetX, area_xmax),
|
|
n_cols, dtype=np.float64
|
|
)
|
|
y_range = np.linspace(
|
|
refh.BoundBox.YMax - self.offsetY,
|
|
max(refh.BoundBox.YMin - self.offsetY, area_ymin),
|
|
n_rows, dtype=np.float64
|
|
) if n_rows > 1 else np.array([refh.BoundBox.YMin], dtype=np.float64)
|
|
|
|
# Pre-filtrar: eliminar puntos claramente fuera del BoundBox del área
|
|
x_range = x_range[(x_range >= self.Area.BoundBox.XMin) & (x_range <= self.Area.BoundBox.XMax)]
|
|
if len(y_range) > 0:
|
|
y_range = y_range[(y_range >= self.Area.BoundBox.YMin) & (y_range <= self.Area.BoundBox.YMax)]
|
|
|
|
return x_range, y_range
|
|
|
|
def calculateAlignedArray(self):
|
|
"""
|
|
Coloca frames en grid alineado (filas y columnas).
|
|
Llama al motor unificado _calculate_placement.
|
|
"""
|
|
return self._calculate_placement(mode='aligned')
|
|
|
|
def calculateNonAlignedArray(self):
|
|
"""
|
|
Coloca frames adaptados al contorno del área (solo columnas).
|
|
Llama al motor unificado _calculate_placement.
|
|
"""
|
|
return self._calculate_placement(mode='non_aligned')
|
|
|
|
def _calculate_placement(self, mode='non_aligned'):
|
|
"""
|
|
Motor de posicionamiento unificado para aligned y non_aligned.
|
|
|
|
aligned: grid Y fijo + isInside (rápido en áreas rectangulares, usa caché)
|
|
non_aligned: intersección área-línea (preciso en bordes irregulares)
|
|
"""
|
|
pointsx, pointsy = self.getAligments()
|
|
if len(pointsx) == 0:
|
|
FreeCAD.Console.PrintWarning("No X alignments found.\n")
|
|
return pd.DataFrame()
|
|
|
|
# Pre-calcular footprints una sola vez
|
|
footprints = []
|
|
for frame in self.FrameSetups:
|
|
footprint = self._get_frame_footprint(frame)
|
|
footprints.append((frame, footprint))
|
|
|
|
if not footprints:
|
|
return pd.DataFrame()
|
|
|
|
min_h = min(ftp[0].Width.Value for ftp in footprints)
|
|
corridor_enabled = self.form.groupCorridor.isChecked()
|
|
corridor_count = 0
|
|
corridor_offset = 0
|
|
ref_width = footprints[0][0].Width.Value
|
|
corridor_val = FreeCAD.Units.Quantity(
|
|
self.form.editColGap.text()).Value - (self.gap_col - ref_width)
|
|
area_ymax = self.Area.BoundBox.YMax
|
|
area_ymin = self.Area.BoundBox.YMin
|
|
ref_frame = footprints[0][0]
|
|
ref_len = ref_frame.Length.Value
|
|
|
|
n_cols = len(pointsx)
|
|
cols = [None] * n_cols
|
|
|
|
# Procesar por lotes para permitir interrupción con barra de progreso
|
|
from PySide.QtCore import QCoreApplication
|
|
for idx, x in enumerate(pointsx):
|
|
col = []
|
|
cx = x + corridor_offset
|
|
|
|
# Actualizar barra de progreso cada 20 columnas
|
|
if idx % 20 == 0 and hasattr(self.form, 'progressBar'):
|
|
self.form.progressBar.setValue(int(100 * idx / n_cols))
|
|
QCoreApplication.processEvents()
|
|
|
|
if mode == 'aligned' and len(pointsy) > 0:
|
|
half_len = ref_len / 2
|
|
for y in pointsy:
|
|
py = y - half_len
|
|
# Vector creado solo si pasa el BoundBox check (ya lo hace isInside internamente)
|
|
tp = FreeCAD.Vector(cx, py, 0.0)
|
|
placed = False
|
|
if self.isInside(ref_frame, tp):
|
|
col.append([ref_frame, tp])
|
|
placed = True
|
|
else:
|
|
# frames alternativos: probar con offsets
|
|
for fi in range(1, len(footprints)):
|
|
fr = footprints[fi][0]
|
|
ld = (ref_len - fr.Length.Value) / 2
|
|
for yoff in (ld, -ld):
|
|
if self.isInside(fr, FreeCAD.Vector(tp.x, tp.y + yoff, 0.0)):
|
|
col.append([fr, FreeCAD.Vector(tp.x, tp.y + yoff, 0.0)])
|
|
placed = True
|
|
break
|
|
if placed:
|
|
break
|
|
if not placed:
|
|
col.append(0)
|
|
else:
|
|
# Non-aligned: intersección de línea vertical con el área
|
|
line = Part.LineSegment(
|
|
FreeCAD.Vector(cx, area_ymax, 0.0),
|
|
FreeCAD.Vector(cx, area_ymin, 0.0)
|
|
).toShape()
|
|
try:
|
|
inter = self.Area.section(line)
|
|
pts = sorted([v.Point for v in inter.Vertexes],
|
|
key=lambda p: p.y, reverse=True)
|
|
for i in range(0, len(pts) - 1, 1 + (len(pts) > 2)):
|
|
top, bot = pts[i], pts[i + 1]
|
|
if top.y - bot.y > min_h:
|
|
self._place_frames_in_segment(col, footprints, cx, top, bot)
|
|
except Exception as e:
|
|
FreeCAD.Console.PrintWarning(f"Segment error: {e}\n")
|
|
|
|
# Corredores
|
|
if corridor_enabled and col:
|
|
corridor_count += 1
|
|
if corridor_count >= self.form.editColCount.value():
|
|
corridor_offset += corridor_val
|
|
corridor_count = 0
|
|
cols[idx] = col
|
|
|
|
return self.adjustToTerrain(cols)
|
|
|
|
def accept(self):
|
|
from datetime import datetime
|
|
starttime = datetime.now()
|
|
|
|
params = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Document")
|
|
auto_save_enabled = params.GetBool("AutoSaveEnabled")
|
|
params.SetBool("AutoSaveEnabled", False)
|
|
FreeCAD.ActiveDocument.RecomputesFrozen = True
|
|
|
|
try:
|
|
items = [
|
|
FreeCAD.ActiveDocument.getObject(self.form.listFrameSetups.item(i).text())
|
|
for i in range(self.form.listFrameSetups.count())
|
|
if self.form.listFrameSetups.item(i).checkState() == QtCore.Qt.Checked
|
|
]
|
|
|
|
self.FrameSetups = list({f.Length.Value: f for f in items}.values())
|
|
self.FrameSetups.sort(key=lambda x: x.Length.Value, reverse=True)
|
|
|
|
self.gap_col = FreeCAD.Units.Quantity(self.form.editGapCols.text()).Value
|
|
self.gap_row = FreeCAD.Units.Quantity(self.form.editGapRows.text()).Value + self.FrameSetups[0].Length.Value
|
|
self.offsetX = FreeCAD.Units.Quantity(self.form.editOffsetHorizontal.text()).Value
|
|
self.offsetY = FreeCAD.Units.Quantity(self.form.editOffsetVertical.text()).Value
|
|
|
|
FreeCAD.ActiveDocument.openTransaction("Create Placement")
|
|
self.calculateWorkingArea()
|
|
|
|
if self.form.cbAlignFrames.isChecked():
|
|
dataframe = self.calculateAlignedArray()
|
|
else:
|
|
dataframe = self.calculateNonAlignedArray()
|
|
|
|
if not dataframe.empty:
|
|
self.createFrameFromPoints(dataframe)
|
|
|
|
import Electrical.group as egroup
|
|
import importlib
|
|
importlib.reload(egroup)
|
|
egroup.groupTrackersToTransformers(5000000, self.gap_row)
|
|
|
|
FreeCAD.ActiveDocument.commitTransaction()
|
|
|
|
finally:
|
|
FreeCAD.ActiveDocument.RecomputesFrozen = False
|
|
params.SetBool("AutoSaveEnabled", auto_save_enabled)
|
|
|
|
elapsed = datetime.now() - starttime
|
|
FreeCAD.Console.PrintMessage(f"Placement: {elapsed}\n")
|
|
FreeCADGui.Control.closeDialog()
|
|
FreeCAD.ActiveDocument.recompute()
|
|
|
|
class adjustToTerrainTaskPanel:
|
|
def __init__(self, obj=None):
|
|
self.obj = obj
|
|
self.form = FreeCADGui.PySideUic.loadUi(PVPlantResources.__dir__ + "/PVPlantPlacementAdjust.ui")
|
|
|
|
def accept(self):
|
|
frames = []
|
|
for obj in FreeCADGui.Selection.getSelection():
|
|
if obj.Name.startswith("Tracker"):
|
|
frames.append(obj)
|
|
elif obj.Name.startswith("FrameArea"):
|
|
frames.extend(obj.Frames)
|
|
adjustToTerrain(frames, self.form.comboMethod.currentIndex() == 0)
|
|
self.close()
|
|
return True
|
|
|
|
def reject(self):
|
|
self.close()
|
|
return False
|
|
|
|
def close(self):
|
|
FreeCADGui.Control.closeDialog()
|
|
|
|
import numpy as np
|
|
from scipy import stats
|
|
|
|
class _PVPlantConvertTaskPanel:
|
|
'''The editmode TaskPanel for Conversions'''
|
|
|
|
def __init__(self):
|
|
self.To = None
|
|
# self.form:
|
|
self.form = FreeCADGui.PySideUic.loadUi(os.path.join(PVPlantResources.__dir__, "PVPlantPlacementConvert.ui"))
|
|
self.form.setWindowIcon(QtGui.QIcon(os.path.join(PVPlantResources.DirIcons, "Trace.svg")))
|
|
self.form.buttonTo.clicked.connect(self.addTo)
|
|
|
|
def addTo(self):
|
|
sel = FreeCADGui.Selection.getSelection()
|
|
if len(sel) > 0:
|
|
self.To = sel[0]
|
|
self.form.editTo.setText(self.To.Label)
|
|
|
|
def accept(self):
|
|
sel = FreeCADGui.Selection.getSelection()
|
|
if sel == self.To:
|
|
return False
|
|
if len(sel) > 0 and self.To is not None:
|
|
FreeCAD.ActiveDocument.openTransaction("Convert to")
|
|
ConvertObjectsTo(sel, self.To)
|
|
return True
|
|
return False
|
|
|
|
|
|
class CommandPVPlantPlacement:
|
|
|
|
def GetResources(self):
|
|
return {'Pixmap': str(os.path.join(PVPlantResources.DirIcons, "way.svg")),
|
|
'Accel': "P,P",
|
|
'MenuText': QT_TRANSLATE_NOOP("Placement", "Placement"),
|
|
'ToolTip': QT_TRANSLATE_NOOP("Placement", "Crear un campo fotovoltaico")}
|
|
|
|
def Activated(self):
|
|
taskd = _PVPlantPlacementTaskPanel(None)
|
|
FreeCADGui.Control.showDialog(taskd)
|
|
|
|
def IsActive(self):
|
|
if FreeCAD.ActiveDocument:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
class CommandAdjustToTerrain:
|
|
|
|
def GetResources(self):
|
|
return {'Pixmap': str(os.path.join(PVPlantResources.DirIcons, "adjust.svg")),
|
|
'Accel': "P, A",
|
|
'MenuText': QT_TRANSLATE_NOOP("Placement", "Adjust"),
|
|
'ToolTip': QT_TRANSLATE_NOOP("Placement", "Adjust object to terrain")}
|
|
|
|
def Activated(self):
|
|
sel = FreeCADGui.Selection.getSelection()
|
|
if len(sel) > 0:
|
|
# adjustToTerrain(sel)
|
|
FreeCADGui.Control.showDialog(adjustToTerrainTaskPanel())
|
|
else:
|
|
print("No selected object")
|
|
|
|
def IsActive(self):
|
|
if FreeCAD.ActiveDocument:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
class CommandConvert:
|
|
def GetResources(self):
|
|
return {'Pixmap': str(os.path.join(PVPlantResources.DirIcons, "convert.svg")),
|
|
'Accel': "P, C",
|
|
'MenuText': QT_TRANSLATE_NOOP("Placement", "Convert"),
|
|
'ToolTip': QT_TRANSLATE_NOOP("Placement", "Convertir un objeto en otro")}
|
|
|
|
def IsActive(self):
|
|
return (not FreeCAD.ActiveDocument is None and
|
|
not FreeCAD.ActiveDocument.getObject("Site") is None and
|
|
not FreeCAD.ActiveDocument.getObject("Terrain") is None and
|
|
not FreeCAD.ActiveDocument.getObject("TrackerSetup") is None)
|
|
|
|
def Activated(self):
|
|
taskd = _PVPlantConvertTaskPanel()
|
|
FreeCADGui.Control.showDialog(taskd)
|
|
|
|
|
|
'''if FreeCAD.GuiUp:
|
|
FreeCADGui.addCommand('PVPlantPlacement', _CommandPVPlantPlacement())
|
|
FreeCADGui.addCommand('PVPlantAdjustToTerrain', _CommandAdjustToTerrain())
|
|
FreeCADGui.addCommand('PVPlantConvertTo', _CommandConvert())'''
|