Files
bim_engine/demo/index.html
yuding 507112fcf9 feat: add camera switch, fix click event/dialog resize/map state sync
- fix(engine): adapt click handler to base engine array format data[0].url/ids
- feat(toolbar): add perspective/orthographic camera switch button with dynamic icon
- fix(dialog): clamp resize to container bounds to prevent overflow
- feat(demo): add auto-combine feature with robust URL parsing and validation
- fix(walk): close minimap on walk exit and sync map state between toolbar and walk panel
- fix(engine): correct MiniMap getstate() casing to match base engine API
- build: rebuild demo libs
2026-03-05 17:43:50 +08:00

675 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>iFlow Engine Demo</title>
<!-- 从本地 lib 目录加载 SDK 文件 -->
<script src="./lib/iflow-engine.umd.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f5f5f5;
height: 100vh;
display: flex;
overflow: hidden;
/* 防止整个页面滚动 */
}
/* 左侧侧边栏:控制面板 */
.sidebar {
width: 320px;
background: white;
border-right: 1px solid #e0e0e0;
padding: 20px;
overflow-y: auto;
/* 内容过多时可滚动 */
display: flex;
flex-direction: column;
gap: 20px;
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.05);
z-index: 10;
}
.sidebar h1 {
font-size: 1.4rem;
color: #333;
margin-bottom: 10px;
}
.control-group {
background: #f9f9f9;
padding: 15px;
border-radius: 8px;
border: 1px solid #eee;
}
.control-group h2 {
font-size: 1rem;
color: #555;
margin-bottom: 10px;
padding-bottom: 5px;
border-bottom: 2px solid #e0e0e0;
}
.btn-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
button {
padding: 6px 12px;
font-size: 0.9rem;
border: 1px solid #ddd;
background: #fff;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
flex: 1 1 auto;
/* 按钮自动填充 */
}
button:hover {
background: #f0f0f0;
border-color: #ccc;
}
button.primary {
background: #0078d4;
color: white;
border-color: #0063b1;
}
button.primary:hover {
background: #0063b1;
}
/* 右侧主内容区:引擎展示 */
.main-content {
flex: 1;
position: relative;
background: #eef2f5;
display: flex;
justify-content: center;
align-items: center;
}
#app {
width: 100%;
height: 100%;
background: #fff;
position: relative;
}
</style>
</head>
<body>
<!-- 左侧控制面板 -->
<aside class="sidebar">
<h1>BIM SDK Demo</h1>
<!-- 1. 语言设置 -->
<div class="control-group">
<h2>🌍 语言 (Language)</h2>
<div class="btn-container">
<button class="primary" onclick="setLang('zh-CN')">中文</button>
<button class="primary" onclick="setLang('en-US')">English</button>
</div>
</div>
<!-- 2. 弹窗测试 -->
<div class="control-group">
<h2>🪟 弹窗 (Dialog)</h2>
<div class="btn-container">
<button onclick="openTestDialog()">测试弹窗</button>
<button onclick="openInfoDialog()">信息弹窗</button>
<button onclick="openRedDialog()">警告弹窗</button>
</div>
</div>
<!-- 3. 工具栏操作 -->
<div class="control-group">
<h2>🛠️ 工具栏 (Toolbar)</h2>
<div class="btn-container">
<button onclick="toggleToolbar()">显隐工具栏</button>
<button onclick="toggleLabel()">显隐标签</button>
<button onclick="toggleLocationBtn()">显隐定位按钮</button>
</div>
<div class="btn-container" style="margin-top: 8px;">
<button onclick="addCustomGroup()">加组</button>
<button onclick="addCustomButton()">加按钮</button>
</div>
<div class="btn-container" style="margin-top: 8px;">
<button onclick="setToolbarType('default')">默认样式</button>
<button onclick="setToolbarType('glass-pill')">胶囊样式</button>
</div>
</div>
<!-- 4. 样式主题 -->
<div class="control-group">
<h2>🎨 样式 (Theme)</h2>
<div class="btn-container">
<button onclick="setTheme('dark')">深色 (Dark)</button>
<button onclick="setTheme('light')">浅色 (Light)</button>
<button onclick="setCustomTheme()">自定义 (Red)</button>
</div>
</div>
<!-- 6. 功能面板 -->
<div class="control-group">
<h2>📑 功能面板 (Panels)</h2>
<div class="btn-container">
<button onclick="openPropertyPanel()">属性面板</button>
</div>
</div>
<!-- 7. 多Tab加载 -->
<div class="control-group">
<h2>📺 多Tab加载 (Multi-Tab)</h2>
<div class="btn-container">
<button class="primary" onclick="window.open('./multi-tab.html', '_blank')">打开多Tab视图</button>
</div>
<div style="margin-top: 8px; font-size: 0.8rem; color: #888;">
两个独立面板,各自加载模型,可拖拽分割线调整宽度
</div>
</div>
<!-- 8. 构件树数据 -->
<div class="control-group">
<h2>🌳 构件树 (Tree Data)</h2>
<div class="btn-container">
<button onclick="getLevelTree()">楼层树</button>
<button onclick="getTypeTree()">类型树</button>
<button onclick="getMajorTree()">专业树</button>
</div>
</div>
<!-- 5. 3D 引擎 -->
<div class="control-group">
<h2>🎮 3D 引擎 (Engine3D)</h2>
<div class="btn-container">
<button class="primary" onclick="initEngine3D()">初始化引擎</button>
<button class="primary" onclick="loadModel()">加载模型</button>
<button onclick="switchModel()">切换模型</button>
<button onclick="loadCombinedModel()">组合模型</button>
<button onclick="openCombineDialog()">自动组合</button>
</div>
<div class="btn-container" style="margin-top: 8px;">
<button onclick="pauseRendering()">暂停渲染</button>
<button onclick="resumeRendering()">恢复渲染</button>
</div>
<div style="margin-top: 10px; font-size: 0.85rem; color: #666;">
<div>状态: <span id="engine-status">未初始化</span></div>
</div>
</div>
</aside>
<!-- 右侧主区域 -->
<main class="main-content">
<div id="app"></div>
</main>
<!-- 自动组合弹窗 -->
<div id="combine-overlay" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,.45); z-index:9999; display:none; justify-content:center; align-items:center;">
<div style="background:#fff; border-radius:10px; padding:24px; width:520px; max-width:90vw; box-shadow:0 8px 32px rgba(0,0,0,.18);">
<h3 style="margin:0 0 12px; font-size:1.1rem; color:#333;">自动组合加载</h3>
<p style="margin:0 0 10px; font-size:.85rem; color:#888;">请输入模型 URL每行一个或用英文逗号分隔</p>
<textarea id="combine-urls" rows="6" style="width:100%; padding:10px; font-size:.85rem; border:1px solid #ddd; border-radius:6px; resize:vertical; font-family:monospace;" placeholder="https://example.com/model1/&#10;https://example.com/model2/"></textarea>
<div style="display:flex; justify-content:flex-end; gap:8px; margin-top:14px;">
<button onclick="closeCombineDialog()" style="min-width:72px;">取消</button>
<button class="primary" onclick="confirmCombineLoad()" style="min-width:72px;">确定加载</button>
</div>
</div>
</div>
<script>
let engine = null;
let isToolbarVisible = true;
let isLabelVisible = true;
let isLocationVisible = true;
let customGroupAdded = false;
let engine3DInitialized = false;
// 初始化引擎
window.onload = () => {
if (window.IflowEngine) {
const Engine = window.IflowEngine.BimEngine;
try {
engine = new Engine('app', { locale: 'zh-CN' });
initEngine3D();
console.log('Engine initialized:', engine);
} catch (err) {
console.error('Init failed:', err);
}
} else {
console.error('SDK not found');
}
};
// --- 语言设置 ---
function setLang(lang) {
if (engine) engine.setLocale(lang);
}
// --- 弹窗测试 ---
function openTestDialog() {
if (!engine || !engine.dialog) return;
engine.dialog.create({
title: 'dialog.testTitle',
width: 350,
height: 200,
position: 'center',
draggable: true,
resizable: true
});
}
function openInfoDialog() {
if (!engine || !engine.dialog) return;
engine.dialog.showInfoDialog();
}
function openRedDialog() {
if (!engine || !engine.dialog) return;
engine.dialog.create({
title: 'Alert',
content: '<div style="color: #ffcccc;">Critical Warning!</div>',
width: 300,
height: 150,
backgroundColor: 'rgba(100, 0, 0, 0.95)',
headerBackgroundColor: '#cc0000',
titleColor: '#ffffff',
borderColor: '#ff6666',
position: { x: 50, y: 50 }
});
}
// --- 工具栏操作 ---
function toggleToolbar() {
if (!engine || !engine.toolbar) return;
isToolbarVisible = !isToolbarVisible;
engine.toolbar.setVisible(isToolbarVisible);
}
function toggleLabel() {
if (!engine || !engine.toolbar) return;
isLabelVisible = !isLabelVisible;
engine.toolbar.setShowLabel(isLabelVisible);
}
function toggleLocationBtn() {
if (!engine || !engine.toolbar) return;
isLocationVisible = !isLocationVisible;
engine.toolbar.setButtonVisibility('location', isLocationVisible);
}
function addCustomGroup() {
if (!engine || !engine.toolbar) return;
if (customGroupAdded) {
alert('已添加过');
return;
}
engine.toolbar.addGroup('custom-group', 'group-1');
customGroupAdded = true;
}
function addCustomButton() {
if (!engine || !engine.toolbar) return;
if (!customGroupAdded) {
alert('Please add custom group first / 请先添加自定义组');
return;
}
const btnId = 'custom-btn-' + Date.now();
engine.toolbar.addButton({
id: btnId,
groupId: 'custom-group',
type: 'button',
label: 'New',
icon: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><path d="M8 14s1.5 2 4 2 4-2 4-2"></path><line x1="9" y1="9" x2="9.01" y2="9"></line><line x1="15" y1="9" x2="15.01" y2="9"></line></svg>',
onClick: (btn) => {
alert('Clicked: ' + btn.label);
}
});
}
function setToolbarType(type) {
if (!engine || !engine.toolbar) return;
engine.toolbar.setType(type);
console.log('Toolbar type changed to:', type);
}
// --- 主题操作 ---
function setTheme(themeName) {
if (engine) engine.setTheme(themeName);
}
function setCustomTheme() {
if (!engine) return;
// 定义一个红色主题
engine.setCustomTheme({
name: 'red-alert',
primary: '#d32f2f',
primaryHover: '#b71c1c',
background: '#ffebee', // 浅红背景
panelBackground: 'rgba(255, 255, 255, 0.9)',
textPrimary: '#b71c1c',
textSecondary: '#e57373',
border: '#ffcdd2',
icon: '#d32f2f',
iconActive: '#b71c1c',
componentBackground: 'rgba(255, 205, 210, 0.3)',
componentHover: 'rgba(255, 205, 210, 0.8)',
componentActive: '#e57373'
});
}
// --- 3D 引擎操作 ---
/**
* 初始化 3D 引擎
*/
function initEngine3D() {
if (!engine || !engine.engine) {
alert('引擎未创建,请先等待页面加载完成');
return;
}
if (engine.engine.isInitialized()) {
alert('3D 引擎已经初始化过了');
updateEngineStatus('已初始化');
return;
}
try {
// 初始化引擎,使用默认配置
const success = engine.engine.initialize({
backgroundColor: 0x333333, // 深色背景
version: 'v2', // WebGL 版本
showStats: false, // 显示性能统计
showViewCube: true // 显示视图立方体
});
if (success) {
engine3DInitialized = true;
updateEngineStatus('已初始化');
console.log('✅ 3D 引擎初始化成功');
loadModel();
} else {
updateEngineStatus('初始化失败');
console.error('❌ 3D 引擎初始化失败');
}
} catch (error) {
updateEngineStatus('初始化错误');
console.error('❌ 3D 引擎初始化错误:', error);
}
}
/**
* 加载 3D 模型
*/
function loadModel() {
if (!engine || !engine.engine) {
alert('引擎未创建,请先等待页面加载完成');
return;
}
if (!engine.engine.isInitialized()) {
alert('请先初始化 3D 引擎!');
return;
}
try {
// 加载模型文件(从 model 目录)
const modelUrl = 'https://lyz-1259524260.cos.ap-guangzhou.myqcloud.com/iflow/models/417664a3-76c8-4d94-9344-1337246a5d4e/';
engine.engine.loadModel([modelUrl], {
position: [0, 0, 0], // 初始位置
rotation: [0, 0, 0], // 初始旋转
scale: [1, 1, 1] // 初始缩放
});
console.log('✅ 模型加载请求已发送:', modelUrl);
} catch (error) {
console.error('❌ 模型加载错误:', error);
}
}
/**
* 加载组合模型 - 同时加载多个模型
*/
function loadCombinedModel() {
if (!engine || !engine.engine) {
alert('引擎未创建,请先等待页面加载完成');
return;
}
if (!engine.engine.isInitialized()) {
alert('请先初始化 3D 引擎!');
return;
}
try {
const modelUrls = [
'https://lyz-1259524260.cos.ap-guangzhou.myqcloud.com/iflow/models/974feae2-4be3-4e85-9d5e-ea655b0c890d/',
'https://lyz-1259524260.cos.ap-guangzhou.myqcloud.com/iflow/models/6129815d-9ae4-4414-a9ab-e7c3ee1b2584/',
'https://lyz-1259524260.cos.ap-guangzhou.myqcloud.com/iflow/models/8ddf95f4-28cc-4218-94c3-165e69fa51b1/'
];
engine.engine.loadModel(modelUrls, {
position: [0, 0, 0],
rotation: [0, 0, 0],
scale: [1, 1, 1]
});
console.log('✅ 组合模型加载请求已发送:', modelUrls);
} catch (error) {
console.error('❌ 组合模型加载错误:', error);
}
}
/**
* 暂停渲染
*/
function pauseRendering() {
if (!engine || !engine.engine || !engine.engine.isInitialized()) {
alert('请先初始化 3D 引擎!');
return;
}
engine.engine.pauseRendering();
console.log('✅ 渲染已暂停');
}
/**
* 恢复渲染
*/
function resumeRendering() {
if (!engine || !engine.engine || !engine.engine.isInitialized()) {
alert('请先初始化 3D 引擎!');
return;
}
engine.engine.resumeRendering();
console.log('✅ 渲染已恢复');
}
/**
* 切换模型 - 输入新的模型 URL
*/
function switchModel() {
if (!engine || !engine.engine) {
alert('引擎未创建,请先等待页面加载完成');
return;
}
if (!engine.engine.isInitialized()) {
alert('请先初始化 3D 引擎!');
return;
}
const newUrl = prompt('请输入新的模型 URL:', 'https://lyz-1259524260.cos.ap-guangzhou.myqcloud.com/iflow/models/8634e556-a94e-4ba7-be3e-2ea1507cced5/');
if (!newUrl || newUrl.trim() === '') {
return;
}
try {
engine.engine.loadModel([newUrl.trim()], {
position: [0, 0, 0],
rotation: [0, 0, 0],
scale: [1, 1, 1]
});
console.log('✅ 切换模型请求已发送:', newUrl);
} catch (error) {
console.error('❌ 切换模型错误:', error);
}
}
/**
* 打开属性面板
*/
function openPropertyPanel() {
if (!engine || !engine.propertyPanel) {
console.error('Property panel not available');
return;
}
engine.propertyPanel.show();
}
// --- 构件树数据 ---
function getLevelTree() {
if (!engine || !engine.engine || !engine.engine.isInitialized()) {
alert('请先初始化 3D 引擎并加载模型!');
return;
}
const data = engine.engine.getLevelTreeData();
console.log('🌳 楼层树数据:', data);
}
function getTypeTree() {
if (!engine || !engine.engine || !engine.engine.isInitialized()) {
alert('请先初始化 3D 引擎并加载模型!');
return;
}
const data = engine.engine.getTypeTreeData();
console.log('🌳 类型树数据:', data);
}
function getMajorTree() {
if (!engine || !engine.engine || !engine.engine.isInitialized()) {
alert('请先初始化 3D 引擎并加载模型!');
return;
}
const data = engine.engine.getMajorTreeData();
console.log('🌳 专业树数据:', data);
}
/**
* 更新引擎状态显示
*/
function updateEngineStatus(status) {
const statusEl = document.getElementById('engine-status');
if (statusEl) {
statusEl.textContent = status;
// 根据状态设置颜色
if (status === '已初始化') {
statusEl.style.color = '#28a745';
} else if (status === '初始化失败' || status === '初始化错误') {
statusEl.style.color = '#dc3545';
} else {
statusEl.style.color = '#666';
}
}
}
// --- 自动组合加载 ---
function openCombineDialog() {
if (!engine || !engine.engine || !engine.engine.isInitialized()) {
alert('请先初始化 3D 引擎!');
return;
}
const overlay = document.getElementById('combine-overlay');
overlay.style.display = 'flex';
}
function closeCombineDialog() {
const overlay = document.getElementById('combine-overlay');
overlay.style.display = 'none';
}
function confirmCombineLoad() {
let raw = document.getElementById('combine-urls').value.trim();
if (!raw) {
alert('请输入至少一个模型 URL');
return;
}
// 1) 统一清洗去除智能引号、零宽字符、BOM 等不可见字符
raw = raw
.replace(/[\u2018\u2019]/g, "'") // “” → '
.replace(/[\u201C\u201D]/g, '"') // “” → "
.replace(/[\u200B\uFEFF\u00A0]/g, '') // 零宽空格、BOM、不换行空格
.trim();
// 2) 尝试当 JSON 数组解析
let urls = [];
try {
const jsonStr = raw.replace(/'/g, '"');
const parsed = JSON.parse(jsonStr);
if (Array.isArray(parsed)) {
urls = parsed.map(s => String(s).trim()).filter(Boolean);
}
} catch(e) {
// 3) JSON 失败,走分割逻辑
urls = raw.split(/[,;\n\r]+/)
.map(s => s.trim().replace(/^['"|\[\]\s]+|['"|\[\]\s]+$/g, ''))
.filter(Boolean);
}
// 4) 只保留 http/https 开头的合法 URL
urls = urls.filter(s => /^https?:\/\//i.test(s));
if (urls.length === 0) {
alert('未解析到有效的 URL请确保以 http:// 或 https:// 开头');
return;
}
// 5) 逐个校验 URL 是否合法
for (let i = 0; i < urls.length; i++) {
try {
new URL(urls[i]);
} catch(e) {
alert('第 ' + (i+1) + ' 个 URL 无效:\n' + urls[i] + '\n\n请检查是否包含特殊字符');
console.error('❌ 无效 URL原始字符:', JSON.stringify(urls[i]));
return;
}
}
try {
engine.engine.loadModel(urls, {
position: [0, 0, 0],
rotation: [0, 0, 0],
scale: [1, 1, 1]
});
console.log('✅ 自动组合加载已发送:', urls);
closeCombineDialog();
document.getElementById('combine-urls').value = '';
} catch (error) {
console.error('❌ 自动组合加载错误:', error);
}
}
</script>
</body>
</html>