abstract-3d 2.12.2 → 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 (56) 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.d.ts +35 -22
  9. package/lib/renderers/ifc/ifc.d.ts.map +1 -1
  10. package/lib/renderers/ifc/ifc.js +124 -49
  11. package/lib/renderers/ifc/ifc.js.map +1 -1
  12. package/lib/renderers/react/index.d.ts +0 -1
  13. package/lib/renderers/react/index.d.ts.map +1 -1
  14. package/lib/renderers/react/index.js.map +1 -1
  15. package/lib/renderers/react/react-dimension.d.ts +2 -2
  16. package/lib/renderers/react/react-dimension.d.ts.map +1 -1
  17. package/lib/renderers/react/react-dimension.js +2 -2
  18. package/lib/renderers/react/react-dimension.js.map +1 -1
  19. package/lib/renderers/react/react-group.d.ts +3 -2
  20. package/lib/renderers/react/react-group.d.ts.map +1 -1
  21. package/lib/renderers/react/react-group.js +3 -3
  22. package/lib/renderers/react/react-group.js.map +1 -1
  23. package/lib/renderers/react/react-material.d.ts +2 -2
  24. package/lib/renderers/react/react-material.d.ts.map +1 -1
  25. package/lib/renderers/react/react-material.js +3 -3
  26. package/lib/renderers/react/react-material.js.map +1 -1
  27. package/lib/renderers/react/react-relation.d.ts +10 -0
  28. package/lib/renderers/react/react-relation.d.ts.map +1 -0
  29. package/lib/renderers/react/react-relation.js +31 -0
  30. package/lib/renderers/react/react-relation.js.map +1 -0
  31. package/lib/renderers/react/react-scene.d.ts +4 -7
  32. package/lib/renderers/react/react-scene.d.ts.map +1 -1
  33. package/lib/renderers/react/react-scene.js +4 -4
  34. package/lib/renderers/react/react-scene.js.map +1 -1
  35. package/lib/renderers/react/react.d.ts +8 -7
  36. package/lib/renderers/react/react.d.ts.map +1 -1
  37. package/lib/renderers/react/react.js +2 -2
  38. package/lib/renderers/react/react.js.map +1 -1
  39. package/package.json +2 -2
  40. package/src/abstract-3d.ts +33 -6
  41. package/src/renderers/ifc/ifc-encoding.ts +31 -7
  42. package/src/renderers/ifc/ifc.test.ts +233 -102
  43. package/src/renderers/ifc/ifc.ts +263 -80
  44. package/src/renderers/react/index.ts +0 -1
  45. package/src/renderers/react/react-dimension.tsx +3 -3
  46. package/src/renderers/react/react-group.test.ts +2 -2
  47. package/src/renderers/react/react-group.tsx +5 -4
  48. package/src/renderers/react/react-material.tsx +4 -4
  49. package/src/renderers/react/react-relation.tsx +86 -0
  50. package/src/renderers/react/react-scene.tsx +9 -21
  51. package/src/renderers/react/react.tsx +11 -14
  52. package/lib/renderers/react/react-hotspot.d.ts +0 -27
  53. package/lib/renderers/react/react-hotspot.d.ts.map +0 -1
  54. package/lib/renderers/react/react-hotspot.js +0 -37
  55. package/lib/renderers/react/react-hotspot.js.map +0 -1
  56. 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
6
  import type { Polyline, Triangulation } from "./ifc-geometry.js";
7
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];
11
74
 
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;
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";
119
+
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,16 +138,10 @@ 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
147
  /** A triangulated mesh or a line, `type` is the RepresentationType of the shape representation it's written in */
