Skip to content
Draft
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
129 changes: 129 additions & 0 deletions packages/core/src/openbim/IDSSpecifications/conformance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import * as FRAGS from "@thatopen/fragments";
import * as WEBIFC from "web-ifc";
import * as fs from "fs";
import * as path from "path";
import { describe, expect, it, vi } from "vitest";
import { loadSourceBundle } from "../../../tests/source-bundle";

const OBC = await loadSourceBundle();
const directory =
process.env.IDS_CONFORMANCE_DIR ??
path.resolve(__dirname, "../../../tests/fixtures/ids-conformance");
const files = fs
.readdirSync(directory, { recursive: true })
.map(String)
.filter(
(name) =>
/(^|\/)(pass|fail)-.*\.ids$/.test(name) &&
fs.existsSync(path.join(directory, name.replace(/\.ids$/, ".ifc"))),
);
const cases = files.map((name) => ({
name,
label: name,
transform: (xml: string) => xml,
}));
cases.push({
name: "property/fail-properties_with_a_null_value_fail.ids",
label: "null property without a datatype still fails",
transform: (xml) => xml.replace(' dataType="IFCLABEL"', ""),
});
cases.push({
name: "ids/pass-optional_specifications_may_still_pass_if_nothing_is_applicable.ids",
label: "requirements do not inspect unrelated objects when nothing applies",
transform: (xml) =>
xml.replace(
"<simpleValue>Waldo</simpleValue>",
"<simpleValue>Unrelated objects must not be checked</simpleValue>",
),
});
describe("IDS full conformance", () => {
it.each(cases)(
"$label",
async ({ name, transform }) => {
const importer = new FRAGS.IfcImporter();
importer.wasm = {
path: `${path.resolve(__dirname, "../../../../../node_modules/web-ifc")}/`,
absolute: true,
};
importer.includeRelationNames = true;
importer.includeMaterialProperties = true;
for (const [code, parent, child] of [
[
WEBIFC.IFCRELASSOCIATESCLASSIFICATION,
"ClassificationForObjects",
"HasAssociations",
],
[WEBIFC.IFCRELASSOCIATESMATERIAL, "AssociatedTo", "HasAssociations"],
[WEBIFC.IFCRELDEFINESBYTYPE, "Types", "IsTypedBy"],
[WEBIFC.IFCRELAGGREGATES, "IsDecomposedBy", "Decomposes"],
[WEBIFC.IFCRELNESTS, "IsNestedBy", "Nests"],
[WEBIFC.IFCRELASSIGNSTOGROUP, "IsGroupedBy", "HasAssignments"],
[
WEBIFC.IFCRELCONTAINEDINSPATIALSTRUCTURE,
"ContainsElements",
"ContainedInStructure",
],
[WEBIFC.IFCRELVOIDSELEMENT, "HasOpenings", "VoidsElements"],
[WEBIFC.IFCRELFILLSELEMENT, "HasFillings", "FillsVoids"],
] as const)
importer.relations.set(code, {
forRelated: child,
forRelating: parent,
});
const ifc = fs.readFileSync(
path.join(directory!, name.replace(/\.ids$/, ".ifc")),
"utf8",
);
for (const [, category] of ifc.matchAll(
/#\d+\s*=\s*(IFC[A-Z0-9_]+)\s*\(/g,
)) {
const code = WEBIFC[category as keyof typeof WEBIFC];
if (typeof code === "number") importer.classes.abstract.add(code);
}
const bytes = await importer.process({
bytes: new TextEncoder().encode(ifc),
raw: true,
});
const model = new FRAGS.SingleThreadedFragmentsModel(
"conformance",
bytes,
true,
);
const components = new OBC.Components();
const spy = vi
.spyOn(components.get(OBC.FragmentsManager), "list", "get")
.mockReturnValue(new Map([["conformance", model]]) as never);
try {
const specs = components
.get(OBC.IDSSpecifications)
.load(
transform(fs.readFileSync(path.join(directory!, name), "utf8")),
);
let pass = true;
for (const spec of specs)
if (
!(await spec.verify([/^conformance$/], { skipIfFails: false })).pass
)
pass = false;
expect(pass).toBe(path.basename(name).startsWith("pass-"));
const exported = components
.get(OBC.IDSSpecifications)
.export({ title: "IDS conformance" }, specs);
const roundtrip = components.get(OBC.IDSSpecifications).load(exported);
expect(roundtrip.length).toBe(specs.length);
let repeated = true;
for (const spec of roundtrip)
if (
!(await spec.verify([/^conformance$/], { skipIfFails: false })).pass
)
repeated = false;
expect(repeated).toBe(pass);
} finally {
spy.mockRestore();
components.dispose();
model.dispose();
}
},
60000,
);
});
115 changes: 61 additions & 54 deletions packages/core/src/openbim/IDSSpecifications/example.html
Original file line number Diff line number Diff line change
@@ -1,67 +1,74 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />

<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">

