freecad-cam/Mod/BIM/bimcommands/BimLibrary.py
2026-02-01 01:59:24 +01:00

1086 lines
40 KiB
Python

# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * Copyright (c) 2018 Yorik van Havre <yorik@uncreated.net> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * 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 Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
from __future__ import print_function
"""The BIM library tool"""
import sys
import os
import FreeCAD
import FreeCADGui
QT_TRANSLATE_NOOP = FreeCAD.Qt.QT_TRANSLATE_NOOP
translate = FreeCAD.Qt.translate
PARAMS = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/BIM")
FILTERS = [
"*.fcstd",
"*.FCStd",
"*.FCSTD",
"*.stp",
"*.STP",
"*.step",
"*.STEP",
"*.brp",
"*.BRP",
"*.brep",
"*.BREP",
"*.ifc",
"*.IFC",
"*.sat",
"*.SAT",
]
TEMPLIBPATH = os.path.join(FreeCAD.getUserAppDataDir(), "BIM", "OfflineLibrary")
THUMBNAILSPATH = os.path.join(TEMPLIBPATH, "__thumbcache__")
LIBRARYURL = "https://github.com/FreeCAD/FreeCAD-library/tree/master"
RAWURL = LIBRARYURL.replace("/tree", "/raw")
LIBINDEXFILE = "OfflineLibrary.py"
USE_API = True # True to use github API instead of web fetching... Way faster
REFRESH_INTERVAL = (
3600 # Min seconds between allowing a new API calls (3600 = one hour)
)
# TODO as https://github.com/yorikvanhavre/BIM_Workbench/pull/77
# All the print() statements in your code should be replaced by
# FreeCAD.Console.PrintMessage() or FreeCAD.Console.PrintWarning() or
# FreeCAD.Console.PrintError() and the text should be placed in a translate()
# function and "\n" should be added to it.
# Example FreeCAD.Console.PrintError(translate("BIM","Please save the document first")+"\n")
# It would be cool if the preview image would have a max width of the available
# column width, so if the task column is smaller than the image, it gets smaller
# to fit the space. I don't remember exactly how to do that, but it should be
# findable in QDesigner
class BIM_Library:
def GetResources(self):
return {
"Pixmap": "BIM_Library",
"MenuText": QT_TRANSLATE_NOOP("BIM_Library", "Objects library"),
"ToolTip": QT_TRANSLATE_NOOP("BIM_Library", "Opens the objects library"),
}
def Activated(self):
# trying to locate the parts library
pr = FreeCAD.ParamGet("User parameter:Plugins/parts_library")
libok = False
self.librarypath = pr.GetString("destination", "")
if self.librarypath:
if os.path.exists(self.librarypath):
libok = True
else:
# check if the library is at the standard addon location
addondir = os.path.join(FreeCAD.getUserAppDataDir(), "Mod", "parts_library")
if os.path.exists(addondir):
# save file paths with forward slashes even on windows
pr.SetString("destination", addondir.replace("\\", "/"))
libok = True
FreeCADGui.Control.showDialog(BIM_Library_TaskPanel(offlinemode=libok))
class BIM_Library_TaskPanel:
def __init__(self, offlinemode=False):
from PySide import QtCore, QtGui
self.mainDocName = FreeCAD.Gui.ActiveDocument.Document.Name
self.previewDocName = "Viewer"
self.linked = False
self.librarypath = FreeCAD.ParamGet(
"User parameter:Plugins/parts_library"
).GetString("destination", "")
self.form = FreeCADGui.PySideUic.loadUi(":/ui/dialogLibrary.ui")
self.form.setWindowIcon(QtGui.QIcon(":/icons/BIM_Library.svg"))
# setting up a flat (no directories) file model for search
self.filemodel = QtGui.QStandardItemModel()
self.filemodel.setColumnCount(1)
# setting up a directory model that shows only fcstd, step and brep
self.dirmodel = LibraryModel()
self.dirmodel.setRootPath(self.librarypath)
self.dirmodel.setNameFilters(self.getFilters())
self.dirmodel.setNameFilterDisables(False)
self.form.tree.setModel(self.dirmodel)
self.form.buttonInsert.clicked.connect(self.insert)
self.form.buttonLink.clicked.connect(self.link)
self.modelmode = 1 # 0 = File search, 1 = Dir mode
# Don't show columns for size, file type, and last modified
self.form.tree.setHeaderHidden(True)
self.form.tree.hideColumn(1)
self.form.tree.hideColumn(2)
self.form.tree.hideColumn(3)
self.form.tree.setRootIndex(self.dirmodel.index(self.librarypath))
self.form.searchBox.textChanged.connect(self.onSearch)
# external search
sites = {
"BimObject": [
"bimobject.png",
"https://www.bimobject.com/en/product?filetype=8&freetext=",
],
"NBS Library": [
"nbslibrary.png",
"https://www.nationalbimlibrary.com/en/search/?facet=Xo-P0w&searchTerm=",
],
"BIMTool": [
"bimtool.png",
"https://www.bimtool.com/Catalog.aspx?criterio=",
],
"3DFindIt": ["3dfindit.svg", "https://www.3dfindit.com/textsearch?q="],
"GrabCAD": [
"grabcad.svg",
"https://grabcad.com/library?softwares=step-slash-iges&query=",
],
}
for k, v in sites.items():
self.form.comboSearch.addItem(QtGui.QIcon(":/icons/"+v[0]), k, v[1])
self.form.comboSearch.currentIndexChanged.connect(self.onExternalSearch)
# retrieve preferences
self.form.checkOnline.toggled.connect(self.onCheckOnline)
self.form.checkOnline.setChecked(
PARAMS.GetBool("LibraryOnline", not offlinemode)
)
self.form.checkFCStdOnly.toggled.connect(self.onCheckFCStdOnly)
self.form.checkFCStdOnly.setChecked(PARAMS.GetBool("LibraryFCStdOnly", False))
self.form.checkWebSearch.toggled.connect(self.onCheckWebSearch)
self.form.checkWebSearch.setChecked(PARAMS.GetBool("LibraryWebSearch", False))
self.form.check3DPreview.toggled.connect(self.onCheck3DPreview)
self.form.check3DPreview.setChecked(PARAMS.GetBool("3DPreview", False))
# collapsables
if PARAMS.GetBool("LibraryPreview", False):
self.form.framePreview.show()
self.form.buttonPreview.setText(translate("BIM", "Preview") + "")
else:
self.form.framePreview.hide()
self.form.buttonPreview.setText(translate("BIM", "Preview") + "")
self.form.buttonPreview.clicked.connect(self.onButtonPreview)
self.form.frameOptions.hide()
self.form.buttonOptions.setText(translate("BIM", "Options") + "")
self.form.buttonOptions.clicked.connect(self.onButtonOptions)
# saving functionality, is disabled for now
self.form.buttonSave.hide()
self.form.checkThumbnail.hide()
# self.form.buttonSave.clicked.connect(self.addtolibrary)
# self.form.checkThumbnail.toggled.connect(self.onCheckThumbnail)
# self.form.checkThumbnail.setChecked(PARAMS.GetBool("SaveThumbnails",False))
# self.fcstdCB = QtGui.QCheckBox('FCStd')
# self.fcstdCB.setCheckState(QtCore.Qt.Checked)
# self.fcstdCB.setEnabled(False)
# self.fcstdCB.hide()
# self.stepCB = QtGui.QCheckBox('STEP')
# self.stepCB.setCheckState(QtCore.Qt.Checked)
# self.stepCB.hide()
# self.stlCB = QtGui.QCheckBox('STL')
# self.stlCB.setCheckState(QtCore.Qt.Checked)
# self.stlCB.hide()
# update the tree
self.onCheckOnline()
def onItemSelected(self, selected, deselected):
"""Generates and displays needed previews"""
from PySide import QtGui
if not selected:
return
index = selected[0].indexes()[0]
if self.modelmode == 1:
path = self.dirmodel.filePath(index)
else:
path = self.filemodel.itemFromIndex(index).toolTip()
if path.startswith(":github"):
path = RAWURL + "/" + path[7:]
thumb = self.getThumbnail(path)
if thumb:
px = QtGui.QPixmap(thumb)
else:
px = QtGui.QPixmap()
self.form.framePreview.setPixmap(px)
if False:
# TO BE REFACTORED
import Part
import zipfile
self.previewOn = PARAMS.GetBool("3DPreview", False)
try:
self.path = self.dirmodel.filePath(index)
except:
self.path = self.previousIndex
print(self.path)
self.isFile = os.path.isfile(self.path)
# if the 3D preview checkbox is on ticked, show the preview
if self.previewOn == True or self.linked == True:
if self.isFile == True:
# close a non linked preview document
if self.linked == False:
try:
FreeCAD.closeDocument(self.previewDocName)
except:
pass
# create different kinds of previews based on file type
if (
self.path.lower().endswith(".stp")
or self.path.lower().endswith(".step")
or self.path.lower().endswith(".brp")
or self.path.lower().endswith(".brep")
):
self.previewDocName = "Viewer"
FreeCAD.newDocument(self.previewDocName)
FreeCAD.setActiveDocument(self.previewDocName)
Part.show(Part.read(self.path))
FreeCADGui.SendMsgToActiveView("ViewFit")
elif self.path.lower().endswith(".fcstd"):
openedDoc = FreeCAD.openDocument(self.path)
FreeCADGui.SendMsgToActiveView("ViewFit")
self.previewDocName = FreeCAD.ActiveDocument.Name
thumbnailSave = PARAMS.GetBool("SaveThumbnails", False)
if thumbnailSave == True:
FreeCAD.ActiveDocument.save()
if self.linked == False:
self.previousIndex = self.path
# create a 2D image preview
if self.path.lower().endswith(".fcstd"):
zfile = zipfile.ZipFile(self.path)
files = zfile.namelist()
# check for meta-file if it's really a FreeCAD document
if files[0] == "Document.xml":
image = "thumbnails/Thumbnail.png"
if image in files:
image = zfile.read(image)
thumbfile = tempfile.mkstemp(suffix=".png")[1]
thumb = open(thumbfile, "wb")
thumb.write(image)
thumb.close()
im = QtGui.QPixmap(thumbfile)
self.form.framePreview.setPixmap(im)
return self.previewDocName, self.previousIndex, self.linked
self.form.framePreview.clear()
return self.previewDocName, self.previousIndex, self.linked
def link(self, index):
# check if the main document is open
try:
# check if the working document is saved
if FreeCAD.getDocument(self.mainDocName).FileName == "":
FreeCAD.Console.PrintWarning(translate("BIM","Please save the working file before linking.")+"\n")
else:
self.previewOn = PARAMS.GetBool("3DPreview", False)
self.linked = True
if self.previewOn != True:
BIM_Library_TaskPanel.clicked(self, index, previewDocName="Viewer")
self.librarypath = ""
# save the file prior to linking
BIM_Library_TaskPanel.addtolibrary(self)
# link a document if it has been previously saved
if self.fileDialog[0] != "":
FreeCADGui.Selection.clearSelection()
# link only root objects
for obj in FreeCAD.ActiveDocument.RootObjects:
FreeCADGui.Selection.addSelection(obj)
objects = FreeCADGui.Selection.getSelection()
# tries to create a link for each object in the selection
for obj in objects:
try:
link = (
FreeCAD.getDocument(self.mainDocName)
.addObject("App::Link", "Link")
.setLink(obj)
)
# FreeCAD.getDocument(self.mainDocName).getObject('Link').Label=FreeCAD.ActiveDocument.ActiveObject.Label
FreeCAD.getDocument(self.mainDocName).getObject(
link
).Label = FreeCAD.ActiveDocument.ActiveObject.Label
except:
pass
FreeCAD.setActiveDocument(self.mainDocName)
self.librarypath = FreeCAD.ParamGet(
"User parameter:Plugins/parts_library"
).GetString("destination", "")
self.linked = False
return self.linked
except:
FreeCAD.Console.PrintWarning(
translate("BIM","It is not possible to link because the main document is closed.")+"\n")
def addtolibrary(self):
# DISABLED
import Part, Mesh, os
self.fileDialog = QtGui.QFileDialog.getSaveFileName(
None, "Save As", self.librarypath
)
#print(self.fileDialog[0])
# check if file saving has been canceled and save .fcstd, .step and .stl copies
if self.fileDialog[0] != "":
# remove the file extension from the file path
fileName = os.path.splitext(self.fileDialog[0])[0]
FCfilename = fileName + ".fcstd"
FreeCAD.ActiveDocument.saveAs(FCfilename)
if self.stepCB.isChecked() or self.stlCB.isChecked():
toexport = []
objs = FreeCAD.ActiveDocument.Objects
for obj in objs:
if obj.ViewObject.Visibility == True:
toexport.append(obj)
if self.stepCB.isChecked() and self.linked == False:
STEPfilename = fileName + ".step"
Part.export(toexport, STEPfilename)
if self.stlCB.isChecked() and self.linked == False:
STLfilename = fileName + ".stl"
Mesh.export(toexport, STLfilename)
return self.fileDialog[0]
def onSearch(self, text):
if text:
self.setSearchModel(text)
else:
self.setFileModel()
def setSearchModel(self, text):
import PartGui
from PySide import QtGui
def add_line(f, dp):
if self.isAllowed(f) and (text.lower() in f.lower()):
it = QtGui.QStandardItem(f)
it.setToolTip(os.path.join(dp, f))
self.filemodel.appendRow(it)
if f.lower().endswith(".fcstd"):
it.setIcon(QtGui.QIcon(":icons/freecad-doc.png"))
elif f.lower().endswith(".ifc"):
it.setIcon(QtGui.QIcon(":/icons/IFC.svg"))
else:
it.setIcon(QtGui.QIcon(":/icons/Part_document.svg"))
self.form.tree.setModel(self.filemodel)
self.filemodel.clear()
if self.form.checkOnline.isChecked():
res = self.getOfflineLib(structured=True)
for i in range(len(res[0])):
add_line(res[0][i], res[2][i])
else:
res = os.walk(self.librarypath)
for dp, dn, fn in res:
for f in fn:
if not os.path.isdir(os.path.join(dp, f)):
add_line(f, dp)
self.modelmode = 0
def getFilters(self):
if self.form.checkFCStdOnly.isChecked():
return FILTERS
else:
return FILTERS[:3]
def isAllowed(self, filename):
e = os.path.splitext(filename)[1]
if e in [f[1:] for f in FILTERS]:
if e in [f[1:] for f in self.getFilters()]:
return True
else:
return False
else:
return True
def setFileModel(self):
# self.form.tree.clear()
self.form.tree.setModel(self.dirmodel)
self.dirmodel.setRootPath(self.librarypath)
self.dirmodel.setNameFilters(self.getFilters())
self.dirmodel.setNameFilterDisables(False)
self.form.tree.setRootIndex(self.dirmodel.index(self.librarypath))
self.modelmode = 1
self.form.tree.setHeaderHidden(True)
self.form.tree.hideColumn(1)
self.form.tree.hideColumn(2)
self.form.tree.hideColumn(3)
self.form.tree.selectionModel().selectionChanged.connect(self.onItemSelected)
def setOnlineModel(self):
from PySide import QtGui
import PartGui
def addItems(root, d, path):
for k, v in d.items():
if self.isAllowed(k):
it = QtGui.QStandardItem(k)
root.appendRow(it)
it.setToolTip(path + "/" + k)
if isinstance(v, dict):
it.setIcon(
QtGui.QIcon.fromTheme(
"folder", QtGui.QIcon(":/icons/Group.svg")
)
)
addItems(it, v, path + "/" + k)
it.setToolTip("")
elif k.lower().endswith(".fcstd"):
it.setIcon(QtGui.QIcon(":icons/freecad-doc.png"))
elif k.lower().endswith(".ifc"):
it.setIcon(QtGui.QIcon(":/icons/IFC.svg"))
else:
it.setIcon(QtGui.QIcon(":/icons/Part_document.svg"))
self.form.tree.setModel(self.filemodel)
self.filemodel.clear()
d = self.getOfflineLib()
addItems(self.filemodel, d, ":github")
self.modelmode = 0
self.form.tree.selectionModel().selectionChanged.connect(self.onItemSelected)
def getOfflineLib(self, structured=False):
def addDir(d, root):
fn = []
dn = []
dp = []
for k, v in d.items():
if isinstance(v, dict) and v:
fn2, dn2, dp2 = addDir(v, root + "/" + k)
fn.extend(fn2)
dn.extend(dn2)
dp.extend(dp2)
elif v:
fn.append(k)
dn.append(root)
dp.append(root)
return fn, dn, dp
templibfile = os.path.join(TEMPLIBPATH, LIBINDEXFILE)
if not os.path.exists(templibfile):
FreeCAD.Console.PrintError(
translate("BIM", "No structure in cache. Please refresh.") + "\n"
)
return {}
import sys
sys.path.append(TEMPLIBPATH)
import OfflineLibrary
d = OfflineLibrary.library
if structured:
return addDir(d, ":github")
else:
return d
def urlencode(self, text):
#print(text, type(text))
if sys.version_info.major < 3:
import urllib
return urllib.quote_plus(text)
else:
import urllib.parse
return urllib.parse.quote_plus(text)
def openUrl(self, url):
from PySide import QtGui
s = PARAMS.GetBool("LibraryWebSearch", False)
if s:
import WebGui
WebGui.openBrowser(url)
else:
QtGui.QDesktopServices.openUrl(url)
def needsFullSpace(self):
return True
def getStandardButtons(self):
from PySide import QtGui
return QtGui.QDialogButtonBox.Close
def reject(self):
if hasattr(self, "box") and self.box:
self.box.off()
FreeCADGui.Control.closeDialog()
FreeCAD.ActiveDocument.recompute()
def insert(self, index=None):
# check if the main document is open
try:
FreeCAD.setActiveDocument(self.mainDocName)
except:
FreeCAD.Console.PrintError(
translate(
"BIM",
"It is not possible to insert this object because the document has been closed.",
)
+ "\n"
)
return
if self.previewDocName in FreeCAD.listDocuments().keys():
FreeCAD.closeDocument(self.previewDocName)
if not index:
index = self.form.tree.selectedIndexes()
if not index:
return
index = index[0]
if self.modelmode == 1:
path = self.dirmodel.filePath(index)
else:
path = self.filemodel.itemFromIndex(index).toolTip()
if path.startswith(":github"):
path = self.download(RAWURL + "/" + path[7:])
before = FreeCAD.ActiveDocument.Objects
self.name = os.path.splitext(os.path.basename(path))[0]
ext = os.path.splitext(path.lower())[1]
if ext in [".stp", ".step", ".brp", ".brep"]:
self.place(path)
elif ext == ".fcstd":
FreeCADGui.ActiveDocument.mergeProject(path)
from DraftGui import todo
todo.delay(self.reject, None)
elif ext == ".ifc":
from importers import importIFC
importIFC.ZOOMOUT = False
importIFC.insert(path, FreeCAD.ActiveDocument.Name)
from DraftGui import todo
todo.delay(self.reject, None)
elif ext in [".sat", ".sab"]:
try:
# InventorLoader addon
import importerIL
except ImportError:
try:
# CADExchanger addon
import CadExchangerIO
except ImportError:
FreeCAD.Console.PrintError(
translate(
"BIM",
"Error: Unable to import SAT files - InventorLoader or CadExchanger addon must be installed",
)
+ "\n"
)
else:
path = CadExchangerIO.insert(
path, FreeCAD.ActiveDocument.Name, returnpath=True
)
self.place(path)
else:
path = importerIL.insert(path, FreeCAD.ActiveDocument.Name)
FreeCADGui.Selection.clearSelection()
for o in FreeCAD.ActiveDocument.Objects:
if not o in before:
FreeCADGui.Selection.addSelection(o)
FreeCADGui.SendMsgToActiveView("ViewSelection")
def download(self, url):
import urllib.request
filepath = os.path.join(TEMPLIBPATH, url.split("/")[-1])
url = url.replace(" ", "%20")
if not os.path.exists(filepath):
from PySide import QtCore, QtGui
QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor)
u = urllib.request.urlopen(url)
if not u:
FreeCAD.Console.PrintError(
translate("BIM", "Error: Unable to download") + " " + url + "\n"
)
b = u.read()
f = open(filepath, "wb")
f.write(b)
f.close()
QtGui.QApplication.restoreOverrideCursor()
return filepath
def place(self, path):
import Part
self.shape = Part.read(path)
if hasattr(FreeCADGui, "Snapper"):
try:
import DraftTrackers
except Exception:
import draftguitools.gui_trackers as DraftTrackers
self.box = DraftTrackers.ghostTracker(
self.shape, dotted=True, scolor=(0.0, 0.0, 1.0), swidth=1.0
)
self.delta = self.shape.BoundBox.Center
self.box.move(self.delta)
self.box.on()
if hasattr(FreeCAD, "DraftWorkingPlane"):
FreeCAD.DraftWorkingPlane.setup()
self.origin = self.makeOriginWidget()
FreeCADGui.Snapper.getPoint(
movecallback=self.mouseMove,
callback=self.mouseClick,
extradlg=self.origin,
)
else:
Part.show(self.shape)
def makeOriginWidget(self):
from PySide import QtGui
w = QtGui.QWidget()
w.setWindowTitle(translate("BIM", "Insertion point"))
w.setWindowIcon(
QtGui.QIcon(
os.path.join(os.path.dirname(__file__), "icons", "BIM_Library.svg")
)
)
l = QtGui.QVBoxLayout()
w.setLayout(l)
c = QtGui.QComboBox()
c.ObjectName = "comboOrigin"
w.comboOrigin = c
c.addItems(
[
translate("BIM", "Origin"),
translate("BIM", "Top left"),
translate("BIM", "Top center"),
translate("BIM", "Top right"),
translate("BIM", "Middle left"),
translate("BIM", "Middle center"),
translate("BIM", "Middle right"),
translate("BIM", "Bottom left"),
translate("BIM", "Bottom center"),
translate("BIM", "Bottom right"),
]
)
c.setCurrentIndex(PARAMS.GetInt("LibraryDefaultInsert", 0))
c.currentIndexChanged.connect(self.storeInsert)
l.addWidget(c)
return w
def storeInsert(self, index):
PARAMS.SetInt("LibraryDefaultInsert", index)
def mouseMove(self, point, info):
self.box.move(point.add(self.getDelta()))
def mouseClick(self, point, info):
if point:
import Arch
self.box.off()
self.shape.translate(point.add(self.getDelta()))
obj = Arch.makeEquipment()
obj.Shape = self.shape
obj.Label = self.name
self.reject()
def getDelta(self):
d = FreeCAD.Vector(
-self.shape.BoundBox.Center.x, -self.shape.BoundBox.Center.y, 0
)
idx = self.origin.comboOrigin.currentIndex()
if idx <= 0:
return FreeCAD.Vector()
elif idx == 1:
return d.add(
FreeCAD.Vector(
self.shape.BoundBox.XLength / 2, -self.shape.BoundBox.YLength / 2, 0
)
)
elif idx == 2:
return d.add(FreeCAD.Vector(0, -self.shape.BoundBox.YLength / 2, 0))
elif idx == 3:
return d.add(
FreeCAD.Vector(
-self.shape.BoundBox.XLength / 2,
-self.shape.BoundBox.YLength / 2,
0,
)
)
elif idx == 4:
return d.add(FreeCAD.Vector(self.shape.BoundBox.XLength / 2, 0, 0))
elif idx == 5:
return d
elif idx == 6:
return d.add(FreeCAD.Vector(-self.shape.BoundBox.XLength / 2, 0, 0))
elif idx == 7:
return d.add(
FreeCAD.Vector(
self.shape.BoundBox.XLength / 2, self.shape.BoundBox.YLength / 2, 0
)
)
elif idx == 8:
return d.add(FreeCAD.Vector(0, self.shape.BoundBox.YLength / 2, 0))
elif idx == 9:
return d.add(
FreeCAD.Vector(
-self.shape.BoundBox.XLength / 2, self.shape.BoundBox.YLength / 2, 0
)
)
def getOnlineContentsWEB(self, url):
"""Returns a dirs,files pair representing files found from a github url. OBSOLETE"""
# obsolete code - now using getOnlineContentsAPI
import urllib.request
result = {}
u = urllib.request.urlopen(url)
if u:
p = u.read()
if sys.version_info.major >= 3:
p = str(p)
dirs = re.findall(r"<.*?octicon-file-directory.*?href.*?>(.*?)</a>", p)
files = re.findall(r'<.*?octicon-file".*?href.*?>(.*?)</a>', p)
nfiles = []
for f in files:
for ft in self.getFilters():
if f.endswith(ft[1:]):
nfiles.append(f)
break
files = nfiles
for d in dirs:
# <spans>
if "</span" in d:
d1 = re.findall(r"<span.*?>(.*?)<", d)
d2 = re.findall(r"</span>(.*?)$", d)
if d1 and d2:
d = d1[0] + "/" + d2[0]
r = self.getOnlineContentsWEB(url + "/" + d.replace(" ", "%20"))
result[d] = r
for f in files:
result[f] = f
else:
FreeCAD.Console.PrintError(
translate("BIM", "Cannot open URL") + ":" + url + "\n"
)
return result
def getOnlineContentsAPI(self, url):
"""same as getOnlineContents but uses github API (faster)"""
import requests
import json
result = {}
count = 0
r = requests.get(
"https://api.github.com/repos/FreeCAD/FreeCAD-library/git/trees/master?recursive=1"
)
if r.ok:
j = json.loads(r.content)
if j["truncated"]:
print(
"WARNING: The fetched content exceeds maximum Github allowance and is truncated"
)
t = j["tree"]
for f in t:
path = f["path"].split("/")
if f["type"] == "tree":
name = None
else:
name = path[-1]
path = path[:-1]
host = result
for fp in path:
if fp in host:
host = host[fp]
else:
host[fp] = {}
host = host[fp]
if name:
for ft in self.getFilters():
if name.endswith(ft[1:]):
break
else:
continue
host[name] = name
count += 1
else:
FreeCAD.Console.PrintError(
translate("BIM", "Could not fetch library contents") + "\n"
)
# print("result:",result)
if not result:
FreeCAD.Console.PrintError(
translate("BIM", "No results fetched from online library") + "\n"
)
else:
FreeCAD.Console.PrintLog("BIM Library: Reloaded " + str(count) + " files\n")
return result
def onCheckOnline(self, state=None):
"""if the Online checkbox is clicked"""
import datetime
if state == None:
state = self.form.checkOnline.isChecked()
# save state
PARAMS.SetBool("LibraryOnline", state)
if state:
# online
if USE_API:
needrefresh = True
timestamp = datetime.datetime.now()
if os.path.exists(os.path.join(TEMPLIBPATH, LIBINDEXFILE)):
stored = PARAMS.GetUnsigned("LibraryTimeStamp", 0)
if stored:
stored = datetime.datetime.fromtimestamp(stored)
if (timestamp - stored).total_seconds() < REFRESH_INTERVAL:
needrefresh = False
if needrefresh:
PARAMS.SetUnsigned("LibraryTimeStamp", int(timestamp.timestamp()))
self.onRefresh()
else:
FreeCAD.Console.PrintLog("BIM Library: Using cached library\n")
self.setOnlineModel()
self.form.buttonLink.setEnabled(False)
else:
# offline
self.setFileModel()
self.form.buttonLink.setEnabled(True)
def onRefresh(self):
"""refreshes the tree"""
from PySide import QtCore, QtGui
def writeOfflineLib():
if USE_API:
rootfiles = self.getOnlineContentsAPI(LIBRARYURL)
else:
rootfiles = self.getOnlineContentsWEB(LIBRARYURL)
if rootfiles:
templibfile = os.path.join(TEMPLIBPATH, LIBINDEXFILE)
os.makedirs(TEMPLIBPATH, exist_ok=True)
tf = open(templibfile, "w", encoding="utf8")
tf.write("library=" + str(rootfiles) + "\n")
tf.close()
self.setOnlineModel()
reply = PARAMS.GetBool("LibraryWarning", False)
if not reply:
reply = QtGui.QMessageBox.information(
None, "", translate("BIM", "Warning, this can take several minutes!")
)
if reply:
PARAMS.SetBool("LibraryWarning", True)
self.form.setEnabled(False)
QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor)
self.form.repaint()
QtGui.QApplication.processEvents()
QtCore.QTimer.singleShot(1, writeOfflineLib)
self.form.setEnabled(True)
QtGui.QApplication.restoreOverrideCursor()
else:
self.setOnlineModel()
def onCheckFCStdOnly(self, state):
"""if the FCStd only checkbox is clicked"""
# save state
PARAMS.SetBool("LibraryFCStdOnly", state)
self.dirmodel.setNameFilters(self.getFilters())
self.onCheckOnline(self.form.checkOnline.isChecked())
def onCheckWebSearch(self, state):
"""if the web search checkbox is clicked"""
# save state
PARAMS.SetBool("LibraryWebSearch", state)
def onCheck3DPreview(self, state):
"""if the 3D preview checkbox is clicked"""
# save state
PARAMS.SetBool("3DPreview", state)
self.previewOn = PARAMS.GetBool("3DPreview", False)
try:
FreeCAD.closeDocument(self.previewDocName)
except:
pass
if self.previewOn == True:
self.previewDocName = "Viewer"
self.doc = FreeCAD.newDocument(self.previewDocName)
FreeCADGui.ActiveDocument.ActiveView.viewIsometric()
return self.previewDocName
def onCheckThumbnail(self, state):
"""if the thumbnail checkbox is clicked"""
# save state
PARAMS.SetBool("SaveThumbnails", state)
def onButtonOptions(self):
"""hides/shows the options"""
if self.form.frameOptions.isVisible():
self.form.frameOptions.hide()
self.form.buttonOptions.setText(translate("BIM", "Options") + "")
else:
self.form.frameOptions.show()
self.form.buttonOptions.setText(translate("BIM", "Options") + "")
def onButtonPreview(self):
"""hides/shows the preview"""
if self.form.framePreview.isVisible():
self.form.framePreview.hide()
self.form.buttonPreview.setText(translate("BIM", "Preview") + "")
PARAMS.SetBool("LibraryPreview", False)
else:
self.form.framePreview.show()
self.form.buttonPreview.setText(translate("BIM", "Preview") + "")
PARAMS.SetBool("LibraryPreview", True)
def getThumbnail(self, filepath):
"""returns a thumbnail image path for a given file path"""
import urllib.request
import urllib.parse
import zipfile
import io
if not filepath.lower().endswith(".fcstd"):
return None
iconname = self.getHashname(filepath)
iconfile = os.path.join(THUMBNAILSPATH, iconname)
if os.path.exists(iconfile):
return iconfile
else:
if self.form.checkOnline.isChecked():
# download file
u = urllib.request.urlopen(urllib.parse.quote(filepath, safe=":/."))
fdata = u.read()
u.close()
f = io.BytesIO(fdata)
else:
f = filepath
zfile = zipfile.ZipFile(f)
if "thumbnails/Thumbnail.png" in zfile.namelist():
data = zfile.read("thumbnails/Thumbnail.png")
os.makedirs(os.path.dirname(iconfile), exist_ok=True)
thumb = open(iconfile, "wb")
thumb.write(data)
thumb.close()
return iconfile
else:
return None
def getHashname(self, filepath):
"""creates a png filename for a given file path"""
import hashlib
filepath = self.cleanPath(filepath)
return hashlib.md5(filepath.encode()).hexdigest() + ".png"
def cleanPath(self, filepath):
"""cleans a file path into subfolder/subfolder/file form"""
import urllib.request
import urllib.parse
if filepath.startswith(self.librarypath):
# strip local part od the path
filepath = filepath[len(self.librarypath) :]
if filepath.startswith(RAWURL):
filepath = filepath[len(RAWURL) :]
filepath = filepath.replace("\\", "/")
if filepath.startswith("/"):
filepath = filepath[1:]
filepath = urllib.parse.quote(filepath)
return filepath
def onExternalSearch(self, index):
"""searches on external websites"""
if index > 0:
baseurl = self.form.comboSearch.itemData(index)
term = self.form.searchBox.text()
if term:
self.openUrl(baseurl + self.urlencode(term))
if FreeCAD.GuiUp:
from PySide import QtCore, QtGui
class LibraryModel(QtGui.QFileSystemModel):
"a custom QFileSystemModel that displays freecad file icons"
def __init__(self):
QtGui.QFileSystemModel.__init__(self)
def data(self, index, role):
if index.column() == 0 and role == QtCore.Qt.DecorationRole:
if index.data().lower().endswith(".fcstd"):
return QtGui.QIcon(":icons/freecad-doc.png")
elif index.data().lower().endswith(".ifc"):
return QtGui.QIcon(
os.path.join(os.path.dirname(__file__), "icons", "IFC.svg")
)
elif index.data().lower() == "private":
return QtGui.QIcon.fromTheme("folder-lock")
return super(LibraryModel, self).data(index, role)
FreeCADGui.addCommand("BIM_Library", BIM_Library())