337 lines
13 KiB
JavaScript
337 lines
13 KiB
JavaScript
/**
|
||
* demo_viewer.js —— W-07 实时演示前端控制
|
||
* 职责:three.js 渲染世界 + 500ms 轮询状态(<1s 实时反映) + 调用 /scense_demo/api/*.dspy
|
||
* 状态隔离:所有修改走覆盖层接口,结束演示后正式数据不变(服务端保证)。
|
||
*/
|
||
(function () {
|
||
'use strict';
|
||
var API = {
|
||
start: '/scense_demo/api/start.dspy',
|
||
stop: '/scense_demo/api/stop.dspy',
|
||
camera: '/scense_demo/api/camera.dspy',
|
||
pause: '/scense_demo/api/pause.dspy',
|
||
edit: '/scense_demo/api/entity_edit.dspy',
|
||
attrs: '/scense_demo/api/entity_attrs.dspy',
|
||
hotload: '/scense_demo/api/hotload.dspy',
|
||
presence: '/scense_demo/api/presence.dspy',
|
||
fullscreen: '/scense_demo/api/fullscreen.dspy',
|
||
state: '/scense_demo/api/state.dspy'
|
||
};
|
||
|
||
var state = {
|
||
sessionId: null,
|
||
worldId: null,
|
||
status: null,
|
||
cameraMode: 'orbit',
|
||
timer: null,
|
||
entities: {},
|
||
three: null,
|
||
scene: null,
|
||
camera: null,
|
||
renderer: null,
|
||
meshes: {},
|
||
raf: null,
|
||
clock: 0,
|
||
attrsMode: false
|
||
};
|
||
|
||
function getParam(name) {
|
||
var m = new RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search);
|
||
return m ? decodeURIComponent(m[1]) : null;
|
||
}
|
||
|
||
function post(url, body) {
|
||
return fetch(url, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(body || {})
|
||
}).then(function (r) { return r.json(); });
|
||
}
|
||
|
||
function setLabel(text, color) {
|
||
var el = document.getElementById('demo_session_label');
|
||
if (el) { el.textContent = text; if (color) el.style.color = color; }
|
||
}
|
||
|
||
function ensureThree() {
|
||
if (window.THREE) return Promise.resolve(window.THREE);
|
||
return new Promise(function (resolve) {
|
||
var s = document.createElement('script');
|
||
s.src = 'https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js';
|
||
s.onload = function () { resolve(window.THREE); };
|
||
s.onerror = function () { resolve(null); };
|
||
document.head.appendChild(s);
|
||
});
|
||
}
|
||
|
||
function getContainer() {
|
||
var c = document.getElementById('demo_canvas_container');
|
||
if (!c) return null;
|
||
c.style.display = 'block';
|
||
return c;
|
||
}
|
||
|
||
function initThree() {
|
||
var container = getContainer();
|
||
if (!container) return;
|
||
container.innerHTML = '';
|
||
var w = container.clientWidth || 600;
|
||
var h = container.clientHeight || 400;
|
||
if (!state.three) return;
|
||
var T = state.three;
|
||
state.scene = new T.Scene();
|
||
state.scene.background = new T.Color(0x1a1d26);
|
||
state.camera = new T.PerspectiveCamera(60, w / h, 0.1, 1000);
|
||
state.camera.position.set(8, 6, 10);
|
||
state.camera.lookAt(0, 0, 0);
|
||
var grid = new T.GridHelper(20, 20, 0x3a4152, 0x2a2f3d);
|
||
state.scene.add(grid);
|
||
var amb = new T.AmbientLight(0xffffff, 0.5);
|
||
state.scene.add(amb);
|
||
var dir = new T.DirectionalLight(0xffffff, 0.8);
|
||
dir.position.set(10, 20, 10);
|
||
state.scene.add(dir);
|
||
state.renderer = new T.WebGLRenderer({antialias: true});
|
||
state.renderer.setSize(w, h);
|
||
container.appendChild(state.renderer.domElement);
|
||
animate();
|
||
}
|
||
|
||
function animate() {
|
||
state.raf = requestAnimationFrame(animate);
|
||
if (!state.renderer || !state.scene) return;
|
||
state.clock += 0.01;
|
||
Object.keys(state.meshes).forEach(function (id) {
|
||
var m = state.meshes[id];
|
||
if (m && m.userData && m.userData.rot) {
|
||
m.rotation.y = state.clock * (m.userData.rotSpeed || 0.5);
|
||
}
|
||
});
|
||
applyCamera();
|
||
state.renderer.render(state.scene, state.camera);
|
||
}
|
||
|
||
function applyCamera() {
|
||
if (!state.camera) return;
|
||
if (state.cameraMode === 'topdown') {
|
||
state.camera.position.lerp(new state.three.Vector3(0, 20, 0.01), 0.08);
|
||
state.camera.lookAt(0, 0, 0);
|
||
} else if (state.cameraMode === 'firstperson') {
|
||
state.camera.position.lerp(new state.three.Vector3(2, 2, 6), 0.08);
|
||
state.camera.lookAt(0, 1.5, 0);
|
||
} else {
|
||
state.camera.position.lerp(new state.three.Vector3(8, 6, 10), 0.08);
|
||
state.camera.lookAt(0, 0, 0);
|
||
}
|
||
}
|
||
|
||
function renderState(data) {
|
||
if (!state.scene || !state.three) return;
|
||
var seen = {};
|
||
(data.entities || []).forEach(function (ent) {
|
||
seen[ent.entity_id] = true;
|
||
var cfg = ent.state || {};
|
||
var color = cfg.color || '#4A90D9';
|
||
var size = cfg.size || 1;
|
||
if (!state.meshes[ent.entity_id]) {
|
||
var geo = new state.three.BoxGeometry(size, size, size);
|
||
var mat = new state.three.MeshStandardMaterial({color: new state.three.Color(color)});
|
||
var mesh = new state.three.Mesh(geo, mat);
|
||
mesh.position.set((Object.keys(state.meshes).length % 6) * 2.5 - 5, 0.5, 0);
|
||
mesh.userData = {rot: true, rotSpeed: cfg.rotSpeed || 0.5};
|
||
state.scene.add(mesh);
|
||
state.meshes[ent.entity_id] = mesh;
|
||
} else {
|
||
var m = state.meshes[ent.entity_id];
|
||
if (cfg.color) m.material.color.set(cfg.color);
|
||
}
|
||
});
|
||
Object.keys(state.meshes).forEach(function (id) {
|
||
if (!seen[id]) {
|
||
state.scene.remove(state.meshes[id]);
|
||
delete state.meshes[id];
|
||
}
|
||
});
|
||
}
|
||
|
||
function setAttrsPanel(data) {
|
||
var el = getContainer();
|
||
if (!el) return;
|
||
var lines = ['【实体属性】 ' + data.entity_name + ' (' + data.entity_id + ')'];
|
||
lines.push('--- 基线(base, 演示开始快照) ---');
|
||
lines.push(JSON.stringify(data.base, null, 2));
|
||
lines.push('--- 当前(demo, 演示中值) ---');
|
||
lines.push(JSON.stringify(data.demo, null, 2));
|
||
lines.push('--- 合并(merged, 渲染值) ---');
|
||
lines.push(JSON.stringify(data.merged, null, 2));
|
||
var div = document.createElement('pre');
|
||
div.style.color = '#8A93A6';
|
||
div.style.whiteSpace = 'pre-wrap';
|
||
div.textContent = lines.join('\n');
|
||
el.innerHTML = '';
|
||
el.appendChild(div);
|
||
}
|
||
|
||
function poll() {
|
||
if (!state.sessionId) return;
|
||
post(API.state, {session_id: state.sessionId}).then(function (r) {
|
||
if (r.status === 200 && r.data) {
|
||
state.status = r.data.status;
|
||
state.cameraMode = r.data.camera_mode || state.cameraMode;
|
||
setLabel('会话 ' + state.sessionId.slice(0, 12) + ' · ' + state.status +
|
||
' · 视角 ' + state.cameraMode + ' · 在线 ' + (r.data.viewer_count || 0), '#22C55E');
|
||
if (state.attrsMode) {
|
||
state.attrsMode = false;
|
||
}
|
||
if (state.scene) renderState(r.data);
|
||
} else {
|
||
setLabel('轮询失败: ' + (r.message || '未知'), '#EF4444');
|
||
}
|
||
}).catch(function (e) {
|
||
setLabel('轮询异常: ' + e.message, '#EF4444');
|
||
});
|
||
}
|
||
|
||
function startPolling() {
|
||
if (state.timer) clearInterval(state.timer);
|
||
state.timer = setInterval(poll, 500); // 500ms 轮询 → 编辑实体 <1s 反映
|
||
poll();
|
||
}
|
||
|
||
function stopPolling() {
|
||
if (state.timer) { clearInterval(state.timer); state.timer = null; }
|
||
}
|
||
|
||
var demoViewer = {
|
||
startDemo: function () {
|
||
var worldId = state.worldId || getParam('world_id');
|
||
if (!worldId) {
|
||
worldId = window.prompt('请输入世界 ID (W-01 world.id):');
|
||
if (!worldId) { setLabel('已取消:缺少 world_id', '#FF8800'); return; }
|
||
}
|
||
state.worldId = worldId;
|
||
ensureThree().then(function (T) {
|
||
state.three = T;
|
||
return post(API.start, {world_id: worldId, session_name: '演示-' + worldId});
|
||
}).then(function (r) {
|
||
if (r.status === 200 && r.data) {
|
||
state.sessionId = r.data.session_id;
|
||
initThree();
|
||
startPolling();
|
||
setLabel('演示已开始: ' + r.data.session_id.slice(0, 12), '#22C55E');
|
||
} else {
|
||
setLabel('开始失败: ' + (r.message || '未知'), '#EF4444');
|
||
}
|
||
}).catch(function (e) {
|
||
setLabel('开始异常: ' + e.message, '#EF4444');
|
||
});
|
||
},
|
||
|
||
stopDemo: function () {
|
||
if (!state.sessionId) { setLabel('未开始演示', '#FF8800'); return; }
|
||
post(API.stop, {session_id: state.sessionId}).then(function (r) {
|
||
stopPolling();
|
||
if (r.status === 200) {
|
||
setLabel('演示已结束,正式数据未被修改', '#22C55E');
|
||
} else {
|
||
setLabel('结束失败: ' + (r.message || '未知'), '#EF4444');
|
||
}
|
||
state.sessionId = null;
|
||
state.meshes = {};
|
||
var c = getContainer();
|
||
if (c) { c.innerHTML = ''; c.textContent = '3D 预览区(已结束)'; }
|
||
});
|
||
},
|
||
|
||
switchCamera: function (mode) {
|
||
if (!state.sessionId) { setLabel('请先开始演示', '#FF8800'); return; }
|
||
post(API.camera, {session_id: state.sessionId, camera_mode: mode}).then(function (r) {
|
||
if (r.status === 200) { state.cameraMode = r.data.camera_mode; setLabel('视角已切换: ' + r.data.camera_mode, '#22C55E'); }
|
||
else setLabel('视角切换失败: ' + (r.message || '未知'), '#EF4444');
|
||
});
|
||
},
|
||
|
||
pauseDemo: function () {
|
||
if (!state.sessionId) { setLabel('请先开始演示', '#FF8800'); return; }
|
||
post(API.pause, {session_id: state.sessionId, paused: true}).then(function (r) {
|
||
if (r.status === 200) setLabel('已暂停', '#FF8800');
|
||
else setLabel('暂停失败: ' + (r.message || '未知'), '#EF4444');
|
||
});
|
||
},
|
||
|
||
resumeDemo: function () {
|
||
if (!state.sessionId) { setLabel('请先开始演示', '#FF8800'); return; }
|
||
post(API.pause, {session_id: state.sessionId, paused: false}).then(function (r) {
|
||
if (r.status === 200) setLabel('已继续', '#22C55E');
|
||
else setLabel('继续失败: ' + (r.message || '未知'), '#EF4444');
|
||
});
|
||
},
|
||
|
||
editEntity: function () {
|
||
if (!state.sessionId) { setLabel('请先开始演示', '#FF8800'); return; }
|
||
var entityId = window.prompt('输入要编辑的实体 ID:');
|
||
if (!entityId) return;
|
||
var fieldsRaw = window.prompt('输入字段 JSON(如 {"color":"#22C55E","size":2,"rotSpeed":1}):');
|
||
if (!fieldsRaw) return;
|
||
var fields;
|
||
try { fields = JSON.parse(fieldsRaw); }
|
||
catch (e) { setLabel('字段 JSON 非法: ' + e.message, '#EF4444'); return; }
|
||
post(API.edit, {session_id: state.sessionId, entity_id: entityId, fields: fields}).then(function (r) {
|
||
if (r.status === 200) setLabel('实体已更新(<1s 前端反映)', '#22C55E');
|
||
else setLabel('编辑失败: ' + (r.message || '未知'), '#EF4444');
|
||
});
|
||
},
|
||
|
||
showAttrs: function () {
|
||
if (!state.sessionId) { setLabel('请先开始演示', '#FF8800'); return; }
|
||
var entityId = window.prompt('输入要查看的实体 ID:');
|
||
if (!entityId) return;
|
||
post(API.attrs, {session_id: state.sessionId, entity_id: entityId}).then(function (r) {
|
||
if (r.status === 200 && r.data) { state.attrsMode = true; setAttrsPanel(r.data); }
|
||
else setLabel('查询失败: ' + (r.message || '未知'), '#EF4444');
|
||
});
|
||
},
|
||
|
||
hotload: function () {
|
||
if (!state.sessionId) { setLabel('请先开始演示', '#FF8800'); return; }
|
||
var scriptId = window.prompt('输入脚本 ID (script_engine 联动):');
|
||
if (!scriptId) return;
|
||
var content = window.prompt('输入脚本内容(保存即语法校验+热加载):');
|
||
if (!content) return;
|
||
post(API.hotload, {session_id: state.sessionId, script_id: scriptId, script_content: content}).then(function (r) {
|
||
if (r.status === 200) setLabel('脚本已热加载: ' + r.data.status, '#22C55E');
|
||
else setLabel('热加载失败: ' + (r.message || '未知'), '#EF4444');
|
||
});
|
||
},
|
||
|
||
joinPresence: function () {
|
||
if (!state.sessionId) { setLabel('请先开始演示', '#FF8800'); return; }
|
||
post(API.presence, {session_id: state.sessionId, action: 'join'}).then(function (r) {
|
||
if (r.status === 200) setLabel('已加入多人预览, 当前在线 ' + r.data.viewer_count, '#22C55E');
|
||
else setLabel('加入失败: ' + (r.message || '未知'), '#EF4444');
|
||
});
|
||
},
|
||
|
||
leavePresence: function () {
|
||
if (!state.sessionId) { setLabel('请先开始演示', '#FF8800'); return; }
|
||
post(API.presence, {session_id: state.sessionId, action: 'leave', user_id: ''}).then(function (r) {
|
||
if (r.status === 200) setLabel('已离开预览', '#22C55E');
|
||
else setLabel('离开失败: ' + (r.message || '未知'), '#EF4444');
|
||
});
|
||
},
|
||
|
||
toggleFullscreen: function () {
|
||
if (!state.sessionId) { setLabel('请先开始演示', '#FF8800'); return; }
|
||
var el = getContainer();
|
||
var enabled = false;
|
||
if (document.fullscreenElement) { document.exitFullscreen(); enabled = false; }
|
||
else if (el && el.requestFullscreen) { el.requestFullscreen(); enabled = true; }
|
||
post(API.fullscreen, {session_id: state.sessionId, enabled: enabled}).then(function (r) {
|
||
if (r.status === 200) setLabel('全屏: ' + (enabled ? '开' : '关'), '#22C55E');
|
||
});
|
||
}
|
||
};
|
||
|
||
window.demoViewer = demoViewer;
|
||
})();
|