View Issue Details

IDProjectCategoryView StatusLast Update
0002267File formatsGeneralpublic2017-07-05 17:04
Reporterl3iggs Assigned Tokeithsloan52  
PriorityhighSeverityminorReproducibilityalways
Status closedResolutionfixed 
PlatformlinuxOSArch Linux 
Product Version0.15 
Summary0002267: failure to open/import CSG
DescriptionFreeCAD fails to import my .csg model file produced by OpenSCAD with the following error:

Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/usr/share/freecad/Mod/OpenSCAD/importCSG.py", line 85, in open
    processcsg(filename)
  File "/usr/share/freecad/Mod/OpenSCAD/importCSG.py", line 136, in processcsg
    result = parser.parse(f.read())
  File "/usr/share/freecad/Mod/OpenSCAD/ply/yacc.py", line 265, in parse
    return self.parseopt_notrack(input,lexer,debug,tracking,tokenfunc)
  File "/usr/share/freecad/Mod/OpenSCAD/ply/yacc.py", line 971, in parseopt_notrack
    p.callable(pslice)
  File "/usr/share/freecad/Mod/OpenSCAD/importCSG.py", line 155, in p_block_list_
    p[0] = p[1] + p[2]
<type 'exceptions.TypeError'>: can only concatenate list (not "NoneType") to list
Steps To ReproduceOpen plate1.csg
Additional InformationFile attached
TagsCSG, DXF
FreeCAD Information

Relationships

related to 0003072 new FreeCAD Please add a function exportDXF to TopoShape 
related to 0003035 closedkeithsloan52 FreeCAD exportDXF won't export LWPOLYLINE correctly 

Activities

l3iggs

2015-09-16 15:46

reporter  

plate1.csg (86,136 bytes)

Kunda1

2017-02-02 17:30

administrator   ~0008175

Last edited: 2017-02-02 17:31

I can reproduce on FreeCAD-0.17.git201612261451.glibc2.17-x86_64.AppImage

OS: "Manjaro Linux"
Word size of OS: 64-bit
Word size of FreeCAD: 64-bit
Version: 0.17.9374 (Git)
Build type: None
Branch: master
Hash: 86b3463ee11692b491716b7f7db34c82eee28e76
Python version: 2.7.6
Qt version: 4.8.6
Coin version: 4.0.0a
OCC version: 6.8.0.oce-0.17

Here is the Report output:


WARNING: Token 'DOT' defined, but not used
WARNING: Token 'WORD' defined, but not used
WARNING: There are 2 unused tokens
Unable to create 'parsetab.py'
[Errno 30] Read-only file system: 'parsetab.py'
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/tmp/.mount_mDnZ3R/usr/lib/freecad-daily/Mod/OpenSCAD/importCSG.py", line 115, in insert
    processcsg(filename)
  File "/tmp/.mount_mDnZ3R/usr/lib/freecad-daily/Mod/OpenSCAD/importCSG.py", line 139, in processcsg
    result = parser.parse(f.read())
  File "/tmp/.mount_mDnZ3R/usr/lib/freecad-daily/Mod/OpenSCAD/ply/yacc.py", line 265, in parse
    return self.parseopt_notrack(input,lexer,debug,tracking,tokenfunc)
  File "/tmp/.mount_mDnZ3R/usr/lib/freecad-daily/Mod/OpenSCAD/ply/yacc.py", line 971, in parseopt_notrack
    p.callable(pslice)
  File "/tmp/.mount_mDnZ3R/usr/lib/freecad-daily/Mod/OpenSCAD/importCSG.py", line 158, in p_block_list_
    p[0] = p[1] + p[2]
<type 'exceptions.TypeError'>: can only concatenate list (not "NoneType") to list

Kunda1

2017-05-11 03:28

administrator   ~0008949

Forum thread: https://forum.freecadweb.org/viewtopic.php?f=8&t=22382

Kunda1

2017-05-11 21:21

administrator   ~0008964

@l3iggs please check out https://forum.freecadweb.org/viewtopic.php?f=8&t=22382&p=173814#p173811

keithsloan52

2017-05-12 14:58

developer   ~0008975

With the latest version of freecad-daily it no longer crashes at line 158 but still has a problem.

I have reduced the file to a much smaller and simpler one that still has the same problem
plate2.csg (393 bytes)

keithsloan52

2017-05-12 15:25

developer   ~0008976

Okay I think I have tracked down the problem a bit more.
When importCSG.py encounters a minkowski request and the two objects it is to operate on are 2D it exports the 2D objects as DXF files and then calls OpenSCAD to perform the minikowski operation requesting the output as a dxf file. i.e. Something like minkowski(){import(file = "fc-11954-672761-000016.dxf"); import(file = "fc-11954-672761-000017.dxf");}
In the case of this file the dxf input files are just single lines and OpenSCAD is not producing any output file. Note sure there is anyway to fix this

keithsloan52

2017-05-12 16:20

developer   ~0008977

Okay I created a simple file with just an offset plate3.csg. If open this with FreeCAD and save it I get plate3.fcstd. If I then export that file as dxf I get plate3.dxf. All well and good but if I look at the dxf file created by importCSG using the function export from the library importDXF I get a file that looks like fc-12862-44183-000002.dxf i.e. a single line which is not right. Don't know if @yorik can help
plate3.csg (276 bytes)
plate3.fcstd (4,071 bytes)
plate3.dxf (888 bytes)   
plate3.dxf (888 bytes)   
fc-12862-441813-000002.dxf (750 bytes)   
fc-12862-441813-000002.dxf (750 bytes)   

keithsloan52

2017-06-07 11:50

developer   ~0009304

Last edited: 2017-06-07 12:09

Okay I have done some further digging.

The DXF file is created in OpenSCADUtils.py in function process2D_ObjectsViaOpenSCADShape using library importDXF using function export from the Draft Mod workbench.

If I dump the object as a BREP file I get debugFC2.brep which is a polygon, where as the same object as a DXF file fc-03100-455637-000017.dxf does not correspond.

So I conclude that there must be a bug in the importDXF export function.

Now my understanding which may well be wrong is that importDXF uses the Old FreeCAD DXF library. I tried looking at the code and got nowhere. Its totally un-impenetrable as far as my coding skills go. I also understand that FreeCAD now uses a new DXF library and indeed if I export the BREP file as DXF I get a correct DXF file.

If there is a way of using the new DXF library with a FreeCAD object or shape than I will gladly change the OpenSCAD code.

I believe @yorik looks after the DXF library but he is a very busy person ( Does he ever sleep given the amount he does? ) maybe he could advise.

See enhancement request 0003072
@Kunda1

debugFC2.brep (1,450 bytes)
fc-03100-455637-000017.dxf (760 bytes)   
fc-03100-455637-000017.dxf (760 bytes)   

yorik

2017-06-08 19:12

administrator   ~0009313

@keithsloan52 could you give me a step-by step procedure to reproduce the problem (the wrong dxf file)?

keithsloan52

2017-06-08 19:49

developer   ~0009314

To get the problem just try and open plate2.csg see file above.

The way I am debugging the problem is with modified versions of importCSG.py and OpenSCADUtils.py to output debugging information

With the moded versions you will then see in report view just before the failure something like

processing circle
successfully exported /tmp/fc-03737-034770-000001.dxf
processing Offset2D
successfully exported /tmp/fc-03737-034778-000002.dxf
callopenscadstring : minkowski(){import(file = "fc-03737-034770-000001.dxf"); import(file = "fc-03737-034778-000002.dxf");}
Traceback (most recent call last):

The BREP versions are also dumped to /tmp as debugFC1.brep & debugFC2.brep
debugFC1 is the circle and debugFC2 is the wire

The dxf files should match corresponding export of the brep files, but the wire does not.

I am attaching debugging versions here or you can get them from branch 2D https://github.com/KeithSloan/FreeCAD_sf_master They live in /usr/lib/freecad/mod/OpenSCAD
plate2-2.csg (393 bytes)
importCSG.py (44,604 bytes)   
# -*- coding: utf8 -*-

#***************************************************************************
#*                                                                         *
#*   Copyright (c) 2012 Keith Sloan <keith@sloan-home.co.uk>               *
#*                                                                         *
#*   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 :                                                    *
#*                                                                         *
#*     Thanks to shoogen on the FreeCAD forum and Peter Li                 *
#*     for programming advice and some code.                               *
#*                                                                         *
#*                                                                         *
#***************************************************************************
__title__="FreeCAD OpenSCAD Workbench - CSG importer"
__author__ = "Keith Sloan <keith@sloan-home.co.uk>"
__url__ = ["http://www.sloan-home.co.uk/ImportCSG"]

printverbose = False

import FreeCAD, os, sys
if FreeCAD.GuiUp:
    import FreeCADGui
    gui = True
else:
    if printverbose: print("FreeCAD Gui not present.")
    gui = False


import ply.lex as lex
import ply.yacc as yacc
import Part

from OpenSCADFeatures import *
from OpenSCADUtils import *

params = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/OpenSCAD")
printverbose = params.GetBool('printVerbose',False)

if open.__module__ == '__builtin__':
    pythonopen = open # to distinguish python built-in open function from the one declared here

# Get the token map from the lexer.  This is required.
import tokrules
from tokrules import tokens

try:
    _encoding = QtGui.QApplication.UnicodeUTF8
    def translate(context, text):
        "convenience function for Qt translator"
        from PySide import QtGui
        return QtGui.QApplication.translate(context, text, None, _encoding)
except AttributeError:
    def translate(context, text):
        "convenience function for Qt translator"
        from PySide import QtGui
        return QtGui.QApplication.translate(context, text, None)

def open(filename):
    "called when freecad opens a file."
    global doc
    global pathName
    docname = os.path.splitext(os.path.basename(filename))[0]
    doc = FreeCAD.newDocument(docname)
    if filename.lower().endswith('.scad'):
        tmpfile=callopenscad(filename)
        if workaroundforissue128needed():
            pathName = '' #https://github.com/openscad/openscad/issues/128
            #pathName = os.getcwd() #https://github.com/openscad/openscad/issues/128
        else:
            pathName = os.path.dirname(os.path.normpath(filename))
        processcsg(tmpfile)
        try:
            os.unlink(tmpfile)
        except OSError:
            pass
    else:
        pathName = os.path.dirname(os.path.normpath(filename))
        processcsg(filename)
    return doc

def insert(filename,docname):
    "called when freecad imports a file"
    global doc
    global pathName
    groupname = os.path.splitext(os.path.basename(filename))[0]
    try:
        doc=FreeCAD.getDocument(docname)
    except NameError:
        doc=FreeCAD.newDocument(docname)
    #importgroup = doc.addObject("App::DocumentObjectGroup",groupname)
    if filename.lower().endswith('.scad'):
        tmpfile=callopenscad(filename)
        if workaroundforissue128needed():
            pathName = '' #https://github.com/openscad/openscad/issues/128
            #pathName = os.getcwd() #https://github.com/openscad/openscad/issues/128
        else:
            pathName = os.path.dirname(os.path.normpath(filename))
        processcsg(tmpfile)
        try:
            os.unlink(tmpfile)
        except OSError:
            pass
    else:
        pathName = os.path.dirname(os.path.normpath(filename))
        processcsg(filename)

