diff --git a/packages/core/src/openbim/IDSSpecifications/src/Specification.ts b/packages/core/src/openbim/IDSSpecifications/src/Specification.ts index d3b692d4..36646367 100644 --- a/packages/core/src/openbim/IDSSpecifications/src/Specification.ts +++ b/packages/core/src/openbim/IDSSpecifications/src/Specification.ts @@ -1,7 +1,7 @@ import * as FRAGS from "@thatopen/fragments"; import { Components } from "../../../core/Components"; import { IDSCheckResult, IDSSpecificationData, IfcVersion } from "./types"; -import { UUID } from "../../../utils"; +import { UUID, ModelIdMapUtils } from "../../../utils"; import { IDSFacet } from "./facets"; import { IDSSpecifications } from ".."; import { ModelIdMap } from "../../../fragments"; @@ -60,14 +60,19 @@ export class IDSSpecification implements IDSSpecificationData { if (this.requirements.size === 0) return result; - // Get applicable elements - const entities: ModelIdMap = {}; + // Get applicable elements. In IDS an element is applicable only when it + // matches every applicability facet, so each facet collects its own + // matches and the results are intersected. + const facetEntities: ModelIdMap[] = []; const applicabilityPromises = []; for (const facet of this.applicability) { - applicabilityPromises.push(facet.getEntities(modelIds, entities)); + const facetMatches: ModelIdMap = {}; + facetEntities.push(facetMatches); + applicabilityPromises.push(facet.getEntities(modelIds, facetMatches)); } await Promise.all(applicabilityPromises); + const entities = ModelIdMapUtils.intersect(facetEntities); // Test applicable elements against requirements const requirementPromises = []; diff --git a/packages/core/src/openbim/IDSSpecifications/src/specification.test.ts b/packages/core/src/openbim/IDSSpecifications/src/specification.test.ts new file mode 100644 index 00000000..66e19b9b --- /dev/null +++ b/packages/core/src/openbim/IDSSpecifications/src/specification.test.ts @@ -0,0 +1,89 @@ +import * as FRAGS from "@thatopen/fragments"; +import * as fs from "fs"; +import * as path from "path"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import type { Components } from "../../../index"; +import { loadSourceBundle } from "../../../../tests/source-bundle"; + +// Needs the real `Components`: see `loadSourceBundle`. +const OBC = await loadSourceBundle(); + +const MODEL_ID = "type-psets"; + +// `tests/fixtures/type-psets.ifc` (see facets/property.test.ts): six walls, +// four of which have Pset_WallCommon.IsExternal = TRUE, directly or through +// their type. +const WALLS = [40, 41, 42, 43, 45, 46]; +const WITH_IS_EXTERNAL_TRUE = [40, 41, 42, 46]; + +const convert = async () => { + const importer = new FRAGS.IfcImporter(); + const root = path.resolve(__dirname, "../../../.."); + importer.wasm = { + path: `${path.resolve(root, "../../node_modules/web-ifc")}/`, + absolute: true, + }; + const ifc = fs.readFileSync( + path.join(root, "tests", "fixtures", "type-psets.ifc"), + ); + const bytes = await importer.process({ + bytes: new Uint8Array(ifc), + raw: true, + }); + return new FRAGS.SingleThreadedFragmentsModel(MODEL_ID, bytes, true); +}; + +describe("IDSSpecification applicability", () => { + let components: Components; + let model: FRAGS.SingleThreadedFragmentsModel; + + beforeAll(async () => { + model = await convert(); + components = new OBC.Components(); + const fragments = components.get(OBC.FragmentsManager); + vi.spyOn(fragments, "list", "get").mockReturnValue( + new Map([[MODEL_ID, model]]) as never, + ); + }, 60_000); + + afterAll(() => { + vi.restoreAllMocks(); + components?.dispose(); + model?.dispose(); + }); + + const wall = () => + new OBC.IDSEntity(components, { type: "simple", parameter: "IFCWALL" }); + + const isExternal = () => { + const property = new OBC.IDSProperty( + components, + { type: "simple", parameter: "Pset_WallCommon" }, + { type: "simple", parameter: "IsExternal" }, + ); + property.value = { type: "simple", parameter: true }; + return property; + }; + + const tested = async ( + ...applicability: InstanceType[] + ) => { + const spec = components + .get(OBC.IDSSpecifications) + .create("applicability", ["IFC4"]); + for (const facet of applicability) spec.applicability.add(facet); + spec.requirements.add(wall()); + const result = await spec.test([new RegExp(MODEL_ID)]); + return [...(result.get(MODEL_ID)?.keys() ?? [])].sort((a, b) => a - b); + }; + + it("tests every element a single facet selects", async () => { + expect(await tested(wall())).toEqual(WALLS); + }); + + it("tests only the elements that match every applicability facet", async () => { + // IDS: applicability facets are combined with AND, so "a wall that is + // external" must not test the two walls that are not. + expect(await tested(wall(), isExternal())).toEqual(WITH_IS_EXTERNAL_TRUE); + }); +});