@@ -51,41 +152,39 @@ type NodeMesh = { readonly material: Material; readonly layer: string | undefine
51
152
 
52
153
  const representationTypes = ["Tessellation", "Curve3D"] as const;
53
154
 
54
- /** The `data` of the groups from the root down to a group and the layer its meshes are on */
55
- type Layering = {
56
- readonly layerOf: IfcOptions["layer"];
57
- readonly path: ReadonlyArray<Record<string, string>>;
58
- readonly layer: string | undefined;
59
- };
60
-
61
155
  /** A group that becomes an element, groups without any triangulated geometry or lines in them are left out */
62
156
  type ElementNode = {
63
157
  readonly group: Group;
64
- readonly elementClass: IfcElementClass;
65
158
  readonly pos: Vec3;
66
159
  readonly rot: Vec3;
67
160
  readonly meshes: ReadonlyArray<NodeMesh>;
68
161
  readonly children: ReadonlyArray<ElementNode>;
69
162
  };
70
163
 
71
- 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 };
72
166
 
73
167
  /**
74
168
  * `owner` is the encoded GlobalId and OwnerHistory every IfcRoot entity starts with, call it once per entity since the GlobalId must be unique.
75
- * `layers` are the representation items (face sets and polylines) 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
76
171
  */
77
172
  type IfcContext = {
78
173
  readonly body: number;
79
174
  readonly owner: (id?: string) => string;
175
+ readonly entity: IfcOptions["entity"];
80
176
  readonly styles: Map<string, number>;
81
177
  readonly layers: Map<string, Array<number>>;
178
+ readonly elements: Map<string, IfcElement>;
179
+ readonly ports: Map<number, Array<number>>;
82
180
  };
83
181
 
84
182
  /**
85
183
  * IFC4 (Reference View) with every mesh as an IfcTriangulatedFaceSet and every line as an IfcPolyline without thickness, in millimetres. Every
86
- * top-level group and every group with a `data.id` or `data.name` becomes an IfcBuildingElementProxy placed relative to its parent element and
87
- * aggregated into it. Other groups are only transforms or metadata, so their meshes are merged into the parent element. With `layer`, the face sets
88
- * and polylines are assigned to an IfcPresentationLayerAssignment per layer, the presentation layer of the Reference View
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
89
188
  */
90
189
  export function render(scene: Scene, options: IfcOptions = {}): string {
91
190
  const name = options.name ?? "Abstract 3D";
@@ -99,7 +198,15 @@ export function render(scene: Scene, options: IfcOptions = {}): string {
99
198
  const world = axisPlacement(m, vec3Zero, vec3Zero);
100
199
  const context = add(m, `IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,${ref(world)},$)`);
101
200
  const body = add(m, `IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,${ref(context)},$,.MODEL_VIEW.,$)`);
102
- 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
+ };
103
210
  const units = [
104
211
  add(m, "IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.)"),
105
212
  add(m, "IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.)"),
@@ -120,17 +227,16 @@ export function render(scene: Scene, options: IfcOptions = {}): string {
120
227
 
121
228
  const center = scene.center_deprecated ?? vec3Zero;
122
229
  const sceneRot = scene.rotation_deprecated ?? vec3Zero;
123
- const top: Layering = { layerOf: options.layer, path: [], layer: undefined };
124
- const rootGroup: Group | undefined = options.root && { pos: center, rot: sceneRot, groups: scene.groups, data: { name: options.root.name } };
125
- const nodes = rootGroup
126
- ? [elementNode(rootGroup, center, sceneRot, groupLayering(rootGroup, top), options.root)]
127
- : scene.groups.map((g) =>
128
- elementNode(g, vec3TransRot(g.pos, center, sceneRot), vec3RotCombine(sceneRot, g.rot ?? vec3Zero), groupLayering(g, top))
129
- );
130
- 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));
131
233
  if (elements.length > 0) {
132
234
  add(m, `IFCRELCONTAINEDINSPATIALSTRUCTURE(${c.owner()},$,$,${refs(elements)},${ref(storey)})`);
133
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)})`));
134
240
  c.layers.forEach((items, layer) => add(m, `IFCPRESENTATIONLAYERASSIGNMENT(${str(layer)},$,${refs(items)},$)`));
135
241
 
136
242
  const date = now.toISOString().split(".")[0] ?? "1970-01-01T00:00:00";
@@ -149,71 +255,148 @@ function ownerHistoryEntity(m: MutableIfc, author: string, organization: string,
149
255
  return add(m, `IFCOWNERHISTORY(${ref(user)},${ref(app)},$,.ADDED.,${timestamp},$,$,${timestamp})`);
150
256
  }
151
257
 
152
- /** `layering` is the group's own, see `groupLayering` */
153
- function elementNode(group: Group, pos: Vec3, rot: Vec3, layering: Layering, elementClass = proxyClass): ElementNode | undefined {
154
- const { meshes, children } = elementContent(group, vec3Zero, vec3Zero, layering);
155
- 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;
156
262
  }
157
263
 
158
264
  /**
159
- * 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
160
- * `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
161
267
  */
