IFC Viewer SDK · v1.18.0
将 IFC 查看器嵌入您自己的产品
将同样的浏览器查看器嵌入您的 CDE、数字孪生或内部项目工具。直接从您的应用加载 IFC 数据,每个模型都在客户端解析。
三行代码即可首次渲染
const viewer = new IfcViewer("#viewer");
await viewer.add("project.ifc", ifcBytes);
// → rendered, validated, all on the client- 100% 客户端运行
- 无需上传后端
- 零运行时依赖
- 约 6 KB 封装
- TypeScript 类型
- 10 种语言
- 通过 IDS 验证的引擎
接入
快速开始
选择你的技术栈。复制、粘贴、渲染——无需构建步骤,无需 API key。
<div id="viewer" style="height:520px"></div>
<script type="module">
import { IfcViewer } from "https://www.ifcvieweronline.eu/sdk/ifc-viewer.es.js";
const viewer = new IfcViewer("#viewer");
// Load IFC bytes from your app — nothing is uploaded
const bytes = await fetch("/models/project.ifc").then(r => r.arrayBuffer());
await viewer.add("project.ifc", bytes);
</script>零 JavaScript —— 把标签放进任意页面或仪表盘,设置属性即可。
<script type="module" src="https://www.ifcvieweronline.eu/sdk/ifc-viewer.es.js"></script>
<ifc-viewer model="https://your-cde.com/model.ifc" ui="minimal" accent="#3fa866"
style="display:block;height:520px"></ifc-viewer>在 effect 中挂载,卸载时销毁。可安全应对 React 18 StrictMode 的双重调用。
import { useEffect, useRef } from "react";
import { IfcViewer } from "https://www.ifcvieweronline.eu/sdk/ifc-viewer.es.js";
export function ModelViewer({ url }) {
const host = useRef(null);
useEffect(() => {
const viewer = new IfcViewer(host.current, { model: url });
return () => viewer.dispose(); // StrictMode-safe teardown
}, [url]);
return <div ref={host} style={{ height: 520 }} />;
}在 onMounted 中挂载,在 onUnmounted 中销毁。
<script setup>
import { onMounted, onUnmounted, ref } from "vue";
import { IfcViewer } from "https://www.ifcvieweronline.eu/sdk/ifc-viewer.es.js";
const host = ref(null);
let viewer;
onMounted(() => { viewer = new IfcViewer(host.value, { model: "https://host/m.ifc" }); });
onUnmounted(() => viewer && viewer.dispose());
</script>
<template>
<div ref="host" style="height: 520px" />
</template>使用我们的托管或自行托管
示例代码从我们的 CDN 导入,可在任意源上使用。若要自行托管,从你自己的域名提供 ifc-viewer.es.js —— SDK 会相对脚本自动推断应用 URL。应用部署在别处?设置 baseUrl 选项。
暂无 npm 包:按 URL 导入模块(见上),或把 ifc-viewer.es.js 内置到你的应用中。
实证
在线演示
本页面正在运行该 SDK。点击控件以获取示例 IFC 并驱动查看器——每个按钮都会显示它所调用的方法。
⚠ 示例主机可能正在限流。请稍后重试。
架构
工作原理
你的页面里只有一个极小的封装,完整查看器运行在沙箱 iframe 中,你的字节在两者之间传递——并在它们原本所在的地方完成解析。
你的应用
一个约 6 KB 的 ES 模块,创建 iframe 并提供带类型的 JS API。3D 与解析的全部重量都不会进入你的打包产物。
postMessage 桥接
IFC 字节以 ArrayBuffer 形式转移到 iframe(零拷贝)。命令与查询是相互关联的请求/响应消息。
查看器
three.js 加上编译为 WebAssembly 的 web-ifc,在 iframe 内解析并渲染模型——就在访客的机器上。
为什么它是私密的
模型永远不会到达服务器。没有需要加固的上传链路,没有数据驻留问题,也没有按文件大小排队。
为什么它能扩展
计算在每位访客的设备上进行。分发 200 MB 的模型与 2 MB 的成本相同——它只是一个静态文件。
概念
核心概念
动手前需要了解的四件事。
挂载与就绪
new IfcViewer(target) 会立即挂载,并在查看器就绪前把命令排队。await IfcViewer.create(target)(或 whenReady())返回一个可直接使用的实例。
// queues commands until ready
const viewer = new IfcViewer("#viewer");
// …or await an instance that is ready to use
const viewer = await IfcViewer.create("#viewer");字节还是 URL
add(name, bytes) 传输你已经持有的字节——无需 CORS,不上传任何内容;buffer 会被转移(变为 detached),如果之后还要用请传入副本。addFromUrl(url) 则去获取一个启用了 CORS 的公开 URL。
// you already have the bytes — no CORS, nothing uploaded
await viewer.add("a.ifc", bytes);
// …or load a public, CORS-enabled URL
await viewer.addFromUrl("https://host/b.ifc");事件
用 on(event, cb) 订阅,它会返回一个取消订阅的函数。可用事件有 ready、model-progress、model-loaded、validation-completed、model-error 和 element-selected。
const off = viewer.on("model-loaded", (m) => {
console.log(m.fileName, m.elementCount);
});
// later: off();查询
按需取数:getModels、getElement、getValidation、getStats、getIssues、checkIds、checkEir、screenshot。每个都是请求/响应,并在超时后拒绝(30 秒;IDS / EIR 最长 120 秒)。
const models = await viewer.getModels();
const { qualityScore } = await viewer.getValidation();
const png = await viewer.screenshot();参考
API 参考
全部方法,按组列出——含签名与返回类型。随后是构造函数选项、事件以及 <ifc-viewer> web component。
构造函数
new IfcViewer(target, options?)IfcViewer挂载到 CSS 选择器或元素。参见下方选项。
IfcViewer.create(target, options?)Promise<IfcViewer>创建查看器,并在其可接受命令时兑现。
加载模型
add(name, bytes)Promise<ModelLoadedEvent>从您的应用加载 IFC 字节(ArrayBuffer/Uint8Array)。缓冲区以零拷贝方式转移。
addFromUrl(url, name?)Promise<ModelLoadedEvent>加载一个启用 CORS 的公开 IFC URL。
removeModel(modelId)void按 id 卸载某个模型。
clear()void移除所有已加载的模型。
相机与选择
select(expressId | globalId, modelId?)void选中并框选构件 —— 按 IFC expressID,或按 GlobalId(同时找到其所属模型)。
setView(view)void飞到场景的命名视图 —— iso、front、back、left、right、top、bottom —— 并让整个场景入画。无返回值(1.14 至 1.17 期间查看器会忽略它)。
frame({ view?, fill?, azimuth?, elevation?, animate? })Promise<{ scope }>用于展示的取景:模型占画面的 `fill`(0.2–0.98,默认 0.85),按包围盒角点而非包围球拟合。azimuth / elevation 可从任意角度观察。
frame(elementId, modelId?, options?)Promise<{ scope: 'element' }>框选单个构件(expressID):未指定视图时保持当前角度。构件不在模型中时拒绝。
fit() · reset()void从当前角度带边距地框选活动模型 / 重置相机。
setCamera(position, direction)void将相机置于某位置并朝某方向观察。
getCamera()Promise<CameraState | null>相机的位置与朝向——保存下来,再用 lookAt 恢复。
lookAt(position, target, animate?)Promise<void>将相机飞到某个位置并看向目标(场景米,Y 轴向上)。
setWalkMode(enabled, { speed? })Promise<WalkState>第一人称漫游模式(WASD 移动,拖动环顾,Esc 退出)。触发 walk-changed。
getWalkState()Promise<WalkState>漫游模式是否开启,以及速度。
可见性
isolate(ifcType?)void隔离一个类别(如 "IfcWall");不传参则清除。
showAll()void恢复全部可见性(清除隐藏与隔离)。
hideElements(ids, modelId?) · showElements(ids, modelId?)void按 expressID 隐藏 / 显示一组元素。
setModelVisible(modelId, visible)Promise<void>在联合场景中显示或隐藏某个模型,而不卸载它。
setModelOpacity(opacity, modelId?)Promise<void>使模型半透明(0.05–1)——例如机电周围的建筑。
isolateModel(modelId | null)Promise<void>仅显示此模型;传 null 恢复全部显示。
外观与展示
setBackground(background)Promise<BackgroundState>更改场景背景:预设('white'、'paper'、'blueprint'、'sky'、'studio')、'#rrggbb'、'#上,#下' 或 { top, bottom }。不会保存为访客自己的偏好。
getBackground()Promise<BackgroundState>当前的场景背景。
setAccent(color)Promise<void>运行时更改界面强调色(#rrggbb)。
setClientMode(enabled)Promise<void>开启或关闭客户视图——面向相关方的展示视图,等同于 ui: 'client'。
setRenderQuality(quality)Promise<void>'quality' 启用更重的渲染用于主视觉图;日常使用 'standard'。
日照分析
setSolar(opts?)Promise<SolarState>在场地当地的日期和时间生成真实阴影:{ date: '06-21', time: '18:00' }。场地取自 IFC 地理参照;模型没有时请传入 location——绝不会静默使用默认城市。
getSolar()Promise<SolarState>分析的日期、时间和时区,以及场地位置。
地图模式
setSiteContext(opts?)Promise<SiteContextState>将模型连同地形和 OpenStreetMap 周边放到地图上。调用即表示你的页面代表其访客同意加载瓦片;请展示署名。
getSiteContext()Promise<SiteContextState>地图模式状态、放置位置以及必须展示的署名。
场景
openScene(url | SceneDocument)Promise<void>打开另一个场景:.scene.json 的 URL,或一个场景文档(它压缩后放在框架地址里传递,因此必须能放进链接)。查看器随之重新加载;等待中的调用会被拒绝,ready 会再次触发。
exportScene({ title?, description?, camera? })Promise<SceneExport>将屏幕上的场景导出为场景文档——按 URL 引用的模型、图层、孪生、地图、相机——并附上需要注明的数据源和承载它的链接。
packScene(doc)Promise<string | null>自行将场景文档打包成 #scene= 链接(过大无法放入链接时返回 null)。
数据图层与孪生
getLayerPresets()Promise<LayerPreset[]>公共实时数据源目录,以查看器语言命名,并说明每个源在浏览器中需要什么(密钥、代理)以及在场地附近是否有数据。
addLayer({ preset } | { url, live? } | { geojson })Promise<DataLayerInfo>添加图层:目录中的数据源({ preset })、某个 URL 上的 GeoJSON 或实时数据流({ url, live }),或你已有的 GeoJSON({ geojson })。仅限本次会话:不会保存到访问者的图层中。
getLayers()Promise<DataLayerInfo[]>所有数据图层:状态、要素数量、范围、刷新和署名。
setLayerVisible(id, visible)Promise<DataLayerInfo>显示或隐藏图层。
frameLayer(id)Promise<void>将相机移到图层的要素上。
removeLayer(id)Promise<void>移除图层。
getTwin()Promise<TwinState>当前的运营孪生:设备数据源(状态、最近读数),以及每个绑定命中的规则、颜色、标签值和是否在告警。
剖切
addSection({ axis?, offset?, level?, flip? })Promise<SectionsState & { id }>添加剖切面。{ level: '一层' } 是该楼层的平面图;{ axis: 'x', offset: 4.5 } 是 4.5 米处的剖面。
updateSection(id, { offset?, enabled?, flipped? })Promise<SectionsState>移动、启用/停用或翻转剖切面。
removeSection(id?)Promise<SectionsState>移除一个剖切面;不传 id 时移除全部剖切。
setSectionBox('model' | 'selection' | false)Promise<SectionsState>围绕模型或选中构件的剖切盒;false 将其移除。
getSections()Promise<SectionsState>所有剖切面、剖切盒,以及可用于按楼层剖切的楼层。
导览
startTour(template?, { autoplay?, title? })Promise<TourState>启动内置导览:'social'、'client-walkthrough' 或 'technical-review'(逐一查看校验问题)。由查看器自带的导览栏播放。
playTour({ title?, steps }, { startAt?, autoplay? })Promise<TourState>播放你编写的导览:相机停靠点,可带说明、高亮与隔离的类别。autoplay:true 或每站毫秒数。
nextTourStep() · prevTourStep() · goToTourStep(i)Promise<TourState>在停靠点之间切换。
setTourAutoplay(autoplay)Promise<TourState>为正在播放的导览开启或关闭自动前进。
stopTour()Promise<TourState>停止导览并交还相机。
getTour()Promise<TourState>导览及其停靠点,格式与 playTour() 相同——可保存后再次播放。
演示导演
getPresentationRecipes()Promise<PresentationRecipe[]>内置导演配方(会议演示、短视频、LinkedIn 预告、发布风格、编辑风格…)。
createPresentation(recipe?, options?)Promise<PresentationState>按配方把模型变成剪辑好的视频:镜头依据 IFC 规划,带字幕和音乐。会用结果打开 Clip Studio。通过 presentation-progress 跟踪进度。
exportPresentation({ resolution?, music? })Promise<PresentationVideo>在访客浏览器中编码演示并返回 MP4 字节——可上传、附加或在 <video> 中播放。
closePresentation()Promise<void>关闭 Clip Studio。
封面工作室
getCoverOptions()Promise<CoverCatalog>封面工作室提供的配方、模板、格式和配色。
createCover({ recipe?, template?, format?, palette?, text? })Promise<CoverState>生成封面:先运行配方(会拍摄所需视图),再设置模板、格式、配色和文字。会打开封面工作室,仍可编辑。
getCover()Promise<CoverState | null>当前的封面;封面工作室关闭时为 null。
exportCover({ type?, slide? })Promise<CoverFile>导出:png / jpeg(单页),pdf / pptx(整份文档),zip(全部页面)。返回字节。
closeCover()Promise<void>关闭封面工作室。
文章套件
activate()void立即启动延迟加载的查看器(海报按钮的作用)。之前的调用会排队执行。
bindSteps(steps, { rootMargin? })() => void滚动叙事:每个段落经过屏幕中部时,取景、隔离、移动太阳或运行你的代码。返回解绑函数。
setTurntable(enabled | degPerSec)Promise<{ active, speed }>空闲时缓慢旋转(度/秒;false 停止)。首次触碰即停止,减少动态效果时不运行。
toggleFullscreen()Promise<void>将插图放大到全屏,或恢复。
setPaused(paused)Promise<{ paused }>暂停或恢复渲染。pauseOffscreen 会自动处理。
场景分组
getGroups()Promise<SceneGroupsState>场景中的所有分组——从 IFC 推断(项目、场地、位置)以及用户创建的分组。
createGroup(name, modelIds?)Promise<string>创建分组,可同时放入模型。返回其 id。
renameGroup(id, name) · deleteGroup(id)Promise<void>重命名或删除用户分组。自动分组不可编辑。
assignToGroup(itemId, groupId | null | 'loose')Promise<void>将模型或点云移入分组;null 交还给自动分组,'loose' 表示不属于任何分组。
setGroupVisible(id, visible) · isolateGroup(id | null)Promise<void>显示或隐藏整个分组,或只显示一个分组。
frameGroup(id)Promise<void>将相机对准某个分组,包括隐藏的成员。
测量
setMeasureTool(tool)Promise<void>为访客启用测量工具(会打开测量面板),或传 'none'。结果通过 measurements-changed 返回。
getMeasurements()Promise<MeasurementsState>屏幕上的所有测量。数值始终为国际单位——米、平方米、度——与界面显示无关。
clearMeasurements(id?)Promise<MeasurementsState>移除一个测量,或全部。
查询
getModels()Promise<ModelSummary[]>列出已加载的模型。返回 Promise。
findElements({ ifcClass?, globalId?, name?, modelId?, limit? })Promise<ElementSummary[]>在已加载的模型中按 IFC 类、GlobalId 和/或名称查找构件。每个结果都包含 getElement()、select() 和 frame() 所需的信息。
getElement(expressId | globalId, modelId?)Promise<IfcElementData | null>按 expressID 或 GlobalId。构件的 IFC 数据:带 GlobalId 的属性、其属性集和工程量,以及其类型的数据 —— typeName、typeProperties —— 含单位。effectivePropertySets 合并两者,构件自身的值优先。
validate(modelId?, { force? })Promise<ValidationRunResult>立即校验一个模型(默认活动模型),并以其 Health Score 和问题计数解析。失败时附带原因拒绝。
getValidation()Promise<ValidationSummary | null>屏幕上的校验结果;在任何一次运行产生结果之前为 null。见“校验生命周期”。
getValidationStatus()Promise<ValidationStatus>校验当前所处状态:idle、running(含进度)、done 或 error。
getStats()Promise<StatsResult>按类别统计每个模型的元素数量——用于仪表盘图表。返回 Promise。
getIssues({ severity?, limit?, modelId? })Promise<IssuesResult>用于表格的校验问题(按严重程度 / 数量过滤)。返回 Promise。
screenshot()Promise<string>将当前三维视图捕获为 PNG data URL。返回 Promise。
compare({ base, head, baseLabel?, headLabel? })Promise<{ changes }>按 URL 比较两次交付(按 GlobalId),并在结果上打开比较工作区。
面板
openPanel(panel | null) · closePanel()void打开某个工具面板,或传入 null 关闭当前打开的面板。若面板不可用——被外壳隐藏,或没有可作用的已加载内容——则不执行任何操作,也不报错。
getPanels()Promise<PanelsResult>当前打开的面板,以及此刻可供选择的面板。未出现在 `available` 中的工具无法打开,而不只是被禁用。
setPanels(panels)void在运行时把工具栏限制为这些面板。与 URL 参数 `panels=` 使用同一套名称,并优先于它。只会收窄查看器提供的内容,绝不新增;空数组表示不显示工具栏。
点云
addPointCloud(fileName, bytes)Promise<string>从字节加载扫描——LAS、LAZ、COPC、PLY、PCD 或文本。缓冲区是转移而非复制,返回点云 id。
addPointCloudFromUrl(url, fileName?)Promise<string>加载由查看器自行获取的扫描。该 URL 必须允许 CORS。
listPointClouds()Promise<PointCloudInfo[]>所有已加载的扫描。`pointCount` 是常驻内存的数量,`declaredCount` 是文件所含的数量——当预算截断解析时两者不同。
removePointCloud(cloudId) · clearPointClouds()Promise<void>卸载一个扫描并释放其 GPU 缓冲区,或全部卸载。
setPointCloudVisible(cloudId, visible)Promise<void>显示或隐藏一个扫描,而不卸载它。
fitPointCloud(cloudId?)Promise<void>把相机对准某个扫描,或第一个加载的扫描。
setPointCloudDisplay(display, renderBudget?)Promise<void>点大小、不透明度、着色模式、密度、置信度阈值——所有扫描共用。每一项都是着色器 uniform,因此在两千万点上也是即时生效。
inspectPointCloud(enabled?)Promise<void>启用点击读取。启用后点击一个点会发出 `pointcloud-picked`,同时带上场景坐标和文件中的坐标。
setPointCloudPlacement(placement, cloudId?)Promise<void>手动微调扫描:位置、偏航、调平、缩放。可传部分字段且会被钳制;叠加在推导出的对齐之上,而不是替换它。
setPointCloudUpAxis(axis, cloudId?)Promise<void>修正扫描以哪个轴为上。PLY、PCD 和文本都不声明方向,由查看器推断——请先检查 `upAxisSource`。
Imported 3D models
addMesh(files)Promise<string>从字节导入模型。需要一个列表:.gltf 需要它的 .bin 和贴图,.obj 需要它的 .mtl——只传入口文件会得到灰色几何体。
addMeshFromUrl(urls)Promise<string>导入由查看器获取的模型。请传入它需要的全部 URL;它们会并行获取,且都必须允许 CORS。
listMeshes()Promise<MeshInfo[]>所有已导入的模型,含三角面和贴图数量。在信任旁边的数值之前,请先读取 `unitSource` 和 `upAxisSource`。
removeMesh(meshId?) · clearMeshes()Promise<void>移除一个导入并释放其几何体、材质和贴图,或全部移除。
setMeshVisible(visible, meshId?)Promise<void>显示或隐藏一个导入,而不卸载它。
fitMesh(meshId?)Promise<void>把相机对准某个导入,或全部导入。
setMeshPlacement(placement, meshId?)Promise<void>手动放置导入:位置、偏航、调平、缩放。可传部分字段且会被钳制。导入初始居中于 IFC 之上、落在其地面。
setMeshUpAxis(axis, meshId?)Promise<void>修正来源的垂直轴。仅对 OBJ 有意义——glTF 规范强制 Y 向上,因此它报告 `upAxisSource: 'declared'`。
setMeshUnit(unitScale, meshId?)Promise<void>修正来源单位:1 米、0.01 厘米、0.001 毫米、0.3048 英尺。这些格式都不记录单位,查看器根据模型尺寸推断。
IDS 与 EIR 合规
checkIds(idsXml)Promise<IdsResult>对照 buildingSMART IDS(.ids XML)检查模型。在 worker 中运行。按规格返回通过/未通过。
checkEir(profile)Promise<IdsResult>按 EIR / BIM 校验配置检查模型——可传对象、其 JSON(含简写形式),或内置配置的 id,如 'builtin-en14351-1'。编译为 IDS,在同一引擎上运行:返回与 checkIds 相同的 IdsResult。
语言
setLanguage(lang)void在运行时切换界面语言。
getLanguages()string[]列出支持的语言代码(标签见 IfcViewer.LANGUAGES)。
生命周期与事件
whenReady()Promise<void>当查看器可接受命令时兑现。
isReadybooleanGetter —— 查看器发出就绪信号后为 true。
on(event, cb)() => void订阅事件;返回取消订阅的函数。
off(event, cb)void移除先前添加的事件监听器。
dispose()void销毁查看器并移除 iframe。
并发的 add() / addFromUrl() 调用在内部串行执行,每个 promise 都与自己的那次加载关联 —— 由应用发起的加载(URL 参数、iframe 内上传)绝不会兑现你的 add() promise。
构造函数选项
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
ui | 'minimal' | 'full' | 'kiosk' | 'client' | 'article' | 'embed' | 'minimal' | 界面预设。'embed':用于自行提供文件的宿主应用 —— 3D、选中即显示属性、带“校验”和“测量”的紧凑工具栏;无打开、分享、账户或语言。 |
toolbar | boolean | preset | 显示工具栏(默认取决于预设)。false 仅保留 3D 视图和面板。 |
tools | ('validate' | 'measure')[] | ['validate', 'measure'] | ui: 'embed' 紧凑工具栏上的工具 —— 'validate'、'measure'。[] 表示无。 |
autoFrame | boolean | true | 每次加载完成后,以 iso 视角带边距框选整个场景。false 则由你控制相机。 |
validate | boolean | true | 加载时执行校验(驱动 Health Score)。 |
panel | boolean | false | 自动打开校验面板。 |
panels | PanelName[] | — | 从第一帧起就把工具栏限制为这些面板——`['scene','map']`,或用 `[]` 表示不显示工具栏。名称与 setPanels 及 URL 参数 `panels=` 相同。优于加载后再调用 setPanels:那样会先显示完整工具栏再移除工具。 |
lang | string | auto | 强制界面语言(如 en、es、ja)。 |
accent | #rrggbb | brand | 为查看器着色,与你的仪表盘一致。 |
model | string | — | 就绪后自动加载这个公开的 IFC URL。 |
background | BackgroundSpec | — | 从第一帧起的场景背景——预设、'#rrggbb' 或 '#上,#下'。 |
map | boolean | ('terrain' | 'buildings' | 'showcase')[] | — | 加载后将模型放到地图上:true,或图层 ['terrain','buildings','showcase']。 |
solar · moon | 'MM-DDTHH:MM' · boolean | — | 在场地当地时间打开日照分析:'06-21T18:00'。 |
scans | string[] | — | 与模型一同加载的点云 URL。 |
layers | string | — | 要打开的数据图层配置:从“数据图层”面板导出的 JSON 的 URL(实时数据源、样式、警报)。自 1.16 起。 |
scene | string | — | 要打开的场景:场景文档的 URL(模型、数据图层、实时设备、地图、相机)。自 1.17 起。 |
view · fill | CameraView · number | — | 所有模型加载完成后,从该视图紧凑取景。ui: 'article' 默认 'iso'。 |
wheel | 'always' | 'ctrl' | 'always' | 'ctrl':滚轮滚动你的页面,只有按住 Ctrl/⌘ 才缩放,就像嵌入的地图。ui: 'article' 默认如此。 |
lazy | boolean | 'visible' | false | 需要时才加载:true 显示带按钮的海报,'visible' 在插图接近屏幕时启动。 |
poster · posterTitle · posterText · launchLabel | string | — | 查看器就绪前显示的海报图片、标题、文字和按钮文字。 |
aspectRatio | string ('16/10') | — | 按比例设置尺寸('16/10'):海报与查看器共用同一个框,页面不会跳动。 |
turntable | boolean | number | false | 加载后空闲缓慢旋转(true = 6°/秒,或指定度/秒)。 |
pauseOffscreen | boolean | ui === 'article' | 离开屏幕时停止渲染。ui: 'article' 时默认 true。 |
fullscreenButton | boolean | false | 角落里的小放大按钮。 |
height · width | number | string | '100%' | iframe 尺寸(数字 → px)。 |
baseUrl | string | auto | 应用的基础 URL。由脚本 URL 自动推断。 |
loadTimeout | number | 120000 | N 毫秒后拒绝 add()/addFromUrl()(0 表示禁用)。 |
className · title | string | — | 额外的 class / iframe 的 title。 |
on* callbacks | function | — | 便捷回调 —— 与 .on(...) 等价。 |
事件
| 事件 | Payload | 说明 |
|---|---|---|
ready | { languages } | 查看器已挂载并就绪。 |
model-progress | { percent, phase } | 加载进度(下载 → 解析 → 渲染)。 |
model-loaded | { modelId, fileName, elementCount, fromCache } | 某个模型加载完成。 |
validation-started | { modelId } | 某个模型开始校验 —— 加载之后(validate: true)或通过 validate()。 |
validation-completed | { modelId, qualityScore, errors, warnings, info, total } | 校验完成:Health Score 与计数,以及它们所属的模型(联合场景的汇总为 null)。 |
validation-failed | { modelId, message } | 校验无法运行或未完成 —— 附带原因。 |
model-error | { message, url?, name? } | 一次加载失败。 |
element-selected | { expressId, modelId, ifcType, name, globalId } | 用户选中了一个构件 —— 附带其 GlobalId,便于与你自己的数据对应。 |
pointcloud-picked | { cloudId, position, sourcePosition, classification, intensity, distance } | 从扫描数据中读取了一个点,使用文件自身坐标而非场景坐标。 |
map-feature-picked | { id, name?, label?, featureKind, heightM?, heightEstimated } | 点击了 OpenStreetMap 周边中的一栋建筑。仅为背景,不属于模型。 |
walk-changed | { active, speed } | 漫游模式被开启或关闭——由访客或宿主触发。 |
measurements-changed | { tool, units, items } | 添加、移除或重命名了测量。携带完整列表。 |
tour-started | { title, total, template } | 导览开始播放。 |
tour-step | { index, total, caption } | 导览切换到另一个停靠点。 |
tour-ended | { completed } | 导览停止;到达最后一站时 completed 为 true。 |
presentation-progress | { stage, label?, progress } | 导演正在生成或导出演示。 |
layer-feature-picked | { layerId, layer, featureId, geometry, lonLat, properties } | 点击了数据图层中的要素:其图层、属性以及一个可标记的点。 |
alert | { kind, from, id, name, rule, count, sample } | 数据图层或孪生的某条规则开始或停止告警。 |
静态成员
IfcViewer.LANGUAGES({ code, label }[],母语名称)与 IfcViewer.SUPPORTED_LANGUAGES(语言码)—— 可在查看器就绪前构建语言选择器。
Web component
零 JavaScript —— 把标签放进任意页面或仪表盘,设置属性即可。
属性: modeluilangaccentvalidatepanelbase-urltoolbartoolsauto-frame
事件会以名为 ifcviewer:<type> 的 DOM CustomEvent 重新派发(detail = payload)。底层的 IfcViewer 位于元素的 .viewer 上;getStats / getIssues / screenshot 会转发过去。
const el = document.querySelector("ifc-viewer");
el.addEventListener("ifcviewer:validation-completed", (e) => {
updateScore(e.detail.qualityScore);
});
const stats = await el.getStats();指南
范例
可直接复制粘贴的模式,对应团队真正会做的事。
构件检查器
在 3D 中点击,把 IFC 属性集读进你自己的面板。
viewer.on("element-selected", async (e) => {
const data = await viewer.getElement(e.expressId, e.modelId);
renderPanel(data.name, data.globalId, data.propertySets);
});Health Score 组件
在你的仪表盘中呈现模型的质量分与问题数量。
viewer.on("validation-completed", (v) => {
setScore(v.qualityScore); // 0–100, or null
setCounts(v.errors, v.warnings, v.info);
});IDS 合规检查
对照 buildingSMART IDS 校验,并按规格列出通过 / 未通过。
const ids = await fetch("/specs/project.ids").then(r => r.text());
const res = await viewer.checkIds(ids); // runs in a worker
res.specs.forEach((spec) => {
row(spec.name, spec.status, spec.passedCount + "/" + spec.applicableCount);
spec.failures.forEach((f) => onClick(() => viewer.select(f.expressId, f.modelId)));
});EIR / BIM 校验
运行可编辑的 EIR 配置(ISO 19650 风格)—— 无需 .ids 文件。失败项会带上构件的 GlobalId。
// An editable EIR profile — object, JSON, or the compact shorthand below.
const profile = {
name: "Door QA",
rules: [
{ type: "requiredProperty", entity: "IfcDoor", property: "FireRating", severity: "error" },
{ type: "allowedValues", entity: "IfcDoor", property: "FireRating",
values: ["EI30", "EI60", "EI90"], severity: "warning" },
],
};
const res = await viewer.checkEir(profile); // same IdsResult as checkIds
res.specs.forEach((spec) => {
spec.failures.forEach((f) => row(spec.name, f.globalId, f.reasons.join(" · ")));
});用于博客文章的场景
白色背景、模型位于其场地,以及夏至傍晚的阴影——预先声明,无需点击。
// Declared up front: the iframe boots with all of it, no clicks, no flash.
const viewer = await IfcViewer.create("#viewer", {
model: "https://example.com/house.ifc",
ui: "client",
background: "white",
map: ["terrain", "buildings"], // your page declares tile consent
solar: "06-21T19:30", // summer solstice, site-local
});
// …or change the story as the reader scrolls
await viewer.setSolar({ date: "12-21", time: "09:30" });
const { attributions } = await viewer.getSiteContext(); // show these平面图与测量
按楼层名称剖出平面图,让读者测量,再以国际单位读取结果。
const { levels } = await viewer.getSections();
await viewer.addSection({ level: levels[0].name }); // floor plan, 1.2 m up
viewer.setView("top");
await viewer.setMeasureTool("distance");
viewer.on("measurements-changed", ({ items }) => {
// value is SI — metres, m², degrees — whatever the viewer displays
render(items.map((m) => m.kind + ": " + m.value?.toFixed(2)));
});文章中的引导式导览
用 getCamera() 定义一次停靠点,然后让导览在正文旁自动播放。
// 1. Once, while writing the post: frame each view and copy the camera.
console.log(JSON.stringify(await viewer.getCamera()));
// 2. In the page: play the stops, self-running, next to your text.
await viewer.playTour({
title: "Casa Poblenou",
steps: [
{ position: { x: 52, y: 29, z: 24 }, target: { x: 18, y: 7, z: -11 }, caption: "The site" },
{ position: { x: 18, y: 40, z: 11 }, target: { x: 18, y: 0, z: 10 },
caption: "Structure", isolate: ["IfcColumn", "IfcSlab"] },
],
}, { autoplay: 6000 });
viewer.on("tour-step", ({ index, caption }) => highlightParagraph(index));从你的页面生成模型视频
用配方生成演示并取回 MP4——无需录屏,无需上传。
viewer.on("presentation-progress", ({ stage, progress }) => bar.value = progress ?? 0);
await viewer.createPresentation("linkedin-teaser", { title: "Casa Poblenou", targetSec: 20 });
const { bytes, mimeType } = await viewer.exportPresentation({ resolution: 1080 });
video.src = URL.createObjectURL(new Blob([bytes], { type: mimeType }));为你的文章配一张封面
用你的标题生成模型的 Pinterest 图钉或 LinkedIn 卡片,并取回 PNG。
await viewer.createCover({
recipe: "pinterest",
text: { title: "Casa Poblenou", location: "Barcelona", website: "https://example.com" },
});
const { bytes, mimeType } = await viewer.exportCover({ type: "png" });
img.src = URL.createObjectURL(new Blob([bytes], { type: mimeType }));文章中的模型
ui: 'article' 只有画布,模型填满画面,滚轮让读者继续阅读。用 frame() 选择角度。
const viewer = new IfcViewer("#figure", {
ui: "article", // canvas only, tight iso framing, wheel scrolls the page
model: "https://example.com/pavilion.ifc",
lazy: true, // nothing loads until the reader asks
poster: "/img/pavilion.jpg",
posterTitle: "The pavilion, live",
aspectRatio: "16/10", // no layout shift from poster to viewer
background: "auto", // paper on a light page, studio on a dark one
turntable: true, // a slow turn until the reader takes over
fullscreenButton: true,
validate: false,
});随文字推进的故事
固定查看器,让每个段落决定镜头:隔离墙体、绕行一周、引入傍晚的阳光。
// <div id="figure" style="position:sticky;top:1rem"></div>
// <p id="s1">…</p> <p id="s2">…</p> <p id="s3">…</p>
const viewer = new IfcViewer("#figure", { ui: "article", aspectRatio: "16/10", model: url });
viewer.bindSteps([
{ el: "#s1", frame: { view: "iso" } },
{ el: "#s2", isolate: "IfcWall", frame: { azimuth: 225, elevation: 20 } },
{ el: "#s3", isolate: null, solar: { active: true, date: "06-21", time: "19:30" } },
]);在你的页面中嵌入实时城市孪生
打开一个已发布的场景,响应它的告警和点击,在旁边添加你自己的实时数据源,并分享结果链接。
const viewer = new IfcViewer("#twin", {
ui: "client",
scene: "https://www.ifcvieweronline.eu/scenes/barcelona-placa-catalunya.scene.json",
});
viewer.on("alert", (a) => { if (a.kind === "start") notify(a.name + ": " + a.rule) });
viewer.on("layer-feature-picked", (f) => showCard(f.layer, f.properties));
// Your own live source next to the scene's (GeoJSON with CORS, every 60 s)
await viewer.addLayer({ url: "https://example.org/sensors.geojson", live: 60 });
const { link, sources } = await viewer.exportScene({ title: "Our twin" });你应用中的产品页
固定版本,交出文件,展示厂商写在类型里的数据:传热系数、抗风压等级、制造商。
import { IfcViewer } from "https://www.ifcvieweronline.eu/sdk/1.18.0/ifc-viewer.es.js";
const viewer = await IfcViewer.create("#product-3d", {
ui: "embed", // 3D + properties, Validate and Measure only
panels: ["properties"],
lang: "es",
});
const bytes = await fetch(product.ifcUrl).then(r => r.arrayBuffer());
await viewer.add("V-70-PR.ifc", bytes); // framed for you
// The product, by class (or by its GlobalId) — no expressIDs to know
const [win] = await viewer.findElements({ ifcClass: "IfcWindow" });
// What the manufacturer put in the type (IfcWindowType → HasPropertySets)
const el = await viewer.getElement(win.globalId);
const pset = (name) => el.effectivePropertySets.find(s => s.name === name);
const uw = pset("Pset_WindowCommon")?.properties.find(p => p.name === "ThermalTransmittance");
console.log(el.typeName, el.globalId, uw?.value, uw?.unit); // "Ventana V-70 practicable" … 1.2 "W/(m²·K)"
const { qualityScore } = await viewer.validate(); // Health Score, on demand
// Declared performance per EN 14351-1: wind, water, acoustics, Uw, g / τv, DoP
const en = await viewer.checkEir("builtin-en14351-1");
const missing = en.specs.filter(s => s.status === "fail").map(s => s.name);适配你的品牌
用一个强调色让查看器与你的产品保持一致。
new IfcViewer("#viewer", { accent: "#3fa866" });
// …or on the web component:
// <ifc-viewer accent="#3fa866" model="…"></ifc-viewer>语言选择器
用查看器自带的语言构建一个选择器。
// build a <select> from the bundled languages
IfcViewer.LANGUAGES.forEach(({ code, label }) => addOption(code, label));
// switch at runtime
viewer.setLanguage("ja");Health Score
校验生命周期
validate: true 会在每个模型加载完成后校验;validate() 按需运行并以结果解析。getValidation() 在各个时刻返回什么:
| 时刻 | getValidation() 返回 |
|---|---|
| 尚未校验 | null —— getValidationStatus() 显示 idle。 |
| 首次运行进行中 | null —— getValidationStatus() 显示 running 及进度;validation-started 已触发。 |
| 运行完成后 | { qualityScore, errors, warnings, info, total, status: 'done', modelId }。多个模型时为汇总(modelId: null)。 |
| 新一轮运行进行中 | 上一次的数字,status: 'running'。 |
| 运行失败后 | 上一次的数字,status: 'error' 并附 error —— 若之前没有则为 null。已触发 validation-failed,validate() 已拒绝。 |
版本
固定版本
自 1.18.0 起,每个版本还会发布到一个永不改变的路径。生产环境请固定版本;可变路径始终指向最新版本。
<script type="module">
import { IfcViewer } from "https://www.ifcvieweronline.eu/sdk/1.18.0/ifc-viewer.es.js";
</script>| 路径 | 缓存 | 用途 |
|---|---|---|
/sdk/ifc-viewer.es.js | 每次加载都会重新验证 | 试用 —— 总是最新版本。 |
/sdk/1.18.0/ifc-viewer.es.js | 不可变,缓存一年 | 生产 —— 运行的正是你测试过的代码。 |
固定版本的 SDK 会打开哪个查看器?
iframe 是同源的在线查看器,而不是冻结的副本:固定版本的 SDK 保持自己的 API 与行为,而其中的查看器持续改进。查看器会响应任何已发布 SDK 发送的每条命令 —— 构建时会检查 —— 所以旧的固定版本依然可用。
同时固定内容
SDK 旁的 versions.json 列出了每个版本的 Subresource Integrity 哈希:
<script type="module" src="https://www.ifcvieweronline.eu/sdk/1.18.0/ifc-viewer.es.js"
integrity="sha384-WsT4d0DS1oqva4rLWCGBDrx50uZVA3B95IDWbfxaFt8lnjs6NgOrjKNcWQL57/0z" crossorigin="anonymous"></script>更新日志
1.18.0 —— 宿主应用中的目录对象。getElement() 返回 GlobalId 与类型数据(typeName、带单位的 typeProperties、effectivePropertySets;IFC4 类型与 IFC2x3 样式)。新增 validate()、getValidationStatus()、validation-started / validation-failed,validate: true 现在会在每次加载后真正校验。加载后自动框选,fit() 与 frame() 为小物体留出边距,frame(elementId),setView() 重新可以移动相机。ui: 'embed' 支持 toolbar 与 tools。固定版本位于 /sdk/1.18.0/。新增 findElements();getElement() / select() / element-selected 支持 GlobalId;门窗的框与扇属性集,以及类型的材料;在 ui: 'embed' 中属性面板会让选中的物体保持可见。 checkEir('builtin-en14351-1') 检查符合 EN 14351-1 的窗和外门的声明性能——抗风压、水密性、隔声、传热系数、g 与 τv、制造商与产品型号代码——基于标准 IFC 属性集;EIR 规则新增 where(仅检查具有某属性值的构件)和 optional。
帮助
疑难排查
少数几个容易踩到的坑。
什么都没渲染出来
依次检查:查看器是否到达 ready、模型是否加载成功(model-loaded 而非 model-error)、容器高度是否非零、字节来源是否为有效的 IFC。model-error 的消息会指出原因。
加载模型时出现 CORS 错误
CORS 只影响 addFromUrl(url)。如果字节在你手里,自行获取并调用 add(name, bytes) —— 因为数据是直接交付的,不涉及 CORS。
iframe 是空白的或拒绝加载
应用必须可被嵌入 —— 主机上不能有 X-Frame-Options: DENY,也不能有限制性的 frame-ancestors CSP。我们托管的应用允许嵌入。
调用 add() 后我的 ArrayBuffer 变空了
add() 会转移该 buffer 以实现零拷贝交付,这会让它在你的代码中变为 detached。若之后仍需这些字节,请传入副本(bytes.slice(0))。
加载或查询超时
add()/addFromUrl() 在 loadTimeout 后拒绝(默认 120 秒;0 表示禁用)。查询在 30 秒后拒绝,checkIds 在 120 秒后拒绝。慢设备上的大模型可能需要更大的 loadTimeout。
支持哪些浏览器?
任何支持 WebAssembly 与 ES 模块的现代常青浏览器 —— Chrome、Edge、Firefox、Safari。不支持 Internet Explorer。
在我本地的开发服务器上返回 404
SPA 开发服务器可能会用 HTML 回退拦截 /sdk/。请在真实构建产物或静态托管上测试,或直接导入 /sdk/ifc-viewer.es.js 模块。