def processcsg(filename):
    global doc

    if printverbose: print ('ImportCSG Version 0.6a')
    # Build the lexer
    if printverbose: print('Start Lex')
    lex.lex(module=tokrules)
    if printverbose: print('End Lex')

    # Build the parser   
    if printverbose: print('Load Parser')
    # No debug out otherwise Linux has protection exception
    parser = yacc.yacc(debug=0)
    if printverbose: print('Parser Loaded')
    # Give the lexer some input
    #f=open('test.scad', 'r')
    f = pythonopen(filename, 'r')
    #lexer.input(f.read())

    if printverbose: print('Start Parser')
    # Swap statements to enable Parser debugging
    #result = parser.parse(f.read(),debug=1)
    result = parser.parse(f.read())
    f.close()
    if printverbose:
        print('End Parser')
        print(result)
    FreeCAD.Console.PrintMessage('End processing CSG file\n')
    doc.recompute()

def p_block_list_(p):
    '''
    block_list : statement
               | block_list statement
               | statementwithmod
               | block_list statementwithmod
    '''
    #if printverbose: print("Block List")
    #if printverbose: print(p[1])
    if(len(p) > 2) :
        if printverbose: print(p[2])
        p[0] = p[1] + p[2]
    else :
        p[0] = p[1]
    #if printverbose: print("End Block List")

def p_render_action(p):
    'render_action : render LPAREN keywordargument_list RPAREN OBRACE block_list EBRACE'
    if printverbose: print("Render (ignored)")
    p[0] = p[6]

def p_group_action1(p):
    'group_action1 : group LPAREN RPAREN OBRACE block_list EBRACE'
    if printverbose: print("Group")
# Test if need for implicit fuse
    if (len(p[5]) > 1) :
        p[0] = [fuse(p[5],"Group")]
    else :
        p[0] = p[5]

def p_group_action2(p) :
    'group_action2 : group LPAREN RPAREN SEMICOL'
    if printverbose: print("Group2")
    p[0] = []
   
def p_boolean(p) :
    '''
    boolean : true
            | false
    '''
    p[0] = p[1]

#def p_string(p):
#    'string : QUOTE ID QUOTE'
#    p[0] = p[2]

def p_stripped_string(p):
    'stripped_string : STRING'
    p[0] = p[1].strip('"')

def p_statement(p):
    '''statement : part
                 | operation
                 | multmatrix_action
                 | group_action1
                 | group_action2
                 | color_action
                 | render_action
                 | not_supported
    '''
    p[0] = p[1]

def p_anymodifier(p):
    '''anymodifier : MODIFIERBACK
                   | MODIFIERDEBUG
                   | MODIFIERROOT
                   | MODIFIERDISABLE
    '''
    #just return the plain modifier for now
    #has to be changed when the modifiers are inplemented
    #please note that disabled objects usually are stript of the CSG ouput during compilation
    p[0] = p[1]

def p_statementwithmod(p):
    '''statementwithmod : anymodifier statement'''
    #ignore the modifiers but add them to the label
    modifier = p[1]
    obj = p[2]
    if hasattr(obj,'Label'):
        obj.Label = modifier + obj.Label
    p[0] = obj

def p_part(p):
    '''
    part : sphere_action
         | cylinder_action
         | cube_action
         | circle_action
         | square_action
         | text_action
         | polygon_action_nopath
         | polygon_action_plus_path
         | polyhedron_action
         '''
    p[0] = p[1]

def p_2d_point(p):
    '2d_point : OSQUARE NUMBER COMMA NUMBER ESQUARE'
    global points_list
    if printverbose: print("2d Point")
    p[0] = [float(p[2]),float(p[4])]

def p_points_list_2d(p):
    '''
    points_list_2d : 2d_point COMMA
                   | points_list_2d 2d_point COMMA
                   | points_list_2d 2d_point
                   '''
    if p[2] == ',' :
        #if printverbose:
        #    print("Start List")
        #    print(p[1])
        p[0] = [p[1]]
    else :
        if printverbose:
            print(p[1])
            print(p[2])
        p[1].append(p[2])
        p[0] = p[1]
    #if printverbose: print(p[0])

def p_3d_point(p):
    '3d_point : OSQUARE NUMBER COMMA NUMBER COMMA NUMBER ESQUARE'
    global points_list
    if printverbose: print("3d point")
    p[0] = [p[2],p[4],p[6]]
   
def p_points_list_3d(p):
    '''
    points_list_3d : 3d_point COMMA
               | points_list_3d 3d_point COMMA
               | points_list_3d 3d_point
               '''
    if p[2] == ',' :
        if printverbose: print("Start List")
        if printverbose: print(p[1])
        p[0] = [p[1]]
    else :
        if printverbose: print(p[1])
        if printverbose: print(p[2])
        p[1].append(p[2])
        p[0] = p[1]
    if printverbose: print(p[0])

def p_path_points(p):
    '''
    path_points : NUMBER COMMA
                | path_points NUMBER COMMA
                | path_points NUMBER
                '''
    #if printverbose: print("Path point")
    if p[2] == ',' :
        #if printverbose: print('Start list')
        #if printverbose: print(p[1])
        p[0] = [int(p[1])]
    else :
        #if printverbose: print(p[1])
        #if printverbose: print(len(p[1]))
        #if printverbose: print(p[2])
        p[1].append(int(p[2]))
        p[0] = p[1]
    #if printverbose: print(p[0])


def p_path_list(p):
    'path_list : OSQUARE path_points ESQUARE'
    #if printverbose: print('Path List ')
    #if printverbose: print(p[2])
    p[0] = p[2]

def p_path_set(p) :
    '''
    path_set : path_list
             | path_set COMMA path_list
             '''
    #if printverbose: print('Path Set')
    #if printverbose: print(len(p))
    if len(p) == 2 :
        p[0] = [p[1]]
    else :
        p[1].append(p[3])
        p[0] = p[1]
    #if printverbose: print(p[0])

def p_operation(p):
    '''
    operation : difference_action
              | intersection_action
              | union_action
              | rotate_extrude_action
              | linear_extrude_with_twist
              | rotate_extrude_file
              | import_file1
              | surface_action
              | projection_action
              | hull_action
              | minkowski_action
              | offset_action
              '''
    p[0] = p[1]

def placeholder(name,children,arguments):
    from OpenSCADFeatures import OpenSCADPlaceholder
    newobj=doc.addObject("Part::FeaturePython",name)
    OpenSCADPlaceholder(newobj,children,str(arguments))
    if gui:
        if FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/OpenSCAD").\
            GetBool('useViewProviderTree'):
            from OpenSCADFeatures import ViewProviderTree
            ViewProviderTree(newobj.ViewObject)
        else:
            newobj.ViewObject.Proxy = 0
    #don't hide the children
    return newobj

def CGALFeatureObj(name,children,arguments=[]):
    myobj=doc.addObject("Part::FeaturePython",name)
    CGALFeature(myobj,name,children,str(arguments))
    if gui:
        for subobj in children:
            subobj.ViewObject.hide()
        if FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/OpenSCAD").\
            GetBool('useViewProviderTree'):
            from OpenSCADFeatures import ViewProviderTree
            ViewProviderTree(myobj.ViewObject)
        else:
            myobj.ViewObject.Proxy = 0
    return myobj

def p_offset_action(p):
    'offset_action : offset LPAREN keywordargument_list RPAREN OBRACE block_list EBRACE'
    if len(p[6]) == 0:
        newobj = placeholder('group',[],'{}')
    elif (len(p[6]) == 1 ): #single object
        subobj = p[6]
    else:
        subobj = fuse(p[6],"Offset Union")
    if 'r' in p[3] :
        offset = float(p[3]['r'])
    if 'delta' in p[3] : 
        offset = float(p[3]['delta'])
    #print subobj
    #print subobj[0].Label
    pl = subobj[0].PropertiesList
    #print pl
    if 'Shapes' in pl :
       v = subobj[0].Shapes.Volume
    if 'Base' in pl and 'Tool' in pl : 
       v = subobj[0].Base.Shape.Volume + subobj[0].Tool.Shape.Volume
    # if Base & Tool present then Shape may not be set properly
    # e.g. Group() { polygon box }
    elif 'Shape' in pl :
       v = subobj[0].Shape.Volume
    if v == 0 :
       newobj=doc.addObject("Part::Offset2D",'Offset2D')
       newobj.Source = subobj[0]
       newobj.Value = offset
       if 'r' in p[3] :
           newobj.Join = 0 
       else :
           newobj.Join = 2 
    else :
       newobj=doc.addObject("Part::Offset",'offset')
       # if 3D is Shape always valid ??
       newobj.Shape = subobj[0].Shape.makeOffset(offset)
    newobj.Document.recompute()
    subobj[0].ViewObject.hide()
#    if gui:
#        if FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/OpenSCAD").\
#            GetBool('useViewProviderTree'):
#            from OpenSCADFeatures import ViewProviderTree
#            ViewProviderTree(newobj.ViewObject)
#        else:
#            newobj.ViewObject.Proxy = 0
    p[0] = [newobj]

def p_hull_action(p):
    'hull_action : hull LPAREN RPAREN OBRACE block_list EBRACE'
    p[0] = [ CGALFeatureObj(p[1],p[5]) ]

def p_minkowski_action(p):
    '''
    minkowski_action : minkowski LPAREN keywordargument_list RPAREN OBRACE block_list EBRACE'''
    p[0] = [ CGALFeatureObj(p[1],p[6],p[3]) ]

def p_not_supported(p):
    '''
    not_supported : glide LPAREN keywordargument_list RPAREN OBRACE block_list EBRACE
                  | resize LPAREN keywordargument_list RPAREN OBRACE block_list EBRACE
                  | subdiv LPAREN keywordargument_list RPAREN OBRACE block_list EBRACE
                  '''
    if gui and not FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/OpenSCAD").\
            GetBool('usePlaceholderForUnsupported'):
        from PySide import QtGui
        QtGui.QMessageBox.critical(None, unicode(translate('OpenSCAD',"Unsupported Function"))+" : "+p[1],unicode(translate('OpenSCAD',"Press OK")))
    else:
        p[0] = [placeholder(p[1],p[6],p[3])]

def p_size_vector(p):
    'size_vector : OSQUARE NUMBER COMMA NUMBER COMMA NUMBER ESQUARE'
    if printverbose: print("size vector")
    p[0] = [p[2],p[4],p[6]]

def p_keywordargument(p):
    '''keywordargument : ID EQ boolean
    | ID EQ NUMBER
    | ID EQ size_vector
    | ID EQ vector
    | ID EQ 2d_point
    | text EQ stripped_string
    | ID EQ stripped_string
     '''
    p[0] = (p[1],p[3])
    if printverbose: print(p[0])

def p_keywordargument_list(p):
    '''
    keywordargument_list : keywordargument
               | keywordargument_list COMMA keywordargument
    '''
    if len(p) == 2:
        p[0] = {p[1][0] : p[1][1]}
    else:
        p[1][p[3][0]] = p[3][1]
        p[0]=p[1]

