Skip to content

Commit e62f65b

Browse files
Merge pull request #261 from COS301-SE-2024/stable
Stable
2 parents fabb46f + 95cbfe0 commit e62f65b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+3617
-207
lines changed

.DS_Store

-6 KB
Binary file not shown.

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ build
99
.vercel
1010
coverage
1111
.svelte-kit
12-
node_modules
12+
node_modules
13+
.DS_Store

bun.lockb

-3.32 KB
Binary file not shown.

package.json

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/bun.lockb

-11.2 KB
Binary file not shown.

src/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,12 @@
7575
"@threlte/extras": "^8.11.4",
7676
"@threlte/rapier": "^2.0.1",
7777
"@threlte/xr": "^0.1.4",
78+
"@types/aframe": "^1.2.7",
7879
"@types/canvas-confetti": "^1.6.4",
7980
"@types/three": "^0.165.0",
8081
"@vercel/analytics": "^1.3.1",
8182
"@vercel/speed-insights": "^1.0.12",
83+
"aframe": "^1.6.0",
8284
"aws-sdk": "^2.1670.0",
8385
"axios": "^1.7.2",
8486
"bcrypt": "^5.1.1",
@@ -88,8 +90,10 @@
8890
"mongoose": "^8.4.5",
8991
"playroomkit": "^0.0.79",
9092
"stream-chat": "^8.37.0",
93+
"svelte-dnd-action": "^0.9.50",
9194
"svelte-french-toast": "^1.2.0",
9295
"svelte-tweakpane-ui": "^1.3.0",
93-
"three": "^0.165.0"
96+
"three": "^0.165.0",
97+
"webxr": "^0.0.0"
9498
}
9599
}

src/src/lib/components/annotations/3dAnnotations.svelte

