PageSourceSearch

https://interactive-materials.github.io/shape-haptics/js/hapticproject.js

js interactive-materials.github.io collected 2026-10-03 09:20:46 UTC 25,711 bytes, 871 lines download raw bytes

1const DISPLACEMENT_DENSITY = 5;
2
3const SPRING_SAMPLING_DENSITY = 251;
4
5const SPRING_SAMPLING_SUBDENSITY = 4;
6
7const CONTACT_TOLERANCE = 0.005;
8
9const INIT_ITERATION = 10;
10
11const SCALE_UP = 100;
12
13const SIDE_PARAM = {
14  thickness: 1.2,
15  armThickness: 1.5,
16  length: 13,
17  throw: 7,
18  angle: 20,
19  ring: 6
20};
21
22const SPRING_PARAM = {
23  enable: true,
24  height: 28,
25  gap: 4,
26  thickness: 1.4,
27  width: 20,
28  count: 3
29};
30
31const CASE_PARAM = {
32  sideSpringClearance: 1.0,
33  springWidth: 22,
34  wall: 4,
35  topBuffer: 2,
36  baseBuffer: 4.5,
37  hole: 2.0,
38  holeOffset: 7.0
39};
40
41const SLIDER_PARAM = {
42  base: 4.0,
43  travel: 10.0,
44  preloadSide: 1.0,
45  preloadBase: 1.0,
46  stopper: 3,
47  fillet: 1.5,
48  tolerance: 0.1,
49  maxDeflection: 4.0
50};
51
52class HapticProject {
53  constructor(_name, _sides, _caseP, _sliderP, _springP, _sideSpringP) {
54    this.name = _name;
55
56    this.updateDisplay = true;
57    this.updateGallery = false;
58
59    this.parameters = {
60      case: _caseP,
61      slider: _sliderP,
62      spring: _springP,
63      side: _sideSpringP
64    };
65
66    this.sides = _sides;
67
68    this.fdData = [];
69
70    this.drawingBase = undefined;
71    this.drawingView = undefined;
72    this.drawingFab = undefined;
73
74    this.profile = {
75      left: new HapticProfile(this, "LEFT"),
76      right:
77        this.sides === "DOUBLE" ? new HapticProfile(this, "RIGHT") : undefined
78    };
79
80    this.updateBase();
81
82    this.updateView(0);
83  }
84
85  initFD() {
86    this.fdData = [];
87    for (var i in this.profile) {
88      if (this.profile[i]) this.profile[i].fdData = [];
89    }
90
91    var count = this.parameters.slider.travel * DISPLACEMENT_DENSITY;
92    for (var i = 0; i <= count; i++) {
93      this.fdData.push({
94        d: i * (1 / DISPLACEMENT_DENSITY),
95        force: 0,
96        forceReverse: 0,
97        springForce: 0
98      });
99
100      for (var j in this.profile) {
101        if (this.profile[j])
102          this.profile[j].fdData.push({
103            d: i * (1 / DISPLACEMENT_DENSITY),
104            angle: 0,
105            deflection: 0,
106            ratioForce: 0,
107            ratioFriction: 0,
108            ratioFrictionUp: 0,
109            offset: 0,
110            force: 0,
111            friction: 0
112          });
113      }
114    }
115  }
116
117  updateFD(index, pos) {
118    if (index < this.fdData.length) {
119      var springCoeff = calcSpringGradient(
120        this.parameters.spring.width,
121        this.parameters.spring.thickness
122      ); // FORMULA FOR MAIN SPRING COEFF HERE
123      this.fdData[index].springForce =
124        (this.fdData[index].d + this.parameters.slider.preloadBase) *
125        springCoeff;
126
127      var tempForce = this.parameters.spring.enable
128        ? this.fdData[index].springForce
129        : 0;
130
131      var tempForceReverse = this.parameters.spring.enable
132        ? this.fdData[index].springForce
133        : 0;
134
135      for (var i in this.profile) {
136        if (this.profile[i]) {
137          if (this.profile[i].type !== "SYMMETRICAL") {
138            this.profile[i].updateProfile(pos);
139            this.profile[i].calcForce(index, CONTACT_TOLERANCE * SCALE_UP);
140
141            var sideCoeff = calcSideGradient(this.parameters.side.thickness);
142
143            var frictionCoeff = FRICTION_COEFFICIENT;
144            var sideForce =
145              sideCoeff * this.profile[i].fdData[index].deflection;
146
147            var avgArr = [];
148            for (var j = -1; j <= 1; j++) {
149              if (this.profile[i].fdData[index + j]) {
150                avgArr.push(this.profile[i].fdData[index + j]);
151              }
152            }
153
154            var ratioAvgForce =
155              avgArr.map(v => v.ratioForce).reduce((acc, v) => acc + v) /
156              avgArr.length;
157            var ratioAvgFriction =
158              avgArr.map(v => v.ratioFriction).reduce((acc, v) => acc + v) /
159              avgArr.length;
160            var ratioAvgFrictionUp =
161              avgArr.map(v => v.ratioFrictionUp).reduce((acc, v) => acc + v) /
162              avgArr.length;
163
164            this.profile[i].fdData[index].force = sideForce * ratioAvgForce;
165
166            this.profile[i].fdData[index].friction =
167              sideForce * ratioAvgFriction * frictionCoeff;
168
169            tempForce += this.profile[i].fdData[index].force;
170            tempForce += this.profile[i].fdData[index].friction;
171
172            tempForceReverse += this.profile[i].fdData[index].force;
173            tempForceReverse -= this.profile[i].fdData[index].friction;
174
175            var springFrictionRev =
176              this.fdData[index].springForce *
177              ratioAvgFrictionUp *
178              frictionCoeff;
179
180            // tempForceReverse -= this.parameters.spring.enable
181            //   ? springFrictionRev
182            //   : 0;
183          }
184        }
185      }
186
187      if (this.profile.left && this.profile.right) {
188        var follower;
189        var profile;
190        if (this.profile.left.type === "SYMMETRICAL") {
191          var follower = this.profile.left;
192          var profile = this.profile.right;
193          this.profile.left.fdData[index] = {...this.profile.right.fdData[index]};
194        } else if (this.profile.right.type === "SYMMETRICAL") {
195          var follower = this.profile.right;
196          var profile = this.profile.left;
197          this.profile.right.fdData[index] = {...this.profile.left.fdData[index]};
198        }
199
200        if (follower && profile) {
201          follower.fdData[index].force = profile.fdData[index].force;
202          follower.fdData[index].friction = profile.fdData[index].friction;
203
204          tempForce += follower.fdData[index].force;
205          tempForce += follower.fdData[index].friction;
206
207          tempForceReverse += follower.fdData[index].force;
208          tempForceReverse -= follower.fdData[index].friction;
209
210          var springFrictionRev =
211            this.fdData[index].springForce * ratioAvgFrictionUp * frictionCoeff;
212
213          // tempForceReverse -= this.parameters.spring.enable
214          //   ? springFrictionRev
215          //   : 0;
216        }
217      }
218
219      this.fdData[index].force = tempForce;
220      this.fdData[index].forceReverse = tempForceReverse;
221      this.updateGallery = true;
222    }
223  }
224
225  smoothFD() {
226    this.fdData.forEach((d, index) => {
227      var springCoeff = calcSpringGradient(
228        this.parameters.spring.width,
229        this.parameters.spring.thickness
230      ); // FORMULA FOR MAIN SPRING COEFF HERE
231      this.fdData[index].springForce =
232        (this.fdData[index].d + this.parameters.slider.preloadBase) *
233        springCoeff;
234
235      var tempForce = this.parameters.spring.enable
236        ? this.fdData[index].springForce
237        : 0;
238
239      var tempForceReverse = this.parameters.spring.enable
240        ? this.fdData[index].springForce
241        : 0;
242
243      for (var i in this.profile) {
244        if (this.profile[i]) {
245          var sideCoeff = calcSideGradient(this.parameters.side.thickness);
246          var frictionCoeff = FRICTION_COEFFICIENT;
247          var sideForce = sideCoeff * this.profile[i].fdData[index].deflection;
248
249          var avgArr = [];
250          for (var j = -1; j <= 1; j++) {
251            if (this.profile[i].fdData[index + j]) {
252              avgArr.push(this.profile[i].fdData[index + j]);
253            }
254          }
255
256          var ratioAvgForce =
257            avgArr.map(v => v.ratioForce).reduce((acc, v) => acc + v) /
258            avgArr.length;
259          var ratioAvgFriction =
260            avgArr.map(v => v.ratioFriction).reduce((acc, v) => acc + v) /
261            avgArr.length;
262          var ratioAvgFrictionUp =
263            avgArr.map(v => v.ratioFrictionUp).reduce((acc, v) => acc + v) /
264            avgArr.length;
265
266          this.profile[i].fdData[index].force = sideForce * ratioAvgForce;
267
268          this.profile[i].fdData[index].friction =
269            sideForce * ratioAvgFriction * frictionCoeff;
270
271          tempForce += this.profile[i].fdData[index].force;
272          tempForce += this.profile[i].fdData[index].friction;
273
274          tempForceReverse += this.profile[i].fdData[index].force;
275          tempForceReverse -= this.profile[i].fdData[index].friction;
276
277          var springFrictionRev =
278            this.fdData[index].springForce * ratioAvgFrictionUp * frictionCoeff;
279          // tempForceReverse -= this.parameters.spring.enable
280          //   ? springFrictionRev
281          //   : 0;
282        }
283      }
284
285      if (this.fdData[index].force !== tempForce) {
286        this.fdData[index].force = tempForce;
287        this.fdData[index].forceReverse = tempForceReverse;
288        this.updateGallery = true;
289      }
290    });
291  }
292
293  updateBase() {
294    if (this.drawingBase) this.drawingBase.remove();
295    switch (this.sides) {
296      case "SINGLE":
297        this.drawingBase = generateCaseSingle(
298          this.parameters.case,
299          this.parameters.slider,
300          this.parameters.side,
301          this.parameters.spring
302        );
303        break;
304      case "DOUBLE":
305        this.drawingBase = generateCaseDouble(
306          this.parameters.case,
307          this.parameters.slider,
308          this.parameters.side,
309          this.parameters.side,
310          this.parameters.spring
311        );
312        break;
313    }
314
315    for (var i in this.profile) {
316      if (this.profile[i]) this.profile[i].updateBase();
317    }
318
319    if (!this.parameters.spring.enable)
320      this.drawingBase.children["SPRING"].remove();
321
322    this.initFD();
323
324    this.updateDisplay = true;
325    this.updateGallery = true;
326  }
327
328  updateView(i) {
329    if (this.drawingView) this.drawingView.remove();
330
331    switch (this.sides) {
332      case "SINGLE":
333        this.drawingView = generateViewCaseSingle(
334          this.parameters.case,
335          this.parameters.slider,
336          this.parameters.side,
337          this.parameters.spring,
338          this.fdData.length > 0 ? this.profile.left.fdData[i].angle : 0,
339          this.fdData.length > 0 ? this.fdData[i].d : 0
340        );
341
342        break;
343      case "DOUBLE":
344        this.drawingView = generateViewCaseDouble(
345          this.parameters.case,
346          this.parameters.slider,
347          this.parameters.side,
348          this.parameters.spring,
349          this.profile.left ? this.profile.left.fdData[i].angle : 0,
350          this.profile.right ? this.profile.right.fdData[i].angle : 0,
351          this.fdData.length > 0 ? this.fdData[i].d : 0
352        );
353        break;
354    }
355
356    for (var s in this.profile) {
357      if (this.profile[s]) {
358        if (this.profile[s].active) {
359          var p = this.profile[s];
360          if (p.base) p.base.strokeWidth = 0;
361          if (p.create) p.create.strokeWidth = 0;
362          if (p.active) p.active.strokeWidth = 0;
363          // p.updateProfile(i);
364          // p.setActiveProfile();
365
366          if (this.profile[s].activeView) this.profile[s].activeView.remove();
367          this.profile[s].activeView = this.profile[s].active.clone();
368          this.profile[s].activeView.translate(0, this.fdData[i].d);
369          this.drawingView.addChild(this.profile[s].activeView);
370          this.profile[s].updateProfile(i);
371        }
372      }
373    }
374
375    this.drawingBase.strokeWidth = 0;
376
377    // for (var j in this.profile) {
378    //   if (this.profile[j]) {
379    //     var p = this.profile[j];
380    //     if (p.base) p.base.strokeWidth = 0;
381    //     if (p.create) p.create.strokeWidth = 0;
382    //     if (p.active) p.active.strokeWidth = 0;
383    //     this.profile[j].updateProfile(i);
384    //   }
385    // }
386
387    this.drawingView.strokeColor = "rgba(0, 0, 0, 1)";
388    this.drawingView.strokeWidth = 1 / MIN_ZOOM;
389    this.drawingView.strokeCap = "round";
390    this.drawingView.strokeJoin = "round";
391
392    if (this.drawingFab) this.drawingFab.remove();
393
394    this.drawingFab = this.drawingView.clone();
395    this.drawingFab.children["CASE 1"].remove();
396    this.drawingFab.children["SPRING 1"].remove();
397    this.drawingFab.addChild(this.drawingBase.children["CASE"].clone());
398    if (this.parameters.spring.enable)
399      this.drawingFab.addChild(this.drawingBase.children["SPRING"].clone());
400    this.drawingFab.addChild(this.drawingBase.children["OUTER"].clone());
401    this.drawingFab.addChild(this.drawingBase.children["MASK"].clone());
402
403    var rect = this.drawingFab.bounds;
404    if (this.parameters.spring.enable)
405      this.drawingFab.children["SPRING 1"].translate(rect.width * 1.1, 0);
406    this.drawingFab.children["CASE 1"].translate(rect.width * 2.2, 0);
407    this.drawingFab.children["CIRCLE1 1"].translate(rect.width * 2.2, 0);
408    this.drawingFab.children["CIRCLE2 1"].translate(rect.width * 2.2, 0);
409    this.drawingFab.children["CIRCLE3 1"].translate(rect.width * 2.2, 0);
410    this.drawingFab.children["CIRCLE4 1"].translate(rect.width * 2.2, 0);
411    this.drawingFab.children["MASK 1"].translate(rect.width * 3.3, 0);
412    this.drawingFab.children["OUTER 1"].translate(rect.width * 4.4, 0);
413    var fabPos = new paper.Point(rect.x, rect.y);
414    this.drawingFab.translate(origin.subtract(fabPos));
415    this.drawingFab.strokeWidth = 0;
416
417    if (!this.parameters.spring.enable)
418      this.drawingView.children["SPRING"].remove();
419
420    this.updateDisplay = false;
421  }
422}
423
424class HapticProfile {
425  constructor(_parent, _side) {
426    this.parent = _parent;
427    this.side = _side;
428    this.base;
429
430    this.import;
431    this.create;
432    this.createView;
433    this.active;
434    this.activeView;
435    this.symmetrical;
436
437    this.type;
438
439    this.fdData = [];
440  }
441
442  updateBase() {
443    this.base = this.parent.drawingBase.children[this.side];
444    var spine = this.base.firstSegment.point.getDistance(
445      this.base.lastSegment.point
446    );
447    var arr = [
448      this.import,
449      this.create,
450      this.createView,
451      this.active,
452      this.activeView
453    ];
454
455    arr.forEach(p => {
456      if (p) {
457        var oldSpine = p.firstSegment.point.getDistance(p.lastSegment.point);
458        p.scale(1, spine / oldSpine, p.firstSegment.point);
459        p.translate(
460          this.base.firstSegment.point.subtract(p.firstSegment.point)
461        );
462      }
463    });
464  }
465
466  updateWidth(w) {
467    var rect = this.active.bounds;
468    var currW =
469      this.side === "LEFT"
470        ? this.active.firstSegment.point.x - rect.x
471        : rect.width - this.active.firstSegment.point.x + rect.x;
472    if (currW > 0) {
473      switch (this.type) {
474        case "IMPORT":
475          this.import.scale(w / currW, 1, this.import.firstSegment.point);
476          break;
477        case "CREATE":
478          this.create.scale(w / currW, 1, this.create.firstSegment.point);
479          break;
480      }
481      this.setActiveProfile();
482      this.parent.updateGallery = true;
483    }
484  }
485
486  get profileWidth() {
487    if (this.active) {
488      var rect = this.active.bounds;
489      var currW =
490        this.side === "LEFT"
491          ? this.active.firstSegment.point.x - rect.x
492          : rect.width - this.active.firstSegment.point.x + rect.x;
493      return currW;
494    }
495    return undefined;
496  }
497
498  updateProfile(i) {
499    switch (this.type) {
500      case "CREATE":
501        var t = new paper.Point(
502          0,
503          this.fdData[i].d -
504            (this.createView.firstSegment.point.y -
505              this.base.firstSegment.point.y)
506        );
507        this.createView.translate(t);
508        this.createView.strokeColor = "rgba(0, 0, 0, 1)";
509        this.createView.strokeWidth = 0.1;
510
511        this.create.remove();
512        this.create = new paper.Path([...this.createView.segments]);
513        this.create.translate(
514          t.x,
515          this.base.firstSegment.point.y - this.create.firstSegment.point.y
516        );
517        this.create.selected = false;
518        this.create.strokeWidth = 0;
519        this.active = this.create;
520        break;
521      case "SYMMETRICAL":
522        this.setActiveProfile();
523        if (this.createView) {
524          this.createView.selected = false;
525          this.createView.remove();
526        }
527        break;
528      default:
529        if (this.createView) {
530          this.createView.selected = false;
531          this.createView.remove();
532        }
533        break;
534    }
535  }
536
537  setImport(path) {
538    if (this.import) this.import.remove();
539    this.import = path;
540  }
541
542  setActiveType(type) {
543    this.type = type;
544    this.setActiveProfile();
545  }
546
547  setActiveProfile() {
548    switch (this.type) {
549      case "IMPORT":
550        this.active = this.import ? this.import : undefined;
551        break;
552      case "CREATE":
553        if (this.create) {
554          this.active = this.create;
555          if (this.createView) this.createView.remove();
556          this.createView = this.create.clone();
557        } else {
558          this.create = this.base.clone();
559          this.createView = this.create.clone();
560          this.active = this.create;
561        }
562        this.createView.selected = true;
563        break;
564      case "SYMMETRICAL":
565        var path =
566          this.parent.sides === "DOUBLE"
567            ? this.side === "LEFT"
568              ? this.parent.profile.right.active
569              : this.parent.profile.left.active
570            : undefined;
571        if (path) {
572          path = path.clone();
573          path = path.translate(
574            this.base.firstSegment.point.subtract(path.firstSegment.point)
575          );
576          path = path.scale(-1, 1, path.firstSegment.point);
577        }
578        this.active = path ? path : undefined;
579        break;
580    }
581  }
582
583  calcForce(i, tolerance) {
584    if (this.active) {
585      i = Math.floor(i);
586      if (i < this.fdData.length) {
587        var path = this.active.clone();
588        if (this.side === "RIGHT") {
589          path = path.translate(
590            this.parent.profile.left.base.firstSegment.point.subtract(
591              path.firstSegment.point
592            )
593          );
594          path = path.scale(-1, 1, path.firstSegment.point);
595        }
596
597        var replacementSegments = [];
598        path.segments.forEach((s, i, arr) => {
599          if (i === 0 || i === arr.length - 1) {
600            replacementSegments.push(s);
601          } else if (i > 0 && i < arr.length - 1) {
602            var os = path.getOffsetOf(s.point);
603            var osNext = path.getOffsetOf(arr[i + 1].point);
604            var os1 = os - 1.5 / DISPLACEMENT_DENSITY;
605            var os2 = os + 1.5 / DISPLACEMENT_DENSITY;
606            if (os1 > 0 && os2 < osNext) {
607              var p1 = path.getPointAt(os1);
608              var p2 = path.getPointAt(os2);
609              var tan1 = path
610                .getTangentAt(os1)
611                .normalize(0.7 / DISPLACEMENT_DENSITY);
612              var tan2 = path
613                .getTangentAt(os2)
614                .normalize(0.7 / DISPLACEMENT_DENSITY);
615              tan2 = tan2.rotate(180);
616
617              var tempPath1 = path.clone();
618              var tempPath2 = path.clone();
619              tempPath1.divideAt(os1);
620              tempPath2.divideAt(os2);
621
622              var s1 = tempPath1.segments[i];
623              var s2 = tempPath2.segments[i + 1];
624
625              s1.handleOut = tan1;
626              s2.handleIn = tan2;
627
628              replacementSegments.push(s1);
629              replacementSegments.push(s2);
630
631              tempPath1.remove();
632              tempPath2.remove();
633            } else {
634              replacementSegments.push(s);
635            }
636          }
637        });
638
639        path.segments = [...replacementSegments];
640
641        var pathClosed = path.clone();
642        pathClosed.lineBy(100, 0);
643        pathClosed.reverse();
644        pathClosed.lineBy(100, 0);
645        pathClosed.reverse();
646        pathClosed.closePath();
647
648        path.translate(0, this.fdData[i].d);
649
650        pathClosed.translate(0, this.fdData[i].d);
651
652        var arm = this.parent.drawingBase.children["ARM"].clone();
653        var pivot = arm.firstSegment.point;
654
655        arm.scale(SCALE_UP, pivot);
656        path.scale(SCALE_UP, pivot);
657        pathClosed.scale(SCALE_UP, pivot);
658
659        var tip = new paper.Path([
660          arm.segments[2].point,
661          arm.segments[3].point,
662          arm.segments[4].point
663        ]);
664
665        var tipPtArr = [];
666
667        for (var j = 0; j <= SPRING_SAMPLING_DENSITY; j++) {
668          var offset = j * (tip.length / SPRING_SAMPLING_DENSITY);
669          tipPtArr.push(tip.getPointAt(offset));
670        }
671
672        var contactPoint = this.calcContact(
673          i,
674          tolerance,
675          0,
676          pivot,
677          tipPtArr,
678          path,
679          pathClosed,
680          0,
681          this.parent.parameters.side.angle
682        );
683
684        switch (contactPoint.type) {
685          case "contact":
686            if (contactPoint.springAngle !== this.fdData[i].angle)
687              this.parent.updateDisplay = true;
688
689            var armVectorInit = new paper.Point(
690              arm.lastSegment.point.subtract(arm.firstSegment.point)
691            );
692            var armVector = armVectorInit.rotate(
693              contactPoint.springAngle,
694              new paper.Point(0, 0)
695            );
696            var armVectorN = armVector.rotate(-90);
697            var angleBetween = armVectorN.getDirectedAngle(contactPoint.normal);
698            var profileRatio = Math.abs(
699              Math.cos((angleBetween * Math.PI) / 180)
700            );
701            var yRatio = contactPoint.normal.y / contactPoint.normal.length;
702            var frictionRatio = Math.abs(
703              contactPoint.tangent.y / contactPoint.tangent.length
704            );
705
706            this.fdData[i].angle = contactPoint.springAngle;
707            this.fdData[i].deflection =
708              Math.abs(armVector.x - armVectorInit.x) / SCALE_UP;
709
710            this.fdData[i].ratioForce = yRatio * profileRatio;
711            this.fdData[i].ratioFriction = frictionRatio * profileRatio;
712
713            var angleFriction = contactPoint.tangent.getDirectedAngle(
714              new paper.Point(0, -1)
715            );
716            var frictionRatioUp =
717              angleFriction < 0 && Math.abs(angleFriction) > 0.1
718                ? -Math.cos((angleFriction / 180) * Math.PI) *
719                  contactPoint.tangent.y
720                : 0;
721            this.fdData[i].ratioFrictionUp = frictionRatioUp;
722
723            break;
724
725          case "no contact":
726            this.fdData[i].angle = 0;
727            this.fdData[i].deflection = 0;
728            this.fdData[i].ratioForce = 0;
729            this.fdData[i].ratioFriction = 0;
730            this.fdData[i].ratioFrictionUp = 0;
731            break;
732        }
733
734        // path.scale(1 / SCALE_UP, pivot);
735        // if (i === 0) {
736        //   path.selected = true;
737        // }
738
739        path.remove();
740        pathClosed.remove();
741        arm.remove();
742        tip.remove();
743      }
744    }
745  }
746
747  calcContact(
748    i,
749    tolerance,
750    count,
751    pivot,
752    tipPtArr,
753    path,
754    pathClosed,
755    angleStart,
756    angleEnd
757  ) {
758    var tipCullNull = tipPtArr.filter(p => p !== null);
759    var tipStart = tipCullNull.map(p => p.rotate(angleStart, pivot));
760    var angleMiddle = (angleStart + angleEnd) / 2;
761    var tipMiddle = tipCullNull.map(p => p.rotate(angleMiddle, pivot));
762    var tipEnd = tipCullNull.map(p => p.rotate(angleEnd, pivot));
763
764    var iterate = false;
765
766    var containsStart = false;
767    var containsMiddle = false;
768    var containsEnd = false;
769
770    tipStart.forEach((p, k, arr) => {
771      if (k === arr.length - 1 || k % SPRING_SAMPLING_SUBDENSITY === 0) {
772        if (pathClosed.contains(p)) {
773          containsStart = true;
774        }
775      }
776    });
777
778    tipMiddle.forEach((p, k, arr) => {
779      if (k === arr.length - 1 || k % SPRING_SAMPLING_SUBDENSITY === 0) {
780        if (pathClosed.contains(p)) {
781          containsMiddle = true;
782        }
783      }
784    });
785
786    tipEnd.forEach((p, k, arr) => {
787      if (k === arr.length - 1 || k % SPRING_SAMPLING_SUBDENSITY === 0) {
788        if (pathClosed.contains(p)) {
789          containsEnd = true;
790        }
791      }
792    });
793
794    if (containsStart && containsMiddle && containsEnd) {
795      return { offset: undefined, type: "exceed" };
796    } else if (!containsStart && !containsMiddle && !containsEnd) {
797      return { offset: undefined, type: "no contact" };
798    } else {
799      var aS, aE;
800      var tip;
801      if (containsStart && !containsMiddle) {
802        tip = tipMiddle.map(p => p);
803        aS = angleStart;
804        aE = angleMiddle;
805      } else if (containsMiddle && !containsEnd) {
806        tip = tipEnd.map(p => p);
807        aS = angleMiddle;
808        aE = angleEnd;
809      }
810
811      if (count > INIT_ITERATION) {
812        for (let p of tip) {
813          var pt = path.getNearestPoint(p);
814          if (pt.getDistance(p) < tolerance) {
815            var os = path.getOffsetOf(pt);
816            var nor = path.getNormalAt(os).normalize();
817            var tan = path.getTangentAt(os).normalize();
818
819            return {
820              point: pt,
821              offset: os,
822              normal: nor,
823              tangent: tan,
824              springAngle: aS,
825              type: "contact"
826            };
827          }
828        }
829
830        iterate = true;
831        //         var cpArr = tip.map(p => {
832        //           var pt = path.getNearestPoint(p);
833        //           return { point: pt, distance: pt.getDistance(p) };
834        //         });
835
836        //         cpArr.sort((a, b) => a.distance - b.distance);
837
838        //         if (cpArr[0].distance < tolerance) {
839        //           var cpt = cpArr[0].point;
840        //           var os = path.getOffsetOf(cpt);
841        //           var nor = path.getNormalAt(os).normalize();
842        //           var tan = path.getTangentAt(os).normalize();
843
844        //           return {
845        //             point: cpt,
846        //             offset: os,
847        //             normal: nor,
848        //             tangent: tan,
849        //             springAngle: aS,
850        //             type: "contact"
851        //           };
852        //         } else iterate = true;
853      } else iterate = true;
854    }
855
856    if (iterate) {
857      count++;
858      return this.calcContact(
859        i,
860        tolerance,
861        count,
862        pivot,
863        tipPtArr,
864        path,
865        pathClosed,
866        aS,
867        aE
868      );
869    }
870  }
871}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.