def p_color_action(p):
    'color_action : color LPAREN vector RPAREN OBRACE block_list EBRACE'
    import math
    if printverbose: print("Color")
    color = tuple([float(f) for f in p[3][:3]]) #RGB
    transp = 100 - int(math.floor(100*float(p[3][3]))) #Alpha
    if gui:
        for obj in p[6]:
            obj.ViewObject.ShapeColor =color
            obj.ViewObject.Transparency = transp
    p[0] = p[6]

# Error rule for syntax errors
def p_error(p):
    if printverbose: print("Syntax error in input!")
    if printverbose: print(p)

def fuse(lst,name):
    global doc
    if printverbose: print("Fuse")
    if printverbose: print(lst)
    if len(lst) == 0:
        myfuse = placeholder('group',[],'{}')
    elif len(lst) == 1:
       return lst[0]
    # Is this Multi Fuse
    elif len(lst) > 2:
       if printverbose: print("Multi Fuse")
       myfuse = doc.addObject('Part::MultiFuse',name)
       myfuse.Shapes = lst
       if gui:
           for subobj in myfuse.Shapes:
               subobj.ViewObject.hide()
    else:
       if printverbose: print("Single Fuse")
       myfuse = doc.addObject('Part::Fuse',name)
       myfuse.Base = lst[0]
       myfuse.Tool = lst[1]
       myfuse.Shape = Part.makeCompound([myfuse.Base.Shape,myfuse.Tool.Shape])
       if gui:
           myfuse.Base.ViewObject.hide()
           myfuse.Tool.ViewObject.hide()
    return(myfuse)

def p_union_action(p):
    'union_action : union LPAREN RPAREN OBRACE block_list EBRACE'
    if printverbose: print("union")
    newpart = fuse(p[5],p[1])
    if printverbose: print("Push Union Result")
    p[0] = [newpart]
    if printverbose: print("End Union")
    
def p_difference_action(p):  
    'difference_action : difference LPAREN RPAREN OBRACE block_list EBRACE'

    if printverbose: print("difference")
    if printverbose: print(len(p[5]))
    if printverbose: print(p[5])
    if (len(p[5]) == 0 ): #nochild
        mycut = placeholder('group',[],'{}')
    elif (len(p[5]) == 1 ): #single object
        p[0] = p[5]
    else:
# Cut using Fuse    
        mycut = doc.addObject('Part::Cut',p[1])
        mycut.Base = p[5][0]
#       Can only Cut two objects do we need to fuse extras
        if (len(p[5]) > 2 ):
           if printverbose: print("Need to Fuse Extra First")
           mycut.Tool = fuse(p[5][1:],'union')
        else :
           mycut.Tool = p[5][1]
        if gui:
            mycut.Base.ViewObject.hide()
            mycut.Tool.ViewObject.hide()
        if printverbose: print("Push Resulting Cut")
        p[0] = [mycut]
    if printverbose: print("End Cut")

def p_intersection_action(p):
    'intersection_action : intersection LPAREN RPAREN OBRACE block_list EBRACE'

    if printverbose: print("intersection")
    # Is this Multi Common
    if (len(p[5]) > 2):
       if printverbose: print("Multi Common")
       mycommon = doc.addObject('Part::MultiCommon',p[1])
       mycommon.Shapes = p[5]
       if gui:
           for subobj in mycommon.Shapes:
               subobj.ViewObject.hide()
    elif (len(p[5]) == 2):
       if printverbose: print("Single Common")
       mycommon = doc.addObject('Part::Common',p[1])
       mycommon.Base = p[5][0]
       mycommon.Tool = p[5][1]
       if gui:
           mycommon.Base.ViewObject.hide()
           mycommon.Tool.ViewObject.hide()
    elif (len(p[5]) == 1):
        mycommon = p[5][0]
    else : # 1 child
        mycommon = placeholder('group',[],'{}')
    p[0] = [mycommon]
    if printverbose: print("End Intersection")

def process_rotate_extrude(obj):
    newobj=doc.addObject("Part::FeaturePython",'RefineRotateExtrude')
    RefineShape(newobj,obj)
    if gui:
        if FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/OpenSCAD").\
            GetBool('useViewProviderTree'):
            from OpenSCADFeatures import ViewProviderTree
            ViewProviderTree(newobj.ViewObject)
        else:
            newobj.ViewObject.Proxy = 0
        obj.ViewObject.hide()
    myrev = doc.addObject("Part::Revolution","RotateExtrude")
    myrev.Source = newobj
    myrev.Axis = (0.00,1.00,0.00)
    myrev.Base = (0.00,0.00,0.00)
    myrev.Angle = 360.00
    myrev.Placement=FreeCAD.Placement(FreeCAD.Vector(),FreeCAD.Rotation(0,0,90))
    if gui:
        newobj.ViewObject.hide()
    return(myrev)

def p_rotate_extrude_action(p): 
    'rotate_extrude_action : rotate_extrude LPAREN keywordargument_list RPAREN OBRACE block_list EBRACE'
    if printverbose: print("Rotate Extrude")
    print p[6]
    if (len(p[6]) > 1) :
        part = fuse(p[6],"Rotate Extrude Union")
    else :
        part = p[6][0]
    p[0] = [process_rotate_extrude(part)]
    if printverbose: print("End Rotate Extrude")

def p_rotate_extrude_file(p):
    'rotate_extrude_file : rotate_extrude LPAREN keywordargument_list RPAREN SEMICOL'
    if printverbose: print("Rotate Extrude File")
    filen,ext =p[3]['file'] .rsplit('.',1)
    obj = process_import_file(filen,ext,p[3]['layer'])
    p[0] = [process_rotate_extrude(obj)]
    if printverbose: print("End Rotate Extrude File")

def process_linear_extrude(obj,h) :
    #if gui:
    newobj=doc.addObject("Part::FeaturePython",'RefineLinearExtrude')
    RefineShape(newobj,obj)#mylinear)
    if gui:
        if FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/OpenSCAD").\
            GetBool('useViewProviderTree'):
            from OpenSCADFeatures import ViewProviderTree
            ViewProviderTree(newobj.ViewObject)
        else:
            newobj.ViewObject.Proxy = 0
        obj.ViewObject.hide()
        #mylinear.ViewObject.hide()
    mylinear = doc.addObject("Part::Extrusion","LinearExtrude")
    mylinear.Base = newobj #obj
    mylinear.Dir = (0,0,h)
    mylinear.Placement=FreeCAD.Placement()
    # V17 change to False mylinear.Solid = True
    mylinear.Solid = False
    if gui:
        newobj.ViewObject.hide()
    return(mylinear)

def process_linear_extrude_with_twist(base,height,twist) :   
    newobj=doc.addObject("Part::FeaturePython",'twist_extrude')
    Twist(newobj,base,height,-twist) #base is an FreeCAD Object, heigth and twist are floats
    if gui:
        if FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/OpenSCAD").\
            GetBool('useViewProviderTree'):
            from OpenSCADFeatures import ViewProviderTree
            ViewProviderTree(newobj.ViewObject)
        else:
            newobj.ViewObject.Proxy = 0
    #import ViewProviderTree from OpenSCADFeatures
    #ViewProviderTree(obj.ViewObject)
    return(newobj)

def p_linear_extrude_with_twist(p):
    'linear_extrude_with_twist : linear_extrude LPAREN keywordargument_list RPAREN OBRACE block_list EBRACE'
    if printverbose: print("Linear Extrude With Twist")
    h = float(p[3]['height'])
    if printverbose: print("Twist : ",p[3])
    if 'twist' in p[3]:
        t = float(p[3]['twist'])
    else:
        t = 0
    # Test if null object like from null text
    if (len(p[6]) == 0) :
        p[0] = []
        return
    if (len(p[6]) > 1) :
        obj = fuse(p[6],"Linear Extrude Union")
    else :
        obj = p[6][0]
    if t:
        newobj = process_linear_extrude_with_twist(obj,h,t)
    else:
        newobj = process_linear_extrude(obj,h)
    if p[3]['center']=='true' :
       center(newobj,0,0,h)
    p[0] = [newobj]
    if printverbose: print("End Linear Extrude with twist")

def p_import_file1(p):
    'import_file1 : import LPAREN keywordargument_list RPAREN SEMICOL'
    if printverbose: print("Import File")
    filen,ext =p[3]['file'].rsplit('.',1)
    p[0] = [process_import_file(filen,ext,p[3]['layer'])]
    if printverbose: print("End Import File")

def p_surface_action(p):
    'surface_action : surface LPAREN keywordargument_list RPAREN SEMICOL'
    if printverbose: print("Surface")
    obj = doc.addObject("Part::Feature",'surface')
    obj.Shape,xoff,yoff=makeSurfaceVolume(p[3]['file'])
    if p[3]['center']=='true' :
        center(obj,xoff,yoff,0.0)
    p[0] = [obj]
    if printverbose: print("End surface")

def process_import_file(fname,ext,layer):
    if printverbose: print("Importing : "+fname+"."+ext+" Layer : "+layer)
    if ext.lower() in reverseimporttypes()['Mesh']:
        obj=process_mesh_file(fname,ext)
    elif ext.lower() == 'dxf' :
        obj=processDXF(fname,layer)
    else:
        raise ValueError("Unsupported file extension %s" % ext)
    return(obj)

def process_mesh_file(fname,ext):
    import Mesh,Part
    fullname = fname+'.'+ext
    filename = os.path.join(pathName,fullname)
    objname = os.path.split(fname)[1]
    mesh1 = doc.getObject(objname) #reuse imported object
    if not mesh1:
        Mesh.insert(filename)
        mesh1=doc.getObject(objname)
    if mesh1 is not None:
        if gui:
            mesh1.ViewObject.hide()
        sh=Part.Shape()
        sh.makeShapeFromMesh(mesh1.Mesh.Topology,0.1)
        solid = Part.Solid(sh)
        obj=doc.addObject('Part::Feature',"Mesh")
        #ImportObject(obj,mesh1) #This object is not mutable from the GUI
        #ViewProviderTree(obj.ViewObject)
        solid=solid.removeSplitter()
        if solid.Volume < 0:
            #sh.reverse()
            #sh = sh.copy()
            solid.complement()
        obj.Shape=solid#.removeSplitter()
    else: #mesh1 is None
        FreeCAD.Console.PrintError('Mesh not imported %s.%s %s\n' % \
                (objname,ext,filename))
        import Part
        obj=doc.addObject('Part::Feature',"FailedMeshImport")
        obj.Shape=Part.Compound([])
    return(obj)


def processTextCmd(t):
    import os
    from OpenSCADUtils import callopenscadstring
    tmpfilename = callopenscadstring(t,'dxf')
    from OpenSCAD2Dgeom import importDXFface 
    face = importDXFface(tmpfilename,None,None)
    obj=doc.addObject('Part::Feature','text')
    obj.Shape=face
    try:
        os.unlink(tmpfilename)
    except OSError:
        pass
    return(obj)

