eaglelandsonce commited on
Commit
15de190
·
verified ·
1 Parent(s): 6430538

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +78 -99
index.html CHANGED
@@ -3,43 +3,31 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Terraform 3D Icon Visualizer [Correctly Served]</title>
7
  <style>
8
- body { margin: 0; background-color: #0d1117; color: white; font-family: sans-serif; overflow: hidden; }
9
  canvas { display: block; }
10
- #loader {
11
- position: absolute;
12
- top: 50%;
13
- left: 50%;
14
- transform: translate(-50%, -50%);
15
- z-index: 200;
16
- color: #c9d1d9;
17
- font-size: 1.5em;
18
- }
19
- #info {
20
- position: absolute;
21
- top: 10px;
22
- width: 100%;
23
- text-align: center;
24
- z-index: 100;
25
- color: #c9d1d9;
26
- font-size: 1.2em;
27
- }
28
- #tooltip {
29
- position: absolute;
30
- display: none;
31
- padding: 10px;
32
- background-color: rgba(30, 30, 40, 0.85);
33
- border: 1px solid #444;
34
- border-radius: 5px;
35
- pointer-events: none;
36
- }
37
  </style>
38
  </head>
39
  <body>
 
 
 
 
 
 
 
 
40
  <div id="info">Terraform Azure 3D Visualizer</div>
41
- <div id="loader">Loading 3D Models...</div>
42
  <div id="tooltip"></div>
 
 
43
  <script type="importmap">
