Adding shapes to ofxkinect point cloud

Hi,

I’m making a point cloud using an edited version of the ofxkinect example. I’m trying to replace the glpoints with 3d objects with textures eg. ofBox with image textures. Can anyone point me in the right direction of how I would go about achieving this? I’m quite new to OF and new to opengl, coming more from processing.

Any help would be greatly appreciated

Here’s the code.

testApp.h

  
  
#pragma once  
  
#include "ofMain.h"  
  
class testApp : public ofBaseApp {  
	public:  
		void setup();  
		void update();  
		void draw();  
		  
		void keyPressed(int key);  
		void keyReleased(int key);  
		void mouseMoved(int x, int y);  
		void mouseDragged(int x, int y, int button);  
		void mousePressed(int x, int y, int button);  
		void mouseReleased(int x, int y, int button);  
		void windowResized(int w, int h);  
		void dragEvent(ofDragInfo dragInfo);  
		void gotMessage(ofMessage msg);  
		  
		ofImage ofLogo; // the OF logo  
		ofLight light; // creates a light and enables lighting  
		ofEasyCam cam; // add mouse controls for camera movement  
};  
  
  

testApp.cpp

  
  
#include "testApp.h"  
  
//--------------------------------------------------------------  
void testApp::setup(){  
	ofSetVerticalSync(true);  
  
	// this uses depth information for occlusion  
	// rather than always drawing things on top of each other  
	glEnable(GL_DEPTH_TEST);  
	  
	// ofBox uses texture coordinates from 0-1, so you can load whatever  
	// sized images you want and still use them to texture your box  
	// but we have to explicitly normalize our tex coords here  
	ofEnableNormalizedTexCoords();  
	  
	// loads the OF logo from disk  
	ofLogo.loadImage("of.png");  
	  
	// draw the ofBox outlines with some weight  
	ofSetLineWidth(1);  
}  
  
//--------------------------------------------------------------  
void testApp::update(){  
  
}  
  
//--------------------------------------------------------------  
void testApp::draw(){  
	ofBackground(0, 0, 0);  
	  
	float movementSpeed = .1;  
	float cloudSize = ofGetWidth() / 2;  
	float maxBoxSize = 100;  
	float spacing = 1;  
	int boxCount = 100;  
	  
	cam.begin();  
	  
	for(int i = 0; i < boxCount; i++) {  
		ofPushMatrix();  
		  
		float t = (ofGetElapsedTimef() + i * spacing) * movementSpeed;  
		ofVec3f pos(  
			ofSignedNoise(t, 0, 0),  
			ofSignedNoise(0, t, 0),  
			ofSignedNoise(0, 0, t));  
		  
		float boxSize = maxBoxSize * ofNoise(pos.x, pos.y, pos.z);  
		  
		pos *= cloudSize;  
		ofTranslate(pos);  
		ofRotateX(pos.x);  
		ofRotateY(pos.y);  
		ofRotateZ(pos.z);  
		  
		ofLogo.bind();  
		ofFill();  
		ofSetColor(255);  
		ofBox(boxSize);  
		ofLogo.unbind();  
		  
		ofNoFill();  
		ofSetColor(ofColor::fromHsb(sinf(t) * 128 + 128, 255, 255));  
		ofBox(boxSize * 1.1f);  
		  
		ofPopMatrix();  
	}  
	  
	cam.end();  
}  
  
  
//--------------------------------------------------------------  
void testApp::keyPressed(int key){  
  
}  
  
//--------------------------------------------------------------  
void testApp::keyReleased(int key){  
  
}  
  
//--------------------------------------------------------------  
void testApp::mouseMoved(int x, int y){  
  
}  
  
//--------------------------------------------------------------  
void testApp::mouseDragged(int x, int y, int button){  
  
}  
  
//--------------------------------------------------------------  
void testApp::mousePressed(int x, int y, int button){  
  
}  
  
//--------------------------------------------------------------  
void testApp::mouseReleased(int x, int y, int button){  
  
}  
  
//--------------------------------------------------------------  
void testApp::windowResized(int w, int h){  
  
}  
  
//--------------------------------------------------------------  
void testApp::gotMessage(ofMessage msg){  
  
}  
  
//--------------------------------------------------------------  
void testApp::dragEvent(ofDragInfo dragInfo){   
  
}  
  
  

