threepipe
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ThreeSVGRendererPlugin.ts 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. import {
  2. AViewerPluginSync,
  3. IObject3D,
  4. type IViewerEvent,
  5. Mesh,
  6. onChange,
  7. PerspectiveCamera2,
  8. ThreeViewer,
  9. timeout,
  10. uiButton,
  11. uiFolderContainer,
  12. uiToggle,
  13. uiVector,
  14. Vector2,
  15. } from 'threepipe'
  16. import {FillPass, HiddenChainPass, SVGMesh, SVGRenderer, VisibleChainPass} from './three-svg-renderer'
  17. import {Vertex} from './three-mesh-halfedge'
  18. /**
  19. * SVG Rendering from 3d scenes helper plugin using [three-svg-renderer](https://www.npmjs.com/package/three-svg-renderer) (GPLV3 Licenced)
  20. */
  21. @uiFolderContainer('SVG Renderer')
  22. export class ThreeSVGRendererPlugin extends AViewerPluginSync {
  23. static readonly PluginType = 'ThreeSVGRendererPlugin'
  24. @uiToggle()
  25. @onChange(ThreeSVGRendererPlugin.prototype.setDirty)
  26. enabled = true
  27. /**
  28. * Automatically render when camera or any object changes.
  29. */
  30. @uiToggle()
  31. @onChange(ThreeSVGRendererPlugin.prototype.setDirty)
  32. autoRender = true
  33. /**
  34. * Use the fill pass to draw polygons.(both fills and strokes)
  35. */
  36. @uiToggle()
  37. @onChange(ThreeSVGRendererPlugin.prototype.setDirty)
  38. drawPolygons = true
  39. /**
  40. * Draw polygon fills. (fill color from material.color)
  41. */
  42. @uiToggle()
  43. @onChange(ThreeSVGRendererPlugin.prototype.setDirty)
  44. drawPolygonFills = true
  45. /**
  46. * Draw polygon strokes. (stroke color from material.color)
  47. */
  48. @uiToggle()
  49. @onChange(ThreeSVGRendererPlugin.prototype.setDirty)
  50. drawPolygonStrokes = true
  51. /**
  52. * Draw image fills. (fill image from rendered canvas image).
  53. * Make sure canvas is rendered(and render pipeline has a render pass) before calling this.
  54. */
  55. @uiToggle()
  56. @onChange(ThreeSVGRendererPlugin.prototype.setDirty)
  57. drawImageFills = false
  58. /**
  59. * Draw visible contours of meshes.
  60. */
  61. @uiToggle()
  62. @onChange(ThreeSVGRendererPlugin.prototype.setDirty)
  63. drawVisibleContours = true
  64. /**
  65. * Draw hidden contours of meshes.
  66. */
  67. @uiToggle()
  68. @onChange(ThreeSVGRendererPlugin.prototype.setDirty)
  69. drawHiddenContours = true
  70. /**
  71. * Update meshes on every render. If this is false, meshes will only be updated when they change. (tracked using objectUpdate event)
  72. */
  73. @uiToggle()
  74. @onChange(ThreeSVGRendererPlugin.prototype.setDirty)
  75. alwaysUpdateMeshes = true
  76. /**
  77. * Min and Max Crease angle for mesh edges.
  78. */
  79. @uiVector()
  80. @onChange(ThreeSVGRendererPlugin.prototype.setDirty)
  81. creaseAngle = new Vector2(80, 100)
  82. /**
  83. * Automatically create SVG objects for all meshes in the scene.
  84. * If this is false, you will have to manually create SVG objects for meshes using `makeSVGObject` method.
  85. */
  86. @uiToggle()
  87. @onChange(ThreeSVGRendererPlugin.prototype.setDirty)
  88. autoMakeSvgObjects = true
  89. readonly renderer = new SVGRenderer()
  90. readonly svgNodeContainer = document.createElement('div')
  91. protected readonly _fillPass: FillPass
  92. setDirty(...args: any[]): any {
  93. if (args[0] === 'enabled') {
  94. const last = args[2]
  95. const current = args[1]
  96. if (last !== current && this._meshes?.size) {
  97. this._toggleMaterialRendering([...this._meshes.values()], !current)
  98. }
  99. if (this.svgNodeContainer) {
  100. this.svgNodeContainer.style.display = current ? '' : 'none'
  101. }
  102. }
  103. this._viewer?.setDirty()
  104. }
  105. /**
  106. * @param enabled
  107. * @param autoAddToContainer - automatically add the svg to the viewer container and style it same as the viewer is position is absolute
  108. */
  109. constructor(enabled = true, readonly autoAddToContainer = true) {
  110. super()
  111. this._onResize = this._onResize.bind(this)
  112. this._onMeshDispose = this._onMeshDispose.bind(this)
  113. this._onMeshUpdate = this._onMeshUpdate.bind(this)
  114. this.updateMeshes = this.updateMeshes.bind(this)
  115. this.enabled = enabled
  116. this.svgNodeContainer.style.position = 'absolute'
  117. this.svgNodeContainer.style.display = 'none'
  118. // This pass will draw fills for meshes using the three.js material color
  119. this._fillPass = new FillPass()
  120. // This pass will draw visible contours of meshes on top of fills
  121. // using black color and solid line of width 1
  122. const visibleChainPass = new VisibleChainPass({
  123. // color: '#000000',
  124. // width: 1,
  125. })
  126. // This pass will draw hidden contours on top of visible and fills
  127. // using red color, dash line of width 1
  128. const hiddenChainPass = new HiddenChainPass({
  129. // color: '#FF0000',
  130. // width: 1,
  131. // dasharray: '2,2',
  132. })
  133. this.renderer.addPass(this._fillPass)
  134. this.renderer.addPass(visibleChainPass)
  135. this.renderer.addPass(hiddenChainPass)
  136. Vertex.MAX_LOOP = 10000 // todo; this is for large models that get stuck.
  137. // this.renderer.addPass(new SingularityPointPass())
  138. // this.renderer.addPass(new TexturePass())
  139. }
  140. protected _lastStyles?: string = undefined
  141. onAdded(viewer: ThreeViewer) {
  142. super.onAdded(viewer)
  143. // this.renderer.setSize(viewer.canvas.clientWidth, viewer.canvas.clientHeight)
  144. // this._refreshParams() // this is done before rendering
  145. if (this.autoAddToContainer) {
  146. viewer.container.prepend(this.svgNodeContainer) // behind the canvas so that we get pointer events and see other stuff
  147. this.svgNodeContainer.style.pointerEvents = 'none'
  148. const canvasStyles = getComputedStyle(viewer.canvas)
  149. if (canvasStyles.position === 'absolute') {
  150. this._lastStyles = this.svgNodeContainer.style.cssText
  151. // copy styles from canvas to svg so it looks the same.
  152. this.svgNodeContainer.style.top = canvasStyles.top
  153. this.svgNodeContainer.style.left = canvasStyles.left
  154. this.svgNodeContainer.style.width = canvasStyles.width
  155. this.svgNodeContainer.style.height = canvasStyles.height
  156. // this.svgNodeContainer.style.zIndex = '999999' // svg should be behind the canvas
  157. } else {
  158. this._viewer?.console.warn('ThreeSVGRendererPlugin: canvas position should be absolute for proper rendering')
  159. }
  160. viewer.renderManager.addEventListener('resize', this._onResize)
  161. }
  162. this.svgNodeContainer.style.display = this.enabled ? '' : 'none'
  163. viewer.scene.modelRoot.addEventListener('objectUpdate', this.updateMeshes)
  164. }
  165. private _meshesNeedsUpdate = true
  166. updateMeshes() {
  167. console.log('updateMeshes')
  168. this._meshesNeedsUpdate = true
  169. }
  170. onRemove(viewer: ThreeViewer) {
  171. super.onRemove(viewer)
  172. if (this.autoAddToContainer) {
  173. viewer.container.removeChild(this.svgNodeContainer)
  174. }
  175. if (this._lastStyles !== undefined) {
  176. this.svgNodeContainer.style.cssText = this._lastStyles
  177. this._lastStyles = undefined
  178. }
  179. viewer.renderManager.removeEventListener('resize', this._onResize)
  180. this._meshes.clear() // ?
  181. this.svgNodeContainer.style.display = 'none'
  182. viewer.scene.modelRoot.removeEventListener('objectUpdate', this.updateMeshes)
  183. }
  184. protected _onMeshDispose(ev: any) {
  185. if (!ev.target) return
  186. const mesh = ev.target as Mesh
  187. const svgMesh = this._meshes.get(mesh.uuid)
  188. if (!svgMesh) return
  189. svgMesh.dispose()
  190. this._meshes.delete(mesh.uuid)
  191. mesh.removeEventListener('dispose', this._onMeshDispose)
  192. mesh.removeEventListener('objectUpdate', this._onMeshUpdate)
  193. }
  194. protected _onMeshUpdate(ev: any) {
  195. if (!ev.target) return
  196. const mesh = ev.target as Mesh
  197. const svgMesh = this._meshes.get(mesh.uuid)
  198. if (!svgMesh) return
  199. svgMesh.updateObject()
  200. }
  201. protected _meshes = new Map<string, SVGMesh>()
  202. protected _refreshMeshes(root?: IObject3D) {
  203. if (!this.autoMakeSvgObjects) return []
  204. if (!root && this._viewer) root = this._viewer.scene.modelRoot
  205. if (!root) return []
  206. root.traverse(o=>{
  207. this.makeSVGObject(o)
  208. })
  209. }
  210. makeSVGObject(o: IObject3D) {
  211. if (!(o.isMesh && !this._meshes.has(o.uuid))) return
  212. const ud = o.userData
  213. o.userData = {}
  214. const svgMesh = new SVGMesh(o as any as Mesh)
  215. o.userData = ud
  216. this._meshes.set(o.uuid, svgMesh)
  217. this._toggleMaterialRendering([svgMesh], !this.enabled)
  218. o.addEventListener('dispose', this._onMeshDispose)
  219. o.addEventListener('objectUpdate', this._onMeshUpdate) // todo: check if we need to do object update everytime and what actions specifically.
  220. this._meshesNeedsUpdate = true // because we have a new mesh
  221. }
  222. private _rendering = false
  223. static SVG_RENDER_TIMEOUT = 2000
  224. protected _toggleMaterialRendering(meshes: SVGMesh[], enable: boolean) {
  225. const materials = []
  226. for (const mesh of meshes) {
  227. materials.push(...Array.isArray(mesh.material) ? mesh.material : [mesh.material])
  228. }
  229. // enable rendering of material colors
  230. for (const mat of materials) {
  231. if (mat.colorWrite !== undefined) {
  232. if (enable && !mat.colorWrite) {
  233. mat.colorWrite = true
  234. delete mat.userData.forcedLinearDepth
  235. // mat.userData._colorWriteSet = true
  236. } else if (!enable /* && mat.userData._colorWriteSet*/) {
  237. mat.colorWrite = false
  238. mat.userData.forcedLinearDepth = 1 // for gbuffer plugin
  239. // delete mat.userData._colorWriteSet
  240. }
  241. }
  242. }
  243. }
  244. @uiButton()
  245. async render() {
  246. if (!this._viewer || !this._viewer.renderEnabled) return
  247. if (this.isDisabled()) return
  248. if (this._rendering) return
  249. this._rendering = true
  250. this._refreshParams()
  251. this._refreshMeshes()
  252. const meshes = [...this._meshes.values()]
  253. // todo: make sure all meshes are in the scene
  254. // todo only use meshes that should be rendered.
  255. if (!meshes.length) {
  256. this._rendering = false
  257. return ''
  258. }
  259. const camera = this._viewer.scene.mainCamera as PerspectiveCamera2
  260. try {
  261. if (this.drawImageFills) {
  262. this._toggleMaterialRendering(meshes, true)
  263. this._viewer.setDirty()
  264. this._viewer.canvas.style.opacity = '0'
  265. await this._viewer.doOnce('preFrame') // because we are already in postRender or postFrame.
  266. await this._viewer.doOnce('postFrame') // todo wait for progressive also maybe
  267. await this._viewer.doOnce('postFrame') // once more
  268. this._fillPass.options.fillImage = this._viewer.canvas.toDataURL('image/png')
  269. // disable rendering of material colors
  270. this._toggleMaterialRendering(meshes, false)
  271. this._viewer.setDirty()
  272. await this._viewer.doOnce('preFrame') // already in postFrame
  273. await this._viewer.doOnce('postFrame')
  274. this._viewer.canvas.style.opacity = '1'
  275. }
  276. // this._fillPass.options.fillImage = this._viewer.canvas.toDataURL('image/png')
  277. // this._viewer.renderEnabled = false
  278. this.renderer.viewmap.skipActions = false
  279. const svgPromise = this.renderer.generateSVG(meshes, camera, {
  280. w: this._viewer.canvas.width,
  281. h: this._viewer.canvas.height,
  282. })
  283. let svgResolved = false
  284. timeout(ThreeSVGRendererPlugin.SVG_RENDER_TIMEOUT).then(()=>{ // todo: make support in libs to cancel the promise. this will just wait for an action to complete.
  285. if (!svgResolved) {
  286. console.warn('timeout')
  287. this.renderer.viewmap.skipActions = true
  288. }
  289. })
  290. const svg = await svgPromise
  291. svgResolved = true
  292. this.renderer.viewmap.skipActions = false
  293. // this._viewer.renderEnabled = true
  294. this.svgNodeContainer.innerHTML = svg.node.outerHTML
  295. this._rendering = false
  296. return svg.svg()
  297. } catch (e) {
  298. console.error(e)
  299. }
  300. this._rendering = false
  301. return ''
  302. }
  303. @uiButton()
  304. download() {
  305. const svg = this.svgNodeContainer.innerHTML
  306. const blob = new Blob([svg], {type: 'image/svg+xml'})
  307. this._viewer?.exportBlob(blob, 'scene.svg')
  308. }
  309. protected _viewerListeners = {
  310. postRender: (_: IViewerEvent)=>{
  311. if (this.autoRender) this.render()
  312. },
  313. }
  314. get svgNode() {
  315. if (!this.svgNodeContainer.children.length) return undefined
  316. if (this.svgNodeContainer.children.length > 1) {
  317. this._viewer?.console.warn('ThreeSVGRenderer: Multiple svg nodes in container, should not be possible')
  318. }
  319. return this.svgNodeContainer.children[0]
  320. }
  321. protected _refreshParams() {
  322. if (this.isDisabled()) return
  323. if (this._meshesNeedsUpdate) {
  324. this.renderer.viewmap.options.updateMeshes = true
  325. this._meshesNeedsUpdate = false
  326. } else this.renderer.viewmap.options.updateMeshes = this.alwaysUpdateMeshes
  327. this.renderer.viewmap.options.creaseAngle.min = this.creaseAngle.x
  328. this.renderer.viewmap.options.creaseAngle.max = this.creaseAngle.y
  329. const passes = this.renderer.drawHandler.passes
  330. const fillPass = passes.find(p=>p instanceof FillPass)!
  331. const visibleContourPass = passes.find(p=>p instanceof VisibleChainPass)!
  332. const hiddenContourPass = passes.find(p=>p instanceof HiddenChainPass)!
  333. if (fillPass) {
  334. fillPass.enabled = this.drawPolygons && (this.drawPolygonFills || this.drawPolygonStrokes || this.drawImageFills)
  335. fillPass.drawFills = this.drawPolygonFills
  336. fillPass.drawStrokes = this.drawPolygonStrokes
  337. fillPass.drawImageFills = this.drawImageFills
  338. }
  339. if (visibleContourPass) {
  340. visibleContourPass.enabled = this.drawVisibleContours
  341. }
  342. if (hiddenContourPass) {
  343. hiddenContourPass.enabled = this.drawHiddenContours
  344. }
  345. }
  346. protected _onResize() {
  347. if (!this._viewer) return
  348. // this.renderer.setSize(this._viewer.canvas.clientWidth, this._viewer.canvas.clientHeight)
  349. }
  350. }
  351. // adding here since they dont show up in dependencies.txt somehow
  352. /**
  353. * @license
  354. * three-svg-renderer
  355. *
  356. * GNU GENERAL PUBLIC LICENSE
  357. * Version 3, 29 June 2007
  358. *
  359. * Copyright (c) 2022 Axel Antoine
  360. */
  361. /**
  362. * @license
  363. * three-mesh-halfedge
  364. *
  365. * MIT License
  366. *
  367. * Copyright (c) 2022 Axel Antoine
  368. */