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.
- package/lib/abstract-3d.d.ts +32 -5
- package/lib/abstract-3d.d.ts.map +1 -1
- package/lib/abstract-3d.js.map +1 -1
- package/lib/renderers/ifc/ifc-encoding.d.ts +5 -2
- package/lib/renderers/ifc/ifc-encoding.d.ts.map +1 -1
- package/lib/renderers/ifc/ifc-encoding.js +30 -7
- package/lib/renderers/ifc/ifc-encoding.js.map +1 -1
- package/lib/renderers/ifc/ifc.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 +33 -6
- 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
package/src/renderers/ifc/ifc.ts
CHANGED
|
@@ -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
|
-
/**
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|
87
|
-
* aggregated into it. Other groups are only transforms
|
|
88
|
-
* and polylines are
|
|
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 = {
|
|
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
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
/** `
|
|
153
|
-
function elementNode(group: Group, pos: Vec3, rot: Vec3,
|
|
154
|
-
const { meshes, children } = elementContent(group, vec3Zero, vec3Zero,
|
|
155
|
-
return meshes.length > 0 || children.length > 0 ? { group,
|
|
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
|
|
160
|
-
*
|
|
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,
|
|
163
|
-
const meshes = (group.meshes ?? []).flatMap((
|
|
164
|
-
const
|
|
165
|
-
const
|
|
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
|
|
178
|
-
return child.
|
|
179
|
-
? { meshes: [], children: [elementNode(child, childPos, childRot,
|
|
180
|
-
: elementContent(child, childPos, childRot,
|
|
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 {
|
|
286
|
+
return {
|
|
287
|
+
meshes: [...meshes, ...nested.flatMap((n) => n.meshes)],
|
|
288
|
+
children: nested.flatMap((n) => n.children),
|
|
289
|
+
};
|
|
183
290
|
}
|
|
184
291
|
|
|
185
|
-
/**
|
|
186
|
-
function
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
/**
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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}`,
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
44
|
+
if (isRelation) {
|
|
45
45
|
return (
|
|
46
46
|
<meshBasicMaterial
|
|
47
|
-
key="
|
|
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
|
+
}
|