RayMelius Claude Opus 4.6 commited on
Commit
97c7b3e
·
1 Parent(s): a797f1b

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>

Files changed (2) hide show
  1. web/3d.html +13 -0
  2. 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
- #canvas-container { flex: 1; position: relative; min-width: 0; }
45
- #cityCanvas { width: 100%; height: 100%; display: block; }
 
 
46
 
47
- /* SIDEBAR */
48
  #sidebar {
49
- width: 260px; background: #16213e; border-left: 2px solid #0f3460;
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" id="btn-rect-zoom" onclick="toggleRectZoom()" title="Draw a rectangle to zoom into that area (Shift+drag)"></button>
283
- <button class="ctrl-btn" onclick="zoomBy(1.3)" title="Zoom In (scroll up)"></button>
284
- <button class="ctrl-btn" onclick="zoomBy(1/1.3)" title="Zoom Out (scroll down)"></button>
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="canvas-container">
294
- <canvas id="cityCanvas"></canvas>
295
- <div id="tooltip"></div>
 
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 = document.getElementById('cityCanvas');
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();