Best JavaScript code snippet using fast-check-monorepo
world-render.js
Source:world-render.js
1// Copyright 2011-2012 Kevin Reid under the terms of the MIT License as detailed2// in the accompanying file README.md or <http://opensource.org/licenses/MIT>.3(function () {4 "use strict";5 6 var AAB = cubes.util.AAB;7 var Blockset = cubes.Blockset;8 var CubeRotation = cubes.util.CubeRotation;9 var DirtyQueue = cubes.util.DirtyQueue;10 var IntVectorMap = cubes.util.IntVectorMap;11 var measuring = cubes.measuring;12 var mod = cubes.util.mod;13 var round = Math.round;14 var UNIT_PX = cubes.util.UNIT_PX;15 var UNIT_PY = cubes.util.UNIT_PY;16 var ZEROVEC = cubes.util.ZEROVEC;17 18 // The side length of the chunks the world is broken into for rendering.19 // Smaller chunks are faster to update when the world changes, but have a higher per-frame cost.20 var LIGHT_TEXTURE_SIZE = 16; // must be power of 221 var CHUNKSIZE = LIGHT_TEXTURE_SIZE - 2;22 23 // 3D Euclidean distance, squared (for efficiency).24 function dist3sq(v1, v2) {25 var x = v1[0] - v2[0];26 var y = v1[1] - v2[1];27 var z = v1[2] - v2[2];28 return x*x + y*y + z*z;29 }30 31 var angleStep = Math.PI/2;32 function discreteRotation(angle) {33 return Math.round(angle/angleStep) * angleStep;34 }35 var rotationsByCode = CubeRotation.byCode;36 var distanceInfoCache = {}, lastSeenRenderDistance = null;37 function renderDistanceInfo(newRenderDistance) {38 // TODO add some visibility into whether this one-element cache is thrashing39 if (newRenderDistance !== lastSeenRenderDistance) {40 //if (typeof console !== "undefined") console.log("Building renderDistanceInfo for " + newRenderDistance.toFixed(1));41 lastSeenRenderDistance = newRenderDistance;42 43 // The distance in chunk-lengths at which chunks are visible44 var chunkDistance = Math.ceil(newRenderDistance/CHUNKSIZE);45 46 // The squared distance at which chunks should be included.47 // The offset of CHUNKSIZE is to account for the origin of a chunk being at one corner.48 var boundSquared = Math.pow(newRenderDistance + CHUNKSIZE, 2);49 distanceInfoCache.addChunkDistanceSquared = boundSquared;50 // The distance at which invisible chunks are dropped from memory. Semi-arbitrary figure...51 distanceInfoCache.dropChunkDistanceSquared = Math.pow(newRenderDistance + 2*CHUNKSIZE, 2);52 // A static table of the offsets of the chunks visible from the player location53 var nearChunkOrder = [];54 for (var x = -chunkDistance-1; x <= chunkDistance; x++)55 for (var y = -chunkDistance-1; y <= chunkDistance; y++)56 for (var z = -chunkDistance-1; z <= chunkDistance; z++) {57 var v = [x*CHUNKSIZE,y*CHUNKSIZE,z*CHUNKSIZE];58 if (dist3sq(v, ZEROVEC) <= boundSquared) {59 nearChunkOrder.push(v);60 }61 }62 nearChunkOrder.sort(function (a,b) {63 return dist3sq(a, ZEROVEC) - dist3sq(b, ZEROVEC);64 });65 distanceInfoCache.nearChunkOrder = Object.freeze(nearChunkOrder);66 }67 return distanceInfoCache;68 }69 70 function WorldRenderer(world, getViewPosition, renderer, optAudio, scheduleDraw, showBoundaries) {71 var gl = renderer.context;72 var config = renderer.config; // TODO eliminate need for this73 74 // World properties cached and used by chunk calculation75 var wx = world.wx;76 var wy = world.wy;77 var wz = world.wz;78 var g = world.g;79 var rawBlocks = world.raw;80 var rawRotations = world.rawRotations;81 var rawLighting = world.rawLighting;82 var inBounds = world.inBounds;83 var lightOutside = world.lightOutside;84 85 // Table of all world rendering chunks which have RenderBundles created, indexed by [x,y,z] of the low-side coordinates (i.e. divisible by CHUNKSIZE).86 var chunks = new IntVectorMap();87 88 var nonemptyChunks = new IntVectorMap();89 function compareByPlayerDistance(a,b) {90 return dist3sq(a, playerChunk) - dist3sq(b, playerChunk);91 }92 // Queue of chunks to rerender. Array (first-to-do at the end); each element is [x,z] where x and z are the low coordinates of the chunk.93 var dirtyChunks = new DirtyQueue(compareByPlayerDistance);94 // Queue of chunks to render for the first time. Distinguished from dirtyChunks in that it can be flushed if the view changes.95 var addChunks = new DirtyQueue(compareByPlayerDistance);96 97 // The origin of the chunk which the player is currently in. Changes to this are used to decide to recompute chunk visibility.98 var playerChunk = null;99 100 // Like chunks, but for circuits. Indexed by the circuit origin block.101 var circuitRenderers = new IntVectorMap();102 var blockset = world.blockset;103 104 // Cached blockset characteristics105 var tileSize = blockset.tileSize;106 var ID_EMPTY = Blockset.ID_EMPTY;107 108 var particles = [];109 110 var boundaryR = new renderer.RenderBundle(gl.LINES, null, function (vertices, normals, colors) {111 function common() {112 normals.push(0, 0, 0);113 normals.push(0, 0, 0);114 colors.push(0.5,0.5,0.5, 1);115 colors.push(0.5,0.5,0.5, 1);116 }117 var extent = 20;118 119 var vec = [];120 for (var dim = 0; dim < 3; dim++) {121 var ud = mod(dim+1,3);122 var vd = mod(dim+2,3);123 for (var u = 0; u < 2; u++)124 for (var v = 0; v < 2; v++) {125 vec[ud] = [world.wx, world.wy, world.wz][ud]*u;126 vec[vd] = [world.wx, world.wy, world.wz][vd]*v;127 vec[dim] = -extent;128 vertices.push(vec[0],vec[1],vec[2]);129 vec[dim] = [world.wx, world.wy, world.wz][dim] + extent;130 vertices.push(vec[0],vec[1],vec[2]);131 common();132 }133 }134 }, {aroundDraw: function (draw) {135 renderer.setLineWidth(1);136 draw();137 }});138 var textureDebugR = new renderer.RenderBundle(139 gl.TRIANGLE_STRIP,140 function () { return blockset.getRenderData(renderer).texture; },141 function (vertices, normals, texcoords) {142 var x = 1;143 var y = 1;144 var z = 0;145 vertices.push(-x, -y, z);146 vertices.push(x, -y, z);147 vertices.push(-x, y, z);148 vertices.push(x, y, z);149 150 normals.push(0,0,0);151 normals.push(0,0,0);152 normals.push(0,0,0);153 normals.push(0,0,0);154 texcoords.push(0, 1);155 texcoords.push(1, 1);156 texcoords.push(0, 0);157 texcoords.push(1, 0);158 }, {159 aroundDraw: function (draw) {160 var restoreView = renderer.saveView();161 renderer.setViewTo2D();162 gl.disable(gl.DEPTH_TEST); // TODO should be handled by renderer?163 gl.depthMask(false);164 draw();165 restoreView();166 gl.enable(gl.DEPTH_TEST);167 gl.depthMask(true);168 }169 });170 171 // --- methods, internals ---172 173 function deleteChunks() {174 chunks.forEachValue(function (chunk) {175 chunk.deleteResources();176 });177 circuitRenderers.forEachValue(function (cr) {178 cr.deleteResources();179 });180 181 chunks = new IntVectorMap();182 nonemptyChunks = new IntVectorMap();183 circuitRenderers = new IntVectorMap();184 dirtyChunks.clear();185 addChunks.clear();186 }187 188 function rerenderChunks() {189 dirtyChunks.clear();190 chunks.forEach(function (chunk, coords) {191 chunk.dirtyGeometry = true;192 dirtyChunks.enqueue(coords);193 });194 }195 196 var listenerBlockset = {197 interest: isAlive,198 // TODO: Optimize by rerendering only if render data (not just texture image) changed, and only199 // chunks containing the changed block ID?200 texturingChanged: dirtyAll,201 tableChanged: dirtyAll202 };203 204 var listenerRenderDistance = {205 interest: isAlive,206 changed: function (v) {207 playerChunk = null; // TODO kludge. The effect of this is to reevaluate which chunks are visible208 addChunks.clear();209 scheduleDraw();210 }211 };212 var listenerRedraw = {213 interest: isAlive,214 changed: function (v) {215 scheduleDraw();216 }217 };218 function deleteResources() {219 deleteChunks();220 textureDebugR.deleteResources();221 world.listen.cancel(listenerWorld);222 blockset.listen.cancel(listenerBlockset);223 config.renderDistance.listen.cancel(listenerRenderDistance);224 subWRs.forEach(function (record) {225 record.wr.deleteResources();226 });227 world = blockset = chunks = nonemptyChunks = dirtyChunks = addChunks = textureDebugR = null;228 }229 function isAlive() {230 // Are we still interested in notifications etc?231 return !!world;232 }233 this.deleteResources = deleteResources;234 function chunkIntersectsWorld(chunkOrigin) {235 var x = chunkOrigin[0];236 var y = chunkOrigin[1];237 var z = chunkOrigin[2];238 return x >= 0 && x - CHUNKSIZE < world.wx &&239 y >= 0 && y - CHUNKSIZE < world.wy &&240 z >= 0 && z - CHUNKSIZE < world.wz;241 }242 243 function chunkContaining(cube) {244 var x = cube[0];245 var y = cube[1];246 var z = cube[2];247 return chunks.get([248 x - mod(x, CHUNKSIZE),249 y - mod(y, CHUNKSIZE),250 z - mod(z, CHUNKSIZE)251 ]);252 }253 254 // x,z must be multiples of CHUNKSIZE255 function setDirtyChunk(x, y, z, dirtyType) {256 var k = [x, y, z];257 if (!chunkIntersectsWorld(k)) return;258 var chunk = chunks.get(k);259 if (chunk) {260 // This routine is used only for "this block changed", so if there is261 // not already a chunk, we don't create it.262 chunk[dirtyType] = true;263 dirtyChunks.enqueue(k);264 }265 }266 267 function dirtyChunksForBlock(cube, dirtyType) {268 if (!isAlive()) return;269 270 var x = cube[0];271 var y = cube[1];272 var z = cube[2];273 274 var xm = mod(x, CHUNKSIZE);275 var ym = mod(y, CHUNKSIZE);276 var zm = mod(z, CHUNKSIZE);277 x -= xm;278 y -= ym;279 z -= zm;280 281 setDirtyChunk(x,y,z, dirtyType);282 if (xm === 0) setDirtyChunk(x-CHUNKSIZE,y,z, dirtyType);283 if (ym === 0) setDirtyChunk(x,y-CHUNKSIZE,z, dirtyType);284 if (zm === 0) setDirtyChunk(x,y,z-CHUNKSIZE, dirtyType);285 if (xm === CHUNKSIZE-1) setDirtyChunk(x+CHUNKSIZE,y,z, dirtyType);286 if (ym === CHUNKSIZE-1) setDirtyChunk(x,y+CHUNKSIZE,z, dirtyType);287 if (zm === CHUNKSIZE-1) setDirtyChunk(x,y,z+CHUNKSIZE, dirtyType);288 289 // TODO: This is actually "Schedule updateSomeChunks()" and shouldn't actually require a frame redraw unless the update does something in view290 scheduleDraw();291 }292 // entry points for change listeners293 function dirtyBlock(cube) {294 dirtyChunksForBlock(cube, "dirtyGeometry");295 }296 function relitBlock(cube) {297 // TODO: Do no light-texture work if lighting is disabled in config â but catch up when it is reenabled.298 dirtyChunksForBlock(cube, "dirtyLighting");299 }300 function dirtyAll() {301 if (!isAlive()) return;302 blockset = world.blockset;303 rerenderChunks();304 }305 function dirtyCircuit(circuit) {306 if (!isAlive()) return;307 var o = circuit.getOrigin();308 var r = circuitRenderers.get(o);309 if (r) {310 r.recompute();311 } else {312 addCircuits();313 }314 }315 316 function deletedCircuit(circuit) {317 if (!isAlive()) return;318 circuitRenderers.delete(circuit.getOrigin());319 }320 var listenerWorld = {321 interest: isAlive,322 dirtyBlock: dirtyBlock,323 relitBlock: relitBlock,324 bodiesChanged: scheduleDraw,325 dirtyAll: dirtyAll,326 dirtyCircuit: dirtyCircuit,327 deletedCircuit: deletedCircuit,328 changedBlockset: dirtyAll,329 transientEvent: function (cube, type, kind) {330 if (!isAlive()) return;331 332 if (optAudio) {333 optAudio.play(vec3.add([0.5,0.5,0.5], cube), type, kind, 1);334 }335 336 switch (kind) {337 case "destroy":338 addParticles(cube, true);339 break;340 case "create":341 addParticles(cube, false);342 break;343 }344 }345 };346 347 function addCircuits() {348 // Add circuits which are in viewing distance.349 // Note: This enumerates every circuit in the world. Currently, this is more efficient than the alternatives because there are not many circuits in typical data. When that changes, we should revisit this and use some type of spatial index to make it efficient. Testing per-block is *not* efficient.350 if (!playerChunk) return;351 var renderDistance = config.renderDistance.get();352 var rdi = renderDistanceInfo(renderDistance);353 world.getCircuits().forEach(function (circuit, origin) {354 if (dist3sq(origin, playerChunk) < rdi.addChunkDistanceSquared) {355 if (!circuitRenderers.get(origin)) {356 circuitRenderers.set(origin, makeCircuitRenderer(circuit));357 }358 }359 });360 }361 362 function updateSomeChunks() {363 // Determine if chunks' visibility to the player has changed364 var rdi = renderDistanceInfo(config.renderDistance.get());365 var pos = getViewPosition();366 var newPlayerChunk = [round(pos[0] - mod(pos[0], CHUNKSIZE)),367 round(pos[1] - mod(pos[1], CHUNKSIZE)),368 round(pos[2] - mod(pos[2], CHUNKSIZE))];369 if (playerChunk === null || newPlayerChunk[0] !== playerChunk[0]370 || newPlayerChunk[1] !== playerChunk[1]371 || newPlayerChunk[2] !== playerChunk[2]) {372 //console.log("nPC ", newPlayerChunk[0], newPlayerChunk[1], newPlayerChunk[2]);373 374 playerChunk = newPlayerChunk;375 376 // Add chunks which are in viewing distance.377 rdi.nearChunkOrder.forEach(function (offset) {378 var chunkKey = [playerChunk[0] + offset[0], playerChunk[1] + offset[1], playerChunk[2] + offset[2]];379 if (!chunks.has(chunkKey) && chunkIntersectsWorld(chunkKey)) {380 addChunks.enqueue(chunkKey, chunks.get(chunkKey));381 }382 });383 // Drop now-invisible chunks. Has a higher boundary so that we're not constantly reloading chunks if the player is moving back and forth.384 var dds = rdi.dropChunkDistanceSquared;385 chunks.forEach(function (chunk, chunkKey) {386 if (dist3sq(chunkKey, playerChunk) > dds) {387 chunk.deleteResources();388 chunks.delete(chunkKey);389 nonemptyChunks.delete(chunkKey);390 }391 });392 393 addCircuits();394 395 // Drop now-invisible circuits396 // TODO: This works off the origin, but circuits can be arbitrarily large so we should test against their AABB397 circuitRenderers.forEach(function (cr, cube) {398 if (dist3sq(cube, playerChunk) > dds) {399 cr.deleteResources();400 circuitRenderers.delete(cube);401 }402 });403 }404 405 // Update chunks from the queues.406 var deadline = Date.now() + (addChunks.size() > 30 ? 30 : 10);407 var count = 0;408 // Chunks to add409 while (addChunks.size() > 0 && Date.now() < deadline) {410 count += calcChunk(addChunks.dequeue());411 }412 // Dirty chunks (only if visible)413 while (dirtyChunks.size() > 0 && Date.now() < deadline) {414 var chunkKey = dirtyChunks.dequeue();415 if (chunks.has(chunkKey)) {416 count += calcChunk(chunkKey);417 }418 }419 measuring.chunkCount.inc(count);420 421 if (addChunks.size() > 0 || dirtyChunks.size() > 0) {422 // Schedule rendering more chunks423 scheduleDraw();424 }425 426 subWRs.forEach(function (record) {427 record.wr.updateSomeChunks();428 });429 }430 this.updateSomeChunks = updateSomeChunks;431 432 function addParticles(cube, mode) {433 var chunk = chunkContaining(cube);434 var lightTexture = chunk ? chunk.getLightTexture() : null;435 436 particles.push(new renderer.BlockParticles(437 cube,438 tileSize,439 world.gtv(cube),440 mode,441 world.gRotv(cube),442 lightTexture));443 }444 445 446 447 function draw() {448 // Draw chunks.449 renderer.setTileSize(blockset.tileSize);450 nonemptyChunks.forEachValue(function (chunk) {451 if (renderer.aabbInView(chunk.aabb))452 chunk.draw();453 });454 455 // Draw circuits.456 circuitRenderers.forEachValue(function (cr) {457 cr.draw();458 });459 460 461 // Draw particles.462 for (var i = 0; i < particles.length; i++) {463 var particleSystem = particles[i];464 if (particleSystem.expired()) {465 if (i < particles.length - 1) {466 particles[i] = particles.pop();467 } else {468 particles.pop();469 }470 i--;471 } else {472 particleSystem.draw();473 }474 }475 if (particles.length > 0) {476 // If there are any particle systems, we need to continue animating.477 scheduleDraw();478 }479 480 if (showBoundaries) {481 boundaryR.draw();482 }483 // Draw texture debug.484 if (config.debugTextureAllocation.get()) {485 textureDebugR.draw();486 }487 488 // Draw subworlds.489 subWRs.forEach(function (record) {490 var body = record.body;491 var bodyWorld = body.skin;492 var wr = record.wr;493 494 var restoreView = renderer.saveView();495 renderer.modifyModelview(function (m) {496 mat4.translate(m, body.pos);497 498 // Translate to body AABB center499 var aabb = body.aabb;500 mat4.translate(m, [501 (aabb[1] + aabb[0]) * 0.5,502 (aabb[3] + aabb[2]) * 0.5,503 (aabb[5] + aabb[4]) * 0.5504 ]);505 506 // Apply rotation507 mat4.rotateY(m, discreteRotation(body.yaw));508 509 // Translate to center world about AABB center510 mat4.translate(m, [bodyWorld.wx * -0.5, bodyWorld.wy * -0.5, bodyWorld.wz * -0.5]);511 });512 wr.draw();513 restoreView();514 });515 }516 this.draw = draw;517 518 var renderData; // updated as needed by chunk recalculate519 520 // return value is for measuring only521 function calcChunk(chunkKey) {522 var work = 0;523 var c = chunks.get(chunkKey);524 if (c) {525 if (c.dirtyGeometry) {526 c.dirtyGeometry = false;527 c.recompute();528 work = 1;529 }530 if (c.dirtyLighting) {531 c.dirtyLighting = false;532 c.recomputeLight();533 work = 1;534 }535 } else {536 chunks.set(chunkKey, makeChunk(chunkKey));537 work = 1;538 }539 return work;540 }541 542 function makeChunk(chunkKey) {543 var chunkOriginX = chunkKey[0];544 var chunkOriginY = chunkKey[1];545 var chunkOriginZ = chunkKey[2];546 var chunkLimitX = Math.min(wx, chunkOriginX + CHUNKSIZE);547 var chunkLimitY = Math.min(wy, chunkOriginY + CHUNKSIZE);548 var chunkLimitZ = Math.min(wz, chunkOriginZ + CHUNKSIZE);549 var nonempty = false;550 var lightTexture;551 var mustRebuild = function () { return true; };552 var ltData = new Uint8Array(LIGHT_TEXTURE_SIZE*LIGHT_TEXTURE_SIZE*LIGHT_TEXTURE_SIZE);553 554 var chunk = new renderer.RenderBundle(gl.TRIANGLES,555 function () { return renderData.texture; },556 function (vertices, normals, texcoords) {557 measuring.chunk.start();558 renderData = blockset.getRenderData(renderer);559 var rotatedBlockFaceData = renderData.rotatedBlockFaceData;560 var BOGUS_BLOCK_DATA = rotatedBlockFaceData.bogus;561 var types = renderData.types;562 var opaques = types.map(function (t) { return t.opaque; });563 564 // these variables are used by face() and written by the loop565 var x,y,z;566 var thisOpaque;567 var rawIndex;568 569 function face(vFacing, data) {570 var fx = vFacing[0]; var xfx = x + fx;571 var fy = vFacing[1]; var yfy = y + fy;572 var fz = vFacing[2]; var zfz = z + fz;573 // TODO between the g() and the inBounds() we're testing the neighbor twice574 if (thisOpaque && opaques[g(xfx,yfy,zfz)]) {575 // this face is invisible576 return;577 } else {578 var faceVertices = data.vertices;579 var faceTexcoords = data.texcoords;580 var vl = faceVertices.length / 3;581 for (var i = 0; i < vl; i++) {582 var vi = i*3;583 var ti = i*2;584 vertices.push(faceVertices[vi ]+x,585 faceVertices[vi+1]+y,586 faceVertices[vi+2]+z);587 texcoords.push(faceTexcoords[ti], faceTexcoords[ti+1]);588 normals.push(fx, fy, fz);589 }590 }591 }592 593 for (x = chunkOriginX; x < chunkLimitX; x++)594 for (y = chunkOriginY; y < chunkLimitY; y++)595 for (z = chunkOriginZ; z < chunkLimitZ; z++) {596 // raw array access inlined and simplified for efficiency597 rawIndex = (x*wy+y)*wz+z;598 var value = rawBlocks[rawIndex];599 if (value === ID_EMPTY) continue;600 601 var rotIndex = rawRotations[rawIndex];602 var rot = rotationsByCode[rotIndex];603 var faceData = (rotatedBlockFaceData[value] || BOGUS_BLOCK_DATA)[rotIndex];604 thisOpaque = opaques[value];605 606 face(rot.nx, faceData.lx);607 face(rot.ny, faceData.ly);608 face(rot.nz, faceData.lz);609 face(rot.px, faceData.hx);610 face(rot.py, faceData.hy);611 face(rot.pz, faceData.hz);612 }613 614 var wasNonempty = nonempty;615 nonempty = vertices.length > 0;616 if (nonempty !== wasNonempty) {617 if (nonempty) {618 nonemptyChunks.set(chunkKey, chunk);619 } else {620 nonemptyChunks.delete(chunkKey);621 }622 }623 624 measuring.chunk.end();625 }, {626 aroundDraw: function (draw) {627 if (mustRebuild()) sendLightTexture();628 renderer.setLightTexture(lightTexture);629 draw();630 }631 });632 633 function copyLightTexture() {634 // Repack data635 for (var x = chunkOriginX - 1; x <= chunkLimitX; x++)636 for (var y = chunkOriginY - 1; y <= chunkLimitY; y++)637 for (var z = chunkOriginZ - 1; z <= chunkLimitZ; z++) {638 var ltIndex = ( ( mod(x, LIGHT_TEXTURE_SIZE) *LIGHT_TEXTURE_SIZE639 + mod(y, LIGHT_TEXTURE_SIZE))*LIGHT_TEXTURE_SIZE640 + mod(z, LIGHT_TEXTURE_SIZE));641 var rawIndex = (x*wy+y)*wz+z;642 ltData[ltIndex] = inBounds(x,y,z) ? rawLighting[rawIndex] : lightOutside;643 }644 645 sendLightTexture();646 }647 648 function sendLightTexture() {649 if (mustRebuild()) {650 lightTexture = gl.createTexture();651 mustRebuild = renderer.currentContextTicket();652 gl.bindTexture(gl.TEXTURE_2D, lightTexture);653 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);654 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);655 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);656 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);657 } else {658 gl.bindTexture(gl.TEXTURE_2D, lightTexture);659 }660 gl.texImage2D(gl.TEXTURE_2D,661 0, // level662 gl.LUMINANCE, // internalformat663 LIGHT_TEXTURE_SIZE, // width664 LIGHT_TEXTURE_SIZE*LIGHT_TEXTURE_SIZE, // height665 0, // border666 gl.LUMINANCE, // format667 gl.UNSIGNED_BYTE, // type668 ltData);669 gl.bindTexture(gl.TEXTURE_2D, null);670 }671 672 // This is needed because when the calc function is first called by constructing the RenderBundle, 'chunk' has not yet been assigned.673 if (nonempty)674 nonemptyChunks.set(chunkKey, chunk);675 676 chunk.aabb = new AAB(677 chunkOriginX, chunkLimitX,678 chunkOriginY, chunkLimitY,679 chunkOriginZ, chunkLimitZ680 );681 682 chunk.recomputeLight = copyLightTexture;683 684 chunk.getLightTexture = function () {685 if (mustRebuild()) sendLightTexture();686 return lightTexture;687 };688 689 copyLightTexture();690 691 return chunk;692 }693 694 var CYL_RESOLUTION = 9;695 function calcCylinder(pt1, pt2, radius, vertices, normals) {696 function pushVertex(vec) {697 vertices.push(vec[0], vec[1], vec[2]);698 normals.push(0,0,0);699 }700 //function pushNormal(vec) {701 // normals.push(vec[0], vec[1], vec[2]);702 //}703 704 var length = vec3.subtract(pt2, pt1, vec3.create());705 var perp1 = vec3.cross(length, length[1] ? UNIT_PX : UNIT_PY, vec3.create());706 var perp2 = vec3.cross(perp1, length, vec3.create());707 vec3.normalize(perp1);708 vec3.normalize(perp2);709 function incr(i, r) {710 return vec3.add(711 vec3.scale(perp1, Math.sin(i/10*Math.PI*2), vec3.create()),712 vec3.scale(perp2, Math.cos(i/10*Math.PI*2), vec3.create()));713 }714 for (var i = 0; i < CYL_RESOLUTION; i++) {715 var p1 = incr(i);716 var p2 = incr(mod(i+1, CYL_RESOLUTION));717 //pushNormal(p2);718 //pushNormal(p2);719 //pushNormal(p1);720 //pushNormal(p1);721 //pushNormal(p1);722 //pushNormal(p2);723 vec3.scale(p1, radius);724 vec3.scale(p2, radius);725 var v0 = vec3.add(pt1, p2, vec3.create());726 var v1 = vec3.add(pt2, p2, vec3.create());727 var v2 = vec3.add(pt2, p1, vec3.create());728 var v3 = vec3.add(pt1, p1, vec3.create());729 pushVertex(v0);730 pushVertex(v1);731 pushVertex(v2);732 pushVertex(v2);733 pushVertex(v3);734 pushVertex(v0);735 }736 return 6*CYL_RESOLUTION;737 }738 739 var CENTER = [0.5, 0.5, 0.5];740 var beamRadius = round(0.08 * tileSize) / tileSize;741 function makeCircuitRenderer(circuit) {742 var dyns;743 var circuitRenderer = new renderer.RenderBundle(gl.TRIANGLES, null, function (vertices, normals, colors) {744 dyns = [];745 circuit.getEdges().forEach(function (record) {746 var net = record[0];747 var fromBlock = record[1];748 var block = record[2];749 750 var cbase = colors.length;751 var numVertices = calcCylinder(752 vec3.add(fromBlock, CENTER, vec3.create()),753 vec3.add(block, CENTER, vec3.create()),754 beamRadius,755 vertices, normals);756 for (var i = 0; i < numVertices; i++)757 colors.push(1,1,1,1); 758 759 dyns.push(function () {760 var carr = circuitRenderer.colors.array;761 var value = circuit.getNetValue(net);762 var color;763 var alpha = 0.5;764 if (value === null || value === undefined) {765 color = [0,0,0,alpha];766 } else if (value === false) {767 color = [0,0,0.2,alpha];768 } else if (value === true) {769 color = [0.2,0.2,1,alpha];770 } else if (typeof value === 'number') {771 // TODO: represent negatives too772 if (value <= 1)773 color = [value, 0, 0, alpha];774 else775 color = [1, 1 - (1/value), 0, alpha];776 } else {777 color = [1,1,1,alpha];778 }779 for (var i = 0, p = cbase; i < numVertices; i++) {780 carr[p++] = color[0];781 carr[p++] = color[1];782 carr[p++] = color[2];783 carr[p++] = color[3];784 }785 });786 });787 }, {788 aroundDraw: function (baseDraw) {789 dyns.forEach(function (f) { f(); });790 circuitRenderer.colors.send(gl.DYNAMIC_DRAW);791 baseDraw();792 }793 });794 795 return circuitRenderer;796 }797 798 // For info/debug displays799 function chunkRendersToDo() {800 return dirtyChunks.size() + addChunks.size();801 }802 this.chunkRendersToDo = chunkRendersToDo;803 804 // --- bodies in the world ---805 806 var subWRs = [];807 world.forEachBody(function (body) {808 if (!body.skin) return;809 subWRs.push({810 body: body, 811 wr: new WorldRenderer(body.skin, function () { return [0,0,0]; }, renderer, optAudio, scheduleDraw, false)812 });813 });814 815 // --- init ---816 817 world.listen(listenerWorld);818 blockset.listen(listenerBlockset);819 config.renderDistance.listen(listenerRenderDistance);820 config.debugTextureAllocation.listen(listenerRedraw);821 822 Object.freeze(this);823 }824 825 WorldRenderer.LIGHT_TEXTURE_SIZE = LIGHT_TEXTURE_SIZE; // exposed for shader826 cubes.WorldRenderer = Object.freeze(WorldRenderer);...
timeSeriesSepalExport.js
Source:timeSeriesSepalExport.js
1const {toFeatureCollection} = require('sepal/ee/aoi')2const {hasImagery: hasOpticalImagery} = require('sepal/ee/optical/collection')3const {hasImagery: hasRadarImagery} = require('sepal/ee/radar/collection')4const {hasImagery: hasPlanetImagery} = require('sepal/ee/planet/collection')5const tile = require('sepal/ee/tile')6const {exportImageToSepal$} = require('../jobs/export/toSepal')7const {mkdirSafe$} = require('task/rxjs/fileSystem')8const {concat, forkJoin, from, of, map, mergeMap, scan, switchMap, tap} = require('rxjs')9const {swallow} = require('sepal/rxjs')10const Path = require('path')11const {terminal$} = require('sepal/terminal')12const {sequence} = require('sepal/utils/array')13const moment = require('moment')14const ee = require('sepal/ee')15const {getCurrentContext$} = require('task/jobs/service/context')16const {getCollection$} = require('sepal/ee/timeSeries/collection')17const _ = require('lodash')18const log = require('sepal/log').getLogger('task')19const DATE_DELTA = 320const DATE_DELTA_UNIT = 'months'21module.exports = {22 submit$: (_id, {workspacePath, description, ...retrieveOptions}) =>23 getCurrentContext$().pipe(24 switchMap(({config}) => {25 const preferredDownloadDir = workspacePath26 ? `${config.homeDir}/${workspacePath}/`27 : `${config.homeDir}/downloads/${description}/`28 return mkdirSafe$(preferredDownloadDir, {recursive: true}).pipe(29 switchMap(downloadDir =>30 export$({description, downloadDir, ...retrieveOptions})31 )32 )33 })34 )35}36const export$ = ({37 downloadDir,38 description,39 recipe,40 indicator,41 scale,42 tileSize,43 shardSize,44 fileDimensions,45 crs,46 crsTransform47}) => {48 const aoi = recipe.model.aoi49 const sources = recipe.model.sources50 const dataSets = sources.dataSets51 const {startDate, endDate} = recipe.model.dates52 const reflectance = recipe.model.options.corrections.includes('SR') ? 'SR' : 'TOA'53 const tiles = tile(toFeatureCollection(aoi), tileSize) // synchronous EE54 const exportTiles$ = tileIds => {55 const totalTiles = tileIds.length56 const tile$ = from(57 tileIds.map((tileId, tileIndex) =>58 ({tileId, tileIndex})59 )60 )61 return concat(62 of({totalTiles}),63 tile$.pipe(64 mergeMap(tile => exportTile$(tile), 1)65 )66 )67 }68 const exportTile$ = ({tileId, tileIndex}) =>69 concat(70 of({tileIndex, chunks: 0}),71 exportChunks$(createChunks$({tileId, tileIndex})),72 postProcess$(Path.join(downloadDir, `${tileIndex}`))73 )74 const createChunks$ = ({tileId, tileIndex}) => {75 const tile = tiles.filterMetadata('system:index', 'equals', tileId).first()76 const from = moment(startDate)77 const to = moment(endDate)78 const duration = moment(to).subtract(1, 'day').diff(from, DATE_DELTA_UNIT)79 const dateOffsets = sequence(0, duration, DATE_DELTA)80 const chunks$ = dateOffsets.map(dateOffset => {81 const start = moment(from).add(dateOffset, DATE_DELTA_UNIT)82 const startString = start.format('YYYY-MM-DD')83 const end = moment.min(moment(start).add(DATE_DELTA, DATE_DELTA_UNIT), to)84 const endString = end.format('YYYY-MM-DD')85 const dateRange = `${startString}_${endString}`86 return hasImagery$(87 tile.geometry(),88 ee.Date(startString),89 ee.Date(endString)90 ).pipe(91 switchMap(notEmpty => {92 if (notEmpty) {93 return createTimeSeries$(tile, startString, endString).pipe(94 map(timeSeries =>95 ({tileIndex, timeSeries, dateRange, notEmpty})96 )97 )98 } else {99 return of({tileIndex, dateRange, notEmpty})100 }101 })102 )103 })104 return forkJoin(chunks$)105 }106 const isRadar = () => _.isEqual(Object.values(dataSets).flat(), ['SENTINEL_1'])107 const isOptical = () => Object.keys(dataSets).find(type => ['LANDSAT', 'SENTINEL_2'].includes(type))108 const createTimeSeries$ = (feature, startDate, endDate) => {109 const images$ = getCollection$({recipe, bands: [indicator], startDate, endDate})110 return images$.pipe(111 map(images => {112 images = images.select(indicator)113 const distinctDateImages = images.distinct('date')114 const timeSeries = ee.ImageCollection(115 ee.Join.saveAll('images')116 .apply({117 primary: distinctDateImages,118 secondary: images,119 condition: ee.Filter.equals({120 leftField: 'date',121 rightField: 'date'122 })123 })124 .map(image => ee.ImageCollection(ee.List(image.get('images')))125 .median()126 .rename(image.getString('date'))127 ))128 .toBands()129 .regexpRename('.*(.{10})', '$1')130 .clip(feature.geometry())131 return timeSeries.select(timeSeries.bandNames().sort())132 })133 )134 }135 const hasImagery$ = (geometry, startDate, endDate) =>136 ee.getInfo$(137 isRadar()138 ? hasRadarImagery({geometry, startDate, endDate, orbits: recipe.model.options.orbits})139 : isOptical()140 ? hasOpticalImagery({dataSets: extractDataSets(dataSets), reflectance, geometry, startDate, endDate})141 : hasPlanetImagery({sources: {...sources, source: Object.values(sources.dataSets).flat()[0]}, geometry, startDate, endDate}),142 'check if date range has imagery'143 )144 const exportChunks$ = chunks$ =>145 chunks$.pipe(146 switchMap(chunks => {147 const nonEmptyChunks = chunks.filter(({notEmpty}) => notEmpty)148 const totalChunks = nonEmptyChunks.length149 return concat(150 of({totalChunks}),151 from(nonEmptyChunks).pipe(152 mergeMap(chunk => exportChunk$(chunk))153 )154 )155 })156 )157 const exportChunk$ = ({tileIndex, timeSeries, dateRange}) => {158 const chunkDescription = `${description}_${tileIndex}_${dateRange}`159 const chunkDownloadDir = `${downloadDir}/${tileIndex}/chunk-${dateRange}`160 const export$ = exportImageToSepal$({161 image: timeSeries,162 folder: chunkDescription,163 description: chunkDescription,164 downloadDir: chunkDownloadDir,165 scale,166 crs,167 crsTransform,168 shardSize,169 fileDimensions170 }).pipe(swallow())171 return concat(172 export$,173 of({completedChunk: true})174 )175 }176 const tileIds$ = ee.getInfo$(tiles.aggregate_array('system:index'), `time-series image ids ${description}`)177 return tileIds$.pipe(178 switchMap(tileIds => exportTiles$(tileIds)),179 tap(progress => log.trace(() => `time-series: ${JSON.stringify(progress)}`)),180 scan(181 (acc, progress) => {182 return ({183 ...acc,184 ...progress,185 chunks: progress.chunks === undefined186 ? acc.chunks + (progress.completedChunk ? 1 : 0)187 : progress.chunks188 })189 },190 {tileIndex: 0, chunks: 0}191 ),192 map(toProgress)193 )194}195const postProcess$ = downloadDir =>196 terminal$('sepal-stack-time-series', [downloadDir])197 .pipe(198 tap(({stream, value}) => {199 if (value)200 stream === 'stdout' ? log.info(value) : log.warn(value)201 }),202 swallow()203 )204const toProgress = ({totalTiles = 0, tileIndex = 0, totalChunks = 0, chunks = 0}) => {205 const currentTilePercent = totalChunks ? Math.round(100 * chunks / totalChunks) : 0206 const currentTile = tileIndex + 1207 return currentTilePercent < 100208 ? {209 totalChunks,210 chunks,211 totalTiles,212 tileIndex,213 defaultMessage: `Exported ${currentTilePercent}% of tile ${currentTile} out of ${totalTiles}.`,214 messageKey: 'tasks.retrieve.time_series_to_sepal.progress',215 messageArgs: {currentTilePercent, currentTile, totalTiles}216 }217 : {218 totalChunks,219 chunks,220 totalTiles,221 tileIndex,222 defaultMessage: `Assembling tile ${currentTile} out of ${totalTiles}...`,223 messageKey: 'tasks.retrieve.time_series_to_sepal.assembling',224 messageArgs: {currentTile, totalTiles}225 }226}227const extractDataSets = sources =>228 Object.values(sources)229 .flat()230 .map(dataSet =>231 dataSet === 'LANDSAT_TM'232 ? ['LANDSAT_4', 'LANDSAT_5']233 : dataSet === 'LANDSAT_TM_T2'234 ? ['LANDSAT_4_T2', 'LANDSAT_5_T2']235 : dataSet236 )...
ipV6.spec.ts
Source:ipV6.spec.ts
1import { ipV6 } from '../../../src/arbitrary/ipV6';2import { Value } from '../../../src/check/arbitrary/definition/Value';3import {4 assertProduceValuesShrinkableWithoutContext,5 assertShrinkProducesSameValueWithoutInitialContext,6 assertProduceCorrectValues,7 assertProduceSameValueGivenSameSeed,8 assertGenerateIndependentOfSize,9} from './__test-helpers__/ArbitraryAssertions';10import { buildShrinkTree, renderTree } from './__test-helpers__/ShrinkTree';11describe('ipV6 (integration)', () => {12 const isValidIpV4 = (value: string) => {13 const chunks = value.split('.').map((v) => {14 if (v[0] === '0') {15 if (v[1] === 'x' || v[1] === 'X') return parseInt(v, 16);16 return parseInt(v, 8);17 }18 return parseInt(v, 10);19 });20 // one invalid chunk21 if (chunks.find((v) => Number.isNaN(v)) !== undefined) return false;22 // maximal amount of 4 chunks23 if (chunks.length > 4) return false;24 // all chunks, except the last one are inferior or equal to 25525 if (chunks.slice(0, -1).find((v) => v < 0 && v > 255) !== undefined) return false;26 // last chunk must be below 256^(5 â number of chunks)27 return chunks[chunks.length - 1] < 256 ** (5 - chunks.length);28 };29 const isCorrect = (value: string) => {30 const firstElision = value.indexOf('::');31 if (firstElision !== -1) {32 // At most one '::'33 if (value.substr(firstElision + 1).includes('::')) return false;34 }35 const chunks = value.split(':');36 const last = chunks[chunks.length - 1];37 // The ipv4 can only be composed of 4 decimal chunks separated by dots38 // 1.1000 is not a valid IP v4 in the context of IP v639 const endByIpV4 = last.includes('.') && isValidIpV4(last);40 const nonEmptyChunks = chunks.filter((c) => c !== '');41 const hexaChunks = endByIpV4 ? nonEmptyChunks.slice(0, nonEmptyChunks.length - 1) : nonEmptyChunks;42 if (!hexaChunks.every((s) => /^[0-9a-f]{1,4}$/.test(s))) return false;43 const equivalentChunkLength = endByIpV4 ? hexaChunks.length + 2 : hexaChunks.length;44 return firstElision !== -1 ? equivalentChunkLength < 8 : equivalentChunkLength === 8;45 };46 const ipV6Builder = () => ipV6();47 it('should produce the same values given the same seed', () => {48 assertProduceSameValueGivenSameSeed(ipV6Builder);49 });50 it('should only produce correct values', () => {51 assertProduceCorrectValues(ipV6Builder, isCorrect);52 });53 it('should produce values seen as shrinkable without any context', () => {54 assertProduceValuesShrinkableWithoutContext(ipV6Builder);55 });56 it('should be able to shrink to the same values without initial context', () => {57 assertShrinkProducesSameValueWithoutInitialContext(ipV6Builder);58 });59 it('should be independent of global settings overriding defaults on size', () => {60 assertGenerateIndependentOfSize(ipV6Builder);61 });62 it.each`63 source64 ${'::1'}65 ${'0123:5678:9abc:ef01:2345:6789:128.0.0.1'}66 ${'0123:5678:9abc:ef01:2345:6789:0000:ffff'}67 ${'0:56:9abc:ef01:234:67:8.8.8.8'}68 ${'0:56:9abc:ef01:234:67:0:f'}69 ${'::5678:9abc:ef01:2345:6789:128.0.0.1'}70 ${'::5678:9abc:ef01:2345:6789:0000:ffff'}71 ${'::9abc:ef01:2345:6789:128.0.0.1'}72 ${'::9abc:ef01:2345:6789:0000:ffff'}73 ${'5678::9abc:ef01:2345:6789:128.0.0.1'}74 ${'5678::9abc:ef01:2345:6789:0000:ffff'}75 ${'::ef01:2345:6789:128.0.0.1'}76 ${'::ef01:2345:6789:0000:ffff'}77 ${'9abc::ef01:2345:6789:128.0.0.1'}78 ${'9abc::ef01:2345:6789:0000:ffff'}79 ${'5678:9abc::ef01:2345:6789:128.0.0.1'}80 ${'5678:9abc::ef01:2345:6789:0000:ffff'}81 ${'::2345:6789:128.0.0.1'}82 ${'::2345:6789:0000:ffff'}83 ${'ef01::2345:6789:128.0.0.1'}84 ${'ef01::2345:6789:0000:ffff'}85 ${'9abc:ef01::2345:6789:128.0.0.1'}86 ${'9abc:ef01::2345:6789:0000:ffff'}87 ${'5678:9abc:ef01::2345:6789:128.0.0.1'}88 ${'5678:9abc:ef01::2345:6789:0000:ffff'}89 ${'::6789:128.0.0.1'}90 ${'::6789:0000:ffff'}91 ${'2345::6789:128.0.0.1'}92 ${'2345::6789:0000:ffff'}93 ${'ef01:2345::6789:128.0.0.1'}94 ${'ef01:2345::6789:0000:ffff'}95 ${'9abc:ef01:2345::6789:128.0.0.1'}96 ${'9abc:ef01:2345::6789:0000:ffff'}97 ${'5678:9abc:ef01:2345::6789:128.0.0.1'}98 ${'5678:9abc:ef01:2345::6789:0000:ffff'}99 ${'::128.0.0.1'}100 ${'::0000:ffff'}101 ${'2345::128.0.0.1'}102 ${'2345::0000:ffff'}103 ${'ef01:2345::128.0.0.1'}104 ${'ef01:2345::0000:ffff'}105 ${'9abc:ef01:2345::128.0.0.1'}106 ${'9abc:ef01:2345::0000:ffff'}107 ${'5678:9abc:ef01:2345::128.0.0.1'}108 ${'5678:9abc:ef01:2345::0000:ffff'}109 ${'0123:5678:9abc:ef01:2345::128.0.0.1'}110 ${'0123:5678:9abc:ef01:2345::0000:ffff'}111 ${'::0123'}112 ${'6789::0123'}113 ${'2345:6789::0123'}114 ${'ef01:2345:6789::0123'}115 ${'9abc:ef01:2345:6789::0123'}116 ${'5678:9abc:ef01:2345:6789::0123'}117 ${'0123:5678:9abc:ef01:2345:6789::0123'}118 ${'::'}119 ${'0123::'}120 ${'6789:0123::'}121 ${'2345:6789:0123::'}122 ${'ef01:2345:6789:0123::'}123 ${'9abc:ef01:2345:6789:0123::'}124 ${'5678:9abc:ef01:2345:6789:0123::'}125 ${'0123:5678:9abc:ef01:2345:6789:0123::'}126 `('should be able to generate $source with fc.ipV6()', ({ source }) => {127 // Arrange / Act128 const arb = ipV6();129 const out = arb.canShrinkWithoutContext(source);130 // Assert131 expect(out).toBe(true);132 });133 it.each`134 rawValue135 ${'::1'}136 ${'0123:5678:9abc:ef01:2345:6789:128.0.0.1'}137 `('should be able to shrink $rawValue', ({ rawValue }) => {138 // Arrange139 const arb = ipV6();140 const value = new Value(rawValue, undefined);141 // Act142 const renderedTree = renderTree(buildShrinkTree(arb, value, { numItems: 100 })).join('\n');143 // Assert144 expect(arb.canShrinkWithoutContext(rawValue)).toBe(true);145 expect(renderedTree).toMatchSnapshot();146 });...
Using AI Code Generation
1const fc = require('fast-check');2const { nonEmptyChunks } = require('fast-check/lib/check/arbitrary/ChunksArbitrary.js');3fc.assert(4 fc.property(nonEmptyChunks(fc.integer()), (chunks) => {5 return chunks.length > 0;6 })7);8const { nonEmptyChunks } = require('fast-check/lib/check/arbitrary/ChunksArbitrary.js');9SyntaxError: Unexpected token {10 at wrapSafe (internal/modules/cjs/loader.js:1101:16)11 at Module._compile (internal/modules/cjs/loader.js:1153:27)12 at Object.Module._extensions..js (internal/modules/cjs/loader.js:1213:10)13 at Module.load (internal/modules/cjs/loader.js:1032:32)14 at Function.Module._load (internal/modules/cjs/loader.js:924:14)15 at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12)16What is the correct way to import the nonEmptyChunks method?17const fc = require('fast-check');18const nonEmptyChunks = require('fast-check/lib/check/arbitrary/ChunksArbitrary.js').nonEmptyChunks;19fc.assert(20 fc.property(nonEmptyChunks(fc.integer()), (chunks) => {21 return chunks.length > 0;22 })23);
Using AI Code Generation
1import { nonEmptyChunks } from 'fast-check';2import { nonEmptyChunks } from 'fast-check';3import { nonEmptyChunks } from 'fast-check';4import { nonEmptyChunks } from 'fast-check';5import { nonEmptyChunks } from 'fast-check';6import { nonEmptyChunks } from 'fast-check';7import { nonEmptyChunks } from 'fast-check';8import { nonEmptyChunks } from 'fast-check';9import { nonEmptyChunks } from 'fast-check';10import { nonEmptyChunks } from 'fast-check';11import { nonEmptyChunks } from 'fast-check';12import { nonEmptyChunks } from 'fast-check';13import { nonEmptyChunks } from 'fast-check';14import { nonEmptyChunks } from 'fast-check';15import { nonEmptyChunks } from 'fast-check';16import { nonEmptyChunks } from 'fast-check';17import { nonEmptyChunks } from 'fast-check';18import { nonEmptyChunks } from '
Using AI Code Generation
1import { nonEmptyChunks } from "fast-check";2import { suite } from "uvu";3import * as assert from "uvu/assert";4const test = suite("Test nonEmptyChunks");5test("test nonEmptyChunks", () => {6 const chunks = nonEmptyChunks({ maxChunkSize: 4 }, 4);7 assert.is(chunks, [[1, 2, 3, 4]]);8});9test.run();10Could you provide more details on your setup? (How did you install fast-check-monorepo? How did you run the test? Which version of fast-check are you using?)
Using AI Code Generation
1const fc = require('fast-check');2const nonEmptyChunks = require('fast-check-monorepo').nonEmptyChunks;3const { nonEmptyArray } = require('fast-check');4const { array } = require('fast-check');5const { tuple } = require('fast-check');6const { constantFrom } = require('fast-check');7const test = () => {8 fc.assert(9 fc.property(nonEmptyChunks(nonEmptyArray(constantFrom(1, 2, 3))), (arr) => {10 console.log(arr);11 return true;
Learn to execute automation testing from scratch with LambdaTest Learning Hub. Right from setting up the prerequisites to run your first automation test, to following best practices and diving deeper into advanced test scenarios. LambdaTest Learning Hubs compile a list of step-by-step guides to help you be proficient with different test automation frameworks i.e. Selenium, Cypress, TestNG etc.
You could also refer to video tutorials over LambdaTest YouTube channel to get step by step demonstration from industry experts.
Get 100 minutes of automation test minutes FREE!!