Skip to content

Commit fc5a81e

Browse files
committed
segments
1 parent 1cbe198 commit fc5a81e

File tree

6 files changed

+313
-8
lines changed

6 files changed

+313
-8
lines changed

libs/soba/performances/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export * from './adaptive-dpr/adaptive-dpr';
22
export * from './adaptive-events/adaptive-events';
33
export * from './points/points';
4+
export * from './segments/segment-object';
5+
export * from './segments/segments';

libs/soba/performances/src/points/points.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
Directive,
77
Injector,
88
Input,
9+
NgZone,
10+
OnInit,
911
TemplateRef,
1012
computed,
1113
effect,
@@ -202,17 +204,24 @@ export class NgtsPointsInstances {
202204
`,
203205
schemas: [CUSTOM_ELEMENTS_SCHEMA],
204206
})
205-
export class NgtsPoint {
207+
export class NgtsPoint implements OnInit {
206208
@Input() pointRef = injectNgtRef<PositionPoint>();
207209

210+
private zone = inject(NgZone);
211+
private injector = inject(Injector);
208212
pointsInstancesApi = injectNgtsPointsInstanceApi();
209213

210-
constructor() {
211-
effect((onCleanup) => {
212-
const api = this.pointsInstancesApi();
213-
api.getParent().nativeElement;
214-
onCleanup(() => api.subscribe(this.pointRef));
215-
});
214+
ngOnInit() {
215+
effect(
216+
(onCleanup) => {
217+
const cleanup = this.zone.runOutsideAngular(() => {
218+
const api = this.pointsInstancesApi();
219+
return api.subscribe(this.pointRef);
220+
});
221+
onCleanup(cleanup);
222+
},
223+
{ injector: this.injector },
224+
);
216225
}
217226
}
218227

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import * as THREE from 'three';
2+
3+
export class SegmentObject {
4+
color: THREE.Color;
5+
start: THREE.Vector3;
6+
end: THREE.Vector3;
7+
constructor() {
8+
this.color = new THREE.Color('white');
9+
this.start = new THREE.Vector3(0, 0, 0);
10+
this.end = new THREE.Vector3(0, 0, 0);
11+
}
12+
}
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import {
2+
CUSTOM_ELEMENTS_SCHEMA,
3+
Component,
4+
Injector,
5+
Input,
6+
NgZone,
7+
OnInit,
8+
computed,
9+
effect,
10+
forwardRef,
11+
inject,
12+
signal,
13+
untracked,
14+
} from '@angular/core';
15+
import {
16+
NgtArgs,
17+
NgtRef,
18+
createInjectionToken,
19+
extend,
20+
injectBeforeRender,
21+
injectNgtRef,
22+
is,
23+
signalStore,
24+
type NgtColor,
25+
type NgtVector3,
26+
} from 'angular-three';
27+
import * as THREE from 'three';
28+
import { Line2, LineMaterial, LineSegmentsGeometry } from 'three-stdlib';
29+
import { SegmentObject } from './segment-object';
30+
31+
export type NgtsSegmentState = {
32+
start?: NgtVector3;
33+
end?: NgtVector3;
34+
color?: NgtColor;
35+
};
36+
37+
export type NgtsSegmentsState = {
38+
limit: number;
39+
lineWidth: number;
40+
};
41+
42+
declare global {
43+
interface HTMLElementTagNameMap {
44+
'ngt-segment-object': SegmentObject;
45+
'ngts-segment': NgtsSegmentState;
46+
'ngts-segments': NgtsSegmentsState;
47+
}
48+
}
49+
50+
const normPos = (pos: NgtsSegmentState['start']): SegmentObject['start'] =>
51+
pos instanceof THREE.Vector3
52+
? pos
53+
: new THREE.Vector3(...((typeof pos === 'number' ? [pos, pos, pos] : pos) as [number, number, number]));
54+
55+
extend({ SegmentObject });
56+
57+
export const [injectNgtsSegmentsApi, provideNgtsSegmentsApi] = createInjectionToken(
58+
(segments: NgtsSegments) => segments.api,
59+
{ isRoot: false, deps: [forwardRef(() => NgtsSegments)] },
60+
);
61+
62+
@Component({
63+
selector: 'ngts-segment',
64+
standalone: true,
65+
template: `
66+
<ngt-segment-object [ref]="segmentRef" [color]="color()" [start]="normalizedStart()" [end]="normalizedEnd()" />
67+
`,
68+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
69+
})
70+
export class NgtsSegment implements OnInit {
71+
private inputs = signalStore<NgtsSegmentState>({});
72+
73+
@Input() segmentRef = injectNgtRef<SegmentObject>();
74+
75+
@Input({ alias: 'start' }) set _start(start: NgtVector3) {
76+
this.inputs.set({ start });
77+
}
78+
79+
@Input({ alias: 'end' }) set _end(end: NgtVector3) {
80+
this.inputs.set({ end });
81+
}
82+
83+
@Input({ alias: 'color' }) set _color(color: NgtColor) {
84+
this.inputs.set({ color });
85+
}
86+
87+
private segmentsApi = injectNgtsSegmentsApi();
88+
private injector = inject(Injector);
89+
private zone = inject(NgZone);
90+
91+
private start = this.inputs.select('start');
92+
private end = this.inputs.select('end');
93+
94+
normalizedStart = computed(() => normPos(this.start()));
95+
normalizedEnd = computed(() => normPos(this.end()));
96+
color = this.inputs.select('color');
97+
98+
ngOnInit() {
99+
effect(
100+
(onCleanup) => {
101+
const cleanup = this.zone.runOutsideAngular(() => {
102+
const api = this.segmentsApi();
103+
return api.subscribe(this.segmentRef);
104+
});
105+
onCleanup(cleanup);
106+
},
107+
{ injector: this.injector },
108+
);
109+
}
110+
}
111+
112+
@Component({
113+
selector: 'ngts-segments',
114+
standalone: true,
115+
template: `
116+
<ngt-primitive *args="[line]" [ref]="segmentsRef">
117+
<ngt-primitive *args="[geometry]" attach="geometry" />
118+
<ngt-primitive
119+
*args="[material]"
120+
attach="material"
121+
ngtCompound
122+
[vertexColors]="true"
123+
[resolution]="resolution"
124+
[linewidth]="lineWidth()"
125+
/>
126+
<ng-content />
127+
</ngt-primitive>
128+
`,
129+
imports: [NgtArgs],
130+
providers: [provideNgtsSegmentsApi()],
131+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
132+
})
133+
export class NgtsSegments {
134+
private inputs = signalStore<NgtsSegmentsState>({ limit: 1000, lineWidth: 1 });
135+
136+
@Input() segmentsRef = injectNgtRef<Line2>();
137+
138+
@Input({ alias: 'limit' }) set _limit(limit: number) {
139+
this.inputs.set({ limit });
140+
}
141+
142+
@Input({ alias: 'lineWidth' }) set _lineWidth(lineWidth: number) {
143+
this.inputs.set({ lineWidth });
144+
}
145+
146+
private segments = signal<NgtRef<SegmentObject>[]>([]);
147+
private limit = this.inputs.select('limit');
148+
149+
private positions = computed(() => Array(this.limit() * 6).fill(0));
150+
private colors = computed(() => Array(this.limit() * 6).fill(0));
151+
152+
resolution = new THREE.Vector2(512, 512);
153+
lineWidth = this.inputs.select('lineWidth');
154+
line = new Line2();
155+
material = new LineMaterial();
156+
geometry = new LineSegmentsGeometry();
157+
158+
api = computed(() => ({
159+
subscribe: (segmentRef: NgtRef<SegmentObject>) => {
160+
untracked(() => {
161+
this.segments.update((s) => [...s, segmentRef]);
162+
});
163+
164+
return () => {
165+
untracked(() => {
166+
this.segments.update((s) => s.filter((segment) => segment !== segmentRef));
167+
});
168+
};
169+
},
170+
}));
171+
172+
constructor() {
173+
this.beforeRender();
174+
}
175+
176+
private beforeRender() {
177+
injectBeforeRender(() => {
178+
const [segments, limit, positions, colors] = [
179+
this.segments(),
180+
this.limit(),
181+
this.positions(),
182+
this.colors(),
183+
];
184+
for (let i = 0; i < limit; i++) {
185+
const segmentRef = segments[i];
186+
const segment = is.ref(segmentRef) ? segmentRef.nativeElement : segmentRef;
187+
if (segment) {
188+
positions[i * 6 + 0] = segment.start.x;
189+
positions[i * 6 + 1] = segment.start.y;
190+
positions[i * 6 + 2] = segment.start.z;
191+
192+
positions[i * 6 + 3] = segment.end.x;
193+
positions[i * 6 + 4] = segment.end.y;
194+
positions[i * 6 + 5] = segment.end.z;
195+
196+
colors[i * 6 + 0] = segment.color.r;
197+
colors[i * 6 + 1] = segment.color.g;
198+
colors[i * 6 + 2] = segment.color.b;
199+
200+
colors[i * 6 + 3] = segment.color.r;
201+
colors[i * 6 + 4] = segment.color.g;
202+
colors[i * 6 + 5] = segment.color.b;
203+
}
204+
}
205+
this.geometry.setColors(colors);
206+
this.geometry.setPositions(positions);
207+
this.line.computeLineDistances();
208+
});
209+
}
210+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { NgFor } from '@angular/common';
2+
import { CUSTOM_ELEMENTS_SCHEMA, Component } from '@angular/core';
3+
import { injectBeforeRender, injectNgtRef } from 'angular-three';
4+
import { NgtsOrbitControls } from 'angular-three-soba/controls';
5+
import { NgtsSegment, NgtsSegments, type SegmentObject } from 'angular-three-soba/performances';
6+
import { makeCanvasOptions, makeDecorators, makeStoryFunction } from '../setup-canvas';
7+
8+
@Component({
9+
standalone: true,
10+
template: `
11+
<ngts-segments [limit]="10_000" [lineWidth]="0.1">
12+
<ngts-segment
13+
*ngFor="let ref of refs"
14+
color="orange"
15+
[segmentRef]="ref"
16+
[start]="[0, 0, 0]"
17+
[end]="[0, 0, 0]"
18+
/>
19+
</ngts-segments>
20+
<ngts-orbit-controls />
21+
`,
22+
imports: [NgtsOrbitControls, NgtsSegment, NgtsSegments, NgFor],
23+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
24+
})
25+
class PerformanceSegmentsStory {
26+
refs = Array.from({ length: 10_000 }, () => injectNgtRef<SegmentObject>());
27+
28+
constructor() {
29+
injectBeforeRender(({ clock }) => {
30+
this.refs.forEach((ref, i) => {
31+
const time = clock.elapsedTime;
32+
const x = Math.sin((i / 5000) * Math.PI) * 10;
33+
const y = Math.cos((i / 5000) * Math.PI) * 10;
34+
const z = Math.cos((i * time) / 1000);
35+
ref.nativeElement.start.set(x, y, z);
36+
ref.nativeElement.end.set(x + Math.sin(time + i), y + Math.cos(time + i), z);
37+
ref.nativeElement.color.setRGB(x / 10, y / 10, z);
38+
});
39+
});
40+
}
41+
}
42+
43+
@Component({
44+
standalone: true,
45+
template: `
46+
<ngts-segments [limit]="6" [lineWidth]="2">
47+
<ngts-segment [start]="[0, 0, 0]" [end]="[10, 0, 0]" color="red" />
48+
<ngts-segment [start]="[0, 0, 0]" [end]="[0, 10, 0]" color="blue" />
49+
<ngts-segment [start]="[0, 0, 0]" [end]="[0, 0, 10]" color="green" />
50+
<ngts-segment [start]="[0, 0, 0]" [end]="[-10, 0, 0]" [color]="[1, 0, 0]" />
51+
<ngts-segment [start]="[0, 0, 0]" [end]="[0, -10, 0]" [color]="[0, 1, 0]" />
52+
<ngts-segment [start]="[0, 0, 0]" [end]="[0, 0, -10]" [color]="[1, 1, 0]" />
53+
</ngts-segments>
54+
<ngts-orbit-controls />
55+
`,
56+
imports: [NgtsSegments, NgtsSegment, NgtsOrbitControls],
57+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
58+
})
59+
class BasicSegmentsStory {}
60+
61+
export default {
62+
title: 'Performance/Segments',
63+
decorators: makeDecorators(),
64+
};
65+
66+
const canvasOptions = makeCanvasOptions({
67+
camera: { position: [10, 10, 10] },
68+
controls: false,
69+
});
70+
71+
export const Basic = makeStoryFunction(BasicSegmentsStory, canvasOptions);
72+
export const Performance = makeStoryFunction(PerformanceSegmentsStory, canvasOptions);

libs/soba/src/setup-canvas.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ const STORY_INPUTS = new InjectionToken<Signal<Record<string, unknown>>>('story
7979
8080
<ng-container *ngIf="canvasOptions.lights">
8181
<ngt-ambient-light [intensity]="canvasOptions.useLegacyLights ? 0.8 : 0.8 * Math.PI" />
82-
<ngt-point-light [intensity]="1" [position]="[0, 6, 0]" />
82+
<ngt-point-light [intensity]="canvasOptions.useLegacyLights ? 1 : 1 * Math.PI" [position]="[0, 6, 0]" />
8383
</ng-container>
8484
8585
<ng-container *ngIf="canvasOptions.controls">

0 commit comments

Comments
 (0)