import { DataTexture , LuminanceFormat , UnsignedByteType , ClampToEdgeWrapping } from "three" ; import { LinearFilter } from "three/src/constants" ; import Channel from "./Channel" ; import { FuseChannel } from "./types" ; export default class FusedChannelData { private width : number ; private height : number ; private fused : Uint8ClampedArray ; public fusedTexture : DataTexture ; public maskTexture : DataTexture ; private maskChannelLoaded : boolean ; private maskChannelIndex : number ; private useSingleThread : boolean ; private fuseWorkersWorking : number ; private isFusing : boolean ; private workers : Worker [ ] ; private workersCount : number ; private fuseRequested : FuseChannel [ ] | null ; private fuseMethodRequested : string ; private channelsDataToFuse : Channel [ ] ; constructor ( atlasX , atlasY ) { this . width = atlasX ; this . height = atlasY ; this . fused = new Uint8ClampedArray ( this . width * this . height * 4 ) ; this . fusedTexture = new DataTexture ( this . fused , this . width , this . height ) ; this . fusedTexture . generateMipmaps = false ; this . fusedTexture . magFilter = LinearFilter ; this . fusedTexture . minFilter = LinearFilter ; this . fusedTexture . wrapS = ClampToEdgeWrapping ; this . fusedTexture . wrapT = ClampToEdgeWrapping ; this . maskTexture = new DataTexture ( new Uint8ClampedArray ( this . width * this . height ) , this . width , this . height , LuminanceFormat , UnsignedByteType ) ; this . maskTexture . generateMipmaps = false ; this . maskTexture . magFilter = LinearFilter ; this . maskTexture . minFilter = LinearFilter ; this . maskTexture . wrapS = ClampToEdgeWrapping ; this . maskTexture . wrapT = ClampToEdgeWrapping ; this . maskTexture . unpackAlignment = 1 ; this . maskChannelLoaded = false ; this . maskChannelIndex = 0 ; this . useSingleThread = true ; this . fuseWorkersWorking = 0 ; this . isFusing = false ; this . fuseRequested = null ; this . fuseMethodRequested = "" ; this . channelsDataToFuse = [ ] ; this . workers = [ ] ; this . workersCount = 0 ; this . setupWorkers ( ) ; } static onFuseComplete ( ) { } static setOnFuseComplete ( onFuseComplete ) { FusedChannelData . onFuseComplete = onFuseComplete ; } public cleanup ( ) { if ( this . workers && this . workers . length > 0 ) { for ( let i = 0 ; i < this . workers . length ; ++ i ) { this . workers [ i ] . onmessage = null ; this . workers [ i ] . terminate ( ) ; } } this . workers = [ ] ; this . workersCount = 0 ; this . fusedTexture . dispose ( ) ; this . maskTexture . dispose ( ) ; } private setupWorkers ( ) { if ( this . useSingleThread || ! window . Worker ) { this . workersCount = 0 ; return ; } const npx = this . height * this . width ; this . workersCount = 4 ; this . fuseWorkersWorking = 0 ; const onWorkEnded = ( e ) => { this . fuseWorkersWorking ++ ; this . fused . set ( e . data . data , Math . floor ( e . data . workerindex * ( npx / this . workersCount ) ) * 4 ) ; if ( this . fuseWorkersWorking === this . workersCount ) { this . fusedTexture . image = { data : this . fused , width : this . width , height : this . height } ; this . fusedTexture . needsUpdate = true ; this . fuseWorkersWorking = 0 ; if ( FusedChannelData . onFuseComplete ) { FusedChannelData . onFuseComplete ( ) ; } this . isFusing = false ; if ( this . fuseRequested ) { this . fuse ( this . fuseRequested , this . fuseMethodRequested , this . channelsDataToFuse ) ; } this . fuseRequested = null ; this . channelsDataToFuse = [ ] ; } } ; this . workers = [ ] ; for ( let index = 0 ; index < this . workersCount ; index ++ ) { const worker = new Worker ( new URL ( "./FuseWorker.js" , import . meta . url ) ) ; worker . onmessage = onWorkEnded ; worker . onerror = function ( e ) { alert ( "Error: Line " + e . lineno + " in " + e . filename + ": " + e . message ) ; } ; this . workers . push ( worker ) ; } } public onChannelLoaded ( batch , channels ) { if ( this . useSingleThread || ! window . Worker ) { return ; } const npx = this . height * this . width ; for ( let i = 0 ; i < this . workersCount ; ++ i ) { for ( let j = 0 ; j < batch . length ; ++ j ) { const channelIndex = batch [ j ] ; const arr = channels [ channelIndex ] . imgData . data . buffer . slice ( Math . floor ( i * ( npx / this . workersCount ) ) , Math . floor ( ( i + 1 ) * ( npx / this . workersCount ) - 1 ) ) ; const workerData = { msgtype : "channeldata" , channelindex : channelIndex , workerindex : i , w : this . width , h : this . height , data : arr , } ; this . workers [ i ] . postMessage ( workerData , [ workerData . data ] ) ; } } } fuse ( combination , fuseMethod , channels ) { fuseMethod = fuseMethod || "m" ; let canFuse = false ; for ( let i = 0 ; i < combination . length ; ++ i ) { const c = combination [ i ] ; const idx = c . chIndex ; if ( channels [ idx ] . loaded ) { c . lut = channels [ idx ] . combineLuts ( c . rgbColor , c . lut ) ; canFuse = true ; } } if ( ! canFuse ) { return ; } if ( this . useSingleThread || ! window . Worker ) { this . singleThreadedFuse ( combination , channels ) ; if ( FusedChannelData . onFuseComplete ) { FusedChannelData . onFuseComplete ( ) ; } return ; } if ( this . isFusing ) { this . fuseRequested = combination ; this . channelsDataToFuse = channels ; this . fuseMethodRequested = fuseMethod ; return ; } this . fuseWorkersWorking = 0 ; this . isFusing = true ; for ( let index = 0 ; index < this . workersCount ; index ++ ) { this . workers [ index ] . postMessage ( { msgtype : "fuse" , combination : combination , fuseMethod : fuseMethod , } ) ; } } private singleThreadedFuse ( combination , channels ) { let ar , ag , ab , aa , c , lr , lg , lb , la , opacity , channeldata ; let cx , fx , idx ; const cl = combination . length ; const npx4 = this . height * this . width * 4 ; const npx = this . height * this . width ; const fused = this . fused ; for ( let x = 0 ; x < npx4 ; x += 4 ) { fused [ x + 0 ] = 0 ; fused [ x + 1 ] = 0 ; fused [ x + 2 ] = 0 ; fused [ x + 3 ] = 255 ; } let value = 0 ; for ( let i = 0 ; i < cl ; ++ i ) { c = combination [ i ] ; idx = c . chIndex ; if ( ! channels [ idx ] . loaded ) { continue ; } if ( c . rgbColor ) { channeldata = channels [ idx ] . imgData . data ; for ( cx = 0 , fx = 0 ; cx < npx ; cx += 1 , fx += 4 ) { value = channeldata [ cx ] ; lr = c . lut [ value * 4 + 0 ] ; lg = c . lut [ value * 4 + 1 ] ; lb = c . lut [ value * 4 + 2 ] ; la = c . lut [ value * 4 + 3 ] ; opacity = la / 255.0 ; ar = fused [ fx + 0 ] ; fused [ fx + 0 ] = Math . max ( ar , lr * opacity ) ; ag = fused [ fx + 1 ] ; fused [ fx + 1 ] = Math . max ( ag , lg * opacity ) ; ab = fused [ fx + 2 ] ; fused [ fx + 2 ] = Math . max ( ab , lb * opacity ) ; aa = fused [ fx + 3 ] ; fused [ fx + 3 ] = Math . max ( aa , la ) ; } } } for ( let x = 0 ; x < npx4 ; x += 4 ) { fused [ x + 0 ] = Math . min ( fused [ x + 0 ] , 255 ) ; fused [ x + 1 ] = Math . min ( fused [ x + 1 ] , 255 ) ; fused [ x + 2 ] = Math . min ( fused [ x + 2 ] , 255 ) ; } this . fusedTexture . image = { data : this . fused , width : this . width , height : this . height , } ; this . fusedTexture . needsUpdate = true ; } public setChannelAsMask ( idx , channel ) { if ( ! channel || ! channel . loaded ) { return false ; } const datacopy = channel . imgData . data . buffer . slice ( 0 ) ; const maskData = { data : new Uint8ClampedArray ( datacopy ) , width : this . width , height : this . height , } ; this . maskTexture . image = maskData ; this . maskTexture . needsUpdate = true ; this . maskChannelLoaded = true ; this . maskChannelIndex = idx ; return true ; } }