@@ -36,6 +36,29 @@ type ParseableGltfLoader = GLTFLoader & {
parseAsync ( data : ArrayBuffer | string , path : string ) : Promise < LoadedGltf > ;
parseAsync ( data : ArrayBuffer | string , path : string ) : Promise < LoadedGltf > ;
} ;
} ;
type PreviewObjectSnapshot = {
position : ReturnType < Object3D [ "position" ] [ "clone" ] > ;
quaternion : ReturnType < Object3D [ "quaternion" ] [ "clone" ] > ;
visible : boolean ;
parent : Object3D | null ;
} ;
type PreviewPlaybackState = {
operation : PreviewOperation | null ;
isPlaying : boolean ;
rafId : number | null ;
startedAtMs : number ;
baseTimeMs : number ;
currentTimeMs : number ;
currentFrameIndex : number ;
frameTimesMs : number [ ] ;
totalTimeMs : number ;
objectSnapshots : Map < Object3D , PreviewObjectSnapshot > ;
objectMap : Map < string , Object3D > ;
attachedOriginalParents : Map < Object3D , Object3D | null > ;
rotatingObjects : Map < Object3D , { axis : "x" | "y" | "z" ; rate : number } > ;
} ;
export type PreviewBasePoint = {
export type PreviewBasePoint = {
x? : number ;
x? : number ;
y? : number ;
y? : number ;
@@ -86,10 +109,13 @@ const zeroBasePoint: PreviewResolvedBasePoint = {
} ;
} ;
const previewEnvironmentUrl = "/DayCityOutdoor.exr" ;
const previewEnvironmentUrl = "/DayCityOutdoor.exr" ;
const DEFAULT_PREVIEW_FRAME_INTERVAL_MS = 20 ;
const SECOND_MS = 1000 ;
let runtime : PreviewRuntime | null = null ;
let runtime : PreviewRuntime | null = null ;
let activePreviewContext : PreviewLoadContext | null = null ;
let activePreviewContext : PreviewLoadContext | null = null ;
let previewEnvironmentTexturePromise : Promise < Texture > | null = null ;
let previewEnvironmentTexturePromise : Promise < Texture > | null = null ;
let previewPlayback : PreviewPlaybackState = createEmptyPlaybackState ( ) ;
export function openModelPreview (
export function openModelPreview (
model : ModelItem ,
model : ModelItem ,
@@ -127,18 +153,14 @@ export function openModelPreview(
</div>
</div>
<div class="preview-scene-section">
<div class="preview-scene-section">
<div class="preview-scene-body">
<div class="preview-scene-body">
${ canWrite ? `
<div style="display: flex; align-items: center; ">
<button id="previewCaptureThumbB tn" type="button">截缩略图 </button>
<button id="previewImportSceneBtn" class="primary-b tn" type="button">导入 </button>
<label class="preview-switch">
<input id="previewTransparentThumb" type="checkbox" />
<span>透明背景截图</span>
</label>
` : "" }
<button id="previewImportSceneBtn" class="primary-btn" type="button">导入场景</button>
<label class="preview-switch">
<label class="preview-switch">
<input id="previewUseBasePoint" type="checkbox" />
<input id="previewUseBasePoint" type="checkbox" />
<span>启用基点导入 </span>
<span>启用基点</span>
</label>
</label>
</div>
<div class="preview-basepoint-grid">
<div class="preview-basepoint-grid">
<label><span>X</span><input name="baseX" type="number" value="0" step="0.001" /></label>
<label><span>X</span><input name="baseX" type="number" value="0" step="0.001" /></label>
<label><span>Y</span><input name="baseY" type="number" value="0" step="0.001" /></label>
<label><span>Y</span><input name="baseY" type="number" value="0" step="0.001" /></label>
@@ -158,6 +180,13 @@ export function openModelPreview(
</div>
</div>
` ,
` ,
actions : {
actions : {
更 新 缩 略 图 ( ) {
if ( canWrite ) {
screenshotModel ( model . id , onThumbnailSaved ) . catch ( ( error ) = > {
} )
}
} ,
关 闭 ( ) {
关 闭 ( ) {
disposePreview ( ) ;
disposePreview ( ) ;
w2popup . close ( ) ;
w2popup . close ( ) ;
@@ -166,7 +195,8 @@ export function openModelPreview(
} ) ;
} ) ;
popup . self
popup . self
. on ( "open:after" , ( ) = > {
. on ( "open:after" , ( ) = > { popup
console . log ( popup )
bindProcessPlaceholder ( operations ) ;
bindProcessPlaceholder ( operations ) ;
bindImportScene ( model , operations , options . onImportScene ) ;
bindImportScene ( model , operations , options . onImportScene ) ;
if ( canWrite ) bindThumbnailCapture ( model . id , onThumbnailSaved ) ;
if ( canWrite ) bindThumbnailCapture ( model . id , onThumbnailSaved ) ;
@@ -406,6 +436,7 @@ function isAbortError(error: unknown) {
}
}
function cleanupPreviewContextResources ( context : PreviewLoadContext ) {
function cleanupPreviewContextResources ( context : PreviewLoadContext ) {
stopPreviewPlayback ( ) ;
if ( context . animationId !== undefined ) {
if ( context . animationId !== undefined ) {
cancelAnimationFrame ( context . animationId ) ;
cancelAnimationFrame ( context . animationId ) ;
context . animationId = undefined ;
context . animationId = undefined ;
@@ -454,7 +485,14 @@ function disposeMaterial(material: unknown) {
}
}
function bindThumbnailCapture ( modelId : number , onThumbnailSaved ? : ( ) = > Promise < void > | void ) {
function bindThumbnailCapture ( modelId : number , onThumbnailSaved ? : ( ) = > Promise < void > | void ) {
document . querySelector < HTMLButtonElement > ( "#previewCaptureThumbBtn" ) ? . addEventListener ( "click" , async ( ) = > {
document . querySelector < HTMLButtonElement > ( "#previewCaptureThumbBtn" ) ? . addEventListener ( "click" , ( ) = > {
screenshotModel ( modelId , onThumbnailSaved ) . catch ( ( error ) = > {
} )
} ) ;
}
async function screenshotModel ( modelId : number , onThumbnailSaved ? : ( ) = > Promise < void > | void ) {
try {
try {
if ( ! runtime ) {
if ( ! runtime ) {
throw new Error ( "模型还未加载完成" ) ;
throw new Error ( "模型还未加载完成" ) ;
@@ -463,7 +501,7 @@ function bindThumbnailCapture(modelId: number, onThumbnailSaved?: () => Promise<
if ( ! renderer || ! scene || ! camera || ! controls ) {
if ( ! renderer || ! scene || ! camera || ! controls ) {
throw new Error ( "模型还未加载完成" ) ;
throw new Error ( "模型还未加载完成" ) ;
}
}
const transparent = document. querySelector< HTMLInputElement> ( "#previewTransparentThumb") ? . checked ? ? false ;
const transparent = true ; // document. querySelector< HTMLInputElement>( "#previewTransparentThumb")?.checked ?? false;
controls . update ( ) ;
controls . update ( ) ;
const oldBackground = scene . background ;
const oldBackground = scene . background ;
const oldClearAlpha = renderer . getClearAlpha ( ) ;
const oldClearAlpha = renderer . getClearAlpha ( ) ;
@@ -487,7 +525,6 @@ function bindThumbnailCapture(modelId: number, onThumbnailSaved?: () => Promise<
} catch ( error ) {
} catch ( error ) {
notifyError ( error ) ;
notifyError ( error ) ;
}
}
} ) ;
}
}
function getSelectedOperation ( operations : PreviewOperation [ ] ) {
function getSelectedOperation ( operations : PreviewOperation [ ] ) {
@@ -581,6 +618,7 @@ function bindProcessPlaceholder(operations: PreviewOperation[]) {
document . querySelectorAll ( ".preview-process-tree li" ) . forEach ( ( item ) = > item . classList . remove ( "is-active" ) ) ;
document . querySelectorAll ( ".preview-process-tree li" ) . forEach ( ( item ) = > item . classList . remove ( "is-active" ) ) ;
button . closest ( "li" ) ? . classList . add ( "is-active" ) ;
button . closest ( "li" ) ? . classList . add ( "is-active" ) ;
const index = Number ( button . dataset . processIndex ? ? 0 ) ;
const index = Number ( button . dataset . processIndex ? ? 0 ) ;
stopPreviewPlayback ( ) ;
updateStatus ( operations [ index ] ? ? null ) ;
updateStatus ( operations [ index ] ? ? null ) ;
} ) ;
} ) ;
} ) ;
} ) ;
@@ -600,15 +638,387 @@ function bindProcessPlaceholder(operations: PreviewOperation[]) {
notify ( "当前工艺暂无播放数据" ) ;
notify ( "当前工艺暂无播放数据" ) ;
return ;
return ;
}
}
notify ( ` ${ operation . CraftName || "当前工艺" } 已读取 ${ operation . frameCount } 帧,播放逻辑待接入 ` ) ;
startPreviewPlayback ( operation ) ;
} ) ;
} ) ;
document . querySelector < HTMLButtonElement > ( "#previewPauseBtn" ) ? . addEventListener ( "click" , ( ) = > {
document . querySelector < HTMLButtonElement > ( "#previewPauseBtn" ) ? . addEventListener ( "click" , ( ) = > {
updateStatus ( getSelectedOperation ( operations ) ) ;
updateStatus ( getSelectedOperation ( operations ) ) ;
notify ( "暂停逻辑待接入" ) ;
pausePreviewPlayback ( ) ;
} ) ;
} ) ;
}
}
function startPreviewPlayback ( operation : PreviewOperation ) {
if ( ! runtime ? . context . modelObject || ! operation . parsedCraftPlayData ) {
notify ( "模型还未加载完成" ) ;
return ;
}
if ( previewPlayback . isPlaying && previewPlayback . operation === operation ) {
return ;
}
if ( previewPlayback . operation === operation && previewPlayback . currentTimeMs > 0 && previewPlayback . currentTimeMs < previewPlayback . totalTimeMs ) {
resumePreviewPlayback ( ) ;
return ;
}
resetPreviewPlaybackScene ( ) ;
const frames = operation . parsedCraftPlayData . OPERATION . frames ;
const frameTimesMs = buildPreviewFrameTimes ( frames ) ;
previewPlayback = {
operation ,
isPlaying : true ,
rafId : null ,
startedAtMs : performance.now ( ) ,
baseTimeMs : 0 ,
currentTimeMs : 0 ,
currentFrameIndex : - 1 ,
frameTimesMs ,
totalTimeMs : frameTimesMs [ frameTimesMs . length - 1 ] ? ? 0 ,
objectSnapshots : capturePreviewObjectSnapshots ( runtime . context . modelObject ) ,
objectMap : buildPreviewObjectMap ( runtime . context . modelObject , operation ) ,
attachedOriginalParents : new Map ( ) ,
rotatingObjects : new Map ( )
} ;
setPlaybackButtons ( true ) ;
updatePreviewPlaybackStatus ( operation , 0 ) ;
previewPlayback . rafId = window . requestAnimationFrame ( stepPreviewPlayback ) ;
}
function pausePreviewPlayback() {
if ( ! previewPlayback . isPlaying ) return ;
refreshPreviewPlaybackClock ( ) ;
cancelPreviewPlaybackRaf ( ) ;
previewPlayback . isPlaying = false ;
setPlaybackButtons ( false ) ;
updatePreviewPlaybackStatus ( previewPlayback . operation , previewPlayback . currentTimeMs ) ;
}
function resumePreviewPlayback() {
if ( ! previewPlayback . operation || previewPlayback . isPlaying ) return ;
if ( previewPlayback . currentTimeMs >= previewPlayback . totalTimeMs ) {
resetPreviewPlaybackScene ( ) ;
previewPlayback . currentTimeMs = 0 ;
previewPlayback . currentFrameIndex = - 1 ;
previewPlayback . baseTimeMs = 0 ;
}
previewPlayback . isPlaying = true ;
previewPlayback . startedAtMs = performance . now ( ) ;
previewPlayback . baseTimeMs = previewPlayback . currentTimeMs ;
setPlaybackButtons ( true ) ;
previewPlayback . rafId = window . requestAnimationFrame ( stepPreviewPlayback ) ;
}
function stopPreviewPlayback() {
cancelPreviewPlaybackRaf ( ) ;
resetPreviewPlaybackScene ( ) ;
previewPlayback = createEmptyPlaybackState ( ) ;
setPlaybackButtons ( false ) ;
}
function stepPreviewPlayback ( timestamp : number ) {
if ( ! previewPlayback . isPlaying || ! previewPlayback . operation ? . parsedCraftPlayData || ! runtime ? . context . modelObject ) return ;
const nextTimeMs = Math . min (
previewPlayback . baseTimeMs + Math . max ( 0 , timestamp - previewPlayback . startedAtMs ) ,
previewPlayback . totalTimeMs
) ;
applyPreviewContinuousRotations ( nextTimeMs ) ;
previewPlayback . currentTimeMs = nextTimeMs ;
const nextFrameIndex = resolvePreviewFrameIndex ( previewPlayback . currentTimeMs , previewPlayback . frameTimesMs ) ;
applyPreviewFramesUntil ( nextFrameIndex ) ;
updatePreviewPlaybackStatus ( previewPlayback . operation , previewPlayback . currentTimeMs ) ;
if ( previewPlayback . currentTimeMs >= previewPlayback . totalTimeMs ) {
cancelPreviewPlaybackRaf ( ) ;
previewPlayback . isPlaying = false ;
setPlaybackButtons ( false ) ;
return ;
}
previewPlayback . rafId = window . requestAnimationFrame ( stepPreviewPlayback ) ;
}
function refreshPreviewPlaybackClock() {
if ( ! previewPlayback . isPlaying ) return ;
previewPlayback . currentTimeMs = Math . min (
previewPlayback . baseTimeMs + Math . max ( 0 , performance . now ( ) - previewPlayback . startedAtMs ) ,
previewPlayback . totalTimeMs
) ;
previewPlayback . baseTimeMs = previewPlayback . currentTimeMs ;
previewPlayback . startedAtMs = performance . now ( ) ;
}
function applyPreviewFramesUntil ( nextFrameIndex : number ) {
const frames = previewPlayback . operation ? . parsedCraftPlayData ? . OPERATION . frames ? ? [ ] ;
if ( nextFrameIndex <= previewPlayback . currentFrameIndex ) return ;
for ( let index = previewPlayback . currentFrameIndex + 1 ; index <= nextFrameIndex && index < frames . length ; index += 1 ) {
applyPreviewFrame ( frames [ index ] ) ;
}
previewPlayback . currentFrameIndex = Math . min ( nextFrameIndex , frames . length - 1 ) ;
}
function applyPreviewFrame ( frame : P_OPERATION [ "OPERATION" ] [ "frames" ] [ number ] ) {
for ( const attachData of frame . attachs ? ? [ ] ) {
if ( attachData . IsAttach ) {
applyPreviewAttach ( attachData . AttachToModelCode , attachData . List_AttachModel ? ? [ ] ) ;
}
}
for ( const visibleData of frame . visibles ? ? [ ] ) {
const object = findPreviewObject ( visibleData . ModelCode ) ;
if ( object ) object . visible = parseInteger ( visibleData . Visible ) === 1 ;
}
for ( const craftData of frame . crafts ? ? [ ] ) {
if ( parseInteger ( craftData . CraftValue ) === 1 ) {
const operation = findPreviewOperation ( craftData . CraftCode ) ;
if ( operation ? . parsedCraftPlayData ) {
applyPreviewFrame ( operation . parsedCraftPlayData . OPERATION . frames [ 0 ] ) ;
}
}
}
for ( const rotationData of frame . rotations ? ? [ ] ) {
applyPreviewRotation ( rotationData . ModelCode , rotationData . Axis , Number ( rotationData . Rate ) ) ;
}
for ( const moveData of frame . objStates ? ? [ ] ) {
applyPreviewObjState ( moveData ) ;
}
for ( const attachData of frame . attachs ? ? [ ] ) {
if ( ! attachData . IsAttach ) {
detachPreviewAttach ( attachData . List_AttachModel ? ? [ ] ) ;
}
}
}
function applyPreviewObjState ( moveData : P_OPERATION [ "OPERATION" ] [ "frames" ] [ number ] [ "objStates" ] [ number ] ) {
const object = findPreviewObject ( moveData . i ) ;
if ( ! object || ! isValidFrameTransform ( moveData ) ) return ;
const position = parsePreviewNumbers ( moveData . tx , moveData . ty , moveData . tz ) ;
const quaternion = parsePreviewNumbers ( moveData . qx , moveData . qy , moveData . qz , moveData . qw ) ;
if ( ! position || ! quaternion ) return ;
object . position . set ( position [ 0 ] , position [ 1 ] , position [ 2 ] ) ;
object . quaternion . set ( quaternion [ 0 ] , quaternion [ 1 ] , quaternion [ 2 ] , quaternion [ 3 ] ) ;
object . updateMatrixWorld ( ) ;
}
function applyPreviewRotation ( modelCode : string , axisValue : string , rate : number ) {
const object = findPreviewObject ( modelCode ) ;
const axis = normalizeAxis ( axisValue ) ;
if ( ! object || ! axis || ! Number . isFinite ( rate ) ) return ;
if ( Math . abs ( rate ) <= 0.000001 ) {
previewPlayback . rotatingObjects . delete ( object ) ;
return ;
}
previewPlayback . rotatingObjects . set ( object , { axis , rate } ) ;
}
function applyPreviewContinuousRotations ( nextTimeMs : number ) {
if ( previewPlayback . rotatingObjects . size === 0 ) return ;
const deltaSeconds = Math . max ( 0 , nextTimeMs - previewPlayback . currentTimeMs ) / SECOND_MS ;
for ( const [ object , rotation ] of previewPlayback . rotatingObjects ) {
object . rotation [ rotation . axis ] += rotation . rate * deltaSeconds ;
object . updateMatrixWorld ( ) ;
}
}
function applyPreviewAttach ( rootCode : string , attachModels : Array < { ModelCode : string } > ) {
const root = findPreviewObject ( rootCode ) ;
if ( ! root ) return ;
for ( const attachModel of attachModels ) {
const object = findPreviewObject ( attachModel . ModelCode ) ;
if ( ! object || object === root ) continue ;
if ( ! previewPlayback . attachedOriginalParents . has ( object ) ) {
previewPlayback . attachedOriginalParents . set ( object , object . parent ) ;
}
root . attach ( object ) ;
}
}
function detachPreviewAttach ( attachModels : Array < { ModelCode : string } > ) {
for ( const attachModel of attachModels ) {
const object = findPreviewObject ( attachModel . ModelCode ) ;
if ( ! object ) continue ;
const originalParent = previewPlayback . attachedOriginalParents . get ( object ) ;
if ( originalParent ) {
originalParent . attach ( object ) ;
} else {
runtime ? . scene . attach ( object ) ;
}
previewPlayback . attachedOriginalParents . delete ( object ) ;
}
}
function resetPreviewPlaybackScene() {
cancelPreviewPlaybackRaf ( ) ;
previewPlayback . rotatingObjects . clear ( ) ;
for ( const [ object , originalParent ] of previewPlayback . attachedOriginalParents ) {
if ( originalParent ) {
originalParent . attach ( object ) ;
}
}
previewPlayback . attachedOriginalParents . clear ( ) ;
for ( const [ object , snapshot ] of previewPlayback . objectSnapshots ) {
if ( snapshot . parent && object . parent !== snapshot . parent ) {
snapshot . parent . attach ( object ) ;
}
object . position . copy ( snapshot . position ) ;
object . quaternion . copy ( snapshot . quaternion ) ;
object . visible = snapshot . visible ;
object . updateMatrixWorld ( ) ;
}
}
function buildPreviewFrameTimes ( frames : P_OPERATION [ "OPERATION" ] [ "frames" ] ) {
let previousTime = 0 ;
return frames . map ( ( frame , index ) = > {
const parsed = Number ( frame . time ) ;
const timeMs = Number . isFinite ( parsed ) ? Math . max ( 0 , parsed * SECOND_MS ) : index * DEFAULT_PREVIEW_FRAME_INTERVAL_MS ;
previousTime = index === 0 ? timeMs : Math.max ( previousTime + 1 , timeMs ) ;
return previousTime ;
} ) ;
}
function resolvePreviewFrameIndex ( currentTimeMs : number , frameTimesMs : number [ ] ) {
let index = - 1 ;
for ( let i = 0 ; i < frameTimesMs . length ; i += 1 ) {
if ( currentTimeMs >= frameTimesMs [ i ] ) index = i ;
else break ;
}
return index ;
}
function buildPreviewObjectMap ( root : Object3D , operation : PreviewOperation ) {
const objectMap = new Map < string , Object3D > ( ) ;
root . traverse ( ( object ) = > {
const values = [
object . userData ? . ModelId ,
object . userData ? . ModelCode ,
object . userData ? . RootId ,
object . userData ? . id ,
object . name
] ;
for ( const value of values ) {
const key = normalizePreviewCode ( value ) ;
if ( key && ! objectMap . has ( key ) ) objectMap . set ( key , object ) ;
}
} ) ;
const targetCodes = collectPreviewOperationModelCodes ( operation ) ;
if ( targetCodes . length > 0 && ! targetCodes . some ( code = > objectMap . has ( code ) ) ) {
for ( const code of targetCodes ) objectMap . set ( code , root ) ;
}
return objectMap ;
}
function capturePreviewObjectSnapshots ( root : Object3D ) {
const snapshots = new Map < Object3D , PreviewObjectSnapshot > ( ) ;
root . traverse ( ( object ) = > {
snapshots . set ( object , {
position : object.position.clone ( ) ,
quaternion : object.quaternion.clone ( ) ,
visible : object.visible ,
parent : object.parent
} ) ;
} ) ;
return snapshots ;
}
function collectPreviewOperationModelCodes ( operation : PreviewOperation ) {
const codes = new Set < string > ( ) ;
if ( operation . ModelCode ) codes . add ( normalizePreviewCode ( operation . ModelCode ) ) ;
if ( operation . ModelCodeParent ) codes . add ( normalizePreviewCode ( operation . ModelCodeParent ) ) ;
for ( const frame of operation . parsedCraftPlayData ? . OPERATION . frames ? ? [ ] ) {
for ( const state of frame . objStates ? ? [ ] ) codes . add ( normalizePreviewCode ( state . i ) ) ;
for ( const item of frame . visibles ? ? [ ] ) codes . add ( normalizePreviewCode ( item . ModelCode ) ) ;
for ( const item of frame . rotations ? ? [ ] ) codes . add ( normalizePreviewCode ( item . ModelCode ) ) ;
for ( const item of frame . attachs ? ? [ ] ) {
codes . add ( normalizePreviewCode ( item . AttachToModelCode ) ) ;
for ( const child of item . List_AttachModel ? ? [ ] ) codes . add ( normalizePreviewCode ( child . ModelCode ) ) ;
}
}
return [ . . . codes ] . filter ( Boolean ) ;
}
function findPreviewObject ( modelCode : string ) {
const code = normalizePreviewCode ( modelCode ) ;
return code ? previewPlayback . objectMap . get ( code ) ? ? null : null ;
}
function findPreviewOperation ( craftCode : string ) {
const code = normalizePreviewCode ( craftCode ) ;
if ( ! code || ! previewPlayback . operation ) return null ;
return previewPlayback . operation . CraftCode === code ? previewPlayback.operation : null ;
}
function parsePreviewNumbers ( . . . valuesRaw : string [ ] ) {
const values = valuesRaw . map ( Number ) ;
if ( values . some ( value = > ! Number . isFinite ( value ) ) ) return null ;
return values ;
}
function isValidFrameTransform ( moveData : P_OPERATION [ "OPERATION" ] [ "frames" ] [ number ] [ "objStates" ] [ number ] ) {
return [ moveData . tx , moveData . ty , moveData . tz , moveData . qx , moveData . qy , moveData . qz , moveData . qw ]
. every ( value = > value !== "NaN" && value !== "" ) ;
}
function normalizeAxis ( axis : string ) : "x" | "y" | "z" | null {
const value = axis . trim ( ) . toLowerCase ( ) ;
return value === "x" || value === "y" || value === "z" ? value : null ;
}
function parseInteger ( value : string ) {
const parsed = Number . parseInt ( value , 10 ) ;
return Number . isFinite ( parsed ) ? parsed : 0 ;
}
function normalizePreviewCode ( value : unknown ) {
return String ( value ? ? "" ) . trim ( ) ;
}
function updatePreviewPlaybackStatus ( operation : PreviewOperation | null , currentTimeMs : number ) {
const status = document . querySelector < HTMLDivElement > ( "#previewProcessStatus" ) ;
if ( ! status || ! operation ) return ;
const total = Math . max ( previewPlayback . totalTimeMs , 0 ) ;
const frameText = previewPlayback . currentFrameIndex >= 0
? ` ${ previewPlayback . currentFrameIndex + 1 } / ${ operation . frameCount } `
: ` 0/ ${ operation . frameCount } ` ;
status . innerHTML = ` 播放 ${ frameText } , ${ formatPlaybackSeconds ( currentTimeMs ) } / ${ formatPlaybackSeconds ( total ) } s ` ;
}
function setPlaybackButtons ( isPlaying : boolean ) {
const playButton = document . querySelector < HTMLButtonElement > ( "#previewPlayBtn" ) ;
const pauseButton = document . querySelector < HTMLButtonElement > ( "#previewPauseBtn" ) ;
if ( playButton ) playButton . textContent = isPlaying ? "播放中" : "播放" ;
if ( pauseButton ) pauseButton . disabled = ! isPlaying ;
}
function cancelPreviewPlaybackRaf() {
if ( previewPlayback . rafId !== null ) {
window . cancelAnimationFrame ( previewPlayback . rafId ) ;
previewPlayback . rafId = null ;
}
}
function createEmptyPlaybackState ( ) : PreviewPlaybackState {
return {
operation : null ,
isPlaying : false ,
rafId : null ,
startedAtMs : 0 ,
baseTimeMs : 0 ,
currentTimeMs : 0 ,
currentFrameIndex : - 1 ,
frameTimesMs : [ ] ,
totalTimeMs : 0 ,
objectSnapshots : new Map ( ) ,
objectMap : new Map ( ) ,
attachedOriginalParents : new Map ( ) ,
rotatingObjects : new Map ( )
} ;
}
function formatPlaybackSeconds ( ms : number ) {
return ( Math . max ( 0 , ms ) / SECOND_MS ) . toFixed ( 2 ) ;
}
async function initPreview ( url : string , context : PreviewLoadContext ) {
async function initPreview ( url : string , context : PreviewLoadContext ) {
ensurePreviewContext ( context ) ;
ensurePreviewContext ( context ) ;
const viewport = document . querySelector < HTMLDivElement > ( "#modelPreviewViewport" ) ;
const viewport = document . querySelector < HTMLDivElement > ( "#modelPreviewViewport" ) ;