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.
Files changed (61) hide show
  1. package/lib/abstract-3d.d.ts +32 -5
  2. package/lib/abstract-3d.d.ts.map +1 -1
  3. package/lib/abstract-3d.js.map +1 -1
  4. package/lib/renderers/ifc/ifc-encoding.d.ts +5 -2
  5. package/lib/renderers/ifc/ifc-encoding.d.ts.map +1 -1
  6. package/lib/renderers/ifc/ifc-encoding.js +30 -7
  7. package/lib/renderers/ifc/ifc-encoding.js.map +1 -1
  8. package/lib/renderers/ifc/ifc-geometry.d.ts +4 -0
  9. package/lib/renderers/ifc/ifc-geometry.d.ts.map +1 -1
  10. package/lib/renderers/ifc/ifc-geometry.js +8 -0
  11. package/lib/renderers/ifc/ifc-geometry.js.map +1 -1
  12. package/lib/renderers/ifc/ifc.d.ts +36 -23
  13. package/lib/renderers/ifc/ifc.d.ts.map +1 -1
  14. package/lib/renderers/ifc/ifc.js +164 -67
  15. package/lib/renderers/ifc/ifc.js.map +1 -1
  16. package/lib/renderers/react/index.d.ts +0 -1
  17. package/lib/renderers/react/index.d.ts.map +1 -1
  18. package/lib/renderers/react/index.js.map +1 -1
  19. package/lib/renderers/react/react-dimension.d.ts +2 -2
  20. package/lib/renderers/react/react-dimension.d.ts.map +1 -1
  21. package/lib/renderers/react/react-dimension.js +2 -2
  22. package/lib/renderers/react/react-dimension.js.map +1 -1
  23. package/lib/renderers/react/react-group.d.ts +3 -2
  24. package/lib/renderers/react/react-group.d.ts.map +1 -1
  25. package/lib/renderers/react/react-group.js +3 -3
  26. package/lib/renderers/react/react-group.js.map +1 -1
  27. package/lib/renderers/react/react-material.d.ts +2 -2
  28. package/lib/renderers/react/react-material.d.ts.map +1 -1
  29. package/lib/renderers/react/react-material.js +3 -3
  30. package/lib/renderers/react/react-material.js.map +1 -1
  31. package/lib/renderers/react/react-relation.d.ts +10 -0
  32. package/lib/renderers/react/react-relation.d.ts.map +1 -0
  33. package/lib/renderers/react/react-relation.js +31 -0
  34. package/lib/renderers/react/react-relation.js.map +1 -0
  35. package/lib/renderers/react/react-scene.d.ts +4 -7
  36. package/lib/renderers/react/react-scene.d.ts.map +1 -1
  37. package/lib/renderers/react/react-scene.js +4 -4
  38. package/lib/renderers/react/react-scene.js.map +1 -1
  39. package/lib/renderers/react/react.d.ts +8 -7
  40. package/lib/renderers/react/react.d.ts.map +1 -1
  41. package/lib/renderers/react/react.js +2 -2
  42. package/lib/renderers/react/react.js.map +1 -1
  43. package/package.json +2 -2
  44. package/src/abstract-3d.ts +33 -6
  45. package/src/renderers/ifc/ifc-encoding.ts +31 -7
  46. package/src/renderers/ifc/ifc-geometry.ts +12 -0
  47. package/src/renderers/ifc/ifc.test.ts +285 -101
  48. package/src/renderers/ifc/ifc.ts +313 -98
  49. package/src/renderers/react/index.ts +0 -1
  50. package/src/renderers/react/react-dimension.tsx +3 -3
  51. package/src/renderers/react/react-group.test.ts +2 -2
  52. package/src/renderers/react/react-group.tsx +5 -4
  53. package/src/renderers/react/react-material.tsx +4 -4
  54. package/src/renderers/react/react-relation.tsx +86 -0
  55. package/src/renderers/react/react-scene.tsx +9 -21
  56. package/src/renderers/react/react.tsx +11 -14
  57. package/lib/renderers/react/react-hotspot.d.ts +0 -27
  58. package/lib/renderers/react/react-hotspot.d.ts.map +0 -1
  59. package/lib/renderers/react/react-hotspot.js +0 -37
  60. package/lib/renderers/react/react-hotspot.js.map +0 -1
  61. package/src/renderers/react/react-hotspot.tsx +0 -112
@@ -1,18 +1,125 @@
1
1
  import { Color, SRGBColorSpace } from "three";
2
- import type { Scene, Group, Material, Vec3 } from "../../abstract-3d.js";
3
- import { vec3Zero, vec3PosX, vec3PosY, vec3Rot, vec3TransRot, vec3RotCombine } from "../../abstract-3d.js";
2
+ import type { Scene, Group, Material, Relation, Vec3 } from "../../abstract-3d.js";
3
+ import { vec3Zero, vec3PosX, vec3PosY, vec3Rot, vec3Sub, vec3TransRot, vec3RotCombine, vec3RotInverse } from "../../abstract-3d.js";
4
4
  import type { MutableIfc } from "./ifc-encoding.js";
5
5
  import { add, guid, ifcFile, point, real, ref, refs, serialize, str } from "./ifc-encoding.js";
6
- import type { Triangulation } from "./ifc-geometry.js";
7
- import { toIfc, triangulate } from "./ifc-geometry.js";
6
+ import type { Polyline, Triangulation } from "./ifc-geometry.js";
7
+ import { polyline, toIfc, triangulate } from "./ifc-geometry.js";
8
8
 