44
  {
45
  "imports": {
@@ -49,13 +37,28 @@
49
  }
50
  </script>
51
 
 
52
  <script type="module">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  import * as THREE from 'three';
54
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
55
  import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
56
  import { SVGLoader } from 'three/addons/loaders/SVGLoader.js';
57
 
58
- // --- Terraform Code ---
59
  const terraformCode = `
60
  resource "azurerm_resource_group" "tfexample" { name = "my-terraform-rg" }
61
  resource "azurerm_virtual_network" "tfexample" { resource_group_name = azurerm_resource_group.tfexample.name }
@@ -65,7 +68,7 @@
65
  resource "azurerm_linux_virtual_machine_scale_set" "tfexample" { subnet_id = azurerm_subnet.tfexample.id }
66
  `;
67
 
68
- // --- Parser ---
69
  function parseTerraform(code) {
70
  const resources = [], dependencies = new Set(), regex = /resource "([\w_]+)" "([\w_]+)"/g;
71
  let match;
@@ -78,7 +81,7 @@
78
  return { nodes: resources, edges: Array.from(dependencies).map(d => JSON.parse(d)) };
79
  }
80
 
81
- // --- Icon Map ---
82
  const iconMap = {
83
  "azurerm_resource_group": "https://raw.githubusercontent.com/microsoft/vscode-azure-icons/main/icons/svg/resourcegroup.svg",
84
  "azurerm_virtual_network": "https://raw.githubusercontent.com/microsoft/vscode-azure-icons/main/icons/svg/virtualnetwork.svg",
@@ -89,70 +92,49 @@
89
  "default": "https://raw.githubusercontent.com/microsoft/vscode-azure-icons/main/icons/svg/resource.svg"
90
  };
91
 
92
- // --- Global Variables ---
93
  let camera, scene, renderer, labelRenderer, controls;
94
- const nodes = {}; // Map from node.id to Three.js object
95
- const intersectableObjects = []; // For raycasting
96
 
97
- /**
98
- * Creates a 3D visual node for a resource, returning a Promise
99
- */
100
  function createNodeVisual(node, position) {
101
  return new Promise((resolve, reject) => {
102
  const is3D = node.type === 'azurerm_resource_group' || node.type === 'azurerm_virtual_network';
 
103
 
104
  if (is3D) {
105
- // --- Create 3D Extruded Icon ---
106
- const iconUrl = iconMap[node.type] || iconMap.default;
107
- const svgLoader = new SVGLoader();
108
- svgLoader.load(iconUrl, (data) => {
109
  const iconGroup = new THREE.Group();
110
  const extrudeSettings = { depth: 0.8, bevelEnabled: true, bevelThickness: 0.2, bevelSize: 0.1, bevelSegments: 2 };
111
-
112
  data.paths.forEach(path => {
113
  const material = new THREE.MeshStandardMaterial({ color: path.color, metalness: 0.5, roughness: 0.5 });
114
- const shapes = SVGLoader.createShapes(path);
115
- shapes.forEach(shape => {
116
  const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
117
- geometry.center(); // Center the geometry itself
118
- const mesh = new THREE.Mesh(geometry, material);
119
- iconGroup.add(mesh);
120
  });
121
  });
122
-
123
  const box = new THREE.Box3().setFromObject(iconGroup);
124
- const scale = 5 / box.getSize(new THREE.Vector3()).length();
125
- iconGroup.scale.set(scale, scale, scale);
126
  iconGroup.position.copy(position);
127
  resolve(iconGroup);
128
-
129
- }, undefined, (error) => {
130
- console.error(`Failed to load SVG for ${node.id}:`, error);
131
- reject(error);
132
- });
133
  } else {
134
- // --- Create 2D Sprite Icon (Fallback) ---
135
- const iconUrl = iconMap[node.type] || iconMap.default;
136
- const textureLoader = new THREE.TextureLoader();
137
- textureLoader.load(iconUrl, (map) => {
138
- const material = new THREE.SpriteMaterial({ map: map });
139
- const sprite = new THREE.Sprite(material);
140
  sprite.scale.set(6, 6, 6);
141
  sprite.position.copy(position);
142
  resolve(sprite);
143
- }, undefined, (error) => {
144
- console.error(`Failed to load texture for ${node.id}:`, error);
145
- reject(error);
146
- });
147
  }
148
  });
149
  }
150
 
151
- /**
152
- * Main function to initialize the scene and run the visualizer
153
- */
154
  async function init() {
155
- // --- Basic Scene Setup ---
 
 
156
  scene = new THREE.Scene();
157
  scene.background = new THREE.Color(0x0d1117);
158
  camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
@@ -172,53 +154,50 @@
172
  dirLight.position.set(8, 15, 10);
173
  scene.add(dirLight);
174
 
175
- // --- Load and Create All Nodes ---
176
  const graph = parseTerraform(terraformCode);
 
177
  const nodePromises = graph.nodes.map((node, index) => {
178
  const angle = (index / graph.nodes.length) * Math.PI * 2;
179
  const position = new THREE.Vector3(30 * Math.cos(angle), (Math.random() - 0.5) * 15, 30 * Math.sin(angle));
180
  return createNodeVisual(node, position);
181
  });
182
 
183
- // --- Wait for all nodes to be loaded ---
184
  const loadedNodes = await Promise.all(nodePromises);
185
- document.getElementById('loader').style.display = 'none'; // Hide loader
186
 
187
- // --- Add nodes to scene and map ---
188
  loadedNodes.forEach((visualNode, index) => {
189
  const nodeInfo = graph.nodes[index];
190
  visualNode.userData = { id: nodeInfo.id, type: nodeInfo.type };
191
  nodes[nodeInfo.id] = visualNode;
192
  intersectableObjects.push(visualNode);
193
  scene.add(visualNode);
194
-
195
  const labelDiv = document.createElement('div');
196
  labelDiv.textContent = nodeInfo.id;
197
  labelDiv.style.color = '#c9d1d9';
198
  labelDiv.style.fontSize = '12px';
199
  labelDiv.style.marginTop = '-1em';
200
  const label = new CSS2DObject(labelDiv);
201
- label.position.set(0, -5, 0); // Position label below icon
202
  visualNode.add(label);
203
  });
204
 
205
- // --- Draw Dependency Lines ---
206
  graph.edges.forEach(edge => {
207
  const startNode = nodes[edge.from];
208
  const endNode = nodes[edge.to];
209
  if (startNode && endNode) {
210
- const material = new THREE.LineBasicMaterial({ color: 0x3081f8, transparent: true, opacity: 0.7 });
211
  const geometry = new THREE.BufferGeometry().setFromPoints([startNode.position, endNode.position]);
212
- const line = new THREE.Line(geometry, material);
213
- scene.add(line);
214
  }
215
  });
216
 
217
- // --- Start Animation ---
218
  animate();
219
  }
220
 
221
- // --- Animation and Interaction Loop ---
222
  const raycaster = new THREE.Raycaster();
223
  const mouse = new THREE.Vector2();
224
  const tooltip = document.getElementById('tooltip');
@@ -226,34 +205,34 @@
226
  function animate() {
227
  requestAnimationFrame(animate);
228
  controls.update();
229
-
230
- // Raycasting for tooltips
231
  raycaster.setFromCamera(mouse, camera);
232
  const intersects = raycaster.intersectObjects(intersectableObjects, true);
233
- let hovered = false;
234
  if (intersects.length > 0) {
235
  let obj = intersects[0].object;
236
- while(obj.parent && !obj.userData.id) obj = obj.parent; // Find parent group
237
  if (obj.userData.id) {
238
  tooltip.style.display = 'block';
239
- tooltip.style.left = (event.clientX + 15) + 'px';
240
- tooltip.style.top = (event.clientY + 15) + 'px';
241
  tooltip.innerHTML = `<b>Type:</b> ${obj.userData.type}<br><b>Name:</b> ${obj.userData.id}`;
242
- hovered = true;
243
  }
 
 
244
  }
245
- if(!hovered) tooltip.style.display = 'none';
246
 
247
  renderer.render(scene, camera);
248
  labelRenderer.render(scene, camera);
249
  }
250
 
251
- function onMouseMove(event) {
252
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
253
  mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
254
- }
 
 
 
 
255
 
256
- window.addEventListener('mousemove', onMouseMove, false);
257
  window.addEventListener('resize', () => {
258
  if(camera) {
259
  camera.aspect = window.innerWidth / window.innerHeight;
@@ -261,14 +240,14 @@
261
  renderer.setSize(window.innerWidth, window.innerHeight);
262
  labelRenderer.setSize(window.innerWidth, window.innerHeight);
263
  }
264
- }, false);
265
 
266
- // --- Run Everything ---
267
  init().catch(error => {
268
- console.error("Failed to initialize visualizer:", error);
269
- document.getElementById('loader').innerText = 'Error: Could not load 3D models. See console for details.';
 
270
  });
271
-
272
  </script>
273
  </body>
274
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Terraform 3D Icon Visualizer</title>
7
  <style>
8
+ body { margin: 0; background-color: #0d1117; color: white; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; overflow: hidden; }
9
  canvas { display: block; }
10
+ #loader-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; z-index: 200; backdrop-filter: blur(5px); background-color: rgba(13, 17, 23, 0.5);}
11
+ #loader-content { text-align: center; padding: 2em; background-color: #161b22; border-radius: 10px; border: 1px solid #30363d; }
12
+ #loader-content p { font-size: 1.5em; color: #c9d1d9; margin-top: 0; }
13
+ #loader-content span { font-size: 1em; color: #8b949e; max-width: 500px; display: inline-block; }
14
+ #info { position: absolute; top: 10px; width: 100%; text-align: center; z-index: 100; color: #c9d1d9; font-size: 1.2em; pointer-events: none; }
15
+ #tooltip { position: absolute; display: none; padding: 10px; background-color: rgba(22, 27, 34, 0.9); border: 1px solid #30363d; border-radius: 5px; pointer-events: none; backdrop-filter: blur(5px); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  </style>
17
  </head>
18
  <body>
19
+ <!-- This container provides status updates and error messages -->
20
+ <div id="loader-container">
21
+ <div id="loader-content">
22
+ <p>Loading Visualizer...</p>
23
+ <span id="loader-message">Initializing 3D scene...</span>
24
+ </div>
25
+ </div>
26
+
27
  <div id="info">Terraform Azure 3D Visualizer</div>
 
28
  <div id="tooltip"></div>
29
+
30
+ <!-- Importmap for Three.js modules -->
31
  <script type="importmap">
32
  {
33
  "imports": {
 
37
  }
38
  </script>
39
 
40
+ <!-- Main JavaScript Module -->
41
  <script type="module">
42
+ // BUILT-IN CHECK: This is the critical fix.
43
+ if (window.location.protocol === 'file:') {
44
+ const loaderContent = document.getElementById('loader-content');
45
+ loaderContent.innerHTML = `
46
+ <p style="color: #f85149;">Error: Cannot Run From File</p>
47
+ <span style="max-width: 500px; display: inline-block;">
48
+ You must open this HTML file using a local web server due to browser security rules (CORS).
49
+ <br><br>
50
+ <strong>The easiest way is with the 'Live Server' extension in VS Code.</strong>
51
+ </span>
52
+ `;
53
+ throw new Error("Execution stopped: This file must be served over HTTP/HTTPS, not from a file: URL.");
54
+ }
55
+
56
  import * as THREE from 'three';
57
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
58
  import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
59
  import { SVGLoader } from 'three/addons/loaders/SVGLoader.js';
60
 
61
+ // The Terraform HCL code to visualize
62
  const terraformCode = `
63
  resource "azurerm_resource_group" "tfexample" { name = "my-terraform-rg" }
64
  resource "azurerm_virtual_network" "tfexample" { resource_group_name = azurerm_resource_group.tfexample.name }
 
68
  resource "azurerm_linux_virtual_machine_scale_set" "tfexample" { subnet_id = azurerm_subnet.tfexample.id }
69
  `;
70
 
71
+ // Simple HCL parser for resources and their direct dependencies
72
  function parseTerraform(code) {
73
  const resources = [], dependencies = new Set(), regex = /resource "([\w_]+)" "([\w_]+)"/g;
74
  let match;
 
81
  return { nodes: resources, edges: Array.from(dependencies).map(d => JSON.parse(d)) };
82
  }
83
 
84
+ // Mapping resource types to their official Azure SVG icons
85
  const iconMap = {
86
  "azurerm_resource_group": "https://raw.githubusercontent.com/microsoft/vscode-azure-icons/main/icons/svg/resourcegroup.svg",
87
  "azurerm_virtual_network": "https://raw.githubusercontent.com/microsoft/vscode-azure-icons/main/icons/svg/virtualnetwork.svg",
 
92
  "default": "https://raw.githubusercontent.com/microsoft/vscode-azure-icons/main/icons/svg/resource.svg"
93
  };
94
 
 
95
  let camera, scene, renderer, labelRenderer, controls;
96
+ const nodes = {};
97
+ const intersectableObjects = [];
98
 
99
+ // Function to create a visual node (3D or 2D sprite), returns a Promise
 
 
100
  function createNodeVisual(node, position) {
101
  return new Promise((resolve, reject) => {
102
  const is3D = node.type === 'azurerm_resource_group' || node.type === 'azurerm_virtual_network';
103
+ const iconUrl = iconMap[node.type] || iconMap.default;
104
 
105
  if (is3D) {
106
+ new SVGLoader().load(iconUrl, (data) => {
 
 
 
107
  const iconGroup = new THREE.Group();
108
  const extrudeSettings = { depth: 0.8, bevelEnabled: true, bevelThickness: 0.2, bevelSize: 0.1, bevelSegments: 2 };
 
109
  data.paths.forEach(path => {
110
  const material = new THREE.MeshStandardMaterial({ color: path.color, metalness: 0.5, roughness: 0.5 });
111
+ SVGLoader.createShapes(path).forEach(shape => {
 
112
  const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
113
+ geometry.center();
114
+ iconGroup.add(new THREE.Mesh(geometry, material));
 
115
  });
116
  });
 
117
  const box = new THREE.Box3().setFromObject(iconGroup);
118
+ iconGroup.scale.multiplyScalar(5 / box.getSize(new THREE.Vector3()).length());
 
119
  iconGroup.position.copy(position);
120
  resolve(iconGroup);
121
+ }, undefined, (error) => reject(`SVG Load Error for ${node.id}: ${error}`));
 
 
 
 
122
  } else {
123
+ new THREE.TextureLoader().load(iconUrl, (map) => {
124
+ const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map }));
 
 
 
 
125
  sprite.scale.set(6, 6, 6);
126
  sprite.position.copy(position);
127
  resolve(sprite);
128
+ }, undefined, (error) => reject(`Texture Load Error for ${node.id}: ${error}`));
 
 
 
129
  }
130
  });
131
  }
