@graphty/layout 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +11 -0
- package/.github/workflows/ci.yml +89 -14
- package/.releaserc.json +22 -0
- package/CHANGELOG.md +31 -0
- package/CLAUDE.md +104 -0
- package/CONTRIBUTING.md +1 -0
- package/DEPLOYMENT.md +59 -0
- package/README.md +662 -93
- package/dist/layout-helpers.d.ts +123 -0
- package/dist/layout-helpers.js +458 -0
- package/dist/layout-helpers.js.map +1 -0
- package/dist/layout.d.ts +275 -0
- package/dist/layout.js +2304 -0
- package/dist/layout.js.map +1 -0
- package/dist/vitest.config.d.ts +2 -0
- package/dist/vitest.config.js +30 -0
- package/dist/vitest.config.js.map +1 -0
- package/examples/3d-force-directed.html +611 -0
- package/examples/3d-kamada-kawai.html +394 -0
- package/examples/3d-layout-comparison.html +448 -0
- package/examples/3d-spherical-layout.html +319 -0
- package/examples/arf-layout.html +13 -1
- package/examples/bfs-layout.html +49 -39
- package/examples/bipartite-layout.html +89 -69
- package/examples/circular-layout.html +26 -35
- package/examples/forceatlas2-layout.html +134 -28
- package/examples/index.html +75 -0
- package/examples/kamada-kawai-layout.html +13 -1
- package/examples/multipartite-layout.html +73 -88
- package/examples/planar-layout.html +13 -1
- package/examples/random-layout.html +13 -1
- package/examples/shell-layout.html +65 -34
- package/examples/spectral-layout.html +13 -1
- package/examples/spiral-layout.html +13 -1
- package/examples/spring-layout.html +25 -3
- package/layout-helpers.ts +560 -0
- package/layout.ts +316 -14
- package/package.json +20 -6
- package/test/arf-layout.test.ts +443 -0
- package/test/bfs-layout.test.ts +427 -0
- package/test/bipartite-layout.test.ts +344 -0
- package/test/circular-layout.test.ts +442 -0
- package/test/forceatlas2-layout.test.ts +405 -0
- package/test/fruchterman-reingold-layout.test.ts +477 -0
- package/test/graph-generators.test.ts +450 -0
- package/test/kamada-kawai-layout.test.ts +623 -0
- package/test/multipartite-layout.test.ts +404 -0
- package/test/planar-layout.test.ts +266 -0
- package/test/random-layout.test.ts +254 -0
- package/test/rescale-layout.test.ts +373 -0
- package/test/shell-layout.test.ts +347 -0
- package/test/spectral-layout.test.ts +378 -0
- package/test/spiral-layout.test.ts +338 -0
- package/test/spring-layout.test.ts +241 -0
- package/vite.config.js +36 -0
- package/vitest.config.ts +30 -0
- package/.releaserc +0 -3
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<!-- Eruda Mobile Console -->
|
|
5
|
+
<script src="https://cdn.jsdelivr.net/npm/eruda@3/eruda.min.js"></script>
|
|
6
|
+
<script>
|
|
7
|
+
// Initialize Eruda console for mobile debugging
|
|
8
|
+
if (typeof eruda !== "undefined") {
|
|
9
|
+
eruda.init();
|
|
10
|
+
// Auto-show on mobile devices
|
|
11
|
+
if (/mobile|android|ios|iphone|ipad|ipod/i.test(navigator.userAgent.toLowerCase())) {
|
|
12
|
+
eruda.show();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
</script>
|
|
16
|
+
<meta charset="UTF-8">
|
|
17
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
18
|
+
<title>3D Layout Comparison - @graphty/layout</title>
|
|
19
|
+
<style>
|
|
20
|
+
body {
|
|
21
|
+
margin: 0;
|
|
22
|
+
font-family: Arial, sans-serif;
|
|
23
|
+
overflow: hidden;
|
|
24
|
+
background: #0a0a0a;
|
|
25
|
+
}
|
|
26
|
+
#container {
|
|
27
|
+
display: grid;
|
|
28
|
+
grid-template-columns: 1fr 1fr;
|
|
29
|
+
grid-template-rows: 1fr 1fr;
|
|
30
|
+
width: 100vw;
|
|
31
|
+
height: 100vh;
|
|
32
|
+
gap: 2px;
|
|
33
|
+
}
|
|
34
|
+
.viewport {
|
|
35
|
+
position: relative;
|
|
36
|
+
background: #111;
|
|
37
|
+
}
|
|
38
|
+
.viewport canvas {
|
|
39
|
+
width: 100% !important;
|
|
40
|
+
height: 100% !important;
|
|
41
|
+
}
|
|
42
|
+
.viewport-label {
|
|
43
|
+
position: absolute;
|
|
44
|
+
top: 10px;
|
|
45
|
+
left: 10px;
|
|
46
|
+
color: white;
|
|
47
|
+
background: rgba(0, 0, 0, 0.7);
|
|
48
|
+
padding: 8px 12px;
|
|
49
|
+
border-radius: 4px;
|
|
50
|
+
font-size: 14px;
|
|
51
|
+
font-weight: bold;
|
|
52
|
+
pointer-events: none;
|
|
53
|
+
}
|
|
54
|
+
#controls {
|
|
55
|
+
position: absolute;
|
|
56
|
+
top: 50%;
|
|
57
|
+
left: 50%;
|
|
58
|
+
transform: translate(-50%, -50%);
|
|
59
|
+
background: rgba(0, 0, 0, 0.9);
|
|
60
|
+
padding: 20px;
|
|
61
|
+
border-radius: 8px;
|
|
62
|
+
color: white;
|
|
63
|
+
border: 1px solid #333;
|
|
64
|
+
z-index: 100;
|
|
65
|
+
}
|
|
66
|
+
#info {
|
|
67
|
+
position: absolute;
|
|
68
|
+
bottom: 10px;
|
|
69
|
+
left: 10px;
|
|
70
|
+
color: white;
|
|
71
|
+
background: rgba(0, 0, 0, 0.7);
|
|
72
|
+
padding: 10px;
|
|
73
|
+
border-radius: 5px;
|
|
74
|
+
font-size: 12px;
|
|
75
|
+
}
|
|
76
|
+
button {
|
|
77
|
+
display: block;
|
|
78
|
+
margin: 10px 0;
|
|
79
|
+
padding: 8px 16px;
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
width: 100%;
|
|
82
|
+
background: #2a2a2a;
|
|
83
|
+
color: white;
|
|
84
|
+
border: 1px solid #444;
|
|
85
|
+
border-radius: 4px;
|
|
86
|
+
}
|
|
87
|
+
button:hover {
|
|
88
|
+
background: #3a3a3a;
|
|
89
|
+
}
|
|
90
|
+
label {
|
|
91
|
+
display: block;
|
|
92
|
+
margin: 10px 0;
|
|
93
|
+
}
|
|
94
|
+
select {
|
|
95
|
+
width: 100%;
|
|
96
|
+
padding: 5px;
|
|
97
|
+
margin-top: 5px;
|
|
98
|
+
background: #2a2a2a;
|
|
99
|
+
color: white;
|
|
100
|
+
border: 1px solid #444;
|
|
101
|
+
border-radius: 4px;
|
|
102
|
+
}
|
|
103
|
+
.sync-indicator {
|
|
104
|
+
display: inline-block;
|
|
105
|
+
width: 8px;
|
|
106
|
+
height: 8px;
|
|
107
|
+
background: #4f4;
|
|
108
|
+
border-radius: 50%;
|
|
109
|
+
margin-left: 5px;
|
|
110
|
+
vertical-align: middle;
|
|
111
|
+
}
|
|
112
|
+
</style>
|
|
113
|
+
</head>
|
|
114
|
+
<body>
|
|
115
|
+
<div id="container">
|
|
116
|
+
<div class="viewport" id="viewport1">
|
|
117
|
+
<div class="viewport-label">Spherical Layout</div>
|
|
118
|
+
</div>
|
|
119
|
+
<div class="viewport" id="viewport2">
|
|
120
|
+
<div class="viewport-label">Kamada-Kawai Layout</div>
|
|
121
|
+
</div>
|
|
122
|
+
<div class="viewport" id="viewport3">
|
|
123
|
+
<div class="viewport-label">Spring Layout</div>
|
|
124
|
+
</div>
|
|
125
|
+
<div class="viewport" id="viewport4">
|
|
126
|
+
<div class="viewport-label">ForceAtlas2 Layout</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<div id="controls">
|
|
131
|
+
<h3>3D Layout Comparison <span class="sync-indicator"></span></h3>
|
|
132
|
+
<label>
|
|
133
|
+
Graph Type:
|
|
134
|
+
<select id="graphType">
|
|
135
|
+
<option value="grid">Grid Graph (4×4)</option>
|
|
136
|
+
<option value="complete">Complete Graph (K8)</option>
|
|
137
|
+
<option value="cycle">Cycle Graph (C20)</option>
|
|
138
|
+
<option value="star">Star Graph (S15)</option>
|
|
139
|
+
<option value="wheel">Wheel Graph (W12)</option>
|
|
140
|
+
<option value="bipartite">Bipartite Graph</option>
|
|
141
|
+
<option value="scalefree">Scale-Free Graph</option>
|
|
142
|
+
<option value="random">Random Graph</option>
|
|
143
|
+
</select>
|
|
144
|
+
</label>
|
|
145
|
+
<button id="regenerate">Generate New Graph</button>
|
|
146
|
+
<button id="syncCameras">Sync All Cameras</button>
|
|
147
|
+
<label>
|
|
148
|
+
<input type="checkbox" id="autoRotate" checked> Auto Rotate
|
|
149
|
+
</label>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div id="info">
|
|
153
|
+
<div>Drag to rotate • Scroll to zoom • Click center panel to change graph</div>
|
|
154
|
+
<div>Nodes: <span id="nodeCount">0</span> • Edges: <span id="edgeCount">0</span></div>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<!-- Import map for module resolution -->
|
|
158
|
+
<script type="importmap">
|
|
159
|
+
{
|
|
160
|
+
"imports": {
|
|
161
|
+
"three": "https://unpkg.com/three@0.157.0/build/three.module.js",
|
|
162
|
+
"three/addons/": "https://unpkg.com/three@0.157.0/examples/jsm/"
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
</script>
|
|
166
|
+
|
|
167
|
+
<script type="module">
|
|
168
|
+
import * as THREE from 'https://unpkg.com/three@0.157.0/build/three.module.js';
|
|
169
|
+
import { OrbitControls } from 'https://unpkg.com/three@0.157.0/examples/jsm/controls/OrbitControls.js';
|
|
170
|
+
import {
|
|
171
|
+
circularLayout,
|
|
172
|
+
kamadaKawaiLayout,
|
|
173
|
+
springLayout,
|
|
174
|
+
forceatlas2Layout,
|
|
175
|
+
completeGraph,
|
|
176
|
+
cycleGraph,
|
|
177
|
+
starGraph,
|
|
178
|
+
wheelGraph,
|
|
179
|
+
gridGraph,
|
|
180
|
+
bipartiteGraph,
|
|
181
|
+
scaleFreeGraph,
|
|
182
|
+
randomGraph
|
|
183
|
+
} from './layout.js';
|
|
184
|
+
|
|
185
|
+
// Viewport configurations
|
|
186
|
+
const viewports = [
|
|
187
|
+
{ id: 'viewport1', layout: 'spherical', scene: null, camera: null, renderer: null, controls: null },
|
|
188
|
+
{ id: 'viewport2', layout: 'kamadaKawai', scene: null, camera: null, renderer: null, controls: null },
|
|
189
|
+
{ id: 'viewport3', layout: 'spring', scene: null, camera: null, renderer: null, controls: null },
|
|
190
|
+
{ id: 'viewport4', layout: 'forceatlas2', scene: null, camera: null, renderer: null, controls: null }
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
// Shared graph data
|
|
194
|
+
let currentGraph = null;
|
|
195
|
+
let sharedNodeMaterial = null;
|
|
196
|
+
let sharedEdgeMaterial = null;
|
|
197
|
+
|
|
198
|
+
// Create materials
|
|
199
|
+
function createMaterials() {
|
|
200
|
+
sharedNodeMaterial = new THREE.MeshPhongMaterial({
|
|
201
|
+
vertexColors: true,
|
|
202
|
+
emissive: 0x222222,
|
|
203
|
+
shininess: 100
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
sharedEdgeMaterial = new THREE.LineBasicMaterial({
|
|
207
|
+
color: 0x4466ff,
|
|
208
|
+
transparent: true,
|
|
209
|
+
opacity: 0.3
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Initialize viewport
|
|
214
|
+
function initViewport(viewport) {
|
|
215
|
+
const container = document.getElementById(viewport.id);
|
|
216
|
+
const rect = container.getBoundingClientRect();
|
|
217
|
+
|
|
218
|
+
// Scene
|
|
219
|
+
viewport.scene = new THREE.Scene();
|
|
220
|
+
viewport.scene.background = new THREE.Color(0x111111);
|
|
221
|
+
|
|
222
|
+
// Camera
|
|
223
|
+
viewport.camera = new THREE.PerspectiveCamera(
|
|
224
|
+
60,
|
|
225
|
+
rect.width / rect.height,
|
|
226
|
+
0.1,
|
|
227
|
+
2000
|
|
228
|
+
);
|
|
229
|
+
viewport.camera.position.set(300, 200, 400);
|
|
230
|
+
|
|
231
|
+
// Renderer
|
|
232
|
+
viewport.renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
233
|
+
viewport.renderer.setSize(rect.width, rect.height);
|
|
234
|
+
container.appendChild(viewport.renderer.domElement);
|
|
235
|
+
|
|
236
|
+
// Controls
|
|
237
|
+
viewport.controls = new OrbitControls(viewport.camera, viewport.renderer.domElement);
|
|
238
|
+
viewport.controls.enableDamping = true;
|
|
239
|
+
viewport.controls.dampingFactor = 0.05;
|
|
240
|
+
|
|
241
|
+
// Lighting
|
|
242
|
+
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
|
243
|
+
viewport.scene.add(ambientLight);
|
|
244
|
+
|
|
245
|
+
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.4);
|
|
246
|
+
directionalLight.position.set(1, 1, 1);
|
|
247
|
+
viewport.scene.add(directionalLight);
|
|
248
|
+
|
|
249
|
+
// Groups
|
|
250
|
+
viewport.nodeGroup = new THREE.Group();
|
|
251
|
+
viewport.edgeGroup = new THREE.Group();
|
|
252
|
+
viewport.scene.add(viewport.nodeGroup);
|
|
253
|
+
viewport.scene.add(viewport.edgeGroup);
|
|
254
|
+
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Create graph
|
|
258
|
+
function createGraph(type) {
|
|
259
|
+
switch (type) {
|
|
260
|
+
case 'complete':
|
|
261
|
+
return completeGraph(8);
|
|
262
|
+
case 'cycle':
|
|
263
|
+
return cycleGraph(20);
|
|
264
|
+
case 'star':
|
|
265
|
+
return starGraph(15);
|
|
266
|
+
case 'wheel':
|
|
267
|
+
return wheelGraph(12);
|
|
268
|
+
case 'grid':
|
|
269
|
+
return gridGraph(4, 4);
|
|
270
|
+
case 'bipartite':
|
|
271
|
+
return bipartiteGraph(8, 7, 0.4, Date.now());
|
|
272
|
+
case 'scalefree':
|
|
273
|
+
return scaleFreeGraph(20, 2, Date.now());
|
|
274
|
+
case 'random':
|
|
275
|
+
return randomGraph(20, 0.15, Date.now());
|
|
276
|
+
default:
|
|
277
|
+
return gridGraph(4, 4);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Get layout positions
|
|
282
|
+
function getLayoutPositions(layoutType, graph) {
|
|
283
|
+
const scale = 150;
|
|
284
|
+
const center = [0, 0, 0];
|
|
285
|
+
|
|
286
|
+
switch (layoutType) {
|
|
287
|
+
case 'spherical':
|
|
288
|
+
return circularLayout(graph, scale, center, 3);
|
|
289
|
+
case 'kamadaKawai':
|
|
290
|
+
return kamadaKawaiLayout(graph, null, null, 'weight', scale/150, center, 3);
|
|
291
|
+
case 'spring':
|
|
292
|
+
return springLayout(graph, 1, null, null, 50, scale/150, center, 3);
|
|
293
|
+
case 'forceatlas2':
|
|
294
|
+
return forceatlas2Layout(graph, null, 100, 1.0, 2.0, 1.0, false, false, null, null, null, false, false, null, 3);
|
|
295
|
+
default:
|
|
296
|
+
return circularLayout(graph, scale, center, 3);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Visualize graph in viewport
|
|
301
|
+
function visualizeInViewport(viewport, graph, positions) {
|
|
302
|
+
// Clear previous
|
|
303
|
+
viewport.nodeGroup.clear();
|
|
304
|
+
viewport.edgeGroup.clear();
|
|
305
|
+
|
|
306
|
+
const nodes = graph.nodes();
|
|
307
|
+
const edges = graph.edges();
|
|
308
|
+
|
|
309
|
+
// Calculate node colors based on degree
|
|
310
|
+
const degrees = {};
|
|
311
|
+
nodes.forEach(node => degrees[node] = 0);
|
|
312
|
+
edges.forEach(([u, v]) => {
|
|
313
|
+
degrees[u]++;
|
|
314
|
+
degrees[v]++;
|
|
315
|
+
});
|
|
316
|
+
const maxDegree = Math.max(...Object.values(degrees), 1);
|
|
317
|
+
|
|
318
|
+
// Create nodes
|
|
319
|
+
const nodeGeometry = new THREE.SphereGeometry(6, 16, 16);
|
|
320
|
+
const nodeColors = [];
|
|
321
|
+
const color = new THREE.Color();
|
|
322
|
+
|
|
323
|
+
nodes.forEach((node, i) => {
|
|
324
|
+
const degree = degrees[node];
|
|
325
|
+
color.setHSL(0.6 - (degree / maxDegree) * 0.6, 0.8, 0.5);
|
|
326
|
+
|
|
327
|
+
for (let j = 0; j < nodeGeometry.attributes.position.count; j++) {
|
|
328
|
+
nodeColors.push(color.r, color.g, color.b);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
nodeGeometry.setAttribute('color', new THREE.Float32BufferAttribute(nodeColors, 3));
|
|
333
|
+
|
|
334
|
+
nodes.forEach((node, i) => {
|
|
335
|
+
const mesh = new THREE.Mesh(nodeGeometry.clone(), sharedNodeMaterial.clone());
|
|
336
|
+
const pos = positions[node];
|
|
337
|
+
mesh.position.set(pos[0], pos[1], pos[2]);
|
|
338
|
+
viewport.nodeGroup.add(mesh);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// Create edges
|
|
342
|
+
edges.forEach(([source, target]) => {
|
|
343
|
+
const sourcePos = positions[source];
|
|
344
|
+
const targetPos = positions[target];
|
|
345
|
+
|
|
346
|
+
const points = [
|
|
347
|
+
new THREE.Vector3(sourcePos[0], sourcePos[1], sourcePos[2]),
|
|
348
|
+
new THREE.Vector3(targetPos[0], targetPos[1], targetPos[2])
|
|
349
|
+
];
|
|
350
|
+
|
|
351
|
+
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
|
352
|
+
const line = new THREE.Line(geometry, sharedEdgeMaterial);
|
|
353
|
+
viewport.edgeGroup.add(line);
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Update all viewports
|
|
358
|
+
function updateAllViewports() {
|
|
359
|
+
if (!currentGraph) return;
|
|
360
|
+
|
|
361
|
+
const nodes = currentGraph.nodes();
|
|
362
|
+
const edges = currentGraph.edges();
|
|
363
|
+
document.getElementById('nodeCount').textContent = nodes.length;
|
|
364
|
+
document.getElementById('edgeCount').textContent = edges.length;
|
|
365
|
+
|
|
366
|
+
viewports.forEach(viewport => {
|
|
367
|
+
const positions = getLayoutPositions(viewport.layout, currentGraph);
|
|
368
|
+
visualizeInViewport(viewport, currentGraph, positions);
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Sync cameras
|
|
373
|
+
function syncCameras() {
|
|
374
|
+
const sourceCamera = viewports[0].camera;
|
|
375
|
+
const sourceControls = viewports[0].controls;
|
|
376
|
+
|
|
377
|
+
viewports.slice(1).forEach(viewport => {
|
|
378
|
+
viewport.camera.position.copy(sourceCamera.position);
|
|
379
|
+
viewport.camera.quaternion.copy(sourceCamera.quaternion);
|
|
380
|
+
viewport.controls.target.copy(sourceControls.target);
|
|
381
|
+
viewport.controls.update();
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Initialize
|
|
386
|
+
createMaterials();
|
|
387
|
+
viewports.forEach(viewport => initViewport(viewport));
|
|
388
|
+
|
|
389
|
+
// Create initial graph
|
|
390
|
+
currentGraph = createGraph('grid');
|
|
391
|
+
updateAllViewports();
|
|
392
|
+
|
|
393
|
+
// Controls
|
|
394
|
+
const controlPanel = document.getElementById('controls');
|
|
395
|
+
const graphTypeSelect = document.getElementById('graphType');
|
|
396
|
+
const autoRotateCheckbox = document.getElementById('autoRotate');
|
|
397
|
+
|
|
398
|
+
document.getElementById('regenerate').addEventListener('click', () => {
|
|
399
|
+
currentGraph = createGraph(graphTypeSelect.value);
|
|
400
|
+
updateAllViewports();
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
document.getElementById('syncCameras').addEventListener('click', syncCameras);
|
|
404
|
+
|
|
405
|
+
graphTypeSelect.addEventListener('change', () => {
|
|
406
|
+
currentGraph = createGraph(graphTypeSelect.value);
|
|
407
|
+
updateAllViewports();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Hide controls when clicking outside
|
|
411
|
+
let controlsVisible = true;
|
|
412
|
+
document.addEventListener('click', (e) => {
|
|
413
|
+
if (e.target.closest('#controls')) return;
|
|
414
|
+
controlsVisible = !controlsVisible;
|
|
415
|
+
controlPanel.style.display = controlsVisible ? 'block' : 'none';
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// Animation loop
|
|
419
|
+
function animate() {
|
|
420
|
+
requestAnimationFrame(animate);
|
|
421
|
+
|
|
422
|
+
viewports.forEach(viewport => {
|
|
423
|
+
if (autoRotateCheckbox.checked) {
|
|
424
|
+
viewport.nodeGroup.rotation.y += 0.001;
|
|
425
|
+
viewport.edgeGroup.rotation.y += 0.001;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
viewport.controls.update();
|
|
429
|
+
viewport.renderer.render(viewport.scene, viewport.camera);
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Handle window resize
|
|
434
|
+
window.addEventListener('resize', () => {
|
|
435
|
+
viewports.forEach(viewport => {
|
|
436
|
+
const container = document.getElementById(viewport.id);
|
|
437
|
+
const rect = container.getBoundingClientRect();
|
|
438
|
+
|
|
439
|
+
viewport.camera.aspect = rect.width / rect.height;
|
|
440
|
+
viewport.camera.updateProjectionMatrix();
|
|
441
|
+
viewport.renderer.setSize(rect.width, rect.height);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
animate();
|
|
446
|
+
</script>
|
|
447
|
+
</body>
|
|
448
|
+
</html>
|