How to check if the model out of the viewer container? - autodesk-forge

I made an info card and this card will disappear if the viewer is rotated until the model is not visible. I use isNodevisible but it always returns true.
updateInfoCard() {
if (this.infoCard && this.posModel) {
const pos = this.viewer.worldToClient(this.posModel);
console.log(pos);
this.infoCard.style.left = `${Math.floor(
50 + pos.x - this.infoCard.offsetWidth / 2
)}px`;
this.infoCard.style.top = `${Math.floor(
50 + pos.y - this.infoCard.offsetWidth / 2
)}px`;
const id = this.infoCard.dataset.id;
console.log(this.viewer.isNodeVisible(id));
this.infoCard.style.display = this.viewer.isNodeVisible(id)
? "block"
: "none";
}
}

If I understand your question correctly, you'll probably want to do an intersection test between the camera's frustum and the models's bounding box. That can be done like so:
viewer.addEventListener(Autodesk.Viewing.CAMERA_CHANGE_EVENT, function () {
if (!viewer.model) {
return;
}
const camera = viewer.getCamera();
const matrix = new THREE.Matrix4().multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
const frustum = new THREE.Frustum().setFromMatrix(matrix);
const bbox = viewer.model.getBoundingBox();
console.log('Model in the view?', frustum.intersectsBox(bbox));
});
And if you only want to check the visibility of a specific element (based on its dbID) of your model, you can compute its bounding box like so:
function objectBounds(model, dbId) {
const tree = model.getInstanceTree();
const frags = model.getFragmentList();
const objectBounds = new THREE.Box3();
tree.enumNodeFragments(dbId, function (fragId) {
const fragBounds = new THREE.Box3();
frags.getWorldBounds(fragId, fragBounds);
objectBounds.union(fragBounds);
}, true);
return objectBounds;
}

The function isNodeVisible returns the visibility status of your node in the scene. If you do something like this.viewer.hide(id, model) your function will return false.
If I well understood what you want to achieve, you want to hide an info card when the associated object is occluded by others objects, so we can't see it from our point of view ?
So I think what you need is to check for occlusion. You can take a look at the checkOcclusion function of this point cloud markup extension made by Philippe Leefsma.
To check for node occlusion, you basically need to raycast from your point of view to the node that you want to check. If you hit something and it's your node, there is no occlusion. If it's not the same node, it's mean that something occlude your node.
checkOcclusion (markup) {
const clientPoint = this.viewer.worldToClient(
markup.point)
const offset = $(this.viewer.container).offset()
const rayCaster = this.pointToRaycaster(
this.viewer.impl.canvas,
this.viewer.impl.camera, {
x: clientPoint.x + offset.left,
y: clientPoint.y + offset.top
})
const hitTest = this.viewer.model.rayIntersect(
rayCaster, true, this.dbIds)
if (hitTest) {
if (hitTest.fragId === markup.fragId) {
const offset = {
x: hitTest.point.x - markup.point.x,
y: hitTest.point.y - markup.point.y,
z: hitTest.point.z - markup.point.z
}
const dist = Math.sqrt(
offset.x * offset.x +
offset.y * offset.y +
offset.z * offset.z)
if (this.options.logOcclusionDist) {
console.log(dist)
}
if (dist < this.options.occlusionDist) {
return false
}
}
return true
}
}

Related

How can I show 2 different label sets as markup in the forge viewer

