Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

clean up excess geometry parts alpha API #115

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "add primitive geometry cleanup routine",
"packageName": "@itwin/imodel-transformer",
"email": "[email protected]",
"dependentChangeType": "patch"
}
2 changes: 1 addition & 1 deletion packages/transformer/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@itwin/imodel-transformer",
"version": "0.3.2",
"version": "0.3.3-prune-geom-parts.0",
"description": "API for exporting an iModel's parts and also importing them into another iModel",
"main": "lib/cjs/transformer.js",
"typings": "lib/cjs/transformer",
Expand Down
39 changes: 39 additions & 0 deletions packages/transformer/src/CleanupUnusedGeometryParts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { IModelDb } from "@itwin/core-backend";
import { DbResult, Id64String } from "@itwin/core-bentley";
import { ElementGeometry } from "@itwin/core-common";

/**
* delete all geometry parts that are not referenced by any geometric elements.
* This will be replaced by a more integrated approach
* @internal
*/
export function cleanupUnusedGeometryParts(db: IModelDb) {
const usedGeomParts = new Set<Id64String>();

const allGeomElemIdsQuery = `
SELECT ECInstanceId
FROM bis.GeometricElement
`;
db.withPreparedStatement(allGeomElemIdsQuery, (geomElemIdStmt) => {
while (geomElemIdStmt.step() === DbResult.BE_SQLITE_ROW) {
const geomElemId = geomElemIdStmt.getValue(0).getId();
db.elementGeometryRequest({
elementId: geomElemId,
skipBReps: true, // breps contain no references to geometry parts
onGeometry(geomInfo) {
for (const entry of new ElementGeometry.Iterator(geomInfo)) {
const maybeGeomPart = entry.toGeometryPart();
if (maybeGeomPart)
usedGeomParts.add(maybeGeomPart);
}
},
});
}
});

// NOTE: maybe (negligbly?) faster to do custom query with `NOT InVirtualSet()`
const unusedGeomPartIds = db.queryEntityIds({ from: "bis.GeometryPart" });
for (const usedGeomPartId of usedGeomParts)
unusedGeomPartIds.delete(usedGeomPartId);
db.elements.deleteDefinitionElements([...unusedGeomPartIds]);
}
25 changes: 17 additions & 8 deletions packages/transformer/src/IModelTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { IModelImporter, IModelImporterState, OptimizeGeometryOptions } from "./
import { TransformerLoggerCategory } from "./TransformerLoggerCategory";
import { PendingReference, PendingReferenceMap } from "./PendingReferenceMap";
import { EntityKey, EntityMap } from "./EntityMap";
import { cleanupUnusedGeometryParts } from "./CleanupUnusedGeometryParts";
import { IModelCloneContext } from "./IModelCloneContext";
import { EntityUnifier } from "./EntityUnifier";

Expand Down Expand Up @@ -148,6 +149,12 @@ export interface IModelTransformOptions {
* @beta
*/
optimizeGeometry?: OptimizeGeometryOptions;

/** internal option, will definitely be replaced with a more complete API
* remove unused geometry parts during `finalizeTransformation`
* @internal
*/
cleanupUnusedGeometryParts?: boolean;
}

/**
Expand Down Expand Up @@ -1129,6 +1136,16 @@ export class IModelTransformer extends IModelExportHandler {
partiallyCommittedElem.forceComplete();
}
}

if (this._options.cleanupUnusedGeometryParts)
// FIXME: move to importer
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move to importer

cleanupUnusedGeometryParts(this.targetDb);

if (this._options.optimizeGeometry)
this.importer.optimizeGeometry(this._options.optimizeGeometry);

this.importer.computeProjectExtents();

// this internal is guaranteed stable for just transformer usage
/* eslint-disable @itwin/no-internal */
if ("codeValueBehavior" in this.sourceDb as any) {
Expand Down Expand Up @@ -1450,10 +1467,6 @@ export class IModelTransformer extends IModelExportHandler {
await this.detectRelationshipDeletes();
}

if (this._options.optimizeGeometry)
this.importer.optimizeGeometry(this._options.optimizeGeometry);

this.importer.computeProjectExtents();
this.finalizeTransformation();
}

