feat(demo): add multi-tab demo with dual BimEngine instances

New demo page with two independent BimEngine instances side by side,
draggable divider for resizing, and proper SDK-layer resize handling.
Replaces the iframe-based approach now that ManagerRegistry is instance-based.
Removes unused panelViewer vite config entry.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
yuding
2026-02-28 10:09:17 +08:00
parent 7d7fbd9c14
commit e91a460fea
3 changed files with 451 additions and 1 deletions

404
demo/multi-tab.html Normal file
View File

@@ -0,0 +1,404 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>iFlow Engine - 多Tab加载</title>
<script src="./lib/iflow-engine.umd.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
width: 100%;
height: 100%;
overflow: hidden;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #1e1e1e;
color: #e0e0e0;
}
/* ===== 顶部控制栏 ===== */
.top-bar {
height: 48px;
background: #2d2d2d;
border-bottom: 1px solid #404040;
display: flex;
align-items: center;
padding: 0 16px;
gap: 16px;
flex-shrink: 0;
}
.top-bar .title {
font-size: 14px;
font-weight: 600;
color: #fff;
white-space: nowrap;
}
.top-bar .back-btn {
padding: 4px 12px;
font-size: 13px;
background: transparent;
color: #8ab4f8;
border: 1px solid #555;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
transition: all 0.15s;
}
.top-bar .back-btn:hover {
background: #3a3a3a;
border-color: #8ab4f8;
}
.top-bar .separator {
width: 1px;
height: 24px;
background: #404040;
}
/* 模型 URL 输入区 */
.model-inputs {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
min-width: 0;
}
.model-input-group {
display: flex;
align-items: center;
gap: 6px;
flex: 1;
min-width: 0;
}
.model-input-group label {
font-size: 12px;
color: #aaa;
white-space: nowrap;
}
.model-input-group input {
flex: 1;
min-width: 0;
height: 28px;
padding: 0 8px;
font-size: 12px;
background: #1e1e1e;
color: #e0e0e0;
border: 1px solid #555;
border-radius: 3px;
outline: none;
transition: border-color 0.15s;
}
.model-input-group input:focus {
border-color: #0078d4;
}
.load-btn {
height: 28px;
padding: 0 14px;
font-size: 12px;
font-weight: 500;
background: #0078d4;
color: #fff;
border: none;
border-radius: 3px;
cursor: pointer;
white-space: nowrap;
transition: background 0.15s;
}
.load-btn:hover {
background: #1a88e0;
}
.load-btn:active {
background: #006abc;
}
/* ===== 主内容区:双面板 ===== */
.panels-container {
display: flex;
height: calc(100vh - 48px);
position: relative;
}
.panel {
position: relative;
overflow: hidden;
background: #1a1a1a;
}
.panel-left {
flex: 1 1 0;
min-width: 200px;
}
.panel-right {
flex: 1 1 0;
min-width: 200px;
}
/* 引擎容器 */
.engine-container {
width: 100%;
height: 100%;
}
/* 面板标签 */
.panel-label {
position: absolute;
top: 8px;
left: 12px;
z-index: 10;
font-size: 12px;
font-weight: 600;
color: rgba(255, 255, 255, 0.7);
background: rgba(0, 0, 0, 0.5);
padding: 3px 10px;
border-radius: 3px;
pointer-events: none;
user-select: none;
}
/* ===== 拖拽分割线 ===== */
.divider {
width: 6px;
background: #333;
cursor: col-resize;
flex-shrink: 0;
position: relative;
z-index: 20;
transition: background 0.15s;
}
.divider:hover,
.divider.active {
background: #0078d4;
}
.divider::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 2px;
height: 32px;
background: rgba(255, 255, 255, 0.3);
border-radius: 1px;
}
.divider:hover::after,
.divider.active::after {
background: rgba(255, 255, 255, 0.6);
}
.panels-container.dragging {
cursor: col-resize;
user-select: none;
}
</style>
</head>
<body>
<!-- 顶部控制栏 -->
<div class="top-bar">
<a class="back-btn" href="./index.html">← 返回 Demo</a>
<div class="separator"></div>
<span class="title">多Tab加载双实例</span>
<div class="separator"></div>
<div class="model-inputs">
<!-- 左面板 -->
<div class="model-input-group">
<label>左面板:</label>
<input type="text" id="url-left"
value="https://lyz-1259524260.cos.ap-guangzhou.myqcloud.com/iflow/models/417664a3-76c8-4d94-9344-1337246a5d4e/"
placeholder="模型 URL">
<button class="load-btn" onclick="loadLeft()">加载</button>
</div>
<!-- 右面板 -->
<div class="model-input-group">
<label>右面板:</label>
<input type="text" id="url-right"
value="https://lyz-1259524260.cos.ap-guangzhou.myqcloud.com/iflow/models/e49a5fd1-3018-4938-9a52-6862b56a190b/"
placeholder="模型 URL">
<button class="load-btn" onclick="loadRight()">加载</button>
</div>
</div>
</div>
<!-- 双面板区域 -->
<div class="panels-container" id="panels-container">
<div class="panel panel-left" id="panel-left">
<div class="panel-label">面板 A</div>
<div class="engine-container" id="engine-left"></div>
</div>
<div class="divider" id="divider"></div>
<div class="panel panel-right" id="panel-right">
<div class="panel-label">面板 B</div>
<div class="engine-container" id="engine-right"></div>
</div>
</div>
<script>
// ===== 元素引用 =====
const panelsContainer = document.getElementById('panels-container');
const panelLeft = document.getElementById('panel-left');
const panelRight = document.getElementById('panel-right');
const divider = document.getElementById('divider');
const urlLeft = document.getElementById('url-left');
const urlRight = document.getElementById('url-right');
// ===== 两个独立的 BimEngine 实例 =====
const BimEngine = window.IflowEngine.BimEngine;
let engineLeft = null;
let engineRight = null;
/**
* 初始化单个引擎实例
*/
function createEngine(containerId) {
const engine = new BimEngine(containerId, { locale: 'zh-CN', theme: 'dark' });
const ok = engine.engine.initialize({
backgroundColor: 0x333333,
version: 'v2',
showStats: false,
showViewCube: true
});
if (!ok) {
console.error('[MultiTab] 引擎初始化失败:', containerId);
return null;
}
console.log('[MultiTab] 引擎初始化成功:', containerId);
return engine;
}
/**
* 加载模型到指定引擎
*/
function loadModel(engine, url) {
if (!engine || !engine.engine) return;
const trimmed = url.trim();
if (!trimmed) return;
engine.engine.loadModel([trimmed], {
position: [0, 0, 0],
rotation: [0, 0, 0],
scale: [1, 1, 1]
});
console.log('[MultiTab] 加载模型:', trimmed);
}
function loadLeft() {
loadModel(engineLeft, urlLeft.value);
}
function loadRight() {
loadModel(engineRight, urlRight.value);
}
/**
* 通知引擎调整渲染器尺寸
*/
function resizeEngine(engine) {
if (!engine || !engine.engine) return;
engine.engine.resize();
}
function resizeAll() {
//resizeEngine(engineLeft);
//resizeEngine(engineRight);
}
// ===== 分割线拖拽逻辑 =====
let isDragging = false;
let startX = 0;
let startLeftWidth = 0;
divider.addEventListener('mousedown', (e) => {
isDragging = true;
startX = e.clientX;
startLeftWidth = panelLeft.getBoundingClientRect().width;
panelsContainer.classList.add('dragging');
divider.classList.add('active');
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const containerWidth = panelsContainer.getBoundingClientRect().width;
const dividerWidth = divider.getBoundingClientRect().width;
const available = containerWidth - dividerWidth;
let newLeftWidth = startLeftWidth + dx;
const minWidth = 200;
newLeftWidth = Math.max(minWidth, Math.min(newLeftWidth, available - minWidth));
const leftPercent = (newLeftWidth / available) * 100;
const rightPercent = 100 - leftPercent;
panelLeft.style.flex = `0 0 ${leftPercent}%`;
panelRight.style.flex = `0 0 ${rightPercent}%`;
// 拖拽时实时 resize 引擎
resizeAll();
});
document.addEventListener('mouseup', () => {
if (!isDragging) return;
isDragging = false;
panelsContainer.classList.remove('dragging');
divider.classList.remove('active');
resizeAll();
});
// 窗口 resize
window.addEventListener('resize', resizeAll);
// 回车键触发加载
urlLeft.addEventListener('keydown', (e) => {
if (e.key === 'Enter') loadLeft();
});
urlRight.addEventListener('keydown', (e) => {
if (e.key === 'Enter') loadRight();
});
// ===== 启动 =====
window.onload = () => {
try {
engineLeft = createEngine('engine-left');
engineRight = createEngine('engine-right');
// 自动加载默认模型
const leftUrl = urlLeft.value.trim();
const rightUrl = urlRight.value.trim();
if (leftUrl) loadLeft();
if (rightUrl) loadRight();
} catch (err) {
console.error('[MultiTab] 初始化失败:', err);
}
};
</script>
</body>
</html>