def processDXF(fname,layer):
    global doc
    global pathName
    from OpenSCAD2Dgeom import importDXFface
    if printverbose: print("Process DXF file")
    if printverbose: print("File Name : "+fname)
    if printverbose: print("Layer : "+layer)
    if printverbose: print("PathName : "+pathName)
    dxfname = fname+'.dxf'
    filename = os.path.join(pathName,dxfname)
    shortname = os.path.split(fname)[1]
    if printverbose: print("DXF Full path : "+filename)
    face = importDXFface(filename,layer,doc)
    obj=doc.addObject('Part::Feature','dxf_%s_%s' % (shortname,layer or "all"))
    obj.Shape=face
    if printverbose: print("DXF Diagnostics")
    if printverbose: print(obj.Shape.ShapeType)
    if printverbose: print("Closed : "+str(obj.Shape.isClosed()))
    if printverbose: print(obj.Shape.check())
    if printverbose: print([w.isClosed() for w in obj.Shape.Wires])
    return(obj)

def processSTL(fname):
    if printverbose: print("Process STL file")

def p_multmatrix_action(p):
    'multmatrix_action : multmatrix LPAREN matrix RPAREN OBRACE block_list EBRACE'
    if printverbose: print("MultMatrix")
    transform_matrix = FreeCAD.Matrix()
    if printverbose: print("Multmatrix")
    if printverbose: print(p[3])
    m1l=sum(p[3],[])
    if any('x' in me for me in m1l): #hexfloats
        m1l=[float.fromhex(me) for me in m1l]
        matrixisrounded=False
    elif max((len(me) for me in m1l)) >= 14: #might have double precision
        m1l=[float(me) for me in m1l] # assume precise output
        m1l=[(0 if (abs(me) < 1e-15) else me) for me in m1l]
        matrixisrounded=False
    else: #trucanted numbers
        m1l=[round(float(me),12) for me in m1l] #round
        matrixisrounded=True
    transform_matrix = FreeCAD.Matrix(*tuple(m1l))
    if printverbose: print(transform_matrix)
    if printverbose: print("Apply Multmatrix")
#   If more than one object on the stack for multmatrix fuse first
    if (len(p[6]) == 0) :
        part = placeholder('group',[],'{}')
    elif (len(p[6]) > 1) :
        part = fuse(p[6],"Matrix Union")
    else :
        part = p[6][0]
    if (isspecialorthogonalpython(fcsubmatrix(transform_matrix))) :
        if printverbose: print("special orthogonal")
        if matrixisrounded:
            if printverbose: print("rotation rounded")
            plm=FreeCAD.Placement(transform_matrix)
            plm=FreeCAD.Placement(plm.Base,roundrotation(plm.Rotation))
            part.Placement=plm.multiply(part.Placement)
        else:
            part.Placement=FreeCAD.Placement(transform_matrix).multiply(\
                    part.Placement)
        new_part = part
    elif isrotoinversionpython(fcsubmatrix(transform_matrix)):
        if printverbose: print("orthogonal and inversion")
        cmat,axisvec = decomposerotoinversion(transform_matrix)
        new_part=doc.addObject("Part::Mirroring",'mirr_%s'%part.Name)
        new_part.Source=part
        new_part.Normal=axisvec
        if matrixisrounded:
            if printverbose: print("rotation rounded")
            plm=FreeCAD.Placement(cmat)
            new_part.Placement=FreeCAD.Placement(plm.Base,roundrotation(plm.Rotation))
        else:
            new_part.Placement=FreeCAD.Placement(cmat)
        new_part.Label="mirrored %s" % part.Label
        if gui:
            part.ViewObject.hide()
    elif FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/OpenSCAD").\
        GetBool('useMultmatrixFeature'):
        from OpenSCADFeatures import MatrixTransform
        new_part=doc.addObject("Part::FeaturePython",'Matrix Deformation')
        MatrixTransform(new_part,transform_matrix,part)
        if gui:
            if FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/OpenSCAD").\
                GetBool('useViewProviderTree'):
                from OpenSCADFeatures import ViewProviderTree
                ViewProviderTree(new_part.ViewObject)
            else:
                new_part.ViewObject.Proxy = 0
            part.ViewObject.hide()
    else :
        if printverbose: print("Transform Geometry")
#       Need to recompute to stop transformGeometry causing a crash        
        doc.recompute()
        new_part = doc.addObject("Part::Feature","Matrix Deformation")
      #  new_part.Shape = part.Base.Shape.transformGeometry(transform_matrix)
        new_part.Shape = part.Shape.transformGeometry(transform_matrix) 
        if gui:
            part.ViewObject.hide()
    if False :  
#   Does not fix problemfile or beltTighener although later is closer       
        newobj=doc.addObject("Part::FeaturePython",'RefineMultMatrix')
        RefineShape(newobj,new_part)
        if gui:
            newobj.ViewObject.Proxy = 0
            new_part.ViewObject.hide()   
        p[0] = [newobj]
    else :
        p[0] = [new_part]
    if printverbose: print("Multmatrix applied")
    
def p_matrix(p):
    'matrix : OSQUARE vector COMMA vector COMMA vector COMMA vector ESQUARE'
    if printverbose: print("Matrix")
    p[0] = [p[2],p[4],p[6],p[8]]

def p_vector(p):
    'vector : OSQUARE NUMBER COMMA NUMBER COMMA NUMBER COMMA NUMBER ESQUARE'
    if printverbose: print("Vector")
    p[0] = [p[2],p[4],p[6],p[8]]

def center(obj,x,y,z):
    obj.Placement = FreeCAD.Placement(\
        FreeCAD.Vector(-x/2.0,-y/2.0,-z/2.0),\
        FreeCAD.Rotation(0,0,0,1))
    
def p_sphere_action(p):
    'sphere_action : sphere LPAREN keywordargument_list RPAREN SEMICOL'
    if printverbose: print("Sphere : ",p[3])
    r = float(p[3]['r'])
    mysphere = doc.addObject("Part::Sphere",p[1])
    mysphere.Radius = r
    if printverbose: print("Push Sphere")
    p[0] = [mysphere]
    if printverbose: print("End Sphere")

def myPolygon(n,r1):
    # Adapted from Draft::_Polygon
    import math
    if printverbose: print("My Polygon")
    angle = math.pi*2/n
    nodes = [FreeCAD.Vector(r1,0,0)]
    for i in range(n-1) :
        th = (i+1) * angle
        nodes.append(FreeCAD.Vector(r1*math.cos(th),r1*math.sin(th),0))
    nodes.append(nodes[0])
    polygonwire = Part.makePolygon(nodes)

    polygon = doc.addObject("Part::Feature","Polygon")
    polygon.Shape = Part.Face(polygonwire)
    return(polygon)

def p_cylinder_action(p):
    'cylinder_action : cylinder LPAREN keywordargument_list RPAREN SEMICOL'
    if printverbose: print("Cylinder")
    tocenter = p[3]['center']
    h = float(p[3]['h'])
    r1 = float(p[3]['r1'])
    r2 = float(p[3]['r2'])
    #n = int(p[3]['$fn'])
    n = int(round(float(p[3]['$fn'])))
    fnmax = FreeCAD.ParamGet(\
        "User parameter:BaseApp/Preferences/Mod/OpenSCAD").\
        GetInt('useMaxFN')
    if printverbose: print(p[3])
    if h > 0:
        if ( r1 == r2 and r1 > 0):
            if printverbose: print("Make Cylinder")
            if n < 3 or fnmax != 0 and n > fnmax:
                mycyl=doc.addObject("Part::Cylinder",p[1])
                mycyl.Height = h
                mycyl.Radius = r1
            else :
                if printverbose: print("Make Prism")
                if False: #user Draft Polygon
                    mycyl=doc.addObject("Part::Extrusion","prism")
                    mycyl.Dir = (0,0,h)
                    try :
                        import Draft
                        mycyl.Base = Draft.makePolygon(n,r1,face=True)
                    except :
                        # If Draft can't import (probably due to lack of Pivy on Mac and
                        # Linux builds of FreeCAD), this is a fallback.
                        # or old level of FreeCAD
                        if printverbose: print("Draft makePolygon Failed, falling back on manual polygon")
                        mycyl.Base = myPolygon(n,r1)
                        # mycyl.Solid = True

                    else :
                        pass
                    if gui:
                        mycyl.Base.ViewObject.hide()
                else: #Use Part::Prism primitive
                    mycyl=doc.addObject("Part::Prism","prism")
                    mycyl.Polygon = n
                    mycyl.Circumradius  = r1
                    mycyl.Height  = h

        elif (r1 != r2):
            if n < 3 or fnmax != 0 and n > fnmax:
                if printverbose: print("Make Cone")
                mycyl=doc.addObject("Part::Cone",p[1])
                mycyl.Height = h
                mycyl.Radius1 = r1
                mycyl.Radius2 = r2
            else:
                if printverbose: print("Make Frustum")
                mycyl=doc.addObject("Part::FeaturePython",'frustum')
                Frustum(mycyl,r1,r2,n,h)
                if gui:
                    if FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/OpenSCAD").\
                        GetBool('useViewProviderTree'):
                        from OpenSCADFeatures import ViewProviderTree
                        ViewProviderTree(mycyl.ViewObject)
                    else:
                        mycyl.ViewObject.Proxy = 0
        else: # r1 == r2 == 0
            FreeCAD.Console.PrintWarning('cylinder with radius zero\n')
            mycyl=doc.addObject("Part::Feature","emptycyl")
            mycyl.Shape = Part.Compound([])
    else: # h == 0
        FreeCAD.Console.PrintWarning('cylinder with height <= zero\n')
        mycyl=doc.addObject("Part::Feature","emptycyl")
        mycyl.Shape = Part.Compound([])
    if printverbose: print("Center = ",tocenter)
    if tocenter=='true' :
       center(mycyl,0,0,h)
    if False :  
#   Does not fix problemfile or beltTighener although later is closer       
        newobj=doc.addObject("Part::FeaturePython",'RefineCylinder')
        RefineShape(newobj,mycyl)
        if gui:
            if FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/OpenSCAD").\
                GetBool('useViewProviderTree'):
                from OpenSCADFeatures import ViewProviderTree
                ViewProviderTree(newobj.ViewObject)
            else:
                newobj.ViewObject.Proxy = 0
            mycyl.ViewObject.hide()
        p[0] = [newobj]
    else :
        p[0] = [mycyl]
    if printverbose: print("End Cylinder")

def p_cube_action(p):
    'cube_action : cube LPAREN keywordargument_list RPAREN SEMICOL'
    global doc
    l,w,h = [float(str1) for str1 in p[3]['size']]
    if (l > 0 and w > 0 and h >0):
        if printverbose: print("cube : ",p[3])
        mycube=doc.addObject('Part::Box',p[1])
        mycube.Length=l
        mycube.Width=w
        mycube.Height=h
    else:
        FreeCAD.Console.PrintWarning('cube with radius zero\n')
        mycube=doc.addObject("Part::Feature","emptycube")
        mycube.Shape = Part.Compound([])
    if p[3]['center']=='true' :
       center(mycube,l,w,h);
    p[0] = [mycube]
    if printverbose: print("End Cube")

