Skip to content

Commit 842c9fb

Browse files
authored
Merge pull request #149 from remcoder/feature/custom-volume
2 parents fa1111f + 3a12364 commit 842c9fb

3 files changed

Lines changed: 246 additions & 21 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { test, expect } from 'vitest';
2+
import { BufferGeometry, Vector3 } from 'three';
3+
import { ExtrusionGeometry } from '../extrusion-geometry';
4+
5+
test('ExtrusionGeometry should be defined', () => {
6+
expect(ExtrusionGeometry).toBeDefined();
7+
});
8+
9+
test('ExtrusionGeometry extends BufferGeometry', () => {
10+
const geometry = new ExtrusionGeometry();
11+
expect(geometry).toBeInstanceOf(ExtrusionGeometry);
12+
expect(geometry).toBeInstanceOf(BufferGeometry);
13+
});
14+
15+
test('ExtrusionGeometry should be of type "ExtrusionGeometry"', () => {
16+
const geometry = new ExtrusionGeometry();
17+
expect(geometry.type).toBe('ExtrusionGeometry');
18+
});
19+
20+
test('ExtrusionGeometry should have default values', () => {
21+
const geometry = new ExtrusionGeometry();
22+
expect(geometry.parameters.points).toEqual([new Vector3()]);
23+
expect(geometry.parameters.lineWidth).toBe(0.6);
24+
expect(geometry.parameters.lineHeight).toBe(0.2);
25+
expect(geometry.parameters.radialSegments).toBe(8);
26+
});
27+
28+
test('ExtrusionGeometry constructor should set values', () => {
29+
const points = [new Vector3(), new Vector3()];
30+
const lineWidth = 0.5;
31+
const lineHeight = 0.3;
32+
const radialSegments = 10;
33+
const geometry = new ExtrusionGeometry(points, lineWidth, lineHeight, radialSegments);
34+
expect(geometry.parameters.points).toEqual(points);
35+
expect(geometry.parameters.lineWidth).toBe(lineWidth);
36+
expect(geometry.parameters.lineHeight).toBe(lineHeight);
37+
expect(geometry.parameters.radialSegments).toBe(radialSegments);
38+
});
39+
40+
test('ExtrusionGeometry should set normals, uvs and indices', () => {
41+
const points = [new Vector3(0, 0, 0), new Vector3(1, 0, 0)];
42+
const geometry = new ExtrusionGeometry(points);
43+
expect(geometry.attributes.position).toBeDefined();
44+
expect(geometry.attributes.normal).toBeDefined();
45+
expect(geometry.attributes.uv).toBeDefined();
46+
});
47+
48+
test('ExtrusionGeometry should generate buffer data', () => {
49+
const points = [new Vector3(0, 0, 0), new Vector3(1, 0, 0)];
50+
const geometry = new ExtrusionGeometry(points);
51+
expect(geometry.attributes.position.array.length).toBeGreaterThan(0);
52+
expect(geometry.attributes.normal.array.length).toBeGreaterThan(0);
53+
expect(geometry.attributes.uv.array.length).toBeGreaterThan(0);
54+
});

