#!/usr/bin/env python3
# coding: utf-8
#
# libraries for FreeCAD's Assembly 4 workbench
#
# LGPL
# Copyright HUBERT Zoltán



"""
    +-----------------------------------------------+
    |          shouldn't these be DEFINE's ?        |
    +-----------------------------------------------+
"""

import os
#__dir__ = os.path.dirname(__file__)
wbPath   = os.path.dirname(__file__)
iconPath = os.path.join( wbPath, 'Resources/icons' )
libPath  = os.path.join( wbPath, 'Resources/library' )

from PySide import QtGui, QtCore
import FreeCADGui as Gui
import FreeCAD as App
from FreeCAD import Console as FCC



# Types of datum objects
datumTypes = [  'PartDesign::CoordinateSystem', \
                'PartDesign::Plane',            \
                'PartDesign::Line',             \
                'PartDesign::Point']


partInfo =[     'PartID',                       \
                'PartName',                     \
                'PartDescription',              \
                'PartSupplier']

containerTypes = [  'App::Part', 'PartDesign::Body' ]


VEC_0 = App.Vector(0, 0, 0)
VEC_X = App.Vector(1, 0, 0)
VEC_Y = App.Vector(0, 1, 0)
VEC_Z = App.Vector(0, 0, 1)
VEC_T = App.Vector(1, 1, 1)


rotX = App.Placement( VEC_0, App.Rotation( VEC_X, 90) )
rotY = App.Placement( VEC_0, App.Rotation( VEC_Y, 90) )
rotZ = App.Placement( VEC_0, App.Rotation( VEC_Z, 90) )



def findObjectLink(obj, doc = App.ActiveDocument):
    for o in doc.Objects:
        if hasattr(o, 'LinkedObject'):
            if o.LinkedObject == obj:
                return o
    return(None)


def getSelectionPath(docName, objName, subObjName):
        val = []
        if (docName is None) or (docName == ''):
            docName = App.ActiveDocument.Name
        val.append(docName)
        if objName and (objName != ''):
            val.append(objName)
            if subObjName and (subObjName != ''):
                for son in subObjName.split('.'):
                    if son and (son != ''):
                        val.append(son)

        return val


"""
    +-----------------------------------------------+
    |           Object helper functions             |
    +-----------------------------------------------+
"""

def cloneObject(obj):
    container = obj.getParentGeoFeatureGroup()
    result = None
    if obj.Document and container:
        #result = obj.Document.copyObject(obj, False)
        result = obj.Document.addObject('App::Link', obj.Name)
        result.LinkedObject = obj
        result.Label = obj.Label
        container.addObject(result)
        result.recompute()
        #container = result.getParentGeoFeatureGroup()
        #if container:
        container.recompute()
        #if result.Document:
        result.Document.recompute()
    return result


def placeObjectToLCS( attObj, attLink, attDoc, attLCS ):
    expr = makeExpressionDatum( attLink, attDoc, attLCS )
    # FCC.PrintMessage('expression = '+expr)
    # indicate the this fastener has been placed with the Assembly4 workbench
    if not hasattr(attObj,'SolverId'):
        makeAsmProperties(attObj)
    # the fastener is attached by its Origin, no extra LCS
    attObj.AttachedBy = 'Origin'
    # store the part where we're attached to in the constraints object
    attObj.AttachedTo = attLink+'#'+attLCS
    # load the built expression into the Expression field of the constraint
    attObj.setExpression( 'Placement', expr )
    # Which solver is used
    attObj.SolverId = 'Asm4EE'
    # recompute the object to apply the placement:
    attObj.recompute()
    container = attObj.getParentGeoFeatureGroup()
    if container:
        container.recompute()
    if attObj.Document:
        attObj.Document.recompute()




