CVNSS commited on
Commit
bf931b9
·
verified ·
1 Parent(s): 96485c7

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +442 -18
index.html CHANGED
@@ -1,19 +1,443 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Aquaponics 3D Pilot 20m² – Anguilla & Lettuce</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <style>
8
+ html, body { height: 100%; width: 100%; margin: 0; padding: 0; overflow: hidden; }
9
+ body {
10
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
11
+ background: linear-gradient(135deg, #1a1a2e, #16213e);
12
+ color: #fff;
13
+ }
14
+ #container { width: 100vw; height: 100vh; }
15
+ #dashboard {
16
+ position: absolute; top: 20px; right: 20px; width: 350px; z-index: 10;
17
+ background: rgba(10,20,40,0.95); border-radius: 14px; border: 2px solid #00bcd4;
18
+ box-shadow: 0 8px 32px rgba(0,188,212,0.2);
19
+ padding: 18px 22px 16px 22px;
20
+ }
21
+ #dashboard h3 {
22
+ color: #00bcd4; text-align: center; margin: 0 0 12px 0; font-size: 17px;
23
+ letter-spacing: 1px; font-weight: bold;
24
+ }
25
+ .section { margin-bottom: 14px; }
26
+ .section label { font-size: 12px; font-weight: bold; color: #4CAF50; }
27
+ .toggle-btn {
28
+ display: block; width: 100%; margin: 5px 0 0 0; padding: 8px 0;
29
+ border: none; border-radius: 16px; font-size: 12px;
30
+ background: linear-gradient(45deg, #2196F3, #00bcd4);
31
+ color: #fff; cursor: pointer; transition: all 0.2s;
32
+ font-weight: 600;
33
+ }
34
+ .toggle-btn.active { background: linear-gradient(45deg, #4CAF50, #8BC34A);}
35
+ .toggle-btn.off { background: linear-gradient(45deg, #f44336, #ff5722);}
36
+ .sensor-row { display: flex; justify-content: space-between; padding: 2px 0;}
37
+ .sensor-label { color: #eee; font-size: 12px;}
38
+ .sensor-value { font-family: 'Courier New', monospace; font-weight: bold; color: #00bcd4;}
39
+ #view-controls {
40
+ position: absolute; bottom: 25px; left: 25px; z-index: 11;
41
+ background: rgba(10,20,40,0.92); border-radius: 8px; border: 1px solid #00bcd4;
42
+ padding: 14px 10px 10px 12px;
43
+ }
44
+ #view-controls button {
45
+ margin: 0 2px 4px 0; padding: 7px 12px; border-radius: 5px;
46
+ background: linear-gradient(45deg, #673AB7, #9C27B0); color: #fff; border: none;
47
+ font-size: 11px; font-weight: 500; cursor: pointer;
48
+ transition: 0.2s;
49
+ }
50
+ #view-controls button:hover { box-shadow: 0 4px 10px #9C27B040; }
51
+ #info-panel {
52
+ position: absolute; top: 18px; left: 20px; z-index: 10; width: 310px;
53
+ background: rgba(0,0,0,0.72); border-radius: 10px; padding: 14px 20px 10px 20px;
54
+ font-size: 12px; border: 1px solid #4CAF50;
55
+ color: #fafafa;
56
+ }
57
+ .legend { margin-top: 12px; border-top: 1px solid #222; padding-top: 10px;}
58
+ .color-box { display: inline-block; width: 11px; height: 11px; border-radius: 2px;
59
+ margin-right: 5px; vertical-align: middle;}
60
+ .group-btn { display: inline-block; margin: 1px 2px 2px 0; }
61
+ #popup-info {
62
+ display:none; position:fixed; left:50%; top:40%; z-index:999;
63
+ min-width:200px; max-width:360px;
64
+ background:rgba(15,30,50,0.97); color:#fff; border:2px solid #00bcd4;
65
+ border-radius: 14px; padding: 18px 22px;
66
+ box-shadow: 0 8px 32px rgba(0,188,212,0.15);
67
+ transform:translate(-50%, -50%);
68
+ }
69
+ #popup-info h4 { margin:0 0 6px 0; color:#4CAF50;}
70
+ #popup-info button { background:#00bcd4;color:#fff;border:none;padding:4px 12px;border-radius:8px;margin-top:10px;}
71
+ @media (max-width:900px) {
72
+ #dashboard,#info-panel { width: 90vw; max-width: 99vw; left: 0; right: 0; margin: auto;}
73
+ }
74
+ </style>
75
+ </head>
76
+ <body>
77
+ <div id="container"></div>
78
+
79
+ <!-- DASHBOARD -->
80
+ <div id="dashboard">
81
+ <h3>🌱 Aquaponics IoT Dashboard</h3>
82
+ <div class="section">
83
+ <label>🌡️ Sensor Monitor</label>
84
+ <div class="sensor-row"><span class="sensor-label">pH:</span> <span class="sensor-value" id="ph">6.8</span></div>
85
+ <div class="sensor-row"><span class="sensor-label">EC (μS/cm):</span> <span class="sensor-value" id="ec">1240</span></div>
86
+ <div class="sensor-row"><span class="sensor-label">TDS (ppm):</span> <span class="sensor-value" id="tds">620</span></div>
87
+ <div class="sensor-row"><span class="sensor-label">Temp (°C):</span> <span class="sensor-value" id="temp">24.6</span></div>
88
+ <div class="sensor-row"><span class="sensor-label">DO (mg/L):</span> <span class="sensor-value" id="do">6.2</span></div>
89
+ <div class="sensor-row"><span class="sensor-label">NO₃⁻ (mg/L):</span> <span class="sensor-value" id="no3">85</span></div>
90
+ <div class="sensor-row"><span class="sensor-label">Salinity (ppt):</span> <span class="sensor-value" id="sal">3</span></div>
91
+ </div>
92
+ <div class="section">
93
+ <label>⚙️ System Controls</label>
94
+ <button class="toggle-btn active" id="led-toggle" onclick="toggleGroup('lighting')">LED Grow Lights</button>
95
+ <button class="toggle-btn active" id="water-toggle" onclick="toggleGroup('waterFlow')">Water Flow</button>
96
+ <button class="toggle-btn active" id="drip-toggle" onclick="toggleGroup('pumps')">Drip/Pump</button>
97
+ <button class="toggle-btn active" id="sensor-toggle" onclick="toggleGroup('sensors')">Sensor Display</button>
98
+ </div>
99
+ <div class="section">
100
+ <label>🌿 Biology Groups</label>
101
+ <button class="toggle-btn active group-btn" id="plants-toggle" onclick="toggleGroup('plants')">Lettuce (Rau xà lách)</button>
102
+ <button class="toggle-btn active group-btn" id="tilapia-toggle" onclick="toggleGroup('tilapia')">Tilapia</button>
103
+ <button class="toggle-btn active group-btn" id="eel-toggle" onclick="toggleGroup('eels')">Anguilla marmorata</button>
104
+ </div>
105
+ <div class="section">
106
+ <label>🔧 Show/Hide</label>
107
+ <button class="toggle-btn active group-btn" id="pipes-toggle" onclick="toggleGroup('nftSystem')">NFT Pipes</button>
108
+ <button class="toggle-btn active group-btn" id="frame-toggle" onclick="toggleGroup('frame')">Frame</button>
109
+ <button class="toggle-btn active group-btn" id="tank-toggle" onclick="toggleGroup('tank')">Fish Tank</button>
110
+ <button class="toggle-btn active group-btn" id="filters-toggle" onclick="toggleGroup('filters')">Filter Boxes</button>
111
+ </div>
112
+ </div>
113
+
114
+ <!-- VIEW CONTROLS -->
115
+ <div id="view-controls">
116
+ <div style="color:#00bcd4; font-weight:bold; margin-bottom:8px;">📷 View</div>
117
+ <button onclick="setCamera('isometric')">Isometric</button>
118
+ <button onclick="setCamera('top')">Top</button>
119
+ <button onclick="setCamera('side')">Side</button>
120
+ <button onclick="setCamera('front')">Front</button>
121
+ <button onclick="resetCamera()">Reset</button>
122
+ </div>
123
+ <!-- INFO PANEL -->
124
+ <div id="info-panel">
125
+ <h3>🔹 3D Aquaponics Pilot 20m²</h3>
126
+ <p><b>System:</b> 5m × 4m × 2.4m. <b>Tank:</b> 2×1×1m. <b>Pipes:</b> 12×NFT. <b>Plants:</b> 216 Lettuce. <b>Fish:</b> 58 Tilapia + 9 Anguilla. <b>Sensor:</b> 7.</p>
127
+ <div class="legend">
128
+ <b>Water Path:</b>
129
+ <div><span class="color-box" style="background: #2196F3;"></span>Tank→Filter</div>
130
+ <div><span class="color-box" style="background: #4CAF50;"></span>Biofilter</div>
131
+ <div><span class="color-box" style="background: #FF9800;"></span>Pump→NFT</div>
132
+ <div><span class="color-box" style="background: #9C27B0;"></span>Return</div>
133
+ </div>
134
+ <div class="legend">
135
+ <b>IoT Sensors:</b>
136
+ <span style="color:#2196F3;">● Water</span>
137
+ <span style="color:#FFEB3B;">● Temp</span>
138
+ <span style="color:#4CAF50;">● pH/EC</span>
139
+ <span style="color:#F44336;">● Flow</span>
140
+ <span style="color:#9C27B0;">● LED</span>
141
+ <span style="color:#00bcd4;">● Salinity</span>
142
+ </div>
143
+ </div>
144
+
145
+ <!-- POPUP INFO (TÍNH NĂNG SẼ BỔ SUNG) -->
146
+ <div id="popup-info"></div>
147
+
148
+ <!-- THREE.JS + CONTROLS -->
149
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
150
+ <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.min.js"></script>
151
+ <script>
152
+ // ====== THREE.JS SCENE SETUP ======
153
+ const scene = new THREE.Scene();
154
+ scene.background = new THREE.Color(0x1a1a2e);
155
+ const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
156
+ let defaultCameraPos = new THREE.Vector3(8, 6, 8);
157
+ camera.position.copy(defaultCameraPos);
158
+ camera.lookAt(0, 1, 0);
159
+
160
+ const renderer = new THREE.WebGLRenderer({ antialias: true });
161
+ renderer.setSize(window.innerWidth, window.innerHeight);
162
+ renderer.shadowMap.enabled = true;
163
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
164
+ document.getElementById('container').appendChild(renderer.domElement);
165
+
166
+ // Orbit controls (mouse drag, zoom, rotate)
167
+ const controls = new THREE.OrbitControls(camera, renderer.domElement);
168
+ controls.target.set(0, 1, 0);
169
+ controls.update();
170
+
171
+ // Lighting
172
+ const ambientLight = new THREE.AmbientLight(0x404040, 0.7);
173
+ scene.add(ambientLight);
174
+ const dirLight = new THREE.DirectionalLight(0xffffff, 1.3);
175
+ dirLight.position.set(10, 16, 10);
176
+ dirLight.castShadow = true;
177
+ scene.add(dirLight);
178
+
179
+ // ===== MATERIALS =====
180
+ const materials = {
181
+ steel: new THREE.MeshPhongMaterial({ color: 0x666666, shininess: 110 }),
182
+ tank: new THREE.MeshPhongMaterial({ color: 0x2196F3, transparent: true, opacity: 0.38, shininess: 120 }),
183
+ water: new THREE.MeshPhongMaterial({ color: 0x1976D2, transparent: true, opacity: 0.74, shininess: 200 }),
184
+ filter: new THREE.MeshPhongMaterial({ color: 0x795548, shininess: 55 }),
185
+ pipe: new THREE.MeshPhongMaterial({ color: 0x424242, shininess: 80 }),
186
+ nftPipe: new THREE.MeshPhongMaterial({ color: 0x607D8B, shininess: 85 }),
187
+ plant: new THREE.MeshPhongMaterial({ color: 0x92f58b, shininess: 26 }), // lettuce: light green
188
+ tilapia: new THREE.MeshPhongMaterial({ color: 0xFF5722, shininess: 70 }),
189
+ eel: new THREE.MeshPhongMaterial({ color: 0x222211, shininess: 90 }), // base color for eel
190
+ sensor: new THREE.MeshPhongMaterial({ color: 0x9C27B0, shininess: 140 }),
191
+ led: new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.85 }),
192
+ ground: new THREE.MeshLambertMaterial({ color: 0x3E2723 }),
193
+ manifold: new THREE.MeshPhongMaterial({ color: 0x37474F, shininess: 70 })
194
+ };
195
+
196
+ // ===== GROUPS =====
197
+ const groups = {};
198
+ ['frame','tank','filters','pumps','pipes','nftSystem','plants','tilapia','eels','sensors','lighting','waterFlow'].forEach(k=>{
199
+ groups[k]=new THREE.Group(); scene.add(groups[k]);
200
+ });
201
+
202
+ // ===== HELPERS =====
203
+ function createBox(w,h,d,m){const g=new THREE.BoxGeometry(w,h,d);const o=new THREE.Mesh(g,m);o.castShadow=o.receiveShadow=true;return o;}
204
+ function createCylinder(r1,r2,h,m){const g=new THREE.CylinderGeometry(r1,r2,h,20);const o=new THREE.Mesh(g,m);o.castShadow=o.receiveShadow=true;return o;}
205
+ function createSphere(r,m){const g=new THREE.SphereGeometry(r,18,16);const o=new THREE.Mesh(g,m);o.castShadow=o.receiveShadow=true;return o;}
206
+ // Create a "spotted eel" by combining a dark cylinder + random white spots
207
+ function createMarbledEel(length=0.18){
208
+ let group = new THREE.Group();
209
+ let body = createCylinder(0.024,0.03,length,materials.eel);
210
+ body.rotation.z = Math.PI/2;
211
+ // Random spots (simulate marble pattern)
212
+ for(let s=0;s<10;s++){
213
+ let spot = createSphere(0.009,new THREE.MeshPhongMaterial({color:0xffffff,shininess:200}));
214
+ let phi = Math.random()*Math.PI*2;
215
+ let dz = (Math.random()-0.5)*length*0.95;
216
+ let dx = Math.cos(phi)*0.022;
217
+ let dy = Math.sin(phi)*0.02;
218
+ spot.position.set(dx,dz,dy);
219
+ spot.rotation.z = Math.PI/2;
220
+ body.add(spot);
221
+ }
222
+ group.add(body);
223
+ return group;
224
+ }
225
+
226
+ // ===== GROUND + AXES + GRID =====
227
+ const ground = new THREE.Mesh(new THREE.PlaneGeometry(15,15), materials.ground);
228
+ ground.rotation.x = -Math.PI/2; ground.position.y = -0.01; ground.receiveShadow = true; scene.add(ground);
229
+ scene.add(new THREE.GridHelper(15,30,0x444444,0x333333));
230
+ scene.add(new THREE.AxesHelper(2.5));
231
+
232
+ // ===== 1. FRAME STRUCTURE =====
233
+ [[-2,-1.5],[2,-1.5],[2,1.5],[-2,1.5]].forEach(([x,z])=>{
234
+ let post = createBox(0.04,2.4,0.04,materials.steel); post.position.set(x,1.2,z); groups.frame.add(post);
235
+ });
236
+ [0.4,1.2,2.0].forEach(h=>{
237
+ // X
238
+ [ -1.5, 1.5 ].forEach(z=>{
239
+ let rail = createBox(4.0,0.04,0.04,materials.steel);
240
+ rail.position.set(0,h,z); groups.frame.add(rail);
241
+ });
242
+ // Z
243
+ [ -2.0, 2.0 ].forEach(x=>{
244
+ let rail = createBox(0.04,0.04,3.0,materials.steel);
245
+ rail.position.set(x,h,0); groups.frame.add(rail);
246
+ });
247
+ });
248
+
249
+ // ===== 2. FISH TANK (2x1x1m) =====
250
+ let tank = createBox(2.0,1.0,1.0,materials.tank); tank.position.set(0,0.5,0); groups.tank.add(tank);
251
+ let water = createBox(1.95,0.85,0.95,materials.water); water.position.set(0,0.5,0); groups.tank.add(water);
252
+
253
+ // ===== 3. FILTER BOXES =====
254
+ let swirl = createBox(0.8,0.8,0.8,materials.filter); swirl.position.set(-2.5,0.4,0); groups.filters.add(swirl);
255
+ let bf1 = createBox(1.0,0.4,0.5,materials.filter); bf1.position.set(-0.5,0.2,0); groups.filters.add(bf1);
256
+ let bf2 = createBox(1.0,0.4,0.5,materials.filter); bf2.position.set(0.5,0.2,0); groups.filters.add(bf2);
257
+
258
+ // ===== 4. PUMP/DRIP SYSTEM =====
259
+ let pump = createBox(0.3,0.3,0.3,materials.pipe); pump.position.set(1.5,0.15,-0.5); groups.pumps.add(pump);
260
+ let aerator = createBox(0.2,0.2,0.2,materials.pipe); aerator.position.set(1.2,0.1,0.5); groups.pumps.add(aerator);
261
+
262
+ // ===== 5. NFT PIPES SYSTEM (12) + LETTUCE PLANTS (216) =====
263
+ for(let row=0; row<2; row++){
264
+ for(let i=0; i<6; i++){
265
+ let pipe = createCylinder(0.05,0.05,3.5,materials.nftPipe);
266
+ pipe.position.set(0,1.2+(i*0.25),row?0.8:-0.8);
267
+ pipe.rotation.z=Math.PI/2; pipe.rotation.y=-0.052; groups.nftSystem.add(pipe);
268
+ // Lettuce plants
269
+ for(let j=0;j<18;j++){
270
+ let lettuce = createSphere(0.045,materials.plant);
271
+ lettuce.position.set(-1.6+(j*0.18),1.2+(i*0.25)+0.08,row?0.8:-0.8);
272
+ lettuce.userData = { type: 'plant', name: 'Lettuce', info: 'Rau xà lách (Lactuca sativa)' };
273
+ lettuce.cursor = 'pointer';
274
+ lettuce.onClick = () => showInfo({ title: "Lettuce (Rau xà lách)", content: "NFT Lettuce (Lactuca sativa). Aquaponics crop." });
275
+ groups.plants.add(lettuce);
276
+ }
277
+ }
278
+ }
279
+
280
+ // ===== 6. FISH POPULATION =====
281
+ let tilapiaFish=[]; let eelFish=[];
282
+ // Tilapia (as before)
283
+ for(let i=0;i<58;i++){
284
+ let sz=0.03+Math.random()*0.04,fish=createSphere(sz,materials.tilapia);
285
+ let x=(Math.random()-0.5)*1.8, y=0.2+Math.random()*0.6, z=(Math.random()-0.5)*0.9;
286
+ fish.position.set(x,y,z);
287
+ fish.userData = { type: 'fish', name: 'Tilapia', info: 'Oreochromis spp.' };
288
+ fish.cursor = 'pointer';
289
+ fish.onClick = () => showInfo({ title: "Tilapia", content: "Tilapia (Oreochromis spp.)" });
290
+ tilapiaFish.push({mesh:fish,velocity:new THREE.Vector3((Math.random()-0.5)*0.003,(Math.random()-0.5)*0.002,(Math.random()-0.5)*0.003)});
291
+ groups.tilapia.add(fish);
292
+ }
293
+ // Anguilla marmorata (marbled eel): show as large marbled eel with white spots, bottom dweller
294
+ for(let i=0;i<9;i++){
295
+ let eelObj = createMarbledEel(0.16+Math.random()*0.05);
296
+ eelObj.position.set((Math.random()-0.5)*1.5,0.12+Math.random()*0.1,(Math.random()-0.5)*0.7);
297
+ eelObj.userData = { type: 'eel', name: 'Anguilla marmorata', info: 'Cá chình bông - Marbled eel' };
298
+ eelObj.cursor = 'pointer';
299
+ eelObj.onClick = () => showInfo({ title: "Cá chình bông", content: "Anguilla marmorata (Marbled eel) – đặc điểm: thân có vệt trắng, sống đáy, chịu mặn tốt." });
300
+ eelFish.push({mesh:eelObj,velocity:new THREE.Vector3((Math.random()-0.5)*0.001,0,(Math.random()-0.5)*0.001)});
301
+ groups.eels.add(eelObj);
302
+ }
303
+
304
+ // ===== 7. SENSORS =====
305
+ let sensorMap = [
306
+ { id:'ph',name:'pH',pos:[-0.8,0.5,0.3],color:0xFF9800, value:6.8 },
307
+ { id:'ec',name:'EC',pos:[0.8,0.5,-0.3],color:0x2196F3, value:1240 },
308
+ { id:'tds',name:'TDS',pos:[0,0.8,0],color:0x9C27B0, value:620 },
309
+ { id:'temp',name:'Temp',pos:[-0.5,0.3,0],color:0xF44336, value:24.6 },
310
+ { id:'do',name:'DO',pos:[0.5,0.6,0.4],color:0x00BCD4, value:6.2 },
311
+ { id:'no3',name:'NO₃⁻',pos:[0.5,0.2,0],color:0x4CAF50, value:85 },
312
+ { id:'sal',name:'Salinity',pos:[-0.8,0.18,-0.2],color:0x00bcd4, value:3 }
313
+ ];
314
+ sensorMap.forEach(s=>{
315
+ let device = createBox(0.05,0.05,0.05,materials.sensor);
316
+ device.position.set(...s.pos); device.userData = { type: "sensor", name: s.name, info: `${s.name} sensor` };
317
+ device.cursor = 'pointer';
318
+ device.onClick = () => showInfo({ title: s.name, content: `Sensor value: ${s.value} ${s.name === 'Salinity' ? 'ppt' : ''}` });
319
+ groups.sensors.add(device);
320
+ let led = createSphere(0.01,new THREE.MeshBasicMaterial({color:s.color}));
321
+ led.position.set(s.pos[0],s.pos[1]+0.04,s.pos[2]);
322
+ groups.sensors.add(led);
323
+ });
324
+
325
+ // ===== 8. LIGHTING =====
326
+ for(let i=0;i<4;i++){
327
+ let light = createBox(0.8,0.05,0.1,materials.led);
328
+ light.position.set(-1.5+(i*1.0),2.2,0); groups.lighting.add(light);
329
+ }
330
+
331
+ // ===== 9. WATER FLOW VISUALIZATION =====
332
+ let waterFlowParticles=[],flowColors=[0x2196F3,0x4CAF50,0xFF9800,0x9C27B0];
333
+ for(let i=0;i<80;i++){
334
+ let particle = createSphere(0.008,new THREE.MeshBasicMaterial({
335
+ color:flowColors[i%flowColors.length],transparent:true,opacity:0.78
336
+ }));
337
+ particle.userData={pathIndex:Math.floor(Math.random()*8),progress:Math.random(),speed:0.006+Math.random()*0.009};
338
+ waterFlowParticles.push(particle); groups.waterFlow.add(particle);
339
+ }
340
+
341
+ // ===== GROUP VISIBILITY TOGGLES =====
342
+ let groupState={
343
+ lighting:true,waterFlow:true,pumps:true,sensors:true,plants:true,tilapia:true,eels:true,nftSystem:true,frame:true,tank:true,filters:true
344
+ };
345
+ function toggleGroup(name){
346
+ groupState[name]=!groupState[name];
347
+ groups[name].visible=groupState[name];
348
+ // Change button style:
349
+ let btn = document.getElementById(name+'-toggle');
350
+ if(btn){ btn.classList.toggle('active',groupState[name]); btn.classList.toggle('off',!groupState[name]); }
351
+ }
352
+ // Initialize all as visible:
353
+ Object.keys(groups).forEach(k=>groups[k].visible=true);
354
+
355
+ // ===== VIEW CONTROLS =====
356
+ function setCamera(view){
357
+ if(view==='isometric'){camera.position.set(8,6,8);}
358
+ else if(view==='top'){camera.position.set(0,16,0);}
359
+ else if(view==='side'){camera.position.set(10,2,0);}
360
+ else if(view==='front'){camera.position.set(0,2,10);}
361
+ camera.lookAt(0,1,0);
362
+ controls.target.set(0,1,0); controls.update();
363
+ }
364
+ function resetCamera(){ camera.position.copy(defaultCameraPos); camera.lookAt(0,1,0); controls.target.set(0,1,0); controls.update(); }
365
+
366
+ // ===== ANIMATION LOOP =====
367
+ function animate(){
368
+ requestAnimationFrame(animate);
369
+ // Animate tilapia
370
+ tilapiaFish.forEach(f=>{
371
+ f.mesh.position.add(f.velocity);
372
+ let p=f.mesh.position;
373
+ if(p.x>0.9||p.x<-0.9)f.velocity.x*=-1;
374
+ if(p.y>1.0||p.y<0.12)f.velocity.y*=-1;
375
+ if(p.z>0.48||p.z<-0.48)f.velocity.z*=-1;
376
+ if(Math.random()<0.012){f.velocity.x+=(Math.random()-0.5)*0.0015;f.velocity.z+=(Math.random()-0.5)*0.0015;}
377
+ });
378
+ // Animate eels
379
+ eelFish.forEach(e=>{
380
+ e.mesh.position.add(e.velocity);
381
+ let p=e.mesh.position;
382
+ if(p.x>0.8||p.x<-0.8)e.velocity.x*=-1;
383
+ if(p.z>0.45||p.z<-0.45)e.velocity.z*=-1;
384
+ });
385
+ // Animate water flow
386
+ if(groupState.waterFlow){
387
+ waterFlowParticles.forEach(p=>{
388
+ let d=p.userData; d.progress+=d.speed;
389
+ if(d.progress>=1){ d.progress=0; d.pathIndex=(d.pathIndex+1)%8;}
390
+ let pts=[
391
+ [0.95,0.65,0],[ -2.5,0.65,0],[ -0.5,0.2,0],[ 0.5,0.2,0],
392
+ [1.5,0.15,-0.5],[1.75,1.3,0],[-1.7,1.8,0],[0,0.5,0]
393
+ ];
394
+ let curr=pts[d.pathIndex], nxt=pts[(d.pathIndex+1)%pts.length];
395
+ p.position.lerpVectors(new THREE.Vector3(...curr),new THREE.Vector3(...nxt),d.progress);
396
+ });
397
+ }
398
+ renderer.render(scene,camera);
399
+ }
400
+ animate();
401
+
402
+ // ===== HANDLE WINDOW RESIZE =====
403
+ window.addEventListener('resize',()=>{
404
+ camera.aspect=window.innerWidth/window.innerHeight; camera.updateProjectionMatrix();
405
+ renderer.setSize(window.innerWidth,window.innerHeight);
406
+ });
407
+
408
+ // ======= POPUP (HOOK sẵn) =======
409
+ function showInfo(obj){
410
+ const el = document.getElementById('popup-info');
411
+ el.innerHTML = `<h4>${obj.title}</h4><div>${obj.content}</div>
412
+ <button onclick="hideInfo()">OK</button>`;
413
+ el.style.display = 'block';
414
+ }
415
+ function hideInfo(){ document.getElementById('popup-info').style.display='none'; }
416
+
417
+ // ======= PICK/CLICK EVENTS – sẽ cập nhật chi tiết sau =======
418
+ renderer.domElement.addEventListener('click', (event) => {
419
+ let mouse = new THREE.Vector2(
420
+ (event.clientX / renderer.domElement.clientWidth) * 2 - 1,
421
+ -(event.clientY / renderer.domElement.clientHeight) * 2 + 1
422
+ );
423
+ let raycaster = new THREE.Raycaster();
424
+ raycaster.setFromCamera(mouse, camera);
425
+
426
+ let intersects = raycaster.intersectObjects(
427
+ [].concat(
428
+ groups.sensors.children,
429
+ groups.plants.children,
430
+ groups.tilapia.children,
431
+ groups.eels.children
432
+ ), true);
433
+ if (intersects.length > 0) {
434
+ let obj = intersects[0].object;
435
+ // Find topmost userData
436
+ while (obj && !obj.onClick && obj.parent) obj = obj.parent;
437
+ if (obj && obj.onClick) obj.onClick();
438
+ }
439
+ });
440
+
441
+ </script>
442
+ </body>
443
  </html>