src/extrusion-geometry.ts

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import { BufferGeometry, Float32BufferAttribute, Vector2, Vector3 } from 'three';
2+
3+
class ExtrusionGeometry extends BufferGeometry {
4+
parameters: {
5+
points: Vector3[];
6+
lineWidth: number;
7+
lineHeight: number;
8+
radialSegments: number;
9+
closed: boolean;
10+
};
11+
constructor(
12+
points: Vector3[] = [new Vector3()],
13+
lineWidth: number = 0.6,
14+
lineHeight: number = 0.2,
15+
radialSegments: number = 8
16+
) {
17+
super();
18+
19+
this.type = 'ExtrusionGeometry';
20+
21+
this.parameters = {
22+
points: points,
23+
lineWidth: lineWidth,
24+
lineHeight: lineHeight,
25+
radialSegments: radialSegments,
26+
closed: false
27+
};
28+
29+
// helper variables
30+
31+
const vertex = new Vector3();
32+
const normal = new Vector3();
33+
const uv = new Vector2();
34+
35+
// buffer
36+
37+
const vertices: number[] = [];
38+
const normals: number[] = [];
39+
const uvs: number[] = [];
40+
const indices: number[] = [];
41+
42+
// create buffer data
43+
44+
generateBufferData();
45+
46+
// build geometry
47+
48+
this.setIndex(indices);
49+
this.setAttribute('position', new Float32BufferAttribute(vertices, 3));
50+
this.setAttribute('normal', new Float32BufferAttribute(normals, 3));
51+
this.setAttribute('uv', new Float32BufferAttribute(uvs, 2));
52+
53+
// functions
54+
55+
function generateBufferData(): void {
56+
for (let i = 0; i < points.length; i++) {
57+
generateSegment(i);
58+
}
59+
60+
// if the geometry is not closed, generate the last row of vertices and normals
61+
// at the regular position on the given path
62+
//
63+
// if the geometry is closed, duplicate the first row of vertices and normals (uvs will differ)
64+
65+
generateSegment(closed === false ? points.length - 1 : 0);
66+
67+
// uvs are generated in a separate function.
68+
// this makes it easy compute correct values for closed geometries
69+
70+
generateUVs();
71+
72+
// finally create faces
73+
74+
generateIndices();
75+
}
76+
77+
function generateSegment(i: number): void {
78+
// First get the tangent to the corner between the two segments.
79+
80+
const [P, N, B] = computeCornerAngles(i);
81+
82+
// generate points around the tangent
83+
84+
for (let j = 0; j <= radialSegments; j++) {
85+
const v = (j / radialSegments) * Math.PI * 2;
86+
const sin = Math.sin(v);
87+
const cos = -Math.cos(v);
88+
89+
// normal
90+
normal.x = cos * N.x + sin * B.x;
91+
normal.y = cos * N.y + sin * B.y;
92+
normal.z = cos * N.z + sin * B.z;
93+
94+
normal.normalize();
95+
normals.push(normal.x, normal.y, normal.z);
96+
97+
// vertex
98+
99+
vertex.x = P.x + lineWidth * normal.x * 0.5;
100+
vertex.y = P.y + lineWidth * normal.y * 0.5;
101+
vertex.z = P.z + lineHeight * normal.z * 0.5;
102+
vertices.push(vertex.x, vertex.y, vertex.z);
103+
}
104+
}
105+
106+
function generateIndices(): void {
107+
for (let j = 1; j < points.length; j++) {
108+
for (let i = 1; i <= radialSegments; i++) {
109+
const a = (radialSegments + 1) * (j - 1) + (i - 1);
110+
const b = (radialSegments + 1) * j + (i - 1);
111+
const c = (radialSegments + 1) * j + i;
112+
const d = (radialSegments + 1) * (j - 1) + i;
113+
114+
// faces
115+
116+
indices.push(a, b, d);
117+
indices.push(b, c, d);
118+
}
119+
}
120+
}
121+
122+
function generateUVs(): void {
123+
for (let i = 0; i < points.length; i++) {
124+
for (let j = 0; j <= radialSegments; j++) {
125+
uv.x = i / points.length;
126+
uv.y = j / radialSegments;
127+
128+
uvs.push(uv.x, uv.y);
129+
}
130+
}
131+
}
132+
133+
function computeCornerAngles(i: number): Array<Vector3> {
134+
const P = points[i];
135+
const tangent = new Vector3();
136+
const N = new Vector3();
137+
const B = new Vector3();
138+
const vec = new Vector3();
139+
140+
tangent
141+
.copy(P)
142+
.sub(points[i - 1] || P)
143+
.normalize()
144+
.add((points[i + 1] || P).clone().sub(P).normalize())
145+
.normalize();
146+
147+
// Calculate the normal and binormal vectors for the segment
148+
// it used to be pre-computed using a curve `.computeFrenetFrames`
149+
let min = Number.MAX_VALUE;
150+
const tx = Math.abs(tangent.x);
151+
const ty = Math.abs(tangent.y);
152+
const tz = Math.abs(tangent.z);
153+
154+
if (tx <= min) {
155+
min = tx;
156+
N.set(1, 0, 0);
157+
}
158+
159+
if (ty <= min) {
160+
min = ty;
161+
N.set(0, 1, 0);
162+
}
163+
164+
if (tz <= min) {
165+
N.set(0, 0, 1);
166+
}
167+
168+
vec.crossVectors(tangent, N).normalize();
169+
170+
N.crossVectors(tangent, vec);
171+
B.crossVectors(tangent, N);
172+
173+
return [P, N, B];
174+
}
175+
}
176+
}
177+
178+
export { ExtrusionGeometry };

