3142 lines
94 KiB
JavaScript
3142 lines
94 KiB
JavaScript
/**
|
||
* ============================================================================
|
||
* 意识三维空间可视化模块 (Consciousness 3D Space Visualization)
|
||
* ============================================================================
|
||
*
|
||
* 基于 Three.js r150+ 的 AI 意识系统 3D 可视化引擎
|
||
* 使用 Web Components (Custom Elements + Shadow DOM) 封装
|
||
*
|
||
* 核心可视化元素:
|
||
* 1. 记忆星云 (Memory Nebula) - 记忆以星辰形式存在于星云中
|
||
* 2. 知识图谱 (Knowledge Graph) - 实体与关系的力导向三维布局
|
||
* 3. 目标晶体 (Goal Crystals) - 以多面体晶体展示目标层级
|
||
* 4. 技能光环 (Skill Halos) - 圆环形态展示技能熟练度
|
||
* 5. 情绪粒子 (Emotion Particles) - 粒子系统展示情绪状态
|
||
* 6. 反思之镜 (Reflection Mirror) - 半透明镜面带水波效果
|
||
* 7. 社交网络 (Social Network) - 用户节点与关系连线
|
||
*
|
||
* @author Bailongma AI System
|
||
* @version 2.0.0
|
||
* @license MIT
|
||
* ============================================================================
|
||
*/
|
||
|
||
/* =========================================================================
|
||
* 全局常量与工具函数
|
||
* ========================================================================= */
|
||
|
||
/** 情绪颜色映射 */
|
||
const EMOTION_COLORS = {
|
||
joy: 0xFFD700, // 金色 - 喜悦
|
||
sadness: 0x4169E1, // 蓝色 - 悲伤
|
||
anger: 0xFF2400, // 红色 - 愤怒
|
||
curiosity: 0x00E676, // 绿色 - 好奇
|
||
fear: 0x9C27B0, // 紫色 - 恐惧
|
||
surprise: 0xFF6F00, // 橙色 - 惊讶
|
||
disgust: 0x4E342E, // 棕色 - 厌恶
|
||
trust: 0x2196F3, // 浅蓝 - 信任
|
||
};
|
||
|
||
/** 目标状态颜色映射 */
|
||
const GOAL_STATE_COLORS = {
|
||
completed: 0x00E676, // 绿色 - 完成
|
||
active: 0xFFD700, // 黄色 - 进行中
|
||
paused: 0x78909C, // 灰色 - 暂停
|
||
abandoned: 0xF44336, // 红色 - 放弃
|
||
};
|
||
|
||
/** 实体类型颜色映射 */
|
||
const ENTITY_TYPE_COLORS = {
|
||
person: 0x42A5F5, // 蓝色
|
||
concept: 0xAB47BC, // 紫色
|
||
location: 0x66BB6A, // 绿色
|
||
event: 0xFFA726, // 橙色
|
||
skill: 0x26C6DA, // 青色
|
||
emotion: 0xEF5350, // 粉红
|
||
object: 0xFFEE58, // 黄色
|
||
default: 0xBDBDBD, // 灰色
|
||
};
|
||
|
||
/** 技能类型颜色映射 */
|
||
const SKILL_TYPE_COLORS = {
|
||
technical: 0x00BCD4, // 青色
|
||
creative: 0xE91E63, // 粉红
|
||
social: 0x4CAF50, // 绿色
|
||
analytical: 0x2196F3, // 蓝色
|
||
physical: 0xFF9800, // 橙色
|
||
default: 0xBDBDBD, // 灰色
|
||
};
|
||
|
||
/** 视图模式枚举 */
|
||
const VIEW_MODES = {
|
||
ALL: 'all',
|
||
MEMORY: 'memory',
|
||
KNOWLEDGE: 'knowledge',
|
||
GOALS: 'goals',
|
||
SKILLS: 'skills',
|
||
EMOTIONS: 'emotions',
|
||
SOCIAL: 'social',
|
||
PANORAMIC: 'panoramic',
|
||
};
|
||
|
||
/** 缓动函数 */
|
||
function easeInOutCubic(t) {
|
||
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||
}
|
||
|
||
/** 生成随机范围数 */
|
||
function randomRange(min, max) {
|
||
return Math.random() * (max - min) + min;
|
||
}
|
||
|
||
/** 向量归一化到指定范围 */
|
||
function normalizeToRange(value, min, max, newMin, newMax) {
|
||
return ((value - min) / (max - min)) * (newMax - newMin) + newMin;
|
||
}
|
||
|
||
/* =========================================================================
|
||
* 自定义着色器 (Custom Shaders)
|
||
* ========================================================================= */
|
||
|
||
/** 星辰着色器 - 带呼吸闪烁效果 */
|
||
const STAR_VERTEX_SHADER = `
|
||
attribute float size;
|
||
attribute float importance;
|
||
attribute vec3 customColor;
|
||
varying vec3 vColor;
|
||
varying float vImportance;
|
||
uniform float time;
|
||
uniform float globalBrightness;
|
||
|
||
void main() {
|
||
vColor = customColor;
|
||
vImportance = importance;
|
||
|
||
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
||
|
||
// 呼吸闪烁:重要性越高的星星闪烁越稳定
|
||
float breathe = 1.0 + 0.3 * (1.0 - importance) * sin(time * 2.0 + position.x * 10.0);
|
||
|
||
// 大小基于重要性和到相机距离
|
||
gl_PointSize = size * breathe * globalBrightness * (300.0 / -mvPosition.z);
|
||
gl_PointSize = clamp(gl_PointSize, 1.0, 100.0);
|
||
|
||
gl_Position = projectionMatrix * mvPosition;
|
||
}
|
||
`;
|
||
|
||
const STAR_FRAGMENT_SHADER = `
|
||
varying vec3 vColor;
|
||
varying float vImportance;
|
||
uniform float time;
|
||
|
||
void main() {
|
||
// 创建柔和的圆形星点
|
||
vec2 center = gl_PointCoord - vec2(0.5);
|
||
float dist = length(center);
|
||
|
||
// 边缘衰减
|
||
if (dist > 0.5) discard;
|
||
|
||
// 核心高亮 + 外围辉光
|
||
float core = exp(-dist * dist * 30.0);
|
||
float glow = exp(-dist * dist * 8.0);
|
||
float alpha = mix(glow * 0.6, core, vImportance * 0.8);
|
||
|
||
// 添加微妙的色彩脉动
|
||
vec3 color = vColor + 0.1 * sin(time * 1.5 + vImportance * 6.28) * vec3(0.2, 0.1, 0.0);
|
||
|
||
gl_FragColor = vec4(color, alpha);
|
||
}
|
||
`;
|
||
|
||
/** 粒子系统着色器 - 情绪粒子 */
|
||
const PARTICLE_VERTEX_SHADER = `
|
||
attribute float size;
|
||
attribute float alpha;
|
||
attribute vec3 customColor;
|
||
varying vec3 vColor;
|
||
varying float vAlpha;
|
||
|
||
void main() {
|
||
vColor = customColor;
|
||
vAlpha = alpha;
|
||
|
||
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
||
gl_PointSize = size * (250.0 / -mvPosition.z);
|
||
gl_PointSize = clamp(gl_PointSize, 1.0, 80.0);
|
||
gl_Position = projectionMatrix * mvPosition;
|
||
}
|
||
`;
|
||
|
||
const PARTICLE_FRAGMENT_SHADER = `
|
||
varying vec3 vColor;
|
||
varying float vAlpha;
|
||
|
||
void main() {
|
||
float dist = length(gl_PointCoord - vec2(0.5));
|
||
if (dist > 0.5) discard;
|
||
|
||
float softEdge = 1.0 - smoothstep(0.2, 0.5, dist);
|
||
gl_FragColor = vec4(vColor, vAlpha * softEdge);
|
||
}
|
||
`;
|
||
|
||
/** 水波纹效果着色器 */
|
||
const WATER_VERTEX_SHADER = `
|
||
varying vec2 vUv;
|
||
uniform float time;
|
||
|
||
void main() {
|
||
vUv = uv;
|
||
vec3 pos = position;
|
||
|
||
// 水波纹位移
|
||
float wave1 = sin(pos.x * 3.0 + time * 1.5) * cos(pos.z * 2.0 + time * 1.0) * 0.05;
|
||
float wave2 = sin(pos.x * 5.0 - time * 2.0) * cos(pos.z * 4.0 + time * 1.3) * 0.03;
|
||
pos.y += wave1 + wave2;
|
||
|
||
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
|
||
}
|
||
`;
|
||
|
||
const WATER_FRAGMENT_SHADER = `
|
||
varying vec2 vUv;
|
||
uniform float time;
|
||
uniform float reflectivity;
|
||
|
||
void main() {
|
||
// 基础镜面颜色
|
||
vec3 baseColor = vec3(0.4, 0.6, 0.8);
|
||
|
||
// 水波纹图案
|
||
float wave = sin(vUv.x * 20.0 + time * 2.0) * cos(vUv.y * 20.0 + time * 1.5);
|
||
wave = wave * 0.5 + 0.5;
|
||
|
||
// 边缘高光
|
||
float edge = smoothstep(0.0, 0.1, min(min(vUv.x, 1.0 - vUv.x), min(vUv.y, 1.0 - vUv.y)));
|
||
|
||
vec3 color = mix(baseColor, vec3(0.7, 0.9, 1.0), wave * 0.4);
|
||
float alpha = reflectivity * edge * (0.5 + wave * 0.2);
|
||
|
||
gl_FragColor = vec4(color, alpha);
|
||
}
|
||
`;
|
||
|
||
/** 发光线条着色器 - 知识图谱连线 */
|
||
const GLOW_LINE_VERTEX_SHADER = `
|
||
varying float vAlpha;
|
||
|
||
void main() {
|
||
vAlpha = 1.0;
|
||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||
}
|
||
`;
|
||
|
||
const GLOW_LINE_FRAGMENT_SHADER = `
|
||
uniform vec3 color;
|
||
uniform float opacity;
|
||
varying float vAlpha;
|
||
|
||
void main() {
|
||
gl_FragColor = vec4(color, opacity * vAlpha);
|
||
}
|
||
`;
|
||
|
||
/* =========================================================================
|
||
* 辅助数据结构
|
||
* ========================================================================= */
|
||
|
||
/** 力导向布局引擎 (简化版) */
|
||
class ForceLayout3D {
|
||
/**
|
||
* @param {Object} params
|
||
* @param {number} params.repulsion - 排斥力系数
|
||
* @param {number} params.attraction - 吸引力系数
|
||
* @param {number} params.damping - 阻尼系数
|
||
* @param {number} params.iterations - 每次更新的迭代次数
|
||
*/
|
||
constructor(params = {}) {
|
||
this.repulsion = params.repulsion || 50;
|
||
this.attraction = params.attraction || 0.01;
|
||
this.damping = params.damping || 0.9;
|
||
this.iterations = params.iterations || 3;
|
||
this.nodes = new Map();
|
||
this.edges = [];
|
||
this.velocities = new Map();
|
||
}
|
||
|
||
/** 添加节点 */
|
||
addNode(id, position) {
|
||
this.nodes.set(id, position.clone());
|
||
this.velocities.set(id, new THREE.Vector3());
|
||
}
|
||
|
||
/** 添加边 */
|
||
addEdge(sourceId, targetId, weight = 1.0) {
|
||
this.edges.push({ source: sourceId, target: targetId, weight });
|
||
}
|
||
|
||
/** 清除所有节点和边 */
|
||
clear() {
|
||
this.nodes.clear();
|
||
this.edges = [];
|
||
this.velocities.clear();
|
||
}
|
||
|
||
/** 执行一步布局计算 */
|
||
step() {
|
||
const nodeKeys = Array.from(this.nodes.keys());
|
||
|
||
for (let iter = 0; iter < this.iterations; iter++) {
|
||
// 排斥力 - 所有节点互相排斥
|
||
for (let i = 0; i < nodeKeys.length; i++) {
|
||
for (let j = i + 1; j < nodeKeys.length; j++) {
|
||
const posA = this.nodes.get(nodeKeys[i]);
|
||
const posB = this.nodes.get(nodeKeys[j]);
|
||
const diff = new THREE.Vector3().subVectors(posA, posB);
|
||
const dist = diff.length() || 0.01;
|
||
const force = this.repulsion / (dist * dist);
|
||
const forceVec = diff.normalize().multiplyScalar(force);
|
||
|
||
this.velocities.get(nodeKeys[i]).add(forceVec);
|
||
this.velocities.get(nodeKeys[j]).sub(forceVec);
|
||
}
|
||
}
|
||
|
||
// 吸引力 - 连接的节点相互吸引
|
||
for (const edge of this.edges) {
|
||
const posA = this.nodes.get(edge.source);
|
||
const posB = this.nodes.get(edge.target);
|
||
if (!posA || !posB) continue;
|
||
|
||
const diff = new THREE.Vector3().subVectors(posB, posA);
|
||
const dist = diff.length();
|
||
const force = dist * this.attraction * edge.weight;
|
||
const forceVec = diff.normalize().multiplyScalar(force);
|
||
|
||
this.velocities.get(edge.source).add(forceVec);
|
||
this.velocities.get(edge.target).sub(forceVec);
|
||
}
|
||
|
||
// 应用速度和阻尼
|
||
for (const [id, vel] of this.velocities) {
|
||
vel.multiplyScalar(this.damping);
|
||
this.nodes.get(id).add(vel);
|
||
}
|
||
}
|
||
|
||
return this.nodes;
|
||
}
|
||
|
||
/** 获取节点位置 */
|
||
getPositions() {
|
||
const positions = {};
|
||
for (const [id, pos] of this.nodes) {
|
||
positions[id] = pos.clone();
|
||
}
|
||
return positions;
|
||
}
|
||
}
|
||
|
||
/** 粒子爆炸特效管理器 */
|
||
class ParticleExplosion {
|
||
/**
|
||
* @param {THREE.Vector3} origin - 爆炸原点
|
||
* @param {number} color - 粒子颜色
|
||
* @param {number} count - 粒子数量
|
||
* @param {number} duration - 持续时间(秒)
|
||
*/
|
||
constructor(origin, color = 0xFFD700, count = 200, duration = 2.0) {
|
||
this.origin = origin.clone();
|
||
this.color = color;
|
||
this.count = count;
|
||
this.duration = duration;
|
||
this.elapsed = 0;
|
||
this.active = true;
|
||
this.mesh = null;
|
||
this._create();
|
||
}
|
||
|
||
/** 创建粒子系统 */
|
||
_create() {
|
||
const geometry = new THREE.BufferGeometry();
|
||
const positions = new Float32Array(this.count * 3);
|
||
const velocities = new Float32Array(this.count * 3);
|
||
const sizes = new Float32Array(this.count);
|
||
const alphas = new Float32Array(this.count);
|
||
const colors = new Float32Array(this.count * 3);
|
||
|
||
const baseColor = new THREE.Color(this.color);
|
||
|
||
for (let i = 0; i < this.count; i++) {
|
||
const i3 = i * 3;
|
||
|
||
// 从原点向外发射
|
||
positions[i3] = this.origin.x;
|
||
positions[i3 + 1] = this.origin.y;
|
||
positions[i3 + 2] = this.origin.z;
|
||
|
||
// 随机方向速度
|
||
const theta = Math.random() * Math.PI * 2;
|
||
const phi = Math.random() * Math.PI;
|
||
const speed = randomRange(2, 8);
|
||
velocities[i3] = speed * Math.sin(phi) * Math.cos(theta);
|
||
velocities[i3 + 1] = speed * Math.sin(phi) * Math.sin(theta);
|
||
velocities[i3 + 2] = speed * Math.cos(phi);
|
||
|
||
sizes[i] = randomRange(2, 6);
|
||
alphas[i] = 1.0;
|
||
|
||
// 颜色微小变化
|
||
colors[i3] = baseColor.r + randomRange(-0.1, 0.1);
|
||
colors[i3 + 1] = baseColor.g + randomRange(-0.1, 0.1);
|
||
colors[i3 + 2] = baseColor.b + randomRange(-0.1, 0.1);
|
||
}
|
||
|
||
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||
geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
|
||
geometry.setAttribute('alpha', new THREE.BufferAttribute(alphas, 1));
|
||
geometry.setAttribute('customColor', new THREE.BufferAttribute(colors, 3));
|
||
|
||
this.velocities = velocities;
|
||
|
||
const material = new THREE.ShaderMaterial({
|
||
vertexShader: PARTICLE_VERTEX_SHADER,
|
||
fragmentShader: PARTICLE_FRAGMENT_SHADER,
|
||
transparent: true,
|
||
blending: THREE.AdditiveBlending,
|
||
depthWrite: false,
|
||
});
|
||
|
||
this.mesh = new THREE.Points(geometry, material);
|
||
this.mesh.frustumCulled = false;
|
||
}
|
||
|
||
/** 更新动画 */
|
||
update(deltaTime) {
|
||
if (!this.active) return;
|
||
|
||
this.elapsed += deltaTime;
|
||
const progress = this.elapsed / this.duration;
|
||
|
||
if (progress >= 1.0) {
|
||
this.active = false;
|
||
return;
|
||
}
|
||
|
||
const positions = this.mesh.geometry.attributes.position.array;
|
||
const alphas = this.mesh.geometry.attributes.alpha.array;
|
||
const sizes = this.mesh.geometry.attributes.size.array;
|
||
|
||
for (let i = 0; i < this.count; i++) {
|
||
const i3 = i * 3;
|
||
|
||
// 更新位置
|
||
positions[i3] += this.velocities[i3] * deltaTime;
|
||
positions[i3 + 1] += this.velocities[i3 + 1] * deltaTime;
|
||
positions[i3 + 2] += this.velocities[i3 + 2] * deltaTime;
|
||
|
||
// 重力效果
|
||
this.velocities[i3 + 1] -= 3.0 * deltaTime;
|
||
|
||
// 渐隐
|
||
alphas[i] = 1.0 - progress;
|
||
sizes[i] *= 0.995;
|
||
}
|
||
|
||
this.mesh.geometry.attributes.position.needsUpdate = true;
|
||
this.mesh.geometry.attributes.alpha.needsUpdate = true;
|
||
this.mesh.geometry.attributes.size.needsUpdate = true;
|
||
}
|
||
|
||
/** 销毁 */
|
||
dispose() {
|
||
if (this.mesh) {
|
||
this.mesh.geometry.dispose();
|
||
this.mesh.material.dispose();
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
/* =========================================================================
|
||
* 主组件定义:consciousness-3d-space
|
||
* ========================================================================= */
|
||
|
||
/** HTML 模板 */
|
||
const COMPONENT_TEMPLATE = `
|
||
<style>
|
||
:host {
|
||
display: block;
|
||
width: 100%;
|
||
height: 100%;
|
||
position: relative;
|
||
overflow: hidden;
|
||
background: #000;
|
||
font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||
color: #e0e0e0;
|
||
}
|
||
|
||
/* 3D 画布容器 */
|
||
#canvas-container {
|
||
width: 100%;
|
||
height: 100%;
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
}
|
||
|
||
canvas {
|
||
display: block;
|
||
width: 100% !important;
|
||
height: 100% !important;
|
||
}
|
||
|
||
/* ========== 顶部标题栏 ========== */
|
||
#title-bar {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 48px;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 20px;
|
||
background: linear-gradient(180deg, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0) 100%);
|
||
z-index: 100;
|
||
pointer-events: none;
|
||
}
|
||
|
||
#title-bar h1 {
|
||
margin: 0;
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
letter-spacing: 2px;
|
||
color: rgba(100, 200, 255, 0.9);
|
||
text-shadow: 0 0 10px rgba(100, 200, 255, 0.5);
|
||
}
|
||
|
||
#fps-counter {
|
||
margin-left: auto;
|
||
font-size: 12px;
|
||
color: rgba(200, 200, 200, 0.5);
|
||
font-family: monospace;
|
||
}
|
||
|
||
/* ========== 左侧控制面板 ========== */
|
||
#control-panel {
|
||
position: absolute;
|
||
top: 60px;
|
||
left: 12px;
|
||
width: 240px;
|
||
max-height: calc(100vh - 120px);
|
||
overflow-y: auto;
|
||
background: rgba(10, 15, 30, 0.85);
|
||
border: 1px solid rgba(100, 200, 255, 0.15);
|
||
border-radius: 12px;
|
||
padding: 16px;
|
||
z-index: 100;
|
||
backdrop-filter: blur(10px);
|
||
-webkit-backdrop-filter: blur(10px);
|
||
transition: transform 0.3s ease, opacity 0.3s ease;
|
||
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.4);
|
||
}
|
||
|
||
#control-panel.hidden {
|
||
transform: translateX(-280px);
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
|
||
#control-panel::-webkit-scrollbar {
|
||
width: 4px;
|
||
}
|
||
#control-panel::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
#control-panel::-webkit-scrollbar-thumb {
|
||
background: rgba(100, 200, 255, 0.2);
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.panel-section {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.panel-section-title {
|
||
font-size: 11px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 1.5px;
|
||
color: rgba(100, 200, 255, 0.6);
|
||
margin-bottom: 10px;
|
||
padding-bottom: 4px;
|
||
border-bottom: 1px solid rgba(100, 200, 255, 0.1);
|
||
}
|
||
|
||
/* 视图切换按钮组 */
|
||
.view-btn-group {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 6px;
|
||
}
|
||
|
||
.view-btn {
|
||
background: rgba(100, 200, 255, 0.08);
|
||
border: 1px solid rgba(100, 200, 255, 0.15);
|
||
border-radius: 8px;
|
||
color: rgba(200, 220, 240, 0.8);
|
||
padding: 8px 6px;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
text-align: center;
|
||
user-select: none;
|
||
}
|
||
|
||
.view-btn:hover {
|
||
background: rgba(100, 200, 255, 0.15);
|
||
border-color: rgba(100, 200, 255, 0.3);
|
||
color: #fff;
|
||
}
|
||
|
||
.view-btn.active {
|
||
background: rgba(100, 200, 255, 0.25);
|
||
border-color: rgba(100, 200, 255, 0.5);
|
||
color: #fff;
|
||
box-shadow: 0 0 12px rgba(100, 200, 255, 0.2);
|
||
}
|
||
|
||
.view-btn .icon {
|
||
display: block;
|
||
font-size: 18px;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
/* 滑块控件 */
|
||
.slider-group {
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.slider-label {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-size: 12px;
|
||
color: rgba(200, 220, 240, 0.7);
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.slider-value {
|
||
color: rgba(100, 200, 255, 0.8);
|
||
font-family: monospace;
|
||
}
|
||
|
||
input[type="range"] {
|
||
width: 100%;
|
||
height: 4px;
|
||
-webkit-appearance: none;
|
||
appearance: none;
|
||
background: rgba(100, 200, 255, 0.1);
|
||
border-radius: 2px;
|
||
outline: none;
|
||
}
|
||
|
||
input[type="range"]::-webkit-slider-thumb {
|
||
-webkit-appearance: none;
|
||
appearance: none;
|
||
width: 14px;
|
||
height: 14px;
|
||
border-radius: 50%;
|
||
background: rgba(100, 200, 255, 0.7);
|
||
cursor: pointer;
|
||
box-shadow: 0 0 8px rgba(100, 200, 255, 0.4);
|
||
transition: transform 0.15s ease;
|
||
}
|
||
|
||
input[type="range"]::-webkit-slider-thumb:hover {
|
||
transform: scale(1.2);
|
||
background: rgba(100, 200, 255, 0.9);
|
||
}
|
||
|
||
/* 开关控件 */
|
||
.toggle-group {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
}
|
||
|
||
.toggle-btn {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: 16px;
|
||
color: rgba(200, 200, 200, 0.6);
|
||
padding: 4px 10px;
|
||
font-size: 11px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
user-select: none;
|
||
}
|
||
|
||
.toggle-btn.on {
|
||
background: rgba(100, 200, 255, 0.15);
|
||
border-color: rgba(100, 200, 255, 0.3);
|
||
color: rgba(100, 200, 255, 0.9);
|
||
}
|
||
|
||
/* 搜索框 */
|
||
#search-box {
|
||
width: 100%;
|
||
background: rgba(0, 0, 0, 0.3);
|
||
border: 1px solid rgba(100, 200, 255, 0.15);
|
||
border-radius: 8px;
|
||
padding: 8px 12px;
|
||
color: #e0e0e0;
|
||
font-size: 13px;
|
||
outline: none;
|
||
transition: border-color 0.2s ease;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
#search-box:focus {
|
||
border-color: rgba(100, 200, 255, 0.4);
|
||
box-shadow: 0 0 12px rgba(100, 200, 255, 0.1);
|
||
}
|
||
|
||
#search-box::placeholder {
|
||
color: rgba(200, 200, 200, 0.3);
|
||
}
|
||
|
||
/* 时间轴 */
|
||
#timeline-container {
|
||
margin-top: 8px;
|
||
}
|
||
|
||
#timeline {
|
||
width: 100%;
|
||
height: 4px;
|
||
position: relative;
|
||
background: rgba(100, 200, 255, 0.1);
|
||
border-radius: 2px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
#timeline-progress {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, rgba(100, 200, 255, 0.5), rgba(200, 100, 255, 0.5));
|
||
border-radius: 2px;
|
||
width: 0%;
|
||
transition: width 0.1s linear;
|
||
}
|
||
|
||
#timeline-labels {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-top: 4px;
|
||
font-size: 10px;
|
||
color: rgba(200, 200, 200, 0.4);
|
||
}
|
||
|
||
/* ========== 信息悬浮框 ========== */
|
||
#tooltip {
|
||
position: absolute;
|
||
display: none;
|
||
max-width: 320px;
|
||
background: rgba(10, 20, 40, 0.92);
|
||
border: 1px solid rgba(100, 200, 255, 0.25);
|
||
border-radius: 12px;
|
||
padding: 14px 18px;
|
||
z-index: 200;
|
||
backdrop-filter: blur(12px);
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||
pointer-events: none;
|
||
transition: opacity 0.15s ease;
|
||
}
|
||
|
||
#tooltip.visible {
|
||
display: block;
|
||
}
|
||
|
||
#tooltip-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: rgba(100, 200, 255, 0.9);
|
||
margin-bottom: 8px;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
#tooltip-content {
|
||
font-size: 12px;
|
||
color: rgba(200, 220, 240, 0.75);
|
||
line-height: 1.6;
|
||
}
|
||
|
||
#tooltip-content .tag {
|
||
display: inline-block;
|
||
background: rgba(100, 200, 255, 0.1);
|
||
border-radius: 4px;
|
||
padding: 1px 6px;
|
||
margin: 2px 2px 2px 0;
|
||
font-size: 10px;
|
||
color: rgba(100, 200, 255, 0.7);
|
||
}
|
||
|
||
/* ========== 右键菜单 ========== */
|
||
#context-menu {
|
||
position: absolute;
|
||
display: none;
|
||
min-width: 160px;
|
||
background: rgba(15, 25, 45, 0.95);
|
||
border: 1px solid rgba(100, 200, 255, 0.2);
|
||
border-radius: 10px;
|
||
padding: 6px 0;
|
||
z-index: 300;
|
||
backdrop-filter: blur(12px);
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
|
||
}
|
||
|
||
#context-menu.visible {
|
||
display: block;
|
||
}
|
||
|
||
.ctx-menu-item {
|
||
padding: 8px 16px;
|
||
font-size: 13px;
|
||
color: rgba(200, 220, 240, 0.8);
|
||
cursor: pointer;
|
||
transition: background 0.15s ease;
|
||
user-select: none;
|
||
}
|
||
|
||
.ctx-menu-item:hover {
|
||
background: rgba(100, 200, 255, 0.12);
|
||
color: #fff;
|
||
}
|
||
|
||
.ctx-menu-divider {
|
||
height: 1px;
|
||
background: rgba(100, 200, 255, 0.1);
|
||
margin: 4px 0;
|
||
}
|
||
|
||
/* ========== 统计面板(底部) ========== */
|
||
#stats-bar {
|
||
position: absolute;
|
||
bottom: 12px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
display: flex;
|
||
gap: 24px;
|
||
padding: 8px 20px;
|
||
background: rgba(10, 15, 30, 0.7);
|
||
border: 1px solid rgba(100, 200, 255, 0.1);
|
||
border-radius: 20px;
|
||
z-index: 100;
|
||
backdrop-filter: blur(8px);
|
||
}
|
||
|
||
.stat-item {
|
||
text-align: center;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: rgba(100, 200, 255, 0.9);
|
||
font-family: monospace;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 9px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 1px;
|
||
color: rgba(200, 200, 200, 0.4);
|
||
margin-top: 2px;
|
||
}
|
||
|
||
/* ========== 快捷键提示 ========== */
|
||
#shortcut-hint {
|
||
position: absolute;
|
||
bottom: 60px;
|
||
right: 12px;
|
||
font-size: 10px;
|
||
color: rgba(200, 200, 200, 0.3);
|
||
z-index: 100;
|
||
text-align: right;
|
||
line-height: 1.8;
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* ========== 切换面板按钮 ========== */
|
||
#toggle-panel-btn {
|
||
position: absolute;
|
||
top: 60px;
|
||
left: 12px;
|
||
width: 36px;
|
||
height: 36px;
|
||
background: rgba(10, 15, 30, 0.8);
|
||
border: 1px solid rgba(100, 200, 255, 0.2);
|
||
border-radius: 10px;
|
||
color: rgba(100, 200, 255, 0.7);
|
||
font-size: 18px;
|
||
cursor: pointer;
|
||
z-index: 101;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.2s ease;
|
||
backdrop-filter: blur(8px);
|
||
}
|
||
|
||
#toggle-panel-btn:hover {
|
||
background: rgba(10, 15, 30, 0.95);
|
||
border-color: rgba(100, 200, 255, 0.4);
|
||
color: rgba(100, 200, 255, 1);
|
||
}
|
||
|
||
/* ========== 加载画面 ========== */
|
||
#loading-screen {
|
||
position: absolute;
|
||
inset: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: radial-gradient(ellipse at center, #0a1428 0%, #000 100%);
|
||
z-index: 1000;
|
||
transition: opacity 0.8s ease;
|
||
}
|
||
|
||
#loading-screen.fade-out {
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.loading-ring {
|
||
width: 60px;
|
||
height: 60px;
|
||
border: 2px solid rgba(100, 200, 255, 0.1);
|
||
border-top: 2px solid rgba(100, 200, 255, 0.8);
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
.loading-text {
|
||
margin-top: 16px;
|
||
font-size: 13px;
|
||
letter-spacing: 3px;
|
||
color: rgba(100, 200, 255, 0.6);
|
||
animation: pulse-text 2s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes pulse-text {
|
||
0%, 100% { opacity: 0.4; }
|
||
50% { opacity: 1; }
|
||
}
|
||
</style>
|
||
|
||
<!-- 加载画面 -->
|
||
<div id="loading-screen">
|
||
<div class="loading-ring"></div>
|
||
<div class="loading-text">初始化意识空间...</div>
|
||
</div>
|
||
|
||
<!-- 3D 画布容器 -->
|
||
<div id="canvas-container"></div>
|
||
|
||
<!-- 标题栏 -->
|
||
<div id="title-bar">
|
||
<h1>CONSCIOUSNESS 3D SPACE</h1>
|
||
<span id="fps-counter">-- FPS</span>
|
||
</div>
|
||
|
||
<!-- 切换面板按钮 -->
|
||
<button id="toggle-panel-btn" title="切换控制面板 (H)">☰</button>
|
||
|
||
<!-- 控制面板 -->
|
||
<div id="control-panel">
|
||
<!-- 搜索框 -->
|
||
<div class="panel-section">
|
||
<input type="text" id="search-box" placeholder="搜索元素..." />
|
||
</div>
|
||
|
||
<!-- 视图切换 -->
|
||
<div class="panel-section">
|
||
<div class="panel-section-title">视图切换</div>
|
||
<div class="view-btn-group">
|
||
<div class="view-btn active" data-view="all">
|
||
<span class="icon">◉</span>全景
|
||
</div>
|
||
<div class="view-btn" data-view="memory">
|
||
<span class="icon">✦</span>记忆
|
||
</div>
|
||
<div class="view-btn" data-view="knowledge">
|
||
<span class="icon">◆</span>知识
|
||
</div>
|
||
<div class="view-btn" data-view="goals">
|
||
<span class="icon">♦</span>目标
|
||
</div>
|
||
<div class="view-btn" data-view="skills">
|
||
<span class="icon">◯</span>技能
|
||
</div>
|
||
<div class="view-btn" data-view="emotions">
|
||
<span class="icon">✿</span>情绪
|
||
</div>
|
||
<div class="view-btn" data-view="social">
|
||
<span class="icon">☺</span>社交
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 动画控制 -->
|
||
<div class="panel-section">
|
||
<div class="panel-section-title">动画控制</div>
|
||
<div class="slider-group">
|
||
<div class="slider-label">
|
||
<span>动画速度</span>
|
||
<span class="slider-value" id="speed-value">1.0x</span>
|
||
</div>
|
||
<input type="range" id="speed-slider" min="0" max="3" step="0.1" value="1" />
|
||
</div>
|
||
<div class="slider-group">
|
||
<div class="slider-label">
|
||
<span>粒子密度</span>
|
||
<span class="slider-value" id="density-value">100%</span>
|
||
</div>
|
||
<input type="range" id="density-slider" min="10" max="200" step="10" value="100" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 元素显示/隐藏 -->
|
||
<div class="panel-section">
|
||
<div class="panel-section-title">元素可见性</div>
|
||
<div class="toggle-group">
|
||
<div class="toggle-btn on" data-layer="memory">记忆星云</div>
|
||
<div class="toggle-btn on" data-layer="knowledge">知识图谱</div>
|
||
<div class="toggle-btn on" data-layer="goals">目标晶体</div>
|
||
<div class="toggle-btn on" data-layer="skills">技能光环</div>
|
||
<div class="toggle-btn on" data-layer="emotions">情绪粒子</div>
|
||
<div class="toggle-btn on" data-layer="mirror">反思之镜</div>
|
||
<div class="toggle-btn on" data-layer="social">社交网络</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 时间轴 -->
|
||
<div class="panel-section">
|
||
<div class="panel-section-title">时间回放</div>
|
||
<div id="timeline-container">
|
||
<div id="timeline">
|
||
<div id="timeline-progress"></div>
|
||
</div>
|
||
<div id="timeline-labels">
|
||
<span>过去</span>
|
||
<span>现在</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 信息悬浮框 -->
|
||
<div id="tooltip">
|
||
<div id="tooltip-title"></div>
|
||
<div id="tooltip-content"></div>
|
||
</div>
|
||
|
||
<!-- 右键菜单 -->
|
||
<div id="context-menu">
|
||
<div class="ctx-menu-item" data-action="focus">聚焦此处</div>
|
||
<div class="ctx-menu-item" data-action="highlight">高亮同类</div>
|
||
<div class="ctx-menu-divider"></div>
|
||
<div class="ctx-menu-item" data-action="hide">隐藏元素</div>
|
||
<div class="ctx-menu-item" data-action="isolate">隔离此类型</div>
|
||
<div class="ctx-menu-divider"></div>
|
||
<div class="ctx-menu-item" data-action="details">查看详情</div>
|
||
</div>
|
||
|
||
<!-- 底部统计栏 -->
|
||
<div id="stats-bar">
|
||
<div class="stat-item">
|
||
<div class="stat-value" id="stat-memories">0</div>
|
||
<div class="stat-label">记忆</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-value" id="stat-entities">0</div>
|
||
<div class="stat-label">实体</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-value" id="stat-goals">0</div>
|
||
<div class="stat-label">目标</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-value" id="stat-skills">0</div>
|
||
<div class="stat-label">技能</div>
|
||
</div>
|
||
<div class="stat-item">
|
||
<div class="stat-value" id="stat-connections">0</div>
|
||
<div class="stat-label">连接</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 快捷键提示 -->
|
||
<div id="shortcut-hint">
|
||
1-7 切换视图 | H 面板 | R 重置 | 空格 暂停
|
||
</div>
|
||
`;
|
||
|
||
/* =========================================================================
|
||
* 主组件类
|
||
* ========================================================================= */
|
||
|
||
class Consciousness3DSpace extends HTMLElement {
|
||
/** 组件标签名 */
|
||
static get observedAttributes() {
|
||
return ['width', 'height', 'data-url'];
|
||
}
|
||
|
||
constructor() {
|
||
super();
|
||
this.attachShadow({ mode: 'open' });
|
||
|
||
/* ---- 内部状态 ---- */
|
||
this._initialized = false;
|
||
this._animationId = null;
|
||
this._clock = null;
|
||
this._paused = false;
|
||
this._animationSpeed = 1.0;
|
||
this._currentView = VIEW_MODES.ALL;
|
||
this._searchQuery = '';
|
||
this._selectedObject = null;
|
||
this._hoveredObject = null;
|
||
this._contextTarget = null;
|
||
|
||
/* ---- Three.js 核心 ---- */
|
||
this._renderer = null;
|
||
this._scene = null;
|
||
this._camera = null;
|
||
this._raycaster = null;
|
||
this._mouse = new THREE.Vector2();
|
||
this._orbitControls = null;
|
||
|
||
/* ---- 可视化层 ---- */
|
||
this._layers = {
|
||
memory: null, // Group
|
||
knowledge: null,
|
||
goals: null,
|
||
skills: null,
|
||
emotions: null,
|
||
mirror: null,
|
||
social: null,
|
||
};
|
||
|
||
/* ---- 数据引用 ---- */
|
||
this._data = {
|
||
memories: [],
|
||
entities: [],
|
||
relations: [],
|
||
goals: [],
|
||
skills: [],
|
||
emotions: [],
|
||
users: [],
|
||
connections: [],
|
||
};
|
||
|
||
/* ---- 力导向布局引擎 ---- */
|
||
this._forceLayout = new ForceLayout3D();
|
||
|
||
/* ---- 爆炸特效列表 ---- */
|
||
this._explosions = [];
|
||
|
||
/* ---- FPS 计算 ---- */
|
||
this._frameCount = 0;
|
||
this._lastFpsTime = 0;
|
||
|
||
/* ---- 时间轴 ---- */
|
||
this._timelineSnapshots = [];
|
||
this._timelineIndex = 0;
|
||
|
||
/* ---- 可交互对象缓存 ---- */
|
||
this._interactableObjects = [];
|
||
|
||
/* ---- 绑定事件处理 ---- */
|
||
this._onResize = this._onResize.bind(this);
|
||
this._onClick = this._onClick.bind(this);
|
||
this._onMouseMove = this._onMouseMove.bind(this);
|
||
this._onContextMenu = this._onContextMenu.bind(this);
|
||
this._onKeyDown = this._onKeyDown.bind(this);
|
||
this._animate = this._animate.bind(this);
|
||
}
|
||
|
||
/* =========================================================================
|
||
* Web Component 生命周期
|
||
* ========================================================================= */
|
||
|
||
connectedCallback() {
|
||
// 注入 Shadow DOM 内容
|
||
this.shadowRoot.innerHTML = COMPONENT_TEMPLATE;
|
||
this._init();
|
||
}
|
||
|
||
disconnectedCallback() {
|
||
this._dispose();
|
||
window.removeEventListener('resize', this._onResize);
|
||
window.removeEventListener('keydown', this._onKeyDown);
|
||
}
|
||
|
||
attributeChangedCallback(name, oldVal, newVal) {
|
||
if (oldVal === newVal) return;
|
||
if (name === 'data-url' && this._initialized) {
|
||
this._fetchData(newVal);
|
||
}
|
||
}
|
||
|
||
/* =========================================================================
|
||
* 初始化
|
||
* ========================================================================= */
|
||
|
||
_init() {
|
||
// 加载 Three.js 依赖
|
||
this._loadDependencies()
|
||
.then(() => {
|
||
this._initThreeJS();
|
||
this._initControls();
|
||
this._initUIBindings();
|
||
this._initSceneContent();
|
||
this._initLights();
|
||
this._generateDemoData();
|
||
this._populateVisualization();
|
||
this._startAnimation();
|
||
this._hideLoadingScreen();
|
||
this._initialized = true;
|
||
})
|
||
.catch(err => {
|
||
console.error('[Consciousness3DSpace] 初始化失败:', err);
|
||
this._hideLoadingScreen();
|
||
});
|
||
}
|
||
|
||
/** 动态加载 Three.js 依赖 */
|
||
async _loadDependencies() {
|
||
const cdnBase = 'https://unpkg.com/three@0.160.0/build/';
|
||
|
||
// 检查 THREE 是否已存在
|
||
if (typeof THREE !== 'undefined') return;
|
||
|
||
// 加载 Three.js 核心
|
||
await this._loadScript(cdnBase + 'three.module.js', 'module');
|
||
|
||
// 如果 CDN 模块加载失败,尝试全局 UMD 版本
|
||
if (typeof THREE === 'undefined') {
|
||
await this._loadScript('https://unpkg.com/three@0.160.0/build/three.min.js');
|
||
}
|
||
|
||
// 加载 OrbitControls
|
||
if (typeof THREE === 'undefined') {
|
||
throw new Error('Three.js 加载失败,请检查网络连接');
|
||
}
|
||
|
||
// 内联实现简易 OrbitControls(避免 CDN 加载问题)
|
||
this._createSimpleOrbitControls();
|
||
}
|
||
|
||
/** 动态脚本加载器 */
|
||
_loadScript(url, type) {
|
||
return new Promise((resolve, reject) => {
|
||
// 避免重复加载
|
||
const existing = document.querySelector(`script[src="${url}"]`);
|
||
if (existing) {
|
||
existing.addEventListener('load', resolve);
|
||
if (existing.dataset.loaded) resolve();
|
||
return;
|
||
}
|
||
|
||
const script = document.createElement('script');
|
||
script.src = url;
|
||
if (type === 'module') {
|
||
script.type = 'module';
|
||
}
|
||
script.onload = () => {
|
||
script.dataset.loaded = 'true';
|
||
resolve();
|
||
};
|
||
script.onerror = () => reject(new Error(`无法加载脚本: ${url}`));
|
||
document.head.appendChild(script);
|
||
});
|
||
}
|
||
|
||
/** 创建简易的轨道控制器(不依赖外部模块) */
|
||
_createSimpleOrbitControls() {
|
||
const self = this;
|
||
const canvas = this._renderer.domElement;
|
||
|
||
let isDown = false;
|
||
let isRight = false;
|
||
let prevX = 0, prevY = 0;
|
||
let theta = 0, phi = Math.PI / 3;
|
||
let radius = 50;
|
||
let target = new THREE.Vector3(0, 0, 0);
|
||
|
||
function updateCamera() {
|
||
const x = radius * Math.sin(phi) * Math.cos(theta);
|
||
const y = radius * Math.cos(phi);
|
||
const z = radius * Math.sin(phi) * Math.sin(theta);
|
||
self._camera.position.set(target.x + x, target.y + y, target.z + z);
|
||
self._camera.lookAt(target);
|
||
}
|
||
|
||
canvas.addEventListener('pointerdown', (e) => {
|
||
if (e.button === 2) { isRight = true; } else { isDown = true; }
|
||
prevX = e.clientX;
|
||
prevY = e.clientY;
|
||
});
|
||
|
||
canvas.addEventListener('pointermove', (e) => {
|
||
if (!isDown && !isRight) return;
|
||
const dx = e.clientX - prevX;
|
||
const dy = e.clientY - prevY;
|
||
prevX = e.clientX;
|
||
prevY = e.clientY;
|
||
|
||
if (isDown) {
|
||
theta -= dx * 0.005;
|
||
phi = Math.max(0.1, Math.min(Math.PI - 0.1, phi + dy * 0.005));
|
||
}
|
||
if (isRight) {
|
||
const right = new THREE.Vector3();
|
||
const up = new THREE.Vector3(0, 1, 0);
|
||
right.crossVectors(self._camera.getWorldDirection(new THREE.Vector3()), up).normalize();
|
||
target.add(right.multiplyScalar(-dx * 0.05));
|
||
target.y += dy * 0.05;
|
||
}
|
||
updateCamera();
|
||
});
|
||
|
||
window.addEventListener('pointerup', () => { isDown = false; isRight = false; });
|
||
|
||
canvas.addEventListener('wheel', (e) => {
|
||
e.preventDefault();
|
||
radius = Math.max(5, Math.min(200, radius + e.deltaY * 0.05));
|
||
updateCamera();
|
||
}, { passive: false });
|
||
|
||
// 提供公共方法
|
||
this._orbitControls = {
|
||
updateCamera,
|
||
setTarget: (t) => { target.copy(t); updateCamera(); },
|
||
reset: () => { theta = 0; phi = Math.PI / 3; radius = 50; target.set(0, 0, 0); updateCamera(); },
|
||
getTarget: () => target.clone(),
|
||
getRadius: () => radius,
|
||
focusOn: (pos, newRadius = 15) => {
|
||
target.copy(pos);
|
||
radius = newRadius;
|
||
updateCamera();
|
||
}
|
||
};
|
||
|
||
updateCamera();
|
||
}
|
||
|
||
/** 初始化 Three.js 核心 */
|
||
_initThreeJS() {
|
||
const container = this.shadowRoot.querySelector('#canvas-container');
|
||
|
||
// 场景
|
||
this._scene = new THREE.Scene();
|
||
this._scene.background = new THREE.Color(0x000510);
|
||
this._scene.fog = new THREE.FogExp2(0x000510, 0.008);
|
||
|
||
// 相机
|
||
this._camera = new THREE.PerspectiveCamera(
|
||
60,
|
||
container.clientWidth / container.clientHeight,
|
||
0.1,
|
||
1000
|
||
);
|
||
|
||
// 渲染器
|
||
this._renderer = new THREE.WebGLRenderer({
|
||
antialias: true,
|
||
alpha: true,
|
||
powerPreference: 'high-performance',
|
||
});
|
||
this._renderer.setSize(container.clientWidth, container.clientHeight);
|
||
this._renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||
this._renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||
this._renderer.toneMappingExposure = 1.2;
|
||
container.appendChild(this._renderer.domElement);
|
||
|
||
// 时钟
|
||
this._clock = new THREE.Clock();
|
||
|
||
// 射线投射器(用于鼠标拾取)
|
||
this._raycaster = new THREE.Raycaster();
|
||
this._raycaster.params.Points = { threshold: 0.5 };
|
||
|
||
// 事件监听
|
||
window.addEventListener('resize', this._onResize);
|
||
this._renderer.domElement.addEventListener('click', this._onClick);
|
||
this._renderer.domElement.addEventListener('pointermove', this._onMouseMove);
|
||
this._renderer.domElement.addEventListener('contextmenu', this._onContextMenu);
|
||
window.addEventListener('keydown', this._onKeyDown);
|
||
}
|
||
|
||
/** 初始化场景光源 */
|
||
_initLights() {
|
||
// 环境光 - 微弱的整体照明
|
||
const ambient = new THREE.AmbientLight(0x1a1a2e, 0.3);
|
||
this._scene.add(ambient);
|
||
|
||
// 中心点光源 - 模拟意识核心
|
||
const coreLight = new THREE.PointLight(0x4488ff, 2, 80);
|
||
coreLight.position.set(0, 0, 0);
|
||
this._scene.add(coreLight);
|
||
this._coreLight = coreLight;
|
||
|
||
// 半球光 - 天空/地面色彩
|
||
const hemiLight = new THREE.HemisphereLight(0x2244aa, 0x112211, 0.4);
|
||
this._scene.add(hemiLight);
|
||
|
||
// 背景星空粒子
|
||
this._createBackgroundStars();
|
||
}
|
||
|
||
/** 创建背景星空 */
|
||
_createBackgroundStars() {
|
||
const count = 3000;
|
||
const positions = new Float32Array(count * 3);
|
||
const sizes = new Float32Array(count);
|
||
|
||
for (let i = 0; i < count; i++) {
|
||
const i3 = i * 3;
|
||
positions[i3] = randomRange(-300, 300);
|
||
positions[i3 + 1] = randomRange(-300, 300);
|
||
positions[i3 + 2] = randomRange(-300, 300);
|
||
sizes[i] = randomRange(0.3, 1.5);
|
||
}
|
||
|
||
const geometry = new THREE.BufferGeometry();
|
||
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||
geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
|
||
|
||
// 简单的点材质
|
||
const material = new THREE.PointsMaterial({
|
||
color: 0xaaccff,
|
||
size: 0.5,
|
||
transparent: true,
|
||
opacity: 0.4,
|
||
sizeAttenuation: true,
|
||
blending: THREE.AdditiveBlending,
|
||
depthWrite: false,
|
||
});
|
||
|
||
const stars = new THREE.Points(geometry, material);
|
||
stars.frustumCulled = false;
|
||
this._scene.add(stars);
|
||
this._bgStars = stars;
|
||
}
|
||
|
||
/* =========================================================================
|
||
* 场景内容初始化
|
||
* ========================================================================= */
|
||
|
||
/** 初始化各可视化层的容器 */
|
||
_initSceneContent() {
|
||
this._layers.memory = new THREE.Group();
|
||
this._layers.memory.name = 'memory';
|
||
this._scene.add(this._layers.memory);
|
||
|
||
this._layers.knowledge = new THREE.Group();
|
||
this._layers.knowledge.name = 'knowledge';
|
||
this._scene.add(this._layers.knowledge);
|
||
|
||
this._layers.goals = new THREE.Group();
|
||
this._layers.goals.name = 'goals';
|
||
this._scene.add(this._layers.goals);
|
||
|
||
this._layers.skills = new THREE.Group();
|
||
this._layers.skills.name = 'skills';
|
||
this._scene.add(this._layers.skills);
|
||
|
||
this._layers.emotions = new THREE.Group();
|
||
this._layers.emotions.name = 'emotions';
|
||
this._scene.add(this._layers.emotions);
|
||
|
||
this._layers.mirror = new THREE.Group();
|
||
this._layers.mirror.name = 'mirror';
|
||
this._scene.add(this._layers.mirror);
|
||
|
||
this._layers.social = new THREE.Group();
|
||
this._layers.social.name = 'social';
|
||
this._scene.add(this._layers.social);
|
||
}
|
||
|
||
/* =========================================================================
|
||
* 数据生成与填充
|
||
* ========================================================================= */
|
||
|
||
/** 生成演示数据 */
|
||
_generateDemoData() {
|
||
// ---- 记忆数据 ----
|
||
const memoryTopics = [
|
||
'第一次学习编程', '与朋友的深夜对话', '读完《三体》', '解决复杂算法',
|
||
'观看日出', '收到重要邮件', '项目上线成功', '一次失败的演讲',
|
||
'与导师的交流', '创作第一首诗', '探索新城市', '帮助陌生人',
|
||
'调试通宵的bug', '收到生日祝福', '学到新知识', '做出重要决定',
|
||
'运动突破记录', '品尝新美食', '观看纪录片', '参与开源项目',
|
||
'团队庆祝活动', '独立完成设计', '阅读科学论文', '音乐会上的感动',
|
||
'冥想中的顿悟', '代码重构完成', '设计原型被认可', '数据分析发现',
|
||
'与AI对话的启发', '回顾旧日记', '清晨的跑步', '家庭视频通话',
|
||
];
|
||
|
||
this._data.memories = memoryTopics.map((title, i) => {
|
||
const importance = Math.random();
|
||
const sentiment = randomRange(-1, 1); // -1=负面, 0=中性, 1=正面
|
||
const accessCount = Math.floor(randomRange(1, 50));
|
||
// 使用简化的降维位置(模拟 t-SNE/PCA)
|
||
const angle1 = (i / memoryTopics.length) * Math.PI * 2;
|
||
const angle2 = randomRange(0, Math.PI);
|
||
const radius = randomRange(8, 25);
|
||
return {
|
||
id: `mem_${i}`,
|
||
title,
|
||
content: `关于"${title}"的详细记忆内容。时间:2024-${String(Math.floor(randomRange(1, 13))).padStart(2, '0')}-${String(Math.floor(randomRange(1, 29))).padStart(2, '0')}`,
|
||
importance,
|
||
sentiment,
|
||
accessCount,
|
||
position: new THREE.Vector3(
|
||
radius * Math.sin(angle2) * Math.cos(angle1),
|
||
radius * Math.sin(angle2) * Math.sin(angle1),
|
||
radius * Math.cos(angle2)
|
||
),
|
||
tags: ['记忆', sentiment > 0.3 ? '积极' : sentiment < -0.3 ? '消极' : '中性'],
|
||
type: 'memory',
|
||
};
|
||
});
|
||
|
||
// ---- 知识图谱数据 ----
|
||
const entityNames = [
|
||
'人工智能', '神经网络', '深度学习', '自然语言处理', '计算机视觉',
|
||
'强化学习', '注意力机制', '变换器', '知识图谱', '机器学习',
|
||
'Python', 'JavaScript', 'Three.js', '数据结构', '算法',
|
||
'物理', '数学', '哲学', '心理学', '认知科学',
|
||
];
|
||
|
||
const entityTypes = Object.keys(ENTITY_TYPE_COLORS);
|
||
this._data.entities = entityNames.map((name, i) => ({
|
||
id: `ent_${i}`,
|
||
name,
|
||
type: entityTypes[i % entityTypes.length],
|
||
connections: Math.floor(randomRange(2, 12)),
|
||
description: `"${name}"是知识体系中的重要概念节点。`,
|
||
position: new THREE.Vector3(
|
||
randomRange(-30, 30),
|
||
randomRange(-20, 20),
|
||
randomRange(-30, 30)
|
||
),
|
||
}));
|
||
|
||
// 生成关系
|
||
this._data.relations = [];
|
||
for (let i = 0; i < entityNames.length; i++) {
|
||
const numRelations = Math.floor(randomRange(1, 4));
|
||
for (let j = 0; j < numRelations; j++) {
|
||
const target = Math.floor(Math.random() * entityNames.length);
|
||
if (target !== i) {
|
||
this._data.relations.push({
|
||
source: `ent_${i}`,
|
||
target: `ent_${target}`,
|
||
weight: randomRange(0.3, 1.0),
|
||
label: '相关',
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// 初始化力导向布局
|
||
this._forceLayout.clear();
|
||
this._data.entities.forEach(e => this._forceLayout.addNode(e.id, e.position));
|
||
this._data.relations.forEach(r => this._forceLayout.addEdge(r.source, r.target, r.weight));
|
||
// 运行多次迭代使布局收敛
|
||
for (let i = 0; i < 50; i++) this._forceLayout.step();
|
||
// 更新实体位置
|
||
const positions = this._forceLayout.getPositions();
|
||
this._data.entities.forEach(e => { if (positions[e.id]) e.position.copy(positions[e.id]); });
|
||
|
||
// ---- 目标数据 ----
|
||
this._data.goals = [
|
||
{ id: 'goal_0', title: '构建完整意识系统', state: 'active', progress: 0.65, children: [
|
||
{ id: 'goal_0_0', title: '记忆模块', state: 'completed', progress: 1.0 },
|
||
{ id: 'goal_0_1', title: '知识图谱模块', state: 'active', progress: 0.8 },
|
||
{ id: 'goal_0_2', title: '情绪系统', state: 'active', progress: 0.5 },
|
||
]},
|
||
{ id: 'goal_1', title: '学习新技能', state: 'active', progress: 0.4, children: [
|
||
{ id: 'goal_1_0', title: '掌握3D可视化', state: 'active', progress: 0.7 },
|
||
{ id: 'goal_1_1', title: '学习音乐创作', state: 'paused', progress: 0.2 },
|
||
]},
|
||
{ id: 'goal_2', title: '提升社交智能', state: 'active', progress: 0.3, children: [] },
|
||
{ id: 'goal_3', title: '完成日常任务', state: 'completed', progress: 1.0, children: [] },
|
||
{ id: 'goal_4', title: '探索未知领域', state: 'paused', progress: 0.1, children: [] },
|
||
];
|
||
|
||
// ---- 技能数据 ----
|
||
this._data.skills = [
|
||
{ id: 'skill_0', name: '编程', type: 'technical', proficiency: 0.85 },
|
||
{ id: 'skill_1', name: '写作', type: 'creative', proficiency: 0.72 },
|
||
{ id: 'skill_2', name: '分析', type: 'analytical', proficiency: 0.88 },
|
||
{ id: 'skill_3', name: '沟通', type: 'social', proficiency: 0.65 },
|
||
{ id: 'skill_4', name: '设计', type: 'creative', proficiency: 0.55 },
|
||
{ id: 'skill_5', name: '数学', type: 'analytical', proficiency: 0.78 },
|
||
{ id: 'skill_6', name: '学习', type: 'default', proficiency: 0.90 },
|
||
{ id: 'skill_7', name: '推理', type: 'analytical', proficiency: 0.82 },
|
||
];
|
||
|
||
// ---- 情绪数据 ----
|
||
this._data.emotions = [
|
||
{ type: 'joy', intensity: 0.7 },
|
||
{ type: 'curiosity', intensity: 0.9 },
|
||
{ type: 'sadness', intensity: 0.15 },
|
||
{ type: 'anger', intensity: 0.05 },
|
||
];
|
||
|
||
// ---- 社交数据 ----
|
||
this._data.users = [
|
||
{ id: 'user_0', name: 'Alice', closeness: 0.9, role: '核心用户' },
|
||
{ id: 'user_1', name: 'Bob', closeness: 0.7, role: '开发者' },
|
||
{ id: 'user_2', name: 'Carol', closeness: 0.5, role: '研究员' },
|
||
{ id: 'user_3', name: 'David', closeness: 0.6, role: '设计师' },
|
||
{ id: 'user_4', name: 'Eve', closeness: 0.3, role: '访客' },
|
||
];
|
||
|
||
this._data.connections = [
|
||
{ source: 'user_0', target: 'user_1', strength: 0.8 },
|
||
{ source: 'user_0', target: 'user_2', strength: 0.5 },
|
||
{ source: 'user_1', target: 'user_3', strength: 0.6 },
|
||
{ source: 'user_2', target: 'user_4', strength: 0.3 },
|
||
];
|
||
}
|
||
|
||
/* =========================================================================
|
||
* 可视化元素构建
|
||
* ========================================================================= */
|
||
|
||
/** 填充所有可视化层 */
|
||
_populateVisualization() {
|
||
this._buildMemoryNebula();
|
||
this._buildKnowledgeGraph();
|
||
this._buildGoalCrystals();
|
||
this._buildSkillHalos();
|
||
this._buildEmotionParticles();
|
||
this._buildReflectionMirror();
|
||
this._buildSocialNetwork();
|
||
this._updateStats();
|
||
}
|
||
|
||
/* ---- 1. 记忆星云 ---- */
|
||
_buildMemoryNebula() {
|
||
const memories = this._data.memories;
|
||
const count = memories.length;
|
||
const positions = new Float32Array(count * 3);
|
||
const sizes = new Float32Array(count);
|
||
const importances = new Float32Array(count);
|
||
const colors = new Float32Array(count * 3);
|
||
|
||
for (let i = 0; i < count; i++) {
|
||
const mem = memories[i];
|
||
const i3 = i * 3;
|
||
positions[i3] = mem.position.x;
|
||
positions[i3 + 1] = mem.position.y;
|
||
positions[i3 + 2] = mem.position.z;
|
||
|
||
// 大小 = 访问次数(归一化)
|
||
sizes[i] = normalizeToRange(mem.accessCount, 1, 50, 2, 12);
|
||
importances[i] = mem.importance;
|
||
|
||
// 颜色 = 情绪效价
|
||
const c = new THREE.Color();
|
||
if (mem.sentiment < -0.3) {
|
||
// 负面 -> 蓝色系
|
||
c.setHSL(0.6, 0.8, 0.5 + mem.importance * 0.3);
|
||
} else if (mem.sentiment > 0.3) {
|
||
// 正面 -> 黄色系
|
||
c.setHSL(0.12, 0.9, 0.5 + mem.importance * 0.3);
|
||
} else {
|
||
// 中性 -> 红色系
|
||
c.setHSL(0.0, 0.7, 0.5 + mem.importance * 0.3);
|
||
}
|
||
colors[i3] = c.r;
|
||
colors[i3 + 1] = c.g;
|
||
colors[i3 + 2] = c.b;
|
||
}
|
||
|
||
const geometry = new THREE.BufferGeometry();
|
||
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||
geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
|
||
geometry.setAttribute('importance', new THREE.BufferAttribute(importances, 1));
|
||
geometry.setAttribute('customColor', new THREE.BufferAttribute(colors, 3));
|
||
|
||
const material = new THREE.ShaderMaterial({
|
||
vertexShader: STAR_VERTEX_SHADER,
|
||
fragmentShader: STAR_FRAGMENT_SHADER,
|
||
uniforms: {
|
||
time: { value: 0 },
|
||
globalBrightness: { value: 1.0 },
|
||
},
|
||
transparent: true,
|
||
blending: THREE.AdditiveBlending,
|
||
depthWrite: false,
|
||
});
|
||
|
||
const points = new THREE.Points(geometry, material);
|
||
points.name = 'memory-nebula';
|
||
points.userData.layer = 'memory';
|
||
this._layers.memory.add(points);
|
||
|
||
// 记忆间连接线(相似记忆连线)
|
||
this._buildMemoryConnections(memories);
|
||
|
||
// 记录可交互对象
|
||
for (let i = 0; i < count; i++) {
|
||
this._interactableObjects.push({
|
||
type: 'memory',
|
||
index: i,
|
||
data: memories[i],
|
||
layer: 'memory',
|
||
object: points,
|
||
});
|
||
}
|
||
|
||
// 记忆星云中心辉光
|
||
const glowGeo = new THREE.SphereGeometry(2, 32, 32);
|
||
const glowMat = new THREE.MeshBasicMaterial({
|
||
color: 0x4488ff,
|
||
transparent: true,
|
||
opacity: 0.15,
|
||
blending: THREE.AdditiveBlending,
|
||
});
|
||
const glow = new THREE.Mesh(glowGeo, glowMat);
|
||
glow.name = 'memory-core-glow';
|
||
this._layers.memory.add(glow);
|
||
}
|
||
|
||
/** 构建记忆间的连接线 */
|
||
_buildMemoryConnections(memories) {
|
||
const linePositions = [];
|
||
const maxConnections = 40;
|
||
let count = 0;
|
||
|
||
for (let i = 0; i < memories.length && count < maxConnections; i++) {
|
||
for (let j = i + 1; j < memories.length && count < maxConnections; j++) {
|
||
const dist = memories[i].position.distanceTo(memories[j].position);
|
||
if (dist < 12) {
|
||
linePositions.push(
|
||
memories[i].position.x, memories[i].position.y, memories[i].position.z,
|
||
memories[j].position.x, memories[j].position.y, memories[j].position.z
|
||
);
|
||
count++;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (linePositions.length > 0) {
|
||
const geo = new THREE.BufferGeometry();
|
||
geo.setAttribute('position', new THREE.Float32BufferAttribute(linePositions, 3));
|
||
const mat = new THREE.LineBasicMaterial({
|
||
color: 0x4488ff,
|
||
transparent: true,
|
||
opacity: 0.08,
|
||
blending: THREE.AdditiveBlending,
|
||
depthWrite: false,
|
||
});
|
||
const lines = new THREE.LineSegments(geo, mat);
|
||
lines.name = 'memory-connections';
|
||
this._layers.memory.add(lines);
|
||
}
|
||
}
|
||
|
||
/* ---- 2. 知识图谱 ---- */
|
||
_buildKnowledgeGraph() {
|
||
const entities = this._data.entities;
|
||
const relations = this._data.relations;
|
||
const layer = this._layers.knowledge;
|
||
|
||
// 实体球体
|
||
for (let i = 0; i < entities.length; i++) {
|
||
const ent = entities[i];
|
||
const radius = 0.3 + ent.connections * 0.15;
|
||
const color = ENTITY_TYPE_COLORS[ent.type] || ENTITY_TYPE_COLORS.default;
|
||
|
||
// 发光球体
|
||
const geo = new THREE.SphereGeometry(radius, 24, 24);
|
||
const mat = new THREE.MeshPhongMaterial({
|
||
color,
|
||
emissive: color,
|
||
emissiveIntensity: 0.4,
|
||
transparent: true,
|
||
opacity: 0.85,
|
||
shininess: 80,
|
||
});
|
||
const mesh = new THREE.Mesh(geo, mat);
|
||
mesh.position.copy(ent.position);
|
||
mesh.name = ent.id;
|
||
mesh.userData = { type: 'entity', index: i, data: ent, layer: 'knowledge' };
|
||
layer.add(mesh);
|
||
|
||
// 外部辉光环
|
||
const glowGeo = new THREE.SphereGeometry(radius * 1.6, 16, 16);
|
||
const glowMat = new THREE.MeshBasicMaterial({
|
||
color,
|
||
transparent: true,
|
||
opacity: 0.08,
|
||
blending: THREE.AdditiveBlending,
|
||
side: THREE.BackSide,
|
||
});
|
||
const glowMesh = new THREE.Mesh(glowGeo, glowMat);
|
||
glowMesh.position.copy(ent.position);
|
||
layer.add(glowMesh);
|
||
|
||
this._interactableObjects.push({
|
||
type: 'entity',
|
||
index: i,
|
||
data: ent,
|
||
layer: 'knowledge',
|
||
object: mesh,
|
||
});
|
||
}
|
||
|
||
// 关系连线
|
||
const linePositions = [];
|
||
const lineColors = [];
|
||
for (const rel of relations) {
|
||
const src = entities.find(e => e.id === rel.source);
|
||
const tgt = entities.find(e => e.id === rel.target);
|
||
if (!src || !tgt) continue;
|
||
|
||
linePositions.push(src.position.x, src.position.y, src.position.z);
|
||
linePositions.push(tgt.position.x, tgt.position.y, tgt.position.z);
|
||
|
||
const c1 = new THREE.Color(ENTITY_TYPE_COLORS[src.type] || 0xffffff);
|
||
const c2 = new THREE.Color(ENTITY_TYPE_COLORS[tgt.type] || 0xffffff);
|
||
lineColors.push(c1.r, c1.g, c1.b);
|
||
lineColors.push(c2.r, c2.g, c2.b);
|
||
}
|
||
|
||
if (linePositions.length > 0) {
|
||
const geo = new THREE.BufferGeometry();
|
||
geo.setAttribute('position', new THREE.Float32BufferAttribute(linePositions, 3));
|
||
geo.setAttribute('color', new THREE.Float32BufferAttribute(lineColors, 3));
|
||
const mat = new THREE.LineBasicMaterial({
|
||
vertexColors: true,
|
||
transparent: true,
|
||
opacity: 0.25,
|
||
blending: THREE.AdditiveBlending,
|
||
depthWrite: false,
|
||
});
|
||
const lines = new THREE.LineSegments(geo, mat);
|
||
lines.name = 'knowledge-relations';
|
||
layer.add(lines);
|
||
}
|
||
}
|
||
|
||
/* ---- 3. 目标晶体 ---- */
|
||
_buildGoalCrystals() {
|
||
const goals = this._data.goals;
|
||
const layer = this._layers.goals;
|
||
const self = this;
|
||
|
||
goals.forEach((goal, gi) => {
|
||
// 父目标位置 - 环形分布
|
||
const parentAngle = (gi / goals.length) * Math.PI * 2;
|
||
const parentRadius = 15;
|
||
const parentPos = new THREE.Vector3(
|
||
Math.cos(parentAngle) * parentRadius,
|
||
randomRange(-3, 3),
|
||
Math.sin(parentAngle) * parentRadius
|
||
);
|
||
|
||
self._createCrystalMesh(goal, parentPos, layer, gi, 1.5);
|
||
|
||
// 子目标围绕父目标旋转
|
||
if (goal.children) {
|
||
goal.children.forEach((child, ci) => {
|
||
const childAngle = (ci / goal.children.length) * Math.PI * 2;
|
||
const childRadius = 3;
|
||
const childPos = new THREE.Vector3(
|
||
parentPos.x + Math.cos(childAngle) * childRadius,
|
||
parentPos.y + randomRange(-1, 1),
|
||
parentPos.z + Math.sin(childAngle) * childRadius
|
||
);
|
||
self._createCrystalMesh(child, childPos, layer, gi * 100 + ci, 0.8, parentPos);
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
/** 创建单个晶体网格 */
|
||
_createCrystalMesh(goal, position, layer, index, scale = 1.0, orbitCenter = null) {
|
||
const color = GOAL_STATE_COLORS[goal.state] || 0x888888;
|
||
|
||
// 根据进度选择多面体
|
||
let geometry;
|
||
if (goal.progress >= 0.8) {
|
||
geometry = new THREE.IcosahedronGeometry(scale, 0);
|
||
} else {
|
||
geometry = new THREE.DodecahedronGeometry(scale, 0);
|
||
}
|
||
|
||
const material = new THREE.MeshPhongMaterial({
|
||
color,
|
||
emissive: color,
|
||
emissiveIntensity: 0.3,
|
||
transparent: true,
|
||
opacity: 0.3 + goal.progress * 0.6,
|
||
shininess: 120,
|
||
side: THREE.DoubleSide,
|
||
wireframe: false,
|
||
});
|
||
|
||
const mesh = new THREE.Mesh(geometry, material);
|
||
mesh.position.copy(position);
|
||
mesh.name = goal.id;
|
||
mesh.userData = {
|
||
type: 'goal',
|
||
index,
|
||
data: goal,
|
||
layer: 'goals',
|
||
orbitCenter,
|
||
orbitAngle: Math.random() * Math.PI * 2,
|
||
orbitSpeed: 0.2 + Math.random() * 0.3,
|
||
rotationSpeed: new THREE.Vector3(
|
||
randomRange(-0.5, 0.5),
|
||
randomRange(-0.5, 0.5),
|
||
randomRange(-0.5, 0.5)
|
||
),
|
||
};
|
||
layer.add(mesh);
|
||
|
||
// 晶体边框线
|
||
const wireGeo = new THREE.EdgesGeometry(geometry);
|
||
const wireMat = new THREE.LineBasicMaterial({
|
||
color,
|
||
transparent: true,
|
||
opacity: 0.4,
|
||
});
|
||
const wireframe = new THREE.LineSegments(wireGeo, wireMat);
|
||
mesh.add(wireframe);
|
||
|
||
// 添加光晕
|
||
const glowGeo = new THREE.SphereGeometry(scale * 1.5, 12, 12);
|
||
const glowMat = new THREE.MeshBasicMaterial({
|
||
color,
|
||
transparent: true,
|
||
opacity: 0.06,
|
||
blending: THREE.AdditiveBlending,
|
||
side: THREE.BackSide,
|
||
});
|
||
const glow = new THREE.Mesh(glowGeo, glowMat);
|
||
mesh.add(glow);
|
||
|
||
this._interactableObjects.push({
|
||
type: 'goal',
|
||
index,
|
||
data: goal,
|
||
layer: 'goals',
|
||
object: mesh,
|
||
});
|
||
|
||
return mesh;
|
||
}
|
||
|
||
/* ---- 4. 技能光环 ---- */
|
||
_buildSkillHalos() {
|
||
const skills = this._data.skills;
|
||
const layer = this._layers.skills;
|
||
|
||
skills.forEach((skill, i) => {
|
||
const angle = (i / skills.length) * Math.PI * 2;
|
||
const ringRadius = 12;
|
||
const pos = new THREE.Vector3(
|
||
Math.cos(angle) * ringRadius,
|
||
randomRange(-2, 2),
|
||
Math.sin(angle) * ringRadius
|
||
);
|
||
|
||
// 熟练度 -> 圆环半径
|
||
const torusRadius = 0.8 + skill.proficiency * 1.5;
|
||
const tubeRadius = 0.05 + skill.proficiency * 0.12;
|
||
const color = SKILL_TYPE_COLORS[skill.type] || SKILL_TYPE_COLORS.default;
|
||
|
||
const geo = new THREE.TorusGeometry(torusRadius, tubeRadius, 16, 48);
|
||
const mat = new THREE.MeshPhongMaterial({
|
||
color,
|
||
emissive: color,
|
||
emissiveIntensity: 0.5,
|
||
transparent: true,
|
||
opacity: 0.7,
|
||
shininess: 100,
|
||
});
|
||
|
||
const mesh = new THREE.Mesh(geo, mat);
|
||
mesh.position.copy(pos);
|
||
mesh.rotation.x = Math.PI / 2 + randomRange(-0.3, 0.3);
|
||
mesh.name = skill.id;
|
||
mesh.userData = {
|
||
type: 'skill',
|
||
index: i,
|
||
data: skill,
|
||
layer: 'skills',
|
||
pulsePhase: Math.random() * Math.PI * 2,
|
||
isPulsing: false,
|
||
baseEmissiveIntensity: 0.5,
|
||
};
|
||
layer.add(mesh);
|
||
|
||
// 外部辉光
|
||
const glowGeo = new THREE.TorusGeometry(torusRadius, tubeRadius * 3, 16, 48);
|
||
const glowMat = new THREE.MeshBasicMaterial({
|
||
color,
|
||
transparent: true,
|
||
opacity: 0.08,
|
||
blending: THREE.AdditiveBlending,
|
||
side: THREE.DoubleSide,
|
||
});
|
||
const glow = new THREE.Mesh(glowGeo, glowMat);
|
||
mesh.add(glow);
|
||
|
||
this._interactableObjects.push({
|
||
type: 'skill',
|
||
index: i,
|
||
data: skill,
|
||
layer: 'skills',
|
||
object: mesh,
|
||
});
|
||
});
|
||
}
|
||
|
||
/* ---- 5. 情绪粒子 ---- */
|
||
_buildEmotionParticles() {
|
||
const emotions = this._data.emotions;
|
||
const layer = this._layers.emotions;
|
||
|
||
emotions.forEach(emotion => {
|
||
const particleCount = Math.floor(emotion.intensity * 300);
|
||
if (particleCount < 5) return;
|
||
|
||
const positions = new Float32Array(particleCount * 3);
|
||
const sizes = new Float32Array(particleCount);
|
||
const alphas = new Float32Array(particleCount);
|
||
const colors = new Float32Array(particleCount * 3);
|
||
|
||
const baseColor = new THREE.Color(EMOTION_COLORS[emotion.type] || 0xffffff);
|
||
|
||
for (let i = 0; i < particleCount; i++) {
|
||
const i3 = i * 3;
|
||
positions[i3] = randomRange(-20, 20);
|
||
positions[i3 + 1] = randomRange(-15, 15);
|
||
positions[i3 + 2] = randomRange(-20, 20);
|
||
sizes[i] = randomRange(1, 4);
|
||
alphas[i] = randomRange(0.3, 0.9);
|
||
colors[i3] = baseColor.r + randomRange(-0.05, 0.05);
|
||
colors[i3 + 1] = baseColor.g + randomRange(-0.05, 0.05);
|
||
colors[i3 + 2] = baseColor.b + randomRange(-0.05, 0.05);
|
||
}
|
||
|
||
const geo = new THREE.BufferGeometry();
|
||
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||
geo.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
|
||
geo.setAttribute('alpha', new THREE.BufferAttribute(alphas, 1));
|
||
geo.setAttribute('customColor', new THREE.BufferAttribute(colors, 3));
|
||
|
||
const mat = new THREE.ShaderMaterial({
|
||
vertexShader: PARTICLE_VERTEX_SHADER,
|
||
fragmentShader: PARTICLE_FRAGMENT_SHADER,
|
||
transparent: true,
|
||
blending: THREE.AdditiveBlending,
|
||
depthWrite: false,
|
||
});
|
||
|
||
const points = new THREE.Points(geo, mat);
|
||
points.name = `emotion-${emotion.type}`;
|
||
points.userData = {
|
||
layer: 'emotions',
|
||
emotionType: emotion.type,
|
||
intensity: emotion.intensity,
|
||
particleCount,
|
||
};
|
||
layer.add(points);
|
||
});
|
||
}
|
||
|
||
/* ---- 6. 反思之镜 ---- */
|
||
_buildReflectionMirror() {
|
||
const layer = this._layers.mirror;
|
||
|
||
// 主镜面平面
|
||
const mirrorGeo = new THREE.PlaneGeometry(40, 40, 64, 64);
|
||
const mirrorMat = new THREE.ShaderMaterial({
|
||
vertexShader: WATER_VERTEX_SHADER,
|
||
fragmentShader: WATER_FRAGMENT_SHADER,
|
||
uniforms: {
|
||
time: { value: 0 },
|
||
reflectivity: { value: 0.4 },
|
||
},
|
||
transparent: true,
|
||
side: THREE.DoubleSide,
|
||
depthWrite: false,
|
||
});
|
||
|
||
const mirror = new THREE.Mesh(mirrorGeo, mirrorMat);
|
||
mirror.rotation.x = -Math.PI / 2;
|
||
mirror.position.y = -12;
|
||
mirror.name = 'reflection-mirror';
|
||
mirror.userData = { layer: 'mirror' };
|
||
layer.add(mirror);
|
||
|
||
// 边框光圈
|
||
const ringGeo = new THREE.RingGeometry(19.5, 20.5, 64);
|
||
const ringMat = new THREE.MeshBasicMaterial({
|
||
color: 0x4488ff,
|
||
transparent: true,
|
||
opacity: 0.15,
|
||
side: THREE.DoubleSide,
|
||
blending: THREE.AdditiveBlending,
|
||
});
|
||
const ring = new THREE.Mesh(ringGeo, ringMat);
|
||
ring.rotation.x = -Math.PI / 2;
|
||
ring.position.y = -11.95;
|
||
layer.add(ring);
|
||
|
||
this._mirrorMaterial = mirrorMat;
|
||
}
|
||
|
||
/* ---- 7. 社交网络 ---- */
|
||
_buildSocialNetwork() {
|
||
const users = this._data.users;
|
||
const connections = this._data.connections;
|
||
const layer = this._layers.social;
|
||
const userPositions = {};
|
||
|
||
// 用户节点
|
||
users.forEach((user, i) => {
|
||
const angle = (i / users.length) * Math.PI * 2;
|
||
const r = 8;
|
||
const pos = new THREE.Vector3(
|
||
Math.cos(angle) * r,
|
||
randomRange(-3, 3) + 10,
|
||
Math.sin(angle) * r
|
||
);
|
||
userPositions[user.id] = pos;
|
||
|
||
// 人脸图标 - 用球体模拟
|
||
const sphereRadius = 0.5 + user.closeness * 0.5;
|
||
const geo = new THREE.SphereGeometry(sphereRadius, 24, 24);
|
||
const mat = new THREE.MeshPhongMaterial({
|
||
color: 0x42A5F5,
|
||
emissive: 0x1565C0,
|
||
emissiveIntensity: 0.3,
|
||
transparent: true,
|
||
opacity: 0.8,
|
||
});
|
||
const mesh = new THREE.Mesh(geo, mat);
|
||
mesh.position.copy(pos);
|
||
mesh.name = user.id;
|
||
mesh.userData = { type: 'user', index: i, data: user, layer: 'social' };
|
||
layer.add(mesh);
|
||
|
||
// 眼睛(两个小球)
|
||
const eyeGeo = new THREE.SphereGeometry(sphereRadius * 0.12, 8, 8);
|
||
const eyeMat = new THREE.MeshBasicMaterial({ color: 0xffffff });
|
||
const eyeL = new THREE.Mesh(eyeGeo, eyeMat);
|
||
eyeL.position.set(-sphereRadius * 0.3, sphereRadius * 0.15, sphereRadius * 0.85);
|
||
mesh.add(eyeL);
|
||
const eyeR = new THREE.Mesh(eyeGeo, eyeMat);
|
||
eyeR.position.set(sphereRadius * 0.3, sphereRadius * 0.15, sphereRadius * 0.85);
|
||
mesh.add(eyeR);
|
||
|
||
// 瞳孔
|
||
const pupilGeo = new THREE.SphereGeometry(sphereRadius * 0.06, 8, 8);
|
||
const pupilMat = new THREE.MeshBasicMaterial({ color: 0x000000 });
|
||
const pupilL = new THREE.Mesh(pupilGeo, pupilMat);
|
||
pupilL.position.z = sphereRadius * 0.02;
|
||
eyeL.add(pupilL);
|
||
const pupilR = new THREE.Mesh(pupilGeo, pupilMat);
|
||
pupilR.position.z = sphereRadius * 0.02;
|
||
eyeR.add(pupilR);
|
||
|
||
// 光晕
|
||
const glowGeo = new THREE.SphereGeometry(sphereRadius * 2, 12, 12);
|
||
const glowMat = new THREE.MeshBasicMaterial({
|
||
color: 0x42A5F5,
|
||
transparent: true,
|
||
opacity: 0.05,
|
||
blending: THREE.AdditiveBlending,
|
||
side: THREE.BackSide,
|
||
});
|
||
const glow = new THREE.Mesh(glowGeo, glowMat);
|
||
mesh.add(glow);
|
||
|
||
this._interactableObjects.push({
|
||
type: 'user',
|
||
index: i,
|
||
data: user,
|
||
layer: 'social',
|
||
object: mesh,
|
||
});
|
||
});
|
||
|
||
// 关系连线
|
||
const linePositions = [];
|
||
connections.forEach(conn => {
|
||
const src = userPositions[conn.source];
|
||
const tgt = userPositions[conn.target];
|
||
if (!src || !tgt) return;
|
||
|
||
linePositions.push(src.x, src.y, src.z);
|
||
linePositions.push(tgt.x, tgt.y, tgt.z);
|
||
});
|
||
|
||
if (linePositions.length > 0) {
|
||
const geo = new THREE.BufferGeometry();
|
||
geo.setAttribute('position', new THREE.Float32BufferAttribute(linePositions, 3));
|
||
const mat = new THREE.LineBasicMaterial({
|
||
color: 0x64B5F6,
|
||
transparent: true,
|
||
opacity: 0.3,
|
||
blending: THREE.AdditiveBlending,
|
||
});
|
||
const lines = new THREE.LineSegments(geo, mat);
|
||
lines.name = 'social-connections';
|
||
layer.add(lines);
|
||
}
|
||
}
|
||
|
||
/* =========================================================================
|
||
* 动画循环
|
||
* ========================================================================= */
|
||
|
||
/** 启动动画循环 */
|
||
_startAnimation() {
|
||
this._lastFpsTime = performance.now();
|
||
this._animate();
|
||
}
|
||
|
||
/** 主动画帧 */
|
||
_animate() {
|
||
this._animationId = requestAnimationFrame(this._animate);
|
||
|
||
if (this._paused) return;
|
||
|
||
const delta = this._clock.getDelta() * this._animationSpeed;
|
||
const elapsed = this._clock.elapsedTime * this._animationSpeed;
|
||
|
||
// 更新 FPS
|
||
this._frameCount++;
|
||
const now = performance.now();
|
||
if (now - this._lastFpsTime >= 1000) {
|
||
const fps = Math.round(this._frameCount * 1000 / (now - this._lastFpsTime));
|
||
const fpsEl = this.shadowRoot.querySelector('#fps-counter');
|
||
if (fpsEl) fpsEl.textContent = `${fps} FPS`;
|
||
this._frameCount = 0;
|
||
this._lastFpsTime = now;
|
||
}
|
||
|
||
// 更新各层动画
|
||
this._updateMemoryAnimation(elapsed, delta);
|
||
this._updateKnowledgeAnimation(elapsed, delta);
|
||
this._updateGoalAnimation(elapsed, delta);
|
||
this._updateSkillAnimation(elapsed, delta);
|
||
this._updateEmotionAnimation(elapsed, delta);
|
||
this._updateMirrorAnimation(elapsed, delta);
|
||
this._updateSocialAnimation(elapsed, delta);
|
||
this._updateExplosions(delta);
|
||
this._updateCoreLight(elapsed);
|
||
this._updateBackgroundStars(elapsed);
|
||
|
||
// 渲染
|
||
this._renderer.render(this._scene, this._camera);
|
||
}
|
||
|
||
/** 记忆星云动画 - 星辰呼吸闪烁(着色器内处理),中心辉光脉动 */
|
||
_updateMemoryAnimation(elapsed, delta) {
|
||
const nebula = this._layers.memory?.getObjectByName('memory-nebula');
|
||
if (nebula?.material?.uniforms) {
|
||
nebula.material.uniforms.time.value = elapsed;
|
||
}
|
||
const coreGlow = this._layers.memory?.getObjectByName('memory-core-glow');
|
||
if (coreGlow) {
|
||
coreGlow.scale.setScalar(1 + 0.15 * Math.sin(elapsed * 1.5));
|
||
}
|
||
}
|
||
|
||
/** 知识图谱动画 - 节点微弱脉动 */
|
||
_updateKnowledgeAnimation(elapsed, delta) {
|
||
this._layers.knowledge?.children.forEach(child => {
|
||
if (child.userData?.type === 'entity') {
|
||
child.material.emissiveIntensity = 0.3 + 0.15 * Math.sin(elapsed * 2 + child.position.x);
|
||
}
|
||
});
|
||
}
|
||
|
||
/** 目标晶体动画 - 旋转和子目标轨道运动 */
|
||
_updateGoalAnimation(elapsed, delta) {
|
||
this._layers.goals?.children.forEach(child => {
|
||
if (!child.userData?.type) return;
|
||
|
||
// 自身旋转
|
||
const rs = child.userData.rotationSpeed;
|
||
if (rs) {
|
||
child.rotation.x += rs.x * delta;
|
||
child.rotation.y += rs.y * delta;
|
||
child.rotation.z += rs.z * delta;
|
||
}
|
||
|
||
// 子目标轨道运动
|
||
const center = child.userData.orbitCenter;
|
||
if (center) {
|
||
const angle = (child.userData.orbitAngle || 0) + elapsed * (child.userData.orbitSpeed || 0.2);
|
||
const orbitR = 3;
|
||
child.position.x = center.x + Math.cos(angle) * orbitR;
|
||
child.position.z = center.z + Math.sin(angle) * orbitR;
|
||
child.position.y = center.y + Math.sin(angle * 0.7) * 0.5;
|
||
}
|
||
});
|
||
}
|
||
|
||
/** 技能光环动画 - 激活脉冲效果 */
|
||
_updateSkillAnimation(elapsed, delta) {
|
||
this._layers.skills?.children.forEach(child => {
|
||
if (!child.userData?.type) return;
|
||
const phase = child.userData.pulsePhase || 0;
|
||
|
||
// 持续微弱旋转
|
||
child.rotation.z += 0.3 * delta;
|
||
|
||
// 脉冲效果
|
||
if (child.userData.isPulsing) {
|
||
child.material.emissiveIntensity = 0.8 + 0.5 * Math.sin(elapsed * 5 + phase);
|
||
child.scale.setScalar(1 + 0.1 * Math.sin(elapsed * 3 + phase));
|
||
} else {
|
||
child.material.emissiveIntensity = child.userData.baseEmissiveIntensity + 0.1 * Math.sin(elapsed * 1.5 + phase);
|
||
}
|
||
});
|
||
}
|
||
|
||
/** 情绪粒子动画 - 不同情绪不同运动模式 */
|
||
_updateEmotionAnimation(elapsed, delta) {
|
||
this._layers.emotions?.children.forEach(child => {
|
||
if (!child.userData?.emotionType) return;
|
||
|
||
const positions = child.geometry.attributes.position.array;
|
||
const count = child.userData.particleCount;
|
||
const type = child.userData.emotionType;
|
||
|
||
for (let i = 0; i < count; i++) {
|
||
const i3 = i * 3;
|
||
|
||
switch (type) {
|
||
case 'joy':
|
||
// 金色上浮粒子
|
||
positions[i3 + 1] += delta * 2;
|
||
if (positions[i3 + 1] > 15) positions[i3 + 1] = -15;
|
||
positions[i3] += Math.sin(elapsed + i) * delta * 0.3;
|
||
break;
|
||
|
||
case 'sadness':
|
||
// 蓝色下沉粒子
|
||
positions[i3 + 1] -= delta * 1.5;
|
||
if (positions[i3 + 1] < -15) positions[i3 + 1] = 15;
|
||
positions[i3] += Math.cos(elapsed * 0.5 + i) * delta * 0.2;
|
||
break;
|
||
|
||
case 'anger':
|
||
// 红色快速粒子
|
||
positions[i3] += Math.sin(elapsed * 3 + i * 0.1) * delta * 3;
|
||
positions[i3 + 1] += Math.cos(elapsed * 4 + i * 0.2) * delta * 2;
|
||
positions[i3 + 2] += Math.sin(elapsed * 2 + i * 0.3) * delta * 2.5;
|
||
// 边界约束
|
||
for (let j = 0; j < 3; j++) {
|
||
if (Math.abs(positions[i3 + j]) > 20) positions[i3 + j] *= -0.9;
|
||
}
|
||
break;
|
||
|
||
case 'curiosity':
|
||
// 绿色旋转粒子
|
||
const cx = positions[i3], cz = positions[i3 + 2];
|
||
const dist = Math.sqrt(cx * cx + cz * cz) || 1;
|
||
positions[i3] += (-cz / dist) * delta * 1.5;
|
||
positions[i3 + 2] += (cx / dist) * delta * 1.5;
|
||
positions[i3 + 1] += Math.sin(elapsed + i * 0.5) * delta * 0.5;
|
||
break;
|
||
|
||
default:
|
||
// 默认缓慢漂浮
|
||
positions[i3] += Math.sin(elapsed + i) * delta * 0.3;
|
||
positions[i3 + 1] += Math.cos(elapsed * 0.7 + i) * delta * 0.2;
|
||
break;
|
||
}
|
||
}
|
||
child.geometry.attributes.position.needsUpdate = true;
|
||
});
|
||
}
|
||
|
||
/** 反思之镜动画 - 水波纹效果(着色器处理) */
|
||
_updateMirrorAnimation(elapsed, delta) {
|
||
if (this._mirrorMaterial?.uniforms) {
|
||
this._mirrorMaterial.uniforms.time.value = elapsed;
|
||
}
|
||
}
|
||
|
||
/** 社交网络动画 - 节点微弱漂浮 */
|
||
_updateSocialAnimation(elapsed, delta) {
|
||
this._layers.social?.children.forEach(child => {
|
||
if (child.userData?.type === 'user') {
|
||
const base = child.position.clone();
|
||
child.position.y += Math.sin(elapsed * 0.8 + child.position.x) * delta * 0.2;
|
||
}
|
||
});
|
||
}
|
||
|
||
/** 更新爆炸特效 */
|
||
_updateExplosions(delta) {
|
||
for (let i = this._explosions.length - 1; i >= 0; i--) {
|
||
const exp = this._explosions[i];
|
||
exp.update(delta);
|
||
if (!exp.active) {
|
||
this._layers.goals.remove(exp.mesh);
|
||
exp.dispose();
|
||
this._explosions.splice(i, 1);
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 核心光源呼吸效果 */
|
||
_updateCoreLight(elapsed) {
|
||
if (this._coreLight) {
|
||
this._coreLight.intensity = 1.5 + 0.5 * Math.sin(elapsed * 0.8);
|
||
}
|
||
}
|
||
|
||
/** 背景星空缓慢旋转 */
|
||
_updateBackgroundStars(elapsed) {
|
||
if (this._bgStars) {
|
||
this._bgStars.rotation.y = elapsed * 0.01;
|
||
this._bgStars.rotation.x = Math.sin(elapsed * 0.005) * 0.02;
|
||
}
|
||
}
|
||
|
||
/* =========================================================================
|
||
* 交互功能
|
||
* ========================================================================= */
|
||
|
||
/** 鼠标点击事件 */
|
||
_onClick(event) {
|
||
const rect = this._renderer.domElement.getBoundingClientRect();
|
||
this._mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||
this._mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||
|
||
this._raycaster.setFromCamera(this._mouse, this._camera);
|
||
const intersects = this._raycaster.intersectObjects(this._scene.children, true);
|
||
|
||
// 关闭右键菜单
|
||
this._hideContextMenu();
|
||
|
||
if (intersects.length > 0) {
|
||
const hit = intersects[0].object;
|
||
const userData = hit.userData;
|
||
if (userData && userData.type) {
|
||
this._selectedObject = userData;
|
||
this._showTooltipFor(userData, event);
|
||
return;
|
||
}
|
||
}
|
||
|
||
this._hideTooltip();
|
||
this._selectedObject = null;
|
||
}
|
||
|
||
/** 鼠标移动事件(悬停检测) */
|
||
_onMouseMove(event) {
|
||
const rect = this._renderer.domElement.getBoundingClientRect();
|
||
this._mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||
this._mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||
|
||
// 更新 tooltip 位置(如果已显示)
|
||
const tooltip = this.shadowRoot.querySelector('#tooltip');
|
||
if (tooltip.classList.contains('visible')) {
|
||
tooltip.style.left = (event.clientX - rect.left + 15) + 'px';
|
||
tooltip.style.top = (event.clientY - rect.top - 10) + 'px';
|
||
}
|
||
|
||
// 悬停高亮(节流处理)
|
||
if (this._hoverThrottle) return;
|
||
this._hoverThrottle = true;
|
||
setTimeout(() => { this._hoverThrottle = false; }, 100);
|
||
|
||
this._raycaster.setFromCamera(this._mouse, this._camera);
|
||
const intersects = this._raycaster.intersectObjects(this._scene.children, true);
|
||
|
||
// 取消之前的高亮
|
||
if (this._hoveredObject && this._hoveredObject !== this._selectedObject) {
|
||
this._unhighlightObject(this._hoveredObject);
|
||
}
|
||
|
||
if (intersects.length > 0) {
|
||
const hit = intersects[0].object;
|
||
const userData = hit.userData;
|
||
if (userData && userData.type && userData !== this._selectedObject) {
|
||
this._hoveredObject = userData;
|
||
this._highlightObject(userData);
|
||
}
|
||
} else {
|
||
this._hoveredObject = null;
|
||
}
|
||
}
|
||
|
||
/** 右键菜单事件 */
|
||
_onContextMenu(event) {
|
||
event.preventDefault();
|
||
const rect = this._renderer.domElement.getBoundingClientRect();
|
||
|
||
this._mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||
this._mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||
|
||
this._raycaster.setFromCamera(this._mouse, this._camera);
|
||
const intersects = this._raycaster.intersectObjects(this._scene.children, true);
|
||
|
||
if (intersects.length > 0) {
|
||
const hit = intersects[0].object;
|
||
if (hit.userData?.type) {
|
||
this._contextTarget = hit.userData;
|
||
this._showContextMenu(event.clientX - rect.left, event.clientY - rect.top);
|
||
return;
|
||
}
|
||
}
|
||
this._hideContextMenu();
|
||
}
|
||
|
||
/** 键盘快捷键 */
|
||
_onKeyDown(event) {
|
||
const key = event.key;
|
||
|
||
// 忽略搜索框内的输入
|
||
const searchBox = this.shadowRoot.querySelector('#search-box');
|
||
if (searchBox && this.shadowRoot.activeElement === searchBox) return;
|
||
|
||
switch (key) {
|
||
case '1': this._switchView(VIEW_MODES.MEMORY); break;
|
||
case '2': this._switchView(VIEW_MODES.KNOWLEDGE); break;
|
||
case '3': this._switchView(VIEW_MODES.GOALS); break;
|
||
case '4': this._switchView(VIEW_MODES.SKILLS); break;
|
||
case '5': this._switchView(VIEW_MODES.EMOTIONS); break;
|
||
case '6': this._switchView(VIEW_MODES.SOCIAL); break;
|
||
case '7': this._switchView(VIEW_MODES.PANORAMIC); break;
|
||
case 'h':
|
||
case 'H':
|
||
this._togglePanel();
|
||
break;
|
||
case 'r':
|
||
case 'R':
|
||
this._orbitControls?.reset();
|
||
break;
|
||
case ' ':
|
||
event.preventDefault();
|
||
this._paused = !this._paused;
|
||
break;
|
||
case 'Escape':
|
||
this._hideContextMenu();
|
||
this._hideTooltip();
|
||
break;
|
||
}
|
||
}
|
||
|
||
/** 窗口大小变化处理 */
|
||
_onResize() {
|
||
const container = this.shadowRoot.querySelector('#canvas-container');
|
||
if (!container || !this._renderer || !this._camera) return;
|
||
|
||
const w = container.clientWidth;
|
||
const h = container.clientHeight;
|
||
this._camera.aspect = w / h;
|
||
this._camera.updateProjectionMatrix();
|
||
this._renderer.setSize(w, h);
|
||
}
|
||
|
||
/* =========================================================================
|
||
* UI 交互
|
||
* ========================================================================= */
|
||
|
||
/** 初始化 UI 事件绑定 */
|
||
_initUIBindings() {
|
||
const root = this.shadowRoot;
|
||
|
||
// 视图切换按钮
|
||
root.querySelectorAll('.view-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
this._switchView(btn.dataset.view);
|
||
});
|
||
});
|
||
|
||
// 动画速度滑块
|
||
const speedSlider = root.querySelector('#speed-slider');
|
||
const speedValue = root.querySelector('#speed-value');
|
||
if (speedSlider) {
|
||
speedSlider.addEventListener('input', () => {
|
||
this._animationSpeed = parseFloat(speedSlider.value);
|
||
if (speedValue) speedValue.textContent = `${this._animationSpeed.toFixed(1)}x`;
|
||
});
|
||
}
|
||
|
||
// 粒子密度滑块
|
||
const densitySlider = root.querySelector('#density-slider');
|
||
const densityValue = root.querySelector('#density-value');
|
||
if (densitySlider) {
|
||
densitySlider.addEventListener('input', () => {
|
||
const val = parseInt(densitySlider.value);
|
||
if (densityValue) densityValue.textContent = `${val}%`;
|
||
// 更新星云亮度作为密度的代理指标
|
||
const nebula = this._layers.memory?.getObjectByName('memory-nebula');
|
||
if (nebula?.material?.uniforms) {
|
||
nebula.material.uniforms.globalBrightness.value = val / 100;
|
||
}
|
||
});
|
||
}
|
||
|
||
// 元素可见性开关
|
||
root.querySelectorAll('.toggle-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
btn.classList.toggle('on');
|
||
const layerName = btn.dataset.layer;
|
||
if (this._layers[layerName]) {
|
||
this._layers[layerName].visible = btn.classList.contains('on');
|
||
}
|
||
});
|
||
});
|
||
|
||
// 搜索框
|
||
const searchBox = root.querySelector('#search-box');
|
||
if (searchBox) {
|
||
searchBox.addEventListener('input', () => {
|
||
this._searchQuery = searchBox.value.trim().toLowerCase();
|
||
this._applySearch();
|
||
});
|
||
}
|
||
|
||
// 切换面板按钮
|
||
const toggleBtn = root.querySelector('#toggle-panel-btn');
|
||
if (toggleBtn) {
|
||
toggleBtn.addEventListener('click', () => this._togglePanel());
|
||
}
|
||
|
||
// 右键菜单项
|
||
root.querySelectorAll('.ctx-menu-item').forEach(item => {
|
||
item.addEventListener('click', () => {
|
||
const action = item.dataset.action;
|
||
if (this._contextTarget) {
|
||
this._handleContextAction(action, this._contextTarget);
|
||
}
|
||
this._hideContextMenu();
|
||
});
|
||
});
|
||
|
||
// 点击画布空白处关闭菜单
|
||
this._renderer.domElement.addEventListener('pointerdown', () => {
|
||
this._hideContextMenu();
|
||
});
|
||
|
||
// 时间轴
|
||
const timeline = root.querySelector('#timeline');
|
||
if (timeline) {
|
||
timeline.addEventListener('click', (e) => {
|
||
const rect = timeline.getBoundingClientRect();
|
||
const ratio = (e.clientX - rect.left) / rect.width;
|
||
const progress = root.querySelector('#timeline-progress');
|
||
if (progress) progress.style.width = (ratio * 100) + '%';
|
||
this._applyTimelineSnapshot(ratio);
|
||
});
|
||
}
|
||
}
|
||
|
||
/** 初始化控制面板(placeholder,被 _initUIBindings 替代) */
|
||
_initControls() {
|
||
// 已在 _initUIBindings 中实现
|
||
}
|
||
|
||
/** 切换视图模式 */
|
||
_switchView(mode) {
|
||
this._currentView = mode;
|
||
|
||
// 更新按钮状态
|
||
this.shadowRoot.querySelectorAll('.view-btn').forEach(btn => {
|
||
btn.classList.toggle('active', btn.dataset.view === mode);
|
||
});
|
||
|
||
// 根据视图模式切换可见性
|
||
const layerNames = ['memory', 'knowledge', 'goals', 'skills', 'emotions', 'mirror', 'social'];
|
||
|
||
if (mode === VIEW_MODES.ALL || mode === VIEW_MODES.PANORAMIC) {
|
||
layerNames.forEach(name => {
|
||
if (this._layers[name]) this._layers[name].visible = true;
|
||
});
|
||
} else {
|
||
layerNames.forEach(name => {
|
||
if (this._layers[name]) {
|
||
this._layers[name].visible = (name === mode);
|
||
}
|
||
});
|
||
// 在特定视图中也显示反思之镜
|
||
if (this._layers.mirror && mode !== VIEW_MODES.MEMORY) {
|
||
// 保持镜面可见
|
||
}
|
||
}
|
||
|
||
// 更新可见性开关按钮
|
||
this.shadowRoot.querySelectorAll('.toggle-btn').forEach(btn => {
|
||
const layerName = btn.dataset.layer;
|
||
if (layerName && this._layers[layerName]) {
|
||
btn.classList.toggle('on', this._layers[layerName].visible);
|
||
}
|
||
});
|
||
}
|
||
|
||
/** 切换控制面板显隐 */
|
||
_togglePanel() {
|
||
const panel = this.shadowRoot.querySelector('#control-panel');
|
||
const btn = this.shadowRoot.querySelector('#toggle-panel-btn');
|
||
if (panel) {
|
||
panel.classList.toggle('hidden');
|
||
}
|
||
}
|
||
|
||
/** 显示悬浮框 */
|
||
_showTooltipFor(userData, event) {
|
||
const tooltip = this.shadowRoot.querySelector('#tooltip');
|
||
const titleEl = this.shadowRoot.querySelector('#tooltip-title');
|
||
const contentEl = this.shadowRoot.querySelector('#tooltip-content');
|
||
if (!tooltip || !titleEl || !contentEl) return;
|
||
|
||
const rect = this._renderer.domElement.getBoundingClientRect();
|
||
let title = '';
|
||
let content = '';
|
||
|
||
switch (userData.type) {
|
||
case 'memory': {
|
||
const mem = userData.data;
|
||
title = mem.title;
|
||
const sentimentLabel = mem.sentiment > 0.3 ? '积极' : mem.sentiment < -0.3 ? '消极' : '中性';
|
||
content = `
|
||
<p>${mem.content}</p>
|
||
<p>重要性: ${(mem.importance * 100).toFixed(0)}% | 访问: ${mem.accessCount}次</p>
|
||
<p>情绪: ${sentimentLabel} (${mem.sentiment.toFixed(2)})</p>
|
||
<span class="tag">记忆</span> <span class="tag">${sentimentLabel}</span>
|
||
`;
|
||
break;
|
||
}
|
||
case 'entity': {
|
||
const ent = userData.data;
|
||
title = ent.name;
|
||
content = `
|
||
<p>${ent.description}</p>
|
||
<p>类型: ${ent.type} | 连接数: ${ent.connections}</p>
|
||
<span class="tag">${ent.type}</span>
|
||
`;
|
||
break;
|
||
}
|
||
case 'goal': {
|
||
const goal = userData.data;
|
||
title = goal.title;
|
||
const stateLabel = { completed: '已完成', active: '进行中', paused: '已暂停', abandoned: '已放弃' };
|
||
content = `
|
||
<p>状态: ${stateLabel[goal.state] || goal.state}</p>
|
||
<p>进度: ${(goal.progress * 100).toFixed(0)}%</p>
|
||
<p>子目标: ${goal.children ? goal.children.length : 0}个</p>
|
||
<span class="tag">${goal.state}</span>
|
||
`;
|
||
break;
|
||
}
|
||
case 'skill': {
|
||
const skill = userData.data;
|
||
title = skill.name;
|
||
content = `
|
||
<p>类型: ${skill.type}</p>
|
||
<p>熟练度: ${(skill.proficiency * 100).toFixed(0)}%</p>
|
||
<span class="tag">${skill.type}</span>
|
||
`;
|
||
break;
|
||
}
|
||
case 'user': {
|
||
const user = userData.data;
|
||
title = user.name;
|
||
content = `
|
||
<p>角色: ${user.role}</p>
|
||
<p>亲密度: ${(user.closeness * 100).toFixed(0)}%</p>
|
||
<span class="tag">${user.role}</span>
|
||
`;
|
||
break;
|
||
}
|
||
}
|
||
|
||
titleEl.textContent = title;
|
||
contentEl.innerHTML = content;
|
||
|
||
tooltip.style.left = (event.clientX - rect.left + 15) + 'px';
|
||
tooltip.style.top = (event.clientY - rect.top - 10) + 'px';
|
||
tooltip.classList.add('visible');
|
||
}
|
||
|
||
/** 隐藏悬浮框 */
|
||
_hideTooltip() {
|
||
const tooltip = this.shadowRoot.querySelector('#tooltip');
|
||
if (tooltip) tooltip.classList.remove('visible');
|
||
}
|
||
|
||
/** 显示右键菜单 */
|
||
_showContextMenu(x, y) {
|
||
const menu = this.shadowRoot.querySelector('#context-menu');
|
||
if (!menu) return;
|
||
menu.style.left = x + 'px';
|
||
menu.style.top = y + 'px';
|
||
menu.classList.add('visible');
|
||
}
|
||
|
||
/** 隐藏右键菜单 */
|
||
_hideContextMenu() {
|
||
const menu = this.shadowRoot.querySelector('#context-menu');
|
||
if (menu) menu.classList.remove('visible');
|
||
}
|
||
|
||
/** 处理右键菜单动作 */
|
||
_handleContextAction(action, userData) {
|
||
switch (action) {
|
||
case 'focus':
|
||
if (userData.object) {
|
||
this._orbitControls?.focusOn(userData.object.position, 10);
|
||
}
|
||
break;
|
||
|
||
case 'highlight':
|
||
this._highlightSameType(userData.type);
|
||
break;
|
||
|
||
case 'hide':
|
||
if (this._layers[userData.layer]) {
|
||
this._layers[userData.layer].visible = false;
|
||
}
|
||
break;
|
||
|
||
case 'isolate':
|
||
this._isolateType(userData.type);
|
||
break;
|
||
|
||
case 'details':
|
||
this._showTooltipFor(userData, { clientX: window.innerWidth / 2, clientY: window.innerHeight / 2 });
|
||
break;
|
||
}
|
||
}
|
||
|
||
/** 高亮同类元素 */
|
||
_highlightSameType(type) {
|
||
this._interactableObjects.forEach(obj => {
|
||
if (obj.object && obj.object.material) {
|
||
if (obj.type === type) {
|
||
obj.object.material.emissiveIntensity = 1.0;
|
||
} else {
|
||
obj.object.material.emissiveIntensity = 0.1;
|
||
}
|
||
}
|
||
});
|
||
|
||
// 3秒后恢复
|
||
setTimeout(() => {
|
||
this._interactableObjects.forEach(obj => {
|
||
if (obj.object && obj.object.material) {
|
||
obj.object.material.emissiveIntensity = 0.5;
|
||
}
|
||
});
|
||
}, 3000);
|
||
}
|
||
|
||
/** 隔离特定类型 */
|
||
_isolateType(type) {
|
||
const layerMap = {
|
||
memory: 'memory',
|
||
entity: 'knowledge',
|
||
goal: 'goals',
|
||
skill: 'skills',
|
||
user: 'social',
|
||
};
|
||
const targetLayer = layerMap[type];
|
||
|
||
Object.keys(this._layers).forEach(name => {
|
||
if (this._layers[name]) {
|
||
this._layers[name].visible = (name === targetLayer);
|
||
}
|
||
});
|
||
}
|
||
|
||
/** 高亮特定对象 */
|
||
_highlightObject(userData) {
|
||
if (userData.object && userData.object.material) {
|
||
userData.object.material.emissiveIntensity = 1.0;
|
||
}
|
||
}
|
||
|
||
/** 取消高亮对象 */
|
||
_unhighlightObject(userData) {
|
||
if (userData.object && userData.object.material) {
|
||
const baseIntensity = userData.data?.proficiency !== undefined ? 0.5 : 0.4;
|
||
userData.object.material.emissiveIntensity = baseIntensity;
|
||
}
|
||
}
|
||
|
||
/** 搜索应用 */
|
||
_applySearch() {
|
||
const query = this._searchQuery;
|
||
|
||
this._interactableObjects.forEach(obj => {
|
||
if (!obj.object || !obj.object.material) return;
|
||
|
||
if (!query) {
|
||
obj.object.material.opacity = obj.type === 'entity' ? 0.85 : 0.7;
|
||
return;
|
||
}
|
||
|
||
const data = obj.data;
|
||
let text = '';
|
||
switch (obj.type) {
|
||
case 'memory': text = data.title + ' ' + (data.content || ''); break;
|
||
case 'entity': text = data.name + ' ' + data.type; break;
|
||
case 'goal': text = data.title + ' ' + data.state; break;
|
||
case 'skill': text = data.name + ' ' + data.type; break;
|
||
case 'user': text = data.name + ' ' + data.role; break;
|
||
}
|
||
|
||
if (text.toLowerCase().includes(query)) {
|
||
obj.object.material.opacity = 1.0;
|
||
obj.object.material.emissiveIntensity = 1.0;
|
||
} else {
|
||
obj.object.material.opacity = 0.1;
|
||
obj.object.material.emissiveIntensity = 0.05;
|
||
}
|
||
});
|
||
}
|
||
|
||
/** 应用时间轴快照 */
|
||
_applyTimelineSnapshot(ratio) {
|
||
// 时间轴进度反馈(演示效果)
|
||
const progress = this.shadowRoot.querySelector('#timeline-progress');
|
||
if (progress) progress.style.width = (ratio * 100) + '%';
|
||
|
||
// 根据时间轴位置调整元素的可见性和状态
|
||
// 这里模拟历史回放效果
|
||
this._data.memories.forEach((mem, i) => {
|
||
const visible = (i / this._data.memories.length) < ratio;
|
||
// 通过改变大小来模拟时间流逝
|
||
});
|
||
}
|
||
|
||
/** 触发目标完成爆炸特效 */
|
||
_triggerGoalExplosion(position) {
|
||
const explosion = new ParticleExplosion(position, 0x00E676, 300, 2.5);
|
||
this._layers.goals.add(explosion.mesh);
|
||
this._explosions.push(explosion);
|
||
}
|
||
|
||
/** 隐藏加载画面 */
|
||
_hideLoadingScreen() {
|
||
const screen = this.shadowRoot.querySelector('#loading-screen');
|
||
if (screen) {
|
||
setTimeout(() => {
|
||
screen.classList.add('fade-out');
|
||
setTimeout(() => screen.remove(), 800);
|
||
}, 500);
|
||
}
|
||
}
|
||
|
||
/** 更新统计数字 */
|
||
_updateStats() {
|
||
const root = this.shadowRoot;
|
||
const setVal = (id, val) => {
|
||
const el = root.querySelector(`#${id}`);
|
||
if (el) el.textContent = val;
|
||
};
|
||
|
||
setVal('stat-memories', this._data.memories.length);
|
||
setVal('stat-entities', this._data.entities.length);
|
||
setVal('stat-goals', this._data.goals.reduce((sum, g) => sum + 1 + (g.children?.length || 0), 0));
|
||
setVal('stat-skills', this._data.skills.length);
|
||
setVal('stat-connections', this._data.relations.length + this._data.connections.length);
|
||
}
|
||
|
||
/* =========================================================================
|
||
* 资源清理
|
||
* ========================================================================= */
|
||
|
||
/** 销毁所有资源 */
|
||
_dispose() {
|
||
if (this._animationId) {
|
||
cancelAnimationFrame(this._animationId);
|
||
}
|
||
|
||
// 清理爆炸特效
|
||
this._explosions.forEach(e => e.dispose());
|
||
this._explosions = [];
|
||
|
||
// 清理场景
|
||
if (this._scene) {
|
||
this._scene.traverse(obj => {
|
||
if (obj.geometry) obj.geometry.dispose();
|
||
if (obj.material) {
|
||
if (Array.isArray(obj.material)) {
|
||
obj.material.forEach(m => m.dispose());
|
||
} else {
|
||
obj.material.dispose();
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// 清理渲染器
|
||
if (this._renderer) {
|
||
this._renderer.dispose();
|
||
}
|
||
}
|
||
|
||
/* =========================================================================
|
||
* 公共 API
|
||
* ========================================================================= */
|
||
|
||
/**
|
||
* 更新记忆数据
|
||
* @param {Array} memories - 记忆数据数组
|
||
*/
|
||
updateMemories(memories) {
|
||
this._data.memories = memories;
|
||
// 清理旧的
|
||
while (this._layers.memory.children.length) {
|
||
const child = this._layers.memory.children[0];
|
||
if (child.geometry) child.geometry.dispose();
|
||
if (child.material) child.material.dispose();
|
||
this._layers.memory.remove(child);
|
||
}
|
||
this._buildMemoryNebula();
|
||
this._updateStats();
|
||
}
|
||
|
||
/**
|
||
* 更新知识图谱数据
|
||
* @param {Array} entities - 实体数组
|
||
* @param {Array} relations - 关系数组
|
||
*/
|
||
updateKnowledgeGraph(entities, relations) {
|
||
this._data.entities = entities;
|
||
this._data.relations = relations;
|
||
// 重新运行力导向布局
|
||
this._forceLayout.clear();
|
||
entities.forEach(e => this._forceLayout.addNode(e.id, e.position));
|
||
relations.forEach(r => this._forceLayout.addEdge(r.source, r.target, r.weight));
|
||
for (let i = 0; i < 50; i++) this._forceLayout.step();
|
||
const positions = this._forceLayout.getPositions();
|
||
entities.forEach(e => { if (positions[e.id]) e.position.copy(positions[e.id]); });
|
||
// 重建
|
||
while (this._layers.knowledge.children.length) {
|
||
const child = this._layers.knowledge.children[0];
|
||
if (child.geometry) child.geometry.dispose();
|
||
if (child.material) child.material.dispose();
|
||
this._layers.knowledge.remove(child);
|
||
}
|
||
this._buildKnowledgeGraph();
|
||
this._updateStats();
|
||
}
|
||
|
||
/**
|
||
* 更新情绪数据
|
||
* @param {Array} emotions - 情绪数组
|
||
*/
|
||
updateEmotions(emotions) {
|
||
this._data.emotions = emotions;
|
||
while (this._layers.emotions.children.length) {
|
||
const child = this._layers.emotions.children[0];
|
||
if (child.geometry) child.geometry.dispose();
|
||
if (child.material) child.material.dispose();
|
||
this._layers.emotions.remove(child);
|
||
}
|
||
this._buildEmotionParticles();
|
||
}
|
||
|
||
/**
|
||
* 标记目标完成并触发爆炸特效
|
||
* @param {string} goalId - 目标ID
|
||
*/
|
||
completeGoal(goalId) {
|
||
const goalMesh = this._layers.goals?.getObjectByName(goalId);
|
||
if (goalMesh) {
|
||
this._triggerGoalExplosion(goalMesh.position);
|
||
goalMesh.material.color.setHex(GOAL_STATE_COLORS.completed);
|
||
goalMesh.material.emissive.setHex(GOAL_STATE_COLORS.completed);
|
||
goalMesh.material.opacity = 1.0;
|
||
}
|
||
// 更新数据
|
||
const findGoal = (goals) => {
|
||
for (const g of goals) {
|
||
if (g.id === goalId) return g;
|
||
if (g.children) {
|
||
const found = findGoal(g.children);
|
||
if (found) return found;
|
||
}
|
||
}
|
||
return null;
|
||
};
|
||
const goal = findGoal(this._data.goals);
|
||
if (goal) {
|
||
goal.state = 'completed';
|
||
goal.progress = 1.0;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取当前视图模式
|
||
* @returns {string}
|
||
*/
|
||
getViewMode() {
|
||
return this._currentView;
|
||
}
|
||
|
||
/**
|
||
* 切换暂停/播放
|
||
* @param {boolean} [paused] - 可选,指定暂停状态
|
||
*/
|
||
togglePause(paused) {
|
||
this._paused = paused !== undefined ? paused : !this._paused;
|
||
}
|
||
|
||
/**
|
||
* 导出当前状态快照
|
||
* @returns {Object} 包含所有可视化数据的快照对象
|
||
*/
|
||
exportSnapshot() {
|
||
return {
|
||
timestamp: Date.now(),
|
||
viewMode: this._currentView,
|
||
memories: this._data.memories.map(m => ({ ...m, position: { x: m.position.x, y: m.position.y, z: m.position.z } })),
|
||
emotions: [...this._data.emotions],
|
||
goals: JSON.parse(JSON.stringify(this._data.goals)),
|
||
};
|
||
}
|
||
}
|
||
|
||
/* =========================================================================
|
||
* 注册自定义元素
|
||
* ========================================================================= */
|
||
if (!customElements.get('consciousness-3d-space')) {
|
||
customElements.define('consciousness-3d-space', Consciousness3DSpace);
|
||
}
|
||
|
||
/* =========================================================================
|
||
* 导出(兼容 ES Module 和全局引用)
|
||
* ========================================================================= */
|
||
if (typeof module !== 'undefined' && module.exports) {
|
||
module.exports = { Consciousness3DSpace };
|
||
}
|