X-Git-Url: https://gitweb.ps.run/cloth_sim/blobdiff_plain/f48718f397ffbc1eb006460c495cf260668bd545..56f6324db4aa55946a6b5ef7de3e1df8b0e22bca:/Scripts/cloth.js diff --git a/Scripts/cloth.js b/Scripts/cloth.js index 546f7d3..aaa9d24 100644 --- a/Scripts/cloth.js +++ b/Scripts/cloth.js @@ -1,128 +1,80 @@ -/** - * Convenience Function for calculating the distance between two vectors - * because THREE JS Vector functions mutate variables - * @param {Vector3} a - Vector A - * @param {Vector3} b - Vector B - */ -function vectorLength(a, b) { - let v1 = new THREE.Vector3(); - v1.copy(a); - let v2 = new THREE.Vector3(); - v2.copy(b); - - return v1.sub(v2).length(); -} - -/** - * Class representing a quad face - * Each face consists of two triangular mesh faces - * containts four indices for determining vertices - * and six springs, one between each of the vertices - */ -export class Face { - a; - b; - c; - d; - - springs = []; - - constructor(a, b, c, d) { - this.a = a; - this.b = b; - this.c = c; - this.d = d; +const DAMPING = 0.03; +const DRAG = 1 - DAMPING; +const MASS = 0.35; +const GRAVITY = new THREE.Vector3(0, -9.81 * MASS, 0); +const K = 1; + +const options = { + wind: true, +}; + +class Constraint { + constructor(p1, p2, restDist) { + this.p1 = p1; + this.p2 = p2; + this.restDist = restDist; + } + + satisfy() { + const diff = this.p2.position.clone().sub(this.p1.position); + const currentDist = diff.length(); + if (currentDist == 0) return; + if (currentDist <= this.restDist) return; + //const correction = diff.multiplyScalar(1 - (this.restDist / currentDist)); + const correction = diff.multiplyScalar((currentDist - this.restDist) / currentDist); + correction.multiplyScalar(K); + correction.clampLength(0, 1); + const correctionHalf = correction.multiplyScalar(0.5); + if (this.p1.movable && this.p2.movable) { + this.p1.position.add(correctionHalf); + this.p2.position.sub(correctionHalf); + } else if (! this.p1.movable && this.p2.movable) { + this.p2.position.sub(correction); + } else if (this.p1.movable && ! this.p2.movable) { + this.p1.position.add(correction); + } } } -/** - * Class representing a single spring - * has a current and resting length - * and indices to the two connected vertices - */ -export class Spring { - restLength; - currentLength; - index1; - index2; - - - /** - * set vertex indices - * and calculate inital length based on the - * vertex positions - * @param {Array} vertices - * @param {number} index1 - * @param {number} index2 - */ - constructor(vertices, index1, index2) { - this.index1 = index1; - this.index2 = index2; - - let length = vectorLength(vertices[index1], vertices[index2]); - this.restLength = length; - this.currentLength = length; - } - - getDirection(vertices) { - let direction = new THREE.Vector3(); - direction.copy(vertices[this.index1]); - - direction.sub(vertices[this.index2]); - direction.divideScalar(vectorLength(vertices[this.index1], vertices[this.index2])); - - return direction; - } +class Particle { + movable = true; + + constructor(x, y, z, mass) { + this.position = new THREE.Vector3(x, y, z); + this.previous = new THREE.Vector3(x, y, z); + this.acceleration = new THREE.Vector3(0, 0, 0); + this.mass = mass; + } + addForce(force) { + this.acceleration.add( + force.clone().multiplyScalar(1/this.mass) + ); + } + verlet(dt) { + // verlet algorithm + // next position = 2 * current Position - previous position + acceleration * (passed time)^2 + // acceleration (dv/dt) = F(net) + const nextPosition = this.position.clone().sub(this.previous); + nextPosition.multiplyScalar(DRAG); + nextPosition.add(this.position); + nextPosition.add(this.acceleration.multiplyScalar(dt*dt)); + + if (this.movable) { + this.previous = this.position; + this.position = nextPosition; + } - update(vertices) { - let length = vectorLength(vertices[this.index1], vertices[this.index2]); - this.currentLength = length; + this.acceleration.set(0, 0, 0); } } -/** - * Class representing a single piece of cloth - * contains THREE JS geometry, - * logically represented by an array of adjacent faces - * and vertex weights which are accessed by the same - * indices as the vertices in the Mesh - */ -export class Cloth { - VertexWeight = 1; - - geometry = new THREE.Geometry(); - - faces = []; - - vertexWeights = []; - - vertexRigidness = []; - - fixedPoints = []; - - externalForces = []; - windForce = 50; - - windFactor = new THREE.Vector3(0, 0, 0); - - /** - * creates a rectangular piece of cloth - * takes the size of the cloth - * and the number of vertices it should be composed of - * @param {number} width - width of the cloth - * @param {number} height - height of the cloth - * @param {number} numPointsWidth - number of vertices in horizontal direction - * @param {number} numPointsHeight - number of vertices in vertical direction - */ - createBasic(width, height, numPointsWidth, numPointsHeight) { - /** resulting vertices and faces */ - let vertices = []; - let faces = []; - +class Cloth { + constructor(width, height, numPointsWidth, numPointsHeight) { this.width = width; this.height = height; this.numPointsWidth = numPointsWidth; this.numPointsHeight = numPointsHeight; + this.windFactor = new THREE.Vector3(5, 2, 2); /** * distance between two vertices horizontally/vertically @@ -134,488 +86,143 @@ export class Cloth { /** * iterate over the number of vertices in x/y axis - * and add a new Vector3 to "vertices" + * and add a new Particle to "particles" */ + this.particles = []; for (let y = 0; y < numPointsHeight; y++) { for (let x = 0; x < numPointsWidth; x++) { - vertices.push( - new THREE.Vector3((x - ((numPointsWidth-1)/2)) * stepWidth, height - (y + ((numPointsHeight-1)/2)) * stepHeight, 0) + this.particles.push( + new Particle( + (x - ((numPointsWidth-1)/2)) * stepWidth, + height - (y + ((numPointsHeight-1)/2)) * stepHeight, + 0, + MASS) ); } } - /** - * helper function to calculate index of vertex - * in "vertices" array based on its x and y positions - * in the mesh - * @param {number} x - x index of vertex - * @param {number} y - y index of vertex - */ - function getVertexIndex(x, y) { - return y * numPointsWidth + x; - } + //this.particles[this.getVertexIndex(0, 0)].movable = false; + const n = 3; + for (let i = 0; i <= n; i++) + this.particles[this.getVertexIndex(0, Math.floor((numPointsHeight-1)*(i/n)))].movable = false; + //this.particles[this.getVertexIndex(0, numPointsHeight-1)].movable = false; + //this.particles[this.getVertexIndex(numPointsWidth-1, 0)].movable = false; + + const REST_DIST_X = width / (numPointsWidth-1); + const REST_DIST_Y = height / (numPointsHeight-1); /** - * generate faces based on 4 vertices - * and 6 springs each + * generate constraints (springs) */ - for (let y = 0; y < numPointsHeight - 1; y++) { - for (let x = 0; x < numPointsWidth - 1; x++) { - let newFace = new Face( - getVertexIndex(x, y), - getVertexIndex(x, y + 1), - getVertexIndex(x + 1, y), - getVertexIndex(x + 1, y + 1), - ); - - newFace.springs.push(new Spring(vertices, getVertexIndex(x, y), getVertexIndex(x + 1, y))); // oben - newFace.springs.push(new Spring(vertices, getVertexIndex(x, y), getVertexIndex(x, y + 1))); // links - newFace.springs.push(new Spring(vertices, getVertexIndex(x, y), getVertexIndex(x + 1, y + 1))); // oben links -> unten rechts diagonal - newFace.springs.push(new Spring(vertices, getVertexIndex(x + 1, y), getVertexIndex(x, y + 1))); // oben rechts -> unten links diagonal - newFace.springs.push(new Spring(vertices, getVertexIndex(x + 1, y), getVertexIndex(x + 1, y + 1))); // rechts - newFace.springs.push(new Spring(vertices, getVertexIndex(x, y + 1), getVertexIndex(x + 1, y + 1))); // unten - - faces.push(newFace); + this.constraints = []; + for (let y = 0; y < numPointsHeight; y++) { + for (let x = 0; x < numPointsWidth; x++) { + if (x < numPointsWidth-1) { + this.constraints.push(new Constraint( + this.particles[this.getVertexIndex(x, y)], + this.particles[this.getVertexIndex(x+1, y)], + REST_DIST_X + )); + } + if (y < numPointsHeight-1) { + this.constraints.push(new Constraint( + this.particles[this.getVertexIndex(x, y)], + this.particles[this.getVertexIndex(x, y+1)], + REST_DIST_Y + )); + } } } - - /** - * call createExplicit - * with generated vertices and faces - */ - this.createExplicit(vertices, faces); - - /** - * hand cloth from left and right upper corners - */ - this.fixedPoints.push(getVertexIndex(0, 0)); - this.fixedPoints.push(getVertexIndex(0, 19)); } + generateGeometry() { + const geometry = new THREE.BufferGeometry(); - /** - * Generate THREE JS Geometry - * (list of vertices and list of indices representing triangles) - * and calculate the weight of each face and split it between - * surrounding vertices - * @param {Array} vertices - * @param {Array} faces - */ - createExplicit(vertices, faces) { + const vertices = []; + const normals = []; + const indices = []; - /** - * Copy vertices and initialize vertex weights to 0 - */ - for (let i in vertices) { - this.geometry.vertices.push(vertices[i].clone()); - this.previousPositions.push(vertices[i].clone()); - // this.geometry.vertices.push(vertices[i]); - // this.previousPositions.push(vertices[i]); - this.vertexWeights.push(0); - this.vertexRigidness.push(false); - this.externalForces.push(new THREE.Vector3(0,0,0)); + for (let particle of this.particles) { + vertices.push( + particle.position.x, + particle.position.y, + particle.position.z); } - /** - * copy faces, - * generate two triangles per face, - * calculate weight of face as its area - * and split between the 4 vertices - */ - for (let i in faces) { - let face = faces[i]; - /** copy faces to class member */ - this.faces.push(face); + const numPointsWidth = this.numPointsWidth; + const numPointsHeight = this.numPointsHeight; - /** generate triangles */ - this.geometry.faces.push(new THREE.Face3( - face.a, face.b, face.c - )); - this.geometry.faces.push(new THREE.Face3( - face.c, face.b, face.d - )); - - /** - * calculate area of face as combined area of - * its two composing triangles - */ - let xLength = vectorLength(this.geometry.vertices[face.b], this.geometry.vertices[face.a]); - let yLength = vectorLength(this.geometry.vertices[face.c], this.geometry.vertices[face.a]); - let weight = xLength * yLength / 2; - - xLength = vectorLength(this.geometry.vertices[face.b], this.geometry.vertices[face.d]); - yLength = vectorLength(this.geometry.vertices[face.c], this.geometry.vertices[face.d]); - weight += xLength * yLength / 2; - - weight *= 10; - - /** - * split weight equally between four surrounding vertices - */ - this.vertexWeights[face.a] += weight / 4; - this.vertexWeights[face.b] += weight / 4; - this.vertexWeights[face.c] += weight / 4; - this.vertexWeights[face.d] += weight / 4; - } - - /** - * let THREE JS compute bounding sphere around generated mesh - * needed for View Frustum Culling internally - */ - this.geometry.computeBoundingSphere(); - this.geometry.computeFaceNormals(); - this.geometry.computeVertexNormals(); - } - - /** - * generate a debug mesh for visualizing - * vertices and springs of the cloth - * and add it to scene for rendering - * @param {Scene} scene - Scene to add Debug Mesh to - */ - createDebugMesh(scene) { - /** - * helper function to generate a single line - * between two Vertices with a given color - * @param {Vector3} from - * @param {Vector3} to - * @param {number} color - */ - function addLine(from, to, color) { - let geometry = new THREE.Geometry(); - geometry.vertices.push(from); - geometry.vertices.push(to); - let material = new THREE.LineBasicMaterial({ color: color, linewidth: 10 }); - let line = new THREE.Line(geometry, material); - line.renderOrder = 1; - scene.add(line); - } /** - * helper function to generate a small sphere - * at a given Vertex Position with color - * @param {Vector3} point - * @param {number} color + * generate faces based on 4 vertices + * and 6 springs each */ - function addPoint(point, color) { - const geometry = new THREE.SphereGeometry(0.05, 32, 32); - const material = new THREE.MeshBasicMaterial({ color: color }); - const sphere = new THREE.Mesh(geometry, material); - sphere.position.set(point.x, point.y, point.z); - scene.add(sphere); + for (let y = 0; y < numPointsHeight - 1; y++) { + for (let x = 0; x < numPointsWidth - 1; x++) { + indices.push( + this.getVertexIndex(x, y), + this.getVertexIndex(x+1, y), + this.getVertexIndex(x+1, y+1) + ); + indices.push( + this.getVertexIndex(x, y), + this.getVertexIndex(x+1, y+1), + this.getVertexIndex(x, y+1) + ); + } } - let lineColor = 0x000000; - let pointColor = 0xff00000; - - /** - * generate one line for each of the 6 springs - * and one point for each of the 4 vertices - * for all of the faces - */ - for (let i in this.faces) { - let face = this.faces[i]; - addLine(this.geometry.vertices[face.a], this.geometry.vertices[face.b], lineColor); - addLine(this.geometry.vertices[face.a], this.geometry.vertices[face.c], lineColor); - addLine(this.geometry.vertices[face.a], this.geometry.vertices[face.d], lineColor); - addLine(this.geometry.vertices[face.b], this.geometry.vertices[face.c], lineColor); - addLine(this.geometry.vertices[face.b], this.geometry.vertices[face.d], lineColor); - addLine(this.geometry.vertices[face.c], this.geometry.vertices[face.d], lineColor); - - addPoint(this.geometry.vertices[face.a], pointColor); - addPoint(this.geometry.vertices[face.b], pointColor); - addPoint(this.geometry.vertices[face.c], pointColor); - addPoint(this.geometry.vertices[face.d], pointColor); + geometry.setIndex(indices); + geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)); + //geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3)); + geometry.computeBoundingSphere(); + geometry.computeVertexNormals(); + + return geometry; + } + updateGeometry(geometry) { + const positions = geometry.attributes.position.array; + for (let i in this.particles) { + let p = this.particles[i]; + positions[i*3+0] = p.position.x; + positions[i*3+1] = p.position.y; + positions[i*3+2] = p.position.z; } + geometry.attributes.position.needsUpdate = true; + geometry.computeBoundingSphere(); + geometry.computeVertexNormals(); } - - previousPositions = []; - time = 0; - /** - * - * @param {number} dt time in seconds since last frame - */ simulate(dt) { - for (let i in this.geometry.vertices) { - let acceleration = this.getAcceleration(i, dt); - - //acceleration.clampLength(0, 10); - - if (Math.abs(acceleration.length()) <= 10e-4) { - acceleration.set(0, 0, 0); - } - - let currentPosition = this.verlet(this.geometry.vertices[i].clone(), this.previousPositions[i].clone(), acceleration, dt); - //let currentPosition = this.euler(this.geometry.vertices[i], acceleration, dt); - - this.previousPositions[i].copy(this.geometry.vertices[i]); - this.geometry.vertices[i].copy(currentPosition); - } - - this.checkIntersect(); - - this.time += dt; - - for (let face of this.faces) { - for (let spring of face.springs) { - spring.update(this.geometry.vertices); - } + let now = performance.now(); + for (let particle of this.particles) { + let vertex = particle.position; + let fWind = new THREE.Vector3( + this.windFactor.x * (Math.sin(vertex.x * vertex.y * now)+1), + this.windFactor.y * Math.cos(vertex.z * now), + this.windFactor.z * Math.sin(Math.cos(5 * vertex.x * vertex.y * vertex.z)) + ); + // normalize then multiply? + if (options.wind) + particle.addForce(fWind); + // calculate wind with normal? + + particle.addForce(GRAVITY); + + particle.verlet(dt); } - /** - * let THREE JS compute bounding sphere around generated mesh - * needed for View Frustum Culling internally - */ - - this.geometry.verticesNeedUpdate = true; - this.geometry.elementsNeedUpdate = true; - this.geometry.computeBoundingSphere(); - this.geometry.computeFaceNormals(); - this.geometry.computeVertexNormals(); - - } - -checkIntersect() { - let npw = this.numPointsWidth; - function getX(i, ) { return i % npw; } - function getY(i) { return Math.floor(i / npw); } - for (let i in this.geometry.vertices) { - for (let j in this.geometry.vertices) { - this.vertexRigidness[i] = false; - this.vertexRigidness[j] = false; - if (i == j || (Math.abs(getX(i) - getX(j)) == 1 && Math.abs(getY(i) - getY(j)) == 1)) - continue; - let posI = this.geometry.vertices[i]; - let posJ = this.geometry.vertices[j]; - let dist = posI.distanceTo(posJ); - const collisionDistance = Math.min(this.width / this.numPointsWidth, this.height / this.numPointsHeight); - if (dist < collisionDistance) { - this.vertexRigidness[i] = true; - this.vertexRigidness[j] = true; - let diff = this.geometry.vertices[i].clone().sub(this.geometry.vertices[j]).normalize().multiplyScalar((collisionDistance - dist) * 1.001 / 2); - if (!(this.fixedPoints.includes(i) || this.fixedPoints.includes(j))) { - this.geometry.vertices[i].add(diff); - this.geometry.vertices[j].sub(diff); - } - } + + for (let constraint of this.constraints) { + constraint.satisfy(); } + //console.log(tmpCorrection); } -} - -/** - * Equation of motion for each vertex which represents the acceleration - * @param {number} vertexIndex The index of the current vertex whose acceleration should be calculated - * @param {number} dt The time passed since last frame - */ -getAcceleration(vertexIndex, dt) { - if (this.fixedPoints.includes(parseInt(vertexIndex)) || - this.vertexRigidness[vertexIndex]) { - return new THREE.Vector3(0, 0, 0); - } - - let externalForce = this.externalForces[vertexIndex]; - let vertex = this.geometry.vertices[vertexIndex];//.add(externalForce); - - // Mass of vertex - let M = this.vertexWeights[vertexIndex]; - // constant gravity - let g = new THREE.Vector3(0, -9.8, 0); - // stiffness - let k = 1000; - - // Wind vector - let fWind = new THREE.Vector3( - this.windFactor.x * (Math.sin(vertex.x * vertex.y * this.time)+1), - this.windFactor.y * Math.cos(vertex.z * this.time), - this.windFactor.z * Math.sin(Math.cos(5 * vertex.x * vertex.y * vertex.z)) - ); - //console.log(fWind); - /** - * constant determined by the properties of the surrounding fluids (air) - * achievement of cloth effects through try out - * */ - let a = 0.1; - - let velocity = new THREE.Vector3( - (this.previousPositions[vertexIndex].x - vertex.x) / dt, - (this.previousPositions[vertexIndex].y - vertex.y) / dt, - (this.previousPositions[vertexIndex].z - vertex.z) / dt - ); - - //console.log(velocity, vertex, this.previousPositions[vertexIndex]); - - let fAirResistance = velocity.multiply(velocity).multiplyScalar(-a); - - let springSum = new THREE.Vector3(0, 0, 0); - - // Get the bounding springs and add them to the needed springs - // TODO: optimize - - const numPointsX = this.numPointsWidth; - const numPointsY = this.numPointsHeight; - const numFacesX = numPointsX - 1; - const numFacesY = numPointsY - 1; - - function getFaceIndex(x, y) { - return y * numFacesX + x; - } - - let indexX = vertexIndex % numPointsX; - let indexY = Math.floor(vertexIndex / numPointsX); - - let springs = []; - - // 0 oben - // 1 links - // 2 oben links -> unten rechts diagonal - // 3 oben rechts -> unten links diagonal - // 4 rechts - // 5 unten - - let ul = indexX > 0 && indexY < numPointsY - 1; - let ur = indexX < numPointsX - 1 && indexY < numPointsY - 1; - let ol = indexX > 0 && indexY > 0; - let or = indexX < numPointsX - 1 && indexY > 0; - - if (ul) { - let faceUL = this.faces[getFaceIndex(indexX - 1, indexY)]; - springs.push(faceUL.springs[3]); - if (!ol) - springs.push(faceUL.springs[0]); - springs.push(faceUL.springs[4]); - } - if (ur) { - let faceUR = this.faces[getFaceIndex(indexX, indexY)]; - springs.push(faceUR.springs[2]); - if (!or) - springs.push(faceUR.springs[0]); - if (!ul) - springs.push(faceUR.springs[1]); - } - if (ol) { - let faceOL = this.faces[getFaceIndex(indexX - 1, indexY - 1)]; - springs.push(faceOL.springs[2]); - springs.push(faceOL.springs[4]); - springs.push(faceOL.springs[5]); - } - if (or) { - let faceOR = this.faces[getFaceIndex(indexX , indexY - 1)]; - springs.push(faceOR.springs[3]); - if (!ol) - springs.push(faceOR.springs[1]); - springs.push(faceOR.springs[5]); - } - - for (let spring of springs) { - let springDirection = spring.getDirection(this.geometry.vertices); - - if (spring.index1 == vertexIndex) - springDirection.multiplyScalar(-1); - - springSum.add(springDirection.multiplyScalar(k * (spring.restLength - spring.currentLength))); - } - - let result = new THREE.Vector3(1, 1, 1); - result.multiplyScalar(M).multiply(g).add(fWind).add(externalForce).add(fAirResistance).sub(springSum); - - document.getElementById("Output").innerText = "SpringSum: " + Math.floor(springSum.y); - - let threshold = 1; - let forceReduktion = 0.8; - if(Math.abs(externalForce.z) > threshold){ - externalForce.z *= forceReduktion; - } else { - externalForce.z = 0; - } - - if(Math.abs(externalForce.y) > threshold){ - externalForce.y *= forceReduktion; - } else { - externalForce.y = 0; - } - - if(Math.abs(externalForce.x) > threshold){ - externalForce.x *= forceReduktion; - } else { - externalForce.x = 0; - } - - - - return result; -} - -/** - * The Verlet algorithm as an integrator - * to get the next position of a vertex - * @param {Vector3} currentPosition - * @param {Vector3} previousPosition - * @param {Vector3} acceleration - * @param {number} passedTime The delta time since last frame - */ -verlet(currentPosition, previousPosition, acceleration, passedTime) { - // verlet algorithm - // next position = 2 * current Position - previous position + acceleration * (passed time)^2 - // acceleration (dv/dt) = F(net) - // Dependency for one vertex: gravity, fluids/air, springs - const DRAG = 0.97; - let nextPosition = new THREE.Vector3( - (currentPosition.x - previousPosition.x) * DRAG + currentPosition.x + acceleration.x * (passedTime * passedTime), - (currentPosition.y - previousPosition.y) * DRAG + currentPosition.y + acceleration.y * (passedTime * passedTime), - (currentPosition.z - previousPosition.z) * DRAG + currentPosition.z + acceleration.z * (passedTime * passedTime), - ); - - // let nextPosition = new THREE.Vector3( - // (2 * currentPosition.x) - previousPosition.x + acceleration.x * (passedTime * passedTime), - // (2 * currentPosition.y) - previousPosition.y + acceleration.y * (passedTime * passedTime), - // (2 * currentPosition.z) - previousPosition.z + acceleration.z * (passedTime * passedTime), - // ); - - return nextPosition; -} - -euler(currentPosition, acceleration, passedTime) { - let nextPosition = new THREE.Vector3( - currentPosition.x + acceleration.x * passedTime, - currentPosition.y + acceleration.y * passedTime, - currentPosition.z + acceleration.z * passedTime, - ); - - return nextPosition; -} - -wind(intersects) { - let intersect = intersects[0]; - this.externalForces[intersect.face.a].z -= this.windForce; - this.externalForces[intersect.face.b].z -= this.windForce; - this.externalForces[intersect.face.c].z -= this.windForce; -} - -mousePressed = false; -mouseMoved = false; -intersects; - -mousePress(intersects){ - this.mousePressed = true; - this.intersects = intersects; - -} - -mouseMove(mousePos){ - this.mouseMoved = true; - if(this.mousePressed){ - let intersect = this.intersects[0]; - this.externalForces[intersect.face.a].add(mousePos.clone().sub(this.geometry.vertices[intersect.face.a]).multiplyScalar(90)); - /* - this.geometry.vertices[intersect.face.a].x = mousePos.x; - this.geometry.vertices[intersect.face.a].y = mousePos.y; - this.geometry.vertices[intersect.face.a].z = mousePos.z; - */ + * helper function to calculate index of vertex + * in "vertices" array based on its x and y positions + * in the mesh + * @param {number} x - x index of vertex + * @param {number} y - y index of vertex + */ + getVertexIndex(x, y) { + return y * this.numPointsWidth + x; } -} - -mouseRelease(){ - this.mousePressed = false; -} - -} - +} \ No newline at end of file