"""
    +-----------------------------------------------+
    |      Create default Assembly4 properties      |
    +-----------------------------------------------+
"""
def makeAsmProperties( obj, reset=False ):
    # property AssemblyType
    # DEPRECATED
    '''
    if not hasattr(obj,'AssemblyType'):
        obj.addProperty( 'App::PropertyString', 'AssemblyType', 'Assembly' )
    obj.setPropertyStatus('AssemblyType','ReadOnly')
    '''
    # property AttachedBy
    if not hasattr(obj,'AttachedBy'):
        obj.addProperty( 'App::PropertyString', 'AttachedBy', 'Assembly' )
    obj.setPropertyStatus('AttachedBy'  ,'ReadOnly')
    # property AttachedTo
    if not hasattr(obj,'AttachedTo'):
        obj.addProperty( 'App::PropertyString', 'AttachedTo', 'Assembly' )
    obj.setPropertyStatus('AttachedTo'  ,'ReadOnly')
    # property AttachmentOffset
    if not hasattr(obj,'AttachmentOffset'):
        obj.addProperty( 'App::PropertyPlacement', 'AttachmentOffset', 'Assembly' )
    # property SolverId
    if not hasattr(obj,'SolverId'):
        obj.addProperty( 'App::PropertyString', 'SolverId', 'Assembly' )
    if reset:
        obj.AttachedBy = ''
        obj.AttachedTo = ''
        obj.AttachmentOffset = App.Placement()
        obj.SolverId = ''
    return


# checks whether there is a Variables container, and returns it
def getVarContainer():
    retval = None
    # check whether there already is a Variables object
    variables = App.ActiveDocument.getObject('Variables')
    if variables and variables.TypeId=='App::FeaturePython':
            # signature of a PropertyContainer
            if hasattr(variables,'Type') :
                if variables.Type == 'App::PropertyContainer':
                    retval = variables
    return retval


# the Variables container
def makeVarContainer():
    retval = None
    # check whether there already is a Variables object
    variables = App.ActiveDocument.getObject('Variables')
    if variables :
        if variables.TypeId=='App::FeaturePython':
            # signature of a PropertyContainer
            if hasattr(variables,'Type') :
                if variables.Type == 'App::PropertyContainer':
                    retval = variables
            # for compatibility
            else: 
                variables.addProperty('App::PropertyString', 'Type')
                variables.Type = 'App::PropertyContainer'            
                retval = variables
        else:
            FCC.PrintWarning('This Part contains an incompatible \"Variables\" object, ')
            FCC.PrintWarning('this could lead to unexpected results\n')
    # there is none, so we create it
    else:
        variables = App.ActiveDocument.addObject('App::FeaturePython','Variables')
        variables.ViewObject.Proxy = setCustomIcon(object,'Asm4_Variables.svg')
        # signature or a PropertyContainer
        variables.addProperty('App::PropertyString', 'Type')
        variables.Type = 'App::PropertyContainer'
        retval = variables
    return retval


# custom icon
# views/view_custom.py
# https://wiki.freecadweb.org/Viewprovider
# https://wiki.freecadweb.org/Custom_icon_in_tree_view
#
# obj = App.ActiveDocument.addObject("App::FeaturePython", "Name")
# obj.ViewObject.Proxy = ViewProviderCustomIcon( obj, path + "FreeCADIco.png")
# icon download to file
#
# usage:
# object = App.ActiveDocument.addObject('App::FeaturePython','objName')
# object = model.newObject('App::FeaturePython','objName')
# object.ViewObject.Proxy = Asm4.setCustomIcon(object,'Asm4_Variables.svg')
class setCustomIcon():
    def __init__( self, obj, iconFile):
        #obj.Proxy = self
        self.customIcon = os.path.join( iconPath, iconFile )

    def getIcon(self):                                              # GetIcon
        return self.customIcon




"""
    +-----------------------------------------------+
    |         check whether a workbench exists      |
    +-----------------------------------------------+
"""
def checkWorkbench( workbench ):
    # checks whether the specified workbench is installed
    listWB = Gui.listWorkbenches()
    hasWB = False
    for wb in listWB.keys():
        if wb == workbench:
            hasWB = True
    return hasWB

