Commit a9dcf2ed by xhw

uploadImage

parent 3a29c1d7
...@@ -43,7 +43,7 @@ export class SparkSplatViewer { ...@@ -43,7 +43,7 @@ export class SparkSplatViewer {
// Spark渲染器配置 // Spark渲染器配置
maxStdDev: options.maxStdDev ?? Math.sqrt(5), maxStdDev: options.maxStdDev ?? Math.sqrt(5),
sortRadial: options.sortRadial ?? true, sortRadial: options.sortRadial ?? false,
enableLod: options.enableLod ?? true, enableLod: options.enableLod ?? true,
lodSplatScale: options.lodSplatScale ?? 1.0, lodSplatScale: options.lodSplatScale ?? 1.0,
...@@ -117,11 +117,17 @@ export class SparkSplatViewer { ...@@ -117,11 +117,17 @@ export class SparkSplatViewer {
antialias: false, antialias: false,
alpha: false, alpha: false,
canvas: options.canvas || undefined, canvas: options.canvas || undefined,
powerPreference: "high-performance", // 强制独显
depth: false, // 3DGS 不需要 depth
stencil: false,
}); });
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight); this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1));
this.renderer.toneMapping = THREE.ACESFilmicToneMapping; // this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
this.renderer.toneMappingExposure = 1; // this.renderer.toneMappingExposure = 1;
// 改为:
this.renderer.toneMapping = THREE.NoToneMapping; // 或 LinearToneMapping
if (!options.canvas) { if (!options.canvas) {
this.container.appendChild(this.renderer.domElement); this.container.appendChild(this.renderer.domElement);
...@@ -293,17 +299,35 @@ export class SparkSplatViewer { ...@@ -293,17 +299,35 @@ export class SparkSplatViewer {
_startRenderLoop() { _startRenderLoop() {
const animate = () => { const animate = () => {
if (this.isDestroyed) return; if (this.isDestroyed) return;
requestAnimationFrame(animate); // 先预约下一帧,避免掉帧
const deltaTime = this._clock.getDelta(); const deltaTime = this._clock.getDelta();
// 自动旋转(仅在用户未交互时) // 自动旋转
if (this.options.autoRotate && !this._isUserInteracting) { if (this.options.autoRotate && !this._isUserInteracting) {
this._doAutoRotate(deltaTime); this._doAutoRotate(deltaTime);
this._needsRender = true;
} }
// 控制器阻尼更新
if (this.controls.enableDamping) {
const oldPos = this._lastCameraPos.clone();
const oldQuat = this._lastCameraQuat.clone();
this.controls.update();
if (!this.camera.position.equals(oldPos) || !this.camera.quaternion.equals(oldQuat)) {
this._needsRender = true;
this._lastCameraPos.copy(this.camera.position);
this._lastCameraQuat.copy(this.camera.quaternion);
}
} else {
this.controls.update(); this.controls.update();
}
// 只在需要时渲染
if (this._needsRender) {
this.renderer.render(this.scene, this.camera); this.renderer.render(this.scene, this.camera);
requestAnimationFrame(animate); this._needsRender = false;
}
}; };
requestAnimationFrame(animate); requestAnimationFrame(animate);
} }
...@@ -316,6 +340,7 @@ export class SparkSplatViewer { ...@@ -316,6 +340,7 @@ export class SparkSplatViewer {
this.camera.aspect = w / h; this.camera.aspect = w / h;
this.camera.updateProjectionMatrix(); this.camera.updateProjectionMatrix();
this.renderer.setSize(w, h); this.renderer.setSize(w, h);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.0)); // ← 保持限制
} }
/** 获取设备类型 */ /** 获取设备类型 */
......
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
#upload-toast.success { background: rgba(34, 139, 74, 0.95); } #upload-toast.success { background: rgba(34, 139, 74, 0.95); }
#upload-toast.error { background: rgba(178, 48, 48, 0.95); } #upload-toast.error { background: rgba(178, 48, 48, 0.95); }
#upload-toast.loading { background: rgba(40, 40, 60, 0.92); } #upload-toast.loading { background: rgba(40, 40, 60, 0.92); }
/* Ctrl+O 快捷键提示(不突兀:角落常驻、低透明度) */ /* Ctrl+O 快捷键提示 */
#open-file-hint { #open-file-hint {
position: fixed; position: fixed;
right: 16px; right: 16px;
...@@ -77,17 +77,81 @@ ...@@ -77,17 +77,81 @@
font-family: inherit; font-family: inherit;
font-size: 11px; font-size: 11px;
} }
/* FPS 计数器 */
#fps-counter {
position: fixed;
top: 14px;
left: 14px;
padding: 5px 12px;
border-radius: 6px;
background: rgba(20, 20, 30, 0.75);
color: #00ff88;
font-family: 'Courier New', monospace;
font-size: 13px;
font-weight: bold;
z-index: 10000;
pointer-events: none;
letter-spacing: 0.5px;
border: 1px solid rgba(0, 255, 136, 0.2);
}
/* 后端生成模型按钮 */
#backend-generate-btn {
position: fixed;
top: 14px;
right: 14px;
padding: 8px 16px;
border-radius: 8px;
border: 1px solid rgba(100, 150, 255, 0.35);
background: rgba(30, 40, 70, 0.75);
color: #aaccff;
font-size: 13px;
font-family: inherit;
cursor: pointer;
z-index: 10001;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 6px;
backdrop-filter: blur(4px);
}
#backend-generate-btn:hover {
background: rgba(40, 60, 110, 0.9);
color: #fff;
border-color: rgba(100, 150, 255, 0.7);
transform: translateY(-1px);
}
#backend-generate-btn:active {
transform: translateY(0);
}
#backend-generate-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
#backend-generate-btn .spinner {
display: inline-block;
width: 12px;
height: 12px;
border: 2px solid rgba(170, 204, 255, 0.3);
border-top-color: #aaccff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style> </style>
<base target="_blank"> <base target="_blank">
<base target="_blank"> <base target="_blank">
<base target="_blank">
<base target="_blank">
<base target="_blank">
<base target="_blank">
<base target="_blank">
</head> </head>
<body> <body>
<div id="viewer-container"></div> <div id="viewer-container"></div>
<!-- FPS 显示 -->
<div id="fps-counter">FPS: 0</div>
<!-- 后端生成模型按钮 -->
<button id="backend-generate-btn" title="选择图片,上传到后端生成 3D 模型">🖼️ 上传图片生成模型</button>
<!-- 隐藏的图片选择框(后端生成用) -->
<input type="file" id="image-file-input" accept="image/jpeg,image/jpg,image/png,image/webp,image/bmp" style="display:none" />
<!-- Ctrl+O 本地上传:隐藏文件选择框 + 结果提示 --> <!-- Ctrl+O 本地上传:隐藏文件选择框 + 结果提示 -->
<input type="file" id="spz-file-input" accept=".spz" style="display:none" /> <input type="file" id="spz-file-input" accept=".spz" style="display:none" />
<div id="upload-toast"></div> <div id="upload-toast"></div>
...@@ -96,40 +160,116 @@ ...@@ -96,40 +160,116 @@
<script type="module"> <script type="module">
import { SparkSplatViewer } from "./SparkSplatViewer.js"; import { SparkSplatViewer } from "./SparkSplatViewer.js";
// 初始化组件 // ═══════════════════════════════════════════════════
// 后端接口配置
// ═══════════════════════════════════════════════════
const BACKEND_CONFIG = {
// JWT 登录接口
loginUrl: 'https://admin.mindepoch.com:9999/VegaLoginJWT',
// 生成模型接口
predictUrl: 'https://admin.mindepoch.com:9999/GSPredict',
// 登录凭据
credentials: {
username: 'Vega001',
password: '88888888abc',
},
};
// Token 缓存(内存 + localStorage 双保险)
let cachedToken = null;
function getStoredToken() {
if (cachedToken) return cachedToken;
try {
return localStorage.getItem('vega_jwt_token') || null;
} catch (e) {
return null;
}
}
function setStoredToken(token) {
cachedToken = token;
try {
if (token) {
localStorage.setItem('vega_jwt_token', token);
} else {
localStorage.removeItem('vega_jwt_token');
}
} catch (e) {
// localStorage 不可用,仅内存缓存
}
}
function clearStoredToken() {
cachedToken = null;
try {
localStorage.removeItem('vega_jwt_token');
} catch (e) {}
}
// ═══════════════════════════════════════════════════
// JWT 登录:获取 Access Token
// ═══════════════════════════════════════════════════
/**
* 调用 VegaLoginJWT 接口获取 token
* @returns {Promise<string>} JWT token
*/
async function loginAndGetToken() {
showToast('正在登录获取 Token…', 'loading', 0);
console.log('[Auth] POST', BACKEND_CONFIG.loginUrl);
const response = await fetch(BACKEND_CONFIG.loginUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(BACKEND_CONFIG.credentials),
});
if (!response.ok) {
throw new Error(`登录失败 HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
console.log('[Auth] 登录返回:', data);
// 兼容多种返回格式:data.token / data.data.token / data.access_token
const token = data.token || data.access_token || (data.data && data.data.token) || (data.data && data.data.access_token);
if (!token) {
throw new Error(`登录成功但未返回 token,返回内容: ${JSON.stringify(data)}`);
}
setStoredToken(token);
showToast('登录成功,Token 已获取', 'success', 1500);
console.log('[Auth] Token 已缓存');
return token;
}
// ═══════════════════════════════════════════════════
// 初始化 3D 渲染器
// ═══════════════════════════════════════════════════
const container = document.getElementById("viewer-container"); const container = document.getElementById("viewer-container");
const viewer = new SparkSplatViewer(container, { const viewer = new SparkSplatViewer(container, {
// 场景配置
backgroundColor: 0x1a1a2e, backgroundColor: 0x1a1a2e,
// 相机配置
fov: 45, fov: 45,
fovMobile: 20, fovMobile: 20,
initialPosition: { x: 0, y: 0, z: 5 }, initialPosition: { x: 0, y: 0, z: 5 },
// 控制器配置
enablePan: false, enablePan: false,
minPolarAngle: Math.PI / 2 - Math.PI / 6, minPolarAngle: Math.PI / 2 - Math.PI / 6,
maxPolarAngle: Math.PI / 2 + Math.PI / 6, maxPolarAngle: Math.PI / 2 + Math.PI / 6,
minAzimuthAngle: -Math.PI / 10, minAzimuthAngle: -Math.PI / 10,
maxAzimuthAngle: Math.PI / 10, maxAzimuthAngle: Math.PI / 10,
autoRotate: true,
// 自动旋转配置(新增) autoRotateSpeed: 0.03,
autoRotate: true, // 开启自动旋转 autoRotateIdleDelay: 2000,
autoRotateSpeed: 0.1, // 速度(推荐 0.2~0.5)
autoRotateIdleDelay: 2000, // 空闲多久后恢复(ms)
// 模型配置
defaultModelUrl: "./Advance.spz", defaultModelUrl: "./Advance.spz",
modelRotationX: -Math.PI, modelRotationX: -Math.PI,
// 缩放配置
desktopZoom: 2.5, desktopZoom: 2.5,
mobileZoom: 0.55, mobileZoom: 0.55,
tabletZoom: 1.0, tabletZoom: 1.0,
// 回调
onLoad: (mesh) => { onLoad: (mesh) => {
console.log("默认模型加载完成", mesh); console.log("默认模型加载完成", mesh);
}, },
...@@ -138,29 +278,32 @@ ...@@ -138,29 +278,32 @@
}, },
}); });
// 暴露到全局,方便调试和外部调用
window.viewer = viewer; window.viewer = viewer;
// ═══════════════════════════════════════════════════ // ═══════════════════════════════════════════════════
// Ctrl+O 快捷键:本地选择 .spz 文件上传并加载 // FPS 实时显示
// ═══════════════════════════════════════════════════ // ═══════════════════════════════════════════════════
const fpsCounter = document.getElementById('fps-counter');
let frameCount = 0;
let fpsLastTime = performance.now();
(function fpsLoop() {
frameCount++;
const now = performance.now();
if (now - fpsLastTime >= 1000) {
fpsCounter.textContent = `FPS: ${frameCount}`;
frameCount = 0;
fpsLastTime = now;
}
requestAnimationFrame(fpsLoop);
})();
const spzFileInput = document.getElementById("spz-file-input"); // ═══════════════════════════════════════════════════
const openFileHint = document.getElementById("open-file-hint"); // Toast 提示系统
// ═══════════════════════════════════════════════════
// 页面加载完成后淡入快捷键提示
requestAnimationFrame(() => openFileHint.classList.add("visible"));
// 点击提示也可以打开文件选择框
openFileHint.addEventListener("click", () => spzFileInput.click());
const uploadToast = document.getElementById("upload-toast"); const uploadToast = document.getElementById("upload-toast");
let toastTimer = null; let toastTimer = null;
/**
* 显示上传结果提示
* @param {string} msg - 提示内容
* @param {"success"|"error"|"loading"} type - 类型
* @param {number} duration - 自动隐藏时长(ms),0 表示不自动隐藏
*/
function showToast(msg, type = "loading", duration = 2500) { function showToast(msg, type = "loading", duration = 2500) {
uploadToast.textContent = msg; uploadToast.textContent = msg;
uploadToast.className = "show " + type; uploadToast.className = "show " + type;
...@@ -172,7 +315,185 @@ ...@@ -172,7 +315,185 @@
} }
} }
// Ctrl+O 打开本地文件选择框(阻止浏览器默认的"打开文件"行为) // ═══════════════════════════════════════════════════
// 后端接口:上传图片 → 生成模型 → 自动加载(含自动登录)
// ═══════════════════════════════════════════════════
const backendBtn = document.getElementById("backend-generate-btn");
const imageFileInput = document.getElementById("image-file-input");
let isBackendLoading = false;
/**
* 调用 GSPredict 接口上传图片生成模型
* 自动处理:无 token → 先登录 → 再请求 → token 过期 → 重新登录重试
*
* @param {File} imageFile - 用户选择的图片文件
* @param {boolean} [retryOnAuthFail=true] - token 失效时是否自动重试一次
*/
async function generateModelFromImage(imageFile, retryOnAuthFail = true) {
if (isBackendLoading) return;
isBackendLoading = true;
backendBtn.disabled = true;
backendBtn.innerHTML = '<span class="spinner"></span> 生成中…';
let token = getStoredToken();
// 如果没有 token,先登录
if (!token) {
try {
token = await loginAndGetToken();
} catch (err) {
showToast(`登录失败: ${err.message}`, 'error');
isBackendLoading = false;
backendBtn.disabled = false;
backendBtn.innerHTML = '🖼️ 上传图片生成模型';
return;
}
}
const url = BACKEND_CONFIG.predictUrl;
const formData = new FormData();
formData.append('image', imageFile);
showToast(`正在上传图片: ${imageFile.name}…`, 'loading', 0);
console.log('[Backend] POST', url, 'file:', imageFile.name, 'size:', imageFile.size);
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
},
body: formData,
});
// 处理 HTTP 401/403 或 code 408(缺少 token)→ 清除缓存并重试
if (response.status === 401 || response.status === 403) {
throw new Error('AUTH_FAIL');
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
console.log('[Backend] 接口返回:', data);
// 业务层鉴权失败(code 408: 缺少访问token)
if (data.code === 408) {
throw new Error('AUTH_FAIL');
}
// 校验返回状态码(兼容 code: 0 和 code: 200)
const code = data.code;
if (code !== 200 && code !== 0) {
throw new Error(data.message || `后端返回错误码: ${code}`);
}
// 提取下载链接
let downloadUrl = data.download_url || data.file_url;
if (!downloadUrl) {
throw new Error('后端未返回 download_url 或 file_url');
}
// 后端返回的是相对路径(如 /GSFiles/xxx.spz?...),拼接完整域名
if (downloadUrl.startsWith('/')) {
downloadUrl = 'https://admin.mindepoch.com:9999' + downloadUrl;
}
const filename = data.filename || data.stored_filename || 'model.spz';
const width = data.width;
const height = data.height;
const inferenceSec = data.inference_seconds;
showToast(
`生成成功: ${filename}${width ? ` (${width}x${height})` : ''}` +
`${inferenceSec ? ` 推理 ${inferenceSec.toFixed(1)}s` : ''},开始加载…`,
'success',
3000
);
// 加载模型
window.loadNewModel(downloadUrl, {
autoFit: true,
onBeforeLoad: () => {
showToast(`正在下载模型: ${filename}…`, 'loading', 0);
},
onComplete: (mesh) => {
showToast(`模型 ${filename} 加载完成`, 'success');
console.log('[Backend] 模型加载完成:', filename, mesh);
},
onError: (err) => {
showToast(`模型加载失败: ${err.message || err}`, 'error');
console.error('[Backend] 模型加载失败:', err);
},
onProgress: (e) => {
if (e && e.lengthComputable) {
const pct = ((e.loaded / e.total) * 100).toFixed(0);
showToast(`下载模型: ${filename}${pct}%`, 'loading', 0);
}
},
});
} catch (err) {
// token 失效,自动重新登录并重试一次
if (err.message === 'AUTH_FAIL' && retryOnAuthFail) {
console.warn('[Auth] Token 失效,尝试重新登录…');
clearStoredToken();
isBackendLoading = false;
backendBtn.disabled = false;
backendBtn.innerHTML = '🖼️ 上传图片生成模型';
// 递归调用,但 retryOnAuthFail = false 防止无限循环
return generateModelFromImage(imageFile, false);
}
console.error('[Backend] 请求失败:', err);
let msg = err.message || '未知错误';
if (msg.includes('Failed to fetch') || msg.includes('NetworkError')) {
msg += '(可能是跨域/CORS问题)';
}
showToast(`后端请求失败: ${msg}`, 'error');
} finally {
isBackendLoading = false;
backendBtn.disabled = false;
backendBtn.innerHTML = '🖼️ 上传图片生成模型';
}
}
// 暴露到全局
window.generateModelFromImage = generateModelFromImage;
window.loginAndGetToken = loginAndGetToken;
window.clearStoredToken = clearStoredToken;
// 按钮点击 → 打开图片选择框
backendBtn.addEventListener('click', () => {
imageFileInput.click();
});
// 选中图片后自动上传并生成
imageFileInput.addEventListener('change', async () => {
const file = imageFileInput.files && imageFileInput.files[0];
imageFileInput.value = ''; // 允许重复选择同一文件
if (!file) return;
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/bmp'];
if (!validTypes.includes(file.type)) {
showToast(`不支持的图片格式: ${file.type},请上传 jpg/png/webp/bmp`, 'error');
return;
}
await generateModelFromImage(file);
});
// ═══════════════════════════════════════════════════
// Ctrl+O 快捷键:本地选择 .spz 文件上传并加载
// ═══════════════════════════════════════════════════
const spzFileInput = document.getElementById("spz-file-input");
const openFileHint = document.getElementById("open-file-hint");
requestAnimationFrame(() => openFileHint.classList.add("visible"));
openFileHint.addEventListener("click", () => spzFileInput.click());
window.addEventListener("keydown", (e) => { window.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey && e.code === "KeyO") { if ((e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey && e.code === "KeyO") {
e.preventDefault(); e.preventDefault();
...@@ -180,10 +501,9 @@ ...@@ -180,10 +501,9 @@
} }
}); });
// 选中文件后:校验后缀 → 加载 → 提示结果
spzFileInput.addEventListener("change", async () => { spzFileInput.addEventListener("change", async () => {
const file = spzFileInput.files && spzFileInput.files[0]; const file = spzFileInput.files && spzFileInput.files[0];
spzFileInput.value = ""; // 允许重复选择同一个文件 spzFileInput.value = "";
if (!file) return; if (!file) return;
if (!/\.spz$/i.test(file.name)) { if (!/\.spz$/i.test(file.name)) {
...@@ -219,128 +539,59 @@ ...@@ -219,128 +539,59 @@
// 加载新模型的方法(供外部调用) // 加载新模型的方法(供外部调用)
// ═══════════════════════════════════════════════════ // ═══════════════════════════════════════════════════
/**
* 加载新模型并替换当前模型
* @param {string} url - 模型文件路径 (.spz 支持 gzip v1~v3 和 NGSP/ZSTD v4;另支持 .ply, .splat, .ksplat, .sog, .rad 等)
* @param {Object} options - 可选配置
*/
window.loadNewModel = (url, options = {}) => { window.loadNewModel = (url, options = {}) => {
viewer.loadModel(url, { viewer.loadModel(url, {
autoFit: options.autoFit !== false, // 默认自动适配相机 autoFit: options.autoFit !== false,
zoomDuration: options.zoomDuration ?? 500, // 缩放动画时长 zoomDuration: options.zoomDuration ?? 500,
desktopZoom: options.desktopZoom ?? 2.2, // 桌面端缩放 desktopZoom: options.desktopZoom ?? 2.2,
mobileZoom: options.mobileZoom ?? 0.55, // 移动端缩放 mobileZoom: options.mobileZoom ?? 0.55,
tabletZoom: options.tabletZoom ?? 1.0, // 平板端缩放 tabletZoom: options.tabletZoom ?? 1.0,
onBeforeLoad: options.onBeforeLoad, // 加载开始前回调(可显示 loading 避免白屏) onBeforeLoad: options.onBeforeLoad,
onComplete: options.onComplete, // 加载完成回调 onComplete: options.onComplete,
onError: options.onError, // 加载失败回调 onError: options.onError,
onProgress: options.onProgress, // 进度回调 onProgress: options.onProgress,
}); });
}; };
// 使用示例:
// loadNewModel("./new-model.spz");
// loadNewModel("./new-model.spz", { autoFit: true, desktopZoom: 3.0 });
//
// 避免白屏:在 onBeforeLoad 中显示 loading 遮罩
// loadNewModel("./new-model.spz", {
// onBeforeLoad: () => { document.getElementById('loading').style.display = 'flex'; },
// onComplete: () => { document.getElementById('loading').style.display = 'none'; },
// onError: () => { document.getElementById('loading').style.display = 'none'; },
// });
// ═══════════════════════════════════════════════════ // ═══════════════════════════════════════════════════
// 自动旋转控制方法(供外部调用) // 自动旋转控制方法(供外部调用)
// ═══════════════════════════════════════════════════ // ═══════════════════════════════════════════════════
/**
* 开启/关闭自动旋转
* @param {boolean} enabled - true 开启, false 关闭
*/
window.setAutoRotate = (enabled) => { window.setAutoRotate = (enabled) => {
viewer.setAutoRotate(enabled); viewer.setAutoRotate(enabled);
}; };
/**
* 获取当前自动旋转状态
* @returns {boolean}
*/
window.getAutoRotate = () => { window.getAutoRotate = () => {
return viewer.getAutoRotate(); return viewer.getAutoRotate();
}; };
/**
* 设置自动旋转速度
* @param {number} speed - 速度值(推荐 0.1 ~ 1.0)
*/
window.setAutoRotateSpeed = (speed) => { window.setAutoRotateSpeed = (speed) => {
viewer.setAutoRotateSpeed(speed); viewer.setAutoRotateSpeed(speed);
}; };
/**
* 获取当前自动旋转速度
* @returns {number}
*/
window.getAutoRotateSpeed = () => { window.getAutoRotateSpeed = () => {
return viewer.getAutoRotateSpeed(); return viewer.getAutoRotateSpeed();
}; };
/**
* 设置空闲延迟时间
* @param {number} delayMs - 延迟时间(毫秒)
*/
window.setAutoRotateIdleDelay = (delayMs) => { window.setAutoRotateIdleDelay = (delayMs) => {
viewer.setAutoRotateIdleDelay(delayMs); viewer.setAutoRotateIdleDelay(delayMs);
}; };
/**
* 获取当前空闲延迟时间
* @returns {number}
*/
window.getAutoRotateIdleDelay = () => { window.getAutoRotateIdleDelay = () => {
return viewer.getAutoRotateIdleDelay(); return viewer.getAutoRotateIdleDelay();
}; };
/**
* 立即暂停自动旋转
*/
window.pauseAutoRotate = () => { window.pauseAutoRotate = () => {
viewer.pauseAutoRotate(); viewer.pauseAutoRotate();
}; };
/**
* 恢复自动旋转
*/
window.resumeAutoRotate = () => { window.resumeAutoRotate = () => {
viewer.resumeAutoRotate(); viewer.resumeAutoRotate();
}; };
/**
* 切换自动旋转状态
* @returns {boolean} 切换后的状态
*/
window.toggleAutoRotate = () => { window.toggleAutoRotate = () => {
return viewer.toggleAutoRotate(); return viewer.toggleAutoRotate();
}; };
// 其他可用方法:
// viewer.resetCamera(); // 重置相机
// viewer.fitCameraToModel(); // 适配相机到模型
// viewer.getCameraInfo(); // 获取相机信息
// viewer.animateZoomTo(2.0, 800); // 平滑缩放到指定值
// viewer.destroy(); // 销毁组件
//
// 自动旋转控制:
// setAutoRotate(true/false); // 开启/关闭自动旋转
// toggleAutoRotate(); // 切换自动旋转状态
// setAutoRotateSpeed(0.5); // 设置旋转速度
// setAutoRotateIdleDelay(3000); // 设置空闲延迟(ms)
// pauseAutoRotate(); // 暂停自动旋转
// resumeAutoRotate(); // 恢复自动旋转
// 获取模型包围盒
// const bounds = viewer.getModelBounds();
</script> </script>
</body> </body>
</html> </html>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment