撤銷更改
目前,撤銷一個有缺陷的變更集的唯一方法是從 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