Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
32 changes: 26 additions & 6 deletions packages/core/src/openbim/IDSSpecifications/src/facets/Entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,16 @@ export class IDSEntity extends IDSFacet {
checks?: IDSCheck[],
) {
if (!this.predefinedType) return null;
if (!("value" in itemData.PredefinedType)) return null;

const isRequirementUserDefined =
typeof this.predefinedType.parameter === "string" &&
this.predefinedType.parameter === "USERDEFINED";

let value = itemData.PredefinedType.value;
// Some entities (e.g. IfcSite in IFC4) have no PredefinedType attribute
// at all; their item data has no such key. Treat that as "no value", so
// the requirement fails instead of throwing.
const own = itemData.PredefinedType;
let value = own && "value" in own ? own.value : undefined;

// TODO: Do not remember what is this for
if (value === "USERDEFINED" && !isRequirementUserDefined) {
Expand All @@ -164,11 +167,28 @@ export class IDSEntity extends IDSFacet {
const model = fragments.list.get(modelId);
if (model && "value" in itemData._localId) {
const [data] = await model.getItemsData([itemData._localId.value], {
relations: { IsTypedBy: { attributes: true, relations: false } },
relations: {
IsTypedBy: { attributes: true, relations: false },
IsDefinedBy: { attributes: true, relations: false },
},
});
if (Array.isArray(data.IsTypedBy)) {
const typeAttrs = data.IsTypedBy[0];
if (typeAttrs && "value" in typeAttrs.PredefinedType) {
// The type is reached through IsTypedBy or, in models converted by
// IfcImporter, through IsDefinedBy (as in IDSProperty.getTypePsets).
const definitions = [
...(Array.isArray(data.IsTypedBy) ? data.IsTypedBy : []),
...(Array.isArray(data.IsDefinedBy) ? data.IsDefinedBy : []),
];
const typeAttrs = definitions.find(
(definition) =>
definition?._category &&
"value" in definition._category &&
/TYPE$/.test(String(definition._category.value)),
);
if (typeAttrs) {
if (
typeAttrs?.PredefinedType &&
"value" in typeAttrs.PredefinedType
) {
value = typeAttrs.PredefinedType.value;
if (value === "USERDEFINED" && !isRequirementUserDefined) {
const attrNames = Object.keys(typeAttrs);
Expand Down
122 changes: 122 additions & 0 deletions packages/core/src/openbim/IDSSpecifications/src/facets/entity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
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, ModelIdMap } 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`: an IfcSite (no PredefinedType attribute in
// IFC4) and walls whose PredefinedType is STANDARD (see property.test.ts).
// `type-predefinedtype.ifc` is the same file with wall #40's PredefinedType
// unset ($); its type, IfcWallType #52, is STANDARD.
const INHERITED_ID = "type-predefinedtype";
const WALLS = [40, 41, 42, 43, 45, 46];

const convert = async (fixture = "type-psets.ifc", modelId = MODEL_ID) => {
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", fixture));
const bytes = await importer.process({
bytes: new Uint8Array(ifc),
raw: true,
});
return new FRAGS.SingleThreadedFragmentsModel(modelId, bytes, true);
};

describe("IDSEntity predefinedType", () => {
let components: Components;
let model: FRAGS.SingleThreadedFragmentsModel;

let inherited: FRAGS.SingleThreadedFragmentsModel;

beforeAll(async () => {
model = await convert();
inherited = await convert("type-predefinedtype.ifc", INHERITED_ID);
components = new OBC.Components();
const fragments = components.get(OBC.FragmentsManager);
vi.spyOn(fragments, "list", "get").mockReturnValue(
new Map([
[MODEL_ID, model],
[INHERITED_ID, inherited],
]) as never,
);
}, 60_000);

afterAll(() => {
vi.restoreAllMocks();
components?.dispose();
model?.dispose();
inherited?.dispose();
});

const entity = (name: string, predefinedType: string) => {
const facet = new OBC.IDSEntity(components, {
type: "simple",
parameter: name,
});
facet.predefinedType = { type: "simple", parameter: predefinedType };
return facet;
};

const select = async (
facet: InstanceType<typeof OBC.IDSEntity>,
modelId = MODEL_ID,
) => {
const collector: ModelIdMap = {};
await facet.getEntities([new RegExp(`^${modelId}$`)], collector);
return [...(collector[modelId] ?? [])].sort((a, b) => a - b);
};

const passes = async (
facet: InstanceType<typeof OBC.IDSEntity>,
ids: number[],
modelId = MODEL_ID,
) => {
const results = new FRAGS.DataMap<
string,
FRAGS.DataMap<number, { pass: boolean }>
>();
await facet.test({ [modelId]: new Set(ids) }, results as never, {
skipIfFails: false,
});
return Object.fromEntries(
[...results.get(modelId)!.entries()].map(([id, r]) => [id, r.pass]),
);
};

it("does not select an entity that has no PredefinedType attribute", async () => {
expect(await select(entity("IFCSITE", "USERDEFINED"))).toEqual([]);
});

it("fails, rather than throws, as a requirement on such an entity", async () => {
const sites =
(await model.getItemsOfCategories([/^IFCSITE$/])).IFCSITE ?? [];
expect(sites.length).toBe(1);
expect(await passes(entity("IFCSITE", "USERDEFINED"), sites)).toEqual({
[sites[0]]: false,
});
});

it("still matches entities that have the predefined type", async () => {
expect(await select(entity("IFCWALL", "STANDARD"))).toEqual(WALLS);
});

it("takes the predefined type from the element's type when its own is unset", async () => {
// IDS: an occurrence without its own PredefinedType inherits its type's.
expect(await select(entity("IFCWALL", "STANDARD"), INHERITED_ID)).toEqual(
WALLS,
);
expect(
await passes(entity("IFCWALL", "STANDARD"), [40], INHERITED_ID),
).toEqual({ 40: true });
});
});
46 changes: 46 additions & 0 deletions packages/core/tests/fixtures/type-predefinedtype.ifc
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ViewDefinition [ReferenceView]'),'2;1');
FILE_NAME('type-predefinedtype.ifc','2026-09-28T00:00:00',(''),(''),'','','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCPROJECT('0YvctVUKr0kugbFTf53O9L',$,'Project',$,$,$,$,(#12),#7);
#2=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#7=IFCUNITASSIGNMENT((#2));
#8=IFCCARTESIANPOINT((0.,0.,0.));
#9=IFCDIRECTION((0.,0.,1.));
#10=IFCDIRECTION((1.,0.,0.));
#11=IFCAXIS2PLACEMENT3D(#8,#9,#10);
#12=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,#11,$);
#20=IFCSITE('1YvctVUKr0kugbFTf53O9L',$,'Site',$,$,#21,$,$,.ELEMENT.,$,$,$,$,$);
#21=IFCLOCALPLACEMENT($,#11);
#22=IFCRELAGGREGATES('2YvctVUKr0kugbFTf53O9L',$,$,$,#1,(#20));
#30=IFCLOCALPLACEMENT(#21,#11);
#31=IFCCARTESIANPOINT((1.,1.,1.));
#32=IFCBLOCK(#11,1.,1.,1.);
#33=IFCCSGSOLID(#32);
#34=IFCSHAPEREPRESENTATION(#12,'Body','CSG',(#33));
#35=IFCPRODUCTDEFINITIONSHAPE($,$,(#34));
#40=IFCWALL('3YvctVUKr0kugbFTf53O9L',$,'Typed wall A',$,$,#30,#35,$,$);
#41=IFCWALL('4YvctVUKr0kugbFTf53O9L',$,'Typed wall B',$,$,#30,#35,$,.STANDARD.);
#42=IFCWALL('5YvctVUKr0kugbFTf53O9L',$,'Instance pset wall',$,$,#30,#35,$,.STANDARD.);
#43=IFCWALL('6YvctVUKr0kugbFTf53O9L',$,'Bare wall',$,$,#30,#35,$,.STANDARD.);
#45=IFCWALL('DYvctVUKr0kugbFTf53O9L',$,'Typed wall overriding IsExternal',$,$,#30,#35,$,.STANDARD.);
#46=IFCWALL('EYvctVUKr0kugbFTf53O9L',$,'Typed wall with another prop in the same pset',$,$,#30,#35,$,.STANDARD.);
#44=IFCRELCONTAINEDINSPATIALSTRUCTURE('7YvctVUKr0kugbFTf53O9L',$,$,$,(#40,#41,#42,#43,#45,#46),#20);
#50=IFCPROPERTYSINGLEVALUE('IsExternal',$,IFCBOOLEAN(.T.),$);
#51=IFCPROPERTYSET('8YvctVUKr0kugbFTf53O9L',$,'Pset_WallCommon',$,(#50));
#52=IFCWALLTYPE('9YvctVUKr0kugbFTf53O9L',$,'Exterior type',$,$,(#51),$,$,$,.STANDARD.);
#53=IFCRELDEFINESBYTYPE('AYvctVUKr0kugbFTf53O9L',$,$,$,(#40,#41,#45,#46),#52);
#60=IFCPROPERTYSINGLEVALUE('IsExternal',$,IFCBOOLEAN(.T.),$);
#61=IFCPROPERTYSET('BYvctVUKr0kugbFTf53O9L',$,'Pset_WallCommon',$,(#60));
#62=IFCRELDEFINESBYPROPERTIES('CYvctVUKr0kugbFTf53O9L',$,$,$,(#42),#61);
#70=IFCPROPERTYSINGLEVALUE('IsExternal',$,IFCBOOLEAN(.F.),$);
#71=IFCPROPERTYSET('FYvctVUKr0kugbFTf53O9L',$,'Pset_WallCommon',$,(#70));
#72=IFCRELDEFINESBYPROPERTIES('GYvctVUKr0kugbFTf53O9L',$,$,$,(#45),#71);
#80=IFCPROPERTYSINGLEVALUE('LoadBearing',$,IFCBOOLEAN(.T.),$);
#81=IFCPROPERTYSET('HYvctVUKr0kugbFTf53O9L',$,'Pset_WallCommon',$,(#80));
#82=IFCRELDEFINESBYPROPERTIES('IYvctVUKr0kugbFTf53O9L',$,$,$,(#46),#81);
ENDSEC;
END-ISO-10303-21;
Loading