What you’re looking for is called “a voxel” and there’s lots of different techniques to render a lot of them at once, all of which have different tradeoffs.

Have you checked out the examples/gl/vboExample? Because that’ll show you how to use ofVboMesh, which is what you’re going to want if you want textured cubes. You probably want to just render all the cubes as a part of one mesh, so just make lots and lots of cubes for each position in the point cloud that you want to represent, then move each block into place, then draw it all at once. In my experience with this doing instancing, i.e. drawing one VBO of a cube 320x240 times is slower than just putting 320x240 cubes in one ofVboMesh and then just drawing that. However if you don’t have a pretty fast computer that can be a bit slow sadly. You should give it a shot though and see how it comes out. The final way to do this, and probably the “right way” is to use a geometry shader and there’s an example here that might be able to show you more about how it works: https://github.com/chriskiefer/MarchingCubesGPU

I realize that’s kind of a block of information, but I hope it’s helpful.

Thank you Joshua,

That seems to have pointed me in the right direction. I’ve managed to attach an ofImage to the glpoints using vbomesh, I wanted to try a simpler version before I moved on to 3d shapes would it be a similar process as this for using ofBox.

I want to use an array of different textures to attach randomly to the glpoints how would I go about this? Would I use vector texture?

Thanks

testApp.h

  
  
#pragma once  
  
#include "ofMain.h"  
#include "ofxOpenCv.h"  
#include "ofxKinect.h"  
  
class testApp : public ofBaseApp {  
public:  
	  
	void setup();  
	void update();  
	void draw();  
	void exit();  
	  
	void drawPointCloud();  
	  
	void keyPressed(int key);  
	void mouseDragged(int x, int y, int button);  
	void mousePressed(int x, int y, int button);  
	void mouseReleased(int x, int y, int button);  
	void windowResized(int w, int h);  
	  
	ofxKinect kinect;  
	  
	int angle;  
	  
	// used for viewing the point cloud  
	ofEasyCam easyCam;  
    int near;  
    int step;  
    int nearThresh;  
    int farThresh;  
    int pointSize;  
    bool wall;  
      
    ofVboMesh vbomesh;  
    ofImage texture;  
      
};  
  
  

testApp.cpp

  
  
#include "testApp.h"  
  
//--------------------------------------------------------------  
void testApp::setup() {  
      
	ofSetLogLevel(OF_LOG_VERBOSE);  
      
	kinect.init();  
      
	// enable depth->video image calibration  
	kinect.setRegistration(true);  
    kinect.setDepthClipping (0,300);  
	  
	kinect.open(); // opens first available kinect  
	  
	ofSetFrameRate(60);  
	  
	// zero the tilt on startup  
	angle = 0;  
	kinect.setCameraTiltAngle(angle);  
      
    near = -1000;  
      
    step = 2;  
      
    nearThresh = 50;  
    farThresh = 1000;  
    pointSize = 3;  
    wall = FALSE;  
      
    ofDisableArbTex();  
    texture.loadImage("radial.png");  
  
      
}  
  
//--------------------------------------------------------------  
void testApp::update() {  
	  
	ofBackground(0, 0, 0);  
	  
	kinect.update();  
	  
	// there is a new frame and we are connected  
	if(kinect.isFrameNew()) {  
		  
    }  
  
}  
//--------------------------------------------------------------  
void testApp::draw() {  
      
    if(wall != FALSE) {  
      
        ofSetColor(20, 20, 20);  
        glBegin(GL_QUADS);  
        glVertex3f(0.0, ofGetHeight(), -600);  
        glVertex3f(ofGetWidth(), ofGetHeight(), -600);  
        glVertex3f(ofGetWidth(), 0, -600);  
        glVertex3f(0, 0, -600);  
        glEnd();  
          
        //fake wall  
        ofSetColor(50, 50, 50);  
        glBegin(GL_QUADS);  
        glVertex3f(0.0, ofGetHeight(), 0);  
        glVertex3f(ofGetWidth(), ofGetHeight(), 0);  
        glVertex3f(ofGetWidth(), ofGetHeight(), -600);  
        glVertex3f(0, ofGetHeight(), -600);  
        glEnd();  
          
    }  
	  
	ofSetColor(255, 255, 255);  
  
    easyCam.begin();  
    drawPointCloud();  
    easyCam.end();  
		  
	// draw instructions  
	ofSetColor(255, 255, 255);  
	stringstream reportStream;  
	reportStream << "accel is: " << ofToString(kinect.getMksAccel().x, 2) << " / "  
	<< ofToString(kinect.getMksAccel().y, 2) << " / "  
	<< ofToString(kinect.getMksAccel().z, 2) << endl  
    << "fps: " << ofGetFrameRate() << endl  
	<< "press c to close the connection and o to open it again, connection is: " << kinect.isConnected() << endl  
	<< "press UP and DOWN to change the tilt angle: " << angle << " degrees" << endl  
    << "nearThresh = " << nearThresh << endl  
    << "farThresh = " << farThresh << endl  
    << "step = " << step << endl  
    << "pointSize = " << pointSize << endl;  
	ofDrawBitmapString(reportStream.str(),20,20);  
      
}  
  
