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:
@@ -171,6 +171,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<!-- 5. 3D 引擎 -->
|
<!-- 5. 3D 引擎 -->
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<h2>🎮 3D 引擎 (Engine3D)</h2>
|
<h2>🎮 3D 引擎 (Engine3D)</h2>
|
||||||
@@ -178,6 +189,7 @@
|
|||||||
<button class="primary" onclick="initEngine3D()">初始化引擎</button>
|
<button class="primary" onclick="initEngine3D()">初始化引擎</button>
|
||||||
<button class="primary" onclick="loadModel()">加载模型</button>
|
<button class="primary" onclick="loadModel()">加载模型</button>
|
||||||
<button onclick="switchModel()">切换模型</button>
|
<button onclick="switchModel()">切换模型</button>
|
||||||
|
<button onclick="loadCombinedModel()">组合模型</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-container" style="margin-top: 8px;">
|
<div class="btn-container" style="margin-top: 8px;">
|
||||||
<button onclick="pauseRendering()">暂停渲染</button>
|
<button onclick="pauseRendering()">暂停渲染</button>
|
||||||
@@ -410,6 +422,38 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载组合模型 - 同时加载多个模型
|
||||||
|
*/
|
||||||
|
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/417664a3-76c8-4d94-9344-1337246a5d4e/',
|
||||||
|
'https://lyz-1259524260.cos.ap-guangzhou.myqcloud.com/iflow/models/e49a5fd1-3018-4938-9a52-6862b56a190b/'
|
||||||
|
];
|
||||||
|
|
||||||
|
engine.engine.loadModel(modelUrls, {
|
||||||
|
position: [0, 0, 0],
|
||||||
|
rotation: [0, 0, 0],
|
||||||
|
scale: [1, 1, 1]
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ 组合模型加载请求已发送:', modelUrls);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 组合模型加载错误:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 暂停渲染
|
* 暂停渲染
|
||||||
*/
|
*/
|
||||||
|
|||||||
404
demo/multi-tab.html
Normal file
404
demo/multi-tab.html
Normal 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>
|
||||||
@@ -16,7 +16,9 @@ export default defineConfig({
|
|||||||
outDir: 'dist',
|
outDir: 'dist',
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: {
|
input: {
|
||||||
main: resolve(__dirname, 'index.html')
|
main: resolve(__dirname, 'index.html'),
|
||||||
|
viewer: resolve(__dirname, 'viewer.html'),
|
||||||
|
multiTab: resolve(__dirname, 'multi-tab.html')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user