Lines changed: 78 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,45 +3,54 @@
33
import * as THREE from 'three';
44
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
55
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
6-
import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js';
76
import Menu from '$lib/components/hotspot/3dMenu.svelte';
8-
import { Button } from 'flowbite-svelte';
7+
import { P } from 'flowbite-svelte';
98
109
let canvas: HTMLCanvasElement;
1110
let camera: THREE.PerspectiveCamera, scene: THREE.Scene, renderer: THREE.WebGLRenderer;
1211
let controls: OrbitControls;
13-
let labelRenderer: CSS2DRenderer;
1412
let raycaster: THREE.Raycaster;
1513
let mouse: THREE.Vector2;
16-
let annotationMode = false; // Toggle for annotation mode
14+
let annotationMode = false;
1715
let annotationText = '';
1816
let activePoint: THREE.Vector3 | null = null;
1917
let tooltipX: number = 0;
2018
let tooltipY: number = 0;
19+
let currentModel: THREE.Object3D | null = null;
2120
2221
export let data: {
2322
role: string;
2423
models: { title: string; file_path: string; description: string }[];
2524
};
2625
2726
let { models } = data;
28-
let selectedModel: string | null = null;
27+
2928
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+
};
3135
} = {};
3236
3337
function toggleAnnotationMode() {
34-
annotationMode = !annotationMode;
38+
if (data.role === 'lecturer') {
39+
annotationMode = true;
40+
} else if (data.role === 'student') {
41+
annotationMode = false;
42+
}
43+
3544
if (!annotationMode) {
36-
activePoint = null; // Clear active point when exiting annotation mode
45+
activePoint = null;
3746
}
3847
}
3948
4049
function addAnnotation() {
4150
if (annotationText.trim() && activePoint) {
4251
createAnnotation(activePoint, annotationText);
43-
annotationText = ''; // Clear text after adding
44-
activePoint = null; // Clear active point
52+
annotationText = '';
53+
activePoint = null;
4554
}
4655
}
4756
@@ -59,14 +68,30 @@
5968
sprite.scale.set(0.05, 0.05, 0.05);
6069
scene.add(sprite);
6170
71+
// Create the label element
6272
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';
6482
labelDiv.textContent = text;
65-
const label = new CSS2DObject(labelDiv);
66-
label.position.copy(position);
67-
scene.add(label);
83+
document.body.appendChild(labelDiv);
6884
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+
}
7095
}
7196
7297
function onMouseClick(event: MouseEvent) {
@@ -94,12 +119,26 @@
94119
tooltipY = -(vector.y * heightHalf) + heightHalf;
95120
}
96121
}
122+
function handleBeforeUnload() {
123+
removeAllAnnotations();
124+
}
97125
98126
onMount(() => {
99127
initScene();
100128
animate();
129+
toggleAnnotationMode();
130+
const urlParams = new URLSearchParams(window.location.search);
131+
const modelPath = urlParams.get('model');
101132
133+
if (modelPath) {
134+
loadModel(modelPath);
135+
}
102136
window.addEventListener('click', onMouseClick);
137+
window.addEventListener('beforeunload', handleBeforeUnload);
138+
return () => {
139+
window.removeEventListener('beforeunload', handleBeforeUnload);
140+
window.removeEventListener('click', onMouseClick);
141+
};
103142
});
104143
105144
function initScene() {
@@ -111,13 +150,6 @@
111150
renderer.setSize(window.innerWidth, window.innerHeight);
112151
renderer.setClearColor(0xffffff);
113152
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-
121153
raycaster = new THREE.Raycaster();
122154
mouse = new THREE.Vector2();
123155
@@ -138,20 +170,30 @@
138170
function loadModel(file_path: string) {
139171
const loader = new GLTFLoader();
140172
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);
142184
});
143185
}
144186
145187
function handleModelSelection(file_path: string) {
146-
selectedModel = file_path;
147-
localStorage.setItem('selectedModel', selectedModel);
188+
removeAllAnnotations();
148189
loadModel(file_path);
190+
const url = new URL(window.location.href);
191+
url.searchParams.set('model', file_path);
192+
window.history.pushState({}, '', url);
149193
}
150194
151195
function animate() {
152196
requestAnimationFrame(animate);
153-
154-
// Get the canvas's bounding rect
155197
if (!canvas) return;
156198
const rect = canvas.getBoundingClientRect();
157199
const canvasWidth = rect.width;
@@ -160,21 +202,15 @@
160202
Object.values(annotations).forEach(({ position, labelDiv }) => {
161203
const spriteScreenPosition = position.clone().project(camera);
162204
163-
// normalized coordinates to pixel coordinates
164205
const widthHalf = canvasWidth / 2;
165206
const heightHalf = canvasHeight / 2;
166207
const spriteX = spriteScreenPosition.x * widthHalf + widthHalf;
167208
const spriteY = -(spriteScreenPosition.y * heightHalf) + heightHalf;
168209
169-
// Update the label's position
170-
labelDiv.style.position = 'absolute';
210+
labelDiv.style.position = 'fixed';
171211
labelDiv.style.left = `${spriteX + rect.left}px`;
172212
labelDiv.style.top = `${spriteY + rect.top}px`;
173213
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
178214
if (
179215
spriteScreenPosition.z < 0 ||
180216
spriteX < 0 ||
@@ -188,27 +224,26 @@
188224
}
189225
});
190226
191-
// Render the scene and labels
192227
renderer.render(scene, camera);
193-
labelRenderer.render(scene, camera);
194228
}
195229
196230
function onWindowResize() {
197231
camera.aspect = window.innerWidth / window.innerHeight;
198232
camera.updateProjectionMatrix();
199233
renderer.setSize(window.innerWidth, window.innerHeight);
200-
labelRenderer.setSize(window.innerWidth, window.innerHeight);
201234
}
202235
</script>
203236

204237
<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>
209245
<canvas bind:this={canvas}></canvas>
210246

211-
<!-- Annotation input -->
212247
{#if annotationMode && activePoint}
213248
<div class="annotation-input" style="left: {tooltipX}px; top: {tooltipY}px;">
214249
<input type="text" bind:value={annotationText} placeholder="Enter annotation" />
@@ -241,19 +276,10 @@
241276
}
242277
243278
.annotation-input input {
244-
width: 100px; /* Adjust width */
279+
width: 100px;
245280
}
246281
247282
.annotation-input button {
248283
margin-top: 5px;
249284
}
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-
}
259285
</style>

0 commit comments

Comments
 (0)