Files
speckle-server/packages/viewer/src/modules/extensions/DiffExtension.ts
T
Alexandru Popovici a43aaedcca On Demand Automatic RTE (#4808)
* chore(viewer-lib): testing numbers

* feat(viewer-lib): Implemented a way to compute the projected pixxel difference between fp32 and fp64 as it would happen on the GPU. With this we can determine which streams need RTE

* feata(viewer-lib): Proper precision loss detection

* feat(viewer-lib): Removed default 'USE_RTE' defines from all non instanced mesh materials. Updated standard shader to compile and work properly in non RTE mode

* fix(viewer-lib): non-RTE vertex transform also takes pivot into account

* feat(viewer-lib): All required vertex programs now have proper non-RT vertex and shadow transformation pipelines

* feat(viewer-lib): Mesh batch determines if it needs RTE on it's own

* feat(viewer-lib): Shadowmap now also renders properly with selective RTE

* feat(viewer-lib): Instanced batches no longer use RTE regardless of the RTE need source

* feat(viewer-lib): Lines now have no RTE by default and it's only enabled on demand by the batch when required

* feat(viewer-lib): Points and point clouds no longer use RTE by default. It's enabled on demand
2025-05-26 15:22:26 +03:00

735 lines
24 KiB
TypeScript

import { Color, DoubleSide, FrontSide, Material } from 'three'
import { type TreeNode, WorldTree } from '../tree/WorldTree.js'
import Logger from '../utils/Logger.js'
import { groupBy } from 'lodash-es'
import { GeometryType } from '../batching/Batch.js'
import SpeckleLineMaterial from '../materials/SpeckleLineMaterial.js'
import SpecklePointMaterial from '../materials/SpecklePointMaterial.js'
import SpeckleStandardMaterial from '../materials/SpeckleStandardMaterial.js'
import { NodeRenderView } from '../tree/NodeRenderView.js'
import { type IViewer } from '../../IViewer.js'
import { Extension } from './Extension.js'
import { SpeckleTypeAllRenderables } from '../loaders/GeometryConverter.js'
import { SpeckleLoader } from '../loaders/Speckle/SpeckleLoader.js'
import { GPass } from '../pipeline/Passes/GPass.js'
import { DepthPass } from '../pipeline/Passes/DepthPass.js'
type SpeckleMaterialType =
| SpeckleStandardMaterial
| SpecklePointMaterial
| SpeckleLineMaterial
export enum VisualDiffMode {
PLAIN,
COLORED
}
export interface DiffResult {
unchanged: Array<TreeNode>
added: Array<TreeNode>
removed: Array<TreeNode>
modified: Array<Array<TreeNode>>
}
interface VisualDiffResult {
unchanged: Array<NodeRenderView>
added: Array<NodeRenderView>
removed: Array<NodeRenderView>
modifiedOld: Array<NodeRenderView>
modifiedNew: Array<NodeRenderView>
}
export class DiffExtension extends Extension {
public get enabled(): boolean {
return this._enabled
}
public set enabled(value: boolean) {
this._enabled = value
}
protected tree: WorldTree
private addedMaterialMesh: SpeckleStandardMaterial
private changedNewMaterialMesh: SpeckleStandardMaterial
private changedOldMaterialMesh: SpeckleStandardMaterial
private removedMaterialMesh: SpeckleStandardMaterial
private addedMaterialPoint: SpecklePointMaterial
private changedNewMaterialPoint: SpecklePointMaterial
private changedOldMaterialPoint: SpecklePointMaterial
private removedMaterialPoint: SpecklePointMaterial
private addedMaterials: Array<SpeckleMaterialType> = []
private changedOldMaterials: Array<SpeckleMaterialType> = []
private changedNewMaterials: Array<SpeckleMaterialType> = []
private removedMaterials: Array<SpeckleMaterialType> = []
private _materialGroups:
| {
rvs: NodeRenderView[]
material: SpeckleMaterialType
}[]
| null
private _visualDiff!: VisualDiffResult
private _diffTime = -1
private _diffMode: VisualDiffMode = VisualDiffMode.COLORED
public constructor(viewer: IViewer) {
super(viewer)
this.tree = viewer.getWorldTree()
this.addedMaterialMesh = new SpeckleStandardMaterial({
color: new Color('#00ff00'),
emissive: 0x0,
roughness: 1,
metalness: 0,
opacity: 1,
side: FrontSide
})
this.addedMaterialMesh.vertexColors = false
this.addedMaterialMesh.depthWrite = true
this.addedMaterialMesh.transparent = true
this.addedMaterialMesh.clipShadows = true
this.addedMaterialMesh.color.convertSRGBToLinear()
this.addedMaterialMesh.clippingPlanes = []
this.changedNewMaterialMesh = new SpeckleStandardMaterial({
color: new Color('#ffff00'),
emissive: 0x0,
roughness: 1,
metalness: 0,
opacity: 1,
side: FrontSide
})
this.changedNewMaterialMesh.vertexColors = false
this.changedNewMaterialMesh.transparent = true
this.changedNewMaterialMesh.depthWrite = true
this.changedNewMaterialMesh.clipShadows = true
this.changedNewMaterialMesh.color.convertSRGBToLinear()
this.changedNewMaterialMesh.clippingPlanes = []
this.changedOldMaterialMesh = new SpeckleStandardMaterial({
color: new Color('#ffff00'),
emissive: 0x0,
roughness: 1,
metalness: 0,
opacity: 1,
side: FrontSide
})
this.changedOldMaterialMesh.vertexColors = false
this.changedOldMaterialMesh.transparent = true
this.changedOldMaterialMesh.depthWrite = true
this.changedOldMaterialMesh.clipShadows = true
this.changedOldMaterialMesh.color.convertSRGBToLinear()
this.changedOldMaterialMesh.clippingPlanes = []
this.removedMaterialMesh = new SpeckleStandardMaterial({
color: new Color('#ff0000'),
emissive: 0x0,
roughness: 1,
metalness: 0,
opacity: 1,
side: FrontSide
})
this.removedMaterialMesh.vertexColors = false
this.removedMaterialMesh.transparent = true
this.removedMaterialMesh.depthWrite = true
this.removedMaterialMesh.clipShadows = true
this.removedMaterialMesh.color.convertSRGBToLinear()
this.removedMaterialMesh.clippingPlanes = []
this.addedMaterialPoint = new SpecklePointMaterial({
color: 0x00ff00,
vertexColors: false,
size: 2,
sizeAttenuation: false
})
this.addedMaterialPoint.transparent = true
this.addedMaterialPoint.color.convertSRGBToLinear()
this.addedMaterialPoint.toneMapped = false
this.addedMaterialPoint.clippingPlanes = []
this.changedNewMaterialPoint = new SpecklePointMaterial({
color: 0xffff00,
vertexColors: false,
size: 2,
sizeAttenuation: false
})
this.changedNewMaterialPoint.transparent = true
this.changedNewMaterialPoint.color.convertSRGBToLinear()
this.changedNewMaterialPoint.toneMapped = false
this.changedNewMaterialPoint.clippingPlanes = []
this.changedOldMaterialPoint = new SpecklePointMaterial({
color: 0xffff00,
vertexColors: false,
size: 2,
sizeAttenuation: false
})
this.changedOldMaterialPoint.transparent = true
this.changedOldMaterialPoint.color.convertSRGBToLinear()
this.changedOldMaterialPoint.toneMapped = false
this.changedOldMaterialPoint.clippingPlanes = []
this.removedMaterialPoint = new SpecklePointMaterial({
color: 0xff0000,
vertexColors: false,
size: 2,
sizeAttenuation: false
})
this.removedMaterialPoint.transparent = true
this.removedMaterialPoint.color.convertSRGBToLinear()
this.removedMaterialPoint.toneMapped = false
this.removedMaterialPoint.clippingPlanes = []
}
private dynamicallyLoadedDiffResources = [] as string[]
public async diff(
urlA: string,
urlB: string,
mode: VisualDiffMode,
authToken?: string
): Promise<DiffResult> {
const loadPromises = []
this.dynamicallyLoadedDiffResources = []
if (!this.tree.findId(urlA)) {
loadPromises.push(
this.viewer.loadObject(
new SpeckleLoader(this.viewer.getWorldTree(), urlA, authToken),
false
)
)
this.dynamicallyLoadedDiffResources.push(urlA)
}
if (!this.tree.findId(urlB)) {
loadPromises.push(
this.viewer.loadObject(
new SpeckleLoader(this.viewer.getWorldTree(), urlB, authToken),
false
)
)
this.dynamicallyLoadedDiffResources.push(urlB)
}
await Promise.all(loadPromises)
const diffResult = await this.getDiff(urlA, urlB)
const depthPasses = this.viewer.getRenderer().pipeline.getPass('DEPTH')
depthPasses.forEach((value: GPass) => {
;(value as DepthPass).depthSide = FrontSide
})
this.updateVisualDiff(0, mode)
return Promise.resolve(diffResult)
}
/** Currently, the diff does not store the existing materials. We can do that if we need to */
public async undiff(): Promise<void> {
const depthPasses = this.viewer.getRenderer().pipeline.getPass('DEPTH')
depthPasses.forEach((value: GPass) => {
;(value as DepthPass).depthSide = DoubleSide
})
this.resetMaterialGroups()
this.viewer.getRenderer().resetMaterials()
const unloadPromises = []
if (this.dynamicallyLoadedDiffResources.length !== 0) {
for (const id of this.dynamicallyLoadedDiffResources)
unloadPromises.push(this.viewer.unloadObject(id))
}
this.dynamicallyLoadedDiffResources = []
await Promise.all(unloadPromises)
}
private buildIdMaps(
rvs: Array<TreeNode>,
idMap: { [id: string]: { node: TreeNode; applicationId: string } },
appIdMap: { [id: string]: TreeNode }
) {
for (let k = 0; k < rvs.length; k++) {
const atomicRv = rvs[k]
const applicationId = atomicRv.model.raw.applicationId
? atomicRv.model.raw.applicationId
: this.tree
.getAncestors(atomicRv)
.find((value) => value.model.raw.applicationId)?.model.raw.applicationId
idMap[atomicRv.model.raw.id] = {
node: atomicRv,
applicationId
}
if (applicationId) {
appIdMap[applicationId] = atomicRv
}
}
}
private async getDiff(urlA: string, urlB: string): Promise<DiffResult> {
const diffResult = await this.diffIterative(urlA, urlB)
this._visualDiff = this.getVisualDiffResult(diffResult)
return Promise.resolve(diffResult)
}
private diffIterative(urlA: string, urlB: string): Promise<DiffResult> {
const diffResult: DiffResult = {
unchanged: [],
added: [],
removed: [],
modified: []
}
const renderTreeA = this.tree.getRenderTree(urlA)
const renderTreeB = this.tree.getRenderTree(urlB)
if (!renderTreeA) {
return Promise.reject(
new Error(`Could not make diff. Resource ${urlA} could not be fetched`)
)
}
if (!renderTreeB) {
return Promise.reject(
new Error(`Could not make diff. Resource ${urlB} could not be fetched`)
)
}
let rvsA: TreeNode[] = renderTreeA.getRenderableNodes(...SpeckleTypeAllRenderables)
let rvsB: TreeNode[] = renderTreeB.getRenderableNodes(...SpeckleTypeAllRenderables)
rvsA = rvsA.map((value) => {
return renderTreeA.getAtomicParent(value)
})
rvsB = rvsB.map((value) => {
return renderTreeB.getAtomicParent(value)
})
rvsA = [...Array.from(new Set(rvsA))]
rvsB = [...Array.from(new Set(rvsB))]
const idMapA: { [id: string]: { node: TreeNode; applicationId: string } } = {}
const appIdMapA: { [id: string]: TreeNode } = {}
this.buildIdMaps(rvsA, idMapA, appIdMapA)
const idMapB: { [id: string]: { node: TreeNode; applicationId: string } } = {}
const appIdMapB: { [id: string]: TreeNode } = {}
this.buildIdMaps(rvsB, idMapB, appIdMapB)
for (let k = 0; k < rvsB.length; k++) {
const res = idMapA[rvsB[k].model.raw.id]?.node
if (res) {
diffResult.unchanged.push(res)
} else {
const applicationId = idMapB[rvsB[k].model.raw.id].applicationId
if (!applicationId) {
Logger.error(
`No application ID found. Object id:${rvsB[k].model.raw.id} is considered 'added'!`
)
diffResult.added.push(rvsB[k])
continue
}
const res2 = appIdMapA[applicationId]
if (res2) {
diffResult.modified.push([res2, rvsB[k]])
} else {
diffResult.added.push(rvsB[k])
}
}
}
for (let k = 0; k < rvsA.length; k++) {
const res = idMapB[rvsA[k].model.raw.id]?.node
if (!res) {
const applicationId = idMapA[rvsA[k].model.raw.id].applicationId
if (!applicationId) {
Logger.error(
`No application ID found. Object id:${rvsA[k].model.raw.id} is considered 'removed'!`
)
diffResult.removed.push(rvsA[k])
continue
}
const res2 = appIdMapB[applicationId]
if (!res2) {
diffResult.removed.push(rvsA[k])
}
} else {
diffResult.unchanged.push(res)
}
}
return Promise.resolve(diffResult)
}
public updateVisualDiff(time?: number, mode?: VisualDiffMode): void {
if ((mode !== undefined && mode !== this._diffMode) || !this._materialGroups) {
this.resetMaterialGroups()
/** Catering to typescript */
if (mode !== undefined) {
this.buildMaterialGroups(mode)
this._diffMode = mode
}
}
if (time !== undefined && time !== this._diffTime) {
this.setDiffTime(time)
this._diffTime = time
}
if (this._materialGroups)
this._materialGroups.forEach((value) => {
this.viewer.getRenderer().setMaterial(value.rvs, value.material)
})
this.viewer.requestRender()
}
private setDiffTime(time: number) {
const from = Math.min(Math.max(1 - time, 0), 1)
const to = Math.min(Math.max(time, 0), 1)
this.addedMaterials.forEach((mat) => {
mat.opacity =
(mat as never)['clampOpacity'] !== undefined
? Math.min(from, (mat as never)['clampOpacity'])
: from
mat.depthWrite = from < 0.5 ? false : true
mat.transparent = mat.opacity < 1
mat.needsCopy = true
})
this.changedOldMaterials.forEach((mat) => {
mat.opacity =
(mat as never)['clampOpacity'] !== undefined
? Math.min(to, (mat as never)['clampOpacity'])
: to
mat.depthWrite = to < 0.5 ? false : true
mat.transparent = mat.opacity < 1
mat.needsCopy = true
})
this.changedNewMaterials.forEach((mat) => {
mat.opacity =
(mat as never)['clampOpacity'] !== undefined
? Math.min(from, (mat as never)['clampOpacity'])
: from
mat.depthWrite = from < 0.5 ? false : true
mat.transparent = mat.opacity < 1
mat.needsCopy = true
})
this.removedMaterials.forEach((mat) => {
mat.opacity =
(mat as never)['clampOpacity'] !== undefined
? Math.min(to, (mat as never)['clampOpacity'])
: to
mat.depthWrite = to < 0.5 ? false : true
mat.transparent = mat.opacity < 1
mat.needsCopy = true
})
}
private buildMaterialGroups(mode: VisualDiffMode) {
switch (mode) {
case VisualDiffMode.COLORED:
this._materialGroups = this.getColoredMaterialGroups(this._visualDiff)
break
case VisualDiffMode.PLAIN:
this._materialGroups = this.getPlainMaterialGroups(this._visualDiff)
break
default:
Logger.error(`Unsupported visual diff mode ${mode}`)
}
}
private resetMaterialGroups() {
this._materialGroups = null
this.addedMaterials = []
this.changedOldMaterials = []
this.changedNewMaterials = []
this.removedMaterials = []
}
private getVisualDiffResult(diffResult: DiffResult): VisualDiffResult {
const renderTree = this.tree.getRenderTree()
const addedRvs = diffResult.added.flatMap((value) => {
return renderTree.getRenderViewsForNode(value)
})
const removedRvs = diffResult.removed.flatMap((value) => {
return renderTree.getRenderViewsForNode(value)
})
const unchangedRvs = diffResult.unchanged.flatMap((value) => {
return renderTree.getRenderViewsForNode(value)
})
const modifiedOldRvs = diffResult.modified
.flatMap((value) => {
return renderTree.getRenderViewsForNode(value[0])
})
.filter((value) => {
return !unchangedRvs.includes(value) && !removedRvs.includes(value)
})
const modifiedNewRvs = diffResult.modified
.flatMap((value) => {
return renderTree.getRenderViewsForNode(value[1])
})
.filter((value) => {
return !unchangedRvs.includes(value) && !addedRvs.includes(value)
})
return {
unchanged: unchangedRvs,
added: addedRvs,
removed: removedRvs,
modifiedOld: modifiedOldRvs,
modifiedNew: modifiedNewRvs
}
}
private getColoredMaterialGroups(visualDiffResult: VisualDiffResult) {
const groups = [
// MESHES & LINES
// Currently lines work with mesh specific materials due to how the LineBatch is implemented.
// We could use specific line materials, but it won't make a difference until we elevate the
// LineBatch a bit
{
rvs: visualDiffResult.added.filter(
(value) =>
value.geometryType === GeometryType.MESH ||
value.geometryType === GeometryType.LINE
),
material: this.addedMaterialMesh
},
{
rvs: visualDiffResult.modifiedNew.filter(
(value) =>
value.geometryType === GeometryType.MESH ||
value.geometryType === GeometryType.LINE
),
material: this.changedNewMaterialMesh
},
{
rvs: visualDiffResult.modifiedOld.filter(
(value) =>
value.geometryType === GeometryType.MESH ||
value.geometryType === GeometryType.LINE
),
material: this.changedOldMaterialMesh
},
{
rvs: visualDiffResult.removed.filter(
(value) =>
value.geometryType === GeometryType.MESH ||
value.geometryType === GeometryType.LINE
),
material: this.removedMaterialMesh
},
//POINTS
{
rvs: visualDiffResult.added.filter(
(value) =>
value.geometryType === GeometryType.POINT ||
value.geometryType === GeometryType.POINT_CLOUD
),
material: this.addedMaterialPoint
},
{
rvs: visualDiffResult.modifiedNew.filter(
(value) =>
value.geometryType === GeometryType.POINT ||
value.geometryType === GeometryType.POINT_CLOUD
),
material: this.changedNewMaterialPoint
},
{
rvs: visualDiffResult.modifiedOld.filter(
(value) =>
value.geometryType === GeometryType.POINT ||
value.geometryType === GeometryType.POINT_CLOUD
),
material: this.changedOldMaterialPoint
},
{
rvs: visualDiffResult.removed.filter(
(value) =>
value.geometryType === GeometryType.POINT ||
value.geometryType === GeometryType.POINT_CLOUD
),
material: this.removedMaterialPoint
}
]
this.addedMaterials.push(this.addedMaterialMesh, this.addedMaterialPoint)
this.changedOldMaterials.push(
this.changedOldMaterialMesh,
this.changedOldMaterialPoint
)
this.changedNewMaterials.push(
this.changedNewMaterialMesh,
this.changedNewMaterialPoint
)
this.removedMaterials.push(this.removedMaterialMesh, this.removedMaterialPoint)
return groups.filter((value) => value.rvs.length > 0)
}
private getPlainMaterialGroups(visualDiffResult: VisualDiffResult) {
const added = this.getBatchesSubgroups(visualDiffResult.added)
const changedOld = this.getBatchesSubgroups(visualDiffResult.modifiedOld)
const changedNew = this.getBatchesSubgroups(visualDiffResult.modifiedNew)
const removed = this.getBatchesSubgroups(visualDiffResult.removed)
this.addedMaterials = added.map(
(value: { rvs: NodeRenderView[]; material: SpeckleMaterialType }) =>
value.material
)
this.changedOldMaterials = changedOld.map(
(value: { rvs: NodeRenderView[]; material: SpeckleMaterialType }) =>
value.material
)
this.changedNewMaterials = changedNew.map(
(value: { rvs: NodeRenderView[]; material: SpeckleMaterialType }) =>
value.material
)
this.removedMaterials = removed.map(
(value: { rvs: NodeRenderView[]; material: SpeckleMaterialType }) =>
value.material
)
return [...added, ...changedOld, ...changedNew, ...removed]
}
private getBatchesSubgroups(subgroup: Array<NodeRenderView>): {
rvs: NodeRenderView[]
material: SpeckleMaterialType
}[] {
const groupBatches = groupBy(subgroup, 'batchId')
const materialGroup: {
rvs: NodeRenderView[]
material: SpeckleMaterialType
}[] = []
for (const k in groupBatches) {
const matClone: SpeckleMaterialType = (
this.viewer.getRenderer().getBatchMaterial(groupBatches[k][0]) as Material
).clone() as SpeckleMaterialType
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(matClone as any)['clampOpacity'] = matClone.opacity
matClone.opacity = 0.5
matClone.transparent = true
materialGroup.push({
rvs: groupBatches[k],
material: matClone
})
}
return materialGroup
}
/** Keeping this for reference */
// private intersection(o1: object, o2: object) {
// const [k1, k2] = [Object.keys(o1), Object.keys(o2)]
// const [first, next] = k1.length > k2.length ? [k2, o1] : [k1, o2]
// return first.filter((k) => k in next)
// }
// private diffBoolean(urlA: string, urlB: string): Promise<DiffResult> {
// const diffResult: DiffResult = {
// unchanged: [],
// added: [],
// removed: [],
// modified: []
// }
// const renderTreeA = this.tree!.getRenderTree(urlA)
// const renderTreeB = this.tree!.getRenderTree(urlB)
// if (!renderTreeA) {
// return Promise.reject(
// `Could not make diff. Resource ${urlA} could not be fetched`
// )
// }
// if (!renderTreeB) {
// return Promise.reject(
// `Could not make diff. Resource ${urlB} could not be fetched`
// )
// }
// let rvsA: TreeNode[] = renderTreeA.getRenderableNodes(...SpeckleTypeAllRenderables)
// let rvsB: TreeNode[] = renderTreeB.getRenderableNodes(...SpeckleTypeAllRenderables)
// rvsA = rvsA.map((value: TreeNode) => {
// return renderTreeA.getAtomicParent(value)
// })
// rvsB = rvsB.map((value) => {
// return renderTreeB.getAtomicParent(value)
// })
// rvsA = [...Array.from(new Set(rvsA))]
// rvsB = [...Array.from(new Set(rvsB))]
// const idMapA: { [id: string]: { node: TreeNode; applicationId: string } } = {}
// const appIdMapA: { [id: string]: TreeNode } = {}
// this.buildIdMaps(rvsA, idMapA, appIdMapA)
// const idMapB: { [id: string]: { node: TreeNode; applicationId: string } } = {}
// const appIdMapB: { [id: string]: TreeNode } = {}
// this.buildIdMaps(rvsB, idMapB, appIdMapB)
// /** Get the ids which are common between the two maps. This will be objects
// * which have not changed
// */
// const unchanged: Array<string> = this.intersection(idMapA, idMapB)
// /** We remove the unchanged objects from B and end up with changed + added */
// const addedModified = _.omit(idMapB, unchanged)
// /** We remove the unchanged objects from A and end up with changed + removed */
// const removedModified = _.omit(idMapA, unchanged)
// /** We remove the changed objects from B. An object from B is changed if
// * it's application ID exists in A
// */
// const added = _.omit(addedModified, function (value: { applicationId: string }) {
// return (
// value.applicationId !== undefined &&
// appIdMapA[value.applicationId] !== undefined
// )
// })
// /** We remove the changed objects from A. An object from A is changed if
// * it's application ID exists in B
// */
// const removed = _.omit(
// removedModified,
// function (value: { applicationId: string }) {
// return (
// value.applicationId !== undefined &&
// appIdMapB[value.applicationId] !== undefined
// )
// }
// )
// /** We remove the removed objects from A, leaving us only changed objects */
// const modifiedRemoved = _.omit(removedModified, Object.keys(removed))
// /** We remove the removed objects from B, leaving us only changed objects */
// const modifiedAdded = _.omit(addedModified, Object.keys(added))
// /** We fill the arrays from here on out */
// const modifiedOld = (Object.values(modifiedRemoved) as { node: TreeNode }[]).map(
// (value: { node: TreeNode }) => value.node
// )
// const modifiedNew = (Object.values(modifiedAdded) as { node: TreeNode }[]).map(
// (value: { node: TreeNode }) => value.node
// )
// diffResult.unchanged.push(...unchanged.map((value) => idMapA[value].node))
// diffResult.unchanged.push(...unchanged.map((value) => idMapB[value].node))
// diffResult.removed.push(
// ...(Object.values(removed) as { node: TreeNode }[]).map(
// (value: { node: TreeNode }) => value.node
// )
// )
// diffResult.added.push(
// ...(Object.values(added) as { node: TreeNode }[]).map(
// (value: { node: TreeNode }) => value.node
// )
// )
// modifiedOld.forEach((value, index) => {
// value
// diffResult.modified.push([modifiedOld[index], modifiedNew[index]])
// })
// return Promise.resolve(diffResult)
// }
}