import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js'; const BG_COLOR = 0x0f172a; const GRID_COLOR = 0x334155; // slate-700 const RING_COLOR = 0x475569; // slate-600 const LABEL_COLOR = '#94a3b8'; // slate-400 export interface SceneContext { scene: THREE.Scene; camera: THREE.PerspectiveCamera; renderer: THREE.WebGLRenderer; labelRenderer: CSS2DRenderer; controls: OrbitControls; patternGroup: THREE.Group; } export function createScene(container: HTMLElement): SceneContext { const scene = new THREE.Scene(); scene.background = new THREE.Color(BG_COLOR); // Camera const aspect = container.clientWidth / container.clientHeight; const camera = new THREE.PerspectiveCamera(55, aspect, 0.1, 100); camera.position.set(3, 2.5, 3); camera.lookAt(0, 0, 0); // WebGL renderer const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(container.clientWidth, container.clientHeight); container.appendChild(renderer.domElement); // CSS2D renderer for labels const labelRenderer = new CSS2DRenderer(); labelRenderer.setSize(container.clientWidth, container.clientHeight); labelRenderer.domElement.style.position = 'absolute'; labelRenderer.domElement.style.top = '0'; labelRenderer.domElement.style.left = '0'; labelRenderer.domElement.style.pointerEvents = 'none'; container.appendChild(labelRenderer.domElement); // Orbit controls const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.08; controls.minDistance = 1.5; controls.maxDistance = 12; // Lighting const ambientLight = new THREE.AmbientLight(0xffffff, 0.4); scene.add(ambientLight); const dirLight = new THREE.DirectionalLight(0xffffff, 0.8); dirLight.position.set(5, 8, 5); scene.add(dirLight); const dirLight2 = new THREE.DirectionalLight(0xffffff, 0.3); dirLight2.position.set(-3, -2, -4); scene.add(dirLight2); // Grid on XZ plane const gridHelper = new THREE.GridHelper(6, 12, GRID_COLOR, GRID_COLOR); (gridHelper.material as THREE.Material).opacity = 0.3; (gridHelper.material as THREE.Material).transparent = true; scene.add(gridHelper); // Axis lines addAxisLines(scene); // Axis labels addAxisLabel(scene, 'X', new THREE.Vector3(3.3, 0, 0)); addAxisLabel(scene, 'Y', new THREE.Vector3(0, 3.3, 0)); addAxisLabel(scene, 'Z', new THREE.Vector3(0, 0, 3.3)); // Reference dBi rings on XZ plane addReferenceRings(scene); // Group for pattern meshes const patternGroup = new THREE.Group(); scene.add(patternGroup); // Resize handler const onResize = () => { const w = container.clientWidth; const h = container.clientHeight; camera.aspect = w / h; camera.updateProjectionMatrix(); renderer.setSize(w, h); labelRenderer.setSize(w, h); }; window.addEventListener('resize', onResize); // Animation loop const animate = () => { requestAnimationFrame(animate); controls.update(); renderer.render(scene, camera); labelRenderer.render(scene, camera); }; animate(); return { scene, camera, renderer, labelRenderer, controls, patternGroup }; } function addAxisLines(scene: THREE.Scene) { const len = 3.2; const xGeo = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(0, 0, 0), new THREE.Vector3(len, 0, 0), ]); const xMat = new THREE.LineBasicMaterial({ color: 0xef4444, opacity: 0.6, transparent: true }); scene.add(new THREE.Line(xGeo, xMat)); const yGeo = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, len, 0), ]); const yMat = new THREE.LineBasicMaterial({ color: 0x22c55e, opacity: 0.6, transparent: true }); scene.add(new THREE.Line(yGeo, yMat)); const zGeo = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, len), ]); const zMat = new THREE.LineBasicMaterial({ color: 0x3b82f6, opacity: 0.6, transparent: true }); scene.add(new THREE.Line(zGeo, zMat)); } function addAxisLabel(scene: THREE.Scene, text: string, position: THREE.Vector3) { const el = document.createElement('span'); el.textContent = text; el.style.color = LABEL_COLOR; el.style.fontFamily = "'Inter', sans-serif"; el.style.fontSize = '12px'; el.style.fontWeight = '600'; const label = new CSS2DObject(el); label.position.copy(position); scene.add(label); } function addReferenceRings(scene: THREE.Scene) { const dbiLevels = [-3, 0, 3, 6]; const minDbi = -3; const maxDbi = 6; dbiLevels.forEach((dbi) => { const normalized = (dbi - minDbi) / (maxDbi - minDbi); const radius = 0.1 + normalized * 1.9; // map to [0.1, 2.0] const curve = new THREE.EllipseCurve(0, 0, radius, radius, 0, 2 * Math.PI, false, 0); const points = curve.getPoints(64); const geo = new THREE.BufferGeometry().setFromPoints( points.map((p) => new THREE.Vector3(p.x, 0, p.y)) ); const mat = new THREE.LineBasicMaterial({ color: RING_COLOR, opacity: 0.35, transparent: true, }); const ring = new THREE.Line(geo, mat); scene.add(ring); // Label for the ring const el = document.createElement('span'); el.textContent = `${dbi >= 0 ? '+' : ''}${dbi} dBi`; el.style.color = '#64748b'; // slate-500 el.style.fontFamily = "'Inter', sans-serif"; el.style.fontSize = '10px'; const label = new CSS2DObject(el); label.position.set(radius + 0.1, 0, 0); scene.add(label); }); }