X3DReader.py 35 KB

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