X3DReader.py 36 KB

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