abstract-3d 2.12.2 → 3.0.1
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 +33 -6
- package/lib/abstract-3d.d.ts.map +1 -1
- package/lib/abstract-3d.js +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.d.ts +35 -22
- package/lib/renderers/ifc/ifc.d.ts.map +1 -1
- package/lib/renderers/ifc/ifc.js +124 -49
- 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 +39 -8
- package/src/renderers/ifc/ifc-encoding.ts +31 -7
- package/src/renderers/ifc/ifc.test.ts +233 -102
- package/src/renderers/ifc/ifc.ts +263 -80
- 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,12 +1,12 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
import type { Scene } from "../../abstract-3d.js";
|
|
3
|
-
import { box, culledLine, 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
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 */
|
|
@@ -14,6 +14,9 @@ const normalize = (ifc: string): string => ifc.replaceAll(/'[0-9A-Za-z_$]{22}'/g
|
|
|
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
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
|
+
|
|
17
20
|
/** The layer of every face set and polyline of the element named `name`, in order */
|
|
18
21
|
function elementLayers(ifc: string, name: string): ReadonlyArray<string | undefined> {
|
|
19
22
|
const byId = entities(ifc);
|
|
@@ -23,25 +26,34 @@ function elementLayers(ifc: string, name: string): ReadonlyArray<string | undefi
|
|
|
23
26
|
)
|
|
24
27
|
);
|
|
25
28
|
// The element's owner history, placement and shape, the shape's representations and their context and items
|
|
26
|
-
const [, , shape] = ids(
|
|
29
|
+
const [, , shape] = ids(byId.get(named(ifc, name))!);
|
|
27
30
|
const items = ids(byId.get(shape)!).flatMap((representation) => ids(byId.get(representation)!).slice(1));
|
|
28
31
|
return items.map((item) => layers.get(item));
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
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(
|
|
34
45
|
[box(vec3(1, 1, 1), red)],
|
|
35
46
|
vec3Zero,
|
|
36
47
|
vec3Zero,
|
|
37
48
|
[
|
|
38
|
-
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" },
|
|
39
50
|
group([sphere(1, red)], vec3Zero, vec3Zero, [group([], vec3Zero, vec3Zero, [group([cylinder(vec3Zero, 1, 2, red)])])], { type: "fan_wall" }),
|
|
40
51
|
],
|
|
41
|
-
{
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
|
|
52
|
+
{ key: "FAN" }
|
|
53
|
+
),
|
|
54
|
+
id,
|
|
55
|
+
layer,
|
|
56
|
+
});
|
|
45
57
|
|
|
46
58
|
describe("ifc encoding", () => {
|
|
47
59
|
it("writes reals with a decimal point", () => {
|
|
@@ -62,6 +74,14 @@ describe("ifc encoding", () => {
|
|
|
62
74
|
expect(guid(ifcFile(), uuid)).toBe(first);
|
|
63
75
|
expect(guid(m, uuid)).not.toBe(first);
|
|
64
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
|
+
});
|
|
65
85
|
});
|
|
66
86
|
|
|
67
87
|
describe("ifc triangulation", () => {
|
|
@@ -87,20 +107,23 @@ describe("ifc triangulation", () => {
|
|
|
87
107
|
});
|
|
88
108
|
|
|
89
109
|
describe("ifc render", () => {
|
|
90
|
-
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", () => {
|
|
91
111
|
const ifc = render(
|
|
92
112
|
scene([
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
+
},
|
|
104
127
|
group([box(vec3(1, 1, 1), red)]),
|
|
105
128
|
group([]),
|
|
106
129
|
])
|
|
@@ -108,7 +131,7 @@ describe("ifc render", () => {
|
|
|
108
131
|
expect(ifc).toMatch(/^ISO-10303-21;/);
|
|
109
132
|
expect(ifc).toContain("FILE_SCHEMA(('IFC4'));");
|
|
110
133
|
expect(ifc.trimEnd()).toMatch(/END-ISO-10303-21;$/);
|
|
111
|
-
// The empty group is left out, the group without
|
|
134
|
+
// The empty group is left out, the group without id or name is merged into its parent
|
|
112
135
|
expect(count(ifc, "IFCBUILDINGELEMENTPROXY")).toBe(4);
|
|
113
136
|
expect(count(ifc, "IFCTRIANGULATEDFACESET")).toBe(4);
|
|
114
137
|
expect(count(ifc, "IFCRELAGGREGATES")).toBe(4);
|
|
@@ -120,10 +143,13 @@ describe("ifc render", () => {
|
|
|
120
143
|
expect(ifc).toContain("IFCCOLOURRGB($,1.,0.,0.)");
|
|
121
144
|
expect(ifc).toMatch(/[=]IFCSURFACESTYLERENDERING\(#\d+,0\.5,\$,\$,\$,\$,\$,\$,\.NOTDEFINED\.\);/);
|
|
122
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")}',`);
|
|
123
148
|
expect(ifc).toContain(",'588c9525-0f1b-4b0b-a3e1-ba10f8a922ff',$,$,#");
|
|
124
149
|
expect(ifc).toContain(",'588c9525-0f1b-4b0b-a3e1-ba10f8a922ff',.NOTDEFINED.)");
|
|
150
|
+
expect(ifc).not.toContain("not read");
|
|
125
151
|
expect(ifc).toContain(",'Fan',$,$,#");
|
|
126
|
-
// IFC requires a name, top-level groups without
|
|
152
|
+
// IFC requires a name, top-level groups without one are named after their position
|
|
127
153
|
expect(ifc).toContain(",'Element 2',$,$,#");
|
|
128
154
|
});
|
|
129
155
|
|
|
@@ -141,11 +167,11 @@ describe("ifc render", () => {
|
|
|
141
167
|
const green = { normal: "rgb(0, 255, 0)" };
|
|
142
168
|
const ifc = render(
|
|
143
169
|
scene([
|
|
144
|
-
|
|
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))]),
|
|
145
172
|
layer: "A",
|
|
146
|
-
}
|
|
147
|
-
])
|
|
148
|
-
{ layer: (path) => path.at(-1)?.layer }
|
|
173
|
+
},
|
|
174
|
+
])
|
|
149
175
|
);
|
|
150
176
|
const byId = entities(ifc);
|
|
151
177
|
expect(count(ifc, "IFCPOLYLINE")).toBe(1);
|
|
@@ -166,9 +192,7 @@ describe("ifc render", () => {
|
|
|
166
192
|
const green = { normal: "rgb(0, 255, 0)" };
|
|
167
193
|
const ifc = render(
|
|
168
194
|
scene([
|
|
169
|
-
group([line(vec3Zero, vec3(1, 0, 0), 1, green), culledLine(vec3Zero, vec3(0, 1, 0), vec3(0, 0, 1), 1, green)],
|
|
170
|
-
name: "Lines only",
|
|
171
|
-
}),
|
|
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" },
|
|
172
196
|
])
|
|
173
197
|
);
|
|
174
198
|
expect(count(ifc, "IFCBUILDINGELEMENTPROXY")).toBe(1);
|
|
@@ -180,30 +204,44 @@ describe("ifc render", () => {
|
|
|
180
204
|
expect(ifc).toMatch(/[=]IFCSHAPEREPRESENTATION\(#\d+,'Body','Curve3D',\(#\d+,#\d+\)\);/);
|
|
181
205
|
});
|
|
182
206
|
|
|
183
|
-
it("
|
|
207
|
+
it("writes each element as the class entity gives it, an IfcBuildingElementProxy otherwise", () => {
|
|
184
208
|
const ifc = render(
|
|
185
209
|
scene([
|
|
186
|
-
|
|
187
|
-
|
|
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
|
+
},
|
|
188
218
|
]),
|
|
189
|
-
{
|
|
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
|
+
}
|
|
190
229
|
);
|
|
191
230
|
expect(ifc).toMatch(/[=]IFCUNITARYEQUIPMENT\('[^']{22}',#\d+,'AHU',\$,\$,#\d+,\$,\$,\.AIRHANDLER\.\);/);
|
|
192
|
-
expect(
|
|
193
|
-
|
|
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
|
|
194
235
|
expect(ifc).toMatch(/[=]IFCRELCONTAINEDINSPATIALSTRUCTURE\('[^']{22}',#\d+,\$,\$,\(#\d+\),#\d+\);/);
|
|
195
236
|
expect(count(ifc, "IFCRELAGGREGATES")).toBe(4);
|
|
196
237
|
});
|
|
197
238
|
|
|
198
239
|
it("writes the owner history on every rooted entity and the author and application in the header", () => {
|
|
199
|
-
const ifc = render(
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
application: { name: "airCalc ACOM", version: "10.1" },
|
|
205
|
-
}
|
|
206
|
-
);
|
|
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
|
+
});
|
|
207
245
|
expect(ifc).toContain("/* name */ 'Abstract 3D.ifc',");
|
|
208
246
|
expect(ifc).toContain("/* author */ ('Ivan'),\n/* organization */ ('Acme'),");
|
|
209
247
|
expect(ifc).toContain("/* originating_system */ 'airCalc ACOM',");
|
|
@@ -231,12 +269,12 @@ describe("ifc render", () => {
|
|
|
231
269
|
expect(count(ifc, "IFCORGANIZATION")).toBe(1);
|
|
232
270
|
});
|
|
233
271
|
|
|
234
|
-
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", () => {
|
|
235
273
|
const ifc = render(
|
|
236
274
|
scene([
|
|
237
275
|
group([box(vec3(1, 1, 1), red)], vec3Zero, vec3Zero, [
|
|
238
|
-
group([box(vec3(1, 1, 1), red)], vec3Zero, vec3Zero, [group([sphere(1, red)],
|
|
239
|
-
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" },
|
|
240
278
|
]),
|
|
241
279
|
])
|
|
242
280
|
);
|
|
@@ -247,74 +285,167 @@ describe("ifc render", () => {
|
|
|
247
285
|
expect(ifc).toMatch(/[=]IFCRELAGGREGATES\('[^']{22}',#\d+,\$,\$,#\d+,\(#\d+,#\d+\)\);/);
|
|
248
286
|
});
|
|
249
287
|
|
|
250
|
-
it("puts the meshes on the layer of the nearest group above them with one,
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
paths.push(path);
|
|
256
|
-
const key = path.filter((d) => "key" in d).at(-1)?.key;
|
|
257
|
-
return path.at(-1)?.type === "panel" ? "acomSECTIONS" : key ? `acom${key}` : undefined;
|
|
258
|
-
},
|
|
259
|
-
});
|
|
260
|
-
// Box, panel, fan and the motor two groups without data further down
|
|
261
|
-
expect(elementLayers(ifc, "a1")).toEqual(["acomFAN", "acomSECTIONS", "acomFAN", "acomFAN"]);
|
|
262
|
-
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"]);
|
|
263
293
|
// One layer per name across the elements
|
|
264
294
|
expect(count(ifc, "IFCPRESENTATIONLAYERASSIGNMENT")).toBe(3);
|
|
265
|
-
expect(ifc).toMatch(/[=]IFCPRESENTATIONLAYERASSIGNMENT\('
|
|
266
|
-
expect(ifc).toMatch(/[=]IFCPRESENTATIONLAYERASSIGNMENT\('
|
|
267
|
-
expect(paths).toContainEqual([{ name: "AHU" }, { id: "a1", key: "FAN" }, { type: "panel" }]);
|
|
268
|
-
// The root, 3 components and their panel and fan, groups without data are skipped
|
|
269
|
-
expect(paths).toHaveLength(10);
|
|
270
|
-
});
|
|
295
|
+
expect(ifc).toMatch(/[=]IFCPRESENTATIONLAYERASSIGNMENT\('SECTIONS',\$,\(#\d+,#\d+,#\d+\),\$\);/);
|
|
296
|
+
expect(ifc).toMatch(/[=]IFCPRESENTATIONLAYERASSIGNMENT\('FAN',\$,\((#\d+,){5}#\d+\),\$\);/);
|
|
271
297
|
|
|
272
|
-
|
|
273
|
-
const ifc = render(
|
|
298
|
+
const nested = render(
|
|
274
299
|
scene([
|
|
275
|
-
|
|
276
|
-
[box(vec3(1, 1, 1), red)],
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
layered("", [group([box(vec3(1, 1, 1), red)])]),
|
|
282
|
-
layered("B", [group([], vec3Zero, vec3Zero, [group([box(vec3(1, 1, 1), red)])])]),
|
|
283
|
-
layered("B", [group([box(vec3(1, 1, 1), red)], vec3Zero, vec3Zero, [], { name: "Inner", type: "fan" })]),
|
|
284
|
-
],
|
|
285
|
-
{ layer: "A" }
|
|
286
|
-
),
|
|
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
|
+
},
|
|
287
306
|
group([box(vec3(1, 1, 1), red)]),
|
|
288
|
-
])
|
|
289
|
-
{ layer: (path) => path.at(-1)?.layer }
|
|
307
|
+
])
|
|
290
308
|
);
|
|
291
|
-
|
|
292
|
-
expect(elementLayers(
|
|
293
|
-
|
|
294
|
-
expect(
|
|
295
|
-
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);
|
|
296
313
|
});
|
|
297
314
|
|
|
298
315
|
it("only adds the layers to the output", () => {
|
|
299
316
|
vi.useFakeTimers({ now: new Date("2026-10-12T10:00:00Z") });
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
]);
|
|
305
|
-
const plain = normalize(render(s));
|
|
306
|
-
const undefinedLayers = normalize(render(s, { layer: () => undefined }));
|
|
307
|
-
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")));
|
|
308
321
|
vi.useRealTimers();
|
|
309
322
|
expect(count(plain, "IFCPRESENTATIONLAYERASSIGNMENT")).toBe(0);
|
|
310
|
-
expect(
|
|
311
|
-
|
|
312
|
-
|
|
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.))");
|
|
313
432
|
});
|
|
314
433
|
|
|
315
434
|
it("only references entities that exist", () => {
|
|
316
435
|
const ifc = render(
|
|
317
|
-
scene(
|
|
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
|
+
)
|
|
318
449
|
);
|
|
319
450
|
const ids = new Set(Array.from(ifc.matchAll(/^#(\d+)=/gm), (match) => match[1]));
|
|
320
451
|
const referenced = Array.from(ifc.matchAll(/[(,]#(\d+)/g), (match) => match[1]);
|