Allow using createPassage with out of bounds coordinates when TILE_CENTERED_HEIGHT_MA...
[0ad.git] / binaries / data / mods / public / maps / random / rmgen-common / gaia_terrain.js
blob7bb26cc25b201408b84d6adddc5eb420f1eee215
1 /**
2  * @file These functions are often used to create a landscape, for instance shaping mountains, hills, rivers or grass and dirt patches.
3  */
5 /**
6  * Bumps add slight, diverse elevation differences to otherwise completely level terrain.
7  */
8 function createBumps(constraint, count, minSize, maxSize, spread, failFraction = 0, elevation = 2)
10         g_Map.log("Creating bumps");
11         createAreas(
12                 new ChainPlacer(
13                         minSize || 1,
14                         maxSize || Math.floor(scaleByMapSize(4, 6)),
15                         spread || Math.floor(scaleByMapSize(2, 5)),
16                         failFraction),
17                 new SmoothElevationPainter(ELEVATION_MODIFY, elevation, 2),
18                 constraint,
19                 count || scaleByMapSize(100, 200));
22 /**
23  * Hills are elevated, planar, impassable terrain areas.
24  */
25 function createHills(terrainset, constraint, tileClass, count, minSize, maxSize, spread, failFraction = 0.5, elevation = 18, elevationSmoothing = 2)
27         g_Map.log("Creating hills");
28         createAreas(
29                 new ChainPlacer(
30                         minSize || 1,
31                         maxSize || Math.floor(scaleByMapSize(4, 6)),
32                         spread || Math.floor(scaleByMapSize(16, 40)),
33                         failFraction),
34                 [
35                         new LayeredPainter(terrainset, [1, elevationSmoothing]),
36                         new SmoothElevationPainter(ELEVATION_SET, elevation, elevationSmoothing),
37                         new TileClassPainter(tileClass)
38                 ],
39                 constraint,
40                 count || scaleByMapSize(1, 4) * getNumPlayers());
43 /**
44  * Mountains are impassable smoothened cones.
45  */
46 function createMountains(terrain, constraint, tileClass, count, maxHeight, minRadius, maxRadius, numCircles)
48         g_Map.log("Creating mountains");
49         let mapSize = g_Map.getSize();
51         for (let i = 0; i < (count || scaleByMapSize(1, 4) * getNumPlayers()); ++i)
52                 createMountain(
53                         maxHeight !== undefined ? maxHeight : Math.floor(scaleByMapSize(30, 50)),
54                         minRadius || Math.floor(scaleByMapSize(3, 4)),
55                         maxRadius || Math.floor(scaleByMapSize(6, 12)),
56                         numCircles || Math.floor(scaleByMapSize(4, 10)),
57                         constraint,
58                         randIntExclusive(0, mapSize),
59                         randIntExclusive(0, mapSize),
60                         terrain,
61                         tileClass,
62                         14);
65 /**
66  * Create a mountain using a technique very similar to ChainPlacer.
67  */
68 function createMountain(maxHeight, minRadius, maxRadius, numCircles, constraints, x, z, terrain, tileClass, fcc = 0, q = [])
70         let position = new Vector2D(x, z);
71         let constraint = new AndConstraint(constraints);
73         if (!g_Map.inMapBounds(position) || !constraint.allows(position))
74                 return;
76         let mapSize = g_Map.getSize();
77         let queueEmpty = !q.length;
79         let gotRet = [];
80         for (let i = 0; i < mapSize; ++i)
81         {
82                 gotRet[i] = [];
83                 for (let j = 0; j < mapSize; ++j)
84                         gotRet[i][j] = -1;
85         }
87         --mapSize;
89         minRadius = Math.max(1, Math.min(minRadius, maxRadius));
91         let edges = [[x, z]];
92         let circles = [];
94         for (let i = 0; i < numCircles; ++i)
95         {
96                 let badPoint = false;
97                 let [cx, cz] = pickRandom(edges);
99                 let radius;
100                 if (queueEmpty)
101                         radius = randIntInclusive(minRadius, maxRadius);
102                 else
103                 {
104                         radius = q.pop();
105                         queueEmpty = !q.length;
106                 }
108                 let sx = Math.max(0, cx - radius);
109                 let sz = Math.max(0, cz - radius);
110                 let lx = Math.min(cx + radius, mapSize);
111                 let lz = Math.min(cz + radius, mapSize);
113                 let radius2 = Math.square(radius);
115                 for (let ix = sx; ix <= lx; ++ix)
116                 {
117                         for (let iz = sz; iz <= lz; ++iz)
118                         {
119                                 let pos = new Vector2D(ix, iz);
121                                 if (Math.euclidDistance2D(ix, iz, cx, cz) > radius2 || !g_Map.inMapBounds(pos))
122                                         continue;
124                                 if (!constraint.allows(pos))
125                                 {
126                                         badPoint = true;
127                                         break;
128                                 }
130                                 let state = gotRet[ix][iz];
131                                 if (state == -1)
132                                 {
133                                         gotRet[ix][iz] = -2;
134                                 }
135                                 else if (state >= 0)
136                                 {
137                                         edges.splice(state, 1);
138                                         gotRet[ix][iz] = -2;
140                                         for (let k = state; k < edges.length; ++k)
141                                                 --gotRet[edges[k][0]][edges[k][1]];
142                                 }
143                         }
145                         if (badPoint)
146                                 break;
147                 }
149                 if (badPoint)
150                         continue;
152                 circles.push([cx, cz, radius]);
154                 for (let ix = sx; ix <= lx; ++ix)
155                         for (let iz = sz; iz <= lz; ++iz)
156                         {
157                                 if (gotRet[ix][iz] != -2 ||
158                                     fcc && (x - ix > fcc || ix - x > fcc || z - iz > fcc || iz - z > fcc) ||
159                                     ix > 0 && gotRet[ix-1][iz] == -1 ||
160                                     iz > 0 && gotRet[ix][iz-1] == -1 ||
161                                     ix < mapSize && gotRet[ix+1][iz] == -1 ||
162                                     iz < mapSize && gotRet[ix][iz+1] == -1)
163                                         continue;
165                                 edges.push([ix, iz]);
166                                 gotRet[ix][iz] = edges.length - 1;
167                         }
168         }
170         for (let [cx, cz, radius] of circles)
171         {
172                 let circlePosition = new Vector2D(cx, cz);
173                 let sx = Math.max(0, cx - radius);
174                 let sz = Math.max(0, cz - radius);
175                 let lx = Math.min(cx + radius, mapSize);
176                 let lz = Math.min(cz + radius, mapSize);
178                 let clumpHeight = radius / maxRadius * maxHeight * randFloat(0.8, 1.2);
180                 for (let ix = sx; ix <= lx; ++ix)
181                         for (let iz = sz; iz <= lz; ++iz)
182                         {
183                                 let position = new Vector2D(ix, iz);
184                                 let distance = position.distanceTo(circlePosition);
186                                 let newHeight =
187                                         randIntInclusive(0, 2) +
188                                         Math.round(2/3 * clumpHeight * (Math.sin(Math.PI * 2/3 * (3/4 - distance / radius)) + 0.5));
190                                 if (distance > radius)
191                                         continue;
193                                 if (g_Map.getHeight(position) < newHeight)
194                                         g_Map.setHeight(position, newHeight);
195                                 else if (g_Map.getHeight(position) >= newHeight && g_Map.getHeight(position) < newHeight + 4)
196                                         g_Map.setHeight(position, newHeight + 4);
198                                 if (terrain)
199                                         createTerrain(terrain).place(position);
201                                 if (tileClass)
202                                         tileClass.add(position);
203                         }
204         }
208  * Generates a volcano mountain. Smoke and lava are optional.
210  * @param {number} center - Vector2D location on the tilemap.
211  * @param {number} tileClass - Painted onto every tile that is occupied by the volcano.
212  * @param {string} terrainTexture - The texture painted onto the volcano hill.
213  * @param {array} lavaTextures - Three different textures for the interior, from the outside to the inside.
214  * @param {boolean} smoke - Whether to place smoke particles.
215  * @param {number} elevationType - Elevation painter type, ELEVATION_SET = absolute or ELEVATION_MODIFY = relative.
216  */
217 function createVolcano(position, tileClass, terrainTexture, lavaTextures, smoke, elevationType)
219         g_Map.log("Creating volcano");
221         let clLava = g_Map.createTileClass();
222         let layers = [
223                 {
224                         "clumps": diskArea(scaleByMapSize(18, 25)),
225                         "elevation": 15,
226                         "tileClass": tileClass,
227                         "steepness": 3
228                 },
229                 {
230                         "clumps": diskArea(scaleByMapSize(16, 23)),
231                         "elevation": 25,
232                         "tileClass": g_Map.createTileClass(),
233                         "steepness": 3
234                 },
235                 {
236                         "clumps": diskArea(scaleByMapSize(10, 15)),
237                         "elevation": 45,
238                         "tileClass": g_Map.createTileClass(),
239                         "steepness": 3
240                 },
241                 {
242                         "clumps": diskArea(scaleByMapSize(8, 11)),
243                         "elevation": 62,
244                         "tileClass": g_Map.createTileClass(),
245                         "steepness": 3
246                 },
247                 {
248                         "clumps": diskArea(scaleByMapSize(4, 6)),
249                         "elevation": 42,
250                         "tileClass": clLava,
251                         "painter": lavaTextures && new LayeredPainter([terrainTexture, ...lavaTextures], [1, 1, 1]),
252                         "steepness": 1
253                 }
254         ];
256         for (let i = 0; i < layers.length; ++i)
257                 createArea(
258                         new ClumpPlacer(layers[i].clumps, 0.7, 0.05, Infinity, position),
259                         [
260                                 layers[i].painter || new LayeredPainter([terrainTexture, terrainTexture], [3]),
261                                 new SmoothElevationPainter(elevationType, layers[i].elevation, layers[i].steepness),
262                                 new TileClassPainter(layers[i].tileClass)
263                         ],
264                         i == 0 ? null : stayClasses(layers[i - 1].tileClass, 1));
266         if (smoke)
267         {
268                 let num = Math.floor(diskArea(scaleByMapSize(3, 5)));
269                 createObjectGroup(
270                         new SimpleGroup(
271                                 [new SimpleObject("actor|particle/smoke.xml", num, num, 0, 7)],
272                                 false,
273                                 clLava,
274                                 position),
275                         0,
276                 stayClasses(tileClass, 1));
277         }
281  * Paint the given terrain texture in the given sizes at random places of the map to diversify monotone land texturing.
282  */
283 function createPatches(sizes, terrain, constraint, count,  tileClass, failFraction =  0.5)
285         for (let size of sizes)
286                 createAreas(
287                         new ChainPlacer(1, Math.floor(scaleByMapSize(3, 5)), size, failFraction),
288                         [
289                                 new TerrainPainter(terrain),
290                                 new TileClassPainter(tileClass)
291                         ],
292                         constraint,
293                         count);
297  * Same as createPatches, but each patch consists of a set of textures drawn depending to the distance of the patch border.
298  */
299 function createLayeredPatches(sizes, terrains, terrainWidths, constraint, count, tileClass, failFraction = 0.5)
301         for (let size of sizes)
302                 createAreas(
303                         new ChainPlacer(1, Math.floor(scaleByMapSize(3, 5)), size, failFraction),
304                         [
305                                 new LayeredPainter(terrains, terrainWidths),
306                                 new TileClassPainter(tileClass)
307                         ],
308                         constraint,
309                         count);
313  * Creates a meandering river at the given location and width.
314  * Optionally calls a function on the affected tiles.
316  * @property start - A Vector2D in tile coordinates stating where the river starts.
317  * @property end - A Vector2D in tile coordinates stating where the river ends.
318  * @property parallel - Whether the shorelines should be parallel or meander separately.
319  * @property width - Size between the two shorelines.
320  * @property fadeDist - Size of the shoreline.
321  * @property deviation - Fuzz effect on the shoreline if greater than 0.
322  * @property heightRiverbed - Ground height of the riverbed.
323  * @proeprty heightLand - Ground height of the end of the shoreline.
324  * @property meanderShort - Strength of frequent meanders.
325  * @property meanderLong - Strength of less frequent meanders.
326  * @property [constraint] - If given, ignores any tiles that don't satisfy the given Constraint.
327  * @property [waterFunc] - Optional function called on tiles within the river.
328  *                         Provides location on the tilegrid, new elevation and
329  *                         the location on the axis parallel to the river as a fraction of the river length.
330  * @property [landFunc] - Optional function called on land tiles, providing ix, iz, shoreDist1, shoreDist2.
331  * @property [minHeight] - If given, only changes the elevation below this height while still calling the given functions.
332  */
333 function paintRiver(args)
335         g_Map.log("Creating river");
337         // Model the river meandering as the sum of two sine curves.
338         let meanderShort = fractionToTiles(args.meanderShort / scaleByMapSize(35, 160));
339         let meanderLong = fractionToTiles(args.meanderLong / scaleByMapSize(35, 100));
341         // Unless the river is parallel, each riverside will receive an own random seed and starting angle.
342         let seed1 = randFloat(2, 3);
343         let seed2 = randFloat(2, 3);
345         let startingAngle1 = randFloat(0, 1);
346         let startingAngle2 = randFloat(0, 1);
348         // Computes the deflection of the river at a given point.
349         let riverCurve = (riverFraction, startAngle, seed) =>
350                 meanderShort * rndRiver(startAngle + fractionToTiles(riverFraction) / 128, seed) +
351                 meanderLong * rndRiver(startAngle + fractionToTiles(riverFraction) / 256, seed);
353         // Describe river location in vectors.
354         let riverLength = args.start.distanceTo(args.end);
355         let unitVecRiver = Vector2D.sub(args.start, args.end).normalize();
357         // Describe river boundaries.
358         let riverMinX = Math.min(args.start.x, args.end.x);
359         let riverMinZ = Math.min(args.start.y, args.end.y);
360         let riverMaxX = Math.max(args.start.x, args.end.x);
361         let riverMaxZ = Math.max(args.start.y, args.end.y);
363         let mapSize = g_Map.getSize();
364         for (let ix = 0; ix < mapSize; ++ix)
365                 for (let iz = 0; iz < mapSize; ++iz)
366                 {
367                         let vecPoint = new Vector2D(ix, iz);
369                         if (args.constraint && !args.constraint.allows(vecPoint))
370                                 continue;
372                         // Compute the shortest distance to the river.
373                         let distanceToRiver = distanceOfPointFromLine(args.start, args.end, vecPoint);
375                         // Closest point on the river (i.e the foot of the perpendicular).
376                         let river = Vector2D.sub(vecPoint, unitVecRiver.perpendicular().mult(distanceToRiver));
378                         // Only process points that actually are perpendicular with the river.
379                         if (river.x < riverMinX || river.x > riverMaxX ||
380                             river.y < riverMinZ || river.y > riverMaxZ)
381                                 continue;
383                         // Coordinate between 0 and 1 on the axis parallel to the river.
384                         let riverFraction = river.distanceTo(args.start) / riverLength;
386                         // Amplitude of the river at this location.
387                         let riverCurve1 = riverCurve(riverFraction, startingAngle1, seed1);
388                         let riverCurve2 = args.parallel ? riverCurve1 : riverCurve(riverFraction, startingAngle2, seed2);
390                         // Add noise.
391                         let deviation = args.deviation * randFloat(-1, 1);
393                         // Compute the distance to the shoreline.
394                         let shoreDist1 = riverCurve1 + distanceToRiver - deviation - args.width / 2;
395                         let shoreDist2 = riverCurve2 + distanceToRiver - deviation + args.width / 2;
397                         // Create the elevation for the water and the slopy shoreline and call the user functions.
398                         if (shoreDist1 < 0 && shoreDist2 > 0)
399                         {
400                                 let height = args.heightRiverbed;
402                                 if (shoreDist1 > -args.fadeDist)
403                                         height += (args.heightLand - args.heightRiverbed) * (1 + shoreDist1 / args.fadeDist);
404                                 else if (shoreDist2 < args.fadeDist)
405                                         height += (args.heightLand - args.heightRiverbed) * (1 - shoreDist2 / args.fadeDist);
407                                 if (args.minHeight === undefined || height < args.minHeight)
408                                         g_Map.setHeight(vecPoint, height);
410                                 if (args.waterFunc)
411                                         args.waterFunc(vecPoint, height, riverFraction);
412                         }
413                         else if (args.landFunc)
414                                 args.landFunc(vecPoint, shoreDist1, shoreDist2);
415                 }
419  * Helper function to create a meandering river.
420  * It works the same as sin or cos function with the difference that it's period is 1 instead of 2 pi.
421  */
422 function rndRiver(f, seed)
424         let rndRw = seed;
426         for (let i = 0; i <= f; ++i)
427                 rndRw = 10 * (rndRw % 1);
429         let rndRr = f % 1;
430         let retVal = (Math.floor(f) % 2 ? -1 : 1) * rndRr * (rndRr - 1);
432         let rndRe = Math.floor(rndRw) % 5;
433         if (rndRe == 0)
434                 retVal *= 2.3 * (rndRr - 0.5) * (rndRr - 0.5);
435         else if (rndRe == 1)
436                 retVal *= 2.6 * (rndRr - 0.3) * (rndRr - 0.7);
437         else if (rndRe == 2)
438                 retVal *= 22 * (rndRr - 0.2) * (rndRr - 0.3) * (rndRr - 0.3) * (rndRr - 0.8);
439         else if (rndRe == 3)
440                 retVal *= 180 * (rndRr - 0.2) * (rndRr - 0.2) * (rndRr - 0.4) * (rndRr - 0.6) * (rndRr - 0.6) * (rndRr - 0.8);
441         else if (rndRe == 4)
442                 retVal *= 2.6 * (rndRr - 0.5) * (rndRr - 0.7);
444         return retVal;
448  * Add small rivers with shallows starting at a central river ending at the map border, if the given Constraint is met.
449  */
450 function createTributaryRivers(riverAngle, riverCount, riverWidth, heightRiverbed, heightRange, maxAngle, tributaryRiverTileClass, shallowTileClass, constraint)
452         g_Map.log("Creating tributary rivers");
453         let waviness = 0.4;
454         let smoothness = scaleByMapSize(3, 12);
455         let offset = 0.1;
456         let tapering = 0.05;
457         let heightShallow = -2;
459         let mapSize = g_Map.getSize();
460         let mapCenter = g_Map.getCenter();
461         let mapBounds = g_Map.getBounds();
463         let riverConstraint = avoidClasses(tributaryRiverTileClass, 3);
464         if (shallowTileClass)
465                 riverConstraint = new AndConstraint([riverConstraint, avoidClasses(shallowTileClass, 2)]);
467         for (let i = 0; i < riverCount; ++i)
468         {
469                 // Determining tributary river location
470                 let searchCenter = new Vector2D(fractionToTiles(randFloat(tapering, 1 - tapering)), mapCenter.y);
471                 let sign = randBool() ? 1 : -1;
472                 let distanceVec = new Vector2D(0, sign * tapering);
474                 let searchStart = Vector2D.add(searchCenter, distanceVec).rotateAround(riverAngle, mapCenter);
475                 let searchEnd = Vector2D.sub(searchCenter, distanceVec).rotateAround(riverAngle, mapCenter);
477                 let start = findLocationInDirectionBasedOnHeight(searchStart, searchEnd, heightRange[0], heightRange[1], 4);
478                 if (!start)
479                         continue;
481                 start.round();
482                 let end = Vector2D.add(mapCenter, new Vector2D(mapSize, 0).rotate(riverAngle - sign * randFloat(maxAngle, 2 * Math.PI - maxAngle))).round();
484                 // Create river
485                 if (!createArea(
486                         new PathPlacer(start, end, riverWidth, waviness, smoothness, offset, tapering),
487                         [
488                                 new SmoothElevationPainter(ELEVATION_SET, heightRiverbed, 4),
489                                 new TileClassPainter(tributaryRiverTileClass)
490                         ],
491                         new AndConstraint([constraint, riverConstraint])))
492                         continue;
494                 // Create small puddles at the map border to ensure players being separated
495                 createArea(
496                         new ClumpPlacer(diskArea(riverWidth / 2), 0.95, 0.6, Infinity, end),
497                         new SmoothElevationPainter(ELEVATION_SET, heightRiverbed, 3),
498                         constraint);
499         }
501         // Create shallows
502         if (shallowTileClass)
503         {
504                 g_Map.log("Creating shallows in the tributary rivers...");
505                 for (let z of [0.25, 0.75])
506                         createPassage({
507                                 "start": new Vector2D(mapBounds.left, fractionToTiles(z)).rotateAround(riverAngle, mapCenter),
508                                 "end": new Vector2D(mapBounds.right, fractionToTiles(z)).rotateAround(riverAngle, mapCenter),
509                                 "startWidth": scaleByMapSize(8, 12),
510                                 "endWidth": scaleByMapSize(8, 12),
511                                 "smoothWidth": 2,
512                                 "constraint": new HeightConstraint(-Infinity, heightShallow),
513                                 "startHeight": heightShallow,
514                                 "endHeight": heightShallow,
515                                 "tileClass": shallowTileClass
516                         });
517         }
521  * Creates a smooth, passable path between between start and end with the given startWidth and endWidth.
522  * Paints the given tileclass and terrain.
524  * @property {Vector2D} start - Location of the passage.
525  * @property {Vector2D} end
526  * @property {Constraint} [constraint] - Only tiles that meet this constraint are changed.
527  * @property {number} startWidth - Size of the passage (perpendicular to the direction of the passage).
528  * @property {number} endWidth
529  * @property {number} [startHeight] - Fixed height to be used if the height at the location shouldn't be used.
530  * @property {number} [endHeight]
531  * @property {number} [maxHeight] - If given, do not touch any terrain above this height.
532  * @property {number} smoothWidth - Number of tiles at the passage border to apply height interpolation.
533  * @property {number} [tileClass] - Marks the passage with this tile class.
534  * @property {string} [terrain] - Texture to be painted on the passage area.
535  * @property {string} [edgeTerrain] - Texture to be painted on the borders of the passage.
536  * @returns {Area}
537  */
538 function createPassage(args)
540         let bound = x => Math.max(0, Math.min(Math.round(x), g_Map.height.length - 1));
542         let startHeight = args.startHeight !== undefined ? args.startHeight : g_Map.getHeight(new Vector2D(bound(args.start.x), bound(args.start.y)));
543         let endHeight = args.endHeight !== undefined ? args.endHeight : g_Map.getHeight(new Vector2D(bound(args.end.x), bound(args.end.y)));
545         let passageVec = Vector2D.sub(args.end, args.start);
546         let widthDirection = passageVec.perpendicular().normalize();
547         let lengthStep = 1 / (2 * passageVec.length());
548         let points = [];
550         for (let lengthFraction = 0; lengthFraction <= 1; lengthFraction += lengthStep)
551         {
552                 let locationLength = Vector2D.add(args.start, Vector2D.mult(passageVec, lengthFraction));
553                 let halfPassageWidth = (args.startWidth + (args.endWidth - args.startWidth) * lengthFraction) / 2;
554                 let passageHeight = startHeight + (endHeight - startHeight) * lengthFraction;
556                 for (let stepWidth = -halfPassageWidth; stepWidth <= halfPassageWidth; stepWidth += 0.5)
557                 {
558                         let location = Vector2D.add(locationLength, Vector2D.mult(widthDirection, stepWidth)).round();
560                         if (!g_Map.inMapBounds(location) ||
561                             args.constraint && !args.constraint.allows(location))
562                                 continue;
564                         points.push(location);
566                         let smoothDistance = args.smoothWidth + Math.abs(stepWidth) - halfPassageWidth;
568                         g_Map.setHeight(
569                                 location,
570                                 smoothDistance > 0 ?
571                                         (g_Map.getHeight(location) * smoothDistance + passageHeight / smoothDistance) / (smoothDistance + 1 / smoothDistance) :
572                                         passageHeight);
574                         if (args.tileClass)
575                                 args.tileClass.add(location);
577                         if (args.edgeTerrain && smoothDistance > 0)
578                                 createTerrain(args.edgeTerrain).place(location);
579                         else if (args.terrain)
580                                 createTerrain(args.terrain).place(location);
581                 }
582         }
584         return new Area(points);
588  * Returns the first location between startPoint and endPoint that lies within the given heightrange.
589  */
590 function findLocationInDirectionBasedOnHeight(startPoint, endPoint, minHeight, maxHeight, offset = 0)
592         let stepVec = Vector2D.sub(endPoint, startPoint);
593         let distance = Math.ceil(stepVec.length());
594         stepVec.normalize();
596         for (let i = 0; i < distance; ++i)
597         {
598                 let pos = Vector2D.add(startPoint, Vector2D.mult(stepVec, i));
599                 let ipos = pos.clone().round();
601                 if (g_Map.validHeight(ipos) &&
602                     g_Map.getHeight(ipos) >= minHeight &&
603                     g_Map.getHeight(ipos) <= maxHeight)
604                         return pos.add(stepVec.mult(offset));
605         }
607         return undefined;