From 0c190f4d2463607074f4cdc61873a79a6a67b33d Mon Sep 17 00:00:00 2001 From: furti Date: Thu, 28 Mar 2019 18:34:15 +0100 Subject: [PATCH] Add BooleanMesh The BooleanMesh is a abstract FeaturePython object that can build a final mesh from a base mesh and a list of boolean operations. The following BooleanFeatures are available: - Addition: Adds the features base geometry to the mesh - Subtraction: Subtracts the features base geometry from the mesh affects #33 --- Resources/Icons/BooleanMeshFeatureAdd.svg | 189 ++++++++++ .../Icons/BooleanMeshFeatureDisabled.svg | 196 +++++++++++ .../Icons/BooleanMeshFeatureSubtract.svg | 192 ++++++++++ boolean_mesh.py | 328 ++++++++++++++++++ 4 files changed, 905 insertions(+) create mode 100644 Resources/Icons/BooleanMeshFeatureAdd.svg create mode 100644 Resources/Icons/BooleanMeshFeatureDisabled.svg create mode 100644 Resources/Icons/BooleanMeshFeatureSubtract.svg create mode 100644 boolean_mesh.py diff --git a/Resources/Icons/BooleanMeshFeatureAdd.svg b/Resources/Icons/BooleanMeshFeatureAdd.svg new file mode 100644 index 0000000..2cbe282 --- /dev/null +++ b/Resources/Icons/BooleanMeshFeatureAdd.svg @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Resources/Icons/BooleanMeshFeatureDisabled.svg b/Resources/Icons/BooleanMeshFeatureDisabled.svg new file mode 100644 index 0000000..c256284 --- /dev/null +++ b/Resources/Icons/BooleanMeshFeatureDisabled.svg @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Resources/Icons/BooleanMeshFeatureSubtract.svg b/Resources/Icons/BooleanMeshFeatureSubtract.svg new file mode 100644 index 0000000..159000b --- /dev/null +++ b/Resources/Icons/BooleanMeshFeatureSubtract.svg @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/boolean_mesh.py b/boolean_mesh.py new file mode 100644 index 0000000..a0dfcb3 --- /dev/null +++ b/boolean_mesh.py @@ -0,0 +1,328 @@ +'''Base FeaturePython implementation that can handle boolean operations with meshes''' + +import FreeCAD +import Mesh + +from base_lithophane_processor import BaseLithophaneProcessor +from utils.resource_utils import iconPath +import utils.qtutils as qtutils + +MODE_MAPPING = { + 'Additive': 'union', + 'Subtractive': 'difference' +} + +ICON_MAPPING = { + 'Additive': iconPath('BooleanMeshFeatureAdd.svg'), + 'Subtractive': iconPath('BooleanMeshFeatureSubtract.svg') +} + + +class BooleanMeshProcessor(BaseLithophaneProcessor): + def __init__(self, description, checkExecutionFunction, extractMeshFunction, processingStepsFunction): + super(BooleanMeshProcessor, self).__init__(description) + + self.result = None + self.checkExecutionFunction = checkExecutionFunction + self.extractMeshFunction = extractMeshFunction + self.processingStepsFunction = processingStepsFunction + + def checkExecution(self): + return self.checkExecutionFunction() + + def processingDone(self, obj, params): + self.result = self.extractMeshFunction(obj, params) + + def getProcessingSteps(self, obj): + return self.processingStepsFunction(obj) + + +class BooleanMeshFeature(object): + def __init__(self, obj, base, mode): + obj.Proxy = self + + self.setProperties(obj) + + obj.Base = base + obj.Mode = mode + + obj.Base.ViewObject.Visibility = False + + def execute(self, obj): + import MeshPart + + base = obj.Base + + if hasattr(base, 'Mesh') and isinstance(base, Mesh.Mesh): + self.mesh = base + elif hasattr(base, 'Shape') and base.Shape: + self.mesh = MeshPart.meshFromShape(base.Shape.copy(False)) + else: + self.mesh = MeshPart.meshFromShape(base.copy(False)) + + def applyOperationToMesh(self, mesh): + if not self.Object.Enabled: + return mesh + + import OpenSCADUtils + + openscadOperation = MODE_MAPPING[self.Object.Mode] + + return OpenSCADUtils.meshoptempfile(openscadOperation, (mesh, self.mesh)) + + def setProperties(self, obj): + self.Object = obj + pl = obj.PropertiesList + + if not 'Base' in pl: + obj.addProperty("App::PropertyLink", "Base", "Boolean", + "The geometry to apply to the mesh") + + if not 'Mode' in pl: + obj.addProperty("App::PropertyEnumeration", "Mode", "Boolean", + "The type of operation to apply to the mesh") + + obj.Mode = ['Additive', 'Subtractive'] + + if not 'Enabled' in pl: + obj.addProperty("App::PropertyBool", "Enabled", "Boolean", + "When False, the operation will not be applied").Enabled = True + + def onDocumentRestored(self, obj): + self.setProperties(obj) + + def __getstate__(self): + '''We do not store any data for now''' + pass + + def __setstate__(self, state): + '''We do not store any data for now''' + pass + + +class ViewProviderBooleanMeshFeature(object): + def __init__(self, vobj): + vobj.Proxy = self + + def attach(self, vobj): + self.ViewObject = vobj + self.Object = vobj.Object + + from pivy import coin + + self.coinNode = coin.SoGroup() + vobj.addDisplayMode(self.coinNode, "Standard") + + def onChanged(self, vp, prop): + pass + + def claimChildren(self): + return [self.Object.Base] + + def getIcon(self): + if not self.Object.Enabled: + return iconPath('BooleanMeshFeatureDisabled.svg') + + return ICON_MAPPING[self.Object.Mode] + + def getDisplayModes(self, obj): + return ["Standard"] + + def getDefaultDisplayMode(self): + return "Standard" + + def __getstate__(self): + '''We do not store any data for now''' + pass + + def __setstate__(self, state): + '''We do not store any data for now''' + pass + + +class BooleanMesh(object): + def __init__(self, obj): + obj.Proxy = self + + self.setProperties(obj) + + def getDescription(self): + raise NotImplementedError + + def getIcon(self): + return None + + def checkBaseMeshExecution(self): + if self.Object.LithophaneImage is None: + qtutils.showInfo('No LithophaneImage linked', + 'Please link a lithophane Image to the mesh to caculate the geometry') + + return None + + return self.Object.LithophaneImage.Proxy + + def extractBaseMesh(self, obj, params): + raise NotImplementedError + + def getBaseProcessingSteps(self, obj): + raise NotImplementedError + + def getResultName(self, obj): + if not obj.LithophaneImage: + return "Result" + + return obj.LithophaneImage.Name + '_Result' + + def execute(self, obj): + if not obj.Result: + m = FreeCAD.ActiveDocument.addObject( + "Mesh::Feature", self.getResultName(obj)) + + obj.Result = m + + resultMesh = Mesh.Mesh() + + description = self.getDescription() + + baseMeshProcessor = BooleanMeshProcessor( + description + " (Base)", self.checkBaseMeshExecution, self.extractBaseMesh, self.getBaseProcessingSteps) + baseMeshProcessor.execute(obj) + + mesh = baseMeshProcessor.result + + if len(obj.Features) > 0: + for meshFeature in obj.Features: + mesh = meshFeature.Proxy.applyOperationToMesh(mesh) + + resultMesh.addMesh(mesh) + + obj.Result.Mesh = resultMesh + + def addAdditiveFeature(self, base): + createFeature(self.Object, base, 'Additive') + + def addSubtractiveFeature(self, base): + createFeature(self.Object, base, 'Subtractive') + + def setProperties(self, obj): + self.Object = obj + self.isBooleanMesh = True + + pl = obj.PropertiesList + + if not 'LithophaneImage' in pl: + obj.addProperty("App::PropertyLink", "LithophaneImage", "Image", + "The image used to build the geometry") + + if not 'Result' in pl: + obj.addProperty("App::PropertyLink", "Result", "Mesh", + "The mesh that stores the final result") + + if not 'Features' in pl: + obj.addProperty("App::PropertyLinkList", "Features", "Feature", + "The boolean operations to apply to the mesh") + + def onDocumentRestored(self, obj): + self.setProperties(obj) + + def __getstate__(self): + '''We do not store any data for now''' + pass + + def __setstate__(self, state): + '''We do not store any data for now''' + pass + + +class ViewProviderBooleanMesh(): + def __init__(self, vobj): + vobj.Proxy = self + + def attach(self, vobj): + self.ViewObject = vobj + self.Object = vobj.Object + self.BooleanMesh = self.Object.Proxy + + from pivy import coin + + self.coinNode = coin.SoGroup() + vobj.addDisplayMode(self.coinNode, "Standard") + + def onChanged(self, vp, prop): + if prop == 'Visibility': + self.Object.Result.ViewObject.Visibility = vp.Visibility + + def onDelete(self, vp, subelements): + if self.Object.Result: + FreeCAD.ActiveDocument.removeObject(self.Object.Result.Name) + + return True + + def claimChildren(self): + children = [] + + children.extend(self.Object.Features) + children.append(self.Object.Result) + + return children + + def getDisplayModes(self, obj): + return ["Standard"] + + def getDefaultDisplayMode(self): + return "Standard" + + def getIcon(self): + return self.BooleanMesh.getIcon() + + def __getstate__(self): + '''We do not store any data for now''' + pass + + def __setstate__(self, state): + '''We do not store any data for now''' + pass + + +def createFeature(booleanMesh, base, mode): + feature = FreeCAD.ActiveDocument.addObject( + "App::FeaturePython", base.Label + "_" + mode) + BooleanMeshFeature(feature, base, mode) + ViewProviderBooleanMeshFeature(feature.ViewObject) + + features = booleanMesh.Features + features.append(feature) + booleanMesh.Features = features + + +if __name__ == '__main__': + + class DummyMesh(BooleanMesh): + def __init__(self, obj): + super(DummyMesh, self).__init__(obj) + + def getDescription(self): + return 'Dummy' + + def checkBaseMeshExecution(self): + return '' + + def getBaseProcessingSteps(self, obj): + return [] + + def extractBaseMesh(self, obj, params): + return Mesh.createBox() + + booleanMesh = FreeCAD.ActiveDocument.addObject( + "App::FeaturePython", 'DummyMesh') + DummyMesh(booleanMesh) + ViewProviderBooleanMesh(booleanMesh.ViewObject) + + additiveBox = App.ActiveDocument.addObject("Part::Box", "AdditiveBox") + createFeature(booleanMesh, additiveBox, 'Additive') + + subtractiveBox = App.ActiveDocument.addObject( + "Part::Box", "SubtractiveBox") + subtractiveBox.Placement = App.Placement( + App.Vector(-10, -10, -10), App.Rotation(App.Vector(0, 0, 1), 0)) + createFeature(booleanMesh, subtractiveBox, 'Subtractive')