9
- /** Element classes with the same attributes as IfcBuildingElementProxy (GlobalId, ..., Tag, PredefinedType) */
10
- export type IfcElementEntity = "IfcBuildingElementProxy" | "IfcUnitaryEquipment";
9
+ /** IFC4's PredefinedType values of the element classes, besides NOTDEFINED and USERDEFINED */
10
+ type PredefinedTypes = {
11
+ readonly IfcBuildingElementProxy: "COMPLEX" | "ELEMENT" | "PARTIAL" | "PROVISIONFORVOID" | "PROVISIONFORSPACE";
12
+ readonly IfcBuildingElementPart: "INSULATION" | "PRECASTPANEL";
13
+ readonly IfcUnitaryEquipment: "AIRHANDLER" | "AIRCONDITIONINGUNIT" | "DEHUMIDIFIER" | "SPLITSYSTEM" | "ROOFTOPUNIT";
14
+ readonly IfcFan:
15
+ | "CENTRIFUGALFORWARDCURVED"
16
+ | "CENTRIFUGALRADIAL"
17
+ | "CENTRIFUGALBACKWARDINCLINEDCURVED"
18
+ | "CENTRIFUGALAIRFOIL"
19
+ | "TUBEAXIAL"
20
+ | "VANEAXIAL"
21
+ | "PROPELLORAXIAL";
22
+ readonly IfcFilter: "AIRPARTICLEFILTER" | "COMPRESSEDAIRFILTER" | "ODORFILTER" | "OILFILTER" | "STRAINER" | "WATERFILTER";
23
+ readonly IfcCoil:
24
+ | "DXCOOLINGCOIL"
25
+ | "ELECTRICHEATINGCOIL"
26
+ | "GASHEATINGCOIL"
27
+ | "HYDRONICCOIL"
28
+ | "STEAMHEATINGCOIL"
29
+ | "WATERCOOLINGCOIL"
30
+ | "WATERHEATINGCOIL";
31
+ readonly IfcAirToAirHeatRecovery:
32
+ | "FIXEDPLATECOUNTERFLOWEXCHANGER"
33
+ | "FIXEDPLATECROSSFLOWEXCHANGER"
34
+ | "FIXEDPLATEPARALLELFLOWEXCHANGER"
35
+ | "ROTARYWHEEL"
36
+ | "RUNAROUNDCOILLOOP"
37
+ | "HEATPIPE"
38
+ | "TWINTOWERENTHALPYRECOVERYLOOPS"
39
+ | "THERMOSIPHONSEALEDTUBEHEATEXCHANGERS"
40
+ | "THERMOSIPHONCOILTYPEHEATEXCHANGERS";
41
+ readonly IfcDamper:
42
+ | "BACKDRAFTDAMPER"
43
+ | "BALANCINGDAMPER"
44
+ | "BLASTDAMPER"
45
+ | "CONTROLDAMPER"
46
+ | "FIREDAMPER"
47
+ | "FIRESMOKEDAMPER"
48
+ | "FUMEHOODEXHAUST"
49
+ | "GRAVITYDAMPER"
50
+ | "GRAVITYRELIEFDAMPER"
51
+ | "RELIEFDAMPER"
52
+ | "SMOKEDAMPER";
53
+ readonly IfcDuctSilencer: "FLATOVAL" | "RECTANGULAR" | "ROUND";
54
+ readonly IfcHumidifier:
55
+ | "STEAMINJECTION"
56
+ | "ADIABATICAIRWASHER"
57
+ | "ADIABATICPAN"
58
+ | "ADIABATICWETTEDELEMENT"
59
+ | "ADIABATICATOMIZING"
60
+ | "ADIABATICULTRASONIC"
61
+ | "ADIABATICRIGIDMEDIA"
62
+ | "ADIABATICCOMPRESSEDAIRNOZZLE"
63
+ | "ASSISTEDELECTRIC"
64
+ | "ASSISTEDNATURALGAS"
65
+ | "ASSISTEDPROPANE"
66
+ | "ASSISTEDBUTANE"
67
+ | "ASSISTEDSTEAM";
68
+ };
69
+
70
+ /** An element class, all with the attributes GlobalId, ..., Tag, PredefinedType, and one of its PredefinedTypes, which defaults to NOTDEFINED */
71
+ export type IfcEntity = {
72
+ readonly [E in keyof PredefinedTypes]: { readonly entity: E; readonly predefinedType?: PredefinedTypes[E] | "NOTDEFINED" };
73
+ }[keyof PredefinedTypes];
74
+
75
+ /** IFC4's IfcDistributionSystemEnum, the system a port belongs to, it only connects to ports of the same system */
76
+ export type IfcDistributionSystem =
77
+ | "AIRCONDITIONING"
78
+ | "AUDIOVISUAL"
79
+ | "CHEMICAL"
80
+ | "CHILLEDWATER"
81
+ | "COMMUNICATION"
82
+ | "COMPRESSEDAIR"
83
+ | "CONDENSERWATER"
84
+ | "CONTROL"
85
+ | "CONVEYING"
86
+ | "DATA"
87
+ | "DISPOSAL"
88
+ | "DOMESTICCOLDWATER"
89
+ | "DOMESTICHOTWATER"
90
+ | "DRAINAGE"
91
+ | "EARTHING"
92
+ | "ELECTRICAL"
93
+ | "ELECTROACOUSTIC"
94
+ | "EXHAUST"
95
+ | "FIREPROTECTION"
96
+ | "FUEL"
97
+ | "GAS"
98
+ | "HAZARDOUS"
99
+ | "HEATING"
100
+ | "LIGHTING"
101
+ | "LIGHTNINGPROTECTION"
102
+ | "MUNICIPALSOLIDWASTE"
103
+ | "OIL"
104
+ | "OPERATIONAL"
105
+ | "POWERGENERATION"
106
+ | "RAINWATER"
107
+ | "REFRIGERATION"
108
+ | "SECURITY"
109
+ | "SEWAGE"
110
+ | "SIGNAL"
111
+ | "STORMWATER"
112
+ | "TELEPHONE"
113
+ | "TV"
114
+ | "VACUUM"
115
+ | "VENT"
116
+ | "VENTILATION"
117
+ | "WASTEWATER"
118
+ | "WATERSUPPLY";
11
119
 