Expand Down Expand Up @@ -1685,10 +1698,6 @@ export class IModelTransformer extends IModelExportHandler {
await this.exporter.exportChanges(options);
await this.processDeferredElements(); // eslint-disable-line deprecation/deprecation

if (this._options.optimizeGeometry)
this.importer.optimizeGeometry(this._options.optimizeGeometry);

this.importer.computeProjectExtents();
this.finalizeTransformation();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@

import { assert, expect } from "chai";
import * as fs from "fs";
import * as child_process from "child_process";
import * as path from "path";
import * as Semver from "semver";
import * as sinon from "sinon";
import {
CategorySelector, DisplayStyle3d, DocumentListModel, Drawing, DrawingCategory, DrawingGraphic, DrawingModel, ECSqlStatement, Element,
ElementMultiAspect, ElementOwnsChildElements, ElementOwnsExternalSourceAspects, ElementOwnsMultiAspects, ElementOwnsUniqueAspect, ElementRefersToElements,
ElementUniqueAspect, ExternalSourceAspect, GenericPhysicalMaterial, GeometricElement, IModelDb, IModelElementCloneContext, IModelHost, IModelJsFs,
ElementUniqueAspect, ExternalSourceAspect, GenericPhysicalMaterial, GeometricElement, GeometryPart, IModelDb, IModelElementCloneContext, IModelHost, IModelJsFs,
InformationRecordModel, InformationRecordPartition, LinkElement, Model, ModelSelector, OrthographicViewDefinition,
PhysicalModel, PhysicalObject, PhysicalPartition, PhysicalType, Relationship, RenderMaterialElement, RepositoryLink, Schema, SnapshotDb, SpatialCategory, StandaloneDb,
SubCategory, Subject, Texture,
Expand All @@ -22,9 +23,9 @@ import * as TestUtils from "../TestUtils";
import { DbResult, Guid, Id64, Id64String, Logger, LogLevel, OpenMode } from "@itwin/core-bentley";
import {
AxisAlignedBox3d, BriefcaseIdValue, Code, CodeScopeSpec, CodeSpec, ColorDef, CreateIModelProps, DefinitionElementProps, ElementAspectProps, ElementProps,
ExternalSourceAspectProps, GeometricElement2dProps, ImageSourceFormat, IModel, IModelError, InformationPartitionElementProps, ModelProps, PhysicalElementProps, Placement3d, ProfileOptions, QueryRowFormat, RelatedElement, RelationshipProps, RepositoryLinkProps,
ExternalSourceAspectProps, GeometricElement2dProps, GeometryPartProps, GeometryStreamBuilder, GeometryStreamProps, ImageSourceFormat, IModel, IModelError, InformationPartitionElementProps, ModelProps, PhysicalElementProps, Placement3d, ProfileOptions, QueryRowFormat, RelatedElement, RelationshipProps, RepositoryLinkProps,
} from "@itwin/core-common";
import { Point3d, Range3d, StandardViewIndex, Transform, YawPitchRollAngles } from "@itwin/core-geometry";
import { Box, Point3d, Range3d, StandardViewIndex, Transform, Vector3d, YawPitchRollAngles } from "@itwin/core-geometry";
import { IModelExporter, IModelExportHandler, IModelTransformer, IModelTransformOptions, TransformerLoggerCategory } from "../../transformer";
import {
AspectTrackingImporter,
Expand Down Expand Up @@ -2635,6 +2636,97 @@ describe("IModelTransformer", () => {
targetDb.close();
});

function createBigGeomPart(): GeometryStreamProps {
const geometryStreamBuilder = new GeometryStreamBuilder();
for (let i = 1; i < 2_00; ++i) {
geometryStreamBuilder.appendGeometry(Box.createDgnBox(
Point3d.createZero(), Vector3d.unitX(), Vector3d.unitY(), new Point3d(0, 0, i),
i, i, i, i, true,
)!);
}
return geometryStreamBuilder.geometryStream;
}

function createBoxWithGeomParts(geometryPartId: Id64String): GeometryStreamProps {
const geometryStreamBuilder = new GeometryStreamBuilder();
geometryStreamBuilder.appendGeometry(Box.createDgnBox(
Point3d.createZero(), Vector3d.unitX(), Vector3d.unitY(), new Point3d(0, 0, 0),
0, 0, 0, 0, true,
)!);
geometryStreamBuilder.appendGeometryPart3d(geometryPartId);
geometryStreamBuilder.appendGeometryPart3d(geometryPartId);
return geometryStreamBuilder.geometryStream;
}

it("processAll prunes unnecessary geometry parts", async function () {
const sourceDbFile = IModelTransformerTestUtils.prepareOutputFile("IModelTransformer", "PruneGeomParts.bim");
const sourceDb = SnapshotDb.createEmpty(sourceDbFile, { rootSubject: { name: "PruneGeomPartsSrc" } });

const sourceModelId = PhysicalModel.insert(sourceDb, IModel.rootSubjectId, "Physical");
const categoryId = SpatialCategory.insert(sourceDb, IModel.dictionaryId, "SpatialCategory", { color: ColorDef.green.toJSON() });

const [physObj1, physObj2] = [1, 2].map((i) => {
const geometryPartProps: GeometryPartProps = {
classFullName: GeometryPart.classFullName,
model: IModelDb.dictionaryId,
code: GeometryPart.createCode(sourceDb, IModelDb.dictionaryId, `GeometryPart${i}`),
geom: createBigGeomPart(),
};

const geomPartId = sourceDb.elements.insertElement(geometryPartProps);

const physObjProps: PhysicalElementProps = {
classFullName: PhysicalObject.classFullName,
model: sourceModelId,
category: categoryId,
code: Code.createEmpty(),
userLabel: "PhysicalObject1",
geom: createBoxWithGeomParts(geomPartId),
placement: {
origin: Point3d.create(1, 1, 1),
angles: YawPitchRollAngles.createDegrees(0, 0, 0),
},
};

const physObjId = sourceDb.elements.insertElement(physObjProps);

return { geomPartId, id: physObjId };
});

const targetDbFile: string = IModelTransformerTestUtils.prepareOutputFile("IModelTransformer", "PruneGeomParts-Target.bim");
const targetDb = SnapshotDb.createEmpty(targetDbFile, { rootSubject: { name: "PruneGeomParts" } });
targetDb.saveChanges();

const transformer = new IModelTransformer(sourceDb, targetDb, { cleanupUnusedGeometryParts: true });
// expect this to not reject, adding chai as promised makes the error less readable
await transformer.processSchemas();
const targetModelId = PhysicalModel.insert(targetDb, IModel.rootSubjectId, "Physical");

const physObj1Elem = sourceDb.elements.getElement(physObj1.id);

transformer.context.remapElement(physObj1Elem.model, targetModelId);
transformer.shouldExportElement = (elem) => elem.id !== physObj2.id;
await transformer.processAll();

function printSize(p: string) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FIXME: remove from test

// eslint-disable-next-line
console.log(`Size of ${p}\n`, child_process.execSync(`du -h ${p}`, {encoding: "utf-8"}));
}

printSize(sourceDbFile);
printSize(targetDbFile);

assert(Id64.isValidId64(transformer.context.findTargetElementId(physObj1.id)));
assert(!Id64.isValidId64(transformer.context.findTargetElementId(physObj2.id)));
expect(count(sourceDb, GeometryPart.classFullName)).to.equal(2);
expect(count(targetDb, GeometryPart.classFullName)).to.equal(1);

// clean up
transformer.dispose();
sourceDb.close();
targetDb.close();
});

/** unskip to generate a javascript CPU profile on just the processAll portion of an iModel */
it.skip("should profile an IModel transformation", async function () {
const sourceDbFile = IModelTransformerTestUtils.prepareOutputFile("IModelTransformer", "ProfileTransformation.bim");
Expand Down