Best practice for removing Mesh objects in ThreeJS? - vue.js

What are some common ways in ThreeJS to work with creating Meshes (consisting of Geometry and Materials) and then later deleting those objects?
My use case is to display lat/long points on a rotating 3D globe. Each point should be clickable and show further information. The points displayed should also be able to change based on data that is binded with Vue.
I currently use something like this but am running into memory leak issues:
var material = new THREE.MeshPhongMaterial({
color: 0xdc143c,
});
var cone = new THREE.Mesh(
new THREE.ConeBufferGeometry(radius, height, 8, 1, true),
material
);
cone.position.y = height * 0.5;
cone.rotation.x = Math.PI;
var sphere = new THREE.Mesh(
new THREE.SphereBufferGeometry(sphereRadius, 16, 8),
material
);
export default class Marker extends THREE.Object3D {
constructor() {
super();
this.name = "Marker";
this.add(cone, sphere);
}
destroy() {
this.remove(cone, sphere);
}
}
Are there libraries on top of Three that makes the management of Meshes/Materials/Geometry more simple to work with?

What I do is create a function in some utils file, and any time I need to dispose of a mesh, I pass the mesh in as the function parameter:
const removeMesh = (meshToRemove) => {
meshToRemove.visible = false;
scene.remove(meshToRemove);
meshToRemove.geometry.dispose();
meshToRemove.material.dispose();
meshToRemove= undefined;
}

Related

Vue Leaflet Setting custom TileLayer bounds

I'm trying to display a large number of images using leaflet and I'm having problems making the tileLayers display properly.
I'm extending the TileLayer class and overriding the createTile function like this:
`
let imageLayer = TileLayer.extend({
createTile: function (coords, done) {
const image = window.L.DomUtil.create("img");
image.src = `http://placekitten.com/g/200/300`;
image.crossOrigin = "";
image.onload = function () {
this.onload = function () {};
done(null, image);
};
return image;
}
});
Result
I want to change the position of the images and to only display them in areas I have already defined (for example between offsetting the image at 0,0 to be at 1,1 or using latLng
([8273.300000000001, 10618.02] , [9232.653535353536, 9658.666464646465])).
Ive tried a bunch of different methods and almost always came back to bounds, which don't work and setting the bounds doesn't change anything.
I don't want to use ImageOverlays because the images I'm trying to display are sliced and named with the leaflet naming scheme (0_0_1.png).

call a component method from main vue app