132
 
133
+ // Main function to initialize the scene and run the visualizer
 
 
134
  async function init() {
135
+ const loaderMessage = document.getElementById('loader-message');
136
+
137
+ // Setup basic Three.js scene
138
  scene = new THREE.Scene();
139
  scene.background = new THREE.Color(0x0d1117);
140
  camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
 
154
  dirLight.position.set(8, 15, 10);
155
  scene.add(dirLight);
156
 
157
+ // Parse Terraform and create node creation promises
158
  const graph = parseTerraform(terraformCode);
159
+ loaderMessage.innerText = `Found ${graph.nodes.length} resources. Loading models...`;
160
  const nodePromises = graph.nodes.map((node, index) => {
161
  const angle = (index / graph.nodes.length) * Math.PI * 2;
162
  const position = new THREE.Vector3(30 * Math.cos(angle), (Math.random() - 0.5) * 15, 30 * Math.sin(angle));
163
  return createNodeVisual(node, position);
164
  });
165
 
166
+ // Wait for all models to load before proceeding
167
  const loadedNodes = await Promise.all(nodePromises);
168
+ document.getElementById('loader-container').style.display = 'none';
169
 
170
+ // Add loaded nodes to the scene
171
  loadedNodes.forEach((visualNode, index) => {
172
  const nodeInfo = graph.nodes[index];
173
  visualNode.userData = { id: nodeInfo.id, type: nodeInfo.type };
174
  nodes[nodeInfo.id] = visualNode;
175
  intersectableObjects.push(visualNode);
176
  scene.add(visualNode);
 
177
  const labelDiv = document.createElement('div');
178
  labelDiv.textContent = nodeInfo.id;
179
  labelDiv.style.color = '#c9d1d9';
180
  labelDiv.style.fontSize = '12px';
181
  labelDiv.style.marginTop = '-1em';
182
  const label = new CSS2DObject(labelDiv);
183
+ label.position.set(0, -5, 0);
184
  visualNode.add(label);
185
  });
186
 
187
+ // Draw dependency lines now that nodes have positions
188
  graph.edges.forEach(edge => {
189
  const startNode = nodes[edge.from];
190
  const endNode = nodes[edge.to];
191
  if (startNode && endNode) {
 
192
  const geometry = new THREE.BufferGeometry().setFromPoints([startNode.position, endNode.position]);
193
+ scene.add(new THREE.Line(geometry, new THREE.LineBasicMaterial({ color: 0x3081f8, transparent: true, opacity: 0.7 })));
 
194
  }
195
  });
196
 
197
+ // Start the animation loop
198
  animate();
199
  }
