接入模型预览工艺播放
This commit is contained in:
@@ -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,
|
||||||
@@ -406,6 +432,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;
|
||||||
@@ -581,6 +608,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 +628,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");
|
||||||
|
|||||||
Reference in New Issue
Block a user