Skip to content

Instantly share code, notes, and snippets.

@wallabyway
Last active March 18, 2021 13:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wallabyway/992b1a26606003e7e4efd9c550fb5b9f to your computer and use it in GitHub Desktop.
Save wallabyway/992b1a26606003e7e4efd9c550fb5b9f to your computer and use it in GitHub Desktop.
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;
@Boomasundari
Copy link

I need complete source code for DiffTool Extension. Where can I get that?. Please help me to find this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment