-
Notifications
You must be signed in to change notification settings - Fork 25
Drawing Bounding Boxes with a Web Component
In this tutorial we'll use the Web Component support added in XML3D 5.2 to create our own HTML element that draws the bounding box of a given XML3D element. To do this we'll:
- Create the HTML template for our element
- Add Javascript logic to our element to make it interactive
- Register our new web component with XML3D
- Add an instance of it to an XML3D scene and use it to draw bounding boxes
Before we begin you may want to familiarize yourself with Web Components and how they work. Also, some browsers don't support web components yet so you may need to use a polyfill alongside xml3d.js to provide the missing functionality!
The first step in creating a web component is to craft an HTML <template>
. The body of this template is what will be created inside the Shadow DOM of every instance of our custom element. In our case we'll need some XML3D elements in here that turn the bounding box of an object into an actual box of lines to be drawn by XML3D. Lets start with the template element itself:
<template name="xml3d-boundingbox" target="">
</template>
Every template element needs to specify a name
. This will be the tag name of our custom element and must contain a hyphen (-). We also add a 'target' attribute that we'll use as one way to tell our element which XML3D object we want to see the bounding box of.
Now lets add the XML3D elements to draw the bounding box:
<template name="xml3d-boundingbox" target="">
<material id="bboxMaterial" model="urn:xml3d:material:flat">
<float3 name="diffuseColor">1 0 0</float3>
</material>
<mesh type="lines" material="#bboxMaterial">
<data compute="position = bbox.createPositionsFromBBox(bbox)">
<float3 name="bbox">0 0 0 0 0 0</float3>
</data>
</mesh>
</template>
Our <material>
definition is pretty ordinary, the interesting stuff starts inside our <mesh>
object. First, we set the type
of the mesh to lines
. In this mode XML3D will work its way through the list of positions
that we supply the mesh, taking 2 points at a time and drawing a line between them. In XML3D bounding boxes are axis aligned and only have two 'points', the minimum and the maximum point, as shown below:
Obviously this is not the list of positions that the <mesh>
is expecting, so we'll need to generate that list out of the information we do have: the two points. We can do this using a custom Xflow operator that takes the two points as input and generates an output called position
, which will be the list of vertices that the <mesh>
needs to draw the box. The operator itself is covered in another tutorial, for now lets assume that this operator was already defined and registered with XML3D.
That about does it for our template, but we're going to need some way to put the right input data into that Xflow operator (the float3
element with name bbox
), because right now all it's going to do is draw a box with zero size! Remember, everything inside our template will be inside the Shadow DOM of our custom element, inaccessible from the outside. So how can we do this?
Our web component is going to need a second... component, namely some Javascript to make it interactive and to put the right bounding box data into the input of our Xflow operator. Lets create a new Javascript file and start defining the prototype for our custom element, which will let us add our own functions to it and tap into the built in lifecycle callbacks that web components provide.
The first thing we'll need to do is react to changes in the target
attribute. Lets decide that this attribute accepts a string as input that corresponds to the HTML ID of the element we want to draw the bounding box of. When we want to draw the bounding box of a different element, we just change the target
attribute of our xml3d-boundingbox
element to that other element's ID.
var ourPrototype = {
attributeChangedCallback: function(attr, oldVal, newVal) {
if (attr === "target") {
var otherElement = document.getElementById(newVal);
if (!otherElement) {
return;
}
var bbox = otherElement.getWorldBoundingBox();
this.shadowRoot.querySelector("float3[name='bbox']").textContent = bbox.toDOMString();
}
}
};
The attributeChangedCallback
is one of the lifecycle callbacks that all custom elements can tap into. Any time an attribute on our element changes this function will be called. If that attribute is target
we first find the element in the document with the given ID. If we find one we then get its world space bounding box through the getWorldBoundingBox
function that XML3D provides on all scene elements.
Once we have the bounding box we need to feed it into our Xflow operator so we can generate lines out of it. Remember, everything that was in our <template>
is now inside the Shadow DOM of our custom element, so this is where we need to look to find the float3
element that feeds the operator. We use a query selector that reads "find the first float3 element with a 'name' attribute that has the value 'bbox'" to find that float3 element. Lastly, we convert the bounding box to a DOM string and insert it into the text content of the float3 element. From here Xflow will automatically re-compute the operator and generate our lines for us.
Whew. So this is ok, but maybe not every element that we're interested in has an ID. Lets add another function to our bounding box element that we can call through Javascript, taking directly as input the HTML element that we want to see the bounding box of:
var ourPrototype = {
attributeChangedCallback: function(attr, oldVal, newVal) {
if (attr === "target") {
var otherElement = document.getElementById(newVal);
if (!otherElement) {
return;
}
this.showBBoxFor(otherElement);
}
},
showBBoxFor: function(element) {
var bbox = element.getWorldBoundingBox();
this.shadowRoot.querySelector("float3[name='bbox']").textContent = bbox.toDOMString();
}
};
That looks like enough to do what we need, there's only one more step before we can start using our web component: registering it with XML3D.
Before a web component can be used it has to be registered with XML3D (and with the browser itself, but XML3D takes care of that part for us). We can do this directly in the Javascript file of our component, that way it will be registered automatically whenever we include it in a scene (after the xml3d.js script!)
var ourPrototype = {
attributeChangedCallback: function(attr, oldVal, newVal) {
if (attr === "target") {
var otherElement = document.getElementById(newVal);
if (!otherElement) {
return;
}
this.showBBoxFor(otherElement);
}
},
showBBoxFor: function(element) {
var bbox = element.getWorldBoundingBox();
this.shadowRoot.querySelector("float3[name='bbox']").textContent = bbox.toDOMString();
}
};
window.componentReady = XML3D.registerComponent("bbox-template.html", {proto: ourPrototype});
That's really all there is to it. XML3D will load the HTML file containing our element's <template>
and then register it with the browser. Giving your own prototype for the element is optional, but since we do XML3D will pass that along to the browser as well.
Since this whole process may take a while the registerComponent
function returns a Promise that resolves once the web component is ready to be used. It's best to make use of this to wait until all web components are ready to go before creating instances of them in your scene. In this case we're just going to export the Promise to window
so we can use it later, but in serious use cases you'll probably want to create your own system of registering components.
Finally all that preparation is done and we're ready to create our very own <xml3d-boundingbox>
element in an XML3D scene. Lets take the simple scene from the Basics of XML3D tutorial as our starting point:
<html>
<head>
<script type="text/javascript" src="http://www.xml3d.org/xml3d/script/xml3d.js"></script>
<script type="text/javascript" src="http://www.xml3d.org/xml3d/script/tools/camera.js"></script>
<script type="text/javascript" src="boundingbox-component.js"></script>
<script type="text/javascript">
window.componentReady.then(function() {
var bboxElement = document.createElement("xml3d-boundingbox");
document.querySelector("xml3d").appendChild(bboxElement);
bboxElement.showBBoxFor( document.querySelector("mesh") );
});
</script>
<title>My very first XML3D teapot</title>
</head>
<body>
<xml3d>
<material id="orangePhong" model="urn:xml3d:material:phong">
<float3 name="diffuseColor" >1 0.5 0</float3>
<float name="ambientIntensity" >0.5</float>
</material>
<light id="light1" model="urn:xml3d:light:directional">
<float3 name="intensity" >1 1 1</float3>
</light>
<transform id="cameraTransform" translation="0 0 100"></transform>
<view transform="#cameraTransform"></view>
<group material="#orangePhong" style="transform: translate3d(0px,-20px,0px)" >
<mesh src="resource/teapot.json"></mesh>
</group>
</xml3d>
</body>
</html>
We've included the boundingbox-component.js
file that we created earlier with our custom element prototype. This loads and registers the component with XML3D. We then wait on the Promise that we exported to window
earlier which resolves when the component is ready to be used.
Once the Promise resolves we create an instance of our component using the standard document.createElement
function and append it to the <xml3d>
element. Then we use the showBBoxFor
function on our element to show the bounding box of the first (and only) <mesh>
element in the scene.