From 773ba30d15aacc395faa145d91a263b5ec7e95ea Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Sat, 11 Apr 2026 22:09:00 +0200 Subject: [PATCH] Add water volume utility functions --- src/runtime-three/water-volume-utils.ts | 108 ++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 src/runtime-three/water-volume-utils.ts diff --git a/src/runtime-three/water-volume-utils.ts b/src/runtime-three/water-volume-utils.ts new file mode 100644 index 00000000..efeec7c4 --- /dev/null +++ b/src/runtime-three/water-volume-utils.ts @@ -0,0 +1,108 @@ +import { Euler, Quaternion, Vector3 } from "three"; + +import type { RuntimeWaterVolume } from "./runtime-scene-build"; + +export interface RuntimeWaterContact { + volume: RuntimeWaterVolume; + localPoint: { + x: number; + y: number; + z: number; + }; + surfaceHeight: number; +} + +function getWaterVolumeQuaternion(volume: RuntimeWaterVolume): Quaternion { + return new Quaternion().setFromEuler( + new Euler( + (volume.rotationDegrees.x * Math.PI) / 180, + (volume.rotationDegrees.y * Math.PI) / 180, + (volume.rotationDegrees.z * Math.PI) / 180, + "XYZ" + ) + ); +} + +export function getWaterVolumeLocalPoint( + point: { x: number; y: number; z: number }, + volume: RuntimeWaterVolume +) { + const offset = new Vector3( + point.x - volume.center.x, + point.y - volume.center.y, + point.z - volume.center.z + ); + const inverseRotation = getWaterVolumeQuaternion(volume).invert(); + + offset.applyQuaternion(inverseRotation); + + return { + x: offset.x, + y: offset.y, + z: offset.z + }; +} + +export function isPointInsideWaterVolume( + point: { x: number; y: number; z: number }, + volume: RuntimeWaterVolume +): boolean { + const localPoint = getWaterVolumeLocalPoint(point, volume); + + return ( + Math.abs(localPoint.x) <= volume.size.x * 0.5 && + Math.abs(localPoint.y) <= volume.size.y * 0.5 && + Math.abs(localPoint.z) <= volume.size.z * 0.5 + ); +} + +export function resolveWaterSurfaceHeightAtPoint( + volume: RuntimeWaterVolume, + point: { x: number; y: number; z: number } +): number { + const rotation = getWaterVolumeQuaternion(volume); + const topCenter = new Vector3(0, volume.size.y * 0.5, 0) + .applyQuaternion(rotation) + .add(new Vector3(volume.center.x, volume.center.y, volume.center.z)); + const normal = new Vector3(0, 1, 0).applyQuaternion(rotation); + + if (Math.abs(normal.y) <= 1e-5) { + return topCenter.y; + } + + return ( + topCenter.y - + (normal.x * (point.x - topCenter.x) + + normal.z * (point.z - topCenter.z)) / + normal.y + ); +} + +export function resolveWaterContact( + point: { x: number; y: number; z: number }, + volumes: RuntimeWaterVolume[] +): RuntimeWaterContact | null { + let bestContact: RuntimeWaterContact | null = null; + + for (const volume of volumes) { + if (!isPointInsideWaterVolume(point, volume)) { + continue; + } + + const surfaceHeight = resolveWaterSurfaceHeightAtPoint(volume, point); + const contact: RuntimeWaterContact = { + volume, + localPoint: getWaterVolumeLocalPoint(point, volume), + surfaceHeight + }; + + if ( + bestContact === null || + contact.surfaceHeight > bestContact.surfaceHeight + ) { + bestContact = contact; + } + } + + return bestContact; +} \ No newline at end of file