import { BufferGeometry , Euler , Object3D , Vector3 , Color , Mesh , Group , Material , MeshPhongMaterial , Plane , DoubleSide , } from "three" ; import { STLExporter } from "three/examples/jsm/exporters/STLExporter" ; import { GLTFExporter } from "three/examples/jsm/exporters/GLTFExporter" ; import { defaultMaterialSettings } from "./constants/materials" ; import FileSaver from "./FileSaver" ; import NaiveSurfaceNets from "./NaiveSurfaceNets.js" ; import MarchingCubes from "./MarchingCubes" ; import Volume from "./Volume" ; import { Bounds } from "./types.js" ; import { ThreeJsPanel } from "./ThreeJsPanel.js" ; const ALPHA_THRESHOLD = 0.9 ; export default class MeshVolume { private volume : Volume ; private meshRoot : Object3D ; private meshPivot : Group ; private meshrep : ( Group | null ) [ ] ; private channelColors : [ number , number , number ] [ ] ; private channelOpacities : number [ ] ; private bounds : Bounds ; private scale : Vector3 ; constructor ( volume ) { this . volume = volume ; this . meshRoot = new Object3D ( ) ; this . meshRoot . name = "Mesh Surface Group" ; this . meshPivot = new Group ( ) ; this . meshPivot . name = "MeshContainerNode" ; this . meshPivot . add ( this . meshRoot ) ; this . meshrep = [ ] ; this . channelColors = [ ] ; this . channelOpacities = [ ] ; this . scale = new Vector3 ( 1 , 1 , 1 ) ; this . bounds = { bmin : new Vector3 ( - 0.5 , - 0.5 , - 0.5 ) , bmax : new Vector3 ( 0.5 , 0.5 , 0.5 ) , } ; } cleanup ( ) { for ( let i = 0 ; i < this . volume . num_channels ; ++ i ) { this . destroyIsosurface ( i ) ; } } setVisible ( isVisible ) { this . meshRoot . visible = isVisible ; } doRender ( _canvas ) { } get3dObject ( ) { return this . meshPivot ; } onChannelData ( batch ) { for ( let j = 0 ; j < batch . length ; ++ j ) { const idx = batch [ j ] ; if ( this . meshrep [ idx ] ) { const isovalue = this . getIsovalue ( idx ) ; this . updateIsovalue ( idx , isovalue === undefined ? 127 : isovalue ) ; } } } setScale ( scale ) { this . scale = scale ; this . meshRoot . scale . copy ( new Vector3 ( 0.5 * scale . x , 0.5 * scale . y , 0.5 * scale . z ) ) ; } setFlipAxes ( flipX , flipY , flipZ ) { this . meshRoot . scale . copy ( new Vector3 ( 0.5 * this . scale . x * flipX , 0.5 * this . scale . y * flipY , 0.5 * this . scale . z * flipZ ) ) ; } setTranslation ( vec3xyz ) { this . meshPivot . position . copy ( vec3xyz ) ; } setRotation ( eulerXYZ ) { this . meshPivot . rotation . copy ( eulerXYZ ) ; this . updateClipFromBounds ( ) ; } setResolution ( _x , _y ) { } setOrthoThickness ( _value ) { } setAxisClip ( axis , minval , maxval , _isOrthoAxis ) { this . bounds . bmax [ axis ] = maxval ; this . bounds . bmin [ axis ] = minval ; this . updateClipFromBounds ( ) ; } updateMeshColors ( channelColors ) { this . channelColors = channelColors ; for ( let i = 0 ; i < this . volume . num_channels ; ++ i ) { const meshrep = this . meshrep [ i ] ; if ( meshrep ) { const rgb = channelColors [ i ] ; const c = ( rgb [ 0 ] << 16 ) | ( rgb [ 1 ] << 8 ) | rgb [ 2 ] ; meshrep . traverse ( function ( child ) { if ( child instanceof Mesh ) { child . material . color = new Color ( c ) ; } } ) ; } } } createMaterialForChannel ( rgb , alpha , _transp ) { const col = ( rgb [ 0 ] << 16 ) | ( rgb [ 1 ] << 8 ) | rgb [ 2 ] ; const material = new MeshPhongMaterial ( { color : new Color ( col ) , shininess : defaultMaterialSettings . shininess , specular : new Color ( defaultMaterialSettings . specularColor ) , opacity : alpha , transparent : alpha < ALPHA_THRESHOLD , side : DoubleSide , } ) ; return material ; } createMeshForChannel ( channelIndex , colorrgb , isovalue , alpha , transp ) { const geometries = this . generateIsosurfaceGeometry ( channelIndex , isovalue ) ; const material = this . createMaterialForChannel ( colorrgb , alpha , transp ) ; const theObject = new Group ( ) ; theObject . name = "Channel" + channelIndex ; theObject . userData = { isovalue : isovalue } ; for ( let i = 0 ; i < geometries . length ; ++ i ) { const mesh = new Mesh ( geometries [ i ] , material ) ; theObject . add ( mesh ) ; } return theObject ; } updateIsovalue ( channel , value ) { const meshrep = this . meshrep [ channel ] ; if ( ! meshrep ) { return ; } if ( meshrep . userData . isovalue === value ) { return ; } const opacity = this . channelOpacities [ channel ] ; const color = this . channelColors [ channel ] ; this . destroyIsosurface ( channel ) ; const newmeshrep = this . createMeshForChannel ( channel , color , value , opacity , false ) ; this . meshrep [ channel ] = newmeshrep ; this . meshRoot . add ( newmeshrep ) ; } getIsovalue ( channel ) { const meshrep = this . meshrep [ channel ] ; if ( ! meshrep ) { return undefined ; } return meshrep . userData . isovalue ; } updateClipRegion ( xmin , xmax , ymin , ymax , zmin , zmax ) { this . bounds = { bmin : new Vector3 ( xmin - 0.5 , ymin - 0.5 , zmin - 0.5 ) , bmax : new Vector3 ( xmax - 0.5 , ymax - 0.5 , zmax - 0.5 ) , } ; this . updateClipFromBounds ( ) ; } updateClipFromBounds ( ) { const xmin = this . bounds . bmin . x ; const ymin = this . bounds . bmin . y ; const zmin = this . bounds . bmin . z ; const xmax = this . bounds . bmax . x ; const ymax = this . bounds . bmax . y ; const zmax = this . bounds . bmax . z ; const euler = this . meshPivot . rotation ; for ( let channel = 0 ; channel < this . meshrep . length ; ++ channel ) { const meshrep = this . meshrep [ channel ] ; if ( ! meshrep ) { continue ; } const planes = [ ] ; if ( xmin > - 0.5 ) { planes . push ( new Plane ( new Vector3 ( 1 , 0 , 0 ) . applyEuler ( euler ) , this . meshRoot . position . x + - xmin * this . scale . x ) ) ; } if ( ymin > - 0.5 ) { planes . push ( new Plane ( new Vector3 ( 0 , 1 , 0 ) . applyEuler ( euler ) , this . meshRoot . position . y + - ymin * this . scale . y ) ) ; } if ( zmin > - 0.5 ) { planes . push ( new Plane ( new Vector3 ( 0 , 0 , 1 ) . applyEuler ( euler ) , this . meshRoot . position . z + - zmin * this . scale . z ) ) ; } if ( xmax < 0.5 ) { planes . push ( new Plane ( new Vector3 ( - 1 , 0 , 0 ) . applyEuler ( euler ) , this . meshRoot . position . x + xmax * this . scale . x ) ) ; } if ( ymax < 0.5 ) { planes . push ( new Plane ( new Vector3 ( 0 , - 1 , 0 ) . applyEuler ( euler ) , this . meshRoot . position . y + ymax * this . scale . y ) ) ; } if ( zmax < 0.5 ) { planes . push ( new Plane ( new Vector3 ( 0 , 0 , - 1 ) . applyEuler ( euler ) , this . meshRoot . position . z + zmax * this . scale . z ) ) ; } meshrep . traverse ( function ( child ) { if ( child instanceof Mesh ) { child . material . clippingPlanes = planes ; } } ) ; } } updateOpacity ( channel , value ) { this . channelOpacities [ channel ] = value ; const meshrep = this . meshrep [ channel ] ; if ( ! meshrep ) { return ; } meshrep . traverse ( function ( child ) { if ( child instanceof Mesh ) { child . material . opacity = value ; child . material . transparent = value < ALPHA_THRESHOLD ; } } ) ; } hasIsosurface ( channel ) { return ! ! this . meshrep [ channel ] ; } createIsosurface ( channel , color , value , alpha , transp ) { if ( ! this . meshrep [ channel ] ) { if ( value === undefined ) { value = 127 ; } if ( alpha === undefined ) { alpha = 1.0 ; } if ( transp === undefined ) { transp = alpha < ALPHA_THRESHOLD ; } const meshrep = this . createMeshForChannel ( channel , color , value , alpha , transp ) ; this . meshrep [ channel ] = meshrep ; this . channelOpacities [ channel ] = alpha ; this . channelColors [ channel ] = color ; this . meshRoot . add ( meshrep ) ; } } destroyIsosurface ( channel ) { const meshrep = this . meshrep [ channel ] ; if ( meshrep ) { this . meshRoot . remove ( meshrep ) ; meshrep . traverse ( function ( child ) { if ( child instanceof Mesh ) { child . material . dispose ( ) ; child . geometry . dispose ( ) ; } } ) ; this . meshrep [ channel ] = null ; } } saveChannelIsosurface ( channelIndex , type , namePrefix ) { const meshrep = this . meshrep [ channelIndex ] ; if ( ! meshrep ) { return ; } if ( type === "STL" ) { this . exportSTL ( meshrep , namePrefix + "_" + this . volume . channel_names [ channelIndex ] ) ; } else if ( type === "GLTF" ) { const prevviz = [ ] ; for ( let i = 0 ; i < this . meshrep . length ; ++ i ) { const meshrepi = this . meshrep [ i ] ; if ( meshrepi ) { prevviz [ i ] = meshrepi . visible ; meshrepi . visible = i === channelIndex ; } } this . exportGLTF ( this . meshRoot , namePrefix + "_" + this . volume . channel_names [ channelIndex ] ) ; for ( let i = 0 ; i < this . meshrep . length ; ++ i ) { const meshrepi = this . meshrep [ i ] ; if ( meshrepi ) { meshrepi . visible = prevviz [ i ] ; } } } } exportSTL ( input , fname ) { const ex = new STLExporter ( ) ; const output = ex . parse ( input , { binary : true } ) ; FileSaver . saveBinary ( ( output as unknown as DataView ) . buffer , fname + ".stl" ) ; } exportGLTF ( input , fname ) { const gltfExporter = new GLTFExporter ( ) ; const options = { trs : false , onlyVisible : true , truncateDrawRange : true , binary : true , forceIndices : false , forcePowerOfTwoTextures : true , } ; gltfExporter . parse ( input , function ( result ) { if ( result instanceof ArrayBuffer ) { FileSaver . saveArrayBuffer ( result , fname + ".glb" ) ; } else { const output = JSON . stringify ( result , null , 2 ) ; FileSaver . saveString ( output , fname + ".gltf" ) ; } } , options ) ; } generateIsosurfaceGeometry ( channelIndex , isovalue ) { if ( ! this . volume ) { return [ ] ; } const volumeData = this . volume . channels [ channelIndex ] . volumeData ; const marchingcubes = true ; if ( marchingcubes ) { const effect = new MarchingCubes ( [ this . volume . x , this . volume . y , this . volume . z ] , new Material ( ) , false , false , true , volumeData ) ; effect . position . copy ( this . meshRoot . position ) ; effect . scale . set ( 0.5 * this . scale . x , 0.5 * this . scale . y , 0.5 * this . scale . z ) ; effect . isovalue = isovalue ; const geometries = effect . generateGeometry ( ) ; return geometries || [ ] ; } else { const result = NaiveSurfaceNets . surfaceNets ( volumeData , [ this . volume . x , this . volume . y , this . volume . z ] , isovalue ) ; return NaiveSurfaceNets . constructTHREEGeometry ( result ) ; } } }