import { describe, expect, it } from 'vitest'; import * as THREE from 'three'; import { applyCrossSectionClipping, clampCrossSectionOffset, createCrossSectionSnapshot, createSafeBounds, getAxisNormal, getCrossSectionPlaneVisual, getCrossSectionPoint, measureObjectBounds, normalizeCrossSectionSettings, } from './crossSection'; describe('cross-section math', () => { it('normalizes missing and invalid settings safely', () => { expect(normalizeCrossSectionSettings(null)).toEqual({ enabled: false, axis: 'x', offset: 0, invert: false, }); expect(normalizeCrossSectionSettings({ enabled: true, axis: 'z', offset: 4, invert: true })).toEqual({ enabled: true, axis: 'z', offset: 1, invert: true, }); expect(clampCrossSectionOffset(Number.NaN)).toBe(0); expect(clampCrossSectionOffset(-8)).toBe(-1); expect(clampCrossSectionOffset(0.25)).toBe(0.25); }); it('builds axis normals and supports inverted clipping direction', () => { expectVectorClose(getAxisNormal('x'), new THREE.Vector3(1, 0, 0)); expectVectorClose(getAxisNormal('y', true), new THREE.Vector3(0, -1, 0)); expectVectorClose(getAxisNormal('z'), new THREE.Vector3(0, 0, 1)); }); it('maps normalized offsets onto the selected model axis', () => { const bounds = new THREE.Box3( new THREE.Vector3(-2, -10, 4), new THREE.Vector3(6, 30, 12), ); const point = getCrossSectionPoint(bounds, 'x', 0.5); expect(point.x).toBeCloseTo(4); expect(point.y).toBeCloseTo(10); expect(point.z).toBeCloseTo(8); }); it('creates a plane through the resolved point with the expected normal', () => { const bounds = new THREE.Box3( new THREE.Vector3(-2, -2, -2), new THREE.Vector3(2, 2, 2), ); const snapshot = createCrossSectionSnapshot(bounds, { enabled: true, axis: 'x', offset: 0, }); expect(snapshot).not.toBeNull(); expectVectorClose(snapshot!.normal, new THREE.Vector3(1, 0, 0)); expect(snapshot!.point.x).toBeCloseTo(0); expect(snapshot!.plane.distanceToPoint(snapshot!.point)).toBeCloseTo(0); expect(snapshot!.plane.distanceToPoint(new THREE.Vector3(1, 0, 0))).toBeGreaterThan(0); expect(snapshot!.plane.distanceToPoint(new THREE.Vector3(-1, 0, 0))).toBeLessThan(0); }); it('flips plane normal without moving the cross-section point', () => { const bounds = new THREE.Box3( new THREE.Vector3(-4, -4, -4), new THREE.Vector3(4, 4, 4), ); const normal = createCrossSectionSnapshot(bounds, { enabled: true, axis: 'y', offset: 0.25, }); const inverted = createCrossSectionSnapshot(bounds, { enabled: true, axis: 'y', offset: 0.25, invert: true, }); expect(normal).not.toBeNull(); expect(inverted).not.toBeNull(); expectVectorClose(normal!.point, inverted!.point); expectVectorClose(normal!.normal, new THREE.Vector3(0, 1, 0)); expectVectorClose(inverted!.normal, new THREE.Vector3(0, -1, 0)); expect(normal!.plane.distanceToPoint(normal!.point)).toBeCloseTo(0); expect(inverted!.plane.distanceToPoint(inverted!.point)).toBeCloseTo(0); }); it('returns finite fallback bounds for empty objects and degenerate axes', () => { const empty = new THREE.Object3D(); const measured = measureObjectBounds(empty); expect(measured.isEmpty()).toBe(false); expect(measured.min.x).toBeLessThan(measured.max.x); expect(measured.min.y).toBeLessThan(measured.max.y); expect(measured.min.z).toBeLessThan(measured.max.z); const flat = createSafeBounds( new THREE.Box3(new THREE.Vector3(1, 2, 3), new THREE.Vector3(1, 8, 3)), ); expect(flat.max.x - flat.min.x).toBeGreaterThan(0); expect(flat.max.y - flat.min.y).toBeGreaterThan(0); expect(flat.max.z - flat.min.z).toBeGreaterThan(0); }); it('derives helper plane dimensions from the two non-normal axes', () => { const bounds = new THREE.Box3( new THREE.Vector3(-5, -2, -1), new THREE.Vector3(5, 2, 1), ); const visual = getCrossSectionPlaneVisual(bounds, { enabled: true, axis: 'z', offset: 0, }, 1); expect(visual).not.toBeNull(); expect(visual!.width).toBeCloseTo(10); expect(visual!.height).toBeCloseTo(4); expectVectorClose(visual!.position, new THREE.Vector3(0, 0, 0)); }); }); describe('applyCrossSectionClipping', () => { it('adds a clipping plane to mesh materials and restores previous state', () => { const existingPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), -2); const crossSectionPlane = new THREE.Plane(new THREE.Vector3(1, 0, 0), 0); const material = new THREE.MeshStandardMaterial({ color: 'red' }); material.clippingPlanes = [existingPlane]; material.clipShadows = false; const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), material); const restore = applyCrossSectionClipping(mesh, crossSectionPlane); expect(material.clippingPlanes).toHaveLength(2); expect(material.clippingPlanes?.[0]).toBe(existingPlane); expect(material.clippingPlanes?.[1]).toBe(crossSectionPlane); expect(material.clipShadows).toBe(true); restore(); expect(material.clippingPlanes).toHaveLength(1); expect(material.clippingPlanes?.[0]).toBe(existingPlane); expect(material.clipShadows).toBe(false); mesh.geometry.dispose(); material.dispose(); }); it('handles material arrays and avoids applying the same material twice', () => { const sharedMaterial = new THREE.MeshBasicMaterial(); const uniqueMaterial = new THREE.MeshBasicMaterial(); const crossSectionPlane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0); const mesh = new THREE.Mesh( new THREE.BoxGeometry(1, 1, 1), [sharedMaterial, uniqueMaterial, sharedMaterial], ); const restore = applyCrossSectionClipping(mesh, crossSectionPlane, { preserveExistingPlanes: false, clipShadows: false, }); expect(sharedMaterial.clippingPlanes).toEqual([crossSectionPlane]); expect(uniqueMaterial.clippingPlanes).toEqual([crossSectionPlane]); expect(sharedMaterial.clipShadows).toBe(false); expect(uniqueMaterial.clipShadows).toBe(false); restore(); expect(sharedMaterial.clippingPlanes).toBeNull(); expect(uniqueMaterial.clippingPlanes).toBeNull(); mesh.geometry.dispose(); sharedMaterial.dispose(); uniqueMaterial.dispose(); }); }); function expectVectorClose(actual: THREE.Vector3, expected: THREE.Vector3) { expect(actual.x).toBeCloseTo(expected.x); expect(actual.y).toBeCloseTo(expected.y); expect(actual.z).toBeCloseTo(expected.z); }