@simarena/viewport 0.0.1-alpha.0 → 0.0.1-alpha.1
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.
- package/package.json +24 -18
- package/src/index.ts +40 -10
- package/src/sensors/contact.ts +124 -0
- package/src/sensors/directions.ts +103 -0
- package/src/sensors/index.ts +4 -0
- package/src/sensors/lidar.ts +120 -0
- package/src/sync.ts +128 -124
- package/src/types.ts +83 -0
- package/src/viewer/builder.ts +492 -0
- package/src/viewer/scene.ts +253 -0
- package/src/viewer/types.ts +193 -0
- package/src/viewport.ts +71 -58
- package/src/compiler/threejs/index.ts +0 -389
- package/src/parser/threejs/index.ts +0 -84
package/package.json
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simarena/viewport",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.1",
|
|
4
4
|
"description": "SimArena viewport — Three.js rendering, body sync, camera, WebSocket streaming",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"robotics",
|
|
7
|
+
"simarena",
|
|
8
|
+
"simulation",
|
|
9
|
+
"three.js",
|
|
10
|
+
"viewport",
|
|
11
|
+
"websocket"
|
|
12
|
+
],
|
|
5
13
|
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/simarena/viewport"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"src"
|
|
20
|
+
],
|
|
6
21
|
"type": "module",
|
|
7
|
-
"
|
|
22
|
+
"main": "./src/index.ts",
|
|
8
23
|
"exports": {
|
|
9
24
|
".": {
|
|
10
25
|
"source": "./src/index.ts",
|
|
@@ -12,29 +27,20 @@
|
|
|
12
27
|
"types": "./src/index.ts"
|
|
13
28
|
}
|
|
14
29
|
},
|
|
15
|
-
"main": "./src/index.ts",
|
|
16
30
|
"scripts": {
|
|
17
|
-
"check": "tsc --noEmit"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
"url": "https://github.com/simarena/viewport"
|
|
31
|
+
"check": "tsc --noEmit",
|
|
32
|
+
"lint": "oxlint",
|
|
33
|
+
"lint:fix": "oxlint --fix",
|
|
34
|
+
"format": "oxfmt",
|
|
35
|
+
"format:check": "oxfmt --check"
|
|
23
36
|
},
|
|
24
37
|
"dependencies": {
|
|
25
|
-
"@simarena/format": "0.0.1-alpha.0",
|
|
26
38
|
"three": "^0.183.2"
|
|
27
39
|
},
|
|
28
40
|
"devDependencies": {
|
|
29
41
|
"@types/three": "^0.183.1",
|
|
42
|
+
"oxfmt": "^0.49.0",
|
|
43
|
+
"oxlint": "^1.64.0",
|
|
30
44
|
"typescript": "^6.0.2"
|
|
31
|
-
},
|
|
32
|
-
"peerDependencies": {
|
|
33
|
-
"three": "^0.170.0"
|
|
34
|
-
},
|
|
35
|
-
"peerDependenciesMeta": {
|
|
36
|
-
"three": {
|
|
37
|
-
"optional": true
|
|
38
|
-
}
|
|
39
45
|
}
|
|
40
46
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,43 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export type {
|
|
1
|
+
export { createViewport } from "./viewport.js";
|
|
2
|
+
export type { Viewport, ViewportOpts } from "./viewport.js";
|
|
3
3
|
|
|
4
|
-
export {
|
|
5
|
-
export type {
|
|
4
|
+
export { buildBodyIdx, applyTransforms, flattenBodies, restoreBodies } from "./sync.js";
|
|
5
|
+
export type { BodySnapshot } from "./sync.js";
|
|
6
|
+
export { loadBodies, connectSim } from "./sync.js";
|
|
7
|
+
export type { BodyInfo, LoadBodiesOpts, LoadBodiesResult, SimOpts, SimHandle } from "./sync.js";
|
|
6
8
|
|
|
7
|
-
export {
|
|
8
|
-
export type { Viewport, ViewportOpts } from './viewport.js';
|
|
9
|
+
export type { SimScene, SimFrame, Contact, SimConnection, SimCommand, SimEvent } from "./types.js";
|
|
9
10
|
|
|
10
|
-
export {
|
|
11
|
-
export type {
|
|
12
|
-
export {
|
|
13
|
-
export
|
|
11
|
+
export { LidarViz } from "./sensors/index.js";
|
|
12
|
+
export type { LidarScanConfig, ScanPattern } from "./sensors/index.js";
|
|
13
|
+
export { buildDirs, dirs2D, dirs3D, dirsLivox, dirsFlash } from "./sensors/index.js";
|
|
14
|
+
export { ContactViz } from "./sensors/index.js";
|
|
15
|
+
|
|
16
|
+
export type {
|
|
17
|
+
GeomDesc,
|
|
18
|
+
BodyDesc,
|
|
19
|
+
RigidBodyDesc,
|
|
20
|
+
SoftBodyDesc,
|
|
21
|
+
SkinnedBodyDesc,
|
|
22
|
+
InstancedBodyDesc,
|
|
23
|
+
SensorDesc,
|
|
24
|
+
SimCamera,
|
|
25
|
+
VisualScene,
|
|
26
|
+
MaterialDesc,
|
|
27
|
+
LightDesc,
|
|
28
|
+
LightType,
|
|
29
|
+
ParticleDesc,
|
|
30
|
+
GaussianSplatDesc,
|
|
31
|
+
EnvironmentDesc,
|
|
32
|
+
} from "./viewer/types.js";
|
|
33
|
+
|
|
34
|
+
export { SimScene as SimRenderer } from "./viewer/scene.js";
|
|
35
|
+
export type { VizState } from "./viewer/scene.js";
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
buildSceneFromVisual,
|
|
39
|
+
buildGeomMesh,
|
|
40
|
+
createPrimitiveMesh,
|
|
41
|
+
createLightFromDesc,
|
|
42
|
+
} from "./viewer/builder.js";
|
|
43
|
+
export type { BuiltScene } from "./viewer/builder.js";
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import type { Contact } from "../types.js";
|
|
3
|
+
|
|
4
|
+
const LAYER = 3;
|
|
5
|
+
const SPHERE_R = 0.015;
|
|
6
|
+
const ARROW_LEN = 0.12;
|
|
7
|
+
const FORCE_LOW = 0.5;
|
|
8
|
+
const FORCE_HIGH = 50;
|
|
9
|
+
|
|
10
|
+
const arrowDir = new THREE.Vector3();
|
|
11
|
+
const arrowOrigin = new THREE.Vector3();
|
|
12
|
+
const _color = new THREE.Color();
|
|
13
|
+
|
|
14
|
+
function forceColor(force: number): THREE.Color {
|
|
15
|
+
const t = Math.min(1, Math.max(0, (force - FORCE_LOW) / (FORCE_HIGH - FORCE_LOW)));
|
|
16
|
+
const hue = 0.6 - t * 0.6;
|
|
17
|
+
return _color.clone().setHSL(hue, 1, 0.55);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function makeSphere(scene: THREE.Object3D, geo: THREE.SphereGeometry): THREE.Mesh {
|
|
21
|
+
const mat = new THREE.MeshBasicMaterial({
|
|
22
|
+
color: 0xffffff,
|
|
23
|
+
depthTest: false,
|
|
24
|
+
transparent: true,
|
|
25
|
+
opacity: 0.85,
|
|
26
|
+
});
|
|
27
|
+
const m = new THREE.Mesh(geo, mat);
|
|
28
|
+
m.layers.set(LAYER);
|
|
29
|
+
m.renderOrder = 999;
|
|
30
|
+
m.visible = false;
|
|
31
|
+
scene.add(m);
|
|
32
|
+
return m;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function makeArrow(scene: THREE.Object3D): THREE.ArrowHelper {
|
|
36
|
+
const a = new THREE.ArrowHelper(
|
|
37
|
+
new THREE.Vector3(0, 0, 1),
|
|
38
|
+
new THREE.Vector3(0, 0, 0),
|
|
39
|
+
ARROW_LEN,
|
|
40
|
+
0xffffff,
|
|
41
|
+
ARROW_LEN * 0.3,
|
|
42
|
+
ARROW_LEN * 0.15,
|
|
43
|
+
);
|
|
44
|
+
a.layers.set(LAYER);
|
|
45
|
+
a.line.layers.set(LAYER);
|
|
46
|
+
a.cone.layers.set(LAYER);
|
|
47
|
+
a.renderOrder = 999;
|
|
48
|
+
a.visible = false;
|
|
49
|
+
scene.add(a);
|
|
50
|
+
return a;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class ContactViz {
|
|
54
|
+
#scene: THREE.Object3D;
|
|
55
|
+
#geo: THREE.SphereGeometry;
|
|
56
|
+
#spheres: THREE.Mesh[] = [];
|
|
57
|
+
#arrows: THREE.ArrowHelper[] = [];
|
|
58
|
+
visible = true;
|
|
59
|
+
|
|
60
|
+
constructor(scene: THREE.Object3D) {
|
|
61
|
+
this.#scene = scene;
|
|
62
|
+
this.#geo = new THREE.SphereGeometry(SPHERE_R, 8, 6);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
update(contacts: Contact[]): void {
|
|
66
|
+
if (!this.visible) {
|
|
67
|
+
this.hide();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
while (this.#spheres.length < contacts.length) {
|
|
72
|
+
this.#spheres.push(makeSphere(this.#scene, this.#geo));
|
|
73
|
+
this.#arrows.push(makeArrow(this.#scene));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (let i = 0; i < contacts.length; i++) {
|
|
77
|
+
const c = contacts[i];
|
|
78
|
+
if (!c) continue;
|
|
79
|
+
const sphere = this.#spheres[i]!;
|
|
80
|
+
const arrow = this.#arrows[i]!;
|
|
81
|
+
const col = forceColor(c.force);
|
|
82
|
+
|
|
83
|
+
sphere.position.set(c.pos[0], c.pos[1], c.pos[2]);
|
|
84
|
+
(sphere.material as THREE.MeshBasicMaterial).color.copy(col);
|
|
85
|
+
sphere.visible = true;
|
|
86
|
+
|
|
87
|
+
if (c.normal) {
|
|
88
|
+
arrowDir.set(c.normal[0], c.normal[1], c.normal[2]).normalize();
|
|
89
|
+
arrowOrigin.set(c.pos[0], c.pos[1], c.pos[2]);
|
|
90
|
+
arrow.position.copy(arrowOrigin);
|
|
91
|
+
arrow.setDirection(arrowDir);
|
|
92
|
+
arrow.setColor(col);
|
|
93
|
+
const len = Math.min(ARROW_LEN * 3, ARROW_LEN + c.force * 0.002);
|
|
94
|
+
arrow.setLength(len, len * 0.3, len * 0.15);
|
|
95
|
+
arrow.visible = true;
|
|
96
|
+
} else {
|
|
97
|
+
arrow.visible = false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (let i = contacts.length; i < this.#spheres.length; i++) {
|
|
102
|
+
this.#spheres[i]!.visible = false;
|
|
103
|
+
this.#arrows[i]!.visible = false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
hide(): void {
|
|
108
|
+
for (const s of this.#spheres) s.visible = false;
|
|
109
|
+
for (const a of this.#arrows) a.visible = false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
dispose(): void {
|
|
113
|
+
for (const s of this.#spheres) {
|
|
114
|
+
(s.material as THREE.Material).dispose();
|
|
115
|
+
this.#scene.remove(s);
|
|
116
|
+
}
|
|
117
|
+
for (const a of this.#arrows) {
|
|
118
|
+
this.#scene.remove(a);
|
|
119
|
+
}
|
|
120
|
+
this.#spheres = [];
|
|
121
|
+
this.#arrows = [];
|
|
122
|
+
this.#geo.dispose();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/** LiDAR ray direction generators — pure math, no dependencies. */
|
|
2
|
+
|
|
3
|
+
/** Horizontal ring — 2D planar scan (SICK, RPLiDAR, Hokuyo) */
|
|
4
|
+
export function dirs2D(samples = 360, hfov: [number, number] = [-180, 180]): Float64Array {
|
|
5
|
+
const out = new Float64Array(samples * 3);
|
|
6
|
+
const start = (hfov[0] * Math.PI) / 180;
|
|
7
|
+
const span = ((hfov[1] - hfov[0]) * Math.PI) / 180;
|
|
8
|
+
for (let i = 0; i < samples; i++) {
|
|
9
|
+
const a = start + (i / samples) * span;
|
|
10
|
+
out[i * 3] = Math.cos(a);
|
|
11
|
+
out[i * 3 + 1] = Math.sin(a);
|
|
12
|
+
out[i * 3 + 2] = 0;
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Multi-channel vertical rings — 3D spinning (Velodyne, Ouster) */
|
|
18
|
+
export function dirs3D(
|
|
19
|
+
channels = 16,
|
|
20
|
+
azimuth = 120,
|
|
21
|
+
hfov: [number, number] = [-180, 180],
|
|
22
|
+
vfov: [number, number] = [-15, 15],
|
|
23
|
+
): Float64Array {
|
|
24
|
+
const n = channels * azimuth,
|
|
25
|
+
out = new Float64Array(n * 3);
|
|
26
|
+
const hStart = (hfov[0] * Math.PI) / 180,
|
|
27
|
+
hSpan = ((hfov[1] - hfov[0]) * Math.PI) / 180;
|
|
28
|
+
for (let c = 0; c < channels; c++) {
|
|
29
|
+
const phi = ((vfov[0] + (c / (channels - 1)) * (vfov[1] - vfov[0])) * Math.PI) / 180;
|
|
30
|
+
for (let a = 0; a < azimuth; a++) {
|
|
31
|
+
const th = hStart + (a / azimuth) * hSpan;
|
|
32
|
+
const i = c * azimuth + a;
|
|
33
|
+
out[i * 3] = Math.cos(phi) * Math.cos(th);
|
|
34
|
+
out[i * 3 + 1] = Math.cos(phi) * Math.sin(th);
|
|
35
|
+
out[i * 3 + 2] = Math.sin(phi);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Time-varying petal pattern (Livox Mid-360 style) */
|
|
42
|
+
export function dirsLivox(t: number, rays = 600, maxEl = 20): Float64Array {
|
|
43
|
+
const out = new Float64Array(rays * 3),
|
|
44
|
+
el = (maxEl * Math.PI) / 180;
|
|
45
|
+
for (let i = 0; i < rays; i++) {
|
|
46
|
+
const alpha = (i / rays) * Math.PI * 6 + t * 0.2;
|
|
47
|
+
const az = alpha,
|
|
48
|
+
e = Math.sin(alpha * 2.3) * el;
|
|
49
|
+
out[i * 3] = Math.cos(e) * Math.cos(az);
|
|
50
|
+
out[i * 3 + 1] = Math.cos(e) * Math.sin(az);
|
|
51
|
+
out[i * 3 + 2] = Math.sin(e);
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Perspective grid — flash / depth camera */
|
|
57
|
+
export function dirsFlash(w = 64, h = 48, fovDeg = 60): Float64Array {
|
|
58
|
+
const n = w * h,
|
|
59
|
+
out = new Float64Array(n * 3),
|
|
60
|
+
fov = (fovDeg * Math.PI) / 180;
|
|
61
|
+
for (let y = 0; y < h; y++) {
|
|
62
|
+
for (let x = 0; x < w; x++) {
|
|
63
|
+
const nx = (x / (w - 1)) * 2 - 1,
|
|
64
|
+
ny = (y / (h - 1)) * 2 - 1;
|
|
65
|
+
const ax = Math.atan(nx * Math.tan(fov / 2) * (w / h));
|
|
66
|
+
const ay = Math.atan(ny * Math.tan(fov / 2));
|
|
67
|
+
const i = y * w + x;
|
|
68
|
+
out[i * 3] = Math.cos(ay) * Math.cos(ax);
|
|
69
|
+
out[i * 3 + 1] = Math.sin(ax);
|
|
70
|
+
out[i * 3 + 2] = -Math.sin(ay);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type ScanPattern = "2d" | "3d" | "livox" | "flash";
|
|
77
|
+
|
|
78
|
+
export interface LidarScanConfig {
|
|
79
|
+
pattern?: ScanPattern;
|
|
80
|
+
horizontalSamples?: number;
|
|
81
|
+
horizontalFov?: [number, number];
|
|
82
|
+
verticalFov?: [number, number];
|
|
83
|
+
channels?: number;
|
|
84
|
+
range?: [number, number];
|
|
85
|
+
sampleRate?: number;
|
|
86
|
+
height?: number;
|
|
87
|
+
near?: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Build directions from a LidarScanConfig */
|
|
91
|
+
export function buildDirs(cfg: LidarScanConfig, t = 0): Float64Array {
|
|
92
|
+
const pattern = cfg.pattern ?? "2d";
|
|
93
|
+
if (pattern === "3d")
|
|
94
|
+
return dirs3D(
|
|
95
|
+
cfg.channels ?? 16,
|
|
96
|
+
cfg.horizontalSamples ?? 120,
|
|
97
|
+
cfg.horizontalFov ?? [-180, 180],
|
|
98
|
+
cfg.verticalFov ?? [-15, 15],
|
|
99
|
+
);
|
|
100
|
+
if (pattern === "livox") return dirsLivox(t, cfg.horizontalSamples ?? 600);
|
|
101
|
+
if (pattern === "flash") return dirsFlash();
|
|
102
|
+
return dirs2D(cfg.horizontalSamples ?? 120, cfg.horizontalFov ?? [-180, 180]);
|
|
103
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LidarViz — Three.js visualization of LiDAR scan data.
|
|
3
|
+
* Reads pre-computed distances from sim.sensordata and draws lines or points.
|
|
4
|
+
* No raycasting — pure display component.
|
|
5
|
+
*/
|
|
6
|
+
import * as THREE from "three";
|
|
7
|
+
import type { LidarScanConfig, ScanPattern } from "./directions.js";
|
|
8
|
+
import { buildDirs } from "./directions.js";
|
|
9
|
+
|
|
10
|
+
export type { LidarScanConfig, ScanPattern };
|
|
11
|
+
|
|
12
|
+
const LINE_COLOR = 0x00ff88;
|
|
13
|
+
const COLOR_LOW = new THREE.Color().setHSL(0, 1, 0.6);
|
|
14
|
+
const COLOR_HIGH = new THREE.Color().setHSL(0.55, 1, 0.6);
|
|
15
|
+
|
|
16
|
+
export class LidarViz {
|
|
17
|
+
readonly group: THREE.Group;
|
|
18
|
+
readonly name: string;
|
|
19
|
+
|
|
20
|
+
private dirs: Float64Array;
|
|
21
|
+
private n: number;
|
|
22
|
+
private range: number;
|
|
23
|
+
private height: number;
|
|
24
|
+
private pattern: ScanPattern;
|
|
25
|
+
private channels: number;
|
|
26
|
+
private azimuth: number;
|
|
27
|
+
private body: THREE.Object3D;
|
|
28
|
+
private lines: THREE.LineSegments | null = null;
|
|
29
|
+
private points: THREE.Points | null = null;
|
|
30
|
+
private lineMat: THREE.LineBasicMaterial;
|
|
31
|
+
private ptsMat: THREE.PointsMaterial;
|
|
32
|
+
|
|
33
|
+
constructor(name: string, body: THREE.Object3D, cfg: LidarScanConfig) {
|
|
34
|
+
this.name = name;
|
|
35
|
+
this.body = body;
|
|
36
|
+
this.pattern = cfg.pattern ?? "2d";
|
|
37
|
+
this.range = cfg.range?.[1] ?? 10;
|
|
38
|
+
this.height = cfg.height ?? 0.3;
|
|
39
|
+
this.channels = cfg.channels ?? 16;
|
|
40
|
+
this.azimuth = cfg.horizontalSamples ?? 120;
|
|
41
|
+
this.dirs = buildDirs(cfg);
|
|
42
|
+
this.n = this.dirs.length / 3;
|
|
43
|
+
|
|
44
|
+
this.lineMat = new THREE.LineBasicMaterial({ color: LINE_COLOR });
|
|
45
|
+
this.ptsMat = new THREE.PointsMaterial({
|
|
46
|
+
size: 0.05,
|
|
47
|
+
vertexColors: true,
|
|
48
|
+
sizeAttenuation: true,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
this.group = new THREE.Group();
|
|
52
|
+
this.group.frustumCulled = false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
update(dists: number[] | null): void {
|
|
56
|
+
if (this.lines) {
|
|
57
|
+
this.group.remove(this.lines);
|
|
58
|
+
this.lines.geometry.dispose();
|
|
59
|
+
this.lines = null;
|
|
60
|
+
}
|
|
61
|
+
if (this.points) {
|
|
62
|
+
this.group.remove(this.points);
|
|
63
|
+
this.points.geometry.dispose();
|
|
64
|
+
this.points = null;
|
|
65
|
+
}
|
|
66
|
+
if (!dists || dists.length === 0) return;
|
|
67
|
+
|
|
68
|
+
const origin = new THREE.Vector3();
|
|
69
|
+
this.body.getWorldPosition(origin);
|
|
70
|
+
origin.z += this.height;
|
|
71
|
+
|
|
72
|
+
const pts: number[] = [],
|
|
73
|
+
cols: number[] = [],
|
|
74
|
+
linePts: number[] = [];
|
|
75
|
+
|
|
76
|
+
for (let i = 0; i < Math.min(this.n, dists.length); i++) {
|
|
77
|
+
const d = dists[i] ?? this.range;
|
|
78
|
+
const dx = this.dirs[i * 3] ?? 0,
|
|
79
|
+
dy = this.dirs[i * 3 + 1] ?? 0,
|
|
80
|
+
dz = this.dirs[i * 3 + 2] ?? 0;
|
|
81
|
+
const hx = origin.x + dx * d,
|
|
82
|
+
hy = origin.y + dy * d,
|
|
83
|
+
hz = origin.z + dz * d;
|
|
84
|
+
|
|
85
|
+
if (this.pattern === "2d") {
|
|
86
|
+
linePts.push(origin.x, origin.y, origin.z, hx, hy, hz);
|
|
87
|
+
} else if (d < this.range) {
|
|
88
|
+
pts.push(hx, hy, hz);
|
|
89
|
+
const t =
|
|
90
|
+
this.pattern === "3d"
|
|
91
|
+
? Math.floor(i / this.azimuth) / Math.max(1, this.channels - 1)
|
|
92
|
+
: d / this.range;
|
|
93
|
+
const c = new THREE.Color().lerpColors(COLOR_LOW, COLOR_HIGH, t);
|
|
94
|
+
cols.push(c.r, c.g, c.b);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (linePts.length > 0) {
|
|
99
|
+
const geo = new THREE.BufferGeometry();
|
|
100
|
+
geo.setAttribute("position", new THREE.Float32BufferAttribute(linePts, 3));
|
|
101
|
+
this.lines = new THREE.LineSegments(geo, this.lineMat);
|
|
102
|
+
this.lines.frustumCulled = false;
|
|
103
|
+
this.group.add(this.lines);
|
|
104
|
+
}
|
|
105
|
+
if (pts.length > 0) {
|
|
106
|
+
const geo = new THREE.BufferGeometry();
|
|
107
|
+
geo.setAttribute("position", new THREE.Float32BufferAttribute(pts, 3));
|
|
108
|
+
geo.setAttribute("color", new THREE.Float32BufferAttribute(cols, 3));
|
|
109
|
+
this.points = new THREE.Points(geo, this.ptsMat);
|
|
110
|
+
this.group.add(this.points);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
dispose(): void {
|
|
115
|
+
this.lines?.geometry.dispose();
|
|
116
|
+
this.points?.geometry.dispose();
|
|
117
|
+
this.lineMat.dispose();
|
|
118
|
+
this.ptsMat.dispose();
|
|
119
|
+
}
|
|
120
|
+
}
|