200
 
 
201
  const raycaster = new THREE.Raycaster();
202
  const mouse = new THREE.Vector2();
203
  const tooltip = document.getElementById('tooltip');
 
205
  function animate() {
206
  requestAnimationFrame(animate);
207
  controls.update();
208
+
209
+ // Handle hover/tooltip logic
210
  raycaster.setFromCamera(mouse, camera);
211
  const intersects = raycaster.intersectObjects(intersectableObjects, true);
 
212
  if (intersects.length > 0) {
213
  let obj = intersects[0].object;
214
+ while(obj.parent && !obj.userData.id) obj = obj.parent;
215
  if (obj.userData.id) {
216
  tooltip.style.display = 'block';
 
 
217
  tooltip.innerHTML = `<b>Type:</b> ${obj.userData.type}<br><b>Name:</b> ${obj.userData.id}`;
 
218
  }
219
+ } else {
220
+ tooltip.style.display = 'none';
221
  }
 
222
 
223
  renderer.render(scene, camera);
224
  labelRenderer.render(scene, camera);
225
  }
226
 
227
+ window.addEventListener('mousemove', (event) => {
228
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
229
  mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
230
+ if (tooltip.style.display === 'block') {
231
+ tooltip.style.left = `${event.clientX + 15}px`;
232
+ tooltip.style.top = `${event.clientY}px`;
233
+ }
234
+ });
235
 
 
236
  window.addEventListener('resize', () => {
237
  if(camera) {
238
  camera.aspect = window.innerWidth / window.innerHeight;
 
240
  renderer.setSize(window.innerWidth, window.innerHeight);
241
  labelRenderer.setSize(window.innerWidth, window.innerHeight);
242
  }
243
+ });
244
 
245
+ // Run the main initialization function
246
  init().catch(error => {
247
+ console.error("Visualizer initialization failed:", error);
248
+ const loaderContent = document.getElementById('loader-content');
249
+ loaderContent.innerHTML = `<p style="color: #f85149;">A critical error occurred.</p><span>Check the browser's developer console (F12) for details.</span>`;
250
  });
 
251
  </script>
252
  </body>
253
  </html>