I'm trying to show some labels in the forge viewer, based on the example markup code. My code works fine for one dataset, but when I add another one, I get: "BuildingData.js:113 Uncaught TypeError: this.frags[("dbId" + dbId)] is not iterable". The 2 datasets are extensions that extend the BuildingData class & they just create a button & call super.init(). I don't get why it fails when I activate the second label set.
I'm using v7 of the viewer.
class BuildingData extends Autodesk.Viewing.Extension {
constructor(viewer, options) {
super(viewer, options);
}
init(){
//Callback to update labels
const updateLabelsCallback = () => {
if(this.button.getState() === 0) {
this.updateLabels();
}
};
//Events when to update the labels
OnEvent(this.viewer, Autodesk.Viewing.CAMERA_CHANGE_EVENT, updateLabelsCallback);
OnEvent(this.viewer, Autodesk.Viewing.ISOLATE_EVENT, updateLabelsCallback);
OnEvent(this.viewer, Autodesk.Viewing.HIDE_EVENT, updateLabelsCallback);
OnEvent(this.viewer, Autodesk.Viewing.SHOW_EVENT, updateLabelsCallback);
//Add the button to the data bar
this.button.onClick = (ev) => {
this.enabled = !this.enabled;
this.button.setState(this.enabled ? 0 : 1);
this.showLabels();
};
this.viewer.dataBar.getControl("dataGrp").addControl(this.button);
}
cleanup(){
//remove button from data bar
this.viewer.dataBar.getControl("dataGroup").removeControl(this.button);
//remove labels
let labels = FindAll(`#${this.viewer.clientContainer.id} div.adsk-viewing-viewer label.data.${this.dataClass}`);
for(let label of labels) label.remove();
}
showLabels() {
let viewerContainer = Find(`#${this.viewer.clientContainer.id} div.adsk-viewing-viewer`);
//remove old labels
let labels = FindAll(`#${this.viewer.clientContainer.id} div.adsk-viewing-viewer label.data.${this.dataClass}`);
for(let label of labels) label.remove();
//show new labels?
if(!this.enabled) return;
//check if the model tree is available
let tree = this.viewer.model.getInstanceTree();
if(tree === undefined){
console.log("Model tree is not loaded yet");
return;
}
//select sessor & fit to view
const onClick = (e) => {
this.viewer.select(e.currentTarget.dataset.dbId);
this.viewer.utilities.fitToView();
};
this.frags = [];
for(let i = 0; i < this.labels.length; i++){
const label = this.labels[i];
this.frags["dbId" + label.dbId] = [];
// create the label for the dbId
let lbl = document.createElement("label");
lbl.classList.add("data");
lbl.classList.add("update");
lbl.classList.add(this.dataClass);
lbl.dataset.dbId = label.dbId;
lbl.style.cssText = `display: ${this.viewer.isNodeVisible(label.dbId) ? "block" : "none"}`;
//add click event
OnEvent(lbl, "click", onClick);
let span = document.createElement("span");
lbl.appendChild(span);
span.innerText = label.name;
viewerContainer.appendChild(lbl);
//Collect fragment ids of dbId
tree.enumNodeFragments(label.dbId, (fragId) => {
this.frags["dbId" + label.dbId].push(fragId);
this.updateLabels();
});
}
}
updateLabels() {
for(const label of FindAll(`#${this.viewer.clientContainer.id} div.adsk-viewing-viewer .update`)){
const dbId = label.dataset.dbId;
//get center of the dbId based on the bounding box of the fragments
const pos = this.viewer.worldToClient(this.getBoundingBox(dbId).center());
//position label in the center of the box
label.style.cssText = `left: ${Math.floor(pos.x - label.offsetWidth / 2)}px`;
label.style.cssText +=`top: ${Math.floor(pos.y - label.offsetHeight / 2)}px`;
label.style.cssText +=`display: ${this.viewer.isNodeVisible(dbId) ? "block" : "none"})`;
}
}
getBoundingBox(dbId) {
var fragList = this.viewer.model.getFragmentList();
const nodebBox = new THREE.Box3()
//get bounding box for each fragment
for(const fragId of this.frags["dbId" + dbId]){ //<----- ERROR is here
const fragBBox = new THREE.Box3();
fragList.getWorldBounds(fragId, fragBBox);
nodebBox.union(fragBBox);
}
return nodebBox
}
}
I found the solution, I was trying to update all labels (of both datasets) in the viewer & not only the ones corresponding to the active dataset. Changing 1 line in updateLabels() fixed it:
const label of FindAll(`#${this.viewer.clientContainer.id} div.adsk-viewing-viewer .data.${this.dataClass}`

Please tell me how to appear markup near a specific 2D object in Forge Viewer

I am developing HTML + JS using the forge viewer of Autodesk-forge.
I am looking for a way to display text near a specific object in 2D on the viewer.
Please let me know if there is a fix for the code below, or any other way to do it.
What I tried and result
I practiced the contents of the following article
https://adndevblog.typepad.com/technology_perspective/2020/12/forge-viewer-markup-along-dbid.html
The markup could be displayed on the 3D model as expected.
However, it was not possible to set the markup position on the 2D model.
Code (excerpt)
Calling part
viewer.addEventListener(Autodesk.Viewing.OBJECT_TREE_CREATED_EVENT, () => {
createMarkUp(viewer, {
icons: [
{ dbId: 11146, label: 'コメントあり', css: 'iconWarning fas fa-exclamation-triangle fa-2x
faa-flash animated' },
],
onClick: (id) => {
viewer.select(id);
}
});
});
  Logic part
  
function createMarkUp(viewer, options) {
let _group = null;
let _button = null;
let _icons = options.icons || [];
let _frags = null;
viewer._enabled = true;
load();
showIcons(true);
function load() {
const updateIconsCallback = () => {
if (viewer._enabled) {
updateIcons();
}
};
viewer.addEventListener(Autodesk.Viewing.CAMERA_CHANGE_EVENT, updateIconsCallback);
viewer.addEventListener(Autodesk.Viewing.ISOLATE_EVENT, updateIconsCallback);
viewer.addEventListener(Autodesk.Viewing.HIDE_EVENT, updateIconsCallback);
viewer.addEventListener(Autodesk.Viewing.SHOW_EVENT, updateIconsCallback);
return true;
}
function showIcons(show) {
const $viewer = $('#' + viewer.clientContainer.id + ' div.adsk-viewing-viewer');
// remove previous...
$('#' + viewer.clientContainer.id + ' div.adsk-viewing-viewer label.markup').remove();
if (!show) return;
// do we have anything to show?
if (_icons === undefined || _icons === null) return;
// do we have access to the instance tree?
const tree = viewer.model.getInstanceTree();
if (tree === undefined) { console.log('Loading tree...'); return; }
const onClick = (e) => {
if (options.onClick)
options.onClick($(e.currentTarget).data('id'));
};
_frags = {}
for (var i = 0; i < _icons.length; i++) {
// we need to collect all the fragIds for a given dbId
const icon = _icons[i];
_frags['dbId' + icon.dbId] = []
// create the label for the dbId
const $label = $(`
<label class="markup update" data-id="${icon.dbId}">
<span class="${icon.css}"> ${icon.label || ''}</span>
</label>
`);
$label.css('display', viewer.isNodeVisible(icon.dbId) ? 'block' : 'none');
$label.on('click', onClick);
$viewer.append($label);
// now collect the fragIds
tree.enumNodeFragments(icon.dbId, function (fragId) {
_frags['dbId' + icon.dbId].push(fragId);
updateIcons(); // re-position of each fragId found
});
}
}
function getModifiedWorldBoundingBox(dbId) {
const fragList = viewer.model.getFragmentList();
const nodebBox = new THREE.Box3();
// for each fragId on the list, get the bounding box
for (const fragId of _frags['dbId' + dbId]) {
const fragbBox = new THREE.Box3();
fragList.getWorldBounds(fragId, fragbBox);
nodebBox.union(fragbBox); // create a unifed bounding box
}
return nodebBox
}
function updateIcons() {
for (const label of $('#' + viewer.clientContainer.id + ' div.adsk-viewing-viewer .update'))
{
const $label = $(label);
const id = $label.data('id');
// get the center of the dbId (based on its fragIds bounding boxes)
const pos = viewer.worldToClient(getModifiedWorldBoundingBox(id).center());
// position the label center to it
$label.css('left', Math.floor(pos.x - $label[0].offsetWidth / 2) + 'px');
$label.css('top', Math.floor(pos.y - $label[0].offsetHeight / 2) + 'px');
$label.css('display', viewer.isNodeVisible(id) ? 'block' : 'none');
}
}
 }
Constitution
  HTML+JS
Unfortunately, the code above cannot be applied to 2D drawings as those use a different internal data structure.
With that said, you should still be able to achieve what you want because the Viewer APIs provide some utilities for 2D designs as well. For example, based on this blog post, you can obtain the bounding box of a specific element in your 2D drawing like so:
function getBounds2D(viewer, model, dbId) {
const frags = model.getFragmentList();
let bounds = new THREE.Box3();
let boundsCallback = new Autodesk.Viewing.Private.BoundsCallback(bounds);
let fragIds = frags.fragments.dbId2fragId[dbId]; // Find all fragments including this object's primitives
if (!Array.isArray(fragIds)) {
fragIds = [fragIds];
}
for (const fragId of fragIds) {
// Get the actual mesh with all geometry data for given fragment
const mesh = frags.getVizmesh(fragId);
const vbr = new Autodesk.Viewing.Private.VertexBufferReader(mesh.geometry, viewer.impl.use2dInstancing);
vbr.enumGeomsForObject(dbId, boundsCallback); // Update bounds based on all primitives linked to our dbId
}
return bounds;
}
This will return a THREE.Box3 structure defining the extent of the 2D primitive. From there, you could grab the center of the bounding box, project it to screen coordinates, and use the screen coords to position your HTML overlay elements, for example:
const bounds = getBounds2D(viewer, viewer.model, someDbId);
const coords = viewer.worldToClient(bounds.center());
$label.css('left', Math.floor(coords.x + 'px');
$label.css('top', Math.floor(coords.y + 'px');

How can I assign dynamic data from service via subscribe to doughnut chart in angular?

how can I dynamically assign data to doughnutchartdata. how can I link data coming from service file via subscribe method on ngOnIt to doughnutchartdata. so that I can link multisetdata to one coming dynamically without hardcoding it
Can I also show that data in center dynamically by linking to ctx.fillText
my .ts file
public doughnutChartLabels: Label[] = ['Download Sales', 'In-Store Sales', 'Mail-Order Sales'];
public doughnutChartData: MultiDataSet = [
];
radius = length * Math.SQRT2 / 2;
colors= [
{
backgroundColor: [
'#E6B01C',
'#1454A3',
'#22C3BD',
'yellow'
]
}
];
public doughnutChartType: ChartType = 'doughnut';
public doughnutChartPlugins: PluginServiceGlobalRegistrationAndOptions[] = [{
beforeDraw(chart) {
const ctx = chart.ctx;
const txt = '26';
//Get options from the center object in options
const sidePadding = 60;
const sidePaddingCalculated = (sidePadding / 100) * (this.radius * 2)
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const centerX = ((chart.chartArea.left + chart.chartArea.right) / 2);
const centerY = ((chart.chartArea.top + chart.chartArea.bottom) / 2);
//Get the width of the string and also the width of the element minus 10 to give it 5px side padding
const stringWidth = ctx.measureText(txt).width;
const elementWidth = (this.radius * 2) - sidePaddingCalculated;
// Find out how much the font can grow in width.
const widthRatio = elementWidth / stringWidth;
const newFontSize = Math.floor(30 * widthRatio);
const elementHeight = (this.radius * 2);
// Pick a new font size so it will not be larger than the height of label.
// const fontSizeToUse = Math.min(newFontSize, elementHeight);
// ctx.font = fontSizeToUse + 'px Arial';
ctx.fillStyle = 'blue';
// Draw text in center
ctx.fillText('26', centerX, centerY);
}
}];
constructor(private roleService:RoleDashboardService) { }
ngOnInit() {
this.roleService.getWidget).subscribe((data)=>{
console.log(data)
this.doughnutChartData
console.log(this.doughnutChartData)
})
}
// events
public chartClicked({ event, active }: { event: MouseEvent, active: {}[] }): void {
console.log(event, active);
}
public chartHovered({ event, active }: { event: MouseEvent, active: {}[] }): void {
console.log(event, active);
}
Depends on what backend you use. For example, if u obtain data through php from mysql you would use the below:
array1=[];
array2=[];
//api call on backend
this.http.get('http://localhost/angular9/chartData.php').subscribe(data => {
//create array and push the data
this.array1.push(data);
//assign this array to another arraya and map it
this.array2 = this.array1[0].map(function(item, keys) {
var mixarrayy = Object.keys(item).concat(Object.values(item));
return mixarrayy;
});

How do you render a line markup with the Markups Extension?

I previously asked question about if there was a programatic way to render markups with the markups extension. This worked, or at least, for text markups! Now I am trying to do the same thing with line markups; however, I am stuck with one issue. How do you add the locations to the markup? I have an array of locations that I am trying to assign to it, but there doesn't seem to be a function, and when I try to directly assign locations with markup.location = [etc], for some reason it changes all the numbers to infinity.
So, how can I assign the location array to the markup?
This is how I am loading them:
let MarkupsCore = Autodesk.Viewing.Extensions.Markups.Core;
let line = new MarkupsCore.MarkupFreehand(25, markupTool); //eslint-disable-line
line.locations = [{x: 2, y: 3}]; //something like this
markupTool.addMarkup(line);
line.setSize({ x: markup.x, y: markup.y}, markup.width, markup.height);
line.updateStyle(true);
The MarkupFreehand cannot be used directly, it should be replaced by EditModeFreehand. It's also not easy to archive your request, to create a line markup, in a few codes. Here is the code snippet I used to create a line markup with the MarkupCore extension:
function createLineStartPt( mousePosX, mousePosY, editor ) {
const editMode = markup.editMode;
editor.snapper && editor.snapper.clearSnapped();
editMode.lastX = editMode.initialX = mousePosX;
editMode.lastY = editMode.initialY = mousePosY;
//set the starting point
const position = editMode.position = editor.clientToMarkups( mousePosX, mousePosY );
editMode.movements = [position];
const size = editMode.size = editor.sizeFromClientToMarkups( 1, 1 );
// Create pen.
editor.beginActionGroup();
const markupId = editor.getId();
const createPen = editMode.createPen( markupId, position, size, 0, [{x: 0, y: 0 }] );
createPen.execute();
editMode.createAbsolutePath( position );
editMode.selectedMarkup = editor.getMarkup( markupId );
editMode.creationBegin();
}
function createLineEndPt( mousePosX, mousePosY, editor ) {
const editMode = markup.editMode;
const selectedMarkup = editMode.selectedMarkup;
if( !selectedMarkup || !editMode.creating )
return;
const movements = editMode.movements;
const location = editor.clientToMarkups( mousePosX, mousePosY );
const dx = editMode.lastX - mousePosX;
const dy = editMode.lastY - mousePosY;
const moveTol = 25; // 5^2, compare to square to avoid using square root of distance
if( movements.length > 1 && (dx*dx + dy*dy) < moveTol ) {
movements[movements.length - 1] = location;
editMode.removeFromAbsolutePath( 1 );
} else {
movements.push( location );
editMode.lastX = mousePosX;
editMode.lastY = mousePosY;
}
editMode.addToAbsolutePath([location]);
const appendPen = editMode.setPen( editMode.position, editMode.size, editMode.absolutePath, true );
appendPen.execute();
}
function endLineDrawing( editor ) {
const editMode = markup.editMode;
if( !editMode.creating )
return editMode.creationEnd();
let movements = editMode.movements;
const cameraWidth = editMode.viewer.impl.camera.right - editMode.viewer.impl.camera.left;
const cameraHeight = editMode.viewer.impl.camera.top - editMode.viewer.impl.camera.bottom;
const cameraDiagSq = cameraWidth * cameraWidth + cameraHeight * cameraHeight;
movements = Autodesk.Viewing.Extensions.Markups.Core.Utils.simplify( movements, cameraDiagSq * 0.00000001, true );
const xs = movements.map(function(item) { return item.x });
const ys = movements.map(function(item) { return item.y });
const l = Math.min.apply(null, xs);
const t = Math.min.apply(null, ys);
const r = Math.max.apply(null, xs);
const b = Math.max.apply(null, ys);
const width = r - l; // Already in markup coords space
const height = b - t; // Already in markup coords space
const position = {
x: l + width * 0.5,
y: t + height * 0.5
};
const size = editMode.size = {x: width, y: height};
// Adjust points to relate from the shape's center
const locations = movements.map(function(point){
return {
x: point.x - position.x,
y: point.y - position.y
};
});
const endPen = editMode.setPen( position, size, locations, false );
endPen.execute();
editMode.creationEnd();
}
Then call them in this way:
// Load the extionsion
let markup;
viewer.loadExtension( 'Autodesk.Viewing.MarkupsCore' )
.then(function( markupsExt ) {
markup = markupsExt;
});
// Enter markup editer mode and change it to freehand mode
markup.enterEditMode();
const freehand = new Autodesk.Viewing.Extensions.Markups.Core.EditModeFreehand( markup );
markup.changeEditMode( freehand );
// Create start point of the line
createLineStartPt( 360.03125, 191.3125, markup );
// Create end point of the line
createLineEndPt( 460.03125, 191.3125, markup );
// Tell the markup tool to finish drawing
endLineDrawing( markup );
Here is the result of the above codes:
Note. All mousePos prefixed variables are coordinates in the client coordinate system in the browser viewport, see below link for details. After getting the mosue's clientX or clientY, you have to minus markup.svg.getBoundingClientRect() to adjust their values.
https://developer.mozilla.org/en-US/docs/Web/CSS/CSSOM_View/Coordinate_systems#Client
Hope it helps!

How to move 3D model on Cesium

I wanted to move the model dynamically using keyboard shortcuts. I could not find relevant article on that.
So for now, I'm trying to move the model on click. When click on the model. The model has to move in one direction (increment the value 1 on tick). Find below the sandcastle code for that.
var selectedMesh; var i=0;
var viewer = new Cesium.Viewer('cesiumContainer', {
infoBox: false,
selectionIndicator: false
});
var handle = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
function createModel(url, height) {
viewer.entities.removeAll();
var position = Cesium.Cartesian3.fromDegrees(-123.0744619, 44.0503706, height);
var heading = Cesium.Math.toRadians(135);
var pitch = 0;
var roll = 0;
var orientation = Cesium.Transforms.headingPitchRollQuaternion(position, heading, pitch, roll);
var entity = viewer.entities.add({
name: url,
position: position,
orientation: orientation,
model: {
uri: url,
minimumPixelSize: 128
}
});
viewer.trackedEntity = entity;
viewer.clock.onTick.addEventListener(function () {
if (selectedMesh) {
console.log("Before 0 : " + selectedMesh.primitive.modelMatrix[12]);
selectedMesh.primitive.modelMatrix[12] = selectedMesh.primitive.modelMatrix[12] + 1;
console.log("After 0 : " + selectedMesh.primitive.modelMatrix[12]);
}
});
}
handle.setInputAction(function (movement) {
console.log("LEFT CLICK");
var pick = viewer.scene.pick(movement.position);
if (Cesium.defined(pick) && Cesium.defined(pick.node) && Cesium.defined(pick.mesh)) {
if (!selectedMesh) {
selectedMesh = pick;
}
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
var options = [{
text: 'Aircraft',
onselect: function () {
createModel('../../SampleData/models/CesiumAir/Cesium_Air.bgltf', 5000.0);
}
}, {
text: 'Ground vehicle',
onselect: function () {
createModel('../../SampleData/models/CesiumGround/Cesium_Ground.bgltf', 0);
}
}, {
text: 'Milk truck',
onselect: function () {
createModel('../../SampleData/models/CesiumMilkTruck/CesiumMilkTruck.bgltf', 0);
}
}, {
text: 'Skinned character',
onselect: function () {
createModel('../../SampleData/models/CesiumMan/Cesium_Man.bgltf', 0);
}
}];
Sandcastle.addToolbarMenu(options);
When I click, the model is moving for the first time. After that, It stays on the same place. I've printed the value in the console. It seems the value is not changing. I'm not sure about the problem here. or I'm implementing the transformation wrongly.
If you keep track of the current lat and lon of the entity, and adjust that lat and lon based on user input, all you need to do is update the orientation of the entity.
var lon = // the updated lon
var lat = // updated lat
var position = Cesium.Cartesian3.fromDegrees(lon, lat, height);
var heading = Cesium.Math.toRadians(135);
var pitch = 0;
var roll = 0;
// create an orientation based on the new position
var orientation = Cesium.Transforms.headingPitchRollQuaternion(position, heading, pitch, roll);
Then you just need to update the orientation of the entity.
entity.orientation = orientation;
By changing the value, the models orientation, and therefore position will get updated.