X3DReader.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924
  1. # Contributed by Seva Alekseyev <sevaa@nih.gov> with National Institutes of Health, 2016
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from math import pi, sin, cos, sqrt
  4. import numpy
  5. from UM.Job import Job
  6. from UM.Logger import Logger
  7. from UM.Math.Matrix import Matrix
  8. from UM.Math.Vector import Vector
  9. from UM.Mesh.MeshBuilder import MeshBuilder
  10. from UM.Mesh.MeshReader import MeshReader
  11. from cura.Scene.CuraSceneNode import CuraSceneNode as SceneNode
  12. MYPY = False
  13. try:
  14. if not MYPY:
  15. import xml.etree.cElementTree as ET
  16. except ImportError:
  17. import xml.etree.ElementTree as ET
  18. # TODO: preserve the structure of scenes that contain several objects
  19. # Use CADPart, for example, to distinguish between separate objects
  20. DEFAULT_SUBDIV = 16 # Default subdivision factor for spheres, cones, and cylinders
  21. EPSILON = 0.000001
  22. class Shape:
  23. # Expects verts in MeshBuilder-ready format, as a n by 3 mdarray
  24. # with vertices stored in rows
  25. def __init__(self, verts, faces, index_base, name):
  26. self.verts = verts
  27. self.faces = faces
  28. # Those are here for debugging purposes only
  29. self.index_base = index_base
  30. self.name = name
  31. class X3DReader(MeshReader):
  32. def __init__(self, application):
  33. super().__init__(application)
  34. self._supported_extensions = [".x3d"]
  35. self._namespaces = {}
  36. # Main entry point
  37. # Reads the file, returns a SceneNode (possibly with nested ones), or None
  38. def _read(self, file_name):
  39. try:
  40. self.defs = {}
  41. self.shapes = []
  42. tree = ET.parse(file_name)
  43. xml_root = tree.getroot()
  44. if xml_root.tag != "X3D":
  45. return None
  46. scale = 1000 # Default X3D unit it one meter, while Cura's is one millimeters
  47. if xml_root[0].tag == "head":
  48. for head_node in xml_root[0]:
  49. if head_node.tag == "unit" and head_node.attrib.get("category") == "length":
  50. scale *= float(head_node.attrib["conversionFactor"])
  51. break
  52. xml_scene = xml_root[1]
  53. else:
  54. xml_scene = xml_root[0]
  55. if xml_scene.tag != "Scene":
  56. return None
  57. self.transform = Matrix()
  58. self.transform.setByScaleFactor(scale)
  59. self.index_base = 0
  60. # Traverse the scene tree, populate the shapes list
  61. self.processChildNodes(xml_scene)
  62. if self.shapes:
  63. builder = MeshBuilder()
  64. builder.setVertices(numpy.concatenate([shape.verts for shape in self.shapes]))
  65. builder.setIndices(numpy.concatenate([shape.faces for shape in self.shapes]))
  66. builder.calculateNormals()
  67. builder.setFileName(file_name)
  68. mesh_data = builder.build()
  69. # Manually try and get the extents of the mesh_data. This should prevent nasty NaN issues from
  70. # leaving the reader.
  71. mesh_data.getExtents()
  72. node = SceneNode()
  73. node.setMeshData(mesh_data)
  74. node.setSelectable(True)
  75. node.setName(file_name)
  76. else:
  77. return None
  78. except Exception:
  79. Logger.logException("e", "Exception in X3D reader")
  80. return None
  81. return node
  82. # ------------------------- XML tree traversal
  83. def processNode(self, xml_node):
  84. xml_node = self.resolveDefUse(xml_node)
  85. if xml_node is None:
  86. return
  87. tag = xml_node.tag
  88. if tag in ("Group", "StaticGroup", "CADAssembly", "CADFace", "CADLayer", "Collision"):
  89. self.processChildNodes(xml_node)
  90. if tag == "CADPart":
  91. self.processTransform(xml_node) # TODO: split the parts
  92. elif tag == "LOD":
  93. self.processNode(xml_node[0])
  94. elif tag == "Transform":
  95. self.processTransform(xml_node)
  96. elif tag == "Shape":
  97. self.processShape(xml_node)
  98. def processShape(self, xml_node):
  99. # Find the geometry and the appearance inside the Shape
  100. geometry = appearance = None
  101. for sub_node in xml_node:
  102. if sub_node.tag == "Appearance" and not appearance:
  103. appearance = self.resolveDefUse(sub_node)
  104. elif sub_node.tag in self.geometry_importers and not geometry:
  105. geometry = self.resolveDefUse(sub_node)
  106. # TODO: appearance is completely ignored. At least apply the material color...
  107. if not geometry is None:
  108. try:
  109. self.verts = self.faces = [] # Safeguard
  110. self.geometry_importers[geometry.tag](self, geometry)
  111. m = self.transform.getData()
  112. verts = m.dot(self.verts)[:3].transpose()
  113. self.shapes.append(Shape(verts, self.faces, self.index_base, geometry.tag))
  114. self.index_base += len(verts)
  115. except Exception:
  116. Logger.logException("e", "Exception in X3D reader while reading %s", geometry.tag)
  117. # Returns the referenced node if the node has USE, the same node otherwise.
  118. # May return None is USE points at a nonexistent node
  119. # In X3DOM, when both DEF and USE are in the same node, DEF is ignored.
  120. # Big caveat: XML element objects may evaluate to boolean False!!!
  121. # Don't ever use "if node:", use "if not node is None:" instead
  122. def resolveDefUse(self, node):
  123. USE = node.attrib.get("USE")
  124. if USE:
  125. return self.defs.get(USE, None)
  126. DEF = node.attrib.get("DEF")
  127. if DEF:
  128. self.defs[DEF] = node
  129. return node
  130. def processChildNodes(self, node):
  131. for c in node:
  132. self.processNode(c)
  133. Job.yieldThread()
  134. # Since this is a grouping node, will recurse down the tree.
  135. # According to the spec, the final transform matrix is:
  136. # T * C * R * SR * S * -SR * -C
  137. # Where SR corresponds to the rotation matrix to scaleOrientation
  138. # C and SR are rather exotic. S, slightly less so.
  139. def processTransform(self, node):
  140. rot = readRotation(node, "rotation", (0, 0, 1, 0)) # (angle, axisVactor) tuple
  141. trans = readVector(node, "translation", (0, 0, 0)) # Vector
  142. scale = readVector(node, "scale", (1, 1, 1)) # Vector
  143. center = readVector(node, "center", (0, 0, 0)) # Vector
  144. scale_orient = readRotation(node, "scaleOrientation", (0, 0, 1, 0)) # (angle, axisVactor) tuple
  145. # Store the previous transform; in Cura, the default matrix multiplication is in place
  146. prev = Matrix(self.transform.getData()) # It's deep copy, I've checked
  147. # The rest of transform manipulation will be applied in place
  148. got_center = (center.x != 0 or center.y != 0 or center.z != 0)
  149. T = self.transform
  150. if trans.x != 0 or trans.y != 0 or trans.z != 0:
  151. T.translate(trans)
  152. if got_center:
  153. T.translate(center)
  154. if rot[0] != 0:
  155. T.rotateByAxis(*rot)
  156. if scale.x != 1 or scale.y != 1 or scale.z != 1:
  157. got_scale_orient = scale_orient[0] != 0
  158. if got_scale_orient:
  159. T.rotateByAxis(*scale_orient)
  160. # No scale by vector in place operation in UM
  161. S = Matrix()
  162. S.setByScaleVector(scale)
  163. T.multiply(S)
  164. if got_scale_orient:
  165. T.rotateByAxis(-scale_orient[0], scale_orient[1])
  166. if got_center:
  167. T.translate(-center)
  168. self.processChildNodes(node)
  169. self.transform = prev
  170. # ------------------------- Geometry importers
  171. # They are supposed to fill the self.verts and self.faces arrays, the caller will do the rest
  172. # Primitives
  173. def processGeometryBox(self, node):
  174. (dx, dy, dz) = readFloatArray(node, "size", [2, 2, 2])
  175. dx /= 2
  176. dy /= 2
  177. dz /= 2
  178. self.reserveFaceAndVertexCount(12, 8)
  179. # xz plane at +y, ccw
  180. self.addVertex(dx, dy, dz)
  181. self.addVertex(-dx, dy, dz)
  182. self.addVertex(-dx, dy, -dz)
  183. self.addVertex(dx, dy, -dz)
  184. # xz plane at -y
  185. self.addVertex(dx, -dy, dz)
  186. self.addVertex(-dx, -dy, dz)
  187. self.addVertex(-dx, -dy, -dz)
  188. self.addVertex(dx, -dy, -dz)
  189. self.addQuad(0, 1, 2, 3) # +y
  190. self.addQuad(4, 0, 3, 7) # +x
  191. self.addQuad(7, 3, 2, 6) # -z
  192. self.addQuad(6, 2, 1, 5) # -x
  193. self.addQuad(5, 1, 0, 4) # +z
  194. self.addQuad(7, 6, 5, 4) # -y
  195. # The sphere is subdivided into nr rings and ns segments
  196. def processGeometrySphere(self, node):
  197. r = readFloat(node, "radius", 0.5)
  198. subdiv = readIntArray(node, "subdivision", None)
  199. if subdiv:
  200. if len(subdiv) == 1:
  201. nr = ns = subdiv[0]
  202. else:
  203. (nr, ns) = subdiv
  204. else:
  205. nr = ns = DEFAULT_SUBDIV
  206. lau = pi / nr # Unit angle of latitude (rings) for the given tesselation
  207. lou = 2 * pi / ns # Unit angle of longitude (segments)
  208. self.reserveFaceAndVertexCount(ns*(nr*2 - 2), 2 + (nr - 1)*ns)
  209. # +y and -y poles
  210. self.addVertex(0, r, 0)
  211. self.addVertex(0, -r, 0)
  212. # The non-polar vertices go from x=0, negative z plane counterclockwise -
  213. # to -x, to +z, to +x, back to -z
  214. for ring in range(1, nr):
  215. for seg in range(ns):
  216. self.addVertex(-r*sin(lou * seg) * sin(lau * ring),
  217. r*cos(lau * ring),
  218. -r*cos(lou * seg) * sin(lau * ring))
  219. vb = 2 + (nr - 2) * ns # First vertex index for the bottom cap
  220. # Faces go in order: top cap, sides, bottom cap.
  221. # Sides go by ring then by segment.
  222. # Caps
  223. # Top cap face vertices go in order: down right up
  224. # (starting from +y pole)
  225. # Bottom cap goes: up left down (starting from -y pole)
  226. for seg in range(ns):
  227. self.addTri(0, seg + 2, (seg + 1) % ns + 2)
  228. self.addTri(1, vb + (seg + 1) % ns, vb + seg)
  229. # Sides
  230. # Side face vertices go in order: down right upleft, downright up left
  231. for ring in range(nr - 2):
  232. tvb = 2 + ring * ns
  233. # First vertex index for the top edge of the ring
  234. bvb = tvb + ns
  235. # First vertex index for the bottom edge of the ring
  236. for seg in range(ns):
  237. nseg = (seg + 1) % ns
  238. self.addQuad(tvb + seg, bvb + seg, bvb + nseg, tvb + nseg)
  239. def processGeometryCone(self, node):
  240. r = readFloat(node, "bottomRadius", 1)
  241. height = readFloat(node, "height", 2)
  242. bottom = readBoolean(node, "bottom", True)
  243. side = readBoolean(node, "side", True)
  244. n = readInt(node, "subdivision", DEFAULT_SUBDIV)
  245. d = height / 2
  246. angle = 2 * pi / n
  247. self.reserveFaceAndVertexCount((n if side else 0) + (n-2 if bottom else 0), n+1)
  248. # Vertex 0 is the apex, vertices 1..n are the bottom
  249. self.addVertex(0, d, 0)
  250. for i in range(n):
  251. self.addVertex(-r * sin(angle * i), -d, -r * cos(angle * i))
  252. # Side face vertices go: up down right
  253. if side:
  254. for i in range(n):
  255. self.addTri(1 + (i + 1) % n, 0, 1 + i)
  256. if bottom:
  257. for i in range(2, n):
  258. self.addTri(1, i, i+1)
  259. def processGeometryCylinder(self, node):
  260. r = readFloat(node, "radius", 1)
  261. height = readFloat(node, "height", 2)
  262. bottom = readBoolean(node, "bottom", True)
  263. side = readBoolean(node, "side", True)
  264. top = readBoolean(node, "top", True)
  265. n = readInt(node, "subdivision", DEFAULT_SUBDIV)
  266. nn = n * 2
  267. angle = 2 * pi / n
  268. hh = height/2
  269. self.reserveFaceAndVertexCount((nn if side else 0) + (n - 2 if top else 0) + (n - 2 if bottom else 0), nn)
  270. # The seam is at x=0, z=-r, vertices go ccw -
  271. # to pos x, to neg z, to neg x, back to neg z
  272. for i in range(n):
  273. rs = -r * sin(angle * i)
  274. rc = -r * cos(angle * i)
  275. self.addVertex(rs, hh, rc)
  276. self.addVertex(rs, -hh, rc)
  277. if side:
  278. for i in range(n):
  279. ni = (i + 1) % n
  280. self.addQuad(ni * 2 + 1, ni * 2, i * 2, i * 2 + 1)
  281. for i in range(2, nn-3, 2):
  282. if top:
  283. self.addTri(0, i, i+2)
  284. if bottom:
  285. self.addTri(1, i+1, i+3)
  286. # Semi-primitives
  287. def processGeometryElevationGrid(self, node):
  288. dx = readFloat(node, "xSpacing", 1)
  289. dz = readFloat(node, "zSpacing", 1)
  290. nx = readInt(node, "xDimension", 0)
  291. nz = readInt(node, "zDimension", 0)
  292. height = readFloatArray(node, "height", False)
  293. ccw = readBoolean(node, "ccw", True)
  294. if nx <= 0 or nz <= 0 or len(height) < nx*nz:
  295. return # That's weird, the wording of the standard suggests grids with zero quads are somehow valid
  296. self.reserveFaceAndVertexCount(2*(nx-1)*(nz-1), nx*nz)
  297. for z in range(nz):
  298. for x in range(nx):
  299. self.addVertex(x * dx, height[z*nx + x], z * dz)
  300. for z in range(1, nz):
  301. for x in range(1, nx):
  302. self.addTriFlip((z - 1)*nx + x - 1, z*nx + x, (z - 1)*nx + x, ccw)
  303. self.addTriFlip((z - 1)*nx + x - 1, z*nx + x - 1, z*nx + x, ccw)
  304. def processGeometryExtrusion(self, node):
  305. ccw = readBoolean(node, "ccw", True)
  306. begin_cap = readBoolean(node, "beginCap", True)
  307. end_cap = readBoolean(node, "endCap", True)
  308. cross = readFloatArray(node, "crossSection", (1, 1, 1, -1, -1, -1, -1, 1, 1, 1))
  309. cross = [(cross[i], cross[i+1]) for i in range(0, len(cross), 2)]
  310. spine = readFloatArray(node, "spine", (0, 0, 0, 0, 1, 0))
  311. spine = [(spine[i], spine[i+1], spine[i+2]) for i in range(0, len(spine), 3)]
  312. orient = readFloatArray(node, "orientation", None)
  313. if orient:
  314. # This converts X3D's axis/angle rotation to a 3x3 numpy matrix
  315. def toRotationMatrix(rot):
  316. (x, y, z) = rot[:3]
  317. a = rot[3]
  318. s = sin(a)
  319. c = cos(a)
  320. t = 1-c
  321. return numpy.array((
  322. (x * x * t + c, x * y * t - z*s, x * z * t + y * s),
  323. (x * y * t + z*s, y * y * t + c, y * z * t - x * s),
  324. (x * z * t - y * s, y * z * t + x * s, z * z * t + c)))
  325. orient = [toRotationMatrix(orient[i:i+4]) if orient[i+3] != 0 else None for i in range(0, len(orient), 4)]
  326. scale = readFloatArray(node, "scale", None)
  327. if scale:
  328. scale = [numpy.array(((scale[i], 0, 0), (0, 1, 0), (0, 0, scale[i+1])))
  329. if scale[i] != 1 or scale[i+1] != 1 else None for i in range(0, len(scale), 2)]
  330. # Special treatment for the closed spine and cross section.
  331. # Let's save some memory by not creating identical but distinct vertices;
  332. # later we'll introduce conditional logic to link the last vertex with
  333. # the first one where necessary.
  334. crossClosed = cross[0] == cross[-1]
  335. if crossClosed:
  336. cross = cross[:-1]
  337. nc = len(cross)
  338. cross = [numpy.array((c[0], 0, c[1])) for c in cross]
  339. ncf = nc if crossClosed else nc - 1
  340. # Face count along the cross; for closed cross, it's the same as the
  341. # respective vertex count
  342. spine_closed = spine[0] == spine[-1]
  343. if spine_closed:
  344. spine = spine[:-1]
  345. ns = len(spine)
  346. spine = [Vector(*s) for s in spine]
  347. nsf = ns if spine_closed else ns - 1
  348. # This will be used for fallback, where the current spine point joins
  349. # two collinear spine segments. No need to recheck the case of the
  350. # closed spine/last-to-first point juncture; if there's an angle there,
  351. # it would kick in on the first iteration of the main loop by spine.
  352. def findFirstAngleNormal():
  353. for i in range(1, ns - 1):
  354. spt = spine[i]
  355. z = (spine[i + 1] - spt).cross(spine[i - 1] - spt)
  356. if z.length() > EPSILON:
  357. return z
  358. # All the spines are collinear. Fallback to the rotated source
  359. # XZ plane.
  360. # TODO: handle the situation where the first two spine points match
  361. if len(spine) < 2:
  362. return Vector(0, 0, 1)
  363. v = spine[1] - spine[0]
  364. orig_y = Vector(0, 1, 0)
  365. orig_z = Vector(0, 0, 1)
  366. if v.cross(orig_y).length() > EPSILON:
  367. # Spine at angle with global y - rotate the z accordingly
  368. a = v.cross(orig_y) # Axis of rotation to get to the Z
  369. (x, y, z) = a.normalized().getData()
  370. s = a.length()/v.length()
  371. c = sqrt(1-s*s)
  372. t = 1-c
  373. m = numpy.array((
  374. (x * x * t + c, x * y * t + z*s, x * z * t - y * s),
  375. (x * y * t - z*s, y * y * t + c, y * z * t + x * s),
  376. (x * z * t + y * s, y * z * t - x * s, z * z * t + c)))
  377. orig_z = Vector(*m.dot(orig_z.getData()))
  378. return orig_z
  379. self.reserveFaceAndVertexCount(2*nsf*ncf + (nc - 2 if begin_cap else 0) + (nc - 2 if end_cap else 0), ns*nc)
  380. z = None
  381. for i, spt in enumerate(spine):
  382. if (i > 0 and i < ns - 1) or spine_closed:
  383. snext = spine[(i + 1) % ns]
  384. sprev = spine[(i - 1 + ns) % ns]
  385. y = snext - sprev
  386. vnext = snext - spt
  387. vprev = sprev - spt
  388. try_z = vnext.cross(vprev)
  389. # Might be zero, then all kinds of fallback
  390. if try_z.length() > EPSILON:
  391. if z is not None and try_z.dot(z) < 0:
  392. try_z = -try_z
  393. z = try_z
  394. elif not z: # No z, and no previous z.
  395. # Look ahead, see if there's at least one point where
  396. # spines are not collinear.
  397. z = findFirstAngleNormal()
  398. elif i == 0: # And non-crossed
  399. snext = spine[i + 1]
  400. y = snext - spt
  401. z = findFirstAngleNormal()
  402. else: # last point and not crossed
  403. sprev = spine[i - 1]
  404. y = spt - sprev
  405. # If there's more than one point in the spine, z is already set.
  406. # One point in the spline is an error anyway.
  407. z = z.normalized()
  408. y = y.normalized()
  409. x = y.cross(z) # Already normalized
  410. m = numpy.array(((x.x, y.x, z.x), (x.y, y.y, z.y), (x.z, y.z, z.z)))
  411. # Columns are the unit vectors for the xz plane for the cross-section
  412. if orient:
  413. mrot = orient[i] if len(orient) > 1 else orient[0]
  414. if not mrot is None:
  415. m = m.dot(mrot) # Tested against X3DOM, the result matches, still not sure :(
  416. if scale:
  417. mscale = scale[i] if len(scale) > 1 else scale[0]
  418. if not mscale is None:
  419. m = m.dot(mscale)
  420. # First the cross-section 2-vector is scaled,
  421. # then rotated (which may make it a 3-vector),
  422. # then applied to the xz plane unit vectors
  423. sptv3 = numpy.array(spt.getData()[:3])
  424. for cpt in cross:
  425. v = sptv3 + m.dot(cpt)
  426. self.addVertex(*v)
  427. if begin_cap:
  428. self.addFace([x for x in range(nc - 1, -1, -1)], ccw)
  429. # Order of edges in the face: forward along cross, forward along spine,
  430. # backward along cross, backward along spine, flipped if now ccw.
  431. # This order is assumed later in the texture coordinate assignment;
  432. # please don't change without syncing.
  433. for s in range(ns - 1):
  434. for c in range(ncf):
  435. self.addQuadFlip(s * nc + c, s * nc + (c + 1) % nc,
  436. (s + 1) * nc + (c + 1) % nc, (s + 1) * nc + c, ccw)
  437. if spine_closed:
  438. # The faces between the last and the first spine points
  439. b = (ns - 1) * nc
  440. for c in range(ncf):
  441. self.addQuadFlip(b + c, b + (c + 1) % nc,
  442. (c + 1) % nc, c, ccw)
  443. if end_cap:
  444. self.addFace([(ns - 1) * nc + x for x in range(0, nc)], ccw)
  445. # Triangle meshes
  446. # Helper for numerous nodes with a Coordinate subnode holding vertices
  447. # That all triangle meshes and IndexedFaceSet
  448. # num_faces can be a function, in case the face count is a function of vertex count
  449. def startCoordMesh(self, node, num_faces):
  450. ccw = readBoolean(node, "ccw", True)
  451. self.readVertices(node) # This will allocate and fill the vertex array
  452. if hasattr(num_faces, "__call__"):
  453. num_faces = num_faces(self.getVertexCount())
  454. self.reserveFaceCount(num_faces)
  455. return ccw
  456. def processGeometryIndexedTriangleSet(self, node):
  457. index = readIntArray(node, "index", [])
  458. num_faces = len(index) // 3
  459. ccw = int(self.startCoordMesh(node, num_faces))
  460. for i in range(0, num_faces*3, 3):
  461. self.addTri(index[i + 1 - ccw], index[i + ccw], index[i+2])
  462. def processGeometryIndexedTriangleStripSet(self, node):
  463. strips = readIndex(node, "index")
  464. ccw = int(self.startCoordMesh(node, sum([len(strip) - 2 for strip in strips])))
  465. for strip in strips:
  466. sccw = ccw # Running CCW value, reset for each strip
  467. for i in range(len(strip) - 2):
  468. self.addTri(strip[i + 1 - sccw], strip[i + sccw], strip[i+2])
  469. sccw = 1 - sccw
  470. def processGeometryIndexedTriangleFanSet(self, node):
  471. fans = readIndex(node, "index")
  472. ccw = int(self.startCoordMesh(node, sum([len(fan) - 2 for fan in fans])))
  473. for fan in fans:
  474. for i in range(1, len(fan) - 1):
  475. self.addTri(fan[0], fan[i + 1 - ccw], fan[i + ccw])
  476. def processGeometryTriangleSet(self, node):
  477. ccw = int(self.startCoordMesh(node, lambda num_vert: num_vert // 3))
  478. for i in range(0, self.getVertexCount(), 3):
  479. self.addTri(i + 1 - ccw, i + ccw, i+2)
  480. def processGeometryTriangleStripSet(self, node):
  481. strips = readIntArray(node, "stripCount", [])
  482. ccw = int(self.startCoordMesh(node, sum([n-2 for n in strips])))
  483. vb = 0
  484. for n in strips:
  485. sccw = ccw
  486. for i in range(n-2):
  487. self.addTri(vb + i + 1 - sccw, vb + i + sccw, vb + i + 2)
  488. sccw = 1 - sccw
  489. vb += n
  490. def processGeometryTriangleFanSet(self, node):
  491. fans = readIntArray(node, "fanCount", [])
  492. ccw = int(self.startCoordMesh(node, sum([n-2 for n in fans])))
  493. vb = 0
  494. for n in fans:
  495. for i in range(1, n-1):
  496. self.addTri(vb, vb + i + 1 - ccw, vb + i + ccw)
  497. vb += n
  498. # Quad geometries from the CAD module, might be relevant for printing
  499. def processGeometryQuadSet(self, node):
  500. ccw = self.startCoordMesh(node, lambda num_vert: 2*(num_vert // 4))
  501. for i in range(0, self.getVertexCount(), 4):
  502. self.addQuadFlip(i, i+1, i+2, i+3, ccw)
  503. def processGeometryIndexedQuadSet(self, node):
  504. index = readIntArray(node, "index", [])
  505. num_quads = len(index) // 4
  506. ccw = self.startCoordMesh(node, num_quads*2)
  507. for i in range(0, num_quads*4, 4):
  508. self.addQuadFlip(index[i], index[i+1], index[i+2], index[i+3], ccw)
  509. # 2D polygon geometries
  510. # Won't work for now, since Cura expects every mesh to have a nontrivial convex hull
  511. # The only way around that is merging meshes.
  512. def processGeometryDisk2D(self, node):
  513. innerRadius = readFloat(node, "innerRadius", 0)
  514. outerRadius = readFloat(node, "outerRadius", 1)
  515. n = readInt(node, "subdivision", DEFAULT_SUBDIV)
  516. angle = 2 * pi / n
  517. self.reserveFaceAndVertexCount(n*4 if innerRadius else n-2, n*2 if innerRadius else n)
  518. for i in range(n):
  519. s = sin(angle * i)
  520. c = cos(angle * i)
  521. self.addVertex(outerRadius*c, outerRadius*s, 0)
  522. if innerRadius:
  523. self.addVertex(innerRadius*c, innerRadius*s, 0)
  524. ni = (i+1) % n
  525. self.addQuad(2*i, 2*ni, 2*ni+1, 2*i+1)
  526. if not innerRadius:
  527. for i in range(2, n):
  528. self.addTri(0, i-1, i)
  529. def processGeometryRectangle2D(self, node):
  530. (x, y) = readFloatArray(node, "size", (2, 2))
  531. self.reserveFaceAndVertexCount(2, 4)
  532. self.addVertex(-x/2, -y/2, 0)
  533. self.addVertex(x/2, -y/2, 0)
  534. self.addVertex(x/2, y/2, 0)
  535. self.addVertex(-x/2, y/2, 0)
  536. self.addQuad(0, 1, 2, 3)
  537. def processGeometryTriangleSet2D(self, node):
  538. verts = readFloatArray(node, "vertices", ())
  539. num_faces = len(verts) // 6;
  540. verts = [(verts[i], verts[i+1], 0) for i in range(0, 6 * num_faces, 2)]
  541. self.reserveFaceAndVertexCount(num_faces, num_faces * 3)
  542. for vert in verts:
  543. self.addVertex(*vert)
  544. # The front face is on the +Z side, so CCW is a variable
  545. for i in range(0, num_faces*3, 3):
  546. a = Vector(*verts[i+2]) - Vector(*verts[i])
  547. b = Vector(*verts[i+1]) - Vector(*verts[i])
  548. self.addTriFlip(i, i+1, i+2, a.x*b.y > a.y*b.x)
  549. # General purpose polygon mesh
  550. def processGeometryIndexedFaceSet(self, node):
  551. faces = readIndex(node, "coordIndex")
  552. ccw = self.startCoordMesh(node, sum([len(face) - 2 for face in faces]))
  553. for face in faces:
  554. if len(face) == 3:
  555. self.addTriFlip(face[0], face[1], face[2], ccw)
  556. elif len(face) > 3:
  557. self.addFace(face, ccw)
  558. geometry_importers = {
  559. "IndexedFaceSet": processGeometryIndexedFaceSet,
  560. "IndexedTriangleSet": processGeometryIndexedTriangleSet,
  561. "IndexedTriangleStripSet": processGeometryIndexedTriangleStripSet,
  562. "IndexedTriangleFanSet": processGeometryIndexedTriangleFanSet,
  563. "TriangleSet": processGeometryTriangleSet,
  564. "TriangleStripSet": processGeometryTriangleStripSet,
  565. "TriangleFanSet": processGeometryTriangleFanSet,
  566. "QuadSet": processGeometryQuadSet,
  567. "IndexedQuadSet": processGeometryIndexedQuadSet,
  568. "TriangleSet2D": processGeometryTriangleSet2D,
  569. "Rectangle2D": processGeometryRectangle2D,
  570. "Disk2D": processGeometryDisk2D,
  571. "ElevationGrid": processGeometryElevationGrid,
  572. "Extrusion": processGeometryExtrusion,
  573. "Sphere": processGeometrySphere,
  574. "Box": processGeometryBox,
  575. "Cylinder": processGeometryCylinder,
  576. "Cone": processGeometryCone
  577. }
  578. # Parses the Coordinate.@point field, fills the verts array.
  579. def readVertices(self, node):
  580. for c in node:
  581. if c.tag == "Coordinate":
  582. c = self.resolveDefUse(c)
  583. if not c is None:
  584. pt = c.attrib.get("point")
  585. if pt:
  586. # allow the list of float values in 'point' attribute to
  587. # be separated by commas or whitespace as per spec of
  588. # XML encoding of X3D
  589. # Ref ISO/IEC 19776-1:2015 : Section 5.1.2
  590. co = [float(x) for vec in pt.split(',') for x in vec.split()]
  591. num_verts = len(co) // 3
  592. self.verts = numpy.empty((4, num_verts), dtype=numpy.float32)
  593. self.verts[3,:] = numpy.ones((num_verts), dtype=numpy.float32)
  594. # Group by three
  595. for i in range(num_verts):
  596. self.verts[:3,i] = co[3*i:3*i+3]
  597. # Mesh builder helpers
  598. def reserveFaceAndVertexCount(self, num_faces, num_verts):
  599. # Unlike the Cura MeshBuilder, we use 4-vectors stored as columns for easier transform
  600. self.verts = numpy.zeros((4, num_verts), dtype=numpy.float32)
  601. self.verts[3,:] = numpy.ones((num_verts), dtype=numpy.float32)
  602. self.num_verts = 0
  603. self.reserveFaceCount(num_faces)
  604. def reserveFaceCount(self, num_faces):
  605. self.faces = numpy.zeros((num_faces, 3), dtype=numpy.int32)
  606. self.num_faces = 0
  607. def getVertexCount(self):
  608. return self.verts.shape[1]
  609. def addVertex(self, x, y, z):
  610. self.verts[0, self.num_verts] = x
  611. self.verts[1, self.num_verts] = y
  612. self.verts[2, self.num_verts] = z
  613. self.num_verts += 1
  614. # Indices are 0-based for this shape, but they won't be zero-based in the merged mesh
  615. def addTri(self, a, b, c):
  616. self.faces[self.num_faces, 0] = self.index_base + a
  617. self.faces[self.num_faces, 1] = self.index_base + b
  618. self.faces[self.num_faces, 2] = self.index_base + c
  619. self.num_faces += 1
  620. def addTriFlip(self, a, b, c, ccw):
  621. if ccw:
  622. self.addTri(a, b, c)
  623. else:
  624. self.addTri(b, a, c)
  625. # Needs to be convex, but not necessaily planar
  626. # Assumed ccw, cut along the ac diagonal
  627. def addQuad(self, a, b, c, d):
  628. self.addTri(a, b, c)
  629. self.addTri(c, d, a)
  630. def addQuadFlip(self, a, b, c, d, ccw):
  631. if ccw:
  632. self.addTri(a, b, c)
  633. self.addTri(c, d, a)
  634. else:
  635. self.addTri(a, c, b)
  636. self.addTri(c, a, d)
  637. # Arbitrary polygon triangulation.
  638. # Doesn't assume convexity and doesn't check the "convex" flag in the file.
  639. # Works by the "cutting of ears" algorithm:
  640. # - Find an outer vertex with the smallest angle and no vertices inside its adjacent triangle
  641. # - Remove the triangle at that vertex
  642. # - Repeat until done
  643. # Vertex coordinates are supposed to be already set
  644. def addFace(self, indices, ccw):
  645. # Resolve indices to coordinates for faster math
  646. face = [Vector(data=self.verts[0:3, i]) for i in indices]
  647. # Need a normal to the plane so that we can know which vertices form inner angles
  648. normal = findOuterNormal(face)
  649. if not normal: # Couldn't find an outer edge, non-planar polygon maybe?
  650. return
  651. # Find the vertex with the smallest inner angle and no points inside, cut off. Repeat until done
  652. n = len(face)
  653. vi = [i for i in range(n)] # We'll be using this to kick vertices from the face
  654. while n > 3:
  655. max_cos = EPSILON # We don't want to check anything on Pi angles
  656. i_min = 0 # max cos corresponds to min angle
  657. for i in range(n):
  658. inext = (i + 1) % n
  659. iprev = (i + n - 1) % n
  660. v = face[vi[i]]
  661. next = face[vi[inext]] - v
  662. prev = face[vi[iprev]] - v
  663. nextXprev = next.cross(prev)
  664. if nextXprev.dot(normal) > EPSILON: # If it's an inner angle
  665. cos = next.dot(prev) / (next.length() * prev.length())
  666. if cos > max_cos:
  667. # Check if there are vertices inside the triangle
  668. no_points_inside = True
  669. for j in range(n):
  670. if j != i and j != iprev and j != inext:
  671. vx = face[vi[j]] - v
  672. if pointInsideTriangle(vx, next, prev, nextXprev):
  673. no_points_inside = False
  674. break
  675. if no_points_inside:
  676. max_cos = cos
  677. i_min = i
  678. self.addTriFlip(indices[vi[(i_min + n - 1) % n]], indices[vi[i_min]], indices[vi[(i_min + 1) % n]], ccw)
  679. vi.pop(i_min)
  680. n -= 1
  681. self.addTriFlip(indices[vi[0]], indices[vi[1]], indices[vi[2]], ccw)
  682. # ------------------------------------------------------------
  683. # X3D field parsers
  684. # ------------------------------------------------------------
  685. def readFloatArray(node, attr, default):
  686. s = node.attrib.get(attr)
  687. if not s:
  688. return default
  689. return [float(x) for x in s.split()]
  690. def readIntArray(node, attr, default):
  691. s = node.attrib.get(attr)
  692. if not s:
  693. return default
  694. return [int(x, 0) for x in s.split()]
  695. def readFloat(node, attr, default):
  696. s = node.attrib.get(attr)
  697. if not s:
  698. return default
  699. return float(s)
  700. def readInt(node, attr, default):
  701. s = node.attrib.get(attr)
  702. if not s:
  703. return default
  704. return int(s, 0)
  705. def readBoolean(node, attr, default):
  706. s = node.attrib.get(attr)
  707. if not s:
  708. return default
  709. return s.lower() == "true"
  710. def readVector(node, attr, default):
  711. v = readFloatArray(node, attr, default)
  712. return Vector(v[0], v[1], v[2])
  713. def readRotation(node, attr, default):
  714. v = readFloatArray(node, attr, default)
  715. return (v[3], Vector(v[0], v[1], v[2]))
  716. # Returns the -1-separated runs
  717. def readIndex(node, attr):
  718. v = readIntArray(node, attr, [])
  719. chunks = []
  720. chunk = []
  721. for i in range(len(v)):
  722. if v[i] == -1:
  723. if chunk:
  724. chunks.append(chunk)
  725. chunk = []
  726. else:
  727. chunk.append(v[i])
  728. if chunk:
  729. chunks.append(chunk)
  730. return chunks
  731. # Given a face as a sequence of vectors, returns a normal to the polygon place that forms a right triple
  732. # with a vector along the polygon sequence and a vector backwards
  733. def findOuterNormal(face):
  734. n = len(face)
  735. for i in range(n):
  736. for j in range(i+1, n):
  737. edge = face[j] - face[i]
  738. if edge.length() > EPSILON:
  739. edge = edge.normalized()
  740. prev_rejection = Vector()
  741. is_outer = True
  742. for k in range(n):
  743. if k != i and k != j:
  744. pt = face[k] - face[i]
  745. pte = pt.dot(edge)
  746. rejection = pt - edge*pte
  747. if rejection.dot(prev_rejection) < -EPSILON: # points on both sides of the edge - not an outer one
  748. is_outer = False
  749. break
  750. elif rejection.length() > prev_rejection.length(): # Pick a greater rejection for numeric stability
  751. prev_rejection = rejection
  752. if is_outer: # Found an outer edge, prev_rejection is the rejection inside the face. Generate a normal.
  753. return edge.cross(prev_rejection)
  754. return False
  755. # Given two *collinear* vectors a and b, returns the coefficient that takes b to a.
  756. # No error handling.
  757. # For stability, taking the ration between the biggest coordinates would be better...
  758. def ratio(a, b):
  759. if b.x > EPSILON or b.x < -EPSILON:
  760. return a.x / b.x
  761. elif b.y > EPSILON or b.y < -EPSILON:
  762. return a.y / b.y
  763. else:
  764. return a.z / b.z
  765. def pointInsideTriangle(vx, next, prev, nextXprev):
  766. vxXprev = vx.cross(prev)
  767. r = ratio(vxXprev, nextXprev)
  768. if r < 0:
  769. return False
  770. vxXnext = vx.cross(next);
  771. s = -ratio(vxXnext, nextXprev)
  772. return s > 0 and (s + r) < 1