@threepipe/plugin-svg-renderer
Version:
Plugins for SVG Rendering of 3d objects for Threepipe
406 lines (368 loc) • 15.1 kB
text/typescript
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)
*/
export class ThreeSVGRendererPlugin extends AViewerPluginSync {
static readonly PluginType = 'ThreeSVGRendererPlugin'
enabled = true
/**
* Automatically render when camera or any object changes.
*/
autoRender = true
/**
* Use the fill pass to draw polygons.(both fills and strokes)
*/
drawPolygons = true
/**
* Draw polygon fills. (fill color from material.color)
*/
drawPolygonFills = true
/**
* Draw polygon strokes. (stroke color from material.color)
*/
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.
*/
drawImageFills = false
/**
* Draw visible contours of meshes.
*/
drawVisibleContours = true
/**
* Draw hidden contours of meshes.
*/
drawHiddenContours = true
/**
* Update meshes on every render. If this is false, meshes will only be updated when they change. (tracked using objectUpdate event)
*/
alwaysUpdateMeshes = true
/**
* Min and Max Crease angle for mesh edges.
*/
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.
*/
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
}
}
}
}
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 ''
}
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
*/