Skip to content

Commit 07df894

Browse files
committed
points
1 parent d2c3d76 commit 07df894

File tree

6 files changed

+496
-1
lines changed

6 files changed

+496
-1
lines changed

libs/soba/performances/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './lib/adaptive-dpr';
22
export * from './lib/adaptive-events';
33
export * from './lib/instances/instances';
4+
export * from './lib/points/points';

libs/soba/performances/src/lib/instances/instances.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,6 @@ export class NgtsInstances {
140140
autoEffect(() => {
141141
const instancedMesh = this.instancedMesh()?.nativeElement;
142142
if (!instancedMesh) return;
143-
// instancedMesh.instanceMatrix.array = this.buffers().matrices;
144143
checkUpdate(instancedMesh.instanceMatrix);
145144
});
146145
});
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
import {
2+
afterNextRender,
3+
ChangeDetectionStrategy,
4+
Component,
5+
computed,
6+
CUSTOM_ELEMENTS_SCHEMA,
7+
ElementRef,
8+
inject,
9+
input,
10+
viewChild,
11+
} from '@angular/core';
12+
import { checkUpdate, extend, injectBeforeRender, NgtPoints, omit, pick, resolveRef } from 'angular-three';
13+
import { injectAutoEffect } from 'ngxtension/auto-effect';
14+
import { mergeInputs } from 'ngxtension/inject-inputs';
15+
import { BufferAttribute, BufferGeometry, DynamicDrawUsage, Matrix4, Points, Vector3 } from 'three';
16+
import { NgtPositionPoint, PositionPoint } from './position-point';
17+
18+
@Component({
19+
selector: 'ngts-point',
20+
standalone: true,
21+
template: `
22+
<ngt-position-point #positionPoint [parameters]="options()" [instance]="points.pointsRef()">
23+
<ng-content />
24+
</ngt-position-point>
25+
`,
26+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
27+
changeDetection: ChangeDetectionStrategy.OnPush,
28+
})
29+
export class NgtsPoint {
30+
options = input({} as Partial<NgtPositionPoint>);
31+
32+
positionPoint = viewChild.required<ElementRef<PositionPoint>>('positionPoint');
33+
34+
points = inject(NgtsPointsInstances);
35+
36+
constructor() {
37+
extend({ PositionPoint });
38+
39+
const autoEffect = injectAutoEffect();
40+
41+
afterNextRender(() => {
42+
autoEffect(() => {
43+
return this.points.subscribe(this.positionPoint().nativeElement);
44+
});
45+
});
46+
}
47+
}
48+
49+
@Component({
50+
selector: 'ngts-points-buffer',
51+
standalone: true,
52+
template: `
53+
<ngt-points #points [parameters]="options()">
54+
<ngt-buffer-geometry>
55+
<ngt-buffer-attribute
56+
attach="attributes.position"
57+
[count]="positions().length / stride()"
58+
[array]="positions()"
59+
[itemSize]="stride()"
60+
[usage]="DynamicDrawUsage"
61+
/>
62+
@if (colors(); as colors) {
63+
<ngt-buffer-attribute
64+
attach="attributes.color"
65+
[count]="colors.length / stride()"
66+
[array]="colors"
67+
[itemSize]="3"
68+
[usage]="DynamicDrawUsage"
69+
/>
70+
}
71+
@if (sizes(); as sizes) {
72+
<ngt-buffer-attribute
73+
attach="attributes.size"
74+
[count]="sizes.length / stride()"
75+
[array]="sizes"
76+
[itemSize]="1"
77+
[usage]="DynamicDrawUsage"
78+
/>
79+
}
80+
</ngt-buffer-geometry>
81+
<ng-content />
82+
</ngt-points>
83+
`,
84+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
85+
changeDetection: ChangeDetectionStrategy.OnPush,
86+
})
87+
export class NgtsPointsBuffer {
88+
positions = input.required<Float32Array>();
89+
colors = input<Float32Array>();
90+
sizes = input<Float32Array>();
91+
stride = input<2 | 3>(3);
92+
options = input({} as Partial<NgtPositionPoint>);
93+
94+
pointsRef = viewChild.required<ElementRef<Points>>('points');
95+
96+
constructor() {
97+
extend({ Points, BufferAttribute, BufferGeometry });
98+
99+
injectBeforeRender(() => {
100+
const points = this.pointsRef()?.nativeElement;
101+
if (!points) return;
102+
103+
const attributes = points.geometry.attributes;
104+
checkUpdate(attributes['position']);
105+
if (this.colors()) checkUpdate(attributes['color']);
106+
if (this.sizes()) checkUpdate(attributes['size']);
107+
});
108+
}
109+
110+
protected readonly DynamicDrawUsage = DynamicDrawUsage;
111+
}
112+
113+
const parentMatrix = new Matrix4();
114+
const position = new Vector3();
115+
116+
export interface NgtsPointsInstancesOptions extends Partial<NgtPoints> {
117+
range?: number;
118+
limit: number;
119+
}
120+
121+
const defaultInstancesOptions: NgtsPointsInstancesOptions = { limit: 1000 };
122+
123+
@Component({
124+
selector: 'ngts-points-instances',
125+
standalone: true,
126+
template: `
127+
<ngt-points
128+
#points
129+
[userData]="{ instances: positionPoints }"
130+
[matrixAutoUpdate]="false"
131+
[raycast]="nullRaycast"
132+
[parameters]="parameters()"
133+
>
134+
<ngt-buffer-geometry>
135+
<ngt-buffer-attribute
136+
attach="attributes.position"
137+
[count]="buffers().positions.length / 3"
138+
[array]="buffers().positions"
139+
[itemSize]="3"
140+
[usage]="DynamicDrawUsage"
141+
/>
142+
<ngt-buffer-attribute
143+
attach="attributes.color"
144+
[count]="buffers().colors.length / 3"
145+
[array]="buffers().colors"
146+
[itemSize]="3"
147+
[usage]="DynamicDrawUsage"
148+
/>
149+
<ngt-buffer-attribute
150+
attach="attributes.size"
151+
[count]="buffers().sizes.length"
152+
[array]="buffers().sizes"
153+
[itemSize]="1"
154+
[usage]="DynamicDrawUsage"
155+
/>
156+
</ngt-buffer-geometry>
157+
<ng-content />
158+
</ngt-points>
159+
`,
160+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
161+
changeDetection: ChangeDetectionStrategy.OnPush,
162+
})
163+
export class NgtsPointsInstances {
164+
protected readonly nullRaycast = () => null;
165+
166+
options = input(defaultInstancesOptions, { transform: mergeInputs(defaultInstancesOptions) });
167+
parameters = omit(this.options, ['limit', 'range']);
168+
169+
pointsRef = viewChild.required<ElementRef<Points>>('points');
170+
171+
private limit = pick(this.options, 'limit');
172+
173+
buffers = computed(() => {
174+
const limit = this.limit();
175+
176+
return {
177+
positions: new Float32Array(limit * 3),
178+
colors: Float32Array.from({ length: limit * 3 }, () => 1),
179+
sizes: Float32Array.from({ length: limit }, () => 1),
180+
};
181+
});
182+
183+
positionPoints: Array<ElementRef<PositionPoint> | PositionPoint> = [];
184+
185+
constructor() {
186+
extend({ Points, BufferAttribute, BufferGeometry });
187+
const autoEffect = injectAutoEffect();
188+
189+
afterNextRender(() => {
190+
autoEffect(() => {
191+
const points = this.pointsRef()?.nativeElement;
192+
if (!points) return;
193+
checkUpdate(points.geometry.attributes['position']);
194+
});
195+
});
196+
197+
injectBeforeRender(() => {
198+
const points = this.pointsRef()?.nativeElement;
199+
if (!points) return;
200+
201+
const { limit, range } = this.options();
202+
const { positions, sizes, colors } = this.buffers();
203+
204+
points.updateMatrix();
205+
points.updateMatrixWorld();
206+
parentMatrix.copy(points.matrixWorld).invert();
207+
208+
points.geometry.drawRange.count = Math.min(
209+
limit,
210+
range !== undefined ? range : limit,
211+
this.positionPoints.length,
212+
);
213+
214+
for (let i = 0; i < this.positionPoints.length; i++) {
215+
const positionPoint = resolveRef(this.positionPoints[i]);
216+
if (positionPoint) {
217+
positionPoint.getWorldPosition(position).applyMatrix4(parentMatrix);
218+
position.toArray(positions, i * 3);
219+
checkUpdate(points.geometry.attributes['position']);
220+
221+
positionPoint.matrixWorldNeedsUpdate = true;
222+
positionPoint.color.toArray(colors, i * 3);
223+
checkUpdate(points.geometry.attributes['color']);
224+
225+
sizes.set([positionPoint.size], i);
226+
checkUpdate(points.geometry.attributes['size']);
227+
}
228+
}
229+
});
230+
}
231+
232+
subscribe(ref: ElementRef<PositionPoint> | PositionPoint) {
233+
this.positionPoints.push(ref);
234+
return () => {
235+
this.positionPoints = this.positionPoints.filter((p) => p !== ref);
236+
};
237+
}
238+
239+
protected readonly DynamicDrawUsage = DynamicDrawUsage;
240+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { ElementRef } from '@angular/core';
2+
import { NgtObject3DNode, resolveRef } from 'angular-three';
3+
import { Color, Group, Intersection, Matrix4, Points, Ray, Raycaster, Sphere, Vector3 } from 'three';
4+
5+
export type NgtPositionPoint = NgtObject3DNode<PositionPoint, typeof PositionPoint>;
6+
7+
const _inverseMatrix = new Matrix4();
8+
const _ray = new Ray();
9+
const _sphere = new Sphere();
10+
const _position = new Vector3();
11+
12+
export class PositionPoint extends Group {
13+
size: number;
14+
color: Color;
15+
instance: ElementRef<Points> | Points | null | undefined;
16+
17+
constructor() {
18+
super();
19+
this.size = 0;
20+
this.color = new Color('white');
21+
this.instance = undefined;
22+
}
23+
24+
// This will allow the virtual instance have bounds
25+
get geometry() {
26+
return resolveRef(this.instance)?.geometry;
27+
}
28+
29+
override raycast(raycaster: Raycaster, intersects: Intersection[]) {
30+
const parent = resolveRef(this.instance);
31+
if (!parent || !parent.geometry) return;
32+
const instanceId = parent.userData['instances'].indexOf(this);
33+
// If the instance wasn't found or exceeds the parents draw range, bail out
34+
if (instanceId === -1 || instanceId > parent.geometry.drawRange.count) return;
35+
36+
const threshold = raycaster.params.Points?.threshold ?? 1;
37+
_sphere.set(this.getWorldPosition(_position), threshold);
38+
if (raycaster.ray.intersectsSphere(_sphere) === false) return;
39+
40+
_inverseMatrix.copy(parent.matrixWorld).invert();
41+
_ray.copy(raycaster.ray).applyMatrix4(_inverseMatrix);
42+
43+
const localThreshold = threshold / ((this.scale.x + this.scale.y + this.scale.z) / 3);
44+
const localThresholdSq = localThreshold * localThreshold;
45+
const rayPointDistanceSq = _ray.distanceSqToPoint(this.position);
46+
47+
if (rayPointDistanceSq < localThresholdSq) {
48+
const intersectPoint = new Vector3();
49+
_ray.closestPointToPoint(this.position, intersectPoint);
50+
intersectPoint.applyMatrix4(this.matrixWorld);
51+
const distance = raycaster.ray.origin.distanceTo(intersectPoint);
52+
if (distance < raycaster.near || distance > raycaster.far) return;
53+
intersects.push({
54+
distance: distance,
55+
distanceToRay: Math.sqrt(rayPointDistanceSq),
56+
point: intersectPoint,
57+
index: instanceId,
58+
face: null,
59+
object: this,
60+
});
61+
}
62+
}
63+
}
64+
65+
declare global {
66+
interface HTMLElementTagNameMap {
67+
/**
68+
* @extends ngt-group
69+
* @rawOptions instance|color|size
70+
*/
71+
'ngt-position-point': NgtPositionPoint;
72+
}
73+
}

0 commit comments

Comments
 (0)