bend_puzzle/client/input/drag.js
2026-07-31 12:54:46 +09:00

73 lines
3.7 KiB
JavaScript

(function(root,factory){
const api=factory(root.BendFrameScheduler);
if(typeof module==='object'&&module.exports)module.exports=factory(require('./frame-scheduler.js'));
root.BendDragScheduler=api;
})(typeof globalThis!=='undefined'?globalThis:this,function(FrameScheduler){
'use strict';
const STATES=Object.freeze(['idle','armed','running','draining','settling','cancelled']);
function createDragScheduler(options={}){
if(!FrameScheduler?.createFrameScheduler)throw new TypeError('BendFrameScheduler is required');
if(typeof options.onFrame!=='function')throw new TypeError('Drag onFrame callback is required');
const maxSamples=Math.max(2,Number(options.maxSamples)||12);
const trim=typeof options.trim==='function'?options.trim:samples=>samples.splice(0,Math.max(0,samples.length-maxSamples));
let state='idle',pointerId=null,latest=null,logical=[],revision=0,visualRevision=0,logicalRevision=0;
const frame=FrameScheduler.createFrameScheduler({
interval:options.interval,
tolerance:options.tolerance,
watchdogDelay:options.watchdogDelay,
requestFrame:options.requestFrame,
cancelFrame:options.cancelFrame,
setDelay:options.setDelay,
clearDelay:options.clearDelay,
now:options.now,
onWatchdog:options.onWatchdog,
commit:(_signal,timestamp)=>options.onFrame(api,timestamp)
});
const assertState=next=>{if(!STATES.includes(next))throw new TypeError(`Unknown drag state: ${next}`);state=next};
const clearData=()=>{pointerId=null;latest=null;logical.length=0;revision=0;visualRevision=0;logicalRevision=0};
const arm=id=>{
if(id==null)throw new TypeError('Drag pointerId is required');
frame.cancel({resetCadence:true});clearData();pointerId=id;assertState('armed');return api;
};
const push=(sample,{schedule=true}={})=>{
if(!sample||sample.pointerId==null)return false;
if(state==='idle'||state==='cancelled'||state==='settling')arm(sample.pointerId);
if(sample.pointerId!==pointerId||state==='draining')return false;
latest=sample;logical.push(sample);trim(logical,maxSamples);revision++;
if(state==='armed')assertState('running');
if(schedule)frame.push(revision);
return revision;
};
const requestFrame=()=>{if(latest&&state!=='idle'&&state!=='cancelled')frame.push(revision)};
const beginDrain=sample=>{
if(sample)push(sample,{schedule:false});
if(state==='idle'||state==='cancelled')return false;
frame.cancel();assertState('draining');return true;
};
const beginSettling=()=>{if(state==='draining'||state==='running'||state==='armed')assertState('settling');frame.cancel();return state==='settling'};
const settle=()=>{frame.cancel({resetCadence:true});clearData();assertState('idle')};
const cancel=()=>{frame.cancel({resetCadence:true});clearData();assertState('cancelled')};
const takeLogical=max=>{
const count=Math.max(0,Math.min(logical.length,Number(max)||1));
return logical.splice(0,count);
};
const drainLogical=()=>logical.splice(0);
const markVisual=()=>{visualRevision=revision};
const markLogical=()=>{if(!logical.length)logicalRevision=revision};
const inspect=()=>Object.freeze({
state,pointerId,latest,logicalCount:logical.length,revision,visualRevision,logicalRevision,
freshVisual:visualRevision!==revision,freshLogical:logicalRevision!==revision
});
const api=Object.freeze({
arm,push,requestFrame,beginDrain,beginSettling,settle,cancel,takeLogical,drainLogical,
latest:()=>latest,
hasLogical:()=>logical.length>0,
markVisual,markLogical,inspect
});
return api;
}
return Object.freeze({STATES,createDragScheduler});
});