abstract-3d 2.12.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/abstract-3d.d.ts +32 -5
- package/lib/abstract-3d.d.ts.map +1 -1
- package/lib/abstract-3d.js.map +1 -1
- package/lib/renderers/ifc/ifc-encoding.d.ts +5 -2
- package/lib/renderers/ifc/ifc-encoding.d.ts.map +1 -1
- package/lib/renderers/ifc/ifc-encoding.js +30 -7
- package/lib/renderers/ifc/ifc-encoding.js.map +1 -1
- package/lib/renderers/ifc/ifc-geometry.d.ts +4 -0
- package/lib/renderers/ifc/ifc-geometry.d.ts.map +1 -1
- package/lib/renderers/ifc/ifc-geometry.js +8 -0
- package/lib/renderers/ifc/ifc-geometry.js.map +1 -1
- package/lib/renderers/ifc/ifc.d.ts +36 -23
- package/lib/renderers/ifc/ifc.d.ts.map +1 -1
- package/lib/renderers/ifc/ifc.js +164 -67
- package/lib/renderers/ifc/ifc.js.map +1 -1
- package/lib/renderers/react/index.d.ts +0 -1
- package/lib/renderers/react/index.d.ts.map +1 -1
- package/lib/renderers/react/index.js.map +1 -1
- package/lib/renderers/react/react-dimension.d.ts +2 -2
- package/lib/renderers/react/react-dimension.d.ts.map +1 -1
- package/lib/renderers/react/react-dimension.js +2 -2
- package/lib/renderers/react/react-dimension.js.map +1 -1
- package/lib/renderers/react/react-group.d.ts +3 -2
- package/lib/renderers/react/react-group.d.ts.map +1 -1
- package/lib/renderers/react/react-group.js +3 -3
- package/lib/renderers/react/react-group.js.map +1 -1
- package/lib/renderers/react/react-material.d.ts +2 -2
- package/lib/renderers/react/react-material.d.ts.map +1 -1
- package/lib/renderers/react/react-material.js +3 -3
- package/lib/renderers/react/react-material.js.map +1 -1
- package/lib/renderers/react/react-relation.d.ts +10 -0
- package/lib/renderers/react/react-relation.d.ts.map +1 -0
- package/lib/renderers/react/react-relation.js +31 -0
- package/lib/renderers/react/react-relation.js.map +1 -0
- package/lib/renderers/react/react-scene.d.ts +4 -7
- package/lib/renderers/react/react-scene.d.ts.map +1 -1
- package/lib/renderers/react/react-scene.js +4 -4
- package/lib/renderers/react/react-scene.js.map +1 -1
- package/lib/renderers/react/react.d.ts +8 -7
- package/lib/renderers/react/react.d.ts.map +1 -1
- package/lib/renderers/react/react.js +2 -2
- package/lib/renderers/react/react.js.map +1 -1
- package/package.json +2 -2
- package/src/abstract-3d.ts +33 -6
- package/src/renderers/ifc/ifc-encoding.ts +31 -7
- package/src/renderers/ifc/ifc-geometry.ts +12 -0
- package/src/renderers/ifc/ifc.test.ts +285 -101
- package/src/renderers/ifc/ifc.ts +313 -98
- package/src/renderers/react/index.ts +0 -1
- package/src/renderers/react/react-dimension.tsx +3 -3
- package/src/renderers/react/react-group.test.ts +2 -2
- package/src/renderers/react/react-group.tsx +5 -4
- package/src/renderers/react/react-material.tsx +4 -4
- package/src/renderers/react/react-relation.tsx +86 -0
- package/src/renderers/react/react-scene.tsx +9 -21
- package/src/renderers/react/react.tsx +11 -14
- package/lib/renderers/react/react-hotspot.d.ts +0 -27
- package/lib/renderers/react/react-hotspot.d.ts.map +0 -1
- package/lib/renderers/react/react-hotspot.js +0 -37
- package/lib/renderers/react/react-hotspot.js.map +0 -1
- package/src/renderers/react/react-hotspot.tsx +0 -112
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
|
-
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
|
-
/**
|
|
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];
|
|
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
|
|
13
|
-
readonly
|
|
14
|
-
|
|
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
|
-
|
|
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 */
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
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 = {
|
|
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
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
/** `
|
|
147
|
-
function elementNode(group: Group, pos: Vec3, rot: Vec3,
|
|
148
|
-
const { meshes, children } = elementContent(group, vec3Zero, vec3Zero,
|
|
149
|
-
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;
|
|
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
|
|
154
|
-
*
|
|
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,
|
|
157
|
-
const meshes = (group.meshes ?? []).flatMap((
|
|
158
|
-
const triangulation = triangulate(
|
|
159
|
-
|
|
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
|
|
165
|
-
return child.
|
|
166
|
-
? { meshes: [], children: [elementNode(child, childPos, childRot,
|
|
167
|
-
: 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);
|
|
168
285
|
});
|
|
169
|
-
return {
|
|
286
|
+
return {
|
|
287
|
+
meshes: [...meshes, ...nested.flatMap((n) => n.meshes)],
|
|
288
|
+
children: nested.flatMap((n) => n.children),
|
|
289
|
+
};
|
|
170
290
|
}
|
|
171
291
|
|
|
172
|
-
/**
|
|
173
|
-
function
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
/**
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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}`,
|
|
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
|
-
/**
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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(
|
|
410
|
+
items.push(item);
|
|
213
411
|
} else {
|
|
214
|
-
c.layers.set(layer, [
|
|
412
|
+
c.layers.set(mesh.layer, [item]);
|
|
215
413
|
}
|
|
216
414
|
}
|
|
217
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
|
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}
|