From 39c9b9d5d10e1dc7e2e75c67b5bce418b552c4ab Mon Sep 17 00:00:00 2001 From: Helen Kwok <107752876+helenkwok@users.noreply.github.com> Date: Sun, 11 Oct 2026 13:26:41 +1030 Subject: [PATCH] fix(ids): check a type object's own property sets in IDSProperty --- .../IDSSpecifications/src/facets/Property.ts | 21 +++++- .../src/facets/property-type-object.test.ts | 75 +++++++++++++++++++ 2 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 packages/core/src/openbim/IDSSpecifications/src/facets/property-type-object.test.ts diff --git a/packages/core/src/openbim/IDSSpecifications/src/facets/Property.ts b/packages/core/src/openbim/IDSSpecifications/src/facets/Property.ts index 40f16f01..e36f5805 100644 --- a/packages/core/src/openbim/IDSSpecifications/src/facets/Property.ts +++ b/packages/core/src/openbim/IDSSpecifications/src/facets/Property.ts @@ -84,7 +84,10 @@ export class IDSProperty extends IDSFacet { // Matching sets, by localId: their name and the property names that // matched. A set attached to a type (IfcTypeObject.HasPropertySets) // has no DefinesOccurrence, so its elements are found from the type. - const matchedSets = new Map }>(); + const matchedSets = new Map< + number, + { name: string; props: Set } + >(); for (const set of data) { const definesOccurrence = set.DefinesOccurrence ?? set.DefinesOcurrence; @@ -220,7 +223,8 @@ export class IDSProperty extends IDSFacet { for (const type of types) { if (!Array.isArray(type.HasPropertySets)) continue; const sets = type.HasPropertySets.flatMap((set) => { - const id = set._localId && "value" in set._localId ? set._localId.value : null; + const id = + set._localId && "value" in set._localId ? set._localId.value : null; const matched = matchedSets.get(id as number); return matched ? [matched] : []; }); @@ -260,12 +264,15 @@ export class IDSProperty extends IDSFacet { const props = list ? set[list] : undefined; if (!Array.isArray(props)) return false; return props.some( - (prop) => prop.Name && "value" in prop.Name && prop.Name.value === propName, + (prop) => + prop.Name && "value" in prop.Name && prop.Name.value === propName, ); }); const inherits = inherited .get(id)! - .some((set) => [...set.props].some((prop) => !overrides(set.name, prop))); + .some((set) => + [...set.props].some((prop) => !overrides(set.name, prop)), + ); if (inherits) applies.push(id); } ModelIdMapUtils.append(collector, modelId, ...applies); @@ -422,6 +429,12 @@ export class IDSProperty extends IDSFacet { } } + // When the item is itself a type object (e.g. an IfcWallType selected by + // the applicability), its own property sets are in HasPropertySets. + if (Array.isArray(item.HasPropertySets)) { + psets.push(...item.HasPropertySets); + } + return psets; } diff --git a/packages/core/src/openbim/IDSSpecifications/src/facets/property-type-object.test.ts b/packages/core/src/openbim/IDSSpecifications/src/facets/property-type-object.test.ts new file mode 100644 index 00000000..f57bb356 --- /dev/null +++ b/packages/core/src/openbim/IDSSpecifications/src/facets/property-type-object.test.ts @@ -0,0 +1,75 @@ +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 property.test.ts): IfcWallType #52 +// carries Pset_WallCommon.IsExternal = TRUE in its HasPropertySets. +const WALL_TYPE = 52; + +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("IDSProperty on a type object", () => { + 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(); + }); + + it("checks the type object's own property sets", async () => { + const spec = components + .get(OBC.IDSSpecifications) + .create("wall types are external", ["IFC4"]); + spec.applicability.add( + new OBC.IDSEntity(components, { + type: "simple", + parameter: "IFCWALLTYPE", + }), + ); + const property = new OBC.IDSProperty( + components, + { type: "simple", parameter: "Pset_WallCommon" }, + { type: "simple", parameter: "IsExternal" }, + ); + property.value = { type: "simple", parameter: true }; + spec.requirements.add(property); + + const result = await spec.test([new RegExp(MODEL_ID)]); + const typeResult = result.get(MODEL_ID)?.get(WALL_TYPE); + expect(typeResult?.pass).toBe(true); + }); +});