diff --git a/docs/casting.md b/docs/casting.md index df0841b..ad3d25f 100644 --- a/docs/casting.md +++ b/docs/casting.md @@ -89,7 +89,7 @@ DISPLAY=:0 FRAMELY_MEDIA_BIN=/绝对路径/media/bin \ 提交调整时携带打开窗口时的实际 `expectedEye`,后端重新读取采集几何后进行核对;主力眼在编辑期间改变时拒绝应用,要求重新打开,不能用旧眼的有效范围验证新眼。 -应用时以当前串流运行状态为准:正在串流则用保存后的取景重启,未串流则不自动开启。保存成功即结束编辑会话;后续重启失败仍保持窗口关闭并明确报告“取景已保存”,不能让过期窗口恢复编辑。原生宿主通过每次轮询返回的 `framingEditor` 状态补偿可能丢失的关闭命令;后台忙时返回 `null`,宿主保留当前状态。普通轮询的空关闭/停止回执不得等待串流锁。 +应用时以当前串流运行状态为准:正在串流则用保存后的取景重启,未串流则不自动开启。保存成功即结束编辑会话;后续重启失败仍保持窗口关闭并明确报告“取景已保存”,不能让过期窗口恢复编辑。原生宿主通过每次轮询返回的 `framingEditor` 状态补偿可能丢失的关闭命令;后台忙时返回 `null`,宿主保留当前状态。普通轮询的空关闭/停止回执不得等待串流锁。四角悬停复用界面的振动设置。 空间框与采集共用工厂面板旋转、相机内参及 `grow_for_undistort` 投影;不能用通用 OpenVR render frustum 替代该投影。运行中的框只接受当前采集进程输出的几何信息,缺少有效校准时隐藏并报错,不显示近似框。 diff --git a/tests/cast_framing_feedback.mjs b/tests/cast_framing_feedback.mjs new file mode 100644 index 0000000..a21b980 --- /dev/null +++ b/tests/cast_framing_feedback.mjs @@ -0,0 +1,26 @@ +import {build} from 'esbuild'; +import assert from 'node:assert/strict'; +import {createRequire} from 'node:module'; + +const listeners=new Map(),windowListeners=new Map(); +Object.defineProperty(globalThis,'navigator',{value:{languages:['zh-CN']},configurable:true}); +globalThis.document={documentElement:{lang:''},addEventListener(name,fn){listeners.set(name,fn);}}; +globalThis.window={addEventListener(name,fn){windowListeners.set(name,fn);}}; +class Element { + constructor(attributes={},parentElement=null){this.attributes=attributes;this.parentElement=parentElement;} + closest(selector){for(let node=this;node;node=node.parentElement){const a=node.attributes;if(selector.startsWith('[inert]')){if(a.inert||a.disabled||a['aria-disabled']==='true')return node;}else if(a['data-framely-interactive']!==undefined||a.tabindex!==undefined)return node;}return null;} +} +globalThis.Element=Element;globalThis.getComputedStyle=()=>({cursor:'default'}); +const result=await build({stdin:{contents:`import React from 'react';import {renderToStaticMarkup} from 'react-dom/server';import {FramingEditor} from './ui/src/cast-framing-editor';export {installHoverFeedback} from './sdk/src/hover';export const render=()=>renderToStaticMarkup({}} onError={()=>{}}/>);`,loader:'tsx',resolveDir:process.cwd()},bundle:true,platform:'node',format:'cjs',write:false}); +const module={exports:{}};new Function('module','exports','require',result.outputFiles[0].text)(module,module.exports,createRequire(import.meta.url)); +const markup=module.exports.render(); +assert.equal((markup.match(/data-framely-interactive=""/g)||[]).length,4,'all four corner markers use the shared haptic hook'); +const pulses=[];module.exports.installHoverFeedback(kind=>pulses.push(kind)); +const frame=new Element({tabindex:0}),nw=new Element({'data-framely-interactive':''},frame),ne=new Element({'data-framely-interactive':''},frame),child=new Element({},nw); +const over=target=>listeners.get('pointerover')({target}); +const out=(target,relatedTarget)=>listeners.get('pointerout')({target,relatedTarget}); +over(frame);out(frame,nw);over(nw);out(nw,child);over(child);out(child,ne);over(ne); +assert.deepEqual(pulses,['hover','hover','hover'],'entering each corner pulses once; children do not repeat'); +frame.attributes.inert=true;out(ne,nw);over(nw);listeners.get('click')({target:nw});assert.equal(pulses.length,3,'pending/closed controls cannot pulse'); +frame.attributes.inert=false;windowListeners.get('blur')();over(nw);listeners.get('click')({target:nw});assert.deepEqual(pulses.slice(-2),['hover','activate']); +console.log('PASS: four framing corners use existing preference-aware feedback, one hover pulse per corner, inert editors have no feedback'); diff --git a/ui/src/cast-framing-editor.tsx b/ui/src/cast-framing-editor.tsx index 8f06cc2..ab5c5c0 100644 --- a/ui/src/cast-framing-editor.tsx +++ b/ui/src/cast-framing-editor.tsx @@ -36,7 +36,7 @@ export function FramingEditor({settings,view,onChange,onError,spatial=false,mini {!spatial&&} {view&&
}