InspectGeometryTool.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. /*
  2. * InspectGeometryTool.js
  3. *
  4. * @author realor
  5. */
  6. import { Tool } from './Tool.js'
  7. import { Tree } from '../ui/Tree.js'
  8. import { TabbedPane } from '../ui/TabbedPane.js'
  9. import { Controls } from '../ui/Controls.js'
  10. import { Solid } from '../core/Solid.js'
  11. import { SolidOptimizer } from '../core/SolidOptimizer.js'
  12. import { ObjectUtils } from '../utils/ObjectUtils.js'
  13. import { I18N } from '../i18n/I18N.js'
  14. import * as THREE from '../lib/three.module.js'
  15. class InspectGeometryTool extends Tool {
  16. constructor(application, options) {
  17. super(application)
  18. this.name = 'inspect_geometry'
  19. this.label = 'tool.inspect_geometry.label'
  20. this.className = 'inspect_geometry'
  21. this.object = null
  22. this.selectedNode = null
  23. this.highlightGroup = null
  24. this.lineMaterial = new THREE.LineBasicMaterial({ color: 0, linewidth: 1.5, depthTest: false, transparent: true })
  25. this.pointsMaterial = new THREE.PointsMaterial({ color: 0, size: 4, sizeAttenuation: false, depthTest: false, transparent: true })
  26. this.vertexMaterial = new THREE.PointsMaterial({ color: 0x000080, size: 8, sizeAttenuation: false, depthTest: false, transparent: true })
  27. this.sceneUuid = null
  28. this.setOptions(options)
  29. this.createPanel()
  30. }
  31. createPanel() {
  32. this.panel = this.application.createPanel(this.label, 'left')
  33. this.panel.bodyElem.classList.add('padding')
  34. this.tabbedPane = new TabbedPane(this.panel.bodyElem)
  35. const geometryInventoryPanel = this.tabbedPane.addTab('geom_inventory', 'label.geometry_inventory')
  36. geometryInventoryPanel.classList.add('inspect_geometry')
  37. const geometryPanel = this.tabbedPane.addTab('geom_detail', 'label.geometry_detail')
  38. geometryPanel.classList.add('inspect_geometry')
  39. // geometry inventory panel
  40. this.geometryToDisplayElem = Controls.addNumberField(geometryInventoryPanel, 'geom_to_display', 'label.geometries_display', '100', 'row')
  41. this.geometryToDisplayElem.style.width = '60px'
  42. this.searchButton = Controls.addButton(geometryInventoryPanel, 'geom_search', 'button.search', () => this.searchGeometries())
  43. this.listInvElem = document.createElement('ul')
  44. this.listInvElem.className = 'summary'
  45. geometryInventoryPanel.appendChild(this.listInvElem)
  46. this.geometryCountElem = document.createElement('li')
  47. this.listInvElem.appendChild(this.geometryCountElem)
  48. this.instanceCountElem = document.createElement('li')
  49. this.listInvElem.appendChild(this.instanceCountElem)
  50. this.modeledTriangleCountElem = document.createElement('li')
  51. this.listInvElem.appendChild(this.modeledTriangleCountElem)
  52. this.renderedTriangleCountElem = document.createElement('li')
  53. this.listInvElem.appendChild(this.renderedTriangleCountElem)
  54. this.listInvElem.style.display = 'none'
  55. this.geometryTable = Controls.addTable(
  56. geometryInventoryPanel,
  57. 'geom_table',
  58. ['label.geometry_id', 'label.geometry_instances', 'label.geometry_triangles', 'label.geometry_total_triangles'],
  59. 'data'
  60. )
  61. this.geometryTable.style.display = 'none'
  62. // geometry panel
  63. this.messageElem = document.createElement('div')
  64. this.messageElem.style.padding = '4px'
  65. geometryPanel.appendChild(this.messageElem)
  66. I18N.set(this.messageElem, 'innerHTML', 'tool.inspect_geometry.help')
  67. this.listElem = document.createElement('ul')
  68. this.listElem.className = 'summary'
  69. geometryPanel.appendChild(this.listElem)
  70. this.objectNameElem = document.createElement('li')
  71. this.listElem.appendChild(this.objectNameElem)
  72. this.geometryIdElem = document.createElement('li')
  73. this.listElem.appendChild(this.geometryIdElem)
  74. this.vertexCountElem = document.createElement('li')
  75. this.listElem.appendChild(this.vertexCountElem)
  76. this.faceCountElem = document.createElement('li')
  77. this.listElem.appendChild(this.faceCountElem)
  78. this.isManifoldElem = document.createElement('li')
  79. this.listElem.appendChild(this.isManifoldElem)
  80. this.optimizeButton = Controls.addButton(geometryPanel, 'optimize', 'button.optimize', () => this.optimize())
  81. this.optimizeButton.style.display = 'none'
  82. this.geometryTree = new Tree(geometryPanel)
  83. this._onPointerDown = this.onPointerDown.bind(this)
  84. this._onSelection = this.onSelection.bind(this)
  85. }
  86. activate() {
  87. const application = this.application
  88. const container = application.container
  89. container.addEventListener('pointerdown', this._onPointerDown, false)
  90. application.addEventListener('selection', this._onSelection, false)
  91. if (this.sceneUuid !== application.scene.uuid) {
  92. this.listInvElem.style.display = 'none'
  93. this.geometryTable.tBodies[0].innerHTML = ''
  94. this.geometryTable.style.display = 'none'
  95. }
  96. this.panel.visible = true
  97. let object = application.selection.object
  98. if (object instanceof Solid) {
  99. if (object !== this.object) {
  100. this.showSolid(object)
  101. }
  102. } else {
  103. this.clear()
  104. }
  105. }
  106. deactivate() {
  107. const application = this.application
  108. const container = application.container
  109. container.removeEventListener('pointerdown', this._onPointerDown, false)
  110. application.removeEventListener('selection', this._onSelection, false)
  111. this.panel.visible = false
  112. }
  113. onPointerDown(event) {
  114. if (!this.isCanvasEvent(event)) return
  115. const application = this.application
  116. const pointerPosition = this.getEventPosition(event)
  117. const baseObject = application.baseObject
  118. const intersect = this.intersect(pointerPosition, baseObject, true)
  119. if (intersect) {
  120. let object = intersect.object
  121. application.selection.set(object)
  122. this.tabbedPane.showTab('geom_detail')
  123. } else {
  124. application.selection.clear()
  125. }
  126. }
  127. onSelection() {
  128. const application = this.application
  129. const object = application.selection.object
  130. if (object instanceof Solid) {
  131. if (object !== this.object) {
  132. this.showSolid(object)
  133. }
  134. } else {
  135. this.clear()
  136. }
  137. }
  138. showSolid(solid) {
  139. this.object = solid
  140. this.clearHighlight()
  141. this.messageElem.style.display = 'none'
  142. const formatter = new Intl.NumberFormat()
  143. this.listElem.style.display = ''
  144. I18N.set(this.objectNameElem, 'innerHTML', 'message.object_name', solid.name)
  145. I18N.set(this.geometryIdElem, 'innerHTML', 'message.geometry_id', solid.geometry.id)
  146. I18N.set(this.faceCountElem, 'innerHTML', 'message.face_count', formatter.format(solid.geometry.faces.length))
  147. I18N.set(this.vertexCountElem, 'innerHTML', 'message.vertex_count', formatter.format(solid.geometry.vertices.length))
  148. I18N.set(this.isManifoldElem, 'innerHTML', 'message.is_manifold', solid.geometry.isManifold)
  149. this.application.i18n.updateTree(this.listElem)
  150. this.optimizeButton.style.display = ''
  151. const tree = this.geometryTree
  152. tree.clear()
  153. let geometry = solid.geometry
  154. const vertices = geometry.vertices
  155. const round = x => {
  156. return Math.round(x * 1000) / 1000
  157. }
  158. const vector2String = vector => {
  159. return '(' + round(vector.x) + ', ' + round(vector.y) + ', ' + round(vector.z) + ')'
  160. }
  161. const addVertices = (node, loop) => {
  162. const indices = loop.indices
  163. for (let i = 0; i < indices.length; i++) {
  164. let vertex = vertices[loop.indices[i]]
  165. let vertexNode = node.addNode('v-' + loop.indices[i] + ': ' + vector2String(vertex), () => this.highlight(vertexNode, solid, [loop], vertex), 'vertex')
  166. }
  167. }
  168. for (let f = 0; f < geometry.faces.length; f++) {
  169. let face = geometry.faces[f]
  170. let label = 'face-' + f + ' (' + face.outerLoop.indices.length + 'v'
  171. if (face.holes.length > 0) label += ', ' + face.holes.length + 'h)'
  172. else label += ')'
  173. let faceNode = tree.addNode(label, () => this.highlight(faceNode, solid, [face.outerLoop, ...face.holes]), 'face' + (face.holes.length > 0 ? ' holes' : ''))
  174. faceNode.addNode('normal: ' + vector2String(face.normal), () => {}, 'normal')
  175. let outerNode = faceNode.addNode('outerLoop (' + face.outerLoop.indices.length + 'v)', () => this.highlight(outerNode, solid, [face.outerLoop]), 'loop')
  176. addVertices(outerNode, face.outerLoop)
  177. for (let h = 0; h < face.holes.length; h++) {
  178. let hole = face.holes[h]
  179. let holeNode = faceNode.addNode('hole-' + h + ' (' + hole.indices.length + 'v)', () => this.highlight(holeNode, solid, [hole]), 'hole')
  180. addVertices(holeNode, hole)
  181. }
  182. }
  183. }
  184. clear() {
  185. this.geometryTree.clear()
  186. this.object = null
  187. this.clearHighlight()
  188. this.messageElem.style.display = ''
  189. this.listElem.style.display = 'none'
  190. this.optimizeButton.style.display = 'none'
  191. }
  192. addLoop(solid, loop) {
  193. const vertices = []
  194. const indices = loop.indices
  195. const matrixWorld = solid.matrixWorld
  196. for (let i = 0; i <= indices.length; i++) {
  197. vertices.push(
  198. loop
  199. .getVertex(i % indices.length)
  200. .clone()
  201. .applyMatrix4(matrixWorld)
  202. )
  203. }
  204. let geometry = new THREE.BufferGeometry()
  205. geometry.setFromPoints(vertices)
  206. let lines = new THREE.Line(geometry, this.lineMaterial)
  207. lines.raycast = function() {}
  208. this.highlightGroup.add(lines)
  209. let points = new THREE.Points(geometry, this.pointsMaterial)
  210. points.raycast = function() {}
  211. this.highlightGroup.add(points)
  212. }
  213. highlight(node, solid, loops, vertex) {
  214. if (this.selectedNode !== null) {
  215. this.selectedNode.removeClass('selected')
  216. }
  217. node.addClass('selected')
  218. this.selectedNode = node
  219. if (this.highlightGroup !== null) {
  220. this.application.removeObject(this.highlightGroup)
  221. }
  222. this.highlightGroup = new THREE.Group()
  223. this.highlightGroup.renderOrder = 2
  224. for (let loop of loops) {
  225. this.addLoop(solid, loop)
  226. }
  227. if (vertex) {
  228. let geometry = new THREE.BufferGeometry()
  229. geometry.setFromPoints([vertex.clone().applyMatrix4(solid.matrixWorld)])
  230. let points = new THREE.Points(geometry, this.vertexMaterial)
  231. points.raycast = function() {}
  232. this.highlightGroup.add(points)
  233. }
  234. this.application.addObject(this.highlightGroup, this.application.overlays)
  235. }
  236. clearHighlight() {
  237. if (this.highlightGroup !== null) {
  238. this.application.removeObject(this.highlightGroup)
  239. this.highlightGroup = null
  240. }
  241. if (this.selectedNode !== null) {
  242. this.selectedNode.removeClass('selected')
  243. this.selectedNode = null
  244. }
  245. }
  246. optimize() {
  247. const object = this.object
  248. if (object !== null && object.type === 'Solid') {
  249. let optimizer = new SolidOptimizer(object.geometry)
  250. let geometry = optimizer.optimize()
  251. object.updateGeometry(geometry)
  252. this.showSolid(object)
  253. this.application.notifyObjectsChanged(object)
  254. }
  255. }
  256. searchGeometries() {
  257. const application = this.application
  258. this.sceneUuid = application.scene.uuid
  259. let geometryMap = new Map()
  260. let instanceCount = 0
  261. let modeledTriangleCount = 0
  262. let renderedTriangleCount = 0
  263. function traverse(object) {
  264. if (!object.visible) return
  265. if (object instanceof Solid) {
  266. let geometry = object.geometry
  267. let entry = geometryMap.get(geometry.id)
  268. if (entry === undefined) {
  269. entry = {
  270. geometry: geometry,
  271. triangleCount: geometry.getTriangleCount(),
  272. instances: []
  273. }
  274. geometryMap.set(geometry.id, entry)
  275. modeledTriangleCount += entry.triangleCount
  276. }
  277. entry.instances.push(object)
  278. instanceCount++
  279. renderedTriangleCount += entry.triangleCount
  280. } else {
  281. const children = object.children
  282. for (let child of children) {
  283. traverse(child)
  284. }
  285. }
  286. }
  287. traverse(application.baseObject)
  288. let entries = Array.from(geometryMap.values())
  289. entries.sort((a, b) => {
  290. let sizeA = a.triangleCount * a.instances.length
  291. let sizeB = b.triangleCount * b.instances.length
  292. return sizeB - sizeA
  293. })
  294. const formatter = new Intl.NumberFormat()
  295. this.listInvElem.style.display = ''
  296. I18N.set(this.geometryCountElem, 'innerHTML', 'message.geometry_count', formatter.format(entries.length))
  297. I18N.set(this.instanceCountElem, 'innerHTML', 'message.instance_count', formatter.format(instanceCount))
  298. I18N.set(this.modeledTriangleCountElem, 'innerHTML', 'message.modeled_triangle_count', formatter.format(modeledTriangleCount))
  299. I18N.set(this.renderedTriangleCountElem, 'innerHTML', 'message.rendered_triangle_count', formatter.format(renderedTriangleCount))
  300. this.application.i18n.updateTree(this.listInvElem)
  301. this.geometryTable.tBodies[0].innerHTML = ''
  302. let entryCount = Math.min(entries.length, parseInt(this.geometryToDisplayElem.value))
  303. for (let i = 0; i < entryCount; i++) {
  304. let entry = entries[i]
  305. let rowElem = Controls.addTableRow(this.geometryTable)
  306. rowElem.children[0].innerHTML = entry.geometry.id
  307. let instanceLink = document.createElement('a')
  308. instanceLink.href = '#'
  309. instanceLink.innerHTML = '' + formatter.format(entry.instances.length)
  310. instanceLink.addEventListener('click', () => this.showInstances(entry))
  311. rowElem.children[1].appendChild(instanceLink)
  312. rowElem.children[2].innerHTML = formatter.format(entry.triangleCount)
  313. rowElem.children[3].innerHTML = formatter.format(entry.triangleCount * entry.instances.length)
  314. }
  315. this.geometryTable.style.display = ''
  316. }
  317. showInstances(entry) {
  318. this.application.selection.set(...entry.instances)
  319. }
  320. }
  321. export { InspectGeometryTool }