Skip to content

Commit f90fddd

Browse files
committed
v0.5.0: Interactive 3D Tank Visualizer — Allow users to visually configure and inspect tank dimensions and fill levels in an intuitive 3D environment.
1 parent 3a7a634 commit f90fddd

4 files changed

Lines changed: 227 additions & 1 deletion

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@adametherzlab/tank-level",
3-
"version": "0.4.0",
3+
"version": "0.5.0",
44
"description": "Tank level calculator — volume from height for cylindrical, rectangular, conical & spherical vessels with strapping tables, fill rate, alarms & inventory",
55
"type": "module",
66
"main": "src/index.ts",

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ export { convertVolume, convertPercentage } from './converter';
2626
export { strappingTable, fillRate, tankAlarms, tankInventory } from './monitoring';
2727
export { TankConfigStore } from './storage';
2828
export type { SavedTankConfig, ListConfigOptions, SaveConfigInput } from './storage';
29+
export { TankVisualizer } from './visualizer';

src/visualizer.ts

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import * as THREE from 'three';
2+
import type { VesselConfig, Dimensions } from './types';
3+
4+
export class TankVisualizer {
5+
private scene: THREE.Scene;
6+
private camera: THREE.PerspectiveCamera;
7+
private renderer: THREE.WebGLRenderer;
8+
private tankMesh?: THREE.Mesh;
9+
private liquidMesh?: THREE.Mesh;
10+
11+
constructor(private container: HTMLElement) {
12+
this.scene = new THREE.Scene();
13+
this.camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
14+
this.renderer = new THREE.WebGLRenderer({ antialias: true });
15+
16+
this.initScene();
17+
this.setupLights();
18+
this.setupCamera();
19+
}
20+
21+
private initScene() {
22+
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
23+
this.container.appendChild(this.renderer.domElement);
24+
this.camera.position.z = 5;
25+
}
26+
27+
private setupLights() {
28+
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
29+
this.scene.add(ambientLight);
30+
31+
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
32+
directionalLight.position.set(5, 5, 5);
33+
this.scene.add(directionalLight);
34+
}
35+
36+
private setupCamera() {
37+
const controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
38+
controls.enableDamping = true;
39+
controls.dampingFactor = 0.05;
40+
}
41+
42+
private createTankGeometry(config: VesselConfig): THREE.BufferGeometry {
43+
switch (config.type) {
44+
case 'cylindrical':
45+
return this.createCylindricalGeometry(config.dimensions, config.orientation);
46+
case 'rectangular':
47+
return this.createRectangularGeometry(config.dimensions);
48+
case 'spherical':
49+
return this.createSphericalGeometry(config.dimensions);
50+
case 'conical':
51+
return this.createConicalGeometry(config.dimensions);
52+
case 'elliptical':
53+
return this.createEllipticalGeometry(config.dimensions);
54+
default:
55+
throw new Error('Unsupported vessel type');
56+
}
57+
}
58+
59+
private createCylindricalGeometry(dimensions: Dimensions, orientation?: string) {
60+
const radius = (dimensions.diameter || 0) / 2;
61+
const height = dimensions.height || 0;
62+
const geometry = new THREE.CylinderGeometry(radius, radius, height, 32);
63+
64+
if (orientation === 'horizontal') {
65+
geometry.rotateZ(Math.PI / 2);
66+
}
67+
return geometry;
68+
}
69+
70+
private createRectangularGeometry(dimensions: Dimensions) {
71+
return new THREE.BoxGeometry(
72+
dimensions.length || 0,
73+
dimensions.width || 0,
74+
dimensions.height || 0
75+
);
76+
}
77+
78+
private createSphericalGeometry(dimensions: Dimensions) {
79+
const radius = (dimensions.diameter || 0) / 2;
80+
return new THREE.SphereGeometry(radius, 32, 32);
81+
}
82+
83+
private createConicalGeometry(dimensions: Dimensions) {
84+
const topRadius = (dimensions.topDiameter || 0) / 2;
85+
const bottomRadius = (dimensions.bottomDiameter || 0) / 2;
86+
const height = dimensions.height || 0;
87+
return new THREE.CylinderGeometry(topRadius, bottomRadius, height, 32);
88+
}
89+
90+
private createEllipticalGeometry(dimensions: Dimensions) {
91+
const major = (dimensions.majorDiameter || 0) / 2;
92+
const minor = (dimensions.minorDiameter || 0) / 2;
93+
const length = dimensions.length || 0;
94+
return new THREE.CapsuleGeometry(major, length, 4, 8);
95+
}
96+
97+
updateTank(config: VesselConfig, fillLevel: number) {
98+
this.clearScene();
99+
100+
// Create tank mesh
101+
const tankGeometry = this.createTankGeometry(config);
102+
const tankMaterial = new THREE.MeshPhongMaterial({
103+
color: 0xcccccc,
104+
transparent: true,
105+
opacity: 0.7
106+
});
107+
this.tankMesh = new THREE.Mesh(tankGeometry, tankMaterial);
108+
this.scene.add(this.tankMesh);
109+
110+
// Create liquid surface
111+
const liquidGeometry = this.createLiquidGeometry(config, fillLevel);
112+
const liquidMaterial = new THREE.MeshPhongMaterial({
113+
color: 0x0099ff,
114+
transparent: true,
115+
opacity: 0.8
116+
});
117+
this.liquidMesh = new THREE.Mesh(liquidGeometry, liquidMaterial);
118+
this.scene.add(this.liquidMesh);
119+
}
120+
121+
private createLiquidGeometry(config: VesselConfig, fillLevel: number) {
122+
switch (config.type) {
123+
case 'cylindrical':
124+
return this.createCylindricalLiquid(config.dimensions, fillLevel, config.orientation);
125+
case 'rectangular':
126+
return this.createRectangularLiquid(config.dimensions, fillLevel);
127+
case 'spherical':
128+
return this.createSphericalLiquid(config.dimensions, fillLevel);
129+
default:
130+
return new THREE.PlaneGeometry(1, 1);
131+
}
132+
}
133+
134+
private createCylindricalLiquid(dimensions: Dimensions, fillLevel: number, orientation?: string) {
135+
const radius = (dimensions.diameter || 0) / 2;
136+
const height = orientation === 'horizontal' ? dimensions.diameter || 0 : dimensions.height || 0;
137+
const liquidHeight = Math.min(fillLevel, height);
138+
139+
return new THREE.CylinderGeometry(
140+
radius,
141+
radius,
142+
liquidHeight,
143+
32
144+
);
145+
}
146+
147+
private createRectangularLiquid(dimensions: Dimensions, fillLevel: number) {
148+
return new THREE.BoxGeometry(
149+
dimensions.length || 0,
150+
dimensions.width || 0,
151+
Math.min(fillLevel, dimensions.height || 0)
152+
);
153+
}
154+
155+
private createSphericalLiquid(dimensions: Dimensions, fillLevel: number) {
156+
const radius = (dimensions.diameter || 0) / 2;
157+
const liquidHeight = Math.min(fillLevel, dimensions.diameter || 0);
158+
return new THREE.SphereGeometry(
159+
radius,
160+
32,
161+
32,
162+
0,
163+
Math.PI * 2,
164+
0,
165+
Math.PI * (liquidHeight / (dimensions.diameter || 1))
166+
);
167+
}
168+
169+
private clearScene() {
170+
if (this.tankMesh) {
171+
this.scene.remove(this.tankMesh);
172+
this.tankMesh.geometry.dispose();
173+
}
174+
if (this.liquidMesh) {
175+
this.scene.remove(this.liquidMesh);
176+
this.liquidMesh.geometry.dispose();
177+
}
178+
}
179+
180+
animate() {
181+
requestAnimationFrame(() => this.animate());
182+
this.renderer.render(this.scene, this.camera);
183+
}
184+
}

