Ayush-Singh commited on
Commit
a0552d4
·
1 Parent(s): 14f8b5d

Fix FileNotFoundError for interactive_demo.html

Browse files
Files changed (1) hide show
  1. server/interactive_demo.html +756 -0
server/interactive_demo.html ADDED
@@ -0,0 +1,756 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Structural Design Env - Interactive Demo</title>
7
+ <meta name="description" content="Interactive demo for StructuralDesignEnv">
8
+ <style>
9
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
10
+
11
+ :root {
12
+ --bg-color: #0f1115;
13
+ --panel-bg: rgba(25, 28, 36, 0.7);
14
+ --border-color: rgba(255, 255, 255, 0.1);
15
+ --text-main: #f3f4f6;
16
+ --text-dim: #9ca3af;
17
+ --accent: #3b82f6;
18
+ --accent-hover: #2563eb;
19
+ --success: #10b981;
20
+ --error: #ef4444;
21
+ --warning: #f59e0b;
22
+ }
23
+
24
+ * {
25
+ box-sizing: border-box;
26
+ margin: 0;
27
+ padding: 0;
28
+ }
29
+
30
+ body {
31
+ font-family: 'Inter', sans-serif;
32
+ background-color: var(--bg-color);
33
+ background-image:
34
+ radial-gradient(at 0% 0%, hsla(253,16%,7%,1) 0, transparent 50%),
35
+ radial-gradient(at 100% 0%, hsla(225,39%,15%,1) 0, transparent 50%),
36
+ radial-gradient(at 100% 100%, hsla(339,20%,12%,1) 0, transparent 50%);
37
+ background-attachment: fixed;
38
+ color: var(--text-main);
39
+ height: 100vh;
40
+ display: flex;
41
+ flex-direction: column;
42
+ overflow: hidden;
43
+ }
44
+
45
+ header {
46
+ padding: 1.5rem 2rem;
47
+ display: flex;
48
+ justify-content: space-between;
49
+ align-items: center;
50
+ border-bottom: 1px solid var(--border-color);
51
+ background: rgba(15, 17, 21, 0.6);
52
+ backdrop-filter: blur(12px);
53
+ z-index: 10;
54
+ }
55
+
56
+ h1 {
57
+ font-size: 1.5rem;
58
+ font-weight: 600;
59
+ background: linear-gradient(to right, #60a5fa, #a78bfa);
60
+ -webkit-background-clip: text;
61
+ -webkit-text-fill-color: transparent;
62
+ margin: 0;
63
+ }
64
+
65
+ .header-controls {
66
+ display: flex;
67
+ gap: 1rem;
68
+ align-items: center;
69
+ }
70
+
71
+ select, button {
72
+ font-family: 'Inter', sans-serif;
73
+ background: rgba(255, 255, 255, 0.05);
74
+ border: 1px solid var(--border-color);
75
+ color: white;
76
+ padding: 0.5rem 1rem;
77
+ border-radius: 0.5rem;
78
+ font-size: 0.9rem;
79
+ outline: none;
80
+ cursor: pointer;
81
+ transition: all 0.2s;
82
+ }
83
+
84
+ select:focus, button:focus {
85
+ border-color: var(--accent);
86
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3);
87
+ }
88
+
89
+ select option {
90
+ background: var(--bg-color);
91
+ }
92
+
93
+ button {
94
+ background: var(--accent);
95
+ border: none;
96
+ font-weight: 500;
97
+ }
98
+
99
+ button:hover {
100
+ background: var(--accent-hover);
101
+ }
102
+
103
+ button.secondary {
104
+ background: rgba(255, 255, 255, 0.1);
105
+ }
106
+
107
+ button.secondary:hover {
108
+ background: rgba(255, 255, 255, 0.2);
109
+ }
110
+
111
+ .main-container {
112
+ display: flex;
113
+ flex: 1;
114
+ overflow: hidden;
115
+ }
116
+
117
+ .left-panel {
118
+ flex: 2;
119
+ padding: 2rem;
120
+ display: flex;
121
+ flex-direction: column;
122
+ border-right: 1px solid var(--border-color);
123
+ position: relative;
124
+ }
125
+
126
+ .right-panel {
127
+ flex: 1;
128
+ padding: 2rem;
129
+ background: var(--panel-bg);
130
+ backdrop-filter: blur(10px);
131
+ overflow-y: auto;
132
+ border-left: 1px solid rgba(255,255,255,0.05);
133
+ }
134
+
135
+ .toolbar {
136
+ display: flex;
137
+ gap: 0.5rem;
138
+ margin-bottom: 1.5rem;
139
+ background: rgba(0, 0, 0, 0.3);
140
+ padding: 0.5rem;
141
+ border-radius: 0.75rem;
142
+ border: 1px solid var(--border-color);
143
+ }
144
+
145
+ .tool-btn {
146
+ background: transparent;
147
+ color: var(--text-dim);
148
+ flex: 1;
149
+ }
150
+
151
+ .tool-btn.active {
152
+ background: rgba(255, 255, 255, 0.1);
153
+ color: var(--text-main);
154
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.1);
155
+ }
156
+
157
+ .svg-container {
158
+ flex: 1;
159
+ background: rgba(0, 0, 0, 0.2);
160
+ border-radius: 1rem;
161
+ border: 1px solid var(--border-color);
162
+ position: relative;
163
+ overflow: auto;
164
+ display: flex;
165
+ align-items: center;
166
+ justify-content: center;
167
+ }
168
+
169
+ #grid-svg {
170
+ cursor: crosshair;
171
+ }
172
+
173
+ /* Tooltip style */
174
+ .tooltip {
175
+ position: absolute;
176
+ background: rgba(0,0,0,0.8);
177
+ border: 1px solid rgba(255,255,255,0.1);
178
+ padding: 8px 12px;
179
+ border-radius: 6px;
180
+ pointer-events: none;
181
+ font-size: 13px;
182
+ z-index: 100;
183
+ display: none;
184
+ color: #fff;
185
+ backdrop-filter: blur(4px);
186
+ }
187
+
188
+ .panel-section {
189
+ margin-bottom: 2rem;
190
+ }
191
+
192
+ .panel-section h3 {
193
+ font-size: 0.85rem;
194
+ text-transform: uppercase;
195
+ letter-spacing: 0.05em;
196
+ color: var(--text-dim);
197
+ margin-bottom: 1rem;
198
+ border-bottom: 1px solid var(--border-color);
199
+ padding-bottom: 0.5rem;
200
+ }
201
+
202
+ .stat-grid {
203
+ display: grid;
204
+ grid-template-columns: 1fr 1fr;
205
+ gap: 1rem;
206
+ }
207
+
208
+ .stat-card {
209
+ background: rgba(255, 255, 255, 0.03);
210
+ border: 1px solid var(--border-color);
211
+ border-radius: 0.75rem;
212
+ padding: 1rem;
213
+ transition: transform 0.2s;
214
+ }
215
+
216
+ .stat-card:hover {
217
+ transform: translateY(-2px);
218
+ background: rgba(255, 255, 255, 0.05);
219
+ }
220
+
221
+ .stat-value {
222
+ font-size: 1.5rem;
223
+ font-weight: 600;
224
+ margin-bottom: 0.25rem;
225
+ }
226
+
227
+ .stat-label {
228
+ font-size: 0.8rem;
229
+ color: var(--text-dim);
230
+ }
231
+
232
+ .status-badge {
233
+ display: inline-block;
234
+ padding: 0.25rem 0.75rem;
235
+ border-radius: 9999px;
236
+ font-size: 0.8rem;
237
+ font-weight: 500;
238
+ margin-bottom: 1rem;
239
+ }
240
+
241
+ .status-valid {
242
+ background: rgba(16, 185, 129, 0.2);
243
+ color: #34d399;
244
+ border: 1px solid rgba(16, 185, 129, 0.3);
245
+ }
246
+
247
+ .status-invalid {
248
+ background: rgba(239, 68, 68, 0.2);
249
+ color: #f87171;
250
+ border: 1px solid rgba(239, 68, 68, 0.3);
251
+ }
252
+
253
+ .status-unconverged {
254
+ background: rgba(245, 158, 11, 0.2);
255
+ color: #fbbf24;
256
+ border: 1px solid rgba(245, 158, 11, 0.3);
257
+ }
258
+
259
+ .message-box {
260
+ background: rgba(0, 0, 0, 0.3);
261
+ border-radius: 0.5rem;
262
+ padding: 1rem;
263
+ font-family: monospace;
264
+ font-size: 0.85rem;
265
+ color: var(--text-dim);
266
+ white-space: pre-wrap;
267
+ max-height: 200px;
268
+ overflow-y: auto;
269
+ }
270
+
271
+ .loading-overlay {
272
+ position: absolute;
273
+ top: 0; left: 0; right: 0; bottom: 0;
274
+ background: rgba(15, 17, 21, 0.8);
275
+ display: flex;
276
+ justify-content: center;
277
+ align-items: center;
278
+ z-index: 50;
279
+ color: white;
280
+ font-size: 1.2rem;
281
+ backdrop-filter: blur(4px);
282
+ display: none;
283
+ }
284
+
285
+ .spinner {
286
+ width: 40px;
287
+ height: 40px;
288
+ border: 4px solid rgba(255,255,255,0.1);
289
+ border-left-color: var(--accent);
290
+ border-radius: 50%;
291
+ animation: spin 1s linear infinite;
292
+ margin-right: 15px;
293
+ }
294
+
295
+ @keyframes spin {
296
+ to { transform: rotate(360deg); }
297
+ }
298
+
299
+ table {
300
+ width: 100%;
301
+ border-collapse: collapse;
302
+ font-size: 0.85rem;
303
+ }
304
+ th, td {
305
+ text-align: left;
306
+ padding: 0.5rem;
307
+ border-bottom: 1px solid rgba(255,255,255,0.1);
308
+ }
309
+ th {
310
+ color: var(--text-dim);
311
+ font-weight: 500;
312
+ }
313
+ </style>
314
+ </head>
315
+ <body>
316
+
317
+ <header>
318
+ <h1>Structural Design Env</h1>
319
+ <div class="header-controls">
320
+ <select id="task-select">
321
+ <option value="task1_warehouse">Task 1: Warehouse</option>
322
+ <option value="task2_office">Task 2: Office</option>
323
+ <option value="task3_hospital">Task 3: Hospital</option>
324
+ </select>
325
+ <button id="btn-reset" class="secondary">New Episode</button>
326
+ <button id="btn-done">Done (Evaluate)</button>
327
+ </div>
328
+ </header>
329
+
330
+ <div class="main-container">
331
+ <div class="left-panel">
332
+ <div id="loading" class="loading-overlay">
333
+ <div class="spinner"></div> Loading...
334
+ </div>
335
+
336
+ <div class="toolbar">
337
+ <button class="tool-btn active" data-mode="place_column">Place Column</button>
338
+ <button class="tool-btn" data-mode="place_beam_x">Place Beam X (Horiz)</button>
339
+ <button class="tool-btn" data-mode="place_beam_y">Place Beam Y (Vert)</button>
340
+ <button class="tool-btn" data-mode="remove">Remove Element</button>
341
+ </div>
342
+
343
+ <div class="toolbar" style="margin-top: -1rem; margin-bottom: 1.5rem;">
344
+ <label style="color: var(--text-dim); font-size: 0.9rem; align-self: center; margin-left: 1rem;">Section:</label>
345
+ <select id="col-section-select" style="margin-left: 0.5rem;">
346
+ <option value="HEB140">HEB140</option>
347
+ <option value="HEB160">HEB160</option>
348
+ <option value="HEB200" selected>HEB200</option>
349
+ <option value="HEB240">HEB240</option>
350
+ <option value="HEB300">HEB300</option>
351
+ <option value="HEB360">HEB360</option>
352
+ <option value="HEB400">HEB400</option>
353
+ </select>
354
+ <select id="beam-section-select" style="margin-left: 0.5rem; display: none;">
355
+ <option value="IPE200">IPE200</option>
356
+ <option value="IPE240">IPE240</option>
357
+ <option value="IPE300" selected>IPE300</option>
358
+ <option value="IPE360">IPE360</option>
359
+ <option value="IPE400">IPE400</option>
360
+ <option value="IPE450">IPE450</option>
361
+ <option value="IPE500">IPE500</option>
362
+ </select>
363
+ </div>
364
+
365
+ <div class="svg-container" id="svg-wrap">
366
+ <svg id="grid-svg" width="100%" height="100%">
367
+ <!-- Generated via JS -->
368
+ </svg>
369
+ </div>
370
+ </div>
371
+
372
+ <div class="right-panel">
373
+ <div class="panel-section">
374
+ <h3>Design Status</h3>
375
+ <div id="status-badge" class="status-badge status-unconverged">Waiting for layout...</div>
376
+
377
+ <div class="stat-grid">
378
+ <div class="stat-card">
379
+ <div class="stat-value" id="stat-mass">0 kg</div>
380
+ <div class="stat-label">Total Steel Mass</div>
381
+ </div>
382
+ <div class="stat-card">
383
+ <div class="stat-value" id="stat-carbon">0 kg</div>
384
+ <div class="stat-label">Embodied Carbon</div>
385
+ </div>
386
+ <div class="stat-card">
387
+ <div class="stat-value" id="stat-drift">0 %</div>
388
+ <div class="stat-label">Max Drift Ratio</div>
389
+ </div>
390
+ <div class="stat-card">
391
+ <div class="stat-value" id="stat-deflection">0 mm</div>
392
+ <div class="stat-label">Max Deflection</div>
393
+ </div>
394
+ </div>
395
+ </div>
396
+
397
+ <div class="panel-section">
398
+ <h3>Utilization Hotspots</h3>
399
+ <table id="hotspot-table">
400
+ <thead>
401
+ <tr>
402
+ <th>Element</th>
403
+ <th>UR Bending</th>
404
+ <th>UR Shear</th>
405
+ <th>UR Axial</th>
406
+ </tr>
407
+ </thead>
408
+ <tbody id="hotspot-tbody">
409
+ <tr><td colspan="4" style="text-align:center; color: var(--text-dim)">No data</td></tr>
410
+ </tbody>
411
+ </table>
412
+ </div>
413
+
414
+ <div class="panel-section">
415
+ <h3>Environment Message</h3>
416
+ <div class="message-box" id="env-message">Initialize an episode to start.</div>
417
+ </div>
418
+ </div>
419
+ </div>
420
+
421
+ <div id="tooltip" class="tooltip"></div>
422
+
423
+ <script>
424
+ let sessionId = null;
425
+ let currentMode = 'place_column';
426
+ let currentState = null;
427
+ let cellSpacing = 40; // Pixels per grid cell
428
+
429
+ const svg = document.getElementById('grid-svg');
430
+ const tooltip = document.getElementById('tooltip');
431
+ const loading = document.getElementById('loading');
432
+
433
+ // Setup toolbar interactions
434
+ document.querySelectorAll('.tool-btn').forEach(btn => {
435
+ btn.addEventListener('click', (e) => {
436
+ document.querySelectorAll('.tool-btn').forEach(b => b.classList.remove('active'));
437
+ e.target.classList.add('active');
438
+ currentMode = e.target.dataset.mode;
439
+
440
+ if(currentMode === 'place_column') {
441
+ document.getElementById('col-section-select').style.display = 'block';
442
+ document.getElementById('beam-section-select').style.display = 'none';
443
+ } else if(currentMode.includes('beam')) {
444
+ document.getElementById('col-section-select').style.display = 'none';
445
+ document.getElementById('beam-section-select').style.display = 'block';
446
+ } else {
447
+ document.getElementById('col-section-select').style.display = 'none';
448
+ document.getElementById('beam-section-select').style.display = 'none';
449
+ }
450
+ });
451
+ });
452
+
453
+ document.getElementById('btn-reset').addEventListener('click', resetEnvironment);
454
+ document.getElementById('btn-done').addEventListener('click', finishEpisode);
455
+
456
+ async function setLoading(isLoading) {
457
+ loading.style.display = isLoading ? 'flex' : 'none';
458
+ }
459
+
460
+ async function resetEnvironment() {
461
+ setLoading(true);
462
+ const taskId = document.getElementById('task-select').value;
463
+ try {
464
+ const res = await fetch('/reset', {
465
+ method: 'POST',
466
+ headers: {'Content-Type': 'application/json'},
467
+ body: JSON.stringify({ task_id: taskId })
468
+ });
469
+ const data = await res.json();
470
+ sessionId = data.session_id;
471
+ currentState = data.observation;
472
+ renderUI();
473
+ } catch (err) {
474
+ console.error(err);
475
+ alert('Error resetting environment.');
476
+ }
477
+ setLoading(false);
478
+ }
479
+
480
+ async function sendAction(actionData) {
481
+ if (!sessionId) return;
482
+ setLoading(true);
483
+ try {
484
+ const res = await fetch('/step', {
485
+ method: 'POST',
486
+ headers: {'Content-Type': 'application/json'},
487
+ body: JSON.stringify({
488
+ session_id: sessionId,
489
+ message: JSON.stringify(actionData)
490
+ })
491
+ });
492
+ const data = await res.json();
493
+ currentState = data.observation;
494
+
495
+ if(data.done) {
496
+ alert(`Episode complete! Reward: ${data.reward}`);
497
+ }
498
+
499
+ renderUI();
500
+ } catch (err) {
501
+ console.error(err);
502
+ }
503
+ setLoading(false);
504
+ }
505
+
506
+ async function finishEpisode() {
507
+ if (!sessionId) return;
508
+ await sendAction({action_type: 'done'});
509
+ }
510
+
511
+ function getColorForUR(ur) {
512
+ if (ur === 0 || !ur) return '#6b7280'; // gray (unassembled/no load)
513
+ if (ur > 1.0) return '#ef4444'; // red (fail)
514
+ if (ur > 0.8) return '#f59e0b'; // orange (high)
515
+ return '#34d399'; // green (safe)
516
+ }
517
+
518
+ function renderUI() {
519
+ if (!currentState) return;
520
+
521
+ // 1. Update Physics Stats
522
+ const isUnconverged = currentState.message && currentState.message.includes("Not converged");
523
+ const isValid = currentState.is_structurally_valid;
524
+
525
+ const badge = document.getElementById('status-badge');
526
+ if (isUnconverged) {
527
+ badge.className = 'status-badge status-unconverged';
528
+ badge.innerText = 'Not converged (Structurally incomplete)';
529
+ } else if (isValid) {
530
+ badge.className = 'status-badge status-valid';
531
+ badge.innerText = 'Safe & Valid';
532
+ } else {
533
+ badge.className = 'status-badge status-invalid';
534
+ badge.innerText = 'Code Violations Detected';
535
+ }
536
+
537
+ document.getElementById('stat-mass').innerText = currentState.total_steel_mass_kg.toFixed(0) + ' kg';
538
+ document.getElementById('stat-carbon').innerText = currentState.carbon_kg.toFixed(0) + ' kg';
539
+ document.getElementById('stat-drift').innerText = (currentState.max_lateral_drift_ratio * 100).toFixed(3) + '%';
540
+ document.getElementById('stat-deflection').innerText = currentState.max_deflection_mm.toFixed(1) + ' mm';
541
+ document.getElementById('env-message').innerText = currentState.message || "Ready";
542
+
543
+ // Render Hotspots Tab
544
+ const tbody = document.getElementById('hotspot-tbody');
545
+ tbody.innerHTML = '';
546
+ // We'll fake critical elements if state.critical_members is not robustly structured,
547
+ // or we'll filter top URs from placed_elements
548
+ let elements = currentState.placed_elements || [];
549
+ elements = elements.filter(e => e.utilization_ratio > 0)
550
+ .sort((a,b) => b.utilization_ratio - a.utilization_ratio)
551
+ .slice(0, 5);
552
+
553
+ if (elements.length === 0) {
554
+ tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; color: var(--text-dim)">No active load data</td></tr>';
555
+ } else {
556
+ elements.forEach(e => {
557
+ const tr = document.createElement('tr');
558
+ tr.innerHTML = `
559
+ <td>${e.element_id}</td>
560
+ <td><span style="color: ${getColorForUR(e.ur_bending)}">${e.ur_bending ? e.ur_bending.toFixed(2) : '-'}</span></td>
561
+ <td><span style="color: ${getColorForUR(e.ur_shear)}">${e.ur_shear ? e.ur_shear.toFixed(2) : '-'}</span></td>
562
+ <td><span style="color: ${getColorForUR(e.utilization_ratio)}">${e.utilization_ratio.toFixed(2)}</span></td>
563
+ `;
564
+ tbody.appendChild(tr);
565
+ });
566
+ }
567
+
568
+ // 2. Render SVG Grid
569
+ renderGrid();
570
+ }
571
+
572
+ function renderGrid() {
573
+ svg.innerHTML = '';
574
+ if (!currentState) return;
575
+
576
+ const w = currentState.grid_width;
577
+ const d = currentState.grid_depth;
578
+
579
+ // Grid logic: x=column (0 to w), y=row (0 to d)
580
+ // Flip Y so that y=0 is at the bottom visually
581
+
582
+ const svgW = w * cellSpacing + 80;
583
+ const svgH = d * cellSpacing + 80;
584
+ // Add viewBox to make it responsive
585
+ svg.setAttribute('viewBox', `0 0 ${svgW} ${svgH}`);
586
+
587
+ const offsetX = 40;
588
+ const offsetY = 40;
589
+
590
+ const getX = (gx) => offsetX + gx * cellSpacing;
591
+ const getY = (gy) => offsetY + (d - gy) * cellSpacing;
592
+
593
+ // Draw background grid
594
+ for(let x=0; x<=w; x++) {
595
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
596
+ line.setAttribute('x1', getX(x));
597
+ line.setAttribute('y1', getY(0));
598
+ line.setAttribute('x2', getX(x));
599
+ line.setAttribute('y2', getY(d));
600
+ line.setAttribute('stroke', 'rgba(255,255,255,0.05)');
601
+ line.setAttribute('stroke-width', '1');
602
+ svg.appendChild(line);
603
+ }
604
+ for(let y=0; y<=d; y++) {
605
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
606
+ line.setAttribute('x1', getX(0));
607
+ line.setAttribute('y1', getY(y));
608
+ line.setAttribute('x2', getX(w));
609
+ line.setAttribute('y2', getY(y));
610
+ line.setAttribute('stroke', 'rgba(255,255,255,0.05)');
611
+ line.setAttribute('stroke-width', '1');
612
+ svg.appendChild(line);
613
+ }
614
+
615
+ // Clickable areas (invisible rects centered on nodes and spanning edges)
616
+ for(let x=0; x<=w; x++) {
617
+ for(let y=0; y<=d; y++) {
618
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
619
+ rect.setAttribute('x', getX(x) - cellSpacing/2);
620
+ rect.setAttribute('y', getY(y) - cellSpacing/2);
621
+ rect.setAttribute('width', cellSpacing);
622
+ rect.setAttribute('height', cellSpacing);
623
+ rect.setAttribute('fill', 'transparent');
624
+ rect.style.cursor = 'pointer';
625
+
626
+ const handleHover = (e) => {
627
+ tooltip.style.display = 'block';
628
+ tooltip.style.left = e.pageX + 15 + 'px';
629
+ tooltip.style.top = e.pageY + 15 + 'px';
630
+ tooltip.innerHTML = `Grid(${x}, ${y})`;
631
+ rect.setAttribute('fill', 'rgba(255,255,255,0.05)');
632
+ };
633
+ const handleOut = () => {
634
+ tooltip.style.display = 'none';
635
+ rect.setAttribute('fill', 'transparent');
636
+ };
637
+
638
+ rect.addEventListener('mousemove', handleHover);
639
+ rect.addEventListener('mouseleave', handleOut);
640
+ rect.addEventListener('click', () => handleGridClick(x, y));
641
+
642
+ svg.appendChild(rect);
643
+ }
644
+ }
645
+
646
+ // Draw placed structural elements
647
+ const elements = currentState.placed_elements || [];
648
+
649
+ // Draw Beams first so they are behind columns
650
+ elements.filter(e => e.type === 'Beam').forEach(b => {
651
+ const parts = b.element_id.split('_');
652
+ // e.g. beam_0_0_5_0_0
653
+ if(parts.length >= 6) {
654
+ const x1 = parseInt(parts[1]);
655
+ const y1 = parseInt(parts[2]);
656
+ const x2 = parseInt(parts[3]);
657
+ const y2 = parseInt(parts[4]);
658
+ // Only draw ground floor beams for demo
659
+ const floor = parseInt(parts[5]);
660
+ if(floor !== 0) return;
661
+
662
+ const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
663
+ line.setAttribute('x1', getX(x1));
664
+ line.setAttribute('y1', getY(y1));
665
+ line.setAttribute('x2', getX(x2));
666
+ line.setAttribute('y2', getY(y2));
667
+ line.setAttribute('stroke', getColorForUR(b.utilization_ratio));
668
+ line.setAttribute('stroke-width', '6');
669
+ line.setAttribute('stroke-linecap', 'round');
670
+
671
+ line.addEventListener('mousemove', (e) => {
672
+ tooltip.style.display = 'block';
673
+ tooltip.style.left = e.pageX + 15 + 'px';
674
+ tooltip.style.top = e.pageY + 15 + 'px';
675
+ tooltip.innerHTML = `${b.element_id}<br/>UR: ${b.utilization_ratio.toFixed(2)}`;
676
+ });
677
+ line.addEventListener('mouseleave', () => tooltip.style.display = 'none');
678
+ line.addEventListener('click', (e) => {
679
+ e.stopPropagation();
680
+ if(currentMode === 'remove') {
681
+ sendAction({action_type: 'remove_element', element_id: b.element_id});
682
+ }
683
+ });
684
+
685
+ svg.appendChild(line);
686
+ }
687
+ });
688
+
689
+ // Draw Columns
690
+ elements.filter(e => e.type === 'Column').forEach(c => {
691
+ const parts = c.element_id.split('_');
692
+ // col_x_y_f
693
+ if(parts.length >= 4) {
694
+ const x = parseInt(parts[1]);
695
+ const y = parseInt(parts[2]);
696
+ const floor = parseInt(parts[3]);
697
+ if(floor !== 0) return; // Only 2D ground view
698
+
699
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
700
+ circle.setAttribute('cx', getX(x));
701
+ circle.setAttribute('cy', getY(y));
702
+ circle.setAttribute('r', '8');
703
+ circle.setAttribute('fill', getColorForUR(c.utilization_ratio));
704
+ circle.setAttribute('stroke', 'rgba(255,255,255,0.8)');
705
+ circle.setAttribute('stroke-width', '2');
706
+
707
+ circle.addEventListener('mousemove', (e) => {
708
+ tooltip.style.display = 'block';
709
+ tooltip.style.left = e.pageX + 15 + 'px';
710
+ tooltip.style.top = e.pageY + 15 + 'px';
711
+ tooltip.innerHTML = `${c.element_id} [${c.section}]<br/>UR: ${c.utilization_ratio.toFixed(2)}`;
712
+ });
713
+ circle.addEventListener('mouseleave', () => tooltip.style.display = 'none');
714
+ circle.addEventListener('click', (e) => {
715
+ e.stopPropagation();
716
+ if(currentMode === 'remove') {
717
+ sendAction({action_type: 'remove_element', element_id: c.element_id});
718
+ }
719
+ });
720
+
721
+ svg.appendChild(circle);
722
+ }
723
+ });
724
+ }
725
+
726
+ function handleGridClick(x, y) {
727
+ if(currentMode === 'place_column') {
728
+ const sec = document.getElementById('col-section-select').value;
729
+ sendAction({
730
+ action_type: 'place_column',
731
+ grid_x: x, grid_y: y, floor: 0, section: sec
732
+ });
733
+ } else if(currentMode === 'place_beam_x') {
734
+ const sec = document.getElementById('beam-section-select').value;
735
+ sendAction({
736
+ action_type: 'place_beam',
737
+ from_node_x: x, from_node_y: y,
738
+ to_node_x: x+1, to_node_y: y,
739
+ floor: 0, section: sec, orientation: 'x'
740
+ });
741
+ } else if(currentMode === 'place_beam_y') {
742
+ const sec = document.getElementById('beam-section-select').value;
743
+ sendAction({
744
+ action_type: 'place_beam',
745
+ from_node_x: x, from_node_y: y,
746
+ to_node_x: x, to_node_y: y+1,
747
+ floor: 0, section: sec, orientation: 'y'
748
+ });
749
+ }
750
+ }
751
+
752
+ // Initial load
753
+ resetEnvironment();
754
+ </script>
755
+ </body>
756
+ </html>