//--------------------------------------------------------------  
void testApp::drawPointCloud() {  
	int w = 640;  
	int h = 480;  
	//ofMesh mesh;  
    vbomesh.clear();  
	vbomesh.setMode(OF_PRIMITIVE_POINTS);  
	for(int y = 0; y < h; y += step) {  
		for(int x = 0; x < w; x += step) {  
              
            float distance = kinect.getDistanceAt(x, y);  
              
			if(distance > nearThresh && distance < farThresh) {  
                  
				//vbomesh.addColor(ofFloatColor(1.0, 0.2 ,0.82));  
				vbomesh.addVertex(kinect.getWorldCoordinateAt(x, y));  
                  
			}  
		}  
	}  
      
	glPointSize(pointSize);  
	ofPushMatrix();  
	// the projected points are 'upside down' and 'backwards'  
	ofScale(1, -1, -1);  
	ofTranslate(0, 0, near);  
	glEnable(GL_DEPTH_TEST);  
    glEnable(GL_POINT_SMOOTH);  
	ofEnableAlphaBlending();  
    ofEnablePointSprites();  
    texture.getTextureReference().bind();  
    vbomesh.drawFaces();  
      
    ofDisablePointSprites();  
	glDisable(GL_DEPTH_TEST);  
	ofPopMatrix();  
      
}  
  
//--------------------------------------------------------------  
void testApp::exit() {  
	kinect.setCameraTiltAngle(0); // zero the tilt on exit  
	kinect.close();  
}  
  
//--------------------------------------------------------------  
void testApp::keyPressed (int key) {  
	switch (key) {  
        case 'o':  
			kinect.setCameraTiltAngle(angle); // go back to prev tilt  
			kinect.open();  
			break;  
			  
		case 'c':  
			kinect.setCameraTiltAngle(0); // zero the tilt  
			kinect.close();  
			break;  
			  
		case '1':  
			kinect.setLed(ofxKinect::LED_GREEN);  
			break;  
			  
		case '2':  
			kinect.setLed(ofxKinect::LED_YELLOW);  
			break;  
			  
		case '3':  
			kinect.setLed(ofxKinect::LED_RED);  
			break;  
			  
		case '4':  
			kinect.setLed(ofxKinect::LED_BLINK_GREEN);  
			break;  
			  
		case '5':  
			kinect.setLed(ofxKinect::LED_BLINK_YELLOW_RED);  
			break;  
			  
		case '0':  
			kinect.setLed(ofxKinect::LED_OFF);  
			break;  
			  
		case OF_KEY_UP:  
			angle++;  
			if(angle>30) angle=30;  
			kinect.setCameraTiltAngle(angle);  
			break;  
			  
		case OF_KEY_DOWN:  
			angle--;  
			if(angle<-30) angle=-30;  
			kinect.setCameraTiltAngle(angle);  
			break;  
        case 't':  
            near += 20;  
            break;  
        case 'y':  
            near -= 20;  
            break;  
        case 'g':  
            step ++;  
            break;  
        case 'h':  
            if(step > 1) {  
                step --;  
            }  
            break;  
        case 'a':  
            nearThresh +=20;  
            break;  
        case 's':  
            nearThresh -=20;  
            break;  
        case 'd':  
            farThresh +=20;  
            break;  
        case 'f':  
            farThresh -=20;  
            break;  
        case 'n':  
            pointSize ++;  
            break;  
        case 'm':  
            pointSize --;  
            break;  
        case 'w':  
            if(wall == TRUE){  
                wall = FALSE;  
            } else if (wall == FALSE) {  
                wall = TRUE;  
            }  
            break;  
        case 'p':  
            ofToggleFullscreen();  
            break;  
              
	}  
}  
  