def p_circle_action(p) :
    'circle_action : circle LPAREN keywordargument_list RPAREN SEMICOL'
    if printverbose: print("Circle : "+str(p[3]))
    r = float(p[3]['r'])
    # Avoid zero radius
    if r == 0 : r = 0.00001
    n = int(p[3]['$fn'])
    fnmax = FreeCAD.ParamGet(\
        "User parameter:BaseApp/Preferences/Mod/OpenSCAD").\
        GetInt('useMaxFN',50)
    # Alter Max polygon to control if polygons are circles or polygons
    # in the modules preferences
    import Draft
    if n == 0 or fnmax != 0 and n >= fnmax:
        mycircle = FreeCAD.ActiveDocument.addObject("Part::Part2DObjectPython",'circle')
        Draft._Circle(mycircle)
        mycircle.Radius = r
        mycircle.MakeFace = True
        #mycircle = Draft.makeCircle(r,face=True) # would call doc.recompute
        #mycircle = doc.addObject('Part::Circle',p[1]) #would not create a face
        #mycircle.Radius = r
    else :
        #mycircle = Draft.makePolygon(n,r) # would call doc.recompute
        mycircle = FreeCAD.ActiveDocument.addObject("Part::Part2DObjectPython",'polygon')
        Draft._Polygon(mycircle)
        mycircle.FacesNumber = n
        mycircle.Radius = r
        mycircle.DrawMode = "inscribed"
        mycircle.MakeFace = True
    if gui:
        Draft._ViewProviderDraft(mycircle.ViewObject)
    if printverbose: print("Push Circle")
    p[0] = [mycircle]

def p_square_action(p) :
    'square_action : square LPAREN keywordargument_list RPAREN SEMICOL'
    if printverbose: print("Square")
    size = p[3]['size']
    x = float(size[0])
    y = float(size[1])
    mysquare = doc.addObject('Part::Plane',p[1])
    mysquare.Length=x
    mysquare.Width=y
    if p[3]['center']=='true' :
       center(mysquare,x,y,0)
    p[0] = [mysquare]

def addString(t,s,p):
    return(t + ', ' +s+' = "'+p[3][s]+'"')

def addValue(t,v,p):
    return(t + ', ' +v+' = '+p[3][v])

def p_text_action(p) :
    'text_action : text LPAREN keywordargument_list RPAREN SEMICOL'
    # If text string is null ignore
    if p[3]['text'] == "" or p[3]['text'] == " " :
        p[0] = []
        return
    t = 'text ( text="'+p[3]['text']+'"'
    t = addValue(t,'size',p)
    t = addString(t,'spacing',p)
    t = addString(t,'font',p)
    t = addString(t,'direction',p)
    t = addString(t,'language',p)
    t = addString(t,'script',p)
    t = addString(t,'halign',p)
    t = addString(t,'valign',p)
    t = addValue(t,'$fn',p)
    t = addValue(t,'$fa',p)
    t = addValue(t,'$fs',p)
    t = t+');'

    FreeCAD.Console.PrintMessage("textmsg : "+t+"\n")
    p[0] = [processTextCmd(t)]

def convert_points_list_to_vector(l):
    v = []
    for i in l :
        if printverbose: print(i)
        v.append(FreeCAD.Vector(i[0],i[1]))
    if printverbose: print(v)
    return(v)


def p_polygon_action_nopath(p) :
    'polygon_action_nopath : polygon LPAREN points EQ OSQUARE points_list_2d ESQUARE COMMA paths EQ undef COMMA keywordargument_list RPAREN SEMICOL'
    if printverbose: print("Polygon")
    if printverbose: print(p[6])
    v = convert_points_list_to_vector(p[6])
    mypolygon = doc.addObject('Part::Feature',p[1])
    if printverbose: print("Make Parts")
    # Close Polygon
    v.append(v[0])
    parts = Part.makePolygon(v)
    if printverbose: print("update object")
    mypolygon.Shape = Part.Face(parts)
    p[0] = [mypolygon]

def p_polygon_action_plus_path(p) :
    'polygon_action_plus_path : polygon LPAREN points EQ OSQUARE points_list_2d ESQUARE COMMA paths EQ OSQUARE path_set ESQUARE COMMA keywordargument_list RPAREN SEMICOL'
    if printverbose: print("Polygon with Path")
    if printverbose: print(p[6])
    v = convert_points_list_to_vector(p[6])
    if printverbose: print("Path Set List")
    if printverbose: print(p[12])
    for i in p[12] :
         if printverbose: print(i)
         mypolygon = doc.addObject('Part::Feature','wire')
         path_list = []
         for j in i :
             j = int(j)
             if printverbose: print(j)
             path_list.append(v[j])
#        Close path
         path_list.append(v[int(i[0])])
         if printverbose: print('Path List')
         if printverbose: print(path_list)
         wire = Part.makePolygon(path_list)
         mypolygon.Shape = Part.Face(wire)
         p[0] = [mypolygon]
#        This only pushes last polygon

def make_face(v1,v2,v3):
    wire = Part.makePolygon([v1,v2,v3,v1])
    face = Part.Face(wire)
    return face

def p_polyhedron_action(p) :
    '''polyhedron_action : polyhedron LPAREN points EQ OSQUARE points_list_3d ESQUARE COMMA faces EQ OSQUARE path_set ESQUARE COMMA keywordargument_list RPAREN SEMICOL
                      | polyhedron LPAREN points EQ OSQUARE points_list_3d ESQUARE COMMA triangles EQ OSQUARE points_list_3d ESQUARE COMMA keywordargument_list RPAREN SEMICOL'''
    if printverbose: print("Polyhedron Points")
    v = []
    for i in p[6] :
        if printverbose: print(i)
        v.append(FreeCAD.Vector(float(i[0]),float(i[1]),float(i[2])))
    if printverbose:
        print(v)
        print ("Polyhedron "+p[9])
        print (p[12])
    faces_list = []    
    mypolyhed = doc.addObject('Part::Feature',p[1])
    for i in p[12] :
        if printverbose: print(i)
        v2 = FreeCAD.Vector
        pp =[v2(v[k]) for k in i]
        # Add first point to end of list to close polygon
        pp.append(pp[0])
        print pp
        w = Part.makePolygon(pp)
        f = Part.Face(w)
        #f = make_face(v[int(i[0])],v[int(i[1])],v[int(i[2])])
        faces_list.append(f)
    shell=Part.makeShell(faces_list)
    solid=Part.Solid(shell).removeSplitter()
    if solid.Volume < 0:
        solid.reverse()
    mypolyhed.Shape=solid
    p[0] = [mypolyhed]

def p_projection_action(p) :
    'projection_action : projection LPAREN keywordargument_list RPAREN OBRACE block_list EBRACE'
    if printverbose: print('Projection')
    if p[3]['cut']=='true' :
        planedim=1e9 # large but finite
        #infinite planes look bad in the GUI
        planename='xy_plane_used_for_project_cut'
        obj=doc.addObject('Part::MultiCommon','projection_cut')
        plane = doc.getObject(planename)
        if not plane:
            plane=doc.addObject("Part::Plane",planename)
            plane.Length=planedim*2
            plane.Width=planedim*2
            plane.Placement = FreeCAD.Placement(FreeCAD.Vector(\
                     -planedim,-planedim,0),FreeCAD.Rotation())
            if gui:
                plane.ViewObject.hide()
        if (len(p[6]) > 1):
            subobj = [fuse(p[6],"projection_cut_implicit_group")]
        else:
            subobj = p[6]
        obj.Shapes = [plane]+subobj
        if gui:
            subobj[0].ViewObject.hide()
        p[0] = [obj]
    else: # cut == 'false' => true projection
        if gui and not FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/OpenSCAD").\
                GetBool('usePlaceholderForUnsupported'):
            from PySide import QtGui
            QtGui.QMessageBox.critical(None, unicode(translate('OpenSCAD',"Unsupported Function"))+" : "+p[1],unicode(translate('OpenSCAD',"Press OK")))
        else:
            p[0] = [placeholder(p[1],p[6],p[3])]
importCSG.py (44,604 bytes)   
OpenSCADUtils.py (24,641 bytes)   
#***************************************************************************
#*                                                                         *
#*   Copyright (c) 2012 Sebastian Hoogen <github@sebastianhoogen.de>       *
#*                                                                         *
#*   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__="FreeCAD OpenSCAD Workbench - Utility Fuctions"
__author__ = "Sebastian Hoogen"
__url__ = ["http://www.freecadweb.org"]

'''
This Script includes various pyhton helper functions that are shared across
the module
'''

try:
    from PySide import QtGui
    _encoding = QtGui.QApplication.UnicodeUTF8
    def translate(context, text):
        "convenience function for Qt translator"
        return QtGui.QApplication.translate(context, text, None, _encoding)
except AttributeError:
    def translate(context, text):
        "convenience function for Qt translator"
        from PySide import QtGui
        return QtGui.QApplication.translate(context, text, None)

try:
    import FreeCAD
    BaseError = FreeCAD.Base.FreeCADError
except (ImportError, AttributeError):
    BaseError = RuntimeError

class OpenSCADError(BaseError):
    def __init__(self,value):
        self.value= value
    #def __repr__(self):
    #    return self.msg
    def __str__(self):
        return repr(self.value)

def searchforopenscadexe():
    import os,sys,subprocess
    if sys.platform == 'win32':
        testpaths = [os.path.join(os.environ.get('Programfiles(x86)','C:'),\
            'OpenSCAD\\openscad.exe')]
        if 'ProgramW6432' in os.environ:
            testpaths.append(os.path.join(os.environ.get('ProgramW6432','C:')\
                ,'OpenSCAD\\openscad.exe'))
        for testpath in testpaths:
            if os.path.isfile(testpath):
                return testpath
    elif sys.platform == 'darwin':
        ascript = ('tell application "Finder"\n'
        'POSIX path of (application file id "org.openscad.OpenSCAD"'
        'as alias)\n'
        'end tell')
        p1=subprocess.Popen(['osascript','-'],stdin=subprocess.PIPE,\
                stdout=subprocess.PIPE,stderr=subprocess.PIPE)
        stdout,stderr = p1.communicate(ascript)
        if p1.returncode == 0:
            opathl=stdout.split('\n')
            if len(opathl) >=1:
                return opathl[0]+'Contents/MacOS/OpenSCAD'
        #test the default path
        testpath="/Applications/OpenSCAD.app/Contents/MacOS/OpenSCAD"
        if os.path.isfile(testpath):
            return testpath
    else: #unix
        p1=subprocess.Popen(['which','openscad'],stdout=subprocess.PIPE)
        if p1.wait() == 0:
            opath=p1.stdout.read().split('\n')[0]
            return opath

def workaroundforissue128needed():
    '''sets the import path depending on the OpenSCAD Verion
    for versions <= 2012.06.23 to the current working dir
    for versions above to the inputfile dir
    see https://github.com/openscad/openscad/issues/128'''
    vdate=getopenscadversion().split('-')[0]
    vdate=vdate.split(' ')[2].split('.')
    year,mon=int(vdate[0]),int(vdate[1])
    return (year<2012 or (year==2012 and (mon <6 or (mon == 6 and \
        (len(vdate)<3 or int(vdate[2]) <=23)))))
    #ifdate=int(vdate[0])+(int(vdate[1])-1)/12.0
    #if len(vdate)>2:
    #    fdate+=int((vdate[2])-1)/12.0/31.0
    #return fdate < 2012.4759