162
- function elementContent(group: Group, pos: Vec3, rot: Vec3, layering: Layering): Pick<ElementNode, "meshes" | "children"> {
163
- const meshes = (group.meshes ?? []).flatMap((mesh): ReadonlyArray<NodeMesh> => {
164
- const { material } = mesh;
165
- const { layer } = layering;
166
- const triangulation = triangulate(mesh.geometry, pos, rot);
167
- const line = polyline(mesh.geometry, pos, rot);
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);
168
272
  return triangulation
169
273
  ? [{ type: "Tessellation", material, layer, triangulation }]
170
274
  : line
171
275
  ? [{ type: "Curve3D", material, layer, polyline: line }]
172
276
  : [];
173
277
  });
174
- const nested = (group.groups ?? []).map((child) => {
278
+ const nested = mergeSameId(group.groups ?? []).map((child) => {
175
279
  const childPos = vec3TransRot(child.pos, pos, rot);
176
280
  const childRot = vec3RotCombine(rot, child.rot ?? vec3Zero);
177
- const childLayering = groupLayering(child, layering);
178
- return child.data?.["id"] !== undefined || child.data?.["name"] !== undefined
179
- ? { meshes: [], children: [elementNode(child, childPos, childRot, childLayering)].flatMap((node) => node ?? []) }
180
- : 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);
181
285
  });
182
- 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
+ };
183
290
  }
184
291
 
185
- /** Groups with `data` are added to the path and get the layer `layerOf` returns for it, if any, otherwise the layer of the parent */
186
- function groupLayering(group: Group, parent: Layering): Layering {
187
- const data = group.data;
188
- if (data === undefined || Object.keys(data).length === 0) {
189
- return parent;
190
- }
191
- const path = [...parent.path, data];
192
- const layer = parent.layerOf?.(path);
193
- 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
+ });
194
313
  }
195
314
 
196
- /** `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 */
197
- function element(m: MutableIfc, node: ElementNode, path: string, parentPlacement: number, c: IfcContext): number {
198
- const placement = add(m, `IFCLOCALPLACEMENT(${ref(parentPlacement)},${ref(axisPlacement(m, node.pos, node.rot))})`);
199
- const representations = representationTypes.flatMap((type) => {
200
- const items = node.meshes.filter((mesh) => mesh.type === type).map((mesh) => representationItem(m, mesh, c));
201
- return items.length > 0 ? [add(m, `IFCSHAPEREPRESENTATION(${ref(c.body)},'Body',${str(type)},${refs(items)})`)] : [];
202
- });
203
- const shape = representations.length > 0 ? ref(add(m, `IFCPRODUCTDEFINITIONSHAPE($,$,${refs(representations)})`)) : "$";
204
- const id = node.group.data?.["id"];
205
- const name = node.group.data?.["name"] ?? id ?? `Element ${path}`;
206
- const { entity, predefinedType = "NOTDEFINED" } = node.elementClass;
207
- 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
+ }
208
336
  aggregate(
209
337
  m,
210
338
  c,
211
339
  ifcElement,
212
- 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))
213
341
  );
214
342
  return ifcElement;
215
343
  }
216
344
 
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
+
217
400
  /** One face set or polyline per mesh, so meshes on different layers are never in the same item */
