| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405 |
- import {
- AViewerPluginSync,
- IObject3D,
- type IViewerEvent,
- Mesh,
- onChange,
- PerspectiveCamera2,
- ThreeViewer,
- timeout,
- uiButton,
- uiFolderContainer,
- uiToggle,
- uiVector,
- Vector2,
- } from 'threepipe'
- import {FillPass, HiddenChainPass, SVGMesh, SVGRenderer, VisibleChainPass} from './three-svg-renderer'
- import {Vertex} from './three-mesh-halfedge'
-
- /**
- * SVG Rendering from 3d scenes helper plugin using [three-svg-renderer](https://www.npmjs.com/package/three-svg-renderer) (GPLV3 Licenced)
- */
- @uiFolderContainer('SVG Renderer')
- export class ThreeSVGRendererPlugin extends AViewerPluginSync {
- static readonly PluginType = 'ThreeSVGRendererPlugin'
-
- @uiToggle()
- @onChange(ThreeSVGRendererPlugin.prototype.setDirty)
- enabled = true
- /**
- * Automatically render when camera or any object changes.
- */
- @uiToggle()
- @onChange(ThreeSVGRendererPlugin.prototype.setDirty)
- autoRender = true
-
- /**
- * Use the fill pass to draw polygons.(both fills and strokes)
- */
- @uiToggle()
- @onChange(ThreeSVGRendererPlugin.prototype.setDirty)
- drawPolygons = true
- /**
- * Draw polygon fills. (fill color from material.color)
- */
- @uiToggle()
- @onChange(ThreeSVGRendererPlugin.prototype.setDirty)
- drawPolygonFills = true
- /**
- * Draw polygon strokes. (stroke color from material.color)
- */
- @uiToggle()
- @onChange(ThreeSVGRendererPlugin.prototype.setDirty)
- drawPolygonStrokes = true
- /**
- * Draw image fills. (fill image from rendered canvas image).
- * Make sure canvas is rendered(and render pipeline has a render pass) before calling this.
- */
- @uiToggle()
- @onChange(ThreeSVGRendererPlugin.prototype.setDirty)
- drawImageFills = false
- /**
- * Draw visible contours of meshes.
- */
- @uiToggle()
- @onChange(ThreeSVGRendererPlugin.prototype.setDirty)
- drawVisibleContours = true
- /**
- * Draw hidden contours of meshes.
- */
- @uiToggle()
- @onChange(ThreeSVGRendererPlugin.prototype.setDirty)
- drawHiddenContours = true
-
- /**
- * Update meshes on every render. If this is false, meshes will only be updated when they change. (tracked using objectUpdate event)
- */
- @uiToggle()
- @onChange(ThreeSVGRendererPlugin.prototype.setDirty)
- alwaysUpdateMeshes = true
-
- /**
- * Min and Max Crease angle for mesh edges.
- */
- @uiVector()
- @onChange(ThreeSVGRendererPlugin.prototype.setDirty)
- creaseAngle = new Vector2(80, 100)
-
- /**
- * Automatically create SVG objects for all meshes in the scene.
- * If this is false, you will have to manually create SVG objects for meshes using `makeSVGObject` method.
- */
- @uiToggle()
- @onChange(ThreeSVGRendererPlugin.prototype.setDirty)
- autoMakeSvgObjects = true
-
- readonly renderer = new SVGRenderer()
- readonly svgNodeContainer = document.createElement('div')
- protected readonly _fillPass: FillPass
-
- setDirty(...args: any[]): any {
- if (args[0] === 'enabled') {
- const last = args[2]
- const current = args[1]
- if (last !== current && this._meshes?.size) {
- this._toggleMaterialRendering([...this._meshes.values()], !current)
- }
- if (this.svgNodeContainer) {
- this.svgNodeContainer.style.display = current ? '' : 'none'
- }
- }
- this._viewer?.setDirty()
- }
-
- /**
- * @param enabled
- * @param autoAddToContainer - automatically add the svg to the viewer container and style it same as the viewer is position is absolute
- */
- constructor(enabled = true, readonly autoAddToContainer = true) {
- super()
- this._onResize = this._onResize.bind(this)
- this._onMeshDispose = this._onMeshDispose.bind(this)
- this._onMeshUpdate = this._onMeshUpdate.bind(this)
- this.updateMeshes = this.updateMeshes.bind(this)
- this.enabled = enabled
- this.svgNodeContainer.style.position = 'absolute'
- this.svgNodeContainer.style.display = 'none'
- // This pass will draw fills for meshes using the three.js material color
- this._fillPass = new FillPass()
-
- // This pass will draw visible contours of meshes on top of fills
- // using black color and solid line of width 1
- const visibleChainPass = new VisibleChainPass({
- // color: '#000000',
- // width: 1,
- })
-
- // This pass will draw hidden contours on top of visible and fills
- // using red color, dash line of width 1
- const hiddenChainPass = new HiddenChainPass({
- // color: '#FF0000',
- // width: 1,
- // dasharray: '2,2',
- })
-
- this.renderer.addPass(this._fillPass)
- this.renderer.addPass(visibleChainPass)
- this.renderer.addPass(hiddenChainPass)
-
- Vertex.MAX_LOOP = 10000 // todo; this is for large models that get stuck.
- // this.renderer.addPass(new SingularityPointPass())
- // this.renderer.addPass(new TexturePass())
- }
-
- protected _lastStyles?: string = undefined
- onAdded(viewer: ThreeViewer) {
- super.onAdded(viewer)
- // this.renderer.setSize(viewer.canvas.clientWidth, viewer.canvas.clientHeight)
- // this._refreshParams() // this is done before rendering
- if (this.autoAddToContainer) {
- viewer.container.prepend(this.svgNodeContainer) // behind the canvas so that we get pointer events and see other stuff
- this.svgNodeContainer.style.pointerEvents = 'none'
- const canvasStyles = getComputedStyle(viewer.canvas)
- if (canvasStyles.position === 'absolute') {
- this._lastStyles = this.svgNodeContainer.style.cssText
- // copy styles from canvas to svg so it looks the same.
- this.svgNodeContainer.style.top = canvasStyles.top
- this.svgNodeContainer.style.left = canvasStyles.left
- this.svgNodeContainer.style.width = canvasStyles.width
- this.svgNodeContainer.style.height = canvasStyles.height
- // this.svgNodeContainer.style.zIndex = '999999' // svg should be behind the canvas
- } else {
- this._viewer?.console.warn('ThreeSVGRendererPlugin: canvas position should be absolute for proper rendering')
- }
- viewer.renderManager.addEventListener('resize', this._onResize)
- }
- this.svgNodeContainer.style.display = this.enabled ? '' : 'none'
- viewer.scene.modelRoot.addEventListener('objectUpdate', this.updateMeshes)
- }
-
- private _meshesNeedsUpdate = true
- updateMeshes() {
- console.log('updateMeshes')
- this._meshesNeedsUpdate = true
- }
-
- onRemove(viewer: ThreeViewer) {
- super.onRemove(viewer)
- if (this.autoAddToContainer) {
- viewer.container.removeChild(this.svgNodeContainer)
- }
- if (this._lastStyles !== undefined) {
- this.svgNodeContainer.style.cssText = this._lastStyles
- this._lastStyles = undefined
- }
- viewer.renderManager.removeEventListener('resize', this._onResize)
- this._meshes.clear() // ?
- this.svgNodeContainer.style.display = 'none'
- viewer.scene.modelRoot.removeEventListener('objectUpdate', this.updateMeshes)
- }
-
- protected _onMeshDispose(ev: any) {
- if (!ev.target) return
- const mesh = ev.target as Mesh
- const svgMesh = this._meshes.get(mesh.uuid)
- if (!svgMesh) return
- svgMesh.dispose()
- this._meshes.delete(mesh.uuid)
- mesh.removeEventListener('dispose', this._onMeshDispose)
- mesh.removeEventListener('objectUpdate', this._onMeshUpdate)
- }
- protected _onMeshUpdate(ev: any) {
- if (!ev.target) return
- const mesh = ev.target as Mesh
- const svgMesh = this._meshes.get(mesh.uuid)
- if (!svgMesh) return
- svgMesh.updateObject()
- }
- protected _meshes = new Map<string, SVGMesh>()
- protected _refreshMeshes(root?: IObject3D) {
- if (!this.autoMakeSvgObjects) return []
- if (!root && this._viewer) root = this._viewer.scene.modelRoot
- if (!root) return []
- root.traverse(o=>{
- this.makeSVGObject(o)
- })
- }
-
- makeSVGObject(o: IObject3D) {
- if (!(o.isMesh && !this._meshes.has(o.uuid))) return
- const ud = o.userData
- o.userData = {}
- const svgMesh = new SVGMesh(o as any as Mesh)
- o.userData = ud
- this._meshes.set(o.uuid, svgMesh)
- this._toggleMaterialRendering([svgMesh], !this.enabled)
- o.addEventListener('dispose', this._onMeshDispose)
- o.addEventListener('objectUpdate', this._onMeshUpdate) // todo: check if we need to do object update everytime and what actions specifically.
- this._meshesNeedsUpdate = true // because we have a new mesh
- }
-
- private _rendering = false
-
- static SVG_RENDER_TIMEOUT = 2000
-
- protected _toggleMaterialRendering(meshes: SVGMesh[], enable: boolean) {
- const materials = []
- for (const mesh of meshes) {
- materials.push(...Array.isArray(mesh.material) ? mesh.material : [mesh.material])
- }
- // enable rendering of material colors
- for (const mat of materials) {
- if (mat.colorWrite !== undefined) {
- if (enable && !mat.colorWrite) {
- mat.colorWrite = true
- delete mat.userData.forcedLinearDepth
- // mat.userData._colorWriteSet = true
- } else if (!enable /* && mat.userData._colorWriteSet*/) {
- mat.colorWrite = false
- mat.userData.forcedLinearDepth = 1 // for gbuffer plugin
- // delete mat.userData._colorWriteSet
- }
- }
- }
-
- }
-
- @uiButton()
- async render() {
- if (!this._viewer || !this._viewer.renderEnabled) return
- if (this.isDisabled()) return
- if (this._rendering) return
- this._rendering = true
- this._refreshParams()
- this._refreshMeshes()
-
- const meshes = [...this._meshes.values()]
- // todo: make sure all meshes are in the scene
- // todo only use meshes that should be rendered.
- if (!meshes.length) {
- this._rendering = false
- return ''
- }
- const camera = this._viewer.scene.mainCamera as PerspectiveCamera2
- try {
- if (this.drawImageFills) {
- this._toggleMaterialRendering(meshes, true)
- this._viewer.setDirty()
- this._viewer.canvas.style.opacity = '0'
- await this._viewer.doOnce('preFrame') // because we are already in postRender or postFrame.
- await this._viewer.doOnce('postFrame') // todo wait for progressive also maybe
-
- await this._viewer.doOnce('postFrame') // once more
- this._fillPass.options.fillImage = this._viewer.canvas.toDataURL('image/png')
-
- // disable rendering of material colors
- this._toggleMaterialRendering(meshes, false)
-
- this._viewer.setDirty()
- await this._viewer.doOnce('preFrame') // already in postFrame
- await this._viewer.doOnce('postFrame')
- this._viewer.canvas.style.opacity = '1'
- }
-
- // this._fillPass.options.fillImage = this._viewer.canvas.toDataURL('image/png')
- // this._viewer.renderEnabled = false
- this.renderer.viewmap.skipActions = false
- const svgPromise = this.renderer.generateSVG(meshes, camera, {
- w: this._viewer.canvas.width,
- h: this._viewer.canvas.height,
- })
- let svgResolved = false
- timeout(ThreeSVGRendererPlugin.SVG_RENDER_TIMEOUT).then(()=>{ // todo: make support in libs to cancel the promise. this will just wait for an action to complete.
- if (!svgResolved) {
- console.warn('timeout')
- this.renderer.viewmap.skipActions = true
- }
- })
- const svg = await svgPromise
- svgResolved = true
- this.renderer.viewmap.skipActions = false
- // this._viewer.renderEnabled = true
- this.svgNodeContainer.innerHTML = svg.node.outerHTML
- this._rendering = false
- return svg.svg()
- } catch (e) {
- console.error(e)
- }
- this._rendering = false
- return ''
- }
-
- @uiButton()
- download() {
- const svg = this.svgNodeContainer.innerHTML
- const blob = new Blob([svg], {type: 'image/svg+xml'})
- this._viewer?.exportBlob(blob, 'scene.svg')
- }
-
- protected _viewerListeners = {
- postRender: (_: IViewerEvent)=>{
- if (this.autoRender) this.render()
- },
- }
-
- get svgNode() {
- if (!this.svgNodeContainer.children.length) return undefined
- if (this.svgNodeContainer.children.length > 1) {
- this._viewer?.console.warn('ThreeSVGRenderer: Multiple svg nodes in container, should not be possible')
- }
- return this.svgNodeContainer.children[0]
- }
-
- protected _refreshParams() {
- if (this.isDisabled()) return
- if (this._meshesNeedsUpdate) {
- this.renderer.viewmap.options.updateMeshes = true
- this._meshesNeedsUpdate = false
- } else this.renderer.viewmap.options.updateMeshes = this.alwaysUpdateMeshes
- this.renderer.viewmap.options.creaseAngle.min = this.creaseAngle.x
- this.renderer.viewmap.options.creaseAngle.max = this.creaseAngle.y
-
- const passes = this.renderer.drawHandler.passes
- const fillPass = passes.find(p=>p instanceof FillPass)!
- const visibleContourPass = passes.find(p=>p instanceof VisibleChainPass)!
- const hiddenContourPass = passes.find(p=>p instanceof HiddenChainPass)!
- if (fillPass) {
- fillPass.enabled = this.drawPolygons && (this.drawPolygonFills || this.drawPolygonStrokes || this.drawImageFills)
- fillPass.drawFills = this.drawPolygonFills
- fillPass.drawStrokes = this.drawPolygonStrokes
- fillPass.drawImageFills = this.drawImageFills
- }
- if (visibleContourPass) {
- visibleContourPass.enabled = this.drawVisibleContours
- }
- if (hiddenContourPass) {
- hiddenContourPass.enabled = this.drawHiddenContours
- }
-
- }
- protected _onResize() {
- if (!this._viewer) return
- // this.renderer.setSize(this._viewer.canvas.clientWidth, this._viewer.canvas.clientHeight)
- }
-
- }
-
-
- // adding here since they dont show up in dependencies.txt somehow
- /**
- * @license
- * three-svg-renderer
- *
- * GNU GENERAL PUBLIC LICENSE
- * Version 3, 29 June 2007
- *
- * Copyright (c) 2022 Axel Antoine
- */
- /**
- * @license
- * three-mesh-halfedge
- *
- * MIT License
- *
- * Copyright (c) 2022 Axel Antoine
- */
|