<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="icon" type="image/x-icon" href="https://thatopen.github.io/engine_components/resources/favicon.ico">
<title>IDS Specifications</title>
<style>
body {
margin: 0;
padding: 0;
font-family: "Plus Jakarta Sans", sans-serif;
overflow: hidden;
}

.full-screen {
width: 100vw;
height: 100vh;
position: relative;
overflow: hidden;
}

.options-menu {
position: fixed;
min-width: unset;
top: 5px;
right: 5px;
height: fit-content;
max-height: calc(100vh - 10px);
}

.phone-menu-toggler {
visibility: hidden;
}

@media (max-width: 480px) {
.options-menu {
visibility: hidden;
bottom: 5px;
left: 5px;
<link
rel="stylesheet"
href="https://fonts.googleapis.com/icon?family=Material+Icons"
/>
<link
rel="icon"
type="image/x-icon"
href="https://thatopen.github.io/engine_components/resources/favicon.ico"
/>
<title>IDS Specifications</title>
<style>
body {
margin: 0;
padding: 0;
font-family: "Plus Jakarta Sans", sans-serif;
overflow: hidden;
}

.options-menu-visible {
visibility: visible;
.full-screen {
width: 100vw;
height: 100vh;
position: relative;
overflow: hidden;
}

.phone-menu-toggler {
visibility: visible;
.options-menu {
position: fixed;
min-width: unset;
top: 5px;
right: 5px;
height: fit-content;
max-height: calc(100vh - 10px);
}

.phone-menu-toggler {
visibility: hidden;
}
}
</style>
</head>

<body>
<div class="full-screen" id="container"></div>
<script type="module" src="./example.ts"></script>
</body>
@media (max-width: 480px) {
.options-menu {
visibility: hidden;
bottom: 5px;
left: 5px;
}

.options-menu-visible {
visibility: visible;
}

.phone-menu-toggler {
visibility: visible;
position: fixed;
top: 5px;
right: 5px;
}
}
</style>
</head>

</html>
<body>
<div class="full-screen" id="container"></div>
<script type="module" src="./example.ts"></script>
</body>
</html>
3 changes: 2 additions & 1 deletion packages/core/src/openbim/IDSSpecifications/example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,8 @@ const restoreModelMaterials = () => {
for (const [material, data] of originalColors) {
const { color, transparent, opacity } = data;
material.transparent = transparent;
const isLod = "isLodMaterial" in material && (material as any).isLodMaterial;
const isLod =
"isLodMaterial" in material && (material as any).isLodMaterial;
if (isLod) {
(material as any).lodOpacity = opacity;
material.lodColor.setHex(color);
Expand Down
17 changes: 13 additions & 4 deletions packages/core/src/openbim/IDSSpecifications/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@ export class IDSSpecifications extends Component {
ignoreDeclaration: true,
ignorePiTags: true,
numberParseOptions: { leadingZeros: true, hex: true },
parseAttributeValue: true,
parseAttributeValue: false,
parseTagValue: false,
preserveOrder: false,
// Decode &amp; &lt; &gt; &quot; &apos; so values compare equal to the
// model's text. fast-xml-parser applies its expansion limits by default.
processEntities: true,
removeNSPrefix: true,
trimValues: true,
trimValues: false,
});

IDSInfo?: IDSInfo;
Expand Down Expand Up @@ -112,7 +113,10 @@ export class IDSSpecifications extends Component {
const ids = IDSSpecifications.xmlParser.parse(data).ids;
const { specifications, info } = ids;

this.IDSInfo = { ...info };
this.IDSInfo = {
...info,
...(info.date ? { date: new Date(info.date) } : {}),
};

if (specifications && specifications.specification) {
const specs = Array.isArray(specifications.specification)
Expand Down Expand Up @@ -218,6 +222,11 @@ export class IDSSpecifications extends Component {
specification.description = description;
specification.instructions = instructions;
specification.requirementsDescription = requirementsDescription;
const { minOccurs, maxOccurs } = applicability ?? {};
if (minOccurs !== undefined)
specification.minOccurs = Number(minOccurs);
if (maxOccurs !== undefined && maxOccurs !== "unbounded")
specification.maxOccurs = Number(maxOccurs);
specification.applicability.add(...applicabilities);
specification.requirements.add(...reqs);
result.push(specification);
Expand All @@ -240,7 +249,7 @@ export class IDSSpecifications extends Component {
specifications: Iterable<IDSSpecification> = this.list.values(),
) {
const _specifications = specifications ?? this.list;
const xml = `<ids xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://standards.buildingsmart.org/IDS http://standards.buildingsmart.org/IDS/1.0/ids.xsd" xmlns:ids="http://standards.buildingsmart.org/IDS">
const xml = `<ids xmlns="http://standards.buildingsmart.org/IDS" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://standards.buildingsmart.org/IDS http://standards.buildingsmart.org/IDS/1.0/ids.xsd" xmlns:ids="http://standards.buildingsmart.org/IDS">
<!-- Made with That Open Engine ${Components.release} (https://github.com/thatopen/engine_components) -->
<info>
<title>${XML.escape(info.title)}</title>
Expand Down
Loading
Loading