# since Asm4 v0.20 an assembly is called "Assembly" again
def getAssembly():
    # return checkModel()
    retval = None
    if App.ActiveDocument:
        # the current (as per v0.90) assembly container
        assy = App.ActiveDocument.getObject('Assembly')
        if assy and assy.TypeId == 'App::Part'  \
                and assy.Type   == 'Assembly'   \
                and assy.getParentGeoFeatureGroup() is None:
            retval = assy
        else:
            # former Asm4 Model compatibility check:
            model = App.ActiveDocument.getObject('Model')
            if model and model.TypeId == 'App::Part'  \
                    and model.Type    == 'Assembly'   \
                    and model.getParentGeoFeatureGroup() is None:
                retval = model
            else:
                # last chance, very old Asm4 Model
                if model and model.TypeId=='App::Part'  \
                        and model.getParentGeoFeatureGroup() is None:
                    FCC.PrintMessage("Deprecated Asm4 Model detected, this could lead to uncompatibilities\n")
                    retval = model
    return retval


# checks and returns whether there is an Asm4 Assembly Model in the active document
# DEPRECATED : it's called Assembly again
def checkModel():
    return getAssembly()
    '''
    retval = None
    if App.ActiveDocument:
        model = App.ActiveDocument.getObject('Model')
        # the current (as per v0.12) assembly container
        if model and model.TypeId=='App::Part' \
                and model.Type == 'Assembly'   \
                and model.getParentGeoFeatureGroup() is None:
            retval = model
        else:
            # former Assembly compatibility check:
            assy = App.ActiveDocument.getObject('Assembly')
            if assy and assy.TypeId=='App::Part'  \
                    and assy.Type == 'Assembly'   \
                    and assy.getParentGeoFeatureGroup() is None:
                retval = assy
            else:
                # last chance, very old Asm4 Model
                if  model   and model.TypeId=='App::Part'  \
                            and model.getParentGeoFeatureGroup() is None:
                    retval = model
    return retval
    '''


# returns the selected object and its selection hierarchy
# the first element in the tree is the uppermost container name
# the last is the object name
# elements are separated by '.' (dot)
# usage:
# ( obj, tree ) = Asm4.getSelectionTree(index=0)
def getSelectionTree(index=0):
    retval = (None,None)
    # we obviously need something selected
    # if len(Gui.Selection.getSelection()) >= index:
    if len(Gui.Selection.getSelection()) > index:
        selObj = Gui.Selection.getSelection()[index]
        retval = ( selObj, None )
        # objects at the document root don't have a selection tree
        if len(Gui.Selection.getSelectionEx("", 0)[0].SubElementNames) >= index:
            # we only treat thefirst selected object
            # this is a dot-separated list of the selection hierarchy
            selList = Gui.Selection.getSelectionEx("", 0)[0].SubElementNames[index]
            # this is the final tree table
            # this first element will be overwritten later
            selTree = ['root part']
            # parse the list to find all objects
            rest = selList
            while rest:
                (parent, dot, rest) = rest.partition('.')
                # FCC.PrintMessage('found '+parent+'\n')
                selTree.append(parent)
            # if we did find things
            if len(selTree)>1:
                # if the last one is not the selected object, it might be a sub-element of it
                if selTree[-1]!=selObj.Name:
                    selTree = selTree[0:-1]
                # the last one should the selected object
                if selTree[-1]==selObj.Name:
                    topObj = App.ActiveDocument.getObject(selTree[1])
                    rootObj = topObj.getParentGeoFeatureGroup()
                    selTree[0] = rootObj.Name
                    # all went well, we return the selected object and it's tree
                    retval = ( selObj, selTree )
    return retval


# get all datums in a part
def getPartLCS( part ):
    partLCS = [ ]
    # parse all objects in the part (they return strings)
    for objName in part.getSubObjects(1):
        # get the proper objects
        # all object names end with a "." , this needs to be removed
        obj = part.getObject( objName[0:-1] )
        if obj.TypeId in datumTypes:
            partLCS.append( obj )
        elif obj.TypeId == 'App::DocumentObjectGroup':
            datums = getPartLCS(obj)
            for datum in datums:
                partLCS.append(datum)
    return partLCS