//--------------------------------------------------------------  
void testApp::mouseDragged(int x, int y, int button)  
{}  
  
//--------------------------------------------------------------  
void testApp::mousePressed(int x, int y, int button)  
{}  
  
//--------------------------------------------------------------  
void testApp::mouseReleased(int x, int y, int button)  
{}  
  
//--------------------------------------------------------------  
void testApp::windowResized(int w, int h)  
{}  
  
  

If you just want textures for now, you can use point sprites. Check out the example in examples/gl/billboardExample

Don’t forget to unbind your texture when you are done with it.

Thanks Nick,

The billboard example only uses one image for the texture. Is there a way to use multiple images and apply them randomly?

Do you want to use parts of different images in the same mesh, i.e. like the left corner of one image, the right corner of another, etc? Or do you want to use an image made of different images across your mesh?

In either case I think you want to create an ofTexture out of multiple different ofTexture instances and then bind that to your mesh.

Hi Joshua,

I’m trying to have each individual glpoint display randomly a different image. i.e have an array of images img1.png, img2.png, img3.png etc and display them in the vbomesh.

Are you saying that I need to create an ofTexture of those images and then apply that texture to the mesh?

How do I create and ofTexture from a group of images?

Thanks

Well, that’s one way to do it, I might not be understanding exactly what you’re trying to do though, so there might be a better way. You can just store a texture made up of multiple ofTextures by binding an FBO or something like that and then drawing lots of different textures, like:

  
  
fbo.begin();  
tex1.draw(0, 0, 100, 100);  
tex2.draw(100, 0, 200, 100);  
tex3.draw(200, 0, 300, 100);  
fbo.end();  
  

That might not be the optimal way to do it if you have say 1000 images though. Does that sort of help?

Thanks for all the help Joshua,

I’ve had a go trying to implement it, but I can’t seem to get it to work not sure if I’m doing it correctly.

I found something that is visually similar to what I’m looking for - http://forum.openframeworks.cc/t/advice-for-drawing-a-single-images–texture-many-times-very-quickly/12171/0

I’m only looking to have maybe about 10 - 20 texture images.

testApp.h

  
  
#pragma once  
  
#include "ofMain.h"  
#include "ofxOpenCv.h"  
#include "ofxKinect.h"  
  
class testApp : public ofBaseApp {  
public:  
	  
	void setup();  
	void update();  
	void draw();  
	void exit();  
	  
	void drawPointCloud();  
	  
	void keyPressed(int key);  
	void mouseDragged(int x, int y, int button);  
	void mousePressed(int x, int y, int button);  
	void mouseReleased(int x, int y, int button);  
	void windowResized(int w, int h);  
	  
	ofxKinect kinect;  
	  
	int angle;  
	  
	// used for viewing the point cloud  
	ofEasyCam easyCam;  
    int near;  
    int step;  
    int nearThresh;  
    int farThresh;  
    int pointSize;  
    bool wall;  
      
    ofVboMesh vbomesh;  
    ofImage tex1;  
    ofImage tex2;  
    ofImage tex3;  
      
    ofFbo fbo;  
      
};  
  
  

testApp.cpp

  
  
#include "testApp.h"  
  
//--------------------------------------------------------------  
void testApp::setup() {  
      
	ofSetLogLevel(OF_LOG_VERBOSE);  
      
	kinect.init();  
      
	// enable depth->video image calibration  
	kinect.setRegistration(true);  
    kinect.setDepthClipping (0,300);  
	  
	kinect.open(); // opens first available kinect  
	  
	ofSetFrameRate(60);  
	  
	// zero the tilt on startup  
	angle = 0;  
	kinect.setCameraTiltAngle(angle);  
      
    near = -1000;  
      
    step = 2;  
      
    nearThresh = 50;  
    farThresh = 1000;  
    pointSize = 3;  
    wall = FALSE;  
      
    ofDisableArbTex();  
    tex1.loadImage("radial.png");  
    tex2.loadImage("stripes.png");  
    tex3.loadImage("dot.png");  
  
    fbo.begin();  
    ofClear(255, 255, 255);  
    fbo.end();  
      
}  
  