218
401
  function representationItem(m: MutableIfc, mesh: NodeMesh, c: IfcContext): number {
219
402
  const [item, style] =
@@ -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}
@@ -0,0 +1,86 @@
1
+ import type { ThreeEvent } from "@react-three/fiber";
2
+ import React from "react";
3
+ import type { Group, Relation } from "../../abstract-3d.js";
4
+ import { vec3Scale } from "../../abstract-3d.js";
5
+ import { ReactMaterial } from "./react-material.js";
6
+ import { ReactMesh } from "./react-mesh.js";
7
+
8
+ const outlineMat = { normal: "rgb(0, 0, 0)", opacity: 1.0 };
9
+
10
+ /** The shape of each active relation, e.g. where a component can be inserted on an air relation, clickable and darker when hovered */
11
+ export const ReactRelations = React.memo(
12
+ ({
13
+ relations,
14
+ activeRelations,
15
+ onClickRelation,
16
+ }: {
17
+ readonly relations: ReadonlyArray<Relation> | undefined;
18
+ readonly activeRelations: ReadonlyArray<string> | undefined;
19
+ readonly onClickRelation?: (relation: Relation, e: ThreeEvent<MouseEvent>) => void;
20
+ }): React.JSX.Element => (
21
+ <>
22
+ {relations
23
+ ?.filter((r) => activeRelations?.includes(r.id))
24
+ .map((r) => (
25
+ <ReactRelation key={r.id} relation={r} onClickRelation={onClickRelation} />
26
+ ))}
27
+ </>
28
+ )
29
+ );
30
+
31
+ function ReactRelation({
32
+ relation,
33
+ onClickRelation,
34
+ }: {
35
+ readonly relation: Relation;
36
+ readonly onClickRelation?: (relation: Relation, e: ThreeEvent<MouseEvent>) => void;
37
+ }): React.JSX.Element {
38
+ const [hovered, setHovered] = React.useState<boolean>(false);
39
+ return (
40
+ <group
41
+ onClick={(e) => {
42
+ if (onClickRelation) {
43
+ e.stopPropagation();
44
+ onClickRelation(relation, e);
45
+ }
46
+ }}
47
+ onPointerOver={(e) => {
48
+ e.stopPropagation();
49
+ document.body.style.cursor = "pointer";
50
+ setHovered(true);
51
+ }}
52
+ onPointerOut={() => {
53
+ document.body.style.cursor = "auto";
54
+ setHovered(false);
55
+ }}
56
+ onContextMenu={(e) => e.stopPropagation()}
57
+ >
58
+ {relation.groups?.map((g, i) => (
59
+ <ReactRelationGroup key={i} g={g} hovered={hovered} />
60
+ ))}
61
+ </group>
62
+ );
63
+ }
64
+
65
+ /** A group of a relation's shape, unlit, with a black outline around its boxes */
66
+ function ReactRelationGroup({ g, hovered }: { readonly g: Group; readonly hovered: boolean }): React.JSX.Element {
67
+ return (
68
+ <group position={[g.pos.x, g.pos.y, g.pos.z]} rotation={[g.rot?.x ?? 0, g.rot?.y ?? 0, g.rot?.z ?? 0]}>
69
+ {g.meshes?.map((m, i) => (
70
+ <React.Fragment key={i}>
71
+ <ReactMesh mesh={m}>
72
+ <ReactMaterial isText={false} isRelation={true} material={m.material} hovered={hovered} />
73
+ </ReactMesh>
74
+ {m.geometry.type === "Box" && (
75
+ <ReactMesh mesh={{ ...m, geometry: { ...m.geometry, size: vec3Scale(m.geometry.size, 1.0125) } }}>
76
+ <ReactMaterial isText={false} isRelation={true} drawBackOnly={true} material={outlineMat} hovered={hovered} />
77
+ </ReactMesh>
78
+ )}
79
+ </React.Fragment>
80
+ ))}
81
+ {g.groups?.map((c, i) => (
82
+ <ReactRelationGroup key={i} g={c} hovered={hovered} />
83
+ ))}
84
+ </group>
85
+ );
86
+ }