Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-04-09 07:48:51

0001 #!/usr/bin/env python
0002 #
0003 # Copyright (c) 2019 Opticks Team. All Rights Reserved.
0004 #
0005 # This file is part of Opticks
0006 # (see https://bitbucket.org/simoncblyth/opticks).
0007 #
0008 # Licensed under the Apache License, Version 2.0 (the "License"); 
0009 # you may not use this file except in compliance with the License.  
0010 # You may obtain a copy of the License at
0011 #
0012 #   http://www.apache.org/licenses/LICENSE-2.0
0013 #
0014 # Unless required by applicable law or agreed to in writing, software 
0015 # distributed under the License is distributed on an "AS IS" BASIS, 
0016 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  
0017 # See the License for the specific language governing permissions and 
0018 # limitations under the License.
0019 #
0020 
0021 """
0022 CSG_
0023 =====
0024 
0025 Used by tboolean- for CSG solid construction and serialization
0026 for reading by npy-/NCSG 
0027 
0028 """
0029 import os, sys, logging, json, string, numpy as np
0030 log = logging.getLogger(__name__)
0031 
0032 # bring in enum values from sysrap/OpticksCSG.h
0033 from opticks.sysrap.OpticksCSG import CSG_
0034 from opticks.analytic.glm import make_trs, to_pyline, to_codeline, to_cpplist, make_scale
0035 #from opticks.analytic.bezier import Bezier
0036 from opticks.analytic.prism import make_segment, make_trapezoid, make_icosahedron, make_prism, make_cubeplanes
0037 from opticks.analytic.textgrid import TextGrid
0038 from opticks.analytic.tbool import TBoolBashFunction
0039 from opticks.analytic.nnode_test_cpp import NNodeTestCPP
0040 
0041 
0042 Q0,Q1,Q2,Q3 = 0,1,2,3
0043 X,Y,Z,W = 0,1,2,3
0044 
0045 TREE_NODES = lambda height:( (0x1 << (1+(height))) - 1 )
0046 TREE_PRIMITIVES = lambda height:1 << height 
0047 TREE_EXPECTED = map(TREE_NODES, range(10))   # [1, 3, 7, 15, 31, 63, 127, 255, 511, 1023]
0048 
0049 fromstring_  = lambda s:np.fromstring(s, dtype=np.float32, sep=",")
0050 
0051 def from_arg_(s, dtype=np.float32,sep=","):
0052    r = None 
0053    if type(s) is str:
0054        r = np.fromstring(s, dtype=dtype, sep=sep)
0055    elif s is not None:
0056        r = np.asarray(s, dtype=dtype) 
0057    pass
0058    return r
0059 
0060 
0061 class CSG(CSG_):
0062     """
0063     Serialization layout here must echo that in NCSG 
0064     """
0065     NJ, NK = 4, 4
0066     FILENAME = "csg.txt"
0067     METANAME = "csgmeta.json"
0068     CONVEX_POLYHEDRA = [CSG_.TRAPEZOID]
0069 
0070 
0071     def depth_(self, label=False):
0072         """Label tree nodes with their depth from the root, return maxdepth"""
0073         def depth_r(node, depth):
0074             if node is None:return depth
0075             #node.textra = " %s" % depth
0076             if label:
0077                 node.depth = depth
0078             pass
0079             if node.left is None and node.right is None:
0080                 return depth
0081             else:
0082                 ldepth = depth_r(node.left, depth+1)
0083                 rdepth = depth_r(node.right, depth+1)
0084                 return max(ldepth, rdepth)
0085             pass
0086         pass
0087         return depth_r(self, 0) 
0088 
0089 
0090     def subdepth_(self, label=False):
0091         """label tree with *subdepth* : the max height of each node treated as a subtree"""
0092         def subdepth_r(node, depth):
0093             assert not node is None
0094             subdepth = node.depth_(label=False)
0095             if label:
0096                 node.subdepth = subdepth 
0097             pass
0098             #node.textra = " %s" % subdepth
0099             pass
0100             if node.left is None and node.right is None:
0101                 pass 
0102             else:
0103                 subdepth_r(node.left, depth=depth+1)
0104                 subdepth_r(node.right, depth=depth+1)
0105             pass
0106         pass
0107         subdepth_r(self, 0)
0108 
0109 
0110     def subtrees_(self, subdepth=1):
0111         """collect all subtrees of a particular subdepth"""
0112         subtrees = []
0113         def subtrees_r(node):
0114             if node is None:return
0115             subtrees_r(node.left)
0116             subtrees_r(node.right)
0117             # postorder visit 
0118             if node.subdepth == subdepth:
0119                 subtrees.append(node)  
0120             pass
0121         pass
0122         subtrees_r(self)
0123         return subtrees
0124 
0125 
0126     def balance(self):
0127         assert hasattr(self, 'subdepth'), "balancing requires subdepth labels first"
0128         def balance_r(node, depth):
0129             leaf = node.is_leaf 
0130             if not leaf: 
0131                 balance_r(node.left, depth+1)
0132                 balance_r(node.right, depth+1)
0133             pass
0134             # postorder visit 
0135             if not leaf:
0136                 print("%2d %2d %2d : %s " % (node.subdepth,node.left.subdepth,node.right.subdepth, node)) 
0137             pass
0138         pass
0139         balance_r(self, 0)
0140 
0141 
0142     def positivize(self):
0143         """
0144         Using background info from Rossignac, "Blist: A Boolean list formulation of CSG trees"
0145 
0146         * http://www.cc.gatech.edu/~jarek/papers/Blist.pdf  
0147         * https://www.ics.uci.edu/~pattis/ICS-31/lectures/simplify/lecture.html
0148 
0149         The positive form of a CSG expression is obtained by 
0150         distributing any negations down the tree to end up  
0151         with complements in the leaves only.
0152 
0153         * DIFFERNCE -> "-" 
0154         * UNION -> "+"  "OR"
0155         * INTERSECTION -> "*"  "AND"
0156         * COMPLEMENT -> "!" 
0157 
0158         deMorgan Tranformations
0159 
0160             A - B -> A * !B
0161             !(A*B) -> !A + !B
0162             !(A+B) -> !A * !B
0163             !(A - B) -> !(A*!B) -> !A + B
0164 
0165         Example
0166 
0167         (A+B)(C-(D-E))                      (A+B)(C(!D+E))
0168 
0169                        *                                   *
0170                                                                         
0171                   +          -                        +          *
0172                  / \        / \                      / \        / \
0173                 A   B      C    -                   A   B      C    +  
0174                                / \                                 / \
0175                               D   E                              !D   E
0176 
0177         test_positivize
0178 
0179             original
0180 
0181                          in                    
0182                  un              di            
0183              sp      sp      bo          di    
0184                                      bo      bo
0185             positivize
0186 
0187                          in                    
0188                  un              in            
0189              sp      sp      bo          un    
0190                                     !bo      bo
0191 
0192         """
0193         deMorganSwap = {CSG_.INTERSECTION:CSG_.UNION, CSG_.UNION:CSG_.INTERSECTION }
0194 
0195         def positivize_r(node, negate=False, depth=0):
0196 
0197 
0198             if node.left is None and node.right is None:
0199                 if negate:
0200                     node.complement = not node.complement
0201                 pass
0202             else:
0203                 #log.info("beg: %s %s " % (node, "NEGATE" if negate else "") ) 
0204                 if node.typ in [CSG_.INTERSECTION, CSG_.UNION]:
0205 
0206                     if negate:    #  !( A*B ) ->  !A + !B       !(A+B) ->     !A * !B
0207                         node.typ = deMorganSwap.get(node.typ, None)
0208                         assert node.typ
0209                         left_negate = True 
0210                         right_negate = True
0211                     else:        #   A * B ->  A * B         A+B ->  A+B
0212                         left_negate = False
0213                         right_negate = False
0214                     pass
0215                 elif node.typ == CSG_.DIFFERENCE:
0216 
0217                     if negate:  #      !(A - B) -> !(A*!B) -> !A + B
0218                         node.typ = CSG_.UNION 
0219                         left_negate = True
0220                         right_negate = False 
0221                     else:
0222                         node.typ = CSG_.INTERSECTION    #    A - B ->  A*!B
0223                         left_negate = False
0224                         right_negate = True 
0225                     pass
0226                 else:
0227                     assert 0, "unexpected node.typ %s " % node.typ
0228                 pass
0229 
0230                 #log.info("end: %s " % node ) 
0231                 positivize_r(node.left, negate=left_negate, depth=depth+1)
0232                 positivize_r(node.right, negate=right_negate, depth=depth+1)
0233             pass
0234         pass
0235         positivize_r(self)
0236         assert self.is_positive_form()
0237         self.analyse()
0238 
0239     def operators_(self, minsubdepth=0):
0240         ops = set()
0241         def operators_r(node, depth):
0242             if node.left is None and node.right is None:
0243                 pass
0244             else:
0245                 assert node.typ in [CSG_.INTERSECTION, CSG_.UNION, CSG_.DIFFERENCE]
0246 
0247                 # preorder visit
0248                 if node.subdepth >= minsubdepth:
0249                     ops.add(node.typ)
0250                 pass
0251 
0252                 operators_r(node.left, depth+1)
0253                 operators_r(node.right, depth+1)
0254             pass
0255         pass
0256         operators_r(self, 0)
0257         return list(ops)
0258 
0259 
0260     def is_balance_disabled(self):
0261         disabled = [] 
0262         def is_balance_disabled_r(node, depth):
0263             if node.left is None and node.right is None:
0264                 if node.balance_disabled:        
0265                     disabled.append(node)
0266                 pass
0267             else:
0268                 if node.balance_disabled:        
0269                     disabled.append(node)
0270                 pass
0271                 is_balance_disabled_r(node.left, depth+1)
0272                 is_balance_disabled_r(node.right, depth+1)
0273             pass
0274         pass
0275         is_balance_disabled_r(self, 0)
0276 
0277         return len(disabled) > 0
0278 
0279 
0280     def is_positive_form(self):
0281         ops = self.operators_()
0282         return not CSG.DIFFERENCE in ops
0283 
0284     def is_mono_operator(self):
0285         ops = self.operators_()
0286         return len(ops) == 1 
0287 
0288     def is_mono_bileaf(self):
0289         """
0290         Example of a mono bileaf tree, with all opertors 
0291         above subdepth 1 (bileaf level) being the same, in this case: union.
0292         The numbers label the subdepth.::
0293 
0294                                                                                                        un 7            
0295                                                                                         un 6                     in 1    
0296                                                                         un 5                     in 1         cy 0     !cy 0
0297                                                         un 4                     in 1         cy 0     !cy 0                
0298                                         un 3                     in 1         cy 0     !cy 0                                
0299                         un 2                     in 1         cy 0     !cy 0                                                
0300                 in 1             in 1         cy 0     !cy 0                                                                
0301             cy 0     !cy 0     cy 0     !cy 0              
0302 
0303 
0304         """ 
0305         subdepth = 1   # bileaf level, one step up from the primitives
0306         ops = self.operators_(minsubdepth=subdepth+1)  # look at operators above the bileafs 
0307         return len(ops) == 1 
0308 
0309 
0310     def primitives(self):
0311         prims = []
0312         def primitives_r(node, depth):
0313             if node.left is None and node.right is None:
0314                 prims.append(node)
0315             else:
0316                 primitives_r(node.left, depth+1)
0317                 primitives_r(node.right, depth+1)
0318             pass
0319         pass
0320         primitives_r(self, 0)
0321         return prims
0322 
0323 
0324     def inorder_(self):
0325         """Return inorder sequence of nodes"""
0326         inorder = []
0327         def inorder_r(node):
0328             if node is None:return
0329             inorder_r(node.left)
0330             inorder.append(node)
0331             inorder_r(node.right)
0332         pass
0333         inorder_r(self)
0334         return inorder
0335 
0336     def parenting_(self):
0337         """Label tree nodes with their parent"""
0338         def parenting_r(node, parent):
0339             if node is None:return 
0340             node.parent = parent
0341             parenting_r(node.left, node)
0342             parenting_r(node.right, node)
0343         pass
0344         parenting_r(self, None)
0345 
0346     def rooting_(self):
0347         """Label tree nodes with root"""
0348         def rooting_r(node, root):
0349             if node is None:return 
0350             node.root = root
0351             rooting_r(node.left, root)
0352             rooting_r(node.right, root)
0353         pass
0354         rooting_r(self, self) 
0355 
0356     elevation = property(lambda self:self.root.height - self.depth) 
0357 
0358 
0359     def alabels_(self):
0360         """Auto label nodes with primitive code letters"""
0361         prims = [] 
0362         def alabels_r(node):
0363             if node is None:return 
0364             alabels_r(node.left)
0365             alabels_r(node.right)
0366             # postorder visit, so primitives always visited before their parents
0367             if node.is_primitive:
0368                 alabel = string.ascii_lowercase[len(prims)]
0369                 prims.append(node)
0370             else: 
0371                 if node.left is None or node.right is None:
0372                     log.warning("alabels_ malformed binary tree") 
0373                     alabel = "ERR"
0374                 else:
0375                     alabel = node.left.alabel + node.right.alabel
0376                 pass
0377             pass
0378 
0379             reserved_words = ["def","in","if","else"]
0380             if alabel in reserved_words:
0381                 alabel = alabel + "_"
0382             pass 
0383             node.alabel = alabel
0384             #node.textra = " %s" % alabel
0385         pass
0386         alabels_r(self) 
0387 
0388 
0389     def analyse(self):
0390         """
0391         Call from root node to label the tree, root node is annotated with:
0392 
0393         height 
0394              maximum node depth
0395  
0396         totnodes 
0397              complete binary tree node count for the height 
0398             
0399         ::
0400 
0401             In [45]: map(TREE_NODES, range(10))
0402             Out[45]: [1, 3, 7, 15, 31, 63, 127, 255, 511, 1023] 
0403 
0404         """
0405         self.height = self.depth_(label=True)
0406         self.parenting_()
0407         self.rooting_()
0408         self.subdepth_(label=True)
0409         self.alabels_()
0410 
0411         self.totnodes = TREE_NODES(self.height)
0412         inorder = self.inorder_()
0413 
0414         txt = TextGrid( self.height+1, len(inorder) )
0415         for i,node in enumerate(inorder):
0416             label = "%s%s" % (node.dsc, node.textra) if hasattr(node,'textra') else node.dsc
0417             txt.a[node.depth, i] = label 
0418         pass
0419         self.txt = txt
0420 
0421     is_root = property(lambda self:hasattr(self,'height') and hasattr(self,'totnodes'))
0422 
0423     @classmethod
0424     def treedir(cls, base, idx):
0425         return os.path.join(base, "%d" % idx )
0426 
0427     @classmethod
0428     def txtpath(cls, base):
0429         return os.path.join(base, cls.FILENAME )
0430 
0431     @classmethod
0432     def Metapath(cls, base):
0433         return os.path.join(base, cls.METANAME )
0434 
0435     @classmethod
0436     def SaveMeta(cls, base, topmeta={}):
0437         path = cls.Metapath(base)
0438         json.dump(topmeta,open(path,"w"))
0439 
0440     @classmethod
0441     def Serialize(cls, trees, args, outerfirst=1):
0442         """
0443         :param trees: list of CSG instances of solid root nodes
0444         :param args: namespace instance provided by opticks_main directory to save the tree serializations, under an indexed directory 
0445         :param outerfirst: when 1 signifies that the first listed tree contains is the outermost volume 
0446 
0447         1. saves each tree into a separate directories
0448         2. saves FILENAME csg.txt containing boundary strings at top level
0449         3. saves METANAME csgmeta.json containing tree level metadata at top level
0450 
0451         """
0452         assert type(trees) is list 
0453         #base = args.csgpath 
0454         assert args.csgpath is None, (args.csgpath, args.csgname, "args.csgpath no longer used, replace with csgname=${FUNCNAME/--} " )
0455 
0456         base = args.resolve("$TMP/%s" % args.csgname )
0457         assert type(base) is str and len(base) > 5, ("invalid base directory %s " % base)
0458 
0459         log.info("CSG.Serialize : writing %d trees to directory %s " % (len(trees), base))
0460         if not os.path.exists(base):
0461             os.makedirs(base)
0462         pass
0463         for it, tree in enumerate(trees):
0464             treedir = cls.treedir(base,it)
0465             tree.save(treedir)
0466         pass
0467 
0468         boundaries = list(map(lambda tree:tree.boundary, trees))
0469         cls.CheckNonBlank(boundaries)
0470         open(cls.txtpath(base),"w").write("\n".join(boundaries))
0471 
0472         csgmeta = {}
0473         csgmeta["mode"] = "PyCsgInBox" 
0474         csgmeta["analytic"] = 1 
0475         csgmeta["name"] = os.path.basename(base)
0476         csgmeta["csgpath"] = base
0477         csgmeta["outerfirst"] = outerfirst 
0478         csgmeta["autocontainer"] = args.autocontainer
0479         csgmeta["autoobject"] = args.autoobject
0480         csgmeta["autoemitconfig"] = args.autoemitconfig
0481         csgmeta["autoseqmap"] = args.autoseqmap
0482 
0483         meta_fmt_ = lambda meta:"_".join(["%s=%s" % kv for kv in meta.items()])
0484         print(meta_fmt_(csgmeta))  # communicates to tboolean--
0485         cls.SaveMeta(base, csgmeta)     # read by NCSG::Deserialize
0486         pass
0487 
0488     @classmethod
0489     def CheckNonBlank(cls, boundaries):
0490         boundaries2 = list(filter(None, boundaries))
0491         assert len(boundaries) == len(boundaries2), "there are blank boundaries\n%s" % "\n".join(boundaries) 
0492 
0493     @classmethod
0494     def Deserialize(cls, base):
0495         base = os.path.expandvars(base) 
0496         assert os.path.exists(base)
0497         boundaries = open(cls.txtpath(base)).read().splitlines()
0498         cls.CheckNonBlank(boundaries)
0499         trees = []
0500         for idx, boundary in enumerate(boundaries): 
0501             tree = cls.load(cls.treedir(base, idx))      
0502             tree.boundary = boundary 
0503             trees.append(tree)
0504         pass
0505         return trees
0506 
0507 
0508     @classmethod
0509     def CubePlanes(cls, hsize=0.5):
0510         planes = np.zeros([6,4], dtype=np.float32)  # unit cube for testing
0511         for i in range(3):
0512             plp = np.array([int(i==0),int(i==1),int(i==2),hsize], dtype=np.float32)  
0513             pln = np.array([-int(i==0),-int(i==1),-int(i==2),hsize], dtype=np.float32)  
0514             planes[2*i+0] = plp
0515             planes[2*i+1] = pln
0516         pass
0517         return planes
0518 
0519 
0520 
0521     @classmethod
0522     def MakeConvexPolyhedron_(cls, src, type_="convexpolyhedron"):
0523         """see tboolean-segment- """
0524  
0525         bbox = src.bbox
0526         planes = src.planes
0527         verts = src.verts
0528         faces = src.faces
0529         srcmeta = src.srcmeta
0530 
0531         obj = CSG(type_)
0532         obj.planes = planes
0533         obj.verts = verts
0534         obj.faces = faces
0535  
0536         obj.param2[:3] = bbox[0]
0537         obj.param3[:3] = bbox[1]
0538         obj.meta.update(srcmeta)
0539 
0540         return obj
0541 
0542 
0543 
0544     @classmethod
0545     def MakeConvexPolyhedron(cls, pl, vv, bb, src, type_="convexpolyhedron"):
0546         """see tboolean-segment- """
0547  
0548         bbox = src.bbox
0549         planes = src.planes
0550         verts = src.verts
0551         faces = src.faces
0552 
0553         assert np.all( planes == pl )
0554         assert np.all( bbox == bb )
0555         assert np.all( verts == vv )
0556         assert len(planes) == len(faces)
0557 
0558         return cls.MakeConvexPolyhedron_(src)
0559 
0560     @classmethod
0561     def MakeSegment(cls, phi0, phi1, sz, sr ):
0562         """see tboolean-segment- """
0563         planes, verts, bbox, src = make_segment(phi0,phi1,sz,sr)
0564         return cls.MakeConvexPolyhedron(planes, verts, bbox, src, "segment")
0565 
0566     @classmethod
0567     def MakeTrapezoid(cls, z=200, x1=160, y1=20, x2=691.02, y2=20):
0568         planes, verts, bbox, src = make_trapezoid(z,x1,y1,x2,y2)
0569         return cls.MakeConvexPolyhedron(planes, verts, bbox, src, "trapezoid")
0570 
0571     @classmethod
0572     def MakeIcosahedron(cls, scale=100.):
0573         planes, verts, bbox, src = make_icosahedron(scale=scale)
0574         return cls.MakeConvexPolyhedron(planes, verts, bbox, src, "convexpolyhedron")
0575 
0576     @classmethod
0577     def MakePrism(cls, angle, height, depth):
0578         planes, verts, bbox, src = make_prism(angle, height, depth)
0579         return cls.MakeConvexPolyhedron(planes, verts, bbox, src, "convexpolyhedron")
0580 
0581 
0582     @classmethod
0583     def MakeCubePlanes(cls, hx=0.5, hy=0.5, hz=0.5):
0584         src = make_cubeplanes(hx, hy, hz)
0585         return cls.MakeConvexPolyhedron_(src)
0586 
0587 
0588 
0589     @classmethod
0590     def MakeTorus(cls, R=100., r=10., name="MakeTorus"):
0591         assert R > r 
0592         srcmeta = dict(src_type="torus")
0593         obj = CSG("torus", param=[0,0,r,R], name=name)
0594         obj.meta.update(srcmeta)
0595         return obj
0596 
0597     @classmethod
0598     def MakeHyperboloid(cls, r0=100., zf=100., z1=-100., z2=100., name="MakeHyperboloid"):
0599         """
0600         Radius increases to sqrt(2)*r0 at z = +-zf
0601         """
0602         assert z2 > z1
0603         assert r0 > 0
0604         assert zf > 0
0605         srcmeta = dict(src_type="hyperboloid")
0606         obj = CSG("hyperboloid", param=[r0,zf,z1,z2], name=name)
0607         obj.meta.update(srcmeta)
0608         return obj
0609 
0610     @classmethod
0611     def MakeCubic(cls, A=1., B=1., C=1., D=1., z1=-100., z2=100., name="MakeCubic"):
0612         assert z2 > z1
0613         srcmeta = dict(src_type="cubic")
0614         obj = CSG("cubic", param=[A,B,C,D], param1=[z1,z2,0,0], name=name)
0615         obj.meta.update(srcmeta)
0616         return obj
0617 
0618     @classmethod
0619     def MakeCubicBezier(cls, zr, name="MakeCubicBezier"):
0620         zr = np.asarray(zr) 
0621         assert len(zr) > 2 and len(zr) <= 4, zr
0622         z = zr[:,0]
0623         r = zr[:,1]
0624         rr = r*r 
0625 
0626         zrr = np.zeros( (4,2), dtype=np.float32)
0627         zrr[:,0] = z 
0628         zrr[:,1] = rr 
0629 
0630         z1 = z[0]
0631         z2 = z[-1]
0632         assert z2 > z1
0633         zco = Bezier.ZCoeff(zrr) 
0634         assert len(zco) == 4
0635         A,B,C,D = zco
0636         return cls.MakeCubic(A=A,B=B,C=C,D=D,z1=z1,z2=z2, name=name) 
0637 
0638     @classmethod
0639     def MakeUndefined(cls, name="MakeUndefined", **kwa):
0640         obj = CSG("undefined", name=name)
0641         obj.meta.update(kwa)
0642         return obj
0643 
0644 
0645     @classmethod
0646     def MakeEllipsoid(cls, axes=[1,1,1], name="MakeEllipsoid", zcut1=None, zcut2=None):
0647 
0648         axyz = np.asarray(axes, dtype=np.float32)
0649 
0650         ax = float(axyz[0])
0651         by = float(axyz[1])
0652         cz = float(axyz[2])
0653 
0654         zcut2 =  cz if zcut2 is None else float(zcut2)
0655         zcut1 = -cz if zcut1 is None else float(zcut1)
0656 
0657 
0658         srcmeta = dict(src_type="ellipsoid", src_ax=ax, src_by=by, src_cz=cz, src_zcut1=zcut1, src_zcut2=zcut2 )
0659 
0660         scale = axyz/cz    ## NB scale is 1 in z, for simple z1/z2
0661         z2 = zcut2
0662         z1 = zcut1
0663 
0664 
0665         cn = CSG("zsphere", name=name)
0666 
0667         cn.param[0] = 0
0668         cn.param[1] = 0
0669         cn.param[2] = 0
0670         cn.param[3] = cz
0671 
0672         cn.param1[0] = z1
0673         cn.param1[1] = z2
0674         cn.param1[2] = 0
0675         cn.param1[3] = 0
0676 
0677         cn.scale = scale
0678 
0679         cn.meta.update(srcmeta)
0680  
0681         log.info("MakeEllipsoid axyz %s scale %s " % (repr(axyz), repr(cn.scale)))
0682 
0683         return cn
0684 
0685 
0686 
0687 
0688     def serialize(self, suppress_identity=False):
0689         """
0690         Array is sized for a complete tree, empty slots stay all zero
0691         """
0692         if not self.is_root: self.analyse()
0693         buf = np.zeros((self.totnodes,self.NJ,self.NK), dtype=np.float32 )
0694 
0695         transforms = []
0696         planes = []
0697 
0698         def serialize_r(node, idx): 
0699             """
0700             :param node:
0701             :param idx: 0-based complete binary tree index, left:2*idx+1, right:2*idx+2 
0702             """
0703             trs = node.transform  
0704             if trs is None and suppress_identity == False:
0705                 trs = np.eye(4, dtype=np.float32)  
0706                 # make sure root node always has a transform, incase of global placement 
0707                 # hmm root node is just an op-node it doesnt matter, need transform slots for all primitives 
0708             pass
0709 
0710             if trs is None:
0711                 itransform = 0 
0712             else:
0713                 itransform = len(transforms) + 1  # 1-based index pointing to the transform
0714                 transforms.append(trs)
0715             pass
0716             log.debug(" itransform : %s " % itransform )
0717 
0718             node_planes = node.planes
0719             if len(node_planes) == 0:
0720                 planeIdx = 0
0721                 planeNum = 0
0722             else:
0723                 planeIdx = len(planes) + 1   # 1-based index pointing to the first plane for the node
0724                 planeNum = len(node_planes)
0725                 planes.extend(node_planes)
0726             pass 
0727             log.debug("serialize_r idx %3d itransform %2d planeIdx %2d " % (idx, itransform, planeIdx))
0728 
0729             buf[idx] = node.as_array(itransform, planeIdx, planeNum)
0730 
0731             if node.left is not None and node.right is not None:
0732                 serialize_r( node.left,  2*idx+1)
0733                 serialize_r( node.right, 2*idx+2)
0734             pass
0735         pass
0736 
0737         serialize_r(self, 0)
0738 
0739         tbuf = np.vstack(transforms).reshape(-1,4,4) if len(transforms) > 0 else None 
0740         pbuf = np.vstack(planes).reshape(-1,4) if len(planes) > 0 else None
0741 
0742         log.debug("serialized CSG of height %2d into buf with %3d nodes, %3d transforms, %3d planes, meta %r " % (self.height, len(buf), len(transforms), len(planes), self.meta ))  
0743         assert tbuf is not None
0744 
0745         return buf, tbuf, pbuf
0746 
0747 
0748 
0749     def save_nodemeta(self, treedir):
0750         def save_nodemeta_r(node, idx):
0751             nodemetapath = self.metapath(treedir,idx)
0752             nodemeta = {}
0753             nodemeta.update(node.meta)
0754             nodemeta.update(idx=idx)
0755 
0756             dir_ = os.path.dirname(nodemetapath)
0757             if not os.path.exists(dir_):
0758                 os.makedirs(dir_)
0759             pass 
0760             log.debug("write nodemeta to %s %r " % (nodemetapath, nodemeta)  )
0761             json.dump(nodemeta,open(nodemetapath,"w"))
0762 
0763             if node.left is not None and node.right is not None:
0764                 save_nodemeta_r(node.left, 2*idx+1)
0765                 save_nodemeta_r(node.right, 2*idx+2)
0766             pass
0767         pass
0768         save_nodemeta_r(self,0)
0769 
0770     def save_src(self, treedir):
0771         """
0772         Persist src verts and faces from ConvexPolyhedronSrc for use by cfg4 CMaker::ConvertPrimitive 
0773         """
0774         if len(self.faces) == 0: return
0775         assert len(self.faces) == len(self.planes)
0776         assert len(self.verts) > 0
0777 
0778         facepath = self.facepath(treedir)
0779         vertpath = self.vertpath(treedir)
0780 
0781         np.save(facepath, self.faces)
0782         np.save(vertpath, self.verts)
0783 
0784 
0785     def save(self, treedir, soIdx=0, lvIdx=0):
0786         if not os.path.exists(treedir):
0787             os.makedirs(treedir)
0788         pass
0789 
0790         nodebuf, tranbuf, planebuf = self.serialize() 
0791 
0792         metapath = self.metapath(treedir)
0793 
0794         log.debug("write treemeta to %s %r  " % (metapath,self.meta)  )
0795         json.dump(self.meta,open(metapath,"w"))
0796 
0797         self.save_nodemeta(treedir)
0798         self.save_src(treedir)
0799 
0800         lvIdx_t = os.path.basename(treedir)
0801 
0802         dir_matches_lvIdx = int(lvIdx_t) == lvIdx
0803         if not dir_matches_lvIdx:
0804             log.warning("basename of treedir %s does not match the lvIdx %d " % ( lvIdx_t , lvIdx )) 
0805         pass
0806         if not lvIdx == 0:      ## testing uses lvIdx 0 
0807             assert dir_matches_lvIdx
0808         pass
0809 
0810         tboolpath = self.tboolpath(treedir, lvIdx)
0811         height = self.height 
0812 
0813         log.info(" soIdx %d lvIdx %d height %d " % ( soIdx, lvIdx, height ))
0814 
0815         idxbuf = np.array([[0, soIdx, lvIdx, height]], dtype=np.uint32) 
0816 
0817         self.write_tbool(lvIdx, tboolpath)
0818 
0819         nntpath = self.nntpath(treedir, lvIdx)
0820         self.write_NNodeTest(lvIdx, nntpath)
0821 
0822         nodepath = self.nodepath(treedir)
0823         np.save(nodepath, nodebuf)
0824         pass
0825         if tranbuf is not None:
0826             tranpath = self.tranpath(treedir)
0827             np.save(tranpath, tranbuf)
0828         pass
0829         if planebuf is not None:
0830             planepath = self.planepath(treedir)
0831             np.save(planepath, planebuf)
0832         pass
0833         if idxbuf is not None:
0834             idxpath = self.idxpath(treedir)
0835             log.info("save to idxpath %s " % idxpath)
0836             np.save(idxpath, idxbuf)
0837         pass
0838 
0839 
0840     stream = property(lambda self:self.save(sys.stdout))
0841 
0842     
0843     ## these paths must match those in NCSGData::MakeSPECS
0844     @classmethod
0845     def tranpath(cls, treedir):
0846         return os.path.join(treedir,"srctransforms.npy") 
0847     @classmethod
0848     def planepath(cls, treedir):
0849         return os.path.join(treedir,"srcplanes.npy") 
0850     @classmethod
0851     def idxpath(cls, treedir):
0852         return os.path.join(treedir,"srcidx.npy") 
0853     @classmethod
0854     def vertpath(cls, treedir):
0855         return os.path.join(treedir,"srcverts.npy") 
0856     @classmethod
0857     def facepath(cls, treedir):
0858         return os.path.join(treedir,"srcfaces.npy") 
0859     @classmethod
0860     def nodepath(cls, treedir):
0861         return os.path.join(treedir,"srcnodes.npy") 
0862 
0863 
0864     # Formerly must match NCSG::TREE_META NCSG::NODE_META
0865     # TREE_META = "meta.json"
0866     # NODE_META = "nodemeta.json"
0867     #
0868     # Now must match NPYMeta::META NPYMeta::ITEM_META  
0869     META = "meta.json"
0870     ITEM_META = "item_meta.json"
0871 
0872     @classmethod
0873     def metapath(cls, treedir, idx=-1):
0874         return os.path.join(treedir,cls.META) if idx == -1 else os.path.join(treedir,str(idx),cls.ITEM_META)
0875 
0876     @classmethod
0877     def tboolpath(cls, treedir, name):
0878         return os.path.join(treedir,"tbool%s.bash" % name) 
0879     @classmethod
0880     def nntpath(cls, treedir, name):
0881         return os.path.join(treedir,"NNodeTest_%s.cc" % name) 
0882 
0883 
0884     @classmethod
0885     def load(cls, treedir):
0886         tree = cls.deserialize(treedir) 
0887         log.info("load %s DONE -> %r " % (treedir, tree) )
0888         return tree
0889 
0890     @classmethod
0891     def deserialize(cls, treedir):
0892         assert os.path.exists(treedir), treedir
0893         log.info("load %s " % (treedir) )
0894          
0895         nodepath = cls.nodepath(treedir)
0896         metapath = cls.metapath(treedir)
0897         tranpath = cls.tranpath(treedir)
0898         planepath = cls.planepath(treedir)
0899 
0900         nodebuf = np.load(nodepath) 
0901         tranbuf = np.load(tranpath) if os.path.exists(tranpath) else None
0902         planebuf = np.load(planepath) if os.path.exists(planepath) else None
0903 
0904         totnodes = len(nodebuf)
0905         try:
0906             height = TREE_EXPECTED.index(totnodes)
0907         except ValueError:
0908             log.fatal("invalid serialization of length %d not in expected %r " % (totnodes,TREE_EXPECTED))
0909             assert 0
0910 
0911         def deserialize_r(buf, idx):
0912             node = cls.from_array(buf[idx]) if idx < len(buf) else None
0913 
0914             if node is not None and node.itransform is not None and node.itransform > 0:
0915                 assert tranbuf is not None and node.itransform - 1 < len(tranbuf)  
0916                 node.transform = tranbuf[node.itransform - 1]
0917             pass
0918                
0919             if node is not None and node.iplane is not None and node.iplane > 0:
0920                 assert planebuf is not None and node.iplane - 1 < len(planebuf) 
0921                 assert node.nplane > 3 and node.iplane - 1 + node.nplane <= len(planebuf)
0922                 node.planes = planebuf[node.iplane-1:node.iplane-1+node.nplane]
0923             pass
0924  
0925             if node is not None:
0926                 node.left  = deserialize_r(buf, 2*idx+1)
0927                 node.right = deserialize_r(buf, 2*idx+2)
0928             pass
0929             return node  
0930         pass
0931         root = deserialize_r(nodebuf, 0)
0932         root.totnodes = totnodes
0933         root.height = height 
0934         return root
0935 
0936 
0937 
0938     def __init__(self, typ_, name="", left=None, right=None, param=None, param1=None, param2=None, param3=None, boundary="", complement=False, translate=None, rotate=None, scale=None,  **kwa):
0939         if type(typ_) is str:
0940             typ = self.fromdesc(typ_)
0941         else:
0942             typ = typ_  
0943         pass
0944 
0945         type_ok = type(typ) is int and typ > -1 
0946         if not type_ok:
0947             log.fatal("entered CSG type is invalid : you probably beed to update python enums with : sysrap-;sysrap-csg-generate ")
0948         pass
0949         assert type_ok, (typ_, typ, type(typ))
0950 
0951         self.typ = typ
0952         self.name = name   
0953         self.left = left
0954         self.right = right
0955         self.parent = None
0956 
0957         self.param = param
0958         self.param1 = param1
0959         self.param2 = param2
0960         self.param3 = param3
0961 
0962         if len(boundary) == 0 and getattr(self.__class__,'boundary',None) != None:
0963             boundary = self.__class__.boundary  
0964             log.debug("using defaulted CSG.boundary %s " % boundary )
0965         pass
0966         self.boundary = boundary
0967 
0968         self.translate = translate
0969         self.rotate = rotate
0970         self.scale = scale
0971         self._transform = None
0972         self.complement = complement
0973         self.balance_disabled = False
0974 
0975         self.planes = []
0976 
0977         # verts and faces for persisting ConvexPolyhedronSrc metadata required for the cfg4 CMaker::ConvertPrimitive
0978         self.verts = []
0979         self.faces = []
0980 
0981         # class kwa defaults are overidden by instance kwa 
0982         meta = {} 
0983         meta.update( getattr(self.__class__,'kwa', {}))
0984         meta.update(kwa)
0985         pass
0986         self.meta = meta
0987 
0988 
0989     def _get_name(self):
0990         """When no name is given a name based on typ is returned, but not persisted, to avoid becoming stale on changing typ"""
0991         noname = self._name is None or len(self._name) == 0
0992         return self.desc(self.typ) if noname else self._name
0993     def _set_name(self, name):
0994         self._name = name
0995     name = property(_get_name, _set_name)
0996 
0997     def _get_translate(self):
0998         return self._translate 
0999     def _set_translate(self, s):
1000         if s is None: s="0,0,0"
1001         self._translate = from_arg_(s) 
1002     translate = property(_get_translate, _set_translate)
1003 
1004     def _get_rotate(self):
1005         return self._rotate
1006     def _set_rotate(self, s):
1007         if s is None: s="0,0,1,0"
1008         self._rotate = from_arg_(s)
1009     rotate = property(_get_rotate, _set_rotate)
1010 
1011     def _get_scale(self):
1012         return self._scale
1013     def _set_scale(self, s):
1014         if s is None: s=[1,1,1]
1015         self._scale = from_arg_(s)
1016     scale = property(_get_scale, _set_scale)
1017 
1018     def _get_transform(self):
1019         if self._transform is None: 
1020             self._transform = make_trs(self._translate, self._rotate, self._scale ) 
1021         return self._transform
1022     def _set_transform(self, trs):
1023         self._transform = from_arg_(trs)
1024     transform = property(_get_transform, _set_transform)
1025 
1026     def _get_param(self):
1027         return self._param
1028     def _set_param(self, v):
1029         if self.is_primitive and v is None: v = [0,0,0,0]
1030         self._param = np.asarray(v, dtype=np.float32) if v is not None else None
1031     param = property(_get_param, _set_param)
1032 
1033     def _get_param1(self):
1034         return self._param1
1035     def _set_param1(self, v):
1036         if self.is_primitive and v is None: v = [0,0,0,0]
1037         self._param1 = np.asarray(v, dtype=np.float32) if v is not None else None
1038     param1 = property(_get_param1, _set_param1)
1039 
1040     def _get_param2(self):
1041         return self._param2
1042     def _set_param2(self, v):
1043         if self.is_primitive and v is None: v = [0,0,0,0]
1044         self._param2 = np.asarray(v, dtype=np.float32) if v is not None else None
1045     param2 = property(_get_param2, _set_param2)
1046 
1047     def _get_param3(self):
1048         return self._param3
1049     def _set_param3(self, v):
1050         if self.is_primitive and v is None: v = [0,0,0,0]
1051         self._param3 = np.asarray(v, dtype=np.float32) if v is not None else None
1052     param3 = property(_get_param3, _set_param3)
1053 
1054 
1055 
1056     def as_array(self, itransform=0, planeIdx=0, planeNum=0):
1057         """
1058         Both primitive and internal nodes:
1059 
1060         * q2.u.w : CSG type code eg CSG_UNION, CSG_DIFFERENCE, CSG_INTERSECTION, CSG_SPHERE, CSG_BOX, ... 
1061         * q3.u.w : 1-based transform index, 0 for None
1062 
1063         Primitive nodes only:
1064 
1065         * q0 : 4*float parameters eg center and radius for sphere
1066 
1067         """
1068         arr = np.zeros( (self.NJ, self.NK), dtype=np.float32 )
1069        
1070         if self.param is not None:  # avoid gibberish in buffer
1071             arr[Q0] = self.param
1072         pass
1073         if self.param1 is not None:  
1074             arr[Q1] = self.param1
1075         pass
1076         if self.param2 is not None:  
1077             arr[Q2] = self.param2
1078         pass
1079         if self.param3 is not None:  
1080             arr[Q3] = self.param3
1081         pass
1082 
1083         #if self.transform is not None:
1084         if True:
1085             assert itransform > 0, itransform  # 1-based transform index
1086             arr.view(np.uint32)[Q3,W] = itransform 
1087         pass
1088 
1089         if self.complement:
1090             # view as float in order to set signbit 0x80000000
1091             # do via float as this works even when the value is integer zero yielding negative zero
1092             # AND with 0x7fffffff to recover the transform idx
1093             np.copysign(arr.view(np.float32)[Q3,W:W+1], -1. , arr.view(np.float32)[Q3,W:W+1] )  
1094         pass
1095 
1096         if len(self.planes) > 0:
1097             assert planeIdx > 0 and planeNum > 3, (planeIdx, planeNum)  # 1-based plane index
1098             arr.view(np.uint32)[Q0,X] = planeIdx   # cf NNode::planeIdx
1099             arr.view(np.uint32)[Q0,Y] = planeNum   # cf NNode::planeNum
1100         pass
1101 
1102         arr.view(np.uint32)[Q2,W] = self.typ
1103 
1104         return arr
1105 
1106     @classmethod
1107     def from_array(cls, arr):
1108         """
1109         Huh: looks incomplete, not loading params
1110         """ 
1111         typ = int(arr.view(np.uint32)[Q2,W])
1112         itransform = int(arr.view(np.uint32)[Q3,W]) if typ < cls.SPHERE else 0 
1113         complement = np.signbit( arr.view(np.float32)[Q3,W:W+1] )
1114 
1115         if typ in cls.CONVEX_POLYHEDRA:
1116             iplane = int(arr.view(np.uint32)[Q0,X])  
1117             nplane = int(arr.view(np.uint32)[Q0,Y]) 
1118         else:
1119             iplane = 0 
1120             nplane = 0 
1121         pass 
1122 
1123         log.info("CSG.from_array typ %d %s itransform %d iplane %d nplane %d " % (typ, cls.desc(typ), itransform, iplane, nplane) )
1124 
1125         n = cls(typ) if typ > 0 else None
1126         if n is not None:
1127             n.itransform = itransform if itransform > 0 else None
1128             n.iplane = iplane if iplane > 0 else None
1129             n.nplane = nplane if nplane > 0 else None
1130             n.complement = complement 
1131         pass
1132         return n 
1133 
1134     def dump(self, msg="CSG.dump", detailed=False):
1135         self.analyse()
1136         log.info(msg + " name:%s" % self.name)
1137         sys.stderr.write("%s" % repr(self) + "\n")
1138         if detailed:
1139             self.Dump(self)
1140         pass
1141         sys.stderr.write("\n%s\n" % self.txt)
1142 
1143 
1144 
1145 
1146 
1147     @classmethod 
1148     def Dump(cls, node, depth=0):
1149         indent = "   " * depth    
1150 
1151         label = node.label(indent)
1152         content = node.content()
1153 
1154         sys.stderr.write( "%-50s : %s \n" % (label, content))
1155 
1156         if node.left and node.right:
1157             cls.Dump(node.left, depth+1)
1158             cls.Dump(node.right, depth+1)
1159         pass
1160 
1161     def label(self, indent=""):
1162         return "%s %s;%s " % (indent, self.desc(self.typ),self.name )
1163 
1164     def content_operator(self, lang="py"):
1165         fmt = "left=%s, right=%s" if lang == "py" else "&%s, &%s" 
1166         return fmt % ( self.left.alabel, self.right.alabel )  
1167 
1168     def content_operator_parenting(self, lang="py"):
1169         if lang == "py":
1170             return None
1171         pass
1172         return "%s.parent = &%s ; %s.parent = &%s ; " % (self.left.alabel, self.alabel, self.right.alabel, self.alabel )
1173 
1174 
1175     @classmethod
1176     def num_param_quads(cls, typ):
1177         npq = None
1178         if typ in [cls.CONVEXPOLYHEDRON, cls.TRAPEZOID, cls.SEGMENT, cls.UNDEFINED]:
1179             npq = 0
1180         elif typ in [cls.SPHERE, cls.CONE, cls.BOX3, cls.BOX, cls.PLANE, cls.TORUS, cls.HYPERBOLOID]:
1181             npq = 1
1182         elif typ in [cls.ZSPHERE, cls.CYLINDER, cls.DISC, cls.SLAB, cls.CUBIC]:
1183             npq = 2 
1184         else:
1185             assert 0, "add num_param_quads for typ %s " % cls.desc(typ) 
1186         pass
1187         return npq 
1188 
1189     def content_param(self, lang="py"):
1190         if lang == "cpp" and self.num_param_quads(self.typ) == 0: 
1191             return None 
1192 
1193         if self.param is not None:
1194             param = to_codeline(self.param, "param" if lang == "py" else None, lang=lang) 
1195         else:
1196             param = None
1197         pass
1198         return param 
1199 
1200     def content_param1(self, lang="py"):
1201         if lang == "cpp" and self.num_param_quads(self.typ) < 2:   
1202             return None 
1203 
1204         if self.param1 is not None:
1205             param1 = to_codeline(self.param1, "param1" if lang == "py" else None, lang=lang) 
1206         else:
1207             param1 = None
1208         pass
1209         return param1
1210 
1211     def content_complement(self, lang="py"):
1212         if self.complement:
1213             if lang == "py":
1214                 complement = "complement = True"
1215             else:
1216                 complement = None 
1217             pass
1218         else:
1219             complement = None 
1220         pass
1221         return complement
1222 
1223     def content_complement_extras(self, lang="py"):
1224         if self.complement:
1225             if lang == "py":
1226                 complement = None
1227             else:
1228                 complement = "%s.complement = true" % self.alabel
1229             pass
1230         else:
1231             complement = None 
1232         pass
1233         return complement
1234 
1235 
1236     def content_transform(self, lang="py"):
1237         if lang == "py":
1238             line = to_codeline(self.transform, "%s.transform" % self.alabel, lang=lang )  
1239         else:
1240             line = to_codeline(self.transform, "%s.transform" % self.alabel, lang=lang )
1241         pass
1242         return line
1243 
1244     def content_type(self, lang="py"):
1245         if lang == "py":
1246             return ""
1247         pass
1248         if self.typ in [self.BOX3]:
1249             type_ = "nbox" 
1250         elif self.typ in [self.TRAPEZOID,self.SEGMENT,self.CONVEXPOLYHEDRON]:
1251             type_ = "nconvexpolyhedron" 
1252         else:
1253             type_ = "n%s" % self.desc(self.typ)
1254         pass
1255         return type_
1256 
1257     def content_maketype(self, lang="py"):
1258         if lang == "py":
1259             return self.desc(self.typ)
1260         pass
1261         if self.typ in [self.TRAPEZOID,self.SEGMENT,self.CONVEXPOLYHEDRON]:
1262             maketype_ = "convexpolyhedron" 
1263         else:
1264             maketype_ = "%s" % self.desc(self.typ)
1265         pass
1266         return maketype_
1267 
1268 
1269     def content_primitive(self, lang="py"):
1270         param = self.content_param(lang)
1271         param1 = self.content_param1(lang)
1272         complement = self.content_complement(lang)
1273         return ",".join(filter(None,[param,param1,complement]))
1274 
1275     def content(self, lang="py"):
1276         extras = []  
1277         if self.is_primitive:
1278             content_ = self.content_primitive(lang=lang)
1279             complement_ = self.content_complement_extras(lang=lang)
1280             extras.extend(filter(None,[complement_]))
1281         else:
1282             content_ = self.content_operator(lang=lang) 
1283             parenting_ = self.content_operator_parenting(lang=lang) 
1284             extras.extend(filter(None,[parenting_]))
1285             assert not self.complement
1286         pass
1287 
1288         extras_ = " ; ".join(extras)
1289 
1290         if len(extras) > 0:
1291             extras_ += " ; "
1292         pass
1293 
1294         if lang == "py":
1295             code = "%s = CSG(\"%s\", %s)" % (self.alabel, self.desc(self.typ), content_)
1296         else:
1297             type_ = self.content_type(lang)
1298             maketype_ = self.content_maketype(lang)
1299             code = "%s %s = make_%s(%s) ; %s.label = \"%s\" ; %s  " % ( type_, self.alabel, maketype_, content_, self.alabel, self.alabel, extras_ )
1300         pass
1301         return code
1302 
1303 
1304     def as_python_planes(self, node): 
1305         assert len(node.planes) > 0
1306         lines = []
1307         lines.append("%s.planes = np.zeros( (%d,4), dtype=np.float32)" % (node.alabel, len(node.planes)))
1308         for i in range(len(node.planes)):
1309             line = to_pyline(node.planes[i], "%s.planes[%d]" % (node.alabel, i))
1310             lines.append(line)
1311         pass
1312         lines.append("# convexpolyhedron are defined by planes and require manual aabbox definition")
1313         lines.append(to_pyline(node.param2[:3], "%s.param2[:3]" % node.alabel))
1314         lines.append(to_pyline(node.param3[:3], "%s.param3[:3]" % node.alabel))
1315         lines.append("")
1316         return lines
1317 
1318 
1319     def as_cpp_planes(self, node): 
1320         assert len(node.planes) > 0
1321         lines = []
1322         lines.append("std::vector<glm::vec4> planes ; ")
1323         for i in range(len(node.planes)):
1324             line = "planes.push_back(glm::vec4(%s));" % to_cpplist(node.planes[i])
1325             lines.append(line)
1326         pass
1327         lines.append("// convexpolyhedron are defined by planes and require manual aabbox definition")
1328         lines.append("//  npq : %s " % self.num_param_quads(node.typ) )
1329         lines.append("nbbox bb = make_bbox( %s , %s ) ; " % ( to_cpplist(node.param2[:3]), to_cpplist(node.param3[:3])))
1330         lines.append("%s.set_planes(planes);" % node.alabel)
1331         lines.append("%s.set_bbox(bb);" % node.alabel )
1332         lines.append("")
1333         return lines
1334 
1335 
1336     def as_code(self, lang="py"):
1337         lines = []
1338         def as_code_r(node):
1339             if node is None:return
1340             as_code_r(node.left) 
1341             as_code_r(node.right) 
1342 
1343             # postorder visit
1344             line = node.content(lang)
1345             lines.append(line)    
1346             if node.transform is not None:
1347                 line = node.content_transform(lang)
1348                 lines.append(line)    
1349             pass
1350 
1351             if len(node.planes) > 0:
1352                 plins = self.as_python_planes(node) if lang == "py" else self.as_cpp_planes(node)
1353                 for plin in plins:
1354                     lines.append(plin)
1355                 pass
1356             pass
1357 
1358             if not node.is_primitive:
1359                 lines.append("")
1360             pass
1361         pass
1362         as_code_r(self) 
1363         return "\n".join(lines)
1364 
1365 
1366     def as_tbool(self, name="esr"):
1367         tbf = TBoolBashFunction(name=name, root=self.alabel, body=self.as_code(lang="py")  )
1368         return str(tbf)
1369 
1370     def as_NNodeTest(self, name="esr"):
1371         nnt  = NNodeTestCPP(name=name, root=self.alabel, body=self.as_code(lang="cpp")  )
1372         return str(nnt)
1373 
1374     def dump_tbool(self, name):
1375         sys.stderr.write("\n"+self.as_tbool(name))
1376 
1377     def dump_NNodeTest(self, name):
1378         sys.stderr.write("\n"+self.as_NNodeTest(name))
1379 
1380     def write_tbool(self, name, path):
1381         open(path, "w").write("\n"+self.as_tbool(name))
1382 
1383     def write_NNodeTest(self, name, path):
1384         open(path, "w").write("\n"+self.as_NNodeTest(name))
1385 
1386 
1387     def _get_tag(self):
1388         return self.desc(self.typ)[0:2]
1389         #return self.name[0:2]
1390     tag = property(_get_tag)
1391 
1392 
1393     dsc = property(lambda self:"%s%s"%("!" if self.complement else "", self.tag ))
1394 
1395     def __repr__(self):
1396         rrep = " height:%d totnodes:%d " % (self.height, self.totnodes) if self.is_root else ""  
1397         dlr = ",".join(map(repr,filter(None,[self.left,self.right])))
1398         if len(dlr) > 0: dlr = "(%s)" % dlr 
1399         return "".join(filter(None, [self.dsc, dlr, rrep])) 
1400 
1401 
1402     def __call__(self, p):
1403         """
1404         SDF : signed distance field
1405         """
1406         if self.typ == self.SPHERE:
1407             center = self.param[:3]
1408             radius = self.param[3]
1409             pc = np.asarray(p) - center
1410             return np.sqrt(np.sum(pc*pc)) - radius 
1411         else:
1412             assert 0 
1413 
1414     is_convex_polyhedron = property(lambda self:self.typ in self.CONVEX_POLYHEDRA )
1415     is_primitive = property(lambda self:self.typ >= self.SPHERE )
1416     is_bileaf = property(lambda self:self.left is not None and self.left.is_leaf and self.right is not None and self.right.is_leaf )
1417     is_leaf = property(lambda self:self.left is None and self.right is None)
1418     is_zero = property(lambda self:self.typ == self.ZERO )
1419     is_operator = property(lambda self:self.typ in [self.UNION,self.DIFFERENCE,self.INTERSECTION])
1420     is_lrzero = property(lambda self:self.is_operator and self.left.is_zero and self.right.is_zero )      # l-zero AND r-zero
1421     is_rzero = property(lambda self:self.is_operator and not(self.left.is_zero) and self.right.is_zero )  # r-zero BUT !l-zero
1422     is_lzero = property(lambda self:self.is_operator and self.left.is_zero and not(self.right.is_zero) )  # l-zero BUT !r-zero
1423 
1424 
1425 
1426     @classmethod
1427     def Union(cls, a, b, **kwa):
1428         return cls(cls.UNION, left=a, right=b, **kwa)
1429 
1430     @classmethod
1431     def Intersection(cls, a, b, **kwa):
1432         return cls(cls.INTERSECTION, left=a, right=b, **kwa)
1433 
1434     @classmethod
1435     def Difference(cls, a, b, **kwa):
1436         return cls(cls.DIFFERENCE, left=a, right=b, **kwa)
1437 
1438 
1439     def union(self, other):
1440         return CSG(self.UNION, left=self, right=other)
1441 
1442     def subtract(self, other):
1443         return CSG(self.DIFFERENCE, left=self, right=other)
1444 
1445     def intersect(self, other):
1446         return CSG(self.INTERSECTION, left=self, right=other)
1447 
1448     def __add__(self, other):
1449         return self.union(other)
1450 
1451     def __sub__(self, other):
1452         return self.subtract(other)
1453 
1454     def __mul__(self, other):
1455         return self.intersect(other)
1456 
1457    
1458 
1459 def test_serialize_deserialize():
1460     log.info("test_serialize_deserialize")
1461     container = CSG("box", param=[0,0,0,1000], boundary="Rock//perfectAbsorbSurface/Vacuum" )
1462    
1463     s = CSG("sphere")
1464     b = CSG("box", translate="0,0,20", rotate="0,0,1,45", scale="1,2,3" )
1465     sub = CSG("union", left=s, right=b, boundary="Vacuum///GlassShottF2", hello="world")
1466 
1467     trees0 = [container, sub]
1468 
1469     base = "$TMP/csg_py"
1470     CSG.Serialize(trees0, base )
1471     trees1 = CSG.Deserialize(base)
1472 
1473     assert len(trees1) == len(trees0)
1474 
1475     for i in range(len(trees1)):
1476         assert np.all( trees0[i].transform == trees1[i].transform )
1477 
1478     
1479 def test_analyse():
1480     log.info("test_analyse")
1481     s = CSG("sphere")
1482     b = CSG("box")
1483     sub = CSG("union", left=s, right=b)
1484     sub.analyse()
1485 
1486 
1487 def test_trapezoid():
1488     log.info("test_trapezoid")
1489 
1490     tr = CSG("trapezoid")
1491     tr.planes = CSG.CubePlanes(0.5)
1492     tr.boundary = "dummy"   
1493  
1494     trees0 = [tr]
1495     base = "$TMP/test_trapezoid"
1496 
1497     CSG.Serialize(trees0, base )
1498     trees1 = CSG.Deserialize(base)
1499 
1500     assert len(trees1) == len(trees0)
1501     tr1 = trees1[0] 
1502 
1503     assert np.all( tr1.planes == tr.planes )
1504 
1505 
1506 
1507 def test_positivize():
1508     log.info("test_positivize")
1509 
1510     a = CSG("sphere", param=[0,0,-50,100] ) 
1511     b = CSG("sphere", param=[0,0, 50,100] ) 
1512     c = CSG("box", param=[0,0, 50,100] ) 
1513     d = CSG("box", param=[0,0, 0,100] ) 
1514     e = CSG("box", param=[0,0, 0,100] ) 
1515 
1516     ab = CSG("union", left=a, right=b )
1517     de = CSG("difference", left=d, right=e )
1518     cde = CSG("difference", left=c, right=de )
1519 
1520     abcde = CSG("intersection", left=ab, right=cde )
1521 
1522     abcde.analyse()
1523     print("original\n\n%s" % abcde.txt)
1524     print("operators: " + " ".join(map(CSG.desc, abcde.operators_())))
1525 
1526     abcde.positivize() 
1527     print("positivize\n\n%s" % abcde.txt)
1528     print("operators: " + " ".join(map(CSG.desc, abcde.operators_())))
1529 
1530 
1531 
1532 
1533 
1534 def test_positivize_2():
1535     """
1536     Example from p4 of Rossignac Blist paper 
1537 
1538     :: 
1539 
1540         INFO:__main__:test_positivize_2
1541         original
1542 
1543                              di                            
1544              in                                      in    
1545           a          un              un                   g
1546                   b       c       d          di            
1547                                           e       f        
1548         operators: union intersection difference
1549         positivize
1550 
1551                              in                            
1552              in                                      un    
1553           a          un              in                  !g
1554                   b       c      !d          un            
1555                                          !e       f        
1556         operators: union intersection
1557 
1558     """
1559     log.info("test_positivize_2")
1560 
1561     a = CSG("sphere", param=[0,0,-50,100], name="a") 
1562     b = CSG("sphere", param=[0,0, 50,100], name="b") 
1563     c = CSG("box", param=[0,0, 50,100], name="c") 
1564     d = CSG("box", param=[0,0, 0,100], name="d") 
1565     e = CSG("box", param=[0,0, 0,100], name="e") 
1566     f = CSG("box", param=[0,0, 0,100], name="f") 
1567     g = CSG("box", param=[0,0, 0,100], name="g") 
1568 
1569     bc = CSG.Union(b,c)
1570     abc = CSG.Intersection(a, bc)
1571     ef = CSG.Difference(e,f)
1572     def_ = CSG.Union(d, ef)
1573     defg = CSG.Intersection(def_, g)
1574     abcdefg = CSG.Difference(abc, defg)
1575 
1576     root = abcdefg
1577 
1578 
1579     root.analyse()
1580     print("original\n\n%s" % root.txt)
1581     print("operators: " + " ".join(map(CSG.desc, root.operators_())))
1582 
1583     root.positivize() 
1584     print("positivize\n\n%s" % root.txt)
1585     print("operators: " + " ".join(map(CSG.desc, root.operators_())))
1586 
1587 
1588 
1589 
1590 
1591 
1592 def test_subdepth():
1593     log.info("test_subdepth")
1594  
1595     sprim = "sphere box cone zsphere cylinder trapezoid"
1596     primitives = list(map(CSG, sprim.split()))
1597 
1598     sp,bo,co,zs,cy,tr = primitives
1599 
1600     root = sp - bo - co - zs - cy - tr
1601 
1602     root.analyse()    
1603     print(root.txt)
1604 
1605 
1606 
1607 def test_balance():
1608     log.info("test_balance")
1609  
1610     sprim = "sphere box cone zsphere cylinder trapezoid"
1611     primitives = list(map(CSG, sprim.split()))
1612 
1613     sp,bo,co,zs,cy,tr = primitives
1614 
1615     #root = sp - bo - co - zs - cy - tr
1616     #root = sp * bo * co * zs * cy * tr
1617     root = sp + bo + co + zs + cy + tr
1618 
1619     root.analyse()    
1620     print(root.txt)
1621 
1622     root.balance()
1623 
1624 
1625 def test_content_generate():
1626     log.info("test_content_generate")
1627 
1628     a = CSG("sphere", param=[0,0,-50,100], name="a") 
1629     b = CSG("sphere", param=[0,0,-50,  99], name="b") 
1630     b.transform = [[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]] 
1631 
1632     ab = a - b 
1633 
1634     ab.analyse()
1635     obj = ab 
1636 
1637     for lang in "py cpp".split():
1638         print(lang)
1639         #print(obj.content(lang))
1640         print(obj.as_code(lang))
1641 
1642 
1643 
1644 def test_load(lvidx):
1645     idfold = os.environ['OPTICKS_IDFOLD']   # from opticks_main
1646     base = os.path.join(idfold, "extras") 
1647     tree = CSG.load(CSG.treedir(base, lvidx))      
1648     return tree 
1649 
1650 
1651 
1652 if __name__ == '__main__':
1653     pass
1654 
1655     from opticks.ana.base import opticks_main
1656     args = opticks_main()
1657 
1658     
1659 
1660 
1661     #test_serialize_deserialize()
1662     #test_analyse()
1663     #test_trapezoid()
1664 
1665     test_positivize()
1666     #test_positivize_2()
1667     #test_subdepth()
1668 
1669     #test_balance()
1670     #test_content_generate()
1671 
1672 
1673     #RSU = 56 
1674     #ESR = 57 
1675     #tree = test_load(ESR)
1676     #tree.analyse()
1677     #print(tree.txt)
1678 
1679    
1680 
1681