I'm super new to Vue and I'm trying to get into components.
Basically, I have the main script file with my new vue app (with a countdown function based on setInterval and in the same file a vue component( it's a stamina bar made with canvas) with width increasing and decreasing methods. Basically, I would like to decrease the bar width every second but I don't get how to call the methods declared in the component. I guess that when they are declared inside the component I could access them from everywhere, but I didn't find a proper solution ( I tried with $refs but it's not working)
Is there any way where I can call the addWidth() and subWidth() function from a method in my new Vue app?
thank you
this is the component
Vue.component('stamina', {
template: '<div><canvas ref="stamina" style="height:25px;width:300px;" /></div>',
data(){
return {
stamina_width:300,
// vueCanvas: null
}
},
methods: {
drawStamina(){
// clear canvas
this.vueCanvas.clearRect(0, 0, 300, 50);
// draw rect
this.vueCanvas.rect(20,20, this.stamina_width, 100);
//colorize with gradient
var grd = this.vueCanvas.createLinearGradient(0, 0, 300, 0);
grd.addColorStop(0, "red");
grd.addColorStop(0.5, "orange");
grd.addColorStop(1, "green");
this.vueCanvas.fillStyle = grd;
//fill the rect with gradient
this.vueCanvas.fillRect(0, 10,this.stamina_width, 10);
},
addWidth() {
this.stamina_width += 20
this.drawStamina()
},
subWidth() {
this.stamina_width -= 20
this.drawStamina()
}
},
mounted () {
var ctx = this.$refs.stamina.getContext('2d');
this.vueCanvas = ctx;
var stam = this.stamina_width;
var grd = this.vueCanvas.createLinearGradient(0, 0, 300, 0);
grd.addColorStop(0, "red");
grd.addColorStop(0.5, "orange");
grd.addColorStop(1, "green");
this.vueCanvas.fillStyle = grd;
//fill the rect with gradient
this.vueCanvas.fillRect(0,25,stam,25);
}
});
and this is the main vue
var match = new Vue({
el:'#app',
methods:{
//call the methods from component stamina
}
yes you can call methods up/down of parent/child components. You may need to look at creating custom events and listen for them.
Another option I believe might be less complex is to consider using Vuex.
All of your addWidth and subWidth logic will reside in a centralized place this way. All of your components--no matter how nested can "subscribe" to these things (and modify them if needed).
Check out actions. You'll dispatch an action addWidth which sends it to Vuex. In your component that is responsible for rendering the stamina bar I would use a computed property to watch for updates. Something like this:
computed: {
barWidth() {
return this.$store.getters["bar/width"]; // 20
}
}
That is just a really rough example and will not likely be what you're looking for.
VueSchool has some nice (free) courses to help get you rolling with Vuex as well.

Updating texture and geometry in three.js

I'm trying to figure out if there is a way of getting a callback once a texture has been updated? I'm swapping the geometry in the scene but sharing the same changing underlying texture based on a canvas and I'm noticing random corruption that eventually disappears as the program runs. I think this is as a result of thrashing that is causing the geometry changes to become unsynchronized with the texture updates.
So I realize the established mechanism is to set is needsUpdate = true on the texture and material, but can I know when that update has been applied so that I can add the new geometry to the scene? I think I am adding the geometry before that update actually gets to the gpu and using the old texture for random frames. Is this possible, or does the texture update definitely get processed before the scene is drawn?
If you can point me in the right direction it would be much appreciated.
Additionally I've added an update event listener to the material, which gets called frequently, but I still end up with the same issues relating to the texture update happening after the geometry has been updated.
Here is an example of what I mean, some code removed. The general idea is to share the same texture / material among many generated objects. Update the canvas, and then add the new object into the scene. This for the most part works under chrome, but occasionally glitches where a frame has the previous texture applied.
var material, texture, texture_canvas, context;
var frames = [];
var total_frames = 24;
var last_frame = undefined;
var current_frame = 0;
var scene;
function init() {
// initialize scene, etc
scene = new THREE.Scene();
// Set up render etc, omitted
texture_canvas = document.createElement('canvas');
texture_canvas.height = 1024;
texgture_canvas.width = 1024;
context = texture_canvas.getContext( '2d' );
context.fillStyle = '#000000';
context.fillRect( 0, 0,
texture_canvas.width,
texture_canvas.height );
texture = new THREE.Texture( this.texture_canvas );
texture.minFilter = THREE.LinearFilter;
texture.magFilter = THREE.LinearFilter;
texture.generateMipmaps = false;
material = new THREE.MeshLambertMaterial({ map: texture } );
for (var i=0; i<total_frames; i++) {
var mesh = new THREE.Mesh( generate_geometry(), material );
frames.push(mesh);
}
}
function animate() {
if (last_frame) {
scene.remove(frames[last_frame]);
}
// Update / generate new canvas
context.drawImage( generate_image(), 0, 0 );
texture.needsUpdate = true;
material.needsUpdate = true;
frames[current_frame].uvsNeedUpdate = true;
scene.add(frames[current_frame % total_frames]);
last_frame = current_frame;
current_frame++;
requestAnimationFrame( animate );
}

Panning the map to certain extent javascript API

I want to limit map extent to the initial extent of the map and limit user from panning more than certain extent.
I tried following but nothing has changed:
map = new Map( "map" , {
basemap: "gray",
center: [-85.416, 49.000],
zoom : 6,
logo: false,
sliderStyle: "small"
});
dojo.connect(map, "onExtentChange", function (){
var initExtent = map.extent;
var extent = map.extent.getCenter();
if(initExtent.contains(extent)){}
else{map.setExtent(initExtent)}
});
Just to flesh out Simon's answer somewhat, and give an example. Ideally you need two variables at the same scope as map:
initExtent to store the boundary of your valid extent, and
validExtent to store the last valid extent found while panning, so that you can bounce back to it.
I've used the newer dojo.on event syntax as well for this example, it's probably a good idea to move to this as per the documentation's recommendation - I assume ESRI will discontinue the older style at some point.
var map;
var validExtent;
var initExtent;
[...]
require(['dojo/on'], function(on) {
on(map, 'pan', function(evt) {
if ( !initExtent.contains(evt.extent) ) {
console.log('Outside bounds!');
} else {
console.log('Updated extent');
validExtent = evt.extent;
}
});
on(map, 'pan-end', function(evt) {
if ( !initExtent.contains(evt.extent) ) {
map.setExtent(validExtent);
}
});
});
You can do the same with the zoom events, or use extent-change if you want to trap everything. Up to you.
It looks like your extent changed function is setting the initial extent variable to the maps current extent and then checking if that extent contains the current extents centre point - which of course it always will.
Instead, declare initExtent at the same scope of the map variable. Then, change the on load event to set this global scope variable rather than a local variable. In the extent changed function, don't update the value of initExtent, simply check the initExtent contains the entire of the current extent.
Alternatively you could compare each bound of the current extent to each bound of the initExtent, e.g. is initExtent.xmin < map.extent.xmin and if any are, create a new extent setting any exceeded bounds to the initExtent values.
The only problem is these techniques will allow the initExtent to be exceeded briefly, but will then snap the extent back once the extent changed function fires and catches up.
I originally posted this solution on gis.stackexchange in answer to this question: https://gis.stackexchange.com/a/199366
Here's a code sample from that post:
//This function limits the extent of the map to prevent users from scrolling
//far away from the initial extent.
function limitMapExtent(map) {
var initialExtent = map.extent;
map.on('extent-change', function(event) {
//If the map has moved to the point where it's center is
//outside the initial boundaries, then move it back to the
//edge where it moved out
var currentCenter = map.extent.getCenter();
if (!initialExtent.contains(currentCenter) &&
event.delta.x !== 0 && event.delta.y !== 0) {
var newCenter = map.extent.getCenter();
//check each side of the initial extent and if the
//current center is outside that extent,
//set the new center to be on the edge that it went out on
if (currentCenter.x < initialExtent.xmin) {
newCenter.x = initialExtent.xmin;
}
if (currentCenter.x > initialExtent.xmax) {
newCenter.x = initialExtent.xmax;
}
if (currentCenter.y < initialExtent.ymin) {
newCenter.y = initialExtent.ymin;
}
if (currentCenter.y > initialExtent.ymax) {
newCenter.y = initialExtent.ymax;
}
map.centerAt(newCenter);
}
});
}
And here's a working jsFiddle example: http://jsfiddle.net/sirhcybe/aL1p24xy/

Updating a chart with new data in App SDK 2.0

I am using a chart to visualize data in a TimeboxScopedApp, and I want to update the data when scope changes. The more brute-force approach of using remove() then redrawing the chart as described here leaves me with an overlaid "Loading..." mask, but otherwise works. The natural approach of using the Highchart native redraw() method would be my preference, only I don't know how to access the actual Highchart object and not the App SDK wrapper.
Here's the relevant part of the code:
var chart = Ext.getCmp('componentQualityChart');
if (chart) {
var chartCfg = chart.getChartConfig();
chartCfg.xAxis.categories = components;
chart.setChartConfig(chartCfg);
chart.setChartData(data);
chart.redraw(); // this doesn't work with the wrapper object
} else { // draw chart for the first time
How do I go about redrawing the chart with the new data?
Assuming chart (componentQualityChart) is an instance of Rally.ui.chart.Chart, you can access the HighCharts instance like this:
var highcharts = chart.down('highchart').chart;
// Now you have access to the normal highcharts interface, so
// you could change the xAxis
highcharts.xAxis[0].setCategories([...], true);
// Or you could change the series data
highcharts.series[0].data.push({ ... }); //Add a new data point
// Or pretty much anything else highcharts lets you do
Using _unmask() removes the overlaid "Loading.." mask
if (this.down('#myChart')) {
this.remove('myChart');
}
this.add(
{
xtype: 'rallychart',
height: 400,
itemId: 'myChart',
chartConfig: {
//....
},
chartData: {
categories: scheduleStateGroups,
series: [
{
//...
}
]
}
}
);
this.down('#myChart')._unmask();