//--------------------------------------------------------------  
void testApp::update() {  
	  
	ofBackground(0, 0, 0);  
	  
	kinect.update();  
	  
	// there is a new frame and we are connected  
	if(kinect.isFrameNew()) {  
		  
    }  
      
    fbo.begin();  
        tex1.draw(0, 0, 100, 100);  
        tex2.draw(100, 0, 200, 100);  
        tex3.draw(200, 0, 300, 100);  
    fbo.end();  
  
}  
//--------------------------------------------------------------  
void testApp::draw() {  
      
    if(wall != FALSE) {  
      
        ofSetColor(20, 20, 20);  
        glBegin(GL_QUADS);  
        glVertex3f(0.0, ofGetHeight(), -600);  
        glVertex3f(ofGetWidth(), ofGetHeight(), -600);  
        glVertex3f(ofGetWidth(), 0, -600);  
        glVertex3f(0, 0, -600);  
        glEnd();  
          
        //fake wall  
        ofSetColor(50, 50, 50);  
        glBegin(GL_QUADS);  
        glVertex3f(0.0, ofGetHeight(), 0);  
        glVertex3f(ofGetWidth(), ofGetHeight(), 0);  
        glVertex3f(ofGetWidth(), ofGetHeight(), -600);  
        glVertex3f(0, ofGetHeight(), -600);  
        glEnd();  
          
    }  
	  
	ofSetColor(255, 255, 255);  
  
    easyCam.begin();  
    drawPointCloud();  
    easyCam.end();  
		  
	// draw instructions  
	ofSetColor(255, 255, 255);  
	stringstream reportStream;  
	reportStream << "accel is: " << ofToString(kinect.getMksAccel().x, 2) << " / "  
	<< ofToString(kinect.getMksAccel().y, 2) << " / "  
	<< ofToString(kinect.getMksAccel().z, 2) << endl  
    << "fps: " << ofGetFrameRate() << endl  
	<< "press c to close the connection and o to open it again, connection is: " << kinect.isConnected() << endl  
	<< "press UP and DOWN to change the tilt angle: " << angle << " degrees" << endl  
    << "nearThresh = " << nearThresh << endl  
    << "farThresh = " << farThresh << endl  
    << "step = " << step << endl  
    << "pointSize = " << pointSize << endl;  
	ofDrawBitmapString(reportStream.str(),20,20);  
      
}  
  
//--------------------------------------------------------------  
void testApp::drawPointCloud() {  
	int w = 640;  
	int h = 480;  
	//ofMesh mesh;  
    vbomesh.clear();  
	vbomesh.setMode(OF_PRIMITIVE_POINTS);  
	for(int y = 0; y < h; y += step) {  
		for(int x = 0; x < w; x += step) {  
              
            float distance = kinect.getDistanceAt(x, y);  
              
			if(distance > nearThresh && distance < farThresh) {  
                  
				//vbomesh.addColor(ofFloatColor(1.0, 0.2 ,0.82));  
				vbomesh.addVertex(kinect.getWorldCoordinateAt(x, y));  
                  
			}  
		}  
	}  
      
	glPointSize(pointSize);  
	ofPushMatrix();  
	// the projected points are 'upside down' and 'backwards'  
	ofScale(1, -1, -1);  
	ofTranslate(0, 0, near);  
	glEnable(GL_DEPTH_TEST);  
    glEnable(GL_POINT_SMOOTH);  
	ofEnableAlphaBlending();  
    ofEnablePointSprites();  
    fbo.getTextureReference().bind();  
    vbomesh.drawFaces();  
      
    ofDisablePointSprites();  
	glDisable(GL_DEPTH_TEST);  
    fbo.unbind();  
	ofPopMatrix();  
      
}  
  
//--------------------------------------------------------------  
void testApp::exit() {  
	kinect.setCameraTiltAngle(0); // zero the tilt on exit  
	kinect.close();  
}  
  