# get the document group called Part
# (if it exists, else return None
def getPartsGroup():
    retval = None
    partsGroup = App.ActiveDocument.getObject('Parts')
    if partsGroup and partsGroup.TypeId=='App::DocumentObjectGroup':
        retval = partsGroup
    return retval


# Get almost all Objects within the passed Selection.
# A Selection is one or more marked Object(s) anywhere
# in the opened Document. The idea is to get a similar
# Selection back, as If you would "copy" the marked Objects.
# The Window that pops up and shows are affected Objects, calls it
# The Dependencies
# Objects within Compounds and Bodys and also Linked Objects are left out.
# 
# NOTE: Theoretically we could use the App.ActiveDocument.DependencyGraph function,
# to get really every Object behind a selection.
def getDependenciesList( CompleteSelection ):
    deDendenciesList = [ ]
    for Selection in CompleteSelection:
        # A possible container for more Sub-Objects
        SubObjects = [ ]
        # If an Object has more objects to offer, get them
        if hasattr(Selection,'getSubObjects'):
            SubObjNames=Selection.getSubObjects()
            # Some Objects return None Objects,
            # even if it has the 'getSubObjects' attribute
            # 'getSubObjects' delivers unique Names with a trailing .
            for SubObjName in SubObjNames:              
                SubObjects.append(App.ActiveDocument.getObject(SubObjName[0:-1]))
        # If they are more Sub-Objects within that particular selection,
        # go get them. It doesn't matter If it is a Group or Part or Link.
        if SubObjects is not None:            
            SubObjects = getDependenciesList(SubObjects)
            # Adding the Sub-Objects in that way, prevents nested Objects in Objects
            for SubObject in SubObjects:
                deDendenciesList.append(SubObject)
        # In order to make not iterable, single Objects,
        # iterable enclosing [] are needed
        deDendenciesList.append([Selection])
    return deDendenciesList


"""
    +-----------------------------------------------+
    |           get the next instance's name        |
    +-----------------------------------------------+
"""
def nextInstance( name, startAtOne=False ):
    # if there is no such name, return the original
    if not App.ActiveDocument.getObject(name) and not startAtOne:
        return name
    # there is already one, we increment
    else:
        if startAtOne:
            instanceNum = 1
        else:
            instanceNum = 2
        while App.ActiveDocument.getObject( name+'_'+str(instanceNum) ):
            instanceNum += 1
        return name+'_'+str(instanceNum)



"""
    +-----------------------------------------------+
    |          return the ExpressionEngine          |
    |           of the Placement property           |
    +-----------------------------------------------+
"""
def placementEE( EE ):
    if not EE:
        return None
    else:
        for expr in EE:
            if expr[0] == 'Placement':
                return expr[1]
    return None




"""
    +-----------------------------------------------+
    |              some geometry tests              |
    +-----------------------------------------------+
"""
def isVector( vect ):
    if isinstance(vect,App.Vector):
        return True
    return False

def isCircle(shape):
    if shape.isValid()  and hasattr(shape,'Curve') \
                        and shape.Curve.TypeId=='Part::GeomCircle' \
                        and hasattr(shape.Curve,'Center') \
                        and hasattr(shape.Curve,'Radius'):
        return True
    return False

def isLine(shape):
    if shape.isValid()  and hasattr(shape,'Curve') \
                        and shape.Curve.TypeId=='Part::GeomLine' \
                        and hasattr(shape,'Placement'):
        return True
    return False

def isSegment(shape):
    if shape.isValid()  and hasattr(shape,'Curve') \
                        and shape.Curve.TypeId=='Part::GeomLine' \
                        and hasattr(shape,'Length') \
                        and hasattr(shape,'Vertexes') \
                        and len(shape.Vertexes)==2:
        return True
    return False


