abstract-3d 2.12.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/abstract-3d.d.ts +32 -5
- package/lib/abstract-3d.d.ts.map +1 -1
- package/lib/abstract-3d.js.map +1 -1
- package/lib/renderers/ifc/ifc-encoding.d.ts +5 -2
- package/lib/renderers/ifc/ifc-encoding.d.ts.map +1 -1
- package/lib/renderers/ifc/ifc-encoding.js +30 -7
- package/lib/renderers/ifc/ifc-encoding.js.map +1 -1
- package/lib/renderers/ifc/ifc-geometry.d.ts +4 -0
- package/lib/renderers/ifc/ifc-geometry.d.ts.map +1 -1
- package/lib/renderers/ifc/ifc-geometry.js +8 -0
- package/lib/renderers/ifc/ifc-geometry.js.map +1 -1
- package/lib/renderers/ifc/ifc.d.ts +36 -23
- package/lib/renderers/ifc/ifc.d.ts.map +1 -1
- package/lib/renderers/ifc/ifc.js +164 -67
- package/lib/renderers/ifc/ifc.js.map +1 -1
- package/lib/renderers/react/index.d.ts +0 -1
- package/lib/renderers/react/index.d.ts.map +1 -1
- package/lib/renderers/react/index.js.map +1 -1
- package/lib/renderers/react/react-dimension.d.ts +2 -2
- package/lib/renderers/react/react-dimension.d.ts.map +1 -1
- package/lib/renderers/react/react-dimension.js +2 -2
- package/lib/renderers/react/react-dimension.js.map +1 -1
- package/lib/renderers/react/react-group.d.ts +3 -2
- package/lib/renderers/react/react-group.d.ts.map +1 -1
- package/lib/renderers/react/react-group.js +3 -3
- package/lib/renderers/react/react-group.js.map +1 -1
- package/lib/renderers/react/react-material.d.ts +2 -2
- package/lib/renderers/react/react-material.d.ts.map +1 -1
- package/lib/renderers/react/react-material.js +3 -3
- package/lib/renderers/react/react-material.js.map +1 -1
- package/lib/renderers/react/react-relation.d.ts +10 -0
- package/lib/renderers/react/react-relation.d.ts.map +1 -0
- package/lib/renderers/react/react-relation.js +31 -0
- package/lib/renderers/react/react-relation.js.map +1 -0
- package/lib/renderers/react/react-scene.d.ts +4 -7
- package/lib/renderers/react/react-scene.d.ts.map +1 -1
- package/lib/renderers/react/react-scene.js +4 -4
- package/lib/renderers/react/react-scene.js.map +1 -1
- package/lib/renderers/react/react.d.ts +8 -7
- package/lib/renderers/react/react.d.ts.map +1 -1
- package/lib/renderers/react/react.js +2 -2
- package/lib/renderers/react/react.js.map +1 -1
- package/package.json +2 -2
- package/src/abstract-3d.ts +33 -6
- package/src/renderers/ifc/ifc-encoding.ts +31 -7
- package/src/renderers/ifc/ifc-geometry.ts +12 -0
- package/src/renderers/ifc/ifc.test.ts +285 -101
- package/src/renderers/ifc/ifc.ts +313 -98
- package/src/renderers/react/index.ts +0 -1
- package/src/renderers/react/react-dimension.tsx +3 -3
- package/src/renderers/react/react-group.test.ts +2 -2
- package/src/renderers/react/react-group.tsx +5 -4
- package/src/renderers/react/react-material.tsx +4 -4
- package/src/renderers/react/react-relation.tsx +86 -0
- package/src/renderers/react/react-scene.tsx +9 -21
- package/src/renderers/react/react.tsx +11 -14
- package/lib/renderers/react/react-hotspot.d.ts +0 -27
- package/lib/renderers/react/react-hotspot.d.ts.map +0 -1
- package/lib/renderers/react/react-hotspot.js +0 -37
- package/lib/renderers/react/react-hotspot.js.map +0 -1
- package/src/renderers/react/react-hotspot.tsx +0 -112
|
@@ -1,45 +1,59 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
import type { Scene } from "../../abstract-3d.js";
|
|
3
|
-
import { box, cylinder, group, line, roundHole, sphere, vec2, vec3, vec3Zero } from "../../abstract-3d.js";
|
|
2
|
+
import type { Group, Scene } from "../../abstract-3d.js";
|
|
3
|
+
import { box, culledLine, cylinder, group, line, plane, roundHole, sphere, vec2, vec3, vec3Zero } from "../../abstract-3d.js";
|
|
4
4
|
import { guid, ifcFile, real, str } from "./ifc-encoding.js";
|
|
5
|
-
import { triangulate } from "./ifc-geometry.js";
|
|
5
|
+
import { polyline, triangulate } from "./ifc-geometry.js";
|
|
6
6
|
import { render } from "./ifc.js";
|
|
7
7
|
|
|
8
8
|
const red = { normal: "rgb(255, 0, 0)", opacity: 0.5 };
|
|
9
|
-
const scene = (groups: Scene["groups"]): Scene => ({ size_deprecated: vec3Zero, groups });
|
|
9
|
+
const scene = (groups: Scene["groups"], relations?: Scene["relations"]): Scene => ({ size_deprecated: vec3Zero, groups, relations });
|
|
10
10
|
const count = (ifc: string, entity: string): number => ifc.split(`=${entity}(`).length - 1;
|
|
11
11
|
const ids = (s: string): ReadonlyArray<string> => Array.from(s.matchAll(/#(\d+)/g), (match) => match[1]);
|
|
12
12
|
/** Random GlobalIds replaced, to compare files */
|
|
13
13
|
const normalize = (ifc: string): string => ifc.replaceAll(/'[0-9A-Za-z_$]{22}'/g, "'guid'");
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
const entities = (ifc: string): ReadonlyMap<string, string> => new Map(Array.from(ifc.matchAll(/^#(\d+)=(.*);$/gm), (match) => [match[1], match[2]]));
|
|
16
|
+
|
|
17
|
+
/** The #id of the element or port named `name` */
|
|
18
|
+
const named = (ifc: string, name: string): string => new RegExp(`^#(\\d+)=IFC\\w+\\('[^']{22}',#\\d+,'${name}',`, "m").exec(ifc)![1];
|
|
19
|
+
|
|
20
|
+
/** The layer of every face set and polyline of the element named `name`, in order */
|
|
16
21
|
function elementLayers(ifc: string, name: string): ReadonlyArray<string | undefined> {
|
|
17
|
-
const
|
|
22
|
+
const byId = entities(ifc);
|
|
18
23
|
const layers = new Map(
|
|
19
24
|
Array.from(ifc.matchAll(/[=]IFCPRESENTATIONLAYERASSIGNMENT\('([^']*)',\$,\(([^)]*)\),\$\);/g)).flatMap(([, layer, items]) =>
|
|
20
25
|
ids(items).map((item) => [item, layer] as const)
|
|
21
26
|
)
|
|
22
27
|
);
|
|
23
|
-
// The element's owner history, placement and shape, the shape's
|
|
24
|
-
const [, , shape] = ids(
|
|
25
|
-
const
|
|
28
|
+
// The element's owner history, placement and shape, the shape's representations and their context and items
|
|
29
|
+
const [, , shape] = ids(byId.get(named(ifc, name))!);
|
|
30
|
+
const items = ids(byId.get(shape)!).flatMap((representation) => ids(byId.get(representation)!).slice(1));
|
|
26
31
|
return items.map((item) => layers.get(item));
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
/** The location, axis and ref direction of the placement of the element or port named `name`, in IFC's Z-up */
|
|
35
|
+
function placementOf(ifc: string, name: string): ReadonlyArray<string> {
|
|
36
|
+
const byId = entities(ifc);
|
|
37
|
+
const [, placement] = ids(byId.get(named(ifc, name))!);
|
|
38
|
+
const [, axes] = ids(byId.get(placement)!);
|
|
39
|
+
return ids(byId.get(axes)!).map((id) => byId.get(id)!);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** An air handler component with a panel and a fan with a motor, the motor's meshes two groups without id or name down */
|
|
43
|
+
const component = (id: string, layer: string): Group => ({
|
|
44
|
+
...group(
|
|
32
45
|
[box(vec3(1, 1, 1), red)],
|
|
33
46
|
vec3Zero,
|
|
34
47
|
vec3Zero,
|
|
35
48
|
[
|
|
36
|
-
group([box(vec3(1, 1, 1), red)], vec3Zero, vec3Zero, [], { type: "panel" }),
|
|
49
|
+
{ ...group([box(vec3(1, 1, 1), red)], vec3Zero, vec3Zero, [], { type: "panel" }), layer: "SECTIONS" },
|
|
37
50
|
group([sphere(1, red)], vec3Zero, vec3Zero, [group([], vec3Zero, vec3Zero, [group([cylinder(vec3Zero, 1, 2, red)])])], { type: "fan_wall" }),
|
|
38
51
|
],
|
|
39
|
-
{
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
|
|
52
|
+
{ key: "FAN" }
|
|
53
|
+
),
|
|
54
|
+
id,
|
|
55
|
+
layer,
|
|
56
|
+
});
|
|
43
57
|
|
|
44
58
|
describe("ifc encoding", () => {
|
|
45
59
|
it("writes reals with a decimal point", () => {
|
|
@@ -60,6 +74,14 @@ describe("ifc encoding", () => {
|
|
|
60
74
|
expect(guid(ifcFile(), uuid)).toBe(first);
|
|
61
75
|
expect(guid(m, uuid)).not.toBe(first);
|
|
62
76
|
});
|
|
77
|
+
|
|
78
|
+
it("derives the guid from a hash of any other id", () => {
|
|
79
|
+
const fromId = guid(ifcFile(), "588c9525-0f1b-4b0b-a3e1-ba10f8a922ff_supply");
|
|
80
|
+
expect(fromId).toMatch(/^[0-9A-Za-z_$]{22}$/);
|
|
81
|
+
expect(guid(ifcFile(), "588c9525-0f1b-4b0b-a3e1-ba10f8a922ff_supply")).toBe(fromId);
|
|
82
|
+
expect(guid(ifcFile(), "588c9525-0f1b-4b0b-a3e1-ba10f8a922ff_extract")).not.toBe(fromId);
|
|
83
|
+
expect(guid(ifcFile(), "")).toMatch(/^[0-9A-Za-z_$]{22}$/);
|
|
84
|
+
});
|
|
63
85
|
});
|
|
64
86
|
|
|
65
87
|
describe("ifc triangulation", () => {
|
|
@@ -76,23 +98,32 @@ describe("ifc triangulation", () => {
|
|
|
76
98
|
expect(triangulate(cylinder(vec3Zero, 1, 2, red, vec3Zero, false, [], 0, Math.PI).geometry)!.triangles.length).toBeGreaterThan(0);
|
|
77
99
|
expect(triangulate(line(vec3Zero, vec3(1, 1, 1), 1, red).geometry)).toBeUndefined();
|
|
78
100
|
});
|
|
101
|
+
|
|
102
|
+
it("converts the end points of lines like the triangle points and nothing else", () => {
|
|
103
|
+
expect(polyline(line(vec3(1, 2, 3), vec3(4, 5, 6), 1, red).geometry, vec3(0, 10, 0))).toEqual(["(1.,-3.,12.)", "(4.,-6.,15.)"]);
|
|
104
|
+
expect(polyline(culledLine(vec3Zero, vec3(1, 0, 0), vec3(0, 0, 1), 1, red).geometry)).toEqual(["(0.,0.,0.)", "(1.,0.,0.)"]);
|
|
105
|
+
expect(polyline(box(vec3(1, 1, 1), red).geometry)).toBeUndefined();
|
|
106
|
+
});
|
|
79
107
|
});
|
|
80
108
|
|
|
81
109
|
describe("ifc render", () => {
|
|
82
|
-
it("writes an element per top-level group and group with
|
|
110
|
+
it("writes an element per top-level group and group with an id or name, merging the other groups into their parent", () => {
|
|
83
111
|
const ifc = render(
|
|
84
112
|
scene([
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
113
|
+
{
|
|
114
|
+
...group(
|
|
115
|
+
[box(vec3(1, 1, 1), red)],
|
|
116
|
+
vec3Zero,
|
|
117
|
+
vec3Zero,
|
|
118
|
+
[
|
|
119
|
+
{ ...group([sphere(1, red)]), name: "Fan" },
|
|
120
|
+
group([box(vec3(2, 2, 2), red)], vec3(0, 100, 0)),
|
|
121
|
+
{ ...group([line(vec3Zero, vec3(1, 0, 0), 1, red)]), name: "Lines only" },
|
|
122
|
+
],
|
|
123
|
+
{ id: "not read" }
|
|
124
|
+
),
|
|
125
|
+
id: "588c9525-0f1b-4b0b-a3e1-ba10f8a922ff",
|
|
126
|
+
},
|
|
96
127
|
group([box(vec3(1, 1, 1), red)]),
|
|
97
128
|
group([]),
|
|
98
129
|
])
|
|
@@ -100,8 +131,8 @@ describe("ifc render", () => {
|
|
|
100
131
|
expect(ifc).toMatch(/^ISO-10303-21;/);
|
|
101
132
|
expect(ifc).toContain("FILE_SCHEMA(('IFC4'));");
|
|
102
133
|
expect(ifc.trimEnd()).toMatch(/END-ISO-10303-21;$/);
|
|
103
|
-
// The
|
|
104
|
-
expect(count(ifc, "IFCBUILDINGELEMENTPROXY")).toBe(
|
|
134
|
+
// The empty group is left out, the group without id or name is merged into its parent
|
|
135
|
+
expect(count(ifc, "IFCBUILDINGELEMENTPROXY")).toBe(4);
|
|
105
136
|
expect(count(ifc, "IFCTRIANGULATEDFACESET")).toBe(4);
|
|
106
137
|
expect(count(ifc, "IFCRELAGGREGATES")).toBe(4);
|
|
107
138
|
expect(count(ifc, "IFCRELCONTAINEDINSPATIALSTRUCTURE")).toBe(1);
|
|
@@ -112,10 +143,13 @@ describe("ifc render", () => {
|
|
|
112
143
|
expect(ifc).toContain("IFCCOLOURRGB($,1.,0.,0.)");
|
|
113
144
|
expect(ifc).toMatch(/[=]IFCSURFACESTYLERENDERING\(#\d+,0\.5,\$,\$,\$,\$,\$,\$,\.NOTDEFINED\.\);/);
|
|
114
145
|
expect(ifc).not.toContain("IFCSURFACESTYLESHADING(");
|
|
146
|
+
// The GlobalId comes from the uuid, the name and tag are the id when there's no name
|
|
147
|
+
expect(ifc).toContain(`IFCBUILDINGELEMENTPROXY('${guid(ifcFile(), "588c9525-0f1b-4b0b-a3e1-ba10f8a922ff")}',`);
|
|
115
148
|
expect(ifc).toContain(",'588c9525-0f1b-4b0b-a3e1-ba10f8a922ff',$,$,#");
|
|
116
149
|
expect(ifc).toContain(",'588c9525-0f1b-4b0b-a3e1-ba10f8a922ff',.NOTDEFINED.)");
|
|
150
|
+
expect(ifc).not.toContain("not read");
|
|
117
151
|
expect(ifc).toContain(",'Fan',$,$,#");
|
|
118
|
-
// IFC requires a name, top-level groups without
|
|
152
|
+
// IFC requires a name, top-level groups without one are named after their position
|
|
119
153
|
expect(ifc).toContain(",'Element 2',$,$,#");
|
|
120
154
|
});
|
|
121
155
|
|
|
@@ -129,30 +163,85 @@ describe("ifc render", () => {
|
|
|
129
163
|
expect(count(ifc, "IFCSURFACESTYLE")).toBe(2);
|
|
130
164
|
});
|
|
131
165
|
|
|
132
|
-
it("
|
|
166
|
+
it("writes a line as a polyline in a Curve3D representation next to the face sets, styled with its colour and on its group's layer", () => {
|
|
167
|
+
const green = { normal: "rgb(0, 255, 0)" };
|
|
133
168
|
const ifc = render(
|
|
134
169
|
scene([
|
|
135
|
-
|
|
136
|
-
|
|
170
|
+
{
|
|
171
|
+
...group([box(vec3(1, 1, 1), red)], vec3Zero, vec3Zero, [group([line(vec3(1, 2, 3), vec3(4, 5, 6), 2, green)], vec3(0, 100, 0))]),
|
|
172
|
+
layer: "A",
|
|
173
|
+
},
|
|
174
|
+
])
|
|
175
|
+
);
|
|
176
|
+
const byId = entities(ifc);
|
|
177
|
+
expect(count(ifc, "IFCPOLYLINE")).toBe(1);
|
|
178
|
+
const [, polylineId, start, end] = /^#(\d+)=IFCPOLYLINE\(\(#(\d+),#(\d+)\)\);$/m.exec(ifc)!;
|
|
179
|
+
// The line keeps its group's offset, Y-up 100 becomes Z
|
|
180
|
+
expect([byId.get(start), byId.get(end)]).toEqual(["IFCCARTESIANPOINT((1.,-3.,102.))", "IFCCARTESIANPOINT((4.,-6.,105.))"]);
|
|
181
|
+
const [tessellation, curve] = ids(Array.from(byId.values()).find((e) => e.startsWith("IFCPRODUCTDEFINITIONSHAPE("))!);
|
|
182
|
+
expect(byId.get(tessellation)).toMatch(/^IFCSHAPEREPRESENTATION\(#\d+,'Body','Tessellation',\(#\d+\)\)$/);
|
|
183
|
+
expect(byId.get(curve)).toMatch(new RegExp(`^IFCSHAPEREPRESENTATION\\(#\\d+,'Body','Curve3D',\\(#${polylineId}\\)\\)$`));
|
|
184
|
+
const [, style] = ids(Array.from(byId.values()).find((e) => e.startsWith(`IFCSTYLEDITEM(#${polylineId},`))!);
|
|
185
|
+
const [colour] = ids(byId.get(style)!);
|
|
186
|
+
expect(byId.get(style)).toBe(`IFCCURVESTYLE($,$,$,#${colour},.T.)`);
|
|
187
|
+
expect(byId.get(colour)).toBe("IFCCOLOURRGB($,0.,1.,0.)");
|
|
188
|
+
expect(elementLayers(ifc, "Element 1")).toEqual(["A", "A"]);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("writes an element with only lines, styling each colour once", () => {
|
|
192
|
+
const green = { normal: "rgb(0, 255, 0)" };
|
|
193
|
+
const ifc = render(
|
|
194
|
+
scene([
|
|
195
|
+
{ ...group([line(vec3Zero, vec3(1, 0, 0), 1, green), culledLine(vec3Zero, vec3(0, 1, 0), vec3(0, 0, 1), 1, green)]), name: "Lines only" },
|
|
196
|
+
])
|
|
197
|
+
);
|
|
198
|
+
expect(count(ifc, "IFCBUILDINGELEMENTPROXY")).toBe(1);
|
|
199
|
+
expect(ifc).toContain(",'Lines only',$,$,#");
|
|
200
|
+
expect(count(ifc, "IFCPOLYLINE")).toBe(2);
|
|
201
|
+
expect(count(ifc, "IFCCURVESTYLE")).toBe(1);
|
|
202
|
+
expect(count(ifc, "IFCTRIANGULATEDFACESET")).toBe(0);
|
|
203
|
+
expect(count(ifc, "IFCSHAPEREPRESENTATION")).toBe(1);
|
|
204
|
+
expect(ifc).toMatch(/[=]IFCSHAPEREPRESENTATION\(#\d+,'Body','Curve3D',\(#\d+,#\d+\)\);/);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("writes each element as the class entity gives it, an IfcBuildingElementProxy otherwise", () => {
|
|
208
|
+
const ifc = render(
|
|
209
|
+
scene([
|
|
210
|
+
{
|
|
211
|
+
...group([], vec3Zero, vec3Zero, [
|
|
212
|
+
{ ...group([box(vec3(1, 1, 1), red)]), name: "Filter" },
|
|
213
|
+
{ ...group([box(vec3(1, 1, 1), red)], vec3(0, 0, 10)), name: "Fan", description: "Supply" },
|
|
214
|
+
{ ...group([box(vec3(1, 1, 1), red)], vec3(0, 0, 20)), name: "Inspection" },
|
|
215
|
+
]),
|
|
216
|
+
name: "AHU",
|
|
217
|
+
},
|
|
137
218
|
]),
|
|
138
|
-
{
|
|
219
|
+
{
|
|
220
|
+
entity: (g) =>
|
|
221
|
+
g.name === "AHU"
|
|
222
|
+
? { entity: "IfcUnitaryEquipment", predefinedType: "AIRHANDLER" }
|
|
223
|
+
: g.name === "Fan"
|
|
224
|
+
? { entity: "IfcFan", predefinedType: "CENTRIFUGALBACKWARDINCLINEDCURVED" }
|
|
225
|
+
: g.name === "Filter"
|
|
226
|
+
? { entity: "IfcFilter" }
|
|
227
|
+
: undefined,
|
|
228
|
+
}
|
|
139
229
|
);
|
|
140
230
|
expect(ifc).toMatch(/[=]IFCUNITARYEQUIPMENT\('[^']{22}',#\d+,'AHU',\$,\$,#\d+,\$,\$,\.AIRHANDLER\.\);/);
|
|
141
|
-
expect(
|
|
142
|
-
|
|
231
|
+
expect(ifc).toMatch(/[=]IFCFAN\('[^']{22}',#\d+,'Fan','Supply',\$,#\d+,#\d+,\$,\.CENTRIFUGALBACKWARDINCLINEDCURVED\.\);/);
|
|
232
|
+
expect(ifc).toMatch(/[=]IFCFILTER\('[^']{22}',#\d+,'Filter',\$,\$,#\d+,#\d+,\$,\.NOTDEFINED\.\);/);
|
|
233
|
+
expect(ifc).toMatch(/[=]IFCBUILDINGELEMENTPROXY\('[^']{22}',#\d+,'Inspection',/);
|
|
234
|
+
// Only the AHU is in the storey, the groups are aggregated into it
|
|
143
235
|
expect(ifc).toMatch(/[=]IFCRELCONTAINEDINSPATIALSTRUCTURE\('[^']{22}',#\d+,\$,\$,\(#\d+\),#\d+\);/);
|
|
144
236
|
expect(count(ifc, "IFCRELAGGREGATES")).toBe(4);
|
|
145
237
|
});
|
|
146
238
|
|
|
147
239
|
it("writes the owner history on every rooted entity and the author and application in the header", () => {
|
|
148
|
-
const ifc = render(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
application: { name: "airCalc ACOM", version: "10.1" },
|
|
154
|
-
}
|
|
155
|
-
);
|
|
240
|
+
const ifc = render(scene([group([box(vec3(1, 1, 1), red)], vec3Zero, vec3Zero, [{ ...group([sphere(1, red)]), name: "Fan" }])]), {
|
|
241
|
+
author: "Ivan",
|
|
242
|
+
organization: "Acme",
|
|
243
|
+
application: { name: "airCalc ACOM", version: "10.1" },
|
|
244
|
+
});
|
|
156
245
|
expect(ifc).toContain("/* name */ 'Abstract 3D.ifc',");
|
|
157
246
|
expect(ifc).toContain("/* author */ ('Ivan'),\n/* organization */ ('Acme'),");
|
|
158
247
|
expect(ifc).toContain("/* originating_system */ 'airCalc ACOM',");
|
|
@@ -180,12 +269,12 @@ describe("ifc render", () => {
|
|
|
180
269
|
expect(count(ifc, "IFCORGANIZATION")).toBe(1);
|
|
181
270
|
});
|
|
182
271
|
|
|
183
|
-
it("makes elements only of groups with
|
|
272
|
+
it("makes elements only of groups with an id or name, merging groups with only data into their parent", () => {
|
|
184
273
|
const ifc = render(
|
|
185
274
|
scene([
|
|
186
275
|
group([box(vec3(1, 1, 1), red)], vec3Zero, vec3Zero, [
|
|
187
|
-
group([box(vec3(1, 1, 1), red)], vec3Zero, vec3Zero, [group([sphere(1, red)],
|
|
188
|
-
group([sphere(1, red)], vec3Zero, vec3Zero, [], {
|
|
276
|
+
group([box(vec3(1, 1, 1), red)], vec3Zero, vec3Zero, [{ ...group([sphere(1, red)]), id: "fan" }], { type: "panel", id: "data" }),
|
|
277
|
+
{ ...group([sphere(1, red)], vec3Zero, vec3Zero, [], { type: "motor" }), name: "Motor" },
|
|
189
278
|
]),
|
|
190
279
|
])
|
|
191
280
|
);
|
|
@@ -196,73 +285,168 @@ describe("ifc render", () => {
|
|
|
196
285
|
expect(ifc).toMatch(/[=]IFCRELAGGREGATES\('[^']{22}',#\d+,\$,\$,#\d+,\(#\d+,#\d+\)\);/);
|
|
197
286
|
});
|
|
198
287
|
|
|
199
|
-
it("puts the meshes on the layer of the nearest group above them with one,
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
paths.push(path);
|
|
205
|
-
const key = path.filter((d) => "key" in d).at(-1)?.key;
|
|
206
|
-
return path.at(-1)?.type === "panel" ? "acomSECTIONS" : key ? `acom${key}` : undefined;
|
|
207
|
-
},
|
|
208
|
-
});
|
|
209
|
-
// Box, panel, fan and the motor two groups without data further down
|
|
210
|
-
expect(elementLayers(ifc, "a1")).toEqual(["acomFAN", "acomSECTIONS", "acomFAN", "acomFAN"]);
|
|
211
|
-
expect(elementLayers(ifc, "a3")).toEqual(["acomDUCTCONNECTORS", "acomSECTIONS", "acomDUCTCONNECTORS", "acomDUCTCONNECTORS"]);
|
|
288
|
+
it("puts the meshes on the layer of the nearest group above them with one, through elements and groups without one", () => {
|
|
289
|
+
const ifc = render(scene([component("a1", "FAN"), component("a2", "FAN"), component("a3", "DUCTCONNECTORS")]));
|
|
290
|
+
// Box, panel, fan and the motor two groups without a layer further down
|
|
291
|
+
expect(elementLayers(ifc, "a1")).toEqual(["FAN", "SECTIONS", "FAN", "FAN"]);
|
|
292
|
+
expect(elementLayers(ifc, "a3")).toEqual(["DUCTCONNECTORS", "SECTIONS", "DUCTCONNECTORS", "DUCTCONNECTORS"]);
|
|
212
293
|
// One layer per name across the elements
|
|
213
294
|
expect(count(ifc, "IFCPRESENTATIONLAYERASSIGNMENT")).toBe(3);
|
|
214
|
-
expect(ifc).toMatch(/[=]IFCPRESENTATIONLAYERASSIGNMENT\('
|
|
215
|
-
expect(ifc).toMatch(/[=]IFCPRESENTATIONLAYERASSIGNMENT\('
|
|
216
|
-
expect(paths).toContainEqual([{ name: "AHU" }, { id: "a1", key: "FAN" }, { type: "panel" }]);
|
|
217
|
-
// The root, 3 components and their panel and fan, groups without data are skipped
|
|
218
|
-
expect(paths).toHaveLength(10);
|
|
219
|
-
});
|
|
295
|
+
expect(ifc).toMatch(/[=]IFCPRESENTATIONLAYERASSIGNMENT\('SECTIONS',\$,\(#\d+,#\d+,#\d+\),\$\);/);
|
|
296
|
+
expect(ifc).toMatch(/[=]IFCPRESENTATIONLAYERASSIGNMENT\('FAN',\$,\((#\d+,){5}#\d+\),\$\);/);
|
|
220
297
|
|
|
221
|
-
|
|
222
|
-
const ifc = render(
|
|
298
|
+
const nested = render(
|
|
223
299
|
scene([
|
|
224
|
-
|
|
225
|
-
[box(vec3(1, 1, 1), red)],
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
layered("", [group([box(vec3(1, 1, 1), red)])]),
|
|
231
|
-
layered("B", [group([], vec3Zero, vec3Zero, [group([box(vec3(1, 1, 1), red)])])]),
|
|
232
|
-
layered("B", [group([box(vec3(1, 1, 1), red)], vec3Zero, vec3Zero, [], { name: "Inner", type: "fan" })]),
|
|
233
|
-
],
|
|
234
|
-
{ layer: "A" }
|
|
235
|
-
),
|
|
300
|
+
{
|
|
301
|
+
...group([box(vec3(1, 1, 1), red)], vec3Zero, vec3Zero, [
|
|
302
|
+
{ ...group([box(vec3(1, 1, 1), red)], vec3Zero, vec3Zero, [], {}), name: "Inner" },
|
|
303
|
+
]),
|
|
304
|
+
layer: "A",
|
|
305
|
+
},
|
|
236
306
|
group([box(vec3(1, 1, 1), red)]),
|
|
237
|
-
])
|
|
238
|
-
{ layer: (path) => path.at(-1)?.layer }
|
|
307
|
+
])
|
|
239
308
|
);
|
|
240
|
-
|
|
241
|
-
expect(elementLayers(
|
|
242
|
-
|
|
243
|
-
expect(
|
|
244
|
-
expect(count(ifc, "IFCPRESENTATIONLAYERASSIGNMENT")).toBe(2);
|
|
309
|
+
// Elements inherit the layer too, meshes without a layer above them get none
|
|
310
|
+
expect(elementLayers(nested, "Inner")).toEqual(["A"]);
|
|
311
|
+
expect(elementLayers(nested, "Element 2")).toEqual([undefined]);
|
|
312
|
+
expect(count(nested, "IFCPRESENTATIONLAYERASSIGNMENT")).toBe(1);
|
|
245
313
|
});
|
|
246
314
|
|
|
247
315
|
it("only adds the layers to the output", () => {
|
|
248
316
|
vi.useFakeTimers({ now: new Date("2026-10-12T10:00:00Z") });
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
]);
|
|
254
|
-
const plain = normalize(render(s));
|
|
255
|
-
const undefinedLayers = normalize(render(s, { layer: () => undefined }));
|
|
256
|
-
const withLayers = normalize(render(s, { layer: () => "A" }));
|
|
317
|
+
const fan = { ...group([sphere(1, red)], vec3Zero, vec3Zero, [group([cylinder(vec3Zero, 1, 2, red)])]), name: "Fan" };
|
|
318
|
+
const s = (layer?: string): Scene => scene([group([box(vec3(1, 1, 1), red)], vec3(1, 2, 3), vec3(0.1, 0, 0), [{ ...fan, layer }])]);
|
|
319
|
+
const plain = normalize(render(s()));
|
|
320
|
+
const withLayer = normalize(render(s("A")));
|
|
257
321
|
vi.useRealTimers();
|
|
258
322
|
expect(count(plain, "IFCPRESENTATIONLAYERASSIGNMENT")).toBe(0);
|
|
259
|
-
expect(
|
|
260
|
-
|
|
261
|
-
|
|
323
|
+
expect(withLayer.replace(/^#\d+=IFCPRESENTATIONLAYERASSIGNMENT\('A',\$,\(#\d+,#\d+\),\$\);\n/m, "")).toBe(plain);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("writes a relation between two elements as a source port and a sink port nested in them and connected", () => {
|
|
327
|
+
const ifc = render(
|
|
328
|
+
scene(
|
|
329
|
+
[
|
|
330
|
+
{ ...group([box(vec3(1, 1, 1), red)]), id: "filter" },
|
|
331
|
+
{ ...group([box(vec3(1, 1, 1), red)], vec3(100, 0, 0)), id: "fan" },
|
|
332
|
+
],
|
|
333
|
+
[{ id: "r1", from: "filter", to: "fan", pos: vec3(50, 0, 0), name: "Filter to fan" }]
|
|
334
|
+
),
|
|
335
|
+
{ port: () => ({ predefinedType: "DUCT", systemType: "VENTILATION" }) }
|
|
336
|
+
);
|
|
337
|
+
const ports = Array.from(
|
|
338
|
+
ifc.matchAll(/^#(\d+)=IFCDISTRIBUTIONPORT\('[^']{22}',#\d+,'Filter to fan',\$,\$,#\d+,\$,\.(\w+)\.,\.DUCT\.,\.VENTILATION\.\);$/gm)
|
|
339
|
+
);
|
|
340
|
+
expect(ports.map(([, , flow]) => flow)).toEqual(["SOURCE", "SINK"]);
|
|
341
|
+
const [source, sink] = ports.map(([, id]) => id);
|
|
342
|
+
expect(ifc).toMatch(new RegExp(`=IFCRELCONNECTSPORTS\\('[^']{22}',#\\d+,\\$,\\$,#${source},#${sink},\\$\\);`));
|
|
343
|
+
expect(ifc).toMatch(new RegExp(`=IFCRELNESTS\\('[^']{22}',#\\d+,\\$,\\$,#${named(ifc, "filter")},\\(#${source}\\)\\);`));
|
|
344
|
+
expect(ifc).toMatch(new RegExp(`=IFCRELNESTS\\('[^']{22}',#\\d+,\\$,\\$,#${named(ifc, "fan")},\\(#${sink}\\)\\);`));
|
|
345
|
+
// Each port is placed relative to its element, the fan's at -50 along x
|
|
346
|
+
const byId = entities(ifc);
|
|
347
|
+
const [, sinkPlacement] = ids(byId.get(sink)!);
|
|
348
|
+
expect(ids(byId.get(sinkPlacement)!)[0]).toBe(ids(byId.get(named(ifc, "fan"))!)[1]);
|
|
349
|
+
expect(ifc).toContain("IFCCARTESIANPOINT((-50.,0.,0.))");
|
|
350
|
+
// Ports are only nested, not contained in the storey
|
|
351
|
+
expect(ifc).toMatch(/[=]IFCRELCONTAINEDINSPATIALSTRUCTURE\('[^']{22}',#\d+,\$,\$,\(#\d+,#\d+\),#\d+\);/);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("writes sibling groups with the same id as one element, its parts in their places, with ports from relations to either part", () => {
|
|
355
|
+
const ifc = render(
|
|
356
|
+
scene(
|
|
357
|
+
[
|
|
358
|
+
{ ...group([box(vec3(1, 1, 1), red)], vec3(0, 0, 0)), id: "exchanger", name: "PTD", description: "Plate", layer: "SUPPLY" },
|
|
359
|
+
{ ...group([box(vec3(1, 1, 1), red)], vec3(0, 100, 0)), id: "exchanger", name: "PTD", description: "Plate", layer: "EXHAUST" },
|
|
360
|
+
{ ...group([box(vec3(1, 1, 1), red)], vec3(100, 100, 0)), id: "filter" },
|
|
361
|
+
],
|
|
362
|
+
[{ id: "r1", from: "filter", to: "exchanger", pos: vec3(50, 100, 0) }]
|
|
363
|
+
)
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
expect(count(ifc, "IFCBUILDINGELEMENTPROXY")).toBe(2);
|
|
367
|
+
expect(ifc).toMatch(/[=]IFCBUILDINGELEMENTPROXY\('[^']{22}',#\d+,'PTD','Plate',/);
|
|
368
|
+
expect(elementLayers(ifc, "PTD")).toEqual(["SUPPLY", "EXHAUST"]);
|
|
369
|
+
expect(placementOf(ifc, "PTD")[0]).toBe("IFCCARTESIANPOINT((0.,0.,0.))");
|
|
370
|
+
expect(count(ifc, "IFCRELCONNECTSPORTS")).toBe(1);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("writes a relation with one element as an open port with the relation's groups as its shape, and leaves out relations without elements", () => {
|
|
374
|
+
const terminal = { ...group([plane(vec3Zero, vec2(10, 10), red)], vec3(50, 0, 0)), id: "terminal", layer: "DUCTCONNECTORS" };
|
|
375
|
+
const ifc = render(
|
|
376
|
+
scene(
|
|
377
|
+
[{ ...group([box(vec3(1, 1, 1), red)]), id: "fan" }],
|
|
378
|
+
[
|
|
379
|
+
{ id: "sup", from: "fan", to: "terminal", pos: vec3(50, 0, 0), name: "SUP", groups: [terminal] },
|
|
380
|
+
{ id: "oda", to: "fan", pos: vec3(-50, 0, 0), name: "ODA" },
|
|
381
|
+
{ id: "unknown", from: "nothing", to: "missing", pos: vec3Zero, name: "Unknown" },
|
|
382
|
+
]
|
|
383
|
+
)
|
|
384
|
+
);
|
|
385
|
+
expect(count(ifc, "IFCDISTRIBUTIONPORT")).toBe(2);
|
|
386
|
+
expect(ifc).toMatch(/[=]IFCDISTRIBUTIONPORT\('[^']{22}',#\d+,'SUP',\$,\$,#\d+,#\d+,\.SOURCE\.,\.NOTDEFINED\.,\$\);/);
|
|
387
|
+
expect(ifc).toMatch(/[=]IFCDISTRIBUTIONPORT\('[^']{22}',#\d+,'ODA',\$,\$,#\d+,\$,\.SINK\.,\.NOTDEFINED\.,\$\);/);
|
|
388
|
+
expect(count(ifc, "IFCRELCONNECTSPORTS")).toBe(0);
|
|
389
|
+
// Both ports in one nesting
|
|
390
|
+
expect(count(ifc, "IFCRELNESTS")).toBe(1);
|
|
391
|
+
// The shape is relative to the port, the terminal's group isn't an element
|
|
392
|
+
expect(elementLayers(ifc, "SUP")).toEqual(["DUCTCONNECTORS"]);
|
|
393
|
+
expect(ifc).toContain("(5.,0.,5.)");
|
|
394
|
+
expect(ifc).not.toContain("'terminal'");
|
|
395
|
+
expect(ifc).not.toContain("'Unknown'");
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("places a port in its element's rotated axes, and the relation's shape on the source's port only", () => {
|
|
399
|
+
const ifc = render(
|
|
400
|
+
scene(
|
|
401
|
+
[
|
|
402
|
+
{ ...group([box(vec3(1, 1, 1), red)], vec3Zero, vec3(0, Math.PI / 2, 0)), id: "a" },
|
|
403
|
+
{ ...group([box(vec3(1, 1, 1), red)], vec3(0, 0, -20)), id: "b" },
|
|
404
|
+
],
|
|
405
|
+
[
|
|
406
|
+
{
|
|
407
|
+
id: "r",
|
|
408
|
+
from: "a",
|
|
409
|
+
to: "b",
|
|
410
|
+
name: "A to B",
|
|
411
|
+
pos: vec3(0, 0, -10),
|
|
412
|
+
rot: vec3(0, Math.PI / 2, 0),
|
|
413
|
+
groups: [group([plane(vec3Zero, vec2(1, 1), red)], vec3(0, 0, -10), vec3(0, Math.PI / 2, 0))],
|
|
414
|
+
},
|
|
415
|
+
]
|
|
416
|
+
)
|
|
417
|
+
);
|
|
418
|
+
const [source, sink] = Array.from(ifc.matchAll(/^#(\d+)=IFCDISTRIBUTIONPORT\(.*'A to B',\$,\$,#\d+,([#$\d]+),/gm), ([, , shape]) => shape);
|
|
419
|
+
expect(source).toMatch(/^#\d+$/);
|
|
420
|
+
expect(sink).toBe("$");
|
|
421
|
+
// The element is turned 90° around Y-up so its x points along -z, where the relation is, and the relation turned the same way
|
|
422
|
+
const byId = entities(ifc);
|
|
423
|
+
const [sourcePort] = Array.from(ifc.matchAll(/^#(\d+)=IFCDISTRIBUTIONPORT\(/gm), (match) => match[1]);
|
|
424
|
+
const [, placement] = ids(byId.get(sourcePort)!);
|
|
425
|
+
const [, axes] = ids(byId.get(placement)!);
|
|
426
|
+
expect(ids(byId.get(axes)!).map((id) => byId.get(id))).toEqual([
|
|
427
|
+
"IFCCARTESIANPOINT((10.,0.,0.))",
|
|
428
|
+
"IFCDIRECTION((0.,0.,1.))",
|
|
429
|
+
"IFCDIRECTION((1.,0.,0.))",
|
|
430
|
+
]);
|
|
431
|
+
expect(placementOf(ifc, "b")[0]).toBe("IFCCARTESIANPOINT((0.,20.,0.))");
|
|
262
432
|
});
|
|
263
433
|
|
|
264
434
|
it("only references entities that exist", () => {
|
|
265
|
-
const ifc = render(
|
|
435
|
+
const ifc = render(
|
|
436
|
+
scene(
|
|
437
|
+
[
|
|
438
|
+
{
|
|
439
|
+
...group([box(vec3(1, 1, 1), red), line(vec3Zero, vec3(1, 1, 1), 1, red)], vec3(1, 2, 3), vec3(0.1, 0.2, 0.3), [group([sphere(1, red)])]),
|
|
440
|
+
id: "a",
|
|
441
|
+
},
|
|
442
|
+
{ ...group([box(vec3(1, 1, 1), red)]), id: "b" },
|
|
443
|
+
],
|
|
444
|
+
[
|
|
445
|
+
{ id: "r", from: "a", to: "b", pos: vec3Zero, groups: [group([plane(vec3Zero, vec2(1, 1), red)], vec3(1, 2, 3), vec3(0.1, 0.2, 0.3))] },
|
|
446
|
+
{ id: "o", from: "b", pos: vec3Zero },
|
|
447
|
+
]
|
|
448
|
+
)
|
|
449
|
+
);
|
|
266
450
|
const ids = new Set(Array.from(ifc.matchAll(/^#(\d+)=/gm), (match) => match[1]));
|
|
267
451
|
const referenced = Array.from(ifc.matchAll(/[(,]#(\d+)/g), (match) => match[1]);
|
|
268
452
|
expect(referenced.filter((id) => !ids.has(id))).toEqual([]);
|