跳到主要内容

场景时钟

跨端累计运行时间、暂停冻结、实例身份与完整自定义界面

15.3 将场景时钟计算放入共享核心。网页主线程、严格 Worker、原生 QuickJS/C 和桌面脚本使用同一语义。sceneClock 为已实现能力;共同轨迹播放已在15.5接通,scenePlayback/sceneRoutePlayback 为 true;sceneLinePlayback/sceneTracks 为 true,见线动画与样式曲线。见轨迹播放。移动宿主、外机 Windows/macOS 与正式发行仍需分别验收。

import {map} from 'sqsmap';
if (!(await map.sqs.getCapabilities()).sceneClock) throw Error('需要匹配场景时钟版本');
const scene = await map.sqs.scenes.create({id:'clock'});
await scene.start();
const running = await scene.getClock(); // {id, sceneInstanceId, state, elapsedMs}
await scene.pause();
const paused = await scene.getClock();
await scene.resume();
await scene.stop();
const stopped = await scene.getClock();
await map.sqs.scenes.remove(scene);

elapsedMs 是当前场景实例的累计运行毫秒,允许小数。草稿为 0,暂停和停止时冻结,恢复保留累计值;重复 start/resume 不重置时间。共同场景 stop 为终态。删除之后旧句柄返回 STALE_HANDLE,同名重建是新的身份与时钟。每次查询返回不可变历史快照,不修改以前的返回值。

查询是纯读取:不产生 sqs.scenechange,不增加写入水位、不请求新的渲染帧,也不等待资源、动画完成或 ready。状态控制仍产生现有生命周期事件,控制失败不提交时间状态。需要显示时间时,用户界面自行按刷新节奏查询;不提供常驻引擎 UI。时间随地图持有,脚本退出后运行中的场景继续计时。

宿主传入单调时间,核心排除显式暂停区间,计入运行时后台/未查询期间的单调间隔。不同平台的系统睡眠行为取决于该平台单调时钟;此 API 不表示日期或绝对墙钟。旧桌面 map.scenes 保留其兼容计时规则,新代码使用 map.sqs.scenes。也不要用 elapsedMs 与另一地图的宿主时间戳相减。

动态库使用 sqsm_map_scene_json,例如 {"method":"getClock","args":[{"id":"clock","sceneInstanceId":"由create返回的身份"}]},仍是 ABI1,输出使用 sqsm_map_bytes_free 释放。完整 C 示例、头文件和原生脚本类型来自相同核心提交;源码附件不是正式发行动态库。

以下完整 HTML 包含六个按钮的事件、HTML/CSS、时间查询、图层与署名、错误提示和销毁清理。直接运行完整页面,共同脚本。示例页放在完整 SDK 分发根目录;部署到其它位置时,将 import 地址改为自己的完整 SDK 地址。

<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>SqsMap scene clock</title>
<style>html,body{margin:0;height:100%;font:14px system-ui}#map{height:100%}#panel{position:absolute;top:12px;left:12px;background:white;padding:12px;border-radius:8px}button{margin:3px}#credits{position:absolute;bottom:8px;right:8px;background:#fff;padding:4px}#time{font-size:20px;font-weight:bold}#error{color:#a00;max-width:300px}</style>
<div id="map"></div><div id="panel"><div id="time">Loading</div><div id="controls"></div><div id="error" role="status"></div></div><div id="credits"></div>
<script type="module">
import {MapView} from './dist/index.js';
const map=await MapView.create(document.querySelector('#map')),time=document.querySelector('#time'),error=document.querySelector('#error'),controls=document.querySelector('#controls');
map.jumpTo({center:[0,0],zoom:12});map.addSource('point',{type:'geojson',attribution:'Synthetic example data',data:{type:'FeatureCollection',features:[{type:'Feature',id:1,properties:{},geometry:{type:'Point',coordinates:[0,0]}}]}}).addLayer({id:'point',type:'circle',source:'point',layout:{visibility:'none'},paint:{'circle-color':'#f33','circle-radius':12}});await map.flush();
let scene,alive=true,busy=false,frame=0,reading=false;const buttons={};
function updateButtons(state){for(const [action,button] of Object.entries(buttons))button.disabled=busy||({create:!!scene,start:state!=='draft',pause:state!=='running',resume:state!=='paused',stop:!scene||state==='stopped',remove:!scene}[action]);}
async function create(){scene=await map.sqs.scenes.create({id:'clock'});await scene.attachSource(await map.getSource('point'));}
for(const action of ['create','start','pause','resume','stop','remove']){
 const button=document.createElement('button');button.textContent=action;buttons[action]=button;controls.append(button);
 button.onclick=async()=>{busy=true;error.textContent='';updateButtons('');try{if(action==='create')await create();else if(action==='remove'){await map.sqs.scenes.remove(scene);scene=undefined;}else await scene[action]();}catch(e){error.textContent=e.message;}finally{busy=false;await observe();}};
}
const creditSub=map.onAttributionsChange(e=>document.querySelector('#credits').textContent=e.attributions.text);
const sceneSub=map.on('sqs.scenechange',e=>console.log('Scene lifecycle',e));
async function observe(){if(reading||!alive)return;reading=true;try{const clock=scene?await scene.getClock():undefined;if(!alive)return;time.textContent=clock?clock.state+' '+(clock.elapsedMs/1000).toFixed(3)+' s':'No scene';updateButtons(clock?.state);document.querySelector('#credits').textContent=(await map.getAttributions()).text;}catch(e){if(alive)error.textContent=e.message;}finally{reading=false;}}
function tick(){void observe();if(alive)frame=requestAnimationFrame(tick);}
await create();map.setLayoutProperty('point','visibility','visible');await map.flush();await observe();tick();
addEventListener('pagehide',()=>{alive=false;cancelAnimationFrame(frame);creditSub();sceneSub();map.destroy();},{once:true});
</script></html>