Integrate 3D city into main UI: iframe viewport 7/8 + data panel 1/8 right
Browse files- index.html: replace 2D canvas with 3D iframe (7/8 width), keep sidebar
right (1/8) with agents/chat/events tabs
- 3d.html: hide status bar and controls when embedded, postMessage bridge
for agent selection and camera controls
- Auto-detect file:// protocol for local demo mode (no server needed)
- Added collision avoidance, smooth mountains, 75 agents, new buildings
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- web/3d.html +13 -0
- web/index.html +33 -36
web/3d.html
CHANGED
|
@@ -1934,6 +1934,7 @@ function selectAgent(agentId) {
|
|
| 1934 |
const data = simState.agents?.[agentId];
|
| 1935 |
if (data) updateAgentInfo(agentId, data);
|
| 1936 |
document.getElementById('info-panel').classList.add('visible');
|
|
|
|
| 1937 |
}
|
| 1938 |
|
| 1939 |
function selectBuilding(buildingId) {
|
|
@@ -2288,6 +2289,18 @@ async function pollFallback() {
|
|
| 2288 |
// ============================================================
|
| 2289 |
// BOOT
|
| 2290 |
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2291 |
connectWebSocket();
|
| 2292 |
setInterval(pollFallback, 3000);
|
| 2293 |
animate();
|
|
|
|
| 1934 |
const data = simState.agents?.[agentId];
|
| 1935 |
if (data) updateAgentInfo(agentId, data);
|
| 1936 |
document.getElementById('info-panel').classList.add('visible');
|
| 1937 |
+
if (window.parent !== window) window.parent.postMessage({ type: 'agent-select', agentId }, '*');
|
| 1938 |
}
|
| 1939 |
|
| 1940 |
function selectBuilding(buildingId) {
|
|
|
|
| 2289 |
// ============================================================
|
| 2290 |
// BOOT
|
| 2291 |
// ============================================================
|
| 2292 |
+
const isEmbedded = window.parent !== window;
|
| 2293 |
+
if (isEmbedded) {
|
| 2294 |
+
document.getElementById('status-bar').style.display = 'none';
|
| 2295 |
+
document.getElementById('controls-bar').style.display = 'none';
|
| 2296 |
+
const hint = document.getElementById('zoom-hint');
|
| 2297 |
+
if (hint) hint.style.display = 'none';
|
| 2298 |
+
window.addEventListener('message', (e) => {
|
| 2299 |
+
if (e.data?.type === 'zoom-in') zoomIn();
|
| 2300 |
+
if (e.data?.type === 'zoom-out') zoomOut();
|
| 2301 |
+
if (e.data?.type === 'reset-camera') resetCamera();
|
| 2302 |
+
});
|
| 2303 |
+
}
|
| 2304 |
connectWebSocket();
|
| 2305 |
setInterval(pollFallback, 3000);
|
| 2306 |
animate();
|
web/index.html
CHANGED
|
@@ -40,14 +40,16 @@
|
|
| 40 |
}
|
| 41 |
.dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
| 42 |
.dot.green { background: #4ecca3; } .dot.yellow { background: #f0c040; } .dot.red { background: #e94560; }
|
| 43 |
-
#main { display: flex; height: calc(100vh - 50px); }
|
| 44 |
-
#
|
| 45 |
-
#
|
|
|
|
|
|
|
| 46 |
|
| 47 |
-
/*
|
| 48 |
#sidebar {
|
| 49 |
-
|
| 50 |
-
display: flex; flex-direction: column; overflow: hidden;
|
| 51 |
}
|
| 52 |
.sidebar-tabs {
|
| 53 |
display: flex; border-bottom: 2px solid #0f3460; flex-shrink: 0;
|
|
@@ -85,7 +87,7 @@
|
|
| 85 |
.agent-list-item .agent-name { font-weight: 600; }
|
| 86 |
.agent-list-item .agent-action { color: #666; font-size: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
| 87 |
|
| 88 |
-
#conversations-panel { padding: 0; flex: 1; overflow-y: auto; }
|
| 89 |
.conv-card {
|
| 90 |
margin: 8px; padding: 10px; background: #1a1a2e; border-radius: 6px;
|
| 91 |
border: 1px solid #0f3460;
|
|
@@ -279,10 +281,9 @@
|
|
| 279 |
oninput="onLlmProbSlider(this.value)" title="LLM usage probability">
|
| 280 |
<span id="llm-prob-label" style="font-size:10px;color:#4ecca3;min-width:28px;text-align:right;">10%</span>
|
| 281 |
<span style="color:#1a3a6e;margin:0 4px">│</span>
|
| 282 |
-
<button class="ctrl-btn"
|
| 283 |
-
<button class="ctrl-btn" onclick="
|
| 284 |
-
<button class="ctrl-btn" onclick="
|
| 285 |
-
<button class="ctrl-btn" onclick="zoomFit()" title="Fit entire city on screen">Fit</button>
|
| 286 |
</span>
|
| 287 |
<span id="api-calls">API: 0</span>
|
| 288 |
<span id="cost">$0.00</span>
|
|
@@ -290,18 +291,12 @@
|
|
| 290 |
</div>
|
| 291 |
</div>
|
| 292 |
<div id="main">
|
| 293 |
-
<div id="
|
| 294 |
-
<
|
| 295 |
-
<
|
|
|
|
| 296 |
<div id="toast-container"></div>
|
| 297 |
<div id="llm-popup"><div class="llm-pop-title">SWITCH LLM</div></div>
|
| 298 |
-
<input type="range" id="pan-x" min="0" max="100" value="0"
|
| 299 |
-
style="position:absolute;bottom:4px;left:10px;right:10px;width:calc(100% - 20px);height:14px;opacity:0.5;z-index:50;"
|
| 300 |
-
oninput="onPanSlider()">
|
| 301 |
-
<input type="range" id="pan-y" min="0" max="100" value="0"
|
| 302 |
-
orient="vertical"
|
| 303 |
-
style="position:absolute;right:4px;top:10px;bottom:24px;width:14px;height:calc(100% - 34px);opacity:0.5;z-index:50;writing-mode:vertical-lr;direction:ltr;-webkit-appearance:slider-vertical;"
|
| 304 |
-
oninput="onPanSlider()">
|
| 305 |
</div>
|
| 306 |
<div id="sidebar">
|
| 307 |
<div class="sidebar-tabs">
|
|
@@ -684,19 +679,7 @@ function switchTab(tab) {
|
|
| 684 |
// CANVAS SETUP
|
| 685 |
// ============================================================
|
| 686 |
function initCanvas() {
|
| 687 |
-
canvas
|
| 688 |
-
ctx = canvas.getContext('2d');
|
| 689 |
-
resizeCanvas();
|
| 690 |
-
window.addEventListener('resize', resizeCanvas);
|
| 691 |
-
canvas.addEventListener('click', onCanvasClick);
|
| 692 |
-
canvas.addEventListener('mousemove', onCanvasMouseMove);
|
| 693 |
-
canvas.addEventListener('mousedown', onCanvasDragStart);
|
| 694 |
-
canvas.addEventListener('mousemove', onCanvasDrag);
|
| 695 |
-
canvas.addEventListener('mouseup', onCanvasDragEnd);
|
| 696 |
-
canvas.addEventListener('mouseleave', onCanvasDragEnd);
|
| 697 |
-
canvas.addEventListener('wheel', onCanvasWheel, {passive: false});
|
| 698 |
-
initParticles();
|
| 699 |
-
requestAnimationFrame(animate);
|
| 700 |
}
|
| 701 |
function resizeCanvas() {
|
| 702 |
const c = document.getElementById('canvas-container');
|
|
@@ -805,7 +788,7 @@ function toggleRectZoom() {
|
|
| 805 |
rectZoomMode = !rectZoomMode;
|
| 806 |
const btn = document.getElementById('btn-rect-zoom');
|
| 807 |
if (btn) btn.classList.toggle('active', rectZoomMode);
|
| 808 |
-
canvas.style.cursor = rectZoomMode ? 'crosshair' : 'default';
|
| 809 |
}
|
| 810 |
|
| 811 |
function animate() {
|
|
@@ -3910,10 +3893,24 @@ document.addEventListener('click', () => {
|
|
| 3910 |
}
|
| 3911 |
});
|
| 3912 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3913 |
// ============================================================
|
| 3914 |
// INIT
|
| 3915 |
// ============================================================
|
| 3916 |
-
initCanvas();
|
| 3917 |
showDefaultDetail();
|
| 3918 |
fetchState();
|
| 3919 |
fetchControls();
|
|
|
|
| 40 |
}
|
| 41 |
.dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
| 42 |
.dot.green { background: #4ecca3; } .dot.yellow { background: #f0c040; } .dot.red { background: #e94560; }
|
| 43 |
+
#main { display: flex; flex-direction: row; height: calc(100vh - 50px); }
|
| 44 |
+
#viewport-3d { flex: 7; position: relative; min-width: 0; }
|
| 45 |
+
#viewport-3d iframe { width: 100%; height: 100%; border: none; display: block; }
|
| 46 |
+
#canvas-container { display: none; }
|
| 47 |
+
#cityCanvas { display: none; }
|
| 48 |
|
| 49 |
+
/* RIGHT DATA PANEL (1/8 of screen) */
|
| 50 |
#sidebar {
|
| 51 |
+
flex: 1; background: #16213e; border-left: 2px solid #0f3460;
|
| 52 |
+
display: flex; flex-direction: column; overflow: hidden; min-width: 0;
|
| 53 |
}
|
| 54 |
.sidebar-tabs {
|
| 55 |
display: flex; border-bottom: 2px solid #0f3460; flex-shrink: 0;
|
|
|
|
| 87 |
.agent-list-item .agent-name { font-weight: 600; }
|
| 88 |
.agent-list-item .agent-action { color: #666; font-size: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
| 89 |
|
| 90 |
+
#conversations-panel { padding: 0; flex: 1; overflow-y: auto; min-width: 0; }
|
| 91 |
.conv-card {
|
| 92 |
margin: 8px; padding: 10px; background: #1a1a2e; border-radius: 6px;
|
| 93 |
border: 1px solid #0f3460;
|
|
|
|
| 281 |
oninput="onLlmProbSlider(this.value)" title="LLM usage probability">
|
| 282 |
<span id="llm-prob-label" style="font-size:10px;color:#4ecca3;min-width:28px;text-align:right;">10%</span>
|
| 283 |
<span style="color:#1a3a6e;margin:0 4px">│</span>
|
| 284 |
+
<button class="ctrl-btn" onclick="send3d('zoom-in')" title="Zoom In (scroll in 3D view)">+</button>
|
| 285 |
+
<button class="ctrl-btn" onclick="send3d('zoom-out')" title="Zoom Out (scroll in 3D view)">-</button>
|
| 286 |
+
<button class="ctrl-btn" onclick="send3d('reset-camera')" title="Reset 3D camera">Fit</button>
|
|
|
|
| 287 |
</span>
|
| 288 |
<span id="api-calls">API: 0</span>
|
| 289 |
<span id="cost">$0.00</span>
|
|
|
|
| 291 |
</div>
|
| 292 |
</div>
|
| 293 |
<div id="main">
|
| 294 |
+
<div id="viewport-3d">
|
| 295 |
+
<iframe id="city3d" allow="autoplay"></iframe>
|
| 296 |
+
<script>document.getElementById('city3d').src = location.protocol === 'file:' ? '3d.html' : '/3d';</script>
|
| 297 |
+
<div id="tooltip" style="z-index:200"></div>
|
| 298 |
<div id="toast-container"></div>
|
| 299 |
<div id="llm-popup"><div class="llm-pop-title">SWITCH LLM</div></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
</div>
|
| 301 |
<div id="sidebar">
|
| 302 |
<div class="sidebar-tabs">
|
|
|
|
| 679 |
// CANVAS SETUP
|
| 680 |
// ============================================================
|
| 681 |
function initCanvas() {
|
| 682 |
+
// 2D canvas disabled — 3D viewport in iframe handles rendering
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 683 |
}
|
| 684 |
function resizeCanvas() {
|
| 685 |
const c = document.getElementById('canvas-container');
|
|
|
|
| 788 |
rectZoomMode = !rectZoomMode;
|
| 789 |
const btn = document.getElementById('btn-rect-zoom');
|
| 790 |
if (btn) btn.classList.toggle('active', rectZoomMode);
|
| 791 |
+
if (canvas) canvas.style.cursor = rectZoomMode ? 'crosshair' : 'default';
|
| 792 |
}
|
| 793 |
|
| 794 |
function animate() {
|
|
|
|
| 3893 |
}
|
| 3894 |
});
|
| 3895 |
|
| 3896 |
+
// ============================================================
|
| 3897 |
+
// 3D IFRAME BRIDGE
|
| 3898 |
+
// ============================================================
|
| 3899 |
+
function send3d(cmd) {
|
| 3900 |
+
const f = document.getElementById('city3d');
|
| 3901 |
+
if (f?.contentWindow) f.contentWindow.postMessage({ type: cmd }, '*');
|
| 3902 |
+
}
|
| 3903 |
+
window.addEventListener('message', (e) => {
|
| 3904 |
+
if (e.data?.type === 'agent-select' && e.data.agentId) {
|
| 3905 |
+
selectedAgentId = e.data.agentId;
|
| 3906 |
+
switchTab('agents');
|
| 3907 |
+
fetchAgentDetail(e.data.agentId);
|
| 3908 |
+
}
|
| 3909 |
+
});
|
| 3910 |
+
|
| 3911 |
// ============================================================
|
| 3912 |
// INIT
|
| 3913 |
// ============================================================
|
|
|
|
| 3914 |
showDefaultDetail();
|
| 3915 |
fetchState();
|
| 3916 |
fetchControls();
|