BoundingClientRectObserver using IntersectionObserver returns empty rects - intersection-observer

I'm trying to create an observer that triggers when the bounding rect of an element changes (a BoundingClientRectObserver), and this is what I have so far:
customElements.define(
'bounding-client-rect-observer-helper',
class extends HTMLDivElement {
disconnectedCallback () { this.dispatchEvent(new Event('$disconnected')) }
},
{extends: 'div'}
)
class BoundingClientRectObserver {
#intersectionObserver
#helperElement
#timeoutId
constructor (element, callback, startNow=true) {
if (typeof element == 'string') {
element = document.querySelector(element)
}
this.element = element
this.callback = callback
this.#helperElement = document.createElement(
'div', {is: 'bounding-client-rect-observer-helper'}
)
this.#helperElement.style.position = 'fixed'
this.#helperElement.style.visibility = 'hidden'
this.#intersectionObserver = new IntersectionObserver(
entries => this.#debouncedCallback(entries),
{root: this.element, threshold: [0,1.0]}
)
this.disconnected = this.disconnected.bind(this)
this.running = false
if (startNow) this.start()
}
start () {
if (this.running) return
this.element.append(this.#helperElement)
this.#helperElement.addEventListener('$disconnected', this.disconnected)
this.#intersectionObserver.observe(this.#helperElement)
this.running = true
}
#innerCallback () {
const rect = this.element.getBoundingClientRect()
this.#helperElement.style.width = rect.width + 'px'
this.#helperElement.style.height = rect.height + 'px'
this.#helperElement.style.left = rect.left + 'px'
this.#helperElement.style.top = rect.top + 'px'
this.callback(rect)
}
#debouncedCallback (entries) {
console.log(entries)
clearTimeout(this.#timeoutId)
this.#timeoutId = setTimeout(() => this.#innerCallback(), 200)
}
disconnected () {
if (this.running) {this.element.append(this.#helperElement)}
}
stop () {
if (!this.running) return
this.#intersectionObserver.unobserve(this.#helperElement)
this.#helperElement.removeEventListener('$disconnected', this.disconnected)
this.#helperElement.remove()
this.running = false
}
}
I'm adding a helper element to the root element (the one I want to check if its bounding client rect changes) with the same bounding client rect, since in the IntersectionObserver docs it says that the element to observe should be a descendant of the root element. So in theory, everytime the size or position of the root element changes, the callback should be called, but this is not happening, and I suspect it's because bounding client rects in entries (I'm printing the entries in #debouncedCallback function) are empty. can you see where I got it wrong?

Related

is there is any solution for openlayer pop up not working?

var container = document.getElementById('popup');
var content = document.getElementById('popup-content');
var closer = document.getElementById('popup-closer');
var overlay = new Overlay({
element: container,
autoPan: true,
autoPanAnimation: {
duration: 250,
},
});
closer.onclick = function () {
overlay.setPosition(undefined);
closer.blur();
return false;
};
this.draw.on('drawend', (e: any) => {
const coordinate = e.coordinate;
const hdms = toStringHDMS(toLonLat(coordinate));
content.innerHTML =
'<p> Current Position are :</p><code>' + hdms + '</code>';
overlay.setPosition(coordinate);
});
this.map.addOverlay(overlay);
}
pop up is not coming after the draw end. but the pop up is working on map single click events .i could not find anything related on internet

How to check if the model out of the viewer container?

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
}
}

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');

Events to listen to before updating camera