tests/visualizer.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { describe, it, expect } from 'bun:test';
2+
import { TankVisualizer } from '../src/visualizer';
3+
import type { VesselConfig } from '../src/types';
4+
5+
describe('TankVisualizer', () => {
6+
const mockContainer = document.createElement('div');
7+
8+
it('should create cylindrical tank geometry', () => {
9+
const visualizer = new TankVisualizer(mockContainer);
10+
const config: VesselConfig = {
11+
type: 'cylindrical',
12+
dimensions: { diameter: 2, height: 5 },
13+
orientation: 'vertical'
14+
};
15+
16+
visualizer.updateTank(config, 2.5);
17+
expect(visualizer).toBeDefined();
18+
});
19+
20+
it('should create rectangular tank geometry', () => {
21+
const visualizer = new TankVisualizer(mockContainer);
22+
const config: VesselConfig = {
23+
type: 'rectangular',
24+
dimensions: { length: 4, width: 3, height: 2 }
25+
};
26+
27+
visualizer.updateTank(config, 1);
28+
expect(visualizer).toBeDefined();
29+
});
30+
31+
it('should create spherical tank geometry', () => {
32+
const visualizer = new TankVisualizer(mockContainer);
33+
const config: VesselConfig = {
34+
type: 'spherical',
35+
dimensions: { diameter: 4 }
36+
};
37+
38+
visualizer.updateTank(config, 3);
39+
expect(visualizer).toBeDefined();
40+
});
41+
});

0 commit comments

Comments
 (0)