src/webgl-preview.ts

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
AmbientLight,
1010
AxesHelper,
1111
BufferGeometry,
12-
CatmullRomCurve3,
1312
Color,
1413
ColorRepresentation,
1514
Euler,
@@ -24,11 +23,12 @@ import {
2423
PointLight,
2524
REVISION,
2625
Scene,
27-
TubeGeometry,
2826
Vector3,
2927
WebGLRenderer
3028
} from 'three';
3129

30+
import { ExtrusionGeometry } from './extrusion-geometry';
31+
3232
type RenderLayer = { extrusion: number[]; travel: number[]; z: number };
3333
type GVector3 = {
3434
x: number;
@@ -606,40 +606,33 @@ export class WebGLPreview {
606606

607607
addTubeLine(vertices: number[], color: number): void {
608608
let curvePoints: Vector3[] = [];
609-
const curves: CatmullRomCurve3[] = [];
609+
const extrusionPaths: Vector3[][] = [];
610610

611611
// Merging into one curve for performance
612612
for (let i = 0; i < vertices.length; i += 6) {
613-
const v = vertices.slice(i, i + 6);
613+
const v = vertices.slice(i, i + 9);
614614
const startPoint = new Vector3(v[0], v[1], v[2]);
615615
const endPoint = new Vector3(v[3], v[4], v[5]);
616+
const nextPoint = new Vector3(v[6], v[7], v[8]);
616617

617-
if (curvePoints.length === 0) {
618-
curvePoints.push(startPoint);
619-
}
618+
curvePoints.push(startPoint);
620619

621-
if (!curvePoints[curvePoints.length - 1].equals(startPoint)) {
622-
curves.push(new CatmullRomCurve3(curvePoints, false, 'catmullrom', 0));
620+
if (!endPoint.equals(nextPoint)) {
621+
curvePoints.push(endPoint);
622+
extrusionPaths.push(curvePoints);
623623
curvePoints = [];
624-
curvePoints.push(startPoint);
625624
}
626-
627-
curvePoints.push(endPoint);
628625
}
629626

630-
if (curvePoints.length > 1) {
631-
curves.push(new CatmullRomCurve3(curvePoints, false, 'catmullrom', 0));
632-
}
627+
extrusionPaths.forEach((extrusionPath) => {
628+
const geometry = new ExtrusionGeometry(extrusionPath, this.extrusionWidth, 0.2, 4);
629+
this.disposables.push(geometry);
633630

634-
curves.forEach((curve) => {
635631
const material = new MeshLambertMaterial({ color: color });
636632
this.disposables.push(material);
637-
const segments = Math.ceil(curve.getLength() * 2);
638-
const geometry = new TubeGeometry(curve, segments, this.extrusionWidth / 2, 4, false);
639-
this.disposables.push(geometry);
640-
const lineSegments = new Mesh(geometry, material);
641633

642-
this.group?.add(lineSegments);
634+
const mesh = new Mesh(geometry, material);
635+
this.group?.add(mesh);
643636
});
644637
}
645638

0 commit comments

Comments
 (0)