|
3 | 3 | import * as THREE from 'three';
|
4 | 4 | import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
5 | 5 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
6 |
| - import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js'; |
7 | 6 | import Menu from '$lib/components/hotspot/3dMenu.svelte';
|
8 |
| - import { Button } from 'flowbite-svelte'; |
| 7 | + import { P } from 'flowbite-svelte'; |
9 | 8 |
|
10 | 9 | let canvas: HTMLCanvasElement;
|
11 | 10 | let camera: THREE.PerspectiveCamera, scene: THREE.Scene, renderer: THREE.WebGLRenderer;
|
12 | 11 | let controls: OrbitControls;
|
13 |
| - let labelRenderer: CSS2DRenderer; |
14 | 12 | let raycaster: THREE.Raycaster;
|
15 | 13 | let mouse: THREE.Vector2;
|
16 |
| - let annotationMode = false; // Toggle for annotation mode |
| 14 | + let annotationMode = false; |
17 | 15 | let annotationText = '';
|
18 | 16 | let activePoint: THREE.Vector3 | null = null;
|
19 | 17 | let tooltipX: number = 0;
|
20 | 18 | let tooltipY: number = 0;
|
| 19 | + let currentModel: THREE.Object3D | null = null; |
21 | 20 |
|
22 | 21 | export let data: {
|
23 | 22 | role: string;
|
24 | 23 | models: { title: string; file_path: string; description: string }[];
|
25 | 24 | };
|
26 | 25 |
|
27 | 26 | let { models } = data;
|
28 |
| - let selectedModel: string | null = null; |
| 27 | +
|
29 | 28 | const annotations: {
|
30 |
| - [key: string]: { position: THREE.Vector3; text: string; labelDiv: HTMLDivElement }; |
| 29 | + [key: string]: { |
| 30 | + position: THREE.Vector3; |
| 31 | + text: string; |
| 32 | + labelDiv: HTMLDivElement; |
| 33 | + sprite: THREE.Sprite; |
| 34 | + }; |
31 | 35 | } = {};
|
32 | 36 |
|
33 | 37 | function toggleAnnotationMode() {
|
34 |
| - annotationMode = !annotationMode; |
| 38 | + if (data.role === 'lecturer') { |
| 39 | + annotationMode = true; |
| 40 | + } else if (data.role === 'student') { |
| 41 | + annotationMode = false; |
| 42 | + } |
| 43 | +
|
35 | 44 | if (!annotationMode) {
|
36 |
| - activePoint = null; // Clear active point when exiting annotation mode |
| 45 | + activePoint = null; |
37 | 46 | }
|
38 | 47 | }
|
39 | 48 |
|
40 | 49 | function addAnnotation() {
|
41 | 50 | if (annotationText.trim() && activePoint) {
|
42 | 51 | createAnnotation(activePoint, annotationText);
|
43 |
| - annotationText = ''; // Clear text after adding |
44 |
| - activePoint = null; // Clear active point |
| 52 | + annotationText = ''; |
| 53 | + activePoint = null; |
45 | 54 | }
|
46 | 55 | }
|
47 | 56 |
|
|
59 | 68 | sprite.scale.set(0.05, 0.05, 0.05);
|
60 | 69 | scene.add(sprite);
|
61 | 70 |
|
| 71 | + // Create the label element |
62 | 72 | const labelDiv = document.createElement('div');
|
63 |
| - labelDiv.className = 'annotation-label'; |
| 73 | + labelDiv.style.backgroundColor = 'rgba(30, 30, 30, 0.9)'; |
| 74 | + labelDiv.style.padding = '2px 5px'; |
| 75 | + labelDiv.style.borderRadius = '3px'; |
| 76 | + labelDiv.style.fontSize = '12px'; |
| 77 | + labelDiv.style.color = 'white'; |
| 78 | + labelDiv.style.fontWeight = 'bold'; |
| 79 | + labelDiv.style.pointerEvents = 'none'; |
| 80 | + labelDiv.style.userSelect = 'none'; |
| 81 | + labelDiv.style.zIndex = '1000'; |
64 | 82 | labelDiv.textContent = text;
|
65 |
| - const label = new CSS2DObject(labelDiv); |
66 |
| - label.position.copy(position); |
67 |
| - scene.add(label); |
| 83 | + document.body.appendChild(labelDiv); |
68 | 84 |
|
69 |
| - annotations[text] = { position, text, labelDiv }; |
| 85 | + annotations[text] = { position, text, labelDiv, sprite }; |
| 86 | + } |
| 87 | +
|
| 88 | + export function removeAnnotation(text: string) { |
| 89 | + const annotation = annotations[text]; |
| 90 | + if (annotation) { |
| 91 | + document.body.removeChild(annotation.labelDiv); |
| 92 | + scene.remove(annotation.sprite); |
| 93 | + delete annotations[text]; |
| 94 | + } |
70 | 95 | }
|
71 | 96 |
|
72 | 97 | function onMouseClick(event: MouseEvent) {
|
|
94 | 119 | tooltipY = -(vector.y * heightHalf) + heightHalf;
|
95 | 120 | }
|
96 | 121 | }
|
| 122 | + function handleBeforeUnload() { |
| 123 | + removeAllAnnotations(); |
| 124 | + } |
97 | 125 |
|
98 | 126 | onMount(() => {
|
99 | 127 | initScene();
|
100 | 128 | animate();
|
| 129 | + toggleAnnotationMode(); |
| 130 | + const urlParams = new URLSearchParams(window.location.search); |
| 131 | + const modelPath = urlParams.get('model'); |
101 | 132 |
|
| 133 | + if (modelPath) { |
| 134 | + loadModel(modelPath); |
| 135 | + } |
102 | 136 | window.addEventListener('click', onMouseClick);
|
| 137 | + window.addEventListener('beforeunload', handleBeforeUnload); |
| 138 | + return () => { |
| 139 | + window.removeEventListener('beforeunload', handleBeforeUnload); |
| 140 | + window.removeEventListener('click', onMouseClick); |
| 141 | + }; |
103 | 142 | });
|
104 | 143 |
|
105 | 144 | function initScene() {
|
|
111 | 150 | renderer.setSize(window.innerWidth, window.innerHeight);
|
112 | 151 | renderer.setClearColor(0xffffff);
|
113 | 152 |
|
114 |
| - labelRenderer = new CSS2DRenderer(); |
115 |
| - labelRenderer.setSize(window.innerWidth, window.innerHeight); |
116 |
| - labelRenderer.domElement.style.position = 'absolute'; |
117 |
| - labelRenderer.domElement.style.top = '0'; |
118 |
| - labelRenderer.domElement.style.pointerEvents = 'none'; |
119 |
| - document.body.appendChild(labelRenderer.domElement); |
120 |
| -
|
121 | 153 | raycaster = new THREE.Raycaster();
|
122 | 154 | mouse = new THREE.Vector2();
|
123 | 155 |
|
|
138 | 170 | function loadModel(file_path: string) {
|
139 | 171 | const loader = new GLTFLoader();
|
140 | 172 | loader.load(file_path, (gltf) => {
|
141 |
| - scene.add(gltf.scene); |
| 173 | + if (currentModel) { |
| 174 | + scene.remove(currentModel); |
| 175 | + } |
| 176 | + currentModel = gltf.scene; |
| 177 | + scene.add(currentModel); |
| 178 | + }); |
| 179 | + } |
| 180 | +
|
| 181 | + function removeAllAnnotations() { |
| 182 | + Object.keys(annotations).forEach((text) => { |
| 183 | + removeAnnotation(text); |
142 | 184 | });
|
143 | 185 | }
|
144 | 186 |
|
145 | 187 | function handleModelSelection(file_path: string) {
|
146 |
| - selectedModel = file_path; |
147 |
| - localStorage.setItem('selectedModel', selectedModel); |
| 188 | + removeAllAnnotations(); |
148 | 189 | loadModel(file_path);
|
| 190 | + const url = new URL(window.location.href); |
| 191 | + url.searchParams.set('model', file_path); |
| 192 | + window.history.pushState({}, '', url); |
149 | 193 | }
|
150 | 194 |
|
151 | 195 | function animate() {
|
152 | 196 | requestAnimationFrame(animate);
|
153 |
| -
|
154 |
| - // Get the canvas's bounding rect |
155 | 197 | if (!canvas) return;
|
156 | 198 | const rect = canvas.getBoundingClientRect();
|
157 | 199 | const canvasWidth = rect.width;
|
|
160 | 202 | Object.values(annotations).forEach(({ position, labelDiv }) => {
|
161 | 203 | const spriteScreenPosition = position.clone().project(camera);
|
162 | 204 |
|
163 |
| - // normalized coordinates to pixel coordinates |
164 | 205 | const widthHalf = canvasWidth / 2;
|
165 | 206 | const heightHalf = canvasHeight / 2;
|
166 | 207 | const spriteX = spriteScreenPosition.x * widthHalf + widthHalf;
|
167 | 208 | const spriteY = -(spriteScreenPosition.y * heightHalf) + heightHalf;
|
168 | 209 |
|
169 |
| - // Update the label's position |
170 |
| - labelDiv.style.position = 'absolute'; |
| 210 | + labelDiv.style.position = 'fixed'; |
171 | 211 | labelDiv.style.left = `${spriteX + rect.left}px`;
|
172 | 212 | labelDiv.style.top = `${spriteY + rect.top}px`;
|
173 | 213 |
|
174 |
| - console.log(`Sprite Position: ${spriteX}, ${spriteY}`); |
175 |
| - console.log(`Canvas Bounds: ${rect.left}, ${rect.top}, ${rect.width}, ${rect.height}`); |
176 |
| -
|
177 |
| - // Show/hide labels based on visibility |
178 | 214 | if (
|
179 | 215 | spriteScreenPosition.z < 0 ||
|
180 | 216 | spriteX < 0 ||
|
|
188 | 224 | }
|
189 | 225 | });
|
190 | 226 |
|
191 |
| - // Render the scene and labels |
192 | 227 | renderer.render(scene, camera);
|
193 |
| - labelRenderer.render(scene, camera); |
194 | 228 | }
|
195 | 229 |
|
196 | 230 | function onWindowResize() {
|
197 | 231 | camera.aspect = window.innerWidth / window.innerHeight;
|
198 | 232 | camera.updateProjectionMatrix();
|
199 | 233 | renderer.setSize(window.innerWidth, window.innerHeight);
|
200 |
| - labelRenderer.setSize(window.innerWidth, window.innerHeight); |
201 | 234 | }
|
202 | 235 | </script>
|
203 | 236 |
|
204 | 237 | <div class="scene-wrapper">
|
205 |
| - <Menu {models} onModelSelect={handleModelSelection} /> |
206 |
| - <Button on:click={toggleAnnotationMode}> |
207 |
| - {annotationMode ? 'Exit Annotation Mode' : 'Enter Annotation Mode'} |
208 |
| - </Button> |
| 238 | + <div class="flex items-center space-x-4"> |
| 239 | + <Menu {models} onModelSelect={handleModelSelection} /> |
| 240 | + <P class=" font-semibold text-violet-700"> |
| 241 | + Tip: Open on the Menu to choose your model, then click on the desired location to add |
| 242 | + annotations. |
| 243 | + </P> |
| 244 | + </div> |
209 | 245 | <canvas bind:this={canvas}></canvas>
|
210 | 246 |
|
211 |
| - <!-- Annotation input --> |
212 | 247 | {#if annotationMode && activePoint}
|
213 | 248 | <div class="annotation-input" style="left: {tooltipX}px; top: {tooltipY}px;">
|
214 | 249 | <input type="text" bind:value={annotationText} placeholder="Enter annotation" />
|
|
241 | 276 | }
|
242 | 277 |
|
243 | 278 | .annotation-input input {
|
244 |
| - width: 100px; /* Adjust width */ |
| 279 | + width: 100px; |
245 | 280 | }
|
246 | 281 |
|
247 | 282 | .annotation-input button {
|
248 | 283 | margin-top: 5px;
|
249 | 284 | }
|
250 |
| -
|
251 |
| - :global(.annotation-label) { |
252 |
| - background-color: rgba(41, 39, 39, 0.7); |
253 |
| - padding: 2px 5px; |
254 |
| - border-radius: 3px; |
255 |
| - font-size: 12px; |
256 |
| - pointer-events: none; |
257 |
| - user-select: none; |
258 |
| - } |
259 | 285 | </style>
|
0 commit comments