<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three/build/three.module.js",
"three-spritetext": "https://unpkg.com/three-spritetext/dist/three-spritetext.mjs",
"3d-force-graph": "https://unpkg.com/3d-force-graph/dist/3d-force-graph.mjs"
}
}
</script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="controls">
<h3>ToT 3D Explorer</h3>
<label>THÊM Ý TƯỞNG (NODE)</label>
<input type="text" id="nodeName" placeholder="Tên ý tưởng...">
<button onclick="addNewNode()">Thêm Nút</button>
<hr>
<label>TẠO LIÊN KẾT (LINK)</label>
<input type="text" id="sourceNode" placeholder="ID Nguồn (số)">
<input type="text" id="targetNode" placeholder="ID Đích (số)">
<input type="text" id="linkText" placeholder="Nội dung liên kết (VD: Gây ra)">
<button onclick="addNewLink()">Kết nối</button>
<hr>
<p id="selectedInfo" style="font-size: 0.85em; color: #ffeb3b; min-height: 20px;">Chưa chọn mục nào</p>
<button class="delete" onclick="deleteElement()">Xóa phần tử</button>
<div class="instructions">
<b>Thao tác:</b><br>
• Cuộn chuột: Zoom<br>
• Chuột trái: Xoay không gian<br>
• Chuột phải: Di chuyển nội dung
</div>
</div>
<div id="3d-graph"></div>
<script src="script.js"></script>
</body>
</html>
body {
margin: 0;
overflow: hidden;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #050505;
}
#controls {
position: absolute; top: 15px; left: 15px;
background: rgba(0, 0, 0, 0.8);
padding: 20px; border-radius: 12px;
color: #e0e0e0;
backdrop-filter: blur(10px);
z-index: 10;
border: 1px solid rgba(255,255,255,0.1);
width: 250px;
box-shadow: 0 4px 15px rgba(0,0,0,0.5);
}
input, button {
margin: 8px 0;
display: block;
width: 100%;
padding: 10px;
border-radius: 6px;
border: 1px solid #333;
background: #1a1a1a;
color: white;
box-sizing: border-box;
}
button {
cursor: pointer;
background: #2196f3;
border: none;
font-weight: bold;
transition: 0.3s;
}
button:hover {
background: #1976d2;
}
button.delete {
background: #d32f2f;
margin-top: 15px;
}
button.delete:hover {
background: #b71c1c;
}
.instructions {
font-size: 0.75em;
color: #888;
margin-top: 15px;
line-height: 1.4;
}
h3 {
margin: 0 0 15px 0;
color: #2196f3;
font-size: 1.2em;
text-align: center;
border-bottom: 1px solid #333;
padding-bottom: 10px;
}
hr {
border: 0;
border-top: 1px solid #333;
margin: 15px 0;
}
label {
font-size: 0.8em;
color: #aaa;
}
// Dữ liệu ban đầu
let gData = {
nodes: [
{ id: '1', name: 'Vấn đề chính', color: '#f44336' },
{ id: '2', name: 'Giải pháp A', color: '#4caf50' },
{ id: '3', name: 'Kết quả mong đợi', color: '#2196f3' }
],
links: [
{ source: '1', target: '2', label: 'Áp dụng' },
{ source: '2', target: '3', label: 'Dẫn đến' }
]
};
let selectedObj = null;
const Graph = ForceGraph3D()
(document.getElementById('3d-graph'))
.graphData(gData)
.nodeLabel('name')
.nodeAutoColorBy('id')
// Cấu hình hiển thị nhãn cho Link
.linkThreeObjectExtend(true)
.linkThreeObject(link => {
// Sử dụng SpriteText để nhãn luôn hướng về camera
const sprite = new SpriteText(`${link.label || ''}`);
sprite.color = '#cccccc';
sprite.textHeight = 1.5; // Kích thước chữ
return sprite;
})
.linkPositionUpdate((sprite, { start, end }) => {
// Định vị nhãn ở giữa sợi dây liên kết
const middlePos = Object.assign({}, ...['x', 'y', 'z'].map(c => ({
[c]: start[c] + (end[c] - start[c]) / 2
})));
Object.assign(sprite.position, middlePos);
})
.linkDirectionalParticles(3)
.linkDirectionalParticleSpeed(0.005)
.onNodeClick(node => {
selectedObj = { type: 'node', data: node };
document.getElementById('selectedInfo').innerText = `Chọn Nút: ${node.name} (ID: ${node.id})`;
})
.onLinkClick(link => {
selectedObj = { type: 'link', data: link };
document.getElementById('selectedInfo').innerText = `Chọn Link: ${link.label || 'Không tên'}`;
});
function addNewNode() {
const name = document.getElementById('nodeName').value || "Ý tưởng mới";
const { nodes, links } = Graph.graphData();
// Tạo ID số tiếp theo
const maxId = nodes.length > 0 ? Math.max(...nodes.map(n => parseInt(n.id))) : 0;
const id = (maxId + 1).toString();
Graph.graphData({
nodes: [...nodes, { id, name }],
links: links
});
document.getElementById('nodeName').value = '';
}
function addNewLink() {
const source = document.getElementById('sourceNode').value;
const target = document.getElementById('targetNode').value;
const label = document.getElementById('linkText').value || "";
const { nodes, links } = Graph.graphData();
if (nodes.find(n => n.id === source) && nodes.find(n => n.id === target)) {
Graph.graphData({
nodes: nodes,
links: [...links, { source, target, label }]
});
// Xóa input sau khi thêm
document.getElementById('sourceNode').value = '';
document.getElementById('targetNode').value = '';
document.getElementById('linkText').value = '';
} else {
alert("Lỗi: ID nguồn hoặc đích không tồn tại trên bản đồ!");
}
}
function deleteElement() {
if (!selectedObj) return alert("Vui lòng chọn một nút hoặc liên kết trước!");
let { nodes, links } = Graph.graphData();
if (selectedObj.type === 'node') {
nodes = nodes.filter(n => n.id !== selectedObj.data.id);
links = links.filter(l => l.source.id !== selectedObj.data.id && l.target.id !== selectedObj.data.id);
} else {
links = links.filter(l => l !== selectedObj.data);
}
Graph.graphData({ nodes, links });
selectedObj = null;
document.getElementById('selectedInfo').innerText = "Đã xóa phần tử thành công.";
}