/**
 * 3D Foundation Project
 * Copyright 2019 Smithsonian Institution
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */ import { Vector3 , Quaternion , Box3 , Mesh , Group , Matrix4 , Box3Helper , Object3D , FrontSide , BackSide , DoubleSide } from "three" ; import Notification from "@ff/ui/Notification" ; import { ITypedEvent , Node , types } from "@ff/graph/Component" ; import CObject3D from "@ff/scene/components/CObject3D" ; import * as helpers from "@ff/three/helpers" ; import { IDocument , INode } from "client/schema/document" ; import { EDerivativeQuality , EDerivativeUsage , EUnitType , IModel , ESideType , TSideType } from "client/schema/model" ; import unitScaleFactor from "../utils/unitScaleFactor" ; import UberPBRMaterial , { EShaderMode } from "../shaders/UberPBRMaterial" ; import Derivative from "../models/Derivative" ; import DerivativeList from "../models/DerivativeList" ; import CVAnnotationView from "./CVAnnotationView" ; import CVAssetManager from "./CVAssetManager" ; import CVAssetReader from "./CVAssetReader" ; import { Vector3 as LocalVector3 } from "client/schema/common" ; import CRenderer from "@ff/scene/components/CRenderer" ; import CVEnvironment from "./CVEnvironment" ; const _vec3a = new Vector3 ( ) ; const _vec3b = new Vector3 ( ) ; const _quat = new Quaternion ( ) ; const _box = new Box3 ( ) ; export interface ITagUpdateEvent extends ITypedEvent < "tag-update" > { } /**
 * Graph component rendering a model or model part.
 *
 * ### Events
 * - *"bounding-box"* - emitted after the model's bounding box changed
 */ export default class CVModel2 extends CObject3D { static readonly typeName : string = "CVModel2" ; static readonly text : string = "Model" ; static readonly icon : string = "cube" ; static readonly rotationOrder = "ZYX" ; protected static readonly ins = { globalUnits : types . Enum ( "Model.GlobalUnits" , EUnitType , EUnitType . cm ) , localUnits : types . Enum ( "Model.LocalUnits" , EUnitType , EUnitType . cm ) , quality : types . Enum ( "Model.Quality" , EDerivativeQuality , EDerivativeQuality . High ) , tags : types . String ( "Model.Tags" ) , renderOrder : types . Number ( "Model.RenderOrder" , 0 ) , shadowSide : types . Enum ( "Model.ShadowSide" , ESideType , ESideType . Back ) , activeTags : types . String ( "Model.ActiveTags" ) , autoLoad : types . Boolean ( "Model.AutoLoad" , true ) , position : types . Vector3 ( "Model.Position" ) , rotation : types . Vector3 ( "Model.Rotation" ) , center : types . Event ( "Model.Center" ) , shader : types . Enum ( "Material.Shader" , EShaderMode , EShaderMode . Default ) , override : types . Boolean ( "Material.Override" , false ) , color : types . ColorRGB ( "Material.BaseColor" ) , opacity : types . Percent ( "Material.Opacity" , 1.0 ) , hiddenOpacity : types . Percent ( "Material.HiddenOpacity" , 0.0 ) , roughness : types . Percent ( "Material.Roughness" , 0.8 ) , metalness : types . Percent ( "Material.Metalness" , 0.1 ) , occlusion : types . Percent ( "Material.Occlusion" , 0.25 ) , doubleSided : types . Boolean ( "Material.DoubleSided" , false ) , dumpDerivatives : types . Event ( "Derivatives.Dump" ) , } ; protected static readonly outs = { unitScale : types . Number ( "UnitScale" , { preset : 1 , precision : 5 } ) , quality : types . Enum ( "LoadedQuality" , EDerivativeQuality ) , updated : types . Event ( "Updated" ) , } ; ins = this . addInputs < CObject3D , typeof CVModel2 . ins > ( CVModel2 . ins ) ; outs = this . addOutputs < CObject3D , typeof CVModel2 . outs > ( CVModel2 . outs ) ; get settingProperties ( ) { return [ this . ins . visible , this . ins . quality , this . ins . localUnits , this . ins . tags , this . ins . renderOrder , this . ins . shadowSide , this . ins . shader , this . ins . override , this . ins . color , this . ins . opacity , this . ins . hiddenOpacity , this . ins . roughness , this . ins . metalness , this . ins . occlusion , this . ins . doubleSided ] ; } private _derivatives = new DerivativeList ( ) ; private _activeDerivative : Derivative = null ; private _visible : boolean = true ; private _boxFrame : Mesh = null ; private _localBoundingBox = new Box3 ( ) ; constructor ( node , id ) { super ( node , id ) ; this . object3D = new Group ( ) ; } get derivatives ( ) { return this . _derivatives ; } get activeDerivative ( ) { return this . _activeDerivative ; } get localBoundingBox ( ) : Readonly < Box3 > { return this . _localBoundingBox ; } protected get assetManager ( ) { return this . getMainComponent ( CVAssetManager ) ; } protected get assetReader ( ) { return this . getMainComponent ( CVAssetReader ) ; } protected get renderer ( ) { return this . getMainComponent ( CRenderer ) ; } create ( ) { super . create ( ) ; const av = this . node . createComponent ( CVAnnotationView ) ; av . ins . unitScale . linkFrom ( this . outs . unitScale ) ; const maxTextureSize = this . renderer . outs . maxTextureSize . value ; if ( maxTextureSize < 2048 ) { this . ins . quality . setValue ( EDerivativeQuality . Low ) ; } else if ( maxTextureSize < 4096 ) { this . ins . quality . setValue ( EDerivativeQuality . Medium ) ; } else { this . ins . quality . setValue ( EDerivativeQuality . High ) ; } } update ( ) { const ins = this . ins ; if ( ins . tags . changed || ins . activeTags . changed || ins . visible . changed ) { let visible = ins . visible . value ; if ( visible ) { const tags = ins . tags . value . split ( "," ) . map ( tag => tag . trim ( ) ) . filter ( tag => tag ) ; const activeTags = ins . activeTags . value . split ( "," ) . map ( tag => tag . trim ( ) ) . filter ( tag => tag ) ; visible = ! tags . length ; activeTags . forEach ( activeTag => { if ( tags . indexOf ( activeTag ) >= 0 ) { visible = true ; } } ) ; } const overrideActive = this . ins . override . value ; this . _visible = visible ; if ( visible ) { this . object3D . visible = true ; if ( overrideActive ) { this . updateMaterial ( ) ; } } else if ( ins . visible . value && overrideActive && this . ins . hiddenOpacity . value > 0 ) { this . object3D . visible = true ; this . updateMaterial ( ) ; } else { this . object3D . visible = false ; } } if ( ins . tags . changed ) { this . emit < ITagUpdateEvent > ( { type : "tag-update" } ) ; } if ( ! this . activeDerivative && ins . autoLoad . changed && ins . autoLoad . value ) { this . autoLoad ( ins . quality . value ) ; } else if ( ins . quality . changed ) { const derivative = this . derivatives . select ( EDerivativeUsage . Web3D , ins . quality . value ) ; if ( derivative && derivative !== this . activeDerivative ) { this . loadDerivative ( derivative ) . catch ( error => { console . warn ( "Model.update - failed to load derivative" ) ; console . warn ( error ) ; } ) ; } } if ( ins . renderOrder . changed ) { this . updateRenderOrder ( this . object3D , ins . renderOrder . value ) ; } if ( ins . localUnits . changed || ins . globalUnits . changed ) { this . updateUnitScale ( ) ; } if ( ins . shader . changed ) { this . updateShader ( ) ; } if ( ins . override . value && ins . shader . value === EShaderMode . Default && ( ins . override . changed || ins . color . changed || ins . opacity . changed || ins . roughness . changed || ins . metalness . changed || ins . occlusion . changed ) ) { this . updateMaterial ( ) ; } if ( ins . center . changed ) { this . center ( ) ; } if ( ins . position . changed || ins . rotation . changed ) { this . updateMatrixFromProps ( ) ; } if ( ins . dumpDerivatives . changed ) { console . log ( this . derivatives . toString ( true ) ) ; } return true ; } dispose ( ) { this . derivatives . clear ( ) ; this . _activeDerivative = null ; super . dispose ( ) ; } center ( ) { const object3D = this . object3D ; const position = this . ins . position ; object3D . matrix . decompose ( _vec3a , _quat , _vec3b ) ; object3D . matrix . makeRotationFromQuaternion ( _quat ) ; _box . makeEmpty ( ) ; helpers . computeLocalBoundingBox ( object3D , _box , object3D . parent ) ; _box . getCenter ( _vec3a ) ; _vec3a . multiplyScalar ( - 1 ) . toArray ( position . value ) ; position . set ( ) ; } setFromMatrix ( matrix ) { const ins = this . ins ; matrix . decompose ( _vec3a , _quat , _vec3b ) ; _vec3a . multiplyScalar ( 1 / this . outs . unitScale . value ) . toArray ( ins . position . value ) ; ins . position . set ( ) ; helpers . quaternionToDegrees ( _quat , CVModel2 . rotationOrder , ins . rotation . value ) ; ins . rotation . set ( ) ; } fromDocument ( document , node ) { const { ins , outs } = this ; if ( ! isFinite ( node . model ) ) { throw new Error ( "model property missing in node" ) ; } const data = document . models [ node . model ] ; const units = EUnitType [ data . units || "cm" ] ; ins . localUnits . setValue ( isFinite ( units ) ? units : EUnitType . cm ) ; ins . visible . setValue ( data . visible !== undefined ? data . visible : true ) ; ins . tags . setValue ( data . tags || "" ) ; ins . renderOrder . setValue ( data . renderOrder !== undefined ? data . renderOrder : 0 ) ; const side = ESideType [ data . shadowSide || "Back" ] ; ins . shadowSide . setValue ( isFinite ( side ) ? side : ESideType . Back ) ; ins . position . reset ( ) ; ins . rotation . reset ( ) ; if ( data . translation ) { ins . position . copyValue ( data . translation ) ; } if ( data . rotation ) { _quat . fromArray ( data . rotation ) ; helpers . quaternionToDegrees ( _quat , CVModel2 . rotationOrder , ins . rotation . value ) ; ins . rotation . set ( ) ; } if ( data . boundingBox ) { const boundingBox = this . _localBoundingBox ; boundingBox . min . fromArray ( data . boundingBox . min ) ; boundingBox . max . fromArray ( data . boundingBox . max ) ; this . _boxFrame = new ( Box3Helper as any ) ( boundingBox , "#009cde" ) ; this . addObject3D ( this . _boxFrame ) ; this . _boxFrame . updateMatrixWorld ( true ) ; outs . updated . set ( ) ; } if ( data . derivatives ) { this . derivatives . fromJSON ( data . derivatives ) ; } if ( data . material ) { const material = data . material ; ins . copyValues ( { override : true , color : material . color || ins . color . schema . preset , opacity : material . opacity !== undefined ? material . opacity : ins . opacity . schema . preset , hiddenOpacity : material . hiddenOpacity !== undefined ? material . hiddenOpacity : ins . hiddenOpacity . schema . preset , roughness : material . roughness !== undefined ? material . roughness : ins . roughness . schema . preset , metalness : material . metalness !== undefined ? material . metalness : ins . metalness . schema . preset , occlusion : material . occlusion !== undefined ? material . occlusion : ins . occlusion . schema . preset , doubleSided : material . doubleSided !== undefined ? material . doubleSided : ins . doubleSided . schema . preset } ) ; } if ( data . annotations ) { this . getComponent ( CVAnnotationView ) . fromData ( data . annotations ) ; } this . emit < ITagUpdateEvent > ( { type : "tag-update" } ) ; this . ins . autoLoad . set ( ) ; return node . model ; } toDocument ( document , node ) { const data = { units : EUnitType [ this . ins . localUnits . getValidatedValue ( ) ] } as IModel ; const ins = this . ins ; if ( ! ins . visible . value ) { data . visible = false ; } if ( ins . tags . value ) { data . tags = ins . tags . value ; } if ( ins . renderOrder . value !== 0 ) { data . renderOrder = ins . renderOrder . value ; } if ( ins . shadowSide . value != ESideType . Back ) { data . shadowSide = ESideType [ this . ins . shadowSide . getValidatedValue ( ) ] as TSideType ; } const position = ins . position . value ; if ( position [ 0 ] !== 0 || position [ 1 ] !== 0 || position [ 2 ] !== 0 ) { data . translation = ins . position . value ; } const rotation = ins . rotation . value ; if ( rotation [ 0 ] !== 0 || rotation [ 1 ] !== 0 || rotation [ 2 ] !== 0 ) { helpers . degreesToQuaternion ( rotation , CVModel2 . rotationOrder , _quat ) ; data . rotation = _quat . toArray ( ) ; } if ( ins . override . value ) { data . material = { color : ins . color . value , opacity : ins . opacity . value , hiddenOpacity : ins . hiddenOpacity . value , roughness : ins . roughness . value , metalness : ins . metalness . value , occlusion : ins . occlusion . value , doubleSided : ins . doubleSided . value } ; } data . boundingBox = { min : this . _localBoundingBox . min . toArray ( ) as LocalVector3 , max : this . _localBoundingBox . max . toArray ( ) as LocalVector3 } ; data . derivatives = this . derivatives . toJSON ( ) ; const annotations = this . getComponent ( CVAnnotationView ) . toData ( ) ; if ( annotations && annotations . length > 0 ) { data . annotations = annotations ; } document . models = document . models || [ ] ; const modelIndex = document . models . length ; document . models . push ( data ) ; return modelIndex ; } protected updateShader ( ) { const shader = this . ins . shader . getValidatedValue ( ) ; this . object3D . traverse ( object => { const material = object [ "material" ] as UberPBRMaterial ; if ( material && material . isUberPBRMaterial ) { material . setShaderMode ( shader ) ; } } ) ; } protected updateMaterial ( ) { const ins = this . ins ; this . object3D . traverse ( object => { const material = object [ "material" ] as UberPBRMaterial ; if ( material && material . isUberPBRMaterial ) { material . aoMapMix . setScalar ( ins . occlusion . value ) ; material . color . fromArray ( ins . color . value ) ; material . opacity = this . _visible ? ins . opacity . value : ins . hiddenOpacity . value ; material . transparent = material . opacity < 1 || ! ! material . alphaMap ; material . roughness = ins . roughness . value ; material . metalness = ins . metalness . value ; material . side = ins . doubleSided . value ? DoubleSide : FrontSide ; } } ) ; } protected updateUnitScale ( ) { const fromUnits = this . ins . localUnits . getValidatedValue ( ) ; const toUnits = this . ins . globalUnits . getValidatedValue ( ) ; this . outs . unitScale . setValue ( unitScaleFactor ( fromUnits , toUnits ) ) ; if ( ENV_DEVELOPMENT ) { console . log ( "Model.updateUnitScale, from: %s, to: %s" , fromUnits , toUnits ) ; } this . updateMatrixFromProps ( ) ; } protected updateMatrixFromProps ( ) { const ins = this . ins ; const unitScale = this . outs . unitScale . value ; const object3D = this . object3D ; _vec3a . fromArray ( ins . position . value ) . multiplyScalar ( unitScale ) ; helpers . degreesToQuaternion ( ins . rotation . value , CVModel2 . rotationOrder , _quat ) ; _vec3b . setScalar ( unitScale ) ; object3D . matrix . compose ( _vec3a , _quat , _vec3b ) ; object3D . matrixWorldNeedsUpdate = true ; this . outs . updated . set ( ) ; } protected updateRenderOrder ( model , value ) { model . renderOrder = value ; model . children . forEach ( child => this . updateRenderOrder ( child , value ) ) ; } param quality protected autoLoad ( quality ) { const sequence = [ ] ; const lowestQualityDerivative = this . derivatives . select ( EDerivativeUsage . Web3D , EDerivativeQuality . Thumb ) ; if ( lowestQualityDerivative ) { sequence . push ( lowestQualityDerivative ) ; } const targetQualityDerivative = this . derivatives . select ( EDerivativeUsage . Web3D , quality ) ; if ( targetQualityDerivative && targetQualityDerivative !== lowestQualityDerivative ) { sequence . push ( targetQualityDerivative ) ; } if ( sequence . length === 0 ) { Notification . show ( `No 3D derivatives available for '${ this . displayName }'.` ) ; return Promise . resolve ( ) ; } return sequence . reduce ( ( promise , derivative ) => { return promise . then ( ( ) => this . loadDerivative ( derivative ) ) ; } , Promise . resolve ( ) ) ; } param derivative protected loadDerivative ( derivative ) { if ( ! this . node || ! this . assetReader ) { console . warn ( "Model load interrupted." ) ; return ; } return derivative . load ( this . assetReader ) . then ( ( ) => { if ( ! derivative . model ) { return ; } if ( ! this . assetManager . initialLoad && ! this . _activeDerivative ) { this . assetManager . initialLoad = true ; } if ( this . _activeDerivative ) { this . removeObject3D ( this . _activeDerivative . model ) ; this . _activeDerivative . unload ( ) ; } this . _activeDerivative = derivative ; this . addObject3D ( derivative . model ) ; this . renderer . activeSceneComponent . scene . updateMatrixWorld ( true ) ; if ( this . _boxFrame ) { this . removeObject3D ( this . _boxFrame ) ; this . _boxFrame . geometry . dispose ( ) ; this . _boxFrame = null ; } helpers . computeLocalBoundingBox ( derivative . model , this . _localBoundingBox ) ; this . outs . updated . set ( ) ; if ( ENV_DEVELOPMENT ) { const box = { min : this . _localBoundingBox . min . toArray ( ) , max : this . _localBoundingBox . max . toArray ( ) } ; console . log ( "CVModel.onLoad - bounding box: " , box ) ; } this . outs . quality . setValue ( derivative . data . quality ) ; if ( this . ins . override . value ) { this . updateMaterial ( ) ; } if ( this . ins . shadowSide . value != ESideType . Back ) { this . object3D . traverse ( object => { const material = object [ "material" ] as UberPBRMaterial ; if ( material && material . isUberPBRMaterial ) { if ( this . ins . shadowSide . value == ESideType . Front ) { material . shadowSide = FrontSide ; } else { material . shadowSide = BackSide ; } } } ) ; } this . getGraphComponent ( CVEnvironment ) . ins . dirty . set ( ) ; if ( this . ins . renderOrder . value !== 0 ) this . updateRenderOrder ( this . object3D , this . ins . renderOrder . value ) ; } ) . catch ( error => Notification . show ( `Failed to load model derivative: ${ error . message }` ) ) ; } protected addObject3D ( object ) { this . object3D . add ( object ) ; this . object3D . traverse ( node => { if ( node . type === "Mesh" ) { this . registerPickableObject3D ( node , true ) ; } } ) ; } }