撤销更改
目前,撤销一个有缺陷的变更集的唯一方法是从 iModel Hub 中移除它,这可能会导致许多副作用(无法撤销)。一个更好的方法是在时间线中撤销变更集,并将其作为新的变更集引入。尽管这种方法仍然具有侵入性,并且需要锁定Schema,但它更安全,因为它允许撤销操作恢复之前的变更,确保时间线中没有任何内容被永久删除。(任何操作都被记录了,有点像git了)
BriefcaseDb.revertAndPushChanges
允许推送一个单一的变更集,该变更集撤销从当前版本到指定历史变更集的所有变更。
以下是一些细节和要求:
-
在调用 iModel 时,它不能有任何本地修改。
-
该操作是原子性的;如果操作失败,数据库将恢复到其之前的状态。
-
撤销操作需要一个模式锁(对 iModel 的独占锁),因为它不会锁定受撤销影响的每个单独元素。
-
如果在撤销后没有提供描述,则会创建并推送一个默认的变更集描述,这将释放模式锁。
-
在模式同步(SchemaSync)期间不会撤销模式更改,或者在不使用模式同步时可以选择性地跳过模式更改。
具体的代码可参见
github\itwinjs-core\full-stack-tests\backend\src\integration\SchemaSync.test.ts
// 2. Insert a element for the classconst el1 = await createEl({ p1: "test1" });const el2 = await createEl({ p1: "test2" });b1.saveChanges();await b1.pushChanges({ description: "insert 2 elements" });// 3. Update the element.await updateEl(el1, { p1: "test3" });b1.saveChanges();await b1.pushChanges({ description: "update element 1" });// 4. Delete the element.await deleteEl(el2);const el3 = await createEl({ p1: "test4" });b1.saveChanges();await b1.pushChanges({ description: "delete element 2" });// 5. import schema and insert element 4 & update element 3await addPropertyAndImportSchema(b1);const el4 = await createEl({ p1: "test5", p2: "test6" });await updateEl(el3, { p1: "test7", p2: "test8" });b1.saveChanges();await b1.pushChanges({ description: "import schema, insert element 4 & update element 3" });assert.isDefined(findEl(el1));assert.isUndefined(findEl(el2));assert.isDefined(findEl(el3));assert.isDefined(findEl(el4));// eslint-disable-next-line @typescript-eslint/no-deprecatedassert.deepEqual(Object.getOwnPropertyNames(b1.getMetaData("TestDomain:Test2dElement").properties), ["p1", "p2"]);// 6. Revert to timeline 2await b2.revertAndPushChanges({ toIndex: 3, description: "revert to timeline 2" });assert.equal((await getChanges()).at(-1)!.description, "revert to timeline 2");await b1.pullChanges();assert.isUndefined(findEl(el1));assert.isUndefined(findEl(el2));assert.isUndefined(findEl(el3));assert.isUndefined(findEl(el4));// eslint-disable-next-line @typescript-eslint/no-deprecatedassert.deepEqual(Object.getOwnPropertyNames(b1.getMetaData("TestDomain:Test2dElement").properties), ["p1", "p2"]);await b2.revertAndPushChanges({ toIndex: 7, description: "reinstate last reverted changeset" });assert.equal((await getChanges()).at(-1)!.description, "reinstate last reverted changeset");await b1.pullChanges();assert.isDefined(findEl(el1));assert.isUndefined(findEl(el2));assert.isDefined(findEl(el3));assert.isDefined(findEl(el4));// eslint-disable-next-line @typescript-eslint/no-deprecatedassert.deepEqual(Object.getOwnPropertyNames(b1.getMetaData("TestDomain:Test2dElement").properties), ["p1", "p2"]);await addPropertyAndImportSchema(b1);const el5 = await createEl({ p1: "test9", p2: "test10", p3: "test11" });await updateEl(el1, { p1: "test12", p2: "test13", p3: "test114" });b1.saveChanges();await b1.pushChanges({ description: "import schema, insert element 5 & update element 1" });// eslint-disable-next-line @typescript-eslint/no-deprecatedassert.deepEqual(Object.getOwnPropertyNames(b1.getMetaData("TestDomain:Test2dElement").properties), ["p1", "p2", "p3"]);// skip schema changes & auto generated commentawait b1.revertAndPushChanges({ toIndex: 2, skipSchemaChanges: true });assert.equal((await getChanges()).at(-1)!.description, "Reverted changes from 9 to 2 (schema changes skipped)");assert.isUndefined(findEl(el1));assert.isUndefined(findEl(el2));assert.isUndefined(findEl(el3));assert.isUndefined(findEl(el4));assert.isUndefined(findEl(el5));// eslint-disable-next-line @typescript-eslint/no-deprecatedassert.deepEqual(Object.getOwnPropertyNames(b1.getMetaData("TestDomain:Test2dElement").properties), ["p1", "p2", "p3"]);await b1.revertAndPushChanges({ toIndex: 10 });assert.equal((await getChanges()).at(-1)!.description, "Reverted changes from 10 to 10 (schema changes skipped)");assert.isDefined(findEl(el1));assert.isUndefined(findEl(el2));assert.isDefined(findEl(el3));assert.isDefined(findEl(el4));assert.isDefined(findEl(el5));// eslint-disable-next-line @typescript-eslint/no-deprecatedassert.deepEqual(Object.getOwnPropertyNames(b1.getMetaData("TestDomain:Test2dElement").properties), ["p1", "p2", "p3"]);// schema sync should be skip for revertawait b2.pullChanges();const b3 = await HubWrappers.downloadAndOpenBriefcase({ iTwinId, iModelId: rwIModelId, accessToken: adminToken });assert.isTrue(SchemaSync.isEnabled(b3));await addPropertyAndImportSchema(b1);// eslint-disable-next-line @typescript-eslint/no-deprecatedassert.deepEqual(Object.getOwnPropertyNames(b1.getMetaData("TestDomain:Test2dElement").properties), ["p1", "p2", "p3", "p4"]);// b3 should get new property via schema syncawait b3.pullChanges();// eslint-disable-next-line @typescript-eslint/no-deprecatedassert.deepEqual(Object.getOwnPropertyNames(b3.getMetaData("TestDomain:Test2dElement").properties), ["p1", "p2", "p3", "p4"]);// b2 should not see new property even after revert// eslint-disable-next-line @typescript-eslint/no-deprecatedassert.deepEqual(Object.getOwnPropertyNames(b2.getMetaData("TestDomain:Test2dElement").properties), ["p1", "p2", "p3"]);await b2.revertAndPushChanges({ toIndex: 11 });assert.equal((await getChanges()).at(-1)!.description, "Reverted changes from 11 to 11 (schema changes skipped)");// eslint-disable-next-line @typescript-eslint/no-deprecatedassert.deepEqual(Object.getOwnPropertyNames(b2.getMetaData("TestDomain:Test2dElement").properties), ["p1", "p2", "p3"]);await b1.pullChanges();await b2.pullChanges();await b3.pullChanges();
从代码中可以看到revertAndPushChanges 可以支持:
- 回退
调用 revertAndPushChanges({ toIndex: N, description }),将当前 briefcase 的状态回退到第 N 个 changeset,并将回退结果作为新的 changeset 推送到服务器。
回退后,所有在 N 之后新增或修改的元素、属性都会被移除或还原,Schema 结构也会恢复到当时的状态。
- 恢复
调用 revertAndPushChanges({ toIndex: N, description }),将当前 briefcase 的状态回退到第 N 个 changeset,并将回退结果作为新的 changeset 推送到服务器。
回退后,所有在 N 之后新增或修改的元素、属性都会被移除或还原,Schema 结构也会恢复到当时的状态。
- 跳过 Schema 变更的回退
支持 skipSchemaChanges: true,只回退数据变更,保留 Schema 结构不变。
用于只想撤销数据操作而不影响 Schema 的场景。
Display
实例
某些场景需要反复显示相同的图形。例如,想象你正在编写一个装饰器(Decorator),用于在道路网络的许多交叉路口显示停车标志。你可能会为每个单独的停车标志创建一个 RenderGraphic
并绘制它们,但这样做会通过多次复制相同的几何形状而浪费大量内存,并且通过多次调用绘制操作而降低帧率。简而言之,将一个 glTF 模型在多个指定的位置以实例化方式高效渲染
WebGL 提供了实例化渲染(instanced rendering),以更高效地支持此类用例。你可以定义一个停车标志图形的单一表示形式,然后告诉渲染器在不同的位置、方向和缩放下多次绘制它。iTwin.js 现在提供了易于使用的 API,以便你创建实例化图形:
-
GraphicTemplate
定义图形的外观。你可以通过GraphicBuilder.finishTemplate
、RenderSystem.createTemplateFromDescription
或readGltfTemplate
获取模板。 -
RenderInstances
定义要绘制的模板实例集合。除了变换矩阵(Transform)之外,每个实例还可以覆盖模板外观的某些方面,如颜色和线宽,并且每个实例都可以有一个独特的Feature
,以便每个实例都能作为一个独立的实体行为。你可以使用RenderInstancesParamsBuilder
创建RenderInstances
。 -
RenderSystem.createGraphicFromTemplate
从图形模板和实例集合生成RenderGraphic
。 -
GraphicTemplate
和RenderInstances
都是可重用的——你可以为给定的模板生成多个实例集合,并将相同的实例集合用于多个不同的模板。
对于上述停车标志的例子,你可能有一个表示停车标志的 glTF 模型和一个包含每个停车标志位置的数组。然后,你可以使用以下函数生成一个在这些位置绘制停车标志的图形:
export async function instanceGltfModel(gltf: Uint8Array | object, positions: Point3d[], iModel: IModelConnection): Promise<RenderGraphic> {// Decode the raw glTF as an instanceable template.const template = (await readGltfTemplate({ gltf, iModel }))?.template;if (!template) {throw new Error("Failed to decode glTF model.");}// Generate an Id for a "model" to contain the instances.const modelId = iModel.transientIds.getNext();// Define multiple instances, one at each of the specified positions.const instancesBuilder = RenderInstancesParamsBuilder.create({ modelId });for (const position of positions) {instancesBuilder.add({// Translate to the specified position.transform: Transform.createTranslation(position),// Assign a unique pickable Id.feature: iModel.transientIds.getNext(),});}const instancesParams = instancesBuilder.finish();const instances = IModelApp.renderSystem.createRenderInstances(instancesParams);// Create a graphic that associates the instances with the template.return IModelApp.renderSystem.createGraphicFromTemplate({ template, instances });
}
主要流程说明
-
解码 glTF 模板
使用 readGltfTemplate 将传入的 glTF 数据(可以是二进制或对象)解码为可实例化的模板(template)。如果解码失败则抛出异常。 -
生成临时模型 Id
通过 iModel.transientIds.getNext() 生成一个唯一的临时模型 Id,用于标识这些实例属于哪个“模型”。 -
构建实例参数
使用 RenderInstancesParamsBuilder 创建实例参数构建器。遍历所有位置(positions),为每个位置:- 创建一个平移变换(Transform),将 glTF 模型移动到该位置。
- 分配一个唯一的 feature Id,便于后续拾取和高亮。
-
生成实例参数和实例对象
调用 finish() 得到所有实例参数,再用 IModelApp.renderSystem.createRenderInstances 创建实例对象。 -
生成最终渲染图形
调用 IModelApp.renderSystem.createGraphicFromTemplate,将模板和实例对象组合成一个可渲染的图形(RenderGraphic),用于在视图中显示。
覆盖线条颜色
iTwin.js 允许在显示时,动态覆盖几何图形的外观。然而,与 SubCategoryAppearance
和 GeometryParams
不同,它们可以区分“线条颜色”和“填充颜色”,而 FeatureAppearance
只提供一个单一的颜色覆盖,适用于所有类型的几何图形。
为了解决这种差异,我们新增了一种方法,允许你独立于其他几何图形动态覆盖线性几何图形的颜色和透明度。线性几何图形包括开放曲线、线字符串、点字符串以及平面区域的轮廓。
详细解释
-
FeatureAppearance.lineRgb
:-
作用:控制线性几何图形的颜色。
-
未定义:如果未定义,线性几何图形将使用
FeatureAppearance.rgb
的颜色。 -
false
:表示不对线性几何图形的颜色进行覆盖。 -
指定颜色:可以指定一个
RgbColor
,仅适用于线性几何图形。
-
-
FeatureAppearance.lineTransparency
:-
作用:控制线性几何图形的透明度。
-
未定义:如果未定义,线性几何图形将使用
FeatureAppearance.transparency
的透明度。 -
false
:表示不对线性几何图形的透明度进行覆盖。
-
FeatureAppearance.fromJSON({rgb: RgbColor.fromColorDef(ColorDef.white),lineRgb: false,lineTransparency: 0.5,}),
控制实景显示隐藏
之前通过 DisplayStyleState.attachRealityModel
方法添加实景模型(Context Reality Model),现在可以通过启用 ContextRealityModel.invisible
标志来隐藏。
import { ContextRealityModelProps, ContextRealityModels } from "@itwin/core-common";// 构造实景模型属性
const realityModelProps: ContextRealityModelProps = {tilesetUrl: "https://your-reality-data-url/tileset.json",name: "实景模型名称",description: "描述信息",// 还可以设置 appearanceOverrides、planarClipMask、classifiers 等
};// 获取当前视图的 DisplayStyleSettings
const displayStyle = viewport.displayStyle.settings;// 获取或创建 ContextRealityModels
const contextRealityModels = displayStyle.contextRealityModels;// 添加实景模型
const realityModel = contextRealityModels.add(realityModelProps);// 隐藏实景模型
realityModel.invisible = true;// 显示实景模型
realityModel.invisible = false;
更复杂的控制(如外观覆盖、裁剪等),可以设置 appearanceOverrides、planarClipMask 等属性。
等高线展示
const contourDisplayProps: ContourDisplayProps = {displayContours: true,groups: [{contourDef: {showGeometry: true,majorStyle: { color: ..., pixelWidth: ..., pattern: ... },minorStyle: { color: ..., pixelWidth: ..., pattern: ... },minorInterval: 2,majorIntervalCount: 8,},subCategories: CompressedId64Set.sortAndCompress([ "0x5b", "0x5a" ]),},{contourDef: {showGeometry: false,majorStyle: { ... },minorStyle: { ... },minorInterval: 1,majorIntervalCount: 7,},subCategories: CompressedId64Set.sortAndCompress([ "0x5c", "0x6a" ]),},],
};
- displayContours: true 必须为 true,才会显示等高线。
- groups 数组定义了多个等高线分组,每组可以指定不同的样式和适用的 subCategory(子类别)。
- contourDef 里可以分别设置主等高线(major)和次等高线(minor)的颜色、线宽、线型、间隔等。
- showGeometry 控制是否同时显示原始几何体。
交互工具
元素定位
在调用 ElementLocateManager.doLocate
之后,现在可以使用 Reset 来选择一些被其他元素遮挡的元素。以前,Reset 只会在定位孔径(locate aperture)内选择可见的元素。
截面视图
- 在 iTwin.js 中,视图可以嵌套或附加,比如:
- 一个 SheetView 可以通过 ViewAttachment 附加其他视图;
- 一个 DrawingView 可以通过 SectionDrawing 附加 SpatialView。
- 当你在屏幕上点击或定位元素时,实际命中的几何体可能并不直接属于当前主视图,而是属于某个被附加的子视图。
- HitPath 结构体就用来描述这种“命中路径”,它包含两个可选属性:
- viewAttachment:描述命中点是通过哪个 ViewAttachment 附加视图获得的(如 SheetView 附加的 DrawingView)。
- sectionDrawingAttachment:描述命中点是通过哪个 SectionDrawing 附加视图获得的(如 DrawingView 附加的 SpatialView)
Presentation
扩展数据属性
计算属性规范中新增了一个可选的 extendedData
属性。该属性允许将计算属性字段与一些额外信息关联起来,这在动态创建的计算属性中可能特别有用
量值支持
添加对“比例”格式类型的支持(例如“1:2”)
示例:格式化比例
假设已经注册并初始化了一个 UnitsProvider
,以下是格式化比例的方法:
const ratioFormatProps: FormatProps = {type: "Ratio",ratioType: "OneToN", // Formats the ratio in "1:N" formcomposite: {includeZero: true,units: [{ name: "Units.HORIZONTAL_PER_VERTICAL" },],},
};const ratioFormat = new Format("Ratio");
ratioFormat.fromJSON(unitsProvider, ratioFormatProps).catch(() => {});
支持Node22
支持Electron 33
API 的弃用
@itwin/appui-abstract
LayoutFragmentProps
、ContentLayoutProps
、LayoutSplitPropsBase
、LayoutHorizontalSplitProps
、LayoutVerticalSplitProps
和 StandardContentLayouts
已被弃用。请改用 @itwin/appui-react 中的相同 API。
BackendItemsManager
是内部 API,本不应被外部使用。它已被弃用,并将在 5.0.0 版本中被移除。请改用 @itwin/appui-react 中的 UiFramework.backstage
。
@itwin/core-backend
IModelHost.snapshotFileNameResolver
和 FileNameResolver
已被弃用。请确保为 SnapshotConnection.openFile
提供已解析的文件路径。
@itwin/core-frontend
SnapshotConnection.openRemote
已被弃用。在 Web 应用程序中,使用 CheckpointConnection.openRemote
来打开对 iModel 的连接。
@itwin/core-quantity
-
FormatType
、ScientificType
和ShowSignOption
已从整型枚举重构为字符串枚举,并新增了RatioType
作为字符串枚举。由于字符串枚举不需要序列化方法,相关的toString
函数(包括formatTypeToString
、scientificTypeToString
和showSignOptionToString
)已被弃用。 -
Parser.parseToQuantityValue
已被弃用。请改用现有的Parser.parseQuantityString
方法。
@itwin/presentation-common
PresentationRpcInterface
的所有公共方法已被弃用。今后,不应直接调用 RPC 接口。应改用公共包装器,例如 PresentationManager
。
已弃用的 ECSqlStatement
ECSqlStatement
在 4.11 版本中已被弃用。请改用 IModelDb.createQueryReader
或 ECDb.createQueryReader
。
以下与 ECSqlStatement
相关的类也被标记为弃用:
-
ECEnumValue
-
ECSqlValue
-
ECSqlValueIterator
-
ECSqlColumnInfo
在并发查询中,QueryOptions.convertClassIdsToClassNames
和 QueryOptionsBuilder.setConvertClassIdsToNames()
已被弃用。请改用 ECSQL 的 ec_classname()
函数将类 ID 转换为类名称。
即将被移除的 API
以下 @itwin/core-common 中的 API 正在从 @itwin/core-bentley 重新导出,并且在下一个主版本中将被移除,而不会提前标记为弃用。请改为从 @itwin/core-bentley 导入它们。
-
BentleyStatus
-
BentleyError
-
IModelStatus
-
BriefcaseStatus
-
DbResult
-
ChangeSetStatus
-
GetMetaDataFunction
-
LogFunction
-
LoggingMetaData