@woosh/meep-engine 2.43.9 → 2.43.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/core/binary/is_data_url.js +12 -0
  2. package/core/binary/url_to_data_url.js +28 -0
  3. package/core/collection/queue/Deque.js +2 -2
  4. package/core/geom/3d/shape/AbstractShape3D.js +9 -0
  5. package/core/geom/3d/shape/TransformedShape3D.js +8 -0
  6. package/core/geom/3d/shape/UnionShape3D.js +35 -0
  7. package/core/geom/3d/shape/UnitCubeShape3D.js +10 -0
  8. package/core/geom/3d/shape/UnitSphereShape3D.js +10 -0
  9. package/editor/ecs/component/editors/ImagePathEditor.js +15 -66
  10. package/editor/ecs/component/editors/LargeStrongEditor.js +107 -0
  11. package/engine/graphics/ecs/path/PathDisplaySystem.js +92 -4
  12. package/engine/graphics/ecs/path/tube/build/{GeometryOutput.js → StreamGeometryBuilder.js} +1 -1
  13. package/engine/graphics/ecs/path/tube/build/TubePathBuilder.js +16 -14
  14. package/engine/graphics/ecs/path/tube/build/build_geometry_catmullrom.spec.js +32 -0
  15. package/engine/graphics/ecs/path/tube/build/computeFrenetFrames.js +6 -1
  16. package/engine/graphics/ecs/path/tube/build/makeTubeGeometry.js +6 -4
  17. package/engine/graphics/ecs/path/tube/build/make_cap.js +10 -10
  18. package/engine/graphics/ecs/path/tube/build/make_ring_faces.js +1 -1
  19. package/engine/graphics/ecs/path/tube/build/make_ring_vertices.js +1 -1
  20. package/engine/graphics/impostors/octahedral/shader/ImpostorShaderV0.js +143 -27
  21. package/engine/graphics/micron/plugin/shaded_geometry/MicronShadedGeometryRenderAdapter.js +5 -0
  22. package/engine/graphics/particles/particular/engine/utils/volume/ParticleVolume.d.ts +3 -1
  23. package/engine/graphics/particles/particular/engine/utils/volume/ParticleVolume.js +148 -5
  24. package/engine/graphics/particles/particular/engine/utils/volume/SamplingFunctionKind.d.ts +4 -0
  25. package/engine/graphics/particles/particular/engine/utils/volume/SamplingFunctionKind.js +8 -0
  26. package/engine/graphics/particles/particular/engine/utils/volume/prototypeParticleVolume.js +7 -4
  27. package/engine/navigation/ecs/components/Path.spec.js +21 -0
  28. package/package.json +1 -1
  29. package/samples/terrain/from_image_2.js +33 -0
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Returns true if string is a data URL
3
+ * @param {string} url
4
+ * @return {boolean}
5
+ */
6
+ export function is_data_url(url) {
7
+ if (typeof url !== "string") {
8
+ return false;
9
+ }
10
+
11
+ return url.startsWith('data:');
12
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ *
3
+ * @param {string} url
4
+ * @returns {string}
5
+ */
6
+ export async function url_to_data_url(url) {
7
+ return fetch(url)
8
+ .then(response => {
9
+ return response.blob();
10
+ })
11
+ .then(blob => {
12
+ const fr = new FileReader();
13
+
14
+ return new Promise((resolve, reject) => {
15
+
16
+ fr.onload = () => {
17
+ resolve(fr.result);
18
+ };
19
+
20
+ fr.onerror = reject;
21
+ fr.onabort = reject;
22
+
23
+ fr.readAsDataURL(blob);
24
+
25
+ });
26
+
27
+ });
28
+ }
@@ -302,7 +302,7 @@ Deque.prototype.peek = Deque.prototype.getFirst;
302
302
  Deque.prototype.push = Deque.prototype.addFirst;
303
303
  Deque.prototype.pop = Deque.prototype.removeFirst;
304
304
 
305
- /*
306
- Standard queue method
305
+ /**
306
+ * Standard queue method
307
307
  */
308
308
  Deque.prototype.add = Deque.prototype.addLast;
@@ -63,6 +63,15 @@ export class AbstractShape3D {
63
63
  throw new Error('Not Implemented');
64
64
  }
65
65
 
66
+ /**
67
+ *
68
+ * @param {number[]|ArrayLike<number>|Float32Array|Float64Array} result format: x0,y0,z0,x1,y1,z1
69
+ * @returns {void}
70
+ */
71
+ compute_bounding_box(result) {
72
+ throw new Error('Not Implemented');
73
+ }
74
+
66
75
  /**
67
76
  * @template {AbstractShape3D} T
68
77
  * @param {T} other
@@ -3,6 +3,7 @@ import { AbstractShape3D } from "./AbstractShape3D.js";
3
3
  import { sign } from "../../../math/sign.js";
4
4
  import { isArrayEqualStrict } from "../../../collection/array/isArrayEqualStrict.js";
5
5
  import { computeHashFloatArray } from "../../../math/hash/computeHashFloatArray.js";
6
+ import { aabb3_matrix4_project } from "../aabb/aabb3_matrix4_project.js";
6
7
 
7
8
  /**
8
9
  *
@@ -108,6 +109,13 @@ export class TransformedShape3D extends AbstractShape3D {
108
109
  return this.__subject.volume * scale_modifier;
109
110
  }
110
111
 
112
+ compute_bounding_box(result) {
113
+ const tmp = [];
114
+ this.__subject.compute_bounding_box(tmp);
115
+
116
+ aabb3_matrix4_project(result, tmp, this.__matrix);
117
+ }
118
+
111
119
  signed_distance_gradient_at_point(result, point) {
112
120
  // transform point to subject's local space
113
121
  vec3.transformMat4(scratch_v3_0, point, this.__inverse_matrix);
@@ -5,6 +5,7 @@ import { compareNumbers } from "../../../primitives/numbers/compareNumbers.js";
5
5
  import { min2 } from "../../../math/min2.js";
6
6
  import { compute_signed_distance_gradient_by_sampling } from "./util/compute_signed_distance_gradient_by_sampling.js";
7
7
  import { isArrayEqual } from "../../../collection/array/isArrayEqual.js";
8
+ import { max2 } from "../../../math/max2.js";
8
9
 
9
10
  /**
10
11
  * To avoid severe performance overhead, we limit number of possible rejections
@@ -44,6 +45,40 @@ export class UnionShape3D extends AbstractShape3D {
44
45
  this.__child_volumes = [];
45
46
  }
46
47
 
48
+ compute_bounding_box(result) {
49
+ let x0 = Infinity, y0 = Infinity, z0 = Infinity,
50
+ x1 = -Infinity, y1 = -Infinity, z1 = -Infinity;
51
+
52
+ const children = this.children;
53
+ const n = children.length;
54
+
55
+ const tmp_aabb3 = new Float32Array(6);
56
+
57
+ for (let i = 0; i < n; i++) {
58
+ const child = children[i];
59
+
60
+ child.compute_bounding_box(tmp_aabb3);
61
+
62
+ x0 = min2(x0, tmp_aabb3[0]);
63
+ y0 = min2(y0, tmp_aabb3[1]);
64
+ z0 = min2(z0, tmp_aabb3[2]);
65
+
66
+ x1 = max2(x1, tmp_aabb3[3]);
67
+ y1 = max2(y1, tmp_aabb3[4]);
68
+ z1 = max2(z1, tmp_aabb3[5]);
69
+
70
+ }
71
+
72
+
73
+ result[0] = x0;
74
+ result[1] = y0;
75
+ result[2] = z0;
76
+
77
+ result[3] = x1;
78
+ result[4] = y1;
79
+ result[5] = z1;
80
+ }
81
+
47
82
  /**
48
83
  *
49
84
  * @private
@@ -13,6 +13,16 @@ export class UnitCubeShape3D extends AbstractShape3D {
13
13
  return 1;
14
14
  }
15
15
 
16
+ compute_bounding_box(result) {
17
+ result[0] = -0.5;
18
+ result[1] = -0.5;
19
+ result[2] = -0.5;
20
+
21
+ result[3] = 0.5;
22
+ result[4] = 0.5;
23
+ result[5] = 0.5;
24
+ }
25
+
16
26
  nearest_point_on_surface(result, reference) {
17
27
  const r_x = reference[0];
18
28
  const r_y = reference[1];
@@ -15,6 +15,16 @@ export class UnitSphereShape3D extends AbstractShape3D {
15
15
  return 3.14159265;
16
16
  }
17
17
 
18
+ compute_bounding_box(result) {
19
+ result[0] = -0.5;
20
+ result[1] = -0.5;
21
+ result[2] = -0.5;
22
+
23
+ result[3] = 0.5;
24
+ result[4] = 0.5;
25
+ result[5] = 0.5;
26
+ }
27
+
18
28
  nearest_point_on_surface(result, reference) {
19
29
 
20
30
  const r_x = reference[0];
@@ -1,72 +1,18 @@
1
1
  import { TypeEditor } from "../TypeEditor.js";
2
- import { StringEditor } from "./primitive/StringEditor.js";
3
2
  import EmptyView from "../../../../view/elements/EmptyView.js";
4
3
  import ImageView from "../../../../view/elements/image/ImageView.js";
5
- import ObservedString from "../../../../core/model/ObservedString.js";
6
- import { ObservedStringEditor } from "./ObservedStringEditor.js";
7
4
  import ButtonView from "../../../../view/elements/button/ButtonView.js";
8
-
9
- /**
10
- *
11
- * @param {string} url
12
- * @return {boolean}
13
- */
14
- function is_data_url(url) {
15
- if (typeof url !== "string") {
16
- return false;
17
- }
18
-
19
- return url.startsWith('data:');
20
- }
21
-
22
- /**
23
- *
24
- * @param {string} url
25
- * @returns {string}
26
- */
27
- async function url_to_data_url(url) {
28
- return fetch(url)
29
- .then(response => {
30
- return response.blob();
31
- })
32
- .then(blob => {
33
- const fr = new FileReader();
34
-
35
- return new Promise((resolve, reject) => {
36
-
37
- fr.onload = () => {
38
- resolve(fr.result);
39
- };
40
-
41
- fr.onerror = reject;
42
- fr.onabort = reject;
43
-
44
- fr.readAsDataURL(blob);
45
-
46
- });
47
-
48
- });
49
- }
5
+ import { url_to_data_url } from "../../../../core/binary/url_to_data_url.js";
6
+ import { is_data_url } from "../../../../core/binary/is_data_url.js";
7
+ import { LargeStrongEditor } from "./LargeStrongEditor.js";
50
8
 
51
9
  export class ImagePathEditor extends TypeEditor {
52
10
  inline = true;
53
11
 
54
12
  build(parent, field, registry) {
55
- let url_editor;
56
-
57
- if (field.type === String) {
13
+ const url_editor = new LargeStrongEditor();
58
14
 
59
- url_editor = new StringEditor();
60
-
61
- } else if (field.type === ObservedString) {
62
-
63
- url_editor = new ObservedStringEditor();
64
-
65
- } else {
66
- throw new Error(`Unsupported type`);
67
- }
68
-
69
- const vEditor = url_editor.build(parent, field, registry);
15
+ const editor = url_editor.build(parent, field, registry);
70
16
 
71
17
  const r = new EmptyView({
72
18
  css: {
@@ -87,14 +33,14 @@ export class ImagePathEditor extends TypeEditor {
87
33
  field.adapter.write(parent, field.name, url);
88
34
 
89
35
  // update editor value
90
- vEditor.el.value = url;
36
+ editor.methods.set_value(url);
91
37
 
92
- update();
38
+ input_changed();
93
39
  }
94
40
  });
95
41
 
96
42
  b_toDataURL.attr({
97
- title:'Convert URL into DATA_URL, embedding the data. This allows to bypass CORRS restrictions if the URL points to a different origin from the one where the texture resides'
43
+ title: 'Convert URL into DATA_URL, embedding the data. This allows to bypass CORRS restrictions if the URL points to a different origin from the one where the texture resides'
98
44
  });
99
45
 
100
46
  function update_data_url_status() {
@@ -116,15 +62,18 @@ export class ImagePathEditor extends TypeEditor {
116
62
  }
117
63
  }
118
64
 
119
- function update() {
120
- vPreview.__setSource(vEditor.el.value);
65
+ function input_changed() {
66
+ const url = field.adapter.read(parent, field.name);
67
+
68
+ vPreview.__setSource(url);
69
+
121
70
  update_data_url_status();
122
71
  }
123
72
 
124
73
  r.addChild(vPreview);
125
- r.addChild(vEditor);
74
+ r.addChild(editor.view);
126
75
 
127
- vEditor.el.addEventListener('input', update);
76
+ editor.signals.input_changed.add(input_changed);
128
77
 
129
78
  r.on.linked.add(update_data_url_status);
130
79
 
@@ -0,0 +1,107 @@
1
+ import { TypeEditor } from "../TypeEditor.js";
2
+ import { StringEditor } from "./primitive/StringEditor.js";
3
+ import ObservedString from "../../../../core/model/ObservedString.js";
4
+ import { ObservedStringEditor } from "./ObservedStringEditor.js";
5
+ import EmptyView from "../../../../view/elements/EmptyView.js";
6
+ import LabelView from "../../../../view/common/LabelView.js";
7
+ import { MouseEvents } from "../../../../engine/input/devices/events/MouseEvents.js";
8
+ import Signal from "../../../../core/events/signal/Signal.js";
9
+
10
+ /**
11
+ * If path is larger than this - it will be truncated and displayed as non-input
12
+ * @readonly
13
+ * @type {number}
14
+ */
15
+ const MAX_PATH_LENGTH_EDITABLE = 256;
16
+ /**
17
+ * How long to crop to
18
+ * @readonly
19
+ * @type {number}
20
+ */
21
+ const MAX_CROP_LENGTH = 32;
22
+
23
+ export class LargeStrongEditor extends TypeEditor {
24
+ inline = true;
25
+
26
+ build(parent, field, registry) {
27
+ let url_editor;
28
+
29
+ if (field.type === String) {
30
+
31
+ url_editor = new StringEditor();
32
+
33
+ } else if (field.type === ObservedString) {
34
+
35
+ url_editor = new ObservedStringEditor();
36
+
37
+ } else {
38
+ throw new Error(`Unsupported type`);
39
+ }
40
+
41
+ let in_focus = false;
42
+
43
+ const vEditor = url_editor.build(parent, field, registry);
44
+
45
+ const truncated_url = new ObservedString('');
46
+ const vTruncated = new LabelView(truncated_url);
47
+
48
+ vTruncated.el.addEventListener(MouseEvents.Click, () => {
49
+ in_focus = true;
50
+ update();
51
+ vEditor.el.focus();
52
+ });
53
+ vEditor.el.addEventListener('focusin', () => {
54
+ in_focus = true;
55
+ update();
56
+ });
57
+ vEditor.el.addEventListener('blur', () => {
58
+ in_focus = false;
59
+ update();
60
+ });
61
+
62
+ const r = new EmptyView();
63
+
64
+ function update() {
65
+ const url = field.adapter.read(parent, field.name);
66
+
67
+ if (typeof url === "string" && url.length > MAX_PATH_LENGTH_EDITABLE && !in_focus) {
68
+ r.removeChild(vEditor);
69
+
70
+ truncated_url.set('[...]'+url.slice(0, MAX_CROP_LENGTH));
71
+
72
+ if (!r.hasChild(vTruncated)) {
73
+ r.addChild(vTruncated);
74
+ }
75
+
76
+ } else if(!r.hasChild(vEditor)){
77
+ r.removeChild(vTruncated);
78
+ r.addChild(vEditor);
79
+ }
80
+ }
81
+
82
+ const input_changed = new Signal();
83
+
84
+ vEditor.el.addEventListener('input', () => {
85
+
86
+ const value = vEditor.el.value;
87
+
88
+ input_changed.send1(value);
89
+
90
+ update();
91
+ });
92
+ r.on.linked.add(update);
93
+
94
+ return {
95
+ view: r, signals: {
96
+ input_changed
97
+ },
98
+ methods: {
99
+ set_value(v) {
100
+ field.adapter.write(parent, field.name, v);
101
+ update();
102
+ vEditor.el.value = v;
103
+ }
104
+ }
105
+ };
106
+ }
107
+ }
@@ -11,6 +11,7 @@ import { RibbonPathBuilder } from "./ribbon/RibbonPathBuilder.js";
11
11
  import { PathEvents } from "../../../navigation/ecs/components/PathEvents.js";
12
12
  import { assert } from "../../../../core/assert.js";
13
13
  import { TubePathBuilder } from "./tube/build/TubePathBuilder.js";
14
+ import { Deque } from "../../../../core/collection/queue/Deque.js";
14
15
 
15
16
  const builders = {
16
17
  [PathDisplayType.None]: function (style, path, result) {
@@ -82,6 +83,14 @@ const builders = {
82
83
  }
83
84
  };
84
85
 
86
+ /**
87
+ * Maximum amount of time allowed per system tick to spend on processing update queue
88
+ * in milliseconds
89
+ * @readonly
90
+ * @type {number}
91
+ */
92
+ const UPDATE_PROCESSING_BUDGET_MS = 10;
93
+
85
94
  class PathDisplayContext extends SystemEntityContext {
86
95
  constructor() {
87
96
  super();
@@ -100,6 +109,8 @@ class PathDisplayContext extends SystemEntityContext {
100
109
  const ownedEntities = this.__owned_entities;
101
110
  ownedEntities.splice(0, ownedEntities.length);
102
111
 
112
+ // todo check that the main entity still exists before we decide to spawn new entities
113
+
103
114
  /**
104
115
  * @type {PathDisplay}
105
116
  */
@@ -180,6 +191,16 @@ class PathDisplayContext extends SystemEntityContext {
180
191
  this.__build();
181
192
  }
182
193
 
194
+ request_update() {
195
+ /**
196
+ *
197
+ * @type {PathDisplaySystem}
198
+ */
199
+ const system = this.system;
200
+
201
+ system.request_entity_update(this.entity);
202
+ }
203
+
183
204
  link() {
184
205
  super.link();
185
206
 
@@ -189,8 +210,8 @@ class PathDisplayContext extends SystemEntityContext {
189
210
 
190
211
  const entity = this.entity;
191
212
 
192
- ecd.addEntityEventListener(entity, PathEvents.Changed, this.rebuild, this);
193
- ecd.addEntityEventListener(entity, PathDisplayEvents.Changed, this.rebuild, this);
213
+ ecd.addEntityEventListener(entity, PathEvents.Changed, this.request_update, this);
214
+ ecd.addEntityEventListener(entity, PathDisplayEvents.Changed, this.request_update, this);
194
215
  }
195
216
 
196
217
  unlink() {
@@ -200,8 +221,16 @@ class PathDisplayContext extends SystemEntityContext {
200
221
 
201
222
  const entity = this.entity;
202
223
 
203
- ecd.removeEntityEventListener(entity, PathEvents.Changed, this.rebuild, this);
204
- ecd.removeEntityEventListener(entity, PathDisplayEvents.Changed, this.rebuild, this);
224
+ ecd.removeEntityEventListener(entity, PathEvents.Changed, this.request_update, this);
225
+ ecd.removeEntityEventListener(entity, PathDisplayEvents.Changed, this.request_update, this);
226
+
227
+ /**
228
+ *
229
+ * @type {PathDisplaySystem}
230
+ */
231
+ const system = this.system;
232
+
233
+ system.cancel_entity_update(entity);
205
234
 
206
235
  // destroy existing owned entities
207
236
  this.__destroy_existing_entities();
@@ -238,6 +267,35 @@ export class PathDisplaySystem extends AbstractContextSystem {
238
267
  * @type {Reference<RibbonXPlugin>}
239
268
  */
240
269
  this.plugin = null;
270
+
271
+ /**
272
+ * Deferred queue of entities slated to be rebuilt
273
+ * @type {Deque<number>}
274
+ * @private
275
+ */
276
+ this.__rebuild_queue = new Deque();
277
+ }
278
+
279
+ /**
280
+ * Request that path displays be rebuilt
281
+ * @package
282
+ * @param {number} entity
283
+ */
284
+ request_entity_update(entity) {
285
+ assert.isNonNegativeInteger(entity, 'entity');
286
+
287
+ if (!this.__rebuild_queue.has(entity)) {
288
+ this.__rebuild_queue.add(entity);
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Cancel any pending updates
294
+ * @package
295
+ * @param {number} entity
296
+ */
297
+ cancel_entity_update(entity) {
298
+ this.__rebuild_queue.remove(entity);
241
299
  }
242
300
 
243
301
  async startup(entityManager, readyCallback, errorCallback) {
@@ -253,4 +311,34 @@ export class PathDisplaySystem extends AbstractContextSystem {
253
311
 
254
312
  super.shutdown(entityManager, readyCallback, errorCallback);
255
313
  }
314
+
315
+ update(time_delta) {
316
+ // process update queue
317
+
318
+ const queue = this.__rebuild_queue;
319
+ if (queue.isEmpty()) {
320
+ return;
321
+ }
322
+
323
+ const t0 = performance.now();
324
+
325
+ do {
326
+ // note that we don't need to check if the queue is empty here for the first iteration, as we already checked earlier
327
+ const entity = queue.pop();
328
+
329
+ /**
330
+ *
331
+ * @type {PathDisplayContext|undefined}
332
+ */
333
+ const ctx = this.__getEntityContext(entity);
334
+
335
+ // check that entity still exists
336
+ if (ctx === undefined) {
337
+ continue;
338
+ }
339
+
340
+ ctx.rebuild();
341
+
342
+ } while ((performance.now() - t0) < UPDATE_PROCESSING_BUDGET_MS && !queue.isEmpty());
343
+ }
256
344
  }
@@ -4,7 +4,7 @@ import { BufferAttribute, BufferGeometry } from "three";
4
4
  /**
5
5
  * Helper class, allowing us to treat geometry build process as a stream without having to create intermediate arrays and performing unnecessary copying
6
6
  */
7
- export class GeometryOutput {
7
+ export class StreamGeometryBuilder {
8
8
  constructor() {
9
9
  /**
10
10
  *
@@ -1,4 +1,4 @@
1
- import { BufferGeometry, MeshBasicMaterial, MeshMatcapMaterial, MeshStandardMaterial, Vector3 } from "three";
1
+ import { BufferGeometry, MeshBasicMaterial, MeshMatcapMaterial, MeshStandardMaterial } from "three";
2
2
  import { InterpolationType } from "../../../../../navigation/ecs/components/InterpolationType.js";
3
3
  import EntityBuilder from "../../../../../ecs/EntityBuilder.js";
4
4
  import { Transform } from "../../../../../ecs/transform/Transform.js";
@@ -11,6 +11,7 @@ import { ShadedGeometryFlags } from "../../../mesh-v2/ShadedGeometryFlags.js";
11
11
  import { m3_rm_compose_transform } from "../../../../../../view/m3_rm_compose_transform.js";
12
12
  import { fix_shape_normal_order } from "./fix_shape_normal_order.js";
13
13
  import { compute_smooth_profile_normals } from "./compute_smooth_profile_normals.js";
14
+ import { assert } from "../../../../../../core/assert.js";
14
15
 
15
16
  /**
16
17
  *
@@ -28,6 +29,9 @@ function make_geometry_segment(
28
29
  shape, shape_normal, shape_transform,
29
30
  segment_start, segment_end
30
31
  ) {
32
+ assert.notNaN(segment_start, 'segment_start');
33
+ assert.notNaN(segment_end, 'segment_end');
34
+ assert.greaterThanOrEqual(segment_end, segment_start, 'segment_start must be <= segment_end');
31
35
 
32
36
  const interpolation = path_component.interpolation;
33
37
 
@@ -129,8 +133,6 @@ export class TubePathBuilder {
129
133
  // generate three.js curve from path
130
134
  const path_component = this.path;
131
135
 
132
- const three_points = [];
133
-
134
136
  const pointCount = path_component.getPointCount();
135
137
 
136
138
  if (pointCount < 2) {
@@ -138,15 +140,6 @@ export class TubePathBuilder {
138
140
  return;
139
141
  }
140
142
 
141
- for (let i = 0; i < pointCount; i++) {
142
- const v3 = new Vector3();
143
-
144
- path_component.getPosition(i, v3);
145
-
146
- three_points.push(v3);
147
- }
148
-
149
-
150
143
  const material_def = make_material(style, this.assetManager);
151
144
 
152
145
  material_def.color.set(style.color.toUint());
@@ -172,8 +165,17 @@ export class TubePathBuilder {
172
165
  for (let i = 0; i < segment_count; i++) {
173
166
  const i2 = i * 2;
174
167
 
175
- const segment_start = style.path_mask[i2];
176
- const segment_end = style.path_mask[i2 + 1];
168
+ let segment_start = style.path_mask[i2];
169
+ let segment_end = style.path_mask[i2 + 1];
170
+
171
+ // check for wrong ordering
172
+ if (segment_end < segment_start) {
173
+ // violated interval constraint, re-order
174
+ const t = segment_end;
175
+
176
+ segment_end = segment_start;
177
+ segment_start = t;
178
+ }
177
179
 
178
180
  const geometry = make_geometry_segment(
179
181
  path_component, style,
@@ -0,0 +1,32 @@
1
+ import Path from "../../../../../navigation/ecs/components/Path.js";
2
+ import { build_geometry_catmullrom } from "./build_geometry_catmullrom.js";
3
+ import { TubePathStyle } from "../TubePathStyle.js";
4
+ import { CapType } from "../CapType.js";
5
+
6
+ test('very tiny segment of a path', () => {
7
+ const path = Path.fromJSON({
8
+ points: [3, 1, 1, 3, 1, 3, 7, 1, 3, 7, 1, 7, 3, 1, 7, 3, 1, 8]
9
+ });
10
+
11
+ const style = new TubePathStyle();
12
+ style.cap_type = CapType.Flat;
13
+ style.width = 0.1;
14
+ style.path_mask = [0, 0.00005900000113712167];
15
+ style.resolution = 10;
16
+ style.shape = [1, -2.4492937051703357e-16, 0.9238795042037964, -0.3826834261417389, 0.7071067690849304, -0.7071067690849304, 0.3826834261417389, -0.9238795042037964, -1.8369701465288538e-16, -1, -0.3826834261417389, -0.9238795042037964, -0.7071067690849304, -0.7071067690849304, -0.9238795042037964, -0.3826834261417389, -1, 1.2246468525851679e-16, -0.9238795042037964, 0.3826834261417389, -0.7071067690849304, 0.7071067690849304, -0.3826834261417389, 0.9238795042037964, 6.123234262925839e-17, 1, 0.3826834261417389, 0.9238795042037964, 0.7071067690849304, 0.7071067690849304, 0.9238795042037964, 0.3826834261417389];
17
+
18
+ const result = build_geometry_catmullrom(path, style, style.shape, [
19
+ 1.131973639570565e-16, 1, 0.3826834559440613, 0.9238795042037964, 0.7071067690849304, 0.7071067690849304, 0.9238795042037964, 0.3826834559440613, 1, -8.489801965906992e-17, 0.9238795042037964, -0.3826834559440613, 0.7071067690849304, -0.7071067690849304, 0.3826834559440613, -0.9238795042037964, -5.659868197852824e-17, -1, -0.3826834559440613, -0.9238795042037964, -0.7071067690849304, -0.7071067690849304, -0.9238795042037964, -0.3826834559440613, -1, 4.244900982953496e-17, -0.9238795042037964, 0.3826834559440613, -0.7071067690849304, 0.7071067690849304, -0.3826834559440613, 0.9238795042037964
20
+ ], [
21
+ 0.10000000149011612, 0, 0, -0, 0.10000000149011612, 0, 0, 0, 1
22
+ ], 0, 0.00005900000113712167);
23
+
24
+
25
+ const attribute_position = result.getAttribute("position");
26
+ expect(attribute_position).toBeDefined();
27
+ expect(attribute_position).not.toBeNull();
28
+
29
+ for (let i = 0; i < attribute_position.array.length; i++) {
30
+ expect(attribute_position.array[i]).not.toBeNaN();
31
+ }
32
+ });
@@ -101,8 +101,13 @@ export function computeFrenetFrames(points, closed = false, normal_hint) {
101
101
  // no tangent, copy previous one
102
102
  if (i > 0) {
103
103
  tangents[i] = tangents[i - 1];
104
- continue;
104
+ } else {
105
+ // very first tangent is undefined, set something arbitrary
106
+ // TODO take normal_hint into account
107
+ tangents[i] = new Vector3(0, 0, 1);
105
108
  }
109
+ continue;
110
+
106
111
  }
107
112
 
108
113
  tangents[i] = new Vector3(