Related
I am building an app in three.js, and basically the point is to have rows and columns of individual blocks that also extend upwards. When you click on a block it will change colour, so I have implemented clipping along all three axes, so that the middle blocks can be reached as well. I have also added a Gui using lil-gui, and that is where the problem comes in. I have controls for the amount of rows, columns and the height, as well as for how much it should clip in each direction. When initialized originally, the rows, columns and height are all set to 1, and if you increase any of them from there, the amount of blocks will correctly update in the specified direction.
My problem comes in when I load a previous save. The previous state is saved to localStorage, in a function that runs every few minutes, and saving also occurs with ctrl + s. It converts my current state, a 3 dimensional array, into a much simpler object, which is also a 3 dimensional object. It saves correctly, I have logged the output multiple times. When it loads, I convert the string into a JSON object, which I then convert into a state object, and I then render that state object. The strangest thing is that it only displays roughly half of what it should in all 3 directions, E.G A 3x3x3 grid is rendered as a 2x2x2 grid, but as soon as I click on rows, columns or height in my GUI, and click out again, it magically renders properly. What is also odd is that although all of my GUI fields have a min, a max and a step, they are unconstrained in the gui.
I have simplified my code so that it only deals with rows, as an mcve but the problem scales up as well.
The lil-gui panel code
function setUpPanel(){
const panel = new GUI( {width:310} );
const folder1 = panel.addFolder( "Size" );
const folder2 = panel.addFolder( "X Clipping" );
settings = {
'rows' : rows,
planeX: {
blocks: rows,
},
};
rowsObj = folder1.add(settings, 'rows').min(1).step(1).max(50).setValue( rows ).onFinishChange( function ( size ) {
rowsElement.max(size);
rowsElement.setValue(size);
rowsElement.updateDisplay();
rows = size;
redraw();
} );
rowsElement = folder2.add(settings.planeX, 'blocks',1, rows, 1).onChange( setClippingPlaneRows );
}
The code called when the app is started
import * as THREE from './js/three.module.js';
import { GUI } from './js/lil-gui.module.min.js';
import { OrbitControls } from './js/OrbitControls.js';
const blockOffset = 0.1;
const blockSize = 1;
const BlockStates = {
Empty: Symbol('e'),
}
const clippingPlanes = [
new THREE.Plane( new THREE.Vector3( -1, 0, 0 ), 0, {name: "plane"} ),
];
//gui object
let rowsObj
//more gui object
let rowsElement
const emptyMaterial = new THREE.MeshStandardMaterial( {color: 0xffffff, opacity: 0.5, transparent: true} );
emptyMaterial.clippingPlanes = clippingPlanes;
let rows=1;
let state = [];
let boxes = new THREE.Group();
let settings;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight , 0.1, 1000 );
camera.position.set(-1.5,2.5,3);
const renderer = new THREE.WebGLRenderer();
renderer.localClippingEnabled = true;
renderer.setSize( window.innerWidth, window.innerHeight);
document.body.appendChild( renderer.domElement );
var autoSaveID = setInterval(function() {
localStorage.lastSave = JSON.stringify(createJsonObjectFromState());
localStorage.rows = rows;
}, 300000);
if(localStorage.lastSave){
let loadSave = confirm("There is a previous save detected. Would you like to load it?");
if(loadSave){
const loadedObj = createStateObjectFromJsonString(localStorage.lastSave);
state = loadedObj.state;
boxes = loadedObj.boxes;
rows = localStorage.rows;
boxes.position.set(-(rows * (blockSize + blockOffset)- blockSize - blockOffset)/2,0, 0);
}
}
const light = new THREE.AmbientLight( 0x404040, 10);
const controls = new OrbitControls(camera, renderer.domElement );
setUpPanel();
drawBoxes();
boxes.position.set(0,0,0);
document.addEventListener('keydown', e => {
if (e.ctrlKey && e.key === 's') {
// Prevent the Save dialog from opening
e.preventDefault();
saveCurrent();
}
});
scene.add( boxes );
scene.add( light );
renderer.render(scene, camera);
The save function
function saveCurrent(){
let stringVersion = JSON.stringify(createJsonObjectFromState());
//save to local storage
if(localStorage.lastSave){
let writeSave = confirm("There is a previous save detected. Would you like to overwrite it?");
if(writeSave){
localStorage.lastSave = stringVersion;
localStorage.rows = rows;
}
}
else{
localStorage.lastSave = stringVersion;
localStorage.rows = rows;
}
}
createStateObjectFromJsonString
function createStateObjectFromJsonString(jsonString){
let newObj =[];
let newBoxes=new THREE.Group();
let jsonObj = JSON.parse(jsonString);
for(let i = 0; i < jsonObj.length; i++){
let box;
switch(jsonObj[i]){
case "E":
box = drawBox(BlockStates.Empty);
box.position.set(i*(blockSize + blockOffset ), 0, 0);
newBoxes.add(box);
newObj.push({state: BlockStates.Empty, item:box});
break;
}
}
return {state: newObj, boxes: newBoxes};
}
createJsonObjectFromState
function createJsonObjectFromState(){
const JSONObj = [];
for(let i = 0; i < rows; i++){
switch(state[i].state){
case BlockStates.Empty:
JSONObj.push("E");
break;
}
}
return JSONObj;
}
The function to set the clipping to a specific amount of blocks
function setClippingPlaneRows(amt){
clippingPlanes[0].constant = rows%2 === 0 ? (blockSize + blockOffset) * (amt - Math.floor(rows/2) ) : (blockSize + blockOffset) *( amt - Math.floor(rows/2) - 0.5);
}
The redraw function
function redraw(){
drawBoxes();
}
The drawBox function
function drawBox(state){
const geometry = new THREE.BoxGeometry(blockSize, blockSize, blockSize);
let material = emptyMaterial;
return new THREE.Mesh( geometry, material );
}
The drawBoxes function
function drawBoxes(){
if(state.length === 0)
{
boxes.clear();
state = [];
for(let i = 0; i < rows; i++){
const box = drawBox(BlockStates.Empty);
box.position.set(i*(blockSize + blockOffset ), 0, 0);
boxes.add(box);
state.push({state : BlockStates.Empty, item: box});
}
}
else{
if(state.length <= rows){
//just draw in the extra rows
for(let i = state.length; i < rows; i++){
const box = drawBox(BlockStates.Empty);
box.position.set(i*(blockSize + blockOffset ), 0, 0);
boxes.add(box);
state.push({state : BlockStates.Empty, item: box});
}
}
else{
//give a warning, and if accepted remove neccesary rows
let accepted = confirm("Making the rows smaller could remove some of the work you have done. Are you sure?");
if(accepted){
for(let i = 0; i < state.length - rows; i++){
const box = state[state.length - 1].item;
boxes.remove(box);
state.pop();
}
}
}
}
//plane initial positions
boxes.position.set(-(rows * (blockSize + blockOffset)- blockSize - blockOffset)/2, 0, 0);
setClippingPlaneRows(rows);
}
The animation part
function animate() {
let frame = requestAnimationFrame( animate );
renderer.render( scene, camera );
}
animate();
I apologize for posting so much code, I just have no idea where the problem is stemming from. I have tried many things, such as checking the size of the rows, columns, etc at many points, but they are always correct. I have tried using setValue() to force the gui to call the onChange function, which hasn't done anything. I have tried many other variations too, but no luck.
Edit
I have discovered while playing around that if I set rows to a number other than 1 in the code itself, such as 7, and don't load a saved file, a very similar problem occurs. Only about half of the blocks are displaying, but this time the gui behaves itself and remains constrained.
Goal
To get this mini-game working in next.js powered environment 'the right way'.
Background
The linked game uses Three.js which in turn requires the window object to update certain variables according to the game logic.
And next.js does not have a window object defined on the server side, as explained here.
So, to tackle this problem, I moved the window object inside of useEffect hook provided by react as mentioned in this article.
Problem
Moving window inside of useEffect worked but other helper variables depends on its value, so to make it all work without errors, I had to move the entire javascript code with some minor adjustments inside of useEffect of a component page that I created.
Extra
I started this project by trying to recreate this game using react-three-fiber as it abstracts well with Next.js but I had to change my ways because the documentation was not clear and lacks example code.
To even change the default camera setting from perspective to orthographic inside of Canvas tag was a little unruly.
But as explained by Bruno Simon, Paul Henschel and others, three becomes quite hard to manage/scale/change/reuse when the code becomes complex.
And I wish to maintain the component property of my code.
How shall I go about it?
Thankyou for any leads, references to courses, articles.
This stackoverflow thread poses a similar question to mine but I have my what ifs.
Code
import styles from '../styles/Home.module.css'
import { React, useState, useLayoutEffect, useRef, useEffect } from 'react'
import * as THREE from 'three'
import * as CANNON from 'cannon'
export default function Home() {
useEffect(() => {
window.focus(); // Capture keys right away (by default focus is on editor)
let camera, scene, renderer; // ThreeJS globals
let world; // CannonJs world
let lastTime; // Last timestamp of animation
let stack; // Parts that stay solid on top of each other
let overhangs; // Overhanging parts that fall down
const boxHeight = 1; // Height of each layer
const originalBoxSize = 3; // Original width and height of a box
let autopilot;
let gameEnded;
let robotPrecision; // Determines how precise the game is on autopilot
const scoreElement = document.getElementById("score");
const instructionsElement = document.getElementById("instructions");
const resultsElement = document.getElementById("results");
init();
// Determines how precise the game is on autopilot
function setRobotPrecision() {
robotPrecision = Math.random() * 1 - 0.5;
}
function init() {
autopilot = true;
gameEnded = false;
lastTime = 0;
stack = [];
overhangs = [];
setRobotPrecision();
// Initialize CannonJS
world = new CANNON.World();
world.gravity.set(0, -10, 0); // Gravity pulls things down
world.broadphase = new CANNON.NaiveBroadphase();
world.solver.iterations = 40;
// Initialize ThreeJs
const aspect = window.innerWidth / window.innerHeight;
const width = 10;
const height = width / aspect;
camera = new THREE.OrthographicCamera(
width / -2, // left
width / 2, // right
height / 2, // top
height / -2, // bottom
0, // near plane
100 // far plane
);
/*
// If you want to use perspective camera instead, uncomment these lines
camera = new THREE.PerspectiveCamera(
45, // field of view
aspect, // aspect ratio
1, // near plane
100 // far plane
);
*/
camera.position.set(4, 4, 4);
camera.lookAt(0, 0, 0);
scene = new THREE.Scene();
// Foundation
addLayer(0, 0, originalBoxSize, originalBoxSize);
// First layer
addLayer(-10, 0, originalBoxSize, originalBoxSize, "x");
// Set up lights
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.6);
dirLight.position.set(10, 20, 0);
scene.add(dirLight);
// Set up renderer
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setAnimationLoop(animation);
document.body.appendChild(renderer.domElement);
}
function startGame() {
autopilot = false;
gameEnded = false;
lastTime = 0;
stack = [];
overhangs = [];
if (instructionsElement) instructionsElement.style.display = "none";
if (resultsElement) resultsElement.style.display = "none";
if (scoreElement) scoreElement.innerText = 0;
if (world) {
// Remove every object from world
while (world.bodies.length > 0) {
world.remove(world.bodies[0]);
}
}
if (scene) {
// Remove every Mesh from the scene
while (scene.children.find((c) => c.type == "Mesh")) {
const mesh = scene.children.find((c) => c.type == "Mesh");
scene.remove(mesh);
}
// Foundation
addLayer(0, 0, originalBoxSize, originalBoxSize);
// First layer
addLayer(-10, 0, originalBoxSize, originalBoxSize, "x");
}
if (camera) {
// Reset camera positions
camera.position.set(4, 4, 4);
camera.lookAt(0, 0, 0);
}
}
function addLayer(x, z, width, depth, direction) {
const y = boxHeight * stack.length; // Add the new box one layer higher
const layer = generateBox(x, y, z, width, depth, false);
layer.direction = direction;
stack.push(layer);
}
function addOverhang(x, z, width, depth) {
const y = boxHeight * (stack.length - 1); // Add the new box one the same layer
const overhang = generateBox(x, y, z, width, depth, true);
overhangs.push(overhang);
}
function generateBox(x, y, z, width, depth, falls) {
// ThreeJS
const geometry = new THREE.BoxGeometry(width, boxHeight, depth);
const color = new THREE.Color(`hsl(${30 + stack.length * 4}, 100%, 50%)`);
const material = new THREE.MeshLambertMaterial({ color });
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(x, y, z);
scene.add(mesh);
// CannonJS
const shape = new CANNON.Box(
new CANNON.Vec3(width / 2, boxHeight / 2, depth / 2)
);
let mass = falls ? 5 : 0; // If it shouldn't fall then setting the mass to zero will keep it stationary
mass *= width / originalBoxSize; // Reduce mass proportionately by size
mass *= depth / originalBoxSize; // Reduce mass proportionately by size
const body = new CANNON.Body({ mass, shape });
body.position.set(x, y, z);
world.addBody(body);
return {
threejs: mesh,
cannonjs: body,
width,
depth
};
}
function cutBox(topLayer, overlap, size, delta) {
const direction = topLayer.direction;
const newWidth = direction == "x" ? overlap : topLayer.width;
const newDepth = direction == "z" ? overlap : topLayer.depth;
// Update metadata
topLayer.width = newWidth;
topLayer.depth = newDepth;
// Update ThreeJS model
topLayer.threejs.scale[direction] = overlap / size;
topLayer.threejs.position[direction] -= delta / 2;
// Update CannonJS model
topLayer.cannonjs.position[direction] -= delta / 2;
// Replace shape to a smaller one (in CannonJS you can't simply just scale a shape)
const shape = new CANNON.Box(
new CANNON.Vec3(newWidth / 2, boxHeight / 2, newDepth / 2)
);
topLayer.cannonjs.shapes = [];
topLayer.cannonjs.addShape(shape);
}
window.addEventListener("mousedown", eventHandler);
window.addEventListener("touchstart", eventHandler);
window.addEventListener("keydown", function (event) {
if (event.key == " ") {
event.preventDefault();
eventHandler();
return;
}
if (event.key == "R" || event.key == "r") {
event.preventDefault();
startGame();
return;
}
});
function eventHandler() {
if (autopilot) startGame();
else splitBlockAndAddNextOneIfOverlaps();
}
function splitBlockAndAddNextOneIfOverlaps() {
if (gameEnded) return;
const topLayer = stack[stack.length - 1];
const previousLayer = stack[stack.length - 2];
const direction = topLayer.direction;
const size = direction == "x" ? topLayer.width : topLayer.depth;
const delta =
topLayer.threejs.position[direction] -
previousLayer.threejs.position[direction];
const overhangSize = Math.abs(delta);
const overlap = size - overhangSize;
if (overlap > 0) {
cutBox(topLayer, overlap, size, delta);
// Overhang
const overhangShift = (overlap / 2 + overhangSize / 2) * Math.sign(delta);
const overhangX =
direction == "x"
? topLayer.threejs.position.x + overhangShift
: topLayer.threejs.position.x;
const overhangZ =
direction == "z"
? topLayer.threejs.position.z + overhangShift
: topLayer.threejs.position.z;
const overhangWidth = direction == "x" ? overhangSize : topLayer.width;
const overhangDepth = direction == "z" ? overhangSize : topLayer.depth;
addOverhang(overhangX, overhangZ, overhangWidth, overhangDepth);
// Next layer
const nextX = direction == "x" ? topLayer.threejs.position.x : -10;
const nextZ = direction == "z" ? topLayer.threejs.position.z : -10;
const newWidth = topLayer.width; // New layer has the same size as the cut top layer
const newDepth = topLayer.depth; // New layer has the same size as the cut top layer
const nextDirection = direction == "x" ? "z" : "x";
if (scoreElement) scoreElement.innerText = stack.length - 1;
addLayer(nextX, nextZ, newWidth, newDepth, nextDirection);
} else {
missedTheSpot();
}
}
function missedTheSpot() {
const topLayer = stack[stack.length - 1];
// Turn to top layer into an overhang and let it fall down
addOverhang(
topLayer.threejs.position.x,
topLayer.threejs.position.z,
topLayer.width,
topLayer.depth
);
world.remove(topLayer.cannonjs);
scene.remove(topLayer.threejs);
gameEnded = true;
if (resultsElement && !autopilot) resultsElement.style.display = "flex";
}
function animation(time) {
if (lastTime) {
const timePassed = time - lastTime;
const speed = 0.008;
const topLayer = stack[stack.length - 1];
const previousLayer = stack[stack.length - 2];
// The top level box should move if the game has not ended AND
// it's either NOT in autopilot or it is in autopilot and the box did not yet reach the robot position
const boxShouldMove =
!gameEnded &&
(!autopilot ||
(autopilot &&
topLayer.threejs.position[topLayer.direction] <
previousLayer.threejs.position[topLayer.direction] +
robotPrecision));
if (boxShouldMove) {
// Keep the position visible on UI and the position in the model in sync
topLayer.threejs.position[topLayer.direction] += speed * timePassed;
topLayer.cannonjs.position[topLayer.direction] += speed * timePassed;
// If the box went beyond the stack then show up the fail screen
if (topLayer.threejs.position[topLayer.direction] > 10) {
missedTheSpot();
}
} else {
// If it shouldn't move then is it because the autopilot reached the correct position?
// Because if so then next level is coming
if (autopilot) {
splitBlockAndAddNextOneIfOverlaps();
setRobotPrecision();
}
}
// 4 is the initial camera height
if (camera.position.y < boxHeight * (stack.length - 2) + 4) {
camera.position.y += speed * timePassed;
}
updatePhysics(timePassed);
renderer.render(scene, camera);
}
lastTime = time;
}
function updatePhysics(timePassed) {
world.step(timePassed / 1000); // Step the physics world
// Copy coordinates from Cannon.js to Three.js
overhangs.forEach((element) => {
element.threejs.position.copy(element.cannonjs.position);
element.threejs.quaternion.copy(element.cannonjs.quaternion);
});
}
window.addEventListener("resize", () => {
// Adjust camera
console.log("resize", window.innerWidth, window.innerHeight);
const aspect = window.innerWidth / window.innerHeight;
const width = 10;
const height = width / aspect;
camera.top = height / 2;
camera.bottom = height / -2;
// Reset renderer
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.render(scene, camera);
});
})
return (
<div className={styles.container}>
<div className={styles.nav}>
<h2>Ritik Jangir</h2>
</div>
</div>
)
}
I'm using Pixi with PixiOverlay on leaflet. I have the following jsfiddle for a dummy simulation. The objective: once you click Add Image 2 - it adds a picture of a hamster randomly on the map.
It (almost) work.
the problem:
Error message: "BaseTexture added to the cache with an id [hamster] that already had an entry"
I couldn't figure our where to put the loader and how to integrate it properly in terms of code organization: (do I need to use it only once?) what if I have other layers to add? So I assume my challenge is here:
this.loader.load((loader, resources) => {...}
Minor: how to reduce the size of the hamster :-)
my JS code (also on jsfiddle)
class Simulation
{
constructor()
{
// center of the map
var center = [1.8650, 51.2094];
// Create the map
this.map = L.map('map').setView(center, 2);
// Set up the OSM layer
L.tileLayer(
'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18
}).addTo(this.map);
this.imagesLayer = new L.layerGroup();
this.imagesLayer.addTo(this.map);
}
_getRandomCoord()
{
var randLat = Math.floor(Math.random() * 90);
randLat *= Math.round(Math.random()) ? 1 : -1;
var randLon = Math.floor(Math.random() * 180);
randLon *= Math.round(Math.random()) ? 1 : -1;
return [randLat,randLon]
}
addImage2()
{
this.loader = new PIXI.Loader()
this.loader.add('hamster', 'https://cdn-icons-png.flaticon.com/512/196/196817.png')
this.loader.load((loader, resources) => {
let markerTexture = resources.hamster.texture
let markerLatLng = this._getRandomCoord()
let marker = new PIXI.Sprite(markerTexture)
marker.anchor.set(0.5, 1)
let pixiContainer = new PIXI.Container()
pixiContainer.addChild(marker)
let firstDraw = true
let prevZoom
let pixiOverlay = L.pixiOverlay(utils => {
let zoom = utils.getMap().getZoom()
let container = utils.getContainer()
let renderer = utils.getRenderer()
let project = utils.latLngToLayerPoint
let scale = utils.getScale()
if (firstDraw) {
let markerCoords = project(markerLatLng)
marker.x = markerCoords.x
marker.y = markerCoords.y
}
if (firstDraw || prevZoom !== zoom) {
marker.scale.set(1 / scale)
}
firstDraw = true
prevZoom = zoom
renderer.render(container)
}, pixiContainer)
this.imagesLayer.addLayer(pixiOverlay);
})
}
addTriangle()
{
console.log("Trinalge")
var polygonLatLngs = [
[51.509, -0.08],
[51.503, -0.06],
[51.51, -15.047],
[21.509, -0.08]
];
var projectedPolygon;
var triangle = new PIXI.Graphics();
var pixiContainer = new PIXI.Container();
pixiContainer.addChild(triangle);
var firstDraw = true;
var prevZoom;
var pixiOverlay = L.pixiOverlay(function(utils) {
var zoom = utils.getMap().getZoom();
var container = utils.getContainer();
var renderer = utils.getRenderer();
var project = utils.latLngToLayerPoint;
var scale = utils.getScale();
if (firstDraw) {
projectedPolygon = polygonLatLngs.map(function(coords) {return project(coords);});
}
if (firstDraw || prevZoom !== zoom) {
triangle.clear();
triangle.lineStyle(3 / scale, 0x3388ff, 1);
triangle.beginFill(0x3388ff, 0.2);
projectedPolygon.forEach(function(coords, index) {
if (index == 0) triangle.moveTo(coords.x, coords.y);
else triangle.lineTo(coords.x, coords.y);
});
triangle.endFill();
}
firstDraw = false;
prevZoom = zoom;
renderer.render(container);
}.bind(this), pixiContainer);
this.imagesLayer.addLayer(pixiOverlay)
}
removeLayer()
{
this.imagesLayer.clearLayers();
}
}
var simulation = new Simulation();
TLDR: Updated jsfiddle:
https://jsfiddle.net/gbsdfm97/
more info below:
First problem: loading resources (textures)
There was error in console because you loaded hamster image on each click:
addImage2()
{
this.loader = new PIXI.Loader()
this.loader.add('hamster', 'https://cdn-icons-png.flaticon.com/512/196/196817.png')
this.loader.load((loader, resources) => {
...
Better approach is to load image (resource) once at beginning and then just reuse what is loaded in memory:
constructor()
{
...
this.markerTexture = null;
this._loadPixiResources();
}
...
_loadPixiResources()
{
this.loader = new PIXI.Loader()
this.loader.add('hamster', 'https://cdn-icons-png.flaticon.com/512/196/196817.png')
this.loader.load((loader, resources) => {
this.markerTexture = resources.hamster.texture;
})
}
...
addImage2()
{
...
let marker = new PIXI.Sprite(this.markerTexture);
Second problem: size of hamsters :)
Scale was set like this:
marker.scale.set(1 / scale)
Which was too big - so changed it to:
// affects size of hamsters:
this.scaleFactor = 0.05;
...
marker.scale.set(this.scaleFactor / scale);
Scale of hamsters (not triangles!) is now updated when zoom changes - so when user uses mouse scroll wheel etc.
Third problem: too many layers in pixiOverlay
Previously on each click on Add Image 2 or Add Triangle button there was added new pixiContainer and new pixiOverlay which was added as new layer: this.imagesLayer.addLayer(pixiOverlay);
New version is a bit simplified: there is only one pixiContainer and one pixiOverlay created at beginning:
constructor()
{
...
// Create one Pixi container for pixiOverlay in which we will keep hamsters and triangles:
this.pixiContainer = new PIXI.Container();
let prevZoom;
// Create one pixiOverlay:
this.pixiOverlay = L.pixiOverlay((utils, data) => {
...
}, this.pixiContainer)
this.imagesLayer.addLayer(this.pixiOverlay);
}
this.pixiOverlay is added as one layer
then in rest of program we reuse this.pixiOverlay
also we reuse this.pixiContainer because it is returned from utils - see:
let container = utils.getContainer() // <-- this is our "this.pixiContainer"
...
container.addChild(marker)
renderer.render(container)
Bonus: Triangles
Now you can add many triangles - one per each click.
Note: triangles do not change scale - this is a difference compared to hamsters.
I on a project making projection of maps onto a point cloud possible in Potree.
We are loading the maps from OpenStreetMap's tile API and are loading more tiles as the user zooms on point cloud in order to give the user a more detailed experience. We are projecting the data from the tiles to the point cloud using Three.js Texture which we are extracting from a canvas element where we place the tiles.
We have made all we code to get the tiles from OSM, cut them in the right size and place them in the canvas. And the solution is working quite well for smaller point clouds, but as the area grows and more tiles/images are needed and the canvas element gets bigger (we are scaling the canvas to follow the zoom level in order to give a better resolution) we get into trouble with an huge memory use (around 10 gb in the task manager, but then I investigate the memory use in the chrome dev tools the memory use is a lot lower). The big memory use will cause the browser to become unresponsive and eventually die.
My question is there for is there some kind of memory leak in this code or something you have to be very careful about when you use images and canvasses?
The code:
Potree.MapTextureManager = class MapTextureManager {
constructor(projection, bbMin, bbMax) {
this.projection = projection;
this.bbMin = bbMin;
this.bbMax = bbMax;
this._mapCanvas = document.getElementById("texture");
let ratio = (bbMax[0] - bbMin[0]) / (bbMax[1] - bbMin[1]);
let minHeight = 256;
this._mapCanvas.width = minHeight * ratio;
this._mapCanvas.height = minHeight;
this._minWeb = proj4(swiss, WGS84, [this.bbMin[0], this.bbMin[1]]);
this._maxWeb = proj4(swiss, WGS84, [this.bbMax[0], this.bbMax[1]]);
this.updateTexture(this._minWeb, this._maxWeb);
this._cachedTexture = null;
this._drawnImages = [];
this.geometryNodeIds = new Set();
this._cachedTileImages = [];
this._currentMaxZoom = this.getTiles(this._minWeb, this._maxWeb)[0].zoom;
}
updateTextureFor(visibleNodes, matrixWorld) {
visibleNodes.forEach(visibleNode => {
if (!this.geometryNodeIds.has(visibleNode.geometryNode.id)) {
this.geometryNodeIds.add(visibleNode.geometryNode.id);
var swiss = proj4.defs("test");
var WGS84 = proj4.defs("WGS84");
let nodeBox = Potree.utils.computeTransformedBoundingBox(visibleNode.geometryNode.boundingBox, matrixWorld);
let minWeb = proj4(swiss, WGS84, [nodeBox.min.x, nodeBox.min.y]);
let maxWeb = proj4(swiss, WGS84, [nodeBox.max.x, nodeBox.max.y]);
this.updateTexture(minWeb, maxWeb);
}
});
}
updateTexture(minWeb, maxWeb) {
let canvasEl = this._mapCanvas;
let tiles = this.getTiles(minWeb, maxWeb);
let tilePromises = this.tilePromisesFor(tiles);
tilePromises.forEach(tilePromise => {
tilePromise.then(tileImage => {
if (tileImage.tile.zoom > this._currentMaxZoom) {
this.resizeCanvasTo(tileImage.tile.zoom);
}
this._cachedTileImages.push(tileImage);
this._cachedTileImages.sort((tileImage1, tileImage2) => {
if (tileImage1.tile.zoom >= tileImage2.tile.zoom) {
return 1;
} else {
return -1;
}
});
let myArray = this._cachedTileImages.filter((el) => !this._drawnImages.includes(el));
myArray.forEach(tileImage => {
// if (this._drawnImages.indexOf(tileImage) === -1) {
this.drawTileOnCanvas(canvasEl, tileImage.image, tileImage.tile);
this._drawnImages.push(tileImage);
// }
});
if (this._cachedTexture) {
this._cachedTexture.dispose();
this._cachedTexture = null;
}
});
});
}
get mapTexture() {
if (this._cachedTexture) {
return this._cachedTexture;
}
let texture = new THREE.CanvasTexture(this._mapCanvas);
texture.minFilter = THREE.LinearFilter;
texture.needsUpdate = true;
this._cachedTexture = texture;
return texture;
}
getTiles(minCoord, maxCoord, zoom = 1) {
let maxZoom = 18;
let minNumberOfTiles = 4;
let minX = this.long2tile(minCoord[0], zoom);
let minY = this.lat2tile(minCoord[1], zoom);
let maxX = this.long2tile(maxCoord[0], zoom);
let maxY = this.lat2tile(maxCoord[1], zoom);
let arrayX = [minX, maxX].sort();
let arrayY = [minY, maxY].sort();
let tiles = [];
for (var x = arrayX[0]; x <= arrayX[1]; x++) {
for (var y = arrayY[0]; y <= arrayY[1]; y++) {
tiles.push({ X: x, Y: y, zoom: zoom });
}
}
// We want at least minNumberOfTiles tiles per pointcloud node
if (tiles.length >= minNumberOfTiles || zoom === maxZoom) {
return tiles;
} else {
return this.getTiles(minCoord, maxCoord, zoom + 1);
}
}
tilePromisesFor(Tiles) {
return Tiles.map(function (tile, i) {
return new Promise((resolve, reject) => {
let image = new Image(256, 256);
image.crossOrigin = "Anonymous";
image.onload = function () {
let data = { tile: tile, image: image };
resolve(data);
}
image.src = "https://tile.openstreetmap.org" + "/" + tile.zoom + "/" + tile.X + "/" + tile.Y + ".png";
})
});
}
drawTileOnCanvas(canvas, image, tile) {
let ctx = canvas.getContext("2d");
ctx.drawImage(image, sX, sY, imageWidthToBeDrawn, imageHeightToBeDrawn, dX, dY, drawingWidth, drawingHeight);
image.src = "";
image = null;
}
resizeCanvasTo(zoom) {
let canvas = this._mapCanvas;
let multiplier = Math.pow(2, zoom - this._currentMaxZoom);
let ctx = canvas.getContext("2d");
// create a temporary canvas obj to cache the pixel data //
var temp_cnvs = document.createElement('canvas');
var temp_cntx = temp_cnvs.getContext('2d');
// set it to the new width & height and draw the current canvas data into it //
temp_cnvs.width = canvas.width * multiplier;;
temp_cnvs.height = canvas.height * multiplier;;
temp_cntx.drawImage(canvas, 0, 0);
// resize & clear the original canvas and copy back in the cached pixel data //
canvas.width = canvas.width * multiplier;
canvas.height = canvas.height * multiplier;
ctx.scale(multiplier, multiplier);
ctx.drawImage(temp_cnvs, 0, 0);
this._currentMaxZoom = zoom;
temp_cnvs = null;
temp_cntx = null;
}
};
I have tried to remove some of the unnecessary code. So please say if you are missing something.
I am a novice programmer working with OpenJScad written in Javascript to build 3D models.
I am trying to figure out how to structure my code so that I can access an object's instance properties that are dynamically created with user input parameters. I have a parent Gear class with the following variable...
// Gear parent class
Gear = function(numTeeth, circularPitch, pressureAngle, clearance, thickness)
{
var pitchRadius = numTeeth * circularPitch / (2 * Math.PI);
I am making several Gear sub-classes that accept user parameters, ie...
// Spur Gear
function makeSpur(params)
{
var gear = new Gear(
params.spurTeeth,
params.circularPitch,
params.pressureAngle,
params.clearance,
params.inputBore
);
if(params.inputBore > 0)
{
var inputBore = CSG.cylinder({start: [0,0,-params.thickness2], end:
[0,0,params.thickness2], radius: params.inputBore, resolution: 16});
gear = gear.subtract(inputBore).rotateX(90);
}
return gear;
...and then dynamically generating location coordinates based on the pitchRadius property of another Gear object...
// function main
var spurGear = makeSpur(params);
spurGear = spurGear.translate([-pinionGear.pitchRadius,0,0]);
Everything renders, except when I try to access the pitchRadius property from another Gear instance. Ive read about prototypes and accessing private / public properties, but I just can't figure out how to structure the code so that I can access instance properties in function main.
Here is the full code...
include("gears.jscad");
// Here we define the user editable parameters:
function getParameterDefinitions() {
return [
{ name: 'circularPitch', caption: 'Circular pitch:', type: 'float', initial: 5 },
{ name: 'pressureAngle', caption: 'Pressure angle:', type: 'float', initial: 20 },
{ name: 'clearance', caption: 'Clearance:', type: 'float', initial: 0 },
{ name: 'thickness', caption: 'Thickness of transmission gears:', type: 'float', initial: 5 },
{ name: 'spurTeeth', caption: 'Number of spur teeth:', type: 'int', initial: 32 },
{ name: 'pinionTeeth', caption: 'Number of pinion teeth:', type: 'int', initial: 14 },
{ name: 'bore', caption: 'Radius of shaft:', type: 'float', initial: 5 }
];
}
// Main function
function main(params)
{
// declare parts
spurGear = new makeSpur(params);
pinionGear = new makePinion(params);
// assemble parts
spurGear = spurGear.translate([-pinionGear.pitchRadius, 0, -20]); // BREAKS CODE
pinionGear = pinionGear.translate([-spurGear.pitchRadius, 0, 20]); // BREAKS CODE
return [spurGear,pinionGear];
}
// Spur Gear
function makeSpur(params)
{
var gear = new involuteGear(
params.spurTeeth,
params.circularPitch,
params.pressureAngle,
params.clearance,
params.thickness,
params.bore
);
if(params.bore > 0)
{
var bore = CSG.cylinder({start: [0,0,-params.thickness], end: [0,0,params.thickness], radius: params.bore, resolution: 16});
gear = gear.subtract(bore).rotateX(90);
}
return gear;
}
// Pinion Gear
function makePinion(params)
{
var gear = new involuteGear(
params.pinionTeeth,
params.circularPitch,
params.pressureAngle,
params.clearance,
params.thickness,
params.bore
);
if(params.bore > 0)
{
var bore = CSG.cylinder({start: [0,0,-params.thickness], end: [0,0,params.thickness], radius: params.bore, resolution: 16});
gear = gear.subtract(bore).rotateX(90);
}
return gear;
}
// title: Gear
// author: Joost Nieuwenhuijse
// license: MIT License
/*
For gear terminology see:
http://www.astronomiainumbria.org/advanced_internet_files/meccanica/easyweb.easynet.co.uk/_chrish/geardata.htm
Algorithm based on:
http://www.cartertools.com/involute.html
circularPitch: The distance between adjacent teeth measured at the pitch circle
*/
function involuteGear(numTeeth, circularPitch, pressureAngle, clearance, thickness)
{
// default values:
if(arguments.length < 3) pressureAngle = 20;
if(arguments.length < 4) clearance = 0;
if(arguments.length < 4) thickness = 1;
var addendum = circularPitch / Math.PI;
var dedendum = addendum + clearance;
// radiuses of the 4 circles:
this.pitchRadius = numTeeth * circularPitch / (2 * Math.PI);
// this.getpitchRadius = function() {
//return pitchRadius;
//};
var baseRadius = this.pitchRadius * Math.cos(Math.PI * pressureAngle / 180);
var outerRadius = this.pitchRadius + addendum;
var rootRadius = this.pitchRadius - dedendum;
var maxtanlength = Math.sqrt(outerRadius*outerRadius - baseRadius*baseRadius);
var maxangle = maxtanlength / baseRadius;
var tl_at_pitchcircle = Math.sqrt(this.pitchRadius*this.pitchRadius - baseRadius*baseRadius);
var angle_at_pitchcircle = tl_at_pitchcircle / baseRadius;
var diffangle = angle_at_pitchcircle - Math.atan(angle_at_pitchcircle);
var angularToothWidthAtBase = Math.PI / numTeeth + 2*diffangle;
// build a single 2d tooth in the 'points' array:
var resolution = 5;
var points = [new CSG.Vector2D(0,0)];
for(var i = 0; i <= resolution; i++)
{
// first side of the tooth:
var angle = maxangle * i / resolution;
var tanlength = angle * baseRadius;
var radvector = CSG.Vector2D.fromAngle(angle);
var tanvector = radvector.normal();
var p = radvector.times(baseRadius).plus(tanvector.times(tanlength));
points[i+1] = p;
// opposite side of the tooth:
radvector = CSG.Vector2D.fromAngle(angularToothWidthAtBase - angle);
tanvector = radvector.normal().negated();
p = radvector.times(baseRadius).plus(tanvector.times(tanlength));
points[2 * resolution + 2 - i] = p;
}
// create the polygon and extrude into 3D:
var tooth3d = new CSG.Polygon2D(points).extrude({offset: [0, 0, thickness]});
var allteeth = new CSG();
for(var j = 0; j < numTeeth; j++)
{
var ang = j*360/numTeeth;
var rotatedtooth = tooth3d.rotateZ(ang);
allteeth = allteeth.unionForNonIntersecting(rotatedtooth);
}
// build the root circle:
points = [];
var toothAngle = 2 * Math.PI / numTeeth;
var toothCenterAngle = 0.5 * angularToothWidthAtBase;
for(var k = 0; k < numTeeth; k++)
{
var angl = toothCenterAngle + k * toothAngle;
var p1 = CSG.Vector2D.fromAngle(angl).times(rootRadius);
points.push(p1);
}
// create the polygon and extrude into 3D:
var rootcircle = new CSG.Polygon2D(points).extrude({offset: [0, 0, thickness]});
var result = rootcircle.union(allteeth);
// center at origin:
result = result.translate([0, 0, -thickness/2]);
return result;
}
I noticed that you are actually returning CSG object in the constructor, so try to use properties container described in OpenJSCAD User guide. According to the guide properties variable is intended to store metadata for the object.
This is an example from guide:
var cube = CSG.cube({radius: 1.0});
cube.properties.aCorner = new CSG.Vector3D([1, 1, 1]);
Additional comments:
You are returning different object then this in your constructor
If you will do something like this: gear = gear.rotateX(90); then you have new object
If you will use properties then metadata is cloned when you do transformation.