scense_demo/wwwroot/demo_viewer.js

362 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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;
})();
/* —— 按钮事件委托demo_viewer.ui 按钮无 script 绑定,按文本路由)—— */
(function () {
var ACT = {
'开始演示': function () { window.demoViewer.startDemo(); },
'结束演示': function () { window.demoViewer.stopDemo(); },
'暂停': function () { window.demoViewer.pauseDemo(); },
'继续': function () { window.demoViewer.resumeDemo(); },
'编辑实体': function () { window.demoViewer.editEntity(); },
'查看属性': function () { window.demoViewer.showAttrs(); },
'保存脚本(热加载)': function () { window.demoViewer.hotload(); },
'全屏': function () { window.demoViewer.toggleFullscreen(); },
'视角:环绕': function () { window.demoViewer.switchCamera('orbit'); },
'视角:俯视': function () { window.demoViewer.switchCamera('topdown'); },
'视角:第一人称': function () { window.demoViewer.switchCamera('firstperson'); },
'加入多人预览': function () { window.demoViewer.joinPresence(); },
'离开预览': function () { window.demoViewer.leavePresence(); }
};
document.addEventListener('click', function (e) {
var b = e.target && e.target.closest ? e.target.closest('button') : null;
if (!b || !window.demoViewer) return;
var txt = (b.textContent || '').trim();
if (ACT[txt]) { e.preventDefault(); ACT[txt](); }
});
})();