Skip to content

Commit 72cef4a

Browse files
committed
segments
1 parent 07df894 commit 72cef4a

File tree

7 files changed

+288
-5
lines changed

7 files changed

+288
-5
lines changed

libs/core/src/lib/utils/parameters.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ export function vector2(options: Signal<NgtAnyRecord>, key: string, keepUndefine
8282
{ equal: (a, b) => !!a && !!b && a.equals(b) },
8383
);
8484
}
85+
86+
export function vector3(input: Signal<NgtVector3>): Signal<Vector3>;
87+
export function vector3(input: Signal<NgtVector3>, keepUndefined: true): Signal<Vector3>;
8588
export function vector3<TObject extends object>(
8689
options: Signal<TObject>,
8790
key: KeysOfType<TObject, NgtVector3>,
@@ -91,7 +94,29 @@ export function vector3<TObject extends object>(
9194
key: KeysOfType<TObject, NgtVector3>,
9295
keepUndefined: true,
9396
): Signal<Vector3 | undefined>;
94-
export function vector3(options: Signal<NgtAnyRecord>, key: string, keepUndefined = false) {
97+
export function vector3(
98+
inputOrOptions: Signal<NgtAnyRecord> | Signal<NgtVector3>,
99+
keyOrKeepUndefined?: string | true,
100+
keepUndefined?: boolean,
101+
) {
102+
if (typeof keyOrKeepUndefined === 'undefined' || typeof keyOrKeepUndefined === 'boolean') {
103+
keepUndefined = !!keyOrKeepUndefined;
104+
const input = inputOrOptions as Signal<NgtVector3>;
105+
return computed(
106+
() => {
107+
const value = input();
108+
if (keepUndefined && value == undefined) return undefined;
109+
if (typeof value === 'number') return new Vector3(value, value, value);
110+
else if (value) return new Vector3(...(value as Vector3Tuple));
111+
else return new Vector3();
112+
},
113+
{ equal: (a, b) => !!a && !!b && a.equals(b) },
114+
);
115+
}
116+
117+
const options = inputOrOptions as Signal<NgtAnyRecord>;
118+
const key = keyOrKeepUndefined as string;
119+
95120
return computed(
96121
() => {
97122
const value = options()[key];

libs/soba/performances/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './lib/adaptive-dpr';
22
export * from './lib/adaptive-events';
33
export * from './lib/instances/instances';
44
export * from './lib/points/points';
5+
export * from './lib/segments/segments';
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { NgtNode } from 'angular-three';
2+
import { Color, Vector3 } from 'three';
3+
4+
export type NgtSegmentObject = NgtNode<SegmentObject, typeof SegmentObject>;
5+
6+
export class SegmentObject {
7+
color: Color;
8+
start: Vector3;
9+
end: Vector3;
10+
11+
constructor() {
12+
this.color = new Color('white');
13+
this.start = new Vector3(0, 0, 0);
14+
this.end = new Vector3(0, 0, 0);
15+
}
16+
}
17+
18+
declare global {
19+
interface HTMLElementTagNameMap {
20+
/**
21+
* @rawOptions start|color|end
22+
*/
23+
'ngt-segment-object': NgtSegmentObject;
24+
}
25+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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 { extend, injectBeforeRender, NgtArgs, NgtVector3, omit, pick, resolveRef, vector3 } from 'angular-three';
13+
import { injectAutoEffect } from 'ngxtension/auto-effect';
14+
import { mergeInputs } from 'ngxtension/inject-inputs';
15+
import { ColorRepresentation, Vector2 } from 'three';
16+
import { Line2, LineMaterial, LineMaterialParameters, LineSegmentsGeometry } from 'three-stdlib';
17+
import { SegmentObject } from './segment-object';
18+
19+
@Component({
20+
selector: 'ngts-segment',
21+
standalone: true,
22+
template: `
23+
<ngt-segment-object #segment [color]="color()" [start]="normalizedStart()" [end]="normalizedEnd()" />
24+
`,
25+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
26+
changeDetection: ChangeDetectionStrategy.OnPush,
27+
})
28+
export class NgtsSegment {
29+
start = input.required<NgtVector3>();
30+
end = input.required<NgtVector3>();
31+
color = input<ColorRepresentation>();
32+
33+
normalizedStart = vector3(this.start);
34+
normalizedEnd = vector3(this.end);
35+
36+
segment = viewChild.required<ElementRef<SegmentObject>>('segment');
37+
38+
segments = inject(NgtsSegments);
39+
40+
constructor() {
41+
extend({ SegmentObject });
42+
const autoEffect = injectAutoEffect();
43+
44+
afterNextRender(() => {
45+
autoEffect(() => {
46+
return this.segments.subscribe(this.segment());
47+
});
48+
});
49+
}
50+
}
51+
52+
export interface NgtsSegmentsOptions extends LineMaterialParameters {
53+
limit: number;
54+
lineWidth: number;
55+
}
56+
57+
const defaultSegmentsOptions: NgtsSegmentsOptions = {
58+
limit: 1000,
59+
lineWidth: 1.0,
60+
};
61+
62+
@Component({
63+
selector: 'ngts-segments',
64+
standalone: true,
65+
template: `
66+
<ngt-primitive #line *args="[line]">
67+
<ngt-primitive *args="[geometry]" attach="geometry" />
68+
<ngt-primitive *args="[material]" attach="material" [parameters]="materialParameters()" />
69+
<ng-content />
70+
</ngt-primitive>
71+
`,
72+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
73+
changeDetection: ChangeDetectionStrategy.OnPush,
74+
imports: [NgtArgs],
75+
})
76+
export class NgtsSegments {
77+
options = input(defaultSegmentsOptions, { transform: mergeInputs(defaultSegmentsOptions) });
78+
parameters = omit(this.options, ['limit', 'lineWidth']);
79+
80+
private lineWidth = pick(this.options, 'lineWidth');
81+
private limit = pick(this.options, 'limit');
82+
83+
lineRef = viewChild<ElementRef<Line2>>('line');
84+
85+
segments: Array<ElementRef<SegmentObject> | SegmentObject> = [];
86+
87+
line = new Line2();
88+
material = new LineMaterial();
89+
geometry = new LineSegmentsGeometry();
90+
resolution = new Vector2(512, 512);
91+
92+
materialParameters = computed(() => ({
93+
vertexColors: true,
94+
resolution: this.resolution,
95+
linewidth: this.lineWidth(),
96+
...this.parameters(),
97+
}));
98+
99+
positions = computed(() => {
100+
const limit = this.limit();
101+
return Array.from({ length: limit * 6 }, () => 0);
102+
});
103+
colors = computed(() => {
104+
const limit = this.limit();
105+
return Array.from({ length: limit * 6 }, () => 0);
106+
});
107+
108+
constructor() {
109+
injectBeforeRender(() => {
110+
const [limit, positions, colors] = [this.limit(), this.positions(), this.colors()];
111+
112+
for (let i = 0; i < limit; i++) {
113+
const segment = resolveRef(this.segments[i]);
114+
if (segment) {
115+
positions[i * 6 + 0] = segment.start.x;
116+
positions[i * 6 + 1] = segment.start.y;
117+
positions[i * 6 + 2] = segment.start.z;
118+
119+
positions[i * 6 + 3] = segment.end.x;
120+
positions[i * 6 + 4] = segment.end.y;
121+
positions[i * 6 + 5] = segment.end.z;
122+
123+
colors[i * 6 + 0] = segment.color.r;
124+
colors[i * 6 + 1] = segment.color.g;
125+
colors[i * 6 + 2] = segment.color.b;
126+
127+
colors[i * 6 + 3] = segment.color.r;
128+
colors[i * 6 + 4] = segment.color.g;
129+
colors[i * 6 + 5] = segment.color.b;
130+
}
131+
}
132+
133+
this.geometry.setColors(colors);
134+
this.geometry.setPositions(positions);
135+
this.line.computeLineDistances();
136+
});
137+
}
138+
139+
subscribe(ref: ElementRef<SegmentObject> | SegmentObject) {
140+
this.segments.push(ref);
141+
return () => {
142+
this.segments = this.segments.filter((i) => i !== ref);
143+
};
144+
}
145+
}

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,10 @@ export default {
170170
decorators: makeDecorators(),
171171
} as Meta;
172172

173-
// <Setup cameraPosition={new Vector3(10, 10, 10)}>
174-
175173
export const BasicPointsBuffer = makeStoryFunction(BasicPointsBufferStory, {
176-
camera: { position: [10, 10, 10] },
174+
camera: { position: [5, 5, 5] },
177175
});
178176

179177
export const BasicPointsInstances = makeStoryFunction(BasicPointsInstancesStory, {
180-
camera: { position: [10, 10, 10] },
178+
camera: { position: [5, 5, 5] },
181179
});
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, input, viewChildren } from '@angular/core';
2+
import { Meta } from '@storybook/angular';
3+
import { injectBeforeRender } from 'angular-three';
4+
import { NgtsOrbitControls } from 'angular-three-soba/controls';
5+
import { NgtsSegment, NgtsSegments } from 'angular-three-soba/performances';
6+
import { makeDecorators, makeStoryObject, number } from '../setup-canvas';
7+
8+
@Component({
9+
standalone: true,
10+
template: `
11+
<ngts-segments [options]="{ limit: limit(), lineWidth: lineWidth() }">
12+
@for (index of count; track $index) {
13+
<ngts-segment [start]="[0, 0, 0]" [end]="[0, 0, 0]" [color]="'orange'" />
14+
}
15+
</ngts-segments>
16+
<ngts-orbit-controls />
17+
`,
18+
imports: [NgtsSegments, NgtsSegment, NgtsOrbitControls],
19+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
20+
changeDetection: ChangeDetectionStrategy.OnPush,
21+
})
22+
class PerformanceSegmentsStory {
23+
limit = input(10000);
24+
lineWidth = input(0.1);
25+
26+
count = Array.from({ length: 10000 }).map((_, i) => i);
27+
28+
segmentsRef = viewChildren(NgtsSegment);
29+
30+
constructor() {
31+
injectBeforeRender(({ clock }) => {
32+
const segments = this.segmentsRef();
33+
segments.forEach((segment, index) => {
34+
const segmentObject = segment.segment().nativeElement;
35+
36+
const time = clock.elapsedTime;
37+
const x = Math.sin((index / 5000) * Math.PI) * 10;
38+
const y = Math.cos((index / 5000) * Math.PI) * 10;
39+
const z = Math.cos((index * time) / 1000);
40+
segmentObject.start.set(x, y, z);
41+
segmentObject.end.set(x + Math.sin(time + index), y + Math.cos(time + index), z);
42+
segmentObject.color.setRGB(x / 10, y / 10, z);
43+
});
44+
});
45+
}
46+
}
47+
48+
@Component({
49+
standalone: true,
50+
template: `
51+
<ngts-segments [options]="{ limit: limit(), lineWidth: lineWidth() }">
52+
<ngts-segment [start]="[0, 0, 0]" [end]="[10, 0, 0]" [color]="'red'" />
53+
<ngts-segment [start]="[0, 0, 0]" [end]="[0, 10, 0]" [color]="'blue'" />
54+
<ngts-segment [start]="[0, 0, 0]" [end]="[0, 0, 10]" [color]="'green'" />
55+
<ngts-segment [start]="[0, 0, 0]" [end]="[-10, 0, 0]" [color]="'rgb(255, 0, 0)'" />
56+
<ngts-segment [start]="[0, 0, 0]" [end]="[0, -10, 0]" [color]="'rgb(0, 255, 0)'" />
57+
<ngts-segment [start]="[0, 0, 0]" [end]="[0, 0, -10]" [color]="'rgb(0, 0, 255)'" />
58+
</ngts-segments>
59+
<ngts-orbit-controls />
60+
`,
61+
changeDetection: ChangeDetectionStrategy.OnPush,
62+
imports: [NgtsSegments, NgtsSegment, NgtsOrbitControls],
63+
})
64+
class BasicSegmentsStory {
65+
limit = input(6);
66+
lineWidth = input(2.0);
67+
}
68+
69+
export default {
70+
title: 'Performances/Segments',
71+
decorators: makeDecorators(),
72+
} as Meta;
73+
74+
export const BasicSegments = makeStoryObject(BasicSegmentsStory, {
75+
canvasOptions: { camera: { position: [10, 10, 10] }, controls: false },
76+
argsOptions: {
77+
limit: number(6, { range: true, min: 1, max: 100, step: 1 }),
78+
lineWidth: number(2.0, { range: true, min: 0.1, max: 10, step: 0.1 }),
79+
},
80+
});
81+
82+
export const PerformanceSegments = makeStoryObject(PerformanceSegmentsStory, {
83+
canvasOptions: { camera: { position: [10, 10, 10] }, controls: false },
84+
argsOptions: {
85+
limit: number(10000, { range: true, min: 1, max: 10000, step: 1 }),
86+
lineWidth: number(0.1, { range: true, min: 0.1, max: 10, step: 0.1 }),
87+
},
88+
});

tools/scripts/generate-soba-json.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const entryPoints = [
1616
'vanilla-exports/src/index.ts',
1717
'performances/src/lib/instances/position-mesh.ts',
1818
'performances/src/lib/points/position-point.ts',
19+
'performances/src/lib/segments/segment-object.ts',
1920
];
2021

2122
const paths = [];

0 commit comments

Comments
 (0)