From af79de8c01bad70ae7be1c25756dc26ce362510a Mon Sep 17 00:00:00 2001 From: DeepSOIC Date: Mon, 11 Jun 2018 18:41:31 +0300 Subject: [PATCH] New feature: PartDesign Pattern! --- InitGui.py | 6 + Lattice2.py | 2 + Lattice2PartDesignFeatures.py | 1 + lattice2AttachablePlacement.py | 7 +- lattice2BaseFeature.py | 32 ++- lattice2Common.py | 9 + lattice2GeomUtils.py | 15 ++ lattice2PDPattern.py | 394 +++++++++++++++++++++++++++++++++ lattice2ShapeCopy.py | 23 +- 9 files changed, 474 insertions(+), 15 deletions(-) create mode 100644 Lattice2PartDesignFeatures.py create mode 100644 lattice2PDPattern.py diff --git a/InitGui.py b/InitGui.py index 25d868e..83bd26b 100644 --- a/InitGui.py +++ b/InitGui.py @@ -83,6 +83,12 @@ class Lattice2Workbench (Workbench): ) self.appendToolbar('Lattice2CompoundFeatures', cmdsCompoundTools) self.appendMenu('Lattice2', cmdsCompoundTools) + + cmdsPDTools = ([] + + Lattice2.PartDesignFeatures.PDPattern.exportedCommands + ) + self.appendToolbar('Lattice2PartDesignFeatres', cmdsPDTools) + self.appendMenu('Lattice2', cmdsPDTools) cmdsGuiTools = ([] + Lattice2.GuiTools.Inspect.exportedCommands diff --git a/Lattice2.py b/Lattice2.py index 022418a..04dc313 100644 --- a/Lattice2.py +++ b/Lattice2.py @@ -31,6 +31,8 @@ import Lattice2CompoundFeatures as CompoundFeatures import Lattice2ArrayFeatures as ArrayFeatures +import Lattice2PartDesignFeatures as PartDesignFeatures + import Lattice2GuiTools as GuiTools import lattice2_rc as resource_module diff --git a/Lattice2PartDesignFeatures.py b/Lattice2PartDesignFeatures.py new file mode 100644 index 0000000..ccffce1 --- /dev/null +++ b/Lattice2PartDesignFeatures.py @@ -0,0 +1 @@ +import lattice2PDPattern as PDPattern diff --git a/lattice2AttachablePlacement.py b/lattice2AttachablePlacement.py index a4e4f28..662407c 100644 --- a/lattice2AttachablePlacement.py +++ b/lattice2AttachablePlacement.py @@ -50,7 +50,8 @@ def makeAttachablePlacement(name): ViewProviderAttachablePlacement(obj.ViewObject) else: obj = lattice2BaseFeature.makeLatticeFeature(name, AttachablePlacement, ViewProviderAttachablePlacement, no_disable_attacher= True) - obj.addExtension("Part::AttachExtensionPython", None) + if not obj.hasExtension('Part::AttachExtension'): + obj.addExtension("Part::AttachExtensionPython", None) return obj @@ -67,6 +68,10 @@ class AttachablePlacement(lattice2BaseFeature.LatticeFeature): obj.positionBySupport() return [obj.Placement] + + def onDocumentRestored(self, selfobj): + #override that disables disabling of attacher + pass class ViewProviderAttachablePlacement(lattice2BaseFeature.ViewProviderLatticeFeature): diff --git a/lattice2BaseFeature.py b/lattice2BaseFeature.py index 0b3bc65..2bf62e0 100644 --- a/lattice2BaseFeature.py +++ b/lattice2BaseFeature.py @@ -57,21 +57,12 @@ def makeLatticeFeature(name, AppClass, ViewClass, no_body = False, no_disable_at body = activeBody() if body and not no_body: - obj = body.newObject("Part::Part2DObjectPython",name) + obj = body.newObject("Part::Part2DObjectPython",name) #hack: body accepts any 2dobjectpython, thinking it is a sketch. Use it to get into body. This does cause some weirdness (e.g. one can Pad a placement), but that is rather minor. obj.AttacherType = 'Attacher::AttachEngine3D' - if not no_disable_attacher: - attachprops = [ - 'Support', - 'MapMode', - 'MapReversed', - 'MapPathParameter', - 'AttachmentOffset', - ] - for prop in attachprops: - obj.setEditorMode(prop, 2) #hidden else: obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython",name) AppClass(obj) + if FreeCAD.GuiUp: if ViewClass: vp = ViewClass(obj.ViewObject) @@ -254,6 +245,25 @@ class LatticeFeature(): def __setstate__(self,state): return None + def disableAttacher(self, selfobj, enable= False): + if selfobj.isDerivedFrom('Part::Part2DObject'): + attachprops = [ + 'Support', + 'MapMode', + 'MapReversed', + 'MapPathParameter', + 'AttachmentOffset', + ] + for prop in attachprops: + selfobj.setEditorMode(prop, 0 if enable else 2) + if enable: + selfobj.MapMode = selfobj.MapMode #trigger attachment, to make it update property states + + def onDocumentRestored(self, selfobj): + #override to have attachment! + self.disableAttacher(selfobj) + + class ViewProviderLatticeFeature: "A View Provider for base lattice object" diff --git a/lattice2Common.py b/lattice2Common.py index 40c9246..5554a95 100644 --- a/lattice2Common.py +++ b/lattice2Common.py @@ -34,6 +34,8 @@ def translate(context, text, disambig): def getParamRefine(): return FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Part/Boolean").GetBool("RefineModel") +def getParamPDRefine(): + return FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/PartDesign").GetBool("RefineModel") def getIconPath(icon_dot_svg): return ":/icons/" + icon_dot_svg @@ -93,3 +95,10 @@ def screen(feature): def activeBody(): return FreeCADGui.ActiveDocument.ActiveView.getActiveObject("pdbody") + +def bodyOf(feature): + body = feature.getParentGeoFeatureGroup() + if body.isDerivedFrom('PartDesign::Body'): + return body + else: + return None diff --git a/lattice2GeomUtils.py b/lattice2GeomUtils.py index d1af914..d71c7eb 100644 --- a/lattice2GeomUtils.py +++ b/lattice2GeomUtils.py @@ -30,6 +30,21 @@ __title__="Geometric utility routines for Lattice workbench for FreeCAD" __author__ = "DeepSOIC" __url__ = "" +def PlacementsFuzzyCompare(plm1, plm2): + pos_eq = (plm1.Base - plm2.Base).Length < 1e-7 # 1e-7 is OCC's Precision::Confusion + + q1 = plm1.Rotation.Q + q2 = plm2.Rotation.Q + # rotations are equal if q1 == q2 or q1 == -q2. + # Invert one of Q's if their scalar product is negative, before comparison. + if q1[0]*q2[0] + q1[1]*q2[1] + q1[2]*q2[2] + q1[3]*q2[3] < 0: + q2 = [-v for v in q2] + rot_eq = ( abs(q1[0]-q2[0]) + + abs(q1[1]-q2[1]) + + abs(q1[2]-q2[2]) + + abs(q1[3]-q2[3]) ) < 1e-12 # 1e-12 is OCC's Precision::Angular (in radians) + return pos_eq and rot_eq + def makeOrientationFromLocalAxes(ZAx, XAx = None): ''' diff --git a/lattice2PDPattern.py b/lattice2PDPattern.py new file mode 100644 index 0000000..1e4643a --- /dev/null +++ b/lattice2PDPattern.py @@ -0,0 +1,394 @@ +#*************************************************************************** +#* * +#* Copyright (c) 2018 - Victor Titov (DeepSOIC) * +#* * +#* * +#* 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 * +#* * +#*************************************************************************** + +__title__="Lattice PartDesign Pattern object: a partdesign pattern based on Lattice array." +__author__ = "DeepSOIC" +__url__ = "" + +import FreeCAD as App +import Part + +from lattice2Common import * +import lattice2BaseFeature +import lattice2Executer +from lattice2ShapeCopy import shallowCopy, transformCopy_Smart + +from lattice2PopulateCopies import DereferenceArray + + +class FeatureUnsupportedError(RuntimeError): + pass +class NotPartDesignFeatureError(RuntimeError): + pass +class FeatureFailure(RuntimeError): + pass +class ScopeError(RuntimeError): + pass + + +class MultiTransformSettings(object): + selfintersections = False #if True, take care of intersections between occurences. If False, optimize assuming occurences do not intersect. + sign_override = +1 #+1 for keep sign, -1 for invert, +2 for force positive, -2 for force negative + + +def makeFeature(): + '''makeFeature(): makes a PartDesignPattern object.''' + obj = activeBody().newObject("PartDesign::FeaturePython","LatticePattern") + LatticePDPattern(obj) + if FreeCAD.GuiUp: + ViewProviderLatticePDPattern(obj.ViewObject) + return obj + +def getBodySequence(body, skipfirst = False): + visited = set() + result = [] + curfeature = body.Tip + while True: + if curfeature in visited: + raise ValueError("Feature sequence is looped in {body}".format(body= body.Name)) + if curfeature is None: + break + if not curfeature.isDerivedFrom('PartDesign::Feature'): + break + if not body.hasObject(curfeature): + break + if curfeature.isDerivedFrom('PartDesign::FeatureBase'): + #base feature for body. Do not include. + break + visited.add(curfeature) + result.insert(0, curfeature) + curfeature = curfeature.BaseFeature + if skipfirst: + result.pop(0) + return result + +def feature_sign(feature, raise_if_unsupported = False): + """feature_sign(feature, raise_if_unsupported = False): returns +1 for additive PD features, -1 for subtractive PD features, and 0 for the remaining (unsupported)""" + additive_types = [ + 'PartDesign::Pad', + 'PartDesign::Revolution', + ] + subtractive_types = [ + 'PartDesign::Pocket', + 'PartDesign::Groove', + ] + def unsupported(): + if raise_if_unsupported: + raise FeatureUnsupportedError("Feature {name} is neither additive nor subtractive. Unsupported.".format(name= feature.Name)) + else: + return 0 + + if not feature.isDerivedFrom('PartDesign::Feature'): + raise NotPartDesignFeatureError("Feature {name} is not a PartDesign feature. Unsupported.".format(name= feature.Name)) + if hasattr(feature, 'AddSubType'): #part-o-magic; possibly PartDesign future + t = feature.AddSubType + if t == 'Additive': + return +1 + elif t == 'Subtractive': + return -1 + else: + return unsupported() + typ = feature.TypeId + if typ in additive_types: + return 1 + if typ in subtractive_types: + return -1 + if 'Additive' in typ: + return +1 + if 'Subtractive' in typ: + return -1 + if typ == 'PartDesign::Boolean': + t = feature.Type + if t == 'Fuse': + return +1 + elif t == 'Cut': + return -1 + else: + return unsupported() + return unsupported() + +def getFeatureShapes(feature): + sign = feature_sign(feature, raise_if_unsupported= True) + if hasattr(feature, 'AddSubShape'): + sh = shallowCopy(feature.AddSubShape) + sh.Placement = feature.Placement + return [(sign, sh)] + elif feature.isDerivedFrom('PartDesign::Boolean'): + return [(sign, obj.Shape) for obj in feature.Group] + else: + raise FeatureUnsupportedError("Feature {name} is not supported.".format(name= feature.Name)) + +def is_supported(feature): + if hasattr(feature, 'Proxy') and hasattr(feature.Proxy, 'applyTransformed'): + return True + try: + sign = feature_sign(feature, raise_if_unsupported= True) + return True + except FeatureUnsupportedError, NotPartDesignFeatureError: + return False + +def applyFeature(baseshape, feature, transforms, mts): + if hasattr(feature, 'Proxy') and hasattr(feature.Proxy, 'applyTransformed'): + return feature.Proxy.applyTransformed(feature, baseshape, transforms, mts) + task = getFeatureShapes(feature) + for sign,featureshape in task: + actionshapes = [] + for transform in transforms: + actionshapes.append(shallowCopy(featureshape, transform)) + + if mts.selfintersections: + pass #to fuse the shapes to baseshape one by one + else: + actionshapes = [Part.Compound(actionshapes)] #to fuse all at once, saving for computing intersections between the occurences of the feature + + for actionshape in actionshapes: + assert(sign != 0) + realsign = sign * mts.sign_override + if abs(mts.sign_override) == +2: + realsign = int(mts.sign_override / 2) + if realsign > 0: + baseshape = baseshape.fuse(actionshape) + elif realsign < 0: + baseshape = baseshape.cut(actionshape) + if baseshape.isNull(): + raise FeatureFailure('applying {name} failed - returned shape is null'.format(name= feature.Name)) + return baseshape + +class LatticePDPattern(object): + def __init__(self,obj): + obj.addProperty('App::PropertyLinkListGlobal','FeaturesToCopy',"Lattice Pattern","Features to be copied (can be a body)") + obj.addProperty('App::PropertyLinkGlobal','PlacementsFrom',"Lattice Pattern","Reference placement (placement that marks where the original feature is)") + obj.addProperty('App::PropertyLink','PlacementsTo',"Lattice Pattern","Target placements") + + obj.addProperty('App::PropertyEnumeration','Referencing',"Lattice Pattern","Reference placement mode (sets what to grab the feature by).") + obj.Referencing = ['Origin','First item', 'Last item', 'Use PlacementsFrom'] + + obj.addProperty('App::PropertyBool', 'IgnoreUnsupported', "Lattice Pattern", "Skip unsupported features such as fillets, instead of throwing errors") + obj.addProperty('App::PropertyBool', 'SkipFirstInBody', "Lattice Pattern", "Skip first body feature (which may be used as support for the important features).") + + obj.addProperty('App::PropertyEnumeration', 'SignOverride', "Lattice Pattern", "Use it to change Pockets into Pads.") + obj.SignOverride = ['keep', 'invert', 'as additive', 'as subtractive'] + + obj.addProperty('App::PropertyBool', 'Selfintersections', "Lattice Pattern", "If True, take care of intersections between occurences. If False, you get a slight speed boost.") + + obj.addProperty('App::PropertyBool', 'Refine', "PartDesign", "If True, remove redundant edges after this operation.") + obj.Refine = getParamPDRefine() + + obj.addProperty('App::PropertyBool', 'SingleSolid', "PartDesign", "If True, discard solids not joined with the base.") + + obj.Proxy = self + + def execute(self, selfobj): + if selfobj.BaseFeature is None: + baseshape = Part.Compound([]) + else: + baseshape = selfobj.BaseFeature.Shape + + mts = MultiTransformSettings() + mts.sign_override = {'keep': +1, 'invert': -1, 'as additive': +2 , 'as subtractive': -2}[selfobj.SignOverride] + mts.selfintersections = selfobj.Selfintersections + + result = self.applyTransformed(selfobj, baseshape, None, mts) + if selfobj.SingleSolid: + # not proper implementation, but should do for majority of cases: pick the largest solid. + vmax = 0 + vmax_solid = None + for s in result.Solids: + v = s.Volume + if v > vmax: + vmax = v + vmax_solid = s + if vmax_solid is None: + raise ValueError("No solids in result. Maybe the result is corrupted because of failed BOP, or all the material was removed in the end.") + result = vmax_solid + if selfobj.SingleSolid or len(result.Solids) == 1: + result = transformCopy_Smart(result.Solids[0], selfobj.Placement) + if selfobj.Refine: + result = result.removeSplitter() + selfobj.Shape = result + + def applyTransformed(self, selfobj, baseshape, transforms, mts): + featurelist = [] + has_bodies = False + has_features = False + for lnk in selfobj.FeaturesToCopy: + if lnk.isDerivedFrom('PartDesign::Body'): + featurelist.extend(getBodySequence(lnk, skipfirst= selfobj.SkipFirstInBody)) + has_bodies = True + else: + featurelist.append(lnk) + has_features = True + + #check cross-links + if selfobj.Referencing == 'Use PlacementsFrom': + body_ref = bodyOf(selfobj.PlacementsFrom) + else: + body_ref = bodyOf(selfobj) + for feature in featurelist: + if bodyOf(feature) is not body_ref: + raise ScopeError('Reference placement and the feature are not in the same body (use Shapebinder or Ghost to bring the placement in).') + + + placements = lattice2BaseFeature.getPlacementsList(selfobj.PlacementsTo, selfobj) + placements = DereferenceArray(selfobj, placements, selfobj.PlacementsFrom, selfobj.Referencing) + if selfobj.Referencing == 'First item' and transforms is None: + placements.pop(0) #to not repeat the feature where it was applied already + elif selfobj.Referencing == 'Last item' and transforms is None: + placements.pop() #to not repeat the feature where it was applied already + if not transforms is None: + newplacements = [] + for transform in transforms: + newplacements += [transform.multiply(plm) for plm in placements] + placements = newplacements + for feature in featurelist: + try: + baseshape = applyFeature(baseshape, feature, placements, mts) + except FeatureUnsupportedError as err: + if not selfobj.IgnoreUnsupported: + raise + else: + App.Console.PrintLog('{name} is unsupported, skipped.\n'.format(name= feature.Name)) + return baseshape + + def __getstate__(self): + return None + + def __setstate__(self,state): + return None + + +class ViewProviderLatticePDPattern: + "A View Provider for the Lattice PartDesign Pattern object" + + def __init__(self,vobj): + vobj.Proxy = self + + def getIcon(self): + return getIconPath("Lattice2_PDPattern.svg") + + def attach(self, vobj): + self.ViewObject = vobj + self.Object = vobj.Object + + + def __getstate__(self): + return None + + def __setstate__(self,state): + return None + + def claimChildren(self): + return [self.Object.PlacementsTo] + + def onDelete(self, feature, subelements): # subelements is a tuple of strings + return True + +# -------------------------- /document object -------------------------------------------------- + +# -------------------------- Gui command -------------------------------------------------- + + +def CreateLatticePDPattern(features, latticeObjFrom, latticeObjTo, refmode): + FreeCADGui.addModule("lattice2PDPattern") + FreeCADGui.addModule("lattice2Executer") + + #fill in properties + FreeCADGui.doCommand("f = lattice2PDPattern.makeFeature()") + reprfeatures = ', '.join(['App.ActiveDocument.'+f.Name for f in features]) + FreeCADGui.doCommand("f.FeaturesToCopy = [{features}]".format(features= reprfeatures)) + FreeCADGui.doCommand("f.PlacementsTo = App.ActiveDocument."+latticeObjTo.Name) + if latticeObjFrom is not None: + FreeCADGui.doCommand("f.PlacementsFrom = App.ActiveDocument."+latticeObjFrom.Name) + FreeCADGui.doCommand("f.Referencing = "+repr(refmode)) + + #execute + FreeCADGui.doCommand("lattice2Executer.executeFeature(f)") + + #hide something + FreeCADGui.doCommand("f.PlacementsTo.ViewObject.hide()") + FreeCADGui.doCommand("f.BaseFeature.ViewObject.hide()") + + #finalize + FreeCADGui.doCommand("Gui.Selection.addSelection(f)") + FreeCADGui.doCommand("f = None") + + +def cmdPDPattern(): + sel = FreeCADGui.Selection.getSelectionEx() + (lattices, shapes) = lattice2BaseFeature.splitSelection(sel) + if len(shapes) > 0 and len(lattices) == 2: + FreeCAD.ActiveDocument.openTransaction("Lattice Pattern") + latticeFrom = lattices[0] + latticeTo = lattices[1] + CreateLatticePDPattern([so.Object for so in shapes], latticeFrom.Object, latticeTo.Object,'Use PlacementsFrom') + deselect(sel) + FreeCAD.ActiveDocument.commitTransaction() + elif len(shapes) > 0 and len(lattices) == 1: + FreeCAD.ActiveDocument.openTransaction("Lattice Pattern") + latticeTo = lattices[0] + CreateLatticePDPattern([so.Object for so in shapes], None, latticeTo.Object,'First item') + deselect(sel) + FreeCAD.ActiveDocument.commitTransaction() + else: + raise SelectionError("Bad selection", + "Please select either:\n" + " one or more PartDesign features, and one or two placements/arrays \n" + "or\n" + " a template body and two placements/arrays, one from selected body and one from active body." + ) + + +class CommandLatticePDPattern: + "Command to create Lattice PartDesign Pattern feature" + def GetResources(self): + return {'Pixmap' : getIconPath("Lattice2_PDPattern.svg"), + 'MenuText': "Lattice PartDesign Pattern", + 'Accel': "", + 'ToolTip': "Lattice PartDesign Pattern command. Replicates partdesign features at every placement in array."} + + def Activated(self): + try: + if len(FreeCADGui.Selection.getSelection())==0: + infoMessage("Lattice PartDesign Pattern", + "Lattice PartDesign Pattern command. Replicates partdesign features at every placement in array.\n\n" + "Please select features to repeat, reference placement (optional), and target placement/array. \n\n" + "You can use features from another body. Then, reference placement is required. You can also select a body (a \"template body\"), then all features from that body will be replicated.\n\n" + "Please observe scope restrictions. Reference placement must be in same body the original features are in; target placement/array must be in active body. You can create Lattice Arrays " + "right in PartDesign bodies, but you can't drag them in after the fact. You can import arrays of placements from elsewhere using a Shapebinder, or Part-o-Magic Ghost.") + return + if activeBody() is None: + infoMessage("Lattice PartDesign Pattern", "No active body. Please, activate a body, first.") + cmdPDPattern() + except Exception as err: + msgError(err) + + def IsActive(self): + if FreeCAD.ActiveDocument: + return True + else: + return False + +if FreeCAD.GuiUp: + FreeCADGui.addCommand('Lattice2_PDPattern', CommandLatticePDPattern()) + +exportedCommands = ['Lattice2_PDPattern'] diff --git a/lattice2ShapeCopy.py b/lattice2ShapeCopy.py index d4f728a..1bbf19e 100644 --- a/lattice2ShapeCopy.py +++ b/lattice2ShapeCopy.py @@ -28,10 +28,12 @@ __doc__ = "Utility methods to copy shapes" import FreeCAD import Part +from lattice2GeomUtils import PlacementsFuzzyCompare def shallowCopy(shape, extra_placement = None): """shallowCopy(shape, extra_placement = None): creates a shallow copy of a shape. The - copy will match by isSame/isEqual/isPartner tests, but will have an independent placement.""" + copy will match by isSame/isEqual/isPartner tests, but will have an independent placement. + Supports matrix, but the matrix should be pure placement (not be mirroring).""" copiers = { "Vertex": lambda sh: sh.Vertexes[0], @@ -59,7 +61,7 @@ def shallowCopy(shape, extra_placement = None): def deepCopy(shape, extra_placement = None): """deepCopy(shape, extra_placement = None): Copies all subshapes. The copy will not match by isSame/isEqual/ - isPartner tests.""" + isPartner tests. If matrix is provided, redirects the call to transformCopy.""" if extra_placement is not None: if hasattr(extra_placement, 'toMatrix'): @@ -72,7 +74,7 @@ def deepCopy(shape, extra_placement = None): def transformCopy(shape, extra_placement = None): """transformCopy(shape, extra_placement = None): creates a deep copy shape with shape's placement applied to - the subelements (the placement of returned shape is zero).""" + the subelements (the placement of returned shape is zero). Supports matrices, including mirroring matrices.""" if extra_placement is None: extra_placement = FreeCAD.Placement() @@ -88,6 +90,21 @@ def transformCopy(shape, extra_placement = None): ret.transformShape(extra_placement.multiply(splm), True) return ret +def transformCopy_Smart(shape, feature_placement): + """transformCopy_Smart(shape, feature_placement): gets rid of shape's internal placement + (by applying transform to all its elements), and assigns feature_placement to the placement. + I.e. feature_placement is the additional transform to apply. Unlike transformCopy, creates + a shallow copy if possible. Does not support matrices.""" + + if shape.isNull(): + return shape + if PlacementsFuzzyCompare(shape.Placement, FreeCAD.Placement()): + sh = shallowCopy(shape) + else: + sh = transformCopy(shape) + sh.Placement = feature_placement + return sh + copy_types = ["Shallow copy", "Deep copy", "Transformed deep copy"] copy_functions = [shallowCopy, deepCopy, transformCopy]