feat: upgrade to v1.2.0 with model param validation and UI improvements
- Upgrade iflow-engine-base to ^2.0.0 - Add sanitizeModelParams for robust model operation validation - Add try-catch error handling for render mode and model tool calls - Preserve tree scroll position across tab switches - Optimize tree node reveal with visibility check and centered scrolling - Refactor collectModelParams to support multi-model grouping - Fix tree CSS: remove duplicates, constrain overflow, improve layout - Move version label to bottom-left - Rebuild demo libs
This commit is contained in:
@@ -27,7 +27,7 @@
|
||||
.bim-engine-version {
|
||||
position: absolute;
|
||||
bottom: 6px;
|
||||
right: 10px;
|
||||
left: 10px;
|
||||
font-size: 11px;
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
pointer-events: none;
|
||||
@@ -35,4 +35,4 @@
|
||||
z-index: 1;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import type { MeasureUnit, MeasurePrecision } from '../measure-panel/types';
|
||||
import type { SectionBoxRange } from '../section-box-panel/types';
|
||||
import type { ManagerRegistry } from '../../core/manager-registry';
|
||||
// 导入第三方 SDK 的 createEngine 函数(从 npm 包引入)
|
||||
//import { createEngine as createEngineSDK } from 'iflow-engine-base';
|
||||
import { createEngine as createEngineSDK } from '../../../../bim_engine_base/dist/bim-engine-sdk.es';
|
||||
import { createEngine as createEngineSDK } from 'iflow-engine-base';
|
||||
//import { createEngine as createEngineSDK } from '../../../../bim_engine_base/dist/bim-engine-sdk.es';
|
||||
import "../../../../bim_engine_base/dist/iflow-engine-base.css"
|
||||
|
||||
export type { EngineOptions, ModelLoadOptions, EngineInfo };
|
||||
@@ -579,7 +579,11 @@ export class Engine implements IBimComponent {
|
||||
console.warn('[Engine] Cannot set render mode: engine not initialized.');
|
||||
return;
|
||||
}
|
||||
this.engine.engineModelModule.setMode(mode);
|
||||
try {
|
||||
this.engine.engineModelModule.setMode(mode);
|
||||
} catch (e) {
|
||||
console.warn('[Engine] Failed to set render mode:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 结束:渲染模式 ====================
|
||||
@@ -935,7 +939,13 @@ export class Engine implements IBimComponent {
|
||||
console.warn('[Engine] Cannot highlight model: engine not initialized.');
|
||||
return;
|
||||
}
|
||||
this.engine.modelToolModule.highlightModel(models);
|
||||
const safeModels = this.sanitizeModelParams(models);
|
||||
if (!safeModels.length) return;
|
||||
try {
|
||||
this.engine.modelToolModule.highlightModel(safeModels);
|
||||
} catch (e) {
|
||||
console.warn('[Engine] highlightModel failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
public unhighlightAllModels(): void {
|
||||
@@ -951,7 +961,13 @@ export class Engine implements IBimComponent {
|
||||
console.warn('[Engine] Cannot view scale to model: engine not initialized.');
|
||||
return;
|
||||
}
|
||||
this.engine.modelToolModule.viewScaleToModel(models);
|
||||
const safeModels = this.sanitizeModelParams(models);
|
||||
if (!safeModels.length) return;
|
||||
try {
|
||||
this.engine.modelToolModule.viewScaleToModel(safeModels);
|
||||
} catch (e) {
|
||||
console.warn('[Engine] viewScaleToModel failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
public hideModels(models: { url: string; ids: number[] }[]): void {
|
||||
@@ -959,7 +975,13 @@ export class Engine implements IBimComponent {
|
||||
console.warn('[Engine] Cannot hide models: engine not initialized.');
|
||||
return;
|
||||
}
|
||||
this.engine.modelToolModule.hideModel(models);
|
||||
const safeModels = this.sanitizeModelParams(models);
|
||||
if (!safeModels.length) return;
|
||||
try {
|
||||
this.engine.modelToolModule.hideModel(safeModels);
|
||||
} catch (e) {
|
||||
console.warn('[Engine] hideModels failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
public showModel(models: { url: string; ids: number[] }[]): void {
|
||||
@@ -967,7 +989,60 @@ export class Engine implements IBimComponent {
|
||||
console.warn('[Engine] Cannot show model: engine not initialized.');
|
||||
return;
|
||||
}
|
||||
this.engine.modelToolModule.showModel(models);
|
||||
const safeModels = this.sanitizeModelParams(models);
|
||||
if (!safeModels.length) return;
|
||||
try {
|
||||
this.engine.modelToolModule.showModel(safeModels);
|
||||
} catch (e) {
|
||||
console.warn('[Engine] showModel failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
private sanitizeModelParams(models: { url: string; ids: number[] }[]): { url: string; ids: number[] }[] {
|
||||
if (!Array.isArray(models)) return [];
|
||||
|
||||
const normalizeUrl = (url: string): string => url.replace(/\/+$/, '');
|
||||
|
||||
const loadedModels = Array.isArray((this.engine as any)?.models)
|
||||
? ((this.engine as any).models as Array<{ url?: string; nodesMap?: Map<number, { indexes?: unknown[] }> }> )
|
||||
: [];
|
||||
const modelMap = new Map<string, { nodesMap?: Map<number, { indexes?: unknown[] }> }>();
|
||||
for (const loadedModel of loadedModels) {
|
||||
if (loadedModel?.url) {
|
||||
modelMap.set(normalizeUrl(loadedModel.url), loadedModel);
|
||||
}
|
||||
}
|
||||
|
||||
const result: { url: string; ids: number[] }[] = [];
|
||||
for (const model of models) {
|
||||
if (!model || typeof model.url !== 'string' || !model.url) continue;
|
||||
if (!Array.isArray(model.ids)) continue;
|
||||
|
||||
const loadedModel = modelMap.get(normalizeUrl(model.url));
|
||||
if (!loadedModel || !(loadedModel.nodesMap instanceof Map)) {
|
||||
continue;
|
||||
}
|
||||
const idsSet = new Set<number>();
|
||||
for (const rawId of model.ids) {
|
||||
if (!Number.isFinite(rawId)) continue;
|
||||
const id = Number(rawId);
|
||||
if (!Number.isInteger(id)) continue;
|
||||
|
||||
const node = loadedModel.nodesMap.get(id);
|
||||
if (!node || !Array.isArray(node.indexes) || node.indexes.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
idsSet.add(id);
|
||||
}
|
||||
|
||||
const ids = Array.from(idsSet);
|
||||
if (!ids.length) continue;
|
||||
|
||||
result.push({ url: model.url, ids });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1115,4 +1190,3 @@ export class Engine implements IBimComponent {
|
||||
this._isInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ export class BimTab implements IBimComponent {
|
||||
private tabMap: Map<string, TabItem> = new Map();
|
||||
/** id -> 内容容器 */
|
||||
private panelMap: Map<string, HTMLElement> = new Map();
|
||||
private panelTreeScrollTopMap: Map<string, number> = new Map();
|
||||
/** 主题/语言订阅解除函数 */
|
||||
private unsubscribeLocale: (() => void) | null = null;
|
||||
private unsubscribeTheme: (() => void) | null = null;
|
||||
@@ -175,8 +176,14 @@ export class BimTab implements IBimComponent {
|
||||
// 更新面板显示
|
||||
this.panelMap.forEach((panel, id) => {
|
||||
const isActive = id === tabId;
|
||||
if (!isActive) {
|
||||
this.savePanelTreeScrollTop(id, panel);
|
||||
}
|
||||
panel.classList.toggle('is-active', isActive);
|
||||
panel.style.display = isActive ? 'block' : 'none';
|
||||
panel.style.display = isActive ? '' : 'none';
|
||||
if (isActive) {
|
||||
this.restorePanelTreeScrollTop(id, panel);
|
||||
}
|
||||
});
|
||||
|
||||
if (this.options.onChange) {
|
||||
@@ -234,10 +241,27 @@ export class BimTab implements IBimComponent {
|
||||
this.unsubscribeTheme = null;
|
||||
}
|
||||
this.panelMap.clear();
|
||||
this.panelTreeScrollTopMap.clear();
|
||||
this.tabMap.clear();
|
||||
this.element.remove();
|
||||
}
|
||||
|
||||
private savePanelTreeScrollTop(tabId: string, panel: HTMLElement): void {
|
||||
const treeContent = panel.querySelector<HTMLElement>('.bim-tree-content');
|
||||
if (!treeContent) return;
|
||||
this.panelTreeScrollTopMap.set(tabId, treeContent.scrollTop);
|
||||
}
|
||||
|
||||
private restorePanelTreeScrollTop(tabId: string, panel: HTMLElement): void {
|
||||
const savedTop = this.panelTreeScrollTopMap.get(tabId);
|
||||
if (savedTop === undefined) return;
|
||||
const treeContent = panel.querySelector<HTMLElement>('.bim-tree-content');
|
||||
if (!treeContent) return;
|
||||
requestAnimationFrame(() => {
|
||||
treeContent.scrollTop = savedTop;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具:解析标题(优先翻译,不存在则回退原值)
|
||||
*/
|
||||
@@ -251,4 +275,3 @@ export class BimTab implements IBimComponent {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -109,31 +109,6 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 树内容区域 */
|
||||
.bim-tree-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 2px 0;
|
||||
min-height: 0; /* 允许在父容器中正确滚动 */
|
||||
}
|
||||
|
||||
/* 节点行容器 */
|
||||
.bim-tree-node {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 节点内容行 (Flex 布局) */
|
||||
.bim-tree-node-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 32px; /* 标准行高 */
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
border-radius: 4px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.bim-tree-node-content:hover {
|
||||
background-color: var(--bim-component-bg-hover);
|
||||
}
|
||||
@@ -286,7 +261,10 @@
|
||||
/* 树内容区域 */
|
||||
.bim-tree-content {
|
||||
flex: 1;
|
||||
overflow: auto; /* 支持横向和纵向滚动 */
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior: contain;
|
||||
scroll-behavior: auto;
|
||||
padding: 2px 0;
|
||||
min-height: 0;
|
||||
}
|
||||
@@ -295,8 +273,7 @@
|
||||
.bim-tree-node {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: fit-content; /* 关键:让节点容器跟随内容宽度 */
|
||||
min-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 节点内容行 (Flex 布局) */
|
||||
@@ -309,8 +286,6 @@
|
||||
border-radius: 4px;
|
||||
padding-right: 8px;
|
||||
|
||||
/* 关键:允许宽度根据内容撑开,且至少占满容器 */
|
||||
width: fit-content;
|
||||
min-width: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export class BimTree implements IBimComponent {
|
||||
private nodeMap: Map<string, BimTreeNode> = new Map();
|
||||
private rootNodes: BimTreeNode[] = [];
|
||||
private selectedNode: BimTreeNode | null = null; // 当前选中的高亮节点
|
||||
private lastRevealedNodeId: string | null = null;
|
||||
|
||||
// 订阅清理函数
|
||||
private unsubscribeLocale: (() => void) | null = null;
|
||||
@@ -210,6 +211,11 @@ export class BimTree implements IBimComponent {
|
||||
* 定位到指定节点
|
||||
*/
|
||||
public revealNode(node: BimTreeNode) {
|
||||
if (this.lastRevealedNodeId === node.config.id && this.isNodeFullyVisible(node)) {
|
||||
return;
|
||||
}
|
||||
this.lastRevealedNodeId = node.config.id;
|
||||
|
||||
// 1. 关闭搜索下拉
|
||||
if (this.searchResults) {
|
||||
this.searchResults.classList.remove('is-visible');
|
||||
@@ -227,9 +233,39 @@ export class BimTree implements IBimComponent {
|
||||
this.handleNodeSelect(node);
|
||||
|
||||
// 4. 滚动到可视区域
|
||||
setTimeout(() => {
|
||||
node.element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}, 100);
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
this.scrollNodeIntoContentCenter(node);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private isNodeFullyVisible(node: BimTreeNode): boolean {
|
||||
const container = this.contentElement;
|
||||
if (!container || !node?.element?.isConnected) return false;
|
||||
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const nodeRect = node.element.getBoundingClientRect();
|
||||
return nodeRect.top >= containerRect.top && nodeRect.bottom <= containerRect.bottom;
|
||||
}
|
||||
|
||||
private scrollNodeIntoContentCenter(node: BimTreeNode): void {
|
||||
const container = this.contentElement;
|
||||
if (!container || !node?.element?.isConnected) return;
|
||||
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const nodeRect = node.element.getBoundingClientRect();
|
||||
if (nodeRect.top >= containerRect.top && nodeRect.bottom <= containerRect.bottom) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeTopInContainer = nodeRect.top - containerRect.top + container.scrollTop;
|
||||
const targetTop = Math.max(0, nodeTopInContainer - (container.clientHeight - nodeRect.height) / 2);
|
||||
|
||||
container.scrollTo({
|
||||
top: targetTop,
|
||||
behavior: 'auto'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -279,6 +315,7 @@ export class BimTree implements IBimComponent {
|
||||
this.rootNodes.forEach(node => node.destroy());
|
||||
this.rootNodes = [];
|
||||
this.nodeMap.clear();
|
||||
this.lastRevealedNodeId = null;
|
||||
this.element.remove();
|
||||
this.selectedNode = null;
|
||||
}
|
||||
|
||||
@@ -82,6 +82,11 @@ interface TransformedNodeData extends EngineTreeNode {
|
||||
_modelUrl: string;
|
||||
}
|
||||
|
||||
interface ModelParam {
|
||||
url: string;
|
||||
ids: number[];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 工具函数
|
||||
// ============================================================================
|
||||
@@ -123,14 +128,38 @@ async function hashIds(ids: string[]): Promise<string> {
|
||||
* 递归收集节点及其所有子孙节点的 ids
|
||||
* 用于父级节点操作时,获取其下所有叶子节点对应的构件 ids
|
||||
*/
|
||||
function collectAllIds(node: BimTreeNode): string[] {
|
||||
const result: string[] = [];
|
||||
const data = node.config.data as TransformedNodeData | undefined;
|
||||
if (data?.ids?.length) {
|
||||
result.push(...data.ids);
|
||||
}
|
||||
for (const child of node.children || []) {
|
||||
result.push(...collectAllIds(child));
|
||||
function collectModelParams(node: BimTreeNode): ModelParam[] {
|
||||
const grouped = new Map<string, Set<number>>();
|
||||
|
||||
const collect = (current: BimTreeNode): void => {
|
||||
const data = current.config.data as TransformedNodeData | undefined;
|
||||
const modelUrl = data?._modelUrl;
|
||||
if (modelUrl && data?.ids?.length) {
|
||||
let idSet = grouped.get(modelUrl);
|
||||
if (!idSet) {
|
||||
idSet = new Set<number>();
|
||||
grouped.set(modelUrl, idSet);
|
||||
}
|
||||
for (const rawId of data.ids) {
|
||||
const id = Number(rawId);
|
||||
if (Number.isFinite(id)) {
|
||||
idSet.add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const child of current.children || []) {
|
||||
collect(child);
|
||||
}
|
||||
};
|
||||
|
||||
collect(node);
|
||||
|
||||
const result: ModelParam[] = [];
|
||||
for (const [url, idSet] of grouped) {
|
||||
if (idSet.size > 0) {
|
||||
result.push({ url, ids: Array.from(idSet) });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -337,19 +366,8 @@ export class ConstructTreeManagerBtn extends BaseManager {
|
||||
* 勾选 → showModel,取消勾选 → hideModels
|
||||
*/
|
||||
onNodeCheck: (node) => {
|
||||
const nodeData = node.config.data as TransformedNodeData | undefined;
|
||||
if (!nodeData?._modelUrl) return;
|
||||
|
||||
const ids = nodeData.ids?.length
|
||||
? nodeData.ids
|
||||
: collectAllIds(node);
|
||||
|
||||
if (!ids.length) return;
|
||||
|
||||
const modelParam = [{
|
||||
url: nodeData._modelUrl,
|
||||
ids: ids.map(Number)
|
||||
}];
|
||||
const modelParam = collectModelParams(node);
|
||||
if (!modelParam.length) return;
|
||||
|
||||
if (node.checkState === TreeNodeCheckState.Checked) {
|
||||
this.registry.engine3d?.showModel(modelParam);
|
||||
@@ -362,19 +380,8 @@ export class ConstructTreeManagerBtn extends BaseManager {
|
||||
* 节点选中回调 - 高亮并跳转到模型构件
|
||||
*/
|
||||
onNodeSelect: (node) => {
|
||||
const nodeData = node.config.data as TransformedNodeData | undefined;
|
||||
if (!nodeData?._modelUrl) return;
|
||||
|
||||
const ids = nodeData.ids?.length
|
||||
? nodeData.ids
|
||||
: collectAllIds(node);
|
||||
|
||||
if (!ids.length) return;
|
||||
|
||||
const modelParam = [{
|
||||
url: nodeData._modelUrl,
|
||||
ids: ids.map(Number)
|
||||
}];
|
||||
const modelParam = collectModelParams(node);
|
||||
if (!modelParam.length) return;
|
||||
|
||||
this.registry.engine3d?.unhighlightAllModels();
|
||||
this.registry.engine3d?.highlightModel(modelParam);
|
||||
@@ -438,7 +445,6 @@ export class ConstructTreeManagerBtn extends BaseManager {
|
||||
],
|
||||
activeId: 'component',
|
||||
onChange: () => {
|
||||
resetAllTrees();
|
||||
this.dialog?.fitWidth();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user