const express = require('express'); const dgram = require('dgram'); const oscParser = require('./osc-parser'); const bodyParser = require('body-parser'); const { Bundle, Client } = require('node-osc'); const path = require('path'); const config = require('../frontend/assets/config.json'); const app = express(); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({extended:true})); app.use(express.static('./frontend/assets')); app.get('/', function (req, res) { res.sendFile('./frontend/index.html', { root: path.resolve(__dirname + '/..') }); }); const httpServer = require('http').Server(app); /* -------- * RECEIVER * -------- */ const receiverIo = require('socket.io')(httpServer, { transports: ['websocket'] }); const onSocketListening = function() { const address = receiverUdpSocket.address(); console.log('Serveur TUIO en écoute sur : ' + address.address + ':' + address.port); }; const onSocketConnection = function(socket) { receiverUdpSocket.on('message', function(msg) { socket.emit('osc', oscParser.decode(msg)); }); }; const receiverUdpSocket = dgram.createSocket('udp4'); receiverUdpSocket.on('listening', onSocketListening); receiverUdpSocket.bind(config.app.oscUdpPort, '127.0.0.1'); app.get('/receiver/json', function (req, res) { res.status(200).send(); }); receiverIo.sockets.on('connection', (socket) =>{ console.log(`Connecté au client ${socket.id}`); const dgramCallback = function (buf) { if (config.app.debug && config.app.debugLog.backend.receiver.oscDatagram) { console.log(oscParser.decode(buf)); } socket.emit('osc', oscParser.decode(buf)); }; // forward UDP packets via socket.io receiverUdpSocket.on('message', dgramCallback); // prevent memory leak on disconnect socket.on('disconnect', function (socket) { receiverUdpSocket.removeListener('message', dgramCallback); }); }); /* ------- * EMITTER * ------- */ const emitterOscClient = new Client('127.0.0.1', config.app.oscUdpPort); let alive = []; let fseq; let objectsAlive = [] ; let lastDots = []; function getHypotenuse(touch1, touch2) { const x = Math.abs(touch1.x - touch2.x); const y = Math.abs(touch1.y - touch2.y); return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); } function getTop(dotTrio) { const dist01 = getHypotenuse(dotTrio[0], dotTrio[1]); const dist02 = getHypotenuse(dotTrio[0], dotTrio[2]); const dist12 = getHypotenuse(dotTrio[1], dotTrio[2]); const diff01m02 = Math.abs(dist01 - dist02); const diff01m12 = Math.abs(dist01 - dist12); const diff02m12 = Math.abs(dist02 - dist12); if (diff01m02 < diff02m12 && diff01m02 < diff01m12) { return 0; } else if (diff01m12 < diff01m02 && diff01m12 < diff02m12) { return 1; } else if (diff02m12 < diff01m02 && diff02m12 < diff01m12) { return 2; } } function getAngleApex(dotTrio, topIndex) { let dotA; let dotB; let dotC; dotB = dotTrio[topIndex]; if (topIndex == 0) { dotA = dotTrio[1]; dotC = dotTrio[2]; } else if (topIndex == 1) { dotA = dotTrio[0]; dotC = dotTrio[2]; } else if (topIndex == 2) { dotA = dotTrio[0]; dotC = dotTrio[1]; } const AB = [dotB.x - dotA.x, dotB.y - dotA.y]; const CB = [dotB.x - dotC.x, dotB.y - dotC.y]; const dotProd = (AB[0] * CB[0] + AB[1] * CB[1]); const crossProd = (AB[0] * CB[1] - AB[1] * CB[0]); const alpha = Math.atan2(crossProd, dotProd); //return alpha ; return Math.abs(Math.floor(alpha * 180. / Math.PI + 0.5)) ; } function getOrientation(dotTrio, topIndex) { let dotA; let dotB; let dotC; dotB = dotTrio[topIndex]; if (topIndex == 0) { dotA = dotTrio[1]; dotC = dotTrio[2]; } else if (topIndex == 1) { dotA = dotTrio[0]; dotC = dotTrio[2]; } else if (topIndex == 2) { dotA = dotTrio[0]; dotC = dotTrio[1]; } const middlePt = [(dotA.x + dotC.x) / 2, (dotA.y + dotC.y) / 2 ] ; let diff = [dotB.x - middlePt[0], dotB.y - middlePt[1]] ; //const length = Math.sqrt(Math.pow(diff[0], 2) + Math.pow(diff[1], 2) ) ; //normalize diff //diff = [diff[0] / length, diff[1] / length]; const rad = Math.atan2(diff[1], diff[0]) ; return Math.floor(rad * 180 / Math.PI) ; //return length ; } function objectGarbageCollector(){ for(const triangle of objectsAlive){ triangle.remainingDuration -= 1; }; objectsAlive = objectsAlive.filter(triangle => triangle.remainingDuration > 0); createBundle(); console.dir(objectsAlive); } setInterval(objectGarbageCollector, 1000); let currentOscBundle = null; let hasPending = false; function createBundle(){ currentOscBundle = new Bundle ; currentOscBundle.append([ '/tuio/2Dobj', 'alive'].concat(objectsAlive.map(t => t.matchingObject.apexAngle) )); for(const triangle of objectsAlive){ currentOscBundle.append([ '/tuio/2Dobj', 'set', triangle.matchingObject.apexAngle, triangle.matchingObject.apexAngle, triangle.center[0], triangle.center[1], triangle.orientation, 0.0, 0.0, 0.0, 0.0, 0.0 ]); } currentOscBundle.append(['/tuio/2Dobj', 'fseq', fseq]); hasPending = true; } function listDots(touches){ const dots = []; for(const touch of touches){ dots.push({ id: touch.identifier, x: touch.clientX, y: touch.clientY }); }; if (config.app.debug && config.app.debugLog.backend.emitter.dots) { console.log('-- dots --', dots); } return dots } function listSegments(dots){ const segments = []; if (dots.length > 2) { for (var i = 0; i < dots.length; i++) { for (var j = 0; j < dots.length; j++) { if (j !== i) { /* on vérifie que le segment n'est pas déjà listé */ const alreadyExists = segments.find(segment => { return segment.identifiers.includes(i) && segment.identifiers.includes(j); }); /* on calcule la taille du segment (l'hypoténuse) */ var hyp = getHypotenuse(dots[i], dots[j]); /* on garde uniquement les segments inférieurs à 750px (valeur par défaut) * cette valeur est la variable de configuration "maxDistanceBetweenPoints" */ if (!alreadyExists && hyp <= config.app.maxDistanceBetweenPoints) { segments.push({ identifiers: [i, j], x1: dots[i].x, x2: dots[j].x, y1: dots[i].y, y2: dots[j].y, hyp }); } } } } } if (config.app.debug && config.app.debugLog.backend.emitter.segments) { console.log('-- segments --', segments); } return segments; } function listTriangles(segments){ const triangles = []; /* on boucle sur les segments */ for(const segment of segments) { const dot1 = segment.identifiers[0]; const dot2 = segment.identifiers[1]; /* on vérifie que le triangle n'est pas déjà listé */ const alreadyExists = triangles.find(triangle => { return triangle.includes(dot1) && triangle.includes(dot2); }); if (!alreadyExists) { /* on cherche les segments qui contiennent un des 2 points du segment actuel * ex: si le segment actuel est AB, on cherche un segment contenant A (pour AC) et un autre contenant B (pour BC) */ const found1 = segments.findIndex(seg => { return (seg.identifiers.includes(dot1) && !seg.identifiers.includes(dot2)); }); const found2 = segments.findIndex(seg => { return (seg.identifiers.includes(dot2) && !seg.identifiers.includes(dot1)); }); /* si on trouve bien les 2 segments (AC et BC), on peut créer un triangle */ if (found1 !== -1 && found2 !== -1) { /* on devine quel est le 3ème point du triangle par rapport au segment actuel (le point C par rapport au segment AB) */ const dot3 = segments[found1].identifiers.find(identifier => { return identifier !== dot1 && identifier !== dot2; }); triangles.push([dot1, dot2, dot3]); } } }; if (config.app.debug && config.app.debugLog.backend.emitter.triangles) { console.log('-- triangles --', triangles); } return triangles } function filterTriangles(dots, triangles){ const filteredTriangles = []; /* Définition de l'apex, de la position du centre et de l'orientation */ for(const triangle of triangles){ const objTriangle = {} ; objTriangle.dots = []; objTriangle.dots[0] = dots[triangle[0]]; objTriangle.dots[1] = dots[triangle[1]]; objTriangle.dots[2] = dots[triangle[2]]; objTriangle.apex = getTop(objTriangle.dots); objTriangle.center = [ (objTriangle.dots[0].x + objTriangle.dots[1].x + objTriangle.dots[2].x) / 3, (objTriangle.dots[0].y + objTriangle.dots[1].y + objTriangle.dots[2].y) / 3 ]; objTriangle.angleApex = getAngleApex(objTriangle.dots, objTriangle.apex); objTriangle.orientation = getOrientation(objTriangle.dots, objTriangle.apex); if (config.app.debug && config.app.debugLog.backend.emitter.apex) { console.log('-- apex --', objTriangle.apex); console.log('angle: ', objTriangle.angleApex); console.log('centerPos: ' + objTriangle.center); console.log('orientation: ' + objTriangle.orientation); } //verify if triangle has a corresponding triangle in config const matchingObject = config.objects.find(item => { return objTriangle.angleApex > item.apexAngle - config.app.matchingTolerance && objTriangle.angleApex < item.apexAngle + config.app.matchingTolerance; }); if (matchingObject) { objTriangle.matchingObject = matchingObject; filteredTriangles.push(objTriangle); if (config.app.debug && config.app.debugLog.backend.emitter.matchingObject) { console.log('-- matchingObject --', objTriangle); } } }; return filteredTriangles; } function updateAliveTriangles(filteredTriangles) { for (const triangle of filteredTriangles) { let idx = objectsAlive.findIndex(item => { return triangle.matchingObject.name == item.matchingObject.name; }); if (idx == -1) { triangle.remainingDuration = config.app.remainingDuration; objectsAlive.push(triangle); } else { triangle.remainingDuration = config.app.remainingDuration; objectsAlive[idx] = triangle; } } if (config.app.debug && config.app.debugLog.backend.emitter.aliveTriangles) { console.log('-- aliveTriangles --', objectsAlive); } } const sendBundle = () => { if (hasPending) { emitterOscClient.send(currentOscBundle, () => { hasPending = false; }); } setTimeout(() => { sendBundle(); }, config.app.timerRefresh); }; sendBundle(); app.post('/emitter/json', function (req, res) { if (config.app.debug && config.app.debugLog.backend.emitter.httpRequest) { console.log('### Emitter POST request ###'); } let oscBundle; // if (req.body.type === 'touchend') { // fseq = fseq ? fseq + 1 : 1; // const aliveMessage = [ '/tuio/2Dcur', 'alive' ].concat(alive); // currentOscBundle = new Bundle( // [ '/tuio/2Dcur', 'source', `tangibles${req.body.section.toString()}@127.0.0.1` ], // aliveMessage, // [ '/tuio/2Dcur', 'fseq', fseq ], // [ // '/tuio/2Dcur', // 'del', // req.body.touches[0].identifier // ] // ); // emitterOscClient.send(currentOscBundle, () => { // const index = alive.indexOf(req.body.touches[0].identifier); // alive.splice(index, 1); // if (alive.length === 0) { // currentOscBundle = new Bundle( // [ '/tuio/2Dcur', 'source', `tangibles${req.body.section.toString()}@127.0.0.1` ], // [ '/tuio/2Dcur', 'alive' ], // [ '/tuio/2Dcur', 'fseq', fseq ] // ); // emitterOscClient.send(currentOscBundle, () => { // res.status(200).send(); // fseq = 0; // hasPending = false; // }); // } else { // res.status(200).send(); // } // }); // } else { if (req.body.touches && req.body.touches.length && req.body.touches.length > 0) { fseq = fseq ? fseq + 1 : 1; const touches = Object.keys(req.body.touches); const aliveMessage = [ '/tuio/2Dcur', 'alive' ].concat(alive); touches.forEach(touch => { const id = req.body.touches[touch].identifier; if (!alive.includes(id)) { alive.push(id); aliveMessage.push(id); } }); /* Listage de tous les points */ const dots = listDots(req.body.touches); lastDots = dots; const segments = listSegments(dots); const triangles = listTriangles(segments); const filteredTriangles = filterTriangles(dots, triangles); updateAliveTriangles(filteredTriangles); createBundle(); res.status(200).send(); } else { res.status(200).send(); } }); httpServer.listen(config.app.httpPort, function () { console.log(`Votre app est disponible sur http://localhost:${config.app.httpPort} !`) });