/** * 3D Foundation Project * Copyright 2025 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, Group, Matrix4, Box3Helper, Object3D, FrontSide, BackSide, DoubleSide, Texture, Material, MeshStandardMaterial, NoBlending, AdditiveBlending, Color, MeshPhysicalMaterial, ObjectSpaceNormalMap } 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 { EShaderMode } from "client/schema/setup"; import { EDerivativeQuality, EDerivativeUsage, EUnitType, IModel, ESideType, TSideType, EAssetType, EMapType, IPBRMaterialSettings } from "client/schema/model"; import unitScaleFactor from "../utils/unitScaleFactor"; 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"; import CVSetup from "./CVSetup"; import { Dictionary } from "client/../../libs/ff-core/source/types"; import Asset from "client/models/Asset"; //////////////////////////////////////////////////////////////////////////////// const _vec3a = new Vector3(); const _vec3b = new Vector3(); const _quat = new Quaternion(); const _quat1 = new Quaternion(); const _box = new Box3(); const _mat4 = new Matrix4(); export interface ITagUpdateEvent extends ITypedEvent<"tag-update"> { } export interface IModelLoadEvent extends ITypedEvent<"model-load"> { quality: EDerivativeQuality; model: CVModel2; } /** * Describes an overlay image */ export interface IOverlay { texture: Texture; asset: Asset; fromFile: boolean; isDirty: boolean; } /** * 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 = { name: types.String("Model.Name"), globalUnits: types.Enum("Model.GlobalUnits", EUnitType, EUnitType.cm), localUnits: types.Enum("Model.LocalUnits", EUnitType, EUnitType.cm), quality: types.Enum("Model.Quality", EDerivativeQuality, EDerivativeQuality.Thumb), 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), variant: types.Option("Material.Variant", [], 0), overlayMap: types.Option("Material.OverlayMap", ["None"], 0), slicerEnabled: types.Boolean("Material.SlicerEnabled", true), 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", { preset: 0.25, max: 2.0}), 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"), overlayMap: types.Option("Material.OverlayMap", ["None"], 0), variant: types.Option("Material.Variant", [], 0), }; ins = this.addInputs(CVModel2.ins); outs = this.addOutputs(CVModel2.outs); get settingProperties() { return [ this.ins.name, this.ins.visible, this.ins.quality, this.ins.localUnits, this.ins.tags, this.ins.renderOrder, this.ins.shadowSide, this.ins.shader, this.ins.variant, this.ins.overlayMap, this.ins.slicerEnabled, 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; /** * Separate from activeDerivative because when switching quality levels, * we want to keep the active model until the new one is ready */ private _loadingDerivative :Derivative = null; private _visible: boolean = true; private _boxFrame: Box3Helper = null; private _localBoundingBox = new Box3(); private _prevPosition: Vector3 = new Vector3(0.0,0.0,0.0); private _prevRotation: Vector3 = new Vector3(0.0,0.0,0.0); private _materialCache: Dictionary = {}; private _clayColor = new Color("#a67a6c").convertLinearToSRGB(); private _wireColor = new Color("#004966").convertLinearToSRGB(); private _wireEmissiveColor = new Color("#004966").convertLinearToSRGB(); private _overlays: Dictionary = {}; constructor(node: Node, id: string) { super(node, id); this.object3D = new Group(); this.object3D.name = "Model"; } get snapshotProperties() { return [ this.ins.visible, this.ins.quality, this.ins.overlayMap, this.ins.override, this.ins.opacity, this.ins.variant, this.ins.roughness, this.ins.metalness, this.ins.color, this.ins.slicerEnabled ]; } get derivatives() { return this._derivatives; } get activeDerivative() { return this._activeDerivative; } get localBoundingBox(): Readonly { return this._localBoundingBox; } protected get assetManager() { return this.getMainComponent(CVAssetManager); } protected get assetReader() { return this.getMainComponent(CVAssetReader); } protected get renderer() { return this.getMainComponent(CRenderer); } getOriginalMaterial(id: string): Readonly { return this._materialCache[id]; } getOverlay(key: string) : IOverlay { if(key in this._overlays) { return this._overlays[key]; } else { const overlayProp = this.ins.overlayMap; if(!overlayProp.schema.options.includes(key)) { overlayProp.setOptions(overlayProp.schema.options.concat(key)); } return this._overlays[key] = { texture: null, asset: null, fromFile: false, isDirty: false }; } } getOverlays() { //TODO: This should be more efficient return Object.keys(this._overlays).filter(key => this.activeDerivative.findAssets(EAssetType.Image).some(image => image.data.uri == key)) .map(key => this._overlays[key]); } deleteOverlay(key: string) { const overlayProp = this.ins.overlayMap; const options = overlayProp.schema.options; options.splice(options.indexOf(key),1); overlayProp.setOptions(options); this._overlays[key].texture.dispose(); delete this._overlays[key]; } create() { super.create(); // link units with annotation view const av = this.node.createComponent(CVAnnotationView); av.ins.unitScale.linkFrom(this.outs.unitScale); } update() { const ins = this.ins; if (ins.name.changed) { this.node.name = ins.name.value; } if (ins.tags.changed || ins.activeTags.changed || ins.visible.changed) { let visible = ins.visible.value; if (visible) { // determine visibility based on whether a tag of this model is selected 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; } this.outs.updated.set(); } if (ins.tags.changed) { this.emit({ type: "tag-update" }); } if (!this.activeDerivative && ins.autoLoad.changed && ins.autoLoad.value) { this.autoLoad().then(()=> { let setup = this.getGraphComponent(CVSetup); if (!setup.derivatives.ins.enabled.value){ const derivative = this.derivatives.select(EDerivativeUsage.Web3D, EDerivativeQuality.High); this.ins.quality.setValue(derivative.data.quality); } }); } else if (ins.quality.changed) { const derivative = this.derivatives.select(EDerivativeUsage.Web3D, ins.quality.value); if (derivative) { 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.overlayMap.changed) { this.updateOverlayMap(); } if (ins.variant.changed) { this.updateVariant(); } if (ins.shadowSide.changed) { this.updateShadowSide(); } if (ins.override.value && ins.shader.value === EShaderMode.Default && (ins.override.changed || ins.color.changed || ins.opacity.changed || ins.doubleSided.changed || ins.roughness.changed || ins.metalness.changed || ins.occlusion.changed)) { this.updateMaterial(); } else if(ins.override.changed && !ins.override.value && ins.shader.value === EShaderMode.Default) { this.object3D.traverse(object => { const material = object["material"] as MeshStandardMaterial; if (material) { const cachedMat = this._materialCache[material.uuid]; if(cachedMat) { material.aoMapIntensity = cachedMat.occlusion; material.color.fromArray(cachedMat.color); material.opacity = cachedMat.opacity; material.transparent = cachedMat.transparent; material.roughness = cachedMat.roughness; material.metalness = cachedMat.metalness; material.side = cachedMat.doubleSided ? DoubleSide : FrontSide; material.needsUpdate = true; } } }); } 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; for (let key in this._overlays) { if(this._overlays[key].texture) { this._overlays[key].texture.dispose(); } } super.dispose(); } center() { const object3D = this.object3D; const position = this.ins.position; // remove position and scaling, but preserve rotation object3D.matrix.decompose(_vec3a, _quat, _vec3b); object3D.matrix.makeRotationFromQuaternion(_quat); // compute local bounding box and set position offset _box.makeEmpty(); helpers.computeLocalBoundingBox(object3D, _box, object3D.parent); _box.getCenter(_vec3a); _vec3a.multiplyScalar(-1).toArray(position.value); // trigger matrix update position.set(); } setFromMatrix(matrix: Matrix4) { 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: IDocument, node: INode): number { const { ins, outs } = this; if (!isFinite(node.model)) { throw new Error("model property missing in node"); } const data = document.models[node.model]; ins.name.setValue(node.name); 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); this._prevPosition.fromArray(data.translation); } if (data.rotation) { _quat.fromArray(data.rotation); helpers.quaternionToDegrees(_quat, CVModel2.rotationOrder, ins.rotation.value); this._prevRotation.fromArray(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(boundingBox, "#009cde"); this.addObject3D(this._boxFrame); this._boxFrame.updateMatrixWorld(true); const setup = this.getGraphComponent(CVSetup, true); if(setup && setup.navigation.ins.autoZoom.value) { setup.navigation.ins.zoomExtents.set(); } outs.updated.set(); this.updateUnitScale(); } 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.overlayMap) { ins.overlayMap.setValue(data.overlayMap); } if (data.annotations) { this.getComponent(CVAnnotationView).fromData(data.annotations); } // emit tag update event this.emit({ type: "tag-update" }); // trigger automatic loading of derivatives if active this.ins.autoLoad.set(); return node.model; } toDocument(document: IDocument, node: INode): number { 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 }; } if (ins.overlayMap.value !== 0) { data.overlayMap = ins.overlayMap.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(); const updateList = []; this.object3D.traverse(object => { const material = object["material"] as Material; if (material && material.defines && !updateList.includes(material.uuid)) { this.setShaderMode(shader, material); updateList.push(material.uuid); } }); } protected updateShadowSide() { this.object3D.traverse(object => { const material = object["material"] as Material; if (material) { if(this.ins.shadowSide.value == ESideType.Front) { material.shadowSide = FrontSide; } else { material.shadowSide = BackSide; } material.needsUpdate = true; } }); } updateOverlayMap() { // only update if we are not currently tweening const setup = this.getGraphComponent(CVSetup, true); if (setup && setup.snapshots.outs.tweening.value) { setup.snapshots.outs.end.once("value", () => { this.ins.overlayMap.set(); this.update(); }); return; } const overlays = this.getOverlays(); const currIdx = this.ins.overlayMap.value-1; if (currIdx >= 0 && overlays.length > currIdx) { const mapURI = this.getOverlays()[currIdx].asset.data.uri; const texture = this.getOverlay(mapURI).texture; if(texture) { this.updateOverlayMaterial(texture, mapURI); } else { this.assetReader.getTexture(mapURI).then(map => { this.getOverlay(mapURI).texture = map; this.updateOverlayMaterial(map, mapURI); }); } } else { this.updateOverlayMaterial(null, null); } } protected updateVariant() { if(!this.activeDerivative) { return; } const variantName = this.ins.variant.getOptionText(); const variantIndex = this.activeDerivative.model.userData["variants"].findIndex( ( v ) => v.name.includes( variantName ) ); if(variantIndex < 0) { return; } const parser = this.activeDerivative.model.userData["parser"]; this.object3D.traverse( async ( subobject ) => { var object = subobject as any; if ( ! object.isMesh || ! object.userData.gltfExtensions ) return; const meshVariantDef = object.userData.gltfExtensions[ 'KHR_materials_variants' ]; if ( ! meshVariantDef ) return; if ( ! object.userData.originalMaterial ) { object.userData.originalMaterial = object.material; } const mapping = meshVariantDef.mappings .find( ( mapping ) => mapping.variants.includes( variantIndex ) ); if ( mapping ) { parser.getDependency( 'material', mapping.material ).then((variantMat) => { const shader = object.material.userData.shader; object.material.copy( variantMat); object.material.userData.shader = shader; // add back shader reference that's lost //parser.assignFinalMaterial( object ); this.activeDerivative.model.userData["variants"].variantMaterials[variantMat.uuid] = variantMat; // cache variant const material = object.material; this._materialCache[material.uuid] = { color: material.color.toArray(), opacity: material.opacity, hiddenOpacity: this.ins.hiddenOpacity.schema.preset, roughness: material.roughness, metalness: material.metalness, occlusion: material.aoMapIntensity, doubleSided: material.side == DoubleSide, transparent: material.transparent } }); } else { object.material = object.userData.originalMaterial; } if (this.ins.override.value) { this.updateMaterial(); } this.outs.variant.setValue(this.ins.variant.value); } ); this.outs.updated.set(); } // helper function to update overlay map state protected updateOverlayMaterial(texture: Texture, uri: string) { if(this.object3D) { this.object3D.traverse(object => { const material = object["material"]; if (material && material.defines) { if(texture) { texture.flipY = false; material.defines["OVERLAY_ALPHA"] = uri.endsWith(".jpg"); } material.defines["USE_ZONEMAP"] = texture != null; material.userData.shader.uniforms.zoneMap.value = texture; material.needsUpdate = true; } }); this.outs.overlayMap.setValue(this.ins.overlayMap.value); } } protected updateMaterial() { const ins = this.ins; this.object3D.traverse(object => { const material = object["material"] as MeshStandardMaterial; if (material && material.defines) { material.aoMapIntensity = ins.occlusion.value; material.color.fromArray(ins.color.value); material.opacity = this._visible ? ins.opacity.value : ins.hiddenOpacity.value; material.transparent = material.opacity < 1 || this._materialCache[material.uuid].transparent; //material.depthWrite = material.opacity === 1; material.roughness = ins.roughness.value; material.metalness = ins.metalness.value; material.side = ins.doubleSided.value ? DoubleSide : FrontSide; material.needsUpdate = true; } }); } 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; //TODO: Cleanup & optimize annotation update helpers.degreesToQuaternion([this._prevRotation.x, this._prevRotation.y, this._prevRotation.z], CVModel2.rotationOrder, _quat1); _quat1.invert(); _vec3b.setScalar(1); _mat4.compose(_vec3a.fromArray(ins.position.value), _quat, _vec3b); const annotations = this.getComponent(CVAnnotationView); annotations.getAnnotations().forEach(anno => { _vec3a.fromArray(anno.data.position); _vec3a.sub(this._prevPosition); _vec3a.applyQuaternion(_quat1); _vec3a.applyMatrix4(_mat4); anno.data.position = _vec3a.toArray(); _vec3a.fromArray(anno.data.direction); _vec3a.applyQuaternion(_quat1); _vec3a.applyQuaternion(_quat); anno.data.direction = _vec3a.toArray(); anno.update(); annotations.updateAnnotation(anno, true); }); this._prevPosition.copy(_vec3a.fromArray(ins.position.value)); this._prevRotation.copy(_vec3a.fromArray(ins.rotation.value)); this.outs.updated.set(); } protected updateRenderOrder(model: Object3D, value: number) { const delta = value - model.renderOrder; model.renderOrder = value; model.children.forEach(child => this.updateRenderOrder(child, delta)); } /** * Automatically loads derivatives up to the given quality. * First loads the lowest available quality (usually thumb), then * loads the desired quality level. * @param quality */ protected autoLoad(): Promise { const nearestDerivative = this.derivatives.select(EDerivativeUsage.Web3D, this.ins.quality.value); if (nearestDerivative) { return this.loadDerivative(nearestDerivative); }else { Notification.show(`No 3D derivatives available for '${this.displayName}'.`); return Promise.resolve(); }; } public unload(){ if (this._activeDerivative) { if(this._activeDerivative.model) this.removeObject3D(this._activeDerivative.model); this._activeDerivative.unload(); this._activeDerivative = null; } } public isLoading(){ return !!this._loadingDerivative; } /** * Loads and displays the given derivative. * @param derivative */ protected async loadDerivative(derivative: Derivative): Promise { if(!this.node || !this.assetReader) { // TODO: Better way to handle active loads when node has been disposed? console.warn("Model load interrupted."); return; } if(this._loadingDerivative && this._loadingDerivative != derivative) { this._loadingDerivative.unload(); this._loadingDerivative = null; } if (this._activeDerivative == derivative){ return; } if(this._loadingDerivative == derivative) { return new Promise(resolve=> this._loadingDerivative.on("load", resolve)); } this._loadingDerivative = derivative; return derivative.load(this.assetReader) .then(() => { if ( !derivative.model || !this.node || (this._activeDerivative && derivative.data.quality != this.ins.quality.value) ) { //Either derivative is not valid, or we have been disconnected, // or this derivative is no longer needed as it's not the requested quality // AND we already have _something_ to display derivative.unload(); return; } if(this._activeDerivative && this._activeDerivative == derivative){ //a race condition can happen where a derivative fires it's callback but it's already the active one. return; } this.unload(); this._activeDerivative = derivative; this._loadingDerivative = null; this.addObject3D(derivative.model); this.renderer.activeSceneComponent.scene.updateMatrixWorld(true); if (this._boxFrame) { this.removeObject3D(this._boxFrame); this._boxFrame.dispose(); this._boxFrame = null; } // update bounding box based on loaded derivative this._localBoundingBox.makeEmpty(); helpers.computeLocalBoundingBox(derivative.model, this._localBoundingBox); this.outs.updated.set(); // handle empty bounds case if(this._localBoundingBox.isEmpty()) { _vec3a.setScalar(0); this._localBoundingBox.set(_vec3a, _vec3a); } if (ENV_DEVELOPMENT) { // log bounding box to console const box = { min: this._localBoundingBox.min.toArray(), max: this._localBoundingBox.max.toArray() }; console.log("CVModel.onLoad - bounding box: ", box); } // update loaded quality property this.outs.quality.setValue(derivative.data.quality); // cache original material properties this.object3D.traverse(object => { const material = object["material"] as MeshStandardMaterial; if (material) { this._materialCache[material.uuid] = { color: material.color.toArray(), opacity: material.opacity, hiddenOpacity: this.ins.hiddenOpacity.schema.preset, roughness: material.roughness, metalness: material.metalness, occlusion: material.aoMapIntensity, doubleSided: material.side == DoubleSide, transparent: material.transparent } } }); if (this.ins.override.value) { this.updateMaterial(); } // update shadow render side if(this.ins.shadowSide.value != ESideType.Back) { this.updateShadowSide(); } // make sure render order is correct if(this.ins.renderOrder.value !== 0) this.updateRenderOrder(this.object3D, this.ins.renderOrder.value); // load overlays const overlayProp = this.ins.overlayMap; overlayProp.setOptions(["None"]); derivative.findAssets(EAssetType.Image).filter(image => image.data.mapType === EMapType.Zone).forEach(image => { overlayProp.setOptions(overlayProp.schema.options.concat(image.data.uri)); const overlay = this.getOverlay(image.data.uri); overlay.asset = image; overlay.fromFile = true; }); // load variants const variants = derivative.model.userData["variants"] ? derivative.model.userData["variants"].map( ( variant ) => variant.name ) : null; if(variants) { const variantSet = new Set(this.ins.variant.schema.options); variants.forEach(variantSet.add, variantSet); this.ins.variant.setOptions([...variantSet]); this.ins.variant.setOption(variants[0],true,true); this.updateVariant(); } if(this.ins.overlayMap.value !== 0) { this.ins.overlayMap.set(); } this.emit({ type: "model-load", quality: derivative.data.quality, model: this }); //this.getGraphComponent(CVSetup).navigation.ins.zoomExtents.set(); }).catch(error =>{ if(error.name == "AbortError" || error.name == "ABORT_ERR") return; console.error(error); Notification.show(`Failed to load model derivative: ${error.message}`) }); } protected addObject3D(object: Object3D) { this.object3D.add(object); this.object3D.traverse(node => { if (node.type === "Mesh") { this.registerPickableObject3D(node, true); } }); } setShaderMode(mode: EShaderMode, inMaterial: Material) { const material = inMaterial as MeshStandardMaterial; Object.assign(material, material.userData.paramCopy); material.defines["MODE_NORMALS"] = false; material.defines["MODE_XRAY"] = false; material.defines["OBJECTSPACE_NORMALMAP"] = !!(material.normalMap && material.normalMapType === ObjectSpaceNormalMap); material.side = material.defines["CUT_PLANE"] ? DoubleSide : material.side; material.needsUpdate = true; switch(mode) { case EShaderMode.Clay: material.userData.paramCopy = { color: material.color, map: material.map, roughness: material.roughness, metalness: material.metalness, aoMapIntensity: material.aoMapIntensity, blending: material.blending, transparent: material.transparent, depthWrite: material.depthWrite, envMap: material.envMap }; if(material.type == "MeshPhysicalMaterial") { const physMat = material as MeshPhysicalMaterial; material.userData.paramCopy["transmission"] = physMat.transmission; physMat.transmission = 0; } material.envMap = null; material.color = this._clayColor; material.map = null; material.roughness = 1; material.metalness = 0; material.aoMapIntensity *= 1; material.blending = NoBlending; material.transparent = false; material.depthWrite = true; break; case EShaderMode.Normals: material.userData.paramCopy = { blending: material.blending, transparent: material.transparent, depthWrite: material.depthWrite, }; material.defines["MODE_NORMALS"] = true; material.blending = NoBlending; material.transparent = false; material.depthWrite = true; break; case EShaderMode.XRay: material.userData.paramCopy = { side: material.side, blending: material.blending, transparent: material.transparent, depthWrite: material.depthWrite, }; material.defines["MODE_XRAY"] = true; material.side = DoubleSide; material.blending = AdditiveBlending; material.transparent = true; material.depthWrite = false; break; case EShaderMode.Wireframe: material.userData.paramCopy = { color: material.color, emissive: material.emissive, roughness: material.roughness, metalness: material.metalness, wireframe: material.wireframe, map: material.map, aoMap: material.aoMap, emissiveMap: material.emissiveMap, normalMap: material.normalMap, }; material.color = this._wireColor; material.emissive = this._wireEmissiveColor; material.roughness = 0.8; material.metalness = 0.1; material.wireframe = true; material.map = null; material.aoMap = null; material.emissiveMap = null; material.normalMap = null; material.defines["OBJECTSPACE_NORMALMAP"] = false; break; } } }