def isFlatFace(shape):
    if shape.isValid()  and hasattr(shape,'Area')   \
                        and shape.Area > 1.0e-6     \
                        and hasattr(shape,'Volume') \
                        and shape.Volume < 1.0e-9:
        return True
    return False


def isHoleAxis(obj):
    if not obj:
        return False
    if hasattr(obj, 'AttacherType'):
        if obj.AttacherType == 'Attacher::AttachEngineLine':
            return True
    return False


def isPart(obj):
    if not obj:
        return False
    if hasattr(obj, 'TypeId'):
        if obj.TypeId == 'App::Part':
            return True
    return False


def isAppLink(obj):
    if not obj:
        return False
    if hasattr(obj, 'TypeId') and obj.TypeId == 'App::Link':
        return True
    return False


def isLinkToPart(obj):
    if not obj:
        return False
    if obj.TypeId == 'App::Link' and hasattr(obj.LinkedObject,'isDerivedFrom'):
        if  obj.LinkedObject.isDerivedFrom('App::Part') or obj.LinkedObject.isDerivedFrom('PartDesign::Body'):
            return True
    return False


'''
def isPartLinkAssembly(obj):
    if not obj:
        return False
    if hasattr(obj,'AssemblyType') and hasattr(obj,'SolverId'):
        if obj.AssemblyType == 'Part::Link' or obj.AssemblyType == '' :
            return True
    return False
'''


def isAsm4EE(obj):
    if not obj:
        return False
    # we only need to check for the SolverId
    if hasattr(obj,'SolverId') :
        if obj.SolverId=='Asm4EE' or obj.SolverId=='Placement::ExpressionEngine' or obj.SolverId=='' :
            return True
    # legacy check
    # DEPRECATED, to be removed
    elif hasattr(obj,'AssemblyType') :
        if obj.AssemblyType == 'Asm4EE' or obj.AssemblyType == '' :
            FCC.PrintMessage('Found legacy AssemblyType property, adding new empty SolverId property\n')
            # add the new property to convert legacy object
            obj.addProperty( 'App::PropertyString', 'SolverId', 'Assembly' )
            return True
    return False


def isAssembly(obj):
    if not obj:
        return False
    if obj.TypeId=='App::Part' and obj.Name=='Assembly':
        if hasattr(obj,'Type') and obj.Type=='Assembly':
            return True
    return False


def isAsm4Model(obj):
    return isAssembly(obj)


"""
    +-----------------------------------------------+
    |           Shows a Warning message box         |
    +-----------------------------------------------+
"""
def warningBox( text ):
    msgBox = QtGui.QMessageBox()
    msgBox.setWindowTitle( 'FreeCAD Warning' )
    msgBox.setIcon( QtGui.QMessageBox.Critical )
    msgBox.setText( text )
    msgBox.exec_()
    return


def confirmBox( text ):
    msgBox = QtGui.QMessageBox()
    msgBox.setWindowTitle('FreeCAD Warning')
    msgBox.setIcon(QtGui.QMessageBox.Warning)
    msgBox.setText(text)
    msgBox.setInformativeText('Are you sure you want to proceed ?')
    msgBox.setStandardButtons(QtGui.QMessageBox.Cancel | QtGui.QMessageBox.Ok)
    msgBox.setEscapeButton(QtGui.QMessageBox.Cancel)
    msgBox.setDefaultButton(QtGui.QMessageBox.Ok)
    retval = msgBox.exec_()
    # Cancel = 4194304
    # Ok = 1024
    if retval == 1024:
        # user confirmed
        return True
    # anything else than OK
    return False


"""
    +-----------------------------------------------+
    |        Drop-down menu to group buttons        |
    +-----------------------------------------------+
"""
# from https://github.com/HakanSeven12/FreeCAD-Geomatics-Workbench/commit/d82d27b47fcf794bf6f9825405eacc284de18996
class dropDownCmd:
    def __init__(self, cmdlist, menu, tooltip = None, icon = None):
        self.cmdlist = cmdlist
        self.menu = menu
        if tooltip is None:
            self.tooltip = menu
        else:
            self.tooltip = tooltip

    def GetCommands(self):
        return tuple(self.cmdlist)

    def GetResources(self):
        return { 'MenuText': self.menu, 'ToolTip': self.tooltip }