Updating camera and target based on dbid of a selected node. The code starts with MobileVR function. I am updating camera and target according to frag mesh retrieved with dbid and then moving to VR mode. Currently I have an event listeners for GEOMETRY_LOADED_EVENT, OBJECT_TREE_CREATED_EVENT and EXTENSION_LOADED_EVENT. Currently it works with using a timeout setTimeout(() => { onSpaceObjectTreeCreated(); }, 3000); see image 1, but not without the the timeout image 2. Is there some other event that I should wait before running the code or updating the camera?
function onSpaceObjectTreeCreated() {
const nav = viewer.navigation;
const cam = nav.getCamera();
const it = viewer.model.getData().instanceTree;
let xPos, yPos, zPos;
it.enumNodeFragments(nodeId, (frag) => {
const mesh = viewer.impl.getRenderProxy(viewer.model, frag);
xPos = mesh.matrixWorld.elements[12];
yPos = mesh.matrixWorld.elements[13];
zPos = mesh.matrixWorld.elements[14];
console.log('x: ' + xPos + ' y: ' + yPos + ' z: ' + zPos);
}, false);
zPos = -41000;
cam.position.set(xPos, yPos, zPos);
cam.target.set(xPos, yPos + 10000, zPos);
}
function onViewerGeometryLoaded() {
const nav = viewer.navigation;
const cam = nav.getCamera();
if (nodeId == -1) {
viewer.setGroundShadow(false);
let xValue = viewer.getCamera().position.x;
let yValue = viewer.getCamera().position.y;
let zValue = viewer.getCamera().position.z;
let bbz = viewer.model.getData().bbox.min.z;
let zValue2 = zValue - bbz;
zValue = zValue * 0.3;
yValue = (zValue2 * 0.7071) * -1;
let nav = viewer.navigation;
let cam = viewer.getCamera();
cam.position.set(xValue, yValue, zValue);
} else {
setTimeout(() => {
onSpaceObjectTreeCreated();
}, 3000);
}
viewer.impl.sceneUpdated();
viewer.navigation.updateCamera();
document.getElementById("toolbar-vrTool").click();
};
function afterViewerEvents() {
var events = [
Autodesk.Viewing.GEOMETRY_LOADED_EVENT,
Autodesk.Viewing.OBJECT_TREE_CREATED_EVENT,
Autodesk.Viewing.EXTENSION_LOADED_EVENT
];
async.each(events,
function (event, callback) {
var handler = function (ev) {
viewer.removeEventListener(
event, handler);
console.log('Event: ' + event);
console.log('Ev: ' + ev.extensionId);
callback();
};
viewer.addEventListener(
event, handler);
},
function (err) {
onViewerGeometryLoaded();
});
}
function mobileVR(arkUrn: string, lviUrn: string, zOffset: number, spaceId: number) {
let element = document.getElementById("mobileViewer");
viewer = new Autodesk.Viewing.Private.GuiViewer3D(element);
let options = {
'env': 'AutodeskProduction',
'getAccessToken': getToken,
'refreshToken': getToken
};
av.Initializer(
options,
() => {
viewer.initialize();
loadDocument(arkUrn, zOffset);
if (lviUrn != "") {
loadDocument(lviUrn, zOffset);
}
viewer.loadExtension('Autodesk.Viewing.WebVR');
}
);
nodeId = spaceId;
afterViewerEvents();
}
Try to hookup the events after initializing the viewer and before loading the document:
viewer.initialize();
afterViewerEvents();
loadDocument(arkUrn, zOffset);
Also I don't get why you are using Autodesk.Viewing.EXTENSION_LOADED_EVENT, several extensions are being loaded automatically by the viewer upon startup or model loading, this event will be fired multiple times. If you are looking for a specific extension being loaded you need to check the extensionId and remove the handler only if this is the extension you are waiting for...
Hope that helps

Custom Parameters get cleared canvas to Json

I am adding svg to canvas and want to set custom Element Parameters. I shows custom parameter when we console log getActiveObject() but when we use canvas.toJSON() Element Parameter node values does not change.
var canvas = new fabric.Canvas('designcontainer'),
/* Save additional attributes in Serialization */
var ElementParameters = {
ElementType:'',
imageType:'',
top:'',
left:'',
colors:'',
isBaseTier:'',
atLevel:''
};
fabric.Object.prototype.toObject = (function (toObject) {
return function () {
return fabric.util.object.extend(toObject.call(this), {
ElementParameters:{
ElementType:'',
imageType:'',
top:'',
left:'',
colors:'',
isBaseTier:'',
atLevel:''
},
});
};
})(fabric.Object.prototype.toObject);
/* End : Save additional attributes in Serialization */
var Designer = {
addElement: function(e,p){ /* e = element, image src | p = parameters set for image */
if(p.imageType == "svg"){
if(p.ElementType == "caketier"){
var group = [];
console.log('Before ');
console.log(ElementParameters);
$.extend(ElementParameters,p);
console.log('After ');
console.log(ElementParameters);
fabric.loadSVGFromURL(e,function(objects,options){
var shape = fabric.util.groupSVGElements(objects,options);
var bound = shape.getBoundingRect();
shape.set({
left: p.left,
top: p.top,
width:bound.width+2,
height:bound.height,
angle:0,
centeredScaling:true,
ElementParameters:ElementParameters
});
if(shape.paths && baseColor.length > 0){
for(var i = 0;i<shape.paths.length;i++) shape.paths[i].setFill(baseColor[i]);
}
canvas.add(shape);
shape.setControlsVisibility(HideControls);
canvas.renderAll();
},function(item, object) {
object.set('id',item.getAttribute('id'));
group.push(object);
});
}
}
}
}
$(".tierbox").on('click',function(){
var i = $(this).find('img'),
src = i.attr('src'),
param = i.data('parameters');
Designer.addElement(src,param);
});
Now when I call JSON.stringify(json), Element Parameter node does not get overwrite with values set in shape.set() method.
Replace fabric.Object.prototype.toObject = (function (toObject) { ... } to
fabric.Object.prototype.toObject = (function (toObject) {
return function () {
return fabric.util.object.extend(toObject.call(this), {
ElementParameters:this.ElementParameters
});
};
})(fabric.Object.prototype.toObject);