def getopenscadversion(osfilename=None):
    import os,subprocess,time
    if not osfilename:
        import FreeCAD
        osfilename = FreeCAD.ParamGet(\
            "User parameter:BaseApp/Preferences/Mod/OpenSCAD").\
            GetString('openscadexecutable')
    if osfilename and os.path.isfile(osfilename):
        p=subprocess.Popen([osfilename,'-v'],\
            stdout=subprocess.PIPE,stderr=subprocess.PIPE,universal_newlines=True)
        p.wait()
        stdout=p.stdout.read().strip()
        stderr=p.stderr.read().strip()
        return (stdout or stderr)

def newtempfilename():
    import os,time
    formatstr='fc-%05d-%06d-%06d'
    count = 0
    while True:
        count+=1
        yield formatstr % (os.getpid(),int(time.time()*100) % 1000000,count)

tempfilenamegen=newtempfilename()

def callopenscad(inputfilename,outputfilename=None,outputext='csg',keepname=False):
    '''call the open scad binary
    returns the filename of the result (or None),
    please delete the file afterwards'''
    import FreeCAD,os,subprocess,tempfile,time
    def check_output2(*args,**kwargs):
        kwargs.update({'stdout':subprocess.PIPE,'stderr':subprocess.PIPE})
        p=subprocess.Popen(*args,**kwargs)
        stdoutd,stderrd = p.communicate()
        if p.returncode != 0:
            raise OpenSCADError('%s %s\n' % (stdoutd.strip(),stderrd.strip()))
            #raise Exception,'stdout %s\n stderr%s' %(stdoutd,stderrd)
        if stderrd.strip():
            FreeCAD.Console.PrintWarning(stderrd+u'\n')
        if stdoutd.strip():
            FreeCAD.Console.PrintMessage(stdoutd+u'\n')
            return stdoutd

    osfilename = FreeCAD.ParamGet(\
        "User parameter:BaseApp/Preferences/Mod/OpenSCAD").\
        GetString('openscadexecutable')
    if osfilename and os.path.isfile(osfilename):
        if not outputfilename:
            dir1=tempfile.gettempdir()
            if keepname:
                outputfilename=os.path.join(dir1,'%s.%s' % (os.path.split(\
                    inputfilename)[1].rsplit('.',1)[0],outputext))
            else:
                outputfilename=os.path.join(dir1,'%s.%s' % \
                    (tempfilenamegen.next(),outputext))
        check_output2([osfilename,'-o',outputfilename, inputfilename])
        return outputfilename
    else:
        raise OpenSCADError('OpenSCAD executeable unavailable')

def callopenscadstring(scadstr,outputext='csg'):
    '''create a tempfile and call the open scad binary
    returns the filename of the result (or None),
    please delete the file afterwards'''
    import os,tempfile,time
    dir1=tempfile.gettempdir()
    inputfilename=os.path.join(dir1,'%s.scad' % tempfilenamegen.next())
    inputfile = open(inputfilename,'w')
    inputfile.write(scadstr)
    inputfile.close()
    outputfilename = callopenscad(inputfilename,outputext=outputext,\
        keepname=True)
    os.unlink(inputfilename)
    return outputfilename

def reverseimporttypes():
    '''allows to search for supported filetypes by module'''

    def getsetfromdict(dict1,index):
        if index in dict1:
            return dict1[index]
        else:
            set1=set()
            dict1[index]=set1
            return set1

    importtypes={}
    import FreeCAD
    for key,value in FreeCAD.getImportType().iteritems():
        if type(value) is str:
            getsetfromdict(importtypes,value).add(key)
        else:
            for vitem in value:
                getsetfromdict(importtypes,vitem).add(key)
    return importtypes


def fcsubmatrix(m):
    """Extracts the 3x3 Submatrix from a freecad Matrix Object
    as a list of row vectors"""
    return [[m.A11,m.A12,m.A13],[m.A21,m.A22,m.A23],[m.A31,m.A32,m.A33]]

def multiplymat(l,r):
    """multiply matrices given as lists of row vectors"""
    rt=zip(*r) #transpose r
    mat=[]
    for y in range(len(rt)):
        mline=[]
        for x in range(len(l)):
            mline.append(sum([le*re for le,re in zip(l[y],rt[x])]))
        mat.append(mline)
    return mat

def isorthogonal(submatrix,precision=4):
    """checking if 3x3 Matrix is ortogonal (M*Transp(M)==I)"""
    prod=multiplymat(submatrix,zip(*submatrix))
    return [[round(f,precision) for f in line] \
        for line in prod]==[[1,0,0],[0,1,0],[0,0,1]]

def detsubmatrix(s):
    """get the determinant of a 3x3 Matrix given as list of row vectors"""
    return s[0][0]*s[1][1]*s[2][2]+s[0][1]*s[1][2]*s[2][0]+\
           s[0][2]*s[1][0]*s[2][1]-s[2][0]*s[1][1]*s[0][2]-\
           s[2][1]*s[1][2]*s[0][0]-s[2][2]*s[1][0]*s[0][1]

def isspecialorthogonalpython(submat,precision=4):
    return isorthogonal(submat,precision) and round(detsubmatrix(submat),precision)==1

def isrotoinversionpython(submat,precision=4):
    return isorthogonal(submat,precision) and round(detsubmatrix(submat),precision)==-1

def isspecialorthogonal(mat,precision=4):
    return abs(mat.submatrix(3).isOrthogonal(10**(-precision))-1.0) < \
            10**(-precision) and \
            abs(mat.submatrix(3).determinant()-1.0) < 10**(-precision)

def decomposerotoinversion(m,precision=4):
    import FreeCAD
    rmat = [[round(f,precision) for f in line] for line in fcsubmatrix(m)]
    cmat = FreeCAD.Matrix()
    if rmat ==[[-1,0,0],[0,1,0],[0,0,1]]:
        cmat.scale(-1,1,1)
        return m*cmat,FreeCAD.Vector(1)
    elif rmat ==[[1,0,0],[0,-1,0],[0,0,1]]:
        cmat.scale(1,-1,1)
        return m*cmat, FreeCAD.Vector(0,1)
    elif rmat ==[[1,0,0],[0,1,0],[0,0,-1]]:
        cmat.scale(1,1,-1)
        return m*cmat, FreeCAD.Vector(0,0,1)
    else:
        cmat.scale(1,1,-1)
        return m*cmat, FreeCAD.Vector(0,0,1)

def mirror2mat(nv,bv):
    import FreeCAD
    """calculate the transformation matrix of a mirror feature"""
    mbef=FreeCAD.Matrix()
    mbef.move(bv * -1)
    maft=FreeCAD.Matrix()
    maft.move(bv)
    return maft*vec2householder(nv)*mbef

def vec2householder(nv):
    """calculated the householder matrix for a given normal vector"""
    import FreeCAD
    lnv=nv.dot(nv)
    l=2/lnv if lnv > 0 else 0
    hh=FreeCAD.Matrix(nv.x*nv.x*l,nv.x*nv.y*l,nv.x*nv.z*l,0,\
                      nv.y*nv.x*l,nv.y*nv.y*l,nv.y*nv.z*l,0,\
                      nv.z*nv.x*l,nv.z*nv.y*l,nv.z*nv.z*l,0,0,0,0,0)
    return FreeCAD.Matrix()-hh


def angneg(d):
    return d if (d <= 180.0) else (d-360)

def shorthexfloat(f):
    s=f.hex()
    mantisse, exponent = f.hex().split('p',1)
    return '%sp%s' % (mantisse.rstrip('0'),exponent)


def comparerotations(r1,r2):
    import FreeCAD
    '''compares two rotations
    a value of zero means that they are identical'''
    r2c=FreeCAD.Rotation(r2)
    r2c.invert()
    return r1.multiply(r2c).Angle

def findbestmatchingrotation(r1):
    import FreeCAD
    vangl = \