"""
    +-----------------------------------------------+
    |         returns the object Label (Name)       |
    +-----------------------------------------------+
"""


# Label (Name)
def labelName( obj ):
    if obj:
        if obj.Name == obj.Label:
            txt = obj.Label
        else:
            txt = obj.Label +' ('+obj.Name+')'
        return txt
    else:
        return None



"""
    +-----------------------------------------------+
    |         populate the ExpressionEngine         |
    +-----------------------------------------------+
"""
def makeExpressionPart( attLink, attDoc, attLCS, linkedDoc, linkLCS ):
    # if everything is defined
    if attLink and attLCS and linkedDoc and linkLCS:
        # this is where all the magic is, see:
        #
        # https://forum.freecadweb.org/viewtopic.php?p=278124#p278124
        #
        # as of FreeCAD v0.19 the syntax is different:
        # https://forum.freecadweb.org/viewtopic.php?f=17&t=38974&p=337784#p337784
        # expr = ParentLink.Placement * ParentPart#LCS.Placement * constr_LinkName.AttachmentOffset * LinkedPart#LCS.Placement ^ -1
        # expr = LCS_in_the_assembly.Placement * constr_LinkName.AttachmentOffset * LinkedPart#LCS.Placement ^ -1
        # the AttachmentOffset is now a property of the App::Link
        # expr = LCS_in_the_assembly.Placement * AttachmentOffset * LinkedPart#LCS.Placement ^ -1
        expr = attLCS+'.Placement * AttachmentOffset * '+linkedDoc+'#'+linkLCS+'.Placement ^ -1'
        # if we're attached to another sister part (and not the Parent Assembly)
        # we need to take into account the Placement of that Part.
        if attDoc:
            expr = attLink+'.Placement * '+attDoc+'#'+expr
    else:
        expr = False
    return expr


def makeExpressionDatum( attLink, attPart, attLCS ):
    # check that everything is defined
    if attLCS:
        # expr = Link.Placement * LinkedPart#LCS.Placement
        expr = attLCS +'.Placement * AttachmentOffset'
        if attLink and attPart:
            expr = attLink+'.Placement * '+attPart+'#'+expr
    else:
        expr = None
    return expr




"""
    +-----------------------------------------------+
    |        ExpressionEngine for Fasteners         |
    +-----------------------------------------------+
"""
# is in the FastenersLib.py file



# checks whether an App::Part is selected, and that it's at the root of the document
def getSelectedRootPart():
    retval = None
    selection = Gui.Selection.getSelection()
    if len(selection)==1:
        selObj = selection[0]
        # only consider App::Parts at the root of the document
        if selObj.TypeId=='App::Part' and selObj.getParentGeoFeatureGroup() is None:
            retval = selObj
    return retval


def getSelectedContainer():
    retval = None
    selection = Gui.Selection.getSelection()
    if len(selection)==1:
        selObj = selection[0]
        if selObj.TypeId in containerTypes:
            retval = selObj
    return retval


# returns the selected App::Link
def getSelectedLink():
    retval = None
    selection = Gui.Selection.getSelection()
    if len(selection)==1:
        selObj = selection[0]
        # it's an App::Link
        if selObj.isDerivedFrom('App::Link') and selObj.LinkedObject is not None and selObj.LinkedObject.TypeId in containerTypes:
            retval = selObj
    return retval


# returns the selected Asm4 variant link
def getSelectedVarLink():
    retval = None
    selection = Gui.Selection.getSelection()
    if len(selection)==1:
        selObj = selection[0]
        # it's an App::Link
        if selObj.TypeId=='Part::FeaturePython':
            if hasattr(selObj,'Type') and selObj.Type=='Asm4::VariantLink':
                retval = selObj
    return retval


