Last active
March 18, 2021 13:59
-
-
Save wallabyway/992b1a26606003e7e4efd9c550fb5b9f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { getParameterByName } from "../globals"; | |
import { isMobileDevice } from "../compat"; | |
const av = Autodesk.Viewing; | |
const avp = av.Private; | |
// Enum for all extension names we are using here | |
const ExtNames = { | |
BimWalk: 'Autodesk.BimWalk', | |
Bookmarks: 'Autodesk.AEC.CanvasBookmarkExtension', | |
Levels: 'Autodesk.AEC.LevelsExtension', | |
CrossFade: 'Autodesk.CrossFadeEffects', | |
Hyperlinks: 'Autodesk.AEC.HyperlinkExtension', | |
Minimap: 'Autodesk.AEC.Minimap3DExtension', | |
DropMe: 'Autodesk.AEC.DropMeExtension', | |
ZoomWindow: 'Autodesk.Viewing.ZoomWindow', | |
FusionOrbit: 'Autodesk.Viewing.FusionOrbit', | |
DiffTool: 'Autodesk.DiffTool', | |
ModelStructure: 'Autodesk.ModelStructure', | |
}; | |
// Get model key. Input may be bubbleNode, Model, or model key already. | |
const makeKey = (value) => { | |
if (value instanceof av.BubbleNode) { | |
return value.getModelKey(); | |
} | |
// For LMV models, get bubbleNode first | |
if (value instanceof av.Model) { | |
return value.getDocumentNode().getModelKey(); | |
} | |
if (typeof value === 'string') { | |
return value; | |
} | |
console.error("makeKey: Input must be key, model, or BubbleNode"); | |
}; | |
const createModelItem = (node) => { | |
return { | |
// av.Model or null (if the model root is loading) | |
model: null, // av.Model | |
// Same as model.getDocumentNode(), but model may be null if the root is not loaded yet. | |
node: node, // av.BubbleNode | |
// A model is "set to be visible" if either | |
// a) model is already displayed | |
// b) model will be displayed as soon as model root is loaded | |
visible: false, | |
// Url used for the loadModel - needed for cancelling loads if model root is not available yet | |
url: null, | |
// Indicates that the model root of this model is currently loading | |
loading: false, | |
// Indicates that we already tried loading this model, but failed | |
error: false | |
}; | |
}; | |
// Empty 3D views may be confusing and easily mistaken for a bug. | |
// Therefore, we notify the user if the shown model is empty. | |
const warnIfModelEmpty = (model) => { | |
const data = model.getData(); | |
const bubbleNode = data.loadOptions.bubbleNode; | |
const modelEmpty = (model.is3d() && data.metadata.stats && !data.metadata.stats.num_fragments); | |
if (modelEmpty) { | |
const viewName = bubbleNode.name(); | |
const modelName = bubbleNode.getRootNode().children[0].name(); | |
console.warn(`Empty View "${viewName}" in model "${modelName}".`); | |
} | |
}; | |
const isVectorFinite = (vec) => { | |
return isFinite(vec.x) && isFinite(vec.y) && isFinite(vec.z); | |
}; | |
const isBoxFinite = (box) => { | |
return isVectorFinite(box.min) && isVectorFinite(box.max); | |
}; | |
const getUpVector = (model) => { | |
let upVectorArray = model.getUpVector(); | |
return upVectorArray && new THREE.Vector3().fromArray(upVectorArray); | |
}; | |
const isCameraValid = (camera) => { | |
return ( | |
isVectorFinite(camera.position) && | |
isVectorFinite(camera.target) && | |
isVectorFinite(camera.up) && | |
isFinite(camera.orthoScale) | |
); | |
}; | |
// Helper function for home camera: | |
// Check how many models are intersecting the frustum when using a certain camera. | |
const countCatchedModels = (models, camPos, frustum) => { | |
let catchedModels = 0; | |
for (let i=0; i<models.length; i++) { | |
// model outside frustum? => consider as missed | |
const model = models[i]; | |
const bbox = model.getBoundingBox(); | |
if (frustum.intersectsBox(bbox) === Autodesk.Viewing.Private.FrustumIntersector.OUTSIDE) { | |
continue; | |
} | |
// also consider models as missed if they are extremely far away from the camera | |
const diag = bbox.size().length; | |
const dist = bbox.distanceToPoint(camPos); | |
if (dist > diag * 50) { | |
continue; | |
} | |
catchedModels++; | |
} | |
return catchedModels; | |
}; | |
// As home camera for aggregated views, we use the default camera of one of the visible models. | |
// The choice is done in a way that we have as many visible models in frustum as possible. | |
const updateHomeCamera = (viewer) => { | |
// Filter out any crappy models in advance (empty or infinite bbox) | |
let models = viewer.getVisibleModels(); | |
models = models.filter(model => isBoxFinite(model.getBoundingBox())); | |
let tmpCam = viewer.impl.camera.clone(); | |
let frustum = new Autodesk.Viewing.Private.FrustumIntersector(); | |
// Get model from the first model that defines it (must match anyway). | |
// For the camera, we choose the default cam of the largest model (wrt. to data size). | |
let upVector = undefined; | |
let camera = undefined; | |
let bestScore = undefined; | |
for (let i=0; i<models.length; i++) { | |
const model = models[i]; | |
// Choose the first up-vector we get | |
// We assume identical ones - otherwise, the aggregated model would be weird anyway. | |
if (!upVector) { | |
upVector = getUpVector(model); | |
} | |
// Consider default cam if valid | |
const modelCam = model.getDefaultCamera(); | |
if (!modelCam || !isCameraValid(modelCam)) { | |
continue; | |
} | |
const modelBox = model.getBoundingBox(); | |
// Configure frustum test for this camera | |
tmpCam.position.copy(modelCam.position); | |
tmpCam.target.copy(modelCam.target); | |
tmpCam.up.copy(modelCam.up); | |
tmpCam.isPerspective = modelCam.isPerspective; | |
tmpCam.near = modelBox.distanceToPoint(tmpCam.position); | |
tmpCam.far = tmpCam.near + modelBox.size().length; | |
tmpCam.updateMatrixWorld(); | |
tmpCam.updateProjectionMatrix(); | |
tmpCam.matrixWorldInverse.getInverse(tmpCam.matrixWorld); | |
frustum.reset(tmpCam); | |
// Choose camera that catched most models | |
const score = countCatchedModels(models, modelCam.position, frustum); | |
if (!camera || score >= bestScore) { | |
camera = modelCam; | |
bestScore = score; | |
} | |
} | |
if (!camera) { | |
return; | |
} | |
// Extend camera by... | |
// 1. pivot: Otherwise, autocam may leave the pivot point at some far-away position | |
// and the camera will orbit into void on next move. | |
// 2. worldup: To make sure that we are not using the wrong axis for orbiting | |
// 3. fov: always use reasonable default | |
camera.pivot = camera.target; | |
camera.worlup = upVector; | |
camera.fov = 45.0; | |
viewer.autocam.setHomeViewFrom(camera); | |
}; | |
export class AggregatedView { | |
constructor() { | |
// The purpose of the global offset is to avoid float inaccuracies for georeferenced | |
// models with large offset. Note that we only use (0,0,0) as long as we don't | |
// know anything better (see resetRefPoint()) | |
// | |
// By default, LMV chooses the center of the model. This is okay for | |
// a single model. But for multiple ones, it would mean to center all models | |
// independently, so that their relative placement would be lost. | |
this.globalOffset = undefined; | |
// refPoint is chosen once per view switch and determined from the first model | |
// to be shown. The globalOffset is only reset if too far away from the current refPoint. | |
this.refPoint = undefined; | |
// Contains modelItems for all models that are in memory or currently loading. | |
// Indexed by modelKey (string). | |
this.modelItems = {}; | |
// Indicates whether we have to setup LMV for a new view on next model add, | |
// e.g., creating/updating the LMV toolbar and reset tools. We want to do this only | |
// on startup or after an explicit view switch by the user. | |
this.resetOnNextModelAdd = true; | |
this.memTracker = null; | |
this.cameraInitialized = false; | |
// If true, the viewer is configured for 3D viewing, otherwise for 2d. | |
// This flag is determined whenever adding the first model to an empty view. | |
this.is3D = undefined; | |
// If a camera is set before the global offset is determined, we keep it here and | |
// apply it as soon as globalOffset is initialized. | |
this.pendingCamera = null; | |
// Since extension load is async, viewer.getExtension() may return null although we called loadExtension already. | |
// For extensions that we load/unload dynamically, we track the state here - to avoid loading the same extension twice. | |
this.extensionLoaded = {}; | |
this.loadersInProgress = {}; | |
// Current diff-view configuration. | |
this.diff = { | |
enabled: false, | |
diffBubble: undefined, | |
primaryBubble: undefined, | |
diffBubbleLabel: undefined, | |
primaryBubbleLabel: undefined, | |
progressCallback: undefined, | |
refNames: undefined | |
}; | |
// Keys of all models that we keep ghosted during diff mode | |
// (if they are not participating in the diff) | |
this.modelIsGhosted = {}; | |
// Cache diffs to avoid frequent recomputation | |
this.diffCache = []; | |
// Set this to be notified when diff load/computation is finished | |
this.onDiffDone = undefined; | |
// Indicates that the first 3D model is loading and we have to wait for it | |
// to finish before we can load another model. This is needed to determine a consistent globalOffset. | |
this.waitForFirstModel = false; | |
// Internally used array of callbacks that are triggered when a model is unloaded. | |
this.onUnload = []; | |
} | |
// @param {Object} [options] - Configuration options for aggregated view | |
// @param {Object} [options.viewerConfig] - Used for initializing GuiViewer3D | |
// @param {bool} [options.disableBookmarks] - Disable display of visual bookmarks ( | |
// @param {Object} [options.clusterfck] - Dependency-Injection of clusterfck library (enables clustering of Bookmark icons) | |
// @param {function(av.BubbleNode)=>object} [options.getCustomLoadOptions] - Allows for applying custom options to be used for all model loading. | |
// The callback returns an options object that is applied by default in all model-load calls. | |
// @param {object} [options.viewerStartOptions] - Options passed to the viewer initialization process. | |
// @param {Autodesk.Viewing.MultiViewerFactory} [options.multiViewerFactory] - Optional multi viewer factory. Used to create multiple viewers with shared resources. | |
// @param {bool} [options.ignoreGlobalOffset] - Forces globalOffset to undefined for all loaded models. | |
// Effect of this is that all models are auto-centered using the model bbox. | |
// Note that this does only work if you never show more than one 3D viewable at once. | |
// @param {bool} [options.unloadUnfinishedModels] - By setting unloadUnfinishedModels, when calling hide(bubbleNode), it will unload models that haven't been fully loaded. | |
// Used in order to reduce amount of file loaders when switching between models. | |
// @returns {Promise} - When resolved all required extensions are loaded. | |
init(parentDiv, options = {}) { | |
this.initViewerInstance(parentDiv, options); | |
this.memTracker = new av.ModelMemoryTracker(this.viewer, options.memoryLimit); | |
// Activate transparency improvement by default | |
this.viewer.impl.showTransparencyWhenMoving(); | |
this.options = options; | |
this._registerLmvEventListeners(); | |
return this._loadExtensions(); | |
} | |
// Initialize a new viewer instance. | |
initViewerInstance(parentDiv, options = {}) { | |
const ViewerClass = options.viewerClass || (options.headlessViewer ? av.Viewer3D : av.GuiViewer3D); | |
if (options.multiViewerFactory) { | |
this.viewer = options.multiViewerFactory.createViewer(parentDiv, options.viewerConfig, ViewerClass); | |
} else { | |
this.viewer = new ViewerClass(parentDiv, options.viewerConfig); | |
} | |
this.viewer.start(undefined, undefined, undefined, undefined, options.viewerStartOptions); | |
} | |
/** | |
* Method that can be overwritten to log errors to service. Default implementation just logs to console.error. | |
* @param args The messages and other things that should be logged. | |
* @private | |
*/ | |
_onError(...args) { | |
console.error(...args); | |
} | |
// Resets tools, UI, camera, and refPoint. To be called when an explicit view switch occurred instead of just toggling visibility of models. | |
reset() { | |
// hide all previously visible models, so that there are no visible models in the viewer anymore. | |
// This will make sure that the viewer will configured itself properly when adding the first new model. | |
this.hideAll(); | |
// make sure that the camera is reset when new models are added | |
this.pendingCamera = null; | |
this.cameraInitialized = false; | |
// Reset UI and tools on next model-add | |
this.resetOnNextModelAdd = true; | |
// Make sure that no LMV tools remain in the middle of an interaction - like measurements, sections etc. | |
this._stopActiveTools(); | |
// Finish previous diff and discard cached diff results when switching to another view | |
this._unloadDiffTool(true); | |
// Consider that refPoint may change with next viewable | |
this.refPoint = undefined; | |
this.loadersInProgress = {}; | |
} | |
// Find lmv model for given bubbleNode or key. | |
getModel(node) { | |
const item = this._getItem(node); | |
return item && item.model; | |
} | |
isEmpty() { | |
return !this.getVisibleNodes().length; | |
} | |
// Makes sure that a model is loaded and shown | |
show(bubbleNode) { | |
// Auto-Configure for 2D or 3D based on first node to show | |
const isFirstViewable = this.isEmpty(); | |
if (isFirstViewable) { | |
this._initForFirstViewable(bubbleNode); | |
} | |
const modelKey = bubbleNode.getModelKey(); | |
// Either load model or show it directly | |
let model = this.getModel(modelKey); | |
if (model) { | |
this._showModel(model); | |
} else { | |
// Model will be added later when root is loaded | |
this.load(bubbleNode); | |
} | |
// After load(), the model item always exists | |
this.modelItems[modelKey].visible = true; | |
// keep memTracker up-to-date about visible/used models | |
this._updateModelTimestamps(); | |
// consolidate new models if possible | |
this._consolidateVisibleModels(); | |
} | |
hide(bubbleNode) { | |
let item = this._getItem(bubbleNode); | |
if (!item) { | |
return; | |
} | |
// Note that the model root might not be loaded yet. In this case, we don't need to call hideModel: | |
// onModelLoaded() callback takes care that new models are only added if still set to visible. | |
if (item.model) { | |
if (this.options.unloadUnfinishedModels && !item.model.isLoadDone()) { | |
this.unload(item.node); | |
// Verify the model has been really unloaded. | |
// It's needed in case two models share the same bubbleNode, and only one has been unloaded (i.e underlayRaster). | |
if (item.model) { | |
this.viewer.unloadModel(item.model); | |
} | |
} else { | |
this.viewer.hideModel(item.model.id); | |
} | |
// Recompute home-view for remaining visible models | |
updateHomeCamera(this.viewer); | |
} | |
item.visible = false; | |
// Make sure that if model is unloaded immedateiyl if we memory is low | |
this._cleanupModels(); | |
} | |
isVisible(bubbleNode) { | |
const item = this._getItem(bubbleNode); | |
return item && item.visible; | |
} | |
hideAll() { | |
for (let key in this.modelItems) { | |
this.hide(key); | |
} | |
} | |
getVisibleNodes() { | |
let nodes = []; | |
for (let key in this.modelItems) { | |
// Note that item.model may be null during loading, so we cannot use item.model.getDocumentNode() | |
let item = this.modelItems[key]; | |
if (item.visible) { | |
nodes.push(item.node); | |
} | |
} | |
return nodes; | |
} | |
// Checks if the Otg manifest for a 3D viewable is available and complete. If not, it reports an error and returns false. | |
isOtgManifestMissing(bubbleNode) { | |
if (bubbleNode.is2D()) { | |
return false; | |
} | |
let otgNode = bubbleNode.getOtgGraphicsNode(); | |
if (otgNode && !otgNode.error) { | |
return false; | |
} | |
// This can only happen if the overall manifest conversion | |
// succeeded, but conversion failed for some of its viewables. | |
// A failure of the whole document conversion should have been handled outside on manifest load already. | |
if (!otgNode) { | |
this._onError(`Otg node missing for viewable '${bubbleNode.name()}'.`); | |
} else { | |
this._onError(`Otg translation failed for viewable '${bubbleNode.name()}'. Error:`, otgNode.error); | |
} | |
if (this.onViewerNotification) { | |
this.onViewerNotification('error', `Model Translation failed for viewable: '${bubbleNode.name()}'.`); | |
} | |
return true; | |
} | |
// @param {BubbleNode} node | |
// @param {Object} [customLoadOptions] - Optional extra loadOptions to override/extend the default ones. | |
load(bubbleNode, customLoadOptions = null) { | |
// customLoadOptions is only provided if the client calls load() manually from outside. Usually, load() is rather called internally | |
// by AggregatedView whenever we show a model that is not cached yet. For those internal load() calls, custom loadOptions are controlled | |
// via a callback specified in AggregatedView options. | |
if (!customLoadOptions && this.options.getCustomLoadOptions) { | |
customLoadOptions = this.options.getCustomLoadOptions(bubbleNode); | |
} | |
// get or create model item | |
const modelKey = makeKey(bubbleNode); | |
let item = this.modelItems[modelKey]; | |
if (!item) { | |
item = createModelItem(bubbleNode); | |
this.modelItems[modelKey] = item; | |
} | |
// do nothing if model load was triggered before | |
if (item.model || item.loading || item.error) { | |
return; | |
} | |
// For 3D models, we need some extra code for globalOffset handling | |
if (bubbleNode.is3D()) { | |
// Defer loading if necessary | |
if (this.waitForFirstModel) { | |
// Another 3D viewable is loading. We have to wait for it, so that we | |
// know which globalOffset to use. Loading will be triggered in _modelRootLoadEnded() later. | |
return; | |
} | |
// If globalOffset is not initialized yet, block other models until this one is finished | |
if (!this.options.ignoreGlobalOffset && !this.globalOffset) { | |
this.waitForFirstModel = true; | |
} | |
} | |
item.loading = true; | |
// If client requires Otg for 3D viewables, check if Otg manifest is available and complete for 3D case. | |
if (this.options.disableSvf && this.isOtgManifestMissing(bubbleNode)) { | |
return; | |
} | |
// Get LMV document to obtain acm session | |
let root = bubbleNode.getRootNode(); | |
let doc = root.getDocument(); | |
const loadOptions = { | |
// model is translated by -globalOffset. If undefined, LMV uses the model center. | |
globalOffset: this.globalOffset, | |
// consider geo-referencing for consistent placement: If the model contains a georeferencing transform, | |
// it is applied by LMV at load-time. | |
applyRefPoint: true, | |
isAEC: true, | |
// A too fine-grained BVH reduces the performance gain when using consolidation. This is avoided by using the recommended BVH defaults. | |
// When setting useConsolidation to true (default for AEC), this would be done automatically. But this would also run consolidation preprocessing when | |
// a model is loaded, which may temporarily affect the framerate and bypass the memory tracking. Therefore, we run consolidation | |
// later as soon as all animations are finished. | |
bvhOptions: avp.Consolidation.getDefaultBVHOptions(), | |
useConsolidation: false, | |
// We don't want LMV to auto-add models on load, but control adding of models ourselves instead. E.g., because: | |
// - The user might have toggled the model off or done a view switch meanwhile | |
// - The model might just be loaded for other purposes than show (e.g. support models for 2D diff) | |
loadAsHidden: true, | |
// Remember bubble node. This simplifies debugging, e.g. by helping to get the view name for a model. | |
bubbleNode: bubbleNode, | |
// Reduce memory consumption by computing bounding boxes on-the-fly. This is only a problem for the model explode tool, which we do not use. | |
disablePrecomputedNodeBoxes: true, | |
// Avoid LMV from auto-resetting the camera on model-add. E.g., we don't want to jump the camera around just because we toggled visibility of a model. | |
preserveView: true, | |
// Track progress of fragment list loading (used for diff progress bar) | |
onFragmentListLoadProgress: () => this._updateDiffLoadProgress(), | |
// Finding layers in propDb adds significant effort to the worker. If any client of AggregatedView wants to use 3DModelLayers, | |
// we can add an option for that - or allow some general customization of load options. | |
disable3DModelLayers: true, | |
// Avoid loadDocumentNode() from auto-unloading all other models | |
keepCurrentModels: true, | |
// Prevent the creation of LMV's default UI. | |
headlessViewer: this.options.headlessViewer | |
}; | |
const onModelLoaded = this._onModelLoaded.bind(this); | |
const onModelLoadFailed = (errorCode) => this._onModelLoadFailed(bubbleNode, errorCode); | |
if (customLoadOptions) { | |
Object.assign(loadOptions, customLoadOptions); | |
} | |
return new Promise((resolve, reject) => { | |
this.viewer.loadDocumentNode(doc, bubbleNode, loadOptions).then((model) => { | |
onModelLoaded(model); | |
resolve(model); | |
}).catch((error) => { | |
onModelLoadFailed(error) | |
reject(error); | |
}); | |
}); | |
} | |
unload(bubbleNode) { | |
this.viewer.unloadDocumentNode(bubbleNode); | |
// Usually, item will exist - unless the model was not loaded via AggregatedView. | |
const item = this._getItem(bubbleNode); | |
if (item) { | |
// Signal that model loading was cancelled. | |
// Why needed?: The model we just unloaded might have been the first one - and others might be waiting for it to obtain the globalOffset. | |
if (item.loading) { | |
this._onModelLoadEnded(bubbleNode); | |
} | |
// Remove model from overall geometry-load progress tracking. | |
// TODO: We shouldn't have to care for this here. We should better add general support of aggregated load progress in Viewer3D directly. | |
if (item.model) { | |
delete this.loadersInProgress[item.model.id]; | |
} | |
} | |
// delete item state | |
let key = makeKey(bubbleNode); | |
delete this.modelItems[key]; | |
// Notify listener callbacks | |
this.onUnload.forEach(cb => cb(bubbleNode)); | |
} | |
unloadAll() { | |
let keys = Object.keys(this.modelItems).slice(); | |
for (var i = 0; i < keys.length; i++) { | |
let key = keys[i]; | |
let item = this.modelItems[key]; | |
this.unload(item.node); | |
} | |
} | |
// Set camera in global coords. | |
// - The current global offset is automatically subtracted | |
// - You don't have to specify all members, e.g., can leave out up or fov. Only defined values are replaced. | |
// - You can call it independent of loading state: If no model is loaded yet, the camera change is applied after first model add | |
// - Note that the call only has effect on current view, i.e., is discarded on reset/viewSwitch calls. | |
setCameraGlobal(camera) { | |
// We copy the vector-values to avoid time-dependent traps if input vectors are changed after the call | |
this.pendingCamera = { | |
position: camera.position && new THREE.Vector3().copy(camera.position), | |
target: camera.target && new THREE.Vector3().copy(camera.target), | |
up: camera.up && new THREE.Vector3().copy(camera.up), | |
fov: camera.fov, | |
isPerspective: camera.isPerspective | |
}; | |
// Apply camera right now if possible - otherwise later after camera initialize on first model add | |
this._applyPendingCameraWhenReady(); | |
} | |
setCamera(camera) { | |
this.viewer.impl.setViewFromCamera(camera, true); | |
this.cameraInitialized = true; | |
} | |
startBimWalk() { | |
this.bimWalkStartPending = true; | |
this._startBimWalkWhenReady(); | |
} | |
stopBimWalk() { | |
let ext = this.viewer.getExtension(ExtNames.BimWalk); | |
if (ext && ext.isActive()) { | |
ext.deactivate(); | |
} | |
// If a start was pending, cancel it. | |
this.bimWalkStartPending = false; | |
} | |
isBimWalkActive() { | |
let ext = this.viewer.getExtension('Autodesk.BimWalk'); | |
return (ext && ext.activeStatus) || this.bimWalkStartPending; | |
} | |
// Use this for explicit view switches. See setNodes() for params. | |
switchView(bubbleNodes, diffConfig) { | |
this.reset(); | |
this.setNodes(bubbleNodes, diffConfig); | |
} | |
// Load/Unload models so that the currently loaded models matches with the given list of svfs. | |
// | |
// Note: Use switchView() to do an explicit view switch (including resetting tools/camera) | |
// Use setModels() to reconfiguring which models are visible in the current view. | |
// | |
// @param {av.BubbleNode|av.BubbleNode[]} bubbleNodes - The nodes to be shown. | |
// @param {Object] [diffConfig] - Options to activate diff views. | |
// | |
// A diffConfig contains: | |
// @param {av.BubbleNode[]} [diff.primaryBubbles] - A subset of 'bubbleNodes' that participates in the diff. If 'bubbleNodes' contains more, these will be ghosted. | |
// These nodes represent the current/as-is state. | |
// @param {av.BubbleNode[]} [diff.diffBubbles] - Length must match primaryBubbles. For each node primaryBubbles[i], diffBubbles[i] provides the corresponding "before" state to compare against. | |
// @param {av.BubbleNode} [diff.supportBubbles.diff] - If svfs are sheet nodes, diff.supportModels must provide | |
// the bubbleNodes for the corresponding 3D support models. { diff, primary } | |
// @param {av.BubbleNode} [diff.supportBubbles.primary] | |
setNodes(bubbleNodes, diffConfig) { | |
// Don't be pedantic if just called with a single node or null | |
bubbleNodes = bubbleNodes || []; | |
bubbleNodes = bubbleNodes instanceof av.BubbleNode ? [bubbleNodes] : bubbleNodes; | |
// Collect nodes that are to be changed to visible | |
const modelMustBeShown = node => !this.isVisible(node); | |
const modelsToShow = bubbleNodes.filter(modelMustBeShown); // {LMVModelLink[]} | |
// Create temp object to check which nodes finally to be shown | |
let newModelKeys = {}; // {string[]} | |
bubbleNodes.forEach(node => { newModelKeys[node.getModelKey()] = true; }); | |
// Collect nodes to be unloaded | |
const modelMustBeHidden = ( node => (newModelKeys[node.getModelKey()] === undefined) ); | |
const modelsToHide = this.getVisibleNodes().filter(modelMustBeHidden); // {av.BubbleNode[]} | |
// Unload first. This will also reset the global offset if the set of model changed completely | |
modelsToHide.forEach(svf => { this.hide(svf); }); | |
// Load all new models | |
modelsToShow.forEach(node => { this.show(node); }); | |
// Enable and configure diff, or disable if diffConfig is null. | |
this._setDiff(diffConfig); | |
} | |
// May be null if extension is not loaded yet. | |
getFloorSelector() { | |
return this.levelsExtension && this.levelsExtension.floorSelector; | |
} | |
// Define the set of BubbleNodes for which we create InCanvas-Bookmarks. | |
// @param {BubbleNode[]} bookmarks | |
setBookmarks(bookmarks) { | |
this.bookmarks = bookmarks; | |
this._updateBookmarks(); | |
} | |
// Returns true if all pending loading is finished. More concrete, it means that there is no... | |
// - model-root loading | |
// - geometry loading, or | |
// - propDbLoading | |
// pending or in progress. | |
isLoadDone() { | |
for (let key in this.modelItems) { | |
const item = this.modelItems[key]; | |
const model = item && item.model; | |
const modelRootPending = !model && !item.error; | |
const geomPending = model && !model.isLoadDone(); | |
const propDbPending = model && model.getPropertyDb() && !model.getPropertyDb().isLoadDone(); | |
const texLoading = (avp.TextureLoader.requestsInProgress() > 0); | |
if (modelRootPending || geomPending || propDbPending || texLoading) { | |
return false; | |
} | |
} | |
return true; | |
} | |
// Returns a promise that resolves when isLoadDone() returns true. | |
waitForLoadDone() { | |
return new Promise((resolve) => { | |
if (this.isLoadDone()) { | |
resolve(); | |
} | |
// On each load-relevant event, check if loading is finished. | |
const onEvent = () => { | |
if (!this.isLoadDone()) { | |
return; | |
} | |
this.viewer.removeEventListener(av.GEOMETRY_LOADED_EVENT, onEvent); | |
this.viewer.removeEventListener(av.OBJECT_TREE_CREATED_EVENT, onEvent); | |
this.viewer.removeEventListener(av.TEXTURES_LOADED_EVENT, onEvent); | |
this.onUnload.splice(this.onUnload.indexOf(onEvent), 1); | |
resolve(); | |
}; | |
// register event listeners to try again if something changes | |
this.viewer.addEventListener(av.GEOMETRY_LOADED_EVENT, onEvent); | |
this.viewer.addEventListener(av.OBJECT_TREE_CREATED_EVENT, onEvent); | |
this.viewer.addEventListener(av.TEXTURES_LOADED_EVENT, onEvent); | |
this.onUnload.push(onEvent); | |
}); | |
} | |
// --------------------------- | |
// Internal member functions: | |
// --------------------------- | |
_setDiff(diff) { | |
if (diff) { | |
this.diff.diffBubbles = diff.diffBubbles; | |
this.diff.primaryBubbles = diff.primaryBubbles; | |
this.diff.diffBubbleLabel = diff.diffBubbleLabel; | |
this.diff.primaryBubbleLabel = diff.primaryBubbleLabel; | |
this.diff.progressCallback = diff.progressCallback; | |
this.diff.supportBubbles = diff.supportBubbles; | |
this.diff.empty = diff.empty; | |
this.diff.refNames = diff.refNames; | |
// If support models are needed, make sure that they are loaded (if necessary) | |
if (diff.supportBubbles) { | |
this.load(diff.supportBubbles.primary); | |
this.load(diff.supportBubbles.diff); | |
} | |
// start progress bar right now - so that it is also visible if | |
// we have to load some models first. | |
if (!diff.empty && diff.progressCallback) { | |
this._signalDiffLoadProgress(0, true); | |
} | |
this.diffStartTime = performance.now(); | |
} | |
else { | |
this.diff.diffBubbles = undefined; | |
this.diff.primaryBubbles = undefined; | |
this.diff.diffBubbleLabel = undefined; | |
this.diff.primaryBubbleLabel = undefined; | |
this.diff.progressCallback = undefined; | |
this.diff.supportBubbles = undefined; | |
this.diff.refNames = undefined; | |
} | |
this.diff.enabled = Boolean(diff); | |
this.diffNeedsUpdate = true; | |
this._updateDiff(); | |
} | |
// add model to LMV scene (must be in memory) | |
_showModel(model) { | |
// Skip any automatic tool reset from LMV. We trigger these explicitly, | |
// but only if an explicit view switch (=> LMVViewer.reset()) has occurred. | |
// Reason: We only want tool resets on explicit view switches. The scene may | |
// also be just temporarily empty when toggling visibility of models. | |
// In this case, we don't want any automagic resets. | |
const preserveTools = true; | |
this.viewer.showModel(model.id, preserveTools); | |
// Consider new visible model for home camera | |
updateHomeCamera(this.viewer); | |
} | |
_onModelAdded(event) { | |
const model = event.model; | |
// if camera is not initialized yet, use default camera from the first added model. | |
// TODO: Think about some cleaner choice of the start camera that doesn't dependend on | |
// what is loaded first. But it's not fully clear how to define it, because | |
// the list of displayed models may change arbitrarily and we don't know bboxes in advance... | |
if (!this.cameraInitialized) { | |
this._initCamera(model); | |
} | |
if (model.getData().underlayRaster) { | |
return; | |
} | |
if (this.resetOnNextModelAdd) { | |
// Unload ZoomWindow and reload after recreating UI | |
if (this.viewer.getExtension(ExtNames.ZoomWindow)) { | |
this.viewer.unloadExtension(ExtNames.ZoomWindow); | |
} | |
this.resetOnNextModelAdd = false; | |
if (!this.options.headlessViewer) { | |
this.viewer.createUI(model); | |
} | |
this.viewer.activateDefaultNavigationTools(!this.is3D); | |
// Integrate the zoom window functionality. This also automatically | |
// moves the Zoom tool into this submenu. | |
this._loadExtension(ExtNames.ZoomWindow).catch(err => this._onError(err)); | |
// If startBimWalk() was called for this view, activate it | |
this._startBimWalkWhenReady(); | |
} | |
// log console warning if model is empty | |
warnIfModelEmpty(model); | |
const node = event.model.getDocumentNode(); | |
// Image files don't have a document node. In that case, skip ghosting part, which is not relevant to images anyway. | |
if (node) { | |
// Make sure that ghosting is applied if needed. Removing a model resets visibility, | |
// so we assume isModelGhosted as false. | |
const modelKey = node.getModelKey(); | |
delete this.modelIsGhosted[modelKey]; | |
this._updateGhosting(); | |
} | |
} | |
// Called whenever the geometry of a model has finished loading. | |
_onGeometryLoaded(event) { | |
this._consolidateVisibleModels(); | |
this._updateDiff(); | |
this.loadersInProgress[event.model.id] = 100; | |
} | |
_onExtensionLoaded(event) { | |
const extName = event.extensionId; | |
// If startBimWalk has been called before BimWalk was loaded, start it when ready | |
this._startBimWalkWhenReady(); | |
if (extName === ExtNames.Levels) { | |
this.levelsExtension = this.viewer.getExtension(ExtNames.Levels); | |
} | |
// Make sure that BookmarksExtension gets current bookmarks when loaded | |
if (extName === ExtNames.Bookmarks) { | |
this._updateBookmarks(); | |
} | |
} | |
_registerLmvEventListeners() { | |
// As soon as all geometry is loaded, make sure that consolidation is triggered if the transition is already finished. | |
// Note that this also includes to cleanup memory if necessary. | |
this.viewer.addEventListener(av.GEOMETRY_LOADED_EVENT, this._onGeometryLoaded.bind(this)); | |
// Update UI if a new model is added | |
this.viewer.addEventListener(av.MODEL_ADDED_EVENT, this._onModelAdded.bind(this)); | |
this.viewer.addEventListener(av.EXTENSION_LOADED_EVENT, this._onExtensionLoaded.bind(this)); | |
// Compute overall loading progress | |
this.viewer.addEventListener(av.PROGRESS_UPDATE_EVENT, this._onProgressUpdate.bind(this)); | |
// If diff is enabled, make sure that it starts as soon as all required models are loaded | |
// For this, we don't need all geometry, but other data (fragments loaded + propDbLoader created). | |
// NOTE: We cannot do this in the onModelLoaded() callback, because only the MODEL_ROOT_LOADED_EVENT | |
// is called AFTER loading the whole fragment list and creating the propDbLoader. | |
this.viewer.addEventListener(av.MODEL_ROOT_LOADED_EVENT, this._updateDiff.bind(this)); | |
} | |
// Makes sure that all visible models and fully loaded models are consolidated if memory allows it. | |
// Triggered whenever a model finished loading, added, or if new memory gets available. | |
_consolidateVisibleModels() { | |
// Free some memory if needed and possible | |
this._cleanupModels(); | |
//Skip consolidation on mobile due to more limited memory on weaker devices. | |
if (isMobileDevice()) | |
return; | |
//This duplicates logic from Viewer3D.loadDocumentNode, because AggregateView does its | |
//loadModel calls manually. We need this flag to be able to debug memory issues in Design Collab | |
let cparam = getParameterByName("useConsolidation"); | |
if (cparam === "false") | |
return; | |
// For each 3D model, trigger consolidation if needed and possible | |
const visibleModels = this.viewer.getVisibleModels(); | |
for (let i=0; i<visibleModels.length; i++) { | |
const model = visibleModels[i]; | |
// Don't consolidate anything if we are running out of memory | |
if (this.memTracker.memoryExceeded()) { | |
return; | |
} | |
// Consolidation requires model + all geometry to be loaded | |
const modelLoaded = model && model.isLoadDone(); | |
if (!modelLoaded) { | |
return; | |
} | |
// Skip anything 2D (sheets/leaflets) | |
if (model.is2d()) { | |
return; | |
} | |
// Consolidate it if not done already | |
if (!model.isConsolidated()) { | |
// consolidate model | |
const glRenderer = this.viewer.impl.glrenderer(); | |
const materials = this.viewer.impl.getMaterials(); | |
model.consolidate(materials, undefined, glRenderer, true); | |
// Consolidation raises mem-consumption. => Run cleanup again. | |
this._cleanupModels(); | |
} | |
} | |
} | |
// Makes sure that long unused models are removed deleted to free memory if needed. | |
// | |
// It is called whenever either... | |
// a) A model goes out of use (hideModel() call) | |
// b) Memory consumption has grown (model geometry loaded or model consolidation was run) | |
_cleanupModels() { | |
// Make sure that LRU timestamps are up-to-date. | |
this._updateModelTimestamps(); | |
// define customized unloadModel function | |
const unloadModel = (model) => { | |
const node = model.getDocumentNode(); | |
// Usually, the model supposed to be uploaded will not be in use. | |
// But: In rare cases, the geometryLoaded event of a model might arrive earlier than the onDone() callback. | |
// Reason is that the onDone() callback is delayed by a setTimeout in GuiViewer.onSuccessChanged(). | |
// Such a model is added to the viewer before we even know about it. Therefore, it doesn't have any timestamp yet. | |
// | |
// If this happens in combination with a "close to memory limit" scenario, the model is incorrectly classified as "unused" | |
// by ModelMemoryTracker. TODO: Check if we can find a more elegant solution to prevent this problem. | |
const item = this._getItem(node); | |
if (item && item.visible) { | |
return; | |
} | |
this.unload(node); | |
}; | |
// let memTracker free memory if necessary | |
this.memTracker.cleanup(unloadModel); | |
} | |
// Update LRU timestamps for all models that are currently in use. | |
// We must overload/customize this function to consider the 2d support models | |
_updateModelTimestamps() { | |
let visibleModels = this.viewer.getVisibleModels().slice(); | |
// Consider diff support models if 2D diff is active | |
const supp = this._getDiffSupportModels(); | |
if (this.diff.enabled && supp) { | |
if (supp.diff) visibleModels.push(supp.diff); | |
if (supp.primary) visibleModels.push(supp.primary); | |
} | |
this.memTracker.updateModelTimestamps(visibleModels); | |
} | |
_stopActiveTools() { | |
const sectionExtension = this.viewer.getExtension('Autodesk.Section'); | |
if (sectionExtension && sectionExtension.isActive()) { | |
sectionExtension.enableSectionTool(false); | |
} | |
this.stopBimWalk(); | |
// Pass false to the getPropertyPanel() method, otherwise it would try to create the | |
// property panel if not yet existing. | |
const propertyPanel = !this.options.headlessViewer && this.viewer.getPropertyPanel(false); | |
if (propertyPanel && propertyPanel.isVisible()) { | |
propertyPanel.setVisible(false); | |
} | |
} | |
_onModelLoaded(model) { | |
// get model item | |
let item = this._getItem(model); | |
if (!item) { | |
// This can only happen if unload() has been called after load meanwhile. | |
// In this case, we don't need the model anymore. | |
return; | |
} | |
// If this is the first viewable, we use it to initialize the global offset | |
if (model.is3d() && this.waitForFirstModel && !this.options.ignoreGlobalOffset) { | |
this.globalOffset = new THREE.Vector3().copy(model.myData.globalOffset); | |
this._onGlobalOffsetChanged(); | |
} | |
// Store model in modelItem | |
item.model = model; | |
// If item is still set to visible, add it | |
if (item.visible) { | |
this._showModel(item.model); | |
} | |
// Make sure that the new model gets latest timestamp for LRU caching | |
this._updateModelTimestamps(); | |
// Update diff progress bar if a model-root of a diffModel was loaded | |
this._updateDiffLoadProgress(); | |
this._onModelLoadEnded(item.node); | |
} | |
_onModelLoadFailed(bubbleNode, errorCode) { | |
this._onError(`Failed to load model: ${bubbleNode.name()}. Error code: ${errorCode}`); | |
this._onModelLoadEnded(bubbleNode); | |
} | |
// Called if model-root load succeeded, failed, or was cancelled | |
_onModelLoadEnded(bubbleNode) { | |
const item = this._getItem(bubbleNode); | |
item.loading = false; | |
// Allow other models to load | |
if (bubbleNode.is3D() && this.waitForFirstModel) { | |
// Unblock 3D model loading | |
this.waitForFirstModel = false; | |
// Trigger loading of any models for which loading was deferred. | |
for (var key in this.modelItems) { | |
// Note that load() already takes care that nothing is loaded twice | |
const node = this.modelItems[key].node; | |
this.load(node); | |
} | |
} | |
} | |
/* @param {av.Model} */ | |
_initCamera(model, withTransition) { | |
this.viewer.impl.setViewFromFile(model, !withTransition); | |
// Makes sure that the viewer home button is also properly working for 2D sheets. | |
this.viewer.impl.controls.recordHomeView(); | |
this.cameraInitialized = true; | |
// If there was a setCameraGlobal that we couldn't apply immediately, do it now | |
if (this.pendingCamera) { | |
this._applyPendingCameraWhenReady(); | |
} | |
} | |
// Returns item for a given bubbleNode/key/model - or undefined if unknown. | |
_getItem(bubbleNode) { | |
let key = makeKey(bubbleNode); | |
return this.modelItems[key]; | |
} | |
// The refPoint is a 3d position from which we know that all 3d viewables to display must be close to it. | |
// We use the refPoint to determine the globalOffset that we use in LMV for loadOptions. | |
// If the refPoint changes slightly, globalOffset may remain the same. But if it is by a large value, | |
// the global offset needs to be updated - which requires a reload for all models. | |
// | |
// Purpose of all this is to determine in advance which globalOffset we have to use for loading. The globalOffset must be... | |
// 1. close to the geo-coords of each displayed model | |
// 2. known in advance (because it is used for loadOptions) | |
// 3. identical for all models displayed at once | |
// | |
// @param {BubbleNode} bubbleNode - node that is about to be shown | |
_updateRefPoint(bubbleNode) { | |
const isFirstViewable = this.isEmpty(); | |
if (!isFirstViewable || !bubbleNode.is3D() || this.options.ignoreGlobalOffset) { | |
return; | |
} | |
// Following code assumes that getAecModelData is called after AecModelMata was downloaded. | |
// Having no AECModelData in this case means that this is a model other than Revit | |
// and setting globalOffset to zero will essentially reset the potentially existing correct global offset | |
// If we encounter such a scenario (e.g. IFC model), we just skip updating global offset here | |
const aecModelData = bubbleNode.getAecModelData(); | |
if (!aecModelData) { | |
return; | |
} | |
const tf = aecModelData && aecModelData.refPointTransformation; // Matrix4x3 as array[12] | |
this.refPoint = tf ? | |
{ x: tf[9], y: tf[10], z: 0.0 } : // refPoint = refPointTransform * (0,0,0) | |
{ x: 0, y: 0, z: 0 }; // fallback: use origin if we have no aecModelData | |
// Workaround: We apply the global offset only in (x,y), because: | |
// a) The large offsets are usually only happening for (x,y) | |
// b) An offset in z would need special handling in the LevelsExtension | |
// Check if the current globalOffset is sufficiently close to the refPoint to avoid inaccuracies. | |
const MaxDistSqr = 4.0e6; | |
const distSqr = this.globalOffset && THREE.Vector3.prototype.distanceToSquared.call(this.refPoint, this.globalOffset); | |
if (!this.globalOffset || distSqr > MaxDistSqr) { | |
this.globalOffset = new THREE.Vector3().copy(this.refPoint); | |
// unload all previous models that we loaded with previous geo offset | |
// In theory, we could skip 2d models here. But, a changed offset usually | |
// happens only on project switch, so that the 2d models are unlikely to be needed too. | |
this.unloadAll(); | |
this._onGlobalOffsetChanged(); | |
} | |
} | |
_onGlobalOffsetChanged() { | |
// Make sure that bookmark positions are calculated based on the latest global offset | |
const bookmarkExt = this.viewer.getExtension(ExtNames.Bookmarks); | |
if (bookmarkExt) { | |
bookmarkExt.resetGlobalOffset(this.globalOffset); | |
} | |
} | |
async _loadExtensions() { | |
const loadingPromises = []; | |
// Cross-fade effects (e.g. for ghosted floors on LevelsPanel hover) are optional. On mobile, we disable them to save memory. | |
let enableCrossFade = !av.isMobileDevice(); | |
if (enableCrossFade) { | |
loadingPromises.push(this._loadExtension(ExtNames.CrossFade)); | |
} | |
loadingPromises.push(this._loadExtension(ExtNames.Levels, this.viewer.config)); | |
loadingPromises.push(this._loadExtension(ExtNames.ModelStructure, this.viewer.config)); | |
// Disable auto-load of default hyperlink extension. Reasons: | |
// a) It produces asserts for background loading, because it assumes 'viewer.model' to exist | |
// b) We are using AEC hyperlink extension instead. | |
let config = this.viewer.config; | |
config.disabledExtensions = this.viewer.config.disabledExtensions || {}; | |
config.disabledExtensions.hyperlink = true; | |
// Load AEC Hyperlink extension | |
loadingPromises.push(this._loadExtension(ExtNames.Hyperlinks, { | |
// Connect hyperlink handler | |
loadViewableCb: (bubbleNode, numHyperlinks) => { | |
if (this.onHyperlink) { | |
this.onHyperlink(bubbleNode, numHyperlinks); | |
} else { | |
// default-handler: Trigger view switch to linked sheet | |
this.switchView([bubbleNode]); | |
} | |
}, | |
})); | |
// Minimap | |
if (!this.options.disableMinimap) { | |
loadingPromises.push(this._loadExtension(ExtNames.Minimap, { | |
// Allow clients to track minimap usage. | |
// Todo: Finally, tracking should be consistently handled automatically inside LMV. | |
trackUsage: this._trackMinimapUsage ? this._trackMinimapUsage.bind(this) : undefined | |
})); | |
} | |
return Promise.all(loadingPromises); | |
} | |
// Load LMV extension with given options + optional options specified via this.options.extensionOptions | |
async _loadExtension(extName, options) { | |
const extOptions = this.options.extensionOptions; | |
const customOptions = extOptions && extOptions[extName]; | |
const combinedOptions = Object.assign({}, options, customOptions); | |
return this.viewer.loadExtension(extName, combinedOptions); | |
} | |
// Load/Unload extensions depending on whether we are entering a 2D or 3D view | |
_updateExtensions() { | |
const bookmarkOptions = { | |
// Share global offset, so that bookmarks are placed correctly | |
globalOffset: this.globalOffset, | |
onBookmark: (bookmark, camera) => { | |
// Forward to handler to invoke view switch | |
if (this.onBookmark) { | |
this.onBookmark(bookmark, camera); | |
} else { | |
// default handler: Invoke view-switch to selected bookmark | |
this.switchView([bookmark]); | |
} | |
// Activate BIMWalk for perspective views. It is important to do that | |
// after invoking the handler to switch views. | |
// Otherwise, BimWalk would be switched off again. | |
if (camera.isPerspective) { | |
this.startBimWalk(); | |
} | |
}, | |
clusterfck: this.options.clusterfck, | |
clusteringThreshold: 110, // threshold is (icon_width * 5), depends on "THREE.Vector3.distanceTo()" | |
}; | |
const dropMeOptions = { | |
enableGuidance: true, | |
onDrop: this._handleDropMe.bind(this) // connect DropMe handler | |
}; | |
const toggleExtension = (extName, options, enable) => { | |
const extLoaded = this.extensionLoaded[extName]; | |
if (!extLoaded && enable) { | |
this._loadExtension(extName, options).catch(err => this._onError(err)); | |
} else if (extLoaded && !enable) { | |
this.viewer.unloadExtension(extName); | |
} | |
this.extensionLoaded[extName] = enable; | |
}; | |
// only 3D | |
toggleExtension(ExtNames.Bookmarks, bookmarkOptions, this.is3D); | |
// only 2D | |
toggleExtension(ExtNames.DropMe, dropMeOptions, !this.is3D); | |
} | |
_initForFirstViewable(bubbleNode) { | |
// Check if we are about to switch between 2D and 3D view | |
const dimChanged = (this.is3D !== bubbleNode.is3D()); | |
if (dimChanged) { | |
// Make sure that UI, camera, tools etc. are reset on next model-add. | |
// This is only for convenience, so that the client doesn't have to care to call reset() from outside on 2D/3D switches. | |
// If reset() was already called, we don't do it again, because this would | |
// revert some settings (setCameraGlobal or startBimWalk) that the user might have done for this view already. | |
if (!this.resetOnNextModelAdd) { | |
this.reset(); | |
} | |
this.is3D = bubbleNode.is3D(); | |
} | |
// Make sure that the right extensions are loaded/unloaded | |
this._updateExtensions(); | |
// Before loading a 3D viewable, we must choose a refPoint for the view. | |
this._updateRefPoint(bubbleNode); | |
} | |
_updateBookmarks() { | |
let ext = this.viewer.getExtension(ExtNames.Bookmarks); | |
if (!ext) { | |
// Extension is only loaded in 3D mode | |
return; | |
} | |
ext.resetBookmarks(this.bookmarks); | |
} | |
// Modify current camera based on the one specified in last setCameraGlobal call. | |
_applyPendingCameraWhenReady() { | |
// Applying the user camera requires the viewer camera to be default-initialized first. | |
// This also implies that the viewer global offset is already determined. | |
if (!this.cameraInitialized) { | |
return; | |
} | |
// consume pending camera overrides | |
let newCam = this.pendingCamera; | |
this.pendingCamera = null; | |
let cam = this.viewer.impl.camera; | |
if (newCam.position) cam.position.copy(newCam.position) | |
if (newCam.target) cam.target.copy(newCam.target) | |
if (newCam.up) cam.up.copy(newCam.up); | |
if (newCam.fov) cam.fov = newCam.fov; | |
if (newCam.isPerspective !== undefined) cam.isPerspective = newCam.isPerspective; | |
const globalOffset = this.globalOffset || this.viewer.model.getData().globalOffset; | |
// GlobalOffset only affects 3D models | |
if (this.is3D && !this.options.ignoreGlobalOffset) { | |
cam.position.sub(globalOffset); | |
cam.target.sub(globalOffset); | |
} | |
this.viewer.impl.syncCamera(); | |
} | |
_handleDropMe(pos, dir, mode) { | |
if (this.onDrop) { | |
this.onDrop(pos, dir, mode); | |
return; | |
} | |
// Default handler: Purpose is to enable use of AggregatedView on its own and to test/demonstrate its usage. | |
// | |
// The DropMe defines the camera for the 3D view. | |
// But: What 3D view is used for that may finally vary depending on application context. | |
// For the default hanlder, we have no unique definition what 3D view we want to switch to. So, we have to use some heuristic choices. | |
function findMain3DView(viewer) { | |
// Get document root of current sheet | |
// Note that DropMeTool only operates on a single sheet, so using viewer.impl.model is okay here. | |
const model = viewer.impl.model; | |
const sheetNode = model && model.getDocumentNode(); | |
const root = sheetNode && sheetNode.getRootNode(); | |
if (!root) { | |
return; | |
} | |
const views = root && root.search(av.BubbleNode.MODEL_NODE); | |
// If the document has master views, use the first one of those | |
const masterViewFolderName = '08f99ae5-b8be-4f8d-881b-128675723c10'; | |
const isMasterView = (node) => node.parent && node.parent.name() === masterViewFolderName; | |
const masterViews = views.filter(isMasterView); | |
if (masterViews[0]) { | |
return masterViews[0]; | |
} | |
// Check if there is some view with Revit default name "{3D}" | |
const isDefault3DView = (node) => node.name == "{3D}"; | |
const default3DView = views.filter(isDefault3DView)[0]; | |
if (default3DView) { | |
return default3DView; | |
} | |
// last fallback - just return first 3D view we find | |
return views[0]; | |
} | |
// No 3D view that we can switch to | |
let node = findMain3DView(this.viewer); | |
if (!node) { | |
console.warn("DropMe handler: Document does not contain a 3D view to switch to"); | |
return; | |
} | |
// Setup perspective camera from DropMe input | |
let camera = { | |
position: pos, | |
target: pos.clone().add(dir), | |
isPerspective: true // Note that DropMeTool usually produces "inside" views, which are not possible with ortho cameras | |
}; | |
this.switchView(node); | |
this.setCameraGlobal(camera); | |
} | |
// All we want is to make sure that BIMWalk will actually start if startBimWalk was called - independent of timing. | |
_startBimWalkWhenReady() { | |
if (!this.bimWalkStartPending) { | |
return; | |
} | |
let viewReady = !this.resetOnNextModelAdd; // tools/UI are ready for this view | |
let bimWalk = this.viewer.getExtension(ExtNames.BimWalk); | |
// Make sure that FusionOrbit doesn't switch BimWalk off again, because it resets NavTools on load | |
let fusionOrbit = this.viewer.getExtension(ExtNames.FusionOrbit); | |
if (viewReady && bimWalk && fusionOrbit) { | |
bimWalk.activate(); | |
this.bimWalkStartPending = false; | |
} | |
} | |
_unloadDiffTool(clearCache) { | |
const ext = this.viewer.getExtension(ExtNames.DiffTool); | |
if (ext) { | |
if (clearCache) { | |
// discard cached diffs | |
this.diffCache.length = 0; | |
} | |
this.viewer.unloadExtension(ExtNames.DiffTool); | |
} | |
} | |
// currently used for load progress for 2d diff (as long as we need full geometry) | |
_onProgressUpdate(event) { | |
if (!event.model || event.state !== av.ProgressState.LOADING) { | |
return; | |
} | |
// debugging hint: use 'event.model.myData.basePath' as a key | |
this.loadersInProgress[event.model.id] = event.percent; | |
this._updateDiffLoadProgress(); | |
} | |
_getDiffSupportModels() { | |
const supp = this.diff.supportBubbles; | |
if (!supp) { | |
return; | |
} | |
return { | |
diff: this.getModel(this.diff.supportBubbles.diff), | |
primary: this.getModel(this.diff.supportBubbles.primary) | |
}; | |
} | |
// For 2D diff: Checks if the required support models for current diff mode are fully available. | |
// If so, they are returned, otherwise it returns null. | |
_diffSupportModelsReady() { | |
const supportModels = this._getDiffSupportModels(); | |
const diffLoaded = supportModels.diff && supportModels.diff.isLoadDone(); | |
const primaryLoaded = supportModels.primary && supportModels.primary.isLoadDone(); | |
return diffLoaded && primaryLoaded; | |
} | |
// If diff mode is active, all models that are not participating in the diff are | |
// rendered in ghosted style. | |
_updateGhosting() { | |
// keys of all models used in diff | |
let usedInDiff = {}; | |
if (this.diff.enabled && this.diffBubbles) { | |
usedInDiff = this.diffBubbles.forEach( (b) => usedInDiff[b.getModelKey()] ); | |
} | |
for (let key in this.modelItems) { | |
// skip models if root is not loaded yet | |
let model = this.getModel(key); | |
if (!model) { | |
continue; | |
} | |
let ghosted = this.diff.enabled && !usedInDiff[key]; | |
let wasGhosted = !!this.modelIsGhosted[key]; | |
if (ghosted != wasGhosted) { | |
model.setAllVisibility(!ghosted); | |
// keep track which models are ghosted | |
if (ghosted) { | |
this.modelIsGhosted[key] = true; | |
} else { | |
delete this.modelIsGhosted[key]; | |
} | |
} | |
} | |
} | |
// Get array of models from given bubbleNode array. | |
// NOTE: This will be removed, because the only difference to viewer.getVisibleModels() is that it may contain null-entries | |
// for models that are still loading. Note that _updateDiffLoadProgress() then needs some revision too, because it currently relies on these null-entries. | |
getModels(bubbleNodes) { | |
return bubbleNodes.map( (node) => this.getModel(node) ); | |
} | |
_signalDiffLoadProgress(percent, force) { | |
if (force || percent !== this._lastDiffLoadPercent) { | |
// update progress bar | |
const msg = av.i18n.t('Loading Model for Change Visualization'); | |
this.diff.progressCallback(percent, msg); | |
this._lastDiffLoadPercent = percent; | |
} | |
} | |
_updateDiffLoadProgress() { | |
// Do nothing if we are not waiting for a diff | |
if (!this.diffNeedsUpdate || !this.diff.enabled || this.diff.empty || !this.diff.progressCallback) { | |
return; | |
} | |
// get array of all models | |
const diffModels = this.getModels(this.diff.diffBubbles); | |
const primModels = this.getModels(this.diff.primaryBubbles); | |
const allModels = diffModels.concat(primModels); | |
// for 2d, also consider support models | |
const supportModels = this._getDiffSupportModels(); | |
if (supportModels) { | |
allModels.push( | |
supportModels.diff, | |
supportModels.primary | |
); | |
} | |
// check how many models roots need to be loaded | |
const loadedModels = allModels.filter(Boolean).length; | |
// Reserve the first 10% progress bar for loading model roots | |
const ModelRootPercent = 10; | |
// As long as not all roots are loaded, just track model-root loading | |
if (loadedModels < allModels.length) { | |
const percent = Math.floor(ModelRootPercent * loadedModels / allModels.length); | |
this._signalDiffLoadProgress(percent); | |
return; | |
} | |
// For 2D diff, we track the model-load progress including geometry | |
// This can be changed as soon as we eliminate geom loading from 2d too. | |
if (!this.is3D) { | |
// sum up progress percent of all models we need | |
const countPercentDone = (acc, model) => acc + (this.loadersInProgress[model.id] | 0); | |
const geomPercentDone = allModels.reduce(countPercentDone, 0); | |
const geomPercentTotal = allModels.length * 100; | |
// map average loader percent to the upper 90%, because we had reserved some percentage | |
// for the model root loading already. | |
const percent = ModelRootPercent + Math.floor((100 - ModelRootPercent) * geomPercentDone / geomPercentTotal); | |
this._signalDiffLoadProgress(percent); | |
return; | |
} | |
// Track fragment list loading | |
const countLoadedFrags = (acc, model) => (acc + model.getData().fragsLoadedNoGeom); | |
const countFragsTotal = (acc, model) => (acc + model.getData().metadata.stats.num_fragments); | |
const fragsLoaded = allModels.reduce(countLoadedFrags, 0); | |
const fragsTotal = allModels.reduce(countFragsTotal, 0); | |
const percent = ModelRootPercent + Math.floor((100 - ModelRootPercent) * fragsLoaded / fragsTotal); | |
this._signalDiffLoadProgress(percent); | |
} | |
_updateDiff() { | |
if (!this.diffNeedsUpdate) { | |
return; | |
} | |
this._updateGhosting(); | |
this._updateDiffLoadProgress(); | |
if (!this.diff.enabled) { | |
// Unload extension if needed on DiffMode disabling | |
this._unloadDiffTool(); | |
return; | |
} | |
// collect all models participating in the diff | |
const diffModels = this.getModels(this.diff.diffBubbles); | |
const primModels = this.getModels(this.diff.primaryBubbles); | |
for (let i=0; i<diffModels.length; i++) { | |
const diffModel = diffModels[i]; | |
const primModel = primModels[i]; | |
// If loading is in progress, modelA or modelB might just be a boolean value | |
const diffModelLoaded = (diffModel instanceof av.Model) && (diffModel.isOTG() || diffModel.isLoadDone()); | |
const primModelLoaded = (primModel instanceof av.Model) && (primModel.isOTG() || primModel.isLoadDone()); | |
if (!diffModelLoaded || !primModelLoaded) { | |
// We cannot start diff until all models are loaded (for 2D, we also need geometry) | |
return; | |
} | |
// Make sure that the property db is loaded. Otherwise, we will retry on next | |
// MODEL_ROOT_LOADED event. | |
if (!diffModel.getPropertyDb() || !primModel.getPropertyDb()) { | |
return; | |
} | |
} | |
// If we need support models (for 2D diff), make sure that these are reday too | |
let supportModels = undefined; | |
const supportModelsNeeded = !!this.diff.supportBubbles; | |
if (supportModelsNeeded) { | |
if (!this._diffSupportModelsReady()) { | |
// We cannot start diff before support models (incl. geometry) | |
// are fully available. So stop here and wait until called again by | |
// next geometry-loaded event. | |
return; | |
} | |
supportModels = this._getDiffSupportModels(); | |
} | |
const elapsed = performance.now() - this.diffStartTime; | |
console.log('Time for loading diff models: ', elapsed); | |
this._setDiffModels(diffModels, primModels, supportModels); | |
} | |
_setDiffModels(diffModels, primaryModels, supportModels) { | |
// don't reset diff again until next setModels() call | |
this.diffNeedsUpdate = false; | |
// If we are in DiffMode, but no model version changed, we just show | |
// all in ghosted and are done. | |
if (!primaryModels.length) { | |
this._unloadDiffTool(); | |
return; | |
} | |
const ext = this.viewer.getExtension(ExtNames.DiffTool); | |
if (ext) { | |
ext.replaceModels(diffModels, primaryModels, supportModels); | |
} else { | |
const cfg = { | |
diffModels: diffModels, | |
primaryModels: primaryModels, | |
supportModels: supportModels, // = { primary: av.Model, diff: av.Model } | |
diffadp: false, | |
availableDiffModes: [ 'overlay', 'sidebyside' ], | |
diffMode: 'overlay', | |
versionA: this.diff.primaryBubbleLabel, | |
versionB: this.diff.diffBubbleLabel, | |
refNames: this.diff.refNames, | |
mimeType: 'application/vnd.autodesk.revit', // change to "C4R or Revit" | |
hotReload: true, | |
diffCache: this.diffCache, | |
// To run with OTG, we need to use the new code path for side-by-side that uses a single viewer instance. | |
useSplitScreenExtension: true, | |
// Add a section with details, such as which versions are compared. | |
// If activated, the overlay displaying the version labels is not | |
// rendered as that information is already contained in the panel. | |
// This option is disabled by default for backwards compatibility. | |
showDetailsSection: true, | |
progress: (percent, state) => { | |
// Diff progress might be null if diffConfig has changed already. This may happen when clicking ('Exit Changes') | |
// while waiting for the diff. | |
if (!this.diff.progressCallback) { | |
return; | |
} | |
if (state === Autodesk.DiffTool.DIFFTOOL_PROGRESS_STATES.LoadingPropDb) { | |
this.diff.progressCallback(percent, av.i18n.t('Loading element properties')); | |
return; | |
} | |
if (percent === 100 && this.onDiffDone) { | |
this.onDiffDone(); | |
} | |
this.diff.progressCallback(percent, av.i18n.t('Computing change visualization')); | |
}, | |
// Exclude objects based on selected level | |
excludeFromDiff: (model, dbId) => { | |
return this.levelsExtension && !this.levelsExtension.floorSelector.isVisible(model, dbId); | |
}, | |
// Whenever the DiffTools sets dbIds to visible for a model, | |
// we rerun the current FloorSelectionFilter to make sure that | |
// an object is only shown if both - FloorSelectionFilter and | |
// DiffTool allow it. | |
setNodesOff: (model) => { | |
const levelExt = this.levelsExtension; | |
if (levelExt) { | |
levelExt.floorSelector._floorSelectorFilter.reApplyFilter(model); | |
} | |
}, | |
hideModeSwitchButton: true | |
}; | |
this._loadExtension(ExtNames.DiffTool, cfg).catch(err => this._onError(err)); | |
} | |
} | |
} | |
AggregatedView.ExtNames = ExtNames; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I need complete source code for DiffTool Extension. Where can I get that?. Please help me to find this.