File indexing completed on 2026-04-09 07:48:51
0001
0002
0003
0004
0005
0006
0007
0008
0009
0010
0011
0012
0013
0014
0015
0016
0017
0018
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
0033 from opticks.sysrap.OpticksCSG import CSG_
0034 from opticks.analytic.glm import make_trs, to_pyline, to_codeline, to_cpplist, make_scale
0035
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))
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
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
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
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
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
0204 if node.typ in [CSG_.INTERSECTION, CSG_.UNION]:
0205
0206 if negate:
0207 node.typ = deMorganSwap.get(node.typ, None)
0208 assert node.typ
0209 left_negate = True
0210 right_negate = True
0211 else:
0212 left_negate = False
0213 right_negate = False
0214 pass
0215 elif node.typ == CSG_.DIFFERENCE:
0216
0217 if negate:
0218 node.typ = CSG_.UNION
0219 left_negate = True
0220 right_negate = False
0221 else:
0222 node.typ = CSG_.INTERSECTION
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
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
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
0306 ops = self.operators_(minsubdepth=subdepth+1)
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
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
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
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))
0485 cls.SaveMeta(base, csgmeta)
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)
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
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
0707
0708 pass
0709
0710 if trs is None:
0711 itransform = 0
0712 else:
0713 itransform = len(transforms) + 1
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
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:
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
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
0865
0866
0867
0868
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
0978 self.verts = []
0979 self.faces = []
0980
0981
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:
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
1084 if True:
1085 assert itransform > 0, itransform
1086 arr.view(np.uint32)[Q3,W] = itransform
1087 pass
1088
1089 if self.complement:
1090
1091
1092
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)
1098 arr.view(np.uint32)[Q0,X] = planeIdx
1099 arr.view(np.uint32)[Q0,Y] = 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
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
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 )
1421 is_rzero = property(lambda self:self.is_operator and not(self.left.is_zero) and self.right.is_zero )
1422 is_lzero = property(lambda self:self.is_operator and self.left.is_zero and not(self.right.is_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
1616
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
1640 print(obj.as_code(lang))
1641
1642
1643
1644 def test_load(lvidx):
1645 idfold = os.environ['OPTICKS_IDFOLD']
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
1662
1663
1664
1665 test_positivize()
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681