(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 11.25, 12.0, 13.0,
14.0, 15.0, 16.0, (180.0/11.0), 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 22.5,
23.0, 24.0, 25.0, 26.0, 27.0, 28.0, 29.0, 30.0, 31.0, 32.0, (360.0/11.0),
33.0, 33.75, 34.0, 35.0, 36.0, 37.0, 38.0, 39.0, 40.0, 41.0, 42.0, 43.0,
44.0, 45.0, 46.0, 47.0, 48.0, 49.0,(540.0/11.0), 50.0, 51.0, (360.0/7.0),
52.0, 53.0, 54.0, 55.0, 56.0, 56.25, 57.0, 58.0, 59.0, 60.0, 61.0, 62.0,
63.0, 64.0, 65.0,(720.0/11.0), 66.0, 67.0, 67.5, 68.0, 69.0, 70.0, 71.0,
72.0, 73.0, 74.0, 75.0, 76.0, 77.0, 78.0, 78.75, 79.0, 80.0, 81.0,(900.0/11.0),
82.0, 83.0, 84.0, 85.0, 86.0, 87.0, 88.0, 89.0, 90.0, 91.0, 92.0, 93.0, 94.0,
95.0, 96.0, 97.0, 98.0,(1080.0/11.0), 99.0, 100.0, 101.0, 101.25, 102.0,
(720.0/7.0), 103.0, 104.0, 105.0, 106.0, 107.0, 108.0, 109.0, 110.0, 111.0,
112.0, 112.5, 113.0, 114.0, (1260.0/11), 115.0, 116.0, 117.0, 118.0, 119.0,
120.0, 121.0, 122.0, 123.0, 123.75, 124.0, 125.0, 126.0, 127.0, 128.0,
 129.0, 130.0,(1440.0/11.0), 131.0, 132.0, 133.0, 134.0, 135.0, 136.0,
137.0, 138.0, 139.0, 140.0, 141.0, 142.0, 143.0, 144.0, 145.0, 146.0, 146.25,
147.0, (1620.0/11.0), 148.0, 149.0, 150.0, 151.0, 152.0, 153.0, 154.0,
(1080.0/7.0), 155.0, 156.0, 157.0, 157.5, 158.0, 159.0, 160.0, 161.0, 162.0,
163.0, (1800.0/11.0), 164.0, 165.0, 166.0, 167.0, 168.0, 168.75, 169.0, 170.0,
171.0, 172.0, 173.0, 174.0, 175.0, 176.0, 177.0,178.0, 179.0,180.0,
-179.0, -178.0, -177.0, -176.0, -175.0, -174.0, -173.0, -172.0, -171.0, -170.0,
-169.0, -168.75, -168.0, -167.0, -166.0, -165.0, -164.0, (-1800.0/11.0),
-163.0, -162.0, -161.0, -160.0, -159.0, -158.0, -157.5, -157.0, -156.0,
-155.0, (-1080.0/7.0), -154.0, -153.0, -152.0, -151.0, -150.0, -149.0, -148.0,
(-1620.0/11.0), -147.0, -146.25, -146.0, -145.0, -144.0, -143.0, -142.0,
-141.0, -140.0, -139.0,-138.0, -137.0, -136.0, -135.0, -134.0, -133.0, -132.0,
 -131.0, (-1440/11.0), -130.0, -129.0, -128.0,-127.0, -126.0, -125.0, -124.0,
 -123.75, -123.0, -122.0, -121.0, -120.0, -119.0, -118.0, -117.0, -116.0,
-115.0,(-1260.0/11.0), -114.0, -113.0, -112.5, -112.0, -111.0, -110.0, -109.0,
-108.0, -107.0, -106.0, -105.0,-104.0, -103.0,(-720.0/7.0), -102.0, -101.25,
-101.0, -100.0, -99.0, (-1080.0/11.0), -98.0, -97.0, -96.0, -95.0, -94.0,
-93.0, -92.0, -91.0, -90.0, -89.0, -88.0, -87.0, -86.0, -85.0, -84.0, -83.0,
-82.0,(-900.0/11.0), -81.0, -80.0, -79.0, -78.75, -78.0, -77.0, -76.0, -75.0,
-74.0, -73.0, -72.0, -71.0, -70.0, -69.0, -68.0, -67.5, -67.0, -66.0,
(-720.0/11.0), -65.0, -64.0, -63.0, -62.0, -61.0, -60.0, -59.0, -58.0, -57.0,
-56.25, -56.0, -55.0, -54.0, -53.0, -52.0,(-360.0/7.0), -51.0, -50.0,
(-540.0/11.0), -49.0, -48.0, -47.0, -46.0, -45.0, -44.0, -43.0, -42.0, -41.0,
-40.0, -39.0, -38.0, -37.0, -36.0, -35.0, -34.0, -33.75, -33.0,(-360.0/11.0),
-32.0, -31.0, -30.0, -29.0, -28.0, -27.0, -26.0, -25.0, -24.0, -23.0, -22.5,
-22.0, -21.0, -20.0, -19.0, -18.0, -17.0,(-180.0/11.0), -16.0, -15.0, -14.0,
-13.0, -12.0, -11.25, -11.0, -10.0, -9.0, -8.0, -7.0, -6.0, -5.0, -4.0, -3.0,
-2.0, -1.0)
    def tup2nvect(tup):
        """convert a tuple to a normalized vector"""
        v=FreeCAD.Vector(*tup)
        v.normalize()
        return v

    def wkaxes():
        """well known axes for rotations"""
        vtupl=((1,0,0),(0,1,0),(0,0,1),
            (1,1,0),(1,0,1),(0,1,1),(-1,1,0),(-1,0,1),(0,1,-1),
            (1,1,1),(1,1,-1),(1,-1,1),(-1,1,1))
        return tuple(tup2nvect(tup) for tup in vtupl)

    bestrot=FreeCAD.Rotation()
    dangle = comparerotations(r1,bestrot)
    for axis in wkaxes():
        for angle in vangl:
            for axissign in (1.0,-1.0):
                r2=FreeCAD.Rotation(axis*axissign,angle)
                dangletest = comparerotations(r1,r2)
                if dangletest < dangle:
                    bestrot = r2
                    dangle = dangletest
    return (bestrot,dangle)

def roundrotation(rot,maxangulardistance=1e-5):
    '''guess the rotation axis and angle for a rotation
    recreated from rounded floating point values
    (from a quaterion or transformation matrix)'''
    def teststandardrot(r1,maxangulardistance=1e-5):
        '''test a few common rotations beforehand'''
        import FreeCAD,itertools
        eulers = []
        for angle in (90,-90,180,45,-45,135,-135):
            for euler in itertools.permutations((0,0,angle)):
                eulers.append(euler)
        for euler in itertools.product((0,45,90,135,180,-45,-90,-135),repeat=3):
                eulers.append(euler)
        for euler in eulers:
            r2 = FreeCAD.Rotation(*euler)
            if comparerotations(r1,r2) < maxangulardistance:
                return r2

    if rot.isNull():
        return rot
    firstguess = teststandardrot(rot,maxangulardistance)
    if firstguess is not None:
        return firstguess
    #brute force
    bestguess,angulardistance = findbestmatchingrotation(rot)
    if angulardistance < maxangulardistance: #use guess
        return bestguess
    else: #use original
        return rot

def callopenscadmeshstring(scadstr):
    """Call OpenSCAD and return the result as a Mesh"""
    import Mesh,os
    tmpfilename=callopenscadstring(scadstr,'stl')
    newmesh=Mesh.Mesh()
    newmesh.read(tmpfilename)
    try:
        os.unlink(tmpfilename)
    except OSError:
        pass
    return newmesh

def meshopinline(opname,iterable1):
    """uses OpenSCAD to combine meshes
    takes the name of the CGAL operation and an iterable (tuple,list) of 
    FreeCAD Mesh objects
    includes all the mesh data in the SCAD file
    """
    from exportCSG import mesh2polyhedron
    return callopenscadmeshstring('%s(){%s}' % (opname,' '.join(\
        (mesh2polyhedron(meshobj) for meshobj in iterable1))))

def meshoptempfile(opname,iterable1):
    """uses OpenSCAD to combine meshes
    takes the name of the CGAL operation and an iterable (tuple,list) of 
    FreeCAD Mesh objects
    uses stl files to supply the mesh data
    """
    import os,tempfile
    dir1=tempfile.gettempdir()
    filenames = []
    for mesh in iterable1:
        outputfilename=os.path.join(dir1,'%s.stl' % tempfilenamegen.next())
        mesh.write(outputfilename)
        filenames.append(outputfilename)
    #absolute path causes error. We rely that the scad file will be in the dame tmpdir
    meshimports = ' '.join("import(file = \"%s\");" % \
        #filename \
        os.path.split(filename)[1] for filename in filenames)
    result = callopenscadmeshstring('%s(){%s}' % (opname,meshimports))
    for filename in filenames:
        try:
            os.unlink(filename)
        except OSError:
            pass
    return result

def meshoponobjs(opname,inobjs):
    """
    takes a string (operation name) and a list of Feature Objects
    returns a mesh and a list of objects that were used
    Part Objects will be meshed
    """
    objs=[]
    meshes=[]
    for obj in inobjs:
        if obj.isDerivedFrom('Mesh::Feature'):
            objs.append(obj)
            meshes.append(obj.Mesh)
        elif obj.isDerivedFrom('Part::Feature'):
            #mesh the shape
            import FreeCAD
            params = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/OpenSCAD")
            objs.append(obj)
            if False: # disabled due to issue 1292
                import MeshPart
                meshes.append(MeshPart.meshFromShape(obj.Shape,params.GetFloat(\
                'meshmaxlength',1.0), params.GetFloat('meshmaxarea',0.0),\
                 params.GetFloat('meshlocallen',0.0),\
                 params.GetFloat('meshdeflection',0.0)))
            else:
                import Mesh
                meshes.append(Mesh.Mesh(obj.Shape.tessellate(params.GetFloat(\
                            'meshmaxlength',1.0))))
        else:
            pass #neither a mesh nor a part
    if len(objs) > 0:
            return (meshoptempfile(opname,meshes),objs)
    else:
            return (None,[])

def process2D_ObjectsViaOpenSCADShape(ObjList,Operation,doc):
    import FreeCAD,importDXF
    import os,tempfile
    dir1=tempfile.gettempdir()
    filenames = []
    for item in ObjList :
        outputfilename=os.path.join(dir1,'%s.dxf' % tempfilenamegen.next())
        importDXF.export([item],outputfilename,True,True)
        filenames.append(outputfilename)
    dxfimports = ' '.join("import(file = \"%s\");" % \
        #filename \
        os.path.split(filename)[1] for filename in filenames)
    print "callopenscadstring : " + '%s(){%s}' % (Operation,dxfimports) 
    tmpfilename = callopenscadstring('%s(){%s}' % (Operation,dxfimports),'dxf')
    from OpenSCAD2Dgeom import importDXFface
    # TBD: assure the given doc is active
    face = importDXFface(tmpfilename,None,None)
    #clean up
    filenames.append(tmpfilename) #delete the ouptut file as well
    try:
        os.unlink(tmpfilename)
    except OSError:
        pass
    return face

def process2D_ObjectsViaOpenSCAD(ObjList,Operation,doc=None):
    import FreeCAD
    doc = doc or FreeCAD.activeDocument()
    face=process2D_ObjectsViaOpenSCADShape(ObjList,Operation,doc)
    obj=doc.addObject('Part::Feature',Operation)
    obj.Shape=face
    # Hide Children
    if FreeCAD.GuiUp:
       for index in ObjList :
          index.ViewObject.hide()
    return(obj)

def process3D_ObjectsViaOpenSCADShape(ObjList,Operation,maxmeshpoints=None):
    import FreeCAD,Mesh,Part
    params = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/OpenSCAD")
    if False: # disabled due to issue 1292
        import MeshPart
        meshes = [MeshPart.meshFromShape(obj.Shape,params.GetFloat(\
                'meshmaxlength',1.0), params.GetFloat('meshmaxarea',0.0),\
                 params.GetFloat('meshlocallen',0.0),\
                 params.GetFloat('meshdeflection',0.0)) for obj in ObjList]
    else:
        meshes = [Mesh.Mesh(obj.Shape.tessellate(params.GetFloat(\
                            'meshmaxlength',1.0))) for obj in ObjList]
    if max(mesh.CountPoints for mesh in meshes) < \
            (maxmeshpoints or params.GetInt('tempmeshmaxpoints',5000)):
        stlmesh = meshoptempfile(Operation,meshes)
        sh=Part.Shape()
        sh.makeShapeFromMesh(stlmesh.Topology,0.1)
        solid = Part.Solid(sh)
        solid=solid.removeSplitter()
        if solid.Volume < 0:
           solid.complement()
        return solid

def process3D_ObjectsViaOpenSCAD(doc,ObjList,Operation):
    solid = process3D_ObjectsViaOpenSCADShape(ObjList,Operation)
    if solid is not None:
        obj=doc.addObject('Part::Feature',Operation) #non parametric objec
        obj.Shape=solid#.removeSplitter()
        if FreeCAD.GuiUp:
          for index in ObjList :
              index.ViewObject.hide()
        return(obj)

def process_ObjectsViaOpenSCADShape(doc,children,name,maxmeshpoints=None):
    if all((not obj.Shape.isNull() and obj.Shape.Volume == 0) \
            for obj in children):
        #KS temp debug export
	x = 1
	for obj in children :
            obj.Shape.exportBrep("/tmp/debugFC"+str(x)+".brep")
            x = x + 1
        return process2D_ObjectsViaOpenSCADShape(children,name,doc)
    elif all((not obj.Shape.isNull() and obj.Shape.Volume > 0) \
            for obj in children):
        return process3D_ObjectsViaOpenSCADShape(children,name,maxmeshpoints)
    else:
        import FreeCAD
        FreeCAD.Console.PrintError( unicode(translate('OpenSCAD',\
            "Error all shapes must be either 2D or both must be 3D"))+u'\n')