//--------------------------------------------------------------  
void testApp::keyPressed (int key) {  
	switch (key) {  
        case 'o':  
			kinect.setCameraTiltAngle(angle); // go back to prev tilt  
			kinect.open();  
			break;  
			  
		case 'c':  
			kinect.setCameraTiltAngle(0); // zero the tilt  
			kinect.close();  
			break;  
			  
		case '1':  
			kinect.setLed(ofxKinect::LED_GREEN);  
			break;  
			  
		case '2':  
			kinect.setLed(ofxKinect::LED_YELLOW);  
			break;  
			  
		case '3':  
			kinect.setLed(ofxKinect::LED_RED);  
			break;  
			  
		case '4':  
			kinect.setLed(ofxKinect::LED_BLINK_GREEN);  
			break;  
			  
		case '5':  
			kinect.setLed(ofxKinect::LED_BLINK_YELLOW_RED);  
			break;  
			  
		case '0':  
			kinect.setLed(ofxKinect::LED_OFF);  
			break;  
			  
		case OF_KEY_UP:  
			angle++;  
			if(angle>30) angle=30;  
			kinect.setCameraTiltAngle(angle);  
			break;  
			  
		case OF_KEY_DOWN:  
			angle--;  
			if(angle<-30) angle=-30;  
			kinect.setCameraTiltAngle(angle);  
			break;  
        case 't':  
            near += 20;  
            break;  
        case 'y':  
            near -= 20;  
            break;  
        case 'g':  
            step ++;  
            break;  
        case 'h':  
            if(step > 1) {  
                step --;  
            }  
            break;  
        case 'a':  
            nearThresh +=20;  
            break;  
        case 's':  
            nearThresh -=20;  
            break;  
        case 'd':  
            farThresh +=20;  
            break;  
        case 'f':  
            farThresh -=20;  
            break;  
        case 'n':  
            pointSize ++;  
            break;  
        case 'm':  
            pointSize --;  
            break;  
        case 'w':  
            if(wall == TRUE){  
                wall = FALSE;  
            } else if (wall == FALSE) {  
                wall = TRUE;  
            }  
            break;  
        case 'p':  
            ofToggleFullscreen();  
            break;  
              
	}  
}  
  
//--------------------------------------------------------------  
void testApp::mouseDragged(int x, int y, int button)  
{}  
  
//--------------------------------------------------------------  
void testApp::mousePressed(int x, int y, int button)  
{}  
  
//--------------------------------------------------------------  
void testApp::mouseReleased(int x, int y, int button)  
{}  
  
//--------------------------------------------------------------  
void testApp::windowResized(int w, int h)  
{}  
  
  

If anyone else is trying to have 3d cubes point cloud there is a good example here from a kinect workshop
http://cargocollective.com/benMcChesney/Hacking-the-Kinect-GAFFTA some good slides and info.

I tried it with having textured cubes but it was too heavy on my computer.

I’m still trying to get a set of images appear randomly on gl points here is what I have sofar I’m trying to get each point to have a random image.

For your random images, have you considered just making a giant sprite sheet and then randomly assigning texture coordinates to each point? If you use a fixed sprite size (i.e. 16x16 or 64x64) it should be easy to make sure you’re properly placing the sprites in the drawn textures.

Thanks this is the problem I’ve been having is assigning each texture to an individual point rather than to every point.

This is how I have been doing it, but its a mess because a different texture is displayed each frame.

testApp.h

  
  
ofImage img1;  
ofImage img2;  
ofImage img3;  
ofTexture tex [3];  
  

testApp.cpp

  
  
//setup()  
img1.loadImage("parts/01.png");  
img2.loadImage("parts/02.png");  
img3.loadImage("parts/03.png");  
  
tex[0] = img1.getTextureReference();  
tex[1] = img2.getTextureReference();  
tex[2] = img3.getTextureReference();  
  
//drawPointCloud()  
tex[int(ofRandom(0,3))].bind();  
vbomesh.drawVertices();  
tex[int(ofRandom(0,3))].unbind();  
  

Do I need to make a sprite sheet? or is that just an easier way of loading the images in one go?

Is it possible to do something like

foreach point assign tex[1]

I’m not quite sure how to assign a texture coordinate does it have to happen in the update function?

Thank you so much for your help

Hi dreamaerd,

have you made any progress? I’m intrested in your solution as I’m facing a similar problem. I’m trying to play lots of different sequences of small images (eg walking dog) based on kinect input, but with a different sequence order / speed for each particle so to create an ‘ordered chaos’.

Simply making a particle from scratch works but doens’t scale very well :slight_smile:

Tnx,
Kj