def getSelectedDatum():
    selectedObj = None
    # check that something is selected
    if len(Gui.Selection.getSelection())==1:
        selection = Gui.Selection.getSelection()[0]
        # check that it's a datum
        if selection.TypeId in datumTypes:
            selectedObj = selection
    # now we should be safe
    return selectedObj


def create_assembly():
    # provision to supply a document in create_assembly call different
    # from App.ActiveDocument. This sohould however modify each calls
    # like getAssembly and similar methods.
    d_doc = App.ActiveDocument

    # check whether there is already Model in the document
    # assy = App.ActiveDocument.getObject('Assembly')

    assy = getAssembly()
    if assy is not None:
        if assy.TypeId=='App::Part':
            message = "This document already contains a valid Assembly, please use it"
            warningBox(message)
            # set the Type to Assembly
            assy.Type = 'Assembly'
        else:
            message  = "This document already contains another FreeCAD object called \"Assembly\", "
            message += "but it's of type \""+assy.TypeId+"\", unsuitable for an assembly. I can\'t proceed."
            warningBox(message)
        # abort
        return

    # there is no object called "Assembly"
    # create a group 'Parts' to hold all parts in the assembly document (if any)
    # must be done before creating the assembly
    partsGroup = d_doc.getObject('Parts')
    if partsGroup is None:
        partsGroup = d_doc.addObject( 'App::DocumentObjectGroup', 'Parts' )
    # create a new App::Part called 'Assembly'
    assembly = d_doc.addObject('App::Part','Assembly')
    # set the type as a "proof" that it's an Assembly
    assembly.Type='Assembly'
    assembly.addProperty( 'App::PropertyString', 'AssemblyType', 'Assembly' )
    assembly.AssemblyType = 'Part::Link'
    # add an LCS at the root of the Model, and attach it to the 'Origin'
    lcs0 = assembly.newObject('PartDesign::CoordinateSystem','LCS_Origin')
    lcs0.Support = [(assembly.Origin.OriginFeatures[0],'')]
    lcs0.MapMode = 'ObjectXY'
    lcs0.MapReversed = False
    # create a group Constraints to store future solver constraints there
    assembly.newObject('App::DocumentObjectGroup','Constraints')
    d_doc.getObject('Constraints').Visibility = False
    # create an object Variables to hold variables to be used in this document
    assembly.addObject(makeVarContainer())
    # create a group Configurations to store future solver constraints there
    assembly.newObject('App::DocumentObjectGroup','Configurations')
    d_doc.getObject('Configurations').Visibility = False

    # move existing parts and bodies at the document root to the Parts group
    # not nested inside other parts, to keep hierarchy
    if partsGroup.TypeId=='App::DocumentObjectGroup':
        for obj in d_doc.Objects:
            if obj.TypeId in containerTypes and obj.Name!='Assembly' and obj.getParentGeoFeatureGroup() is None:
                partsGroup.addObject(obj)
    else:
        warningBox( 'There seems to already be a Parts object, you might get unexpected behaviour' )

    # recompute to get rid of the small overlays
    assembly.recompute()
    d_doc.recompute()


"""
    +-----------------------------------------------+
    |                 Unit Spin Box                 |
    +-----------------------------------------------+

usage:

self.YtranslSpinBox = QtGui.QDoubleSpinBox() → self.YtranslSpinBox = Asm4.QUnitSpinBox()
"""
class QUnitSpinBox(QtGui.QDoubleSpinBox):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)        
        _, self.length_divisor, self.default_unit = (
            App.Units.schemaTranslate(
                App.Units.Quantity("1 mm"),
                App.Units.getSchema()
            )
        )
        self.setSuffix(" " + self.default_unit)

    def value(self) -> float:
        """gets the value in mm"""
        return super().value() * self.length_divisor

    def setValue(self, distance: float):
        """sets the value in mm"""        
        return super().setValue(
            distance / self.length_divisor,
        )