def process_ObjectsViaOpenSCAD(doc,children,name):
    if all((not obj.Shape.isNull() and obj.Shape.Volume == 0) \
            for obj in children):
        return process2D_ObjectsViaOpenSCAD(children,name,doc)
    elif all((not obj.Shape.isNull() and obj.Shape.Volume > 0) \
            for obj in children):
        return process3D_ObjectsViaOpenSCAD(doc,children,name)
    else:
        import FreeCAD
        FreeCAD.Console.PrintError( unicode(translate('OpenSCAD',\
            "Error all shapes must be either 2D or both must be 3D"))+u'\n')

def removesubtree(objs):
    def addsubobjs(obj,toremoveset):
        toremove.add(obj)
        for subobj in obj.OutList:
            addsubobjs(subobj,toremoveset)

    import FreeCAD
    toremove=set()
    for obj in objs:
        addsubobjs(obj,toremove)
    checkinlistcomplete =False
    while not checkinlistcomplete:
        for obj in toremove:
            if (obj not in objs) and (frozenset(obj.InList) - toremove):
                toremove.remove(obj)
                break
        else:
            checkinlistcomplete = True
    for obj in toremove:
        obj.Document.removeObject(obj.Name)

def applyPlacement(shape):
    if shape.Placement.isNull():
        return shape
    else:
        import Part
        if shape.ShapeType == 'Solid':
            return Part.Solid(shape.childShapes()[0])
        elif shape.ShapeType == 'Face':
            return Part.Face(shape.childShapes())
        elif shape.ShapeType == 'Compound':
            return Part.Compound(shape.childShapes())
        elif shape.ShapeType == 'Wire':
            return Part.Wire(shape.childShapes())
        elif shape.ShapeType == 'Shell':
            return Part.Shell(shape.childShapes())
        else:
            return Part.Compound([shape])
OpenSCADUtils.py (24,641 bytes)   

keithsloan52

2017-06-08 20:06

developer   ~0009315

The code to output the Brep's is around line 549 in OpenSCADUtils.py
and the output of the DXF is at line 483

keithsloan52

2017-06-13 12:49

developer   ~0009354

Last edited: 2017-06-13 13:08

OpenSCAD does not support POLYLINE so have to use LWPOLYLINE. The Draft Library importDXF.export has option for lwPoly but this does not appear to be working i.e. for a three sided polygon ( triangle ) it produces just a line
 
def export(objectslist,filename,nospline=False,lwPoly=False):
    "called when freecad exports a file. If nospline=True, bsplines are exported as straight segs lwPoly=True for OpenSCAD DXF"

But does explain why one is able to load brep versions and correctly export as DXF as the default is to call export with lwPoly=False

@yorik any ideas what has changed with LWPOLYLINE and why this is no longer working.

keithsloan52

2017-06-16 09:24

developer   ~0009405

Last edited: 2017-06-16 11:33

Not sure if it helps but loaded the debugFC2.brep file into FreeCAD and exported as a DXF file ( with Polyline ) Polyline.dxf. The bad file if one uses the FreeCAD importDXF.export with lwpoly flag is Bad-LWpolyline.dxf. Polyline.dxf converted to v13 using QCAD is QCAD-Polygon.dxf and has the three sided polygon as LWPOLYLINE
ENTITIES
  0
LWPOLYLINE
  5
29
330
55
100
AcDbEntity
  8
0
 62
     0
100
AcDbPolyline
 90
        3
 70
     1
 43
0.0
 10
37.4203102168
 20
15.5
 10
138.579689783
 20
15.5
 10
88.0
 20
66.0796897832
  0


@yorik

Bad-LWpolyline.dxf (760 bytes)   
Bad-LWpolyline.dxf (760 bytes)   
Polyline.dxf (888 bytes)   
Polyline.dxf (888 bytes)   
QCAD_Polygon.dxf (101,056 bytes)   
QCAD_Polygon.dxf (101,056 bytes)   

keithsloan52

2017-06-20 03:39

developer   ~0009462

Okay I changed Draft importDXF.py around line 1730 to
if hasattr(dxfLibrary,"LwPolyLine"):
   1731                     print "dxfobject append"
   1732                     dxfobject.append(dxfLibrary.LwPolyLine(getWire(wire,nospline,asis=asis), [0.0,0.0],
   1733                                                            int(DraftGeomUtils.isReallyClosed(wire)), color=color,
   1734                                                            layer=layer))
And getWire to output vertex's and the stdout then has
processing Offset2D
dxfobject append
('wire verts: ', [(37.42031021678297, 15.5, 0.0, None, None, 0.0), (138.57968978321702, 15.5, 0.0, None, None, 0.0), (88.0, 66.07968978321702, 0.0, None, None, 0.0)])
successfully exported /tmp/fc-11318-575989-000002.dxf
which looks good, but the output dxf file only contains the first vertex
AcDbPolyline
 90
3
 70
1
 10
0.0
 20
0.0
 10
37.4203102168
 20
15.5
  0
ENDSEC

So I conclude that the problem is in dxflibrary.LwPolyline which is only outputting the first vertex in the passed list.

SO I altered dxfLibrary around line 450 to

    449     for point in self.points:
    450                         print "point"
    451                         print point
    452                         result+='%s\n' %_point(point[0:2])


And the print of point only outputs one value
('wire verts: ', [(37.42031021678297, 15.5, 0.0, None, None, 0.0), (138.57968978321702, 15.5, 0.0, None, None, 0.0), (88.0, 66.07968978321702, 0.0, None, None, 0.0)])
point
(37.42031021678297, 15.5, 0.0, None, None, 0.0)

Should I try posting in the forum? @Kunda1

keithsloan52

2017-06-20 04:00

developer   ~0009463

Okay spotted the problem in dxfLibrary.py

Code looks like
for point in self.points:
    452                         print "point"
    453                         print point
    454                         result+='%s\n' %_point(point[0:2]) 
    455                         if len(point)>4: 
    456                                 width1, width2 = point[3], point[4]
    457                         if width1!=None: result+=' 40\n%s\n' %width1
    458                         if width2!=None: result+=' 41\n%s\n' %width2
    459                         if len(point)==6:
    460                                 bulge = point[5]
    461                                 if bulge:
    462                                         result+=' 42\n%s\n' %bulge
    463                         return result

The return result is indented to far so that it returns on the first value. It should look like
for point in self.points:
    452                         print "point"
    453                         print point
    454                         result+='%s\n' %_point(point[0:2]) 
    455                         if len(point)>4: 
    456                                 width1, width2 = point[3], point[4]
    457                         if width1!=None: result+=' 40\n%s\n' %width1
    458                         if width2!=None: result+=' 41\n%s\n' %width2
    459                         if len(point)==6:
    460                                 bulge = point[5]
    461                                 if bulge:
    462                                         result+=' 42\n%s\n' %bulge
    463                 return result

keithsloan52

2017-06-20 04:22

developer   ~0009464

Okay Pull request submitted for Yorik's dxflibrary
@Yorik

I note that the readme says no longer used by FreeCAD, but it is still used by OpenSCAD workbench importCSG

Kunda1

2017-06-25 15:47

administrator   ~0009526

@yorik can you weigh in? Seems Keith has sent a PR o your DXF branch on your personal repo.

yorik

2017-06-26 14:06

administrator   ~0009543

The PR is merged

Kunda1

2017-06-26 21:41

administrator   ~0009553

Marking as fixed. Users need to set the preference for FreeCAD to load the dxf library to pick up latest version.
Thanks Keith!

Kunda1

2017-07-05 17:04

administrator   ~0009670

Closing

Issue History

Date Modified Username Field Change
2015-09-16 15:46 l3iggs New Issue
2015-09-16 15:46 l3iggs File Added: plate1.csg
2017-01-16 09:04 Kunda1 Project FreeCAD => File formats
2017-01-16 09:04 Kunda1 Category Bug => General
2017-02-02 17:30 Kunda1 Note Added: 0008175
2017-02-02 17:31 Kunda1 Note Edited: 0008175
2017-02-02 17:31 Kunda1 Status new => confirmed
2017-02-02 17:32 Kunda1 Tag Attached: CSG
2017-05-11 03:28 Kunda1 Note Added: 0008949
2017-05-11 21:21 Kunda1 Note Added: 0008964
2017-05-12 14:58 keithsloan52 File Added: plate2.csg
2017-05-12 14:58 keithsloan52 Note Added: 0008975
2017-05-12 15:25 keithsloan52 Note Added: 0008976
2017-05-12 16:20 keithsloan52 File Added: plate3.csg
2017-05-12 16:20 keithsloan52 File Added: plate3.fcstd
2017-05-12 16:20 keithsloan52 File Added: plate3.dxf
2017-05-12 16:20 keithsloan52 File Added: fc-12862-441813-000002.dxf
2017-05-12 16:20 keithsloan52 Note Added: 0008977
2017-06-07 11:50 keithsloan52 File Added: debugFC2.brep
2017-06-07 11:50 keithsloan52 File Added: fc-03100-455637-000017.dxf
2017-06-07 11:50 keithsloan52 Note Added: 0009304
2017-06-07 12:09 Kunda1 Note Edited: 0009304
2017-06-07 12:09 Kunda1 Relationship added related to 0003072
2017-06-07 12:10 Kunda1 Tag Attached: DXF
2017-06-07 12:10 Kunda1 Priority normal => high
2017-06-08 19:12 yorik Note Added: 0009313
2017-06-08 19:49 keithsloan52 File Added: plate2-2.csg
2017-06-08 19:49 keithsloan52 File Added: importCSG.py
2017-06-08 19:49 keithsloan52 File Added: OpenSCADUtils.py
2017-06-08 19:49 keithsloan52 Note Added: 0009314
2017-06-08 20:06 keithsloan52 Note Added: 0009315
2017-06-13 12:49 keithsloan52 Note Added: 0009354
2017-06-13 13:08 keithsloan52 Note Edited: 0009354
2017-06-15 16:26 Kunda1 Relationship added related to 0003035
2017-06-16 09:24 keithsloan52 File Added: Bad-LWpolyline.dxf
2017-06-16 09:24 keithsloan52 File Added: Polyline.dxf
2017-06-16 09:24 keithsloan52 File Added: QCAD_Polygon.dxf
2017-06-16 09:24 keithsloan52 Note Added: 0009405
2017-06-16 11:33 Kunda1 Note Edited: 0009405
2017-06-20 03:39 keithsloan52 Note Added: 0009462
2017-06-20 04:00 keithsloan52 Note Added: 0009463
2017-06-20 04:22 keithsloan52 Note Added: 0009464
2017-06-25 15:47 Kunda1 Note Added: 0009526
2017-06-26 14:06 yorik Note Added: 0009543
2017-06-26 21:41 Kunda1 Assigned To => keithsloan52
2017-06-26 21:41 Kunda1 Status confirmed => resolved
2017-06-26 21:41 Kunda1 Resolution open => fixed
2017-06-26 21:41 Kunda1 Note Added: 0009553
2017-07-05 17:04 Kunda1 Note Added: 0009670
2017-07-05 17:04 Kunda1 Status resolved => closed