From ce3282cf1733d6e7b042781537a847ad600d58f1 Mon Sep 17 00:00:00 2001 From: Munther Hindi Date: Tue, 25 Feb 2025 08:43:59 -0800 Subject: [PATCH 1/8] Modified buildDocTree using Zhang's suggestion to work with Windows --- Utils/calcCenterOfMass.py | 24 +++++++++++-- freecad/gdml/GDMLObjects.py | 2 +- freecad/gdml/exportGDML.py | 68 ++++++++++++++++++++----------------- 3 files changed, 59 insertions(+), 35 deletions(-) diff --git a/Utils/calcCenterOfMass.py b/Utils/calcCenterOfMass.py index e5e6d86c..c21e2a40 100644 --- a/Utils/calcCenterOfMass.py +++ b/Utils/calcCenterOfMass.py @@ -166,7 +166,7 @@ def MonteCarloMoments(shape, nsim: int) -> Moments: return moments -def topObjects(part: App.Part) -> list: +def topObjects(part) -> list: ''' return a list containing the top shapes contained in part a top shape may contain other shapes, but cannot be contained in other shapes within Part @@ -177,7 +177,7 @@ def topObjects(part: App.Part) -> list: return [] -def partCM(part: App.Part) -> tuple: +def partCM(part) -> tuple: ''' Note, what we are calling the total moment of inertia is NOT the total moment of inertia at all! @@ -251,6 +251,26 @@ def partCM(part: App.Part) -> tuple: # prettyPrint(obj.Label, vol, cmglob, IIorigin) + elif obj.TypeId == "Mesh::Feature": + mesh = obj.Mesh + shape = Part.Shape() + shape.makeShapeFromMesh(mesh.Topology, 0.5) + solid = Part.makeSolid(shape) + II0 = solid.MatrixOfInertia + globalPlacement = obj.getGlobalPlacement() + rotglob = globalPlacement.Rotation + rotobj = obj.Placement.Rotation + cm0 = shape.CenterOfGravity + vol = shape.Volume + cmglob = globalPlacement.Base + rotglob*rotobj.inverted()*(cm0 - obj.Placement.Base) + mom0: Moments = Moments(vol, cmglob, II0) + mom0.M = mom0.inertiaRotated(rotobj.inverted()) + mom0.M = mom0.inertiaRotated(rotglob) + IIorigin: Matrix = mom0.inertiaAboutOrigin() + II += IIorigin + cm += vol * cmglob + totVol += vol + elif obj.TypeId == 'App::Part': partVol, cm0, II0 = partCM(obj) cm += partVol * cm0 diff --git a/freecad/gdml/GDMLObjects.py b/freecad/gdml/GDMLObjects.py index f0386187..78f59b8c 100644 --- a/freecad/gdml/GDMLObjects.py +++ b/freecad/gdml/GDMLObjects.py @@ -4386,7 +4386,7 @@ def reMesh(self, fp): # def execute(self, fp): in GDMLsolid def createGeometry(self, fp): - breakpoint() + # breakpoint() currPlacement = fp.Placement mul = GDMLShared.getMult(fp) FCfaces = [] diff --git a/freecad/gdml/exportGDML.py b/freecad/gdml/exportGDML.py index da77e20f..99195450 100644 --- a/freecad/gdml/exportGDML.py +++ b/freecad/gdml/exportGDML.py @@ -156,7 +156,6 @@ def nameUsedFor(obj): @staticmethod def getVolumeName(vol) -> str: - # breakpoint() if vol in NameManager._volumeNamesDict: return NameManager._volumeNamesDict[vol] @@ -1257,7 +1256,6 @@ def _getSubVols(vol, placement, volLabel): # vol must be an assembly, recurse for obj in childObjects[vol]: - # breakpoint() typeId = obj.TypeId tObj = obj # print(obj.Label) @@ -2139,7 +2137,6 @@ def processVolume(vol, xmlParent, psPlacement): # So for s in list is not so good # type 1 straight GDML type = 2 for GEMC # xmlVol could be created dummy volume - # breakpoint() if vol.TypeId == "App::Link": print("Volume is Link") placement = vol.Placement @@ -2294,7 +2291,6 @@ def processVolAssem(vol, xmlParent, parentName, psPlacement=None): # If the vol is placed inside a solid # and that solid has a non-zero placement # we need to shift vol by inverse of the psPlacement - # breakpoint() if vol.Label[:12] != "NOT_Expanded": print(f"process VolAsm Name {vol.Name} Label {vol.Label}") volName = NameManager.getName(vol) @@ -2374,20 +2370,16 @@ def createWorldVol(volName): ET.SubElement(gxml, "volume", {"name": volName, "material": "G4_AIR"}) return worldVol - def buildDocTree(): from PySide import QtWidgets - # buildDocTree now builds global childObjects - # Used in exportGDML and GDMLCommands global childObjects childObjects = {} # dictionary of list of child objects for each object - - # TypeIds that should not go in to the tree skippedTypes = ["App::Origin", "Sketcher::SketchObject", "Part::Compound"] def addDaughters(item: QtWidgets.QTreeWidgetItem): + print (f"--------addDaughters {item.text(0)}") objectLabel = item.text(0) object = App.ActiveDocument.getObjectsByLabel(objectLabel)[0] if object not in childObjects: @@ -2407,32 +2399,44 @@ def addDaughters(item: QtWidgets.QTreeWidgetItem): # Get world volume from document tree widget worldObj = FreeCADGui.Selection.getSelection()[0] - tree = FreeCADGui.getMainWindow().findChildren(QtGui.QTreeWidget)[0] - it = QtGui.QTreeWidgetItemIterator(tree) - doc = FreeCAD.ActiveDocument - found = False + # tree = FreeCADGui.getMainWindow().findChildren(QtGui.QTreeWidget)[0] + # it = QtGui.QTreeWidgetItemIterator(tree) - for nextObject in it: - item = nextObject.value() - treeLabel = item.text(0) - if not found: - if treeLabel != doc.Label: - continue + mw1 = FreeCADGui.getMainWindow() + print (f"---------Number of trees {len(mw1.findChildren(QtGui.QTreeWidget))}") + treesSel = mw1.findChildren(QtGui.QTreeWidget) + print (f"---------Number of trees {len(treesSel)}") + # breakpoint() - found = True - try: - objs = doc.getObjectsByLabel(treeLabel) - if len(objs) == 0: - continue + doc = FreeCAD.ActiveDocument + found = False - obj = objs[0] - if obj == worldObj: - # we presume first app part is world volume - addDaughters(item) - break - except Exception as e: - print(e) - FreeCADobject = None + for tree in treesSel: + print(f"--------Tree {tree.objectName()}") + items = tree.selectedItems() + for item in items: + treeLabel = item.text(0) + print(f"--------Item {treeLabel}") + print(f"--------Doc.Label {doc.Label}") + # if not found: + # if treeLabel != doc.Label: + # continue + # found = True + try: + objs = doc.getObjectsByLabel(treeLabel) + print(f"--------Objects {objs}") + if len(objs) == 0: + continue + + obj = objs[0] + if obj == worldObj: + print(f"--------World Object {obj.Label}") + # we presume first app part is world volume + addDaughters(item) + break + except Exception as e: + print(e) + FreeCADobject = None def isContainer(obj): From e229037275abbebcadbfb5aeb959967f1a96656f Mon Sep 17 00:00:00 2001 From: Munther Hindi Date: Sun, 18 Jan 2026 17:35:01 -0800 Subject: [PATCH 2/8] update to SymmetricGDML openmc-alpha branch --- GDML_math.md | 61 + freecad/gdml/GDMLMaterials.py | 76 +- freecad/gdml/GDMLObjects.py | 87 +- freecad/gdml/GDMLShared.py | 24 +- .../gdml/Resources/Isotopic_abundances.csv | 93 + freecad/gdml/Resources/ReactorMaterials.xml | 120 + freecad/gdml/__init__.py | 1 + freecad/gdml/arrayUtils.py | 4 +- freecad/gdml/exportGDML.py | 44 +- freecad/gdml/exportOpenMC.py | 6460 +++++++++++++++++ freecad/gdml/importGDML.py | 36 +- freecad/gdml/polygonsHelper.py | 280 + freecad/gdml/revolveHelper.py | 178 + temp.txt | 13 + 14 files changed, 7385 insertions(+), 92 deletions(-) create mode 100644 GDML_math.md create mode 100644 freecad/gdml/Resources/Isotopic_abundances.csv create mode 100644 freecad/gdml/Resources/ReactorMaterials.xml create mode 100644 freecad/gdml/exportOpenMC.py create mode 100644 freecad/gdml/polygonsHelper.py create mode 100644 freecad/gdml/revolveHelper.py create mode 100644 temp.txt diff --git a/GDML_math.md b/GDML_math.md new file mode 100644 index 00000000..912aa469 --- /dev/null +++ b/GDML_math.md @@ -0,0 +1,61 @@ +In GDML (Geometry Description Markup Language), which is used for defining complex geometries in simulations, particularly within the context of particle physics (like with Geant4), certain mathematical functions are allowed for specifying parameters and dimensions. These functions are used in expressions within GDML to provide dynamic values for various geometry properties. + +### Allowed Mathematical Functions in GDML: +GDML supports a limited set of standard mathematical functions that you can use directly within your XML definitions. These functions include: + +1. **Arithmetic Operations:** + - `+` : Addition + - `-` : Subtraction + - `*` : Multiplication + - `/` : Division + - `%` : Modulus (remainder) + +2. **Power Functions:** + - `pow(a, b)` : Returns `a` raised to the power of `b`. + +3. **Square Root:** + - `sqrt(x)` : Returns the square root of `x`. + +4. **Trigonometric Functions:** + - `sin(x)` : Sine of `x` (angle in radians). + - `cos(x)` : Cosine of `x` (angle in radians). + - `tan(x)` : Tangent of `x` (angle in radians). + - `asin(x)` : Arcsine of `x`, result in radians. + - `acos(x)` : Arccosine of `x`, result in radians. + - `atan(x)` : Arctangent of `x`, result in radians. + - `atan2(y, x)` : Arctangent of `y/x`, result in radians (takes into account the signs of both arguments to determine the correct quadrant). + +5. **Hyperbolic Functions:** + - `sinh(x)` : Hyperbolic sine of `x`. + - `cosh(x)` : Hyperbolic cosine of `x`. + - `tanh(x)` : Hyperbolic tangent of `x`. + +6. **Exponential and Logarithmic Functions:** + - `exp(x)` : Returns `e` raised to the power of `x`. + - `log(x)` : Natural logarithm of `x` (log base `e`). + - `log10(x)` : Base-10 logarithm of `x`. + +7. **Absolute Value:** + - `abs(x)` : Returns the absolute value of `x`. + +8. **Minimum and Maximum:** + - `min(a, b)` : Returns the smaller of `a` and `b`. + - `max(a, b)` : Returns the larger of `a` and `b`. + +9. **Constants:** + - `pi` : Represents the mathematical constant π (pi). + - `e` : Represents the base of the natural logarithm (approximately 2.71828). + +### Usage in GDML: +These functions can be used in attributes for defining dimensions, positions, rotations, and other numeric properties. For example: + +```xml + + +``` + +### Notes: +- **Syntax:** Functions should be written in a syntax that matches typical programming or mathematical conventions (e.g., `sqrt(4)` for the square root of 4). +- **Units:** When using these functions, ensure that any numerical results are in the correct units, as GDML does not automatically handle unit conversions. + +These functions give GDML users flexibility in defining complex geometrical shapes and relationships, ensuring that geometrical parameters can be dynamically calculated based on expressions rather than fixed values. diff --git a/freecad/gdml/GDMLMaterials.py b/freecad/gdml/GDMLMaterials.py index cb4c38b5..b6d180cf 100644 --- a/freecad/gdml/GDMLMaterials.py +++ b/freecad/gdml/GDMLMaterials.py @@ -31,6 +31,8 @@ from PySide import QtGui, QtCore +from freecad.gdml.importGDML import processReactor + class GDMLMaterial(QtGui.QComboBox): @@ -62,10 +64,22 @@ def getMaterialsList(): materials = doc.Materials g4Mats = doc.getObject('G4Materials') + try: + reactorMats = doc.getObject('ReactorMaterials') + + except: + from .importGDML import processReactor + from .init_gui import joinDir + + print('Load Geant4 Materials XML') + processReactor(doc, joinDir("Resources/ReactorMaterials.xml")) + materials = doc.Materials + reactorMats = doc.getObject('ReactorMaterials') + try: if materials is not None: for m in materials.OutList: - if m.Label != "Geant4": + if m.Label != "Geant4" and m.Label != "RaactorMaterials": matList.append(m.Label) print(matList) except: @@ -80,6 +94,15 @@ def getMaterialsList(): except: pass + try: + if reactorMats is not None: + for m in reactorMats.OutList: + for n in m.OutList: + matList.append(n.Label) + # print(matList) + except: + pass + return matList @@ -100,20 +123,23 @@ def refreshG4Materials(doc): def newGetGroupedMaterials(): - from .importGDML import joinDir, processGEANT4 + from .importGDML import joinDir, processGEANT4, processReactor from .GDMLObjects import GroupedMaterials print(f'New getGroupedMaterials len GroupMaterials {len(GroupedMaterials)}') # if len(GroupedMaterials) == 0: mlen = len(GroupedMaterials) if mlen >= 0: doc = FreeCAD.activeDocument() - if not hasattr(doc, 'Materials') or not hasattr(doc, 'G4Materials'): + if not hasattr(doc, 'Materials') or not hasattr(doc, 'G4Materials') or not hasattr(doc, 'ReactorMaterials'): processGEANT4(doc, joinDir("Resources/Geant4Materials.xml")) + processReactor(doc, joinDir("Resources/ReactorMaterials.xml")) docG4Materials = doc.G4Materials if not hasattr(docG4Materials, 'version'): refreshG4Materials(doc) docG4Materials = doc.G4Materials + reactorMaterials = doc.ReactorMaterials print(f'doc.G4Materials {docG4Materials}') + print(f'doc.ReactorMaterials {reactorMaterials}') for g in docG4Materials.Group: # print(f'g : {g.Label}') for s in g.Group: @@ -122,6 +148,9 @@ def newGetGroupedMaterials(): GroupedMaterials[g.Label].append(s.Label) else: GroupedMaterials[g.Label] = [s.Label] + + GroupedMaterials[reactorMaterials.Label] = [g.Label for g in reactorMaterials.Group] + matList = [] docMaterials = doc.Materials print(f'doc.Materials {docMaterials}') @@ -136,44 +165,3 @@ def newGetGroupedMaterials(): GroupedMaterials['Normal'] = matList return GroupedMaterials - - -def getGroupedMaterials(): - print('getGroupedMaterials') - from .GDMLObjects import GroupedMaterials - from .importGDML import setupEtree - from .init_gui import joinDir - - if len(GroupedMaterials) == 0: - etree, root = setupEtree(joinDir("Resources/Geant4Materials.xml")) - materials = root.find('materials') - - for material in materials.findall('material'): - name = material.get('name') - print(name) - if name is None: - print("Missing Name") - else: - for auxiliary in material.findall('auxiliary'): - auxtype = auxiliary.get('auxtype') - if auxtype == 'Material-type': - auxvalue = auxiliary.get('auxvalue') - if auxvalue in GroupedMaterials: - GroupedMaterials[auxvalue].append(name) - else: - GroupedMaterials[auxvalue] = [name] - - doc = FreeCAD.activeDocument() - docMaterials = doc.Materials - matList = [] - - if doc.Materials is not None: - for m in docMaterials.OutList: - if m.Label != "Geant4": - if m.Label not in matList: - matList.append(m.Label) - - if len(matList) > 0: - GroupedMaterials['Normal'] = matList - - return GroupedMaterials diff --git a/freecad/gdml/GDMLObjects.py b/freecad/gdml/GDMLObjects.py index 78f59b8c..9be48d40 100644 --- a/freecad/gdml/GDMLObjects.py +++ b/freecad/gdml/GDMLObjects.py @@ -2249,7 +2249,7 @@ def createGeometry(self, fp): sdir = FreeCAD.Vector(0, 0, 1) outerTorus = Part.makeTorus( - rtor, rmax, spnt, sdir, 0, 360, getAngleDeg(fp.aunit, fp.deltaphi) + rtor, rmax, spnt, sdir, -180, 180, getAngleDeg(fp.aunit, fp.deltaphi) ) if rmin > 0: innerTorus = Part.makeTorus( @@ -2257,15 +2257,16 @@ def createGeometry(self, fp): rmin, spnt, sdir, - 0, - 360, + -180, + 180, getAngleDeg(fp.aunit, fp.deltaphi), ) torus = outerTorus.cut(innerTorus) else: torus = outerTorus + if fp.startphi != 0: - torus.rotate(spnt, sdir, getAngleDeg(fp.aunit, fp.startphi)) + torus = torus.rotated(spnt, sdir, getAngleDeg(fp.aunit, fp.startphi)) fp.Shape = torus if hasattr(fp, "scale"): super().scale(fp) @@ -2365,7 +2366,7 @@ def createGeometry(self, fp): y = mul * fp.y z = mul * fp.z angle = getAngleDeg(fp.aunit, fp.PhiTwist) - # lower rectanngle vertexes + # lower rectangle vertexes v1 = FreeCAD.Vector(-x / 2, -y / 2, -z / 2) v2 = FreeCAD.Vector(x / 2, -y / 2, -z / 2) v3 = FreeCAD.Vector(x / 2, y / 2, -z / 2) @@ -4283,13 +4284,13 @@ def __init__( ): super().__init__(obj) obj.addProperty( - "App::PropertyInteger", "facets", "GDMLGmshTessellated", "Facets" - ).facets = len(facets) - obj.setEditorMode("facets", 1) + "App::PropertyInteger", "numFacets", "GDMLGmshTessellated", "Facets" + ).numFacets = len(facets) + obj.setEditorMode("numFacets", 1) obj.addProperty( - "App::PropertyInteger", "vertex", "GDMLGmshTessellated", "Vertex" - ).vertex = len(vertex) - obj.setEditorMode("vertex", 1) + "App::PropertyInteger", "numVertex", "GDMLGmshTessellated", "Vertex" + ).numVertex = len(vertex) + obj.setEditorMode("numVertex", 1) # Properties NOT the same GmshTessellate GmshMinTessellate #obj.addProperty( # "App::PropertyFloat", @@ -4324,19 +4325,19 @@ def __init__( updateColour(obj, colour, material) self.Type = "GDMLGmshTessellated" self.SourceObj = sourceObj - self.Vertex = vertex - self.Facets = facets + self.vertex = vertex + self.facets = facets self.Object = obj self.colour = colour obj.Proxy = self obj.Proxy.Type = "GDMLGmshTessellated" def updateParams(self, vertex, facets, flag): - self.Vertex = vertex - self.Facets = facets - self.facets = len(facets) - self.vertex = len(vertex) - print(f"Vertex : {self.vertex} Facets : {self.facets}") + self.vertex = vertex + self.facets = facets + self.numFacets = len(self.facets) + self.numVertex = len(self.vertex) + print(f"Vertex : {self.numVertex} Facets : {self.numFacets}") def onChanged(self, fp, prop): """Do something when a property has changed""" @@ -4375,36 +4376,35 @@ def reMesh(self, fp): initialize() meshObj(fp.Proxy.SourceObj, 2, True, fp.Proxy.Object) - facets = getFacets() - vertex = getVertex() - fp.Proxy.Vertex = vertex - self.Object.vertex = len(vertex) - fp.Proxy.Facets = facets - self.Object.facets = len(facets) + self.facets = getFacets() + self.vertex = getVertex() + fp.Proxy.vertex = self.vertex + self.Object.numVertex = len(self.vertex) + fp.Proxy.facets = self.facets + self.Object.numFacets = len(self.facets) FreeCADGui.updateGui() # def execute(self, fp): in GDMLsolid def createGeometry(self, fp): - # breakpoint() currPlacement = fp.Placement mul = GDMLShared.getMult(fp) FCfaces = [] - for f in self.Facets: + for f in self.facets: if len(f) == 3: face = GDMLShared.triangle( - mul * self.Vertex[f[0]], - mul * self.Vertex[f[1]], - mul * self.Vertex[f[2]] + mul * self.vertex[f[0]], + mul * self.vertex[f[1]], + mul * self.vertex[f[2]] ) if face is not None: FCfaces.append(face) else: # len should then be 4 quadFace = GDMLShared.quad( - mul * self.Vertex[f[0]], - mul * self.Vertex[f[1]], - mul * self.Vertex[f[2]], - mul * self.Vertex[f[3]] + mul * self.vertex[f[0]], + mul * self.vertex[f[1]], + mul * self.vertex[f[2]], + mul * self.vertex[f[3]] ) if quadFace is not None: FCfaces.append(quadFace) @@ -4412,16 +4412,16 @@ def createGeometry(self, fp): print(f"Create Quad Failed {f[0]} {f[1]} {f[2]} {f[3]}") print("Creating as two triangles") face = GDMLShared.triangle( - mul * self.Vertex[f[0]], - mul * self.Vertex[f[1]], - mul * self.Vertex[f[2]] + mul * self.vertex[f[0]], + mul * self.vertex[f[1]], + mul * self.vertex[f[2]] ) if face is not None: FCfaces.append(face) face = GDMLShared.triangle( - mul * self.Vertex[f[0]], - mul * self.Vertex[f[2]], - mul * self.Vertex[f[3]] + mul * self.vertex[f[0]], + mul * self.vertex[f[2]], + mul * self.vertex[f[3]] ) if face is not None: FCfaces.append(face) @@ -5765,6 +5765,15 @@ def makeSphere(rmin, rmax, startphi, deltaphi, starttheta, deltatheta, \ obj.recompute() return obj +def makeTrap(z, theta, phi, x1, x2, x3, x4, y1, y2, alpha, aunit, lunit, \ + material, colour=None): + obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "GDMLTrap") + if obj is not None: + GDMLTrap(obj, z, theta, phi, x1, x2, x3, x4, y1, y2, alpha, aunit, lunit, \ + material, colour=None) + ViewProvider(obj.ViewObject) + obj.recompute() + return obj def makeTube(rmin, rmax, z, startphi, deltaphi, aunit, lunit, material, \ colour=None): diff --git a/freecad/gdml/GDMLShared.py b/freecad/gdml/GDMLShared.py index 1e906f18..8e1f523d 100644 --- a/freecad/gdml/GDMLShared.py +++ b/freecad/gdml/GDMLShared.py @@ -1078,21 +1078,37 @@ def setPlacement(obj, xml, invertRotation=True): obj.Placement.Base = FreeCAD.Vector(0, 0, 0) posName = getRef(xml, "positionref") - # TODO deal with unit if posName is not None: row = getPositionRow(posName) if row is not None: + unit_alias = defineSpreadsheet.getAlias(definesColumn['pos_unit'] + str(row)) + default_unit = True + if unit_alias is not None: + unit = defineSpreadsheet.get(unit_alias) + default_unit = False xAlias = defineSpreadsheet.getAlias(definesColumn['pos_x'] + str(row)) if xAlias is not None: - obj.setExpression('.Placement.Base.x', f"<>.{xAlias}") + if default_unit: + expr = f"<>.{xAlias}" + else: + expr = f"<>.{xAlias}*1{unit}" + obj.setExpression('.Placement.Base.x', expr) yAlias = defineSpreadsheet.getAlias(definesColumn['pos_y'] + str(row)) if yAlias is not None: - obj.setExpression('.Placement.Base.y', f"<>.{yAlias}") + if default_unit: + expr = f"<>.{yAlias}" + else: + expr = f"<>.{yAlias}*1{unit}" + obj.setExpression('.Placement.Base.y', expr) zAlias = defineSpreadsheet.getAlias(definesColumn['pos_z'] + str(row)) if zAlias is not None: - obj.setExpression('.Placement.Base.z', f"<>.{zAlias}") + if default_unit: + expr = f"<>.{zAlias}" + else: + expr = f"<>.{zAlias}*1{unit}" + obj.setExpression('.Placement.Base.z', expr) else: pos = xml.find("position") diff --git a/freecad/gdml/Resources/Isotopic_abundances.csv b/freecad/gdml/Resources/Isotopic_abundances.csv new file mode 100644 index 00000000..58c193e6 --- /dev/null +++ b/freecad/gdml/Resources/Isotopic_abundances.csv @@ -0,0 +1,93 @@ +# Z symbol element nuclide_1 atomic_weight_1 atmoic_fraction_1 nuclide_2 atomic_weight_2 atomic_fraction_2 .... + 1 H hydrogen H1 1.00783 0.999885 H2 2.0141 0.000115 + 2 He helium He3 3.01603 0.002 He4 4.0026 0.999 + 3 Li lithium Li6 6.01512 0.0759 Li7 7.016 0.9241 + 4 Be beryllium Be9 9.01218 1 + 5 B boron B10 10.0129 0.199 B11 11.0093 0.801 + 6 C carbon C12 12.0 0.9893 C13 13.0034 0.0107 + 7 N nitrogen N14 14.0031 0.99632 N15 15.0001 0.00368 + 8 O oxygen O16 15.9949 0.9975 O17 16.9991 0.00038 O18 17.9992 0.002 + 9 F fluorine F19 18.9984 1.0 + 10 Ne neon Ne20 19.9924 0.9048 Ne21 20.9938 0.0027 Ne22 21.9914 0.0925 + 11 Na sodium Na23 22.9898 1.0 + 12 Mg magnesium Mg24 23.985 0.7899 Mg25 24.9858 0.1000 Mg26 25.9826 0.1101 + 13 Al aluminium Al27 26.9815 1.0 + 14 Si silicon Si28 27.9769 0.92229 Si29 28.9765 0.047832 Si30 29.9738 0.030872 + 15 P phosphorus P31 30.9738 1.00 + 16 S sulfur S32 31.9721 0.9493 S33 32.9715 0.0076 S34 33.9679 0.0429 S36 35.9671 0.002 + 17 Cl chlorine Cl35 34.9689 0.7578 Cl37 36.9659 0.2422 + 18 Ar argon Ar36 35.9675 0.003365 Ar38 37.9627 0.000632 Ar40 39.9624 0.9960 + 19 K potassium K39 38.9637 0.93258 K40 39.964 0.000117 K41 40.9618 0.067302 + 20 Ca calcium Ca40 39.9626 0.96941 Ca42 41.9586 0.00647 Ca43 42.9588 0.00135 Ca44 43.9555 0.02086 Ca46 45.9537 0.00004 Ca48 47.9525 0.00187 + 21 Sc scandium Sc45 44.9559 1.00 + 22 Ti titanium Ti46 45.9526 0.0825 Ti47 46.9518 0.0744 Ti48 47.9479 0.7372 Ti49 48.9479 0.0541 Ti50 49.9448 0.0518 + 23 V vanadium V50 49.9472 0.0025 V51 50.944 0.9975 + 24 Cr chromium Cr50 49.946 0.04345 Cr52 51.9405 0.83789 Cr53 52.9406 0.09501 Cr54 53.9389 0.02365 + 25 Mn manganese Mn55 54.938 1.00 + 26 Fe iron Fe54 53.9396 0.05845 Fe56 55.9349 0.91754 Fe57 56.9354 0.02119 Fe58 57.9333 0.00282 + 27 Co cobalt Co59 58.9332 1.00 + 28 Ni nickel Ni58 57.9353 0.680769 Ni60 59.9308 0.262231 Ni61 60.9311 0.011399 Ni62 61.9283 0.036345 Ni64 63.928 0.009256 + 29 Cu copper Cu63 62.9296 0.6917 Cu65 64.9278 0.3083 + 30 Zn zinc Zn64 63.9291 0.4863 Zn66 65.926 0.2790 Zn67 66.9271 0.0410 Zn68 67.9248 0.1875 Zn70 69.9253 0.0062 + 31 Ga gallium Ga69 68.9256 0.60108 Ga71 70.9247 0.39892 + 32 Ge germanium Ge70 69.9242 0.2084 Ge72 71.9221 0.2754 Ge73 72.9235 0.0773 Ge74 73.9212 0.3628 Ge76 75.9214 0.0761 + 33 As arsenic As75 74.9216 1.00 + 34 Se selenium Se74 73.9225 0.0089 Se76 75.9192 0.0937 Se77 76.9199 0.0763 Se78 77.9173 0.2377 Se80 79.9165 0.4961 Se82 81.9167 0.0873 + 35 Br bromine Br79 78.9183 0.5069 Br81 80.9163 0.4931 + 36 Kr krypton Kr78 77.9204 0.0035 Kr80 79.9164 0.0228 Kr82 81.9135 0.1158 Kr83 82.9141 0.1149 Kr84 83.9115 0.5700 Kr86 85.9106 0.1730 + 37 Rb rubidium Rb85 84.9118 0.7217 Rb87 86.9092 0.2783 + 38 Sr strontium Sr84 83.9134 0.0056 Sr86 85.9093 0.0986 Sr87 86.9089 0.0700 Sr88 87.9056 0.8258 + 39 Y yttrium Y89 88.9058 1.00 + 40 Zr zirconium Zr90 89.9047 0.5145 Zr91 90.9056 0.1129 Zr92 91.905 0.1715 Zr94 93.9063 0.1738 Zr96 95.9083 0.0280 + 41 Nb niobium Nb93 92.9064 1.00 + 42 Mo molybdenum Mo92 91.9068 0.1484 Mo94 93.9051 0.0925 Mo95 94.9058 0.1592 Mo96 95.9047 0.1668 Mo97 96.906 0.0955 Mo98 97.9054 0.2413 Mo100 99.9075 0.0963 + 43 Tc technetium Tc98 97.9072 1.00 + 44 Ru ruthenium Ru96 95.9076 0.0554 Ru98 97.9053 0.0187 Ru99 98.9059 0.1276 Ru100 99.9042 0.1260 Ru101 100.906 0.1706 Ru102 101.904 0.3155 Ru104 103.905 0.1862 + 45 Rh rhodium Rh103 102.906 1.00 + 46 Pd palladium Pd102 101.906 0.0102 Pd104 103.904 0.1114 Pd105 104.905 0.2233 Pd106 105.903 0.2733 Pd108 107.904 0.2646 Pd110 109.905 0.1172 + 47 Ag silver Ag107 106.905 0.51839 Ag109 108.905 0.48161 + 48 Cd cadmium Cd106 105.906 0.0125 Cd108 107.904 0.0089 Cd110 109.903 0.1249 Cd111 110.904 0.1280 Cd112 111.903 0.2413 Cd113 112.904 0.1222 Cd114 113.903 0.2873 Cd116 115.905 0.0749 + 49 In indium In113 112.904 0.0429 In115 114.904 0.9571 + 50 Sn tin Sn112 111.905 0.0097 Sn114 113.903 0.0066 Sn115 114.903 0.0034 Sn116 115.902 0.1454 Sn117 116.903 0.0768 Sn118 117.902 0.2422 Sn119 118.903 0.0859 Sn120 119.902 0.3258 Sn122 121.903 0.0463 Sn124 123.905 0.0579 + 51 Sb antimony Sb121 120.904 0.5721 Sb123 122.904 0.4279 + 52 Te tellurium Te120 119.904 0.0009 Te122 121.903 0.0255 Te123 122.904 0.0089 Te124 123.903 0.0474 Te125 124.904 0.0707 Te126 125.903 0.1884 Te128 127.904 0.3174 Te130 129.906 0.3408 + 53 I iodine I127 126.904 1.00 + 54 Xe xenon Xe124 123.906 0.00090 Xe126 125.904 0.0009 Xe128 127.904 0.0192 Xe129 128.905 0.2644 Xe130 129.904 0.0408 Xe131 130.905 0.2118 Xe132 131.904 0.2689 Xe134 133.905 0.1044 Xe136 135.907 0.0887 + 55 Cs caesium Cs133 132.905 1.00 + 56 Ba barium Ba130 129.906 0.00106 Ba132 131.905 0.00101 Ba134 133.905 0.0241 Ba135 134.906 0.06592 Ba136 135.905 0.07854 Ba137 136.906 0.11232 Ba138 137.905 0.71698 + 57 La lanthanum La138 137.907 0.00090 La139 138.906 0.99910 + 58 Ce cerium Ce136 135.907 0.00185 Ce138 137.906 0.00251 Ce140 139.905 0.88450 Ce142 141.909 0.11114 + 59 Pr praseodymium Pr141 140.908 1.0 + 60 Nd neodymium Nd142 141.908 0.272 Nd143 142.91 0.122 Nd144 143.91 0.238 Nd145 144.913 0.083 Nd146 145.913 0.172 Nd148 147.917 0.057 Nd150 149.921 0.056 + 61 Pm promethium Pm145 144.913 0.00 Pm146 145.915 0.00 Pm147 146.915 0.00 + 62 Sm samarium Sm144 143.912 0.0307 Sm147 146.915 0.1499 Sm148 147.915 0.1124 Sm149 148.917 0.1382 Sm150 149.917 0.0738 Sm152 151.92 0.2675 Sm154 153.922 0.2275 + 63 Eu europium Eu151 150.92 0.4781 Eu153 152.921 0.5219 + 64 Gd gadolinium Gd152 151.92 0.0020 Gd154 153.921 0.0218 Gd155 154.923 0.1480 Gd156 155.922 0.2047 Gd157 156.924 0.1565 Gd158 157.924 0.2484 Gd160 159.927 0.2186 + 65 Tb terbium Tb159 158.925 1.00 + 66 Dy dysprosium Dy156 155.924 0.0006 Dy158 157.924 0.0010 Dy160 159.925 0.0234 Dy161 160.927 0.1891 Dy162 161.927 0.2551 Dy163 162.929 0.2490 Dy164 163.929 0.2818 + 67 Ho holmium Ho165 164.93 1.00 + 68 Er erbium Er162 161.929 0.0014 Er164 163.929 0.0161 Er166 165.93 0.3361 Er167 166.932 0.2293 Er168 167.932 0.2678 Er170 169.935 0.1493 + 69 Tm thulium Tm169 168.934 1.00 + 70 Yb ytterbium Yb168 167.934 0.0013 Yb170 169.935 0.0304 Yb171 170.936 0.1428 Yb172 171.936 0.2183 Yb173 172.938 0.1613 Yb174 173.939 0.3183 Yb176 175.943 0.1276 + 71 Lu lutetium Lu175 174.941 0.9741 Lu176 175.943 0.0259 + 72 Hf hafnium Hf174 173.94 0.0016 Hf176 175.941 0.0526 Hf177 176.943 0.1860 Hf178 177.944 0.2728 Hf179 178.946 0.1362 Hf180 179.947 0.3508 + 73 Ta tantalum Ta180 179.947 0.00012 Ta181 180.948 0.99988 + 74 W tungsten W180 179.947 0.0012 W182 181.948 0.2650 W183 182.95 0.1431 W184 183.951 0.3064 W186 185.954 0.2843 + 75 Re rhenium Re185 184.953 0.3740 Re187 186.956 0.6260 + 76 Os osmium Os184 183.952 0.0002 Os186 185.954 0.0159 Os187 186.956 0.0196 Os188 187.956 0.1324 Os189 188.958 0.1615 Os190 189.958 0.2626 Os192 191.961 0.4078 + 77 Ir iridium Ir191 190.961 0.373 Ir193 192.963 0.627 + 78 Pt platinum Pt190 189.96 0.00014 Pt192 191.961 0.00782 Pt194 193.963 0.32967 Pt195 194.965 0.33832 Pt196 195.965 0.25242 Pt198 197.968 0.07163 + 79 Au gold Au197 196.967 1.00 + 80 Hg mercury Hg196 195.966 0.0015 Hg198 197.967 0.0997 Hg199 198.968 0.1687 Hg200 199.968 0.2310 Hg201 200.97 0.1318 Hg202 201.971 0.2986 Hg204 203.973 0.0687 + 81 Tl thallium Tl203 202.972 0.29524 Tl205 204.974 0.70476 + 82 Pb lead Pb204 203.973 0.014 Pb205 204.974 0.000 Pb206 205.974 0.241 Pb207 206.976 0.221 Pb208 207.977 0.524 + 83 Bi bismuth Bi209 208.98 1.00 + 84 Po polonium Po209 208.982 0.00 + 85 At astatine At210 210.987 0.00 + 86 Rn radon Rn222 222.018 0.00 + 87 Fr francium Fr223 223.02 0.00 + 88 Ra radium Ra226 226.025 0.00 + 89 Ac actinium Ac227 227.028 0.00 + 90 Th thorium Th228 228.029 0.00 Th229 229.032 0.00 Th230 230.033 0.00 Th232 232.038 1.00 + 91 Pa protactinium Pa231 231.036 1.0 + 92 U uranium U234 234.041 0.000055 U235 235.044 0.0072 U238 238.051 0.992745 \ No newline at end of file diff --git a/freecad/gdml/Resources/ReactorMaterials.xml b/freecad/gdml/Resources/ReactorMaterials.xml new file mode 100644 index 00000000..a6611189 --- /dev/null +++ b/freecad/gdml/Resources/ReactorMaterials.xml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/freecad/gdml/__init__.py b/freecad/gdml/__init__.py index 47070612..da3ae31e 100644 --- a/freecad/gdml/__init__.py +++ b/freecad/gdml/__init__.py @@ -8,3 +8,4 @@ FreeCAD.addExportType("XML (*.xml)","freecad.gdml.exportGDML") FreeCAD.addExportType("GEMC - stl (*.gemc)","freecad.gdml.exportGDML") FreeCAD.addExportType("GEMC - gdml (*.GEMC)","freecad.gdml.exportGDML") +FreeCAD.addExportType("OpenMC (*.xml)","freecad.gdml.exportOpenMC") diff --git a/freecad/gdml/arrayUtils.py b/freecad/gdml/arrayUtils.py index b1feaabb..04a74816 100644 --- a/freecad/gdml/arrayUtils.py +++ b/freecad/gdml/arrayUtils.py @@ -81,10 +81,10 @@ def placementList(array, offsetVector=Vector(0, 0, 0), rot=FreeCAD.Rotation()): elif arrayType == "PointArray": placementList = [] pointObj = array.PointObject - points = pointObj.Links + points = pointObj.Points.Points extraTranslation = array.ExtraPlacement.Base for i, point in enumerate(points): - pos = point.Placement.Base + offsetVector + extraTranslation + pos = point + offsetVector + extraTranslation placementList.append(FreeCAD.Placement(pos, rot)) return placementList diff --git a/freecad/gdml/exportGDML.py b/freecad/gdml/exportGDML.py index 99195450..d693ab18 100644 --- a/freecad/gdml/exportGDML.py +++ b/freecad/gdml/exportGDML.py @@ -975,6 +975,7 @@ def processIsotope( # print(dir(obj)) item.set("formula", str(obj.formula)) + if ( hasattr(obj, "unit") or hasattr(obj, "atom_value") @@ -3144,6 +3145,7 @@ class SolidExporter: "Part::Revolution": "RevolutionExporter", "Part::Box": "BoxExporter", "Part::Cylinder": "CylinderExporter", + "Tube": "TubeExporter", "Part::Cone": "ConeExporter", "Part::Sphere": "SphereExporter", "Part::Cut": "BooleanExporter", @@ -3209,6 +3211,11 @@ def getExporter(obj): elif typeId == "Clone": return CloneExporter(obj) else: + name = obj.Name + if name[:4] == 'Tube': + return TubeExporter(obj) + + # default, fall thru typeId = obj.TypeId else: typeId = obj.TypeId @@ -3488,6 +3495,39 @@ def position(self): return pos +class TubeExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + + def export(self): + if self.exported(): + return + super().export() + + # Needs unique Name + # This is for non GDML cylinder/tube + ET.SubElement( + solids, + "tube", + { + "name": self.name(), + "rmin": str(self.obj.InnerRadius.Value), + "rmax": str(self.obj.OuterRadius.Value), + "deltaphi": str(360), + "aunit": "deg", + "z": str(self.obj.Height.Value), + "lunit": "mm", + }, + ) + self._exportScaled() + + def position(self): + delta = FreeCAD.Vector(0, 0, self.obj.Height.Value / 2) + # see comments in BoxExporter + pos = self.obj.Placement.Base + self.obj.Placement.Rotation * delta + return pos + + class ConeExporter(SolidExporter): def __init__(self, obj): super().__init__(obj) @@ -4521,9 +4561,9 @@ def export(self): extraRotation.Angle = -extraRotation.Angle rot = extraRotation * rotBase pointObj = self.obj.PointObject - points = pointObj.Links + points = pointObj.Points.Points for i, point in enumerate(points): - pos = point.Placement.Base + positionVector + extraTranslation + pos = point + positionVector + extraTranslation nodeName = f"{self.name()}_{i}" nodeXML = ET.SubElement( unionXML, "multiUnionNode", {"name": nodeName} diff --git a/freecad/gdml/exportOpenMC.py b/freecad/gdml/exportOpenMC.py new file mode 100644 index 00000000..d97fbc43 --- /dev/null +++ b/freecad/gdml/exportOpenMC.py @@ -0,0 +1,6460 @@ +from __future__ import annotations +# Mon Aug 26 2024 +# Sat Mar 28 8:44 AM PDT 2023 +# ************************************************************************** +# * * +# * Copyright (c) 2019 Keith Sloan * +# * (c) 2020 Dam Lambert * +# * (c) 2021 Munther Hindi +# * * +# * 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 * +# * * +# * Acknowledgements : Ideas & code copied from * +# * https://github.com/ignamv/geanTipi * +# * * +# *************************************************************************** +__title__ = "FreeCAD - GDML exporter Version" +__author__ = "Keith Sloan " +__url__ = ["https://github.com/KeithSloan/FreeCAD_Geant4"] + +from gettext import translation +from sys import breakpointhook + +import FreeCAD, os, Part, math +import Sketcher +import FreeCAD as App +import FreeCADGui +from PySide import QtGui + +from FreeCAD import Vector +import BOPTools.SplitAPI +from BOPTools import BOPFeatures + + +import random +import re +from .GDMLObjects import GDMLcommon, GDMLBox, GDMLTube + +# modif add +# from .GDMLObjects import getMult, convertionlisteCharToLunit + +import sys +from pathlib import Path + +from .GDMLShared import getMult +from .GmshUtils import meshObj +from .polygonsHelper import inner_outer + +try: + import lxml.etree as ET + + FreeCAD.Console.PrintMessage("running with lxml.etree\n") + XML_IO_VERSION = "lxml" +except ImportError: + try: + import xml.etree.ElementTree as ET + + FreeCAD.Console.PrintMessage("running with xml.etree.ElementTree\n") + XML_IO_VERSION = "xml" + except ImportError: + FreeCAD.Console.PrintMessage("pb xml lib not found\n") + sys.exit() + +# xml handling +# import argparse +# from xml.etree.ElementTree import XML +################################# + +global zOrder +global element_table # dictionary of ElementIsotopes object for an element symbol as key +global nuclide_table # dictionary of Isotope obj for nuclide name as key + +element_table = None + +from .GDMLObjects import ( + GDMLfraction, + GDMLcomposite, + GDMLisotope, + GDMLconstant, + GDMLvariable, + GDMLquantity, +) + +from . import GDMLShared + + +def get_active_branch_name(): + gdml_dir = FreeCAD.getUserAppDataDir() + "/Mod/GDML" + head_dir = Path(gdml_dir) / ".git" / "HEAD" + try: + with head_dir.open("r") as f: content = f.read().splitlines() + except: + return f"Can't locate .git directory in {gdml_dir}" + + for line in content: + if line[0:4] == "ref:": + return line.partition("refs/heads/")[2] + + +# *************************************************************************** +# Tailor following to your requirements ( Should all be strings ) * +# no doubt there will be a problem when they do implement Value +if open.__module__ in ["__builtin__", "io"]: + pythonopen = open # to distinguish python built-in open function from the one declared here + +# ## modifs lambda + + +class NameManager: + _nameCountDict: dict[str, int] = {} + _solidsNamesDict = {} + _volumeNamesDict = {} + + @staticmethod + def init(): + NameManager._nameCountDict = {} + NameManager._solidsNamesDict = {} + NameManager._volumeNamesDict = {} + + @staticmethod + def getName(obj) -> str: + if obj in NameManager._solidsNamesDict: + return NameManager._solidsNamesDict[obj] + name = obj.Label + if len(name) > 4: + if name[0:4] == "GDML": + if "_" in name: + name = name.split("_", 1)[1] + + if name[0].isdigit(): + name = "S" + name + + if name in NameManager._nameCountDict: + count = 1 + NameManager._nameCountDict[name] + NameManager._nameCountDict[name] = count + name = name + str(count) + else: + NameManager._nameCountDict[name] = 0 + + NameManager._solidsNamesDict[obj] = name + + return name + + @staticmethod + def nameUsedFor(obj): + if obj in NameManager._solidsNamesDict: + return NameManager._solidsNamesDict[obj] + else: + return None + + @staticmethod + def getVolumeName(vol) -> str: + if vol in NameManager._volumeNamesDict: + return NameManager._volumeNamesDict[vol] + + if vol.TypeId == "App::Part": + return vol.Label + + elif vol.TypeId == "App::Link": + return NameManager.getVolumeName(vol.LinkedObject) + else: + name = NameManager.nameUsedFor(vol) + if name is None: + name = "LV_"+NameManager.getName(vol) + NameManager._volumeNamesDict[vol] = name + return name + + @staticmethod + def getPhysvolName(vol): + name = NameManager.getName(vol) + return "PV_" + name + +# ## end modifs lambda + +################################# +# Switch functions +################################ + + +class switch(object): + value = None + + def __new__(class_, value): + class_.value = value + return True + + +def case(*args): + return any((arg == switch.value for arg in args)) + + +class MultiPlacer: + def __init__(self, obj): + self.obj = obj + self._name = NameManager.getName(obj) + + def place(self, volRef): + print("Can't place base class MultiPlace") + + def xml(self): + print("Can't place base class MultiPlace") + + def name(self): + return self._name + + @staticmethod + def getPlacer(obj): + if obj.TypeId == "Part::Mirroring": + return MirrorPlacer(obj) + else: + print(f"{obj.Label} is not a placer") + return None + + +class MirrorPlacer(MultiPlacer): + def __init__(self, obj): + super().__init__(obj) + self.assembly = None # defined AFTER place() + + def xml(self): + return self.assembly + + def place(self, volRef): + global structure + name = self.name() + assembly = ET.Element("assembly", {"name": name}) + # structure.insert(0, assembly) + # insert just before worlVol, which should be last + worldIndex = len(structure) - 1 + structure.insert(worldIndex, assembly) + pos = self.obj.Source.Placement.Base + name = volRef + "_mirror" + # bordersurface might need physvol to have a name + physvolName = NameManager.getPhysvolName(self.obj) + pvol = ET.SubElement( + assembly, "physvol", {"name": physvolName} + ) + ET.SubElement(pvol, "volumeref", {"ref": volRef}) + normal = self.obj.Normal + # reflect the position about the reflection plane + unitVec = normal.normalize() + posAlongNormal = pos.dot(unitVec) * unitVec + posParalelToPlane = pos - posAlongNormal + newPos = posParalelToPlane - posAlongNormal + # first reflect about x-axis + # then rotate to bring x-axis to direction of normal + rotX = False + if normal.x == 1: + scl = Vector(-1, 1, 1) + newPos = Vector(-pos.x, pos.y, pos.z) + elif normal.y == 1: + scl = Vector(1, -1, 1) + newPos = Vector(pos.x, -pos.y, pos.z) + elif normal.z == 1: + scl = Vector(1, 1, 1 - 1) + newPos = Vector(pos.x, pos.y, -pos.z) + else: + scl = Vector(-1, 1, 1) + newPos = Vector(-pos.x, pos.y, pos.z) + rotX = True + + rot = FreeCAD.Rotation() + if rotX is True: + # rotation to bring normal to x-axis (might have to reverse) + rot = FreeCAD.Rotation(Vector(1, 0, 0), unitVec) + # The angle of rotation of the image twice the angle of rotation of the mirror + rot.Angle = 2 * rot.Angle + newPos = rot * newPos + sourcePlacement = FreeCAD.Placement(newPos, rot) + # placement = self.obj.Placement*sourcePlacement + placement = sourcePlacement + exportPosition(name, pvol, placement.Base) + if rotX is True: + exportRotation(name, pvol, placement.Rotation) + exportScaling(name, pvol, scl) + + self.assembly = assembly + + +class PhysVolPlacement: + def __init__(self, ref, placement): + self.ref = ref # name reference: a string + self.placement = placement # physvol placement + + +######################################################### +# Pretty format GDML # +######################################################### + + +def indent(elem, level=0): + i = "\n" + level * " " + j = "\n" + (level - 1) * " " + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + " " + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for subelem in elem: + indent(subelem, level + 1) + if not elem.tail or not elem.tail.strip(): + elem.tail = j + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = j + return elem + + +######################################### + +def cleanGDMLname(name): + # Clean GDML name for Geant4 + # Replace space and special characters with '_' + return name.replace('\r','').replace('(','_').replace(')','_').replace(' ','_') + + +def nameFromLabel(label): + if " " not in label: + return label + else: + return label.split(" ")[0] + + +def initGDML(): + NS = "http://www.w3.org/2001/XMLSchema-instance" + location_attribute = "{%s}noNamespaceSchemaLocation" % NS + # For some reason on my system around Sep 30, 2024, the following url is unreachable, + # I think because http:// is no longer accepted, so use https:// instead. DID NOT WORK!, + # although wget of url works. I don't know what's going on + gdml = ET.Element( + "gdml", + attrib={ + location_attribute: "https://service-spi.web.cern.ch/service-spi/app/releases/GDML/schema/gdml.xsd" + }, + ) + + return gdml + + +class Isotope: + def __init__(self, nuclide, atomic_weight, fraction, element): + self.nuclide = nuclide # nuclide, example 'U235' + self.atomic_weight = atomic_weight # float atomic weight (234.99, eg) + self.fraction = fraction # atomic fraction of isotope in its element + self.element = element # the ElementIsotopes this isotope belongs to + + def weight_fraction(self): + return self.fraction * self.atomic_weight/self.element.atomic_weight + + +class ElementIsotopes: + def __init__(self, Z, symbol, element, atomic_weight, isotopes): + self.Z = Z + self.symbol = symbol + self.element = element # element name + self.atomic_weight = atomic_weight # mean atomic weight of element + self.isotopes = isotopes # list[Isotope] + + +def gen_element_table(file_name): + '''read the given file and return a dictionary of ElementIsotopes: dict[element_symbol] = ElementIsotope + ''' + + table = {} + + fd = open(file_name, 'r') + for line in fd.readlines(): + if line[0] == '#': + continue + words = line.strip().split() + Z = int(words[0]) + sym = words[1] + element = words[2].lower() + nuclide_list = words[3:] + if 3 * int(len(nuclide_list) / 3) != len(nuclide_list): + print( + f"*** Nuclide list for element {Z} {element} might be missing something a nuclide or a fraction. Please check file ***") + exit(0) + + num_words = len(nuclide_list) + tot_fraction = 0 + average_atomic_weight = 0 + isotopes_list = [] + elementIsotopes = ElementIsotopes(Z, sym, element, average_atomic_weight, isotopes_list) + for i in range(0, num_words, 3): + nuclide = nuclide_list[i] + atomic_weight = float(nuclide_list[i + 1]) + fraction = float(nuclide_list[i + 2]) + isotope = Isotope(nuclide, atomic_weight, fraction, elementIsotopes) + isotopes_list.append(isotope) + tot_fraction += fraction + average_atomic_weight += atomic_weight + + # sanity check for typos + nuclide_element = nuclide[:len(sym)] + if nuclide_element != sym: + print(f"nuclide {nuclide} does not have the same element name as its element {sym}") + return + # another check, atomic weight should be within 0.1% of mass number + mass_number = int(nuclide[len(sym):]) + if abs(mass_number - atomic_weight) / atomic_weight > 5.0e-3: + print( + f" {nuclide} atomic wieght {atomic_weight} different than mass number by more than 0.5%. Please check") + + average_atomic_weight = average_atomic_weight / len(isotopes_list) + elementIsotopes.atomic_weight = average_atomic_weight + table[sym] = elementIsotopes + # sanity check + if abs(tot_fraction - 1.0) > 2.0e-03 and tot_fraction != 0: + print(f"Warning: {element} sum of fraction is {tot_fraction} differs from 1.0 by at least 0.2%") + + return table + +def gen_nuclide_table(): + global element_table + ''' + put all nuclides is a dictionary dict[nuclide] = Isotope (isotope class containing the nuclide) + ''' + table = {} + for elementSymbol in element_table: + elementObject = element_table[elementSymbol] + isotopes = elementObject.isotopes + for isotope in isotopes: + table[isotope.nuclide] = isotope + + return table + +################################# +# Setup GDML environment +################################# + +def GDMLstructure(): + # print("Setup GDML structure") + ################################# + # globals + ################################ + from .init_gui import joinDir + global gdml, geometry, materials + global WorldVOL + global defineCnt, LVcount, PVcount, POScount, ROTcount, SCLcount + global centerDefined + global identityDefined + global identityName + global gxml + global material_ids + global element_table + global nuclide_table + + + centerDefined = False + identityDefined = False + identityName = 'identity' + + + defineCnt = LVcount = PVcount = POScount = ROTcount = SCLcount = 1 + # gdml = initGDML() + + materials = ET.Element('materials') + geometry = ET.Element('geometry') + material_ids = {} + + if element_table is None: + element_table = gen_element_table(joinDir('Resources/Isotopic_abundances.csv')) + nuclide_table = gen_nuclide_table() + return + + +def defineMaterials(): + # Replaced by loading Default + # print("Define Materials") + global materials + +def defineWorldBox(bbox): + global solids + for obj in FreeCAD.ActiveDocument.Objects: + # print("{} + {} = ".format(bbox, obj.Shape.BoundBox)) + if hasattr(obj, "Shape"): + bbox.add(obj.Shape.BoundBox) + if hasattr(obj, "Mesh"): + bbox.add(obj.Mesh.BoundBox) + if hasattr(obj, "Points"): + bbox.add(obj.Points.BoundBox) + # print(bbox) + # Solids get added to solids section of gdml ( solids is a global ) + name = "WorldBox" + ET.SubElement( + solids, + "box", + { + "name": name, + "x": str(1000), + "y": str(1000), + "z": str(1000), + # 'x': str(2*max(abs(bbox.XMin), abs(bbox.XMax))), \ + # 'y': str(2*max(abs(bbox.YMin), abs(bbox.YMax))), \ + # 'z': str(2*max(abs(bbox.ZMin), abs(bbox.ZMax))), \ + "lunit": "mm", + }, + ) + return name + + +def quaternion2XYZ(rot): + """ + convert a quaternion rotation to a sequence of rotations around X, Y, Z + Here is my (Munther Hindi) derivation: + First, the rotation matrices for rotations around X, Y, Z axes. + + Rx = [ 1 0 0] + [ 0 cos(a) -sin(a)] + [ 0 sin(a) cos(a)] + + Ry= [ cos(b) 0 sin(b)] + [ 0 1 0] + [-sin(b) 0 cos(b)] + + Rz = [ cos(g) -sin(g) 0] + [ sin(g) cos(g) 0] + [ 0 0 1] + + Rederivation from the previous version. Geant processes the rotation from + the gdml as R = Rx Ry Rz, i.e, Rx applied last, not first, so now we have + + R = Rx Ry Rz = + [cosb*cosg, -cosb*sing, sinb], + [cosa*sing+cosg*sina*sinb, cosa*cosg-sina*sinb*sing, -cosb*sina], + [sina*sing-cosa*cosg*sinb, cosa*sinb*sing+cosg*sina, cosa*cosb] + + To get the angles a(lpha), b(eta) for rotations around x, y axes, transform the unit vector (0,0,1) + [x,y,z] = Q*(0,0,1) = R*(0,0,1) ==> + x = sin(b) + y = -sin(a)cos(b) + z = cos(a)cos(b) + + ==> a = atan2(-y, x) = atan2(sin(a)*cos(b), cos(a)*cos(b)) = atan2(sin(a), cos(a)) + then b = atan2(x*cos(a), z) = atan2(sin(b)*cos(a), cos(b)*cos(a)] = atan2(sin(b), cos(b)) + + Once a, b are found, g(amma) can be found by transforming (1, 0, 0), or (0,1,0) + Since now a, b are known, one can form the inverses of Rx, Ry: + Rx^-1 = Rx(-a) + Ry^-1 = Ry(-b) + + Now R*(1,0,0) = Rx*Ry*Rz(1,0,0) = (x, y, z) + multiply both sides by Ry^-1 Rx^-1: + Ry^-1 Rx^-1 Rx Ry Rz (1,0,0) = Rz (1,0,0) = Ry(-b) Rx(-a) (x, y, z) = (xp, yp, zp) + ==> + xp = cos(g) + yp = sin(g) + zp = 0 + + and g = atan2(yp, zp) + """ + v = rot * Vector(0, 0, 1) + print(v) + # solution 1. + if v.x > 1.0: + v.x = 1.0 + if v.x < -1.0: + v.x = -1.0 + b = math.asin(v.x) + if math.cos(b) > 0: + a = math.atan2(-v.y, v.z) + else: + a = math.atan2(v.y, -v.z) + # sanity check 1 + ysolution = -math.sin(a) * math.cos(b) + zsolution = math.cos(a) * math.cos(b) + if v.y * ysolution < 0 or v.z * zsolution < 0: + print("Trying second solution") + b = math.pi - b + if math.cos(b) > 0: + a = math.atan2(-v.y, v.z) + else: + a = math.atan2(v.y, -v.z) + # sanity check 2 + ysolution = -math.sin(a) * math.cos(b) + zsolution = math.cos(a) * math.cos(b) + if v.y * ysolution < 0 or v.z * zsolution < 0: + print("Failed both solutions!") + print(v.y, ysolution) + print(v.z, zsolution) + Ryinv = FreeCAD.Rotation(Vector(0, 1, 0), math.degrees(-b)) + Rxinv = FreeCAD.Rotation(Vector(1, 0, 0), math.degrees(-a)) + vp = Ryinv * Rxinv * rot * Vector(1, 0, 0) + g = math.atan2(vp.y, vp.x) + + return [math.degrees(a), math.degrees(b), math.degrees(g)] + + +def exportPosition(name, xml, pos): + global POScount + global centerDefined + GDMLShared.trace("export Position") + GDMLShared.trace(pos) + x = pos[0] + y = pos[1] + z = pos[2] + + posType, posName = GDMLShared.getPositionName(name) + print(f"exportPosition: name {name} posType {posType} posName {posName}") + print(f"x {x}") + print(f"y {y}") + print(f"z {z}") + + if posType is None: # The part is not in the gdmlInfo spread spreadsheet + if x == 0 and y == 0 and z == 0: + if not centerDefined: + centerDefined = True + ET.SubElement( + define, + "position", + {"name": "center", "x": "0", "y": "0", "z": "0", "unit": "mm"}, + ) + ET.SubElement(xml, "positionref", {"ref": "center"}) + return + else: + # just export insitu position + posName = "P-" + name + str(POScount) + POScount += 1 + posxml = ET.SubElement(xml, "position", {"name": posName, "unit": "mm"}) + + else: # the object exists in the gdmlInfo sheet + if posType == "positionref": # it necessarily has a posName + # does it already exist in the define section + posxml = define.find("position[@name='%s']" % posName) + if posxml is not None: + ET.SubElement(xml, "positionref", {"ref": posName}) + return + else: + print(f"name {name} exists in the gdmlInfo sheet, but not in the define sheet") + print(f"Should not happen, but we'll create {posName} in the define section") + posxml = ET.SubElement(define, "position", {"name": str(posName), "unit": "mm"}) + + else: # insitu position. It may not necessarily have a name + posxml = ET.SubElement(xml, "position", {"unit": "mm"}) + if posName is not None: + posxml.attrib["name"] = posName + + if x != 0: + posxml.attrib["x"] = str(x) + if y != 0: + posxml.attrib["y"] = str(y) + if z != 0: + posxml.attrib["z"] = str(z) + + +def exportRotation(name, xml, rot, invertRotation=True): + print("Export Rotation") + global ROTcount + global identityDefined + global identityName + + angles = quaternion2XYZ(rot) + a0 = angles[0] + a1 = angles[1] + a2 = angles[2] + if invertRotation: + a0 = -a0 + a1 = -a1 + a2 = -a2 + + rotType, rotName = GDMLShared.getRotationName(name) + if rotType is None: # The part is not in the gdmlInfo spread spreadsheet + if rot.Angle == 0: + if not identityDefined: + identityDefined = True + rotxml = define.find("rotation[@name='%s']" % identityName) + if rotxml == None: + ET.SubElement( + define, + "rotation", + {"name": identityName, "x": "0", "y": "0", "z": "0"}, + ) + + ET.SubElement(xml, "rotationref", {"ref": identityName}) + return + + else: + # just export insitu rotation (NOT a rotationref) + rotName = "R-" + name + str(ROTcount) + ROTcount += 1 + rotxml = ET.SubElement(xml, "rotation", {"name": rotName, "unit": "deg"}) + + else: # the object exists in the gdmlInfo sheet + if rotType == "rotationref": # it necessarily has a rotName + # does it already exist in the define section + rotxml = define.find("rotation[@name='%s']" % rotName) + if rotxml is not None: + ET.SubElement(xml, "rotationref", {"ref": rotName}) + return + else: + print(f"name {name} exists in the gdmlInfo sheet, but not in the define sheet") + print(f"Should not happen, but we'll create {rotName} in the define section") + rotxml = ET.SubElement(define, "rotation", {"name": str(rotName), "unit": "degree"}) + + else: # insitu rotation. It may not necessarily have a name + rotxml = ET.SubElement(xml, "rotation", {"unit": "degree"}) + if rotName is not None: + rotxml.attrib["name"] = rotName + + if abs(a0) != 0: + rotxml.attrib["x"] = str(a0) + if abs(a1) != 0: + rotxml.attrib["y"] = str(a1) + if abs(a2) != 0: + rotxml.attrib["z"] = str(a2) + + return + + +def exportScaling(name, xml, scl): + global SCLcount + global centerDefined + GDMLShared.trace("export Scaling") + GDMLShared.trace(scl) + x = scl[0] + y = scl[1] + z = scl[2] + sclName = "S-" + name + str(SCLcount) + SCLcount += 1 + ET.SubElement( + define, + "scale", + {"name": sclName, "x": str(x), "y": str(y), "z": str(z)}, + ) + ET.SubElement(xml, "scaleref", {"ref": sclName}) + + +def processPlacement(name, xml, placement): + exportPosition(name, xml, placement.Base) + exportRotation(name, xml, placement.Rotation) + +#-------------------------- OpenMC Materials exporting code ----------------------------------- +import re + + +def elementSymbol(elem): + pattern = r'^([A-Z][a-z]?)$' # Element symbol, say C, or Cd + match = re.match(pattern, elem) + if match: + sym = match.group(1) + return sym + + pattern = r'^([A-Z][a-z]?)_element$' # Element of the form Cd_element or C_element + + match = re.match(pattern, elem) + if match: + sym = match.group(1) + return sym + + pattern = r'^G4_([A-Z][a-z]?$)' # Element Ga_sym + match = re.match(pattern, elem) + if match: + sym = match.group(1) + return sym + + # Try element defined in Elements Group + elem_grp = elementGroup(elem) + if elem_grp is not None: + sym = elem_grp.formula + return sym + + return None + + +def elementGroup(elem): + elementsGroup = FreeCAD.ActiveDocument.getObject('Elements') + for grp in elementsGroup.Group: + if grp.Label == elem: + return grp + + return None + + +def materialGroup(mat): + materialGroup = FreeCAD.ActiveDocument.getObject('Materials') + for grp in materialGroup.Group: + if grp.Label == mat: + return grp + + return None + + +def merge_lists(list1, list2): + ''' merge the two lists of dictionaries + Say list1 is [{'nuclide': A, 'fraction': fA1, 'type': typeA}, {'nuclide': B,'fraction': fB1, 'type': typeB},...] + and list2 is + [{nuclide: C,'fraction': fC, 'type': typeC}, {nuclide: A,'fraction': fA2, 'type': typeA2},...] + + then we eant to return a list + [{'nuclide': A, 'fraction': fA+fA2, 'type': typeA}, {'nuclide': 'B', ....}, {'nuclide': C, ...}] + ''' + + both_lists = list1 + list2 + merged_dict = {} + for nuc_dict in both_lists: + nuclide = nuc_dict['nuclide'] + frac = nuc_dict['fraction'] + typ = nuc_dict['type'] + if nuclide in merged_dict: + frac += merged_dict[nuclide][0] + else: + merged_dict[nuclide] = (frac, nuc_dict['type']) + + combined_list = [] + for nuclide in merged_dict: + combined_dict = {'nuclide': nuclide, 'fraction': merged_dict[nuclide][0], 'type': merged_dict[nuclide][1]} + combined_list.append(combined_dict) + + return combined_list + + +def material_nuclides(mat): + ''' + return a list of + {'nuclide': nuclide(str), 'fraction': fraction, 'type': (ao for atomic fraction | wo for weioght fraction) + + for the material + + :param: mat: name of material (a string) + ''' + global element_table + global nuclide_table + + nuclide_list = [] # a list of {'nuclide': isotope, 'fraction', fraction, 'type': 'ao' or 'wo'} + + # A single natural element + sym = elementSymbol(mat) + if sym is not None: + elementIsotopes = element_table[sym] + isotopes = elementIsotopes.isotopes + for isotope in isotopes: + nuclide_list.append({'nuclide': isotope.nuclide, 'fraction': isotope.fraction, 'type': 'ao'}) + return nuclide_list + + print(mat) + mat_obj = FreeCAD.ActiveDocument.getObject(mat) + if mat_obj is None: + # perhaps the minus in the name. clean the name + mat = mat.replace('-', '_') + mat_obj = FreeCAD.ActiveDocument.getObject(mat) + print(f"Cannot find object with name {mat}") + + print(mat_obj.Label) + + # A Chemical formula + if hasattr(mat_obj, 'formula'): + for grp in mat_obj.Group: + element_count = grp.n + # print(element_count) + elementName = grp.Label[:grp.Label.find(' :')] + sym = elementSymbol(elementName) + if sym not in element_table: + print(f" formula {mat_obj.Label} has a component {grp.Label} with unknown element {elementName}") + continue + + elementIsotopes = element_table[sym] + isotopes = elementIsotopes.isotopes + for isotope in isotopes: + # print(nucleus) + nuc_dict = {} + nuc_dict['nuclide'] = isotope.nuclide + nuc_dict['fraction'] = element_count * isotope.fraction + nuc_dict['type'] = 'ao' + nuclide_list.append(nuc_dict) + return nuclide_list + + # A mixture + print(f"I think {mat} is a mixture") + print(mat_obj.Group) + # in a mixture the fractions are weight fractions + for grp in mat_obj.Group: + print(grp.Label) + mat_or_element = grp.Label[:grp.Label.find(' :')] + weight_fraction = float(grp.Label[1+grp.Label.find(':'):]) + print(mat_or_element) + sym = elementSymbol(mat_or_element) + if sym is not None: # The mixture invloves a natural element + component_list = material_nuclides(mat_or_element) + for component in component_list: + isotopeName = component['nuclide'] + isotopeObject = nuclide_table[isotopeName] + # for a natural element, the isotope fractions are atomic fractions + # for mixtures (of elements) the fractions should be weight fractions + # The components returned above are for an element an so need to convert to weight_fractions + component['fraction'] *= weight_fraction * isotopeObject.atomic_weight/isotopeObject.element.atomic_weight + nuclide_list = merge_lists(nuclide_list, component_list) + continue + + print(f"checking if {grp.Label} is defined in the Elements Group") + elem_grp = elementGroup(mat_or_element) + if elem_grp is not None: + for isotope in elem_grp.Group: + print(isotope.Label) + index = isotope.Label.find(' :') + print(index) + isotope_name = isotope.Label[:isotope.Label.find(' :')] + isotope_fraction = isotope.n + nuc_dict = {} + nuc_dict['nuclide'] = isotope_name + # in an element definition the isotope fractions are ATOMIC fractions, + # but here we are processing mixtures, the fractions should be WEIGHT fractions + # so we need to convert + isotopeObject = nuclide_table[isotope_name] + nuc_dict['fraction'] = isotope_fraction * isotopeObject.atomic_weight/isotopeObject.element.atomic_weight + nuc_dict['type'] = 'wo' + nuclide_list.append(nuc_dict) + continue + + # TODO: Should Check if the component is actually a Single isotope + + print(f"checking if {grp.Label} is defined in the Materials Group") + mat_grp = materialGroup(mat_or_element) + if mat_grp is not None: + fraction = float(grp.Label[grp.Label.find(':') + 1:]) + component_list = material_nuclides(mat_or_element) + if component_list is None: + print(f"material component {mat_or_element} is not a material and not an element") + continue + # multiply the fractions in the returned list by the fraction of the material in the mixture + for component in component_list: + component['fraction'] *= fraction + # a material fraction in a mixture is a weight fraction + # the component might be an element and hence the resulting component might atomic fraction + # we check and compensate here + if component['type'] == 'ao': + nuclideName = component['nuclide'] + nuclideObject = nuclide_table[nuclideName] + component['fraction'] *= nuclideObject.atomic_weight/nuclideObject.element.atomic_weight + # update the current nuclide_list with that of the current material + nuclide_list = merge_lists(nuclide_list, component_list) + + else: + print(f" material {mat} has a component {grp.Label} that is not in Group Elements or in group Materials") + + return nuclide_list + + +def processMaterials(): + print("\nProcess Materials") + global materials + + for GName in [ + # "Define", + # "Isotopes", + # "Elements", + "Materials", + ]: + Grp = FreeCAD.ActiveDocument.getObject(GName) + if Grp is not None: + # print(Grp.TypeId+" : "+Grp.Label) + print(Grp.Label) + if processGroup(Grp) is False: + break + +def createMaterials(group): + global materials + global material_ids + + density_multipliers = { + 'g/cm3': 1, + 'g/cc': 1, + 'mg/cm3': 0.001 + } + + preprocessMaterialIds() + for mat in material_ids: + + print(f"processing material {mat}") + id = material_ids[mat] + + item = ET.SubElement( + materials, "material", {"name": str(mat) } + ) + + item.attrib['id'] = str(id) + + # property must be first + # openmc does not use properties + ''' + for prop in obj.PropertiesList: + if obj.getGroupOfProperty(prop) == "Properties": + ET.SubElement( + item, + "property", + {"name": prop, "ref": getattr(obj, prop)}, + ) + ''' + + + # hyphens are wreaking havoc in getting properties from labels. + # so replace them with underscores + mat = mat.replace('-', '_') + obj = FreeCAD.ActiveDocument.getObject(mat) + + if hasattr(obj, "Tvalue"): + temperature = float(obj.Tvalue) + if hasattr(obj, "Tunit"): + if obj.Tunit == 'Celsius' or obj.Tunit == 'C' or obj.Tunit == 'Centigrade': + temperature += 273.15 + elif obj.Tunit == 'Fahrenheit' or obj.Tunit == 'F': # ich!! nobody should do that + temperature = 273.15 + (temperature - 32)/9*5 + item['temperature'] = str(temperature) + + + if hasattr(obj, "Dunit") or hasattr(obj, "Dvalue"): + # print("Dunit or DValue") + D = ET.SubElement(item, "density") + unit_multiplier = 1 + if hasattr(obj, "Dunit"): + if obj.Dunit in density_multipliers: + unit_multiplier = density_multipliers[obj.Dunit] + D.set("units", 'g/cm3') + else: + D.set("units", str(obj.Dunit)) + else: # default to g/cm3, if no Dunit is given + D.set("units", 'g/cm3') + if hasattr(obj, "Dvalue"): + D.set("value", str(obj.Dvalue*unit_multiplier)) + + # process common options material / element + nuclides_in_material = material_nuclides(mat) + for nuclide in nuclides_in_material: + xml_item = ET.SubElement(item, 'nuclide') + + nuclide_name = nuclide['nuclide'] + nuclide_fraction = nuclide['fraction'] + fraction_type = nuclide['type'] + + xml_item.attrib['name'] = str(nuclide_name) + xml_item.attrib[fraction_type] = str(nuclide_fraction) + + +def createElement(elementLabel, item): + global materials + + elementsGroup = FreeCAD.ActiveDocument.getObject('Elements') + for grp in elementsGroup.Group: + if grp.Label == elementLabel: + for nuclideGrp in grp.Group: + nuclide_name = nuclideGrp.Label[:nuclideGrp.Label.find(' :')] + fraction = nuclideGrp.n + xml_item = ET.SubElement(item, 'nuclide') + xml_item.attrib['name'] = str(nuclide_name) + xml_item.attrib['ao'] = str(fraction) + +# -------------------------- End process OpenMC materials ------------------------ + +def processGroup(obj): + print("Process Group " + obj.Label) + # print(obj.TypeId) + # print(obj.Group) + # if hasattr(obj,'Group') : + # return + if hasattr(obj, "Group"): + # print(" Object List : "+obj.Label) + # print(obj) + while switch(obj.Label): + if case("Define"): + # print("Constants") + # skip defines for openMC. + # createDefine(obj.Group) + break + + if case("Quantities"): + # print("Quantities") + # skip quantities + # createQuantities(obj.Group) + break + + if case("Isotopes"): + # print("Isotopes") + # skip Isotopes + # createIsotopes(obj.Group) + break + + if case("Elements"): + # print("Elements") + # createElements(obj.Group) + break + + if case("Materials"): + print("Materials") + createMaterials(obj.Group) + break + + if case("Geant4"): + # Do not export predefine in Geant4 + print("Geant4") + break + + +from itertools import islice + + +def consume(iterator): + next(islice(iterator, 2, 2), None) + + +def getDefaultMaterial(): + # should get this from GDML settings under "settings" + # for now this does not exist, so simply put steel + return "G4_STAINLESS-STEEL" + + +def getMaterial(obj): + # Temporary fix until the SetMaterials works + # Somehow (now Feb 20) if a new gdml object is added + # the default material is Geant4, and SetMaterials fails to change it + from .GDMLMaterials import getMaterialsList + global usedGeant4Materials + + GDMLShared.trace("get Material : " + obj.Label) + if hasattr(obj, "material"): + material = obj.material + elif hasattr(obj, "Tool"): + GDMLShared.trace("Has tool - check Base") + try: + material = getMaterial(obj.Base) + except Exception as e: + print(e) + print("Using default material") + material = getDefaultMaterial() + + elif hasattr(obj, "Base"): + GDMLShared.trace("Has Base - check Base") + try: + material = getMaterial(obj.Base) + except Exception as e: + print(e) + print("Using default material") + material = getDefaultMaterial() + + elif hasattr(obj, "Objects"): + GDMLShared.trace("Has Objects - check Objects") + try: + material = getMaterial(obj.Objects[0]) + except Exception as e: + print(e) + print("Using default material") + material = getDefaultMaterial() + + else: + material = getDefaultMaterial() + + if material[0:3] == "G4_": + print(f"Found Geant material {material}") + usedGeant4Materials.add(material) + + return material + + +""" +def printObjectInfo(xmlVol, volName, xmlParent, parentName): + print("Process Object : "+obj.Label+' Type '+obj.TypeId) + if xmlVol is not None : + xmlstr = ET.tostring(xmlVol) + else : + xmlstr = 'None' + print('Volume : '+volName+' : '+str(xmlstr)) + if xmlParent is not None : + xmlstr = ET.tostring(xmlParent) + else : + xmlstr = 'None' + print('Parent : '+str(parentName)+' : '+str(xmlstr)) +""" + + +def invPlacement(placement): + inv = placement.inverse() + return inv + # tra = inv.Base + # rot = inv.Rotation + # T = FreeCAD.Placement(tra, FreeCAD.Rotation()) + # R = FreeCAD.Placement(FreeCAD.Vector(), rot) + # return R*T + + +def isArrayType(obj): + obj1 = obj + if obj.TypeId == "App::Link": + obj1 = obj.LinkedObject + if obj1.TypeId == "Part::FeaturePython": + if not hasattr(obj1.Proxy, 'Type'): + return False # discovered that InvoluteGears don't have a 'Type' + typeId = obj1.Proxy.Type + if typeId == "Array": + if obj1.ArrayType == "ortho": + return True + elif obj1.ArrayType == "polar": + return True + elif typeId == "PathArray": + return True + elif typeId == "PointArray": + return True + elif typeId == "Clone": + clonedObj = obj1.Objects[0] + return isArrayType(clonedObj) + + else: + return False + else: + return False + + +def isArrayOfPart(obj): + ''' test if the obj (an array) is an array of App::Part ''' + obj1 = obj + if obj.TypeId == "App::Link": + obj1 = obj.LinkedObject + typeId = obj1.Proxy.Type + if typeId == "Clone": + clonedObj = obj1.Objects[0] + return isArrayOfPart(clonedObj) + else: + return obj1.Base.TypeId == "App::Part" + +def processArrayPart(array): + # array: array object + global physVolStack + global universe_dict + from . import arrayUtils + # array: Array item + # new approach 2024-08-07: + # Create an assembly for the array. The assembly has the name of the array Part + # place the repeated array elements in this new assembly and place + # the array assembly in the xmlVol with the position and rotation of the array + # xmlVol: xml item into which the array elements are placed/exported + + # arrayRef = NameManager.getName(array) + base = array.Base + baseName = NameManager.getName(array.Base) + # create a cell with id = universe_dict[baseName] + # baseUniverseId = universe_dict[baseName] + # basePhysVol = physVolStack.pop() + # baseRotation = basePhysVol.placement.Rotation + # baseTranslation = basePhysVol.placement.Base + + arrayType = arrayUtils.typeOfArray(array) + saved_placement = FreeCAD.Placement(base.Placement.Base, base.Placement.Rotation) + doc = FreeCAD.ActiveDocument + while switch(arrayType): + if case("ortho"): + placements = arrayUtils.placementList(array) + print(f'Number of placements = {len(placements)}') + for i, placement in enumerate(placements): + base.Placement = placement + doc.recompute() + geomExporter = GeomObjExporter.getExporter(base) + geomExporter.export() + break + + if case("polar"): + placements = arrayUtils.placementList(array) + print(f'Number of placements = {len(placements)}') + for i, placement in enumerate(placements): + base.Placement = placement + doc.recompute() + geomExporter = GeomObjExporter.getExporter(base) + geomExporter.export() + break + + if case("PathArray") or case("PointArray"): + placements = arrayUtils.placementList(array) + for i, placement in enumerate(placements): + base.Placement = placement + doc.recompute() + geomExporter = GeomObjExporter.getExporter(base) + geomExporter.export() + break + + base.Placement = saved_placement + doc.recompute() + +def printVolumeInfo(vol, xmlVol, xmlParent, parentName): + if xmlVol is not None: + xmlstr = ET.tostring(xmlVol) + else: + xmlstr = "None" + print(xmlstr) + GDMLShared.trace(" " + vol.Label + " - " + str(xmlstr)) + if xmlParent is not None: + xmlstr = ET.tostring(xmlParent) + else: + xmlstr = "None" + GDMLShared.trace(" Parent : " + str(parentName) + " : " + str(xmlstr)) + + +def createWorldVol(volName): + print("Need to create Dummy Volume and World Box ") + bbox = FreeCAD.BoundBox() + boxName = defineWorldBox(bbox) + worldVol = ET.SubElement(structure, "volume", {"name": volName}) + ET.SubElement(worldVol, "materialref", {"ref": "G4_AIR"}) + ET.SubElement(worldVol, "solidref", {"ref": boxName}) + ET.SubElement(gxml, "volume", {"name": volName, "material": "G4_AIR"}) + return worldVol + +def buildDocTree(): + from PySide import QtWidgets + + global obj_top_children_dict + obj_top_children_dict = {} # dictionary of list of child objects for each object + # TypeIds that should not go in to the tree + skippedTypes = ["App::Origin", "Sketcher::SketchObject", "Part::Compound"] + + def addDaughters(item: QtWidgets.QTreeWidgetItem): + print (f"--------addDaughters {item.text(0)}") + objectLabel = item.text(0) + object = App.ActiveDocument.getObjectsByLabel(objectLabel)[0] + if object not in obj_top_children_dict: + obj_top_children_dict[object] = [] + for i in range(item.childCount()): + childItem = item.child(i) + treeLabel = childItem.text(0) + try: + childObject = App.ActiveDocument.getObjectsByLabel(treeLabel)[0] + objType = childObject.TypeId + if objType not in skippedTypes: + obj_top_children_dict[object].append(childObject) + addDaughters(childItem) + except Exception as e: + print(e) + return + + # Get world volume from document tree widget + worldObj = FreeCADGui.Selection.getSelection()[0] + # tree = FreeCADGui.getMainWindow().findChildren(QtGui.QTreeWidget)[0] + # it = QtGui.QTreeWidgetItemIterator(tree) + + mw1 = FreeCADGui.getMainWindow() + print (f"---------Number of trees {len(mw1.findChildren(QtGui.QTreeWidget))}") + treesSel = mw1.findChildren(QtGui.QTreeWidget) + print (f"---------Number of trees {len(treesSel)}") + + doc = FreeCAD.ActiveDocument + found = False + + for tree in treesSel: + print(f"--------Tree {tree.objectName()}") + items = tree.selectedItems() + for item in items: + treeLabel = item.text(0) + print(f"--------Item {treeLabel}") + print(f"--------Doc.Label {doc.Label}") + # if not found: + # if treeLabel != doc.Label: + # continue + # found = True + try: + objs = doc.getObjectsByLabel(treeLabel) + print(f"--------Objects {objs}") + if len(objs) == 0: + continue + + obj = objs[0] + if obj == worldObj: + print(f"--------World Object {obj.Label}") + # we presume first app part is world volume + addDaughters(item) + break + except Exception as e: + print(e) + FreeCADobject = None + + +def isContainer(obj): + # return True if The App::Part is of the form: + # App::Part + # -solid (Part:xxx) + # -App::Part + # -App::Part + # .... + # So a container satisfies the current isAssembly requirements + # plus the siblings must have the above form + # obj that satisfy isContainer get exported as + # + # + # + # + # + # This is in contrast to assembly, which is exported as + # + # + # .... + # + # Must be assembly first + global obj_top_children_dict + + if not isAssembly(obj): + return False + heads = assemblyHeads(obj) + if heads is None: + return False + if len(heads) < 2: + return False + + # first must be solid + if not SolidExporter.isSolid(heads[0]): + return False + + # we cannot have an array as the containing solid of a container + if isArrayType(heads[0]): + return False + + # rest must not be solids, but only second is tested here + if SolidExporter.isSolid(heads[1]): + return False + + return True + + +# is the object something we export as a Volume, or Assembly or a solid? +def isGeometryExport(obj): + return obj.TypeId == "App::Part" or SolidExporter.isSolid(obj) + + +def isAssembly(obj): + # return True if obj is an assembly. + # To be an assembly the obj must be: + # (1) an App::Part or an App::Link and + # (2) it has either (1) At least one App::Part as a subpart or + # (2) more than one "terminal" object + # A terminal object is one that has associated with it ONE volume + # A volume refers to ONE solid + # A terminal item CAN be a boolean, or an extrusion (and in the future + # a chamfer or a fillet. So a terminal element need NOT have an empty + # child list + # N.B. App::Link is treated as a non-assembly, even though it might be linked + # to an assembly, because all we need to place it is the volref of its link + + global obj_top_children_dict + + print(f"testing isAsembly for: {obj.Label}") + if obj.TypeId != "App::Part": + return False + + for ob in obj_top_children_dict[obj]: + if ob.TypeId == "App::Part" or ob.TypeId == "App::Link": + print(True) + return True # Yes, even if ONE App::Part is under this, we treat it as an assembly + + if len(obj_top_children_dict[obj]) > 1: + print("Yes, it is an Assembly") + return True + else: + # need to check for arrays. Arrays of App::Part are treated as an assembly + if len(obj_top_children_dict[obj]) == 1: + topObject = obj_top_children_dict[obj][0] + if isArrayType(topObject) and isArrayOfPart(topObject): + return True + else: + return False + else: + return False + + +def assemblyHeads(obj): + # return a list of subassembly heads for this object + # Subassembly heads are themselves either assemblies + # or terminal objects (those that produce a 1: # More than one GDML Object need to insert Dummy + return False + if ( + gCount == 1 and len(obj.OutList) == 2 + ): # Just a single GDML obj insert Dummy + return False + return True + + +def exportWorldVol(vol, fileExt): + global obj_top_children_dict + global WorldVOL + + WorldVOL = vol.Label + if fileExt != ".xml": + print("Export World Process Volume : " + vol.Label) + GDMLShared.trace("Export Word Process Volume" + vol.Label) + + if checkGDMLstructure(vol) is False: + GDMLShared.trace("Insert Dummy Volume") + # createXMLvolume("dummy") + xmlParent = createWorldVol(vol.Label) + parentName = vol.Label + else: + GDMLShared.trace("Valid Structure") + xmlParent = None + parentName = None + else: + xmlParent = None + parentName = None + + # print(vol.OutList) + vCount, lCount, gCount = countGDMLObj(vol) + print(f"Root GDML Counts {vCount} {gCount}") + + # Munther Please check/correct + # if gCount > 0: # one GDML defining world volume + # if isAssembly(vol): + # heads = assemblyHeads(vol) + # worlSolid = heads[0] + # xmlVol = processVolume(worlSolid, xmlParent, volName=WorldVOL) + # for obj in heads[1:]: # skip first volume (done above) + # processVolAssem(obj, xmlVol, WorldVOL) + # else: + # xmlVol = processVolume(vol, xmlParent) + # else: # no volume defining world + # xmlVol = createXMLassembly(vol.Label) + # processAssembly(vol, xmlVol, xmlParent, parentName) + + # The world volume does not have a parent + + processDocTree(1) + +def exportGDML(first, filepath, fileExt): + from . import GDMLShared + from sys import platform + + global zOrder + + global usedGeant4Materials + usedGeant4Materials = set() + + global gdml + global universe_dict + + + universe_dict = {} + universe_dict['worlVOL'] = 1 # world volume = root volume = 1 + SurfaceExporter.reset_ids() + + # GDMLShared.setTrace(True) + GDMLShared.trace("exportGDML") + + print("====> Start OpenMC Export 0.1") + branch = get_active_branch_name() + print(f"branch: {branch}") + print("File extension : " + fileExt) + + GDMLstructure() + zOrder = 1 + # TODO: process and export materials + processMaterials() + exportWorldVol(first, fileExt) + + params = FreeCAD.ParamGet( + "User parameter:BaseApp/Preferences/Mod/GDML" + ) + exportG4Materials = params.GetBool('exportG4Materials', False) + + # format & write GDML file + # xmlstr = ET.tostring(structure) + # print('Structure : '+str(xmlstr)) + if fileExt == ".xml": + # indent(gdml) + print(len(list(geometry))) + print("Write to xml file") + # ET.ElementTree(gdml).write(filepath, 'utf-8', True) + # ET.ElementTree(gdml).write(filepath, xml_declaration=True) + # Problem with pretty Print on Windows ? + combined_file = filepath + geom_file_path = filepath[:-4] + '_geometry.xml' + materials_file_path = filepath[:-4] + '_materials.xml' + openmc = ET.Element('openmc') + openmc.append(materials) + openmc.append(geometry) + if platform == "win32": + indent(geometry) + indent(materials) + ET.ElementTree(materials).write(materials_file_path, xml_declaration=True, encoding='UTF-8') + ET.ElementTree(geometry).write(geom_file_path, xml_declaration=True, encoding='UTF-8') + ET.ElementTree(openmc).write(filepath, xml_declaration=True, encoding='UTF-8') + else: + ET.ElementTree(materials).write(materials_file_path, pretty_print=True, xml_declaration=True, encoding='UTF-8') + ET.ElementTree(geometry).write(geom_file_path, pretty_print=True, xml_declaration=True, encoding='UTF-8') + ET.ElementTree(openmc).write(filepath, pretty_print=True, xml_declaration=True, encoding='UTF-8') + print("OpenMC xml files written") + + +def preprocessMaterialIds(): + + global obj_top_children_dict + global material_ids + + material_ids = {} + + for obj in obj_top_children_dict: + if len(obj_top_children_dict[obj]) == 0: + continue # this item will get exported as a child of some other item + if isContainer(obj) or isAssembly(obj): + pass + + elif SolidExporter.isSolid(obj): + solidExporter = SolidExporter.getExporter(obj) + + if solidExporter is None: + pass + else: + material_name = getMaterial(obj) + if material_name not in material_ids: + id = len(material_ids) + 1 + material_ids[material_name] = id + + for child in obj_top_children_dict[obj]: + if isContainer(child) or isAssembly(child): + pass + elif SolidExporter.isSolid(child): + material_name = getMaterial(child) + if material_name not in material_ids: + id = len(material_ids) + 1 + material_ids[material_name] = id + + print(f" Material ids: {material_ids}") + +#------------------------------ Geometry Objects exporters ------------------------- + +from abc import ABC, abstractmethod +class GeomObjExporter(ABC): + global obj_top_children_dict + + def __init__(self, obj): + self.obj = obj + + @staticmethod + def getExporter(obj): + if isContainer(obj): + return ContainerExporter(obj) + elif isAssembly(obj): + return AssemblyExporter(obj) + elif SolidExporter.isSolid(obj): + return SingleSolidExporter(obj) + else: + childObjects = obj_top_children_dict[obj] + if len(childObjects) == 1: + return SingleVolumeExporter(obj) + else: + print(f"Can't find a GeomObjExporter for {obj.Label}") + return + + @abstractmethod + def getRegion(self): + ''' get region surrounding this object ''' + + @abstractmethod + def export(self): + ''' export all surfaces needed and a cell with a material and region for the object ''' + + +class ContainerExporter(GeomObjExporter): + def __init__(self, obj): + super().__init__(obj) + firstChild = obj_top_children_dict[obj][0] + + if not SolidExporter.isSolid(firstChild): + print(f" Expect a solid for first child of {obj.Label}, but {firstChild.Label} is not") + return + + self.containerSolidExporter = SolidExporter.getExporter(firstChild) + self.children_geom_exporters = [] + for child in obj_top_children_dict[obj][1:]: + geomExporter = GeomObjExporter.getExporter(child) + if geomExporter is None: + print(f" No GeomObjectExporter for {child.Label}. Skipping") + continue + self.children_geom_exporters.append(geomExporter) + + def getRegion(self): + ''' Note this region is that of the surrounding solid + If this container is contained in another container, the solid of this container should + be subtacted from it's motherregion + ''' + return self.containerSolidExporter.get_region() + + def myRegion(self): + ''' + return region occupied by material in this Exporter + This is the solid region for the container minus the region occupied by all its children + ''' + solid_region = self.containerSolidExporter.get_region() + children_region = Region("") + for child_geom_exporter in self.children_geom_exporters: + child_region = child_geom_exporter.getRegion() + if child_region is not None: + children_region = children_region.union(child_region) + + return solid_region.cut(children_region) + + def export(self): + # Very ugly way of checking whether we are exporting world volume + worldObject = list(obj_top_children_dict.keys())[0] + if self.obj == worldObject: + self.containerSolidExporter.set_boundary_type("vacuum") + + self.containerSolidExporter.export() # This exports the SURFACES of the container + for child_geom_exporter in self.children_geom_exporters: + child_geom_exporter.export() + region = self.myRegion() + # material is that of the containing box + mat_name = getMaterial(self.containerSolidExporter.obj) + cellExporter = CellExporter(self.obj.Label, material_name=mat_name, region=region.expr) + cellExporter.export() + + +class AssemblyExporter(GeomObjExporter): + def __init__(self, obj): + super().__init__(obj) + self.children_geom_exporters = [] + firstChild = obj_top_children_dict[obj][0] + + arrayOfPart = False + if isArrayType(firstChild) and obj_top_children_dict[firstChild][0].TypeId == "App::Part": + processArrayPart(firstChild) + arrayOfPart = True + + for child in obj_top_children_dict[obj]: + if child is firstChild and arrayOfPart: + continue + + geomExporter = GeomObjExporter.getExporter(child) + if geomExporter is None: + print(f" No GeomObjectExporter for {child.Label}. Skipping") + continue + self.children_geom_exporters.append(geomExporter) + + def getRegion(self): + children_region = Region("") + for child_geom_exporter in self.children_geom_exporters: + children_region = children_region.union(child_geom_exporter.getRegion()) + + return children_region + + def export(self): + for child_geom_exporter in self.children_geom_exporters: + child_geom_exporter.export() + + +class SingleVolumeExporter(GeomObjExporter): + ''' + A Logical volume of the forma: + App::Part + Solid + ''' + global material_ids + def __init__(self, obj): + super().__init__(obj) + child = obj_top_children_dict[obj][0] + self.child_solid_exporter = SolidExporter.getExporter(child) + if self.child_solid_exporter is None: + if hasattr(child, "TypeId"): + print(f"TpeId {child.TypeId} does not have a SolidExporter") + print(f"{child.Label} has no SolidExporter") + + def getRegion(self): + if self.child_solid_exporter is None: + return + + return self.child_solid_exporter.get_region() + + def export(self): + if self.child_solid_exporter is None: + return + self.child_solid_exporter.export() + mat_name = getMaterial(self.child_solid_exporter.obj) + solidRegion = self.child_solid_exporter.get_region() + cellExporter = CellExporter(self.obj.Label, material_name=mat_name, region=solidRegion.expr) + cellExporter.export() + + +class SingleSolidExporter(GeomObjExporter): + def __init__(self, obj): + super().__init__(obj) + + self.solidExporter = SolidExporter.getExporter(obj) + + def getRegion(self): + return self.solidExporter.get_region() + + def export(self): + if self.solidExporter is None: + print(f"There is no exporter for object {self.obj.Label}") + return + + self.solidExporter.export() + solidRegion = self.solidExporter.get_region() + mat_name = getMaterial(self.obj) + cellExporter = CellExporter(self.obj.Label, material_name=mat_name, region=solidRegion.expr) + cellExporter.export() + +#------------------------------ Geometry Objects exporters ------------------------- + +def processDocTree(rootUniverseId): + + global obj_top_children_dict + objects = list(obj_top_children_dict.keys()) + worldObj = objects[0] + + geomExporter = GeomObjExporter.getExporter(worldObj) + print(f"{worldObj.Label} {worldObj}") + + geomExporter.export() + + +def exportGDMLworld(first, filepath, fileExt): + global obj_top_children_dict + + buildDocTree() # creates global obj_top_children_dict + NameManager.init() + SolidExporter.init() + + # for debugging doc tree + for obj in obj_top_children_dict: + s = "" + for child in obj_top_children_dict[obj]: + s += child.Label + ", " + print(f"{obj.Label} [{s}]") + + if filepath.lower().endswith(".xml"): + # GDML Export + print("OpenMC Export") + # if hasattr(first,'InList') : + # print(len(first.InList)) + + vCount, lcount, gCount = countGDMLObj(first) + if gCount > 1: + from .GDMLQtDialogs import showInvalidWorldVol + + showInvalidWorldVol() + else: + exportGDML(first, filepath, fileExt) + + +def hexInt(f): + return hex(int(f * 255))[2:].zfill(2) + +def exportMaterials(first, filename): + if filename.lower().endswith(".xml"): + print("Export Materials to XML file : " + filename) + xml = ET.Element("xml") + global define + define = ET.SubElement(xml, "define") + global materials + materials = ET.SubElement(xml, "materials") + processMaterials() + indent(xml) + ET.ElementTree(xml).write(filename) + else: + print("File extension must be xml") + + +def create_gcard(path, flag): + basename = os.path.basename(path) + print("Create gcard : " + basename) + print("Path : " + path) + gcard = ET.Element("gcard") + ET.SubElement(gcard, "detector", {"name": "target_cad", "factory": "CAD"}) + if flag is True: + ET.SubElement( + gcard, "detector", {"name": "target_gdml", "factory": "GDML"} + ) + indent(gcard) + path = os.path.join(path, basename + ".gcard") + ET.ElementTree(gcard).write(path) + + +def checkDirectory(path): + if not os.path.exists(path): + print("Creating Directory : " + path) + os.mkdir(path) + +def export(exportList, filepath): + "called when FreeCAD exports a file" + + print("OpenMC exporter version 0.1") + + first = exportList[0] + print(f"Export Volume: {first.Label}") + + import os + + path, fileExt = os.path.splitext(filepath) + print("filepath : " + path) + print("file extension : " + fileExt) + + if fileExt.lower() == ".xml": + # import cProfile, pstats + # profiler = cProfile.Profile() + # profiler.enable() + + if first.TypeId == "App::Part": + exportGDMLworld(first, filepath, fileExt) + # + # elif first.Label == "Materials": + # exportMaterials(first, filepath) + + else: + print("Needs to be a Part for export") + from PySide import QtGui + + QtGui.QMessageBox.critical( + None, "Need to select a Part for export", "Need to select Part of GDML Volume to be exported \n\n Press OK to return" + ) + # profiler.disable() + # stats = pstats.Stats(profiler).sort_stats('cumtime') + # stats.print_stats() + +# +# ------------------------------------------------------------------------------------------------------- +# + +class Region: + def __init__(self, expr): + self.expr = expr + + def union(self, other): + if self.expr == "": + return Region(f"({other.expr})") + return Region(f"(({self.expr}) | ({other.expr}))") + + def intersection(self, other): + if self.expr == "": + return Region(f"({other.expr})") + return Region(f"(({self.expr}) ({other.expr}))") + + def cut(self, other): + if self.expr == "": + return Region(f"~({other.expr})") + return Region(f"(({self.expr}) ~ ({other.expr}))") + + def __str__(self): + return self.expr + +class CellExporter: + _ids = [] # dictionary of name vs id: __ids[name] = id + _ids_dict = {} + def __init__(self, name, material_name=None, region=None, universe=None, fill=None, + rotation=None, translation=None): + + id = 1 + while id in CellExporter._ids: + id += 1 + CellExporter._ids.append(id) + CellExporter._ids_dict[name] = id + + self.id = id + self.name = name + if material_name in material_ids: + self.material = material_ids[material_name] + else: + self.material = None + + self.fill = fill + + self.region = region + self.rotation = rotation + self.translation = translation + self.universe = universe + + def export(self): + cell = ET.SubElement(geometry, 'cell') + cell.attrib['name'] = self.name + cell.attrib['id'] = str(self.id) + if self.universe is not None: + cell.attrib['universe'] = str(self.universe) + if self.material is not None: + cell.attrib['material'] = str(self.material) + + if self.region is not None: + cell.attrib['region'] = str(self.region) + + if self.fill is not None: + cell.attrib['fill'] = str(self.fill) + + if self.rotation is not None: + angles = quaternion2XYZ(self.rotation) + cell.attrib['rotation'] = f"{angles[0]} {angles[1]} {angles[2]}" + + if self.translation is not None: + cell.attrib['translation'] = f"{self.translation.x} {self.translation.y} {self.translation.z}" + + +def quadric_coeffs(F, **kwargs): + ''' Given a function F that calculates a surface F(x, y, z, **kwargs) return + the coefficients A, B, C, D, E, F, G, H, J, K of the quadric expression + A x^2 + B y^2 + C z^2 + D xy + E yz + F xz + G x + H y + J z + K + # Suppose one has a means of evaluating F(x, y, z) = A x^2 + B y^2 + C z^2 + + # D xy + E yz + F xz + + # G x + H y + J z + + # K + # Usually this is done by rotating/translating a vector (x', y', z') in which the surface has a simple + # form (say x'2 + y'2) = R^2 for a cylinder surface, to the system in which the axes are rotated/translated + # say, v = R v', where is the 4x4 transformation matrix. + # Now the numerical extraction of the coefficients A, B, C, .... + # K = F(0, 0, 0) + # A = 1/2 (F(1, 0, 0) + F(-1, 0, 0)) - K + # B = 1/2 (F(0, 1, 0) + F(0, -1, 0)) - K + # C = 1/2 (F(0, 0, 1) + F(0, 0, -1)) - K + # G = 1/2 (F(1, 0, 0) - F(-1, 0, 0)) + # H = 1/2 (F(0, 1, 0) - F(0, -1, 0)) + # J = 1/2 (F(0, 0, 1) - F(0, 0, -1)) + # For the cross terms (D, E, F) + # F(1, 1, 0) = A + B + D + G + H + K ==> D = F(1, 1, 0) -A -B -G -H -K + # F(0, 1, 1) = B + C + E + H + J + K ==> E = F(0, 1, 1) -B -C -H -J -K + # F(1, 0, 1) = A + C + F + G + J + K ==> F = F(1, 0, 1) -A -C -G -J -K + ''' + + K = F(0, 0, 0, **kwargs) + A = 1/2 * (F(1, 0, 0, **kwargs) + F(-1, 0, 0, **kwargs)) - K + B = 1/2 * (F(0, 1, 0, **kwargs) + F(0, -1, 0, **kwargs)) - K + C = 1/2 * (F(0, 0, 1, **kwargs) + F(0, 0, -1, **kwargs)) - K + G = 1/2 * (F(1, 0, 0, **kwargs) - F(-1, 0, 0, **kwargs)) + H = 1/2 * (F(0, 1, 0, **kwargs) - F(0, -1, 0, **kwargs)) + J = 1/2 * (F(0, 0, 1, **kwargs) - F(0, 0, -1, **kwargs)) + # For the cross terms (D, E, F) + D = F(1, 1, 0, **kwargs) -A -B -G -H -K + E = F(0, 1, 1, **kwargs) -B -C -H -J -K + F_coeff = F(1, 0, 1, **kwargs) -A -C -G -J -K + + print(A, B, C, D, E, F_coeff, G, H, J, K) + return A, B, C, D, E, F_coeff, G, H, J, K + +def ellipsoid(x, y, z, ax=1.0, by=1.0, cz=1.0, center=Vector(0,0,0), rotation=FreeCAD.Rotation()): + ''' for an ellipsoid center at the origin and axes coinciding with the x, y, and z axes + The equation is x^2/ax^2 + y^2/by^2 + z^2/cz^2 - 1 = 0; + If the origin is translated to x0, y0, z0 the equation becomes + (x-x0)^2 / ax^2 + (y-y0)^2 / by^2 + (z-z0)^2 / cz^2 - 1 = 0; + If the axes are subjected to a rotation R, then the equation becomes + (x'-x0)^2 / ax^2 + (y'-y0)^2 / by^2 + (z'-z0)^2 / cz^2 - 1 = 0; + where (x', y', z') = R^-1 (x, y, z) + ''' + + vprime = rotation.inverted()*(Vector(x, y, z) - center) + + x = vprime.x + y = vprime.y + z = vprime.z + + return (x/ax)**2 + (y/by)**2 + (z/cz)**2 - 1 + +def cylinder(x, y, z, radius=1, center=Vector(0,0,0), axis=Vector(0, 0, 1)): + + rotation = FreeCAD.Rotation(Vector(0, 0, 1), axis) + vprime = rotation.inverted()*(Vector(x, y, z) - center) + + x = vprime.x + y = vprime.y + z = vprime.z + + return x*x + y*y - radius*radius + +def elliptical_tube(x, y, z, dx=1, dy=1, center=Vector(0,0,0), rotation=FreeCAD.Rotation()): + + R = rotation.inverted() + vprime = R*(Vector(x, y, z) - center) + + x = vprime.x + y = vprime.y + z = vprime.z + + return (x/dx)**2 + (y/dy)**2 - 1.0 + + +def elliptical_cone(x, y, z, dx=1, dy=1, zHeight=1, center=Vector(0,0,0), rotation=FreeCAD.Rotation()): + + R = rotation.inverted() + vprime = R*(Vector(x, y, z) - center) + + x = vprime.x + y = vprime.y + z = vprime.z + + return (x/dx)**2 + (y/dy)**2 - (zHeight - z)**2 + + +def cone(x, y, z, center=Vector(0,0,0), axis=Vector(0, 0, 1), theta=1.0): + + rotation = FreeCAD.Rotation(Vector(0, 0, 1), axis) + vprime = rotation.inverted()*(Vector(x, y, z) - center) + + + x = vprime.x + y = vprime.y + z = vprime.z + t = math.tan(theta) + + return x**2 + y**2 - (z*t)**2 + + +def paraboloid(x, y, z, k1, k2, center=Vector(0,0,0), rotation=FreeCAD.Rotation()): + + vprime = rotation.inverted()*(Vector(x, y, z) - center) + + x = vprime.x + y = vprime.y + z = vprime.z + + return x**2 + y**2 - (k1*z + k2) + +class SurfaceExporter: + _ids = [] # dictionary of name vs id: __ids[name] = id + _ids_dict = {} + def __init__(self, name, type, coeffs, boundary="transmission"): + id = 1 + while id in SurfaceExporter._ids: + id += 1 + SurfaceExporter._ids.append(id) + SurfaceExporter._ids_dict[name] = id + print(f"Creating {name} id={id}") + + self.id = id + self.type = type + if name != "": + self.name = name + else: + self.name = f"{type}_{id}" + self.boundary = boundary + self.coeffs = coeffs + + @staticmethod + def reset_ids(): + SurfaceExporter._ids = [] + SurfaceExporter._ids_dict = {} + + def export(self): + surface = ET.SubElement(geometry, 'surface') + surface.attrib['id'] = str(self.id) + surface.attrib['name'] = str(self.name) + surface.attrib['type'] = str(self.type) + surface.attrib['coeffs'] = str(self.coeffs) + surface.attrib['boundary'] = self.boundary + + def translate(self, T): + return + + def rotate(self, R): + return + + def __hash__(self): + # hash coefficients to within 1 part in 10^4. This so when we compare two + # surfaces we will consider them to be the same if all the coefficients agree + # to within 1 part in 10^4 + s = [] + for w in self.coeffs.strip().split(): + s.append(float(f" {float(w):.4e}")) + + return hash(tuple(s)) + + def __eq__(self, other): + + # note that if the coefficients match, then we return ==, eventhough the ids might be different + if not (type(self) is type(other)): + return False + + return hash(self) == hash(other) + + +class PlaneSurfaceExporter(SurfaceExporter): + def __init__(self, name:str, normal:FreeCAD.Vector, D:float): + self.normal = normal + self.D = 0.1*D/self.normal.Length + self.normal.normalize() + self.to_coeffs() + super().__init__(name, "plane", self.coeffs) + + def translate(self, T): + T = 0.1 * T # convert from mm to cm for openmc + self.D += self.normal.dot(T) + self.to_coeffs() + + def rotate(self, R): + self.normal = R*self.normal + self.to_coeffs() + + def to_coeffs(self): + self.coeffs = f"{self.normal.x} {self.normal.y} {self.normal.z} {self.D}" + + def inRegion(self, point): + pcm = 0.1*point + + return self.normal.dot(pcm) - self.D < 0 + + +class SphereSurfaceExporter(SurfaceExporter): + def __init__(self, name, center, radius): + self.center = 0.1*center + self.radius = 0.1*radius + self.to_coeffs() + super().__init__(name, "sphere", self.coeffs) + + def to_coeffs(self): + self.coeffs = f"{self.center.x} {self.center.y} {self.center.z} {self.radius}" + + def translate(self, T): + T = 0.1 * T # convert from mm to cm for openmc + self.center += T + self.to_coeffs() + + def rotate(self, R): + self.center = R*self.center + self.to_coeffs() + + +class CylinderSurfaceExporter(SurfaceExporter): + def __init__(self, name, center, axis, radius): + # convert to quadric surface: + self.center = 0.1*center + self.axis = axis + self.radius = 0.1*radius + self.to_quadric() + super().__init__(name, "quadric", self.coeffs) + + def to_quadric(self): + A, B, C, D, E, F, G, H, J, K = quadric_coeffs(cylinder, radius=self.radius, + center=self.center, axis=self.axis) + + self.coeffs = f"{A} {B} {C} {D} {E} {F} {G} {H} {J} {K}" + + def translate(self, T): + T = 0.1 * T # convert from mm to cm for openmc + self.center += T + self.to_quadric() + + + def rotate(self, R): + self.axis = R*self.axis + self.center = R*self.center + self.to_quadric() + + +class EllipticalCylinderSurfaceExporter(SurfaceExporter): + def __init__(self, name, center,dx, dy): + # convert to quadric surface: + self.center = 0.1*center + self.dx = 0.1*dx + self.dy = 0.1*dy + self.rotation = FreeCAD.Rotation() + self.to_quadric() + super().__init__(name, "quadric", self.coeffs) + + def to_quadric(self): + A, B, C, D, E, F, G, H, J, K = quadric_coeffs(elliptical_tube, dx=self.dx, dy=self.dy, + center=self.center, rotation=self.rotation) + + self.coeffs = f"{A} {B} {C} {D} {E} {F} {G} {H} {J} {K}" + + def translate(self, T): + T = 0.1 * T # convert from mm to cm for openmc + self.center += T + self.to_quadric() + + + def rotate(self, R): + self.rotation = R*self.rotation + self.center = R*self.center + self.to_quadric() + + +class EllipticalConeSurfaceExporter(SurfaceExporter): + def __init__(self, name, center, dx, dy, zHeight): + # convert to quadric surface: + self.center = 0.1*center + self.dx = dx # this is ration, so no conversion from mm to cm + self.dy = dy + self.zHeight = 0.1*zHeight + self.rotation = FreeCAD.Rotation() + self.to_quadric() + super().__init__(name, "quadric", self.coeffs) + + def to_quadric(self): + A, B, C, D, E, F, G, H, J, K = quadric_coeffs(elliptical_cone, dx=self.dx, dy=self.dy, + zHeight=self.zHeight, + center=self.center, rotation=self.rotation) + + self.coeffs = f"{A} {B} {C} {D} {E} {F} {G} {H} {J} {K}" + + def translate(self, T): + T = 0.1 * T # convert from mm to cm for openmc + self.center += T + self.to_quadric() + + + def rotate(self, R): + self.rotation = R*self.rotation + self.center = R*self.center + self.to_quadric() + + +class ConeSurfaceExporter(SurfaceExporter): + def __init__(self, name, center, axis, theta): + ''' + :param: center: cone vertex posidion + :param: axis: unit vector directiom of cone axis + :param: theta: cone half angle, in radians + ''' + # convert to quadric surface: + self.center = 0.1*center + self.axis = axis + self.theta = theta + + self.to_quadric() + super().__init__(name, "quadric", self.coeffs) + + def to_quadric(self): + A, B, C, D, E, F, G, H, J, K = quadric_coeffs(cone, center=self.center, axis=self.axis, theta=self.theta) + self.coeffs = f"{A} {B} {C} {D} {E} {F} {G} {H} {J} {K}" + + + def translate(self, T): + T = 0.1 * T # convert from mm to cm for openmc + self.center += T + self.to_quadric() + + + def rotate(self, R): + self.axis = R*self.axis + self.center = R*self.center + self.to_quadric() + + +class EllipsoidSurfaceExporter(SurfaceExporter): + def __init__(self, name, ax, by, cz, center=Vector(0, 0, 0), rotation=FreeCAD.Rotation()): + ''' + :param: center: cone vertex posidion + :param: axis: unit vector directiom of cone axis + :param: theta: cone half angle, in radians + ''' + # convert to quadric surface: + self.ax = 0.1*ax + self.by = 0.1*by + self.cz = 0.1*cz + self.center = 0.1*center + self.rotation = rotation + + self.to_quadric() + super().__init__(name, "quadric", self.coeffs) + + def to_quadric(self): + A, B, C, D, E, F, G, H, J, K = quadric_coeffs(ellipsoid, ax=self.ax, by=self.by, cz=self.cz, + center=self.center, rotation=self.rotation) + self.coeffs = f"{A} {B} {C} {D} {E} {F} {G} {H} {J} {K}" + + + def translate(self, T): + T = 0.1 * T # convert from mm to cm for openmc + self.center += T + self.to_quadric() + + + def rotate(self, R): + self.center = R*self.center # is this needed + self.rotation = R*self.rotation + self.to_quadric() + + +class ParaboloidSurfaceExporter(SurfaceExporter): + def __init__(self, name, rlo, rhi, dz, center=Vector(0, 0, 0), rotation=FreeCAD.Rotation()): + ''' + + Equation of paraboloid is given by + (x^2+y^2) - rho^2 = k1 z + k2 + :param: rlo: k1 * (-dz) + k2; value of rho at z=-dz + :param: rhi: k1 * (+dz) + k2 value of rho at z=+dz + :param: dx: half height of parbolid + :param: center: center of paraboloid + :param: rotation: rotaion of axis from z-axis + ''' + # Equation of Paraboloid in geant4 is: + # (x^2+y^2) = rho^2 - k1 z + k2 + # k1 and k2 determined from + # rlo^2 = k1 *(-dz) + k2 + # rhi^2 = k1 *(+dz) + k2 + # ==> k1 = (rhi^2 - rlo^2)/2 dz + # ==> k2 = (rlo^2+rhi^2)/2 + + # convert to quadric surface: + dz *= 0.1 # convert to cm + rlo *= 0.1 + rhi *= 0.1 + self.k1 = (rhi*rhi - rlo*rlo)/(2*dz) + self.k2 = (rhi*rhi + rlo*rlo)/2 + self.center = 0.1*center + self.rotation = rotation + + self.to_quadric() + super().__init__(name, "quadric", self.coeffs) + + def to_quadric(self): + A, B, C, D, E, F, G, H, J, K = quadric_coeffs(paraboloid, k1=self.k1, k2=self.k2, + center=self.center, rotation=self.rotation) + self.coeffs = f"{A} {B} {C} {D} {E} {F} {G} {H} {J} {K}" + + + def translate(self, T): + T = 0.1 * T # convert from mm to cm for openmc + self.center += T + self.to_quadric() + + + def rotate(self, R): + self.center = R*self.center # is this needed + self.rotation = R*self.rotation + self.to_quadric() + + +class SolidExporter: + # Abstract class to export object as gdml + _exported = [] # a list of already exported objects + solidExporters = { + "GDMLArb8": "AutoTessellateExporter", + "GDMLBox": "GDMLBoxExporter", + "GDMLCone": "GDMLConeExporter", + "GDMLcutTube": "GDMLcutTubeExporter", + "GDMLElCone": "GDMLElConeExporter", + "GDMLEllipsoid": "GDMLEllipsoidExporter", + "GDMLElTube": "GDMLElTubeExporter", + "GDMLHype": "GDMLHypeExporter", + "GDMLOrb": "GDMLOrbExporter", + "GDMLPara": "AutoTessellateExporter", + "GDMLParaboloid": "GDMLParaboloidExporter", + "GDMLPolycone": "GDMLPolyconeExporter", + "GDMLGenericPolycone": "GDMLGenericPolyconeExporter", + "GDMLPolyhedra": "AutoTessellateExporter", + "GDMLGenericPolyhedra": "AutoTessellateExporter", + "GDMLSphere": "GDMLSphereExporter", + "GDMLTessellated": "AutoTessellateExporter", + "GDMLSampledTessellated": "AutoTessellateExporter", + "GDMLGmshTessellated": "AutoTessellateExporter", + "GDMLTetra": "AutoTessellateExporter", + "GDMLTetrahedron": "AutoTessellateExporter", + "GDMLTorus": "GDMLTorusExporter", + "GDMLTrap": "AutoTessellateExporter", + "GDMLTrd": "AutoTessellateExporter", + "GDMLTube": "GDMLTubeExporter", + "GDMLTwistedbox": "GDMLTwistedboxExporter", + "GDMLTwistedtrap": "GDMLTwistedtrapExporter", + "GDMLTwistedtrd": "GDMLTwistedtrdExporter", + "GDMLTwistedtubs": "GDMLTwistedtubsExporter", + "GDMLXtru": "AutoTessellateExporter", + "Mesh::Feature": "AutoTessellateExporter", + "Part::MultiFuse": "MultiFuseExporter", + "Part::MultiCommon": "MultiCommonExporter", + "Part::Extrusion": "ExtrusionExporter", + "Part::Revolution": "RevolutionExporter", + "Part::Box": "BoxExporter", + "Part::Cylinder": "CylinderExporter", + "Part::Torus": "TorusExporter", + "Tube": "TubeExporter", + "Part::Cone": "ConeExporter", + "Part::Sphere": "SphereExporter", + "Part::Cut": "BooleanExporter", + "Part::Fuse": "BooleanExporter", + "Part::Common": "BooleanExporter", + "Part::Fillet": "AutoTessellateExporter", + "Part::Chamfer": "AutoTessellateExporter", + "Part::Loft": "AutoTessellateExporter", + "Part::Sweep": "AutoTessellateExporter" + } + + @staticmethod + def init(): + SolidExporter._exported = [] + + @staticmethod + def isSolid(obj): + """Return True for objects whose primary identity is their own Shape.""" + print(f"isSolid {obj.Label}") + # return hasattr(obj, 'Shape') # does not work. App::Parts have Shape, but they are not solids! + + obj1 = obj + if obj.TypeId == "App::Link": + obj1 = obj.LinkedObject + + if not hasattr(obj, "Shape") or obj.Shape.isNull(): + return False + + # Exclude known containers / special cases + if obj.isDerivedFrom("App::Part"): + return False # container, aggregate shape + + if obj.isDerivedFrom("PartDesign::Body"): + # optional: treat Body as container rather than primitive + return False + + # Part::Compound is a grey zone: + # treat it as "real shape" or as aggregate depending on your needs + # Example: treat as aggregate and skip here: + if obj.TypeId in ("Part::Compound", "Part::CompoundPython"): + return False + + # Everything else derived from Part::Feature is fair game + if obj.isDerivedFrom("Part::Feature"): + return True + + if obj1.TypeId == "Part::FeaturePython": + if hasattr(obj, 'Shape') and not obj.Shape.isNull(): + return True + + return False + + @staticmethod + def getExporter(obj): + if obj.TypeId == "Part::FeaturePython": + if hasattr(obj.Proxy, 'Type'): + typeId = obj.Proxy.Type + if typeId == "Array": + if obj.ArrayType == "ortho": + return OrthoArrayExporter(obj) + elif obj.ArrayType == "polar": + return PolarArrayExporter(obj) + elif typeId == "PathArray": + return PathArrayExporter(obj) + elif typeId == "PointArray": + return PointArrayExporter(obj) + elif typeId == "Clone": + return CloneExporter(obj) + else: + name = obj.Name + if name[:4] == 'Tube': + return TubeExporter(obj) + + # default, fall thru + typeId = obj.TypeId + else: + typeId = obj.TypeId + + if typeId in SolidExporter.solidExporters: + classname = SolidExporter.solidExporters[typeId] + # kludge for classes imported from another module + # globals["RevolutionExporter"] returns key error + if classname == "ExtrusionExporter": + return ExtrusionExporter(obj) + elif classname == "RevolutionExporter": + return RevolutionExporter(obj) + else: + print(f"classname {classname}") + klass = globals()[classname] + return klass(obj) + elif obj.TypeId == "Part::FeaturePython": # This may appear to be duplication of above, but + # we need to pass through all the specialized exporters + # before we fall back to tessellation + if hasattr(obj, 'Shape') and not obj.Shape.isNull(): + print(f"{obj.Label} does not have a Native Solid Exporter, Using AutoTessellator") + return AutoTessellateExporter(obj) + else: + if hasattr(obj, 'Shape') and not obj.Shape.isNull(): + print(f"{obj.Label} does not have a native Solid Exporter, Using AutoTessellator") + return AutoTessellateExporter(obj) + + return None + + def __init__(self, obj): + self.obj = obj + self._name = NameManager.getName(obj) + self.region = None + self.surfaces = [] + + def generate_surfaces(self): + ''' build the list of surfaces. Implemented by implementors''' + return + + def name(self): + return self._name + + def position(self): + # return self.obj.Placement.Base we ar expect each solid to translate/rotate its surfaces + return FreeCAD.Vector() + + def rotation(self): + return FreeCAD.Rotation() + # return self.obj.Placement.Rotation. we expect objects to rotate their surfaces + + + def placement(self): + return FreeCAD.Placement(self.position(), self.rotation()) + + + def exported(self): + return self.obj in SolidExporter._exported + + def export(self): + if self.region is None: + self.generate_surfaces() + for surf in self.surfaces: + surf.export() + return + + def rotate(self, R): + for surf in self.surfaces: + surf.rotate(R) + + def translate(self, T): + for surf in self.surfaces: + surf.translate(T) + + def get_region(self): + if self.region is None: + self.generate_surfaces() + return self.region + + def set_boundary_type(self, boundary_type): + if self.region is None: + self.generate_surfaces() + for surf in self.surfaces: + surf.boundary=boundary_type + + def position_globally(self): + identity = FreeCAD.Placement() + placement = self.obj.getGlobalPlacement() + + if placement != identity: + rot = placement.Rotation + trans = placement.Base + for surf in self.surfaces: + surf.rotate(rot) + surf.translate(trans) + + + def getMult(self): + ''' return multiplier for length units of self.obj''' + unit = "mm" # set default + # Watch for unit and lunit + # print('getMult : '+str(fp)) + if hasattr(self.obj, "lunit"): + unit = self.obj.lunit + elif hasattr(self.obj, "unit"): + unit = self.obj.unit + elif hasattr(self.obj, "attrib"): + if "unit" in self.obj.attrib: + unit = self.obj.attrib["unit"] + elif "lunit" in self.obj.attrib: + unit = self.obj.attrib["lunit"] + else: + return 1 + + # The exporters will convert to cm. Here we convert to mm + unitsDict = { + "mm": 1, + "cm": 10.0, + "m": 1000., + "um": 0.001, + "nm": 1.0e-6, + # "dm": 100, decimeter not recognized by geant + "km": 1000000.0, + } + if unit in unitsDict: + return unitsDict[unit] + + print("unit not handled : " + unit) + + +class CloneExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + self._position = self.obj.Placement.Base + self._rotation = self.obj.Placement.Rotation + + def position(self): + return self._position + + def rotation(self): + return self._rotation + + def getScale(self): + if self.hasScale(): + # For rotation first, followed by scaling, the scaling would + # need to change: Looking for scaling S', such that + # S*R v = R*S' v ==> + # S' = R^-1 * S * R + # + # s = FreeCAD.Matrix() + # s.scale(self.obj.Scale.x, self.obj.Scale.y, self.obj.Scale.z) + # rot = FreeCAD.Rotation(self.rotation()) + # rot.Angle = -rot.Angle + # sprime = rot.inverted()*s*rot + # return FreeCAD.Vector(sprime.A11, sprime.A22, sprime.A33) + # + # For scaling then rotating + return self.obj.Scale + else: + return FreeCAD.Vector(1, 1, 1) + + def export(self): + if len(self.obj.Objects) == 1: + self._export1(self.obj.Objects[0]) + return + + # Here deal with scaling multi objects: + # Make a multiunion of all the objects, then scale that + exporters = [] + for obj in self.obj.Objects: + exporter = SolidExporter.getExporter(obj) + exporter.export() + exporters.append(exporter) + + unionXML = ET.SubElement(solids, "multiUnion", {"name": self.name()}) + for i, exporter in enumerate(exporters): + volRef = exporter.name() + nodeName = f"{self.name()}_{i}" + nodeXML = ET.SubElement( + unionXML, "multiUnionNode", {"name": nodeName} + ) + ET.SubElement(nodeXML, "solid", {"ref": volRef}) + exportPosition(nodeName, nodeXML, exporter.position()) + rot = FreeCAD.Rotation(exporter.rotation()) + # for reasons that I don't understand, in booleans and multiunions + # angle is NOT reversed, so undo reversal of exportRotation + exportRotation(nodeName, nodeXML, rot, invertRotation=False) + + self._exportScaled() + + def _export1(self, clonedObj): + # for now we only deal with one cloned object + clonedObj = self.obj.Objects[0] + exporter = SolidExporter.getExporter(clonedObj) + exporter.export() + # The scaling of the position turns out to be more complicated + # than I first thought (MMH). Draft->scale scales the position of the + # the cloned object, i.e., the clone has a placement that already + # includes the scaling of the placement of the cloned object, so it is + # not necessary to repeat the scaling. HOWEVER, for several of the + # objects we deal with, the position that is + # exported to the gdml IS NOT obj.Placement. For example, a regular box + # as its origin at corner, whereas a gdml box has its origin at the + # center, so we take that into account on export by adding a shift by + # half of each dimension. Draft/scale will scale the + # cube.Placement, which is (0,0,0), so nothing happens. The solution: + # get the clone position, unscale it, then get the exporter.position(), + # and then scale THAT. Note that once an object has been cloned, the + # clone no longer keeps track of the objects POSITION, but it does + # keep track of its dimensions. So if the object is doubles in size, + # the (scaled) double will change, but if the object is MOVED, the + # clone will not change its position! So the following algorithm, would + # fail. There is no way to know if the difference between the scaled + # position and the clone's position is due to the clone moving or the + # object moving. + clonedPlacement = FreeCAD.Placement( + exporter.placement() + ) # copy the placement + m = clonedPlacement.Matrix + invRotation = FreeCAD.Placement(m.inverse()).Rotation + clonedPosition = clonedPlacement.Base + clonedRotation = clonedPlacement.Rotation + # unrotate original position + r1 = invRotation * clonedPosition + objPosition = FreeCAD.Vector(clonedObj.Placement.Base) + if self.hasScale(): + scale = self.obj.Scale + r1.scale(scale.x, scale.y, scale.z) + delta = self.obj.Placement.Base - objPosition.scale( + scale.x, scale.y, scale.z + ) + else: + delta = self.obj.Placement.Base - objPosition + objRotation = FreeCAD.Rotation(clonedObj.Placement.Rotation) + myRotation = self.obj.Placement.Rotation + # additional rotation of clone + objRotation.invert() + additionalRotation = myRotation * objRotation + desiredRotation = additionalRotation * clonedRotation + r2 = desiredRotation * r1 + # if neither clone not original have moved, delta should be zero + # If it is not, there is no way to know which moved, but we are working + # on the assumption only clone moved + # same consideration for rotations. Draft scale copies the obj.Placement.Rotation + # But that is not necessarily the exporters rotation (e.g. extruded ellipses + # rotation depends on orientation of ellipse). Further, the clone itself + # could have an extra rotation. + print(r2, delta) + clonedPosition = r2 + delta + placement = FreeCAD.Placement(clonedPosition, desiredRotation) + + self._position = placement.Base + self._rotation = placement.Rotation + self._name = exporter.name() + + +class BoxExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + + def generate_surfaces(self): + self.surfaces = [] + placement = self.obj.Placement + + normal = Vector(1, 0, 0) + D = 0 + x1surface = PlaneSurfaceExporter(f"{self.obj.Label}_x1_plane)", normal, D) + D = self.obj.Length.Value + x2surface = PlaneSurfaceExporter(f"{self.obj.Label}_x2_plane)", normal, D) + + normal = Vector(0, 1, 0) + D = 0 + y1surface = PlaneSurfaceExporter(f"{self.obj.Label}_y1_plane)", normal, D) + D = self.obj.Width.Value + y2surface = PlaneSurfaceExporter(f"{self.obj.Label}_y2_plane)", normal, D) + + normal = Vector(0, 0, 1) + D = 0 + z1surface = PlaneSurfaceExporter(f"{self.obj.Label}_z1_plane)", normal, D) + D = self.obj.Height.Value + z2surface = PlaneSurfaceExporter(f"{self.obj.Label}_z2_plane)", normal, D) + + self.surfaces = [x1surface, x2surface, y1surface, y2surface, z1surface, z2surface] + self.region = Region(f"+{x1surface.id} -{x2surface.id} +{y1surface.id} -{y2surface.id} +{z1surface.id} -{z2surface.id}") + + self.position_globally() + + + +class CylinderExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + + def generate_surfaces(self): + normal = Vector(0, 0, 1) + D = self.obj.Height.Value + top_surface = PlaneSurfaceExporter(f"{self.obj.Label}_top", normal, D) + D = 0 + bottom_surface = PlaneSurfaceExporter(f"{self.obj.Label}_bot", normal, D) + + axis = Vector(0, 0, 1) + center = Vector(0, 0, 0) + radius = self.obj.Radius.Value + cylinder_surface = CylinderSurfaceExporter(f"{self.name()}_cylinder", center, axis, radius) + + self.surfaces = [top_surface, bottom_surface, cylinder_surface] + self.region = Region(f"-{cylinder_surface.id} +{bottom_surface.id} -{top_surface.id}") + + self.position_globally() + + +class TubeExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + + def generate_surfaces(self): + normal = Vector(0, 0, 1) + + D = self.obj.Height.Value + top_surface = PlaneSurfaceExporter(f"{self.obj.Label}_top)", normal, D) + D = 0 + bottom_surface = PlaneSurfaceExporter(f"{self.obj.Label}_bot)", normal, D) + + if self.obj.InnerRadius != 0: + radius = self.obj.InnerRadius.Value + axis = normal + center = Vector(0, 0, 0) + inner_surface = CylinderSurfaceExporter(f"{self.name()}_ir)", center, axis, radius) + else: + inner_surface = None + + radius = self.obj.OuterRadius.Value + outer_surface = CylinderSurfaceExporter(f"{self.name()}_ot)", center, axis, radius) + + self.surfaces = [top_surface, bottom_surface, outer_surface] + if inner_surface is not None: + self.surfaces.append(inner_surface) + + region_expr = f"-{top_surface.id} +{bottom_surface.id} -{outer_surface.id}" + if inner_surface is not None: + region_expr += f" +{inner_surface.id}" + + self.region = Region(region_expr) + + self.position_globally() + +class GDMLElTubeExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + + def generate_surfaces(self): + axis = Vector(0, 0, 1) + mul = self.getMult(self.obj) + + D = mul*self.obj.dz + center = Vector(0, 0, 0) + + name = NameManager.getName(self.obj) + top_surface = PlaneSurfaceExporter(f"{name}_top", axis, D) + bottom_surface = PlaneSurfaceExporter(f"{name}_bot", axis, -D) + + dx = mul*self.obj.dx + dy = mul*self.obj.dy + surf = EllipticalCylinderSurfaceExporter(f"{name}", center, dx, dy) + + self.surfaces = [top_surface, bottom_surface, surf] + self.region = Region(f"-{surf.id} +{bottom_surface.id} -{top_surface.id}") + + self.position_globally() + + +class ConeExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + + def generate_surfaces(self): + + # Cone half angle + r1 = self.obj.Radius1.Value + r2 = self.obj.Radius2.Value + h = self.obj.Height.Value + theta = math.atan(abs(r2-r1)/h) + if r1 > r2: + axis = Vector(0, 0, 1) + center = Vector(0, 0, r1/math.tan(theta)) + surface = ConeSurfaceExporter(self.name(), center, axis, theta) + bottom_plane = PlaneSurfaceExporter(self.name()+'_bot', axis, 0) + top_plane = PlaneSurfaceExporter(self.name()+'_top', axis, h) + elif r2 > r1: + axis = Vector(0, 0, -1) + center = Vector(0, 0, -r1/math.tan(theta)) + surface = ConeSurfaceExporter(self.name(), center, axis, theta) + bottom_plane = PlaneSurfaceExporter(self.name()+'_bot', axis, 0) + top_plane = PlaneSurfaceExporter(self.name()+'_top', axis, h) + else: + center = Vector(0, 0, 0) + axis = Vector(0, 0, 1) + surface = ConeSurfaceExporter(self.name(), center, axis, r1) + bottom_plane = PlaneSurfaceExporter(self.name()+'_bot', axis, 0) + top_plane = PlaneSurfaceExporter(self.name()+'_top', axis, h) + + self.surfaces = [surface, bottom_plane, top_plane] + self.region = Region(f'-{surface.id} +{bottom_plane.id} -{top_plane.id}') + + angle = self.obj.Angle.Value + if angle != 360: + phi0_normal = Vector(0, 1, 0) + phi0_plane = PlaneSurfaceExporter(self.name()+'_phi0', phi0_normal, 0) + + phi1_normal = Vector(math.cos(math.radians(angle+90)), math.sin(math.radians(angle+90)), 0) + phi1_plane = PlaneSurfaceExporter(self.name()+'_phi0', phi1_normal, 0) + self.surfaces += [phi0_plane, phi1_plane] + + if phi0_normal.cross(phi1_normal).z > 0: + self.region = Region(f'-{surface.id} +{bottom_plane.id} -{top_plane.id} +{phi0_plane.id} -{phi1_plane.id}') + else: + self.region = Region(f'-{surface.id} +{bottom_plane.id} -{top_plane.id} (+{phi0_plane.id} | -{phi1_plane.id})') + + + + self.position_globally() + + +class SphereExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + + def generate_surfaces(self): + self.surfaces = [] + center = Vector(0, 0, 0) # Note off -center spheres are handled by their position placement + radius = self.obj.Radius.Value + sphere_surface = SphereSurfaceExporter(f"{self.name()}_surf", center, radius) + self.surfaces.append(sphere_surface) + + if self.obj.Angle1 != 0: + z1 = self.obj.Radius * math.sin(math.radians(self.obj.Angle1)) + normal = Vector(0, 0, 1) + D = z1 + z1surface = PlaneSurfaceExporter(f"{self.obj.Label}_z1_plane)", normal, D) + self.surfaces.append(z1surface) + + if self.obj.Angle2 != 0: + z2 = self.obj.Radius * math.sin(math.radians(self.obj.Angle2)) + normal = Vector(0, 0, 1) + D = z2 + z2surface = PlaneSurfaceExporter(f"{self.obj.Label}_z2_plane)", normal, D) + self.surfaces.append(z2surface) + + if self.obj.Angle3 != 360: + phi0_normal = Vector(0, 1, 0) + D = 0 + phi0_plane = PlaneSurfaceExporter(f"{self.obj.Label}_pho0_plane)", phi0_normal, D) + self.surfaces.append(phi0_plane) + + nx = -math.sin(math.radians(self.obj.Angle3)) + ny = math.cos(math.radians(self.obj.Angle3)) + phi1_normal = Vector(nx, ny, 0) + + phi1_plane = PlaneSurfaceExporter(f"{self.obj.Label}_pho1_plane)", phi1_normal, D) + self.surfaces.append(phi1_plane) + + region = f"-{sphere_surface.id}" + if self.obj.Angle1 != 0: + region += f" +{z1surface.id}" + if self.obj.Angle2 != 0: + region += f" -{z2surface.id}" + + if self.obj.Angle3 != 360: + if phi0_normal.cross(phi1_normal).z > 0: + region += f" +{phi0_plane.id} -{phi1_plane.id}" + else: + region += f"(+{phi0_plane.id} | -{phi1_plane.id})" + + self.region = Region(region) + + self.position_globally() + + +class BooleanExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + baseExporter = SolidExporter.getExporter(self.obj.Base) + basePlacement = baseExporter.placement() + self._placement = self.obj.Placement * basePlacement + + def isBoolean(self, obj): + id = obj.TypeId + return id == "Part::Cut" or id == "Part::Fuse" or id == "Part::Common" + + def boolOperation(self, obj): + opsDict = { + "Part::Cut": "subtraction", + "Part::Fuse": "union", + "Part::Common": "intersection", + } + if obj.TypeId in opsDict: + return opsDict[obj.TypeId] + else: + print(f"Boolean type {obj.TypeId} not handled yet") + return None + + def position(self): + return self._placement.Base + + def rotation(self): + return self._placement.Rotation + + def placement(self): + return self._placement + + def generate_surfaces(self): + self.surfaces = [] + """ + In FreeCAD doc booleans that are themselves composed of other booleans + are listed in sequence, eg: + topBool: + Base: Nonbool_0 + Tool: bool1: + Base: bool2: + Base: Nonbool_1 + Tool: Nonbool_2 + Tool: bool3: + Base: Nonbool_3 + Tool: Nonbool_4 + In the gdml file, boolean solids must always refer to PREVIOUSLY + defined solids. So the last booleans must be written first: + + + + + + + + + + + The code below first builds the list of booleans in order: + [topBool, bool1, bool2, bool3] + + Then outputs them to gdml in reverse order. + In the process of scanning for booleans, the Nonbooleans are exported + """ + + objPlacement = self.obj.Placement + + obj = self.obj + boolsList = [obj] # list of booleans that are part of obj + # dynamic list that is used to figure out when we've iterated over all + # subobjects that are booleans + tmpList = [obj] + ref1 = {} # first solid exporter + ref2 = {} # second solid exporter + while len(tmpList) > 0: + boolobj = tmpList.pop() + solidExporter = SolidExporter.getExporter(boolobj.Base) + ref1[boolobj] = solidExporter + if self.isBoolean(boolobj.Base): + tmpList.append(boolobj.Base) + boolsList.append(boolobj.Base) + + solidExporter = SolidExporter.getExporter(boolobj.Tool) + ref2[boolobj] = solidExporter + if self.isBoolean(boolobj.Tool): + tmpList.append(boolobj.Tool) + boolsList.append(boolobj.Tool) + + # Now tmpList is empty and boolsList has list of all booleans + self.region = Region("") + self.surfaces = [] + for boolobj in reversed(boolsList): + operation = self.boolOperation(boolobj) + if operation is None: + continue + solidName = boolobj.Label + solidExporter1 = ref1[boolobj] + solidExporter2 = ref2[boolobj] + region1 = solidExporter1.get_region() + region2 = solidExporter2.get_region() + + self.surfaces += solidExporter1.surfaces + self.surfaces += solidExporter2.surfaces + + if operation == 'union': + self.region = region1.union(region2) + elif operation == 'subtraction': + self.region = region1.cut(region2) + elif operation == 'common': + self.region = region1.intersection(region2) + + if objPlacement != FreeCAD.Placement(): + translation = objPlacement.Base + rotation = objPlacement.Rotation + for surf in self.surfaces: + surf.rotate(rotation) + surf.translate(translation) + + +class GDMLBoxExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + + def generate_surfaces(self): + mul = self.getMult() + normal = Vector(1, 0, 0) + D = -self.obj.x/2 * mul + x1surface = PlaneSurfaceExporter(f"{self.obj.Label}_x1_plane", normal, D) + D = self.obj.x/2 * mul + x2surface = PlaneSurfaceExporter(f"{self.obj.Label}_x2_plane", normal, D) + + normal = Vector(0, 1, 0) + D = -self.obj.y/2 * mul + y1surface = PlaneSurfaceExporter(f"{self.obj.Label}_y1_plane", normal, D) + D = self.obj.y/2 * mul + y2surface = PlaneSurfaceExporter(f"{self.obj.Label}_y2_plane", normal, D) + + normal = Vector(0, 0, 1) + D = -self.obj.z/2 * mul + z1surface = PlaneSurfaceExporter(f"{self.obj.Label}_z1_plane", normal, D) + D = self.obj.z/2 * mul + z2surface = PlaneSurfaceExporter(f"{self.obj.Label}_z2_plane", normal, D) + + self.surfaces = [x1surface, x2surface, y1surface, y2surface, z1surface, z2surface] + self.region = Region(f"+{x1surface.id} -{x2surface.id} +{y1surface.id} -{y2surface.id} +{z1surface.id} -{z2surface.id}") + + self.position_globally() + + +class GDMLConeExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + + def generate_surfaces(self): + from .GDMLObjects import checkFullCircle, getAngleRad + def cone_surface(name_suffix, r1, r2, h): + theta = math.atan(abs(r1-r2)/h) + if abs(r1-r2) < 1.0e-9: # a cylinder, not a cone + return CylinderSurfaceExporter(self.name, Vector(0, 0, 0), Vector(0, 0, 1), r1) + + if r1 > r2: # cone base at bottom, cone vertex at top + H = r1/math.tan(theta) # Height of cone, from base to vertex + axis = Vector(0, 0, 1) # has to be z-axis for rotations to work correcly + center = (H - h/2) * Vector(0, 0, 1) # center is location of vertex + return ConeSurfaceExporter(name+name_suffix, center, axis, theta) + else: # r1 0: + region_expr += f"+{phi0_plane.id} -{phi1_plane.id}" + else: + region_expr += f"(+{phi0_plane.id} | -{phi1_plane.id})" + + self.region = Region(region_expr) + self.position_globally() + + +class GDMLcutTubeExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + + def generate_surfaces(self): + from .GDMLObjects import checkFullCircle, getAngleRad + + region_expr = "" + axis = Vector(0, 0, 1) + mul = self.getMult() + center = Vector(0, 0, 0) + radius = self.obj.rmin * mul + if radius != 0: + surf = CylinderSurfaceExporter(f"{self.name()}_ir", center, axis, radius) + self.surfaces.append(surf) + region_expr += f"+{surf.id} " + + radius = self.obj.rmax * mul + surf = CylinderSurfaceExporter(f"{self.name()}_or", center, axis, radius) + self.surfaces.append(surf) + region_expr += f"-{surf.id} " + + normal_top = Vector(self.obj.highX, self.obj.highY, self.obj.highZ) + normal_top.normalize() + D_top = Vector(0, 0, mul*self.obj.z/2).dot(normal_top) + surf = PlaneSurfaceExporter(f"{self.name()}_top", normal_top, D_top) + self.surfaces.append(surf) + region_expr += f"-{surf.id} " + + normal_bot = Vector(self.obj.lowX, self.obj.lowY, self.obj.lowZ) + normal_bot.normalize() + # D_bot = mul*self.obj.z/2 # not minus because the normal at the bottom points away from the bottom + # this is better calculated as Vector(0, 0, -z/2).dot(nromal) + D_bot = Vector(0, 0, -mul*self.obj.z/2).dot(normal_bot) + surf = PlaneSurfaceExporter(f"{self.name()}_bot", normal_bot, D_bot) + self.surfaces.append(surf) + region_expr += f"-{surf.id} " + + if not checkFullCircle(self.obj.aunit, self.obj.deltaphi): + startPhi = getAngleRad(self.obj.aunit, self.obj.startphi) + nx = -math.sin(startPhi) + ny = math.cos(startPhi) + phi0_normal = Vector(nx, ny, 0) + D = 0 + phi0_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi0_plane", phi0_normal, D) + self.surfaces.append(phi0_plane) + + deltaphi = getAngleRad(self.obj.aunit, self.obj.deltaphi) + endphi = startPhi + deltaphi + nx = -math.sin(endphi) + ny = math.cos(endphi) + phi1_normal = Vector(nx, ny, 0) + + phi1_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi1_plane", phi1_normal, D) + self.surfaces.append(phi1_plane) + + if phi0_normal.cross(phi1_normal).z > 0: + region_expr += f"+{phi0_plane.id} -{phi1_plane.id}" + else: + region_expr += f"(+{phi0_plane.id} | -{phi1_plane.id})" + + + self.region = Region(region_expr) + self.position_globally() + + +class GDMLElConeExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + + def generate_surfaces(self): + mul = self.getMult() + dx = self.obj.dx + dy = self.obj.dy + zcut = mul*self.obj.zcut + zmax = mul*self.obj.zmax + name = NameManager.getName(self.obj) + center = Vector(0, 0, 0) + surf = EllipticalConeSurfaceExporter(name+'_cone', center, dx, dy, zmax) + self.surfaces.append(surf) + + normal = Vector(0, 0, 1) + bot_plane = PlaneSurfaceExporter(name+'_bot', normal, -zcut) + top_plane = PlaneSurfaceExporter(name+'_top', normal, zcut) + self.surfaces += [bot_plane, top_plane] + + self.region = Region(f"-{surf.id} +{bot_plane.id} -{top_plane.id}") + + self.position_globally() + + +class GDMLEllipsoidExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + + + def generate_surfaces(self): + mul = self.getMult() + name = NameManager.getName(self.obj) + surf1 = EllipsoidSurfaceExporter(name, mul*self.obj.ax, mul*self.obj.by, mul*self.obj.cz) + self.surfaces = [surf1] + region_expr = f"-{surf1.id}" + if hasattr(self.obj, 'zcut1'): + bot_plane = PlaneSurfaceExporter(name+'_zcut1', Vector(0, 0, 1), self.obj.zcut1) + self.surfaces.append(bot_plane) + region_expr += f' +{bot_plane.id}' + if hasattr(self.obj, 'zcut2'): + top_plane = PlaneSurfaceExporter(name+'_zcut2', Vector(0, 0, 1), self.obj.zcut2) + self.surfaces.append(top_plane) + region_expr += f' -{top_plane.id}' + + self.region = Region(region_expr) + self.position_globally() + + +class GDMLHypeExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + + def generate_surfaces(self): + from .GDMLObjects import getAngleRad + + name = NameManager.getName(self.obj) + + mul = self.getMult() + name = NameManager.getName(self.obj) + rmin = mul*self.obj.rmin + rmax = mul*self.obj.rmax + z = mul*self.obj.z + outst = getAngleRad(self.obj.aunit, self.obj.outst) + inst = getAngleRad(self.obj.aunit, self.obj.inst) + + # this should probably be a global variable, but + # for now adopt the value used in geant4.10.07.p02 + NUMBER_OF_DIVISIONS = 36 + sqrtan1 = math.tan(inst) + sqrtan1 *= sqrtan1 + sqrtan2 = math.tan(outst) + sqrtan2 *= sqrtan2 + + # Prepare two polylines + ns = NUMBER_OF_DIVISIONS + if sqrtan1 == 0.0: + nz1 = 2 + else: + nz1 = ns + 1 + if sqrtan2 == 0.0: + nz2 = 2 + else: + nz2 = ns + 1 + + halfZ = z / 2 + # + # solid generated by external hyperbeloid + dz2 = z / (nz2 - 1) + zz = [halfZ - dz2 * i for i in range(0, nz2)] + rr = [math.sqrt(sqrtan2 * zi * zi + rmax * rmax) for zi in zz] + + self.region = Region("") + + planes_dict = {} + outer_region = Region("") + for i in range(0, nz2-1): + v0 = Vector(rr[i], 0, zz[i]) + v1 = Vector(rr[i+1], 0, zz[i+1]) + surface = cone_from_line_segment(v0, v1) + self.surfaces.append(surface) + edge_region = f"-{surface.id} " + for v in [v0, v1]: + if v.z in planes_dict: + plane = planes_dict[v.z] + else: + plane = PlaneSurfaceExporter(", Vector(0, 0, 1", v.z) + self.surfaces.append(plane) + if v==v0: + edge_region += f"+{plane.id} " + else: + edge_region += f"-{plane.id} " + outer_region = outer_region.union(edge_region) + + self.region = outer_region + + if rmin != 0: + # + # solid generated by internal hyperboloid + dz1 = z / (nz1 - 1) + zz = [halfZ - dz1 * i for i in range(0, nz1)] + rr = [math.sqrt(sqrtan1 * zi * zi + rmin * rmin) for zi in zz] + + inner_region = Region("") + for i in range(0, nz1-1): + v0 = Vector(rr[i], 0, zz[i]) + v1 = Vector(rr[i+1], 0, zz[i+1]) + surface = cone_from_line_segment(v0, v1) + self.surfaces.append(surface) + edge_region = f"-{surface.id} " + for v in [v0, v1]: + if v.z in planes_dict: + plane = planes_dict[v.z] + else: + plane = PlaneSurfaceExporter(", Vector(0, 0, 1", v.z) + self.surfaces.append(plane) + if v == v0: + edge_region += f"+{plane.id} " + else: + edge_region += f"-{plane.id} " + inner_region = inner_region.union(edge_region) + + self.region = self.region.cut(inner_region) + + top_plane = PlaneSurfaceExporter(name+'_top', Vector(0, 0, 1), z/2) + bot_plane = PlaneSurfaceExporter(name+'_top', Vector(0, 0, 1), -z/2) + self.surfaces += [top_plane, bot_plane] + self.region = self.region.intersection(Region(f"-{top_plane.id} +{bot_plane.id}")) + + self.position_globally() + + +class GDMLParaboloidExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + + def generate_surfaces(self): + mul = self.getMult() + name = NameManager.getName(self.obj) + rlo = mul*self.obj.rlo + rhi = mul*self.obj.rhi + dz = mul*self.obj.dz + + surf = ParaboloidSurfaceExporter(name, rlo, rhi, dz) + top_surface = PlaneSurfaceExporter(name+'_top', Vector(0, 0, 1), dz) + bot_surface = PlaneSurfaceExporter(name+'_bot', Vector(0, 0, 1), -dz) + + self.surfaces = [surf, top_surface, bot_surface] + self.region = Region(f"-{surf.id} +{bot_surface.id} -{top_surface.id}") + + self.position_globally() + + +class GDMLOrbExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + + def generate_surfaces(self): + mul = self.getMult() + center = Vector(0, 0, 0) + radius = self.obj.rmin * mul + surf = SphereSurfaceExporter(f"{self.name()}", center, radius) + self.surfaces.append(surf) + self.region = Region(f"-{surf.id}") + self.position_globally() + + +class GDMLPolyconeExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + + def generate_surfaces(self): + from .GDMLObjects import getAngleRad + + name = NameManager.getName(self.obj) + mul = self.getMult() + planes_dict = {} + phi0 = getAngleRad(self.obj.aunit, self.obj.startphi) + deltaphi = getAngleRad(self.obj.aunit, self.obj.deltaphi) + phi1 = phi0 + deltaphi + + zplanes = self.obj.OutList + num_zplanes = len(zplanes) + + self.region = Region("") + + normal = Vector(0, 0, 1) + for i in range(num_zplanes - 1): + region = "" + + zplane0 = zplanes[i] + zplane1 = zplanes[i + 1] + + rmin0 = mul * zplane0.rmin + rmax0 = mul * zplane0.rmax + z0 = mul * zplane0.z + + rmin1 = mul * zplane1.rmin + rmax1 = mul * zplane1.rmax + z1 = mul * zplane1.z + if rmin0 != 0 and rmin1 != 0: + v0 = Vector(rmin0, 0, z0) + v1 = Vector(rmin1, 0, z1) + surface = cone_from_line_segment(v0, v1) + self.surfaces.append(surface) + region += f"+{surface.id} " + + if rmax0 != 0 and rmax1 != 0: + v0 = Vector(rmax0, 0, z0) + v1 = Vector(rmax1, 0, z1) + surface = cone_from_line_segment(v0, v1) + self.surfaces.append(surface) + region += f"-{surface.id} " + + if z0 in planes_dict: + bot_plane = planes_dict[z0] + else: + bot_plane = PlaneSurfaceExporter(name+'_bot', normal, z0) + planes_dict[z0] = bot_plane + self.surfaces.append(bot_plane) + + if z1 in planes_dict: + top_plane = planes_dict[z1] + else: + top_plane = PlaneSurfaceExporter(name+'_top', normal, z1) + planes_dict[z1] = top_plane + self.surfaces.append(top_plane) + + region += f"+{bot_plane.id} -{top_plane.id} " + + self.region = self.region.union(Region(region)) + + + if deltaphi < 2*math.pi: + phi0_normal = Vector(math.sin(phi0), -math.cos(phi0), 0) + phi1_normal = Vector(math.sin(phi1), -math.cos(phi1), 0) + phi0_plane = PlaneSurfaceExporter(name+'_phi0', phi0_normal, 0) + phi1_plane = PlaneSurfaceExporter(name+'_phi1', phi1_normal, 0) + self.surfaces += [phi0_plane, phi1_plane] + if phi0_normal.cross(phi1_normal).z > 0: + region = f" +{phi0_plane.id} -{phi1_plane.id}" + else: + region = f"( +{phi0_plane.id} | -{phi1_plane.id})" + + self.region = self.region.intersection(Region(region)) + + self.position_globally() + + +class GDMLGenericPolyconeExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + + def generate_surfaces(self): + from .GDMLObjects import getAngleRad + + name = NameManager.getName(self.obj) + mul = self.getMult() + planes_dict = {} + phi0 = getAngleRad(self.obj.aunit, self.obj.startphi) + deltaphi = getAngleRad(self.obj.aunit, self.obj.deltaphi) + phi1 = phi0 + deltaphi + + zplanes = self.obj.OutList + num_zplanes = len(zplanes) + + self.region = Region("") + + normal = Vector(0, 0, 1) + for i in range(num_zplanes): + region = "" + + zplane0 = zplanes[i] + zplane1 = zplanes[(i + 1) % num_zplanes] + + r0 = mul * zplane0.r + z0 = mul * zplane0.z + + r1 = mul * zplane1.r + z1 = mul * zplane1.z + + v0 = Vector(r0, 0, z0) + v1 = Vector(r1, 0, z1) + surface = cone_from_line_segment(v0, v1) + self.surfaces.append(surface) + + if i == num_zplanes - 1: + region += f"+{surface.id} " + else: + region += f"-{surface.id} " + + + if z0 in planes_dict: + bot_plane = planes_dict[z0] + else: + bot_plane = PlaneSurfaceExporter(name+'_bot', normal, z0) + planes_dict[z0] = bot_plane + self.surfaces.append(bot_plane) + + if z1 in planes_dict: + top_plane = planes_dict[z1] + else: + top_plane = PlaneSurfaceExporter(name+'_top', normal, z1) + planes_dict[z1] = top_plane + self.surfaces.append(top_plane) + + if i < num_zplanes - 1: + region += f"+{bot_plane.id} -{top_plane.id} " + self.region = self.region.union(Region(region)) + else: + self.region = self.region.intersection(Region(region)) + + + if deltaphi < 2*math.pi: + phi0_normal = Vector(math.sin(phi0), -math.cos(phi0), 0) + phi1_normal = Vector(math.sin(phi1), -math.cos(phi1), 0) + phi0_plane = PlaneSurfaceExporter(name+'_phi0', phi0_normal, 0) + phi1_plane = PlaneSurfaceExporter(name+'_phi1', phi1_normal, 0) + self.surfaces += [phi0_plane, phi1_plane] + if phi0_normal.cross(phi1_normal).z > 0: + region = f" +{phi0_plane.id} -{phi1_plane.id}" + else: + region = f"( +{phi0_plane.id} | -{phi1_plane.id})" + + self.region = self.region.intersection(Region(region)) + + self.position_globally() + + +class GDMLSphereExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + + def generate_surfaces(self): + from .GDMLObjects import checkFullCircle, getAngleRad + + mul = self.getMult() + center = Vector(0, 0, 0) + radius = self.obj.rmin * mul + if radius != 0: + inner_surface = SphereSurfaceExporter(f"{self.name()}_ir", center, radius) + self.surfaces.append(inner_surface) + else: + inner_surface = None + + radius = self.obj.rmax * mul + outer_surface = SphereSurfaceExporter(f"{self.name()}_or", + center, radius) + self.surfaces.append(outer_surface) + + if not checkFullCircle(self.obj.aunit, self.obj.deltaphi): + startPhi = getAngleRad(self.obj.aunit, self.obj.startphi) + nx = -math.sin(startPhi) + ny = math.cos(startPhi) + phi0_normal = Vector(nx, ny, 0) + D = 0 + + phi0_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi0_plane", phi0_normal, D) + self.surfaces.append(phi0_plane) + + deltaphi = getAngleRad(self.obj.aunit, self.obj.deltaphi) + endphi = startPhi + deltaphi + nx = -math.sin(endphi) + ny = math.cos(endphi) + phi1_normal = Vector(nx, ny, 0) + + phi1_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi1_plane", phi1_normal, D) + self.surfaces.append(phi1_plane) + + startTheta = getAngleRad(self.obj.aunit, self.obj.starttheta) + deltaTheta = getAngleRad(self.obj.aunit, self.obj.deltatheta) + endTheta = startTheta + deltaTheta + if endTheta > math.pi: + endTheta = math.pi + + cones_region = "" + if startTheta > 0: + center = Vector(0, 0, 0) + axis = Vector(0, 0, 1) + mid_plane = PlaneSurfaceExporter(f"{self.obj.Label}_mid_plane", axis, 0) + + cone1_surface = None + if startTheta < math.pi/2: + cone1_surface = ConeSurfaceExporter(f"{self.obj.Label}_thet1_cone", center, axis, startTheta) + elif startTheta > math.pi/2: + cone1_surface = ConeSurfaceExporter(f"{self.obj.Label}_thet1_cone", center, axis, math.pi-startTheta) + + cone2_surface = None + if endTheta < math.pi/2: + cone2_surface = ConeSurfaceExporter(f"{self.obj.Label}_thet2_cone", center, axis, endTheta) + elif endTheta > math.pi/2 and endTheta != math.pi: + cone2_surface = ConeSurfaceExporter(f"{self.obj.Label}_thet2_cone", center, axis, math.pi-endTheta) + + if cone1_surface is not None: + self.surfaces.append(cone1_surface) + if cone2_surface is not None: + self.surfaces.append(cone2_surface) + self.surfaces. append(mid_plane) + + # three cases: + # case 1, startTheta, endTheta < 90 deg + if (0 < startTheta < math.pi/2) and (0 < endTheta < math.pi/2): + cones_region = f"+{cone1_surface.id} -{cone2_surface.id} +{mid_plane.id}" + + # case 2 startTheta < 90, endTheta > 90 + elif (0 < startTheta < math.pi/2) and (math.pi/2 < endTheta < math.pi): + cones_region = f"(+{mid_plane.id} +{cone1_surface.id}) | (-{mid_plane.id} +{cone2_surface.id})" + + # case 3, startTheta, endTheta > 90 deg + elif (math.pi/2 < startTheta < math.pi) and (math.pi/2 < endTheta < math.pi): + cones_region = f"+{cone2_surface.id} -{cone1_surface.id} -{mid_plane.id}" + + # edge cases: one or the other of starTheta, endTheta = math.pi/2, math.pi + elif (startTheta == math.pi/2) and (endTheta < math.pi): + cones_region = f"-{mid_plane.id} +{cone2_surface.id}" + + elif (0 < startTheta < math.pi/2) and endTheta == math.pi/2: + cones_region = f"+{cone1_surface.id} +{mid_plane.id}" + + elif startTheta == math.pi/2 and endTheta == math.pi: + cones_region = f"-{mid_plane.id}" + + elif startTheta > math.pi/2 and endTheta == math.pi: + cones_region = f"-{cone1_surface.id} -{mid_plane.id}" + + elif startTheta == 0 and endTheta != math.pi: + center = Vector(0, 0, 0) + axis = Vector(0, 0, 1) + mid_plane = PlaneSurfaceExporter(f"{self.obj.Label}_mid_plane", axis, 0) + self.surfaces.append(mid_plane) + + if endTheta < math.pi/2: + cone2_surface = ConeSurfaceExporter(f"{self.obj.Label}_thet2_cone", center, axis, endTheta) + self.surfaces.append(cone2_surface) + cones_region = f"-{cone2_surface.id} +{mid_plane.id}" + elif endTheta > math.pi/2: + cone2_surface = ConeSurfaceExporter(f"{self.obj.Label}_thet2_cone", center, axis, math.pi - endTheta) + self.surfaces.append(cone2_surface) + cones_region = f"+{cone2_surface.id} | +{mid_plane.id}" + else: # endTheta == pi/2 + cones_region = f"+{mid_plane.id}" + + region = f"-{outer_surface.id}" + if inner_surface is not None: + region += f" +{inner_surface.id}" + + if not checkFullCircle(self.obj.aunit, self.obj.deltaphi): + if phi0_normal.cross(phi1_normal).z > 0: + region += f" +{phi0_plane.id} -{phi1_plane.id}" + else: + region += f" (+{phi0_plane.id} | -{phi1_plane.id})" + + + self.region = Region(region) + if cones_region != "": + self.region = self.region.intersection(Region(cones_region)) + + + self.position_globally() + + +def generate_half_loop_surfaces(loop_half, surfaces): + ''' + generate surfaces needed for the half loop(circle). + :param: loop_half: vertexes of the half circle + :return: The region of the surfaces + ''' + + Nvert = len(loop_half) + region = Region("") + # Instead of producing a bottom and top planes for each edge, we produce + # only one plane per vertex. To keep track if we produced a plane for a given + # vertex, we use a dictionary plane_dict[vertex.z = surface. See below of how we decide the region + planes_dict = {} + for i in range(0, Nvert - 1): + v0 = loop_half[i] + v1 = loop_half[i + 1] + outer = v1.z > v0.z + surface = cone_from_line_segment(v0, v1) + if surface is not None: + surfaces.append(surface) + if outer: + edge_region = f"-{surface.id} " + else: + edge_region = f"+{surface.id} " + + for v in [v0, v1]: + if v.z in planes_dict: + plane = planes_dict[v.z] + else: + plane = PlaneSurfaceExporter("", Vector(0, 0, 1), v.z) + surfaces.append(plane) + planes_dict[v.z] = plane + if outer and v == v0: + plane_region = f"+{plane.id} " + elif outer and v == v1: + plane_region = f"-{plane.id} " + elif (not outer) and v == v0: + plane_region = f"-{plane.id} " + elif (not outer) and v == v1: + plane_region = f"+{plane.id} " + + edge_region += plane_region + + region = region.union(Region(edge_region)) + + return region + +class GDMLTorusExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + deviation = obj.ViewObject.Deviation/100 + costhet = 1 - deviation + thet = math.acos(costhet) + N = int(2*math.pi/(2*thet)) # number of divisions of the circle that that given a maximum radial difference + # if Deviation, in percent + # make it even + self.N = 2*int(N/2) + + + def inner_outer(self, center, radius): + ''' Return two ploylines, one is the outer (-90, 90 deg) + and one the inner (90, 270 degrees) + ''' + thet = -math.pi/2 + dethet = 2*math.pi/(self.N) + verts = [] + for i in range(self.N+1): + x = radius * math.cos(thet) + z = radius * math.sin(thet) + verts.append(center + Vector(x, 0, z)) + thet += dethet + + outer = verts[:int(self.N/2)+1] + inner = verts[int(self.N/2):] + return inner, outer + + def generate_circle_revolve(self, radius): + mul = self.getMult() + center = mul * self.obj.rtor * Vector(1, 0, 0) + inner, outer = self.inner_outer(center, mul * radius) + outer_region = generate_half_loop_surfaces(outer, self.surfaces) + inner_region = generate_half_loop_surfaces(inner, self.surfaces) + + region = outer_region.intersection(inner_region) + return region + + def generate_surfaces(self): + from .GDMLObjects import checkFullCircle, getAngleRad + + self.region = self.generate_circle_revolve(self.obj.rmax) + if self.obj.rmin > 0: + inside_region = self.generate_circle_revolve(self.obj.rmin) + self.region = self.region.cut(inside_region) + + if not checkFullCircle(self.obj.aunit, self.obj.deltaphi): + startPhi = getAngleRad(self.obj.aunit, self.obj.startphi) + nx = -math.sin(startPhi) + ny = math.cos(startPhi) + phi0_normal = Vector(nx, ny, 0) + D = 0 + + phi0_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi0_plane", phi0_normal, D) + self.surfaces.append(phi0_plane) + + deltaphi = getAngleRad(self.obj.aunit, self.obj.deltaphi) + endphi = startPhi + deltaphi + nx = -math.sin(endphi) + ny = math.cos(endphi) + phi1_normal = Vector(nx, ny, 0) + + phi1_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi1_plane", phi1_normal, D) + self.surfaces.append(phi1_plane) + + if phi0_normal.cross(phi1_normal).z > 0: + self.region = self.region.intersection(Region(f"+{phi0_plane.id} -{phi1_plane.id}")) + else: + self.region = self.region.intersection(Region(f"(+{phi0_plane.id} | -{phi1_plane.id})")) + + self.position_globally() + + +class TorusExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + deviation = self.obj.ViewObject.Deviation/100 + costhet = 1 - deviation + thet = math.acos(costhet) + self.dthet = 2*thet + + def get_verts(self): + ''' this is quite a bit more involved than appears''' + verts = [] + center = self.obj.Radius1.Value * Vector(1, 0, 0) + angle1 = math.radians(self.obj.Angle1) + angle2 = math.radians(self.obj.Angle2) + + v1 = Vector(math.cos(angle1), 0, -math.sin(angle1)) # unit vector in direction of angle1 + v2 = Vector(math.cos(angle2), 0, -math.sin(angle2)) # unit vector in direction of angle2 + # angular span is angle from v1 to v2, in CW direction. + sinthet = v1.cross(v2).y + thet = math.atan2(sinthet, v1.dot(v2)) + if thet < 0: + thet = 2*math.pi + thet + + N = int(thet/self.dthet) + 1 + if N < 2: + N = 2 + + radius = self.obj.Radius2.Value + dthet = thet/(N-1) # this is local + if thet < 2*math.pi: + verts.append(center) + + verts += [center + radius * Vector(math.cos(angle1+i*dthet), 0, -math.sin(angle1+i*dthet)) for i in range(N)] + + return verts + + def process_convex_polygon(self, verts): + + # generate the surfaces and region (one region!) for a convex polygon in the + # x-z plane. The assumption is that the verts are in CCW order. One needs to specify + # exactly what we mean by CCW in the x-z plane. If I look at the x-z plane, such that x is to my right + # and z upward, then a polygon with xy verts arranged CCW is one for which for, for two consecutive edges + # e1 and e2, e1 x e2 is in the -y direction. Yes, minus y, not positive y. + N = len(verts) + region_expr = "" + zmin = min([v.z for v in verts]) + zmax = max([v.z for v in verts]) + + for i in range(N): + v0 = verts[i] + v1 = verts[(i+1) % N] + surf = cone_from_line_segment(v0, v1) + if surf is None: + continue + self.surfaces.append(surf) + # now we have to worry about the other half of the cone cutting out part of the polygon. + # it can do that if the vertex of the cone lies between zmin and zmax + extra_plane = None + # ugly, but we must remember surface properties are already in cm, so we need to multiply them by 10 + if isinstance(surf, ConeSurfaceExporter) and (zmin < 10*surf.center.z < zmax): + extra_plane = PlaneSurfaceExporter("", Vector(0, 0, 1), 10*surf.center.z) + self.surfaces.append(extra_plane) + + if v1.z > v0.z: # we are on the up side + region_expr += f"-{surf.id} " + + elif v1.z < v0.z: # on the down side, the extra plane can interfere + if extra_plane is not None: + if v1.z < 0: + region_expr += f"(+{surf.id} | +{extra_plane.id}) " + else: + region_expr += f"(+{surf.id} | -{extra_plane.id}) " + else: + region_expr += f"+{surf.id} " + + else: # v1.z == v0.z, the surface is a plane, either at the very top or very bottom, for the closed polygon + if v1.x < v0.x: + region_expr += f"-{surf.id} " + else: + region_expr += f"+{surf.id} " + + + self.region = self.region.union(Region(region_expr)) + + + def generate_surfaces(self): + verts = self.get_verts() + verts.reverse() + + self.region = Region("") + # do we have a concave polygon in the x-z plane? + if (self.obj.Angle2 - self.obj.Angle1) < 360: + # Not a complete circle, so there is possibility of convext polygon + radius = self.obj.Radius2.Value + center = self.obj.Radius1.Value * Vector(1, 0, 0) + angle1 = math.radians(self.obj.Angle1) + angle2 = math.radians(self.obj.Angle2) + v1 = radius * Vector(math.cos(angle1), 0, -math.sin(angle1)) + v2 = radius * Vector(math.cos(angle2), 0, -math.sin(angle2)) + if v1.cross(v2).y < 0: # This is concave, break it into two convex polygons + imid = int(len(verts)/2) + seg1 = verts[:imid+1] + seg1.append(center) + seg2 = verts[imid:] + for seg in [seg1, seg2]: + self.process_convex_polygon(seg) + else: + self.process_convex_polygon(verts) + else: + self.process_convex_polygon(verts) + + if self.obj.Angle3 != 360: + startPhi = 0 + nx = -math.sin(startPhi) + ny = math.cos(startPhi) + phi0_normal = Vector(nx, ny, 0) + D = 0 + phi0_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi0_plane", phi0_normal, D) + self.surfaces.append(phi0_plane) + + deltaphi = math.radians(self.obj.Angle3) + endphi = startPhi + deltaphi + nx = -math.sin(endphi) + ny = math.cos(endphi) + phi1_normal = Vector(nx, ny, 0) + + phi1_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi1_plane", phi1_normal, D) + self.surfaces.append(phi1_plane) + + if phi0_normal.cross(phi1_normal).z > 0: + region = f" +{phi0_plane.id} -{phi1_plane.id}" + else: + region = f" (+{phi0_plane.id} | -{phi1_plane.id})" + + self.region = self.region.intersection(Region(region)) + + self.position_globally() + + +class GDMLTubeExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + + def generate_surfaces(self): + from .GDMLObjects import checkFullCircle, getAngleRad + + normal = Vector(0, 0, 1) + mul = self.getMult() + + D = self.obj.z/2*mul + # a gdml tube has origin at its center + top_surface = PlaneSurfaceExporter(f"{self.name()}_top", normal, D) + + D = -self.obj.z/2*mul + # a gdml tube has origin at its center + bottom_surface = PlaneSurfaceExporter(f"{self.name()}_bot", normal, D) + + center = Vector(0, 0, 0) + axis = normal + radius = self.obj.rmin * mul + if radius != 0: + inner_surface = CylinderSurfaceExporter(f"{self.name()}_ir", center, axis, radius) + else: + inner_surface = None + + radius = self.obj.rmax * mul + outer_surface = CylinderSurfaceExporter(f"{self.name()}_or", center, axis, radius) + + if not checkFullCircle(self.obj.aunit, self.obj.deltaphi): + startPhi = getAngleRad(self.obj.aunit, self.obj.startphi) + nx = -math.sin(startPhi) + ny = math.cos(startPhi) + phi0_normal = Vector(nx, ny, 0) + D = 0 + phi0_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi0_plane", phi0_normal, D) + + deltaphi = getAngleRad(self.obj.aunit, self.obj.deltaphi) + endphi = startPhi + deltaphi + nx = -math.sin(endphi) + ny = math.cos(endphi) + phi1_normal = Vector(nx, ny, 0) + + phi1_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi1_plane", phi1_normal, D) + + self.surfaces = [top_surface, bottom_surface, outer_surface] + if inner_surface is not None: + self.surfaces.append(inner_surface) + + if not checkFullCircle(self.obj.aunit, self.obj.deltaphi): + self.surfaces += [phi0_plane, phi1_plane] + + + region = f"+{bottom_surface.id} -{top_surface.id} -{outer_surface.id}" + if inner_surface is not None: + region += f" +{inner_surface.id}" + + if not checkFullCircle(self.obj.aunit, self.obj.deltaphi): + if phi0_normal.cross(phi1_normal).z > 0: + region += f" +{phi0_plane.id} -{phi1_plane.id}" + else: + region += f" (+{phi0_plane.id} | -{phi1_plane.id})" + + self.region = Region(region) + self.position_globally() + + +class GDMLTwistedSolidExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + self.slice = 0 + self.dTwist = obj.ViewObject.AngularDeflection.Value + self.dTwist = min(self.dTwist, 5) + + +class GDMLTwistedboxExporter(GDMLTwistedSolidExporter): + def __init__(self, obj): + super().__init__(obj) + + def make4Walls(self, x, y): + name = NameManager.getName(self.obj) + surf1 = PlaneSurfaceExporter(f"{name}_{self.slice}_1", Vector(1, 0, 0), x/2) + surf2 = PlaneSurfaceExporter(f"{name}_{self.slice}_2", Vector(0, 1, 0), y/2) + surf3 = PlaneSurfaceExporter(f"{name}_{self.slice}_3", Vector(-1, 0, 0), x/2) + surf4 = PlaneSurfaceExporter(f"{name}_{self.slice}_4", Vector(0, -1, 0), y/2) + + return [surf1, surf2, surf3, surf4] + + def generate_surfaces(self): + from .GDMLObjects import getAngleDeg + mul = self.getMult() + + x = mul * self.obj.x + y = mul * self.obj.y + z = mul * self.obj.z + + angle = getAngleDeg(self.obj.aunit, self.obj.PhiTwist) + dPhi = self.dTwist # 2 degree rotation per step + N = int(angle/dPhi) + N = max(N, 10) + dz = z /N + + planes_dict = {} + region_expr = "" + z0 = -z/2 + for i in range(0, N): + name = NameManager.getName(self.obj) + walls = self.make4Walls(x, y) + self.surfaces += walls + if z0 in planes_dict: + bot_plane = planes_dict[z0] + else: + bot_plane = PlaneSurfaceExporter(f"{name}_{self.slice}_bot", Vector(0, 0, 1), z0) + planes_dict[z0] = bot_plane + self.surfaces.append(bot_plane) + + z0 += dz + if z0 in planes_dict: + top_plane = planes_dict[z0] + else: + top_plane = PlaneSurfaceExporter(f"{name}_{self.slice}_top", Vector(0, 0, 1), z0) + planes_dict[z0] = top_plane + self.surfaces.append(top_plane) + + if region_expr == "": + region_expr = f"(-{walls[0].id} -{walls[1].id} -{walls[2].id} -{walls[3].id} +{bot_plane.id} -{top_plane.id})" + else: + region_expr += f" | (-{walls[0].id} -{walls[1].id} -{walls[2].id} -{walls[3].id} +{bot_plane.id} -{top_plane.id})" + + rot = FreeCAD.Rotation(Vector(0, 0, 1), -angle / 2 + i * dPhi) + for wall in walls: + wall.rotate(rot) + self.slice += 1 + + self.region = Region(region_expr) + + self.position_globally() + + +class GDMLTwistedtrdExporter(GDMLTwistedSolidExporter): + def __init__(self, obj): + super().__init__(obj) + + def make4Walls(self, x, y): + name = NameManager.getName(self.obj) + surf1 = PlaneSurfaceExporter(f"{name}_{self.slice}_1", Vector(1, 0, 0), x/2) + surf2 = PlaneSurfaceExporter(f"{name}_{self.slice}_2", Vector(0, 1, 0), y/2) + surf3 = PlaneSurfaceExporter(f"{name}_{self.slice}_3", Vector(-1, 0, 0), x/2) + surf4 = PlaneSurfaceExporter(f"{name}_{self.slice}_4", Vector(0, -1, 0), y/2) + + return [surf1, surf2, surf3, surf4] + + def generate_surfaces(self): + from .GDMLObjects import getAngleDeg + + mul = self.getMult() + + x1 = self.obj.x1 * mul + x2 = self.obj.x2 * mul + y1 = self.obj.y1 * mul + y2 = self.obj.y2 * mul + z = self.obj.z * mul + + angle = getAngleDeg(self.obj.aunit, self.obj.PhiTwist) + dPhi = self.dTwist # 2 degree rotation per step + N = int(angle/dPhi) + N = max(N, 10) + dz = z /N + + planes_dict = {} + region_expr = "" + z0 = -z/2 + for i in range(0, N): + t = i * 1.0 / (N - 1) + xside = x1 + t * (x2 - x1) + yside = y1 + t * (y2 - y1) + + name = NameManager.getName(self.obj) + walls = self.make4Walls(xside, yside) + self.surfaces += walls + if z0 in planes_dict: + bot_plane = planes_dict[z0] + else: + bot_plane = PlaneSurfaceExporter(f"{name}_{self.slice}_bot", Vector(0, 0, 1), z0) + planes_dict[z0] = bot_plane + self.surfaces.append(bot_plane) + + z0 += dz + if z0 in planes_dict: + top_plane = planes_dict[z0] + else: + top_plane = PlaneSurfaceExporter(f"{name}_{self.slice}_top", Vector(0, 0, 1), z0) + planes_dict[z0] = top_plane + self.surfaces.append(top_plane) + + if region_expr == "": + region_expr = f"(-{walls[0].id} -{walls[1].id} -{walls[2].id} -{walls[3].id} +{bot_plane.id} -{top_plane.id})" + else: + region_expr += f" | (-{walls[0].id} -{walls[1].id} -{walls[2].id} -{walls[3].id} +{bot_plane.id} -{top_plane.id})" + + rot = FreeCAD.Rotation(Vector(0, 0, 1), -angle / 2 + i * dPhi) + for wall in walls: + wall.rotate(rot) + self.slice += 1 + + self.region = Region(region_expr) + + self.position_globally() + + +class GDMLTwistedtrapExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + + def make4Walls(self, verts): + name = NameManager.getName(self.obj) + n = len(verts) + surfaces = [] + for i in range(n): + i1 = (i+1) % n + v0 = verts[i] + v1 = verts[i1] + dx = (v1-v0).x + dy = (v1-v0).y + normal = Vector(dy, -dx, 0) + normal.normalize() + D = v0.dot(normal) + + surf = PlaneSurfaceExporter(f"{name}_{self.slice}_1", normal, D) + surfaces.append(surf) + + return surfaces + + def generate_surfaces(self): + from .GDMLObjects import getAngleRad, getAngleDeg + + mul = self.getMult() + alpha = getAngleRad(self.obj.aunit, self.obj.Alph) + theta = getAngleRad(self.obj.aunit, self.obj.Theta) + phi = getAngleRad(self.obj.aunit, self.obj.Phi) + PhiTwist = getAngleDeg(self.obj.aunit, self.obj.PhiTwist) + y1 = mul * self.obj.y1 + x1 = mul * self.obj.x1 + x2 = mul * self.obj.x2 + y2 = mul * self.obj.y2 + x3 = mul * self.obj.x3 + x4 = mul * self.obj.x4 + z = mul * self.obj.z + + dTwist = self.dTwist + N = int(PhiTwist/dTwist) + N = max(N, 10) + dz = z /N + + tanalpha = math.tan(alpha) + + dt = 1.0 /N + t = 0 + + tanthet = math.tan(theta) + cosphi = math.cos(phi) + sinphi = math.sin(phi) + rhomax = z * tanthet + xoffset = -rhomax * cosphi / 2 + yoffset = -rhomax * sinphi / 2 + + planes_dict = {} + region_expr = "" + + for i in range(N): + # Vertexes, counter clock wise order + y = y1 + t * (y2 - y1) # go continuously from y1 to y2 + dx = y * tanalpha + x13 = x1 + t * (x3 - x1) # go continuously from x1 to x3 + x24 = x2 + t * (x4 - x2) # go continuously from x1 to x3 + zt = -z / 2 + t * z + rho = i * dz * tanthet + dxphi = xoffset + rho * cosphi + dyphi = yoffset + rho * sinphi + v1 = FreeCAD.Vector(-x13 / 2 - dx / 2 + dxphi, -y / 2 + dyphi, zt) + v2 = FreeCAD.Vector(x13 / 2 - dx / 2 + dxphi, -y / 2 + dyphi, zt) + v3 = FreeCAD.Vector(x24 / 2 + dx / 2 + dxphi, y / 2 + dyphi, zt) + v4 = FreeCAD.Vector(-x24 / 2 + dx / 2 + dxphi, y / 2 + dyphi, zt) + p = Part.makePolygon([v1, v2, v3, v4, v1]) + name = NameManager.getName(self.obj) + walls = self.make4Walls([v1, v2, v3, v4]) + + self.surfaces += walls + if zt in planes_dict: + bot_plane = planes_dict[zt] + else: + bot_plane = PlaneSurfaceExporter(f"{name}_{self.slice}_bot", Vector(0, 0, 1), zt) + planes_dict[zt] = bot_plane + self.surfaces.append(bot_plane) + + zt += dz + if zt in planes_dict: + top_plane = planes_dict[zt] + else: + top_plane = PlaneSurfaceExporter(f"{name}_{self.slice}_top", Vector(0, 0, 1), zt) + planes_dict[zt] = top_plane + self.surfaces.append(top_plane) + + if region_expr == "": + region_expr = f"(-{walls[0].id} -{walls[1].id} -{walls[2].id} -{walls[3].id} +{bot_plane.id} -{top_plane.id})" + else: + region_expr += f" | (-{walls[0].id} -{walls[1].id} -{walls[2].id} -{walls[3].id} +{bot_plane.id} -{top_plane.id})" + + rot = FreeCAD.Rotation(Vector(0, 0, 1), -PhiTwist / 2 + i * dTwist) + for wall in walls: + wall.rotate(rot) + + t += dt + self.slice += 1 + + self.region = Region(region_expr) + + self.position_globally() + + +class GDMLTwistedtubsExporter(GDMLTwistedSolidExporter): + def __init__(self, obj): + super().__init__(obj) + + def tubeWalls(self, rin, rout, phi0, phi1): + ''' return four surfaces: a cylinder of radius rin, None if rin = 0 + a cylinder or radius rout, two planes with normals in the x-y plane + one starting at ph0, the other ending at phi1 + ''' + cylinder_in = None + name = NameManager.getName(self.obj) + center = Vector(0, 0, 0) + axis = Vector(0, 0, 1) + if rin > 0: + cylinder_in = CylinderSurfaceExporter(name+"_in", center, axis, rin) + cylinder_out = CylinderSurfaceExporter(name+"_out", center, axis, rout) + + u0 = Vector(math.cos(phi0), math.sin(phi0), 0) + u1 = Vector(math.cos(phi1), math.sin(phi1), 0) + n0 = Vector(-u0.y, u0.x, 0) + n1 = Vector(-u1.y, u1.x, 0) + + phi0_plane = PlaneSurfaceExporter(name+"_phi0", n0, 0) + phi1_plane = PlaneSurfaceExporter(name+"_phi1", n1, 0) + + return [cylinder_in, cylinder_out, phi0_plane, phi1_plane] + + def generate_surfaces(self): + from .GDMLObjects import getAngleRad, getAngleDeg + mul = self.getMult() + rin = mul*self.obj.endinnerrad + rout = mul*self.obj.endouterrad + z = mul*self.obj.zlen + phi = getAngleRad(self.obj.aunit, self.obj.phi) + angle = getAngleDeg(self.obj.aunit, self.obj.twistedangle) + dTwist = self.dTwist # 2 degree rotation per step + N = int(angle/dTwist) + N = max(N, 10) + dz = z /N + + planes_dict = {} + region_expr = "" + z0 = -z/2 + phi0 = -phi/2 + phi1 = phi/2 + + region_expr = "" + self.region = Region("") + for i in range(0, N): + name = NameManager.getName(self.obj) + walls = self.tubeWalls(rin, rout, phi0, phi1) + rin_cylinder = walls[0] + rout_cylinder = walls[1] + + if rin > 0: + self.surfaces += walls + else: + self.surfaces += walls[1:] + + if z0 in planes_dict: + bot_plane = planes_dict[z0] + else: + bot_plane = PlaneSurfaceExporter(f"{name}_{self.slice}_bot", Vector(0, 0, 1), z0) + planes_dict[z0] = bot_plane + self.surfaces.append(bot_plane) + + z0 += dz + if z0 in planes_dict: + top_plane = planes_dict[z0] + else: + top_plane = PlaneSurfaceExporter(f"{name}_{self.slice}_top", Vector(0, 0, 1), z0) + planes_dict[z0] = top_plane + self.surfaces.append(top_plane) + + phi0_plane = walls[2] + phi1_plane = walls[3] + phi0_normal = phi0_plane.normal + phi1_normal = phi1_plane.normal + + if rin > 0: + rin_region = f"+{rin_cylinder.id}" + else: + rin_region = "" + + if phi0_normal.cross(phi1_normal).z > 0: + region_expr += f'({rin_region} -{rout_cylinder.id} +{bot_plane.id} -{top_plane.id} +{phi0_plane.id} -{phi1_plane.id})' + else: + region_expr += f'({rin_region} -{rout_cylinder.id} +{bot_plane.id} -{top_plane.id} (+{phi0_plane.id} | -{phi1_plane.id}))' + if i != N -1: + region_expr += ' | ' + + rot = FreeCAD.Rotation(Vector(0, 0, 1), -angle / 2 + i * dTwist) + for wall in [phi0_plane, phi1_plane]: + wall.rotate(rot) + self.slice += 1 + + self.region = Region(region_expr) + + self.position_globally() + + +class MultiFuseExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + + def name(self): + solidName = "MultiFuse" + self.obj.Label + return solidName + + def generate_surfaces(self): + print("Output Solids") + exporters = [] + self.surfaces = [] + self.region = Region("") + for sub in self.obj.OutList: + exporter = SolidExporter.getExporter(sub) + if exporter is not None: + exporter.generate_surfaces() + self.surfaces += exporter.surfaces + if self.region.expr == "": + self.region = Region(f'({exporter.get_region()})') + else: + self.region = self.region.union(exporter.get_region()) + + ''' + The surfaces generated above already contain the absoulte placement, including that of the + boolean object, so no need for this + + objPlacement = self.obj.Placement + if objPlacement != FreeCAD.Placement(): + translation = objPlacement.Base + rotation = objPlacement.Rotation + for surf in self.surfaces: + surf.rotate(rotation) + surf.translate(translation) + ''' + +class MultiCommonExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + + def name(self): + solidName = "MultiCommon" + self.obj.Label + return solidName + + def generate_surfaces(self): + print("Output Solids") + exporters = [] + self.surfaces = [] + self.region = Region("") + for sub in self.obj.OutList: + exporter = SolidExporter.getExporter(sub) + if exporter is not None: + exporter.generate_surfaces() + self.surfaces += exporter.surfaces + if self.region.expr == "": + self.region = Region(f'({exporter.get_region()})') + else: + self.region = self.region.intersection(exporter.get_region()) + + + +class OrthoArrayExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + + + def generate_surfaces(self): + self.region = Region("") + + # for the time being and array is treated is a solid with a single material + # its region is the union of the array of regions of the individual array elements + from . import arrayUtils + base = self.obj.Base + print(f"Base {base.Label}") + if hasattr(base, "TypeId") and base.TypeId == "App::Part": + print( + f"**** Arrays of {base.TypeId} ({base.Label}) currently not supported ***" + ) + return + + baseExporter = SolidExporter.getExporter(base) + if baseExporter is None: + print(f"Cannot export {base.Label}") + return + + # heuristic: If the rotation is not intrinsic to the base solid, then the + # position must be rotated as well. + tolerance = 1.0e-7 + rotatePosition = not base.Placement.Rotation.isSame(base.getGlobalPlacement().Rotation, tolerance) + positionRotation = base.getGlobalPlacement().Rotation + if rotatePosition: + print(f" Will apply position rotation to {base.Label} because global rotation is different than base rotation") + # we need to also check for the array object rotation. This is NOT included in the global placement + if not self.obj.Placement.Rotation.isSame(base.getGlobalPlacement().Rotation, tolerance): + rotatePosition = True + positionRotation = self.obj.Placement.Rotation + + for i, placement in enumerate(arrayUtils.placementList(self.obj, offsetVector=self.obj.Placement.Base)): + solidExporter = SolidExporter.getExporter(base) + item_region = solidExporter.get_region() + translation = placement.Base + if rotatePosition: + translation = positionRotation*translation + # the rotation should also apply to the base, not just the position + solidExporter.rotate(positionRotation) + solidExporter.translate(translation) + self.region = self.region.union(item_region) + self.surfaces += solidExporter.surfaces + + +class PolarArrayExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + + def generate_surfaces(self): + from . import arrayUtils + base = self.obj.Base + + self.region = Region("") + + print(base.Label) + appPartBase = False + if hasattr(base, "TypeId") and base.TypeId == "App::Part": + print( + f"**** Arrays of {base.TypeId} ({base.Label}) currently not supported ***" + ) + + return + + + baseExporter = SolidExporter.getExporter(base) + if baseExporter is None: + print(f"Cannot export {base.Label}") + return + + arrayRotation = self.obj.Placement.Rotation + basePos = base.Placement.Base + # arrayUtils takes care of the offset center of of rotation + # But the array may have been shifted/rotatied by its parent. + # Since the array itself is not exported, but rather the array base is, + # the base global position does not reflect the arrays global position + + placements = arrayUtils.placementList(self.obj, rot=arrayRotation) + + # I'll try doing the rotations from scratch + # let GlobalPlacement = G # Note: FOR AN ARRAY GLOBAL PLACEMENT APPLIES to the ARRAY, NOT BASE of the ARRAY + # let Container Placement = C (the effect of all the App::Parts that contain the base) + # let the base placement be P + # then must have G = C*P ==> C = G*P^-1 + # If the rotation by the polar array is rot + # Then the position after rotation ought to be + # rotation by polar array: position_by_array = array_center + rot * (P.Base - array_center) + # Position globally rotated_global_position = C*position_by_array + # Because we don't give a position or a rotation to the solid exporter surfaces (we only rotate or translate them) + # We need to first unrotate and untranslate the surfaces, then translate and rotate by the array rotation and + # translation as above + arrayObj = self.obj + G = arrayObj.getGlobalPlacement() + P = arrayObj.Placement + Pinv = P.inverse() + C = G*Pinv # this is what puts the array in its position globally + baseInvPlacement = base.Placement.inverse() + + for placement in placements: + rot = arrayRotation*placement.Rotation + local_rotated_position = arrayObj.Placement.Base + self.obj.Center + rot*(basePos - self.obj.Center) + global_rotated_position = C*local_rotated_position + + # rot = rot * baseRotation + # rot.Angle = -rot.Angle # undo angle reversal by exportRotation + + + solidExporter = SolidExporter.getExporter(base) + # get region generates a region with # global translation and global rotation + # let us assume now that object rotation is 0 + # array utils give position rotated about array center then shifter by array center + # + item_region = solidExporter.get_region() # also positions solidExorter object globally + # undo surfaces positioning + solidExporter.translate(baseInvPlacement.Base) + solidExporter.rotate(baseInvPlacement.Rotation) + # then apply rotated global position + solidExporter.rotate(rot) + solidExporter.translate(global_rotated_position) + self.region = self.region.union(item_region) + self.surfaces += solidExporter.surfaces + + +class PathArrayExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + + + def generate_surfaces(self): + self.region = Region("") + + base = self.obj.Base + print(base.Label) + if hasattr(base, "TypeId") and base.TypeId == "App::Part": + print( + f"**** Arrays of {base.TypeId} ({base.Label}) currently not supported ***" + ) + return + + baseExporter = SolidExporter.getExporter(base) + if baseExporter is None: + print(f"Cannot export {base.Label}") + return + + count = self.obj.Count + positionVector = base.Placement.Base + rot = base.Placement.Rotation + + extraTranslation = self.obj.ExtraTranslation + + pathObj = self.obj.PathObject + path = pathObj.Shape.Edges[0] + points = path.discretize(Number=count) + + for i, point in enumerate(points): + pos = point + positionVector + extraTranslation + solidExporter = SolidExporter.getExporter(base) + item_region = solidExporter.get_region() + solidExporter.translate(pos) + self.region = self.region.union(item_region) + self.surfaces += solidExporter.surfaces + + +class PointArrayExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + + def generate_surfaces(self): + from . import arrayUtils + + self.surfaces = [] + self.region = Region("") + + base = self.obj.Base + print(base.Label) + if hasattr(base, "TypeId") and base.TypeId == "App::Part": + print( + f"**** Arrays of {base.TypeId} ({base.Label}) currently not supported ***" + ) + return + + baseExporter = SolidExporter.getExporter(base) + if baseExporter is None: + print(f"Cannot export {base.Label}") + return + + extraTranslation = self.obj.ExtraPlacement.Base + extraRotation = self.obj.ExtraPlacement.Rotation + + pointObj = self.obj.PointObject + points = pointObj.Points.Points + arrayTranslation = self.obj.Placement.Base + arrayRotation = self.obj.Placement.Rotation + + # points = arrayUtils.placementList(self.obj, offsetVector=self.obj.Placement.Base) + for point in points: + if not arrayRotation.isSame(FreeCAD.Rotation(), 1e-7): + point = arrayRotation*point + pos = point + arrayTranslation + solidExporter = SolidExporter.getExporter(base) + item_region = solidExporter.get_region() + solidExporter.rotate(arrayRotation) + solidExporter.translate(pos) + self.region = self.region.union(item_region) + self.surfaces += solidExporter.surfaces + + +# +# ------------------------------revolutionExporter ---------------------------- +# +global Deviation # Fractional deviation of revolve object +############################################# +# Helper functions for Revolve construction + +# One of the closed curves (list of edges) representing a part +# of the sketch + +def cone_from_line_segment(v0, v1): + ''' + Given a line segment in the x-z plane, generate a cone in the half space containing the segment + :param: v0: One point on the segment (v0=Vector(x0, 0, z0) + :param: v1: another point on the segment v1=(Vector(x1, 0, z1) + :Return: The cone passing through the line segment. if v0.z == v1.z, return a cylinder + ''' + + dx = v1.x - v0.x + dz = v1.z - v0.z + u = (v1-v0).normalize() # unit vector fro m v0 to v1 + if abs(dz) > 1e-9: + theta = math.atan(abs(dx/dz)) + axis = Vector(0, 0, 1) + + if abs(u.x) < 1e-9: # return a cylinder + surf = CylinderSurfaceExporter("", Vector(0, 0, 0), Vector(0, 0, 1), abs(v0.x)) + else: # return a cone + t2 = -v0.x / u.x + center = v0 + t2 * u + surf = ConeSurfaceExporter("", center, axis, theta) + + return surf + + elif abs(dx) > 1e-9: # a horizontal line, not sure if a plane is the correct answer + surf = PlaneSurfaceExporter("", Vector(0, 0, 1), v0.z) + return surf + + return None + + + +class ClosedCurve: + def __init__(self, name, edgeList): + self.name = name + self.face = Part.Face(Part.Wire(edgeList)) + self.edgeList = edgeList + + def isInside(self, otherCurve): + # ClosedCurves are closed: so if ANY vertex of the otherCurve + # is inside, then the whole curve is inside + return self.face.isInside( + otherCurve.edgeList[0].Vertexes[0].Point, 0.001, True + ) + + @staticmethod + def isCircle(arc1, arc2): + ''' + test if two arcs can be joined into a single circle + return true if so, false if not + TODO: make tests in terms of some small fraction epsilon + ''' + # Both must be circular arcs + c1 = arc1.Curve + c2 = arc2.Curve + if (c1.TypeId != "Part::GeomCircle" or + c2.TypeId != "Part::GeomCircle"): + print("Not Arc") + return False + # They must have same radius + if c1.Radius != c2.Radius: + print("Not same radius") + return False + # They must have the same center + if c1.Center != c2.Center: + print("not same center") + return False + # They must join end to end + # for reasons I don't understand, both arcs + # have the same first and last parameters and the + # last parameter is 2*pi. The sort edges must + # not be calculating edges correctly + ''' + if (c1.FirstParameter != c2.LastParameter or + c1.LastParameter != c2.FirstParameter): + print("dont match ends") + print(f'c1.0 {c1.FirstParameter} c1.1 {c1.LastParameter}') + print(f'c2.0 {c2.FirstParameter} c2.1 {c2.LastParameter}') + return False + ''' + if (arc1.Vertexes[0].Point != arc2.Vertexes[1].Point or + arc1.Vertexes[1].Point != arc2.Vertexes[0].Point): + print("dont match ends") + print(f'c1.0 {arc1.Vertexes[0].Point} c1.1 {arc1.Vertexes[1].Point}') + print(f'c2.0 {arc2.Vertexes[0].Point} c2.1 {arc2.Vertexes[1].Point}') + return False + + # They must be in the same plane + if c1.Axis != c2.Axis: + print("not same axis") + return False + + return True + + @staticmethod + def arcs2circle(arc1, arc2): + ''' + combine two arc edges into a single circle edge + ''' + circle = None + if ClosedCurve.isCircle(arc1, arc2): + curve = arc1.Curve + circle = Part.makeCircle(curve.Radius, curve.Center, curve.Axis) + return circle + + +class RevolvedClosedCurve(ClosedCurve): + def __init__(self, name, edgelist): + super().__init__(name, edgelist) + self.position = Vector(0, 0, 0) + self.rotation = [0, 0, 0] # TBD + self.deflectionFraction = 0.001 + self.surfaces = [] + self.region = Region("") + + def add_top_bottom(self, verts): + ''' If there is a single top/bottom vertex the cylinders/conves + meeting there will eventuall y extend to a common region that is outside + the revolved solid. So we must cap the top/botton with a top/bottom + plane in that case''' + zs = [v.z for v in verts] + maxz = max(zs) + minz = min(zs) + top_plane = PlaneSurfaceExporter("", Vector(0, 0, 1), maxz) + # add a top plane if one does not exist already + add_it = True + for surf in self.surfaces: + if surf == top_plane: + add_it = False + break + if add_it: + self.surfaces.append(top_plane) + self.region = self.region.intersection(Region(f"-{top_plane.id}")) + + bottom_plane = PlaneSurfaceExporter("", Vector(0, 0, 1), minz) + # add a top plane if one does not exist already + add_it = True + for surf in self.surfaces: + if surf == bottom_plane: + add_it = False + break + if add_it: + self.surfaces.append(bottom_plane) + self.region = self.region.intersection(Region(f"+{bottom_plane.id}")) + + def getVertexes(self): + return self.discretize() + + def generated_triangulated_surfaces(self, xy_verts): + from .polygonsHelper import triangulate_polygon_earclip, inner_outer + polys = triangulate_polygon_earclip(xy_verts) + + self.region = Region("") + for poly in polys: + verts = [Vector(v.x, 0, v.y) for v in poly] + arrangeCCW(verts, Vector(0, -1, 0)) # arrange in CCW in xz plane + inner, outer = inner_outer(verts) + + nouter = len(outer) + outer_region = Region("") + for i in range(0, nouter-1): + v0 = outer[i] + v1 = outer[i+1] + surface = cone_from_line_segment(v0, v1) + if surface is not None: + top_plane = PlaneSurfaceExporter("", Vector(0, 0, 1), v1.z) + bot_plane = PlaneSurfaceExporter("", Vector(0, 0, 1), v0.z) + self.surfaces += [surface, top_plane, bot_plane] + outer_region = outer_region.union(Region(f"-{surface.id} -{top_plane.id} +{bot_plane.id}")) + + ninner = len(inner) + inner_region = Region("") + for i in range(0, ninner-1): + v0 = inner[i] + v1 = inner[i+1] + surface = cone_from_line_segment(v0, v1) + if surface is not None: + top_plane = PlaneSurfaceExporter("", Vector(0, 0, 1), v0.z) + bot_plane = PlaneSurfaceExporter("", Vector(0, 0, 1), v1.z) + self.surfaces += [surface, top_plane, bot_plane] + inner_region = inner_region.union(Region(f"+{surface.id} -{top_plane.id} +{bot_plane.id}")) + + region = outer_region.intersection(inner_region) + self.region = self.region.union(region) + + def generate_surfaces(self): + from .polygonsHelper import is_convex_polygon, inner_outer + + verts = self.getVertexes() + # the revolve and triangulate algorithm assume the last point and first point are NOT the same. + # more generally, we omit edges of near zero length + vpruned = [] + for i in range(len(verts)): + i1 = (i+1) % len(verts) + if (verts[i1] - verts[i]).Length > 1.0e-07: + vpruned.append(verts[i]) + + arrangeCCW(vpruned, Vector(0, -1, 0)) # arrange in CCW in xz plane + verts = vpruned + xy_verts = [Vector(v.x, v.z, 0) for v in verts] + if not is_convex_polygon(xy_verts): + self.generated_triangulated_surfaces(xy_verts) + return + + inner, outer = inner_outer(verts) + outer_region = generate_half_loop_surfaces(outer, self.surfaces) + inner_region = generate_half_loop_surfaces(inner, self.surfaces) + + region = outer_region.intersection(inner_region) + + zs = [v.z for v in verts] + maxz = max(zs) + minz = min(zs) + top_plane = PlaneSurfaceExporter("", Vector(0, 0, 1), maxz) + bot_plane = PlaneSurfaceExporter("", Vector(0, 0, 1), minz) + self.surfaces += [top_plane, bot_plane] + + bounding_planes = Region(f"-{top_plane.id} +{bot_plane.id}") + + self.region = region.intersection(bounding_planes) + + + def discretize(self): + deflection = Deviation * radialExtent(self.edgeList) + print(f"Deflection = {deflection}") + edge = self.edgeList[0] + return edge.discretize(Deflection=deflection) + + +class RevolvedNEdges(RevolvedClosedCurve): + def __init__(self, name, edgelist): + super().__init__(name, edgelist) + + def getVertexes(self): + verts = [] + for i, e in enumerate(self.edgeList): + + while switch(e.Curve.TypeId): + if case("Part::GeomLineSegment"): + print("Part::GeomLineSegment") + verts.append(e.Vertexes[0].Point) + break + + if case("Part::GeomLine"): + print("Part::GeomLine") + verts.append(e.Vertexes[0].Point) + break + + else: + curveName = self.name + "_c" + str(i) + curveSection = RevolvedClosedCurve( + curveName, [e]) + verts += curveSection.discretize() + break + return verts + + +# arrange a list of edges in the x-y plane in Counter Clockwise direction +# This can be easily generalized for points in ANY plane: if the normal +# defining the desired direction of the plane is given, then the z component +# below should be changed a dot prduct with the normal +def arrangeCCW(verts, normal=Vector(0, 0, 1)): + reverse = False + v0 = verts[0] + rays = [(v - v0) for v in verts[1:]] + area = 0 + for i, ray in enumerate(rays[:-1]): + area += (rays[i].cross(rays[i + 1])).dot(normal) + if area < 0: + verts.reverse() + reverse = True + + return reverse + + +# Utility to determine if vector from point v0 to point v1 (v1-v0) +# is on same side of normal or opposite. Return true if v points along normal + + +def pointInsideEdge(v0, v1, normal): + v = v1 - v0 + if v.dot(normal) < 0: + return False + else: + return True + + +def edgelistArea(edgelist: list[Part.Edge]) -> float: + face = Part.Face(Part.Wire(edgelist)) + return face.Area + + +def sortEdgelistsByFaceArea(listoflists): + listoflists.sort(reverse=True, key=edgelistArea) + + +# return maxRadialdistance - minRadialDistance +def radialExtent(edges, axis=Vector(0, 0, 1)): + rmin = sys.float_info.max + rmax = -sys.float_info.max + for e in edges: + b = e.BoundBox + for i in range(0, 8): # loop over box boundaries + v = b.getPoint(i) + radialVector = v - v.dot(axis) * axis + r = radialVector.Length + if r < rmin: + rmin = r + elif r > rmax: + rmax = r + + return rmax - rmin + + + +def getRevolvedCurve(name, edges): + # Return an RevolvedClosedCurve object of the list of edges + + if len(edges) == 1: # single edge ==> a closed curve, or curve section + return RevolvedClosedCurve(name, edges) + + else: # three or more edges + return RevolvedNEdges(name, edges) + + +def revolve_convex_polygon(verts, eps=1.0e-4): + '''Given the vertexes of a convex polygon (as FreeCAD Vectors), + produce surfaces, the interior of which forms a surface of revolution + of the triangle. + + The vertexes of the triangle are to be given as x-z point (in the x-z plane) + and are assumed to be already given in CCW order + There are three possible surfaces: + (1) A horizontal line (z = z0 = constant) -> horizontal plane + (2) A vertical line (x = x0 = constant) -> a cylinder + (2) A slanted line (x0, z0) -> (x1, z1) -> a cone + ''' + + surfaces = [] + expr = "" + for i in range(len(verts)): + i1 = (i + 1) % len(verts) + v0 = verts[i] + v1 = verts[i1] + dx = v1.x - v0.x + dz = v1.z - v0.z + normal = Vector(dz, 0, -dx) + if normal.Length < 1.0e-07: + continue + normal.normalize() + + if abs(dx) < eps: #a cylinder + center = Vector(0, 0, 0) + axis = Vector(0, 0, 1) + radius = abs(v0.x) + cylinder = CylinderSurfaceExporter("", center, axis, radius) + surfaces.append(cylinder) + if normal.x < 0: + expr += f"+{cylinder.id} " + else: + expr += f"-{cylinder.id} " + + elif abs(dz) < eps: # a horizontal plane + D = v0.dot(normal) + surf = PlaneSurfaceExporter("", normal, D) + surfaces.append(surf) + expr += f"-{surf.id} " + + else: # must be a slanted line. a conical surface + theta = math.atan(abs(dx/dz)) + t2 = v0.dot(normal)/normal.z + center = t2*Vector(0, 0, 1) + axis = Vector(0, 0, 1) + surf = ConeSurfaceExporter("", center, axis, theta) + surfaces.append(surf) + if normal.x > 0: + expr += f"-{surf.id} " + else: + expr += f"+{surf.id} " + + # we want to select half space of the cone that includes the edge + # The normal to the plane is Vector(0, 0, 1) (the z-axis) + # D = coneverex.z + # if v0.z < center.z, we are on the lower half, i.e, on the - sode f the plane + # other was we are on the + side of the plane + selecting_plane = PlaneSurfaceExporter("", axis, center.z) + surfaces.append(selecting_plane) + if v0.z < center.z: + expr += f"-{selecting_plane.id} " + else: + expr += f"+{selecting_plane.id} " + + return surfaces, Region(expr) + + +class RevolutionExporter(SolidExporter): + def __init__(self, revolveObj): + super().__init__(revolveObj) + self.sketchObj = revolveObj.Source + self.lastName = self.obj.Label # initial name: might be modified later + # generate the positions that get computed during export + self.region = None + self.surfaces = [] + + def name(self): + # override default name in SolidExporter + prefix = "" + if self.lastName[0].isdigit(): + prefix = "R" + return prefix + self.lastName + + def position(self): + # This presumes export has been called before position() + # Things will be screwed up, otherwise + return self._position + + def rotation(self): + # This presumes export has been called before position() + # Things will be screwed up, other wise + return self._rotation + + def generate_surfaces(self): + # + global Deviation + revolveObj = self.obj + axis = revolveObj.Axis + angle = revolveObj.Angle + revolveCenter = revolveObj.Base + + # Fractional deviation + Deviation = revolveObj.ViewObject.Deviation / 100.0 + + + # rotation to take revolve direction to z -axis + rot_dir_to_z = FreeCAD.Rotation(axis, Vector(0, 0, 1)) + edges = [edge.rotated(Vector(0, 0, 0), rot_dir_to_z.Axis, math.degrees(rot_dir_to_z.Angle)) + for edge in revolveObj.Source.Shape.Edges] + + # adjustment of Symmetric revolves + if revolveObj.Symmetric: + edges = [edge.rotated(Vector(0, 0, 0), Vector(0, 0, 1), -angle/2) + for edge in edges] + + # adjustment for off-center revolve axis + if revolveCenter != Vector(0, 0, 0): + edges = [edge.translated(-revolveCenter) for edge in edges] + + + sortededges = Part.sortEdges(edges) + + # sort by largest area to smallest area + sortEdgelistsByFaceArea(sortededges) + # getClosedCurve returns one of the sub classes of ClosedCurve that + # knows how to export the specific closed edges + # Make names based on Revolve name + eName = revolveObj.Label + # get a list of curves (instances of class ClosedCurve) + # for each set of closed edges + curves = [] + for i, edges in enumerate(sortededges): + curve = getRevolvedCurve(eName + str(i), edges) + if curve is not None: + curves.append(curve) + if len(curves) == 0: + print("No edges that can be revolved were found") + return + + # build a generalized binary tree of closed curves. + root = Node(curves[0], None, 0) + for c in curves[1:]: + root.insert(c) + + # Traverse the tree. The list returned is a list of [Node, parity], + # where parity = 0, says add to parent, 1 mean subtract + lst = root.preOrderTraversal(root) + rootnode = lst[0][0] + rootCurve = rootnode.closedCurve + rootCurve.generate_surfaces() + self.surfaces = rootCurve.surfaces + + firstName = rootCurve.name + booleanName = firstName + + self.region = rootCurve.region + + rootPos = rootCurve.position + rootRot = ( + rootCurve.rotation + ) # for now consider only angle of rotation about z-axis + + for c in lst[1:]: + node = c[0] + parity = c[1] + curve = node.closedCurve + curve.generate_surfaces() + self.surfaces += curve.surfaces + if parity == 0: + self.region = self.region.union(curve.region) + else: + self.region = self.region.cut(curve.region) + + # add cutoff planes if revolution angle is not 360 deg + if angle != 360: + phi0_normal = Vector(0, 1, 0) + phi0_plane = PlaneSurfaceExporter(self.name()+'_phi0', phi0_normal, 0) + phi1_normal = Vector(math.cos(math.radians(angle+90)), math.sin(math.radians(angle+90)), 0) + phi1_plane = PlaneSurfaceExporter(self.name()+'_phi1', phi1_normal, 0) + self.surfaces += [phi0_plane, phi1_plane] + + if phi0_normal.cross(phi1_normal).z > 0: + planes_region = Region(f"+{phi0_plane.id} -{phi1_plane.id}") + else: + planes_region = f"(+{phi0_plane.id} | -{phi1_plane.id})" + + self.region = self.region.intersection(planes_region) + + rot_z_to_dir = FreeCAD.Rotation(Vector(0, 0, 1), axis) # rotation from z-axis to original axis of revolution + for surf in self.surfaces: + surf.rotate(rot_z_to_dir) + + self.position_globally() + +# +# -----------------------------------------------extrusionExporter----------------------------------------------------- +# +############################################# +# Helper functions for extrude construction + +# One of the closed curves (list of edges) representing a part +# of the sketch + +class ExtrudedClosedCurve(ClosedCurve): + def __init__(self, name, edgelist, height): + super().__init__(name, edgelist) + self.height = height + self.position = Vector(0, 0, 0) + self.rotation = [0, 0, 0] # TBD + self.region = None + self.surfaces = [] + + + def generate_surfaces(self): + verts = self.discretize() + self.surfaces, self.region = exportXtru(self.name, verts, self.height) + + def midPoint(self): + edge = self.edgeList[0] + verts = edge.discretize(Number=51) + return verts[int(len(verts) / 2)] + + def discretize(self): + global Deviation + edge = self.edgeList[0] + deflection = Deviation * edge.BoundBox.DiagonalLength + print(f"Deflection = {deflection}") + return edge.discretize(Deflection=deflection) + + +class ExtrudedCircle(ExtrudedClosedCurve): + def __init__(self, name, edgelist, height): + super().__init__(name, edgelist, height) + self.position = edgelist[0].Curve.Center + + def generate_surfaces(self): + edge = self.edgeList[0] + print(f"circle position {self.position}") + surf = exportTube(self.name, self.position, edge.Curve.Radius, self.height) + self.surfaces = [surf] + self.region = Region(f"-{surf.id}") + + +class ExtrudedArcSection(ExtrudedClosedCurve): + def __init__(self, name, edgelist, height): + super().__init__(name, edgelist, height) + # Note extrusion polyogn will be in absolute coordinates + # since arc section is relative to that, position is actually (0,0,0) + # same goes for rotation + + def midPoint(self): + edge = self.edgeList[0] + verts = edge.discretize(Number=3) + return verts[1] + + def area(self): + edge = self.edgeList[0] + v0 = edge.Vertexes[0].Point + v1 = edge.Vertexes[1].Point + L1 = Part.LineSegment(v0, v1) + chordEdge = Part.Edge(L1) + face = Part.Face(Part.Wire([edge, chordEdge])) + + return face.Area + + def generate_surfaces(self): + global solids + + edge = self.edgeList[0] + radius = edge.Curve.Radius + center = edge.Curve.Center + # First form a bounding rectangle (polygon) for the arc. + # Arc edges + v1 = edge.Vertexes[0].Point + v2 = edge.Vertexes[1].Point + vmid = self.midPoint() + + # midpoint of chord + vc = (v1 + v2) / 2 + v = v2 - v1 + u = v.normalize() + # extend the ends of the chord so extrusion can cut all of circle, if needed + v1 = vc + radius * u + v2 = vc - radius * u + # component of vmid perpendicular to u + vc_vmid = vmid - vc + n = vc_vmid - u.dot(vc_vmid) * u + n.normalize() + + xtruName = self.name + "_xtru" + + chord_plane, chord_region = exportXtru(xtruName, [v1, v2], self.height)[0] + # we want the plane normal to point AWAY from the chord + if chord_plane.normal.dot(vmid) > 0: + chord_plane.normal = -chord_plane.Normal + chord_plane.to_coeffs() + self.surfaces.append(chord_plane) + + # tube to be cut1 + tubeName = self.name + "_tube" + cylinder = exportTube(tubeName, center, edge.Curve.Radius, self.height) + self.surfaces.append(cylinder) + + self.region = Region(f"-{cylinder.id} -{chord_plane.id}") + + +class ExtrudedEllipticalSection(ExtrudedClosedCurve): + def __init__(self, name, edgelist, height): + super().__init__(name, edgelist, height) + # Note extrusion polyogn will be in absolute coordinates + # since arc section is relative to that, position is actually (0,0,0) + # same goes for rotation + + + def generate_surfaces(self): + global solids + + edge = self.edgeList[0] + deflection = Deviation * edge.BoundBox.DiagonalLength + points = edge.discretize(Deflection=deflection) + points.append(points[0]) + arrangeCCW(points) + self.surfaces, self.region = exportXtru(self.name, points, self.height) + + +class ExtrudedBSpline(ExtrudedClosedCurve): + def __init__(self, name, edgelist, height): + super().__init__(name, edgelist, height) + + +class ExtrudedNEdges(ExtrudedClosedCurve): + def __init__(self, name, edgelist, height): + super().__init__(name, edgelist, height) + + def isSubtraction(self, edge): + # Does the given edge increase or decrease the area + # of the polygon formed by verts + ftot = Part.Face(Part.Wire(self.edgeList)) + # form face from edge and its chord + v0 = edge.Vertexes[0].Point + v1 = edge.Vertexes[1].Point + L1 = Part.LineSegment(v0, v1) + E1 = Part.Edge(L1) + fEdge = Part.Face(Part.Wire([edge, E1])) + + # form face from other edges without edge being tested + edgesWithout = [] + for e in self.edgeList: + if e != edge: + edgesWithout.append(e) + else: + v0 = edge.Vertexes[0].Point + v1 = edge.Vertexes[1].Point + L1 = Part.LineSegment(v0, v1) + edgesWithout.append(Part.Edge(L1)) + fwithout = Part.Face(Part.Wire(edgesWithout)) + + totArea = ftot.Area + edgeArea = fEdge.Area + withoutArea = fwithout.Area + print( + f"totArea {totArea}, edgeArea {edgeArea}, withoutArea {withoutArea}" + ) + + if totArea < 0.999 * ( + edgeArea + withoutArea + ): # 0.99 safety margin for totArea = edgeArea+withoutArea + if totArea > edgeArea: + return True + else: + # we need to reverse order of subtraction + return None # poor way of signaling need to swap subtraction order + else: + return False + + def generate_surfaces(self): + verts = [] + + for i, e in enumerate(self.edgeList): + while switch(e.Curve.TypeId): + if case("Part::GeomLineSegment"): + verts.append(e.Vertexes[0].Point) + break + + if case("Part::GeomLine"): + verts.append(e.Vertexes[0].Point) + break + + else: + print(f"Curve {e.Curve.TypeId}") + arcXtruName = self.name + "_g" + str(i) + arcSection = ExtrudedClosedCurve( + arcXtruName, [e], self.height + ) + bsplineVerts = arcSection.discretize() + verts = verts + bsplineVerts + break + + # verts.append(verts[0]) + xtruName = self.name + self.surfaces, self.region = exportXtru(xtruName, verts, self.height) + +# Node of a tree that represents the topology of the sketch being exported +# a left_child is a ClosedCurve that is inside of its parent +# a right_sibling is a closedCurve that is outside of its parent + + +class Node: + def __init__(self, closedCurve, parent, parity): + # the nomenclature is redundant, but a reminder that + # left is a child and right a sibling + self.parent = parent + if parent is None: + self.parity = ( + 0 # if parity is 0, print as union with current solid + ) + # if parity is 1, print as subtraction from other solid + else: + self.parity = parity + + self.left_child = None + self.right_sibling = None + self.closedCurve = closedCurve + + def insert(self, closedCurve): + if self.closedCurve: # not sure why this test is necessary + if self.closedCurve.isInside(closedCurve): + # given curve is inside this curve: + # if this node does not have a child, + # insert it as the left_child + # otherwise check if it is a child of the child + if self.left_child is None: + self.left_child = Node(closedCurve, self, 1 - self.parity) + else: + self.left_child.insert(closedCurve) + else: # Since we have no intersecting curves (for well constructed sketch + # if the given curve is not inside this node, it must be outside + if self.right_sibling is None: + self.right_sibling = Node(closedCurve, self, self.parity) + else: + self.right_sibling.insert(closedCurve) + else: + self.closedCurve = closedCurve + + def preOrderTraversal(self, root): + res = [] + if root: + res.append([root, root.parity]) + res = res + self.preOrderTraversal(root.left_child) + res = res + self.preOrderTraversal(root.right_sibling) + + return res + + +def discretizeMinusOne(edgeList, iSkip): + # return discretized edge list except for iSkip + verts = [] + for i in range(len(edgeList)): + edge = edgeList[i] + if ( + i == iSkip + or edge.Curve.TypeId == "Part::GeomLine" + or edge.Curve.TypeId == "Part::GeomLineSegment" + ): + verts.append(edge.Vertexes[0].Point) + verts.append(edge.Vertexes[1].Point) + else: + verts += edge.discretize(24) + return verts + + +def exportTube(name, center, radius, height): + global solids + + axis = Vector(0, 0, 1) + outer_surface = CylinderSurfaceExporter(name, center, axis, radius) + return outer_surface + + +def exportXtru(name, vlist, height): + from .polygonsHelper import is_convex_polygon, triangulate_polygon_earclip + + def convex_poly_planes(points): + planes = [] + for i in range(len(points)): + i1 = (i+1) % len(points) + edge = points[i1] - points[i] + normal = Vector(edge.y, -edge.x, 0) + normal.normalize() + + D = normal.dot(points[i]) + planes.append(PlaneSurfaceExporter(name, normal, D)) + return planes + + def convex_poly_region(planes): + expr = "" + for surf in planes: + expr += f"-{surf.id} " + return Region(expr) + + + # We are assuming that the points that form + # the edges to be extruded are al coplanar and in the x-y plane + # with a possible zoffset, which is taken as the common + # z-coordinate of the first vertex + if len(vlist) < 3: + return [], Region("") + + # prune verts: Closed curves get stitched from adjacent edges + # sometimes the beginning vertex of the next edge is the same as the end vertex + # of the previous edge. Geant gives a warning about that. To remove the warning + # we will remove vertices closer to each other than 0.1 nm - 1e-7 mm. If someone is trying + # to model objects with smaller dimensions, then they should take a lesson in Quantum Mechanic2s + # Because only adjacent edges could be the same, we just compare each edge to the preceding edge + vpruned = vlist[:] + arrangeCCW(vpruned) + for i in range(len(vlist)): + i1 = (i+1) % len(vlist) + if ((vlist[i1] - vlist[i]).Length < 1e-7): + vpruned.remove(vlist[i1]) + + planes = [] + + if is_convex_polygon(vpruned): + planes = convex_poly_planes(vpruned) + region = convex_poly_region(planes) + + else: + region = Region("") + polygon_list = triangulate_polygon_earclip(vpruned) + for polygon in polygon_list: + convex_planes = convex_poly_planes(polygon) + convex_region = convex_poly_region(convex_planes) + + # TODO: remove common planes with opposite normals + planes += convex_planes + region = region.union(convex_region) + + return planes, region + +def getExtrudedCurve(name, edges, height): + # Return an ExtrudedClosedCurve object of the list of edges + + if len(edges) == 1: # single edge ==> a closed curve, or curve section + e = edges[0] + if len(e.Vertexes) == 1: # a closed curve + closed = True + else: + closed = False # a section of a curve + + while switch(e.Curve.TypeId): + if case("Part::GeomLineSegment"): + print(" Sketch not closed") + return ExtrudedClosedCurve(edges, name, height) + + if case("Part::GeomLine"): + print(" Sketch not closed") + return ExtrudedClosedCurve(name, edges, height) + + if case("Part::GeomCircle"): + if closed is True: + print("Circle") + return ExtrudedCircle(name, edges, height) + else: + print("Arc of Circle") + return ExtrudedArcSection(name, edges, height) + + else: + print(" B spline") + return ExtrudedClosedCurve(name, edges, height) + + elif len(edges) == 2: # exactly two edges + # if the two edges are tow arcs that make a circle, extrude resulting circle + edge = ClosedCurve.arcs2circle(edges[0], edges[1]) + if edge is not None: + return ExtrudedCircle(name, [edge], height) + else: + return ExtrudedNEdges(name, edges, height) + else: # three or more edges + return ExtrudedNEdges(name, edges, height) + + +class ExtrusionExporter(SolidExporter): + def __init__(self, extrudeObj): + global Deviation + super().__init__(extrudeObj) + self.sketchObj = extrudeObj.Base + self.lastName = self.obj.Label # initial name: might be modified later + Deviation = self.obj.ViewObject.Deviation / 100.0 + # generate the positions that get computed during export + + def position(self): + # This presumes export has been called before position() + # Things will be screwed up, otherwise + return self._position + + def rotation(self): + # This presumes export has been called before position() + # Things will be screwed up, otherwise + return self._rotation + + def name(self): + # override default name in SolidExporter + prefix = "" + if self.lastName[0].isdigit(): + prefix = "X" + return prefix + self.lastName + + def generate_surfaces(self): + # The placement of the extruded item gets calculated here, during export + # but boolean exporter tries to get the position BEFORE the export here happens + # so to generate the position before the export happens, the doExport flag is used + # to run this code to generate the position, WITHOUT actually doing the export + # + + sketchObj: Sketcher.SketchObject = self.sketchObj + extrudeObj: Part.Feature = self.obj + eName = self.name() + + extrudeDirection = extrudeObj.Dir + + # rotation to take extrude direction to z -axis + savedSketchPlacement = self.sketchObj.Placement + self.sketchObj.Placement *= savedSketchPlacement.inverse() + # do we need a recompute after this? + + # rot_dir_to_z = FreeCAD.Rotation(extrudeDirection, Vector(0, 0, 1)) + # edges = [edge.rotated(Vector(0, 0, 0), rot_dir_to_z.Axis, math.degrees(rot_dir_to_z.Angle)) for edge in sketchObj.Shape.Edges] + edges = sketchObj.Shape.Edges + sortededges = Part.sortEdges(edges) + # sort by largest area to smallest area + sortEdgelistsByFaceArea(sortededges) + # getCurve returns one of the sub classes of ClosedCurve that + # knows how to export the specific closed edges + # Make names based on Extrude name + # curves = [getCurve(edges, extrudeObj.Label + str(i)) for i, edges + # in enumerate(sortededges)] + + if extrudeObj.Symmetric is True: + height = extrudeObj.LengthFwd.Value + else: + height = extrudeObj.LengthFwd.Value + extrudeObj.LengthRev.Value + # get a list of curves (instances of class ClosedCurve) for each + # set of closed edges + curves = [ + getExtrudedCurve(eName + str(i), edges, height) + for i, edges in enumerate(sortededges) + ] + # build a generalized binary tree of closed curves. + root = Node(curves[0], None, 0) + for c in curves[1:]: + root.insert(c) + + # Traverse the tree. The list returned is a list of [Node, parity], + # where parity = 0, says add to parent, 1 mean subtract + lst = root.preOrderTraversal(root) + rootnode = lst[0][0] + rootCurve = rootnode.closedCurve + rootCurve.generate_surfaces() # a curve is created with a unique name + + # Our construction assumes extrusion is along z-axis + # and that the sketch object does not have any placement AND + # that the edges in a acurve are positioned relative to sketc, + # It turns out that is WRONG. The edges already fold in the sketch's + # placement. So we undo the effect of the sketched non-identity + # placement and then apply it back again at the end + + self.region = rootCurve.region + + firstName = rootCurve.name + booleanName = firstName + + rootPos = rootCurve.position + rootRot = ( + rootCurve.rotation + ) # for now consider only angle of rotation about z-axis + + self.surfaces = rootCurve.surfaces + + for c in lst[1:]: + node = c[0] + parity = c[1] + curve = node.closedCurve + curve.generate_surfaces() + self.surfaces += curve.surfaces + if parity == 0: + self.region = self.region.union(curve.region) + else: + self.region = self.region.cut(curve.region) + + if extrudeObj.Symmetric is False: + if extrudeObj.Reversed is False: + zoffset = extrudeObj.LengthRev.Value + else: + zoffset = extrudeObj.LengthFwd.Value + else: + zoffset = extrudeObj.LengthFwd.Value / 2 + + axis = Vector(0, 0, 1) + top_surface = PlaneSurfaceExporter(self.obj.Label+'_top', axis, zoffset+height) + bottom_surface = PlaneSurfaceExporter(self.obj.Label+'_bot', axis, zoffset) + self.surfaces += [bottom_surface, top_surface] + self.region = Region(self.region.expr + f" -{top_surface.id} +{bottom_surface.id}") + + # Restore the sketch Placement + # undo the sketch placement + self.sketchObj.Placement = savedSketchPlacement + print(f"Extrude global placement = {self.obj.getGlobalPlacement()}") + print(f"Sketch local placement = {self.sketchObj.Placement}") + globalPlacement = self.obj.getGlobalPlacement()*self.sketchObj.Placement + print(f"sketch elements global placement is {globalPlacement}") + + T = globalPlacement.Base + R = globalPlacement.Rotation + + for surf in self.surfaces: + surf.rotate(R) + surf.translate(T) + + +class ShapeMesher: + deflection = 0.5 + angularDeflection = math.radians(28.5) + + def __init__(self, shape): + import MeshPart + + self.shape = shape + self.mesh = MeshPart.meshFromShape(Shape=shape, LinearDeflection=ShapeMesher.deflection, + AngularDeflection=ShapeMesher.angularDeflection, Relative=False) + + self.mesh.harmonizeNormals() + # Debug + + self.edge_dict = self.edge_face_dict() + + print(f"Num faces = {len(self.mesh.Facets)}") + print(f"Num edges = {len(self.edge_dict)}") + + def _edge_dir(self, edge): + """Return a unit vector along the geometric edge defined by mesh point indices.""" + idx = list(edge) + v0 = self.mesh.Points[idx[0]].Vector + v1 = self.mesh.Points[idx[1]].Vector + v = v1 - v0 + if v.Length == 0: + return v + v.normalize() + return v + + def edge_face_dict(self): + d = {} + for face in self.mesh.Facets: + pointIndexes = face.PointIndices + n = len(pointIndexes) + for i in range(n): + i1 = (i+1) % n + + edge = (pointIndexes[i], pointIndexes[i1]) + fz = frozenset(edge) + if fz not in d: + d[fz] = [face.Index] + else: + d[fz].append(face.Index) + return d + + def is_mesh_concave(self): + edge, _ = self.get_concave_edge() + return edge is not None + + def my_concave_edge_test(self, edge): + faceIndexes = self.edge_dict[edge] + f1 = self.mesh.Facets[faceIndexes[0]] + f2 = self.mesh.Facets[faceIndexes[1]] + c1 = (Vector(f1.Points[0]) + Vector(f1.Points[1]) + Vector(f1.Points[2])) / 3 + c2 = (Vector(f2.Points[0]) + Vector(f2.Points[1]) + Vector(f2.Points[2])) / 3 + eps = (c2-c1).Length/10 + p1 = c1 - eps * f1.Normal + p2 = c2 - eps * f2.Normal + + if abs(f1.Normal.dot(f2.Normal)) > 0.999: + self.display_edge(edge) + return True + + if (p2 - p1).Length > (c2 - c1).Length: + self.display_edge(edge) + return True + + return False + + def get_concave_edge(self): + """ + Return (edge, nmean, max_concave_angle) where: + - edge: a concave edge chosen among all concave candidates + - nmean: n1 + n2 for that edge (for your plane construction) + - max_concave_angle: maximum dihedral angle (in radians) among + all edges that are classified as concave + + If no concave edges are found, returns (None, None, 0.0). + """ + # Your “interesting concavity” threshold per *edge* + MIN_DIHEDRAL_ANGLE = math.radians(5.0) # tune as you like + MIN_SIN = math.sin(MIN_DIHEDRAL_ANGLE) + + # How close to -1 we require (n1 × n2) · ê to be + EPS_SIGN = 1e-3 # tolerance; adjust if needed + + best_edge = None + best_nmean = None + + best_sin_concave = 0.0 # sin(dihedral) for the **most concave** edge (among concave ones) + chosen_debug = None + + for edge, faceIndexes in self.edge_dict.items(): + # Only internal edges with exactly two adjacent facets + if len(faceIndexes) != 2: + continue + + f1 = self.mesh.Facets[faceIndexes[0]] + f2 = self.mesh.Facets[faceIndexes[1]] + + v_edge = self._edge_dir(edge) + if v_edge.Length == 0: + continue + + n1 = f1.Normal + n2 = f2.Normal + + # Cross product gives a vector along the intersection line + n1xn2 = n1.cross(n2) + sinthet = n1xn2.Length # ~ sin(dihedral angle between n1 and n2) + + # 1) Filter out almost-parallel normals: flat or nearly flat adjacency + if sinthet < MIN_SIN: + continue + + # 2) Concavity sign test using your sign convention + n1xn2.normalize() + dot_ce = n1xn2.dot(v_edge) + + # We consider the edge concave only if the cross product + # is pointing reliably in the opposite direction of the edge + if self.my_concave_edge_test(edge): + # print("my_concave_edge_test says this edge is convex. The get concave_edge says it is concave") + if sinthet > best_sin_concave: + best_sin_concave = sinthet + best_edge = edge + best_nmean = n1 + n2 + + angle = math.asin(max(min(sinthet, 1.0), -1.0)) + + if best_edge is None: + # No concave edges found at all + return None, None, 0.0 + + # Convert the maximum concave sin(angle) to an angle in radians + max_concave_angle = math.asin( + max(min(best_sin_concave, 1.0), -1.0) + ) + + return best_edge, best_nmean, max_concave_angle + + def share_edge(self, iFace1, iFace2): + ''' return true if faces with indexes iFace1, iFace2 share an edge''' + + edges_set = set() + face1 = self.mesh.Facets[iFace1] + face2 = self.mesh.Facets[iFace2] + for f in [face1, face2]: + pointIndexes = f.PointIndices + n = len(f.PointIndices) + for i in range(n): + i1 = (i+1) % n + edge = (pointIndexes[i], pointIndexes[i1]) + fz = frozenset(edge) + edges_set.add(fz) + + return len(edges_set) < 6 # if the faces share an edge, then there should be less than 6 edges in the set + + + def get_face_edges(self, iFace): + ''' return the three edges as a frozenset of the end point indexes of the endpoint''' + face = self.mesh.Facets[iFace] + pointIndexes = face.PointIndices + n = len(pointIndexes) + edges = [] + for i in range(n): + i1 = (i+1) % n + edge = (pointIndexes[i], pointIndexes[i1]) + fz = frozenset(edge) + edges.append(fz) + + return edges + + + def concave_face_chains(self): + ''' return a list of a list of faces that share an edge and that are concave at the shared edge''' + concave_edges = self.get_concave_edges() + + def concave_edge_chain(edge, chain_set): + if edge in concave_edges: + chain_set.add(edge) + concave_edges.remove(edge) + f1 = self.edge_dict[edge][0] + f2 = self.edge_dict[edge][1] + for f in [f1, f2]: + edges = self.get_face_edges(f) + for e in edges: + concave_edge_chain(e, chain_set) + else: + return + + concave_chains = [] + while len(concave_edges) > 0: + edge_set = set() + edge = concave_edges[0] + concave_edge_chain(edge, edge_set) + + face_set = set() + for e in edge_set: + f1 = self.edge_dict[e][0] + f2 = self.edge_dict[e][1] + face_set.add(f1) + face_set.add(f2) + concave_chains.append(face_set) + + return concave_chains + + def get_concave_edges(self): + """ + return list of concave edges + """ + chosen_debug = None + + concaveList = [] + for edge, faceIndexes in self.edge_dict.items(): + # Only internal edges with exactly two adjacent facets + if len(faceIndexes) != 2: + continue + + f1 = self.mesh.Facets[faceIndexes[0]] + f2 = self.mesh.Facets[faceIndexes[1]] + + v_edge = self._edge_dir(edge) + if v_edge.Length == 0: + continue + + if self.my_concave_edge_test(edge): + concaveList.append(edge) + + return concaveList + + def is_valid(self): + ''' + Validate mesh: + (1) Volume must be > deflection^3 + (2) All faces must have area > deflection^2 + (3) All edges must have length > deflection + (4) all normals must have length > 0 + (5) number of faces must greater than 4 + ''' + nfaces = self.mesh.CountFacets + if nfaces <= 4: + print(f"mesh has 4 or fewer faces = {nfaces}") + return 5 + + if self.mesh.Volume < ShapeMesher.deflection**3: + print(f"mesh volume {self.mesh.Volume} less than deflectin^3 {ShapeMesher.deflection**3}") + return 1 + + for i, face in enumerate(self.mesh.Facets): + if face.Normal.Length < 0.5: + print(f"face {i}'s normal length is less than 1: {face.Normal.Length}") + return 4 + if face.Area < ShapeMesher.deflection*ShapeMesher.deflection: + print(f"face area smaller than deflection^2: {face.Area}") + return 2 + + n = len(face.Points) + for j in range(n): + v0 = Vector(face.Points[j][0], face.Points[j][1], face.Points[j][2]) + j1 = (j+1) % n + v1 = Vector(face.Points[j1][0], face.Points[j1][1], face.Points[j1][2]) + edge_len = (v1-v0).Length + if edge_len < ShapeMesher.deflection: + print(f"Warning edge {j} of face {i} is shorter than deflection: edge length = {edge_len} deflection = {ShapeMesher.deflection}") + continue + + return 0 + + def display_face(self, iFace, color): + facet = self.mesh.Facets[iFace] + pnts = facet.Points + vecs = [ Vector(pnt) for pnt in pnts ] + vecs.append(vecs[0]) + wire = Part.makePolygon(vecs) + face = Part.Face(wire) + obj = Part.show(face) + obj.ViewObject.ShapeColor = color + + normal = facet.Normal + c = (vecs[0] + vecs[1] + vecs[2])/3 + p1 = c - 0.5*normal + p2 = c + 2.0*normal + L = Part.LineSegment(p1, p2) + Part.show(L.toShape(), "normal") + + def display_edge(self, edge): + pts = list(edge) + idx1 = pts[0] + idx2 = pts[1] + v1 = Vector(self.mesh.Points[idx1].x, self.mesh.Points[idx1].y, self.mesh.Points[idx1].z) + v2 = Vector(self.mesh.Points[idx2].x, self.mesh.Points[idx2].y, self.mesh.Points[idx2].z) + L = Part.LineSegment(v1, v2) + shape = L.toShape() + # shape.ViewObject.LineColor = (255, 255, 255) + Part.show(shape, "edge") + +def compound_to_solids(shape): + """Convert a Compound (or other) to a list of Solids if possible.""" + if shape.ShapeType == "Solid": + return [shape] + sols = [] + # If we got a Compound of shells / faces, try to make solids from its shells + for sh in shape.Shells: + try: + sld = Part.makeSolid(sh) + except Exception: + continue + if sld.ShapeType == "Solid" and sld.isValid(): + sols.append(sld) + # If there are already Solids in the compound, include them + for s in shape.Solids: + if s.ShapeType == "Solid" and s.isValid(): + sols.append(s) + return sols + +def common_via_bopfeatures(shapeA, shapeB, base_name="CommonTmp"): + """Compute intersection of two Part.Shapes using the same BOPFeatures + path that the GUI 'Common' command uses. Returns the resulting Shape.""" + doc = FreeCAD.ActiveDocument + + # 1) Create temporary Part::Feature objects + objA = doc.addObject("Part::Feature", base_name + "_A") + objA.Shape = shapeA + objB = doc.addObject("Part::Feature", base_name + "_B") + objB.Shape = shapeB + doc.recompute() + + # 2) Run BOPFeatures.make_multi_common on their names + bp = BOPFeatures.BOPFeatures(doc) + common_obj = bp.make_multi_common([objA.Name, objB.Name]) + doc.recompute() + + # 3) Extract the resulting Shape + common_shape = common_obj.Shape + + # (Optional) clean up the temp inputs, keep only result if you like + # doc.removeObject(objA.Name) + # doc.removeObject(objB.Name) + + return common_shape, common_obj + +MAX_DEPTH = 50 +def decompose_recursive(shapeMesher, results, depth=0): + + if depth >= MAX_DEPTH: + results.append(shapeMesher) + return + + mesh = shapeMesher.mesh + concave_edge, nmean, max_angle = shapeMesher.get_concave_edge() + MIN_GLOBAL_ANGLE = math.radians(5.0) + + if concave_edge is None or max_angle < MIN_GLOBAL_ANGLE: + results.append(shapeMesher) + return + + edge_pts = [mesh.Points[pntIndex].Vector for pntIndex in list(concave_edge)] + p0, p1 = edge_pts + edgeVec = p1 - p0 + # DEBUG + # edge_line = Part.makeLine(p0, p1) + # Part.show(edge_line, "edge_line") + + normal = edgeVec.cross(nmean) + if normal.Length == 0: + results.append(shapeMesher) + return + normal.normalize() + + base = shapeMesher.shape + bbox = base.BoundBox + planeWidth = 2 * bbox.DiagonalLength + + # (if you still want to re-center by COM, keep your translation here) + cuttingPlane = Part.makePlane(planeWidth, planeWidth, p0, normal) + # translate plane so center of mass of plane is at edge point + planeCM = cuttingPlane.CenterOfMass + cuttingPlane.Placement.Base -= planeCM - p0 + + if not cuttingPlane.isValid(): + results.append(shapeMesher) + return + + # Make solid from shell + solid = Part.makeSolid(base) + if solid.ShapeType != "Solid" or not solid.isValid(): + results.append(shapeMesher) + return + + # Build two slabs by extruding plane + n = cuttingPlane.normalAt(0.5, 0.5) + if n.Length == 0: + results.append(shapeMesher) + return + n.normalize() + h = solid.BoundBox.DiagonalLength * 2.0 + + upper_extrude = cuttingPlane.extrude(n * h) + lower_extrude = cuttingPlane.extrude(-n * h) + + # Use BOPFeatures (same as GUI) to get the intersections + upper, upper_obj = common_via_bopfeatures(solid, upper_extrude, "UpperCut") + lower, lower_obj = common_via_bopfeatures(solid, lower_extrude, "LowerCut") + + print("upper:", upper.ShapeType, "valid:", upper.isValid(), "vol:", upper.Volume) + print("lower:", lower.ShapeType, "valid:", lower.isValid(), "vol:", lower.Volume) + + child_shapes = [] + + # upper can be a Solid or a Compound; extract solids if needed + if upper.ShapeType == "Solid" and upper.isValid(): + child_shapes.append(upper) + elif upper.Solids: + for s in upper.Solids: + if s.isValid(): + child_shapes.append(s) + + if lower.ShapeType == "Solid" and lower.isValid(): + child_shapes.append(lower) + elif lower.Solids: + for s in lower.Solids: + if s.isValid(): + child_shapes.append(s) + + # If nothing usable came out, stop recursing on this shape + if not child_shapes: + results.append(shapeMesher) + return + + for s in child_shapes: + print(f"child: type={s.ShapeType}, valid={s.isValid()}, vol={s.Volume}") + if not s.isValid(): + continue + + mesher = ShapeMesher(s) + + code = mesher.is_valid() + + if code == 1: # volume too small + continue + elif code == 5: # too few faces + results.append(mesher) + continue + elif code == 4: # normal too small. + results.append(mesher) + continue + + + # if s.Volume < ShapeMesher.deflection**3: + # results.append(ShapeMesher(s)) + # print("Skipped decomposing this shape. Volume too small") + # continue + # should check if the cutting resulted in more than one component + components = mesher.mesh.getSeparateComponents() + if len(components) > 1: + print("** cut mesh has more than one component **") + + decompose_recursive(mesher, results, depth+1) + + +class AutoTessellateExporter(SolidExporter): + + shapesDict = {} # a dictionary of exported shapes and their names + + def __init__(self, obj): + super().__init__(obj) + self.index = 0 + + # So my understanding of the structure of the mesh, so far, is the following + # if E is the number of Edges, F the number of faces and V the number of Volumes + # then Edge_ids go from 1 - E + # Face ids go form E+1 - E+1+F + # and volume ids go from E+F+1+1 - E+F+1+1+V + # so mesh.getElementNode(id) will get the edge node ids of id is in the Edge ids range + # will get the face id nodes for ids in the Face ids range + # and will get the volume id nodes for id in the Volume id range. + # For edges there will be two node ids, corresponding to the ends of the edge + # For faces, there will be three ids, corresponding to the triangle coordinates + # and for Volume ids, there will be four node ids, corresponding to the four corners of + # of the tetrahedron + # mesh.Node[id] gives the Vector coordinates of the node, that is the point + # P.S: Face ids are those of the boundary faces only, not of every tetrahedron surface + + # get a list of planes, one for each face. At present the sense of + # the normal is arbitrary. When we build the regions for each tetrahedron, we will + # figure out the in/out of each plane + + def face_to_plane(self, face): + normal = Vector(face.Normal.x, face.Normal.y, face.Normal.z) + # remember we need to convert from mm to cm + point0 = Vector(face.Points[0][0], face.Points[0][1], face.Points[0][2]) + D = normal.dot(point0) + planesurface = PlaneSurfaceExporter(self.name()+f"_{self.index}", normal, D) + + self.index += 1 + return planesurface + + def remove_redundant_planes(self, planes): + set_of_planes = set() + for plane in planes: + set_of_planes.add(plane) + return list(set_of_planes) + + + def generate_surfaces(self): + from .chatGPT_extrusionExporter import analyze_extruded_mesh + import MeshPart + + if self.obj.TypeId != 'Mesh::Feature': + shape = self.obj.Shape.copy(False) + bbox = shape.BoundBox + minLength = min([bbox.XLength, bbox.YLength, bbox.ZLength]) + + # shape.Placement = FreeCAD.Placement() # remove object's placement + + viewObject = self.obj.ViewObject + deflection = viewObject.Deviation # this is in percent + linearDeflection = deflection/100*minLength + angularDeflection = viewObject.AngularDeflection.Value + ShapeMesher.deflection = linearDeflection + ShapeMesher.angularDeflection = angularDeflection + + shapeMesher = ShapeMesher(shape) + + components = shapeMesher.mesh.getSeparateComponents() + else: + components = self.obj.Mesh.getSeparateComponents() + + self.region = Region("") + for i, comp in enumerate(components): + V = comp.CountPoints + E = comp.CountEdges + F = comp.CountFacets + chi = V - E + F + genus = (2 - chi) / 2 + print(f"Component {i} Verts = {V}, Edges = {E}, Faces = {F}, Eueler = {chi} genus = {genus}") + + shape = Part.Shape() + tolerance = 0.1 + shape.makeShapeFromMesh(comp.Topology, 0.1) + componentMesher = ShapeMesher(shape) + component_region = "" + + concave_face_indexes = set() + # region for concave edges + concave_chains = componentMesher.concave_face_chains() + colors = [(200, 0, 0), (0,200, 0), (0, 0, 200), (200, 0, 200), (0, 200, 200), (200, 200, 0)] + nc = len(colors) + j = 0 + for chain in concave_chains: + chain_region = "" + color = colors[j] + j = (j +1) % nc + for iFace in chain: + face = componentMesher.mesh.Facets[iFace] + surf = self.face_to_plane(face) + self.surfaces.append(surf) + if chain_region == "": + chain_region += f"-{surf.id} " + else: + chain_region += f"| -{surf.id} " + concave_face_indexes.add(iFace) + componentMesher.display_face(iFace, color) + component_region += f"({chain_region}) " + + # non-concave faces + facet_indexes = [facet.Index for facet in componentMesher.mesh.Facets] + for idx in facet_indexes: + if idx in concave_face_indexes: # skip faces that have been already processed as concave faces + continue + face = componentMesher.mesh.Facets[idx] + componentMesher.display_face(idx, (150, 150, 150)) + surf = self.face_to_plane(face) + self.surfaces.append(surf) + component_region += f"-{surf.id} " + + + self.region = self.region.union(Region(component_region)) + + self.position_globally() diff --git a/freecad/gdml/importGDML.py b/freecad/gdml/importGDML.py index c26efe04..a62c6cd5 100644 --- a/freecad/gdml/importGDML.py +++ b/freecad/gdml/importGDML.py @@ -3289,6 +3289,22 @@ def processGEANT4(doc, filename): geant4Grp = newGroupPython(materials, "Geant4") processMaterialsG4(geant4Grp, root) +def processReactor(doc, filename): + print("process Reactor Materials : " + filename) + etree, root = setupEtree(filename) + # etree.ElementTree(root).write("/tmp/test2", 'utf-8', True) + materials = doc.getObject("Materials") + if materials is None: + materials = doc.addObject( + "App::DocumentObjectGroupPython", "Materials" + ) + # Avoid duplicate Geant4 group - Palo import of GDML + reactorGrp = doc.getObject("ReactorMaterials") + if reactorGrp is None: + reactorGrp = newGroupPython(materials, "ReactorMaterials") + + processMaterialsReactor(reactorGrp, root) + def processMaterialsDocSet(doc, root): print("Process Materials DocSet") @@ -3446,6 +3462,24 @@ def processMaterialsG4(G4rp, root): ).version = 1.0 processNewG4(materialsGrp, mats_xml) +def processMaterialsReactor(grp, root): + mats_xml = root.find("materials") + if mats_xml is not None: + try: + isotopesGrp = FreeCAD.ActiveDocument.Isotopes + except: + isotopesGrp = doc.addObject( + "App::DocumentObjectGroupPython", "Isotopes" + ) + processIsotopes(isotopesGrp, mats_xml) + try: + elementsGrp = FreeCAD.ActiveDocument.Elements + except: + elementsGrp = doc.addObject( + "App::DocumentObjectGroupPython", "Elements" + ) + processElements(elementsGrp, mats_xml) + processMaterials(grp, mats_xml) def processDefines(root, doc): GDMLShared.trace("Call set Define") @@ -3545,11 +3579,11 @@ def processGDML(doc, flag, filename, prompt, processType, initFlg): GDMLShared.trace(setup.attrib) preProcessLoops.preprocessLoops(root) - from .GDMLMaterials import getGroupedMaterials from .GDMLMaterials import newGetGroupedMaterials processMaterialsDocSet(doc, root) processGEANT4(doc, joinDir("Resources/Geant4Materials.xml")) + processReactor(doc, joinDir("Resources/ReactorMaterials.xml")) groupMaterials = newGetGroupedMaterials() solids = root.find("solids") diff --git a/freecad/gdml/polygonsHelper.py b/freecad/gdml/polygonsHelper.py new file mode 100644 index 00000000..d9ef41cc --- /dev/null +++ b/freecad/gdml/polygonsHelper.py @@ -0,0 +1,280 @@ +from FreeCAD import Vector +from typing import List, Optional + +# ---------- Helpers ---------- +def polygon_area_signed(vecs): + """Signed area of polygon given as list of FreeCAD.Vector. + >0 for CCW, <0 for CW, 0 if degenerate.""" + area = 0.0 + n = len(vecs) + for i in range(n): + v0 = vecs[i] + v1 = vecs[(i + 1) % n] + area += v0.x * v1.y - v1.x * v0.y + return 0.5 * area + + +def is_point_in_triangle(p, a, b, c, eps=1e-12): + """Check if point p (Vector) is inside or on triangle (a,b,c) using barycentric test.""" + v0 = c - a + v1 = b - a + v2 = p - a + + dot00 = v0.x * v0.x + v0.y * v0.y + dot01 = v0.x * v1.x + v0.y * v1.y + dot02 = v0.x * v2.x + v0.y * v2.y + dot11 = v1.x * v1.x + v1.y * v1.y + dot12 = v1.x * v2.x + v1.y * v2.y + + denom = dot00 * dot11 - dot01 * dot01 + if abs(denom) < eps: + return False # Degenerate triangle + + inv_denom = 1.0 / denom + u = (dot11 * dot02 - dot01 * dot12) * inv_denom + v = (dot00 * dot12 - dot01 * dot02) * inv_denom + w = 1.0 - u - v + + # Allow small negatives due to floating‑point rounding + return (u >= -eps) and (v >= -eps) and (w >= -eps) + + +def is_convex(prev_v, curr_v, next_v, orientation_ccw, eps=1e-12): + """ + Test if vertex curr_v is convex given neighbors prev_v and next_v. + orientation_ccw: True if polygon is CCW, False if CW. + All vectors are FreeCAD.Vector with z ~ 0. + """ + v1 = curr_v - prev_v + v2 = next_v - prev_v + cross_z = v1.x * v2.y - v1.y * v2.x + if orientation_ccw: + return cross_z > eps + else: + return cross_z < -eps + + +# ---------- Main triangulation (returns triangles as Vectors) ---------- + +def triangulate_polygon_earclip(vecs): + """ + Triangulate a simple polygon (possibly concave) using ear clipping. + + vecs: list of FreeCAD.Vector vertices, in order (CW or CCW), not repeated first/last. + Assumed to lie in plane z=0 (XY plane). + + Returns: + triangles: list of (v0, v1, v2) where each v* is a FreeCAD.Vector. + Raises: + ValueError if polygon is too small, degenerate, or appears not simple. + """ + n = len(vecs) + if n < 3: + raise ValueError("Need at least 3 points to form a polygon") + if n == 3: + return [(vecs[0], vecs[1], vecs[2])] + + # Determine orientation from signed area + area = polygon_area_signed(vecs) + if abs(area) < 1e-16: + raise ValueError("Degenerate polygon (area ~ 0)") + orientation_ccw = (area > 0.0) + + # Work on a list of indices referencing vecs + V = list(range(n)) + tri_indices = [] + + max_iter = 5 * n * n + iters = 0 + + while len(V) > 3 and iters < max_iter: + iters += 1 + ear_found = False + + for i in range(len(V)): + i_prev = V[(i - 1) % len(V)] + i_curr = V[i] + i_next = V[(i + 1) % len(V)] + + p_prev = vecs[i_prev] + p_curr = vecs[i_curr] + p_next = vecs[i_next] + + # 1. convex corner? + if not is_convex(p_prev, p_curr, p_next, orientation_ccw): + continue + + # 2. no other vertex inside this ear? + a, b, c = p_prev, p_curr, p_next + ear_ok = True + for j in V: + if j in (i_prev, i_curr, i_next): + continue + if is_point_in_triangle(vecs[j], a, b, c): + ear_ok = False + break + + if not ear_ok: + continue + + # 3. ear found, clip it + tri_indices.append((i_prev, i_curr, i_next)) + del V[i] + ear_found = True + break + + if not ear_found: + raise ValueError( + "No ear found; polygon may be self‑intersecting " + "or numerically problematic" + ) + + if len(V) == 3: + tri_indices.append((V[0], V[1], V[2])) + elif len(V) > 3: + raise ValueError("Ear clipping failed to reduce polygon to triangles") + + # Convert index triples to Vector triples + triangles = [[vecs[i], vecs[j], vecs[k]] for (i, j, k) in tri_indices] + return triangles + + + + +# ------------------------------------------------------------ +# Assumptions about your Vector class: +# - has __eq__(self, other) +# - has __sub__(self, other) +# - has __add__(self, other) +# - has cross(self, other) -> Vector (3D cross product) +# - attributes .x, .y, .z (z will be 0 for all polygon vertices) +# +# Below I include: +# 1) Ear-clipping triangulation using Vector +# 2) Convex decomposition (triangulate + greedy merging) +# ------------------------------------------------------------ + +# ========================= +# Basic geometry helpers +# ========================= + +def cross_z(o: Vector, a: Vector, b: Vector) -> float: + """ + Oriented 2D cross product using the z-component of 3D cross: + returns z-component of (a - o) x (b - o). + > 0 → OAB is CCW in the x-y plane. + """ + oa = a - o + ob = b - o + return (oa.cross(ob)).z + + +def is_convex_polygon(poly: List[Vector]) -> bool: + """ + Check if a simple polygon (CCW) is convex. + For a CCW polygon, all consecutive edge cross products must be >= 0. + """ + n = len(poly) + if n < 3: + return False + + for i in range(n): + a = poly[i] + b = poly[(i + 1) % n] + c = poly[(i + 2) % n] + cr = cross_z(a, b, c) + if cr < 0: + return False + return True + + +def inner_outer(polygon): + ''' given a polygon (set of vertexes) in the x-z plane + return two sets of vertexes, an outer one and an inner one + Find minz, maxz. All vertexes on the inside have one or more edges to their right. + All vertexes on the outer have one or more edges to their left + + Assumption: the vertexes are already arranged in CCW in the x-z plane + ''' + + minz = min([v.z for v in polygon]) + maxz = max([v.z for v in polygon]) + + istart = 0 + for i, v in enumerate(polygon): + if v.z == minz: + istart = i + break + + n = len(polygon) + outer = [] + # find the outer vertexes. Since the polygon is arranged in CCW as we + # climb from minz, we are climbing on the outside + for i in range(istart, istart+n, 1): + v = polygon[i % n] + if v.z < maxz: + outer.append(v) + else: + outer.append(v) + break + + istart = 0 + for i, v in enumerate(polygon): + if v.z == maxz: + istart = i + break + + # find the inner vertexes. Since the polygon is arranged in CCW as we + # descend from maxz, we are descending on the inside + # note that both outer an inner polygon share the minz and maxz vertexes + inner = [] + for i in range(istart, istart+n, 1): + v = polygon[i % n] + if v.z > minz: + inner.append(v) + else: + inner.append(v) + break + + return inner, outer + + +def convex_hull(verts): + ''' + My first attempt at returning the convex hull of a a polygon: Note we already have a CCW polygon, + the points are NOT arbitrary points in space. + :returns: a list of lists of vertexes, The first list is the convex hull, the remaining lists are edge + triangle vertexes that have been removed from the hull + ''' + + done = False + len_previous = len(verts) + n = len(verts) + hull = [i for i in range(n)] # indexes of vertexes that make up the hull. We start with all polygon vertexes + clipped_triangles = [] + while True: + iremoved = [] + for i in range(n): + iprev = (i-1) % n + inext = (i+1) % n + v1 = verts[hull[i]] - verts[hull[iprev]] + v2 = verts[hull[inext]] -verts[hull[i]] + if (v1.cross(v2)).z < 0: + iremoved.append(hull[i]) + clipped_triangle = [hull[iprev], hull[i], hull[inext]] + clipped_triangles.append(clipped_triangle) + for i in iremoved: + hull.remove(i) + if len(hull) == n: + break + n = len(hull) + + return [hull, clipped_triangles] + + + + + + + + diff --git a/freecad/gdml/revolveHelper.py b/freecad/gdml/revolveHelper.py new file mode 100644 index 00000000..d67c68f5 --- /dev/null +++ b/freecad/gdml/revolveHelper.py @@ -0,0 +1,178 @@ + +import math + +# ---------- 2D helpers ---------- + +def polygon_centroid_xz(points): + """Approximate centroid of a simple polygon in XZ plane (CCW or CW).""" + n = len(points) + if n == 0: + raise ValueError("Empty polygon") + if n == 1: + return points[0] + if n == 2: + return (0.5*(points[0][0] + points[1][0]), + 0.5*(points[0][1] + points[1][1])) + + A = 0.0 + Cx = 0.0 + Cz = 0.0 + for (x0, z0), (x1, z1) in zip(points, points[1:] + points[:1]): + cross = x0 * z1 - x1 * z0 + A += cross + Cx += (x0 + x1) * cross + Cz += (z0 + z1) * cross + A *= 0.5 + if abs(A) < 1e-16: + # Degenerate-ish; fallback to average + x_mean = sum(p[0] for p in points) / n + z_mean = sum(p[1] for p in points) / n + return x_mean, z_mean + + Cx /= (6.0 * A) + Cz /= (6.0 * A) + return Cx, Cz + + +def edge_normal_2d(p0, p1): + """ + 2D outward normal (nx, nz) for edge p0->p1 in XZ plane, + assuming polygon is CCW. + """ + x0, z0 = p0 + x1, z1 = p1 + dx = x1 - x0 + dz = z1 - z0 + length = math.hypot(dx, dz) + if length == 0.0: + return 0.0, 0.0 + nx = dz / length + nz = -dx / length + return nx, nz + + +# ---------- Interior 3D point for the swept solid ---------- + +def interior_3d_point_from_profile(profile_xz, phi): + """ + Given a 2D profile polygon in XZ (possibly concave), find a 3D point + that lies in the interior of its solid of revolution. + """ + if profile_xz[0] == profile_xz[-1]: + pts = profile_xz[:-1] + else: + pts = profile_xz + + cx, cz = polygon_centroid_xz(pts) + theta_mid = 0.5 * phi + x_int = cx * math.cos(theta_mid) + y_int = cx * math.sin(theta_mid) + z_int = cz + return x_int, y_int, z_int + + +# ---------- Classify each profile segment ---------- + +def classify_profile_edges(profile_xz, eps=1e-9): + """ + For one closed polygon (possibly concave), revolved about Z by angle phi, + determine for each 2D edge: + + - what 3D surface type it becomes: 'z_plane', 'cylinder', 'cone' + - geometric parameters you may need (z_const, R, p0, p1, line normal) + + Returns: + segments_info: list of dicts, one per segment + """ + # Ensure closed + if profile_xz[0] != profile_xz[-1]: + pts = profile_xz + [profile_xz[0]] + else: + pts = list(profile_xz) + + segments_info = [] + + # Precompute centroid for cone sign in 2D + cx, cz = polygon_centroid_xz(pts[:-1]) + + for (x0, z0), (x1, z1) in zip(pts, pts[1:]): + dx = x1 - x0 + dz = z1 - z0 + + if abs(dz) < eps and abs(dx) > eps: + # Horizontal edge -> plane z = z0 + z_const = z0 + # Implicit f = z - z_const + + segments_info.append({ + 'type': 'z_plane', + 'z': z_const, + 'p0': (x0, z0), + 'p1': (x1, z1) + }) + + elif abs(dx) < eps and abs(dz) > eps: + # Vertical edge -> cylinder radius |x0| + x_const = x0 + R = abs(x_const) + if R < eps: + # Edge exactly on axis: special (often a plane through axis). + # Handle as needed in your own code. + continue + + segments_info.append({ + 'type': 'cylinder', + 'R': R, + 'p0': (x0, z0), + 'p1': (x1, z1), + }) + + else: + # Slanted edge -> cone (or plane through axis) + # Use 2D geometry to determine which side is interior. + nx, nz = edge_normal_2d((x0, z0), (x1, z1)) + + segments_info.append({ + 'type': 'cone', + 'p0': (x0, z0), + 'p1': (x1, z1), + 'nx_2d': nx, + 'nz_2d': nz + }) + + return segments_info + + +# ---------- Top-level: classify one closed polygon ---------- + +def classify_revolved_polygon(profile_xz, eps=1e-9): + """ + Given a single closed polygon (possibly concave) in XZ (CCW) and a + classify all surfaces for the corresponding + solid of revolution. + + Returns: + segments_info: list of dicts for each profile edge: + - 'type': 'z_plane' | 'cylinder' | 'cone' + - geometry params (z, R, p0,p1 or 2D line data) + """ + segments_info = classify_profile_edges(profile_xz, eps=eps) + return segments_info +''' +or each entry in `segments_info`: + - If `type == 'z_plane'`: + - You already know how to emit a `` with `z = info['z']`. + - `inside_is_negative` tells you whether the “inside” side is `f(z) <= 0` (your “−” side) or `f(z) >= 0`. + - If `type == 'cylinder'`: + - Emit a `` of radius `R = info['R']` around the z‑axis. + - `inside_is_negative` decides whether `r <= R` or `r >= R` is inside. + - If `type == 'cone'`: + - Use your own line‑segment‑to‑cone logic on `p0,p1`, + + +The region for that one polygon’s solid of revolution is simply: + +- The intersection of **all** those half‑spaces (every segment’s surface + radial planes, if any), with the sign given by `inside_is_negative`. + +That holds whether the polygon is convex or concave, as long as it’s a single, simple loop (no self‑intersections, no holes). +''' \ No newline at end of file diff --git a/temp.txt b/temp.txt new file mode 100644 index 00000000..5cd353a5 --- /dev/null +++ b/temp.txt @@ -0,0 +1,13 @@ +cp ../../SymmetricGDML/freecad/gdml/arrayUtils.py ./freecad/gdml/arrayUtils.py +cp ../../SymmetricGDML/freecad/gdml/exportGDML.py ./freecad/gdml/exportGDML.py +cp ../../SymmetricGDML/freecad/gdml/exportOpenMC.py ./freecad/gdml/exportOpenMC.py +cp ../../SymmetricGDML/freecad/gdml/GDMLMaterials.py ./freecad/gdml/GDMLMaterials.py +cp ../../SymmetricGDML/freecad/gdml/GDMLObjects.py ./freecad/gdml/GDMLObjects.py +cp ../../SymmetricGDML/freecad/gdml/GDMLShared.py ./freecad/gdml/GDMLShared.py +cp ../../SymmetricGDML/freecad/gdml/importGDML.py ./freecad/gdml/importGDML.py +cp ../../SymmetricGDML/freecad/gdml/__init__.py ./freecad/gdml/__init__.py +cp ../../SymmetricGDML/freecad/gdml/polygonsHelper.py .//freecad/gdml/polygonsHelper.py +cp ../../SymmetricGDML/freecad/gdml/Resources/Isotopic_abundances.csv ./freecad/gdml/Resources/Isotopic_abundances.csv +cp ../../SymmetricGDML/freecad/gdml/Resources/ReactorMaterials.xml ./freecad/gdml/Resources/ReactorMaterials.xml +cp ../../SymmetricGDML/freecad/gdml/revolveHelper.py ./freecad/gdml/revolveHelper.py +cp ../../SymmetricGDML/GDML_math.md ./ From 841c770f3043c9e985ba50e846344b36e3288664 Mon Sep 17 00:00:00 2001 From: Munther Hindi Date: Sun, 18 Jan 2026 17:36:07 -0800 Subject: [PATCH 3/8] update to SymmetricGDML openmc-alpha branch --- temp.txt | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 temp.txt diff --git a/temp.txt b/temp.txt deleted file mode 100644 index 5cd353a5..00000000 --- a/temp.txt +++ /dev/null @@ -1,13 +0,0 @@ -cp ../../SymmetricGDML/freecad/gdml/arrayUtils.py ./freecad/gdml/arrayUtils.py -cp ../../SymmetricGDML/freecad/gdml/exportGDML.py ./freecad/gdml/exportGDML.py -cp ../../SymmetricGDML/freecad/gdml/exportOpenMC.py ./freecad/gdml/exportOpenMC.py -cp ../../SymmetricGDML/freecad/gdml/GDMLMaterials.py ./freecad/gdml/GDMLMaterials.py -cp ../../SymmetricGDML/freecad/gdml/GDMLObjects.py ./freecad/gdml/GDMLObjects.py -cp ../../SymmetricGDML/freecad/gdml/GDMLShared.py ./freecad/gdml/GDMLShared.py -cp ../../SymmetricGDML/freecad/gdml/importGDML.py ./freecad/gdml/importGDML.py -cp ../../SymmetricGDML/freecad/gdml/__init__.py ./freecad/gdml/__init__.py -cp ../../SymmetricGDML/freecad/gdml/polygonsHelper.py .//freecad/gdml/polygonsHelper.py -cp ../../SymmetricGDML/freecad/gdml/Resources/Isotopic_abundances.csv ./freecad/gdml/Resources/Isotopic_abundances.csv -cp ../../SymmetricGDML/freecad/gdml/Resources/ReactorMaterials.xml ./freecad/gdml/Resources/ReactorMaterials.xml -cp ../../SymmetricGDML/freecad/gdml/revolveHelper.py ./freecad/gdml/revolveHelper.py -cp ../../SymmetricGDML/GDML_math.md ./ From 85616eca3b2fbe9a66cdaa0b05a5e4339e15add9 Mon Sep 17 00:00:00 2001 From: Munther Hindi Date: Wed, 28 Jan 2026 21:39:18 -0800 Subject: [PATCH 4/8] fixed links, defines --- freecad/gdml/GDMLShared.py | 4 +- freecad/gdml/exportOpenMC.py | 713 ++++++++++++++++++++++++++++++----- 2 files changed, 620 insertions(+), 97 deletions(-) diff --git a/freecad/gdml/GDMLShared.py b/freecad/gdml/GDMLShared.py index 8e1f523d..5eb409c2 100644 --- a/freecad/gdml/GDMLShared.py +++ b/freecad/gdml/GDMLShared.py @@ -761,8 +761,8 @@ def extract_variables(expression): def adjust_floats(expression) -> str: - # change floats of the form d. to d.0 - pattern = r'\b\d[.]\w*' + # change floats of the form d[dd..]. to d.0 + pattern = r'\b\d+[.]\w*' # Find all matches in the expression floats = re.findall(pattern, expression) diff --git a/freecad/gdml/exportOpenMC.py b/freecad/gdml/exportOpenMC.py index d97fbc43..c626649e 100644 --- a/freecad/gdml/exportOpenMC.py +++ b/freecad/gdml/exportOpenMC.py @@ -43,6 +43,7 @@ from FreeCAD import Vector import BOPTools.SplitAPI from BOPTools import BOPFeatures +from collections import defaultdict import random @@ -85,15 +86,6 @@ element_table = None -from .GDMLObjects import ( - GDMLfraction, - GDMLcomposite, - GDMLisotope, - GDMLconstant, - GDMLvariable, - GDMLquantity, -) - from . import GDMLShared @@ -290,12 +282,6 @@ def place(self, volRef): self.assembly = assembly -class PhysVolPlacement: - def __init__(self, ref, placement): - self.ref = ref # name reference: a string - self.placement = placement # physvol placement - - ######################################################### # Pretty format GDML # ######################################################### @@ -747,7 +733,25 @@ def processPlacement(name, xml, placement): import re +def elementSymbol_from_Z(Z): + '''return element symbol for element with atomic number Z''' + global element_table + for sym in element_table: + elementIsotopes = element_table[sym] + if elementIsotopes.Z == Z: + return sym + return None + + + def elementSymbol(elem): + # first try an actual element define via material + obj = FreeCAD.ActiveDocument.getObject(elem) + if hasattr(obj, 'Z'): + Z = int(obj.Z) + sym = elementSymbol_from_Z(Z) + return sym + pattern = r'^([A-Z][a-z]?)$' # Element symbol, say C, or Cd match = re.match(pattern, elem) if match: @@ -769,7 +773,7 @@ def elementSymbol(elem): # Try element defined in Elements Group elem_grp = elementGroup(elem) - if elem_grp is not None: + if elem_grp is not None and hasattr(elem_grp, 'formula'): sym = elem_grp.formula return sym @@ -778,8 +782,16 @@ def elementSymbol(elem): def elementGroup(elem): elementsGroup = FreeCAD.ActiveDocument.getObject('Elements') + elem1 = elem[:] + if elem1.endswith("_element"): + elem1 = elem1[:-len("_element")] + for grp in elementsGroup.Group: - if grp.Label == elem: + label = grp.Label + if label.endswith("_element"): + label = label[:-len("_element")] + + if label == elem1: return grp return None @@ -787,6 +799,7 @@ def elementGroup(elem): def materialGroup(mat): materialGroup = FreeCAD.ActiveDocument.getObject('Materials') + for grp in materialGroup.Group: if grp.Label == mat: return grp @@ -800,7 +813,7 @@ def merge_lists(list1, list2): and list2 is [{nuclide: C,'fraction': fC, 'type': typeC}, {nuclide: A,'fraction': fA2, 'type': typeA2},...] - then we eant to return a list + then we want to return a list [{'nuclide': A, 'fraction': fA+fA2, 'type': typeA}, {'nuclide': 'B', ....}, {'nuclide': C, ...}] ''' @@ -888,7 +901,7 @@ def material_nuclides(mat): weight_fraction = float(grp.Label[1+grp.Label.find(':'):]) print(mat_or_element) sym = elementSymbol(mat_or_element) - if sym is not None: # The mixture invloves a natural element + if sym is not None: # The mixture involves a natural element component_list = material_nuclides(mat_or_element) for component in component_list: isotopeName = component['nuclide'] @@ -897,7 +910,8 @@ def material_nuclides(mat): # for mixtures (of elements) the fractions should be weight fractions # The components returned above are for an element an so need to convert to weight_fractions component['fraction'] *= weight_fraction * isotopeObject.atomic_weight/isotopeObject.element.atomic_weight - nuclide_list = merge_lists(nuclide_list, component_list) + component['type'] = 'wo' + nuclide_list = merge_lists(nuclide_list, component_list) continue print(f"checking if {grp.Label} is defined in the Elements Group") @@ -915,7 +929,7 @@ def material_nuclides(mat): # but here we are processing mixtures, the fractions should be WEIGHT fractions # so we need to convert isotopeObject = nuclide_table[isotope_name] - nuc_dict['fraction'] = isotope_fraction * isotopeObject.atomic_weight/isotopeObject.element.atomic_weight + nuc_dict['fraction'] = weight_fraction*isotope_fraction * isotopeObject.atomic_weight/isotopeObject.element.atomic_weight nuc_dict['type'] = 'wo' nuclide_list.append(nuc_dict) continue @@ -939,7 +953,7 @@ def material_nuclides(mat): if component['type'] == 'ao': nuclideName = component['nuclide'] nuclideObject = nuclide_table[nuclideName] - component['fraction'] *= nuclideObject.atomic_weight/nuclideObject.element.atomic_weight + component['fraction'] *= weight_fraction*nuclideObject.atomic_weight/nuclideObject.element.atomic_weight # update the current nuclide_list with that of the current material nuclide_list = merge_lists(nuclide_list, component_list) @@ -1029,7 +1043,10 @@ def createMaterials(group): else: # default to g/cm3, if no Dunit is given D.set("units", 'g/cm3') if hasattr(obj, "Dvalue"): - D.set("value", str(obj.Dvalue*unit_multiplier)) + density = float(obj.Dvalue) + if obj.Dvalue <= 0: + density = 1.0e-25 + D.set("value", str(density*unit_multiplier)) # process common options material / element nuclides_in_material = material_nuclides(mat) @@ -1230,8 +1247,6 @@ def isArrayOfPart(obj): def processArrayPart(array): # array: array object - global physVolStack - global universe_dict from . import arrayUtils # array: Array item # new approach 2024-08-07: @@ -1309,20 +1324,64 @@ def createWorldVol(volName): ET.SubElement(gxml, "volume", {"name": volName, "material": "G4_AIR"}) return worldVol + +def get_global_placement(obj): + """ + Placement of obj in the current traversal context. + + If the current logical parent on parent_stack is an App::Link, + use that link's hierarchy; otherwise fall back to the object's + own global placement in the document. + + GPT-5.1 crap that does not work + if parent_stack: + parent = parent_stack[-1] + if parent.TypeId == "App::Link" and getattr(parent, "LinkedObject", None): + try: + # Placement via the Link's hierarchy: includes link.Placement + return parent.getSubObject(obj.Name, retType=3) + except Exception: + pass + + # Generic / non-link context + try: + return obj.getSubObject("", retType=3) + except Exception: + return obj.Placement + + """ + + pl = obj.Placement + for parent in reversed(parent_stack): + pl = parent.Placement*pl + + return pl + + +#------------------------------------------------------------------------------ def buildDocTree(): from PySide import QtWidgets global obj_top_children_dict + global parent_stack + obj_top_children_dict = {} # dictionary of list of child objects for each object + parent_stack = [] # a LIFO of the parent of the current child being processed by exporters + # TypeIds that should not go in to the tree - skippedTypes = ["App::Origin", "Sketcher::SketchObject", "Part::Compound"] + skippedTypes = ["App::Origin", "Sketcher::SketchObject", "Part::Compound", "App::FeaturePython", "Points::Feature"] def addDaughters(item: QtWidgets.QTreeWidgetItem): print (f"--------addDaughters {item.text(0)}") objectLabel = item.text(0) object = App.ActiveDocument.getObjectsByLabel(objectLabel)[0] + if object not in obj_top_children_dict: obj_top_children_dict[object] = [] + + if SolidExporter.isSolid(object): # that takes care of arrays and booleans + return + for i in range(item.childCount()): childItem = item.child(i) treeLabel = childItem.text(0) @@ -1377,6 +1436,24 @@ def addDaughters(item: QtWidgets.QTreeWidgetItem): FreeCADobject = None + # Links will have empty children with above because iemchildcount is zero for links + # so we populate links with the children of the object they are linked to + for obj in obj_top_children_dict: + if obj.TypeId == "App::Link": + target = obj.LinkedObject + if target in obj_top_children_dict: + obj_top_children_dict[obj] = obj_top_children_dict[target] + + +def dot_separated_path_to(target): + global paretn_stack + + ''' Return a string of the form rootParentLabel.parent_1_Label.parent_2_Label...target Label''' + path = parent_stack[:] + return ".".join(obj.Label for obj in path[1:])+"."+target.Label +#------------------------------------------------------------------------------ + + def isContainer(obj): # return True if The App::Part is of the form: # App::Part @@ -1448,10 +1525,11 @@ def isAssembly(obj): print(f"testing isAsembly for: {obj.Label}") if obj.TypeId != "App::Part": - return False + if (obj.TypeId == "App::Link" and obj.LinkedObject.TypeId != "App::Part"): + return False for ob in obj_top_children_dict[obj]: - if ob.TypeId == "App::Part" or ob.TypeId == "App::Link": + if ob.TypeId == "App::Part" or (ob.TypeId == "App::Link" and ob.LinkedObject.TypeId == "App::Part"): print(True) return True # Yes, even if ONE App::Part is under this, we treat it as an assembly @@ -1723,6 +1801,22 @@ def __init__(self, obj): @staticmethod def getExporter(obj): + ''' + if obj.TypeId == "App::Link": + target = obj.LinkedObject + doc = FreeCAD.ActiveDocument + children = [doc.copyObject(child, True) for child in obj_top_children_dict[target]] + obj_top_children_dict[obj] = children + for child in children: + obj_top_children_dict[child] = [] + if len(children) == 1: + return SingleVolumeExporter(obj) + elif len(children) > 1: + return AssemblyExporter(obj) + else: + return None + ''' + if isContainer(obj): return ContainerExporter(obj) elif isAssembly(obj): @@ -1791,9 +1885,18 @@ def export(self): if self.obj == worldObject: self.containerSolidExporter.set_boundary_type("vacuum") - self.containerSolidExporter.export() # This exports the SURFACES of the container - for child_geom_exporter in self.children_geom_exporters: - child_geom_exporter.export() + # Push this container as current parent + parent_stack.append(self.obj) + try: + # Export container's own surfaces + self.containerSolidExporter.export() + + # Export all children under this container + for child_geom_exporter in self.children_geom_exporters: + child_geom_exporter.export() + finally: + parent_stack.pop() + region = self.myRegion() # material is that of the containing box mat_name = getMaterial(self.containerSolidExporter.obj) @@ -1808,7 +1911,7 @@ def __init__(self, obj): firstChild = obj_top_children_dict[obj][0] arrayOfPart = False - if isArrayType(firstChild) and obj_top_children_dict[firstChild][0].TypeId == "App::Part": + if isArrayType(firstChild) and firstChild.Base.isDerivedFrom("App::Part"): processArrayPart(firstChild) arrayOfPart = True @@ -1830,13 +1933,18 @@ def getRegion(self): return children_region def export(self): - for child_geom_exporter in self.children_geom_exporters: - child_geom_exporter.export() + # Push this assembly as current parent + parent_stack.append(self.obj) + try: + for child_geom_exporter in self.children_geom_exporters: + child_geom_exporter.export() + finally: + parent_stack.pop() class SingleVolumeExporter(GeomObjExporter): ''' - A Logical volume of the forma: + A Logical volume of the form: App::Part Solid ''' @@ -1859,10 +1967,20 @@ def getRegion(self): def export(self): if self.child_solid_exporter is None: return - self.child_solid_exporter.export() + + # Treat this volume (App::Part or App::Link) as the current parent + parent_stack.append(self.obj) + try: + # Inside here, the solid exporter will see self.obj as parent + self.child_solid_exporter.export() + finally: + parent_stack.pop() + mat_name = getMaterial(self.child_solid_exporter.obj) solidRegion = self.child_solid_exporter.get_region() - cellExporter = CellExporter(self.obj.Label, material_name=mat_name, region=solidRegion.expr) + cellExporter = CellExporter(self.obj.Label, + material_name=mat_name, + region=solidRegion.expr) cellExporter.export() @@ -2215,6 +2333,11 @@ def __init__(self, name, type, coeffs, boundary="transmission"): self.boundary = boundary self.coeffs = coeffs + self.saved_state = vars(self).copy() + + def copy(self): + return # should be declared abstract + @staticmethod def reset_ids(): SurfaceExporter._ids = [] @@ -2285,6 +2408,11 @@ def __init__(self, name, center, radius): self.radius = 0.1*radius self.to_coeffs() super().__init__(name, "sphere", self.coeffs) + self.saved_radius = radius + self.saved_center = center + + def copy(self): + return SphereSurfaceExporter(self.name, self.saved_center, self.saved_radius) def to_coeffs(self): self.coeffs = f"{self.center.x} {self.center.y} {self.center.z} {self.radius}" @@ -2307,6 +2435,13 @@ def __init__(self, name, center, axis, radius): self.radius = 0.1*radius self.to_quadric() super().__init__(name, "quadric", self.coeffs) + self.saved_radius = radius + self.saved_center = center + self.saved_axis = axis + + + def copy(self): + return CylinderSurfaceExporter(self.name, self.saved_center, self.saved_axis, self.saved_radius) def to_quadric(self): A, B, C, D, E, F, G, H, J, K = quadric_coeffs(cylinder, radius=self.radius, @@ -2335,6 +2470,15 @@ def __init__(self, name, center,dx, dy): self.rotation = FreeCAD.Rotation() self.to_quadric() super().__init__(name, "quadric", self.coeffs) + self.saved_dx = dx + self.saved_dy = dy + self.saved_rotation = FreeCAD.Rotation() + self.saved_center = center + + + def copy(self): + return EllipticalCylinderSurfaceExporter(self.name, self.saved_center, self.saved_dx, self.saved_dy) + def to_quadric(self): A, B, C, D, E, F, G, H, J, K = quadric_coeffs(elliptical_tube, dx=self.dx, dy=self.dy, @@ -2364,6 +2508,16 @@ def __init__(self, name, center, dx, dy, zHeight): self.rotation = FreeCAD.Rotation() self.to_quadric() super().__init__(name, "quadric", self.coeffs) + self.saved_center = center + self.saved_dx = dx + self.saved_dy = dy + self.saved_zHeight = zHeight + self.saved_rotation = FreeCAD.Rotation() + + + def copy(self): + return EllipticalConeSurfaceExporter(self.name, self.saved_center, self.saved_dx, self.saved_dy, self.saved_zHeight) + def to_quadric(self): A, B, C, D, E, F, G, H, J, K = quadric_coeffs(elliptical_cone, dx=self.dx, dy=self.dy, @@ -2399,6 +2553,13 @@ def __init__(self, name, center, axis, theta): self.to_quadric() super().__init__(name, "quadric", self.coeffs) + self.saved_center = center + self.saved_axis = axis + self.saved_theta = theta + + def copy(self): + return ConeSurfaceExporter(self.name, self.saved_center, self.saved_axis, self.saved_theta) + def to_quadric(self): A, B, C, D, E, F, G, H, J, K = quadric_coeffs(cone, center=self.center, axis=self.axis, theta=self.theta) self.coeffs = f"{A} {B} {C} {D} {E} {F} {G} {H} {J} {K}" @@ -2432,6 +2593,15 @@ def __init__(self, name, ax, by, cz, center=Vector(0, 0, 0), rotation=FreeCAD.Ro self.to_quadric() super().__init__(name, "quadric", self.coeffs) + self.saved_ax = ax + self.saved_by = by + self.saved_cz = cz + self.saved_center = center + self.saved_rotation = rotation + + + def copy(self): + return EllipsoidSurfaceExporter(self.name, self.saved_ax, self.saved_by, self.saved_cz, self.saved_center, self.saved_rotation) def to_quadric(self): A, B, C, D, E, F, G, H, J, K = quadric_coeffs(ellipsoid, ax=self.ax, by=self.by, cz=self.cz, @@ -2483,6 +2653,15 @@ def __init__(self, name, rlo, rhi, dz, center=Vector(0, 0, 0), rotation=FreeCAD. self.to_quadric() super().__init__(name, "quadric", self.coeffs) + self.saved_rlo = rlo + self.saved_rhi = rhi + self.saved_dz = dz + self.saved_center = center + self.saved_rotation = rotation + + def copy(self): + return ParaboloidSurfaceExporter(self.name, self.saved_rlo, self.saved_rhi, self.saved_dz, self.saved_center, self.saved_rotation) + def to_quadric(self): A, B, C, D, E, F, G, H, J, K = quadric_coeffs(paraboloid, k1=self.k1, k2=self.k2, center=self.center, rotation=self.rotation) @@ -2580,6 +2759,10 @@ def isSolid(obj): # optional: treat Body as container rather than primitive return False + # we tread arrays of parts as containers, not as solids + if isArrayType(obj1) and obj1.Base.isDerivedFrom("App::Part"): + return False + # Part::Compound is a grey zone: # treat it as "real shape" or as aggregate depending on your needs # Example: treat as aggregate and skip here: @@ -2598,6 +2781,11 @@ def isSolid(obj): @staticmethod def getExporter(obj): + if hasattr(obj, 'LinkedObject'): + solidExporter = SolidExporter.getExporter(obj.LinkedObject) + if solidExporter is not None: + return type(solidExporter)(obj) + if obj.TypeId == "Part::FeaturePython": if hasattr(obj.Proxy, 'Type'): typeId = obj.Proxy.Type @@ -2704,7 +2892,8 @@ def set_boundary_type(self, boundary_type): def position_globally(self): identity = FreeCAD.Placement() - placement = self.obj.getGlobalPlacement() + + placement = get_global_placement(self.obj) if placement != identity: rot = placement.Rotation @@ -2713,7 +2902,6 @@ def position_globally(self): surf.rotate(rot) surf.translate(trans) - def getMult(self): ''' return multiplier for length units of self.obj''' unit = "mm" # set default @@ -2883,21 +3071,21 @@ def generate_surfaces(self): normal = Vector(1, 0, 0) D = 0 - x1surface = PlaneSurfaceExporter(f"{self.obj.Label}_x1_plane)", normal, D) + x1surface = PlaneSurfaceExporter(f"{self.obj.Label}_x1_plane", normal, D) D = self.obj.Length.Value - x2surface = PlaneSurfaceExporter(f"{self.obj.Label}_x2_plane)", normal, D) + x2surface = PlaneSurfaceExporter(f"{self.obj.Label}_x2_plane", normal, D) normal = Vector(0, 1, 0) D = 0 - y1surface = PlaneSurfaceExporter(f"{self.obj.Label}_y1_plane)", normal, D) + y1surface = PlaneSurfaceExporter(f"{self.obj.Label}_y1_plane", normal, D) D = self.obj.Width.Value - y2surface = PlaneSurfaceExporter(f"{self.obj.Label}_y2_plane)", normal, D) + y2surface = PlaneSurfaceExporter(f"{self.obj.Label}_y2_plane", normal, D) normal = Vector(0, 0, 1) D = 0 - z1surface = PlaneSurfaceExporter(f"{self.obj.Label}_z1_plane)", normal, D) + z1surface = PlaneSurfaceExporter(f"{self.obj.Label}_z1_plane", normal, D) D = self.obj.Height.Value - z2surface = PlaneSurfaceExporter(f"{self.obj.Label}_z2_plane)", normal, D) + z2surface = PlaneSurfaceExporter(f"{self.obj.Label}_z2_plane", normal, D) self.surfaces = [x1surface, x2surface, y1surface, y2surface, z1surface, z2surface] self.region = Region(f"+{x1surface.id} -{x2surface.id} +{y1surface.id} -{y2surface.id} +{z1surface.id} -{z2surface.id}") @@ -3055,27 +3243,27 @@ def generate_surfaces(self): z1 = self.obj.Radius * math.sin(math.radians(self.obj.Angle1)) normal = Vector(0, 0, 1) D = z1 - z1surface = PlaneSurfaceExporter(f"{self.obj.Label}_z1_plane)", normal, D) + z1surface = PlaneSurfaceExporter(f"{self.obj.Label}_z1_plane", normal, D) self.surfaces.append(z1surface) if self.obj.Angle2 != 0: z2 = self.obj.Radius * math.sin(math.radians(self.obj.Angle2)) normal = Vector(0, 0, 1) D = z2 - z2surface = PlaneSurfaceExporter(f"{self.obj.Label}_z2_plane)", normal, D) + z2surface = PlaneSurfaceExporter(f"{self.obj.Label}_z2_plane", normal, D) self.surfaces.append(z2surface) if self.obj.Angle3 != 360: phi0_normal = Vector(0, 1, 0) D = 0 - phi0_plane = PlaneSurfaceExporter(f"{self.obj.Label}_pho0_plane)", phi0_normal, D) + phi0_plane = PlaneSurfaceExporter(f"{self.obj.Label}_pho0_plane", phi0_normal, D) self.surfaces.append(phi0_plane) nx = -math.sin(math.radians(self.obj.Angle3)) ny = math.cos(math.radians(self.obj.Angle3)) phi1_normal = Vector(nx, ny, 0) - phi1_plane = PlaneSurfaceExporter(f"{self.obj.Label}_pho1_plane)", phi1_normal, D) + phi1_plane = PlaneSurfaceExporter(f"{self.obj.Label}_pho1_plane", phi1_normal, D) self.surfaces.append(phi1_plane) region = f"-{sphere_surface.id}" @@ -3101,6 +3289,8 @@ def __init__(self, obj): baseExporter = SolidExporter.getExporter(self.obj.Base) basePlacement = baseExporter.placement() self._placement = self.obj.Placement * basePlacement + # self._placement = get_global_placement(obj) # according to GPT-5.1. Does not work! + def isBoolean(self, obj): id = obj.TypeId @@ -3160,8 +3350,6 @@ def generate_surfaces(self): In the process of scanning for booleans, the Nonbooleans are exported """ - objPlacement = self.obj.Placement - obj = self.obj boolsList = [obj] # list of booleans that are part of obj # dynamic list that is used to figure out when we've iterated over all @@ -3193,8 +3381,8 @@ def generate_surfaces(self): solidName = boolobj.Label solidExporter1 = ref1[boolobj] solidExporter2 = ref2[boolobj] - region1 = solidExporter1.get_region() - region2 = solidExporter2.get_region() + region1 = solidExporter1.get_region() # this will generate the surfaces + region2 = solidExporter2.get_region() # this will generate the surfaces self.surfaces += solidExporter1.surfaces self.surfaces += solidExporter2.surfaces @@ -3203,12 +3391,12 @@ def generate_surfaces(self): self.region = region1.union(region2) elif operation == 'subtraction': self.region = region1.cut(region2) - elif operation == 'common': + elif operation == 'intersection': self.region = region1.intersection(region2) - if objPlacement != FreeCAD.Placement(): - translation = objPlacement.Base - rotation = objPlacement.Rotation + if self._placement != FreeCAD.Placement(): + translation = self._placement.Base + rotation = self._placement.Rotation for surf in self.surfaces: surf.rotate(rotation) surf.translate(translation) @@ -3285,7 +3473,7 @@ def cone_surface(name_suffix, r1, r2, h): region_expr += f"-{outer_cone.id} " bottom_plane = PlaneSurfaceExporter(self.obj.Label+'_bot', Vector(0, 0, 1), -self.obj.z/2*mul) - top_plane = PlaneSurfaceExporter(self.obj.Label+'_bot', Vector(0, 0, 1), self.obj.z/2*mul) + top_plane = PlaneSurfaceExporter(self.obj.Label+'_top', Vector(0, 0, 1), self.obj.z/2*mul) self.surfaces += [bottom_plane, top_plane] region_expr += f"+{bottom_plane.id} -{top_plane.id}" @@ -4809,7 +4997,7 @@ def generate_surfaces(self): for point in points: if not arrayRotation.isSame(FreeCAD.Rotation(), 1e-7): point = arrayRotation*point - pos = point + arrayTranslation + pos = point + arrayTranslation + extraTranslation solidExporter = SolidExporter.getExporter(base) item_region = solidExporter.get_region() solidExporter.rotate(arrayRotation) @@ -5875,6 +6063,110 @@ def generate_surfaces(self): surf.rotate(R) surf.translate(T) +#--------------------Auto tessellation classes and routines ----------------------------------------------------------- + +class Plane: + def __init__(self, normal, D): + self.normal = normal + self.D = D + + def inside(self, point): + return self.normal.dot(point) < self.D + + def __hash__(self): + s = [float(f" {f:.4e}") for f in self.normal] + s.append(float(f" {self.D:.4e}")) + return hash(tuple(s)) + + def __eq__(self, other): + if not (type(self) is type(other)): + return False + + return hash(self) == hash(other) + + +class RegionSet: + def __init__(self, v2planes_dict, mesh): + ''' v2planes_dict is a dictionary of a mesh point index and all the Planes that pass through that point + and a Plane that contains it as value ''' + self.v2p = v2planes_dict + self.mesh = mesh + + def is_edge_inside(self, edge, plane): + ''' test if the edge (a pair of point indexes is inside this RegionSet + To be inside both points must be in the set of points AND there must be points + in the set on BOTH sides of the given plane + ''' + + mypoints = self.v2p.keys() + + count = 0 + for vid in edge: + if vid in mypoints: + count += 1 + if count >= 2: + break + + if count < 2: + return False + + nplus = 0 # number of points on the + side of the plane + nminus = 0 # number of points on the nedgative side of the plane + + eps = 1.0e-9 + for vid in mypoints: + point = self.mesh.Points[vid].Vector + dist = plane.normal.dot(point) - plane.D + if dist < -eps: + nminus += 1 + elif dist > eps: + nplus += 1 + if nplus > 0 and nminus > 0: + return True + + return False + + def split_by_plane(self, plane): + ''' split this region set into two region sets, one where all the points are on the inside of + the plane and one on the outside''' + + region1 = defaultdict(list) + region2 = defaultdict(list) + + eps = 1.0e-9 + onplane = [] # list of points on the plane (gain, there are point indexes, not points) + for vid in list(self.v2p.keys()): + vec = self.mesh.Points[vid].Vector + dist = plane.normal.dot(vec) - plane.D + if dist < -eps: + region1[vid] = self.v2p[vid][:] + elif dist > eps: + region2[vid] = self.v2p[vid][:] # otherwise, put the planes in region 2 + else: + onplane.append(vid) + + # add the splitting plane to the edge points and remove the "wrong" face from the edge vertexes + plane1 = plane # just a change of notation for clarity + plane2 = Plane(-plane.normal, -plane.D) # flip the cutting plane for region2 + for vid in onplane: + region1[vid] = self.v2p[vid][:] + region2[vid] = self.v2p[vid][:] + for pl in region1[vid][:]: # copy so we don't screw up the list while we're iterating over it + if pl.normal.dot(plane1.normal) < 0: + region1[vid].remove(pl) # remove the wrong plane + region1[vid].append(plane1) # and add the cutting plane + + for pl in region2[vid][:]: # copy so we don't screw up the list while we're iterating over it + if pl.normal.dot(plane2.normal) < 0: + region2[vid].remove(pl) # remove the wrong plane + region2[vid].append(plane2) # and add the cutting plane + + R1 = RegionSet(region1, self.mesh) + R2 = RegionSet(region2, self.mesh) + + return R1, R2 + + class ShapeMesher: deflection = 0.5 @@ -5891,6 +6183,7 @@ def __init__(self, shape): # Debug self.edge_dict = self.edge_face_dict() + self.parallel_face_dict = self.parallel_set() print(f"Num faces = {len(self.mesh.Facets)}") print(f"Num edges = {len(self.edge_dict)}") @@ -5906,6 +6199,37 @@ def _edge_dir(self, edge): v.normalize() return v + def plane(self, facet): + normal = facet.Normal + vid = facet.PointIndices[0] + p0 = self.mesh.Points[vid].Vector + D = normal.dot(p0) + return Plane(normal, D) + + def vertex_to_planes_map(self): + v2planes = {} + for fi, facet in enumerate(self.mesh.Facets): + for vid in facet.PointIndices: + plane = self.plane(facet) + v2planes[vid] = plane + return v2planes + + def parallel_set(self): + ''' return a dictionary of face as key and set of faces that are parallel with that face''' + par_face_dict = {} + for face1 in self.mesh.Facets: + s = set() + normal1 = face1.Normal + for face2 in self.mesh.Facets: + if face1.Index == face2.Index: + continue + normal2 = face2.Normal + if normal2.dot(normal1) > 0.999: + s.add(face2.Index) + par_face_dict[face1.Index] = s + + return par_face_dict + def edge_face_dict(self): d = {} for face in self.mesh.Facets: @@ -5937,8 +6261,7 @@ def my_concave_edge_test(self, edge): p2 = c2 - eps * f2.Normal if abs(f1.Normal.dot(f2.Normal)) > 0.999: - self.display_edge(edge) - return True + return False if (p2 - p1).Length > (c2 - c1).Length: self.display_edge(edge) @@ -6080,6 +6403,12 @@ def concave_edge_chain(edge, chain_set): f2 = self.edge_dict[e][1] face_set.add(f1) face_set.add(f2) + # add all edges that are parallel to the two faces to the same chain + for f1par in self.parallel_face_dict[f1]: + face_set.add(f1par) + for f2par in self.parallel_face_dict[f2]: + face_set.add(f2par) + concave_chains.append(face_set) return concave_chains @@ -6174,6 +6503,61 @@ def display_edge(self, edge): # shape.ViewObject.LineColor = (255, 255, 255) Part.show(shape, "edge") + def edge_plane(self, edge): + ''' Return a plane that passes through the edge. The plane is midway between the two + faces that border thed edge + ''' + faceIndexes = self.edge_dict[edge] + f1 = self.mesh.Facets[faceIndexes[0]] + f2 = self.mesh.Facets[faceIndexes[1]] + v_edge = self._edge_dir(edge) + n1 = f1.Normal + n2 = f2.Normal + nmean = n1 + n2 # only direction matters, so we don't need to normalize + + plane_normal = v_edge.cross(nmean) # note that the sense of the plane_normal could be eiher way + plane_normal.normalize() + p0 = self.mesh.Points[list(edge)[0]].Vector # one of the points on the edge + D = plane_normal.dot(p0) + + return Plane(plane_normal, D) + + + def split_along_concaves(self): + ''' Returns a list of RegionSets that are presumably all convex + It does this by splitting an initial RegionSet containing all the mesh points (point indexes, actually) + into two halves by a splitting plane that passes through a concaved edge. Do this until all the + concaved edges have been consumed + ''' + + concave_edges = self.get_concave_edges() + + # first form a RegionSet of all the points and planes of faces + v2p = defaultdict(list) + for fi, facet in enumerate(self.mesh.Facets): + for vid in facet.PointIndices: + plane = self.plane(facet) + v2p[vid].append(plane) + + r0 = RegionSet(v2p, self.mesh) + regions = [r0] + while len(concave_edges) > 0: + edge = concave_edges[0] + for region in regions: + plane = self.edge_plane(edge) # create a splitting plane along that edge + if region.is_edge_inside(edge, plane): # find which region edge is inside + region1, region2 = region.split_by_plane(plane) # get the two split regions + if region1 is None or region2 is None: + break # no split happened. Cutting by a degenerate plane, ie. a plane that has been used before + regions.remove(region) # remove the region that has been just split + regions.append(region1) # and replace with the two split regions + regions.append(region2) + break + concave_edges.remove(edge) # remove the splitting edge from the list of concave edges + + return regions + + def compound_to_solids(shape): """Convert a Compound (or other) to a list of Solids if possible.""" if shape.ShapeType == "Solid": @@ -6365,12 +6749,147 @@ def __init__(self, obj): # the normal is arbitrary. When we build the regions for each tetrahedron, we will # figure out the in/out of each plane + self.surfacesDict = {} # to reducs the number of duplicate planes for parallel faces + + def solidMesh(self): + import ObjectsFem + import femmesh.gmshtools as gmshtools + + def gmsh_sizes_from_view(obj, + elems_across=10, + interior_factor=3.0, + min_hmin = 10.0): + """ + Map FreeCAD ViewObject Deviation / AngularDeflection to Gmsh mesh sizes. + Returns: + h_min, h_max, min_elems_2pi + """ + v = obj.ViewObject + + shp = obj.Shape + bbox = shp.BoundBox + + L = max(bbox.XLength, bbox.YLength, bbox.ZLength) + + if L <= 0: + # degenerate + h_min = min_hmin + else: + h_min = max(L / elems_across, min_hmin) + + h_max = h_min * interior_factor + + return h_min, h_max + + doc = FreeCAD.ActiveDocument + + + + h_min, h_max = gmsh_sizes_from_view(self.obj) + # mesh = doc.addObject('Fem::FemMeshGmshFromShape', 'GmshMesh') + mesh = ObjectsFem.makeMeshGmsh(doc, f"{self.obj.Label}_temporary_mesh") + print(mesh, mesh.TypeId) + + mesh.Shape = self.obj + + mesh.CharacteristicLengthMin = h_min + mesh.CharacteristicLengthMax = h_max + + # Curvature-based refinement + mesh.MeshSizeFromCurvature = True + + # 3D mesh algorithm and optimization + mesh.Algorithm3D = 1 # Tetrahedral + mesh.OptimizeStd = True + + mesher = gmshtools.GmshTools(mesh) + mesher.create_mesh() + doc.recompute() + + # 4 loop over tetrahedra, creating planes and regions for each + + fem_mesh = mesh.FemMesh + faces_attr = fem_mesh.Faces + if isinstance(faces_attr, dict): + face_ids = list(faces_attr.keys()) + else: + face_ids = list(faces_attr) + + # So my understanding of the structure of the mesh, so far, is the following + # if E is the number of Edges, F the number of faces and V the number of Volumes + # then Edge_ids go from 1 - E + # Face ids go form E+1 - E+1+F + # and volume ids go from E+F+1+1 - E+F+1+1+V + # so mesh.getElementNoded(id) will get the edge node ids of id is in the Edge ids range + # will get the face id nodes for ids in the Face ids range + # and will get the volume id nodes for id in the Volume id range. + # For edges there will be two node ids, corresponding to the ends of the edge + # For faces, there will be three ids, corresponding to the triangle coordinates + # and for Volume ids, there will be four node ids, corresponding to the four coreners of + # of the tetrahedron + # mesh.Node[id] gives the Vector coordinates of the node, that is the point + # P.S: Face ids are those of the boundary faces only, not of every tetrahedron surface + + # get a list of planes, one for each face. At present the sense of + # the normal is arbitrary. When we build the regions for each tetrahedron, we will + # figure out the in/out of each plane + + def plane_from_face(node_ids): + face_key = frozenset(node_ids) + coords = [fem_mesh.Nodes[n] for n in node_ids] + v1 = coords[1] - coords[0] + v2 = coords[2] - coords[1] + normal = v1.cross(v2) + normal.normalize() + D = normal.dot(coords[0]) + surf = PlaneSurfaceExporter(f"{self.obj.Label}_{vol_id}_{i}", normal, D) + plane_nodes_dict[face_key] = surf # given three node ids, we identify the plane + # regardless of the order in which node ids are given + surfaces.append(surf) + + surfaces = [] + plane_nodes_dict = {} + region_expr = "" + + for vol_id in fem_mesh.Volumes: + volume_region = "" + # Get the node IDs associated with this tetrahedron + # For a 1st order mesh, this will return 4 nodes + node_ids = fem_mesh.getElementNodes(vol_id) + for i in range(4): # we should doublecheck we are using tetrahedra + opposite_corner = node_ids[i] + face_corners = node_ids[:i] + node_ids[i + 1:] # other corners + face = frozenset(face_corners) + if face not in plane_nodes_dict: # if face not already in dictionary, build it + plane_from_face(face_corners) + vector_to_face = fem_mesh.Nodes[face_corners[0]] - fem_mesh.Nodes[opposite_corner] + surface = plane_nodes_dict[face] + if surface.normal.dot(vector_to_face) > 0: # normal points to outside, we want to be in + volume_region += f"-{surface.id} " + else: + volume_region += f"+{surface.id} " + + if region_expr == "": + region_expr = f"({volume_region})" + else: + region_expr += f"|({volume_region})" + + return surfaces, Region(region_expr) + def face_to_plane(self, face): + def plane_key(): + return (round(normal.x, 7), round(normal.y, 7), round(normal.z, 7), round(D, 7)) + normal = Vector(face.Normal.x, face.Normal.y, face.Normal.z) # remember we need to convert from mm to cm point0 = Vector(face.Points[0][0], face.Points[0][1], face.Points[0][2]) D = normal.dot(point0) - planesurface = PlaneSurfaceExporter(self.name()+f"_{self.index}", normal, D) + key = plane_key() + if key in self.surfacesDict: + return None # may be a poor way of signaling to the caller that the surface has been processed + else: + planesurface = PlaneSurfaceExporter(self.name()+f"_{self.index}", normal, D) + self.surfacesDict[key] = planesurface self.index += 1 return planesurface @@ -6381,18 +6900,31 @@ def remove_redundant_planes(self, planes): set_of_planes.add(plane) return list(set_of_planes) + def check_convex_face(self, face, mesh): + normal = Vector(face.Normal.x, face.Normal.y, face.Normal.z) + point0 = Vector(face.Points[0][0], face.Points[0][1], face.Points[0][2]) + D = normal.dot(point0) + pnts = [p.Vector for p in mesh.Points] + + for p in pnts: + if p.dot(normal) > D: + return False + return True def generate_surfaces(self): - from .chatGPT_extrusionExporter import analyze_extruded_mesh import MeshPart + global obj_top_children_dict if self.obj.TypeId != 'Mesh::Feature': + shape = self.obj.Shape.copy(False) + bbox = shape.BoundBox minLength = min([bbox.XLength, bbox.YLength, bbox.ZLength]) # shape.Placement = FreeCAD.Placement() # remove object's placement + print(f"Autotessellating {self.obj.Label} which is of type {self.obj.TypeId}") viewObject = self.obj.ViewObject deflection = viewObject.Deviation # this is in percent linearDeflection = deflection/100*minLength @@ -6417,44 +6949,35 @@ def generate_surfaces(self): shape = Part.Shape() tolerance = 0.1 - shape.makeShapeFromMesh(comp.Topology, 0.1) + shape.makeShapeFromMesh(comp.Topology, tolerance) componentMesher = ShapeMesher(shape) component_region = "" - concave_face_indexes = set() - # region for concave edges - concave_chains = componentMesher.concave_face_chains() - colors = [(200, 0, 0), (0,200, 0), (0, 0, 200), (200, 0, 200), (0, 200, 200), (200, 200, 0)] - nc = len(colors) + region_sets = componentMesher.split_along_concaves() j = 0 - for chain in concave_chains: - chain_region = "" - color = colors[j] - j = (j +1) % nc - for iFace in chain: - face = componentMesher.mesh.Facets[iFace] - surf = self.face_to_plane(face) + for region in region_sets: + planes = set() + for vid in region.v2p: + vid_planes = region.v2p[vid] + for pl in vid_planes: + planes.add(pl) + region_expr = "" + k = 0 + for plane in planes: + # i = mesh component index + # j = region index in this component + # k = plane index for planes in this region + surf = PlaneSurfaceExporter(self.name()+f"_{i}_{j}_{k}", Vector(plane.normal), plane.D) + k += 1 self.surfaces.append(surf) - if chain_region == "": - chain_region += f"-{surf.id} " - else: - chain_region += f"| -{surf.id} " - concave_face_indexes.add(iFace) - componentMesher.display_face(iFace, color) - component_region += f"({chain_region}) " - - # non-concave faces - facet_indexes = [facet.Index for facet in componentMesher.mesh.Facets] - for idx in facet_indexes: - if idx in concave_face_indexes: # skip faces that have been already processed as concave faces - continue - face = componentMesher.mesh.Facets[idx] - componentMesher.display_face(idx, (150, 150, 150)) - surf = self.face_to_plane(face) - self.surfaces.append(surf) - component_region += f"-{surf.id} " - + region_expr += f"-{surf.id} " + if component_region == "": + component_region = f"({region_expr})" + else: + component_region += f" | ({region_expr})" + j += 1 + # TODO, test if the regions formaed are convex or not. self.region = self.region.union(Region(component_region)) self.position_globally() From 8e68b08f6d6a1a902818416ff89bb23a778865ce Mon Sep 17 00:00:00 2001 From: Munther Hindi Date: Fri, 30 Jan 2026 08:48:44 -0800 Subject: [PATCH 5/8] Using Cached surfaces to reduce export size --- freecad/gdml/exportOpenMC.py | 64 ++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/freecad/gdml/exportOpenMC.py b/freecad/gdml/exportOpenMC.py index c626649e..fd7269e0 100644 --- a/freecad/gdml/exportOpenMC.py +++ b/freecad/gdml/exportOpenMC.py @@ -1742,6 +1742,10 @@ def exportGDML(first, filepath, fileExt): openmc = ET.Element('openmc') openmc.append(materials) openmc.append(geometry) + + SurfaceExporter.export_surfaces() + CellExporter.export_cells() + if platform == "win32": indent(geometry) indent(materials) @@ -2150,6 +2154,8 @@ def __str__(self): class CellExporter: _ids = [] # dictionary of name vs id: __ids[name] = id _ids_dict = {} + cell_cache = [] + def __init__(self, name, material_name=None, region=None, universe=None, fill=None, rotation=None, translation=None): @@ -2173,7 +2179,24 @@ def __init__(self, name, material_name=None, region=None, universe=None, fill=No self.translation = translation self.universe = universe - def export(self): + @staticmethod + def export_cells(): + for cell in CellExporter.cell_cache: + cell.replace_surface_ids(SurfaceExporter.replacement_ids) + cell._export() + + CellExporter.cell_cache = [] # reset the cache after exporting + + def replace_surface_ids(self, replacement_dict): + ''' replace every occurrence of a surface id in our region with its replacement id''' + region = self.region + for surface_id in replacement_dict: + pattern = rf"([+-]){re.escape(str(surface_id))}\b" + replacement = rf"\g<1>{str(replacement_dict[surface_id])}" + region = re.sub(pattern, replacement, region) + self.region = region + + def _export(self): cell = ET.SubElement(geometry, 'cell') cell.attrib['name'] = self.name cell.attrib['id'] = str(self.id) @@ -2195,6 +2218,9 @@ def export(self): if self.translation is not None: cell.attrib['translation'] = f"{self.translation.x} {self.translation.y} {self.translation.z}" + def export(self): + CellExporter.cell_cache.append(self) + def quadric_coeffs(F, **kwargs): ''' Given a function F that calculates a surface F(x, y, z, **kwargs) return @@ -2316,14 +2342,16 @@ def paraboloid(x, y, z, k1, k2, center=Vector(0,0,0), rotation=FreeCAD.Rotation( class SurfaceExporter: _ids = [] # dictionary of name vs id: __ids[name] = id _ids_dict = {} + cached_surfaces = {} + replacement_ids = {} + def __init__(self, name, type, coeffs, boundary="transmission"): id = 1 while id in SurfaceExporter._ids: id += 1 SurfaceExporter._ids.append(id) SurfaceExporter._ids_dict[name] = id - print(f"Creating {name} id={id}") - +# self.id = id self.type = type if name != "": @@ -2338,12 +2366,20 @@ def __init__(self, name, type, coeffs, boundary="transmission"): def copy(self): return # should be declared abstract + @staticmethod + def export_surfaces(): + for key in SurfaceExporter.cached_surfaces: + surface = SurfaceExporter.cached_surfaces[key] + surface._export() + @staticmethod def reset_ids(): SurfaceExporter._ids = [] SurfaceExporter._ids_dict = {} + SurfaceExporter.cached_surfaces = {} + SurfaceExporter.replacement_ids = {} - def export(self): + def _export(self): surface = ET.SubElement(geometry, 'surface') surface.attrib['id'] = str(self.id) surface.attrib['name'] = str(self.name) @@ -2351,19 +2387,33 @@ def export(self): surface.attrib['coeffs'] = str(self.coeffs) surface.attrib['boundary'] = self.boundary + def export(self): + key = self.mykey() + if key not in self.cached_surfaces: + SurfaceExporter.cached_surfaces[key] = self + else: + cached_surface = SurfaceExporter.cached_surfaces[key] + SurfaceExporter.replacement_ids[self.id] = cached_surface.id + def translate(self, T): return def rotate(self, R): return + def mykey(self): + s = "" + for w in self.coeffs.strip().split(): + s += f" {float(w):.6e}" + return s + def __hash__(self): - # hash coefficients to within 1 part in 10^4. This so when we compare two + # hash coefficients to within 1 part in 10^6. This so when we compare two # surfaces we will consider them to be the same if all the coefficients agree - # to within 1 part in 10^4 + # to within 1 part in 10^6 s = [] for w in self.coeffs.strip().split(): - s.append(float(f" {float(w):.4e}")) + s.append(float(f" {float(w):.6e}")) return hash(tuple(s)) From c8109b9d50f2bd3a284699e525f49d8c9f2c030c Mon Sep 17 00:00:00 2001 From: Munther Hindi Date: Wed, 4 Feb 2026 13:42:33 -0800 Subject: [PATCH 6/8] many fixes: polar array, processArrayPart, material temperate. Code cleanup --- freecad/gdml/exportOpenMC.py | 124 ++++++++++++----------------------- 1 file changed, 42 insertions(+), 82 deletions(-) diff --git a/freecad/gdml/exportOpenMC.py b/freecad/gdml/exportOpenMC.py index fd7269e0..82eee4e5 100644 --- a/freecad/gdml/exportOpenMC.py +++ b/freecad/gdml/exportOpenMC.py @@ -39,6 +39,8 @@ import FreeCAD as App import FreeCADGui from PySide import QtGui +import copy + from FreeCAD import Vector import BOPTools.SplitAPI @@ -550,7 +552,6 @@ def quaternion2XYZ(rot): and g = atan2(yp, zp) """ v = rot * Vector(0, 0, 1) - print(v) # solution 1. if v.x > 1.0: v.x = 1.0 @@ -596,10 +597,6 @@ def exportPosition(name, xml, pos): z = pos[2] posType, posName = GDMLShared.getPositionName(name) - print(f"exportPosition: name {name} posType {posType} posName {posName}") - print(f"x {x}") - print(f"y {y}") - print(f"z {z}") if posType is None: # The part is not in the gdmlInfo spread spreadsheet if x == 0 and y == 0 and z == 0: @@ -725,10 +722,6 @@ def exportScaling(name, xml, scl): ET.SubElement(xml, "scaleref", {"ref": sclName}) -def processPlacement(name, xml, placement): - exportPosition(name, xml, placement.Base) - exportRotation(name, xml, placement.Rotation) - #-------------------------- OpenMC Materials exporting code ----------------------------------- import re @@ -859,7 +852,6 @@ def material_nuclides(mat): nuclide_list.append({'nuclide': isotope.nuclide, 'fraction': isotope.fraction, 'type': 'ao'}) return nuclide_list - print(mat) mat_obj = FreeCAD.ActiveDocument.getObject(mat) if mat_obj is None: # perhaps the minus in the name. clean the name @@ -867,7 +859,6 @@ def material_nuclides(mat): mat_obj = FreeCAD.ActiveDocument.getObject(mat) print(f"Cannot find object with name {mat}") - print(mat_obj.Label) # A Chemical formula if hasattr(mat_obj, 'formula'): @@ -896,10 +887,8 @@ def material_nuclides(mat): print(mat_obj.Group) # in a mixture the fractions are weight fractions for grp in mat_obj.Group: - print(grp.Label) mat_or_element = grp.Label[:grp.Label.find(' :')] weight_fraction = float(grp.Label[1+grp.Label.find(':'):]) - print(mat_or_element) sym = elementSymbol(mat_or_element) if sym is not None: # The mixture involves a natural element component_list = material_nuclides(mat_or_element) @@ -975,8 +964,6 @@ def processMaterials(): ]: Grp = FreeCAD.ActiveDocument.getObject(GName) if Grp is not None: - # print(Grp.TypeId+" : "+Grp.Label) - print(Grp.Label) if processGroup(Grp) is False: break @@ -1027,7 +1014,7 @@ def createMaterials(group): temperature += 273.15 elif obj.Tunit == 'Fahrenheit' or obj.Tunit == 'F': # ich!! nobody should do that temperature = 273.15 + (temperature - 32)/9*5 - item['temperature'] = str(temperature) + item.attrib['temperature'] = str(temperature) if hasattr(obj, "Dunit") or hasattr(obj, "Dvalue"): @@ -1180,22 +1167,6 @@ def getMaterial(obj): return material -""" -def printObjectInfo(xmlVol, volName, xmlParent, parentName): - print("Process Object : "+obj.Label+' Type '+obj.TypeId) - if xmlVol is not None : - xmlstr = ET.tostring(xmlVol) - else : - xmlstr = 'None' - print('Volume : '+volName+' : '+str(xmlstr)) - if xmlParent is not None : - xmlstr = ET.tostring(xmlParent) - else : - xmlstr = 'None' - print('Parent : '+str(parentName)+' : '+str(xmlstr)) -""" - - def invPlacement(placement): inv = placement.inverse() return inv @@ -1270,7 +1241,6 @@ def processArrayPart(array): while switch(arrayType): if case("ortho"): placements = arrayUtils.placementList(array) - print(f'Number of placements = {len(placements)}') for i, placement in enumerate(placements): base.Placement = placement doc.recompute() @@ -1279,8 +1249,7 @@ def processArrayPart(array): break if case("polar"): - placements = arrayUtils.placementList(array) - print(f'Number of placements = {len(placements)}') + placements = arrayUtils.placementList(array, offsetVector=base.Placement.Base) for i, placement in enumerate(placements): base.Placement = placement doc.recompute() @@ -1300,19 +1269,6 @@ def processArrayPart(array): base.Placement = saved_placement doc.recompute() -def printVolumeInfo(vol, xmlVol, xmlParent, parentName): - if xmlVol is not None: - xmlstr = ET.tostring(xmlVol) - else: - xmlstr = "None" - print(xmlstr) - GDMLShared.trace(" " + vol.Label + " - " + str(xmlstr)) - if xmlParent is not None: - xmlstr = ET.tostring(xmlParent) - else: - xmlstr = "None" - GDMLShared.trace(" Parent : " + str(parentName) + " : " + str(xmlstr)) - def createWorldVol(volName): print("Need to create Dummy Volume and World Box ") @@ -1916,8 +1872,10 @@ def __init__(self, obj): arrayOfPart = False if isArrayType(firstChild) and firstChild.Base.isDerivedFrom("App::Part"): + parent_stack.append(self.obj) processArrayPart(firstChild) arrayOfPart = True + parent_stack.pop() for child in obj_top_children_dict[obj]: if child is firstChild and arrayOfPart: @@ -2345,7 +2303,7 @@ class SurfaceExporter: cached_surfaces = {} replacement_ids = {} - def __init__(self, name, type, coeffs, boundary="transmission"): + def __init__(self, name, type, coeffs="", boundary="transmission"): id = 1 while id in SurfaceExporter._ids: id += 1 @@ -2428,11 +2386,11 @@ def __eq__(self, other): class PlaneSurfaceExporter(SurfaceExporter): def __init__(self, name:str, normal:FreeCAD.Vector, D:float): + super().__init__(name, "plane") self.normal = normal self.D = 0.1*D/self.normal.Length self.normal.normalize() self.to_coeffs() - super().__init__(name, "plane", self.coeffs) def translate(self, T): T = 0.1 * T # convert from mm to cm for openmc @@ -2454,10 +2412,10 @@ def inRegion(self, point): class SphereSurfaceExporter(SurfaceExporter): def __init__(self, name, center, radius): + super().__init__(name, "sphere") self.center = 0.1*center self.radius = 0.1*radius self.to_coeffs() - super().__init__(name, "sphere", self.coeffs) self.saved_radius = radius self.saved_center = center @@ -2480,11 +2438,11 @@ def rotate(self, R): class CylinderSurfaceExporter(SurfaceExporter): def __init__(self, name, center, axis, radius): # convert to quadric surface: + super().__init__(name, "quadric") self.center = 0.1*center self.axis = axis self.radius = 0.1*radius self.to_quadric() - super().__init__(name, "quadric", self.coeffs) self.saved_radius = radius self.saved_center = center self.saved_axis = axis @@ -2498,6 +2456,7 @@ def to_quadric(self): center=self.center, axis=self.axis) self.coeffs = f"{A} {B} {C} {D} {E} {F} {G} {H} {J} {K}" + print(self.name, self.coeffs) def translate(self, T): T = 0.1 * T # convert from mm to cm for openmc @@ -2514,12 +2473,12 @@ def rotate(self, R): class EllipticalCylinderSurfaceExporter(SurfaceExporter): def __init__(self, name, center,dx, dy): # convert to quadric surface: + super().__init__(name, "quadric") self.center = 0.1*center self.dx = 0.1*dx self.dy = 0.1*dy self.rotation = FreeCAD.Rotation() self.to_quadric() - super().__init__(name, "quadric", self.coeffs) self.saved_dx = dx self.saved_dy = dy self.saved_rotation = FreeCAD.Rotation() @@ -2551,13 +2510,13 @@ def rotate(self, R): class EllipticalConeSurfaceExporter(SurfaceExporter): def __init__(self, name, center, dx, dy, zHeight): # convert to quadric surface: + super().__init__(name, "quadric") self.center = 0.1*center self.dx = dx # this is ration, so no conversion from mm to cm self.dy = dy self.zHeight = 0.1*zHeight self.rotation = FreeCAD.Rotation() self.to_quadric() - super().__init__(name, "quadric", self.coeffs) self.saved_center = center self.saved_dx = dx self.saved_dy = dy @@ -2596,12 +2555,12 @@ def __init__(self, name, center, axis, theta): :param: theta: cone half angle, in radians ''' # convert to quadric surface: + super().__init__(name, "quadric") self.center = 0.1*center self.axis = axis self.theta = theta self.to_quadric() - super().__init__(name, "quadric", self.coeffs) self.saved_center = center self.saved_axis = axis @@ -2635,6 +2594,7 @@ def __init__(self, name, ax, by, cz, center=Vector(0, 0, 0), rotation=FreeCAD.Ro :param: theta: cone half angle, in radians ''' # convert to quadric surface: + super().__init__(name, "quadric") self.ax = 0.1*ax self.by = 0.1*by self.cz = 0.1*cz @@ -2642,7 +2602,6 @@ def __init__(self, name, ax, by, cz, center=Vector(0, 0, 0), rotation=FreeCAD.Ro self.rotation = rotation self.to_quadric() - super().__init__(name, "quadric", self.coeffs) self.saved_ax = ax self.saved_by = by self.saved_cz = cz @@ -2692,6 +2651,8 @@ def __init__(self, name, rlo, rhi, dz, center=Vector(0, 0, 0), rotation=FreeCAD. # ==> k2 = (rlo^2+rhi^2)/2 # convert to quadric surface: + super().__init__(name, "quadric") + dz *= 0.1 # convert to cm rlo *= 0.1 rhi *= 0.1 @@ -2701,7 +2662,6 @@ def __init__(self, name, rlo, rhi, dz, center=Vector(0, 0, 0), rotation=FreeCAD. self.rotation = rotation self.to_quadric() - super().__init__(name, "quadric", self.coeffs) self.saved_rlo = rlo self.saved_rhi = rhi @@ -2891,6 +2851,9 @@ def __init__(self, obj): self.region = None self.surfaces = [] + def copy(self): + return SolidExporter(self.obj) + def generate_surfaces(self): ''' build the list of surfaces. Implemented by implementors''' return @@ -2917,6 +2880,10 @@ def exported(self): def export(self): if self.region is None: self.generate_surfaces() + if hasattr(self.obj, "BoundaryType"): + boundary_type = self.obj.BoundaryType + self.set_boundary_type(boundary_type) + for surf in self.surfaces: surf.export() return @@ -3174,20 +3141,20 @@ def generate_surfaces(self): normal = Vector(0, 0, 1) D = self.obj.Height.Value - top_surface = PlaneSurfaceExporter(f"{self.obj.Label}_top)", normal, D) + top_surface = PlaneSurfaceExporter(f"{self.obj.Label}_top", normal, D) D = 0 - bottom_surface = PlaneSurfaceExporter(f"{self.obj.Label}_bot)", normal, D) + bottom_surface = PlaneSurfaceExporter(f"{self.obj.Label}_bot", normal, D) + axis = normal + center = Vector(0, 0, 0) if self.obj.InnerRadius != 0: radius = self.obj.InnerRadius.Value - axis = normal - center = Vector(0, 0, 0) - inner_surface = CylinderSurfaceExporter(f"{self.name()}_ir)", center, axis, radius) + inner_surface = CylinderSurfaceExporter(f"{self.name()}_ir", center, axis, radius) else: inner_surface = None radius = self.obj.OuterRadius.Value - outer_surface = CylinderSurfaceExporter(f"{self.name()}_ot)", center, axis, radius) + outer_surface = CylinderSurfaceExporter(f"{self.name()}_or", center, axis, radius) self.surfaces = [top_surface, bottom_surface, outer_surface] if inner_surface is not None: @@ -4904,7 +4871,6 @@ def generate_surfaces(self): self.region = Region("") - print(base.Label) appPartBase = False if hasattr(base, "TypeId") and base.TypeId == "App::Part": print( @@ -4926,7 +4892,7 @@ def generate_surfaces(self): # Since the array itself is not exported, but rather the array base is, # the base global position does not reflect the arrays global position - placements = arrayUtils.placementList(self.obj, rot=arrayRotation) + placements = arrayUtils.placementList(self.obj, offsetVector=basePos) # I'll try doing the rotations from scratch # let GlobalPlacement = G # Note: FOR AN ARRAY GLOBAL PLACEMENT APPLIES to the ARRAY, NOT BASE of the ARRAY @@ -4941,36 +4907,32 @@ def generate_surfaces(self): # We need to first unrotate and untranslate the surfaces, then translate and rotate by the array rotation and # translation as above arrayObj = self.obj - G = arrayObj.getGlobalPlacement() + G = arrayObj.getGlobalPlacement() # In top_obj_dict, arrays don't have children. P = arrayObj.Placement Pinv = P.inverse() C = G*Pinv # this is what puts the array in its position globally baseInvPlacement = base.Placement.inverse() for placement in placements: - rot = arrayRotation*placement.Rotation - local_rotated_position = arrayObj.Placement.Base + self.obj.Center + rot*(basePos - self.obj.Center) - global_rotated_position = C*local_rotated_position - - # rot = rot * baseRotation - # rot.Angle = -rot.Angle # undo angle reversal by exportRotation - + placement1 = placement*baseInvPlacement + rot = placement1.Rotation + pos = placement1.Base solidExporter = SolidExporter.getExporter(base) # get region generates a region with # global translation and global rotation # let us assume now that object rotation is 0 - # array utils give position rotated about array center then shifter by array center + # array utils give position rotated about array center then shifted by array center # - item_region = solidExporter.get_region() # also positions solidExorter object globally - # undo surfaces positioning - solidExporter.translate(baseInvPlacement.Base) - solidExporter.rotate(baseInvPlacement.Rotation) - # then apply rotated global position + item_region = solidExporter.get_region() + # the get_region will position the base globally already. Here we should only add the array + # placements solidExporter.rotate(rot) - solidExporter.translate(global_rotated_position) + solidExporter.translate(pos) self.region = self.region.union(item_region) self.surfaces += solidExporter.surfaces + # self.position_globally() # this should take care of the array's global position + class PathArrayExporter(SolidExporter): def __init__(self, obj): @@ -4981,7 +4943,6 @@ def generate_surfaces(self): self.region = Region("") base = self.obj.Base - print(base.Label) if hasattr(base, "TypeId") and base.TypeId == "App::Part": print( f"**** Arrays of {base.TypeId} ({base.Label}) currently not supported ***" @@ -5023,7 +4984,6 @@ def generate_surfaces(self): self.region = Region("") base = self.obj.Base - print(base.Label) if hasattr(base, "TypeId") and base.TypeId == "App::Part": print( f"**** Arrays of {base.TypeId} ({base.Label}) currently not supported ***" From 20495970838d988dc7c7e77e30779ddf311d7f21 Mon Sep 17 00:00:00 2001 From: Munther Hindi Date: Wed, 4 Feb 2026 13:45:48 -0800 Subject: [PATCH 7/8] Code cleanup --- freecad/gdml/exportGDML.py | 7970 ++++++++++++++++++++---------------- 1 file changed, 4444 insertions(+), 3526 deletions(-) diff --git a/freecad/gdml/exportGDML.py b/freecad/gdml/exportGDML.py index d693ab18..235853a9 100644 --- a/freecad/gdml/exportGDML.py +++ b/freecad/gdml/exportGDML.py @@ -1,4 +1,5 @@ from __future__ import annotations + # Mon Aug 26 2024 # Sat Mar 28 8:44 AM PDT 2023 # ************************************************************************** @@ -31,6 +32,7 @@ __author__ = "Keith Sloan " __url__ = ["https://github.com/KeithSloan/FreeCAD_Geant4"] +from gettext import translation from sys import breakpointhook import FreeCAD, os, Part, math @@ -38,10 +40,15 @@ import FreeCAD as App import FreeCADGui from PySide import QtGui +import copy from FreeCAD import Vector +import BOPTools.SplitAPI +from BOPTools import BOPFeatures +from collections import defaultdict import random +import re from .GDMLObjects import GDMLcommon, GDMLBox, GDMLTube # modif add @@ -50,6 +57,10 @@ import sys from pathlib import Path +from .GDMLShared import getMult +from .GmshUtils import meshObj +from .polygonsHelper import inner_outer + try: import lxml.etree as ET @@ -64,28 +75,17 @@ except ImportError: FreeCAD.Console.PrintMessage("pb xml lib not found\n") sys.exit() + # xml handling # import argparse # from xml.etree.ElementTree import XML ################################# global zOrder +global element_table # dictionary of ElementIsotopes object for an element symbol as key +global nuclide_table # dictionary of Isotope obj for nuclide name as key -from .GDMLObjects import ( - GDMLQuadrangular, - GDMLTriangular, - GDML2dVertex, - GDMLSection, - GDMLmaterial, - GDMLfraction, - GDMLcomposite, - GDMLisotope, - GDMLelement, - GDMLconstant, - GDMLvariable, - GDMLquantity, - GDMLbordersurface, -) +element_table = None from . import GDMLShared @@ -94,7 +94,8 @@ def get_active_branch_name(): gdml_dir = FreeCAD.getUserAppDataDir() + "/Mod/GDML" head_dir = Path(gdml_dir) / ".git" / "HEAD" try: - with head_dir.open("r") as f: content = f.read().splitlines() + with head_dir.open("r") as f: + content = f.read().splitlines() except: return f"Can't locate .git directory in {gdml_dir}" @@ -109,6 +110,7 @@ def get_active_branch_name(): if open.__module__ in ["__builtin__", "io"]: pythonopen = open # to distinguish python built-in open function from the one declared here + # ## modifs lambda @@ -167,7 +169,7 @@ def getVolumeName(vol) -> str: else: name = NameManager.nameUsedFor(vol) if name is None: - name = "LV_"+NameManager.getName(vol) + name = "LV_" + NameManager.getName(vol) NameManager._volumeNamesDict[vol] = name return name @@ -176,6 +178,7 @@ def getPhysvolName(vol): name = NameManager.getName(vol) return "PV_" + name + # ## end modifs lambda ################################# @@ -283,12 +286,6 @@ def place(self, volRef): self.assembly = assembly -class PhysVolPlacement: - def __init__(self, ref, placement): - self.ref = ref # name reference: a string - self.placement = placement # physvol placement - - ######################################################### # Pretty format GDML # ######################################################### @@ -317,7 +314,7 @@ def indent(elem, level=0): def cleanGDMLname(name): # Clean GDML name for Geant4 # Replace space and special characters with '_' - return name.replace('\r','').replace('(','_').replace(')','_').replace(' ','_') + return name.replace('\r', '').replace('(', '_').replace(')', '_').replace(' ', '_') def nameFromLabel(label): @@ -335,49 +332,140 @@ def initGDML(): # although wget of url works. I don't know what's going on gdml = ET.Element( "gdml", - attrib={ - location_attribute: "https://service-spi.web.cern.ch/service-spi/app/releases/GDML/schema/gdml.xsd" - }, - ) + attrib={ + location_attribute: "https://service-spi.web.cern.ch/service-spi/app/releases/GDML/schema/gdml.xsd" + }, + ) return gdml +class Isotope: + def __init__(self, nuclide, atomic_weight, fraction, element): + self.nuclide = nuclide # nuclide, example 'U235' + self.atomic_weight = atomic_weight # float atomic weight (234.99, eg) + self.fraction = fraction # atomic fraction of isotope in its element + self.element = element # the ElementIsotopes this isotope belongs to + + def weight_fraction(self): + return self.fraction * self.atomic_weight / self.element.atomic_weight + + +class ElementIsotopes: + def __init__(self, Z, symbol, element, atomic_weight, isotopes): + self.Z = Z + self.symbol = symbol + self.element = element # element name + self.atomic_weight = atomic_weight # mean atomic weight of element + self.isotopes = isotopes # list[Isotope] + + +def gen_element_table(file_name): + '''read the given file and return a dictionary of ElementIsotopes: dict[element_symbol] = ElementIsotope + ''' + + table = {} + + fd = open(file_name, 'r') + for line in fd.readlines(): + if line[0] == '#': + continue + words = line.strip().split() + Z = int(words[0]) + sym = words[1] + element = words[2].lower() + nuclide_list = words[3:] + if 3 * int(len(nuclide_list) / 3) != len(nuclide_list): + print( + f"*** Nuclide list for element {Z} {element} might be missing something a nuclide or a fraction. Please check file ***") + exit(0) + + num_words = len(nuclide_list) + tot_fraction = 0 + average_atomic_weight = 0 + isotopes_list = [] + elementIsotopes = ElementIsotopes(Z, sym, element, average_atomic_weight, isotopes_list) + for i in range(0, num_words, 3): + nuclide = nuclide_list[i] + atomic_weight = float(nuclide_list[i + 1]) + fraction = float(nuclide_list[i + 2]) + isotope = Isotope(nuclide, atomic_weight, fraction, elementIsotopes) + isotopes_list.append(isotope) + tot_fraction += fraction + average_atomic_weight += atomic_weight + + # sanity check for typos + nuclide_element = nuclide[:len(sym)] + if nuclide_element != sym: + print(f"nuclide {nuclide} does not have the same element name as its element {sym}") + return + # another check, atomic weight should be within 0.1% of mass number + mass_number = int(nuclide[len(sym):]) + if abs(mass_number - atomic_weight) / atomic_weight > 5.0e-3: + print( + f" {nuclide} atomic wieght {atomic_weight} different than mass number by more than 0.5%. Please check") + + average_atomic_weight = average_atomic_weight / len(isotopes_list) + elementIsotopes.atomic_weight = average_atomic_weight + table[sym] = elementIsotopes + # sanity check + if abs(tot_fraction - 1.0) > 2.0e-03 and tot_fraction != 0: + print(f"Warning: {element} sum of fraction is {tot_fraction} differs from 1.0 by at least 0.2%") + + return table + + +def gen_nuclide_table(): + global element_table + ''' + put all nuclides is a dictionary dict[nuclide] = Isotope (isotope class containing the nuclide) + ''' + table = {} + for elementSymbol in element_table: + elementObject = element_table[elementSymbol] + isotopes = elementObject.isotopes + for isotope in isotopes: + table[isotope.nuclide] = isotope + + return table + + ################################# # Setup GDML environment ################################# - def GDMLstructure(): # print("Setup GDML structure") ################################# # globals ################################ - global gdml, constants, variables, define, materials, solids, structure, setup + from .init_gui import joinDir + global gdml, geometry, materials global WorldVOL global defineCnt, LVcount, PVcount, POScount, ROTcount, SCLcount global centerDefined global identityDefined global identityName global gxml + global material_ids + global element_table + global nuclide_table centerDefined = False identityDefined = False identityName = 'identity' defineCnt = LVcount = PVcount = POScount = ROTcount = SCLcount = 1 + # gdml = initGDML() - gdml = initGDML() - define = ET.SubElement(gdml, "define") - materials = ET.SubElement(gdml, "materials") - solids = ET.SubElement(gdml, "solids") - solids.clear() - structure = ET.SubElement(gdml, "structure") - setup = ET.SubElement(gdml, "setup", {"name": "Default", "version": "1.0"}) - gxml = ET.Element("gxml") - _ = SurfaceManager() + materials = ET.Element('materials') + geometry = ET.Element('geometry') + material_ids = {} - return structure + if element_table is None: + element_table = gen_element_table(joinDir('Resources/Isotopic_abundances.csv')) + nuclide_table = gen_nuclide_table() + return def defineMaterials(): @@ -386,44 +474,6 @@ def defineMaterials(): global materials -def exportDefine(name, v): - global define - ET.SubElement( - define, - "position", - { - "name": name, - "unit": "mm", - "x": str(v[0]), - "y": str(v[1]), - "z": str(v[2]), - }, - ) - - -""" -def exportDefineVertex(name, v, index): - global define - ET.SubElement(define, 'position', {'name': name + str(index), - 'unit': 'mm', 'x': str(v.X), 'y': str(v.Y), 'z': str(v.Z)}) -""" - - -def exportDefineVertex(name, v, index): - global define - ET.SubElement( - define, - "position", - { - "name": name + str(index), - "unit": "mm", - "x": str(v.x), - "y": str(v.y), - "z": str(v.z), - }, - ) - - def defineWorldBox(bbox): global solids for obj in FreeCAD.ActiveDocument.Objects: @@ -505,7 +555,6 @@ def quaternion2XYZ(rot): and g = atan2(yp, zp) """ v = rot * Vector(0, 0, 1) - print(v) # solution 1. if v.x > 1.0: v.x = 1.0 @@ -541,217 +590,6 @@ def quaternion2XYZ(rot): return [math.degrees(a), math.degrees(b), math.degrees(g)] -def reportObject(obj): - - GDMLShared.trace("Report Object") - GDMLShared.trace(obj) - GDMLShared.trace("Name : " + obj.Label) - GDMLShared.trace("Type : " + obj.TypeId) - if hasattr(obj, "Placement"): - print("Placement") - print("Pos : " + str(obj.Placement.Base)) - print("axis : " + str(obj.Placement.Rotation.Axis)) - print("angle : " + str(obj.Placement.Rotation.Angle)) - - while switch(obj.TypeId): - - ########################################### - # FreeCAD GDML Parts # - ########################################### - if case("Part::FeaturePython"): - GDMLShared.trace("Part::FeaturePython") - # - # if hasattr(obj.Proxy,'Type'): - # print (obj.Proxy.Type) - # print (obj.Name) - # else : - # print("Not a GDML Feature") - - # print dir(obj) - # print dir(obj.Proxy) - # print("cylinder : Height "+str(obj.Height)+ " Radius "+str(obj.Radius)) - break - ########################################### - # FreeCAD Parts # - ########################################### - if case("Part::Sphere"): - print("Sphere Radius : " + str(obj.Radius)) - break - - if case("Part::Box"): - print( - "cube : (" - + str(obj.Length) - + "," - + str(obj.Width) - + "," - + str(obj.Height) - + ")" - ) - break - - if case("Part::Cylinder"): - print( - "cylinder : Height " - + str(obj.Height) - + " Radius " - + str(obj.Radius) - ) - break - - if case("Part::Cone"): - print( - "cone : Height " - + str(obj.Height) - + " Radius1 " - + str(obj.Radius1) - + " Radius2 " - + str(obj.Radius2) - ) - break - - if case("Part::Torus"): - print("Torus") - print(obj.Radius1) - print(obj.Radius2) - break - - if case("Part::Prism"): - print("Prism") - break - - if case("Part::RegularPolygon"): - print("RegularPolygon") - break - - if case("Part::Extrusion"): - print("Extrusion") - break - - if case("Circle"): - print("Circle") - break - - if case("Extrusion"): - print("Wire extrusion") - break - - if case("Mesh::Feature"): - print("Mesh") - # print dir(obj.Mesh) - break - - print("Other") - print(obj.TypeId) - break - - -def addPhysVol(xmlVol, volName): - GDMLShared.trace("Add PhysVol to Vol : " + volName) - # print(ET.tostring(xmlVol)) - pvol = ET.SubElement(xmlVol, "physvol", {"name": "PV_" + volName}) - ET.SubElement(pvol, "volumeref", {"ref": volName}) - return pvol - - -def getIdentifier(obj): - ''' For objects created from a gdml file we need a unique identifier to locate - the entry in the gdmlInfo sheet. - ''' - return obj.Label - - if hasattr(obj, "CopyNumber"): - append = '.' + str(obj.CopyNumber) - else: - append = '' - - print(f"{obj.Name}{append}") - if obj.TypeId == "App::Part": - name = obj.Name + append - return name - elif obj.TypeId == "App::Link": - return obj.LinkedObject.Name + append - else: - return obj.Name - - -def addPhysVolPlacement(obj, xmlVol, placement, pvName=None, refName=None) -> None: - # obj: App:Part to be placed. - # xmlVol: the xml that the str: - ''' - Unfortunately, geant produces its own internal names for assemblies. The generated name - is a very complicated thing that depends on the volume being placed and the number of times - it is being placed. As part of exporting the the document as gdml, we first build - a dictionary in buildAssemblyDict, that associates with each physvol placement of an assembly - the same name that geant generates. Here we retrieve the physvol name from the dictionary - if the dictKey is a key in the Assembly dictionary, or the name physvol name associated - with the parentObj (an App::Part) - ''' - # Obj is the source used to create candidates - print(f"getPVname {obj.Label}") - if dictKey in AssemblyDict: - print(f"returning name of {dictKey} from Assembly dictionary") - entry = AssemblyDict[dictKey] - return entry.getPVname(obj, idx) else: - print("No Parent") - - return NameManager.getPhysvolName(parentObj) - - @staticmethod - def exportSurfaceProperty(Name, Surface, ref1, ref2): - borderSurface = ET.SubElement( - structure, "bordersurface", {"name": Name, "surfaceproperty": Surface} - ) - ET.SubElement(borderSurface, "physvolref", {"ref": ref1}) - ET.SubElement(borderSurface, "physvolref", {"ref": ref2}) - - - @staticmethod - def checkFaces(pair1, pair2): - def preCheck(shape1, shape2): - # - # Precheck common faces, by checking - # if bounding boxes separation is comparable - # to sum of half-lengths - # - b1 = shape1.BoundBox - b2 = shape2.BoundBox - vcc = b2.Center - b1.Center - if ( - abs(vcc.x) > (b1.XLength + b2.XLength) * 1.01 / 2 - or abs(vcc.y) > (b1.YLength + b2.YLength) * 1.01 / 2 - or abs(vcc.z) > (b1.ZLength + b2.ZLength) * 1.01 / 2 - ): - return False - else: - return True - - tolerence = 1e-6 - obj1 = pair1[0] - matrix1 = pair1[1].Matrix - obj2 = pair2[0] - matrix2 = pair2[1].Matrix - - if hasattr(obj1, "Shape") and hasattr(obj2, "Shape"): - obj1t = obj1.Shape.transformGeometry(matrix1) - obj2t = obj2.Shape.transformGeometry(matrix2) - if not preCheck(obj1t, obj2t): - print("Fails precheck") - return False - - faces1 = obj1t.Faces - faces2 = obj2t.Faces - # faces1 = obj1.Shape.Faces - # faces2 = obj2.Shape.Faces - for f1 in faces1: - comShape = f1.common(faces2, tolerence) - if len(comShape.Faces) > 0: - print("Common") - return True - else: - print("Not common") - return False - - - @staticmethod - def processSurface(name, cnt, surface, - Obj1, obj1, idx1, dictKey1, - Obj2, obj2, idx2, dictKey2): - print(f"processSurface {name} {surface}") - print(f" {Obj1.Label} {obj1.Label} {Obj2.Label} {obj2.Label}") - ref1 = SurfaceManager.getPVname(Obj1, obj1, idx1, dictKey1) - ref2 = SurfaceManager.getPVname(Obj2, obj2, idx2, dictKey2) - SurfaceManager.exportSurfaceProperty(name + str(cnt), surface, ref1, ref2) - return cnt + 1 - - @staticmethod - def processCandidates(name, surface, check, Obj1, dict1, Obj2, dict2): - cnt = 1 - for assem1, set1 in dict1.items(): - print(f"process Candidates {assem1} {check} {len(set1)}") - for assem2, set2 in dict2.items(): - print(f"process Candidates {assem2} {check} {len(set2)}") - for idx1, items1 in enumerate(set1): - obj1 = items1[0] - for idx2, items2 in enumerate(set2): - obj2 = items2[0] - if items1 != items2: - if check: - pairStr = f"{obj1.Label} : {obj2.Label} " - if SurfaceManager.checkFaces(items1, items2): - cnt = SurfaceManager.processSurface(name, cnt, surface, - Obj1, obj1, idx1, assem1, - Obj2, obj2, idx2, assem2) - print(f"<<< Common face : {pairStr} >>>") - cnt += 1 - else: - print(f"<<< No common face : {pairStr} >>>") - else: - cnt = SurfaceManager.processSurface( - name, cnt, surface, - Obj1, obj1, idx1, assem1, - Obj2, obj2, idx2, assem2 - ) - - @staticmethod - def _getSubVols(vol, placement, volLabel): - global childObjects - - """return a flattened list of terminal solids that fall - under this vol. By flattened we mean something like: - vol - subVol1 - subVol2 - solid1 - solid2 - ... - subVol3 - solid3 - solid4 - .... - - The returned list is a list of triples: - ((solid1, placement1, subvol2.Label), (solid2, placement2, subvol2.Label), (solid3, placement3, subVol3.Label), ... - """ - print(f"getSubVols {vol.Label} {volLabel} {placement} ") - volsList = [] - print(f"_getSubVols: isContainer({vol.Label}) = {isContainer(vol)}") - if len(childObjects[vol]) == 0: - return [(vol, placement, volLabel)] - - # we assume that the user meant to select ONLY the top volume of a container as the solid with - # the optical surface property. - if isContainer(vol): - obj = childObjects[vol][0] - return [(obj, placement * obj.Placement, volLabel)] - - # vol must be an assembly, recurse - for obj in childObjects[vol]: - typeId = obj.TypeId - tObj = obj - # print(obj.Label) - if hasattr(obj, "LinkedObject"): - typeId = obj.LinkedObject.TypeId - if len(childObjects[obj]) != 0: - tObj = childObjects[obj][0] - - if typeId == "App::Part": - volsList += SurfaceManager._getSubVols(tObj, placement * obj.Placement, obj.Label) - else: - if typeId == "Part::FeaturePython": - volsList.append((obj, placement, volLabel)) - - return volsList - - @staticmethod - def getSubVols(vol, placement): - """ - given a structure of the form - vol - subVol1 - subVol2 - solid1 - solid2 - ... - subVol3 - solid3 - solid4 - .... - - return a dictionary: - {subVol2.Label: ((solid1, placement1), (solid2, placement2)), - subVol3.Label: ((solid1, placement1), (solid2, placement2))} - """ - - flattenedList = SurfaceManager._getSubVols(vol, placement, vol.Label) - solidsDict = {} - for item in flattenedList: - vol = item[0] - parentLabel = item[2] - if parentLabel in solidsDict: - solidsDict[parentLabel].append((item[0], item[1])) - else: - solidsDict[parentLabel] = [(item[0], item[1])] - - return solidsDict - - @staticmethod - def processBorderSurfaces(): - print("==============================================") - print(f"Export Border Surfaces - Assemblies {len(AssemblyDict)}") - print("==============================================") - # print(AssemblyDict) - doc = FreeCAD.ActiveDocument - - for obj in doc.Objects: - if obj.TypeId == "App::FeaturePython": - print(f"TypeId {obj.TypeId} Name {obj.Label}") - # print(dir(obj)) - # print(obj.Proxy) - if isinstance(obj.Proxy, GDMLbordersurface): - print("Border Surface") - obj1 = SurfaceManager.getPVobject(doc, obj.PV1) - candSet1 = SurfaceManager.getSubVols(obj1, obj1.Placement) - print(f"Candidates 1 : {obj1.Label} {len(candSet1)}") - printSet("Candidate1", candSet1) - obj2 = SurfaceManager.getPVobject(doc, obj.PV2) - candSet2 = SurfaceManager.getSubVols(obj2, obj2.Placement) - print(f"Candidates 2 : {obj2.Label} {len(candSet2)}") - printSet("Candidate2", candSet2) - # default for old borderSurface Objects - check = False - if hasattr(obj, "CheckCommonFaces"): - check = obj.CheckCommonFaces - SurfaceManager.processCandidates( - obj.Label, - obj.Surface, - check, - obj1, - candSet1, - obj2, - candSet2, - ) - - -def printListObj(name, listArg): - print(f"<=== Object {name} list ===>") - for obj in listArg: - print(obj.Label) - print("<===============================") - - -def printSet(name, dictArg): - print(f"<=== Object Set {name} len {len(dictArg)} ===>") - for k, v in dictArg.items(): - print(k) - for obj in v: - print(f"\t {obj[0].Label}") - print("<===============================") - + print(f" material {mat} has a component {grp.Label} that is not in Group Elements or in group Materials") - -def processSpreadsheetMatrix(sheet): - # Stupid way of finding how many rows. columns: - # increase col, row until we get an exception for that cell - # You would think the API would provide a simple function - def ncols(): - n = 0 - try: - # TODO: deal with case n > 26 - while n < 26 * 26: - sheet.get(chr(ord("A") + n) + "1") - n += 1 - except: - pass - return n - - def nrows(): - n = 0 - try: - while n < 256 * 256: - sheet.get("A" + str(n + 1)) - n += 1 - except: - pass - return n - - global define - print("add matrix to define") - - coldim = ncols() - rows = nrows() - - s = "" - for row in range(0, rows): - for col in range(0, coldim): - cell = chr(ord("A") + col) + str(row + 1) - s += str(sheet.get(cell)) + " " - - ET.SubElement( - define, - "matrix", - {"name": sheet.Label, "coldim": str(coldim), "values": s[:-1]}, - ) - - -def processOpticals(): - print("Process Opticals") - Grp = FreeCAD.ActiveDocument.getObject("Opticals") - if hasattr(Grp, "Group"): - for obj in Grp.Group: - print(f"Name : {obj.Label}") - while switch(obj.Label): - if case("Matrix"): - print("Matrix") - for m in obj.Group: - if m.TypeId == "Spreadsheet::Sheet": - processSpreadsheetMatrix(m) - else: - processMatrix(m) - break - - if case("Surfaces"): - print("Surfaces") - print(obj.Group) - for s in obj.Group: - SurfaceManager.processOpticalSurface(s) - break - - if case("SkinSurfaces"): - print("SkinSurfaces") - for s in obj.Group: - SurfaceManager.processSkinSurfaces(s) - break + return nuclide_list def processMaterials(): @@ -1439,115 +961,42 @@ def processMaterials(): global materials for GName in [ - "Define", - "Isotopes", - "Elements", + # "Define", + # "Isotopes", + # "Elements", "Materials", ]: Grp = FreeCAD.ActiveDocument.getObject(GName) if Grp is not None: - # print(Grp.TypeId+" : "+Grp.Label) - print(Grp.Label) if processGroup(Grp) is False: break -def processFractionsComposites(obj, item): - # Fractions are used in Material and Elements - if isinstance(obj.Proxy, GDMLfraction): - # print("GDML fraction :" + obj.Label) - # need to strip number making it unique - ET.SubElement( - item, - "fraction", - {"n": str(obj.n), "ref": nameFromLabel(obj.Label)}, - ) - - if isinstance(obj.Proxy, GDMLcomposite): - # print("GDML Composite") - ET.SubElement( - item, - "composite", - {"n": str(obj.n), "ref": nameFromLabel(obj.Label)}, - ) - - def createMaterials(group): global materials - for obj in group: - if obj.Label != "Geant4": - if not hasattr(obj, 'Group'): - continue - item = ET.SubElement( - materials, "material", {"name": nameFromLabel(obj.Label)} - ) - - # property must be first - for prop in obj.PropertiesList: - if obj.getGroupOfProperty(prop) == "Properties": - ET.SubElement( - item, - "property", - {"name": prop, "ref": getattr(obj, prop)}, - ) + global material_ids - if hasattr(obj, "Tunit") and hasattr(obj, "Tvalue"): - ET.SubElement( - item, - "T", - {"unit": obj.Tunit, "value": str(obj.Tvalue)}, - ) - - if hasattr(obj, "MEEunit"): - ET.SubElement( - item, - "MEE", - {"unit": obj.MEEunit, "value": str(obj.MEEvalue)}, - ) - - if hasattr(obj, "Dunit") or hasattr(obj, "Dvalue"): - # print("Dunit or DValue") - D = ET.SubElement(item, "D") - if hasattr(obj, "Dunit"): - D.set("unit", str(obj.Dunit)) - - if hasattr(obj, "Dvalue"): - D.set("value", str(obj.Dvalue)) - - # process common options material / element - processIsotope(obj, item) - for o in obj.Group: - processFractionsComposites(o, item) + density_multipliers = { + 'g/cm3': 1, + 'g/cc': 1, + 'mg/cm3': 0.001 + } + preprocessMaterialIds() + for mat in material_ids: -def postCreateGeantMaterials(): - ''' - Geant4 materials are not added or created in createMaterials. - Here we add those materials to the materials group - ''' - global materials - global usedGeant4Materials + print(f"processing material {mat}") + id = material_ids[mat] - usedElements = set() - - # collect the used elements in all the used materials - for mat in usedGeant4Materials: - obj = FreeCAD.ActiveDocument.getObjectsByLabel(mat)[0] - for grpItem in obj.Group: - words = grpItem.Label.split(':') - elemName = words[0][:-1] - usedElements.add(elemName) - # create for them - for elemName in usedElements: - obj = FreeCAD.ActiveDocument.getObject(elemName) - createElement(obj) - - for mat in usedGeant4Materials: - obj = FreeCAD.ActiveDocument.getObjectsByLabel(mat)[0] item = ET.SubElement( materials, "material", {"name": str(mat)} ) + + item.attrib['id'] = str(id) + # property must be first + # openmc does not use properties + ''' for prop in obj.PropertiesList: if obj.getGroupOfProperty(prop) == "Properties": ET.SubElement( @@ -1555,180 +1004,68 @@ def postCreateGeantMaterials(): "property", {"name": prop, "ref": getattr(obj, prop)}, ) + ''' + + # hyphens are wreaking havoc in getting properties from labels. + # so replace them with underscores + mat = mat.replace('-', '_') + obj = FreeCAD.ActiveDocument.getObject(mat) + + if hasattr(obj, "Tvalue"): + temperature = float(obj.Tvalue) + if hasattr(obj, "Tunit"): + if obj.Tunit == 'Celsius' or obj.Tunit == 'C' or obj.Tunit == 'Centigrade': + temperature += 273.15 + elif obj.Tunit == 'Fahrenheit' or obj.Tunit == 'F': # ich!! nobody should do that + temperature = 273.15 + (temperature - 32) / 9 * 5 + item.attrib['temperature'] = str(temperature) if hasattr(obj, "Dunit") or hasattr(obj, "Dvalue"): # print("Dunit or DValue") - D = ET.SubElement(item, "D") + D = ET.SubElement(item, "density") + unit_multiplier = 1 if hasattr(obj, "Dunit"): - D.set("unit", str(obj.Dunit)) - + if obj.Dunit in density_multipliers: + unit_multiplier = density_multipliers[obj.Dunit] + D.set("units", 'g/cm3') + else: + D.set("units", str(obj.Dunit)) + else: # default to g/cm3, if no Dunit is given + D.set("units", 'g/cm3') if hasattr(obj, "Dvalue"): - D.set("value", str(obj.Dvalue)) - - if hasattr(obj, "Tunit") and hasattr(obj, "Tvalue"): - ET.SubElement( - item, - "T", - {"unit": obj.Tunit, "value": str(obj.Tvalue)}, - ) + density = float(obj.Dvalue) + if obj.Dvalue <= 0: + density = 1.0e-25 + D.set("value", str(density * unit_multiplier)) - if hasattr(obj, "MEEunit"): - ET.SubElement( - item, - "MEE", - {"unit": obj.MEEunit, "value": str(obj.MEEvalue)}, - ) # process common options material / element - processIsotope(obj, item) - if len(obj.Group) > 0: - for o in obj.Group: - processFractionsComposites(o, item) + nuclides_in_material = material_nuclides(mat) + for nuclide in nuclides_in_material: + xml_item = ET.SubElement(item, 'nuclide') + nuclide_name = nuclide['nuclide'] + nuclide_fraction = nuclide['fraction'] + fraction_type = nuclide['type'] -def createElements(group): - global materials - for obj in group: - createElement(obj) + xml_item.attrib['name'] = str(nuclide_name) + xml_item.attrib[fraction_type] = str(nuclide_fraction) -def createElement(obj): +def createElement(elementLabel, item): global materials - item = ET.SubElement( - materials, "element", {"name": nameFromLabel(obj.Label)} - ) - # Common code Isotope and Elements1 - processIsotope(obj, item) - if len(obj.Group) > 0: - for o in obj.Group: - processFractionsComposites(o, item) + elementsGroup = FreeCAD.ActiveDocument.getObject('Elements') + for grp in elementsGroup.Group: + if grp.Label == elementLabel: + for nuclideGrp in grp.Group: + nuclide_name = nuclideGrp.Label[:nuclideGrp.Label.find(' :')] + fraction = nuclideGrp.n + xml_item = ET.SubElement(item, 'nuclide') + xml_item.attrib['name'] = str(nuclide_name) + xml_item.attrib['ao'] = str(fraction) -def createConstants(group): - global define - for obj in group: - if isinstance(obj.Proxy, GDMLconstant): - # print("GDML constant") - # print(dir(obj)) - - ET.SubElement( - define, "constant", {"name": obj.Label, "value": obj.value} - ) - -def createDefine(group): - global define - - from .GDMLShared import definesColumn - # should come up with a test that relies on version number of the WB to test for this - sheet = FreeCAD.ActiveDocument.getObject("defines") - if sheet is None: # Older docs not having a define spreadsheet - createConstants(group) - createVariables(group) - return - - numRows = GDMLShared.lastRow(sheet) - for row in range(1, numRows+1): - entryType = sheet.get(definesColumn['type']+str(row)) - entryName = sheet.get(definesColumn['name']+str(row)) - - if entryType == 'constant' or entryType == 'variable': - value = sheet.getContents(definesColumn['value'] + str(row)) - if len(value) > 0: - if value[0] == "=": - value = GDMLShared.SheetHandler.FC_expression_to_gdml(value[1:]) - - ET.SubElement( - define, str(entryType), {"name": str(entryName), "value": str(value)} - ) - - elif entryType == "quantity": - quantityType = sheet.get(definesColumn['quantity_type'] + str(row)) - quantityUnit = sheet.get(definesColumn['quantity_unit'] + str(row)) - quantityValue = sheet.getContents(definesColumn['quantity_value'] + str(row)) - if quantityValue[0] == '=': - quantityValue = GDMLShared.SheetHandler.FC_expression_to_gdml(quantityValue) - - ET.SubElement( - define, str(entryType), {"name": str(entryName), "type": str(quantityType), - "value": str(quantityValue),"unit": quantityUnit} - ) - - - elif entryType == "position": - attrib = {} - attrib["name"] = str(entryName) - for prop in ["x", "y", "z", "unit"]: - cell = definesColumn["pos_" + prop] + str(row) - value = sheet.getContents(cell) - if len(value) > 0: - if value[0] == "=": - value = GDMLShared.SheetHandler.FC_expression_to_gdml(value[1:]) - else: - value = sheet.get(cell) - attrib[prop] = str(value) - - ET.SubElement( define, str(entryType), attrib) - - elif entryType == "rotation": - attrib = {} - attrib["name"] = str(entryName) - for prop in ["x", "y", "z", "unit"]: - cell = definesColumn["rot_" + prop] + str(row) - value = sheet.getContents(cell) - if len(value) > 0: - if value[0] == "=": - value = GDMLShared.SheetHandler.FC_expression_to_gdml(value[1:]) - else: - value = sheet.get(cell) - attrib[prop] = str(value) - - ET.SubElement( define, str(entryType), attrib) - - -def createVariables(group): - global define - for obj in group: - if isinstance(obj.Proxy, GDMLvariable): - # print("GDML variable") - # print(dir(obj)) - - ET.SubElement( - define, "variable", {"name": obj.Label, "value": obj.value} - ) - - -def createQuantities(group): - global define - for obj in group: - if isinstance(obj.Proxy, GDMLquantity): - # print("GDML quantity") - # print(dir(obj)) - - ET.SubElement( - define, - "quantity", - { - "name": obj.Label, - "type": obj.type, - "unit": obj.unit, - "value": obj.value, - }, - ) - - -def createIsotopes(group): - global materials - for obj in group: - if isinstance(obj.Proxy, GDMLisotope): - # print("GDML isotope") - # item = ET.SubElement(materials,'isotope',{'N': str(obj.N), \ - # 'Z': str(obj.Z), \ - # 'name' : obj.Label}) - # ET.SubElement(item,'atom',{'unit': obj.unit, \ - # 'value': str(obj.value)}) - item = ET.SubElement(materials, "isotope", {"name": obj.Label}) - processIsotope(obj, item) - +# -------------------------- End process OpenMC materials ------------------------ def processGroup(obj): print("Process Group " + obj.Label) @@ -1742,22 +1079,25 @@ def processGroup(obj): while switch(obj.Label): if case("Define"): # print("Constants") - createDefine(obj.Group) + # skip defines for openMC. + # createDefine(obj.Group) break if case("Quantities"): # print("Quantities") - createQuantities(obj.Group) + # skip quantities + # createQuantities(obj.Group) break if case("Isotopes"): # print("Isotopes") - createIsotopes(obj.Group) + # skip Isotopes + # createIsotopes(obj.Group) break if case("Elements"): # print("Elements") - createElements(obj.Group) + # createElements(obj.Group) break if case("Materials"): @@ -1831,127 +1171,6 @@ def getMaterial(obj): return material -""" -def printObjectInfo(xmlVol, volName, xmlParent, parentName): - print("Process Object : "+obj.Label+' Type '+obj.TypeId) - if xmlVol is not None : - xmlstr = ET.tostring(xmlVol) - else : - xmlstr = 'None' - print('Volume : '+volName+' : '+str(xmlstr)) - if xmlParent is not None : - xmlstr = ET.tostring(xmlParent) - else : - xmlstr = 'None' - print('Parent : '+str(parentName)+' : '+str(xmlstr)) -""" - - -def exportCone(name, radius, height): - cylEl = ET.SubElement( - solids, - "cone", - { - "name": name, - "rmin1": "0", - "rmax1": str(radius), - "rmin2": "0", - "rmax2": str(radius), - "z": str(height), - "startphi": "0", - "deltaphi": "360", - "aunit": "deg", - "lunit": "mm", - }, - ) - return cylEl - - -def buildAssemblyTree(worldVol): - from .AssemblyHelper import AssemblyHelper - - global AssemblyDict - - def processContainer(vol): - objects = assemblyHeads(vol) - imprNum = 1 - for obj in objects[1:]: - print( - f" buildAssemblyTree::processContainer {obj.Label} {obj.TypeId} " - ) - processVolAssem(obj, imprNum) - - def processVolAssem(vol, imprNum): - # vol - Volume Object - # xmlParent - xml of this volume's Parent - if vol.Label[:12] != "NOT_Expanded": - if isContainer(vol): - processContainer(vol) - elif isAssembly(vol): - processAssembly(vol, imprNum) - elif vol.TypeId == "App::Link": - processLink(vol, imprNum) - else: - print( - f"{vol.Label} is neither a link, nor an assembly nor a container" - ) - - def processLink(vol, imprNum): - linkedObj = vol.getLinkedObject() - if linkedObj.Label in AssemblyDict: - entry = AssemblyDict[linkedObj.Label] - instCnt = entry.www - imprNum = entry.xxx + 1 - entry = AssemblyHelper(vol, instCnt, imprNum) - AssemblyDict[vol.Label] = entry - else: - processVolAssem(linkedObj, imprNum) - - def processAssembly(vol, imprNum): - print(f"{vol.Label} typeId= {vol.TypeId}") - if hasattr(vol, "LinkedObject"): - print(f"{vol.Label} has a LinkedObject") - linkedObj = vol.getLinkedObject() - if linkedObj.Label in AssemblyDict: - entry = AssemblyDict[linkedObj.Label] - instCnt = entry.www - imprNum = entry.xxx + 1 - else: - instCnt = AssemblyHelper.maxWww + 1 - entry = AssemblyHelper(vol, instCnt, imprNum) - AssemblyDict[vol.Label] = entry - assemObjs = assemblyHeads(vol) - imprNum += 1 - for obj in assemObjs: - print( - f" buildAssemblyTree::processAssembly {obj.Label} {obj.TypeId} " - ) - if obj.TypeId == "App::Part": - processVolAssem(obj, imprNum) - elif obj.TypeId == "App::Link": - processLink(obj, imprNum) - else: - if SolidExporter.isSolid(obj): - entry.addSolid(obj) - - processContainer(worldVol) - - for k, v in AssemblyDict.items(): - print(f"Assembly: {k} av_{v.www}_impr_{v.xxx}") - - -def createXMLvolume(name): - GDMLShared.trace("create xml volume : " + name) - elem = ET.Element("volume", {"name": name}) - return elem - - -def createXMLassembly(name): - GDMLShared.trace("create xml assembly : " + name) - elem = ET.Element("assembly", {"name": name}) - return elem - - def invPlacement(placement): inv = placement.inverse() return inv @@ -2001,364 +1220,59 @@ def isArrayOfPart(obj): else: return obj1.Base.TypeId == "App::Part" -def processArrayPart(array, xmlVol): - # vol: array object - global physVolStack + +def processArrayPart(array): + # array: array object from . import arrayUtils - # vol: Array item + # array: Array item # new approach 2024-08-07: # Create an assembly for the array. The assembly has the name of the array Part # place the repeated array elements in this new assembly and place # the array assembly in the xmlVol with the position and rotation of the array # xmlVol: xml item into which the array elements are placed/exported - arrayRef = NameManager.getName(array) - arrayXML = createXMLassembly(arrayRef) - print(f"Process Array Part {array.Label} Base {array.Base} {xmlVol}") - processVolAssem(array.Base, None, array.Base.Label) - basePhysVol = physVolStack.pop() - baseRotation = basePhysVol.placement.Rotation - baseTranslation = basePhysVol.placement.Base - - arrayPos = array.Placement.Base - arrayRot = array.Placement.Rotation - - parent = array.InList[0] - print(f"parent {parent}") + # arrayRef = NameManager.getName(array) + base = array.Base + baseName = NameManager.getName(array.Base) + # create a cell with id = universe_dict[baseName] + # baseUniverseId = universe_dict[baseName] + # basePhysVol = physVolStack.pop() + # baseRotation = basePhysVol.placement.Rotation + # baseTranslation = basePhysVol.placement.Base + arrayType = arrayUtils.typeOfArray(array) + saved_placement = FreeCAD.Placement(base.Placement.Base, base.Placement.Rotation) + doc = FreeCAD.ActiveDocument while switch(arrayType): if case("ortho"): - pos = basePhysVol.placement.Base - print(f"basePhysVol: {basePhysVol.ref} position: {arrayPos}") - placements = arrayUtils.placementList(array, offsetVector=pos, rot=baseRotation) - print(f'Number of placements = {len(placements)}') + placements = arrayUtils.placementList(array) for i, placement in enumerate(placements): - ix, iy, iz = arrayUtils.orthoIndexes(i, array) - baseName = array.Base.Label + '-' + str(ix) + '-' + str(iy) + \ - '-' + str(iz) - print(f"Base Name {baseName}") - # print(f"Add Placement to {parent.Label} volref {vol.Base.Label}") - addPhysVolPlacement(array.Base, arrayXML, - placement, pvName=str(baseName)) + base.Placement = placement + doc.recompute() + geomExporter = GeomObjExporter.getExporter(base) + geomExporter.export() break if case("polar"): - positionVector = baseRotation.inverted()*baseTranslation # + vol.Placement.Base - placements = arrayUtils.placementList(array, offsetVector=positionVector, rot=baseRotation) - print(f'Number of placements = {len(placements)}') + placements = arrayUtils.placementList(array, offsetVector=base.Placement.Base) for i, placement in enumerate(placements): - baseName = array.Base.Label + '-' + str(i) - addPhysVolPlacement(array.Base, arrayXML, - placement, pvName=str(baseName)) + base.Placement = placement + doc.recompute() + geomExporter = GeomObjExporter.getExporter(base) + geomExporter.export() break if case("PathArray") or case("PointArray"): - pos = basePhysVol.placement.Base - print(f"basePhysVol: {basePhysVol.ref} position: {arrayPos}") - placements = arrayUtils.placementList(array, offsetVector=pos, rot=baseRotation) + placements = arrayUtils.placementList(array) for i, placement in enumerate(placements): - baseName = array.Base.Label + '-' + str(i) - addPhysVolPlacement(array.Base, arrayXML, - placement, pvName=str(baseName)) + base.Placement = placement + doc.recompute() + geomExporter = GeomObjExporter.getExporter(base) + geomExporter.export() break - placement = array.Placement - # if psPlacement is not None: - # placement = invPlacement(psPlacement) * placement - addPhysVolPlacement(array, xmlVol, placement) - physVolStack.append(PhysVolPlacement(array, placement)) - - structure.append(arrayXML) - # structure.append(xmlVol) - - -def processAssembly(vol, xmlVol, xmlParent, parentName, psPlacement): - global structure - global physVolStack - - # vol - Volume Object - # xmlVol - xml of this assembly - # xmlParent - xml of this volume's Parent - # psPlacement: parent solid placement, may be None - # App::Part will have Booleans & Multifuse objects also in the list - # So for s in list is not so good - # xmlVol could be created dummy volume - - # GDMLShared.setTrace(True) - volName = NameManager.getVolumeName(vol) - # GDMLShared.trace("Process Assembly : " + volName) - # if GDMLShared.getTrace() == True : - # printVolumeInfo(vol, xmlVol, xmlParent, parentName) - assemObjs = assemblyHeads(vol) - # print(f"ProcessAssembly: vol.TypeId {vol.TypeId}") - print(f"ProcessAssembly: {vol.Name} Label {vol.Label}") - print(f"Assem Objs {assemObjs}") - # - # Note that the assembly object is under an App::Part, not - # a solid, so there is no need to adjust for a "parent solid" - # placement. - # - for obj in assemObjs: - if obj.TypeId == "App::Part": - processVolAssem(obj, xmlVol, volName, None) - elif obj.TypeId == "App::Link": - print("Process Link") - # PhysVol needs to be unique - if hasattr(obj, "LinkedObject"): - volRef = NameManager.getVolumeName(obj.LinkedObject) - elif hasattr(obj, "VolRef"): - volRef = obj.VolRef - print(f"VolRef {volRef}") - addPhysVolPlacement(obj, xmlVol, obj.Placement, refName=volRef) - physVolStack.append(PhysVolPlacement(volName, obj.Placement)) - elif isArrayType(obj): - processArrayPart(obj, xmlVol) - else: - _ = processVolume(obj, xmlVol, None) - - # the assembly could be placed in a container; adjust - # for its placement, if any, given in the argument - placement = vol.Placement - if psPlacement is not None: - placement = invPlacement(psPlacement) * placement - addPhysVolPlacement(vol, xmlParent, placement) - physVolStack.append(PhysVolPlacement(volName, placement)) - - structure.append(xmlVol) - - -def processVolume(vol, xmlParent, psPlacement): - - global structure - global physVolStack - - # vol - Volume Object - # xmlParent - xml of this volume's Parent - # App::Part will have Booleans & Multifuse objects also in the list - # So for s in list is not so good - # type 1 straight GDML type = 2 for GEMC - # xmlVol could be created dummy volume - if vol.TypeId == "App::Link": - print("Volume is Link") - placement = vol.Placement - if psPlacement is not None: - placement = invPlacement(psPlacement) * placement - - addPhysVolPlacement( - vol, - xmlParent, - placement) - return - - volName = NameManager.getVolumeName(vol) - - if vol.TypeId == "App::Part": - topObject = topObj(vol) - else: - topObject = vol - if topObject is None: - return - - if isMultiPlacement(topObject): - xmlVol, volName = processMultiPlacement(topObject, xmlParent) - partPlacement = topObject.Placement - if psPlacement is not None: - partPlacement = invPlacement(psPlacement) * partPlacement - else: - solidExporter = SolidExporter.getExporter(topObject) - if solidExporter is None: - return - solidExporter.export() - print(f"Process Volume - solids count {len(list(solids))}") - # 1- adds a # # - # This is in contract to assembly, which is exported as + # This is in contrast to assembly, which is exported as # # # .... # # Must be assembly first - global childObjects + global obj_top_children_dict if not isAssembly(obj): return False @@ -2507,24 +1483,25 @@ def isAssembly(obj): # N.B. App::Link is treated as a non-assembly, even though it might be linked # to an assembly, because all we need to place it is the volref of its link - global childObjects + global obj_top_children_dict print(f"testing isAsembly for: {obj.Label}") if obj.TypeId != "App::Part": - return False + if (obj.TypeId == "App::Link" and obj.LinkedObject.TypeId != "App::Part"): + return False - for ob in childObjects[obj]: - if ob.TypeId == "App::Part" or ob.TypeId == "App::Link": + for ob in obj_top_children_dict[obj]: + if ob.TypeId == "App::Part" or (ob.TypeId == "App::Link" and ob.LinkedObject.TypeId == "App::Part"): print(True) return True # Yes, even if ONE App::Part is under this, we treat it as an assembly - if len(childObjects[obj]) > 1: + if len(obj_top_children_dict[obj]) > 1: print("Yes, it is an Assembly") return True else: # need to check for arrays. Arrays of App::Part are treated as an assembly - if len(childObjects[obj]) == 1: - topObject = childObjects[obj][0] + if len(obj_top_children_dict[obj]) == 1: + topObject = obj_top_children_dict[obj][0] if isArrayType(topObject) and isArrayOfPart(topObject): return True else: @@ -2537,9 +1514,9 @@ def assemblyHeads(obj): # return a list of subassembly heads for this object # Subassembly heads are themselves either assemblies # or terminal objects (those that produce a 1: # More than one GDML Object need to insert Dummy return False if ( - gCount == 1 and len(obj.OutList) == 2 + gCount == 1 and len(obj.OutList) == 2 ): # Just a single GDML obj insert Dummy return False return True -def locateXMLvol(vol): - global structure - xmlVol = structure.find("volume[@name='%s']" % vol.Label) - return xmlVol - - def exportWorldVol(vol, fileExt): - global childObjects + global obj_top_children_dict global WorldVOL WorldVOL = vol.Label if fileExt != ".xml": print("Export World Process Volume : " + vol.Label) GDMLShared.trace("Export Word Process Volume" + vol.Label) - ET.SubElement(setup, "world", {"ref": vol.Label}) if checkGDMLstructure(vol) is False: GDMLShared.trace("Insert Dummy Volume") - createXMLvolume("dummy") + # createXMLvolume("dummy") xmlParent = createWorldVol(vol.Label) parentName = vol.Label - addPhysVol(xmlParent, "dummy") else: GDMLShared.trace("Valid Structure") xmlParent = None @@ -2681,328 +1650,402 @@ def exportWorldVol(vol, fileExt): # The world volume does not have a parent - buildAssemblyTree(vol) - processVolAssem(vol, xmlParent, WorldVOL) - - SurfaceManager.processSkinSurfaces() - SurfaceManager.processBorderSurfaces() - - -def exportElementAsXML(dirPath, fileName, flag, elemName, elem): - # gdml is a global - global gdml, docString, importStr - if elem is not None: - # xmlElem = ET.Element('xml') - # xmlElem.append(elem) - # indent(xmlElem) - if flag is True: - filename = fileName + "-" + elemName + ".xml" - else: - filename = elemName + ".xml" - # ET.ElementTree(xmlElem).write(os.path.join(dirPath,filename)) - ET.ElementTree(elem).write(os.path.join(dirPath, filename)) - docString += "\n' - gdml.append(ET.Entity(elemName)) - - -def exportGDMLstructure(dirPath, fileName): - global gdml, docString, importStr - print("Write GDML structure to Directory") - gdml = initGDML() - docString = "\n\n" - # print(docString) - # print(len(docString)) - # gdml = ET.fromstring(docString.encode("UTF-8")) - indent(gdml) - ET.ElementTree(gdml).write( - os.path.join(dirPath, fileName + ".gdml"), - doctype=docString.encode("UTF-8"), - ) - print("GDML file structure written") + processDocTree(1) def exportGDML(first, filepath, fileExt): from . import GDMLShared from sys import platform - from .AssemblyHelper import AssemblyHelper global zOrder - global AssemblyDict - AssemblyDict = {} - AssemblyHelper.maxWww = 0 global usedGeant4Materials usedGeant4Materials = set() - global physVolStack - physVolStack = [] + global gdml + global universe_dict + + universe_dict = {} + universe_dict['worlVOL'] = 1 # world volume = root volume = 1 + SurfaceExporter.reset_ids() # GDMLShared.setTrace(True) GDMLShared.trace("exportGDML") - print("====> Start GDML Export 2.0") + + print("====> Start OpenMC Export 0.1") branch = get_active_branch_name() print(f"branch: {branch}") print("File extension : " + fileExt) GDMLstructure() zOrder = 1 + # TODO: process and export materials processMaterials() exportWorldVol(first, fileExt) + params = FreeCAD.ParamGet( "User parameter:BaseApp/Preferences/Mod/GDML" ) exportG4Materials = params.GetBool('exportG4Materials', False) - if exportG4Materials: - postCreateGeantMaterials() - processOpticals() + # format & write GDML file # xmlstr = ET.tostring(structure) # print('Structure : '+str(xmlstr)) - if fileExt == ".gdml": + if fileExt == ".xml": # indent(gdml) - print(len(list(solids))) - print("Write to gdml file") + print(len(list(geometry))) + print("Write to xml file") # ET.ElementTree(gdml).write(filepath, 'utf-8', True) # ET.ElementTree(gdml).write(filepath, xml_declaration=True) # Problem with pretty Print on Windows ? + combined_file = filepath + geom_file_path = filepath[:-4] + '_geometry.xml' + materials_file_path = filepath[:-4] + '_materials.xml' + openmc = ET.Element('openmc') + openmc.append(materials) + openmc.append(geometry) + + SurfaceExporter.export_surfaces() + CellExporter.export_cells() + if platform == "win32": - indent(gdml) - ET.ElementTree(gdml).write(filepath, xml_declaration=True, encoding='UTF-8') - else: - ET.ElementTree(gdml).write( - filepath, pretty_print=True, xml_declaration=True, - encoding='UTF-8' - ) - print("GDML file written") - - if fileExt == ".GDML": - filePath = os.path.split(filepath) - print("Input File Path : " + filepath) - fileName = os.path.splitext(filePath[1])[0] - print("File Name : " + fileName) - dirPath = os.path.join(filePath[0], fileName) - print("Directory Path : " + dirPath) - if os.path.exists(dirPath) is False: - if os.path.isdir(dirPath) is False: - os.makedirs(dirPath) - if os.path.isdir(dirPath) is True: - exportGDMLstructure(dirPath, fileName) + indent(geometry) + indent(materials) + ET.ElementTree(materials).write(materials_file_path, xml_declaration=True, encoding='UTF-8') + ET.ElementTree(geometry).write(geom_file_path, xml_declaration=True, encoding='UTF-8') + ET.ElementTree(openmc).write(filepath, xml_declaration=True, encoding='UTF-8') else: - print("Invalid Path") - # change to Qt Warning + ET.ElementTree(materials).write(materials_file_path, pretty_print=True, xml_declaration=True, + encoding='UTF-8') + ET.ElementTree(geometry).write(geom_file_path, pretty_print=True, xml_declaration=True, encoding='UTF-8') + ET.ElementTree(openmc).write(filepath, pretty_print=True, xml_declaration=True, encoding='UTF-8') + print("OpenMC xml files written") - if fileExt == ".xml": - xmlElem = ET.Element("xml") - xmlElem.append(solids) - xmlElem.append(structure) - indent(xmlElem) - ET.ElementTree(xmlElem).write(filepath) - print("XML file written") +def preprocessMaterialIds(): + global obj_top_children_dict + global material_ids -def exportGDMLworld(first, filepath, fileExt): - global childObjects + material_ids = {} - buildDocTree() # creates global childObjects - NameManager.init() - SolidExporter.init() + for obj in obj_top_children_dict: + if len(obj_top_children_dict[obj]) == 0: + continue # this item will get exported as a child of some other item + if isContainer(obj) or isAssembly(obj): + pass - # for debugging doc tree - for obj in childObjects: - s = "" - for child in childObjects[obj]: - s += child.Label + ", " - print(f"{obj.Label} [{s}]") + elif SolidExporter.isSolid(obj): + solidExporter = SolidExporter.getExporter(obj) - if filepath.lower().endswith(".gdml"): - # GDML Export - print("GDML Export") - # if hasattr(first,'InList') : - # print(len(first.InList)) + if solidExporter is None: + pass + else: + material_name = getMaterial(obj) + if material_name not in material_ids: + id = len(material_ids) + 1 + material_ids[material_name] = id - vCount, lcount, gCount = countGDMLObj(first) - if gCount > 1: - from .GDMLQtDialogs import showInvalidWorldVol + for child in obj_top_children_dict[obj]: + if isContainer(child) or isAssembly(child): + pass + elif SolidExporter.isSolid(child): + material_name = getMaterial(child) + if material_name not in material_ids: + id = len(material_ids) + 1 + material_ids[material_name] = id - showInvalidWorldVol() - else: - exportGDML(first, filepath, fileExt) + print(f" Material ids: {material_ids}") -def hexInt(f): - return hex(int(f * 255))[2:].zfill(2) +# ------------------------------ Geometry Objects exporters ------------------------- +from abc import ABC, abstractmethod -def formatPosition(pos): - s = str(pos[0]) + "*mm " + str(pos[1]) + "*mm " + str(pos[2]) + "*mm" - print(s) - return s +class GeomObjExporter(ABC): + global obj_top_children_dict -def scanForStl(first, gxml, path, flag): - from .GDMLColourMap import lookupColour - global childObjects + def __init__(self, obj): + self.obj = obj + + @staticmethod + def getExporter(obj): + ''' + if obj.TypeId == "App::Link": + target = obj.LinkedObject + doc = FreeCAD.ActiveDocument + children = [doc.copyObject(child, True) for child in obj_top_children_dict[target]] + obj_top_children_dict[obj] = children + for child in children: + obj_top_children_dict[child] = [] + if len(children) == 1: + return SingleVolumeExporter(obj) + elif len(children) > 1: + return AssemblyExporter(obj) + else: + return None + ''' - # if flag == True ignore Parts that convert - print("scanForStl") - print(first.Name + " : " + first.Label + " : " + first.TypeId) - while switch(first.TypeId): + if isContainer(obj): + return ContainerExporter(obj) + elif isAssembly(obj): + return AssemblyExporter(obj) + elif SolidExporter.isSolid(obj): + return SingleSolidExporter(obj) + else: + childObjects = obj_top_children_dict[obj] + if len(childObjects) == 1: + return SingleVolumeExporter(obj) + else: + print(f"Can't find a GeomObjExporter for {obj.Label}") + return - if case("App::Origin"): - # print("App Origin") - return + @abstractmethod + def getRegion(self): + ''' get region surrounding this object ''' - if case("App::GeoFeature"): - # print("App GeoFeature") - return + @abstractmethod + def export(self): + ''' export all surfaces needed and a cell with a material and region for the object ''' - if case("App::Line"): - # print("App Line") - return - if case("App::Plane"): - # print("App Plane") +class ContainerExporter(GeomObjExporter): + def __init__(self, obj): + super().__init__(obj) + firstChild = obj_top_children_dict[obj][0] + + if not SolidExporter.isSolid(firstChild): + print(f" Expect a solid for first child of {obj.Label}, but {firstChild.Label} is not") return - break + self.containerSolidExporter = SolidExporter.getExporter(firstChild) + self.children_geom_exporters = [] + for child in obj_top_children_dict[obj][1:]: + geomExporter = GeomObjExporter.getExporter(child) + if geomExporter is None: + print(f" No GeomObjectExporter for {child.Label}. Skipping") + continue + self.children_geom_exporters.append(geomExporter) - if flag is True: - # - # Now deal with objects that map to GDML solids - # - while switch(first.TypeId): - if case("Part::FeaturePython"): - return + def getRegion(self): + ''' Note this region is that of the surrounding solid + If this container is contained in another container, the solid of this container should + be subtacted from it's motherregion + ''' + return self.containerSolidExporter.get_region() - if case("Part::Box"): - print(" Box") - return + def myRegion(self): + ''' + return region occupied by material in this Exporter + This is the solid region for the container minus the region occupied by all its children + ''' + solid_region = self.containerSolidExporter.get_region() + children_region = Region("") + for child_geom_exporter in self.children_geom_exporters: + child_region = child_geom_exporter.getRegion() + if child_region is not None: + children_region = children_region.union(child_region) - if case("Part::Cylinder"): - print(" Cylinder") - return + return solid_region.cut(children_region) - if case("Part::Cone"): - print(" Cone") - return + def export(self): + # Very ugly way of checking whether we are exporting world volume + worldObject = list(obj_top_children_dict.keys())[0] + if self.obj == worldObject: + self.containerSolidExporter.set_boundary_type("vacuum") - if case("Part::Sphere"): - print(" Sphere") - return + # Push this container as current parent + parent_stack.append(self.obj) + try: + # Export container's own surfaces + self.containerSolidExporter.export() - break + # Export all children under this container + for child_geom_exporter in self.children_geom_exporters: + child_geom_exporter.export() + finally: + parent_stack.pop() - # Deal with Booleans which will have Tool - if hasattr(first, "Tool"): - print(first.TypeId) - scanForStl(first.Base, gxml, path, flag) - scanForStl(first.Tool, gxml, path, flag) + region = self.myRegion() + # material is that of the containing box + mat_name = getMaterial(self.containerSolidExporter.obj) + cellExporter = CellExporter(self.obj.Label, material_name=mat_name, region=region.expr) + cellExporter.export() - for obj in childObjects[first]: - scanForStl(obj, gxml, path, flag) - if first.TypeId != "App::Part": - if hasattr(first, "Shape"): - print("Write out stl") - print( - "===> Name : " - + first.Name - + " Label : " - + first.Label - + " \ - Type :" - + first.TypeId - + " : " - + str(hasattr(first, "Shape")) - ) - newpath = os.path.join(path, first.Label + ".stl") - print("Exporting : " + newpath) - first.Shape.exportStl(newpath) - # Set Defaults - colHex = "ff0000" - mat = "G4Si" - if hasattr(first.ViewObject, "ShapeColor"): - # print(dir(first)) - col = first.ViewObject.ShapeColor - colHex = hexInt(col[0]) + hexInt(col[1]) + hexInt(col[2]) - print("===> Colour " + str(col) + " " + colHex) - mat = lookupColour(col) - print("Material : " + mat) - if hasattr(first, "Placement"): - print(first.Placement.Base) - pos = formatPosition(first.Placement.Base) - ET.SubElement( - gxml, - "volume", - { - "name": first.Label, - "color": colHex, - "material": mat, - "position": pos, - }, - ) +class AssemblyExporter(GeomObjExporter): + def __init__(self, obj): + super().__init__(obj) + self.children_geom_exporters = [] + firstChild = obj_top_children_dict[obj][0] + + arrayOfPart = False + if isArrayType(firstChild) and firstChild.Base.isDerivedFrom("App::Part"): + parent_stack.append(self.obj) + processArrayPart(firstChild) + arrayOfPart = True + parent_stack.pop() + + for child in obj_top_children_dict[obj]: + if child is firstChild and arrayOfPart: + continue + geomExporter = GeomObjExporter.getExporter(child) + if geomExporter is None: + print(f" No GeomObjectExporter for {child.Label}. Skipping") + continue + self.children_geom_exporters.append(geomExporter) -def exportGXML(first, path, flag): - print("Path : " + path) - # basename = 'target_'+os.path.basename(path) - gxml = ET.Element("gxml") - print("ScanForStl") - scanForStl(first, gxml, path, flag) - # format & write gxml file - indent(gxml) - print("Write to gxml file") - # ET.ElementTree(gxml).write(os.path.join(path,basename+'.gxml')) - ET.ElementTree(gxml).write(os.path.join(path, "target_cad.gxml")) - print("gxml file written") + def getRegion(self): + children_region = Region("") + for child_geom_exporter in self.children_geom_exporters: + children_region = children_region.union(child_geom_exporter.getRegion()) + return children_region -def exportMaterials(first, filename): - if filename.lower().endswith(".xml"): - print("Export Materials to XML file : " + filename) - xml = ET.Element("xml") - global define - define = ET.SubElement(xml, "define") - global materials - materials = ET.SubElement(xml, "materials") - processMaterials() - indent(xml) - ET.ElementTree(xml).write(filename) - else: - print("File extension must be xml") + def export(self): + # Push this assembly as current parent + parent_stack.append(self.obj) + try: + for child_geom_exporter in self.children_geom_exporters: + child_geom_exporter.export() + finally: + parent_stack.pop() -def exportOpticals(first, filename): - if filename.lower().endswith(".xml"): - print("Export Opticals to XML file : " + filename) - xml = ET.Element("xml") - global define - define = ET.SubElement(xml, "define") - global solids - solids = ET.SubElement(xml, "solids") - processOpticals() - indent(xml) - ET.ElementTree(xml).write(filename) - else: - print("File extension must be xml") +class SingleVolumeExporter(GeomObjExporter): + ''' + A Logical volume of the form: + App::Part + Solid + ''' + global material_ids + def __init__(self, obj): + super().__init__(obj) + child = obj_top_children_dict[obj][0] + self.child_solid_exporter = SolidExporter.getExporter(child) + if self.child_solid_exporter is None: + if hasattr(child, "TypeId"): + print(f"TpeId {child.TypeId} does not have a SolidExporter") + print(f"{child.Label} has no SolidExporter") + + def getRegion(self): + if self.child_solid_exporter is None: + return -def create_gcard(path, flag): - basename = os.path.basename(path) - print("Create gcard : " + basename) - print("Path : " + path) - gcard = ET.Element("gcard") - ET.SubElement(gcard, "detector", {"name": "target_cad", "factory": "CAD"}) - if flag is True: - ET.SubElement( + return self.child_solid_exporter.get_region() + + def export(self): + if self.child_solid_exporter is None: + return + + # Treat this volume (App::Part or App::Link) as the current parent + parent_stack.append(self.obj) + try: + # Inside here, the solid exporter will see self.obj as parent + self.child_solid_exporter.export() + finally: + parent_stack.pop() + + mat_name = getMaterial(self.child_solid_exporter.obj) + solidRegion = self.child_solid_exporter.get_region() + cellExporter = CellExporter(self.obj.Label, + material_name=mat_name, + region=solidRegion.expr) + cellExporter.export() + + +class SingleSolidExporter(GeomObjExporter): + def __init__(self, obj): + super().__init__(obj) + + self.solidExporter = SolidExporter.getExporter(obj) + + def getRegion(self): + return self.solidExporter.get_region() + + def export(self): + if self.solidExporter is None: + print(f"There is no exporter for object {self.obj.Label}") + return + + self.solidExporter.export() + solidRegion = self.solidExporter.get_region() + mat_name = getMaterial(self.obj) + cellExporter = CellExporter(self.obj.Label, material_name=mat_name, region=solidRegion.expr) + cellExporter.export() + + +# ------------------------------ Geometry Objects exporters ------------------------- + +def processDocTree(rootUniverseId): + global obj_top_children_dict + objects = list(obj_top_children_dict.keys()) + worldObj = objects[0] + + geomExporter = GeomObjExporter.getExporter(worldObj) + print(f"{worldObj.Label} {worldObj}") + + geomExporter.export() + + +def exportGDMLworld(first, filepath, fileExt): + global obj_top_children_dict + + buildDocTree() # creates global obj_top_children_dict + NameManager.init() + SolidExporter.init() + + # for debugging doc tree + for obj in obj_top_children_dict: + s = "" + for child in obj_top_children_dict[obj]: + s += child.Label + ", " + print(f"{obj.Label} [{s}]") + + if filepath.lower().endswith(".xml"): + # GDML Export + print("OpenMC Export") + # if hasattr(first,'InList') : + # print(len(first.InList)) + + vCount, lcount, gCount = countGDMLObj(first) + if gCount > 1: + from .GDMLQtDialogs import showInvalidWorldVol + + showInvalidWorldVol() + else: + exportGDML(first, filepath, fileExt) + + +def hexInt(f): + return hex(int(f * 255))[2:].zfill(2) + + +def exportMaterials(first, filename): + if filename.lower().endswith(".xml"): + print("Export Materials to XML file : " + filename) + xml = ET.Element("xml") + global define + define = ET.SubElement(xml, "define") + global materials + materials = ET.SubElement(xml, "materials") + processMaterials() + indent(xml) + ET.ElementTree(xml).write(filename) + else: + print("File extension must be xml") + + +def create_gcard(path, flag): + basename = os.path.basename(path) + print("Create gcard : " + basename) + print("Path : " + path) + gcard = ET.Element("gcard") + ET.SubElement(gcard, "detector", {"name": "target_cad", "factory": "CAD"}) + if flag is True: + ET.SubElement( gcard, "detector", {"name": "target_gdml", "factory": "GDML"} ) indent(gcard) @@ -3015,41 +2058,11 @@ def checkDirectory(path): print("Creating Directory : " + path) os.mkdir(path) -def exportGEMC(first, path, flag): - # flag = True GEMC - GDML - # flag = False just CAD - global gxml - - print("Export GEMC") - # basename = os.path.basename(path) - print(path) - print(flag) - checkDirectory(path) - # Create CAD directory - cadPath = os.path.join(path, "cad") - checkDirectory(cadPath) - # Create gcard - create_gcard(path, flag) - exportGXML(first, cadPath, flag) - if flag is True: - print("Create GDML directory") - gdmlPath = os.path.join(path, "gdml") - checkDirectory(gdmlPath) - # gdmlFilePath = os.path.join(gdmlPath,basename+'.gdml') - gdmlFilePath = os.path.join(gdmlPath, "target_gdml.gdml") - exportGDML(first, gdmlFilePath, "gdml") - # newpath = os.path.join(gdmlPath,basename+'.gxml') - newpath = os.path.join(gdmlPath, "target_gdml.gxml") - indent(gxml) - ET.ElementTree(gxml).write(newpath) - def export(exportList, filepath): "called when FreeCAD exports a file" - global refPlacement - refPlacement = {} # a dictionary of name as key, and placement as value - # the name could that of {str(replacement_dict[surface_id])}" + region = re.sub(pattern, replacement, region) + self.region = region + + def _export(self): + cell = ET.SubElement(geometry, 'cell') + cell.attrib['name'] = self.name + cell.attrib['id'] = str(self.id) + if self.universe is not None: + cell.attrib['universe'] = str(self.universe) + if self.material is not None: + cell.attrib['material'] = str(self.material) + + if self.region is not None: + cell.attrib['region'] = str(self.region) + + if self.fill is not None: + cell.attrib['fill'] = str(self.fill) + + if self.rotation is not None: + angles = quaternion2XYZ(self.rotation) + cell.attrib['rotation'] = f"{angles[0]} {angles[1]} {angles[2]}" + + if self.translation is not None: + cell.attrib['translation'] = f"{self.translation.x} {self.translation.y} {self.translation.z}" + + def export(self): + CellExporter.cell_cache.append(self) + + +def quadric_coeffs(F, **kwargs): + ''' Given a function F that calculates a surface F(x, y, z, **kwargs) return + the coefficients A, B, C, D, E, F, G, H, J, K of the quadric expression + A x^2 + B y^2 + C z^2 + D xy + E yz + F xz + G x + H y + J z + K + # Suppose one has a means of evaluating F(x, y, z) = A x^2 + B y^2 + C z^2 + + # D xy + E yz + F xz + + # G x + H y + J z + + # K + # Usually this is done by rotating/translating a vector (x', y', z') in which the surface has a simple + # form (say x'2 + y'2) = R^2 for a cylinder surface, to the system in which the axes are rotated/translated + # say, v = R v', where is the 4x4 transformation matrix. + # Now the numerical extraction of the coefficients A, B, C, .... + # K = F(0, 0, 0) + # A = 1/2 (F(1, 0, 0) + F(-1, 0, 0)) - K + # B = 1/2 (F(0, 1, 0) + F(0, -1, 0)) - K + # C = 1/2 (F(0, 0, 1) + F(0, 0, -1)) - K + # G = 1/2 (F(1, 0, 0) - F(-1, 0, 0)) + # H = 1/2 (F(0, 1, 0) - F(0, -1, 0)) + # J = 1/2 (F(0, 0, 1) - F(0, 0, -1)) + # For the cross terms (D, E, F) + # F(1, 1, 0) = A + B + D + G + H + K ==> D = F(1, 1, 0) -A -B -G -H -K + # F(0, 1, 1) = B + C + E + H + J + K ==> E = F(0, 1, 1) -B -C -H -J -K + # F(1, 0, 1) = A + C + F + G + J + K ==> F = F(1, 0, 1) -A -C -G -J -K + ''' + + K = F(0, 0, 0, **kwargs) + A = 1 / 2 * (F(1, 0, 0, **kwargs) + F(-1, 0, 0, **kwargs)) - K + B = 1 / 2 * (F(0, 1, 0, **kwargs) + F(0, -1, 0, **kwargs)) - K + C = 1 / 2 * (F(0, 0, 1, **kwargs) + F(0, 0, -1, **kwargs)) - K + G = 1 / 2 * (F(1, 0, 0, **kwargs) - F(-1, 0, 0, **kwargs)) + H = 1 / 2 * (F(0, 1, 0, **kwargs) - F(0, -1, 0, **kwargs)) + J = 1 / 2 * (F(0, 0, 1, **kwargs) - F(0, 0, -1, **kwargs)) + # For the cross terms (D, E, F) + D = F(1, 1, 0, **kwargs) - A - B - G - H - K + E = F(0, 1, 1, **kwargs) - B - C - H - J - K + F_coeff = F(1, 0, 1, **kwargs) - A - C - G - J - K + + print(A, B, C, D, E, F_coeff, G, H, J, K) + return A, B, C, D, E, F_coeff, G, H, J, K + + +def ellipsoid(x, y, z, ax=1.0, by=1.0, cz=1.0, center=Vector(0, 0, 0), rotation=FreeCAD.Rotation()): + ''' for an ellipsoid center at the origin and axes coinciding with the x, y, and z axes + The equation is x^2/ax^2 + y^2/by^2 + z^2/cz^2 - 1 = 0; + If the origin is translated to x0, y0, z0 the equation becomes + (x-x0)^2 / ax^2 + (y-y0)^2 / by^2 + (z-z0)^2 / cz^2 - 1 = 0; + If the axes are subjected to a rotation R, then the equation becomes + (x'-x0)^2 / ax^2 + (y'-y0)^2 / by^2 + (z'-z0)^2 / cz^2 - 1 = 0; + where (x', y', z') = R^-1 (x, y, z) + ''' + + vprime = rotation.inverted() * (Vector(x, y, z) - center) + + x = vprime.x + y = vprime.y + z = vprime.z + + return (x / ax) ** 2 + (y / by) ** 2 + (z / cz) ** 2 - 1 + + +def cylinder(x, y, z, radius=1, center=Vector(0, 0, 0), axis=Vector(0, 0, 1)): + rotation = FreeCAD.Rotation(Vector(0, 0, 1), axis) + vprime = rotation.inverted() * (Vector(x, y, z) - center) + + x = vprime.x + y = vprime.y + z = vprime.z + + return x * x + y * y - radius * radius + + +def elliptical_tube(x, y, z, dx=1, dy=1, center=Vector(0, 0, 0), rotation=FreeCAD.Rotation()): + R = rotation.inverted() + vprime = R * (Vector(x, y, z) - center) + + x = vprime.x + y = vprime.y + z = vprime.z + + return (x / dx) ** 2 + (y / dy) ** 2 - 1.0 + + +def elliptical_cone(x, y, z, dx=1, dy=1, zHeight=1, center=Vector(0, 0, 0), rotation=FreeCAD.Rotation()): + R = rotation.inverted() + vprime = R * (Vector(x, y, z) - center) + + x = vprime.x + y = vprime.y + z = vprime.z + + return (x / dx) ** 2 + (y / dy) ** 2 - (zHeight - z) ** 2 + + +def cone(x, y, z, center=Vector(0, 0, 0), axis=Vector(0, 0, 1), theta=1.0): + rotation = FreeCAD.Rotation(Vector(0, 0, 1), axis) + vprime = rotation.inverted() * (Vector(x, y, z) - center) + + x = vprime.x + y = vprime.y + z = vprime.z + t = math.tan(theta) + + return x ** 2 + y ** 2 - (z * t) ** 2 + + +def paraboloid(x, y, z, k1, k2, center=Vector(0, 0, 0), rotation=FreeCAD.Rotation()): + vprime = rotation.inverted() * (Vector(x, y, z) - center) + + x = vprime.x + y = vprime.y + z = vprime.z + + return x ** 2 + y ** 2 - (k1 * z + k2) + + +class SurfaceExporter: + _ids = [] # dictionary of name vs id: __ids[name] = id + _ids_dict = {} + cached_surfaces = {} + replacement_ids = {} + + def __init__(self, name, type, coeffs="", boundary="transmission"): + id = 1 + while id in SurfaceExporter._ids: + id += 1 + SurfaceExporter._ids.append(id) + SurfaceExporter._ids_dict[name] = id + # + self.id = id + self.type = type + if name != "": + self.name = name + else: + self.name = f"{type}_{id}" + self.boundary = boundary + self.coeffs = coeffs + + self.saved_state = vars(self).copy() + + def copy(self): + return # should be declared abstract + + @staticmethod + def export_surfaces(): + for key in SurfaceExporter.cached_surfaces: + surface = SurfaceExporter.cached_surfaces[key] + surface._export() + + @staticmethod + def reset_ids(): + SurfaceExporter._ids = [] + SurfaceExporter._ids_dict = {} + SurfaceExporter.cached_surfaces = {} + SurfaceExporter.replacement_ids = {} + + def _export(self): + surface = ET.SubElement(geometry, 'surface') + surface.attrib['id'] = str(self.id) + surface.attrib['name'] = str(self.name) + surface.attrib['type'] = str(self.type) + surface.attrib['coeffs'] = str(self.coeffs) + surface.attrib['boundary'] = self.boundary + + def export(self): + key = self.mykey() + if key not in self.cached_surfaces: + SurfaceExporter.cached_surfaces[key] = self + else: + cached_surface = SurfaceExporter.cached_surfaces[key] + SurfaceExporter.replacement_ids[self.id] = cached_surface.id + + def translate(self, T): + return + + def rotate(self, R): + return + + def mykey(self): + s = "" + for w in self.coeffs.strip().split(): + s += f" {float(w):.6e}" + return s + + def __hash__(self): + # hash coefficients to within 1 part in 10^6. This so when we compare two + # surfaces we will consider them to be the same if all the coefficients agree + # to within 1 part in 10^6 + s = [] + for w in self.coeffs.strip().split(): + s.append(float(f" {float(w):.6e}")) + + return hash(tuple(s)) + + def __eq__(self, other): + + # note that if the coefficients match, then we return ==, eventhough the ids might be different + if not (type(self) is type(other)): + return False + + return hash(self) == hash(other) + + +class PlaneSurfaceExporter(SurfaceExporter): + def __init__(self, name: str, normal: FreeCAD.Vector, D: float): + super().__init__(name, "plane") + self.normal = normal + self.D = 0.1 * D / self.normal.Length + self.normal.normalize() + self.to_coeffs() + + def translate(self, T): + T = 0.1 * T # convert from mm to cm for openmc + self.D += self.normal.dot(T) + self.to_coeffs() + + def rotate(self, R): + self.normal = R * self.normal + self.to_coeffs() + + def to_coeffs(self): + self.coeffs = f"{self.normal.x} {self.normal.y} {self.normal.z} {self.D}" + + def inRegion(self, point): + pcm = 0.1 * point + + return self.normal.dot(pcm) - self.D < 0 + + +class SphereSurfaceExporter(SurfaceExporter): + def __init__(self, name, center, radius): + super().__init__(name, "sphere") + self.center = 0.1 * center + self.radius = 0.1 * radius + self.to_coeffs() + self.saved_radius = radius + self.saved_center = center + + def copy(self): + return SphereSurfaceExporter(self.name, self.saved_center, self.saved_radius) + + def to_coeffs(self): + self.coeffs = f"{self.center.x} {self.center.y} {self.center.z} {self.radius}" + + def translate(self, T): + T = 0.1 * T # convert from mm to cm for openmc + self.center += T + self.to_coeffs() + + def rotate(self, R): + self.center = R * self.center + self.to_coeffs() + + +class CylinderSurfaceExporter(SurfaceExporter): + def __init__(self, name, center, axis, radius): + # convert to quadric surface: + super().__init__(name, "quadric") + self.center = 0.1 * center + self.axis = axis + self.radius = 0.1 * radius + self.to_quadric() + self.saved_radius = radius + self.saved_center = center + self.saved_axis = axis + + def copy(self): + return CylinderSurfaceExporter(self.name, self.saved_center, self.saved_axis, self.saved_radius) + + def to_quadric(self): + A, B, C, D, E, F, G, H, J, K = quadric_coeffs(cylinder, radius=self.radius, + center=self.center, axis=self.axis) + + self.coeffs = f"{A} {B} {C} {D} {E} {F} {G} {H} {J} {K}" + + def translate(self, T): + T = 0.1 * T # convert from mm to cm for openmc + self.center += T + self.to_quadric() + + def rotate(self, R): + self.axis = R * self.axis + self.center = R * self.center + self.to_quadric() + + +class EllipticalCylinderSurfaceExporter(SurfaceExporter): + def __init__(self, name, center, dx, dy): + # convert to quadric surface: + super().__init__(name, "quadric") + self.center = 0.1 * center + self.dx = 0.1 * dx + self.dy = 0.1 * dy + self.rotation = FreeCAD.Rotation() + self.to_quadric() + self.saved_dx = dx + self.saved_dy = dy + self.saved_rotation = FreeCAD.Rotation() + self.saved_center = center + + def copy(self): + return EllipticalCylinderSurfaceExporter(self.name, self.saved_center, self.saved_dx, self.saved_dy) + + def to_quadric(self): + A, B, C, D, E, F, G, H, J, K = quadric_coeffs(elliptical_tube, dx=self.dx, dy=self.dy, + center=self.center, rotation=self.rotation) + + self.coeffs = f"{A} {B} {C} {D} {E} {F} {G} {H} {J} {K}" + + def translate(self, T): + T = 0.1 * T # convert from mm to cm for openmc + self.center += T + self.to_quadric() + + def rotate(self, R): + self.rotation = R * self.rotation + self.center = R * self.center + self.to_quadric() + + +class EllipticalConeSurfaceExporter(SurfaceExporter): + def __init__(self, name, center, dx, dy, zHeight): + # convert to quadric surface: + super().__init__(name, "quadric") + self.center = 0.1 * center + self.dx = dx # this is ration, so no conversion from mm to cm + self.dy = dy + self.zHeight = 0.1 * zHeight + self.rotation = FreeCAD.Rotation() + self.to_quadric() + self.saved_center = center + self.saved_dx = dx + self.saved_dy = dy + self.saved_zHeight = zHeight + self.saved_rotation = FreeCAD.Rotation() + + def copy(self): + return EllipticalConeSurfaceExporter(self.name, self.saved_center, self.saved_dx, self.saved_dy, + self.saved_zHeight) + + def to_quadric(self): + A, B, C, D, E, F, G, H, J, K = quadric_coeffs(elliptical_cone, dx=self.dx, dy=self.dy, + zHeight=self.zHeight, + center=self.center, rotation=self.rotation) + + self.coeffs = f"{A} {B} {C} {D} {E} {F} {G} {H} {J} {K}" + + def translate(self, T): + T = 0.1 * T # convert from mm to cm for openmc + self.center += T + self.to_quadric() + + def rotate(self, R): + self.rotation = R * self.rotation + self.center = R * self.center + self.to_quadric() + + +class ConeSurfaceExporter(SurfaceExporter): + def __init__(self, name, center, axis, theta): + ''' + :param: center: cone vertex posidion + :param: axis: unit vector directiom of cone axis + :param: theta: cone half angle, in radians + ''' + # convert to quadric surface: + super().__init__(name, "quadric") + self.center = 0.1 * center + self.axis = axis + self.theta = theta + + self.to_quadric() + + self.saved_center = center + self.saved_axis = axis + self.saved_theta = theta + + def copy(self): + return ConeSurfaceExporter(self.name, self.saved_center, self.saved_axis, self.saved_theta) + + def to_quadric(self): + A, B, C, D, E, F, G, H, J, K = quadric_coeffs(cone, center=self.center, axis=self.axis, theta=self.theta) + self.coeffs = f"{A} {B} {C} {D} {E} {F} {G} {H} {J} {K}" + + def translate(self, T): + T = 0.1 * T # convert from mm to cm for openmc + self.center += T + self.to_quadric() + + def rotate(self, R): + self.axis = R * self.axis + self.center = R * self.center + self.to_quadric() + + +class EllipsoidSurfaceExporter(SurfaceExporter): + def __init__(self, name, ax, by, cz, center=Vector(0, 0, 0), rotation=FreeCAD.Rotation()): + ''' + :param: center: cone vertex posidion + :param: axis: unit vector directiom of cone axis + :param: theta: cone half angle, in radians + ''' + # convert to quadric surface: + super().__init__(name, "quadric") + self.ax = 0.1 * ax + self.by = 0.1 * by + self.cz = 0.1 * cz + self.center = 0.1 * center + self.rotation = rotation + + self.to_quadric() + self.saved_ax = ax + self.saved_by = by + self.saved_cz = cz + self.saved_center = center + self.saved_rotation = rotation + + def copy(self): + return EllipsoidSurfaceExporter(self.name, self.saved_ax, self.saved_by, self.saved_cz, self.saved_center, + self.saved_rotation) + + def to_quadric(self): + A, B, C, D, E, F, G, H, J, K = quadric_coeffs(ellipsoid, ax=self.ax, by=self.by, cz=self.cz, + center=self.center, rotation=self.rotation) + self.coeffs = f"{A} {B} {C} {D} {E} {F} {G} {H} {J} {K}" + + def translate(self, T): + T = 0.1 * T # convert from mm to cm for openmc + self.center += T + self.to_quadric() + + def rotate(self, R): + self.center = R * self.center # is this needed + self.rotation = R * self.rotation + self.to_quadric() + + +class ParaboloidSurfaceExporter(SurfaceExporter): + def __init__(self, name, rlo, rhi, dz, center=Vector(0, 0, 0), rotation=FreeCAD.Rotation()): + ''' + + Equation of paraboloid is given by + (x^2+y^2) - rho^2 = k1 z + k2 + :param: rlo: k1 * (-dz) + k2; value of rho at z=-dz + :param: rhi: k1 * (+dz) + k2 value of rho at z=+dz + :param: dx: half height of parbolid + :param: center: center of paraboloid + :param: rotation: rotaion of axis from z-axis + ''' + # Equation of Paraboloid in geant4 is: + # (x^2+y^2) = rho^2 - k1 z + k2 + # k1 and k2 determined from + # rlo^2 = k1 *(-dz) + k2 + # rhi^2 = k1 *(+dz) + k2 + # ==> k1 = (rhi^2 - rlo^2)/2 dz + # ==> k2 = (rlo^2+rhi^2)/2 + + # convert to quadric surface: + super().__init__(name, "quadric") + + dz *= 0.1 # convert to cm + rlo *= 0.1 + rhi *= 0.1 + self.k1 = (rhi * rhi - rlo * rlo) / (2 * dz) + self.k2 = (rhi * rhi + rlo * rlo) / 2 + self.center = 0.1 * center + self.rotation = rotation + + self.to_quadric() + + self.saved_rlo = rlo + self.saved_rhi = rhi + self.saved_dz = dz + self.saved_center = center + self.saved_rotation = rotation + + def copy(self): + return ParaboloidSurfaceExporter(self.name, self.saved_rlo, self.saved_rhi, self.saved_dz, self.saved_center, + self.saved_rotation) + + def to_quadric(self): + A, B, C, D, E, F, G, H, J, K = quadric_coeffs(paraboloid, k1=self.k1, k2=self.k2, + center=self.center, rotation=self.rotation) + self.coeffs = f"{A} {B} {C} {D} {E} {F} {G} {H} {J} {K}" + + def translate(self, T): + T = 0.1 * T # convert from mm to cm for openmc + self.center += T + self.to_quadric() + + def rotate(self, R): + self.center = R * self.center # is this needed + self.rotation = R * self.rotation + self.to_quadric() class SolidExporter: # Abstract class to export object as gdml _exported = [] # a list of already exported objects solidExporters = { - "GDMLArb8": "GDMLArb8Exporter", + "GDMLArb8": "AutoTessellateExporter", "GDMLBox": "GDMLBoxExporter", "GDMLCone": "GDMLConeExporter", "GDMLcutTube": "GDMLcutTubeExporter", @@ -3117,34 +2703,35 @@ class SolidExporter: "GDMLElTube": "GDMLElTubeExporter", "GDMLHype": "GDMLHypeExporter", "GDMLOrb": "GDMLOrbExporter", - "GDMLPara": "GDMLParaExporter", + "GDMLPara": "AutoTessellateExporter", "GDMLParaboloid": "GDMLParaboloidExporter", "GDMLPolycone": "GDMLPolyconeExporter", "GDMLGenericPolycone": "GDMLGenericPolyconeExporter", - "GDMLPolyhedra": "GDMLPolyhedraExporter", - "GDMLGenericPolyhedra": "GDMLGenericPolyhedraExporter", + "GDMLPolyhedra": "AutoTessellateExporter", + "GDMLGenericPolyhedra": "AutoTessellateExporter", "GDMLSphere": "GDMLSphereExporter", - "GDMLTessellated": "GDMLTessellatedExporter", - "GDMLSampledTessellated": "GDMLSampledTessellatedExporter", - # Use the GDMLTessellated exporter", - "GDMLGmshTessellated": "GDMLTessellatedExporter", - "GDMLTetra": "GDMLTetraExporter", - "GDMLTetrahedron": "GDMLTetrahedronExporter", + "GDMLTessellated": "AutoTessellateExporter", + "GDMLSampledTessellated": "AutoTessellateExporter", + "GDMLGmshTessellated": "AutoTessellateExporter", + "GDMLTetra": "AutoTessellateExporter", + "GDMLTetrahedron": "AutoTessellateExporter", "GDMLTorus": "GDMLTorusExporter", - "GDMLTrap": "GDMLTrapExporter", - "GDMLTrd": "GDMLTrdExporter", + "GDMLTrap": "AutoTessellateExporter", + "GDMLTrd": "AutoTessellateExporter", "GDMLTube": "GDMLTubeExporter", "GDMLTwistedbox": "GDMLTwistedboxExporter", "GDMLTwistedtrap": "GDMLTwistedtrapExporter", "GDMLTwistedtrd": "GDMLTwistedtrdExporter", "GDMLTwistedtubs": "GDMLTwistedtubsExporter", - "GDMLXtru": "GDMLXtruExporter", - "Mesh::Feature": "GDMLMeshExporter", + "GDMLXtru": "AutoTessellateExporter", + "Mesh::Feature": "AutoTessellateExporter", "Part::MultiFuse": "MultiFuseExporter", + "Part::MultiCommon": "MultiCommonExporter", "Part::Extrusion": "ExtrusionExporter", "Part::Revolution": "RevolutionExporter", "Part::Box": "BoxExporter", "Part::Cylinder": "CylinderExporter", + "Part::Torus": "TorusExporter", "Tube": "TubeExporter", "Part::Cone": "ConeExporter", "Part::Sphere": "SphereExporter", @@ -3163,39 +2750,52 @@ def init(): @staticmethod def isSolid(obj): + """Return True for objects whose primary identity is their own Shape.""" print(f"isSolid {obj.Label}") # return hasattr(obj, 'Shape') # does not work. App::Parts have Shape, but they are not solids! obj1 = obj if obj.TypeId == "App::Link": obj1 = obj.LinkedObject - if obj1.TypeId == "Part::FeaturePython": - return True # All Part::FeturePython have a 'Shape', and a Shape can be tessellated - ''' - typeId = obj1.Proxy.Type - if typeId == "Array": - if obj1.ArrayType == "ortho": - return True - elif obj1.ArrayType == "polar": - return True - elif typeId == "PathArray": - return True - elif typeId == "PointArray": - return True - elif typeId == "Clone": - clonedObj = obj1.Objects[0] - return SolidExporter.isSolid(clonedObj) - else: - return obj1.Proxy.Type in SolidExporter.solidExporters - ''' + if not hasattr(obj, "Shape") or obj.Shape.isNull(): + return False - else: - return obj1.TypeId in SolidExporter.solidExporters + # Exclude known containers / special cases + if obj.isDerivedFrom("App::Part"): + return False # container, aggregate shape + + if obj.isDerivedFrom("PartDesign::Body"): + # optional: treat Body as container rather than primitive + return False + + # we tread arrays of parts as containers, not as solids + if isArrayType(obj1) and obj1.Base.isDerivedFrom("App::Part"): + return False + + # Part::Compound is a grey zone: + # treat it as "real shape" or as aggregate depending on your needs + # Example: treat as aggregate and skip here: + if obj.TypeId in ("Part::Compound", "Part::CompoundPython"): + return False + + # Everything else derived from Part::Feature is fair game + if obj.isDerivedFrom("Part::Feature"): + return True + + if obj1.TypeId == "Part::FeaturePython": + if hasattr(obj, 'Shape') and not obj.Shape.isNull(): + return True + return False @staticmethod def getExporter(obj): + if hasattr(obj, 'LinkedObject'): + solidExporter = SolidExporter.getExporter(obj.LinkedObject) + if solidExporter is not None: + return type(solidExporter)(obj) + if obj.TypeId == "Part::FeaturePython": if hasattr(obj.Proxy, 'Type'): typeId = obj.Proxy.Type @@ -3233,25 +2833,41 @@ def getExporter(obj): klass = globals()[classname] return klass(obj) elif obj.TypeId == "Part::FeaturePython": # This may appear to be duplication of above, but - # we need to pass through all the specialized exporters - # before we fall back to tessellation - return AutoTessellateExporter(obj) + # we need to pass through all the specialized exporters + # before we fall back to tessellation + if hasattr(obj, 'Shape') and not obj.Shape.isNull(): + print(f"{obj.Label} does not have a Native Solid Exporter, Using AutoTessellator") + return AutoTessellateExporter(obj) else: - print(f"{obj.Label} does not have a Solid Exporter") - return None + if hasattr(obj, 'Shape') and not obj.Shape.isNull(): + print(f"{obj.Label} does not have a native Solid Exporter, Using AutoTessellator") + return AutoTessellateExporter(obj) + + return None def __init__(self, obj): self.obj = obj self._name = NameManager.getName(obj) + self.region = None + self.surfaces = [] + + def copy(self): + return SolidExporter(self.obj) + + def generate_surfaces(self): + ''' build the list of surfaces. Implemented by implementors''' + return def name(self): return self._name def position(self): - return self.obj.Placement.Base + # return self.obj.Placement.Base we ar expect each solid to translate/rotate its surfaces + return FreeCAD.Vector() def rotation(self): - return self.obj.Placement.Rotation + return FreeCAD.Rotation() + # return self.obj.Placement.Rotation. we expect objects to rotate their surfaces def placement(self): return FreeCAD.Placement(self.position(), self.rotation()) @@ -3260,43 +2876,78 @@ def exported(self): return self.obj in SolidExporter._exported def export(self): - if not self.exported(): - SolidExporter._exported.append(self.obj) + if self.region is None: + self.generate_surfaces() + if hasattr(self.obj, "BoundaryType"): + boundary_type = self.obj.BoundaryType + self.set_boundary_type(boundary_type) + + for surf in self.surfaces: + surf.export() return - def hasScale(self): - return hasattr(self.obj, "scale") or hasattr(self.obj, "Scale") + def rotate(self, R): + for surf in self.surfaces: + surf.rotate(R) - def getScale(self): - if hasattr(self.obj, "ScaleVector"): - return self.obj.ScaleVector - elif hasattr(self.obj, "scale"): - return self.obj.scale - elif hasattr(self.obj, "Scale"): - return self.obj.Scale + def translate(self, T): + for surf in self.surfaces: + surf.translate(T) + + def get_region(self): + if self.region is None: + self.generate_surfaces() + return self.region + + def set_boundary_type(self, boundary_type): + if self.region is None: + self.generate_surfaces() + for surf in self.surfaces: + surf.boundary = boundary_type + + def position_globally(self): + identity = FreeCAD.Placement() + + placement = get_global_placement(self.obj) + + if placement != identity: + rot = placement.Rotation + trans = placement.Base + for surf in self.surfaces: + surf.rotate(rot) + surf.translate(trans) + + def getMult(self): + ''' return multiplier for length units of self.obj''' + unit = "mm" # set default + # Watch for unit and lunit + # print('getMult : '+str(fp)) + if hasattr(self.obj, "lunit"): + unit = self.obj.lunit + elif hasattr(self.obj, "unit"): + unit = self.obj.unit + elif hasattr(self.obj, "attrib"): + if "unit" in self.obj.attrib: + unit = self.obj.attrib["unit"] + elif "lunit" in self.obj.attrib: + unit = self.obj.attrib["lunit"] else: - return FreeCAD.vector(1, 1, 1) + return 1 + + # The exporters will convert to cm. Here we convert to mm + unitsDict = { + "mm": 1, + "cm": 10.0, + "m": 1000., + "um": 0.001, + "nm": 1.0e-6, + # "dm": 100, decimeter not recognized by geant + "km": 1000000.0, + } + if unit in unitsDict: + return unitsDict[unit] - def _exportScaled(self): - if self.hasScale(): - scale = self.getScale() - if scale.x == 1.0 and scale.y == 1.0 and scale.z == 1.0: - return - xml = ET.SubElement( - solids, "scaledSolid", {"name": self._name + "_scaled"} - ) - ET.SubElement(xml, "solidref", {"ref": self.name()}) - ET.SubElement( - xml, - "scale", - { - "name": self.name() + "_scale", - "x": str(scale.x), - "y": str(scale.y), - "z": str(scale.z), - }, - ) - self._name += "_scaled" + print("unit not handled : " + unit) class CloneExporter(SolidExporter): @@ -3423,174 +3074,229 @@ def _export1(self, clonedObj): self._position = placement.Base self._rotation = placement.Rotation self._name = exporter.name() - self._exportScaled() class BoxExporter(SolidExporter): def __init__(self, obj): super().__init__(obj) - def export(self): - if self.exported(): - return - super().export() + def generate_surfaces(self): + self.surfaces = [] + placement = self.obj.Placement - ET.SubElement( - solids, - "box", - { - "name": self.name(), - "x": str(self.obj.Length.Value), - "y": str(self.obj.Width.Value), - "z": str(self.obj.Height.Value), - "lunit": "mm", - }, - ) - self._exportScaled() + normal = Vector(1, 0, 0) + D = 0 + x1surface = PlaneSurfaceExporter(f"{self.obj.Label}_x1_plane", normal, D) + D = self.obj.Length.Value + x2surface = PlaneSurfaceExporter(f"{self.obj.Label}_x2_plane", normal, D) - def position(self): - delta = FreeCAD.Vector( - self.obj.Length.Value / 2, - self.obj.Width.Value / 2, - self.obj.Height.Value / 2, - ) - # Part::Box has its origin at the corner - # gdml box has its origin at the center - # In FC, rotations are about corner. In GDML about - # center. The following gives correct position of center - # of exported cube - pos = self.obj.Placement.Base + self.obj.Placement.Rotation * delta - return pos + normal = Vector(0, 1, 0) + D = 0 + y1surface = PlaneSurfaceExporter(f"{self.obj.Label}_y1_plane", normal, D) + D = self.obj.Width.Value + y2surface = PlaneSurfaceExporter(f"{self.obj.Label}_y2_plane", normal, D) + + normal = Vector(0, 0, 1) + D = 0 + z1surface = PlaneSurfaceExporter(f"{self.obj.Label}_z1_plane", normal, D) + D = self.obj.Height.Value + z2surface = PlaneSurfaceExporter(f"{self.obj.Label}_z2_plane", normal, D) + + self.surfaces = [x1surface, x2surface, y1surface, y2surface, z1surface, z2surface] + self.region = Region( + f"+{x1surface.id} -{x2surface.id} +{y1surface.id} -{y2surface.id} +{z1surface.id} -{z2surface.id}") + + self.position_globally() class CylinderExporter(SolidExporter): def __init__(self, obj): super().__init__(obj) - def export(self): - if self.exported(): - return - super().export() + def generate_surfaces(self): + normal = Vector(0, 0, 1) + D = self.obj.Height.Value + top_surface = PlaneSurfaceExporter(f"{self.obj.Label}_top", normal, D) + D = 0 + bottom_surface = PlaneSurfaceExporter(f"{self.obj.Label}_bot", normal, D) - # Needs unique Name - # This is for non GDML cylinder/tube - ET.SubElement( - solids, - "tube", - { - "name": self.name(), - "rmax": str(self.obj.Radius.Value), - "deltaphi": str(float(self.obj.Angle.Value)), - "aunit": "deg", - "z": str(self.obj.Height.Value), - "lunit": "mm", - }, - ) - self._exportScaled() + axis = Vector(0, 0, 1) + center = Vector(0, 0, 0) + radius = self.obj.Radius.Value + cylinder_surface = CylinderSurfaceExporter(f"{self.name()}_cylinder", center, axis, radius) - def position(self): - delta = FreeCAD.Vector(0, 0, self.obj.Height.Value / 2) - # see comments in BoxExporter - pos = self.obj.Placement.Base + self.obj.Placement.Rotation * delta - return pos + self.surfaces = [top_surface, bottom_surface, cylinder_surface] + self.region = Region(f"-{cylinder_surface.id} +{bottom_surface.id} -{top_surface.id}") + + self.position_globally() class TubeExporter(SolidExporter): def __init__(self, obj): super().__init__(obj) - def export(self): - if self.exported(): - return - super().export() + def generate_surfaces(self): + normal = Vector(0, 0, 1) - # Needs unique Name - # This is for non GDML cylinder/tube - ET.SubElement( - solids, - "tube", - { - "name": self.name(), - "rmin": str(self.obj.InnerRadius.Value), - "rmax": str(self.obj.OuterRadius.Value), - "deltaphi": str(360), - "aunit": "deg", - "z": str(self.obj.Height.Value), - "lunit": "mm", - }, - ) - self._exportScaled() + D = self.obj.Height.Value + top_surface = PlaneSurfaceExporter(f"{self.obj.Label}_top", normal, D) + D = 0 + bottom_surface = PlaneSurfaceExporter(f"{self.obj.Label}_bot", normal, D) - def position(self): - delta = FreeCAD.Vector(0, 0, self.obj.Height.Value / 2) - # see comments in BoxExporter - pos = self.obj.Placement.Base + self.obj.Placement.Rotation * delta - return pos + axis = normal + center = Vector(0, 0, 0) + if self.obj.InnerRadius != 0: + radius = self.obj.InnerRadius.Value + inner_surface = CylinderSurfaceExporter(f"{self.name()}_ir", center, axis, radius) + else: + inner_surface = None + radius = self.obj.OuterRadius.Value + outer_surface = CylinderSurfaceExporter(f"{self.name()}_or", center, axis, radius) -class ConeExporter(SolidExporter): + self.surfaces = [top_surface, bottom_surface, outer_surface] + if inner_surface is not None: + self.surfaces.append(inner_surface) + + region_expr = f"-{top_surface.id} +{bottom_surface.id} -{outer_surface.id}" + if inner_surface is not None: + region_expr += f" +{inner_surface.id}" + + self.region = Region(region_expr) + + self.position_globally() + + +class GDMLElTubeExporter(SolidExporter): def __init__(self, obj): super().__init__(obj) - def export(self): - if self.exported(): - return - super().export() + def generate_surfaces(self): + axis = Vector(0, 0, 1) + mul = self.getMult(self.obj) - ET.SubElement( - solids, - "cone", - { - "name": self.name(), - "rmax1": str(self.obj.Radius1.Value), - "rmax2": str(self.obj.Radius2.Value), - "deltaphi": str(float(self.obj.Angle.Value)), - "aunit": "deg", - "z": str(self.obj.Height.Value), - "lunit": "mm", - }, - ) - self._exportScaled() + D = mul * self.obj.dz + center = Vector(0, 0, 0) - def position(self): - # Adjustment for position in GDML - delta = FreeCAD.Vector(0, 0, self.obj.Height.Value / 2) - # see comments in BoxExporter - pos = self.obj.Placement.Base + self.obj.Placement.Rotation * delta - return pos + name = NameManager.getName(self.obj) + top_surface = PlaneSurfaceExporter(f"{name}_top", axis, D) + bottom_surface = PlaneSurfaceExporter(f"{name}_bot", axis, -D) + + dx = mul * self.obj.dx + dy = mul * self.obj.dy + surf = EllipticalCylinderSurfaceExporter(f"{name}", center, dx, dy) + + self.surfaces = [top_surface, bottom_surface, surf] + self.region = Region(f"-{surf.id} +{bottom_surface.id} -{top_surface.id}") + + self.position_globally() + + +class ConeExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + + def generate_surfaces(self): + + # Cone half angle + r1 = self.obj.Radius1.Value + r2 = self.obj.Radius2.Value + h = self.obj.Height.Value + theta = math.atan(abs(r2 - r1) / h) + if r1 > r2: + axis = Vector(0, 0, 1) + center = Vector(0, 0, r1 / math.tan(theta)) + surface = ConeSurfaceExporter(self.name(), center, axis, theta) + bottom_plane = PlaneSurfaceExporter(self.name() + '_bot', axis, 0) + top_plane = PlaneSurfaceExporter(self.name() + '_top', axis, h) + elif r2 > r1: + axis = Vector(0, 0, -1) + center = Vector(0, 0, -r1 / math.tan(theta)) + surface = ConeSurfaceExporter(self.name(), center, axis, theta) + bottom_plane = PlaneSurfaceExporter(self.name() + '_bot', axis, 0) + top_plane = PlaneSurfaceExporter(self.name() + '_top', axis, h) + else: + center = Vector(0, 0, 0) + axis = Vector(0, 0, 1) + surface = ConeSurfaceExporter(self.name(), center, axis, r1) + bottom_plane = PlaneSurfaceExporter(self.name() + '_bot', axis, 0) + top_plane = PlaneSurfaceExporter(self.name() + '_top', axis, h) + + self.surfaces = [surface, bottom_plane, top_plane] + self.region = Region(f'-{surface.id} +{bottom_plane.id} -{top_plane.id}') + + angle = self.obj.Angle.Value + if angle != 360: + phi0_normal = Vector(0, 1, 0) + phi0_plane = PlaneSurfaceExporter(self.name() + '_phi0', phi0_normal, 0) + + phi1_normal = Vector(math.cos(math.radians(angle + 90)), math.sin(math.radians(angle + 90)), 0) + phi1_plane = PlaneSurfaceExporter(self.name() + '_phi0', phi1_normal, 0) + self.surfaces += [phi0_plane, phi1_plane] + + if phi0_normal.cross(phi1_normal).z > 0: + self.region = Region( + f'-{surface.id} +{bottom_plane.id} -{top_plane.id} +{phi0_plane.id} -{phi1_plane.id}') + else: + self.region = Region( + f'-{surface.id} +{bottom_plane.id} -{top_plane.id} (+{phi0_plane.id} | -{phi1_plane.id})') + + self.position_globally() class SphereExporter(SolidExporter): def __init__(self, obj): super().__init__(obj) - def export(self): - if self.exported(): - return - super().export() + def generate_surfaces(self): + self.surfaces = [] + center = Vector(0, 0, 0) # Note off -center spheres are handled by their position placement + radius = self.obj.Radius.Value + sphere_surface = SphereSurfaceExporter(f"{self.name()}_surf", center, radius) + self.surfaces.append(sphere_surface) + + if self.obj.Angle1 != 0: + z1 = self.obj.Radius * math.sin(math.radians(self.obj.Angle1)) + normal = Vector(0, 0, 1) + D = z1 + z1surface = PlaneSurfaceExporter(f"{self.obj.Label}_z1_plane", normal, D) + self.surfaces.append(z1surface) + + if self.obj.Angle2 != 0: + z2 = self.obj.Radius * math.sin(math.radians(self.obj.Angle2)) + normal = Vector(0, 0, 1) + D = z2 + z2surface = PlaneSurfaceExporter(f"{self.obj.Label}_z2_plane", normal, D) + self.surfaces.append(z2surface) + + if self.obj.Angle3 != 360: + phi0_normal = Vector(0, 1, 0) + D = 0 + phi0_plane = PlaneSurfaceExporter(f"{self.obj.Label}_pho0_plane", phi0_normal, D) + self.surfaces.append(phi0_plane) + + nx = -math.sin(math.radians(self.obj.Angle3)) + ny = math.cos(math.radians(self.obj.Angle3)) + phi1_normal = Vector(nx, ny, 0) + + phi1_plane = PlaneSurfaceExporter(f"{self.obj.Label}_pho1_plane", phi1_normal, D) + self.surfaces.append(phi1_plane) + + region = f"-{sphere_surface.id}" + if self.obj.Angle1 != 0: + region += f" +{z1surface.id}" + if self.obj.Angle2 != 0: + region += f" -{z2surface.id}" + + if self.obj.Angle3 != 360: + if phi0_normal.cross(phi1_normal).z > 0: + region += f" +{phi0_plane.id} -{phi1_plane.id}" + else: + region += f"(+{phi0_plane.id} | -{phi1_plane.id})" - ET.SubElement( - solids, - "sphere", - { - "name": self.name(), - "rmax": str(self.obj.Radius.Value), - "starttheta": str(90.0 - float(self.obj.Angle2.Value)), - "deltatheta": str( - float(self.obj.Angle2.Value - self.obj.Angle1.Value) - ), - "deltaphi": str(float(self.obj.Angle3.Value)), - "aunit": "deg", - "lunit": "mm", - }, - ) - self._exportScaled() + self.region = Region(region) - def position(self): - # see comments in processBoxObject - unrotatedpos = self.obj.Placement.Base - pos = self.obj.Placement.Rotation * unrotatedpos - return pos + self.position_globally() class BooleanExporter(SolidExporter): @@ -3599,6 +3305,7 @@ def __init__(self, obj): baseExporter = SolidExporter.getExporter(self.obj.Base) basePlacement = baseExporter.placement() self._placement = self.obj.Placement * basePlacement + # self._placement = get_global_placement(obj) # according to GPT-5.1. Does not work! def isBoolean(self, obj): id = obj.TypeId @@ -3625,7 +3332,8 @@ def rotation(self): def placement(self): return self._placement - def export(self): + def generate_surfaces(self): + self.surfaces = [] """ In FreeCAD doc booleans that are themselves composed of other booleans are listed in sequence, eg: @@ -3656,11 +3364,6 @@ def export(self): Then outputs them to gdml in reverse order. In the process of scanning for booleans, the Nonbooleans are exported """ - GDMLShared.trace("Process Boolean Object") - if self.exported(): - return - super().export() - obj = self.obj boolsList = [obj] # list of booleans that are part of obj @@ -3676,679 +3379,1367 @@ def export(self): if self.isBoolean(boolobj.Base): tmpList.append(boolobj.Base) boolsList.append(boolobj.Base) - else: - solidExporter.export() solidExporter = SolidExporter.getExporter(boolobj.Tool) ref2[boolobj] = solidExporter if self.isBoolean(boolobj.Tool): tmpList.append(boolobj.Tool) boolsList.append(boolobj.Tool) - else: - solidExporter.export() # Now tmpList is empty and boolsList has list of all booleans + self.region = Region("") + self.surfaces = [] for boolobj in reversed(boolsList): operation = self.boolOperation(boolobj) if operation is None: continue solidName = boolobj.Label - boolXML = ET.SubElement( - solids, str(operation), {"name": solidName} - ) - ET.SubElement(boolXML, "first", {"ref": ref1[boolobj].name()}) - ET.SubElement(boolXML, "second", {"ref": ref2[boolobj].name()}) - # process position & rotation - # Note that only the second item in the boolean (the Tool in FC parlance) - # gets a position and a rotation. But these are relative to the - # first. So convolve placement of second with inverse placement of first - placementFirst = ref1[boolobj].placement() - placementSecond = invPlacement(placementFirst) * ref2[boolobj].placement() - rot = placementSecond.Rotation - pos = placementSecond.Base # must also rotate position - toolObj = ref2[boolobj].obj # the tool object of the boolean - if placementFirst == FreeCAD.Placement(): # we give up on expressions, unless 1st object (Base( has no placement - xexpr = GDMLShared.getPropertyExpression(toolObj, '.Placement.Base.x') - yexpr = GDMLShared.getPropertyExpression(toolObj, '.Placement.Base.y') - zexpr = GDMLShared.getPropertyExpression(toolObj, '.Placement.Base.z') - pos = (xexpr, yexpr, zexpr) - - exportPosition(toolObj.Name, boolXML, pos) - # For booleans, gdml want actual rotation, not reverse - # processRotation export negative of rotation angle(s) - exportRotation(toolObj.Name, boolXML, rot, invertRotation=False) - self._exportScaled() - - -class GDMLSolidExporter(SolidExporter): - def __init__(self, obj, tag, propertyList=None): + solidExporter1 = ref1[boolobj] + solidExporter2 = ref2[boolobj] + region1 = solidExporter1.get_region() # this will generate the surfaces + region2 = solidExporter2.get_region() # this will generate the surfaces + + self.surfaces += solidExporter1.surfaces + self.surfaces += solidExporter2.surfaces + + if operation == 'union': + self.region = region1.union(region2) + elif operation == 'subtraction': + self.region = region1.cut(region2) + elif operation == 'intersection': + self.region = region1.intersection(region2) + + if self._placement != FreeCAD.Placement(): + translation = self._placement.Base + rotation = self._placement.Rotation + for surf in self.surfaces: + surf.rotate(rotation) + surf.translate(translation) + + +class GDMLBoxExporter(SolidExporter): + def __init__(self, obj): super().__init__(obj) - self._name = NameManager.getName(obj) - self.propertyList = propertyList - self.tag = tag - def name(self): - return self._name + def generate_surfaces(self): + mul = self.getMult() + normal = Vector(1, 0, 0) + D = -self.obj.x / 2 * mul + x1surface = PlaneSurfaceExporter(f"{self.obj.Label}_x1_plane", normal, D) + D = self.obj.x / 2 * mul + x2surface = PlaneSurfaceExporter(f"{self.obj.Label}_x2_plane", normal, D) - def export(self): - if self.exported(): - return - super().export() + normal = Vector(0, 1, 0) + D = -self.obj.y / 2 * mul + y1surface = PlaneSurfaceExporter(f"{self.obj.Label}_y1_plane", normal, D) + D = self.obj.y / 2 * mul + y2surface = PlaneSurfaceExporter(f"{self.obj.Label}_y2_plane", normal, D) - if self.propertyList is None: - return # presumably the child will do its own export in that case + normal = Vector(0, 0, 1) + D = -self.obj.z / 2 * mul + z1surface = PlaneSurfaceExporter(f"{self.obj.Label}_z1_plane", normal, D) + D = self.obj.z / 2 * mul + z2surface = PlaneSurfaceExporter(f"{self.obj.Label}_z2_plane", normal, D) - attrib = {} - attrib["name"] = self.name() - for prop in self.propertyList: - attrib[prop] = str(GDMLShared.getPropertyExpression(self.obj, prop)) # get expression or value + self.surfaces = [x1surface, x2surface, y1surface, y2surface, z1surface, z2surface] + self.region = Region( + f"+{x1surface.id} -{x2surface.id} +{y1surface.id} -{y2surface.id} +{z1surface.id} -{z2surface.id}") - ET.SubElement(solids, str(self.tag), attrib) - self._exportScaled() + self.position_globally() -class GDMLArb8Exporter(GDMLSolidExporter): +class GDMLConeExporter(SolidExporter): def __init__(self, obj): - super().__init__(obj, 'arb8', ['v1x', 'v1y', 'v2x', 'v2y', 'v3x', 'v3y', 'v4x', 'v4y', - 'v5x', 'v5y', 'v6x', 'v6y', 'v7x', 'v7y', 'v8x', 'v8y', 'dz', 'lunit']) + super().__init__(obj) - def export(self): - if self.exported(): - return - super().export() + def generate_surfaces(self): + from .GDMLObjects import checkFullCircle, getAngleRad + def cone_surface(name_suffix, r1, r2, h): + theta = math.atan(abs(r1 - r2) / h) + if abs(r1 - r2) < 1.0e-9: # a cylinder, not a cone + return CylinderSurfaceExporter(self.name, Vector(0, 0, 0), Vector(0, 0, 1), r1) + + if r1 > r2: # cone base at bottom, cone vertex at top + H = r1 / math.tan(theta) # Height of cone, from base to vertex + axis = Vector(0, 0, 1) # has to be z-axis for rotations to work correcly + center = (H - h / 2) * Vector(0, 0, 1) # center is location of vertex + return ConeSurfaceExporter(name + name_suffix, center, axis, theta) + else: # r1 0: + region_expr += f"+{phi0_plane.id} -{phi1_plane.id}" + else: + region_expr += f"(+{phi0_plane.id} | -{phi1_plane.id})" - ET.SubElement( - solids, - "arb8", - { - "name": self.name(), - "v1x": str(self.obj.v1x), - "v1y": str(self.obj.v1y), - "v2x": str(self.obj.v2x), - "v2y": str(self.obj.v2y), - "v3x": str(self.obj.v3x), - "v3y": str(self.obj.v3y), - "v4x": str(self.obj.v4x), - "v4y": str(self.obj.v4y), - "v5x": str(self.obj.v5x), - "v5y": str(self.obj.v5y), - "v6x": str(self.obj.v6x), - "v6y": str(self.obj.v6y), - "v7x": str(self.obj.v7x), - "v7y": str(self.obj.v7y), - "v8x": str(self.obj.v8x), - "v8y": str(self.obj.v8y), - "dz": str(self.obj.dz), - "lunit": self.obj.lunit, - }, - ) - self._exportScaled() + self.region = Region(region_expr) + self.position_globally() -class GDMLBoxExporter(GDMLSolidExporter): +class GDMLcutTubeExporter(SolidExporter): def __init__(self, obj): - super().__init__(obj, "box", ['x', 'y', 'z', 'lunit']) + super().__init__(obj) + def generate_surfaces(self): + from .GDMLObjects import checkFullCircle, getAngleRad + + region_expr = "" + axis = Vector(0, 0, 1) + mul = self.getMult() + center = Vector(0, 0, 0) + radius = self.obj.rmin * mul + if radius != 0: + surf = CylinderSurfaceExporter(f"{self.name()}_ir", center, axis, radius) + self.surfaces.append(surf) + region_expr += f"+{surf.id} " + + radius = self.obj.rmax * mul + surf = CylinderSurfaceExporter(f"{self.name()}_or", center, axis, radius) + self.surfaces.append(surf) + region_expr += f"-{surf.id} " + + normal_top = Vector(self.obj.highX, self.obj.highY, self.obj.highZ) + normal_top.normalize() + D_top = Vector(0, 0, mul * self.obj.z / 2).dot(normal_top) + surf = PlaneSurfaceExporter(f"{self.name()}_top", normal_top, D_top) + self.surfaces.append(surf) + region_expr += f"-{surf.id} " + + normal_bot = Vector(self.obj.lowX, self.obj.lowY, self.obj.lowZ) + normal_bot.normalize() + # D_bot = mul*self.obj.z/2 # not minus because the normal at the bottom points away from the bottom + # this is better calculated as Vector(0, 0, -z/2).dot(nromal) + D_bot = Vector(0, 0, -mul * self.obj.z / 2).dot(normal_bot) + surf = PlaneSurfaceExporter(f"{self.name()}_bot", normal_bot, D_bot) + self.surfaces.append(surf) + region_expr += f"-{surf.id} " + + if not checkFullCircle(self.obj.aunit, self.obj.deltaphi): + startPhi = getAngleRad(self.obj.aunit, self.obj.startphi) + nx = -math.sin(startPhi) + ny = math.cos(startPhi) + phi0_normal = Vector(nx, ny, 0) + D = 0 + phi0_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi0_plane", phi0_normal, D) + self.surfaces.append(phi0_plane) + + deltaphi = getAngleRad(self.obj.aunit, self.obj.deltaphi) + endphi = startPhi + deltaphi + nx = -math.sin(endphi) + ny = math.cos(endphi) + phi1_normal = Vector(nx, ny, 0) + + phi1_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi1_plane", phi1_normal, D) + self.surfaces.append(phi1_plane) + + if phi0_normal.cross(phi1_normal).z > 0: + region_expr += f"+{phi0_plane.id} -{phi1_plane.id}" + else: + region_expr += f"(+{phi0_plane.id} | -{phi1_plane.id})" -class GDMLConeExporter(GDMLSolidExporter): - def __init__(self, obj): - super().__init__(obj, "cone", ['rmin1', 'rmin2', 'rmax1', 'rmax2', 'startphi', 'deltaphi', 'aunit', 'z', 'lunit']) + self.region = Region(region_expr) + self.position_globally() -class GDMLcutTubeExporter(GDMLSolidExporter): +class GDMLElConeExporter(SolidExporter): def __init__(self, obj): - super().__init__(obj, "cutTube", ['rmin', 'rmax', 'startphi', 'deltaphi', 'aunit', 'z', - 'highX', 'highY', 'highZ', 'lowX', 'lowY', 'lowZ', 'lunit'] ) + super().__init__(obj) + def generate_surfaces(self): + mul = self.getMult() + dx = self.obj.dx + dy = self.obj.dy + zcut = mul * self.obj.zcut + zmax = mul * self.obj.zmax + name = NameManager.getName(self.obj) + center = Vector(0, 0, 0) + surf = EllipticalConeSurfaceExporter(name + '_cone', center, dx, dy, zmax) + self.surfaces.append(surf) -class GDMLElConeExporter(GDMLSolidExporter): - def __init__(self, obj): - super().__init__(obj, 'elcone', ['dx', 'dy', 'zcut', 'zmax', 'lunit']) + normal = Vector(0, 0, 1) + bot_plane = PlaneSurfaceExporter(name + '_bot', normal, -zcut) + top_plane = PlaneSurfaceExporter(name + '_top', normal, zcut) + self.surfaces += [bot_plane, top_plane] + self.region = Region(f"-{surf.id} +{bot_plane.id} -{top_plane.id}") -class GDMLEllipsoidExporter(GDMLSolidExporter): - def __init__(self, obj): - super().__init__(obj, 'ellipsoid', ['ax', 'by', 'cz', 'zcut1', 'zcut2', 'lunit']) + self.position_globally() -class GDMLElTubeExporter(GDMLSolidExporter): +class GDMLEllipsoidExporter(SolidExporter): def __init__(self, obj): - super().__init__(obj, 'eltube', ['dx', 'dy', 'dz', 'lunit']) - + super().__init__(obj) -class GDMLHypeExporter(GDMLSolidExporter): + def generate_surfaces(self): + mul = self.getMult() + name = NameManager.getName(self.obj) + surf1 = EllipsoidSurfaceExporter(name, mul * self.obj.ax, mul * self.obj.by, mul * self.obj.cz) + self.surfaces = [surf1] + region_expr = f"-{surf1.id}" + if hasattr(self.obj, 'zcut1'): + bot_plane = PlaneSurfaceExporter(name + '_zcut1', Vector(0, 0, 1), self.obj.zcut1) + self.surfaces.append(bot_plane) + region_expr += f' +{bot_plane.id}' + if hasattr(self.obj, 'zcut2'): + top_plane = PlaneSurfaceExporter(name + '_zcut2', Vector(0, 0, 1), self.obj.zcut2) + self.surfaces.append(top_plane) + region_expr += f' -{top_plane.id}' + + self.region = Region(region_expr) + self.position_globally() + + +class GDMLHypeExporter(SolidExporter): def __init__(self, obj): - super().__init__(obj, 'hype', ['rmin', 'rmax', 'z', 'inst', 'outst', 'aunit', 'lunit']) + super().__init__(obj) + def generate_surfaces(self): + from .GDMLObjects import getAngleRad + + name = NameManager.getName(self.obj) + + mul = self.getMult() + name = NameManager.getName(self.obj) + rmin = mul * self.obj.rmin + rmax = mul * self.obj.rmax + z = mul * self.obj.z + outst = getAngleRad(self.obj.aunit, self.obj.outst) + inst = getAngleRad(self.obj.aunit, self.obj.inst) + + # this should probably be a global variable, but + # for now adopt the value used in geant4.10.07.p02 + NUMBER_OF_DIVISIONS = 36 + sqrtan1 = math.tan(inst) + sqrtan1 *= sqrtan1 + sqrtan2 = math.tan(outst) + sqrtan2 *= sqrtan2 + + # Prepare two polylines + ns = NUMBER_OF_DIVISIONS + if sqrtan1 == 0.0: + nz1 = 2 + else: + nz1 = ns + 1 + if sqrtan2 == 0.0: + nz2 = 2 + else: + nz2 = ns + 1 -class GDMLParaboloidExporter(GDMLSolidExporter): - def __init__(self, obj): - super().__init__(obj, 'paraboloid', ['rlo', 'rhi', 'dz', 'lunit']) + halfZ = z / 2 + # + # solid generated by external hyperbeloid + dz2 = z / (nz2 - 1) + zz = [halfZ - dz2 * i for i in range(0, nz2)] + rr = [math.sqrt(sqrtan2 * zi * zi + rmax * rmax) for zi in zz] + + self.region = Region("") + + planes_dict = {} + outer_region = Region("") + for i in range(0, nz2 - 1): + v0 = Vector(rr[i], 0, zz[i]) + v1 = Vector(rr[i + 1], 0, zz[i + 1]) + surface = cone_from_line_segment(v0, v1) + self.surfaces.append(surface) + edge_region = f"-{surface.id} " + for v in [v0, v1]: + if v.z in planes_dict: + plane = planes_dict[v.z] + else: + plane = PlaneSurfaceExporter(", Vector(0, 0, 1", v.z) + self.surfaces.append(plane) + if v == v0: + edge_region += f"+{plane.id} " + else: + edge_region += f"-{plane.id} " + outer_region = outer_region.union(edge_region) + self.region = outer_region -class GDMLOrbExporter(GDMLSolidExporter): - def __init__(self, obj): - super().__init__(obj, 'orb', ['r', 'lunit']) + if rmin != 0: + # + # solid generated by internal hyperboloid + dz1 = z / (nz1 - 1) + zz = [halfZ - dz1 * i for i in range(0, nz1)] + rr = [math.sqrt(sqrtan1 * zi * zi + rmin * rmin) for zi in zz] + + inner_region = Region("") + for i in range(0, nz1 - 1): + v0 = Vector(rr[i], 0, zz[i]) + v1 = Vector(rr[i + 1], 0, zz[i + 1]) + surface = cone_from_line_segment(v0, v1) + self.surfaces.append(surface) + edge_region = f"-{surface.id} " + for v in [v0, v1]: + if v.z in planes_dict: + plane = planes_dict[v.z] + else: + plane = PlaneSurfaceExporter(", Vector(0, 0, 1", v.z) + self.surfaces.append(plane) + if v == v0: + edge_region += f"+{plane.id} " + else: + edge_region += f"-{plane.id} " + inner_region = inner_region.union(edge_region) + self.region = self.region.cut(inner_region) -class GDMLParaExporter(GDMLSolidExporter): - def __init__(self, obj): - super().__init__(obj, 'para', ['x', 'y', 'z', 'alpha', 'theta', 'phi', 'aunit', 'lunit']) + top_plane = PlaneSurfaceExporter(name + '_top', Vector(0, 0, 1), z / 2) + bot_plane = PlaneSurfaceExporter(name + '_top', Vector(0, 0, 1), -z / 2) + self.surfaces += [top_plane, bot_plane] + self.region = self.region.intersection(Region(f"-{top_plane.id} +{bot_plane.id}")) + self.position_globally() -class GDMLPolyconeExporter(GDMLSolidExporter): + +class GDMLParaboloidExporter(SolidExporter): def __init__(self, obj): - super().__init__(obj, 'polycone') + super().__init__(obj) - def export(self): - if self.exported(): - return - super().export() - - cone = ET.SubElement( - solids, - "polycone", - { - "name": self.name(), - "startphi": str(self.obj.startphi), - "deltaphi": str(self.obj.deltaphi), - "aunit": self.obj.aunit, - "lunit": self.obj.lunit, - }, - ) - self._exportScaled() + def generate_surfaces(self): + mul = self.getMult() + name = NameManager.getName(self.obj) + rlo = mul * self.obj.rlo + rhi = mul * self.obj.rhi + dz = mul * self.obj.dz - for zplane in self.obj.OutList: - ET.SubElement( - cone, - "zplane", - { - "rmin": str(zplane.rmin), - "rmax": str(zplane.rmax), - "z": str(zplane.z), - }, - ) + surf = ParaboloidSurfaceExporter(name, rlo, rhi, dz) + top_surface = PlaneSurfaceExporter(name + '_top', Vector(0, 0, 1), dz) + bot_surface = PlaneSurfaceExporter(name + '_bot', Vector(0, 0, 1), -dz) + + self.surfaces = [surf, top_surface, bot_surface] + self.region = Region(f"-{surf.id} +{bot_surface.id} -{top_surface.id}") + self.position_globally() -class GDMLGenericPolyconeExporter(GDMLSolidExporter): + +class GDMLOrbExporter(SolidExporter): def __init__(self, obj): - super().__init__(obj, 'genericPolycone') + super().__init__(obj) - def export(self): - if self.exported(): - return - super().export() - - cone = ET.SubElement( - solids, - "genericPolycone", - { - "name": self.name(), - "startphi": str(self.obj.startphi), - "deltaphi": str(self.obj.deltaphi), - "aunit": self.obj.aunit, - "lunit": self.obj.lunit, - }, - ) - self._exportScaled() - for rzpoint in self.obj.OutList: - ET.SubElement( - cone, "rzpoint", {"r": str(rzpoint.r), "z": str(rzpoint.z)} - ) + def generate_surfaces(self): + mul = self.getMult() + center = Vector(0, 0, 0) + radius = self.obj.rmin * mul + surf = SphereSurfaceExporter(f"{self.name()}", center, radius) + self.surfaces.append(surf) + self.region = Region(f"-{surf.id}") + self.position_globally() -class GDMLGenericPolyhedraExporter(GDMLSolidExporter): +class GDMLPolyconeExporter(SolidExporter): def __init__(self, obj): - super().__init__(obj, 'genericPolyhedra') + super().__init__(obj) - def export(self): - if self.exported(): - return - super().export() - - polyhedra = ET.SubElement( - solids, - "genericPolyhedra", - { - "name": self.name(), - "startphi": str(self.obj.startphi), - "deltaphi": str(self.obj.deltaphi), - "numsides": str(self.obj.numsides), - "aunit": self.obj.aunit, - "lunit": self.obj.lunit, - }, - ) - self._exportScaled() - for rzpoint in self.obj.OutList: - ET.SubElement( - polyhedra, - "rzpoint", - {"r": str(rzpoint.r), "z": str(rzpoint.z)}, - ) + def generate_surfaces(self): + from .GDMLObjects import getAngleRad + + name = NameManager.getName(self.obj) + mul = self.getMult() + planes_dict = {} + phi0 = getAngleRad(self.obj.aunit, self.obj.startphi) + deltaphi = getAngleRad(self.obj.aunit, self.obj.deltaphi) + phi1 = phi0 + deltaphi + + zplanes = self.obj.OutList + num_zplanes = len(zplanes) + + self.region = Region("") + + normal = Vector(0, 0, 1) + for i in range(num_zplanes - 1): + region = "" + + zplane0 = zplanes[i] + zplane1 = zplanes[i + 1] + + rmin0 = mul * zplane0.rmin + rmax0 = mul * zplane0.rmax + z0 = mul * zplane0.z + + rmin1 = mul * zplane1.rmin + rmax1 = mul * zplane1.rmax + z1 = mul * zplane1.z + if rmin0 != 0 and rmin1 != 0: + v0 = Vector(rmin0, 0, z0) + v1 = Vector(rmin1, 0, z1) + surface = cone_from_line_segment(v0, v1) + self.surfaces.append(surface) + region += f"+{surface.id} " + + if rmax0 != 0 and rmax1 != 0: + v0 = Vector(rmax0, 0, z0) + v1 = Vector(rmax1, 0, z1) + surface = cone_from_line_segment(v0, v1) + self.surfaces.append(surface) + region += f"-{surface.id} " + + if z0 in planes_dict: + bot_plane = planes_dict[z0] + else: + bot_plane = PlaneSurfaceExporter(name + '_bot', normal, z0) + planes_dict[z0] = bot_plane + self.surfaces.append(bot_plane) + + if z1 in planes_dict: + top_plane = planes_dict[z1] + else: + top_plane = PlaneSurfaceExporter(name + '_top', normal, z1) + planes_dict[z1] = top_plane + self.surfaces.append(top_plane) + + region += f"+{bot_plane.id} -{top_plane.id} " + + self.region = self.region.union(Region(region)) + + if deltaphi < 2 * math.pi: + phi0_normal = Vector(math.sin(phi0), -math.cos(phi0), 0) + phi1_normal = Vector(math.sin(phi1), -math.cos(phi1), 0) + phi0_plane = PlaneSurfaceExporter(name + '_phi0', phi0_normal, 0) + phi1_plane = PlaneSurfaceExporter(name + '_phi1', phi1_normal, 0) + self.surfaces += [phi0_plane, phi1_plane] + if phi0_normal.cross(phi1_normal).z > 0: + region = f" +{phi0_plane.id} -{phi1_plane.id}" + else: + region = f"( +{phi0_plane.id} | -{phi1_plane.id})" + + self.region = self.region.intersection(Region(region)) + + self.position_globally() -class GDMLPolyhedraExporter(GDMLSolidExporter): +class GDMLGenericPolyconeExporter(SolidExporter): def __init__(self, obj): - super().__init__(obj, 'polyhedra') + super().__init__(obj) - def export(self): - if self.exported(): - return - super().export() - - poly = ET.SubElement( - solids, - "polyhedra", - { - "name": self.name(), - "startphi": str(self.obj.startphi), - "deltaphi": str(self.obj.deltaphi), - "numsides": str(self.obj.numsides), - "aunit": self.obj.aunit, - "lunit": self.obj.lunit, - }, - ) - self._exportScaled() + def generate_surfaces(self): + from .GDMLObjects import getAngleRad - for zplane in self.obj.OutList: - ET.SubElement( - poly, - "zplane", - { - "rmin": str(zplane.rmin), - "rmax": str(zplane.rmax), - "z": str(zplane.z), - }, - ) + name = NameManager.getName(self.obj) + mul = self.getMult() + planes_dict = {} + phi0 = getAngleRad(self.obj.aunit, self.obj.startphi) + deltaphi = getAngleRad(self.obj.aunit, self.obj.deltaphi) + phi1 = phi0 + deltaphi + zplanes = self.obj.OutList + num_zplanes = len(zplanes) -class GDMLSphereExporter(GDMLSolidExporter): - def __init__(self, obj): - super().__init__(obj, "sphere", ['rmin', 'rmax', 'startphi', 'deltaphi', - 'starttheta', 'deltatheta', 'aunit', 'lunit']) + self.region = Region("") + normal = Vector(0, 0, 1) + for i in range(num_zplanes): + region = "" -class GDMLSampledTessellatedExporter(GDMLSolidExporter): - def __init__(self, obj): - super().__init__(obj, 'tesselated') + zplane0 = zplanes[i] + zplane1 = zplanes[(i + 1) % num_zplanes] - def export(self): - if self.exported(): - return - super().export() - - tessName = self.name() - print(f"tessname: {tessName}") - # Use more readable version - tessVname = tessName + "_" - # print(dir(obj)) - - verts = self.obj.vertsList - tess = ET.SubElement(solids, "tessellated", {"name": tessName}) - for i, v in enumerate(verts): - exportDefineVertex(tessVname, v, i) - - i = 0 - indexList = self.obj.indexList - for nVerts in self.obj.vertsPerFacet: - # print(f'Normal at : {n} dot {dot} {clockWise}') - if nVerts == 3: - i0 = indexList[i] - i1 = indexList[i + 1] - i2 = indexList[i + 2] - ET.SubElement( - tess, - "triangular", - { - "vertex1": tessVname + str(i0), - "vertex2": tessVname + str(i1), - "vertex3": tessVname + str(i2), - "type": "ABSOLUTE", - }, - ) - elif nVerts == 4: - i0 = indexList[i] - i1 = indexList[i + 1] - i2 = indexList[i + 2] - i3 = indexList[i + 3] - ET.SubElement( - tess, - "quadrangular", - { - "vertex1": tessVname + str(i0), - "vertex2": tessVname + str(i1), - "vertex3": tessVname + str(i2), - "vertex4": tessVname + str(i3), - "type": "ABSOLUTE", - }, - ) - i += nVerts - self._exportScaled() + r0 = mul * zplane0.r + z0 = mul * zplane0.z + r1 = mul * zplane1.r + z1 = mul * zplane1.z -class GDMLTessellatedExporter(GDMLSolidExporter): - def __init__(self, obj): - super().__init__(obj, 'tesselated') + v0 = Vector(r0, 0, z0) + v1 = Vector(r1, 0, z1) + surface = cone_from_line_segment(v0, v1) + self.surfaces.append(surface) - def export(self): - if self.exported(): - return - super().export() + if i == num_zplanes - 1: + region += f"+{surface.id} " + else: + region += f"-{surface.id} " - tessName = self.name() - # Use more readable version - tessVname = tessName + "_" - # print(dir(obj)) - vertexHashcodeDict = {} + if z0 in planes_dict: + bot_plane = planes_dict[z0] + else: + bot_plane = PlaneSurfaceExporter(name + '_bot', normal, z0) + planes_dict[z0] = bot_plane + self.surfaces.append(bot_plane) - """ - tess = ET.SubElement(solids, 'tessellated', {'name': tessName}) - - #for i, v in enumerate(self.obj.Shape.Vertexes): - for i, v in enumerate(self.obj.Shape.Vertexes): - vertexHashcodeDict[v.hashCode()] = i - exportDefineVertex(tessVname, self.obj.Vertexes[i], i) - - for f in self.obj.Shape.Faces: - # print(f'Normal at : {n} dot {dot} {clockWise}') - vertexes = f.OuterWire.OrderedVertexes - if len(f.Edges) == 3: - i0 = vertexHashcodeDict[vertexes[0].hashCode()] - i1 = vertexHashcodeDict[vertexes[1].hashCode()] - i2 = vertexHashcodeDict[vertexes[2].hashCode()] - ET.SubElement(tess, 'triangular', { - 'vertex1': tessVname+str(i0), - 'vertex2': tessVname+str(i1), - 'vertex3': tessVname+str(i2), - 'type': 'ABSOLUTE'}) - elif len(f.Edges) == 4: - i0 = vertexHashcodeDict[vertexes[0].hashCode()] - i1 = vertexHashcodeDict[vertexes[1].hashCode()] - i2 = vertexHashcodeDict[vertexes[2].hashCode()] - i3 = vertexHashcodeDict[vertexes[3].hashCode()] - ET.SubElement(tess, 'quadrangular', { - 'vertex1': tessVname+str(i0), - 'vertex2': tessVname+str(i1), - 'vertex3': tessVname+str(i2), - 'vertex4': tessVname+str(i3), - 'type': 'ABSOLUTE'}) - """ - tess = ET.SubElement(solids, "tessellated", {"name": tessName}) - placementCorrection = self.obj.Placement.inverse() - for i, v in enumerate(self.obj.Shape.Vertexes): - vertexHashcodeDict[v.hashCode()] = i - exportDefineVertex(tessVname, placementCorrection * v.Point, i) - - for f in self.obj.Shape.Faces: - # print(f'len(f.Edges) {len(f.Edges)}') - # print(f'Normal at : {n} dot {dot} {clockWise}') - vertexes = f.OuterWire.OrderedVertexes - if len(f.Edges) == 3: - i0 = vertexHashcodeDict[vertexes[0].hashCode()] - i1 = vertexHashcodeDict[vertexes[1].hashCode()] - i2 = vertexHashcodeDict[vertexes[2].hashCode()] - ET.SubElement( - tess, - "triangular", - { - "vertex1": tessVname + str(i0), - "vertex2": tessVname + str(i1), - "vertex3": tessVname + str(i2), - "type": "ABSOLUTE", - }, - ) - elif len(f.Edges) == 4: - i0 = vertexHashcodeDict[vertexes[0].hashCode()] - i1 = vertexHashcodeDict[vertexes[1].hashCode()] - i2 = vertexHashcodeDict[vertexes[2].hashCode()] - i3 = vertexHashcodeDict[vertexes[3].hashCode()] - ET.SubElement( - tess, - "quadrangular", - { - "vertex1": tessVname + str(i0), - "vertex2": tessVname + str(i1), - "vertex3": tessVname + str(i2), - "vertex4": tessVname + str(i3), - "type": "ABSOLUTE", - }, - ) - self._exportScaled() + if z1 in planes_dict: + top_plane = planes_dict[z1] + else: + top_plane = PlaneSurfaceExporter(name + '_top', normal, z1) + planes_dict[z1] = top_plane + self.surfaces.append(top_plane) + + if i < num_zplanes - 1: + region += f"+{bot_plane.id} -{top_plane.id} " + self.region = self.region.union(Region(region)) + else: + self.region = self.region.intersection(Region(region)) + + if deltaphi < 2 * math.pi: + phi0_normal = Vector(math.sin(phi0), -math.cos(phi0), 0) + phi1_normal = Vector(math.sin(phi1), -math.cos(phi1), 0) + phi0_plane = PlaneSurfaceExporter(name + '_phi0', phi0_normal, 0) + phi1_plane = PlaneSurfaceExporter(name + '_phi1', phi1_normal, 0) + self.surfaces += [phi0_plane, phi1_plane] + if phi0_normal.cross(phi1_normal).z > 0: + region = f" +{phi0_plane.id} -{phi1_plane.id}" + else: + region = f"( +{phi0_plane.id} | -{phi1_plane.id})" + self.region = self.region.intersection(Region(region)) -class GDMLTetraExporter(GDMLSolidExporter): + self.position_globally() + + +class GDMLSphereExporter(SolidExporter): def __init__(self, obj): - super().__init__(obj, 'tet') + super().__init__(obj) - def export(self): - if self.exported(): - return - super().export() - - tetraName = self.name() - v1Name = tetraName + "v1" - v2Name = tetraName + "v2" - v3Name = tetraName + "v3" - v4Name = tetraName + "v4" - exportDefine(v1Name, self.obj.v1) - exportDefine(v2Name, self.obj.v2) - exportDefine(v3Name, self.obj.v3) - exportDefine(v4Name, self.obj.v4) + def generate_surfaces(self): + from .GDMLObjects import checkFullCircle, getAngleRad - ET.SubElement( - solids, - "tet", - { - "name": tetraName, - "vertex1": v1Name, - "vertex2": v2Name, - "vertex3": v3Name, - "vertex4": v4Name, - }, - ) - self._exportScaled() + mul = self.getMult() + center = Vector(0, 0, 0) + radius = self.obj.rmin * mul + if radius != 0: + inner_surface = SphereSurfaceExporter(f"{self.name()}_ir", center, radius) + self.surfaces.append(inner_surface) + else: + inner_surface = None + + radius = self.obj.rmax * mul + outer_surface = SphereSurfaceExporter(f"{self.name()}_or", + center, radius) + self.surfaces.append(outer_surface) + + if not checkFullCircle(self.obj.aunit, self.obj.deltaphi): + startPhi = getAngleRad(self.obj.aunit, self.obj.startphi) + nx = -math.sin(startPhi) + ny = math.cos(startPhi) + phi0_normal = Vector(nx, ny, 0) + D = 0 + + phi0_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi0_plane", phi0_normal, D) + self.surfaces.append(phi0_plane) + + deltaphi = getAngleRad(self.obj.aunit, self.obj.deltaphi) + endphi = startPhi + deltaphi + nx = -math.sin(endphi) + ny = math.cos(endphi) + phi1_normal = Vector(nx, ny, 0) + + phi1_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi1_plane", phi1_normal, D) + self.surfaces.append(phi1_plane) + + startTheta = getAngleRad(self.obj.aunit, self.obj.starttheta) + deltaTheta = getAngleRad(self.obj.aunit, self.obj.deltatheta) + endTheta = startTheta + deltaTheta + if endTheta > math.pi: + endTheta = math.pi + + cones_region = "" + if startTheta > 0: + center = Vector(0, 0, 0) + axis = Vector(0, 0, 1) + mid_plane = PlaneSurfaceExporter(f"{self.obj.Label}_mid_plane", axis, 0) + + cone1_surface = None + if startTheta < math.pi / 2: + cone1_surface = ConeSurfaceExporter(f"{self.obj.Label}_thet1_cone", center, axis, startTheta) + elif startTheta > math.pi / 2: + cone1_surface = ConeSurfaceExporter(f"{self.obj.Label}_thet1_cone", center, axis, math.pi - startTheta) + + cone2_surface = None + if endTheta < math.pi / 2: + cone2_surface = ConeSurfaceExporter(f"{self.obj.Label}_thet2_cone", center, axis, endTheta) + elif endTheta > math.pi / 2 and endTheta != math.pi: + cone2_surface = ConeSurfaceExporter(f"{self.obj.Label}_thet2_cone", center, axis, math.pi - endTheta) + + if cone1_surface is not None: + self.surfaces.append(cone1_surface) + if cone2_surface is not None: + self.surfaces.append(cone2_surface) + self.surfaces.append(mid_plane) + + # three cases: + # case 1, startTheta, endTheta < 90 deg + if (0 < startTheta < math.pi / 2) and (0 < endTheta < math.pi / 2): + cones_region = f"+{cone1_surface.id} -{cone2_surface.id} +{mid_plane.id}" + + # case 2 startTheta < 90, endTheta > 90 + elif (0 < startTheta < math.pi / 2) and (math.pi / 2 < endTheta < math.pi): + cones_region = f"(+{mid_plane.id} +{cone1_surface.id}) | (-{mid_plane.id} +{cone2_surface.id})" + + # case 3, startTheta, endTheta > 90 deg + elif (math.pi / 2 < startTheta < math.pi) and (math.pi / 2 < endTheta < math.pi): + cones_region = f"+{cone2_surface.id} -{cone1_surface.id} -{mid_plane.id}" + + # edge cases: one or the other of starTheta, endTheta = math.pi/2, math.pi + elif (startTheta == math.pi / 2) and (endTheta < math.pi): + cones_region = f"-{mid_plane.id} +{cone2_surface.id}" + + elif (0 < startTheta < math.pi / 2) and endTheta == math.pi / 2: + cones_region = f"+{cone1_surface.id} +{mid_plane.id}" + + elif startTheta == math.pi / 2 and endTheta == math.pi: + cones_region = f"-{mid_plane.id}" + + elif startTheta > math.pi / 2 and endTheta == math.pi: + cones_region = f"-{cone1_surface.id} -{mid_plane.id}" + + elif startTheta == 0 and endTheta != math.pi: + center = Vector(0, 0, 0) + axis = Vector(0, 0, 1) + mid_plane = PlaneSurfaceExporter(f"{self.obj.Label}_mid_plane", axis, 0) + self.surfaces.append(mid_plane) + + if endTheta < math.pi / 2: + cone2_surface = ConeSurfaceExporter(f"{self.obj.Label}_thet2_cone", center, axis, endTheta) + self.surfaces.append(cone2_surface) + cones_region = f"-{cone2_surface.id} +{mid_plane.id}" + elif endTheta > math.pi / 2: + cone2_surface = ConeSurfaceExporter(f"{self.obj.Label}_thet2_cone", center, axis, math.pi - endTheta) + self.surfaces.append(cone2_surface) + cones_region = f"+{cone2_surface.id} | +{mid_plane.id}" + else: # endTheta == pi/2 + cones_region = f"+{mid_plane.id}" + + region = f"-{outer_surface.id}" + if inner_surface is not None: + region += f" +{inner_surface.id}" + + if not checkFullCircle(self.obj.aunit, self.obj.deltaphi): + if phi0_normal.cross(phi1_normal).z > 0: + region += f" +{phi0_plane.id} -{phi1_plane.id}" + else: + region += f" (+{phi0_plane.id} | -{phi1_plane.id})" + self.region = Region(region) + if cones_region != "": + self.region = self.region.intersection(Region(cones_region)) -class GDMLTetrahedronExporter(GDMLSolidExporter): - def __init__(self, obj): - super().__init__(obj, 'tet') + self.position_globally() - def export(self): - if self.exported(): - return - super().export() - global structure - global solids - tetrahedronName = self.name() - print("Len Tet" + str(len(self.obj.Proxy.Tetra))) - count = 0 - for t in self.obj.Proxy.Tetra: - tetraName = tetrahedronName + "_" + str(count) - v1Name = tetraName + "v1" - v2Name = tetraName + "v2" - v3Name = tetraName + "v3" - v4Name = tetraName + "v4" - exportDefine(v1Name, t[0]) - exportDefine(v2Name, t[1]) - exportDefine(v3Name, t[2]) - exportDefine(v4Name, t[3]) - ET.SubElement( - solids, - "tet", - { - "name": tetraName, - "vertex1": v1Name, - "vertex2": v2Name, - "vertex3": v3Name, - "vertex4": v4Name, - }, - ) - lvName = "LVtetra" + str(count) - lvol = ET.SubElement(structure, "volume", {"name": lvName}) - ET.SubElement(lvol, "materialref", {"ref": self.obj.material}) - ET.SubElement(lvol, "solidref", {"ref": tetraName}) - count += 1 - - # Now put out Assembly - assembly = ET.SubElement( - structure, "assembly", {"name": tetrahedronName} - ) - count = 0 - for t in self.obj.Proxy.Tetra: - lvName = "Tetra" + str(count) - physvol = ET.SubElement( - assembly, "physvol", {"name": "PV_Tetra" + str(count)} - ) - ET.SubElement(physvol, "volumeref", {"ref": lvName}) - # ET.SubElement(physvol, 'position') - # ET.SubElement(physvol, 'rotation') - count += 1 - self._exportScaled() +def generate_half_loop_surfaces(loop_half, surfaces): + ''' + generate surfaces needed for the half loop(circle). + :param: loop_half: vertexes of the half circle + :return: The region of the surfaces + ''' + Nvert = len(loop_half) + region = Region("") + # Instead of producing a bottom and top planes for each edge, we produce + # only one plane per vertex. To keep track if we produced a plane for a given + # vertex, we use a dictionary plane_dict[vertex.z = surface. See below of how we decide the region + planes_dict = {} + for i in range(0, Nvert - 1): + v0 = loop_half[i] + v1 = loop_half[i + 1] + outer = v1.z > v0.z + surface = cone_from_line_segment(v0, v1) + if surface is not None: + surfaces.append(surface) + if outer: + edge_region = f"-{surface.id} " + else: + edge_region = f"+{surface.id} " -class GDMLTorusExporter(GDMLSolidExporter): + for v in [v0, v1]: + if v.z in planes_dict: + plane = planes_dict[v.z] + else: + plane = PlaneSurfaceExporter("", Vector(0, 0, 1), v.z) + surfaces.append(plane) + planes_dict[v.z] = plane + if outer and v == v0: + plane_region = f"+{plane.id} " + elif outer and v == v1: + plane_region = f"-{plane.id} " + elif (not outer) and v == v0: + plane_region = f"-{plane.id} " + elif (not outer) and v == v1: + plane_region = f"+{plane.id} " + + edge_region += plane_region + + region = region.union(Region(edge_region)) + + return region + + +class GDMLTorusExporter(SolidExporter): def __init__(self, obj): - super().__init__(obj, 'torus', ['rmin', 'rmax', 'rtor', - 'startphi', 'deltaphi', 'aunit', 'lunit']) + super().__init__(obj) + deviation = obj.ViewObject.Deviation / 100 + costhet = 1 - deviation + thet = math.acos(costhet) + N = int( + 2 * math.pi / (2 * thet)) # number of divisions of the circle that that given a maximum radial difference + # if Deviation, in percent + # make it even + self.N = 2 * int(N / 2) + + def inner_outer(self, center, radius): + ''' Return two ploylines, one is the outer (-90, 90 deg) + and one the inner (90, 270 degrees) + ''' + thet = -math.pi / 2 + dethet = 2 * math.pi / (self.N) + verts = [] + for i in range(self.N + 1): + x = radius * math.cos(thet) + z = radius * math.sin(thet) + verts.append(center + Vector(x, 0, z)) + thet += dethet + + outer = verts[:int(self.N / 2) + 1] + inner = verts[int(self.N / 2):] + return inner, outer + + def generate_circle_revolve(self, radius): + mul = self.getMult() + center = mul * self.obj.rtor * Vector(1, 0, 0) + inner, outer = self.inner_outer(center, mul * radius) + outer_region = generate_half_loop_surfaces(outer, self.surfaces) + inner_region = generate_half_loop_surfaces(inner, self.surfaces) + + region = outer_region.intersection(inner_region) + return region + + def generate_surfaces(self): + from .GDMLObjects import checkFullCircle, getAngleRad + + self.region = self.generate_circle_revolve(self.obj.rmax) + if self.obj.rmin > 0: + inside_region = self.generate_circle_revolve(self.obj.rmin) + self.region = self.region.cut(inside_region) + + if not checkFullCircle(self.obj.aunit, self.obj.deltaphi): + startPhi = getAngleRad(self.obj.aunit, self.obj.startphi) + nx = -math.sin(startPhi) + ny = math.cos(startPhi) + phi0_normal = Vector(nx, ny, 0) + D = 0 + + phi0_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi0_plane", phi0_normal, D) + self.surfaces.append(phi0_plane) + + deltaphi = getAngleRad(self.obj.aunit, self.obj.deltaphi) + endphi = startPhi + deltaphi + nx = -math.sin(endphi) + ny = math.cos(endphi) + phi1_normal = Vector(nx, ny, 0) + + phi1_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi1_plane", phi1_normal, D) + self.surfaces.append(phi1_plane) + + if phi0_normal.cross(phi1_normal).z > 0: + self.region = self.region.intersection(Region(f"+{phi0_plane.id} -{phi1_plane.id}")) + else: + self.region = self.region.intersection(Region(f"(+{phi0_plane.id} | -{phi1_plane.id})")) + + self.position_globally() -class GDMLTrapExporter(GDMLSolidExporter): +class TorusExporter(SolidExporter): def __init__(self, obj): - super().__init__(obj, 'trap', ['z', 'theta', 'phi', - 'x1', 'x2', 'x3', 'x4', 'y1', 'y2']) + super().__init__(obj) + deviation = self.obj.ViewObject.Deviation / 100 + costhet = 1 - deviation + thet = math.acos(costhet) + self.dthet = 2 * thet - def export(self): - if self.exported(): - return - super().export() + def get_verts(self): + ''' this is quite a bit more involved than appears''' + verts = [] + center = self.obj.Radius1.Value * Vector(1, 0, 0) + angle1 = math.radians(self.obj.Angle1) + angle2 = math.radians(self.obj.Angle2) + + v1 = Vector(math.cos(angle1), 0, -math.sin(angle1)) # unit vector in direction of angle1 + v2 = Vector(math.cos(angle2), 0, -math.sin(angle2)) # unit vector in direction of angle2 + # angular span is angle from v1 to v2, in CW direction. + sinthet = v1.cross(v2).y + thet = math.atan2(sinthet, v1.dot(v2)) + if thet < 0: + thet = 2 * math.pi + thet + + N = int(thet / self.dthet) + 1 + if N < 2: + N = 2 + + radius = self.obj.Radius2.Value + dthet = thet / (N - 1) # this is local + if thet < 2 * math.pi: + verts.append(center) + + verts += [center + radius * Vector(math.cos(angle1 + i * dthet), 0, -math.sin(angle1 + i * dthet)) for i in + range(N)] + + return verts + + def process_convex_polygon(self, verts): + + # generate the surfaces and region (one region!) for a convex polygon in the + # x-z plane. The assumption is that the verts are in CCW order. One needs to specify + # exactly what we mean by CCW in the x-z plane. If I look at the x-z plane, such that x is to my right + # and z upward, then a polygon with xy verts arranged CCW is one for which for, for two consecutive edges + # e1 and e2, e1 x e2 is in the -y direction. Yes, minus y, not positive y. + N = len(verts) + region_expr = "" + zmin = min([v.z for v in verts]) + zmax = max([v.z for v in verts]) + + for i in range(N): + v0 = verts[i] + v1 = verts[(i + 1) % N] + surf = cone_from_line_segment(v0, v1) + if surf is None: + continue + self.surfaces.append(surf) + # now we have to worry about the other half of the cone cutting out part of the polygon. + # it can do that if the vertex of the cone lies between zmin and zmax + extra_plane = None + # ugly, but we must remember surface properties are already in cm, so we need to multiply them by 10 + if isinstance(surf, ConeSurfaceExporter) and (zmin < 10 * surf.center.z < zmax): + extra_plane = PlaneSurfaceExporter("", Vector(0, 0, 1), 10 * surf.center.z) + self.surfaces.append(extra_plane) + + if v1.z > v0.z: # we are on the up side + region_expr += f"-{surf.id} " + + elif v1.z < v0.z: # on the down side, the extra plane can interfere + if extra_plane is not None: + if v1.z < 0: + region_expr += f"(+{surf.id} | +{extra_plane.id}) " + else: + region_expr += f"(+{surf.id} | -{extra_plane.id}) " + else: + region_expr += f"+{surf.id} " - ET.SubElement( - solids, - "trap", - { - "name": self.name(), - "z": str(self.obj.z), - "theta": str(self.obj.theta), - "phi": str(self.obj.phi), - "x1": str(self.obj.x1), - "x2": str(self.obj.x2), - "x3": str(self.obj.x3), - "x4": str(self.obj.x4), - "y1": str(self.obj.y1), - "y2": str(self.obj.y2), - "alpha1": str(self.obj.alpha), - "alpha2": str(self.obj.alpha), - "aunit": self.obj.aunit, - "lunit": self.obj.lunit, - }, - ) - self._exportScaled() + else: # v1.z == v0.z, the surface is a plane, either at the very top or very bottom, for the closed polygon + if v1.x < v0.x: + region_expr += f"-{surf.id} " + else: + region_expr += f"+{surf.id} " + self.region = self.region.union(Region(region_expr)) -class GDMLTrdExporter(GDMLSolidExporter): - def __init__(self, obj): - super().__init__(obj, 'trd', ['z', 'x1', 'x2', 'y1', 'y2', 'lunit']) + def generate_surfaces(self): + verts = self.get_verts() + verts.reverse() + + self.region = Region("") + # do we have a concave polygon in the x-z plane? + if (self.obj.Angle2 - self.obj.Angle1) < 360: + # Not a complete circle, so there is possibility of convext polygon + radius = self.obj.Radius2.Value + center = self.obj.Radius1.Value * Vector(1, 0, 0) + angle1 = math.radians(self.obj.Angle1) + angle2 = math.radians(self.obj.Angle2) + v1 = radius * Vector(math.cos(angle1), 0, -math.sin(angle1)) + v2 = radius * Vector(math.cos(angle2), 0, -math.sin(angle2)) + if v1.cross(v2).y < 0: # This is concave, break it into two convex polygons + imid = int(len(verts) / 2) + seg1 = verts[:imid + 1] + seg1.append(center) + seg2 = verts[imid:] + for seg in [seg1, seg2]: + self.process_convex_polygon(seg) + else: + self.process_convex_polygon(verts) + else: + self.process_convex_polygon(verts) + + if self.obj.Angle3 != 360: + startPhi = 0 + nx = -math.sin(startPhi) + ny = math.cos(startPhi) + phi0_normal = Vector(nx, ny, 0) + D = 0 + phi0_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi0_plane", phi0_normal, D) + self.surfaces.append(phi0_plane) + + deltaphi = math.radians(self.obj.Angle3) + endphi = startPhi + deltaphi + nx = -math.sin(endphi) + ny = math.cos(endphi) + phi1_normal = Vector(nx, ny, 0) + + phi1_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi1_plane", phi1_normal, D) + self.surfaces.append(phi1_plane) + + if phi0_normal.cross(phi1_normal).z > 0: + region = f" +{phi0_plane.id} -{phi1_plane.id}" + else: + region = f" (+{phi0_plane.id} | -{phi1_plane.id})" + self.region = self.region.intersection(Region(region)) -class GDMLTubeExporter(GDMLSolidExporter): - def __init__(self, obj): - super().__init__(obj, 'tube', ['rmin', 'rmax', 'startphi', 'deltaphi', 'aunit', 'z', 'lunit']) + self.position_globally() -class GDMLTwistedboxExporter(GDMLSolidExporter): +class GDMLTubeExporter(SolidExporter): def __init__(self, obj): - super().__init__(obj, 'twistedbox', ['PhiTwist', 'x', 'y', 'z', 'aunit', 'lunit']) + super().__init__(obj) + def generate_surfaces(self): + from .GDMLObjects import checkFullCircle, getAngleRad -class GDMLTwistedtrdExporter(GDMLSolidExporter): - def __init__(self, obj): - super().__init__(obj, 'twistedtrd', ['PhiTwist', 'x1', 'x2', 'y1', 'y2', 'z', 'aunit', 'lunit']) + normal = Vector(0, 0, 1) + mul = self.getMult() + + D = self.obj.z / 2 * mul + # a gdml tube has origin at its center + top_surface = PlaneSurfaceExporter(f"{self.name()}_top", normal, D) + + D = -self.obj.z / 2 * mul + # a gdml tube has origin at its center + bottom_surface = PlaneSurfaceExporter(f"{self.name()}_bot", normal, D) + + center = Vector(0, 0, 0) + axis = normal + radius = self.obj.rmin * mul + if radius != 0: + inner_surface = CylinderSurfaceExporter(f"{self.name()}_ir", center, axis, radius) + else: + inner_surface = None + + radius = self.obj.rmax * mul + outer_surface = CylinderSurfaceExporter(f"{self.name()}_or", center, axis, radius) + + if not checkFullCircle(self.obj.aunit, self.obj.deltaphi): + startPhi = getAngleRad(self.obj.aunit, self.obj.startphi) + nx = -math.sin(startPhi) + ny = math.cos(startPhi) + phi0_normal = Vector(nx, ny, 0) + D = 0 + phi0_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi0_plane", phi0_normal, D) + + deltaphi = getAngleRad(self.obj.aunit, self.obj.deltaphi) + endphi = startPhi + deltaphi + nx = -math.sin(endphi) + ny = math.cos(endphi) + phi1_normal = Vector(nx, ny, 0) + phi1_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi1_plane", phi1_normal, D) -class GDMLTwistedtrapExporter(GDMLSolidExporter): + self.surfaces = [top_surface, bottom_surface, outer_surface] + if inner_surface is not None: + self.surfaces.append(inner_surface) + + if not checkFullCircle(self.obj.aunit, self.obj.deltaphi): + self.surfaces += [phi0_plane, phi1_plane] + + region = f"+{bottom_surface.id} -{top_surface.id} -{outer_surface.id}" + if inner_surface is not None: + region += f" +{inner_surface.id}" + + if not checkFullCircle(self.obj.aunit, self.obj.deltaphi): + if phi0_normal.cross(phi1_normal).z > 0: + region += f" +{phi0_plane.id} -{phi1_plane.id}" + else: + region += f" (+{phi0_plane.id} | -{phi1_plane.id})" + + self.region = Region(region) + self.position_globally() + + +class GDMLTwistedSolidExporter(SolidExporter): def __init__(self, obj): - super().__init__(obj, 'twistedtrap', ['PhiTwist', 'x1', 'x2', 'y1', 'y2', 'x3', 'x4', 'z', - 'Theta', 'Phi', 'Alph', 'aunit', 'lunit']) + super().__init__(obj) + self.slice = 0 + self.dTwist = obj.ViewObject.AngularDeflection.Value + self.dTwist = min(self.dTwist, 5) -class GDMLTwistedtubsExporter(GDMLSolidExporter): +class GDMLTwistedboxExporter(GDMLTwistedSolidExporter): def __init__(self, obj): - super().__init__(obj, 'twistedtubs', ['twistedangle', 'endinnerrad', 'endouterrad', 'zlen', - 'phi', 'aunit', 'lunit']) + super().__init__(obj) + + def make4Walls(self, x, y): + name = NameManager.getName(self.obj) + surf1 = PlaneSurfaceExporter(f"{name}_{self.slice}_1", Vector(1, 0, 0), x / 2) + surf2 = PlaneSurfaceExporter(f"{name}_{self.slice}_2", Vector(0, 1, 0), y / 2) + surf3 = PlaneSurfaceExporter(f"{name}_{self.slice}_3", Vector(-1, 0, 0), x / 2) + surf4 = PlaneSurfaceExporter(f"{name}_{self.slice}_4", Vector(0, -1, 0), y / 2) + + return [surf1, surf2, surf3, surf4] + + def generate_surfaces(self): + from .GDMLObjects import getAngleDeg + mul = self.getMult() + + x = mul * self.obj.x + y = mul * self.obj.y + z = mul * self.obj.z + + angle = getAngleDeg(self.obj.aunit, self.obj.PhiTwist) + dPhi = self.dTwist # 2 degree rotation per step + N = int(angle / dPhi) + N = max(N, 10) + dz = z / N + + planes_dict = {} + region_expr = "" + z0 = -z / 2 + for i in range(0, N): + name = NameManager.getName(self.obj) + walls = self.make4Walls(x, y) + self.surfaces += walls + if z0 in planes_dict: + bot_plane = planes_dict[z0] + else: + bot_plane = PlaneSurfaceExporter(f"{name}_{self.slice}_bot", Vector(0, 0, 1), z0) + planes_dict[z0] = bot_plane + self.surfaces.append(bot_plane) + + z0 += dz + if z0 in planes_dict: + top_plane = planes_dict[z0] + else: + top_plane = PlaneSurfaceExporter(f"{name}_{self.slice}_top", Vector(0, 0, 1), z0) + planes_dict[z0] = top_plane + self.surfaces.append(top_plane) + + if region_expr == "": + region_expr = f"(-{walls[0].id} -{walls[1].id} -{walls[2].id} -{walls[3].id} +{bot_plane.id} -{top_plane.id})" + else: + region_expr += f" | (-{walls[0].id} -{walls[1].id} -{walls[2].id} -{walls[3].id} +{bot_plane.id} -{top_plane.id})" + + rot = FreeCAD.Rotation(Vector(0, 0, 1), -angle / 2 + i * dPhi) + for wall in walls: + wall.rotate(rot) + self.slice += 1 + self.region = Region(region_expr) -class GDMLXtruExporter(GDMLSolidExporter): + self.position_globally() + + +class GDMLTwistedtrdExporter(GDMLTwistedSolidExporter): def __init__(self, obj): - super().__init__(obj, 'xtru') + super().__init__(obj) - def export(self): - if self.exported(): - return - super().export() + def make4Walls(self, x, y): + name = NameManager.getName(self.obj) + surf1 = PlaneSurfaceExporter(f"{name}_{self.slice}_1", Vector(1, 0, 0), x / 2) + surf2 = PlaneSurfaceExporter(f"{name}_{self.slice}_2", Vector(0, 1, 0), y / 2) + surf3 = PlaneSurfaceExporter(f"{name}_{self.slice}_3", Vector(-1, 0, 0), x / 2) + surf4 = PlaneSurfaceExporter(f"{name}_{self.slice}_4", Vector(0, -1, 0), y / 2) + + return [surf1, surf2, surf3, surf4] + + def generate_surfaces(self): + from .GDMLObjects import getAngleDeg + + mul = self.getMult() + + x1 = self.obj.x1 * mul + x2 = self.obj.x2 * mul + y1 = self.obj.y1 * mul + y2 = self.obj.y2 * mul + z = self.obj.z * mul + + angle = getAngleDeg(self.obj.aunit, self.obj.PhiTwist) + dPhi = self.dTwist # 2 degree rotation per step + N = int(angle / dPhi) + N = max(N, 10) + dz = z / N + + planes_dict = {} + region_expr = "" + z0 = -z / 2 + for i in range(0, N): + t = i * 1.0 / (N - 1) + xside = x1 + t * (x2 - x1) + yside = y1 + t * (y2 - y1) + + name = NameManager.getName(self.obj) + walls = self.make4Walls(xside, yside) + self.surfaces += walls + if z0 in planes_dict: + bot_plane = planes_dict[z0] + else: + bot_plane = PlaneSurfaceExporter(f"{name}_{self.slice}_bot", Vector(0, 0, 1), z0) + planes_dict[z0] = bot_plane + self.surfaces.append(bot_plane) - xtru = ET.SubElement( - solids, "xtru", {"name": self.name(), "lunit": self.obj.lunit} - ) - for items in self.obj.OutList: - if items.Type == "twoDimVertex": - ET.SubElement( - xtru, - "twoDimVertex", - {"x": str(items.x), "y": str(items.y)}, - ) - if items.Type == "section": - ET.SubElement( - xtru, - "section", - { - "zOrder": str(items.zOrder), - "zPosition": str(items.zPosition), - "xOffset": str(items.xOffset), - "yOffset": str(items.yOffset), - "scalingFactor": str(items.scalingFactor), - }, - ) - self._exportScaled() + z0 += dz + if z0 in planes_dict: + top_plane = planes_dict[z0] + else: + top_plane = PlaneSurfaceExporter(f"{name}_{self.slice}_top", Vector(0, 0, 1), z0) + planes_dict[z0] = top_plane + self.surfaces.append(top_plane) + + if region_expr == "": + region_expr = f"(-{walls[0].id} -{walls[1].id} -{walls[2].id} -{walls[3].id} +{bot_plane.id} -{top_plane.id})" + else: + region_expr += f" | (-{walls[0].id} -{walls[1].id} -{walls[2].id} -{walls[3].id} +{bot_plane.id} -{top_plane.id})" + rot = FreeCAD.Rotation(Vector(0, 0, 1), -angle / 2 + i * dPhi) + for wall in walls: + wall.rotate(rot) + self.slice += 1 -class GDML2dVertexExporter(GDMLSolidExporter): + self.region = Region(region_expr) + + self.position_globally() + + +class GDMLTwistedtrapExporter(SolidExporter): def __init__(self, obj): - super().__init__(obj, 'twoDimVertex') + super().__init__(obj) - def export(self): - ET.SubElement( - solids, "twoDimVertex", {"x": self.obj.x, "y": self.obj.y} - ) + def make4Walls(self, verts): + name = NameManager.getName(self.obj) + n = len(verts) + surfaces = [] + for i in range(n): + i1 = (i + 1) % n + v0 = verts[i] + v1 = verts[i1] + dx = (v1 - v0).x + dy = (v1 - v0).y + normal = Vector(dy, -dx, 0) + normal.normalize() + D = v0.dot(normal) + + surf = PlaneSurfaceExporter(f"{name}_{self.slice}_1", normal, D) + surfaces.append(surf) + + return surfaces + + def generate_surfaces(self): + from .GDMLObjects import getAngleRad, getAngleDeg + + mul = self.getMult() + alpha = getAngleRad(self.obj.aunit, self.obj.Alph) + theta = getAngleRad(self.obj.aunit, self.obj.Theta) + phi = getAngleRad(self.obj.aunit, self.obj.Phi) + PhiTwist = getAngleDeg(self.obj.aunit, self.obj.PhiTwist) + y1 = mul * self.obj.y1 + x1 = mul * self.obj.x1 + x2 = mul * self.obj.x2 + y2 = mul * self.obj.y2 + x3 = mul * self.obj.x3 + x4 = mul * self.obj.x4 + z = mul * self.obj.z + + dTwist = self.dTwist + N = int(PhiTwist / dTwist) + N = max(N, 10) + dz = z / N + + tanalpha = math.tan(alpha) + + dt = 1.0 / N + t = 0 + + tanthet = math.tan(theta) + cosphi = math.cos(phi) + sinphi = math.sin(phi) + rhomax = z * tanthet + xoffset = -rhomax * cosphi / 2 + yoffset = -rhomax * sinphi / 2 + + planes_dict = {} + region_expr = "" + + for i in range(N): + # Vertexes, counter clock wise order + y = y1 + t * (y2 - y1) # go continuously from y1 to y2 + dx = y * tanalpha + x13 = x1 + t * (x3 - x1) # go continuously from x1 to x3 + x24 = x2 + t * (x4 - x2) # go continuously from x1 to x3 + zt = -z / 2 + t * z + rho = i * dz * tanthet + dxphi = xoffset + rho * cosphi + dyphi = yoffset + rho * sinphi + v1 = FreeCAD.Vector(-x13 / 2 - dx / 2 + dxphi, -y / 2 + dyphi, zt) + v2 = FreeCAD.Vector(x13 / 2 - dx / 2 + dxphi, -y / 2 + dyphi, zt) + v3 = FreeCAD.Vector(x24 / 2 + dx / 2 + dxphi, y / 2 + dyphi, zt) + v4 = FreeCAD.Vector(-x24 / 2 + dx / 2 + dxphi, y / 2 + dyphi, zt) + p = Part.makePolygon([v1, v2, v3, v4, v1]) + name = NameManager.getName(self.obj) + walls = self.make4Walls([v1, v2, v3, v4]) + + self.surfaces += walls + if zt in planes_dict: + bot_plane = planes_dict[zt] + else: + bot_plane = PlaneSurfaceExporter(f"{name}_{self.slice}_bot", Vector(0, 0, 1), zt) + planes_dict[zt] = bot_plane + self.surfaces.append(bot_plane) + + zt += dz + if zt in planes_dict: + top_plane = planes_dict[zt] + else: + top_plane = PlaneSurfaceExporter(f"{name}_{self.slice}_top", Vector(0, 0, 1), zt) + planes_dict[zt] = top_plane + self.surfaces.append(top_plane) + + if region_expr == "": + region_expr = f"(-{walls[0].id} -{walls[1].id} -{walls[2].id} -{walls[3].id} +{bot_plane.id} -{top_plane.id})" + else: + region_expr += f" | (-{walls[0].id} -{walls[1].id} -{walls[2].id} -{walls[3].id} +{bot_plane.id} -{top_plane.id})" + + rot = FreeCAD.Rotation(Vector(0, 0, 1), -PhiTwist / 2 + i * dTwist) + for wall in walls: + wall.rotate(rot) + + t += dt + self.slice += 1 + + self.region = Region(region_expr) + + self.position_globally() -class GDMLborderSurfaceExporter(GDMLSolidExporter): +class GDMLTwistedtubsExporter(GDMLTwistedSolidExporter): def __init__(self, obj): - super().__init__(obj, 'bordersurface') + super().__init__(obj) - def export(self): - borderSurface = ET.SubElement( - structure, - "bordersurface", - {"name": self.obj.Name, "surfaceproperty": self.obj.surface}, - ) + def tubeWalls(self, rin, rout, phi0, phi1): + ''' return four surfaces: a cylinder of radius rin, None if rin = 0 + a cylinder or radius rout, two planes with normals in the x-y plane + one starting at ph0, the other ending at phi1 + ''' + cylinder_in = None + name = NameManager.getName(self.obj) + center = Vector(0, 0, 0) + axis = Vector(0, 0, 1) + if rin > 0: + cylinder_in = CylinderSurfaceExporter(name + "_in", center, axis, rin) + cylinder_out = CylinderSurfaceExporter(name + "_out", center, axis, rout) + + u0 = Vector(math.cos(phi0), math.sin(phi0), 0) + u1 = Vector(math.cos(phi1), math.sin(phi1), 0) + n0 = Vector(-u0.y, u0.x, 0) + n1 = Vector(-u1.y, u1.x, 0) + + phi0_plane = PlaneSurfaceExporter(name + "_phi0", n0, 0) + phi1_plane = PlaneSurfaceExporter(name + "_phi1", n1, 0) + + return [cylinder_in, cylinder_out, phi0_plane, phi1_plane] + + def generate_surfaces(self): + from .GDMLObjects import getAngleRad, getAngleDeg + mul = self.getMult() + rin = mul * self.obj.endinnerrad + rout = mul * self.obj.endouterrad + z = mul * self.obj.zlen + phi = getAngleRad(self.obj.aunit, self.obj.phi) + angle = getAngleDeg(self.obj.aunit, self.obj.twistedangle) + dTwist = self.dTwist # 2 degree rotation per step + N = int(angle / dTwist) + N = max(N, 10) + dz = z / N + + planes_dict = {} + region_expr = "" + z0 = -z / 2 + phi0 = -phi / 2 + phi1 = phi / 2 + + region_expr = "" + self.region = Region("") + for i in range(0, N): + name = NameManager.getName(self.obj) + walls = self.tubeWalls(rin, rout, phi0, phi1) + rin_cylinder = walls[0] + rout_cylinder = walls[1] + + if rin > 0: + self.surfaces += walls + else: + self.surfaces += walls[1:] + + if z0 in planes_dict: + bot_plane = planes_dict[z0] + else: + bot_plane = PlaneSurfaceExporter(f"{name}_{self.slice}_bot", Vector(0, 0, 1), z0) + planes_dict[z0] = bot_plane + self.surfaces.append(bot_plane) + + z0 += dz + if z0 in planes_dict: + top_plane = planes_dict[z0] + else: + top_plane = PlaneSurfaceExporter(f"{name}_{self.slice}_top", Vector(0, 0, 1), z0) + planes_dict[z0] = top_plane + self.surfaces.append(top_plane) + + phi0_plane = walls[2] + phi1_plane = walls[3] + phi0_normal = phi0_plane.normal + phi1_normal = phi1_plane.normal + + if rin > 0: + rin_region = f"+{rin_cylinder.id}" + else: + rin_region = "" + + if phi0_normal.cross(phi1_normal).z > 0: + region_expr += f'({rin_region} -{rout_cylinder.id} +{bot_plane.id} -{top_plane.id} +{phi0_plane.id} -{phi1_plane.id})' + else: + region_expr += f'({rin_region} -{rout_cylinder.id} +{bot_plane.id} -{top_plane.id} (+{phi0_plane.id} | -{phi1_plane.id}))' + if i != N - 1: + region_expr += ' | ' + + rot = FreeCAD.Rotation(Vector(0, 0, 1), -angle / 2 + i * dTwist) + for wall in [phi0_plane, phi1_plane]: + wall.rotate(rot) + self.slice += 1 - print(self.obj.pv1) - if self.obj.pv1[:3] == "av_": - # for assembly auto generated names (starting with 'av_' we do not - # include the 'PV_' in the name - ET.SubElement(borderSurface, "physvolref", {"ref": self.obj.pv1}) - else: - ET.SubElement( - borderSurface, "physvolref", {"ref": "PV_" + self.obj.pv1} - ) - print(self.obj.pv1) - if self.obj.pv2[:3] == "av_": - ET.SubElement(borderSurface, "physvolref", {"ref": self.obj.pv2}) - else: - ET.SubElement( - borderSurface, "physvolref", {"ref": "PV_" + self.obj.pv2} - ) + self.region = Region(region_expr) + + self.position_globally() class MultiFuseExporter(SolidExporter): @@ -4359,231 +4750,300 @@ def name(self): solidName = "MultiFuse" + self.obj.Label return solidName - def export(self): - if self.exported(): - return - super().export() - - GDMLShared.trace("Multifuse - multiunion") - # test and fix - # First add solids in list before reference - print("Output Solids") + def generate_surfaces(self): exporters = [] + self.surfaces = [] + self.region = Region("") for sub in self.obj.OutList: exporter = SolidExporter.getExporter(sub) if exporter is not None: - exporter.export() - exporters.append(exporter) + exporter.generate_surfaces() + self.surfaces += exporter.surfaces + if self.region.expr == "": + self.region = Region(f'({exporter.get_region()})') + else: + self.region = self.region.union(exporter.get_region()) - GDMLShared.trace("Output Solids Complete") - multUnion = ET.SubElement(solids, "multiUnion", {"name": self.name()}) + ''' + The surfaces generated above already contain the absoulte placement, including that of the + boolean object, so no need for this + + objPlacement = self.obj.Placement + if objPlacement != FreeCAD.Placement(): + translation = objPlacement.Base + rotation = objPlacement.Rotation + for surf in self.surfaces: + surf.rotate(rotation) + surf.translate(translation) + ''' - num = 1 - for exp in exporters: - GDMLShared.trace(exp.name()) - node = ET.SubElement( - multUnion, "multiUnionNode", {"name": "node-" + str(num)} - ) - ET.SubElement(node, "solid", {"ref": exp.name()}) - processPlacement(exp.name(), node, exp.placement()) - num += 1 - GDMLShared.trace("Return MultiFuse") - self._exportScaled() +class MultiCommonExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) + + def name(self): + solidName = "MultiCommon" + self.obj.Label + return solidName + + def generate_surfaces(self): + exporters = [] + self.surfaces = [] + self.region = Region("") + for sub in self.obj.OutList: + exporter = SolidExporter.getExporter(sub) + if exporter is not None: + exporter.generate_surfaces() + self.surfaces += exporter.surfaces + if self.region.expr == "": + self.region = Region(f'({exporter.get_region()})') + else: + self.region = self.region.intersection(exporter.get_region()) class OrthoArrayExporter(SolidExporter): def __init__(self, obj): super().__init__(obj) - self._name = "MultiUnion-" + self.obj.Label - def export(self): - if self.exported(): - return - super().export() + def generate_surfaces(self): + self.region = Region("") + # for the time being and array is treated is a solid with a single material + # its region is the union of the array of regions of the individual array elements from . import arrayUtils base = self.obj.Base - print(f"Base {base.Label}") if hasattr(base, "TypeId") and base.TypeId == "App::Part": print( f"**** Arrays of {base.TypeId} ({base.Label}) currently not supported ***" ) return + baseExporter = SolidExporter.getExporter(base) if baseExporter is None: print(f"Cannot export {base.Label}") return - baseExporter.export() - volRef = baseExporter.name() - unionXML = ET.SubElement(solids, "multiUnion", {"name": self.name()}) - basePos = baseExporter.position() - baseRotation = FreeCAD.Rotation(baseExporter.rotation()) - for i, placement in enumerate(arrayUtils.placementList(self.obj, offsetVector=basePos)): - ix, iy, iz = arrayUtils.orthoIndexes(i, self.obj) - nodeName = f"{self.name()}_{ix}_{iy}_{iz}" - translate = placement.Base - nodeXML = ET.SubElement( - unionXML, "multiUnionNode", {"name": nodeName} - ) - ET.SubElement(nodeXML, "solid", {"ref": volRef}) - ET.SubElement( - nodeXML, - "position", - { - "name": f"{self.name()}_pos_{ix}_{iy}_{iz}", - "x": str(translate.x), - "y": str(translate.y), - "z": str(translate.z), - "unit": "mm", - }, - ) - if baseRotation.Angle != 0: - exportRotation(self.name(), nodeXML, baseRotation, invertRotation=False) - self._exportScaled() + # heuristic: If the rotation is not intrinsic to the base solid, then the + # position must be rotated as well. + tolerance = 1.0e-7 + rotatePosition = not base.Placement.Rotation.isSame(base.getGlobalPlacement().Rotation, tolerance) + positionRotation = base.getGlobalPlacement().Rotation + if rotatePosition: + print( + f" Will apply position rotation to {base.Label} because global rotation is different than base rotation") + # we need to also check for the array object rotation. This is NOT included in the global placement + if not self.obj.Placement.Rotation.isSame(base.getGlobalPlacement().Rotation, tolerance): + rotatePosition = True + positionRotation = self.obj.Placement.Rotation + + for i, placement in enumerate(arrayUtils.placementList(self.obj, offsetVector=self.obj.Placement.Base)): + solidExporter = SolidExporter.getExporter(base) + item_region = solidExporter.get_region() + translation = placement.Base + if rotatePosition: + translation = positionRotation * translation + # the rotation should also apply to the base, not just the position + solidExporter.rotate(positionRotation) + solidExporter.translate(translation) + self.region = self.region.union(item_region) + self.surfaces += solidExporter.surfaces class PolarArrayExporter(SolidExporter): def __init__(self, obj): super().__init__(obj) - def name(self): - solidName = "MultiUnion-" + self.obj.Label - return solidName - - def export(self): - if self.exported(): - return - super().export() - + def generate_surfaces(self): from . import arrayUtils base = self.obj.Base - print(base.Label) + + self.region = Region("") + + appPartBase = False if hasattr(base, "TypeId") and base.TypeId == "App::Part": print( f"**** Arrays of {base.TypeId} ({base.Label}) currently not supported ***" ) + return + baseExporter = SolidExporter.getExporter(base) - baseExporter.export() - baseRotation = baseExporter.rotation() - volRef = baseExporter.name() - unionXML = ET.SubElement(solids, "multiUnion", {"name": self.name()}) - positionVector = baseExporter.position() - for i, placement in enumerate(arrayUtils.placementList(self.obj, - offsetVector=positionVector)): - rot = placement.Rotation - pos = placement.Base - rot = rot * baseRotation - rot.Angle = -rot.Angle # undo angle reversal by exportRotation - nodeName = f"{self.name()}_{i}" - nodeXML = ET.SubElement( - unionXML, "multiUnionNode", {"name": nodeName} - ) - ET.SubElement(nodeXML, "solid", {"ref": volRef}) - exportPosition(nodeName, nodeXML, pos) - exportRotation(nodeName, nodeXML, rot) - self._exportScaled() + if baseExporter is None: + print(f"Cannot export {base.Label}") + return + + arrayRotation = self.obj.Placement.Rotation + basePos = base.Placement.Base + # arrayUtils takes care of the offset center of of rotation + # But the array may have been shifted/rotatied by its parent. + # Since the array itself is not exported, but rather the array base is, + # the base global position does not reflect the arrays global position + + placements = arrayUtils.placementList(self.obj, offsetVector=basePos) + + # I'll try doing the rotations from scratch + # let GlobalPlacement = G # Note: FOR AN ARRAY GLOBAL PLACEMENT APPLIES to the ARRAY, NOT BASE of the ARRAY + # let Container Placement = C (the effect of all the App::Parts that contain the base) + # let the base placement be P + # then must have G = C*P ==> C = G*P^-1 + # If the rotation by the polar array is rot + # Then the position after rotation ought to be + # rotation by polar array: position_by_array = array_center + rot * (P.Base - array_center) + # Position globally rotated_global_position = C*position_by_array + # Because we don't give a position or a rotation to the solid exporter surfaces (we only rotate or translate them) + # We need to first unrotate and untranslate the surfaces, then translate and rotate by the array rotation and + # translation as above + arrayObj = self.obj + G = arrayObj.getGlobalPlacement() # In top_obj_dict, arrays don't have children. + P = arrayObj.Placement + Pinv = P.inverse() + C = G * Pinv # this is what puts the array in its position globally + baseInvPlacement = base.Placement.inverse() + + for placement in placements: + placement1 = placement * baseInvPlacement + rot = placement1.Rotation + pos = placement1.Base + + solidExporter = SolidExporter.getExporter(base) + # get region generates a region with # global translation and global rotation + # let us assume now that object rotation is 0 + # array utils give position rotated about array center then shifted by array center + # + item_region = solidExporter.get_region() + # the get_region will position the base globally already. Here we should only add the array + # placements + solidExporter.rotate(rot) + solidExporter.translate(pos) + self.region = self.region.union(item_region) + self.surfaces += solidExporter.surfaces + + # self.position_globally() # this should take care of the array's global position class PathArrayExporter(SolidExporter): def __init__(self, obj): super().__init__(obj) - def name(self): - solidName = "MultiUnion-" + self.obj.Label - return solidName - - def export(self): - if self.exported(): - return - super().export() + def generate_surfaces(self): + self.region = Region("") base = self.obj.Base - print(base.Label) if hasattr(base, "TypeId") and base.TypeId == "App::Part": print( f"**** Arrays of {base.TypeId} ({base.Label}) currently not supported ***" ) return + baseExporter = SolidExporter.getExporter(base) - baseExporter.export() - volRef = baseExporter.name() - unionXML = ET.SubElement(solids, "multiUnion", {"name": self.name()}) + if baseExporter is None: + print(f"Cannot export {base.Label}") + return + count = self.obj.Count - positionVector = baseExporter.position() + positionVector = base.Placement.Base rot = base.Placement.Rotation + extraTranslation = self.obj.ExtraTranslation + pathObj = self.obj.PathObject path = pathObj.Shape.Edges[0] points = path.discretize(Number=count) + for i, point in enumerate(points): pos = point + positionVector + extraTranslation - nodeName = f"{self.name()}_{i}" - nodeXML = ET.SubElement( - unionXML, "multiUnionNode", {"name": nodeName} - ) - ET.SubElement(nodeXML, "solid", {"ref": volRef}) - exportPosition(nodeName, nodeXML, pos) - exportRotation(nodeName, nodeXML, rot) - self._exportScaled() + solidExporter = SolidExporter.getExporter(base) + item_region = solidExporter.get_region() + solidExporter.translate(pos) + self.region = self.region.union(item_region) + self.surfaces += solidExporter.surfaces class PointArrayExporter(SolidExporter): def __init__(self, obj): super().__init__(obj) - def name(self): - solidName = "MultiUnion-" + self.obj.Label - return solidName + def generate_surfaces(self): + from . import arrayUtils - def export(self): - if self.exported(): - return - super().export() + self.surfaces = [] + self.region = Region("") base = self.obj.Base - print(base.Label) if hasattr(base, "TypeId") and base.TypeId == "App::Part": print( f"**** Arrays of {base.TypeId} ({base.Label}) currently not supported ***" ) return + baseExporter = SolidExporter.getExporter(base) - baseExporter.export() - volRef = baseExporter.name() - unionXML = ET.SubElement(solids, "multiUnion", {"name": self.name()}) - positionVector = baseExporter.position() - rotBase = base.Placement.Rotation + if baseExporter is None: + print(f"Cannot export {base.Label}") + return + extraTranslation = self.obj.ExtraPlacement.Base extraRotation = self.obj.ExtraPlacement.Rotation - extraRotation.Angle = -extraRotation.Angle - rot = extraRotation * rotBase + pointObj = self.obj.PointObject points = pointObj.Points.Points - for i, point in enumerate(points): - pos = point + positionVector + extraTranslation - nodeName = f"{self.name()}_{i}" - nodeXML = ET.SubElement( - unionXML, "multiUnionNode", {"name": nodeName} - ) - ET.SubElement(nodeXML, "solid", {"ref": volRef}) - exportPosition(nodeName, nodeXML, pos) - exportRotation(nodeName, nodeXML, rot) - self._exportScaled() + arrayTranslation = self.obj.Placement.Base + arrayRotation = self.obj.Placement.Rotation + + # points = arrayUtils.placementList(self.obj, offsetVector=self.obj.Placement.Base) + for point in points: + if not arrayRotation.isSame(FreeCAD.Rotation(), 1e-7): + point = arrayRotation * point + pos = point + arrayTranslation + extraTranslation + solidExporter = SolidExporter.getExporter(base) + item_region = solidExporter.get_region() + solidExporter.rotate(arrayRotation) + solidExporter.translate(pos) + self.region = self.region.union(item_region) + self.surfaces += solidExporter.surfaces # # ------------------------------revolutionExporter ---------------------------- # global Deviation # Fractional deviation of revolve object + + ############################################# # Helper functions for Revolve construction # One of the closed curves (list of edges) representing a part # of the sketch +def cone_from_line_segment(v0, v1): + ''' + Given a line segment in the x-z plane, generate a cone in the half space containing the segment + :param: v0: One point on the segment (v0=Vector(x0, 0, z0) + :param: v1: another point on the segment v1=(Vector(x1, 0, z1) + :Return: The cone passing through the line segment. if v0.z == v1.z, return a cylinder + ''' + + dx = v1.x - v0.x + dz = v1.z - v0.z + u = (v1 - v0).normalize() # unit vector fro m v0 to v1 + if abs(dz) > 1e-9: + theta = math.atan(abs(dx / dz)) + axis = Vector(0, 0, 1) + + if abs(u.x) < 1e-9: # return a cylinder + surf = CylinderSurfaceExporter("", Vector(0, 0, 0), Vector(0, 0, 1), abs(v0.x)) + else: # return a cone + t2 = -v0.x / u.x + center = v0 + t2 * u + surf = ConeSurfaceExporter("", center, axis, theta) + + return surf + + elif abs(dx) > 1e-9: # a horizontal line, not sure if a plane is the correct answer + surf = PlaneSurfaceExporter("", Vector(0, 0, 1), v0.z) + return surf + + return None + class ClosedCurve: def __init__(self, name, edgeList): @@ -4609,7 +5069,7 @@ def isCircle(arc1, arc2): c1 = arc1.Curve c2 = arc2.Curve if (c1.TypeId != "Part::GeomCircle" or - c2.TypeId != "Part::GeomCircle"): + c2.TypeId != "Part::GeomCircle"): print("Not Arc") return False # They must have same radius @@ -4634,7 +5094,7 @@ def isCircle(arc1, arc2): return False ''' if (arc1.Vertexes[0].Point != arc2.Vertexes[1].Point or - arc1.Vertexes[1].Point != arc2.Vertexes[0].Point): + arc1.Vertexes[1].Point != arc2.Vertexes[0].Point): print("dont match ends") print(f'c1.0 {arc1.Vertexes[0].Point} c1.1 {arc1.Vertexes[1].Point}') print(f'c2.0 {arc2.Vertexes[0].Point} c2.1 {arc2.Vertexes[1].Point}') @@ -4660,17 +5120,119 @@ def arcs2circle(arc1, arc2): class RevolvedClosedCurve(ClosedCurve): - def __init__(self, name, edgelist, angle, axis): + def __init__(self, name, edgelist): super().__init__(name, edgelist) - self.angle = angle - self.axis = axis self.position = Vector(0, 0, 0) self.rotation = [0, 0, 0] # TBD self.deflectionFraction = 0.001 + self.surfaces = [] + self.region = Region("") + + def add_top_bottom(self, verts): + ''' If there is a single top/bottom vertex the cylinders/conves + meeting there will eventuall y extend to a common region that is outside + the revolved solid. So we must cap the top/botton with a top/bottom + plane in that case''' + zs = [v.z for v in verts] + maxz = max(zs) + minz = min(zs) + top_plane = PlaneSurfaceExporter("", Vector(0, 0, 1), maxz) + # add a top plane if one does not exist already + add_it = True + for surf in self.surfaces: + if surf == top_plane: + add_it = False + break + if add_it: + self.surfaces.append(top_plane) + self.region = self.region.intersection(Region(f"-{top_plane.id}")) + + bottom_plane = PlaneSurfaceExporter("", Vector(0, 0, 1), minz) + # add a top plane if one does not exist already + add_it = True + for surf in self.surfaces: + if surf == bottom_plane: + add_it = False + break + if add_it: + self.surfaces.append(bottom_plane) + self.region = self.region.intersection(Region(f"+{bottom_plane.id}")) + + def getVertexes(self): + return self.discretize() + + def generated_triangulated_surfaces(self, xy_verts): + from .polygonsHelper import triangulate_polygon_earclip, inner_outer + polys = triangulate_polygon_earclip(xy_verts) + + self.region = Region("") + for poly in polys: + verts = [Vector(v.x, 0, v.y) for v in poly] + arrangeCCW(verts, Vector(0, -1, 0)) # arrange in CCW in xz plane + inner, outer = inner_outer(verts) + + nouter = len(outer) + outer_region = Region("") + for i in range(0, nouter - 1): + v0 = outer[i] + v1 = outer[i + 1] + surface = cone_from_line_segment(v0, v1) + if surface is not None: + top_plane = PlaneSurfaceExporter("", Vector(0, 0, 1), v1.z) + bot_plane = PlaneSurfaceExporter("", Vector(0, 0, 1), v0.z) + self.surfaces += [surface, top_plane, bot_plane] + outer_region = outer_region.union(Region(f"-{surface.id} -{top_plane.id} +{bot_plane.id}")) + + ninner = len(inner) + inner_region = Region("") + for i in range(0, ninner - 1): + v0 = inner[i] + v1 = inner[i + 1] + surface = cone_from_line_segment(v0, v1) + if surface is not None: + top_plane = PlaneSurfaceExporter("", Vector(0, 0, 1), v0.z) + bot_plane = PlaneSurfaceExporter("", Vector(0, 0, 1), v1.z) + self.surfaces += [surface, top_plane, bot_plane] + inner_region = inner_region.union(Region(f"+{surface.id} -{top_plane.id} +{bot_plane.id}")) + + region = outer_region.intersection(inner_region) + self.region = self.region.union(region) + + def generate_surfaces(self): + from .polygonsHelper import is_convex_polygon, inner_outer + + verts = self.getVertexes() + # the revolve and triangulate algorithm assume the last point and first point are NOT the same. + # more generally, we omit edges of near zero length + vpruned = [] + for i in range(len(verts)): + i1 = (i + 1) % len(verts) + if (verts[i1] - verts[i]).Length > 1.0e-07: + vpruned.append(verts[i]) + + arrangeCCW(vpruned, Vector(0, -1, 0)) # arrange in CCW in xz plane + verts = vpruned + xy_verts = [Vector(v.x, v.z, 0) for v in verts] + if not is_convex_polygon(xy_verts): + self.generated_triangulated_surfaces(xy_verts) + return - def export(self): - verts = self.discretize() - exportPolycone(self.name, verts, self.angle) + inner, outer = inner_outer(verts) + outer_region = generate_half_loop_surfaces(outer, self.surfaces) + inner_region = generate_half_loop_surfaces(inner, self.surfaces) + + region = outer_region.intersection(inner_region) + + zs = [v.z for v in verts] + maxz = max(zs) + minz = min(zs) + top_plane = PlaneSurfaceExporter("", Vector(0, 0, 1), maxz) + bot_plane = PlaneSurfaceExporter("", Vector(0, 0, 1), minz) + self.surfaces += [top_plane, bot_plane] + + bounding_planes = Region(f"-{top_plane.id} +{bot_plane.id}") + + self.region = region.intersection(bounding_planes) def discretize(self): deflection = Deviation * radialExtent(self.edgeList) @@ -4679,30 +5241,11 @@ def discretize(self): return edge.discretize(Deflection=deflection) -class RevolvedCircle(RevolvedClosedCurve): - def __init__(self, name, edgelist, angle, axis): - super().__init__(name, edgelist, angle, axis) - z = edgelist[0].Curve.Center.z - self.position = FreeCAD.Vector(0, 0, z) - - def export(self): - edge = self.edgeList[0] - rmax = edge.Curve.Radius - x = edge.Curve.Center.x - y = edge.Curve.Center.y - startphi = math.degrees(math.atan2(y, x)) - rtor = math.sqrt(x * x + y * y) - exportTorus(self.name, rmax, rtor, startphi, self.angle) - - class RevolvedNEdges(RevolvedClosedCurve): - def __init__(self, name, edgelist, angle, axis): - super().__init__(name, edgelist, angle, axis) - - def export(self): - global solids + def __init__(self, name, edgelist): + super().__init__(name, edgelist) - # maxdev = self.deflectionFraction*radialExtent(self.edgeList) + def getVertexes(self): verts = [] for i, e in enumerate(self.edgeList): @@ -4720,21 +5263,16 @@ def export(self): else: curveName = self.name + "_c" + str(i) curveSection = RevolvedClosedCurve( - curveName, [e], self.angle, self.axis - ) + curveName, [e]) verts += curveSection.discretize() break - - xtruName = self.name - exportPolycone(xtruName, verts, self.angle) + return verts # arrange a list of edges in the x-y plane in Counter Clockwise direction # This can be easily generalized for points in ANY plane: if the normal # defining the desired direction of the plane is given, then the z component # below should be changed a dot prduct with the normal - - def arrangeCCW(verts, normal=Vector(0, 0, 1)): reverse = False v0 = verts[0] @@ -4788,125 +5326,84 @@ def radialExtent(edges, axis=Vector(0, 0, 1)): return rmax - rmin -def exportEllipticalTube(name, dx, dy, height): - global solids - - ET.SubElement( - solids, - "eltube", - { - "name": name, - "dx": str(dx), - "dy": str(dy), - "dz": str(height / 2), - "lunit": "mm", - }, - ) - - -def exportTorus(name, rmax, rtor, startphi, angle): - global solids - - ET.SubElement( - solids, - "torus", - { - "name": name, - "rmin": "0", - "rmax": str(rmax), - "rtor": str(rtor), - "startphi": str(startphi), - "deltaphi": str(angle), - "aunit": "deg", - "lunit": "mm", - }, - ) - - -def exportPolycone(name, vlist, angle): - global solids - - # if x > 0 stratphi = 0 - # if x < 0 startphi = 180 - # FreeCAD says can't recompute of both x < 0 and x> are used in a revolve - startphi = 0 - for v in vlist: - if v.x != 0 or v.y != 0: - startphi = math.degrees(math.atan2(v.y, v.x)) - break - - cone = ET.SubElement( - solids, - "genericPolycone", - { - "name": name, - "startphi": str(startphi), - "deltaphi": str(angle), - "aunit": "deg", - "lunit": "mm", - }, - ) - for v in vlist: - r = math.sqrt(v.x * v.x + v.y * v.y) - ET.SubElement(cone, "rzpoint", {"r": str(r), "z": str(v.z)}) - return - - -def getRevolvedCurve(name, edges, angle, axis): +def getRevolvedCurve(name, edges): # Return an RevolvedClosedCurve object of the list of edges if len(edges) == 1: # single edge ==> a closed curve, or curve section - e = edges[0] - if len(e.Vertexes) == 1: # a closed curve - closed = True - else: - closed = False # a section of a curve - - while switch(e.Curve.TypeId): - if case("Part::GeomCircle"): - if closed is True: - print("Circle") - return RevolvedCircle(name, edges, angle, axis) - else: - print("Revolve Arc of Circle") - return RevolvedClosedCurve(name, edges, angle, axis) - # return RevolvedArcSection(name, edges, height) - - else: - print(f"revolve {e.Curve.TypeId}") - return RevolvedClosedCurve(name, edges, angle, axis) + return RevolvedClosedCurve(name, edges) else: # three or more edges - return RevolvedNEdges(name, edges, angle, axis) + return RevolvedNEdges(name, edges) -# scale up a solid that will be subtracted so it punches through parent -def scaleUp(scaledName, originalName, zFactor): - ss = ET.SubElement(solids, "scaledSolid", {"name": scaledName}) - ET.SubElement(ss, "solidref", {"ref": originalName}) - ET.SubElement( - ss, - "scale", - {"name": originalName + "_ss", "x": "1", "y": "1", "z": str(zFactor)}, - ) +def revolve_convex_polygon(verts, eps=1.0e-4): + '''Given the vertexes of a convex polygon (as FreeCAD Vectors), + produce surfaces, the interior of which forms a surface of revolution + of the triangle. + The vertexes of the triangle are to be given as x-z point (in the x-z plane) + and are assumed to be already given in CCW order + There are three possible surfaces: + (1) A horizontal line (z = z0 = constant) -> horizontal plane + (2) A vertical line (x = x0 = constant) -> a cylinder + (2) A slanted line (x0, z0) -> (x1, z1) -> a cone + ''' -def rotatedPos(closedCurve, rot): - # Circles and ellipses (tubes and elliptical tubes) are referenced to origin - # in GDML and have to be translated to their position via a position reference - # when placed as a physical volume. This is done by adding the translation - # to the Part::Extrusion Placement. However, if the placement includes - # a rotation, Geant4 GDML would rotate the Origin-based curve THEN translate. - # This would not work. We have to translate first THEN rotate. This method - # just does the needed rotation of the position vector - # - pos = closedCurve.position - if isinstance(closedCurve, ExtrudedCircle) or isinstance( - closedCurve, ExtrudedEllipse - ): - pos = rot * closedCurve.position + surfaces = [] + expr = "" + for i in range(len(verts)): + i1 = (i + 1) % len(verts) + v0 = verts[i] + v1 = verts[i1] + dx = v1.x - v0.x + dz = v1.z - v0.z + normal = Vector(dz, 0, -dx) + if normal.Length < 1.0e-07: + continue + normal.normalize() + + if abs(dx) < eps: # a cylinder + center = Vector(0, 0, 0) + axis = Vector(0, 0, 1) + radius = abs(v0.x) + cylinder = CylinderSurfaceExporter("", center, axis, radius) + surfaces.append(cylinder) + if normal.x < 0: + expr += f"+{cylinder.id} " + else: + expr += f"-{cylinder.id} " + + elif abs(dz) < eps: # a horizontal plane + D = v0.dot(normal) + surf = PlaneSurfaceExporter("", normal, D) + surfaces.append(surf) + expr += f"-{surf.id} " + + else: # must be a slanted line. a conical surface + theta = math.atan(abs(dx / dz)) + t2 = v0.dot(normal) / normal.z + center = t2 * Vector(0, 0, 1) + axis = Vector(0, 0, 1) + surf = ConeSurfaceExporter("", center, axis, theta) + surfaces.append(surf) + if normal.x > 0: + expr += f"-{surf.id} " + else: + expr += f"+{surf.id} " + + # we want to select half space of the cone that includes the edge + # The normal to the plane is Vector(0, 0, 1) (the z-axis) + # D = coneverex.z + # if v0.z < center.z, we are on the lower half, i.e, on the - sode f the plane + # other was we are on the + side of the plane + selecting_plane = PlaneSurfaceExporter("", axis, center.z) + surfaces.append(selecting_plane) + if v0.z < center.z: + expr += f"-{selecting_plane.id} " + else: + expr += f"+{selecting_plane.id} " - return pos + return surfaces, Region(expr) class RevolutionExporter(SolidExporter): @@ -4915,7 +5412,8 @@ def __init__(self, revolveObj): self.sketchObj = revolveObj.Source self.lastName = self.obj.Label # initial name: might be modified later # generate the positions that get computed during export - self.export(doExport=False) + self.region = None + self.surfaces = [] def name(self): # override default name in SolidExporter @@ -4934,11 +5432,7 @@ def rotation(self): # Things will be screwed up, other wise return self._rotation - def export(self, doExport=True): - # The placement of the revolved item gets calculated here, during export - # but boolean exporter tries to get the position BEFORE the export here happens - # so to generate the position before the export happens, the doExport flag is used - # to run this code to generate the position, WITHOUT actually doing the export + def generate_surfaces(self): # global Deviation revolveObj = self.obj @@ -4956,14 +5450,13 @@ def export(self, doExport=True): # adjustment of Symmetric revolves if revolveObj.Symmetric: - edges = [edge.rotated(Vector(0, 0, 0), Vector(0, 0, 1), -angle/2) + edges = [edge.rotated(Vector(0, 0, 0), Vector(0, 0, 1), -angle / 2) for edge in edges] # adjustment for off-center revolve axis if revolveCenter != Vector(0, 0, 0): edges = [edge.translated(-revolveCenter) for edge in edges] - sortededges = Part.sortEdges(edges) # sort by largest area to smallest area @@ -4976,7 +5469,7 @@ def export(self, doExport=True): # for each set of closed edges curves = [] for i, edges in enumerate(sortededges): - curve = getRevolvedCurve(eName + str(i), edges, angle, axis) + curve = getRevolvedCurve(eName + str(i), edges) if curve is not None: curves.append(curve) if len(curves) == 0: @@ -4993,11 +5486,14 @@ def export(self, doExport=True): lst = root.preOrderTraversal(root) rootnode = lst[0][0] rootCurve = rootnode.closedCurve - if doExport: - rootCurve.export() # a curve is created with a unique name + rootCurve.generate_surfaces() + self.surfaces = rootCurve.surfaces + firstName = rootCurve.name booleanName = firstName + self.region = rootCurve.region + rootPos = rootCurve.position rootRot = ( rootCurve.rotation @@ -5007,81 +5503,33 @@ def export(self, doExport=True): node = c[0] parity = c[1] curve = node.closedCurve - if doExport: - curve.export() + curve.generate_surfaces() + self.surfaces += curve.surfaces if parity == 0: - boolType = "union" - secondName = curve.name - secondPos = curve.position + self.region = self.region.union(curve.region) else: - boolType = "subtraction" - secondName = curve.name - secondPos = curve.position - - booleanName = curve.name + "_bool" - if doExport: - boolSolid = ET.SubElement(solids, boolType, {"name": booleanName}) - ET.SubElement(boolSolid, "first", {"ref": firstName}) - ET.SubElement(boolSolid, "second", {"ref": secondName}) - - relativePosition = secondPos - rootPos - zAngle = curve.rotation[2] - rootRot[2] - posName = curve.name + "_pos" - rotName = curve.name + "_rot" - # position of second relative to first - if doExport: - exportDefine(posName, relativePosition) - ET.SubElement( - define, - "rotation", - { - "name": rotName, - "unit": "deg", - "x": "0", - "y": "0", - "z": str(zAngle), - }, - ) - - ET.SubElement(boolSolid, "positionref", {"ref": posName}) - ET.SubElement(boolSolid, "rotationref", {"ref": rotName}) - firstName = booleanName - - self.lastName = booleanName - # Because the position of each closed curve might not be at the - # origin, whereas primitives (tubes, cones, etc, are created centered at - # the origin, we need to shift the position of the very first node by its - # position, in addition to the shift by the Revolution placement - - revolvePosition = revolveObj.Placement.Base - zoffset = Vector(0, 0, 0) - angles = quaternion2XYZ(revolveObj.Placement.Rotation) - # need to add rotations of elliptical tubes. Assume extrusion is on z-axis - # Probably will not work in general - zAngle = angles[2] + rootRot[2] - print(rootPos) - print(rootCurve.name) - print(rootCurve.position) - rootPos = rotatedPos(rootCurve, revolveObj.Placement.Rotation) - print(rootPos) - Base = revolvePosition + rootPos - zoffset - - # add back the off-center revolve axis - if revolveCenter != Vector(0, 0, 0): - Base += revolveCenter - - rotX = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), angles[0]) - rotY = FreeCAD.Rotation(FreeCAD.Vector(0, 1, 0), angles[1]) - rotZ = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), zAngle) + self.region = self.region.cut(curve.region) + + # add cutoff planes if revolution angle is not 360 deg + if angle != 360: + phi0_normal = Vector(0, 1, 0) + phi0_plane = PlaneSurfaceExporter(self.name() + '_phi0', phi0_normal, 0) + phi1_normal = Vector(math.cos(math.radians(angle + 90)), math.sin(math.radians(angle + 90)), 0) + phi1_plane = PlaneSurfaceExporter(self.name() + '_phi1', phi1_normal, 0) + self.surfaces += [phi0_plane, phi1_plane] + + if phi0_normal.cross(phi1_normal).z > 0: + planes_region = Region(f"+{phi0_plane.id} -{phi1_plane.id}") + else: + planes_region = f"(+{phi0_plane.id} | -{phi1_plane.id})" - rot = rotX * rotY * rotZ + self.region = self.region.intersection(planes_region) - # rotate back to revolve direction - rot = rot_dir_to_z.inverted() * rot + rot_z_to_dir = FreeCAD.Rotation(Vector(0, 0, 1), axis) # rotation from z-axis to original axis of revolution + for surf in self.surfaces: + surf.rotate(rot_z_to_dir) - placement = FreeCAD.Placement(Base, FreeCAD.Rotation(rot)) - self._position = placement.Base - self._rotation = placement.Rotation + self.position_globally() # @@ -5099,10 +5547,12 @@ def __init__(self, name, edgelist, height): self.height = height self.position = Vector(0, 0, 0) self.rotation = [0, 0, 0] # TBD + self.region = None + self.surfaces = [] - def export(self): + def generate_surfaces(self): verts = self.discretize() - exportXtru(self.name, verts, self.height) + self.surfaces, self.region = exportXtru(self.name, verts, self.height) def midPoint(self): edge = self.edgeList[0] @@ -5120,11 +5570,14 @@ def discretize(self): class ExtrudedCircle(ExtrudedClosedCurve): def __init__(self, name, edgelist, height): super().__init__(name, edgelist, height) - self.position = edgelist[0].Curve.Center + Vector(0, 0, height / 2) + self.position = edgelist[0].Curve.Center - def export(self): + def generate_surfaces(self): edge = self.edgeList[0] - exportTube(self.name, edge.Curve.Radius, self.height) + print(f"circle position {self.position}") + surf = exportTube(self.name, self.position, edge.Curve.Radius, self.height) + self.surfaces = [surf] + self.region = Region(f"-{surf.id}") class ExtrudedArcSection(ExtrudedClosedCurve): @@ -5149,139 +5602,14 @@ def area(self): return face.Area - def export(self): - global solids - - edge = self.edgeList[0] - radius = edge.Curve.Radius - # First form a bounding rectangle (polygon) for the arc. - # Arc edges - v1 = edge.Vertexes[0].Point - v2 = edge.Vertexes[1].Point - vmid = self.midPoint() - - # midpoint of chord - vc = (v1 + v2) / 2 - v = v2 - v1 - u = v.normalize() - # extend the ends of the chord so extrusion can cut all of circle, if needed - v1 = vc + radius * u - v2 = vc - radius * u - # component of vmid perpendicular to u - vc_vmid = vmid - vc - n = vc_vmid - u.dot(vc_vmid) * u - n.normalize() - # complete edges of box paerpendicular to chord, toward mid arc point - v3 = v2 + 2 * radius * n - v4 = v1 + 2 * radius * n - - xtruName = self.name + "_xtru" - exportXtru(xtruName, [v1, v2, v3, v4], self.height) - - # tube to be cut1 - tubeName = self.name + "_tube" - exportTube(tubeName, edge.Curve.Radius, self.height) - - # note, it is mandatory that name be that of ClosedCurve - intersect = ET.SubElement(solids, "intersection", {"name": self.name}) - ET.SubElement(intersect, "first", {"ref": xtruName}) - ET.SubElement(intersect, "second", {"ref": tubeName}) - pos = edge.Curve.Center + Vector(0, 0, self.height / 2) - exportPosition(tubeName, intersect, pos) - - -class ExtrudedEllipse(ExtrudedClosedCurve): - def __init__(self, name, edgelist, height): - super().__init__(name, edgelist, height) - curve = edgelist[0].Curve - self.position = curve.Center + Vector(0, 0, height / 2) - angle = math.degrees(curve.AngleXU) - self.rotation = [0, 0, angle] - - def export(self): - edge = self.edgeList[0] - exportEllipticalTube( - self.name, - edge.Curve.MajorRadius, - edge.Curve.MinorRadius, - self.height, - ) - - -class ExtrudedEllipticalSection(ExtrudedClosedCurve): - def __init__(self, name, edgelist, height): - super().__init__(name, edgelist, height) - # Note extrusion polyogn will be in absolute coordinates - # since arc section is relative to that, position is actually (0,0,0) - # same goes for rotation - - """ - def midPoint(self): - edge = self.edgeList[0] - a = edge.Curve.MajorRadius - b = edge.Curve.MinorRadius - angleXU = edge.Curve.AngleXU - thet1 = edge.FirstParameter # in radians, in unorated ellipse - thet2 = edge.LastParameter # in radians, in onrated ellipse - thetmid = (thet1+thet2)/2 + angleXU - - # Major axis angle seems to be off by pi for some ellipse. - # Restrict it to be be between 0 an pi - if angleXU < 0: - angleXU += 180 - - # TODO must deal with case where cutting chord is along major axis - # u_vc_vcenter = vc_vcenter.normalize() - # unit vector from center of circle to center of chord - - # vertexes of triangle formed by chord ends and ellise mid point - # In polar coordinates equation of ellipse is - # r(thet) = a*(1-eps*eps)/(1+eps*cos(thet)) - # if the ellipse is rotated by an angle AngleXU, then - # x = r*cos(thet+angleXU), y = r*sin(thet+angleXU), - # for thet in frame of unrotated ellipse - # now edge.FirstParameter is beginning angle of unrotated ellipse - - def sqr(x): - return x*x - - def r(thet): - return math.sqrt(1.0/(sqr(math.cos(thet)/a) + sqr(math.sin(thet)/b))) - - rmid = r(thetmid) - vmid = Vector(rmid*math.cos(thetmid), rmid*math.sin(thetmid), 0) - - vmid += edge.Curve.Center - - return vmid - """ - - def export(self): + def generate_surfaces(self): global solids edge = self.edgeList[0] - a = dx = edge.Curve.MajorRadius - b = dy = edge.Curve.MinorRadius - - # vertexes of triangle formed by chord ends and ellise mid point - # In polar coordinates equation of ellipse is - # r(thet) = a*(1-eps*eps)/(1+eps*cos(thet)) - # if the ellipse is rotated by an angle AngleXU, then - # x = r*cos(thet+angleXU), y = r*sin(thet+angleXU), - # for thet in frame of unrotated ellipse - # now edge.FirstParameter is beginning angle of unrotated ellipse - # polar equation of ellipse, with r measured from FOCUS. Focus at a*eps - # r = lambda thet: a*(1-eps*eps)/(1+eps*math.cos(thet)) - # polar equation of ellipse, with r measured from center a*eps - - def sqr(x): - return x * x - - def r(thet): - return math.sqrt( - 1.0 / (sqr(math.cos(thet) / a) + sqr(math.sin(thet) / b)) - ) - + radius = edge.Curve.Radius + center = edge.Curve.Center + # First form a bounding rectangle (polygon) for the arc. + # Arc edges v1 = edge.Vertexes[0].Point v2 = edge.Vertexes[1].Point vmid = self.midPoint() @@ -5289,164 +5617,53 @@ def r(thet): # midpoint of chord vc = (v1 + v2) / 2 v = v2 - v1 - u = v.normalize() # unit vector from v1 to v2 - # extend the ends of the chord so extrusion can cut all of ellipse, - # if needed - v1 = vc + 2 * a * u - v2 = vc - 2 * a * u - + u = v.normalize() + # extend the ends of the chord so extrusion can cut all of circle, if needed + v1 = vc + radius * u + v2 = vc - radius * u # component of vmid perpendicular to u vc_vmid = vmid - vc n = vc_vmid - u.dot(vc_vmid) * u n.normalize() - v3 = v2 + 2 * a * n - v4 = v1 + 2 * a * n xtruName = self.name + "_xtru" - exportXtru(xtruName, [v1, v2, v3, v4], self.height) + + chord_plane, chord_region = exportXtru(xtruName, [v1, v2], self.height)[0] + # we want the plane normal to point AWAY from the chord + if chord_plane.normal.dot(vmid) > 0: + chord_plane.normal = -chord_plane.Normal + chord_plane.to_coeffs() + self.surfaces.append(chord_plane) # tube to be cut1 tubeName = self.name + "_tube" - exportEllipticalTube(tubeName, dx, dy, self.height) - - # note, it is mandatory that name be that of ClosedCurve - intersect = ET.SubElement(solids, "intersection", {"name": self.name}) - ET.SubElement(intersect, "first", {"ref": xtruName}) - ET.SubElement(intersect, "second", {"ref": tubeName}) - pos = edge.Curve.Center + Vector(0, 0, self.height / 2) - exportPosition(tubeName, intersect, pos) - rotName = tubeName + "_rot" - # zAngle = math.degrees(edge.Curve.AngleXU) - # Focus1 is on the positive x side, Focus2 on the negative side - dy = edge.Curve.Focus1[1] - edge.Curve.Focus2[1] - dx = edge.Curve.Focus1[0] - edge.Curve.Focus2[0] - zAngle = math.degrees(math.atan2(dy, dx)) - print(f"{self.name} zAngle = {zAngle}") - # if zAngle < 0: - # zAngle += 180 - ET.SubElement( - define, - "rotation", - { - "name": rotName, - "unit": "deg", - "x": "0", - "y": "0", - "z": str(zAngle), - }, - ) - - ET.SubElement(intersect, "rotationref", {"ref": rotName}) - + cylinder = exportTube(tubeName, center, edge.Curve.Radius, self.height) + self.surfaces.append(cylinder) -class ExtrudedBSpline(ExtrudedClosedCurve): - def __init__(self, name, edgelist, height): - super().__init__(name, edgelist, height) + self.region = Region(f"-{cylinder.id} -{chord_plane.id}") -class Extruded2Edges(ExtrudedClosedCurve): +class ExtrudedEllipticalSection(ExtrudedClosedCurve): def __init__(self, name, edgelist, height): super().__init__(name, edgelist, height) + # Note extrusion polyogn will be in absolute coordinates + # since arc section is relative to that, position is actually (0,0,0) + # same goes for rotation - def export(self): + def generate_surfaces(self): global solids - # form normals to the edges. For case of two edges, - # sidedness is irrelevant - v0 = self.edgeList[0].Vertexes[0].Point - v1 = self.edgeList[0].Vertexes[1].Point - e = v1 - v0 - if e.x == 0: - ny = 0 - nx = 1 - elif e.y == 0: - nx = 0 - ny = 1 - else: - nx = 1 - ny = -e.x / e.y - normal = Vector(nx, ny, 0).normalize() - - edgeCurves = [] # list of ExtrudedClosedCurve's - - for i, e in enumerate(self.edgeList): # just TWO edges - while switch(e.Curve.TypeId): - if case("Part::GeomLineSegment"): - break - - if case("Part::GeomLine"): - break - - if case("Part::GeomCircle"): - print("Arc of Circle") - arcXtruName = self.name + "_c" + str(i) - arcSection = ExtrudedArcSection( - arcXtruName, [e], self.height - ) - arcSection.export() - - midpnt = arcSection.midPoint() - inside = pointInsideEdge(midpnt, v0, normal) - edgeCurves.append([arcXtruName, inside]) - break - - if case("Part::GeomEllipse"): - print("Arc of Ellipse") - arcXtruName = self.name + "_e" + str(i) - arcSection = ExtrudedEllipticalSection( - arcXtruName, [e], self.height - ) - arcSection.export() - midpnt = arcSection.midPoint() - inside = pointInsideEdge(midpnt, v0, normal) - edgeCurves.append([arcXtruName, inside]) - break - - else: - print(f"Arc of {e.Curve.TypeId}") - arcXtruName = self.name + "_bs" + str(i) - arcSection = ExtrudedClosedCurve( - arcXtruName, [e], self.height - ) - arcSection.export() - - midpnt = arcSection.midPoint() - inside = pointInsideEdge(midpnt, v0, normal) - edgeCurves.append([arcXtruName, inside]) - break - - if len(edgeCurves) == 1: - # change our name to be that of the constructed curve - # not a violation of the contract of a unique name, - # since the curve name is based on ours - self.position = arcSection.position - self.rotation = arcSection.rotation - self.name = edgeCurves[0][0] - - else: - inside0 = edgeCurves[0][1] - inside1 = edgeCurves[1][1] - sameSide = inside0 == inside1 - if sameSide is False: - booleanSolid = ET.SubElement( - solids, "union", {"name": self.name} - ) - else: - booleanSolid = ET.SubElement( - solids, "subtraction", {"name": self.name} - ) + edge = self.edgeList[0] + deflection = Deviation * edge.BoundBox.DiagonalLength + points = edge.discretize(Deflection=deflection) + points.append(points[0]) + arrangeCCW(points) + self.surfaces, self.region = exportXtru(self.name, points, self.height) - area0 = edgelistArea([self.edgeList[0]]) - area1 = edgelistArea([self.edgeList[1]]) - if area0 > area1: - firstSolid = edgeCurves[0][0] - secondSolid = edgeCurves[1][0] - else: - firstSolid = edgeCurves[1][0] - secondSolid = edgeCurves[0][0] - ET.SubElement(booleanSolid, "first", {"ref": firstSolid}) - ET.SubElement(booleanSolid, "second", {"ref": secondSolid}) +class ExtrudedBSpline(ExtrudedClosedCurve): + def __init__(self, name, edgelist, height): + super().__init__(name, edgelist, height) class ExtrudedNEdges(ExtrudedClosedCurve): @@ -5484,7 +5701,7 @@ def isSubtraction(self, edge): ) if totArea < 0.999 * ( - edgeArea + withoutArea + edgeArea + withoutArea ): # 0.99 safety margin for totArea = edgeArea+withoutArea if totArea > edgeArea: return True @@ -5494,9 +5711,7 @@ def isSubtraction(self, edge): else: return False - def export(self): - global solids - + def generate_surfaces(self): verts = [] for i, e in enumerate(self.edgeList): @@ -5521,7 +5736,7 @@ def export(self): # verts.append(verts[0]) xtruName = self.name - exportXtru(xtruName, verts, self.height) + self.surfaces, self.region = exportXtru(xtruName, verts, self.height) # Node of a tree that represents the topology of the sketch being exported @@ -5582,9 +5797,9 @@ def discretizeMinusOne(edgeList, iSkip): for i in range(len(edgeList)): edge = edgeList[i] if ( - i == iSkip - or edge.Curve.TypeId == "Part::GeomLine" - or edge.Curve.TypeId == "Part::GeomLineSegment" + i == iSkip + or edge.Curve.TypeId == "Part::GeomLine" + or edge.Curve.TypeId == "Part::GeomLineSegment" ): verts.append(edge.Vertexes[0].Point) verts.append(edge.Vertexes[1].Point) @@ -5593,72 +5808,73 @@ def discretizeMinusOne(edgeList, iSkip): return verts -def exportTube(name, radius, height): +def exportTube(name, center, radius, height): global solids - ET.SubElement( - solids, - "tube", - { - "name": name, - "rmax": str(radius), - "z": str(height), - "startphi": "0", - "deltaphi": "360", - "aunit": "deg", - "lunit": "mm", - }, - ) + axis = Vector(0, 0, 1) + outer_surface = CylinderSurfaceExporter(name, center, axis, radius) + return outer_surface def exportXtru(name, vlist, height): - global solids - - # We are assuming that the points to that form the + from .polygonsHelper import is_convex_polygon, triangulate_polygon_earclip + + def convex_poly_planes(points): + planes = [] + for i in range(len(points)): + i1 = (i + 1) % len(points) + edge = points[i1] - points[i] + normal = Vector(edge.y, -edge.x, 0) + normal.normalize() + + D = normal.dot(points[i]) + planes.append(PlaneSurfaceExporter(name, normal, D)) + return planes + + def convex_poly_region(planes): + expr = "" + for surf in planes: + expr += f"-{surf.id} " + return Region(expr) + + # We are assuming that the points that form # the edges to be extruded are al coplanar and in the x-y plane # with a possible zoffset, which is taken as the common # z-coordinate of the first vertex if len(vlist) < 3: - return - zoffset = vlist[0].z - xtru = ET.SubElement(solids, "xtru", {"name": name, "lunit": "mm"}) + return [], Region("") # prune verts: Closed curves get stitched from adjacent edges - # sometimes the beginning vertex f the next edge is the same as the end vertex + # sometimes the beginning vertex of the next edge is the same as the end vertex # of the previous edge. Geant gives a warning about that. To remove the warning # we will remove vertices closer to each other than 0.1 nm - 1e-7 mm. If someone is trying - # to model objects with smaller dimensions, then they should take a lesson in Quantum Mechanics + # to model objects with smaller dimensions, then they should take a lesson in Quantum Mechanic2s # Because only adjacent edges could be the same, we just compare each edge to the preceding edge vpruned = vlist[:] + arrangeCCW(vpruned) for i in range(len(vlist)): - i1 = (i+1) % len(vlist) + i1 = (i + 1) % len(vlist) if ((vlist[i1] - vlist[i]).Length < 1e-7): vpruned.remove(vlist[i1]) - for v in vpruned: - ET.SubElement(xtru, "twoDimVertex", {"x": str(v.x), "y": str(v.y)}) - ET.SubElement( - xtru, - "section", - { - "zOrder": "0", - "zPosition": str(zoffset), - "xOffset": "0", - "yOffset": "0", - "scalingFactor": "1", - }, - ) - ET.SubElement( - xtru, - "section", - { - "zOrder": "1", - "zPosition": str(height + zoffset), - "xOffset": "0", - "yOffset": "0", - "scalingFactor": "1", - }, - ) + planes = [] + + if is_convex_polygon(vpruned): + planes = convex_poly_planes(vpruned) + region = convex_poly_region(planes) + + else: + region = Region("") + polygon_list = triangulate_polygon_earclip(vpruned) + for polygon in polygon_list: + convex_planes = convex_poly_planes(polygon) + convex_region = convex_poly_region(convex_planes) + + # TODO: remove common planes with opposite normals + planes += convex_planes + region = region.union(convex_region) + + return planes, region def getExtrudedCurve(name, edges, height): @@ -5688,14 +5904,6 @@ def getExtrudedCurve(name, edges, height): print("Arc of Circle") return ExtrudedArcSection(name, edges, height) - if case("Part::GeomEllipse"): - if closed is True: - print("Ellipse") - return ExtrudedEllipse(name, edges, height) - else: - print("Arc of Ellipse") - return ExtrudedEllipticalSection(name, edges, height) - else: print(" B spline") return ExtrudedClosedCurve(name, edges, height) @@ -5719,7 +5927,6 @@ def __init__(self, extrudeObj): self.lastName = self.obj.Label # initial name: might be modified later Deviation = self.obj.ViewObject.Deviation / 100.0 # generate the positions that get computed during export - self.export(doExport=False) def position(self): # This presumes export has been called before position() @@ -5738,7 +5945,7 @@ def name(self): prefix = "X" return prefix + self.lastName - def export(self, doExport=True): + def generate_surfaces(self): # The placement of the extruded item gets calculated here, during export # but boolean exporter tries to get the position BEFORE the export here happens # so to generate the position before the export happens, the doExport flag is used @@ -5749,11 +5956,16 @@ def export(self, doExport=True): extrudeObj: Part.Feature = self.obj eName = self.name() - extrudeDirection = extrudeObj.Dir + extrudeDirection = extrudeObj.Dir # rotation to take extrude direction to z -axis - rot_dir_to_z = FreeCAD.Rotation(extrudeDirection, Vector(0, 0, 1)) - edges = [edge.rotated(Vector(0, 0, 0), rot_dir_to_z.Axis, math.degrees(rot_dir_to_z.Angle)) for edge in sketchObj.Shape.Edges] + savedSketchPlacement = self.sketchObj.Placement + self.sketchObj.Placement *= savedSketchPlacement.inverse() + # do we need a recompute after this? + + # rot_dir_to_z = FreeCAD.Rotation(extrudeDirection, Vector(0, 0, 1)) + # edges = [edge.rotated(Vector(0, 0, 0), rot_dir_to_z.Axis, math.degrees(rot_dir_to_z.Angle)) for edge in sketchObj.Shape.Edges] + edges = sketchObj.Shape.Edges sortededges = Part.sortEdges(edges) # sort by largest area to smallest area sortEdgelistsByFaceArea(sortededges) @@ -5762,6 +5974,7 @@ def export(self, doExport=True): # Make names based on Extrude name # curves = [getCurve(edges, extrudeObj.Label + str(i)) for i, edges # in enumerate(sortededges)] + if extrudeObj.Symmetric is True: height = extrudeObj.LengthFwd.Value else: @@ -5782,8 +5995,17 @@ def export(self, doExport=True): lst = root.preOrderTraversal(root) rootnode = lst[0][0] rootCurve = rootnode.closedCurve - if doExport: - rootCurve.export() # a curve is created with a unique name + rootCurve.generate_surfaces() # a curve is created with a unique name + + # Our construction assumes extrusion is along z-axis + # and that the sketch object does not have any placement AND + # that the edges in a acurve are positioned relative to sketc, + # It turns out that is WRONG. The edges already fold in the sketch's + # placement. So we undo the effect of the sketched non-identity + # placement and then apply it back again at the end + + self.region = rootCurve.region + firstName = rootCurve.name booleanName = firstName @@ -5792,129 +6014,705 @@ def export(self, doExport=True): rootCurve.rotation ) # for now consider only angle of rotation about z-axis + self.surfaces = rootCurve.surfaces + for c in lst[1:]: node = c[0] parity = c[1] curve = node.closedCurve - if doExport: - curve.export() + curve.generate_surfaces() + self.surfaces += curve.surfaces if parity == 0: - boolType = "union" - secondName = curve.name - secondPos = curve.position + self.region = self.region.union(curve.region) else: - boolType = "subtraction" - secondName = ( - curve.name + "_s" - ) # scale solids along z, so it punches thru - if doExport: - scaleUp(secondName, curve.name, 1.10) - secondPos = curve.position - Vector(0, 0, 0.01 * height) - - booleanName = curve.name + "_bool" - if doExport: - boolSolid = ET.SubElement(solids, boolType, {"name": booleanName}) - ET.SubElement(boolSolid, "first", {"ref": firstName}) - ET.SubElement(boolSolid, "second", {"ref": secondName}) - relativePosition = secondPos - rootPos - zAngle = curve.rotation[2] - rootRot[2] - posName = curve.name + "_pos" - rotName = curve.name + "_rot" - if doExport: - exportDefine( - posName, relativePosition - ) # position of second relative to first - ET.SubElement( - define, - "rotation", - { - "name": rotName, - "unit": "deg", - "x": "0", - "y": "0", - "z": str(zAngle), - }, - ) + self.region = self.region.cut(curve.region) - ET.SubElement(boolSolid, "positionref", {"ref": posName}) - ET.SubElement(boolSolid, "rotationref", {"ref": rotName}) - firstName = booleanName - - self.lastName = ( - booleanName # our name should the name f the last solid created - ) - - # Because the position of each closed curve might not be at the - # origin, whereas primitives (tubes, cones, etc, are created centered - # at the origin, we need to shift the position of the very first node - # by its position, in addition to the shift by the Extrusion placement - extrudeObj = self.obj - extrudePosition = extrudeObj.Placement.Base if extrudeObj.Symmetric is False: if extrudeObj.Reversed is False: - zoffset = Vector(0, 0, extrudeObj.LengthRev.Value) + zoffset = extrudeObj.LengthRev.Value else: - zoffset = Vector(0, 0, extrudeObj.LengthFwd.Value) + zoffset = extrudeObj.LengthFwd.Value else: - zoffset = Vector(0, 0, extrudeObj.LengthFwd.Value / 2) + zoffset = extrudeObj.LengthFwd.Value / 2 - angles = quaternion2XYZ(extrudeObj.Placement.Rotation) - # need to add rotations of elliptical tubes. - # Assume extrusion is on z-axis - # Probably will not work in general - zAngle = angles[2] + rootRot[2] - rootPos = rotatedPos(rootCurve, extrudeObj.Placement.Rotation) - print(rootPos) - Base = extrudePosition + rootPos - zoffset + axis = Vector(0, 0, 1) + top_surface = PlaneSurfaceExporter(self.obj.Label + '_top', axis, zoffset + height) + bottom_surface = PlaneSurfaceExporter(self.obj.Label + '_bot', axis, zoffset) + self.surfaces += [bottom_surface, top_surface] + self.region = Region(self.region.expr + f" -{top_surface.id} +{bottom_surface.id}") - rotX = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), angles[0]) - rotY = FreeCAD.Rotation(FreeCAD.Vector(0, 1, 0), angles[1]) - rotZ = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), zAngle) + # Restore the sketch Placement + # undo the sketch placement + self.sketchObj.Placement = savedSketchPlacement + print(f"Extrude global placement = {self.obj.getGlobalPlacement()}") + print(f"Sketch local placement = {self.sketchObj.Placement}") + globalPlacement = self.obj.getGlobalPlacement() * self.sketchObj.Placement + print(f"sketch elements global placement is {globalPlacement}") - rot = rotZ * rotY * rotX + T = globalPlacement.Base + R = globalPlacement.Rotation - # rotate back to extrude direction - rot = rot_dir_to_z.inverted() * rot + for surf in self.surfaces: + surf.rotate(R) + surf.translate(T) - placement = FreeCAD.Placement(Base, FreeCAD.Rotation(rot)) - self._position = placement.Base - self._rotation = placement.Rotation +# --------------------Auto tessellation classes and routines ----------------------------------------------------------- -class GDMLMeshExporter(GDMLSolidExporter): - # FreeCAD Mesh only supports triangular Facets - def __init__(self, obj): - super().__init__(obj, 'tessellated') +class Plane: + def __init__(self, normal, D): + self.normal = normal + self.D = D - def export(self): - if self.exported(): - return - super().export() - - tessName = self.name().replace('\r','').replace('(','_').replace(')','_') - # Use more readable version - tessVname = tessName + "_" - tess = ET.SubElement(solids, "tessellated", {"name": tessName}) - placementCorrection = self.obj.Placement.inverse() - for i, v in enumerate(self.obj.Mesh.Points): - v = FreeCAD.Vector(v.x, v.y, v.z) - exportDefineVertex(tessVname, placementCorrection * v, i) - for f in self.obj.Mesh.Facets: - indices = f.PointIndices - i0 = indices[0] - i1 = indices[1] - i2 = indices[2] - ET.SubElement( - tess, - "triangular", - { - "vertex1": tessVname + str(i0), - "vertex2": tessVname + str(i1), - "vertex3": tessVname + str(i2), - "type": "ABSOLUTE", - }, - ) - self._exportScaled() + def inside(self, point): + return self.normal.dot(point) < self.D + + def __hash__(self): + s = [float(f" {f:.4e}") for f in self.normal] + s.append(float(f" {self.D:.4e}")) + return hash(tuple(s)) + + def __eq__(self, other): + if not (type(self) is type(other)): + return False + + return hash(self) == hash(other) + + +class RegionSet: + def __init__(self, v2planes_dict, mesh): + ''' v2planes_dict is a dictionary of a mesh point index and all the Planes that pass through that point + and a Plane that contains it as value ''' + self.v2p = v2planes_dict + self.mesh = mesh + + def is_edge_inside(self, edge, plane): + ''' test if the edge (a pair of point indexes is inside this RegionSet + To be inside both points must be in the set of points AND there must be points + in the set on BOTH sides of the given plane + ''' + + mypoints = self.v2p.keys() + + count = 0 + for vid in edge: + if vid in mypoints: + count += 1 + if count >= 2: + break + + if count < 2: + return False + + nplus = 0 # number of points on the + side of the plane + nminus = 0 # number of points on the nedgative side of the plane + + eps = 1.0e-9 + for vid in mypoints: + point = self.mesh.Points[vid].Vector + dist = plane.normal.dot(point) - plane.D + if dist < -eps: + nminus += 1 + elif dist > eps: + nplus += 1 + if nplus > 0 and nminus > 0: + return True + + return False + + def split_by_plane(self, plane): + ''' split this region set into two region sets, one where all the points are on the inside of + the plane and one on the outside''' + + region1 = defaultdict(list) + region2 = defaultdict(list) + + eps = 1.0e-9 + onplane = [] # list of points on the plane (gain, there are point indexes, not points) + for vid in list(self.v2p.keys()): + vec = self.mesh.Points[vid].Vector + dist = plane.normal.dot(vec) - plane.D + if dist < -eps: + region1[vid] = self.v2p[vid][:] + elif dist > eps: + region2[vid] = self.v2p[vid][:] # otherwise, put the planes in region 2 + else: + onplane.append(vid) + + # add the splitting plane to the edge points and remove the "wrong" face from the edge vertexes + plane1 = plane # just a change of notation for clarity + plane2 = Plane(-plane.normal, -plane.D) # flip the cutting plane for region2 + for vid in onplane: + region1[vid] = self.v2p[vid][:] + region2[vid] = self.v2p[vid][:] + for pl in region1[vid][:]: # copy so we don't screw up the list while we're iterating over it + if pl.normal.dot(plane1.normal) < 0: + region1[vid].remove(pl) # remove the wrong plane + region1[vid].append(plane1) # and add the cutting plane + + for pl in region2[vid][:]: # copy so we don't screw up the list while we're iterating over it + if pl.normal.dot(plane2.normal) < 0: + region2[vid].remove(pl) # remove the wrong plane + region2[vid].append(plane2) # and add the cutting plane + + R1 = RegionSet(region1, self.mesh) + R2 = RegionSet(region2, self.mesh) + + return R1, R2 + + +class ShapeMesher: + deflection = 0.5 + angularDeflection = math.radians(28.5) + + def __init__(self, shape): + import MeshPart + + self.shape = shape + self.mesh = MeshPart.meshFromShape(Shape=shape, LinearDeflection=ShapeMesher.deflection, + AngularDeflection=ShapeMesher.angularDeflection, Relative=False) + + self.mesh.harmonizeNormals() + # Debug + + self.edge_dict = self.edge_face_dict() + self.parallel_face_dict = self.parallel_set() + + print(f"Num faces = {len(self.mesh.Facets)}") + print(f"Num edges = {len(self.edge_dict)}") + + def _edge_dir(self, edge): + """Return a unit vector along the geometric edge defined by mesh point indices.""" + idx = list(edge) + v0 = self.mesh.Points[idx[0]].Vector + v1 = self.mesh.Points[idx[1]].Vector + v = v1 - v0 + if v.Length == 0: + return v + v.normalize() + return v + + def plane(self, facet): + normal = facet.Normal + vid = facet.PointIndices[0] + p0 = self.mesh.Points[vid].Vector + D = normal.dot(p0) + return Plane(normal, D) + + def vertex_to_planes_map(self): + v2planes = {} + for fi, facet in enumerate(self.mesh.Facets): + for vid in facet.PointIndices: + plane = self.plane(facet) + v2planes[vid] = plane + return v2planes + + def parallel_set(self): + ''' return a dictionary of face as key and set of faces that are parallel with that face''' + par_face_dict = {} + for face1 in self.mesh.Facets: + s = set() + normal1 = face1.Normal + for face2 in self.mesh.Facets: + if face1.Index == face2.Index: + continue + normal2 = face2.Normal + if normal2.dot(normal1) > 0.999: + s.add(face2.Index) + par_face_dict[face1.Index] = s + + return par_face_dict + + def edge_face_dict(self): + d = {} + for face in self.mesh.Facets: + pointIndexes = face.PointIndices + n = len(pointIndexes) + for i in range(n): + i1 = (i + 1) % n + + edge = (pointIndexes[i], pointIndexes[i1]) + fz = frozenset(edge) + if fz not in d: + d[fz] = [face.Index] + else: + d[fz].append(face.Index) + return d + + def is_mesh_concave(self): + edge, _ = self.get_concave_edge() + return edge is not None + + def my_concave_edge_test(self, edge): + faceIndexes = self.edge_dict[edge] + f1 = self.mesh.Facets[faceIndexes[0]] + f2 = self.mesh.Facets[faceIndexes[1]] + c1 = (Vector(f1.Points[0]) + Vector(f1.Points[1]) + Vector(f1.Points[2])) / 3 + c2 = (Vector(f2.Points[0]) + Vector(f2.Points[1]) + Vector(f2.Points[2])) / 3 + eps = (c2 - c1).Length / 10 + p1 = c1 - eps * f1.Normal + p2 = c2 - eps * f2.Normal + + if abs(f1.Normal.dot(f2.Normal)) > 0.999: + return False + + if (p2 - p1).Length > (c2 - c1).Length: + self.display_edge(edge) + return True + + return False + + def get_concave_edge(self): + """ + Return (edge, nmean, max_concave_angle) where: + - edge: a concave edge chosen among all concave candidates + - nmean: n1 + n2 for that edge (for your plane construction) + - max_concave_angle: maximum dihedral angle (in radians) among + all edges that are classified as concave + + If no concave edges are found, returns (None, None, 0.0). + """ + # Your “interesting concavity” threshold per *edge* + MIN_DIHEDRAL_ANGLE = math.radians(5.0) # tune as you like + MIN_SIN = math.sin(MIN_DIHEDRAL_ANGLE) + + # How close to -1 we require (n1 × n2) · ê to be + EPS_SIGN = 1e-3 # tolerance; adjust if needed + + best_edge = None + best_nmean = None + + best_sin_concave = 0.0 # sin(dihedral) for the **most concave** edge (among concave ones) + chosen_debug = None + + for edge, faceIndexes in self.edge_dict.items(): + # Only internal edges with exactly two adjacent facets + if len(faceIndexes) != 2: + continue + + f1 = self.mesh.Facets[faceIndexes[0]] + f2 = self.mesh.Facets[faceIndexes[1]] + + v_edge = self._edge_dir(edge) + if v_edge.Length == 0: + continue + + n1 = f1.Normal + n2 = f2.Normal + + # Cross product gives a vector along the intersection line + n1xn2 = n1.cross(n2) + sinthet = n1xn2.Length # ~ sin(dihedral angle between n1 and n2) + + # 1) Filter out almost-parallel normals: flat or nearly flat adjacency + if sinthet < MIN_SIN: + continue + + # 2) Concavity sign test using your sign convention + n1xn2.normalize() + dot_ce = n1xn2.dot(v_edge) + + # We consider the edge concave only if the cross product + # is pointing reliably in the opposite direction of the edge + if self.my_concave_edge_test(edge): + # print("my_concave_edge_test says this edge is convex. The get concave_edge says it is concave") + if sinthet > best_sin_concave: + best_sin_concave = sinthet + best_edge = edge + best_nmean = n1 + n2 + + angle = math.asin(max(min(sinthet, 1.0), -1.0)) + + if best_edge is None: + # No concave edges found at all + return None, None, 0.0 + + # Convert the maximum concave sin(angle) to an angle in radians + max_concave_angle = math.asin( + max(min(best_sin_concave, 1.0), -1.0) + ) + + return best_edge, best_nmean, max_concave_angle + + def share_edge(self, iFace1, iFace2): + ''' return true if faces with indexes iFace1, iFace2 share an edge''' + + edges_set = set() + face1 = self.mesh.Facets[iFace1] + face2 = self.mesh.Facets[iFace2] + for f in [face1, face2]: + pointIndexes = f.PointIndices + n = len(f.PointIndices) + for i in range(n): + i1 = (i + 1) % n + edge = (pointIndexes[i], pointIndexes[i1]) + fz = frozenset(edge) + edges_set.add(fz) + + return len(edges_set) < 6 # if the faces share an edge, then there should be less than 6 edges in the set + + def get_face_edges(self, iFace): + ''' return the three edges as a frozenset of the end point indexes of the endpoint''' + face = self.mesh.Facets[iFace] + pointIndexes = face.PointIndices + n = len(pointIndexes) + edges = [] + for i in range(n): + i1 = (i + 1) % n + edge = (pointIndexes[i], pointIndexes[i1]) + fz = frozenset(edge) + edges.append(fz) + + return edges + + def concave_face_chains(self): + ''' return a list of a list of faces that share an edge and that are concave at the shared edge''' + concave_edges = self.get_concave_edges() + + def concave_edge_chain(edge, chain_set): + if edge in concave_edges: + chain_set.add(edge) + concave_edges.remove(edge) + f1 = self.edge_dict[edge][0] + f2 = self.edge_dict[edge][1] + for f in [f1, f2]: + edges = self.get_face_edges(f) + for e in edges: + concave_edge_chain(e, chain_set) + else: + return + + concave_chains = [] + while len(concave_edges) > 0: + edge_set = set() + edge = concave_edges[0] + concave_edge_chain(edge, edge_set) + + face_set = set() + for e in edge_set: + f1 = self.edge_dict[e][0] + f2 = self.edge_dict[e][1] + face_set.add(f1) + face_set.add(f2) + # add all edges that are parallel to the two faces to the same chain + for f1par in self.parallel_face_dict[f1]: + face_set.add(f1par) + for f2par in self.parallel_face_dict[f2]: + face_set.add(f2par) + + concave_chains.append(face_set) + + return concave_chains + + def get_concave_edges(self): + """ + return list of concave edges + """ + chosen_debug = None + + concaveList = [] + for edge, faceIndexes in self.edge_dict.items(): + # Only internal edges with exactly two adjacent facets + if len(faceIndexes) != 2: + continue + + f1 = self.mesh.Facets[faceIndexes[0]] + f2 = self.mesh.Facets[faceIndexes[1]] + + v_edge = self._edge_dir(edge) + if v_edge.Length == 0: + continue + + if self.my_concave_edge_test(edge): + concaveList.append(edge) + + return concaveList + + def is_valid(self): + ''' + Validate mesh: + (1) Volume must be > deflection^3 + (2) All faces must have area > deflection^2 + (3) All edges must have length > deflection + (4) all normals must have length > 0 + (5) number of faces must greater than 4 + ''' + nfaces = self.mesh.CountFacets + if nfaces <= 4: + print(f"mesh has 4 or fewer faces = {nfaces}") + return 5 + + if self.mesh.Volume < ShapeMesher.deflection ** 3: + print(f"mesh volume {self.mesh.Volume} less than deflectin^3 {ShapeMesher.deflection ** 3}") + return 1 + + for i, face in enumerate(self.mesh.Facets): + if face.Normal.Length < 0.5: + print(f"face {i}'s normal length is less than 1: {face.Normal.Length}") + return 4 + if face.Area < ShapeMesher.deflection * ShapeMesher.deflection: + print(f"face area smaller than deflection^2: {face.Area}") + return 2 + + n = len(face.Points) + for j in range(n): + v0 = Vector(face.Points[j][0], face.Points[j][1], face.Points[j][2]) + j1 = (j + 1) % n + v1 = Vector(face.Points[j1][0], face.Points[j1][1], face.Points[j1][2]) + edge_len = (v1 - v0).Length + if edge_len < ShapeMesher.deflection: + print( + f"Warning edge {j} of face {i} is shorter than deflection: edge length = {edge_len} deflection = {ShapeMesher.deflection}") + continue + + return 0 + + def display_face(self, iFace, color): + facet = self.mesh.Facets[iFace] + pnts = facet.Points + vecs = [Vector(pnt) for pnt in pnts] + vecs.append(vecs[0]) + wire = Part.makePolygon(vecs) + face = Part.Face(wire) + obj = Part.show(face) + obj.ViewObject.ShapeColor = color + + normal = facet.Normal + c = (vecs[0] + vecs[1] + vecs[2]) / 3 + p1 = c - 0.5 * normal + p2 = c + 2.0 * normal + L = Part.LineSegment(p1, p2) + Part.show(L.toShape(), "normal") + + def display_edge(self, edge): + pts = list(edge) + idx1 = pts[0] + idx2 = pts[1] + v1 = Vector(self.mesh.Points[idx1].x, self.mesh.Points[idx1].y, self.mesh.Points[idx1].z) + v2 = Vector(self.mesh.Points[idx2].x, self.mesh.Points[idx2].y, self.mesh.Points[idx2].z) + L = Part.LineSegment(v1, v2) + shape = L.toShape() + # shape.ViewObject.LineColor = (255, 255, 255) + Part.show(shape, "edge") + + def edge_plane(self, edge): + ''' Return a plane that passes through the edge. The plane is midway between the two + faces that border thed edge + ''' + faceIndexes = self.edge_dict[edge] + f1 = self.mesh.Facets[faceIndexes[0]] + f2 = self.mesh.Facets[faceIndexes[1]] + v_edge = self._edge_dir(edge) + n1 = f1.Normal + n2 = f2.Normal + nmean = n1 + n2 # only direction matters, so we don't need to normalize + + plane_normal = v_edge.cross(nmean) # note that the sense of the plane_normal could be eiher way + plane_normal.normalize() + p0 = self.mesh.Points[list(edge)[0]].Vector # one of the points on the edge + D = plane_normal.dot(p0) + + return Plane(plane_normal, D) + + def split_along_concaves(self): + ''' Returns a list of RegionSets that are presumably all convex + It does this by splitting an initial RegionSet containing all the mesh points (point indexes, actually) + into two halves by a splitting plane that passes through a concaved edge. Do this until all the + concaved edges have been consumed + ''' + + concave_edges = self.get_concave_edges() + + # first form a RegionSet of all the points and planes of faces + v2p = defaultdict(list) + for fi, facet in enumerate(self.mesh.Facets): + for vid in facet.PointIndices: + plane = self.plane(facet) + v2p[vid].append(plane) + + r0 = RegionSet(v2p, self.mesh) + regions = [r0] + while len(concave_edges) > 0: + edge = concave_edges[0] + for region in regions: + plane = self.edge_plane(edge) # create a splitting plane along that edge + if region.is_edge_inside(edge, plane): # find which region edge is inside + region1, region2 = region.split_by_plane(plane) # get the two split regions + if region1 is None or region2 is None: + break # no split happened. Cutting by a degenerate plane, ie. a plane that has been used before + regions.remove(region) # remove the region that has been just split + regions.append(region1) # and replace with the two split regions + regions.append(region2) + break + concave_edges.remove(edge) # remove the splitting edge from the list of concave edges + + return regions + + +def compound_to_solids(shape): + """Convert a Compound (or other) to a list of Solids if possible.""" + if shape.ShapeType == "Solid": + return [shape] + sols = [] + # If we got a Compound of shells / faces, try to make solids from its shells + for sh in shape.Shells: + try: + sld = Part.makeSolid(sh) + except Exception: + continue + if sld.ShapeType == "Solid" and sld.isValid(): + sols.append(sld) + # If there are already Solids in the compound, include them + for s in shape.Solids: + if s.ShapeType == "Solid" and s.isValid(): + sols.append(s) + return sols + + +def common_via_bopfeatures(shapeA, shapeB, base_name="CommonTmp"): + """Compute intersection of two Part.Shapes using the same BOPFeatures + path that the GUI 'Common' command uses. Returns the resulting Shape.""" + doc = FreeCAD.ActiveDocument + + # 1) Create temporary Part::Feature objects + objA = doc.addObject("Part::Feature", base_name + "_A") + objA.Shape = shapeA + objB = doc.addObject("Part::Feature", base_name + "_B") + objB.Shape = shapeB + doc.recompute() + + # 2) Run BOPFeatures.make_multi_common on their names + bp = BOPFeatures.BOPFeatures(doc) + common_obj = bp.make_multi_common([objA.Name, objB.Name]) + doc.recompute() + + # 3) Extract the resulting Shape + common_shape = common_obj.Shape + + # (Optional) clean up the temp inputs, keep only result if you like + # doc.removeObject(objA.Name) + # doc.removeObject(objB.Name) + + return common_shape, common_obj + + +MAX_DEPTH = 50 + + +def decompose_recursive(shapeMesher, results, depth=0): + if depth >= MAX_DEPTH: + results.append(shapeMesher) + return + + mesh = shapeMesher.mesh + concave_edge, nmean, max_angle = shapeMesher.get_concave_edge() + MIN_GLOBAL_ANGLE = math.radians(5.0) + + if concave_edge is None or max_angle < MIN_GLOBAL_ANGLE: + results.append(shapeMesher) + return + + edge_pts = [mesh.Points[pntIndex].Vector for pntIndex in list(concave_edge)] + p0, p1 = edge_pts + edgeVec = p1 - p0 + # DEBUG + # edge_line = Part.makeLine(p0, p1) + # Part.show(edge_line, "edge_line") + + normal = edgeVec.cross(nmean) + if normal.Length == 0: + results.append(shapeMesher) + return + normal.normalize() + + base = shapeMesher.shape + bbox = base.BoundBox + planeWidth = 2 * bbox.DiagonalLength + + # (if you still want to re-center by COM, keep your translation here) + cuttingPlane = Part.makePlane(planeWidth, planeWidth, p0, normal) + # translate plane so center of mass of plane is at edge point + planeCM = cuttingPlane.CenterOfMass + cuttingPlane.Placement.Base -= planeCM - p0 + + if not cuttingPlane.isValid(): + results.append(shapeMesher) + return + + # Make solid from shell + solid = Part.makeSolid(base) + if solid.ShapeType != "Solid" or not solid.isValid(): + results.append(shapeMesher) + return + + # Build two slabs by extruding plane + n = cuttingPlane.normalAt(0.5, 0.5) + if n.Length == 0: + results.append(shapeMesher) + return + n.normalize() + h = solid.BoundBox.DiagonalLength * 2.0 + + upper_extrude = cuttingPlane.extrude(n * h) + lower_extrude = cuttingPlane.extrude(-n * h) + + # Use BOPFeatures (same as GUI) to get the intersections + upper, upper_obj = common_via_bopfeatures(solid, upper_extrude, "UpperCut") + lower, lower_obj = common_via_bopfeatures(solid, lower_extrude, "LowerCut") + + print("upper:", upper.ShapeType, "valid:", upper.isValid(), "vol:", upper.Volume) + print("lower:", lower.ShapeType, "valid:", lower.isValid(), "vol:", lower.Volume) + + child_shapes = [] + + # upper can be a Solid or a Compound; extract solids if needed + if upper.ShapeType == "Solid" and upper.isValid(): + child_shapes.append(upper) + elif upper.Solids: + for s in upper.Solids: + if s.isValid(): + child_shapes.append(s) + + if lower.ShapeType == "Solid" and lower.isValid(): + child_shapes.append(lower) + elif lower.Solids: + for s in lower.Solids: + if s.isValid(): + child_shapes.append(s) + + # If nothing usable came out, stop recursing on this shape + if not child_shapes: + results.append(shapeMesher) + return + + for s in child_shapes: + print(f"child: type={s.ShapeType}, valid={s.isValid()}, vol={s.Volume}") + if not s.isValid(): + continue + + mesher = ShapeMesher(s) + + code = mesher.is_valid() + + if code == 1: # volume too small + continue + elif code == 5: # too few faces + results.append(mesher) + continue + elif code == 4: # normal too small. + results.append(mesher) + continue + + # if s.Volume < ShapeMesher.deflection**3: + # results.append(ShapeMesher(s)) + # print("Skipped decomposing this shape. Volume too small") + # continue + # should check if the cutting resulted in more than one component + components = mesher.mesh.getSeparateComponents() + if len(components) > 1: + print("** cut mesh has more than one component **") + + decompose_recursive(mesher, results, depth + 1) class AutoTessellateExporter(SolidExporter): @@ -5922,134 +6720,254 @@ class AutoTessellateExporter(SolidExporter): def __init__(self, obj): super().__init__(obj) + self.index = 0 + + # So my understanding of the structure of the mesh, so far, is the following + # if E is the number of Edges, F the number of faces and V the number of Volumes + # then Edge_ids go from 1 - E + # Face ids go form E+1 - E+1+F + # and volume ids go from E+F+1+1 - E+F+1+1+V + # so mesh.getElementNode(id) will get the edge node ids of id is in the Edge ids range + # will get the face id nodes for ids in the Face ids range + # and will get the volume id nodes for id in the Volume id range. + # For edges there will be two node ids, corresponding to the ends of the edge + # For faces, there will be three ids, corresponding to the triangle coordinates + # and for Volume ids, there will be four node ids, corresponding to the four corners of + # of the tetrahedron + # mesh.Node[id] gives the Vector coordinates of the node, that is the point + # P.S: Face ids are those of the boundary faces only, not of every tetrahedron surface + + # get a list of planes, one for each face. At present the sense of + # the normal is arbitrary. When we build the regions for each tetrahedron, we will + # figure out the in/out of each plane + + self.surfacesDict = {} # to reducs the number of duplicate planes for parallel faces + + def solidMesh(self): + import ObjectsFem + import femmesh.gmshtools as gmshtools + + def gmsh_sizes_from_view(obj, + elems_across=10, + interior_factor=3.0, + min_hmin=10.0): + """ + Map FreeCAD ViewObject Deviation / AngularDeflection to Gmsh mesh sizes. + Returns: + h_min, h_max, min_elems_2pi + """ + v = obj.ViewObject + + shp = obj.Shape + bbox = shp.BoundBox + + L = max(bbox.XLength, bbox.YLength, bbox.ZLength) + + if L <= 0: + # degenerate + h_min = min_hmin + else: + h_min = max(L / elems_across, min_hmin) - def export(self): - import MeshPart + h_max = h_min * interior_factor - shape = self.obj.Shape.copy(False) - shape.Placement = FreeCAD.Placement() # remove object's placement - alreadyExportedName = AutoTessellateExporter.alreadyExported(shape) - if alreadyExportedName is not None: - self._name = alreadyExportedName - return + return h_min, h_max - else: - AutoTessellateExporter.shapesDict[shape] = self.name() - - viewObject = self.obj.ViewObject - deflection = viewObject.Deviation - angularDeflection = math.radians(viewObject.AngularDeflection) - mesh = MeshPart.meshFromShape(Shape=shape, LinearDeflection=deflection, - AngularDeflection=angularDeflection, Relative=False) - - tessName = self.name() - # Use more readable version - tess = ET.SubElement(solids, "tessellated", {"name": tessName}) - tessVname = tessName + "_" - placementCorrection = self.obj.Placement.inverse() - for i, v in enumerate(mesh.Points): - v = FreeCAD.Vector(v.x, v.y, v.z) - exportDefineVertex(tessVname, v, i) - for f in mesh.Facets: - indices = f.PointIndices - i0 = indices[0] - i1 = indices[1] - i2 = indices[2] - ET.SubElement( - tess, - "triangular", - { - "vertex1": tessVname + str(i0), - "vertex2": tessVname + str(i1), - "vertex3": tessVname + str(i2), - "type": "ABSOLUTE", - }, - ) - self._exportScaled() + doc = FreeCAD.ActiveDocument + h_min, h_max = gmsh_sizes_from_view(self.obj) + # mesh = doc.addObject('Fem::FemMeshGmshFromShape', 'GmshMesh') + mesh = ObjectsFem.makeMeshGmsh(doc, f"{self.obj.Label}_temporary_mesh") + print(mesh, mesh.TypeId) - @staticmethod - def centerOfMass(pts: [Vector]) -> Vector: - cm = Vector(0, 0, 0) - for pt in pts: - cm += pt + mesh.Shape = self.obj - return cm + mesh.CharacteristicLengthMin = h_min + mesh.CharacteristicLengthMax = h_max - @staticmethod - def principalMoments(pts: [Vector]) -> tuple: - Ixx = 0 - Iyy = 0 - Izz = 0 - for pt in pts: - Ixx += pt.y * pt.y + pt.z * pt.z - Iyy += pt.x * pt.x + pt.z * pt.z - Izz += pt.x * pt.x + pt.y * pt.y + # Curvature-based refinement + mesh.MeshSizeFromCurvature = True - return Ixx, Iyy, Izz + # 3D mesh algorithm and optimization + mesh.Algorithm3D = 1 # Tetrahedral + mesh.OptimizeStd = True + mesher = gmshtools.GmshTools(mesh) + mesher.create_mesh() + doc.recompute() - @staticmethod - def identicalShapes(shp1, shp2) -> bool: - # return True if shapes are the same - verts1 = shp1.Vertexes - verts2 = shp2.Vertexes + # 4 loop over tetrahedra, creating planes and regions for each - # Test 1, same number of vertexes - if len(verts1) != len(verts2): - return False + fem_mesh = mesh.FemMesh + faces_attr = fem_mesh.Faces + if isinstance(faces_attr, dict): + face_ids = list(faces_attr.keys()) + else: + face_ids = list(faces_attr) + + # So my understanding of the structure of the mesh, so far, is the following + # if E is the number of Edges, F the number of faces and V the number of Volumes + # then Edge_ids go from 1 - E + # Face ids go form E+1 - E+1+F + # and volume ids go from E+F+1+1 - E+F+1+1+V + # so mesh.getElementNoded(id) will get the edge node ids of id is in the Edge ids range + # will get the face id nodes for ids in the Face ids range + # and will get the volume id nodes for id in the Volume id range. + # For edges there will be two node ids, corresponding to the ends of the edge + # For faces, there will be three ids, corresponding to the triangle coordinates + # and for Volume ids, there will be four node ids, corresponding to the four coreners of + # of the tetrahedron + # mesh.Node[id] gives the Vector coordinates of the node, that is the point + # P.S: Face ids are those of the boundary faces only, not of every tetrahedron surface + + # get a list of planes, one for each face. At present the sense of + # the normal is arbitrary. When we build the regions for each tetrahedron, we will + # figure out the in/out of each plane + + def plane_from_face(node_ids): + face_key = frozenset(node_ids) + coords = [fem_mesh.Nodes[n] for n in node_ids] + v1 = coords[1] - coords[0] + v2 = coords[2] - coords[1] + normal = v1.cross(v2) + normal.normalize() + D = normal.dot(coords[0]) + surf = PlaneSurfaceExporter(f"{self.obj.Label}_{vol_id}_{i}", normal, D) + plane_nodes_dict[face_key] = surf # given three node ids, we identify the plane + # regardless of the order in which node ids are given + surfaces.append(surf) + + surfaces = [] + plane_nodes_dict = {} + region_expr = "" + + for vol_id in fem_mesh.Volumes: + volume_region = "" + # Get the node IDs associated with this tetrahedron + # For a 1st order mesh, this will return 4 nodes + node_ids = fem_mesh.getElementNodes(vol_id) + for i in range(4): # we should doublecheck we are using tetrahedra + opposite_corner = node_ids[i] + face_corners = node_ids[:i] + node_ids[i + 1:] # other corners + face = frozenset(face_corners) + if face not in plane_nodes_dict: # if face not already in dictionary, build it + plane_from_face(face_corners) + vector_to_face = fem_mesh.Nodes[face_corners[0]] - fem_mesh.Nodes[opposite_corner] + surface = plane_nodes_dict[face] + if surface.normal.dot(vector_to_face) > 0: # normal points to outside, we want to be in + volume_region += f"-{surface.id} " + else: + volume_region += f"+{surface.id} " - # Test 2, Center of mass - pts1 = [v.Point for v in shp1.Vertexes] - pts2 = [v.Point for v in shp2.Vertexes] - cm1 = AutoTessellateExporter.centerOfMass(pts1) - cm2 = AutoTessellateExporter.centerOfMass(pts2) - if (cm1 - cm2).Length > 1e-04: - return False + if region_expr == "": + region_expr = f"({volume_region})" + else: + region_expr += f"|({volume_region})" - # Test 3, compare volumes. - # I am not sure which is faster, moment of inertial calculation or volume calculation - # I Shape.Volume is calculated in C it is probably faster than CM and should be done first - # But I don't know for sure - if (shp1.Volume - shp2.Volume) > 1e-6: - return False + return surfaces, Region(region_expr) - # Test 4, same moments of inertia - II1 = AutoTessellateExporter.principalMoments(pts1) - II2 = AutoTessellateExporter.principalMoments(pts2) + def face_to_plane(self, face): + def plane_key(): + return (round(normal.x, 7), round(normal.y, 7), round(normal.z, 7), round(D, 7)) - for i, II in enumerate(II1): - if abs(II1[i] - II2[i]) > 1e-08: + normal = Vector(face.Normal.x, face.Normal.y, face.Normal.z) + # remember we need to convert from mm to cm + point0 = Vector(face.Points[0][0], face.Points[0][1], face.Points[0][2]) + D = normal.dot(point0) + key = plane_key() + if key in self.surfacesDict: + return None # may be a poor way of signaling to the caller that the surface has been processed + else: + planesurface = PlaneSurfaceExporter(self.name() + f"_{self.index}", normal, D) + self.surfacesDict[key] = planesurface + + self.index += 1 + return planesurface + + def remove_redundant_planes(self, planes): + set_of_planes = set() + for plane in planes: + set_of_planes.add(plane) + return list(set_of_planes) + + def check_convex_face(self, face, mesh): + normal = Vector(face.Normal.x, face.Normal.y, face.Normal.z) + point0 = Vector(face.Points[0][0], face.Points[0][1], face.Points[0][2]) + D = normal.dot(point0) + pnts = [p.Vector for p in mesh.Points] + + for p in pnts: + if p.dot(normal) > D: return False + return True - # Well, ChatGPT says in principle one can have all moments of inertia to be the same for all axes - # and the shapes be different. Dr. Omar Hijab also convinced me of this. - # If all of the above is true, it is likely that the shapes are the same. But to be on - # the safe side, we sample a few of the points and assume they are ordered the same. - # If the shapes are really the same but because the ordering of the points is different then - # at worst we export the same shape twice. This is much better than exporting one shape, when in fact - # they are two different shapes. The likelihood that two different shapes match in order is extremely - # small - - # Test 4 - nsamples = int(len(verts1)/10) + 1 - sampled = set() # to avoid sampling same point twice we keep track of already sampled points - if nsamples >= len(pts1): - nsamples = len(pts1) - - while len(sampled) < nsamples: - index = random.randint(0, nsamples-1) - if (pts2[index]-pts1[index]).Length > 1e-04: - return False - sampled.add(index) + def generate_surfaces(self): + import MeshPart + global obj_top_children_dict - return True + if self.obj.TypeId != 'Mesh::Feature': - @staticmethod - def alreadyExported(shape) -> str | None: - for shp in AutoTessellateExporter.shapesDict: - if AutoTessellateExporter.identicalShapes(shape, shp): - return AutoTessellateExporter.shapesDict[shp] # return name of shape + shape = self.obj.Shape.copy(False) + + bbox = shape.BoundBox + minLength = min([bbox.XLength, bbox.YLength, bbox.ZLength]) + + # shape.Placement = FreeCAD.Placement() # remove object's placement + + print(f"Autotessellating {self.obj.Label} which is of type {self.obj.TypeId}") + viewObject = self.obj.ViewObject + deflection = viewObject.Deviation # this is in percent + linearDeflection = deflection / 100 * minLength + angularDeflection = viewObject.AngularDeflection.Value + ShapeMesher.deflection = linearDeflection + ShapeMesher.angularDeflection = angularDeflection + + shapeMesher = ShapeMesher(shape) + + components = shapeMesher.mesh.getSeparateComponents() + else: + components = self.obj.Mesh.getSeparateComponents() + + self.region = Region("") + for i, comp in enumerate(components): + V = comp.CountPoints + E = comp.CountEdges + F = comp.CountFacets + chi = V - E + F + genus = (2 - chi) / 2 + print(f"Component {i} Verts = {V}, Edges = {E}, Faces = {F}, Eueler = {chi} genus = {genus}") + + shape = Part.Shape() + tolerance = 0.1 + shape.makeShapeFromMesh(comp.Topology, tolerance) + componentMesher = ShapeMesher(shape) + component_region = "" + + region_sets = componentMesher.split_along_concaves() + j = 0 + for region in region_sets: + planes = set() + for vid in region.v2p: + vid_planes = region.v2p[vid] + for pl in vid_planes: + planes.add(pl) + region_expr = "" + k = 0 + for plane in planes: + # i = mesh component index + # j = region index in this component + # k = plane index for planes in this region + surf = PlaneSurfaceExporter(self.name() + f"_{i}_{j}_{k}", Vector(plane.normal), plane.D) + k += 1 + self.surfaces.append(surf) + region_expr += f"-{surf.id} " + if component_region == "": + component_region = f"({region_expr})" + else: + component_region += f" | ({region_expr})" + j += 1 - return None # shape not already exported + # TODO, test if the regions formaed are convex or not. + self.region = self.region.union(Region(component_region)) + self.position_globally() From 2a03a6eb8ad8cb9d772e04674b2502efda6674c1 Mon Sep 17 00:00:00 2001 From: Munther Hindi Date: Wed, 4 Feb 2026 17:56:59 -0800 Subject: [PATCH 8/8] restore exportGDML.py --- freecad/gdml/exportGDML.py | 7916 ++++++++++++++++-------------------- 1 file changed, 3498 insertions(+), 4418 deletions(-) diff --git a/freecad/gdml/exportGDML.py b/freecad/gdml/exportGDML.py index 235853a9..2bbc39a1 100644 --- a/freecad/gdml/exportGDML.py +++ b/freecad/gdml/exportGDML.py @@ -32,7 +32,6 @@ __author__ = "Keith Sloan " __url__ = ["https://github.com/KeithSloan/FreeCAD_Geant4"] -from gettext import translation from sys import breakpointhook import FreeCAD, os, Part, math @@ -40,15 +39,10 @@ import FreeCAD as App import FreeCADGui from PySide import QtGui -import copy from FreeCAD import Vector -import BOPTools.SplitAPI -from BOPTools import BOPFeatures -from collections import defaultdict import random -import re from .GDMLObjects import GDMLcommon, GDMLBox, GDMLTube # modif add @@ -57,10 +51,6 @@ import sys from pathlib import Path -from .GDMLShared import getMult -from .GmshUtils import meshObj -from .polygonsHelper import inner_outer - try: import lxml.etree as ET @@ -75,17 +65,28 @@ except ImportError: FreeCAD.Console.PrintMessage("pb xml lib not found\n") sys.exit() - # xml handling # import argparse # from xml.etree.ElementTree import XML ################################# global zOrder -global element_table # dictionary of ElementIsotopes object for an element symbol as key -global nuclide_table # dictionary of Isotope obj for nuclide name as key -element_table = None +from .GDMLObjects import ( + GDMLQuadrangular, + GDMLTriangular, + GDML2dVertex, + GDMLSection, + GDMLmaterial, + GDMLfraction, + GDMLcomposite, + GDMLisotope, + GDMLelement, + GDMLconstant, + GDMLvariable, + GDMLquantity, + GDMLbordersurface, +) from . import GDMLShared @@ -286,6 +287,12 @@ def place(self, volRef): self.assembly = assembly +class PhysVolPlacement: + def __init__(self, ref, placement): + self.ref = ref # name reference: a string + self.placement = placement # physvol placement + + ######################################################### # Pretty format GDML # ######################################################### @@ -340,132 +347,41 @@ def initGDML(): return gdml -class Isotope: - def __init__(self, nuclide, atomic_weight, fraction, element): - self.nuclide = nuclide # nuclide, example 'U235' - self.atomic_weight = atomic_weight # float atomic weight (234.99, eg) - self.fraction = fraction # atomic fraction of isotope in its element - self.element = element # the ElementIsotopes this isotope belongs to - - def weight_fraction(self): - return self.fraction * self.atomic_weight / self.element.atomic_weight - - -class ElementIsotopes: - def __init__(self, Z, symbol, element, atomic_weight, isotopes): - self.Z = Z - self.symbol = symbol - self.element = element # element name - self.atomic_weight = atomic_weight # mean atomic weight of element - self.isotopes = isotopes # list[Isotope] - - -def gen_element_table(file_name): - '''read the given file and return a dictionary of ElementIsotopes: dict[element_symbol] = ElementIsotope - ''' - - table = {} - - fd = open(file_name, 'r') - for line in fd.readlines(): - if line[0] == '#': - continue - words = line.strip().split() - Z = int(words[0]) - sym = words[1] - element = words[2].lower() - nuclide_list = words[3:] - if 3 * int(len(nuclide_list) / 3) != len(nuclide_list): - print( - f"*** Nuclide list for element {Z} {element} might be missing something a nuclide or a fraction. Please check file ***") - exit(0) - - num_words = len(nuclide_list) - tot_fraction = 0 - average_atomic_weight = 0 - isotopes_list = [] - elementIsotopes = ElementIsotopes(Z, sym, element, average_atomic_weight, isotopes_list) - for i in range(0, num_words, 3): - nuclide = nuclide_list[i] - atomic_weight = float(nuclide_list[i + 1]) - fraction = float(nuclide_list[i + 2]) - isotope = Isotope(nuclide, atomic_weight, fraction, elementIsotopes) - isotopes_list.append(isotope) - tot_fraction += fraction - average_atomic_weight += atomic_weight - - # sanity check for typos - nuclide_element = nuclide[:len(sym)] - if nuclide_element != sym: - print(f"nuclide {nuclide} does not have the same element name as its element {sym}") - return - # another check, atomic weight should be within 0.1% of mass number - mass_number = int(nuclide[len(sym):]) - if abs(mass_number - atomic_weight) / atomic_weight > 5.0e-3: - print( - f" {nuclide} atomic wieght {atomic_weight} different than mass number by more than 0.5%. Please check") - - average_atomic_weight = average_atomic_weight / len(isotopes_list) - elementIsotopes.atomic_weight = average_atomic_weight - table[sym] = elementIsotopes - # sanity check - if abs(tot_fraction - 1.0) > 2.0e-03 and tot_fraction != 0: - print(f"Warning: {element} sum of fraction is {tot_fraction} differs from 1.0 by at least 0.2%") - - return table - - -def gen_nuclide_table(): - global element_table - ''' - put all nuclides is a dictionary dict[nuclide] = Isotope (isotope class containing the nuclide) - ''' - table = {} - for elementSymbol in element_table: - elementObject = element_table[elementSymbol] - isotopes = elementObject.isotopes - for isotope in isotopes: - table[isotope.nuclide] = isotope - - return table - - ################################# # Setup GDML environment ################################# + def GDMLstructure(): # print("Setup GDML structure") ################################# # globals ################################ - from .init_gui import joinDir - global gdml, geometry, materials + global gdml, constants, variables, define, materials, solids, structure, setup global WorldVOL global defineCnt, LVcount, PVcount, POScount, ROTcount, SCLcount global centerDefined global identityDefined global identityName global gxml - global material_ids - global element_table - global nuclide_table centerDefined = False identityDefined = False identityName = 'identity' defineCnt = LVcount = PVcount = POScount = ROTcount = SCLcount = 1 - # gdml = initGDML() - materials = ET.Element('materials') - geometry = ET.Element('geometry') - material_ids = {} + gdml = initGDML() + define = ET.SubElement(gdml, "define") + materials = ET.SubElement(gdml, "materials") + solids = ET.SubElement(gdml, "solids") + solids.clear() + structure = ET.SubElement(gdml, "structure") + setup = ET.SubElement(gdml, "setup", {"name": "Default", "version": "1.0"}) + gxml = ET.Element("gxml") + _ = SurfaceManager() - if element_table is None: - element_table = gen_element_table(joinDir('Resources/Isotopic_abundances.csv')) - nuclide_table = gen_nuclide_table() - return + return structure def defineMaterials(): @@ -474,6 +390,44 @@ def defineMaterials(): global materials +def exportDefine(name, v): + global define + ET.SubElement( + define, + "position", + { + "name": name, + "unit": "mm", + "x": str(v[0]), + "y": str(v[1]), + "z": str(v[2]), + }, + ) + + +""" +def exportDefineVertex(name, v, index): + global define + ET.SubElement(define, 'position', {'name': name + str(index), + 'unit': 'mm', 'x': str(v.X), 'y': str(v.Y), 'z': str(v.Z)}) +""" + + +def exportDefineVertex(name, v, index): + global define + ET.SubElement( + define, + "position", + { + "name": name + str(index), + "unit": "mm", + "x": str(v.x), + "y": str(v.y), + "z": str(v.z), + }, + ) + + def defineWorldBox(bbox): global solids for obj in FreeCAD.ActiveDocument.Objects: @@ -555,6 +509,7 @@ def quaternion2XYZ(rot): and g = atan2(yp, zp) """ v = rot * Vector(0, 0, 1) + print(v) # solution 1. if v.x > 1.0: v.x = 1.0 @@ -590,6 +545,216 @@ def quaternion2XYZ(rot): return [math.degrees(a), math.degrees(b), math.degrees(g)] +def reportObject(obj): + GDMLShared.trace("Report Object") + GDMLShared.trace(obj) + GDMLShared.trace("Name : " + obj.Label) + GDMLShared.trace("Type : " + obj.TypeId) + if hasattr(obj, "Placement"): + print("Placement") + print("Pos : " + str(obj.Placement.Base)) + print("axis : " + str(obj.Placement.Rotation.Axis)) + print("angle : " + str(obj.Placement.Rotation.Angle)) + + while switch(obj.TypeId): + + ########################################### + # FreeCAD GDML Parts # + ########################################### + if case("Part::FeaturePython"): + GDMLShared.trace("Part::FeaturePython") + # + # if hasattr(obj.Proxy,'Type'): + # print (obj.Proxy.Type) + # print (obj.Name) + # else : + # print("Not a GDML Feature") + + # print dir(obj) + # print dir(obj.Proxy) + # print("cylinder : Height "+str(obj.Height)+ " Radius "+str(obj.Radius)) + break + ########################################### + # FreeCAD Parts # + ########################################### + if case("Part::Sphere"): + print("Sphere Radius : " + str(obj.Radius)) + break + + if case("Part::Box"): + print( + "cube : (" + + str(obj.Length) + + "," + + str(obj.Width) + + "," + + str(obj.Height) + + ")" + ) + break + + if case("Part::Cylinder"): + print( + "cylinder : Height " + + str(obj.Height) + + " Radius " + + str(obj.Radius) + ) + break + + if case("Part::Cone"): + print( + "cone : Height " + + str(obj.Height) + + " Radius1 " + + str(obj.Radius1) + + " Radius2 " + + str(obj.Radius2) + ) + break + + if case("Part::Torus"): + print("Torus") + print(obj.Radius1) + print(obj.Radius2) + break + + if case("Part::Prism"): + print("Prism") + break + + if case("Part::RegularPolygon"): + print("RegularPolygon") + break + + if case("Part::Extrusion"): + print("Extrusion") + break + + if case("Circle"): + print("Circle") + break + + if case("Extrusion"): + print("Wire extrusion") + break + + if case("Mesh::Feature"): + print("Mesh") + # print dir(obj.Mesh) + break + + print("Other") + print(obj.TypeId) + break + + +def addPhysVol(xmlVol, volName): + GDMLShared.trace("Add PhysVol to Vol : " + volName) + # print(ET.tostring(xmlVol)) + pvol = ET.SubElement(xmlVol, "physvol", {"name": "PV_" + volName}) + ET.SubElement(pvol, "volumeref", {"ref": volName}) + return pvol + + +def getIdentifier(obj): + ''' For objects created from a gdml file we need a unique identifier to locate + the entry in the gdmlInfo sheet. + ''' + return obj.Label + + if hasattr(obj, "CopyNumber"): + append = '.' + str(obj.CopyNumber) + else: + append = '' + + print(f"{obj.Name}{append}") + if obj.TypeId == "App::Part": + name = obj.Name + append + return name + elif obj.TypeId == "App::Link": + return obj.LinkedObject.Name + append + else: + return obj.Name + + +def addPhysVolPlacement(obj, xmlVol, placement, pvName=None, refName=None) -> None: + # obj: App:Part to be placed. + # xmlVol: the xml that the str: + ''' + Unfortunately, geant produces its own internal names for assemblies. The generated name + is a very complicated thing that depends on the volume being placed and the number of times + it is being placed. As part of exporting the the document as gdml, we first build + a dictionary in buildAssemblyDict, that associates with each physvol placement of an assembly + the same name that geant generates. Here we retrieve the physvol name from the dictionary + if the dictKey is a key in the Assembly dictionary, or the name physvol name associated + with the parentObj (an App::Part) + ''' + # Obj is the source used to create candidates + print(f"getPVname {obj.Label}") + if dictKey in AssemblyDict: + print(f"returning name of {dictKey} from Assembly dictionary") + entry = AssemblyDict[dictKey] + return entry.getPVname(obj, idx) + else: + print("No Parent") - elementIsotopes = element_table[sym] - isotopes = elementIsotopes.isotopes - for isotope in isotopes: - # print(nucleus) - nuc_dict = {} - nuc_dict['nuclide'] = isotope.nuclide - nuc_dict['fraction'] = element_count * isotope.fraction - nuc_dict['type'] = 'ao' - nuclide_list.append(nuc_dict) - return nuclide_list - - # A mixture - print(f"I think {mat} is a mixture") - print(mat_obj.Group) - # in a mixture the fractions are weight fractions - for grp in mat_obj.Group: - mat_or_element = grp.Label[:grp.Label.find(' :')] - weight_fraction = float(grp.Label[1 + grp.Label.find(':'):]) - sym = elementSymbol(mat_or_element) - if sym is not None: # The mixture involves a natural element - component_list = material_nuclides(mat_or_element) - for component in component_list: - isotopeName = component['nuclide'] - isotopeObject = nuclide_table[isotopeName] - # for a natural element, the isotope fractions are atomic fractions - # for mixtures (of elements) the fractions should be weight fractions - # The components returned above are for an element an so need to convert to weight_fractions - component[ - 'fraction'] *= weight_fraction * isotopeObject.atomic_weight / isotopeObject.element.atomic_weight - component['type'] = 'wo' - nuclide_list = merge_lists(nuclide_list, component_list) - continue - - print(f"checking if {grp.Label} is defined in the Elements Group") - elem_grp = elementGroup(mat_or_element) - if elem_grp is not None: - for isotope in elem_grp.Group: - print(isotope.Label) - index = isotope.Label.find(' :') - print(index) - isotope_name = isotope.Label[:isotope.Label.find(' :')] - isotope_fraction = isotope.n - nuc_dict = {} - nuc_dict['nuclide'] = isotope_name - # in an element definition the isotope fractions are ATOMIC fractions, - # but here we are processing mixtures, the fractions should be WEIGHT fractions - # so we need to convert - isotopeObject = nuclide_table[isotope_name] - nuc_dict[ - 'fraction'] = weight_fraction * isotope_fraction * isotopeObject.atomic_weight / isotopeObject.element.atomic_weight - nuc_dict['type'] = 'wo' - nuclide_list.append(nuc_dict) - continue - - # TODO: Should Check if the component is actually a Single isotope - - print(f"checking if {grp.Label} is defined in the Materials Group") - mat_grp = materialGroup(mat_or_element) - if mat_grp is not None: - fraction = float(grp.Label[grp.Label.find(':') + 1:]) - component_list = material_nuclides(mat_or_element) - if component_list is None: - print(f"material component {mat_or_element} is not a material and not an element") - continue - # multiply the fractions in the returned list by the fraction of the material in the mixture - for component in component_list: - component['fraction'] *= fraction - # a material fraction in a mixture is a weight fraction - # the component might be an element and hence the resulting component might atomic fraction - # we check and compensate here - if component['type'] == 'ao': - nuclideName = component['nuclide'] - nuclideObject = nuclide_table[nuclideName] - component[ - 'fraction'] *= weight_fraction * nuclideObject.atomic_weight / nuclideObject.element.atomic_weight - # update the current nuclide_list with that of the current material - nuclide_list = merge_lists(nuclide_list, component_list) + return NameManager.getPhysvolName(parentObj) - else: - print(f" material {mat} has a component {grp.Label} that is not in Group Elements or in group Materials") + @staticmethod + def exportSurfaceProperty(Name, Surface, ref1, ref2): + borderSurface = ET.SubElement( + structure, "bordersurface", {"name": Name, "surfaceproperty": Surface} + ) + ET.SubElement(borderSurface, "physvolref", {"ref": ref1}) + ET.SubElement(borderSurface, "physvolref", {"ref": ref2}) + + @staticmethod + def checkFaces(pair1, pair2): + def preCheck(shape1, shape2): + # + # Precheck common faces, by checking + # if bounding boxes separation is comparable + # to sum of half-lengths + # + b1 = shape1.BoundBox + b2 = shape2.BoundBox + vcc = b2.Center - b1.Center + if ( + abs(vcc.x) > (b1.XLength + b2.XLength) * 1.01 / 2 + or abs(vcc.y) > (b1.YLength + b2.YLength) * 1.01 / 2 + or abs(vcc.z) > (b1.ZLength + b2.ZLength) * 1.01 / 2 + ): + return False + else: + return True + + tolerence = 1e-6 + obj1 = pair1[0] + matrix1 = pair1[1].Matrix + obj2 = pair2[0] + matrix2 = pair2[1].Matrix + + if hasattr(obj1, "Shape") and hasattr(obj2, "Shape"): + obj1t = obj1.Shape.transformGeometry(matrix1) + obj2t = obj2.Shape.transformGeometry(matrix2) + if not preCheck(obj1t, obj2t): + print("Fails precheck") + return False + + faces1 = obj1t.Faces + faces2 = obj2t.Faces + # faces1 = obj1.Shape.Faces + # faces2 = obj2.Shape.Faces + for f1 in faces1: + comShape = f1.common(faces2, tolerence) + if len(comShape.Faces) > 0: + print("Common") + return True + else: + print("Not common") + return False + + @staticmethod + def processSurface(name, cnt, surface, + Obj1, obj1, idx1, dictKey1, + Obj2, obj2, idx2, dictKey2): + print(f"processSurface {name} {surface}") + print(f" {Obj1.Label} {obj1.Label} {Obj2.Label} {obj2.Label}") + ref1 = SurfaceManager.getPVname(Obj1, obj1, idx1, dictKey1) + ref2 = SurfaceManager.getPVname(Obj2, obj2, idx2, dictKey2) + SurfaceManager.exportSurfaceProperty(name + str(cnt), surface, ref1, ref2) + return cnt + 1 + + @staticmethod + def processCandidates(name, surface, check, Obj1, dict1, Obj2, dict2): + cnt = 1 + for assem1, set1 in dict1.items(): + print(f"process Candidates {assem1} {check} {len(set1)}") + for assem2, set2 in dict2.items(): + print(f"process Candidates {assem2} {check} {len(set2)}") + for idx1, items1 in enumerate(set1): + obj1 = items1[0] + for idx2, items2 in enumerate(set2): + obj2 = items2[0] + if items1 != items2: + if check: + pairStr = f"{obj1.Label} : {obj2.Label} " + if SurfaceManager.checkFaces(items1, items2): + cnt = SurfaceManager.processSurface(name, cnt, surface, + Obj1, obj1, idx1, assem1, + Obj2, obj2, idx2, assem2) + print(f"<<< Common face : {pairStr} >>>") + cnt += 1 + else: + print(f"<<< No common face : {pairStr} >>>") + else: + cnt = SurfaceManager.processSurface( + name, cnt, surface, + Obj1, obj1, idx1, assem1, + Obj2, obj2, idx2, assem2 + ) + + @staticmethod + def _getSubVols(vol, placement, volLabel): + global childObjects + + """return a flattened list of terminal solids that fall + under this vol. By flattened we mean something like: + vol + subVol1 + subVol2 + solid1 + solid2 + ... + subVol3 + solid3 + solid4 + .... + + The returned list is a list of triples: + ((solid1, placement1, subvol2.Label), (solid2, placement2, subvol2.Label), (solid3, placement3, subVol3.Label), ... + """ + print(f"getSubVols {vol.Label} {volLabel} {placement} ") + volsList = [] + print(f"_getSubVols: isContainer({vol.Label}) = {isContainer(vol)}") + if len(childObjects[vol]) == 0: + return [(vol, placement, volLabel)] + + # we assume that the user meant to select ONLY the top volume of a container as the solid with + # the optical surface property. + if isContainer(vol): + obj = childObjects[vol][0] + return [(obj, placement * obj.Placement, volLabel)] + + # vol must be an assembly, recurse + for obj in childObjects[vol]: + typeId = obj.TypeId + tObj = obj + # print(obj.Label) + if hasattr(obj, "LinkedObject"): + typeId = obj.LinkedObject.TypeId + if len(childObjects[obj]) != 0: + tObj = childObjects[obj][0] + + if typeId == "App::Part": + volsList += SurfaceManager._getSubVols(tObj, placement * obj.Placement, obj.Label) + else: + if typeId == "Part::FeaturePython": + volsList.append((obj, placement, volLabel)) + + return volsList + + @staticmethod + def getSubVols(vol, placement): + """ + given a structure of the form + vol + subVol1 + subVol2 + solid1 + solid2 + ... + subVol3 + solid3 + solid4 + .... + + return a dictionary: + {subVol2.Label: ((solid1, placement1), (solid2, placement2)), + subVol3.Label: ((solid1, placement1), (solid2, placement2))} + """ + + flattenedList = SurfaceManager._getSubVols(vol, placement, vol.Label) + solidsDict = {} + for item in flattenedList: + vol = item[0] + parentLabel = item[2] + if parentLabel in solidsDict: + solidsDict[parentLabel].append((item[0], item[1])) + else: + solidsDict[parentLabel] = [(item[0], item[1])] + + return solidsDict + + @staticmethod + def processBorderSurfaces(): + print("==============================================") + print(f"Export Border Surfaces - Assemblies {len(AssemblyDict)}") + print("==============================================") + # print(AssemblyDict) + doc = FreeCAD.ActiveDocument + + for obj in doc.Objects: + if obj.TypeId == "App::FeaturePython": + print(f"TypeId {obj.TypeId} Name {obj.Label}") + # print(dir(obj)) + # print(obj.Proxy) + if isinstance(obj.Proxy, GDMLbordersurface): + print("Border Surface") + obj1 = SurfaceManager.getPVobject(doc, obj.PV1) + candSet1 = SurfaceManager.getSubVols(obj1, obj1.Placement) + print(f"Candidates 1 : {obj1.Label} {len(candSet1)}") + printSet("Candidate1", candSet1) + obj2 = SurfaceManager.getPVobject(doc, obj.PV2) + candSet2 = SurfaceManager.getSubVols(obj2, obj2.Placement) + print(f"Candidates 2 : {obj2.Label} {len(candSet2)}") + printSet("Candidate2", candSet2) + # default for old borderSurface Objects + check = False + if hasattr(obj, "CheckCommonFaces"): + check = obj.CheckCommonFaces + SurfaceManager.processCandidates( + obj.Label, + obj.Surface, + check, + obj1, + candSet1, + obj2, + candSet2, + ) + + +def printListObj(name, listArg): + print(f"<=== Object {name} list ===>") + for obj in listArg: + print(obj.Label) + print("<===============================") + + +def printSet(name, dictArg): + print(f"<=== Object Set {name} len {len(dictArg)} ===>") + for k, v in dictArg.items(): + print(k) + for obj in v: + print(f"\t {obj[0].Label}") + print("<===============================") + + +def processSpreadsheetMatrix(sheet): + # Stupid way of finding how many rows. columns: + # increase col, row until we get an exception for that cell + # You would think the API would provide a simple function + def ncols(): + n = 0 + try: + # TODO: deal with case n > 26 + while n < 26 * 26: + sheet.get(chr(ord("A") + n) + "1") + n += 1 + except: + pass + return n + + def nrows(): + n = 0 + try: + while n < 256 * 256: + sheet.get("A" + str(n + 1)) + n += 1 + except: + pass + return n + + global define + print("add matrix to define") + + coldim = ncols() + rows = nrows() + + s = "" + for row in range(0, rows): + for col in range(0, coldim): + cell = chr(ord("A") + col) + str(row + 1) + s += str(sheet.get(cell)) + " " - return nuclide_list + ET.SubElement( + define, + "matrix", + {"name": sheet.Label, "coldim": str(coldim), "values": s[:-1]}, + ) + + +def processOpticals(): + print("Process Opticals") + Grp = FreeCAD.ActiveDocument.getObject("Opticals") + if hasattr(Grp, "Group"): + for obj in Grp.Group: + print(f"Name : {obj.Label}") + while switch(obj.Label): + if case("Matrix"): + print("Matrix") + for m in obj.Group: + if m.TypeId == "Spreadsheet::Sheet": + processSpreadsheetMatrix(m) + else: + processMatrix(m) + break + + if case("Surfaces"): + print("Surfaces") + print(obj.Group) + for s in obj.Group: + SurfaceManager.processOpticalSurface(s) + break + + if case("SkinSurfaces"): + print("SkinSurfaces") + for s in obj.Group: + SurfaceManager.processSkinSurfaces(s) + break def processMaterials(): @@ -961,42 +1434,115 @@ def processMaterials(): global materials for GName in [ - # "Define", - # "Isotopes", - # "Elements", + "Define", + "Isotopes", + "Elements", "Materials", ]: Grp = FreeCAD.ActiveDocument.getObject(GName) if Grp is not None: + # print(Grp.TypeId+" : "+Grp.Label) + print(Grp.Label) if processGroup(Grp) is False: break +def processFractionsComposites(obj, item): + # Fractions are used in Material and Elements + if isinstance(obj.Proxy, GDMLfraction): + # print("GDML fraction :" + obj.Label) + # need to strip number making it unique + ET.SubElement( + item, + "fraction", + {"n": str(obj.n), "ref": nameFromLabel(obj.Label)}, + ) + + if isinstance(obj.Proxy, GDMLcomposite): + # print("GDML Composite") + ET.SubElement( + item, + "composite", + {"n": str(obj.n), "ref": nameFromLabel(obj.Label)}, + ) + + def createMaterials(group): global materials - global material_ids + for obj in group: + if obj.Label != "Geant4": + if not hasattr(obj, 'Group'): + continue + item = ET.SubElement( + materials, "material", {"name": nameFromLabel(obj.Label)} + ) - density_multipliers = { - 'g/cm3': 1, - 'g/cc': 1, - 'mg/cm3': 0.001 - } + # property must be first + for prop in obj.PropertiesList: + if obj.getGroupOfProperty(prop) == "Properties": + ET.SubElement( + item, + "property", + {"name": prop, "ref": getattr(obj, prop)}, + ) + + if hasattr(obj, "Tunit") and hasattr(obj, "Tvalue"): + ET.SubElement( + item, + "T", + {"unit": obj.Tunit, "value": str(obj.Tvalue)}, + ) + + if hasattr(obj, "MEEunit"): + ET.SubElement( + item, + "MEE", + {"unit": obj.MEEunit, "value": str(obj.MEEvalue)}, + ) + + if hasattr(obj, "Dunit") or hasattr(obj, "Dvalue"): + # print("Dunit or DValue") + D = ET.SubElement(item, "D") + if hasattr(obj, "Dunit"): + D.set("unit", str(obj.Dunit)) + + if hasattr(obj, "Dvalue"): + D.set("value", str(obj.Dvalue)) - preprocessMaterialIds() - for mat in material_ids: + # process common options material / element + processIsotope(obj, item) + for o in obj.Group: + processFractionsComposites(o, item) - print(f"processing material {mat}") - id = material_ids[mat] +def postCreateGeantMaterials(): + ''' + Geant4 materials are not added or created in createMaterials. + Here we add those materials to the materials group + ''' + global materials + global usedGeant4Materials + + usedElements = set() + + # collect the used elements in all the used materials + for mat in usedGeant4Materials: + obj = FreeCAD.ActiveDocument.getObjectsByLabel(mat)[0] + for grpItem in obj.Group: + words = grpItem.Label.split(':') + elemName = words[0][:-1] + usedElements.add(elemName) + # create for them + for elemName in usedElements: + obj = FreeCAD.ActiveDocument.getObject(elemName) + createElement(obj) + + for mat in usedGeant4Materials: + obj = FreeCAD.ActiveDocument.getObjectsByLabel(mat)[0] item = ET.SubElement( materials, "material", {"name": str(mat)} ) - - item.attrib['id'] = str(id) - # property must be first - # openmc does not use properties - ''' for prop in obj.PropertiesList: if obj.getGroupOfProperty(prop) == "Properties": ET.SubElement( @@ -1004,100 +1550,210 @@ def createMaterials(group): "property", {"name": prop, "ref": getattr(obj, prop)}, ) - ''' - - # hyphens are wreaking havoc in getting properties from labels. - # so replace them with underscores - mat = mat.replace('-', '_') - obj = FreeCAD.ActiveDocument.getObject(mat) - - if hasattr(obj, "Tvalue"): - temperature = float(obj.Tvalue) - if hasattr(obj, "Tunit"): - if obj.Tunit == 'Celsius' or obj.Tunit == 'C' or obj.Tunit == 'Centigrade': - temperature += 273.15 - elif obj.Tunit == 'Fahrenheit' or obj.Tunit == 'F': # ich!! nobody should do that - temperature = 273.15 + (temperature - 32) / 9 * 5 - item.attrib['temperature'] = str(temperature) if hasattr(obj, "Dunit") or hasattr(obj, "Dvalue"): # print("Dunit or DValue") - D = ET.SubElement(item, "density") - unit_multiplier = 1 + D = ET.SubElement(item, "D") if hasattr(obj, "Dunit"): - if obj.Dunit in density_multipliers: - unit_multiplier = density_multipliers[obj.Dunit] - D.set("units", 'g/cm3') - else: - D.set("units", str(obj.Dunit)) - else: # default to g/cm3, if no Dunit is given - D.set("units", 'g/cm3') + D.set("unit", str(obj.Dunit)) + if hasattr(obj, "Dvalue"): - density = float(obj.Dvalue) - if obj.Dvalue <= 0: - density = 1.0e-25 - D.set("value", str(density * unit_multiplier)) + D.set("value", str(obj.Dvalue)) + + if hasattr(obj, "Tunit") and hasattr(obj, "Tvalue"): + ET.SubElement( + item, + "T", + {"unit": obj.Tunit, "value": str(obj.Tvalue)}, + ) + if hasattr(obj, "MEEunit"): + ET.SubElement( + item, + "MEE", + {"unit": obj.MEEunit, "value": str(obj.MEEvalue)}, + ) # process common options material / element - nuclides_in_material = material_nuclides(mat) - for nuclide in nuclides_in_material: - xml_item = ET.SubElement(item, 'nuclide') + processIsotope(obj, item) + if len(obj.Group) > 0: + for o in obj.Group: + processFractionsComposites(o, item) - nuclide_name = nuclide['nuclide'] - nuclide_fraction = nuclide['fraction'] - fraction_type = nuclide['type'] - xml_item.attrib['name'] = str(nuclide_name) - xml_item.attrib[fraction_type] = str(nuclide_fraction) +def createElements(group): + global materials + for obj in group: + createElement(obj) -def createElement(elementLabel, item): +def createElement(obj): global materials + item = ET.SubElement( + materials, "element", {"name": nameFromLabel(obj.Label)} + ) + # Common code Isotope and Elements1 + processIsotope(obj, item) - elementsGroup = FreeCAD.ActiveDocument.getObject('Elements') - for grp in elementsGroup.Group: - if grp.Label == elementLabel: - for nuclideGrp in grp.Group: - nuclide_name = nuclideGrp.Label[:nuclideGrp.Label.find(' :')] - fraction = nuclideGrp.n - xml_item = ET.SubElement(item, 'nuclide') - xml_item.attrib['name'] = str(nuclide_name) - xml_item.attrib['ao'] = str(fraction) + if len(obj.Group) > 0: + for o in obj.Group: + processFractionsComposites(o, item) -# -------------------------- End process OpenMC materials ------------------------ +def createConstants(group): + global define + for obj in group: + if isinstance(obj.Proxy, GDMLconstant): + # print("GDML constant") + # print(dir(obj)) -def processGroup(obj): - print("Process Group " + obj.Label) - # print(obj.TypeId) - # print(obj.Group) - # if hasattr(obj,'Group') : - # return - if hasattr(obj, "Group"): - # print(" Object List : "+obj.Label) - # print(obj) - while switch(obj.Label): + ET.SubElement( + define, "constant", {"name": obj.Label, "value": obj.value} + ) + + +def createDefine(group): + global define + + from .GDMLShared import definesColumn + # should come up with a test that relies on version number of the WB to test for this + sheet = FreeCAD.ActiveDocument.getObject("defines") + if sheet is None: # Older docs not having a define spreadsheet + createConstants(group) + createVariables(group) + return + + numRows = GDMLShared.lastRow(sheet) + for row in range(1, numRows + 1): + entryType = sheet.get(definesColumn['type'] + str(row)) + entryName = sheet.get(definesColumn['name'] + str(row)) + + if entryType == 'constant' or entryType == 'variable': + value = sheet.getContents(definesColumn['value'] + str(row)) + if len(value) > 0: + if value[0] == "=": + value = GDMLShared.SheetHandler.FC_expression_to_gdml(value[1:]) + + ET.SubElement( + define, str(entryType), {"name": str(entryName), "value": str(value)} + ) + + elif entryType == "quantity": + quantityType = sheet.get(definesColumn['quantity_type'] + str(row)) + quantityUnit = sheet.get(definesColumn['quantity_unit'] + str(row)) + quantityValue = sheet.getContents(definesColumn['quantity_value'] + str(row)) + if quantityValue[0] == '=': + quantityValue = GDMLShared.SheetHandler.FC_expression_to_gdml(quantityValue) + + ET.SubElement( + define, str(entryType), {"name": str(entryName), "type": str(quantityType), + "value": str(quantityValue), "unit": quantityUnit} + ) + + + elif entryType == "position": + attrib = {} + attrib["name"] = str(entryName) + for prop in ["x", "y", "z", "unit"]: + cell = definesColumn["pos_" + prop] + str(row) + value = sheet.getContents(cell) + if len(value) > 0: + if value[0] == "=": + value = GDMLShared.SheetHandler.FC_expression_to_gdml(value[1:]) + else: + value = sheet.get(cell) + attrib[prop] = str(value) + + ET.SubElement(define, str(entryType), attrib) + + elif entryType == "rotation": + attrib = {} + attrib["name"] = str(entryName) + for prop in ["x", "y", "z", "unit"]: + cell = definesColumn["rot_" + prop] + str(row) + value = sheet.getContents(cell) + if len(value) > 0: + if value[0] == "=": + value = GDMLShared.SheetHandler.FC_expression_to_gdml(value[1:]) + else: + value = sheet.get(cell) + attrib[prop] = str(value) + + ET.SubElement(define, str(entryType), attrib) + + +def createVariables(group): + global define + for obj in group: + if isinstance(obj.Proxy, GDMLvariable): + # print("GDML variable") + # print(dir(obj)) + + ET.SubElement( + define, "variable", {"name": obj.Label, "value": obj.value} + ) + + +def createQuantities(group): + global define + for obj in group: + if isinstance(obj.Proxy, GDMLquantity): + # print("GDML quantity") + # print(dir(obj)) + + ET.SubElement( + define, + "quantity", + { + "name": obj.Label, + "type": obj.type, + "unit": obj.unit, + "value": obj.value, + }, + ) + + +def createIsotopes(group): + global materials + for obj in group: + if isinstance(obj.Proxy, GDMLisotope): + # print("GDML isotope") + # item = ET.SubElement(materials,'isotope',{'N': str(obj.N), \ + # 'Z': str(obj.Z), \ + # 'name' : obj.Label}) + # ET.SubElement(item,'atom',{'unit': obj.unit, \ + # 'value': str(obj.value)}) + item = ET.SubElement(materials, "isotope", {"name": obj.Label}) + processIsotope(obj, item) + + +def processGroup(obj): + print("Process Group " + obj.Label) + # print(obj.TypeId) + # print(obj.Group) + # if hasattr(obj,'Group') : + # return + if hasattr(obj, "Group"): + # print(" Object List : "+obj.Label) + # print(obj) + while switch(obj.Label): if case("Define"): # print("Constants") - # skip defines for openMC. - # createDefine(obj.Group) + createDefine(obj.Group) break if case("Quantities"): # print("Quantities") - # skip quantities - # createQuantities(obj.Group) + createQuantities(obj.Group) break if case("Isotopes"): # print("Isotopes") - # skip Isotopes - # createIsotopes(obj.Group) + createIsotopes(obj.Group) break if case("Elements"): # print("Elements") - # createElements(obj.Group) + createElements(obj.Group) break if case("Materials"): @@ -1171,6 +1827,127 @@ def getMaterial(obj): return material +""" +def printObjectInfo(xmlVol, volName, xmlParent, parentName): + print("Process Object : "+obj.Label+' Type '+obj.TypeId) + if xmlVol is not None : + xmlstr = ET.tostring(xmlVol) + else : + xmlstr = 'None' + print('Volume : '+volName+' : '+str(xmlstr)) + if xmlParent is not None : + xmlstr = ET.tostring(xmlParent) + else : + xmlstr = 'None' + print('Parent : '+str(parentName)+' : '+str(xmlstr)) +""" + + +def exportCone(name, radius, height): + cylEl = ET.SubElement( + solids, + "cone", + { + "name": name, + "rmin1": "0", + "rmax1": str(radius), + "rmin2": "0", + "rmax2": str(radius), + "z": str(height), + "startphi": "0", + "deltaphi": "360", + "aunit": "deg", + "lunit": "mm", + }, + ) + return cylEl + + +def buildAssemblyTree(worldVol): + from .AssemblyHelper import AssemblyHelper + + global AssemblyDict + + def processContainer(vol): + objects = assemblyHeads(vol) + imprNum = 1 + for obj in objects[1:]: + print( + f" buildAssemblyTree::processContainer {obj.Label} {obj.TypeId} " + ) + processVolAssem(obj, imprNum) + + def processVolAssem(vol, imprNum): + # vol - Volume Object + # xmlParent - xml of this volume's Parent + if vol.Label[:12] != "NOT_Expanded": + if isContainer(vol): + processContainer(vol) + elif isAssembly(vol): + processAssembly(vol, imprNum) + elif vol.TypeId == "App::Link": + processLink(vol, imprNum) + else: + print( + f"{vol.Label} is neither a link, nor an assembly nor a container" + ) + + def processLink(vol, imprNum): + linkedObj = vol.getLinkedObject() + if linkedObj.Label in AssemblyDict: + entry = AssemblyDict[linkedObj.Label] + instCnt = entry.www + imprNum = entry.xxx + 1 + entry = AssemblyHelper(vol, instCnt, imprNum) + AssemblyDict[vol.Label] = entry + else: + processVolAssem(linkedObj, imprNum) + + def processAssembly(vol, imprNum): + print(f"{vol.Label} typeId= {vol.TypeId}") + if hasattr(vol, "LinkedObject"): + print(f"{vol.Label} has a LinkedObject") + linkedObj = vol.getLinkedObject() + if linkedObj.Label in AssemblyDict: + entry = AssemblyDict[linkedObj.Label] + instCnt = entry.www + imprNum = entry.xxx + 1 + else: + instCnt = AssemblyHelper.maxWww + 1 + entry = AssemblyHelper(vol, instCnt, imprNum) + AssemblyDict[vol.Label] = entry + assemObjs = assemblyHeads(vol) + imprNum += 1 + for obj in assemObjs: + print( + f" buildAssemblyTree::processAssembly {obj.Label} {obj.TypeId} " + ) + if obj.TypeId == "App::Part": + processVolAssem(obj, imprNum) + elif obj.TypeId == "App::Link": + processLink(obj, imprNum) + else: + if SolidExporter.isSolid(obj): + entry.addSolid(obj) + + processContainer(worldVol) + + for k, v in AssemblyDict.items(): + print(f"Assembly: {k} av_{v.www}_impr_{v.xxx}") + + +def createXMLvolume(name): + GDMLShared.trace("create xml volume : " + name) + elem = ET.Element("volume", {"name": name}) + return elem + + +def createXMLassembly(name): + GDMLShared.trace("create xml assembly : " + name) + elem = ET.Element("assembly", {"name": name}) + return elem + + def invPlacement(placement): inv = placement.inverse() return inv @@ -1221,58 +1998,362 @@ def isArrayOfPart(obj): return obj1.Base.TypeId == "App::Part" -def processArrayPart(array): - # array: array object +def processArrayPart(array, xmlVol): + # vol: array object + global physVolStack from . import arrayUtils - # array: Array item + # vol: Array item # new approach 2024-08-07: # Create an assembly for the array. The assembly has the name of the array Part # place the repeated array elements in this new assembly and place # the array assembly in the xmlVol with the position and rotation of the array # xmlVol: xml item into which the array elements are placed/exported - # arrayRef = NameManager.getName(array) - base = array.Base - baseName = NameManager.getName(array.Base) - # create a cell with id = universe_dict[baseName] - # baseUniverseId = universe_dict[baseName] - # basePhysVol = physVolStack.pop() - # baseRotation = basePhysVol.placement.Rotation - # baseTranslation = basePhysVol.placement.Base + arrayRef = NameManager.getName(array) + arrayXML = createXMLassembly(arrayRef) + print(f"Process Array Part {array.Label} Base {array.Base} {xmlVol}") + processVolAssem(array.Base, None, array.Base.Label) + basePhysVol = physVolStack.pop() + baseRotation = basePhysVol.placement.Rotation + baseTranslation = basePhysVol.placement.Base + arrayPos = array.Placement.Base + arrayRot = array.Placement.Rotation + + parent = array.InList[0] + print(f"parent {parent}") arrayType = arrayUtils.typeOfArray(array) - saved_placement = FreeCAD.Placement(base.Placement.Base, base.Placement.Rotation) - doc = FreeCAD.ActiveDocument while switch(arrayType): if case("ortho"): - placements = arrayUtils.placementList(array) + pos = basePhysVol.placement.Base + print(f"basePhysVol: {basePhysVol.ref} position: {arrayPos}") + placements = arrayUtils.placementList(array, offsetVector=pos, rot=baseRotation) + print(f'Number of placements = {len(placements)}') for i, placement in enumerate(placements): - base.Placement = placement - doc.recompute() - geomExporter = GeomObjExporter.getExporter(base) - geomExporter.export() + ix, iy, iz = arrayUtils.orthoIndexes(i, array) + baseName = array.Base.Label + '-' + str(ix) + '-' + str(iy) + \ + '-' + str(iz) + print(f"Base Name {baseName}") + # print(f"Add Placement to {parent.Label} volref {vol.Base.Label}") + addPhysVolPlacement(array.Base, arrayXML, + placement, pvName=str(baseName)) break if case("polar"): - placements = arrayUtils.placementList(array, offsetVector=base.Placement.Base) + positionVector = baseRotation.inverted() * baseTranslation # + vol.Placement.Base + placements = arrayUtils.placementList(array, offsetVector=positionVector, rot=baseRotation) + print(f'Number of placements = {len(placements)}') for i, placement in enumerate(placements): - base.Placement = placement - doc.recompute() - geomExporter = GeomObjExporter.getExporter(base) - geomExporter.export() + baseName = array.Base.Label + '-' + str(i) + addPhysVolPlacement(array.Base, arrayXML, + placement, pvName=str(baseName)) break if case("PathArray") or case("PointArray"): - placements = arrayUtils.placementList(array) + pos = basePhysVol.placement.Base + print(f"basePhysVol: {basePhysVol.ref} position: {arrayPos}") + placements = arrayUtils.placementList(array, offsetVector=pos, rot=baseRotation) for i, placement in enumerate(placements): - base.Placement = placement - doc.recompute() - geomExporter = GeomObjExporter.getExporter(base) - geomExporter.export() + baseName = array.Base.Label + '-' + str(i) + addPhysVolPlacement(array.Base, arrayXML, + placement, pvName=str(baseName)) + break + + placement = array.Placement + # if psPlacement is not None: + # placement = invPlacement(psPlacement) * placement + addPhysVolPlacement(array, xmlVol, placement) + physVolStack.append(PhysVolPlacement(array, placement)) + + structure.append(arrayXML) + # structure.append(xmlVol) + + +def processAssembly(vol, xmlVol, xmlParent, parentName, psPlacement): + global structure + global physVolStack + + # vol - Volume Object + # xmlVol - xml of this assembly + # xmlParent - xml of this volume's Parent + # psPlacement: parent solid placement, may be None + # App::Part will have Booleans & Multifuse objects also in the list + # So for s in list is not so good + # xmlVol could be created dummy volume + + # GDMLShared.setTrace(True) + volName = NameManager.getVolumeName(vol) + # GDMLShared.trace("Process Assembly : " + volName) + # if GDMLShared.getTrace() == True : + # printVolumeInfo(vol, xmlVol, xmlParent, parentName) + assemObjs = assemblyHeads(vol) + # print(f"ProcessAssembly: vol.TypeId {vol.TypeId}") + print(f"ProcessAssembly: {vol.Name} Label {vol.Label}") + print(f"Assem Objs {assemObjs}") + # + # Note that the assembly object is under an App::Part, not + # a solid, so there is no need to adjust for a "parent solid" + # placement. + # + for obj in assemObjs: + if obj.TypeId == "App::Part": + processVolAssem(obj, xmlVol, volName, None) + elif obj.TypeId == "App::Link": + print("Process Link") + # PhysVol needs to be unique + if hasattr(obj, "LinkedObject"): + volRef = NameManager.getVolumeName(obj.LinkedObject) + elif hasattr(obj, "VolRef"): + volRef = obj.VolRef + print(f"VolRef {volRef}") + addPhysVolPlacement(obj, xmlVol, obj.Placement, refName=volRef) + physVolStack.append(PhysVolPlacement(volName, obj.Placement)) + elif isArrayType(obj): + processArrayPart(obj, xmlVol) + else: + _ = processVolume(obj, xmlVol, None) + + # the assembly could be placed in a container; adjust + # for its placement, if any, given in the argument + placement = vol.Placement + if psPlacement is not None: + placement = invPlacement(psPlacement) * placement + addPhysVolPlacement(vol, xmlParent, placement) + physVolStack.append(PhysVolPlacement(volName, placement)) + + structure.append(xmlVol) + + +def processVolume(vol, xmlParent, psPlacement): + global structure + global physVolStack + + # vol - Volume Object + # xmlParent - xml of this volume's Parent + # App::Part will have Booleans & Multifuse objects also in the list + # So for s in list is not so good + # type 1 straight GDML type = 2 for GEMC + # xmlVol could be created dummy volume + if vol.TypeId == "App::Link": + print("Volume is Link") + placement = vol.Placement + if psPlacement is not None: + placement = invPlacement(psPlacement) * placement + + addPhysVolPlacement( + vol, + xmlParent, + placement) + return + + volName = NameManager.getVolumeName(vol) + + if vol.TypeId == "App::Part": + topObject = topObj(vol) + else: + topObject = vol + if topObject is None: + return + + if isMultiPlacement(topObject): + xmlVol, volName = processMultiPlacement(topObject, xmlParent) + partPlacement = topObject.Placement + if psPlacement is not None: + partPlacement = invPlacement(psPlacement) * partPlacement + else: + solidExporter = SolidExporter.getExporter(topObject) + if solidExporter is None: + return + solidExporter.export() + print(f"Process Volume - solids count {len(list(solids))}") + # 1- adds a # # - # This is in contrast to assembly, which is exported as + # This is in contract to assembly, which is exported as # # # .... # # Must be assembly first - global obj_top_children_dict + global childObjects if not isAssembly(obj): return False @@ -1483,25 +2503,24 @@ def isAssembly(obj): # N.B. App::Link is treated as a non-assembly, even though it might be linked # to an assembly, because all we need to place it is the volref of its link - global obj_top_children_dict + global childObjects print(f"testing isAsembly for: {obj.Label}") if obj.TypeId != "App::Part": - if (obj.TypeId == "App::Link" and obj.LinkedObject.TypeId != "App::Part"): - return False + return False - for ob in obj_top_children_dict[obj]: - if ob.TypeId == "App::Part" or (ob.TypeId == "App::Link" and ob.LinkedObject.TypeId == "App::Part"): + for ob in childObjects[obj]: + if ob.TypeId == "App::Part" or ob.TypeId == "App::Link": print(True) return True # Yes, even if ONE App::Part is under this, we treat it as an assembly - if len(obj_top_children_dict[obj]) > 1: + if len(childObjects[obj]) > 1: print("Yes, it is an Assembly") return True else: # need to check for arrays. Arrays of App::Part are treated as an assembly - if len(obj_top_children_dict[obj]) == 1: - topObject = obj_top_children_dict[obj][0] + if len(childObjects[obj]) == 1: + topObject = childObjects[obj][0] if isArrayType(topObject) and isArrayOfPart(topObject): return True else: @@ -1514,9 +2533,9 @@ def assemblyHeads(obj): # return a list of subassembly heads for this object # Subassembly heads are themselves either assemblies # or terminal objects (those that produce a \n' + gdml.append(ET.Entity(elemName)) + + +def exportGDMLstructure(dirPath, fileName): + global gdml, docString, importStr + print("Write GDML structure to Directory") + gdml = initGDML() + docString = "\n\n" + # print(docString) + # print(len(docString)) + # gdml = ET.fromstring(docString.encode("UTF-8")) + indent(gdml) + ET.ElementTree(gdml).write( + os.path.join(dirPath, fileName + ".gdml"), + doctype=docString.encode("UTF-8"), + ) + print("GDML file structure written") def exportGDML(first, filepath, fileExt): from . import GDMLShared from sys import platform + from .AssemblyHelper import AssemblyHelper global zOrder + global AssemblyDict + AssemblyDict = {} + AssemblyHelper.maxWww = 0 global usedGeant4Materials usedGeant4Materials = set() - global gdml - global universe_dict - - universe_dict = {} - universe_dict['worlVOL'] = 1 # world volume = root volume = 1 - SurfaceExporter.reset_ids() + global physVolStack + physVolStack = [] # GDMLShared.setTrace(True) GDMLShared.trace("exportGDML") - - print("====> Start OpenMC Export 0.1") + print("====> Start GDML Export 2.0") branch = get_active_branch_name() print(f"branch: {branch}") print("File extension : " + fileExt) GDMLstructure() zOrder = 1 - # TODO: process and export materials processMaterials() exportWorldVol(first, fileExt) - params = FreeCAD.ParamGet( "User parameter:BaseApp/Preferences/Mod/GDML" ) exportG4Materials = params.GetBool('exportG4Materials', False) - + if exportG4Materials: + postCreateGeantMaterials() + processOpticals() # format & write GDML file # xmlstr = ET.tostring(structure) # print('Structure : '+str(xmlstr)) - if fileExt == ".xml": + if fileExt == ".gdml": # indent(gdml) - print(len(list(geometry))) - print("Write to xml file") + print(len(list(solids))) + print("Write to gdml file") # ET.ElementTree(gdml).write(filepath, 'utf-8', True) # ET.ElementTree(gdml).write(filepath, xml_declaration=True) # Problem with pretty Print on Windows ? - combined_file = filepath - geom_file_path = filepath[:-4] + '_geometry.xml' - materials_file_path = filepath[:-4] + '_materials.xml' - openmc = ET.Element('openmc') - openmc.append(materials) - openmc.append(geometry) - - SurfaceExporter.export_surfaces() - CellExporter.export_cells() - if platform == "win32": - indent(geometry) - indent(materials) - ET.ElementTree(materials).write(materials_file_path, xml_declaration=True, encoding='UTF-8') - ET.ElementTree(geometry).write(geom_file_path, xml_declaration=True, encoding='UTF-8') - ET.ElementTree(openmc).write(filepath, xml_declaration=True, encoding='UTF-8') + indent(gdml) + ET.ElementTree(gdml).write(filepath, xml_declaration=True, encoding='UTF-8') else: - ET.ElementTree(materials).write(materials_file_path, pretty_print=True, xml_declaration=True, - encoding='UTF-8') - ET.ElementTree(geometry).write(geom_file_path, pretty_print=True, xml_declaration=True, encoding='UTF-8') - ET.ElementTree(openmc).write(filepath, pretty_print=True, xml_declaration=True, encoding='UTF-8') - print("OpenMC xml files written") - - -def preprocessMaterialIds(): - global obj_top_children_dict - global material_ids + ET.ElementTree(gdml).write( + filepath, pretty_print=True, xml_declaration=True, + encoding='UTF-8' + ) + print("GDML file written") + + if fileExt == ".GDML": + filePath = os.path.split(filepath) + print("Input File Path : " + filepath) + fileName = os.path.splitext(filePath[1])[0] + print("File Name : " + fileName) + dirPath = os.path.join(filePath[0], fileName) + print("Directory Path : " + dirPath) + if os.path.exists(dirPath) is False: + if os.path.isdir(dirPath) is False: + os.makedirs(dirPath) + if os.path.isdir(dirPath) is True: + exportGDMLstructure(dirPath, fileName) + else: + print("Invalid Path") + # change to Qt Warning - material_ids = {} + if fileExt == ".xml": + xmlElem = ET.Element("xml") + xmlElem.append(solids) + xmlElem.append(structure) + indent(xmlElem) + ET.ElementTree(xmlElem).write(filepath) + print("XML file written") - for obj in obj_top_children_dict: - if len(obj_top_children_dict[obj]) == 0: - continue # this item will get exported as a child of some other item - if isContainer(obj) or isAssembly(obj): - pass - elif SolidExporter.isSolid(obj): - solidExporter = SolidExporter.getExporter(obj) +def exportGDMLworld(first, filepath, fileExt): + global childObjects - if solidExporter is None: - pass - else: - material_name = getMaterial(obj) - if material_name not in material_ids: - id = len(material_ids) + 1 - material_ids[material_name] = id + buildDocTree() # creates global childObjects + NameManager.init() + SolidExporter.init() - for child in obj_top_children_dict[obj]: - if isContainer(child) or isAssembly(child): - pass - elif SolidExporter.isSolid(child): - material_name = getMaterial(child) - if material_name not in material_ids: - id = len(material_ids) + 1 - material_ids[material_name] = id + # for debugging doc tree + for obj in childObjects: + s = "" + for child in childObjects[obj]: + s += child.Label + ", " + print(f"{obj.Label} [{s}]") - print(f" Material ids: {material_ids}") + if filepath.lower().endswith(".gdml"): + # GDML Export + print("GDML Export") + # if hasattr(first,'InList') : + # print(len(first.InList)) + vCount, lcount, gCount = countGDMLObj(first) + if gCount > 1: + from .GDMLQtDialogs import showInvalidWorldVol -# ------------------------------ Geometry Objects exporters ------------------------- + showInvalidWorldVol() + else: + exportGDML(first, filepath, fileExt) -from abc import ABC, abstractmethod +def hexInt(f): + return hex(int(f * 255))[2:].zfill(2) -class GeomObjExporter(ABC): - global obj_top_children_dict - def __init__(self, obj): - self.obj = obj +def formatPosition(pos): + s = str(pos[0]) + "*mm " + str(pos[1]) + "*mm " + str(pos[2]) + "*mm" + print(s) + return s - @staticmethod - def getExporter(obj): - ''' - if obj.TypeId == "App::Link": - target = obj.LinkedObject - doc = FreeCAD.ActiveDocument - children = [doc.copyObject(child, True) for child in obj_top_children_dict[target]] - obj_top_children_dict[obj] = children - for child in children: - obj_top_children_dict[child] = [] - if len(children) == 1: - return SingleVolumeExporter(obj) - elif len(children) > 1: - return AssemblyExporter(obj) - else: - return None - ''' - if isContainer(obj): - return ContainerExporter(obj) - elif isAssembly(obj): - return AssemblyExporter(obj) - elif SolidExporter.isSolid(obj): - return SingleSolidExporter(obj) - else: - childObjects = obj_top_children_dict[obj] - if len(childObjects) == 1: - return SingleVolumeExporter(obj) - else: - print(f"Can't find a GeomObjExporter for {obj.Label}") - return +def scanForStl(first, gxml, path, flag): + from .GDMLColourMap import lookupColour + global childObjects - @abstractmethod - def getRegion(self): - ''' get region surrounding this object ''' + # if flag == True ignore Parts that convert + print("scanForStl") + print(first.Name + " : " + first.Label + " : " + first.TypeId) + while switch(first.TypeId): - @abstractmethod - def export(self): - ''' export all surfaces needed and a cell with a material and region for the object ''' + if case("App::Origin"): + # print("App Origin") + return + if case("App::GeoFeature"): + # print("App GeoFeature") + return -class ContainerExporter(GeomObjExporter): - def __init__(self, obj): - super().__init__(obj) - firstChild = obj_top_children_dict[obj][0] + if case("App::Line"): + # print("App Line") + return - if not SolidExporter.isSolid(firstChild): - print(f" Expect a solid for first child of {obj.Label}, but {firstChild.Label} is not") + if case("App::Plane"): + # print("App Plane") return - self.containerSolidExporter = SolidExporter.getExporter(firstChild) - self.children_geom_exporters = [] - for child in obj_top_children_dict[obj][1:]: - geomExporter = GeomObjExporter.getExporter(child) - if geomExporter is None: - print(f" No GeomObjectExporter for {child.Label}. Skipping") - continue - self.children_geom_exporters.append(geomExporter) + break - def getRegion(self): - ''' Note this region is that of the surrounding solid - If this container is contained in another container, the solid of this container should - be subtacted from it's motherregion - ''' - return self.containerSolidExporter.get_region() + if flag is True: + # + # Now deal with objects that map to GDML solids + # + while switch(first.TypeId): + if case("Part::FeaturePython"): + return - def myRegion(self): - ''' - return region occupied by material in this Exporter - This is the solid region for the container minus the region occupied by all its children - ''' - solid_region = self.containerSolidExporter.get_region() - children_region = Region("") - for child_geom_exporter in self.children_geom_exporters: - child_region = child_geom_exporter.getRegion() - if child_region is not None: - children_region = children_region.union(child_region) + if case("Part::Box"): + print(" Box") + return - return solid_region.cut(children_region) + if case("Part::Cylinder"): + print(" Cylinder") + return - def export(self): - # Very ugly way of checking whether we are exporting world volume - worldObject = list(obj_top_children_dict.keys())[0] - if self.obj == worldObject: - self.containerSolidExporter.set_boundary_type("vacuum") + if case("Part::Cone"): + print(" Cone") + return - # Push this container as current parent - parent_stack.append(self.obj) - try: - # Export container's own surfaces - self.containerSolidExporter.export() + if case("Part::Sphere"): + print(" Sphere") + return - # Export all children under this container - for child_geom_exporter in self.children_geom_exporters: - child_geom_exporter.export() - finally: - parent_stack.pop() + break - region = self.myRegion() - # material is that of the containing box - mat_name = getMaterial(self.containerSolidExporter.obj) - cellExporter = CellExporter(self.obj.Label, material_name=mat_name, region=region.expr) - cellExporter.export() + # Deal with Booleans which will have Tool + if hasattr(first, "Tool"): + print(first.TypeId) + scanForStl(first.Base, gxml, path, flag) + scanForStl(first.Tool, gxml, path, flag) + for obj in childObjects[first]: + scanForStl(obj, gxml, path, flag) -class AssemblyExporter(GeomObjExporter): - def __init__(self, obj): - super().__init__(obj) - self.children_geom_exporters = [] - firstChild = obj_top_children_dict[obj][0] - - arrayOfPart = False - if isArrayType(firstChild) and firstChild.Base.isDerivedFrom("App::Part"): - parent_stack.append(self.obj) - processArrayPart(firstChild) - arrayOfPart = True - parent_stack.pop() - - for child in obj_top_children_dict[obj]: - if child is firstChild and arrayOfPart: - continue + if first.TypeId != "App::Part": + if hasattr(first, "Shape"): + print("Write out stl") + print( + "===> Name : " + + first.Name + + " Label : " + + first.Label + + " \ + Type :" + + first.TypeId + + " : " + + str(hasattr(first, "Shape")) + ) + newpath = os.path.join(path, first.Label + ".stl") + print("Exporting : " + newpath) + first.Shape.exportStl(newpath) + # Set Defaults + colHex = "ff0000" + mat = "G4Si" + if hasattr(first.ViewObject, "ShapeColor"): + # print(dir(first)) + col = first.ViewObject.ShapeColor + colHex = hexInt(col[0]) + hexInt(col[1]) + hexInt(col[2]) + print("===> Colour " + str(col) + " " + colHex) + mat = lookupColour(col) + print("Material : " + mat) + if hasattr(first, "Placement"): + print(first.Placement.Base) + pos = formatPosition(first.Placement.Base) + ET.SubElement( + gxml, + "volume", + { + "name": first.Label, + "color": colHex, + "material": mat, + "position": pos, + }, + ) - geomExporter = GeomObjExporter.getExporter(child) - if geomExporter is None: - print(f" No GeomObjectExporter for {child.Label}. Skipping") - continue - self.children_geom_exporters.append(geomExporter) - def getRegion(self): - children_region = Region("") - for child_geom_exporter in self.children_geom_exporters: - children_region = children_region.union(child_geom_exporter.getRegion()) +def exportGXML(first, path, flag): + print("Path : " + path) + # basename = 'target_'+os.path.basename(path) + gxml = ET.Element("gxml") + print("ScanForStl") + scanForStl(first, gxml, path, flag) + # format & write gxml file + indent(gxml) + print("Write to gxml file") + # ET.ElementTree(gxml).write(os.path.join(path,basename+'.gxml')) + ET.ElementTree(gxml).write(os.path.join(path, "target_cad.gxml")) + print("gxml file written") - return children_region - def export(self): - # Push this assembly as current parent - parent_stack.append(self.obj) - try: - for child_geom_exporter in self.children_geom_exporters: - child_geom_exporter.export() - finally: - parent_stack.pop() +def exportMaterials(first, filename): + if filename.lower().endswith(".xml"): + print("Export Materials to XML file : " + filename) + xml = ET.Element("xml") + global define + define = ET.SubElement(xml, "define") + global materials + materials = ET.SubElement(xml, "materials") + processMaterials() + indent(xml) + ET.ElementTree(xml).write(filename) + else: + print("File extension must be xml") -class SingleVolumeExporter(GeomObjExporter): - ''' - A Logical volume of the form: - App::Part - Solid - ''' - global material_ids - - def __init__(self, obj): - super().__init__(obj) - child = obj_top_children_dict[obj][0] - self.child_solid_exporter = SolidExporter.getExporter(child) - if self.child_solid_exporter is None: - if hasattr(child, "TypeId"): - print(f"TpeId {child.TypeId} does not have a SolidExporter") - print(f"{child.Label} has no SolidExporter") - - def getRegion(self): - if self.child_solid_exporter is None: - return - - return self.child_solid_exporter.get_region() - - def export(self): - if self.child_solid_exporter is None: - return - - # Treat this volume (App::Part or App::Link) as the current parent - parent_stack.append(self.obj) - try: - # Inside here, the solid exporter will see self.obj as parent - self.child_solid_exporter.export() - finally: - parent_stack.pop() - - mat_name = getMaterial(self.child_solid_exporter.obj) - solidRegion = self.child_solid_exporter.get_region() - cellExporter = CellExporter(self.obj.Label, - material_name=mat_name, - region=solidRegion.expr) - cellExporter.export() - - -class SingleSolidExporter(GeomObjExporter): - def __init__(self, obj): - super().__init__(obj) - - self.solidExporter = SolidExporter.getExporter(obj) - - def getRegion(self): - return self.solidExporter.get_region() - - def export(self): - if self.solidExporter is None: - print(f"There is no exporter for object {self.obj.Label}") - return - - self.solidExporter.export() - solidRegion = self.solidExporter.get_region() - mat_name = getMaterial(self.obj) - cellExporter = CellExporter(self.obj.Label, material_name=mat_name, region=solidRegion.expr) - cellExporter.export() - - -# ------------------------------ Geometry Objects exporters ------------------------- - -def processDocTree(rootUniverseId): - global obj_top_children_dict - objects = list(obj_top_children_dict.keys()) - worldObj = objects[0] - - geomExporter = GeomObjExporter.getExporter(worldObj) - print(f"{worldObj.Label} {worldObj}") - - geomExporter.export() - - -def exportGDMLworld(first, filepath, fileExt): - global obj_top_children_dict - - buildDocTree() # creates global obj_top_children_dict - NameManager.init() - SolidExporter.init() - - # for debugging doc tree - for obj in obj_top_children_dict: - s = "" - for child in obj_top_children_dict[obj]: - s += child.Label + ", " - print(f"{obj.Label} [{s}]") - - if filepath.lower().endswith(".xml"): - # GDML Export - print("OpenMC Export") - # if hasattr(first,'InList') : - # print(len(first.InList)) - - vCount, lcount, gCount = countGDMLObj(first) - if gCount > 1: - from .GDMLQtDialogs import showInvalidWorldVol - - showInvalidWorldVol() - else: - exportGDML(first, filepath, fileExt) - - -def hexInt(f): - return hex(int(f * 255))[2:].zfill(2) - - -def exportMaterials(first, filename): - if filename.lower().endswith(".xml"): - print("Export Materials to XML file : " + filename) - xml = ET.Element("xml") - global define - define = ET.SubElement(xml, "define") - global materials - materials = ET.SubElement(xml, "materials") - processMaterials() - indent(xml) - ET.ElementTree(xml).write(filename) - else: - print("File extension must be xml") +def exportOpticals(first, filename): + if filename.lower().endswith(".xml"): + print("Export Opticals to XML file : " + filename) + xml = ET.Element("xml") + global define + define = ET.SubElement(xml, "define") + global solids + solids = ET.SubElement(xml, "solids") + processOpticals() + indent(xml) + ET.ElementTree(xml).write(filename) + else: + print("File extension must be xml") def create_gcard(path, flag): @@ -2059,10 +3012,41 @@ def checkDirectory(path): os.mkdir(path) +def exportGEMC(first, path, flag): + # flag = True GEMC - GDML + # flag = False just CAD + global gxml + + print("Export GEMC") + # basename = os.path.basename(path) + print(path) + print(flag) + checkDirectory(path) + # Create CAD directory + cadPath = os.path.join(path, "cad") + checkDirectory(cadPath) + # Create gcard + create_gcard(path, flag) + exportGXML(first, cadPath, flag) + if flag is True: + print("Create GDML directory") + gdmlPath = os.path.join(path, "gdml") + checkDirectory(gdmlPath) + # gdmlFilePath = os.path.join(gdmlPath,basename+'.gdml') + gdmlFilePath = os.path.join(gdmlPath, "target_gdml.gdml") + exportGDML(first, gdmlFilePath, "gdml") + # newpath = os.path.join(gdmlPath,basename+'.gxml') + newpath = os.path.join(gdmlPath, "target_gdml.gxml") + indent(gxml) + ET.ElementTree(gxml).write(newpath) + + def export(exportList, filepath): "called when FreeCAD exports a file" + global refPlacement - print("OpenMC exporter version 0.1") + refPlacement = {} # a dictionary of name as key, and placement as value + # the name could that of {str(replacement_dict[surface_id])}" - region = re.sub(pattern, replacement, region) - self.region = region - - def _export(self): - cell = ET.SubElement(geometry, 'cell') - cell.attrib['name'] = self.name - cell.attrib['id'] = str(self.id) - if self.universe is not None: - cell.attrib['universe'] = str(self.universe) - if self.material is not None: - cell.attrib['material'] = str(self.material) - - if self.region is not None: - cell.attrib['region'] = str(self.region) - - if self.fill is not None: - cell.attrib['fill'] = str(self.fill) - - if self.rotation is not None: - angles = quaternion2XYZ(self.rotation) - cell.attrib['rotation'] = f"{angles[0]} {angles[1]} {angles[2]}" - - if self.translation is not None: - cell.attrib['translation'] = f"{self.translation.x} {self.translation.y} {self.translation.z}" - - def export(self): - CellExporter.cell_cache.append(self) - - -def quadric_coeffs(F, **kwargs): - ''' Given a function F that calculates a surface F(x, y, z, **kwargs) return - the coefficients A, B, C, D, E, F, G, H, J, K of the quadric expression - A x^2 + B y^2 + C z^2 + D xy + E yz + F xz + G x + H y + J z + K - # Suppose one has a means of evaluating F(x, y, z) = A x^2 + B y^2 + C z^2 + - # D xy + E yz + F xz + - # G x + H y + J z + - # K - # Usually this is done by rotating/translating a vector (x', y', z') in which the surface has a simple - # form (say x'2 + y'2) = R^2 for a cylinder surface, to the system in which the axes are rotated/translated - # say, v = R v', where is the 4x4 transformation matrix. - # Now the numerical extraction of the coefficients A, B, C, .... - # K = F(0, 0, 0) - # A = 1/2 (F(1, 0, 0) + F(-1, 0, 0)) - K - # B = 1/2 (F(0, 1, 0) + F(0, -1, 0)) - K - # C = 1/2 (F(0, 0, 1) + F(0, 0, -1)) - K - # G = 1/2 (F(1, 0, 0) - F(-1, 0, 0)) - # H = 1/2 (F(0, 1, 0) - F(0, -1, 0)) - # J = 1/2 (F(0, 0, 1) - F(0, 0, -1)) - # For the cross terms (D, E, F) - # F(1, 1, 0) = A + B + D + G + H + K ==> D = F(1, 1, 0) -A -B -G -H -K - # F(0, 1, 1) = B + C + E + H + J + K ==> E = F(0, 1, 1) -B -C -H -J -K - # F(1, 0, 1) = A + C + F + G + J + K ==> F = F(1, 0, 1) -A -C -G -J -K - ''' - - K = F(0, 0, 0, **kwargs) - A = 1 / 2 * (F(1, 0, 0, **kwargs) + F(-1, 0, 0, **kwargs)) - K - B = 1 / 2 * (F(0, 1, 0, **kwargs) + F(0, -1, 0, **kwargs)) - K - C = 1 / 2 * (F(0, 0, 1, **kwargs) + F(0, 0, -1, **kwargs)) - K - G = 1 / 2 * (F(1, 0, 0, **kwargs) - F(-1, 0, 0, **kwargs)) - H = 1 / 2 * (F(0, 1, 0, **kwargs) - F(0, -1, 0, **kwargs)) - J = 1 / 2 * (F(0, 0, 1, **kwargs) - F(0, 0, -1, **kwargs)) - # For the cross terms (D, E, F) - D = F(1, 1, 0, **kwargs) - A - B - G - H - K - E = F(0, 1, 1, **kwargs) - B - C - H - J - K - F_coeff = F(1, 0, 1, **kwargs) - A - C - G - J - K - - print(A, B, C, D, E, F_coeff, G, H, J, K) - return A, B, C, D, E, F_coeff, G, H, J, K - - -def ellipsoid(x, y, z, ax=1.0, by=1.0, cz=1.0, center=Vector(0, 0, 0), rotation=FreeCAD.Rotation()): - ''' for an ellipsoid center at the origin and axes coinciding with the x, y, and z axes - The equation is x^2/ax^2 + y^2/by^2 + z^2/cz^2 - 1 = 0; - If the origin is translated to x0, y0, z0 the equation becomes - (x-x0)^2 / ax^2 + (y-y0)^2 / by^2 + (z-z0)^2 / cz^2 - 1 = 0; - If the axes are subjected to a rotation R, then the equation becomes - (x'-x0)^2 / ax^2 + (y'-y0)^2 / by^2 + (z'-z0)^2 / cz^2 - 1 = 0; - where (x', y', z') = R^-1 (x, y, z) - ''' - - vprime = rotation.inverted() * (Vector(x, y, z) - center) - - x = vprime.x - y = vprime.y - z = vprime.z - - return (x / ax) ** 2 + (y / by) ** 2 + (z / cz) ** 2 - 1 - - -def cylinder(x, y, z, radius=1, center=Vector(0, 0, 0), axis=Vector(0, 0, 1)): - rotation = FreeCAD.Rotation(Vector(0, 0, 1), axis) - vprime = rotation.inverted() * (Vector(x, y, z) - center) - - x = vprime.x - y = vprime.y - z = vprime.z - - return x * x + y * y - radius * radius - - -def elliptical_tube(x, y, z, dx=1, dy=1, center=Vector(0, 0, 0), rotation=FreeCAD.Rotation()): - R = rotation.inverted() - vprime = R * (Vector(x, y, z) - center) - - x = vprime.x - y = vprime.y - z = vprime.z - - return (x / dx) ** 2 + (y / dy) ** 2 - 1.0 - - -def elliptical_cone(x, y, z, dx=1, dy=1, zHeight=1, center=Vector(0, 0, 0), rotation=FreeCAD.Rotation()): - R = rotation.inverted() - vprime = R * (Vector(x, y, z) - center) - - x = vprime.x - y = vprime.y - z = vprime.z - - return (x / dx) ** 2 + (y / dy) ** 2 - (zHeight - z) ** 2 - - -def cone(x, y, z, center=Vector(0, 0, 0), axis=Vector(0, 0, 1), theta=1.0): - rotation = FreeCAD.Rotation(Vector(0, 0, 1), axis) - vprime = rotation.inverted() * (Vector(x, y, z) - center) - - x = vprime.x - y = vprime.y - z = vprime.z - t = math.tan(theta) - - return x ** 2 + y ** 2 - (z * t) ** 2 - - -def paraboloid(x, y, z, k1, k2, center=Vector(0, 0, 0), rotation=FreeCAD.Rotation()): - vprime = rotation.inverted() * (Vector(x, y, z) - center) - - x = vprime.x - y = vprime.y - z = vprime.z - - return x ** 2 + y ** 2 - (k1 * z + k2) - - -class SurfaceExporter: - _ids = [] # dictionary of name vs id: __ids[name] = id - _ids_dict = {} - cached_surfaces = {} - replacement_ids = {} - - def __init__(self, name, type, coeffs="", boundary="transmission"): - id = 1 - while id in SurfaceExporter._ids: - id += 1 - SurfaceExporter._ids.append(id) - SurfaceExporter._ids_dict[name] = id - # - self.id = id - self.type = type - if name != "": - self.name = name - else: - self.name = f"{type}_{id}" - self.boundary = boundary - self.coeffs = coeffs - - self.saved_state = vars(self).copy() - - def copy(self): - return # should be declared abstract - - @staticmethod - def export_surfaces(): - for key in SurfaceExporter.cached_surfaces: - surface = SurfaceExporter.cached_surfaces[key] - surface._export() - - @staticmethod - def reset_ids(): - SurfaceExporter._ids = [] - SurfaceExporter._ids_dict = {} - SurfaceExporter.cached_surfaces = {} - SurfaceExporter.replacement_ids = {} - - def _export(self): - surface = ET.SubElement(geometry, 'surface') - surface.attrib['id'] = str(self.id) - surface.attrib['name'] = str(self.name) - surface.attrib['type'] = str(self.type) - surface.attrib['coeffs'] = str(self.coeffs) - surface.attrib['boundary'] = self.boundary + elif first.Label == "Opticals": + exportOpticals(first, filepath) - def export(self): - key = self.mykey() - if key not in self.cached_surfaces: - SurfaceExporter.cached_surfaces[key] = self else: - cached_surface = SurfaceExporter.cached_surfaces[key] - SurfaceExporter.replacement_ids[self.id] = cached_surface.id - - def translate(self, T): - return - - def rotate(self, R): - return - - def mykey(self): - s = "" - for w in self.coeffs.strip().split(): - s += f" {float(w):.6e}" - return s - - def __hash__(self): - # hash coefficients to within 1 part in 10^6. This so when we compare two - # surfaces we will consider them to be the same if all the coefficients agree - # to within 1 part in 10^6 - s = [] - for w in self.coeffs.strip().split(): - s.append(float(f" {float(w):.6e}")) - - return hash(tuple(s)) - - def __eq__(self, other): - - # note that if the coefficients match, then we return ==, eventhough the ids might be different - if not (type(self) is type(other)): - return False - - return hash(self) == hash(other) - - -class PlaneSurfaceExporter(SurfaceExporter): - def __init__(self, name: str, normal: FreeCAD.Vector, D: float): - super().__init__(name, "plane") - self.normal = normal - self.D = 0.1 * D / self.normal.Length - self.normal.normalize() - self.to_coeffs() - - def translate(self, T): - T = 0.1 * T # convert from mm to cm for openmc - self.D += self.normal.dot(T) - self.to_coeffs() - - def rotate(self, R): - self.normal = R * self.normal - self.to_coeffs() - - def to_coeffs(self): - self.coeffs = f"{self.normal.x} {self.normal.y} {self.normal.z} {self.D}" - - def inRegion(self, point): - pcm = 0.1 * point - - return self.normal.dot(pcm) - self.D < 0 - - -class SphereSurfaceExporter(SurfaceExporter): - def __init__(self, name, center, radius): - super().__init__(name, "sphere") - self.center = 0.1 * center - self.radius = 0.1 * radius - self.to_coeffs() - self.saved_radius = radius - self.saved_center = center - - def copy(self): - return SphereSurfaceExporter(self.name, self.saved_center, self.saved_radius) - - def to_coeffs(self): - self.coeffs = f"{self.center.x} {self.center.y} {self.center.z} {self.radius}" - - def translate(self, T): - T = 0.1 * T # convert from mm to cm for openmc - self.center += T - self.to_coeffs() - - def rotate(self, R): - self.center = R * self.center - self.to_coeffs() - - -class CylinderSurfaceExporter(SurfaceExporter): - def __init__(self, name, center, axis, radius): - # convert to quadric surface: - super().__init__(name, "quadric") - self.center = 0.1 * center - self.axis = axis - self.radius = 0.1 * radius - self.to_quadric() - self.saved_radius = radius - self.saved_center = center - self.saved_axis = axis - - def copy(self): - return CylinderSurfaceExporter(self.name, self.saved_center, self.saved_axis, self.saved_radius) - - def to_quadric(self): - A, B, C, D, E, F, G, H, J, K = quadric_coeffs(cylinder, radius=self.radius, - center=self.center, axis=self.axis) - - self.coeffs = f"{A} {B} {C} {D} {E} {F} {G} {H} {J} {K}" - - def translate(self, T): - T = 0.1 * T # convert from mm to cm for openmc - self.center += T - self.to_quadric() - - def rotate(self, R): - self.axis = R * self.axis - self.center = R * self.center - self.to_quadric() - - -class EllipticalCylinderSurfaceExporter(SurfaceExporter): - def __init__(self, name, center, dx, dy): - # convert to quadric surface: - super().__init__(name, "quadric") - self.center = 0.1 * center - self.dx = 0.1 * dx - self.dy = 0.1 * dy - self.rotation = FreeCAD.Rotation() - self.to_quadric() - self.saved_dx = dx - self.saved_dy = dy - self.saved_rotation = FreeCAD.Rotation() - self.saved_center = center - - def copy(self): - return EllipticalCylinderSurfaceExporter(self.name, self.saved_center, self.saved_dx, self.saved_dy) - - def to_quadric(self): - A, B, C, D, E, F, G, H, J, K = quadric_coeffs(elliptical_tube, dx=self.dx, dy=self.dy, - center=self.center, rotation=self.rotation) - - self.coeffs = f"{A} {B} {C} {D} {E} {F} {G} {H} {J} {K}" - - def translate(self, T): - T = 0.1 * T # convert from mm to cm for openmc - self.center += T - self.to_quadric() - - def rotate(self, R): - self.rotation = R * self.rotation - self.center = R * self.center - self.to_quadric() - - -class EllipticalConeSurfaceExporter(SurfaceExporter): - def __init__(self, name, center, dx, dy, zHeight): - # convert to quadric surface: - super().__init__(name, "quadric") - self.center = 0.1 * center - self.dx = dx # this is ration, so no conversion from mm to cm - self.dy = dy - self.zHeight = 0.1 * zHeight - self.rotation = FreeCAD.Rotation() - self.to_quadric() - self.saved_center = center - self.saved_dx = dx - self.saved_dy = dy - self.saved_zHeight = zHeight - self.saved_rotation = FreeCAD.Rotation() - - def copy(self): - return EllipticalConeSurfaceExporter(self.name, self.saved_center, self.saved_dx, self.saved_dy, - self.saved_zHeight) - - def to_quadric(self): - A, B, C, D, E, F, G, H, J, K = quadric_coeffs(elliptical_cone, dx=self.dx, dy=self.dy, - zHeight=self.zHeight, - center=self.center, rotation=self.rotation) - - self.coeffs = f"{A} {B} {C} {D} {E} {F} {G} {H} {J} {K}" - - def translate(self, T): - T = 0.1 * T # convert from mm to cm for openmc - self.center += T - self.to_quadric() - - def rotate(self, R): - self.rotation = R * self.rotation - self.center = R * self.center - self.to_quadric() - - -class ConeSurfaceExporter(SurfaceExporter): - def __init__(self, name, center, axis, theta): - ''' - :param: center: cone vertex posidion - :param: axis: unit vector directiom of cone axis - :param: theta: cone half angle, in radians - ''' - # convert to quadric surface: - super().__init__(name, "quadric") - self.center = 0.1 * center - self.axis = axis - self.theta = theta - - self.to_quadric() - - self.saved_center = center - self.saved_axis = axis - self.saved_theta = theta - - def copy(self): - return ConeSurfaceExporter(self.name, self.saved_center, self.saved_axis, self.saved_theta) + print("Export XML structure & solids") + exportGDML(first, filepath, ".xml") - def to_quadric(self): - A, B, C, D, E, F, G, H, J, K = quadric_coeffs(cone, center=self.center, axis=self.axis, theta=self.theta) - self.coeffs = f"{A} {B} {C} {D} {E} {F} {G} {H} {J} {K}" + elif fileExt == ".gemc": + exportGEMC(first, path, False) - def translate(self, T): - T = 0.1 * T # convert from mm to cm for openmc - self.center += T - self.to_quadric() + elif fileExt == ".GEMC": + exportGEMC(first, path, True) - def rotate(self, R): - self.axis = R * self.axis - self.center = R * self.center - self.to_quadric() - - -class EllipsoidSurfaceExporter(SurfaceExporter): - def __init__(self, name, ax, by, cz, center=Vector(0, 0, 0), rotation=FreeCAD.Rotation()): - ''' - :param: center: cone vertex posidion - :param: axis: unit vector directiom of cone axis - :param: theta: cone half angle, in radians - ''' - # convert to quadric surface: - super().__init__(name, "quadric") - self.ax = 0.1 * ax - self.by = 0.1 * by - self.cz = 0.1 * cz - self.center = 0.1 * center - self.rotation = rotation - - self.to_quadric() - self.saved_ax = ax - self.saved_by = by - self.saved_cz = cz - self.saved_center = center - self.saved_rotation = rotation - - def copy(self): - return EllipsoidSurfaceExporter(self.name, self.saved_ax, self.saved_by, self.saved_cz, self.saved_center, - self.saved_rotation) - - def to_quadric(self): - A, B, C, D, E, F, G, H, J, K = quadric_coeffs(ellipsoid, ax=self.ax, by=self.by, cz=self.cz, - center=self.center, rotation=self.rotation) - self.coeffs = f"{A} {B} {C} {D} {E} {F} {G} {H} {J} {K}" - - def translate(self, T): - T = 0.1 * T # convert from mm to cm for openmc - self.center += T - self.to_quadric() - - def rotate(self, R): - self.center = R * self.center # is this needed - self.rotation = R * self.rotation - self.to_quadric() - - -class ParaboloidSurfaceExporter(SurfaceExporter): - def __init__(self, name, rlo, rhi, dz, center=Vector(0, 0, 0), rotation=FreeCAD.Rotation()): - ''' - Equation of paraboloid is given by - (x^2+y^2) - rho^2 = k1 z + k2 - :param: rlo: k1 * (-dz) + k2; value of rho at z=-dz - :param: rhi: k1 * (+dz) + k2 value of rho at z=+dz - :param: dx: half height of parbolid - :param: center: center of paraboloid - :param: rotation: rotaion of axis from z-axis - ''' - # Equation of Paraboloid in geant4 is: - # (x^2+y^2) = rho^2 - k1 z + k2 - # k1 and k2 determined from - # rlo^2 = k1 *(-dz) + k2 - # rhi^2 = k1 *(+dz) + k2 - # ==> k1 = (rhi^2 - rlo^2)/2 dz - # ==> k2 = (rlo^2+rhi^2)/2 - - # convert to quadric surface: - super().__init__(name, "quadric") - - dz *= 0.1 # convert to cm - rlo *= 0.1 - rhi *= 0.1 - self.k1 = (rhi * rhi - rlo * rlo) / (2 * dz) - self.k2 = (rhi * rhi + rlo * rlo) / 2 - self.center = 0.1 * center - self.rotation = rotation - - self.to_quadric() - - self.saved_rlo = rlo - self.saved_rhi = rhi - self.saved_dz = dz - self.saved_center = center - self.saved_rotation = rotation - - def copy(self): - return ParaboloidSurfaceExporter(self.name, self.saved_rlo, self.saved_rhi, self.saved_dz, self.saved_center, - self.saved_rotation) - - def to_quadric(self): - A, B, C, D, E, F, G, H, J, K = quadric_coeffs(paraboloid, k1=self.k1, k2=self.k2, - center=self.center, rotation=self.rotation) - self.coeffs = f"{A} {B} {C} {D} {E} {F} {G} {H} {J} {K}" - - def translate(self, T): - T = 0.1 * T # convert from mm to cm for openmc - self.center += T - self.to_quadric() - - def rotate(self, R): - self.center = R * self.center # is this needed - self.rotation = R * self.rotation - self.to_quadric() +# +# ------------------------------------------------------------------------------------------------------- +# class SolidExporter: # Abstract class to export object as gdml _exported = [] # a list of already exported objects solidExporters = { - "GDMLArb8": "AutoTessellateExporter", + "GDMLArb8": "GDMLArb8Exporter", "GDMLBox": "GDMLBoxExporter", "GDMLCone": "GDMLConeExporter", "GDMLcutTube": "GDMLcutTubeExporter", @@ -2703,35 +3116,34 @@ class SolidExporter: "GDMLElTube": "GDMLElTubeExporter", "GDMLHype": "GDMLHypeExporter", "GDMLOrb": "GDMLOrbExporter", - "GDMLPara": "AutoTessellateExporter", + "GDMLPara": "GDMLParaExporter", "GDMLParaboloid": "GDMLParaboloidExporter", "GDMLPolycone": "GDMLPolyconeExporter", "GDMLGenericPolycone": "GDMLGenericPolyconeExporter", - "GDMLPolyhedra": "AutoTessellateExporter", - "GDMLGenericPolyhedra": "AutoTessellateExporter", + "GDMLPolyhedra": "GDMLPolyhedraExporter", + "GDMLGenericPolyhedra": "GDMLGenericPolyhedraExporter", "GDMLSphere": "GDMLSphereExporter", - "GDMLTessellated": "AutoTessellateExporter", - "GDMLSampledTessellated": "AutoTessellateExporter", - "GDMLGmshTessellated": "AutoTessellateExporter", - "GDMLTetra": "AutoTessellateExporter", - "GDMLTetrahedron": "AutoTessellateExporter", + "GDMLTessellated": "GDMLTessellatedExporter", + "GDMLSampledTessellated": "GDMLSampledTessellatedExporter", + # Use the GDMLTessellated exporter", + "GDMLGmshTessellated": "GDMLTessellatedExporter", + "GDMLTetra": "GDMLTetraExporter", + "GDMLTetrahedron": "GDMLTetrahedronExporter", "GDMLTorus": "GDMLTorusExporter", - "GDMLTrap": "AutoTessellateExporter", - "GDMLTrd": "AutoTessellateExporter", + "GDMLTrap": "GDMLTrapExporter", + "GDMLTrd": "GDMLTrdExporter", "GDMLTube": "GDMLTubeExporter", "GDMLTwistedbox": "GDMLTwistedboxExporter", "GDMLTwistedtrap": "GDMLTwistedtrapExporter", "GDMLTwistedtrd": "GDMLTwistedtrdExporter", "GDMLTwistedtubs": "GDMLTwistedtubsExporter", - "GDMLXtru": "AutoTessellateExporter", - "Mesh::Feature": "AutoTessellateExporter", + "GDMLXtru": "GDMLXtruExporter", + "Mesh::Feature": "GDMLMeshExporter", "Part::MultiFuse": "MultiFuseExporter", - "Part::MultiCommon": "MultiCommonExporter", "Part::Extrusion": "ExtrusionExporter", "Part::Revolution": "RevolutionExporter", "Part::Box": "BoxExporter", "Part::Cylinder": "CylinderExporter", - "Part::Torus": "TorusExporter", "Tube": "TubeExporter", "Part::Cone": "ConeExporter", "Part::Sphere": "SphereExporter", @@ -2750,52 +3162,38 @@ def init(): @staticmethod def isSolid(obj): - """Return True for objects whose primary identity is their own Shape.""" print(f"isSolid {obj.Label}") # return hasattr(obj, 'Shape') # does not work. App::Parts have Shape, but they are not solids! obj1 = obj if obj.TypeId == "App::Link": obj1 = obj.LinkedObject - - if not hasattr(obj, "Shape") or obj.Shape.isNull(): - return False - - # Exclude known containers / special cases - if obj.isDerivedFrom("App::Part"): - return False # container, aggregate shape - - if obj.isDerivedFrom("PartDesign::Body"): - # optional: treat Body as container rather than primitive - return False - - # we tread arrays of parts as containers, not as solids - if isArrayType(obj1) and obj1.Base.isDerivedFrom("App::Part"): - return False - - # Part::Compound is a grey zone: - # treat it as "real shape" or as aggregate depending on your needs - # Example: treat as aggregate and skip here: - if obj.TypeId in ("Part::Compound", "Part::CompoundPython"): - return False - - # Everything else derived from Part::Feature is fair game - if obj.isDerivedFrom("Part::Feature"): - return True - if obj1.TypeId == "Part::FeaturePython": - if hasattr(obj, 'Shape') and not obj.Shape.isNull(): + return True # All Part::FeturePython have a 'Shape', and a Shape can be tessellated + ''' + typeId = obj1.Proxy.Type + if typeId == "Array": + if obj1.ArrayType == "ortho": + return True + elif obj1.ArrayType == "polar": + return True + elif typeId == "PathArray": + return True + elif typeId == "PointArray": return True + elif typeId == "Clone": + clonedObj = obj1.Objects[0] + return SolidExporter.isSolid(clonedObj) - return False + else: + return obj1.Proxy.Type in SolidExporter.solidExporters + ''' + + else: + return obj1.TypeId in SolidExporter.solidExporters @staticmethod def getExporter(obj): - if hasattr(obj, 'LinkedObject'): - solidExporter = SolidExporter.getExporter(obj.LinkedObject) - if solidExporter is not None: - return type(solidExporter)(obj) - if obj.TypeId == "Part::FeaturePython": if hasattr(obj.Proxy, 'Type'): typeId = obj.Proxy.Type @@ -2835,39 +3233,23 @@ def getExporter(obj): elif obj.TypeId == "Part::FeaturePython": # This may appear to be duplication of above, but # we need to pass through all the specialized exporters # before we fall back to tessellation - if hasattr(obj, 'Shape') and not obj.Shape.isNull(): - print(f"{obj.Label} does not have a Native Solid Exporter, Using AutoTessellator") - return AutoTessellateExporter(obj) + return AutoTessellateExporter(obj) else: - if hasattr(obj, 'Shape') and not obj.Shape.isNull(): - print(f"{obj.Label} does not have a native Solid Exporter, Using AutoTessellator") - return AutoTessellateExporter(obj) - - return None + print(f"{obj.Label} does not have a Solid Exporter") + return None def __init__(self, obj): self.obj = obj self._name = NameManager.getName(obj) - self.region = None - self.surfaces = [] - - def copy(self): - return SolidExporter(self.obj) - - def generate_surfaces(self): - ''' build the list of surfaces. Implemented by implementors''' - return def name(self): return self._name def position(self): - # return self.obj.Placement.Base we ar expect each solid to translate/rotate its surfaces - return FreeCAD.Vector() + return self.obj.Placement.Base def rotation(self): - return FreeCAD.Rotation() - # return self.obj.Placement.Rotation. we expect objects to rotate their surfaces + return self.obj.Placement.Rotation def placement(self): return FreeCAD.Placement(self.position(), self.rotation()) @@ -2876,78 +3258,43 @@ def exported(self): return self.obj in SolidExporter._exported def export(self): - if self.region is None: - self.generate_surfaces() - if hasattr(self.obj, "BoundaryType"): - boundary_type = self.obj.BoundaryType - self.set_boundary_type(boundary_type) - - for surf in self.surfaces: - surf.export() + if not self.exported(): + SolidExporter._exported.append(self.obj) return - def rotate(self, R): - for surf in self.surfaces: - surf.rotate(R) - - def translate(self, T): - for surf in self.surfaces: - surf.translate(T) - - def get_region(self): - if self.region is None: - self.generate_surfaces() - return self.region - - def set_boundary_type(self, boundary_type): - if self.region is None: - self.generate_surfaces() - for surf in self.surfaces: - surf.boundary = boundary_type - - def position_globally(self): - identity = FreeCAD.Placement() + def hasScale(self): + return hasattr(self.obj, "scale") or hasattr(self.obj, "Scale") - placement = get_global_placement(self.obj) - - if placement != identity: - rot = placement.Rotation - trans = placement.Base - for surf in self.surfaces: - surf.rotate(rot) - surf.translate(trans) - - def getMult(self): - ''' return multiplier for length units of self.obj''' - unit = "mm" # set default - # Watch for unit and lunit - # print('getMult : '+str(fp)) - if hasattr(self.obj, "lunit"): - unit = self.obj.lunit - elif hasattr(self.obj, "unit"): - unit = self.obj.unit - elif hasattr(self.obj, "attrib"): - if "unit" in self.obj.attrib: - unit = self.obj.attrib["unit"] - elif "lunit" in self.obj.attrib: - unit = self.obj.attrib["lunit"] + def getScale(self): + if hasattr(self.obj, "ScaleVector"): + return self.obj.ScaleVector + elif hasattr(self.obj, "scale"): + return self.obj.scale + elif hasattr(self.obj, "Scale"): + return self.obj.Scale else: - return 1 - - # The exporters will convert to cm. Here we convert to mm - unitsDict = { - "mm": 1, - "cm": 10.0, - "m": 1000., - "um": 0.001, - "nm": 1.0e-6, - # "dm": 100, decimeter not recognized by geant - "km": 1000000.0, - } - if unit in unitsDict: - return unitsDict[unit] + return FreeCAD.vector(1, 1, 1) - print("unit not handled : " + unit) + def _exportScaled(self): + if self.hasScale(): + scale = self.getScale() + if scale.x == 1.0 and scale.y == 1.0 and scale.z == 1.0: + return + xml = ET.SubElement( + solids, "scaledSolid", {"name": self._name + "_scaled"} + ) + ET.SubElement(xml, "solidref", {"ref": self.name()}) + ET.SubElement( + xml, + "scale", + { + "name": self.name() + "_scale", + "x": str(scale.x), + "y": str(scale.y), + "z": str(scale.z), + }, + ) + self._name += "_scaled" class CloneExporter(SolidExporter): @@ -3074,238 +3421,182 @@ def _export1(self, clonedObj): self._position = placement.Base self._rotation = placement.Rotation self._name = exporter.name() + self._exportScaled() class BoxExporter(SolidExporter): def __init__(self, obj): super().__init__(obj) - def generate_surfaces(self): - self.surfaces = [] - placement = self.obj.Placement - - normal = Vector(1, 0, 0) - D = 0 - x1surface = PlaneSurfaceExporter(f"{self.obj.Label}_x1_plane", normal, D) - D = self.obj.Length.Value - x2surface = PlaneSurfaceExporter(f"{self.obj.Label}_x2_plane", normal, D) - - normal = Vector(0, 1, 0) - D = 0 - y1surface = PlaneSurfaceExporter(f"{self.obj.Label}_y1_plane", normal, D) - D = self.obj.Width.Value - y2surface = PlaneSurfaceExporter(f"{self.obj.Label}_y2_plane", normal, D) - - normal = Vector(0, 0, 1) - D = 0 - z1surface = PlaneSurfaceExporter(f"{self.obj.Label}_z1_plane", normal, D) - D = self.obj.Height.Value - z2surface = PlaneSurfaceExporter(f"{self.obj.Label}_z2_plane", normal, D) + def export(self): + if self.exported(): + return + super().export() - self.surfaces = [x1surface, x2surface, y1surface, y2surface, z1surface, z2surface] - self.region = Region( - f"+{x1surface.id} -{x2surface.id} +{y1surface.id} -{y2surface.id} +{z1surface.id} -{z2surface.id}") + ET.SubElement( + solids, + "box", + { + "name": self.name(), + "x": str(self.obj.Length.Value), + "y": str(self.obj.Width.Value), + "z": str(self.obj.Height.Value), + "lunit": "mm", + }, + ) + self._exportScaled() - self.position_globally() + def position(self): + delta = FreeCAD.Vector( + self.obj.Length.Value / 2, + self.obj.Width.Value / 2, + self.obj.Height.Value / 2, + ) + # Part::Box has its origin at the corner + # gdml box has its origin at the center + # In FC, rotations are about corner. In GDML about + # center. The following gives correct position of center + # of exported cube + pos = self.obj.Placement.Base + self.obj.Placement.Rotation * delta + return pos class CylinderExporter(SolidExporter): def __init__(self, obj): super().__init__(obj) - def generate_surfaces(self): - normal = Vector(0, 0, 1) - D = self.obj.Height.Value - top_surface = PlaneSurfaceExporter(f"{self.obj.Label}_top", normal, D) - D = 0 - bottom_surface = PlaneSurfaceExporter(f"{self.obj.Label}_bot", normal, D) - - axis = Vector(0, 0, 1) - center = Vector(0, 0, 0) - radius = self.obj.Radius.Value - cylinder_surface = CylinderSurfaceExporter(f"{self.name()}_cylinder", center, axis, radius) + def export(self): + if self.exported(): + return + super().export() - self.surfaces = [top_surface, bottom_surface, cylinder_surface] - self.region = Region(f"-{cylinder_surface.id} +{bottom_surface.id} -{top_surface.id}") + # Needs unique Name + # This is for non GDML cylinder/tube + ET.SubElement( + solids, + "tube", + { + "name": self.name(), + "rmax": str(self.obj.Radius.Value), + "deltaphi": str(float(self.obj.Angle.Value)), + "aunit": "deg", + "z": str(self.obj.Height.Value), + "lunit": "mm", + }, + ) + self._exportScaled() - self.position_globally() + def position(self): + delta = FreeCAD.Vector(0, 0, self.obj.Height.Value / 2) + # see comments in BoxExporter + pos = self.obj.Placement.Base + self.obj.Placement.Rotation * delta + return pos class TubeExporter(SolidExporter): def __init__(self, obj): super().__init__(obj) - def generate_surfaces(self): - normal = Vector(0, 0, 1) + def export(self): + if self.exported(): + return + super().export() - D = self.obj.Height.Value - top_surface = PlaneSurfaceExporter(f"{self.obj.Label}_top", normal, D) - D = 0 - bottom_surface = PlaneSurfaceExporter(f"{self.obj.Label}_bot", normal, D) + # Needs unique Name + # This is for non GDML cylinder/tube + ET.SubElement( + solids, + "tube", + { + "name": self.name(), + "rmin": str(self.obj.InnerRadius.Value), + "rmax": str(self.obj.OuterRadius.Value), + "deltaphi": str(360), + "aunit": "deg", + "z": str(self.obj.Height.Value), + "lunit": "mm", + }, + ) + self._exportScaled() - axis = normal - center = Vector(0, 0, 0) - if self.obj.InnerRadius != 0: - radius = self.obj.InnerRadius.Value - inner_surface = CylinderSurfaceExporter(f"{self.name()}_ir", center, axis, radius) - else: - inner_surface = None + def position(self): + delta = FreeCAD.Vector(0, 0, self.obj.Height.Value / 2) + # see comments in BoxExporter + pos = self.obj.Placement.Base + self.obj.Placement.Rotation * delta + return pos - radius = self.obj.OuterRadius.Value - outer_surface = CylinderSurfaceExporter(f"{self.name()}_or", center, axis, radius) - self.surfaces = [top_surface, bottom_surface, outer_surface] - if inner_surface is not None: - self.surfaces.append(inner_surface) +class ConeExporter(SolidExporter): + def __init__(self, obj): + super().__init__(obj) - region_expr = f"-{top_surface.id} +{bottom_surface.id} -{outer_surface.id}" - if inner_surface is not None: - region_expr += f" +{inner_surface.id}" + def export(self): + if self.exported(): + return + super().export() - self.region = Region(region_expr) + ET.SubElement( + solids, + "cone", + { + "name": self.name(), + "rmax1": str(self.obj.Radius1.Value), + "rmax2": str(self.obj.Radius2.Value), + "deltaphi": str(float(self.obj.Angle.Value)), + "aunit": "deg", + "z": str(self.obj.Height.Value), + "lunit": "mm", + }, + ) + self._exportScaled() - self.position_globally() + def position(self): + # Adjustment for position in GDML + delta = FreeCAD.Vector(0, 0, self.obj.Height.Value / 2) + # see comments in BoxExporter + pos = self.obj.Placement.Base + self.obj.Placement.Rotation * delta + return pos -class GDMLElTubeExporter(SolidExporter): +class SphereExporter(SolidExporter): def __init__(self, obj): super().__init__(obj) - def generate_surfaces(self): - axis = Vector(0, 0, 1) - mul = self.getMult(self.obj) - - D = mul * self.obj.dz - center = Vector(0, 0, 0) - - name = NameManager.getName(self.obj) - top_surface = PlaneSurfaceExporter(f"{name}_top", axis, D) - bottom_surface = PlaneSurfaceExporter(f"{name}_bot", axis, -D) - - dx = mul * self.obj.dx - dy = mul * self.obj.dy - surf = EllipticalCylinderSurfaceExporter(f"{name}", center, dx, dy) + def export(self): + if self.exported(): + return + super().export() - self.surfaces = [top_surface, bottom_surface, surf] - self.region = Region(f"-{surf.id} +{bottom_surface.id} -{top_surface.id}") + ET.SubElement( + solids, + "sphere", + { + "name": self.name(), + "rmax": str(self.obj.Radius.Value), + "starttheta": str(90.0 - float(self.obj.Angle2.Value)), + "deltatheta": str( + float(self.obj.Angle2.Value - self.obj.Angle1.Value) + ), + "deltaphi": str(float(self.obj.Angle3.Value)), + "aunit": "deg", + "lunit": "mm", + }, + ) + self._exportScaled() - self.position_globally() + def position(self): + # see comments in processBoxObject + unrotatedpos = self.obj.Placement.Base + pos = self.obj.Placement.Rotation * unrotatedpos + return pos -class ConeExporter(SolidExporter): - def __init__(self, obj): - super().__init__(obj) - - def generate_surfaces(self): - - # Cone half angle - r1 = self.obj.Radius1.Value - r2 = self.obj.Radius2.Value - h = self.obj.Height.Value - theta = math.atan(abs(r2 - r1) / h) - if r1 > r2: - axis = Vector(0, 0, 1) - center = Vector(0, 0, r1 / math.tan(theta)) - surface = ConeSurfaceExporter(self.name(), center, axis, theta) - bottom_plane = PlaneSurfaceExporter(self.name() + '_bot', axis, 0) - top_plane = PlaneSurfaceExporter(self.name() + '_top', axis, h) - elif r2 > r1: - axis = Vector(0, 0, -1) - center = Vector(0, 0, -r1 / math.tan(theta)) - surface = ConeSurfaceExporter(self.name(), center, axis, theta) - bottom_plane = PlaneSurfaceExporter(self.name() + '_bot', axis, 0) - top_plane = PlaneSurfaceExporter(self.name() + '_top', axis, h) - else: - center = Vector(0, 0, 0) - axis = Vector(0, 0, 1) - surface = ConeSurfaceExporter(self.name(), center, axis, r1) - bottom_plane = PlaneSurfaceExporter(self.name() + '_bot', axis, 0) - top_plane = PlaneSurfaceExporter(self.name() + '_top', axis, h) - - self.surfaces = [surface, bottom_plane, top_plane] - self.region = Region(f'-{surface.id} +{bottom_plane.id} -{top_plane.id}') - - angle = self.obj.Angle.Value - if angle != 360: - phi0_normal = Vector(0, 1, 0) - phi0_plane = PlaneSurfaceExporter(self.name() + '_phi0', phi0_normal, 0) - - phi1_normal = Vector(math.cos(math.radians(angle + 90)), math.sin(math.radians(angle + 90)), 0) - phi1_plane = PlaneSurfaceExporter(self.name() + '_phi0', phi1_normal, 0) - self.surfaces += [phi0_plane, phi1_plane] - - if phi0_normal.cross(phi1_normal).z > 0: - self.region = Region( - f'-{surface.id} +{bottom_plane.id} -{top_plane.id} +{phi0_plane.id} -{phi1_plane.id}') - else: - self.region = Region( - f'-{surface.id} +{bottom_plane.id} -{top_plane.id} (+{phi0_plane.id} | -{phi1_plane.id})') - - self.position_globally() - - -class SphereExporter(SolidExporter): - def __init__(self, obj): - super().__init__(obj) - - def generate_surfaces(self): - self.surfaces = [] - center = Vector(0, 0, 0) # Note off -center spheres are handled by their position placement - radius = self.obj.Radius.Value - sphere_surface = SphereSurfaceExporter(f"{self.name()}_surf", center, radius) - self.surfaces.append(sphere_surface) - - if self.obj.Angle1 != 0: - z1 = self.obj.Radius * math.sin(math.radians(self.obj.Angle1)) - normal = Vector(0, 0, 1) - D = z1 - z1surface = PlaneSurfaceExporter(f"{self.obj.Label}_z1_plane", normal, D) - self.surfaces.append(z1surface) - - if self.obj.Angle2 != 0: - z2 = self.obj.Radius * math.sin(math.radians(self.obj.Angle2)) - normal = Vector(0, 0, 1) - D = z2 - z2surface = PlaneSurfaceExporter(f"{self.obj.Label}_z2_plane", normal, D) - self.surfaces.append(z2surface) - - if self.obj.Angle3 != 360: - phi0_normal = Vector(0, 1, 0) - D = 0 - phi0_plane = PlaneSurfaceExporter(f"{self.obj.Label}_pho0_plane", phi0_normal, D) - self.surfaces.append(phi0_plane) - - nx = -math.sin(math.radians(self.obj.Angle3)) - ny = math.cos(math.radians(self.obj.Angle3)) - phi1_normal = Vector(nx, ny, 0) - - phi1_plane = PlaneSurfaceExporter(f"{self.obj.Label}_pho1_plane", phi1_normal, D) - self.surfaces.append(phi1_plane) - - region = f"-{sphere_surface.id}" - if self.obj.Angle1 != 0: - region += f" +{z1surface.id}" - if self.obj.Angle2 != 0: - region += f" -{z2surface.id}" - - if self.obj.Angle3 != 360: - if phi0_normal.cross(phi1_normal).z > 0: - region += f" +{phi0_plane.id} -{phi1_plane.id}" - else: - region += f"(+{phi0_plane.id} | -{phi1_plane.id})" - - self.region = Region(region) - - self.position_globally() - - -class BooleanExporter(SolidExporter): +class BooleanExporter(SolidExporter): def __init__(self, obj): super().__init__(obj) baseExporter = SolidExporter.getExporter(self.obj.Base) basePlacement = baseExporter.placement() self._placement = self.obj.Placement * basePlacement - # self._placement = get_global_placement(obj) # according to GPT-5.1. Does not work! def isBoolean(self, obj): id = obj.TypeId @@ -3332,8 +3623,7 @@ def rotation(self): def placement(self): return self._placement - def generate_surfaces(self): - self.surfaces = [] + def export(self): """ In FreeCAD doc booleans that are themselves composed of other booleans are listed in sequence, eg: @@ -3364,6 +3654,10 @@ def generate_surfaces(self): Then outputs them to gdml in reverse order. In the process of scanning for booleans, the Nonbooleans are exported """ + GDMLShared.trace("Process Boolean Object") + if self.exported(): + return + super().export() obj = self.obj boolsList = [obj] # list of booleans that are part of obj @@ -3379,1367 +3673,680 @@ def generate_surfaces(self): if self.isBoolean(boolobj.Base): tmpList.append(boolobj.Base) boolsList.append(boolobj.Base) + else: + solidExporter.export() solidExporter = SolidExporter.getExporter(boolobj.Tool) ref2[boolobj] = solidExporter if self.isBoolean(boolobj.Tool): tmpList.append(boolobj.Tool) boolsList.append(boolobj.Tool) + else: + solidExporter.export() # Now tmpList is empty and boolsList has list of all booleans - self.region = Region("") - self.surfaces = [] for boolobj in reversed(boolsList): operation = self.boolOperation(boolobj) if operation is None: continue solidName = boolobj.Label - solidExporter1 = ref1[boolobj] - solidExporter2 = ref2[boolobj] - region1 = solidExporter1.get_region() # this will generate the surfaces - region2 = solidExporter2.get_region() # this will generate the surfaces - - self.surfaces += solidExporter1.surfaces - self.surfaces += solidExporter2.surfaces - - if operation == 'union': - self.region = region1.union(region2) - elif operation == 'subtraction': - self.region = region1.cut(region2) - elif operation == 'intersection': - self.region = region1.intersection(region2) - - if self._placement != FreeCAD.Placement(): - translation = self._placement.Base - rotation = self._placement.Rotation - for surf in self.surfaces: - surf.rotate(rotation) - surf.translate(translation) - - -class GDMLBoxExporter(SolidExporter): - def __init__(self, obj): - super().__init__(obj) - - def generate_surfaces(self): - mul = self.getMult() - normal = Vector(1, 0, 0) - D = -self.obj.x / 2 * mul - x1surface = PlaneSurfaceExporter(f"{self.obj.Label}_x1_plane", normal, D) - D = self.obj.x / 2 * mul - x2surface = PlaneSurfaceExporter(f"{self.obj.Label}_x2_plane", normal, D) - - normal = Vector(0, 1, 0) - D = -self.obj.y / 2 * mul - y1surface = PlaneSurfaceExporter(f"{self.obj.Label}_y1_plane", normal, D) - D = self.obj.y / 2 * mul - y2surface = PlaneSurfaceExporter(f"{self.obj.Label}_y2_plane", normal, D) - - normal = Vector(0, 0, 1) - D = -self.obj.z / 2 * mul - z1surface = PlaneSurfaceExporter(f"{self.obj.Label}_z1_plane", normal, D) - D = self.obj.z / 2 * mul - z2surface = PlaneSurfaceExporter(f"{self.obj.Label}_z2_plane", normal, D) - - self.surfaces = [x1surface, x2surface, y1surface, y2surface, z1surface, z2surface] - self.region = Region( - f"+{x1surface.id} -{x2surface.id} +{y1surface.id} -{y2surface.id} +{z1surface.id} -{z2surface.id}") - - self.position_globally() + boolXML = ET.SubElement( + solids, str(operation), {"name": solidName} + ) + ET.SubElement(boolXML, "first", {"ref": ref1[boolobj].name()}) + ET.SubElement(boolXML, "second", {"ref": ref2[boolobj].name()}) + # process position & rotation + # Note that only the second item in the boolean (the Tool in FC parlance) + # gets a position and a rotation. But these are relative to the + # first. So convolve placement of second with inverse placement of first + placementFirst = ref1[boolobj].placement() + placementSecond = invPlacement(placementFirst) * ref2[boolobj].placement() + rot = placementSecond.Rotation + pos = placementSecond.Base # must also rotate position + toolObj = ref2[boolobj].obj # the tool object of the boolean + if placementFirst == FreeCAD.Placement(): # we give up on expressions, unless 1st object (Base( has no placement + xexpr = GDMLShared.getPropertyExpression(toolObj, '.Placement.Base.x') + yexpr = GDMLShared.getPropertyExpression(toolObj, '.Placement.Base.y') + zexpr = GDMLShared.getPropertyExpression(toolObj, '.Placement.Base.z') + pos = (xexpr, yexpr, zexpr) + + exportPosition(toolObj.Name, boolXML, pos) + # For booleans, gdml want actual rotation, not reverse + # processRotation export negative of rotation angle(s) + exportRotation(toolObj.Name, boolXML, rot, invertRotation=False) + self._exportScaled() -class GDMLConeExporter(SolidExporter): - def __init__(self, obj): +class GDMLSolidExporter(SolidExporter): + def __init__(self, obj, tag, propertyList=None): super().__init__(obj) + self._name = NameManager.getName(obj) + self.propertyList = propertyList + self.tag = tag - def generate_surfaces(self): - from .GDMLObjects import checkFullCircle, getAngleRad - def cone_surface(name_suffix, r1, r2, h): - theta = math.atan(abs(r1 - r2) / h) - if abs(r1 - r2) < 1.0e-9: # a cylinder, not a cone - return CylinderSurfaceExporter(self.name, Vector(0, 0, 0), Vector(0, 0, 1), r1) - - if r1 > r2: # cone base at bottom, cone vertex at top - H = r1 / math.tan(theta) # Height of cone, from base to vertex - axis = Vector(0, 0, 1) # has to be z-axis for rotations to work correcly - center = (H - h / 2) * Vector(0, 0, 1) # center is location of vertex - return ConeSurfaceExporter(name + name_suffix, center, axis, theta) - else: # r1 0: - region_expr += f"+{phi0_plane.id} -{phi1_plane.id}" - else: - region_expr += f"(+{phi0_plane.id} | -{phi1_plane.id})" - - self.region = Region(region_expr) - self.position_globally() + def name(self): + return self._name + def export(self): + if self.exported(): + return + super().export() -class GDMLcutTubeExporter(SolidExporter): - def __init__(self, obj): - super().__init__(obj) + if self.propertyList is None: + return # presumably the child will do its own export in that case - def generate_surfaces(self): - from .GDMLObjects import checkFullCircle, getAngleRad - - region_expr = "" - axis = Vector(0, 0, 1) - mul = self.getMult() - center = Vector(0, 0, 0) - radius = self.obj.rmin * mul - if radius != 0: - surf = CylinderSurfaceExporter(f"{self.name()}_ir", center, axis, radius) - self.surfaces.append(surf) - region_expr += f"+{surf.id} " - - radius = self.obj.rmax * mul - surf = CylinderSurfaceExporter(f"{self.name()}_or", center, axis, radius) - self.surfaces.append(surf) - region_expr += f"-{surf.id} " - - normal_top = Vector(self.obj.highX, self.obj.highY, self.obj.highZ) - normal_top.normalize() - D_top = Vector(0, 0, mul * self.obj.z / 2).dot(normal_top) - surf = PlaneSurfaceExporter(f"{self.name()}_top", normal_top, D_top) - self.surfaces.append(surf) - region_expr += f"-{surf.id} " - - normal_bot = Vector(self.obj.lowX, self.obj.lowY, self.obj.lowZ) - normal_bot.normalize() - # D_bot = mul*self.obj.z/2 # not minus because the normal at the bottom points away from the bottom - # this is better calculated as Vector(0, 0, -z/2).dot(nromal) - D_bot = Vector(0, 0, -mul * self.obj.z / 2).dot(normal_bot) - surf = PlaneSurfaceExporter(f"{self.name()}_bot", normal_bot, D_bot) - self.surfaces.append(surf) - region_expr += f"-{surf.id} " - - if not checkFullCircle(self.obj.aunit, self.obj.deltaphi): - startPhi = getAngleRad(self.obj.aunit, self.obj.startphi) - nx = -math.sin(startPhi) - ny = math.cos(startPhi) - phi0_normal = Vector(nx, ny, 0) - D = 0 - phi0_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi0_plane", phi0_normal, D) - self.surfaces.append(phi0_plane) - - deltaphi = getAngleRad(self.obj.aunit, self.obj.deltaphi) - endphi = startPhi + deltaphi - nx = -math.sin(endphi) - ny = math.cos(endphi) - phi1_normal = Vector(nx, ny, 0) - - phi1_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi1_plane", phi1_normal, D) - self.surfaces.append(phi1_plane) - - if phi0_normal.cross(phi1_normal).z > 0: - region_expr += f"+{phi0_plane.id} -{phi1_plane.id}" - else: - region_expr += f"(+{phi0_plane.id} | -{phi1_plane.id})" + attrib = {} + attrib["name"] = self.name() + for prop in self.propertyList: + attrib[prop] = str(GDMLShared.getPropertyExpression(self.obj, prop)) # get expression or value - self.region = Region(region_expr) - self.position_globally() + ET.SubElement(solids, str(self.tag), attrib) + self._exportScaled() -class GDMLElConeExporter(SolidExporter): +class GDMLArb8Exporter(GDMLSolidExporter): def __init__(self, obj): - super().__init__(obj) - - def generate_surfaces(self): - mul = self.getMult() - dx = self.obj.dx - dy = self.obj.dy - zcut = mul * self.obj.zcut - zmax = mul * self.obj.zmax - name = NameManager.getName(self.obj) - center = Vector(0, 0, 0) - surf = EllipticalConeSurfaceExporter(name + '_cone', center, dx, dy, zmax) - self.surfaces.append(surf) + super().__init__(obj, 'arb8', ['v1x', 'v1y', 'v2x', 'v2y', 'v3x', 'v3y', 'v4x', 'v4y', + 'v5x', 'v5y', 'v6x', 'v6y', 'v7x', 'v7y', 'v8x', 'v8y', 'dz', 'lunit']) - normal = Vector(0, 0, 1) - bot_plane = PlaneSurfaceExporter(name + '_bot', normal, -zcut) - top_plane = PlaneSurfaceExporter(name + '_top', normal, zcut) - self.surfaces += [bot_plane, top_plane] - - self.region = Region(f"-{surf.id} +{bot_plane.id} -{top_plane.id}") - - self.position_globally() + def export(self): + if self.exported(): + return + super().export() + ET.SubElement( + solids, + "arb8", + { + "name": self.name(), + "v1x": str(self.obj.v1x), + "v1y": str(self.obj.v1y), + "v2x": str(self.obj.v2x), + "v2y": str(self.obj.v2y), + "v3x": str(self.obj.v3x), + "v3y": str(self.obj.v3y), + "v4x": str(self.obj.v4x), + "v4y": str(self.obj.v4y), + "v5x": str(self.obj.v5x), + "v5y": str(self.obj.v5y), + "v6x": str(self.obj.v6x), + "v6y": str(self.obj.v6y), + "v7x": str(self.obj.v7x), + "v7y": str(self.obj.v7y), + "v8x": str(self.obj.v8x), + "v8y": str(self.obj.v8y), + "dz": str(self.obj.dz), + "lunit": self.obj.lunit, + }, + ) + self._exportScaled() -class GDMLEllipsoidExporter(SolidExporter): - def __init__(self, obj): - super().__init__(obj) - def generate_surfaces(self): - mul = self.getMult() - name = NameManager.getName(self.obj) - surf1 = EllipsoidSurfaceExporter(name, mul * self.obj.ax, mul * self.obj.by, mul * self.obj.cz) - self.surfaces = [surf1] - region_expr = f"-{surf1.id}" - if hasattr(self.obj, 'zcut1'): - bot_plane = PlaneSurfaceExporter(name + '_zcut1', Vector(0, 0, 1), self.obj.zcut1) - self.surfaces.append(bot_plane) - region_expr += f' +{bot_plane.id}' - if hasattr(self.obj, 'zcut2'): - top_plane = PlaneSurfaceExporter(name + '_zcut2', Vector(0, 0, 1), self.obj.zcut2) - self.surfaces.append(top_plane) - region_expr += f' -{top_plane.id}' - - self.region = Region(region_expr) - self.position_globally() - - -class GDMLHypeExporter(SolidExporter): +class GDMLBoxExporter(GDMLSolidExporter): def __init__(self, obj): - super().__init__(obj) + super().__init__(obj, "box", ['x', 'y', 'z', 'lunit']) - def generate_surfaces(self): - from .GDMLObjects import getAngleRad - - name = NameManager.getName(self.obj) - - mul = self.getMult() - name = NameManager.getName(self.obj) - rmin = mul * self.obj.rmin - rmax = mul * self.obj.rmax - z = mul * self.obj.z - outst = getAngleRad(self.obj.aunit, self.obj.outst) - inst = getAngleRad(self.obj.aunit, self.obj.inst) - - # this should probably be a global variable, but - # for now adopt the value used in geant4.10.07.p02 - NUMBER_OF_DIVISIONS = 36 - sqrtan1 = math.tan(inst) - sqrtan1 *= sqrtan1 - sqrtan2 = math.tan(outst) - sqrtan2 *= sqrtan2 - - # Prepare two polylines - ns = NUMBER_OF_DIVISIONS - if sqrtan1 == 0.0: - nz1 = 2 - else: - nz1 = ns + 1 - if sqrtan2 == 0.0: - nz2 = 2 - else: - nz2 = ns + 1 - halfZ = z / 2 - # - # solid generated by external hyperbeloid - dz2 = z / (nz2 - 1) - zz = [halfZ - dz2 * i for i in range(0, nz2)] - rr = [math.sqrt(sqrtan2 * zi * zi + rmax * rmax) for zi in zz] - - self.region = Region("") - - planes_dict = {} - outer_region = Region("") - for i in range(0, nz2 - 1): - v0 = Vector(rr[i], 0, zz[i]) - v1 = Vector(rr[i + 1], 0, zz[i + 1]) - surface = cone_from_line_segment(v0, v1) - self.surfaces.append(surface) - edge_region = f"-{surface.id} " - for v in [v0, v1]: - if v.z in planes_dict: - plane = planes_dict[v.z] - else: - plane = PlaneSurfaceExporter(", Vector(0, 0, 1", v.z) - self.surfaces.append(plane) - if v == v0: - edge_region += f"+{plane.id} " - else: - edge_region += f"-{plane.id} " - outer_region = outer_region.union(edge_region) - - self.region = outer_region - - if rmin != 0: - # - # solid generated by internal hyperboloid - dz1 = z / (nz1 - 1) - zz = [halfZ - dz1 * i for i in range(0, nz1)] - rr = [math.sqrt(sqrtan1 * zi * zi + rmin * rmin) for zi in zz] - - inner_region = Region("") - for i in range(0, nz1 - 1): - v0 = Vector(rr[i], 0, zz[i]) - v1 = Vector(rr[i + 1], 0, zz[i + 1]) - surface = cone_from_line_segment(v0, v1) - self.surfaces.append(surface) - edge_region = f"-{surface.id} " - for v in [v0, v1]: - if v.z in planes_dict: - plane = planes_dict[v.z] - else: - plane = PlaneSurfaceExporter(", Vector(0, 0, 1", v.z) - self.surfaces.append(plane) - if v == v0: - edge_region += f"+{plane.id} " - else: - edge_region += f"-{plane.id} " - inner_region = inner_region.union(edge_region) - - self.region = self.region.cut(inner_region) - - top_plane = PlaneSurfaceExporter(name + '_top', Vector(0, 0, 1), z / 2) - bot_plane = PlaneSurfaceExporter(name + '_top', Vector(0, 0, 1), -z / 2) - self.surfaces += [top_plane, bot_plane] - self.region = self.region.intersection(Region(f"-{top_plane.id} +{bot_plane.id}")) - - self.position_globally() - - -class GDMLParaboloidExporter(SolidExporter): +class GDMLConeExporter(GDMLSolidExporter): def __init__(self, obj): - super().__init__(obj) - - def generate_surfaces(self): - mul = self.getMult() - name = NameManager.getName(self.obj) - rlo = mul * self.obj.rlo - rhi = mul * self.obj.rhi - dz = mul * self.obj.dz + super().__init__(obj, "cone", + ['rmin1', 'rmin2', 'rmax1', 'rmax2', 'startphi', 'deltaphi', 'aunit', 'z', 'lunit']) - surf = ParaboloidSurfaceExporter(name, rlo, rhi, dz) - top_surface = PlaneSurfaceExporter(name + '_top', Vector(0, 0, 1), dz) - bot_surface = PlaneSurfaceExporter(name + '_bot', Vector(0, 0, 1), -dz) - self.surfaces = [surf, top_surface, bot_surface] - self.region = Region(f"-{surf.id} +{bot_surface.id} -{top_surface.id}") - - self.position_globally() - - -class GDMLOrbExporter(SolidExporter): +class GDMLcutTubeExporter(GDMLSolidExporter): def __init__(self, obj): - super().__init__(obj) + super().__init__(obj, "cutTube", ['rmin', 'rmax', 'startphi', 'deltaphi', 'aunit', 'z', + 'highX', 'highY', 'highZ', 'lowX', 'lowY', 'lowZ', 'lunit']) - def generate_surfaces(self): - mul = self.getMult() - center = Vector(0, 0, 0) - radius = self.obj.rmin * mul - surf = SphereSurfaceExporter(f"{self.name()}", center, radius) - self.surfaces.append(surf) - self.region = Region(f"-{surf.id}") - self.position_globally() - -class GDMLPolyconeExporter(SolidExporter): +class GDMLElConeExporter(GDMLSolidExporter): def __init__(self, obj): - super().__init__(obj) + super().__init__(obj, 'elcone', ['dx', 'dy', 'zcut', 'zmax', 'lunit']) - def generate_surfaces(self): - from .GDMLObjects import getAngleRad - - name = NameManager.getName(self.obj) - mul = self.getMult() - planes_dict = {} - phi0 = getAngleRad(self.obj.aunit, self.obj.startphi) - deltaphi = getAngleRad(self.obj.aunit, self.obj.deltaphi) - phi1 = phi0 + deltaphi - - zplanes = self.obj.OutList - num_zplanes = len(zplanes) - - self.region = Region("") - - normal = Vector(0, 0, 1) - for i in range(num_zplanes - 1): - region = "" - - zplane0 = zplanes[i] - zplane1 = zplanes[i + 1] - - rmin0 = mul * zplane0.rmin - rmax0 = mul * zplane0.rmax - z0 = mul * zplane0.z - - rmin1 = mul * zplane1.rmin - rmax1 = mul * zplane1.rmax - z1 = mul * zplane1.z - if rmin0 != 0 and rmin1 != 0: - v0 = Vector(rmin0, 0, z0) - v1 = Vector(rmin1, 0, z1) - surface = cone_from_line_segment(v0, v1) - self.surfaces.append(surface) - region += f"+{surface.id} " - - if rmax0 != 0 and rmax1 != 0: - v0 = Vector(rmax0, 0, z0) - v1 = Vector(rmax1, 0, z1) - surface = cone_from_line_segment(v0, v1) - self.surfaces.append(surface) - region += f"-{surface.id} " - - if z0 in planes_dict: - bot_plane = planes_dict[z0] - else: - bot_plane = PlaneSurfaceExporter(name + '_bot', normal, z0) - planes_dict[z0] = bot_plane - self.surfaces.append(bot_plane) - if z1 in planes_dict: - top_plane = planes_dict[z1] - else: - top_plane = PlaneSurfaceExporter(name + '_top', normal, z1) - planes_dict[z1] = top_plane - self.surfaces.append(top_plane) - - region += f"+{bot_plane.id} -{top_plane.id} " - - self.region = self.region.union(Region(region)) - - if deltaphi < 2 * math.pi: - phi0_normal = Vector(math.sin(phi0), -math.cos(phi0), 0) - phi1_normal = Vector(math.sin(phi1), -math.cos(phi1), 0) - phi0_plane = PlaneSurfaceExporter(name + '_phi0', phi0_normal, 0) - phi1_plane = PlaneSurfaceExporter(name + '_phi1', phi1_normal, 0) - self.surfaces += [phi0_plane, phi1_plane] - if phi0_normal.cross(phi1_normal).z > 0: - region = f" +{phi0_plane.id} -{phi1_plane.id}" - else: - region = f"( +{phi0_plane.id} | -{phi1_plane.id})" - - self.region = self.region.intersection(Region(region)) - - self.position_globally() - - -class GDMLGenericPolyconeExporter(SolidExporter): +class GDMLEllipsoidExporter(GDMLSolidExporter): def __init__(self, obj): - super().__init__(obj) - - def generate_surfaces(self): - from .GDMLObjects import getAngleRad - - name = NameManager.getName(self.obj) - mul = self.getMult() - planes_dict = {} - phi0 = getAngleRad(self.obj.aunit, self.obj.startphi) - deltaphi = getAngleRad(self.obj.aunit, self.obj.deltaphi) - phi1 = phi0 + deltaphi - - zplanes = self.obj.OutList - num_zplanes = len(zplanes) - - self.region = Region("") - - normal = Vector(0, 0, 1) - for i in range(num_zplanes): - region = "" - - zplane0 = zplanes[i] - zplane1 = zplanes[(i + 1) % num_zplanes] - - r0 = mul * zplane0.r - z0 = mul * zplane0.z - - r1 = mul * zplane1.r - z1 = mul * zplane1.z - - v0 = Vector(r0, 0, z0) - v1 = Vector(r1, 0, z1) - surface = cone_from_line_segment(v0, v1) - self.surfaces.append(surface) + super().__init__(obj, 'ellipsoid', ['ax', 'by', 'cz', 'zcut1', 'zcut2', 'lunit']) - if i == num_zplanes - 1: - region += f"+{surface.id} " - else: - region += f"-{surface.id} " - - if z0 in planes_dict: - bot_plane = planes_dict[z0] - else: - bot_plane = PlaneSurfaceExporter(name + '_bot', normal, z0) - planes_dict[z0] = bot_plane - self.surfaces.append(bot_plane) - - if z1 in planes_dict: - top_plane = planes_dict[z1] - else: - top_plane = PlaneSurfaceExporter(name + '_top', normal, z1) - planes_dict[z1] = top_plane - self.surfaces.append(top_plane) - - if i < num_zplanes - 1: - region += f"+{bot_plane.id} -{top_plane.id} " - self.region = self.region.union(Region(region)) - else: - self.region = self.region.intersection(Region(region)) - - if deltaphi < 2 * math.pi: - phi0_normal = Vector(math.sin(phi0), -math.cos(phi0), 0) - phi1_normal = Vector(math.sin(phi1), -math.cos(phi1), 0) - phi0_plane = PlaneSurfaceExporter(name + '_phi0', phi0_normal, 0) - phi1_plane = PlaneSurfaceExporter(name + '_phi1', phi1_normal, 0) - self.surfaces += [phi0_plane, phi1_plane] - if phi0_normal.cross(phi1_normal).z > 0: - region = f" +{phi0_plane.id} -{phi1_plane.id}" - else: - region = f"( +{phi0_plane.id} | -{phi1_plane.id})" - self.region = self.region.intersection(Region(region)) - - self.position_globally() +class GDMLElTubeExporter(GDMLSolidExporter): + def __init__(self, obj): + super().__init__(obj, 'eltube', ['dx', 'dy', 'dz', 'lunit']) -class GDMLSphereExporter(SolidExporter): +class GDMLHypeExporter(GDMLSolidExporter): def __init__(self, obj): - super().__init__(obj) + super().__init__(obj, 'hype', ['rmin', 'rmax', 'z', 'inst', 'outst', 'aunit', 'lunit']) - def generate_surfaces(self): - from .GDMLObjects import checkFullCircle, getAngleRad - mul = self.getMult() - center = Vector(0, 0, 0) - radius = self.obj.rmin * mul - if radius != 0: - inner_surface = SphereSurfaceExporter(f"{self.name()}_ir", center, radius) - self.surfaces.append(inner_surface) - else: - inner_surface = None - - radius = self.obj.rmax * mul - outer_surface = SphereSurfaceExporter(f"{self.name()}_or", - center, radius) - self.surfaces.append(outer_surface) - - if not checkFullCircle(self.obj.aunit, self.obj.deltaphi): - startPhi = getAngleRad(self.obj.aunit, self.obj.startphi) - nx = -math.sin(startPhi) - ny = math.cos(startPhi) - phi0_normal = Vector(nx, ny, 0) - D = 0 - - phi0_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi0_plane", phi0_normal, D) - self.surfaces.append(phi0_plane) - - deltaphi = getAngleRad(self.obj.aunit, self.obj.deltaphi) - endphi = startPhi + deltaphi - nx = -math.sin(endphi) - ny = math.cos(endphi) - phi1_normal = Vector(nx, ny, 0) - - phi1_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi1_plane", phi1_normal, D) - self.surfaces.append(phi1_plane) - - startTheta = getAngleRad(self.obj.aunit, self.obj.starttheta) - deltaTheta = getAngleRad(self.obj.aunit, self.obj.deltatheta) - endTheta = startTheta + deltaTheta - if endTheta > math.pi: - endTheta = math.pi - - cones_region = "" - if startTheta > 0: - center = Vector(0, 0, 0) - axis = Vector(0, 0, 1) - mid_plane = PlaneSurfaceExporter(f"{self.obj.Label}_mid_plane", axis, 0) - - cone1_surface = None - if startTheta < math.pi / 2: - cone1_surface = ConeSurfaceExporter(f"{self.obj.Label}_thet1_cone", center, axis, startTheta) - elif startTheta > math.pi / 2: - cone1_surface = ConeSurfaceExporter(f"{self.obj.Label}_thet1_cone", center, axis, math.pi - startTheta) - - cone2_surface = None - if endTheta < math.pi / 2: - cone2_surface = ConeSurfaceExporter(f"{self.obj.Label}_thet2_cone", center, axis, endTheta) - elif endTheta > math.pi / 2 and endTheta != math.pi: - cone2_surface = ConeSurfaceExporter(f"{self.obj.Label}_thet2_cone", center, axis, math.pi - endTheta) - - if cone1_surface is not None: - self.surfaces.append(cone1_surface) - if cone2_surface is not None: - self.surfaces.append(cone2_surface) - self.surfaces.append(mid_plane) - - # three cases: - # case 1, startTheta, endTheta < 90 deg - if (0 < startTheta < math.pi / 2) and (0 < endTheta < math.pi / 2): - cones_region = f"+{cone1_surface.id} -{cone2_surface.id} +{mid_plane.id}" - - # case 2 startTheta < 90, endTheta > 90 - elif (0 < startTheta < math.pi / 2) and (math.pi / 2 < endTheta < math.pi): - cones_region = f"(+{mid_plane.id} +{cone1_surface.id}) | (-{mid_plane.id} +{cone2_surface.id})" - - # case 3, startTheta, endTheta > 90 deg - elif (math.pi / 2 < startTheta < math.pi) and (math.pi / 2 < endTheta < math.pi): - cones_region = f"+{cone2_surface.id} -{cone1_surface.id} -{mid_plane.id}" - - # edge cases: one or the other of starTheta, endTheta = math.pi/2, math.pi - elif (startTheta == math.pi / 2) and (endTheta < math.pi): - cones_region = f"-{mid_plane.id} +{cone2_surface.id}" - - elif (0 < startTheta < math.pi / 2) and endTheta == math.pi / 2: - cones_region = f"+{cone1_surface.id} +{mid_plane.id}" - - elif startTheta == math.pi / 2 and endTheta == math.pi: - cones_region = f"-{mid_plane.id}" - - elif startTheta > math.pi / 2 and endTheta == math.pi: - cones_region = f"-{cone1_surface.id} -{mid_plane.id}" - - elif startTheta == 0 and endTheta != math.pi: - center = Vector(0, 0, 0) - axis = Vector(0, 0, 1) - mid_plane = PlaneSurfaceExporter(f"{self.obj.Label}_mid_plane", axis, 0) - self.surfaces.append(mid_plane) - - if endTheta < math.pi / 2: - cone2_surface = ConeSurfaceExporter(f"{self.obj.Label}_thet2_cone", center, axis, endTheta) - self.surfaces.append(cone2_surface) - cones_region = f"-{cone2_surface.id} +{mid_plane.id}" - elif endTheta > math.pi / 2: - cone2_surface = ConeSurfaceExporter(f"{self.obj.Label}_thet2_cone", center, axis, math.pi - endTheta) - self.surfaces.append(cone2_surface) - cones_region = f"+{cone2_surface.id} | +{mid_plane.id}" - else: # endTheta == pi/2 - cones_region = f"+{mid_plane.id}" - - region = f"-{outer_surface.id}" - if inner_surface is not None: - region += f" +{inner_surface.id}" - - if not checkFullCircle(self.obj.aunit, self.obj.deltaphi): - if phi0_normal.cross(phi1_normal).z > 0: - region += f" +{phi0_plane.id} -{phi1_plane.id}" - else: - region += f" (+{phi0_plane.id} | -{phi1_plane.id})" +class GDMLParaboloidExporter(GDMLSolidExporter): + def __init__(self, obj): + super().__init__(obj, 'paraboloid', ['rlo', 'rhi', 'dz', 'lunit']) - self.region = Region(region) - if cones_region != "": - self.region = self.region.intersection(Region(cones_region)) - self.position_globally() +class GDMLOrbExporter(GDMLSolidExporter): + def __init__(self, obj): + super().__init__(obj, 'orb', ['r', 'lunit']) -def generate_half_loop_surfaces(loop_half, surfaces): - ''' - generate surfaces needed for the half loop(circle). - :param: loop_half: vertexes of the half circle - :return: The region of the surfaces - ''' - - Nvert = len(loop_half) - region = Region("") - # Instead of producing a bottom and top planes for each edge, we produce - # only one plane per vertex. To keep track if we produced a plane for a given - # vertex, we use a dictionary plane_dict[vertex.z = surface. See below of how we decide the region - planes_dict = {} - for i in range(0, Nvert - 1): - v0 = loop_half[i] - v1 = loop_half[i + 1] - outer = v1.z > v0.z - surface = cone_from_line_segment(v0, v1) - if surface is not None: - surfaces.append(surface) - if outer: - edge_region = f"-{surface.id} " - else: - edge_region = f"+{surface.id} " +class GDMLParaExporter(GDMLSolidExporter): + def __init__(self, obj): + super().__init__(obj, 'para', ['x', 'y', 'z', 'alpha', 'theta', 'phi', 'aunit', 'lunit']) - for v in [v0, v1]: - if v.z in planes_dict: - plane = planes_dict[v.z] - else: - plane = PlaneSurfaceExporter("", Vector(0, 0, 1), v.z) - surfaces.append(plane) - planes_dict[v.z] = plane - if outer and v == v0: - plane_region = f"+{plane.id} " - elif outer and v == v1: - plane_region = f"-{plane.id} " - elif (not outer) and v == v0: - plane_region = f"-{plane.id} " - elif (not outer) and v == v1: - plane_region = f"+{plane.id} " - edge_region += plane_region +class GDMLPolyconeExporter(GDMLSolidExporter): + def __init__(self, obj): + super().__init__(obj, 'polycone') - region = region.union(Region(edge_region)) + def export(self): + if self.exported(): + return + super().export() + + cone = ET.SubElement( + solids, + "polycone", + { + "name": self.name(), + "startphi": str(self.obj.startphi), + "deltaphi": str(self.obj.deltaphi), + "aunit": self.obj.aunit, + "lunit": self.obj.lunit, + }, + ) + self._exportScaled() - return region + for zplane in self.obj.OutList: + ET.SubElement( + cone, + "zplane", + { + "rmin": str(zplane.rmin), + "rmax": str(zplane.rmax), + "z": str(zplane.z), + }, + ) -class GDMLTorusExporter(SolidExporter): +class GDMLGenericPolyconeExporter(GDMLSolidExporter): def __init__(self, obj): - super().__init__(obj) - deviation = obj.ViewObject.Deviation / 100 - costhet = 1 - deviation - thet = math.acos(costhet) - N = int( - 2 * math.pi / (2 * thet)) # number of divisions of the circle that that given a maximum radial difference - # if Deviation, in percent - # make it even - self.N = 2 * int(N / 2) - - def inner_outer(self, center, radius): - ''' Return two ploylines, one is the outer (-90, 90 deg) - and one the inner (90, 270 degrees) - ''' - thet = -math.pi / 2 - dethet = 2 * math.pi / (self.N) - verts = [] - for i in range(self.N + 1): - x = radius * math.cos(thet) - z = radius * math.sin(thet) - verts.append(center + Vector(x, 0, z)) - thet += dethet - - outer = verts[:int(self.N / 2) + 1] - inner = verts[int(self.N / 2):] - return inner, outer - - def generate_circle_revolve(self, radius): - mul = self.getMult() - center = mul * self.obj.rtor * Vector(1, 0, 0) - inner, outer = self.inner_outer(center, mul * radius) - outer_region = generate_half_loop_surfaces(outer, self.surfaces) - inner_region = generate_half_loop_surfaces(inner, self.surfaces) - - region = outer_region.intersection(inner_region) - return region - - def generate_surfaces(self): - from .GDMLObjects import checkFullCircle, getAngleRad - - self.region = self.generate_circle_revolve(self.obj.rmax) - if self.obj.rmin > 0: - inside_region = self.generate_circle_revolve(self.obj.rmin) - self.region = self.region.cut(inside_region) - - if not checkFullCircle(self.obj.aunit, self.obj.deltaphi): - startPhi = getAngleRad(self.obj.aunit, self.obj.startphi) - nx = -math.sin(startPhi) - ny = math.cos(startPhi) - phi0_normal = Vector(nx, ny, 0) - D = 0 - - phi0_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi0_plane", phi0_normal, D) - self.surfaces.append(phi0_plane) - - deltaphi = getAngleRad(self.obj.aunit, self.obj.deltaphi) - endphi = startPhi + deltaphi - nx = -math.sin(endphi) - ny = math.cos(endphi) - phi1_normal = Vector(nx, ny, 0) - - phi1_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi1_plane", phi1_normal, D) - self.surfaces.append(phi1_plane) - - if phi0_normal.cross(phi1_normal).z > 0: - self.region = self.region.intersection(Region(f"+{phi0_plane.id} -{phi1_plane.id}")) - else: - self.region = self.region.intersection(Region(f"(+{phi0_plane.id} | -{phi1_plane.id})")) + super().__init__(obj, 'genericPolycone') - self.position_globally() + def export(self): + if self.exported(): + return + super().export() + + cone = ET.SubElement( + solids, + "genericPolycone", + { + "name": self.name(), + "startphi": str(self.obj.startphi), + "deltaphi": str(self.obj.deltaphi), + "aunit": self.obj.aunit, + "lunit": self.obj.lunit, + }, + ) + self._exportScaled() + for rzpoint in self.obj.OutList: + ET.SubElement( + cone, "rzpoint", {"r": str(rzpoint.r), "z": str(rzpoint.z)} + ) -class TorusExporter(SolidExporter): +class GDMLGenericPolyhedraExporter(GDMLSolidExporter): def __init__(self, obj): - super().__init__(obj) - deviation = self.obj.ViewObject.Deviation / 100 - costhet = 1 - deviation - thet = math.acos(costhet) - self.dthet = 2 * thet + super().__init__(obj, 'genericPolyhedra') - def get_verts(self): - ''' this is quite a bit more involved than appears''' - verts = [] - center = self.obj.Radius1.Value * Vector(1, 0, 0) - angle1 = math.radians(self.obj.Angle1) - angle2 = math.radians(self.obj.Angle2) - - v1 = Vector(math.cos(angle1), 0, -math.sin(angle1)) # unit vector in direction of angle1 - v2 = Vector(math.cos(angle2), 0, -math.sin(angle2)) # unit vector in direction of angle2 - # angular span is angle from v1 to v2, in CW direction. - sinthet = v1.cross(v2).y - thet = math.atan2(sinthet, v1.dot(v2)) - if thet < 0: - thet = 2 * math.pi + thet - - N = int(thet / self.dthet) + 1 - if N < 2: - N = 2 - - radius = self.obj.Radius2.Value - dthet = thet / (N - 1) # this is local - if thet < 2 * math.pi: - verts.append(center) - - verts += [center + radius * Vector(math.cos(angle1 + i * dthet), 0, -math.sin(angle1 + i * dthet)) for i in - range(N)] - - return verts - - def process_convex_polygon(self, verts): - - # generate the surfaces and region (one region!) for a convex polygon in the - # x-z plane. The assumption is that the verts are in CCW order. One needs to specify - # exactly what we mean by CCW in the x-z plane. If I look at the x-z plane, such that x is to my right - # and z upward, then a polygon with xy verts arranged CCW is one for which for, for two consecutive edges - # e1 and e2, e1 x e2 is in the -y direction. Yes, minus y, not positive y. - N = len(verts) - region_expr = "" - zmin = min([v.z for v in verts]) - zmax = max([v.z for v in verts]) - - for i in range(N): - v0 = verts[i] - v1 = verts[(i + 1) % N] - surf = cone_from_line_segment(v0, v1) - if surf is None: - continue - self.surfaces.append(surf) - # now we have to worry about the other half of the cone cutting out part of the polygon. - # it can do that if the vertex of the cone lies between zmin and zmax - extra_plane = None - # ugly, but we must remember surface properties are already in cm, so we need to multiply them by 10 - if isinstance(surf, ConeSurfaceExporter) and (zmin < 10 * surf.center.z < zmax): - extra_plane = PlaneSurfaceExporter("", Vector(0, 0, 1), 10 * surf.center.z) - self.surfaces.append(extra_plane) - - if v1.z > v0.z: # we are on the up side - region_expr += f"-{surf.id} " - - elif v1.z < v0.z: # on the down side, the extra plane can interfere - if extra_plane is not None: - if v1.z < 0: - region_expr += f"(+{surf.id} | +{extra_plane.id}) " - else: - region_expr += f"(+{surf.id} | -{extra_plane.id}) " - else: - region_expr += f"+{surf.id} " + def export(self): + if self.exported(): + return + super().export() + + polyhedra = ET.SubElement( + solids, + "genericPolyhedra", + { + "name": self.name(), + "startphi": str(self.obj.startphi), + "deltaphi": str(self.obj.deltaphi), + "numsides": str(self.obj.numsides), + "aunit": self.obj.aunit, + "lunit": self.obj.lunit, + }, + ) + self._exportScaled() + for rzpoint in self.obj.OutList: + ET.SubElement( + polyhedra, + "rzpoint", + {"r": str(rzpoint.r), "z": str(rzpoint.z)}, + ) - else: # v1.z == v0.z, the surface is a plane, either at the very top or very bottom, for the closed polygon - if v1.x < v0.x: - region_expr += f"-{surf.id} " - else: - region_expr += f"+{surf.id} " - self.region = self.region.union(Region(region_expr)) +class GDMLPolyhedraExporter(GDMLSolidExporter): + def __init__(self, obj): + super().__init__(obj, 'polyhedra') - def generate_surfaces(self): - verts = self.get_verts() - verts.reverse() + def export(self): + if self.exported(): + return + super().export() + + poly = ET.SubElement( + solids, + "polyhedra", + { + "name": self.name(), + "startphi": str(self.obj.startphi), + "deltaphi": str(self.obj.deltaphi), + "numsides": str(self.obj.numsides), + "aunit": self.obj.aunit, + "lunit": self.obj.lunit, + }, + ) + self._exportScaled() - self.region = Region("") - # do we have a concave polygon in the x-z plane? - if (self.obj.Angle2 - self.obj.Angle1) < 360: - # Not a complete circle, so there is possibility of convext polygon - radius = self.obj.Radius2.Value - center = self.obj.Radius1.Value * Vector(1, 0, 0) - angle1 = math.radians(self.obj.Angle1) - angle2 = math.radians(self.obj.Angle2) - v1 = radius * Vector(math.cos(angle1), 0, -math.sin(angle1)) - v2 = radius * Vector(math.cos(angle2), 0, -math.sin(angle2)) - if v1.cross(v2).y < 0: # This is concave, break it into two convex polygons - imid = int(len(verts) / 2) - seg1 = verts[:imid + 1] - seg1.append(center) - seg2 = verts[imid:] - for seg in [seg1, seg2]: - self.process_convex_polygon(seg) - else: - self.process_convex_polygon(verts) - else: - self.process_convex_polygon(verts) - - if self.obj.Angle3 != 360: - startPhi = 0 - nx = -math.sin(startPhi) - ny = math.cos(startPhi) - phi0_normal = Vector(nx, ny, 0) - D = 0 - phi0_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi0_plane", phi0_normal, D) - self.surfaces.append(phi0_plane) - - deltaphi = math.radians(self.obj.Angle3) - endphi = startPhi + deltaphi - nx = -math.sin(endphi) - ny = math.cos(endphi) - phi1_normal = Vector(nx, ny, 0) - - phi1_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi1_plane", phi1_normal, D) - self.surfaces.append(phi1_plane) - - if phi0_normal.cross(phi1_normal).z > 0: - region = f" +{phi0_plane.id} -{phi1_plane.id}" - else: - region = f" (+{phi0_plane.id} | -{phi1_plane.id})" + for zplane in self.obj.OutList: + ET.SubElement( + poly, + "zplane", + { + "rmin": str(zplane.rmin), + "rmax": str(zplane.rmax), + "z": str(zplane.z), + }, + ) - self.region = self.region.intersection(Region(region)) - self.position_globally() +class GDMLSphereExporter(GDMLSolidExporter): + def __init__(self, obj): + super().__init__(obj, "sphere", ['rmin', 'rmax', 'startphi', 'deltaphi', + 'starttheta', 'deltatheta', 'aunit', 'lunit']) -class GDMLTubeExporter(SolidExporter): +class GDMLSampledTessellatedExporter(GDMLSolidExporter): def __init__(self, obj): - super().__init__(obj) + super().__init__(obj, 'tesselated') - def generate_surfaces(self): - from .GDMLObjects import checkFullCircle, getAngleRad + def export(self): + if self.exported(): + return + super().export() + + tessName = self.name() + print(f"tessname: {tessName}") + # Use more readable version + tessVname = tessName + "_" + # print(dir(obj)) + + verts = self.obj.vertsList + tess = ET.SubElement(solids, "tessellated", {"name": tessName}) + for i, v in enumerate(verts): + exportDefineVertex(tessVname, v, i) + + i = 0 + indexList = self.obj.indexList + for nVerts in self.obj.vertsPerFacet: + # print(f'Normal at : {n} dot {dot} {clockWise}') + if nVerts == 3: + i0 = indexList[i] + i1 = indexList[i + 1] + i2 = indexList[i + 2] + ET.SubElement( + tess, + "triangular", + { + "vertex1": tessVname + str(i0), + "vertex2": tessVname + str(i1), + "vertex3": tessVname + str(i2), + "type": "ABSOLUTE", + }, + ) + elif nVerts == 4: + i0 = indexList[i] + i1 = indexList[i + 1] + i2 = indexList[i + 2] + i3 = indexList[i + 3] + ET.SubElement( + tess, + "quadrangular", + { + "vertex1": tessVname + str(i0), + "vertex2": tessVname + str(i1), + "vertex3": tessVname + str(i2), + "vertex4": tessVname + str(i3), + "type": "ABSOLUTE", + }, + ) + i += nVerts + self._exportScaled() - normal = Vector(0, 0, 1) - mul = self.getMult() - D = self.obj.z / 2 * mul - # a gdml tube has origin at its center - top_surface = PlaneSurfaceExporter(f"{self.name()}_top", normal, D) +class GDMLTessellatedExporter(GDMLSolidExporter): + def __init__(self, obj): + super().__init__(obj, 'tesselated') - D = -self.obj.z / 2 * mul - # a gdml tube has origin at its center - bottom_surface = PlaneSurfaceExporter(f"{self.name()}_bot", normal, D) + def export(self): + if self.exported(): + return + super().export() - center = Vector(0, 0, 0) - axis = normal - radius = self.obj.rmin * mul - if radius != 0: - inner_surface = CylinderSurfaceExporter(f"{self.name()}_ir", center, axis, radius) - else: - inner_surface = None + tessName = self.name() + # Use more readable version + tessVname = tessName + "_" + # print(dir(obj)) + vertexHashcodeDict = {} - radius = self.obj.rmax * mul - outer_surface = CylinderSurfaceExporter(f"{self.name()}_or", center, axis, radius) + """ + tess = ET.SubElement(solids, 'tessellated', {'name': tessName}) + + #for i, v in enumerate(self.obj.Shape.Vertexes): + for i, v in enumerate(self.obj.Shape.Vertexes): + vertexHashcodeDict[v.hashCode()] = i + exportDefineVertex(tessVname, self.obj.Vertexes[i], i) + + for f in self.obj.Shape.Faces: + # print(f'Normal at : {n} dot {dot} {clockWise}') + vertexes = f.OuterWire.OrderedVertexes + if len(f.Edges) == 3: + i0 = vertexHashcodeDict[vertexes[0].hashCode()] + i1 = vertexHashcodeDict[vertexes[1].hashCode()] + i2 = vertexHashcodeDict[vertexes[2].hashCode()] + ET.SubElement(tess, 'triangular', { + 'vertex1': tessVname+str(i0), + 'vertex2': tessVname+str(i1), + 'vertex3': tessVname+str(i2), + 'type': 'ABSOLUTE'}) + elif len(f.Edges) == 4: + i0 = vertexHashcodeDict[vertexes[0].hashCode()] + i1 = vertexHashcodeDict[vertexes[1].hashCode()] + i2 = vertexHashcodeDict[vertexes[2].hashCode()] + i3 = vertexHashcodeDict[vertexes[3].hashCode()] + ET.SubElement(tess, 'quadrangular', { + 'vertex1': tessVname+str(i0), + 'vertex2': tessVname+str(i1), + 'vertex3': tessVname+str(i2), + 'vertex4': tessVname+str(i3), + 'type': 'ABSOLUTE'}) + """ + tess = ET.SubElement(solids, "tessellated", {"name": tessName}) + placementCorrection = self.obj.Placement.inverse() + for i, v in enumerate(self.obj.Shape.Vertexes): + vertexHashcodeDict[v.hashCode()] = i + exportDefineVertex(tessVname, placementCorrection * v.Point, i) + + for f in self.obj.Shape.Faces: + # print(f'len(f.Edges) {len(f.Edges)}') + # print(f'Normal at : {n} dot {dot} {clockWise}') + vertexes = f.OuterWire.OrderedVertexes + if len(f.Edges) == 3: + i0 = vertexHashcodeDict[vertexes[0].hashCode()] + i1 = vertexHashcodeDict[vertexes[1].hashCode()] + i2 = vertexHashcodeDict[vertexes[2].hashCode()] + ET.SubElement( + tess, + "triangular", + { + "vertex1": tessVname + str(i0), + "vertex2": tessVname + str(i1), + "vertex3": tessVname + str(i2), + "type": "ABSOLUTE", + }, + ) + elif len(f.Edges) == 4: + i0 = vertexHashcodeDict[vertexes[0].hashCode()] + i1 = vertexHashcodeDict[vertexes[1].hashCode()] + i2 = vertexHashcodeDict[vertexes[2].hashCode()] + i3 = vertexHashcodeDict[vertexes[3].hashCode()] + ET.SubElement( + tess, + "quadrangular", + { + "vertex1": tessVname + str(i0), + "vertex2": tessVname + str(i1), + "vertex3": tessVname + str(i2), + "vertex4": tessVname + str(i3), + "type": "ABSOLUTE", + }, + ) + self._exportScaled() - if not checkFullCircle(self.obj.aunit, self.obj.deltaphi): - startPhi = getAngleRad(self.obj.aunit, self.obj.startphi) - nx = -math.sin(startPhi) - ny = math.cos(startPhi) - phi0_normal = Vector(nx, ny, 0) - D = 0 - phi0_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi0_plane", phi0_normal, D) - deltaphi = getAngleRad(self.obj.aunit, self.obj.deltaphi) - endphi = startPhi + deltaphi - nx = -math.sin(endphi) - ny = math.cos(endphi) - phi1_normal = Vector(nx, ny, 0) +class GDMLTetraExporter(GDMLSolidExporter): + def __init__(self, obj): + super().__init__(obj, 'tet') - phi1_plane = PlaneSurfaceExporter(f"{self.obj.Label}_phi1_plane", phi1_normal, D) + def export(self): + if self.exported(): + return + super().export() + + tetraName = self.name() + v1Name = tetraName + "v1" + v2Name = tetraName + "v2" + v3Name = tetraName + "v3" + v4Name = tetraName + "v4" + exportDefine(v1Name, self.obj.v1) + exportDefine(v2Name, self.obj.v2) + exportDefine(v3Name, self.obj.v3) + exportDefine(v4Name, self.obj.v4) - self.surfaces = [top_surface, bottom_surface, outer_surface] - if inner_surface is not None: - self.surfaces.append(inner_surface) + ET.SubElement( + solids, + "tet", + { + "name": tetraName, + "vertex1": v1Name, + "vertex2": v2Name, + "vertex3": v3Name, + "vertex4": v4Name, + }, + ) + self._exportScaled() - if not checkFullCircle(self.obj.aunit, self.obj.deltaphi): - self.surfaces += [phi0_plane, phi1_plane] - region = f"+{bottom_surface.id} -{top_surface.id} -{outer_surface.id}" - if inner_surface is not None: - region += f" +{inner_surface.id}" +class GDMLTetrahedronExporter(GDMLSolidExporter): + def __init__(self, obj): + super().__init__(obj, 'tet') - if not checkFullCircle(self.obj.aunit, self.obj.deltaphi): - if phi0_normal.cross(phi1_normal).z > 0: - region += f" +{phi0_plane.id} -{phi1_plane.id}" - else: - region += f" (+{phi0_plane.id} | -{phi1_plane.id})" + def export(self): + if self.exported(): + return + super().export() - self.region = Region(region) - self.position_globally() + global structure + global solids + tetrahedronName = self.name() + print("Len Tet" + str(len(self.obj.Proxy.Tetra))) + count = 0 + for t in self.obj.Proxy.Tetra: + tetraName = tetrahedronName + "_" + str(count) + v1Name = tetraName + "v1" + v2Name = tetraName + "v2" + v3Name = tetraName + "v3" + v4Name = tetraName + "v4" + exportDefine(v1Name, t[0]) + exportDefine(v2Name, t[1]) + exportDefine(v3Name, t[2]) + exportDefine(v4Name, t[3]) + ET.SubElement( + solids, + "tet", + { + "name": tetraName, + "vertex1": v1Name, + "vertex2": v2Name, + "vertex3": v3Name, + "vertex4": v4Name, + }, + ) + lvName = "LVtetra" + str(count) + lvol = ET.SubElement(structure, "volume", {"name": lvName}) + ET.SubElement(lvol, "materialref", {"ref": self.obj.material}) + ET.SubElement(lvol, "solidref", {"ref": tetraName}) + count += 1 + + # Now put out Assembly + assembly = ET.SubElement( + structure, "assembly", {"name": tetrahedronName} + ) + count = 0 + for t in self.obj.Proxy.Tetra: + lvName = "Tetra" + str(count) + physvol = ET.SubElement( + assembly, "physvol", {"name": "PV_Tetra" + str(count)} + ) + ET.SubElement(physvol, "volumeref", {"ref": lvName}) + # ET.SubElement(physvol, 'position') + # ET.SubElement(physvol, 'rotation') + count += 1 + self._exportScaled() -class GDMLTwistedSolidExporter(SolidExporter): +class GDMLTorusExporter(GDMLSolidExporter): def __init__(self, obj): - super().__init__(obj) - self.slice = 0 - self.dTwist = obj.ViewObject.AngularDeflection.Value - self.dTwist = min(self.dTwist, 5) + super().__init__(obj, 'torus', ['rmin', 'rmax', 'rtor', + 'startphi', 'deltaphi', 'aunit', 'lunit']) -class GDMLTwistedboxExporter(GDMLTwistedSolidExporter): +class GDMLTrapExporter(GDMLSolidExporter): def __init__(self, obj): - super().__init__(obj) - - def make4Walls(self, x, y): - name = NameManager.getName(self.obj) - surf1 = PlaneSurfaceExporter(f"{name}_{self.slice}_1", Vector(1, 0, 0), x / 2) - surf2 = PlaneSurfaceExporter(f"{name}_{self.slice}_2", Vector(0, 1, 0), y / 2) - surf3 = PlaneSurfaceExporter(f"{name}_{self.slice}_3", Vector(-1, 0, 0), x / 2) - surf4 = PlaneSurfaceExporter(f"{name}_{self.slice}_4", Vector(0, -1, 0), y / 2) - - return [surf1, surf2, surf3, surf4] - - def generate_surfaces(self): - from .GDMLObjects import getAngleDeg - mul = self.getMult() - - x = mul * self.obj.x - y = mul * self.obj.y - z = mul * self.obj.z - - angle = getAngleDeg(self.obj.aunit, self.obj.PhiTwist) - dPhi = self.dTwist # 2 degree rotation per step - N = int(angle / dPhi) - N = max(N, 10) - dz = z / N - - planes_dict = {} - region_expr = "" - z0 = -z / 2 - for i in range(0, N): - name = NameManager.getName(self.obj) - walls = self.make4Walls(x, y) - self.surfaces += walls - if z0 in planes_dict: - bot_plane = planes_dict[z0] - else: - bot_plane = PlaneSurfaceExporter(f"{name}_{self.slice}_bot", Vector(0, 0, 1), z0) - planes_dict[z0] = bot_plane - self.surfaces.append(bot_plane) - - z0 += dz - if z0 in planes_dict: - top_plane = planes_dict[z0] - else: - top_plane = PlaneSurfaceExporter(f"{name}_{self.slice}_top", Vector(0, 0, 1), z0) - planes_dict[z0] = top_plane - self.surfaces.append(top_plane) + super().__init__(obj, 'trap', ['z', 'theta', 'phi', + 'x1', 'x2', 'x3', 'x4', 'y1', 'y2']) - if region_expr == "": - region_expr = f"(-{walls[0].id} -{walls[1].id} -{walls[2].id} -{walls[3].id} +{bot_plane.id} -{top_plane.id})" - else: - region_expr += f" | (-{walls[0].id} -{walls[1].id} -{walls[2].id} -{walls[3].id} +{bot_plane.id} -{top_plane.id})" - - rot = FreeCAD.Rotation(Vector(0, 0, 1), -angle / 2 + i * dPhi) - for wall in walls: - wall.rotate(rot) - self.slice += 1 - - self.region = Region(region_expr) + def export(self): + if self.exported(): + return + super().export() - self.position_globally() + ET.SubElement( + solids, + "trap", + { + "name": self.name(), + "z": str(self.obj.z), + "theta": str(self.obj.theta), + "phi": str(self.obj.phi), + "x1": str(self.obj.x1), + "x2": str(self.obj.x2), + "x3": str(self.obj.x3), + "x4": str(self.obj.x4), + "y1": str(self.obj.y1), + "y2": str(self.obj.y2), + "alpha1": str(self.obj.alpha), + "alpha2": str(self.obj.alpha), + "aunit": self.obj.aunit, + "lunit": self.obj.lunit, + }, + ) + self._exportScaled() -class GDMLTwistedtrdExporter(GDMLTwistedSolidExporter): +class GDMLTrdExporter(GDMLSolidExporter): def __init__(self, obj): - super().__init__(obj) - - def make4Walls(self, x, y): - name = NameManager.getName(self.obj) - surf1 = PlaneSurfaceExporter(f"{name}_{self.slice}_1", Vector(1, 0, 0), x / 2) - surf2 = PlaneSurfaceExporter(f"{name}_{self.slice}_2", Vector(0, 1, 0), y / 2) - surf3 = PlaneSurfaceExporter(f"{name}_{self.slice}_3", Vector(-1, 0, 0), x / 2) - surf4 = PlaneSurfaceExporter(f"{name}_{self.slice}_4", Vector(0, -1, 0), y / 2) - - return [surf1, surf2, surf3, surf4] - - def generate_surfaces(self): - from .GDMLObjects import getAngleDeg - - mul = self.getMult() - - x1 = self.obj.x1 * mul - x2 = self.obj.x2 * mul - y1 = self.obj.y1 * mul - y2 = self.obj.y2 * mul - z = self.obj.z * mul - - angle = getAngleDeg(self.obj.aunit, self.obj.PhiTwist) - dPhi = self.dTwist # 2 degree rotation per step - N = int(angle / dPhi) - N = max(N, 10) - dz = z / N - - planes_dict = {} - region_expr = "" - z0 = -z / 2 - for i in range(0, N): - t = i * 1.0 / (N - 1) - xside = x1 + t * (x2 - x1) - yside = y1 + t * (y2 - y1) - - name = NameManager.getName(self.obj) - walls = self.make4Walls(xside, yside) - self.surfaces += walls - if z0 in planes_dict: - bot_plane = planes_dict[z0] - else: - bot_plane = PlaneSurfaceExporter(f"{name}_{self.slice}_bot", Vector(0, 0, 1), z0) - planes_dict[z0] = bot_plane - self.surfaces.append(bot_plane) - - z0 += dz - if z0 in planes_dict: - top_plane = planes_dict[z0] - else: - top_plane = PlaneSurfaceExporter(f"{name}_{self.slice}_top", Vector(0, 0, 1), z0) - planes_dict[z0] = top_plane - self.surfaces.append(top_plane) - - if region_expr == "": - region_expr = f"(-{walls[0].id} -{walls[1].id} -{walls[2].id} -{walls[3].id} +{bot_plane.id} -{top_plane.id})" - else: - region_expr += f" | (-{walls[0].id} -{walls[1].id} -{walls[2].id} -{walls[3].id} +{bot_plane.id} -{top_plane.id})" - - rot = FreeCAD.Rotation(Vector(0, 0, 1), -angle / 2 + i * dPhi) - for wall in walls: - wall.rotate(rot) - self.slice += 1 + super().__init__(obj, 'trd', ['z', 'x1', 'x2', 'y1', 'y2', 'lunit']) - self.region = Region(region_expr) - self.position_globally() +class GDMLTubeExporter(GDMLSolidExporter): + def __init__(self, obj): + super().__init__(obj, 'tube', ['rmin', 'rmax', 'startphi', 'deltaphi', 'aunit', 'z', 'lunit']) -class GDMLTwistedtrapExporter(SolidExporter): +class GDMLTwistedboxExporter(GDMLSolidExporter): def __init__(self, obj): - super().__init__(obj) + super().__init__(obj, 'twistedbox', ['PhiTwist', 'x', 'y', 'z', 'aunit', 'lunit']) - def make4Walls(self, verts): - name = NameManager.getName(self.obj) - n = len(verts) - surfaces = [] - for i in range(n): - i1 = (i + 1) % n - v0 = verts[i] - v1 = verts[i1] - dx = (v1 - v0).x - dy = (v1 - v0).y - normal = Vector(dy, -dx, 0) - normal.normalize() - D = v0.dot(normal) - - surf = PlaneSurfaceExporter(f"{name}_{self.slice}_1", normal, D) - surfaces.append(surf) - - return surfaces - - def generate_surfaces(self): - from .GDMLObjects import getAngleRad, getAngleDeg - - mul = self.getMult() - alpha = getAngleRad(self.obj.aunit, self.obj.Alph) - theta = getAngleRad(self.obj.aunit, self.obj.Theta) - phi = getAngleRad(self.obj.aunit, self.obj.Phi) - PhiTwist = getAngleDeg(self.obj.aunit, self.obj.PhiTwist) - y1 = mul * self.obj.y1 - x1 = mul * self.obj.x1 - x2 = mul * self.obj.x2 - y2 = mul * self.obj.y2 - x3 = mul * self.obj.x3 - x4 = mul * self.obj.x4 - z = mul * self.obj.z - - dTwist = self.dTwist - N = int(PhiTwist / dTwist) - N = max(N, 10) - dz = z / N - - tanalpha = math.tan(alpha) - - dt = 1.0 / N - t = 0 - - tanthet = math.tan(theta) - cosphi = math.cos(phi) - sinphi = math.sin(phi) - rhomax = z * tanthet - xoffset = -rhomax * cosphi / 2 - yoffset = -rhomax * sinphi / 2 - - planes_dict = {} - region_expr = "" - - for i in range(N): - # Vertexes, counter clock wise order - y = y1 + t * (y2 - y1) # go continuously from y1 to y2 - dx = y * tanalpha - x13 = x1 + t * (x3 - x1) # go continuously from x1 to x3 - x24 = x2 + t * (x4 - x2) # go continuously from x1 to x3 - zt = -z / 2 + t * z - rho = i * dz * tanthet - dxphi = xoffset + rho * cosphi - dyphi = yoffset + rho * sinphi - v1 = FreeCAD.Vector(-x13 / 2 - dx / 2 + dxphi, -y / 2 + dyphi, zt) - v2 = FreeCAD.Vector(x13 / 2 - dx / 2 + dxphi, -y / 2 + dyphi, zt) - v3 = FreeCAD.Vector(x24 / 2 + dx / 2 + dxphi, y / 2 + dyphi, zt) - v4 = FreeCAD.Vector(-x24 / 2 + dx / 2 + dxphi, y / 2 + dyphi, zt) - p = Part.makePolygon([v1, v2, v3, v4, v1]) - name = NameManager.getName(self.obj) - walls = self.make4Walls([v1, v2, v3, v4]) - - self.surfaces += walls - if zt in planes_dict: - bot_plane = planes_dict[zt] - else: - bot_plane = PlaneSurfaceExporter(f"{name}_{self.slice}_bot", Vector(0, 0, 1), zt) - planes_dict[zt] = bot_plane - self.surfaces.append(bot_plane) - zt += dz - if zt in planes_dict: - top_plane = planes_dict[zt] - else: - top_plane = PlaneSurfaceExporter(f"{name}_{self.slice}_top", Vector(0, 0, 1), zt) - planes_dict[zt] = top_plane - self.surfaces.append(top_plane) - - if region_expr == "": - region_expr = f"(-{walls[0].id} -{walls[1].id} -{walls[2].id} -{walls[3].id} +{bot_plane.id} -{top_plane.id})" - else: - region_expr += f" | (-{walls[0].id} -{walls[1].id} -{walls[2].id} -{walls[3].id} +{bot_plane.id} -{top_plane.id})" +class GDMLTwistedtrdExporter(GDMLSolidExporter): + def __init__(self, obj): + super().__init__(obj, 'twistedtrd', ['PhiTwist', 'x1', 'x2', 'y1', 'y2', 'z', 'aunit', 'lunit']) - rot = FreeCAD.Rotation(Vector(0, 0, 1), -PhiTwist / 2 + i * dTwist) - for wall in walls: - wall.rotate(rot) - t += dt - self.slice += 1 +class GDMLTwistedtrapExporter(GDMLSolidExporter): + def __init__(self, obj): + super().__init__(obj, 'twistedtrap', ['PhiTwist', 'x1', 'x2', 'y1', 'y2', 'x3', 'x4', 'z', + 'Theta', 'Phi', 'Alph', 'aunit', 'lunit']) - self.region = Region(region_expr) - self.position_globally() +class GDMLTwistedtubsExporter(GDMLSolidExporter): + def __init__(self, obj): + super().__init__(obj, 'twistedtubs', ['twistedangle', 'endinnerrad', 'endouterrad', 'zlen', + 'phi', 'aunit', 'lunit']) -class GDMLTwistedtubsExporter(GDMLTwistedSolidExporter): +class GDMLXtruExporter(GDMLSolidExporter): def __init__(self, obj): - super().__init__(obj) + super().__init__(obj, 'xtru') - def tubeWalls(self, rin, rout, phi0, phi1): - ''' return four surfaces: a cylinder of radius rin, None if rin = 0 - a cylinder or radius rout, two planes with normals in the x-y plane - one starting at ph0, the other ending at phi1 - ''' - cylinder_in = None - name = NameManager.getName(self.obj) - center = Vector(0, 0, 0) - axis = Vector(0, 0, 1) - if rin > 0: - cylinder_in = CylinderSurfaceExporter(name + "_in", center, axis, rin) - cylinder_out = CylinderSurfaceExporter(name + "_out", center, axis, rout) - - u0 = Vector(math.cos(phi0), math.sin(phi0), 0) - u1 = Vector(math.cos(phi1), math.sin(phi1), 0) - n0 = Vector(-u0.y, u0.x, 0) - n1 = Vector(-u1.y, u1.x, 0) - - phi0_plane = PlaneSurfaceExporter(name + "_phi0", n0, 0) - phi1_plane = PlaneSurfaceExporter(name + "_phi1", n1, 0) - - return [cylinder_in, cylinder_out, phi0_plane, phi1_plane] - - def generate_surfaces(self): - from .GDMLObjects import getAngleRad, getAngleDeg - mul = self.getMult() - rin = mul * self.obj.endinnerrad - rout = mul * self.obj.endouterrad - z = mul * self.obj.zlen - phi = getAngleRad(self.obj.aunit, self.obj.phi) - angle = getAngleDeg(self.obj.aunit, self.obj.twistedangle) - dTwist = self.dTwist # 2 degree rotation per step - N = int(angle / dTwist) - N = max(N, 10) - dz = z / N - - planes_dict = {} - region_expr = "" - z0 = -z / 2 - phi0 = -phi / 2 - phi1 = phi / 2 - - region_expr = "" - self.region = Region("") - for i in range(0, N): - name = NameManager.getName(self.obj) - walls = self.tubeWalls(rin, rout, phi0, phi1) - rin_cylinder = walls[0] - rout_cylinder = walls[1] - - if rin > 0: - self.surfaces += walls - else: - self.surfaces += walls[1:] + def export(self): + if self.exported(): + return + super().export() - if z0 in planes_dict: - bot_plane = planes_dict[z0] - else: - bot_plane = PlaneSurfaceExporter(f"{name}_{self.slice}_bot", Vector(0, 0, 1), z0) - planes_dict[z0] = bot_plane - self.surfaces.append(bot_plane) + xtru = ET.SubElement( + solids, "xtru", {"name": self.name(), "lunit": self.obj.lunit} + ) + for items in self.obj.OutList: + if items.Type == "twoDimVertex": + ET.SubElement( + xtru, + "twoDimVertex", + {"x": str(items.x), "y": str(items.y)}, + ) + if items.Type == "section": + ET.SubElement( + xtru, + "section", + { + "zOrder": str(items.zOrder), + "zPosition": str(items.zPosition), + "xOffset": str(items.xOffset), + "yOffset": str(items.yOffset), + "scalingFactor": str(items.scalingFactor), + }, + ) + self._exportScaled() - z0 += dz - if z0 in planes_dict: - top_plane = planes_dict[z0] - else: - top_plane = PlaneSurfaceExporter(f"{name}_{self.slice}_top", Vector(0, 0, 1), z0) - planes_dict[z0] = top_plane - self.surfaces.append(top_plane) - phi0_plane = walls[2] - phi1_plane = walls[3] - phi0_normal = phi0_plane.normal - phi1_normal = phi1_plane.normal +class GDML2dVertexExporter(GDMLSolidExporter): + def __init__(self, obj): + super().__init__(obj, 'twoDimVertex') - if rin > 0: - rin_region = f"+{rin_cylinder.id}" - else: - rin_region = "" + def export(self): + ET.SubElement( + solids, "twoDimVertex", {"x": self.obj.x, "y": self.obj.y} + ) - if phi0_normal.cross(phi1_normal).z > 0: - region_expr += f'({rin_region} -{rout_cylinder.id} +{bot_plane.id} -{top_plane.id} +{phi0_plane.id} -{phi1_plane.id})' - else: - region_expr += f'({rin_region} -{rout_cylinder.id} +{bot_plane.id} -{top_plane.id} (+{phi0_plane.id} | -{phi1_plane.id}))' - if i != N - 1: - region_expr += ' | ' - rot = FreeCAD.Rotation(Vector(0, 0, 1), -angle / 2 + i * dTwist) - for wall in [phi0_plane, phi1_plane]: - wall.rotate(rot) - self.slice += 1 +class GDMLborderSurfaceExporter(GDMLSolidExporter): + def __init__(self, obj): + super().__init__(obj, 'bordersurface') - self.region = Region(region_expr) + def export(self): + borderSurface = ET.SubElement( + structure, + "bordersurface", + {"name": self.obj.Name, "surfaceproperty": self.obj.surface}, + ) - self.position_globally() + print(self.obj.pv1) + if self.obj.pv1[:3] == "av_": + # for assembly auto generated names (starting with 'av_' we do not + # include the 'PV_' in the name + ET.SubElement(borderSurface, "physvolref", {"ref": self.obj.pv1}) + else: + ET.SubElement( + borderSurface, "physvolref", {"ref": "PV_" + self.obj.pv1} + ) + print(self.obj.pv1) + if self.obj.pv2[:3] == "av_": + ET.SubElement(borderSurface, "physvolref", {"ref": self.obj.pv2}) + else: + ET.SubElement( + borderSurface, "physvolref", {"ref": "PV_" + self.obj.pv2} + ) class MultiFuseExporter(SolidExporter): @@ -4750,256 +4357,219 @@ def name(self): solidName = "MultiFuse" + self.obj.Label return solidName - def generate_surfaces(self): + def export(self): + if self.exported(): + return + super().export() + + GDMLShared.trace("Multifuse - multiunion") + # test and fix + # First add solids in list before reference + print("Output Solids") exporters = [] - self.surfaces = [] - self.region = Region("") for sub in self.obj.OutList: exporter = SolidExporter.getExporter(sub) if exporter is not None: - exporter.generate_surfaces() - self.surfaces += exporter.surfaces - if self.region.expr == "": - self.region = Region(f'({exporter.get_region()})') - else: - self.region = self.region.union(exporter.get_region()) - - ''' - The surfaces generated above already contain the absoulte placement, including that of the - boolean object, so no need for this - - objPlacement = self.obj.Placement - if objPlacement != FreeCAD.Placement(): - translation = objPlacement.Base - rotation = objPlacement.Rotation - for surf in self.surfaces: - surf.rotate(rotation) - surf.translate(translation) - ''' + exporter.export() + exporters.append(exporter) + GDMLShared.trace("Output Solids Complete") + multUnion = ET.SubElement(solids, "multiUnion", {"name": self.name()}) -class MultiCommonExporter(SolidExporter): - def __init__(self, obj): - super().__init__(obj) - - def name(self): - solidName = "MultiCommon" + self.obj.Label - return solidName + num = 1 + for exp in exporters: + GDMLShared.trace(exp.name()) + node = ET.SubElement( + multUnion, "multiUnionNode", {"name": "node-" + str(num)} + ) + ET.SubElement(node, "solid", {"ref": exp.name()}) + processPlacement(exp.name(), node, exp.placement()) + num += 1 - def generate_surfaces(self): - exporters = [] - self.surfaces = [] - self.region = Region("") - for sub in self.obj.OutList: - exporter = SolidExporter.getExporter(sub) - if exporter is not None: - exporter.generate_surfaces() - self.surfaces += exporter.surfaces - if self.region.expr == "": - self.region = Region(f'({exporter.get_region()})') - else: - self.region = self.region.intersection(exporter.get_region()) + GDMLShared.trace("Return MultiFuse") + self._exportScaled() class OrthoArrayExporter(SolidExporter): def __init__(self, obj): super().__init__(obj) + self._name = "MultiUnion-" + self.obj.Label - def generate_surfaces(self): - self.region = Region("") + def export(self): + if self.exported(): + return + super().export() - # for the time being and array is treated is a solid with a single material - # its region is the union of the array of regions of the individual array elements from . import arrayUtils base = self.obj.Base + print(f"Base {base.Label}") if hasattr(base, "TypeId") and base.TypeId == "App::Part": print( f"**** Arrays of {base.TypeId} ({base.Label}) currently not supported ***" ) return - baseExporter = SolidExporter.getExporter(base) if baseExporter is None: print(f"Cannot export {base.Label}") return + baseExporter.export() + volRef = baseExporter.name() + unionXML = ET.SubElement(solids, "multiUnion", {"name": self.name()}) + basePos = baseExporter.position() + baseRotation = FreeCAD.Rotation(baseExporter.rotation()) + for i, placement in enumerate(arrayUtils.placementList(self.obj, offsetVector=basePos)): + ix, iy, iz = arrayUtils.orthoIndexes(i, self.obj) + nodeName = f"{self.name()}_{ix}_{iy}_{iz}" + translate = placement.Base + nodeXML = ET.SubElement( + unionXML, "multiUnionNode", {"name": nodeName} + ) + ET.SubElement(nodeXML, "solid", {"ref": volRef}) + ET.SubElement( + nodeXML, + "position", + { + "name": f"{self.name()}_pos_{ix}_{iy}_{iz}", + "x": str(translate.x), + "y": str(translate.y), + "z": str(translate.z), + "unit": "mm", + }, + ) + if baseRotation.Angle != 0: + exportRotation(self.name(), nodeXML, baseRotation, invertRotation=False) - # heuristic: If the rotation is not intrinsic to the base solid, then the - # position must be rotated as well. - tolerance = 1.0e-7 - rotatePosition = not base.Placement.Rotation.isSame(base.getGlobalPlacement().Rotation, tolerance) - positionRotation = base.getGlobalPlacement().Rotation - if rotatePosition: - print( - f" Will apply position rotation to {base.Label} because global rotation is different than base rotation") - # we need to also check for the array object rotation. This is NOT included in the global placement - if not self.obj.Placement.Rotation.isSame(base.getGlobalPlacement().Rotation, tolerance): - rotatePosition = True - positionRotation = self.obj.Placement.Rotation - - for i, placement in enumerate(arrayUtils.placementList(self.obj, offsetVector=self.obj.Placement.Base)): - solidExporter = SolidExporter.getExporter(base) - item_region = solidExporter.get_region() - translation = placement.Base - if rotatePosition: - translation = positionRotation * translation - # the rotation should also apply to the base, not just the position - solidExporter.rotate(positionRotation) - solidExporter.translate(translation) - self.region = self.region.union(item_region) - self.surfaces += solidExporter.surfaces + self._exportScaled() class PolarArrayExporter(SolidExporter): def __init__(self, obj): super().__init__(obj) - def generate_surfaces(self): - from . import arrayUtils - base = self.obj.Base + def name(self): + solidName = "MultiUnion-" + self.obj.Label + return solidName - self.region = Region("") + def export(self): + if self.exported(): + return + super().export() - appPartBase = False + from . import arrayUtils + base = self.obj.Base + print(base.Label) if hasattr(base, "TypeId") and base.TypeId == "App::Part": print( f"**** Arrays of {base.TypeId} ({base.Label}) currently not supported ***" ) - return - baseExporter = SolidExporter.getExporter(base) - if baseExporter is None: - print(f"Cannot export {base.Label}") - return - - arrayRotation = self.obj.Placement.Rotation - basePos = base.Placement.Base - # arrayUtils takes care of the offset center of of rotation - # But the array may have been shifted/rotatied by its parent. - # Since the array itself is not exported, but rather the array base is, - # the base global position does not reflect the arrays global position - - placements = arrayUtils.placementList(self.obj, offsetVector=basePos) - - # I'll try doing the rotations from scratch - # let GlobalPlacement = G # Note: FOR AN ARRAY GLOBAL PLACEMENT APPLIES to the ARRAY, NOT BASE of the ARRAY - # let Container Placement = C (the effect of all the App::Parts that contain the base) - # let the base placement be P - # then must have G = C*P ==> C = G*P^-1 - # If the rotation by the polar array is rot - # Then the position after rotation ought to be - # rotation by polar array: position_by_array = array_center + rot * (P.Base - array_center) - # Position globally rotated_global_position = C*position_by_array - # Because we don't give a position or a rotation to the solid exporter surfaces (we only rotate or translate them) - # We need to first unrotate and untranslate the surfaces, then translate and rotate by the array rotation and - # translation as above - arrayObj = self.obj - G = arrayObj.getGlobalPlacement() # In top_obj_dict, arrays don't have children. - P = arrayObj.Placement - Pinv = P.inverse() - C = G * Pinv # this is what puts the array in its position globally - baseInvPlacement = base.Placement.inverse() - - for placement in placements: - placement1 = placement * baseInvPlacement - rot = placement1.Rotation - pos = placement1.Base - - solidExporter = SolidExporter.getExporter(base) - # get region generates a region with # global translation and global rotation - # let us assume now that object rotation is 0 - # array utils give position rotated about array center then shifted by array center - # - item_region = solidExporter.get_region() - # the get_region will position the base globally already. Here we should only add the array - # placements - solidExporter.rotate(rot) - solidExporter.translate(pos) - self.region = self.region.union(item_region) - self.surfaces += solidExporter.surfaces - - # self.position_globally() # this should take care of the array's global position + baseExporter.export() + baseRotation = baseExporter.rotation() + volRef = baseExporter.name() + unionXML = ET.SubElement(solids, "multiUnion", {"name": self.name()}) + positionVector = baseExporter.position() + for i, placement in enumerate(arrayUtils.placementList(self.obj, + offsetVector=positionVector)): + rot = placement.Rotation + pos = placement.Base + rot = rot * baseRotation + rot.Angle = -rot.Angle # undo angle reversal by exportRotation + nodeName = f"{self.name()}_{i}" + nodeXML = ET.SubElement( + unionXML, "multiUnionNode", {"name": nodeName} + ) + ET.SubElement(nodeXML, "solid", {"ref": volRef}) + exportPosition(nodeName, nodeXML, pos) + exportRotation(nodeName, nodeXML, rot) + self._exportScaled() class PathArrayExporter(SolidExporter): def __init__(self, obj): super().__init__(obj) - def generate_surfaces(self): - self.region = Region("") + def name(self): + solidName = "MultiUnion-" + self.obj.Label + return solidName + + def export(self): + if self.exported(): + return + super().export() base = self.obj.Base + print(base.Label) if hasattr(base, "TypeId") and base.TypeId == "App::Part": print( f"**** Arrays of {base.TypeId} ({base.Label}) currently not supported ***" ) return - baseExporter = SolidExporter.getExporter(base) - if baseExporter is None: - print(f"Cannot export {base.Label}") - return - + baseExporter.export() + volRef = baseExporter.name() + unionXML = ET.SubElement(solids, "multiUnion", {"name": self.name()}) count = self.obj.Count - positionVector = base.Placement.Base + positionVector = baseExporter.position() rot = base.Placement.Rotation - extraTranslation = self.obj.ExtraTranslation - pathObj = self.obj.PathObject path = pathObj.Shape.Edges[0] points = path.discretize(Number=count) - for i, point in enumerate(points): pos = point + positionVector + extraTranslation - solidExporter = SolidExporter.getExporter(base) - item_region = solidExporter.get_region() - solidExporter.translate(pos) - self.region = self.region.union(item_region) - self.surfaces += solidExporter.surfaces + nodeName = f"{self.name()}_{i}" + nodeXML = ET.SubElement( + unionXML, "multiUnionNode", {"name": nodeName} + ) + ET.SubElement(nodeXML, "solid", {"ref": volRef}) + exportPosition(nodeName, nodeXML, pos) + exportRotation(nodeName, nodeXML, rot) + self._exportScaled() class PointArrayExporter(SolidExporter): def __init__(self, obj): super().__init__(obj) - def generate_surfaces(self): - from . import arrayUtils + def name(self): + solidName = "MultiUnion-" + self.obj.Label + return solidName - self.surfaces = [] - self.region = Region("") + def export(self): + if self.exported(): + return + super().export() base = self.obj.Base + print(base.Label) if hasattr(base, "TypeId") and base.TypeId == "App::Part": print( f"**** Arrays of {base.TypeId} ({base.Label}) currently not supported ***" ) return - baseExporter = SolidExporter.getExporter(base) - if baseExporter is None: - print(f"Cannot export {base.Label}") - return - + baseExporter.export() + volRef = baseExporter.name() + unionXML = ET.SubElement(solids, "multiUnion", {"name": self.name()}) + positionVector = baseExporter.position() + rotBase = base.Placement.Rotation extraTranslation = self.obj.ExtraPlacement.Base extraRotation = self.obj.ExtraPlacement.Rotation - + extraRotation.Angle = -extraRotation.Angle + rot = extraRotation * rotBase pointObj = self.obj.PointObject points = pointObj.Points.Points - arrayTranslation = self.obj.Placement.Base - arrayRotation = self.obj.Placement.Rotation - - # points = arrayUtils.placementList(self.obj, offsetVector=self.obj.Placement.Base) - for point in points: - if not arrayRotation.isSame(FreeCAD.Rotation(), 1e-7): - point = arrayRotation * point - pos = point + arrayTranslation + extraTranslation - solidExporter = SolidExporter.getExporter(base) - item_region = solidExporter.get_region() - solidExporter.rotate(arrayRotation) - solidExporter.translate(pos) - self.region = self.region.union(item_region) - self.surfaces += solidExporter.surfaces + for i, point in enumerate(points): + pos = point + positionVector + extraTranslation + nodeName = f"{self.name()}_{i}" + nodeXML = ET.SubElement( + unionXML, "multiUnionNode", {"name": nodeName} + ) + ET.SubElement(nodeXML, "solid", {"ref": volRef}) + exportPosition(nodeName, nodeXML, pos) + exportRotation(nodeName, nodeXML, rot) + self._exportScaled() # @@ -5014,36 +4584,6 @@ def generate_surfaces(self): # One of the closed curves (list of edges) representing a part # of the sketch -def cone_from_line_segment(v0, v1): - ''' - Given a line segment in the x-z plane, generate a cone in the half space containing the segment - :param: v0: One point on the segment (v0=Vector(x0, 0, z0) - :param: v1: another point on the segment v1=(Vector(x1, 0, z1) - :Return: The cone passing through the line segment. if v0.z == v1.z, return a cylinder - ''' - - dx = v1.x - v0.x - dz = v1.z - v0.z - u = (v1 - v0).normalize() # unit vector fro m v0 to v1 - if abs(dz) > 1e-9: - theta = math.atan(abs(dx / dz)) - axis = Vector(0, 0, 1) - - if abs(u.x) < 1e-9: # return a cylinder - surf = CylinderSurfaceExporter("", Vector(0, 0, 0), Vector(0, 0, 1), abs(v0.x)) - else: # return a cone - t2 = -v0.x / u.x - center = v0 + t2 * u - surf = ConeSurfaceExporter("", center, axis, theta) - - return surf - - elif abs(dx) > 1e-9: # a horizontal line, not sure if a plane is the correct answer - surf = PlaneSurfaceExporter("", Vector(0, 0, 1), v0.z) - return surf - - return None - class ClosedCurve: def __init__(self, name, edgeList): @@ -5120,119 +4660,17 @@ def arcs2circle(arc1, arc2): class RevolvedClosedCurve(ClosedCurve): - def __init__(self, name, edgelist): + def __init__(self, name, edgelist, angle, axis): super().__init__(name, edgelist) + self.angle = angle + self.axis = axis self.position = Vector(0, 0, 0) self.rotation = [0, 0, 0] # TBD self.deflectionFraction = 0.001 - self.surfaces = [] - self.region = Region("") - - def add_top_bottom(self, verts): - ''' If there is a single top/bottom vertex the cylinders/conves - meeting there will eventuall y extend to a common region that is outside - the revolved solid. So we must cap the top/botton with a top/bottom - plane in that case''' - zs = [v.z for v in verts] - maxz = max(zs) - minz = min(zs) - top_plane = PlaneSurfaceExporter("", Vector(0, 0, 1), maxz) - # add a top plane if one does not exist already - add_it = True - for surf in self.surfaces: - if surf == top_plane: - add_it = False - break - if add_it: - self.surfaces.append(top_plane) - self.region = self.region.intersection(Region(f"-{top_plane.id}")) - - bottom_plane = PlaneSurfaceExporter("", Vector(0, 0, 1), minz) - # add a top plane if one does not exist already - add_it = True - for surf in self.surfaces: - if surf == bottom_plane: - add_it = False - break - if add_it: - self.surfaces.append(bottom_plane) - self.region = self.region.intersection(Region(f"+{bottom_plane.id}")) - - def getVertexes(self): - return self.discretize() - - def generated_triangulated_surfaces(self, xy_verts): - from .polygonsHelper import triangulate_polygon_earclip, inner_outer - polys = triangulate_polygon_earclip(xy_verts) - - self.region = Region("") - for poly in polys: - verts = [Vector(v.x, 0, v.y) for v in poly] - arrangeCCW(verts, Vector(0, -1, 0)) # arrange in CCW in xz plane - inner, outer = inner_outer(verts) - - nouter = len(outer) - outer_region = Region("") - for i in range(0, nouter - 1): - v0 = outer[i] - v1 = outer[i + 1] - surface = cone_from_line_segment(v0, v1) - if surface is not None: - top_plane = PlaneSurfaceExporter("", Vector(0, 0, 1), v1.z) - bot_plane = PlaneSurfaceExporter("", Vector(0, 0, 1), v0.z) - self.surfaces += [surface, top_plane, bot_plane] - outer_region = outer_region.union(Region(f"-{surface.id} -{top_plane.id} +{bot_plane.id}")) - - ninner = len(inner) - inner_region = Region("") - for i in range(0, ninner - 1): - v0 = inner[i] - v1 = inner[i + 1] - surface = cone_from_line_segment(v0, v1) - if surface is not None: - top_plane = PlaneSurfaceExporter("", Vector(0, 0, 1), v0.z) - bot_plane = PlaneSurfaceExporter("", Vector(0, 0, 1), v1.z) - self.surfaces += [surface, top_plane, bot_plane] - inner_region = inner_region.union(Region(f"+{surface.id} -{top_plane.id} +{bot_plane.id}")) - - region = outer_region.intersection(inner_region) - self.region = self.region.union(region) - - def generate_surfaces(self): - from .polygonsHelper import is_convex_polygon, inner_outer - - verts = self.getVertexes() - # the revolve and triangulate algorithm assume the last point and first point are NOT the same. - # more generally, we omit edges of near zero length - vpruned = [] - for i in range(len(verts)): - i1 = (i + 1) % len(verts) - if (verts[i1] - verts[i]).Length > 1.0e-07: - vpruned.append(verts[i]) - - arrangeCCW(vpruned, Vector(0, -1, 0)) # arrange in CCW in xz plane - verts = vpruned - xy_verts = [Vector(v.x, v.z, 0) for v in verts] - if not is_convex_polygon(xy_verts): - self.generated_triangulated_surfaces(xy_verts) - return - - inner, outer = inner_outer(verts) - outer_region = generate_half_loop_surfaces(outer, self.surfaces) - inner_region = generate_half_loop_surfaces(inner, self.surfaces) - - region = outer_region.intersection(inner_region) - - zs = [v.z for v in verts] - maxz = max(zs) - minz = min(zs) - top_plane = PlaneSurfaceExporter("", Vector(0, 0, 1), maxz) - bot_plane = PlaneSurfaceExporter("", Vector(0, 0, 1), minz) - self.surfaces += [top_plane, bot_plane] - bounding_planes = Region(f"-{top_plane.id} +{bot_plane.id}") - - self.region = region.intersection(bounding_planes) + def export(self): + verts = self.discretize() + exportPolycone(self.name, verts, self.angle) def discretize(self): deflection = Deviation * radialExtent(self.edgeList) @@ -5241,11 +4679,30 @@ def discretize(self): return edge.discretize(Deflection=deflection) +class RevolvedCircle(RevolvedClosedCurve): + def __init__(self, name, edgelist, angle, axis): + super().__init__(name, edgelist, angle, axis) + z = edgelist[0].Curve.Center.z + self.position = FreeCAD.Vector(0, 0, z) + + def export(self): + edge = self.edgeList[0] + rmax = edge.Curve.Radius + x = edge.Curve.Center.x + y = edge.Curve.Center.y + startphi = math.degrees(math.atan2(y, x)) + rtor = math.sqrt(x * x + y * y) + exportTorus(self.name, rmax, rtor, startphi, self.angle) + + class RevolvedNEdges(RevolvedClosedCurve): - def __init__(self, name, edgelist): - super().__init__(name, edgelist) + def __init__(self, name, edgelist, angle, axis): + super().__init__(name, edgelist, angle, axis) + + def export(self): + global solids - def getVertexes(self): + # maxdev = self.deflectionFraction*radialExtent(self.edgeList) verts = [] for i, e in enumerate(self.edgeList): @@ -5263,16 +4720,21 @@ def getVertexes(self): else: curveName = self.name + "_c" + str(i) curveSection = RevolvedClosedCurve( - curveName, [e]) + curveName, [e], self.angle, self.axis + ) verts += curveSection.discretize() break - return verts + + xtruName = self.name + exportPolycone(xtruName, verts, self.angle) # arrange a list of edges in the x-y plane in Counter Clockwise direction # This can be easily generalized for points in ANY plane: if the normal # defining the desired direction of the plane is given, then the z component # below should be changed a dot prduct with the normal + + def arrangeCCW(verts, normal=Vector(0, 0, 1)): reverse = False v0 = verts[0] @@ -5326,84 +4788,125 @@ def radialExtent(edges, axis=Vector(0, 0, 1)): return rmax - rmin -def getRevolvedCurve(name, edges): +def exportEllipticalTube(name, dx, dy, height): + global solids + + ET.SubElement( + solids, + "eltube", + { + "name": name, + "dx": str(dx), + "dy": str(dy), + "dz": str(height / 2), + "lunit": "mm", + }, + ) + + +def exportTorus(name, rmax, rtor, startphi, angle): + global solids + + ET.SubElement( + solids, + "torus", + { + "name": name, + "rmin": "0", + "rmax": str(rmax), + "rtor": str(rtor), + "startphi": str(startphi), + "deltaphi": str(angle), + "aunit": "deg", + "lunit": "mm", + }, + ) + + +def exportPolycone(name, vlist, angle): + global solids + + # if x > 0 stratphi = 0 + # if x < 0 startphi = 180 + # FreeCAD says can't recompute of both x < 0 and x> are used in a revolve + startphi = 0 + for v in vlist: + if v.x != 0 or v.y != 0: + startphi = math.degrees(math.atan2(v.y, v.x)) + break + + cone = ET.SubElement( + solids, + "genericPolycone", + { + "name": name, + "startphi": str(startphi), + "deltaphi": str(angle), + "aunit": "deg", + "lunit": "mm", + }, + ) + for v in vlist: + r = math.sqrt(v.x * v.x + v.y * v.y) + ET.SubElement(cone, "rzpoint", {"r": str(r), "z": str(v.z)}) + return + + +def getRevolvedCurve(name, edges, angle, axis): # Return an RevolvedClosedCurve object of the list of edges if len(edges) == 1: # single edge ==> a closed curve, or curve section - return RevolvedClosedCurve(name, edges) + e = edges[0] + if len(e.Vertexes) == 1: # a closed curve + closed = True + else: + closed = False # a section of a curve + + while switch(e.Curve.TypeId): + if case("Part::GeomCircle"): + if closed is True: + print("Circle") + return RevolvedCircle(name, edges, angle, axis) + else: + print("Revolve Arc of Circle") + return RevolvedClosedCurve(name, edges, angle, axis) + # return RevolvedArcSection(name, edges, height) + + else: + print(f"revolve {e.Curve.TypeId}") + return RevolvedClosedCurve(name, edges, angle, axis) else: # three or more edges - return RevolvedNEdges(name, edges) + return RevolvedNEdges(name, edges, angle, axis) -def revolve_convex_polygon(verts, eps=1.0e-4): - '''Given the vertexes of a convex polygon (as FreeCAD Vectors), - produce surfaces, the interior of which forms a surface of revolution - of the triangle. +# scale up a solid that will be subtracted so it punches through parent +def scaleUp(scaledName, originalName, zFactor): + ss = ET.SubElement(solids, "scaledSolid", {"name": scaledName}) + ET.SubElement(ss, "solidref", {"ref": originalName}) + ET.SubElement( + ss, + "scale", + {"name": originalName + "_ss", "x": "1", "y": "1", "z": str(zFactor)}, + ) - The vertexes of the triangle are to be given as x-z point (in the x-z plane) - and are assumed to be already given in CCW order - There are three possible surfaces: - (1) A horizontal line (z = z0 = constant) -> horizontal plane - (2) A vertical line (x = x0 = constant) -> a cylinder - (2) A slanted line (x0, z0) -> (x1, z1) -> a cone - ''' - surfaces = [] - expr = "" - for i in range(len(verts)): - i1 = (i + 1) % len(verts) - v0 = verts[i] - v1 = verts[i1] - dx = v1.x - v0.x - dz = v1.z - v0.z - normal = Vector(dz, 0, -dx) - if normal.Length < 1.0e-07: - continue - normal.normalize() - - if abs(dx) < eps: # a cylinder - center = Vector(0, 0, 0) - axis = Vector(0, 0, 1) - radius = abs(v0.x) - cylinder = CylinderSurfaceExporter("", center, axis, radius) - surfaces.append(cylinder) - if normal.x < 0: - expr += f"+{cylinder.id} " - else: - expr += f"-{cylinder.id} " - - elif abs(dz) < eps: # a horizontal plane - D = v0.dot(normal) - surf = PlaneSurfaceExporter("", normal, D) - surfaces.append(surf) - expr += f"-{surf.id} " - - else: # must be a slanted line. a conical surface - theta = math.atan(abs(dx / dz)) - t2 = v0.dot(normal) / normal.z - center = t2 * Vector(0, 0, 1) - axis = Vector(0, 0, 1) - surf = ConeSurfaceExporter("", center, axis, theta) - surfaces.append(surf) - if normal.x > 0: - expr += f"-{surf.id} " - else: - expr += f"+{surf.id} " - - # we want to select half space of the cone that includes the edge - # The normal to the plane is Vector(0, 0, 1) (the z-axis) - # D = coneverex.z - # if v0.z < center.z, we are on the lower half, i.e, on the - sode f the plane - # other was we are on the + side of the plane - selecting_plane = PlaneSurfaceExporter("", axis, center.z) - surfaces.append(selecting_plane) - if v0.z < center.z: - expr += f"-{selecting_plane.id} " - else: - expr += f"+{selecting_plane.id} " +def rotatedPos(closedCurve, rot): + # Circles and ellipses (tubes and elliptical tubes) are referenced to origin + # in GDML and have to be translated to their position via a position reference + # when placed as a physical volume. This is done by adding the translation + # to the Part::Extrusion Placement. However, if the placement includes + # a rotation, Geant4 GDML would rotate the Origin-based curve THEN translate. + # This would not work. We have to translate first THEN rotate. This method + # just does the needed rotation of the position vector + # + pos = closedCurve.position + if isinstance(closedCurve, ExtrudedCircle) or isinstance( + closedCurve, ExtrudedEllipse + ): + pos = rot * closedCurve.position - return surfaces, Region(expr) + return pos class RevolutionExporter(SolidExporter): @@ -5412,8 +4915,7 @@ def __init__(self, revolveObj): self.sketchObj = revolveObj.Source self.lastName = self.obj.Label # initial name: might be modified later # generate the positions that get computed during export - self.region = None - self.surfaces = [] + self.export(doExport=False) def name(self): # override default name in SolidExporter @@ -5432,7 +4934,11 @@ def rotation(self): # Things will be screwed up, other wise return self._rotation - def generate_surfaces(self): + def export(self, doExport=True): + # The placement of the revolved item gets calculated here, during export + # but boolean exporter tries to get the position BEFORE the export here happens + # so to generate the position before the export happens, the doExport flag is used + # to run this code to generate the position, WITHOUT actually doing the export # global Deviation revolveObj = self.obj @@ -5469,7 +4975,7 @@ def generate_surfaces(self): # for each set of closed edges curves = [] for i, edges in enumerate(sortededges): - curve = getRevolvedCurve(eName + str(i), edges) + curve = getRevolvedCurve(eName + str(i), edges, angle, axis) if curve is not None: curves.append(curve) if len(curves) == 0: @@ -5486,14 +4992,11 @@ def generate_surfaces(self): lst = root.preOrderTraversal(root) rootnode = lst[0][0] rootCurve = rootnode.closedCurve - rootCurve.generate_surfaces() - self.surfaces = rootCurve.surfaces - + if doExport: + rootCurve.export() # a curve is created with a unique name firstName = rootCurve.name booleanName = firstName - self.region = rootCurve.region - rootPos = rootCurve.position rootRot = ( rootCurve.rotation @@ -5503,33 +5006,81 @@ def generate_surfaces(self): node = c[0] parity = c[1] curve = node.closedCurve - curve.generate_surfaces() - self.surfaces += curve.surfaces + if doExport: + curve.export() if parity == 0: - self.region = self.region.union(curve.region) + boolType = "union" + secondName = curve.name + secondPos = curve.position else: - self.region = self.region.cut(curve.region) - - # add cutoff planes if revolution angle is not 360 deg - if angle != 360: - phi0_normal = Vector(0, 1, 0) - phi0_plane = PlaneSurfaceExporter(self.name() + '_phi0', phi0_normal, 0) - phi1_normal = Vector(math.cos(math.radians(angle + 90)), math.sin(math.radians(angle + 90)), 0) - phi1_plane = PlaneSurfaceExporter(self.name() + '_phi1', phi1_normal, 0) - self.surfaces += [phi0_plane, phi1_plane] - - if phi0_normal.cross(phi1_normal).z > 0: - planes_region = Region(f"+{phi0_plane.id} -{phi1_plane.id}") - else: - planes_region = f"(+{phi0_plane.id} | -{phi1_plane.id})" + boolType = "subtraction" + secondName = curve.name + secondPos = curve.position + + booleanName = curve.name + "_bool" + if doExport: + boolSolid = ET.SubElement(solids, boolType, {"name": booleanName}) + ET.SubElement(boolSolid, "first", {"ref": firstName}) + ET.SubElement(boolSolid, "second", {"ref": secondName}) + + relativePosition = secondPos - rootPos + zAngle = curve.rotation[2] - rootRot[2] + posName = curve.name + "_pos" + rotName = curve.name + "_rot" + # position of second relative to first + if doExport: + exportDefine(posName, relativePosition) + ET.SubElement( + define, + "rotation", + { + "name": rotName, + "unit": "deg", + "x": "0", + "y": "0", + "z": str(zAngle), + }, + ) + + ET.SubElement(boolSolid, "positionref", {"ref": posName}) + ET.SubElement(boolSolid, "rotationref", {"ref": rotName}) + firstName = booleanName + + self.lastName = booleanName + # Because the position of each closed curve might not be at the + # origin, whereas primitives (tubes, cones, etc, are created centered at + # the origin, we need to shift the position of the very first node by its + # position, in addition to the shift by the Revolution placement + + revolvePosition = revolveObj.Placement.Base + zoffset = Vector(0, 0, 0) + angles = quaternion2XYZ(revolveObj.Placement.Rotation) + # need to add rotations of elliptical tubes. Assume extrusion is on z-axis + # Probably will not work in general + zAngle = angles[2] + rootRot[2] + print(rootPos) + print(rootCurve.name) + print(rootCurve.position) + rootPos = rotatedPos(rootCurve, revolveObj.Placement.Rotation) + print(rootPos) + Base = revolvePosition + rootPos - zoffset + + # add back the off-center revolve axis + if revolveCenter != Vector(0, 0, 0): + Base += revolveCenter + + rotX = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), angles[0]) + rotY = FreeCAD.Rotation(FreeCAD.Vector(0, 1, 0), angles[1]) + rotZ = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), zAngle) - self.region = self.region.intersection(planes_region) + rot = rotX * rotY * rotZ - rot_z_to_dir = FreeCAD.Rotation(Vector(0, 0, 1), axis) # rotation from z-axis to original axis of revolution - for surf in self.surfaces: - surf.rotate(rot_z_to_dir) + # rotate back to revolve direction + rot = rot_dir_to_z.inverted() * rot - self.position_globally() + placement = FreeCAD.Placement(Base, FreeCAD.Rotation(rot)) + self._position = placement.Base + self._rotation = placement.Rotation # @@ -5547,12 +5098,10 @@ def __init__(self, name, edgelist, height): self.height = height self.position = Vector(0, 0, 0) self.rotation = [0, 0, 0] # TBD - self.region = None - self.surfaces = [] - def generate_surfaces(self): + def export(self): verts = self.discretize() - self.surfaces, self.region = exportXtru(self.name, verts, self.height) + exportXtru(self.name, verts, self.height) def midPoint(self): edge = self.edgeList[0] @@ -5570,14 +5119,11 @@ def discretize(self): class ExtrudedCircle(ExtrudedClosedCurve): def __init__(self, name, edgelist, height): super().__init__(name, edgelist, height) - self.position = edgelist[0].Curve.Center + self.position = edgelist[0].Curve.Center + Vector(0, 0, height / 2) - def generate_surfaces(self): + def export(self): edge = self.edgeList[0] - print(f"circle position {self.position}") - surf = exportTube(self.name, self.position, edge.Curve.Radius, self.height) - self.surfaces = [surf] - self.region = Region(f"-{surf.id}") + exportTube(self.name, edge.Curve.Radius, self.height) class ExtrudedArcSection(ExtrudedClosedCurve): @@ -5602,14 +5148,139 @@ def area(self): return face.Area - def generate_surfaces(self): + def export(self): + global solids + + edge = self.edgeList[0] + radius = edge.Curve.Radius + # First form a bounding rectangle (polygon) for the arc. + # Arc edges + v1 = edge.Vertexes[0].Point + v2 = edge.Vertexes[1].Point + vmid = self.midPoint() + + # midpoint of chord + vc = (v1 + v2) / 2 + v = v2 - v1 + u = v.normalize() + # extend the ends of the chord so extrusion can cut all of circle, if needed + v1 = vc + radius * u + v2 = vc - radius * u + # component of vmid perpendicular to u + vc_vmid = vmid - vc + n = vc_vmid - u.dot(vc_vmid) * u + n.normalize() + # complete edges of box paerpendicular to chord, toward mid arc point + v3 = v2 + 2 * radius * n + v4 = v1 + 2 * radius * n + + xtruName = self.name + "_xtru" + exportXtru(xtruName, [v1, v2, v3, v4], self.height) + + # tube to be cut1 + tubeName = self.name + "_tube" + exportTube(tubeName, edge.Curve.Radius, self.height) + + # note, it is mandatory that name be that of ClosedCurve + intersect = ET.SubElement(solids, "intersection", {"name": self.name}) + ET.SubElement(intersect, "first", {"ref": xtruName}) + ET.SubElement(intersect, "second", {"ref": tubeName}) + pos = edge.Curve.Center + Vector(0, 0, self.height / 2) + exportPosition(tubeName, intersect, pos) + + +class ExtrudedEllipse(ExtrudedClosedCurve): + def __init__(self, name, edgelist, height): + super().__init__(name, edgelist, height) + curve = edgelist[0].Curve + self.position = curve.Center + Vector(0, 0, height / 2) + angle = math.degrees(curve.AngleXU) + self.rotation = [0, 0, angle] + + def export(self): + edge = self.edgeList[0] + exportEllipticalTube( + self.name, + edge.Curve.MajorRadius, + edge.Curve.MinorRadius, + self.height, + ) + + +class ExtrudedEllipticalSection(ExtrudedClosedCurve): + def __init__(self, name, edgelist, height): + super().__init__(name, edgelist, height) + # Note extrusion polyogn will be in absolute coordinates + # since arc section is relative to that, position is actually (0,0,0) + # same goes for rotation + + """ + def midPoint(self): + edge = self.edgeList[0] + a = edge.Curve.MajorRadius + b = edge.Curve.MinorRadius + angleXU = edge.Curve.AngleXU + thet1 = edge.FirstParameter # in radians, in unorated ellipse + thet2 = edge.LastParameter # in radians, in onrated ellipse + thetmid = (thet1+thet2)/2 + angleXU + + # Major axis angle seems to be off by pi for some ellipse. + # Restrict it to be be between 0 an pi + if angleXU < 0: + angleXU += 180 + + # TODO must deal with case where cutting chord is along major axis + # u_vc_vcenter = vc_vcenter.normalize() + # unit vector from center of circle to center of chord + + # vertexes of triangle formed by chord ends and ellise mid point + # In polar coordinates equation of ellipse is + # r(thet) = a*(1-eps*eps)/(1+eps*cos(thet)) + # if the ellipse is rotated by an angle AngleXU, then + # x = r*cos(thet+angleXU), y = r*sin(thet+angleXU), + # for thet in frame of unrotated ellipse + # now edge.FirstParameter is beginning angle of unrotated ellipse + + def sqr(x): + return x*x + + def r(thet): + return math.sqrt(1.0/(sqr(math.cos(thet)/a) + sqr(math.sin(thet)/b))) + + rmid = r(thetmid) + vmid = Vector(rmid*math.cos(thetmid), rmid*math.sin(thetmid), 0) + + vmid += edge.Curve.Center + + return vmid + """ + + def export(self): global solids - edge = self.edgeList[0] - radius = edge.Curve.Radius - center = edge.Curve.Center - # First form a bounding rectangle (polygon) for the arc. - # Arc edges + edge = self.edgeList[0] + a = dx = edge.Curve.MajorRadius + b = dy = edge.Curve.MinorRadius + + # vertexes of triangle formed by chord ends and ellise mid point + # In polar coordinates equation of ellipse is + # r(thet) = a*(1-eps*eps)/(1+eps*cos(thet)) + # if the ellipse is rotated by an angle AngleXU, then + # x = r*cos(thet+angleXU), y = r*sin(thet+angleXU), + # for thet in frame of unrotated ellipse + # now edge.FirstParameter is beginning angle of unrotated ellipse + # polar equation of ellipse, with r measured from FOCUS. Focus at a*eps + # r = lambda thet: a*(1-eps*eps)/(1+eps*math.cos(thet)) + # polar equation of ellipse, with r measured from center a*eps + + def sqr(x): + return x * x + + def r(thet): + return math.sqrt( + 1.0 / (sqr(math.cos(thet) / a) + sqr(math.sin(thet) / b)) + ) + v1 = edge.Vertexes[0].Point v2 = edge.Vertexes[1].Point vmid = self.midPoint() @@ -5617,53 +5288,164 @@ def generate_surfaces(self): # midpoint of chord vc = (v1 + v2) / 2 v = v2 - v1 - u = v.normalize() - # extend the ends of the chord so extrusion can cut all of circle, if needed - v1 = vc + radius * u - v2 = vc - radius * u + u = v.normalize() # unit vector from v1 to v2 + # extend the ends of the chord so extrusion can cut all of ellipse, + # if needed + v1 = vc + 2 * a * u + v2 = vc - 2 * a * u + # component of vmid perpendicular to u vc_vmid = vmid - vc n = vc_vmid - u.dot(vc_vmid) * u n.normalize() + v3 = v2 + 2 * a * n + v4 = v1 + 2 * a * n xtruName = self.name + "_xtru" - - chord_plane, chord_region = exportXtru(xtruName, [v1, v2], self.height)[0] - # we want the plane normal to point AWAY from the chord - if chord_plane.normal.dot(vmid) > 0: - chord_plane.normal = -chord_plane.Normal - chord_plane.to_coeffs() - self.surfaces.append(chord_plane) + exportXtru(xtruName, [v1, v2, v3, v4], self.height) # tube to be cut1 tubeName = self.name + "_tube" - cylinder = exportTube(tubeName, center, edge.Curve.Radius, self.height) - self.surfaces.append(cylinder) + exportEllipticalTube(tubeName, dx, dy, self.height) + + # note, it is mandatory that name be that of ClosedCurve + intersect = ET.SubElement(solids, "intersection", {"name": self.name}) + ET.SubElement(intersect, "first", {"ref": xtruName}) + ET.SubElement(intersect, "second", {"ref": tubeName}) + pos = edge.Curve.Center + Vector(0, 0, self.height / 2) + exportPosition(tubeName, intersect, pos) + rotName = tubeName + "_rot" + # zAngle = math.degrees(edge.Curve.AngleXU) + # Focus1 is on the positive x side, Focus2 on the negative side + dy = edge.Curve.Focus1[1] - edge.Curve.Focus2[1] + dx = edge.Curve.Focus1[0] - edge.Curve.Focus2[0] + zAngle = math.degrees(math.atan2(dy, dx)) + print(f"{self.name} zAngle = {zAngle}") + # if zAngle < 0: + # zAngle += 180 + ET.SubElement( + define, + "rotation", + { + "name": rotName, + "unit": "deg", + "x": "0", + "y": "0", + "z": str(zAngle), + }, + ) - self.region = Region(f"-{cylinder.id} -{chord_plane.id}") + ET.SubElement(intersect, "rotationref", {"ref": rotName}) -class ExtrudedEllipticalSection(ExtrudedClosedCurve): +class ExtrudedBSpline(ExtrudedClosedCurve): + def __init__(self, name, edgelist, height): + super().__init__(name, edgelist, height) + + +class Extruded2Edges(ExtrudedClosedCurve): def __init__(self, name, edgelist, height): super().__init__(name, edgelist, height) - # Note extrusion polyogn will be in absolute coordinates - # since arc section is relative to that, position is actually (0,0,0) - # same goes for rotation - def generate_surfaces(self): + def export(self): global solids - edge = self.edgeList[0] - deflection = Deviation * edge.BoundBox.DiagonalLength - points = edge.discretize(Deflection=deflection) - points.append(points[0]) - arrangeCCW(points) - self.surfaces, self.region = exportXtru(self.name, points, self.height) + # form normals to the edges. For case of two edges, + # sidedness is irrelevant + v0 = self.edgeList[0].Vertexes[0].Point + v1 = self.edgeList[0].Vertexes[1].Point + e = v1 - v0 + if e.x == 0: + ny = 0 + nx = 1 + elif e.y == 0: + nx = 0 + ny = 1 + else: + nx = 1 + ny = -e.x / e.y + normal = Vector(nx, ny, 0).normalize() + edgeCurves = [] # list of ExtrudedClosedCurve's -class ExtrudedBSpline(ExtrudedClosedCurve): - def __init__(self, name, edgelist, height): - super().__init__(name, edgelist, height) + for i, e in enumerate(self.edgeList): # just TWO edges + while switch(e.Curve.TypeId): + if case("Part::GeomLineSegment"): + break + + if case("Part::GeomLine"): + break + + if case("Part::GeomCircle"): + print("Arc of Circle") + arcXtruName = self.name + "_c" + str(i) + arcSection = ExtrudedArcSection( + arcXtruName, [e], self.height + ) + arcSection.export() + + midpnt = arcSection.midPoint() + inside = pointInsideEdge(midpnt, v0, normal) + edgeCurves.append([arcXtruName, inside]) + break + + if case("Part::GeomEllipse"): + print("Arc of Ellipse") + arcXtruName = self.name + "_e" + str(i) + arcSection = ExtrudedEllipticalSection( + arcXtruName, [e], self.height + ) + arcSection.export() + midpnt = arcSection.midPoint() + inside = pointInsideEdge(midpnt, v0, normal) + edgeCurves.append([arcXtruName, inside]) + break + + else: + print(f"Arc of {e.Curve.TypeId}") + arcXtruName = self.name + "_bs" + str(i) + arcSection = ExtrudedClosedCurve( + arcXtruName, [e], self.height + ) + arcSection.export() + + midpnt = arcSection.midPoint() + inside = pointInsideEdge(midpnt, v0, normal) + edgeCurves.append([arcXtruName, inside]) + break + + if len(edgeCurves) == 1: + # change our name to be that of the constructed curve + # not a violation of the contract of a unique name, + # since the curve name is based on ours + self.position = arcSection.position + self.rotation = arcSection.rotation + self.name = edgeCurves[0][0] + + else: + inside0 = edgeCurves[0][1] + inside1 = edgeCurves[1][1] + sameSide = inside0 == inside1 + if sameSide is False: + booleanSolid = ET.SubElement( + solids, "union", {"name": self.name} + ) + else: + booleanSolid = ET.SubElement( + solids, "subtraction", {"name": self.name} + ) + + area0 = edgelistArea([self.edgeList[0]]) + area1 = edgelistArea([self.edgeList[1]]) + if area0 > area1: + firstSolid = edgeCurves[0][0] + secondSolid = edgeCurves[1][0] + else: + firstSolid = edgeCurves[1][0] + secondSolid = edgeCurves[0][0] + + ET.SubElement(booleanSolid, "first", {"ref": firstSolid}) + ET.SubElement(booleanSolid, "second", {"ref": secondSolid}) class ExtrudedNEdges(ExtrudedClosedCurve): @@ -5711,7 +5493,9 @@ def isSubtraction(self, edge): else: return False - def generate_surfaces(self): + def export(self): + global solids + verts = [] for i, e in enumerate(self.edgeList): @@ -5736,7 +5520,7 @@ def generate_surfaces(self): # verts.append(verts[0]) xtruName = self.name - self.surfaces, self.region = exportXtru(xtruName, verts, self.height) + exportXtru(xtruName, verts, self.height) # Node of a tree that represents the topology of the sketch being exported @@ -5808,73 +5592,72 @@ def discretizeMinusOne(edgeList, iSkip): return verts -def exportTube(name, center, radius, height): +def exportTube(name, radius, height): global solids - axis = Vector(0, 0, 1) - outer_surface = CylinderSurfaceExporter(name, center, axis, radius) - return outer_surface + ET.SubElement( + solids, + "tube", + { + "name": name, + "rmax": str(radius), + "z": str(height), + "startphi": "0", + "deltaphi": "360", + "aunit": "deg", + "lunit": "mm", + }, + ) def exportXtru(name, vlist, height): - from .polygonsHelper import is_convex_polygon, triangulate_polygon_earclip - - def convex_poly_planes(points): - planes = [] - for i in range(len(points)): - i1 = (i + 1) % len(points) - edge = points[i1] - points[i] - normal = Vector(edge.y, -edge.x, 0) - normal.normalize() - - D = normal.dot(points[i]) - planes.append(PlaneSurfaceExporter(name, normal, D)) - return planes - - def convex_poly_region(planes): - expr = "" - for surf in planes: - expr += f"-{surf.id} " - return Region(expr) - - # We are assuming that the points that form + global solids + + # We are assuming that the points to that form the # the edges to be extruded are al coplanar and in the x-y plane # with a possible zoffset, which is taken as the common # z-coordinate of the first vertex if len(vlist) < 3: - return [], Region("") + return + zoffset = vlist[0].z + xtru = ET.SubElement(solids, "xtru", {"name": name, "lunit": "mm"}) # prune verts: Closed curves get stitched from adjacent edges - # sometimes the beginning vertex of the next edge is the same as the end vertex + # sometimes the beginning vertex f the next edge is the same as the end vertex # of the previous edge. Geant gives a warning about that. To remove the warning # we will remove vertices closer to each other than 0.1 nm - 1e-7 mm. If someone is trying - # to model objects with smaller dimensions, then they should take a lesson in Quantum Mechanic2s + # to model objects with smaller dimensions, then they should take a lesson in Quantum Mechanics # Because only adjacent edges could be the same, we just compare each edge to the preceding edge vpruned = vlist[:] - arrangeCCW(vpruned) for i in range(len(vlist)): i1 = (i + 1) % len(vlist) if ((vlist[i1] - vlist[i]).Length < 1e-7): vpruned.remove(vlist[i1]) - planes = [] - - if is_convex_polygon(vpruned): - planes = convex_poly_planes(vpruned) - region = convex_poly_region(planes) - - else: - region = Region("") - polygon_list = triangulate_polygon_earclip(vpruned) - for polygon in polygon_list: - convex_planes = convex_poly_planes(polygon) - convex_region = convex_poly_region(convex_planes) - - # TODO: remove common planes with opposite normals - planes += convex_planes - region = region.union(convex_region) - - return planes, region + for v in vpruned: + ET.SubElement(xtru, "twoDimVertex", {"x": str(v.x), "y": str(v.y)}) + ET.SubElement( + xtru, + "section", + { + "zOrder": "0", + "zPosition": str(zoffset), + "xOffset": "0", + "yOffset": "0", + "scalingFactor": "1", + }, + ) + ET.SubElement( + xtru, + "section", + { + "zOrder": "1", + "zPosition": str(height + zoffset), + "xOffset": "0", + "yOffset": "0", + "scalingFactor": "1", + }, + ) def getExtrudedCurve(name, edges, height): @@ -5904,6 +5687,14 @@ def getExtrudedCurve(name, edges, height): print("Arc of Circle") return ExtrudedArcSection(name, edges, height) + if case("Part::GeomEllipse"): + if closed is True: + print("Ellipse") + return ExtrudedEllipse(name, edges, height) + else: + print("Arc of Ellipse") + return ExtrudedEllipticalSection(name, edges, height) + else: print(" B spline") return ExtrudedClosedCurve(name, edges, height) @@ -5927,6 +5718,7 @@ def __init__(self, extrudeObj): self.lastName = self.obj.Label # initial name: might be modified later Deviation = self.obj.ViewObject.Deviation / 100.0 # generate the positions that get computed during export + self.export(doExport=False) def position(self): # This presumes export has been called before position() @@ -5945,7 +5737,7 @@ def name(self): prefix = "X" return prefix + self.lastName - def generate_surfaces(self): + def export(self, doExport=True): # The placement of the extruded item gets calculated here, during export # but boolean exporter tries to get the position BEFORE the export here happens # so to generate the position before the export happens, the doExport flag is used @@ -5959,13 +5751,9 @@ def generate_surfaces(self): extrudeDirection = extrudeObj.Dir # rotation to take extrude direction to z -axis - savedSketchPlacement = self.sketchObj.Placement - self.sketchObj.Placement *= savedSketchPlacement.inverse() - # do we need a recompute after this? - - # rot_dir_to_z = FreeCAD.Rotation(extrudeDirection, Vector(0, 0, 1)) - # edges = [edge.rotated(Vector(0, 0, 0), rot_dir_to_z.Axis, math.degrees(rot_dir_to_z.Angle)) for edge in sketchObj.Shape.Edges] - edges = sketchObj.Shape.Edges + rot_dir_to_z = FreeCAD.Rotation(extrudeDirection, Vector(0, 0, 1)) + edges = [edge.rotated(Vector(0, 0, 0), rot_dir_to_z.Axis, math.degrees(rot_dir_to_z.Angle)) for edge in + sketchObj.Shape.Edges] sortededges = Part.sortEdges(edges) # sort by largest area to smallest area sortEdgelistsByFaceArea(sortededges) @@ -5974,7 +5762,6 @@ def generate_surfaces(self): # Make names based on Extrude name # curves = [getCurve(edges, extrudeObj.Label + str(i)) for i, edges # in enumerate(sortededges)] - if extrudeObj.Symmetric is True: height = extrudeObj.LengthFwd.Value else: @@ -5995,17 +5782,8 @@ def generate_surfaces(self): lst = root.preOrderTraversal(root) rootnode = lst[0][0] rootCurve = rootnode.closedCurve - rootCurve.generate_surfaces() # a curve is created with a unique name - - # Our construction assumes extrusion is along z-axis - # and that the sketch object does not have any placement AND - # that the edges in a acurve are positioned relative to sketc, - # It turns out that is WRONG. The edges already fold in the sketch's - # placement. So we undo the effect of the sketched non-identity - # placement and then apply it back again at the end - - self.region = rootCurve.region - + if doExport: + rootCurve.export() # a curve is created with a unique name firstName = rootCurve.name booleanName = firstName @@ -6014,705 +5792,129 @@ def generate_surfaces(self): rootCurve.rotation ) # for now consider only angle of rotation about z-axis - self.surfaces = rootCurve.surfaces - for c in lst[1:]: node = c[0] parity = c[1] curve = node.closedCurve - curve.generate_surfaces() - self.surfaces += curve.surfaces + if doExport: + curve.export() if parity == 0: - self.region = self.region.union(curve.region) - else: - self.region = self.region.cut(curve.region) - - if extrudeObj.Symmetric is False: - if extrudeObj.Reversed is False: - zoffset = extrudeObj.LengthRev.Value - else: - zoffset = extrudeObj.LengthFwd.Value - else: - zoffset = extrudeObj.LengthFwd.Value / 2 - - axis = Vector(0, 0, 1) - top_surface = PlaneSurfaceExporter(self.obj.Label + '_top', axis, zoffset + height) - bottom_surface = PlaneSurfaceExporter(self.obj.Label + '_bot', axis, zoffset) - self.surfaces += [bottom_surface, top_surface] - self.region = Region(self.region.expr + f" -{top_surface.id} +{bottom_surface.id}") - - # Restore the sketch Placement - # undo the sketch placement - self.sketchObj.Placement = savedSketchPlacement - print(f"Extrude global placement = {self.obj.getGlobalPlacement()}") - print(f"Sketch local placement = {self.sketchObj.Placement}") - globalPlacement = self.obj.getGlobalPlacement() * self.sketchObj.Placement - print(f"sketch elements global placement is {globalPlacement}") - - T = globalPlacement.Base - R = globalPlacement.Rotation - - for surf in self.surfaces: - surf.rotate(R) - surf.translate(T) - - -# --------------------Auto tessellation classes and routines ----------------------------------------------------------- - -class Plane: - def __init__(self, normal, D): - self.normal = normal - self.D = D - - def inside(self, point): - return self.normal.dot(point) < self.D - - def __hash__(self): - s = [float(f" {f:.4e}") for f in self.normal] - s.append(float(f" {self.D:.4e}")) - return hash(tuple(s)) - - def __eq__(self, other): - if not (type(self) is type(other)): - return False - - return hash(self) == hash(other) - - -class RegionSet: - def __init__(self, v2planes_dict, mesh): - ''' v2planes_dict is a dictionary of a mesh point index and all the Planes that pass through that point - and a Plane that contains it as value ''' - self.v2p = v2planes_dict - self.mesh = mesh - - def is_edge_inside(self, edge, plane): - ''' test if the edge (a pair of point indexes is inside this RegionSet - To be inside both points must be in the set of points AND there must be points - in the set on BOTH sides of the given plane - ''' - - mypoints = self.v2p.keys() - - count = 0 - for vid in edge: - if vid in mypoints: - count += 1 - if count >= 2: - break - - if count < 2: - return False - - nplus = 0 # number of points on the + side of the plane - nminus = 0 # number of points on the nedgative side of the plane - - eps = 1.0e-9 - for vid in mypoints: - point = self.mesh.Points[vid].Vector - dist = plane.normal.dot(point) - plane.D - if dist < -eps: - nminus += 1 - elif dist > eps: - nplus += 1 - if nplus > 0 and nminus > 0: - return True - - return False - - def split_by_plane(self, plane): - ''' split this region set into two region sets, one where all the points are on the inside of - the plane and one on the outside''' - - region1 = defaultdict(list) - region2 = defaultdict(list) - - eps = 1.0e-9 - onplane = [] # list of points on the plane (gain, there are point indexes, not points) - for vid in list(self.v2p.keys()): - vec = self.mesh.Points[vid].Vector - dist = plane.normal.dot(vec) - plane.D - if dist < -eps: - region1[vid] = self.v2p[vid][:] - elif dist > eps: - region2[vid] = self.v2p[vid][:] # otherwise, put the planes in region 2 + boolType = "union" + secondName = curve.name + secondPos = curve.position else: - onplane.append(vid) - - # add the splitting plane to the edge points and remove the "wrong" face from the edge vertexes - plane1 = plane # just a change of notation for clarity - plane2 = Plane(-plane.normal, -plane.D) # flip the cutting plane for region2 - for vid in onplane: - region1[vid] = self.v2p[vid][:] - region2[vid] = self.v2p[vid][:] - for pl in region1[vid][:]: # copy so we don't screw up the list while we're iterating over it - if pl.normal.dot(plane1.normal) < 0: - region1[vid].remove(pl) # remove the wrong plane - region1[vid].append(plane1) # and add the cutting plane - - for pl in region2[vid][:]: # copy so we don't screw up the list while we're iterating over it - if pl.normal.dot(plane2.normal) < 0: - region2[vid].remove(pl) # remove the wrong plane - region2[vid].append(plane2) # and add the cutting plane - - R1 = RegionSet(region1, self.mesh) - R2 = RegionSet(region2, self.mesh) - - return R1, R2 - - -class ShapeMesher: - deflection = 0.5 - angularDeflection = math.radians(28.5) - - def __init__(self, shape): - import MeshPart - - self.shape = shape - self.mesh = MeshPart.meshFromShape(Shape=shape, LinearDeflection=ShapeMesher.deflection, - AngularDeflection=ShapeMesher.angularDeflection, Relative=False) - - self.mesh.harmonizeNormals() - # Debug - - self.edge_dict = self.edge_face_dict() - self.parallel_face_dict = self.parallel_set() - - print(f"Num faces = {len(self.mesh.Facets)}") - print(f"Num edges = {len(self.edge_dict)}") - - def _edge_dir(self, edge): - """Return a unit vector along the geometric edge defined by mesh point indices.""" - idx = list(edge) - v0 = self.mesh.Points[idx[0]].Vector - v1 = self.mesh.Points[idx[1]].Vector - v = v1 - v0 - if v.Length == 0: - return v - v.normalize() - return v - - def plane(self, facet): - normal = facet.Normal - vid = facet.PointIndices[0] - p0 = self.mesh.Points[vid].Vector - D = normal.dot(p0) - return Plane(normal, D) - - def vertex_to_planes_map(self): - v2planes = {} - for fi, facet in enumerate(self.mesh.Facets): - for vid in facet.PointIndices: - plane = self.plane(facet) - v2planes[vid] = plane - return v2planes - - def parallel_set(self): - ''' return a dictionary of face as key and set of faces that are parallel with that face''' - par_face_dict = {} - for face1 in self.mesh.Facets: - s = set() - normal1 = face1.Normal - for face2 in self.mesh.Facets: - if face1.Index == face2.Index: - continue - normal2 = face2.Normal - if normal2.dot(normal1) > 0.999: - s.add(face2.Index) - par_face_dict[face1.Index] = s - - return par_face_dict - - def edge_face_dict(self): - d = {} - for face in self.mesh.Facets: - pointIndexes = face.PointIndices - n = len(pointIndexes) - for i in range(n): - i1 = (i + 1) % n - - edge = (pointIndexes[i], pointIndexes[i1]) - fz = frozenset(edge) - if fz not in d: - d[fz] = [face.Index] - else: - d[fz].append(face.Index) - return d - - def is_mesh_concave(self): - edge, _ = self.get_concave_edge() - return edge is not None - - def my_concave_edge_test(self, edge): - faceIndexes = self.edge_dict[edge] - f1 = self.mesh.Facets[faceIndexes[0]] - f2 = self.mesh.Facets[faceIndexes[1]] - c1 = (Vector(f1.Points[0]) + Vector(f1.Points[1]) + Vector(f1.Points[2])) / 3 - c2 = (Vector(f2.Points[0]) + Vector(f2.Points[1]) + Vector(f2.Points[2])) / 3 - eps = (c2 - c1).Length / 10 - p1 = c1 - eps * f1.Normal - p2 = c2 - eps * f2.Normal - - if abs(f1.Normal.dot(f2.Normal)) > 0.999: - return False - - if (p2 - p1).Length > (c2 - c1).Length: - self.display_edge(edge) - return True - - return False - - def get_concave_edge(self): - """ - Return (edge, nmean, max_concave_angle) where: - - edge: a concave edge chosen among all concave candidates - - nmean: n1 + n2 for that edge (for your plane construction) - - max_concave_angle: maximum dihedral angle (in radians) among - all edges that are classified as concave - - If no concave edges are found, returns (None, None, 0.0). - """ - # Your “interesting concavity” threshold per *edge* - MIN_DIHEDRAL_ANGLE = math.radians(5.0) # tune as you like - MIN_SIN = math.sin(MIN_DIHEDRAL_ANGLE) - - # How close to -1 we require (n1 × n2) · ê to be - EPS_SIGN = 1e-3 # tolerance; adjust if needed - - best_edge = None - best_nmean = None - - best_sin_concave = 0.0 # sin(dihedral) for the **most concave** edge (among concave ones) - chosen_debug = None - - for edge, faceIndexes in self.edge_dict.items(): - # Only internal edges with exactly two adjacent facets - if len(faceIndexes) != 2: - continue - - f1 = self.mesh.Facets[faceIndexes[0]] - f2 = self.mesh.Facets[faceIndexes[1]] - - v_edge = self._edge_dir(edge) - if v_edge.Length == 0: - continue - - n1 = f1.Normal - n2 = f2.Normal - - # Cross product gives a vector along the intersection line - n1xn2 = n1.cross(n2) - sinthet = n1xn2.Length # ~ sin(dihedral angle between n1 and n2) - - # 1) Filter out almost-parallel normals: flat or nearly flat adjacency - if sinthet < MIN_SIN: - continue - - # 2) Concavity sign test using your sign convention - n1xn2.normalize() - dot_ce = n1xn2.dot(v_edge) - - # We consider the edge concave only if the cross product - # is pointing reliably in the opposite direction of the edge - if self.my_concave_edge_test(edge): - # print("my_concave_edge_test says this edge is convex. The get concave_edge says it is concave") - if sinthet > best_sin_concave: - best_sin_concave = sinthet - best_edge = edge - best_nmean = n1 + n2 - - angle = math.asin(max(min(sinthet, 1.0), -1.0)) + boolType = "subtraction" + secondName = ( + curve.name + "_s" + ) # scale solids along z, so it punches thru + if doExport: + scaleUp(secondName, curve.name, 1.10) + secondPos = curve.position - Vector(0, 0, 0.01 * height) + + booleanName = curve.name + "_bool" + if doExport: + boolSolid = ET.SubElement(solids, boolType, {"name": booleanName}) + ET.SubElement(boolSolid, "first", {"ref": firstName}) + ET.SubElement(boolSolid, "second", {"ref": secondName}) + relativePosition = secondPos - rootPos + zAngle = curve.rotation[2] - rootRot[2] + posName = curve.name + "_pos" + rotName = curve.name + "_rot" + if doExport: + exportDefine( + posName, relativePosition + ) # position of second relative to first + ET.SubElement( + define, + "rotation", + { + "name": rotName, + "unit": "deg", + "x": "0", + "y": "0", + "z": str(zAngle), + }, + ) - if best_edge is None: - # No concave edges found at all - return None, None, 0.0 + ET.SubElement(boolSolid, "positionref", {"ref": posName}) + ET.SubElement(boolSolid, "rotationref", {"ref": rotName}) + firstName = booleanName - # Convert the maximum concave sin(angle) to an angle in radians - max_concave_angle = math.asin( - max(min(best_sin_concave, 1.0), -1.0) + self.lastName = ( + booleanName # our name should the name f the last solid created ) - return best_edge, best_nmean, max_concave_angle - - def share_edge(self, iFace1, iFace2): - ''' return true if faces with indexes iFace1, iFace2 share an edge''' - - edges_set = set() - face1 = self.mesh.Facets[iFace1] - face2 = self.mesh.Facets[iFace2] - for f in [face1, face2]: - pointIndexes = f.PointIndices - n = len(f.PointIndices) - for i in range(n): - i1 = (i + 1) % n - edge = (pointIndexes[i], pointIndexes[i1]) - fz = frozenset(edge) - edges_set.add(fz) - - return len(edges_set) < 6 # if the faces share an edge, then there should be less than 6 edges in the set - - def get_face_edges(self, iFace): - ''' return the three edges as a frozenset of the end point indexes of the endpoint''' - face = self.mesh.Facets[iFace] - pointIndexes = face.PointIndices - n = len(pointIndexes) - edges = [] - for i in range(n): - i1 = (i + 1) % n - edge = (pointIndexes[i], pointIndexes[i1]) - fz = frozenset(edge) - edges.append(fz) - - return edges - - def concave_face_chains(self): - ''' return a list of a list of faces that share an edge and that are concave at the shared edge''' - concave_edges = self.get_concave_edges() - - def concave_edge_chain(edge, chain_set): - if edge in concave_edges: - chain_set.add(edge) - concave_edges.remove(edge) - f1 = self.edge_dict[edge][0] - f2 = self.edge_dict[edge][1] - for f in [f1, f2]: - edges = self.get_face_edges(f) - for e in edges: - concave_edge_chain(e, chain_set) + # Because the position of each closed curve might not be at the + # origin, whereas primitives (tubes, cones, etc, are created centered + # at the origin, we need to shift the position of the very first node + # by its position, in addition to the shift by the Extrusion placement + extrudeObj = self.obj + extrudePosition = extrudeObj.Placement.Base + if extrudeObj.Symmetric is False: + if extrudeObj.Reversed is False: + zoffset = Vector(0, 0, extrudeObj.LengthRev.Value) else: - return - - concave_chains = [] - while len(concave_edges) > 0: - edge_set = set() - edge = concave_edges[0] - concave_edge_chain(edge, edge_set) - - face_set = set() - for e in edge_set: - f1 = self.edge_dict[e][0] - f2 = self.edge_dict[e][1] - face_set.add(f1) - face_set.add(f2) - # add all edges that are parallel to the two faces to the same chain - for f1par in self.parallel_face_dict[f1]: - face_set.add(f1par) - for f2par in self.parallel_face_dict[f2]: - face_set.add(f2par) - - concave_chains.append(face_set) - - return concave_chains - - def get_concave_edges(self): - """ - return list of concave edges - """ - chosen_debug = None - - concaveList = [] - for edge, faceIndexes in self.edge_dict.items(): - # Only internal edges with exactly two adjacent facets - if len(faceIndexes) != 2: - continue - - f1 = self.mesh.Facets[faceIndexes[0]] - f2 = self.mesh.Facets[faceIndexes[1]] - - v_edge = self._edge_dir(edge) - if v_edge.Length == 0: - continue - - if self.my_concave_edge_test(edge): - concaveList.append(edge) - - return concaveList - - def is_valid(self): - ''' - Validate mesh: - (1) Volume must be > deflection^3 - (2) All faces must have area > deflection^2 - (3) All edges must have length > deflection - (4) all normals must have length > 0 - (5) number of faces must greater than 4 - ''' - nfaces = self.mesh.CountFacets - if nfaces <= 4: - print(f"mesh has 4 or fewer faces = {nfaces}") - return 5 - - if self.mesh.Volume < ShapeMesher.deflection ** 3: - print(f"mesh volume {self.mesh.Volume} less than deflectin^3 {ShapeMesher.deflection ** 3}") - return 1 - - for i, face in enumerate(self.mesh.Facets): - if face.Normal.Length < 0.5: - print(f"face {i}'s normal length is less than 1: {face.Normal.Length}") - return 4 - if face.Area < ShapeMesher.deflection * ShapeMesher.deflection: - print(f"face area smaller than deflection^2: {face.Area}") - return 2 - - n = len(face.Points) - for j in range(n): - v0 = Vector(face.Points[j][0], face.Points[j][1], face.Points[j][2]) - j1 = (j + 1) % n - v1 = Vector(face.Points[j1][0], face.Points[j1][1], face.Points[j1][2]) - edge_len = (v1 - v0).Length - if edge_len < ShapeMesher.deflection: - print( - f"Warning edge {j} of face {i} is shorter than deflection: edge length = {edge_len} deflection = {ShapeMesher.deflection}") - continue - - return 0 - - def display_face(self, iFace, color): - facet = self.mesh.Facets[iFace] - pnts = facet.Points - vecs = [Vector(pnt) for pnt in pnts] - vecs.append(vecs[0]) - wire = Part.makePolygon(vecs) - face = Part.Face(wire) - obj = Part.show(face) - obj.ViewObject.ShapeColor = color - - normal = facet.Normal - c = (vecs[0] + vecs[1] + vecs[2]) / 3 - p1 = c - 0.5 * normal - p2 = c + 2.0 * normal - L = Part.LineSegment(p1, p2) - Part.show(L.toShape(), "normal") - - def display_edge(self, edge): - pts = list(edge) - idx1 = pts[0] - idx2 = pts[1] - v1 = Vector(self.mesh.Points[idx1].x, self.mesh.Points[idx1].y, self.mesh.Points[idx1].z) - v2 = Vector(self.mesh.Points[idx2].x, self.mesh.Points[idx2].y, self.mesh.Points[idx2].z) - L = Part.LineSegment(v1, v2) - shape = L.toShape() - # shape.ViewObject.LineColor = (255, 255, 255) - Part.show(shape, "edge") - - def edge_plane(self, edge): - ''' Return a plane that passes through the edge. The plane is midway between the two - faces that border thed edge - ''' - faceIndexes = self.edge_dict[edge] - f1 = self.mesh.Facets[faceIndexes[0]] - f2 = self.mesh.Facets[faceIndexes[1]] - v_edge = self._edge_dir(edge) - n1 = f1.Normal - n2 = f2.Normal - nmean = n1 + n2 # only direction matters, so we don't need to normalize - - plane_normal = v_edge.cross(nmean) # note that the sense of the plane_normal could be eiher way - plane_normal.normalize() - p0 = self.mesh.Points[list(edge)[0]].Vector # one of the points on the edge - D = plane_normal.dot(p0) - - return Plane(plane_normal, D) - - def split_along_concaves(self): - ''' Returns a list of RegionSets that are presumably all convex - It does this by splitting an initial RegionSet containing all the mesh points (point indexes, actually) - into two halves by a splitting plane that passes through a concaved edge. Do this until all the - concaved edges have been consumed - ''' - - concave_edges = self.get_concave_edges() - - # first form a RegionSet of all the points and planes of faces - v2p = defaultdict(list) - for fi, facet in enumerate(self.mesh.Facets): - for vid in facet.PointIndices: - plane = self.plane(facet) - v2p[vid].append(plane) - - r0 = RegionSet(v2p, self.mesh) - regions = [r0] - while len(concave_edges) > 0: - edge = concave_edges[0] - for region in regions: - plane = self.edge_plane(edge) # create a splitting plane along that edge - if region.is_edge_inside(edge, plane): # find which region edge is inside - region1, region2 = region.split_by_plane(plane) # get the two split regions - if region1 is None or region2 is None: - break # no split happened. Cutting by a degenerate plane, ie. a plane that has been used before - regions.remove(region) # remove the region that has been just split - regions.append(region1) # and replace with the two split regions - regions.append(region2) - break - concave_edges.remove(edge) # remove the splitting edge from the list of concave edges - - return regions - - -def compound_to_solids(shape): - """Convert a Compound (or other) to a list of Solids if possible.""" - if shape.ShapeType == "Solid": - return [shape] - sols = [] - # If we got a Compound of shells / faces, try to make solids from its shells - for sh in shape.Shells: - try: - sld = Part.makeSolid(sh) - except Exception: - continue - if sld.ShapeType == "Solid" and sld.isValid(): - sols.append(sld) - # If there are already Solids in the compound, include them - for s in shape.Solids: - if s.ShapeType == "Solid" and s.isValid(): - sols.append(s) - return sols - - -def common_via_bopfeatures(shapeA, shapeB, base_name="CommonTmp"): - """Compute intersection of two Part.Shapes using the same BOPFeatures - path that the GUI 'Common' command uses. Returns the resulting Shape.""" - doc = FreeCAD.ActiveDocument - - # 1) Create temporary Part::Feature objects - objA = doc.addObject("Part::Feature", base_name + "_A") - objA.Shape = shapeA - objB = doc.addObject("Part::Feature", base_name + "_B") - objB.Shape = shapeB - doc.recompute() - - # 2) Run BOPFeatures.make_multi_common on their names - bp = BOPFeatures.BOPFeatures(doc) - common_obj = bp.make_multi_common([objA.Name, objB.Name]) - doc.recompute() - - # 3) Extract the resulting Shape - common_shape = common_obj.Shape - - # (Optional) clean up the temp inputs, keep only result if you like - # doc.removeObject(objA.Name) - # doc.removeObject(objB.Name) - - return common_shape, common_obj - - -MAX_DEPTH = 50 - - -def decompose_recursive(shapeMesher, results, depth=0): - if depth >= MAX_DEPTH: - results.append(shapeMesher) - return - - mesh = shapeMesher.mesh - concave_edge, nmean, max_angle = shapeMesher.get_concave_edge() - MIN_GLOBAL_ANGLE = math.radians(5.0) - - if concave_edge is None or max_angle < MIN_GLOBAL_ANGLE: - results.append(shapeMesher) - return - - edge_pts = [mesh.Points[pntIndex].Vector for pntIndex in list(concave_edge)] - p0, p1 = edge_pts - edgeVec = p1 - p0 - # DEBUG - # edge_line = Part.makeLine(p0, p1) - # Part.show(edge_line, "edge_line") - - normal = edgeVec.cross(nmean) - if normal.Length == 0: - results.append(shapeMesher) - return - normal.normalize() - - base = shapeMesher.shape - bbox = base.BoundBox - planeWidth = 2 * bbox.DiagonalLength - - # (if you still want to re-center by COM, keep your translation here) - cuttingPlane = Part.makePlane(planeWidth, planeWidth, p0, normal) - # translate plane so center of mass of plane is at edge point - planeCM = cuttingPlane.CenterOfMass - cuttingPlane.Placement.Base -= planeCM - p0 - - if not cuttingPlane.isValid(): - results.append(shapeMesher) - return + zoffset = Vector(0, 0, extrudeObj.LengthFwd.Value) + else: + zoffset = Vector(0, 0, extrudeObj.LengthFwd.Value / 2) - # Make solid from shell - solid = Part.makeSolid(base) - if solid.ShapeType != "Solid" or not solid.isValid(): - results.append(shapeMesher) - return + angles = quaternion2XYZ(extrudeObj.Placement.Rotation) + # need to add rotations of elliptical tubes. + # Assume extrusion is on z-axis + # Probably will not work in general + zAngle = angles[2] + rootRot[2] + rootPos = rotatedPos(rootCurve, extrudeObj.Placement.Rotation) + print(rootPos) + Base = extrudePosition + rootPos - zoffset - # Build two slabs by extruding plane - n = cuttingPlane.normalAt(0.5, 0.5) - if n.Length == 0: - results.append(shapeMesher) - return - n.normalize() - h = solid.BoundBox.DiagonalLength * 2.0 - - upper_extrude = cuttingPlane.extrude(n * h) - lower_extrude = cuttingPlane.extrude(-n * h) - - # Use BOPFeatures (same as GUI) to get the intersections - upper, upper_obj = common_via_bopfeatures(solid, upper_extrude, "UpperCut") - lower, lower_obj = common_via_bopfeatures(solid, lower_extrude, "LowerCut") - - print("upper:", upper.ShapeType, "valid:", upper.isValid(), "vol:", upper.Volume) - print("lower:", lower.ShapeType, "valid:", lower.isValid(), "vol:", lower.Volume) - - child_shapes = [] - - # upper can be a Solid or a Compound; extract solids if needed - if upper.ShapeType == "Solid" and upper.isValid(): - child_shapes.append(upper) - elif upper.Solids: - for s in upper.Solids: - if s.isValid(): - child_shapes.append(s) - - if lower.ShapeType == "Solid" and lower.isValid(): - child_shapes.append(lower) - elif lower.Solids: - for s in lower.Solids: - if s.isValid(): - child_shapes.append(s) - - # If nothing usable came out, stop recursing on this shape - if not child_shapes: - results.append(shapeMesher) - return + rotX = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), angles[0]) + rotY = FreeCAD.Rotation(FreeCAD.Vector(0, 1, 0), angles[1]) + rotZ = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), zAngle) - for s in child_shapes: - print(f"child: type={s.ShapeType}, valid={s.isValid()}, vol={s.Volume}") - if not s.isValid(): - continue + rot = rotZ * rotY * rotX - mesher = ShapeMesher(s) + # rotate back to extrude direction + rot = rot_dir_to_z.inverted() * rot - code = mesher.is_valid() + placement = FreeCAD.Placement(Base, FreeCAD.Rotation(rot)) + self._position = placement.Base + self._rotation = placement.Rotation - if code == 1: # volume too small - continue - elif code == 5: # too few faces - results.append(mesher) - continue - elif code == 4: # normal too small. - results.append(mesher) - continue - # if s.Volume < ShapeMesher.deflection**3: - # results.append(ShapeMesher(s)) - # print("Skipped decomposing this shape. Volume too small") - # continue - # should check if the cutting resulted in more than one component - components = mesher.mesh.getSeparateComponents() - if len(components) > 1: - print("** cut mesh has more than one component **") +class GDMLMeshExporter(GDMLSolidExporter): + # FreeCAD Mesh only supports triangular Facets + def __init__(self, obj): + super().__init__(obj, 'tessellated') - decompose_recursive(mesher, results, depth + 1) + def export(self): + if self.exported(): + return + super().export() + + tessName = self.name().replace('\r', '').replace('(', '_').replace(')', '_') + # Use more readable version + tessVname = tessName + "_" + tess = ET.SubElement(solids, "tessellated", {"name": tessName}) + placementCorrection = self.obj.Placement.inverse() + for i, v in enumerate(self.obj.Mesh.Points): + v = FreeCAD.Vector(v.x, v.y, v.z) + exportDefineVertex(tessVname, placementCorrection * v, i) + for f in self.obj.Mesh.Facets: + indices = f.PointIndices + i0 = indices[0] + i1 = indices[1] + i2 = indices[2] + ET.SubElement( + tess, + "triangular", + { + "vertex1": tessVname + str(i0), + "vertex2": tessVname + str(i1), + "vertex3": tessVname + str(i2), + "type": "ABSOLUTE", + }, + ) + self._exportScaled() class AutoTessellateExporter(SolidExporter): @@ -6720,254 +5922,132 @@ class AutoTessellateExporter(SolidExporter): def __init__(self, obj): super().__init__(obj) - self.index = 0 - - # So my understanding of the structure of the mesh, so far, is the following - # if E is the number of Edges, F the number of faces and V the number of Volumes - # then Edge_ids go from 1 - E - # Face ids go form E+1 - E+1+F - # and volume ids go from E+F+1+1 - E+F+1+1+V - # so mesh.getElementNode(id) will get the edge node ids of id is in the Edge ids range - # will get the face id nodes for ids in the Face ids range - # and will get the volume id nodes for id in the Volume id range. - # For edges there will be two node ids, corresponding to the ends of the edge - # For faces, there will be three ids, corresponding to the triangle coordinates - # and for Volume ids, there will be four node ids, corresponding to the four corners of - # of the tetrahedron - # mesh.Node[id] gives the Vector coordinates of the node, that is the point - # P.S: Face ids are those of the boundary faces only, not of every tetrahedron surface - - # get a list of planes, one for each face. At present the sense of - # the normal is arbitrary. When we build the regions for each tetrahedron, we will - # figure out the in/out of each plane - - self.surfacesDict = {} # to reducs the number of duplicate planes for parallel faces - - def solidMesh(self): - import ObjectsFem - import femmesh.gmshtools as gmshtools - - def gmsh_sizes_from_view(obj, - elems_across=10, - interior_factor=3.0, - min_hmin=10.0): - """ - Map FreeCAD ViewObject Deviation / AngularDeflection to Gmsh mesh sizes. - Returns: - h_min, h_max, min_elems_2pi - """ - v = obj.ViewObject - - shp = obj.Shape - bbox = shp.BoundBox - - L = max(bbox.XLength, bbox.YLength, bbox.ZLength) - - if L <= 0: - # degenerate - h_min = min_hmin - else: - h_min = max(L / elems_across, min_hmin) - - h_max = h_min * interior_factor - return h_min, h_max - - doc = FreeCAD.ActiveDocument + def export(self): + import MeshPart - h_min, h_max = gmsh_sizes_from_view(self.obj) - # mesh = doc.addObject('Fem::FemMeshGmshFromShape', 'GmshMesh') - mesh = ObjectsFem.makeMeshGmsh(doc, f"{self.obj.Label}_temporary_mesh") - print(mesh, mesh.TypeId) + shape = self.obj.Shape.copy(False) + shape.Placement = FreeCAD.Placement() # remove object's placement + alreadyExportedName = AutoTessellateExporter.alreadyExported(shape) + if alreadyExportedName is not None: + self._name = alreadyExportedName + return - mesh.Shape = self.obj + else: + AutoTessellateExporter.shapesDict[shape] = self.name() + + viewObject = self.obj.ViewObject + deflection = viewObject.Deviation + angularDeflection = math.radians(viewObject.AngularDeflection) + mesh = MeshPart.meshFromShape(Shape=shape, LinearDeflection=deflection, + AngularDeflection=angularDeflection, Relative=False) + + tessName = self.name() + # Use more readable version + tess = ET.SubElement(solids, "tessellated", {"name": tessName}) + tessVname = tessName + "_" + placementCorrection = self.obj.Placement.inverse() + for i, v in enumerate(mesh.Points): + v = FreeCAD.Vector(v.x, v.y, v.z) + exportDefineVertex(tessVname, v, i) + for f in mesh.Facets: + indices = f.PointIndices + i0 = indices[0] + i1 = indices[1] + i2 = indices[2] + ET.SubElement( + tess, + "triangular", + { + "vertex1": tessVname + str(i0), + "vertex2": tessVname + str(i1), + "vertex3": tessVname + str(i2), + "type": "ABSOLUTE", + }, + ) + self._exportScaled() - mesh.CharacteristicLengthMin = h_min - mesh.CharacteristicLengthMax = h_max + @staticmethod + def centerOfMass(pts: [Vector]) -> Vector: + cm = Vector(0, 0, 0) + for pt in pts: + cm += pt - # Curvature-based refinement - mesh.MeshSizeFromCurvature = True + return cm - # 3D mesh algorithm and optimization - mesh.Algorithm3D = 1 # Tetrahedral - mesh.OptimizeStd = True + @staticmethod + def principalMoments(pts: [Vector]) -> tuple: + Ixx = 0 + Iyy = 0 + Izz = 0 + for pt in pts: + Ixx += pt.y * pt.y + pt.z * pt.z + Iyy += pt.x * pt.x + pt.z * pt.z + Izz += pt.x * pt.x + pt.y * pt.y - mesher = gmshtools.GmshTools(mesh) - mesher.create_mesh() - doc.recompute() + return Ixx, Iyy, Izz - # 4 loop over tetrahedra, creating planes and regions for each + @staticmethod + def identicalShapes(shp1, shp2) -> bool: + # return True if shapes are the same + verts1 = shp1.Vertexes + verts2 = shp2.Vertexes - fem_mesh = mesh.FemMesh - faces_attr = fem_mesh.Faces - if isinstance(faces_attr, dict): - face_ids = list(faces_attr.keys()) - else: - face_ids = list(faces_attr) - - # So my understanding of the structure of the mesh, so far, is the following - # if E is the number of Edges, F the number of faces and V the number of Volumes - # then Edge_ids go from 1 - E - # Face ids go form E+1 - E+1+F - # and volume ids go from E+F+1+1 - E+F+1+1+V - # so mesh.getElementNoded(id) will get the edge node ids of id is in the Edge ids range - # will get the face id nodes for ids in the Face ids range - # and will get the volume id nodes for id in the Volume id range. - # For edges there will be two node ids, corresponding to the ends of the edge - # For faces, there will be three ids, corresponding to the triangle coordinates - # and for Volume ids, there will be four node ids, corresponding to the four coreners of - # of the tetrahedron - # mesh.Node[id] gives the Vector coordinates of the node, that is the point - # P.S: Face ids are those of the boundary faces only, not of every tetrahedron surface - - # get a list of planes, one for each face. At present the sense of - # the normal is arbitrary. When we build the regions for each tetrahedron, we will - # figure out the in/out of each plane - - def plane_from_face(node_ids): - face_key = frozenset(node_ids) - coords = [fem_mesh.Nodes[n] for n in node_ids] - v1 = coords[1] - coords[0] - v2 = coords[2] - coords[1] - normal = v1.cross(v2) - normal.normalize() - D = normal.dot(coords[0]) - surf = PlaneSurfaceExporter(f"{self.obj.Label}_{vol_id}_{i}", normal, D) - plane_nodes_dict[face_key] = surf # given three node ids, we identify the plane - # regardless of the order in which node ids are given - surfaces.append(surf) - - surfaces = [] - plane_nodes_dict = {} - region_expr = "" - - for vol_id in fem_mesh.Volumes: - volume_region = "" - # Get the node IDs associated with this tetrahedron - # For a 1st order mesh, this will return 4 nodes - node_ids = fem_mesh.getElementNodes(vol_id) - for i in range(4): # we should doublecheck we are using tetrahedra - opposite_corner = node_ids[i] - face_corners = node_ids[:i] + node_ids[i + 1:] # other corners - face = frozenset(face_corners) - if face not in plane_nodes_dict: # if face not already in dictionary, build it - plane_from_face(face_corners) - vector_to_face = fem_mesh.Nodes[face_corners[0]] - fem_mesh.Nodes[opposite_corner] - surface = plane_nodes_dict[face] - if surface.normal.dot(vector_to_face) > 0: # normal points to outside, we want to be in - volume_region += f"-{surface.id} " - else: - volume_region += f"+{surface.id} " + # Test 1, same number of vertexes + if len(verts1) != len(verts2): + return False - if region_expr == "": - region_expr = f"({volume_region})" - else: - region_expr += f"|({volume_region})" + # Test 2, Center of mass + pts1 = [v.Point for v in shp1.Vertexes] + pts2 = [v.Point for v in shp2.Vertexes] + cm1 = AutoTessellateExporter.centerOfMass(pts1) + cm2 = AutoTessellateExporter.centerOfMass(pts2) + if (cm1 - cm2).Length > 1e-04: + return False - return surfaces, Region(region_expr) + # Test 3, compare volumes. + # I am not sure which is faster, moment of inertial calculation or volume calculation + # I Shape.Volume is calculated in C it is probably faster than CM and should be done first + # But I don't know for sure + if (shp1.Volume - shp2.Volume) > 1e-6: + return False - def face_to_plane(self, face): - def plane_key(): - return (round(normal.x, 7), round(normal.y, 7), round(normal.z, 7), round(D, 7)) + # Test 4, same moments of inertia + II1 = AutoTessellateExporter.principalMoments(pts1) + II2 = AutoTessellateExporter.principalMoments(pts2) - normal = Vector(face.Normal.x, face.Normal.y, face.Normal.z) - # remember we need to convert from mm to cm - point0 = Vector(face.Points[0][0], face.Points[0][1], face.Points[0][2]) - D = normal.dot(point0) - key = plane_key() - if key in self.surfacesDict: - return None # may be a poor way of signaling to the caller that the surface has been processed - else: - planesurface = PlaneSurfaceExporter(self.name() + f"_{self.index}", normal, D) - self.surfacesDict[key] = planesurface - - self.index += 1 - return planesurface - - def remove_redundant_planes(self, planes): - set_of_planes = set() - for plane in planes: - set_of_planes.add(plane) - return list(set_of_planes) - - def check_convex_face(self, face, mesh): - normal = Vector(face.Normal.x, face.Normal.y, face.Normal.z) - point0 = Vector(face.Points[0][0], face.Points[0][1], face.Points[0][2]) - D = normal.dot(point0) - pnts = [p.Vector for p in mesh.Points] - - for p in pnts: - if p.dot(normal) > D: + for i, II in enumerate(II1): + if abs(II1[i] - II2[i]) > 1e-08: return False - return True - - def generate_surfaces(self): - import MeshPart - global obj_top_children_dict - - if self.obj.TypeId != 'Mesh::Feature': - shape = self.obj.Shape.copy(False) - - bbox = shape.BoundBox - minLength = min([bbox.XLength, bbox.YLength, bbox.ZLength]) - - # shape.Placement = FreeCAD.Placement() # remove object's placement - - print(f"Autotessellating {self.obj.Label} which is of type {self.obj.TypeId}") - viewObject = self.obj.ViewObject - deflection = viewObject.Deviation # this is in percent - linearDeflection = deflection / 100 * minLength - angularDeflection = viewObject.AngularDeflection.Value - ShapeMesher.deflection = linearDeflection - ShapeMesher.angularDeflection = angularDeflection + # Well, ChatGPT says in principle one can have all moments of inertia to be the same for all axes + # and the shapes be different. Dr. Omar Hijab also convinced me of this. + # If all of the above is true, it is likely that the shapes are the same. But to be on + # the safe side, we sample a few of the points and assume they are ordered the same. + # If the shapes are really the same but because the ordering of the points is different then + # at worst we export the same shape twice. This is much better than exporting one shape, when in fact + # they are two different shapes. The likelihood that two different shapes match in order is extremely + # small + + # Test 4 + nsamples = int(len(verts1) / 10) + 1 + sampled = set() # to avoid sampling same point twice we keep track of already sampled points + if nsamples >= len(pts1): + nsamples = len(pts1) + + while len(sampled) < nsamples: + index = random.randint(0, nsamples - 1) + if (pts2[index] - pts1[index]).Length > 1e-04: + return False + sampled.add(index) - shapeMesher = ShapeMesher(shape) + return True - components = shapeMesher.mesh.getSeparateComponents() - else: - components = self.obj.Mesh.getSeparateComponents() - - self.region = Region("") - for i, comp in enumerate(components): - V = comp.CountPoints - E = comp.CountEdges - F = comp.CountFacets - chi = V - E + F - genus = (2 - chi) / 2 - print(f"Component {i} Verts = {V}, Edges = {E}, Faces = {F}, Eueler = {chi} genus = {genus}") - - shape = Part.Shape() - tolerance = 0.1 - shape.makeShapeFromMesh(comp.Topology, tolerance) - componentMesher = ShapeMesher(shape) - component_region = "" - - region_sets = componentMesher.split_along_concaves() - j = 0 - for region in region_sets: - planes = set() - for vid in region.v2p: - vid_planes = region.v2p[vid] - for pl in vid_planes: - planes.add(pl) - region_expr = "" - k = 0 - for plane in planes: - # i = mesh component index - # j = region index in this component - # k = plane index for planes in this region - surf = PlaneSurfaceExporter(self.name() + f"_{i}_{j}_{k}", Vector(plane.normal), plane.D) - k += 1 - self.surfaces.append(surf) - region_expr += f"-{surf.id} " - if component_region == "": - component_region = f"({region_expr})" - else: - component_region += f" | ({region_expr})" - j += 1 + @staticmethod + def alreadyExported(shape) -> str | None: + for shp in AutoTessellateExporter.shapesDict: + if AutoTessellateExporter.identicalShapes(shape, shp): + return AutoTessellateExporter.shapesDict[shp] # return name of shape - # TODO, test if the regions formaed are convex or not. - self.region = self.region.union(Region(component_region)) + return None # shape not already exported - self.position_globally()