12
- export type IfcElementClass = {
13
- readonly entity: IfcElementEntity;
14
- /** The entity's PredefinedType, e.g. "AIRHANDLER" for IfcUnitaryEquipment, defaults to "NOTDEFINED" */
15
- readonly predefinedType?: string;
120
+ export type IfcPortType = {
121
+ readonly predefinedType: "CABLE" | "CABLECARRIER" | "DUCT" | "PIPE";
122
+ readonly systemType?: IfcDistributionSystem;
16
123
  };
17
124
 
18
125
  export type IfcApplicationInfo = {
@@ -31,55 +138,53 @@ export type IfcOptions = {
31
138
  readonly organization?: string;
32
139
  /** Application that produced the scene, e.g. `{ name: "airCalc ACOM", version: "10.1" }`, defaults to Abstract 3D by Divid AB */
33
140
  readonly application?: IfcApplicationInfo;
34
- /**
35
- * Wraps the whole scene in one element, e.g. `{ entity: "IfcUnitaryEquipment", predefinedType: "AIRHANDLER", name: "AHU" }`, otherwise the
36
- * top-level groups are placed directly in the storey
37
- */
38
- readonly root?: IfcElementClass & { readonly name: string };
39
- /**
40
- * Layer of a group's shapes, from the `data` of every group from the root down to this group, this group last; groups without `data` are skipped
41
- * and the root element of `root` comes first. Undefined or "" keeps the layer of the group above
42
- */
43
- readonly layer?: (path: ReadonlyArray<Record<string, string>>) => string | undefined;
141
+ /** The class of a group that becomes an element, e.g. `{ entity: "IfcFan" }`, IfcBuildingElementProxy when undefined */
142
+ readonly entity?: (group: Group) => IfcEntity | undefined;
143
+ /** The type of a relation's ports, e.g. `{ predefinedType: "DUCT", systemType: "VENTILATION" }`, NOTDEFINED when undefined */
144
+ readonly port?: (relation: Relation) => IfcPortType | undefined;
44
145
  };
45
146
 
46
- type NodeMesh = { readonly material: Material; readonly triangulation: Triangulation; readonly layer: string | undefined };
147
+ /** A triangulated mesh or a line, `type` is the RepresentationType of the shape representation it's written in */
148
+ type NodeMesh = { readonly material: Material; readonly layer: string | undefined } & (
149
+ | { readonly type: "Tessellation"; readonly triangulation: Triangulation }
150
+ | { readonly type: "Curve3D"; readonly polyline: Polyline }
151
+ );
47
152
 
48
- /** The `data` of the groups from the root down to a group and the layer its meshes are on */
49
- type Layering = {
50
- readonly layerOf: IfcOptions["layer"];
51
- readonly path: ReadonlyArray<Record<string, string>>;
52
- readonly layer: string | undefined;
53
- };
153
+ const representationTypes = ["Tessellation", "Curve3D"] as const;
54
154
 
55
- /** A group that becomes an element, groups without any triangulated geometry in them are left out */
155
+ /** A group that becomes an element, groups without any triangulated geometry or lines in them are left out */
56
156
  type ElementNode = {
57
157
  readonly group: Group;
58
- readonly elementClass: IfcElementClass;
59
158
  readonly pos: Vec3;
60
159
  readonly rot: Vec3;
61
160
  readonly meshes: ReadonlyArray<NodeMesh>;
62
161
  readonly children: ReadonlyArray<ElementNode>;
63
162
  };
64
163
 
65
- const proxyClass: IfcElementClass = { entity: "IfcBuildingElementProxy" };
164
+ /** A written element with its placement and where it is in the scene, after the scene's center and rotation */
165
+ type IfcElement = { readonly element: number; readonly placement: number; readonly pos: Vec3; readonly rot: Vec3 };
66
166
 
67
167
  /**
68
168
  * `owner` is the encoded GlobalId and OwnerHistory every IfcRoot entity starts with, call it once per entity since the GlobalId must be unique.
69
- * `layers` are the face sets on each layer
169
+ * `layers` are the representation items (face sets and polylines) on each layer, `elements` the written elements by their group's id and `ports` the
170
+ * ports nested in each element
70
171
  */
71
172
  type IfcContext = {
72
173
  readonly body: number;
73
174
  readonly owner: (id?: string) => string;
175
+ readonly entity: IfcOptions["entity"];
74
176
  readonly styles: Map<string, number>;
75
177
  readonly layers: Map<string, Array<number>>;
178
+ readonly elements: Map<string, IfcElement>;
179
+ readonly ports: Map<number, Array<number>>;
76
180
  };
77
181
 
78
182
  /**
79
- * IFC4 (Reference View) with every mesh as an IfcTriangulatedFaceSet, in millimetres. Every top-level group and every group with a `data.id` or
80
- * `data.name` becomes an IfcBuildingElementProxy placed relative to its parent element and aggregated into it. Other groups are only transforms or
81
- * metadata, so their meshes are merged into the parent element. With `layer`, the face sets are assigned to an IfcPresentationLayerAssignment per
82
- * layer, the presentation layer of the Reference View
183
+ * IFC4 (Reference View) with every mesh as an IfcTriangulatedFaceSet and every line as an IfcPolyline without thickness, in millimetres. Every
184
+ * top-level group and every group with an `id` or `name` becomes an element of the class `entity` gives it, an IfcBuildingElementProxy by default,
185
+ * placed relative to its parent element and aggregated into it. Other groups are only transforms, so their meshes are merged into the parent element.
186
+ * The face sets and polylines are on an IfcPresentationLayerAssignment per `layer`. Every relation becomes an IfcDistributionPort nested in each of
187
+ * the elements of its groups, with the flow leaving `from` and entering `to`, and the two ports of a relation with both are connected
83
188
  */
84
189
  export function render(scene: Scene, options: IfcOptions = {}): string {
85
190
  const name = options.name ?? "Abstract 3D";
@@ -93,7 +198,15 @@ export function render(scene: Scene, options: IfcOptions = {}): string {
93
198
  const world = axisPlacement(m, vec3Zero, vec3Zero);
94
199
  const context = add(m, `IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,${ref(world)},$)`);
95
200
  const body = add(m, `IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,${ref(context)},$,.MODEL_VIEW.,$)`);
96
- const c: IfcContext = { body, owner: (id) => `${str(guid(m, id))},${ref(ownerHistory)}`, styles: new Map(), layers: new Map() };
201
+ const c: IfcContext = {
202
+ body,
203
+ owner: (id) => `${str(guid(m, id))},${ref(ownerHistory)}`,
204
+ entity: options.entity,
205
+ styles: new Map(),
206
+ layers: new Map(),
207
+ elements: new Map(),
208
+ ports: new Map(),
209
+ };
97
210
  const units = [
98
211
  add(m, "IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.)"),
99
212
  add(m, "IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.)"),
@@ -114,17 +227,16 @@ export function render(scene: Scene, options: IfcOptions = {}): string {
114
227
 
115
228
  const center = scene.center_deprecated ?? vec3Zero;
116
229
  const sceneRot = scene.rotation_deprecated ?? vec3Zero;
117
- const top: Layering = { layerOf: options.layer, path: [], layer: undefined };
118
- const rootGroup: Group | undefined = options.root && { pos: center, rot: sceneRot, groups: scene.groups, data: { name: options.root.name } };
119
- const nodes = rootGroup
120
- ? [elementNode(rootGroup, center, sceneRot, groupLayering(rootGroup, top), options.root)]
121
- : scene.groups.map((g) =>
122
- elementNode(g, vec3TransRot(g.pos, center, sceneRot), vec3RotCombine(sceneRot, g.rot ?? vec3Zero), groupLayering(g, top))
123
- );
124
- const elements = nodes.flatMap((node) => node ?? []).map((node, i) => element(m, node, `${i + 1}`, storeyPlacement, c));
230
+ const elements = mergeSameId(scene.groups)
231
+ .flatMap((g) => elementNode(g, vec3TransRot(g.pos, center, sceneRot), vec3RotCombine(sceneRot, g.rot ?? vec3Zero), g.layer) ?? [])
232
+ .map((node, i) => element(m, node, `${i + 1}`, { element: storey, placement: storeyPlacement, pos: vec3Zero, rot: vec3Zero }, c));
125
233
  if (elements.length > 0) {
126
234
  add(m, `IFCRELCONTAINEDINSPATIALSTRUCTURE(${c.owner()},$,$,${refs(elements)},${ref(storey)})`);
127
235
  }
236
+ for (const relation of scene.relations ?? []) {
237
+ relationPorts(m, relation, center, sceneRot, options.port, c);
238
+ }
239
+ c.ports.forEach((ports, e) => add(m, `IFCRELNESTS(${c.owner()},$,$,${ref(e)},${refs(ports)})`));
128
240
  c.layers.forEach((items, layer) => add(m, `IFCPRESENTATIONLAYERASSIGNMENT(${str(layer)},$,${refs(items)},$)`));
129
241
 
130
242
  const date = now.toISOString().split(".")[0] ?? "1970-01-01T00:00:00";
@@ -143,96 +255,199 @@ function ownerHistoryEntity(m: MutableIfc, author: string, organization: string,
143
255
  return add(m, `IFCOWNERHISTORY(${ref(user)},${ref(app)},$,.ADDED.,${timestamp},$,$,${timestamp})`);
144
256
  }
145
257
 
146
- /** `layering` is the group's own, see `groupLayering` */
147
- function elementNode(group: Group, pos: Vec3, rot: Vec3, layering: Layering, elementClass = proxyClass): ElementNode | undefined {
148
- const { meshes, children } = elementContent(group, vec3Zero, vec3Zero, layering);
149
- return meshes.length > 0 || children.length > 0 ? { group, elementClass, pos, rot, meshes, children } : undefined;
258
+ /** `layer` is the group's own or the one it inherits */
259
+ function elementNode(group: Group, pos: Vec3, rot: Vec3, layer: string | undefined): ElementNode | undefined {
260
+ const { meshes, children } = elementContent(group, vec3Zero, vec3Zero, layer);
261
+ return meshes.length > 0 || children.length > 0 ? { group, pos, rot, meshes, children } : undefined;
150
262
  }
151
263
 
152
264
  /**
153
- * Meshes of the group and its descendants that aren't elements, relative to the element the group is at pos/rot in, and the descendants with a
154
- * `data.id` or `data.name` as elements
265
+ * Meshes of the group and its descendants that aren't elements, relative to the element the group is at pos/rot in, and the descendants with an `id`
266
+ * or `name` as elements
155
267
  */
156
- function elementContent(group: Group, pos: Vec3, rot: Vec3, layering: Layering): Pick<ElementNode, "meshes" | "children"> {
157
- const meshes = (group.meshes ?? []).flatMap((mesh) => {
158
- const triangulation = triangulate(mesh.geometry, pos, rot);
159
- return triangulation ? [{ material: mesh.material, triangulation, layer: layering.layer }] : [];
268
+ function elementContent(group: Group, pos: Vec3, rot: Vec3, layer: string | undefined): Pick<ElementNode, "meshes" | "children"> {
269
+ const meshes = (group.meshes ?? []).flatMap(({ material, geometry }): ReadonlyArray<NodeMesh> => {
270
+ const triangulation = triangulate(geometry, pos, rot);
271
+ const line = polyline(geometry, pos, rot);
272
+ return triangulation
273
+ ? [{ type: "Tessellation", material, layer, triangulation }]
274
+ : line
275
+ ? [{ type: "Curve3D", material, layer, polyline: line }]
276
+ : [];
160
277
  });
161
- const nested = (group.groups ?? []).map((child) => {
278
+ const nested = mergeSameId(group.groups ?? []).map((child) => {
162
279
  const childPos = vec3TransRot(child.pos, pos, rot);
163
280
  const childRot = vec3RotCombine(rot, child.rot ?? vec3Zero);
164
- const childLayering = groupLayering(child, layering);
165
- return child.data?.["id"] !== undefined || child.data?.["name"] !== undefined
166
- ? { meshes: [], children: [elementNode(child, childPos, childRot, childLayering)].flatMap((node) => node ?? []) }
167
- : elementContent(child, childPos, childRot, childLayering);
281
+ const childLayer = child.layer ?? layer;
282
+ return child.id !== undefined || child.name !== undefined
283
+ ? { meshes: [], children: [elementNode(child, childPos, childRot, childLayer)].flatMap((node) => node ?? []) }
284
+ : elementContent(child, childPos, childRot, childLayer);
168
285
  });
169
- return { meshes: [...meshes, ...nested.flatMap((n) => n.meshes)], children: nested.flatMap((n) => n.children) };
286
+ return {
287
+ meshes: [...meshes, ...nested.flatMap((n) => n.meshes)],
288
+ children: nested.flatMap((n) => n.children),
289
+ };
170
290
  }
171
291
 
172
- /** Groups with `data` are added to the path and get the layer `layerOf` returns for it, if any, otherwise the layer of the parent */
173
- function groupLayering(group: Group, parent: Layering): Layering {
174
- const data = group.data;
175
- if (data === undefined || Object.keys(data).length === 0) {
176
- return parent;
177
- }
178
- const path = [...parent.path, data];
179
- const layer = parent.layerOf?.(path);
180
- return { ...parent, path, layer: layer === undefined || layer === "" ? parent.layer : layer };
292
+ /** Sibling groups with the same `id` as one group at the origin, with the first's name, description, layer and data, and the groups as its parts */
293
+ function mergeSameId(groups: ReadonlyArray<Group>): ReadonlyArray<Group> {
294
+ return groups.flatMap((g, i) => {
295
+ const same = g.id === undefined ? [] : groups.filter((s) => s.id === g.id);
296
+ if (same.length < 2) {
297
+ return [g];
298
+ }
299
+ return groups.findIndex((s) => s.id === g.id) === i
300
+ ? [
301
+ {
302
+ id: g.id,
303
+ name: g.name,
304
+ description: g.description,
305
+ layer: g.layer,
306
+ data: g.data,
307
+ pos: vec3Zero,
308
+ groups: same.map((s) => ({ ...s, id: undefined, name: undefined })),
309
+ },
310
+ ]
311
+ : [];
312
+ });
181
313
  }
182
314
 
183
- /** `path` is the element's position in the tree (e.g. "2.1"), named after it when the group has no `data.name` or `data.id` since IFC requires a name */
184
- function element(m: MutableIfc, node: ElementNode, path: string, parentPlacement: number, c: IfcContext): number {
185
- const placement = add(m, `IFCLOCALPLACEMENT(${ref(parentPlacement)},${ref(axisPlacement(m, node.pos, node.rot))})`);
186
- const items = node.meshes.map((mesh) => faceSet(m, mesh, c));
187
- const shape =
188
- items.length > 0
189
- ? ref(add(m, `IFCPRODUCTDEFINITIONSHAPE($,$,(${ref(add(m, `IFCSHAPEREPRESENTATION(${ref(c.body)},'Body','Tessellation',${refs(items)})`))}))`))
190
- : "$";
191
- const id = node.group.data?.["id"];
192
- const name = node.group.data?.["name"] ?? id ?? `Element ${path}`;
193
- const { entity, predefinedType = "NOTDEFINED" } = node.elementClass;
194
- const ifcElement = add(m, `${entity.toUpperCase()}(${c.owner(id)},${str(name)},$,$,${ref(placement)},${shape},${str(id)},.${predefinedType}.)`);
315
+ /**
316
+ * `path` is the element's position in the tree (e.g. "2.1"), named after it when the group has no `name` or `id` since IFC requires a name. `parent`
317
+ * is the element or storey the element is placed and aggregated in
318
+ */
319
+ function element(m: MutableIfc, node: ElementNode, path: string, parent: IfcElement, c: IfcContext): number {
320
+ const placement = add(m, `IFCLOCALPLACEMENT(${ref(parent.placement)},${ref(axisPlacement(m, node.pos, node.rot))})`);
321
+ const { id, name = id ?? `Element ${path}`, description } = node.group;
322
+ const { entity, predefinedType = "NOTDEFINED" } = c.entity?.(node.group) ?? { entity: "IfcBuildingElementProxy" };
323
+ const ifcElement = add(
324
+ m,
325
+ `${entity.toUpperCase()}(${c.owner(id)},${str(name)},${str(description)},$,${ref(placement)},${representation(m, node.meshes, c)},${str(id)},.${predefinedType}.)`
326
+ );
327
+ const written: IfcElement = {
328
+ element: ifcElement,
329
+ placement,
330
+ pos: vec3TransRot(node.pos, parent.pos, parent.rot),
331
+ rot: vec3RotCombine(parent.rot, node.rot),
332
+ };
333
+ if (id !== undefined) {
334
+ c.elements.set(id, written);
335
+ }
195
336
  aggregate(
196
337
  m,
197
338
  c,
198
339
  ifcElement,
199
- node.children.map((child, i) => element(m, child, `${path}.${i + 1}`, placement, c))
340
+ node.children.map((child, i) => element(m, child, `${path}.${i + 1}`, written, c))
200
341
  );
201
342
  return ifcElement;
202
343
  }
203
344
 
204
- /** One face set per mesh, so meshes on different layers are never in the same item */
205
- function faceSet(m: MutableIfc, { material, triangulation: { points, triangles }, layer }: NodeMesh, c: IfcContext): number {
206
- const pointList = add(m, `IFCCARTESIANPOINTLIST3D((${points.join(",")}))`);
207
- const faces = add(m, `IFCTRIANGULATEDFACESET(${ref(pointList)},$,.F.,(${triangles.map((t) => `(${t.join(",")})`).join(",")}),$)`);
208
- add(m, `IFCSTYLEDITEM(${ref(faces)},(${ref(surfaceStyle(m, material, c))}),$)`);
209
- if (layer !== undefined) {
210
- const items = c.layers.get(layer);
345
+ /**
346
+ * A port nested in the element of each of the relation's groups that is one, placed relative to it, and the two ports connected when both are. The
347
+ * relation's groups are its shape, merged into the first port relative to its placement, groups in them with an `id` or `name` are left out
348
+ */
349
+ function relationPorts(m: MutableIfc, relation: Relation, center: Vec3, sceneRot: Vec3, portType: IfcOptions["port"], c: IfcContext): void {
350
+ const pos = vec3TransRot(relation.pos, center, sceneRot);
351
+ const rot = vec3RotCombine(sceneRot, relation.rot ?? vec3Zero);
352
+ const portInverse = vec3RotInverse(rot);
353
+ const { predefinedType = "NOTDEFINED", systemType } = portType?.(relation) ?? {};
354
+ const ends = [
355
+ { e: relation.from === undefined ? undefined : c.elements.get(relation.from), flow: "SOURCE" },
356
+ { e: relation.to === undefined ? undefined : c.elements.get(relation.to), flow: "SINK" },
357
+ ].flatMap(({ e, flow }) => (e ? [{ e, flow }] : []));
358
+ const ports = ends.map(({ e, flow }, i) => {
359
+ const inverse = vec3RotInverse(e.rot);
360
+ const placement = add(
361
+ m,
362
+ `IFCLOCALPLACEMENT(${ref(e.placement)},${ref(axisPlacement(m, vec3Rot(vec3Sub(pos, e.pos), vec3Zero, inverse), vec3RotCombine(inverse, rot)))})`
363
+ );
364
+ const meshes =
365
+ i === 0
366
+ ? (relation.groups ?? []).flatMap((g) => {
367
+ const groupPos = vec3Rot(vec3Sub(vec3TransRot(g.pos, center, sceneRot), pos), vec3Zero, portInverse);
368
+ const groupRot = vec3RotCombine(portInverse, vec3RotCombine(sceneRot, g.rot ?? vec3Zero));
369
+ return elementContent(g, groupPos, groupRot, g.layer).meshes;
370
+ })
371
+ : [];
372
+ // Each port's id is the relation's with its flow, so they're kept between exports
373
+ const port = add(
374
+ m,
375
+ `IFCDISTRIBUTIONPORT(${c.owner(`${relation.id}_${flow}`)},${str(relation.name)},$,$,${ref(placement)},${representation(m, meshes, c)},.${flow}.,.${predefinedType}.,${systemType === undefined ? "$" : `.${systemType}.`})`
376
+ );
377
+ const nested = c.ports.get(e.element);
378
+ if (nested) {
379
+ nested.push(port);
380
+ } else {
381
+ c.ports.set(e.element, [port]);
382
+ }
383
+ return port;
384
+ });
385
+ const [relating, related] = ports;
386
+ if (relating !== undefined && related !== undefined) {
387
+ add(m, `IFCRELCONNECTSPORTS(${c.owner()},$,$,${ref(relating)},${ref(related)},$)`);
388
+ }
389
+ }
390
+
391
+ /** The encoded IfcProductDefinitionShape with a shape representation per representation type, or $ without meshes */
392
+ function representation(m: MutableIfc, meshes: ReadonlyArray<NodeMesh>, c: IfcContext): string {
393
+ const representations = representationTypes.flatMap((type) => {
394
+ const items = meshes.filter((mesh) => mesh.type === type).map((mesh) => representationItem(m, mesh, c));
395
+ return items.length > 0 ? [add(m, `IFCSHAPEREPRESENTATION(${ref(c.body)},'Body',${str(type)},${refs(items)})`)] : [];
396
+ });
397
+ return representations.length > 0 ? ref(add(m, `IFCPRODUCTDEFINITIONSHAPE($,$,${refs(representations)})`)) : "$";
398
+ }
399
+
400
+ /** One face set or polyline per mesh, so meshes on different layers are never in the same item */
401
+ function representationItem(m: MutableIfc, mesh: NodeMesh, c: IfcContext): number {
402
+ const [item, style] =
403
+ mesh.type === "Tessellation"
404
+ ? [faceSet(m, mesh.triangulation), surfaceStyle(m, mesh.material, c)]
405
+ : [add(m, `IFCPOLYLINE(${refs(mesh.polyline.map((p) => add(m, `IFCCARTESIANPOINT(${p})`)))})`), curveStyle(m, mesh.material, c)];
406
+ add(m, `IFCSTYLEDITEM(${ref(item)},(${ref(style)}),$)`);
407
+ if (mesh.layer !== undefined) {
408
+ const items = c.layers.get(mesh.layer);
211
409
  if (items) {
212
- items.push(faces);
410
+ items.push(item);
213
411
  } else {
214
- c.layers.set(layer, [faces]);
412
+ c.layers.set(mesh.layer, [item]);
215
413
  }
216
414
  }
217
- return faces;
415
+ return item;
416
+ }
417
+
418
+ function faceSet(m: MutableIfc, { points, triangles }: Triangulation): number {
419
+ const pointList = add(m, `IFCCARTESIANPOINTLIST3D((${points.join(",")}))`);
420
+ return add(m, `IFCTRIANGULATEDFACESET(${ref(pointList)},$,.F.,(${triangles.map((t) => `(${t.join(",")})`).join(",")}),$)`);
218
421
  }
219
422
 
220
423
  function surfaceStyle(m: MutableIfc, material: Material, c: IfcContext): number {
221
424
  const opacity = material.opacity ?? 1;
222
- const key = `${material.normal}_${opacity}`;
425
+ return cachedStyle(c, `surface_${material.normal}_${opacity}`, () => {
426
+ // Rendering rather than its supertype shading, since many viewers only read the transparency from rendering
427
+ const rendering = add(m, `IFCSURFACESTYLERENDERING(${ref(colour(m, material))},${real(1 - opacity)},$,$,$,$,$,$,.NOTDEFINED.)`);
428
+ return add(m, `IFCSURFACESTYLE(${str(material.normal)},.BOTH.,(${ref(rendering)}))`);
429
+ });
430
+ }
431
+
432
+ /** Only the colour, IFC has no line width that viewers reliably show */
433
+ const curveStyle = (m: MutableIfc, material: Material, c: IfcContext): number =>
434
+ cachedStyle(c, `curve_${material.normal}`, () => add(m, `IFCCURVESTYLE($,$,$,${ref(colour(m, material))},.T.)`));
435
+
436
+ function cachedStyle(c: IfcContext, key: string, create: () => number): number {
223
437
  const cached = c.styles.get(key);
224
438
  if (cached !== undefined) {
225
439
  return cached;
226
440
  }
227
- const { r, g, b } = new Color(material.normal).getRGB({ r: 0, g: 0, b: 0 }, SRGBColorSpace);
228
- const colour = add(m, `IFCCOLOURRGB($,${real(r)},${real(g)},${real(b)})`);
229
- // Rendering rather than its supertype shading, since many viewers only read the transparency from rendering
230
- const rendering = add(m, `IFCSURFACESTYLERENDERING(${ref(colour)},${real(1 - opacity)},$,$,$,$,$,$,.NOTDEFINED.)`);
231
- const style = add(m, `IFCSURFACESTYLE(${str(material.normal)},.BOTH.,(${ref(rendering)}))`);
441
+ const style = create();
232
442
  c.styles.set(key, style);
233
443
  return style;
234
444
  }
235
445
 
446
+ function colour(m: MutableIfc, material: Material): number {
447
+ const { r, g, b } = new Color(material.normal).getRGB({ r: 0, g: 0, b: 0 }, SRGBColorSpace);
448
+ return add(m, `IFCCOLOURRGB($,${real(r)},${real(g)},${real(b)})`);
449
+ }
450
+
236
451
  /** The group's local axes, converted to IFC's Z-up where three.js' up (Y) becomes the placement's Axis (Z) */
237
452
  function axisPlacement(m: MutableIfc, pos: Vec3, rot: Vec3): number {
238
453
  const location = add(m, `IFCCARTESIANPOINT(${point(toIfc(pos))})`);
@@ -1,7 +1,6 @@
1
1
  export * from "./react.js";
2
2
  export * from "./react-types.js";
3
3
  export type { SceneMouseEvent } from "./react-scene.js";
4
- export type { HotSpotInfo } from "./react-hotspot.js";
5
4
  export type { CameraType, Camera, ControlsHelper } from "./react-camera.js";
6
5
  export type { MaterialState } from "./react-material.js";
7
6
  export { ERROR_IMG_KEY } from "./react-material.js";
@@ -7,7 +7,7 @@ import { ReactMesh } from "./react-mesh.js";
7
7
 
8
8
  type DimensionCallbacks = {
9
9
  readonly selectedIds?: Record<string, boolean> | undefined;
10
- readonly hotSpotsActive?: boolean;
10
+ readonly relationsActive?: boolean;
11
11
  readonly onClickGroup?: (id: string | undefined, rootData: undefined, data: undefined, e: ThreeEvent<MouseEvent>) => void;
12
12
  readonly onHoverGroup?: (id: string | undefined, rootData: undefined, data: undefined, e: ThreeEvent<MouseEvent>) => void;
13
13
  readonly onContextMenuGroup?: (id: string, rootData: undefined, data: undefined, left: number, top: number, e: ThreeEvent<MouseEvent>) => void;
@@ -56,7 +56,7 @@ export function ReactDimension({
56
56
  children,
57
57
  sceneRotation,
58
58
  selectedIds,
59
- hotSpotsActive,
59
+ relationsActive,
60
60
  onClickGroup,
61
61
  onHoverGroup,
62
62
  onContextMenuGroup,
@@ -69,7 +69,7 @@ export function ReactDimension({
69
69
  readonly _sceneCenter: Vec3 | undefined;
70
70
  } & DimensionCallbacks): React.JSX.Element {
71
71
  const [hovered, setHovered] = React.useState<boolean>(false);
72
- const id = hotSpotsActive || !onClickGroup ? undefined : dimension.id;
72
+ const id = relationsActive || !onClickGroup ? undefined : dimension.id;
73
73
  const highlighted = !!id && (hovered || !!selectedIds?.[id]);
74
74
  const dim = dimensionConvertToTypeMesh(highlighted ? { ...dimension, material: selectMat } : dimension, sceneRotation ?? vec3Zero, material);
75
75
  return visible ? (
@@ -6,7 +6,7 @@ const base: ReactGroupProps = {
6
6
  g: group([]),
7
7
  hoveredIdsExternal: undefined,
8
8
  selectedIds: { a: true },
9
- hotSpotsActive: false,
9
+ relationsActive: false,
10
10
  activeComponents: { a: "Error" },
11
11
  id: "a",
12
12
  rootData: undefined,
@@ -39,7 +39,7 @@ describe("groupPropsEqual", () => {
39
39
  it("is not equal when another prop changes identity", () => {
40
40
  expect(groupPropsEqual(base, { ...base, g: group([]) })).toBe(false);
41
41
  expect(groupPropsEqual(base, { ...base, onClickGroup: () => undefined })).toBe(false);
42
- expect(groupPropsEqual(base, { ...base, hotSpotsActive: true })).toBe(false);
42
+ expect(groupPropsEqual(base, { ...base, relationsActive: true })).toBe(false);
43
43
  expect(groupPropsEqual(base, { ...base, id: "b" })).toBe(false);
44
44
  });
45
45
 
@@ -17,7 +17,7 @@ function ReactGroupBase({
17
17
  materialStateImages,
18
18
  hoveredIdsExternal,
19
19
  selectedIds,
20
- hotSpotsActive,
20
+ relationsActive,
21
21
  activeComponents,
22
22
  useAlphaTest,
23
23
  id,
@@ -59,7 +59,7 @@ function ReactGroupBase({
59
59
  const selected = selectedIds?.[id ?? ""];
60
60
 
61
61
  const materialState = activeComponents?.[id ?? ""];
62
- const disabled = hotSpotsActive && materialState !== "Accept";
62
+ const disabled = relationsActive && materialState !== "Accept";
63
63
  return (
64
64
  <group
65
65
  rotation={[g.rot?.x ?? 0, g.rot?.y ?? 0, g.rot?.z ?? 0]}
@@ -101,7 +101,7 @@ function ReactGroupBase({
101
101
  key={createGroupKey ? createGroupKey(g, i, rootData, id ?? "") : i}
102
102
  g={g}
103
103
  selectedIds={selectedIds}
104
- hotSpotsActive={hotSpotsActive}
104
+ relationsActive={relationsActive}
105
105
  activeComponents={activeComponents}
106
106
  materialStateImages={materialStateImages}
107
107
  hoveredIdsExternal={hoveredIdsExternal}
@@ -158,7 +158,8 @@ export type ReactGroupProps = {
158
158
  readonly hoveredIdsExternal: ReadonlyArray<string> | undefined;
159
159
  readonly hoveredParent?: boolean;
160
160
  readonly selectedIds: Record<string, boolean> | undefined;
161
- readonly hotSpotsActive: boolean;
161
+ /** Relations are active, e.g. to pick one to insert a component on, so only the groups whose material state is Accept can be clicked */
162
+ readonly relationsActive: boolean;
162
163
  readonly useAlphaTest?: boolean;
163
164
  readonly activeComponents: Record<string, MaterialState> | undefined;
164
165
  readonly id: string | undefined;
@@ -15,7 +15,7 @@ export function ReactMaterial({
15
15
  disabled,
16
16
  materialState,
17
17
  isText,
18
- isHotSpot,
18
+ isRelation,
19
19
  drawBackOnly,
20
20
  }: {
21
21
  readonly material: Material;
@@ -24,7 +24,7 @@ export function ReactMaterial({
24
24
  readonly disabled?: boolean;
25
25
  readonly materialState?: MaterialState | undefined;
26
26
  readonly isText: boolean;
27
- readonly isHotSpot?: boolean;
27
+ readonly isRelation?: boolean;
28
28
  readonly drawBackOnly?: boolean;
29
29
  }): React.JSX.Element {
30
30
  const mat = !materialState ? material : materialState === "Accept" ? acceptMat : materialState === "Error" ? errorMar : warningMat;
@@ -41,10 +41,10 @@ export function ReactMaterial({
41
41
  />
42
42
  );
43
43
  }
44
- if (isHotSpot) {
44
+ if (isRelation) {
45
45
  return (
46
46
  <meshBasicMaterial
47
- key="mesh_material_hotspot"
47
+ key="mesh_material_relation"
48
48
  color={getColor(selected, hovered, mat, selectMat)}
49
49
  side={drawBackOnly === true ? BackSide : DoubleSide}
50
50
  depthTest={true}